@rivus/gateway 0.16.2 → 0.17.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 (36) hide show
  1. package/LICENSE +674 -21
  2. package/README.md +25 -0
  3. package/dist/bootstrap/herdr-feishu.d.ts +37 -0
  4. package/dist/bootstrap/herdr-feishu.js +856 -0
  5. package/dist/bootstrap/pi-feishu.d.ts +281 -16
  6. package/dist/bootstrap/pi-feishu.js +17314 -344
  7. package/dist/chunks/background-session-authority.js +2 -38
  8. package/dist/chunks/rivus-daemon-cli.d.ts +1115 -0
  9. package/dist/chunks/rivus-node-entrypoint.js +700 -819
  10. package/dist/chunks/{node-rivus-deployment-manifest.js → runtime-cache.js} +1116 -1475
  11. package/dist/cli.d.ts +2 -1
  12. package/dist/cli.js +1 -0
  13. package/dist/index.d.ts +65 -3
  14. package/dist/index.js +3 -1215
  15. package/dist/mcp.d.ts +7 -63
  16. package/dist/mcp.js +3 -250
  17. package/package.json +29 -14
  18. package/templates/a-share-briefing-analysis.mjs +3 -1
  19. package/templates/a-share-briefing-renderer.mjs +15 -5
  20. package/templates/a-share-index-evidence.mjs +9 -3
  21. package/templates/a-share-market-briefing.mjs +3 -1
  22. package/templates/a-share-overseas-evidence.mjs +12 -4
  23. package/templates/a-share-policy-evidence.mjs +30 -10
  24. package/templates/a-share-sector-evidence.mjs +3 -1
  25. package/templates/acp-stdio-proxy.mjs +10 -4
  26. package/templates/current-weather.mjs +36 -12
  27. package/templates/herdr-feishu.mjs +30 -0
  28. package/templates/html-drive-tools.mjs +36 -12
  29. package/templates/pi-feishu-deployment.bootstrap.ts +1 -3
  30. package/templates/pi-feishu.bootstrap.ts +1 -242
  31. package/templates/rivus-agents.plugin.mjs +3 -1
  32. package/dist/chunks/background-session-control-input.js +0 -45
  33. package/dist/chunks/background-session-service.d.ts +0 -390
  34. package/dist/chunks/index.d.ts +0 -4703
  35. package/dist/chunks/service.js +0 -12112
  36. package/templates/langfuse-drive-e2e.mjs +0 -175
@@ -0,0 +1,856 @@
1
+ import { isAbsolute, join, resolve } from "node:path";
2
+ import { Array as Array$1, Effect, Schema } from "effect";
3
+ import { lstat, mkdir, open, readFile, readdir, unlink } from "node:fs/promises";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
+ import { createConnection } from "node:net";
6
+ import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
7
+ import { stripVTControlCharacters } from "node:util";
8
+ //#region src/core/application/terminal-session/terminal-session-bridge.ts
9
+ function createTerminalSessionBridge(options) {
10
+ if (!options.actors.length || !options.chats.length) throw new Error("Explicit actor and chat allowlists required");
11
+ const bindings = /* @__PURE__ */ new Map();
12
+ const watches = /* @__PURE__ */ new Map();
13
+ const allowed = (actor, chat) => options.actors.includes(actor) && options.chats.includes(chat);
14
+ const bindingFor = (handle, message, refresh = false) => {
15
+ const binding = bindings.get(handle);
16
+ if (!binding || binding.expires + (refresh ? 18e5 : 0) <= options.now() || binding.actorId !== message.actorId || binding.chatId !== message.chatId) throw new Error("Target expired; run /herdr list again");
17
+ return binding;
18
+ };
19
+ const reply = (message, text) => ({
20
+ chatId: message.chatId,
21
+ text
22
+ });
23
+ const watchKey = (message, agent) => JSON.stringify([
24
+ message.actorId,
25
+ message.chatId,
26
+ agent.session,
27
+ agent.pane,
28
+ agent.identity
29
+ ]);
30
+ const bind = (message, agent, form = false) => {
31
+ for (const [handle, binding] of bindings) if (binding.expires + 18e5 <= options.now()) bindings.delete(handle);
32
+ if (bindings.size >= 1e3) bindings.clear();
33
+ const handle = options.newHandle();
34
+ bindings.set(handle, {
35
+ agent,
36
+ actorId: message.actorId,
37
+ chatId: message.chatId,
38
+ form,
39
+ expires: options.now() + 6e5
40
+ });
41
+ return handle;
42
+ };
43
+ const detail = (message, agent, handle, text) => ({
44
+ chatId: message.chatId,
45
+ text: `${agent.session} / ${agent.name} | ${agent.status}\n${text}`,
46
+ menu: {
47
+ kind: "detail",
48
+ agents: [{
49
+ ...agent,
50
+ handle
51
+ }],
52
+ writable: options.writable,
53
+ notice: text
54
+ }
55
+ });
56
+ const recovery = (message) => {
57
+ const handle = message.text.trim().split(/\s+/)[2] ?? "";
58
+ let agents = [];
59
+ try {
60
+ agents = [{
61
+ ...bindingFor(handle, message, true).agent,
62
+ handle
63
+ }];
64
+ } catch {}
65
+ return {
66
+ ...reply(message, "会话已过期、离线或目标已变化(target changed)。刷新原会话不会重发指令;目标已替换时请重新选择。"),
67
+ menu: {
68
+ kind: "recovery",
69
+ writable: false,
70
+ agents
71
+ }
72
+ };
73
+ };
74
+ const execute = (message) => Effect.gen(function* () {
75
+ const match = /^\/herdr(?:\s+(\S+))?(?:\s+([\s\S]*))?$/.exec(message.text.trim());
76
+ if (!match) return null;
77
+ const command = match[1] ?? "help";
78
+ const argument = match[2]?.trim() ?? "";
79
+ if (command === "list" || command === "menu") {
80
+ const { agents, unavailableSessions } = yield* options.sessions.list();
81
+ const page = Math.max(0, Math.min(Math.floor(Number(argument) || 0), Math.max(0, Math.ceil(agents.length / 20) - 1)));
82
+ const choices = (command === "menu" ? agents.slice(page * 20, (page + 1) * 20) : agents).map((agent) => ({
83
+ ...agent,
84
+ handle: bind(message, agent)
85
+ }));
86
+ const warning = unavailableSessions.length ? `\nUnavailable sessions: ${unavailableSessions.join(", ")}; inventory is incomplete.` : "";
87
+ const lines = choices.map((agent) => `${agent.handle} | ${agent.session} | ${agent.name} | ${agent.status}`);
88
+ return {
89
+ ...reply(message, `${new Date(options.now()).toISOString()}\n${lines.join("\n") || "No panels"}${warning}`),
90
+ ...command === "menu" ? { menu: {
91
+ kind: "list",
92
+ agents: choices,
93
+ unavailableSessions,
94
+ page,
95
+ pages: Math.max(1, Math.ceil(agents.length / 20)),
96
+ writable: options.writable
97
+ } } : {}
98
+ };
99
+ }
100
+ if (command === "help") return reply(message, "/herdr menu\n/herdr list\n/herdr read HANDLE\n/herdr screen HANDLE\n/herdr send HANDLE -- TEXT\n/herdr watch HANDLE\n/herdr unwatch HANDLE");
101
+ const parts = ["send", "submit"].includes(command) ? /^(\S+)\s+--\s+([\s\S]+)$/.exec(argument) : null;
102
+ const handle = parts?.[1] ?? argument;
103
+ const binding = yield* Effect.try(() => bindingFor(handle, message, command === "refresh" || command === "unwatch"));
104
+ if (command === "unwatch") {
105
+ const removed = watches.delete(watchKey(message, binding.agent));
106
+ return detail(message, binding.agent, handle, removed ? "已取消关注;agent 仍在运行。" : "当前没有关注此会话。");
107
+ }
108
+ if (command === "open" || command === "compose" || command === "refresh") {
109
+ const current = yield* options.sessions.inspect(binding.agent);
110
+ if (command === "compose" && (!options.writable || current.kind !== "agent" || !current.ready || !["idle", "done"].includes(current.status))) return detail(message, current, handle, "当前不可发送指令,请先查看输出或刷新状态。");
111
+ const selected = command === "open" ? handle : bind(message, current, command === "compose");
112
+ return {
113
+ ...reply(message, `${current.name} | ${current.status}`),
114
+ menu: {
115
+ kind: command === "compose" ? "form" : "detail",
116
+ agents: [{
117
+ ...current,
118
+ handle: selected
119
+ }],
120
+ writable: options.writable
121
+ }
122
+ };
123
+ }
124
+ if (command === "read" || command === "screen") {
125
+ const read = yield* options.sessions.read(binding.agent, command === "screen");
126
+ const title = `${handle} | ${binding.agent.name}`;
127
+ const at = options.now();
128
+ return command === "screen" ? {
129
+ ...reply(message, `${title} — terminal text rendering`),
130
+ screen: {
131
+ title,
132
+ at,
133
+ ...read
134
+ }
135
+ } : reply(message, `${title} | ${new Date(at).toISOString()}${read.truncated || read.text.length > 12e3 ? " | truncated(已截断)" : ""}\n${read.text.slice(-12e3)}`);
136
+ }
137
+ if (command === "send" || command === "submit") {
138
+ if (command === "submit") {
139
+ if (!binding.form || !(yield* options.claim({
140
+ ...message,
141
+ id: `form:${handle}`
142
+ }))) return detail(message, binding.agent, handle, "此表单已提交或失效,请查看输出;新指令请重新打开输入框。");
143
+ }
144
+ if (!options.writable) return reply(message, "Terminal input is disabled for this deployment.");
145
+ const text = parts?.[2];
146
+ if (!text?.trim() || text.length > (command === "submit" ? 1e3 : 8e3) || Array.from(text).some((char) => {
147
+ const code = char.charCodeAt(0);
148
+ return code < 32 && code !== 9 && code !== 10 || code === 127;
149
+ })) return reply(message, "Use /herdr send HANDLE -- TEXT (1–8000 characters, no terminal control characters).");
150
+ const current = yield* options.sessions.inspect(binding.agent);
151
+ if (current.kind !== "agent" || !current.ready || !["idle", "done"].includes(current.status)) return detail(message, current, handle, `Input rejected: agent is ${current.status} or not ready. 请刷新状态后重新打开输入框。`);
152
+ return yield* options.sessions.prompt(binding.agent, text).pipe(Effect.as(detail(message, {
153
+ ...current,
154
+ ready: false
155
+ }, handle, "指令已提交一次;尚未确认 agent 已处理或完成,请查看输出。")), Effect.catchAll(() => Effect.succeed(detail(message, {
156
+ ...current,
157
+ ready: false
158
+ }, handle, "提交结果不确定(uncertain),不会自动重试。请先查看输出,确认后再发送新指令。"))));
159
+ }
160
+ if (command === "watch") {
161
+ for (const [key, watch] of watches) if (watch.expires <= options.now()) watches.delete(key);
162
+ const key = watchKey(message, binding.agent);
163
+ if (!watches.has(key) && watches.size >= 8) return detail(message, binding.agent, handle, "已达到 8 个关注目标,请先取消其他关注。");
164
+ const current = yield* options.sessions.inspect(binding.agent);
165
+ watches.set(key, {
166
+ chatId: message.chatId,
167
+ actorId: message.actorId,
168
+ binding,
169
+ expires: options.now() + 6e5,
170
+ status: current.status
171
+ });
172
+ return detail(message, current, handle, "已关注此会话 10 分钟;重复关注会续期,不增加名额。");
173
+ }
174
+ return reply(message, "Unknown command; use /herdr help.");
175
+ });
176
+ return {
177
+ handle: (message) => Effect.gen(function* () {
178
+ if (!allowed(message.actorId, message.chatId) || !message.text.trim().startsWith("/herdr")) return null;
179
+ const age = options.now() - message.sentAt;
180
+ if (!Number.isFinite(age) || age < -3e4 || age > 12e4) return null;
181
+ if (!(yield* options.claim(message))) return null;
182
+ return yield* execute(message).pipe(Effect.catchAll(() => Effect.succeed(recovery(message))));
183
+ }),
184
+ tick: () => Effect.gen(function* () {
185
+ const results = [];
186
+ for (const [key, watch] of watches) {
187
+ if (watch.expires <= options.now() || !allowed(watch.actorId, watch.chatId)) {
188
+ watches.delete(key);
189
+ continue;
190
+ }
191
+ const current = yield* options.sessions.inspect(watch.binding.agent).pipe(Effect.map((agent) => agent.status), Effect.catchAll(() => Effect.succeed("unavailable")));
192
+ if (current === watch.status) continue;
193
+ const handle = bind(watch, watch.binding.agent);
194
+ results.push({
195
+ reply: detail(watch, {
196
+ ...watch.binding.agent,
197
+ status: current,
198
+ ready: false
199
+ }, handle, `状态有变化 · ${new Date(options.now()).toISOString()}。这是观察结果,不代表任务成功;操作前请刷新状态。`),
200
+ acknowledge: () => {
201
+ if (watches.get(key) === watch) watch.status = current;
202
+ }
203
+ });
204
+ }
205
+ return results;
206
+ })
207
+ };
208
+ }
209
+ //#endregion
210
+ //#region src/adapters/herdr/session/herdr-socket-client.ts
211
+ function createHerdrSocketClient(sessions, timeoutMs = 5e3, discoverSessionsIn) {
212
+ if (!sessions.length && !discoverSessionsIn || new Set(sessions.map((s) => s.name)).size !== sessions.length) throw new Error("Herdr requires unique configured session names");
213
+ if (discoverSessionsIn && !isAbsolute(discoverSessionsIn)) throw new Error("Herdr discovery root must be absolute");
214
+ let configs = new Map(sessions.map((session) => [session.name, session]));
215
+ const refresh = () => Effect.tryPromise({
216
+ try: async () => {
217
+ const discovered = discoverSessionsIn ? await discoverSessions(discoverSessionsIn) : [];
218
+ const next = /* @__PURE__ */ new Map();
219
+ const paths = /* @__PURE__ */ new Set();
220
+ for (const config of [...sessions, ...discovered]) {
221
+ const path = resolve(config.socketPath);
222
+ if (paths.has(path)) continue;
223
+ if (next.has(config.name)) throw new Error("Herdr session aliases collide");
224
+ next.set(config.name, {
225
+ ...config,
226
+ socketPath: path
227
+ });
228
+ paths.add(path);
229
+ }
230
+ configs = next;
231
+ return [...next.keys()];
232
+ },
233
+ catch: (error) => error
234
+ });
235
+ const invoke = (session, method, params) => Effect.tryPromise({
236
+ try: async (signal) => {
237
+ const config = configs.get(session);
238
+ if (!config) throw new Error("Unknown Herdr session");
239
+ const socket = await lstat(config.socketPath);
240
+ if (!socket.isSocket() || (socket.mode & 63) !== 0 || socket.uid !== process.getuid?.()) throw new Error("Herdr socket must be private and owned by the current user");
241
+ return {
242
+ result: await request(config.socketPath, method, params, timeoutMs, signal),
243
+ generation: `${socket.dev}:${socket.ino}:${socket.birthtimeMs}`
244
+ };
245
+ },
246
+ catch: (error) => error
247
+ });
248
+ const listSession = (session) => invoke(session, "session.snapshot", {}).pipe(Effect.flatMap(({ result, generation }) => Effect.try(() => {
249
+ const snapshot = object(result.snapshot);
250
+ if (snapshot.protocol !== 22 || !Array.isArray(snapshot.panes) || !Array.isArray(snapshot.agents)) throw new Error("Herdr protocol 22 session snapshot required");
251
+ const agents = snapshot.agents.map(object);
252
+ const labels = (values, key) => new Map((Array.isArray(values) ? values : []).map((item) => {
253
+ const entry = object(item);
254
+ return [string(entry[key]), string(entry.label)];
255
+ }));
256
+ const workspaces = labels(snapshot.workspaces, "workspace_id");
257
+ const tabs = labels(snapshot.tabs, "tab_id");
258
+ return snapshot.panes.map((entry) => {
259
+ const value = object(entry);
260
+ const pane = string(value.pane_id);
261
+ const terminal = string(value.terminal_id);
262
+ const agent = agents.find((item) => item.pane_id === pane && item.terminal_id === terminal);
263
+ const kind = typeof value.agent === "string" && value.agent ? value.agent : "terminal";
264
+ const workspace = string(value.workspace_id);
265
+ const tab = string(value.tab_id);
266
+ const title = [
267
+ agent?.name,
268
+ value.label,
269
+ value.terminal_title_stripped,
270
+ value.title
271
+ ].find((item) => typeof item === "string" && item.length > 0) ?? kind;
272
+ return {
273
+ session,
274
+ pane,
275
+ identity: createHash("sha256").update(JSON.stringify([
276
+ generation,
277
+ session,
278
+ pane,
279
+ terminal,
280
+ kind,
281
+ value.agent_session ?? null
282
+ ])).digest("hex"),
283
+ kind: kind === "terminal" ? "terminal" : "agent",
284
+ name: `${pane} / ${workspaces.get(workspace) ?? workspace} / ${tabs.get(tab) ?? tab} / ${title}`,
285
+ workspace: workspaces.get(workspace) ?? workspace,
286
+ tab: tabs.get(tab) ?? tab,
287
+ title,
288
+ status: string(value.agent_status),
289
+ ready: kind !== "terminal" && agent?.interactive_ready === true
290
+ };
291
+ });
292
+ })));
293
+ const resolve$1 = (agent) => listSession(agent.session).pipe(Effect.flatMap((agents) => {
294
+ const current = agents.find((item) => item.pane === agent.pane && item.identity === agent.identity);
295
+ return current ? Effect.succeed(current) : Effect.fail(/* @__PURE__ */ new Error("Target changed; list agents again"));
296
+ }));
297
+ return {
298
+ list: () => refresh().pipe(Effect.flatMap((names) => Effect.all(names.map((name) => listSession(name).pipe(Effect.map((agents) => ({
299
+ agents,
300
+ unavailableSessions: []
301
+ })), Effect.catchAll(() => Effect.succeed({
302
+ agents: [],
303
+ unavailableSessions: [name]
304
+ })))), { concurrency: 4 })), Effect.map((results) => ({
305
+ agents: results.flatMap((result) => result.agents),
306
+ unavailableSessions: results.flatMap((result) => result.unavailableSessions)
307
+ }))),
308
+ inspect: resolve$1,
309
+ read: (agent, visible) => resolve$1(agent).pipe(Effect.flatMap(() => invoke(agent.session, "pane.read", {
310
+ pane_id: agent.pane,
311
+ source: visible ? "visible" : "recent_unwrapped",
312
+ format: "text",
313
+ lines: visible ? 80 : 120
314
+ })), Effect.flatMap(({ result }) => Effect.try(() => {
315
+ const read = object(result.read);
316
+ return {
317
+ text: string(read.text),
318
+ truncated: read.truncated === true
319
+ };
320
+ }))),
321
+ prompt: (agent, text) => resolve$1(agent).pipe(Effect.flatMap((current) => {
322
+ if (current.kind !== "agent" || !current.ready || !["idle", "done"].includes(current.status)) return Effect.fail(/* @__PURE__ */ new Error("Agent is not ready for a prompt"));
323
+ return invoke(agent.session, "agent.prompt", {
324
+ target: agent.pane,
325
+ text
326
+ }).pipe(Effect.asVoid);
327
+ }))
328
+ };
329
+ }
330
+ async function discoverSessions(root) {
331
+ const directory = await lstat(root);
332
+ if (!directory.isDirectory() || directory.uid !== process.getuid?.() || (directory.mode & 18) !== 0) throw new Error("Herdr discovery root must be an owned, non-writable directory");
333
+ const candidates = [{
334
+ name: "default",
335
+ socketPath: join(root, "herdr.sock")
336
+ }];
337
+ const named = join(root, "sessions");
338
+ const info = await lstat(named).catch((error) => {
339
+ if (error.code === "ENOENT") return null;
340
+ throw error;
341
+ });
342
+ if (info) {
343
+ if (!info.isDirectory() || info.uid !== process.getuid?.() || (info.mode & 18) !== 0) throw new Error("Invalid Herdr sessions directory");
344
+ for (const entry of (await readdir(named, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) if (entry.isDirectory()) candidates.push({
345
+ name: entry.name,
346
+ socketPath: join(named, entry.name, "herdr.sock")
347
+ });
348
+ }
349
+ const found = [];
350
+ for (const candidate of candidates) if (await lstat(candidate.socketPath).catch((error) => {
351
+ if (error.code === "ENOENT") return null;
352
+ throw error;
353
+ })) found.push(candidate);
354
+ return found;
355
+ }
356
+ function object(value) {
357
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Herdr response");
358
+ return value;
359
+ }
360
+ function string(value) {
361
+ if (typeof value !== "string") throw new Error("Invalid Herdr response field");
362
+ return value;
363
+ }
364
+ function request(path, method, params, timeoutMs, signal) {
365
+ return new Promise((resolve, reject) => {
366
+ const id = randomUUID();
367
+ const socket = createConnection({ path });
368
+ let settled = false;
369
+ let bytes = 0;
370
+ let buffer = "";
371
+ const finish = (error, result) => {
372
+ if (settled) return;
373
+ settled = true;
374
+ clearTimeout(timer);
375
+ signal.removeEventListener("abort", abort);
376
+ socket.destroy();
377
+ if (error) reject(error);
378
+ else resolve(result);
379
+ };
380
+ const abort = () => finish(/* @__PURE__ */ new Error("Herdr request aborted; input may have been submitted"));
381
+ const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("Herdr request timed out; input may have been submitted")), timeoutMs);
382
+ signal.addEventListener("abort", abort, { once: true });
383
+ socket.setEncoding("utf8");
384
+ socket.on("connect", () => socket.write(`${JSON.stringify({
385
+ id,
386
+ method,
387
+ params
388
+ })}\n`));
389
+ socket.on("error", (error) => finish(error));
390
+ socket.on("end", () => finish(/* @__PURE__ */ new Error("Herdr closed without a response; outcome unknown")));
391
+ socket.on("data", (chunk) => {
392
+ bytes += Buffer.byteLength(chunk);
393
+ if (bytes > 1048576) return finish(/* @__PURE__ */ new Error("Herdr response exceeds one MiB"));
394
+ buffer += chunk;
395
+ const newline = buffer.indexOf("\n");
396
+ if (newline < 0) return;
397
+ try {
398
+ const response = object(JSON.parse(buffer.slice(0, newline)));
399
+ if (response.id !== id) throw new Error("Herdr response identity mismatch");
400
+ if (response.error) throw new Error(`Herdr error: ${string(object(response.error).code)}`);
401
+ finish(void 0, object(response.result));
402
+ } catch (error) {
403
+ finish(error);
404
+ }
405
+ });
406
+ if (signal.aborted) abort();
407
+ });
408
+ }
409
+ //#endregion
410
+ //#region src/adapters/herdr/rendering/terminal-text-image.ts
411
+ async function renderTerminalTextImage(screen, fontFiles) {
412
+ const { renderAsync } = await import("@resvg/resvg-js");
413
+ const allLines = stripVTControlCharacters(screen.text).replace(/[^\P{Cc}\n\t]/gu, "").split("\n");
414
+ const expanded = allLines.map((line) => Array.from(line.replaceAll(" ", " ")));
415
+ const lines = expanded.slice(0, 80).map((line) => line.slice(0, 90).join(""));
416
+ const clipped = screen.truncated || allLines.length > 80 || expanded.some((line) => line.length > 90);
417
+ const title = Array.from(stripVTControlCharacters(screen.title).replace(/\p{Cc}/gu, "")).slice(0, 90).join("");
418
+ const width = 1800;
419
+ const height = 100 + Math.max(1, lines.length) * 24;
420
+ const label = `Terminal text rendering | ${new Date(screen.at).toISOString()}${clipped ? " | truncated" : ""}`;
421
+ const body = lines.map((line, i) => `<text x="24" y="${96 + i * 24}">${escapeXml(line)}</text>`).join("");
422
+ return (await renderAsync(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="100%" height="100%" fill="#101827"/><g fill="#e5e7eb" font-family="monospace" font-size="18" xml:space="preserve"><text x="24" y="30">${escapeXml(title)}</text><text x="24" y="58" fill="#94a3b8">${escapeXml(label)}</text>${body}</g></svg>`, { font: {
423
+ loadSystemFonts: !fontFiles?.length,
424
+ ...fontFiles ? { fontFiles: [...fontFiles] } : {},
425
+ defaultFontFamily: "monospace"
426
+ } })).asPng();
427
+ }
428
+ function escapeXml(text) {
429
+ return text.replace(/[&<>"']/g, (char) => ({
430
+ "&": "&amp;",
431
+ "<": "&lt;",
432
+ ">": "&gt;",
433
+ "\"": "&quot;",
434
+ "'": "&apos;"
435
+ })[char]);
436
+ }
437
+ //#endregion
438
+ //#region src/adapters/outbound/persistence/terminal-session/terminal-message-claims.ts
439
+ async function openTerminalMessageClaims(directory, appId) {
440
+ await mkdir(directory, {
441
+ recursive: true,
442
+ mode: 448
443
+ });
444
+ const lockPath = join(directory, "bridge.lock");
445
+ const lock = await open(lockPath, "wx", 384);
446
+ await lock.writeFile(JSON.stringify({
447
+ pid: process.pid,
448
+ appId
449
+ }));
450
+ const claims = join(directory, "claims");
451
+ try {
452
+ await mkdir(claims, {
453
+ recursive: true,
454
+ mode: 448
455
+ });
456
+ } catch (error) {
457
+ await lock.close();
458
+ await unlink(lockPath);
459
+ throw error;
460
+ }
461
+ return {
462
+ claim: (message) => Effect.tryPromise({
463
+ try: async () => {
464
+ const digest = createHash("sha256").update(JSON.stringify([appId, message.id])).digest("hex");
465
+ let file;
466
+ try {
467
+ file = await open(join(claims, digest), "wx", 384);
468
+ } catch (error) {
469
+ if (error.code === "EEXIST") return false;
470
+ throw error;
471
+ }
472
+ try {
473
+ await file.writeFile(JSON.stringify({
474
+ state: "claimed",
475
+ at: Date.now()
476
+ }));
477
+ await file.sync();
478
+ const folder = await open(claims, "r");
479
+ try {
480
+ await folder.sync();
481
+ } finally {
482
+ await folder.close();
483
+ }
484
+ return true;
485
+ } finally {
486
+ await file.close();
487
+ }
488
+ },
489
+ catch: (error) => error
490
+ }),
491
+ close: async () => {
492
+ await lock.close();
493
+ if (JSON.parse(await readFile(lockPath, "utf8")).pid === process.pid) await unlink(lockPath);
494
+ }
495
+ };
496
+ }
497
+ //#endregion
498
+ //#region src/adapters/feishu/terminal-session/feishu-terminal-session.ts
499
+ function parseFeishuTerminalMessage(event) {
500
+ if (!event || typeof event !== "object") return null;
501
+ const value = event;
502
+ const message = value.message;
503
+ if (value.sender?.sender_type !== "user" || !value.sender.sender_id?.open_id || !message?.message_id || !message.chat_id || message.message_type !== "text") return null;
504
+ try {
505
+ const content = JSON.parse(message.content ?? "{}");
506
+ if (!content || typeof content !== "object" || !("text" in content) || typeof content.text !== "string") return null;
507
+ return {
508
+ id: message.message_id,
509
+ actorId: value.sender.sender_id.open_id,
510
+ chatId: message.chat_id,
511
+ sentAt: Number(message.create_time),
512
+ text: content.text.replace(/^@_user_\d+\s*/, "")
513
+ };
514
+ } catch {
515
+ return null;
516
+ }
517
+ }
518
+ async function deliverTerminalBridgeReply(reply, delivery, render) {
519
+ if (reply.menu && delivery.sendCard) return delivery.sendCard(reply.chatId, renderTerminalMenu({
520
+ ...reply.menu,
521
+ ...reply.menu.kind === "recovery" ? { notice: reply.text } : {}
522
+ }));
523
+ if (!reply.screen) return delivery.sendText(reply.chatId, reply.text);
524
+ try {
525
+ await delivery.sendImage(reply.chatId, await render(reply.screen));
526
+ } catch {
527
+ await delivery.sendText(reply.chatId, `${reply.text} | ${new Date(reply.screen.at).toISOString()}${reply.screen.truncated || reply.screen.text.length > 12e3 ? " | truncated(已截断)" : ""}\n图片暂不可用,以下是终端文字:\n${reply.screen.text.slice(-12e3)}`);
528
+ }
529
+ }
530
+ function parseFeishuTerminalAction(event, menuChats) {
531
+ if (!event || typeof event !== "object") return null;
532
+ const value = event;
533
+ const actorId = value.operator?.open_id ?? value.operator?.operator_id?.open_id;
534
+ if (!actorId || !value.event_id || !value.create_time) return null;
535
+ let text;
536
+ let chatId;
537
+ if (value.event_key) {
538
+ if (!["herdr_list", "herdr_help"].includes(value.event_key)) return null;
539
+ chatId = menuChats[actorId];
540
+ text = value.event_key === "herdr_list" ? "/herdr menu" : "/herdr help";
541
+ } else {
542
+ chatId = value.context?.open_chat_id;
543
+ const action = value.action;
544
+ if (action?.tag !== "button") return null;
545
+ const form = /^herdr_submit_([a-f0-9]{12})$/.exec(action.name ?? "");
546
+ if (form) {
547
+ if (typeof action.form_value?.prompt !== "string") return null;
548
+ text = `/herdr submit ${form[1]} -- ${action.form_value.prompt}`;
549
+ } else {
550
+ const command = action.value?.command;
551
+ if (command === "menu") {
552
+ const page = action.value?.page ?? 0;
553
+ if (!Number.isSafeInteger(page) || page < 0) return null;
554
+ text = `/herdr menu ${page}`;
555
+ } else {
556
+ if (!command || ![
557
+ "open",
558
+ "refresh",
559
+ "compose",
560
+ "read",
561
+ "screen",
562
+ "watch",
563
+ "unwatch"
564
+ ].includes(command)) return null;
565
+ const handle = action.value?.handle;
566
+ if (typeof handle !== "string" || !/^[a-f0-9]{12}$/.test(handle)) return null;
567
+ text = `/herdr ${command} ${handle}`;
568
+ }
569
+ }
570
+ }
571
+ return chatId ? {
572
+ id: `action:${value.event_id}`,
573
+ actorId,
574
+ chatId,
575
+ sentAt: Number(value.create_time) / (value.event_key ? 1 : 1e3),
576
+ text
577
+ } : null;
578
+ }
579
+ function renderTerminalMenu(menu) {
580
+ const plain = (content) => ({
581
+ tag: "plain_text",
582
+ content
583
+ });
584
+ const button = (label, command, handle, page) => ({
585
+ tag: "button",
586
+ text: plain(label),
587
+ type: "default",
588
+ behaviors: [{
589
+ type: "callback",
590
+ value: {
591
+ command,
592
+ handle,
593
+ page
594
+ }
595
+ }]
596
+ });
597
+ const status = (agent) => {
598
+ if (agent.status === "unavailable") return "离线/暂不可用";
599
+ if (agent.kind === "terminal") return "终端只读";
600
+ if (agent.ready && ["idle", "done"].includes(agent.status)) return "等待输入";
601
+ return {
602
+ working: "执行中",
603
+ blocked: "等待处理",
604
+ idle: "空闲,尚未确认可输入",
605
+ done: "运行已结束,请刷新确认",
606
+ unknown: "状态未知"
607
+ }[agent.status] ?? "暂不可输入";
608
+ };
609
+ const elements = [];
610
+ if (menu.notice) elements.push({
611
+ tag: "div",
612
+ text: plain(menu.notice)
613
+ });
614
+ if (menu.kind === "list") {
615
+ elements.push({
616
+ tag: "markdown",
617
+ content: `**选择会话** · 第 ${(menu.page ?? 0) + 1}/${menu.pages ?? 1} 页`
618
+ });
619
+ const groups = Array$1.groupBy(menu.agents, (agent) => `${agent.session} / ${agent.workspace ?? "会话"}`);
620
+ for (const [group, agents] of Object.entries(groups)) {
621
+ elements.push({
622
+ tag: "div",
623
+ text: plain(group)
624
+ });
625
+ for (const agent of agents) elements.push(button(`${agent.pane} · ${agent.tab ? agent.tab + " / " : ""}${agent.title ?? agent.name} · ${status(agent)}`.slice(0, 100), "open", agent.handle));
626
+ }
627
+ if (menu.unavailableSessions?.length) elements.push({
628
+ tag: "div",
629
+ text: plain(`以下 session 无法读取,列表不完整:${menu.unavailableSessions.join(", ")}`)
630
+ });
631
+ if (!menu.agents.length) elements.push({
632
+ tag: "markdown",
633
+ content: "当前没有可用会话。"
634
+ });
635
+ if ((menu.page ?? 0) > 0) elements.push(button("上一页", "menu", void 0, menu.page - 1));
636
+ if ((menu.page ?? 0) + 1 < (menu.pages ?? 1)) elements.push(button("下一页", "menu", void 0, menu.page + 1));
637
+ } else if (menu.kind === "recovery") {
638
+ if (menu.agents[0]) elements.push(button("刷新原会话", "refresh", menu.agents[0].handle));
639
+ } else {
640
+ const agent = menu.agents[0];
641
+ elements.push({
642
+ tag: "div",
643
+ text: plain(`目标:${agent.session} / ${agent.name}\n状态:${status(agent)}`)
644
+ });
645
+ if (menu.kind === "form") {
646
+ elements.push({
647
+ tag: "form",
648
+ name: "herdr_prompt",
649
+ elements: [{
650
+ tag: "input",
651
+ name: "prompt",
652
+ required: true,
653
+ input_type: "multiline_text",
654
+ max_length: 1e3,
655
+ placeholder: plain("输入要交给此 agent 的指令"),
656
+ label: plain("指令内容")
657
+ }, {
658
+ tag: "button",
659
+ name: `herdr_submit_${agent.handle}`,
660
+ text: plain("发送"),
661
+ type: "primary_filled",
662
+ form_action_type: "submit"
663
+ }]
664
+ });
665
+ elements.push({
666
+ tag: "markdown",
667
+ content: "每个表单只提交一次;提交后可通过“看输出”核对执行结果。"
668
+ });
669
+ } else {
670
+ if (menu.writable && agent.kind === "agent" && agent.ready && ["idle", "done"].includes(agent.status)) elements.push(button("发指令", "compose", agent.handle));
671
+ else elements.push({
672
+ tag: "div",
673
+ text: plain(menu.writable && agent.kind === "agent" ? "当前不可发送指令。请查看输出,或刷新状态后再试。" : "此会话为只读模式。")
674
+ });
675
+ elements.push(button("关注状态", "watch", agent.handle), button("取消关注", "unwatch", agent.handle));
676
+ }
677
+ elements.push(button("看画面", "screen", agent.handle), button("看输出", "read", agent.handle), button("刷新当前会话", "refresh", agent.handle));
678
+ }
679
+ elements.push(button("刷新会话列表", "menu"));
680
+ return {
681
+ schema: "2.0",
682
+ config: {
683
+ width_mode: "default",
684
+ enable_forward: false
685
+ },
686
+ header: {
687
+ title: plain(menu.kind === "form" ? "向会话发送指令" : "Herdr 会话"),
688
+ template: "blue"
689
+ },
690
+ body: { elements }
691
+ };
692
+ }
693
+ function createFeishuTerminalDelivery(client) {
694
+ return {
695
+ sendCard: async (chatId, card) => {
696
+ if ((await client.im.message.create({
697
+ params: { receive_id_type: "chat_id" },
698
+ data: {
699
+ receive_id: chatId,
700
+ msg_type: "interactive",
701
+ content: JSON.stringify(card)
702
+ }
703
+ })).code !== 0) throw new Error("Feishu card delivery failed");
704
+ },
705
+ sendText: async (chatId, text) => {
706
+ if ((await client.im.message.create({
707
+ params: { receive_id_type: "chat_id" },
708
+ data: {
709
+ receive_id: chatId,
710
+ msg_type: "text",
711
+ content: JSON.stringify({ text })
712
+ }
713
+ })).code !== 0) throw new Error("Feishu text delivery failed");
714
+ },
715
+ sendImage: async (chatId, bytes) => {
716
+ const value = await client.im.image.create({ data: {
717
+ image_type: "message",
718
+ image: Buffer.from(bytes)
719
+ } });
720
+ const imageKey = value?.image_key ?? value?.data?.image_key;
721
+ if (!imageKey) throw new Error("Feishu image upload failed");
722
+ if ((await client.im.message.create({
723
+ params: { receive_id_type: "chat_id" },
724
+ data: {
725
+ receive_id: chatId,
726
+ msg_type: "image",
727
+ content: JSON.stringify({ image_key: imageKey })
728
+ }
729
+ })).code !== 0) throw new Error("Feishu image delivery failed");
730
+ }
731
+ };
732
+ }
733
+ //#endregion
734
+ //#region src/bootstrap/herdr-feishu.ts
735
+ const configSchema = Schema.Struct({
736
+ appId: Schema.NonEmptyTrimmedString,
737
+ appSecret: Schema.NonEmptyTrimmedString,
738
+ sessions: Schema.optional(Schema.Array(Schema.Struct({
739
+ name: Schema.NonEmptyTrimmedString,
740
+ socketPath: Schema.String.pipe(Schema.filter(isAbsolute))
741
+ }))),
742
+ discoverSessionsIn: Schema.optional(Schema.String.pipe(Schema.filter(isAbsolute))),
743
+ allowedActors: Schema.NonEmptyArray(Schema.NonEmptyTrimmedString),
744
+ allowedChats: Schema.NonEmptyArray(Schema.NonEmptyTrimmedString),
745
+ menuChats: Schema.optional(Schema.Record({
746
+ key: Schema.String,
747
+ value: Schema.NonEmptyTrimmedString
748
+ })),
749
+ stateDirectory: Schema.String.pipe(Schema.filter(isAbsolute)),
750
+ writable: Schema.optional(Schema.Boolean),
751
+ fontFiles: Schema.optional(Schema.Array(Schema.String.pipe(Schema.filter(isAbsolute))))
752
+ });
753
+ function configuredSessions(options) {
754
+ if (!Schema.is(configSchema)(options)) throw new Error("Invalid bridge configuration: check credentials, nonempty actor/chat lists, absolute paths and option types");
755
+ for (const [actor, chat] of Object.entries(options.menuChats ?? {})) if (!options.allowedActors.includes(actor) || !options.allowedChats.includes(chat)) throw new Error("Menu destinations must be allowlisted");
756
+ return createHerdrSocketClient(options.sessions ?? [], 5e3, options.discoverSessionsIn);
757
+ }
758
+ async function checkHerdrFeishuBridge(options) {
759
+ const inventory = await Effect.runPromise(configuredSessions(options).list());
760
+ return {
761
+ panels: inventory.agents.length,
762
+ sessionsWithPanels: new Set(inventory.agents.map((agent) => agent.session)).size,
763
+ unavailableSessions: inventory.unavailableSessions,
764
+ writable: options.writable === true,
765
+ feishuVerified: false
766
+ };
767
+ }
768
+ async function startHerdrFeishuBridge(options) {
769
+ const sessions = configuredSessions(options);
770
+ const claims = await openTerminalMessageClaims(options.stateDirectory, options.appId);
771
+ const bridge = createTerminalSessionBridge({
772
+ sessions,
773
+ actors: options.allowedActors,
774
+ chats: options.allowedChats,
775
+ writable: options.writable === true,
776
+ now: Date.now,
777
+ newHandle: () => randomBytes(6).toString("hex"),
778
+ claim: claims.claim
779
+ });
780
+ const client = new Client({
781
+ appId: options.appId,
782
+ appSecret: options.appSecret,
783
+ loggerLevel: LoggerLevel.error
784
+ });
785
+ const ws = new WSClient({
786
+ appId: options.appId,
787
+ appSecret: options.appSecret,
788
+ loggerLevel: LoggerLevel.error
789
+ });
790
+ const delivery = createFeishuTerminalDelivery(client);
791
+ let stopped = false;
792
+ let pending = 0;
793
+ let tail = Promise.resolve();
794
+ const enqueue = (run) => {
795
+ if (stopped || pending >= 32) return false;
796
+ pending++;
797
+ tail = tail.then(run).catch(() => options.onObservation?.({ type: "operation_failed" })).finally(() => {
798
+ pending--;
799
+ });
800
+ return true;
801
+ };
802
+ const receive = async (message) => {
803
+ if (!message) return;
804
+ options.onObservation?.({
805
+ type: "received",
806
+ messageId: message.id
807
+ });
808
+ if (!enqueue(async () => {
809
+ const response = await Effect.runPromise(bridge.handle(message));
810
+ if (response) {
811
+ await deliverTerminalBridgeReply(response, delivery, (screen) => renderTerminalTextImage(screen, options.fontFiles));
812
+ options.onObservation?.({
813
+ type: "replied",
814
+ messageId: message.id
815
+ });
816
+ }
817
+ })) throw new Error("Herdr bridge queue is full or stopped");
818
+ };
819
+ const dispatcher = new EventDispatcher({}).register({
820
+ "im.message.receive_v1": (event) => receive(parseFeishuTerminalMessage(event)),
821
+ "application.bot.menu_v6": (event) => receive(parseFeishuTerminalAction(event, options.menuChats ?? {})),
822
+ "card.action.trigger": async (event) => {
823
+ await receive(parseFeishuTerminalAction(event, options.menuChats ?? {}));
824
+ return {};
825
+ }
826
+ });
827
+ try {
828
+ await ws.start({ eventDispatcher: dispatcher });
829
+ } catch (error) {
830
+ ws.close({ force: true });
831
+ await claims.close();
832
+ throw error;
833
+ }
834
+ const timer = setInterval(() => {
835
+ if (pending) return;
836
+ enqueue(async () => {
837
+ for (const notification of await Effect.runPromise(bridge.tick())) try {
838
+ await deliverTerminalBridgeReply(notification.reply, delivery, (screen) => renderTerminalTextImage(screen, options.fontFiles));
839
+ notification.acknowledge();
840
+ } catch {
841
+ options.onObservation?.({ type: "notification_failed" });
842
+ }
843
+ });
844
+ }, 5e3);
845
+ return { stop: async () => {
846
+ if (stopped) return;
847
+ stopped = true;
848
+ clearInterval(timer);
849
+ ws.close({ force: true });
850
+ await tail;
851
+ await claims.close();
852
+ } };
853
+ }
854
+ //#endregion
855
+ export { checkHerdrFeishuBridge, startHerdrFeishuBridge };
856
+