extract-agent-prompts.js 3.33 KB
#!/usr/bin/env node
/**
 * scripts/extract-agent-prompts.js
 *
 * 从 docs/prd/newfunction/agents-{role}.html 抽取 §2.2 "完整 System Prompt"
 * 区块,写到 system/templates/devloop-{role}/prompt.md。
 *
 *   role ∈ pm | architect | coder | tester | integrator | triager
 *
 * 抽取规则(本脚本只做规则正向匹配,不依赖 cheerio 来保留原始换行):
 *   1. 定位 <h3>2.2 完整 System Prompt</h3>
 *   2. 取其后<b>第一个</b> <pre class="code">...</pre> 的内层
 *   3. 去除全部 <span class="...">...</span> 包装,保留文本
 *   4. 反转义 HTML entity (&lt; &gt; &amp; &quot;)
 *
 * 规格: docs/prd/newfunction/plan-migration.md · P0.B
 */

const fs = require('fs');
const path = require('path');

const ROLES = ['pm', 'architect', 'coder', 'tester', 'integrator', 'triager'];
const SRC_DIR = path.resolve(__dirname, '../docs/prd/newfunction');
const OUT_DIR = path.resolve(__dirname, '../system/templates');

function decodeHtmlEntities(s) {
  return s
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&');
}

function stripSpans(s) {
  // 去除 <span class="...">...</span>,保留内部文本(可能嵌套也用同一规则处理)
  let prev;
  let cur = s;
  do {
    prev = cur;
    cur = cur.replace(/<span\b[^>]*>([\s\S]*?)<\/span>/g, '$1');
  } while (cur !== prev);
  // 去除残留的开闭标签 (容错)
  cur = cur.replace(/<\/?span[^>]*>/g, '');
  return cur;
}

function extractPrompt(html, role) {
  const anchorRe = /<h3[^>]*>\s*2\.2\s+完整\s*System Prompt[^<]*<\/h3>/;
  const am = html.match(anchorRe);
  if (!am) throw new Error(`§2.2 header not found in agents-${role}.html`);
  const after = html.slice(am.index + am[0].length);
  const preRe = /<pre\s+class="code"[^>]*>([\s\S]*?)<\/pre>/;
  const pm = after.match(preRe);
  if (!pm) throw new Error(`<pre class="code"> after §2.2 not found in agents-${role}.html`);
  const inner = pm[1];
  const stripped = stripSpans(inner);
  const decoded = decodeHtmlEntities(stripped);
  return decoded.trim();
}

function run() {
  fs.mkdirSync(OUT_DIR, { recursive: true });
  const results = [];
  for (const role of ROLES) {
    const src = path.join(SRC_DIR, `agents-${role}.html`);
    if (!fs.existsSync(src)) {
      results.push({ role, ok: false, reason: 'source missing: ' + src });
      continue;
    }
    try {
      const html = fs.readFileSync(src, 'utf8');
      const prompt = extractPrompt(html, role);
      const outDir = path.join(OUT_DIR, `devloop-${role}`);
      fs.mkdirSync(outDir, { recursive: true });
      const outPath = path.join(outDir, 'prompt.md');
      const header = `<!-- AUTO-GENERATED by scripts/extract-agent-prompts.js. Edit agents-${role}.html §2.2 instead. -->\n\n`;
      fs.writeFileSync(outPath, header + prompt + '\n');
      results.push({ role, ok: true, bytes: prompt.length, path: outPath });
    } catch (e) {
      results.push({ role, ok: false, reason: e.message });
    }
  }
  for (const r of results) {
    if (r.ok) console.log(`✓ devloop-${r.role}.md (${r.bytes} bytes)`);
    else console.error(`✗ ${r.role}: ${r.reason}`);
  }
  const allOk = results.every(r => r.ok);
  if (!allOk) process.exit(1);
}

if (require.main === module) run();

module.exports = { extractPrompt, stripSpans, decodeHtmlEntities };