Files
AI_spine/tools/model-check.mjs
T
2026-09-17 18:50:43 +08:00

108 lines
7.1 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.
#!/usr/bin/env node
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {loadConfig, resolveStage, publicStage, samInfo} from '../lib/config.mjs';
import {requestJson} from '../providers/http.mjs';
import {runUnderstanding, runCompletionProbe} from '../stages/model-stage.mjs';
import {readImage} from '../lib/images.mjs';
const help = `模型接入与结构理解(新增模式,不调用旧拆件流程)
node pipeline.mjs --mode annotated-parts --reference image.png --out-dir output/run --stop-after understand
node tools/model-check.mjs --stage completion --image local.png --prompt-file task.txt --out-dir output/edit
node tools/model-check.mjs --list-models
node tools/model-check.mjs --dry-run
公共选项:
--config FILE 默认 configs/astra-qwen.json(prompt 相对配置文件)
--env-file FILE 默认项目 .env,shell 环境变量优先
--understanding-model ID 理解模型覆盖(使用原样 ID,不自动映射)
--completion-model ID 图片编辑模型覆盖
--understanding-protocol NAME chat-completions / responses
--stream true|false 覆盖理解阶段的流式设置
--output-mode NAME text-json / json-object / json-schema
--resume 输入/配置/代码和产物哈希相同时复用
--parts-file FILE 离线读取已有 v1/v2 计划,做 Alpha 校验与种子生成,不请求模型
--python FILE ai-spine Conda 环境解释器(默认使用已激活环境)
--image FILE 图片编辑输入,可重复;多图需配置 capabilities
--mask FILE 透明处可编辑的 RGBA PNG;需配置 capabilities
--dry-run 检查配置与本地输入,不请求模型、不生成产物
本轮 --stop-after 仅支持 understand。SAM 路径已配置,但分割/合并/导出尚未接入。`;
export function parseOptions(argv) {
const options = {stage: 'understanding', imagePaths: []};
const valueFlags = new Map([
['--config', 'configFile'], ['--env-file', 'envFile'], ['--reference', 'reference'], ['--out-dir', 'outDir'],
['--understanding-model', 'understandingModel'], ['--completion-model', 'completionModel'],
['--understanding-protocol', 'understandingProtocol'], ['--output-mode', 'outputMode'],
['--stage', 'stage'], ['--mask', 'maskPath'], ['--prompt-file', 'promptFile'], ['--stop-after', 'stopAfter'],
['--mode', 'mode'], ['--python', 'python'], ['--stream', 'streamRaw'], ['--parts-file', 'partsFile'],
]);
for (let i = 0; i < argv.length; i += 1) {
const flag = argv[i];
if (['--help', '-h'].includes(flag)) options.help = true;
else if (flag === '--resume') options.resume = true;
else if (flag === '--dry-run') options.dryRun = true;
else if (flag === '--list-models') options.listModels = true;
else if (valueFlags.has(flag) || flag === '--image') {
const value = argv[++i];
if (!value || value.startsWith('--')) throw new Error(`${flag} 缺少参数值`);
if (flag === '--image') options.imagePaths.push(value);
else options[valueFlags.get(flag)] = value;
} else throw new Error(`未知参数:${flag},请用 --help 查看`);
}
return options;
}
export async function main(argv = process.argv.slice(2)) {
const options = parseOptions(argv);
if (options.help) { console.log(help); return; }
if (!['understanding', 'completion'].includes(options.stage)) throw new Error('--stage 仅支持 understanding 或 completion');
if (options.partsFile && (options.stage !== 'understanding' || options.listModels)) throw new Error('--parts-file 仅用于离线结构理解');
if (options.streamRaw !== undefined && !['true', 'false'].includes(options.streamRaw)) throw new Error('--stream 仅支持 true 或 false');
options.stream = options.streamRaw === undefined ? undefined : options.streamRaw === 'true';
if (options.mode && options.mode !== 'annotated-parts') throw new Error('新入口仅支持 --mode annotated-parts');
if (options.stopAfter && (options.stopAfter !== 'understand' || options.stage !== 'understanding')) {
throw new Error('本轮仅落地模型接入和结构理解;--stop-after 目前仅支持 understand');
}
const loaded = loadConfig(options);
const overrides = options.stage === 'understanding'
? {model: options.understandingModel, protocol: options.understandingProtocol, outputMode: options.outputMode, stream: options.stream}
: {model: options.completionModel};
const stage = resolveStage(loaded, options.stage, overrides, {requireKey: !options.dryRun && !options.partsFile});
if (options.listModels) {
if (options.dryRun) throw new Error('--list-models 会请求模型列表,不能与 --dry-run 同时使用');
const {payload} = await requestJson(stage, 'models', () => ({method: 'GET'}));
if (!Array.isArray(payload.data)) throw new Error('模型列表响应缺少 data 数组,请核对中转站协议');
console.log(payload.data.map((item) => item.id).filter((id) => typeof id === 'string').join('\n'));
return;
}
if (options.dryRun) {
const understanding = resolveStage(loaded, 'understanding', {model: options.understandingModel, protocol: options.understandingProtocol, outputMode: options.outputMode, stream: options.stream}, {requireKey: false});
const completion = resolveStage(loaded, 'completion', {model: options.completionModel}, {requireKey: false});
if (options.reference) readImage(options.reference, {pngOnly: true});
for (const file of options.imagePaths) readImage(file, {pngOnly: true});
console.log(JSON.stringify({understanding: publicStage(understanding), completion: publicStage(completion), sam: samInfo(loaded), credentials_present: Boolean(stage.apiKey && stage.baseUrl), network_requested: false}, null, 2));
return;
}
if (!options.outDir) throw new Error('需要 --out-dir,建议每个实验使用独立目录');
let result;
if (options.stage === 'understanding') {
if (!options.reference) throw new Error('结构理解需要 --reference');
if (options.imagePaths.length || options.maskPath || options.promptFile) throw new Error('结构理解不接受图片编辑参数');
console.log(options.partsFile ? '角色结构理解:离线计划几何校验' : `角色结构理解:${stage.model} (${stage.protocol})`);
result = await runUnderstanding({...options, loaded, stage});
console.log(`${result.reused ? '复用已校验产物' : '结构理解完成,语义待复核'}:${path.join(result.out, 'report.html')}`);
} else {
if (options.reference) throw new Error('图片编辑使用 --image 指定局部画布;额外参考图也通过重复 --image 指定');
console.log(`图片编辑接入测试:${stage.model}`);
result = await runCompletionProbe({...options, stage});
console.log(`${result.reused ? '复用已校验产物' : '原始候选已保存,尚未生成最终部件'}:${result.out}`);
}
return result;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => { console.error(`模型阶段失败:${error.message}`); process.exitCode = 1; });
}