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,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc v5.37.0 — 对话日记检索模块 (ESM)
|
|
3
|
+
*
|
|
4
|
+
* 多Worker并行扫描 memory/dialog/ 下的日记文件,
|
|
5
|
+
* 按时间×匹配度加权排序返回结果。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
9
|
+
import { join, resolve, normalize } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
|
|
12
|
+
const OLLAMA_NATIVE_BASE = 'http://127.0.0.1:11434';
|
|
13
|
+
const EMBEDDING_TIMEOUT_MS = 10000;
|
|
14
|
+
const SEMANTIC_TOP_N = 30;
|
|
15
|
+
const FINAL_TOP_N = 20;
|
|
16
|
+
|
|
17
|
+
const WORKSPACE_DIR = resolve(homedir(), '.openclaw', 'workspace');
|
|
18
|
+
const DIALOG_DIR = join(WORKSPACE_DIR, 'memory', 'dialog');
|
|
19
|
+
|
|
20
|
+
const TIME_DECAY_HALF_LIFE_HOURS = 14;
|
|
21
|
+
const TIME_DECAY_FACTOR = Math.LN2 / TIME_DECAY_HALF_LIFE_HOURS;
|
|
22
|
+
|
|
23
|
+
const MATCH_DENSITY_WEIGHT = 10;
|
|
24
|
+
const TIME_DECAY_WEIGHT = 1.0;
|
|
25
|
+
|
|
26
|
+
const TIME_RANGE_MAP = {
|
|
27
|
+
'3d': 3 * 24 * 60 * 60 * 1000,
|
|
28
|
+
'1w': 7 * 24 * 60 * 60 * 1000,
|
|
29
|
+
'2w': 14 * 24 * 60 * 60 * 1000,
|
|
30
|
+
'1m': 30 * 24 * 60 * 60 * 1000,
|
|
31
|
+
'all': Infinity,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function readEmbeddingConfig() {
|
|
35
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
38
|
+
const cfg = JSON.parse(raw);
|
|
39
|
+
const ecfg = cfg?.models?.providers?.embedding;
|
|
40
|
+
if (ecfg?.models?.length > 0) {
|
|
41
|
+
const baseUrl = (ecfg.baseUrl || OLLAMA_NATIVE_BASE).replace(/\/v1$/, '').replace(/\/+$/, '');
|
|
42
|
+
const model = ecfg.models[0].id || 'bge-m3';
|
|
43
|
+
return { baseUrl, model };
|
|
44
|
+
}
|
|
45
|
+
const ms = cfg?.memorySearch;
|
|
46
|
+
if (ms?.model) {
|
|
47
|
+
return { baseUrl: OLLAMA_NATIVE_BASE, model: ms.model };
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn(`[dialog-recall] \u8bfb\u53d6embedding\u914d\u7f6e\u5931\u8d25: ${err?.message || '未知错误'}`);
|
|
51
|
+
}
|
|
52
|
+
return { baseUrl: OLLAMA_NATIVE_BASE, model: 'bge-m3' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getEmbedding(text, signal) {
|
|
56
|
+
const { baseUrl, model } = await readEmbeddingConfig();
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const combinedSignal = signal || controller.signal;
|
|
59
|
+
const timeoutId = setTimeout(() => controller.abort(), EMBEDDING_TIMEOUT_MS);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const url = `${baseUrl}/api/embeddings`;
|
|
63
|
+
const resp = await fetch(url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ model, prompt: text }),
|
|
67
|
+
signal: combinedSignal,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!resp.ok) {
|
|
71
|
+
throw new Error(`Ollama HTTP ${resp.status}: ${resp.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = await resp.json();
|
|
75
|
+
if (!data || !Array.isArray(data.embedding)) {
|
|
76
|
+
throw new Error('Ollama \u8fd4\u56de\u7684 embedding \u683c\u5f0f\u5f02\u5e38');
|
|
77
|
+
}
|
|
78
|
+
return data.embedding;
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cosineSimilarity(a, b) {
|
|
85
|
+
if (a.length !== b.length) return 0;
|
|
86
|
+
let dot = 0, normA = 0, normB = 0;
|
|
87
|
+
for (let i = 0; i < a.length; i++) {
|
|
88
|
+
dot += a[i] * b[i];
|
|
89
|
+
normA += a[i] * a[i];
|
|
90
|
+
normB += b[i] * b[i];
|
|
91
|
+
}
|
|
92
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
93
|
+
return denom === 0 ? 0 : dot / denom;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DATE_REGEX = /(\d{4}-\d{2}-\d{2})/;
|
|
97
|
+
|
|
98
|
+
function extractDateFromFilename(filename) {
|
|
99
|
+
const match = filename.match(DATE_REGEX);
|
|
100
|
+
return match ? match[1] : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function dateFromMtime(mtime) {
|
|
104
|
+
const y = mtime.getFullYear();
|
|
105
|
+
const m = String(mtime.getMonth() + 1).padStart(2, '0');
|
|
106
|
+
const d = String(mtime.getDate()).padStart(2, '0');
|
|
107
|
+
return `${y}-${m}-${d}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseTimeRange(timeRange) {
|
|
111
|
+
const key = (timeRange || 'all').toLowerCase();
|
|
112
|
+
const rangeMs = TIME_RANGE_MAP[key];
|
|
113
|
+
if (rangeMs === undefined) {
|
|
114
|
+
const hourMatch = key.match(/^(\d+)h$/);
|
|
115
|
+
if (hourMatch) {
|
|
116
|
+
return { cutoffMs: parseInt(hourMatch[1], 10) * 60 * 60 * 1000, label: `${hourMatch[1]}h` };
|
|
117
|
+
}
|
|
118
|
+
const dayMatch = key.match(/^(\d+)d$/);
|
|
119
|
+
if (dayMatch) {
|
|
120
|
+
return { cutoffMs: parseInt(dayMatch[1], 10) * 24 * 60 * 60 * 1000, label: `${dayMatch[1]}d` };
|
|
121
|
+
}
|
|
122
|
+
return { cutoffMs: Infinity, label: 'all' };
|
|
123
|
+
}
|
|
124
|
+
return { cutoffMs: rangeMs, label: key };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ageInHours(fileDate, now) {
|
|
128
|
+
return Math.max(0, (now.getTime() - fileDate.getTime()) / (1000 * 60 * 60));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function computeScore(ageHours, matchCount, totalLines) {
|
|
132
|
+
const timeDecay = Math.exp(-ageHours * TIME_DECAY_FACTOR);
|
|
133
|
+
const density = totalLines > 0 ? matchCount / totalLines : 0;
|
|
134
|
+
const densityBoost = 1 + density * MATCH_DENSITY_WEIGHT;
|
|
135
|
+
return Math.round(timeDecay * TIME_DECAY_WEIGHT * densityBoost * 10000) / 10000;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function scanDialogFiles(dir, cutoffMs, now, maxFiles = 5000, depth = 0) {
|
|
139
|
+
if (depth > 10) return [];
|
|
140
|
+
const results = [];
|
|
141
|
+
|
|
142
|
+
let entries;
|
|
143
|
+
try {
|
|
144
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.warn(`[dialog-recall] \u626b\u63cf\u76ee\u5f55\u5931\u8d25 ${dir}: ${err?.message || '未知错误'}`);
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (results.length >= maxFiles) break;
|
|
152
|
+
const fullPath = join(dir, entry.name);
|
|
153
|
+
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
if (entry.name.startsWith('.')) continue;
|
|
156
|
+
const subFiles = await scanDialogFiles(fullPath, cutoffMs, now, maxFiles - results.length, depth + 1);
|
|
157
|
+
results.push(...subFiles);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
162
|
+
|
|
163
|
+
let fileDateStr = extractDateFromFilename(entry.name);
|
|
164
|
+
|
|
165
|
+
let mtime;
|
|
166
|
+
try {
|
|
167
|
+
const fileStat = await stat(fullPath);
|
|
168
|
+
mtime = fileStat.mtime;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn(`[dialog-recall] \u83b7\u53d6\u6587\u4ef6\u72b6\u6001\u5931\u8d25 ${fullPath}: ${err?.message}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!fileDateStr) {
|
|
175
|
+
fileDateStr = dateFromMtime(mtime);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fileDate = new Date(fileDateStr);
|
|
179
|
+
if (isNaN(fileDate.getTime())) continue;
|
|
180
|
+
|
|
181
|
+
const ageMs = now.getTime() - fileDate.getTime();
|
|
182
|
+
if (ageMs > cutoffMs) continue;
|
|
183
|
+
|
|
184
|
+
results.push({
|
|
185
|
+
file: fullPath,
|
|
186
|
+
dateStr: fileDateStr,
|
|
187
|
+
date: fileDate,
|
|
188
|
+
mtime,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function dispatchToWorkers(fileInfos, query, withContext, pool, priority) {
|
|
196
|
+
if (fileInfos.length === 0) return [];
|
|
197
|
+
|
|
198
|
+
const CHUNK_SIZE = 50;
|
|
199
|
+
|
|
200
|
+
const chunks = [];
|
|
201
|
+
for (let i = 0; i < fileInfos.length; i += CHUNK_SIZE) {
|
|
202
|
+
const chunk = fileInfos.slice(i, i + CHUNK_SIZE);
|
|
203
|
+
chunks.push(chunk.map(f => f.file));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const workerPromises = chunks.map(fileList =>
|
|
207
|
+
pool.exec({
|
|
208
|
+
type: 'dialog-search',
|
|
209
|
+
keyword: query,
|
|
210
|
+
files: fileList,
|
|
211
|
+
withContext: !!withContext,
|
|
212
|
+
}, priority || 'high')
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const rawResults = await Promise.allSettled(workerPromises);
|
|
216
|
+
|
|
217
|
+
const allResults = [];
|
|
218
|
+
for (const r of rawResults) {
|
|
219
|
+
if (r.status === 'fulfilled' && r.value && r.value.results) {
|
|
220
|
+
allResults.push(...r.value.results);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return allResults;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function enrichAndScore(searchResults, fileInfoMap, now) {
|
|
228
|
+
return searchResults
|
|
229
|
+
.map(item => {
|
|
230
|
+
const info = fileInfoMap.get(item.file);
|
|
231
|
+
if (!info) return null;
|
|
232
|
+
const ageHrs = ageInHours(info.date, now);
|
|
233
|
+
const matchCount = item.matchCount || 0;
|
|
234
|
+
const totalLines = item.totalLines || 0;
|
|
235
|
+
const score = computeScore(ageHrs, matchCount, totalLines);
|
|
236
|
+
return {
|
|
237
|
+
file: item.file,
|
|
238
|
+
date: info.dateStr,
|
|
239
|
+
ageHours: Math.round(ageHrs * 10) / 10,
|
|
240
|
+
matchCount,
|
|
241
|
+
totalLines,
|
|
242
|
+
matchDensity: totalLines > 0 ? Math.round((matchCount / totalLines) * 10000) / 10000 : 0,
|
|
243
|
+
score,
|
|
244
|
+
matches: item.matches,
|
|
245
|
+
error: item.error,
|
|
246
|
+
};
|
|
247
|
+
})
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.sort((a, b) => b.score - a.score);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function rerankWithSemantic(enriched, query) {
|
|
253
|
+
if (enriched.length === 0) {
|
|
254
|
+
return { results: [], fallback: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const candidates = enriched.slice(0, SEMANTIC_TOP_N);
|
|
258
|
+
|
|
259
|
+
let queryVector;
|
|
260
|
+
try {
|
|
261
|
+
queryVector = await getEmbedding(query);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.warn(`[dialog-recall] Ollama embedding \u4e0d\u53ef\u7528, \u964d\u7ea7\u56de\u5173\u952e\u8bcd\u6392\u5e8f: ${err.message}`);
|
|
264
|
+
return { results: enriched.slice(0, FINAL_TOP_N), fallback: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const snippetTasks = candidates.map(async (item) => {
|
|
268
|
+
let snippetText = '';
|
|
269
|
+
if (item.matches && item.matches.length > 0) {
|
|
270
|
+
snippetText = item.matches
|
|
271
|
+
.slice(0, 3)
|
|
272
|
+
.map(m => {
|
|
273
|
+
const lines = m.context || (m.matchedLine ? [m.matchedLine] : []);
|
|
274
|
+
return lines.join(' ');
|
|
275
|
+
})
|
|
276
|
+
.join(' ')
|
|
277
|
+
.substring(0, 512);
|
|
278
|
+
}
|
|
279
|
+
if (!snippetText) {
|
|
280
|
+
snippetText = `\u5bf9\u8bdd\u6587\u4ef6 ${item.file} \u4e2d\u5173\u4e8e "${query}" \u7684\u8bb0\u5f55`;
|
|
281
|
+
}
|
|
282
|
+
return { item, snippetText };
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const snippets = await Promise.all(snippetTasks);
|
|
286
|
+
|
|
287
|
+
const embeddingResults = await Promise.allSettled(
|
|
288
|
+
snippets.map(({ snippetText }) => getEmbedding(snippetText))
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const scored = [];
|
|
292
|
+
for (let i = 0; i < snippets.length; i++) {
|
|
293
|
+
const { item } = snippets[i];
|
|
294
|
+
const embResult = embeddingResults[i];
|
|
295
|
+
let semanticScore = 0;
|
|
296
|
+
if (embResult.status === 'fulfilled') {
|
|
297
|
+
semanticScore = cosineSimilarity(queryVector, embResult.value);
|
|
298
|
+
}
|
|
299
|
+
const combinedScore = (item.score * 0.3) + (semanticScore * 0.7);
|
|
300
|
+
scored.push({
|
|
301
|
+
...item,
|
|
302
|
+
score: Math.round(combinedScore * 10000) / 10000,
|
|
303
|
+
semanticScore: Math.round(semanticScore * 10000) / 10000,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
scored.sort((a, b) => b.score - a.score);
|
|
308
|
+
|
|
309
|
+
return { results: scored.slice(0, FINAL_TOP_N), fallback: false };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function handleDialogRecall(params, pool) {
|
|
313
|
+
const query = params.query;
|
|
314
|
+
if (!query) throw new Error('[dialog-recall] missing query \u53c2\u6570');
|
|
315
|
+
|
|
316
|
+
const timeRange = params.timeRange || 'all';
|
|
317
|
+
const withContext = params.context === true;
|
|
318
|
+
|
|
319
|
+
const now = new Date();
|
|
320
|
+
const { cutoffMs, label: timeLabel } = parseTimeRange(timeRange);
|
|
321
|
+
const cutoffDate = cutoffMs === Infinity ? null : new Date(now.getTime() - cutoffMs);
|
|
322
|
+
|
|
323
|
+
const fileInfos = await scanDialogFiles(DIALOG_DIR, cutoffMs, now);
|
|
324
|
+
if (fileInfos.length === 0) {
|
|
325
|
+
return {
|
|
326
|
+
status: 'success',
|
|
327
|
+
query,
|
|
328
|
+
timeRange: timeLabel,
|
|
329
|
+
totalFiles: 0,
|
|
330
|
+
totalMatches: 0,
|
|
331
|
+
results: [],
|
|
332
|
+
message: cutoffDate
|
|
333
|
+
? `\u5728 memory/dialog/ \u4e2d\u672a\u627e\u5230 ${timeLabel} \u5185\u7684 .md \u6587\u4ef6`
|
|
334
|
+
: `\u5728 memory/dialog/ \u4e2d\u672a\u627e\u5230\u4efb\u4f55 .md \u6587\u4ef6`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const fileInfoMap = new Map();
|
|
339
|
+
for (const info of fileInfos) {
|
|
340
|
+
fileInfoMap.set(info.file, info);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const searchResults = await dispatchToWorkers(fileInfos, query, withContext, pool);
|
|
344
|
+
|
|
345
|
+
const enriched = enrichAndScore(searchResults, fileInfoMap, now);
|
|
346
|
+
|
|
347
|
+
const totalMatches = enriched.reduce((s, r) => s + r.matchCount, 0);
|
|
348
|
+
|
|
349
|
+
const mode = params.mode || 'keyword';
|
|
350
|
+
let topResults;
|
|
351
|
+
let semanticInfo = null;
|
|
352
|
+
|
|
353
|
+
if (mode === 'semantic' && enriched.length > 0) {
|
|
354
|
+
const { results, fallback } = await rerankWithSemantic(enriched, query);
|
|
355
|
+
const embCfg = await readEmbeddingConfig();
|
|
356
|
+
topResults = results;
|
|
357
|
+
semanticInfo = {
|
|
358
|
+
enabled: true,
|
|
359
|
+
model: `${embCfg.model}@${embCfg.baseUrl}`,
|
|
360
|
+
fallback: fallback,
|
|
361
|
+
candidateCount: Math.min(enriched.length, SEMANTIC_TOP_N),
|
|
362
|
+
};
|
|
363
|
+
} else {
|
|
364
|
+
topResults = enriched.slice(0, 50);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
status: 'success',
|
|
369
|
+
query,
|
|
370
|
+
mode,
|
|
371
|
+
timeRange: timeLabel,
|
|
372
|
+
totalFiles: fileInfos.length,
|
|
373
|
+
searchedFiles: fileInfos.length,
|
|
374
|
+
totalMatches,
|
|
375
|
+
topResultsCount: topResults.length,
|
|
376
|
+
semantic: semanticInfo,
|
|
377
|
+
results: topResults,
|
|
378
|
+
searchInfo: cutoffDate
|
|
379
|
+
? `\u641c\u7d22\u8303\u56f4: ${timeLabel} (${cutoffDate.toISOString().substring(0, 10)} \u81f3\u4eca)`
|
|
380
|
+
: '\u641c\u7d22\u8303\u56f4: \u5168\u90e8\u65e5\u8bb0\u6587\u4ef6',
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default {
|
|
385
|
+
handleDialogRecall,
|
|
386
|
+
scanDialogFiles,
|
|
387
|
+
parseTimeRange,
|
|
388
|
+
computeScore,
|
|
389
|
+
};
|
package/lib/env.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 env.js — 统一环境变量读取层
|
|
3
|
+
*
|
|
4
|
+
* 所有代码统一通过此模块读取环境变量,不要直接访问 process.env。
|
|
5
|
+
* 好处:
|
|
6
|
+
* 1. 可统一兜底/日志/类型转换
|
|
7
|
+
* 2. 将来可以支持 .env 文件加载
|
|
8
|
+
* 3. 测试时可 mock 这个模块
|
|
9
|
+
*
|
|
10
|
+
* ✅ 安全的环境变量(系统级):USERPROFILE, TEMP, SystemRoot, SYSTEMDRIVE, windir
|
|
11
|
+
* ⚠️ 需评审的变量(配置级):API 密钥、模型名、代理地址
|
|
12
|
+
* ❌ 敏感变量(禁止日志):含 KEY / SECRET / TOKEN / PASSWORD 的变量
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// 白名单:允许直接读取的系统级环境变量(不触发告警)
|
|
16
|
+
const SAFE_SYSTEM_VARS = new Set([
|
|
17
|
+
'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'TEMP', 'TMP',
|
|
18
|
+
'SystemRoot', 'SYSTEMDRIVE', 'windir', 'OS', 'PROCESSOR_ARCHITECTURE',
|
|
19
|
+
'COMPUTERNAME', 'USERNAME', 'LOGONSERVER',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// 敏感变量前缀(读取时不会输出到日志)
|
|
23
|
+
function isSensitive(name) {
|
|
24
|
+
const u = name.toUpperCase();
|
|
25
|
+
return /KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL/i.test(u);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 安全地读取环境变量
|
|
30
|
+
* @param {string} name - 变量名(大小写不敏感,建议大写)
|
|
31
|
+
* @param {*} defaultValue - 不存在时的兜底值
|
|
32
|
+
* @returns {string|*} 变量值或默认值
|
|
33
|
+
*/
|
|
34
|
+
export function getEnv(name, defaultValue = undefined) {
|
|
35
|
+
const value = process.env[name];
|
|
36
|
+
if (value !== undefined) return value;
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 读取布尔型环境变量('1', 'true', 'yes' → true)
|
|
42
|
+
*/
|
|
43
|
+
export function getEnvBool(name, defaultValue = false) {
|
|
44
|
+
const value = getEnv(name, null);
|
|
45
|
+
if (value === null) return defaultValue;
|
|
46
|
+
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 读取数字型环境变量
|
|
51
|
+
*/
|
|
52
|
+
export function getEnvNum(name, defaultValue = 0) {
|
|
53
|
+
const value = getEnv(name, null);
|
|
54
|
+
if (value === null) return defaultValue;
|
|
55
|
+
const num = parseInt(value, 10);
|
|
56
|
+
return isNaN(num) ? defaultValue : num;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default { getEnv, getEnvBool, getEnvNum };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc — L0-L7 复杂度分级规则 & 工具路由映射
|
|
3
|
+
*
|
|
4
|
+
* 从 workers/worker.js 和 scripts/task-router.js(已删除)提取的共享常量,
|
|
5
|
+
* 避免两份副本因维护不同步产生分歧。
|
|
6
|
+
*
|
|
7
|
+
* LEVEL_TOOL_MAP(工具路由映射)与 getLevelConfig()(子agent执行配置)不同,
|
|
8
|
+
* 前者用于 Worker 池工具路由,后者用于子 agent spawn 配置,各司其职无需一致。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ====== L0-L7 复杂度分级规则 ======
|
|
12
|
+
export const LEVEL_RULES = [
|
|
13
|
+
{ level: 'L0', weight: 1.0, patterns: [
|
|
14
|
+
/打包|压缩|复制|重命名|备份|解压|创建.*(文件|目录)/i,
|
|
15
|
+
/copy|zip|compress|backup|rename|mkdir|delete/i,
|
|
16
|
+
/移到桌面|放到桌面|存到桌面/i,
|
|
17
|
+
/\.zip|\.tar|\.gz|\.7z/,
|
|
18
|
+
]},
|
|
19
|
+
{ level: 'L1', weight: 0.9, patterns: [
|
|
20
|
+
/查.*版本|读.*文件|看.*目录|有没有|是否存在/i,
|
|
21
|
+
/多大|多长|多少行|什么时间|什么时候/i,
|
|
22
|
+
/version|exist|check|stat|list.*file|head/i,
|
|
23
|
+
/cat |head |tail |wc /i,
|
|
24
|
+
]},
|
|
25
|
+
{ level: 'L2', weight: 0.85, patterns: [
|
|
26
|
+
/搜索|查找|找到|搜一下|找一下|看看.*有没有/i,
|
|
27
|
+
/search|find|look for|get.*info|tell me about/i,
|
|
28
|
+
]},
|
|
29
|
+
{ level: 'L3', weight: 0.75, patterns: [
|
|
30
|
+
/代码审查|审查代码|review.*code|check.*code/i,
|
|
31
|
+
/简单.*分析|快速.*分析|看一下.*代码|看下.*代码/i,
|
|
32
|
+
/fix|修复|修一下/,
|
|
33
|
+
]},
|
|
34
|
+
{ level: 'L4', weight: 0.7, patterns: [
|
|
35
|
+
/分析|审查|审计|评估|比较|对比|研究|调研/i,
|
|
36
|
+
/analyze|review|audit|evaluate|compare|research/i,
|
|
37
|
+
/架构|设计|方案|优化|重构/i,
|
|
38
|
+
/architecture|design|refactor|optimize/i,
|
|
39
|
+
/performance|security|memory leak|bug/i,
|
|
40
|
+
]},
|
|
41
|
+
{ level: 'L5', weight: 0.6, decompose: true, patterns: [
|
|
42
|
+
/对比.*方案|多角度|多方面|全方位|综合/i,
|
|
43
|
+
/竞品|市场|优缺点|pros.*cons|alternative/i,
|
|
44
|
+
/compare|comprehensive|multi.*angle/i,
|
|
45
|
+
]},
|
|
46
|
+
{ level: 'L6', weight: 0.55, decompose: true, patterns: [
|
|
47
|
+
/深度.*研究|全面.*调研|系统.*分析|完整.*报告/i,
|
|
48
|
+
/deep.*research|comprehensive.*analysis|thorough/i,
|
|
49
|
+
/交叉.*验证|多方.*验证|多维.*度/i,
|
|
50
|
+
]},
|
|
51
|
+
{ level: 'L7', weight: 0.5, decompose: true, patterns: [
|
|
52
|
+
/开发.*项目|搭建.*系统|实现.*功能|从零.*构建/i,
|
|
53
|
+
/项目规划|里程碑|roadmap/i,
|
|
54
|
+
/full.*stack|complete.*system/i,
|
|
55
|
+
]},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// ====== L0-L7 级别 → 工具映射(v5.37.0: 映射到11保留工具) ======
|
|
59
|
+
export const LEVEL_TOOL_MAP = {
|
|
60
|
+
'L0': { tool: 'core_fileManager', params: { action: 'list' }, conf: 0.95 },
|
|
61
|
+
'L1': { tool: 'core_about', conf: 0.92 },
|
|
62
|
+
'L2': { tool: 'core_memorySearch', conf: 0.88 },
|
|
63
|
+
'L3': { tool: 'core_codeEditor', conf: 0.85 },
|
|
64
|
+
'L4': { tool: 'core_webSearch', conf: 0.82 },
|
|
65
|
+
'L5': { tool: 'core_taskPipeline', conf: 0.80 },
|
|
66
|
+
'L6': { tool: 'core_taskPipeline', conf: 0.78 },
|
|
67
|
+
'L7': { tool: 'core_taskPipeline', conf: 0.75 },
|
|
68
|
+
// 搜索类型路由
|
|
69
|
+
'memory_search': { tool: 'core_memorySearch', conf: 0.95 },
|
|
70
|
+
'web_search': { tool: 'core_webSearch', conf: 0.95 },
|
|
71
|
+
'dialog_search': { tool: 'core_memorySearch', conf: 0.95 },
|
|
72
|
+
};
|