# APK 插件管理 入口:应用设置 → 插件管理。按官方、第三方区分,按组合包声明的配置文件分组;展开文件可管理其中有稳定标识的子插件。界面支持简体中文和英语。 管理器不启动 Harness,也不导入插件模块;Harness 启动失败时仍可读取和修改配置。Ubuntu、PRoot 和内置 Node.js 本身需要可用。列表反映配置中的启停状态,并非运行时健康检查或插件依赖是否满足的保证。 修改前先点击“停止运行时”,关闭 Harness 和 Ubuntu 终端。原生生命周期锁也会拒绝运行中的修改,防止绕过界面直接写入。文件禁用后保留子插件选择,再启用时恢复;安全组件和核心配置文件受保护。第三方配置文件损坏时仍保留包级禁用入口。 子插件状态写入独立启动补丁 `launcher-plugins.patch.json`,不修改插件源码,也不覆盖模型供应商配置。文件级禁用保存在配置档案清单中,保留原文件顺序。当前范围是 `web` 配置档案的组合包配置文件;不提供任意路径的文件编辑器,也不管理代理预设内动态创建的插件实例。 第三方更新以包为单位,从固定 npm 注册表读取最新版本,隔离安装并禁用安装脚本。使用实际 Harness 安装锚点解析包,兼容 pnpm 隔离目录。安装完成后会把插件目录内 `@deepseek-ai/*` 的真实副本(含嵌套副本)统一改为指向当前运行时的符号链接,保证带 Symbol 的运行时服务包只有一份物理副本;判定规则、计数与边界见 `docs/插件安装与运行时单例.md`。插件自己的第三方依赖保持原样,其他已安装的共享依赖仍必须与新包安装结果一致,否则保留旧版本并提示不兼容。需要安装脚本、原生编译或新核心 SDK 的包不能通过此入口升级。官方包随运行时升级,避免覆盖 Android 兼容修补。 更新先准备新包,再记录事务并切换链接;失败回滚,上次中断的事务在下次修改或 Harness 启动前恢复。设备不支持链接时保留原插件。已提交版本的暂存目录可能继续被模块引用,因此不做未经引用检查的删除;更新失败且无需恢复时清理失败下载。 ### 从包名或地址导入 除按包名更新外,`import` 支持三种来源:npm 包名(含作用域名 `@scope/foo`)、`https://` 直链(npm 视为 tarball)、`git+https://` 仓库地址(可带 `#`)。来源只作为**一次调用的参数**:不写入清单、不进审计、不出现在返回值里;命令以参数数组交给 npm,不经过 Shell。 校验规则在访客脚本、原生与网页端**三处同一套**:长度 ≤512;字符白名单;拒绝 `http://`、`git://`、`ssh://`、`file:`、`data:`、任何 `..`(含 `%2e` 编码形式)、空白/引号/尖括号/反引号/换行;拒绝以 `-` 开头的输入(防 npm 选项注入);URL 拒绝 userinfo(`https://user:pass@host/...`)。失败一律返回 `PLUGIN_SOURCE_INVALID`,且**不回显**用户输入。 `git+https://` 要求运行时里真的能用 git:先在访客内执行 `git --version`,三种结果分开处理——可用则继续;**确实没有**(spawn 报 `ENOENT`)返回 `PLUGIN_GIT_MISSING`;**探测不了**(超时、被拦、异常退出)返回 `PLUGIN_GIT_UNVERIFIED`,不把"探测不了"谎报成"没有 git"。 导入与更新共用同一条事务路径(暂存安装 → 既有全部校验 → 提交点 → 提交点之后才改清单)。包已在清单里就等同更新;不在清单里则在提交后加入 `dsh.profile.bundles`,并按 `dshMobile.bundleOrder`、`dshMobile.disabledBundles` 的既有约定补齐顺序、清掉禁用行——重复导入同一包不会产生重复条目。地址形态装完后若无法确定装进来的是哪个包(有多个候选或给的名字对不上),返回 `PLUGIN_IMPORT_UNRESOLVED` 而不是猜一个。 ### 数据目录在更新前后保留 插件包内的可变数据目录沿用既有约定名 `data`、`config`、`storage`、`.config`:更新或导入时从"当前已安装版本"完整搬到新版本(含嵌套子目录),再切换链接,因此失败或取消时旧版本仍然完好。 迁移**不跟随符号链接**:目录内出现链接、非常规文件,或嵌套超过 8 层时报 `PLUGIN_DATA_UNSAFE`;累计超过 16 MiB(或文件数超过 4096)时报 `PLUGIN_DATA_TOO_LARGE`。两种情况都放弃本次更新并保留旧版本,不留下半成品。旧包没有这些目录时按正常更新处理,不报错。 ### 状态跨运行时升级的迁移 启停状态写在 `root/.dsh/profiles/web/package.json` 与 `root/.dsh-mobile/launcher-plugins.patch.json`,而运行时升级**不保留**这两处,升级后状态会丢。管理器因此在被保留的 `root/.dsh-mobile/plugin-manager/` 目录里留一份自身快照 `state-snapshot.json`(只含包名、启停状态与子插件禁用行,不含路径、地址或凭据),并把快照指纹写进清单的 `dshMobile.stateToken`。 - 回填由 `recover()` 负责(Harness 启动前与每次修改前都会调用);`list()` 保持只读——既不写快照也不回填清单,避免只读接口产生写入副作用。 - 清单里**已有**我们的状态(指纹一致)时绝不覆盖用户当前选择;只有清单里找不到我们的状态时才按快照补回缺失项,重复调用幂等、不产生重复条目。 - 快照损坏或清单损坏时按"无法回填"处理:不崩、不清空用户数据,清单原样保留。 - 写入全部走 `atomic()`,且管理操作要求运行时已停止(原生生命周期锁也会拦),因此不会与运行中的 Harness 争抢同一份清单。 ### 版本回滚 提交更新或导入时**不删除**旧的插件包目录,而是把它登记为该插件的"上一版"(记录在 `previous.json`);每个插件只留一份,不留历史链——再成功更新一次时替换成最近一次成功的那版。被淘汰的那份按既有"被引用检查"口径处理:拿不准就不删,宁可留着也不破坏仍被引用的模块。 `rollback` 把运行时指向该插件的链接切回上一版,走与更新同一套事务与 `recover()` 机制;**回滚本身可再回滚**(刚被换下来的那版成为新的"上一版")。没有上一版、上一版目录已被清理或该包受保护时报 `PLUGIN_ROLLBACK_UNAVAILABLE`;切换后校验不通过时报 `PLUGIN_ROLLBACK_FAILED`,并恢复到操作前的状态。`list` 如实报告当前版本与可回滚到的版本号(没有时该字段为 `null`)。 `repair` 做两件事:对齐插件目录内 `@deepseek-ai/*` 运行时副本,并把被运行时升级抹掉的**插件自身链接**接回配置档案的模块根(包本体一直在保留区,因此不必重装)。它不改插件自身的版本目录与已经可用的链接;`recover` 只回放未提交的事务日志,而回滚成功后事务日志已经删除——两者都不会把回滚结果改回去。 插件链接为什么会丢:安装时链接落在 `root/.dsh/profiles/node_modules/<包名>`(`modules` 里已有该包时落在那一处),而 `root/.dsh/profiles` **不在**跨版本保留名单里,升级后随新运行时整体重建。此时清单经 `recover()` 回填后仍把插件列为“已启用”,包本体也还在保留区,但没有任何模块根指向它——列表显示“未安装”,Harness 也加载不到该组合包。`repair` 按“最近一次安装”的副本把链接接回原位:已经能解析、目标位被别的有效实体占着时不动(只清掉本来就是坏的悬空链接),保留区里确实没有副本时只记 `missing`(这种情况只能重装),全程失败只计数。 **验证状态(2026-10-03,真机 HONOR AAP-AN00)**:`dshmarket`、`dsh-web`、`dsh-web-mobile` 三个第三方插件在一次内置运行时升级后同时从“已安装”变成“未安装”(`root/.dsh/profiles/web/` 里只剩 4 个构建期文件、没有 `node_modules`),而它们的包本体仍在 `root/.dsh-mobile/plugin-manager/versions/<事务>/node_modules/` 下。接回逻辑已按这份真机形态写成脚本测试(升级模拟 + 接回 + 幂等 + 无副本只记 `missing`),**2026-10-04 又补上这条链路的真机复测**:装 `0206374` 构建(`app-release.apk` 314,398,628 B)后更新内置运行时,设备终端里 `ls -l /root/.dsh/profiles/node_modules/dsh*` 报 `No such file or directory`(三条链接确实被抹掉),点「重新连接」启动 Harness 后再查,三条 junction 都在并指向保留区 `versions/<事务>/node_modules/<包名>`(`dshmarket` 取的是保留区里最新一次安装的事务目录),`…/dshmarket/package.json` 也能直接读到;插件管理页三条从「未安装」变成带版本(`dshmarket` 1.66.8、`dsh-web` 0.4.4、`dsh-web-mobile` 3.0.3)且仍是「已启用」。没有逐个验证这些插件在 Harness 里的实际功能表现。 ## 桥接接口 `managePlugins` 仅通过应用私有原生桥接调用,允许操作如下: | 操作 | 参数 | 含义 | | --- | --- | --- | | `list` | 无 | 列出文件及子插件元数据 | | `enable` | `id`、`enabled` | 整体启停配置文件 | | `child` | `id`、`childId`、`enabled` | 调整子插件开关 | | `update` | `id` | 更新所属第三方插件包 | | `import` | `source`(必填)、`id`(可选,期望的包名) | 从 npm 包名或连接地址导入 | | `rollback` | `id` | 回滚到该插件的上一版本 | `import` 与 `rollback` 的返回值沿用 `list` 的投影(同一份插件元数据),不额外回传地址、路径或凭据。 上表是桥接允许的全部操作。`recover`(回滚中断的更新事务、并在清单被重建时回填状态)、`repair`(已安装插件的运行时链接自愈)与 `graph`(只读模块图探测:只统计 `@deepseek-ai/dsh-tools` 的出现次数与**不同真实路径数**)由原生在 Harness 启动前与读取插件列表时内部触发,不接受网页端传参,也不回传插件清单,只回传处理计数。 包名限制为 214 字符以内的 npm 名称,拒绝路径穿越;子插件标识限制为 128 字符以内的受控字符;导入来源限制为 512 字符以内且只接受上面的白名单形态。原生与脚本双重校验,进程使用参数数组,不拼接 Shell 命令。返回值只含包名、相对配置文件名、版本、可回滚版本和启停状态,限制数量与响应大小;不返回配置内容、用户路径、凭据或原始进程日志。网页端再次校验并由 React 转义展示。 管理进程不接收模型 API 密钥、Harness 认证凭据或设备桥接凭据。审计只记录操作类型及受控结果码(导入与回滚各有一个事件类型,且**不记录来源地址**),沿用至少 90 天的保留策略。 ## 验证范围 脚本测试覆盖文件与子插件状态持久化、父子状态、安全组件保护、损坏配置恢复、更新失败与依赖冲突、中断回滚、pnpm 实际路径解析、YAML 表达式不执行、运行时包单例(安装后消重、嵌套副本、第三方依赖不被改动、已安装插件修复与幂等、悬空链接、越界路径拒绝),以及只读模块图探测(同一真实目录被多处链接引用不误报、两份真实副本判为重复、候选根缺失为零计数、探测不改动文件、不跟随越界链接)。新增覆盖:导入三种来源与重复导入幂等、来源校验逐条拒绝且不回显、git 三态区分、受保护包与无法推导包名的拒绝、数据目录保留(内容一致、嵌套子目录、超限、符号链接、旧包没有数据目录)、运行时升级后的状态回填与幂等、已有状态不被覆盖、快照与清单损坏的受控处理、`list` 保持只读、回滚与再回滚、`repair`/`recover` 不改回滚结果、升级后插件链接接回(接回一次、计数可读、幂等、无副本只记 `missing` 且不造悬空链接)。前端测试覆盖失败状态下管理、运行中禁用修改、停止后管理、更新失败重试、导入与回滚参数校验和英文界面。Android 构建及真机验证需要完整 Android 环境和团队签名配置。