@vrs-soft/wecom-aibot-mcp 2.4.25 → 2.6.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/bin.js +20 -5
- package/dist/channel-server.js +91 -28
- package/dist/client.d.ts +5 -0
- package/dist/client.js +49 -0
- package/dist/config-wizard.d.ts +9 -1
- package/dist/config-wizard.js +52 -6
- package/dist/hooks/permission-hook.d.ts +2 -0
- package/dist/hooks/permission-hook.js +325 -0
- package/dist/hooks/stop-hook.d.ts +2 -0
- package/dist/hooks/stop-hook.js +101 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/http-server.js +64 -4
- package/dist/message-bus.d.ts +15 -1
- package/dist/message-bus.js +12 -1
- package/dist/platform.d.ts +14 -0
- package/dist/platform.js +107 -0
- package/dist/tools/index.js +104 -5
- package/package.json +1 -1
package/dist/platform.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台辅助函数(Windows / macOS / Linux)
|
|
3
|
+
*
|
|
4
|
+
* 用于替代 ps / lsof / ss / kill 等 Unix 专属命令,
|
|
5
|
+
* 让 daemon 启停、Claude 进程树查找在 Windows 也能工作。
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
const IS_WIN = process.platform === 'win32';
|
|
9
|
+
/** 通过 fetch /health 探测 daemon 是否在该端口监听(跨平台) */
|
|
10
|
+
export async function isDaemonAlive(port, timeoutMs = 1500) {
|
|
11
|
+
try {
|
|
12
|
+
const ctrl = new AbortController();
|
|
13
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
14
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: ctrl.signal });
|
|
15
|
+
clearTimeout(t);
|
|
16
|
+
if (!res.ok)
|
|
17
|
+
return false;
|
|
18
|
+
const data = await res.json().catch(() => ({}));
|
|
19
|
+
return data?.status === 'ok';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** 取指定 PID 的父进程 PID;不存在返回 0 */
|
|
26
|
+
export function getParentPid(pid) {
|
|
27
|
+
if (!pid || pid <= 1)
|
|
28
|
+
return 0;
|
|
29
|
+
try {
|
|
30
|
+
if (IS_WIN) {
|
|
31
|
+
// 输出形如:
|
|
32
|
+
// ParentProcessId
|
|
33
|
+
// 1234
|
|
34
|
+
const out = execSync(`wmic process where ProcessId=${pid} get ParentProcessId /value`, {
|
|
35
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
36
|
+
}).toString();
|
|
37
|
+
const m = out.match(/ParentProcessId=(\d+)/);
|
|
38
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const out = execSync(`ps -o ppid= -p ${pid}`, {
|
|
42
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
+
}).toString().trim();
|
|
44
|
+
return parseInt(out, 10) || 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** 取指定 PID 的可执行文件名(comm 字段,如 "claude" / "node") */
|
|
52
|
+
export function getProcessName(pid) {
|
|
53
|
+
if (!pid || pid <= 1)
|
|
54
|
+
return '';
|
|
55
|
+
try {
|
|
56
|
+
if (IS_WIN) {
|
|
57
|
+
// wmic process where ProcessId=N get Name /value -> Name=node.exe
|
|
58
|
+
const out = execSync(`wmic process where ProcessId=${pid} get Name /value`, {
|
|
59
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
60
|
+
}).toString();
|
|
61
|
+
const m = out.match(/Name=(.+?)\s*$/m);
|
|
62
|
+
return (m ? m[1] : '').trim();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return execSync(`ps -p ${pid} -o comm=`, {
|
|
66
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
67
|
+
}).toString().trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 沿进程树向上查找 Claude Code 进程的 PID。
|
|
76
|
+
* 用于 channel-server 注册 active-projects 时定位真正的 TUI 进程
|
|
77
|
+
* (npx 安装下 process.ppid 是 npx 不是 claude)。
|
|
78
|
+
*/
|
|
79
|
+
export function findClaudePid(startPid, maxDepth = 8) {
|
|
80
|
+
let pid = startPid;
|
|
81
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
82
|
+
if (!pid || pid <= 1)
|
|
83
|
+
break;
|
|
84
|
+
const name = getProcessName(pid).toLowerCase();
|
|
85
|
+
// Win 上是 "claude.exe";Unix 上可能是 "claude" 或绝对路径末尾 "/claude"
|
|
86
|
+
if (name === 'claude' || name === 'claude.exe' || name.endsWith('/claude') || name.endsWith('\\claude.exe')) {
|
|
87
|
+
return pid;
|
|
88
|
+
}
|
|
89
|
+
const parent = getParentPid(pid);
|
|
90
|
+
if (!parent || parent === pid)
|
|
91
|
+
break;
|
|
92
|
+
pid = parent;
|
|
93
|
+
}
|
|
94
|
+
return startPid;
|
|
95
|
+
}
|
|
96
|
+
/** 进程是否还在(process.kill(pid, 0) 在 Win/Unix 都可用) */
|
|
97
|
+
export function isProcessAlive(pid) {
|
|
98
|
+
if (!pid)
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
process.kill(pid, 0);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { listAllRobots, getDocMcpUrl, installSkill, VERSION } from '../config-wizard.js';
|
|
4
4
|
import { callDocTool } from '../doc-proxy.js';
|
|
5
|
-
import { connectRobot, disconnectRobot, getClient, getConnectionState, } from '../connection-manager.js';
|
|
6
|
-
import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, } from '../http-server.js';
|
|
7
|
-
import { subscribeWecomMessageByCcId } from '../message-bus.js';
|
|
5
|
+
import { connectRobot, disconnectRobot, getClient, getConnectionState, getAllConnectionStates, } from '../connection-manager.js';
|
|
6
|
+
import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, getOnlineCcIds, getCCRegistryEntry, } from '../http-server.js';
|
|
7
|
+
import { subscribeWecomMessageByCcId, publishCcMessage } from '../message-bus.js';
|
|
8
|
+
import { randomBytes } from 'crypto';
|
|
8
9
|
import { updateWechatModeConfig, loadWechatModeConfig, addPermissionHook, removePermissionHook, addStopHook, removeStopHook, registerActiveProject, unregisterActiveProject } from '../project-config.js';
|
|
9
10
|
import { logger } from '../logger.js';
|
|
10
11
|
// 辅助函数:从 ccId 获取客户端
|
|
@@ -75,9 +76,44 @@ export function registerTools(server) {
|
|
|
75
76
|
};
|
|
76
77
|
});
|
|
77
78
|
// ============================================
|
|
78
|
-
// 工具 4:
|
|
79
|
+
// 工具 4: 检查连接状态(per-CC)
|
|
79
80
|
// ============================================
|
|
80
|
-
server.tool('check_connection', '检查当前 WebSocket
|
|
81
|
+
server.tool('check_connection', '检查当前 WebSocket 连接状态。建议传入 cc_id 以获取本 CC 对应 robot 的状态;不传则返回任一活跃连接(v3 起将下线无参版本)', {
|
|
82
|
+
cc_id: z.string().optional().describe('CC 唯一标识。传入后返回本 CC 对应 robot 的连接状态;不传则降级为旧行为'),
|
|
83
|
+
}, async ({ cc_id }) => {
|
|
84
|
+
if (cc_id) {
|
|
85
|
+
const robotName = getRobotByCcId(cc_id);
|
|
86
|
+
if (!robotName) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: JSON.stringify({ connected: false, ccId: cc_id, error: '未注册的 ccId 或 robot 未连接' }),
|
|
91
|
+
}],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const all = getAllConnectionStates();
|
|
95
|
+
const entry = all.find(s => s.robotName === robotName);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: JSON.stringify({ connected: false, ccId: cc_id, robotName, error: '该 robot 不在 connectionPool 中' }),
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
connected: entry.connected,
|
|
109
|
+
ccId: cc_id,
|
|
110
|
+
robotName: entry.robotName,
|
|
111
|
+
connectedAt: entry.connectedAt,
|
|
112
|
+
}),
|
|
113
|
+
}],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// 旧行为:返回 connectionPool 中任一活跃连接,并提示 deprecation
|
|
81
117
|
const state = getConnectionState();
|
|
82
118
|
return {
|
|
83
119
|
content: [{
|
|
@@ -86,11 +122,74 @@ export function registerTools(server) {
|
|
|
86
122
|
connected: state.connected,
|
|
87
123
|
robotName: state.robotName,
|
|
88
124
|
connectedAt: state.connectedAt,
|
|
125
|
+
warning: '请传入 cc_id 获取该 CC 对应 robot 的状态,无参版本将在 v3.0 移除',
|
|
89
126
|
}),
|
|
90
127
|
}],
|
|
91
128
|
};
|
|
92
129
|
});
|
|
93
130
|
// ============================================
|
|
131
|
+
// 工具 5a: CC 间通信 — 向另一个 CC 发消息(v2.6.0+,单 daemon 范围内)
|
|
132
|
+
// ============================================
|
|
133
|
+
server.tool('send_to_cc', '向同一 daemon 上的另一个 CC 发送消息。目标 CC 收到时会作为 <channel source="cc:..."> 推送唤醒。仅支持同 daemon 间互通,跨 daemon 不通。', {
|
|
134
|
+
cc_id: z.string().describe('自己的 CC 标识(必填)'),
|
|
135
|
+
to_cc: z.string().describe('目标 CC 标识'),
|
|
136
|
+
content: z.string().describe('消息内容(支持 Markdown)'),
|
|
137
|
+
kind: z.enum(['request', 'reply', 'notify']).optional().default('notify').describe('消息语义:request 期待回复 / reply 是对前一条 request 的回复 / notify 单向通知'),
|
|
138
|
+
reply_to: z.string().optional().describe('可选:关联的请求 msgId(用于追踪请求-响应)'),
|
|
139
|
+
}, async ({ cc_id, to_cc, content, kind = 'notify', reply_to }) => {
|
|
140
|
+
if (cc_id === to_cc) {
|
|
141
|
+
return { content: [{ type: 'text', text: JSON.stringify({ delivered: false, reason: 'cannot send to self' }) }] };
|
|
142
|
+
}
|
|
143
|
+
const targetEntry = getCCRegistryEntry(to_cc);
|
|
144
|
+
if (!targetEntry) {
|
|
145
|
+
return { content: [{ type: 'text', text: JSON.stringify({ delivered: false, reason: 'target offline', to_cc }) }] };
|
|
146
|
+
}
|
|
147
|
+
const msgId = `cc_${Date.now()}_${randomBytes(4).toString('hex')}`;
|
|
148
|
+
publishCcMessage({
|
|
149
|
+
msgId,
|
|
150
|
+
fromCc: cc_id,
|
|
151
|
+
toCc: to_cc,
|
|
152
|
+
content,
|
|
153
|
+
kind,
|
|
154
|
+
replyTo: reply_to,
|
|
155
|
+
hopCount: 0,
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
content: [{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: JSON.stringify({ delivered: true, msgId, to_cc, kind }),
|
|
162
|
+
}],
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
// ============================================
|
|
166
|
+
// 工具 5b: CC 间通信 — 列出当前 daemon 上在线的 CC
|
|
167
|
+
// ============================================
|
|
168
|
+
server.tool('list_active_ccs', '列出同一 daemon 上当前在线的所有 CC,用于决定 send_to_cc 的目标', {
|
|
169
|
+
cc_id: z.string().describe('自己的 CC 标识(用于过滤输出,不返回 self)'),
|
|
170
|
+
}, async ({ cc_id }) => {
|
|
171
|
+
const onlineIds = getOnlineCcIds();
|
|
172
|
+
const others = onlineIds
|
|
173
|
+
.filter(id => id !== cc_id)
|
|
174
|
+
.map(id => {
|
|
175
|
+
const entry = getCCRegistryEntry(id);
|
|
176
|
+
return entry ? {
|
|
177
|
+
ccId: id,
|
|
178
|
+
robotName: entry.robotName,
|
|
179
|
+
agentName: entry.agentName,
|
|
180
|
+
mode: entry.mode,
|
|
181
|
+
lastOnline: entry.lastOnline,
|
|
182
|
+
} : null;
|
|
183
|
+
})
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
return {
|
|
186
|
+
content: [{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: JSON.stringify({ self: cc_id, ccs: others }),
|
|
189
|
+
}],
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
// ============================================
|
|
94
193
|
// 工具 6: 获取待处理消息
|
|
95
194
|
// ============================================
|
|
96
195
|
server.tool('get_pending_messages', '获取待处理的微信消息。支持长轮询:传入 timeout_ms 后阻塞等待,有消息立即返回,无消息等到超时。超时后继续轮询,不要停止。', {
|