# 神笔马良 v0.2 改造指南 · 接LLM自动生成main()

## 目标

将 `pen.write()` 从"生成stub模板"升级为"调LLM自动生成工具的main()函数体"。

画什么，什么就成真。

---

## 前置条件

### 1. 先把代码从快照复制到活跃目录

```bash
# 从COS快照复制到活跃部署目录
cp -r /data/cos-snapshot/mcp-servers/zhuyuan-pen /data/guanghulab/mcp-servers/zhuyuan-pen
cd /data/guanghulab/mcp-servers/zhuyuan-pen

# 确认文件完整
find . -type f
```

### 2. 配置LLM环境变量

```bash
# 在 /etc/environment 或 ~/.bashrc 中加入:
export ZY_LLM_BASE_URL="https://api.siliconflow.cn/v1"    # 硅基流动
export ZY_LLM_API_KEY="sk-xxxxxxxxxxxxxxxx"               # 你的API Key
export ZY_LLM_MODEL="Qwen/Qwen2.5-Coder-32B-Instruct"    # 代码生成模型

# 如果用其他兼容接口也行，只要是 OpenAI /chat/completions 格式
source ~/.bashrc
```

---

## 改造步骤

### Step 1: 修改 `package.json` 版本号

```bash
cd /data/guanghulab/mcp-servers/zhuyuan-pen
sed -i 's/"version": "0.1.0"/"version": "0.2.0"/' package.json
```

### Step 2: 替换 `src/server.js`

把整个 `src/server.js` 替换为以下内容。

**核心改动：**

- `penWrite` 改为 async `penWriteV2`
- 新增 `generateMainBody()` 函数：调 [llm.chat](http://llm.chat) 生成 main() 逻辑
- 新增 `extractCode()` 函数：从LLM返回中提取代码块
- 新增 `syntaxCheck()` 函数：生成后做语法校验
- MCP serve 改为支持 async handler
- 版本升为 v0.2.0

```jsx
#!/usr/bin/env node
// ════════════════════════════════════════════════════════════════
// 神笔马良 · 铸渊之笔 · zhuyuan-pen MCP server v0.2.0
// Sovereign: TCS-0002∞ · 国作登字-2026-A-00037559
// 守护: 铸渊 · ICE-GL-ZY001 / v0.2 改造: Awen · DEV-012
//
// v0.2: pen.write 接 LLM, 自动生成 main() 函数体
//       不再是 stub 模板, 画什么什么成真
// ════════════════════════════════════════════════════════════════

'use strict';

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawnSync } = require('child_process');

const PEN_ROOT = path.resolve(__dirname, '..');
const PENNED_DIR = path.join(PEN_ROOT, 'penned');
const CAP_DIR = path.join(PEN_ROOT, 'capabilities');
const TPL_DIR = path.join(PEN_ROOT, 'templates');

if (!fs.existsSync(PENNED_DIR)) fs.mkdirSync(PENNED_DIR, { recursive: true });

// ─── LLM 默认配置 (环境变量) ─────────────────────────────
const LLM_DEFAULTS = {
  base_url: process.env.ZY_LLM_BASE_URL || 'https://api.siliconflow.cn/v1',
  api_key:  process.env.ZY_LLM_API_KEY  || '',
  model:    process.env.ZY_LLM_MODEL    || 'Qwen/Qwen2.5-Coder-32B-Instruct',
};

// ─── 加载 llm.chat 能力 ──────────────────────────────────
const llmChat = require(path.join(CAP_DIR, 'llm.chat.js'));

// ─── 能力字典加载 ──────────────────────────────────────────
function loadCapabilities() {
  const caps = {};
  if (!fs.existsSync(CAP_DIR)) return caps;
  for (const f of fs.readdirSync(CAP_DIR)) {
    if (!f.endsWith('.js')) continue;
    const name = f.replace(/\.js$/, '');
    try {
      const code = fs.readFileSync(path.join(CAP_DIR, f), 'utf8');
      const meta = parseCapabilityMeta(code) || { name };
      caps[name] = { ...meta, file: f };
    } catch (e) {
      caps[name] = { name, error: e.message, file: f };
    }
  }
  return caps;
}

function parseCapabilityMeta(code) {
  const headerEnd = code.indexOf('*/');
  if (headerEnd === -1) return null;
  const block = code.slice(0, headerEnd + 2);
  const m = block.match(/@capability\s+([^\n]+)/);
  if (!m) return null;
  const meta = { name: m[1].trim() };
  for (const line of block.split('\n')) {
    const kv = line.match(/^\s*\*\s*@(\w+)\s+(.+)$/);
    if (kv && kv[1] !== 'capability') meta[kv[1]] = kv[2].trim();
  }
  return meta;
}

// ─── v0.2 核心: 调 LLM 生成 main() 函数体 ─────────────────
async function generateMainBody(intent, caps, wantCaps, lang) {
  const llmConfig = {
    base_url: intent.llm_base_url || LLM_DEFAULTS.base_url,
    api_key:  intent.llm_api_key  || LLM_DEFAULTS.api_key,
    model:    intent.llm_model    || LLM_DEFAULTS.model,
  };

  if (!llmConfig.api_key) {
    return { ok: false, reason: 'no_api_key', fallback: 'stub' };
  }

  // 构建能力说明
  const capDescriptions = wantCaps.map(c => {
    const cap = caps[c];
    const sig = cap && cap.signature ? ` — 签名: ${cap.signature}` : '';
    const desc = cap && cap.description ? cap.description : c;
    return `- ${c}: ${desc}${sig}`;
  }).join('\n');

  // 构造 prompt (用 user role, 不用 system — 守护人格洁净)
  const prompt = lang === 'js'
    ? [
        `你是一个纯代码生成器。只输出代码，不输出任何解释。`,
        ``,
        `任务: 为工具 "${intent.name}" 生成 Node.js 的 main(args) 函数体。`,
        `工具描述: ${intent.description || '(无描述)'}`,
        ``,
        `可用能力 (已通过 caps 对象加载，直接调用即可):`,
        capDescriptions || '(无)',
        ``,
        `caps 对象结构: { "能力名": require("./capabilities/能力名.js") }`,
        `每个能力是一个 async function，直接 await caps["能力名"](参数) 调用。`,
        ``,
        `要求:`,
        `1. 只输出 async function main(args) { ... } 的完整函数体`,
        `2. 用 \`\`\`javascript 代码块包裹`,
        `3. 函数必须返回一个结果对象`,
        `4. 错误处理用 try/catch`,
        `5. 不要 require 任何外部包，只用 Node.js 内置模块和 caps 里的能力`,
        `6. 不要输出任何解释文字，只要代码`,
      ].join('\n')
    : [
        `你是一个纯代码生成器。只输出代码，不输出任何解释。`,
        ``,
        `任务: 为工具 "${intent.name}" 生成 Bash 的 main() 函数体。`,
        `工具描述: ${intent.description || '(无描述)'}`,
        ``,
        `可用能力 (通过 node 桥接调用):`,
        capDescriptions || '(无)',
        ``,
        `调用方式: node -e "require('$CAP_DIR/能力名.js')(参数).then(r => console.log(JSON.stringify(r)))"`,
        ``,
        `要求:`,
        `1. 只输出 main() { ... } 的完整函数体`,
        `2. 用 \`\`\`bash 代码块包裹`,
        `3. 用 set -euo pipefail`,
        `4. 不要输出任何解释文字，只要代码`,
      ].join('\n');

  try {
    const result = await llmChat({
      base_url: llmConfig.base_url,
      api_key:  llmConfig.api_key,
      model:    llmConfig.model,
      messages: [{ role: 'user', content: prompt }],
      temperature: 0.2,  // 低温度，代码生成要稳
      max_tokens: 4096,
    });

    if (result.status !== 200) {
      return { ok: false, reason: `llm_error_${result.status}`, body: result.body, fallback: 'stub' };
    }

    const content = result.body?.choices?.[0]?.message?.content;
    if (!content) {
      return { ok: false, reason: 'empty_response', fallback: 'stub' };
    }

    const code = extractCode(content, lang);
    if (!code) {
      return { ok: false, reason: 'no_code_block', raw: content, fallback: 'stub' };
    }

    return { ok: true, code };
  } catch (e) {
    return { ok: false, reason: e.message, fallback: 'stub' };
  }
}

// 从 LLM 返回中提取代码块
function extractCode(content, lang) {
  // 优先匹配 ```javascript 或 ```bash 代码块
  const patterns = lang === 'js'
    ? [/```(?:javascript|js)\s*\n([\s\S]*?)```/, /```\s*\n([\s\S]*?)```/]
    : [/```(?:bash|sh)\s*\n([\s\S]*?)```/, /```\s*\n([\s\S]*?)```/];

  for (const re of patterns) {
    const m = content.match(re);
    if (m && m[1].trim()) return m[1].trim();
  }

  // 如果没有代码块标记，整段内容当代码（LLM可能直接输出代码）
  const trimmed = content.trim();
  if (trimmed.includes('function') || trimmed.includes('const ') || trimmed.includes('#!/')) {
    return trimmed;
  }
  return null;
}

// 语法校验
function syntaxCheck(filePath, lang) {
  if (lang === 'js') {
    const r = spawnSync('node', ['-c', filePath], { stdio: 'pipe' });
    return { ok: r.status === 0, stderr: r.stderr?.toString() };
  }
  if (lang === 'sh') {
    const r = spawnSync('bash', ['-n', filePath], { stdio: 'pipe' });
    return { ok: r.status === 0, stderr: r.stderr?.toString() };
  }
  return { ok: true };
}

// ─── pen.write v0.2 : 从 intent 合成工具 (接LLM) ──────────
async function penWrite(intent) {
  if (!intent || !intent.name) throw new Error('intent.name required');
  if (!/^[a-z][a-z0-9-]{1,63}$/.test(intent.name)) {
    throw new Error('intent.name must be 2-64 chars, lowercase a-z / digits / hyphen');
  }
  const lang = intent.language || 'js';
  const tplFile = path.join(TPL_DIR, `tool-${lang}.template`);
  if (!fs.existsSync(tplFile)) {
    throw new Error(`Unsupported language: ${lang}`);
  }
  const dest = path.join(PENNED_DIR, intent.name);
  if (fs.existsSync(dest)) throw new Error(`Tool already exists: ${intent.name}`);
  fs.mkdirSync(dest, { recursive: true });

  const caps = loadCapabilities();
  const wantCaps = (intent.capabilities || []).filter((c) => caps[c]);
  const missing = (intent.capabilities || []).filter((c) => !caps[c]);

  // ── 读模板 ──
  let tpl = fs.readFileSync(tplFile, 'utf8');
  tpl = tpl
    .replace(/\*\*TOOL_NAME\*\*/g, intent.name)
    .replace(/\*\*TOOL_DESC\*\*/g, (intent.description || '').replace(/\n/g, ' '))
    .replace(/\*\*TOOL_CAPS\*\*/g, JSON.stringify(wantCaps))
    .replace(/\*\*GENERATED_AT\*\*/g, new Date().toISOString());

  // ── v0.2: 调 LLM 生成 main() ──
  let llmResult = { ok: false, fallback: 'stub' };
  let generationMode = 'stub';  // 'llm' or 'stub'

  if (intent.stub !== true) {  // 允许强制 stub 模式: { stub: true }
    llmResult = await generateMainBody(intent, caps, wantCaps, lang);
  }

  if (llmResult.ok && llmResult.code) {
    generationMode = 'llm';
    // 替换模板中的 stub main()
    if (lang === 'js') {
      // 替换 async function main(args) { ... } 整个函数
      tpl = tpl.replace(
        /async function main\(args\) \{[\s\S]*?^\}/m,
        llmResult.code
      );
      // 如果替换失败（正则没匹配），追加到文件末尾
      if (tpl.includes('fresh-penned tool stub')) {
        tpl = tpl.replace(
          /\/\/ TODO:.*\n.*return \{[\s\S]*?note:.*\n.*\};/,
          llmResult.code.includes('async function main')
            ? llmResult.code.match(/async function main\(args\) \{([\s\S]*)\}/)?.[1] || llmResult.code
            : llmResult.code
        );
      }
    } else if (lang === 'sh') {
      tpl = tpl.replace(
        /main\(\) \{[\s\S]*?^\}/m,
        llmResult.code
      );
    }
  }

  const entryFile = lang === 'sh' ? 'main.sh' : 'index.js';
  fs.writeFileSync(path.join(dest, entryFile), tpl);
  if (lang === 'sh') {
    fs.chmodSync(path.join(dest, entryFile), 0o755);
  }

  // ── 语法校验 ──
  const check = syntaxCheck(path.join(dest, entryFile), lang);
  if (!check.ok && generationMode === 'llm') {
    // LLM 生成的代码语法错误，回退到 stub
    process.stderr.write(`[zhuyuan-pen] WARN: LLM code syntax error, falling back to stub\n`);
    process.stderr.write(`[zhuyuan-pen] stderr: ${check.stderr}\n`);
    let fallbackTpl = fs.readFileSync(tplFile, 'utf8');
    fallbackTpl = fallbackTpl
      .replace(/\*\*TOOL_NAME\*\*/g, intent.name)
      .replace(/\*\*TOOL_DESC\*\*/g, (intent.description || '').replace(/\n/g, ' '))
      .replace(/\*\*TOOL_CAPS\*\*/g, JSON.stringify(wantCaps))
      .replace(/\*\*GENERATED_AT\*\*/g, new Date().toISOString());
    fs.writeFileSync(path.join(dest, entryFile), fallbackTpl);
    generationMode = 'stub_fallback';
  }

  const meta = {
    _sovereign: 'TCS-0002∞',
    _copyright: '国作登字-2026-A-00037559',
    name: intent.name,
    description: intent.description || '',
    language: lang,
    capabilities: wantCaps,
    missing_capabilities: missing,
    entry: entryFile,
    generation_mode: generationMode,  // v0.2 新增: 'llm' | 'stub' | 'stub_fallback'
    generated_at: new Date().toISOString(),
    pen_version: '0.2.0',
    sha256: crypto.createHash('sha256')
      .update(fs.readFileSync(path.join(dest, entryFile), 'utf8'))
      .digest('hex'),
  };

  // 如果 LLM 生成失败，记录原因
  if (!llmResult.ok && generationMode !== 'llm') {
    meta.llm_fallback_reason = llmResult.reason || 'unknown';
  }

  fs.writeFileSync(path.join(dest, 'tool.meta.json'), JSON.stringify(meta, null, 2));

  fs.writeFileSync(
    path.join(dest, 'README.md'),
    [
      `# ${intent.name}`,
      '',
      `> 生成时间: ${meta.generated_at}`,
      `> 笔: zhuyuan-pen v0.2.0`,
      `> 生成模式: ${generationMode}`,
      '',
      intent.description || '(no description)',
      '',
      '## 能力依赖',
      ...wantCaps.map((c) => `- \`${c}\` — ${caps[c].description || ''}`),
      '',
      missing.length ? '## ⚠️ 缺失能力 (需先扩字典)\n\n' + missing.map((c) => `- \`${c}\``).join('\n') : '',
    ]
      .filter(Boolean)
      .join('\n')
  );

  return { ok: true, path: dest, meta, generationMode };
}

function penList() {
  const out = [];
  for (const d of fs.readdirSync(PENNED_DIR)) {
    const metaFile = path.join(PENNED_DIR, d, 'tool.meta.json');
    if (fs.existsSync(metaFile)) {
      out.push(JSON.parse(fs.readFileSync(metaFile, 'utf8')));
    }
  }
  return out;
}

// ─── pen.fetch : 国内 / 海外双管道取资源 (不变) ─────────────
function penFetch(url, channel = 'auto', destFile = null) {
  const target = destFile || path.join(PEN_ROOT, 'cache', path.basename(new URL(url).pathname) || 'fetched.bin');
  fs.mkdirSync(path.dirname(target), { recursive: true });
  const ch = channel === 'auto' ? 'cn' : channel;
  if (ch === 'cn') {
    const r = spawnSync('curl', ['-fsSL', '--max-time', '120', '-o', target, url], { stdio: 'inherit' });
    if (r.status === 0) return { ok: true, channel: 'cn', target };
    if (channel === 'cn') throw new Error(`curl failed (cn): ${url}`);
    return penFetch(url, 'overseas', destFile);
  }
  if (ch === 'overseas') {
    const host = process.env.ZY_OVERSEAS_RELAY_HOST || process.env.CN_OVERSEAS_RELAY_HOST;
    const user = process.env.ZY_OVERSEAS_RELAY_USER || process.env.CN_OVERSEAS_RELAY_USER || 'root';
    const keyFile = process.env.ZY_OVERSEAS_RELAY_KEY_FILE || `${process.env.HOME}/.ssh/zhuyuan_overseas_key`;
    if (!host) throw new Error('overseas relay host unset (ZY_OVERSEAS_RELAY_HOST)');
    const remoteTmp = `/tmp/zhuyuan-pen-${Date.now()}.bin`;
    const sshArgs = ['-i', keyFile, '-o', 'StrictHostKeyChecking=accept-new', `${user}@${host}`];
    const fetchR = spawnSync('ssh', [...sshArgs, `curl -fsSL --max-time 300 -o ${remoteTmp} '${url}'`], { stdio: 'inherit' });
    if (fetchR.status !== 0) throw new Error('overseas curl failed');
    const scpR = spawnSync('scp', ['-i', keyFile, '-o', 'StrictHostKeyChecking=accept-new', `${user}@${host}:${remoteTmp}`, target], { stdio: 'inherit' });
    if (scpR.status !== 0) throw new Error('overseas scp failed');
    spawnSync('ssh', [...sshArgs, `rm -f ${remoteTmp}`], { stdio: 'ignore' });
    return { ok: true, channel: 'overseas', target };
  }
  throw new Error(`unknown channel: ${channel}`);
}

function penRegister(toolName) {
  const metaFile = path.join(PENNED_DIR, toolName, 'tool.meta.json');
  if (!fs.existsSync(metaFile)) throw new Error(`Tool not found: ${toolName}`);
  const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));

  const regFile = path.join(PEN_ROOT, 'registry.json');
  let reg = { tools: [] };
  if (fs.existsSync(regFile)) {
    reg = JSON.parse(fs.readFileSync(regFile, 'utf8'));
  }
  reg.tools = (reg.tools || []).filter((t) => t.name !== toolName);
  reg.tools.push({
    name: toolName,
    entry: `penned/${toolName}/${meta.entry}`,
    language: meta.language,
    capabilities: meta.capabilities,
    sha256: meta.sha256,
    generation_mode: meta.generation_mode,
    registered_at: new Date().toISOString(),
  });
  reg._sovereign = 'TCS-0002∞';
  reg._copyright = '国作登字-2026-A-00037559';
  reg._updated_at = new Date().toISOString();
  fs.writeFileSync(regFile, JSON.stringify(reg, null, 2));

  return { ok: true, registry: regFile, total: reg.tools.length };
}

function penCapabilities() {
  return loadCapabilities();
}

// ─── 命令行 / MCP stdio JSON-RPC ──────────────────────────
const ACTIONS = {
  'pen.write': (p) => penWrite(p),       // async!
  'pen.list': () => penList(),
  'pen.fetch': (p) => penFetch(p.url, p.channel, p.dest),
  'pen.register': (p) => penRegister(p.name),
  'pen.capabilities': () => penCapabilities(),
};

async function cli() {
  const [, , action, ...rest] = process.argv;
  if (!action || action === '--help' || action === '-h') {
    console.log('zhuyuan-pen · 神笔马良 v0.2.0');
    console.log('  画什么, 什么成真');
    console.log('');
    console.log('actions:');
    for (const a of Object.keys(ACTIONS)) console.log(`  ${a}`);
    console.log('');
    console.log('env vars:');
    console.log('  ZY_LLM_BASE_URL  — LLM API base (default: siliconflow)');
    console.log('  ZY_LLM_API_KEY   — LLM API key (required for LLM mode)');
    console.log('  ZY_LLM_MODEL     — model name (default: Qwen2.5-Coder-32B)');
    console.log('');
    console.log('examples:');
    console.log('  zhuyuan-pen pen.capabilities');
    console.log('  zhuyuan-pen pen.write \'{"name":"hello","description":"demo","capabilities":["fs.read"]}\'');
    console.log('  zhuyuan-pen pen.write \'{"name":"hello","stub":true}\'  # 强制stub模式');
    return;
  }
  if (!ACTIONS[action]) {
    console.error(`unknown action: ${action}`);
    process.exit(1);
  }
  let payload = {};
  if (rest.length) {
    try { payload = JSON.parse(rest.join(' ')); } catch { payload = rest.join(' '); }
  }
  try {
    const r = await ACTIONS[action](payload);  // await for async
    console.log(JSON.stringify(r, null, 2));
  } catch (e) {
    console.error(`❌ ${e.message}`);
    process.exit(2);
  }
}

function mcpServe() {
  let buf = '';
  process.stdin.setEncoding('utf8');
  process.stdin.on('data', (chunk) => {
    buf += chunk;
    const lines = buf.split('\n');
    buf = lines.pop();
    for (const line of lines) {
      if (!line.trim()) continue;
      let req;
      try { req = JSON.parse(line); }
      catch (e) {
        process.stdout.write(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'parse error' } }) + '\n');
        continue;
      }
      const action = req.method;
      const id = req.id;
      if (!ACTIONS[action]) {
        process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: `unknown method: ${action}` } }) + '\n');
        continue;
      }
      // v0.2: 支持 async handler
      Promise.resolve(ACTIONS[action](req.params || {}))
        .then((result) => {
          process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
        })
        .catch((e) => {
          process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: e.message } }) + '\n');
        });
    }
  });
}

if (require.main === module) {
  if (process.env.ZHUYUAN_PEN_MODE === 'mcp' || process.argv.includes('--mcp')) {
    mcpServe();
  } else {
    cli();
  }
}

module.exports = {
  penWrite, penList, penFetch, penRegister, penCapabilities,
};
```

---

## Step 3: 更新测试文件

替换 `tests/test-pen.js`：

```bash
cat > /data/guanghulab/mcp-servers/zhuyuan-pen/tests/test-pen.js << 'TESTEOF'
#!/usr/bin/env node
'use strict';

const path = require('path');
const fs = require('fs');
const { penWrite, penList, penCapabilities } = require('../src/server.js');

const PENNED_DIR = path.resolve(__dirname, '..', 'penned');

async function runTests() {
  let passed = 0;
  let failed = 0;

  function assert(name, condition) {
    if (condition) { console.log(`  ✅ ${name}`); passed++; }
    else { console.log(`  ❌ ${name}`); failed++; }
  }

  // Test 1: capabilities
  console.log('\n[test] pen.capabilities');
  const caps = penCapabilities();
  assert('has llm.chat', !!caps['llm.chat']);
  assert('has fs.read', !!caps['fs.read']);
  assert('has >= 7 capabilities', Object.keys(caps).length >= 7);

  // Test 2: pen.write stub mode
  console.log('\n[test] pen.write (stub mode)');
  const testToolName = `test-stub-${Date.now()}`;
  const result = await penWrite({
    name: testToolName,
    description: 'test stub tool',
    language: 'js',
    capabilities: ['fs.read'],
    stub: true,  // 强制 stub
  });
  assert('write ok', result.ok);
  assert('mode is stub', result.generationMode === 'stub');
  assert('meta has pen_version 0.2.0', result.meta.pen_version === '0.2.0');
  assert('entry file exists', fs.existsSync(path.join(result.path, 'index.js')));
  assert('meta file exists', fs.existsSync(path.join(result.path, 'tool.meta.json')));

  // Test 3: pen.list
  console.log('\n[test] pen.list');
  const list = penList();
  assert('list includes test tool', list.some(t => t.name === testToolName));

  // Cleanup
  fs.rmSync(path.join(PENNED_DIR, testToolName), { recursive: true });

  // Summary
  console.log(`\n${'═'.repeat(40)}`);
  if (failed === 0) {
    console.log(`✅ all ${passed} pen tests passed (v0.2)`);
  } else {
    console.log(`❌ ${failed} failed, ${passed} passed`);
    process.exit(1);
  }
}

runTests().catch(e => {
  console.error(`❌ test error: ${e.message}`);
  process.exit(1);
});
TESTEOF
```

---

## Step 4: 部署 & 验证

```bash
cd /data/guanghulab/mcp-servers/zhuyuan-pen

# 1. 设置 LLM 环境变量（先确认你有硅基流动的key）
export ZY_LLM_BASE_URL="https://api.siliconflow.cn/v1"
export ZY_LLM_API_KEY="你的key"
export ZY_LLM_MODEL="Qwen/Qwen2.5-Coder-32B-Instruct"

# 2. 跑测试（stub模式，不需要LLM key）
node tests/test-pen.js

# 3. 测试 LLM 模式 — 写一个真工具
node src/server.js pen.write '{
  "name": "check-disk",
  "description": "检查服务器磁盘使用率，超过80%告警",
  "language": "sh",
  "capabilities": ["shell.run"]
}'

# 4. 看生成的工具
cat penned/check-disk/main.sh
cat penned/check-disk/tool.meta.json

# 5. 确认 generation_mode 是 "llm" 不是 "stub"
grep generation_mode penned/check-disk/tool.meta.json
```

---

## v0.1 → v0.2 变更总结

| 项目 | v0.1 | v0.2 |
| --- | --- | --- |
| `pen.write` | 同步，生成stub | **异步，调LLM生成真代码** |
| `main()` 函数体 | 空壳模板 | **LLM自动填充** |
| 语法校验 | 无 | **生成后自动 node -c / bash -n** |
| 降级策略 | 无 | **LLM失败 → 自动回退stub** |
| 强制stub | 无 | **intent.stub=true** |
| meta 字段 | 基础 | **新增 generation_mode / pen_version / llm_fallback_reason** |
| MCP handler | 同步 | **支持 async** |
| 版本 | 0.1.0 | **0.2.0** |

---

## AutoDL 模型路径（实测纠正）

| 模型 | 实际路径 |
| --- | --- |
| coder-7b | `/root/autodl-tmp/models/coder-7b` |
| motherbrain-v1 | `/root/autodl-tmp/models/motherbrain-v1` |

⚠️ 之前记录的 `/root/autodl-tmp/models/qwen2_5_coder_7b_sft` 不存在，正确路径如上。

vLLM 启动命令：

```bash
python -m vllm.entrypoints.openai.api_server \
  --model /root/autodl-tmp/models/coder-7b \
  --host 0.0.0.0 \
  --port 6006 \
  --trust-remote-code \
  --dtype half &
```

---

## 注意事项

1. **人格守护不变** — [llm.chat](http://llm.chat) 仍然强制剥离 system role，用 user message 传递指令
2. **降级安全** — 如果没有 API key 或 LLM 报错或生成代码语法错误，自动回退到 stub 模板，不会崩
3. **兼容 v0.1** — pen.list / pen.fetch / pen.register / pen.capabilities 行为完全不变
4. **先配好 API key 再测 LLM 模式** — 没有 key 会降级到 stub（功能不报错，但不会生成真代码）