Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

692 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

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