glosc-mcp 1.0.6 → 1.1.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 CHANGED
@@ -29,4 +29,10 @@
29
29
 
30
30
  - `openRef`
31
31
  - 打开引用(文件/文件夹/URL/可执行文件),使用系统默认方式打开(Windows 下使用 `Start-Process`)
32
- - 入参:`{ target: string, args?: string[], wait?: boolean }`
32
+ - 入参:`{ target: string, args?: string[], wait?: boolean }`
33
+
34
+ - `editText`
35
+ - 文本写入/编辑:按行添加/替换/删除(可批量),或创建/替换/删除整个文件
36
+ - 入参(概要):
37
+ - 整文件:`{ path, file: { action: "create"|"replace"|"delete", content?, overwrite? }, encoding?, newline?, ensureFinalNewline?, returnContent? }`
38
+ - 按行:`{ path, edits: [{ op: "add"|"replace"|"delete", ... }], createIfMissing?, encoding?, newline?, ensureFinalNewline?, returnContent? }`
package/build/GloscMcp.js CHANGED
@@ -53,23 +53,6 @@ export class GloscMcp {
53
53
  ],
54
54
  };
55
55
  });
56
- this.server.registerTool("web-search", {
57
- description: "使用 Bing 联网搜索信息",
58
- inputSchema: z.object({
59
- query: z.string().describe("搜索查询内容"),
60
- }),
61
- }, async (input) => {
62
- const url = `https://cn.bing.com/search?q=${encodeURIComponent(input.query)}`;
63
- const content = await GloscTools.usebrowser(url, true, "text");
64
- return {
65
- content: [
66
- {
67
- type: "text",
68
- text: content,
69
- },
70
- ],
71
- };
72
- });
73
56
  this.server.registerTool("readFile", {
74
57
  description: "读取文件内容,支持多种文件类型:文本、图片、表格、文档、压缩包(ZIP、RAR、7Z、TAR、GZ等)",
75
58
  inputSchema: z.object({
@@ -195,6 +178,144 @@ export class GloscMcp {
195
178
  ],
196
179
  };
197
180
  });
181
+ this.server.registerTool("editText", {
182
+ description: "文本写入/编辑:支持按行添加/替换/删除(可批量),或创建/替换/删除整个文件",
183
+ inputSchema: z
184
+ .object({
185
+ path: z.string().describe("文件的绝对路径"),
186
+ encoding: z
187
+ .string()
188
+ .describe("文本编码(默认 utf8)")
189
+ .default("utf8"),
190
+ newline: z
191
+ .enum(["auto", "lf", "crlf"])
192
+ .describe("换行符策略:auto/lf/crlf")
193
+ .default("auto"),
194
+ ensureFinalNewline: z
195
+ .boolean()
196
+ .describe("是否确保文件末尾以换行结尾")
197
+ .default(false),
198
+ returnContent: z
199
+ .boolean()
200
+ .describe("是否在结果中返回最终内容(可能很大)")
201
+ .default(false),
202
+ createIfMissing: z
203
+ .boolean()
204
+ .describe("按行 edits 时文件不存在是否自动新建(默认 false)")
205
+ .default(false),
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 之一",
285
+ }),
286
+ }, async (input) => {
287
+ try {
288
+ const res = await GloscTools.editTextFile({
289
+ path: input.path,
290
+ encoding: input.encoding,
291
+ newline: input.newline,
292
+ ensureFinalNewline: input.ensureFinalNewline,
293
+ returnContent: input.returnContent,
294
+ createIfMissing: input.createIfMissing,
295
+ file: input.file,
296
+ edits: input.edits,
297
+ });
298
+ return {
299
+ content: [
300
+ {
301
+ type: "text",
302
+ text: JSON.stringify(res, null, 2),
303
+ },
304
+ ],
305
+ };
306
+ }
307
+ catch (error) {
308
+ const errorMessage = error instanceof Error ? error.message : String(error);
309
+ return {
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: `编辑文本失败: ${errorMessage}`,
314
+ },
315
+ ],
316
+ };
317
+ }
318
+ });
198
319
  }
199
320
  /**
200
321
  * 注册资源
@@ -492,4 +492,200 @@ if ($null -ne $apps) {
492
492
  }
493
493
  }
494
494
  }
495
+ static normalizeNewlineOption(newline, existingContent) {
496
+ if (newline === "crlf")
497
+ return "\r\n";
498
+ if (newline === "lf")
499
+ return "\n";
500
+ // auto
501
+ if (typeof existingContent === "string" &&
502
+ existingContent.includes("\r\n")) {
503
+ return "\r\n";
504
+ }
505
+ return "\n";
506
+ }
507
+ static splitLinesPreserveEmptyEnd(text) {
508
+ // 支持 \n / \r\n,且保留末尾空行(比如文件以换行结尾)
509
+ if (text === "")
510
+ return [""];
511
+ const lines = text.split(/\r?\n/);
512
+ return lines;
513
+ }
514
+ static joinLines(lines, newline) {
515
+ return lines.join(newline);
516
+ }
517
+ static async editTextFile(options) {
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;
531
+ try {
532
+ originalBuffer = await fs.readFile(filePath);
533
+ }
534
+ catch (e) {
535
+ if (e && (e.code === "ENOENT" || e.code === "ENOTDIR")) {
536
+ existedBefore = false;
537
+ originalBuffer = null;
538
+ }
539
+ else {
540
+ throw e;
541
+ }
542
+ }
543
+ if (options.file) {
544
+ const action = options.file.action;
545
+ if (action === "delete") {
546
+ if (existedBefore) {
547
+ await fs.unlink(filePath);
548
+ }
549
+ return {
550
+ ok: true,
551
+ path: filePath,
552
+ action: "delete",
553
+ existedBefore,
554
+ };
555
+ }
556
+ if (action === "create") {
557
+ if (existedBefore && !options.file.overwrite) {
558
+ throw new Error("文件已存在,action=create 且 overwrite=false");
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
+ }
567
+ }
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
+ }
579
+ // replace
580
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
581
+ const decodedExisting = originalBuffer != null
582
+ ? iconv.decode(originalBuffer, encoding)
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
+ }
590
+ }
591
+ const outBuffer = iconv.encode(content, encoding);
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
+ };
601
+ }
602
+ // line edits
603
+ let text = "";
604
+ if (originalBuffer != null) {
605
+ text = iconv.decode(originalBuffer, encoding);
606
+ }
607
+ else {
608
+ if (!createIfMissing) {
609
+ throw new Error("文件不存在;如需新建请使用 file.action=create 或 createIfMissing=true");
610
+ }
611
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
612
+ text = "";
613
+ }
614
+ const newline = GloscTools.normalizeNewlineOption(options.newline, text);
615
+ // 注意:split 后如果原文为空,返回 [""],这样 lineCount 语义更贴近编辑器
616
+ let lines = GloscTools.splitLinesPreserveEmptyEnd(text);
617
+ const lineCountBefore = lines.length;
618
+ const edits = options.edits ?? [];
619
+ for (const edit of edits) {
620
+ if (edit.op === "add") {
621
+ const position = edit.position ?? "before";
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 追加)`);
630
+ }
631
+ const insertIndex = position === "after"
632
+ ? Math.min(at, lines.length) // after 最多插在最后一行之后
633
+ : at - 1;
634
+ lines.splice(insertIndex, 0, ...edit.lines);
635
+ }
636
+ else if (edit.op === "replace") {
637
+ const start = Math.trunc(edit.start);
638
+ const end = Math.trunc(edit.end ?? edit.start);
639
+ if (!Number.isFinite(start) || start < 1) {
640
+ throw new Error("replace.start 必须为 >=1 的整数");
641
+ }
642
+ if (!Number.isFinite(end) || end < start) {
643
+ throw new Error("replace.end 必须为 >= start 的整数");
644
+ }
645
+ if (end > lines.length) {
646
+ throw new Error(`replace 超出范围:当前行数 ${lines.length}`);
647
+ }
648
+ lines.splice(start - 1, end - start + 1, ...edit.lines);
649
+ }
650
+ else if (edit.op === "delete") {
651
+ const start = Math.trunc(edit.start);
652
+ const end = Math.trunc(edit.end ?? edit.start);
653
+ if (!Number.isFinite(start) || start < 1) {
654
+ throw new Error("delete.start 必须为 >=1 的整数");
655
+ }
656
+ if (!Number.isFinite(end) || end < start) {
657
+ throw new Error("delete.end 必须为 >= start 的整数");
658
+ }
659
+ if (end > lines.length) {
660
+ throw new Error(`delete 超出范围:当前行数 ${lines.length}`);
661
+ }
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
+ }
676
+ }
677
+ const outBuffer = iconv.encode(outText, encoding);
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
+ };
690
+ }
495
691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glosc-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {