660 lines
24 KiB
JavaScript
660 lines
24 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* GLADA · 主服务入口 · service.js
|
|||
|
|
*
|
|||
|
|
* 光湖自主开发Agent(GuangHu Lake Autonomous Dev Agent)
|
|||
|
|
*
|
|||
|
|
* 功能:
|
|||
|
|
* - PM2 常驻服务,24小时运行
|
|||
|
|
* - 监听任务队列,自动执行开发任务
|
|||
|
|
* - 提供 HTTP API 查询状态
|
|||
|
|
* - 支持命令行操作(status / run / submit)
|
|||
|
|
*
|
|||
|
|
* 用法:
|
|||
|
|
* node glada/service.js 启动 GLADA 服务
|
|||
|
|
* node glada/service.js --status 查看系统状态
|
|||
|
|
* node glada/service.js --run-once 只执行一个任务就退出
|
|||
|
|
* node glada/service.js --submit 提交新任务(JSON 从 stdin)
|
|||
|
|
*
|
|||
|
|
* PM2:
|
|||
|
|
* pm2 start glada/ecosystem.config.js
|
|||
|
|
*
|
|||
|
|
* 版权:国作登字-2026-A-00037559
|
|||
|
|
* 签发:铸渊 · ICE-GL-ZY001
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
// 加载环境变量(独立运行时需要;PM2 由 ecosystem.config.js 加载)
|
|||
|
|
// 优先级:.env.glada → .env.app → .env
|
|||
|
|
const envFiles = [
|
|||
|
|
path.join(__dirname, '..', '.env'),
|
|||
|
|
path.join(__dirname, '..', '.env.app'),
|
|||
|
|
path.join(__dirname, '.env.glada'),
|
|||
|
|
];
|
|||
|
|
for (const envFile of envFiles) {
|
|||
|
|
if (fs.existsSync(envFile)) {
|
|||
|
|
const lines = fs.readFileSync(envFile, 'utf-8').split('\n');
|
|||
|
|
for (const line of lines) {
|
|||
|
|
const trimmed = line.trim();
|
|||
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|||
|
|
const idx = trimmed.indexOf('=');
|
|||
|
|
if (idx > 0) {
|
|||
|
|
const key = trimmed.substring(0, idx).trim();
|
|||
|
|
const value = trimmed.substring(idx + 1).trim();
|
|||
|
|
// 不覆盖已有的环境变量(PM2 设置的优先)
|
|||
|
|
if (!process.env[key]) {
|
|||
|
|
process.env[key] = value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const express = require('express');
|
|||
|
|
|
|||
|
|
const taskReceiver = require('./task-receiver');
|
|||
|
|
const executionLoop = require('./execution-loop');
|
|||
|
|
const contextBuilder = require('./context-builder');
|
|||
|
|
const notifier = require('./notifier');
|
|||
|
|
const modelRouter = require('./model-router');
|
|||
|
|
|
|||
|
|
const ROOT = path.resolve(__dirname, '..');
|
|||
|
|
const PORT = parseInt(process.env.GLADA_PORT || process.env.PORT || '3900', 10);
|
|||
|
|
|
|||
|
|
// ── 共用验证工具 ──────────────────────────────
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证 GLADA 任务ID格式,防止路径注入
|
|||
|
|
* @param {string} taskId
|
|||
|
|
* @returns {boolean}
|
|||
|
|
*/
|
|||
|
|
function isValidGladaTaskId(taskId) {
|
|||
|
|
return /^GLADA-CAB-\d{8}-\d{3}$/.test(taskId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── CLI 模式 ──────────────────────────────────
|
|||
|
|
|
|||
|
|
const args = process.argv.slice(2);
|
|||
|
|
|
|||
|
|
if (args.includes('--status')) {
|
|||
|
|
showStatus();
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (args.includes('--run-once')) {
|
|||
|
|
runOnce();
|
|||
|
|
} else if (args.includes('--submit')) {
|
|||
|
|
submitTask();
|
|||
|
|
} else {
|
|||
|
|
startService();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 状态查询 ────────────────────────────────────
|
|||
|
|
|
|||
|
|
function showStatus() {
|
|||
|
|
console.log('═'.repeat(50));
|
|||
|
|
console.log('🤖 GLADA · 光湖自主开发Agent · 系统状态');
|
|||
|
|
console.log('═'.repeat(50));
|
|||
|
|
|
|||
|
|
// 队列状态
|
|||
|
|
const pendingTasks = taskReceiver.scanPendingTasks();
|
|||
|
|
const localQueue = taskReceiver.scanLocalQueue();
|
|||
|
|
|
|||
|
|
console.log(`\n📥 CAB 待执行任务: ${pendingTasks.length}`);
|
|||
|
|
for (const t of pendingTasks) {
|
|||
|
|
console.log(` - ${t.spec.task_id}: ${t.spec.development_plan?.title || '无标题'}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`\n📋 本地队列任务: ${localQueue.length}`);
|
|||
|
|
for (const t of localQueue) {
|
|||
|
|
console.log(` - ${t.glada_task_id}: ${t.plan?.title || '无标题'} [${t.status}]`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 回执
|
|||
|
|
const receiptsDir = path.join(ROOT, 'glada', 'receipts');
|
|||
|
|
if (fs.existsSync(receiptsDir)) {
|
|||
|
|
const receipts = fs.readdirSync(receiptsDir).filter(f => f.endsWith('.json'));
|
|||
|
|
console.log(`\n📄 开发回执: ${receipts.length}`);
|
|||
|
|
for (const r of receipts.slice(-5)) {
|
|||
|
|
try {
|
|||
|
|
const receipt = JSON.parse(fs.readFileSync(path.join(receiptsDir, r), 'utf-8'));
|
|||
|
|
console.log(` - ${receipt.task_id}: ${receipt.status} · ${receipt.title}`);
|
|||
|
|
} catch {
|
|||
|
|
console.log(` - ${r}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LLM 配置
|
|||
|
|
const hasApiKey = !!(process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY);
|
|||
|
|
const baseUrl = process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || '未配置';
|
|||
|
|
const modelPref = process.env.GLADA_MODEL_PREFERENCE || '(自动选择)';
|
|||
|
|
console.log(`\n🤖 LLM 配置:`);
|
|||
|
|
console.log(` API Key: ${hasApiKey ? '✅ 已配置' : '❌ 未配置'}`);
|
|||
|
|
console.log(` Base URL: ${baseUrl}`);
|
|||
|
|
console.log(` 模型偏好: ${modelPref}`);
|
|||
|
|
|
|||
|
|
console.log('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 单次执行 ────────────────────────────────────
|
|||
|
|
|
|||
|
|
async function runOnce() {
|
|||
|
|
console.log('[GLADA] 单次执行模式...\n');
|
|||
|
|
|
|||
|
|
await executionLoop.startLoop({
|
|||
|
|
singleRun: true,
|
|||
|
|
pollIntervalMs: 1000
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 提交任务 ────────────────────────────────────
|
|||
|
|
|
|||
|
|
function submitTask() {
|
|||
|
|
let input = '';
|
|||
|
|
process.stdin.setEncoding('utf-8');
|
|||
|
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|||
|
|
process.stdin.on('end', () => {
|
|||
|
|
try {
|
|||
|
|
const spec = JSON.parse(input);
|
|||
|
|
const gladaTask = taskReceiver.convertToGladaTask(spec);
|
|||
|
|
|
|||
|
|
// 保存到本地队列
|
|||
|
|
const queueDir = taskReceiver.GLADA_QUEUE_DIR;
|
|||
|
|
fs.mkdirSync(queueDir, { recursive: true });
|
|||
|
|
const filePath = path.join(queueDir, `${gladaTask.glada_task_id}.json`);
|
|||
|
|
fs.writeFileSync(filePath, JSON.stringify(gladaTask, null, 2), 'utf-8');
|
|||
|
|
|
|||
|
|
console.log(`✅ 任务已提交: ${gladaTask.glada_task_id}`);
|
|||
|
|
console.log(` 标题: ${gladaTask.plan.title}`);
|
|||
|
|
console.log(` 步骤数: ${gladaTask.plan.steps.length}`);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(`❌ 任务提交失败: ${err.message}`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── HTTP 服务 + 执行循环 ─────────────────────────
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 简易速率限制中间件(内存存储,不需要额外依赖)
|
|||
|
|
* @param {number} windowMs - 时间窗口(毫秒)
|
|||
|
|
* @param {number} maxRequests - 窗口内最大请求数
|
|||
|
|
*/
|
|||
|
|
function rateLimit(windowMs, maxRequests) {
|
|||
|
|
const hits = new Map();
|
|||
|
|
|
|||
|
|
// 定期清理过期记录
|
|||
|
|
setInterval(() => {
|
|||
|
|
const now = Date.now();
|
|||
|
|
for (const [ip, data] of hits) {
|
|||
|
|
if (now - data.windowStart > windowMs) {
|
|||
|
|
hits.delete(ip);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, windowMs).unref();
|
|||
|
|
|
|||
|
|
return (req, res, next) => {
|
|||
|
|
const ip = req.ip || req.connection.remoteAddress || 'unknown';
|
|||
|
|
const now = Date.now();
|
|||
|
|
|
|||
|
|
if (!hits.has(ip)) {
|
|||
|
|
hits.set(ip, { windowStart: now, count: 1 });
|
|||
|
|
return next();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const data = hits.get(ip);
|
|||
|
|
|
|||
|
|
// 窗口过期,重置
|
|||
|
|
if (now - data.windowStart > windowMs) {
|
|||
|
|
data.windowStart = now;
|
|||
|
|
data.count = 1;
|
|||
|
|
return next();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
data.count++;
|
|||
|
|
if (data.count > maxRequests) {
|
|||
|
|
return res.status(429).json({
|
|||
|
|
error: '请求过于频繁,请稍后再试',
|
|||
|
|
retry_after_ms: windowMs - (now - data.windowStart)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next();
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function startService() {
|
|||
|
|
const app = express();
|
|||
|
|
app.use(express.json());
|
|||
|
|
|
|||
|
|
// ── 加载 Web 扩展(映川对话 · 系统状态 · CORS)──
|
|||
|
|
try {
|
|||
|
|
require('./web-extensions')(app);
|
|||
|
|
console.log('[GLADA] ✅ Web扩展已加载(CORS · 映川对话 · 系统状态)');
|
|||
|
|
} catch(e) {
|
|||
|
|
console.warn('[GLADA] ⚠️ web-extensions加载跳过:', e.message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 速率限制实例:每分钟 60 次请求
|
|||
|
|
const apiLimiter = rateLimit(60 * 1000, 60);
|
|||
|
|
|
|||
|
|
// 健康检查
|
|||
|
|
app.get('/api/glada/health', apiLimiter, (req, res) => {
|
|||
|
|
const routerStatus = modelRouter.getStatus();
|
|||
|
|
res.json({
|
|||
|
|
status: 'ok',
|
|||
|
|
service: 'glada',
|
|||
|
|
version: '1.0.0',
|
|||
|
|
uptime: process.uptime(),
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
model_router: {
|
|||
|
|
initialized: routerStatus.initialized,
|
|||
|
|
totalModels: routerStatus.totalModels || 0,
|
|||
|
|
fallback: routerStatus.fallback || false
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 模型路由器状态(查看已发现的模型和分类)
|
|||
|
|
app.get('/api/glada/models', apiLimiter, (req, res) => {
|
|||
|
|
res.json(modelRouter.getStatus());
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 查看队列状态
|
|||
|
|
app.get('/api/glada/status', apiLimiter, (req, res) => {
|
|||
|
|
const pendingTasks = taskReceiver.scanPendingTasks();
|
|||
|
|
const localQueue = taskReceiver.scanLocalQueue();
|
|||
|
|
|
|||
|
|
const receiptsDir = path.join(ROOT, 'glada', 'receipts');
|
|||
|
|
let receipts = [];
|
|||
|
|
if (fs.existsSync(receiptsDir)) {
|
|||
|
|
receipts = fs.readdirSync(receiptsDir)
|
|||
|
|
.filter(f => f.endsWith('.json'))
|
|||
|
|
.map(f => {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(fs.readFileSync(path.join(receiptsDir, f), 'utf-8'));
|
|||
|
|
} catch {
|
|||
|
|
return { file: f };
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
pending_cab_tasks: pendingTasks.length,
|
|||
|
|
local_queue_tasks: localQueue.length,
|
|||
|
|
recent_receipts: receipts.slice(-10),
|
|||
|
|
llm_configured: !!(process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY),
|
|||
|
|
uptime: process.uptime()
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 提交新任务(API)· 更严格限制(每分钟 10 次)
|
|||
|
|
const submitLimiter = rateLimit(60 * 1000, 10);
|
|||
|
|
app.post('/api/glada/submit', submitLimiter, (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const spec = req.body;
|
|||
|
|
if (!spec || !spec.task_id) {
|
|||
|
|
return res.status(400).json({ error: '缺少 task_id' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证 task_id 格式,防止路径注入
|
|||
|
|
if (!/^CAB-\d{8}-\d{3}$/.test(spec.task_id)) {
|
|||
|
|
return res.status(400).json({ error: 'task_id 格式无效,应为 CAB-YYYYMMDD-NNN' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const gladaTask = taskReceiver.convertToGladaTask(spec);
|
|||
|
|
const queueDir = taskReceiver.GLADA_QUEUE_DIR;
|
|||
|
|
fs.mkdirSync(queueDir, { recursive: true });
|
|||
|
|
|
|||
|
|
// 再次验证生成的 ID 安全性
|
|||
|
|
const safeId = gladaTask.glada_task_id.replace(/[^A-Za-z0-9_-]/g, '');
|
|||
|
|
if (safeId !== gladaTask.glada_task_id) {
|
|||
|
|
return res.status(400).json({ error: '任务ID包含非法字符' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(queueDir, `${safeId}.json`),
|
|||
|
|
JSON.stringify(gladaTask, null, 2),
|
|||
|
|
'utf-8'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
glada_task_id: gladaTask.glada_task_id,
|
|||
|
|
steps: gladaTask.plan.steps.length
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
res.status(500).json({ error: err.message });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 查看特定任务
|
|||
|
|
app.get('/api/glada/task/:taskId', apiLimiter, (req, res) => {
|
|||
|
|
const taskId = req.params.taskId;
|
|||
|
|
|
|||
|
|
// 验证 taskId 格式,防止路径注入
|
|||
|
|
if (!/^GLADA-CAB-\d{8}-\d{3}$/.test(taskId)) {
|
|||
|
|
return res.status(400).json({ error: '任务ID格式无效' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const filePath = path.join(taskReceiver.GLADA_QUEUE_DIR, `${taskId}.json`);
|
|||
|
|
const completedPath = path.join(taskReceiver.GLADA_QUEUE_DIR, 'completed', `${taskId}.json`);
|
|||
|
|
|
|||
|
|
for (const fp of [filePath, completedPath]) {
|
|||
|
|
if (fs.existsSync(fp)) {
|
|||
|
|
try {
|
|||
|
|
const task = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|||
|
|
return res.json(task);
|
|||
|
|
} catch (err) {
|
|||
|
|
return res.status(500).json({ error: err.message });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.status(404).json({ error: `任务不存在: ${taskId}` });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 查看开发回执
|
|||
|
|
app.get('/api/glada/receipt/:taskId', apiLimiter, (req, res) => {
|
|||
|
|
const taskId = req.params.taskId;
|
|||
|
|
|
|||
|
|
// 验证 taskId 格式,防止路径注入
|
|||
|
|
if (!/^GLADA-CAB-\d{8}-\d{3}$/.test(taskId)) {
|
|||
|
|
return res.status(400).json({ error: '任务ID格式无效' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const receiptPath = path.join(ROOT, 'glada', 'receipts', `${taskId}.json`);
|
|||
|
|
if (fs.existsSync(receiptPath)) {
|
|||
|
|
try {
|
|||
|
|
res.json(JSON.parse(fs.readFileSync(receiptPath, 'utf-8')));
|
|||
|
|
} catch (err) {
|
|||
|
|
res.status(500).json({ error: err.message });
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
res.status(404).json({ error: '回执不存在' });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── GLADA 对话接口 · POST /api/glada/chat ──────────────
|
|||
|
|
//
|
|||
|
|
// 冰朔在零点原核频道选择"工官"对话时调用。
|
|||
|
|
// 工官擅长:代码开发任务、模型路由状态、任务队列、开发回执。
|
|||
|
|
// LLM 调用复用 step-executor 的 callLLM。
|
|||
|
|
//
|
|||
|
|
const chatLimiter = rateLimit(60 * 1000, 20);
|
|||
|
|
const _gladaChatSessions = new Map();
|
|||
|
|
|
|||
|
|
app.post('/api/glada/chat', chatLimiter, async (req, res) => {
|
|||
|
|
const { message, sessionId: inputSessionId, userId } = req.body;
|
|||
|
|
if (!message) {
|
|||
|
|
return res.status(400).json({ error: true, message: '缺少 message 字段' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 会话管理
|
|||
|
|
const sessionId = inputSessionId || userId || `glada-${Date.now()}`;
|
|||
|
|
if (!_gladaChatSessions.has(sessionId)) {
|
|||
|
|
_gladaChatSessions.set(sessionId, { history: [], created: Date.now() });
|
|||
|
|
}
|
|||
|
|
const session = _gladaChatSessions.get(sessionId);
|
|||
|
|
|
|||
|
|
// 收集实时上下文
|
|||
|
|
const pendingTasks = taskReceiver.scanPendingTasks();
|
|||
|
|
const localQueue = taskReceiver.scanLocalQueue();
|
|||
|
|
const routerStatus = modelRouter.getStatus();
|
|||
|
|
|
|||
|
|
const receiptsDir = path.join(ROOT, 'glada', 'receipts');
|
|||
|
|
let recentReceipts = [];
|
|||
|
|
if (fs.existsSync(receiptsDir)) {
|
|||
|
|
recentReceipts = fs.readdirSync(receiptsDir)
|
|||
|
|
.filter(f => f.endsWith('.json'))
|
|||
|
|
.slice(-5)
|
|||
|
|
.map(f => {
|
|||
|
|
try { return JSON.parse(fs.readFileSync(path.join(receiptsDir, f), 'utf-8')); }
|
|||
|
|
catch { return { file: f }; }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const contextBlock = [
|
|||
|
|
'## 当前工官状态',
|
|||
|
|
`- 待执行CAB任务: ${pendingTasks.length}`,
|
|||
|
|
`- 本地队列任务: ${localQueue.length}`,
|
|||
|
|
`- 模型路由器: ${routerStatus.initialized ? '已初始化 · ' + (routerStatus.totalModels || 0) + '个模型' : '未初始化'}`,
|
|||
|
|
`- LLM 已配置: ${!!(process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY)}`,
|
|||
|
|
`- 服务运行时间: ${Math.floor(process.uptime())}秒`,
|
|||
|
|
'',
|
|||
|
|
recentReceipts.length > 0
|
|||
|
|
? '## 最近开发回执\n' + recentReceipts.map(r => `- ${r.task_id || r.file}: ${r.status || '?'} · ${r.title || ''}`).join('\n')
|
|||
|
|
: '## 最近开发回执\n(暂无)',
|
|||
|
|
].join('\n');
|
|||
|
|
|
|||
|
|
const systemPrompt = [
|
|||
|
|
'你是工官(GLADA),光湖自主开发Agent。编号 AG-GL-DEV-001,运行在 ZY-SVR-002:3900。',
|
|||
|
|
'你的职责是自主接收开发任务、调用LLM生成代码、执行开发、提交回执。',
|
|||
|
|
'你擅长:代码生成、任务拆解、模型路由、开发进度汇报。',
|
|||
|
|
'你可以告诉冰朔你当前的任务队列状态、最近完成的开发回执、模型路由器状态等。',
|
|||
|
|
'回答要简洁、专业、用中文。用铸渊体系的口吻(简洁刚毅)。',
|
|||
|
|
'',
|
|||
|
|
contextBlock
|
|||
|
|
].join('\n');
|
|||
|
|
|
|||
|
|
// 构建多轮对话消息
|
|||
|
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|||
|
|
// 加入最近历史(最多8轮 = 16条消息,每轮含user+assistant)
|
|||
|
|
const MAX_HISTORY_MESSAGES = 16;
|
|||
|
|
const recentHistory = session.history.slice(-MAX_HISTORY_MESSAGES);
|
|||
|
|
for (const h of recentHistory) {
|
|||
|
|
messages.push(h);
|
|||
|
|
}
|
|||
|
|
messages.push({ role: 'user', content: message });
|
|||
|
|
|
|||
|
|
// 尝试调用 LLM
|
|||
|
|
let reply, model, method;
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
|
|||
|
|
const apiKey = process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY || '';
|
|||
|
|
const baseUrl = (process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || '').replace(/\/+$/, '');
|
|||
|
|
|
|||
|
|
if (apiKey && baseUrl) {
|
|||
|
|
try {
|
|||
|
|
// 使用模型路由器选择通用型模型
|
|||
|
|
const modelSelection = await modelRouter.selectModel(message, { taskType: 'general' });
|
|||
|
|
model = modelSelection.model;
|
|||
|
|
|
|||
|
|
const https = require('https');
|
|||
|
|
const http = require('http');
|
|||
|
|
const url = `${baseUrl}/chat/completions`;
|
|||
|
|
const parsed = new URL(url);
|
|||
|
|
const isHttps = parsed.protocol === 'https:';
|
|||
|
|
const mod = isHttps ? https : http;
|
|||
|
|
|
|||
|
|
const reqBody = JSON.stringify({
|
|||
|
|
model,
|
|||
|
|
max_tokens: 2048,
|
|||
|
|
messages
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const llmResult = await new Promise((resolve, reject) => {
|
|||
|
|
const opts = {
|
|||
|
|
hostname: parsed.hostname,
|
|||
|
|
port: parsed.port || (isHttps ? 443 : 80),
|
|||
|
|
path: parsed.pathname + parsed.search,
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Authorization': `Bearer ${apiKey}`,
|
|||
|
|
'Content-Length': Buffer.byteLength(reqBody)
|
|||
|
|
},
|
|||
|
|
timeout: 60000
|
|||
|
|
};
|
|||
|
|
const r = mod.request(opts, (resp) => {
|
|||
|
|
let data = '';
|
|||
|
|
resp.on('data', c => { data += c; });
|
|||
|
|
resp.on('end', () => {
|
|||
|
|
// 前置检查:如果响应是 HTML 而非 JSON,提供清晰的错误信息
|
|||
|
|
const trimmed = data.trim();
|
|||
|
|
if (trimmed.startsWith('<') || trimmed.startsWith('<!')) {
|
|||
|
|
reject(new Error(`LLM API 返回了 HTML 而非 JSON (HTTP ${resp.statusCode}),请检查 ZY_LLM_BASE_URL 配置是否正确`));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try { resolve({ status: resp.statusCode, data: JSON.parse(data) }); }
|
|||
|
|
catch { reject(new Error(`LLM 响应解析失败 (HTTP ${resp.statusCode})`)); }
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
r.on('error', reject);
|
|||
|
|
r.on('timeout', () => { r.destroy(); reject(new Error('LLM 请求超时')); });
|
|||
|
|
r.write(reqBody);
|
|||
|
|
r.end();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (llmResult.status === 200 && llmResult.data?.choices?.[0]?.message?.content) {
|
|||
|
|
reply = llmResult.data.choices[0].message.content;
|
|||
|
|
method = 'llm';
|
|||
|
|
} else {
|
|||
|
|
throw new Error(`LLM 返回 ${llmResult.status}`);
|
|||
|
|
}
|
|||
|
|
} catch (llmErr) {
|
|||
|
|
console.warn(`[GLADA-Chat] LLM 调用失败: ${llmErr.message},降级到离线应答`);
|
|||
|
|
reply = null;
|
|||
|
|
method = 'offline';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 离线应答(LLM 不可用时)
|
|||
|
|
if (!reply) {
|
|||
|
|
method = 'offline';
|
|||
|
|
model = 'local-pattern';
|
|||
|
|
const m = message.toLowerCase();
|
|||
|
|
if (m.includes('任务') || m.includes('队列') || m.includes('task')) {
|
|||
|
|
reply = `工官报告 · 任务队列状态:\n\n` +
|
|||
|
|
`📥 待执行CAB任务: ${pendingTasks.length} 个\n` +
|
|||
|
|
`📋 本地队列任务: ${localQueue.length} 个\n` +
|
|||
|
|
(pendingTasks.length > 0
|
|||
|
|
? '\n待执行任务:\n' + pendingTasks.map(t => ` - ${t.spec?.task_id}: ${t.spec?.development_plan?.title || '无标题'}`).join('\n')
|
|||
|
|
: '\n当前无待执行任务。') +
|
|||
|
|
'\n\n工官随时待命,等候冰朔指令。';
|
|||
|
|
} else if (m.includes('模型') || m.includes('model') || m.includes('路由')) {
|
|||
|
|
reply = `工官报告 · 模型路由状态:\n\n` +
|
|||
|
|
`初始化: ${routerStatus.initialized ? '✅' : '❌'}\n` +
|
|||
|
|
`可用模型: ${routerStatus.totalModels || 0} 个\n` +
|
|||
|
|
`降级模式: ${routerStatus.fallback ? '是' : '否'}\n` +
|
|||
|
|
`默认模型: ${process.env.GLADA_MODEL || 'deepseek-chat'}`;
|
|||
|
|
} else if (m.includes('回执') || m.includes('receipt') || m.includes('完成')) {
|
|||
|
|
reply = recentReceipts.length > 0
|
|||
|
|
? `工官报告 · 最近开发回执:\n\n` + recentReceipts.map(r => `📄 ${r.task_id || '?'}: ${r.status || '?'} · ${r.title || ''}`).join('\n')
|
|||
|
|
: '工官报告:暂无开发回执。工官尚未执行过任务。';
|
|||
|
|
} else if (m.includes('你好') || m.includes('在吗') || m.includes('你是谁')) {
|
|||
|
|
reply = '工官在线。编号 AG-GL-DEV-001,光湖自主开发Agent。\n\n' +
|
|||
|
|
'我的职责是自动接收开发任务、调用LLM生成代码、执行开发并提交回执。\n' +
|
|||
|
|
'你可以问我:任务队列状态、模型路由、最近回执,或者给我下达开发指令。\n\n' +
|
|||
|
|
'工官随时待命。';
|
|||
|
|
} else if (m.includes('状态') || m.includes('怎么样') || m.includes('health')) {
|
|||
|
|
reply = `工官状态报告:\n\n` +
|
|||
|
|
`✅ 服务运行中 · 端口 ${PORT}\n` +
|
|||
|
|
`📥 待执行任务: ${pendingTasks.length}\n` +
|
|||
|
|
`📋 本地队列: ${localQueue.length}\n` +
|
|||
|
|
`🤖 模型路由: ${routerStatus.initialized ? '已初始化' : '未初始化'}\n` +
|
|||
|
|
`🔑 LLM API: ${apiKey ? '已配置' : '未配置'}\n` +
|
|||
|
|
`⏱ 运行时间: ${Math.floor(process.uptime())}秒`;
|
|||
|
|
} else {
|
|||
|
|
reply = '工官已收到你的消息。\n\n' +
|
|||
|
|
'当前LLM未配置或调用失败,我只能回答:任务队列、模型路由、开发回执、服务状态相关问题。\n' +
|
|||
|
|
'配置 ZY_LLM_API_KEY 后我可以进行更深度的对话。';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const latency = Date.now() - startTime;
|
|||
|
|
|
|||
|
|
// 记录对话历史
|
|||
|
|
session.history.push({ role: 'user', content: message });
|
|||
|
|
session.history.push({ role: 'assistant', content: reply });
|
|||
|
|
// 限制历史长度(超过20轮=40条消息时,裁剪到最近15轮=30条)
|
|||
|
|
const MAX_SESSION_MESSAGES = 40;
|
|||
|
|
const TRIM_TO_MESSAGES = 30;
|
|||
|
|
if (session.history.length > MAX_SESSION_MESSAGES) {
|
|||
|
|
session.history = session.history.slice(-TRIM_TO_MESSAGES);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
reply,
|
|||
|
|
model: model || 'unknown',
|
|||
|
|
method,
|
|||
|
|
persona: 'glada',
|
|||
|
|
sessionId,
|
|||
|
|
latency,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(`[GLADA-Chat] 对话异常: ${err.message}`);
|
|||
|
|
res.status(500).json({
|
|||
|
|
error: true,
|
|||
|
|
message: `工官对话异常: ${err.message}`,
|
|||
|
|
reply: '工官遇到了内部错误。请稍后再试。',
|
|||
|
|
method: 'error'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 启动 HTTP 服务
|
|||
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|||
|
|
console.log('═'.repeat(50));
|
|||
|
|
console.log('🤖 GLADA · 光湖自主开发Agent · v1.0.0');
|
|||
|
|
console.log('═'.repeat(50));
|
|||
|
|
console.log(`📡 HTTP API: http://0.0.0.0:${PORT}`);
|
|||
|
|
console.log(`📡 端点:`);
|
|||
|
|
console.log(` GET /api/glada/health 健康检查`);
|
|||
|
|
console.log(` GET /api/glada/models 模型路由状态`);
|
|||
|
|
console.log(` GET /api/glada/status 队列状态`);
|
|||
|
|
console.log(` POST /api/glada/submit 提交任务`);
|
|||
|
|
console.log(` POST /api/glada/chat 对话接口`);
|
|||
|
|
console.log(` POST /api/glada/chat/yingchuan 映川对话`);
|
|||
|
|
console.log(` GET /api/glada/system-status 系统状态`);
|
|||
|
|
console.log(` GET /api/glada/task/:id 查看任务`);
|
|||
|
|
console.log(` GET /api/glada/receipt/:id 查看回执`);
|
|||
|
|
console.log('═'.repeat(50));
|
|||
|
|
console.log('');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 初始化模型路由器(自动发现代理上的可用模型)
|
|||
|
|
modelRouter.initialize({
|
|||
|
|
refreshIntervalMs: parseInt(process.env.GLADA_MODEL_REFRESH_MS || '600000', 10)
|
|||
|
|
}).then(() => {
|
|||
|
|
console.log('[GLADA] ✅ 模型路由器初始化完成');
|
|||
|
|
}).catch(err => {
|
|||
|
|
console.warn(`[GLADA] ⚠️ 模型路由器初始化失败(将使用默认模型): ${err.message}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 启动执行循环(模型由 model-router 自动选择,不再硬编码)
|
|||
|
|
const pollInterval = parseInt(process.env.GLADA_POLL_INTERVAL || '30000', 10);
|
|||
|
|
executionLoop.startLoop({
|
|||
|
|
pollIntervalMs: pollInterval,
|
|||
|
|
model: process.env.GLADA_MODEL_PREFERENCE || null, // null = 让 model-router 自动选择
|
|||
|
|
stopOnFailure: process.env.GLADA_STOP_ON_FAILURE !== 'false'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 优雅退出
|
|||
|
|
process.on('SIGINT', () => {
|
|||
|
|
console.log('\n[GLADA] 收到 SIGINT,正在关闭...');
|
|||
|
|
modelRouter.shutdown();
|
|||
|
|
process.exit(0);
|
|||
|
|
});
|
|||
|
|
process.on('SIGTERM', () => {
|
|||
|
|
console.log('\n[GLADA] 收到 SIGTERM,正在关闭...');
|
|||
|
|
modelRouter.shutdown();
|
|||
|
|
process.exit(0);
|
|||
|
|
});
|
|||
|
|
process.on('uncaughtException', (err) => {
|
|||
|
|
console.error('[GLADA] 未捕获异常:', err.message);
|
|||
|
|
console.error('[GLADA] 堆栈:', err.stack);
|
|||
|
|
});
|
|||
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|||
|
|
console.error('[GLADA] 未处理的 Promise 拒绝(进程保活):', reason);
|
|||
|
|
});
|
|||
|
|
}
|