@vrs-soft/wecom-aibot-mcp 1.0.7 → 1.0.8
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.d.ts +5 -0
- package/dist/bin.js +86 -59
- package/dist/client.d.ts +6 -0
- package/dist/client.js +17 -6
- package/dist/config-wizard.d.ts +10 -0
- package/dist/config-wizard.js +32 -43
- package/dist/connection-log.d.ts +36 -0
- package/dist/connection-log.js +210 -0
- package/dist/connection-manager.d.ts +62 -0
- package/dist/connection-manager.js +245 -0
- package/dist/headless-state.d.ts +40 -38
- package/dist/headless-state.js +136 -139
- package/dist/http-server.d.ts +1 -16
- package/dist/http-server.js +196 -319
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/keepalive-monitor.d.ts +19 -0
- package/dist/keepalive-monitor.js +98 -0
- package/dist/tools/index.d.ts +1 -2
- package/dist/tools/index.js +286 -292
- package/package.json +1 -1
- package/skills/headless-mode/SKILL.md +94 -129
package/dist/bin.d.ts
CHANGED
package/dist/bin.js
CHANGED
|
@@ -7,30 +7,21 @@
|
|
|
7
7
|
* 支持 HTTP Transport 模式:
|
|
8
8
|
* - 固定端口 18963
|
|
9
9
|
* - 支持多 Claude Code 同时连接
|
|
10
|
+
*
|
|
11
|
+
* 连接管理:
|
|
12
|
+
* - 启动时不建立 WebSocket 连接
|
|
13
|
+
* - enter_headless_mode 时按需建立连接
|
|
14
|
+
* - exit_headless_mode 时断开连接
|
|
10
15
|
*/
|
|
11
|
-
import { runConfigWizard, loadConfig, saveConfig, deleteConfig, deleteMcpConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, } from './config-wizard.js';
|
|
16
|
+
import { runConfigWizard, loadConfig, saveConfig, deleteConfig, deleteMcpConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, } from './config-wizard.js';
|
|
12
17
|
import { initClient } from './client.js';
|
|
13
18
|
import { registerTools } from './tools/index.js';
|
|
14
19
|
import { startHttpServer, HTTP_PORT } from './http-server.js';
|
|
15
20
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
-
import { clearAllProjectHooks } from './headless-state.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
const startTime = Date.now();
|
|
22
|
-
const checkInterval = setInterval(() => {
|
|
23
|
-
if (client.isConnected()) {
|
|
24
|
-
clearInterval(checkInterval);
|
|
25
|
-
resolve(true);
|
|
26
|
-
}
|
|
27
|
-
else if (Date.now() - startTime > timeoutMs) {
|
|
28
|
-
clearInterval(checkInterval);
|
|
29
|
-
resolve(false);
|
|
30
|
-
}
|
|
31
|
-
}, 500);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
21
|
+
import { clearAllProjectHooks, getAllHeadlessStates } from './headless-state.js';
|
|
22
|
+
import { loadStats, cleanupOldLogs } from './connection-log.js';
|
|
23
|
+
import { startKeepaliveMonitor, stopKeepaliveMonitor } from './keepalive-monitor.js';
|
|
24
|
+
const VERSION = '1.0.7';
|
|
34
25
|
function showHelp() {
|
|
35
26
|
console.log(`
|
|
36
27
|
企业微信智能机器人 MCP 服务 v${VERSION}
|
|
@@ -46,8 +37,8 @@ function showHelp() {
|
|
|
46
37
|
--version, -v 显示版本号
|
|
47
38
|
--config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
|
|
48
39
|
--add 添加新的机器人配置(多机器人场景)
|
|
40
|
+
--list 列出所有已配置的机器人
|
|
49
41
|
--delete [名称] 删除指定的机器人配置(无参数则显示列表选择)
|
|
50
|
-
--status 显示当前配置状态
|
|
51
42
|
--uninstall 卸载并删除所有配置(包括 MCP 配置、hook、skill)
|
|
52
43
|
|
|
53
44
|
MCP 配置(HTTP Transport):
|
|
@@ -80,16 +71,51 @@ function showVersion() {
|
|
|
80
71
|
console.log(`wecom-aibot-mcp v${VERSION}`);
|
|
81
72
|
}
|
|
82
73
|
function showStatus() {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.log(` Bot ID: ${config.botId}`);
|
|
87
|
-
console.log(` Secret: ${config.secret.slice(0, 8)}...${config.secret.slice(-4)}`);
|
|
88
|
-
console.log(` 目标用户: ${config.targetUserId}`);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
74
|
+
const allRobots = listAllRobots();
|
|
75
|
+
const headlessStates = getAllHeadlessStates();
|
|
76
|
+
if (allRobots.length === 0) {
|
|
91
77
|
console.log('尚未配置,请运行 npx @vrs-soft/wecom-aibot-mcp 启动配置向导');
|
|
78
|
+
return;
|
|
92
79
|
}
|
|
80
|
+
// 构建机器人占用信息
|
|
81
|
+
const robotUsage = new Map();
|
|
82
|
+
for (const { state } of headlessStates) {
|
|
83
|
+
if (state.robotName) {
|
|
84
|
+
robotUsage.set(state.robotName, {
|
|
85
|
+
agentName: state.agentName || '未知',
|
|
86
|
+
projectDir: state.projectDir,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
console.log(`已配置 ${allRobots.length} 个机器人:\n`);
|
|
91
|
+
for (const robot of allRobots) {
|
|
92
|
+
const defaultTag = robot.isDefault ? ' (默认)' : '';
|
|
93
|
+
const usage = robotUsage.get(robot.name);
|
|
94
|
+
const statusTag = usage ? ` [使用中]` : '';
|
|
95
|
+
console.log(` ${robot.name}${defaultTag}${statusTag}`);
|
|
96
|
+
console.log(` Bot ID: ${robot.botId}`);
|
|
97
|
+
console.log(` 目标用户: ${robot.targetUserId}`);
|
|
98
|
+
if (usage) {
|
|
99
|
+
console.log(` 使用者: ${usage.agentName} (${usage.projectDir})`);
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 等待连接验证(用于配置向导验证凭证)
|
|
105
|
+
async function waitForConnection(client, timeoutMs = 10000) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
const checkInterval = setInterval(() => {
|
|
109
|
+
if (client.isConnected()) {
|
|
110
|
+
clearInterval(checkInterval);
|
|
111
|
+
resolve(true);
|
|
112
|
+
}
|
|
113
|
+
else if (Date.now() - startTime > timeoutMs) {
|
|
114
|
+
clearInterval(checkInterval);
|
|
115
|
+
resolve(false);
|
|
116
|
+
}
|
|
117
|
+
}, 500);
|
|
118
|
+
});
|
|
93
119
|
}
|
|
94
120
|
async function main() {
|
|
95
121
|
const args = process.argv.slice(2);
|
|
@@ -102,7 +128,7 @@ async function main() {
|
|
|
102
128
|
showVersion();
|
|
103
129
|
process.exit(0);
|
|
104
130
|
}
|
|
105
|
-
if (args.includes('--status')) {
|
|
131
|
+
if (args.includes('--status') || args.includes('--list')) {
|
|
106
132
|
showStatus();
|
|
107
133
|
process.exit(0);
|
|
108
134
|
}
|
|
@@ -129,6 +155,9 @@ async function main() {
|
|
|
129
155
|
console.log(' ║ Claude Code 审批通道 ║');
|
|
130
156
|
console.log(' ╚════════════════════════════════════════════════════════╝');
|
|
131
157
|
console.log('');
|
|
158
|
+
// 加载统计并清理旧日志(保留 1 小时)
|
|
159
|
+
loadStats();
|
|
160
|
+
cleanupOldLogs(1 / 24); // 保留 1 小时
|
|
132
161
|
// 获取或初始化配置
|
|
133
162
|
let config;
|
|
134
163
|
let ranWizard = false; // 是否运行了配置向导
|
|
@@ -165,35 +194,33 @@ async function main() {
|
|
|
165
194
|
ensureHookInstalled();
|
|
166
195
|
// 清理残留的 headless 状态和 Hook 配置
|
|
167
196
|
clearAllProjectHooks();
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (isInteractive) {
|
|
197
|
+
// 配置向导模式:验证连接并识别用户 ID
|
|
198
|
+
if (isInteractive && (ranWizard || reconfig)) {
|
|
199
|
+
console.log('[mcp] 验证机器人连接...');
|
|
200
|
+
// 临时建立连接验证凭证
|
|
201
|
+
const tempClient = initClient(config.botId, config.secret, config.targetUserId || 'placeholder');
|
|
202
|
+
const connected = await waitForConnection(tempClient, 10000);
|
|
203
|
+
if (!connected) {
|
|
204
|
+
console.log('[mcp] 连接失败,可能是配置错误或机器人未授权');
|
|
205
|
+
console.log('[mcp] 请检查上面的错误提示,修复后重新配置');
|
|
206
|
+
// 删除无效配置,让用户重新输入
|
|
207
|
+
deleteConfig();
|
|
180
208
|
console.log('\n请检查:');
|
|
181
209
|
console.log(' 1. Bot ID 和 Secret 是否正确');
|
|
182
210
|
console.log(' 2. 新建机器人需等待约 2 分钟同步');
|
|
183
211
|
console.log(' 3. 是否已完成授权(机器人详情 → 可使用权限 → 授权)');
|
|
184
212
|
console.log('\n修复后重新运行: npx @vrs-soft/wecom-aibot-mcp --config');
|
|
213
|
+
tempClient.disconnect();
|
|
214
|
+
process.exit(1);
|
|
185
215
|
}
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
// 连接成功
|
|
189
|
-
if (isInteractive && (ranWizard || reconfig)) {
|
|
190
|
-
// 需要通过消息自动识别用户 ID
|
|
216
|
+
// 连接成功
|
|
191
217
|
console.log('\n[mcp] ✅ 机器人连接成功!');
|
|
192
218
|
// 提示用户发送消息来识别用户 ID
|
|
193
|
-
const userId = await detectUserIdFromMessage(
|
|
219
|
+
const userId = await detectUserIdFromMessage(tempClient, 180);
|
|
194
220
|
if (!userId) {
|
|
195
221
|
console.log('\n[mcp] 未能在规定时间内识别用户 ID');
|
|
196
222
|
console.log('[mcp] 请重新运行配置:npx @vrs-soft/wecom-aibot-mcp --config');
|
|
223
|
+
tempClient.disconnect();
|
|
197
224
|
process.exit(1);
|
|
198
225
|
}
|
|
199
226
|
// 更新配置中的用户 ID
|
|
@@ -203,30 +230,30 @@ async function main() {
|
|
|
203
230
|
console.log('\n[mcp] ✅ 配置完成!');
|
|
204
231
|
console.log(`[mcp] 用户 ID: ${userId}`);
|
|
205
232
|
console.log('[mcp] 请重启 Claude Code 以加载 MCP 服务\n');
|
|
233
|
+
// 配置完成后断开连接
|
|
234
|
+
tempClient.disconnect();
|
|
206
235
|
process.exit(0);
|
|
207
236
|
}
|
|
208
|
-
// 创建 MCP Server
|
|
237
|
+
// 创建 MCP Server(不建立 WebSocket 连接)
|
|
209
238
|
const server = new McpServer({
|
|
210
239
|
name: 'wecom-aibot-mcp',
|
|
211
240
|
version: VERSION,
|
|
212
241
|
});
|
|
213
|
-
//
|
|
214
|
-
registerTools(server
|
|
215
|
-
// 启动 HTTP
|
|
242
|
+
// 注册工具(不传入 client,由 ConnectionManager 管理)
|
|
243
|
+
registerTools(server);
|
|
244
|
+
// 启动 HTTP 服务
|
|
216
245
|
console.log(`[mcp] 启动 MCP HTTP Server (端口: ${HTTP_PORT})...`);
|
|
217
|
-
await startHttpServer(
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
wecomClient.cleanupMessages();
|
|
221
|
-
}, 60000);
|
|
246
|
+
await startHttpServer(server);
|
|
247
|
+
// 启动保活监控
|
|
248
|
+
startKeepaliveMonitor();
|
|
222
249
|
console.log(`[mcp] MCP Server 已就绪`);
|
|
223
250
|
console.log(`[mcp] HTTP endpoint: http://127.0.0.1:${HTTP_PORT}/mcp`);
|
|
224
251
|
console.log(`[mcp] 健康检查: http://127.0.0.1:${HTTP_PORT}/health`);
|
|
225
|
-
|
|
252
|
+
console.log(`[mcp] 微信模式:enter_headless_mode 时建立连接`);
|
|
253
|
+
// 退出处理
|
|
226
254
|
const gracefulShutdown = () => {
|
|
227
255
|
console.log('[mcp] 正在关闭...');
|
|
228
|
-
|
|
229
|
-
wecomClient.disconnect();
|
|
256
|
+
stopKeepaliveMonitor();
|
|
230
257
|
process.exit(0);
|
|
231
258
|
};
|
|
232
259
|
// 监听进程信号
|
package/dist/client.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ interface ApprovalRecord {
|
|
|
4
4
|
result?: 'allow-once' | 'allow-always' | 'deny';
|
|
5
5
|
timestamp: number;
|
|
6
6
|
toolName?: string;
|
|
7
|
+
toolInput?: Record<string, unknown>;
|
|
8
|
+
projectDir?: string;
|
|
9
|
+
lastKeepaliveMinute?: number;
|
|
10
|
+
keepaliveCount?: number;
|
|
7
11
|
}
|
|
8
12
|
interface MessageRecord {
|
|
9
13
|
msgid: string;
|
|
@@ -41,6 +45,8 @@ declare class WecomClient {
|
|
|
41
45
|
sendApprovalRequest(title: string, description: string, requestId: string, targetUser?: string): Promise<string>;
|
|
42
46
|
getApprovalResult(taskId: string): 'pending' | 'allow-once' | 'allow-always' | 'deny';
|
|
43
47
|
getPendingApprovals(): string[];
|
|
48
|
+
getPendingApprovalsRecords(): ApprovalRecord[];
|
|
49
|
+
getApprovalRecord(taskId: string): ApprovalRecord | undefined;
|
|
44
50
|
getLatestMessage(afterTimestamp: number): MessageRecord | undefined;
|
|
45
51
|
getPendingMessages(clear?: boolean): MessageRecord[];
|
|
46
52
|
cleanupMessages(maxAgeMs?: number): void;
|
package/dist/client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 管理消息队列和审批状态。
|
|
6
6
|
*/
|
|
7
7
|
import AiBot from '@wecom/aibot-node-sdk';
|
|
8
|
+
import { logConnected, logAuthenticated, logDisconnected, logReconnecting, logError, } from './connection-log.js';
|
|
8
9
|
class WecomClient {
|
|
9
10
|
wsClient;
|
|
10
11
|
approvals = new Map();
|
|
@@ -22,7 +23,7 @@ class WecomClient {
|
|
|
22
23
|
this.wsClient = new AiBot.WSClient({
|
|
23
24
|
botId,
|
|
24
25
|
secret,
|
|
25
|
-
heartbeatInterval:
|
|
26
|
+
heartbeatInterval: 15000, // 15 秒心跳,更快检测断线
|
|
26
27
|
maxReconnectAttempts: -1, // 无限重连
|
|
27
28
|
});
|
|
28
29
|
this.setupEventHandlers();
|
|
@@ -37,14 +38,14 @@ class WecomClient {
|
|
|
37
38
|
}
|
|
38
39
|
setupEventHandlers() {
|
|
39
40
|
this.wsClient.on('connected', () => {
|
|
40
|
-
|
|
41
|
+
logConnected();
|
|
41
42
|
});
|
|
42
43
|
this.wsClient.on('authenticated', () => {
|
|
43
44
|
const wasReconnecting = this.wasReconnecting;
|
|
44
45
|
this.connected = true;
|
|
45
46
|
this.wasReconnecting = false;
|
|
46
47
|
this.reconnectAttempt = 0;
|
|
47
|
-
|
|
48
|
+
logAuthenticated();
|
|
48
49
|
// 重连成功后发送通知
|
|
49
50
|
if (wasReconnecting) {
|
|
50
51
|
this.sendText('【系统】连接已恢复').catch(err => {
|
|
@@ -58,7 +59,7 @@ class WecomClient {
|
|
|
58
59
|
this.connected = false;
|
|
59
60
|
this.wasReconnecting = true;
|
|
60
61
|
this.lastDisconnectTime = Date.now();
|
|
61
|
-
|
|
62
|
+
logDisconnected(reason);
|
|
62
63
|
// 发送断线通知
|
|
63
64
|
this.sendText('【系统】连接中断,正在重连...').catch(err => {
|
|
64
65
|
console.error('[wecom] 发送断线通知失败:', err);
|
|
@@ -66,10 +67,10 @@ class WecomClient {
|
|
|
66
67
|
});
|
|
67
68
|
this.wsClient.on('reconnecting', (attempt) => {
|
|
68
69
|
this.reconnectAttempt = attempt;
|
|
69
|
-
|
|
70
|
+
logReconnecting(attempt);
|
|
70
71
|
});
|
|
71
72
|
this.wsClient.on('error', (err) => {
|
|
72
|
-
|
|
73
|
+
logError(err.message);
|
|
73
74
|
// 检测授权相关错误(40058: invalid Request Parameter)
|
|
74
75
|
if (err.message.includes('40058') || err.message.includes('invalid Request Parameter')) {
|
|
75
76
|
console.log('');
|
|
@@ -90,6 +91,7 @@ class WecomClient {
|
|
|
90
91
|
});
|
|
91
92
|
// 监听模板卡片事件(审批结果)
|
|
92
93
|
this.wsClient.on('event.template_card_event', (frame) => {
|
|
94
|
+
console.log('[wecom] 收到 template_card_event 事件');
|
|
93
95
|
this.handleApprovalResponse(frame);
|
|
94
96
|
});
|
|
95
97
|
// 监听进入会话事件
|
|
@@ -304,6 +306,15 @@ class WecomClient {
|
|
|
304
306
|
.filter(([_, a]) => !a.resolved)
|
|
305
307
|
.map(([taskId, _]) => taskId);
|
|
306
308
|
}
|
|
309
|
+
// 获取所有待处理审批记录(供保活监控使用)
|
|
310
|
+
getPendingApprovalsRecords() {
|
|
311
|
+
return Array.from(this.approvals.values())
|
|
312
|
+
.filter(a => !a.resolved);
|
|
313
|
+
}
|
|
314
|
+
// 获取单个审批记录
|
|
315
|
+
getApprovalRecord(taskId) {
|
|
316
|
+
return this.approvals.get(taskId);
|
|
317
|
+
}
|
|
307
318
|
// 获取最新消息(用于等待回复)
|
|
308
319
|
getLatestMessage(afterTimestamp) {
|
|
309
320
|
return this.messages.find(m => m.timestamp > afterTimestamp && m.from_userid === this.targetUserId);
|
package/dist/config-wizard.d.ts
CHANGED
|
@@ -16,8 +16,18 @@ export declare function deleteMcpConfig(instanceName: string): boolean;
|
|
|
16
16
|
export declare function deleteMcpConfigInteractive(instanceName?: string): Promise<void>;
|
|
17
17
|
export declare function uninstall(): void;
|
|
18
18
|
export declare function addMcpConfig(): Promise<void>;
|
|
19
|
+
export declare function listAllRobots(): Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
botId: string;
|
|
22
|
+
targetUserId: string;
|
|
23
|
+
isDefault: boolean;
|
|
24
|
+
}>;
|
|
19
25
|
export declare function ensureHookInstalled(): void;
|
|
20
26
|
export declare function saveConfig(config: WecomConfig, instanceName?: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* 安装 headless-mode skill 到项目目录
|
|
29
|
+
*/
|
|
30
|
+
export declare function installSkill(projectDir: string): void;
|
|
21
31
|
/**
|
|
22
32
|
* 运行配置向导
|
|
23
33
|
*/
|
package/dist/config-wizard.js
CHANGED
|
@@ -16,6 +16,9 @@ const BOT_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
16
16
|
const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
|
|
17
17
|
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
|
|
18
18
|
const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
|
|
19
|
+
// Skill 模板路径(包内)
|
|
20
|
+
const SKILL_TEMPLATE_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'skills', 'headless-mode');
|
|
21
|
+
const SKILL_TEMPLATE_FILE = path.join(SKILL_TEMPLATE_DIR, 'SKILL.md');
|
|
19
22
|
// MCP 工具权限列表(需要预授权以避免 headless 模式阻断)
|
|
20
23
|
const MCP_TOOL_PERMISSIONS = [
|
|
21
24
|
'mcp__wecom-aibot__*', // 允许所有 wecom-aibot 工具
|
|
@@ -222,10 +225,9 @@ function writeHookScript() {
|
|
|
222
225
|
# HTTP Transport 版本
|
|
223
226
|
#
|
|
224
227
|
# 固定端口: 18963
|
|
225
|
-
#
|
|
228
|
+
# 直接检查 $(pwd)/.claude/headless.json
|
|
226
229
|
|
|
227
230
|
MCP_PORT=18963
|
|
228
|
-
CONFIG_DIR="$HOME/.wecom-aibot-mcp"
|
|
229
231
|
|
|
230
232
|
INPUT=$(cat)
|
|
231
233
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
@@ -244,46 +246,12 @@ case "$TOOL_NAME" in
|
|
|
244
246
|
;;
|
|
245
247
|
esac
|
|
246
248
|
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for i in {1..5}; do
|
|
252
|
-
if [[ -z "$PARENT_PID" ]] || [[ "$PARENT_PID" -eq 1 ]]; then
|
|
253
|
-
break
|
|
254
|
-
fi
|
|
255
|
-
|
|
256
|
-
CANDIDATE="$CONFIG_DIR/headless-$PARENT_PID"
|
|
257
|
-
if [[ -f "$CANDIDATE" ]]; then
|
|
258
|
-
HEADLESS_FILE="$CANDIDATE"
|
|
259
|
-
break
|
|
260
|
-
fi
|
|
261
|
-
|
|
262
|
-
CHILD_PIDS=$(pgrep -P "$PARENT_PID" 2>/dev/null)
|
|
263
|
-
for CHILD_PID in $CHILD_PIDS; do
|
|
264
|
-
CANDIDATE="$CONFIG_DIR/headless-$CHILD_PID"
|
|
265
|
-
if [[ -f "$CANDIDATE" ]]; then
|
|
266
|
-
HEADLESS_FILE="$CANDIDATE"
|
|
267
|
-
break 2
|
|
268
|
-
fi
|
|
269
|
-
done
|
|
270
|
-
|
|
271
|
-
PARENT_PID=$(ps -o ppid= -p "$PARENT_PID" 2>/dev/null | tr -d ' ')
|
|
272
|
-
done
|
|
273
|
-
|
|
274
|
-
# Fallback: 使用最新的 headless 文件
|
|
275
|
-
if [[ -z "$HEADLESS_FILE" ]]; then
|
|
276
|
-
HEADLESS_FILE=$(ls -t "$CONFIG_DIR"/headless-* 2>/dev/null | head -1)
|
|
277
|
-
fi
|
|
249
|
+
# 直接检查项目目录的 headless 状态文件
|
|
250
|
+
PROJECT_DIR=$(pwd)
|
|
251
|
+
HEADLESS_FILE="$PROJECT_DIR/.claude/headless.json"
|
|
278
252
|
|
|
279
253
|
# 不在 headless 模式
|
|
280
|
-
if [[
|
|
281
|
-
exit 0
|
|
282
|
-
fi
|
|
283
|
-
|
|
284
|
-
# 从 headless 文件读取 projectDir
|
|
285
|
-
PROJECT_DIR=$(cat "$HEADLESS_FILE" 2>/dev/null | jq -r '.projectDir // empty')
|
|
286
|
-
if [[ -z "$PROJECT_DIR" ]]; then
|
|
254
|
+
if [[ ! -f "$HEADLESS_FILE" ]]; then
|
|
287
255
|
exit 0
|
|
288
256
|
fi
|
|
289
257
|
|
|
@@ -293,7 +261,7 @@ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
|
293
261
|
exit 0
|
|
294
262
|
fi
|
|
295
263
|
|
|
296
|
-
#
|
|
264
|
+
# 发送审批请求(使用 pwd 作为 projectDir)
|
|
297
265
|
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
298
266
|
BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" \\
|
|
299
267
|
'{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir}')
|
|
@@ -493,7 +461,7 @@ export async function addMcpConfig() {
|
|
|
493
461
|
return;
|
|
494
462
|
}
|
|
495
463
|
// 通过消息自动识别用户 ID
|
|
496
|
-
const targetUserId = await detectUserIdFromMessage(client,
|
|
464
|
+
const targetUserId = await detectUserIdFromMessage(client, 180);
|
|
497
465
|
if (!targetUserId) {
|
|
498
466
|
console.log('\n[config] 未能在规定时间内识别用户 ID');
|
|
499
467
|
console.log('[config] 请重新运行: npx @vrs-soft/wecom-aibot-mcp --add');
|
|
@@ -536,7 +504,7 @@ export async function addMcpConfig() {
|
|
|
536
504
|
}
|
|
537
505
|
}
|
|
538
506
|
// 列出所有机器人配置
|
|
539
|
-
function listAllRobots() {
|
|
507
|
+
export function listAllRobots() {
|
|
540
508
|
const robots = [];
|
|
541
509
|
// 默认配置
|
|
542
510
|
if (fs.existsSync(BOT_CONFIG_FILE)) {
|
|
@@ -663,6 +631,27 @@ export function saveConfig(config, instanceName) {
|
|
|
663
631
|
writeMcpServerConfig(config, instanceName);
|
|
664
632
|
// 写入 MCP 工具权限和 Hook 到 ~/.claude/settings.local.json
|
|
665
633
|
writeMcpPermissions();
|
|
634
|
+
// 安装 skill 到项目目录
|
|
635
|
+
installSkill(process.cwd());
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 安装 headless-mode skill 到项目目录
|
|
639
|
+
*/
|
|
640
|
+
export function installSkill(projectDir) {
|
|
641
|
+
const skillDir = path.join(projectDir, '.claude', 'skills', 'headless-mode');
|
|
642
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
643
|
+
// 确保 skill 目录存在
|
|
644
|
+
if (!fs.existsSync(skillDir)) {
|
|
645
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
646
|
+
}
|
|
647
|
+
// 检查模板文件是否存在
|
|
648
|
+
if (!fs.existsSync(SKILL_TEMPLATE_FILE)) {
|
|
649
|
+
console.log('[config] Skill 模板文件不存在,跳过安装');
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// 写入 skill 文件
|
|
653
|
+
fs.copyFileSync(SKILL_TEMPLATE_FILE, skillFile);
|
|
654
|
+
console.log(`[config] 已安装 skill 到 ${skillFile}`);
|
|
666
655
|
}
|
|
667
656
|
// 创建 readline 接口
|
|
668
657
|
function createRL() {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 连接状态日志模块
|
|
3
|
+
*
|
|
4
|
+
* 记录 WebSocket 连接状态变化,便于分析连接问题
|
|
5
|
+
*/
|
|
6
|
+
interface ConnectionRecord {
|
|
7
|
+
event: 'connected' | 'authenticated' | 'disconnected' | 'reconnecting' | 'error';
|
|
8
|
+
timestamp: string;
|
|
9
|
+
isoTime: string;
|
|
10
|
+
reason?: string;
|
|
11
|
+
attempt?: number;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
connectionDuration?: number;
|
|
14
|
+
}
|
|
15
|
+
interface ConnectionStats {
|
|
16
|
+
totalConnections: number;
|
|
17
|
+
totalDisconnections: number;
|
|
18
|
+
totalReconnects: number;
|
|
19
|
+
totalErrors: number;
|
|
20
|
+
lastConnectTime?: string;
|
|
21
|
+
lastDisconnectTime?: string;
|
|
22
|
+
longestConnection?: number;
|
|
23
|
+
totalConnectionTime?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function loadStats(): ConnectionStats;
|
|
26
|
+
export declare function logConnected(): void;
|
|
27
|
+
export declare function logAuthenticated(): void;
|
|
28
|
+
export declare function logDisconnected(reason: string): void;
|
|
29
|
+
export declare function logReconnecting(attempt: number): void;
|
|
30
|
+
export declare function logError(errorMessage: string): void;
|
|
31
|
+
export declare function getStats(): ConnectionStats;
|
|
32
|
+
export declare function getRecentLogs(count?: number): ConnectionRecord[];
|
|
33
|
+
export declare function cleanupOldLogs(daysToKeep?: number): void;
|
|
34
|
+
export declare function getLogFilePath(): string;
|
|
35
|
+
export declare function getStatsFilePath(): string;
|
|
36
|
+
export {};
|