星期一, 十月 05, 2026

196 115 浏览器 vnc 网页版 | 115 Linux 原生客户端网页版

======浏览器版======

浏览器版这类 docker 烂大街, 有很多个. 随便捡一个来用. 注意下方中文修改成你自己的数据. 

强制解析了几个域名, 因为都是垃圾域名, 没有用, 去掉也行.  

docker run -d \
--name 115 \
--security-opt seccomp:unconfined \
-e PUID=0 \
-e PGID=0 \
--user 0:0 \
--add-host pc.115.com:255.255.255.255 \
--add-host xue.115.com:255.255.255.255 \
--add-host union.115.com:255.255.255.255 \
--add-host appversion.115.com:255.255.255.255 \
--add-host act.115.com:255.255.255.255 \
--add-host job.115.com:255.255.255.255 \
-e PASSWORD=你的网页版VNC登录密码 \
-e DISPLAY_WIDTH=1920 \
-e DISPLAY_HEIGHT=1080 \
-e COOKIE_CID="" \
-e COOKIE_SEID="" \
-e COOKIE_UID="" \
-e COOKIE_KID="" \
-e TZ=Asia/Shanghai \
-e LC_ALL=zh_CN.UTF-8 \
-v /mnt/hdd/files左边是你的实机目录用于存放下载数据_右边不要动:/opt/Downloads \
-v /root/115左边是115浏览器的userdata数据目录_右边不要动:/etc/115 \
-p 127.0.0.1:1150:1150 \
--shm-size 1gb \
--restart unless-stopped \
ghcr.io/dream10201/115docker:latest

直接跑起来就行. 随后网页访问 域名或IP:1150 或者使用反向代理工具代理网页. 如 Traefik 使用目录的方式反向代理: 

http:
 routers:
   115-router:
     entryPoints:
       - "http"
       - "https"
     rule: "Host(`你的.域名`) && PathPrefix(`/你的目录/`)"
     service: "115-service"
     middlewares:
       - "strip-115-prefix"
 middlewares:
   strip-115-prefix:
     stripPrefix:
       prefixes:
         - "/你的目录/"

 services:
   115-service:
     loadBalancer:
       servers:
         - url: "http://127.0.0.1:1150"

 

======客户端版======

先说限制条件, 我的小鸡都是 x86_64 架构的, 没有图形界面, 所以才打算拿 docker 封装 Linux 原生客户端, 通过网页访问方便管理. 纯下载文件用, 如果你是流媒体需求, 请忽略本文. 

所以如果你是 arm arm64 mips 之类的, 那我没有现成的东西给你, 首先 115 官方截止 20261005 就没有开发 arm 架构的原生客户端, 我不确定 arm 架构的 Linux 系统上能不能跑 x86_64 架构客户端打包而成的容器. 你可以试试. 将我下面的三个文件喂给 ai 让它辅助修改. 

 

还有一个可选项, 就是 docker 开启 IPv6, 我不知道 115 平台普及 IPv6 了没有, 我习惯是开起来. 这个要修改 /etc/docker/daemon.json, 有问题善用搜索和 ai, 不展开讲了. 

 

115 原生的 Linux 客户端, 这个就要好好讲讲了, 客户端是 115 在 2026 年新开发的基于 .NET 的 Avalonia UI 做的新版客户端, 长得有点像 Electron 但实际不是, 点击按钮的手感有一点偏 qt, 但没 qt 那种卡顿感. 

这个 docker 在网上没有, 我自己拿两个 ai 研究出来的, 所以以下代码由 ChatGPT 和 DeepSeek 生成. 我只负责研究报错, 分析原因. 

网上没有就要实机编译, 编译预留大概 6 GB 空间, 编译出来的 image 也就是镜像, 以当前的 115 原生客户端版本来看, 做好的镜像是 2.93 GB, 这只是 docker image 看到的大小, 实际还要跑起来, 再加上编译的那些数据, 如果空间不够, 你可以在自己电脑上用 VMware 或者 VirtualBox 建一个虚拟机, 编译好了, 打包上传到服务器导入. 简单说说导出导入方法: 

docker 的镜像可以本机导出, 再导入到其它机器上运行. 只带 image 镜像, 不带 container 容器运行数据. 

docker save 115:latest -o 115.tar
docker save 115:latest | gzip -9 > 115.tar.gz
docker save 115:latest | xz -9 -T0 > 115.tar.xz

三选一, 压缩率从低到高, 所以文件也从大到小. 

导入: 

docker load -i 115.tar
docker load -i 115.tar.gz
docker load -i 115.tar.xz

如果你要带着容器数据迁移 dockerd 和 containerd... 那就扯得更远了, 也很复杂, 单独讲解, 参见 [193 docker 迁移目录和数据 包括 docker 和 containerd | 解决迁移之后依旧存储在原目录的问题]. 


好了, 准备阶段的知识就先啰嗦这么多, 上代码. 这代码是 ai 生成, 可能有屎山, 自行甄别. 我在 Windows 10 的电脑上, 用 VirtualBox 跑 Ubuntu 24.04.1 LTS 是能把 docker 完整编译完, 也能导出成压缩包. 

下面这三个文件, 放在同一个目录中. 第一个文件 Dockerfile: 

FROM jlesage/baseimage-gui:ubuntu-24.04-v4.13.2
LABEL maintainer="lampoppa <lampooppa@gmail.com>"
ENV APP_NAME="115pc"
ENV APP_VERSION="37.3.1"
ENV USER_ID=0
ENV GROUP_ID=0
ENV ENABLE_CJK_FONT=1
ENV DISPLAY_WIDTH="1920"
ENV DISPLAY_HEIGHT="1080"
# 1. 修复 /etc/passwd、/etc/group,并补回缺失目录
RUN rm -f /etc/passwd /etc/group /etc/shadow /etc/gshadow && \
   cp /usr/share/base-passwd/passwd.master /etc/passwd && \
   cp /usr/share/base-passwd/group.master /etc/group && \
   touch /etc/shadow /etc/gshadow && \
   chmod 640 /etc/shadow /etc/gshadow && \
   chmod 644 /etc/passwd /etc/group && \
   rm -f /var/log /var/tmp && \
   mkdir -p /var/log /var/tmp /var/lib/dbus /var/lib/misc && \
   chmod 1777 /var/tmp
# 2. 移除 dbus 状态覆盖(可选)
RUN dpkg-statoverride --remove /var/run/dbus 2>/dev/null || true
# 3. 安装软件包(用 C locale 避免警告)
RUN LANG=C LC_ALL=C apt-get update && \
   LANG=C LC_ALL=C apt-get install -y \
       curl \
       wget \
       gdebi-core \
       locales \
       fonts-wqy-zenhei \
       fonts-wqy-microhei \
       fonts-noto-cjk \
       fontconfig \
       dbus \
       dbus-x11 \
       desktop-file-utils \
       libwebkit2gtk-4.1-0 \
       libsecret-1-0 \
       libnotify4 \
       libgtk-3-0 \
       libva2 \
       libva-drm2 \
       xdg-utils && \
   rm -rf /var/lib/apt/lists/*
# 4. 生成中文 locale
RUN sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen && \
   locale-gen
ENV LANG=zh_CN.UTF-8 \
   LANGUAGE=zh_CN:zh \
   LC_ALL=zh_CN.UTF-8
# 5. 刷新字体缓存
RUN fc-cache -fv
# 6. 下载并安装 115Life(apt 无法自动解决依赖)
RUN curl -L https://down.115.com/client/115pc/lin/115Life_${APP_VERSION}.deb -o /tmp/115Life_${APP_VERSION}.deb && \
   apt-get install -y /tmp/115Life_${APP_VERSION}.deb && \
   rm -f /tmp/115Life_${APP_VERSION}.deb
# 7. D-Bus 初始化脚本
COPY init-dbus.sh /etc/cont-init.d/99-init-dbus.sh
RUN chmod +x /etc/cont-init.d/99-init-dbus.sh
COPY startapp.sh /startapp.sh
RUN chmod +x /startapp.sh


第二个文件 init-dbus.sh: 

#!/bin/sh
# 初始化 D-Bus system daemon
mkdir -p /var/run/dbus
dbus-daemon --system --fork 2>/dev/null || true
echo "D-Bus initialization completed"


第三个文件 startapp.sh: 

#!/bin/sh
if [ ! -d '/Downloads' ]; then
   mkdir -p '/Downloads'
   chmod a+w '/Downloads'
else
   echo "Folder /Downloads already exists!"
fi
# 如果 session D-Bus 没启动,则启动它
if ! pgrep -x "dbus-daemon" > /dev/null 2>&1; then
   dbus-daemon --session --fork 2>/dev/null || true
fi
# 澳大利亚伯斯时间, 个人喜好
export TZ=Australia/Perth
# 中文环境
export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8
export LANGUAGE=zh_CN:zh
export LC_CTYPE=zh_CN.UTF-8
export LC_NUMERIC=zh_CN.UTF-8
export LC_TIME=en_US.UTF-8
export LC_COLLATE=zh_CN.UTF-8
export LC_MONETARY=zh_CN.UTF-8
export LC_MESSAGES=zh_CN.UTF-8
export LC_PAPER=zh_CN.UTF-8
export LC_NAME=zh_CN.UTF-8
export LC_ADDRESS=zh_CN.UTF-8
export LC_TELEPHONE=zh_CN.UTF-8
export LC_MEASUREMENT=zh_CN.UTF-8
export LC_IDENTIFICATION=zh_CN.UTF-8
fc-cache -fv 2>/dev/null || true
exec /usr/bin/115life

准备好三个文件后, 给全部权限, 然后开始编译: 

docker build -t 115 .

上面末尾有个小数点请注意. 

编译好了就可以运行了: 

docker run -d \
   --name 115 \
   -p 127.0.0.1:1150:5800 \
   -v /root/115目录里似乎没有登录数据, 在哪我不知道, 反正重启容器不丢:/config \
   -v /mnt/hdd/files你的存储盘目录:/Downloads \
   --restart unless-stopped \
   115:latest

网页点开即是图形界面, 没有密码保护, 如果放在公网上, 必须要通过 Traefik 或者其它反向代理软件如 Nginx caddy, 加一个 basicauth 或者其它高级认证, 我以 Traefik 的目录反代添加 basicauth 为例, config/115.yaml 动态配置文件: 

http:
 routers:
   115-router:
     entryPoints:
       - "http"
       - "https"
     rule: "Host(`你的.域名`) && PathPrefix(`/115的网页版文件夹/`)"
     service: "115-service"
     middlewares:
       - "strip-115-prefix"
       - "auth-115"
 middlewares:
   strip-115-prefix:
     stripPrefix:
       prefixes:
         - "/115的网页版文件夹/"
   auth-115:
     basicAuth:
       users: #https://rivers.chaitin.cn/tools/htpasswd 可以使用 bcrypt 算法在线加密
         - "root:$2a$10$加密的密钥, root 是用户名"
 services:
   115-service:
     loadBalancer:
       servers:
         - url: "http://127.0.0.1:1150"

下载文件前, 要更改软件内下载目录. 默认在 /config/Downloads, 改成 /Downloads

如果加了 basicauth, 首次访问为了方便可以直接请求: 

https://用户名:密码@你的.域名/115目录/

会看到报错, 

Unexpected error encountered:
Could not load web data: TypeError: Window.fetch: ./webdata.json is an url with embedded credentials.
fetchWebData/<@*****/app/ui.js

在浏览器地址栏回车一次, 就能正常访问了. 

运行整个流程包括上传下载都没有问题之后, 自己搜清理 docker build 缓存教程. 不展开了.  

为啥不用浏览器 docker? 上面那个浏览器 docker 下载大文件夹总会提示 磁盘空间不足, 上级目录给足权限了也没搞定. 另外 .NET framework Avalonia UI 这套东西虽然占用大, 但性能好一些, 我那石头盘可以到很不错的写入速度, 比浏览器好太多. 所以, 自己就参考 https://github.com/badmintoncat/115-client-docker (这个项目也是浏览器, 看得像原生客户端一样... 你看 Dockerfile 里面下载 deb 的地址带 br 就知道了) 重新做了一个 Linux 原生客户端. 

为啥不用 OpenList 这类存储聚合? 我要下 1TB 的数据, 防止 115 以各种理由封号 (没有过但是我担心有). 

附图一张😁 这新版界面的火狐也是丑得不行... 网页左侧是可以隐藏的悬浮窗, 和其它 vnc 一样, 带一个画质调节和剪贴板的基础功能. 


没有任务栏多程序啥的. 需要你可以自己加, 我这是一次性 docker, 只下载 1TB 数据, 你想长期使用的话, 可以把上面三个文件喂给 ai 让它研究研究. 

星期日, 四月 26, 2026

195 Telegram 的极简网页版 mpgram-web

20260528 更新: 这东西半个月的时间在我的 php-cli docker 里创建了两千三百多个 sh 僵尸进程... 扔了. 

 

2026 年 4 月, 机场经历了巨大的动荡, 大量中转机场的国内中转出口被清退、通报, 这里我吐槽那些还在用 SS、Trojan 协议的 SB 机场, 我认为这些能被识别的过时协议和国内出口清退通报有很大关联, 而且从中转迁移到直连还在用这些 SB 协议的 SB 机场, 后续会继续祸害直连. 

为了防止可能到来的 2026 年 5 月或者 6 月直连断网, 以及之后可能彻底和 Telegram 失去联系, 我做了个最终应急方案, esim 海外流量套餐 + 省流量的极简 Telegram 网页版 mpgram-web, 手机和电脑都可以使用. 

提一嘴 esim, 卡是骗多多上卖的二十几块的白卡, LPA(写入工具)走大部分安卓自带的 OMAPI 协议, 安卓上用闭源 Nekoko2(可刷 magisk 给 apk 提权成系统应用, 以支持 telephony api, 此软件功能多些) 或者开源 EasyEUICC (https://easyeuicc.org https://gitea.angry.im/PeterCxy/OpenEUICC/releases 这两个是同一个东西), 这两 LPA 只写卡, 二十几块的白卡自带 stk (SIM 卡工具箱) 切卡删卡功能. 流量买 eskimo 的, 走 aff 新注册有 500MB Global, 随后购买 10GB China 折扣成半价 $13.5, 再购买一次折扣成 $1 的 1GB China, 这是两个券的最优用法, 写卡连上信号后有效期两年. 如果你没见到券或者不是这个价格, 不要购买, 买最低的保卡就行. 

一些入门教程: 

https://www.leavesongs.com/THINK/using-euicc-card-to-support-esim.html

https://euicc-manual.osmocom.org/

https://iecho.cc/posts/convert-esim-to-physical-sim

https://www.muooy.com/417.html


下面是正文.  

项目地址: https://github.com/shinovon/mpgram-web

大概就是 3GQQ 那样, 这是纯网页版, 作者还有一个 j2me 的客户端, 你可以打开 api 功能, 和客户端 (https://github.com/shinovon/mpgram-client)一起使用, 也可以单使用网页版. 


 
图片来自 http://nokiahacking.pl/mpgram-web-telegram-w-kazdej-przegladarce-vt35348.htm

没错, 你没看错, 基于 java 的 j2me 的客户端, 也就是二十年前功能机使用的 jar, 截至发文的 20260426, 依旧在维护更新. 此作者似乎是 j2me 的忠实粉丝, 还接手了早已遁入虚空的 kemulator 项目继续维护 (https://github.com/shinovon/KEmulator/releases)...

本文讲的是自建 mpgram-web, 而不是使用公共服务器, 一是之前有一定数量的封号(All public instances are closed because many people getting their accounts banned by telegram flood prevention system), 二是自己的账号用别人的服务器也不安全. 

这套东西的文档做得其实很烂, 踩了很多坑, 下面是自己倒腾出来的经验. 

1, 安装 docker 并查看 docker compose 的版本号: 

curl -fsSL https://get.docker.com | bash -s docker
docker --version
docker compose version

我本来想自己编译个命令行的版本, 但折腾了几个小时都失败了, 于是就用作者带的 docker-compose.yml 了. 

2, 安装好 docker 之后, 按照教程所说, 编辑 api_values.php 和 config.php 两个文件, 如果你没有申请过 Telegram apps (api_id 和 api_hash), 那么需要打开 https://my.telegram.org/apps 来申请. 将其填入 api_values.php, 下面讲解 config.php 主要的配置及含义. 

define('sessionspath', 'session_dir/'); //和 locale img 这些目录平级, 设置一个保存登录数据的路径
define('PNG_STICKERS', true); //透明贴纸, 用不上
define('FORCE_HTTPS', false); // http 重定向到 https, 有 traefik 等反向代理, 用不上, 127.0.0.1 反代就行
define('CHROME_HTTPS', false); //
当识别到 chrome user-agent 请求头的时候, http 重定向到 https, 用不上, 同上, 走反代
define('CONVERT_VOICE_MESSAGES', false);
define('VOICE_TMP_DIR', sys_get_temp_dir().'/mp/');
define('FFMPEG_DIR', '');  //这三项和语音有关, 需要下载 ffmpeg 可执行文件, 平时我根本用不到语音, 所以跳过了
define('LOGIN_CAPTCHA', false); //登录时输入验证码, 在执行网页登录的时候, 多一步输入验证码的操作, 但我实际测试发现设置为 0 验证码照样出现... 
define('INSTANCE_PASSWORD', 'admin_password'); //登录时输入密码, 如果你的自建 mpgram-web 暴露在公网, 设置这个以防被他人滥用
define('FILE_REWRITE', false); //获取图片等文件时使用目录, 用不上, 我看默认都是 file.php?abc=def 这样请求图片或文件
define('CONVERT_TGS_STICKERS', false);
define('LOTTIE_DIR', '');
define('TGS_TMP_DIR', sys_get_temp_dir().'/mp/');
define('LOTTIE_TO_GIF', true); //和贴纸有关, 用不上, 没开. 这东西需要引入其它 GitHub 项目, 懒得折腾了
define('ENABLE_API', false);
define('ENABLE_LOGIN_API', false); //没折腾这个, 应该是开放给 j2me 客户端 mpgram-client 来使用的
define('DOWNLOAD_SIZE_LIMIT', 1024 * 1024 * 1024);
define('IMAGE_SIZE_LIMIT', 30 * 1024 * 1024);
define('UPLOAD_SIZE_LIMIT', 20 * 1024 * 1024); //限制 file.php 最大获取的大小, 保持默认了
define('ENABLE_PEER_DB', false); //设置为 false 可 
sessionspath 中的登录数据文件大小, 但无法显示好友名称, 会显示 deleted account, 如果你的号聊天特别多, 建议关闭成 false 状态, 不然 sessionspath 底下的 safe.php 文件可以来到 170MB+ 的大小, 这会造成 vendor 也就是 MadelineProto 会抛出 Amp\TimeoutException: Timeout while waiting for help.getConfig 超时报错的异常

3, 改好这两个文件之后, 开始使用 git clone 命令下载源码: 

git clone https://github.com/shinovon/mpgram-web.git

4, 随后进入项目的 docker 文件夹并执行一键编译:  

cd docker

docker compose up --build -d

5, 编译完成之后, 就可以把这几个 container 和 image 都删了, 我用的 lazydocker 在图形界面终端操作的, 如果需要命令删除自己查一下删除 container 和 image 的命令. 

git clone 的目录不要删, 留下, 因为刚才它在 compose 编译的时候引入了另一个项目 MadelineProto 放在了 vendor 目录下, 并且对这个引入的项目打了文件补丁, 所以整个克隆的目录不要动. 

6, php-cli 的 docker, 和 traefik (反向代理)

php-cli 有一些依赖要启用, 所以我重做了 dockerfile, 整合了 mpgram-web 所需的一些东西, 重新编译出一个 image 了. 具体操作如下: 

新建一个 dockerfile 空文件, 里面写入: 

FROM php:8.5.5-cli

# =========================
# 系统依赖
# =========================
RUN apt-get update && apt-get install -y --no-install-recommends \
        libyaml-dev \
        libpng-dev \
        libjpeg-dev \
        libfreetype6-dev \
        libgmp-dev \
        libffi-dev \
        libxml2-dev \
        libonig-dev \
        curl \
        procps \
        vim \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# =========================
# GD 扩展配置
# =========================
RUN docker-php-ext-configure gd \
        --with-freetype \
        --with-jpeg

# =========================
# PHP 内置扩展
# =========================
RUN docker-php-ext-install -j$(nproc) \
        gd \
        mbstring \
        ffi \
        gmp \
        xml \
        fileinfo

# =========================
# PECL yaml 扩展, 这段可以不要, mpgram-web 用不到
# =========================
RUN pecl install yaml \
    && docker-php-ext-enable yaml

# =========================
# Browscap, 这是 mpgram-web 里的功能
# =========================
RUN curl -fsSL \
    http://browscap.org/stream?q=Lite_PHP_BrowsCapINI \
    -o /usr/local/etc/browscap.ini

# =========================
# 自定义 php.ini, 
mpgram-web 自带的 php.ini, 我以 conf.d 的形式加载了
# =========================
RUN curl -fsSL \
    https://raw.githubusercontent.com/shinovon/mpgram-web/refs/heads/master/docker/mpgram_web/php.ini \
    -o /usr/local/etc/php/conf.d/90-php.ini 

7, 随后使用命令编译成 image, 注意末尾有个小数点:

docker build --no-cache -t php-cli:8.5.5 .

再清理编译缓存: 

docker builder prune

最后运行, 注意指定正确的路径, 我是 /root/www: 

docker run -d -p 127.0.0.1:9000:80 --name php-cli --restart=unless-stopped -v /root/www:/var/www/html php-cli:8.5.5 php -S 0.0.0.0:80 -t /var/www/html

8, traefik 的配置, 之前自己写的 index.yaml 配置, 放在 config 文件夹下面自动加载的那种, 访问 mpgram-web 的时候总是卡住很长时间, 我判断是 http 请求被 Traefik 强行重定向到 https 了(127.0.0.1 内部), 所以我就让 ChatGPT 重写了一版, 以下是 index.yaml: 

http:
  routers:

    # =========================
    # ① HTTP → HTTPS 重定向
    # =========================
    us-http:
      entryPoints:
        - "http"
      rule: "Host(`你的网站域名, 如abc.com`)"
      middlewares:
        - redirect-https
      service: noop

    # =========================
    # ② HTTPS 正式入口
    # =========================
    us-https:
      entryPoints:
        - "https"
      rule: "Host(`你的网站域名, 如abc.com`) && PathPrefix(`/`)"
      tls:
        certResolver: "letsencrypt"
      service: us
      middlewares:
        - adrules-cors

  services:
    us:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:9000" #这里是 php-cli 开放的 9000 端口

    # 必须存在的“虚拟 service”(用于 redirect)
    noop:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1"

  middlewares:

    # =========================
    # HTTPS 强制跳转
    # =========================
    redirect-https:
      redirectScheme:
        scheme: https
        permanent: true

    # =========================
    # CORS
    # =========================
    adrules-cors:
      headers:
        accessControlAllowOriginList:
          - "*"
        accessControlAllowHeaders:
          - "*"

以上是 Traefik 的 php-cli 反代配置, 如果你用 nginx 或者 apache 都可以参考. 

至于你的 mpgram-web, 建议放在一个含有随机字符的路径, 如 abc_4138u90i9tqwkrioew3894A, 这样就能防止爬虫遍历目录, 这种东西漏洞应该不少. 暴露在公网的服务器可以访问: https://abc.com/abc_4138u90i9tqwkrioew3894A/ 


这套源码跑起来是没问题, 号码登录可能不行, 使用二维码登录. 收发消息就靠刷新网页, 完完全全就是当年 3GQQ 的体验, 而且我在火狐上都没有自动刷新这一说. 

不过有一个很大的问题, 它是和进程交互的, 当一段时间没有访问后, 再去唤醒进程就会超时一次, 等待时间非常长, 可能达三~五分钟之久, 所以我想到了一个保活方法, 就是定时访问自身来保活, 也就是在 crontab -e 中添加任务. 

首先你要保证浏览器正常登录过一次, 后端才能在 sessionspath 中保存数据, 可以看到文件夹类似: 

qr_942cbc3ef5..........e5bfec.madeline

把 .madeline 之前的文件夹名复制出来. qr_942cbc3ef5..........e5bfec

构造请求. 在 crontab -e 中添加: 

*/5 * * * * /usr/bin/curl 'https://你的域名/abc_4138u90i9tqwkrioew3894A你的mpgram-web路径/chats.php?lang=en&timeoff=-18000&theme=6主题ID自己试去,或者在网页设置好去cookies里找&updint=10&user=qr_942cbc3ef5..........e5bfec你刚才复制的文件夹名' -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Connection: keep-alive'
3 5 * * * /usr/bin/docker restart php-cli

#20260522 更新, 这个东西有屎山, 半个月的时间会积压两千三百多个 sh 僵尸进程没有结束, 所以加上了每天 php-cli 重启.  

如果有多个需要保活, 就添加多行. 如果需要直接编辑 crontab 文件, 去 /var/spool/cron/crontabs 编辑文件.  


至此, 一整套 mpgram-web 就搭好了, 你可以直接使用上面 crontab -e 里的链接访问(前提是必须在此浏览器登录过, 后端存有 session, 浏览器存有 cookies 才行), 哈哈, 是不是有 3GQQ sid 的感觉了? 也不用怕长时间不用下次唤醒超时报错了. 用起来其实还是挺麻烦的, 但这屎山折腾好了以后就会非常舒服, 你甚至还可以使用作者写的 j2me 客户端 mpgram-client 在安卓 j2me loader 模拟器, 或电脑的 kemulator 模拟器中使用. 

 

星期五, 四月 17, 2026

194 bitwarden 转 vaultwarden 自建后, creationDate 和 revisionDate 不能写入数据库的 created_at 和 updated_at 问题处理

bitwarden 简称 bw, vaultwarden 简称 vw

思路:  

1, 先将 bw 的数据导出成 bw.json. 

2, 将 bw.json 导入你自建的 vw. 

3,  将你自建的 vw 也导出一个 vw.json

4, 把 vaultwarden 的 docker / 主程序 停止, 启动, 再停止, 以保证 sqlite wal 全部写入数据库 db.sqlite3 文件.  

5, 可以把 db.sqlite3 文件下载到本地, 用 navicat 或者 dbeaver 等工具打开, 这时你看到 vw 的 sqlite 数据中,  created_at updated_at 全部变成了清一色导入的时间, 奇葩行为..

6, 使用 ai 生成代码, 寻找两个 json 中 id 的对应关系, id 是不同的, 如何一一对应呢?
我使用的是 name + username 的组合对应关系. 除此之外, 当没有 username 的时候(银行卡类型), 用 name + notes 来组合, 以防止重复的情况. 

也就是: 

两个不同的数据库 json 导出文件内容相同, id 不同, bw.json 的两个时间 creationDate 和 revisionDate 是对的, vw.json 中的两个时间是错的. 通过 name+username (没有 username 就用 name+notes) 来找到两个 json 的 "id" 对应关系. 

随后根据这个 id 的对应关系, 将 bw.json 中的 creationDate 和 revisionDate 读取出来, 写到数据库对应 id 中的 created_at 和 updated_at 中去. 

7, 我使用 ChatGPT 生成的代码如下: 

<?php

$bwFile = 'bw.json';
$vwFile = 'vw.json';
$dbFile = 'db.sqlite3';

$bwJson = json_decode(file_get_contents($bwFile), true);
$vwJson = json_decode(file_get_contents($vwFile), true);

if (!$bwJson || !$vwJson) {
    die("JSON 解析失败\n");
}

$bwItems = $bwJson['items'] ?? [];
$vwItems = $vwJson['items'] ?? [];

/**
 * key = name + username/notes
 */
function buildKey($item) {
    $name = $item['name'] ?? '';

    $username = $item['login']['username'] ?? null;
    $notes = $item['notes'] ?? '';

    $second = $username;
    if ($second === null || $second === '') {
        $second = $notes;
    }

    return $name . '|' . $second;
}

/**
 * SQLite connect
 */
$pdo = new PDO("sqlite:" . $dbFile);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

/**
 * =======================
 * 1. BW index
 * =======================
 */
$bwMap = [];

foreach ($bwItems as $item) {
    if (!isset($item['name'])) continue;

    $key = buildKey($item);

    $bwMap[$key] = [
        'creationDate' => $item['creationDate'] ?? null,
        'revisionDate' => $item['revisionDate'] ?? null,
    ];
}

/**
 * =======================
 * 2. Update DB
 * =======================
 */
$updateStmt = $pdo->prepare("
    UPDATE ciphers
    SET created_at = :created_at,
        updated_at = :updated_at
    WHERE uuid = :uuid
");

$updated = 0;
$skipped = 0;
$skippedList = [];

foreach ($vwItems as $item) {

    if (!isset($item['name'], $item['id'])) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'missing name or id',
            'item' => $item
        ];
        continue;
    }

    $key = buildKey($item);

    if (!isset($bwMap[$key])) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'bw not found',
            'id' => $item['id'],
            'key' => $key
        ];
        continue;
    }

    $bw = $bwMap[$key];

    if (empty($bw['creationDate']) || empty($bw['revisionDate'])) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'missing bw dates',
            'id' => $item['id'],
            'key' => $key,
            'bw' => $bw
        ];
        continue;
    }

    try {
        $updateStmt->execute([
            ':created_at' => $bw['creationDate'],
            ':updated_at' => $bw['revisionDate'],
            ':uuid' => $item['id'],
        ]);

        $updated++;

    } catch (Exception $e) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'sql error',
            'id' => $item['id'],
            'error' => $e->getMessage()
        ];
    }
}

/**
 * =======================
 * 3. Output
 * =======================
 */
echo "UPDATED: {$updated}\n";
echo "SKIPPED: {$skipped}\n";

echo "\n=== SKIPPED DETAILS ===\n";

if (empty($skippedList)) {
    echo "NONE\n";
} else {
    foreach ($skippedList as $s) {
        echo json_encode($s, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
    }
} 


运行代码之前, 要把 php.ini 中的两个插件启用, 也就是把这两行前面的分号去掉, 然后重启 php: 

extension=pdo_sqlite
extension=sqlite3 

如果你的 extensions 里面甚至都没有这两个 dll, 那么改 php.ini 也没有用了, 推荐使用类似 wamp 的一键端, 它有很多名字: uniform server, unicontroller, miniserver, 都是同一个软件. 内含 apache mysql php, 只启用 php 就行. 链接: https://sourceforge.net/projects/miniserver/


最终输出如下: 

UPDATED: 成功的数量 SKIPPED: 1 === SKIPPED DETAILS === { "reason": "bw not found", "id": "这里是uuid序号", "key": "这里是json里的name字段|这里是json里的username字段" }  

没找到就说明 bw 里面没有, 这是你自建了以后新增的记录. 

再去看看数据库里面的 created_at 和 updated_at 已经写好了, 但是格式不对, 简单替换下, sql 执行: 

UPDATE ciphers
SET
  created_at = REPLACE(REPLACE(created_at, 'T', ' '), 'Z', ''),
  updated_at = REPLACE(REPLACE(updated_at, 'T', ' '), 'Z', '');


* 如果你需要 备份/同步 数据库, 如使用 mega sync 同步 .db 文件而不同步 wal, 那需要在 crontab -e 中加上: 

0 5 * * * /usr/bin/docker restart vaultwarden

来保证数据库 wal 能自动写入 .db, 而触发同步. 

星期二, 二月 24, 2026

193 docker 迁移目录和数据 包括 docker 和 containerd | 解决迁移之后依旧存储在原目录的问题

首先来讲 docker 和 containerd, docker 是个第三方软件, 它依赖于系统的 containerd, 类似于早些年的套壳 ie 浏览器, ie 坏了, 套壳浏览器也会打不开. 这两块的数据也是分开的, 也就是按照很多网上教程设置完 docker 的 json 之后, docker pull 依旧被拉取到了原目录的根本原因. 

docker 配置路径文件:  /etc/docker/daemon.json

docker 默认数据目录: /var/lib/docker/

containerd 配置路径文件: /etc/containerd/config.toml

containerd 默认数据目录: /var/lib/containerd 

所以这两个配置文件, json 和 toml 要配置好, 再把数据目录复制或移动走, docker 才能完整地运行起来


之前干了件蠢事, 由于小鸡只给了 10GB 的固态硬盘空间, 所以我把 docker 迁移到机械盘了, 机械盘很大, 有 1000GB, 但迁移过去后悔了, 在机器重启之后, 从 docker 服务开始启动到正常运行, 花了至少五分钟, 这还是只有三个容器的情况下, 如果再跑大型的 浏览器微信 qq 客户端 docker 那得花十分钟以上, 重启容器也要花费大量时间, 后来 也不需要挂着微信和 qq 了, 就打算迁移回来. 10GB 完全够用. 

 

======带数据 反向迁移 (从其它位置迁移回默认目录)====== 

现在, docker 是运行中的状态. 目前的状态: 

docker 数据: /mnt/hdd/root/docker 

containerd 数据: /mnt/hdd/root/docker/containerd

我想要迁移回 /var/lib/ 也就是系统默认的地方去. 

从这一步开始, 我详细讲解 docker 带着数据迁移的方法. 大家如果是从默认目录迁移至别处, 注意目录的颜色, 蓝色字体是 原目录, 红色字体是 目标目录, 要迁移到的地方. 

1, 确认目录位置:

docker info | grep "Docker Root Dir"

2, 停止 service 和 socket

systemctl stop docker.socket
systemctl stop docker.service
service containerd stop

3, 复制 docker 文件, 这里排除了 containerd, 因为我放到一起了: 

rsync -a \
 --exclude='containerd' \
 /mnt/hdd/root/docker/ \
 /var/lib/docker/

复制 containerd 中的文件, 由于 containerd 文件极多, rsync 效率会很低, 使用内存中转的方式在后台运行, 这种操作不会在磁盘上生成多余文件, 注意 tar -xf - -C 后面跟的是上级目录, 如果目标已存在不用的 containerd 记得删除. 注意两侧目录都不包含文件夹名称

nohup bash -c 'cd /mnt/hdd/root/docker && tar -cf - containerd | tar -xf - -C /var/lib' > /root/containerd.log 2>&1 &

再使用命令对比目录大小: 

nohup bash -c 'du -sh /mnt/hdd/root/docker/containerd/ > /root/mnt.txt 2>&1' &
du -sh /var/lib/containerd/

4, 修改 docker 配置文件: 

echo '{"data-root": "/var/lib/docker"}' | sudo tee /etc/docker/daemon.json
cat /etc/docker/daemon.json

5, 修改 containerd 配置文件, 打开 /etc/containerd 文件夹, 修改 config.toml: 

root = "/var/lib/containerd"

再看一眼是否改好: 

cat /etc/containerd/config.toml

6, 先启动 containerd 再启动 docker

service restart containerd
service restart docker

7, 删除旧的 docker 文件夹和 containerd 文件夹.  

 

如果你在启动 docker 之后, 看不到 images, 看到 containers 变成了类似 md5 的名字, 那就是 containerd 的目录没设置对, 检查 toml 配置文件和实际存放的位置是不是一致, 如果你用 tar -xf - -C 一定要注意后面跟上级目录. 不然会变成 /var/lib/containerd/containerd, 变成这样也没关系, 移动一下就行. 

我没有使用 docker compose, 如果你使用它, 迁移方法可能会有不同. 


======带数据 正向迁移 (从默认目录迁移至其它位置)======

1, 确认目录位置:

docker info | grep "Docker Root Dir"

2, 停止 service 和 socket

systemctl stop docker.socket
systemctl stop docker.service
service containerd stop

3, 复制文件, 首先是 docker, 这里只迁移 docker, containerd 有自己的目录, 位于 /var/lib/containerd 下: 

rsync -a /var/lib/docker/  /mnt/hdd/docker/

复制 containerd 中的文件, 由于 containerd 文件极多, rsync 效率会很低, 使用内存中转的方式在后台运行, 这种操作不会在磁盘上生成多余文件, 注意 tar -xf - -C 后面跟的是上级目录, 如果已存在不用的 /var/lib/containerd 记得删除. 注意两侧都不包含文件夹名称

nohup bash -c 'cd /var/lib && tar -cf - containerd | tar -xf - -C /mnt/hdd' > /root/containerd.log 2>&1 &

再使用命令对比目录大小: 

nohup bash -c 'du -sh /var/lib/containerd/ > /root/mnt.txt 2>&1' &
du -sh /mnt/hdd/containerd/

4, 修改 docker 配置文件: 

echo '{"data-root": "/mnt/hdd/docker"}' | sudo tee /etc/docker/daemon.json

再看一眼是否改好: 

cat /etc/docker/daemon.json

5, 修改 containerd 配置文件, 打开 /etc/containerd 文件夹, 修改 config.toml: 

root = "/mnt/hdd/containerd"

再看一眼是否改好: 

cat /etc/containerd/config.toml

6, 先启动 containerd 再启动 docker

service containerd restart
service docker restart

7, 删除旧的 docker 文件夹和 containerd 文件夹.