# 更新日志 本项目在此之前没有成文的更新日志:二十轮构建都顶着 `0.1.0`,两处版本号(`package.json` 与客户端的 `CLIENT_VERSION`)互不校验,也没有记录任何一次改动的用户可见后果。本文件从 `0.2.0` 起逐版记录,并把规则写下来: - **minor(0.x)**:一批已完成的用户可见改进。这里的一批可以是若干次提交,只要它们合起来 回答同一个问题。 - **patch**:同一批之内的修正(文档、探针、测试、注释),用户可见行为不变。 - **major**:尚未发生。首次发布到 npm 之前不提升 major;一旦发布,破坏性变更(HTTP API、 注册表 schema、数据文件格式)提升 minor 并在条目里显式标注「破坏性」。 - **版本号有两处**,必须同时改:`package.json` 的 `version`(宿主与更新检查读它,面板身份行 也读它)和 `client/client.js` 里的 `CLIENT_VERSION` 字面量(打包进页面的那份自述)。二者 由 `tools/client-smoke.mjs` 断言相等,并断言本文件的最新小节就是该版本——一份停在旧版本的 更新日志,和一枚说谎的版本徽章是同一类问题。 - **批次号从历史取,不靠记忆**:提交标题里的「第 N 批」,必须是 `npm run batch:next` 当场 打印出来的那个数(它读 `git log` 里已有的最大号再加一)。这个仓库同时被不止一个会话编辑, 各自记得「上一批是 42」就会各自写下 42——第 40、41、42、43 批都这样撞过一次。 ## 0.12.8 — 2026-10-18 ### 批次号不再靠记忆 - 新增 `tools/next-batch.mjs`(`npm run batch:next`):读 `git log --format=%s`,用 `/第\s*(\d+)\s*批/u` 取已有的最大号,加一打印成 `第 N 批`;`--bare` 只打印数字,供脚本拼接。 - 起因是重号:第 40、41、42、43 批各自出现了两个提交,因为两个会话在同一个工作树上编辑,都按各自的记忆报了下一位。规则改为「提交前当场问历史」,本文件开头也写进了这条。 - 第 40/41 批保持现状(改写要穿过另一个会话的提交再强推 main,收益不抵风险);从 0.12.8 起不再产生新的重号。 ## 0.12.7 — 2026-10-18 ### 包里的 README,图片不再断 - `files` 里补上 `docs`:README 的两个语言版本都用相对路径引用 `docs/images/*.png`,而 `files` 决定包里有什么——此前从 GitHub 或 npm 装下来的那份,README 的每张图都是断链。截图总计约 450KB,换来包里文档自洽。 - 新增 `docs/install-check.md`:把「从 GitHub 装一次」的实测完整记录下来(2.4 秒、无构建授权、lockfile 钉到某个提交、装完自动启用、唯一的 peer 警告及其含义、移除后 profile 复原),并回答「为什么不发 npm 包」。 ### 从 Git 源装的,不再去问 npm 有没有新版 - `lib/self-update.js` 新增 `isGitSpec(spec)`,识别 pnpm 接受的几种 Git 写法(`github:owner/repo`、`git+https://…`、`git://…`、裸 `https://….git`,均可带 `#tag`)。 - 命中 Git 源时自更新检查直接返回 `reason: 'git'`,不再请求 npm。在此之前它会走到 npm 查询分支、拿到必然的 404,界面于是显示「npm 上查不到 dsh-overall-mcp 的发布版本」——真话,但答非所问:这个包从没打算发到 npm。现在给出的提示是该执行的那条命令(`dsh plugin add `,想钉版本就加 `#`)。 - `updateSelf` 在这类安装上不会执行任何命令(此前会拼出 `dsh plugin add dsh-overall-mcp@`,那是个必然失败的命令)。 ## 0.12.6 — 2026-10-18 ### 装过的服务器,市场里不再说「安装」 - Smithery 的托管条目在列表里没有端点(要等安装时问详情接口),因此**建不出注册表条目**;上一版拿「能不能建出条目」判断装没装,于是 brave-search 已经装好、市场卡片上仍是一个「安装」按钮。现在名字单独算(`proposedNameOf`,只取决于标题),装没装按名字比对。 - 结果:装过的卡片显示「已安装」,点不进重复安装;要重装仍可以——先在注册表页移除那一行,再回市场装。 ## 0.12.5 — 2026-10-18 ### 检查结论不再钉死在失败上 - 「检查」写下的结论按会话保存,**成功时会被自动升级**:一行的旧结论失败、但此刻的事实(挂着、握手过、工具有)已经成立,列表会把结论换成一次真实的通过记录,不必等用户再点一次检查。装完客户端就当场看工具数的那次动作,最容易抓到「连接刚建好、工具还没注册」的一瞬间,旧行为会把这 12 秒的结论一直挂到重启。 - 判定抽成一个函数(`healthVerdict`),列表兜底、手动检查、后台自愈共用同一套事实,不再各写一份。 ## 0.12.4 — 2026-10-18 ### 授权说明不该点开是一页 JSON - 一个远端端点回 `401` 并给出 `resource_metadata`,意思是「去问授权服务器」——那是一个给程序读的文档,不是发凭据的地方。上一版把它当成**授权说明 →** 的链接,点开就是一页 `{"authorization_servers":[…]}`,读的人看不出下一步该做什么。 - 现在会真的去读那份文档:读到 `authorization_servers` 就说明这是 **OAuth 登录**(浏览器授权,插件这边走不完,装上也调不通),并把链接换成这台服务器**自己的页面**(Smithery 托管的端点从 `<名字>--<作者>.run.tools` 直接推出 `smithery.ai/servers/@<作者>/<名字>`);读不到也仍按 OAuth 处理,只是不点服务器名。 - 表单文案跟着分岔:OAuth 的写「除非服务商另外发了 API key,按 `Bearer <令牌>` 填在这里才能用」;普通 `Bearer` 挑战仍写「按文档填」。旧的 `Authorization` 字段(带 `resourceUrl` 的那种)在「检查更新」时重探一次并自愈,不必重装。 - 不变量:机器可读的元数据 URL 永远不会出现在标着「去获取」的位置。 ## 0.12.3 — 2026-10-18 ### 灯也不许说绿 - 有一个 fiber 不等于能用。挂上了、握手也过了、一个工具都没注册的那一行,圆点一直是绿的——因为灯只按 `connected` 上色,而 `connected` 只回答「有没有 fiber」。现在最近一次「检查」的结论排在它之前:没过就是黄灯,行下行为「已连接 · 检查未通过」,红字仍点名是哪一项没过。 - 结论只活在问它的那个会话里(`index.js` 的 `health` Map),所以这是「刚查过,没过」,而不是一个会跟着行一起存下去的永久状态;重新进面板不会继续挂着黄灯,除非再查一次。 ## 0.12.2 — 2026-10-18 ### 要填的键,顺手给个「去获取」 - 配置表单在字段下面给两个链接:**去获取 →** 指向描述里写明的那个页面(发布方的句子本来就有,不必让人从一句话里抄 URL),**授权说明 →** 指向端点 401 里给出的授权元数据。两个都在侧栏浏览器里打开,侧栏打不开就走系统浏览器。 - 链接只从**声明**的文字里认:错误信息里的 URL 是证据,不会被当成注册页。早先写下的字段没存链接的,也从发布的描述里补出来。 - 这两种链接回答的不是同一个问题——「去哪儿拿」和「它要哪种凭据」——所以分成两个名字,不合并成一句。 ## 0.12.1 — 2026-10-18 - 修:从面板安装托管型 Smithery 条目**永远不会成功**。市场响应里 `raw` 被有意去掉(那是上游整条记录,页面不该背),只把安装需要的一个字段提成 `qualifiedName`;宿主却只读 `raw.qualifiedName`,于是详情端点一次也没被问过,报错还写成「详情端点没有给出 deploymentUrl」——把「没问」说成「对方没给」。现在两处都从 `qualifiedNameOf()` 取名字,宿主合同测试也改成面板真正发回来的形状。 - 修:详情请求失败(429/超时)不再伪装成「它没给端点」,而是如实说「详情端点这次没答上来(HTTP 429)—— 等几秒再点一次」。 - `fetchSmitheryConfig` 失败时返回带 `error`/`status` 的对象而不是 `null`,并补上记录顶层 `deploymentUrl` 的兜底(新记录不再只在 `connections[]` 里给端点)。 ## 0.12.0 — 2026-10-18 ### 检查,按下去就说在查 - 「检查」有了行内状态:点下后按钮变成「检查中…」并禁用,直到结论回来。一次检查等握手最多 12 秒,之前这 12 秒按钮毫无变化,读起来像没点上——于是被点了第二、第三次,每次都在同一行上再起一次检查。 - 一次点击会跟着同一个挂载连问最多 3 次(间隔 4 秒)。首跑的 `npx` 可能还在下载,中间态不再被当结论写下来;按钮在整段时间里都显示「检查中…」。 ### 旧装法的行,说清它为什么红 - `@smithery/cli run …` 装下的行,握手不会完成——原因不是慢,而是 CLI 把记录解析成远程端点,拉起 `mcp-remote` 等浏览器登录。逻辑在 0.11.0 已经改掉(新装的行按来源装成远程 http),但已经装下的行不会自动改变。健康结论失败时,行下因此补一句「移除后重新安装,会按它自己托管的方式装成远程 http」,把唯一有效的下一步写在红字下面。 ## 0.11.0 — 2026-10-18 ### 装,就按来源写的方式装 - `catalog:smithery` 的条目现在分两种装法。Smithery 自己托管(`remote: true`)的,装成远程 http——端点去详情端点要(`connections[].deploymentUrl`);只有他们不托管的,才走 `npx @smithery/cli run`。 - 之前一律装成后者,而它并不是一台本地服务器:CLI 把记录解析成同一个远程端点,拉起 `mcp-remote` 等交互式 OAuth 登录。无头挂载里这一步永远不会完成——实测 `run upstash/context7-mcp` 20 秒零响应,日志停在 `Another instance is running the sign-in for this server on port 4814`。这类行的状态因此永久停在「正在连接」。 - `docker run` 的条目把存下的环境变量变成 `-e KEY=value`,并且插在镜像之前。docker CLI 只读它自己的进程环境,`config.env` 从来没有进过容器;而写在镜像之后的参数会被当成容器自己的命令。 ### 必填,只信端点的 401 - 新 `lib/remote-auth.js`:对 http 端点发一次 `initialize`,把 `401` 和它的 `WWW-Authenticate` 读成一条必填的 `Authorization`。读不出来(超时、`5xx`、端点拒绝探针)就一条都不加——「不知道」不再冒充「不需要」,也不冒充「需要」。 - 这样来的字段在表单里标成「这个端点自己要求的」,与「发布方声明的」分开,一眼能看出证据来自哪里。 ## 0.10.0 — 2026-10-18 ### 检查结论补上耗时 宿主一直在给握手计时:`health.ms` 从写入结论的那天起就在内存结论里,只是那一行没读它—— 读者看到「检查健康 · 5 个工具 · 08:31:07」,知道它**能**用,不知道它**慢不慢**。 现在结论行是「检查健康 · 5 个工具 · 41ms · 08:31:07」。单位跟着量级走:不足一秒报毫秒 (`41ms`),超过一秒报秒(`1.2s`);没有计时就不编一个(旧结论缺 `ms` 时这一段整段不出现)。 ### `--config` 行的两个承诺 `--config` 这类服务器把凭据写在一个 JSON 参数里,两个后果以前都没兑现: - **必填留空仍可保存**。发布方在 schema 里说了 `required: ['braveApiKey']`,表单只在标题上 标一个「必填」,保存照发——于是拿到一次注定失败的重新挂载,再回来读「还缺 braveApiKey」。 现在必填未填时保存按钮禁用,上方点名还差哪几个键(`还有必填项没填:braveApiKey`), 输入框里只有空格也算没填;把已保存的必填值「清除」同样挡住——表单不该产出这一行已经 知道是坏的形态。 - **看不见最终参数**。字段框里显示的是你输入了什么,不是 CLI 会收到什么;手加的键(「其他 键值」里 `配置项` 那类)更是只存在于提交之后。现在表单底部直接给出成形的那一段 `--config {"braveApiKey":"••••","extraMode":"fast"}`,密钥一律以 `••••` 出现,手加的键 也在里面。 ### 握手那一项要真的等 「检查不健康:握手未完成(8s 内没返回)· 21ms」——这两段时间不属于同一件事。前一句里的 8 秒是挂载自己的就绪期限,后一个 21ms 才是这次检查实际花掉的时间:那时它**一眼都没等**, 只读了一枚上次挂载留下的标记。把没观察过的事写成结论,比不检查更糟。 现在检查遇到「正在握手」的行会先等它一次,上限 12 秒(比挂载自己的期限长:这是用户唯一主动 问「到底能不能用」的地方,值得等;扫描和开关不能等,它们照旧立即返回)。等到了就连工具数 一起报健康;等不到才说不健康,并且句子里的秒数是**这次真的等过的**秒数。 顺带把原因写进话里:`@smithery/cli run ` 并不是本地 stdio 服务器——它把服务器解析成 远程 HTTP,再拉 `mcp-remote` 走 OAuth,等人交互登录。无头挂载里没人能应答,`initialize` 永远 不来,于是行停在「正在连接」。这类行现在会明说「这条命令可能要求交互式登录,或首次运行正在 下载」,而不是让人以为只是超时短了。 两条都由 `tools/render-config-dialog.mjs` 断言:必填为空时按钮禁用且点名、已保存时放行; 预览字符串逐字比对,并断言预览里不含明文凭据。 ## 0.9.0 — 2026-10-18 ### 「检查」的结论只活一次 上一版把体检结论写进了 `registry.json`:`row.health = { at, ok, toolCount, checks, ms }`。 写进文件的那一刻它就变成了这台服务器的属性——可它回答的是「此刻能不能用」。宿主重启过一次, 行上还在替一个已经不存在的进程报「检查健康」;用户的原话是「点击一次,只在本次显示,下次进来 之后就不会显示」。 现在结论有两道闸: - **宿主侧**:结论只存内存(`health: Map`),`publicList()` 从它读取; `/health` 不再 `save()`。旧文件里已经写下的 `health` 在启动时被删掉并回写,所以升级后 第一次启动就把注册表清干净了。 - **页面侧**:`keepSessionHealth()` 只保留「本次进入界面亲手点过检查」的那几行 (`checkedThisSession`)。刷新页面 = 新一轮会话 = 一行结论都不显示,直到你再点一次。 - 两道闸都必要:只有内存,刷新页面仍会看到本进程早先的结论;只有页面闸,重启宿主的旧结论 还会被当成事实重新画出来。 ### 说明文字说读者的话 每行那句「这台服务器是干什么的」取自服务器自己的 `initialize` 说明,语言由它的作者决定。 中文面板上于是出现「一行英文夹在中文里」: - 新增 `POST /api/overall-mcp/about-i18n`,只翻 `source: "server"` 的散文;本插件自己写的 `工具:…` 和启动命令原样返回(`source` 由客户端标出,没标的一律当散文)。 - 译文按「语言 + 原句」缓存在 `about-i18n.json`(**不在** `registry.json` 里:它是派生缓存, 删掉只花一次模型调用)。同一句话整台机器只翻一次,重启也不算重复。 - 语言来自页面的 ``,退回 `navigator.language`;`zh-CN` 与 `zh` 共用一个键。 - 模型不可用(`no-llm` / `no-route`)或答非所问(`translate-failed`)时,行上保留服务器 原本的措辞——「翻译不了」不该比「看清这台服务器干什么」更重要,也不该变成一条报错。 - 译文在行上,原始措辞留在 tooltip(`about.original`),需要核对原文时仍拿得到。 ## 0.8.0 — 2026-10-18 ### 必填只信声明 上一版把「声明」读进了配置表单,但「必填」这一栏还留着一条自己的推断:启动命令里出现 `--env GITHUB_TOKEN`,或者一个形如凭据的大写令牌,就按名字猜它是必填。两种错都出在这条 推断上——真正必填的键可能因为名字不显眼而不算必填,而没人要求过的键会把整行顶成「待配置」。 被问到的原话是「MCP 不应该是个标准服务吗」:这话对,只是本插件把标准答案扔在了半路, `argFields` 在猜,而 `officialFields` 读到的 `isRequired` 一路没被标记成「这是声明」。 现在每一档证据都留了痕: - **发布方声明**(官方 Registry 的 `environmentVariables[].isRequired`、 `remotes[].headers[]`、Smithery 的 `configSchema.required`)→ `declared: true`,只有它 能产生「必填」「还缺 xxx」「待配置」。 - **从启动命令认出来的键** → 仍然列在表单里,标「来源未声明」,不计入缺项;表单顶上写明 「一个都没有标必填——只有发布方声明过的才算」。 - `default` 与 `choices` 也一路带到表单(`默认:readonly · 可选值:readonly / readwrite`), 并同时认 snake_case 拼法:`server.json` 规范写的是 `environment_variables` / `is_required`, 服务端返回的是 camelCase,只认一种就等于只看了一半的目录。 - **同名服务器、不同记录,声明的东西可以完全不同**——这不是本插件的缺陷,是目录的现实: `io.github.brave/brave-search-mcp-server` 的包记录里 `BRAVE_API_KEY` 是 `isRequired: true, isSecret: true`;而 `ai.smithery/brave`(同一份代码的托管变体)只声明了 一条 `Authorization: Bearer {smithery_api_key}`,没有任何 `environmentVariables`。按它装的 行走的是 Smithery 自己的 `configSchema`(`required: ["braveApiKey"]`)。三类记录都算「声明」, 所以 `smitheryFieldsOf` 出来的字段同样带 `declared`,否则表单会把一个 schema 明写的键叫作 「来源未声明」。 ### 「检查」:这台服务器到底能不能用 圆点只回答「有没有一个 fiber」,回答不了「模型能不能调到它的工具」。进程死了、fiber 被 dispose、一个工具都没注册,行上照样是「已连接」——这是这一轮最贵的一个谎,因为它的后果 出现在工具调用时,而不是面板上。 每行多了一个「检查」按钮,把一次挂载拆成五项依次作答:启用、挂载、握手、工具、配置。工具数 按 `mcp____` 前缀在宿主工具表里数——不发请求、不花配额,也是唯一能证明「能用」 的信号。结论落盘成 `health`(`{at, ok, toolCount, checks, ms}`),刷新不丢;行上多一行 `检查健康 · 5 个工具 · 08:31:07`,不健康时把第一个 ✗ 的句子原样写出来(「一个工具都没注册: 服务进程可能已经退出」)。没被检查过的行不显示任何结论——一个会自己过期的健康判定,比没有 更难用。 `POST /api/overall-mcp/health` 收 `retry: true`,在行已启用却什么都没挂上时补一次挂载尝试, 于是「检查」要么把它救回来,要么带回真实的错误原文,而不是把「未挂载」原样还给点按钮的人。 `tools/render-server-health.mjs` 断言三种形状:没检查过不编造、健康带工具数与时间、不健康 点名失败的那一项。 ### 一个键的去处 配置表单把「不是请求头的键」一律写进 `env`。对 `--config` 型的服务器(Smithery CLI:`npx -y @smithery/cli run brave`)这是写错了袋子——它只读命令行上那串 JSON,`env` 里的 `braveApiKey` 它一辈子不会看。症状因此特别难查:保存回「已保存」,`configValues` 仍是空的, 行上继续写「还缺 braveApiKey」,看起来像表单没生效,其实是生效了但落到了没人读的地方。 现在按字段自己声明的通道分三袋:`header` → `headers`,`config` → `configValues`(挂载时序列化 成 `--config `),其余 → `env`。同一处修理还带出「其他键值」那行:`--config` 行上新加的 键默认就是「配置项」,提示也从「没有列出来的变量可以自己加」改成「写进那份 --config JSON」, 否则界面替用户选了一条这台服务器读不到的通道。 `tools/render-config-dialog.mjs` 直接断言这条路由:声明的 `config` 键必须落在 `config` 袋里, `env` 键必须落在 `env` 袋里,没人碰过的表单三个袋子都为空(这条同时挡住「把 `••••` 当值回传」)。 `test/index.test.mjs` 再从宿主侧验一遍:存进去之后 `composeMountArgs` 必须真的产出 `["--config", "{\"braveApiKey\":\"…\"}"]`。 「其他键值」还改了一处观感:那块不再预置一行空输入。一个空的输入框加一个空的类型下拉,读起来 就是「请填写」,而这一段恰好相反——它是**没有声明过的键**的兜底,可以永远不碰。现在只剩标题、 一句说明和一个「+ 加一行」,点它才出现一行;空行本来就不参与保存(键或值为空都跳过),于是 「选填」这件事在界面上和代码里终于一致。探针以「没有 移除 按钮」作为这条的断言:预置行必然 带一个 移除。 ## 0.7.0 — 2026-10-17 ### 页头多了一枚「点个 star」 版本号右侧、仅此一处:一枚胶囊形的 ``,指向本仓库,`target="_blank"` + `rel="noreferrer"`。它不参与面板的任何状态,也不读宿主数据——没有计数器,因为一个自己 攒的数字会把「请 star」变成「求 star」。 静止时只有一圈发丝描边和一颗琥珀色的星;指针压上去,星放大并倾斜 12°,胶囊上浮 1px、 描边转暖,背后浮起一层同色柔光,`.6s` 的斜向高光从左侧扫过一次。`prefers-reduced-motion: reduce` 下只保留颜色与描边,不动。 它排在身份行的最后(品牌标记、标题、版本号、〔更新到 v…〕之后),没有填充色,也不占 「每页一个主操作」的名额;两个标签页都在同一位置。`tools/render-market-tab.mjs` 断言了 它是一枚链接、指向本仓库、在新标签页打开,且两个标签页都有。 ## 0.6.0 — 2026-10-17 这一轮回答一个具体的事故:服务器装上了、挂上了、面板写「已连接」,然后每一次工具调用都回 `401`——因为它的 API key 从没填过,而插件从来没问过。目录里其实多数条目**已经声明**了自己 需要什么(官方 Registry 的 `environmentVariables` 带 `isRequired`/`isSecret`),这些字段此前 在安装时被丢掉了。 ### 配置声明:读出来、存下来、只出掩码 - 新增 `lib/server-config.js`:`configSpecFor` 从三处收声明取并集——官方 Registry 的 `packages[].environmentVariables[]` 与 `remotes[].headers[]`;条目自己的启动方式(`-e NAME`、 `--env NAME`、命令/参数/地址里的 `$NAME`/`${NAME}`/`{NAME}` 占位符、裸凭据形状的名字如 `GITHUB_PERSONAL_ACCESS_TOKEN`);`streamable-http`/`sse` 条目固定给一个**可选**的 `Authorization` 头。`origin` 记最强证据(`official` > `args` > `manual`)。声明为空的行不带 `config`,既有消费者看到的响应不变。 - 声明写进注册表条目(`config: { origin, fields }`),不是每次列表重算:徽标重启后还在,手改 过的文件也不会被二次判断。`maskedFields` 是唯一出口:密钥有值 `••••`、无值 `''`,非密钥 (base URL)原样回显;`env`/`headers` 本身永不出现在任何响应里。 - `GET /servers` 每行多 `config` 与 `needConfig`(只有**必填**项为空才为真),整体多一个 「已启用且还差配置」的计数。`lib/catalog.js` 的 `catalogItemToEntry` 在安装时把声明随行写入。 ### 写回去:一个动作、一个工具、一条路由 - `POST /api/overall-mcp/configure` 与模型工具 `overall_mcp_configure`:按字段名写 `{env, headers}`。空字符串等于删除该键(清除);非法名、请求头里的换行、会写到 `Object.prototype` 的键被拒绝并逐条回报。写入后**只重挂本来就启用的行**——停用行保持停用; 确实什么都没改时回 `saved: false` 并说明,不谎报「已保存」。 - 安装路径提前读声明:必填项还空着时,回复带 `needConfig`、`config.missing` 与「还差 …」的 note,且**跳过模型诊断**(`diagnosisSkipped: 'needs-config'`)。凭据形状的挂载错误同样跳过 ——目录已经写明缺哪个键,再让模型从 `401` 反推只会更慢更差。 ### 界面:在点坏之前就说 - 行上多一颗 **待配置** 徽标(没有它,`已连接` 与「工具全挂」长得一模一样),有配置声明的行 多一颗 **配置** 按钮(缺必填值时用主色);页顶在筛选行之上只写一次「有 N 台服务器装好了但 还差配置」并给「去配置」。 - 配置弹层(`ConfigDialog`)按声明列出字段(掩码输入、`必填`、说明),底部留一行自定义键值; 保存走 `/configure`,还缺必填项时不关闭而是继续列出缺什么。安装完成后若 `needConfig` 为真, 面板直接为刚写入的那一行打开弹层,而不是只弹一句提示。 ### 测试与文档 - `test/server-config.test.mjs`:17 条(声明来源/归一化/上限、掩码、写值与拒绝、缺口与状态、 凭据错误分类、`configSpecFor` 的并集与 `origin`)。 - `test/index.test.mjs` 新增 4 条合同:声明的条目以「待配置」装好且不劳烦模型;保存写入并回 掩码、清空即删除、无改动如实回报;停用行保存后仍停用且不产生挂载;同名行拒绝(409)而 `slot` 选中目标行。 - README(中英)、模型工具清单、HTTP API 清单同步;`package.json` 导出 `./lib/server-config.js`。 ## 0.5.0 — 2026-10-16 前两轮把两趟走查的启停交给读者,这一轮补上「看」:本机的普查索引现在可以当成一个来源来读, 代价是零请求;同时把 `package.json` 的三个仓库字段填成真实存在的页面。 ### 普查视图(`?source=census`) - `/catalog` 接受 `source=census`(`lib/catalog.js` 的 `CENSUS_SOURCE`):这一趟归一成 `github-mcp`,回复带 `aliasOf:'github-mcp'`。它**不是第七个来源**——普查那 ~1.8k 行本来 就以 `github-mcp` 的身份计入本页,多一个来源会让「共 N 条」重复计一次,而来源列表是公开 契约。「一个上游,一个数字」没变,缓存也各分一份(键里带 `|census`)。 - 这个视图**只读索引、一次请求都不花**:`fetchGithubMcp` 在 `indexOnly` 下只走索引分支并 立刻返回(没有索引时写「本机还没有普查索引:在 GitHub 访问卡里开始一次普查,建好后这里 直接读它,不发请求」),绝不落回「索引过期就顺手走一遍」那一段(视图下 `githubWalk` 固定为 `null`)。索引超过 24 小时只决定它标不标「旧」,不决定能不能读。 - 视图不动任何状态:不触发普查、不推进恢复走查、也不算「有人看过 GitHub」。 ### 面板 - 来源筛选里 `本地普查(索引)` 排在 `GitHub` 右边(不改 `MARKET_SOURCES`,那是计数键的 来源);数字行按读者选的那个名字写(`本地普查(索引) 1.8k`),名字取自选项自己的标签, 所以筛选器与数字行不会各说一套。 ### 仓库元数据 - `package.json` 填入 `repository`/`homepage`/`bugs`(`https://github.com/YunzhouLi-hub/dsh-overall-mcp`、 其 `#readme`、其 `/issues`),`main` 已推到该远端。此前留空是因为不写未经验证的地址。 ## 0.4.0 — 2026-10-16 第二趟走查(读仓库证据恢复安装命令)也从「打开市场就自己开始、只能靠卸载插件停下」变成一条 可以自己按的路由。普查回答「GitHub 上有哪些 MCP」,这一趟回答「这些仓库该怎么装」。 ### 恢复走查的动词 - 新增 `GET|POST /api/overall-mcp/install-evidence`:`GET` 只读状态、不触发任何请求, `POST {action:'start'}` 开始或从断点续跑,`POST {action:'stop'}` 中止在飞的那一次。 不带 `action` 的 `POST` 仍然等于 `start`——面板打开市场时发的就是这一种。 - `force: true` 才忽略新鲜度检查。不带 `force` 时,正新鲜的走查回 `skipped:'nothing-to-do'`, 没有普查索引可依据时回 `skipped:'no-census'`;两者都不是错误。 - 已经在跑时 `start` 不会再起第二个(回 `started:false` 且 `walking:true`);没有在跑时 `stop` 回 `stopping:false`(「没有东西可停」不是错误)。 - 停止发生在**两个请求之间**:走查每读一个仓库都会检查停手把手,并把 `signal` 一路传给 `readRepoFile` / `fetchText`,所以按了停止不会再等两千多个仓库。当前那条读完后收手, 已恢复的命令照旧落盘,`partial` 写明停在哪(「已手动停止(已试 N 个仓库,下次接着走)」)。 - 状态里新增 `ended: {reason, at}`(`user` / `dispose`)与 `plan`/`progress`:文件在跑的时候 只写「正在抓取」,走到第几个仓库只有宿主进程知道;「我按的停止」与「插件被卸载」也必须分开。 - `progressOf(id, kind)`(`lib/repo-install.js`)让主循环在每次开读之前记下「正在读哪个仓库、 第几个」,于是进度是**对着它自己的计划**报的(`已试 78 / 2.1k 个仓库`),而不是一句「正在抓取」。 ### 面板 - 「已注册」标签页的 GitHub 访问卡里新增第二行恢复状态(`查过 N 个仓库 · 恢复出 M 条安装命令 · 上次是手动停止的 · 没读完:…`),右侧按状态给动词:还有剩的写「接着恢复」(走查自己会跳过 已试过的行),跑起来了写「停止恢复」并报进度。 - 两行各自带自己的动词,因为这两趟走查共享零预算、零时钟:把它们都叫「普查」正是有人按错按钮 (点「继续普查」却在推动安装证据走查)的原因。 - 行上补回 `partial` 的原文(`没读完:停在 900 行:…`):此前那一行只在处于「没有正在跑」时才说时间, 没有说这一趟到底读完了没有,而「停止」正是为了让「没读完」可以发生。 ### 错误码 - `unknown evidence action`(例如 `{action:'stpo'}`)→ **400**,与 `unknown census action` 同规矩。 - `GET /census?action=...`、`GET /install-evidence?action=...` → **400**(`lib/http-status.js` 新增 `action must be sent as a POST` 规则)。读取面不改变状态:一次预取请求不该成为 「一次正在跑的走查到底停没停」的分界线。 ## 0.3.0 — 2026-10-16 普查从「只能等它自己开始、只能靠重启 DSH 让它停下」变成一条可以自己按的路由。 ### 普查的动词 - 新增 `GET|POST /api/overall-mcp/census`:`GET` 读状态且不触发任何上游请求(索引是磁盘读), `POST {action:'start'}` 不等缓存过期就把缺的仓库补上,`POST {action:'stop'}` 中止正在跑的那一次。 - 已经在跑时 `start` 不会再起第二个(回 `started:false` 且 `walking:true`);搜索额度未恢复时 不起步并回 `skipped:'quota'` 与 `waitMs`,而不是花一次请求去换一个 403。 - 停止会中止步调等待本身(`MIN_INTERVAL_MS` 6.5 秒的 `sleep` 变成可中断的),所以「停止」是 立刻生效的,不是「等这次请求回来之后」。 - 已收到的行在走的过程中就落盘,停止不丢行:下一次 `start` 从缺的仓库接着走。被停止的普查 **不会**顺带去抓安装证据(那只在自然结束时触发)——既然按了停止,就不该在别处继续花额度。 - 状态里新增 `ended: {reason, at}`(`user` / `dispose`)与 `stopping`:`crawlGithubRepos` 自己只报 「已中止」,而读者要区分「我按的」与「插件被卸载」,理由只有宿主知道,所以由宿主写下来。 ### 面板 - 「已注册」标签页的 GitHub 访问卡里新增一行普查状态(索引行数、读完/上次读到的时间、谁停的), 右侧按状态给动词:「开始普查」或「停止普查」(后者同时显示进度)。 - 面板不再需要重启 DSH 才能停下普查;页面刷新后这一行也照旧可读(它读的是 `GET /status`)。 ### 错误码 - `unknown census action`(例如 `{action:'stpo'}`)→ **400**,`lib/http-status.js` 新增 `unknown action` 规则。不拒绝的话它会读成「没有动作」并回一份状态,对发送者而言与成功 无法区分。 ## 0.2.0 — 2026-10-16 首个带更新日志的版本。以下内容在 2026-09 至 2026-10 的二十轮构建中陆续落地,此前均以 `0.1.0` 交付、无发布记录;这里按能力归类,不按提交顺序。 ### 面板与定位 - 侧边栏面板,两个标签页:「已注册」(这份配置里实际挂载了什么)与「MCP 市场」(开放目录里 还有什么可装)。搜索、分类、排序、分页、收藏、详情面板、一键安装与卸载。 - **身份行**(第十八轮):面板顶部显示正在运行的插件版本、宿主 pid、已运行时长、数据文件数与 日志条数。版本号本身说明不了进程加载的是哪份代码——面板状态里给出 `source.stale`(磁盘文件 比进程新)并提示重启 DSH。这一条是在一次真实误诊之后加的:2026-10-16 的「github-mcp 源不可用」 来自一个前一天启动、早于修复的宿主进程。 - **动作日志**(第十八轮):宿主把每次扫描、普查、安装、更新、挂载、来源增删写成一行 NDJSON(`events.ndjson`,上限 400 行 / 256 KB,超出时前置一条裁剪记录)。面板可折叠查看, 导出包含宿主信息与日志。 ### 市场聚合 - 五个开放来源:官方 registry、Smithery、Docker MCP Catalog、awesome-remote-mcp-servers、 awesome-mcp-servers;外加一个基于 GitHub 搜索的补充来源。同一仓库被多个来源收录时按仓库 键去重合并。 - **计数口径**(第二十轮):每源数字与「共 N 条」并排显示,因此两者必须同口径——现在是去重 合并后**计入本页**的条数,重叠部分单独写成「去重合并 N 条」,原始抓取数仍在宿主返回体里。 - **两种时间分开**(第二十轮):条目的「上游更新于」是仓库最近一次 push;列表的「列表读取于」 是本机的读取时间。此前两处都写「更新于」,用户无法分辨哪个时钟在说话。 - 目录缓存带条件请求(ETag / Last-Modified),离线或限流时读缓存并标注 `stale`。 - GitHub 不可达时的镜像链;搜索配额耗尽时如实显示为「等待」,不静默降级。 - 流行度数字给出取数口径(哪个来源的哪个字段、何时抓的)。 ### 安装、诊断与更新 - 一键安装:优先用来源自带的传输方式,必要时回落到包运行器(`npx` / `uvx`);同名手工条目 存在时拒绝覆盖(409),除非显式 `overwrite=true`。 - 安装失败自动诊断:把失败分解成可核对的事实,可选让模型解释(没有可用模型时给出原始错误, 不假装解释过)。 - 更新检查与自更新队列;安装记录 `installs.json`;注册表重复行与同名竞态的显式处理。 - **安装回复一义一名**(第二十轮):`server` 永远是服务器名字,`installed` 永远是与市场条目 同义的布尔。 ### 普查(GitHub 仓库索引) - 全量普查把每个条目的仓库元数据(stars、push 时间、README 入口、发布包)落到本地索引,供 面板离线读取。 - **凭据**(第十九轮):`GITHUB_TOKEN` 环境变量 → 数据目录 `github-auth.json` → 面板粘贴 → `gh auth token` → 匿名,五级来源;token 只发给 GitHub,日志与导出里只出现掩码。 **匿名路径保留**:有 token 只是更快、配额更高,没有 token 依然能跑完(更慢)。 - **续跑**(第十九轮):一次网络失败不再终结整场普查——失败按 `planned / attempted / remaining` 记账并退避重试,面板如实显示进度与重试次数。 ### 界面一致性(第二十轮) - **组开关有界并发**:分组总开关与扫描后的新条目挂载改为每名一次、最多 4 路并发。此前串行, 五个成员最坏 40 秒卡在「正在切换…」;并发下同名兄弟共享同一裁决,不会出现「另一个 mcp-client 已提供」这种不真实的宿主断言。 - **推断分类在详情与下拉里也标注**:分类胶囊只在卡片 `title` 里写过「按名称与描述推断」, 现在详情面板可见文本与下拉选项都带「(推断)」。 - **「无使用量数据」明说**:按「使用量最多」排序时,没有使用量数据的条目此前静默回落到质量分, 现在卡片上写明并说明它为何排在后面。 ### 工程化(第二十一轮) - **收藏路由的往返契约用例**:`on` 幂等、裸 `key` 仍然是翻转、写入落到 `registry.json`; 畸形 `on`(非布尔)从 500 降为 400。 - **两支持续探测进入 `test:probes`**(详情链接完整性、分类 chips 的显示/隐藏判定)。两者读实时 目录,因此在没有任何来源应答时以退出码 2 结束并写明「未验证任何东西」,而不是让「0 个死链」 在 0 个条目上冒充绿色。 - **CI 矩阵**(`.github/workflows/verify.yml`):每次 push / PR 在 Node 22 与 24 上跑 `npm run verify:ci`(语法检查、单元测试、注册表文档校验、渲染探针)。插件运行时零依赖, 所以没有安装步骤与锁文件;两个需要真实 Chromium 的视觉探针(`test:layout`)仍留在本地。 - **英文 `README.md`** 与 `CHANGELOG.md`;版本号与更新日志由探针互相钉住。