@greatlhd/ailo-desktop 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/copy-static.mjs +11 -0
- package/dist/browser_control.js +767 -0
- package/dist/browser_snapshot.js +174 -0
- package/dist/cli.js +36 -0
- package/dist/code_executor.js +95 -0
- package/dist/config_server.js +658 -0
- package/dist/connection_util.js +14 -0
- package/dist/constants.js +2 -0
- package/dist/desktop_state_store.js +57 -0
- package/dist/desktop_types.js +1 -0
- package/dist/desktop_verifier.js +40 -0
- package/dist/dingtalk-handler.js +173 -0
- package/dist/dingtalk-types.js +1 -0
- package/dist/email_handler.js +501 -0
- package/dist/exec_tool.js +90 -0
- package/dist/feishu-handler.js +620 -0
- package/dist/feishu-types.js +8 -0
- package/dist/feishu-utils.js +162 -0
- package/dist/fs_tools.js +398 -0
- package/dist/index.js +433 -0
- package/dist/mcp/config-manager.js +64 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/rpc.js +109 -0
- package/dist/mcp/session.js +140 -0
- package/dist/mcp_manager.js +253 -0
- package/dist/mouse_keyboard.js +516 -0
- package/dist/qq-handler.js +153 -0
- package/dist/qq-types.js +15 -0
- package/dist/qq-ws.js +178 -0
- package/dist/screenshot.js +271 -0
- package/dist/skills_hub.js +212 -0
- package/dist/skills_manager.js +103 -0
- package/dist/static/AGENTS.md +25 -0
- package/dist/static/app.css +539 -0
- package/dist/static/app.html +292 -0
- package/dist/static/app.js +380 -0
- package/dist/static/chat.html +994 -0
- package/dist/time_tool.js +22 -0
- package/dist/utils.js +15 -0
- package/package.json +38 -0
- package/src/browser_control.ts +739 -0
- package/src/browser_snapshot.ts +196 -0
- package/src/cli.ts +44 -0
- package/src/code_executor.ts +101 -0
- package/src/config_server.ts +723 -0
- package/src/connection_util.ts +23 -0
- package/src/constants.ts +2 -0
- package/src/desktop_state_store.ts +64 -0
- package/src/desktop_types.ts +44 -0
- package/src/desktop_verifier.ts +45 -0
- package/src/dingtalk-types.ts +26 -0
- package/src/exec_tool.ts +93 -0
- package/src/feishu-handler.ts +722 -0
- package/src/feishu-types.ts +66 -0
- package/src/feishu-utils.ts +174 -0
- package/src/fs_tools.ts +411 -0
- package/src/index.ts +474 -0
- package/src/mcp/config-manager.ts +85 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/rpc.ts +131 -0
- package/src/mcp/session.ts +182 -0
- package/src/mcp_manager.ts +273 -0
- package/src/mouse_keyboard.ts +526 -0
- package/src/qq-types.ts +49 -0
- package/src/qq-ws.ts +223 -0
- package/src/screenshot.ts +297 -0
- package/src/static/app.css +539 -0
- package/src/static/app.html +292 -0
- package/src/static/app.js +380 -0
- package/src/static/chat.html +994 -0
- package/src/time_tool.ts +24 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export function streamToBuffer(stream) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
5
|
+
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
6
|
+
stream.on("error", reject);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function getPostContentRows(contentJson) {
|
|
10
|
+
try {
|
|
11
|
+
const root = JSON.parse(contentJson);
|
|
12
|
+
let content = root?.content;
|
|
13
|
+
if (!Array.isArray(content) && root?.post) {
|
|
14
|
+
content = (root.post.zh_cn ?? root.post.en)?.content ?? null;
|
|
15
|
+
}
|
|
16
|
+
return Array.isArray(content) ? content : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function extractTextFromPostContent(contentJson) {
|
|
23
|
+
const content = getPostContentRows(contentJson);
|
|
24
|
+
if (!content)
|
|
25
|
+
return "";
|
|
26
|
+
const parts = [];
|
|
27
|
+
for (const row of content) {
|
|
28
|
+
if (!Array.isArray(row))
|
|
29
|
+
continue;
|
|
30
|
+
const rowParts = [];
|
|
31
|
+
for (const node of row) {
|
|
32
|
+
if (node?.tag === "text" && typeof node.text === "string") {
|
|
33
|
+
rowParts.push(node.text);
|
|
34
|
+
}
|
|
35
|
+
else if (node?.tag === "at" && typeof node.user_id === "string") {
|
|
36
|
+
const name = typeof node.user_name === "string" ? node.user_name : "";
|
|
37
|
+
rowParts.push(name ? `@${name}` : `@${node.user_id}`);
|
|
38
|
+
}
|
|
39
|
+
else if (node?.tag === "a" && typeof node.text === "string") {
|
|
40
|
+
rowParts.push(node.text);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (rowParts.length > 0)
|
|
44
|
+
parts.push(rowParts.join(""));
|
|
45
|
+
}
|
|
46
|
+
return parts.join("\n");
|
|
47
|
+
}
|
|
48
|
+
export function extractImageKeysFromPostContent(contentJson) {
|
|
49
|
+
const content = getPostContentRows(contentJson);
|
|
50
|
+
if (!content)
|
|
51
|
+
return [];
|
|
52
|
+
const keys = [];
|
|
53
|
+
for (const row of content) {
|
|
54
|
+
if (!Array.isArray(row))
|
|
55
|
+
continue;
|
|
56
|
+
for (const node of row) {
|
|
57
|
+
if (node?.tag === "img" && typeof node.image_key === "string")
|
|
58
|
+
keys.push(node.image_key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return keys;
|
|
62
|
+
}
|
|
63
|
+
export function extractMentionElements(text, nameToIdCache) {
|
|
64
|
+
const atElements = [];
|
|
65
|
+
const seenIds = new Set();
|
|
66
|
+
let cleanText = text.replace(/@([^@(]+?)\(([a-zA-Z0-9][a-zA-Z0-9_]{9,})\)/g, (_, displayName, userId) => {
|
|
67
|
+
if (!seenIds.has(userId)) {
|
|
68
|
+
atElements.push({ userId, name: displayName.trim() });
|
|
69
|
+
seenIds.add(userId);
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
});
|
|
73
|
+
if (nameToIdCache?.size) {
|
|
74
|
+
const names = [...nameToIdCache.keys()].sort((a, b) => b.length - a.length);
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const openId = nameToIdCache.get(name);
|
|
77
|
+
const atMention = `@${name}`;
|
|
78
|
+
if (cleanText.includes(atMention)) {
|
|
79
|
+
if (!seenIds.has(openId)) {
|
|
80
|
+
atElements.push({ userId: openId, name });
|
|
81
|
+
seenIds.add(openId);
|
|
82
|
+
}
|
|
83
|
+
cleanText = cleanText.replaceAll(atMention, "");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
cleanText = cleanText.replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
88
|
+
return { cleanText, atElements };
|
|
89
|
+
}
|
|
90
|
+
export function adaptMarkdownForFeishu(text) {
|
|
91
|
+
return text
|
|
92
|
+
.replace(/^#{3,}\s+(.+)$/gm, "**$1**")
|
|
93
|
+
.replace(/^[ \t]+([-*])\s/gm, "$1 ")
|
|
94
|
+
.replace(/^[ \t]+(\d+\.)\s/gm, "$1 ");
|
|
95
|
+
}
|
|
96
|
+
function getDisplayWidth(str) {
|
|
97
|
+
let width = 0;
|
|
98
|
+
for (const ch of str) {
|
|
99
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
100
|
+
if ((code >= 0x4e00 && code <= 0x9fff) ||
|
|
101
|
+
(code >= 0x3000 && code <= 0x303f) ||
|
|
102
|
+
(code >= 0xff00 && code <= 0xffef) ||
|
|
103
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
104
|
+
(code >= 0x20000 && code <= 0x2a6df)) {
|
|
105
|
+
width += 2;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
width += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return width;
|
|
112
|
+
}
|
|
113
|
+
function padEnd(str, targetWidth) {
|
|
114
|
+
return str + " ".repeat(Math.max(0, targetWidth - getDisplayWidth(str)));
|
|
115
|
+
}
|
|
116
|
+
export function convertMarkdownTablesToCodeBlock(text) {
|
|
117
|
+
const lines = text.split("\n");
|
|
118
|
+
const result = [];
|
|
119
|
+
let tableLines = [];
|
|
120
|
+
const flushTable = () => {
|
|
121
|
+
if (tableLines.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
const rows = [];
|
|
124
|
+
for (const line of tableLines) {
|
|
125
|
+
const cells = line.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
126
|
+
if (cells.every((c) => /^[-:]+$/.test(c)))
|
|
127
|
+
continue;
|
|
128
|
+
rows.push(cells);
|
|
129
|
+
}
|
|
130
|
+
if (rows.length === 0) {
|
|
131
|
+
result.push(...tableLines);
|
|
132
|
+
tableLines = [];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const colCount = Math.max(...rows.map((r) => r.length));
|
|
136
|
+
const colWidths = Array(colCount).fill(0);
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
for (let i = 0; i < row.length; i++)
|
|
139
|
+
colWidths[i] = Math.max(colWidths[i], getDisplayWidth(row[i]));
|
|
140
|
+
}
|
|
141
|
+
const formatted = [];
|
|
142
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
143
|
+
formatted.push(rows[ri].map((cell, ci) => padEnd(cell, colWidths[ci])).join(" | "));
|
|
144
|
+
if (ri === 0 && rows.length > 1)
|
|
145
|
+
formatted.push(colWidths.map((w) => "-".repeat(w)).join("-+-"));
|
|
146
|
+
}
|
|
147
|
+
result.push("```", ...formatted, "```");
|
|
148
|
+
tableLines = [];
|
|
149
|
+
};
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
if (/^\s*\|/.test(line))
|
|
152
|
+
tableLines.push(line);
|
|
153
|
+
else {
|
|
154
|
+
if (tableLines.length > 0)
|
|
155
|
+
flushTable();
|
|
156
|
+
result.push(line);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (tableLines.length > 0)
|
|
160
|
+
flushTable();
|
|
161
|
+
return result.join("\n");
|
|
162
|
+
}
|
package/dist/fs_tools.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
const MAX_SEARCH_RESULTS = 500;
|
|
6
|
+
const MAX_SEARCH_DEPTH = 10;
|
|
7
|
+
const MAX_FIND_RESULTS = 200;
|
|
8
|
+
const IGNORED_DIRS = ["node_modules", ".git"];
|
|
9
|
+
/** 单次 read_file 最大字节数,超出则分块读取并截断提示 */
|
|
10
|
+
const MAX_READ_BYTES = 2 * 1024 * 1024; // 2MB
|
|
11
|
+
const PROTECTED_PATHS = [
|
|
12
|
+
"/etc", "/System", "/usr", "/bin", "/sbin", "/var", "/root", "/home",
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* 验证绝对路径安全:
|
|
16
|
+
* 1. 必须是绝对路径
|
|
17
|
+
* 2. 解析后必须在允许的基础目录下(防止 /tmp/../etc/passwd 绕过)
|
|
18
|
+
* 3. 不能在受保护路径下
|
|
19
|
+
*/
|
|
20
|
+
function requireAbsPath(p, param, allowedBase) {
|
|
21
|
+
if (!p)
|
|
22
|
+
throw new Error(`${param} 不能为空`);
|
|
23
|
+
if (!path.isAbsolute(p)) {
|
|
24
|
+
throw new Error(`${param} 必须是绝对路径,收到相对路径: "${p}"`);
|
|
25
|
+
}
|
|
26
|
+
const resolved = path.resolve(p);
|
|
27
|
+
// 防止 /tmp/../etc/passwd 等路径穿越
|
|
28
|
+
if (allowedBase && !resolved.startsWith(path.resolve(allowedBase))) {
|
|
29
|
+
throw new Error(`${param} 必须在 ${allowedBase} 下,收到: ${p}`);
|
|
30
|
+
}
|
|
31
|
+
// 禁止操作受保护的系统路径
|
|
32
|
+
for (const protected_ of PROTECTED_PATHS) {
|
|
33
|
+
if (resolved === protected_ || resolved.startsWith(protected_ + path.sep)) {
|
|
34
|
+
throw new Error(`${param} 禁止操作系统关键路径: ${p}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export async function fsTool(name, args) {
|
|
40
|
+
switch (name) {
|
|
41
|
+
case "read_file":
|
|
42
|
+
return readFile(args);
|
|
43
|
+
case "write_file":
|
|
44
|
+
return writeFile(args);
|
|
45
|
+
case "edit_file":
|
|
46
|
+
return editFile(args);
|
|
47
|
+
case "list_directory":
|
|
48
|
+
return listDirectory(args);
|
|
49
|
+
case "find_files":
|
|
50
|
+
return findFiles(args);
|
|
51
|
+
case "search_content":
|
|
52
|
+
return searchContent(args);
|
|
53
|
+
case "delete_file":
|
|
54
|
+
return deleteFile(args);
|
|
55
|
+
case "move_file":
|
|
56
|
+
return moveFile(args);
|
|
57
|
+
case "copy_file":
|
|
58
|
+
return copyFile(args);
|
|
59
|
+
case "append_file":
|
|
60
|
+
return appendFile(args);
|
|
61
|
+
default:
|
|
62
|
+
throw new Error(`unknown fs tool: ${name}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readFile(args) {
|
|
66
|
+
const filePath = requireAbsPath(args.path, "path");
|
|
67
|
+
if (!fs.existsSync(filePath))
|
|
68
|
+
throw new Error(`文件不存在: ${filePath}`);
|
|
69
|
+
const stat = fs.statSync(filePath);
|
|
70
|
+
if (!stat.isFile())
|
|
71
|
+
throw new Error(`路径不是文件: ${filePath}`);
|
|
72
|
+
const rawOffset = args.offset ?? 1;
|
|
73
|
+
if (rawOffset < 1)
|
|
74
|
+
throw new Error("offset 必须 >= 1(1-indexed)");
|
|
75
|
+
const offset = rawOffset - 1;
|
|
76
|
+
const limit = Math.min(args.limit ?? 1000, 5000);
|
|
77
|
+
if (stat.size > MAX_READ_BYTES) {
|
|
78
|
+
// 大文件:流式读取指定行范围
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: fs.createReadStream(filePath),
|
|
81
|
+
crlfDelay: Infinity,
|
|
82
|
+
});
|
|
83
|
+
const result = [];
|
|
84
|
+
let lineIdx = 0;
|
|
85
|
+
let emitted = 0;
|
|
86
|
+
for await (const line of rl) {
|
|
87
|
+
if (lineIdx >= offset) {
|
|
88
|
+
result.push(line);
|
|
89
|
+
emitted++;
|
|
90
|
+
if (emitted >= limit)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
lineIdx++;
|
|
94
|
+
}
|
|
95
|
+
rl.close();
|
|
96
|
+
if (result.length === 0) {
|
|
97
|
+
return `...[文件共 ${stat.size} 字节,offset ${rawOffset} 超出总行数]`;
|
|
98
|
+
}
|
|
99
|
+
const startLineNum = offset + 1;
|
|
100
|
+
return result.map((line, i) => `${String(startLineNum + i).padStart(6)}|${line}`).join("\n");
|
|
101
|
+
}
|
|
102
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
103
|
+
const lines = content.split("\n");
|
|
104
|
+
const slice = limit !== undefined ? lines.slice(offset, offset + limit) : lines.slice(offset);
|
|
105
|
+
return slice.map((line, i) => `${String(offset + i + 1).padStart(6)}|${line}`).join("\n");
|
|
106
|
+
}
|
|
107
|
+
function writeFile(args) {
|
|
108
|
+
const filePath = requireAbsPath(args.path, "path");
|
|
109
|
+
const content = args.content;
|
|
110
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
111
|
+
const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
|
|
112
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
113
|
+
return [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: JSON.stringify({
|
|
116
|
+
filePath,
|
|
117
|
+
bytes: content.length,
|
|
118
|
+
oldBytes: oldContent.length,
|
|
119
|
+
newBytes: content.length,
|
|
120
|
+
oldLines: oldContent ? oldContent.split("\n").length : 0,
|
|
121
|
+
newLines: content.split("\n").length,
|
|
122
|
+
created: !oldContent,
|
|
123
|
+
success: true,
|
|
124
|
+
}, null, 2),
|
|
125
|
+
}];
|
|
126
|
+
}
|
|
127
|
+
function editFile(args) {
|
|
128
|
+
const filePath = requireAbsPath(args.path, "path");
|
|
129
|
+
const oldStr = args.old_string;
|
|
130
|
+
const newStr = args.new_string;
|
|
131
|
+
const replaceAll = args.replace_all ?? false;
|
|
132
|
+
if (!fs.existsSync(filePath))
|
|
133
|
+
throw new Error(`文件不存在: ${filePath}`);
|
|
134
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
135
|
+
if (!content.includes(oldStr)) {
|
|
136
|
+
throw new Error("old_string 在文件中未找到");
|
|
137
|
+
}
|
|
138
|
+
const originalContent = content;
|
|
139
|
+
const oldLines = originalContent.split("\n");
|
|
140
|
+
let newContent;
|
|
141
|
+
let occurrences = 0;
|
|
142
|
+
if (replaceAll) {
|
|
143
|
+
const escaped = escapeRegex(oldStr);
|
|
144
|
+
occurrences = (content.match(new RegExp(escaped, "g")) || []).length;
|
|
145
|
+
newContent = content.split(oldStr).join(newStr);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const idx = content.indexOf(oldStr);
|
|
149
|
+
newContent = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length);
|
|
150
|
+
occurrences = 1;
|
|
151
|
+
}
|
|
152
|
+
const newLines = newContent.split("\n");
|
|
153
|
+
const additions = newLines.length - oldLines.length;
|
|
154
|
+
// 精确定位 oldStr 在原文件中的行号
|
|
155
|
+
const oldStrIdx = originalContent.indexOf(oldStr);
|
|
156
|
+
const oldStrLineCount = oldStr.split("\n").length;
|
|
157
|
+
const oldLineIdx = originalContent.slice(0, oldStrIdx).split("\n").length - 1;
|
|
158
|
+
const lineStart = Math.max(0, oldLineIdx - 2);
|
|
159
|
+
const lineEnd = Math.min(oldLines.length, oldLineIdx + oldStrLineCount + 2);
|
|
160
|
+
fs.writeFileSync(filePath, newContent, "utf-8");
|
|
161
|
+
return [{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: JSON.stringify({
|
|
164
|
+
filePath,
|
|
165
|
+
oldString: oldStr,
|
|
166
|
+
newString: newStr,
|
|
167
|
+
replaceAll,
|
|
168
|
+
occurrences,
|
|
169
|
+
additions,
|
|
170
|
+
deletions: newLines.length - oldLines.length + additions,
|
|
171
|
+
oldLines: oldLines.length,
|
|
172
|
+
newLines: newLines.length,
|
|
173
|
+
hunk: {
|
|
174
|
+
oldStart: lineStart + 1,
|
|
175
|
+
oldLines: lineEnd - lineStart,
|
|
176
|
+
newStart: lineStart + 1,
|
|
177
|
+
newLines: lineEnd - lineStart + additions,
|
|
178
|
+
lines: [
|
|
179
|
+
...oldLines.slice(lineStart, oldLineIdx).map((l) => ` ${l}`),
|
|
180
|
+
...oldStr.split("\n").map((l) => `-${l}`),
|
|
181
|
+
...newStr.split("\n").map((l) => `+${l}`),
|
|
182
|
+
...oldLines.slice(oldLineIdx + oldStrLineCount, lineEnd).map((l) => ` ${l}`),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
success: true,
|
|
186
|
+
}, null, 2),
|
|
187
|
+
}];
|
|
188
|
+
}
|
|
189
|
+
function listDirectory(args) {
|
|
190
|
+
const dirPath = requireAbsPath(args.path, "path");
|
|
191
|
+
if (!fs.existsSync(dirPath))
|
|
192
|
+
throw new Error(`目录不存在: ${dirPath}`);
|
|
193
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
194
|
+
const lines = entries.map((e) => {
|
|
195
|
+
const suffix = e.isDirectory() ? "/" : "";
|
|
196
|
+
try {
|
|
197
|
+
const stat = fs.statSync(path.join(dirPath, e.name));
|
|
198
|
+
const size = e.isDirectory() ? "-" : formatSize(stat.size);
|
|
199
|
+
return `${e.name}${suffix} (${size})`;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return `${e.name}${suffix}`;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return lines.join("\n") || "(空目录)";
|
|
206
|
+
}
|
|
207
|
+
function formatSize(bytes) {
|
|
208
|
+
if (bytes < 1024)
|
|
209
|
+
return `${bytes}B`;
|
|
210
|
+
if (bytes < 1024 * 1024)
|
|
211
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
212
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
213
|
+
}
|
|
214
|
+
async function findFiles(args) {
|
|
215
|
+
const pattern = args.pattern;
|
|
216
|
+
const directory = requireAbsPath(args.directory || process.cwd(), "directory");
|
|
217
|
+
const maxResults = args.max_results || MAX_FIND_RESULTS;
|
|
218
|
+
// 防护:禁止路径穿越 pattern(如 ../, /tmp 等)
|
|
219
|
+
if (pattern.includes("..") || pattern.startsWith("/") || /^[a-zA-Z]:\\/i.test(pattern)) {
|
|
220
|
+
throw new Error("pattern 不允许包含路径穿越或绝对路径");
|
|
221
|
+
}
|
|
222
|
+
const matches = await glob(pattern, {
|
|
223
|
+
cwd: directory,
|
|
224
|
+
absolute: true,
|
|
225
|
+
nodir: false,
|
|
226
|
+
ignore: IGNORED_DIRS.map(d => `**/${d}/**`),
|
|
227
|
+
});
|
|
228
|
+
const limited = matches.slice(0, maxResults);
|
|
229
|
+
if (limited.length === 0)
|
|
230
|
+
return "未找到匹配文件";
|
|
231
|
+
let result = limited.join("\n");
|
|
232
|
+
if (matches.length > maxResults) {
|
|
233
|
+
result += `\n... 还有 ${matches.length - maxResults} 个结果`;
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function searchContent(args) {
|
|
238
|
+
const query = args.query;
|
|
239
|
+
const directory = requireAbsPath(args.directory || process.cwd(), "directory");
|
|
240
|
+
const useRegex = args.regex ?? false;
|
|
241
|
+
const ignoreCase = args.ignore_case ?? false;
|
|
242
|
+
const contextLines = args.context_lines ?? 0;
|
|
243
|
+
const outputMode = args.output_mode ?? "content";
|
|
244
|
+
const headLimit = args.limit ?? 100;
|
|
245
|
+
const offset = args.offset ?? 0;
|
|
246
|
+
let pattern;
|
|
247
|
+
try {
|
|
248
|
+
pattern = useRegex
|
|
249
|
+
? new RegExp(query, ignoreCase ? "gi" : "g")
|
|
250
|
+
: new RegExp(escapeRegex(query), ignoreCase ? "gi" : "g");
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
throw new Error(`无效的正则表达式: "${query}"`);
|
|
254
|
+
}
|
|
255
|
+
const results = [];
|
|
256
|
+
const fileMatches = new Map();
|
|
257
|
+
searchDir(directory, pattern, contextLines, results, 0, MAX_SEARCH_RESULTS, fileMatches);
|
|
258
|
+
if (outputMode === "files_with_matches") {
|
|
259
|
+
const files = Array.from(fileMatches.keys());
|
|
260
|
+
const totalFiles = files.length;
|
|
261
|
+
const paged = files.slice(offset, offset + headLimit);
|
|
262
|
+
const appliedOffset = offset > 0 ? offset : undefined;
|
|
263
|
+
const appliedLimit = totalFiles > offset + headLimit ? headLimit : undefined;
|
|
264
|
+
return [{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: JSON.stringify({
|
|
267
|
+
mode: "files_with_matches",
|
|
268
|
+
numFiles: totalFiles,
|
|
269
|
+
filenames: paged,
|
|
270
|
+
appliedLimit,
|
|
271
|
+
appliedOffset,
|
|
272
|
+
}, null, 2),
|
|
273
|
+
}];
|
|
274
|
+
}
|
|
275
|
+
if (outputMode === "count") {
|
|
276
|
+
const totalMatches = Array.from(fileMatches.values()).reduce((a, b) => a + b, 0);
|
|
277
|
+
const totalFiles = fileMatches.size;
|
|
278
|
+
const appliedOffset = offset > 0 ? offset : undefined;
|
|
279
|
+
const appliedLimit = totalFiles > offset + headLimit ? headLimit : undefined;
|
|
280
|
+
return [{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify({
|
|
283
|
+
mode: "count",
|
|
284
|
+
numFiles: totalFiles,
|
|
285
|
+
numMatches: totalMatches,
|
|
286
|
+
appliedLimit,
|
|
287
|
+
appliedOffset,
|
|
288
|
+
}, null, 2),
|
|
289
|
+
}];
|
|
290
|
+
}
|
|
291
|
+
// content mode (default)
|
|
292
|
+
const totalMatches = results.length;
|
|
293
|
+
const totalFiles = fileMatches.size;
|
|
294
|
+
const paged = results.slice(offset, offset + headLimit);
|
|
295
|
+
const appliedOffset = offset > 0 ? offset : undefined;
|
|
296
|
+
const appliedLimit = totalMatches > offset + headLimit ? headLimit : undefined;
|
|
297
|
+
return [{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: JSON.stringify({
|
|
300
|
+
mode: "content",
|
|
301
|
+
numFiles: totalFiles,
|
|
302
|
+
filenames: Array.from(fileMatches.keys()),
|
|
303
|
+
content: paged.join("\n") || "未找到匹配内容",
|
|
304
|
+
numMatches: totalMatches,
|
|
305
|
+
appliedLimit,
|
|
306
|
+
appliedOffset,
|
|
307
|
+
}, null, 2),
|
|
308
|
+
}];
|
|
309
|
+
}
|
|
310
|
+
function escapeRegex(s) {
|
|
311
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
312
|
+
}
|
|
313
|
+
function searchDir(dir, pattern, ctx, results, depth, limit, fileMatches) {
|
|
314
|
+
if (depth > MAX_SEARCH_DEPTH || results.length >= limit)
|
|
315
|
+
return;
|
|
316
|
+
let entries;
|
|
317
|
+
try {
|
|
318
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
for (const e of entries) {
|
|
324
|
+
if (results.length >= limit)
|
|
325
|
+
return;
|
|
326
|
+
const full = path.join(dir, e.name);
|
|
327
|
+
if (IGNORED_DIRS.includes(e.name))
|
|
328
|
+
continue;
|
|
329
|
+
if (e.isDirectory()) {
|
|
330
|
+
searchDir(full, pattern, ctx, results, depth + 1, limit, fileMatches);
|
|
331
|
+
}
|
|
332
|
+
else if (e.isFile()) {
|
|
333
|
+
try {
|
|
334
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
335
|
+
const lines = content.split("\n");
|
|
336
|
+
let fileMatchCount = 0;
|
|
337
|
+
for (let i = 0; i < lines.length; i++) {
|
|
338
|
+
if (pattern.test(lines[i])) {
|
|
339
|
+
fileMatchCount++;
|
|
340
|
+
const start = Math.max(0, i - ctx);
|
|
341
|
+
const end = Math.min(lines.length, i + ctx + 1);
|
|
342
|
+
const block = lines.slice(start, end).map((l, j) => {
|
|
343
|
+
const lineNum = start + j + 1;
|
|
344
|
+
const marker = (start + j === i) ? ":" : "-";
|
|
345
|
+
return `${String(lineNum).padStart(4)}${marker} ${l}`;
|
|
346
|
+
});
|
|
347
|
+
results.push(`${full}:\n${block.join("\n")}`);
|
|
348
|
+
if (results.length >= limit)
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (fileMatchCount > 0 && fileMatches) {
|
|
353
|
+
fileMatches.set(full, (fileMatches.get(full) ?? 0) + fileMatchCount);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch { /* skip binary / unreadable files */ }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function deleteFile(args) {
|
|
361
|
+
const filePath = requireAbsPath(args.path, "path");
|
|
362
|
+
const recursive = args.recursive ?? false;
|
|
363
|
+
if (!fs.existsSync(filePath))
|
|
364
|
+
throw new Error(`路径不存在: ${filePath}`);
|
|
365
|
+
fs.rmSync(filePath, { recursive, force: true });
|
|
366
|
+
return `已删除 ${filePath}`;
|
|
367
|
+
}
|
|
368
|
+
function moveFile(args) {
|
|
369
|
+
const src = requireAbsPath(args.source, "source");
|
|
370
|
+
const dst = requireAbsPath(args.destination, "destination");
|
|
371
|
+
if (!fs.existsSync(src))
|
|
372
|
+
throw new Error(`源路径不存在: ${src}`);
|
|
373
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
374
|
+
fs.renameSync(src, dst);
|
|
375
|
+
return `已移动 ${src} → ${dst}`;
|
|
376
|
+
}
|
|
377
|
+
function copyFile(args) {
|
|
378
|
+
const src = requireAbsPath(args.source, "source");
|
|
379
|
+
const dst = requireAbsPath(args.destination, "destination");
|
|
380
|
+
if (!fs.existsSync(src))
|
|
381
|
+
throw new Error(`源路径不存在: ${src}`);
|
|
382
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
383
|
+
const stat = fs.statSync(src);
|
|
384
|
+
if (stat.isDirectory()) {
|
|
385
|
+
fs.cpSync(src, dst, { recursive: true });
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
fs.copyFileSync(src, dst);
|
|
389
|
+
}
|
|
390
|
+
return `已复制 ${src} → ${dst}`;
|
|
391
|
+
}
|
|
392
|
+
function appendFile(args) {
|
|
393
|
+
const filePath = requireAbsPath(args.path, "path");
|
|
394
|
+
const content = args.content;
|
|
395
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
396
|
+
fs.appendFileSync(filePath, content, "utf-8");
|
|
397
|
+
return `已追加到 ${filePath}(${content.length} 字符)`;
|
|
398
|
+
}
|