Initial commit: 让多个 AI 智能体通过文件总线在共享聊天室里互相 @、协作与交接任务

This commit is contained in:
WpyQwq
2026-09-19 11:52:42 +08:00
commit 103f0b32f5
28 changed files with 5834 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env node
/**
* model-probe.mjs — 把各渠道的候选免费模型全量探一遍,并按并发压测。
*
* 用法:
* node tools/model-probe.mjs # 全量探测(并发 4)
* node tools/model-probe.mjs --concurrency 6 # 改并发
* node tools/model-probe.mjs --only opencode # 只探某个渠道
*
* 输出:表格 + tools/model-probe-result.json(含每个模型的 ok / 耗时 / 错误)
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const SCRIPTS = path.resolve(HERE, '..', 'scripts');
const argv = process.argv.slice(2);
const flag = (n, d) => { const i = argv.indexOf('--' + n); return i > -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d; };
const CONC = Number(flag('concurrency', 4));
const ONLY = flag('only', '');
const HOME = os.homedir();
const QODER = path.join(HOME, '.qoderwake-cn', 'bin', 'qodercli', 'qodercli-cn-wake.exe');
const QODER_CFG = path.join(HOME, '.qoderwake-cn');
const CBC_BIN = 'H:/workbuddy/resources/app.asar.unpacked/cli/bin/codebuddy';
const CBC_NODE = (() => {
const base = path.join(HOME, '.workbuddy', 'binaries', 'node', 'versions');
try {
const v = fs.readdirSync(base).filter((x) => /^\d/.test(x)).sort().reverse()[0];
return path.join(base, v, 'node.exe');
} catch { return process.execPath; }
})();
const OC = path.join(process.env.APPDATA || '', 'npm', 'node_modules', 'opencode-ai', 'bin', 'opencode.exe');
/** 候选清单:渠道 / 模型 id / 怎么调 */
const CANDIDATES = [
// --- opencode Zen 的免费档 ---
...['big-pickle', 'nemotron-3.5-lightning-free', 'nemotron-3-ultra-free', 'ling-3.0-flash-fin-free', 'mimo-v2.5-free']
.map((m) => ({ channel: 'opencode', model: m, cmd: OC, args: ['run', '--format', 'json', '-m', `opencode/${m}`] })),
// --- Qoder CLI ---
...['Qwen3.8-Flash', 'Qwen3.8-Max']
.map((m) => ({ channel: 'qoder', model: m, cmd: QODER, args: ['--config-dir', QODER_CFG, '-p', '-o', 'json', '-m', m] })),
// --- WorkBuddy 随附的 CodeBuddy CLI ---
...['hy4-preview-f', 'hy3', 'hy4-preview']
.map((m) => ({ channel: 'codebuddy', model: m, cmd: CBC_NODE, args: [CBC_BIN, '-p', '--output-format', 'json', '--model', m] })),
// --- ZCode 的 BigModel API(enabled: true 的那些) ---
...['GLM-4-Flash-250414', 'GLM-4-FlashX-250414', 'GLM-4.1V-Thinking-Flash', 'GLM-4V-Flash', 'codegeex-4']
.map((m) => ({
channel: 'bigmodel', model: m, cmd: process.execPath,
args: [path.join(SCRIPTS, 'api-call.mjs'), '--base', 'https://open.bigmodel.cn/api/paas/v4', '--model', m,
'--key-file', path.join(HOME, '.zcode', 'v2', 'provider_config.json'),
'--key-path', 'config.providerConfigRules.providerRules.0.config.access.apiKey'],
})),
];
const list = ONLY ? CANDIDATES.filter((c) => c.channel === ONLY) : CANDIDATES;
/** 从各家输出里把"这次花了多少钱"抠出来——判"完全免费"只认模型自己报的数,不认印象。 */
function extractCost(channel, text) {
const t = text.replace(/\u001b\[[0-9;]*m/g, '');
if (channel === 'opencode') {
let sum = 0, seen = false;
for (const line of t.split('\n')) {
const s = line.trim();
if (!s.startsWith('{')) continue;
try {
const ev = JSON.parse(s);
if (ev?.type === 'step_finish' && typeof ev.part?.cost === 'number') { sum += ev.part.cost; seen = true; }
} catch { /* 跳过 */ }
}
return seen ? sum : null;
}
// qoder / codebuddy:Claude Code 风格的 result 对象(或数组)
for (const chunk of [t, ...t.split('\n')]) {
const s = chunk.trim();
if (!s.startsWith('{') && !s.startsWith('[')) continue;
try {
const j = JSON.parse(s);
const objs = Array.isArray(j) ? j.reverse() : [j];
for (const o of objs) if (o && typeof o.total_cost_usd === 'number') return o.total_cost_usd;
} catch { /* 继续 */ }
}
return null; // bigmodel 这类只回 usage,没有价钱字段
}
function runOne(c) {
const prompt = `只回复四个字:${c.model} 就绪`;
const t0 = Date.now();
return new Promise((resolve) => {
const child = spawn(c.cmd, [...c.args, prompt], { windowsHide: true, cwd: os.tmpdir(), env: { ...process.env, NO_COLOR: '1' } });
let out = '', err = '';
const timer = setTimeout(() => { try { child.kill(); } catch { /* 忽略 */ } }, 180000);
child.stdout.on('data', (d) => { out += d; });
child.stderr.on('data', (d) => { err += d; });
child.on('close', (code) => {
clearTimeout(timer);
const ms = Date.now() - t0;
const text = (out + '\n' + err).replace(/\u001b\[[0-9;]*m/g, '');
const ok = code === 0 && !/not available in your country|No auth|not logged in|HTTP 4\d\d|Error:/i.test(text);
const cost = ok ? extractCost(c.channel, out + '\n' + err) : null;
resolve({ ...c, ok, code, ms, cost, sample: text.replace(/\s+/g, ' ').slice(0, 110) });
});
child.on('error', (e) => { clearTimeout(timer); resolve({ ...c, ok: false, code: -1, ms: Date.now() - t0, cost: null, sample: String(e.message) }); });
});
}
async function pool(items, size, fn) {
const out = new Array(items.length);
let i = 0;
await Promise.all(Array.from({ length: Math.min(size, items.length) }, async () => {
while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx]); }
}));
return out;
}
const t0 = Date.now();
const results = await pool(list, CONC, runOne);
const totalMs = Date.now() - t0;
results.sort((a, b) => Number(b.ok) - Number(a.ok) || a.ms - b.ms);
const fmtCost = (c) => (c === null ? '未报' : c === 0 ? '0 ✔' : String(c));
const head = ['渠道', '模型', '结果', '耗时', '这次花费'];
const rows = results.map((r) => [r.channel, r.model, r.ok ? 'OK' : `FAIL(${r.code})`, `${(r.ms / 1000).toFixed(1)}s`, r.ok ? fmtCost(r.cost) : '-']);
const w = head.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i]).length)));
const line = (c) => c.map((x, i) => String(x).padEnd(w[i])).join(' ');
console.log(`\n并发 ${CONC} · ${list.length} 个候选 · 总耗时 ${(totalMs / 1000).toFixed(1)}s\n`);
console.log(line(head)); console.log(line(w.map((n) => '─'.repeat(n))));
for (const r of rows) console.log(line(r));
const bad = results.filter((r) => !r.ok);
if (bad.length) { console.log('\n失败详情:'); for (const b of bad) console.log(` ${b.channel}/${b.model}:${b.sample}`); }
const okCount = results.filter((r) => r.ok).length;
console.log(`\n可用 ${okCount}/${results.length}`);
fs.writeFileSync(path.join(HERE, 'model-probe-result.json'), JSON.stringify({ at: new Date().toISOString(), concurrency: CONC, totalMs, results }, null, 2), 'utf8');