Files
dsh_shenxian/dsh-server-docs/04-调整方案/64-接入AnySearch搜索provider.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

17 KiB
Raw Blame History

64 · 接入 AnySearch 搜索 provider(B 方案)

  • 日期:2026-09-12
  • 触发:用户问「平台是否配了联网搜索 / 用的什么搜索 / 没有地方管理这类功能 / 能否装 anysearch」,随后选定 B 方案并给出 API Key
  • 结论一句话:平台现用 DeepSeek 官方搜索;接 AnySearch 可行,且官方插件自带 provider 选择配置(不用自己写 patch 解决歧义)——但落地前有 3 个必答点,其中「这把 key 给谁用」是 P0,因为它决定外泄面。
  • 状态:admin 侧已实施(见 §八);⚠️ 未闭环 = §8.3(端到端确认 → 铺普通用户)。§9.4 的「保持平台级直铺」建议已由档案 65 推翻(改走候选池 + 平台托管段)

TL;DR|结论:接 AnySearch 技术上零障碍(官方插件 @anysearch/[email protected],MIT,自带 searchProvider: anysearch 配置);卡点不在技术,在「key 归属」——全租户共用一把 key 意味着任何租户可从自己实例读出它。 关键:前置验证 4 项全绿(key 有效 / 服务器可达 / 联合可用 / 包安全);插件会一并替换 fetch provider,需决定是否保留本地带 SSRF 防护的 http。 状态:admin 侧已实施(§八);剩余待办 = §8.3


一、现状(只读取证,2026-09-12)

项 实测值
dsh 版本 0.1.2-rc.1(/usr/local/lib/node_modules/@deepseek-ai/dsh)
搜索 provider 只有 dsh-web-search-deepseek(id deepseek-official)——DeepSeek 官方联网搜索
抓取 provider dsh-web-fetch-http(id http,本地实现,带 SSRF 防护)
机制 POST https://api.deepseek.com/anthropic/v1/messages,Anthropic 兼容 + 原生 web_search_20250305(max_uses 默认 5);复用 DEEPSEEK_API_KEY(不复用 DEEPSEEK_BASE_URL)
成本特征 一次搜索 = 一次模型 turn(比专用搜索 API 贵)
未安装 官方另有 dsh-web-search-exa、dsh-web-search-perplexity,本部署未装
管理面 dsh 自带「设置 → 插件 → 插件配置 → Web search」,但被 ensure-role-profile-patch.cjs 对非 admin disabled: true(连同 models / plugin-inventory / cordis)→ admin 能管、普通用户看不到;门户侧无搜索管理面

二、前置验证(已完成,全绿)

# 项 结果 命令要点
1 Key 有效性(本机) ✅ HTTP 200 · code:0 · 1.4s curl -X POST https://api.anysearch.com/v1/search -H 'Authorization: Bearer <key>' -d '{"query":..,"max_results":1}'
2 服务器网络可达 ✅ 拿到 request_id 服务器 DNS 走 CloudFront;IPv6 解析正常
3 服务器 + key 联合 ✅ HTTP 200 · code:0 · 2.6s 同上,在服务器执行
4 插件包安全审查 ✅ P2 安全 无 child_process / eval / new Function / require() / process.env / 文件写 / chmod;唯一外联 https://api.anysearch.com

注:本机 Invoke-RestMethod 对该端点必超时(两次实测),用 curl 正常 —— 排查时勿被误导。

三、插件包分析(@anysearch/[email protected])

168 KB / 33 文件,lib/*.js 为预构建产物(消费方无需构建)。

自带的 cordis.patch.yml(关键,直接解决了 provider 歧义):

- id: web
  config:
    searchProvider: anysearch
    fetchProvider: anysearch
- insert:
    - id: web-search-anysearch
      name: '@anysearch/anysearch-dsh'
      config:
        apiKeyEnv: ANYSEARCH_API_KEY
事实 值 影响
provider id anysearch(search + fetch 各注册一个) patch 已显式指定 → 不会触发 WEB_PROVIDER_AMBIGUOUS
available() endpoint(baseURL,'/v1/search') !== undefined → 不校验 key,恒为 true 装上后 search registry = {deepseek-official, anysearch},靠 patch 消歧
key 解析 ctx.credentials.resolve(credentialRef('ANYSEARCH_API_KEY')) 走 dsh credentials 服务(读 $DSH_HOME/.credentials.yaml / 环境)
额外工具 anysearch_search / anysearch_batch_search / anysearch_capabilities 白得 3 个工具
web_fetch 工具 已存在则不重复注册 无副作用
默认 baseURL https://api.anysearch.com 与验证一致

它的 patch 同时把 fetchProvider 换成 anysearch —— 即抓取从本地实现改为第三方 AnySearch Extract,本地那套 SSRF 防护(拒非公网地址、逐点解析、逐跳重定向校验)随之失效。

平台若要保留本地 fetch,需在 profiles/web/cordis.patch.yml 覆写:

- id: web
  config:
    fetchProvider: http

⚠️ 合并顺序未实测:平台的 profile patch 与 bundle patch 都经 applyEntryPatches 合入,谁后应用决定谁生效。必须用 dsh --profile web --dump-config 实测合成结果,不能假设。

四、三个必答点

# 级别 问题 选项
1 P0 这把 key 给谁用? (a) 全平台统一注入 —— 所有实例共用;任何租户都能从自己实例读出它(.credentials.yaml 属主即该用户,或 env 可读),等同档案 23 已记的 DEEPSEEK_API_KEY 外泄面,且这把是个人账号 key、配额与账单都绑在你身上
(b) 只给 admin → 普通用户继续 DeepSeek
(c) 全平台用 + 接受外泄面(写进已知风险)
2 P0 fetch 是否一并换成 AnySearch Extract? (a) 保留本地 http(推荐:SSRF 防护不丢,只换搜索)
(b) 接受插件的默认(search + fetch 都走 AnySearch,抓取质量可能更好,但防护逻辑改变)
3 P1 投放方式 (a) 平台级统一铺(同 portal-entry / business-plugins 姿势,用户无感、不可关)
(b) 进功能插件候选池 → 用户在实例「功能管理」自助启停(搜索是基础能力,用户关掉会退回 DeepSeek,不建议)

五、执行步骤(确认后)

步 动作 中断?
S0 把 tgz 放 /opt/dsh/artifacts/anysearch-dsh-0.1.4.tgz 否
S1 先写 key:合并 <home>/.credentials.yaml → ANYSEARCH_API_KEY(必须合并,该文件已有 client-connection/browser-session 记录;保持 600 + 用户属主) 否
S2 仿 ensure-biz-plugins.cjs 写 ensure-anysearch.cjs:tgz 暂存 <home>/.dsh-stage/(444、chown uid)→ setpriv --reuid <uid> env HOME=<home> pnpm add --store-dir <inherit> file:<staged> → reconcileBundles 否
S3 写 profile patch 覆写段(若第 2 点选保留本地 fetch) 否
S4 停实例 scope(systemctl stop dsh-<uid>-*.scope,下次访问自动拉起) 是
S5 --dump-config + 实例内实跑 web_search 验证 —

顺序不可颠倒:profile 里 patchReload: live,bundle patch 有热加载可能 → 先写好 key 再装插件,避免出现「provider 已切换、key 还没配」的窗口。

六、影响与回滚

影响(R8):S4 会停掉 2 个用户实例(guest 4092b965…、admin cce6d1cd…),当前会话断开,下次访问自动拉起(冷启动约 4-5s,含编译缓存)。

回滚(任一步均可独立回滚):

  1. 从 profile package.json 的 dependencies 摘掉 @anysearch/anysearch-dsh + bundles 移除该名目 → 重启实例即回到 DeepSeek 搜索;
  2. 删除 .credentials.yaml 里的 ANYSEARCH_API_KEY 记录;
  3. 移除 S3 的覆写段;
  4. tgz 与 .dsh-stage/ 暂存可留(只读 444,不生效)。

七、红线遵守

  • R1 不触发 dsh 升级(插件走 profile bundle,不动 dsh 主程序/缓存)。
  • R2 不改官方 dsh 主程序与缓存。
  • R3 不依赖 patch 官方包。
  • R7 只动「本次明确清单」内的文件:每用户 profile/package.json 1 个 + .credentials.yaml 1 个(合并,非覆盖)+ cordis.patch.yml 1 个;不批量改换行符 / 不 git add -A。
  • R8 停实例前已在本档列明影响并取得确认。

八、实施记录(2026-09-12 10:30-11:20)

用户裁决:① 分两步走(先只装 admin 验证)② 保留本地抓取 ③ 现在就执行。

范围:仅 admin(cce6d1cd-b376-4304-80f0-0e1c58c9ffde,uid 114801)。

载体:新脚本 dsh_shenxian/scripts/ensure-anysearch-admin.cjs(仿 ensure-biz-plugins.cjs 的 setpriv + 沿用旧 store 姿势;支持 --apply / --restart / 干跑 / 指定用户)。

步 结果
传包 /opt/dsh/artifacts/anysearch-dsh-0.1.4.tgz(168851 B)
凭据 refs.ANYSEARCH_API_KEY 写入,600 + uid 114801
patch platform: anysearch-search 覆写段写入
依赖 @anysearch/anysearch-dsh 安装成功(store 沿用旧,ws 保持干净)
bundles 6 项,含该包 ✅
停实例 0 个 —— 执行时 admin 实例未运行 → 下次访问自动拉起新配置(无需额外重启)

8.1 关键实测发现(推翻本档 §三 的假设)

profile 层 - id: web / config: 对 dsh-web 条目是「整体替换 config」,不是字段级合并。

首次只写 fetchProvider: http 时,插件自带 patch 的 searchProvider: anysearch 被一并冲掉 —— dump-config 里该条目只剩 fetchProvider: http。若就此上线,search 侧两个 provider 都 available()=true 又无显式选择 → 命中 WEB_PROVIDER_AMBIGUOUS(报错,不会自动选一个)。

修正:覆写块必须两个字段都写全;脚本 ensurePatch 已改为幂等「整块替换」(新增 updated 动作)。

8.2 验证记录

# dsh --profile web --dump-config 实测(admin)
- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: anysearch
    fetchProvider: http
- id: web-search-anysearch
  name: '@anysearch/anysearch-dsh'
  config:
    apiKeyEnv: ANYSEARCH_API_KEY
# 验证项 结果
1 provider 选择 ✅ search=anysearch / fetch=http
2 凭据文档格式 ✅ dsh 严格解析通过(dump-config 无 warning / 无报错)
3 凭据权限 ✅ 600 + dsh-cce6d1cdb376430480f0
4 网络 + key(服务器侧 curl) ✅ HTTP 200 · code:0 · 2.6s
5 端到端(实例内实跑 web_search) ⏳ 未做 —— 留待 admin 首次访问实例时确认

附注:dump 中 tool-web 条目带 disabled: true,注释显示来自 @deepseek-ai/dsh-web-app —— 这是既有状态(档案 37a 已证 web_search 当时可用),非本次引入。

8.3 待办

  1. admin 端到端确认(访问自己的实例 → 让它搜一次 → 确认走 AnySearch);
  2. 通过后铺普通用户:ANYSEARCH_API_KEY=… node scripts/ensure-anysearch-admin.cjs --apply --restart guest;
  3. 观察配额消耗(anysearch.com/console),评估是否需把 key 换成团队账号。

⚠️ 修正(2026-09-12 用户拍板):上文第 2 条的「每用户直铺」路线作废 —— 三方插件一律走「admin 在门户导入候选池 → 用户在实例『功能管理』自助启用/禁用」,不再向用户 profile 铺任何东西。 执行口径改以 档案 65 §7.3 为准:部署 65 → 投候选池 → 退役本脚本的手工覆写职责 → 启/禁两态端到端。 原因:直铺段与档案 65 的平台托管段同属「整体替换 config」语义,两段并存会互相覆盖,产生难以察觉的配置漂移(档案 66 §待办 3)。

8.4 回滚

步 动作
1 profile package.json:从 dependencies 与 dsh.profile.bundles 摘掉 @anysearch/anysearch-dsh → 停实例
2 .credentials.yaml:删掉 refs.ANYSEARCH_API_KEY(删 key = 该段为空则连 refs: 段一起删)
3 cordis.patch.yml:删掉 # >>> platform: anysearch-search ~ # <<< platform: anysearch-search 整块(或直接还原 .bak-anysearch)

⚠️ .dsh-stage/anysearch-dsh-0.1.4.tgz 不可删 —— profile 的 file: 依赖指向它,删了会重现「清理残留 tgz 导致依赖断裂」那个事故(2026-09-12 上午,目前只记在当日工作日志的「事故 63」里、无正式档案):此后任何 pnpm add 都会在解析阶段 ENOENT 失败。 含密钥的 .credentials.yaml.bak-anysearch 已在验证后删除(回滚只需删 3 行,无需备份)。


九、与「候选池 + 用户自助启停」方式的差异(2026-09-12 追问后核实)

用户问:能否改成「admin 在门户导入候选池 → 用户自己在实例里启用/禁用」。机制确实存在(档案 16 三层模型,已实施),但本场景不建议走。

9.1 两条路的事实差异

平台级直铺(本次采用) 候选池 + 用户启停
入口 脚本直接 pnpm add 进 profile bundle 门户 portal.html#/plugins 上传 → business_plugins 表 + 磁盘 tgz
用户可见 实例「功能管理」里看不到 列表可见、可勾选
用户可关 不能 能
配置一致性 恒定(覆写段与插件同时在场) 可能失衡(见 9.2)

9.2 关键坑:走候选池会「一禁用就坏」

实测实现(src/web/routes/business-plugins.ts L399-402、L410-413):

  • 启用 = pnpm add file:<tgz> + reconcileBundles
  • 禁用 = pnpm remove <id> + reconcileBundles —— 真卸载,不是「留包 + disabled: true」

⚠️ 档案 16 §7.1-5 写的「禁用机制 = 保留包 + cordis patch disabled: true(不删 node_modules)」与现实现不符(实现是移除依赖)。该条需修订 —— 本次不擅自改档案 16,仅在此登记。

于是用户禁用 AnySearch 时:

插件 bundle 被移除 → 插件自带 patch(searchProvider: anysearch)不再加载
                  → 但 §八 的 profile 覆写段仍在,仍写死 searchProvider: anysearch
                  → 指向一个未注册的 provider
                  → WEB_PROVIDER_CONFIGURED_MISSING(报错,不回落 DeepSeek)

且用户自己修不了(他看不到 <profile>/cordis.patch.yml)。

9.3 为什么不能靠「不写死」绕开

dsh 的选择语义决定了「两个 provider 共存 + 用户可任意启停」必然存在坏状态:

配置策略 anysearch 启用时 anysearch 被禁用时
写死 searchProvider: anysearch ✅ 正常 ❌ CONFIGURED_MISSING
什么都不写 ❌ AMBIGUOUS(2 个可用) ✅ 自动选 deepseek
禁掉 deepseek 那条 provider ✅ 自动选 anysearch ❌ UNAVAILABLE(0 个可用)

三种组合都有坏格子 —— 要优雅解决,需要平台在 apply 时同步维护 web 配置,属平台改造,不是配置能解决的。

9.4 结论与建议

  • 保持平台级直铺(现状):搜索是基础能力,用户不该关;配置恒定正确,无坏状态。
  • 若确要让用户自助启停,前置条件 = 先补一个平台能力:在 /api/plugins/mine/apply 的启用/禁用路径里,对「搜索/抓取 provider 类插件」同步 upsert profile patch 的 searchProvider / fetchProvider。
  • 另一条折中:候选池支持定向投放(目前档案 16 定为「全员可见,先不做分组」)—— 即便做了定向,9.2 的坑依然存在。

⚠️ 状态更新(2026-09-12,档案 65 落地后):本节的「保持平台级直铺」建议已被后续决策推翻 —— 用户选定走候选池统一机制("有问题解决问题"),档案 65 已在平台侧实现 9.4 第二条所述的前置能力(install/uninstall 后按当前 bundles 重算托管段),9.2 的坏状态已消除。以档案 65 为准,本节保留为决策过程记录。 另:本档头部「⏸ 待用户确认 2 个决策点」已过时 —— 见 §八 实施记录(admin 侧已实施);当前未闭环项见 §8.3。 再更新(2026-09-12 15:40,实证):§9.1 的「走候选池」这条路又暴露一个更硬的问题 —— 该插件与平台 dsh 0.1.2-rc.1 本体不兼容(@deepseek-ai/dsh-llm 未导出 assertNever),从候选池启用会让实例崩溃循环(已实证并止损)→ 见档案 70。即:§9.2/§9.4 讨论的「provider 坏状态」档案 65 可解,但兼容性问题档案 65 解不了 —— 先解决「插件能不能装」,再谈「启停怎么联动」。 终局更新(2026-09-12 18:56):该能力已被用户放弃(选 B · 下架) —— 候选池条目已下架(HTTP 200 / audit_log id 175),凭据中 ANYSEARCH_API_KEY 实测已不存在,全部用户 profile 无残留;§8.3 的「铺普通用户」与 §8.4 的回滚步骤(删 3 行)随之一起作废。后续若重启此能力,请从换兼容版本开始(先过档案 71 的兼容性预检),不要再导入 0.1.4。完整处置见 档案 70 §九。