@vrs-soft/wecom-aibot-mcp 2.3.2 → 2.4.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/dist/daemon.js CHANGED
@@ -47,23 +47,7 @@ class ConnectionDaemon {
47
47
  }
48
48
  loadAllRobots() {
49
49
  const robots = [];
50
- // 主配置文件
51
- const mainConfigPath = path.join(CONFIG_DIR, 'config.json');
52
- if (fs.existsSync(mainConfigPath)) {
53
- try {
54
- const config = JSON.parse(fs.readFileSync(mainConfigPath, 'utf-8'));
55
- robots.push({
56
- name: config.nameTag || 'default',
57
- botId: config.botId,
58
- secret: config.secret,
59
- targetUserId: config.targetUserId,
60
- });
61
- }
62
- catch (err) {
63
- this.log(`加载主配置失败: ${err}`);
64
- }
65
- }
66
- // 机器人配置文件 (robot-*.json)
50
+ // 所有机器人配置(统一 robot-*.json 格式)
67
51
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
68
52
  for (const file of files) {
69
53
  try {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 文档 MCP 代理模块
3
+ *
4
+ * 将文档工具调用代理转发到机器人专属的企业微信文档 MCP 服务(StreamableHTTP)。
5
+ * 每次调用建立独立连接,无需维护长连接状态。
6
+ */
7
+ export interface DocToolResult {
8
+ content: Array<{
9
+ type: string;
10
+ text: string;
11
+ }>;
12
+ isError?: boolean;
13
+ }
14
+ /**
15
+ * 调用机器人文档 MCP 工具
16
+ *
17
+ * @param docMcpUrl 机器人专属的文档 MCP URL(含 uaKey)
18
+ * @param toolName 工具名称,如 create_doc、smartsheet_add_records 等
19
+ * @param args 工具参数
20
+ */
21
+ export declare function callDocTool(docMcpUrl: string, toolName: string, args: Record<string, unknown>): Promise<DocToolResult>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 文档 MCP 代理模块
3
+ *
4
+ * 将文档工具调用代理转发到机器人专属的企业微信文档 MCP 服务(StreamableHTTP)。
5
+ * 每次调用建立独立连接,无需维护长连接状态。
6
+ */
7
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
9
+ import { logger } from './logger.js';
10
+ /**
11
+ * 调用机器人文档 MCP 工具
12
+ *
13
+ * @param docMcpUrl 机器人专属的文档 MCP URL(含 uaKey)
14
+ * @param toolName 工具名称,如 create_doc、smartsheet_add_records 等
15
+ * @param args 工具参数
16
+ */
17
+ export async function callDocTool(docMcpUrl, toolName, args) {
18
+ const client = new Client({ name: 'wecom-aibot-doc-proxy', version: '1.0.0' }, { capabilities: {} });
19
+ try {
20
+ const transport = new StreamableHTTPClientTransport(new URL(docMcpUrl));
21
+ await client.connect(transport);
22
+ const result = await client.callTool({ name: toolName, arguments: args });
23
+ return {
24
+ content: result.content ?? [
25
+ { type: 'text', text: JSON.stringify(result) },
26
+ ],
27
+ isError: result.isError,
28
+ };
29
+ }
30
+ catch (err) {
31
+ logger.error(`[doc-proxy] 调用 ${toolName} 失败:`, err);
32
+ const message = err?.message ?? String(err);
33
+ return {
34
+ content: [{ type: 'text', text: `文档工具调用失败: ${message}\n\n请检查机器人的文档 MCP URL 是否正确,或该机器人是否已授权文档能力。` }],
35
+ isError: true,
36
+ };
37
+ }
38
+ finally {
39
+ try {
40
+ await client.close();
41
+ }
42
+ catch {
43
+ // 关闭时忽略错误
44
+ }
45
+ }
46
+ }
@@ -23,7 +23,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
23
23
  import { registerTools } from './tools/index.js';
24
24
  import { getClient, getConnectionState, getAllConnectionStates, connectAllRobots } from './connection-manager.js';
25
25
  import { subscribeWecomMessage, getSubscriberCount } from './message-bus.js';
26
- import { listAllRobots } from './config-wizard.js';
26
+ import { listAllRobots, VERSION, getAuthToken } from './config-wizard.js';
27
27
  import { logger } from './logger.js';
28
28
  // ESM 兼容的 __dirname
29
29
  const __filename = fileURLToPath(import.meta.url);
@@ -185,7 +185,6 @@ export function getOnlineCcIds() {
185
185
  }
186
186
  // 使用 Map 存储多个待处理审批(按 taskId 索引)
187
187
  const pendingApprovals = new Map();
188
- const VERSION = '2.0.0';
189
188
  const transports = new Map();
190
189
  const sseClients = new Map(); // clientId -> SSEClient
191
190
  // 初始化 MCP Server(不再全局连接)
@@ -479,6 +478,16 @@ export async function startHttpServer(_server, port = HTTP_PORT) {
479
478
  return;
480
479
  }
481
480
  const url = req.url || '/';
481
+ // Auth token 校验(排除 /health)
482
+ const authToken = getAuthToken();
483
+ if (authToken && url !== '/health') {
484
+ const authHeader = req.headers['authorization'];
485
+ if (authHeader !== `Bearer ${authToken}`) {
486
+ res.writeHead(401, { 'Content-Type': 'application/json' });
487
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
488
+ return;
489
+ }
490
+ }
482
491
  // MCP endpoint - 每个客户端一个独立的 server 和 transport
483
492
  // POST /mcp: 初始化或调用工具
484
493
  // GET /mcp: 建立 SSE 流
@@ -20,7 +20,8 @@
20
20
  * - 从 Session 自动获取 robotName
21
21
  */
22
22
  import { z } from 'zod';
23
- import { listAllRobots, installSkill } from '../config-wizard.js';
23
+ import { listAllRobots, getDocMcpUrl, installSkill, VERSION } from '../config-wizard.js';
24
+ import { callDocTool } from '../doc-proxy.js';
24
25
  import { connectRobot, disconnectRobot, getClient, getConnectionState, } from '../connection-manager.js';
25
26
  import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, } from '../http-server.js';
26
27
  import { subscribeWecomMessageByCcId } from '../message-bus.js';
@@ -249,7 +250,7 @@ npx @vrs-soft/wecom-aibot-mcp
249
250
  content: [{
250
251
  type: 'text',
251
252
  text: JSON.stringify({
252
- version: '2.0.0',
253
+ version: VERSION,
253
254
  requirements: {
254
255
  // 权限配置需求
255
256
  permissions: {
@@ -312,13 +313,15 @@ npx @vrs-soft/wecom-aibot-mcp
312
313
  bot_id: z.string().describe('企业微信 Bot ID'),
313
314
  secret: z.string().describe('机器人密钥'),
314
315
  default_user: z.string().optional().describe('默认目标用户'),
315
- }, async ({ name, bot_id, secret, default_user }) => {
316
+ doc_mcp_url: z.string().optional().describe('机器人文档 MCP URL(企业微信文档能力)'),
317
+ }, async ({ name, bot_id, secret, default_user, doc_mcp_url }) => {
316
318
  return {
317
319
  content: [{
318
320
  type: 'text',
319
321
  text: JSON.stringify({
320
322
  message: '请使用 --add 命令添加机器人配置',
321
323
  command: `npx @vrs-soft/wecom-aibot-mcp --add`,
324
+ tip: doc_mcp_url ? '运行向导时在"文档 MCP URL"提示处粘贴您的 URL' : undefined,
322
325
  }, null, 2),
323
326
  }],
324
327
  };
@@ -334,6 +337,7 @@ npx @vrs-soft/wecom-aibot-mcp
334
337
  name: robot.name,
335
338
  botId: robot.botId?.slice(0, 12) + '...', // 只显示前12位
336
339
  targetUser: robot.targetUserId,
340
+ hasDocMcp: !!robot.doc_mcp_url, // 是否配置了文档 MCP 能力
337
341
  }));
338
342
  return {
339
343
  content: [{
@@ -413,11 +417,11 @@ npx @vrs-soft/wecom-aibot-mcp
413
417
  }],
414
418
  };
415
419
  }
416
- // 如果用户传入 cc_id,直接使用;否则自动生成
417
- const finalCcId = cc_id || generateCcId(effectiveAgentName);
418
- // 检查 wecom-aibot.json 是否已有匹配的 ccId(重连场景)
420
+ // 检查 wecom-aibot.json 是否已有 ccId(重连场景)
419
421
  const existingConfig = loadWechatModeConfig(projectDir);
420
- const isReconnect = existingConfig?.ccId === finalCcId;
422
+ // 优先级:cc_id 参数 > (非 agent_name 指定时) 配置文件已有 ccId > 自动生成
423
+ const finalCcId = cc_id || (!agent_name && existingConfig?.ccId) || generateCcId(effectiveAgentName);
424
+ const isReconnect = !!existingConfig?.ccId && existingConfig.ccId === finalCcId;
421
425
  // 注册 ccId 到 CC 注册表(重连直接覆盖,首次注册清理超时条目)
422
426
  registerCcId(finalCcId, selectedRobot.name, effectiveAgentName, mode, projectDir, isReconnect);
423
427
  // 更新项目配置文件中的 wechatMode 为 true
@@ -598,5 +602,205 @@ npx @vrs-soft/wecom-aibot-mcp
598
602
  }],
599
603
  };
600
604
  });
601
- logger.log('[mcp] 已注册 14 个工具');
605
+ // ============================================
606
+ // 文档代理工具(企业微信文档 MCP 能力)
607
+ // 代理转发到机器人专属的 doc_mcp_url
608
+ // ============================================
609
+ // 公共辅助:解析 doc MCP URL
610
+ function resolveDocUrl(robotName) {
611
+ const { url, error } = getDocMcpUrl(robotName);
612
+ if (!url) {
613
+ return {
614
+ url: null,
615
+ errorContent: { content: [{ type: 'text', text: error ?? '未配置文档 MCP URL' }] },
616
+ };
617
+ }
618
+ return { url };
619
+ }
620
+ server.tool('create_doc', '新建文档或智能表格。doc_type: 3=文档, 10=智能表格。创建成功后返回文档链接。', {
621
+ doc_type: z.number().int().describe('文档类型:3=文档,10=智能表格'),
622
+ doc_name: z.string().describe('文档名称,最多 255 个字符'),
623
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
624
+ }, async ({ doc_type, doc_name, robot_name }) => {
625
+ const { url, errorContent } = resolveDocUrl(robot_name);
626
+ if (!url)
627
+ return errorContent;
628
+ return callDocTool(url, 'create_doc', { doc_type, doc_name });
629
+ });
630
+ server.tool('get_doc_content', '获取企业微信文档内容(Markdown 格式)。支持异步轮询:首次调用无需 task_id,若 task_done 为 false 则带 task_id 继续轮询。', {
631
+ type: z.number().int().describe('内容格式:2=Markdown'),
632
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
633
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
634
+ task_id: z.string().optional().describe('任务 ID(轮询时填写)'),
635
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
636
+ }, async ({ type, url: docUrl, docid, task_id, robot_name }) => {
637
+ const { url, errorContent } = resolveDocUrl(robot_name);
638
+ if (!url)
639
+ return errorContent;
640
+ return callDocTool(url, 'get_doc_content', { type, url: docUrl, docid, task_id });
641
+ });
642
+ server.tool('edit_doc_content', '编辑企业微信文档内容,支持 Markdown 格式覆写。', {
643
+ content: z.string().describe('覆写的文档内容'),
644
+ content_type: z.number().int().describe('内容类型:1=Markdown'),
645
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
646
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
647
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
648
+ }, async ({ content, content_type, url: docUrl, docid, robot_name }) => {
649
+ const { url, errorContent } = resolveDocUrl(robot_name);
650
+ if (!url)
651
+ return errorContent;
652
+ return callDocTool(url, 'edit_doc_content', { content, content_type, url: docUrl, docid });
653
+ });
654
+ server.tool('smartsheet_get_sheet', '查询智能表格中的子表信息。', {
655
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
656
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
657
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
658
+ }, async ({ url: docUrl, docid, robot_name }) => {
659
+ const { url, errorContent } = resolveDocUrl(robot_name);
660
+ if (!url)
661
+ return errorContent;
662
+ return callDocTool(url, 'smartsheet_get_sheet', { url: docUrl, docid });
663
+ });
664
+ server.tool('smartsheet_add_sheet', '在智能表格内添加新子表。', {
665
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
666
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
667
+ properties: z.object({ title: z.string().optional() }).optional().describe('子表属性,可设置 title'),
668
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
669
+ }, async ({ url: docUrl, docid, properties, robot_name }) => {
670
+ const { url, errorContent } = resolveDocUrl(robot_name);
671
+ if (!url)
672
+ return errorContent;
673
+ return callDocTool(url, 'smartsheet_add_sheet', { url: docUrl, docid, properties });
674
+ });
675
+ server.tool('smartsheet_update_sheet', '修改智能表格子表的标题。', {
676
+ properties: z.object({
677
+ sheet_id: z.string().describe('子表 ID'),
678
+ title: z.string().describe('新标题'),
679
+ }).describe('子表属性'),
680
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
681
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
682
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
683
+ }, async ({ properties, url: docUrl, docid, robot_name }) => {
684
+ const { url, errorContent } = resolveDocUrl(robot_name);
685
+ if (!url)
686
+ return errorContent;
687
+ return callDocTool(url, 'smartsheet_update_sheet', { properties, url: docUrl, docid });
688
+ });
689
+ server.tool('smartsheet_delete_sheet', '删除智能表格中的子表(不可逆)。', {
690
+ sheet_id: z.string().describe('要删除的子表 ID'),
691
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
692
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
693
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
694
+ }, async ({ sheet_id, url: docUrl, docid, robot_name }) => {
695
+ const { url, errorContent } = resolveDocUrl(robot_name);
696
+ if (!url)
697
+ return errorContent;
698
+ return callDocTool(url, 'smartsheet_delete_sheet', { sheet_id, url: docUrl, docid });
699
+ });
700
+ server.tool('smartsheet_get_fields', '获取智能表格子表的字段(列)信息。', {
701
+ sheet_id: z.string().describe('子表 ID'),
702
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
703
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
704
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
705
+ }, async ({ sheet_id, url: docUrl, docid, robot_name }) => {
706
+ const { url, errorContent } = resolveDocUrl(robot_name);
707
+ if (!url)
708
+ return errorContent;
709
+ return callDocTool(url, 'smartsheet_get_fields', { sheet_id, url: docUrl, docid });
710
+ });
711
+ server.tool('smartsheet_add_fields', '向智能表格子表添加字段(列)。调用前必须先用 smartsheet_get_fields 查看现有字段并用 smartsheet_update_fields 重命名默认字段,否则会多出无用列。', {
712
+ sheet_id: z.string().describe('子表 ID'),
713
+ fields: z.array(z.object({
714
+ field_title: z.string(),
715
+ field_type: z.string(),
716
+ })).describe('要添加的字段列表'),
717
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
718
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
719
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
720
+ }, async ({ sheet_id, fields, url: docUrl, docid, robot_name }) => {
721
+ const { url, errorContent } = resolveDocUrl(robot_name);
722
+ if (!url)
723
+ return errorContent;
724
+ return callDocTool(url, 'smartsheet_add_fields', { sheet_id, fields, url: docUrl, docid });
725
+ });
726
+ server.tool('smartsheet_update_fields', '更新智能表格子表字段的标题(不能更改字段类型)。', {
727
+ sheet_id: z.string().describe('子表 ID'),
728
+ fields: z.array(z.object({
729
+ field_id: z.string(),
730
+ field_title: z.string(),
731
+ field_type: z.string(),
732
+ })).describe('要更新的字段列表'),
733
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
734
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
735
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
736
+ }, async ({ sheet_id, fields, url: docUrl, docid, robot_name }) => {
737
+ const { url, errorContent } = resolveDocUrl(robot_name);
738
+ if (!url)
739
+ return errorContent;
740
+ return callDocTool(url, 'smartsheet_update_fields', { sheet_id, fields, url: docUrl, docid });
741
+ });
742
+ server.tool('smartsheet_delete_fields', '删除智能表格子表中的字段(列)(不可逆)。', {
743
+ sheet_id: z.string().describe('子表 ID'),
744
+ field_ids: z.array(z.string()).describe('要删除的字段 ID 列表'),
745
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
746
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
747
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
748
+ }, async ({ sheet_id, field_ids, url: docUrl, docid, robot_name }) => {
749
+ const { url, errorContent } = resolveDocUrl(robot_name);
750
+ if (!url)
751
+ return errorContent;
752
+ return callDocTool(url, 'smartsheet_delete_fields', { sheet_id, field_ids, url: docUrl, docid });
753
+ });
754
+ server.tool('smartsheet_get_records', '查询智能表格子表的所有记录。', {
755
+ sheet_id: z.string().describe('子表 ID'),
756
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
757
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
758
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
759
+ }, async ({ sheet_id, url: docUrl, docid, robot_name }) => {
760
+ const { url, errorContent } = resolveDocUrl(robot_name);
761
+ if (!url)
762
+ return errorContent;
763
+ return callDocTool(url, 'smartsheet_get_records', { sheet_id, url: docUrl, docid });
764
+ });
765
+ server.tool('smartsheet_add_records', '向智能表格子表添加记录(行)。单次建议不超过 500 行。values 的 key 必须是字段标题(field_title),不能用 field_id。', {
766
+ sheet_id: z.string().describe('子表 ID'),
767
+ records: z.array(z.object({ values: z.record(z.string(), z.unknown()) })).describe('记录列表,values key 为字段标题'),
768
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
769
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
770
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
771
+ }, async ({ sheet_id, records, url: docUrl, docid, robot_name }) => {
772
+ const { url, errorContent } = resolveDocUrl(robot_name);
773
+ if (!url)
774
+ return errorContent;
775
+ return callDocTool(url, 'smartsheet_add_records', { sheet_id, records, url: docUrl, docid });
776
+ });
777
+ server.tool('smartsheet_update_records', '更新智能表格子表中的记录(行)。单次建议不超过 500 行。', {
778
+ sheet_id: z.string().describe('子表 ID'),
779
+ records: z.array(z.object({
780
+ record_id: z.string(),
781
+ values: z.record(z.string(), z.unknown()),
782
+ })).describe('要更新的记录列表,含 record_id 和 values'),
783
+ key_type: z.enum(['CELL_VALUE_KEY_TYPE_FIELD_TITLE', 'CELL_VALUE_KEY_TYPE_FIELD_ID']).describe('values 的 key 类型'),
784
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
785
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
786
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
787
+ }, async ({ sheet_id, records, key_type, url: docUrl, docid, robot_name }) => {
788
+ const { url, errorContent } = resolveDocUrl(robot_name);
789
+ if (!url)
790
+ return errorContent;
791
+ return callDocTool(url, 'smartsheet_update_records', { sheet_id, records, key_type, url: docUrl, docid });
792
+ });
793
+ server.tool('smartsheet_delete_records', '删除智能表格子表中的记录(行)(不可逆)。单次建议不超过 500 行。', {
794
+ sheet_id: z.string().describe('子表 ID'),
795
+ record_ids: z.array(z.string()).describe('要删除的记录 ID 列表'),
796
+ url: z.string().optional().describe('文档访问链接,与 docid 二选一'),
797
+ docid: z.string().optional().describe('文档 docid,与 url 二选一'),
798
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
799
+ }, async ({ sheet_id, record_ids, url: docUrl, docid, robot_name }) => {
800
+ const { url, errorContent } = resolveDocUrl(robot_name);
801
+ if (!url)
802
+ return errorContent;
803
+ return callDocTool(url, 'smartsheet_delete_records', { sheet_id, record_ids, url: docUrl, docid });
804
+ });
805
+ logger.log('[mcp] 已注册 29 个工具(含 15 个文档代理工具)');
602
806
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",