@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,268 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
|
|
6
|
+
function messageText(event) {
|
|
7
|
+
return (event?.data?.message?.content ?? [])
|
|
8
|
+
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
9
|
+
.map((part) => part.text)
|
|
10
|
+
.join('\n')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class HarnessReplyTracker {
|
|
15
|
+
#promptRpcId;
|
|
16
|
+
#lastSeq;
|
|
17
|
+
#openTurn = null;
|
|
18
|
+
#targetTurn = null;
|
|
19
|
+
#stepText = new Map();
|
|
20
|
+
#latestText = '';
|
|
21
|
+
#finished = false;
|
|
22
|
+
#reason = null;
|
|
23
|
+
|
|
24
|
+
constructor({ promptRpcId, afterSeq = -1 }) {
|
|
25
|
+
this.#promptRpcId = promptRpcId;
|
|
26
|
+
this.#lastSeq = afterSeq;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get finished() {
|
|
30
|
+
return this.#finished;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get answer() {
|
|
34
|
+
return this.#latestText.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get reason() {
|
|
38
|
+
return this.#reason;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
consume(entries) {
|
|
42
|
+
let update = null;
|
|
43
|
+
const ordered = [...entries]
|
|
44
|
+
.map((entry) => entry?.event ?? entry)
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1));
|
|
47
|
+
|
|
48
|
+
for (const event of ordered) {
|
|
49
|
+
const seq = event.seq ?? -1;
|
|
50
|
+
if (seq <= this.#lastSeq) continue;
|
|
51
|
+
this.#lastSeq = seq;
|
|
52
|
+
|
|
53
|
+
if (event.type === 'turn/start') {
|
|
54
|
+
this.#openTurn = event.data?.turn ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (event.type === 'user/message'
|
|
58
|
+
&& event.data?.source?.rpcId === this.#promptRpcId) {
|
|
59
|
+
this.#targetTurn = this.#openTurn;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.#targetTurn === null) continue;
|
|
64
|
+
|
|
65
|
+
if (event.type === 'turn/end') {
|
|
66
|
+
if (event.data?.turn !== this.#targetTurn) continue;
|
|
67
|
+
this.#finished = true;
|
|
68
|
+
this.#reason = event.data?.reason ?? null;
|
|
69
|
+
this.#openTurn = null;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (event.data?.turn !== this.#targetTurn) continue;
|
|
74
|
+
|
|
75
|
+
if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
76
|
+
const step = event.data?.step ?? 0;
|
|
77
|
+
const index = event.data.chunk.index ?? 0;
|
|
78
|
+
const key = `${step}:${index}`;
|
|
79
|
+
this.#stepText.set(key, (this.#stepText.get(key) ?? '') + event.data.chunk.text);
|
|
80
|
+
const stepPrefix = `${step}:`;
|
|
81
|
+
const text = [...this.#stepText.entries()]
|
|
82
|
+
.filter(([partKey]) => partKey.startsWith(stepPrefix))
|
|
83
|
+
.sort(([left], [right]) => Number(left.split(':')[1]) - Number(right.split(':')[1]))
|
|
84
|
+
.map(([, part]) => part)
|
|
85
|
+
.join('\n')
|
|
86
|
+
.trim();
|
|
87
|
+
if (text && text !== this.#latestText) {
|
|
88
|
+
this.#latestText = text;
|
|
89
|
+
update = { type: 'text', text };
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (event.type === 'assistant/message') {
|
|
95
|
+
const text = messageText(event);
|
|
96
|
+
if (text && text !== this.#latestText) {
|
|
97
|
+
this.#latestText = text;
|
|
98
|
+
update = { type: 'text', text };
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (event.type === 'tool/call') {
|
|
104
|
+
update = { type: 'tool', name: event.data?.name ?? '工具' };
|
|
105
|
+
} else if (event.type === 'tool/result') {
|
|
106
|
+
update = { type: 'status', text: '正在整理结果…' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return update;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class HarnessRpcError extends Error {
|
|
115
|
+
constructor(method, error) {
|
|
116
|
+
super(`${method}: ${error?.message ?? 'unknown Harness RPC error'}`);
|
|
117
|
+
this.name = 'HarnessRpcError';
|
|
118
|
+
this.method = method;
|
|
119
|
+
this.code = error?.code ?? 'internal';
|
|
120
|
+
this.details = error?.details ?? {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class HarnessClient {
|
|
125
|
+
#baseUrl;
|
|
126
|
+
#workspace;
|
|
127
|
+
#agentPreset;
|
|
128
|
+
#autostart;
|
|
129
|
+
#dshBin;
|
|
130
|
+
#managedProcess = null;
|
|
131
|
+
|
|
132
|
+
constructor({ baseUrl, workspace, agentPreset, autostart, dshBin }) {
|
|
133
|
+
this.#baseUrl = new URL(baseUrl);
|
|
134
|
+
this.#workspace = workspace;
|
|
135
|
+
this.#agentPreset = agentPreset;
|
|
136
|
+
this.#autostart = autostart;
|
|
137
|
+
this.#dshBin = dshBin;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async rpc(method, payload = {}, timeoutMs = 30000, options = {}) {
|
|
141
|
+
const rpcId = options.rpcId ?? `feishu-${randomUUID()}`;
|
|
142
|
+
const response = await fetch(new URL(`/api/${method}`, this.#baseUrl), {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'content-type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
|
146
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) throw new Error(`Harness transport ${method} failed: HTTP ${response.status}`);
|
|
149
|
+
const body = await response.json();
|
|
150
|
+
if (body?.type !== 'server-response' || body?.rpcId !== rpcId) {
|
|
151
|
+
throw new Error(`Harness returned an invalid response for ${method}`);
|
|
152
|
+
}
|
|
153
|
+
if (!body.result?.ok) throw new HarnessRpcError(method, body.result?.error);
|
|
154
|
+
return body.result.value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async health() {
|
|
158
|
+
await this.rpc('host.describe', {}, 5000);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async ensureRunning() {
|
|
163
|
+
try {
|
|
164
|
+
return await this.health();
|
|
165
|
+
} catch (firstError) {
|
|
166
|
+
if (!this.#autostart) throw firstError;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!this.#managedProcess || this.#managedProcess.exitCode !== null) {
|
|
170
|
+
const port = this.#baseUrl.port || (this.#baseUrl.protocol === 'https:' ? '443' : '80');
|
|
171
|
+
this.#managedProcess = spawn(this.#dshBin, [
|
|
172
|
+
'web',
|
|
173
|
+
'--host',
|
|
174
|
+
this.#baseUrl.hostname,
|
|
175
|
+
'--port',
|
|
176
|
+
port,
|
|
177
|
+
], {
|
|
178
|
+
cwd: this.#workspace,
|
|
179
|
+
env: process.env,
|
|
180
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
181
|
+
});
|
|
182
|
+
this.#managedProcess.on('error', (error) => {
|
|
183
|
+
console.error('[bridge] failed to start Harness:', error.message);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const deadline = Date.now() + 60000;
|
|
188
|
+
let lastError;
|
|
189
|
+
while (Date.now() < deadline) {
|
|
190
|
+
await sleep(1000);
|
|
191
|
+
try {
|
|
192
|
+
return await this.health();
|
|
193
|
+
} catch (error) {
|
|
194
|
+
lastError = error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async workspaceId() {
|
|
201
|
+
const { items } = await this.rpc('workspace.list', {});
|
|
202
|
+
const existing = items.find((item) => item.path === this.#workspace);
|
|
203
|
+
if (existing) return existing.workspaceId;
|
|
204
|
+
const created = await this.rpc('workspace.create', { path: this.#workspace });
|
|
205
|
+
return created.workspace.workspaceId;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async createSession() {
|
|
209
|
+
await this.ensureRunning();
|
|
210
|
+
const workspaceId = await this.workspaceId();
|
|
211
|
+
const created = await this.rpc('session.create', {
|
|
212
|
+
workspaceId,
|
|
213
|
+
agentPreset: this.#agentPreset,
|
|
214
|
+
});
|
|
215
|
+
return created.sessionId;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async sessionExists(sessionId) {
|
|
219
|
+
try {
|
|
220
|
+
await this.rpc('session.history', { sessionId, maxMessages: 1 });
|
|
221
|
+
return true;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof HarnessRpcError && error.code === 'session-not-found') return false;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async ask(sessionId, text, options = {}) {
|
|
229
|
+
if (typeof options === 'number') options = { timeoutMs: options };
|
|
230
|
+
const timeoutMs = options.timeoutMs ?? 600000;
|
|
231
|
+
const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
|
|
232
|
+
await this.ensureRunning();
|
|
233
|
+
const before = await this.rpc('session.history', { sessionId, maxMessages: 1 });
|
|
234
|
+
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
235
|
+
const promptRpcId = `feishu-${randomUUID()}`;
|
|
236
|
+
const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq });
|
|
237
|
+
|
|
238
|
+
await this.rpc('session.prompt', {
|
|
239
|
+
sessionId,
|
|
240
|
+
mode: 'queue',
|
|
241
|
+
content: [{ type: 'text', text }],
|
|
242
|
+
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
243
|
+
}, 30000, { rpcId: promptRpcId });
|
|
244
|
+
|
|
245
|
+
const deadline = Date.now() + timeoutMs;
|
|
246
|
+
while (Date.now() < deadline) {
|
|
247
|
+
await sleep(300);
|
|
248
|
+
const history = await this.rpc('session.history', { sessionId, maxMessages: 50 });
|
|
249
|
+
const update = tracker.consume(history.events ?? []);
|
|
250
|
+
if (update && onUpdate) {
|
|
251
|
+
try {
|
|
252
|
+
await onUpdate(update);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.warn('[bridge] ignored a progress update failure:', error.message);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!tracker.finished) continue;
|
|
258
|
+
if (tracker.answer) return tracker.answer;
|
|
259
|
+
|
|
260
|
+
throw new Error(`Harness turn ended without a text reply${tracker.reason ? ` (${JSON.stringify(tracker.reason)})` : ''}`);
|
|
261
|
+
}
|
|
262
|
+
throw new Error(`Harness reply timed out after ${Math.round(timeoutMs / 1000)} seconds`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
stopManagedProcess() {
|
|
266
|
+
if (this.#managedProcess?.exitCode === null) this.#managedProcess.kill('SIGTERM');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function conversationKey(event) {
|
|
2
|
+
const chatType = event?.message?.chat_type;
|
|
3
|
+
if (chatType === 'p2p') {
|
|
4
|
+
const senderId = event?.sender?.sender_id?.open_id || event?.sender?.sender_id?.user_id;
|
|
5
|
+
if (!senderId) throw new Error('Feishu p2p event has no sender id');
|
|
6
|
+
return `p2p:${senderId}`;
|
|
7
|
+
}
|
|
8
|
+
const chatId = event?.message?.chat_id;
|
|
9
|
+
if (!chatId) throw new Error('Feishu group event has no chat id');
|
|
10
|
+
return `group:${chatId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extractText(event) {
|
|
14
|
+
if (event?.message?.message_type !== 'text') return null;
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(event.message.content);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let text = typeof parsed.text === 'string' ? parsed.text : '';
|
|
22
|
+
for (const mention of event.message.mentions ?? []) {
|
|
23
|
+
if (typeof mention.key === 'string' && mention.key) text = text.replaceAll(mention.key, '');
|
|
24
|
+
}
|
|
25
|
+
return text.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function splitText(text, maxChars = 9000) {
|
|
29
|
+
if (text.length <= maxChars) return [text];
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let remaining = text;
|
|
32
|
+
while (remaining.length > maxChars) {
|
|
33
|
+
let splitAt = remaining.lastIndexOf('\n', maxChars);
|
|
34
|
+
if (splitAt < Math.floor(maxChars * 0.6)) splitAt = maxChars;
|
|
35
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
36
|
+
remaining = remaining.slice(splitAt).replace(/^\n+/, '');
|
|
37
|
+
}
|
|
38
|
+
if (remaining) chunks.push(remaining);
|
|
39
|
+
return chunks;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isBotSender(event) {
|
|
43
|
+
return event?.sender?.sender_type === 'bot';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isAllowedSender(event, allowedOpenIds) {
|
|
47
|
+
if (!allowedOpenIds || allowedOpenIds.size === 0) return false;
|
|
48
|
+
if (allowedOpenIds.has('*')) return true;
|
|
49
|
+
const senderOpenId = event?.sender?.sender_id?.open_id;
|
|
50
|
+
return typeof senderOpenId === 'string' && allowedOpenIds.has(senderOpenId);
|
|
51
|
+
}
|