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