zhizhi/scripts/wake-persona.js

413 lines
15 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.

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