@rivus/gateway 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/herdr-feishu.js +24 -794
- package/dist/bootstrap/pi-feishu.js +992 -110
- package/dist/chunks/herdr-feishu-runtime.js +1993 -0
- package/dist/chunks/runtime-cache.js +1 -1
- package/package.json +4 -3
|
@@ -1,752 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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";
|
|
1
|
+
import { a as openTerminalBindingsStore, d as parseFeishuTerminalMessage, i as openTerminalMessageClaims, l as createFeishuTerminalDelivery, r as standaloneHerdrOptionsSchema, s as createHerdrSocketClient, t as createHerdrFeishuBridgeRuntime, u as parseFeishuTerminalAction } from "../chunks/herdr-feishu-runtime.js";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
6
3
|
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.catch(() => 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.catch(() => 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.catch(() => 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.catch(() => 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
|
-
"&": "&",
|
|
431
|
-
"<": "<",
|
|
432
|
-
">": ">",
|
|
433
|
-
"\"": """,
|
|
434
|
-
"'": "'"
|
|
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
4
|
//#region src/bootstrap/herdr-feishu.ts
|
|
735
|
-
const configSchema =
|
|
736
|
-
appId: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
737
|
-
appSecret: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
738
|
-
sessions: Schema.optional(Schema.Array(Schema.Struct({
|
|
739
|
-
name: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
740
|
-
socketPath: Schema.String.check(Schema.makeFilter(isAbsolute))
|
|
741
|
-
}))),
|
|
742
|
-
discoverSessionsIn: Schema.optional(Schema.String.check(Schema.makeFilter(isAbsolute))),
|
|
743
|
-
allowedActors: Schema.NonEmptyArray(Schema.Trimmed.check(Schema.isNonEmpty())),
|
|
744
|
-
allowedChats: Schema.NonEmptyArray(Schema.Trimmed.check(Schema.isNonEmpty())),
|
|
745
|
-
menuChats: Schema.optional(Schema.Record(Schema.String, Schema.Trimmed.check(Schema.isNonEmpty()))),
|
|
746
|
-
stateDirectory: Schema.String.check(Schema.makeFilter(isAbsolute)),
|
|
747
|
-
writable: Schema.optional(Schema.Boolean),
|
|
748
|
-
fontFiles: Schema.optional(Schema.Array(Schema.String.check(Schema.makeFilter(isAbsolute))))
|
|
749
|
-
});
|
|
5
|
+
const configSchema = standaloneHerdrOptionsSchema;
|
|
750
6
|
function configuredSessions(options) {
|
|
751
7
|
if (!Schema.is(configSchema)(options)) throw new Error("Invalid bridge configuration: check credentials, nonempty actor/chat lists, absolute paths and option types");
|
|
752
8
|
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");
|
|
@@ -765,14 +21,9 @@ async function checkHerdrFeishuBridge(options) {
|
|
|
765
21
|
async function startHerdrFeishuBridge(options) {
|
|
766
22
|
const sessions = configuredSessions(options);
|
|
767
23
|
const claims = await openTerminalMessageClaims(options.stateDirectory, options.appId);
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
chats: options.allowedChats,
|
|
772
|
-
writable: options.writable === true,
|
|
773
|
-
now: Date.now,
|
|
774
|
-
newHandle: () => randomBytes(6).toString("hex"),
|
|
775
|
-
claim: claims.claim
|
|
24
|
+
const bindings = await openTerminalBindingsStore(options.stateDirectory, options.appId).catch(async (error) => {
|
|
25
|
+
await claims.close();
|
|
26
|
+
throw error;
|
|
776
27
|
});
|
|
777
28
|
const client = new Client({
|
|
778
29
|
appId: options.appId,
|
|
@@ -784,34 +35,27 @@ async function startHerdrFeishuBridge(options) {
|
|
|
784
35
|
appSecret: options.appSecret,
|
|
785
36
|
loggerLevel: LoggerLevel.error
|
|
786
37
|
});
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
38
|
+
const stopState = { stopped: false };
|
|
39
|
+
const runtime = createHerdrFeishuBridgeRuntime({
|
|
40
|
+
sessions,
|
|
41
|
+
actors: options.allowedActors,
|
|
42
|
+
chats: options.allowedChats,
|
|
43
|
+
writable: options.writable === true,
|
|
44
|
+
claim: claims.claim,
|
|
45
|
+
bindings,
|
|
46
|
+
delivery: createFeishuTerminalDelivery(client, { isStopped: () => stopState.stopped }),
|
|
47
|
+
stopState,
|
|
48
|
+
...options.fontFiles === void 0 ? {} : { fontFiles: options.fontFiles },
|
|
49
|
+
...options.onObservation === void 0 ? {} : { onObservation: options.onObservation },
|
|
50
|
+
closeStores: () => claims.close()
|
|
51
|
+
});
|
|
799
52
|
const receive = async (message) => {
|
|
800
53
|
if (!message) return;
|
|
801
54
|
options.onObservation?.({
|
|
802
55
|
type: "received",
|
|
803
56
|
messageId: message.id
|
|
804
57
|
});
|
|
805
|
-
if (!
|
|
806
|
-
const response = await Effect.runPromise(bridge.handle(message));
|
|
807
|
-
if (response) {
|
|
808
|
-
await deliverTerminalBridgeReply(response, delivery, (screen) => renderTerminalTextImage(screen, options.fontFiles));
|
|
809
|
-
options.onObservation?.({
|
|
810
|
-
type: "replied",
|
|
811
|
-
messageId: message.id
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
})) throw new Error("Herdr bridge queue is full or stopped");
|
|
58
|
+
if (!await runtime.submit(message)) throw new Error("Herdr bridge queue is full or stopped");
|
|
815
59
|
};
|
|
816
60
|
const dispatcher = new EventDispatcher({}).register({
|
|
817
61
|
"im.message.receive_v1": (event) => receive(parseFeishuTerminalMessage(event)),
|
|
@@ -823,29 +67,15 @@ async function startHerdrFeishuBridge(options) {
|
|
|
823
67
|
});
|
|
824
68
|
try {
|
|
825
69
|
await ws.start({ eventDispatcher: dispatcher });
|
|
70
|
+
await runtime.start();
|
|
826
71
|
} catch (error) {
|
|
827
72
|
ws.close({ force: true });
|
|
828
|
-
await
|
|
73
|
+
await runtime.stop().catch(() => void 0);
|
|
829
74
|
throw error;
|
|
830
75
|
}
|
|
831
|
-
const timer = setInterval(() => {
|
|
832
|
-
if (pending) return;
|
|
833
|
-
enqueue(async () => {
|
|
834
|
-
for (const notification of await Effect.runPromise(bridge.tick())) try {
|
|
835
|
-
await deliverTerminalBridgeReply(notification.reply, delivery, (screen) => renderTerminalTextImage(screen, options.fontFiles));
|
|
836
|
-
notification.acknowledge();
|
|
837
|
-
} catch {
|
|
838
|
-
options.onObservation?.({ type: "notification_failed" });
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
}, 5e3);
|
|
842
76
|
return { stop: async () => {
|
|
843
|
-
if (stopped) return;
|
|
844
|
-
stopped = true;
|
|
845
|
-
clearInterval(timer);
|
|
846
77
|
ws.close({ force: true });
|
|
847
|
-
await
|
|
848
|
-
await claims.close();
|
|
78
|
+
await runtime.stop();
|
|
849
79
|
} };
|
|
850
80
|
}
|
|
851
81
|
//#endregion
|