@botcord/daemon 0.2.58 → 0.2.60
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/dist/config.d.ts +4 -1
- package/dist/config.js +2 -2
- package/dist/cross-room.js +3 -1
- package/dist/daemon-config-map.js +6 -0
- package/dist/daemon.js +21 -1
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.js +35 -6
- package/dist/gateway/channels/feishu-registration.d.ts +35 -0
- package/dist/gateway/channels/feishu-registration.js +101 -0
- package/dist/gateway/channels/feishu.d.ts +16 -0
- package/dist/gateway/channels/feishu.js +459 -0
- package/dist/gateway/channels/index.d.ts +2 -0
- package/dist/gateway/channels/index.js +2 -0
- package/dist/gateway/channels/login-session.d.ts +9 -1
- package/dist/gateway/channels/login-session.js +1 -1
- package/dist/gateway/channels/wechat.js +26 -2
- package/dist/gateway/dispatcher.d.ts +3 -0
- package/dist/gateway/dispatcher.js +190 -30
- package/dist/gateway/policy-resolver.d.ts +10 -6
- package/dist/gateway/types.d.ts +1 -1
- package/dist/gateway-control.d.ts +8 -1
- package/dist/gateway-control.js +171 -18
- package/dist/index.js +9 -3
- package/dist/log.d.ts +9 -0
- package/dist/log.js +89 -1
- package/dist/provision.js +7 -1
- package/package.json +2 -1
- package/src/__tests__/cross-room.test.ts +2 -0
- package/src/__tests__/diagnostics.test.ts +37 -1
- package/src/__tests__/gateway-control.test.ts +84 -0
- package/src/__tests__/log.test.ts +28 -1
- package/src/__tests__/policy-updated-handler.test.ts +5 -7
- package/src/__tests__/third-party-gateway.test.ts +28 -0
- package/src/__tests__/wechat-channel.test.ts +47 -0
- package/src/config.ts +6 -3
- package/src/cross-room.ts +3 -1
- package/src/daemon-config-map.ts +3 -0
- package/src/daemon.ts +24 -3
- package/src/diagnostics.ts +36 -6
- package/src/gateway/__tests__/dispatcher.test.ts +62 -4
- package/src/gateway/__tests__/feishu-channel.test.ts +306 -0
- package/src/gateway/channels/feishu-registration.ts +155 -0
- package/src/gateway/channels/feishu.ts +554 -0
- package/src/gateway/channels/index.ts +6 -0
- package/src/gateway/channels/login-session.ts +10 -2
- package/src/gateway/channels/wechat.ts +29 -2
- package/src/gateway/dispatcher.ts +216 -29
- package/src/gateway/policy-resolver.ts +19 -11
- package/src/gateway/types.ts +1 -1
- package/src/gateway-control.ts +188 -17
- package/src/index.ts +9 -3
- package/src/log.ts +100 -1
- package/src/provision.ts +13 -1
|
@@ -78,6 +78,7 @@ export class Dispatcher {
|
|
|
78
78
|
resolveHubUrl;
|
|
79
79
|
transcript;
|
|
80
80
|
queues = new Map();
|
|
81
|
+
deferredMultimodal = new Map();
|
|
81
82
|
/**
|
|
82
83
|
* Last `/hub/typing` ping timestamp per (accountId, conversationId).
|
|
83
84
|
* Used to debounce cancel-previous bursts so we don't trip Hub's 20/min
|
|
@@ -125,6 +126,10 @@ export class Dispatcher {
|
|
|
125
126
|
await this.safeAck(envelope);
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
129
|
+
const managed = this.managedRoutes ? Array.from(this.managedRoutes.values()) : undefined;
|
|
130
|
+
const route = resolveRoute(msg, this.config, managed);
|
|
131
|
+
const mode = resolveQueueMode(route, msg.conversation.kind);
|
|
132
|
+
const queueKey = buildQueueKey(msg);
|
|
128
133
|
// Pre-skip: empty/whitespace text.
|
|
129
134
|
const rawText = typeof msg.text === "string" ? msg.text.trim() : "";
|
|
130
135
|
if (!rawText) {
|
|
@@ -135,20 +140,76 @@ export class Dispatcher {
|
|
|
135
140
|
// From here on, the inbound is a real conversation event — generate a
|
|
136
141
|
// turnId and write the inbound transcript record.
|
|
137
142
|
const turnId = randomUUID();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
// Multimodal-only arrivals (files/images without sender-authored text)
|
|
144
|
+
// should not wake the runtime on their own. Ack them, record the inbound
|
|
145
|
+
// event, and prepend them to the next text-bearing turn for this queue.
|
|
146
|
+
if (isMultimodalOnlyMessage(msg)) {
|
|
147
|
+
await this.safeAck(envelope);
|
|
148
|
+
this.emitInbound(turnId, msg);
|
|
149
|
+
this.deferMultimodal(queueKey, { route, msg, channel, turnId, queuedAt: Date.now() });
|
|
150
|
+
this.log.info("dispatcher: deferred multimodal-only inbound", {
|
|
151
|
+
agentId: msg.accountId,
|
|
152
|
+
roomId: msg.conversation.id,
|
|
153
|
+
topicId: msg.conversation.threadId ?? null,
|
|
154
|
+
turnId,
|
|
155
|
+
messageId: msg.id,
|
|
156
|
+
senderId: msg.sender.id,
|
|
157
|
+
senderKind: msg.sender.kind,
|
|
158
|
+
mode,
|
|
159
|
+
queueKey,
|
|
160
|
+
});
|
|
161
|
+
if (this.onInbound) {
|
|
162
|
+
try {
|
|
163
|
+
await this.onInbound(msg);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
this.log.warn("dispatcher: onInbound threw — continuing", {
|
|
167
|
+
messageId: msg.id,
|
|
168
|
+
error: err instanceof Error ? err.message : String(err),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const deferred = this.takeDeferredMultimodal(queueKey);
|
|
175
|
+
let dispatchMsg = msg;
|
|
176
|
+
let dispatchTurnId = turnId;
|
|
177
|
+
let dispatchRoute = route;
|
|
178
|
+
let dispatchChannel = channel;
|
|
179
|
+
let text = rawText;
|
|
180
|
+
let mergedFromDeferredTurnIds = [];
|
|
181
|
+
if (deferred.length > 0) {
|
|
182
|
+
const merged = this.mergeSerialBuffer([...deferred, { route, msg, channel, turnId }], queueKey);
|
|
183
|
+
if (merged) {
|
|
184
|
+
dispatchMsg = merged.msg;
|
|
185
|
+
dispatchTurnId = merged.turnId;
|
|
186
|
+
dispatchRoute = merged.route;
|
|
187
|
+
dispatchChannel = merged.channel;
|
|
188
|
+
text = merged.text;
|
|
189
|
+
mergedFromDeferredTurnIds = deferred.map((e) => e.turnId);
|
|
190
|
+
for (const entry of deferred) {
|
|
191
|
+
this.transcript.write({
|
|
192
|
+
ts: nowIso(),
|
|
193
|
+
kind: "dropped",
|
|
194
|
+
turnId: entry.turnId,
|
|
195
|
+
agentId: entry.msg.accountId,
|
|
196
|
+
roomId: entry.msg.conversation.id,
|
|
197
|
+
topicId: entry.msg.conversation.threadId ?? null,
|
|
198
|
+
reason: "batch_merged",
|
|
199
|
+
supersededBy: dispatchTurnId,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
142
204
|
// Compose the final user-turn text only for cancel-previous mode, where
|
|
143
205
|
// the dispatcher consumes the pre-composed text directly. Serial mode
|
|
144
206
|
// re-runs the composer at drain time on the merged message (so it sees
|
|
145
207
|
// the full coalesced batch instead of any single arrival), so calling
|
|
146
208
|
// the composer here would just be redundant work.
|
|
147
|
-
let text = rawText;
|
|
148
209
|
let composeFailedError;
|
|
149
210
|
if (mode === "cancel-previous" && this.composeUserTurn) {
|
|
150
211
|
try {
|
|
151
|
-
const composed = this.composeUserTurn(
|
|
212
|
+
const composed = this.composeUserTurn(dispatchMsg);
|
|
152
213
|
if (typeof composed === "string" && composed.length > 0) {
|
|
153
214
|
text = composed;
|
|
154
215
|
}
|
|
@@ -156,7 +217,7 @@ export class Dispatcher {
|
|
|
156
217
|
catch (err) {
|
|
157
218
|
composeFailedError = err instanceof Error ? err.message : String(err);
|
|
158
219
|
this.log.warn("dispatcher: composeUserTurn threw — using raw text", {
|
|
159
|
-
messageId:
|
|
220
|
+
messageId: dispatchMsg.id,
|
|
160
221
|
error: composeFailedError,
|
|
161
222
|
});
|
|
162
223
|
}
|
|
@@ -197,29 +258,29 @@ export class Dispatcher {
|
|
|
197
258
|
if (this.attentionGate) {
|
|
198
259
|
let wake = true;
|
|
199
260
|
try {
|
|
200
|
-
const result = this.attentionGate(
|
|
261
|
+
const result = this.attentionGate(dispatchMsg);
|
|
201
262
|
wake = result instanceof Promise ? await result : result;
|
|
202
263
|
}
|
|
203
264
|
catch (err) {
|
|
204
265
|
this.log.warn("dispatcher: attentionGate threw — waking", {
|
|
205
|
-
messageId:
|
|
266
|
+
messageId: dispatchMsg.id,
|
|
206
267
|
error: err instanceof Error ? err.message : String(err),
|
|
207
268
|
});
|
|
208
269
|
wake = true;
|
|
209
270
|
}
|
|
210
271
|
if (!wake) {
|
|
211
272
|
this.log.debug("dispatcher skip turn: attention policy", {
|
|
212
|
-
messageId:
|
|
213
|
-
accountId:
|
|
214
|
-
conversationId:
|
|
273
|
+
messageId: dispatchMsg.id,
|
|
274
|
+
accountId: dispatchMsg.accountId,
|
|
275
|
+
conversationId: dispatchMsg.conversation.id,
|
|
215
276
|
});
|
|
216
277
|
this.transcript.write({
|
|
217
278
|
ts: nowIso(),
|
|
218
279
|
kind: "attention_skipped",
|
|
219
|
-
turnId,
|
|
220
|
-
agentId:
|
|
221
|
-
roomId:
|
|
222
|
-
topicId:
|
|
280
|
+
turnId: dispatchTurnId,
|
|
281
|
+
agentId: dispatchMsg.accountId,
|
|
282
|
+
roomId: dispatchMsg.conversation.id,
|
|
283
|
+
topicId: dispatchMsg.conversation.threadId ?? null,
|
|
223
284
|
reason: "attention_gate_false",
|
|
224
285
|
});
|
|
225
286
|
return;
|
|
@@ -229,19 +290,19 @@ export class Dispatcher {
|
|
|
229
290
|
this.transcript.write({
|
|
230
291
|
ts: nowIso(),
|
|
231
292
|
kind: "compose_failed",
|
|
232
|
-
turnId,
|
|
233
|
-
agentId:
|
|
234
|
-
roomId:
|
|
235
|
-
topicId:
|
|
293
|
+
turnId: dispatchTurnId,
|
|
294
|
+
agentId: dispatchMsg.accountId,
|
|
295
|
+
roomId: dispatchMsg.conversation.id,
|
|
296
|
+
topicId: dispatchMsg.conversation.threadId ?? null,
|
|
236
297
|
error: composeFailedError,
|
|
237
298
|
fallback: "raw_text",
|
|
238
299
|
});
|
|
239
300
|
}
|
|
240
301
|
if (mode === "cancel-previous") {
|
|
241
|
-
await this.runCancelPrevious(queueKey,
|
|
302
|
+
await this.runCancelPrevious(queueKey, dispatchRoute, text, dispatchMsg, dispatchChannel, dispatchTurnId, mergedFromDeferredTurnIds);
|
|
242
303
|
}
|
|
243
304
|
else {
|
|
244
|
-
await this.runSerial(queueKey,
|
|
305
|
+
await this.runSerial(queueKey, dispatchRoute, text, dispatchMsg, dispatchChannel, dispatchTurnId, mergedFromDeferredTurnIds);
|
|
245
306
|
}
|
|
246
307
|
}
|
|
247
308
|
/** Snapshot of currently running turns keyed by queue key. */
|
|
@@ -283,7 +344,37 @@ export class Dispatcher {
|
|
|
283
344
|
}
|
|
284
345
|
return q;
|
|
285
346
|
}
|
|
286
|
-
|
|
347
|
+
deferMultimodal(queueKey, entry) {
|
|
348
|
+
const list = this.deferredMultimodal.get(queueKey) ?? [];
|
|
349
|
+
list.push(entry);
|
|
350
|
+
while (list.length > MAX_BATCH_BUFFER_ENTRIES) {
|
|
351
|
+
const dropped = list.shift();
|
|
352
|
+
this.log.warn("dispatcher: deferred multimodal buffer overflow — dropped oldest", {
|
|
353
|
+
queueKey,
|
|
354
|
+
droppedMessageId: dropped.msg.id,
|
|
355
|
+
bufferCap: MAX_BATCH_BUFFER_ENTRIES,
|
|
356
|
+
});
|
|
357
|
+
this.transcript.write({
|
|
358
|
+
ts: nowIso(),
|
|
359
|
+
kind: "dropped",
|
|
360
|
+
turnId: dropped.turnId,
|
|
361
|
+
agentId: dropped.msg.accountId,
|
|
362
|
+
roomId: dropped.msg.conversation.id,
|
|
363
|
+
topicId: dropped.msg.conversation.threadId ?? null,
|
|
364
|
+
reason: "queue_overflow",
|
|
365
|
+
supersededBy: null,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
this.deferredMultimodal.set(queueKey, list);
|
|
369
|
+
}
|
|
370
|
+
takeDeferredMultimodal(queueKey) {
|
|
371
|
+
const list = this.deferredMultimodal.get(queueKey);
|
|
372
|
+
if (!list || list.length === 0)
|
|
373
|
+
return [];
|
|
374
|
+
this.deferredMultimodal.delete(queueKey);
|
|
375
|
+
return list;
|
|
376
|
+
}
|
|
377
|
+
async runCancelPrevious(queueKey, route, text, msg, channel, turnId, mergedFromTurnIds = []) {
|
|
287
378
|
const q = this.getQueue(queueKey);
|
|
288
379
|
// Bump the generation on every arrival. Older arrivals still awaiting
|
|
289
380
|
// the prior turn's teardown will observe `myGen !== q.cancelGen` when
|
|
@@ -342,7 +433,7 @@ export class Dispatcher {
|
|
|
342
433
|
});
|
|
343
434
|
return;
|
|
344
435
|
}
|
|
345
|
-
await this.runTurn(queueKey, route, text, msg, channel, turnId,
|
|
436
|
+
await this.runTurn(queueKey, route, text, msg, channel, turnId, mergedFromTurnIds);
|
|
346
437
|
}
|
|
347
438
|
/**
|
|
348
439
|
* Serial mode with coalesce-on-drain semantics:
|
|
@@ -362,7 +453,7 @@ export class Dispatcher {
|
|
|
362
453
|
* merged message so the runtime sees a single coherent prompt covering all
|
|
363
454
|
* coalesced messages.
|
|
364
455
|
*/
|
|
365
|
-
async runSerial(queueKey, route, _text, msg, channel, turnId) {
|
|
456
|
+
async runSerial(queueKey, route, _text, msg, channel, turnId, mergedFromTurnIds = []) {
|
|
366
457
|
const q = this.getQueue(queueKey);
|
|
367
458
|
q.serialBuffer.push({ route, msg, channel, turnId });
|
|
368
459
|
while (q.serialBuffer.length > MAX_BATCH_BUFFER_ENTRIES) {
|
|
@@ -409,8 +500,10 @@ export class Dispatcher {
|
|
|
409
500
|
});
|
|
410
501
|
}
|
|
411
502
|
}
|
|
412
|
-
const
|
|
413
|
-
|
|
503
|
+
const mergedTurnIds = drained.length > 1
|
|
504
|
+
? [...mergedFromTurnIds, ...drained.slice(0, -1).map((e) => e.turnId)]
|
|
505
|
+
: mergedFromTurnIds;
|
|
506
|
+
await this.runTurn(queueKey, merged.route, merged.text, merged.msg, merged.channel, merged.turnId, mergedTurnIds);
|
|
414
507
|
}
|
|
415
508
|
}
|
|
416
509
|
finally {
|
|
@@ -478,8 +571,13 @@ export class Dispatcher {
|
|
|
478
571
|
const latestRaw = latest.msg.raw ?? {};
|
|
479
572
|
const mergedRaw = { ...latestRaw, batch: items };
|
|
480
573
|
const anyMentioned = entries.some((e) => e.msg.mentioned === true);
|
|
574
|
+
const mergedText = entries
|
|
575
|
+
.map((e) => (typeof e.msg.text === "string" ? e.msg.text.trim() : ""))
|
|
576
|
+
.filter((s) => s.length > 0)
|
|
577
|
+
.join("\n");
|
|
481
578
|
const mergedMsg = {
|
|
482
579
|
...latest.msg,
|
|
580
|
+
...(mergedText ? { text: mergedText } : {}),
|
|
483
581
|
mentioned: anyMentioned,
|
|
484
582
|
raw: mergedRaw,
|
|
485
583
|
};
|
|
@@ -905,6 +1003,7 @@ export class Dispatcher {
|
|
|
905
1003
|
// own loop-risk accounting downstream.
|
|
906
1004
|
const isOwnerChat = isOwnerChatRoom(msg);
|
|
907
1005
|
const canDeliverRuntimeText = isOwnerChat || !isBotCordChannel(channel);
|
|
1006
|
+
const canDeliverRuntimeDiagnostics = canDeliverRuntimeText || isBotCordChannel(channel);
|
|
908
1007
|
if (slot.timedOut) {
|
|
909
1008
|
this.transcript.write({
|
|
910
1009
|
ts: nowIso(),
|
|
@@ -917,7 +1016,7 @@ export class Dispatcher {
|
|
|
917
1016
|
error: `runtime timeout after ${this.turnTimeoutMs}ms`,
|
|
918
1017
|
durationMs: Date.now() - slot.dispatchedAt,
|
|
919
1018
|
});
|
|
920
|
-
if (
|
|
1019
|
+
if (canDeliverRuntimeDiagnostics) {
|
|
921
1020
|
await this.sendReply(channel, {
|
|
922
1021
|
channel: msg.channel,
|
|
923
1022
|
accountId: msg.accountId,
|
|
@@ -962,7 +1061,7 @@ export class Dispatcher {
|
|
|
962
1061
|
error: errMsg,
|
|
963
1062
|
durationMs: Date.now() - slot.dispatchedAt,
|
|
964
1063
|
});
|
|
965
|
-
if (
|
|
1064
|
+
if (canDeliverRuntimeDiagnostics) {
|
|
966
1065
|
await this.sendReply(channel, {
|
|
967
1066
|
channel: msg.channel,
|
|
968
1067
|
accountId: msg.accountId,
|
|
@@ -1052,7 +1151,7 @@ export class Dispatcher {
|
|
|
1052
1151
|
runtime: route.runtime,
|
|
1053
1152
|
error: result.error,
|
|
1054
1153
|
});
|
|
1055
|
-
if (
|
|
1154
|
+
if (canDeliverRuntimeDiagnostics) {
|
|
1056
1155
|
const sendResult = await this.sendReply(channel, {
|
|
1057
1156
|
channel: msg.channel,
|
|
1058
1157
|
accountId: msg.accountId,
|
|
@@ -1296,6 +1395,67 @@ function isOwnerChatRoom(msg) {
|
|
|
1296
1395
|
function isBotCordChannel(channel) {
|
|
1297
1396
|
return channel.type === "botcord" || channel.id === "botcord";
|
|
1298
1397
|
}
|
|
1398
|
+
function isMultimodalOnlyMessage(msg) {
|
|
1399
|
+
if (!hasMultimodalContent(msg.raw))
|
|
1400
|
+
return false;
|
|
1401
|
+
return !hasAuthoredText(msg.raw);
|
|
1402
|
+
}
|
|
1403
|
+
function hasAuthoredText(raw) {
|
|
1404
|
+
if (!raw || typeof raw !== "object")
|
|
1405
|
+
return false;
|
|
1406
|
+
const obj = raw;
|
|
1407
|
+
const batch = obj.batch;
|
|
1408
|
+
if (Array.isArray(batch))
|
|
1409
|
+
return batch.some((item) => hasAuthoredText(item));
|
|
1410
|
+
if (typeof obj.text === "string" && obj.text.trim().length > 0) {
|
|
1411
|
+
// BotCord's /hub/inbox `text` may be synthesized from attachment metadata
|
|
1412
|
+
// when payload text is empty, so prefer envelope payload below when present.
|
|
1413
|
+
if (!obj.envelope || typeof obj.envelope !== "object")
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
const envelope = obj.envelope;
|
|
1417
|
+
const payload = envelope?.payload;
|
|
1418
|
+
if (payload) {
|
|
1419
|
+
for (const key of ["text", "body", "message"]) {
|
|
1420
|
+
const value = payload[key];
|
|
1421
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
return false;
|
|
1425
|
+
}
|
|
1426
|
+
const itemList = obj.item_list;
|
|
1427
|
+
if (Array.isArray(itemList)) {
|
|
1428
|
+
return itemList.some((item) => {
|
|
1429
|
+
if (!item || typeof item !== "object")
|
|
1430
|
+
return false;
|
|
1431
|
+
const textItem = item.text_item;
|
|
1432
|
+
return typeof textItem?.text === "string" && textItem.text.trim().length > 0;
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
return typeof obj.text === "string" && obj.text.trim().length > 0;
|
|
1436
|
+
}
|
|
1437
|
+
function hasMultimodalContent(raw) {
|
|
1438
|
+
if (!raw || typeof raw !== "object")
|
|
1439
|
+
return false;
|
|
1440
|
+
const obj = raw;
|
|
1441
|
+
const batch = obj.batch;
|
|
1442
|
+
if (Array.isArray(batch))
|
|
1443
|
+
return batch.some((item) => hasMultimodalContent(item));
|
|
1444
|
+
const envelope = obj.envelope;
|
|
1445
|
+
const payload = envelope?.payload;
|
|
1446
|
+
const attachments = payload?.attachments;
|
|
1447
|
+
if (Array.isArray(attachments) && attachments.length > 0)
|
|
1448
|
+
return true;
|
|
1449
|
+
const itemList = obj.item_list;
|
|
1450
|
+
if (Array.isArray(itemList)) {
|
|
1451
|
+
return itemList.some((item) => {
|
|
1452
|
+
if (!item || typeof item !== "object")
|
|
1453
|
+
return false;
|
|
1454
|
+
return item.type !== 1;
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1299
1459
|
function resolveQueueMode(route, kind) {
|
|
1300
1460
|
if (route.queueMode)
|
|
1301
1461
|
return route.queueMode;
|
|
@@ -22,25 +22,29 @@
|
|
|
22
22
|
* the agent is revoked or the cache must rebuild from scratch.
|
|
23
23
|
*/
|
|
24
24
|
import type { AttentionPolicy } from "@botcord/protocol-core";
|
|
25
|
+
export type DaemonAttentionPolicy = Omit<AttentionPolicy, "mode"> & {
|
|
26
|
+
mode: AttentionPolicy["mode"] | "allowed_senders";
|
|
27
|
+
allowedSenderIds?: string[];
|
|
28
|
+
};
|
|
25
29
|
/** Public surface — kept narrow so the dispatcher can mock easily in tests. */
|
|
26
30
|
export interface PolicyResolverLike {
|
|
27
|
-
resolve(agentId: string, roomId: string | null): Promise<
|
|
31
|
+
resolve(agentId: string, roomId: string | null): Promise<DaemonAttentionPolicy>;
|
|
28
32
|
invalidate(agentId: string, roomId?: string): void;
|
|
29
33
|
/**
|
|
30
34
|
* Install (or replace) the cached policy entry for an agent / room. Used
|
|
31
35
|
* by the `policy_updated` control-frame handler to apply embedded policy
|
|
32
36
|
* payloads without forcing a refetch.
|
|
33
37
|
*/
|
|
34
|
-
put(agentId: string, roomId: string | null, policy:
|
|
38
|
+
put(agentId: string, roomId: string | null, policy: DaemonAttentionPolicy): void;
|
|
35
39
|
}
|
|
36
40
|
export interface PolicyResolverOptions {
|
|
37
41
|
/** Fetcher for the per-agent default. Returning `undefined` means "no policy known"; the resolver falls back to `mode=always`. */
|
|
38
|
-
fetchGlobal: (agentId: string) => Promise<
|
|
42
|
+
fetchGlobal: (agentId: string) => Promise<DaemonAttentionPolicy | undefined>;
|
|
39
43
|
/**
|
|
40
44
|
* Optional per-room fetcher. PR2 supplies this; PR3 leaves it
|
|
41
45
|
* unimplemented and the resolver collapses to the global policy.
|
|
42
46
|
*/
|
|
43
|
-
fetchEffective?: (agentId: string, roomId: string) => Promise<
|
|
47
|
+
fetchEffective?: (agentId: string, roomId: string) => Promise<DaemonAttentionPolicy | undefined>;
|
|
44
48
|
/** Cache TTL in milliseconds. Defaults to 5 minutes. */
|
|
45
49
|
ttlMs?: number;
|
|
46
50
|
}
|
|
@@ -50,8 +54,8 @@ export declare class PolicyResolver implements PolicyResolverLike {
|
|
|
50
54
|
private readonly ttlMs;
|
|
51
55
|
private readonly cache;
|
|
52
56
|
constructor(opts: PolicyResolverOptions);
|
|
53
|
-
resolve(agentId: string, roomId: string | null): Promise<
|
|
57
|
+
resolve(agentId: string, roomId: string | null): Promise<DaemonAttentionPolicy>;
|
|
54
58
|
private safeFetch;
|
|
55
59
|
invalidate(agentId: string, roomId?: string): void;
|
|
56
|
-
put(agentId: string, roomId: string | null, policy:
|
|
60
|
+
put(agentId: string, roomId: string | null, policy: DaemonAttentionPolicy): void;
|
|
57
61
|
}
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -171,7 +171,7 @@ export interface ChannelStatusSnapshot {
|
|
|
171
171
|
lastStopAt?: number;
|
|
172
172
|
lastError?: string | null;
|
|
173
173
|
/** Third-party provider id when this channel is not the built-in BotCord. */
|
|
174
|
-
provider?: "wechat" | "telegram";
|
|
174
|
+
provider?: "wechat" | "telegram" | "feishu";
|
|
175
175
|
/** Last time the adapter polled the upstream provider (ms epoch). */
|
|
176
176
|
lastPollAt?: number;
|
|
177
177
|
/** Last time the adapter accepted an inbound message (ms epoch). */
|
|
@@ -13,9 +13,10 @@ import type { Gateway } from "./gateway/index.js";
|
|
|
13
13
|
import { type DaemonConfig } from "./config.js";
|
|
14
14
|
import { LoginSessionStore } from "./gateway/channels/login-session.js";
|
|
15
15
|
import { getBotQrcode, getQrcodeStatus } from "./gateway/channels/wechat-login.js";
|
|
16
|
+
import { pollFeishuRegistration, startFeishuRegistration } from "./gateway/channels/feishu-registration.js";
|
|
16
17
|
import type { FetchLike } from "./gateway/channels/http-types.js";
|
|
17
18
|
type AckBody = Omit<ControlAck, "id">;
|
|
18
|
-
type GatewayProvider = "telegram" | "wechat";
|
|
19
|
+
type GatewayProvider = "telegram" | "wechat" | "feishu";
|
|
19
20
|
interface UpsertGatewayParams {
|
|
20
21
|
id: string;
|
|
21
22
|
type: GatewayProvider;
|
|
@@ -28,6 +29,7 @@ interface UpsertGatewayParams {
|
|
|
28
29
|
};
|
|
29
30
|
settings?: {
|
|
30
31
|
baseUrl?: string;
|
|
32
|
+
domain?: "feishu" | "lark";
|
|
31
33
|
allowedSenderIds?: string[];
|
|
32
34
|
allowedChatIds?: string[];
|
|
33
35
|
splitAt?: number;
|
|
@@ -45,6 +47,7 @@ interface GatewayLoginStartParams {
|
|
|
45
47
|
accountId: string;
|
|
46
48
|
gatewayId?: string;
|
|
47
49
|
baseUrl?: string;
|
|
50
|
+
domain?: "feishu" | "lark";
|
|
48
51
|
}
|
|
49
52
|
interface GatewayLoginStatusParams {
|
|
50
53
|
provider: GatewayProvider;
|
|
@@ -75,6 +78,10 @@ export interface GatewayControlContext {
|
|
|
75
78
|
getBotQrcode: typeof getBotQrcode;
|
|
76
79
|
getQrcodeStatus: typeof getQrcodeStatus;
|
|
77
80
|
};
|
|
81
|
+
feishuLoginClient?: {
|
|
82
|
+
startFeishuRegistration: typeof startFeishuRegistration;
|
|
83
|
+
pollFeishuRegistration: typeof pollFeishuRegistration;
|
|
84
|
+
};
|
|
78
85
|
/** Override the global fetch — used by `test_gateway` for Telegram getMe. */
|
|
79
86
|
fetchImpl?: FetchLike;
|
|
80
87
|
}
|