Overall MCP:一个蓝色中心连着五个卫星点

dsh-overall-mcp

English | 中文

version 0.12.8

**DeepSeek Harness 插件:首次安装自动扫描宿主机全部 MCP 配置并注册;实时聚合 6 个网络开放 MCP 目录共 5000+ 条目(官方 Registry / Smithery / Docker MCP Catalog / GitHub MCP / Awesome 远程 / Awesome 本地),外加一份本机的 GitHub 普查索引(实测 1758 行),并把这些仓库里能从证据恢复出来的安装命令带上出处补进行里,以卡片形式浏览与一键接入。** 每一轮改动背后的推理、实测数字与被否掉的替代方案记在 [docs/ENGINEERING.zh-CN.md](https://github.com/YunzhouLi-hub/dsh-overall-mcp/blob/main/docs/ENGINEERING.zh-CN.md):README 讲它做什么,那份文件讲它怎么变成这样的。范围更窄的一份是 [docs/install-check.md](https://github.com/YunzhouLi-hub/dsh-overall-mcp/blob/main/docs/install-check.md):一次从 GitHub 安装的实测——耗时、lockfile 钉住什么、以及这个插件为什么不发 npm 包。 ## 界面预览 下面是面板的真实截图:由 `tools/readme-shots.mjs` 用**真实客户端代码**(同一份 `client/client.js`)加一份固定数据渲染,不是手绘示意图,所以图和代码不会各说各话;`npm test` 会核对本页引用的每张图都确实存在。 ### 「已注册」:来源分组,以及 GitHub 访问卡里的两次走查 ![已注册标签页:按来源分组的服务器列表,顶部 GitHub 访问卡写着普查索引、上次停止原因与「接着恢复」](https://raw.githubusercontent.com/YunzhouLi-hub/dsh-overall-mcp/main/docs/images/registered.png) ### 「MCP 市场」:六个来源聚合成一面墙 ![MCP 市场标签页:搜索框、分类与来源筛选、每源计数行、卡片墙,卡片上有星标、许可、标签与媒体条](https://raw.githubusercontent.com/YunzhouLi-hub/dsh-overall-mcp/main/docs/images/market.png) ### 详情:一张卡片背后的全部事实 ![详情面板:仓库统计(Star/Fork/许可/语言/最近推送)、来源与作者、将执行的命令与复制按钮](https://raw.githubusercontent.com/YunzhouLi-hub/dsh-overall-mcp/main/docs/images/detail.png) ## 核心特性 | | 能力 | 说明 | |---|---|---| | 🔍 | 首装全盘扫描 | 安装后自动发现 Claude Desktop、Claude Code、Cursor、VS Code (Copilot)、Windsurf、Codex CLI、Gemini CLI 配置中的所有 MCP 服务器并自动注册 | | 🌐 | MCP 市场 | 自动发现市面上已有的 MCP:官方 Registry、Smithery、Docker MCP Catalog、GitHub MCP、Awesome 远程、Awesome 本地共 **6 源并行聚合,实测 5364 条**(不含 GitHub 源:它**读本机普查索引,有索引时不发任何请求**,见「GitHub 普查索引」一节;索引里那些仓库没有安装信息,宿主会在后台**从仓库证据恢复命令**并标出处,见「从仓库证据恢复安装命令」一节),无需输入关键词即可浏览,带来源筛选与搜索 | | ⚡ | 一键接入 | 市场条目直接安装:npm 包→`npx -y `,Python 包→`uvx`,托管端点→streamable-http,Docker 镜像→`docker run -i --rm mcp/`;社区清单条目提供仓库链接 | | 🧩 | 装好了还差配置 | 装好、挂上、显示「已连接」的服务器,仍可能每一次 `tools/call` 都回 `401`——因为 token 从没填过。现在安装时就读出它的配置声明(官方 Registry 的 `environmentVariables`/`headers`,或条目自己启动命令里的 `-e NAME`、`${NAME}` 占位、裸凭据名),随行存进注册表:行上多一颗 **待配置** 徽标、页顶多一行「有 N 台服务器装好了但还差配置」,**配置**按钮打开掩码表单,保存只重挂本来就启用的行,安装回复则直接为刚写入的那行打开表单(`GET /servers`、`POST /configure`、`overall_mcp_configure`) | | 🛠️ | 原生工具桥接 | 启用的服务器以每服务器一个 `dsh-mcp-client` 实例挂载,工具以 `mcp____` 注册为 DSH 原生工具 | | 🖥️ | 侧栏页面 | 左侧导航「插件」正下方的 **全局 MCP** 入口:**已注册**(按来源分组、可折叠、分组开关、统计筛选)与 **MCP 市场**(浏览/搜索/安装)两个标签页 | | 🎛️ | 分组批量启停 | 每个来源分组一个总开关,一次启用/停用该来源的全部服务器(模型工具 `overall_mcp_set_source_enabled` 同能力) | | ⬆️ | 版本检查与更新 | 对经 `npx`/`uvx`(含 Windows `cmd.exe /c` 包装)启动的服务器检查 npm / PyPI 最新版本,「已注册」页一条 `全部更新(N)` 串行更新并自动重连,带条目级进度与逐条明细,跑完无剩余项时变为绿色 `✓ 已完成` | | 🪪 | 身份行 | 页头第二行写清**这个面板此刻在跑哪份代码**:插件版本、`pid`、已运行时长、能读到的数据文件数、日志条数;磁盘上的代码比进程新时另挂一颗「⚠ 代码已更新,重启 DSH 后生效」——「还是坏的」和「还没载入」从此不是同一句话(`GET /status`) | | 🧾 | 动作日志 | 宿主把自己的动作写进 `events.ndjson`(浏览/各源成败/配额/扫描/普查/证据/安装是否挂上/更新/挂载/注册表修复),页头「运行日志」一键展开;只记元数据,不记密钥,最多留最近 400 行 | | 🔑 | GitHub 凭据与普查续跑 | 面板上可填 token(或一键取 `gh auth token`),校验通过才生效、只发给 GitHub 主机;**匿名照样能普查**,只是配额从 5000 次/小时降到 60 次/小时。一次网络失败不再终结整场普查(按仓库重试 + 退避 + 连续被拒才收工),并且下一次普查从断点继续(`GET\|POST\|DELETE /auth`) | | ⏯️ | 普查手动启停 | 「已注册」页的 GitHub 访问卡里直接写普查状态(索引多少行、上次读到什么时候、谁停的),并按状态给动词:没在跑是「开始普查」(不等缓存过期),在跑是「停止普查」并显示进度。停止是安全的——已收到的行边收边落盘,下次从缺的仓库接着走,且不会再顺带花额度去抓安装证据(`GET\|POST /census`) | | 🔧 | 恢复走查手动启停 | 同一张卡里,**第二趟走查**(读仓库证据恢复安装命令)有自己的状态行与动词:`查过 N 个仓库 · 恢复出 M 条安装命令`,还有剩的就写「接着恢复」,跑起来了就写「停止恢复」并报「已试 N / M 个仓库」。此前它只在打开市场时自己开始、只能靠卸载插件停下——而它跑满一轮是 150ms × 两千多个仓库(`GET\|POST /install-evidence`) | | 🗂️ | 本地普查也能当来源看 | 来源筛选里 `本地普查(索引)` 就排在 `GitHub` 右边:读的是同一份本机索引,但**一次请求都不发**,也不会因为索引过期就顺手走一遍。它不是一个新来源(条数仍算在 GitHub 那一个里,一个上游一个数字),只是让「我在看本机已有的清单,不是刚去 GitHub 问的」成为一个可以选的动作(`?source=census`) | | 🧮 | 数字与时间都写明口径 | 每源条数与「共 N 条」用同一把尺子(去重后计入本页,`deduped` 单列);「列表读取于」是本地读目录的时间、「上游更新于」是仓库最近一次 push,两者不再共用一个「更新于」;按使用量排序时没有使用量数据的卡片直接写「无使用量数据」而不是静默按质量分排;推断出来的分类在卡片、详情与分类下拉里都带「(推断)」 | ## 快速开始 ```sh dsh plugin --profile web add ./dsh-overall-mcp ``` 重启 Profile 后:首次加载自动扫描宿主机 MCP 配置;点左侧导航里 **插件** 下面那个 **全局 MCP**。 页面是侧栏里的一个**全局面板**(不是设置项,也不是插件页里的配置区):侧栏图标行由 `sidebar.panellist` 列表插槽注册(`order: 5`,官方「插件」页是 0、「任务」是 10,所以本入口正好在「插件」正下方),面板本体注册在布局的 `main` 键控插槽里,两者共用同一个 id `overall-mcp`;`main` 用不存在的 key 选中会抛错,所以这两个字符串必须一致,`tools/client-smoke.mjs` 会把它们钉在一起。页面自己就是滚动容器(居中、最宽 1180px),顶部内边距叠了 `--dsh-frame-top-clearance`,避免在 macOS 红绿灯/Windows 标题栏下面被盖住。 ### 「已注册」标签页(对齐 Skills Manager 来源分组) - 顶部统计:`N 台服务器 · M 已启用 · K 已停用 · C 已连接 · X 连接异常` - 筛选:来源下拉、状态下拉(已启用/已停用/已连接/连接异常)、名称搜索 - 分组:按导入来源聚合(Claude Desktop / Claude Code / Cursor / VS Code / Windsurf / Codex CLI / Gemini CLI / 官方 Registry …),每组显示数量与"已启用"计数,右侧总开关批量启停,点击组头折叠/展开 - 每行:连接状态点、名称、**用途行**、事实行(传输方式 · 状态 · 工具数 · 工具前缀 `mcp____` · 包名版本)、启用/停用、移除;行声明了配置时另有一颗 **配置** 按钮 - **写者串行、原子落盘、工具说真话**:这一轮修的是「宿主自己的账本」——审核时发现的都是同一类问题:**文档/工具描述说的和代码做的不一致,而失败的样子和成功长得一样**。 1. **一次只让一个写者改注册表**:宿主每个「读整份注册表 → await(扫盘 / 起 npx / 查 npm)→ 写回」的动作(扫描、安装、启停、来源总开关、移除、清理来源已删除、检查更新、更新、收藏)现在排在同一条 promise 队列上(`serialized()`,`index.js`)。交错的两次「读-改-写」会让后一次写回它开始时的快照,把前一次的改动丢掉(例如一次早于安装开始的扫描,结束后会把刚装的那条覆盖掉)。读路径(列表、市场)不进队列,仍然并行。 2. **落盘走临时文件 + rename**(`writeJson`):原来注册表是就地 `writeFile`,写一半崩掉就留下半截 JSON——而这是唯一一份「丢了就所有服务器都挂不上」的文件(`lib/installs.js` 早就用了临时文件 + rename,注册表反而是漏的那个)。 3. **坏文件不再被当成首次运行**:`readJson` 的「解析失败 → 返回空」换成 `readRegistryDocument()` 的三态——没有文件(真正的首次运行)、**解析失败(损坏)**、正常文档。损坏时先把原文件复制成 `registry.json.corrupt-<时间戳>`,再把原因随每个列表响应带给面板,在页签上方显示一行红字。旧行为下「读不出来」和「你从没加过服务器」完全一样,而随后的第一次保存会把用户唯一的一份覆盖成空注册表。 4. **扫描不再清空收藏**:`favorites` 和 `servers` 在同一个文档里,而 `mergeIntoRegistry` 只返回 `{version, servers}`,调用方又保存它的返回值——一次扫描就会把所有星标清掉。现在它保留自己不负责的顶层键(单测 `mergeIntoRegistry keeps the document keys a scan does not own`)。 5. **安装不覆盖手工条目**:`source: manual`(或没有 source)的行是用户自己写的配置,扫描器一直保护它,只有「从市场安装」这条路会把它连同命令、args、env 一起替换掉,还回报成功。现在冲突返回 **409**,要用 `overwrite: true` 明说;`overall_mcp_install` 与 `/install` 都接受这个参数。 6. **失败安装不计数**:「本市场 N 次」现在只在**挂载成功之后** +1。进了注册表但没挂起来的条目仍然留着(用户能修好再重试),但返回体会带 `counted: false` 和一句说明,而不是像成功一样回答。 7. **来源总开关按名字收敛**(`syncFiber`):关掉一个来源时,同名条目可能还属于别的来源,那个来源没关就不该把它的 fiber 拿走。旧循环逐行断开再逐行重连,会把不属于这个来源的同名行的工具一起带走。 8. **工具描述与参数对齐代码**(`overall_mcp_scan` 不再声称「同名不同契约会改名导入,绝不覆盖」——改名导入早在 `820641c` 就删了,现在是同名原地更新、手工条目不动);`overall_mcp_update` 补上 `slot`/`from`,而且**同名不唯一时报错而不是取第一条**(此前只有面板能指明是哪一行,agent 只能猜,更新错行的结果还回报「无需更新」);`overall_mcp_remove` 的「不带 `slot` 就删第一条」改成同一套 409 语义。 9. **宿主契约有了单测**:`test/index.test.mjs` 用假宿主跑 `apply(ctx)`(mount 必定失败,正好走容错路径),覆盖路由/工具面、同名行的拒绝与 `slot` 命中、手工条目 409 与 `overwrite`、失败安装不计数、损坏注册表被隔离并上报、安装失败时的诊断(问到 / 问不到 / 没有路由 / 没有模型四种)、以及工具参数与文案。三个宿主 peer 依赖(`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-mcp-client`、`@deepseek-ai/dsh-llm`)本仓库不安装,由 `test/hooks.mjs` 映射到 `test/stubs/`;每个用例一个临时 `DSH_HOME`,绝不碰真实 `~/.dsh`。`package.json` 的 `test` 脚本是显式文件列表,这个文件也已列进去(否则新增测试会被静默跳过)。 10. **面板能看见注册表坏掉**:`registryError` 随 `/servers` 返回,页签上方显示「⚠ 注册表文件无法解析(…),本次以空注册表启动;原文件已备份为 registry.json.corrupt-…」——空的列表不再等于「你从没加过服务器」。 - **用途行**:用户要求「MCP下载好之后后面的应该是解释这个MCP的作用」——一行只说 `stdio · 已连接 · 工具前缀 mcp__x__` 等于没说它是干什么的。宿主现在按**证据强度**取一句人话(`lib/about.js`,上限 150 字符、渲染两行): 1. **服务器自己的说明**(`source: 'server'`):MCP `initialize` 结果里的 `instructions`。dsh-mcp-client 把它**原样**发布成 system prompt 的 `mcp:` 节(`### MCP server: ` 前缀由它加,宿主会剥掉),所以宿主只要 `ctx.inject(['systemPrompt'])` + `assemble({})` 把这一段读回来,就是服务器自我介绍的原话——不是我们从名字猜的; 2. **已注册的工具**(`source: 'tools'`):服务器没写说明时的兜底,形如「工具:search、fetch、describe 等 4 个」(`ctx.tools.schemas()` 按 `mcp____` 前缀筛出); 3. **启动命令**(`source: 'command'`):还没连上过的条目至少能看到将要运行什么。 三者的取舍写在 `describeAbout()` 里,来源用 `aboutSourceLabel()` 标注成用途行的 `title`(「来自服务器自身的说明(MCP initialize)」/「根据该服务器已注册的工具推断」/「尚未连接,显示启动命令」),**不把推断伪装成事实**;三样都没有(连名字都推不出 serverName)就**不显示**这一行,而不是编一句。`lib/about.js` 还会剥掉行内 markdown(`###`、`**`、反引号、图片),跳过 `Tools:` 这类标签行与纯图片行——`instructions` 常常整段是 markdown 文档,第一段往往只是它的标题。 重读规则(`needsInstructionsRead()`,单测 + 探针都钉住):dsh-mcp-client **只在服务器连接期间**发布该节,所以「没找到」不能当成最终结论缓存——否则第一次读到的是启动命令、连上之后也永远不会换成服务器自己的说明。宿主记下**上次读取时该服务器的存活状态**,只有状态翻转(或从未读过)才重新 `assemble()`;`source: 'server'` 即终态,`tools`/`command` 只在服务器存活时按 30 秒节流重试,停用的服务器不再产生任何读取。宿主日志里每次补齐都会留一行 `overall-mcp: purpose lines — server N, tools M, command K`,用来核对每台服务器实际命中的是哪一种证据。探针:`tools/probe-server-about.mjs`(离屏渲染三行,断言用途行在事实行之前、被 clamp、`title` 与证据一致) ### 「MCP 市场」标签页(自动发现市面上的 MCP) - 打开即自动拉取**整份市场列表**(全部 ~5.4k 条一次载入:上游成本与小切片完全相同,本地载荷 ~3MB;每个来源各 60 秒 TTL,可手动「刷新」) - **抓取本身是有条件的**:每个上游 URL 记住自己上一次的 `ETag`,下一次带 `If-None-Match` 去问;上游答 `304` 就直接复用本地那份正文,连 JSON 都不重新解析(GitHub raw / jsDelivr / 官方 registry / Smithery / Docker 都支持)。源级 TTL 内连问都不问;TTL 过了才去问,而问的结果又常常是 `304`。元信息行会把这件事实写出来:`列表读取于 12:34:56(缓存)· 3 源未变`——否则「刚刷新过」和「问了、上游说没变」在界面上长得一模一样 - 抓取**失败时保留上一次的那份列表**(标 `stale`,条目照常可用),同时把错误照报给界面;冷启动失败才真的报空。上游抽风不该让整个市场页变空 - 总数以紧凑形式显示:`5.3k 个可用 MCP`(5364 → 5.3k,**截断而非四舍五入**,避免标题数大于真实条数) - **市场页三段式 chrome**(按 v4 原型):① 一整行搜索胶囊(🔍 + 输入框 + 右侧「搜索」按钮,回车即搜);② 分类 chip 行(可横向滚动)+ 同一行右端的紧凑筛选控件(来源下拉 / 排序下拉 / 刷新,描边小按钮样式,不是大蓝块);③ 灰字元信息行 `5.3k 个可用 MCP · 第 1 / 1 页 · 每页 20 条`,右端是缓存时间;各来源条数与已安装数退到下面一行更轻的灰字——控件是一行,数字是另一行,互不挤占 - 统计:各来源条数 · 已安装数 · 当前页码 · 缓存时间。各来源那一行现在带**每个源自己的时间与状态**(`官方 800(12:03)· acme/mcp-registry 42(缓存 12:01)· …`):每个源有自己的 TTL 和验证器,所以它们是**各自变旧**的,一个时间戳说不清五份上游 - **排序**(用户自选,切换即生效,不重新请求上游): - `默认(综合推荐)` — 质量分:已认证(400) > 官方(300) > 可安装(120),再加 log 量纲的流行度(`installs` 与 Star 各最多 +200);已废弃 −500 - `质量最高` — 与默认**同一个**加权分。下拉把它单列,是为了让标签承诺的排序真的被应用,而不是一个和默认同序的空选项 - `使用量最多` — 按**合并后的使用量**倒序:Smithery 的调用次数 + Docker 的拉取次数 + 本面板的安装次数。三者单位不同(`lib/catalog.js` 自己也标注为不可比),因此它只是热度参考;下拉的悬停提示与列表上方的提示行都写明了这一点 - `Star 最多` — 按 GitHub Star 倒序 - `最近更新` — 按 `updatedAt` 倒序 - `名称 A→Z` - **分页**:每页 20 / 50 / 100 条三档可切,带「« 首页 / ‹ 上一页 / n / N / 下一页 › / 末页 »」。5364 条在每页 20 下为 20 页、50 下 8 页、100 下 4 页 - **功能分类**(下拉选择,不是 chip 行):筛选行左端是「分类 + 下拉」——「全部分类 (N)」开头(总数只在它上面显示),随后是功能组(不带数量、仍按数量降序排列),「未分类」收尾;右端是来源 / 排序 / 刷新,用 `margin-left:auto` 贴到行尾,所以分类与筛选之间是行内的空白而不是固定的 8px。19 个功能组放不进一行:早先的横向滚动 chip 行永远有一半看不见,换成换行的 chip 行又会撑出三行「表单感」,因此改成原生下拉。75 个上游 `###` 标题(两清单合计,含同义异名如 Finance / Finance & Fintech、两个 Multimedia)归并为 **19 个功能组**(金融支付、开发工具、办公协作、搜索与知识、营销与客户、多媒体创作、数据与存储、安全与身份、聚合网关、出行与生活、商业与法律、云与运维、其他、社交媒体、自然环境、浏览器自动化、医疗健康、游戏娱乐、语音与语言)。归并在宿主完成(`group` 字段),上游原标题保留在条目上(`category` + 卡片第一个标签)用于溯源;未映射的新标题兜底进「其他」而非消失;三个 registry 不发布类目,其条目归入「未分类」桶;合并记录时类目跟随存活(Smithery 赢得合并的条目仍能按功能组筛出) - 排序、分类与分页都是**视图层**行为:宿主一次返回整份列表,翻页/换排序/切分类不产生任何上游请求- 任一字段缺失的条目不会因此排到最后——缺失不等于 0:有数字的条目优先于无数字的,两者皆无时回落到质量分 - **「未分类」为什么这么多,以及怎么减少**:六个来源里只有两个社区清单带类目(它们的 `###` 标题),**Smithery 与官方 registry 一个类目字段都不发**——实测(2026-10-14)Smithery `/servers` 只发 `id, qualifiedName, namespace, slug, displayName, description, iconUrl, verified, useCount, remote, isDeployed, unlisted, inactive, createdAt, homepage, bySmithery, owner, score`,官方 `/v0/servers` 只发 `name, description, title, version, remotes, packages`。这两类合计 233 条(约 5.4k 条里的 4%),但默认「综合推荐」排序恰好把它们排在第一页(官方 + 认证的 Smithery 条目质量分最高),所以首屏曾经 100% 是「未分类」。现在宿主对**没有上游类目**的条目做一次**关键词推断**(`lib/catalog.js` 的 `inferGroupOf`:19 组词表 + 标题双权重 + 词组越长越优先,`\b` 只加在词首以免 `\bspreadsheet\b` 匹配不到 `spreadsheets`;刻意不收 `server` 这种词——每条都是 MCP server,会把「招聘」的 DevMatch 拖进「云与运维」)。实测官方 registry 抽样 100 条里 **78 条**得到分类、Smithery 10/10;匹配不到的仍然老实显示「未分类」(`其他` 的语义是「上游说了但我们归不了」,与「我们不知道」不同)。推断结果带 `groupSource:'inferred'`,卡片胶囊的悬停提示写「分类(按名称与描述推断):…」,绝不把我们的判断伪装成上游事实。单测见 `test/catalog.test.mjs`(真实条目做样本:Brave Search→搜索与知识、Gmail/Google Sheets→办公协作、DevMatch→商业与法律、匹配不到→未分类) - **本市场安装计数**:上游的使用量只覆盖 Smithery(`useCount`→次启动)与 Docker(`pull_count`→次拉取),谁都不统计「从本面板装的」。用户要求「下载量以后从我们市场下载的也要统计一下」,因此宿主新增 `lib/installs.js`,把每次**成功**安装记到 `$DSH_HOME/overall-mcp/installs.json`(键就是目录条目的 `id`,和卡片、收藏、安装按钮用的是同一个;无 id 时回退成 `name:<服务器名>`;写盘走临时文件 + rename,缺文件/坏文件都退化成「还没装过」而不是抛错)。面板上它是**第二条序列**:卡片统计行显示「⤓ 3 本市场」(悬停给出最近一次安装日期),详情面板有「本市场安装」一行;只有当**排序**用「使用量最多」时两者才相加(`usageOf()`),因为那个问题问的是「这东西有多常用」,而对那 233 条上游不发数字的条目来说,我们自己的安装就是唯一的证据。单测见 `test/installs.test.mjs`(计数递增/首次时间不动/坏文件容错/键回退/写读往返 + 不留临时文件) - **卡片墙是「行优先」的等宽网格,不是多列瀑布流**:`column-count` 的多列墙是**竖着**铺的(先把第 1 列填到底再开第 2 列),于是排好序的列表读起来是「顺着每列往下」——用户选了「使用量最多」后横向扫一眼看到 87.5k / 9.1k / 无数据,就断定排序没生效(实际每列内部都是降序,第三个来源根本没发布使用量),并明确要求「应该是从左到右排序而不是从上到下的排序,这样第三列都是最后的」。现在 `.omkt-grid` 是 `display:grid` + `grid-template-columns:repeat(N, minmax(0,1fr))`(N 由容器查询给出,内联的 `repeat(2, …)` 是没有容器支持时的兜底),卡片按行从左到右铺、名次 `#1` 落在左上角。两个要点:① 必须是 `minmax(0,1fr)` 而不是裸 `1fr`(裸 `1fr` 会被最宽卡片的 min-content 顶开,墙的最小内容宽度从 12082px 降到 14px);② 行的对齐**不是** `align-items:start`——那样一行的矮卡片下面会留个洞(fixture 里那张超高卡片留了 300px),所以用默认的 `stretch` 把整行拉齐,再由卡片自己的 `grid-template-rows:1fr auto` 把富余高度放在正文与页脚之间,页脚始终贴在卡片底边;卡片正文的 `alignContent:start` 保证这些富余不会摊到正文各行之间。`tools/shot.mjs` 的列数断言随之从 `getComputedStyle(grid).columnCount` 改成数 `gridTemplateColumns` 的轨道数(两种窗口盒子:1140px→3 列、820px→2 列) - **排序必须看得见**:① 一旦选了非默认排序,每张卡片页脚最前面出现 `#1 #2 #3…` 名次胶囊(`title` 写明「当前排序下的名次:第 N 名」,等宽数字对齐);② 筛选行里**正在起作用的控件**(排序、来源、分类、注册表的状态/来源)显示为墨色边框 + 浅灰底(`omkt-select-on`),默认态不标记;③ meta 行写出「按使用量最多排序」(`title` 补充「名次从左到右、再下一行;没有该项数据的条目排在后面」)。另外这些控件的 inline `border` 会盖住样式表里的 `:hover`/`:focus` 规则,所以注入样式表里的悬停/聚焦规则都带 `!important`;搜索胶囊没有自己的边框(边框画在胶囊上),聚焦时用 `.omkt-search:has(.omkt-input:focus-visible)` 把胶囊本身染成墨色。`tools/render-market-tab.mjs` 现在直接驱动真实 `