glosc-mcp 1.1.1 → 1.2.1
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 +16 -5
- package/build/GloscMcp.js +93 -109
- package/build/GloscTools.js +147 -159
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,23 +16,34 @@
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
|
-
|
|
20
19
|
### Tools
|
|
21
20
|
|
|
22
21
|
- `listApps`
|
|
23
|
-
- 获取已安装应用列表(Windows 注册表)
|
|
24
22
|
- 入参:`{ query?: string, matchMode?: "contains"|"equals"|"regex", limit?: number }`
|
|
25
23
|
|
|
26
24
|
- `getAppInstallPath`
|
|
27
25
|
- 获取应用安装路径(优先 `InstallLocation`,否则从 `DisplayIcon`/卸载命令推断)
|
|
28
26
|
- 入参:`{ name: string, matchMode?: "contains"|"equals"|"regex", allMatches?: boolean, limit?: number }`
|
|
29
|
-
|
|
30
27
|
- `openRef`
|
|
31
28
|
- 打开引用(文件/文件夹/URL/可执行文件),使用系统默认方式打开(Windows 下使用 `Start-Process`)
|
|
32
29
|
- 入参:`{ target: string, args?: string[], wait?: boolean }`
|
|
33
30
|
|
|
31
|
+
- `readFile`
|
|
32
|
+
- 读取文件内容,支持多种文件类型:文本、图片、表格、文档、压缩包(ZIP、RAR、7Z、TAR、GZ等)
|
|
33
|
+
- 入参:`{ path: string }`
|
|
34
|
+
- 说明:读取 Excel(.xlsx/.xls)时,若只有 1 张表则返回该表的行数组 JSON;若有多张表则返回 `{ sheets: [{ sheetName, rows }] }`。
|
|
34
35
|
- `editText`
|
|
35
36
|
- 文本写入/编辑:按行添加/替换/删除(可批量),或创建/替换/删除整个文件
|
|
36
37
|
- 入参(概要):
|
|
37
|
-
-
|
|
38
|
-
|
|
38
|
+
- 按行:`{ path, edits: [{ op: "add"|"replace"|"delete", ... }], createIfMissing?, encoding?, newline?, ensureFinalNewline?, returnContent? }`
|
|
39
|
+
- `renameFile`
|
|
40
|
+
- 重命名文件(同目录改名)
|
|
41
|
+
- 入参:`{ path: string, newName: string, overwrite?: boolean }`
|
|
42
|
+
|
|
43
|
+
- `moveFile`
|
|
44
|
+
- 移动文件到新路径(必要时自动创建目录;目标为目录时会移动到该目录下)
|
|
45
|
+
- 入参:`{ from: string, to: string, overwrite?: boolean, createDirs?: boolean }`
|
|
46
|
+
|
|
47
|
+
- `listFilesRecursive`
|
|
48
|
+
- 递归获取文件夹中的所有文件(默认最多返回 5000 条,避免输出过大)
|
|
49
|
+
- 入参:`{ dir: string, limit?: number }`
|
package/build/GloscMcp.js
CHANGED
|
@@ -178,122 +178,106 @@ export class GloscMcp {
|
|
|
178
178
|
],
|
|
179
179
|
};
|
|
180
180
|
});
|
|
181
|
-
this.server.registerTool("
|
|
182
|
-
description: "
|
|
183
|
-
inputSchema: z
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
encoding: z
|
|
181
|
+
this.server.registerTool("renameFile", {
|
|
182
|
+
description: "重命名文件(同目录改名)",
|
|
183
|
+
inputSchema: z.object({
|
|
184
|
+
path: z.string().describe("源文件的绝对路径"),
|
|
185
|
+
newName: z
|
|
187
186
|
.string()
|
|
188
|
-
.describe("
|
|
189
|
-
|
|
190
|
-
newline: z
|
|
191
|
-
.enum(["auto", "lf", "crlf"])
|
|
192
|
-
.describe("换行符策略:auto/lf/crlf")
|
|
193
|
-
.default("auto"),
|
|
194
|
-
ensureFinalNewline: z
|
|
187
|
+
.describe("新文件名(仅文件名,不含路径)"),
|
|
188
|
+
overwrite: z
|
|
195
189
|
.boolean()
|
|
196
|
-
.describe("
|
|
190
|
+
.describe("目标已存在时是否覆盖")
|
|
197
191
|
.default(false),
|
|
198
|
-
|
|
192
|
+
}),
|
|
193
|
+
}, async ({ path, newName, overwrite }) => {
|
|
194
|
+
try {
|
|
195
|
+
const res = await GloscTools.renameFile({
|
|
196
|
+
path,
|
|
197
|
+
newName,
|
|
198
|
+
overwrite,
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: "text",
|
|
204
|
+
text: JSON.stringify(res, null, 2),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: `重命名失败: ${errorMessage}`,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
this.server.registerTool("moveFile", {
|
|
222
|
+
description: "移动文件到新路径(必要时自动创建目录)",
|
|
223
|
+
inputSchema: z.object({
|
|
224
|
+
from: z.string().describe("源文件的绝对路径"),
|
|
225
|
+
to: z.string().describe("目标路径(文件路径或目录)"),
|
|
226
|
+
overwrite: z
|
|
199
227
|
.boolean()
|
|
200
|
-
.describe("
|
|
228
|
+
.describe("目标已存在时是否覆盖")
|
|
201
229
|
.default(false),
|
|
202
|
-
|
|
230
|
+
createDirs: z
|
|
203
231
|
.boolean()
|
|
204
|
-
.describe("
|
|
205
|
-
.default(
|
|
206
|
-
file: z
|
|
207
|
-
.union([
|
|
208
|
-
z.object({
|
|
209
|
-
action: z
|
|
210
|
-
.literal("create")
|
|
211
|
-
.describe("新建文件"),
|
|
212
|
-
content: z.string().describe("文件内容"),
|
|
213
|
-
overwrite: z
|
|
214
|
-
.boolean()
|
|
215
|
-
.describe("文件已存在时是否覆盖")
|
|
216
|
-
.default(false),
|
|
217
|
-
}),
|
|
218
|
-
z.object({
|
|
219
|
-
action: z
|
|
220
|
-
.literal("replace")
|
|
221
|
-
.describe("替换整个文件内容"),
|
|
222
|
-
content: z.string().describe("文件内容"),
|
|
223
|
-
}),
|
|
224
|
-
z.object({
|
|
225
|
-
action: z
|
|
226
|
-
.literal("delete")
|
|
227
|
-
.describe("删除文件"),
|
|
228
|
-
}),
|
|
229
|
-
])
|
|
230
|
-
.optional(),
|
|
231
|
-
edits: z
|
|
232
|
-
.array(z.union([
|
|
233
|
-
z.object({
|
|
234
|
-
op: z.literal("add"),
|
|
235
|
-
at: z
|
|
236
|
-
.number()
|
|
237
|
-
.int()
|
|
238
|
-
.min(1)
|
|
239
|
-
.describe("插入位置行号(1-based);允许 lineCount+1 表示追加"),
|
|
240
|
-
position: z
|
|
241
|
-
.enum(["before", "after"])
|
|
242
|
-
.describe("插入到该行之前或之后")
|
|
243
|
-
.default("before"),
|
|
244
|
-
lines: z
|
|
245
|
-
.array(z.string())
|
|
246
|
-
.min(1)
|
|
247
|
-
.describe("要插入的行(不含换行符)"),
|
|
248
|
-
}),
|
|
249
|
-
z.object({
|
|
250
|
-
op: z.literal("replace"),
|
|
251
|
-
start: z
|
|
252
|
-
.number()
|
|
253
|
-
.int()
|
|
254
|
-
.min(1)
|
|
255
|
-
.describe("起始行号(1-based)"),
|
|
256
|
-
end: z
|
|
257
|
-
.number()
|
|
258
|
-
.int()
|
|
259
|
-
.min(1)
|
|
260
|
-
.optional()
|
|
261
|
-
.describe("结束行号(1-based,含);缺省则等于 start"),
|
|
262
|
-
lines: z
|
|
263
|
-
.array(z.string())
|
|
264
|
-
.describe("替换后的行(可多行)"),
|
|
265
|
-
}),
|
|
266
|
-
z.object({
|
|
267
|
-
op: z.literal("delete"),
|
|
268
|
-
start: z
|
|
269
|
-
.number()
|
|
270
|
-
.int()
|
|
271
|
-
.min(1)
|
|
272
|
-
.describe("起始行号(1-based)"),
|
|
273
|
-
end: z
|
|
274
|
-
.number()
|
|
275
|
-
.int()
|
|
276
|
-
.min(1)
|
|
277
|
-
.optional()
|
|
278
|
-
.describe("结束行号(1-based,含);缺省则等于 start"),
|
|
279
|
-
}),
|
|
280
|
-
]))
|
|
281
|
-
.optional(),
|
|
282
|
-
})
|
|
283
|
-
.refine((v) => !!v.file !== !!v.edits, {
|
|
284
|
-
message: "必须且只能提供 file 或 edits 之一",
|
|
232
|
+
.describe("是否自动创建目标目录")
|
|
233
|
+
.default(true),
|
|
285
234
|
}),
|
|
286
|
-
}, async (
|
|
235
|
+
}, async ({ from, to, overwrite, createDirs }) => {
|
|
236
|
+
try {
|
|
237
|
+
const res = await GloscTools.moveFile({
|
|
238
|
+
from,
|
|
239
|
+
to,
|
|
240
|
+
overwrite,
|
|
241
|
+
createDirs,
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: JSON.stringify(res, null, 2),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: `移动失败: ${errorMessage}`,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
this.server.registerTool("listFilesRecursive", {
|
|
265
|
+
description: "递归获取文件夹中的所有文件",
|
|
266
|
+
inputSchema: z.object({
|
|
267
|
+
dir: z.string().describe("目录的绝对路径"),
|
|
268
|
+
limit: z
|
|
269
|
+
.number()
|
|
270
|
+
.int()
|
|
271
|
+
.min(1)
|
|
272
|
+
.max(20000)
|
|
273
|
+
.describe("最多返回多少条(防止输出过大)")
|
|
274
|
+
.default(5000),
|
|
275
|
+
}),
|
|
276
|
+
}, async ({ dir, limit }) => {
|
|
287
277
|
try {
|
|
288
|
-
const res = await GloscTools.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
newline: input.newline,
|
|
292
|
-
ensureFinalNewline: input.ensureFinalNewline,
|
|
293
|
-
returnContent: input.returnContent,
|
|
294
|
-
createIfMissing: input.createIfMissing,
|
|
295
|
-
file: input.file,
|
|
296
|
-
edits: input.edits,
|
|
278
|
+
const res = await GloscTools.listFilesRecursive({
|
|
279
|
+
dir,
|
|
280
|
+
limit,
|
|
297
281
|
});
|
|
298
282
|
return {
|
|
299
283
|
content: [
|
|
@@ -310,7 +294,7 @@ export class GloscMcp {
|
|
|
310
294
|
content: [
|
|
311
295
|
{
|
|
312
296
|
type: "text",
|
|
313
|
-
text:
|
|
297
|
+
text: `列出文件失败: ${errorMessage}`,
|
|
314
298
|
},
|
|
315
299
|
],
|
|
316
300
|
};
|
package/build/GloscTools.js
CHANGED
|
@@ -449,10 +449,28 @@ if ($null -ne $apps) {
|
|
|
449
449
|
else if ([".xlsx", ".xls"].includes(ext)) {
|
|
450
450
|
// Excel文件,解析为JSON
|
|
451
451
|
const workbook = XLSX.read(buffer, { type: "buffer" });
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
452
|
+
const sheetNames = (workbook.SheetNames ?? []).filter((name) => typeof name === "string" && name.trim().length > 0);
|
|
453
|
+
if (sheetNames.length === 0) {
|
|
454
|
+
return JSON.stringify([], null, 2);
|
|
455
|
+
}
|
|
456
|
+
const sheets = sheetNames.map((sheetName) => {
|
|
457
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
458
|
+
const rows = worksheet
|
|
459
|
+
? XLSX.utils.sheet_to_json(worksheet, {
|
|
460
|
+
defval: null,
|
|
461
|
+
})
|
|
462
|
+
: [];
|
|
463
|
+
return {
|
|
464
|
+
sheetName,
|
|
465
|
+
rows,
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
// 兼容:只有 1 张表时保持旧行为(直接返回行数组)
|
|
469
|
+
if (sheets.length === 1) {
|
|
470
|
+
return JSON.stringify(sheets[0].rows, null, 2);
|
|
471
|
+
}
|
|
472
|
+
// 多张表:返回按 sheet 分组的结构,避免数据混淆
|
|
473
|
+
return JSON.stringify({ sheets }, null, 2);
|
|
456
474
|
}
|
|
457
475
|
else if (ext === ".pdf") {
|
|
458
476
|
// PDF文件,提取文本
|
|
@@ -508,184 +526,154 @@ if ($null -ne $apps) {
|
|
|
508
526
|
// 支持 \n / \r\n,且保留末尾空行(比如文件以换行结尾)
|
|
509
527
|
if (text === "")
|
|
510
528
|
return [""];
|
|
511
|
-
|
|
512
|
-
return lines;
|
|
529
|
+
return text.split(/\r?\n/);
|
|
513
530
|
}
|
|
514
531
|
static joinLines(lines, newline) {
|
|
515
532
|
return lines.join(newline);
|
|
516
533
|
}
|
|
517
|
-
static async
|
|
518
|
-
const filePath = options.path?.trim();
|
|
519
|
-
if (!filePath)
|
|
520
|
-
throw new Error("path 不能为空");
|
|
521
|
-
const encoding = options.encoding ?? "utf8";
|
|
522
|
-
const returnContent = options.returnContent ?? false;
|
|
523
|
-
const createIfMissing = options.createIfMissing ?? false;
|
|
524
|
-
const hasFileAction = !!options.file;
|
|
525
|
-
const hasEdits = Array.isArray(options.edits) && options.edits.length > 0;
|
|
526
|
-
if ((hasFileAction && hasEdits) || (!hasFileAction && !hasEdits)) {
|
|
527
|
-
throw new Error("必须且只能提供 file 或 edits 之一");
|
|
528
|
-
}
|
|
529
|
-
let existedBefore = true;
|
|
530
|
-
let originalBuffer = null;
|
|
534
|
+
static async pathExists(p) {
|
|
531
535
|
try {
|
|
532
|
-
|
|
536
|
+
await fs.stat(p);
|
|
537
|
+
return true;
|
|
533
538
|
}
|
|
534
539
|
catch (e) {
|
|
540
|
+
if (e && (e.code === "ENOENT" || e.code === "ENOTDIR"))
|
|
541
|
+
return false;
|
|
542
|
+
throw e;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
static async removePath(targetPath) {
|
|
546
|
+
const st = await fs.lstat(targetPath);
|
|
547
|
+
if (st.isDirectory()) {
|
|
548
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
await fs.unlink(targetPath);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
static async movePathInternal(options) {
|
|
555
|
+
const from = options.from?.trim();
|
|
556
|
+
const to = options.to?.trim();
|
|
557
|
+
if (!from)
|
|
558
|
+
throw new Error("from 不能为空");
|
|
559
|
+
if (!to)
|
|
560
|
+
throw new Error("to 不能为空");
|
|
561
|
+
const overwrite = options.overwrite ?? false;
|
|
562
|
+
const createDirs = options.createDirs ?? true;
|
|
563
|
+
const fromStat = await fs.lstat(from).catch((e) => {
|
|
535
564
|
if (e && (e.code === "ENOENT" || e.code === "ENOTDIR")) {
|
|
536
|
-
|
|
537
|
-
originalBuffer = null;
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
throw e;
|
|
565
|
+
throw new Error("源路径不存在");
|
|
541
566
|
}
|
|
567
|
+
throw e;
|
|
568
|
+
});
|
|
569
|
+
let dest = to;
|
|
570
|
+
const toLooksLikeDir = /[\\/]+$/.test(to);
|
|
571
|
+
if (toLooksLikeDir) {
|
|
572
|
+
dest = path.join(to, path.basename(from));
|
|
542
573
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
|
|
574
|
+
else {
|
|
575
|
+
try {
|
|
576
|
+
const toStat = await fs.lstat(to);
|
|
577
|
+
if (toStat.isDirectory()) {
|
|
578
|
+
dest = path.join(to, path.basename(from));
|
|
548
579
|
}
|
|
549
|
-
return {
|
|
550
|
-
ok: true,
|
|
551
|
-
path: filePath,
|
|
552
|
-
action: "delete",
|
|
553
|
-
existedBefore,
|
|
554
|
-
};
|
|
555
580
|
}
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
throw
|
|
559
|
-
}
|
|
560
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
561
|
-
const newline = GloscTools.normalizeNewlineOption(options.newline, undefined);
|
|
562
|
-
let content = options.file.content ?? "";
|
|
563
|
-
if (options.ensureFinalNewline) {
|
|
564
|
-
if (!content.endsWith("\n") && !content.endsWith("\r\n")) {
|
|
565
|
-
content += newline;
|
|
566
|
-
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
if (!(e && (e.code === "ENOENT" || e.code === "ENOTDIR"))) {
|
|
583
|
+
throw e;
|
|
567
584
|
}
|
|
568
|
-
const outBuffer = iconv.encode(content, encoding);
|
|
569
|
-
await fs.writeFile(filePath, outBuffer);
|
|
570
|
-
return {
|
|
571
|
-
ok: true,
|
|
572
|
-
path: filePath,
|
|
573
|
-
action: existedBefore ? "replace" : "create",
|
|
574
|
-
existedBefore,
|
|
575
|
-
bytesWritten: outBuffer.length,
|
|
576
|
-
content: returnContent ? content : undefined,
|
|
577
|
-
};
|
|
578
585
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
: undefined;
|
|
584
|
-
const newline = GloscTools.normalizeNewlineOption(options.newline, decodedExisting);
|
|
585
|
-
let content = options.file.content ?? "";
|
|
586
|
-
if (options.ensureFinalNewline) {
|
|
587
|
-
if (!content.endsWith("\n") && !content.endsWith("\r\n")) {
|
|
588
|
-
content += newline;
|
|
589
|
-
}
|
|
586
|
+
}
|
|
587
|
+
if (await GloscTools.pathExists(dest)) {
|
|
588
|
+
if (!overwrite) {
|
|
589
|
+
throw new Error("目标已存在;如需覆盖请设置 overwrite=true");
|
|
590
590
|
}
|
|
591
|
-
|
|
592
|
-
await fs.writeFile(filePath, outBuffer);
|
|
593
|
-
return {
|
|
594
|
-
ok: true,
|
|
595
|
-
path: filePath,
|
|
596
|
-
action: "replace",
|
|
597
|
-
existedBefore,
|
|
598
|
-
bytesWritten: outBuffer.length,
|
|
599
|
-
content: returnContent ? content : undefined,
|
|
600
|
-
};
|
|
591
|
+
await GloscTools.removePath(dest);
|
|
601
592
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (originalBuffer != null) {
|
|
605
|
-
text = iconv.decode(originalBuffer, encoding);
|
|
593
|
+
if (createDirs) {
|
|
594
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
606
595
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const at = Math.trunc(edit.at);
|
|
623
|
-
if (!Number.isFinite(at) || at < 1) {
|
|
624
|
-
throw new Error("add.at 必须为 >=1 的整数");
|
|
625
|
-
}
|
|
626
|
-
// 允许 at = lines.length + 1(before)用于追加到末尾
|
|
627
|
-
const maxAt = lines.length + 1;
|
|
628
|
-
if (at > maxAt) {
|
|
629
|
-
throw new Error(`add.at 超出范围:当前最大允许 ${maxAt}(可用 lineCount+1 追加)`);
|
|
596
|
+
try {
|
|
597
|
+
await fs.rename(from, dest);
|
|
598
|
+
return { ok: true, from, to: dest };
|
|
599
|
+
}
|
|
600
|
+
catch (e) {
|
|
601
|
+
// 跨盘符/设备移动在部分平台会抛 EXDEV
|
|
602
|
+
if (e && e.code === "EXDEV") {
|
|
603
|
+
if (fromStat.isDirectory()) {
|
|
604
|
+
const cp = fs.cp;
|
|
605
|
+
if (!cp) {
|
|
606
|
+
throw new Error("跨设备移动目录失败(EXDEV),且当前 Node 版本不支持 fs.cp");
|
|
607
|
+
}
|
|
608
|
+
await cp(from, dest, { recursive: true, force: overwrite });
|
|
609
|
+
await fs.rm(from, { recursive: true, force: true });
|
|
610
|
+
return { ok: true, from, to: dest };
|
|
630
611
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
lines.splice(insertIndex, 0, ...edit.lines);
|
|
612
|
+
await fs.copyFile(from, dest);
|
|
613
|
+
await fs.unlink(from);
|
|
614
|
+
return { ok: true, from, to: dest };
|
|
635
615
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
616
|
+
throw e;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
static async renameFile(options) {
|
|
620
|
+
const from = options.path?.trim();
|
|
621
|
+
const newName = options.newName?.trim();
|
|
622
|
+
if (!from)
|
|
623
|
+
throw new Error("path 不能为空");
|
|
624
|
+
if (!newName)
|
|
625
|
+
throw new Error("newName 不能为空");
|
|
626
|
+
// 限制为“同目录改名”,避免把 rename 当 move 用
|
|
627
|
+
const base = path.basename(newName);
|
|
628
|
+
if (base !== newName) {
|
|
629
|
+
throw new Error("newName 只能是文件名,不能包含路径分隔符");
|
|
630
|
+
}
|
|
631
|
+
const to = path.join(path.dirname(from), newName);
|
|
632
|
+
return GloscTools.movePathInternal({
|
|
633
|
+
from,
|
|
634
|
+
to,
|
|
635
|
+
overwrite: options.overwrite,
|
|
636
|
+
createDirs: true,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
static async moveFile(options) {
|
|
640
|
+
return GloscTools.movePathInternal(options);
|
|
641
|
+
}
|
|
642
|
+
static async listFilesRecursive(options) {
|
|
643
|
+
const dir = options.dir?.trim();
|
|
644
|
+
if (!dir)
|
|
645
|
+
throw new Error("dir 不能为空");
|
|
646
|
+
const limit = Math.max(1, Math.trunc(options.limit ?? 5000));
|
|
647
|
+
const rootStat = await fs.lstat(dir).catch((e) => {
|
|
648
|
+
if (e && (e.code === "ENOENT" || e.code === "ENOTDIR")) {
|
|
649
|
+
throw new Error("目录不存在");
|
|
649
650
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
651
|
+
throw e;
|
|
652
|
+
});
|
|
653
|
+
if (!rootStat.isDirectory()) {
|
|
654
|
+
throw new Error("dir 必须是目录");
|
|
655
|
+
}
|
|
656
|
+
const files = [];
|
|
657
|
+
const stack = [dir];
|
|
658
|
+
let truncated = false;
|
|
659
|
+
while (stack.length > 0) {
|
|
660
|
+
const current = stack.pop();
|
|
661
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
662
|
+
for (const entry of entries) {
|
|
663
|
+
const full = path.join(current, entry.name);
|
|
664
|
+
if (entry.isDirectory()) {
|
|
665
|
+
stack.push(full);
|
|
658
666
|
}
|
|
659
|
-
if (
|
|
660
|
-
|
|
667
|
+
else if (entry.isFile()) {
|
|
668
|
+
files.push(full);
|
|
669
|
+
if (files.length >= limit) {
|
|
670
|
+
truncated = true;
|
|
671
|
+
stack.length = 0;
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
661
674
|
}
|
|
662
|
-
lines.splice(start - 1, end - start + 1);
|
|
663
|
-
if (lines.length === 0)
|
|
664
|
-
lines = [""];
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
const neverEdit = edit;
|
|
668
|
-
throw new Error(`未知 edit.op: ${neverEdit.op}`);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
let outText = GloscTools.joinLines(lines, newline);
|
|
672
|
-
if (options.ensureFinalNewline) {
|
|
673
|
-
if (!outText.endsWith("\n") && !outText.endsWith("\r\n")) {
|
|
674
|
-
outText += newline;
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
|
-
|
|
678
|
-
await fs.writeFile(filePath, outBuffer);
|
|
679
|
-
return {
|
|
680
|
-
ok: true,
|
|
681
|
-
path: filePath,
|
|
682
|
-
action: "edit",
|
|
683
|
-
existedBefore,
|
|
684
|
-
lineCountBefore,
|
|
685
|
-
lineCountAfter: lines.length,
|
|
686
|
-
editsApplied: edits.length,
|
|
687
|
-
bytesWritten: outBuffer.length,
|
|
688
|
-
content: returnContent ? outText : undefined,
|
|
689
|
-
};
|
|
677
|
+
return { ok: true, dir, files, truncated };
|
|
690
678
|
}
|
|
691
679
|
}
|