index.js
8.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* ArenaSK Server — 主入口
*
* 启动: node index.js | node --watch index.js (开发)
*/
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const path = require('path');
const http = require('http');
const WebSocket = require('ws');
const config = require('./config');
const { ensureDataDirs } = require('./services/skill-dir-manager');
const { PluginManager } = require('./src/plugins/plugin-manager');
const { initClaudeChatWSServer } = require('./routes/claude-chat-ws');
const app = express();
const server = http.createServer(app);
const pluginManager = new PluginManager();
// ─── 中间件 ───
app.use(cors(config.cors));
app.use(cookieParser());
// ── 控制字符净化中间件 ──
// Agent 通过 curl 发送的 JSON body 可能包含原始换行/制表符等控制字符,
// 导致 body-parser 的 JSON.parse 失败 (Bad control character in string literal)
// 此中间件在 JSON 解析前预处理 body,转义 JSON 字符串内的控制字符
app.use((req, res, next) => {
if (req.headers['content-type']?.includes('application/json')) {
let rawBody = '';
req.on('data', chunk => rawBody += chunk);
req.on('end', () => {
if (!rawBody) return next();
try {
// 先尝试直接解析
req.body = JSON.parse(rawBody);
req._bodyParsed = true;
next();
} catch (e) {
// 解析失败,清理控制字符后重试
try {
// eslint-disable-next-line no-control-regex
const cleaned = rawBody.replace(/[\x00-\x1F\x7F]/g, (ch) => {
if (ch === '\n') return '\\n';
if (ch === '\r') return '\\r';
if (ch === '\t') return '\\t';
return ''; // 删除其他控制字符
});
req.body = JSON.parse(cleaned);
req._bodyParsed = true;
console.log(`[BodySanitizer] ⚠️ 已净化请求体中的控制字符 (${req.path})`);
next();
} catch (e2) {
res.status(400).json({ error: 'Invalid JSON: ' + e2.message });
}
}
});
} else {
next();
}
});
// express.json() 仅处理未被上面中间件处理的请求
app.use((req, res, next) => {
if (req._bodyParsed) return next();
express.json()(req, res, next);
});
// ─── JSON 语法错误处理 ───
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && err['status'] === 400 && 'body' in err) {
console.warn('❌ 收到无效的 JSON Payload:', err.message);
return res.status(400).json({
error: '无效的 JSON 格式 (请检查是否包含未转义的多行文本、换行符或引号)',
details: err.message
});
}
next(err);
});
// ─── API 路由 ───
app.use('/api/claude-chat', require('./routes/claude-chat'));
app.use('/api/config', require('./routes/config'));
app.use('/api/evolution', require('./routes/evolution'));
app.use('/api/innovator', require('./routes/innovator'));
app.use('/api/master', require('./routes/master-chat'));
app.use('/api/open', require('./routes/open-api'));
app.use('/api/arena', require('./routes/arena'));
app.use('/api/auth', require('./routes/auth'));
app.use('/api/whatsapp', require('./routes/whatsapp-auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/agent', require('./routes/agent-chat'));
app.use('/api/okr', require('./routes/okr'));
app.use('/api/goals', require('./routes/okr')); // 向后兼容别名(27-NAMING 六)
app.use('/api/company', require('./routes/company'));
app.use('/api/tasks', require('./routes/tasks'));
app.use('/api/devloop', require('./routes/devloop'));
app.use('/api/graph-runs', require('./routes/graph-run')); // P3 图执行(T6)
app.use('/api/company', require('./routes/graph-run').companyRouter); // GET /:cid/graph-runs
app.use('/public', require('express').static(require('path').join(__dirname, 'public')));
// ─── 系统端点 ───
app.get('/api/health', (req, res) => res.json({
status: 'ok', timestamp: new Date().toISOString(), plugins: pluginManager.summary(),
}));
app.get('/api/plugins', (req, res) => res.json(pluginManager.summary()));
// ─── 静态文件 (生产环境) ───
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../web/dist')));
app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../web/dist/index.html')));
}
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({ error: err.message });
});
// ─── 启动 ───
async function start() {
try {
ensureDataDirs();
// Graph agent 元数据对齐(P3B W1):把 system/agents 的目录现状
// 同步进 system_agents,并回收指向已删目录的悬空行。必须在 listen() 之前——
// work_graph_nodes.system_agent_id 是指向这张表的外键,第一个请求就可能提交图。
// 幂等 + 同步 + 毫秒级;内部已包 try/catch,失败只降级不挡启动。
require('./services/graph-seed-service').runStartupSeedSafely();
const host = config.host || '0.0.0.0';
server.listen(Number(config.port), host, () => {
console.log(`✅ 服务启动: http://${host}:${config.port}`);
});
const wss = new WebSocket.Server({ server, path: '/ws/claude' });
initClaudeChatWSServer(wss);
// 插件系统(异步,不阻塞 HTTP)
initPlugins().catch(err => console.error('插件启动失败:', err.message));
// 恢复中断任务并起搏 Agent (延迟 5s,等 HTTP 服务完全就绪)
setTimeout(() => {
resumeIncompleteTasks().catch(err => console.error('[Startup] 恢复任务失败:', err.message));
const heartbeatService = require('./services/heartbeat-service');
heartbeatService.start();
// 公司级心跳 (Pipeline 自动驱动)
const { companyHeartbeatService } = require('./services/company-heartbeat-service');
companyHeartbeatService.start();
}, 5000);
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
async function resumeIncompleteTasks() {
const taskMgr = require('./services/skill-task-manager');
const skillsDir = config.skillsCodeDir || path.resolve(__dirname, '../skills');
const incomplete = taskMgr.findIncompleteTasks(skillsDir);
if (incomplete.length === 0) return console.log('[Startup] ✅ 无中断任务');
console.log(`[Startup] 🔄 恢复 ${incomplete.length} 个中断任务...`);
const { _jobManager, _runSkillCreation, _runFeedbackIteration } = require('./routes/open-api');
for (const item of incomplete) {
try {
const job = _jobManager.createJob({
domain: item.task.domain || '', scope: item.task.scope || '',
description: item.task.description || '', keywords: item.task.keywords || [],
});
job.skillName = item.skillName;
if (item.task.pipelineType === 'feedback' && _runFeedbackIteration) {
await _runFeedbackIteration(job, item.task.feedback || '', {
skillName: item.skillName, domain: item.task.domain || '',
scope: item.task.scope || '', keywords: item.task.keywords || [],
});
} else if (_runSkillCreation) {
await _runSkillCreation(job, item.nextStep);
}
} catch (err) {
console.error(`[Startup] 恢复 ${item.skillName} 失败:`, err.message);
}
}
}
async function initPlugins() {
await pluginManager.init();
if (!pluginManager.enabled) return console.log('ℹ️ 插件: 未配置 channel,跳过');
const { runOrchestration, runMasterSession } = require('./src/standalone-entry');
await pluginManager.start({
runOrchestration: ({ task }) => runOrchestration(task),
runMasterSession: ({ message }) => runMasterSession(message),
});
console.log(`✅ 插件就绪: channels=[${pluginManager.channels.map(c => c.id)}], tools=${pluginManager.tools.length}`);
}
// ─── 优雅关闭 ───
async function shutdown() {
console.log('\n正在关闭...');
await pluginManager.shutdown();
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
start();
module.exports = app;