@ghenya/clinn 0.7.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/Logos/StartLogo.txt +7 -0
- package/Mem/history.js +130 -0
- package/Mem/index.js +113 -0
- package/README.md +230 -0
- package/Src/agent.js +342 -0
- package/Src/index.js +984 -0
- package/Src/llm.js +195 -0
- package/Tools/browser.js +133 -0
- package/Tools/custom/.gitkeep +0 -0
- package/Tools/edit_tools.js +93 -0
- package/Tools/extended_tools.js +408 -0
- package/Tools/file_tools.js +201 -0
- package/Tools/index.js +311 -0
- package/Tools/search_tools.js +280 -0
- package/Tools/tokenizer.js +150 -0
- package/config.json +251 -0
- package/package.json +48 -0
package/Tools/index.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fileTools = require("./file_tools");
|
|
4
|
+
const searchTools = require("./search_tools");
|
|
5
|
+
const editTools = require("./edit_tools");
|
|
6
|
+
const extendedTools = require("./extended_tools");
|
|
7
|
+
const browserTools = require("./browser");
|
|
8
|
+
const tokenizer = require("./tokenizer");
|
|
9
|
+
|
|
10
|
+
let toolRegistry = {
|
|
11
|
+
...fileTools,
|
|
12
|
+
...searchTools,
|
|
13
|
+
...editTools,
|
|
14
|
+
...extendedTools,
|
|
15
|
+
...browserTools,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let permissionCallback = null;
|
|
19
|
+
let trustedNames = new Set();
|
|
20
|
+
const CUSTOM_DIR = path.join(require("os").homedir(), ".clinn", "Tools", "custom");
|
|
21
|
+
|
|
22
|
+
function loadCustomTools() {
|
|
23
|
+
if (!fs.existsSync(CUSTOM_DIR)) return;
|
|
24
|
+
const files = fs.readdirSync(CUSTOM_DIR).filter((f) => f.endsWith(".js"));
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
try {
|
|
27
|
+
const mod = require(path.join(CUSTOM_DIR, file));
|
|
28
|
+
if (typeof mod === "object" && mod !== null) {
|
|
29
|
+
Object.assign(toolRegistry, mod);
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// skip broken custom tools
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
loadCustomTools();
|
|
38
|
+
|
|
39
|
+
function saveToolToFile(name, code) {
|
|
40
|
+
if (!fs.existsSync(CUSTOM_DIR)) fs.mkdirSync(CUSTOM_DIR, { recursive: true });
|
|
41
|
+
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) return `[失败] 工具名不合法: ${name}, 只允许字母数字下划线`;
|
|
42
|
+
|
|
43
|
+
const sanitized = code.trim();
|
|
44
|
+
if (!sanitized) return "[失败] 代码不能为空";
|
|
45
|
+
if (sanitized.length > 50000) return "[失败] 代码过长, 最大50000字符";
|
|
46
|
+
|
|
47
|
+
const filePath = path.join(CUSTOM_DIR, name + ".js");
|
|
48
|
+
fs.writeFileSync(filePath, sanitized, "utf-8");
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
delete require.cache[require.resolve(filePath)];
|
|
52
|
+
const mod = require(filePath);
|
|
53
|
+
if (typeof mod !== "object" || mod === null) {
|
|
54
|
+
fs.unlinkSync(filePath);
|
|
55
|
+
return `[失败] 模块必须导出对象, 如 module.exports = { tool_name: { name, description, parameters, execute } }`;
|
|
56
|
+
}
|
|
57
|
+
Object.assign(toolRegistry, mod);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
fs.unlinkSync(filePath);
|
|
60
|
+
return `[失败] 语法错误: ${e.message}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const keys = Object.keys(require(filePath));
|
|
64
|
+
return `[OK] 工具已持久化: ${name}.js (导出: ${keys.join(", ")}) | 路径: ${filePath}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function deleteToolFile(name) {
|
|
68
|
+
const filePath = path.join(CUSTOM_DIR, name + ".js");
|
|
69
|
+
if (!fs.existsSync(filePath)) return `[不存在] ${name}.js`;
|
|
70
|
+
if (!filePath.startsWith(CUSTOM_DIR)) return "[拒绝] 路径越界";
|
|
71
|
+
|
|
72
|
+
const keys = Object.keys(require(filePath));
|
|
73
|
+
fs.unlinkSync(filePath);
|
|
74
|
+
delete require.cache[require.resolve(filePath)];
|
|
75
|
+
|
|
76
|
+
for (const k of keys) {
|
|
77
|
+
delete toolRegistry[k];
|
|
78
|
+
trustedNames.delete(k);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `[OK] 已删除持久化工具: ${name}.js (移除: ${keys.join(", ")})`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function listCustomTools() {
|
|
85
|
+
if (!fs.existsSync(CUSTOM_DIR)) return [];
|
|
86
|
+
const files = fs.readdirSync(CUSTOM_DIR).filter((f) => f.endsWith(".js"));
|
|
87
|
+
return files.map((f) => {
|
|
88
|
+
const name = f.replace(/\.js$/, "");
|
|
89
|
+
const p = path.join(CUSTOM_DIR, f);
|
|
90
|
+
let keys = [];
|
|
91
|
+
try { keys = Object.keys(require(p)); } catch (_) {}
|
|
92
|
+
return { file: f, name, exports: keys };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setPermissionCallback(cb) { permissionCallback = cb; }
|
|
97
|
+
function setTrusted(names) { trustedNames = new Set(names); }
|
|
98
|
+
function addTrusted(name) { trustedNames.add(name); }
|
|
99
|
+
function removeTrusted(name) { trustedNames.delete(name); }
|
|
100
|
+
function loadTools() { return { ...toolRegistry }; }
|
|
101
|
+
|
|
102
|
+
function registerTool(name, tool) { toolRegistry[name] = tool; }
|
|
103
|
+
|
|
104
|
+
function unregisterTool(name) {
|
|
105
|
+
const filePath = path.join(CUSTOM_DIR, name + ".js");
|
|
106
|
+
if (fs.existsSync(filePath)) {
|
|
107
|
+
return `[提示] ${name} 是持久化工具, 请用 /tool_del_saved ${name} 或 delete_tool_file 删除`;
|
|
108
|
+
}
|
|
109
|
+
delete toolRegistry[name];
|
|
110
|
+
trustedNames.delete(name);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function searchToolRegistry(query) {
|
|
114
|
+
const q = query.toLowerCase();
|
|
115
|
+
return Object.entries(toolRegistry)
|
|
116
|
+
.filter(([name, tool]) => {
|
|
117
|
+
if (name.toLowerCase().includes(q)) return true;
|
|
118
|
+
if (tool.description && tool.description.toLowerCase().includes(q)) return true;
|
|
119
|
+
return false;
|
|
120
|
+
})
|
|
121
|
+
.map(([name, tool]) => ({ name, description: tool.description || "", parameters: tool.parameters || {} }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getTool(name) { return toolRegistry[name] || null; }
|
|
125
|
+
|
|
126
|
+
async function checkPermission(name, args) {
|
|
127
|
+
if (trustedNames.has(name)) return true;
|
|
128
|
+
if (permissionCallback) return permissionCallback(name, args);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function executeTool(name, args) {
|
|
133
|
+
const tool = toolRegistry[name];
|
|
134
|
+
if (!tool) throw new Error(`unknown tool: ${name}`);
|
|
135
|
+
if (tool.dangerous) {
|
|
136
|
+
const allowed = await checkPermission(name, args);
|
|
137
|
+
if (!allowed) throw new Error(`permission denied: ${name}`);
|
|
138
|
+
}
|
|
139
|
+
return tool.execute(args);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toFunctionDeclarations() {
|
|
143
|
+
return Object.entries(toolRegistry).map(([key, tool]) => {
|
|
144
|
+
const properties = {};
|
|
145
|
+
const required = [];
|
|
146
|
+
if (tool.parameters) {
|
|
147
|
+
for (const [pn, pd] of Object.entries(tool.parameters)) {
|
|
148
|
+
properties[pn] = { type: pd.type, description: pd.description };
|
|
149
|
+
if (pd.required) required.push(pn);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
type: "function",
|
|
154
|
+
function: {
|
|
155
|
+
name: key,
|
|
156
|
+
description: tool.description || "",
|
|
157
|
+
parameters: { type: "object", properties, required },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// ========== 动态工具过滤: 根据用户提示词正则匹配相关工具 ==========
|
|
163
|
+
|
|
164
|
+
// 预计算每个工具的关键词(启动时生成一次)
|
|
165
|
+
const _toolKeywordsCache = new Map();
|
|
166
|
+
|
|
167
|
+
function _extractKeywords(name, description) {
|
|
168
|
+
const kws = new Set();
|
|
169
|
+
for (const part of name.split("_")) {
|
|
170
|
+
if (part.length >= 2) kws.add(part.toLowerCase());
|
|
171
|
+
}
|
|
172
|
+
if (description) {
|
|
173
|
+
const tokens = tokenizer.segment(description.toLowerCase());
|
|
174
|
+
for (const t of tokens) {
|
|
175
|
+
if (t.length >= 2) kws.add(t);
|
|
176
|
+
}
|
|
177
|
+
const enMatches = description.match(/[a-zA-Z]{2,}/g);
|
|
178
|
+
if (enMatches) {
|
|
179
|
+
for (const m of enMatches) {
|
|
180
|
+
kws.add(m.toLowerCase());
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...kws];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _rebuildKeywords() {
|
|
188
|
+
_toolKeywordsCache.clear();
|
|
189
|
+
for (const [name, tool] of Object.entries(toolRegistry)) {
|
|
190
|
+
_toolKeywordsCache.set(name, _extractKeywords(name, tool.description || ""));
|
|
191
|
+
}
|
|
192
|
+
tokenizer.buildDictFromKeywords([..._toolKeywordsCache.values()]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 初始化构建
|
|
196
|
+
_rebuildKeywords();
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 根据用户提示词正则匹配动态过滤工具声明
|
|
200
|
+
* @param {string} prompt - 用户原始提示词
|
|
201
|
+
* @returns {Array} 匹配的工具 function_declarations 数组
|
|
202
|
+
*/
|
|
203
|
+
function filterToolDeclarations(prompt) {
|
|
204
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
205
|
+
return toFunctionDeclarations();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const promptLower = prompt.toLowerCase();
|
|
209
|
+
const promptTokens = tokenizer.segment(promptLower);
|
|
210
|
+
const promptTokenSet = new Set(promptTokens.map((t) => t.toLowerCase()));
|
|
211
|
+
|
|
212
|
+
const showAllTriggered = promptTokenSet.has("more") || promptTokenSet.has("全部") || promptTokenSet.has("所有") || promptLower.includes("更多工具") || promptLower.includes("show all") || promptLower.includes("all tools");
|
|
213
|
+
|
|
214
|
+
if (showAllTriggered) {
|
|
215
|
+
return toFunctionDeclarations();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const matched = new Set();
|
|
219
|
+
|
|
220
|
+
for (const [name, keywords] of _toolKeywordsCache) {
|
|
221
|
+
if (promptLower.includes(name)) {
|
|
222
|
+
matched.add(name);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
for (const kw of keywords) {
|
|
226
|
+
const kwLower = kw.toLowerCase();
|
|
227
|
+
if (promptTokenSet.has(kwLower)) {
|
|
228
|
+
matched.add(name);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (promptLower.includes(kwLower)) {
|
|
232
|
+
matched.add(name);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (matched.size === 0) {
|
|
239
|
+
return toFunctionDeclarations();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const alwaysInclude = ["search_memory", "save_memory", "list_memory", "delete_memory", "search_history", "list_history_files", "browse_page", "browse_page_text", "compress_context", "forget_conversation", "restart_session", "todo_write"];
|
|
243
|
+
for (const name of alwaysInclude) {
|
|
244
|
+
if (toolRegistry[name]) matched.add(name);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const filtered = {};
|
|
248
|
+
for (const name of matched) {
|
|
249
|
+
if (toolRegistry[name]) filtered[name] = toolRegistry[name];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return _declarationsFrom(filtered);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 从给定的工具注册表子集生成 function_declarations
|
|
257
|
+
*/
|
|
258
|
+
function _declarationsFrom(registry) {
|
|
259
|
+
return Object.entries(registry).map(([key, tool]) => {
|
|
260
|
+
const properties = {};
|
|
261
|
+
const required = [];
|
|
262
|
+
if (tool.parameters) {
|
|
263
|
+
for (const [pn, pd] of Object.entries(tool.parameters)) {
|
|
264
|
+
properties[pn] = { type: pd.type, description: pd.description };
|
|
265
|
+
if (pd.required) required.push(pn);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
type: "function",
|
|
270
|
+
function: {
|
|
271
|
+
name: key,
|
|
272
|
+
description: tool.description || "",
|
|
273
|
+
parameters: { type: "object", properties, required },
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 覆盖 registerTool / saveToolToFile / deleteToolFile 以保持关键词缓存同步
|
|
280
|
+
const _origRegisterTool = registerTool;
|
|
281
|
+
registerTool = function (name, tool) {
|
|
282
|
+
_origRegisterTool(name, tool);
|
|
283
|
+
_toolKeywordsCache.set(name, _extractKeywords(name, (tool && tool.description) || ""));
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const _origSaveToFile = saveToolToFile;
|
|
287
|
+
saveToolToFile = function (name, code) {
|
|
288
|
+
const result = _origSaveToFile(name, code);
|
|
289
|
+
if (result && result.startsWith("[OK]")) _rebuildKeywords();
|
|
290
|
+
return result;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const _origDelToolFile = deleteToolFile;
|
|
294
|
+
deleteToolFile = function (name) {
|
|
295
|
+
const result = _origDelToolFile(name);
|
|
296
|
+
if (result && result.startsWith("[OK]")) _rebuildKeywords();
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
function listToolNames() { return Object.keys(toolRegistry); }
|
|
301
|
+
|
|
302
|
+
function showAllToolDeclarations() { return toFunctionDeclarations(); }
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
loadTools, getTool, executeTool, checkPermission, toFunctionDeclarations,
|
|
306
|
+
registerTool, unregisterTool, searchToolRegistry, listToolNames,
|
|
307
|
+
setPermissionCallback, setTrusted, addTrusted, removeTrusted,
|
|
308
|
+
saveToolToFile, deleteToolFile, listCustomTools,
|
|
309
|
+
filterToolDeclarations, showAllToolDeclarations, _declarationsFrom,
|
|
310
|
+
CUSTOM_DIR,
|
|
311
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const { execSync, spawn } = require("child_process");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
const http = require("http");
|
|
4
|
+
|
|
5
|
+
const execConsoleTool = {
|
|
6
|
+
name: "exec_console",
|
|
7
|
+
description: "执行终端命令并返回结果. 始终返回: exit code + stdout + stderr + 耗时",
|
|
8
|
+
dangerous: true,
|
|
9
|
+
parameters: {
|
|
10
|
+
command: { type: "string", required: true, description: "要执行的命令" },
|
|
11
|
+
cwd: { type: "string", required: false, description: "工作目录, 默认当前目录" },
|
|
12
|
+
timeout: { type: "number", required: false, description: "超时毫秒, 默认30000" },
|
|
13
|
+
},
|
|
14
|
+
execute: async ({ command, cwd, timeout }) => {
|
|
15
|
+
const cwdPath = cwd || process.cwd();
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
try {
|
|
18
|
+
const output = execSync(command, {
|
|
19
|
+
cwd: cwdPath,
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
timeout: timeout || 30000,
|
|
22
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
23
|
+
});
|
|
24
|
+
const ms = Date.now() - start;
|
|
25
|
+
const out = output || "(无输出)";
|
|
26
|
+
return `[exit 0 | ${ms}ms | ${cwdPath}]\n${out.slice(0, 3500)}`;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
const ms = Date.now() - start;
|
|
29
|
+
const code = e.status != null ? e.status : "?";
|
|
30
|
+
const stderr = (e.stderr || e.message || "").slice(0, 1500);
|
|
31
|
+
const stdout = (e.stdout || "").slice(0, 1000);
|
|
32
|
+
let result = `[exit ${code} | ${ms}ms | ${cwdPath}]\n`;
|
|
33
|
+
if (stderr) result += `[stderr]\n${stderr}\n`;
|
|
34
|
+
if (stdout) result += `[stdout]\n${stdout}`;
|
|
35
|
+
if (!stderr && !stdout) result += `执行失败: ${e.message}`;
|
|
36
|
+
return result.slice(0, 3500);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const waitCommandTool = {
|
|
42
|
+
name: "wait_command",
|
|
43
|
+
description: "执行长时间命令并返回结果. 返回: exit code + stdout + stderr + 耗时",
|
|
44
|
+
dangerous: true,
|
|
45
|
+
parameters: {
|
|
46
|
+
command: { type: "string", required: true, description: "要执行的命令" },
|
|
47
|
+
args: { type: "string", required: false, description: "命令参数(空格分隔), 可选" },
|
|
48
|
+
cwd: { type: "string", required: false, description: "工作目录" },
|
|
49
|
+
timeout: { type: "number", required: false, description: "超时毫秒, 默认60000" },
|
|
50
|
+
},
|
|
51
|
+
execute: async ({ command, args, cwd, timeout }) => {
|
|
52
|
+
const cwdPath = cwd || process.cwd();
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const argList = args ? args.split(/\s+/) : [];
|
|
56
|
+
const child = spawn(command, argList, {
|
|
57
|
+
cwd: cwdPath,
|
|
58
|
+
shell: true,
|
|
59
|
+
});
|
|
60
|
+
let stdout = "";
|
|
61
|
+
let stderr = "";
|
|
62
|
+
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
63
|
+
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
child.kill("SIGTERM");
|
|
66
|
+
const ms = Date.now() - start;
|
|
67
|
+
resolve(`[timeout ${timeout || 60000}ms | ${ms}ms | ${cwdPath}]\n${stdout.slice(0, 1500)}${stderr ? "\n[stderr]\n" + stderr.slice(0, 500) : ""}`);
|
|
68
|
+
}, timeout || 60000);
|
|
69
|
+
child.on("close", (code) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
const ms = Date.now() - start;
|
|
72
|
+
let result = `[exit ${code} | ${ms}ms | ${cwdPath}]\n`;
|
|
73
|
+
if (stdout) result += stdout;
|
|
74
|
+
if (stderr) result += `\n[stderr]\n${stderr}`;
|
|
75
|
+
resolve(result.slice(0, 3500));
|
|
76
|
+
});
|
|
77
|
+
child.on("error", (e) => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
const ms = Date.now() - start;
|
|
80
|
+
resolve(`[error | ${ms}ms | ${cwdPath}] ${e.message}`);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const webFetchTool = {
|
|
87
|
+
name: "web_fetch",
|
|
88
|
+
description: "获取URL的网页内容, 自动回退到真实浏览器绕过反爬",
|
|
89
|
+
parameters: {
|
|
90
|
+
url: { type: "string", required: true, description: "要抓取的URL" },
|
|
91
|
+
extractMode: { type: "string", required: false, description: "text/structure/links, 默认text" },
|
|
92
|
+
},
|
|
93
|
+
execute: async ({ url, extractMode }) => {
|
|
94
|
+
const httpResult = await new Promise((resolve) => {
|
|
95
|
+
const mod = url.startsWith("https") ? https : http;
|
|
96
|
+
const start = Date.now();
|
|
97
|
+
const req = mod.get(url, { headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36" } }, (res) => {
|
|
98
|
+
const sc = res.statusCode;
|
|
99
|
+
const ms = Date.now() - start;
|
|
100
|
+
if (sc >= 300 && sc < 400 && res.headers.location) {
|
|
101
|
+
resolve({ ok: false, msg: `[HTTP ${sc} 重定向 | ${ms}ms] -> ${res.headers.location}` });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (sc === 404) { resolve({ ok: false, msg: `[HTTP 404 Not Found | ${ms}ms]` }); return; }
|
|
105
|
+
if (sc === 403) { resolve({ ok: false, msg: `[HTTP 403 Forbidden | ${ms}ms]` }); return; }
|
|
106
|
+
if (sc >= 500) { resolve({ ok: false, msg: `[HTTP ${sc} Server Error | ${ms}ms]` }); return; }
|
|
107
|
+
if (sc !== 200) { resolve({ ok: false, msg: `[HTTP ${sc} | ${ms}ms]` }); return; }
|
|
108
|
+
let data = "";
|
|
109
|
+
res.on("data", (c) => (data += c.toString()));
|
|
110
|
+
res.on("end", () => {
|
|
111
|
+
const mode = extractMode || "text";
|
|
112
|
+
const prefix = `[HTTP 200 OK | ${ms}ms]\n`;
|
|
113
|
+
if (mode === "links") {
|
|
114
|
+
const links = data.match(/href=["']([^"']+)["']/gi) || [];
|
|
115
|
+
const urls = links.map((l) => l.replace(/href=["']/i, "").replace(/["']$/, "")).filter((u) => u.length > 1);
|
|
116
|
+
resolve({ ok: true, msg: prefix + `[${urls.length} 个链接]\n${urls.slice(0, 100).join("\n")}` });
|
|
117
|
+
} else if (mode === "structure") {
|
|
118
|
+
const title = (data.match(/<title[^>]*>([^<]+)<\/title>/i) || [])[1] || "(无标题)";
|
|
119
|
+
const h1s = (data.match(/<h1[^>]*>([^<]+)<\/h1>/gi) || []).map((h) => h.replace(/<\/?h1[^>]*>/gi, "").trim());
|
|
120
|
+
const h2s = (data.match(/<h2[^>]*>([^<]+)<\/h2>/gi) || []).map((h) => h.replace(/<\/?h2[^>]*>/gi, "").trim());
|
|
121
|
+
resolve({ ok: true, msg: prefix + `标题: ${title}\n--- h1 ---\n${h1s.join("\n")}\n--- h2 ---\n${h2s.join("\n")}` });
|
|
122
|
+
} else {
|
|
123
|
+
let text = data
|
|
124
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
125
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
126
|
+
.replace(/<[^>]+>/g, " ")
|
|
127
|
+
.replace(/ /g, " ")
|
|
128
|
+
.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&")
|
|
129
|
+
.replace(/\s+/g, " ").trim();
|
|
130
|
+
resolve({ ok: true, msg: prefix + text.slice(0, 3000) });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
req.on("error", (e) => resolve({ ok: false, msg: `[抓取失败 | ${Date.now() - start}ms] ${e.message}` }));
|
|
135
|
+
req.setTimeout(15000, () => { req.destroy(); resolve({ ok: false, msg: "[超时 15s]" }); });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (httpResult.ok) return httpResult.msg;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const { browse_page: bp } = require("./browser");
|
|
142
|
+
const browserResult = await bp.execute({ url, extractLinks: extractMode === "links" });
|
|
143
|
+
return `[浏览器回退] ${httpResult.msg}\n\n${browserResult}`;
|
|
144
|
+
} catch (_) {
|
|
145
|
+
return httpResult.msg + "\n[提示] 可尝试用 browse_page 工具以真实浏览器打开";
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const setTimerTool = {
|
|
151
|
+
name: "set_timer",
|
|
152
|
+
description: "设置定时器: 在指定秒数后通知AI. AI调用后可以继续其他工作, 时间到达时会收到通知. 用于轮询等待异步任务完成",
|
|
153
|
+
parameters: {
|
|
154
|
+
seconds: { type: "number", required: true, description: "延迟秒数, 范围1-300" },
|
|
155
|
+
message: { type: "string", required: false, description: "定时触发时的提示语, 如 '检查构建结果'" },
|
|
156
|
+
},
|
|
157
|
+
execute: async ({ seconds, message }) => {
|
|
158
|
+
const s = Math.max(1, Math.min(seconds || 5, 300));
|
|
159
|
+
return `[timer set | ${s}s] ${message || "定时器已设置"}`;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const searchMemoryTool = {
|
|
164
|
+
name: "search_memory",
|
|
165
|
+
description: "搜索本地记忆条目(关键词匹配)",
|
|
166
|
+
parameters: {
|
|
167
|
+
query: { type: "string", required: true, description: "搜索关键词" },
|
|
168
|
+
limit: { type: "number", required: false, description: "返回条数, 默认5" },
|
|
169
|
+
},
|
|
170
|
+
execute: () => { return "search_memory must be injected"; },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const saveMemoryTool = {
|
|
174
|
+
name: "save_memory",
|
|
175
|
+
description: "保存一条关键信息到记忆(每条不超过200字)",
|
|
176
|
+
parameters: {
|
|
177
|
+
content: { type: "string", required: true, description: "记忆内容" },
|
|
178
|
+
tags: { type: "string", required: false, description: "标签,逗号分隔" },
|
|
179
|
+
},
|
|
180
|
+
execute: () => { return "save_memory must be injected"; },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const listMemoryTool = {
|
|
184
|
+
name: "list_memory",
|
|
185
|
+
description: "列出所有记忆条目",
|
|
186
|
+
parameters: {
|
|
187
|
+
limit: { type: "number", required: false, description: "返回条数, 默认20" },
|
|
188
|
+
},
|
|
189
|
+
execute: () => { return "list_memory must be injected"; },
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const deleteMemoryTool = {
|
|
193
|
+
name: "delete_memory",
|
|
194
|
+
description: "按ID删除一条记忆",
|
|
195
|
+
parameters: {
|
|
196
|
+
id: { type: "number", required: true, description: "记忆ID" },
|
|
197
|
+
},
|
|
198
|
+
execute: () => { return "delete_memory must be injected"; },
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const searchHistoryTool = {
|
|
202
|
+
name: "search_history",
|
|
203
|
+
description: "搜索 mem/ 文件夹中的历史对话记录, 可以找到过去的对话内容",
|
|
204
|
+
parameters: {
|
|
205
|
+
query: { type: "string", required: true, description: "搜索关键词" },
|
|
206
|
+
limit: { type: "number", required: false, description: "返回条数, 默认10" },
|
|
207
|
+
},
|
|
208
|
+
execute: () => { return "search_history must be injected"; },
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const listHistoryTool = {
|
|
212
|
+
name: "list_history_files",
|
|
213
|
+
description: "列出 mem/ 文件夹中的历史对话文件列表",
|
|
214
|
+
parameters: {},
|
|
215
|
+
execute: () => { return "list_history_files must be injected"; },
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const compressContextTool = {
|
|
219
|
+
name: "compress_context",
|
|
220
|
+
description: "压缩当前上下文: 将历史对话摘要存入记忆并清空对话历史",
|
|
221
|
+
parameters: {},
|
|
222
|
+
execute: () => { return "compress_context must be injected"; },
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const agentSelfInvokeTool = {
|
|
226
|
+
name: "agent_self_invoke",
|
|
227
|
+
description: "Agent自我递归调用: 将子任务交给另一个Clinn实例处理并返回结果",
|
|
228
|
+
dangerous: true,
|
|
229
|
+
parameters: {
|
|
230
|
+
task: { type: "string", required: true, description: "要交给子Agent的任务描述" },
|
|
231
|
+
context: { type: "string", required: false, description: "额外上下文" },
|
|
232
|
+
},
|
|
233
|
+
execute: () => { return "agent_self_invoke must be injected"; },
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const saveToolTool = {
|
|
237
|
+
name: "save_tool",
|
|
238
|
+
description: "编写并持久化一个JS工具模块. 代码必须是完整的CommonJS模块, 导出工具对象. 下次启动自动加载. 模块格式: module.exports = { tool_a: { name, description, parameters, execute: async (args) => '...' }, tool_b: { ... } }",
|
|
239
|
+
dangerous: true,
|
|
240
|
+
parameters: {
|
|
241
|
+
name: { type: "string", required: true, description: "工具文件名(不含.js), 如 'my_utils'" },
|
|
242
|
+
code: { type: "string", required: true, description: "完整的JS模块代码, CommonJS格式, 导出工具对象" },
|
|
243
|
+
},
|
|
244
|
+
execute: () => { return "save_tool must be injected"; },
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const deleteToolFileTool = {
|
|
248
|
+
name: "delete_tool_file",
|
|
249
|
+
description: "删除一个持久化工具文件并卸载其导出的所有工具",
|
|
250
|
+
dangerous: true,
|
|
251
|
+
parameters: {
|
|
252
|
+
name: { type: "string", required: true, description: "工具文件名(不含.js), 如 'my_utils'" },
|
|
253
|
+
},
|
|
254
|
+
execute: () => { return "delete_tool_file must be injected"; },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const listSavedToolsTool = {
|
|
258
|
+
name: "list_saved_tools",
|
|
259
|
+
description: "列出所有持久化保存的工具文件及其导出的工具名",
|
|
260
|
+
parameters: {},
|
|
261
|
+
execute: () => { return "list_saved_tools must be injected"; },
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
exec_console: execConsoleTool,
|
|
266
|
+
wait_command: waitCommandTool,
|
|
267
|
+
web_fetch: webFetchTool,
|
|
268
|
+
set_timer: setTimerTool,
|
|
269
|
+
search_memory: searchMemoryTool,
|
|
270
|
+
save_memory: saveMemoryTool,
|
|
271
|
+
list_memory: listMemoryTool,
|
|
272
|
+
delete_memory: deleteMemoryTool,
|
|
273
|
+
search_history: searchHistoryTool,
|
|
274
|
+
list_history_files: listHistoryTool,
|
|
275
|
+
compress_context: compressContextTool,
|
|
276
|
+
agent_self_invoke: agentSelfInvokeTool,
|
|
277
|
+
save_tool: saveToolTool,
|
|
278
|
+
delete_tool_file: deleteToolFileTool,
|
|
279
|
+
list_saved_tools: listSavedToolsTool,
|
|
280
|
+
};
|