# 硬编码语义索引(um-dsh-websearch) | 语义 | 当前值 | 短路径(文件:行) | 同步说明 | |------|--------|-------------------|----------| | 依赖族版本钉 | **官方包不进入运行时依赖闭包**(ADR-0008):`dependencies` 为空;peer `@deepseek-ai/cordis >=4.0.4 <5` · `schemastery >=3.18.4 <4` · `dsh-credentials` / `dsh-launch-environment` / `dsh-web` / `dsh-web-search-deepseek` 全 `>=0.2.0-rc.1 <0.3.0`;devDependencies 精确 `0.2.1-rc.2`(含 client 侧 locale/settings/plugin-manager/api-remotes/ui-primitives);overrides 同钉 0.2.1-rc.2(含 `dsh-llm: 0.2.1-rc.2`)+ `minimumReleaseAgeExclude` 白名单 0.2.1-rc.2 全族;`engines.dsh` 保持 `>=0.2.0-rc.1 <0.3.0` | package.json:45-52(peerDependencies) · package.json:53-66(devDependencies) · package.json:41-44(engines dsh) · pnpm-workspace.yaml(overrides + minimumReleaseAgeExclude) · tests/boundary.test.mjs(ADR-0008 回归门) | **为什么必须 peer/dev-only**(ADR-0008 根因):0.2.1 的 profile 解析中「profile 本地副本」先于「安装副本」(`dsh-app-boot/lib/index.js:1613-1674`),且新增逐行「精确版本」preflight(同文件 `279-298`:只看 `engines.dsh`,否则只看 `@deepseek-ai/dsh*` peerDependencies,**不查 dependencies**;`2305-2388` 判定并置 `disabled`)——`dependencies` 里的官方包会被 pnpm 装进 profile 闭包、顶掉安装副本,其精确 pin 对不上宿主 → `web` 行被禁 → `ctx.web` 缺席(`tool-web … waiting for web`)。profile 固定 `autoInstallPeers: false`,故 peer 声明不会进闭包,运行时一律解析安装副本。**禁止**用「改精确 pin / 收窄窗口追宿主版本」修复(另一侧宿主会整包被 skip)。升级宿主只改 devDeps + overrides 基线(当前 0.2.1-rc.2),运行时不跟随;旧安装刷新必须卸载→重装(hoisted 副本不自动消失) | | bundle manifest | `dsh.bundle.patch: ./cordis.patch.yml` + 层内容(insert `um-web-search` → `um-dsh-websearch` + **web 行 id 覆盖**) | package.json:20-23 · cordis.patch.yml:13-15(insert) · cordis.patch.yml:37-40(web 行覆盖) · README.md:92-102(层示例) · README.en.md:92-102(层示例) · cordis.patch.snippet.yml:20-21(用户侧覆盖模板) | 改行 id / 行 name 需与 lib 注册 id、README 层示例同步;insert 行 id 同时是设置命名空间(见下) | | web 行 searchProvider 自动接管 | bundle 层 `- id: web / config: {searchProvider: um-web-search, fetchProvider: http}`(安装即接管,卸载即回退 base 默认 `deepseek-official`;后层可覆盖) | cordis.patch.yml:37-40 · cordis.patch.yml:20-31(整体替换警告注释) · README.md:88-111(安装即接管 + 整体替换警告) · README.en.md:88-111 · cordis.patch.snippet.yml:35-39 | **patch 是整体替换**:id 定向 patch 只做浅写 `target[key] = value`,**必须 restate 该行全部键**。0.1.7 base `web` 行同时拥有 `searchProvider` 与 `fetchProvider`(dsh-base/cordis.patch.yml:471-475),只写前者会静默删除后者 → 第二个 fetch provider 出现即 `WEB_PROVIDER_AMBIGUOUS`(ADR-0006 D4)。base 再增键时同步此处 | | client manifest | `dsh.client.platform: web` + inject 边 [`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-settings`, `@deepseek-ai/dsh-client-ui-plugin-manager`, `@deepseek-ai/dsh-api-remotes`] | package.json:24-32 · lib/client.js:33(SLOT=plugins.row.config) · lib/client.js:1031(服务注入) | 0.1.7 目录下 slot 宿主 = ui-plugin-manager(slot `plugins.row.config`)+ ui-settings(`configForms`)+ locale + api-remotes(`remote`);旧的 `dsh-client-ui-settings-plugins` 边已退役。ui-primitives 无 `dsh.client` 声明=非图行,禁止列入 inject(未知成员会拒启动);改边需复核 client.js 的 require/slot 键 | | 版本化字符串(VERSION → USER_AGENT + MCP clientInfo) | 0.7.1 | lib/index.js:37(VERSION) · lib/index.js:39(USER_AGENT) · lib/index.js:921(clientInfo.version) · package.json:3 | VERSION 单源驱动 UA 与 clientInfo;与 package.json version 联动升(scripts/dev-version.mjs 强校验,失步硬报错)。**ABOUT_VERSION 已退役**:关于 tab 随自研卡片删除,`lib/client.js` 不再有该常量,dev-version.mjs:16 把该 target 标为 `required: false`(缺失即跳过,ADR-0006 D3)。0.7.1 = ADR-0008 修复定版(umcommit 面板 2026-10-18 拍板升 patch);ADR-0007 的窗口修订曾沿用 0.7.0 基版 | | provider id 族 | 规范 `um-web-search` · 兼容别名 `exa`(同一伞实例薄包装,共享 available/search/prime) | lib/index.js:33(UM_PROVIDER_ID) · lib/index.js:35(EXA_PROVIDER_ID) · lib/index.js:1198-1199(双注册) · cordis.patch.yml:39(searchProvider) · README.md:213(两 id 可用未配置即报错) · README.en.md:213 | 退役别名需同步五处;别名实例的 keyPresence 读 `UM_WS_EXA_API_KEY`(lib/index.js:1155) | | provider 后端 id 族 | `exa` · `parallel` · `deepseek`(BACKEND_SPECS 只含 exa/parallel;deepseek 走官方类包装,free 档强制关闭) | lib/index.js:573(BACKEND_SPECS) · lib/index.js:575-606(projectProvider) · lib/index.js:404-426(BUILTIN_PROVIDERS) | 新增 provider = BUILTIN_PROVIDERS 加项 + BACKEND_SPECS 加 spec(deepseek 型需伞内特判 `isDeepSeek`,lib/index.js:574/597);client.js `KEY_REF_BASE`(lib/client.js:57-61)同步 | | settings namespace | `um-web-search`(= profile Loader entry id,不再是插件自选名) | lib/index.js:1121(WEB_SEARCH_EXA_SETTINGS_NAMESPACE) · cordis.patch.yml:14(insert 行 id) · lib/client.js:29(SETTINGS_NS) | 三者是**同一个字符串**:行 id 即命名空间,改名 = 存储位置迁移(设置写在 profile patch 的行 `config` 里),旧 `web-search-exa` 段不再被读取(破坏性,须在 CHANGELOG/README 声明);常量保留只为给调用方与索引一个源 | | 凭据写入接口 | 客户端经 `ctx.remote.credentials.set(ref, value)` **位置参数**写密钥值、`describe([refs])` 读状态(`{ok, value: {ref: {configured, writable}}}`);密钥值绝不入 settings config;**防泄露:只消费 configured/writable、已存值绝不回显(•••••• 仅存在性占位)、值输入走官方 `SettingsSecretField`、非空白 trim 语义、非法 ref 记为未接受而非静默跳过** | lib/client.js:524(set(ref, literal)) · lib/client.js:550-557(describe) · lib/client.js:652-661(SettingsSecretField) · lib/client.js:514-531(writeCredentials) | 旧的 `connection.api.credentials` envelope 在 0.1.7 已消失,**禁止再出现**;接口缺失/抛错 → 优雅降级(accepted=false,不写盘);留空值=保持当前凭据(官方语义);失效监听 `ctx.remote.$on("credentials/reference-updated")`(lib/client.js:1044) | | 凭据引用名模式 | `^[A-Za-z_][A-Za-z0-9_]*$`(权威实现 = `isCredentialRefName`,来自 `@deepseek-ai/dsh-credentials`) | lib/index.js:101-103(isValidApiKeyEnv 转调 isCredentialRefName) · lib/index.js:683(credentialRef 包裹) · lib/index.js:1090-1094(迁移路径包裹) · lib/client.js:42(KEY_REF_PATTERN 本地镜像) | Host 侧**不再有本地正则镜像**(ADR-0006 D2):语法由凭据 seam 拥有,插件只转调并把 ref 用 `credentialRef(...)` 品牌化。客户端仍保留一处镜像(lib/client.js:38,用于写前预检 lib/client.js:360)——seam 语法变更时改这一处 | | UM_WS_ 凭据引用族 + 迁移对 | `UM_WS_EXA_API_KEY` / `UM_WS_PARALLEL_API_KEY` / `UM_WS_DEEPSEEK_API_KEY`;迁移对 `EXA_API_KEY→UM_WS_EXA_API_KEY`、`PARALLEL_API_KEY→UM_WS_PARALLEL_API_KEY`(仅拷贝、**绝不删除旧引用**;惰性回退在付费解析链内兜底) | lib/index.js:54-56(常量) · lib/index.js:61-64(迁移对) · lib/index.js:690-700(resolveCredentialWithLegacy) · lib/index.js:1085-1100(migrateUmwsCredentials) · lib/client.js:57-61(KEY_REF_BASE 建议值) | 改名 = 凭据迁移;旧名是共享环境变量时删除即误伤——禁止 unset/delete;新增 provider 需同时补 KEY_REF_BASE 与 client 的 `nextKeyRef` 生成(lib/client.js:220-226) | | snippet 双上限 | 请求侧 700(REQUEST_SNIPPET_CHARACTERS)· 本地截断 800(SNIPPET_MAX_CHARACTERS) | lib/index.js:43 · lib/index.js:45 · 使用点 lib/index.js:228(EXA buildRequest) · lib/index.js:258(PARALLEL buildRequest) · lib/index.js:307/343/362(map 截断) | 两值语义不同勿合并;调整任一需复核另一 | | 托管端点族 | Exa `https://mcp.exa.ai/mcp`(匿名 MCP)· `https://api.exa.ai`(REST)· Parallel `https://search.parallel.ai/mcp` / `https://api.parallel.ai` · DeepSeek `https://api.deepseek.com/anthropic/v1` | lib/index.js:68-71 · lib/index.js:79 · lib/index.js:404-426(BUILTIN_PROVIDERS 引用) | 托管地址;自建代理时在设置里覆盖对应档位 baseURL(client.js 的 DraftField 直接编辑)。`SEARCH_BASE_URL_ENV = "EXA_BASE_URL"`(lib/index.js:77)是唯一的启动环境兜底键 | | MCP 协议版本 | 2025-06-18 | lib/index.js:41(MCP_PROTOCOL_VERSION) · lib/index.js:919(initialize params) | 与两端服务端协商;升级时复核 | | MCP 搜索工具名族 | Exa `web_search_exa` · Parallel `web_search` | lib/index.js:210(MCP_TOOL_WEB_SEARCH) · lib/index.js:222(EXA_SPEC.mcpTool) · lib/index.js:238(PARALLEL_SPEC.mcpTool) | 与各端工具清单一致;改名需同步 | | Parallel MCP 会话 id | `um-dsh--` 截断 ≤100 字符,**每次搜索尝试重新生成**(0.6.0 起不再实例级稳定——并发扇出下各源需要独立会话) | lib/index.js:948(searchAnonymous sessionId) | 免费层按 session_id 限流;改格式注意 100 字符上限 | | MCP 双端握手差异 | initialize 后发送 `notifications/initialized` 仅 Parallel(`mcpPostInitialize: true`,EXA_SPEC 为 false);**响应既可能是 SSE `data:` 帧(Exa)也可能是裸 JSON body(Parallel 实测)**——mcpSseData 双格式兼容 | lib/index.js:225(EXA mcpPostInitialize:false) · lib/index.js:251(PARALLEL mcpPostInitialize:true) · lib/index.js:936-947(通知点) · lib/index.js:140-172(mcpSseData 双解析) | best-effort:通知失败不中断搜索,abort 仍生效;改任一端 framing 需回归双格式用例 | | 配置默认值(0.6.0 providers 模型 + 0.1.7 volatile) | enabled:false · defaultProvider:"exa" · concurrency:1(1–8) · cache:{enabled:false,ttlSeconds:60}(1–86400) · providers=内置三源(exa 开、parallel/deepseek 关;数组索引=优先级);volatile 面 = enabled/defaultProvider/concurrency/cache 整对象/providers 整数组 | lib/index.js:442-451(Config + volatile) · lib/index.js:404-426(BUILTIN_PROVIDERS) · lib/index.js:72-81(单值默认) · lib/client.js:39-40(DEFAULT_NUM_RESULTS/DEFAULT_CACHE_TTL_SECONDS) · lib/client.js:48-55(F 字段名) · tests/render.test.mjs:390-399(fixture 默认) · tests/render-visual.mjs:35-72(fixture 快照) · tests/visual.spec.mjs:145(顺序断言) | 覆盖徽章/状态行判定依赖多处一致;**数组元素不可 volatile**(schemastery 3.18.4 src/index.ts:488-509 对数组 inner 强制 blocked),故 providers 整数组标 volatile、写入走单条 `{op:'set', path:['providers'], value}`(lib/client.js:486);任一处改动需同步 | | 服务端拒绝降级状态码 | 401/402/403/429/5xx(422 明确不降级) | lib/index.js:128-130(isDegradableStatus) | 与 Exa/Parallel 非 2xx 语义一致;deepseek 三档分类复用此表(ADR-0004 D7) | | deepseek 降级三档分类 | 消息含 `(HTTP NNN)` → 按状态表判定;已知不可降级形态(无 web_search_tool_result / unprocessable body / search request failed / credential resolution failed)→ 不降级;其余 detail-bearing `WEB_PROVIDER_ERROR` → 降级 | lib/index.js:980-1005(searchDeepseek catch,判定 996-1003) | 官方类不暴露状态,此分类依赖其消息形态——升级 dsh-web-search-deepseek 时复核 | | 缓存上限 | CACHE_MAX_ENTRIES = 64(插入序逐出最旧) | lib/index.js:83 · lib/index.js:1017-1025(cachePut) | 增大需评估内存;缓存键 = query+maxResults+defaultProvider+concurrency+全 provider 指纹(cacheFingerprint,lib/index.js:1007-1015);失效靠 `loader/volatile-update`(lib/index.js:1189-1192) | | 零存储迁移 | 判定收紧为 `hasLegacyKeys(section) && hasPristineProviders(section.providers)`:21 个旧扁平键任一存活 **且** providers 仍是 pristine BUILTIN 默认 → 合成 providers[];新卡片保存过一次(providers 非 pristine)后旧键退化为惰性残留、新模型优先 | lib/index.js:453-460(LEGACY_KEYS) · lib/index.js:492-494(hasLegacyKeys) · lib/index.js:607-609(hasPristineProviders) · lib/index.js:620-623(判定点) · lib/index.js:516-563(synthesizeLegacyProviders) | **客户端不再有 LEGACY_KEYS 镜像、也没有 unset 循环**:0.1.7 下旧键非 volatile,设置服务 `validatePaths` 会拒写(无法清除),故只做一次性读取合成(ADR-0006 D2 迁移优先级修订)。新增旧键只需改 Host 一处;`preferred` 仅在 legacy 判定成立时压过 `defaultProvider`(lib/index.js:638-640) | | 设置 UI 分层机制 | 卡片 = 官方 primitives:`SettingsForm` + `SettingsFormModel`(字段 spec `settingsTextField`/`settingsNumberField`,写-only 控件承载 providers/cache/keys),值控件 `SettingsValueField`、密钥 `SettingsSecretField`;挂载 = `ctx.configForms.get("um-web-search")` + `ctx.configForms.whileServed([NS], …)` → slot `plugins.row.config`,key = `#` = `um-dsh-websearch#um-web-search`;**无自研 CSS**(仅内联结构 style + 一个主题 token) | lib/client.js:29(SETTINGS_NS) · lib/client.js:33(SLOT) · lib/client.js:35(ROW_KEY) · lib/client.js:37(ORDER=60) · lib/client.js:338(controller) · lib/client.js:901-909(SettingsForm) · lib/client.js:611/652(Value/SecretField) · lib/client.js:1039/1047(挂载) | 自研 CSS 命名空间 `um-dsh-websearch-*`、Modal/tab/手风琴已退役(ADR-0006 D3);**唯一保留的视觉常量**是 hint 主题 token `var(--dsw-alias-label-tertiary)`(lib/client.js:68)——新增颜色/圆角/字号一律走 primitives。provider 列表与 cache 是整值 volatile,模型无法按标量寻址 → 走第三参数写-only 控件,序列化后单条 `scope.mutate`(lib/client.js:344)。命名空间、slot key、服务注入三者必须与 package.json 的 client inject 边一致 | | dev 版本链路 | dev 版 = 基版+14 位 `yyyyMMddHHmmss` 版本码(秒级唯一、严格单调);`pnpm dev:version` bump / `pnpm dev:version:reset` 回基版;同步 package.json · lib/index.js VERSION(**client 目标已退役为可选**);失步硬报错;历史入 `.um.agents/memory/dev-versions.local.md`(本地);dev 版不提交/不 tag/不进 CHANGELOG | scripts/dev-version-core.mjs(纯逻辑) · scripts/dev-version.mjs:14-17(TARGETS,client 目标 required:false) · scripts/dev-version.mjs:27-37(校验+回写) · package.json:14-15(scripts) · ADR-0003 | 版本常量只能经脚本改;umcommit/umrelease 仅作用于基版。ABOUT_VERSION 退役后脚本对该 target 缺失即跳过(不再硬报错),新增版本化常量必须同时登记 TARGETS | | 测试套件构成 | node:test 合计 **175** 用例:plugin.test.mjs 85 + boundary.test.mjs 37 + render.test.mjs 47 + version.test.mjs 6(`pnpm test`,package.json:16 按文件显式传参);Playwright 视觉/交互套件 **16** 用例(tests/visual.spec.mjs,`pnpm test:visual`)独立 runner,**不计入 node:test** | package.json:16-18(test/test:visual/test:visual:update) · tests/plugin.test.mjs · tests/boundary.test.mjs · tests/render.test.mjs · tests/version.test.mjs · playwright.config.mjs(testDir/testMatch/单 worker/1280×900/`maxDiffPixelRatio 0.01`) | 目录级 `node --test tests/` 会误跑 fixture 生成器 render-visual.mjs——按文件显式传参;host 侧用例(plugin/boundary)导入 `lib/index.js`,需要依赖族已安装(`pnpm install`),client 侧用例(render)自带 React/primitives 桩、不依赖 node_modules;改 fixture 后必须先 `node tests/render-visual.mjs` 再跑 visual;用例数变动时同步本行 | | Playwright 视觉测试架构 | fixture 生成器 render-visual.mjs 内联**已发布的 lib/client.js**(生成期读取,tests/render-visual.mjs:32)产出单页 `tests/visual/fixtures/card.html`,页面内含三块桩:mini React(tests/render-visual.mjs:114 起)、stub primitives(tests/render-visual.mjs:271 起,只镜像 props 契约与回调,不镜像像素)、mock DSH ctx(tests/render-visual.mjs:556 起,`configForms.get/whileServed` + `remote.credentials.describe/set` + `remote.$on`,日志全进 `window.__testLog`,tests/render-visual.mjs:677);spec 通过 `file://` 打开该页做行为断言 + 少量自有布局截图 | tests/render-visual.mjs:32(内联源码) · tests/render-visual.mjs:35-72(fixture 数据) · tests/render-visual.mjs:745-746(写 card.html) · tests/visual.spec.mjs:9-23(被测页与再生成命令) · tests/visual.spec.mjs:36(`__testLog.ready`) · tests/visual.spec.mjs:340-352(3 张截图) · playwright.config.mjs | **范围由 ADR-0005 D3 修订**(ADR-0005:7-9):插件不再拥有自研 CSS/像素级 DOM,Playwright 只验证「插件自有组合与交互行为」,官方组件自身渲染由框架负责——旧的多页 fixture(index/interactive/lab.html)与 5 张像素基线作废,新基线 `tests/visual.spec.mjs-snapshots/*.png` 需 `npx playwright test --update-snapshots` 重拍。改 fixture 必须重跑生成器;断言依赖 `window.__testLog`(state/scopeCalls/credSetCalls)与 `data-testid`(card-root/details-modal) | | 配置校验分层(写前双闸,Unreleased) | 宿主:预持久化 `internal/config` 瀑布全局生效,但按 `this.runtime` **双判别限定本插件行**(`Config` 模块实例身份 ‖ 导出名 `web-search-exa`;`{global: true}` 注册,判别模式与 loader 自身监听器一致 plugin-loader lib/index.js:604)——修复 Models 页 `llm-pi-ai` dict 形状 `providers` 被本插件 array schema 误拒(`$.providers expected array but got [object Object]`);`apply` 对本 section 预校验(不变量违规加载即失败);空 `providers` 数组按内置三源做成员归属(与 resolveOptions 运行时语义一致)。客户端:保存前构建「将产生的完整文档」(committed + 全部 plan op + write-only 序列化值)**镜像校验同一组不变量** + 并发 1–8;腐坏 JSON 草稿**整单拒绝**(防部分写入——表单模型先落标量再跑 write-only 处理器);committed `providers` 为非数组对象 → 回退内置渲染 + 双语警示 + 拒绝保留污染形状的保存;provider id 保存时 trim | 宿主 `lib/index.js:1179(瀑布监听器)` · `lib/index.js:1151(apply 预校验)` · `lib/index.js:469(validateConfig)`;客户端 `lib/client.js:282(validateCandidate)` · `lib/client.js:427(candidateSection)` · `lib/client.js:459(saveGuarded)` · `lib/client.js:69(BUILTIN_PROVIDER_IDS 镜像)` | 宿主不变量与客户端镜像必须一致(`KEY_REF_PATTERN`/`BUILTIN_PROVIDER_IDS` 是客户端两处本地镜像,seam/内置族变更时同步);cordis `dispatch.bind(thisArg)` 语义变更时复核判别;e2e 脚本 `.workbuddy/tmp/verify-scoped-waterfall.mjs`(真实 cordis dispatch 三断言)可重跑;双语警示文案键集中在 lib/client.js COPY 表 |