Files
AI_spine/lib/config.mjs
T
2026-09-17 17:44:24 +08:00

93 lines
5.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.
import {existsSync, readFileSync} from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
export function readEnvFile(file) {
if (!existsSync(file)) return {};
const values = {};
for (const line of readFileSync(file, 'utf8').replace(/^\uFEFF/u, '').split(/\r?\n/u)) {
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/u);
if (!match) continue;
let value = match[2];
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
} else value = value.replace(/\s+#.*$/u, '');
values[match[1]] = value;
}
return values;
}
export function loadConfig({configFile, envFile, env = process.env} = {}) {
const file = path.resolve(configFile || path.join(projectRoot, 'configs', 'astra-qwen.json'));
const config = JSON.parse(readFileSync(file, 'utf8').replace(/^\uFEFF/u, ''));
if (config.schema_version !== 1 || !config.providers || !config.stages) {
throw new Error('配置必须声明 schema_version: 1、providers 和 stages');
}
return {config, file, env: {...readEnvFile(envFile || path.join(projectRoot, '.env')), ...env}};
}
export function resolveStage(loaded, name, overrides = {}, {requireKey = true} = {}) {
const stage = loaded.config.stages[name];
if (!stage) throw new Error(`没有配置阶段:${name}`);
const provider = loaded.config.providers[stage.provider];
if (!provider) throw new Error(`没有配置提供方:${stage.provider}`);
const model = String(overrides.model || stage.model || loaded.env[stage.model_env] || stage.default_model || '').trim();
if (!model) throw new Error(`缺少 ${name} 的模型 ID`);
const baseUrl = String(loaded.env[provider.base_url_env] || '').trim().replace(/\/+$/u, '');
const apiKey = String(loaded.env[provider.api_key_env] || '').trim();
if (requireKey && (!baseUrl || !apiKey)) throw new Error(`需要配置 ${provider.base_url_env} 和 ${provider.api_key_env}`);
if (baseUrl) {
const url = new URL(baseUrl);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
throw new Error('提供方 base URL 必须是无用户名、密码、查询参数的 HTTP(S) 地址');
}
}
const protocol = overrides.protocol || provider.protocol;
const allowed = name === 'understanding' ? ['chat-completions', 'responses'] : ['images-edits'];
if (!allowed.includes(protocol)) throw new Error(`${name} 不支持协议:${protocol}`);
const params = {...stage.params, ...overrides.params};
// Semantic fields are owned by adapters, never overridden via opaque params.
for (const key of ['model', 'messages', 'input', 'instructions', 'prompt', 'image', 'image[]', 'mask', 'response_format', 'text', 'stream', 'Authorization', 'api_key', 'apiKey']) {
if (Object.hasOwn(params, key)) throw new Error(`params 不能覆盖保留字段:${key}`);
}
const timeoutMs = provider.timeout_ms ?? 180000;
const maxRetries = provider.max_retries ?? 2;
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 5) {
throw new Error('timeout_ms 必须为正数,max_retries 必须为 0~5 的整数');
}
const outputMode = overrides.outputMode || stage.output_mode || 'text-json';
if (!['text-json', 'json-object', 'json-schema'].includes(outputMode)) throw new Error(`未知 output_mode:${outputMode}`);
const stream = overrides.stream ?? stage.stream ?? false;
if (typeof stream !== 'boolean') throw new Error('stream 必须为 boolean');
const promptPath = path.resolve(path.dirname(loaded.file), overrides.prompt || stage.prompt);
const capabilities = {multiple_images: false, mask: false, ...provider.capabilities};
if (typeof capabilities.mask !== 'boolean' || typeof capabilities.multiple_images !== 'boolean') {
throw new Error('capabilities.mask 和 multiple_images 必须为 boolean');
}
const imageField = provider.image_field || 'image';
if (!['image', 'image[]'].includes(imageField)) throw new Error('image_field 必须是 image 或 image[]');
return {
name, provider: stage.provider, protocol, model, baseUrl, apiKey, timeoutMs, maxRetries,
params, outputMode, stream, promptPath, prompt: readFileSync(promptPath, 'utf8'), capabilities,
imageField, maskEditable: provider.mask_editable || 'transparent',
};
}
export function publicStage(stage) {
return {
provider: stage.provider, protocol: stage.protocol, model: stage.model,
params: stage.params, output_mode: stage.outputMode, stream: stage.stream, timeout_ms: stage.timeoutMs,
max_retries: stage.maxRetries, capabilities: stage.capabilities,
image_field: stage.imageField, mask_editable: stage.maskEditable,
};
}
export function samInfo(loaded) {
const stage = loaded.config.stages.segmentation || {};
const modelDir = loaded.env[stage.model_dir_env] || stage.default_model_dir || '';
const checkpoint = modelDir ? path.join(modelDir, 'sam3.1_multiplex.pt') : '';
return {model_dir: modelDir, checkpoint, checkpoint_exists: Boolean(checkpoint && existsSync(checkpoint)), runtime_status: 'not_integrated'};
}