# 神笔马良 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(功能不报错,但不会生成真代码)