lili 6e388d9bbb feat(amodel-gen): agentic A-model 生成 harness(ReAct+M3)+ skill + 四层设计
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>
2026-06-21 18:50:38 -07:00

76 lines
3.2 KiB
JavaScript
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.

/**
* 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 };