@cjhyy/code-shell-chat 0.8.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.md +416 -0
- package/dist/adapter-factory.d.ts +30 -0
- package/dist/adapter-factory.js +198 -0
- package/dist/attachments.d.ts +13 -0
- package/dist/attachments.js +74 -0
- package/dist/channel.d.ts +112 -0
- package/dist/channel.js +60 -0
- package/dist/chat-gateway.d.ts +58 -0
- package/dist/chat-gateway.js +232 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +259 -0
- package/dist/codeshell.d.ts +6 -0
- package/dist/codeshell.js +6 -0
- package/dist/config.d.ts +125 -0
- package/dist/config.js +469 -0
- package/dist/delivery-queue.d.ts +93 -0
- package/dist/delivery-queue.js +401 -0
- package/dist/desktop-control-client.d.ts +51 -0
- package/dist/desktop-control-client.js +309 -0
- package/dist/dingtalk.d.ts +19 -0
- package/dist/dingtalk.js +93 -0
- package/dist/discord.d.ts +23 -0
- package/dist/discord.js +134 -0
- package/dist/gateway.d.ts +34 -0
- package/dist/gateway.js +287 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/ingress.d.ts +8 -0
- package/dist/ingress.js +61 -0
- package/dist/instance-lock.d.ts +24 -0
- package/dist/instance-lock.js +100 -0
- package/dist/lark.d.ts +20 -0
- package/dist/lark.js +184 -0
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +16 -0
- package/dist/line.d.ts +17 -0
- package/dist/line.js +123 -0
- package/dist/matrix.d.ts +21 -0
- package/dist/matrix.js +208 -0
- package/dist/mattermost.d.ts +18 -0
- package/dist/mattermost.js +170 -0
- package/dist/media.d.ts +39 -0
- package/dist/media.js +223 -0
- package/dist/notification-progress.d.ts +46 -0
- package/dist/notification-progress.js +299 -0
- package/dist/notification-relay.d.ts +43 -0
- package/dist/notification-relay.js +285 -0
- package/dist/platform-canary.d.ts +22 -0
- package/dist/platform-canary.js +55 -0
- package/dist/protocol.d.ts +102 -0
- package/dist/protocol.js +1 -0
- package/dist/service-manager.d.ts +47 -0
- package/dist/service-manager.js +222 -0
- package/dist/slack.d.ts +17 -0
- package/dist/slack.js +136 -0
- package/dist/teams.d.ts +26 -0
- package/dist/teams.js +193 -0
- package/dist/telegram.d.ts +33 -0
- package/dist/telegram.js +263 -0
- package/dist/webhook.d.ts +26 -0
- package/dist/webhook.js +91 -0
- package/dist/wechat-login.d.ts +21 -0
- package/dist/wechat-login.js +97 -0
- package/dist/wechat-storage.d.ts +61 -0
- package/dist/wechat-storage.js +251 -0
- package/dist/wechat.d.ts +129 -0
- package/dist/wechat.js +1082 -0
- package/dist/wecom.d.ts +16 -0
- package/dist/wecom.js +149 -0
- package/dist/whatsapp.d.ts +27 -0
- package/dist/whatsapp.js +218 -0
- package/package.json +123 -0
package/dist/wecom.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChannelAdapter, ChannelMessageHandler, OutgoingMessage } from "./channel.js";
|
|
2
|
+
export interface WeComAdapterConfig {
|
|
3
|
+
botId: string;
|
|
4
|
+
secret: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class WeComAdapter implements ChannelAdapter {
|
|
7
|
+
readonly channel = "wecom";
|
|
8
|
+
readonly capabilities: import("./channel.js").ChannelCapabilities;
|
|
9
|
+
private readonly client;
|
|
10
|
+
private readonly delivery;
|
|
11
|
+
constructor(config: WeComAdapterConfig);
|
|
12
|
+
run(handler: ChannelMessageHandler, signal: AbortSignal): Promise<void>;
|
|
13
|
+
send(target: string, message: OutgoingMessage): Promise<void>;
|
|
14
|
+
private dispatchMedia;
|
|
15
|
+
private downloadable;
|
|
16
|
+
}
|
package/dist/wecom.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { WSClient, } from "@wecom/aibot-node-sdk";
|
|
2
|
+
import { BUILTIN_CHANNEL_CAPABILITIES } from "./channel.js";
|
|
3
|
+
import { dispatchSafely, formatOutgoingMarkdown, waitForAbort } from "./lifecycle.js";
|
|
4
|
+
import { OutgoingDeliveryTracker, outgoingAttachments, safeAttachmentName } from "./media.js";
|
|
5
|
+
export class WeComAdapter {
|
|
6
|
+
channel = "wecom";
|
|
7
|
+
capabilities = BUILTIN_CHANNEL_CAPABILITIES.wecom;
|
|
8
|
+
client;
|
|
9
|
+
delivery = new OutgoingDeliveryTracker();
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.client = new WSClient({
|
|
12
|
+
botId: config.botId,
|
|
13
|
+
secret: config.secret,
|
|
14
|
+
maxReconnectAttempts: -1,
|
|
15
|
+
maxAuthFailureAttempts: 5,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async run(handler, signal) {
|
|
19
|
+
const onMessage = (frame) => {
|
|
20
|
+
const message = frame.body;
|
|
21
|
+
if (!message?.text?.content)
|
|
22
|
+
return;
|
|
23
|
+
void dispatchSafely(handler, {
|
|
24
|
+
channel: this.channel,
|
|
25
|
+
target: message.chatid ?? message.from.userid,
|
|
26
|
+
senderId: message.from.userid,
|
|
27
|
+
text: message.text.content,
|
|
28
|
+
...(frame.headers?.req_id
|
|
29
|
+
? { messageId: frame.headers.req_id }
|
|
30
|
+
: {}),
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const onImage = (frame) => this.dispatchMedia(frame, handler);
|
|
34
|
+
const onMixed = (frame) => this.dispatchMedia(frame, handler);
|
|
35
|
+
const onVoice = (frame) => this.dispatchMedia(frame, handler);
|
|
36
|
+
const onFile = (frame) => this.dispatchMedia(frame, handler);
|
|
37
|
+
const onVideo = (frame) => this.dispatchMedia(frame, handler);
|
|
38
|
+
this.client.on("message.text", onMessage);
|
|
39
|
+
this.client.on("message.image", onImage);
|
|
40
|
+
this.client.on("message.mixed", onMixed);
|
|
41
|
+
this.client.on("message.voice", onVoice);
|
|
42
|
+
this.client.on("message.file", onFile);
|
|
43
|
+
this.client.on("message.video", onVideo);
|
|
44
|
+
this.client.connect();
|
|
45
|
+
try {
|
|
46
|
+
await waitForAbort(signal);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
this.client.off("message.text", onMessage);
|
|
50
|
+
this.client.off("message.image", onImage);
|
|
51
|
+
this.client.off("message.mixed", onMixed);
|
|
52
|
+
this.client.off("message.voice", onVoice);
|
|
53
|
+
this.client.off("message.file", onFile);
|
|
54
|
+
this.client.off("message.video", onVideo);
|
|
55
|
+
this.client.disconnect();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async send(target, message) {
|
|
59
|
+
const attachments = outgoingAttachments(message, this.capabilities.outbound.attachments);
|
|
60
|
+
await this.delivery.run(message, () => [
|
|
61
|
+
...(message.text || message.button
|
|
62
|
+
? [
|
|
63
|
+
() => this.client
|
|
64
|
+
.sendMessage(target, {
|
|
65
|
+
msgtype: "markdown",
|
|
66
|
+
markdown: { content: formatOutgoingMarkdown(message.text, message.button) },
|
|
67
|
+
})
|
|
68
|
+
.then(() => undefined),
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
...attachments.map((attachment) => async () => {
|
|
72
|
+
const type = attachment.kind === "image" ? "image" : attachment.kind === "video" ? "video" : "file";
|
|
73
|
+
const uploaded = await this.client.uploadMedia(Buffer.from(attachment.data), {
|
|
74
|
+
type,
|
|
75
|
+
filename: safeAttachmentName(attachment.name),
|
|
76
|
+
});
|
|
77
|
+
await this.client.sendMediaMessage(target, type, uploaded.media_id, type === "video" ? { title: attachment.name } : undefined);
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
dispatchMedia(frame, handler) {
|
|
82
|
+
const message = frame.body;
|
|
83
|
+
if (!message?.from?.userid)
|
|
84
|
+
return;
|
|
85
|
+
const attachments = [];
|
|
86
|
+
let text = "";
|
|
87
|
+
if (message.msgtype === "image") {
|
|
88
|
+
const image = message.image;
|
|
89
|
+
if (image?.url)
|
|
90
|
+
attachments.push(this.downloadable(message.msgid, "image", image.url, image.aeskey));
|
|
91
|
+
}
|
|
92
|
+
else if (message.msgtype === "file") {
|
|
93
|
+
const file = message.file;
|
|
94
|
+
if (file?.url)
|
|
95
|
+
attachments.push(this.downloadable(message.msgid, "file", file.url, file.aeskey));
|
|
96
|
+
}
|
|
97
|
+
else if (message.msgtype === "video") {
|
|
98
|
+
const video = message.video;
|
|
99
|
+
if (video?.url)
|
|
100
|
+
attachments.push(this.downloadable(message.msgid, "video", video.url, video.aeskey));
|
|
101
|
+
}
|
|
102
|
+
else if (message.msgtype === "voice") {
|
|
103
|
+
// The current WeCom AI Bot protocol exposes voice transcription but no
|
|
104
|
+
// downloadable voice URL, so preserve the useful text without claiming
|
|
105
|
+
// an inbound audio attachment.
|
|
106
|
+
text = message.voice?.content ?? "";
|
|
107
|
+
}
|
|
108
|
+
else if (message.msgtype === "mixed") {
|
|
109
|
+
for (const [index, item] of message.mixed?.msg_item.entries() ?? []) {
|
|
110
|
+
if (item.msgtype === "text")
|
|
111
|
+
text += item.text?.content ?? "";
|
|
112
|
+
if (item.msgtype === "image" && item.image?.url) {
|
|
113
|
+
attachments.push(this.downloadable(`${message.msgid}:${index}`, "image", item.image.url, item.image.aeskey));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!text && attachments.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
void dispatchSafely(handler, {
|
|
120
|
+
channel: this.channel,
|
|
121
|
+
target: message.chatid ?? message.from.userid,
|
|
122
|
+
senderId: message.from.userid,
|
|
123
|
+
text,
|
|
124
|
+
messageId: message.msgid,
|
|
125
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
downloadable(id, kind, url, aesKey) {
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
kind,
|
|
132
|
+
name: `wecom-${id}${kind === "image" ? ".jpg" : kind === "video" ? ".mp4" : ""}`,
|
|
133
|
+
...(kind === "image"
|
|
134
|
+
? { mimeType: "image/jpeg" }
|
|
135
|
+
: kind === "video"
|
|
136
|
+
? { mimeType: "video/mp4" }
|
|
137
|
+
: {}),
|
|
138
|
+
load: async (signal) => {
|
|
139
|
+
if (signal?.aborted)
|
|
140
|
+
throw signal.reason;
|
|
141
|
+
const downloaded = await this.client.downloadFile(url, aesKey);
|
|
142
|
+
if (downloaded.buffer.byteLength > 10 * 1024 * 1024) {
|
|
143
|
+
throw new Error("企业微信附件超过大小限制");
|
|
144
|
+
}
|
|
145
|
+
return downloaded.buffer;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { ChannelMessageHandler, OutgoingMessage, WebhookChannelAdapter } from "./channel.js";
|
|
3
|
+
export interface WhatsAppAdapterConfig {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
appSecret: string;
|
|
6
|
+
verifyToken: string;
|
|
7
|
+
phoneNumberId: string;
|
|
8
|
+
apiVersion?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class WhatsAppAdapter implements WebhookChannelAdapter {
|
|
11
|
+
private readonly fetchFn;
|
|
12
|
+
readonly channel = "whatsapp";
|
|
13
|
+
readonly capabilities: import("./channel.js").ChannelCapabilities;
|
|
14
|
+
readonly webhookPath = "/webhooks/whatsapp";
|
|
15
|
+
private readonly config;
|
|
16
|
+
private readonly delivery;
|
|
17
|
+
constructor(config: WhatsAppAdapterConfig, fetchFn?: typeof fetch);
|
|
18
|
+
run(_handler: ChannelMessageHandler, signal: AbortSignal): Promise<void>;
|
|
19
|
+
handleWebhook(request: IncomingMessage, response: ServerResponse, handler: ChannelMessageHandler, maxBodyBytes: number): Promise<void>;
|
|
20
|
+
send(target: string, message: OutgoingMessage): Promise<void>;
|
|
21
|
+
private textPayload;
|
|
22
|
+
private postPayload;
|
|
23
|
+
private uploadMedia;
|
|
24
|
+
private inboundMedia;
|
|
25
|
+
private downloadMedia;
|
|
26
|
+
private handleVerification;
|
|
27
|
+
}
|
package/dist/whatsapp.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { BUILTIN_CHANNEL_CAPABILITIES } from "./channel.js";
|
|
3
|
+
import { waitForAbort } from "./lifecycle.js";
|
|
4
|
+
import { downloadRemoteAttachment, OutgoingDeliveryTracker, outgoingAttachments, safeAttachmentName, } from "./media.js";
|
|
5
|
+
import { readRequestBody, sendResponse } from "./webhook.js";
|
|
6
|
+
export class WhatsAppAdapter {
|
|
7
|
+
fetchFn;
|
|
8
|
+
channel = "whatsapp";
|
|
9
|
+
capabilities = BUILTIN_CHANNEL_CAPABILITIES.whatsapp;
|
|
10
|
+
webhookPath = "/webhooks/whatsapp";
|
|
11
|
+
config;
|
|
12
|
+
delivery = new OutgoingDeliveryTracker();
|
|
13
|
+
constructor(config, fetchFn = fetch) {
|
|
14
|
+
this.fetchFn = fetchFn;
|
|
15
|
+
this.config = { ...config, apiVersion: config.apiVersion ?? "v25.0" };
|
|
16
|
+
}
|
|
17
|
+
run(_handler, signal) {
|
|
18
|
+
return waitForAbort(signal);
|
|
19
|
+
}
|
|
20
|
+
async handleWebhook(request, response, handler, maxBodyBytes) {
|
|
21
|
+
if (request.method === "GET") {
|
|
22
|
+
this.handleVerification(request, response);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (request.method !== "POST") {
|
|
26
|
+
sendResponse(response, 405, "Method not allowed");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const raw = await readRequestBody(request, maxBodyBytes);
|
|
30
|
+
if (!verifyMetaSignature(raw, request.headers["x-hub-signature-256"], this.config.appSecret)) {
|
|
31
|
+
sendResponse(response, 401, "Invalid signature");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let body;
|
|
35
|
+
try {
|
|
36
|
+
body = JSON.parse(raw.toString("utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
sendResponse(response, 400, "Invalid JSON");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const messages = (body.entry ?? []).flatMap((entry) => (entry.changes ?? []).flatMap((change) => (change.value?.messages ?? []).flatMap((message) => {
|
|
43
|
+
if (!message.from)
|
|
44
|
+
return [];
|
|
45
|
+
const media = this.inboundMedia(message);
|
|
46
|
+
const text = message.text?.body ?? media?.caption ?? "";
|
|
47
|
+
if (!text && !media?.attachment)
|
|
48
|
+
return [];
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
channel: this.channel,
|
|
52
|
+
target: message.from,
|
|
53
|
+
senderId: message.from,
|
|
54
|
+
text,
|
|
55
|
+
...(message.id ? { messageId: message.id } : {}),
|
|
56
|
+
...(media?.attachment ? { attachments: [media.attachment] } : {}),
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
})));
|
|
60
|
+
await Promise.all(messages.map((message) => handler(message)));
|
|
61
|
+
sendResponse(response, 200, "OK");
|
|
62
|
+
}
|
|
63
|
+
async send(target, message) {
|
|
64
|
+
const attachments = outgoingAttachments(message, this.capabilities.outbound.attachments);
|
|
65
|
+
if (attachments.length > 0) {
|
|
66
|
+
await this.delivery.run(message, async () => {
|
|
67
|
+
const uploaded = await Promise.all(attachments.map(async (attachment) => ({
|
|
68
|
+
attachment,
|
|
69
|
+
mediaId: await this.uploadMedia(attachment),
|
|
70
|
+
})));
|
|
71
|
+
let textAssigned = false;
|
|
72
|
+
const steps = [];
|
|
73
|
+
if (message.button || (message.text && uploaded[0]?.attachment.kind === "audio")) {
|
|
74
|
+
steps.push(() => this.postPayload(this.textPayload(target, message)));
|
|
75
|
+
textAssigned = true;
|
|
76
|
+
}
|
|
77
|
+
for (const [index, item] of uploaded.entries()) {
|
|
78
|
+
const kind = item.attachment.kind === "file" ? "document" : item.attachment.kind;
|
|
79
|
+
const media = { id: item.mediaId };
|
|
80
|
+
if (!textAssigned && index === 0 && message.text && kind !== "audio") {
|
|
81
|
+
media.caption = message.text;
|
|
82
|
+
textAssigned = true;
|
|
83
|
+
}
|
|
84
|
+
if (kind === "document")
|
|
85
|
+
media.filename = item.attachment.name;
|
|
86
|
+
const payload = {
|
|
87
|
+
messaging_product: "whatsapp",
|
|
88
|
+
to: target,
|
|
89
|
+
type: kind,
|
|
90
|
+
[kind]: media,
|
|
91
|
+
};
|
|
92
|
+
steps.push(() => this.postPayload(payload));
|
|
93
|
+
}
|
|
94
|
+
if (!textAssigned && message.text) {
|
|
95
|
+
steps.push(() => this.postPayload(this.textPayload(target, message)));
|
|
96
|
+
}
|
|
97
|
+
return steps;
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await this.postPayload(this.textPayload(target, message));
|
|
102
|
+
}
|
|
103
|
+
textPayload(target, message) {
|
|
104
|
+
const payload = message.button
|
|
105
|
+
? {
|
|
106
|
+
messaging_product: "whatsapp",
|
|
107
|
+
to: target,
|
|
108
|
+
type: "interactive",
|
|
109
|
+
interactive: {
|
|
110
|
+
type: "cta_url",
|
|
111
|
+
body: { text: message.text },
|
|
112
|
+
action: {
|
|
113
|
+
name: "cta_url",
|
|
114
|
+
parameters: { display_text: message.button.text, url: message.button.url },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {
|
|
119
|
+
messaging_product: "whatsapp",
|
|
120
|
+
to: target,
|
|
121
|
+
type: "text",
|
|
122
|
+
text: { body: message.text },
|
|
123
|
+
};
|
|
124
|
+
return payload;
|
|
125
|
+
}
|
|
126
|
+
async postPayload(payload) {
|
|
127
|
+
const response = await this.fetchFn(`https://graph.facebook.com/${this.config.apiVersion}/${this.config.phoneNumberId}/messages`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
authorization: `Bearer ${this.config.accessToken}`,
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify(payload),
|
|
134
|
+
signal: AbortSignal.timeout(15_000),
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok)
|
|
137
|
+
throw new Error(`WhatsApp 发送失败(HTTP ${response.status})`);
|
|
138
|
+
}
|
|
139
|
+
async uploadMedia(attachment) {
|
|
140
|
+
const form = new FormData();
|
|
141
|
+
form.set("messaging_product", "whatsapp");
|
|
142
|
+
form.set("file", new Blob([new Uint8Array(attachment.data)], { type: attachment.mimeType }), safeAttachmentName(attachment.name));
|
|
143
|
+
const response = await this.fetchFn(`https://graph.facebook.com/${this.config.apiVersion}/${this.config.phoneNumberId}/media`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { authorization: `Bearer ${this.config.accessToken}` },
|
|
146
|
+
body: form,
|
|
147
|
+
signal: AbortSignal.timeout(30_000),
|
|
148
|
+
});
|
|
149
|
+
const body = (await response.json().catch(() => ({})));
|
|
150
|
+
if (!response.ok || !body.id) {
|
|
151
|
+
throw new Error(body.error?.message ?? `WhatsApp 媒体上传失败(HTTP ${response.status})`);
|
|
152
|
+
}
|
|
153
|
+
return body.id;
|
|
154
|
+
}
|
|
155
|
+
inboundMedia(message) {
|
|
156
|
+
const kindByType = {
|
|
157
|
+
image: "image",
|
|
158
|
+
audio: "audio",
|
|
159
|
+
video: "video",
|
|
160
|
+
document: "file",
|
|
161
|
+
};
|
|
162
|
+
const kind = message.type ? kindByType[message.type] : undefined;
|
|
163
|
+
if (!kind)
|
|
164
|
+
return undefined;
|
|
165
|
+
const media = message[message.type];
|
|
166
|
+
if (!media?.id)
|
|
167
|
+
return undefined;
|
|
168
|
+
const mediaId = media.id;
|
|
169
|
+
const name = safeAttachmentName("filename" in media ? media.filename : undefined, `whatsapp-${mediaId}${kind === "image" ? ".jpg" : kind === "video" ? ".mp4" : ""}`);
|
|
170
|
+
return {
|
|
171
|
+
caption: media.caption,
|
|
172
|
+
attachment: {
|
|
173
|
+
id: mediaId,
|
|
174
|
+
kind,
|
|
175
|
+
name,
|
|
176
|
+
...(media.mime_type ? { mimeType: media.mime_type } : {}),
|
|
177
|
+
load: (signal) => this.downloadMedia(mediaId, signal),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async downloadMedia(mediaId, signal) {
|
|
182
|
+
const metadataResponse = await this.fetchFn(`https://graph.facebook.com/${this.config.apiVersion}/${encodeURIComponent(mediaId)}`, {
|
|
183
|
+
headers: { authorization: `Bearer ${this.config.accessToken}` },
|
|
184
|
+
signal: signal
|
|
185
|
+
? AbortSignal.any([signal, AbortSignal.timeout(15_000)])
|
|
186
|
+
: AbortSignal.timeout(15_000),
|
|
187
|
+
});
|
|
188
|
+
const metadata = (await metadataResponse.json().catch(() => ({})));
|
|
189
|
+
if (!metadataResponse.ok || !metadata.url)
|
|
190
|
+
throw new Error("WhatsApp 媒体下载地址不可用");
|
|
191
|
+
return downloadRemoteAttachment(this.fetchFn, metadata.url, {
|
|
192
|
+
headers: { authorization: `Bearer ${this.config.accessToken}` },
|
|
193
|
+
...(signal ? { signal } : {}),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
handleVerification(request, response) {
|
|
197
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
198
|
+
if (url.searchParams.get("hub.mode") !== "subscribe" ||
|
|
199
|
+
!safeStringEqual(url.searchParams.get("hub.verify_token"), this.config.verifyToken)) {
|
|
200
|
+
sendResponse(response, 403, "Verification failed");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
sendResponse(response, 200, url.searchParams.get("hub.challenge") ?? "");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function verifyMetaSignature(body, header, secret) {
|
|
207
|
+
if (typeof header !== "string" || !header.startsWith("sha256="))
|
|
208
|
+
return false;
|
|
209
|
+
const expected = createHmac("sha256", secret).update(body).digest("hex");
|
|
210
|
+
return safeStringEqual(header.slice(7), expected);
|
|
211
|
+
}
|
|
212
|
+
function safeStringEqual(left, right) {
|
|
213
|
+
if (left === null)
|
|
214
|
+
return false;
|
|
215
|
+
const a = Buffer.from(left);
|
|
216
|
+
const b = Buffer.from(right);
|
|
217
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
218
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cjhyy/code-shell-chat",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "A standalone, transport-agnostic multi-channel chat gateway for Node.js and Bun.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./codeshell": {
|
|
14
|
+
"types": "./dist/codeshell.d.ts",
|
|
15
|
+
"import": "./dist/codeshell.js"
|
|
16
|
+
},
|
|
17
|
+
"./factory": {
|
|
18
|
+
"types": "./dist/adapter-factory.d.ts",
|
|
19
|
+
"import": "./dist/adapter-factory.js"
|
|
20
|
+
},
|
|
21
|
+
"./telegram": {
|
|
22
|
+
"types": "./dist/telegram.d.ts",
|
|
23
|
+
"import": "./dist/telegram.js"
|
|
24
|
+
},
|
|
25
|
+
"./discord": {
|
|
26
|
+
"types": "./dist/discord.d.ts",
|
|
27
|
+
"import": "./dist/discord.js"
|
|
28
|
+
},
|
|
29
|
+
"./slack": {
|
|
30
|
+
"types": "./dist/slack.d.ts",
|
|
31
|
+
"import": "./dist/slack.js"
|
|
32
|
+
},
|
|
33
|
+
"./lark": {
|
|
34
|
+
"types": "./dist/lark.d.ts",
|
|
35
|
+
"import": "./dist/lark.js"
|
|
36
|
+
},
|
|
37
|
+
"./dingtalk": {
|
|
38
|
+
"types": "./dist/dingtalk.d.ts",
|
|
39
|
+
"import": "./dist/dingtalk.js"
|
|
40
|
+
},
|
|
41
|
+
"./wecom": {
|
|
42
|
+
"types": "./dist/wecom.d.ts",
|
|
43
|
+
"import": "./dist/wecom.js"
|
|
44
|
+
},
|
|
45
|
+
"./wechat": {
|
|
46
|
+
"types": "./dist/wechat.d.ts",
|
|
47
|
+
"import": "./dist/wechat.js"
|
|
48
|
+
},
|
|
49
|
+
"./matrix": {
|
|
50
|
+
"types": "./dist/matrix.d.ts",
|
|
51
|
+
"import": "./dist/matrix.js"
|
|
52
|
+
},
|
|
53
|
+
"./mattermost": {
|
|
54
|
+
"types": "./dist/mattermost.d.ts",
|
|
55
|
+
"import": "./dist/mattermost.js"
|
|
56
|
+
},
|
|
57
|
+
"./line": {
|
|
58
|
+
"types": "./dist/line.d.ts",
|
|
59
|
+
"import": "./dist/line.js"
|
|
60
|
+
},
|
|
61
|
+
"./whatsapp": {
|
|
62
|
+
"types": "./dist/whatsapp.d.ts",
|
|
63
|
+
"import": "./dist/whatsapp.js"
|
|
64
|
+
},
|
|
65
|
+
"./teams": {
|
|
66
|
+
"types": "./dist/teams.d.ts",
|
|
67
|
+
"import": "./dist/teams.js"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"bin": {
|
|
71
|
+
"code-shell-chat": "./dist/cli.js"
|
|
72
|
+
},
|
|
73
|
+
"files": [
|
|
74
|
+
"dist",
|
|
75
|
+
"README.md",
|
|
76
|
+
"LICENSE"
|
|
77
|
+
],
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "bun run clean && tsc -p tsconfig.json",
|
|
80
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
81
|
+
"test": "bun test src",
|
|
82
|
+
"dev": "bun run src/cli.ts",
|
|
83
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
84
|
+
},
|
|
85
|
+
"publishConfig": {
|
|
86
|
+
"access": "public"
|
|
87
|
+
},
|
|
88
|
+
"repository": {
|
|
89
|
+
"type": "git",
|
|
90
|
+
"url": "git+https://github.com/cjhyy/codeshell.git",
|
|
91
|
+
"directory": "packages/chat"
|
|
92
|
+
},
|
|
93
|
+
"engines": {
|
|
94
|
+
"node": ">=20.10"
|
|
95
|
+
},
|
|
96
|
+
"keywords": [
|
|
97
|
+
"chat",
|
|
98
|
+
"gateway",
|
|
99
|
+
"bot",
|
|
100
|
+
"telegram",
|
|
101
|
+
"discord",
|
|
102
|
+
"slack",
|
|
103
|
+
"lark",
|
|
104
|
+
"wechat"
|
|
105
|
+
],
|
|
106
|
+
"license": "MIT",
|
|
107
|
+
"dependencies": {
|
|
108
|
+
"@larksuiteoapi/node-sdk": "1.71.0",
|
|
109
|
+
"@slack/socket-mode": "2.0.7",
|
|
110
|
+
"@slack/web-api": "7.19.0",
|
|
111
|
+
"@wecom/aibot-node-sdk": "1.0.7",
|
|
112
|
+
"botbuilder": "4.23.3",
|
|
113
|
+
"dingtalk-stream": "2.1.6-beta.1",
|
|
114
|
+
"discord.js": "14.26.5",
|
|
115
|
+
"qrcode": "1.5.4",
|
|
116
|
+
"qrcode-terminal": "0.12.0",
|
|
117
|
+
"ws": "8.21.0"
|
|
118
|
+
},
|
|
119
|
+
"devDependencies": {
|
|
120
|
+
"@types/qrcode": "1.5.6",
|
|
121
|
+
"@types/ws": "8.18.1"
|
|
122
|
+
}
|
|
123
|
+
}
|