@reserendipity/dsh-superagent 0.0.0-stage → 0.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +50 -0
  3. package/README.md +212 -2
  4. package/cordis.patch.yml +214 -0
  5. package/examples/profile.cordis.patch.yml +31 -0
  6. package/lib/client.js +7695 -0
  7. package/lib/feishu-Dvj-1eJf.js +1325 -0
  8. package/lib/feishu-watch.d.ts +125 -0
  9. package/lib/feishu-watch.js +2 -0
  10. package/lib/github-pull-DRiuuSs8.js +597 -0
  11. package/lib/github-watch.d.ts +2 -0
  12. package/lib/github-watch.js +836 -0
  13. package/lib/host-BIWKSB5j.js +124 -0
  14. package/lib/inbox-watch.d.ts +43 -0
  15. package/lib/inbox-watch.js +343 -0
  16. package/lib/index.d.ts +369 -0
  17. package/lib/index.js +13654 -0
  18. package/lib/instance-status-Cqaju-dw.js +87 -0
  19. package/lib/instances-view.d.ts +164 -0
  20. package/lib/instances-view.js +258 -0
  21. package/lib/intake-TNIkqilh.d.ts +735 -0
  22. package/lib/kind--8woUEex.d.ts +1019 -0
  23. package/lib/poll-Bm5EYA2c.js +819 -0
  24. package/lib/schemas-MhNERx8d.js +4484 -0
  25. package/lib/schemas-lx-F2nnt.d.ts +2134 -0
  26. package/locale/en.json +6 -0
  27. package/locale/zh.json +6 -0
  28. package/package.json +159 -4
  29. package/prompts/admission.md +68 -0
  30. package/prompts/cli.md +17 -0
  31. package/prompts/dispatch.md +59 -0
  32. package/prompts/handoff-orchestrator.md +4 -0
  33. package/prompts/handoff-worker.md +7 -0
  34. package/prompts/handoff.md +6 -0
  35. package/prompts/orchestrator.md +16 -0
  36. package/prompts/presets/brief/communication.md +4 -0
  37. package/prompts/presets/brief/delivery.md +3 -0
  38. package/prompts/presets/brief/scope.md +4 -0
  39. package/prompts/presets/evidence/communication.md +6 -0
  40. package/prompts/presets/evidence/delivery.md +4 -0
  41. package/prompts/presets/evidence/scope.md +5 -0
  42. package/prompts/presets/result/communication.md +4 -0
  43. package/prompts/presets/result/delivery.md +3 -0
  44. package/prompts/presets/result/scope.md +5 -0
  45. package/prompts/replies.md +35 -0
  46. package/prompts/shared.md +9 -0
  47. package/prompts/subscriptions.md +13 -0
  48. package/prompts/team/communication.md +101 -0
  49. package/prompts/team/delivery.md +35 -0
  50. package/prompts/team/scope.md +26 -0
  51. package/prompts/work.md +49 -0
  52. package/prompts/worker.md +10 -0
  53. package/schema/declarative-settings.example.json +26 -0
  54. package/schema/declarative-settings.schema.json +348 -0
  55. package/skills/README.md +14 -0
  56. package/skills/dsh-superagent-feishu/SKILL.md +12 -0
  57. package/skills/dsh-superagent-github/SKILL.md +14 -0
@@ -0,0 +1,1325 @@
1
+ import { c as key, n as sourceContext, r as checkedImText, t as health, v as threadKey } from "./host-BIWKSB5j.js";
2
+ import { E as CliError, O as validItems, a as polled, c as FeishuBotSchema, i as pollThreads$1, n as cursorAt, o as KV } from "./poll-Bm5EYA2c.js";
3
+ import { c as object, i as boolean, m as url, n as _enum, o as looseObject, p as unknown, r as array, s as number, u as string } from "./schemas-MhNERx8d.js";
4
+ import { dirname, extname, join } from "node:path";
5
+ import { mkdtemp, readFile, realpath, rm, stat } from "node:fs/promises";
6
+ import { spawn } from "node:child_process";
7
+ import { createInterface } from "node:readline";
8
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
+ //#region src/sources/feishu-image.ts
10
+ /** Downloads Feishu message images and commits them as model-visible attachments during assessment. */
11
+ /** Extracts Feishu image keys from message text so keys remain paired with the originating message; arbitrary remote IDs are not accepted. */
12
+ function admissionImageKeys(text) {
13
+ return [...new Set(text.match(/\bimg_[a-zA-Z0-9_-]+\b/g) ?? [])];
14
+ }
15
+ async function readAdmissionImage(agent, commands, ref, text, imageKey, stateDir, signal) {
16
+ if (!admissionImageKeys(text).includes(imageKey)) throw new Error("Choose an image key from this source message");
17
+ const messageId = string().regex(/^om_[a-zA-Z0-9_-]+$/).parse(ref.messageId);
18
+ const attachments = agent.ctx.get("attachments");
19
+ if (!attachments) throw new Error("Image reading is unavailable: no attachment service is mounted");
20
+ const route = agent.session.requestHeader()?.config;
21
+ const provider = route?.provider ?? agent.options.provider;
22
+ const model = route?.model ?? agent.options.model;
23
+ const llm = agent.ctx.get("llm");
24
+ if (!llm || !provider || !model) throw new Error("Image reading is unavailable: the current model route is unresolved");
25
+ if (!(await llm.resolveModelInfo(provider, model, signal)).inputModalities?.includes("image")) throw new Error(`Model ${model} does not support image input; the image has not been inspected`);
26
+ signal.throwIfAborted();
27
+ const directory = await mkdtemp(join(stateDir, "admission-image-"));
28
+ try {
29
+ const result = object({ saved_path: string() }).parse(await commands.withSignal(signal).lark([
30
+ "im",
31
+ "+messages-resources-download",
32
+ "--message-id",
33
+ messageId,
34
+ "--file-key",
35
+ imageKey,
36
+ "--type",
37
+ "image",
38
+ "--output",
39
+ "./image"
40
+ ], directory));
41
+ const path = await realpath(result.saved_path);
42
+ if (dirname(path) !== await realpath(directory)) throw new Error("Image download returned a path outside its temporary directory");
43
+ const mediaType = {
44
+ ".png": "image/png",
45
+ ".jpg": "image/jpeg",
46
+ ".jpeg": "image/jpeg",
47
+ ".webp": "image/webp",
48
+ ".gif": "image/gif"
49
+ }[extname(path).toLowerCase()];
50
+ if (!mediaType) throw new Error("The downloaded resource is not a supported PNG/JPEG/WebP/GIF image");
51
+ const file = await stat(path);
52
+ if (!file.isFile() || file.size > Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes)) throw new Error("The image exceeds the deployment byte limit or is not a regular file");
53
+ const data = await readFile(path, { signal });
54
+ signal.throwIfAborted();
55
+ const image = await attachments.saveImage({
56
+ data,
57
+ mediaType,
58
+ name: `${imageKey}${extname(path)}`
59
+ });
60
+ signal.throwIfAborted();
61
+ return {
62
+ source_message_id: messageId,
63
+ image_key: imageKey,
64
+ image
65
+ };
66
+ } finally {
67
+ await rm(directory, {
68
+ recursive: true,
69
+ force: true
70
+ });
71
+ }
72
+ }
73
+ //#endregion
74
+ //#region src/sources/feishu-trigger.ts
75
+ /** Normalizes Feishu live events, chat history items, and topic replies into domain Triggers. */
76
+ const SOURCE = "feishu";
77
+ /** Constructs a Feishu reply channel address; records `direct` explicitly because both group and direct chat IDs start with `oc_`. */
78
+ const feishuChannel = (chatId, messageId, direct = false) => ({
79
+ source: SOURCE,
80
+ thread: `feishu:${chatId}`,
81
+ address: {
82
+ chatId,
83
+ messageId,
84
+ ...direct ? { direct: "true" } : {}
85
+ }
86
+ });
87
+ /** A message as the live event consumer prints it; the history and topic-page paths normalize what they read into this shape. */
88
+ const EventSchema = object({
89
+ message_id: string(),
90
+ chat_id: string(),
91
+ chat_type: _enum(["p2p", "group"]),
92
+ sender_id: string(),
93
+ sender_type: _enum(["user", "bot"]),
94
+ content: string(),
95
+ create_time: string(),
96
+ mentions: array(object({
97
+ id: string(),
98
+ key: string().optional(),
99
+ name: string().optional()
100
+ })).default([]),
101
+ root_id: string().optional(),
102
+ parent_id: string().optional(),
103
+ thread_id: string().optional(),
104
+ message_app_link: url().optional()
105
+ });
106
+ const feishuThreadId = (chatId, rootId, messageId) => `${chatId}:${rootId ?? messageId}`;
107
+ /** Deterministic trigger ID for a Feishu message; every read path for this message produces the same ID. */
108
+ const feishuTriggerId = (messageId) => key("feishu", messageId);
109
+ const withMentionNames = (text, mentions) => mentions.reduce((result, mention) => mention.key && mention.name ? result.replaceAll(mention.key, `@${mention.name}`) : result, text);
110
+ /**
111
+ * Parses a Feishu message timestamp into an ISO-8601 string: live events and the history API return epoch milliseconds as a
112
+ * string, whereas topic/thread pages return a local timestamp string such as `2026-09-11 12:00`.
113
+ */
114
+ function feishuTime(value) {
115
+ const time = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
116
+ if (!Number.isFinite(time)) throw new Error(`Unreadable Feishu message time: ${value}`);
117
+ return new Date(time).toISOString();
118
+ }
119
+ /** Converts a normalized Feishu event into a Trigger, returning undefined for messages sent by the bot itself. */
120
+ function feishuTrigger(input, botId, botAppId) {
121
+ const event = EventSchema.parse(input);
122
+ if (event.sender_id === botId || event.sender_id === botAppId) return;
123
+ const direct = event.chat_type === "p2p";
124
+ const mentioned = event.mentions.some((mention) => mention.id === botId || mention.id === botAppId);
125
+ const thread = feishuThreadId(event.chat_id, event.root_id, event.message_id);
126
+ return {
127
+ id: feishuTriggerId(event.message_id),
128
+ source: SOURCE,
129
+ threads: [{
130
+ source: SOURCE,
131
+ id: thread
132
+ }],
133
+ actor: {
134
+ source: SOURCE,
135
+ id: event.sender_id
136
+ },
137
+ human: event.sender_type === "user",
138
+ ...direct ? { addressed: "direct" } : mentioned ? { addressed: "mention" } : {},
139
+ at: feishuTime(event.create_time),
140
+ text: withMentionNames(event.content, event.mentions),
141
+ channel: feishuChannel(event.chat_id, event.message_id, direct),
142
+ ...event.message_app_link ? { url: event.message_app_link } : {},
143
+ meta: {
144
+ chatId: event.chat_id,
145
+ messageId: event.message_id,
146
+ direct,
147
+ mentioned,
148
+ ...event.root_id ? { rootId: event.root_id } : {},
149
+ ...event.parent_id ? { parentId: event.parent_id } : {},
150
+ ...event.thread_id ? { threadId: event.thread_id } : {}
151
+ }
152
+ };
153
+ }
154
+ function feishuRef(trigger) {
155
+ const meta = trigger.meta;
156
+ if (trigger.source !== "feishu" || !meta.chatId || !meta.messageId) return void 0;
157
+ return {
158
+ triggerId: trigger.id,
159
+ chatId: meta.chatId,
160
+ messageId: meta.messageId,
161
+ at: trigger.at
162
+ };
163
+ }
164
+ /** A message from the history API; a deleted message may have no body/content. */
165
+ const HistoricalMessageSchema = looseObject({
166
+ message_id: string(),
167
+ chat_id: string(),
168
+ create_time: string(),
169
+ root_id: string().optional(),
170
+ parent_id: string().optional(),
171
+ thread_id: string().optional(),
172
+ deleted: boolean().optional(),
173
+ message_app_link: string().optional(),
174
+ sender: looseObject({
175
+ id: string().default(""),
176
+ sender_type: string()
177
+ }),
178
+ body: looseObject({ content: string() }).optional(),
179
+ mentions: array(looseObject({
180
+ id: string(),
181
+ key: string().optional(),
182
+ name: string().optional()
183
+ })).optional()
184
+ });
185
+ /** One page of the history API; items are validated individually so an unreadable item is skipped alone. */
186
+ const HistoryPageSchema = looseObject({
187
+ items: array(unknown()).default([]),
188
+ has_more: boolean().default(false),
189
+ page_token: string().optional()
190
+ });
191
+ function historyText(message) {
192
+ let content;
193
+ try {
194
+ content = JSON.parse(message.body?.content ?? "");
195
+ } catch {
196
+ content = message.body?.content ?? "";
197
+ }
198
+ const plain = content && typeof content === "object" && "text" in content && typeof content.text === "string" ? content.text : void 0;
199
+ return typeof content === "string" ? content : plain ?? JSON.stringify(content);
200
+ }
201
+ function historyEvent(message, chatId, direct) {
202
+ return {
203
+ message_id: message.message_id,
204
+ chat_id: chatId,
205
+ chat_type: direct ? "p2p" : "group",
206
+ create_time: message.create_time,
207
+ sender_id: message.sender.id,
208
+ sender_type: message.sender.sender_type === "user" ? "user" : "bot",
209
+ content: historyText(message),
210
+ mentions: message.mentions ?? [],
211
+ root_id: message.root_id,
212
+ parent_id: message.parent_id,
213
+ thread_id: message.thread_id,
214
+ message_app_link: message.message_app_link
215
+ };
216
+ }
217
+ /**
218
+ * Adapts a topic-page message into the live-event schema, falling back to `root` when Feishu omits `root_id` on replies;
219
+ * an app's message carries its bot's `open_bot_id`, and a deleted message may have no content.
220
+ */
221
+ function topicEvent(message, chatId, direct, root) {
222
+ return {
223
+ message_id: message.message_id,
224
+ chat_id: chatId,
225
+ chat_type: direct ? "p2p" : "group",
226
+ create_time: message.create_time,
227
+ sender_id: message.sender.open_bot_id ?? message.sender.id,
228
+ sender_type: message.sender.sender_type === "user" ? "user" : "bot",
229
+ content: message.content ?? "",
230
+ mentions: message.mentions ?? [],
231
+ parent_id: message.parent_id,
232
+ thread_id: message.thread_id,
233
+ root_id: message.root_id ?? (message.message_id === root ? void 0 : root),
234
+ message_app_link: message.message_app_link
235
+ };
236
+ }
237
+ //#endregion
238
+ //#region src/sources/feishu-read.ts
239
+ /** Trigger-scoped Feishu context reader; retrieved messages are returned for model inspection without entering intake. */
240
+ const messageId = string().regex(/^om_[a-zA-Z0-9_-]+$/);
241
+ const sourceSchema = object({
242
+ message_id: messageId,
243
+ chat_id: string(),
244
+ root_id: messageId.optional(),
245
+ parent_id: messageId.optional(),
246
+ thread_id: string().regex(/^omt?_[a-zA-Z0-9_-]+$/).optional(),
247
+ deleted: boolean().optional()
248
+ });
249
+ const messageSchema = object({
250
+ message_id: messageId,
251
+ chat_id: string(),
252
+ content: string().optional(),
253
+ msg_type: string(),
254
+ sender: object({
255
+ id: string(),
256
+ name: string().optional(),
257
+ open_bot_id: string().optional(),
258
+ sender_type: string()
259
+ }),
260
+ create_time: string(),
261
+ update_time: string().optional(),
262
+ updated: boolean().optional(),
263
+ deleted: boolean().optional(),
264
+ mentions: array(object({
265
+ id: string(),
266
+ name: string().optional(),
267
+ key: string().optional()
268
+ })).optional(),
269
+ message_app_link: string().optional(),
270
+ thread_id: string().optional(),
271
+ root_id: string().optional(),
272
+ parent_id: string().optional()
273
+ });
274
+ /** A topic page; messages are validated individually so an unreadable item is skipped alone and counted. */
275
+ const threadSchema = object({
276
+ messages: array(unknown()),
277
+ has_more: boolean(),
278
+ page_token: string().optional(),
279
+ thread_id: string()
280
+ });
281
+ /** Reads the CLI-provided AppLink for the source message alone, without retaining expanded thread content. */
282
+ async function readFeishuLink(commands, ref) {
283
+ const id = messageId.parse(ref.messageId);
284
+ const message = object({ messages: array(object({
285
+ message_id: messageId,
286
+ chat_id: string(),
287
+ message_app_link: url().optional(),
288
+ deleted: boolean().optional()
289
+ })) }).parse(await commands.lark([
290
+ "im",
291
+ "+messages-mget",
292
+ "--message-ids",
293
+ id,
294
+ "--no-reactions"
295
+ ])).messages.find((item) => item.message_id === id);
296
+ if (!message || message.deleted || message.chat_id !== ref.chatId || !message.message_app_link) throw new Error("Source message link unavailable");
297
+ return message.message_app_link;
298
+ }
299
+ /** Resource references are anchored to an accepted trigger instead of arbitrary chat or message IDs. */
300
+ function feishuReferences(ref) {
301
+ const read = (section) => `feishu/${ref.triggerId}/${section}`;
302
+ return {
303
+ skill: "dsh-superagent-feishu",
304
+ message: read("message"),
305
+ root: read("root"),
306
+ parent: read("parent"),
307
+ thread: read("thread"),
308
+ around: read("around")
309
+ };
310
+ }
311
+ /** Validates a model-supplied resource string before any network request; only `thread` and `around` take pagination cursors. */
312
+ function feishuResource(resource) {
313
+ const match = /^feishu\/([^/]+)\/(message|root|parent|thread|around)(?:\/([^/]+))?$/.exec(resource);
314
+ if (!match || match[3] && match[2] !== "thread" && match[2] !== "around") throw new Error("Use feishu/<trigger-id>/message, /root, /parent, /thread or /around; follow returned next_resource for another page");
315
+ return {
316
+ triggerId: match[1],
317
+ section: match[2],
318
+ cursor: match[3] ? decodeURIComponent(match[3]) : void 0
319
+ };
320
+ }
321
+ async function sourceOf(commands, ref) {
322
+ const id = messageId.parse(ref.messageId);
323
+ const params = JSON.stringify({ message_ids: [id] });
324
+ const listed = await commands.lark([
325
+ "api",
326
+ "GET",
327
+ "/open-apis/im/v1/messages/mget",
328
+ "--params",
329
+ params
330
+ ]);
331
+ const source = object({ items: array(sourceSchema) }).parse(listed).items.find((message) => message.message_id === id);
332
+ if (!source || source.deleted) throw new Error("The source Feishu message is unavailable or deleted; retained trigger text is still readable");
333
+ if (source.chat_id !== ref.chatId) throw new Error("Feishu source chat does not match the accepted trigger");
334
+ return source;
335
+ }
336
+ /** Fetches one message or a bounded topic page of a Trigger's source message with explicit pagination continuation. */
337
+ async function readFeishu(commands, ref, resource, pageSize, order = "asc") {
338
+ const request = feishuResource(resource);
339
+ if (request.triggerId !== ref.triggerId) throw new Error("A Feishu source trigger is required");
340
+ if (request.section === "around") throw new Error("The chat around a message is read with feishuRead");
341
+ const source = await sourceOf(commands, ref);
342
+ if (request.section === "thread") {
343
+ if (!source.thread_id) return {
344
+ status: "no_thread",
345
+ messages: [],
346
+ next_resource: null
347
+ };
348
+ const args = [
349
+ "im",
350
+ "+threads-messages-list",
351
+ "--thread",
352
+ source.thread_id,
353
+ "--order",
354
+ order,
355
+ "--page-size",
356
+ String(pageSize),
357
+ "--no-reactions"
358
+ ];
359
+ if (request.cursor) args.push("--page-token", request.cursor);
360
+ const result = threadSchema.parse(await commands.lark(args));
361
+ const tally = { skipped: 0 };
362
+ const messages = validItems(messageSchema, result.messages, tally);
363
+ const outside = messages.some((message) => message.chat_id !== source.chat_id || message.thread_id && message.thread_id !== source.thread_id);
364
+ if (result.thread_id !== source.thread_id || outside) throw new Error("Feishu returned messages outside the source thread");
365
+ if (result.has_more && (!result.page_token || result.page_token === request.cursor)) throw new Error("Feishu thread pagination did not advance; history is incomplete");
366
+ return {
367
+ status: "available",
368
+ thread_id: result.thread_id,
369
+ messages,
370
+ ...tally.skipped ? { skipped: tally.skipped } : {},
371
+ next_resource: result.has_more ? `feishu/${ref.triggerId}/thread/${encodeURIComponent(result.page_token)}` : null
372
+ };
373
+ }
374
+ const target = request.section === "root" ? source.root_id : request.section === "parent" ? source.parent_id : source.message_id;
375
+ if (!target) return {
376
+ status: `no_${request.section}`,
377
+ message: null
378
+ };
379
+ const message = object({ messages: array(messageSchema) }).parse(await commands.lark([
380
+ "im",
381
+ "+messages-mget",
382
+ "--message-ids",
383
+ target,
384
+ "--no-reactions"
385
+ ])).messages.find((message) => message.message_id === target);
386
+ if (!message) throw new Error("The requested Feishu message is unavailable; do not infer its contents");
387
+ if (message.chat_id !== source.chat_id) throw new Error("Feishu returned a message outside the source chat");
388
+ return {
389
+ status: message.deleted ? "deleted" : "available",
390
+ message
391
+ };
392
+ }
393
+ function isoTime(value) {
394
+ try {
395
+ return feishuTime(value);
396
+ } catch {
397
+ return value;
398
+ }
399
+ }
400
+ function feishuView(message) {
401
+ return {
402
+ id: message.message_id,
403
+ from: message.sender.name || message.sender.id,
404
+ at: isoTime(message.create_time),
405
+ text: message.deleted ? "" : withMentionNames(message.content ?? "", message.mentions ?? []),
406
+ ...message.sender.sender_type !== "user" ? { bot: true } : {},
407
+ ...message.deleted ? { deleted: true } : {},
408
+ ...message.parent_id ? { reply_to: message.parent_id } : {},
409
+ ...message.message_app_link ? { url: message.message_app_link } : {}
410
+ };
411
+ }
412
+ /** Formats a chat history API message for model inspection; the history API identifies senders by `open_id` only. */
413
+ function historyView(message) {
414
+ return {
415
+ id: message.message_id,
416
+ from: message.sender.id,
417
+ at: isoTime(message.create_time),
418
+ text: message.deleted ? "" : withMentionNames(historyText(message), message.mentions ?? []),
419
+ ...message.sender.sender_type !== "user" ? { bot: true } : {},
420
+ ...message.deleted ? { deleted: true } : {},
421
+ ...message.parent_id ? { reply_to: message.parent_id } : {},
422
+ ...message.message_app_link ? { url: message.message_app_link } : {}
423
+ };
424
+ }
425
+ /** Reads chat messages surrounding `ref.at` (five before and five after on initial read, then pages of ten older messages). */
426
+ async function chatAround(commands, ref, cursor, before = 5, after = 5) {
427
+ const time = Date.parse(ref.at ?? "");
428
+ if (!Number.isFinite(time)) throw new Error("The source Feishu message has no time to read the chat around");
429
+ const list = async (parameters) => {
430
+ const params = JSON.stringify({
431
+ container_id_type: "chat",
432
+ container_id: ref.chatId,
433
+ ...parameters
434
+ });
435
+ const page = HistoryPageSchema.parse(await commands.lark([
436
+ "api",
437
+ "GET",
438
+ "/open-apis/im/v1/messages",
439
+ "--params",
440
+ params
441
+ ]));
442
+ return {
443
+ ...page,
444
+ items: validItems(HistoricalMessageSchema, page.items).filter((item) => item.chat_id === ref.chatId)
445
+ };
446
+ };
447
+ const earlier = await list({
448
+ end_time: String(Math.ceil(time / 1e3)),
449
+ sort_type: "ByCreateTimeDesc",
450
+ page_size: cursor ? 10 : before + 1,
451
+ ...cursor ? { page_token: cursor } : {}
452
+ });
453
+ const later = cursor || !after ? [] : (await list({
454
+ start_time: String(Math.floor(time / 1e3)),
455
+ sort_type: "ByCreateTimeAsc",
456
+ page_size: after + 1
457
+ })).items;
458
+ const seen = /* @__PURE__ */ new Set();
459
+ return {
460
+ status: "available",
461
+ messages: [...earlier.items.reverse(), ...later].filter((item) => !seen.has(item.message_id) && seen.add(item.message_id)).map(historyView),
462
+ next_resource: earlier.has_more && earlier.page_token && earlier.page_token !== cursor ? `feishu/${ref.triggerId}/around/${encodeURIComponent(earlier.page_token)}` : null
463
+ };
464
+ }
465
+ async function feishuRead(commands, ref, resource, pageSize) {
466
+ const request = feishuResource(resource);
467
+ if (request.section === "around") {
468
+ if (request.triggerId !== ref.triggerId) throw new Error("A Feishu source trigger is required");
469
+ await sourceOf(commands, ref);
470
+ return chatAround(commands, ref, request.cursor);
471
+ }
472
+ const result = await readFeishu(commands, ref, resource, pageSize);
473
+ if ("messages" in result) return {
474
+ ...result,
475
+ messages: result.messages.map(feishuView)
476
+ };
477
+ return {
478
+ status: result.status,
479
+ message: result.message ? feishuView(result.message) : null
480
+ };
481
+ }
482
+ function sourceRef(trigger) {
483
+ const ref = feishuRef(trigger);
484
+ if (!ref) throw new Error("A Feishu source trigger is required");
485
+ return ref;
486
+ }
487
+ const feishuThreadContext = {
488
+ messageId: (trigger) => String(trigger.meta.messageId ?? ""),
489
+ read: async (commands, trigger, part, cursor, config) => {
490
+ const ref = sourceRef(trigger);
491
+ const body = await feishuRead(commands, ref, `feishu/${trigger.id}/${part}${cursor ? "/" + encodeURIComponent(cursor) : ""}`, config.readPageSize);
492
+ if (part === "thread" && body.status === "no_thread") {
493
+ const around = await chatAround(commands, ref, void 0, 5, 0);
494
+ return {
495
+ messages: around.messages,
496
+ body: {
497
+ status: "chat_neighbours",
498
+ messages: around.messages
499
+ },
500
+ status: "chat_neighbours"
501
+ };
502
+ }
503
+ return {
504
+ messages: "messages" in body ? body.messages : body.message ? [body.message] : [],
505
+ next: "next_resource" in body && body.next_resource ? decodeURIComponent(body.next_resource.split("/").at(-1)) : void 0,
506
+ body: { ...body },
507
+ status: body.status
508
+ };
509
+ },
510
+ images: admissionImageKeys,
511
+ image: (agent, commands, trigger, message, key, stateDir, signal) => readAdmissionImage(agent, commands, {
512
+ ...sourceRef(trigger),
513
+ messageId: message.id
514
+ }, message.text, key, stateDir, signal)
515
+ };
516
+ //#endregion
517
+ //#region src/sources/feishu-channel.ts
518
+ /** Feishu or Lark AppLink that opens a chat; used when the message's own link was not delivered with the event. */
519
+ const feishuChatLink = (brand, chatId) => `https://applink.${brand === "lark" ? "larksuite.com" : "feishu.cn"}/client/chat/open?openChatId=${encodeURIComponent(chatId)}`;
520
+ /**
521
+ * True when Feishu permanently rejects a reply because the target message was recalled, withdrawn, or not found.
522
+ * Only a Feishu response carrying one of these error codes means the target message is gone; timeouts or crashes do not.
523
+ */
524
+ const gone = (error) => error instanceof CliError && error.code !== void 0 && (error.code === 230011 || /withdrawn/i.test(error.message) || Math.floor(error.code / 100) === 2300 && /not found/i.test(error.message));
525
+ /** Replies in the topic of the target message; Feishu keeps at most one reply per idempotency key (the outgoing message's `message.id`). */
526
+ async function sendFeishu(host, message) {
527
+ const messageId = message.channel.address.messageId;
528
+ if (!messageId) throw new Error("Missing Feishu reply target");
529
+ const reply = [
530
+ "im",
531
+ "+messages-reply",
532
+ "--message-id",
533
+ messageId,
534
+ "--markdown",
535
+ checkedImText(message.text),
536
+ "--reply-in-thread",
537
+ "--idempotency-key",
538
+ message.id
539
+ ];
540
+ try {
541
+ await host.commands.lark(reply);
542
+ } catch (error) {
543
+ if (gone(error)) return "gone";
544
+ throw error;
545
+ }
546
+ return "sent";
547
+ }
548
+ /** Fallback chat link on the bot's brand; the message's own deep link on its Trigger is preferred when available. */
549
+ const feishuLink = (address, links) => ({
550
+ url: feishuChatLink(links.feishu?.brand ?? "feishu", address.chatId ?? ""),
551
+ label: {
552
+ zh: "飞书消息",
553
+ en: "Feishu message"
554
+ },
555
+ ...address.messageId ? { triggerId: feishuTriggerId(address.messageId) } : {}
556
+ });
557
+ //#endregion
558
+ //#region src/sources/feishu-loops.ts
559
+ /** Feishu background loops: live event stream consumer, chat/topic history backfill, chat name refresh, and subscribed thread polling. */
560
+ const runFeishu = (ctx, signal) => Promise.allSettled([
561
+ watch,
562
+ backfillLoop,
563
+ chatNamesLoop
564
+ ].map((loop) => loop(ctx, signal)));
565
+ const enabled = (ctx) => ctx.config(SOURCE);
566
+ const bot = (ctx) => {
567
+ const resolved = ctx.deployment.feishu;
568
+ if (!resolved) throw new Error("The Feishu Source has no working bot CLI");
569
+ return resolved;
570
+ };
571
+ async function receive(ctx, trigger) {
572
+ await ctx.receive(trigger);
573
+ const chatId = String(trigger.meta.chatId ?? "");
574
+ if (!chatId) return;
575
+ const name = ctx.state.get(KV.sources.labels("feishu"))?.[chatId] ?? chatId;
576
+ const chat = trigger.meta.direct === true ? { value: {
577
+ zh: "私聊",
578
+ en: "Direct message"
579
+ } } : { value: name };
580
+ for (const ref of trigger.threads) ctx.threads.describe(threadKey(ref), {
581
+ url: chatLink(ctx, chatId),
582
+ facts: { chat }
583
+ });
584
+ }
585
+ const chatLink = (ctx, chatId) => feishuChatLink(ctx.deployment.feishu?.brand ?? "feishu", chatId);
586
+ function available(ctx) {
587
+ if (ctx.configured() && ctx.deployment.feishu) return true;
588
+ const waiting = health("paused", ctx.configured() ? "feishuCliUnset" : "setupPending");
589
+ ctx.report("feishu", ctx.problem ? {
590
+ state: "paused",
591
+ detail: ctx.problem.detail
592
+ } : waiting);
593
+ return false;
594
+ }
595
+ /** Runs the live event consumer only while the Source is enabled; disabling it stops the consumer within five seconds. */
596
+ async function watch(ctx, signal) {
597
+ await ctx.ready;
598
+ if (!available(ctx)) return;
599
+ let retry;
600
+ while (!signal.aborted) {
601
+ if (!enabled(ctx)) {
602
+ ctx.report("feishu", health("paused", "sourceOff"));
603
+ await setTimeout$1(5e3, void 0, { signal }).catch(() => {});
604
+ continue;
605
+ }
606
+ const running = new AbortController();
607
+ const check = setInterval(() => {
608
+ if (!enabled(ctx)) running.abort();
609
+ }, 5e3);
610
+ const stop = () => running.abort();
611
+ signal.addEventListener("abort", stop, { once: true });
612
+ const began = Date.now();
613
+ try {
614
+ await consume(ctx, running.signal);
615
+ } catch (error) {
616
+ if (!running.signal.aborted) ctx.report("feishu", {
617
+ state: "error",
618
+ detail: String(error)
619
+ });
620
+ } finally {
621
+ clearInterval(check);
622
+ signal.removeEventListener("abort", stop);
623
+ }
624
+ if (running.signal.aborted) continue;
625
+ retry = consumerRetry(retry, Date.now() - began);
626
+ await setTimeout$1(retry, void 0, { signal }).catch(() => {});
627
+ }
628
+ }
629
+ /** Wait before restarting the event consumer: 1s after a run that stayed up for a minute, otherwise exponential backoff up to 60s. */
630
+ const consumerRetry = (previous, ranMs) => previous === void 0 || ranMs >= 6e4 ? 1e3 : Math.min(previous * 2, 6e4);
631
+ async function consume(ctx, signal) {
632
+ const child = spawn(bot(ctx).cli, [
633
+ "event",
634
+ "consume",
635
+ "im.message.receive_v1",
636
+ "--as",
637
+ "bot"
638
+ ], {
639
+ stdio: [
640
+ "pipe",
641
+ "pipe",
642
+ "pipe"
643
+ ],
644
+ env: {
645
+ ...process.env,
646
+ LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1",
647
+ LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1"
648
+ }
649
+ });
650
+ const stdout = createInterface({ input: child.stdout });
651
+ const stderr = createInterface({ input: child.stderr });
652
+ let chain = Promise.resolve();
653
+ let diagnostic = "";
654
+ let ready = false;
655
+ const stop = () => {
656
+ child.stdin.end();
657
+ child.kill("SIGTERM");
658
+ };
659
+ signal.addEventListener("abort", stop, { once: true });
660
+ const timeout = setTimeout(() => {
661
+ diagnostic = "Feishu event consumer did not become ready within 45 seconds";
662
+ stop();
663
+ }, 45e3);
664
+ stderr.on("line", (line) => {
665
+ diagnostic = line.slice(0, 4e3);
666
+ if (line.startsWith("[event] ready")) {
667
+ ready = true;
668
+ clearTimeout(timeout);
669
+ ctx.report("feishu", {
670
+ state: "ready",
671
+ detail: "Listening for bot events",
672
+ at: (/* @__PURE__ */ new Date()).toISOString()
673
+ });
674
+ } else if (/WARN|drop|error/.test(line)) ctx.report("feishu", {
675
+ state: "error",
676
+ detail: diagnostic
677
+ });
678
+ });
679
+ stdout.on("line", (line) => {
680
+ chain = chain.then(async () => {
681
+ if (line.length > 1048576) throw new Error("Feishu event exceeds 1 MiB");
682
+ const trigger = feishuTrigger(JSON.parse(line), bot(ctx).botId, bot(ctx).appId);
683
+ if (!trigger || !enabled(ctx)) return;
684
+ const chats = ctx.state.get(KV.feishu.chats) ?? {};
685
+ chats[String(trigger.meta.chatId)] = trigger.meta.direct === true;
686
+ ctx.state.set(KV.feishu.chats, chats);
687
+ await receive(ctx, trigger);
688
+ }).catch((error) => {
689
+ ctx.report("feishu", {
690
+ state: "error",
691
+ detail: String(error)
692
+ });
693
+ });
694
+ });
695
+ try {
696
+ await new Promise((resolve, reject) => {
697
+ child.once("error", reject);
698
+ const exited = (code) => /* @__PURE__ */ new Error(`Feishu event consumer exited (${code}, ready=${ready}): ${diagnostic}`);
699
+ child.once("close", (code) => signal.aborted ? resolve() : reject(exited(code)));
700
+ });
701
+ } finally {
702
+ clearTimeout(timeout);
703
+ signal.removeEventListener("abort", stop);
704
+ stdout.close();
705
+ stderr.close();
706
+ await chain;
707
+ }
708
+ }
709
+ /** Refreshes the map of joined chats (`chat_id -> name`); throws on failure so the previously stored list remains intact. */
710
+ async function refreshChatNames(ctx, signal) {
711
+ const commands = ctx.commands.withSignal(signal);
712
+ const result = looseObject({ chats: array(looseObject({
713
+ chat_id: string(),
714
+ name: string().nullish()
715
+ })).optional() }).parse(await commands.lark([
716
+ "im",
717
+ "+chat-list",
718
+ "--page-all",
719
+ "--page-size",
720
+ "100"
721
+ ]));
722
+ const names = Object.fromEntries((result.chats ?? []).filter((chat) => chat.name).map((chat) => [chat.chat_id, chat.name]));
723
+ ctx.state.set(KV.sources.labels(SOURCE), names);
724
+ return names;
725
+ }
726
+ /** Rereads the chat list every minute independently of history backfill (whose rounds can take much longer), so newly joined chats appear within a minute. */
727
+ async function chatNamesLoop(ctx, signal) {
728
+ await ctx.ready;
729
+ if (!available(ctx)) return;
730
+ while (!signal.aborted) {
731
+ if (enabled(ctx)) try {
732
+ await refreshChatNames(ctx, signal);
733
+ } catch {}
734
+ await setTimeout$1(6e4, void 0, { signal }).catch(() => {});
735
+ }
736
+ }
737
+ async function backfillLoop(ctx, signal) {
738
+ await ctx.ready;
739
+ if (!available(ctx)) {
740
+ ctx.report("history", health("paused", "feishuBotMissing"));
741
+ return;
742
+ }
743
+ while (!signal.aborted) {
744
+ const config = enabled(ctx);
745
+ if (!config) {
746
+ ctx.report("history", health("paused", "sourceOff"));
747
+ await setTimeout$1(6e4, void 0, { signal }).catch(() => {});
748
+ continue;
749
+ }
750
+ const seen = ctx.state.get(KV.feishu.chats) ?? {};
751
+ const known = Object.entries(seen).filter(([, direct]) => direct && config.directMessages);
752
+ const chats = {
753
+ ...Object.fromEntries(config.chats.map((chat) => [chat.chatId, false])),
754
+ ...Object.fromEntries(known)
755
+ };
756
+ const errors = [];
757
+ const tally = { skipped: 0 };
758
+ for (const [chatId, direct] of Object.entries(chats)) {
759
+ if (signal.aborted) break;
760
+ const started = (/* @__PURE__ */ new Date()).toISOString();
761
+ const cursorKey = KV.feishu.history(chatId);
762
+ const since = ctx.state.get(cursorKey);
763
+ if (since === void 0) {
764
+ ctx.state.set(cursorKey, started);
765
+ continue;
766
+ }
767
+ try {
768
+ await backfillChat(ctx, chatId, direct, since, started, signal, tally);
769
+ if (!signal.aborted) ctx.state.set(cursorKey, cursorAt(started));
770
+ } catch (error) {
771
+ if (!signal.aborted) errors.push(`${chatId}: ${String(error)}`);
772
+ }
773
+ }
774
+ await pollThreads(ctx, signal, tally);
775
+ const skipped = tally.skipped ? `; ${tally.skipped} unreadable messages skipped` : "";
776
+ if (!signal.aborted) ctx.report("history", errors.length ? {
777
+ state: "error",
778
+ detail: errors.join("\n")
779
+ } : {
780
+ state: "ready",
781
+ detail: `Configured groups and known direct chats recovered${skipped}`,
782
+ at: (/* @__PURE__ */ new Date()).toISOString()
783
+ });
784
+ await setTimeout$1(6e4, void 0, { signal }).catch(() => {});
785
+ }
786
+ }
787
+ /**
788
+ * Backfills chat messages and active topic replies between `since` and `until` before the caller advances the chat cursor.
789
+ * Topics are stored as `{ id: lastSeenAt }`, and unsubscribed topics older than the polling horizon are pruned.
790
+ */
791
+ async function backfillChat(ctx, chatId, direct, since, until, signal, tally = { skipped: 0 }) {
792
+ const commands = ctx.commands.withSignal(signal);
793
+ const topicsKey = KV.feishu.topics(chatId);
794
+ const topics = { ...ctx.state.get(topicsKey) };
795
+ const scan = async (kind, id) => {
796
+ let page;
797
+ const pages = /* @__PURE__ */ new Set();
798
+ do {
799
+ if (signal.aborted) throw signal.reason;
800
+ const seconds = (time) => String(Math.floor(Date.parse(time) / 1e3));
801
+ const parameters = {
802
+ container_id_type: kind,
803
+ container_id: id,
804
+ start_time: seconds(since),
805
+ end_time: seconds(until),
806
+ sort_type: "ByCreateTimeAsc",
807
+ page_size: 50,
808
+ ...page ? { page_token: page } : {}
809
+ };
810
+ const request = [
811
+ "api",
812
+ "GET",
813
+ "/open-apis/im/v1/messages",
814
+ "--params",
815
+ JSON.stringify(parameters)
816
+ ];
817
+ const result = HistoryPageSchema.parse(await commands.lark(request));
818
+ for (const message of validItems(HistoricalMessageSchema, result.items, tally)) {
819
+ if (message.chat_id !== chatId || kind === "thread" && message.thread_id !== id) throw new Error("Feishu history returned a message outside the requested chat or thread");
820
+ if (message.deleted) continue;
821
+ const at = feishuTime(message.create_time);
822
+ if (message.thread_id && !(Date.parse(topics[message.thread_id] ?? "") >= Date.parse(at))) topics[message.thread_id] = at;
823
+ const trigger = feishuTrigger(historyEvent(message, chatId, direct), bot(ctx).botId, bot(ctx).appId);
824
+ if (trigger) await receive(ctx, trigger);
825
+ }
826
+ if (result.has_more && (!result.page_token || pages.has(result.page_token))) throw new Error("Feishu history pagination did not advance");
827
+ page = result.has_more ? result.page_token : void 0;
828
+ if (page) pages.add(page);
829
+ } while (page);
830
+ };
831
+ await scan("chat", chatId);
832
+ const followed = new Set(ctx.threads.subscribed(SOURCE).filter((thread) => polled(ctx, thread)).map((thread) => String(thread.meta.threadId ?? ctx.triggers.latest(thread.key)?.meta.threadId ?? "")));
833
+ for (const [id, seen] of Object.entries(topics)) if (!followed.has(id) && Date.parse(until) - Date.parse(seen) >= 12096e5) delete topics[id];
834
+ ctx.state.set(topicsKey, topics);
835
+ for (const id of Object.keys(topics)) await scan("thread", id);
836
+ ctx.state.set(topicsKey, topics);
837
+ }
838
+ /** Resolves the retained source message reference for polling a subscribed thread. */
839
+ function threadRef(ctx, thread) {
840
+ const meta = thread.meta;
841
+ if (meta.chatId && meta.messageId && meta.sourceTriggerId) return {
842
+ triggerId: meta.sourceTriggerId,
843
+ chatId: meta.chatId,
844
+ messageId: meta.messageId
845
+ };
846
+ const trigger = ctx.triggers.latest(thread.key);
847
+ return trigger ? feishuRef(trigger) : void 0;
848
+ }
849
+ /** Polls subscribed Feishu threads within the polling horizon so replies missed by the live stream become triggers. */
850
+ async function pollThreads(ctx, signal, tally) {
851
+ await pollThreads$1(ctx, SOURCE, () => !!signal?.aborted, (thread) => pollThread(ctx, thread, signal, tally));
852
+ }
853
+ async function pollThread(ctx, thread, signal, tally) {
854
+ const ref = threadRef(ctx, thread);
855
+ if (!ref) throw new Error("Subscribed thread has no retained source message");
856
+ const config = enabled(ctx);
857
+ if (!config) return;
858
+ const commands = ctx.commands.withSignal(signal);
859
+ const pageSize = config.readPageSize;
860
+ const checkpoint = KV.feishu.seen(thread.key);
861
+ const known = new Set(ctx.state.get(checkpoint) ?? []);
862
+ const initialized = ctx.state.get(KV.feishu.initialized(thread.key));
863
+ if (!initialized) {
864
+ for (const section of ["root", "parent"]) {
865
+ const result = await readFeishu(commands, ref, `feishu/${ref.triggerId}/${section}`, pageSize);
866
+ if ("message" in result && result.message) known.add(result.message.message_id);
867
+ }
868
+ ctx.state.set(checkpoint, [...known]);
869
+ }
870
+ const direct = ctx.state.get(KV.feishu.chats)?.[ref.chatId] === true;
871
+ const since = Date.parse(thread.createdAt);
872
+ const root = thread.ref.id.slice(thread.ref.id.indexOf(":") + 1);
873
+ let resource = `feishu/${ref.triggerId}/thread`;
874
+ while (resource) {
875
+ if (signal?.aborted) return;
876
+ const result = await readFeishu(commands, ref, resource, pageSize, "desc");
877
+ if (!("messages" in result)) throw new Error("Expected Feishu thread messages");
878
+ if (tally && "skipped" in result) tally.skipped += result.skipped ?? 0;
879
+ if ("thread_id" in result && result.thread_id && thread.meta.threadId !== result.thread_id) ctx.threads.patch(thread.key, { threadId: result.thread_id });
880
+ const encounteredKnown = result.messages.some((message) => known.has(message.message_id));
881
+ const { botId, appId } = bot(ctx);
882
+ for (const message of result.messages.filter((item) => !known.has(item.message_id) && !item.deleted).reverse()) {
883
+ if (signal?.aborted) return;
884
+ const trigger = feishuTrigger(topicEvent(message, ref.chatId, direct, root), botId, appId);
885
+ if (!trigger || !initialized && Date.parse(trigger.at) < since) continue;
886
+ await receive(ctx, trigger);
887
+ }
888
+ result.messages.forEach((message) => known.add(message.message_id));
889
+ ctx.state.set(checkpoint, [...known]);
890
+ if (initialized && encounteredKnown) break;
891
+ resource = result.next_resource;
892
+ }
893
+ ctx.state.set(KV.feishu.initialized(thread.key), true);
894
+ }
895
+ //#endregion
896
+ //#region src/sources/feishu.ts
897
+ const MembersSchema = looseObject({
898
+ users: array(looseObject({
899
+ member_id: string(),
900
+ name: string()
901
+ })),
902
+ has_more: boolean(),
903
+ truncations: array(unknown()).optional()
904
+ });
905
+ const FeishuConfigSchema = FeishuBotSchema.extend({
906
+ readPageSize: number().int().min(1).max(50).default(20),
907
+ linkTimeoutMs: number().int().min(1).max(9e4).default(5e3),
908
+ chats: array(object({
909
+ chatId: string().regex(/^oc_[a-zA-Z0-9]+$/),
910
+ name: string().trim().max(60).optional(),
911
+ listen: _enum(["mentions", "all"])
912
+ }).strict()).refine((chats) => new Set(chats.map((chat) => chat.chatId)).size === chats.length, "Duplicate chat IDs").default([]),
913
+ directMessages: boolean().default(true),
914
+ otherChats: _enum(["none", "mentions"]).default("none")
915
+ }).strict();
916
+ const feishuKind = {
917
+ kind: SOURCE,
918
+ refreshLabels: (host, signal) => refreshChatNames(sourceContext(host, feishuKind), signal),
919
+ title: {
920
+ zh: "飞书",
921
+ en: "Feishu"
922
+ },
923
+ description: {
924
+ zh: "接收下列群和私聊中发给机器人的消息,补收群历史并轮询订阅的线程。",
925
+ en: "Receives messages to the bot in these chats and direct messages, recovers chat history and polls subscribed Threads."
926
+ },
927
+ schema: FeishuConfigSchema,
928
+ fields: [
929
+ {
930
+ key: "cli",
931
+ type: "text",
932
+ label: {
933
+ zh: "机器人的 lark-cli",
934
+ en: "Bot lark-cli"
935
+ },
936
+ hint: {
937
+ zh: "命令名或绝对路径,指向只允许机器人身份(strict mode bot)的 lark-cli 配置;执行器以 lark-cli 调用它。保存时读取应用和机器人,然后重启 dsh-superagent。",
938
+ en: "A command name or absolute path of a lark-cli configuration that allows only the bot identity (strict mode bot); the Executor calls it as lark-cli. Saving reads the app and the bot, then restarts dsh-superagent."
939
+ }
940
+ },
941
+ {
942
+ key: "chats",
943
+ type: "rows",
944
+ label: {
945
+ zh: "接收的群",
946
+ en: "Chats"
947
+ },
948
+ hint: {
949
+ zh: "“仅 @”只接收 @ 机器人的消息和已有工单的线程;“全部消息”也接收其余消息,评估可以按规则从中推断工作,需要机器人有读取全部消息的权限。",
950
+ en: "\"@ only\" receives messages that mention the bot and Threads that have Tickets; \"All\" also receives the rest, from which Eval may infer work under the rules; the bot needs permission to read all messages."
951
+ },
952
+ columns: [{
953
+ key: "chatId",
954
+ type: "text",
955
+ label: {
956
+ zh: "群",
957
+ en: "Chat"
958
+ },
959
+ pattern: "oc_[a-zA-Z0-9]+",
960
+ choose: {
961
+ zh: "选择机器人已加入的群",
962
+ en: "Choose a chat the bot has joined"
963
+ },
964
+ refresh: {
965
+ zh: "刷新群列表",
966
+ en: "Refresh chats"
967
+ }
968
+ }, {
969
+ key: "listen",
970
+ type: "select",
971
+ label: {
972
+ zh: "接收",
973
+ en: "Receive"
974
+ },
975
+ options: [{
976
+ value: "mentions",
977
+ label: {
978
+ zh: "仅 @",
979
+ en: "@ only"
980
+ }
981
+ }, {
982
+ value: "all",
983
+ label: {
984
+ zh: "全部消息",
985
+ en: "All"
986
+ }
987
+ }]
988
+ }]
989
+ },
990
+ {
991
+ key: "otherChats",
992
+ type: "select",
993
+ label: {
994
+ zh: "其他群",
995
+ en: "Other chats"
996
+ },
997
+ options: [{
998
+ value: "none",
999
+ label: {
1000
+ zh: "不接收",
1001
+ en: "Not received"
1002
+ }
1003
+ }, {
1004
+ value: "mentions",
1005
+ label: {
1006
+ zh: "仅 @",
1007
+ en: "@ only"
1008
+ }
1009
+ }],
1010
+ hint: {
1011
+ zh: "机器人已加入、但没列在上面的群。“仅 @”接收那里 @ 机器人的消息;谁的消息能发起工作看策略里的准入。",
1012
+ en: "Chats the bot has joined that are not listed above. \"@ only\" receives messages there that mention the bot; whose messages may originate work is the Policy's admission."
1013
+ }
1014
+ },
1015
+ {
1016
+ key: "directMessages",
1017
+ type: "switch",
1018
+ label: {
1019
+ zh: "接收私聊",
1020
+ en: "Direct messages"
1021
+ }
1022
+ },
1023
+ {
1024
+ key: "readPageSize",
1025
+ type: "number",
1026
+ advanced: true,
1027
+ min: 1,
1028
+ max: 50,
1029
+ label: {
1030
+ zh: "每页消息数",
1031
+ en: "Messages per page"
1032
+ },
1033
+ hint: {
1034
+ zh: "智能体读取线程和群历史时,每页的消息数。",
1035
+ en: "Messages per page when agents read Threads and chat history."
1036
+ }
1037
+ },
1038
+ {
1039
+ key: "linkTimeoutMs",
1040
+ type: "number",
1041
+ advanced: true,
1042
+ min: 1,
1043
+ max: 9e4,
1044
+ label: {
1045
+ zh: "原消息链接的查询时限 / 毫秒",
1046
+ en: "Original message link time limit / ms"
1047
+ },
1048
+ hint: {
1049
+ zh: "查询原消息链接最多等待的时间;超时后智能体改为直接读取原消息。",
1050
+ en: "How long to wait for an original message's link; after that, agents read the message itself."
1051
+ }
1052
+ }
1053
+ ],
1054
+ health: ["feishu", "history"],
1055
+ scope({ meta }, config) {
1056
+ if (meta.direct === true) return config.directMessages ? {
1057
+ listened: true,
1058
+ observed: false,
1059
+ row: "direct"
1060
+ } : {
1061
+ listened: false,
1062
+ reason: "direct_messages_not_allowed"
1063
+ };
1064
+ const chat = config.chats.find((item) => item.chatId === meta.chatId);
1065
+ if (chat) return {
1066
+ listened: true,
1067
+ observed: chat.listen === "all",
1068
+ row: chat.chatId
1069
+ };
1070
+ return config.otherChats === "mentions" ? {
1071
+ listened: true,
1072
+ observed: false,
1073
+ row: "other"
1074
+ } : {
1075
+ listened: false,
1076
+ reason: "chat_not_allowed"
1077
+ };
1078
+ },
1079
+ channel: {
1080
+ allowed: (address, config) => feishuKind.scope({ meta: {
1081
+ chatId: address.chatId ?? "",
1082
+ direct: address.direct === "true"
1083
+ } }, config).listened,
1084
+ send: sendFeishu,
1085
+ link: feishuLink
1086
+ },
1087
+ threadContext: feishuThreadContext,
1088
+ find: (query) => ({
1089
+ threads: [],
1090
+ terms: [...new Set(query.match(/om_[A-Za-z0-9_-]+/g) ?? [])]
1091
+ }),
1092
+ repliesTo: (trigger) => typeof trigger.meta.parentId === "string" ? feishuTriggerId(trigger.meta.parentId) : void 0,
1093
+ policy: {
1094
+ unnamed: {
1095
+ person: {
1096
+ zh: "飞书用户",
1097
+ en: "Feishu user"
1098
+ },
1099
+ bot: {
1100
+ zh: "飞书机器人",
1101
+ en: "Feishu bot"
1102
+ }
1103
+ },
1104
+ schema: object({
1105
+ allowed: array(object({
1106
+ id: string().regex(/^ou_[a-zA-Z0-9]+$/),
1107
+ name: string().trim().max(100).optional()
1108
+ }).strict()).refine((users) => new Set(users.map((user) => user.id)).size === users.length, "Duplicate open_ids").default([]),
1109
+ syncChat: string().regex(/^(oc_[a-zA-Z0-9]+)?$/).default(""),
1110
+ bots: array(object({
1111
+ id: string().regex(/^(cli|ou)_[a-zA-Z0-9]+$/),
1112
+ name: string().trim().max(100).optional()
1113
+ }).strict()).refine((bots) => new Set(bots.map((bot) => bot.id)).size === bots.length, "Duplicate bot ids").optional()
1114
+ }).strict(),
1115
+ fields: [
1116
+ {
1117
+ key: "allowed",
1118
+ type: "rows",
1119
+ label: {
1120
+ zh: "能发起工作的飞书用户",
1121
+ en: "Feishu users who may originate work"
1122
+ },
1123
+ columns: [{
1124
+ key: "id",
1125
+ type: "text",
1126
+ label: {
1127
+ zh: "open_id",
1128
+ en: "open_id"
1129
+ },
1130
+ pattern: "ou_[a-zA-Z0-9]+"
1131
+ }, {
1132
+ key: "name",
1133
+ type: "text",
1134
+ label: {
1135
+ zh: "名字",
1136
+ en: "Name"
1137
+ },
1138
+ optional: true
1139
+ }]
1140
+ },
1141
+ {
1142
+ key: "syncChat",
1143
+ type: "text",
1144
+ pattern: "(oc_[a-zA-Z0-9]+)?",
1145
+ label: {
1146
+ zh: "同步来源群",
1147
+ en: "Sync chat"
1148
+ },
1149
+ choose: {
1150
+ zh: "不同步",
1151
+ en: "Do not sync"
1152
+ },
1153
+ hint: {
1154
+ zh: "同步时,名单变成这个群里的人:新成员加入,离开的人移除,名字取群里的名字。启动时和点同步时进行。",
1155
+ en: "Syncing makes the list the people in this chat: new members join, people who left are removed, and names follow the chat. It runs at startup and on Sync."
1156
+ }
1157
+ },
1158
+ {
1159
+ key: "bots",
1160
+ type: "rows",
1161
+ label: {
1162
+ zh: "消息可以成为候选的机器人",
1163
+ en: "Bots whose messages may become candidates"
1164
+ },
1165
+ hint: {
1166
+ zh: "名单里的机器人在接收的群里发的消息成为推断的候选,交给评估;它们不能发起明确的请求,也不能停止工单。其他机器人的消息不接收。",
1167
+ en: "Messages of a bot on this list, in chats the Source receives, become inferred candidates for an assessment; they never make an explicit request or stop a ticket. Other bots' messages are not admitted."
1168
+ },
1169
+ columns: [{
1170
+ key: "id",
1171
+ type: "text",
1172
+ label: {
1173
+ zh: "App ID 或 open_id",
1174
+ en: "App ID or open_id"
1175
+ },
1176
+ pattern: "(cli|ou)_[a-zA-Z0-9]+"
1177
+ }, {
1178
+ key: "name",
1179
+ type: "text",
1180
+ label: {
1181
+ zh: "名字",
1182
+ en: "Name"
1183
+ },
1184
+ optional: true
1185
+ }]
1186
+ }
1187
+ ],
1188
+ explicit: {
1189
+ zh: "名单内的人 @ 机器人或私聊机器人,总是明确的请求,不需要配置。",
1190
+ en: "Someone on the list who mentions the bot or messages it directly always makes an explicit request; that needs no setting."
1191
+ },
1192
+ rows(stored) {
1193
+ const config = FeishuConfigSchema.parse(stored);
1194
+ const mentions = {
1195
+ zh: "仅 @",
1196
+ en: "@ only"
1197
+ };
1198
+ return [
1199
+ ...config.chats.map((chat) => ({
1200
+ id: chat.chatId,
1201
+ label: chat.name || chat.chatId,
1202
+ note: chat.listen === "all" ? {
1203
+ zh: "全部消息",
1204
+ en: "All"
1205
+ } : mentions,
1206
+ cells: {
1207
+ listedUnaddressed: chat.listen === "all",
1208
+ unlistedAddressed: true,
1209
+ unlistedUnaddressed: chat.listen === "all"
1210
+ }
1211
+ })),
1212
+ {
1213
+ id: "other",
1214
+ label: {
1215
+ zh: "其他群",
1216
+ en: "Other chats"
1217
+ },
1218
+ note: config.otherChats === "mentions" ? mentions : {
1219
+ zh: "不接收",
1220
+ en: "Not received"
1221
+ },
1222
+ cells: {
1223
+ listedUnaddressed: false,
1224
+ unlistedAddressed: config.otherChats === "mentions",
1225
+ unlistedUnaddressed: false
1226
+ }
1227
+ },
1228
+ {
1229
+ id: "direct",
1230
+ label: {
1231
+ zh: "私聊",
1232
+ en: "Direct messages"
1233
+ },
1234
+ note: config.directMessages ? {
1235
+ zh: "接收",
1236
+ en: "Received"
1237
+ } : {
1238
+ zh: "不接收",
1239
+ en: "Not received"
1240
+ },
1241
+ cells: {
1242
+ listedUnaddressed: false,
1243
+ unlistedAddressed: config.directMessages,
1244
+ unlistedUnaddressed: false
1245
+ }
1246
+ }
1247
+ ];
1248
+ },
1249
+ sync: {
1250
+ label: {
1251
+ zh: "立即同步",
1252
+ en: "Sync now"
1253
+ },
1254
+ ready: (part) => !!part.syncChat,
1255
+ async run(host, part, signal) {
1256
+ const commands = sourceContext(host, feishuKind).commands.withSignal(signal);
1257
+ const members = [
1258
+ "im",
1259
+ "+chat-members-list",
1260
+ "--chat-id",
1261
+ String(part.syncChat),
1262
+ "--member-types",
1263
+ "user",
1264
+ "--page-all",
1265
+ "--page-limit",
1266
+ "0"
1267
+ ];
1268
+ const result = MembersSchema.parse(await commands.lark(members));
1269
+ if (result.has_more || result.truncations?.length) throw new Error("Feishu returned an incomplete member list");
1270
+ const users = new Map(result.users.map((user) => [user.member_id, user.name]));
1271
+ const known = new Set(part.allowed.map((entry) => entry.id));
1272
+ return {
1273
+ ...part,
1274
+ allowed: [...part.allowed.filter((entry) => users.has(entry.id)).map((entry) => ({
1275
+ ...entry,
1276
+ name: users.get(entry.id)
1277
+ })), ...result.users.filter((user) => !known.has(user.member_id)).map((user) => ({
1278
+ id: user.member_id,
1279
+ name: user.name
1280
+ }))]
1281
+ };
1282
+ }
1283
+ }
1284
+ },
1285
+ environment: {
1286
+ variables: [{
1287
+ key: "DSH_SUPERAGENT_FEISHU_BOT",
1288
+ description: {
1289
+ zh: "飞书机器人;lark-cli 以它行事",
1290
+ en: "The Feishu bot; lark-cli acts as it"
1291
+ }
1292
+ }, {
1293
+ key: "DSH_SUPERAGENT_FEISHU_CHAT",
1294
+ description: {
1295
+ zh: "接下工单的飞书群 chat_id;工单不是从飞书来的时为空",
1296
+ en: "chat_id of the Feishu chat that started the ticket; empty when the ticket did not start on Feishu"
1297
+ }
1298
+ }],
1299
+ values: ({ deployment, trigger }) => ({
1300
+ DSH_SUPERAGENT_FEISHU_BOT: deployment.feishu ? deployment.feishu.name || deployment.feishu.botId : "",
1301
+ DSH_SUPERAGENT_FEISHU_CHAT: trigger?.source === "feishu" ? String(trigger.meta.chatId ?? "") : ""
1302
+ })
1303
+ },
1304
+ facts: (deployment) => deployment?.feishu ? { cli: {
1305
+ zh: `当前机器人 ${deployment.feishu.name || deployment.feishu.botId}`,
1306
+ en: `Current bot ${deployment.feishu.name || deployment.feishu.botId}`
1307
+ } } : {}
1308
+ };
1309
+ /** Returns the Feishu Source's settings, falling back to schema defaults while the Feishu plugin is disabled. */
1310
+ const feishuSettings = (sources) => sources.instance("feishu")?.config ?? FeishuConfigSchema.parse({});
1311
+ const name = "dsh-superagent-feishu";
1312
+ const inject = ["orchestrator"];
1313
+ function apply(ctx) {
1314
+ ctx.effect(() => ctx.orchestrator.sources.register(feishuKind), "feishu: source kind");
1315
+ ctx.effect(() => {
1316
+ const controller = new AbortController();
1317
+ const running = ctx.orchestrator.sources.instances().filter((instance) => instance.kind === feishuKind.kind && instance.installed).map((instance) => runFeishu(sourceContext(ctx.orchestrator, feishuKind, instance.id), controller.signal));
1318
+ return async () => {
1319
+ controller.abort();
1320
+ await Promise.all(running);
1321
+ };
1322
+ });
1323
+ }
1324
+ //#endregion
1325
+ export { withMentionNames as S, feishuThreadId as _, name as a, feishuTriggerId as b, pollThreads as c, feishuChatLink as d, feishuRead as f, feishuRef as g, SOURCE as h, inject as i, refreshChatNames as l, readFeishuLink as m, feishuKind as n, backfillChat as o, feishuReferences as p, feishuSettings as r, consumerRetry as s, apply as t, threadRef as u, feishuTime as v, historyEvent as x, feishuTrigger as y };