Files
superpowers-zh/site/md.mjs
AI不止语 1f66b091b7 fix(site): 官网工具墙停在 20 款 —— Cline / Kilo Code / Crush 的用户在下拉里找不到自己的工具
起因是核对首页:统计块写着「20 支持工具」,同一屏下面的标题写着「一套 skill,
23 款工具通用」,FAQ 里又老老实实枚举了 23 款。三处自己打自己。

根因:文案计数早就跟着 installer 改到了 23,但 `site/build.mjs` 的 `TOOLS`
数组从来没人补 —— audit 只校验文案里的数字,不校验列表本身。

## 1. 工具列表补 3 款 + 修 4 处错的

`TOOLS` 20 条 -> 23 条(= TARGETS 22 条 + Copilot CLI 单独计数)。
统计块的 `20` 是硬编码的,改成 `TOOLS.length`。

漏的三款(installer 一直支持,官网从没有过):Cline、Kilo Code、Crush。

已有条目里查出的错(逐条实测过命令):

| 条目 | 原来 | 现在 | 依据 |
|---|---|---|---|
| Claw Code | `--tool claw` | 直接 `npx superpowers-zh` | detect 是 `.claw` / `CLAW.md`,实测能自动检出 |
| Hermes Agent | `--tool hermes` | `--global --tool hermes` | 官方只自动加载 `~/.hermes/skills/`,项目级要手写 config.yaml,给项目级命令等于给「装了不生效」 |
| Qwen Code | 类型 IDE | CLI | 它是 QwenLM/qwen-code 命令行工具,不是通义灵码 |
| Antigravity | 类型 CLI | IDE | Google 的 AI IDE,docs/README.antigravity.md 里本来就这么写 |

`auto: true/false` 两态不够用了(Hermes 是「能检出但项目级不生效」),换成
`mode: auto / manual / global`,三语各补一条对应说明文案。

## 2. FAQ 的「支持全局」清单少了 4 款

写着 7 款,installer 实际 11 款 —— CodeArts / Crush / Hermes / CodeBuddy
陆续拿到 `--global` 之后这句话没跟。三语同步补齐。
两版 README 的同一句也少了 CodeArts(v1.7.11 补 --global 时漏改),一并修。

## 3. skill 详情页有 6 条死链

`SKILL.md` 里指向兄弟文件的相对链接(`implementer-prompt.md`、
`../requesting-code-review/code-reviewer.md` 等)站点上根本没有这些 .md:
`../` 开头的直接 404,不带 `./` 的被 md.mjs 的 scheme 白名单打成 `href="#"`
—— 点了没反应,比 404 还难查。

`renderMarkdown(src, base)` 加基址参数,相对链接解析到 GitHub 源文件。
5 个目标地址逐个 curl 过,都是 200。全站扫描:死链 0、`href="#"` 0。

## 4. 加两条门禁,堵住这次的漏法

audit 原来只数文案里的数字,这次两个问题它一个都发现不了。补:

- 官网 `TOOLS` 条目数必须 = TARGETS + 1
- 官网 FAQ 全局清单必须包含 installer 里每个带 `global:` 的工具

反向验证过:删掉 Crush、去掉 CodeArts,两条各自报错。

audit 170 pass / 0 fail(新增 2 条);verify-release 114 pass / 1 fail,
唯一那条是 windsurf.com 返回 429 限流,改动前的干净树上同样失败,与本次无关。
2026-08-28 21:48:01 +08:00

170 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 零依赖 Markdown → HTML 渲染器,够用于本项目的 SKILL.md。
// 支持ATX 标题、代码围栏、表格、有/无序列表(含嵌套)、引用、分隔线、
// 行内 code/bold/italic/link原生 HTML 行透传。
function escapeHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 行内:先保护代码片段,再处理粗体/斜体/链接
// base当前文档在仓库里的 URL如 .../blob/main/skills/<name>/),用于把
// SKILL.md 里指向同目录/兄弟目录的相对链接code-reviewer.md、../x/y.md
// 解析成 GitHub 上的真实地址 —— 站点上没有这些 .md 文件,不解析就是死链。
function inline(text, base) {
const codes = [];
// 行内代码 `...`
text = text.replace(/`([^`]+)`/g, (_, c) => {
codes.push('<code>' + escapeHtml(c) + '</code>');
return '' + (codes.length - 1) + '';
});
// 其余文本转义(保护已存在的原生 HTML本项目 SKILL.md 仅含可信内容)
// 链接 [text](url) —— 仅放行安全 schemehttp/https/mailto/锚点/相对路径),
// 阻断 javascript:/data:/vbscript: 等可执行 scheme并转义引号防属性逃逸
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, u) => {
let safe;
if (/^(https?:|mailto:|#)/i.test(u)) {
safe = u; // 绝对链接 / 锚点:原样
} else if (/^[a-z][a-z0-9+.-]*:/i.test(u)) {
safe = '#'; // 其余 schemejavascript:/data:…)一律阻断
} else if (base) {
try { safe = new URL(u, base).href; } // 相对路径 -> 仓库真实地址
catch { safe = '#'; }
} else {
safe = '#';
}
return `<a href="${safe.replace(/"/g, '&quot;')}" target="_blank" rel="noopener">${t}</a>`;
});
// 粗体 **x** / 斜体 *x*
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
text = text.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
// 还原代码片段
text = text.replace(/(\d+)/g, (_, i) => codes[+i]);
return text;
}
export function renderMarkdown(src, base) {
// 去掉 frontmatter
src = src.replace(/^---\n[\s\S]*?\n---\n?/, '');
const lines = src.split('\n');
const out = [];
let i = 0;
const listStack = []; // {type:'ul'|'ol', indent}
function closeListsTo(indent) {
while (listStack.length && listStack[listStack.length - 1].indent >= indent) {
out.push('</li>');
out.push('</' + listStack.pop().type + '>');
}
}
function closeAllLists() {
while (listStack.length) {
out.push('</li>');
out.push('</' + listStack.pop().type + '>');
}
}
while (i < lines.length) {
let line = lines[i];
// 代码围栏
const fence = line.match(/^```(\w*)/);
if (fence) {
closeAllLists();
const lang = fence[1] || '';
const buf = [];
i++;
while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i++; }
i++; // 跳过结束 ```
out.push(`<pre data-lang="${lang}"><code>${escapeHtml(buf.join('\n'))}</code></pre>`);
continue;
}
// 分隔线
if (/^---+\s*$/.test(line) || /^\*\*\*+\s*$/.test(line)) {
closeAllLists();
out.push('<hr>');
i++; continue;
}
// 标题
const h = line.match(/^(#{1,6})\s+(.*)$/);
if (h) {
closeAllLists();
const lvl = h[1].length;
const id = h[2].trim().toLowerCase().replace(/[^\w一-龥]+/g, '-').replace(/^-|-$/g, '');
out.push(`<h${lvl} id="${id}">${inline(h[2].trim(), base)}</h${lvl}>`);
i++; continue;
}
// 表格(当前行含 | 且下一行是分隔行)
if (/\|/.test(line) && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1])) {
closeAllLists();
const parseRow = r => r.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map(c => c.trim());
const header = parseRow(line);
i += 2; // 跳过表头与分隔行
out.push('<div class="md-table"><table><thead><tr>' +
header.map(c => `<th>${inline(c, base)}</th>`).join('') + '</tr></thead><tbody>');
while (i < lines.length && /\|/.test(lines[i]) && lines[i].trim() !== '') {
const cells = parseRow(lines[i]);
out.push('<tr>' + cells.map(c => `<td>${inline(c, base)}</td>`).join('') + '</tr>');
i++;
}
out.push('</tbody></table></div>');
continue;
}
// 引用
if (/^>\s?/.test(line)) {
closeAllLists();
const buf = [];
while (i < lines.length && /^>\s?/.test(lines[i])) { buf.push(lines[i].replace(/^>\s?/, '')); i++; }
out.push('<blockquote>' + inline(buf.join(' '), base) + '</blockquote>');
continue;
}
// 列表项
const li = line.match(/^(\s*)([-*]|\d+\.)\s+(.*)$/);
if (li) {
const indent = li[1].length;
const type = /\d+\./.test(li[2]) ? 'ol' : 'ul';
// 关闭比当前更深的层级
closeListsTo(indent + 1);
const top = listStack[listStack.length - 1];
if (!top || top.indent < indent) {
out.push('<' + type + '>');
listStack.push({ type, indent });
} else {
out.push('</li>');
}
out.push('<li>' + inline(li[3], base));
i++; continue;
}
// 空行
if (line.trim() === '') {
closeAllLists();
i++; continue;
}
// 原生 HTML 行透传
if (/^\s*<\//.test(line) || /^\s*<[a-zA-Z]/.test(line)) {
closeAllLists();
out.push(line);
i++; continue;
}
// 段落(合并连续非空行)
closeAllLists();
const buf = [line];
i++;
while (i < lines.length && lines[i].trim() !== '' &&
!/^(#{1,6}\s|```|>\s?|---+\s*$|\s*([-*]|\d+\.)\s)/.test(lines[i]) &&
!(/\|/.test(lines[i]) && /^\s*\|?[\s:|-]+\|/.test(lines[i + 1] || ''))) {
buf.push(lines[i]); i++;
}
out.push('<p>' + inline(buf.join(' '), base) + '</p>');
}
closeAllLists();
return out.join('\n');
}