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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔍 新tool auto-discover — 自动捕获外部的(非cpu_) OpenClaw 新安装工具并加入推荐
|
|
3
|
+
*
|
|
4
|
+
* 🎯 设计意图:
|
|
5
|
+
* 管理员装了新插件/新工具后,sc 不知道自己不认识它,子 agent
|
|
6
|
+
* 可能调了这个工具却得不到路由推荐。
|
|
7
|
+
* 本模块通过 after_tool_call hook 默默监视外部工具调用,
|
|
8
|
+
* 当某个外部工具被调 ≥3 次且成功率 >60% 时,自动将其加入 CORE_TOOLS 白名单,
|
|
9
|
+
* 标注为"新工具待验证"并赋予初始 confidence=0.5。
|
|
10
|
+
*
|
|
11
|
+
* 🧠 为什么用 after_tool_call 而不是 before_tool_call:
|
|
12
|
+
* before_tool_call 拦截发生在路由之前,外部工具根本不会走到那里,
|
|
13
|
+
* 只有 OpenClaw 原生已知的工具才有 before 事件。但 after_tool_call
|
|
14
|
+
* 是在 OpenClaw 原生执行完成后触发的全局钩子,不管工具是不是sc
|
|
15
|
+
* 认识的,都会触发这个事件。所以能捕获到所有外部工具调用。
|
|
16
|
+
*
|
|
17
|
+
* 🧠 为什么不用开机全量扫描:
|
|
18
|
+
* OpenClaw 工具系统没有"枚举所有已注册工具"的 API。ctx.registerTool()
|
|
19
|
+
* 只能注册新工具,没有 listAllTools() 或类似反射 API。全量扫描要
|
|
20
|
+
* 翻 node_modules 目录枚举 .js 文件然后猜是不是工具——太重、太脆弱。
|
|
21
|
+
* 监听运行时真实调用才是正确路径。
|
|
22
|
+
*
|
|
23
|
+
* v1.0 — 2026-06-01
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { join, dirname } from 'path';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
import { homedir } from 'os';
|
|
29
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
30
|
+
import { CORE_TOOLS, TASK_TIMEOUT_MAP } from './constants.js';
|
|
31
|
+
import { registerToolTier } from './steward-rules.js';
|
|
32
|
+
|
|
33
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
+
const __dirname = dirname(__filename);
|
|
35
|
+
|
|
36
|
+
// ====== persist路径 ======
|
|
37
|
+
// 🧠 设计决策:数据放在 memory/shared/tool-discover/ 而不是 lib/ 下。
|
|
38
|
+
// persist数据应该和运行时代码分离,这样升级插件时旧数据不丢失。
|
|
39
|
+
const DATA_DIR = join(homedir(), '.openclaw', 'workspace', 'memory', 'shared', 'tool-discover');
|
|
40
|
+
const DATA_FILE = join(DATA_DIR, 'discovered-tools.json');
|
|
41
|
+
|
|
42
|
+
// ====== 常量 ======
|
|
43
|
+
// 🧠 设计决策:PROMOTE_THRESHOLD=3(≥3次调用且成功率>60%才加入白名单)。
|
|
44
|
+
// 1次可能是偶然,2次也可能是试水,3次才说明稳定使用。成功率>60%防屎山工具吃资源。
|
|
45
|
+
const PROMOTE_THRESHOLD = 3; // 调用≥3次才考虑注册
|
|
46
|
+
const MIN_SUCCESS_RATE = 0.6; // 成功率>60%才推荐
|
|
47
|
+
// 🧠 设计决策:CONFIDENCE_INIT=0.5(初始置信度0.5,中等)。
|
|
48
|
+
// 不留0.0(永不推荐),不留1.0(过度自信)。0.5="新工具待验证",要靠后续反馈修正。
|
|
49
|
+
const CONFIDENCE_INIT = 0.5; // 首次推荐置信度
|
|
50
|
+
const CONFIDENCE_DECAY = 0.1; // 每次失败-0.1
|
|
51
|
+
const CONFIDENCE_BOOST = 0.05; // 每次成功+0.05
|
|
52
|
+
// 🧠 设计决策:CHECK_INTERVAL_MS=60000(60s检查一次够快了)。
|
|
53
|
+
// 工具调用是s级频率,没必要每次hook触发都扫描全量。1min扫描一次足够的。
|
|
54
|
+
const CHECK_INTERVAL_MS = 60000; // 定时注册检查间隔
|
|
55
|
+
// 🧠 设计决策:PERSIST_DELAY_MS=5000(节流5s不写盘)。
|
|
56
|
+
// 多次工具调用密集触发时,每次写盘太费I/O。5s窗口内合并为一次写盘。
|
|
57
|
+
const PERSIST_DELAY_MS = 5000; // persist节流窗口
|
|
58
|
+
|
|
59
|
+
// ====== 运行时状态 ======
|
|
60
|
+
// 🧠 设计决策:trackedTools 是 Map 而非对象。Map 遍历顺序确定,keys() 可以提前筛出
|
|
61
|
+
// 符合晋升件的候选,比 for...in 快且干净。
|
|
62
|
+
const trackedTools = new Map();
|
|
63
|
+
// trackedTools 每的结构:
|
|
64
|
+
// toolName → {
|
|
65
|
+
// callCount: number, // 总调用次数
|
|
66
|
+
// successCount: number, // 成功次数
|
|
67
|
+
// failCount: number, // 失败次数
|
|
68
|
+
// firstSeen: number, // 首次发现时间戳
|
|
69
|
+
// lastSeen: number, // 最近一次调用时间戳
|
|
70
|
+
// promoted: boolean, // 是否已注册
|
|
71
|
+
// promotedAt: number|null, // 注册时间戳
|
|
72
|
+
// confidence: number, // 推荐置信度 0.0-1.0
|
|
73
|
+
// }
|
|
74
|
+
|
|
75
|
+
let _ctx = null; // 插件上下文引用
|
|
76
|
+
let timerId = null; // 定时检查句柄
|
|
77
|
+
let _persistTimer = null; // persist节流句柄
|
|
78
|
+
let _needsPersist = false; // 是否有待写入的数据
|
|
79
|
+
let _initialized = false; // 是否已加载persist数据
|
|
80
|
+
let _discoveryCount = 0; // 累计发现新工具数(用于日志)
|
|
81
|
+
let _lastPromotedTool = ''; // 最近一次注册的工具名(用于去重日志)
|
|
82
|
+
let _persistLock = Promise.resolve(); // 🔧 Bug1: 串行化写盘 Promise 链,防并发覆盖
|
|
83
|
+
const discoveredTools = new Set(CORE_TOOLS); // 🔧 Bug3: 独立 Set,不污染 CORE_TOOLS 常量数组
|
|
84
|
+
|
|
85
|
+
// ====== 目录/文件初始化 ======
|
|
86
|
+
|
|
87
|
+
async function ensureDataDir() {
|
|
88
|
+
try {
|
|
89
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadPersistedData() {
|
|
94
|
+
try {
|
|
95
|
+
const raw = await readFile(DATA_FILE, 'utf-8');
|
|
96
|
+
const data = JSON.parse(raw);
|
|
97
|
+
if (Array.isArray(data)) {
|
|
98
|
+
for (const entry of data) {
|
|
99
|
+
if (entry.toolName) {
|
|
100
|
+
trackedTools.set(entry.toolName, {
|
|
101
|
+
callCount: entry.callCount || 0,
|
|
102
|
+
successCount: entry.successCount || 0,
|
|
103
|
+
failCount: entry.failCount || 0,
|
|
104
|
+
firstSeen: entry.firstSeen || 0,
|
|
105
|
+
lastSeen: entry.lastSeen || 0,
|
|
106
|
+
promoted: entry.promoted || false,
|
|
107
|
+
promotedAt: entry.promotedAt || null,
|
|
108
|
+
confidence: entry.confidence ?? CONFIDENCE_INIT,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// TODO: 移除调试日志 console.log(`[tool auto-discover] 📂 已加载 ${trackedTools.size} 个已追踪工具`);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// 文件不存在或格式错误 — 从头开始,正常情况
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 节流写盘:工具调用频繁触发时,合并为每5s写一次
|
|
121
|
+
* 🔧 Bug1: 使用 _persistLock Promise 链串行化,防并发覆盖
|
|
122
|
+
*/
|
|
123
|
+
function schedulePersist() {
|
|
124
|
+
_needsPersist = true;
|
|
125
|
+
if (_persistTimer) return; // 已有定时器在等
|
|
126
|
+
|
|
127
|
+
_persistTimer = setTimeout(async () => {
|
|
128
|
+
_persistTimer = null;
|
|
129
|
+
if (!_needsPersist) return;
|
|
130
|
+
_needsPersist = false;
|
|
131
|
+
// 🔧 Bug1: 通过 Promise 链串行化写盘
|
|
132
|
+
await _persist();
|
|
133
|
+
}, PERSIST_DELAY_MS);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 🔧 Bug1: 通过 Promise 链串行化的实际写盘函数
|
|
138
|
+
* 确保 schedulePersist 的定时回调和 shutdown 不会并发写同一文件
|
|
139
|
+
*/
|
|
140
|
+
async function _persist() {
|
|
141
|
+
const prevLock = _persistLock;
|
|
142
|
+
_persistLock = prevLock.then(() => _doWritePersist());
|
|
143
|
+
return _persistLock; // 🔧 BUGFIX: 返回新链而非旧Promise,确保 await 等待实际写盘完成
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 🔧 Bug1: 实际的写盘 I/O(从 _persist 中分离出来,供 Promise 链调用)
|
|
148
|
+
*/
|
|
149
|
+
async function _doWritePersist() {
|
|
150
|
+
try {
|
|
151
|
+
await ensureDataDir();
|
|
152
|
+
const data = [];
|
|
153
|
+
for (const [toolName, info] of trackedTools) {
|
|
154
|
+
if (info.callCount > 0) {
|
|
155
|
+
data.push({ toolName, ...info });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await writeFile(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.warn(`[tool auto-discover] ⚠️ persist失败: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ====== 核心逻辑 ======
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 记录一次外部工具调用(由 after_tool_call hook 触发)
|
|
168
|
+
*
|
|
169
|
+
* 🧠 只追踪非 cpu_ 的工具。cpu_ 工具已经是sc自家工具了,
|
|
170
|
+
* 不需要自己推荐自己。但注意:这个函数对 cpu_ 工具直接返回,
|
|
171
|
+
* 所以必须在 after_tool_call 里先判断再调用,避免 CPU 工具被误记录。
|
|
172
|
+
*
|
|
173
|
+
* @param {string} toolName - 被调用的工具名
|
|
174
|
+
* @param {boolean} success - 本次调用是否成功
|
|
175
|
+
* @param {number} durationMs - 调用耗时(ms)
|
|
176
|
+
*/
|
|
177
|
+
export function recordExternalToolCall(toolName, success, durationMs) {
|
|
178
|
+
if (!toolName || toolName.startsWith('cpu_')) return;
|
|
179
|
+
|
|
180
|
+
// 首次发现:创建记录
|
|
181
|
+
let info = trackedTools.get(toolName);
|
|
182
|
+
if (!info) {
|
|
183
|
+
info = {
|
|
184
|
+
callCount: 0,
|
|
185
|
+
successCount: 0,
|
|
186
|
+
failCount: 0,
|
|
187
|
+
firstSeen: Date.now(),
|
|
188
|
+
lastSeen: Date.now(),
|
|
189
|
+
promoted: false,
|
|
190
|
+
promotedAt: null,
|
|
191
|
+
confidence: CONFIDENCE_INIT,
|
|
192
|
+
};
|
|
193
|
+
trackedTools.set(toolName, info);
|
|
194
|
+
// TODO: 移除调试日志 console.log(`[tool auto-discover] 👀 新外部工具发现: ${toolName}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 更新调用stats
|
|
198
|
+
info.callCount++;
|
|
199
|
+
info.lastSeen = Date.now();
|
|
200
|
+
|
|
201
|
+
if (success) {
|
|
202
|
+
info.successCount++;
|
|
203
|
+
// 🧠 设计决策:成功+0.05 置信度,上限1.0。
|
|
204
|
+
// 加法累积不会跳跃,连续成功更多次才上得去,比较平滑。
|
|
205
|
+
info.confidence = Math.min(1.0, info.confidence + CONFIDENCE_BOOST);
|
|
206
|
+
} else {
|
|
207
|
+
info.failCount++;
|
|
208
|
+
// 🧠 设计决策:失败-0.1 置信度,下限0.0(直接排除推荐)。
|
|
209
|
+
// 失败惩罚是成功奖励的2倍——失败比成功更值得注意。
|
|
210
|
+
info.confidence = Math.max(0.0, info.confidence - CONFIDENCE_DECAY);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 自动注册检查(仅当未注册时)
|
|
214
|
+
if (!info.promoted && info.callCount >= PROMOTE_THRESHOLD) {
|
|
215
|
+
const successRate = info.callCount > 0
|
|
216
|
+
? info.successCount / info.callCount
|
|
217
|
+
: 0;
|
|
218
|
+
if (successRate >= MIN_SUCCESS_RATE) {
|
|
219
|
+
registerDiscoveredTool(toolName, info);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
schedulePersist();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 注册新发现的工具 — 给新工具配超时和管家规则,不涉及路由守卫的白名单
|
|
228
|
+
*
|
|
229
|
+
* 这个函数只负责给新工具配超时和管家规则。
|
|
230
|
+
* 白名单在 constants.js 的 CORE_TOOLS 数组里,由开发者手动维护。
|
|
231
|
+
*
|
|
232
|
+
* 🧠 做了什么:
|
|
233
|
+
* 1. discoveredTools Set 追加名字(仅模块内跟踪,不参与路由守卫)
|
|
234
|
+
* 2. TASK_TIMEOUT_MAP 加默认超时 → 超时监控
|
|
235
|
+
* 3. Steward 管家规则注册 → 安全等级 safe
|
|
236
|
+
* 4. 标记 promoted → 不再重复检查注册
|
|
237
|
+
* 5. 置信度初始0.5保持不变 → 后续靠反馈调
|
|
238
|
+
*
|
|
239
|
+
* @param {string} toolName - 被发现的工具名
|
|
240
|
+
* @param {object} info - 该工具的运行时状态对象(会被直接修改)
|
|
241
|
+
*/
|
|
242
|
+
function registerDiscoveredTool(toolName, info) {
|
|
243
|
+
// 重复保护
|
|
244
|
+
if (info.promoted) return;
|
|
245
|
+
|
|
246
|
+
// 1. 追加到白名单(使用独立 Set,不污染 CORE_TOOLS 常量数组)
|
|
247
|
+
// 🔧 Bug3: 改为 discoveredTools.add(),不污染共享常量
|
|
248
|
+
if (!discoveredTools.has(toolName)) {
|
|
249
|
+
discoveredTools.add(toolName);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 2. 添加超时配置(使用默认值,站长可以之后手动调优)
|
|
253
|
+
if (!TASK_TIMEOUT_MAP[toolName]) {
|
|
254
|
+
try {
|
|
255
|
+
TASK_TIMEOUT_MAP[toolName] = { warn: 60, kill: 120, label: '自动发现工具' };
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 3. 注册管家规则(safe 等级,放行)
|
|
260
|
+
try {
|
|
261
|
+
registerToolTier(toolName, 'safe');
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// 管家规则注册失败不阻断升白
|
|
264
|
+
console.warn(`[tool auto-discover] ⚠️ 管家规则注册失败 (${toolName}): ${err.message}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 4. 标记已晋升
|
|
268
|
+
info.promoted = true;
|
|
269
|
+
info.promotedAt = Date.now();
|
|
270
|
+
_discoveryCount++;
|
|
271
|
+
|
|
272
|
+
// 去重日志:同一个工具不刷屏
|
|
273
|
+
if (_lastPromotedTool !== toolName) {
|
|
274
|
+
_lastPromotedTool = toolName;
|
|
275
|
+
const successRate = (info.successCount / info.callCount * 100).toFixed(1);
|
|
276
|
+
// TODO: 移除调试日志
|
|
277
|
+
// console.log(
|
|
278
|
+
// `[tool auto-discover] ⭐ 新工具自动升白: ${toolName}` +
|
|
279
|
+
// ` (调用${info.callCount}次, 成功率${successRate}%, 置信度${info.confidence.toFixed(2)})`
|
|
280
|
+
// );
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 定时检查所有已追踪但未晋升的工具,看是否有满足件的
|
|
286
|
+
* (兜底检查,不依赖每次 recordExternalToolCall 触发)
|
|
287
|
+
*/
|
|
288
|
+
function periodicCheck() {
|
|
289
|
+
if (!_initialized) return;
|
|
290
|
+
|
|
291
|
+
let promoted = 0;
|
|
292
|
+
for (const [toolName, info] of trackedTools) {
|
|
293
|
+
if (info.promoted) continue;
|
|
294
|
+
if (info.callCount < PROMOTE_THRESHOLD) continue;
|
|
295
|
+
|
|
296
|
+
const successRate = info.callCount > 0
|
|
297
|
+
? info.successCount / info.callCount
|
|
298
|
+
: 0;
|
|
299
|
+
if (successRate >= MIN_SUCCESS_RATE) {
|
|
300
|
+
registerDiscoveredTool(toolName, info);
|
|
301
|
+
promoted++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (promoted > 0) {
|
|
306
|
+
schedulePersist();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ====== 工具导出:core_toolDiscover ======
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* core_toolDiscover - 查询自动发现的工具清单
|
|
314
|
+
*
|
|
315
|
+
* 管理员和子 agent 都能调这个工具,查看当前发现了哪些外部工具、
|
|
316
|
+
* 各个工具的调用stats(次数/成功率/置信度)、以及是否已升白。
|
|
317
|
+
*
|
|
318
|
+
* 参数:无或 { filter: "pending|promoted|all" }
|
|
319
|
+
* 返回:{ summary, discovered }
|
|
320
|
+
*/
|
|
321
|
+
async function toolDiscoverExecute(params) {
|
|
322
|
+
const filter = (params?.filter || 'all').toLowerCase();
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
const entries = [];
|
|
325
|
+
let pendingCount = 0;
|
|
326
|
+
let promotedCount = 0;
|
|
327
|
+
|
|
328
|
+
for (const [toolName, info] of trackedTools) {
|
|
329
|
+
if (filter === 'pending' && info.promoted) continue;
|
|
330
|
+
if (filter === 'promoted' && !info.promoted) continue;
|
|
331
|
+
|
|
332
|
+
const successRate = info.callCount > 0
|
|
333
|
+
? (info.successCount / info.callCount * 100).toFixed(1) + '%'
|
|
334
|
+
: 'N/A';
|
|
335
|
+
|
|
336
|
+
entries.push({
|
|
337
|
+
toolName,
|
|
338
|
+
callCount: info.callCount,
|
|
339
|
+
successCount: info.successCount,
|
|
340
|
+
failCount: info.failCount,
|
|
341
|
+
successRate,
|
|
342
|
+
confidence: info.confidence,
|
|
343
|
+
promoted: info.promoted,
|
|
344
|
+
promotedAt: info.promotedAt ? new Date(info.promotedAt).toISOString() : null,
|
|
345
|
+
firstSeen: info.firstSeen ? new Date(info.firstSeen).toISOString() : null,
|
|
346
|
+
lastSeen: info.lastSeen ? new Date(info.lastSeen).toISOString() : null,
|
|
347
|
+
daysSinceFirstSeen: info.firstSeen ? Math.round((now - info.firstSeen) / 86400000) : 0,
|
|
348
|
+
// 🧠 建议标签:根据置信度和天数给用户建议
|
|
349
|
+
suggestion: info.promoted ? '✅ 已纳入推荐' :
|
|
350
|
+
info.confidence < 0.3 ? '⚠️ 置信度低' :
|
|
351
|
+
info.callCount < PROMOTE_THRESHOLD ? '🔍 观察中' :
|
|
352
|
+
'⚡ 快到升白阈值',
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (info.promoted) promotedCount++;
|
|
356
|
+
else pendingCount++;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 按调用次数降序排列
|
|
360
|
+
entries.sort((a, b) => b.callCount - a.callCount);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
summary: {
|
|
364
|
+
totalTracked: trackedTools.size,
|
|
365
|
+
promotedCount,
|
|
366
|
+
pendingCount,
|
|
367
|
+
totalDiscoveries: _discoveryCount,
|
|
368
|
+
},
|
|
369
|
+
filter,
|
|
370
|
+
discovered: entries,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ====== 注册入口 ======
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 注册tool auto-discover系统
|
|
378
|
+
*
|
|
379
|
+
* 必须在 activate() 中调用,传 ctx 和 deps。
|
|
380
|
+
*
|
|
381
|
+
* @param {object} ctx - OpenClaw 插件上下文
|
|
382
|
+
* @param {object} deps - 依赖注入
|
|
383
|
+
*/
|
|
384
|
+
export async function register(ctx, deps = {}) {
|
|
385
|
+
_ctx = ctx;
|
|
386
|
+
|
|
387
|
+
// 1. 加载persist数据
|
|
388
|
+
await loadPersistedData();
|
|
389
|
+
|
|
390
|
+
// 🔧 Bug2: 重启后,对persist中已 promoted 的工具重新升白
|
|
391
|
+
// CORE_TOOLS/TASK_TIMEOUT_MAP/管家规则在内存中重建,需要从持久数据恢复
|
|
392
|
+
let rePromotedCount = 0;
|
|
393
|
+
for (const [toolName, info] of trackedTools) {
|
|
394
|
+
if (info.promoted) {
|
|
395
|
+
// 临时取消 promoted 标记以通过重复保护检查
|
|
396
|
+
info.promoted = false;
|
|
397
|
+
registerDiscoveredTool(toolName, info);
|
|
398
|
+
// registerDiscoveredTool 内部会再次将 info.promoted 设为 true
|
|
399
|
+
rePromotedCount++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (rePromotedCount > 0 && ctx.logger) {
|
|
403
|
+
ctx.logger.info(`🔍 tool auto-discover: 已恢复 ${rePromotedCount} 个已升白工具`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 🔧 Bug3: after_tool_call hook 已移除 —— 统一由 index.js 的 after_tool_call hook 调用
|
|
407
|
+
// recordExternalToolCall() 处理外部工具记录,避免两个 hook 重复执行。
|
|
408
|
+
// 导出函数由 index.js 的已有 hook 调用。
|
|
409
|
+
|
|
410
|
+
// 2. 🪄 导入历史数据:从 deps.getToolStats 读取已有的 7 天stats
|
|
411
|
+
try {
|
|
412
|
+
if (deps.getToolStats && typeof deps.getToolStats === 'function') {
|
|
413
|
+
const allStats = deps.getToolStats();
|
|
414
|
+
if (allStats && typeof allStats === 'object') {
|
|
415
|
+
let imported = 0;
|
|
416
|
+
for (const [toolName, stat] of Object.entries(allStats)) {
|
|
417
|
+
if (toolName.startsWith('cpu_')) continue;
|
|
418
|
+
if (trackedTools.has(toolName)) continue; // 已有记录,不覆盖
|
|
419
|
+
if (stat.calls < 1) continue;
|
|
420
|
+
|
|
421
|
+
// 从内部化stats导入初始数据
|
|
422
|
+
trackedTools.set(toolName, {
|
|
423
|
+
callCount: stat.calls || 0,
|
|
424
|
+
successCount: stat.successes || 0,
|
|
425
|
+
failCount: stat.failures || 0,
|
|
426
|
+
firstSeen: Date.now() - 7 * 86400000, // 保守估计7天前
|
|
427
|
+
lastSeen: Date.now(),
|
|
428
|
+
promoted: false,
|
|
429
|
+
promotedAt: null,
|
|
430
|
+
confidence: CONFIDENCE_INIT,
|
|
431
|
+
});
|
|
432
|
+
imported++;
|
|
433
|
+
|
|
434
|
+
// 导入时也检查升白件
|
|
435
|
+
const ratio = stat.calls > 0 ? stat.successes / stat.calls : 0;
|
|
436
|
+
if (ratio >= MIN_SUCCESS_RATE && stat.calls >= PROMOTE_THRESHOLD) {
|
|
437
|
+
const info = trackedTools.get(toolName);
|
|
438
|
+
registerDiscoveredTool(toolName, info);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (imported > 0) {
|
|
442
|
+
ctx.logger.info(`🔍 tool auto-discover: 已导入 ${imported} 个历史工具记录`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} catch (e) {
|
|
447
|
+
ctx.logger.warn(`🔍 tool auto-discover: 历史数据导入失败: ${e.message}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 3. 注册 core_toolDiscover 工具
|
|
451
|
+
try {
|
|
452
|
+
ctx.registerTool({
|
|
453
|
+
name: 'core_toolDiscover',
|
|
454
|
+
description: '🔍 查询自动发现的外部工具清单。返回已追踪但未升白/已升白的所有工具及其调用stats和置信度。',
|
|
455
|
+
parameters: {
|
|
456
|
+
type: 'object',
|
|
457
|
+
properties: {
|
|
458
|
+
filter: {
|
|
459
|
+
type: 'string',
|
|
460
|
+
enum: ['all', 'pending', 'promoted'],
|
|
461
|
+
description: '筛选件: all(全部), pending(待升白), promoted(已升白)',
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
required: [],
|
|
465
|
+
},
|
|
466
|
+
execute: toolDiscoverExecute,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// 追加到 CORE_TOOLS(让 before_tool_call 放行)
|
|
470
|
+
if (!CORE_TOOLS.includes('core_toolDiscover')) {
|
|
471
|
+
CORE_TOOLS.push('core_toolDiscover');
|
|
472
|
+
}
|
|
473
|
+
// 超时配置
|
|
474
|
+
if (!TASK_TIMEOUT_MAP['core_toolDiscover']) {
|
|
475
|
+
TASK_TIMEOUT_MAP['core_toolDiscover'] = { warn: 10, kill: 30, label: 'tool auto-discover' };
|
|
476
|
+
}
|
|
477
|
+
// 管家规则
|
|
478
|
+
try {
|
|
479
|
+
registerToolTier('core_toolDiscover', 'safe');
|
|
480
|
+
} catch {}
|
|
481
|
+
|
|
482
|
+
ctx.logger.info('🔍 core_toolDiscover 工具已注册');
|
|
483
|
+
} catch (e) {
|
|
484
|
+
ctx.logger.warn(`🔍 core_toolDiscover 工具注册失败: ${e.message}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 4. 启动定时检查
|
|
488
|
+
timerId = setInterval(periodicCheck, CHECK_INTERVAL_MS);
|
|
489
|
+
|
|
490
|
+
_initialized = true;
|
|
491
|
+
ctx.logger.info(`🔍 tool auto-discoverready (已追踪 ${trackedTools.size} 个工具)`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 优雅关闭:停止定时器、落盘最后数据
|
|
496
|
+
*/
|
|
497
|
+
export async function shutdown() {
|
|
498
|
+
if (timerId) {
|
|
499
|
+
clearInterval(timerId);
|
|
500
|
+
timerId = null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 🔧 Bug1: 通过 _persist() 串行化写盘,避免与 schedulePersist 冲突
|
|
504
|
+
await _persist();
|
|
505
|
+
|
|
506
|
+
_initialized = false;
|
|
507
|
+
// TODO: 移除调试日志 console.log('[tool auto-discover] 🛑 已关闭');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 获取当前已发现工具stats摘要
|
|
512
|
+
* @returns {object}
|
|
513
|
+
*/
|
|
514
|
+
export function getDiscoveredStats() {
|
|
515
|
+
return {
|
|
516
|
+
totalTracked: trackedTools.size,
|
|
517
|
+
promotedCount: [...trackedTools.values()].filter(t => t.promoted).length,
|
|
518
|
+
pendingCount: [...trackedTools.values()].filter(t => !t.promoted).length,
|
|
519
|
+
totalDiscoveries: _discoveryCount,
|
|
520
|
+
tools: [...trackedTools.keys()].sort(),
|
|
521
|
+
};
|
|
522
|
+
}
|