125 lines
8.3 KiB
Markdown
125 lines
8.3 KiB
Markdown
# 65 · 功能插件启停与 web provider 配置联动
|
||||
|
|
|
|||
|
|
- 日期:2026-09-12
|
|||
|
|
- 触发:用户决定 **AnySearch 接入改走候选池统一机制**("有问题解决问题"),而候选池的启停会打穿 profile 层的 provider 硬绑定
|
|||
|
|
- 结论一句话:**把「插件启停」与「dsh-web 的 provider 选择」从两件互不知情的事,变成一件由平台统一维护的事** —— 启用时写死插件声明的 searchProvider,禁用时整段删除,回到「只有一个可用」的自动选择。
|
|||
|
|
- 状态:✅ **已部署并生效**(服务器 `lib/` 2026-09-13 00:13 构建 → **00:15:23 重启即已载入**;08:02 随档案 78 部署再载入一次)
|
|||
|
|
|
|||
|
|
> **TL;DR**|**结论**:候选池「禁用即坏」的根因不是候选池,而是 profile 层那段**硬绑定**的 provider 覆写 —— 插件一被 `pnpm remove`,配置就指向了不存在的 provider(`CONFIGURED_MISSING`,且 dsh 不回落)。
|
|||
|
|
> **解法**:平台在 `install`/`uninstall` 后按**当前 bundles 重算**托管段,让两态都正确。
|
|||
|
|
> **状态**:✅ 代码完成并已用真实数据验证两态(§七),**待部署**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、问题
|
|||
|
|
|
|||
|
|
dsh 的 web provider 选择语义(源码实证):
|
|||
|
|
|
|||
|
|
- **显式配置即硬绑定** —— provider 不在了就报 `WEB_PROVIDER_CONFIGURED_MISSING`,**不回落**
|
|||
|
|
- 未配置时**只有「恰好一个可用」才自动选**,多个可用直接 `WEB_PROVIDER_AMBIGUOUS` 报错
|
|||
|
|
|
|||
|
|
而候选池的禁用是 **`pnpm remove`(真卸载)**,于是:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
禁用插件 → bundle 移除 → 插件自带 patch(searchProvider: anysearch)不再加载
|
|||
|
|
→ 但 profile 覆写段仍在,仍写死 searchProvider: anysearch
|
|||
|
|
→ 指向未注册的 provider → CONFIGURED_MISSING(搜索直接坏,且用户修不了)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**三种静态写法都有坏格子**(详见档案 64 §九.3):写死→禁用即坏;不写→共存即歧义;禁 deepseek→禁用后 0 个可用。
|
|||
|
|
|
|||
|
|
## 二、方案
|
|||
|
|
|
|||
|
|
把 provider 配置从**静态**改成**随 bundles 动态重算**:
|
|||
|
|
|
|||
|
|
| 状态 | profile 托管段 | 结果 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 插件已启用 | `searchProvider: <插件声明值>` + `fetchProvider: http` | ✅ 走 AnySearch,本地抓取保留 |
|
|||
|
|
| 插件已禁用 | **整段删除** | ✅ 只剩 deepseek 一个可用 → 自动选中 |
|
|||
|
|
|
|||
|
|
**平台策略(本档定)**:`fetchProvider` **恒为本地 `http`**,**忽略**插件对 fetch 的声明 —— 这是「保留本地抓取 + SSRF 防护」这条决策的落地位置。插件若声明了别的 fetch provider,不采纳。
|
|||
|
|
|
|||
|
|
## 三、实现
|
|||
|
|
|
|||
|
|
**位置**:`src/web/routes/business-plugins.ts`
|
|||
|
|
|
|||
|
|
| 新增 | 职责 |
|
|||
|
|
|---|---|
|
|||
|
|
| `collectWebProviderClaims(dir, bundles)` | 扫描已启用 bundle 自带的 `cordis.patch.yml`,只取 `- id: web` 条目下 `config.searchProvider` 的**声明值**(跳过 `@deepseek-ai/*` 官方骨架;正则行扫描,不引入 YAML 依赖) |
|
|||
|
|
| `syncWebProviderPatch(dir)` | 幂等重算:先摘掉旧托管段(含历史遗留的 `platform: anysearch-search`),再按当前 bundles 追加 |
|
|||
|
|
|
|||
|
|
**调用点**:`install()` 与 `uninstall()` 里紧接 `reconcileBundles(dir)` 之后 —— 保证「bundles 已定 → 配置随之重算」的因果顺序。
|
|||
|
|
|
|||
|
|
**托管段标记**(用 marker 包裹,便于幂等替换与人工识别):
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# >>> platform: web-provider (managed by business-plugins)
|
|||
|
|
# 由业务插件的启用/禁用自动维护,请勿手改。策略:只换 search,fetch 恒为本地 http(保留 SSRF 防护)。
|
|||
|
|
- id: web
|
|||
|
|
config:
|
|||
|
|
searchProvider: anysearch
|
|||
|
|
fetchProvider: http
|
|||
|
|
# <<< platform: web-provider
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**与既有机制的关系**:
|
|||
|
|
- 与档案 34 的 **profile 快照/还原**天然兼容 —— `SNAPSHOT_FILES` 本就含 `cordis.patch.yml`,探活失败回滚会把这段一起还原。
|
|||
|
|
- 与档案 16 的三层归属模型**不冲突**:这是「功能插件」层内部的实现细节,不新增归属层。
|
|||
|
|
|
|||
|
|
## 四、为什么不让插件自己声明得更明确(备选方案否决记录)
|
|||
|
|
|
|||
|
|
| 方案 | 否决理由 |
|
|||
|
|
|---|---|
|
|||
|
|
| 插件 `package.json` 加 `dsh.webProviders` 字段 | 第三方插件(AnySearch)**不会**按平台的自定义约定声明,等于白设 |
|
|||
|
|
| 平台代码里硬编码插件名 → provider 映射 | 每接一个 provider 插件就要改平台代码;且与「插件自带 patch 已声明」的事实重复 |
|
|||
|
|
| **读插件自带 patch 推导(采用)** | 零配置、通用、自动跟随插件升级 |
|
|||
|
|
|
|||
|
|
## 五、待办与迁移
|
|||
|
|
|
|||
|
|
1. **部署**:`npm run build` → scp `lib/` → `systemctl restart dshs`(**会中断在线用户,R8 需先知会**);
|
|||
|
|
2. **投放候选池**:把 `anysearch-dsh-0.1.4.tgz` 落入 `<dataRoot>/business-plugins/` + 写 `business_plugins` 行(正规入口是门户 `#/plugins` 上传,仅 admin);
|
|||
|
|
3. **迁移 admin**:移除 `scripts/ensure-anysearch-admin.cjs` 写的手工段(`platform: anysearch-search`),改由平台托管段接管 —— 该脚本后续仅保留「铺候选池」职责或整体退役;
|
|||
|
|
4. **验证两态**:启用态(`searchProvider: anysearch`)与禁用态(托管段消失 → 自动选 deepseek)各跑一次 `--dump-config`。
|
|||
|
|
|
|||
|
|
## 六、红线遵守
|
|||
|
|
|
|||
|
|
- **R1/R2**:只改平台自有代码(`src/web/routes/`),不碰官方 dsh 主程序与缓存。
|
|||
|
|
- **R7**:改动仅 1 个源文件(+ 2 处调用);不批量改写、不 `git add -A`。
|
|||
|
|
- **R8**:重启服务前先告知影响并取得确认。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、实施与验证(2026-09-12)
|
|||
|
|
|
|||
|
|
### 7.1 代码
|
|||
|
|
|
|||
|
|
`src/web/routes/business-plugins.ts` 单文件改动:
|
|||
|
|
|
|||
|
|
| 位置 | 改动 |
|
|||
|
|
|---|---|
|
|||
|
|
| 模块级(导出) | 新增 `WEB_PROVIDER_OPEN/CLOSE/LEGACY` + `collectWebProviderClaims()` + `syncWebProviderPatch()` |
|
|||
|
|
| `install()` / `uninstall()` | 在 `reconcileBundles(dir)` 之后各加一行 `syncWebProviderPatch(dir)` |
|
|||
|
|
|
|||
|
|
> 函数刻意放在**模块级并导出**(不依赖 `app`):一是便于被真实产物直接验证,二是为后续加单测留口子。
|
|||
|
|
|
|||
|
|
**自检**:`npm run typecheck` exit 0 · `npm run build` exit 0。
|
|||
|
|
|
|||
|
|
### 7.2 真实数据验证(未用生产文件)
|
|||
|
|
|
|||
|
|
方法:把**编译产物**(`lib/web/routes/business-plugins.js`)以 `business-plugins.new.js` 名义放到服务器**同目录**(相对 import 可解析、不覆盖生产文件),再由一次性脚本在 `/tmp` 副本上跑 admin profile 的**真实数据**(真实 `package.json`、真实 profile patch、真实插件包)。脚本与副本用完即删。
|
|||
|
|
|
|||
|
|
| 态 | 输入 | 期望 | 实测 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **A 启用** | bundles 含 `@anysearch/anysearch-dsh`(照抄 admin 现状,patch 里还带着早期**手工直铺段**) | 清掉手工段、写平台托管段 | ✅ `claims={"search":"anysearch"}`;手工 `anysearch-search` 段被移除;写入 `web-provider` 段(`searchProvider: anysearch` + `fetchProvider: http`) |
|
|||
|
|
| **B 禁用** | 从 deps + bundles 移除该插件、删其 `node_modules` | 托管段应**整段消失** | ✅ `claims={"search":null}`;同步后文件里既无 `web-provider` 也无 `anysearch-search` |
|
|||
|
|
| **C 幂等** | 对态 A 再跑一次 | 逐字不变 | ✅ `true` |
|
|||
|
|
| **D 无残留** | 检查态 B 产物 | 不含任一托管段 | ✅ 两个都 `false` |
|
|||
|
|
|
|||
|
|
**结论**:两态都正确 —— 启用时走 AnySearch 且保留本地抓取;禁用时自动回到 DeepSeek,**没有 `CONFIGURED_MISSING` 坏状态**。迁移也顺带完成(手工段被自动清除)。
|
|||
|
|
|
|||
|
|
### 7.3 待办
|
|||
|
|
|
|||
|
|
1. **部署**:`lib/` → 服务器 + `systemctl restart dshs`(**R8:中断在线用户,待确认窗口**);
|
|||
|
|
2. **投放候选池**:tgz 落 `<dataRoot>/business-plugins/` + 写 `business_plugins` 行(正规入口 = 门户 `#/plugins` 上传);
|
|||
|
|
3. ~~**退役 `scripts/ensure-anysearch-admin.cjs` 的手工覆写职责**~~ → ✅ **2026-09-13 08:0x 已完成**:脚本头加**硬拦**(默认 `exit 2`,仅 `DSH_ALLOW_RETIRED_ANYSEARCH=1` 可放行),保留为「考古/回滚」用途、不再日常可跑;备份 `ensure-anysearch-admin.cjs.bak-retire-20260913`;实测默认拒绝 rc=2、显式放行可干跑、`node --check` 语法 OK。**原条目** —— 由平台托管段接管后,该脚本只应保留「铺候选池」或整体退役(**否则两段并存、后者覆盖前者,会产生难以察觉的配置漂移**);
|
|||
|
|
4. **端到端**:admin 在「功能管理」里启用 → 搜一次;再禁用 → 搜一次(应回落到 DeepSeek)。
|