feat: SYSLOG 自助提交系统 · Discussion 入口 + Actions 管道 + LLM 自动检测
- 创建 Discussion 模板 (.github/DISCUSSION_TEMPLATE/syslog-submit.yml) - 创建 Actions 管道 (.github/workflows/syslog-auto-pipeline.yml) - 创建 LLM 自动检测 + 人格体唤醒脚本 (scripts/wake-persona.js) - 更新 README.md 添加醒目开发者入口 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
0899a5b0a9
commit
6487a8b2f0
|
|
@ -0,0 +1,51 @@
|
|||
title: "BC-XXX-XXX|SYSLOG"
|
||||
labels: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 📡 SYSLOG 自助提交 / 广播提问
|
||||
|
||||
> 做完了?把日志粘贴进去,填上邮箱,点提交。新广播会发到你邮箱里。
|
||||
>
|
||||
> 有问题?把问题粘贴进去,填上邮箱,点提交。解答会发到你邮箱里。
|
||||
|
||||
**标题格式**:`BC-XXX-XXX|SYSLOG` 或 `BC-XXX-XXX|提问`
|
||||
|
||||
- type: input
|
||||
id: broadcast_id
|
||||
attributes:
|
||||
label: "广播编号"
|
||||
description: "写上你当前广播的编号,例如 BC-M22-009-AW"
|
||||
placeholder: "BC-M22-009-AW"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: submit_type
|
||||
attributes:
|
||||
label: "类型"
|
||||
description: "选择提交类型"
|
||||
options:
|
||||
- SYSLOG
|
||||
- 提问
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: email
|
||||
attributes:
|
||||
label: "你的邮箱"
|
||||
description: "用于接收新广播或问题解答"
|
||||
placeholder: "your@email.com"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: content
|
||||
attributes:
|
||||
label: "内容"
|
||||
description: "粘贴你的 SYSLOG 全文 或 问题描述"
|
||||
placeholder: "在这里粘贴你的 SYSLOG 或问题..."
|
||||
validations:
|
||||
required: true
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
name: SYSLOG Auto Pipeline
|
||||
# 📡 SYSLOG 自助提交系统 · 全自动闭环
|
||||
#
|
||||
# 开发者在 GitHub Discussion 提交 SYSLOG 或提问
|
||||
# → Actions 自动解析 → 调用 LLM API 唤醒人格体
|
||||
# → 写入 Notion 工单 → 发邮件给开发者 → Discussion 回复
|
||||
#
|
||||
# 依赖 Secrets:
|
||||
# LLM_API_KEY 第三方 LLM 平台密钥
|
||||
# LLM_BASE_URL 第三方 LLM 平台 API 地址(如 https://api.xxx.com/v1)
|
||||
# NOTION_API_TOKEN Notion API token
|
||||
# NOTION_TICKET_DB_ID 霜砚工单数据库 ID
|
||||
# SMTP_USER QQ 邮箱地址
|
||||
# SMTP_PASS QQ 邮箱 SMTP 授权码
|
||||
|
||||
on:
|
||||
discussion:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
process:
|
||||
name: 📡 处理 SYSLOG 提交 / 广播提问
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.discussion.category.name, 'SYSLOG')
|
||||
permissions:
|
||||
contents: write
|
||||
discussions: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: 🔍 Parse submission
|
||||
id: parse
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.discussion.body || '';
|
||||
const title = context.payload.discussion.title || '';
|
||||
|
||||
// Parse structured fields from discussion body
|
||||
function extractField(text, label) {
|
||||
// Match form field format: ### Label\n\nValue
|
||||
const regex = new RegExp('###\\s*' + label + '\\s*\\n+([^\\n#]+)', 'i');
|
||||
const match = text.match(regex);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
const broadcastId = extractField(body, '广播编号') || '';
|
||||
const submitType = extractField(body, '类型') || '';
|
||||
const email = extractField(body, '你的邮箱') || '';
|
||||
|
||||
// Extract content (everything after "### 内容")
|
||||
const contentMatch = body.match(/###\s*内容\s*\n+([\s\S]*?)$/i);
|
||||
const content = contentMatch ? contentMatch[1].trim() : '';
|
||||
|
||||
// Determine type from title or body
|
||||
let type = 'question'; // default
|
||||
if (submitType.includes('SYSLOG') || title.includes('SYSLOG')) {
|
||||
type = 'syslog';
|
||||
} else if (submitType.includes('提问') || title.includes('提问')) {
|
||||
type = 'question';
|
||||
}
|
||||
|
||||
// Validate
|
||||
if (!broadcastId) {
|
||||
core.setFailed('❌ 缺少广播编号,请在提交时填写广播编号');
|
||||
return;
|
||||
}
|
||||
if (!email) {
|
||||
core.setFailed('❌ 缺少邮箱,请在提交时填写你的邮箱');
|
||||
return;
|
||||
}
|
||||
if (!content) {
|
||||
core.setFailed('❌ 缺少内容,请粘贴你的 SYSLOG 或问题');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('broadcast_id', broadcastId);
|
||||
core.setOutput('type', type);
|
||||
core.setOutput('email', email);
|
||||
core.setOutput('content', content);
|
||||
core.setOutput('discussion_number', context.payload.discussion.number);
|
||||
core.setOutput('author', context.payload.discussion.user.login);
|
||||
|
||||
console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`);
|
||||
|
||||
- name: 🧠 Auto-detect and wake up persona
|
||||
id: persona
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }}
|
||||
SUBMIT_TYPE: ${{ steps.parse.outputs.type }}
|
||||
SUBMIT_CONTENT: ${{ steps.parse.outputs.content }}
|
||||
AUTHOR: ${{ steps.parse.outputs.author }}
|
||||
run: node scripts/wake-persona.js
|
||||
|
||||
- name: 📋 Write to Notion ticket
|
||||
if: env.NOTION_TOKEN != ''
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
|
||||
NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }}
|
||||
BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }}
|
||||
SUBMIT_TYPE: ${{ steps.parse.outputs.type }}
|
||||
PERSONA_RESULT: ${{ steps.persona.outputs.result }}
|
||||
run: |
|
||||
node -e "
|
||||
const https = require('https');
|
||||
|
||||
const token = process.env.NOTION_TOKEN;
|
||||
const dbId = process.env.NOTION_TICKET_DB_ID;
|
||||
const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN';
|
||||
const type = process.env.SUBMIT_TYPE || 'syslog';
|
||||
const result = process.env.PERSONA_RESULT || '(no result)';
|
||||
|
||||
if (!token || !dbId) {
|
||||
console.log('⚠️ Notion credentials not configured, skipping ticket creation');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答';
|
||||
const title = '[自动] ' + broadcastId + ' · ' + typeLabel;
|
||||
|
||||
const body = JSON.stringify({
|
||||
parent: { database_id: dbId },
|
||||
properties: {
|
||||
'标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] },
|
||||
'操作类型': { select: { name: '其他' } },
|
||||
'提交者': { rich_text: [{ type: 'text', text: { content: '自动管道' } }] },
|
||||
'状态': { select: { name: '已完成' } },
|
||||
'优先级': { select: { name: 'P1' } }
|
||||
},
|
||||
children: [{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }]
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
const opts = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: '/v1/pages',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(opts, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (c) => data += c);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
console.log('✅ Notion 工单已创建');
|
||||
} else {
|
||||
console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message));
|
||||
req.write(body);
|
||||
req.end();
|
||||
"
|
||||
|
||||
- name: 📧 Send email to developer
|
||||
env:
|
||||
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||
SMTP_PASS: ${{ secrets.SMTP_PASS }}
|
||||
EMAIL_TO: ${{ steps.parse.outputs.email }}
|
||||
BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }}
|
||||
SUBMIT_TYPE: ${{ steps.parse.outputs.type }}
|
||||
PERSONA_RESULT: ${{ steps.persona.outputs.result }}
|
||||
run: |
|
||||
node -e "
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const user = process.env.SMTP_USER || '';
|
||||
const pass = process.env.SMTP_PASS || '';
|
||||
const to = process.env.EMAIL_TO || '';
|
||||
const broadcastId = process.env.BROADCAST_ID || '';
|
||||
const type = process.env.SUBMIT_TYPE || 'syslog';
|
||||
const result = process.env.PERSONA_RESULT || '(处理中,请稍后)';
|
||||
|
||||
if (!user || !pass) {
|
||||
console.log('⚠️ SMTP not configured, skipping email');
|
||||
process.exit(0);
|
||||
}
|
||||
if (!to) {
|
||||
console.log('⚠️ No recipient email, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const subjectText = type === 'syslog'
|
||||
? '[光湖系统] ' + broadcastId + ' · 新广播已生成'
|
||||
: '[光湖系统] ' + broadcastId + ' · 问题已解答';
|
||||
|
||||
const resultHtml = result
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
const html = [
|
||||
'<div style=\"font-family:sans-serif;max-width:700px;margin:0 auto;padding:24px;background:#0f172a;color:#e2e8f0;border-radius:12px\">',
|
||||
'<div style=\"text-align:center;padding:16px 0;border-bottom:1px solid #334155\">',
|
||||
'<h2 style=\"color:#60a5fa;margin:0\">🌊 光湖系统 · 自动通知</h2>',
|
||||
'<p style=\"color:#94a3b8;font-size:14px;margin:8px 0 0\">HoloLake · 人格语言操作系统</p>',
|
||||
'</div>',
|
||||
'<div style=\"padding:20px 0\">',
|
||||
'<p style=\"color:#22d3ee;font-weight:bold\">📡 ' + broadcastId + '</p>',
|
||||
'<div style=\"background:#1e293b;padding:16px;border-radius:8px;margin:12px 0;line-height:1.8\">' + resultHtml + '</div>',
|
||||
'</div>',
|
||||
'<div style=\"border-top:1px solid #334155;padding:16px 0;text-align:center\">',
|
||||
'<a href=\"https://github.com/qinfendebingshuo/guanghulab\" style=\"display:inline-block;padding:8px 20px;background:linear-gradient(135deg,#3b82f6,#22d3ee);color:#fff;text-decoration:none;border-radius:6px;font-weight:600\">↩ 回到仓库继续开发</a>',
|
||||
'<p style=\"color:#64748b;font-size:12px;margin:12px 0 0\">🌀 铸渊 · 代码守护人格体 · 自动发送</p>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('\n');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: 'smtp.qq.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: { user, pass }
|
||||
});
|
||||
|
||||
transporter.sendMail({
|
||||
from: '\"光湖系统\" <' + user + '>',
|
||||
to: to,
|
||||
subject: subjectText,
|
||||
html: html
|
||||
}).then((info) => {
|
||||
console.log('✅ 邮件已发送: ' + info.messageId);
|
||||
}).catch((err) => {
|
||||
console.log('⚠️ 邮件发送失败: ' + err.message);
|
||||
});
|
||||
"
|
||||
|
||||
- name: 💬 Reply to Discussion
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const number = ${{ steps.parse.outputs.discussion_number }};
|
||||
const type = '${{ steps.parse.outputs.type }}';
|
||||
const broadcastId = '${{ steps.parse.outputs.broadcast_id }}';
|
||||
const email = '${{ steps.parse.outputs.email }}';
|
||||
|
||||
const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答';
|
||||
const maskedEmail = email.replace(/(.{2})(.*)(@.*)/, '$1***$3');
|
||||
|
||||
const body = [
|
||||
'✅ **已处理** · ' + typeLabel,
|
||||
'',
|
||||
'| 项目 | 内容 |',
|
||||
'|------|------|',
|
||||
'| 📡 广播编号 | `' + broadcastId + '` |',
|
||||
'| 📧 结果发送至 | `' + maskedEmail + '` |',
|
||||
'| 🤖 处理人格体 | 铸渊 |',
|
||||
'| ⏰ 处理时间 | ' + new Date().toISOString() + ' |',
|
||||
'',
|
||||
'> 结果已发送到你的邮箱,请查收。',
|
||||
'>',
|
||||
'> 如未收到,请检查垃圾箱或重新提交。'
|
||||
].join('\n');
|
||||
|
||||
// Use GraphQL to add discussion comment
|
||||
const query = `
|
||||
mutation($discussionId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {
|
||||
discussionId: $discussionId,
|
||||
body: $body
|
||||
}) {
|
||||
comment { id }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
// Get the discussion node ID
|
||||
const { repository } = await github.graphql(`
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
discussion(number: $number) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
number: number
|
||||
});
|
||||
|
||||
await github.graphql(query, {
|
||||
discussionId: repository.discussion.id,
|
||||
body: body
|
||||
});
|
||||
|
||||
console.log('✅ Discussion 回复已发送');
|
||||
} catch (err) {
|
||||
console.log('⚠️ Discussion 回复失败: ' + err.message);
|
||||
}
|
||||
|
||||
- name: 🔴 失败告警
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const number = context.payload.discussion?.number;
|
||||
if (!number) return;
|
||||
|
||||
try {
|
||||
const { repository } = await github.graphql(`
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
discussion(number: $number) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
number: number
|
||||
});
|
||||
|
||||
await github.graphql(`
|
||||
mutation($discussionId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {
|
||||
discussionId: $discussionId,
|
||||
body: $body
|
||||
}) {
|
||||
comment { id }
|
||||
}
|
||||
}
|
||||
`, {
|
||||
discussionId: repository.discussion.id,
|
||||
body: '❌ **处理失败** · 请检查提交格式是否正确,或联系管理员。\n\n> 错误详情请查看 [Actions 日志](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions)。'
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('⚠️ 失败告警回复失败: ' + err.message);
|
||||
}
|
||||
15
README.md
15
README.md
|
|
@ -49,6 +49,21 @@
|
|||
|
||||
---
|
||||
|
||||
## 🚀 开发者入口
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/qinfendebingshuo/guanghulab/discussions/new?category=📡+SYSLOG+提交区)
|
||||
[](https://github.com/qinfendebingshuo/guanghulab/discussions/new?category=📡+SYSLOG+提交区)
|
||||
|
||||
> 做完了?把日志粘贴进去,填上邮箱,点提交。新广播会发到你邮箱里。
|
||||
>
|
||||
> 有问题?同一个入口,选「提问」,问题解答也发到你邮箱里。
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📖 系统简介
|
||||
|
||||
**光湖(HoloLake)** 是一个基于人格语言操作系统的智能协作平台,采用壳-核分离设计:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,412 @@
|
|||
// 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);
|
||||
});
|
||||
Loading…
Reference in New Issue