spike 验证通过:M3 经 new-api(openai+tools)agentic 自闭环产出可玩 A-model 游戏。 - harness game-runtime/tools/amodel-gen/(tools/play/m3/prompt/gen.mjs,零三方依赖): scaffold(clone _template)→ ReAct(read/write/list/check/build/done;done 门=check+build 绿; compaction + loop-detection)→ 循环外 play(headless Chrome boot+截图+交互 = 能跑能玩)。 - 3/3 玩法干净自闭环实证:打地鼠 / 陨石躲避 / 反应点击(13-22 步,175-378K token,play PASS)。 - skill .agents/skills/agentic-amodel-generation.md(链路/配方/8 坑/M3 实证/主流定性)+ README/AGENTS 索引同步。 - 设计 docs/agent-specs/2026-06-21-agentic-amodel-generation-design.md(§0 整链路 current→new + 四层 + §0.8 主流对照;draft 待 §6.8 双评审)。 取代旧 gamedef/factory 单次产线(A-model 生产形态);production 删 gamedef 待双评审(D1=A 无退路)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
3.2 KiB
JavaScript
76 lines
3.2 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 = {}) {
|
||
const { maxTokens = 16000, temperature = 0.3, timeoutMs = 180000 } = 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 };
|