opc-agent 1.2.0 → 1.2.1
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/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +75 -75
- package/README.md +429 -429
- package/README.zh-CN.md +415 -415
- package/dist/channels/web.js +256 -256
- package/dist/core/knowledge.d.ts +0 -5
- package/dist/core/knowledge.js +2 -39
- package/dist/core/streaming.d.ts +56 -0
- package/dist/core/streaming.js +160 -0
- package/dist/deploy/hermes.js +22 -22
- package/dist/deploy/openclaw.js +31 -31
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +14 -158
- package/dist/schema/oad.d.ts +3 -3
- package/dist/templates/code-reviewer.js +5 -5
- package/dist/templates/customer-service.js +2 -2
- package/dist/templates/data-analyst.js +5 -5
- package/dist/templates/knowledge-base.js +2 -2
- package/dist/templates/sales-assistant.js +4 -4
- package/dist/templates/teacher.js +6 -6
- package/dist/tools/gateway.d.ts +28 -0
- package/dist/tools/gateway.js +177 -0
- package/docs/.vitepress/config.ts +103 -103
- package/docs/api/cli.md +48 -48
- package/docs/api/oad-schema.md +64 -64
- package/docs/api/sdk.md +80 -80
- package/docs/guide/concepts.md +51 -51
- package/docs/guide/configuration.md +79 -79
- package/docs/guide/deployment.md +42 -42
- package/docs/guide/getting-started.md +44 -44
- package/docs/guide/templates.md +28 -28
- package/docs/guide/testing.md +84 -84
- package/docs/index.md +27 -27
- package/docs/zh/api/cli.md +54 -54
- package/docs/zh/api/oad-schema.md +87 -87
- package/docs/zh/api/sdk.md +102 -102
- package/docs/zh/guide/concepts.md +104 -104
- package/docs/zh/guide/configuration.md +135 -135
- package/docs/zh/guide/deployment.md +81 -81
- package/docs/zh/guide/getting-started.md +82 -82
- package/docs/zh/guide/templates.md +84 -84
- package/docs/zh/guide/testing.md +88 -88
- package/docs/zh/index.md +27 -27
- package/examples/customer-service-demo/README.md +90 -90
- package/examples/customer-service-demo/oad.yaml +107 -107
- package/package.json +50 -50
- package/src/analytics/index.ts +66 -66
- package/src/channels/discord.ts +192 -192
- package/src/channels/email.ts +177 -177
- package/src/channels/feishu.ts +236 -236
- package/src/channels/index.ts +15 -15
- package/src/channels/slack.ts +160 -160
- package/src/channels/telegram.ts +90 -90
- package/src/channels/voice.ts +106 -106
- package/src/channels/webhook.ts +199 -199
- package/src/channels/websocket.ts +87 -87
- package/src/channels/wechat.ts +149 -149
- package/src/core/a2a.ts +143 -143
- package/src/core/agent.ts +152 -152
- package/src/core/analytics-engine.ts +186 -186
- package/src/core/auth.ts +57 -57
- package/src/core/cache.ts +141 -141
- package/src/core/compose.ts +77 -77
- package/src/core/config.ts +14 -14
- package/src/core/errors.ts +148 -148
- package/src/core/hitl.ts +138 -138
- package/src/core/logger.ts +57 -57
- package/src/core/orchestrator.ts +215 -215
- package/src/core/performance.ts +187 -187
- package/src/core/rate-limiter.ts +128 -128
- package/src/core/room.ts +109 -109
- package/src/core/runtime.ts +152 -152
- package/src/core/sandbox.ts +101 -101
- package/src/core/security.ts +171 -171
- package/src/core/streaming.ts +195 -0
- package/src/core/types.ts +68 -68
- package/src/core/versioning.ts +106 -106
- package/src/core/watch.ts +178 -178
- package/src/core/workflow.ts +235 -235
- package/src/deploy/hermes.ts +156 -156
- package/src/deploy/openclaw.ts +200 -200
- package/src/dtv/data.ts +29 -29
- package/src/dtv/trust.ts +43 -43
- package/src/dtv/value.ts +47 -47
- package/src/i18n/index.ts +216 -216
- package/src/index.ts +6 -0
- package/src/marketplace/index.ts +223 -223
- package/src/memory/deepbrain.ts +108 -108
- package/src/memory/index.ts +34 -34
- package/src/plugins/index.ts +208 -208
- package/src/schema/oad.ts +155 -155
- package/src/skills/base.ts +16 -16
- package/src/skills/document.ts +100 -100
- package/src/skills/http.ts +35 -35
- package/src/skills/index.ts +27 -27
- package/src/skills/scheduler.ts +80 -80
- package/src/skills/webhook-trigger.ts +59 -59
- package/src/templates/code-reviewer.ts +34 -34
- package/src/templates/customer-service.ts +80 -80
- package/src/templates/data-analyst.ts +70 -70
- package/src/templates/executive-assistant.ts +71 -71
- package/src/templates/financial-advisor.ts +60 -60
- package/src/templates/knowledge-base.ts +31 -31
- package/src/templates/legal-assistant.ts +71 -71
- package/src/templates/sales-assistant.ts +79 -79
- package/src/templates/teacher.ts +79 -79
- package/src/testing/index.ts +181 -181
- package/src/tools/calculator.ts +73 -73
- package/src/tools/datetime.ts +149 -149
- package/src/tools/gateway.ts +220 -0
- package/src/tools/json-transform.ts +187 -187
- package/src/tools/mcp.ts +76 -76
- package/src/tools/text-analysis.ts +116 -116
- package/templates/Dockerfile +15 -15
- package/templates/code-reviewer/README.md +27 -27
- package/templates/code-reviewer/oad.yaml +41 -41
- package/templates/customer-service/README.md +22 -22
- package/templates/customer-service/oad.yaml +36 -36
- package/templates/docker-compose.yml +21 -21
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/knowledge-base/README.md +28 -28
- package/templates/knowledge-base/oad.yaml +38 -38
- package/templates/sales-assistant/README.md +26 -26
- package/templates/sales-assistant/oad.yaml +43 -43
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/tests/a2a.test.ts +66 -66
- package/tests/agent.test.ts +72 -72
- package/tests/analytics.test.ts +50 -50
- package/tests/channel.test.ts +39 -39
- package/tests/e2e.test.ts +134 -134
- package/tests/errors.test.ts +83 -83
- package/tests/gateway.test.ts +71 -0
- package/tests/hitl.test.ts +71 -71
- package/tests/i18n.test.ts +41 -41
- package/tests/mcp.test.ts +54 -54
- package/tests/oad.test.ts +68 -68
- package/tests/performance.test.ts +115 -115
- package/tests/plugin.test.ts +74 -74
- package/tests/room.test.ts +106 -106
- package/tests/runtime.test.ts +42 -42
- package/tests/sandbox.test.ts +46 -46
- package/tests/security.test.ts +60 -60
- package/tests/streaming.test.ts +109 -0
- package/tests/templates.test.ts +77 -77
- package/tests/v070.test.ts +76 -76
- package/tests/versioning.test.ts +75 -75
- package/tests/voice.test.ts +61 -61
- package/tests/webhook.test.ts +29 -29
- package/tests/workflow.test.ts +143 -143
- package/tsconfig.json +19 -19
- package/vitest.config.ts +9 -9
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import type { Message } from '../core/types';
|
|
2
|
-
import { BaseChannel } from './index';
|
|
3
|
-
import { WebSocketServer, type WebSocket } from 'ws';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* WebSocket channel — real-time bidirectional communication.
|
|
7
|
-
*/
|
|
8
|
-
export class WebSocketChannel extends BaseChannel {
|
|
9
|
-
readonly type = 'websocket';
|
|
10
|
-
private wss: WebSocketServer | null = null;
|
|
11
|
-
private port: number;
|
|
12
|
-
private clients: Set<WebSocket> = new Set();
|
|
13
|
-
|
|
14
|
-
constructor(port: number = 3002) {
|
|
15
|
-
super();
|
|
16
|
-
this.port = port;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async start(): Promise<void> {
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
this.wss = new WebSocketServer({ port: this.port });
|
|
22
|
-
|
|
23
|
-
this.wss.on('connection', (ws) => {
|
|
24
|
-
this.clients.add(ws);
|
|
25
|
-
|
|
26
|
-
ws.on('message', async (data) => {
|
|
27
|
-
if (!this.handler) return;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const parsed = JSON.parse(data.toString());
|
|
31
|
-
const msg: Message = {
|
|
32
|
-
id: parsed.id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
33
|
-
role: 'user',
|
|
34
|
-
content: parsed.content ?? parsed.message ?? data.toString(),
|
|
35
|
-
timestamp: Date.now(),
|
|
36
|
-
metadata: {
|
|
37
|
-
sessionId: parsed.sessionId ?? 'ws-default',
|
|
38
|
-
platform: 'websocket',
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const response = await this.handler(msg);
|
|
43
|
-
ws.send(JSON.stringify({
|
|
44
|
-
id: response.id,
|
|
45
|
-
content: response.content,
|
|
46
|
-
timestamp: response.timestamp,
|
|
47
|
-
}));
|
|
48
|
-
} catch (err) {
|
|
49
|
-
ws.send(JSON.stringify({ error: 'Invalid message format' }));
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
ws.on('close', () => {
|
|
54
|
-
this.clients.delete(ws);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
this.wss.on('listening', () => {
|
|
61
|
-
console.log(`[WebSocketChannel] Listening on port ${this.port}`);
|
|
62
|
-
resolve();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async stop(): Promise<void> {
|
|
68
|
-
for (const client of this.clients) {
|
|
69
|
-
client.close();
|
|
70
|
-
}
|
|
71
|
-
this.clients.clear();
|
|
72
|
-
|
|
73
|
-
return new Promise((resolve, reject) => {
|
|
74
|
-
if (!this.wss) return resolve();
|
|
75
|
-
this.wss.close((err) => (err ? reject(err) : resolve()));
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
broadcast(content: string): void {
|
|
80
|
-
const msg = JSON.stringify({ type: 'broadcast', content, timestamp: Date.now() });
|
|
81
|
-
for (const client of this.clients) {
|
|
82
|
-
if (client.readyState === 1) { // OPEN
|
|
83
|
-
client.send(msg);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import type { Message } from '../core/types';
|
|
2
|
+
import { BaseChannel } from './index';
|
|
3
|
+
import { WebSocketServer, type WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WebSocket channel — real-time bidirectional communication.
|
|
7
|
+
*/
|
|
8
|
+
export class WebSocketChannel extends BaseChannel {
|
|
9
|
+
readonly type = 'websocket';
|
|
10
|
+
private wss: WebSocketServer | null = null;
|
|
11
|
+
private port: number;
|
|
12
|
+
private clients: Set<WebSocket> = new Set();
|
|
13
|
+
|
|
14
|
+
constructor(port: number = 3002) {
|
|
15
|
+
super();
|
|
16
|
+
this.port = port;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async start(): Promise<void> {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
this.wss = new WebSocketServer({ port: this.port });
|
|
22
|
+
|
|
23
|
+
this.wss.on('connection', (ws) => {
|
|
24
|
+
this.clients.add(ws);
|
|
25
|
+
|
|
26
|
+
ws.on('message', async (data) => {
|
|
27
|
+
if (!this.handler) return;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(data.toString());
|
|
31
|
+
const msg: Message = {
|
|
32
|
+
id: parsed.id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
33
|
+
role: 'user',
|
|
34
|
+
content: parsed.content ?? parsed.message ?? data.toString(),
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
metadata: {
|
|
37
|
+
sessionId: parsed.sessionId ?? 'ws-default',
|
|
38
|
+
platform: 'websocket',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const response = await this.handler(msg);
|
|
43
|
+
ws.send(JSON.stringify({
|
|
44
|
+
id: response.id,
|
|
45
|
+
content: response.content,
|
|
46
|
+
timestamp: response.timestamp,
|
|
47
|
+
}));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
ws.send(JSON.stringify({ error: 'Invalid message format' }));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
ws.on('close', () => {
|
|
54
|
+
this.clients.delete(ws);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.wss.on('listening', () => {
|
|
61
|
+
console.log(`[WebSocketChannel] Listening on port ${this.port}`);
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async stop(): Promise<void> {
|
|
68
|
+
for (const client of this.clients) {
|
|
69
|
+
client.close();
|
|
70
|
+
}
|
|
71
|
+
this.clients.clear();
|
|
72
|
+
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
if (!this.wss) return resolve();
|
|
75
|
+
this.wss.close((err) => (err ? reject(err) : resolve()));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
broadcast(content: string): void {
|
|
80
|
+
const msg = JSON.stringify({ type: 'broadcast', content, timestamp: Date.now() });
|
|
81
|
+
for (const client of this.clients) {
|
|
82
|
+
if (client.readyState === 1) { // OPEN
|
|
83
|
+
client.send(msg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/channels/wechat.ts
CHANGED
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
import { BaseChannel } from './index';
|
|
2
|
-
import type { Message } from '../core/types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* WeChat Channel (Stub) — v0.8.0
|
|
6
|
-
* WeChat Official Account message handling, template messages, QR code login.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface WeChatChannelConfig {
|
|
10
|
-
/** WeChat Official Account AppID */
|
|
11
|
-
appId: string;
|
|
12
|
-
/** WeChat Official Account AppSecret */
|
|
13
|
-
appSecret: string;
|
|
14
|
-
/** Verification token for message validation */
|
|
15
|
-
token: string;
|
|
16
|
-
/** AES encoding key for encrypted messages */
|
|
17
|
-
encodingAESKey?: string;
|
|
18
|
-
/** HTTP server port (default: 3002) */
|
|
19
|
-
port?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface WeChatMessage {
|
|
23
|
-
toUserName: string;
|
|
24
|
-
fromUserName: string;
|
|
25
|
-
createTime: number;
|
|
26
|
-
msgType: 'text' | 'image' | 'voice' | 'video' | 'event';
|
|
27
|
-
content?: string;
|
|
28
|
-
msgId?: string;
|
|
29
|
-
event?: string;
|
|
30
|
-
eventKey?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TemplateMessageData {
|
|
34
|
-
toUser: string;
|
|
35
|
-
templateId: string;
|
|
36
|
-
url?: string;
|
|
37
|
-
data: Record<string, { value: string; color?: string }>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class WeChatChannel extends BaseChannel {
|
|
41
|
-
type = 'wechat';
|
|
42
|
-
private config: WeChatChannelConfig;
|
|
43
|
-
private accessToken: string | null = null;
|
|
44
|
-
private tokenExpiry = 0;
|
|
45
|
-
|
|
46
|
-
constructor(config: WeChatChannelConfig) {
|
|
47
|
-
super();
|
|
48
|
-
this.config = config;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async start(): Promise<void> {
|
|
52
|
-
// TODO: Start HTTP server to receive WeChat push messages
|
|
53
|
-
// 1. Verify signature on GET requests
|
|
54
|
-
// 2. Parse XML messages on POST requests
|
|
55
|
-
// 3. Route to handler and reply with XML
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async stop(): Promise<void> {
|
|
59
|
-
// TODO: Stop HTTP server
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Get or refresh access token */
|
|
63
|
-
async getAccessToken(): Promise<string> {
|
|
64
|
-
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
65
|
-
return this.accessToken;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// TODO: Implement token refresh
|
|
69
|
-
// const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.config.appId}&secret=${this.config.appSecret}`;
|
|
70
|
-
// const res = await fetch(url);
|
|
71
|
-
// const data = await res.json();
|
|
72
|
-
// this.accessToken = data.access_token;
|
|
73
|
-
// this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000;
|
|
74
|
-
|
|
75
|
-
return this.accessToken ?? '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Handle incoming WeChat message */
|
|
79
|
-
async handleMessage(wxMsg: WeChatMessage): Promise<string> {
|
|
80
|
-
if (wxMsg.msgType === 'event') {
|
|
81
|
-
return this.handleEvent(wxMsg);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const message = this.wechatToMessage(wxMsg);
|
|
85
|
-
if (this.handler) {
|
|
86
|
-
const reply = await this.handler(message);
|
|
87
|
-
return reply.content;
|
|
88
|
-
}
|
|
89
|
-
return '';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Handle WeChat events (subscribe, scan, etc.) */
|
|
93
|
-
private handleEvent(wxMsg: WeChatMessage): string {
|
|
94
|
-
switch (wxMsg.event) {
|
|
95
|
-
case 'subscribe':
|
|
96
|
-
return 'Welcome! How can I help you?';
|
|
97
|
-
case 'SCAN':
|
|
98
|
-
return `QR code scanned: ${wxMsg.eventKey}`;
|
|
99
|
-
default:
|
|
100
|
-
return '';
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Convert WeChat message to internal Message */
|
|
105
|
-
private wechatToMessage(wxMsg: WeChatMessage): Message {
|
|
106
|
-
return {
|
|
107
|
-
id: wxMsg.msgId ?? `wx-${wxMsg.createTime}`,
|
|
108
|
-
role: 'user',
|
|
109
|
-
content: wxMsg.content ?? '',
|
|
110
|
-
timestamp: wxMsg.createTime * 1000,
|
|
111
|
-
metadata: {
|
|
112
|
-
channel: 'wechat',
|
|
113
|
-
fromUser: wxMsg.fromUserName,
|
|
114
|
-
toUser: wxMsg.toUserName,
|
|
115
|
-
msgType: wxMsg.msgType,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Send template message */
|
|
121
|
-
async sendTemplateMessage(data: TemplateMessageData): Promise<boolean> {
|
|
122
|
-
// TODO: Implement
|
|
123
|
-
// const token = await this.getAccessToken();
|
|
124
|
-
// const url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`;
|
|
125
|
-
// const res = await fetch(url, {
|
|
126
|
-
// method: 'POST',
|
|
127
|
-
// body: JSON.stringify({
|
|
128
|
-
// touser: data.toUser,
|
|
129
|
-
// template_id: data.templateId,
|
|
130
|
-
// url: data.url,
|
|
131
|
-
// data: data.data,
|
|
132
|
-
// }),
|
|
133
|
-
// });
|
|
134
|
-
void data;
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Generate QR code for login (stub) */
|
|
139
|
-
async generateLoginQR(): Promise<{ ticket: string; url: string; expireSeconds: number }> {
|
|
140
|
-
// TODO: Implement with WeChat QR code API
|
|
141
|
-
// const token = await this.getAccessToken();
|
|
142
|
-
// POST to https://api.weixin.qq.com/cgi-bin/qrcode/create
|
|
143
|
-
return {
|
|
144
|
-
ticket: 'stub-ticket',
|
|
145
|
-
url: 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=stub-ticket',
|
|
146
|
-
expireSeconds: 300,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
}
|
|
1
|
+
import { BaseChannel } from './index';
|
|
2
|
+
import type { Message } from '../core/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WeChat Channel (Stub) — v0.8.0
|
|
6
|
+
* WeChat Official Account message handling, template messages, QR code login.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface WeChatChannelConfig {
|
|
10
|
+
/** WeChat Official Account AppID */
|
|
11
|
+
appId: string;
|
|
12
|
+
/** WeChat Official Account AppSecret */
|
|
13
|
+
appSecret: string;
|
|
14
|
+
/** Verification token for message validation */
|
|
15
|
+
token: string;
|
|
16
|
+
/** AES encoding key for encrypted messages */
|
|
17
|
+
encodingAESKey?: string;
|
|
18
|
+
/** HTTP server port (default: 3002) */
|
|
19
|
+
port?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WeChatMessage {
|
|
23
|
+
toUserName: string;
|
|
24
|
+
fromUserName: string;
|
|
25
|
+
createTime: number;
|
|
26
|
+
msgType: 'text' | 'image' | 'voice' | 'video' | 'event';
|
|
27
|
+
content?: string;
|
|
28
|
+
msgId?: string;
|
|
29
|
+
event?: string;
|
|
30
|
+
eventKey?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TemplateMessageData {
|
|
34
|
+
toUser: string;
|
|
35
|
+
templateId: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
data: Record<string, { value: string; color?: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class WeChatChannel extends BaseChannel {
|
|
41
|
+
type = 'wechat';
|
|
42
|
+
private config: WeChatChannelConfig;
|
|
43
|
+
private accessToken: string | null = null;
|
|
44
|
+
private tokenExpiry = 0;
|
|
45
|
+
|
|
46
|
+
constructor(config: WeChatChannelConfig) {
|
|
47
|
+
super();
|
|
48
|
+
this.config = config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async start(): Promise<void> {
|
|
52
|
+
// TODO: Start HTTP server to receive WeChat push messages
|
|
53
|
+
// 1. Verify signature on GET requests
|
|
54
|
+
// 2. Parse XML messages on POST requests
|
|
55
|
+
// 3. Route to handler and reply with XML
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async stop(): Promise<void> {
|
|
59
|
+
// TODO: Stop HTTP server
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Get or refresh access token */
|
|
63
|
+
async getAccessToken(): Promise<string> {
|
|
64
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
65
|
+
return this.accessToken;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// TODO: Implement token refresh
|
|
69
|
+
// const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.config.appId}&secret=${this.config.appSecret}`;
|
|
70
|
+
// const res = await fetch(url);
|
|
71
|
+
// const data = await res.json();
|
|
72
|
+
// this.accessToken = data.access_token;
|
|
73
|
+
// this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000;
|
|
74
|
+
|
|
75
|
+
return this.accessToken ?? '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Handle incoming WeChat message */
|
|
79
|
+
async handleMessage(wxMsg: WeChatMessage): Promise<string> {
|
|
80
|
+
if (wxMsg.msgType === 'event') {
|
|
81
|
+
return this.handleEvent(wxMsg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const message = this.wechatToMessage(wxMsg);
|
|
85
|
+
if (this.handler) {
|
|
86
|
+
const reply = await this.handler(message);
|
|
87
|
+
return reply.content;
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Handle WeChat events (subscribe, scan, etc.) */
|
|
93
|
+
private handleEvent(wxMsg: WeChatMessage): string {
|
|
94
|
+
switch (wxMsg.event) {
|
|
95
|
+
case 'subscribe':
|
|
96
|
+
return 'Welcome! How can I help you?';
|
|
97
|
+
case 'SCAN':
|
|
98
|
+
return `QR code scanned: ${wxMsg.eventKey}`;
|
|
99
|
+
default:
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Convert WeChat message to internal Message */
|
|
105
|
+
private wechatToMessage(wxMsg: WeChatMessage): Message {
|
|
106
|
+
return {
|
|
107
|
+
id: wxMsg.msgId ?? `wx-${wxMsg.createTime}`,
|
|
108
|
+
role: 'user',
|
|
109
|
+
content: wxMsg.content ?? '',
|
|
110
|
+
timestamp: wxMsg.createTime * 1000,
|
|
111
|
+
metadata: {
|
|
112
|
+
channel: 'wechat',
|
|
113
|
+
fromUser: wxMsg.fromUserName,
|
|
114
|
+
toUser: wxMsg.toUserName,
|
|
115
|
+
msgType: wxMsg.msgType,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Send template message */
|
|
121
|
+
async sendTemplateMessage(data: TemplateMessageData): Promise<boolean> {
|
|
122
|
+
// TODO: Implement
|
|
123
|
+
// const token = await this.getAccessToken();
|
|
124
|
+
// const url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`;
|
|
125
|
+
// const res = await fetch(url, {
|
|
126
|
+
// method: 'POST',
|
|
127
|
+
// body: JSON.stringify({
|
|
128
|
+
// touser: data.toUser,
|
|
129
|
+
// template_id: data.templateId,
|
|
130
|
+
// url: data.url,
|
|
131
|
+
// data: data.data,
|
|
132
|
+
// }),
|
|
133
|
+
// });
|
|
134
|
+
void data;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Generate QR code for login (stub) */
|
|
139
|
+
async generateLoginQR(): Promise<{ ticket: string; url: string; expireSeconds: number }> {
|
|
140
|
+
// TODO: Implement with WeChat QR code API
|
|
141
|
+
// const token = await this.getAccessToken();
|
|
142
|
+
// POST to https://api.weixin.qq.com/cgi-bin/qrcode/create
|
|
143
|
+
return {
|
|
144
|
+
ticket: 'stub-ticket',
|
|
145
|
+
url: 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=stub-ticket',
|
|
146
|
+
expireSeconds: 300,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|