钻 reading 'w' 根因(minified 栈实锤):bootGameHost 读 i.viewport.w,而 buildTemplateHostConfig 返回的 viewport=undefined → M3 改 host-config 时破坏返回契约(漏 viewport 键/删 render.js export W/H)。非 minification、非产不出(两局当前 src 生产 min build 后 smoke PASS)。 更广模式:M3 游戏逻辑写得了(check+build 过),但反复搞坏插件 wiring——3 契约点:① host-config return shape(viewport)② registerOrder ③ game.js 取插件漏 .plugins 一层(createGame(opts.runtime) 后从 runtime 解构 → sceneFsm=undefined → reading 'define')。 本次改进: - footgun ×2(prompt.mjs 红线 + littlejs skill ❌→✅):buildTemplateHostConfig 必返 5 键+viewport{w,h};取插件走两层 opts.runtime.plugins。 - m3.mjs timeout 180→300s:M3 大上下文 turn 触发 thinking 单 turn 实测 ~215s(裸简单调用仅 1-3s);180s 误超时+重试更拖。实测效果:wfix3 从 read-thrash(63读0build)转为真写码(27write/check+build PASS)、viewport bug 未再现。 注:踩了 prompt.mjs 模板字面量反引号坑 2 次(node --check 拦下,未浪费 run)——红线:prompt.mjs 内禁反引号。 未达绿:amodel e2e 仍 0/1——wiring 错类是 whack-a-mole(footgun 一个、M3 露下一个),且 per-attempt 步数预算下难一次收口。下一步战略(待创始人定):结构性把 host-config + 工厂 glue 设为固定 plumbing(agent 只写游戏逻辑),根除 wiring 错类。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.5 KiB
JavaScript
78 lines
3.5 KiB
JavaScript
/**
|
||
* m3.mjs — new-api M3 客户端(openai 协议 + function-calling)
|
||
* owner:amodel-gen spike | 消费方:gen.mjs(ReAct 循环每轮调 chat)
|
||
*
|
||
* 单一职责:发一轮 chat/completions(带 tools)→ 返 {message, usage, finishReason}。
|
||
* 含超时 + 重试(429/5xx/IO 重试;4xx 不重试)。模型固定 MiniMax-M3(founder 定)。
|
||
* 密钥(§9):优先 env NEWAPI_KEY,否则从 docs/内网凭据与端点.md(内网允许入仓)解析——doc 是单一事实源。
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
import { readFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { REPO_ROOT } from './tools.mjs';
|
||
|
||
const BASE = (process.env.NEWAPI_BASE_URL || 'http://100.64.0.8:3000').replace(/\/+$/, '');
|
||
const MODEL = process.env.AMGEN_MODEL || 'MiniMax-M3'; // founder:只用 M3
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
/** 取网关 token:env 优先,否则解析项目密钥文档(§9 单一事实源)。 */
|
||
function getKey() {
|
||
if (process.env.NEWAPI_KEY) return process.env.NEWAPI_KEY;
|
||
try {
|
||
const doc = readFileSync(join(REPO_ROOT, 'docs/内网凭据与端点.md'), 'utf8');
|
||
const m = doc.match(/NEWAPI_KEY[^\n]*?(sk-[A-Za-z0-9]+)/);
|
||
if (m) return m[1];
|
||
} catch { /* fall through */ }
|
||
throw new Error('找不到 NEWAPI_KEY(请设 env 或确认 docs/内网凭据与端点.md 存在)');
|
||
}
|
||
const KEY = getKey();
|
||
|
||
/**
|
||
* 发一轮带 tools 的 chat/completions。
|
||
* @param {Array<object>} messages openai 消息数组(含 system/user/assistant/tool)
|
||
* @param {Array<object>} tools function-calling 工具定义
|
||
* @param {{maxTokens?:number, temperature?:number, timeoutMs?:number}} [opt]
|
||
* @returns {Promise<{message:object, usage:object, finishReason:string}>}
|
||
*/
|
||
export async function chat(messages, tools, opt = {}) {
|
||
// timeout 300s:M3 在大上下文/代码生成 turn 会触发服务端 thinking、单 turn 实测达 ~215s(裸简单调用仅 1-3s);
|
||
// 180s 会误超时→重试(反而拖长:180s 等 + 再来一轮)。300s 让 thinking turn 一次完成、不浪费重试(HJ-AGI-003 实测)。
|
||
const { maxTokens = 16000, temperature = 0.3, timeoutMs = 300000 } = opt;
|
||
const body = { model: MODEL, messages, tools, tool_choice: 'auto', max_tokens: maxTokens, temperature };
|
||
let lastErr;
|
||
for (let attempt = 0; attempt < 3; attempt++) {
|
||
const ctrl = new AbortController();
|
||
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||
try {
|
||
const r = await fetch(`${BASE}/v1/chat/completions`, {
|
||
method: 'POST',
|
||
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
signal: ctrl.signal,
|
||
});
|
||
clearTimeout(timer);
|
||
if (!r.ok) {
|
||
const txt = await r.text().catch(() => '');
|
||
// 4xx(除 429)= 请求本身错,不重试,直接抛
|
||
if (r.status !== 429 && r.status < 500) throw new Error(`HTTP ${r.status}: ${txt.slice(0, 300)}`);
|
||
lastErr = new Error(`HTTP ${r.status}: ${txt.slice(0, 200)}`);
|
||
await sleep(1500 * (attempt + 1));
|
||
continue;
|
||
}
|
||
const d = await r.json();
|
||
const ch = d.choices && d.choices[0];
|
||
if (!ch) throw new Error('响应无 choices: ' + JSON.stringify(d).slice(0, 300));
|
||
return { message: ch.message, usage: d.usage || {}, finishReason: ch.finish_reason };
|
||
} catch (e) {
|
||
clearTimeout(timer);
|
||
lastErr = e;
|
||
await sleep(1500 * (attempt + 1)); // 超时/IO 重试
|
||
}
|
||
}
|
||
throw lastErr || new Error('M3 chat 失败(重试耗尽)');
|
||
}
|
||
|
||
export { MODEL, BASE };
|