| 问题 | 裁定 |
|---|---|
| 页面深度 | 列表 + 详情页(不是就地展开、不是只有列表) |
| 访客怎么拿走 | 复制安装命令 + GitHub 仓库外链 + 站内下载 zip(三样都要;「复制 SKILL.md 全文」未单独选,但画板 2g/2h 的文件预览头部有 copy——按画板做,它复制的是当前文件原文) |
| 组织方式 | 按用途分类(与 Notes 首页同构:分类表 + 色点 + 卡片网格) |
| 收录范围 | 自研 + 精选第三方(卡片带出处行 + 「自研 / 精选」徽标;第三方带原仓库链接) |
R6(MCP 管理面)、R-TABS(tab 登记表 + 呈现开关:新 tab 走同一套三处登记)、R11(生产已上线)。 无新凭据。可能有一个新依赖(zip 打包,见「待所有者裁定」第 1 条)。
| 画板 | 路由 | 数据 |
|---|---|---|
| 2f Skills 首页 | /skills | GET /skills:分类(顺序 / 名称 / slug / 色点)× 卡片(name / 徽标 / summary / 元信息行)+ 页脚(总数 / 最近更新) |
| 2g 详情页 · SKILL.md 态 | /skills/<name>(默认文件) | GET /skills/:name:头部元信息 + 安装命令 + 文件清单(含内容)+ zip 大小 |
| 2h 详情页 · Python 态 | /skills/<name>?file=scripts/review.py | 同上;当前文件由查询串决定,切换是客户端状态 |
| 既有 12 块的导航条 | 全站 | TABS 登记表多一项,GlobalNav 零改动 |
画板上的文案是格式不是内容:卡片元信息行 = <出处> · <N> 个文件 · 更新于 <relTime>(自研出处显示 @<owner>,精选显示 owner/repo); 详情元信息 = [v<version> · ]<N> 个文件 · <总大小> · 更新于 <isoDate> · <出处>(第三方时出处是外链 出处 owner/repo ↗); 安装命令 = npx skills add <repo> --skill <name>(由 repo + name 派生,不单独存);zip 按钮文案 下载 zip · <size>。
三张表,与 notes 三张同一分工(读面只读、写面在 mcp);不给 agent_ro / agent_title / agent_image 任何权限(迁移 006 刻意不设默认授权,不写 GRANT 就是全部答案)。
写面校验(全部在 skills_upsert 里做,库级 CHECK 兜底):文件 ≤ 64 个;单文件 ≤ 256 KB;整包 ≤ 512 KB;path 相对、无 ..、不以 / 开头、段字符集 [A-Za-z0-9._-]、深度 ≤ 4; SKILL.md 的 frontmatter name 必须等于 skill name;repo_url 可空、有值时只收 http(s)(与 About originUrl 的 isHttpUrl 同一口径);LICENSE 文件不强制(所有者裁定);未知分类拒;删除仍有 skill 的分类拒。 内容与库内完全一致时整行不动(不刷新 updated_at,与 notes_chapter_upsert 同一约定)。
后端
前端(规则 7:既有页面与 GlobalNav 零改动)
测试
文档
| # | 检查 | 命令 / 期望 |
|---|---|---|
| 1 | 编译与全量测试 | dev.ps1 check 通过;dev.ps1 test 全绿(本轮之前 16 文件 388 用例,新增用例数回填) |
| 2 | 三处登记齐 | site/tabs.test.ts 种子 ↔ 登记表一致通过;site_tab_set 的 enum 下发 runtime, notes, skills, about |
| 3 | 写面校验 | mcp.test.ts:缺 SKILL.md / frontmatter name 不符 / .. 路径 / 绝对路径 / 非法字符 / 深度 > 4 / 单文件 > 256 KB / 整包 > 512 KB / > 64 文件 / 含 NUL / 未知分类 / repoUrl 有值但非 http(s) —— 逐条被拒,库无残留;不带 repoUrl、不带 LICENSE 的 curated 包能发布(所有者裁定非必填) |
| 4 | 幂等 | 同内容重发 → status: unchanged,updated_at 不动;改一个文件 → updated,zip 重打 |
| 5 | 级联与分类保护 | skills_delete 后 skill_files 无残留;skills_category_delete 对仍有 skill 的分类 → ConflictError |
| 6 | 读面 | GET /skills 分类按 sort_order、卡片按 sort_order、页脚统计正确;GET /skills/<name> 文件顺序 SKILL.md 首位;未知 name → 404 |
| 7 | zip | GET /skills/<name>.zip 200、application/zip、Content-Disposition 文件名 = <name>.zip;解压后文件集合与内容与库内一致(测试里回读);未知 → 404;经 next dev 代理与经 Caddy 两条路径都通 |
| 8 | 画板 2f 对照 | 本机实跑截图:四分类 + 色点、徽标色(自研蓝 / 精选灰)、卡片三行、页脚两行、hover 态 |
| 9 | 画板 2g/2h 对照 | 默认 SKILL.md:frontmatter 块 + 正文 + 「本页目录」;点 .py:行号列、三 token 高亮、「本页目录」消失;?file= 深链直达;两处 copy 点击后 1.5s 显示 copied 再回落;repoUrl 为空的 skill 不渲染 GitHub ↗ 与出处链接、其余版式不变 |
| 10 | 呈现开关 | site_tab_set{skills,false} → 导航条三格、/skills 与 /skills/<name> 404;恢复 → 四格,与画板 1a(四格版)一字不差 |
| 11 | 安全 | 含 <script> / <img onerror> 的 md 与 py 文件在页面上只是文本;repoUrl 为 javascript: 时写面拒、前端 safeExternal 二次拦;zip 响应带 nosniff;sandbox.test.ts:agent_ro 读 skills 三表 permission denied |
| 12 | 镜像白名单 | dev.ps1 build 的服务列表含 skills;镜像里 GET /skills 非 404(冒烟清单第 1 条,八服务) |
| 13 | 真实 MCP 协议路径 | 用 XRAY_MCP_TOKEN 对本机 /mcp 发 2026-07-28 契约:tools/list 总数 42;skills_upsert 发布仓库自带的 .claude/skills/encore-api(整包)→ 页面出现 → zip 解压与源目录一致 |
| 14 | gen client | dev.ps1 gen 后 api-client.ts 含 skills 命名空间、不含 mcp;前端类型对得上 |
回退成本:删 apps/api/skills/、apps/web/app/(site)/skills/、components/skills/,登记表两处各删一行,Caddy / next 各删一条路由; 表由一条新迁移 DROP(deploy/migrate.sh 口径,不回滚已施加迁移)。临时下架不用回退:site_tab_set{skills,false} 即可。
同一验收项针对性整改后连续 2 次验证仍不过 → 写 rounds/round-skills/BLOCKED.md,停下呼人。禁止放宽验收标准自我通过。
数字:dev.ps1 check 通过;dev.ps1 test 18 文件 / 413 用例全绿(本轮前 16 / 388;新增 skills/skills.test.ts 9 条、mcp/skills.test.ts 16 条, site/tabs.test.ts / mcp.test.ts / sandbox.test.ts 各改一处断言);管理面工具 34 → 42;前端 tsc --noEmit 通过;新增生产依赖 fflate@0.8.3(exact)。
验收逐项(本机 encore run :4000 + next dev;发布走真实 MCP 协议路径,脚本在会话 scratchpad,token 取自用户环境变量、不进命令行):
| # | 结果 |
|---|---|
| 1 | check 通过;test 18/413 全绿 |
| 2 | tabs.test.ts 种子 ↔ 登记表一致(四格);site_tab_set 的 enum 下发 runtime / notes / skills / about(tools/list 实看) |
| 3 | mcp/skills.test.ts:缺 SKILL.md / 放错目录 / frontmatter name 不符 / 无 frontmatter / .. / 绝对路径 / 非法字符 / 5 段 / 单文件 256 KB+1 / 整包 600 KB / 65 文件 / NUL / svg / 孤立代理对 / 大小写重名 / 空包 / 坏 name 17 种逐条拒;repoUrl: javascript: 在 schema 层拒(真实 MCP 路径也实测拒);curated 不带 repoUrl、不带 LICENSE 能发布 |
| 4 | 同内容重发(含文件顺序打乱)→ unchanged、updated_at 与 zip 字节都不动;改一个文件 / 改 summary → updated、zip 重打。真实 MCP 路径:四个 skill 各重发一次全部 unchanged |
| 5 | skills_delete 后 skill_files 无残留;skills_category_delete 对仍有 skill 的分类 → ConflictError,清空后可删 |
| 6 | GET /skills 分类 / 卡片按 sort_order、空分类不出现、total / latest 正确;GET /skills/<name> 文件顺序 SKILL.md 首位;未知 → 404、大写 / 非 ASCII / 65 字符 → 400(前端统一成 404) |
| 7 | zip:200 + application/zip + attachment; filename="<name>.zip" + nosniff + 强 ETag(内容哈希)+ 一天缓存无 immutable;If-None-Match → 304;HEAD 无正文;解压后 4 个 skill 逐文件与源目录 IDENTICAL;经 next dev 代理与直连 api 两条路径都通(Caddy 那条按同款 regex 写入,待 130 / 生产冒烟) |
| 8 | 画板 2f:四格导航条、分类色点 + slug、卡片(等宽名 + 自研蓝 / 精选灰徽标 + 一句话 + 出处 · N 个文件 · 更新于 N 天前)、页脚两行、hover 态 —— 截图对照通过 |
| 9 | 画板 2g:面包屑 / 等宽 22px 标题 + 徽标 / GitHub ↗ + 下载 zip · 3 KB / INSTALL 面板 / 目录树(SKILL.md 首位、目录 ▾、大小列、选中行 --bg-selected + 600)/ frontmatter 键值块(when_to_use 折叠块并成一段)/ 正文 / 「本页目录」四项且锚点 id 与标题一致;画板 2h:点 .py → 行号列、三 token 高亮、「本页目录」消失、地址栏同步 ?file=scripts%2Freview.py;?file= 深链直达;两处 copy 点击后显示绿色 ✓ copied、1.5 s 后回落;repoUrl 为空的自研 skill 不渲染 GitHub ↗ 与出处链接、版式不变(与 encore-api 那页对比) |
| 10 | site_tab_set{skills,false} → /notes 上导航条三格、/skills 与 /skills/<name> 404、/skills/<name>.zip 与 api /skills 仍 200(边界只到呈现层);恢复 → 四格 |
| 11 | 含 <script> / <img onerror> 的 SKILL.md 与 .py、含 <script> 的 summary 在页面上只是文本(DOM 里 0 个 img,无内容来源的 script);repoUrl: javascript: 写面拒、前端 safeExternal 二次拦;zip 响应 nosniff;sandbox.test.ts:agent_ro 读 skills 三表 permission denied |
| 12 | dev.ps1 的 $hostedServices 已含 skills(构建命令行实看:--services agent,…,site,skills);已于 789007e 发版时完成(2026-09-03):构建命令行实看含 skills,生产 GET /api/skills 200、/skills 页 200 渲染空态、/skills/<不存在>.zip 回 api 的 JSON 404(新 Caddy 扩展名分流生效)。本轮曾在审查未收口时误发起一次 dev.ps1 build,所有者指出后中止 —— 审查一旦要整改镜像就得重打,构建不该早于审查收口 |
| 13 | 真实 MCP 路径(2026-07-28 契约):server/discover 回 ["2026-07-28"],tools/list 42;skills_upsert 发布仓库自带的 encore-api / encore-database / encore-testing(精选,encoredev/skills)+ 一个带 scripts/*.py 与 references/*.md 的自研示例 → 页面出现 → zip 解压与源目录逐文件一致 |
| 14 | dev.ps1 gen 后 api-client.ts 含 skills 命名空间(listSkills / getSkill)、不含 mcp;生成物里被翻掉的三行 app slug 按 BACKLOG 先例还原 |
与任务卡的偏离(实现时的判断,列出来供审查 / 所有者过目):
踩的坑:Write 工具会把 \u0000 / \u001f 这类转义解码成字面控制字符写进 .ts(用户级 CLAUDE.md 记过),shared/skill-pack.ts 用 python 脚本按 chr(92) 拼回转义形式; worktree 开工三件事(npm ci ×2、复制 .secrets.local.cue、PowerShell cwd)按项目记忆处理,dev.ps1 gen 把 app slug 翻成 mkaxu、按先例还原。