@xmanrui/dsh-im 0.1.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 +143 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/bin/dsh-im.mjs +99 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +6211 -0
- package/lib/index.js +13339 -0
- package/package.json +73 -0
- package/plugin-src/client/build.mjs +38 -0
- package/plugin-src/client/channel-logos.js +107 -0
- package/plugin-src/client/channels/dingtalk/api.js +203 -0
- package/plugin-src/client/channels/dingtalk/index.js +709 -0
- package/plugin-src/client/channels/dingtalk/styles.js +140 -0
- package/plugin-src/client/channels/discord/api.js +11 -0
- package/plugin-src/client/channels/discord/index.js +25 -0
- package/plugin-src/client/channels/discord/styles.js +19 -0
- package/plugin-src/client/channels/feishu/api.js +325 -0
- package/plugin-src/client/channels/feishu/index.js +986 -0
- package/plugin-src/client/channels/feishu/styles.js +505 -0
- package/plugin-src/client/channels/qq/api.js +119 -0
- package/plugin-src/client/channels/qq/index.js +402 -0
- package/plugin-src/client/channels/qq/styles.js +19 -0
- package/plugin-src/client/channels/shared/token-api.js +88 -0
- package/plugin-src/client/channels/shared/token-channel.js +277 -0
- package/plugin-src/client/channels/telegram/api.js +11 -0
- package/plugin-src/client/channels/telegram/index.js +25 -0
- package/plugin-src/client/channels/telegram/styles.js +19 -0
- package/plugin-src/client/channels/wecom/api.js +119 -0
- package/plugin-src/client/channels/wecom/index.js +402 -0
- package/plugin-src/client/channels/wecom/styles.js +19 -0
- package/plugin-src/client/channels/weixin/api.js +156 -0
- package/plugin-src/client/channels/weixin/index.js +577 -0
- package/plugin-src/client/channels/weixin/styles.js +120 -0
- package/plugin-src/client/credential-binding.js +117 -0
- package/plugin-src/client/index.js +192 -0
- package/plugin-src/client/lifecycle.js +86 -0
- package/plugin-src/client/styles.js +192 -0
- package/plugin-src/host/build.mjs +24 -0
- package/plugin-src/host/channels/dingtalk/connection-supervisor.mjs +130 -0
- package/plugin-src/host/channels/dingtalk/index.mjs +39 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +122 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +237 -0
- package/plugin-src/host/channels/discord/index.mjs +30 -0
- package/plugin-src/host/channels/discord/production.mjs +17 -0
- package/plugin-src/host/channels/discord/rpc.mjs +21 -0
- package/plugin-src/host/channels/feishu/connection-supervisor.mjs +167 -0
- package/plugin-src/host/channels/feishu/controller.mjs +172 -0
- package/plugin-src/host/channels/feishu/credential-store.mjs +84 -0
- package/plugin-src/host/channels/feishu/index.mjs +73 -0
- package/plugin-src/host/channels/feishu/production.mjs +138 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +454 -0
- package/plugin-src/host/channels/qq/connection-supervisor.mjs +124 -0
- package/plugin-src/host/channels/qq/index.mjs +30 -0
- package/plugin-src/host/channels/qq/production.mjs +109 -0
- package/plugin-src/host/channels/qq/rpc.mjs +146 -0
- package/plugin-src/host/channels/shared/connection-supervisor.mjs +133 -0
- package/plugin-src/host/channels/shared/production.mjs +104 -0
- package/plugin-src/host/channels/shared/rpc.mjs +119 -0
- package/plugin-src/host/channels/telegram/index.mjs +30 -0
- package/plugin-src/host/channels/telegram/production.mjs +17 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +21 -0
- package/plugin-src/host/channels/wecom/connection-supervisor.mjs +124 -0
- package/plugin-src/host/channels/wecom/index.mjs +36 -0
- package/plugin-src/host/channels/wecom/production.mjs +113 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +151 -0
- package/plugin-src/host/channels/weixin/connection-supervisor.mjs +127 -0
- package/plugin-src/host/channels/weixin/index.mjs +39 -0
- package/plugin-src/host/channels/weixin/production.mjs +119 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +178 -0
- package/plugin-src/host/index.mjs +44 -0
- package/plugin-src/host/rpc-authority.mjs +11 -0
- package/scripts/verify-package.mjs +97 -0
- package/src/channels/dingtalk/config-store.mjs +282 -0
- package/src/channels/dingtalk/device-auth.mjs +237 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +579 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +281 -0
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +233 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +752 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +358 -0
- package/src/channels/dingtalk/harness-client.mjs +299 -0
- package/src/channels/dingtalk/state-store.mjs +212 -0
- package/src/channels/discord/config-store.mjs +24 -0
- package/src/channels/discord/discord-api.mjs +153 -0
- package/src/channels/discord/discord-bridge.mjs +15 -0
- package/src/channels/discord/discord-controller.mjs +16 -0
- package/src/channels/discord/discord-runtime.mjs +460 -0
- package/src/channels/discord/harness-client.mjs +3 -0
- package/src/channels/discord/state-store.mjs +3 -0
- package/src/channels/feishu/bridge.mjs +216 -0
- package/src/channels/feishu/config.mjs +71 -0
- package/src/channels/feishu/feishu-app.mjs +51 -0
- package/src/channels/feishu/feishu-channel.mjs +153 -0
- package/src/channels/feishu/feishu-runtime.mjs +222 -0
- package/src/channels/feishu/harness-client.mjs +268 -0
- package/src/channels/feishu/message-utils.mjs +51 -0
- package/src/channels/feishu/multi-bot-controller.mjs +686 -0
- package/src/channels/feishu/plugin-config-store.mjs +204 -0
- package/src/channels/feishu/plugin-controller.mjs +248 -0
- package/src/channels/feishu/registration-manager.mjs +345 -0
- package/src/channels/feishu/state-store.mjs +71 -0
- package/src/channels/qq/config-store.mjs +171 -0
- package/src/channels/qq/harness-client.mjs +3 -0
- package/src/channels/qq/qq-bridge.mjs +177 -0
- package/src/channels/qq/qq-controller.mjs +452 -0
- package/src/channels/qq/qq-runtime.mjs +208 -0
- package/src/channels/qq/qr-auth.mjs +24 -0
- package/src/channels/qq/state-store.mjs +96 -0
- package/src/channels/shared/conversation-state-store.mjs +107 -0
- package/src/channels/shared/editable-message-stream.mjs +81 -0
- package/src/channels/shared/text-harness-bridge.mjs +180 -0
- package/src/channels/shared/token-bot-controller.mjs +298 -0
- package/src/channels/shared/token-config-store.mjs +180 -0
- package/src/channels/telegram/config-store.mjs +24 -0
- package/src/channels/telegram/harness-client.mjs +3 -0
- package/src/channels/telegram/state-store.mjs +3 -0
- package/src/channels/telegram/telegram-api.mjs +120 -0
- package/src/channels/telegram/telegram-bridge.mjs +15 -0
- package/src/channels/telegram/telegram-controller.mjs +16 -0
- package/src/channels/telegram/telegram-runtime.mjs +278 -0
- package/src/channels/wecom/config-store.mjs +170 -0
- package/src/channels/wecom/harness-client.mjs +3 -0
- package/src/channels/wecom/qr-auth.mjs +95 -0
- package/src/channels/wecom/state-store.mjs +90 -0
- package/src/channels/wecom/wecom-bridge.mjs +243 -0
- package/src/channels/wecom/wecom-controller.mjs +474 -0
- package/src/channels/wecom/wecom-runtime.mjs +209 -0
- package/src/channels/weixin/config-store.mjs +182 -0
- package/src/channels/weixin/harness-client.mjs +259 -0
- package/src/channels/weixin/state-store.mjs +112 -0
- package/src/channels/weixin/weixin-api.mjs +319 -0
- package/src/channels/weixin/weixin-bridge.mjs +173 -0
- package/src/channels/weixin/weixin-controller.mjs +545 -0
- package/src/channels/weixin/weixin-runtime.mjs +204 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { generateReqId } from '@wecom/aibot-node-sdk';
|
|
2
|
+
|
|
3
|
+
const HELP_TEXT = [
|
|
4
|
+
'企业微信机器人已连接 DeepSeek Harness。',
|
|
5
|
+
'',
|
|
6
|
+
'直接发送文字即可继续当前会话。',
|
|
7
|
+
'/new 开启一个全新会话',
|
|
8
|
+
'/status 检查连接状态',
|
|
9
|
+
'/help 显示本帮助',
|
|
10
|
+
].join('\n');
|
|
11
|
+
const MAX_REPLY_BYTES = 18_000;
|
|
12
|
+
|
|
13
|
+
function bodyOf(frame) {
|
|
14
|
+
return frame?.body && typeof frame.body === 'object' ? frame.body : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function conversationKey(frame) {
|
|
18
|
+
const body = bodyOf(frame);
|
|
19
|
+
return body.chattype === 'group' ? `group:${body.chatid}` : `direct:${body.from?.userid}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function messageText(frame) {
|
|
23
|
+
const body = bodyOf(frame);
|
|
24
|
+
if (body.msgtype === 'text') return typeof body.text?.content === 'string' ? body.text.content.trim() : '';
|
|
25
|
+
if (body.msgtype === 'voice') return typeof body.voice?.content === 'string' ? body.voice.content.trim() : '';
|
|
26
|
+
if (body.msgtype === 'mixed' && Array.isArray(body.mixed?.msg_item)) {
|
|
27
|
+
return body.mixed.msg_item
|
|
28
|
+
.filter((item) => item?.msgtype === 'text' && typeof item.text?.content === 'string')
|
|
29
|
+
.map((item) => item.text.content)
|
|
30
|
+
.join('\n')
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
|
|
37
|
+
const source = String(text ?? '').trim();
|
|
38
|
+
if (!source) return [];
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let current = '';
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
for (const character of source) {
|
|
43
|
+
const size = Buffer.byteLength(character);
|
|
44
|
+
if (current && bytes + size > maxBytes) {
|
|
45
|
+
chunks.push(current);
|
|
46
|
+
current = character;
|
|
47
|
+
bytes = size;
|
|
48
|
+
} else {
|
|
49
|
+
current += character;
|
|
50
|
+
bytes += size;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (current) chunks.push(current);
|
|
54
|
+
return chunks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function progressText(update) {
|
|
58
|
+
if (update?.type === 'text') return update.text;
|
|
59
|
+
if (update?.type === 'tool') return `正在使用${update.name}…`;
|
|
60
|
+
return update?.text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createWecomBridgeStatus() {
|
|
64
|
+
return {
|
|
65
|
+
messagesReceived: 0,
|
|
66
|
+
messagesReplied: 0,
|
|
67
|
+
messagesRejected: 0,
|
|
68
|
+
lastMessageAt: null,
|
|
69
|
+
lastReplyAt: null,
|
|
70
|
+
lastRejectedAt: null,
|
|
71
|
+
lastError: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class WecomHarnessBridge {
|
|
76
|
+
#client;
|
|
77
|
+
#harness;
|
|
78
|
+
#state;
|
|
79
|
+
#status;
|
|
80
|
+
#logger;
|
|
81
|
+
#replyTimeoutMs;
|
|
82
|
+
#generateReqId;
|
|
83
|
+
#queues = new Map();
|
|
84
|
+
|
|
85
|
+
constructor({
|
|
86
|
+
client,
|
|
87
|
+
harness,
|
|
88
|
+
state,
|
|
89
|
+
status = createWecomBridgeStatus(),
|
|
90
|
+
logger = console,
|
|
91
|
+
replyTimeoutMs = 600_000,
|
|
92
|
+
generateStreamId = generateReqId,
|
|
93
|
+
}) {
|
|
94
|
+
if (!client || typeof client.replyStream !== 'function' || typeof client.sendMessage !== 'function') {
|
|
95
|
+
throw new TypeError('Enterprise WeChat client is required');
|
|
96
|
+
}
|
|
97
|
+
if (!harness || !state) throw new TypeError('Harness client and state store are required');
|
|
98
|
+
this.#client = client;
|
|
99
|
+
this.#harness = harness;
|
|
100
|
+
this.#state = state;
|
|
101
|
+
this.#status = status;
|
|
102
|
+
this.#logger = logger;
|
|
103
|
+
this.#replyTimeoutMs = replyTimeoutMs;
|
|
104
|
+
this.#generateReqId = generateStreamId;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get status() {
|
|
108
|
+
return structuredClone(this.#status);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
accept(frame) {
|
|
112
|
+
const key = conversationKey(frame);
|
|
113
|
+
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
114
|
+
const current = previous
|
|
115
|
+
.catch(() => undefined)
|
|
116
|
+
.then(() => this.#process(frame))
|
|
117
|
+
.finally(() => {
|
|
118
|
+
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
119
|
+
});
|
|
120
|
+
this.#queues.set(key, current);
|
|
121
|
+
return current;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async waitForIdle() {
|
|
125
|
+
await Promise.allSettled([...this.#queues.values()]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async #sendActive(chatId, text) {
|
|
129
|
+
for (const chunk of splitUtf8(text)) {
|
|
130
|
+
await this.#client.sendMessage(chatId, { msgtype: 'markdown', markdown: { content: chunk } });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async #sendImmediate(frame, chatId, text) {
|
|
135
|
+
const chunks = splitUtf8(text);
|
|
136
|
+
if (chunks.length === 0) return;
|
|
137
|
+
try {
|
|
138
|
+
await this.#client.replyStream(frame, this.#generateReqId('stream'), chunks[0], true);
|
|
139
|
+
for (const chunk of chunks.slice(1)) {
|
|
140
|
+
await this.#client.sendMessage(chatId, { msgtype: 'markdown', markdown: { content: chunk } });
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
await this.#sendActive(chatId, text);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async #process(frame) {
|
|
148
|
+
const body = bodyOf(frame);
|
|
149
|
+
const messageId = typeof body.msgid === 'string' ? body.msgid : '';
|
|
150
|
+
const senderId = typeof body.from?.userid === 'string' ? body.from.userid : '';
|
|
151
|
+
const chatId = body.chattype === 'group' ? body.chatid : senderId;
|
|
152
|
+
if (!messageId || !senderId || !chatId || !['single', 'group'].includes(body.chattype)) return;
|
|
153
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
154
|
+
|
|
155
|
+
this.#status.messagesReceived += 1;
|
|
156
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
157
|
+
const text = messageText(frame);
|
|
158
|
+
let streamId = null;
|
|
159
|
+
let streamStarted = false;
|
|
160
|
+
try {
|
|
161
|
+
if (!text) {
|
|
162
|
+
await this.#sendImmediate(frame, chatId, '目前支持文字、语音转写和图文混排中的文字消息。');
|
|
163
|
+
await this.#state.markSeen(messageId);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const command = text.toLowerCase();
|
|
167
|
+
if (command === '/help') {
|
|
168
|
+
await this.#sendImmediate(frame, chatId, HELP_TEXT);
|
|
169
|
+
await this.#state.markSeen(messageId);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (command === '/status') {
|
|
173
|
+
await this.#harness.ensureRunning();
|
|
174
|
+
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
|
175
|
+
await this.#state.markSeen(messageId);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const key = conversationKey(frame);
|
|
179
|
+
if (command === '/new') {
|
|
180
|
+
await this.#state.clearSession(key);
|
|
181
|
+
await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
|
|
182
|
+
await this.#state.markSeen(messageId);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let sessionId = this.#state.sessionFor(key);
|
|
187
|
+
if (!sessionId || !(await this.#harness.sessionExists(sessionId))) {
|
|
188
|
+
sessionId = await this.#harness.createSession();
|
|
189
|
+
await this.#state.setSession(key, sessionId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
streamId = this.#generateReqId('stream');
|
|
193
|
+
try {
|
|
194
|
+
await this.#client.replyStream(frame, streamId, '正在思考中…', false);
|
|
195
|
+
streamStarted = true;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const answer = await this.#harness.ask(sessionId, text, {
|
|
201
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
202
|
+
onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
|
|
203
|
+
? async (update) => {
|
|
204
|
+
const progress = splitUtf8(progressText(update))[0];
|
|
205
|
+
if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const chunks = splitUtf8(answer || '任务已完成,但没有生成可显示的文本。');
|
|
211
|
+
let finalSent = false;
|
|
212
|
+
if (streamStarted && chunks.length > 0) {
|
|
213
|
+
try {
|
|
214
|
+
await this.#client.replyStream(frame, streamId, chunks[0], true);
|
|
215
|
+
for (const chunk of chunks.slice(1)) {
|
|
216
|
+
await this.#client.sendMessage(chatId, { msgtype: 'markdown', markdown: { content: chunk } });
|
|
217
|
+
}
|
|
218
|
+
finalSent = true;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.#logger.warn?.('[dsh-im:wecom] stream finalization failed; using an active reply:', error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!finalSent) await this.#sendActive(chatId, answer);
|
|
224
|
+
await this.#state.markSeen(messageId);
|
|
225
|
+
this.#status.messagesReplied += 1;
|
|
226
|
+
this.#status.lastReplyAt = new Date().toISOString();
|
|
227
|
+
this.#status.lastError = null;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
230
|
+
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
231
|
+
try {
|
|
232
|
+
if (streamStarted && streamId) {
|
|
233
|
+
await this.#client.replyStream(frame, streamId, '消息处理失败,请稍后重试。', true);
|
|
234
|
+
} else {
|
|
235
|
+
await this.#sendImmediate(frame, chatId, '消息处理失败,请稍后重试。');
|
|
236
|
+
}
|
|
237
|
+
await this.#state.markSeen(messageId);
|
|
238
|
+
} catch {
|
|
239
|
+
this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|