@vrs-soft/wecom-aibot-mcp 2.6.0 → 3.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.
@@ -327,19 +327,48 @@ function connectSSE(ccId) {
327
327
  let notification;
328
328
  if (currentEvent === 'cc_message') {
329
329
  // CC 间消息:用 cc:<fromCc> 作为 source 前缀,便于 agent 区分非 wecom 来源
330
+ const meta = {
331
+ source: `cc:${msg.fromCc || ''}`,
332
+ from_cc: msg.fromCc || '',
333
+ to_cc: msg.toCc || '',
334
+ chattype: 'cc',
335
+ cc_id: msg.toCc || '',
336
+ kind: msg.kind || 'notify',
337
+ reply_to: msg.replyTo || '',
338
+ msg_id: msg.msgId || '',
339
+ };
340
+ if (Array.isArray(msg.attachments) && msg.attachments.length > 0) {
341
+ meta.attachments_json = JSON.stringify(msg.attachments);
342
+ meta.attachment_count = String(msg.attachments.length);
343
+ }
330
344
  notification = {
331
345
  method: 'notifications/claude/channel',
332
346
  params: {
333
347
  content: msg.content || '',
348
+ meta,
349
+ },
350
+ };
351
+ }
352
+ else if (currentEvent === 'cc_document_notify') {
353
+ // CC 间文档通知:只含元数据,agent 收到后按需 fetch_document(doc_id)
354
+ const sizeKb = Math.max(1, Math.round((msg.size || 0) / 1024));
355
+ const text = `📎 收到文档「${msg.title || ''}」(${msg.mimeType || ''}, ~${sizeKb} KB),发送方=${msg.fromCc || ''},docId=${msg.docId || ''}。调用 fetch_document(cc_id, doc_id="${msg.docId || ''}") 取内容。`;
356
+ notification = {
357
+ method: 'notifications/claude/channel',
358
+ params: {
359
+ content: text,
334
360
  meta: {
335
361
  source: `cc:${msg.fromCc || ''}`,
336
362
  from_cc: msg.fromCc || '',
337
363
  to_cc: msg.toCc || '',
338
364
  chattype: 'cc',
339
365
  cc_id: msg.toCc || '',
340
- kind: msg.kind || 'notify',
341
- reply_to: msg.replyTo || '',
342
- msg_id: msg.msgId || '',
366
+ kind: 'document',
367
+ doc_id: msg.docId || '',
368
+ title: msg.title || '',
369
+ mime_type: msg.mimeType || '',
370
+ size: String(msg.size || 0),
371
+ expires_at: String(msg.expiresAt || 0),
343
372
  },
344
373
  },
345
374
  };
@@ -454,17 +483,80 @@ function registerChannelTools(server) {
454
483
  // ============================================
455
484
  // 工具 4a: CC 间通信 — send_to_cc / list_active_ccs(v2.6.0+)
456
485
  // ============================================
457
- server.tool('send_to_cc', '向同一 daemon 上的另一个 CC 发送消息。目标 CC 收到时会作为 <channel source="cc:..."> 推送唤醒。仅支持同 daemon 间互通。', {
486
+ server.tool('send_to_cc', '向同一 daemon 上的另一个 CC 发送消息。目标 CC 收到时会作为 <channel source="cc:..."> 推送唤醒。仅支持同 daemon 间互通。支持 attachments 内联小文档(每个 < 16 KB);大文档请改用 upload_document。', {
458
487
  cc_id: z.string().describe('自己的 CC 标识'),
459
488
  to_cc: z.string().describe('目标 CC 标识'),
460
489
  content: z.string().describe('消息内容(支持 Markdown)'),
461
490
  kind: z.enum(['request', 'reply', 'notify']).optional().default('notify').describe('消息语义'),
462
491
  reply_to: z.string().optional().describe('可选:关联的请求 msgId'),
492
+ attachments: z.array(z.object({
493
+ title: z.string(),
494
+ content: z.string(),
495
+ mimeType: z.string().optional(),
496
+ })).optional().describe('可选:内联附件,每个 content < 16 KB'),
463
497
  }, async (params) => forwardToHttpMcp('send_to_cc', params));
464
498
  server.tool('list_active_ccs', '列出同一 daemon 上当前在线的所有 CC', {
465
499
  cc_id: z.string().describe('自己的 CC 标识'),
466
500
  }, async (params) => forwardToHttpMcp('list_active_ccs', params));
467
501
  // ============================================
502
+ // CC 间文档传输(v2.7.0+)— upload_document / fetch_document / list_documents
503
+ // ============================================
504
+ server.tool('upload_document', 'CC 间传输较大文档(> 16 KB 或二进制)。服务端暂存(默认 30 分钟 TTL),向目标 CC 推送 cc_document_notify 事件,接收方按需 fetch_document(docId) 取内容。', {
505
+ cc_id: z.string().describe('自己的 CC 标识'),
506
+ to_cc: z.string().describe('目标 CC 标识'),
507
+ title: z.string().describe('文档标题'),
508
+ content: z.string().describe('文档内容。文本直接传字符串;二进制(图片、PDF 等)传 base64 并设 encoding=base64'),
509
+ mime_type: z.string().optional().describe('MIME 类型,如 text/markdown / image/png'),
510
+ encoding: z.enum(['utf8', 'base64']).optional().default('utf8').describe('内容编码'),
511
+ ttl_seconds: z.number().int().min(60).max(86400).optional().describe('暂存秒数,默认 1800,最大 86400'),
512
+ }, async (params) => forwardToHttpMcp('upload_document', params));
513
+ server.tool('fetch_document', '按 docId 拉取 CC 间暂存的文档内容。仅文档接收方(to_cc)可取。图片自动返回 MCP image content(Claude 可视觉识别)。', {
514
+ cc_id: z.string().describe('自己的 CC 标识(必须 = 文档的 to_cc)'),
515
+ doc_id: z.string().describe('文档 ID(从 cc_document_notify 或 list_documents 获得)'),
516
+ }, async (params) => forwardToHttpMcp('fetch_document', params));
517
+ server.tool('list_documents', '列出当前 CC 收到的所有未过期文档元数据(不含 content)。', {
518
+ cc_id: z.string().describe('自己的 CC 标识'),
519
+ from_cc: z.string().optional().describe('可选:只列指定发送方的文档'),
520
+ include_expired: z.boolean().optional().describe('是否包含已过期,默认 false'),
521
+ }, async (params) => forwardToHttpMcp('list_documents', params));
522
+ // ============================================
523
+ // 共享文件池(v2.7.0+)— share_file / fetch_shared_file / list_shared_files
524
+ // 与 upload_document 不同:无指定接收方,所有 CC 可见
525
+ // ============================================
526
+ server.tool('share_file', '上传一个共享文件到 MCP 服务端,所有在线 CC 都可通过 fetch_shared_file 取用。支持 inline content 或 file_path(server 直读,避免大文件经 Claude context)。默认 5 MB / 30 分钟 TTL。', {
527
+ cc_id: z.string().describe('自己的 CC 标识(owner)'),
528
+ title: z.string().describe('文件标题'),
529
+ content: z.string().optional().describe('二选一:内联内容'),
530
+ file_path: z.string().optional().describe('二选一:server 端文件绝对路径(大文件用,避免走 Claude context)'),
531
+ mime_type: z.string().optional().describe('MIME 类型;file_path 模式按扩展名推断'),
532
+ encoding: z.enum(['utf8', 'base64']).optional().describe('content 模式下的编码;file_path 自动判定'),
533
+ ttl_seconds: z.number().int().min(60).max(86400).optional().describe('暂存秒数,默认 1800'),
534
+ tags: z.array(z.string()).optional().describe('可选标签'),
535
+ }, async (params) => forwardToHttpMcp('share_file', params));
536
+ server.tool('fetch_shared_file', '按 fileId 拉取共享文件内容。任何在线 CC 都能取。图片自动返回 MCP image content(Claude 可视觉识别)。', {
537
+ cc_id: z.string().describe('自己的 CC 标识'),
538
+ file_id: z.string().describe('共享文件 ID'),
539
+ }, async (params) => forwardToHttpMcp('fetch_shared_file', params));
540
+ server.tool('list_shared_files', '列出共享文件池中未过期的文件元数据(不含 content)。', {
541
+ cc_id: z.string().describe('自己的 CC 标识'),
542
+ owner: z.string().optional().describe('可选:按 owner 过滤'),
543
+ tag: z.string().optional().describe('可选:按标签过滤'),
544
+ include_expired: z.boolean().optional().describe('是否包含已过期,默认 false'),
545
+ }, async (params) => forwardToHttpMcp('list_shared_files', params));
546
+ // ============================================
547
+ // 接收方落盘工具(v2.7.0+)
548
+ // ============================================
549
+ server.tool('accept_document', '接受 upload_document 推来的文档:fetch 内容 + 落盘到 {projectDir}/received-file/。【强制】必须先通过 send_message 询问用户、得到肯定回复后才能调用。', {
550
+ cc_id: z.string().describe('自己的 CC 标识'),
551
+ doc_id: z.string().describe('文档 ID'),
552
+ save_as: z.string().optional().describe('可选:自定义文件名'),
553
+ }, async (params) => forwardToHttpMcp('accept_document', params));
554
+ server.tool('accept_shared_file', '从共享池下载文件并落盘到 {projectDir}/received-file/。共享池为 pull 模型,无强制询问。', {
555
+ cc_id: z.string().describe('自己的 CC 标识'),
556
+ file_id: z.string().describe('共享文件 ID'),
557
+ save_as: z.string().optional().describe('可选:自定义文件名'),
558
+ }, async (params) => forwardToHttpMcp('accept_shared_file', params));
559
+ // ============================================
468
560
  // 工具 4: 获取待处理消息
469
561
  // ============================================
470
562
  server.tool('get_pending_messages', '获取待处理的微信消息。支持长轮询:传入 timeout_ms 后阻塞等待,有消息立即返回,无消息等到超时。超时后继续轮询,不要停止。', {
@@ -542,15 +634,21 @@ function registerChannelTools(server) {
542
634
  const hookResult = addPermissionHook(localProjectDir);
543
635
  logger.info('本地 PermissionRequest hook 已写入', { path: hookResult.path, success: hookResult.success });
544
636
  // 注册 Claude TUI 的 PID(不能用 process.ppid,npx 部署时 ppid 是 npx 不是 Claude)
545
- const startPid = process.ppid ?? process.pid;
546
- const claudePid = findClaudePid(startPid);
547
- registerActiveProject(claudePid, localProjectDir);
548
- logger.info('本地 active-projects 已注册', {
549
- pid: claudePid,
550
- rawPpid: startPid,
551
- resolvedClaudePid: claudePid !== startPid,
552
- projectDir: localProjectDir,
553
- });
637
+ // WECOM_TEST_NO_REGISTER=1 时跳过注册,防止集成测试污染宿主机 active-projects.json
638
+ if (!process.env.WECOM_TEST_NO_REGISTER) {
639
+ const startPid = process.ppid ?? process.pid;
640
+ const claudePid = findClaudePid(startPid);
641
+ registerActiveProject(claudePid, localProjectDir);
642
+ logger.info('本地 active-projects 已注册', {
643
+ pid: claudePid,
644
+ rawPpid: startPid,
645
+ resolvedClaudePid: claudePid !== startPid,
646
+ projectDir: localProjectDir,
647
+ });
648
+ }
649
+ else {
650
+ logger.info('WECOM_TEST_NO_REGISTER=1,跳过 active-projects 注册', { projectDir: localProjectDir });
651
+ }
554
652
  // 写入本地 wecom-aibot.json(远程 HTTP MCP 写在远端 fs,agent 本地需要自己落地)
555
653
  updateWechatModeConfig(localProjectDir, {
556
654
  wechatMode: true,
@@ -643,19 +741,19 @@ function registerChannelTools(server) {
643
741
  // 文档代理工具(转发到 HTTP MCP)
644
742
  // ============================================
645
743
  const docTools = [
646
- ['create_doc', '新建文档或智能表格', {
744
+ ['create_wecom_doc', '新建企业微信在线文档或智能表格', {
647
745
  doc_type: z.number().int().describe('文档类型:3=文档,10=智能表格'),
648
746
  doc_name: z.string().describe('文档名称'),
649
747
  robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
650
748
  }],
651
- ['get_doc_content', '获取文档内容(Markdown 格式)', {
749
+ ['get_wecom_doc_content', '获取企业微信在线文档内容(Markdown 格式)', {
652
750
  type: z.number().int().describe('内容格式:2=Markdown'),
653
751
  url: z.string().optional().describe('文档链接'),
654
752
  docid: z.string().optional().describe('文档 docid'),
655
753
  task_id: z.string().optional().describe('任务 ID(轮询时填写)'),
656
754
  robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
657
755
  }],
658
- ['edit_doc_content', '编辑文档内容(Markdown 格式覆写)', {
756
+ ['edit_wecom_doc_content', '编辑企业微信在线文档内容(Markdown 格式覆写)', {
659
757
  content: z.string().describe('覆写的文档内容'),
660
758
  content_type: z.number().int().describe('内容类型:1=Markdown'),
661
759
  url: z.string().optional().describe('文档链接'),
@@ -747,7 +845,7 @@ export async function startChannelServer() {
747
845
  tools: {},
748
846
  },
749
847
  // 告知 Claude 如何处理 channel 事件
750
- instructions: '企业微信消息通过 <channel> 标签推送。属性说明:from=发送者userid, chatid=会话ID(单聊=用户ID,群聊=群ID), chattype=single|group, cc_id=当前会话标识。【强制规则】收到任何用户消息后,必须先执行步骤1再执行步骤2,禁止跳过:1) 立即发送确认 send_message(cc_id, "收到,正在处理...", target_user=chatid);2) 处理任务;3) 发送结果 send_message(cc_id, "【完成】...", target_user=chatid)',
848
+ instructions: '企业微信消息通过 <channel> 标签推送。属性说明:from=发送者userid, chatid=会话ID(单聊=用户ID,群聊=群ID), chattype=single|group, cc_id=当前会话标识。【强制规则1·用户消息】收到任何用户消息后,必须先执行步骤1再执行步骤2,禁止跳过:1) 立即发送确认 send_message(cc_id, "收到,正在处理...", target_user=chatid);2) 处理任务;3) 发送结果 send_message(cc_id, "【完成】...", target_user=chatid)。【强制规则2·CC 间文档】收到 <channel kind="document"> 通知(cc_document_notify)时,禁止擅自落盘:1) 立即 send_message 告知用户("CC <from_cc> 想发送文件「<title>」(<size>, <mime_type>),是否接收?"),target_user 取当前 cc 的 chatid;2) 等用户明确肯定回复(是/接受/yes/同意等);3) 同意后调 accept_document(cc_id, doc_id) 落盘到 {projectDir}/received-file/;4) 发送完成消息并附 saved_path;拒绝则忽略 doc_id 不调 fetch_document。共享池 share_file/fetch_shared_file 为 pull 模型,agent 主动决定,无需询问。',
751
849
  });
752
850
  // 注册工具
753
851
  registerChannelTools(mcpServer);
@@ -1,46 +1,11 @@
1
- export interface WecomConfig {
2
- botId: string;
3
- secret: string;
4
- targetUserId: string;
5
- targetUserName?: string;
6
- nameTag?: string;
7
- doc_mcp_url?: string;
8
- }
9
1
  export declare const VERSION: string;
10
- export declare function loadConfig(): WecomConfig | null;
11
2
  export declare function getAuthToken(): string | undefined;
12
- export declare function setAuthToken(token: string | undefined): boolean;
13
- export declare function getHttpsConfig(): {
14
- certPath: string;
15
- keyPath: string;
16
- } | null;
17
- export declare function setHttpsConfig(certPath: string, keyPath: string): boolean;
18
- export declare function updateMcpAuthHeaders(token?: string): void;
19
- export declare function listAllMcpInstances(): Array<{
20
- name: string;
21
- config: WecomConfig;
22
- }>;
23
3
  export declare function deleteConfig(): void;
24
4
  export declare function deleteHook(): void;
25
5
  export declare function deleteSkills(): void;
26
- export declare function deleteRobotConfig(robotName: string): boolean;
27
- export declare function deleteMcpConfig(instanceName: string): boolean;
28
- export declare function deleteRobotConfigInteractive(robotName?: string): Promise<void>;
29
- export declare function deleteMcpConfigInteractive(instanceName?: string): Promise<void>;
30
6
  export declare function uninstall(): void;
31
- export declare function addMcpConfig(): Promise<void>;
32
- export declare function listAllRobots(): Array<{
33
- name: string;
34
- botId: string;
35
- targetUserId: string;
36
- doc_mcp_url?: string;
37
- }>;
38
- export declare function getDocMcpUrl(robotName?: string): {
39
- url: string | null;
40
- error?: string;
41
- };
42
7
  export declare function ensureHookInstalled(): void;
43
- export type InstallMode = 'full' | 'http-only' | 'channel-only' | 'remote' | 'remote-channel';
8
+ export type InstallMode = 'channel-only' | 'remote' | 'remote-channel';
44
9
  export declare function getInstalledMode(): {
45
10
  mode?: InstallMode;
46
11
  remote?: {
@@ -55,8 +20,7 @@ export declare function ensureGlobalConfigs(mode?: InstallMode, remoteOptions?:
55
20
  upgraded: boolean;
56
21
  previousVersion?: string;
57
22
  };
58
- export declare function runRemoteInstallWizard(): Promise<'remote' | 'remote-channel' | 'server' | null>;
59
- export declare function saveConfig(config: WecomConfig, instanceName?: string): boolean;
23
+ export declare function runRemoteInstallWizard(): Promise<'remote' | 'remote-channel' | null>;
60
24
  /**
61
25
  * 安装 headless-mode skill 到项目目录
62
26
  * 返回:{ success, skillUrl? } - 如果模板不存在,返回 HTTP endpoint URL
@@ -66,26 +30,3 @@ export declare function installSkill(projectDir: string): {
66
30
  skillUrl?: string;
67
31
  message?: string;
68
32
  };
69
- /**
70
- * 运行配置向导
71
- */
72
- export declare function runConfigWizard(): Promise<{
73
- config: WecomConfig;
74
- instanceName: string;
75
- }>;
76
- export declare function findRobotConfigFile(robotName: string): string | null;
77
- export declare function findRobotConfigFileByBotId(botId: string): string | null;
78
- export declare function isRobotNameExists(name: string, excludeBotId?: string): boolean;
79
- /**
80
- * 通过等待用户消息来识别用户 ID(使用已有的 client)
81
- */
82
- export declare function detectUserIdFromMessage(client: any, timeoutSeconds?: number): Promise<string | null>;
83
- /**
84
- * 检查并获取配置
85
- *
86
- * 优先级:
87
- * 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
88
- * 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
89
- * 3. 运行配置向导
90
- */
91
- export declare function getOrInitConfig(): Promise<WecomConfig>;