index.js
11.4 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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/**
* 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')); // /api/goals 别名已退役(27-P3C §六.1)
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('/api/dream', require('./routes/dream')); // 29-DREAM 经验汇流
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(() => {
// 跑全量测试前把后台驱动全关掉:`DISABLE_BACKGROUND_AGENTS=1 bash start.sh`。
//
// 为什么需要这个开关:这些驱动会起 agent,而 agent 的 cwd 是运行态目录
// `system/agents/<agent>/<companyId>/<taskId>/`(locator.taskDir,落在本体目录里属既定设计)。
// 心跳每 5 分钟建一个新的,实测 cto/_global/ 攒到 260+ 子项还在涨 —— 于是扫描
// system/agents/ 的守卫测试(no-dead-path-references 那一类)会被瞬时竞态打红:
// 列到了目录名,进去时那一层还没写完。症状是偶发假失败,重跑就好,最难查。
//
// 关的是「会自己起 agent 的东西」,恰好全在这个 setTimeout 里,所以一条早退就够:
// 两处中断恢复 + 任务队列兜底扫描 + 两路心跳。
if (process.env.DISABLE_BACKGROUND_AGENTS === '1') {
console.log('[Startup] ⏸ DISABLE_BACKGROUND_AGENTS=1 —— 中断恢复 / 任务队列 / 两路心跳全部跳过');
return;
}
resumeIncompleteTasks().catch(err => console.error('[Startup] 恢复任务失败:', err.message));
// 27-P3C:goal 级验收窗口里被打断的那些(status 已终态、判词还是占位值)。
// 和上面同属「中断任务恢复」,也同样要等 HTTP 就绪——它收尾时会打自家 /api/agent/task/report。
require('./services/task-settle').resumePendingGoalValidations()
.catch(err => console.error('[Startup] 重推 goal 验收失败:', err.message));
// 27-P3C 批 3:任务队列。进程重启会丢掉所有在途的 setImmediate 唤醒
// (队列钩子挂在 graph-run-sse.transitionRun 上,那是进程内的),
// 于是重启后队里的任务会一直躺着 —— 界面上和「正在排队」一模一样,正是本批要消灭的症状。
// 所以启动时先补一次全量扫描,再起 60s 兜底定时器(unref,绝不拖着进程不退出)。
try {
const scheduler = require('./services/task-scheduler');
const woken = scheduler.sweep().filter(r => r.started && r.started.length);
console.log(`[Startup] 🗂 任务队列扫描完成,起跑 ${woken.length} 条`);
scheduler.startSweeper();
} 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();
// 29-DREAM 决策 C:经验汇流,每日一次。挂在这个早退之后 ——
// 它会起 agent(提炼 + 去敏判定各一次),归 DISABLE_BACKGROUND_AGENTS 管。
// 只挂定时**不立刻跑一轮**:启动那一刻跑没有额外收益,反而让每次重启都烧一遍。
require('./services/dream-service').startDaily();
}, 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;