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 一律写「远程服务器」。
17 KiB
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,含编译缓存)。
回滚(任一步均可独立回滚):
- 从 profile
package.json的dependencies摘掉@anysearch/anysearch-dsh+bundles移除该名目 → 重启实例即回到 DeepSeek 搜索; - 删除
.credentials.yaml里的ANYSEARCH_API_KEY记录; - 移除 S3 的覆写段;
- tgz 与
.dsh-stage/暂存可留(只读 444,不生效)。
七、红线遵守
- R1 不触发 dsh 升级(插件走 profile bundle,不动 dsh 主程序/缓存)。
- R2 不改官方 dsh 主程序与缓存。
- R3 不依赖 patch 官方包。
- R7 只动「本次明确清单」内的文件:每用户
profile/package.json1 个 +.credentials.yaml1 个(合并,非覆盖)+cordis.patch.yml1 个;不批量改换行符 / 不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 待办
- admin 端到端确认(访问自己的实例 → 让它搜一次 → 确认走 AnySearch);
- 通过后铺普通用户:
ANYSEARCH_API_KEY=… node scripts/ensure-anysearch-admin.cjs --apply --restart guest; - 观察配额消耗(
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 的「走候选池」这条路又暴露一个更硬的问题 —— 该插件与平台 dsh0.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_logid 175),凭据中ANYSEARCH_API_KEY实测已不存在,全部用户 profile 无残留;§8.3 的「铺普通用户」与 §8.4 的回滚步骤(删 3 行)随之一起作废。后续若重启此能力,请从换兼容版本开始(先过档案 71 的兼容性预检),不要再导入0.1.4。完整处置见 档案 70 §九。