openclaw-sc 5.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -0
- package/INSTALL.md +13 -0
- package/LICENSE +233 -0
- package/README.md +123 -0
- package/SECURITY.md +27 -0
- package/index.js +3479 -0
- package/lib/code-review-shared.js +164 -0
- package/lib/config.js +164 -0
- package/lib/constants.js +438 -0
- package/lib/decomposer.js +896 -0
- package/lib/dialog-recall.js +389 -0
- package/lib/env.js +59 -0
- package/lib/level-rules.js +72 -0
- package/lib/log-manager.js +258 -0
- package/lib/preemption.js +278 -0
- package/lib/priority-calc.js +134 -0
- package/lib/prompt-injection.js +748 -0
- package/lib/route-evidence.js +701 -0
- package/lib/shared-fs.js +244 -0
- package/lib/steward-rules.js +648 -0
- package/lib/task-center.js +602 -0
- package/lib/task-chain.js +713 -0
- package/lib/task-checker.js +317 -0
- package/lib/task-profiles.js +255 -0
- package/lib/tcell.js +411 -0
- package/lib/tool-auto-discover.js +522 -0
- package/lib/tool-enrich-subagent.js +375 -0
- package/lib/tool-handlers.js +178 -0
- package/lib/tool-selector.js +459 -0
- package/openclaw.plugin.json +55 -0
- package/package.json +63 -0
- package/security.js +141 -0
- package/tools/bridge.js +3288 -0
- package/tools/dashboard/tk-dashboard.py +262 -0
- package/tools/hippocampus-multi-search.js +1038 -0
- package/tools/hippocampus-sqlite.js +738 -0
- package/tools/hippocampus-store.js +262 -0
- package/tools/mcp-tools.config.json +653 -0
- package/tools/pipeline-engine.js +667 -0
- package/tools/sidecar/_check_tools.py +34 -0
- package/tools/sidecar/sidecar-server.cjs +1360 -0
- package/tools/sidecar/subagent-runner.cjs +1471 -0
- package/tools/system-tools.js +619 -0
- package/vector/README.md +100 -0
- package/vector/usearch-bridge.js +639 -0
- package/vector/usearch-http.js +74 -0
- package/vector/usearch-serve.js +156 -0
- package/workers/worker.js +1419 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 triple-evidence routing决策 — 路由决策triple-evidence记录系统
|
|
3
|
+
*
|
|
4
|
+
* triple-evidence:
|
|
5
|
+
* 本(历史经验):过去同类任务用了什么工具、成功率、平均耗时
|
|
6
|
+
* 原(实时状态):当前 Worker 负载、队列深度、内存水位
|
|
7
|
+
* 用(实践反馈):执行结果、耗时、是否需优化
|
|
8
|
+
*
|
|
9
|
+
* 每次路由决策记录一个结构化 JSON 日志到 shared/route-decisions/
|
|
10
|
+
* 每 100 记录做一次汇总分析
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { homedir, freemem } from "os";
|
|
15
|
+
import { readFile, mkdir, readdir, unlink, writeFile, appendFile, rename } from "fs/promises";
|
|
16
|
+
|
|
17
|
+
// ====== 配置 ======
|
|
18
|
+
const DECISIONS_DIR = join(homedir(), ".openclaw", "workspace", "memory", "shared", "route-decisions");
|
|
19
|
+
const SUMMARY_INTERVAL = 100; // 每 100 记录做一次汇总
|
|
20
|
+
// 🧠 设计决策:MAX_HISTORY_FOR_本=200。查询过去同类任务时最多回溯 200 记录。
|
|
21
|
+
// 200 刚好跨越 ~20 min(假设 10 /min的路由密度),覆盖一次完整会话的典型工作时长。
|
|
22
|
+
// 多了(500+)会拖慢 collect本() 的加载和stats,少了(<50)模糊匹配不够稳定。
|
|
23
|
+
// 配合 24 小时权重衰减(DEFAULT_DECAY_HOURS),老记录权重自然降低,
|
|
24
|
+
// 200 上限保证历史经验不会过度稀释新决策的影响。
|
|
25
|
+
const MAX_HISTORY_FOR_本 = 200;
|
|
26
|
+
const MAX_LOGS_TO_KEEP = 5000; // 最多保留 5000 决策日志
|
|
27
|
+
const DEFAULT_DECAY_HOURS = 24; // 过24小时的老记录权重衰减
|
|
28
|
+
|
|
29
|
+
// 计数器(进程级)
|
|
30
|
+
let decisionCounter = 0;
|
|
31
|
+
let lastSummaryCount = 0;
|
|
32
|
+
|
|
33
|
+
// ====== 原子写入辅助(先写.tmp再rename,防止写入中途崩溃导致文件损坏)======
|
|
34
|
+
async function atomicWrite(filePath, content, options) {
|
|
35
|
+
const tmpPath = filePath + ".tmp." + Date.now();
|
|
36
|
+
try {
|
|
37
|
+
await writeFile(tmpPath, content, options);
|
|
38
|
+
await rename(tmpPath, filePath);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// 清理残留.tmp文件
|
|
41
|
+
try { await unlink(tmpPath); } catch {}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 原子追加到 JSONL 文件(用 fs.promises.appendFile 实现真正的原子追加)
|
|
48
|
+
* 避免 read→modify→write 三步操作导致的并发写入丢失
|
|
49
|
+
*/
|
|
50
|
+
async function atomicAppendJsonl(filePath, recordJson) {
|
|
51
|
+
try {
|
|
52
|
+
await appendFile(filePath, recordJson + "\n", "utf-8");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ====== 路径辅助 ======
|
|
59
|
+
let _dirEnsured = false;
|
|
60
|
+
|
|
61
|
+
async function ensureDecisionsDir() {
|
|
62
|
+
if (_dirEnsured) return;
|
|
63
|
+
try {
|
|
64
|
+
await mkdir(DECISIONS_DIR, { recursive: true });
|
|
65
|
+
_dirEnsured = true;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code !== "EEXIST") throw err;
|
|
68
|
+
_dirEnsured = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 获取当天日志文件名(按天分片,防单文件过大)
|
|
74
|
+
*/
|
|
75
|
+
function getLogFileName() {
|
|
76
|
+
const now = new Date();
|
|
77
|
+
const y = now.getFullYear();
|
|
78
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
79
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
80
|
+
return `route-decisions-${y}-${m}-${d}.jsonl`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 获取汇总文件名
|
|
85
|
+
*/
|
|
86
|
+
function getSummaryFileName() {
|
|
87
|
+
const now = new Date();
|
|
88
|
+
const y = now.getFullYear();
|
|
89
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
90
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
91
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
92
|
+
return `route-summary-${y}-${m}-${d}-${h}.json`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ====== triple-evidence收集 ======
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 收集「本」证据 — 历史经验分析
|
|
99
|
+
* @param {string} taskDesc - 任务描述
|
|
100
|
+
* @returns {Promise<object>} 历史经验数据
|
|
101
|
+
*/
|
|
102
|
+
async function collect本(taskDesc) {
|
|
103
|
+
const entries = await loadRecentDecisions(MAX_HISTORY_FOR_本);
|
|
104
|
+
|
|
105
|
+
// 分工具stats
|
|
106
|
+
const toolStats = {};
|
|
107
|
+
let totalDecisions = entries.length;
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const tool = entry.decision?.recommendedTool || entry.decision?.decision || "unknown";
|
|
111
|
+
if (!toolStats[tool]) {
|
|
112
|
+
toolStats[tool] = { count: 0, success: 0, timeout: 0, error: 0, totalTimeMs: 0 };
|
|
113
|
+
}
|
|
114
|
+
toolStats[tool].count++;
|
|
115
|
+
|
|
116
|
+
const 用 = entry.evidence?.用 || {};
|
|
117
|
+
if (用.status === "success") toolStats[tool].success++;
|
|
118
|
+
else if (用.status === "timeout") toolStats[tool].timeout++;
|
|
119
|
+
else if (用.status === "error") toolStats[tool].error++;
|
|
120
|
+
|
|
121
|
+
if (typeof 用.durationMs === "number") {
|
|
122
|
+
toolStats[tool].totalTimeMs += 用.durationMs;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 计算成功率
|
|
127
|
+
const toolSummary = Object.entries(toolStats).map(([tool, stats]) => {
|
|
128
|
+
const finished = stats.success + stats.timeout + stats.error;
|
|
129
|
+
return {
|
|
130
|
+
tool,
|
|
131
|
+
total: stats.count,
|
|
132
|
+
successRate: finished > 0 ? (stats.success / finished) : 0,
|
|
133
|
+
avgDurationMs: stats.count > 0 ? Math.round(stats.totalTimeMs / stats.count) : 0,
|
|
134
|
+
failureBreakdown: {
|
|
135
|
+
timeout: stats.timeout,
|
|
136
|
+
error: stats.error,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}).sort((a, b) => b.total - a.total);
|
|
140
|
+
|
|
141
|
+
// 模糊匹配相似任务描述(过去200中找关键词重叠)
|
|
142
|
+
const keywords = extractKeywords(taskDesc);
|
|
143
|
+
let similarTasks = [];
|
|
144
|
+
if (keywords.length > 0) {
|
|
145
|
+
similarTasks = entries
|
|
146
|
+
.filter(e => {
|
|
147
|
+
const kws = extractKeywords(e.task || "");
|
|
148
|
+
return kws.some(k => keywords.includes(k));
|
|
149
|
+
})
|
|
150
|
+
.slice(0, 20)
|
|
151
|
+
.map(e => ({
|
|
152
|
+
task: (e.task || "").substring(0, 80),
|
|
153
|
+
tool: e.decision?.recommendedTool || e.decision?.decision || "unknown",
|
|
154
|
+
confidence: e.decision?.confidence || 0,
|
|
155
|
+
status: e.evidence?.用?.status || "unknown",
|
|
156
|
+
durationMs: e.evidence?.用?.durationMs || null,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
totalDecisions,
|
|
162
|
+
toolHistory: toolSummary.slice(0, 15),
|
|
163
|
+
similarTasks: similarTasks.length > 0 ? similarTasks : undefined,
|
|
164
|
+
recentToolCount: toolSummary.length,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 从任务描述中提取关键词
|
|
170
|
+
*/
|
|
171
|
+
function extractKeywords(text) {
|
|
172
|
+
if (!text || typeof text !== "string") return [];
|
|
173
|
+
// 过滤停用词,提取有意义的2-4字中文词和英文词
|
|
174
|
+
const stopWords = new Set([
|
|
175
|
+
"的", "了", "是", "在", "有", "和", "就", "不", "人", "都", "一",
|
|
176
|
+
"一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着",
|
|
177
|
+
"没有", "看", "好", "自己", "这", "他", "她", "它", "们", "那",
|
|
178
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
|
179
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from",
|
|
180
|
+
]);
|
|
181
|
+
const words = text.toLowerCase().split(/[\s,,。;;::、!!??()()\[\]【】{}\"'"“”«»《》\/\\]+/);
|
|
182
|
+
const significant = words
|
|
183
|
+
.map(w => w.trim())
|
|
184
|
+
.filter(w => w.length >= 2 && !stopWords.has(w))
|
|
185
|
+
.filter((w, i, arr) => arr.indexOf(w) === i); // 去重
|
|
186
|
+
return significant.slice(0, 10);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 收集「原」证据 — 当前实时状态
|
|
191
|
+
* @param {object} pool - Worker 池实例
|
|
192
|
+
* @returns {Promise<object>} 实时状态数据
|
|
193
|
+
*/
|
|
194
|
+
function collect原(pool) {
|
|
195
|
+
const stats = pool?.getStats ? pool.getStats() : {};
|
|
196
|
+
const mem = process.memoryUsage();
|
|
197
|
+
const freeGB = freemem() / 1024 / 1024 / 1024;
|
|
198
|
+
|
|
199
|
+
let memLevel = "green";
|
|
200
|
+
if (freeGB < 2) memLevel = "meltdown";
|
|
201
|
+
else if (freeGB < 4) memLevel = "red";
|
|
202
|
+
else if (freeGB < 8) memLevel = "yellow";
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
pool: {
|
|
207
|
+
totalWorkers: stats.total || 0,
|
|
208
|
+
busyWorkers: stats.busy || 0,
|
|
209
|
+
inFlight: stats.inFlight || 0,
|
|
210
|
+
queueDepth: stats.queueDepth || 0,
|
|
211
|
+
queueHigh: stats.queueHigh || 0,
|
|
212
|
+
queueNormal: stats.queueNormal || 0,
|
|
213
|
+
queueLow: stats.queueLow || 0,
|
|
214
|
+
maxWorkers: stats.maxWorkers || 0,
|
|
215
|
+
},
|
|
216
|
+
memory: {
|
|
217
|
+
freeGB: Math.round(freeGB * 100) / 100,
|
|
218
|
+
heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024),
|
|
219
|
+
rssMB: Math.round(mem.rss / 1024 / 1024),
|
|
220
|
+
level: memLevel,
|
|
221
|
+
},
|
|
222
|
+
loadLevel: stats.queueDepth > 10 ? "high" : stats.queueDepth > 5 ? "medium" : "low",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 创建「用」证据占位
|
|
228
|
+
* @param {string} status - pending|success|timeout|error
|
|
229
|
+
* @param {number|null} durationMs - 执行耗时
|
|
230
|
+
* @param {string|null} optimization - 优化建议
|
|
231
|
+
* @returns {object} 实践反馈数据
|
|
232
|
+
*/
|
|
233
|
+
function create用(status = "pending", durationMs = null, optimization = null) {
|
|
234
|
+
return {
|
|
235
|
+
status,
|
|
236
|
+
durationMs,
|
|
237
|
+
optimization,
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ====== 记录写入 ======
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 记录一次路由决策(triple-evidence完整写入)
|
|
246
|
+
* @param {object} routeResult - core_routeTask 返回的路由结果
|
|
247
|
+
* @param {string} taskDesc - 原始任务描述
|
|
248
|
+
* @param {object} pool - Worker 池实例(用于收集「原」)
|
|
249
|
+
* @returns {Promise<string>} 记录 ID
|
|
250
|
+
*/
|
|
251
|
+
export async function recordRouteDecision(routeResult, taskDesc, pool) {
|
|
252
|
+
await ensureDecisionsDir();
|
|
253
|
+
|
|
254
|
+
decisionCounter++;
|
|
255
|
+
|
|
256
|
+
const recordId = `rd_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
257
|
+
|
|
258
|
+
// 并行收集triple-evidence
|
|
259
|
+
const [历史经验] = await Promise.all([
|
|
260
|
+
collect本(taskDesc),
|
|
261
|
+
// collect原 is synchronous, no need to await
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const 实时状态 = collect原(pool);
|
|
265
|
+
|
|
266
|
+
const record = {
|
|
267
|
+
id: recordId,
|
|
268
|
+
counter: decisionCounter,
|
|
269
|
+
timestamp: new Date().toISOString(),
|
|
270
|
+
task: (taskDesc || "").substring(0, 500),
|
|
271
|
+
decision: {
|
|
272
|
+
tree: routeResult.tree || "unknown",
|
|
273
|
+
recommendedTool: routeResult.recommendedTool || routeResult.decision || null,
|
|
274
|
+
confidence: routeResult.confidence || 0,
|
|
275
|
+
strategy: routeResult.strategy || "unknown",
|
|
276
|
+
risk: routeResult.risk || "unknown",
|
|
277
|
+
params: routeResult.params || {},
|
|
278
|
+
note: routeResult.note || "",
|
|
279
|
+
},
|
|
280
|
+
evidence: {
|
|
281
|
+
本: {
|
|
282
|
+
totalDecisions: 历史经验.totalDecisions || 0,
|
|
283
|
+
toolHistory: (历史经验.toolHistory || []).slice(0, 10),
|
|
284
|
+
similarTasks: (历史经验.similarTasks || []).slice(0, 5),
|
|
285
|
+
recentToolCount: 历史经验.recentToolCount || 0,
|
|
286
|
+
},
|
|
287
|
+
原: 实时状态,
|
|
288
|
+
用: create用("pending"),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// 原子追加到当天日志文件
|
|
293
|
+
const logFile = join(DECISIONS_DIR, getLogFileName());
|
|
294
|
+
try {
|
|
295
|
+
await atomicAppendJsonl(logFile, JSON.stringify(record));
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.warn(`[route-evidence] write failed: ${err.message}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 每 SUMMARY_INTERVAL 做一次汇总
|
|
301
|
+
if (decisionCounter - lastSummaryCount >= SUMMARY_INTERVAL) {
|
|
302
|
+
lastSummaryCount = decisionCounter;
|
|
303
|
+
try {
|
|
304
|
+
await generateSummary();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.warn(`[route-evidence] merge failed: ${err.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 限制日志数量
|
|
311
|
+
if (decisionCounter % 50 === 0) {
|
|
312
|
+
try {
|
|
313
|
+
await limitLogFiles();
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return recordId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 更新「用」证据(实践反馈)
|
|
322
|
+
* @param {string} recordId - 之前记录的 ID
|
|
323
|
+
* @param {string} status - success|timeout|error
|
|
324
|
+
* @param {number|null} durationMs - 执行耗时
|
|
325
|
+
* @param {string|null} optimization - 优化建议
|
|
326
|
+
*/
|
|
327
|
+
export async function update用Evidence(recordId, status, durationMs = null, optimization = null) {
|
|
328
|
+
await ensureDecisionsDir();
|
|
329
|
+
|
|
330
|
+
const todayFile = join(DECISIONS_DIR, getLogFileName());
|
|
331
|
+
try {
|
|
332
|
+
const content = await readFile(todayFile, "utf-8");
|
|
333
|
+
const lines = content.split("\n").filter(Boolean);
|
|
334
|
+
let found = false;
|
|
335
|
+
|
|
336
|
+
const updated = lines.map(line => {
|
|
337
|
+
try {
|
|
338
|
+
const record = JSON.parse(line);
|
|
339
|
+
if (record.id === recordId) {
|
|
340
|
+
record.evidence.用 = create用(status, durationMs, optimization);
|
|
341
|
+
found = true;
|
|
342
|
+
}
|
|
343
|
+
return JSON.stringify(record);
|
|
344
|
+
} catch {
|
|
345
|
+
return line;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (found) {
|
|
350
|
+
// 写临时文件→rename 原子替换模式,避免并发读取竞态
|
|
351
|
+
const tmpPath = todayFile + ".tmp." + Date.now();
|
|
352
|
+
await writeFile(tmpPath, updated.join("\n") + "\n", "utf-8");
|
|
353
|
+
await rename(tmpPath, todayFile);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return found;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.warn(`[route-evidence] update evidence failed: ${err.message}`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ====== 日志加载与查询 ======
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 加载最近的决策记录
|
|
367
|
+
* @param {number} limit - 最多加载多少
|
|
368
|
+
* @returns {Promise<Array>} 决策记录数组
|
|
369
|
+
*/
|
|
370
|
+
async function loadRecentDecisions(limit = 100) {
|
|
371
|
+
await ensureDecisionsDir();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const files = await readdir(DECISIONS_DIR);
|
|
375
|
+
const logFiles = files
|
|
376
|
+
.filter(f => f.startsWith("route-decisions-") && f.endsWith(".jsonl"))
|
|
377
|
+
.sort()
|
|
378
|
+
.reverse();
|
|
379
|
+
|
|
380
|
+
const entries = [];
|
|
381
|
+
|
|
382
|
+
for (const file of logFiles) {
|
|
383
|
+
if (entries.length >= limit) break;
|
|
384
|
+
const fp = join(DECISIONS_DIR, file);
|
|
385
|
+
try {
|
|
386
|
+
const content = await readFile(fp, "utf-8");
|
|
387
|
+
const lines = content.split("\n").filter(Boolean).reverse();
|
|
388
|
+
for (const line of lines) {
|
|
389
|
+
if (entries.length >= limit) break;
|
|
390
|
+
try {
|
|
391
|
+
entries.push(JSON.parse(line));
|
|
392
|
+
} catch {}
|
|
393
|
+
}
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return entries;
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.warn(`[route-evidence] load history failed: ${err.message}`);
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ====== 汇总分析 ======
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 生成汇总分析报告
|
|
408
|
+
* 分析最近 1000 记录,输出工具使用排名、成功率、瓶颈识别
|
|
409
|
+
*/
|
|
410
|
+
async function generateSummary() {
|
|
411
|
+
await ensureDecisionsDir();
|
|
412
|
+
|
|
413
|
+
const entries = await loadRecentDecisions(SUMMARY_INTERVAL);
|
|
414
|
+
if (entries.length === 0) return;
|
|
415
|
+
|
|
416
|
+
const toolStats = {};
|
|
417
|
+
const strategyStats = {};
|
|
418
|
+
let totalSuccess = 0;
|
|
419
|
+
let totalFinished = 0;
|
|
420
|
+
const levelRanges = { green: 0, yellow: 0, red: 0, meltdown: 0 };
|
|
421
|
+
let peakQueueDepth = 0;
|
|
422
|
+
let avgQueueDepth = 0;
|
|
423
|
+
const durations = [];
|
|
424
|
+
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
const tool = entry.decision?.recommendedTool || entry.decision?.decision || "unknown";
|
|
427
|
+
const strategy = entry.decision?.strategy || "unknown";
|
|
428
|
+
const 用 = entry.evidence?.用 || {};
|
|
429
|
+
const 原 = entry.evidence?.原 || {};
|
|
430
|
+
|
|
431
|
+
// 工具stats
|
|
432
|
+
if (!toolStats[tool]) toolStats[tool] = { count: 0, success: 0, timeout: 0, error: 0, pending: 0, totalTimeMs: 0 };
|
|
433
|
+
toolStats[tool].count++;
|
|
434
|
+
if (用.status === "success") { toolStats[tool].success++; totalSuccess++; }
|
|
435
|
+
else if (用.status === "timeout") toolStats[tool].timeout++;
|
|
436
|
+
else if (用.status === "error") toolStats[tool].error++;
|
|
437
|
+
else if (用.status === "pending") toolStats[tool].pending++;
|
|
438
|
+
|
|
439
|
+
if (用.status !== "pending") totalFinished++;
|
|
440
|
+
|
|
441
|
+
if (typeof 用.durationMs === "number") {
|
|
442
|
+
toolStats[tool].totalTimeMs += 用.durationMs;
|
|
443
|
+
durations.push(用.durationMs);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 策略stats
|
|
447
|
+
if (!strategyStats[strategy]) strategyStats[strategy] = 0;
|
|
448
|
+
strategyStats[strategy]++;
|
|
449
|
+
|
|
450
|
+
// 状态stats
|
|
451
|
+
const memLevel = 原.memory?.level || "green";
|
|
452
|
+
levelRanges[memLevel] = (levelRanges[memLevel] || 0) + 1;
|
|
453
|
+
|
|
454
|
+
const qd = 原.pool?.queueDepth || 0;
|
|
455
|
+
peakQueueDepth = Math.max(peakQueueDepth, qd);
|
|
456
|
+
avgQueueDepth += qd;
|
|
457
|
+
}
|
|
458
|
+
avgQueueDepth = avgQueueDepth / entries.length;
|
|
459
|
+
|
|
460
|
+
// 计算平均耗时(成功任务)
|
|
461
|
+
const avgDuration = durations.length > 0
|
|
462
|
+
? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
|
|
463
|
+
: null;
|
|
464
|
+
|
|
465
|
+
// 工具排名(按调用次数降序)
|
|
466
|
+
const toolRanking = Object.entries(toolStats)
|
|
467
|
+
.map(([tool, stats]) => {
|
|
468
|
+
const finished = stats.success + stats.timeout + stats.error;
|
|
469
|
+
return {
|
|
470
|
+
tool,
|
|
471
|
+
calls: stats.count,
|
|
472
|
+
successRate: finished > 0 ? Math.round((stats.success / finished) * 100) : null,
|
|
473
|
+
avgDurationMs: stats.count > 0 && stats.totalTimeMs > 0
|
|
474
|
+
? Math.round(stats.totalTimeMs / (stats.success || 1))
|
|
475
|
+
: null,
|
|
476
|
+
pending: stats.pending,
|
|
477
|
+
failures: stats.timeout + stats.error,
|
|
478
|
+
};
|
|
479
|
+
})
|
|
480
|
+
.sort((a, b) => b.calls - a.calls);
|
|
481
|
+
|
|
482
|
+
// 瓶颈识别
|
|
483
|
+
const bottlenecks = [];
|
|
484
|
+
for (const rank of toolRanking) {
|
|
485
|
+
if (rank.successRate !== null && rank.successRate < 60) {
|
|
486
|
+
bottlenecks.push({
|
|
487
|
+
tool: rank.tool,
|
|
488
|
+
issue: `成功率仅 ${rank.successRate}%`,
|
|
489
|
+
failures: rank.failures,
|
|
490
|
+
suggestion: "建议检查该工具实现或增加retry机制",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (peakQueueDepth > 10) {
|
|
496
|
+
bottlenecks.push({
|
|
497
|
+
tool: "system",
|
|
498
|
+
issue: `队列峰值深度 ${peakQueueDepth},可能过载`,
|
|
499
|
+
suggestion: "建议增加 Worker 数或限流",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const summary = {
|
|
504
|
+
id: `summary_${Date.now()}`,
|
|
505
|
+
timestamp: new Date().toISOString(),
|
|
506
|
+
period: {
|
|
507
|
+
analyzedRecords: entries.length,
|
|
508
|
+
from: entries[entries.length - 1]?.timestamp || "unknown",
|
|
509
|
+
to: entries[0]?.timestamp || "unknown",
|
|
510
|
+
},
|
|
511
|
+
overview: {
|
|
512
|
+
totalDecisions: entries.length,
|
|
513
|
+
totalFinished,
|
|
514
|
+
totalSuccess,
|
|
515
|
+
overallSuccessRate: totalFinished > 0
|
|
516
|
+
? Math.round((totalSuccess / totalFinished) * 100)
|
|
517
|
+
: null,
|
|
518
|
+
avgDurationMs: avgDuration,
|
|
519
|
+
peakQueueDepth,
|
|
520
|
+
avgQueueDepth: Math.round(avgQueueDepth * 10) / 10,
|
|
521
|
+
},
|
|
522
|
+
toolRanking: toolRanking.slice(0, 15),
|
|
523
|
+
strategyDistribution: strategyStats,
|
|
524
|
+
memoryDistribution: levelRanges,
|
|
525
|
+
bottlenecks: bottlenecks.length > 0 ? bottlenecks : undefined,
|
|
526
|
+
recommendation: bottlenecks.length > 0
|
|
527
|
+
? `检测到 ${bottlenecks.length} 个瓶颈点,建议关注: ${bottlenecks.map(b => b.tool).join(", ")}`
|
|
528
|
+
: "系统运行良好,未发现明显瓶颈",
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// 原子写入汇总文件
|
|
532
|
+
const summaryFile = join(DECISIONS_DIR, getSummaryFileName());
|
|
533
|
+
await atomicWrite(summaryFile, JSON.stringify(summary, null, 2), "utf-8");
|
|
534
|
+
|
|
535
|
+
return summary;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ====== 日志管理 ======
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 限制日志文件数量,删除最旧的超量文件
|
|
542
|
+
*/
|
|
543
|
+
async function limitLogFiles() {
|
|
544
|
+
try {
|
|
545
|
+
const files = await readdir(DECISIONS_DIR);
|
|
546
|
+
const logFiles = files
|
|
547
|
+
.filter(f => f.startsWith("route-decisions-") && f.endsWith(".jsonl"))
|
|
548
|
+
.sort()
|
|
549
|
+
.reverse();
|
|
550
|
+
|
|
551
|
+
if (logFiles.length < 10) return; // 留够10天的量
|
|
552
|
+
|
|
553
|
+
const toDelete = logFiles.slice(10);
|
|
554
|
+
for (const f of toDelete) {
|
|
555
|
+
const fp = join(DECISIONS_DIR, f);
|
|
556
|
+
try {
|
|
557
|
+
// 检查文件行数(粗略估算是否需要彻底删)
|
|
558
|
+
const content = await readFile(fp, "utf-8");
|
|
559
|
+
const lineCount = content.split("\n").filter(Boolean).length;
|
|
560
|
+
await unlink(fp);
|
|
561
|
+
// TODO: 移除调试日志 console.log(`[route-evidence] 🧹 清理旧日志: ${f} (${lineCount} 记录)`);
|
|
562
|
+
} catch {}
|
|
563
|
+
}
|
|
564
|
+
} catch {}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ====== 查询接口 ======
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 查询路由决策历史
|
|
571
|
+
* @param {object} options - { tool, limit }
|
|
572
|
+
* @returns {Promise<Array>} 匹配的决策记录
|
|
573
|
+
*/
|
|
574
|
+
export async function queryRouteDecisions(options = {}) {
|
|
575
|
+
const { tool, strategy, limit = 20 } = options;
|
|
576
|
+
const entries = await loadRecentDecisions(200);
|
|
577
|
+
|
|
578
|
+
let filtered = entries;
|
|
579
|
+
|
|
580
|
+
if (tool) {
|
|
581
|
+
filtered = filtered.filter(e =>
|
|
582
|
+
e.decision?.recommendedTool === tool || e.decision?.decision === tool
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (strategy) {
|
|
587
|
+
filtered = filtered.filter(e => e.decision?.strategy === strategy);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return filtered.slice(0, limit).map(e => ({
|
|
591
|
+
id: e.id,
|
|
592
|
+
timestamp: e.timestamp,
|
|
593
|
+
task: (e.task || "").substring(0, 100),
|
|
594
|
+
decision: e.decision,
|
|
595
|
+
原: {
|
|
596
|
+
queueDepth: e.evidence?.原?.pool?.queueDepth,
|
|
597
|
+
memoryLevel: e.evidence?.原?.memory?.level,
|
|
598
|
+
},
|
|
599
|
+
用: e.evidence?.用,
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* 获取最新汇总报告
|
|
605
|
+
*/
|
|
606
|
+
export async function getLatestSummary() {
|
|
607
|
+
await ensureDecisionsDir();
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const files = await readdir(DECISIONS_DIR);
|
|
611
|
+
const summaryFiles = files
|
|
612
|
+
.filter(f => f.startsWith("route-summary-") && f.endsWith(".json"))
|
|
613
|
+
.sort()
|
|
614
|
+
.reverse();
|
|
615
|
+
|
|
616
|
+
if (summaryFiles.length === 0) return null;
|
|
617
|
+
|
|
618
|
+
const latest = join(DECISIONS_DIR, summaryFiles[0]);
|
|
619
|
+
const content = await readFile(latest, "utf-8");
|
|
620
|
+
return JSON.parse(content);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.error("[route-evidence] getLatestSummary read summary failed:", err?.message || err);
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* 获取整体stats(供 cpu_stats 等集成)
|
|
629
|
+
*/
|
|
630
|
+
export async function getRouteStats() {
|
|
631
|
+
await ensureDecisionsDir();
|
|
632
|
+
|
|
633
|
+
const files = await readdir(DECISIONS_DIR).catch(() => []);
|
|
634
|
+
const logFiles = files.filter(f => f.startsWith("route-decisions-") && f.endsWith(".jsonl"));
|
|
635
|
+
const summaryFiles = files.filter(f => f.startsWith("route-summary-") && f.endsWith(".json"));
|
|
636
|
+
|
|
637
|
+
// 快速估算总记录数(仅读最新文件的行数)
|
|
638
|
+
let totalApprox = 0;
|
|
639
|
+
if (logFiles.length > 0) {
|
|
640
|
+
try {
|
|
641
|
+
const latest = join(DECISIONS_DIR, logFiles.sort().reverse()[0]);
|
|
642
|
+
const content = await readFile(latest, "utf-8");
|
|
643
|
+
totalApprox += content.split("\n").filter(Boolean).length;
|
|
644
|
+
} catch {}
|
|
645
|
+
// 每个日志文件大约 SUMMARY_INTERVAL
|
|
646
|
+
totalApprox += (logFiles.length - 1) * SUMMARY_INTERVAL;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const latestSummary = await getLatestSummary();
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
totalDecisionsLogged: totalApprox,
|
|
653
|
+
logFiles: logFiles.length,
|
|
654
|
+
summaryFiles: summaryFiles.length,
|
|
655
|
+
decisionsDir: DECISIONS_DIR,
|
|
656
|
+
latestSummary,
|
|
657
|
+
threeEvidences: {
|
|
658
|
+
"本": "历史经验 - 工具调用频率/成功率/平均耗时",
|
|
659
|
+
"原": "实时状态 - Worker负载/队列深度/内存水位",
|
|
660
|
+
"用": "实践反馈 - 执行结果/耗时/优化建议",
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ====== 初始化 ======
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* 初始化决策目录并清理过期数据
|
|
669
|
+
*/
|
|
670
|
+
export async function initRouteEvidence() {
|
|
671
|
+
await ensureDecisionsDir();
|
|
672
|
+
|
|
673
|
+
// 启动时清理超 24 小时的汇总文件(保留最新2份)
|
|
674
|
+
try {
|
|
675
|
+
const files = await readdir(DECISIONS_DIR);
|
|
676
|
+
const summaryFiles = files
|
|
677
|
+
.filter(f => f.startsWith("route-summary-") && f.endsWith(".json"))
|
|
678
|
+
.sort()
|
|
679
|
+
.reverse();
|
|
680
|
+
|
|
681
|
+
if (summaryFiles.length > 2) {
|
|
682
|
+
const toDelete = summaryFiles.slice(2);
|
|
683
|
+
for (const f of toDelete) {
|
|
684
|
+
try { await unlink(join(DECISIONS_DIR, f)); } catch {}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch {}
|
|
688
|
+
|
|
689
|
+
// TODO: 移除调试日志 console.log(`[route-evidence] 🏛️ triple-evidence ready → ${DECISIONS_DIR}`);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export default {
|
|
694
|
+
recordRouteDecision,
|
|
695
|
+
update用Evidence,
|
|
696
|
+
queryRouteDecisions,
|
|
697
|
+
getLatestSummary,
|
|
698
|
+
getRouteStats,
|
|
699
|
+
generateSummary,
|
|
700
|
+
initRouteEvidence,
|
|
701
|
+
};
|