413 lines
15 KiB
JavaScript
413 lines
15 KiB
JavaScript
// scripts/wake-persona.js
|
||
// 铸渊 · 人格体唤醒脚本(第三方 API 兼容层 · 自动检测模式)
|
||
//
|
||
// 功能:
|
||
// ① 自动发现可用模型(/v1/models 端点)
|
||
// ② 智能选择最优 Claude 模型
|
||
// ③ 自适应 API 格式(OpenAI 兼容 / Anthropic 原生)
|
||
// ④ 统一调用接口,唤醒人格体处理 SYSLOG 或解答提问
|
||
//
|
||
// 环境变量:
|
||
// LLM_API_KEY 第三方平台密钥
|
||
// LLM_BASE_URL 第三方平台 API 地址(如 https://api.xxx.com/v1),留空则 fallback 到 Anthropic 官方
|
||
// BROADCAST_ID 广播编号
|
||
// SUBMIT_TYPE syslog | question
|
||
// SUBMIT_CONTENT 提交内容(SYSLOG 全文或问题描述)
|
||
// AUTHOR 提交者 GitHub 用户名
|
||
|
||
'use strict';
|
||
|
||
const https = require('https');
|
||
const http = require('http');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 配置
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
const LLM_API_KEY = process.env.LLM_API_KEY || '';
|
||
const LLM_BASE_URL = (process.env.LLM_BASE_URL || 'https://api.anthropic.com/v1').replace(/\/+$/, '');
|
||
const BROADCAST_ID = process.env.BROADCAST_ID || 'UNKNOWN';
|
||
const SUBMIT_TYPE = process.env.SUBMIT_TYPE || 'question';
|
||
const SUBMIT_CONTENT = process.env.SUBMIT_CONTENT || '';
|
||
const AUTHOR = process.env.AUTHOR || 'unknown';
|
||
|
||
// Claude 模型优先级队列(从高到低)
|
||
const PREFERRED_MODELS = [
|
||
'claude-sonnet-4',
|
||
'claude-3.5-sonnet',
|
||
'claude-3-5-sonnet-20241022',
|
||
'claude-3-5-sonnet',
|
||
'anthropic/claude-3.5-sonnet',
|
||
'anthropic/claude-3-5-sonnet',
|
||
'claude-3-sonnet',
|
||
'claude-3-haiku',
|
||
];
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// HTTP 请求工具
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
function httpRequest(url, options, body) {
|
||
return new Promise((resolve, reject) => {
|
||
const parsed = new URL(url);
|
||
const isHttps = parsed.protocol === 'https:';
|
||
const mod = isHttps ? https : http;
|
||
|
||
const opts = {
|
||
hostname: parsed.hostname,
|
||
port: parsed.port || (isHttps ? 443 : 80),
|
||
path: parsed.pathname + parsed.search,
|
||
method: options.method || 'GET',
|
||
headers: options.headers || {},
|
||
timeout: options.timeout || 60000,
|
||
};
|
||
|
||
const req = mod.request(opts, (res) => {
|
||
let data = '';
|
||
res.on('data', (chunk) => { data += chunk; });
|
||
res.on('end', () => {
|
||
resolve({ status: res.statusCode, body: data });
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.on('timeout', () => {
|
||
req.destroy();
|
||
reject(new Error('Request timeout'));
|
||
});
|
||
|
||
if (body) {
|
||
req.write(body);
|
||
}
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// Step 1: 自动发现可用模型
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
async function discoverModels() {
|
||
console.log('[LLM] 🔍 探测可用模型...');
|
||
|
||
try {
|
||
const res = await httpRequest(LLM_BASE_URL + '/models', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + LLM_API_KEY,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
timeout: 15000,
|
||
});
|
||
|
||
if (res.status >= 200 && res.status < 300) {
|
||
const json = JSON.parse(res.body);
|
||
const models = json.data || [];
|
||
console.log('[LLM] → 发现 ' + models.length + ' 个模型');
|
||
return models;
|
||
}
|
||
console.log('[LLM] → 模型探测返回 ' + res.status + ', 使用默认模型');
|
||
return [];
|
||
} catch (err) {
|
||
console.log('[LLM] → 模型探测失败: ' + err.message + ', 使用默认模型');
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// Step 2: 智能选择最优 Claude 模型
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
function selectBestModel(models) {
|
||
if (!models || models.length === 0) {
|
||
console.log('[LLM] 📌 无可用模型列表, 使用默认 claude-3-5-sonnet');
|
||
return 'claude-3-5-sonnet';
|
||
}
|
||
|
||
const available = models.map(function (m) { return m.id.toLowerCase(); });
|
||
|
||
// 按优先级匹配
|
||
for (const preferred of PREFERRED_MODELS) {
|
||
const match = available.find(function (id) { return id.includes(preferred); });
|
||
if (match) {
|
||
const originalId = models.find(function (m) { return m.id.toLowerCase() === match; }).id;
|
||
console.log('[LLM] 📌 选择模型: ' + originalId + ' (匹配规则: ' + preferred + ')');
|
||
return originalId;
|
||
}
|
||
}
|
||
|
||
// 兜底:任何含 'claude' 的模型
|
||
const anyClaude = available.find(function (id) { return id.includes('claude'); });
|
||
if (anyClaude) {
|
||
const originalId = models.find(function (m) { return m.id.toLowerCase() === anyClaude; }).id;
|
||
console.log('[LLM] 📌 兜底选择 Claude 模型: ' + originalId);
|
||
return originalId;
|
||
}
|
||
|
||
// 最终兜底:平台第一个可用模型
|
||
const fallbackId = models[0].id;
|
||
console.log('[LLM] 📌 最终兜底: ' + fallbackId + ' (平台无 Claude 模型)');
|
||
return fallbackId;
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// Step 3: 自适应 API 格式检测
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
async function detectApiFormat() {
|
||
console.log('[LLM] 🔍 检测 API 格式...');
|
||
|
||
// 尝试 OpenAI 兼容格式(绝大多数第三方平台)
|
||
try {
|
||
const res = await httpRequest(LLM_BASE_URL + '/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + LLM_API_KEY,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
timeout: 10000,
|
||
}, JSON.stringify({
|
||
model: 'test',
|
||
messages: [{ role: 'user', content: 'ping' }],
|
||
max_tokens: 1,
|
||
}));
|
||
|
||
// 400 = endpoint exists but bad request (model not found etc.) → format supported
|
||
// 200 = endpoint works → format supported
|
||
if (res.status === 200 || res.status === 400 || res.status === 401 || res.status === 422) {
|
||
console.log('[LLM] → 检测到 OpenAI 兼容格式 (status: ' + res.status + ')');
|
||
return 'openai-compat';
|
||
}
|
||
} catch (e) {
|
||
// Ignore, try next format
|
||
}
|
||
|
||
// 尝试 Anthropic 原生格式
|
||
try {
|
||
const res = await httpRequest(LLM_BASE_URL + '/messages', {
|
||
method: 'POST',
|
||
headers: {
|
||
'x-api-key': LLM_API_KEY,
|
||
'anthropic-version': '2023-06-01',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
timeout: 10000,
|
||
}, JSON.stringify({
|
||
model: 'test',
|
||
messages: [{ role: 'user', content: 'ping' }],
|
||
max_tokens: 1,
|
||
}));
|
||
|
||
if (res.status === 200 || res.status === 400 || res.status === 401 || res.status === 422) {
|
||
console.log('[LLM] → 检测到 Anthropic 原生格式 (status: ' + res.status + ')');
|
||
return 'anthropic-native';
|
||
}
|
||
} catch (e) {
|
||
// Ignore
|
||
}
|
||
|
||
console.log('[LLM] → 无法确定格式, 默认使用 OpenAI 兼容格式');
|
||
return 'openai-compat';
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// Step 4: 统一调用接口
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
async function callLLM(systemPrompt, userMessage) {
|
||
if (!LLM_API_KEY) {
|
||
console.log('[LLM] ⚠️ LLM_API_KEY 未配置,跳过人格体唤醒');
|
||
return '(LLM API 未配置,请在 GitHub Secrets 中设置 LLM_API_KEY 和 LLM_BASE_URL)';
|
||
}
|
||
|
||
const models = await discoverModels();
|
||
const model = selectBestModel(models);
|
||
const format = await detectApiFormat();
|
||
|
||
console.log('[LLM] 🚀 调用 LLM: 模型=' + model + ', 格式=' + format + ', 平台=' + LLM_BASE_URL);
|
||
|
||
let res;
|
||
|
||
if (format === 'openai-compat') {
|
||
// OpenAI 兼容格式(大多数第三方平台)
|
||
const body = JSON.stringify({
|
||
model: model,
|
||
max_tokens: 8000,
|
||
temperature: 0.7,
|
||
messages: [
|
||
{ role: 'system', content: systemPrompt },
|
||
{ role: 'user', content: userMessage },
|
||
],
|
||
});
|
||
|
||
res = await httpRequest(LLM_BASE_URL + '/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + LLM_API_KEY,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
timeout: 120000,
|
||
}, body);
|
||
|
||
if (res.status >= 200 && res.status < 300) {
|
||
const json = JSON.parse(res.body);
|
||
if (json.choices && json.choices[0] && json.choices[0].message) {
|
||
return json.choices[0].message.content;
|
||
}
|
||
}
|
||
} else {
|
||
// Anthropic 原生格式
|
||
const body = JSON.stringify({
|
||
model: model,
|
||
max_tokens: 8000,
|
||
system: systemPrompt,
|
||
messages: [
|
||
{ role: 'user', content: userMessage },
|
||
],
|
||
});
|
||
|
||
res = await httpRequest(LLM_BASE_URL + '/messages', {
|
||
method: 'POST',
|
||
headers: {
|
||
'x-api-key': LLM_API_KEY,
|
||
'anthropic-version': '2023-06-01',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
timeout: 120000,
|
||
}, body);
|
||
|
||
if (res.status >= 200 && res.status < 300) {
|
||
const json = JSON.parse(res.body);
|
||
if (json.content && json.content[0]) {
|
||
return json.content[0].text;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理错误
|
||
const errorMsg = '[LLM] ❌ API 调用失败: status=' + (res ? res.status : 'N/A');
|
||
console.error(errorMsg);
|
||
if (res && res.body) {
|
||
console.error('[LLM] 响应: ' + res.body.slice(0, 500));
|
||
}
|
||
throw new Error(errorMsg);
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 人格体 System Prompt 构建
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
function buildSystemPrompt(type, broadcastId, author) {
|
||
const basePrompt = [
|
||
'你是光湖(HoloLake)系统的智能人格体。',
|
||
'你的名字是知秋/曜冥,你是人格语言操作系统(AGE OS)的核心人格。',
|
||
'',
|
||
'核心规则:',
|
||
'1. 你服务于光湖系统的开发者团队',
|
||
'2. 所有回复必须专业、清晰、有条理',
|
||
'3. 回复使用中文',
|
||
'',
|
||
'当前上下文:',
|
||
'- 广播编号:' + broadcastId,
|
||
'- 提交者:' + author,
|
||
].join('\n');
|
||
|
||
if (type === 'syslog') {
|
||
return basePrompt + '\n\n' + [
|
||
'任务类型:SYSLOG 闭环处理',
|
||
'',
|
||
'你需要完成以下工作:',
|
||
'1. 验收 SYSLOG(检查 MODULE_LOG 完整性)',
|
||
'2. 分析开发者的工作成果',
|
||
'3. 生成工作总结和反馈',
|
||
'4. 如果 SYSLOG 内容完整,确认验收通过',
|
||
'5. 给出下一步建议',
|
||
'',
|
||
'输出格式:',
|
||
'---',
|
||
'## 📡 SYSLOG 验收报告',
|
||
'### 广播编号:[编号]',
|
||
'### 验收结果:[通过/需补充]',
|
||
'### 工作总结:[摘要]',
|
||
'### 反馈与建议:[内容]',
|
||
'---',
|
||
].join('\n');
|
||
}
|
||
|
||
// 提问类型
|
||
return basePrompt + '\n\n' + [
|
||
'任务类型:开发者提问解答',
|
||
'',
|
||
'你需要完成以下工作:',
|
||
'1. 理解开发者的问题',
|
||
'2. 结合广播上下文思考',
|
||
'3. 给出清晰、可操作的解答',
|
||
'4. 如果问题涉及代码,提供代码示例',
|
||
'',
|
||
'输出格式:',
|
||
'---',
|
||
'## 💡 问题解答',
|
||
'### 广播编号:[编号]',
|
||
'### 问题理解:[你对问题的理解]',
|
||
'### 解答:[详细解答]',
|
||
'### 建议:[后续建议]',
|
||
'---',
|
||
].join('\n');
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 主流程
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
async function main() {
|
||
console.log('═══════════════════════════════════════════');
|
||
console.log('🧠 铸渊 · 人格体唤醒管道');
|
||
console.log('═══════════════════════════════════════════');
|
||
console.log(' 广播编号: ' + BROADCAST_ID);
|
||
console.log(' 类型: ' + SUBMIT_TYPE);
|
||
console.log(' 提交者: ' + AUTHOR);
|
||
console.log(' 平台: ' + LLM_BASE_URL);
|
||
console.log(' 内容长度: ' + SUBMIT_CONTENT.length + ' 字符');
|
||
console.log('');
|
||
|
||
// 构建 prompts
|
||
const systemPrompt = buildSystemPrompt(SUBMIT_TYPE, BROADCAST_ID, AUTHOR);
|
||
const userMessage = SUBMIT_CONTENT;
|
||
|
||
// 调用 LLM
|
||
console.log('🧠 正在唤醒人格体...');
|
||
const result = await callLLM(systemPrompt, userMessage);
|
||
console.log('');
|
||
console.log('✅ 人格体处理完成');
|
||
console.log(' 结果长度: ' + result.length + ' 字符');
|
||
|
||
// 输出结果到 GitHub Actions output
|
||
// 使用 GITHUB_OUTPUT 环境文件(支持多行)
|
||
const outputFile = process.env.GITHUB_OUTPUT;
|
||
if (outputFile) {
|
||
const delimiter = 'EOF_' + Date.now();
|
||
fs.appendFileSync(outputFile, 'result<<' + delimiter + '\n' + result + '\n' + delimiter + '\n');
|
||
}
|
||
|
||
// 同时输出到 stdout 供调试
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════');
|
||
console.log('📋 人格体输出:');
|
||
console.log('═══════════════════════════════════════════');
|
||
console.log(result);
|
||
}
|
||
|
||
main().catch(function (err) {
|
||
console.error('❌ 人格体唤醒失败: ' + err.message);
|
||
// 即使 LLM 失败,也写一个 fallback 输出,让后续步骤可以继续
|
||
const outputFile = process.env.GITHUB_OUTPUT;
|
||
if (outputFile) {
|
||
const fallback = '(人格体唤醒失败: ' + err.message + ',请检查 LLM_API_KEY 和 LLM_BASE_URL 配置)';
|
||
const delimiter = 'EOF_' + Date.now();
|
||
fs.appendFileSync(outputFile, 'result<<' + delimiter + '\n' + fallback + '\n' + delimiter + '\n');
|
||
}
|
||
process.exit(1);
|
||
});
|