cc2im 0.2.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/LICENSE +21 -0
- package/README.en.md +120 -0
- package/README.md +120 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +314 -0
- package/dist/hub/agent-manager.d.ts +63 -0
- package/dist/hub/agent-manager.js +311 -0
- package/dist/hub/hub-context.d.ts +27 -0
- package/dist/hub/hub-context.js +57 -0
- package/dist/hub/index.d.ts +6 -0
- package/dist/hub/index.js +234 -0
- package/dist/hub/launchd.d.ts +7 -0
- package/dist/hub/launchd.js +151 -0
- package/dist/hub/plugin-manager.d.ts +7 -0
- package/dist/hub/plugin-manager.js +29 -0
- package/dist/hub/router.d.ts +21 -0
- package/dist/hub/router.js +35 -0
- package/dist/hub/socket-server.d.ts +23 -0
- package/dist/hub/socket-server.js +191 -0
- package/dist/plugins/channel-manager/index.d.ts +10 -0
- package/dist/plugins/channel-manager/index.js +387 -0
- package/dist/plugins/cron-scheduler/db.d.ts +12 -0
- package/dist/plugins/cron-scheduler/db.js +160 -0
- package/dist/plugins/cron-scheduler/index.d.ts +4 -0
- package/dist/plugins/cron-scheduler/index.js +22 -0
- package/dist/plugins/cron-scheduler/scheduler.d.ts +20 -0
- package/dist/plugins/cron-scheduler/scheduler.js +129 -0
- package/dist/plugins/persistence/db.d.ts +24 -0
- package/dist/plugins/persistence/db.js +121 -0
- package/dist/plugins/persistence/index.d.ts +2 -0
- package/dist/plugins/persistence/index.js +93 -0
- package/dist/plugins/web-monitor/api-routes.d.ts +33 -0
- package/dist/plugins/web-monitor/api-routes.js +474 -0
- package/dist/plugins/web-monitor/index.d.ts +2 -0
- package/dist/plugins/web-monitor/index.js +21 -0
- package/dist/plugins/web-monitor/log-tailer.d.ts +13 -0
- package/dist/plugins/web-monitor/log-tailer.js +74 -0
- package/dist/plugins/web-monitor/monitor-client.d.ts +17 -0
- package/dist/plugins/web-monitor/monitor-client.js +68 -0
- package/dist/plugins/web-monitor/server.d.ts +14 -0
- package/dist/plugins/web-monitor/server.js +205 -0
- package/dist/plugins/web-monitor/stats-reader.d.ts +22 -0
- package/dist/plugins/web-monitor/stats-reader.js +17 -0
- package/dist/plugins/web-monitor/token-stats.d.ts +19 -0
- package/dist/plugins/web-monitor/token-stats.js +86 -0
- package/dist/plugins/web-monitor/usage-stats.d.ts +13 -0
- package/dist/plugins/web-monitor/usage-stats.js +56 -0
- package/dist/plugins/weixin/chunker.d.ts +16 -0
- package/dist/plugins/weixin/chunker.js +142 -0
- package/dist/plugins/weixin/connection.d.ts +46 -0
- package/dist/plugins/weixin/connection.js +270 -0
- package/dist/plugins/weixin/index.d.ts +10 -0
- package/dist/plugins/weixin/index.js +198 -0
- package/dist/plugins/weixin/media-upload.d.ts +22 -0
- package/dist/plugins/weixin/media-upload.js +134 -0
- package/dist/plugins/weixin/media.d.ts +6 -0
- package/dist/plugins/weixin/media.js +83 -0
- package/dist/plugins/weixin/permission.d.ts +35 -0
- package/dist/plugins/weixin/permission.js +96 -0
- package/dist/plugins/weixin/qr-login.d.ts +23 -0
- package/dist/plugins/weixin/qr-login.js +77 -0
- package/dist/plugins/weixin/weixin-channel.d.ts +33 -0
- package/dist/plugins/weixin/weixin-channel.js +123 -0
- package/dist/shared/channel-config.d.ts +8 -0
- package/dist/shared/channel-config.js +14 -0
- package/dist/shared/channel.d.ts +37 -0
- package/dist/shared/channel.js +8 -0
- package/dist/shared/mcp-config.d.ts +5 -0
- package/dist/shared/mcp-config.js +44 -0
- package/dist/shared/plugin.d.ts +32 -0
- package/dist/shared/plugin.js +1 -0
- package/dist/shared/socket.d.ts +5 -0
- package/dist/shared/socket.js +31 -0
- package/dist/shared/types.d.ts +136 -0
- package/dist/shared/types.js +1 -0
- package/dist/spoke/channel-server.d.ts +48 -0
- package/dist/spoke/channel-server.js +383 -0
- package/dist/spoke/index.d.ts +13 -0
- package/dist/spoke/index.js +115 -0
- package/dist/spoke/permission.d.ts +28 -0
- package/dist/spoke/permission.js +142 -0
- package/dist/spoke/socket-client.d.ts +22 -0
- package/dist/spoke/socket-client.js +83 -0
- package/dist/web-frontend/assets/index-CU9vxw8F.js +9 -0
- package/dist/web-frontend/index.html +82 -0
- package/package.json +54 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
export const SOCKET_DIR = join(homedir(), '.cc2im');
|
|
5
|
+
export const HUB_SOCKET_PATH = join(SOCKET_DIR, 'hub.sock');
|
|
6
|
+
export function ensureSocketDir() {
|
|
7
|
+
mkdirSync(SOCKET_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
// ndjson 帧编码/解码
|
|
10
|
+
export function encodeFrame(data) {
|
|
11
|
+
return Buffer.from(JSON.stringify(data) + '\n');
|
|
12
|
+
}
|
|
13
|
+
export function createFrameParser(onFrame) {
|
|
14
|
+
let buffer = '';
|
|
15
|
+
return (chunk) => {
|
|
16
|
+
buffer += chunk.toString();
|
|
17
|
+
let idx;
|
|
18
|
+
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
19
|
+
const line = buffer.slice(0, idx).trim();
|
|
20
|
+
buffer = buffer.slice(idx + 1);
|
|
21
|
+
if (line) {
|
|
22
|
+
try {
|
|
23
|
+
onFrame(JSON.parse(line));
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error('[socket] Bad frame:', line);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export interface HubToSpokeMessage {
|
|
2
|
+
type: 'message';
|
|
3
|
+
userId: string;
|
|
4
|
+
text: string;
|
|
5
|
+
msgType: string;
|
|
6
|
+
mediaPath?: string;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
channelId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HubToSpokePermission {
|
|
11
|
+
type: 'permission_verdict';
|
|
12
|
+
requestId: string;
|
|
13
|
+
behavior: 'allow' | 'deny' | 'always';
|
|
14
|
+
toolName?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SpokeToHubPermissionTimeout {
|
|
17
|
+
type: 'permission_timeout';
|
|
18
|
+
agentId: string;
|
|
19
|
+
requestId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SpokeToHubReply {
|
|
22
|
+
type: 'reply';
|
|
23
|
+
agentId: string;
|
|
24
|
+
userId: string;
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SpokeToHubSendFile {
|
|
28
|
+
type: 'send_file';
|
|
29
|
+
agentId: string;
|
|
30
|
+
userId: string;
|
|
31
|
+
filePath: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SpokeToHubPermissionRequest {
|
|
34
|
+
type: 'permission_request';
|
|
35
|
+
agentId: string;
|
|
36
|
+
requestId: string;
|
|
37
|
+
toolName: string;
|
|
38
|
+
description: string;
|
|
39
|
+
inputPreview: string;
|
|
40
|
+
userId?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SpokeToHubStatus {
|
|
43
|
+
type: 'status';
|
|
44
|
+
agentId: string;
|
|
45
|
+
status: 'ready' | 'busy' | 'error';
|
|
46
|
+
}
|
|
47
|
+
export interface HubToSpokeManagementResult {
|
|
48
|
+
type: 'management_result';
|
|
49
|
+
requestId: string;
|
|
50
|
+
success: boolean;
|
|
51
|
+
data?: any;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SpokeToHubManagement {
|
|
55
|
+
type: 'management';
|
|
56
|
+
agentId: string;
|
|
57
|
+
requestId: string;
|
|
58
|
+
action: 'register' | 'deregister' | 'start' | 'stop' | 'list' | 'cron_create' | 'cron_list' | 'cron_delete' | 'cron_update';
|
|
59
|
+
params?: {
|
|
60
|
+
name?: string;
|
|
61
|
+
cwd?: string;
|
|
62
|
+
claudeArgs?: string[];
|
|
63
|
+
scheduleType?: 'cron' | 'once' | 'interval';
|
|
64
|
+
scheduleValue?: string;
|
|
65
|
+
timezone?: string;
|
|
66
|
+
agentId?: string;
|
|
67
|
+
message?: string;
|
|
68
|
+
jobId?: string;
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface SpokeToHubRegister {
|
|
73
|
+
type: 'register';
|
|
74
|
+
agentId: string;
|
|
75
|
+
pid?: number;
|
|
76
|
+
}
|
|
77
|
+
export type HubToSpoke = HubToSpokeMessage | HubToSpokePermission | HubToSpokeManagementResult;
|
|
78
|
+
export interface SpokeToHubHeartbeat {
|
|
79
|
+
type: 'heartbeat';
|
|
80
|
+
agentId: string;
|
|
81
|
+
}
|
|
82
|
+
export type SpokeToHub = SpokeToHubRegister | SpokeToHubReply | SpokeToHubPermissionRequest | SpokeToHubStatus | SpokeToHubPermissionTimeout | SpokeToHubManagement | SpokeToHubHeartbeat | SpokeToHubSendFile;
|
|
83
|
+
export interface AgentConfig {
|
|
84
|
+
name: string;
|
|
85
|
+
cwd: string;
|
|
86
|
+
claudeArgs?: string[];
|
|
87
|
+
createdAt: string;
|
|
88
|
+
autoStart?: boolean;
|
|
89
|
+
autoMode?: boolean;
|
|
90
|
+
}
|
|
91
|
+
export interface MonitorRegister {
|
|
92
|
+
type: 'register_monitor';
|
|
93
|
+
}
|
|
94
|
+
export interface HubEventData {
|
|
95
|
+
kind: 'agent_online' | 'agent_offline' | 'message_in' | 'message_out' | 'permission_request' | 'permission_verdict' | 'agent_started' | 'agent_stopped' | 'config_changed' | 'channel_status' | 'cron_fired';
|
|
96
|
+
agentId: string;
|
|
97
|
+
timestamp: string;
|
|
98
|
+
userId?: string;
|
|
99
|
+
text?: string;
|
|
100
|
+
toolName?: string;
|
|
101
|
+
behavior?: string;
|
|
102
|
+
code?: number;
|
|
103
|
+
msgType?: string;
|
|
104
|
+
mediaUrl?: string;
|
|
105
|
+
channelId?: string;
|
|
106
|
+
channelType?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface HubEvent {
|
|
109
|
+
type: 'hub_event';
|
|
110
|
+
event: HubEventData;
|
|
111
|
+
}
|
|
112
|
+
export interface AgentsConfig {
|
|
113
|
+
defaultAgent: string;
|
|
114
|
+
agents: Record<string, AgentConfig>;
|
|
115
|
+
channelDefaults?: Record<string, string>;
|
|
116
|
+
}
|
|
117
|
+
export interface CronJob {
|
|
118
|
+
id: string;
|
|
119
|
+
name: string;
|
|
120
|
+
agentId: string;
|
|
121
|
+
scheduleType: 'cron' | 'once' | 'interval';
|
|
122
|
+
scheduleValue: string;
|
|
123
|
+
timezone: string;
|
|
124
|
+
message: string;
|
|
125
|
+
enabled: boolean;
|
|
126
|
+
nextRun: string | null;
|
|
127
|
+
createdAt: string;
|
|
128
|
+
createdBy: string;
|
|
129
|
+
}
|
|
130
|
+
export interface CronRun {
|
|
131
|
+
id: string;
|
|
132
|
+
jobId: string;
|
|
133
|
+
firedAt: string;
|
|
134
|
+
status: 'delivered' | 'queued' | 'failed';
|
|
135
|
+
detail?: string;
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Channel Server — spoke 侧
|
|
3
|
+
* 从 cc2wx.ts 搬迁,改为通过 SpokeSocketClient 发消息到 hub
|
|
4
|
+
*/
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import type { SpokeSocketClient } from './socket-client.js';
|
|
7
|
+
import type { HubToSpokeManagementResult } from '../shared/types.js';
|
|
8
|
+
export declare function createChannelServer(agentId: string): Server<{
|
|
9
|
+
method: string;
|
|
10
|
+
params?: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
_meta?: {
|
|
13
|
+
[x: string]: unknown;
|
|
14
|
+
progressToken?: string | number | undefined;
|
|
15
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
16
|
+
taskId: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
} | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
method: string;
|
|
22
|
+
params?: {
|
|
23
|
+
[x: string]: unknown;
|
|
24
|
+
_meta?: {
|
|
25
|
+
[x: string]: unknown;
|
|
26
|
+
progressToken?: string | number | undefined;
|
|
27
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
28
|
+
taskId: string;
|
|
29
|
+
} | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
} | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
[x: string]: unknown;
|
|
34
|
+
_meta?: {
|
|
35
|
+
[x: string]: unknown;
|
|
36
|
+
progressToken?: string | number | undefined;
|
|
37
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
38
|
+
taskId: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
} | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
/** Handle management result from hub */
|
|
43
|
+
export declare function handleManagementResult(msg: HubToSpokeManagementResult): void;
|
|
44
|
+
export declare function setupTools(server: Server, agentId: string, socketClient: SpokeSocketClient): {
|
|
45
|
+
setLastUserId: (userId: string) => void;
|
|
46
|
+
getCurrentUserId: () => string | null;
|
|
47
|
+
};
|
|
48
|
+
export declare function connectTransport(server: Server): Promise<void>;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Channel Server — spoke 侧
|
|
3
|
+
* 从 cc2wx.ts 搬迁,改为通过 SpokeSocketClient 发消息到 hub
|
|
4
|
+
*/
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
export function createChannelServer(agentId) {
|
|
11
|
+
const server = new Server({ name: `cc2im-spoke-${agentId}`, version: '0.1.0' }, {
|
|
12
|
+
capabilities: {
|
|
13
|
+
tools: {},
|
|
14
|
+
experimental: {
|
|
15
|
+
'claude/channel': {},
|
|
16
|
+
'claude/channel/permission': {},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
instructions: [
|
|
20
|
+
'微信消息通过 <channel source="cc2im"> 到达。',
|
|
21
|
+
'使用 weixin_reply 工具回复文字消息。',
|
|
22
|
+
'使用 weixin_send_file 工具发送图片或文件到微信(支持 jpg/png/gif/pdf 等格式)。',
|
|
23
|
+
'回复时从 channel notification 的 meta.userId 提取用户 ID,传入 user_id 参数。',
|
|
24
|
+
'回复不限长度,cc2im 会自动分段发送到微信。',
|
|
25
|
+
].join('\n'),
|
|
26
|
+
});
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
/** Pending management request resolvers */
|
|
30
|
+
const pendingManagement = new Map();
|
|
31
|
+
/** Handle management result from hub */
|
|
32
|
+
export function handleManagementResult(msg) {
|
|
33
|
+
const pending = pendingManagement.get(msg.requestId);
|
|
34
|
+
if (pending) {
|
|
35
|
+
pendingManagement.delete(msg.requestId);
|
|
36
|
+
pending.resolve({ success: msg.success, data: msg.data, error: msg.error });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Send management request and wait for result */
|
|
40
|
+
function sendManagement(socketClient, agentId, action, params) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const requestId = randomUUID();
|
|
43
|
+
pendingManagement.set(requestId, { resolve });
|
|
44
|
+
const sent = socketClient.send({
|
|
45
|
+
type: 'management',
|
|
46
|
+
agentId,
|
|
47
|
+
requestId,
|
|
48
|
+
action,
|
|
49
|
+
params,
|
|
50
|
+
});
|
|
51
|
+
if (!sent) {
|
|
52
|
+
pendingManagement.delete(requestId);
|
|
53
|
+
return resolve({ success: false, error: 'Hub not connected' });
|
|
54
|
+
}
|
|
55
|
+
// Timeout after 30 seconds
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
if (pendingManagement.has(requestId)) {
|
|
58
|
+
pendingManagement.delete(requestId);
|
|
59
|
+
resolve({ success: false, error: 'Management request timed out' });
|
|
60
|
+
}
|
|
61
|
+
}, 30_000);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function setupTools(server, agentId, socketClient) {
|
|
65
|
+
// Simple fallback: most recent userId. CC should pass explicit user_id from
|
|
66
|
+
// notification meta whenever possible; this is only a last-resort default.
|
|
67
|
+
let lastUserId = null;
|
|
68
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
69
|
+
tools: [
|
|
70
|
+
{
|
|
71
|
+
name: 'weixin_reply',
|
|
72
|
+
description: '回复微信消息给最近发消息的用户',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
text: { type: 'string', description: '回复内容' },
|
|
77
|
+
user_id: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: '目标用户 ID — 从 channel notification 的 meta.userId 提取',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ['text'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'weixin_send_file',
|
|
87
|
+
description: '发送图片或文件到微信用户。支持 jpg/png/gif/pdf 等格式。图片以图片消息显示,其他格式以文件消息显示。',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
file_path: { type: 'string', description: '本地文件的绝对路径' },
|
|
92
|
+
user_id: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: '目标用户 ID — 从 channel notification 的 meta.userId 提取',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ['file_path'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'agent_register',
|
|
102
|
+
description: '注册一个新的 agent(CC 实例)',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
name: { type: 'string', description: 'agent 名称,用于 @mention 路由' },
|
|
107
|
+
cwd: { type: 'string', description: '工作目录的绝对路径' },
|
|
108
|
+
claude_args: {
|
|
109
|
+
type: 'array',
|
|
110
|
+
items: { type: 'string' },
|
|
111
|
+
description: '额外的 claude CLI 参数(如 ["--effort", "max"])',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['name', 'cwd'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'agent_deregister',
|
|
119
|
+
description: '注销一个 agent(停止进程 + 从配置中删除)',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
name: { type: 'string', description: 'agent 名称' },
|
|
124
|
+
},
|
|
125
|
+
required: ['name'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'agent_start',
|
|
130
|
+
description: '启动一个已注册的 agent',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
name: { type: 'string', description: 'agent 名称' },
|
|
135
|
+
},
|
|
136
|
+
required: ['name'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'agent_stop',
|
|
141
|
+
description: '停止一个正在运行的 agent',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
name: { type: 'string', description: 'agent 名称' },
|
|
146
|
+
},
|
|
147
|
+
required: ['name'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'agent_list',
|
|
152
|
+
description: '列出所有 agent 及其状态',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'hub_cron_create',
|
|
160
|
+
description: '创建持久化定时任务(hub 管理,重启不丢失)。到点给指定 agent 发一条消息。',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
name: { type: 'string', description: '任务名称(如"每日晨报")' },
|
|
165
|
+
agent_id: { type: 'string', description: '目标 agent 名称(默认自己)' },
|
|
166
|
+
schedule_type: { type: 'string', enum: ['cron', 'once', 'interval'], description: 'cron=重复(cron表达式) | once=一次性(ISO时间戳) | interval=固定间隔(毫秒)' },
|
|
167
|
+
schedule_value: { type: 'string', description: 'cron: "0 9 * * *" | once: "2026-04-01T09:00:00+08:00" | interval: "3600000"' },
|
|
168
|
+
timezone: { type: 'string', description: 'IANA 时区(默认 Asia/Shanghai)' },
|
|
169
|
+
message: { type: 'string', description: '到点发给 agent 的消息内容' },
|
|
170
|
+
},
|
|
171
|
+
required: ['name', 'schedule_type', 'schedule_value', 'message'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'hub_cron_list',
|
|
176
|
+
description: '列出持久化定时任务(可按 agent 筛选)',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
agent_id: { type: 'string', description: '按 agent 筛选(不填则列出所有)' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'hub_cron_delete',
|
|
186
|
+
description: '删除一个持久化定时任务',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
job_id: { type: 'string', description: '任务 ID' },
|
|
191
|
+
},
|
|
192
|
+
required: ['job_id'],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
}));
|
|
197
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
198
|
+
const { name, arguments: args } = request.params;
|
|
199
|
+
switch (name) {
|
|
200
|
+
case 'weixin_reply': {
|
|
201
|
+
const { text, user_id } = args;
|
|
202
|
+
const targetId = user_id || lastUserId;
|
|
203
|
+
if (!targetId) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text', text: '没有可回复的用户,等待微信消息...' }],
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const sent = socketClient.send({
|
|
210
|
+
type: 'reply',
|
|
211
|
+
agentId,
|
|
212
|
+
userId: targetId,
|
|
213
|
+
text,
|
|
214
|
+
});
|
|
215
|
+
if (!sent) {
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: 'text', text: 'Hub 未连接,消息未送达。稍后重试。' }],
|
|
218
|
+
isError: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: 'text', text: `已发送到微信用户 ${targetId}` }],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
case 'weixin_send_file': {
|
|
226
|
+
const { file_path, user_id } = args;
|
|
227
|
+
const targetId = user_id || lastUserId;
|
|
228
|
+
if (!targetId) {
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: 'text', text: '没有可回复的用户,等待微信消息...' }],
|
|
231
|
+
isError: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Validate file exists
|
|
235
|
+
if (!existsSync(file_path)) {
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: 'text', text: `文件不存在: ${file_path}` }],
|
|
238
|
+
isError: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const sent = socketClient.send({
|
|
242
|
+
type: 'send_file',
|
|
243
|
+
agentId,
|
|
244
|
+
userId: targetId,
|
|
245
|
+
filePath: file_path,
|
|
246
|
+
});
|
|
247
|
+
if (!sent) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: 'text', text: 'Hub 未连接,文件未送达。' }],
|
|
250
|
+
isError: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: 'text', text: `文件已发送到微信用户 ${targetId}: ${file_path}` }],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
case 'agent_register': {
|
|
258
|
+
const { name: agentName, cwd, claude_args } = args;
|
|
259
|
+
const result = await sendManagement(socketClient, agentId, 'register', {
|
|
260
|
+
name: agentName, cwd, claudeArgs: claude_args,
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
content: [{ type: 'text', text: result.success
|
|
264
|
+
? `Agent "${agentName}" 已注册 (cwd: ${cwd})`
|
|
265
|
+
: `注册失败: ${result.error}` }],
|
|
266
|
+
isError: !result.success,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
case 'agent_deregister': {
|
|
270
|
+
const { name: agentName } = args;
|
|
271
|
+
const result = await sendManagement(socketClient, agentId, 'deregister', { name: agentName });
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: 'text', text: result.success
|
|
274
|
+
? `Agent "${agentName}" 已注销`
|
|
275
|
+
: `注销失败: ${result.error}` }],
|
|
276
|
+
isError: !result.success,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
case 'agent_start': {
|
|
280
|
+
const { name: agentName } = args;
|
|
281
|
+
const result = await sendManagement(socketClient, agentId, 'start', { name: agentName });
|
|
282
|
+
return {
|
|
283
|
+
content: [{ type: 'text', text: result.success
|
|
284
|
+
? `Agent "${agentName}" 已启动`
|
|
285
|
+
: `启动失败: ${result.error}` }],
|
|
286
|
+
isError: !result.success,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
case 'agent_stop': {
|
|
290
|
+
const { name: agentName } = args;
|
|
291
|
+
const result = await sendManagement(socketClient, agentId, 'stop', { name: agentName });
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: 'text', text: result.success
|
|
294
|
+
? `Agent "${agentName}" 已停止`
|
|
295
|
+
: `停止失败: ${result.error}` }],
|
|
296
|
+
isError: !result.success,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
case 'agent_list': {
|
|
300
|
+
const result = await sendManagement(socketClient, agentId, 'list');
|
|
301
|
+
if (!result.success) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: `查询失败: ${result.error}` }],
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const agents = result.data;
|
|
308
|
+
if (agents.length === 0) {
|
|
309
|
+
return { content: [{ type: 'text', text: '没有已注册的 agent' }] };
|
|
310
|
+
}
|
|
311
|
+
const lines = agents.map(a => `${a.isDefault ? '★' : '·'} ${a.name} [${a.status}] — ${a.cwd}` +
|
|
312
|
+
(a.claudeArgs.length ? ` (${a.claudeArgs.join(' ')})` : '') +
|
|
313
|
+
(a.autoStart ? ' [autoStart]' : ''));
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
case 'hub_cron_create': {
|
|
319
|
+
const { name: jobName, agent_id, schedule_type, schedule_value, timezone, message } = args;
|
|
320
|
+
const result = await sendManagement(socketClient, agentId, 'cron_create', {
|
|
321
|
+
name: jobName, agentId: agent_id, scheduleType: schedule_type,
|
|
322
|
+
scheduleValue: schedule_value, timezone, message,
|
|
323
|
+
});
|
|
324
|
+
if (!result.success) {
|
|
325
|
+
return { content: [{ type: 'text', text: `创建失败: ${result.error}` }], isError: true };
|
|
326
|
+
}
|
|
327
|
+
const job = result.data;
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: 'text', text: `✅ 定时任务已创建\n` +
|
|
330
|
+
`ID: ${job.id}\n` +
|
|
331
|
+
`名称: ${job.name}\n` +
|
|
332
|
+
`目标: ${job.agentId}\n` +
|
|
333
|
+
`调度: ${job.scheduleType} "${job.scheduleValue}"\n` +
|
|
334
|
+
`下次执行: ${job.nextRun}\n` +
|
|
335
|
+
`消息: ${job.message}` }],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
case 'hub_cron_list': {
|
|
339
|
+
const { agent_id } = args;
|
|
340
|
+
const result = await sendManagement(socketClient, agentId, 'cron_list', { agentId: agent_id });
|
|
341
|
+
if (!result.success) {
|
|
342
|
+
return { content: [{ type: 'text', text: `查询失败: ${result.error}` }], isError: true };
|
|
343
|
+
}
|
|
344
|
+
const jobs = result.data;
|
|
345
|
+
if (jobs.length === 0) {
|
|
346
|
+
return { content: [{ type: 'text', text: '没有定时任务' }] };
|
|
347
|
+
}
|
|
348
|
+
const lines = jobs.map((j) => {
|
|
349
|
+
const status = j.enabled ? '🟢' : '⏸️';
|
|
350
|
+
const runs = j.recentRuns?.length ? ` (最近: ${j.recentRuns[0].status} @ ${j.recentRuns[0].firedAt})` : '';
|
|
351
|
+
return `${status} ${j.name} [${j.scheduleType}: ${j.scheduleValue}] → ${j.agentId}\n 下次: ${j.nextRun || '无'}${runs}\n ID: ${j.id}`;
|
|
352
|
+
});
|
|
353
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
|
|
354
|
+
}
|
|
355
|
+
case 'hub_cron_delete': {
|
|
356
|
+
const { job_id } = args;
|
|
357
|
+
const result = await sendManagement(socketClient, agentId, 'cron_delete', { jobId: job_id });
|
|
358
|
+
return {
|
|
359
|
+
content: [{ type: 'text', text: result.success ? '✅ 定时任务已删除' : `删除失败: ${result.error}` }],
|
|
360
|
+
isError: !result.success,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
default:
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
366
|
+
isError: true,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
function setLastUserId(userId) {
|
|
371
|
+
lastUserId = userId;
|
|
372
|
+
}
|
|
373
|
+
/** The userId of the most recent message (for permission routing). */
|
|
374
|
+
function getCurrentUserId() {
|
|
375
|
+
return lastUserId;
|
|
376
|
+
}
|
|
377
|
+
return { setLastUserId, getCurrentUserId };
|
|
378
|
+
}
|
|
379
|
+
export async function connectTransport(server) {
|
|
380
|
+
const transport = new StdioServerTransport();
|
|
381
|
+
await server.connect(transport);
|
|
382
|
+
console.log('[spoke] MCP server connected via stdio');
|
|
383
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc2im Spoke — MCP channel server,桥接 CC ↔ Hub
|
|
3
|
+
*
|
|
4
|
+
* 启动流程:
|
|
5
|
+
* 1. 从参数读取 agentId
|
|
6
|
+
* 2. 连接 hub Unix socket,注册
|
|
7
|
+
* 3. 启动 MCP channel server(stdio transport,给 CC 用)
|
|
8
|
+
* 4. hub 消息 → MCP channel notification → CC
|
|
9
|
+
* 5. CC weixin_reply → spoke → hub → 微信
|
|
10
|
+
* 6. CC permission_request → spoke → hub → 微信
|
|
11
|
+
* 7. 微信 verdict → hub → spoke → CC
|
|
12
|
+
*/
|
|
13
|
+
export {};
|