opc-agent 4.0.44 → 4.1.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/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/dist/channels/wechat.js +6 -6
- package/dist/cli.js +2 -2
- package/dist/core/runtime.js +18 -0
- package/dist/deploy/index.js +56 -56
- package/dist/providers/index.js +39 -13
- package/dist/studio/server.js +211 -20
- package/dist/studio-ui/index.html +279 -24
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/cli.ts +2 -2
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/runtime.ts +18 -0
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +156 -156
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -608
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +209 -22
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +279 -24
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
package/src/channels/feishu.ts
CHANGED
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
import { BaseChannel } from './index';
|
|
2
|
-
import type { Message } from '../core/types';
|
|
3
|
-
import * as http from 'http';
|
|
4
|
-
import * as https from 'https';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Feishu / Lark Channel — v1.1.0
|
|
8
|
-
*
|
|
9
|
-
* Supports:
|
|
10
|
-
* - Event Subscription (webhook) mode for receiving messages
|
|
11
|
-
* - Bot send via Feishu Open API
|
|
12
|
-
* - URL verification challenge
|
|
13
|
-
* - Message card (interactive) responses
|
|
14
|
-
* - Group chat & P2P messaging
|
|
15
|
-
* - Event deduplication
|
|
16
|
-
* - No external dependencies (uses Node.js built-in http/https)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export interface FeishuChannelConfig {
|
|
20
|
-
/** Feishu App ID */
|
|
21
|
-
appId?: string;
|
|
22
|
-
/** Feishu App Secret */
|
|
23
|
-
appSecret?: string;
|
|
24
|
-
/** Verification token for event subscription */
|
|
25
|
-
verificationToken?: string;
|
|
26
|
-
/** Encrypt key (optional, for encrypted events) */
|
|
27
|
-
encryptKey?: string;
|
|
28
|
-
/** Webhook server port (default: 8081) */
|
|
29
|
-
port?: number;
|
|
30
|
-
/** API base URL (use 'https://open.larksuite.com' for Lark international) */
|
|
31
|
-
apiBase?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface FeishuTokenCache {
|
|
35
|
-
token: string;
|
|
36
|
-
expiresAt: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class FeishuChannel extends BaseChannel {
|
|
40
|
-
readonly type = 'feishu';
|
|
41
|
-
private config: Required<Pick<FeishuChannelConfig, 'port' | 'apiBase'>> & FeishuChannelConfig;
|
|
42
|
-
private server: http.Server | null = null;
|
|
43
|
-
private tokenCache: FeishuTokenCache | null = null;
|
|
44
|
-
private processedEvents = new Set<string>();
|
|
45
|
-
|
|
46
|
-
constructor(config: FeishuChannelConfig = {}) {
|
|
47
|
-
super();
|
|
48
|
-
this.config = {
|
|
49
|
-
appId: config.appId ?? process.env.FEISHU_APP_ID ?? '',
|
|
50
|
-
appSecret: config.appSecret ?? process.env.FEISHU_APP_SECRET ?? '',
|
|
51
|
-
verificationToken: config.verificationToken ?? process.env.FEISHU_VERIFICATION_TOKEN ?? '',
|
|
52
|
-
encryptKey: config.encryptKey ?? process.env.FEISHU_ENCRYPT_KEY,
|
|
53
|
-
port: config.port ?? 8081,
|
|
54
|
-
apiBase: config.apiBase ?? 'https://open.feishu.cn',
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async start(): Promise<void> {
|
|
59
|
-
if (!this.config.appId || !this.config.appSecret) {
|
|
60
|
-
console.warn('[FeishuChannel] Missing appId/appSecret. Set FEISHU_APP_ID and FEISHU_APP_SECRET.');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
this.server = http.createServer(async (req, res) => {
|
|
65
|
-
if (req.method === 'GET' && req.url === '/health') {
|
|
66
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
67
|
-
res.end(JSON.stringify({ status: 'ok', channel: 'feishu' }));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (req.method !== 'POST') {
|
|
72
|
-
res.writeHead(404);
|
|
73
|
-
res.end();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const body = await this.readBody(req);
|
|
79
|
-
const parsed = JSON.parse(body);
|
|
80
|
-
await this.handleEvent(parsed, res);
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error('[FeishuChannel] Error:', err);
|
|
83
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
84
|
-
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
this.server!.listen(this.config.port, () => {
|
|
90
|
-
console.log(`[FeishuChannel] Listening on port ${this.config.port}`);
|
|
91
|
-
resolve();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async stop(): Promise<void> {
|
|
97
|
-
return new Promise((resolve, reject) => {
|
|
98
|
-
if (!this.server) return resolve();
|
|
99
|
-
this.server.close((err) => (err ? reject(err) : resolve()));
|
|
100
|
-
this.server = null;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Handle Feishu event */
|
|
105
|
-
private async handleEvent(body: any, res: http.ServerResponse): Promise<void> {
|
|
106
|
-
// URL verification challenge
|
|
107
|
-
if (body.type === 'url_verification') {
|
|
108
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
109
|
-
res.end(JSON.stringify({ challenge: body.challenge }));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Deduplicate events
|
|
114
|
-
const eventId = body.header?.event_id;
|
|
115
|
-
if (eventId && this.processedEvents.has(eventId)) {
|
|
116
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
117
|
-
res.end(JSON.stringify({ ok: true }));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (eventId) {
|
|
121
|
-
this.processedEvents.add(eventId);
|
|
122
|
-
if (this.processedEvents.size > 1000) {
|
|
123
|
-
const arr = [...this.processedEvents];
|
|
124
|
-
this.processedEvents = new Set(arr.slice(-500));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Verify token
|
|
129
|
-
if (this.config.verificationToken && body.header?.token !== this.config.verificationToken) {
|
|
130
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
131
|
-
res.end(JSON.stringify({ error: 'Invalid verification token' }));
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Handle im.message.receive_v1
|
|
136
|
-
const event = body.event;
|
|
137
|
-
if (body.header?.event_type === 'im.message.receive_v1' && this.handler) {
|
|
138
|
-
const msgBody = event?.message;
|
|
139
|
-
if (!msgBody) {
|
|
140
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
141
|
-
res.end(JSON.stringify({ ok: true }));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const msgType = msgBody.message_type;
|
|
146
|
-
let content = '';
|
|
147
|
-
if (msgType === 'text') {
|
|
148
|
-
try {
|
|
149
|
-
const parsed = JSON.parse(msgBody.content);
|
|
150
|
-
content = parsed.text ?? '';
|
|
151
|
-
} catch {
|
|
152
|
-
content = msgBody.content ?? '';
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
156
|
-
res.end(JSON.stringify({ ok: true }));
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Strip @bot mentions
|
|
161
|
-
content = content.replace(/@_user_\d+/g, '').trim();
|
|
162
|
-
if (!content) {
|
|
163
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
164
|
-
res.end(JSON.stringify({ ok: true }));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const chatId = msgBody.chat_id;
|
|
169
|
-
const senderId = event.sender?.sender_id?.open_id ?? 'unknown';
|
|
170
|
-
|
|
171
|
-
const msg: Message = {
|
|
172
|
-
id: `feishu_${msgBody.message_id}`,
|
|
173
|
-
role: 'user',
|
|
174
|
-
content,
|
|
175
|
-
timestamp: parseInt(msgBody.create_time, 10) || Date.now(),
|
|
176
|
-
metadata: {
|
|
177
|
-
sessionId: `feishu_${chatId}`,
|
|
178
|
-
chatId,
|
|
179
|
-
userId: senderId,
|
|
180
|
-
platform: 'feishu',
|
|
181
|
-
messageId: msgBody.message_id,
|
|
182
|
-
chatType: msgBody.chat_type,
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Don't block the response
|
|
187
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
-
res.end(JSON.stringify({ ok: true }));
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const response = await this.handler(msg);
|
|
192
|
-
await this.sendTextMessage(chatId, response.content);
|
|
193
|
-
} catch (err) {
|
|
194
|
-
console.error('[FeishuChannel] Handler error:', err);
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
200
|
-
res.end(JSON.stringify({ ok: true }));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Parse Feishu event body (exported for testing) */
|
|
204
|
-
static parseEventBody(body: any): { type: string; challenge?: string; eventType?: string; content?: string; chatId?: string; senderId?: string; messageId?: string } {
|
|
205
|
-
if (body.type === 'url_verification') {
|
|
206
|
-
return { type: 'url_verification', challenge: body.challenge };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const eventType = body.header?.event_type;
|
|
210
|
-
const event = body.event;
|
|
211
|
-
|
|
212
|
-
if (eventType === 'im.message.receive_v1' && event?.message) {
|
|
213
|
-
const msgBody = event.message;
|
|
214
|
-
let content = '';
|
|
215
|
-
if (msgBody.message_type === 'text') {
|
|
216
|
-
try {
|
|
217
|
-
const parsed = JSON.parse(msgBody.content);
|
|
218
|
-
content = parsed.text ?? '';
|
|
219
|
-
} catch {
|
|
220
|
-
content = msgBody.content ?? '';
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
type: 'message',
|
|
226
|
-
eventType,
|
|
227
|
-
content: content.replace(/@_user_\d+/g, '').trim(),
|
|
228
|
-
chatId: msgBody.chat_id,
|
|
229
|
-
senderId: event.sender?.sender_id?.open_id,
|
|
230
|
-
messageId: msgBody.message_id,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return { type: 'unknown', eventType };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Get tenant access token (cached) */
|
|
238
|
-
private async getAccessToken(): Promise<string> {
|
|
239
|
-
if (this.tokenCache && Date.now() < this.tokenCache.expiresAt) {
|
|
240
|
-
return this.tokenCache.token;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const body = JSON.stringify({
|
|
244
|
-
app_id: this.config.appId,
|
|
245
|
-
app_secret: this.config.appSecret,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const result = await this.httpsPost(
|
|
249
|
-
`${this.config.apiBase}/open-apis/auth/v3/tenant_access_token/internal`,
|
|
250
|
-
body
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
const data = JSON.parse(result);
|
|
254
|
-
if (data.code !== 0) {
|
|
255
|
-
throw new Error(`[FeishuChannel] Failed to get access token: ${result}`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
this.tokenCache = {
|
|
259
|
-
token: data.tenant_access_token,
|
|
260
|
-
expiresAt: Date.now() + (data.expire - 60) * 1000,
|
|
261
|
-
};
|
|
262
|
-
return this.tokenCache.token;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** Send a text message to a chat */
|
|
266
|
-
async sendTextMessage(chatId: string, text: string): Promise<void> {
|
|
267
|
-
const token = await this.getAccessToken();
|
|
268
|
-
const body = JSON.stringify({
|
|
269
|
-
receive_id: chatId,
|
|
270
|
-
msg_type: 'text',
|
|
271
|
-
content: JSON.stringify({ text }),
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const url = `${this.config.apiBase}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
|
275
|
-
await this.httpsPostWithAuth(url, body, token);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** Send an interactive card message */
|
|
279
|
-
async sendCardMessage(chatId: string, card: Record<string, unknown>): Promise<void> {
|
|
280
|
-
const token = await this.getAccessToken();
|
|
281
|
-
const body = JSON.stringify({
|
|
282
|
-
receive_id: chatId,
|
|
283
|
-
msg_type: 'interactive',
|
|
284
|
-
content: JSON.stringify(card),
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const url = `${this.config.apiBase}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
|
288
|
-
await this.httpsPostWithAuth(url, body, token);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Read request body */
|
|
292
|
-
private readBody(req: http.IncomingMessage): Promise<string> {
|
|
293
|
-
return new Promise((resolve, reject) => {
|
|
294
|
-
const chunks: Buffer[] = [];
|
|
295
|
-
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
296
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
297
|
-
req.on('error', reject);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** HTTPS POST */
|
|
302
|
-
private httpsPost(url: string, body: string): Promise<string> {
|
|
303
|
-
return new Promise((resolve, reject) => {
|
|
304
|
-
const parsed = new URL(url);
|
|
305
|
-
const req = https.request({
|
|
306
|
-
hostname: parsed.hostname,
|
|
307
|
-
path: parsed.pathname + parsed.search,
|
|
308
|
-
method: 'POST',
|
|
309
|
-
headers: {
|
|
310
|
-
'Content-Type': 'application/json',
|
|
311
|
-
'Content-Length': Buffer.byteLength(body),
|
|
312
|
-
},
|
|
313
|
-
}, (res) => {
|
|
314
|
-
const chunks: Buffer[] = [];
|
|
315
|
-
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
316
|
-
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
317
|
-
res.on('error', reject);
|
|
318
|
-
});
|
|
319
|
-
req.on('error', reject);
|
|
320
|
-
req.write(body);
|
|
321
|
-
req.end();
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/** HTTPS POST with Bearer auth */
|
|
326
|
-
private httpsPostWithAuth(url: string, body: string, token: string): Promise<string> {
|
|
327
|
-
return new Promise((resolve, reject) => {
|
|
328
|
-
const parsed = new URL(url);
|
|
329
|
-
const req = https.request({
|
|
330
|
-
hostname: parsed.hostname,
|
|
331
|
-
path: parsed.pathname + parsed.search,
|
|
332
|
-
method: 'POST',
|
|
333
|
-
headers: {
|
|
334
|
-
'Content-Type': 'application/json',
|
|
335
|
-
'Content-Length': Buffer.byteLength(body),
|
|
336
|
-
'Authorization': `Bearer ${token}`,
|
|
337
|
-
},
|
|
338
|
-
}, (res) => {
|
|
339
|
-
const chunks: Buffer[] = [];
|
|
340
|
-
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
341
|
-
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
342
|
-
res.on('error', reject);
|
|
343
|
-
});
|
|
344
|
-
req.on('error', reject);
|
|
345
|
-
req.write(body);
|
|
346
|
-
req.end();
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
1
|
+
import { BaseChannel } from './index';
|
|
2
|
+
import type { Message } from '../core/types';
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
import * as https from 'https';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Feishu / Lark Channel — v1.1.0
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - Event Subscription (webhook) mode for receiving messages
|
|
11
|
+
* - Bot send via Feishu Open API
|
|
12
|
+
* - URL verification challenge
|
|
13
|
+
* - Message card (interactive) responses
|
|
14
|
+
* - Group chat & P2P messaging
|
|
15
|
+
* - Event deduplication
|
|
16
|
+
* - No external dependencies (uses Node.js built-in http/https)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface FeishuChannelConfig {
|
|
20
|
+
/** Feishu App ID */
|
|
21
|
+
appId?: string;
|
|
22
|
+
/** Feishu App Secret */
|
|
23
|
+
appSecret?: string;
|
|
24
|
+
/** Verification token for event subscription */
|
|
25
|
+
verificationToken?: string;
|
|
26
|
+
/** Encrypt key (optional, for encrypted events) */
|
|
27
|
+
encryptKey?: string;
|
|
28
|
+
/** Webhook server port (default: 8081) */
|
|
29
|
+
port?: number;
|
|
30
|
+
/** API base URL (use 'https://open.larksuite.com' for Lark international) */
|
|
31
|
+
apiBase?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FeishuTokenCache {
|
|
35
|
+
token: string;
|
|
36
|
+
expiresAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class FeishuChannel extends BaseChannel {
|
|
40
|
+
readonly type = 'feishu';
|
|
41
|
+
private config: Required<Pick<FeishuChannelConfig, 'port' | 'apiBase'>> & FeishuChannelConfig;
|
|
42
|
+
private server: http.Server | null = null;
|
|
43
|
+
private tokenCache: FeishuTokenCache | null = null;
|
|
44
|
+
private processedEvents = new Set<string>();
|
|
45
|
+
|
|
46
|
+
constructor(config: FeishuChannelConfig = {}) {
|
|
47
|
+
super();
|
|
48
|
+
this.config = {
|
|
49
|
+
appId: config.appId ?? process.env.FEISHU_APP_ID ?? '',
|
|
50
|
+
appSecret: config.appSecret ?? process.env.FEISHU_APP_SECRET ?? '',
|
|
51
|
+
verificationToken: config.verificationToken ?? process.env.FEISHU_VERIFICATION_TOKEN ?? '',
|
|
52
|
+
encryptKey: config.encryptKey ?? process.env.FEISHU_ENCRYPT_KEY,
|
|
53
|
+
port: config.port ?? 8081,
|
|
54
|
+
apiBase: config.apiBase ?? 'https://open.feishu.cn',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async start(): Promise<void> {
|
|
59
|
+
if (!this.config.appId || !this.config.appSecret) {
|
|
60
|
+
console.warn('[FeishuChannel] Missing appId/appSecret. Set FEISHU_APP_ID and FEISHU_APP_SECRET.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.server = http.createServer(async (req, res) => {
|
|
65
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
66
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
67
|
+
res.end(JSON.stringify({ status: 'ok', channel: 'feishu' }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (req.method !== 'POST') {
|
|
72
|
+
res.writeHead(404);
|
|
73
|
+
res.end();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const body = await this.readBody(req);
|
|
79
|
+
const parsed = JSON.parse(body);
|
|
80
|
+
await this.handleEvent(parsed, res);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('[FeishuChannel] Error:', err);
|
|
83
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
84
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
this.server!.listen(this.config.port, () => {
|
|
90
|
+
console.log(`[FeishuChannel] Listening on port ${this.config.port}`);
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async stop(): Promise<void> {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
if (!this.server) return resolve();
|
|
99
|
+
this.server.close((err) => (err ? reject(err) : resolve()));
|
|
100
|
+
this.server = null;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Handle Feishu event */
|
|
105
|
+
private async handleEvent(body: any, res: http.ServerResponse): Promise<void> {
|
|
106
|
+
// URL verification challenge
|
|
107
|
+
if (body.type === 'url_verification') {
|
|
108
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
109
|
+
res.end(JSON.stringify({ challenge: body.challenge }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Deduplicate events
|
|
114
|
+
const eventId = body.header?.event_id;
|
|
115
|
+
if (eventId && this.processedEvents.has(eventId)) {
|
|
116
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
117
|
+
res.end(JSON.stringify({ ok: true }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (eventId) {
|
|
121
|
+
this.processedEvents.add(eventId);
|
|
122
|
+
if (this.processedEvents.size > 1000) {
|
|
123
|
+
const arr = [...this.processedEvents];
|
|
124
|
+
this.processedEvents = new Set(arr.slice(-500));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Verify token
|
|
129
|
+
if (this.config.verificationToken && body.header?.token !== this.config.verificationToken) {
|
|
130
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
131
|
+
res.end(JSON.stringify({ error: 'Invalid verification token' }));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle im.message.receive_v1
|
|
136
|
+
const event = body.event;
|
|
137
|
+
if (body.header?.event_type === 'im.message.receive_v1' && this.handler) {
|
|
138
|
+
const msgBody = event?.message;
|
|
139
|
+
if (!msgBody) {
|
|
140
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
141
|
+
res.end(JSON.stringify({ ok: true }));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const msgType = msgBody.message_type;
|
|
146
|
+
let content = '';
|
|
147
|
+
if (msgType === 'text') {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(msgBody.content);
|
|
150
|
+
content = parsed.text ?? '';
|
|
151
|
+
} catch {
|
|
152
|
+
content = msgBody.content ?? '';
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify({ ok: true }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Strip @bot mentions
|
|
161
|
+
content = content.replace(/@_user_\d+/g, '').trim();
|
|
162
|
+
if (!content) {
|
|
163
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
164
|
+
res.end(JSON.stringify({ ok: true }));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const chatId = msgBody.chat_id;
|
|
169
|
+
const senderId = event.sender?.sender_id?.open_id ?? 'unknown';
|
|
170
|
+
|
|
171
|
+
const msg: Message = {
|
|
172
|
+
id: `feishu_${msgBody.message_id}`,
|
|
173
|
+
role: 'user',
|
|
174
|
+
content,
|
|
175
|
+
timestamp: parseInt(msgBody.create_time, 10) || Date.now(),
|
|
176
|
+
metadata: {
|
|
177
|
+
sessionId: `feishu_${chatId}`,
|
|
178
|
+
chatId,
|
|
179
|
+
userId: senderId,
|
|
180
|
+
platform: 'feishu',
|
|
181
|
+
messageId: msgBody.message_id,
|
|
182
|
+
chatType: msgBody.chat_type,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Don't block the response
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ ok: true }));
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const response = await this.handler(msg);
|
|
192
|
+
await this.sendTextMessage(chatId, response.content);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('[FeishuChannel] Handler error:', err);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
200
|
+
res.end(JSON.stringify({ ok: true }));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Parse Feishu event body (exported for testing) */
|
|
204
|
+
static parseEventBody(body: any): { type: string; challenge?: string; eventType?: string; content?: string; chatId?: string; senderId?: string; messageId?: string } {
|
|
205
|
+
if (body.type === 'url_verification') {
|
|
206
|
+
return { type: 'url_verification', challenge: body.challenge };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const eventType = body.header?.event_type;
|
|
210
|
+
const event = body.event;
|
|
211
|
+
|
|
212
|
+
if (eventType === 'im.message.receive_v1' && event?.message) {
|
|
213
|
+
const msgBody = event.message;
|
|
214
|
+
let content = '';
|
|
215
|
+
if (msgBody.message_type === 'text') {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(msgBody.content);
|
|
218
|
+
content = parsed.text ?? '';
|
|
219
|
+
} catch {
|
|
220
|
+
content = msgBody.content ?? '';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
type: 'message',
|
|
226
|
+
eventType,
|
|
227
|
+
content: content.replace(/@_user_\d+/g, '').trim(),
|
|
228
|
+
chatId: msgBody.chat_id,
|
|
229
|
+
senderId: event.sender?.sender_id?.open_id,
|
|
230
|
+
messageId: msgBody.message_id,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { type: 'unknown', eventType };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Get tenant access token (cached) */
|
|
238
|
+
private async getAccessToken(): Promise<string> {
|
|
239
|
+
if (this.tokenCache && Date.now() < this.tokenCache.expiresAt) {
|
|
240
|
+
return this.tokenCache.token;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const body = JSON.stringify({
|
|
244
|
+
app_id: this.config.appId,
|
|
245
|
+
app_secret: this.config.appSecret,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const result = await this.httpsPost(
|
|
249
|
+
`${this.config.apiBase}/open-apis/auth/v3/tenant_access_token/internal`,
|
|
250
|
+
body
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const data = JSON.parse(result);
|
|
254
|
+
if (data.code !== 0) {
|
|
255
|
+
throw new Error(`[FeishuChannel] Failed to get access token: ${result}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.tokenCache = {
|
|
259
|
+
token: data.tenant_access_token,
|
|
260
|
+
expiresAt: Date.now() + (data.expire - 60) * 1000,
|
|
261
|
+
};
|
|
262
|
+
return this.tokenCache.token;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Send a text message to a chat */
|
|
266
|
+
async sendTextMessage(chatId: string, text: string): Promise<void> {
|
|
267
|
+
const token = await this.getAccessToken();
|
|
268
|
+
const body = JSON.stringify({
|
|
269
|
+
receive_id: chatId,
|
|
270
|
+
msg_type: 'text',
|
|
271
|
+
content: JSON.stringify({ text }),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const url = `${this.config.apiBase}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
|
275
|
+
await this.httpsPostWithAuth(url, body, token);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Send an interactive card message */
|
|
279
|
+
async sendCardMessage(chatId: string, card: Record<string, unknown>): Promise<void> {
|
|
280
|
+
const token = await this.getAccessToken();
|
|
281
|
+
const body = JSON.stringify({
|
|
282
|
+
receive_id: chatId,
|
|
283
|
+
msg_type: 'interactive',
|
|
284
|
+
content: JSON.stringify(card),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const url = `${this.config.apiBase}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
|
288
|
+
await this.httpsPostWithAuth(url, body, token);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Read request body */
|
|
292
|
+
private readBody(req: http.IncomingMessage): Promise<string> {
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const chunks: Buffer[] = [];
|
|
295
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
296
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
297
|
+
req.on('error', reject);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** HTTPS POST */
|
|
302
|
+
private httpsPost(url: string, body: string): Promise<string> {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
const parsed = new URL(url);
|
|
305
|
+
const req = https.request({
|
|
306
|
+
hostname: parsed.hostname,
|
|
307
|
+
path: parsed.pathname + parsed.search,
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: {
|
|
310
|
+
'Content-Type': 'application/json',
|
|
311
|
+
'Content-Length': Buffer.byteLength(body),
|
|
312
|
+
},
|
|
313
|
+
}, (res) => {
|
|
314
|
+
const chunks: Buffer[] = [];
|
|
315
|
+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
316
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
317
|
+
res.on('error', reject);
|
|
318
|
+
});
|
|
319
|
+
req.on('error', reject);
|
|
320
|
+
req.write(body);
|
|
321
|
+
req.end();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** HTTPS POST with Bearer auth */
|
|
326
|
+
private httpsPostWithAuth(url: string, body: string, token: string): Promise<string> {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const parsed = new URL(url);
|
|
329
|
+
const req = https.request({
|
|
330
|
+
hostname: parsed.hostname,
|
|
331
|
+
path: parsed.pathname + parsed.search,
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: {
|
|
334
|
+
'Content-Type': 'application/json',
|
|
335
|
+
'Content-Length': Buffer.byteLength(body),
|
|
336
|
+
'Authorization': `Bearer ${token}`,
|
|
337
|
+
},
|
|
338
|
+
}, (res) => {
|
|
339
|
+
const chunks: Buffer[] = [];
|
|
340
|
+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
341
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
342
|
+
res.on('error', reject);
|
|
343
|
+
});
|
|
344
|
+
req.on('error', reject);
|
|
345
|
+
req.write(body);
|
|
346
|
+
req.end();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|