alemonjs-aichat 1.0.33-beta.0 → 1.0.34

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.
@@ -1,14 +1,17 @@
1
1
  import { redis } from '../redis.js';
2
2
  import { getConfigValue } from 'alemonjs';
3
3
  import fs from 'fs';
4
- import { uploadImageToR2 } from '../s3.js';
4
+ import { uploadImageToR2, uploadMediaToR2 } from '../s3.js';
5
5
  import { TTSClient } from './tts.js';
6
+ import OpenAi from 'openai';
6
7
  import { loadSkillDetail } from './loadSkill.js';
7
8
  import help from '../data/help.json.js';
8
9
  import redisClient from '../config.js';
9
10
  import { isPrivateIP, validateURL } from './security.js';
10
11
  import { spawn } from 'child_process';
11
12
  import * as dns from 'node:dns/promises';
13
+ import path from 'node:path';
14
+ import { randomUUID } from 'node:crypto';
12
15
  import { getWorkspace } from './workspace.js';
13
16
  import { getExecCommandEnv } from '../userEnv.js';
14
17
  import { remoteNodeManager } from '../remoteNodes.js';
@@ -170,7 +173,7 @@ const tools = [
170
173
  type: "function",
171
174
  function: {
172
175
  name: "exec",
173
- description: "执行终端命令. 你可以使用这个工具来执行一些简单的命令, 例如查看当前目录下的文件ls, 查看系统状态top等. 当你想要执行一个命令时, 请提供具体的命令文本, 例如'ls -la'或'top -n 1'. 执行结果将会返回给你, 但请注意, 由于安全限制, 某些命令可能无法执行或返回受限的结果",
176
+ description: "执行终端命令. 禁止用于 AI agent 编程文件操作;public/{guid}/ 内的文件增查改删只能使用 Agent* 文件工具,不能通过 exec、Docker 或容器完成. 你可以使用这个工具来执行一些简单的命令, 例如查看当前目录下的文件ls, 查看系统状态top等. 当你想要执行一个命令时, 请提供具体的命令文本, 例如'ls -la'或'top -n 1'. 执行结果将会返回给你, 但请注意, 由于安全限制, 某些命令可能无法执行或返回受限的结果",
174
177
  parameters: {
175
178
  type: "object",
176
179
  properties: {
@@ -208,6 +211,441 @@ const tools = [
208
211
  },
209
212
  },
210
213
  },
214
+ {
215
+ type: "function",
216
+ function: {
217
+ name: "AgentListWorkspaces",
218
+ description: "列出 public/ 下已有的 AI agent 工作目录 guid,适合在不知道之前项目 guid 时先做发现。找到 guid 后再配合 AgentListFiles 或 AgentSearchFiles 查看 knowledge/ 和项目内容。",
219
+ parameters: {
220
+ type: "object",
221
+ properties: {
222
+ includeKnowledge: {
223
+ type: "boolean",
224
+ description: "是否附带每个工作目录下 knowledge/ 的一级仓库目录名摘要",
225
+ default: true,
226
+ },
227
+ maxResults: {
228
+ type: "number",
229
+ description: "最多返回多少个工作目录,默认 50,最大 200",
230
+ default: 50,
231
+ },
232
+ },
233
+ },
234
+ },
235
+ },
236
+ {
237
+ type: "function",
238
+ function: {
239
+ name: "AgentCreateWorkspace",
240
+ description: "在项目 public/ 下创建一个新的 AI agent 工作目录,并返回可用于后续文件工具的 guid。需要新项目目录时使用它,不要用 exec 调用 uuidgen。",
241
+ parameters: {
242
+ type: "object",
243
+ properties: {
244
+ guid: {
245
+ type: "string",
246
+ description: "可选。指定工作目录ID;不传则自动生成一个安全的随机ID",
247
+ },
248
+ },
249
+ },
250
+ },
251
+ },
252
+ {
253
+ type: "function",
254
+ function: {
255
+ name: "AgentCloneRepository",
256
+ description: "克隆一个公开 http/https Git 仓库到 public/{guid}/ 内作为 AI 知识库。只能执行受限的 git clone,不能执行其他 git、终端、Docker 或容器操作。默认克隆到 knowledge/{仓库名}。",
257
+ parameters: {
258
+ type: "object",
259
+ properties: {
260
+ guid: {
261
+ type: "string",
262
+ description: "项目工作目录ID,对应 public/{guid}/",
263
+ },
264
+ repoUrl: {
265
+ type: "string",
266
+ description: "要克隆的公开 Git 仓库 URL,只允许 http/https,不允许 SSH、本地路径或带账号密码的 URL",
267
+ },
268
+ targetPath: {
269
+ type: "string",
270
+ description: "可选。克隆目标目录,必须是 public/{guid}/ 下的相对路径。默认 knowledge/{仓库名}",
271
+ },
272
+ branch: {
273
+ type: "string",
274
+ description: "可选。要克隆的分支或 tag 名称",
275
+ },
276
+ depth: {
277
+ type: "number",
278
+ description: "可选。浅克隆深度,默认 1,最大 50",
279
+ default: 1,
280
+ },
281
+ },
282
+ required: ["guid", "repoUrl"],
283
+ },
284
+ },
285
+ },
286
+ {
287
+ type: "function",
288
+ function: {
289
+ name: "AgentReviewProjectCommand",
290
+ description: "审核将在 public/{guid}/ 工作目录中执行的项目命令是否安全。会先做本地规则审查,并在可用时使用当前 AI 配置做二次审查。",
291
+ parameters: {
292
+ type: "object",
293
+ properties: {
294
+ guid: {
295
+ type: "string",
296
+ description: "项目工作目录ID,对应 public/{guid}/",
297
+ },
298
+ command: {
299
+ type: "string",
300
+ description: "要审查的项目命令,例如 npm.cmd run build 或 npx.cmd create-vite@latest app",
301
+ },
302
+ workingDirectory: {
303
+ type: "string",
304
+ description: "可选。命令执行目录,必须是 public/{guid}/ 下的相对路径,默认为 .",
305
+ default: ".",
306
+ },
307
+ reviewGuid: {
308
+ type: "string",
309
+ description: "可选。当前聊天或当前 AI 配置对应的 guid;不传时会尝试使用全局 AI 配置做二次审查",
310
+ },
311
+ },
312
+ required: ["guid", "command"],
313
+ },
314
+ },
315
+ },
316
+ {
317
+ type: "function",
318
+ function: {
319
+ name: "AgentGitOperation",
320
+ description: "在 public/{guid}/ 内的 Git 仓库执行受限 Git 操作:查看当前分支、获取分支列表、查看远端列表、切换到已有分支。禁止 commit、push、pull、merge、rebase、reset、tag、stash 等会改动历史或同步远端的操作。",
321
+ parameters: {
322
+ type: "object",
323
+ properties: {
324
+ guid: {
325
+ type: "string",
326
+ description: "项目工作目录ID,对应 public/{guid}/",
327
+ },
328
+ workingDirectory: {
329
+ type: "string",
330
+ description: "可选。Git 仓库所在目录,必须是 public/{guid}/ 下的相对路径,默认为 .",
331
+ default: ".",
332
+ },
333
+ action: {
334
+ type: "string",
335
+ description: "Git 操作类型:currentBranch 当前分支;listBranches 分支列表;listRemotes 远端列表;switchBranch 切换到已有分支",
336
+ enum: ["currentBranch", "listBranches", "listRemotes", "switchBranch"],
337
+ },
338
+ branch: {
339
+ type: "string",
340
+ description: "action=switchBranch 时必填,要切换到的分支名。只允许已有本地分支,或已有的 origin/<branch> 远端分支。",
341
+ },
342
+ includeRemote: {
343
+ type: "boolean",
344
+ description: "action=listBranches 时可选,是否同时包含远端分支,默认 true",
345
+ default: true,
346
+ },
347
+ },
348
+ required: ["guid", "action"],
349
+ },
350
+ },
351
+ },
352
+ {
353
+ type: "function",
354
+ function: {
355
+ name: "AgentProjectCommand",
356
+ description: "在 public/{guid}/ 工作目录内执行受限的项目操作:环境检测、受限命令执行、解压缩、重命名和创建目录。run 动作会自动审查命令安全后再执行。",
357
+ parameters: {
358
+ type: "object",
359
+ properties: {
360
+ guid: {
361
+ type: "string",
362
+ description: "项目工作目录ID,对应 public/{guid}/",
363
+ },
364
+ action: {
365
+ type: "string",
366
+ description: "操作类型:inspect 环境检测;run 运行受限项目命令;extract 解压缩;rename 重命名;mkdir 创建目录",
367
+ enum: ["inspect", "run", "extract", "rename", "mkdir"],
368
+ },
369
+ workingDirectory: {
370
+ type: "string",
371
+ description: "可选。操作目录,必须是 public/{guid}/ 下的相对路径,默认为 .",
372
+ default: ".",
373
+ },
374
+ command: {
375
+ type: "string",
376
+ description: "action=run 时必填,要执行的受限项目命令",
377
+ },
378
+ reviewGuid: {
379
+ type: "string",
380
+ description: "可选。当前聊天或当前 AI 配置对应的 guid;用于自动审查 run 命令",
381
+ },
382
+ archivePath: {
383
+ type: "string",
384
+ description: "action=extract 时必填,要解压的压缩包路径,必须是 public/{guid}/ 下的相对路径",
385
+ },
386
+ destinationPath: {
387
+ type: "string",
388
+ description: "action=extract 时可选,解压目标目录,必须是 public/{guid}/ 下的相对路径,默认解压到当前目录",
389
+ },
390
+ sourcePath: {
391
+ type: "string",
392
+ description: "action=rename 时必填,源文件或目录路径,必须是 public/{guid}/ 下的相对路径",
393
+ },
394
+ targetPath: {
395
+ type: "string",
396
+ description: "action=rename 时必填,目标文件或目录路径,必须是 public/{guid}/ 下的相对路径",
397
+ },
398
+ directoryPath: {
399
+ type: "string",
400
+ description: "action=mkdir 时必填,要创建的目录路径,必须是 public/{guid}/ 下的相对路径",
401
+ },
402
+ },
403
+ required: ["guid", "action"],
404
+ },
405
+ },
406
+ },
407
+ {
408
+ type: "function",
409
+ function: {
410
+ name: "AgentReadFileLines",
411
+ description: "读取项目 public/{guid}/ 目录内指定文本文件的指定行范围。适合写代码前查看文件片段。路径必须是相对 public/{guid}/ 的相对路径。",
412
+ parameters: {
413
+ type: "object",
414
+ properties: {
415
+ guid: {
416
+ type: "string",
417
+ description: "项目工作目录ID,对应 public/{guid}/",
418
+ },
419
+ filePath: {
420
+ type: "string",
421
+ description: "要读取的文本文件路径,必须是 public/{guid}/ 下的相对路径",
422
+ },
423
+ startLine: {
424
+ type: "number",
425
+ description: "起始行号,从 1 开始",
426
+ default: 1,
427
+ },
428
+ endLine: {
429
+ type: "number",
430
+ description: "结束行号,从 1 开始。单次最多返回 500 行",
431
+ },
432
+ },
433
+ required: ["guid", "filePath"],
434
+ },
435
+ },
436
+ },
437
+ {
438
+ type: "function",
439
+ function: {
440
+ name: "AgentWriteFile",
441
+ description: "写入项目 public/{guid}/ 目录内指定文件。默认覆盖整个文件,也可追加到文件末尾。路径必须是相对 public/{guid}/ 的相对路径。",
442
+ parameters: {
443
+ type: "object",
444
+ properties: {
445
+ guid: {
446
+ type: "string",
447
+ description: "项目工作目录ID,对应 public/{guid}/",
448
+ },
449
+ filePath: {
450
+ type: "string",
451
+ description: "要写入的文件路径,必须是 public/{guid}/ 下的相对路径",
452
+ },
453
+ content: {
454
+ type: "string",
455
+ description: "要写入的完整文本内容",
456
+ },
457
+ mode: {
458
+ type: "string",
459
+ description: "写入模式:overwrite 覆盖文件,append 追加到末尾",
460
+ enum: ["overwrite", "append"],
461
+ default: "overwrite",
462
+ },
463
+ },
464
+ required: ["guid", "filePath", "content"],
465
+ },
466
+ },
467
+ },
468
+ {
469
+ type: "function",
470
+ function: {
471
+ name: "AgentInsertFile",
472
+ description: "向项目 public/{guid}/ 目录内指定文本文件的指定行之前插入内容。路径必须是相对 public/{guid}/ 的相对路径。",
473
+ parameters: {
474
+ type: "object",
475
+ properties: {
476
+ guid: {
477
+ type: "string",
478
+ description: "项目工作目录ID,对应 public/{guid}/",
479
+ },
480
+ filePath: {
481
+ type: "string",
482
+ description: "要插入内容的文本文件路径,必须是 public/{guid}/ 下的相对路径",
483
+ },
484
+ lineNumber: {
485
+ type: "number",
486
+ description: "插入位置行号,从 1 开始。内容会插入到该行之前;传入总行数+1 表示追加到文件末尾",
487
+ },
488
+ content: {
489
+ type: "string",
490
+ description: "要插入的文本内容",
491
+ },
492
+ },
493
+ required: ["guid", "filePath", "lineNumber", "content"],
494
+ },
495
+ },
496
+ },
497
+ {
498
+ type: "function",
499
+ function: {
500
+ name: "AgentCreateFile",
501
+ description: "在项目 public/{guid}/ 目录内创建新文件。默认不覆盖已有文件。路径必须是相对 public/{guid}/ 的相对路径。",
502
+ parameters: {
503
+ type: "object",
504
+ properties: {
505
+ guid: {
506
+ type: "string",
507
+ description: "项目工作目录ID,对应 public/{guid}/",
508
+ },
509
+ filePath: {
510
+ type: "string",
511
+ description: "要创建的文件路径,必须是 public/{guid}/ 下的相对路径",
512
+ },
513
+ content: {
514
+ type: "string",
515
+ description: "新文件内容",
516
+ default: "",
517
+ },
518
+ overwrite: {
519
+ type: "boolean",
520
+ description: "是否允许覆盖已存在文件",
521
+ default: false,
522
+ },
523
+ },
524
+ required: ["guid", "filePath"],
525
+ },
526
+ },
527
+ },
528
+ {
529
+ type: "function",
530
+ function: {
531
+ name: "AgentDeleteFile",
532
+ description: "删除项目 public/{guid}/ 目录内指定文件。只能删除文件,不能删除文件夹。路径必须是相对 public/{guid}/ 的相对路径。",
533
+ parameters: {
534
+ type: "object",
535
+ properties: {
536
+ guid: {
537
+ type: "string",
538
+ description: "项目工作目录ID,对应 public/{guid}/",
539
+ },
540
+ filePath: {
541
+ type: "string",
542
+ description: "要删除的文件路径,必须是 public/{guid}/ 下的相对路径",
543
+ },
544
+ },
545
+ required: ["guid", "filePath"],
546
+ },
547
+ },
548
+ },
549
+ {
550
+ type: "function",
551
+ function: {
552
+ name: "AgentListFiles",
553
+ description: "获取项目 public/{guid}/ 目录内指定文件夹的文件列表。路径必须是相对 public/{guid}/ 的相对路径。",
554
+ parameters: {
555
+ type: "object",
556
+ properties: {
557
+ guid: {
558
+ type: "string",
559
+ description: "项目工作目录ID,对应 public/{guid}/",
560
+ },
561
+ folderPath: {
562
+ type: "string",
563
+ description: "要列出的文件夹路径,必须是 public/{guid}/ 下的相对路径。默认为当前工作目录根部",
564
+ default: ".",
565
+ },
566
+ recursive: {
567
+ type: "boolean",
568
+ description: "是否递归列出子目录",
569
+ default: false,
570
+ },
571
+ maxDepth: {
572
+ type: "number",
573
+ description: "递归时的最大深度,默认 3",
574
+ default: 3,
575
+ },
576
+ },
577
+ required: ["guid"],
578
+ },
579
+ },
580
+ },
581
+ {
582
+ type: "function",
583
+ function: {
584
+ name: "AgentSearchFiles",
585
+ description: "在项目 public/{guid}/ 目录内按文件名和文件内容搜索,适合在已克隆的知识库或当前项目中查找 README、类名、函数名、配置项或关键字。不要在说“本地没有”之前跳过这个工具。",
586
+ parameters: {
587
+ type: "object",
588
+ properties: {
589
+ guid: {
590
+ type: "string",
591
+ description: "项目工作目录ID,对应 public/{guid}/",
592
+ },
593
+ query: {
594
+ type: "string",
595
+ description: "要搜索的关键词或文本片段",
596
+ },
597
+ folderPath: {
598
+ type: "string",
599
+ description: "可选。搜索起始目录,必须是 public/{guid}/ 下的相对路径,默认 .",
600
+ default: ".",
601
+ },
602
+ mode: {
603
+ type: "string",
604
+ description: "搜索模式:filename 只搜文件名,content 只搜文件内容,both 两者都搜",
605
+ enum: ["filename", "content", "both"],
606
+ default: "both",
607
+ },
608
+ maxResults: {
609
+ type: "number",
610
+ description: "最多返回多少条结果,默认 20,最大 100",
611
+ default: 20,
612
+ },
613
+ },
614
+ required: ["guid", "query"],
615
+ },
616
+ },
617
+ },
618
+ {
619
+ type: "function",
620
+ function: {
621
+ name: "AgentUploadFileToR2",
622
+ description: "将项目 public/{guid}/ 目录内的构建产物或媒体文件上传到 R2,返回可公开访问的链接。路径必须是相对 public/{guid}/ 的相对路径;拿到链接后可直接发给用户,图片可用 <img=链接> 发送。",
623
+ parameters: {
624
+ type: "object",
625
+ properties: {
626
+ guid: {
627
+ type: "string",
628
+ description: "项目工作目录ID,对应 public/{guid}/",
629
+ },
630
+ filePath: {
631
+ type: "string",
632
+ description: "要上传的文件路径,必须是 public/{guid}/ 下的相对路径",
633
+ },
634
+ fileName: {
635
+ type: "string",
636
+ description: "可选。上传后希望使用的文件名,例如 report.zip 或 preview.png。会自动清理非法字符;图片最终扩展名会跟随转换结果。",
637
+ },
638
+ uploadType: {
639
+ type: "string",
640
+ description: "上传类型。file 用于普通构建产物/压缩包/音视频等;image 会走图片压缩和格式转换流程",
641
+ enum: ["file", "image"],
642
+ default: "file",
643
+ },
644
+ },
645
+ required: ["guid", "filePath"],
646
+ },
647
+ },
648
+ },
211
649
  // {
212
650
  // type: "function",
213
651
  // function: {
@@ -255,12 +693,12 @@ const tools = [
255
693
  },
256
694
  dataType: {
257
695
  type: "string",
258
- description: "要获取或修改的数据类型,例如'user'表示用户相关,获取或修改用户数据,'chatHistory'表示历史对话,历史对话只能获取不能修改",
696
+ description: "要获取或修改的数据类型,例如'user'表示用户相关,获取或修改用户数据,'chatHistory'表示历史对话与历史归档,历史对话只能获取不能修改",
259
697
  enum: ["user", "chatHistory"],
260
698
  },
261
699
  data: {
262
700
  type: "string",
263
- description: "要获取或修改的数据内容, 例如当operation为'set'时,dataType为'user'时, data应该是要存储的数据内容, 例如用户喜欢的颜色是蓝色, 则data可以是'favoriteColor:blue', 当operation为'get'时, dataType为'user'时, data就不需要传入值, 它会返回之前存储的数据, 当operation为'get'时, dataType为'chatHistory'时, data不传入值获取摘要列表, 传入id获取对应的摘要详情",
701
+ description: "要获取或修改的数据内容, 例如当operation为'set'时,dataType为'user'时, data应该是要存储的数据内容, 例如用户喜欢的颜色是蓝色, 则data可以是'favoriteColor:blue', 当operation为'get'时,dataType为'user'时,data可以留空; 当operation为'get'dataType为'chatHistory'时,data留空会返回摘要列表, 传入'guid:id'获取该归档详情, 传入'search:关键词'可搜索相关历史归档",
264
702
  },
265
703
  },
266
704
  },
@@ -316,31 +754,1002 @@ const tools = [
316
754
  method: { type: "string", enum: ["GET", "POST"] },
317
755
  body: { type: "string" },
318
756
  },
319
- required: ["url"],
320
- },
321
- },
322
- },
323
- {
324
- type: "function",
325
- function: {
326
- name: "getAlemonjsConfig",
327
- description: `获取框架配置,详情请参考use-alemon技能说明`,
328
- parameters: {
329
- type: "object",
330
- properties: {
331
- key: {
332
- type: "string",
333
- description: "配置项的键,可用值请参考use-alemon技能说明",
334
- },
335
- value: {
336
- type: "string",
337
- description: "查找时传入值,可用值请参考use-alemon技能说明",
338
- },
757
+ required: ["url"],
758
+ },
759
+ },
760
+ },
761
+ {
762
+ type: "function",
763
+ function: {
764
+ name: "getAlemonjsConfig",
765
+ description: `获取框架配置,详情请参考use-alemon技能说明`,
766
+ parameters: {
767
+ type: "object",
768
+ properties: {
769
+ key: {
770
+ type: "string",
771
+ description: "配置项的键,可用值请参考use-alemon技能说明",
772
+ },
773
+ value: {
774
+ type: "string",
775
+ description: "查找时传入值,可用值请参考use-alemon技能说明",
776
+ },
777
+ },
778
+ },
779
+ },
780
+ },
781
+ ];
782
+ const AGENT_FILE_MAX_READ_LINES = 500;
783
+ const AGENT_FILE_MAX_LIST_ENTRIES = 500;
784
+ const AGENT_FILE_MAX_SEARCH_RESULTS = 100;
785
+ const PROJECT_COMMAND_MAX_OUTPUT = 24000;
786
+ const PROJECT_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
787
+ const ALLOWED_PROJECT_COMMAND_BASES = new Set([
788
+ "dotnet",
789
+ "dotnet.exe",
790
+ "npm",
791
+ "npm.cmd",
792
+ "npx",
793
+ "npx.cmd",
794
+ "pnpm",
795
+ "pnpm.cmd",
796
+ "yarn",
797
+ "yarn.cmd",
798
+ "bun",
799
+ "bunx",
800
+ "node",
801
+ "node.exe",
802
+ "git",
803
+ "where",
804
+ "where.exe",
805
+ ]);
806
+ const ALLOWED_GIT_PROJECT_SUBCOMMANDS = new Set([
807
+ "init",
808
+ "status",
809
+ "branch",
810
+ "rev-parse",
811
+ "log",
812
+ "describe",
813
+ "remote",
814
+ ]);
815
+ const ALLOWED_DOTNET_PROJECT_SUBCOMMANDS = new Set([
816
+ "--version",
817
+ "--info",
818
+ "--list-sdks",
819
+ "--list-runtimes",
820
+ "new",
821
+ "restore",
822
+ "build",
823
+ "run",
824
+ "test",
825
+ "publish",
826
+ "clean",
827
+ "add",
828
+ "remove",
829
+ "list",
830
+ "sln",
831
+ ]);
832
+ const BLOCKED_PROJECT_COMMAND_PATTERNS = [
833
+ { pattern: /&&|\|\||[;|<>`]|[$][(]/, reason: "不允许使用 shell 控制符、重定向、命令替换或反引号" },
834
+ { pattern: /(^|[\s])(rm|del|erase|rmdir|rd)([\s]|$)/i, reason: "不允许使用删除命令" },
835
+ { pattern: /(^|[\s])(powershell|pwsh|cmd|bash|sh|docker|ssh|scp|sftp|curl|wget)([\s]|$)/i, reason: "不允许执行受限外部命令" },
836
+ { pattern: /\.\.[\\/]/, reason: "不允许通过 .. 离开 public 工作目录" },
837
+ ];
838
+ const toToolError = (error) => ({
839
+ success: false,
840
+ error: error instanceof Error ? error.message : String(error),
841
+ });
842
+ const assertPathInside = (root, target) => {
843
+ const relative = path.relative(root, target);
844
+ if (relative && (relative.startsWith("..") || path.isAbsolute(relative))) {
845
+ throw new Error("路径超出 public/{guid}/ 工作目录");
846
+ }
847
+ };
848
+ const getExistingAncestor = (target) => {
849
+ let current = target;
850
+ while (!fs.existsSync(current)) {
851
+ const parent = path.dirname(current);
852
+ if (parent === current) {
853
+ throw new Error("无法解析目标路径");
854
+ }
855
+ current = parent;
856
+ }
857
+ return current;
858
+ };
859
+ const assertRealPathInside = (root, target) => {
860
+ const realRoot = fs.realpathSync.native(root);
861
+ const existing = getExistingAncestor(target);
862
+ const realExisting = fs.realpathSync.native(existing);
863
+ assertPathInside(realRoot, realExisting);
864
+ if (fs.existsSync(target)) {
865
+ const realTarget = fs.realpathSync.native(target);
866
+ assertPathInside(realRoot, realTarget);
867
+ }
868
+ };
869
+ const resolveAgentRoot = (guid) => {
870
+ if (!guid || typeof guid !== "string") {
871
+ throw new Error("guid 不能为空");
872
+ }
873
+ const safeGuid = guid.trim();
874
+ if (!safeGuid || safeGuid === "." || safeGuid === "..") {
875
+ throw new Error("guid 必须是 public 下的有效目录名");
876
+ }
877
+ if (path.isAbsolute(safeGuid) ||
878
+ safeGuid.includes("/") ||
879
+ safeGuid.includes("\\") ||
880
+ safeGuid.includes("\0")) {
881
+ throw new Error("guid 只能作为 public 下的单级目录名");
882
+ }
883
+ const publicRoot = path.resolve(process.cwd(), "public");
884
+ const root = path.resolve(publicRoot, safeGuid);
885
+ assertPathInside(publicRoot, root);
886
+ fs.mkdirSync(root, { recursive: true });
887
+ assertRealPathInside(publicRoot, root);
888
+ return root;
889
+ };
890
+ const createAgentGuid = (guid) => {
891
+ if (guid && guid.trim()) {
892
+ resolveAgentRoot(guid);
893
+ return guid.trim();
894
+ }
895
+ const guidValue = randomUUID();
896
+ resolveAgentRoot(guidValue);
897
+ return guidValue;
898
+ };
899
+ const listAgentWorkspaces = (includeKnowledge = true, maxResults = 50) => {
900
+ const publicRoot = path.resolve(process.cwd(), "public");
901
+ if (!fs.existsSync(publicRoot)) {
902
+ return [];
903
+ }
904
+ const limit = Math.max(1, Math.min(normalizePositiveInteger(maxResults, 50, "maxResults"), 200));
905
+ const ignoredNames = new Set(["sandbox", "image_out"]);
906
+ return fs
907
+ .readdirSync(publicRoot, { withFileTypes: true })
908
+ .filter((entry) => entry.isDirectory() && !ignoredNames.has(entry.name))
909
+ .map((entry) => {
910
+ const root = path.join(publicRoot, entry.name);
911
+ const knowledgeRoot = path.join(root, "knowledge");
912
+ const knowledgeProjects = includeKnowledge && fs.existsSync(knowledgeRoot)
913
+ ? fs
914
+ .readdirSync(knowledgeRoot, { withFileTypes: true })
915
+ .filter((item) => item.isDirectory())
916
+ .map((item) => item.name)
917
+ .slice(0, 20)
918
+ : [];
919
+ return {
920
+ guid: entry.name,
921
+ path: `public/${entry.name}`,
922
+ hasKnowledge: knowledgeProjects.length > 0,
923
+ knowledgeProjects,
924
+ };
925
+ })
926
+ .sort((a, b) => a.guid.localeCompare(b.guid))
927
+ .slice(0, limit);
928
+ };
929
+ const commandTargetsAgentWorkspace = (command) => {
930
+ const normalized = command.replace(/["']/g, "").toLowerCase();
931
+ return (normalized.includes("public/") ||
932
+ normalized.includes("public\\") ||
933
+ normalized.includes("knowledge/") ||
934
+ normalized.includes("knowledge\\"));
935
+ };
936
+ const sanitizeRepositoryName = (repoUrl) => {
937
+ const url = new URL(repoUrl);
938
+ const lastSegment = url.pathname.split("/").filter(Boolean).pop() || "repo";
939
+ const withoutGit = lastSegment.replace(/\.git$/i, "");
940
+ const safeName = withoutGit.replace(/[^a-zA-Z0-9._-]/g, "-");
941
+ return safeName || "repo";
942
+ };
943
+ const validateRepositoryUrl = async (repoUrl) => {
944
+ const url = new URL(repoUrl);
945
+ if (!["http:", "https:"].includes(url.protocol)) {
946
+ throw new Error("仓库地址只允许 http/https");
947
+ }
948
+ if (url.username || url.password) {
949
+ throw new Error("仓库地址不能包含账号密码或 token");
950
+ }
951
+ await validateURL(repoUrl);
952
+ };
953
+ const validateGitRef = (ref) => {
954
+ if (!/^[a-zA-Z0-9._/-]+$/.test(ref) || ref.includes("..")) {
955
+ throw new Error("branch 只能包含字母、数字、点、下划线、短横线和斜杠");
956
+ }
957
+ };
958
+ const normalizeCloneDepth = (depth) => {
959
+ const safeDepth = normalizePositiveInteger(depth, 1, "depth");
960
+ return Math.min(safeDepth, 50);
961
+ };
962
+ const resolveCloneTargetPath = (repoUrl, targetPath) => {
963
+ const safeTargetPath = targetPath?.trim();
964
+ if (safeTargetPath) {
965
+ if (safeTargetPath === "." || safeTargetPath === "/") {
966
+ throw new Error("targetPath 不能是工作目录根部");
967
+ }
968
+ return safeTargetPath;
969
+ }
970
+ return `knowledge/${sanitizeRepositoryName(repoUrl)}`;
971
+ };
972
+ const resolveAgentPath = (guid, inputPath = ".") => {
973
+ if (path.isAbsolute(inputPath) || inputPath.includes("\0")) {
974
+ throw new Error("只能使用相对 public/{guid}/ 的路径");
975
+ }
976
+ const root = resolveAgentRoot(guid);
977
+ const target = path.resolve(root, inputPath || ".");
978
+ assertPathInside(root, target);
979
+ assertRealPathInside(root, target);
980
+ return {
981
+ root,
982
+ target,
983
+ relativePath: path.relative(root, target).replace(/\\/g, "/") || ".",
984
+ };
985
+ };
986
+ const ensureParentDirectory = (target) => {
987
+ fs.mkdirSync(path.dirname(target), { recursive: true });
988
+ };
989
+ const ensureGitRepository = async (cwd) => {
990
+ const result = await runSpawnCommand({
991
+ file: "git",
992
+ args: ["rev-parse", "--is-inside-work-tree"],
993
+ cwd,
994
+ });
995
+ if (result.exitCode !== 0 || result.output.trim().toLowerCase() !== "true") {
996
+ throw new Error("目标目录不是 Git 仓库");
997
+ }
998
+ };
999
+ const getGitCurrentBranch = async (cwd) => {
1000
+ const result = await runSpawnCommand({
1001
+ file: "git",
1002
+ args: ["branch", "--show-current"],
1003
+ cwd,
1004
+ });
1005
+ if (result.exitCode !== 0) {
1006
+ throw new Error(result.output.trim() || "获取当前分支失败");
1007
+ }
1008
+ return result.output.trim();
1009
+ };
1010
+ const listGitBranches = async (cwd, includeRemote) => {
1011
+ const args = ["branch", "--list"];
1012
+ if (includeRemote) {
1013
+ args.push("--all");
1014
+ }
1015
+ const result = await runSpawnCommand({
1016
+ file: "git",
1017
+ args,
1018
+ cwd,
1019
+ });
1020
+ if (result.exitCode !== 0) {
1021
+ throw new Error(result.output.trim() || "获取分支列表失败");
1022
+ }
1023
+ return result.output
1024
+ .split(/\r?\n/)
1025
+ .map((line) => line.trimEnd())
1026
+ .map((line) => {
1027
+ const current = line.startsWith("*");
1028
+ const name = line.replace(/^\*\s*/, "").trim();
1029
+ if (!name || name === "HEAD detached at") {
1030
+ return null;
1031
+ }
1032
+ return {
1033
+ name,
1034
+ current,
1035
+ remote: name.startsWith("remotes/"),
1036
+ };
1037
+ })
1038
+ .filter((item) => !!item);
1039
+ };
1040
+ const listGitRemotes = async (cwd) => {
1041
+ const result = await runSpawnCommand({
1042
+ file: "git",
1043
+ args: ["remote", "-v"],
1044
+ cwd,
1045
+ });
1046
+ if (result.exitCode !== 0) {
1047
+ throw new Error(result.output.trim() || "获取远端列表失败");
1048
+ }
1049
+ const remotes = new Map();
1050
+ for (const line of result.output.split(/\r?\n/)) {
1051
+ const match = line.trim().match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/);
1052
+ if (!match)
1053
+ continue;
1054
+ const [, name, url, type] = match;
1055
+ const current = remotes.get(name) || { name };
1056
+ if (type === "fetch")
1057
+ current.fetch = url;
1058
+ if (type === "push")
1059
+ current.push = url;
1060
+ remotes.set(name, current);
1061
+ }
1062
+ return [...remotes.values()];
1063
+ };
1064
+ const switchGitBranch = async (cwd, branch) => {
1065
+ validateGitRef(branch);
1066
+ const localBranches = await listGitBranches(cwd, false);
1067
+ const localExists = localBranches.some((item) => item.name === branch);
1068
+ if (localExists) {
1069
+ const result = await runSpawnCommand({
1070
+ file: "git",
1071
+ args: ["switch", branch],
1072
+ cwd,
1073
+ });
1074
+ if (result.exitCode !== 0) {
1075
+ throw new Error(result.output.trim() || "切换本地分支失败");
1076
+ }
1077
+ return {
1078
+ branch,
1079
+ source: "local",
1080
+ output: result.output,
1081
+ };
1082
+ }
1083
+ const remoteBranches = await listGitBranches(cwd, true);
1084
+ const remoteName = `remotes/origin/${branch}`;
1085
+ const remoteExists = remoteBranches.some((item) => item.name === remoteName);
1086
+ if (!remoteExists) {
1087
+ throw new Error("目标分支不存在,本地和 origin 远端都未找到该分支");
1088
+ }
1089
+ const result = await runSpawnCommand({
1090
+ file: "git",
1091
+ args: ["switch", "--track", `origin/${branch}`],
1092
+ cwd,
1093
+ });
1094
+ if (result.exitCode !== 0) {
1095
+ throw new Error(result.output.trim() || "切换远端跟踪分支失败");
1096
+ }
1097
+ return {
1098
+ branch,
1099
+ source: "origin",
1100
+ output: result.output,
1101
+ };
1102
+ };
1103
+ const readTextLines = (target) => {
1104
+ const content = fs.readFileSync(target, "utf-8");
1105
+ return content.length ? content.split(/\r?\n/) : [];
1106
+ };
1107
+ const isLikelyTextFile = (filePath) => {
1108
+ const ext = path.extname(filePath).toLowerCase();
1109
+ if (!ext)
1110
+ return true;
1111
+ const binaryExtensions = new Set([
1112
+ ".png",
1113
+ ".jpg",
1114
+ ".jpeg",
1115
+ ".gif",
1116
+ ".webp",
1117
+ ".bmp",
1118
+ ".ico",
1119
+ ".zip",
1120
+ ".tar",
1121
+ ".gz",
1122
+ ".tgz",
1123
+ ".7z",
1124
+ ".rar",
1125
+ ".pdf",
1126
+ ".mp3",
1127
+ ".wav",
1128
+ ".ogg",
1129
+ ".mp4",
1130
+ ".avi",
1131
+ ".mov",
1132
+ ".exe",
1133
+ ".dll",
1134
+ ".so",
1135
+ ".bin",
1136
+ ".woff",
1137
+ ".woff2",
1138
+ ".ttf",
1139
+ ".otf",
1140
+ ]);
1141
+ return !binaryExtensions.has(ext);
1142
+ };
1143
+ const normalizePositiveInteger = (value, fallback, fieldName) => {
1144
+ const numeric = value === undefined ? fallback : Number(value);
1145
+ if (!Number.isInteger(numeric) || numeric < 1) {
1146
+ throw new Error(`${fieldName} 必须是大于等于 1 的整数`);
1147
+ }
1148
+ return numeric;
1149
+ };
1150
+ const getAgentFileList = (root, folder, depth, maxDepth, entries) => {
1151
+ if (entries.length >= AGENT_FILE_MAX_LIST_ENTRIES)
1152
+ return;
1153
+ for (const item of fs.readdirSync(folder, { withFileTypes: true })) {
1154
+ if (entries.length >= AGENT_FILE_MAX_LIST_ENTRIES)
1155
+ return;
1156
+ const fullPath = path.join(folder, item.name);
1157
+ assertPathInside(root, fullPath);
1158
+ const stat = fs.statSync(fullPath);
1159
+ const type = item.isDirectory() ? "directory" : "file";
1160
+ entries.push({
1161
+ path: path.relative(root, fullPath).replace(/\\/g, "/"),
1162
+ type,
1163
+ size: stat.size,
1164
+ modifiedAt: stat.mtime.toISOString(),
1165
+ });
1166
+ if (item.isDirectory() && depth < maxDepth) {
1167
+ getAgentFileList(root, fullPath, depth + 1, maxDepth, entries);
1168
+ }
1169
+ }
1170
+ };
1171
+ const searchFilesWithinWorkspace = ({ root, folder, query, mode, maxResults, }) => {
1172
+ const results = [];
1173
+ const lowerQuery = query.toLowerCase();
1174
+ const walk = (currentFolder) => {
1175
+ if (results.length >= maxResults)
1176
+ return;
1177
+ for (const item of fs.readdirSync(currentFolder, { withFileTypes: true })) {
1178
+ if (results.length >= maxResults)
1179
+ return;
1180
+ const fullPath = path.join(currentFolder, item.name);
1181
+ assertPathInside(root, fullPath);
1182
+ if (item.isDirectory()) {
1183
+ walk(fullPath);
1184
+ continue;
1185
+ }
1186
+ if (!item.isFile()) {
1187
+ continue;
1188
+ }
1189
+ const relativePath = path.relative(root, fullPath).replace(/\\/g, "/");
1190
+ if (mode !== "content" && item.name.toLowerCase().includes(lowerQuery)) {
1191
+ results.push({
1192
+ path: relativePath,
1193
+ type: "filename",
1194
+ preview: item.name,
1195
+ });
1196
+ if (results.length >= maxResults)
1197
+ return;
1198
+ }
1199
+ if (mode === "filename" || !isLikelyTextFile(fullPath)) {
1200
+ continue;
1201
+ }
1202
+ try {
1203
+ const lines = readTextLines(fullPath);
1204
+ for (let index = 0; index < lines.length; index += 1) {
1205
+ if (results.length >= maxResults)
1206
+ return;
1207
+ const line = lines[index];
1208
+ if (line.toLowerCase().includes(lowerQuery)) {
1209
+ results.push({
1210
+ path: relativePath,
1211
+ type: "content",
1212
+ lineNumber: index + 1,
1213
+ preview: line.trim().slice(0, 240),
1214
+ });
1215
+ }
1216
+ }
1217
+ }
1218
+ catch {
1219
+ continue;
1220
+ }
1221
+ }
1222
+ };
1223
+ walk(folder);
1224
+ return results;
1225
+ };
1226
+ const runGitClone = ({ repoUrl, target, branch, depth, }) => {
1227
+ return new Promise((resolve, reject) => {
1228
+ const args = ["clone", "--depth", String(depth)];
1229
+ if (branch) {
1230
+ args.push("--branch", branch);
1231
+ }
1232
+ args.push("--", repoUrl, target);
1233
+ const child = spawn("git", args, {
1234
+ env: {
1235
+ ...process.env,
1236
+ GIT_TERMINAL_PROMPT: "0",
1237
+ },
1238
+ });
1239
+ let output = "";
1240
+ const appendOutput = (data) => {
1241
+ output += data.toString();
1242
+ if (output.length > 12000) {
1243
+ output = output.slice(-12e3);
1244
+ }
1245
+ };
1246
+ const timeout = setTimeout(() => {
1247
+ child.kill();
1248
+ reject(new Error("git clone 超时"));
1249
+ }, 120000);
1250
+ child.stdout.on("data", appendOutput);
1251
+ child.stderr.on("data", appendOutput);
1252
+ child.on("error", (error) => {
1253
+ clearTimeout(timeout);
1254
+ reject(new Error(error.code === "ENOENT"
1255
+ ? "未找到 git 命令,无法克隆仓库"
1256
+ : `git clone 启动失败: ${error.message}`));
1257
+ });
1258
+ child.on("close", (code) => {
1259
+ clearTimeout(timeout);
1260
+ if (code === 0) {
1261
+ resolve(output.trim());
1262
+ return;
1263
+ }
1264
+ reject(new Error(`git clone 失败,退出码 ${code}${output ? `\n${output}` : ""}`));
1265
+ });
1266
+ });
1267
+ };
1268
+ const tokenizeCommand = (command) => {
1269
+ const tokens = [];
1270
+ let current = "";
1271
+ let quote = null;
1272
+ for (let i = 0; i < command.length; i += 1) {
1273
+ const char = command[i];
1274
+ if ((char === '"' || char === "'") && (!quote || quote === char)) {
1275
+ if (quote === char) {
1276
+ quote = null;
1277
+ }
1278
+ else {
1279
+ quote = char;
1280
+ }
1281
+ continue;
1282
+ }
1283
+ if (!quote && /\s/.test(char)) {
1284
+ if (current) {
1285
+ tokens.push(current);
1286
+ current = "";
1287
+ }
1288
+ continue;
1289
+ }
1290
+ current += char;
1291
+ }
1292
+ if (quote) {
1293
+ throw new Error("命令引号未闭合");
1294
+ }
1295
+ if (current) {
1296
+ tokens.push(current);
1297
+ }
1298
+ return tokens;
1299
+ };
1300
+ const trimProjectCommandOutput = (output) => output.length > PROJECT_COMMAND_MAX_OUTPUT
1301
+ ? `${output.slice(0, PROJECT_COMMAND_MAX_OUTPUT)}\n[output truncated]`
1302
+ : output;
1303
+ const isUnsafePathToken = (token) => {
1304
+ if (!token)
1305
+ return false;
1306
+ if (token.includes("\0"))
1307
+ return true;
1308
+ if (token === ".." || token.startsWith("../") || token.startsWith("..\\")) {
1309
+ return true;
1310
+ }
1311
+ if (/^[a-zA-Z]:[\\/]/.test(token)) {
1312
+ return true;
1313
+ }
1314
+ return false;
1315
+ };
1316
+ const buildProjectCommandReview = (command) => {
1317
+ const trimmed = command.trim();
1318
+ if (!trimmed) {
1319
+ return {
1320
+ allowed: false,
1321
+ risk: "blocked",
1322
+ reasons: ["命令不能为空"],
1323
+ baseCommand: "",
1324
+ tokens: [],
1325
+ };
1326
+ }
1327
+ for (const item of BLOCKED_PROJECT_COMMAND_PATTERNS) {
1328
+ if (item.pattern.test(trimmed)) {
1329
+ return {
1330
+ allowed: false,
1331
+ risk: "blocked",
1332
+ reasons: [item.reason],
1333
+ baseCommand: "",
1334
+ tokens: [],
1335
+ };
1336
+ }
1337
+ }
1338
+ let tokens;
1339
+ try {
1340
+ tokens = tokenizeCommand(trimmed);
1341
+ }
1342
+ catch (error) {
1343
+ return {
1344
+ allowed: false,
1345
+ risk: "blocked",
1346
+ reasons: [error instanceof Error ? error.message : String(error)],
1347
+ baseCommand: "",
1348
+ tokens: [],
1349
+ };
1350
+ }
1351
+ if (!tokens.length) {
1352
+ return {
1353
+ allowed: false,
1354
+ risk: "blocked",
1355
+ reasons: ["命令不能为空"],
1356
+ baseCommand: "",
1357
+ tokens,
1358
+ };
1359
+ }
1360
+ if (tokens.some(isUnsafePathToken)) {
1361
+ return {
1362
+ allowed: false,
1363
+ risk: "blocked",
1364
+ reasons: ["命令参数中包含试图离开 public 工作目录的路径"],
1365
+ baseCommand: "",
1366
+ tokens,
1367
+ };
1368
+ }
1369
+ const baseCommand = tokens[0].toLowerCase();
1370
+ if (!ALLOWED_PROJECT_COMMAND_BASES.has(baseCommand)) {
1371
+ return {
1372
+ allowed: false,
1373
+ risk: "blocked",
1374
+ reasons: ["仅允许受限的项目命令,当前基础命令不在允许列表中"],
1375
+ baseCommand,
1376
+ tokens,
1377
+ };
1378
+ }
1379
+ const reasons = [];
1380
+ let risk = "low";
1381
+ if (["node", "node.exe"].includes(baseCommand)) {
1382
+ if (!["-v", "--version"].includes(tokens[1] || "")) {
1383
+ return {
1384
+ allowed: false,
1385
+ risk: "blocked",
1386
+ reasons: ["node 仅允许用于版本检测"],
1387
+ baseCommand,
1388
+ tokens,
1389
+ };
1390
+ }
1391
+ reasons.push("版本检测命令");
1392
+ return {
1393
+ allowed: true,
1394
+ risk,
1395
+ reasons,
1396
+ baseCommand,
1397
+ tokens,
1398
+ };
1399
+ }
1400
+ if (["dotnet", "dotnet.exe"].includes(baseCommand)) {
1401
+ const subcommand = (tokens[1] || "").toLowerCase();
1402
+ if (!ALLOWED_DOTNET_PROJECT_SUBCOMMANDS.has(subcommand)) {
1403
+ return {
1404
+ allowed: false,
1405
+ risk: "blocked",
1406
+ reasons: ["dotnet 仅允许受限的项目和环境检测子命令"],
1407
+ baseCommand,
1408
+ tokens,
1409
+ };
1410
+ }
1411
+ risk = ["new", "restore", "build", "run", "test", "publish", "clean"].includes(subcommand)
1412
+ ? "medium"
1413
+ : "low";
1414
+ reasons.push(risk === "medium"
1415
+ ? ".NET 项目命令会创建、还原或编译工作目录内的项目"
1416
+ : ".NET 环境检测或受限项目查询命令");
1417
+ return {
1418
+ allowed: true,
1419
+ risk,
1420
+ reasons,
1421
+ baseCommand,
1422
+ tokens,
1423
+ };
1424
+ }
1425
+ if (["where", "where.exe"].includes(baseCommand)) {
1426
+ reasons.push("环境探测命令");
1427
+ return {
1428
+ allowed: true,
1429
+ risk,
1430
+ reasons,
1431
+ baseCommand,
1432
+ tokens,
1433
+ };
1434
+ }
1435
+ if (baseCommand === "git") {
1436
+ const subcommand = (tokens[1] || "").toLowerCase();
1437
+ if (!ALLOWED_GIT_PROJECT_SUBCOMMANDS.has(subcommand)) {
1438
+ return {
1439
+ allowed: false,
1440
+ risk: "blocked",
1441
+ reasons: ["git 仅允许受限的只读/初始化子命令"],
1442
+ baseCommand,
1443
+ tokens,
1444
+ };
1445
+ }
1446
+ risk = subcommand === "init" ? "medium" : "low";
1447
+ reasons.push(subcommand === "init" ? "git init 会创建仓库元数据" : "受限 git 查询命令");
1448
+ return {
1449
+ allowed: true,
1450
+ risk,
1451
+ reasons,
1452
+ baseCommand,
1453
+ tokens,
1454
+ };
1455
+ }
1456
+ if (tokens.includes("-g") || tokens.includes("--global")) {
1457
+ return {
1458
+ allowed: false,
1459
+ risk: "blocked",
1460
+ reasons: ["不允许全局安装或修改全局环境"],
1461
+ baseCommand,
1462
+ tokens,
1463
+ };
1464
+ }
1465
+ if (tokens.some((token) => /^https?:\/\//i.test(token))) {
1466
+ return {
1467
+ allowed: false,
1468
+ risk: "blocked",
1469
+ reasons: ["项目命令中不允许直接传入外部 URL"],
1470
+ baseCommand,
1471
+ tokens,
1472
+ };
1473
+ }
1474
+ const subcommand = (tokens[1] || "").toLowerCase();
1475
+ const blockedPackageManagerSubcommands = new Set([
1476
+ "publish",
1477
+ "login",
1478
+ "logout",
1479
+ "config",
1480
+ "cache",
1481
+ "owner",
1482
+ "team",
1483
+ ]);
1484
+ if (blockedPackageManagerSubcommands.has(subcommand)) {
1485
+ return {
1486
+ allowed: false,
1487
+ risk: "blocked",
1488
+ reasons: ["不允许执行账号、发布或全局配置相关命令"],
1489
+ baseCommand,
1490
+ tokens,
1491
+ };
1492
+ }
1493
+ if (["create", "init", "dlx", "exec"].includes(subcommand) || ["npx", "npx.cmd", "bunx"].includes(baseCommand)) {
1494
+ risk = "medium";
1495
+ reasons.push("脚手架或外部执行命令会下载并运行第三方包");
1496
+ }
1497
+ else if (["install", "i", "add", "ci"].includes(subcommand)) {
1498
+ risk = "medium";
1499
+ reasons.push("依赖安装命令会写入项目依赖和锁文件");
1500
+ }
1501
+ else if (["run", "test", "build"].includes(subcommand) || subcommand === "") {
1502
+ risk = "low";
1503
+ reasons.push("项目内脚本或常规包管理命令");
1504
+ }
1505
+ else {
1506
+ risk = "medium";
1507
+ reasons.push("已通过基础命令检查,但属于较少见的包管理子命令");
1508
+ }
1509
+ return {
1510
+ allowed: true,
1511
+ risk,
1512
+ reasons,
1513
+ baseCommand,
1514
+ tokens,
1515
+ };
1516
+ };
1517
+ const getReviewAIConfig = async (reviewGuid) => {
1518
+ const guid = reviewGuid?.trim();
1519
+ const config = await redisClient.getAIConfig(guid || undefined);
1520
+ const currentAI = guid ? await redisClient.getCurrentAI(guid) : "";
1521
+ if (!config.host.trim() || !config.key.trim() || !config.model.trim()) {
1522
+ return null;
1523
+ }
1524
+ return {
1525
+ guid: guid || "global",
1526
+ currentAI,
1527
+ config,
1528
+ };
1529
+ };
1530
+ const reviewProjectCommandWithAI = async ({ command, localReview, reviewGuid, }) => {
1531
+ const aiConfig = await getReviewAIConfig(reviewGuid);
1532
+ if (!aiConfig) {
1533
+ return {
1534
+ status: "skipped",
1535
+ reason: "未找到可用的当前 AI 配置,已仅使用本地规则审查",
1536
+ };
1537
+ }
1538
+ try {
1539
+ const openai = new OpenAi({
1540
+ baseURL: aiConfig.config.host,
1541
+ apiKey: aiConfig.config.key,
1542
+ timeout: 30000,
1543
+ });
1544
+ const response = await openai.chat.completions.create({
1545
+ model: aiConfig.config.model,
1546
+ temperature: 0,
1547
+ max_tokens: 200,
1548
+ messages: [
1549
+ {
1550
+ role: "system",
1551
+ content: "你是一个命令安全审查器。请只输出 JSON,格式为 {\"allowed\":boolean,\"risk\":\"low|medium|high|blocked\",\"reason\":\"简短中文原因\"}。审核标准:命令必须只适用于 public 工作目录内的安全项目操作,不得包含权限提升、删除系统文件、远程控制、数据外传、全局环境修改或危险 shell 行为。"
1552
+ + `\n性格约束:${aiConfig.config.systemPrompt || ""}`,
1553
+ },
1554
+ {
1555
+ role: "user",
1556
+ content: JSON.stringify({
1557
+ command,
1558
+ localReview: {
1559
+ allowed: localReview.allowed,
1560
+ risk: localReview.risk,
1561
+ reasons: localReview.reasons,
1562
+ },
1563
+ }),
339
1564
  },
340
- },
341
- },
342
- },
343
- ];
1565
+ ],
1566
+ });
1567
+ const text = response.choices[0]?.message?.content || "";
1568
+ const match = text.match(/\{[\s\S]*\}/);
1569
+ if (!match) {
1570
+ return {
1571
+ status: "failed",
1572
+ reason: "AI 审查未返回可解析的 JSON",
1573
+ raw: text,
1574
+ };
1575
+ }
1576
+ const parsed = JSON.parse(match[0]);
1577
+ return {
1578
+ status: "completed",
1579
+ currentAI: aiConfig.currentAI || null,
1580
+ model: aiConfig.config.model,
1581
+ guid: aiConfig.guid,
1582
+ allowed: Boolean(parsed.allowed),
1583
+ risk: parsed.risk === "low" ||
1584
+ parsed.risk === "medium" ||
1585
+ parsed.risk === "high" ||
1586
+ parsed.risk === "blocked"
1587
+ ? parsed.risk
1588
+ : "medium",
1589
+ reason: typeof parsed.reason === "string" && parsed.reason.trim()
1590
+ ? parsed.reason.trim()
1591
+ : "AI 未提供明确原因",
1592
+ };
1593
+ }
1594
+ catch (error) {
1595
+ return {
1596
+ status: "failed",
1597
+ reason: error instanceof Error ? error.message : String(error),
1598
+ };
1599
+ }
1600
+ };
1601
+ const runSpawnCommand = ({ file, args, cwd, }) => {
1602
+ return new Promise((resolve, reject) => {
1603
+ const child = spawn(file, args, { cwd });
1604
+ let output = "";
1605
+ const appendOutput = (data) => {
1606
+ output += data.toString();
1607
+ if (output.length > PROJECT_COMMAND_MAX_OUTPUT) {
1608
+ output = output.slice(-PROJECT_COMMAND_MAX_OUTPUT);
1609
+ }
1610
+ };
1611
+ const timeout = setTimeout(() => {
1612
+ child.kill();
1613
+ reject(new Error("命令执行超时"));
1614
+ }, PROJECT_COMMAND_TIMEOUT_MS);
1615
+ child.stdout.on("data", appendOutput);
1616
+ child.stderr.on("data", appendOutput);
1617
+ child.on("error", (error) => {
1618
+ clearTimeout(timeout);
1619
+ reject(new Error(`命令启动失败: ${error.message}`));
1620
+ });
1621
+ child.on("close", (exitCode) => {
1622
+ clearTimeout(timeout);
1623
+ resolve({
1624
+ output: trimProjectCommandOutput(output.trim()),
1625
+ exitCode: exitCode ?? -1,
1626
+ });
1627
+ });
1628
+ });
1629
+ };
1630
+ const findExecutablePaths = async (name, cwd) => {
1631
+ try {
1632
+ if (process.platform === "win32") {
1633
+ const result = await runSpawnCommand({
1634
+ file: "where.exe",
1635
+ args: [name],
1636
+ cwd,
1637
+ });
1638
+ if (result.exitCode !== 0 || !result.output) {
1639
+ return [];
1640
+ }
1641
+ return result.output
1642
+ .split(/\r?\n/)
1643
+ .map((line) => line.trim())
1644
+ .filter(Boolean);
1645
+ }
1646
+ const result = await runSpawnCommand({
1647
+ file: "which",
1648
+ args: [name],
1649
+ cwd,
1650
+ });
1651
+ if (result.exitCode !== 0 || !result.output) {
1652
+ return [];
1653
+ }
1654
+ return result.output
1655
+ .split(/\r?\n/)
1656
+ .map((line) => line.trim())
1657
+ .filter(Boolean);
1658
+ }
1659
+ catch {
1660
+ return [];
1661
+ }
1662
+ };
1663
+ const inspectProjectEnvironment = async (cwd) => {
1664
+ const versionCommands = [
1665
+ { name: "dotnet", file: process.platform === "win32" ? "dotnet.exe" : "dotnet", args: ["--version"] },
1666
+ process.platform === "win32"
1667
+ ? { name: "npm", file: "npm.cmd", args: ["-v"] }
1668
+ : { name: "npm", file: "npm", args: ["-v"] },
1669
+ process.platform === "win32"
1670
+ ? { name: "npx", file: "npx.cmd", args: ["-v"] }
1671
+ : { name: "npx", file: "npx", args: ["-v"] },
1672
+ { name: "node", file: process.platform === "win32" ? "node.exe" : "node", args: ["-v"] },
1673
+ { name: "git", file: "git", args: ["--version"] },
1674
+ ];
1675
+ const versions = {};
1676
+ const paths = {};
1677
+ for (const command of versionCommands) {
1678
+ try {
1679
+ const result = await runSpawnCommand({
1680
+ file: command.file,
1681
+ args: command.args,
1682
+ cwd,
1683
+ });
1684
+ versions[command.name] =
1685
+ result.exitCode === 0 ? result.output || "ok" : `exit ${result.exitCode}`;
1686
+ }
1687
+ catch (error) {
1688
+ versions[command.name] =
1689
+ error instanceof Error ? `unavailable: ${error.message}` : String(error);
1690
+ }
1691
+ paths[command.name] = await findExecutablePaths(command.name, cwd);
1692
+ }
1693
+ return {
1694
+ cwd,
1695
+ versions,
1696
+ paths,
1697
+ entries: fs.readdirSync(cwd, { withFileTypes: true }).slice(0, 50).map((item) => ({
1698
+ name: item.name,
1699
+ type: item.isDirectory() ? "directory" : "file",
1700
+ })),
1701
+ };
1702
+ };
1703
+ const extractArchiveWithinWorkspace = async ({ archive, destination, }) => {
1704
+ ensureParentDirectory(destination);
1705
+ fs.mkdirSync(destination, { recursive: true });
1706
+ const lower = archive.toLowerCase();
1707
+ if (lower.endsWith(".zip")) {
1708
+ if (process.platform === "win32") {
1709
+ return runSpawnCommand({
1710
+ file: "powershell.exe",
1711
+ args: [
1712
+ "-NoProfile",
1713
+ "-Command",
1714
+ `Expand-Archive -LiteralPath '${archive.replace(/'/g, "''")}' -DestinationPath '${destination.replace(/'/g, "''")}' -Force`,
1715
+ ],
1716
+ cwd: destination,
1717
+ });
1718
+ }
1719
+ return runSpawnCommand({
1720
+ file: "unzip",
1721
+ args: ["-o", archive, "-d", destination],
1722
+ cwd: destination,
1723
+ });
1724
+ }
1725
+ if (lower.endsWith(".tar") || lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) {
1726
+ return runSpawnCommand({
1727
+ file: "tar",
1728
+ args: ["-xf", archive, "-C", destination],
1729
+ cwd: destination,
1730
+ });
1731
+ }
1732
+ throw new Error("仅支持 zip、tar、tar.gz、tgz 压缩包");
1733
+ };
1734
+ const performProjectCommandReview = async ({ command, reviewGuid, }) => {
1735
+ const localReview = buildProjectCommandReview(command);
1736
+ const aiReview = await reviewProjectCommandWithAI({
1737
+ command,
1738
+ localReview,
1739
+ reviewGuid,
1740
+ });
1741
+ const allowed = localReview.allowed &&
1742
+ (aiReview.status !== "completed" || aiReview.allowed === true);
1743
+ const risk = aiReview.status === "completed" && aiReview.risk
1744
+ ? aiReview.risk
1745
+ : localReview.risk;
1746
+ return {
1747
+ allowed,
1748
+ risk,
1749
+ localReview,
1750
+ aiReview,
1751
+ };
1752
+ };
344
1753
  /**
345
1754
  * 确保 Docker 容器已创建并运行
346
1755
  */
@@ -348,10 +1757,15 @@ async function ensureContainerRunning(config) {
348
1757
  const { containerName, image, memory, cpus, pidsLimit, networkMode, workspacePath } = config;
349
1758
  if (!containerName)
350
1759
  return;
351
- return new Promise((resolve) => {
1760
+ return new Promise((resolve, reject) => {
352
1761
  const check = spawn("docker", ["inspect", "-f", "{{.State.Running}}", containerName]);
353
1762
  let stdout = "";
354
1763
  check.stdout.on("data", (d) => (stdout += d.toString()));
1764
+ check.on("error", (error) => {
1765
+ reject(new Error(error.code === "ENOENT"
1766
+ ? "Docker 不可用:未找到 docker 命令"
1767
+ : `Docker 检查失败:${error.message}`));
1768
+ });
355
1769
  check.on("close", (code) => {
356
1770
  if (code === 0 && stdout.trim() === "true") {
357
1771
  resolve();
@@ -359,7 +1773,17 @@ async function ensureContainerRunning(config) {
359
1773
  }
360
1774
  if (code === 0 && stdout.trim() === "false") {
361
1775
  const start = spawn("docker", ["start", containerName]);
362
- start.on("close", () => resolve());
1776
+ start.on("error", (error) => {
1777
+ reject(new Error(`Docker 启动容器失败:${error.message}`));
1778
+ });
1779
+ start.on("close", (startCode) => {
1780
+ if (startCode === 0) {
1781
+ resolve();
1782
+ }
1783
+ else {
1784
+ reject(new Error(`Docker 启动容器失败,退出码: ${startCode}`));
1785
+ }
1786
+ });
363
1787
  return;
364
1788
  }
365
1789
  const args = [
@@ -377,9 +1801,13 @@ async function ensureContainerRunning(config) {
377
1801
  const create = spawn("docker", args);
378
1802
  let createStderr = "";
379
1803
  create.stderr.on("data", (d) => (createStderr += d.toString()));
1804
+ create.on("error", (error) => {
1805
+ reject(new Error(`Docker 创建容器失败:${error.message}`));
1806
+ });
380
1807
  create.on("close", (code) => {
381
1808
  if (code !== 0) {
382
- console.error(`[Docker] 容器创建失败: ${containerName}`, createStderr);
1809
+ reject(new Error(`Docker 创建容器失败: ${containerName}${createStderr ? `\n${createStderr}` : ""}`));
1810
+ return;
383
1811
  }
384
1812
  resolve();
385
1813
  });
@@ -411,16 +1839,30 @@ const availableTools = {
411
1839
  return { success: true, data: userData };
412
1840
  }
413
1841
  if (dataType === "chatHistory") {
414
- // 从redis获取聊天历史, key为chatHistory:${userID}
415
- const [guid, id] = data?.split(":") || [];
1842
+ if (!data) {
1843
+ const summaryList = await redisClient.getSummaryList();
1844
+ return { success: true, data: summaryList };
1845
+ }
1846
+ if (data.startsWith("search:")) {
1847
+ const query = data.slice("search:".length).trim();
1848
+ const records = await redisClient.searchArchivedChatRecords(query, {
1849
+ limit: 10,
1850
+ });
1851
+ return { success: true, data: records };
1852
+ }
1853
+ const [guid, id] = data.split(":") || [];
416
1854
  if (guid && id) {
1855
+ const record = await redisClient.getArchivedChatRecord(guid, id);
1856
+ if (record) {
1857
+ return { success: true, data: record };
1858
+ }
417
1859
  const chatHistory = await redisClient.getSummaryDetail(guid, id);
418
1860
  return { success: true, data: chatHistory };
419
1861
  }
420
- else {
421
- const summaryList = await redisClient.getSummaryList();
422
- return { success: true, data: summaryList };
423
- }
1862
+ return {
1863
+ success: false,
1864
+ error: "chatHistory data 只能为空、guid:id search:关键词",
1865
+ };
424
1866
  }
425
1867
  }
426
1868
  },
@@ -784,6 +2226,13 @@ const availableTools = {
784
2226
  */
785
2227
  exec: async ({ userId, groupId, command }) => {
786
2228
  const workspace = getWorkspace(userId);
2229
+ if (commandTargetsAgentWorkspace(command || "")) {
2230
+ return [
2231
+ "exec 当前工作目录不是 public/{guid}/,不能可靠查找 AI agent 工作区里的项目。",
2232
+ `exec 当前默认工作目录: ${workspace}`,
2233
+ "如果要查找之前克隆到 public/{guid}/knowledge/ 的仓库,请改用 AgentListWorkspaces 先找 guid,再用 AgentListFiles 或 AgentSearchFiles。",
2234
+ ].join("\n");
2235
+ }
787
2236
  // 使用新的环境管理获取执行环境
788
2237
  const envInfo = await getExecCommandEnv(groupId || "private", userId);
789
2238
  // 远程节点环境:通过 SSH 执行
@@ -823,6 +2272,9 @@ const availableTools = {
823
2272
  const child = spawn("sh", ["-c", command], { cwd: workspace });
824
2273
  child.stdout.on("data", (d) => (output += d.toString()));
825
2274
  child.stderr.on("data", (d) => (output += d.toString()));
2275
+ child.on("error", (error) => {
2276
+ resolve(`系统执行失败: ${error.message}`);
2277
+ });
826
2278
  child.on("close", () => resolve(output));
827
2279
  });
828
2280
  }
@@ -832,7 +2284,12 @@ const availableTools = {
832
2284
  if (!containerName) {
833
2285
  return "错误:未找到执行环境配置,请先切换执行环境";
834
2286
  }
835
- await ensureContainerRunning(envConfig);
2287
+ try {
2288
+ await ensureContainerRunning(envConfig);
2289
+ }
2290
+ catch (err) {
2291
+ return `容器执行环境不可用: ${err.message}`;
2292
+ }
836
2293
  return new Promise((resolve) => {
837
2294
  let output = "";
838
2295
  const args = ["exec"];
@@ -843,9 +2300,472 @@ const availableTools = {
843
2300
  const child = spawn("docker", args);
844
2301
  child.stdout.on("data", (d) => (output += d.toString()));
845
2302
  child.stderr.on("data", (d) => (output += d.toString()));
2303
+ child.on("error", (error) => {
2304
+ resolve(`Docker 执行失败: ${error.message}`);
2305
+ });
846
2306
  child.on("close", () => resolve(output));
847
2307
  });
848
2308
  },
2309
+ AgentListWorkspaces: async ({ includeKnowledge = true, maxResults = 50, }) => {
2310
+ try {
2311
+ const workspaces = listAgentWorkspaces(includeKnowledge, maxResults);
2312
+ return {
2313
+ success: true,
2314
+ count: workspaces.length,
2315
+ workspaces,
2316
+ };
2317
+ }
2318
+ catch (error) {
2319
+ return toToolError(error);
2320
+ }
2321
+ },
2322
+ AgentCreateWorkspace: async ({ guid }) => {
2323
+ try {
2324
+ const workspaceGuid = createAgentGuid(guid);
2325
+ return {
2326
+ success: true,
2327
+ guid: workspaceGuid,
2328
+ path: `public/${workspaceGuid}`,
2329
+ };
2330
+ }
2331
+ catch (error) {
2332
+ return toToolError(error);
2333
+ }
2334
+ },
2335
+ AgentCloneRepository: async ({ guid, repoUrl, targetPath, branch, depth = 1, }) => {
2336
+ try {
2337
+ await validateRepositoryUrl(repoUrl);
2338
+ if (branch) {
2339
+ validateGitRef(branch);
2340
+ }
2341
+ const safeDepth = normalizeCloneDepth(depth);
2342
+ const cloneTargetPath = resolveCloneTargetPath(repoUrl, targetPath);
2343
+ const { target, relativePath } = resolveAgentPath(guid, cloneTargetPath);
2344
+ if (fs.existsSync(target)) {
2345
+ throw new Error("目标目录已存在,为避免覆盖请换一个 targetPath");
2346
+ }
2347
+ ensureParentDirectory(target);
2348
+ const output = await runGitClone({
2349
+ repoUrl,
2350
+ target,
2351
+ branch,
2352
+ depth: safeDepth,
2353
+ });
2354
+ assertRealPathInside(resolveAgentRoot(guid), target);
2355
+ return {
2356
+ success: true,
2357
+ guid,
2358
+ repoUrl,
2359
+ path: relativePath,
2360
+ depth: safeDepth,
2361
+ branch: branch || null,
2362
+ output,
2363
+ };
2364
+ }
2365
+ catch (error) {
2366
+ return toToolError(error);
2367
+ }
2368
+ },
2369
+ AgentReviewProjectCommand: async ({ guid, command, workingDirectory = ".", reviewGuid, }) => {
2370
+ try {
2371
+ const { relativePath } = resolveAgentPath(guid, workingDirectory);
2372
+ const review = await performProjectCommandReview({
2373
+ command,
2374
+ reviewGuid,
2375
+ });
2376
+ return {
2377
+ success: true,
2378
+ guid,
2379
+ workingDirectory: relativePath,
2380
+ command,
2381
+ ...review,
2382
+ };
2383
+ }
2384
+ catch (error) {
2385
+ return toToolError(error);
2386
+ }
2387
+ },
2388
+ AgentGitOperation: async ({ guid, action, workingDirectory = ".", branch, includeRemote = true, }) => {
2389
+ try {
2390
+ const { target: cwd, relativePath } = resolveAgentPath(guid, workingDirectory);
2391
+ await ensureGitRepository(cwd);
2392
+ if (action === "currentBranch") {
2393
+ const currentBranch = await getGitCurrentBranch(cwd);
2394
+ return {
2395
+ success: true,
2396
+ guid,
2397
+ action,
2398
+ workingDirectory: relativePath,
2399
+ branch: currentBranch || null,
2400
+ };
2401
+ }
2402
+ if (action === "listBranches") {
2403
+ const branches = await listGitBranches(cwd, includeRemote !== false);
2404
+ return {
2405
+ success: true,
2406
+ guid,
2407
+ action,
2408
+ workingDirectory: relativePath,
2409
+ branches,
2410
+ };
2411
+ }
2412
+ if (action === "listRemotes") {
2413
+ const remotes = await listGitRemotes(cwd);
2414
+ return {
2415
+ success: true,
2416
+ guid,
2417
+ action,
2418
+ workingDirectory: relativePath,
2419
+ remotes,
2420
+ };
2421
+ }
2422
+ if (action === "switchBranch") {
2423
+ if (!branch) {
2424
+ throw new Error("action=switchBranch 时必须提供 branch");
2425
+ }
2426
+ const switched = await switchGitBranch(cwd, branch);
2427
+ const currentBranch = await getGitCurrentBranch(cwd);
2428
+ return {
2429
+ success: true,
2430
+ guid,
2431
+ action,
2432
+ workingDirectory: relativePath,
2433
+ branch: currentBranch || branch,
2434
+ source: switched.source,
2435
+ output: switched.output,
2436
+ };
2437
+ }
2438
+ throw new Error("不支持的 Git action");
2439
+ }
2440
+ catch (error) {
2441
+ return toToolError(error);
2442
+ }
2443
+ },
2444
+ AgentProjectCommand: async ({ guid, action, workingDirectory = ".", command, reviewGuid, archivePath, destinationPath, sourcePath, targetPath, directoryPath, }) => {
2445
+ try {
2446
+ const { target: cwd, relativePath } = resolveAgentPath(guid, workingDirectory);
2447
+ if (action === "inspect") {
2448
+ const info = await inspectProjectEnvironment(cwd);
2449
+ return {
2450
+ success: true,
2451
+ guid,
2452
+ action,
2453
+ workingDirectory: relativePath,
2454
+ ...info,
2455
+ };
2456
+ }
2457
+ if (action === "mkdir") {
2458
+ if (!directoryPath) {
2459
+ throw new Error("action=mkdir 时必须提供 directoryPath");
2460
+ }
2461
+ const created = resolveAgentPath(guid, directoryPath);
2462
+ fs.mkdirSync(created.target, { recursive: true });
2463
+ return {
2464
+ success: true,
2465
+ guid,
2466
+ action,
2467
+ path: created.relativePath,
2468
+ };
2469
+ }
2470
+ if (action === "rename") {
2471
+ if (!sourcePath || !targetPath) {
2472
+ throw new Error("action=rename 时必须提供 sourcePath 和 targetPath");
2473
+ }
2474
+ const source = resolveAgentPath(guid, sourcePath);
2475
+ const target = resolveAgentPath(guid, targetPath);
2476
+ if (!fs.existsSync(source.target)) {
2477
+ throw new Error("源路径不存在");
2478
+ }
2479
+ if (fs.existsSync(target.target)) {
2480
+ throw new Error("目标路径已存在,为避免覆盖请更换 targetPath");
2481
+ }
2482
+ ensureParentDirectory(target.target);
2483
+ fs.renameSync(source.target, target.target);
2484
+ return {
2485
+ success: true,
2486
+ guid,
2487
+ action,
2488
+ sourcePath: source.relativePath,
2489
+ targetPath: target.relativePath,
2490
+ };
2491
+ }
2492
+ if (action === "extract") {
2493
+ if (!archivePath) {
2494
+ throw new Error("action=extract 时必须提供 archivePath");
2495
+ }
2496
+ const archive = resolveAgentPath(guid, archivePath);
2497
+ if (!fs.existsSync(archive.target) || !fs.statSync(archive.target).isFile()) {
2498
+ throw new Error("压缩包不存在或不是文件");
2499
+ }
2500
+ const destination = resolveAgentPath(guid, destinationPath || workingDirectory || ".");
2501
+ const result = await extractArchiveWithinWorkspace({
2502
+ archive: archive.target,
2503
+ destination: destination.target,
2504
+ });
2505
+ return {
2506
+ success: result.exitCode === 0,
2507
+ guid,
2508
+ action,
2509
+ archivePath: archive.relativePath,
2510
+ destinationPath: destination.relativePath,
2511
+ output: result.output,
2512
+ exitCode: result.exitCode,
2513
+ error: result.exitCode === 0 ? undefined : "解压命令执行失败",
2514
+ };
2515
+ }
2516
+ if (action === "run") {
2517
+ if (!command) {
2518
+ throw new Error("action=run 时必须提供 command");
2519
+ }
2520
+ const review = await performProjectCommandReview({
2521
+ command,
2522
+ reviewGuid,
2523
+ });
2524
+ if (!review.allowed) {
2525
+ return {
2526
+ success: false,
2527
+ guid,
2528
+ action,
2529
+ command,
2530
+ workingDirectory: relativePath,
2531
+ error: "命令未通过安全审查",
2532
+ review,
2533
+ };
2534
+ }
2535
+ const localReview = review.localReview;
2536
+ const result = await runSpawnCommand({
2537
+ file: localReview.tokens[0],
2538
+ args: localReview.tokens.slice(1),
2539
+ cwd,
2540
+ });
2541
+ return {
2542
+ success: result.exitCode === 0,
2543
+ guid,
2544
+ action,
2545
+ command,
2546
+ workingDirectory: relativePath,
2547
+ review,
2548
+ output: result.output,
2549
+ exitCode: result.exitCode,
2550
+ error: result.exitCode === 0 ? undefined : "项目命令执行失败",
2551
+ };
2552
+ }
2553
+ throw new Error("不支持的 action");
2554
+ }
2555
+ catch (error) {
2556
+ return toToolError(error);
2557
+ }
2558
+ },
2559
+ AgentReadFileLines: async ({ guid, filePath, startLine = 1, endLine, }) => {
2560
+ try {
2561
+ const { target, relativePath } = resolveAgentPath(guid, filePath);
2562
+ if (!fs.existsSync(target)) {
2563
+ throw new Error("文件不存在");
2564
+ }
2565
+ if (!fs.statSync(target).isFile()) {
2566
+ throw new Error("目标路径不是文件");
2567
+ }
2568
+ const lines = readTextLines(target);
2569
+ const safeStartLine = normalizePositiveInteger(startLine, 1, "startLine");
2570
+ const requestedEndLine = normalizePositiveInteger(endLine, Math.min(lines.length || 1, safeStartLine + 199), "endLine");
2571
+ const safeEndLine = Math.min(requestedEndLine, safeStartLine + AGENT_FILE_MAX_READ_LINES - 1, lines.length);
2572
+ if (requestedEndLine < safeStartLine) {
2573
+ throw new Error("endLine 不能小于 startLine");
2574
+ }
2575
+ const content = lines
2576
+ .slice(safeStartLine - 1, safeEndLine)
2577
+ .map((line, index) => `${safeStartLine + index}: ${line}`)
2578
+ .join("\n");
2579
+ return {
2580
+ success: true,
2581
+ path: relativePath,
2582
+ startLine: safeStartLine,
2583
+ endLine: safeEndLine,
2584
+ totalLines: lines.length,
2585
+ truncated: requestedEndLine > safeEndLine,
2586
+ content,
2587
+ };
2588
+ }
2589
+ catch (error) {
2590
+ return toToolError(error);
2591
+ }
2592
+ },
2593
+ AgentWriteFile: async ({ guid, filePath, content, mode = "overwrite", }) => {
2594
+ try {
2595
+ const { target, relativePath } = resolveAgentPath(guid, filePath);
2596
+ ensureParentDirectory(target);
2597
+ if (mode === "append") {
2598
+ fs.appendFileSync(target, content, "utf-8");
2599
+ }
2600
+ else if (mode === "overwrite") {
2601
+ fs.writeFileSync(target, content, "utf-8");
2602
+ }
2603
+ else {
2604
+ throw new Error("mode 只能是 overwrite 或 append");
2605
+ }
2606
+ return {
2607
+ success: true,
2608
+ path: relativePath,
2609
+ mode,
2610
+ bytes: Buffer.byteLength(content, "utf-8"),
2611
+ };
2612
+ }
2613
+ catch (error) {
2614
+ return toToolError(error);
2615
+ }
2616
+ },
2617
+ AgentInsertFile: async ({ guid, filePath, lineNumber, content, }) => {
2618
+ try {
2619
+ const { target, relativePath } = resolveAgentPath(guid, filePath);
2620
+ if (!fs.existsSync(target)) {
2621
+ throw new Error("文件不存在");
2622
+ }
2623
+ if (!fs.statSync(target).isFile()) {
2624
+ throw new Error("目标路径不是文件");
2625
+ }
2626
+ const lines = readTextLines(target);
2627
+ const safeLineNumber = normalizePositiveInteger(lineNumber, 1, "lineNumber");
2628
+ if (safeLineNumber > lines.length + 1) {
2629
+ throw new Error("lineNumber 不能大于总行数 + 1");
2630
+ }
2631
+ lines.splice(safeLineNumber - 1, 0, ...content.split(/\r?\n/));
2632
+ fs.writeFileSync(target, lines.join("\n"), "utf-8");
2633
+ return {
2634
+ success: true,
2635
+ path: relativePath,
2636
+ lineNumber: safeLineNumber,
2637
+ insertedLines: content.split(/\r?\n/).length,
2638
+ totalLines: lines.length,
2639
+ };
2640
+ }
2641
+ catch (error) {
2642
+ return toToolError(error);
2643
+ }
2644
+ },
2645
+ AgentCreateFile: async ({ guid, filePath, content = "", overwrite = false, }) => {
2646
+ try {
2647
+ const { target, relativePath } = resolveAgentPath(guid, filePath);
2648
+ const existed = fs.existsSync(target);
2649
+ if (existed && !overwrite) {
2650
+ throw new Error("文件已存在,如需覆盖请传 overwrite=true");
2651
+ }
2652
+ ensureParentDirectory(target);
2653
+ fs.writeFileSync(target, content, "utf-8");
2654
+ return {
2655
+ success: true,
2656
+ path: relativePath,
2657
+ overwritten: existed && overwrite,
2658
+ bytes: Buffer.byteLength(content, "utf-8"),
2659
+ };
2660
+ }
2661
+ catch (error) {
2662
+ return toToolError(error);
2663
+ }
2664
+ },
2665
+ AgentDeleteFile: async ({ guid, filePath, }) => {
2666
+ try {
2667
+ const { target, relativePath } = resolveAgentPath(guid, filePath);
2668
+ if (!fs.existsSync(target)) {
2669
+ throw new Error("文件不存在");
2670
+ }
2671
+ if (!fs.statSync(target).isFile()) {
2672
+ throw new Error("只能删除文件,不能删除文件夹");
2673
+ }
2674
+ fs.unlinkSync(target);
2675
+ return {
2676
+ success: true,
2677
+ path: relativePath,
2678
+ deleted: true,
2679
+ };
2680
+ }
2681
+ catch (error) {
2682
+ return toToolError(error);
2683
+ }
2684
+ },
2685
+ AgentListFiles: async ({ guid, folderPath = ".", recursive = false, maxDepth = 3, }) => {
2686
+ try {
2687
+ const { root, target, relativePath } = resolveAgentPath(guid, folderPath);
2688
+ if (!fs.existsSync(target)) {
2689
+ throw new Error("文件夹不存在");
2690
+ }
2691
+ if (!fs.statSync(target).isDirectory()) {
2692
+ throw new Error("目标路径不是文件夹");
2693
+ }
2694
+ const entries = [];
2695
+ getAgentFileList(root, target, 1, recursive ? Math.max(1, Math.min(Number(maxDepth) || 3, 10)) : 1, entries);
2696
+ return {
2697
+ success: true,
2698
+ path: relativePath,
2699
+ recursive,
2700
+ truncated: entries.length >= AGENT_FILE_MAX_LIST_ENTRIES,
2701
+ entries,
2702
+ };
2703
+ }
2704
+ catch (error) {
2705
+ return toToolError(error);
2706
+ }
2707
+ },
2708
+ AgentSearchFiles: async ({ guid, query, folderPath = ".", mode = "both", maxResults = 20, }) => {
2709
+ try {
2710
+ const { root, target, relativePath } = resolveAgentPath(guid, folderPath);
2711
+ if (!query?.trim()) {
2712
+ throw new Error("query 不能为空");
2713
+ }
2714
+ if (!fs.existsSync(target)) {
2715
+ throw new Error("搜索目录不存在");
2716
+ }
2717
+ if (!fs.statSync(target).isDirectory()) {
2718
+ throw new Error("搜索起点必须是文件夹");
2719
+ }
2720
+ const safeMaxResults = Math.max(1, Math.min(Number(maxResults) || 20, AGENT_FILE_MAX_SEARCH_RESULTS));
2721
+ const results = searchFilesWithinWorkspace({
2722
+ root,
2723
+ folder: target,
2724
+ query: query.trim(),
2725
+ mode,
2726
+ maxResults: safeMaxResults,
2727
+ });
2728
+ return {
2729
+ success: true,
2730
+ path: relativePath,
2731
+ query: query.trim(),
2732
+ mode,
2733
+ maxResults: safeMaxResults,
2734
+ total: results.length,
2735
+ results,
2736
+ };
2737
+ }
2738
+ catch (error) {
2739
+ return toToolError(error);
2740
+ }
2741
+ },
2742
+ AgentUploadFileToR2: async ({ guid, filePath, fileName, uploadType = "file", }) => {
2743
+ try {
2744
+ const { target, relativePath } = resolveAgentPath(guid, filePath);
2745
+ if (!fs.existsSync(target)) {
2746
+ throw new Error("文件不存在");
2747
+ }
2748
+ if (!fs.statSync(target).isFile()) {
2749
+ throw new Error("只能上传文件,不能上传文件夹");
2750
+ }
2751
+ const url = uploadType === "image"
2752
+ ? await uploadImageToR2(target, fileName)
2753
+ : await uploadMediaToR2(target, fileName);
2754
+ return {
2755
+ success: true,
2756
+ path: relativePath,
2757
+ fileName: fileName || null,
2758
+ uploadType,
2759
+ url,
2760
+ sendText: uploadType === "image"
2761
+ ? `<img=${url}>`
2762
+ : `文件链接: ${url}`,
2763
+ };
2764
+ }
2765
+ catch (error) {
2766
+ return toToolError(error);
2767
+ }
2768
+ },
849
2769
  // /**
850
2770
  // * 执行eval
851
2771
  // * @param code 代码字符串