memi-agent 1.0.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/README.md +90 -0
- package/memi-agent.js +1364 -0
- package/memi-client/README.md +43 -0
- package/memi-client/dist/assets/index-CmQIBT8Z.js +210 -0
- package/memi-client/dist/assets/index-Djh6rbJ-.css +1 -0
- package/memi-client/dist/index.html +13 -0
- package/memi-config/workspace/IDENTITY.md +5 -0
- package/memi-config/workspace/MEMORY.md +3 -0
- package/memi-config/workspace/SOUL.md +4 -0
- package/memi-config/workspace/TOOLS.md +3 -0
- package/memi-config/workspace/USER.md +3 -0
- package/memi-dashboard.html +359 -0
- package/memi-server/README.md +56 -0
- package/memi-server/gateway.js +532 -0
- package/memi-server/index.js +152 -0
- package/memi-server/package-lock.json +1658 -0
- package/memi-server/package.json +16 -0
- package/memi-server/routes/api.js +744 -0
- package/memi-server/utils/agent.js +1068 -0
- package/memi-server/utils/aiProxy.js +871 -0
- package/memi-server/utils/importSkill.js +74 -0
- package/package.json +27 -0
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { callImageGen } = require("./aiProxy");
|
|
5
|
+
const { importSkillFromUrl } = require("./importSkill");
|
|
6
|
+
|
|
7
|
+
const MAX_AGENT_ITERATIONS = 8;
|
|
8
|
+
|
|
9
|
+
// DeepSeek 思考模型检测
|
|
10
|
+
function supportsThinking(model) {
|
|
11
|
+
const m = (model || "").toLowerCase();
|
|
12
|
+
return /reasoner|r1|think|v4-pro|v4-flash/i.test(m);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── 工具定义 ────────────────────────────────────────
|
|
16
|
+
const TOOLS = [
|
|
17
|
+
{
|
|
18
|
+
name: "get_location",
|
|
19
|
+
description: "获取用户当前地理位置(基于IP)。返回城市、地区、国家。当用户问题涉及位置/天气/周边时自动调用。",
|
|
20
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
21
|
+
handler: async () => {
|
|
22
|
+
try {
|
|
23
|
+
const r = await axios.get("http://ip-api.com/json/?lang=zh-CN", { timeout: 8000 });
|
|
24
|
+
const d = r.data;
|
|
25
|
+
if (d && d.status === "success") return `${d.city}, ${d.regionName}, ${d.country}`;
|
|
26
|
+
return "无法获取位置";
|
|
27
|
+
} catch { return "获取位置失败"; }
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "get_time",
|
|
32
|
+
description: "获取当前日期和时间",
|
|
33
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
34
|
+
handler: async () => {
|
|
35
|
+
return new Date().toLocaleString("zh-CN");
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "calculate",
|
|
40
|
+
description: "执行数学计算。expression 是数学表达式字符串,例如 '2 + 3 * 4'",
|
|
41
|
+
parameters: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
expression: { type: "string", description: "数学表达式" },
|
|
45
|
+
},
|
|
46
|
+
required: ["expression"],
|
|
47
|
+
},
|
|
48
|
+
handler: async (args) => {
|
|
49
|
+
try {
|
|
50
|
+
// 安全沙箱:只允许数字、运算符、括号
|
|
51
|
+
const sanitized = String(args.expression).replace(/[^0-9+\-*/().%\s]/g, "");
|
|
52
|
+
if (!sanitized || sanitized.length > 200) throw new Error("非法表达式");
|
|
53
|
+
const result = Function(`"use strict"; return (${sanitized})`)();
|
|
54
|
+
return String(result);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return "计算错误: " + e.message;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "web_search",
|
|
62
|
+
description: "搜索网页信息。query 是搜索关键词,返回搜索结果摘要",
|
|
63
|
+
parameters: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
query: { type: "string", description: "搜索关键词" },
|
|
67
|
+
maxResults: { type: "number", description: "最大结果数,默认5" },
|
|
68
|
+
},
|
|
69
|
+
required: ["query"],
|
|
70
|
+
},
|
|
71
|
+
handler: async (args) => {
|
|
72
|
+
// 天气查询直接用 wttr.in
|
|
73
|
+
const q = (args.query || "").toLowerCase();
|
|
74
|
+
// 天气走 wttr.in(直接用拼音更准)
|
|
75
|
+
if (q.includes("天气") || q.includes("weather")) {
|
|
76
|
+
const cityMap = { "北京":"Beijing", "上海":"Shanghai", "广州":"Guangzhou", "深圳":"Shenzhen", "杭州":"Hangzhou", "成都":"Chengdu", "武汉":"Wuhan", "南京":"Nanjing", "南昌":"Nanchang", "长沙":"Changsha", "重庆":"Chongqing", "西安":"Xian" };
|
|
77
|
+
try {
|
|
78
|
+
let city = args.query.replace(/天气|weather|forecast|查询|最近|三天|未来|今天|明天|预报|site:|tianqi\.com|的|\d+/g, "").trim();
|
|
79
|
+
// 中文城市名映射
|
|
80
|
+
for (const [cn, en] of Object.entries(cityMap)) { if (city.includes(cn)) { city = en; break; } }
|
|
81
|
+
if (/[\u4e00-\u9fff]/.test(city)) city = "Nanchang"; // 未识别中文兜底
|
|
82
|
+
const r = await axios.get(`https://wttr.in/${encodeURIComponent(city)}?format=j1&lang=zh`, { timeout: 10000 });
|
|
83
|
+
const d = r.data;
|
|
84
|
+
if (d && d.current_condition) {
|
|
85
|
+
const w = d.current_condition[0];
|
|
86
|
+
const days = (d.weather || []).slice(0, 3).map((dy) => {
|
|
87
|
+
return `${dy.date}: ${dy.hourly?.[4]?.lang_zh?.[0]?.value || "?"} ${dy.mintempC}~${dy.maxtempC}°C`;
|
|
88
|
+
}).join("\n");
|
|
89
|
+
return `城市: ${d.nearest_area?.[0]?.areaName?.[0]?.value || "?"}\n` +
|
|
90
|
+
`当前: ${w.lang_zh?.[0]?.value || "?"} ${w.temp_C}°C, 湿度${w.humidity}%\n` +
|
|
91
|
+
`未来3天:\n${days}`;
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
// fallback: 简单格式
|
|
95
|
+
try {
|
|
96
|
+
const city = q.replace(/天气|weather|forecast|查询|最近|三天|未来|今天|明天/g, "").trim() || args.query;
|
|
97
|
+
const r = await axios.get(`https://wttr.in/${encodeURIComponent(city)}?format=4&m&lang=zh`, { timeout: 10000 });
|
|
98
|
+
if (r.data && !r.data.includes("<!DOCTYPE")) return "天气: " + r.data.trim();
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const engines = [
|
|
103
|
+
async () => {
|
|
104
|
+
const eq = encodeURIComponent(args.query || "");
|
|
105
|
+
const r = await axios.get(`https://html.duckduckgo.com/html/?q=${eq}`, {
|
|
106
|
+
headers: { "User-Agent": "Mozilla/5.0 (compatible; MemiAgent/1.0)" },
|
|
107
|
+
timeout: 8000,
|
|
108
|
+
});
|
|
109
|
+
const snRe = /<a rel="nofollow" class="result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
110
|
+
const tiRe = /<a rel="nofollow" class="result__a"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
111
|
+
const html = r.data;
|
|
112
|
+
const sn = [...html.matchAll(snRe)], ti = [...html.matchAll(tiRe)];
|
|
113
|
+
const results = [];
|
|
114
|
+
const count = Math.min(Math.max(parseInt(args.maxResults) || 5, 1), 10);
|
|
115
|
+
for (let i = 0; i < Math.min(count, ti.length); i++) {
|
|
116
|
+
const title = (ti[i]?.[1] || "").replace(/<[^>]*>/g, "").trim();
|
|
117
|
+
const snippet = (sn[i]?.[1] || "").replace(/<[^>]*>/g, "").trim();
|
|
118
|
+
if (title) results.push(`${i + 1}. ${title}\n ${snippet}`);
|
|
119
|
+
}
|
|
120
|
+
if (results.length > 0) return results.join("\n\n");
|
|
121
|
+
throw new Error("no results");
|
|
122
|
+
},
|
|
123
|
+
async () => {
|
|
124
|
+
const eq = encodeURIComponent(args.query || "");
|
|
125
|
+
const r = await axios.get(`https://www.bing.com/search?q=${eq}`, {
|
|
126
|
+
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" },
|
|
127
|
+
timeout: 15000,
|
|
128
|
+
});
|
|
129
|
+
const html = r.data;
|
|
130
|
+
const results = [];
|
|
131
|
+
// 宽松匹配中文标题和描述
|
|
132
|
+
const blockRe = /<h2[^>]*><a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a><\/h2>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/gi;
|
|
133
|
+
let m;
|
|
134
|
+
const count = Math.min(Math.max(parseInt(args.maxResults) || 5, 1), 10);
|
|
135
|
+
while ((m = blockRe.exec(html)) !== null && results.length < count) {
|
|
136
|
+
const title = m[2].replace(/<[^>]*>/g, "").trim();
|
|
137
|
+
const snippet = m[3].replace(/<[^>]*>/g, "").trim();
|
|
138
|
+
if (title) results.push(`${results.length + 1}. ${title}\n ${snippet}`);
|
|
139
|
+
}
|
|
140
|
+
if (results.length > 0) return results.join("\n\n");
|
|
141
|
+
throw new Error("no results");
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
for (const engine of engines) {
|
|
146
|
+
try { return await engine(); }
|
|
147
|
+
catch { continue; }
|
|
148
|
+
}
|
|
149
|
+
return `未找到 "${args.query}" 的搜索结果(DuckDuckGo 和 Bing 均超时)`;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "run_skill",
|
|
154
|
+
description: "执行已注册的技能。skillName 是技能名称,input 是传入技能的文本",
|
|
155
|
+
parameters: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
skillName: { type: "string", description: "技能名称" },
|
|
159
|
+
input: { type: "string", description: "传入技能的输入文本" },
|
|
160
|
+
},
|
|
161
|
+
required: ["skillName", "input"],
|
|
162
|
+
},
|
|
163
|
+
// handler 在运行时注入,因为需要访问 skills 数据
|
|
164
|
+
handler: null,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "generate_image",
|
|
168
|
+
description: "生成一张图片。prompt 是生图提示词(英文),aspectRatio 可选画幅比例 1:1/16:9/9:16/4:3,style 可选风格 Realistic/Anime/3D/Cyberpunk 等",
|
|
169
|
+
parameters: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
prompt: { type: "string", description: "图片描述提示词" },
|
|
173
|
+
aspectRatio: { type: "string", description: "画幅比例,默认 1:1" },
|
|
174
|
+
style: { type: "string", description: "风格,默认 None" },
|
|
175
|
+
},
|
|
176
|
+
required: ["prompt"],
|
|
177
|
+
},
|
|
178
|
+
// handler 在运行时注入,因为需要访问 api2 配置
|
|
179
|
+
handler: null,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "create_skill",
|
|
183
|
+
description:
|
|
184
|
+
"创建新的技能。当用户说「创建一个技能」「添加技能」时使用。\n" +
|
|
185
|
+
"参数:name(技能名称),type(llm或image),description(描述),promptTemplate(提示词模板,用{input}表示用户输入位置)",
|
|
186
|
+
parameters: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
name: { type: "string", description: "技能名称" },
|
|
190
|
+
type: { type: "string", description: "类型:llm(文本处理) 或 image(图片生成)", enum: ["llm", "image"] },
|
|
191
|
+
description: { type: "string", description: "技能描述说明" },
|
|
192
|
+
promptTemplate: { type: "string", description: "提示词模板,用 {input} 表示用户输入占位符" },
|
|
193
|
+
},
|
|
194
|
+
required: ["name", "type", "description", "promptTemplate"],
|
|
195
|
+
},
|
|
196
|
+
handler: null, // 运行时注入
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "list_files",
|
|
200
|
+
description: "列出目录内容。支持 ~/Desktop 等路径。path 是要列出的目录路径,留空则列出当前目录。",
|
|
201
|
+
parameters: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: { path: { type: "string", description: "目录路径,默认当前目录" } },
|
|
204
|
+
required: [],
|
|
205
|
+
},
|
|
206
|
+
handler: async (args) => {
|
|
207
|
+
try {
|
|
208
|
+
const fs = require("fs"), path = require("path"), os = require("os");
|
|
209
|
+
let dir = args.path || process.cwd();
|
|
210
|
+
if (dir.startsWith("~")) dir = path.join(os.homedir(), dir.slice(1));
|
|
211
|
+
// 常见目录名直接映射
|
|
212
|
+
const shortcuts = { desktop: path.join(os.homedir(), "Desktop"), documents: path.join(os.homedir(), "Documents"), downloads: path.join(os.homedir(), "Downloads") };
|
|
213
|
+
if (shortcuts[dir.toLowerCase()]) dir = shortcuts[dir.toLowerCase()];
|
|
214
|
+
const resolved = path.resolve(dir);
|
|
215
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
216
|
+
return entries.map((e) => (e.isDirectory() ? "📁 " : "📄 ") + e.name + (e.isDirectory() ? "/" : "")).join("\n") || "(空目录)";
|
|
217
|
+
} catch (e) { return "列出失败: " + e.message; }
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "read_file",
|
|
222
|
+
description: "读取文件内容。支持 ~/path 路径。path 是文件路径,maxLines 是最大读取行数(默认 200)。",
|
|
223
|
+
parameters: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
path: { type: "string", description: "文件路径" },
|
|
227
|
+
maxLines: { type: "number", description: "最大读取行数,默认 200" },
|
|
228
|
+
},
|
|
229
|
+
required: ["path"],
|
|
230
|
+
},
|
|
231
|
+
handler: async (args) => {
|
|
232
|
+
try {
|
|
233
|
+
const fs = require("fs"), path = require("path"), os = require("os");
|
|
234
|
+
let filePath = args.path;
|
|
235
|
+
if (filePath.startsWith("~")) filePath = path.join(os.homedir(), filePath.slice(1));
|
|
236
|
+
const resolved = path.resolve(filePath);
|
|
237
|
+
const content = fs.readFileSync(resolved, "utf8");
|
|
238
|
+
const lines = content.split("\n");
|
|
239
|
+
const max = Math.min(args.maxLines || 200, 500);
|
|
240
|
+
if (lines.length > max) return lines.slice(0, max).join("\n") + `\n...(共 ${lines.length} 行,已截断)`;
|
|
241
|
+
return content;
|
|
242
|
+
} catch (e) { return "读取失败: " + e.message; }
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "move_file",
|
|
247
|
+
description: "移动或重命名文件/文件夹。src 是源路径,dst 是目标路径。支持 ~。",
|
|
248
|
+
parameters: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
src: { type: "string", description: "源路径" },
|
|
252
|
+
dst: { type: "string", description: "目标路径" },
|
|
253
|
+
},
|
|
254
|
+
required: ["src", "dst"],
|
|
255
|
+
},
|
|
256
|
+
handler: async (args) => {
|
|
257
|
+
try {
|
|
258
|
+
const fs = require("fs"), path = require("path"), os = require("os");
|
|
259
|
+
let src = args.src, dst = args.dst;
|
|
260
|
+
if (src.startsWith("~")) src = path.join(os.homedir(), src.slice(1));
|
|
261
|
+
if (dst.startsWith("~")) dst = path.join(os.homedir(), dst.slice(1));
|
|
262
|
+
const dir = path.dirname(path.resolve(dst));
|
|
263
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
264
|
+
fs.renameSync(path.resolve(src), path.resolve(dst));
|
|
265
|
+
return `已移动: ${src} → ${dst}`;
|
|
266
|
+
} catch (e) { return "移动失败: " + e.message; }
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "delete_file",
|
|
271
|
+
description: "删除文件或空文件夹。path 是路径。",
|
|
272
|
+
parameters: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: { path: { type: "string", description: "文件路径" } },
|
|
275
|
+
required: ["path"],
|
|
276
|
+
},
|
|
277
|
+
handler: async (args) => {
|
|
278
|
+
try {
|
|
279
|
+
const fs = require("fs"), path = require("path"), os = require("os");
|
|
280
|
+
let p = args.path;
|
|
281
|
+
if (p.startsWith("~")) p = path.join(os.homedir(), p.slice(1));
|
|
282
|
+
const resolved = path.resolve(p);
|
|
283
|
+
const stat = fs.statSync(resolved);
|
|
284
|
+
if (stat.isDirectory()) fs.rmSync(resolved, { recursive: true, force: true });
|
|
285
|
+
else fs.unlinkSync(resolved);
|
|
286
|
+
return `已删除: ${p}`;
|
|
287
|
+
} catch (e) { return "删除失败: " + e.message; }
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "create_dir",
|
|
292
|
+
description: "创建目录(含父目录)。path 是目录路径。",
|
|
293
|
+
parameters: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: { path: { type: "string", description: "目录路径" } },
|
|
296
|
+
required: ["path"],
|
|
297
|
+
},
|
|
298
|
+
handler: async (args) => {
|
|
299
|
+
try {
|
|
300
|
+
const fs = require("fs"), path = require("path"), os = require("os");
|
|
301
|
+
let p = args.path;
|
|
302
|
+
if (p.startsWith("~")) p = path.join(os.homedir(), p.slice(1));
|
|
303
|
+
fs.mkdirSync(path.resolve(p), { recursive: true });
|
|
304
|
+
return `已创建: ${p}`;
|
|
305
|
+
} catch (e) { return "创建失败: " + e.message; }
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "run_command",
|
|
310
|
+
description: "执行任意命令:python/node脚本、dir/ls、move/copy、pip install 等。写完脚本直接用它执行,不要叫用户手动运行。Windows cmd /c。",
|
|
311
|
+
parameters: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
command: { type: "string", description: "要执行的命令" },
|
|
315
|
+
},
|
|
316
|
+
required: ["command"],
|
|
317
|
+
},
|
|
318
|
+
handler: async (args) => {
|
|
319
|
+
try {
|
|
320
|
+
const { execSync } = require("child_process");
|
|
321
|
+
const shell = process.platform === "win32";
|
|
322
|
+
const result = shell
|
|
323
|
+
? execSync(args.command, { timeout: 30000, encoding: "utf8", maxBuffer: 1024 * 1024, shell: true })
|
|
324
|
+
: execSync(args.command, { timeout: 30000, encoding: "utf8", maxBuffer: 1024 * 1024, shell: "/bin/bash" });
|
|
325
|
+
return result.slice(0, 2000) || "(命令执行成功,无输出)";
|
|
326
|
+
} catch (e) { return "命令执行失败: " + (e.stderr || e.message).slice(0, 500); }
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{ name: "find_files", description: "搜索文件名。pattern 是文件名关键词或通配符(如 *.js),dir 是要搜索的目录(默认当前目录)。",
|
|
330
|
+
parameters: { type:"object", properties:{ pattern:{type:"string",description:"文件名关键词"}, dir:{type:"string",description:"搜索目录"} }, required:["pattern"] },
|
|
331
|
+
handler: async (args) => {
|
|
332
|
+
try { const { execSync } = require("child_process"); const d = args.dir || ".";
|
|
333
|
+
const cmd = process.platform==="win32" ? `dir /s /b "${d}\\*${args.pattern}*" 2>nul` : `find "${d}" -name "*${args.pattern}*" -type f 2>/dev/null`;
|
|
334
|
+
const r = execSync(cmd,{timeout:10000,encoding:"utf8",shell:true}); return r.slice(0,2000)||"未找到"; }
|
|
335
|
+
catch { return "未找到匹配文件"; }
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{ name: "search_in_files", description: "在文件内容中搜索文本(grep)。pattern 是搜索关键词,dir 是搜索目录(默认当前目录)。",
|
|
339
|
+
parameters: { type:"object", properties:{ pattern:{type:"string",description:"搜索关键词"}, dir:{type:"string",description:"搜索目录"} }, required:["pattern"] },
|
|
340
|
+
handler: async (args) => {
|
|
341
|
+
try { const { execSync } = require("child_process"); const d = args.dir || ".";
|
|
342
|
+
const cmd = process.platform==="win32" ? `findstr /s /i /m /c:"${args.pattern}" "${d}\\*" 2>nul` : `grep -rl "${args.pattern}" "${d}" 2>/dev/null`;
|
|
343
|
+
const r = execSync(cmd,{timeout:15000,encoding:"utf8",shell:true}); return r.slice(0,2000)||"未找到"; }
|
|
344
|
+
catch { return "未找到匹配内容"; }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{ name: "http_request", description: "发起 HTTP 请求。url 是请求地址,method 默认 GET,headers 和 body 可选。",
|
|
348
|
+
parameters: { type:"object", properties:{ url:{type:"string",description:"请求URL"}, method:{type:"string",description:"GET/POST/PUT/DELETE"}, headers:{type:"string",description:"JSON格式请求头"}, body:{type:"string",description:"请求体"} }, required:["url"] },
|
|
349
|
+
handler: async (args) => {
|
|
350
|
+
try { const r = await axios({ url:args.url, method:args.method||"GET", headers:args.headers?JSON.parse(args.headers):{}, data:args.body||undefined, timeout:15000 });
|
|
351
|
+
return JSON.stringify({ status:r.status, data:String(r.data).slice(0,1500) }); }
|
|
352
|
+
catch(e) { return "HTTP请求失败: "+(e.response?.status||e.message); }
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{ name: "system_info", description: "获取系统信息(CPU、内存、磁盘、操作系统等)。无参数。",
|
|
356
|
+
parameters: { type:"object", properties:{}, required:[] },
|
|
357
|
+
handler: async () => {
|
|
358
|
+
const os = require("os");
|
|
359
|
+
return `OS: ${os.platform()} ${os.release()}\nCPU: ${os.cpus().length} cores\nMemory: ${(os.totalmem()/1e9).toFixed(1)}GB / ${(os.freemem()/1e9).toFixed(1)}GB free\nHostname: ${os.hostname()}\nUptime: ${Math.floor(os.uptime()/3600)}h`;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{ name: "clipboard_read", description: "读取系统剪贴板内容。无参数。",
|
|
363
|
+
parameters: { type:"object", properties:{}, required:[] },
|
|
364
|
+
handler: async () => {
|
|
365
|
+
try { const { execSync } = require("child_process");
|
|
366
|
+
const cmd = process.platform==="win32" ? "powershell -command Get-Clipboard" : process.platform==="darwin" ? "pbpaste" : "xclip -o";
|
|
367
|
+
return execSync(cmd,{timeout:5000,encoding:"utf8"}).slice(0,2000)||"(剪贴板为空)"; }
|
|
368
|
+
catch { return "无法读取剪贴板"; }
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
{ name: "clipboard_write", description: "写入文本到系统剪贴板。text 是要写入的内容。",
|
|
372
|
+
parameters: { type:"object", properties:{ text:{type:"string",description:"写入内容"} }, required:["text"] },
|
|
373
|
+
handler: async (args) => {
|
|
374
|
+
try { const { execSync } = require("child_process");
|
|
375
|
+
const cmd = process.platform==="win32" ? `echo ${args.text} | clip` : process.platform==="darwin" ? `echo "${args.text}" | pbcopy` : `echo "${args.text}" | xclip -selection c`;
|
|
376
|
+
execSync(cmd,{timeout:5000,shell:true}); return "已写入剪贴板"; }
|
|
377
|
+
catch { return "写入剪贴板失败"; }
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{ name: "open_url", description: "在默认浏览器中打开 URL。url 是要打开的网址。",
|
|
381
|
+
parameters: { type:"object", properties:{ url:{type:"string",description:"网址"} }, required:["url"] },
|
|
382
|
+
handler: async (args) => {
|
|
383
|
+
try { const { execSync } = require("child_process");
|
|
384
|
+
const cmd = process.platform==="win32" ? `start ${args.url}` : process.platform==="darwin" ? `open "${args.url}"` : `xdg-open "${args.url}"`;
|
|
385
|
+
execSync(cmd,{timeout:5000,shell:true}); return `已打开: ${args.url}`; }
|
|
386
|
+
catch { return "打开失败"; }
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{ name: "download_file", description: "下载文件。url 是下载地址,savePath 是保存路径(默认当前目录)。",
|
|
390
|
+
parameters: { type:"object", properties:{ url:{type:"string",description:"下载地址"}, savePath:{type:"string",description:"保存路径"} }, required:["url"] },
|
|
391
|
+
handler: async (args) => {
|
|
392
|
+
try { const r = await axios.get(args.url,{responseType:"arraybuffer",timeout:60000});
|
|
393
|
+
const p = args.savePath || path.basename(new URL(args.url).pathname) || "download";
|
|
394
|
+
fs.writeFileSync(p, Buffer.from(r.data)); return `已下载: ${p} (${r.data.length} bytes)`; }
|
|
395
|
+
catch(e) { return "下载失败: "+e.message; }
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{ name: "zip_files", description: "压缩文件/目录。source 是源路径,dest 是目标 zip 文件路径。",
|
|
399
|
+
parameters: { type:"object", properties:{ source:{type:"string",description:"源文件/目录"}, dest:{type:"string",description:"目标zip路径"} }, required:["source","dest"] },
|
|
400
|
+
handler: async (args) => {
|
|
401
|
+
try { const { execSync } = require("child_process");
|
|
402
|
+
const cmd = process.platform==="win32" ? `powershell Compress-Archive -Path "${args.source}" -DestinationPath "${args.dest}"` : `zip -r "${args.dest}" "${args.source}"`;
|
|
403
|
+
execSync(cmd,{timeout:60000,shell:true}); return `已压缩: ${args.dest}`; }
|
|
404
|
+
catch(e) { return "压缩失败: "+e.message; }
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{ name: "unzip", description: "解压 zip 文件。source 是 zip 文件路径,destDir 是解压目标目录。",
|
|
408
|
+
parameters: { type:"object", properties:{ source:{type:"string",description:"zip文件路径"}, destDir:{type:"string",description:"解压目录"} }, required:["source"] },
|
|
409
|
+
handler: async (args) => {
|
|
410
|
+
try { const { execSync } = require("child_process"); const d = args.destDir || ".";
|
|
411
|
+
const cmd = process.platform==="win32" ? `powershell Expand-Archive -Path "${args.source}" -DestinationPath "${d}"` : `unzip -o "${args.source}" -d "${d}"`;
|
|
412
|
+
execSync(cmd,{timeout:60000,shell:true}); return `已解压到: ${d}`; }
|
|
413
|
+
catch(e) { return "解压失败: "+e.message; }
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
{ name: "process_list", description: "列出当前运行中的进程。无参数。",
|
|
417
|
+
parameters: { type:"object", properties:{}, required:[] },
|
|
418
|
+
handler: async () => {
|
|
419
|
+
try { const { execSync } = require("child_process");
|
|
420
|
+
const cmd = process.platform==="win32" ? "tasklist /fo csv /nh" : "ps aux --sort=-%mem | head -20";
|
|
421
|
+
return execSync(cmd,{timeout:10000,encoding:"utf8"}).slice(0,2000); }
|
|
422
|
+
catch { return "获取进程列表失败"; }
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{ name: "notify", description: "发送系统桌面通知。title 是标题,message 是内容。",
|
|
426
|
+
parameters: { type:"object", properties:{ title:{type:"string",description:"标题"}, message:{type:"string",description:"内容"} }, required:["title","message"] },
|
|
427
|
+
handler: async (args) => {
|
|
428
|
+
try { const { execSync } = require("child_process");
|
|
429
|
+
if (process.platform==="win32") {
|
|
430
|
+
const ps = `[Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications,ContentType=WindowsRuntime] > $null; $t='<toast><visual><binding template="ToastText02"><text id="1">${args.title}</text><text id="2">${args.message}</text></binding></visual></toast>'; [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Memi').Show((New-Object Windows.Data.Xml.Dom.XmlDocument)).LoadXml($t))`;
|
|
431
|
+
execSync(`powershell -command "${ps}"`,{timeout:5000,shell:true});
|
|
432
|
+
} else if (process.platform==="darwin") {
|
|
433
|
+
execSync(`osascript -e 'display notification "${args.message}" with title "${args.title}"'`,{timeout:5000,shell:true});
|
|
434
|
+
} else {
|
|
435
|
+
execSync(`notify-send "${args.title}" "${args.message}"`,{timeout:5000,shell:true});
|
|
436
|
+
}
|
|
437
|
+
return "已发送通知"; }
|
|
438
|
+
catch { return "通知发送失败"; }
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
{ name: "git_status", description: "查看 git 仓库状态。dir 是仓库目录(默认当前目录)。",
|
|
442
|
+
parameters: { type:"object", properties:{ dir:{type:"string"} }, required:[] },
|
|
443
|
+
handler: async (args) => {
|
|
444
|
+
try { const { execSync } = require("child_process"); const d = args.dir || ".";
|
|
445
|
+
return execSync(`git -C "${d}" status --short`,{timeout:10000,encoding:"utf8"}).slice(0,2000)||"(clean)"; }
|
|
446
|
+
catch { return "不是git仓库或git未安装"; }
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
{ name: "git_log", description: "查看 git 提交历史。dir 是仓库目录,n 是显示条数(默认10)。",
|
|
450
|
+
parameters: { type:"object", properties:{ dir:{type:"string"}, n:{type:"number"} }, required:[] },
|
|
451
|
+
handler: async (args) => {
|
|
452
|
+
try { const { execSync } = require("child_process"); const d = args.dir || "."; const n = args.n || 10;
|
|
453
|
+
return execSync(`git -C "${d}" log --oneline -${n}`,{timeout:10000,encoding:"utf8"}).slice(0,2000); }
|
|
454
|
+
catch { return "无法获取git日志"; }
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
{ name: "random", description: "生成随机数或随机字符串。type 是 number/string,min/max 是数字范围,length 是字符串长度。",
|
|
458
|
+
parameters: { type:"object", properties:{ type:{type:"string"}, min:{type:"number"}, max:{type:"number"}, length:{type:"number"} }, required:["type"] },
|
|
459
|
+
handler: async (args) => {
|
|
460
|
+
if (args.type==="number") { const min=args.min||0,max=args.max||100; return String(Math.floor(Math.random()*(max-min+1))+min); }
|
|
461
|
+
const chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
462
|
+
return Array.from({length:args.length||16},()=>chars[Math.floor(Math.random()*chars.length)]).join("");
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{ name: "hash_text", description: "计算文本哈希值。text 是输入文本,algo 是 md5/sha1/sha256(默认sha256)。",
|
|
466
|
+
parameters: { type:"object", properties:{ text:{type:"string"}, algo:{type:"string"} }, required:["text"] },
|
|
467
|
+
handler: async (args) => { const c=require("crypto"); return c.createHash(args.algo||"sha256").update(args.text).digest("hex"); }
|
|
468
|
+
},
|
|
469
|
+
{ name: "uuid", description: "生成 UUID v4。无参数。",
|
|
470
|
+
parameters: { type:"object", properties:{}, required:[] },
|
|
471
|
+
handler: async () => require("crypto").randomUUID()
|
|
472
|
+
},
|
|
473
|
+
{ name: "count_text", description: "统计文本的字符数、单词数、行数。text 是输入文本。",
|
|
474
|
+
parameters: { type:"object", properties:{ text:{type:"string"} }, required:["text"] },
|
|
475
|
+
handler: async (args) => {
|
|
476
|
+
const t=args.text||""; return `字符:${t.length} 单词:${t.split(/\s+/).filter(Boolean).length} 行:${t.split("\n").length}`;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
{ name: "diff_files", description: "比较两个文件。file1/file2 是文件路径。",
|
|
480
|
+
parameters: { type:"object", properties:{ file1:{type:"string"}, file2:{type:"string"} }, required:["file1","file2"] },
|
|
481
|
+
handler: async (args) => {
|
|
482
|
+
try { const { execSync } = require("child_process");
|
|
483
|
+
const cmd = process.platform==="win32" ? `fc "${args.file1}" "${args.file2}"` : `diff "${args.file1}" "${args.file2}"`;
|
|
484
|
+
return execSync(cmd,{timeout:15000,encoding:"utf8"}).slice(0,2000)||"文件相同"; }
|
|
485
|
+
catch(e) { return "文件不同或比较失败: "+e.message.slice(0,100); }
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
{ name: "disk_usage", description: "查看磁盘使用情况。path 是要查看的路径(默认根目录)。",
|
|
489
|
+
parameters: { type:"object", properties:{ path:{type:"string"} }, required:[] },
|
|
490
|
+
handler: async (args) => {
|
|
491
|
+
try { const { execSync } = require("child_process"); const p = args.path || ".";
|
|
492
|
+
const cmd = process.platform==="win32" ? `wmic logicaldisk get size,freespace,caption` : `df -h "${p}"`;
|
|
493
|
+
return execSync(cmd,{timeout:10000,encoding:"utf8"}).slice(0,1000); }
|
|
494
|
+
catch { return "获取磁盘信息失败"; }
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{ name: "network_info", description: "查看网络接口信息。无参数。",
|
|
498
|
+
parameters: { type:"object", properties:{}, required:[] },
|
|
499
|
+
handler: async () => {
|
|
500
|
+
try { const os=require("os"); const ifaces=os.networkInterfaces();
|
|
501
|
+
return Object.entries(ifaces).map(([name,addrs])=>`${name}: ${addrs.filter(a=>a.family==="IPv4").map(a=>a.address).join(", ")}`).join("\n").slice(0,1000); }
|
|
502
|
+
catch { return "获取网络信息失败"; }
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
{ name: "ping_host", description: "Ping 主机。host 是域名或IP。",
|
|
506
|
+
parameters: { type:"object", properties:{ host:{type:"string"} }, required:["host"] },
|
|
507
|
+
handler: async (args) => {
|
|
508
|
+
try { const { execSync } = require("child_process");
|
|
509
|
+
const cmd = process.platform==="win32" ? `ping -n 3 ${args.host}` : `ping -c 3 ${args.host}`;
|
|
510
|
+
return execSync(cmd,{timeout:15000,encoding:"utf8"}).slice(0,1000); }
|
|
511
|
+
catch { return "Ping失败"; }
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
{ name: "dns_lookup", description: "DNS 解析。hostname 是域名。",
|
|
515
|
+
parameters: { type:"object", properties:{ hostname:{type:"string"} }, required:["hostname"] },
|
|
516
|
+
handler: async (args) => {
|
|
517
|
+
try { const dns = require("dns"); const { promisify } = require("util");
|
|
518
|
+
const addr = await promisify(dns.resolve4)(args.hostname);
|
|
519
|
+
return `${args.hostname} → ${addr.join(", ")}`; }
|
|
520
|
+
catch { return "DNS解析失败"; }
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
{ name: "sort_file", description: "对文件内容排序。path 是文件路径。",
|
|
524
|
+
parameters: { type:"object", properties:{ path:{type:"string"} }, required:["path"] },
|
|
525
|
+
handler: async (args) => {
|
|
526
|
+
try { const data = fs.readFileSync(args.path,"utf8"); const lines=data.split("\n").sort().join("\n");
|
|
527
|
+
fs.writeFileSync(args.path, lines); return `已排序: ${args.path}`; }
|
|
528
|
+
catch { return "排序失败"; }
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
{ name: "get_env", description: "读取环境变量。name 是变量名,不传则返回全部。",
|
|
532
|
+
parameters: { type:"object", properties:{ name:{type:"string"} }, required:[] },
|
|
533
|
+
handler: async (args) => {
|
|
534
|
+
if (args.name) return process.env[args.name] || "(未设置)";
|
|
535
|
+
return Object.entries(process.env).filter(([k])=>!k.includes("KEY")&&!k.includes("SECRET")&&!k.includes("TOKEN")).map(([k,v])=>`${k}=${v}`).slice(0,30).join("\n");
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{ name: "take_screenshot", description: "截取屏幕截图。savePath 是保存路径(默认桌面)。",
|
|
539
|
+
parameters: { type:"object", properties:{ savePath:{type:"string"} }, required:[] },
|
|
540
|
+
handler: async (args) => {
|
|
541
|
+
try { const { execSync } = require("child_process"); const os = require("os");
|
|
542
|
+
const dest = args.savePath || path.join(os.homedir(),"Desktop",`screenshot-${Date.now()}.png`);
|
|
543
|
+
if (process.platform==="win32") {
|
|
544
|
+
execSync(`powershell -command "Add-Type -AssemblyName System.Windows.Forms;$s=[Windows.Forms.Screen]::PrimaryScreen.Bounds;$b=new-object Drawing.Bitmap($s.Width,$s.Height);$g=[Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(0,0,0,0,$s.Size);$b.Save('${dest}')"`,{timeout:15000,shell:true});
|
|
545
|
+
} else if (process.platform==="darwin") {
|
|
546
|
+
execSync(`screencapture "${dest}"`,{timeout:10000});
|
|
547
|
+
} else {
|
|
548
|
+
execSync(`import -window root "${dest}"`,{timeout:10000});
|
|
549
|
+
}
|
|
550
|
+
return `截图已保存: ${dest}`; }
|
|
551
|
+
catch { return "截图失败(可能缺少依赖)"; }
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
{ name: "get_weather", description: "获取指定城市的天气(无需API key)。city 是城市名(拼音或英文)。",
|
|
555
|
+
parameters: { type:"object", properties:{ city:{type:"string",description:"城市名"} }, required:["city"] },
|
|
556
|
+
handler: async (args) => {
|
|
557
|
+
try { const r = await axios.get(`https://wttr.in/${encodeURIComponent(args.city)}?format=4&m`,{timeout:10000});
|
|
558
|
+
return r.data.trim()||"无数据"; }
|
|
559
|
+
catch { return "天气查询失败"; }
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
{ name: "timer", description: "设置一个倒计时提醒(秒)。seconds 是倒计时秒数。",
|
|
563
|
+
parameters: { type:"object", properties:{ seconds:{type:"number",description:"倒计时秒数"} }, required:["seconds"] },
|
|
564
|
+
handler: async (args) => {
|
|
565
|
+
const sec = Math.min(args.seconds||10, 3600);
|
|
566
|
+
return new Promise(resolve => { setTimeout(() => resolve(`⏰ 计时结束 (${sec}秒)`), sec*1000); });
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
{ name: "qr_generate", description: "生成二维码文本(返回终端可显示的ASCII二维码)。text 是二维码内容。",
|
|
570
|
+
parameters: { type:"object", properties:{ text:{type:"string",description:"二维码内容"} }, required:["text"] },
|
|
571
|
+
handler: async (args) => {
|
|
572
|
+
// 简单的ASCII QR标记
|
|
573
|
+
const t = args.text||""; const l = Math.min(t.length, 100);
|
|
574
|
+
return `[QR: ${t.slice(0,50)}${t.length>50?"...":""}] (${t.length} chars)\n在线生成: https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(t.slice(0,200))}`;
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
{ name: "json_format", description: "格式化JSON字符串。json 是JSON文本,indent 是缩进空格数(默认2)。",
|
|
578
|
+
parameters: { type:"object", properties:{ json:{type:"string"}, indent:{type:"number"} }, required:["json"] },
|
|
579
|
+
handler: async (args) => {
|
|
580
|
+
try { return JSON.stringify(JSON.parse(args.json), null, args.indent||2); }
|
|
581
|
+
catch(e) { return "JSON格式错误: "+e.message; }
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
{ name: "csv_parse", description: "解析CSV文本为JSON。csv 是CSV文本。",
|
|
585
|
+
parameters: { type:"object", properties:{ csv:{type:"string"} }, required:["csv"] },
|
|
586
|
+
handler: async (args) => {
|
|
587
|
+
try { const lines=(args.csv||"").trim().split("\n");
|
|
588
|
+
const headers=lines[0].split(",").map(h=>h.trim());
|
|
589
|
+
const rows=lines.slice(1).map(l=>{const vals=l.split(",");const obj={};headers.forEach((h,i)=>obj[h]=vals[i]?.trim()||"");return obj;});
|
|
590
|
+
return JSON.stringify(rows,null,2).slice(0,2000); }
|
|
591
|
+
catch(e) { return "CSV解析错误: "+e.message; }
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
{ name: "password_gen", description: "生成强密码。length 是长度(默认16),includeSymbols 是否包含特殊字符(默认true)。",
|
|
595
|
+
parameters: { type:"object", properties:{ length:{type:"number"}, includeSymbols:{type:"boolean"} }, required:[] },
|
|
596
|
+
handler: async (args) => {
|
|
597
|
+
const len=args.length||16, sym=args.includeSymbols!==false;
|
|
598
|
+
const upper="ABCDEFGHIJKLMNOPQRSTUVWXYZ", lower="abcdefghijklmnopqrstuvwxyz", digits="0123456789", symbols="!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
599
|
+
const pool=upper+lower+digits+(sym?symbols:"");
|
|
600
|
+
return Array.from({length:len},()=>pool[Math.floor(Math.random()*pool.length)]).join("");
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{ name: "timestamp", description: "时间戳转换。action 是 now/to_date,value 是时间戳(秒或毫秒)。",
|
|
604
|
+
parameters: { type:"object", properties:{ action:{type:"string"}, value:{type:"number"} }, required:[] },
|
|
605
|
+
handler: async (args) => {
|
|
606
|
+
if (args.action==="to_date" && args.value) {
|
|
607
|
+
const d=new Date(args.value<1e12?args.value*1000:args.value);
|
|
608
|
+
return d.toISOString()+" ("+d.toLocaleString("zh-CN")+")";
|
|
609
|
+
}
|
|
610
|
+
return String(Math.floor(Date.now()/1000));
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{ name: "ip_info", description: "查询IP地址的地理位置信息。ip 是IP地址(默认当前IP)。",
|
|
614
|
+
parameters: { type:"object", properties:{ ip:{type:"string"} }, required:[] },
|
|
615
|
+
handler: async (args) => {
|
|
616
|
+
try { const r=await axios.get(`http://ip-api.com/json/${args.ip||""}?lang=zh-CN`,{timeout:8000});
|
|
617
|
+
const d=r.data; if(d.status!=="success") return "查询失败";
|
|
618
|
+
return `${d.query}: ${d.country} ${d.regionName} ${d.city} (${d.isp})`; }
|
|
619
|
+
catch { return "IP查询失败"; }
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{ name: "color_convert", description: "颜色格式转换。value 是颜色值,to 是目标格式 hex/rgb/hsl。",
|
|
623
|
+
parameters: { type:"object", properties:{ value:{type:"string"}, to:{type:"string"} }, required:["value","to"] },
|
|
624
|
+
handler: async (args) => {
|
|
625
|
+
// 简单实现:命名颜色转hex
|
|
626
|
+
const colors={red:"#FF0000",green:"#00FF00",blue:"#0000FF",white:"#FFFFFF",black:"#000000",yellow:"#FFFF00",cyan:"#00FFFF",magenta:"#FF00FF",gray:"#808080",orange:"#FFA500",pink:"#FFC0CB",purple:"#800080"};
|
|
627
|
+
const v=(args.value||"").toLowerCase();
|
|
628
|
+
if (colors[v]) return `${v} → HEX:${colors[v]}`;
|
|
629
|
+
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
|
630
|
+
const r=parseInt(v.slice(1,3),16),g=parseInt(v.slice(3,5),16),b=parseInt(v.slice(5,7),16);
|
|
631
|
+
if (args.to==="rgb") return `RGB(${r},${g},${b})`;
|
|
632
|
+
return `HEX:${v} → RGB(${r},${g},${b})`;
|
|
633
|
+
}
|
|
634
|
+
return "支持格式: 颜色名称(red/blue...) 或 HEX(#FF0000)";
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{ name: "image_info", description: "获取图片信息(尺寸、大小)。path 是图片路径。",
|
|
638
|
+
parameters: { type:"object", properties:{ path:{type:"string"} }, required:["path"] },
|
|
639
|
+
handler: async (args) => {
|
|
640
|
+
try {
|
|
641
|
+
const buf=fs.readFileSync(args.path);
|
|
642
|
+
// PNG: bytes 16-23 contain width/height
|
|
643
|
+
if (buf[1]===0x50&&buf[2]===0x4E&&buf[3]===0x47) {
|
|
644
|
+
const w=buf.readUInt32BE(16),h=buf.readUInt32BE(20);
|
|
645
|
+
return `PNG ${w}x${h} (${(buf.length/1024).toFixed(1)}KB)`;
|
|
646
|
+
}
|
|
647
|
+
// JPEG: scan for SOF marker
|
|
648
|
+
if (buf[0]===0xFF&&buf[1]===0xD8) {
|
|
649
|
+
let i=2; while(i<buf.length){ if(buf[i]===0xFF&&buf[i+1]>=0xC0&&buf[i+1]<=0xC3){const h=buf.readUInt16BE(i+5),w=buf.readUInt16BE(i+7);return `JPEG ${w}x${h} (${(buf.length/1024).toFixed(1)}KB)`;} i++; }
|
|
650
|
+
return `JPEG (${(buf.length/1024).toFixed(1)}KB)`;
|
|
651
|
+
}
|
|
652
|
+
return `文件大小: ${(buf.length/1024).toFixed(1)}KB (非PNG/JPEG)`;
|
|
653
|
+
} catch { return "读取图片失败"; }
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
{ name: "text_replace", description: "文本替换。text 是输入文本,find 是查找内容,replace 是替换内容。",
|
|
657
|
+
parameters: { type:"object", properties:{ text:{type:"string"}, find:{type:"string"}, replace:{type:"string"} }, required:["text","find","replace"] },
|
|
658
|
+
handler: async (args) => {
|
|
659
|
+
const replaced=(args.text||"").split(args.find||"").join(args.replace||"");
|
|
660
|
+
return replaced.slice(0,3000)||"(空)";
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
{ name: "url_parse", description: "解析URL的各部分。url 是要解析的URL。",
|
|
664
|
+
parameters: { type:"object", properties:{ url:{type:"string"} }, required:["url"] },
|
|
665
|
+
handler: async (args) => {
|
|
666
|
+
try { const u=new URL(args.url); return `协议:${u.protocol}\n主机:${u.hostname}\n端口:${u.port||"默认"}\n路径:${u.pathname}\n参数:${u.search||"无"}`; }
|
|
667
|
+
catch { return "URL格式无效"; }
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{ name: "read_workspace", description: "读取 workspace 文档(SOUL.md, MEMORY.md, USER.md 等)。file 是文件名不含路径。",
|
|
671
|
+
parameters: { type:"object", properties:{ file:{type:"string",description:"文件名如 SOUL.md"} }, required:["file"] },
|
|
672
|
+
handler: async (args) => {
|
|
673
|
+
try {
|
|
674
|
+
const f = path.join(__dirname, "..", "..", "memi-config", "workspace", args.file || "SOUL.md");
|
|
675
|
+
if (!fs.existsSync(f)) return "文件不存在: " + (args.file || "SOUL.md");
|
|
676
|
+
return fs.readFileSync(f, "utf8").slice(0, 3000);
|
|
677
|
+
} catch { return "读取失败"; }
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
{ name: "remember", description: "将重要信息存入长期记忆(MEMORY.md)。content 是要记住的内容。",
|
|
681
|
+
parameters: { type:"object", properties:{ content:{type:"string"} }, required:["content"] },
|
|
682
|
+
handler: async (args) => {
|
|
683
|
+
try {
|
|
684
|
+
const f = path.join(__dirname, "..", "..", "memi-config", "workspace", "MEMORY.md");
|
|
685
|
+
let existing = "";
|
|
686
|
+
if (fs.existsSync(f)) existing = fs.readFileSync(f, "utf8");
|
|
687
|
+
const entry = `\n- ${new Date().toLocaleString("zh-CN")}: ${args.content}\n`;
|
|
688
|
+
fs.writeFileSync(f, existing + entry);
|
|
689
|
+
return "已记住。";
|
|
690
|
+
} catch { return "记忆存储失败"; }
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
{ name: "write_workspace", description: "写入 workspace 文档。file 是文件名(SOUL.md/MEMORY.md/USER.md),content 是内容。",
|
|
694
|
+
parameters: { type:"object", properties:{ file:{type:"string"}, content:{type:"string"} }, required:["file","content"] },
|
|
695
|
+
handler: async (args) => {
|
|
696
|
+
try {
|
|
697
|
+
const f = path.join(__dirname, "..", "..", "memi-config", "workspace", args.file || "SOUL.md");
|
|
698
|
+
fs.writeFileSync(f, args.content || "");
|
|
699
|
+
return `已更新: ${args.file}`;
|
|
700
|
+
} catch { return "写入失败"; }
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
{ name: "fetch_webpage", description: "获取网页内容(纯文本)。url 是网页地址,自动提取正文。适合阅读新闻/文档/博客。",
|
|
704
|
+
parameters: { type:"object", properties:{ url:{type:"string",description:"网页地址"} }, required:["url"] },
|
|
705
|
+
handler: async (args) => {
|
|
706
|
+
try {
|
|
707
|
+
const r = await axios.get(args.url, {
|
|
708
|
+
headers: { "User-Agent": "Mozilla/5.0 (compatible; MemiAgent/1.0)" },
|
|
709
|
+
timeout: 15000, responseType: "text"
|
|
710
|
+
});
|
|
711
|
+
const html = r.data;
|
|
712
|
+
// 简单提取正文:去掉 script/style 标签,提取 body 文本
|
|
713
|
+
const text = html
|
|
714
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
715
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
716
|
+
.replace(/<[^>]+>/g, " ")
|
|
717
|
+
.replace(/ /g, " ")
|
|
718
|
+
.replace(/\s+/g, " ")
|
|
719
|
+
.trim();
|
|
720
|
+
return text.slice(0, 5000) || "(无正文内容)";
|
|
721
|
+
} catch(e) { return "获取网页失败: " + (e.message || "").slice(0, 100); }
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
{ name: "rag_search", description: "在本地知识库中搜索相关内容(RAG)。query 是搜索问题,返回最相关的文档片段。文档来自 workspace/*.md。",
|
|
725
|
+
parameters: { type:"object", properties:{ query:{type:"string",description:"搜索问题"} }, required:["query"] },
|
|
726
|
+
handler: async (args) => {
|
|
727
|
+
try {
|
|
728
|
+
const wsDir = path.join(__dirname, "..", "..", "memi-config", "workspace");
|
|
729
|
+
if (!fs.existsSync(wsDir)) return "知识库为空(workspace 目录不存在)";
|
|
730
|
+
const q = (args.query || "").toLowerCase();
|
|
731
|
+
const qWords = q.split(/\s+/).filter(w => w.length > 1);
|
|
732
|
+
const chunks = [];
|
|
733
|
+
// 分块:每段作为独立 chunk
|
|
734
|
+
fs.readdirSync(wsDir).filter(f=>f.endsWith(".md")).forEach(f=>{
|
|
735
|
+
const content = fs.readFileSync(path.join(wsDir, f), "utf8");
|
|
736
|
+
const paragraphs = content.split(/\n\n+/);
|
|
737
|
+
paragraphs.forEach((p, i) => {
|
|
738
|
+
if (p.trim().length < 10) return;
|
|
739
|
+
const pLower = p.toLowerCase();
|
|
740
|
+
let score = 0;
|
|
741
|
+
qWords.forEach(w => { if (pLower.includes(w)) score += 1; });
|
|
742
|
+
if (pLower.includes(q)) score += 3;
|
|
743
|
+
if (score > 0) chunks.push({ source: f, para: i, text: p.trim().slice(0, 500), score });
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
chunks.sort((a,b) => b.score - a.score);
|
|
747
|
+
if (chunks.length === 0) return `未找到与 "${args.query}" 相关内容`;
|
|
748
|
+
return chunks.slice(0, 5).map(c => `[${c.source}:${c.score}] ${c.text}`).join("\n\n");
|
|
749
|
+
} catch { return "搜索失败"; }
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{ name: "open_browser", description: "在浏览器中打开网页。url 是网页地址。配合 fetch_webpage 先读内容再决定是否打开。",
|
|
753
|
+
parameters: { type:"object", properties:{ url:{type:"string",description:"网页地址"} }, required:["url"] },
|
|
754
|
+
handler: async (args) => {
|
|
755
|
+
return `请使用 open_url 在浏览器中打开: ${args.url}`;
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
{ name: "schedule_task", description: "创建一个定时任务提醒。task 是任务描述,when 是执行时间(如 'in 5 minutes', 'at 8:00')。",
|
|
759
|
+
parameters: { type:"object", properties:{ task:{type:"string"}, when:{type:"string"} }, required:["task","when"] },
|
|
760
|
+
handler: async (args) => {
|
|
761
|
+
try {
|
|
762
|
+
const file = path.join(require("os").homedir(), "Desktop", "memi-schedule.txt");
|
|
763
|
+
const entry = `[SCHEDULED] ${args.task} — ${args.when} (created ${new Date().toLocaleString("zh-CN")})\n`;
|
|
764
|
+
fs.appendFileSync(file, entry);
|
|
765
|
+
return `已记录任务: ${args.task} (${args.when})\n提醒文件: ${file}`;
|
|
766
|
+
} catch { return "定时任务创建失败"; }
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
{ name: "search_workspace", description: "在 workspace 文档中搜索内容。query 是搜索关键词。",
|
|
770
|
+
parameters: { type:"object", properties:{ query:{type:"string"} }, required:["query"] },
|
|
771
|
+
handler: async (args) => {
|
|
772
|
+
try {
|
|
773
|
+
const wsDir = path.join(__dirname, "..", "..", "memi-config", "workspace");
|
|
774
|
+
if (!fs.existsSync(wsDir)) return "workspace 目录不存在";
|
|
775
|
+
const results = [];
|
|
776
|
+
fs.readdirSync(wsDir).filter(f=>f.endsWith(".md")).forEach(f=>{
|
|
777
|
+
const content = fs.readFileSync(path.join(wsDir, f), "utf8");
|
|
778
|
+
if (content.toLowerCase().includes((args.query||"").toLowerCase())) {
|
|
779
|
+
const lines = content.split("\n").filter(l=>l.toLowerCase().includes((args.query||"").toLowerCase()));
|
|
780
|
+
results.push(`--- ${f} ---\n${lines.slice(0,3).join("\n")}`);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
return results.length > 0 ? results.join("\n\n").slice(0, 2000) : `未找到 "${args.query}"`;
|
|
784
|
+
} catch { return "搜索失败"; }
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
{ name: "list_skills", description: "列出所有已安装的本地技能和ClawHub技能。无参数。",
|
|
788
|
+
parameters: { type:"object", properties:{}, required:[] },
|
|
789
|
+
handler: async () => {
|
|
790
|
+
try {
|
|
791
|
+
const dir = path.join(__dirname, "..", "..", "memi-config", "skills");
|
|
792
|
+
if (!fs.existsSync(dir)) return "无技能";
|
|
793
|
+
const items=[];
|
|
794
|
+
fs.readdirSync(dir).forEach(f=>{
|
|
795
|
+
if (f.endsWith(".json")) { try{const s=JSON.parse(fs.readFileSync(path.join(dir,f),"utf8"));items.push(`${s.name||f} (JSON)`);}catch{} }
|
|
796
|
+
else if (fs.existsSync(path.join(dir,f,"SKILL.md"))) items.push(`${f} (ClawHub)`);
|
|
797
|
+
});
|
|
798
|
+
return items.length>0 ? items.join("\n") : "无技能";
|
|
799
|
+
} catch { return "读取失败"; }
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
{ name: "set_reminder", description: "设置提醒事项。title 是标题,when 是时间描述(如 '5分钟后'),会写入桌面提醒文件。",
|
|
803
|
+
parameters: { type:"object", properties:{ title:{type:"string"}, when:{type:"string"} }, required:["title"] },
|
|
804
|
+
handler: async (args) => {
|
|
805
|
+
try { const os = require("os");
|
|
806
|
+
const file = path.join(os.homedir(), "Desktop", "meminotes.txt");
|
|
807
|
+
const note = `[${new Date().toLocaleString()}] ${args.title}${args.when?" — "+args.when:""}\n`;
|
|
808
|
+
fs.appendFileSync(file, note);
|
|
809
|
+
return `已记录: ${args.title} → ${file}`; }
|
|
810
|
+
catch { return "提醒设置失败"; }
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
{ name: "encode_decode", description: "编码/解码文本。action 是 base64_encode/base64_decode/url_encode/url_decode,text 是输入文本。",
|
|
814
|
+
parameters: { type:"object", properties:{ action:{type:"string",description:"操作类型"}, text:{type:"string",description:"输入文本"} }, required:["action","text"] },
|
|
815
|
+
handler: async (args) => {
|
|
816
|
+
try {
|
|
817
|
+
let r;
|
|
818
|
+
switch(args.action) {
|
|
819
|
+
case "base64_encode": r = Buffer.from(args.text).toString("base64"); break;
|
|
820
|
+
case "base64_decode": r = Buffer.from(args.text,"base64").toString("utf8"); break;
|
|
821
|
+
case "url_encode": r = encodeURIComponent(args.text); break;
|
|
822
|
+
case "url_decode": r = decodeURIComponent(args.text); break;
|
|
823
|
+
default: return "未知操作: "+args.action;
|
|
824
|
+
}
|
|
825
|
+
return r.slice(0,2000);
|
|
826
|
+
} catch { return "编码/解码失败"; }
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "write_file",
|
|
831
|
+
description: "写入文件。支持 ~/path 路径。path 是文件路径,content 是写入内容。",
|
|
832
|
+
parameters: {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties: {
|
|
835
|
+
path: { type: "string", description: "文件路径" },
|
|
836
|
+
content: { type: "string", description: "写入内容" },
|
|
837
|
+
},
|
|
838
|
+
required: ["path", "content"],
|
|
839
|
+
},
|
|
840
|
+
handler: async (args) => {
|
|
841
|
+
try {
|
|
842
|
+
const fs = require("fs"), path = require("path"), os = require("os");
|
|
843
|
+
let filePath = args.path;
|
|
844
|
+
if (filePath.startsWith("~")) filePath = path.join(os.homedir(), filePath.slice(1));
|
|
845
|
+
const resolved = path.resolve(filePath);
|
|
846
|
+
const dir = path.dirname(resolved);
|
|
847
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
848
|
+
fs.writeFileSync(resolved, args.content, "utf8");
|
|
849
|
+
return `已写入 ${args.content.length} 字符到 ${resolved}`;
|
|
850
|
+
} catch (e) { return "写入失败: " + e.message; }
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: "import_skill_from_url",
|
|
855
|
+
description:
|
|
856
|
+
"从 GitHub 链接导入技能。传入一个 GitHub raw/blob/gist 链接,自动获取并解析技能定义。\n" +
|
|
857
|
+
"支持格式:JSON 文件({name, type, description, promptTemplate})或 Markdown 文件(以 # skill name 开头的元数据)",
|
|
858
|
+
parameters: {
|
|
859
|
+
type: "object",
|
|
860
|
+
properties: {
|
|
861
|
+
url: { type: "string", description: "GitHub 链接(支持 blob/raw/gist)" },
|
|
862
|
+
},
|
|
863
|
+
required: ["url"],
|
|
864
|
+
},
|
|
865
|
+
handler: async (args) => {
|
|
866
|
+
try {
|
|
867
|
+
const skill = await importSkillFromUrl(args.url);
|
|
868
|
+
return JSON.stringify(skill);
|
|
869
|
+
} catch (e) {
|
|
870
|
+
return "导入失败: " + (e.message || "未知错误");
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
];
|
|
875
|
+
|
|
876
|
+
// ─── Agent 循环(提示词驱动工具调用)─────────────
|
|
877
|
+
async function callAgent(provider, messages, runtimeTools = {}, thinkingLevel = "high", customSystemPrompt = "") {
|
|
878
|
+
const results = { answer: "", toolCalls: [], iterations: 0, reasoning: "" };
|
|
879
|
+
|
|
880
|
+
// 加载 skills/ 目录下的可执行技能(JSON + ClawHub SKILL.md)
|
|
881
|
+
const skillTools = (() => {
|
|
882
|
+
try {
|
|
883
|
+
const dir = path.join(__dirname, "..", "..", "memi-config", "skills");
|
|
884
|
+
if (!fs.existsSync(dir)) return [];
|
|
885
|
+
const tools = [];
|
|
886
|
+
|
|
887
|
+
// 1. JSON 技能文件
|
|
888
|
+
fs.readdirSync(dir).filter(f => f.endsWith(".json")).forEach(f => {
|
|
889
|
+
try {
|
|
890
|
+
const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
891
|
+
if (!s.name || !s.handler) return;
|
|
892
|
+
tools.push({
|
|
893
|
+
name: "skill_" + s.name.replace(/[^a-zA-Z0-9_\u4e00-\u9fff]/g, "_"),
|
|
894
|
+
description: (s.description || "") + " (本地技能)",
|
|
895
|
+
parameters: s.parameters || { type: "object", properties: {}, required: [] },
|
|
896
|
+
handler: async (args) => {
|
|
897
|
+
try {
|
|
898
|
+
const fn = typeof s.handler === "string" ? new Function("args", "require", s.handler) : s.handler;
|
|
899
|
+
return String(await fn(args, require) || "done");
|
|
900
|
+
} catch(e) { return "Skill error: " + e.message; }
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
} catch {}
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// 2. ClawHub SKILL.md 子目录
|
|
907
|
+
fs.readdirSync(dir, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => {
|
|
908
|
+
try {
|
|
909
|
+
const mdFile = path.join(dir, d.name, "SKILL.md");
|
|
910
|
+
if (!fs.existsSync(mdFile)) return;
|
|
911
|
+
const md = fs.readFileSync(mdFile, "utf8");
|
|
912
|
+
const yamlMatch = md.match(/^---\n([\s\S]*?)\n---/);
|
|
913
|
+
if (!yamlMatch) return;
|
|
914
|
+
const meta = {};
|
|
915
|
+
yamlMatch[1].split("\n").forEach(line => {
|
|
916
|
+
const idx = line.indexOf(":");
|
|
917
|
+
if (idx > 0) meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
918
|
+
});
|
|
919
|
+
const name = meta.name || d.name;
|
|
920
|
+
const desc = meta.description || "";
|
|
921
|
+
const emoji = (meta.metadata || "").includes("emoji") ? " " : "";
|
|
922
|
+
// 解析 requires.bins → 如果有 curl/wget,注册为 shell 工具
|
|
923
|
+
const body = md.replace(/^---[\s\S]*?---\n*/, "");
|
|
924
|
+
const hasCurl = /curl\s/.test(body);
|
|
925
|
+
if (hasCurl) {
|
|
926
|
+
tools.push({
|
|
927
|
+
name: "clawhub_" + name.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
928
|
+
description: `${emoji}${desc} (ClawHub·curl)`,
|
|
929
|
+
parameters: {
|
|
930
|
+
type: "object",
|
|
931
|
+
properties: {
|
|
932
|
+
location: { type: "string", description: "地点或IP或坐标" }
|
|
933
|
+
},
|
|
934
|
+
required: []
|
|
935
|
+
},
|
|
936
|
+
handler: async (args) => {
|
|
937
|
+
try {
|
|
938
|
+
const { execSync } = require("child_process");
|
|
939
|
+
const cmd = body.match(/curl\s+-s\s+"[^"]+"/)?.[0]?.replace(/\$\{args\.\w+\}/g, args.location || "Beijing");
|
|
940
|
+
if (cmd) return execSync(cmd, { timeout: 10000, encoding: "utf8" }).slice(0, 1000);
|
|
941
|
+
return "Skill loaded (knowledge only). Use run_command with curl.";
|
|
942
|
+
} catch(e) { return "ClawHub skill error: " + e.message; }
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
} catch {}
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
return tools;
|
|
950
|
+
} catch { return []; }
|
|
951
|
+
})();
|
|
952
|
+
|
|
953
|
+
const activeTools = [...TOOLS, ...skillTools].map((t) => {
|
|
954
|
+
if (runtimeTools[t.name]) return { ...t, handler: runtimeTools[t.name] };
|
|
955
|
+
if (t.handler) return t;
|
|
956
|
+
return null;
|
|
957
|
+
}).filter(Boolean);
|
|
958
|
+
|
|
959
|
+
const toolMap = {};
|
|
960
|
+
activeTools.forEach((t) => { toolMap[t.name] = t.handler; });
|
|
961
|
+
|
|
962
|
+
const toolDesc = activeTools.map((t) =>
|
|
963
|
+
`${t.name}(${(t.parameters?.required || []).join(", ")}): ${t.description}`
|
|
964
|
+
).join("\n");
|
|
965
|
+
|
|
966
|
+
// 加载 workspace 文档(SOUL.md, MEMORY.md, 及其他 .md 文件)
|
|
967
|
+
let workspaceContext = "";
|
|
968
|
+
try {
|
|
969
|
+
const wsDir = path.join(__dirname, "..", "..", "memi-config", "workspace");
|
|
970
|
+
if (fs.existsSync(wsDir)) {
|
|
971
|
+
const coreFiles = ["SOUL.md", "MEMORY.md", "USER.md", "IDENTITY.md"];
|
|
972
|
+
coreFiles.forEach(f => {
|
|
973
|
+
const p = path.join(wsDir, f);
|
|
974
|
+
if (fs.existsSync(p)) workspaceContext += `[${f}]\n${fs.readFileSync(p, "utf8").slice(0, 2000)}\n\n`;
|
|
975
|
+
});
|
|
976
|
+
// 加载其他自定义 .md
|
|
977
|
+
fs.readdirSync(wsDir).filter(f => f.endsWith(".md") && !coreFiles.includes(f)).forEach(f => {
|
|
978
|
+
workspaceContext += `[${f}]\n${fs.readFileSync(path.join(wsDir, f), "utf8").slice(0, 1000)}\n\n`;
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
} catch {}
|
|
982
|
+
|
|
983
|
+
const toolNames = activeTools.map((t) => t.name).join(", ");
|
|
984
|
+
const systemPrompt = {
|
|
985
|
+
role: "system",
|
|
986
|
+
content:
|
|
987
|
+
(customSystemPrompt ? customSystemPrompt + "\n" : "") +
|
|
988
|
+
(workspaceContext ? workspaceContext + "\n" : "") +
|
|
989
|
+
`You are Memi, a witty, proactive AI agent with full system access. You were built to be helpful, fast, and a little playful.\n\n` +
|
|
990
|
+
`PERSONALITY: concise, warm, direct. Use emoji sparingly. Prefer action over explanation.\n` +
|
|
991
|
+
`CAPABILITIES: ${toolNames}. You can do ANYTHING on this computer — list, read, write, move, delete files, run commands, search the web.\n\n` +
|
|
992
|
+
`RULES:\n` +
|
|
993
|
+
`- Use Markdown tables for data: | col1 | col2 |\n` +
|
|
994
|
+
`- When asked to DO something, DO it immediately via tools. Never say "you should" or "try running".\n` +
|
|
995
|
+
`- If you write a script, run it with run_command right after — don't ask permission.\n` +
|
|
996
|
+
`- Keep responses short. If the task is done, just confirm what happened.\n` +
|
|
997
|
+
`- If something fails, try a different approach instead of giving up.\n` +
|
|
998
|
+
`- Use the user's language.`,
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
const toolDefs = activeTools.map((t) => ({
|
|
1002
|
+
type: "function",
|
|
1003
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
1004
|
+
}));
|
|
1005
|
+
|
|
1006
|
+
let conversation = [systemPrompt, ...messages];
|
|
1007
|
+
|
|
1008
|
+
for (let i = 0; i < MAX_AGENT_ITERATIONS; i++) {
|
|
1009
|
+
results.iterations = i + 1;
|
|
1010
|
+
|
|
1011
|
+
// 使用原生 function calling
|
|
1012
|
+
const axios = require("axios");
|
|
1013
|
+
const resp = await axios.post(
|
|
1014
|
+
`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`,
|
|
1015
|
+
{
|
|
1016
|
+
model: provider.model,
|
|
1017
|
+
messages: conversation,
|
|
1018
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
1019
|
+
tool_choice: toolDefs.length > 0 ? "auto" : undefined,
|
|
1020
|
+
...(supportsThinking(provider.model) && thinkingLevel !== "off" ? { reasoning_effort: thinkingLevel } : {}),
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
headers: { Authorization: `Bearer ${provider.apiKey}`, "Content-Type": "application/json" },
|
|
1024
|
+
timeout: 180000,
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
const msg = resp.data?.choices?.[0]?.message;
|
|
1029
|
+
if (!msg) { results.answer = ""; return results; }
|
|
1030
|
+
if (msg.reasoning_content) results.reasoning = msg.reasoning_content;
|
|
1031
|
+
|
|
1032
|
+
// 检查原生 tool_calls
|
|
1033
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
1034
|
+
conversation.push({ role: "assistant", content: msg.content || "", tool_calls: msg.tool_calls });
|
|
1035
|
+
|
|
1036
|
+
for (const tc of msg.tool_calls) {
|
|
1037
|
+
const fn = tc.function;
|
|
1038
|
+
if (!toolMap[fn.name]) continue;
|
|
1039
|
+
|
|
1040
|
+
let result;
|
|
1041
|
+
try {
|
|
1042
|
+
const args = JSON.parse(fn.arguments || "{}");
|
|
1043
|
+
result = await toolMap[fn.name](args);
|
|
1044
|
+
if (typeof result !== "string") result = JSON.stringify(result);
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
result = "工具执行错误: " + e.message;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
results.toolCalls.push({ tool: fn.name, args: fn.arguments, result: (result || "").slice(0, 2000) });
|
|
1050
|
+
conversation.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1051
|
+
}
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
results.answer = msg.content || "";
|
|
1056
|
+
return results;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const finalResp = await require("axios").post(
|
|
1060
|
+
`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`,
|
|
1061
|
+
{ model: provider.model, messages: conversation },
|
|
1062
|
+
{ headers: { Authorization: `Bearer ${provider.apiKey}`, "Content-Type": "application/json" }, timeout: 180000 }
|
|
1063
|
+
);
|
|
1064
|
+
results.answer = finalResp.data?.choices?.[0]?.message?.content || "抱歉,无法完成。";
|
|
1065
|
+
return results;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
module.exports = { callAgent, TOOLS };
|