@reserendipity/dsh-superagent 0.0.0-stage → 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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +50 -0
  3. package/README.md +206 -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,124 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/core/model.ts
3
+ /** Stable key of a thread across the whole domain. */
4
+ const threadKey = (ref) => `${ref.source}:${ref.id}`;
5
+ /**
6
+ * Normalizes valid timestamps to `Date#toISOString` form so stored timestamps compare lexicographically in memory and SQL;
7
+ * values that do not parse as a timestamp are returned unchanged.
8
+ */
9
+ function isoTime(value) {
10
+ const time = Date.parse(value);
11
+ return Number.isFinite(time) ? new Date(time).toISOString() : value;
12
+ }
13
+ /**
14
+ * The tickets whose worker decides a person's follow-up: those it reached on their own Threads that can still decide it
15
+ * and did not hand it back. A ticket can while it is live, also once a person dropped its Subscription to the Thread, and
16
+ * while it is ended, not by a person, with the Trigger waiting to resume it. None once a person sent it to Eval, or while
17
+ * an Eval holds it, which an earlier version may have queued. While a ticket owns it, the message goes to no Eval, and the
18
+ * main Session may only stop the work on it.
19
+ */
20
+ function followupOwners(trigger, records) {
21
+ if (!trigger.human) return [];
22
+ const disposition = records.triggers.disposition(trigger.id);
23
+ if (!disposition || disposition.requeued) return [];
24
+ const returned = new Set(disposition.handbacks?.map((item) => item.ticketId));
25
+ const reached = disposition.fanout.filter((id) => !returned.has(id) && !disposition.context?.includes(id));
26
+ if (!reached.length || records.threads.get(threadKey(trigger.threads[0]))?.evaluating?.triggerIds.includes(trigger.id)) return [];
27
+ return reached.filter((id) => {
28
+ const ticket = records.tickets.get(id);
29
+ return !!ticket && (liveTicket(ticket) || ticket.ended?.by !== "human" && ticket.inputs.some((input) => input.kind === "trigger" && input.triggerId === trigger.id && ticket.undelivered.includes(input.id)));
30
+ });
31
+ }
32
+ /**
33
+ * Whether a Decision settled what a person's message asks for: a stop on it, taken or refused, decides only that ticket's
34
+ * work, routing it to a ticket leaves it to that ticket's worker, and an unavailable one decided nothing.
35
+ */
36
+ const settled = (decision) => !["stop", "unavailable"].includes(decision.kind) && !decision.reason.some((reason) => [
37
+ "stopNotExplicit",
38
+ "stopUnknown",
39
+ "routed"
40
+ ].includes(reason.code));
41
+ /** Whether a ticket is open: it has not ended. */
42
+ const liveTicket = (ticket) => !ticket.ended;
43
+ /** How a ticket ended. */
44
+ const ticketOutcome = (ticket) => ticket.ended?.outcome;
45
+ const runningAssignment = (ticket) => ticket.assignments.find((assignment) => assignment.state === "running");
46
+ /**
47
+ * Whether a Trigger reached a ticket: it started the ticket, came to it as an Input, or is on a Thread the ticket subscribes
48
+ * to; with `context` false, not a Thread the ticket follows only for context, where its worker may read but not write.
49
+ */
50
+ function reached(ticket, trigger, thread, context = true) {
51
+ if (ticket.origin.triggerId === trigger.id) return true;
52
+ const input = ticket.inputs.some((item) => item.kind === "trigger" && item.triggerId === trigger.id);
53
+ return trigger.threads.some((ref) => {
54
+ const current = thread(threadKey(ref));
55
+ return current?.subscriptions.includes(ticket.id) === true && (context || !current.context?.includes(ticket.id));
56
+ }) || input && (context || !trigger.threads.every((ref) => thread(threadKey(ref))?.context?.includes(ticket.id)));
57
+ }
58
+ const latestAssignment = (ticket) => ticket.assignments.at(-1);
59
+ /** The latest report any of a ticket's Assignments made, which is what the ticket last said. */
60
+ const latestReport = (ticket) => ticket.assignments.findLast((assignment) => assignment.report)?.report;
61
+ /**
62
+ * Until when a ticket's worker had a turn to read what reached it: its latest report, but a progress report when `progress`
63
+ * is false, or the end of its latest turn, whichever is later.
64
+ */
65
+ function readUntil(ticket, progress = true) {
66
+ return [(ticket.assignments.findLast((assignment) => assignment.report && (progress || assignment.report.state !== "progress"))?.report)?.at ?? "", ...ticket.assignments.map((assignment) => assignment.idleAt ?? "")].reduce((a, b) => a > b ? a : b);
67
+ }
68
+ /**
69
+ * Whether a ticket waits for a person: its latest Assignment reported blocked or failed, and no person's message reached it
70
+ * since. A later report that is neither, a person's message, or a retry, which starts a new Assignment, lifts it.
71
+ */
72
+ function waitsForPerson(ticket) {
73
+ const report = latestAssignment(ticket)?.report;
74
+ if (report?.state !== "blocked" && report?.state !== "failed") return false;
75
+ return !ticket.inputs.some((input) => input.kind === "trigger" && input.human && !input.context && input.at > report.at);
76
+ }
77
+ //#endregion
78
+ //#region src/core/ids.ts
79
+ /** Stable content-derived identifiers; equal inputs always produce the same id. */
80
+ function key(...values) {
81
+ return createHash("sha256").update(JSON.stringify(values)).digest("hex").slice(0, 32);
82
+ }
83
+ //#endregion
84
+ //#region src/infra/text.ts
85
+ /** Word counting and string formatting utilities for IM message limits and templates. */
86
+ const words = new Intl.Segmenter("zh", { granularity: "word" });
87
+ /** Counts word-like segments across Chinese, English, and mixed text, ignoring punctuation. */
88
+ function wordCount(text) {
89
+ let count = 0;
90
+ for (const segment of words.segment(text)) if (segment.isWordLike) count++;
91
+ return count;
92
+ }
93
+ /** Validates that an outgoing IM Markdown message does not exceed `IM_WORDS` (link labels and URLs both count toward the limit). */
94
+ function checkedImText(text) {
95
+ const count = wordCount(text);
96
+ if (count > 100) throw new Error(`IM message has ${count} words; maximum is 100. Rewrite as a short high-level update. Do not split it into multiple messages.`);
97
+ return text;
98
+ }
99
+ const fill = (template, values) => template.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? "");
100
+ /** Case-insensitive comparison for GitHub logins and repository names. */
101
+ const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
102
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
103
+ //#endregion
104
+ //#region src/sources/host.ts
105
+ /** Fixed English health templates (`{name}` placeholders); the panel localizes each `code` in the interface language. */
106
+ const HEALTH_TEXT = {
107
+ setupPending: "Waiting for the deployment check",
108
+ sourceOff: "Off",
109
+ feishuCliUnset: "The Source is on, and its bot lark-cli is not set",
110
+ feishuBotMissing: "Waiting for the Feishu bot",
111
+ pollFailures: "{count} consecutive failed polls; last: {error}"
112
+ };
113
+ function health(state, code, values = {}) {
114
+ return {
115
+ state,
116
+ detail: fill(HEALTH_TEXT[code], values),
117
+ code,
118
+ ...Object.keys(values).length ? { values } : {}
119
+ };
120
+ }
121
+ /** The context of one Source instance: `instance` defaults to the kind, which is the single-instance case. */
122
+ const sourceContext = (host, kind, instance) => host.context(kind, instance);
123
+ //#endregion
124
+ export { settled as _, fill as a, waitsForPerson as b, key as c, latestAssignment as d, latestReport as f, runningAssignment as g, readUntil as h, escapeRegExp as i, followupOwners as l, reached as m, sourceContext as n, sameName as o, liveTicket as p, checkedImText as r, wordCount as s, health as t, isoTime as u, threadKey as v, ticketOutcome as y };
@@ -0,0 +1,43 @@
1
+ import { _ as output, c as ZodObject, f as ZodString, r as ZodDefault, s as ZodNumber, x as $strict } from "./schemas-lx-F2nnt.js";
2
+ import { U as Trigger, l as OutboundMessage } from "./intake-TNIkqilh.js";
3
+ import { n as SourceKind, r as SourceContext, t as DeliveredMessage } from "./kind--8woUEex.js";
4
+ import { Context } from "@deepseek-ai/cordis";
5
+ //#region src/sources/inbox.d.ts
6
+ declare const InboxConfigSchema: ZodObject<{
7
+ inboxDir: ZodDefault<ZodString>;
8
+ actor: ZodDefault<ZodString>;
9
+ pollSeconds: ZodDefault<ZodNumber>;
10
+ }, $strict>;
11
+ type InboxConfig = output<typeof InboxConfigSchema>;
12
+ /** The inbox root: the configured absolute directory, or `<stateDir>/inbox` when unset. */
13
+ declare function inboxDir(deployment: {
14
+ stateDir: string;
15
+ }, config: InboxConfig): string;
16
+ /** One pending task file as the poll sees it. */
17
+ interface InboxFile {
18
+ path: string;
19
+ relPath: string;
20
+ content: string;
21
+ mtimeMs: number;
22
+ }
23
+ /** The Trigger of one task file; unchanged replays share one id, edits and re-drops do not. */
24
+ declare function inboxTrigger(file: InboxFile, config: InboxConfig, root: string): Trigger;
25
+ /**
26
+ * Renders the Thread's reply file from the pipeline's own receipts, oldest delivery first. Content that no receipt backs -
27
+ * a block a worker wrote straight into the file - is therefore gone after the next delivery, which is the point: the file
28
+ * is the delivery record, and only the Channel delivers. Without `delivered` (a caller outside the pipeline) the single
29
+ * message is appended once, keyed by its id, as before.
30
+ */
31
+ declare function sendInbox(_host: Pick<SourceContext, 'commands'>, message: OutboundMessage, delivered?: readonly DeliveredMessage[]): Promise<'sent' | 'gone'>;
32
+ declare const inboxKind: SourceKind<InboxConfig>;
33
+ /** Read pending files in the inbox, take in each, and archive it under done/. Advance nothing else: the move is the cursor. */
34
+ declare function pollInbox(ctx: SourceContext, config: InboxConfig, signal: AbortSignal): Promise<{
35
+ read: number;
36
+ moved: number;
37
+ failed: number;
38
+ }>;
39
+ declare const name = "dsh-superagent-inbox";
40
+ declare const inject: string[];
41
+ declare function apply(ctx: Context): void;
42
+ //#endregion
43
+ export { InboxConfig, InboxFile, apply, inboxDir, inboxKind, inboxTrigger, inject, name, pollInbox, sendInbox };
@@ -0,0 +1,343 @@
1
+ import { c as key, n as sourceContext, t as health, v as threadKey } from "./host-BIWKSB5j.js";
2
+ import { c as object, r as array, s as number, u as string } from "./schemas-MhNERx8d.js";
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, extname, join, resolve } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ import { setTimeout } from "node:timers/promises";
7
+ //#region src/sources/inbox.ts
8
+ const SOURCE = "inbox";
9
+ const ARCHIVE = "done";
10
+ const MAX_TEXT_CHARS = 2e5;
11
+ const InboxConfigSchema = object({
12
+ /** Absolute inbox directory; empty means `<stateDir>/inbox`. The `done/` subdirectory holds archived tasks and replies. */
13
+ inboxDir: string().trim().max(500).default(""),
14
+ /** Stable actor id dropped files are attributed to; it is the sender name on the Policy's allowed list. */
15
+ actor: string().regex(/^[a-z0-9_-]{1,64}$/).default("local"),
16
+ /** Polling interval in seconds. */
17
+ pollSeconds: number().int().min(5).max(3600).default(10)
18
+ }).strict();
19
+ /** The inbox root: the configured absolute directory, or `<stateDir>/inbox` when unset. */
20
+ function inboxDir(deployment, config) {
21
+ return config.inboxDir ? resolve(config.inboxDir) : join(deployment.stateDir, "inbox");
22
+ }
23
+ const fileUrl = (path) => `file:///${path.replaceAll("\\", "/")}`;
24
+ /** The Trigger of one task file; unchanged replays share one id, edits and re-drops do not. */
25
+ function inboxTrigger(file, config, root) {
26
+ const hash = createHash("sha256").update(file.content).digest("hex").slice(0, 16);
27
+ const stableId = key(SOURCE, file.relPath);
28
+ const id = key(SOURCE, file.relPath, String(Math.floor(file.mtimeMs)), hash);
29
+ const text = file.content.length > MAX_TEXT_CHARS ? `${file.content.slice(0, MAX_TEXT_CHARS)}\n\n[truncated: the file holds ${file.content.length} characters]` : file.content;
30
+ const url = fileUrl(file.path);
31
+ return {
32
+ id,
33
+ source: SOURCE,
34
+ threads: [{
35
+ source: SOURCE,
36
+ id: stableId
37
+ }],
38
+ actor: {
39
+ source: SOURCE,
40
+ id: config.actor
41
+ },
42
+ human: true,
43
+ addressed: "direct",
44
+ at: new Date(file.mtimeMs).toISOString(),
45
+ text,
46
+ url,
47
+ channel: {
48
+ source: SOURCE,
49
+ thread: threadKey({
50
+ source: SOURCE,
51
+ id: stableId
52
+ }),
53
+ address: {
54
+ file: file.relPath,
55
+ path: file.path,
56
+ reply: join(root, ARCHIVE, `${file.relPath.replaceAll(/[\\/]/g, "__")}.reply.md`)
57
+ }
58
+ },
59
+ meta: {
60
+ file: file.relPath,
61
+ path: file.path,
62
+ stableId
63
+ }
64
+ };
65
+ }
66
+ /** One delivered message exactly as it appears in the reply file. */
67
+ const block = (message, at, file) => `<!-- msg:${message.id} -->\n**${message.kind} · ${at} · ${file}**\n\n${message.text}\n\n`;
68
+ /**
69
+ * Renders the Thread's reply file from the pipeline's own receipts, oldest delivery first. Content that no receipt backs -
70
+ * a block a worker wrote straight into the file - is therefore gone after the next delivery, which is the point: the file
71
+ * is the delivery record, and only the Channel delivers. Without `delivered` (a caller outside the pipeline) the single
72
+ * message is appended once, keyed by its id, as before.
73
+ */
74
+ async function sendInbox(_host, message, delivered) {
75
+ const { reply, file } = message.channel.address;
76
+ if (!reply) throw new Error("Missing inbox reply path");
77
+ mkdirSync(dirname(reply), { recursive: true });
78
+ const name = String(file ?? "");
79
+ if (!delivered) {
80
+ let existing = "";
81
+ try {
82
+ existing = readFileSync(reply, "utf8");
83
+ } catch {}
84
+ if (!existing.includes(`<!-- msg:${message.id} -->`)) appendFileSync(reply, block(message, (/* @__PURE__ */ new Date()).toISOString(), name));
85
+ return "sent";
86
+ }
87
+ const record = /* @__PURE__ */ new Map();
88
+ for (const item of delivered) if (!record.has(item.id)) record.set(item.id, block(item, item.sentAt ?? (/* @__PURE__ */ new Date()).toISOString(), name));
89
+ if (!record.has(message.id)) record.set(message.id, block(message, (/* @__PURE__ */ new Date()).toISOString(), name));
90
+ const temporary = `${reply}.tmp-${process.pid}`;
91
+ writeFileSync(temporary, [...record.values()].join(""));
92
+ renameSync(temporary, reply);
93
+ return "sent";
94
+ }
95
+ const inboxKind = {
96
+ kind: SOURCE,
97
+ title: {
98
+ zh: "本地收件目录",
99
+ en: "Local inbox"
100
+ },
101
+ description: {
102
+ zh: "把 Markdown 文件放进收件目录即为向智能体直接提出工单;结果写入同目录 done 下的回信文件。单人部署无需第二个 GitHub 账号。",
103
+ en: "Dropping a Markdown file into the inbox is a direct request to the agent; results land in a reply file under done/. A single-operator deployment needs no second GitHub account."
104
+ },
105
+ schema: InboxConfigSchema,
106
+ fields: [
107
+ {
108
+ key: "inboxDir",
109
+ type: "text",
110
+ label: {
111
+ zh: "收件目录",
112
+ en: "Inbox directory"
113
+ },
114
+ hint: {
115
+ zh: "绝对路径;留空用状态目录下的 inbox。目录里每个 .md 文件是一张请求,处理完连同回信移入 done。",
116
+ en: "Absolute path; empty means inbox under the state directory. Each .md file is one request; processed files and replies move to done/."
117
+ }
118
+ },
119
+ {
120
+ key: "actor",
121
+ type: "text",
122
+ label: {
123
+ zh: "本机发起人",
124
+ en: "Local sender"
125
+ },
126
+ hint: {
127
+ zh: "文件请求归属的发送者 id,须出现在策略名单里才能发起工作。",
128
+ en: "The sender id dropped files are attributed to; it must be on the Policy list to originate work."
129
+ }
130
+ },
131
+ {
132
+ key: "pollSeconds",
133
+ type: "number",
134
+ advanced: true,
135
+ min: 5,
136
+ max: 3600,
137
+ label: {
138
+ zh: "轮询间隔 / 秒",
139
+ en: "Polling interval / seconds"
140
+ }
141
+ }
142
+ ],
143
+ health: [SOURCE],
144
+ scope: () => ({
145
+ listened: true,
146
+ observed: true,
147
+ row: SOURCE
148
+ }),
149
+ channel: {
150
+ allowed: (address) => typeof address.reply === "string" && address.reply.length > 0,
151
+ send: sendInbox,
152
+ link: (address) => ({
153
+ url: address.path ? fileUrl(address.path) : "",
154
+ label: {
155
+ zh: `本地任务 ${address.file ?? ""}`,
156
+ en: `Local task ${address.file ?? ""}`
157
+ }
158
+ })
159
+ },
160
+ threadContext: {
161
+ messageId: (trigger) => String(trigger.meta.stableId ?? ""),
162
+ read: async (_commands, trigger) => {
163
+ const messages = [{
164
+ id: String(trigger.meta.stableId ?? ""),
165
+ from: trigger.actor.id,
166
+ at: trigger.at,
167
+ text: trigger.text
168
+ }];
169
+ return {
170
+ messages,
171
+ body: { messages }
172
+ };
173
+ }
174
+ },
175
+ policy: {
176
+ schema: object({ allowed: array(object({
177
+ id: string().min(1),
178
+ name: string().trim().max(100).optional()
179
+ }).strict()).default([{
180
+ id: "local",
181
+ name: "本机"
182
+ }]) }).strict(),
183
+ fields: [{
184
+ key: "allowed",
185
+ type: "rows",
186
+ label: {
187
+ zh: "能发起工作的发送者",
188
+ en: "Senders who may originate work"
189
+ },
190
+ columns: [{
191
+ key: "id",
192
+ type: "text",
193
+ label: {
194
+ zh: "发送者",
195
+ en: "Sender"
196
+ }
197
+ }, {
198
+ key: "name",
199
+ type: "text",
200
+ label: {
201
+ zh: "名字",
202
+ en: "Name"
203
+ },
204
+ optional: true
205
+ }]
206
+ }],
207
+ explicit: {
208
+ zh: "把任务文件放进收件目录。",
209
+ en: "Drop a task file into the inbox."
210
+ },
211
+ rows: () => [{
212
+ id: SOURCE,
213
+ label: {
214
+ zh: "收件目录",
215
+ en: "Inbox"
216
+ },
217
+ note: {
218
+ zh: "本机的文件投放",
219
+ en: "Local file drops"
220
+ },
221
+ cells: {
222
+ listedUnaddressed: true,
223
+ unlistedAddressed: true,
224
+ unlistedUnaddressed: true
225
+ }
226
+ }]
227
+ },
228
+ environment: {
229
+ variables: [{
230
+ key: "DSH_SUPERAGENT_INBOX_FILE",
231
+ description: {
232
+ zh: "接下工单的本地任务文件名;工单不是从收件目录来的时为空",
233
+ en: "The inbox task file that started the ticket; empty when the ticket did not start on this Source"
234
+ }
235
+ }],
236
+ values: ({ trigger }) => ({ DSH_SUPERAGENT_INBOX_FILE: trigger?.source === SOURCE ? String(trigger.meta.file ?? "") : "" })
237
+ }
238
+ };
239
+ /** Read pending files in the inbox, take in each, and archive it under done/. Advance nothing else: the move is the cursor. */
240
+ async function pollInbox(ctx, config, signal) {
241
+ const root = inboxDir(ctx.deployment, config);
242
+ if (!existsSync(root)) mkdirSync(root, { recursive: true });
243
+ const names = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")).map((entry) => entry.name).sort();
244
+ const archive = join(root, ARCHIVE);
245
+ let read = 0, moved = 0, failed = 0;
246
+ for (const name of names) {
247
+ if (signal.aborted) throw new Error("The inbox Source stopped during the read");
248
+ let file;
249
+ try {
250
+ const path = join(root, name);
251
+ const st = statSync(path);
252
+ file = {
253
+ path,
254
+ relPath: name,
255
+ content: readFileSync(path, "utf8"),
256
+ mtimeMs: st.mtimeMs
257
+ };
258
+ } catch {
259
+ failed++;
260
+ continue;
261
+ }
262
+ read++;
263
+ try {
264
+ const trigger = inboxTrigger(file, config, root);
265
+ await ctx.receive(trigger);
266
+ ctx.threads.describe(threadKey(trigger.threads[0]), {
267
+ url: trigger.url,
268
+ facts: { file: {
269
+ label: {
270
+ zh: "任务文件",
271
+ en: "Task file"
272
+ },
273
+ value: name
274
+ } }
275
+ });
276
+ if (!existsSync(archive)) mkdirSync(archive, { recursive: true });
277
+ let target = join(archive, name);
278
+ if (existsSync(target)) target = join(archive, `${basename(name, extname(name))}-${Math.floor(file.mtimeMs)}${extname(name)}`);
279
+ if (existsSync(target)) target = join(archive, `${basename(name, extname(name))}-${Date.now()}${extname(name)}`);
280
+ renameSync(file.path, target);
281
+ moved++;
282
+ } catch {
283
+ failed++;
284
+ }
285
+ }
286
+ return {
287
+ read,
288
+ moved,
289
+ failed
290
+ };
291
+ }
292
+ async function watch$1(ctx, signal) {
293
+ await ctx.ready;
294
+ if (!ctx.configured()) {
295
+ ctx.report(SOURCE, health("paused", "setupPending"));
296
+ return;
297
+ }
298
+ let failures = 0;
299
+ while (!signal.aborted) {
300
+ const config = ctx.config(SOURCE);
301
+ if (!config) ctx.report(SOURCE, health("paused", "sourceOff"));
302
+ else {
303
+ const at = (/* @__PURE__ */ new Date()).toISOString();
304
+ try {
305
+ const { read, moved, failed } = await pollInbox(ctx, config, signal);
306
+ failures = 0;
307
+ ctx.report(SOURCE, {
308
+ state: "ready",
309
+ detail: `${read} files, ${moved} archived, ${failed} failed`,
310
+ at
311
+ });
312
+ } catch (error) {
313
+ failures += 1;
314
+ if (!signal.aborted && failures >= 3) ctx.report(SOURCE, health("error", "pollFailures", {
315
+ count: String(failures),
316
+ error: String(error)
317
+ }));
318
+ }
319
+ }
320
+ await setTimeout((config?.pollSeconds ?? 10) * 1e3, void 0, { signal }).catch(() => {});
321
+ }
322
+ }
323
+ const name = "dsh-superagent-inbox";
324
+ const inject = ["orchestrator"];
325
+ function apply(ctx) {
326
+ ctx.effect(() => ctx.orchestrator.sources.register(inboxKind), "inbox: source kind");
327
+ ctx.effect(() => {
328
+ const source = sourceContext(ctx.orchestrator, inboxKind);
329
+ const controller = new AbortController();
330
+ const running = watch$1(source, controller.signal).catch((error) => {
331
+ source.report(SOURCE, {
332
+ state: "error",
333
+ detail: String(error)
334
+ });
335
+ });
336
+ return async () => {
337
+ controller.abort();
338
+ await running;
339
+ };
340
+ });
341
+ }
342
+ //#endregion
343
+ export { apply, inboxDir, inboxKind, inboxTrigger, inject, name, pollInbox, sendInbox };