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,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc 独立日志系统 + hippocampus集成
|
|
3
|
+
*
|
|
4
|
+
* 日志目录: plugins/sc/logs/
|
|
5
|
+
* 分文件: worker-pool.log / error.log / access.log
|
|
6
|
+
* 轮转: 单文件最大5MB, 超限自动切割, 滚动保留5个历史
|
|
7
|
+
* hippocampus: 日志缓冲后每15min写入 memory/hippocampus/logs.jsonl
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir, appendFile, stat, rename, unlink } from 'fs/promises';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
// 🔧 移除未使用的 existsSync/constants 导入
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// ====== 路径常量 ======
|
|
19
|
+
const LOG_DIR = join(__dirname, '..', 'logs');
|
|
20
|
+
const HIPPOCAMPUS_DIR = join(__dirname, '..', '..', '..', 'memory', 'hippocampus');
|
|
21
|
+
const LOGS_JSONL = join(HIPPOCAMPUS_DIR, 'logs.jsonl');
|
|
22
|
+
|
|
23
|
+
// ====== 轮转常量 ======
|
|
24
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
|
25
|
+
const MAX_LOG_FILES = 5; // 保留5个历史文件
|
|
26
|
+
|
|
27
|
+
// ====== 日志文件路径 ======
|
|
28
|
+
const LOG_FILES = {
|
|
29
|
+
worker: join(LOG_DIR, 'worker-pool.log'),
|
|
30
|
+
error: join(LOG_DIR, 'error.log'),
|
|
31
|
+
access: join(LOG_DIR, 'access.log'),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ====== hippocampus缓冲 ======
|
|
35
|
+
let hippocampusBuffer = [];
|
|
36
|
+
let hippocampusTimer = null;
|
|
37
|
+
const HIPPOCAMPUS_FLUSH_INTERVAL = 15 * 60 * 1000; // 15min
|
|
38
|
+
const MAX_BUFFER_LINES = 10000; // 最大缓冲行数,超出丢弃最旧数据,防OOM
|
|
39
|
+
|
|
40
|
+
// ====== 轮转锁:防止并发 rotateIfNeeded 的 TOCTOU 竞态 ======
|
|
41
|
+
const rotateLocks = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 获取文件粒度的互斥锁(promise-based)
|
|
45
|
+
* @param {string} key - 文件路径
|
|
46
|
+
* @returns {Promise<() => void>} 解锁函数
|
|
47
|
+
*/
|
|
48
|
+
async function acquireRotateLock(key) {
|
|
49
|
+
while (rotateLocks.has(key)) {
|
|
50
|
+
await rotateLocks.get(key);
|
|
51
|
+
}
|
|
52
|
+
let unlock;
|
|
53
|
+
const promise = new Promise(resolve => { unlock = resolve; });
|
|
54
|
+
rotateLocks.set(key, promise);
|
|
55
|
+
return unlock;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ====== 初始化标记 ======
|
|
59
|
+
let _initialized = false;
|
|
60
|
+
|
|
61
|
+
// ====== 格式化时间戳 ======
|
|
62
|
+
function ts() {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
return now.toISOString().replace('T', ' ').substring(0, 19) + '.' +
|
|
65
|
+
String(now.getMilliseconds()).padStart(3, '0');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ====== 确保日志目录存在 ======
|
|
69
|
+
async function ensureLogDir() {
|
|
70
|
+
try {
|
|
71
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.code !== 'EEXIST') throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ====== 日志轮转:检查文件大小,超限则滚动(带锁防TOCTOU竞态) ======
|
|
78
|
+
async function rotateIfNeeded(filePath) {
|
|
79
|
+
const unlock = await acquireRotateLock(filePath);
|
|
80
|
+
try {
|
|
81
|
+
let stats;
|
|
82
|
+
try {
|
|
83
|
+
stats = await stat(filePath);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// ENOENT = 文件还不存在,无需轮转
|
|
86
|
+
if (err.code === 'ENOENT') return;
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
if (stats.size < MAX_LOG_SIZE) return;
|
|
90
|
+
|
|
91
|
+
// 删除最旧的备份
|
|
92
|
+
const oldest = `${filePath}.${MAX_LOG_FILES}`;
|
|
93
|
+
try { await unlink(oldest); } catch { /* 可能不存在 */ }
|
|
94
|
+
|
|
95
|
+
// 移位:.4 → .5, .3 → .4, ..., .1 → .2
|
|
96
|
+
for (let i = MAX_LOG_FILES - 1; i >= 1; i--) {
|
|
97
|
+
const src = `${filePath}.${i}`;
|
|
98
|
+
const dst = `${filePath}.${i + 1}`;
|
|
99
|
+
try { await rename(src, dst); } catch { /* 可能不存在 */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 当前文件 → .1
|
|
103
|
+
await rename(filePath, `${filePath}.1`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// 只抛出非预期错误
|
|
106
|
+
if (err.code !== 'ENOENT') throw err;
|
|
107
|
+
} finally {
|
|
108
|
+
// 释放锁
|
|
109
|
+
rotateLocks.delete(filePath);
|
|
110
|
+
unlock();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ====== 写入一行到日志文件(含自动轮转) ======
|
|
115
|
+
async function writeLog(filePath, line) {
|
|
116
|
+
try {
|
|
117
|
+
await rotateIfNeeded(filePath);
|
|
118
|
+
await appendFile(filePath, line + '\n', 'utf-8');
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`[log-manager] ⚠️ 写入日志失败 [${filePath}]: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ====== Worker 日志 ======
|
|
125
|
+
async function logWorker(workerId, message) {
|
|
126
|
+
const line = `[${ts()}] [Worker-${workerId}] ${message}`;
|
|
127
|
+
await writeLog(LOG_FILES.worker, line);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ====== 错误日志 ======
|
|
131
|
+
async function logError(source, message, stack) {
|
|
132
|
+
const stackPart = stack ? `\n${stack}` : '';
|
|
133
|
+
const line = `[${ts()}] [${source}] ${message}${stackPart}`;
|
|
134
|
+
await writeLog(LOG_FILES.error, line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ====== 访问日志 ======
|
|
138
|
+
async function logAccess(tool, params, durationMs, resultStatus) {
|
|
139
|
+
const paramSummary = params
|
|
140
|
+
? (typeof params === 'string' ? params : JSON.stringify(params).substring(0, 200))
|
|
141
|
+
: '';
|
|
142
|
+
const line = `[${ts()}] [ACCESS] tool=${tool} duration=${durationMs}ms status=${resultStatus || 'ok'} params=${paramSummary}`;
|
|
143
|
+
await writeLog(LOG_FILES.access, line);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ====== Worker 事件日志 ======
|
|
147
|
+
async function logWorkerEvent(workerId, event, details) {
|
|
148
|
+
const line = `[${ts()}] [Worker-${workerId}] [${event}] ${details}`;
|
|
149
|
+
await writeLog(LOG_FILES.worker, line);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ====== 通用日志(供 console 拦截使用) ======
|
|
153
|
+
async function log(level, source, message) {
|
|
154
|
+
const line = `[${ts()}] [${level.toUpperCase()}] [${source}] ${message}`;
|
|
155
|
+
await writeLog(LOG_FILES.worker, line);
|
|
156
|
+
if (level === 'error' || level === 'warn') {
|
|
157
|
+
await writeLog(LOG_FILES.error, line);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ====== 将缓冲日志刷入hippocampus ======
|
|
162
|
+
async function flushToHippocampus() {
|
|
163
|
+
if (hippocampusBuffer.length === 0) return;
|
|
164
|
+
const batch = hippocampusBuffer.splice(0, hippocampusBuffer.length);
|
|
165
|
+
try {
|
|
166
|
+
const lines = batch.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
|
167
|
+
await appendFile(LOGS_JSONL, lines, 'utf-8');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`[log-manager] ⚠️ hippocampuswrite failed: ${err.message}`);
|
|
170
|
+
// 失败时回放缓冲,不丢数据
|
|
171
|
+
hippocampusBuffer.unshift(...batch);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ====== 将日志目加入hippocampus缓冲(队列模式,防OOM) ======
|
|
176
|
+
function bufferForHippocampus(entry) {
|
|
177
|
+
hippocampusBuffer.push({
|
|
178
|
+
timestamp: ts(),
|
|
179
|
+
...entry,
|
|
180
|
+
});
|
|
181
|
+
// 队列模式:超出上限则丢弃最旧数据
|
|
182
|
+
if (hippocampusBuffer.length > MAX_BUFFER_LINES) {
|
|
183
|
+
hippocampusBuffer.splice(0, hippocampusBuffer.length - MAX_BUFFER_LINES);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ====== 启动hippocampus定时刷入 ======
|
|
188
|
+
function startHippocampusFlush() {
|
|
189
|
+
if (hippocampusTimer) return;
|
|
190
|
+
hippocampusTimer = setInterval(() => {
|
|
191
|
+
flushToHippocampus().catch(err => {
|
|
192
|
+
console.error(`[log-manager] ⚠️ hippocampus定时刷入失败: ${err.message}`);
|
|
193
|
+
});
|
|
194
|
+
}, HIPPOCAMPUS_FLUSH_INTERVAL);
|
|
195
|
+
|
|
196
|
+
// 进程退出前刷一次
|
|
197
|
+
process.on('beforeExit', () => {
|
|
198
|
+
flushToHippocampus().catch(() => {});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ====== 停止hippocampus定时刷入 ======
|
|
203
|
+
function stopHippocampusFlush() {
|
|
204
|
+
if (hippocampusTimer) {
|
|
205
|
+
clearInterval(hippocampusTimer);
|
|
206
|
+
hippocampusTimer = null;
|
|
207
|
+
}
|
|
208
|
+
// 最终刷一次
|
|
209
|
+
flushToHippocampus().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ====== 初始化日志系统 ======
|
|
213
|
+
async function initLogger() {
|
|
214
|
+
if (_initialized) return getLogger();
|
|
215
|
+
_initialized = true;
|
|
216
|
+
|
|
217
|
+
await ensureLogDir();
|
|
218
|
+
startHippocampusFlush();
|
|
219
|
+
|
|
220
|
+
const initMsg = `===== sc 日志系统初始化 [${new Date().toISOString()}] =====`;
|
|
221
|
+
await writeLog(LOG_FILES.worker, initMsg);
|
|
222
|
+
await writeLog(LOG_FILES.error, initMsg);
|
|
223
|
+
await writeLog(LOG_FILES.access, initMsg);
|
|
224
|
+
|
|
225
|
+
// TODO: 移除调试日志 console.log('[sc] 📋 日志系统已初始化 (目录: logs/, 轮转: 5MB×5, hippocampus: 15min)');
|
|
226
|
+
|
|
227
|
+
return getLogger();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ====== 获取日志 API 引用 ======
|
|
231
|
+
function getLogger() {
|
|
232
|
+
return {
|
|
233
|
+
logWorker,
|
|
234
|
+
logError,
|
|
235
|
+
logAccess,
|
|
236
|
+
logWorkerEvent,
|
|
237
|
+
log,
|
|
238
|
+
flush: flushToHippocampus,
|
|
239
|
+
bufferForHippocampus,
|
|
240
|
+
stop: stopHippocampusFlush,
|
|
241
|
+
LOG_DIR,
|
|
242
|
+
initialized: _initialized,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export {
|
|
247
|
+
initLogger,
|
|
248
|
+
getLogger,
|
|
249
|
+
logWorker,
|
|
250
|
+
logError,
|
|
251
|
+
logAccess,
|
|
252
|
+
logWorkerEvent,
|
|
253
|
+
log,
|
|
254
|
+
flushToHippocampus,
|
|
255
|
+
bufferForHippocampus,
|
|
256
|
+
startHippocampusFlush,
|
|
257
|
+
stopHippocampusFlush,
|
|
258
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚡ 高优任务抢占模块 — 高优任务抢占
|
|
3
|
+
*
|
|
4
|
+
* 当所有Worker忙碌 + 队列深度 > PREEMPT_QUEUE_THRESHOLD(26)时触发preempt评估:
|
|
5
|
+
* 1. 计算每个活跃任务的优先级分 = f(紧急度 × 价值 × (1 - 运行占比))
|
|
6
|
+
* 2. 最低分任务被抢占,状态序列化到 shared/preempt/
|
|
7
|
+
* 3. 被抢任务在队列头部等待恢复(恢复到原角色的stemcell)
|
|
8
|
+
*
|
|
9
|
+
* 与 Worker 分化协作:stemcell 优先接手被抢占任务(因原Worker可能已满负荷)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { readFile, writeFile, rename, unlink, mkdir, readdir, stat } from "fs/promises";
|
|
15
|
+
import { SHARED_DIR, PREEMPT_TASK_TIMEOUT_MS, PREEMPT_MAX_AGE_MS, PREEMPT_QUEUE_THRESHOLD } from './constants.js';
|
|
16
|
+
|
|
17
|
+
const PREEMPT_DIR = join(SHARED_DIR, 'preempt');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 确保 preempt 目录存在
|
|
21
|
+
*/
|
|
22
|
+
export async function ensurePreemptDir() {
|
|
23
|
+
try { await mkdir(PREEMPT_DIR, { recursive: true }); } catch (err) {
|
|
24
|
+
console.warn(`[sc] ⚠️ mkdir PREEMPT_DIR失败: ${err?.message || '未知错误'}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 计算任务优先级分
|
|
30
|
+
* score = urgency × value × (1 - runtimeRatio)
|
|
31
|
+
* - urgency: high=1.0, normal=0.6, low=0.3
|
|
32
|
+
* - value: 任务预估值 [0.1, 1.0](未设定=0.5)
|
|
33
|
+
* - runtimeRatio: 已运行时间/超时阈值 [0, 1]
|
|
34
|
+
* 分数越低 → 越容易被preempt
|
|
35
|
+
*
|
|
36
|
+
* 🧠 [设计决策] 高优任务抢占策略:当所有Worker忙碌且队列深度>26(PREEMPT_QUEUE_THRESHOLD)时触发,
|
|
37
|
+
* 按优先级分抢占最低分任务,保持系统响应性。被抢任务序列化到shared/preempt/,
|
|
38
|
+
* 由stemcell兜底恢复。
|
|
39
|
+
* (详见 memory/known-design-decisions.md — 架构设计)
|
|
40
|
+
*
|
|
41
|
+
* @param {object} task - 任务对象
|
|
42
|
+
* @param {object} workerEntry - 正在执行该任务的Worker
|
|
43
|
+
* @param {number} taskTimeoutMs - 该任务类型的超时阈值
|
|
44
|
+
* @returns {number} 优先级分 [0, 1]
|
|
45
|
+
*/
|
|
46
|
+
function calculatePriorityScore(task, workerEntry, taskTimeoutMs) {
|
|
47
|
+
const priority = task.priority || 'normal';
|
|
48
|
+
const urgency = priority === 'high' ? 1.0 : priority === 'normal' ? 0.6 : 0.3;
|
|
49
|
+
const runtime = Date.now() - (workerEntry.currentJobStartTime || workerEntry.idleSince || Date.now());
|
|
50
|
+
const timeout = taskTimeoutMs || 60000;
|
|
51
|
+
const runtimeRatio = Math.min(1, Math.max(0, runtime / timeout));
|
|
52
|
+
const estimatedValue = task._estimatedValue ?? 0.5;
|
|
53
|
+
const finalScore = urgency * estimatedValue * (1 - runtimeRatio);
|
|
54
|
+
return finalScore;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 评估是否需要触发preempt
|
|
59
|
+
*
|
|
60
|
+
* @param {object} pool - Worker 池实例(用于 getStats)
|
|
61
|
+
* @param {object} taskQueues - {high, normal, low} 队列
|
|
62
|
+
* @param {Array} workers - Worker 列表
|
|
63
|
+
* @returns {{ shouldPreempt: boolean, target: object|null, reason: string }}
|
|
64
|
+
*/
|
|
65
|
+
export function evaluatePreemption(pool, taskQueues, workers) {
|
|
66
|
+
const stats = pool.getStats();
|
|
67
|
+
const totalQueued = (taskQueues.high?.length || 0) +
|
|
68
|
+
(taskQueues.normal?.length || 0) +
|
|
69
|
+
(taskQueues.low?.length || 0);
|
|
70
|
+
|
|
71
|
+
// 阈值检查:所有Worker忙碌 + 队列深度 >= PREEMPT_QUEUE_THRESHOLD 才触发preempt
|
|
72
|
+
if (totalQueued < PREEMPT_QUEUE_THRESHOLD) {
|
|
73
|
+
return { shouldPreempt: false, target: null, reason: `队列深度 ${totalQueued} < ${PREEMPT_QUEUE_THRESHOLD},无需preempt` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const aliveBusy = workers.filter(w => w.alive && w.busy && !w.terminating && w.currentJobId);
|
|
77
|
+
if (aliveBusy.length === 0) {
|
|
78
|
+
return { shouldPreempt: false, target: null, reason: '没有活跃的忙碌Worker' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 遍历所有活跃任务,计算最低分
|
|
82
|
+
let lowestScore = Infinity;
|
|
83
|
+
let targetTask = null;
|
|
84
|
+
let targetWorker = null;
|
|
85
|
+
|
|
86
|
+
for (const we of aliveBusy) {
|
|
87
|
+
const jobId = we.currentJobId;
|
|
88
|
+
if (!jobId) continue;
|
|
89
|
+
|
|
90
|
+
// 从 pendingJobs 中找原始任务(通过jobId)
|
|
91
|
+
const pendingJob = pool._pendingJobs?.get?.(jobId);
|
|
92
|
+
if (!pendingJob) continue;
|
|
93
|
+
|
|
94
|
+
const task = pendingJob._task || pendingJob.task;
|
|
95
|
+
if (!task) continue;
|
|
96
|
+
|
|
97
|
+
const taskType = task.type || 'default';
|
|
98
|
+
const typeConfig = pool._taskTimeoutMap?.[taskType] || pool._taskTimeoutMap?.default || {};
|
|
99
|
+
const killTimeout = typeConfig.kill || 120;
|
|
100
|
+
const taskTimeoutMs = (killTimeout || 120) * 1000;
|
|
101
|
+
|
|
102
|
+
const score = calculatePriorityScore(task, we, taskTimeoutMs);
|
|
103
|
+
if (score < lowestScore) {
|
|
104
|
+
lowestScore = score;
|
|
105
|
+
targetTask = { task, jobId, workerEntry: we, pendingJob, timeoutMs: taskTimeoutMs, score };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!targetTask) {
|
|
110
|
+
return { shouldPreempt: false, target: null, reason: '未找到可评估的活跃任务' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
shouldPreempt: true,
|
|
115
|
+
target: targetTask,
|
|
116
|
+
reason: `preempt评估完成: 最低分=${targetTask.score.toFixed(3)} (任务=${targetTask.task.type}, Worker=${targetTask.workerEntry.id})`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 序列化被抢占任务的状态到 shared/preempt/
|
|
122
|
+
*
|
|
123
|
+
* @param {object} target - evaluatePreemption 返回的 target
|
|
124
|
+
* @returns {Promise<string|null>} 状态文件路径,失败返回 null
|
|
125
|
+
*/
|
|
126
|
+
export async function savePreemptState(target) {
|
|
127
|
+
try {
|
|
128
|
+
await ensurePreemptDir();
|
|
129
|
+
const jobId = target.jobId;
|
|
130
|
+
const state = {
|
|
131
|
+
jobId,
|
|
132
|
+
task: target.task,
|
|
133
|
+
preemptedAt: Date.now(),
|
|
134
|
+
preemptedFromWorker: target.workerEntry.id,
|
|
135
|
+
workerRole: target.workerEntry.role,
|
|
136
|
+
score: target.score,
|
|
137
|
+
partialResult: target.workerEntry.partialResult || null,
|
|
138
|
+
pendingJobResolve: null, // 不序列化函数引用
|
|
139
|
+
pendingJobReject: null,
|
|
140
|
+
};
|
|
141
|
+
const fp = join(PREEMPT_DIR, `${jobId}.json`);
|
|
142
|
+
const tmp = join(PREEMPT_DIR, `${jobId}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
143
|
+
await writeFile(tmp, JSON.stringify(state, null, 2), 'utf-8');
|
|
144
|
+
await rename(tmp, fp);
|
|
145
|
+
return fp;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(`[sc] ⚠️ 保存preempt状态失败: ${err.message}`);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 读取被抢占任务的状态
|
|
154
|
+
*
|
|
155
|
+
* @param {string} jobId - 被抢占任务的 jobId
|
|
156
|
+
* @returns {Promise<object|null>} 任务状态
|
|
157
|
+
*/
|
|
158
|
+
export async function readPreemptState(jobId) {
|
|
159
|
+
try {
|
|
160
|
+
await ensurePreemptDir();
|
|
161
|
+
const fp = join(PREEMPT_DIR, `${jobId}.json`);
|
|
162
|
+
const content = await readFile(fp, 'utf-8');
|
|
163
|
+
return JSON.parse(content);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.warn(`[sc] ⚠️ 读取preempt状态失败(jobId=${jobId}): ${err?.message || '未知错误'}`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 清除指定的抢占状态文件(恢复或超时删除后调用)
|
|
172
|
+
*
|
|
173
|
+
* @param {string} jobId
|
|
174
|
+
*/
|
|
175
|
+
export async function clearPreemptState(jobId) {
|
|
176
|
+
try {
|
|
177
|
+
const fp = join(PREEMPT_DIR, `${jobId}.json`);
|
|
178
|
+
await unlink(fp).catch(() => {});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.warn(`[sc] ⚠️ 清理preempt状态失败(jobId=${jobId}): ${err?.message || '未知错误'}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 列出所有被抢占但尚未恢复的任务
|
|
186
|
+
* 会过滤掉超过 PREEMPT_MAX_AGE_MS 的失效率任务
|
|
187
|
+
*
|
|
188
|
+
* @returns {Promise<Array>} 待恢复任务列表(按被抢时间升序)
|
|
189
|
+
*/
|
|
190
|
+
export async function listPreemptedTasks() {
|
|
191
|
+
try {
|
|
192
|
+
await ensurePreemptDir();
|
|
193
|
+
const files = await readdir(PREEMPT_DIR).catch(() => []);
|
|
194
|
+
const tasks = [];
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
let cleaned = 0;
|
|
197
|
+
|
|
198
|
+
for (const f of files) {
|
|
199
|
+
if (!f.endsWith('.json') || f.endsWith('.tmp')) continue;
|
|
200
|
+
const fp = join(PREEMPT_DIR, f);
|
|
201
|
+
try {
|
|
202
|
+
const content = await readFile(fp, 'utf-8');
|
|
203
|
+
const state = JSON.parse(content);
|
|
204
|
+
const age = now - (state.preemptedAt || now);
|
|
205
|
+
if (age > PREEMPT_MAX_AGE_MS) {
|
|
206
|
+
await unlink(fp);
|
|
207
|
+
cleaned++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
tasks.push({
|
|
211
|
+
...state,
|
|
212
|
+
stateFilePath: fp,
|
|
213
|
+
ageMs: age,
|
|
214
|
+
ageSec: Math.round(age / 1000),
|
|
215
|
+
});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
// 损坏的文件也清理
|
|
218
|
+
console.warn(`[sc] ⚠️ 读取preempt文件失败 ${f}: ${err?.message || '未知错误'}`);
|
|
219
|
+
try { await unlink(fp); cleaned++; } catch (e2) {
|
|
220
|
+
console.warn(`[sc] ⚠️ 删除损坏preempt文件失败 ${f}: ${e2?.message || '未知错误'}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (cleaned > 0) {
|
|
226
|
+
// TODO: 移除调试日志 console.log(`[sc] 🧹 清理了 ${cleaned} 个过期/损坏的抢占状态文件`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 按被抢时间升序(最老的优先恢复)
|
|
230
|
+
tasks.sort((a, b) => (a.preemptedAt || 0) - (b.preemptedAt || 0));
|
|
231
|
+
return tasks;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.warn(`[sc] ⚠️ list preempted tasks failed: ${err.message}`);
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 清理超时的抢占状态文件
|
|
240
|
+
*/
|
|
241
|
+
export async function cleanupExpiredPreemptStates() {
|
|
242
|
+
try {
|
|
243
|
+
await ensurePreemptDir();
|
|
244
|
+
const files = await readdir(PREEMPT_DIR).catch(() => []);
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
let cleaned = 0;
|
|
247
|
+
for (const f of files) {
|
|
248
|
+
if (!f.endsWith('.json') || f.endsWith('.tmp')) continue;
|
|
249
|
+
const fp = join(PREEMPT_DIR, f);
|
|
250
|
+
try {
|
|
251
|
+
const content = await readFile(fp, 'utf-8');
|
|
252
|
+
const state = JSON.parse(content);
|
|
253
|
+
if (now - (state.preemptedAt || now) > PREEMPT_MAX_AGE_MS) {
|
|
254
|
+
await unlink(fp);
|
|
255
|
+
cleaned++;
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.warn(`[sc] ⚠️ 清理过期preempt文件失败 ${f}: ${err?.message || '未知错误'}`);
|
|
259
|
+
try { await unlink(fp); cleaned++; } catch (e2) {
|
|
260
|
+
console.warn(`[sc] ⚠️ 删除过期preempt文件失败 ${f}: ${e2?.message || '未知错误'}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (cleaned > 0) console.log(`[sc] 🧹 清理了 ${cleaned} 个过期抢占状态`);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.warn(`[sc] ⚠️ cleanupExpiredPreemptStates失败: ${err?.message || '未知错误'}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default {
|
|
271
|
+
ensurePreemptDir,
|
|
272
|
+
evaluatePreemption,
|
|
273
|
+
savePreemptState,
|
|
274
|
+
readPreemptState,
|
|
275
|
+
clearPreemptState,
|
|
276
|
+
listPreemptedTasks,
|
|
277
|
+
cleanupExpiredPreemptStates,
|
|
278
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc — 三维优先级计算
|
|
3
|
+
*
|
|
4
|
+
* priority = 紧急度(U) × 0.4 + 重要度(I) × 0.4 + CPU消耗(C) × 0.2
|
|
5
|
+
*
|
|
6
|
+
* 输出:0-10 的分数
|
|
7
|
+
*
|
|
8
|
+
* 参数来源(可根据需要扩展):
|
|
9
|
+
* - 紧急度 U: 用户在等回复(10) / 后台任务(1)
|
|
10
|
+
* - 重要度 I: 核心功能(10) / 锦上添花(1)
|
|
11
|
+
* - CPU消耗 C: 搜索类(8) / 代码类(2)
|
|
12
|
+
*
|
|
13
|
+
* 按任务类型自动计算默认值,同时支持手动覆盖。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 任务类型 → (紧急度, 重要度, CPU消耗) 默认映射
|
|
18
|
+
*
|
|
19
|
+
* 紧急度 U ∈ [1, 10] — 1=完全不急, 10=立刻要
|
|
20
|
+
* 重要度 I ∈ [1, 10] — 1=可做可不做, 10=核心功能
|
|
21
|
+
* CPU消耗 C ∈ [1, 10] — 1=几乎不耗CPU, 10=重度计算
|
|
22
|
+
*/
|
|
23
|
+
const TYPE_PRIORITY_DEFAULTS = {
|
|
24
|
+
// 用户交互类
|
|
25
|
+
chat: { urgency: 10, importance: 10, cpuCost: 1, label: '聊天回复' },
|
|
26
|
+
dialog: { urgency: 10, importance: 9, cpuCost: 2, label: '对话记忆检索' },
|
|
27
|
+
|
|
28
|
+
// 搜索查询类
|
|
29
|
+
search: { urgency: 7, importance: 7, cpuCost: 8, label: '搜索查询' },
|
|
30
|
+
scan: { urgency: 6, importance: 6, cpuCost: 7, label: '目录扫描' },
|
|
31
|
+
'semantic-search': { urgency: 7, importance: 7, cpuCost: 9, label: '语义搜索' },
|
|
32
|
+
'dialog-recall': { urgency: 8, importance: 7, cpuCost: 6, label: '对话日记检索' },
|
|
33
|
+
|
|
34
|
+
// 编解码类
|
|
35
|
+
code: { urgency: 7, importance: 8, cpuCost: 2, label: '编码任务' },
|
|
36
|
+
'code-edit': { urgency: 7, importance: 8, cpuCost: 3, label: '代码编辑' },
|
|
37
|
+
'code-review': { urgency: 6, importance: 7, cpuCost: 3, label: '代码审查' },
|
|
38
|
+
'bug-fix': { urgency: 8, importance: 9, cpuCost: 4, label: 'Bug修复' },
|
|
39
|
+
|
|
40
|
+
// 分析类
|
|
41
|
+
analysis: { urgency: 6, importance: 7, cpuCost: 6, label: '数据分析' },
|
|
42
|
+
research: { urgency: 6, importance: 8, cpuCost: 7, label: '深度调研' },
|
|
43
|
+
orchestrate: { urgency: 7, importance: 8, cpuCost: 5, label: '任务编排' },
|
|
44
|
+
diagnose: { urgency: 7, importance: 8, cpuCost: 5, label: '系统诊断' },
|
|
45
|
+
diff: { urgency: 5, importance: 6, cpuCost: 4, label: '差异分析' },
|
|
46
|
+
'process-log': { urgency: 6, importance: 6, cpuCost: 5, label: '日志分析' },
|
|
47
|
+
|
|
48
|
+
// 系统管理类
|
|
49
|
+
system: { urgency: 6, importance: 7, cpuCost: 3, label: '系统管理' },
|
|
50
|
+
sync: { urgency: 5, importance: 6, cpuCost: 4, label: '同步任务' },
|
|
51
|
+
stats: { urgency: 4, importance: 5, cpuCost: 2, label: '状态查询' },
|
|
52
|
+
config: { urgency: 5, importance: 6, cpuCost: 2, label: '配置管理' },
|
|
53
|
+
|
|
54
|
+
// 批量/调度类
|
|
55
|
+
batch: { urgency: 7, importance: 7, cpuCost: 8, label: '批量处理' },
|
|
56
|
+
dispatch: { urgency: 6, importance: 7, cpuCost: 4, label: '任务调度' },
|
|
57
|
+
|
|
58
|
+
// 视觉类
|
|
59
|
+
vision: { urgency: 6, importance: 7, cpuCost: 9, label: '视觉分析' },
|
|
60
|
+
'image-batch': { urgency: 6, importance: 7, cpuCost: 9, label: '图片批量分析' },
|
|
61
|
+
|
|
62
|
+
// 运维/维护类
|
|
63
|
+
maintenance: { urgency: 3, importance: 4, cpuCost: 3, label: '系统维护' },
|
|
64
|
+
cleanup: { urgency: 2, importance: 3, cpuCost: 2, label: '清理任务' },
|
|
65
|
+
learn: { urgency: 1, importance: 7, cpuCost: 6, label: '后台学习' },
|
|
66
|
+
// 🧠 设计决策:sleep-learn 已删除(管理员2026-05-31要求移除后台学习)
|
|
67
|
+
'route-audit': { urgency: 2, importance: 6, cpuCost: 4, label: '路由审计' },
|
|
68
|
+
|
|
69
|
+
// 通用兜底
|
|
70
|
+
general: { urgency: 5, importance: 5, cpuCost: 5, label: '通用任务' },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 计算三维优先级
|
|
75
|
+
*
|
|
76
|
+
* @param {string} type - 任务类型(如 'search', 'code', 'system')
|
|
77
|
+
* @param {Object} [overrides] - 手动覆盖 { urgency?, importance?, cpuCost? }
|
|
78
|
+
* @returns {Promise<number>} 0-10 的优先级分数
|
|
79
|
+
*/
|
|
80
|
+
export async function calculatePriority(type, overrides = {}) {
|
|
81
|
+
const defaults = TYPE_PRIORITY_DEFAULTS[type] || TYPE_PRIORITY_DEFAULTS.general;
|
|
82
|
+
|
|
83
|
+
const urgency = overrides?.urgency !== undefined ? overrides.urgency : defaults.urgency;
|
|
84
|
+
const importance = overrides?.importance !== undefined ? overrides.importance : defaults.importance;
|
|
85
|
+
const cpuCost = overrides?.cpuCost !== undefined ? overrides.cpuCost : defaults.cpuCost;
|
|
86
|
+
|
|
87
|
+
// 确保范围 [1, 10]
|
|
88
|
+
const u = Math.max(1, Math.min(10, Number(urgency) || 5));
|
|
89
|
+
const i = Math.max(1, Math.min(10, Number(importance) || 5));
|
|
90
|
+
const c = Math.max(1, Math.min(10, Number(cpuCost) || 5));
|
|
91
|
+
|
|
92
|
+
// priority = U × 0.4 + I × 0.4 + C × 0.2
|
|
93
|
+
const priority = Math.round((u * 0.4 + i * 0.4 + c * 0.2) * 10) / 10;
|
|
94
|
+
|
|
95
|
+
// 钳制到 [0, 10]
|
|
96
|
+
return Math.max(0, Math.min(10, priority));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 获取任务类型的默认三维参数
|
|
101
|
+
* @param {string} type
|
|
102
|
+
* @returns {Object} { urgency, importance, cpuCost, label }
|
|
103
|
+
*/
|
|
104
|
+
export function getTypeDefaults(type) {
|
|
105
|
+
return { ...(TYPE_PRIORITY_DEFAULTS[type] || TYPE_PRIORITY_DEFAULTS.general) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取所有任务类型默认参数
|
|
110
|
+
* @returns {Object}
|
|
111
|
+
*/
|
|
112
|
+
export function getAllTypeDefaults() {
|
|
113
|
+
return { ...TYPE_PRIORITY_DEFAULTS };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 获取优先级等级标签
|
|
118
|
+
* @param {number} score - 0-10
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
export function getPriorityLabel(score) {
|
|
122
|
+
if (score >= 9) return '紧急';
|
|
123
|
+
if (score >= 7) return '高优先';
|
|
124
|
+
if (score >= 5) return '中优先';
|
|
125
|
+
if (score >= 3) return '低优先';
|
|
126
|
+
return '空闲';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default {
|
|
130
|
+
calculatePriority,
|
|
131
|
+
getTypeDefaults,
|
|
132
|
+
getAllTypeDefaults,
|
|
133
|
+
getPriorityLabel,
|
|
134
|
+
};
|