im-code-agent 0.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/config.env.example +11 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +33 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +2 -0
- package/dist/src-CcW-Vja8.mjs +3452 -0
- package/package.json +41 -0
|
@@ -0,0 +1,3452 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import lark from "@larksuiteoapi/node-sdk";
|
|
10
|
+
//#region src/agent/agent-adapter.ts
|
|
11
|
+
function resolveAgentCommand(config, agentType) {
|
|
12
|
+
const command = config.agents[agentType];
|
|
13
|
+
if (!command) throw new Error(`Agent not configured: ${agentType}`);
|
|
14
|
+
return command;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/agent/agent-process.ts
|
|
18
|
+
const ACP_TOOL_CALL_STANDARD_FIELDS = [
|
|
19
|
+
"toolCallId",
|
|
20
|
+
"kind",
|
|
21
|
+
"status",
|
|
22
|
+
"title",
|
|
23
|
+
"content",
|
|
24
|
+
"content[].type",
|
|
25
|
+
"locations",
|
|
26
|
+
"locations[].path",
|
|
27
|
+
"locations[].line",
|
|
28
|
+
"rawInput",
|
|
29
|
+
"rawOutput",
|
|
30
|
+
"_meta"
|
|
31
|
+
];
|
|
32
|
+
const ACP_REQUEST_PERMISSION_STANDARD_FIELDS = [
|
|
33
|
+
"sessionId",
|
|
34
|
+
"toolCall",
|
|
35
|
+
"toolCall.toolCallId",
|
|
36
|
+
"toolCall.kind",
|
|
37
|
+
"toolCall.status",
|
|
38
|
+
"toolCall.title",
|
|
39
|
+
"toolCall.content",
|
|
40
|
+
"toolCall.locations",
|
|
41
|
+
"toolCall.rawInput",
|
|
42
|
+
"toolCall.rawOutput",
|
|
43
|
+
"toolCall._meta",
|
|
44
|
+
"options",
|
|
45
|
+
"options[].optionId",
|
|
46
|
+
"options[].name",
|
|
47
|
+
"options[].kind",
|
|
48
|
+
"options[]._meta",
|
|
49
|
+
"_meta"
|
|
50
|
+
];
|
|
51
|
+
function collectFieldPaths(value, basePath = "") {
|
|
52
|
+
const paths = /* @__PURE__ */ new Set();
|
|
53
|
+
const walk = (node, path) => {
|
|
54
|
+
if (node === null || node === void 0) return;
|
|
55
|
+
if (Array.isArray(node)) {
|
|
56
|
+
if (path) paths.add(`${path}[]`);
|
|
57
|
+
for (const item of node) walk(item, path ? `${path}[]` : "[]");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (typeof node !== "object") return;
|
|
61
|
+
for (const [key, child] of Object.entries(node)) {
|
|
62
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
63
|
+
paths.add(nextPath);
|
|
64
|
+
walk(child, nextPath);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(value, basePath);
|
|
68
|
+
return Array.from(paths).sort();
|
|
69
|
+
}
|
|
70
|
+
function summarizePermissionRequest(params, rawParams) {
|
|
71
|
+
return {
|
|
72
|
+
topLevelKeys: rawParams && typeof rawParams === "object" ? Object.keys(rawParams) : [],
|
|
73
|
+
fieldPaths: collectFieldPaths(rawParams),
|
|
74
|
+
standardFields: ACP_REQUEST_PERMISSION_STANDARD_FIELDS,
|
|
75
|
+
sessionId: params.sessionId,
|
|
76
|
+
optionsCount: params.options.length,
|
|
77
|
+
options: params.options.map((item) => ({
|
|
78
|
+
optionId: item.id,
|
|
79
|
+
name: item.name,
|
|
80
|
+
kind: item.kind
|
|
81
|
+
}))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function normalizePermissionParams(raw) {
|
|
85
|
+
if (!raw || typeof raw !== "object") return {};
|
|
86
|
+
const obj = raw;
|
|
87
|
+
const sessionId = typeof obj.sessionId === "string" ? obj.sessionId : void 0;
|
|
88
|
+
const toolCall = obj.toolCall;
|
|
89
|
+
const options = ((Array.isArray(obj.options) ? obj.options : void 0) ?? []).map((item) => {
|
|
90
|
+
if (!item || typeof item !== "object") return;
|
|
91
|
+
const opt = item;
|
|
92
|
+
const optionId = typeof opt.optionId === "string" ? opt.optionId : void 0;
|
|
93
|
+
const name = typeof opt.name === "string" ? opt.name : void 0;
|
|
94
|
+
const kind = typeof opt.kind === "string" ? opt.kind : void 0;
|
|
95
|
+
if (!optionId || !name || !kind) return;
|
|
96
|
+
return {
|
|
97
|
+
id: optionId,
|
|
98
|
+
name,
|
|
99
|
+
kind
|
|
100
|
+
};
|
|
101
|
+
}).filter((item) => Boolean(item));
|
|
102
|
+
if (!sessionId || !toolCall) return {};
|
|
103
|
+
return { params: {
|
|
104
|
+
sessionId,
|
|
105
|
+
toolCall,
|
|
106
|
+
options
|
|
107
|
+
} };
|
|
108
|
+
}
|
|
109
|
+
var AgentProcessError = class extends Error {
|
|
110
|
+
constructor(code, message) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.code = code;
|
|
113
|
+
this.name = "AgentProcessError";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var AgentProcess = class {
|
|
117
|
+
#children = /* @__PURE__ */ new Map();
|
|
118
|
+
#defaultRequestTimeoutMs = 3e4;
|
|
119
|
+
#promptIdleTimeoutMs = 12e4;
|
|
120
|
+
#toolUpdateLogDedup = /* @__PURE__ */ new Set();
|
|
121
|
+
constructor(config, logger, onEvent, onPermissionRequest) {
|
|
122
|
+
this.config = config;
|
|
123
|
+
this.logger = logger;
|
|
124
|
+
this.onEvent = onEvent;
|
|
125
|
+
this.onPermissionRequest = onPermissionRequest;
|
|
126
|
+
}
|
|
127
|
+
async checkHealth(agent) {
|
|
128
|
+
const command = resolveAgentCommand(this.config, agent);
|
|
129
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
130
|
+
const notes = [];
|
|
131
|
+
const commandAvailable = await this.isCommandAvailable(command.command);
|
|
132
|
+
if (!commandAvailable) notes.push(`Command is not accessible: ${command.command}`);
|
|
133
|
+
let cliVersion;
|
|
134
|
+
if (agent === "codex") {
|
|
135
|
+
const result = spawnSync("codex", ["--version"], { encoding: "utf8" });
|
|
136
|
+
if (result.status === 0) cliVersion = result.stdout.trim();
|
|
137
|
+
else notes.push("codex CLI is installed but `codex --version` did not exit cleanly");
|
|
138
|
+
}
|
|
139
|
+
const hasStoredAuth = spawnSync("test", ["-f", `${process.env.HOME}/.codex/auth.json`], { stdio: "ignore" }).status === 0;
|
|
140
|
+
if (!hasStoredAuth) notes.push("No ~/.codex/auth.json detected");
|
|
141
|
+
let status = "ready";
|
|
142
|
+
if (!commandAvailable) status = "unavailable";
|
|
143
|
+
else if (!hasStoredAuth) status = "degraded";
|
|
144
|
+
return {
|
|
145
|
+
agent,
|
|
146
|
+
status,
|
|
147
|
+
commandAvailable,
|
|
148
|
+
cliVersion,
|
|
149
|
+
hasStoredAuth,
|
|
150
|
+
notes,
|
|
151
|
+
checkedAt
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async start(options) {
|
|
155
|
+
if (this.#children.has(options.taskId)) throw new Error(`Task is already running: ${options.taskId}`);
|
|
156
|
+
const command = resolveAgentCommand(this.config, options.agent);
|
|
157
|
+
const spawnArgs = [...command.args ?? [], ...options.runtimeArgs ?? []];
|
|
158
|
+
const health = await this.checkHealth(options.agent);
|
|
159
|
+
if (!health.commandAvailable) throw new AgentProcessError("agent_command_unavailable", health.notes.join("; "));
|
|
160
|
+
const child = spawn(command.command, spawnArgs, {
|
|
161
|
+
cwd: options.cwd,
|
|
162
|
+
env: {
|
|
163
|
+
...process.env,
|
|
164
|
+
...command.env
|
|
165
|
+
},
|
|
166
|
+
stdio: "pipe"
|
|
167
|
+
});
|
|
168
|
+
const agentChild = {
|
|
169
|
+
child,
|
|
170
|
+
nextRequestId: 1,
|
|
171
|
+
pendingRequests: /* @__PURE__ */ new Map(),
|
|
172
|
+
stdoutBuffer: "",
|
|
173
|
+
stderrChunks: []
|
|
174
|
+
};
|
|
175
|
+
this.#children.set(options.taskId, agentChild);
|
|
176
|
+
child.stdout.on("data", (chunk) => {
|
|
177
|
+
this.handleStdout(options.taskId, chunk.toString());
|
|
178
|
+
});
|
|
179
|
+
child.stderr.on("data", (chunk) => {
|
|
180
|
+
agentChild.stderrChunks.push(chunk.toString());
|
|
181
|
+
this.logger.warn("agent stderr", {
|
|
182
|
+
taskId: options.taskId,
|
|
183
|
+
chunk: chunk.toString()
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
child.on("spawn", () => {
|
|
187
|
+
this.logger.info("agent process spawned", {
|
|
188
|
+
taskId: options.taskId,
|
|
189
|
+
agent: options.agent,
|
|
190
|
+
command: command.command,
|
|
191
|
+
args: spawnArgs,
|
|
192
|
+
cwd: options.cwd
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
child.on("error", (error) => {
|
|
196
|
+
this.rejectAllPendingRequests(options.taskId, `agent process spawn failed: ${error.message}`);
|
|
197
|
+
this.#children.delete(options.taskId);
|
|
198
|
+
this.clearToolUpdateLogCache(options.taskId);
|
|
199
|
+
this.logger.error("agent process error", {
|
|
200
|
+
taskId: options.taskId,
|
|
201
|
+
error: error.message
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
child.on("exit", (code, signal) => {
|
|
205
|
+
this.rejectAllPendingRequests(options.taskId, "agent process exited");
|
|
206
|
+
this.#children.delete(options.taskId);
|
|
207
|
+
this.clearToolUpdateLogCache(options.taskId);
|
|
208
|
+
this.logger.info("agent process exited", {
|
|
209
|
+
taskId: options.taskId,
|
|
210
|
+
code,
|
|
211
|
+
signal
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
const initialization = await this.initialize(options.taskId);
|
|
215
|
+
agentChild.initialization = initialization;
|
|
216
|
+
let sessionId;
|
|
217
|
+
let loadResult;
|
|
218
|
+
if (options.resumeSessionId) try {
|
|
219
|
+
loadResult = await this.loadSession(options.taskId, options.resumeSessionId, options.cwd);
|
|
220
|
+
sessionId = options.resumeSessionId;
|
|
221
|
+
this.logger.info("agent session resumed", {
|
|
222
|
+
taskId: options.taskId,
|
|
223
|
+
sessionId,
|
|
224
|
+
cwd: options.cwd
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.logger.warn("agent session resume failed, fallback to new session", {
|
|
228
|
+
taskId: options.taskId,
|
|
229
|
+
resumeSessionId: options.resumeSessionId,
|
|
230
|
+
error: error instanceof Error ? error.message : String(error)
|
|
231
|
+
});
|
|
232
|
+
const session = await this.newSession(options.taskId, options.cwd);
|
|
233
|
+
loadResult = session;
|
|
234
|
+
sessionId = session.sessionId;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const session = await this.newSession(options.taskId, options.cwd);
|
|
238
|
+
loadResult = session;
|
|
239
|
+
sessionId = session.sessionId;
|
|
240
|
+
}
|
|
241
|
+
agentChild.sessionId = sessionId;
|
|
242
|
+
this.logger.info("agent initialized", {
|
|
243
|
+
taskId: options.taskId,
|
|
244
|
+
protocolVersion: initialization.protocolVersion,
|
|
245
|
+
agentName: initialization.agentInfo?.name,
|
|
246
|
+
agentVersion: initialization.agentInfo?.version,
|
|
247
|
+
sessionId
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
initialization,
|
|
251
|
+
sessionId,
|
|
252
|
+
models: loadResult.models
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async isCommandAvailable(command) {
|
|
256
|
+
if (command.includes("/")) try {
|
|
257
|
+
await access(command);
|
|
258
|
+
return true;
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return spawnSync("which", [command], { stdio: "ignore" }).status === 0;
|
|
263
|
+
}
|
|
264
|
+
async stop(taskId) {
|
|
265
|
+
const agentChild = this.#children.get(taskId);
|
|
266
|
+
if (!agentChild) return;
|
|
267
|
+
this.rejectAllPendingRequests(taskId, "agent process stopped");
|
|
268
|
+
agentChild.child.kill("SIGTERM");
|
|
269
|
+
this.#children.delete(taskId);
|
|
270
|
+
this.clearToolUpdateLogCache(taskId);
|
|
271
|
+
}
|
|
272
|
+
async initialize(taskId) {
|
|
273
|
+
return this.sendRequest(taskId, "initialize", {
|
|
274
|
+
protocolVersion: 1,
|
|
275
|
+
clientCapabilities: {
|
|
276
|
+
fs: {
|
|
277
|
+
readTextFile: false,
|
|
278
|
+
writeTextFile: false
|
|
279
|
+
},
|
|
280
|
+
terminal: false
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async prompt(taskId, promptText) {
|
|
285
|
+
const agentChild = this.getChild(taskId);
|
|
286
|
+
if (!agentChild.sessionId) throw new Error(`Agent session not initialized for task: ${taskId}`);
|
|
287
|
+
const params = {
|
|
288
|
+
sessionId: agentChild.sessionId,
|
|
289
|
+
prompt: [{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: promptText
|
|
292
|
+
}]
|
|
293
|
+
};
|
|
294
|
+
return this.sendRequest(taskId, "session/prompt", params);
|
|
295
|
+
}
|
|
296
|
+
async setSessionModel(taskId, modelId) {
|
|
297
|
+
const agentChild = this.getChild(taskId);
|
|
298
|
+
if (!agentChild.sessionId) throw new Error(`Agent session not initialized for task: ${taskId}`);
|
|
299
|
+
const params = {
|
|
300
|
+
sessionId: agentChild.sessionId,
|
|
301
|
+
modelId
|
|
302
|
+
};
|
|
303
|
+
await this.sendRequest(taskId, "session/set_model", params);
|
|
304
|
+
}
|
|
305
|
+
async newSession(taskId, cwd) {
|
|
306
|
+
const params = {
|
|
307
|
+
cwd,
|
|
308
|
+
mcpServers: []
|
|
309
|
+
};
|
|
310
|
+
try {
|
|
311
|
+
return await this.sendRequest(taskId, "session/new", params);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw this.mapRequestError(taskId, "session/new", error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async loadSession(taskId, sessionId, cwd) {
|
|
317
|
+
const params = {
|
|
318
|
+
sessionId,
|
|
319
|
+
cwd,
|
|
320
|
+
mcpServers: []
|
|
321
|
+
};
|
|
322
|
+
try {
|
|
323
|
+
return await this.sendRequest(taskId, "session/load", params);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw this.mapRequestError(taskId, "session/load", error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async sendRequest(taskId, method, params) {
|
|
329
|
+
const agentChild = this.getChild(taskId);
|
|
330
|
+
const id = agentChild.nextRequestId++;
|
|
331
|
+
const payload = {
|
|
332
|
+
jsonrpc: "2.0",
|
|
333
|
+
id,
|
|
334
|
+
method,
|
|
335
|
+
...params === void 0 ? {} : { params }
|
|
336
|
+
};
|
|
337
|
+
return new Promise((resolve, reject) => {
|
|
338
|
+
const timeoutMs = method === "session/prompt" ? this.#promptIdleTimeoutMs : this.#defaultRequestTimeoutMs;
|
|
339
|
+
const timer = this.createRequestTimer(taskId, id, method, timeoutMs);
|
|
340
|
+
agentChild.pendingRequests.set(id, {
|
|
341
|
+
method,
|
|
342
|
+
timer,
|
|
343
|
+
timeoutMs,
|
|
344
|
+
resolve: (value) => resolve(value),
|
|
345
|
+
reject
|
|
346
|
+
});
|
|
347
|
+
this.writeMessage(taskId, payload);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
writeMessage(taskId, message) {
|
|
351
|
+
this.getChild(taskId).child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
352
|
+
this.logger.info("agent request sent", {
|
|
353
|
+
taskId,
|
|
354
|
+
method: message.method,
|
|
355
|
+
hasId: "id" in message
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
handleStdout(taskId, chunk) {
|
|
359
|
+
const agentChild = this.getChild(taskId);
|
|
360
|
+
agentChild.stdoutBuffer += chunk;
|
|
361
|
+
const lines = agentChild.stdoutBuffer.split("\n");
|
|
362
|
+
agentChild.stdoutBuffer = lines.pop() ?? "";
|
|
363
|
+
for (const rawLine of lines) {
|
|
364
|
+
const line = rawLine.trim();
|
|
365
|
+
if (!line) continue;
|
|
366
|
+
this.handleMessage(taskId, line).catch((error) => {
|
|
367
|
+
this.logger.warn("agent message handling failed", {
|
|
368
|
+
taskId,
|
|
369
|
+
error: error instanceof Error ? error.message : String(error)
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async handleMessage(taskId, line) {
|
|
375
|
+
let message;
|
|
376
|
+
try {
|
|
377
|
+
message = JSON.parse(line);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.logger.warn("agent emitted non-json stdout", {
|
|
380
|
+
taskId,
|
|
381
|
+
line,
|
|
382
|
+
error: error instanceof Error ? error.message : String(error)
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if ("id" in message && "result" in message) {
|
|
387
|
+
this.handleSuccess(taskId, message);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if ("id" in message && "error" in message) {
|
|
391
|
+
this.handleFailure(taskId, message);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if ("id" in message && "method" in message && !("result" in message) && !("error" in message)) {
|
|
395
|
+
await this.handleInboundRequest(taskId, message);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if ("method" in message && message.method === "session/update") {
|
|
399
|
+
this.handleSessionUpdate(taskId, message);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.logger.info("agent notification received", {
|
|
403
|
+
taskId,
|
|
404
|
+
method: message.method
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async handleInboundRequest(taskId, message) {
|
|
408
|
+
if (message.method !== "request_permission" && message.method !== "session/request_permission") {
|
|
409
|
+
this.logger.warn("agent inbound request is not supported", {
|
|
410
|
+
taskId,
|
|
411
|
+
method: message.method,
|
|
412
|
+
id: message.id
|
|
413
|
+
});
|
|
414
|
+
this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!this.onPermissionRequest) {
|
|
418
|
+
this.logger.warn("permission request handler is not configured", {
|
|
419
|
+
taskId,
|
|
420
|
+
id: message.id
|
|
421
|
+
});
|
|
422
|
+
this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const params = normalizePermissionParams(message.params).params;
|
|
426
|
+
if (!params) {
|
|
427
|
+
this.logger.warn("permission request params missing", {
|
|
428
|
+
taskId,
|
|
429
|
+
id: message.id,
|
|
430
|
+
rawFieldPaths: collectFieldPaths(message.params)
|
|
431
|
+
});
|
|
432
|
+
this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.logger.info("permission request payload", {
|
|
436
|
+
taskId,
|
|
437
|
+
id: message.id,
|
|
438
|
+
method: message.method,
|
|
439
|
+
permission: summarizePermissionRequest(params, message.params),
|
|
440
|
+
toolCallFieldPaths: collectFieldPaths(params.toolCall),
|
|
441
|
+
toolCallStandardFields: ACP_TOOL_CALL_STANDARD_FIELDS
|
|
442
|
+
});
|
|
443
|
+
let outcome;
|
|
444
|
+
try {
|
|
445
|
+
outcome = await this.onPermissionRequest({
|
|
446
|
+
taskId,
|
|
447
|
+
params,
|
|
448
|
+
emitApprovalRequested: (request) => {
|
|
449
|
+
this.onEvent?.({
|
|
450
|
+
type: "agent.approval_requested",
|
|
451
|
+
taskId,
|
|
452
|
+
request
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
} catch (error) {
|
|
457
|
+
this.logger.error("permission request handler failed", {
|
|
458
|
+
taskId,
|
|
459
|
+
id: message.id,
|
|
460
|
+
error: error instanceof Error ? error.message : String(error)
|
|
461
|
+
});
|
|
462
|
+
this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (outcome.decision) this.onEvent?.({
|
|
466
|
+
type: "agent.approval_resolved",
|
|
467
|
+
taskId,
|
|
468
|
+
decision: outcome.decision
|
|
469
|
+
});
|
|
470
|
+
if (outcome.status !== "approved") {
|
|
471
|
+
if (outcome.optionId) this.writeSuccess(taskId, message.id, { outcome: {
|
|
472
|
+
outcome: "selected",
|
|
473
|
+
optionId: outcome.optionId
|
|
474
|
+
} });
|
|
475
|
+
else this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this.writeSuccess(taskId, message.id, { outcome: {
|
|
479
|
+
outcome: "selected",
|
|
480
|
+
optionId: outcome.optionId
|
|
481
|
+
} });
|
|
482
|
+
}
|
|
483
|
+
writeSuccess(taskId, id, result) {
|
|
484
|
+
const agentChild = this.#children.get(taskId);
|
|
485
|
+
if (!agentChild) {
|
|
486
|
+
this.logger.warn("agent response dropped: task is no longer running", {
|
|
487
|
+
taskId,
|
|
488
|
+
id
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const response = {
|
|
493
|
+
jsonrpc: "2.0",
|
|
494
|
+
id,
|
|
495
|
+
result
|
|
496
|
+
};
|
|
497
|
+
agentChild.child.stdin.write(`${JSON.stringify(response)}\n`);
|
|
498
|
+
this.logger.info("agent response sent", {
|
|
499
|
+
taskId,
|
|
500
|
+
id
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
handleSuccess(taskId, message) {
|
|
504
|
+
const agentChild = this.getChild(taskId);
|
|
505
|
+
const pending = agentChild.pendingRequests.get(message.id);
|
|
506
|
+
if (!pending) {
|
|
507
|
+
this.logger.warn("agent response without pending request", {
|
|
508
|
+
taskId,
|
|
509
|
+
id: message.id
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
agentChild.pendingRequests.delete(message.id);
|
|
514
|
+
clearTimeout(pending.timer);
|
|
515
|
+
pending.resolve(message.result);
|
|
516
|
+
}
|
|
517
|
+
handleFailure(taskId, message) {
|
|
518
|
+
const agentChild = this.getChild(taskId);
|
|
519
|
+
if (message.id === null) {
|
|
520
|
+
this.logger.error("agent emitted rpc error without request id", {
|
|
521
|
+
taskId,
|
|
522
|
+
code: message.error.code,
|
|
523
|
+
message: message.error.message
|
|
524
|
+
});
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const pending = agentChild.pendingRequests.get(message.id);
|
|
528
|
+
if (!pending) {
|
|
529
|
+
this.logger.warn("agent error without pending request", {
|
|
530
|
+
taskId,
|
|
531
|
+
id: message.id,
|
|
532
|
+
code: message.error.code,
|
|
533
|
+
message: message.error.message
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
agentChild.pendingRequests.delete(message.id);
|
|
538
|
+
clearTimeout(pending.timer);
|
|
539
|
+
pending.reject(new AgentProcessError("agent_protocol_error", `ACP request failed for ${pending.method}: ${message.error.message} (${message.error.code})`));
|
|
540
|
+
}
|
|
541
|
+
handleSessionUpdate(taskId, message) {
|
|
542
|
+
this.refreshPromptTimeout(taskId);
|
|
543
|
+
const update = message.params?.update;
|
|
544
|
+
if (update?.sessionUpdate === "agent_message_chunk" && update.content?.type === "text") this.onEvent?.({
|
|
545
|
+
type: "agent.output",
|
|
546
|
+
taskId,
|
|
547
|
+
text: update.content.text
|
|
548
|
+
});
|
|
549
|
+
const processText = this.formatProcessUpdate(update);
|
|
550
|
+
if (processText) this.onEvent?.({
|
|
551
|
+
type: "agent.output",
|
|
552
|
+
taskId,
|
|
553
|
+
text: processText
|
|
554
|
+
});
|
|
555
|
+
if (update?.sessionUpdate?.includes("tool")) {
|
|
556
|
+
const toolUpdate = this.toToolInvocation(update);
|
|
557
|
+
this.onEvent?.({
|
|
558
|
+
type: "agent.tool_update",
|
|
559
|
+
taskId,
|
|
560
|
+
update: toolUpdate
|
|
561
|
+
});
|
|
562
|
+
if (this.shouldLogToolUpdate(taskId, toolUpdate)) this.logger.info("agent tool update", {
|
|
563
|
+
taskId,
|
|
564
|
+
sessionId: message.params?.sessionId,
|
|
565
|
+
updateType: update.sessionUpdate,
|
|
566
|
+
tool: {
|
|
567
|
+
toolCallId: toolUpdate.toolCallId,
|
|
568
|
+
toolName: toolUpdate.toolName,
|
|
569
|
+
toolNameSource: toolUpdate.toolNameSource,
|
|
570
|
+
kind: toolUpdate.kind,
|
|
571
|
+
status: toolUpdate.status
|
|
572
|
+
},
|
|
573
|
+
fieldPaths: toolUpdate.fieldPaths,
|
|
574
|
+
standardFields: ACP_TOOL_CALL_STANDARD_FIELDS
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (update?.sessionUpdate && update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "usage_update") this.logger.info("agent session update received", {
|
|
578
|
+
taskId,
|
|
579
|
+
sessionId: message.params?.sessionId,
|
|
580
|
+
updateType: update.sessionUpdate,
|
|
581
|
+
contentType: update?.content?.type
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
formatProcessUpdate(update) {
|
|
585
|
+
if (!update) return;
|
|
586
|
+
if (update.sessionUpdate === "agent_message_chunk") return;
|
|
587
|
+
if (update.sessionUpdate === "usage_update") return;
|
|
588
|
+
if (update.sessionUpdate === "available_commands_update") return;
|
|
589
|
+
if (update.sessionUpdate.includes("tool")) return this.formatToolUpdate(update);
|
|
590
|
+
const summary = this.summarizeUpdate(update);
|
|
591
|
+
return `\n[${update.sessionUpdate}]${summary ? ` ${summary}` : ""}\n`;
|
|
592
|
+
}
|
|
593
|
+
formatToolUpdate(update) {
|
|
594
|
+
const summary = this.toToolInvocation(update);
|
|
595
|
+
const toolName = summary.toolName ?? "工具调用";
|
|
596
|
+
const status = this.readStringField(update, [
|
|
597
|
+
"status",
|
|
598
|
+
"state",
|
|
599
|
+
"phase"
|
|
600
|
+
]);
|
|
601
|
+
const command = summary.command;
|
|
602
|
+
const path = summary.path;
|
|
603
|
+
const error = this.readStringField(update, ["error", "message"]);
|
|
604
|
+
const query = summary.query;
|
|
605
|
+
const url = summary.url;
|
|
606
|
+
const lines = [`\n[tool] ${toolName}${status ? ` (${status})` : ""}`];
|
|
607
|
+
if (query) lines.push(`query: ${this.truncate(query, 160)}`);
|
|
608
|
+
if (url) lines.push(`url: ${this.truncate(url, 200)}`);
|
|
609
|
+
if (command) lines.push(`cmd: ${this.truncate(command, 160)}`);
|
|
610
|
+
if (path) lines.push(`path: ${this.truncate(path, 120)}`);
|
|
611
|
+
if (error) lines.push(`error: ${this.truncate(error, 160)}`);
|
|
612
|
+
lines.push("");
|
|
613
|
+
return lines.join("\n");
|
|
614
|
+
}
|
|
615
|
+
toToolInvocation(update) {
|
|
616
|
+
const command = this.readStringField(update, [
|
|
617
|
+
"command",
|
|
618
|
+
"cmd",
|
|
619
|
+
"shellCommand"
|
|
620
|
+
]) ?? this.findStringByKeyPattern(update, /(cmd|command|shell)/i);
|
|
621
|
+
const path = this.readStringField(update, [
|
|
622
|
+
"path",
|
|
623
|
+
"cwd",
|
|
624
|
+
"targetPath"
|
|
625
|
+
]) ?? this.findStringByKeyPattern(update, /(cwd|path|target)/i);
|
|
626
|
+
const query = this.readStringField(update, [
|
|
627
|
+
"query",
|
|
628
|
+
"q",
|
|
629
|
+
"searchQuery",
|
|
630
|
+
"keyword"
|
|
631
|
+
]) ?? this.findStringByKeyPattern(update, /(query|keyword)\b/i);
|
|
632
|
+
const url = this.readStringField(update, [
|
|
633
|
+
"url",
|
|
634
|
+
"uri",
|
|
635
|
+
"link"
|
|
636
|
+
]) ?? this.findStringByKeyPattern(update, /(url|uri|link)\b/i);
|
|
637
|
+
const toolNameResult = this.extractToolName(update);
|
|
638
|
+
const status = this.readStringField(update, [
|
|
639
|
+
"status",
|
|
640
|
+
"state",
|
|
641
|
+
"phase"
|
|
642
|
+
]);
|
|
643
|
+
const id = this.readStringField(update, [
|
|
644
|
+
"id",
|
|
645
|
+
"toolCallId",
|
|
646
|
+
"tool_call_id"
|
|
647
|
+
]) ?? this.findStringByKeyPattern(update, /\b(id|tool.*id|call.*id)\b/i);
|
|
648
|
+
const title = this.readStringField(update, ["title"]);
|
|
649
|
+
const kind = this.readStringField(update, ["kind"]);
|
|
650
|
+
const error = this.readStringField(update, ["error", "message"]);
|
|
651
|
+
return {
|
|
652
|
+
updateType: update.sessionUpdate,
|
|
653
|
+
toolCallId: id ?? "unknown",
|
|
654
|
+
toolName: toolNameResult.name,
|
|
655
|
+
toolNameSource: toolNameResult.source,
|
|
656
|
+
kind: kind ?? "unknown",
|
|
657
|
+
title,
|
|
658
|
+
status: status ?? "unknown",
|
|
659
|
+
command,
|
|
660
|
+
path,
|
|
661
|
+
query,
|
|
662
|
+
url,
|
|
663
|
+
error,
|
|
664
|
+
fieldPaths: collectFieldPaths(update)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
extractToolName(update) {
|
|
668
|
+
const fromActionType = this.extractToolNameFromActionType(update);
|
|
669
|
+
if (fromActionType) return {
|
|
670
|
+
name: fromActionType,
|
|
671
|
+
source: "action_type"
|
|
672
|
+
};
|
|
673
|
+
const fromTitle = this.extractToolNameFromTitle(update);
|
|
674
|
+
if (fromTitle) return {
|
|
675
|
+
name: fromTitle,
|
|
676
|
+
source: "title"
|
|
677
|
+
};
|
|
678
|
+
const direct = this.readStringField(update, [
|
|
679
|
+
"toolName",
|
|
680
|
+
"tool_name",
|
|
681
|
+
"tool",
|
|
682
|
+
"method",
|
|
683
|
+
"function"
|
|
684
|
+
]) ?? this.findStringByKeyPattern(update, /(tool.*name|tool|method|function|action)/i);
|
|
685
|
+
if (direct && !this.isGenericToolValue(direct)) return {
|
|
686
|
+
name: direct,
|
|
687
|
+
source: "direct_field"
|
|
688
|
+
};
|
|
689
|
+
const knownTool = this.findKnownToolName(update);
|
|
690
|
+
if (knownTool) return {
|
|
691
|
+
name: knownTool,
|
|
692
|
+
source: "known_pattern"
|
|
693
|
+
};
|
|
694
|
+
const command = this.readStringField(update, [
|
|
695
|
+
"command",
|
|
696
|
+
"cmd",
|
|
697
|
+
"shellCommand"
|
|
698
|
+
]) ?? this.findStringByKeyPattern(update, /(cmd|command|shell)/i);
|
|
699
|
+
if (command) return {
|
|
700
|
+
name: command.split(/\s+/)[0] ?? "unknown",
|
|
701
|
+
source: "command"
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
name: "unknown",
|
|
705
|
+
source: "fallback"
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
extractToolNameFromActionType(update) {
|
|
709
|
+
const actionType = this.readStringField(update, ["actionType", "action_type"]) ?? this.findStringByKeyPattern(update, /action.*type/i);
|
|
710
|
+
if (!actionType) return;
|
|
711
|
+
const normalized = actionType.trim().toLowerCase();
|
|
712
|
+
if (normalized === "search") return "web.search_query";
|
|
713
|
+
if (normalized === "open_page") return "web.open";
|
|
714
|
+
if (normalized === "find_text") return "web.find";
|
|
715
|
+
if (normalized === "screenshot") return "web.screenshot";
|
|
716
|
+
return `web.${normalized}`;
|
|
717
|
+
}
|
|
718
|
+
extractToolNameFromTitle(update) {
|
|
719
|
+
const title = this.readStringField(update, ["title"]);
|
|
720
|
+
if (!title) return;
|
|
721
|
+
const normalized = title.toLowerCase();
|
|
722
|
+
if (normalized.includes("searching the web") || normalized.includes("searching for:")) return "web.search_query";
|
|
723
|
+
if (normalized.includes("opening:") || normalized === "open page") return "web.open";
|
|
724
|
+
if (normalized.startsWith("finding:")) return "web.find";
|
|
725
|
+
}
|
|
726
|
+
summarizeUpdate(update) {
|
|
727
|
+
const content = update.content;
|
|
728
|
+
if (content?.type === "text") return content.text;
|
|
729
|
+
if (content?.type === "image") return `[image:${content.mimeType}]`;
|
|
730
|
+
const { sessionUpdate: _sessionUpdate, content: _content, availableCommands: _availableCommands, used: _used, size: _size, ...compact } = update;
|
|
731
|
+
const text = JSON.stringify(compact);
|
|
732
|
+
if (!text || text === "{}") return "";
|
|
733
|
+
return text.length > 300 ? `${text.slice(0, 300)}...` : text;
|
|
734
|
+
}
|
|
735
|
+
readStringField(update, keys) {
|
|
736
|
+
const dict = update;
|
|
737
|
+
for (const key of keys) {
|
|
738
|
+
const value = dict[key];
|
|
739
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
740
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
741
|
+
if (value && typeof value === "object") {
|
|
742
|
+
const nested = value;
|
|
743
|
+
for (const nestedKey of [
|
|
744
|
+
"name",
|
|
745
|
+
"text",
|
|
746
|
+
"command",
|
|
747
|
+
"path",
|
|
748
|
+
"message"
|
|
749
|
+
]) {
|
|
750
|
+
const nestedValue = nested[nestedKey];
|
|
751
|
+
if (typeof nestedValue === "string" && nestedValue.trim()) return nestedValue.trim();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
findStringByKeyPattern(value, pattern, depth = 0) {
|
|
757
|
+
if (depth > 6) return;
|
|
758
|
+
for (const [key, item] of Object.entries(value)) {
|
|
759
|
+
if (typeof item === "string" && item.trim() && pattern.test(key)) return item.trim();
|
|
760
|
+
if (item && typeof item === "object") {
|
|
761
|
+
if (Array.isArray(item)) {
|
|
762
|
+
for (const entry of item) if (entry && typeof entry === "object") {
|
|
763
|
+
const nested = this.findStringByKeyPattern(entry, pattern, depth + 1);
|
|
764
|
+
if (nested) return nested;
|
|
765
|
+
}
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const nested = this.findStringByKeyPattern(item, pattern, depth + 1);
|
|
769
|
+
if (nested) return nested;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
isGenericToolValue(value) {
|
|
774
|
+
const normalized = value.trim().toLowerCase();
|
|
775
|
+
return /^ws_[a-z0-9]+$/.test(normalized) || normalized === "tool" || normalized === "tools" || normalized === "in_progress" || normalized === "completed" || normalized === "running" || normalized === "status";
|
|
776
|
+
}
|
|
777
|
+
findKnownToolName(value, depth = 0) {
|
|
778
|
+
if (depth > 6) return;
|
|
779
|
+
const known = [
|
|
780
|
+
"search_query",
|
|
781
|
+
"web_search",
|
|
782
|
+
"image_query",
|
|
783
|
+
"open",
|
|
784
|
+
"click",
|
|
785
|
+
"find",
|
|
786
|
+
"screenshot",
|
|
787
|
+
"finance",
|
|
788
|
+
"weather",
|
|
789
|
+
"sports",
|
|
790
|
+
"time"
|
|
791
|
+
];
|
|
792
|
+
for (const item of Object.values(value)) {
|
|
793
|
+
if (typeof item === "string") {
|
|
794
|
+
const normalized = item.trim().toLowerCase();
|
|
795
|
+
if (known.includes(normalized)) return normalized;
|
|
796
|
+
}
|
|
797
|
+
if (item && typeof item === "object") {
|
|
798
|
+
if (Array.isArray(item)) {
|
|
799
|
+
for (const entry of item) if (entry && typeof entry === "object") {
|
|
800
|
+
const nested = this.findKnownToolName(entry, depth + 1);
|
|
801
|
+
if (nested) return nested;
|
|
802
|
+
}
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const nested = this.findKnownToolName(item, depth + 1);
|
|
806
|
+
if (nested) return nested;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
shouldLogToolUpdate(taskId, update) {
|
|
811
|
+
const key = `${taskId}:${update.toolCallId}:${update.status}:${update.updateType}`;
|
|
812
|
+
if (this.#toolUpdateLogDedup.has(key)) return false;
|
|
813
|
+
this.#toolUpdateLogDedup.add(key);
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
clearToolUpdateLogCache(taskId) {
|
|
817
|
+
for (const key of this.#toolUpdateLogDedup) if (key.startsWith(`${taskId}:`)) this.#toolUpdateLogDedup.delete(key);
|
|
818
|
+
}
|
|
819
|
+
truncate(text, maxLen) {
|
|
820
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
821
|
+
}
|
|
822
|
+
getChild(taskId) {
|
|
823
|
+
const agentChild = this.#children.get(taskId);
|
|
824
|
+
if (!agentChild) throw new Error(`Agent process not found for task: ${taskId}`);
|
|
825
|
+
return agentChild;
|
|
826
|
+
}
|
|
827
|
+
createRequestTimer(taskId, requestId, method, timeoutMs) {
|
|
828
|
+
return setTimeout(() => {
|
|
829
|
+
const agentChild = this.#children.get(taskId);
|
|
830
|
+
const pending = agentChild?.pendingRequests.get(requestId);
|
|
831
|
+
if (!pending) return;
|
|
832
|
+
agentChild?.pendingRequests.delete(requestId);
|
|
833
|
+
pending.reject(new AgentProcessError("agent_session_timeout", `ACP request timed out for ${method} after ${timeoutMs}ms`));
|
|
834
|
+
}, timeoutMs);
|
|
835
|
+
}
|
|
836
|
+
refreshPromptTimeout(taskId) {
|
|
837
|
+
const agentChild = this.#children.get(taskId);
|
|
838
|
+
if (!agentChild) return;
|
|
839
|
+
for (const [requestId, pending] of agentChild.pendingRequests.entries()) {
|
|
840
|
+
if (pending.method !== "session/prompt") continue;
|
|
841
|
+
clearTimeout(pending.timer);
|
|
842
|
+
pending.timer = this.createRequestTimer(taskId, requestId, pending.method, pending.timeoutMs);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
rejectAllPendingRequests(taskId, reason) {
|
|
846
|
+
const agentChild = this.#children.get(taskId);
|
|
847
|
+
if (!agentChild) return;
|
|
848
|
+
for (const [id, pending] of agentChild.pendingRequests.entries()) {
|
|
849
|
+
clearTimeout(pending.timer);
|
|
850
|
+
pending.reject(new AgentProcessError("agent_session_start_failed", `ACP request interrupted for ${pending.method}: ${reason}`));
|
|
851
|
+
agentChild.pendingRequests.delete(id);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
mapRequestError(taskId, method, error) {
|
|
855
|
+
if (error instanceof AgentProcessError) {
|
|
856
|
+
if (error.code === "agent_session_timeout") {
|
|
857
|
+
const stderr = this.getChild(taskId).stderrChunks.join("\n");
|
|
858
|
+
if (!(spawnSync("test", ["-f", `${process.env.HOME}/.codex/auth.json`], { stdio: "ignore" }).status === 0)) return new AgentProcessError("agent_auth_missing", "Codex authentication is missing. Please complete local Codex login first.");
|
|
859
|
+
return new AgentProcessError("agent_session_timeout", stderr ? `codex-acp did not finish ${method} within timeout. stderr: ${stderr}` : `codex-acp did not finish ${method} within timeout`);
|
|
860
|
+
}
|
|
861
|
+
return error;
|
|
862
|
+
}
|
|
863
|
+
return new AgentProcessError("agent_session_start_failed", error instanceof Error ? error.message : String(error));
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/approval/approval-store.ts
|
|
868
|
+
var ApprovalStore = class {
|
|
869
|
+
#snapshots = /* @__PURE__ */ new Map();
|
|
870
|
+
#waiters = /* @__PURE__ */ new Map();
|
|
871
|
+
set(request) {
|
|
872
|
+
this.#snapshots.set(request.id, {
|
|
873
|
+
request,
|
|
874
|
+
status: "pending"
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
get(requestId) {
|
|
878
|
+
return this.#snapshots.get(requestId);
|
|
879
|
+
}
|
|
880
|
+
delete(requestId) {
|
|
881
|
+
const waiter = this.#waiters.get(requestId);
|
|
882
|
+
if (waiter) {
|
|
883
|
+
clearTimeout(waiter.timer);
|
|
884
|
+
this.#waiters.delete(requestId);
|
|
885
|
+
}
|
|
886
|
+
this.#snapshots.delete(requestId);
|
|
887
|
+
}
|
|
888
|
+
awaitDecision(requestId, timeoutMs) {
|
|
889
|
+
const snapshot = this.#snapshots.get(requestId);
|
|
890
|
+
if (!snapshot) return Promise.reject(/* @__PURE__ */ new Error(`Approval request not found: ${requestId}`));
|
|
891
|
+
if (snapshot.status === "approved" && snapshot.decision) return Promise.resolve({
|
|
892
|
+
status: "approved",
|
|
893
|
+
request: snapshot.request,
|
|
894
|
+
decision: snapshot.decision
|
|
895
|
+
});
|
|
896
|
+
if (snapshot.status === "rejected") return Promise.resolve({
|
|
897
|
+
status: "rejected",
|
|
898
|
+
request: snapshot.request,
|
|
899
|
+
decision: snapshot.decision
|
|
900
|
+
});
|
|
901
|
+
if (snapshot.status === "expired") return Promise.resolve({
|
|
902
|
+
status: "expired",
|
|
903
|
+
request: snapshot.request
|
|
904
|
+
});
|
|
905
|
+
return new Promise((resolve) => {
|
|
906
|
+
const timer = setTimeout(() => {
|
|
907
|
+
const current = this.#snapshots.get(requestId);
|
|
908
|
+
if (!current || current.status !== "pending") return;
|
|
909
|
+
current.status = "expired";
|
|
910
|
+
current.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
911
|
+
this.#snapshots.set(requestId, current);
|
|
912
|
+
this.#waiters.delete(requestId);
|
|
913
|
+
resolve({
|
|
914
|
+
status: "expired",
|
|
915
|
+
request: current.request
|
|
916
|
+
});
|
|
917
|
+
}, timeoutMs);
|
|
918
|
+
this.#waiters.set(requestId, {
|
|
919
|
+
resolve,
|
|
920
|
+
timer
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
resolve(decision) {
|
|
925
|
+
const snapshot = this.#snapshots.get(decision.requestId);
|
|
926
|
+
if (!snapshot) return { accepted: false };
|
|
927
|
+
if (snapshot.status !== "pending") return {
|
|
928
|
+
accepted: false,
|
|
929
|
+
snapshot
|
|
930
|
+
};
|
|
931
|
+
snapshot.status = decision.decision === "approved" ? "approved" : "rejected";
|
|
932
|
+
snapshot.decision = decision;
|
|
933
|
+
snapshot.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
934
|
+
this.#snapshots.set(decision.requestId, snapshot);
|
|
935
|
+
const waiter = this.#waiters.get(decision.requestId);
|
|
936
|
+
if (waiter) {
|
|
937
|
+
clearTimeout(waiter.timer);
|
|
938
|
+
this.#waiters.delete(decision.requestId);
|
|
939
|
+
waiter.resolve(snapshot.status === "approved" ? {
|
|
940
|
+
status: "approved",
|
|
941
|
+
request: snapshot.request,
|
|
942
|
+
decision
|
|
943
|
+
} : {
|
|
944
|
+
status: "rejected",
|
|
945
|
+
request: snapshot.request,
|
|
946
|
+
decision
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
accepted: true,
|
|
951
|
+
snapshot
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/approval/approval-gateway.ts
|
|
957
|
+
var ApprovalGateway = class {
|
|
958
|
+
#resolutionListeners = /* @__PURE__ */ new Set();
|
|
959
|
+
#sessionAllowAll = /* @__PURE__ */ new Set();
|
|
960
|
+
constructor(store, logger) {
|
|
961
|
+
this.store = store;
|
|
962
|
+
this.logger = logger;
|
|
963
|
+
}
|
|
964
|
+
request(request) {
|
|
965
|
+
this.store.set(request);
|
|
966
|
+
this.logger.info("approval request stored", {
|
|
967
|
+
requestId: request.id,
|
|
968
|
+
taskId: request.taskId,
|
|
969
|
+
kind: request.kind
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async requestAndWait(request, timeoutMs) {
|
|
973
|
+
this.request(request);
|
|
974
|
+
const result = await this.store.awaitDecision(request.id, timeoutMs);
|
|
975
|
+
if (result.status === "expired") {
|
|
976
|
+
const snapshot = this.store.get(request.id);
|
|
977
|
+
if (snapshot) {
|
|
978
|
+
this.emitResolution(snapshot);
|
|
979
|
+
this.store.delete(request.id);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
resolve(decision) {
|
|
985
|
+
const result = this.store.resolve(decision);
|
|
986
|
+
if (!result.snapshot) {
|
|
987
|
+
this.logger.warn("approval request not found", {
|
|
988
|
+
requestId: decision.requestId,
|
|
989
|
+
taskId: decision.taskId
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (!result.accepted) {
|
|
994
|
+
this.logger.info("approval request resolve ignored", {
|
|
995
|
+
requestId: decision.requestId,
|
|
996
|
+
taskId: decision.taskId,
|
|
997
|
+
decision: decision.decision,
|
|
998
|
+
status: result.snapshot.status
|
|
999
|
+
});
|
|
1000
|
+
return result.snapshot;
|
|
1001
|
+
}
|
|
1002
|
+
if (decision.decision === "approved" && decision.comment === "approved-for-session") this.#sessionAllowAll.add(decision.taskId);
|
|
1003
|
+
this.logger.info("approval request resolved", {
|
|
1004
|
+
requestId: decision.requestId,
|
|
1005
|
+
taskId: decision.taskId,
|
|
1006
|
+
decision: decision.decision
|
|
1007
|
+
});
|
|
1008
|
+
this.emitResolution(result.snapshot);
|
|
1009
|
+
this.store.delete(decision.requestId);
|
|
1010
|
+
return result.snapshot;
|
|
1011
|
+
}
|
|
1012
|
+
isSessionAllowAll(taskId) {
|
|
1013
|
+
return this.#sessionAllowAll.has(taskId);
|
|
1014
|
+
}
|
|
1015
|
+
onResolved(listener) {
|
|
1016
|
+
this.#resolutionListeners.add(listener);
|
|
1017
|
+
return () => {
|
|
1018
|
+
this.#resolutionListeners.delete(listener);
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
emitResolution(snapshot) {
|
|
1022
|
+
for (const listener of this.#resolutionListeners) listener(snapshot);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/policy/policy-engine.ts
|
|
1027
|
+
function evaluatePolicy(input) {
|
|
1028
|
+
const { kind, hasSessionAllowAll } = input;
|
|
1029
|
+
if (hasSessionAllowAll) return { type: "allow" };
|
|
1030
|
+
return {
|
|
1031
|
+
type: "ask",
|
|
1032
|
+
reason: `Approval required for ${kind}`
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
//#endregion
|
|
1036
|
+
//#region src/approval/permission-handler.ts
|
|
1037
|
+
const APPROVAL_TIMEOUT_MS = 12e4;
|
|
1038
|
+
function findWorkspaceByTask(taskId, taskRunner, workspaces) {
|
|
1039
|
+
const task = taskRunner.getTask(taskId);
|
|
1040
|
+
if (!task) return workspaces[0];
|
|
1041
|
+
return workspaces.find((item) => item.id === task.workspaceId) ?? workspaces[0];
|
|
1042
|
+
}
|
|
1043
|
+
function extractTargetPath(params) {
|
|
1044
|
+
const fromLocation = params.toolCall.locations?.[0]?.path;
|
|
1045
|
+
if (fromLocation) return fromLocation;
|
|
1046
|
+
const rawInput = params.toolCall.rawInput;
|
|
1047
|
+
if (rawInput && typeof rawInput === "object") {
|
|
1048
|
+
const candidate = rawInput.path;
|
|
1049
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function extractCommand(params) {
|
|
1053
|
+
const rawInput = params.toolCall.rawInput;
|
|
1054
|
+
if (typeof rawInput === "string" && rawInput.trim()) return rawInput.trim();
|
|
1055
|
+
if (rawInput && typeof rawInput === "object") for (const key of [
|
|
1056
|
+
"command",
|
|
1057
|
+
"cmd",
|
|
1058
|
+
"shellCommand",
|
|
1059
|
+
"input",
|
|
1060
|
+
"prompt"
|
|
1061
|
+
]) {
|
|
1062
|
+
const value = rawInput[key];
|
|
1063
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1064
|
+
}
|
|
1065
|
+
const content = params.toolCall.content;
|
|
1066
|
+
if (content && content.length > 0) {
|
|
1067
|
+
for (const item of content) if (item.type === "text" && typeof item.text === "string" && item.text.trim()) return item.text.trim();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function inferApprovalKind(params) {
|
|
1071
|
+
const kind = (params.toolCall.kind ?? "").toLowerCase();
|
|
1072
|
+
const title = (params.toolCall.title ?? "").toLowerCase();
|
|
1073
|
+
const cmd = (extractCommand(params) ?? "").toLowerCase();
|
|
1074
|
+
if (/network|http|https|curl|wget/.test(kind) || /network|http|https|curl|wget/.test(title) || /curl|wget/.test(cmd)) return "network";
|
|
1075
|
+
if (/edit|patch|write|file/.test(kind) || /edit|patch|write|apply/.test(title)) return "write";
|
|
1076
|
+
if (/read|view|list/.test(kind) || /read|view|list/.test(title)) return "read";
|
|
1077
|
+
if (/cat\s|ls\s|find\s|rg\s/.test(cmd)) return "read";
|
|
1078
|
+
return "exec";
|
|
1079
|
+
}
|
|
1080
|
+
function chooseAllowOption(options, preferSession) {
|
|
1081
|
+
if (options.length === 0) return;
|
|
1082
|
+
if (preferSession) {
|
|
1083
|
+
const sessionOption = options.find((item) => {
|
|
1084
|
+
const id = item.id.toLowerCase();
|
|
1085
|
+
return id === "approved-for-session" || id === "approve-for-session" || id === "always";
|
|
1086
|
+
});
|
|
1087
|
+
if (sessionOption) return sessionOption.id;
|
|
1088
|
+
}
|
|
1089
|
+
const explicitApproved = options.find((item) => {
|
|
1090
|
+
const id = item.id.toLowerCase();
|
|
1091
|
+
return id === "approved" || id === "approve" || id === "allow" || id === "yes";
|
|
1092
|
+
});
|
|
1093
|
+
if (explicitApproved) return explicitApproved.id;
|
|
1094
|
+
const allow = options.find((item) => item.kind.toLowerCase().startsWith("allow"));
|
|
1095
|
+
if (allow) return allow.id;
|
|
1096
|
+
return options[0]?.id;
|
|
1097
|
+
}
|
|
1098
|
+
function chooseRejectOption(options) {
|
|
1099
|
+
for (const candidate of [
|
|
1100
|
+
"abort",
|
|
1101
|
+
"rejected",
|
|
1102
|
+
"reject",
|
|
1103
|
+
"cancel"
|
|
1104
|
+
]) {
|
|
1105
|
+
const hit = options.find((item) => item.id === candidate);
|
|
1106
|
+
if (hit) return hit.id;
|
|
1107
|
+
}
|
|
1108
|
+
return options.find((item) => item.kind.startsWith("reject"))?.id;
|
|
1109
|
+
}
|
|
1110
|
+
function buildApprovalRequest(taskId, kind, workspace, params) {
|
|
1111
|
+
const now = /* @__PURE__ */ new Date();
|
|
1112
|
+
const command = extractCommand(params);
|
|
1113
|
+
const target = extractTargetPath(params);
|
|
1114
|
+
const title = params.toolCall.title ?? `Permission required: ${kind}`;
|
|
1115
|
+
return {
|
|
1116
|
+
id: randomUUID(),
|
|
1117
|
+
taskId,
|
|
1118
|
+
kind,
|
|
1119
|
+
title,
|
|
1120
|
+
cwd: workspace.cwd,
|
|
1121
|
+
target,
|
|
1122
|
+
command,
|
|
1123
|
+
reason: title,
|
|
1124
|
+
riskLevel: kind === "read" ? "low" : kind === "write" ? "medium" : "high",
|
|
1125
|
+
createdAt: now.toISOString(),
|
|
1126
|
+
expiresAt: new Date(now.getTime() + APPROVAL_TIMEOUT_MS).toISOString()
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function buildDeniedWorkspaceOutcome(taskId, params) {
|
|
1130
|
+
const fallbackRequest = {
|
|
1131
|
+
id: randomUUID(),
|
|
1132
|
+
taskId,
|
|
1133
|
+
kind: "exec",
|
|
1134
|
+
title: "Permission denied: workspace not found",
|
|
1135
|
+
cwd: process.cwd(),
|
|
1136
|
+
reason: "workspace_not_found",
|
|
1137
|
+
riskLevel: "high",
|
|
1138
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1139
|
+
expiresAt: new Date(Date.now() + APPROVAL_TIMEOUT_MS).toISOString()
|
|
1140
|
+
};
|
|
1141
|
+
return {
|
|
1142
|
+
status: "denied",
|
|
1143
|
+
optionId: chooseRejectOption(params.options),
|
|
1144
|
+
approvalRequest: fallbackRequest,
|
|
1145
|
+
decision: {
|
|
1146
|
+
requestId: fallbackRequest.id,
|
|
1147
|
+
taskId,
|
|
1148
|
+
decision: "rejected",
|
|
1149
|
+
comment: "workspace_not_found",
|
|
1150
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1151
|
+
decidedBy: "policy-engine"
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function createPermissionRequestHandler(args) {
|
|
1156
|
+
const { workspaces, approvalGateway, getTaskRunner } = args;
|
|
1157
|
+
return async ({ taskId, params, emitApprovalRequested }) => {
|
|
1158
|
+
const taskRunner = getTaskRunner();
|
|
1159
|
+
const workspace = findWorkspaceByTask(taskId, taskRunner, workspaces);
|
|
1160
|
+
if (!workspace) return buildDeniedWorkspaceOutcome(taskId, params);
|
|
1161
|
+
const kind = inferApprovalKind(params);
|
|
1162
|
+
const request = buildApprovalRequest(taskId, kind, workspace, params);
|
|
1163
|
+
const policy = evaluatePolicy({
|
|
1164
|
+
kind,
|
|
1165
|
+
workspace,
|
|
1166
|
+
hasSessionAllowAll: approvalGateway.isSessionAllowAll(taskId) || taskRunner.isTaskFullAccess(taskId)
|
|
1167
|
+
});
|
|
1168
|
+
if (policy.type === "allow") {
|
|
1169
|
+
const optionId = chooseAllowOption(params.options, true);
|
|
1170
|
+
if (!optionId) return {
|
|
1171
|
+
status: "denied",
|
|
1172
|
+
optionId: chooseRejectOption(params.options),
|
|
1173
|
+
approvalRequest: request,
|
|
1174
|
+
decision: {
|
|
1175
|
+
requestId: request.id,
|
|
1176
|
+
taskId,
|
|
1177
|
+
decision: "rejected",
|
|
1178
|
+
comment: "allow_option_not_found",
|
|
1179
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1180
|
+
decidedBy: "policy-engine"
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
return {
|
|
1184
|
+
status: "approved",
|
|
1185
|
+
optionId,
|
|
1186
|
+
approvalRequest: request,
|
|
1187
|
+
decision: {
|
|
1188
|
+
requestId: request.id,
|
|
1189
|
+
taskId,
|
|
1190
|
+
decision: "approved",
|
|
1191
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1192
|
+
decidedBy: "policy-engine",
|
|
1193
|
+
comment: optionId === "approved-for-session" ? "approved-for-session" : "auto-allow"
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
if (policy.type === "deny") return {
|
|
1198
|
+
status: "denied",
|
|
1199
|
+
optionId: chooseRejectOption(params.options),
|
|
1200
|
+
approvalRequest: request,
|
|
1201
|
+
decision: {
|
|
1202
|
+
requestId: request.id,
|
|
1203
|
+
taskId,
|
|
1204
|
+
decision: "rejected",
|
|
1205
|
+
comment: policy.reason,
|
|
1206
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1207
|
+
decidedBy: "policy-engine"
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
emitApprovalRequested(request);
|
|
1211
|
+
const awaited = await approvalGateway.requestAndWait(request, APPROVAL_TIMEOUT_MS);
|
|
1212
|
+
if (awaited.status === "approved") {
|
|
1213
|
+
const optionId = chooseAllowOption(params.options, awaited.decision.comment === "approved-for-session");
|
|
1214
|
+
if (!optionId) return {
|
|
1215
|
+
status: "denied",
|
|
1216
|
+
optionId: chooseRejectOption(params.options),
|
|
1217
|
+
approvalRequest: request,
|
|
1218
|
+
decision: {
|
|
1219
|
+
requestId: request.id,
|
|
1220
|
+
taskId,
|
|
1221
|
+
decision: "rejected",
|
|
1222
|
+
comment: "allow_option_not_found",
|
|
1223
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1224
|
+
decidedBy: "bridge"
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
return {
|
|
1228
|
+
status: "approved",
|
|
1229
|
+
optionId,
|
|
1230
|
+
approvalRequest: request,
|
|
1231
|
+
decision: awaited.decision
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
if (awaited.status === "rejected") return {
|
|
1235
|
+
status: "rejected",
|
|
1236
|
+
optionId: chooseRejectOption(params.options),
|
|
1237
|
+
approvalRequest: request,
|
|
1238
|
+
decision: awaited.decision
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
status: "expired",
|
|
1242
|
+
optionId: chooseRejectOption(params.options),
|
|
1243
|
+
approvalRequest: request,
|
|
1244
|
+
decision: {
|
|
1245
|
+
requestId: request.id,
|
|
1246
|
+
taskId,
|
|
1247
|
+
decision: "rejected",
|
|
1248
|
+
comment: "approval_timeout",
|
|
1249
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1250
|
+
decidedBy: "system-timeout"
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
//#endregion
|
|
1256
|
+
//#region src/config/load-config.ts
|
|
1257
|
+
const CODEX_ACP_BIN = fileURLToPath(import.meta.resolve("@zed-industries/codex-acp/bin/codex-acp.js"));
|
|
1258
|
+
const DEFAULT_CONFIG_ENV_PATH = resolve(homedir(), ".im-code-agent", "config.env");
|
|
1259
|
+
const DEFAULT_CONFIG_ENV_CONTENT = `# Bridge 基础配置
|
|
1260
|
+
|
|
1261
|
+
# 飞书应用凭据
|
|
1262
|
+
FEISHU_APP_ID=
|
|
1263
|
+
FEISHU_APP_SECRET=
|
|
1264
|
+
|
|
1265
|
+
# 可选:true 时默认 Full Access
|
|
1266
|
+
YOLO_MODE=false
|
|
1267
|
+
|
|
1268
|
+
# 可选:默认工作目录,不填则使用 bridge 启动目录
|
|
1269
|
+
WORKSPACE_DEFAULT_CWD=
|
|
1270
|
+
`;
|
|
1271
|
+
function parseEnvFile(content) {
|
|
1272
|
+
const result = {};
|
|
1273
|
+
for (const rawLine of content.split("\n")) {
|
|
1274
|
+
const line = rawLine.trim();
|
|
1275
|
+
if (!line || line.startsWith("#")) continue;
|
|
1276
|
+
const equalIndex = line.indexOf("=");
|
|
1277
|
+
if (equalIndex <= 0) continue;
|
|
1278
|
+
const key = line.slice(0, equalIndex).trim();
|
|
1279
|
+
let value = line.slice(equalIndex + 1).trim();
|
|
1280
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1281
|
+
result[key] = value;
|
|
1282
|
+
}
|
|
1283
|
+
return result;
|
|
1284
|
+
}
|
|
1285
|
+
function stringifyEnvValue(value) {
|
|
1286
|
+
return /[\s#"'`]/.test(value) ? JSON.stringify(value) : value;
|
|
1287
|
+
}
|
|
1288
|
+
function upsertEnvValue(content, key, value) {
|
|
1289
|
+
const lines = content.split("\n");
|
|
1290
|
+
const rendered = `${key}=${stringifyEnvValue(value)}`;
|
|
1291
|
+
const lineIndex = lines.findIndex((line) => line.trimStart().startsWith(`${key}=`));
|
|
1292
|
+
if (lineIndex >= 0) lines[lineIndex] = rendered;
|
|
1293
|
+
else {
|
|
1294
|
+
if (lines.length > 0 && lines.at(-1) !== "") lines.push("");
|
|
1295
|
+
lines.push(rendered);
|
|
1296
|
+
}
|
|
1297
|
+
return lines.join("\n");
|
|
1298
|
+
}
|
|
1299
|
+
function normalizeCredential(value) {
|
|
1300
|
+
const trimmed = value?.trim();
|
|
1301
|
+
if (!trimmed) return;
|
|
1302
|
+
if (trimmed === "cli_xxx" || trimmed === "xxx") return;
|
|
1303
|
+
return trimmed;
|
|
1304
|
+
}
|
|
1305
|
+
async function resolveNodeCommand() {
|
|
1306
|
+
if (await fileExists(process.execPath)) return process.execPath;
|
|
1307
|
+
return "node";
|
|
1308
|
+
}
|
|
1309
|
+
async function isDirectory(dirPath) {
|
|
1310
|
+
try {
|
|
1311
|
+
return (await stat(dirPath)).isDirectory();
|
|
1312
|
+
} catch {
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async function loadBridgeEnvFile() {
|
|
1317
|
+
await ensureDefaultConfigEnvFile(resolveBridgeEnvPath());
|
|
1318
|
+
const configuredPath = process.env.BRIDGE_ENV_PATH;
|
|
1319
|
+
const envPaths = configuredPath ? [resolve(configuredPath)] : [
|
|
1320
|
+
DEFAULT_CONFIG_ENV_PATH,
|
|
1321
|
+
resolve(process.cwd(), "bridge.env"),
|
|
1322
|
+
resolve(process.cwd(), ".env")
|
|
1323
|
+
];
|
|
1324
|
+
for (const envPath of envPaths) try {
|
|
1325
|
+
return parseEnvFile(await readFile(envPath, "utf8"));
|
|
1326
|
+
} catch {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
return {};
|
|
1330
|
+
}
|
|
1331
|
+
function resolveBridgeEnvPath() {
|
|
1332
|
+
const configuredPath = process.env.BRIDGE_ENV_PATH;
|
|
1333
|
+
return configuredPath ? resolve(configuredPath) : DEFAULT_CONFIG_ENV_PATH;
|
|
1334
|
+
}
|
|
1335
|
+
async function fileExists(filePath) {
|
|
1336
|
+
try {
|
|
1337
|
+
await access(filePath, constants.F_OK);
|
|
1338
|
+
return true;
|
|
1339
|
+
} catch {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async function ensureDefaultConfigEnvFile(configEnvPath) {
|
|
1344
|
+
await createConfigEnvIfMissing(configEnvPath, process.cwd());
|
|
1345
|
+
}
|
|
1346
|
+
async function createConfigEnvIfMissing(targetPath, baseDir) {
|
|
1347
|
+
if (await fileExists(targetPath)) return;
|
|
1348
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
1349
|
+
const configTemplatePath = resolve(baseDir, "config.env.example");
|
|
1350
|
+
if (await fileExists(configTemplatePath)) {
|
|
1351
|
+
await copyFile(configTemplatePath, targetPath);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
await writeFile(targetPath, DEFAULT_CONFIG_ENV_CONTENT, "utf8");
|
|
1355
|
+
}
|
|
1356
|
+
async function promptForFeishuCredentials(envPath) {
|
|
1357
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error(`未检测到飞书配置,请在 ${envPath} 中填写 FEISHU_APP_ID 和 FEISHU_APP_SECRET,或通过环境变量注入后再启动。`);
|
|
1358
|
+
console.warn([
|
|
1359
|
+
"未检测到有效的飞书配置。",
|
|
1360
|
+
`将引导你写入 ${envPath}。`,
|
|
1361
|
+
"直接回车可跳过,但当前启动会直接退出。"
|
|
1362
|
+
].join(" "));
|
|
1363
|
+
const readline = createInterface({
|
|
1364
|
+
input: process.stdin,
|
|
1365
|
+
output: process.stdout
|
|
1366
|
+
});
|
|
1367
|
+
try {
|
|
1368
|
+
const appId = normalizeCredential(await readline.question("请输入飞书 App ID: "));
|
|
1369
|
+
const appSecret = normalizeCredential(await readline.question("请输入飞书 App Secret: "));
|
|
1370
|
+
if (!appId || !appSecret) throw new Error("缺少飞书配置,启动已取消。请填写 FEISHU_APP_ID 和 FEISHU_APP_SECRET 后重试。");
|
|
1371
|
+
const nextContent = upsertEnvValue(upsertEnvValue(await readFile(envPath, "utf8").catch(() => DEFAULT_CONFIG_ENV_CONTENT), "FEISHU_APP_ID", appId), "FEISHU_APP_SECRET", appSecret);
|
|
1372
|
+
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}\n`, "utf8");
|
|
1373
|
+
process.env.FEISHU_APP_ID = appId;
|
|
1374
|
+
process.env.FEISHU_APP_SECRET = appSecret;
|
|
1375
|
+
console.warn(`飞书配置已写入 ${envPath}。`);
|
|
1376
|
+
} finally {
|
|
1377
|
+
readline.close();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async function loadConfig() {
|
|
1381
|
+
const nodeCommand = await resolveNodeCommand();
|
|
1382
|
+
const envPath = resolveBridgeEnvPath();
|
|
1383
|
+
let fileEnv = await loadBridgeEnvFile();
|
|
1384
|
+
const getValue = (key) => process.env[key] ?? fileEnv[key];
|
|
1385
|
+
let appId = normalizeCredential(getValue("FEISHU_APP_ID"));
|
|
1386
|
+
let appSecret = normalizeCredential(getValue("FEISHU_APP_SECRET"));
|
|
1387
|
+
if (!appId || !appSecret) {
|
|
1388
|
+
await promptForFeishuCredentials(envPath);
|
|
1389
|
+
fileEnv = await loadBridgeEnvFile();
|
|
1390
|
+
appId = normalizeCredential(process.env.FEISHU_APP_ID ?? fileEnv.FEISHU_APP_ID);
|
|
1391
|
+
appSecret = normalizeCredential(process.env.FEISHU_APP_SECRET ?? fileEnv.FEISHU_APP_SECRET);
|
|
1392
|
+
}
|
|
1393
|
+
if (!appId || !appSecret) throw new Error(`缺少飞书配置,请在 ${envPath} 中填写 FEISHU_APP_ID 和 FEISHU_APP_SECRET 后重试。`);
|
|
1394
|
+
const yoloMode = getValue("YOLO_MODE")?.trim().toLowerCase() === "true";
|
|
1395
|
+
const workspaceDefaultCwd = getValue("WORKSPACE_DEFAULT_CWD")?.trim();
|
|
1396
|
+
const resolvedDefaultCwd = workspaceDefaultCwd ? resolve(workspaceDefaultCwd) : process.cwd();
|
|
1397
|
+
const defaultCwd = await isDirectory(resolvedDefaultCwd) ? resolvedDefaultCwd : process.cwd();
|
|
1398
|
+
return {
|
|
1399
|
+
feishu: {
|
|
1400
|
+
appId,
|
|
1401
|
+
appSecret
|
|
1402
|
+
},
|
|
1403
|
+
yoloMode,
|
|
1404
|
+
agents: { codex: {
|
|
1405
|
+
command: nodeCommand,
|
|
1406
|
+
args: [CODEX_ACP_BIN]
|
|
1407
|
+
} },
|
|
1408
|
+
workspaces: [{
|
|
1409
|
+
id: "local-default",
|
|
1410
|
+
name: "Local Default",
|
|
1411
|
+
cwd: defaultCwd
|
|
1412
|
+
}]
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/feishu/approval-card-policy.ts
|
|
1417
|
+
function shouldPatchApprovalSummary(decision) {
|
|
1418
|
+
return decision.comment !== "approval_timeout";
|
|
1419
|
+
}
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/feishu/command-router.ts
|
|
1422
|
+
function parseContent(content) {
|
|
1423
|
+
try {
|
|
1424
|
+
return JSON.parse(content);
|
|
1425
|
+
} catch {
|
|
1426
|
+
return {};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async function parseUserCommand(rawPrompt, currentCwd) {
|
|
1430
|
+
if (rawPrompt === "/help") return { type: "help" };
|
|
1431
|
+
if (rawPrompt === "/status") return { type: "status" };
|
|
1432
|
+
if (rawPrompt === "/stop" || rawPrompt === "/interrupt") return { type: "stop" };
|
|
1433
|
+
if (rawPrompt === "/perm") return { type: "show-access" };
|
|
1434
|
+
if (rawPrompt === "/model") return { type: "model" };
|
|
1435
|
+
if (rawPrompt.startsWith("/model ")) return {
|
|
1436
|
+
type: "model",
|
|
1437
|
+
model: rawPrompt.slice(7).trim() || void 0
|
|
1438
|
+
};
|
|
1439
|
+
if (rawPrompt === "/new") return { type: "new" };
|
|
1440
|
+
if (rawPrompt.startsWith("/new ")) {
|
|
1441
|
+
const inputPath = rawPrompt.slice(5).trim();
|
|
1442
|
+
if (!inputPath) return { type: "new" };
|
|
1443
|
+
return {
|
|
1444
|
+
type: "new",
|
|
1445
|
+
cwd: await resolveValidatedCwd(inputPath, currentCwd)
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
type: "prompt",
|
|
1450
|
+
prompt: rawPrompt
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function parseCardActionValue(data) {
|
|
1454
|
+
const payload = data.action?.value ?? data.event?.action?.value;
|
|
1455
|
+
if (!payload) return null;
|
|
1456
|
+
let value = payload;
|
|
1457
|
+
if (typeof value === "string") try {
|
|
1458
|
+
value = JSON.parse(value);
|
|
1459
|
+
} catch {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
if (!value || typeof value !== "object") return null;
|
|
1463
|
+
const item = value;
|
|
1464
|
+
if (item.type === "access" && typeof item.cardId === "string" && typeof item.chatId === "string" && (item.action === "set" || item.action === "clear")) {
|
|
1465
|
+
if (item.action === "clear") return {
|
|
1466
|
+
type: "access",
|
|
1467
|
+
cardId: item.cardId,
|
|
1468
|
+
chatId: item.chatId,
|
|
1469
|
+
action: "clear"
|
|
1470
|
+
};
|
|
1471
|
+
if (item.mode === "standard" || item.mode === "full-access") return {
|
|
1472
|
+
type: "access",
|
|
1473
|
+
cardId: item.cardId,
|
|
1474
|
+
chatId: item.chatId,
|
|
1475
|
+
action: "set",
|
|
1476
|
+
mode: item.mode
|
|
1477
|
+
};
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
if (item.type === "model" && typeof item.cardId === "string" && typeof item.chatId === "string" && typeof item.model === "string") return {
|
|
1481
|
+
type: "model",
|
|
1482
|
+
cardId: item.cardId,
|
|
1483
|
+
chatId: item.chatId,
|
|
1484
|
+
model: item.model
|
|
1485
|
+
};
|
|
1486
|
+
if (typeof item.requestId === "string" && typeof item.taskId === "string" && (item.decision === "approved" || item.decision === "rejected")) return {
|
|
1487
|
+
type: "approval",
|
|
1488
|
+
requestId: item.requestId,
|
|
1489
|
+
taskId: item.taskId,
|
|
1490
|
+
decision: item.decision,
|
|
1491
|
+
comment: typeof item.comment === "string" ? item.comment : void 0
|
|
1492
|
+
};
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
async function resolveValidatedCwd(inputPath, baseCwd) {
|
|
1496
|
+
const candidate = isAbsolute(inputPath) ? inputPath : resolve(baseCwd, inputPath);
|
|
1497
|
+
const dirStat = await stat(candidate).catch(() => void 0);
|
|
1498
|
+
if (!dirStat || !dirStat.isDirectory()) throw new Error(`路径不存在或不是目录:${candidate}`);
|
|
1499
|
+
return resolve(candidate);
|
|
1500
|
+
}
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region src/feishu/card-renderer.ts
|
|
1503
|
+
var TaskCardStreamer = class {
|
|
1504
|
+
#segments = [];
|
|
1505
|
+
#tools = /* @__PURE__ */ new Map();
|
|
1506
|
+
#status = "running";
|
|
1507
|
+
#summary = "执行中";
|
|
1508
|
+
#lastPatchedAt = 0;
|
|
1509
|
+
#pendingPatchTimer;
|
|
1510
|
+
#pendingPatchResolvers = [];
|
|
1511
|
+
#patchQueue = Promise.resolve();
|
|
1512
|
+
#cardMessageId;
|
|
1513
|
+
constructor(deps) {
|
|
1514
|
+
this.deps = deps;
|
|
1515
|
+
}
|
|
1516
|
+
handleOutputChunk(chunk) {
|
|
1517
|
+
const toolUpdate = this.deps.renderer.parseToolChunk(chunk);
|
|
1518
|
+
if (toolUpdate) {
|
|
1519
|
+
this.upsertTool(toolUpdate);
|
|
1520
|
+
this.enqueuePatch(false);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const last = this.#segments[this.#segments.length - 1];
|
|
1524
|
+
if (last?.type === "text") last.content += chunk;
|
|
1525
|
+
else this.#segments.push({
|
|
1526
|
+
type: "text",
|
|
1527
|
+
content: chunk
|
|
1528
|
+
});
|
|
1529
|
+
this.enqueuePatch(false);
|
|
1530
|
+
}
|
|
1531
|
+
handleToolUpdate(update) {
|
|
1532
|
+
const toolUpdate = this.deps.renderer.toToolView(update);
|
|
1533
|
+
if (!toolUpdate) return;
|
|
1534
|
+
this.upsertTool(toolUpdate);
|
|
1535
|
+
this.enqueuePatch(false);
|
|
1536
|
+
}
|
|
1537
|
+
markFailed(error) {
|
|
1538
|
+
this.#status = "failed";
|
|
1539
|
+
this.#summary = error;
|
|
1540
|
+
this.enqueuePatch(true);
|
|
1541
|
+
}
|
|
1542
|
+
markCompleted(summary) {
|
|
1543
|
+
this.#status = "completed";
|
|
1544
|
+
this.#summary = summary ?? "执行完成";
|
|
1545
|
+
this.enqueuePatch(true);
|
|
1546
|
+
}
|
|
1547
|
+
async finalize() {
|
|
1548
|
+
this.enqueuePatch(true);
|
|
1549
|
+
if (this.#pendingPatchTimer) await new Promise((resolve) => {
|
|
1550
|
+
this.#pendingPatchResolvers.push(resolve);
|
|
1551
|
+
});
|
|
1552
|
+
await this.#patchQueue;
|
|
1553
|
+
if (!this.#cardMessageId) await this.ensureCardMessage();
|
|
1554
|
+
}
|
|
1555
|
+
upsertTool(toolUpdate) {
|
|
1556
|
+
const prev = this.#tools.get(toolUpdate.id) ?? { id: toolUpdate.id };
|
|
1557
|
+
this.#tools.set(toolUpdate.id, {
|
|
1558
|
+
...prev,
|
|
1559
|
+
...toolUpdate
|
|
1560
|
+
});
|
|
1561
|
+
if (!this.#segments.some((segment) => segment.type === "tool" && segment.id === toolUpdate.id)) this.#segments.push({
|
|
1562
|
+
type: "tool",
|
|
1563
|
+
id: toolUpdate.id
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
enqueuePatch(force) {
|
|
1567
|
+
const now = Date.now();
|
|
1568
|
+
const wait = force ? 0 : Math.max(0, this.deps.patchMinIntervalMs - (now - this.#lastPatchedAt));
|
|
1569
|
+
if (!force && this.#pendingPatchTimer) return;
|
|
1570
|
+
if (this.#pendingPatchTimer) {
|
|
1571
|
+
clearTimeout(this.#pendingPatchTimer);
|
|
1572
|
+
this.#pendingPatchTimer = void 0;
|
|
1573
|
+
}
|
|
1574
|
+
this.#pendingPatchTimer = setTimeout(() => {
|
|
1575
|
+
this.#pendingPatchTimer = void 0;
|
|
1576
|
+
this.#lastPatchedAt = Date.now();
|
|
1577
|
+
while (this.#pendingPatchResolvers.length > 0) this.#pendingPatchResolvers.shift()?.();
|
|
1578
|
+
this.#patchQueue = this.#patchQueue.then(async () => {
|
|
1579
|
+
const messageId = await this.ensureCardMessage();
|
|
1580
|
+
if (!messageId) return;
|
|
1581
|
+
await this.deps.patchCard(messageId, this.deps.renderer.buildTaskCard({
|
|
1582
|
+
status: this.#status,
|
|
1583
|
+
segments: this.#segments,
|
|
1584
|
+
tools: this.#tools,
|
|
1585
|
+
summary: this.#summary
|
|
1586
|
+
}));
|
|
1587
|
+
}).catch((error) => {
|
|
1588
|
+
this.deps.logger.warn("feishu patch card failed", { error: error instanceof Error ? error.message : String(error) });
|
|
1589
|
+
});
|
|
1590
|
+
}, wait);
|
|
1591
|
+
}
|
|
1592
|
+
async ensureCardMessage() {
|
|
1593
|
+
if (this.#cardMessageId) return this.#cardMessageId;
|
|
1594
|
+
if (this.#status === "running" && this.#segments.length === 0) return;
|
|
1595
|
+
this.#cardMessageId = await this.deps.sendCard(this.deps.chatId, this.deps.renderer.buildTaskCard({
|
|
1596
|
+
status: this.#status,
|
|
1597
|
+
segments: this.#segments,
|
|
1598
|
+
tools: this.#tools,
|
|
1599
|
+
summary: this.#summary
|
|
1600
|
+
}));
|
|
1601
|
+
return this.#cardMessageId;
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
var FeishuCardRenderer = class {
|
|
1605
|
+
buildTaskCard(params) {
|
|
1606
|
+
const footerElement = params.status === "running" ? {
|
|
1607
|
+
tag: "markdown",
|
|
1608
|
+
content: " ",
|
|
1609
|
+
icon: {
|
|
1610
|
+
tag: "custom_icon",
|
|
1611
|
+
img_key: "img_v3_02vb_496bec09-4b43-4773-ad6b-0cdd103cd2bg",
|
|
1612
|
+
size: "16px 16px"
|
|
1613
|
+
},
|
|
1614
|
+
element_id: "loading_icon"
|
|
1615
|
+
} : params.status === "completed" ? {
|
|
1616
|
+
tag: "div",
|
|
1617
|
+
text: {
|
|
1618
|
+
tag: "plain_text",
|
|
1619
|
+
content: "已完成",
|
|
1620
|
+
text_size: "cus-0"
|
|
1621
|
+
}
|
|
1622
|
+
} : {
|
|
1623
|
+
tag: "div",
|
|
1624
|
+
text: {
|
|
1625
|
+
tag: "plain_text",
|
|
1626
|
+
content: `执行失败:${params.summary}`,
|
|
1627
|
+
text_size: "cus-0"
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
const elements = this.buildContentElements(params.segments, params.tools);
|
|
1631
|
+
if (elements.length === 0) elements.push({
|
|
1632
|
+
tag: "markdown",
|
|
1633
|
+
content: " "
|
|
1634
|
+
});
|
|
1635
|
+
elements.push(footerElement);
|
|
1636
|
+
return {
|
|
1637
|
+
schema: "2.0",
|
|
1638
|
+
config: {
|
|
1639
|
+
update_multi: true,
|
|
1640
|
+
width_mode: "fill",
|
|
1641
|
+
streaming_mode: true,
|
|
1642
|
+
style: {
|
|
1643
|
+
text_size: {
|
|
1644
|
+
"cus-0": {
|
|
1645
|
+
default: "notation",
|
|
1646
|
+
pc: "notation",
|
|
1647
|
+
mobile: "notation"
|
|
1648
|
+
},
|
|
1649
|
+
"cus-1": {
|
|
1650
|
+
default: "small",
|
|
1651
|
+
pc: "small",
|
|
1652
|
+
mobile: "small"
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
color: {
|
|
1656
|
+
foot_gray: {
|
|
1657
|
+
light_mode: "rgba(100,106,115,1)",
|
|
1658
|
+
dark_mode: "rgba(182,188,196,1)"
|
|
1659
|
+
},
|
|
1660
|
+
tool_meta_gray: {
|
|
1661
|
+
light_mode: "rgba(100,106,115,1)",
|
|
1662
|
+
dark_mode: "rgba(182,188,196,1)"
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
},
|
|
1666
|
+
summary: { content: params.status === "running" ? "生成中" : params.status === "completed" ? "已完成" : `执行失败:${params.summary}` }
|
|
1667
|
+
},
|
|
1668
|
+
body: {
|
|
1669
|
+
direction: "vertical",
|
|
1670
|
+
vertical_spacing: "6px",
|
|
1671
|
+
padding: "10px 12px 10px 12px",
|
|
1672
|
+
elements
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
buildApprovalSummaryCard(title, decision) {
|
|
1677
|
+
return {
|
|
1678
|
+
schema: "2.0",
|
|
1679
|
+
config: { update_multi: true },
|
|
1680
|
+
body: {
|
|
1681
|
+
direction: "vertical",
|
|
1682
|
+
padding: "10px 12px 10px 12px",
|
|
1683
|
+
elements: [
|
|
1684
|
+
{
|
|
1685
|
+
tag: "markdown",
|
|
1686
|
+
content: `**${this.escapeCardMarkdown(title)}**`
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
tag: "markdown",
|
|
1690
|
+
content: `request: ${this.escapeCardMarkdown(decision.requestId)}`
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
tag: "markdown",
|
|
1694
|
+
content: `时间: ${this.escapeCardMarkdown(decision.decidedAt)}`
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
tag: "markdown",
|
|
1698
|
+
content: `操作者: ${this.escapeCardMarkdown(decision.decidedBy)}`
|
|
1699
|
+
}
|
|
1700
|
+
]
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
buildAccessModeCard(params) {
|
|
1705
|
+
const currentMode = params.overrideMode ?? params.defaultMode;
|
|
1706
|
+
const overrideText = params.overrideMode ? this.formatAccessMode(params.overrideMode) : "继承默认";
|
|
1707
|
+
const sourceText = params.overrideMode ? "会话临时覆盖" : "全局默认";
|
|
1708
|
+
const fullActive = currentMode === "full-access";
|
|
1709
|
+
const standardActive = currentMode === "standard" && Boolean(params.overrideMode);
|
|
1710
|
+
const inheritActive = !params.overrideMode;
|
|
1711
|
+
return {
|
|
1712
|
+
schema: "2.0",
|
|
1713
|
+
config: {
|
|
1714
|
+
update_multi: true,
|
|
1715
|
+
width_mode: "fill",
|
|
1716
|
+
style: {
|
|
1717
|
+
text_size: { "cus-0": {
|
|
1718
|
+
default: "notation",
|
|
1719
|
+
pc: "notation",
|
|
1720
|
+
mobile: "notation"
|
|
1721
|
+
} },
|
|
1722
|
+
color: { foot_gray: {
|
|
1723
|
+
light_mode: "rgba(100,106,115,1)",
|
|
1724
|
+
dark_mode: "rgba(182,188,196,1)"
|
|
1725
|
+
} }
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
body: {
|
|
1729
|
+
direction: "vertical",
|
|
1730
|
+
padding: "10px 12px 10px 12px",
|
|
1731
|
+
elements: [
|
|
1732
|
+
{
|
|
1733
|
+
tag: "markdown",
|
|
1734
|
+
content: "**权限模式**"
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
tag: "markdown",
|
|
1738
|
+
content: `全局默认: ${this.escapeCardMarkdown(this.formatAccessMode(params.defaultMode))}`
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
tag: "markdown",
|
|
1742
|
+
content: `本会话覆盖: ${this.escapeCardMarkdown(overrideText)}${inheritActive ? " ✅" : ""}`
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
tag: "markdown",
|
|
1746
|
+
content: `当前生效: ${this.escapeCardMarkdown(this.formatAccessMode(currentMode))}(${this.escapeCardMarkdown(sourceText)}) ✅`
|
|
1747
|
+
},
|
|
1748
|
+
...params.readonly ? [{
|
|
1749
|
+
tag: "markdown",
|
|
1750
|
+
content: params.readonlyReason ?? "本卡已应用变更,按钮已锁定。"
|
|
1751
|
+
}] : [{
|
|
1752
|
+
tag: "column_set",
|
|
1753
|
+
columns: [
|
|
1754
|
+
this.buildActionButton(fullActive ? "本会话 Full Access(当前)" : "本会话 Full Access", fullActive ? "primary" : "default", {
|
|
1755
|
+
type: "access",
|
|
1756
|
+
cardId: params.cardId,
|
|
1757
|
+
chatId: params.chatId,
|
|
1758
|
+
action: "set",
|
|
1759
|
+
mode: "full-access"
|
|
1760
|
+
}),
|
|
1761
|
+
this.buildActionButton(standardActive ? "本会话 Standard(当前)" : "本会话 Standard", standardActive ? "primary" : "default", {
|
|
1762
|
+
type: "access",
|
|
1763
|
+
cardId: params.cardId,
|
|
1764
|
+
chatId: params.chatId,
|
|
1765
|
+
action: "set",
|
|
1766
|
+
mode: "standard"
|
|
1767
|
+
}),
|
|
1768
|
+
this.buildActionButton(inheritActive ? "恢复默认(当前)" : "恢复默认", inheritActive ? "primary" : "default", {
|
|
1769
|
+
type: "access",
|
|
1770
|
+
cardId: params.cardId,
|
|
1771
|
+
chatId: params.chatId,
|
|
1772
|
+
action: "clear"
|
|
1773
|
+
})
|
|
1774
|
+
]
|
|
1775
|
+
}],
|
|
1776
|
+
{
|
|
1777
|
+
tag: "div",
|
|
1778
|
+
text: {
|
|
1779
|
+
tag: "plain_text",
|
|
1780
|
+
content: "提示:会话覆盖仅当前会话有效,切换/重置会话后恢复默认。",
|
|
1781
|
+
text_size: "cus-0"
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
]
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
buildModelCard(params) {
|
|
1789
|
+
const modelRows = [];
|
|
1790
|
+
for (let index = 0; index < params.models.length; index += 3) {
|
|
1791
|
+
const items = params.models.slice(index, index + 3);
|
|
1792
|
+
modelRows.push({
|
|
1793
|
+
tag: "column_set",
|
|
1794
|
+
columns: items.map((model) => this.buildActionButton(model.id === params.currentModel ? `${model.name}(当前)` : model.name, model.id === params.currentModel ? "primary" : "default", {
|
|
1795
|
+
type: "model",
|
|
1796
|
+
cardId: params.cardId,
|
|
1797
|
+
chatId: params.chatId,
|
|
1798
|
+
model: model.id
|
|
1799
|
+
}))
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
return {
|
|
1803
|
+
schema: "2.0",
|
|
1804
|
+
config: {
|
|
1805
|
+
update_multi: true,
|
|
1806
|
+
width_mode: "fill"
|
|
1807
|
+
},
|
|
1808
|
+
body: {
|
|
1809
|
+
direction: "vertical",
|
|
1810
|
+
padding: "10px 12px 10px 12px",
|
|
1811
|
+
elements: [
|
|
1812
|
+
{
|
|
1813
|
+
tag: "markdown",
|
|
1814
|
+
content: "**模型切换**"
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
tag: "markdown",
|
|
1818
|
+
content: `当前模型: ${this.escapeCardMarkdown(params.currentModel ?? "未设置")}`
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
tag: "markdown",
|
|
1822
|
+
content: `可选模型: ${this.escapeCardMarkdown(params.models.map((item) => item.id).join(", "))}`
|
|
1823
|
+
},
|
|
1824
|
+
...params.readonly ? [{
|
|
1825
|
+
tag: "markdown",
|
|
1826
|
+
content: params.readonlyReason ?? "本卡已应用变更,按钮已锁定。"
|
|
1827
|
+
}] : [...modelRows],
|
|
1828
|
+
{
|
|
1829
|
+
tag: "div",
|
|
1830
|
+
text: {
|
|
1831
|
+
tag: "plain_text",
|
|
1832
|
+
content: "提示:切换模型会重置当前会话,后续消息使用新模型。",
|
|
1833
|
+
text_size: "cus-0"
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
]
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
buildApprovalCard(request, status, decision) {
|
|
1841
|
+
const statusText = status === "pending" ? "待审批" : status === "approved" ? "已批准" : status === "rejected" ? "已拒绝" : "已超时";
|
|
1842
|
+
const elements = [
|
|
1843
|
+
{
|
|
1844
|
+
tag: "markdown",
|
|
1845
|
+
content: `**状态:${this.escapeCardMarkdown(statusText)}**`
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
tag: "markdown",
|
|
1849
|
+
content: `类型: ${this.escapeCardMarkdown(request.kind)} | 风险: ${this.escapeCardMarkdown(request.riskLevel)}`
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
tag: "markdown",
|
|
1853
|
+
content: `标题: ${this.escapeCardMarkdown(request.title)}`
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
tag: "markdown",
|
|
1857
|
+
content: `工作目录: ${this.escapeCardMarkdown(request.cwd)}`
|
|
1858
|
+
}
|
|
1859
|
+
];
|
|
1860
|
+
if (request.command) elements.push({
|
|
1861
|
+
tag: "markdown",
|
|
1862
|
+
content: `命令: \`${this.escapeCardMarkdown(this.shorten(request.command, 140))}\``
|
|
1863
|
+
});
|
|
1864
|
+
if (request.target) elements.push({
|
|
1865
|
+
tag: "markdown",
|
|
1866
|
+
content: `目标: ${this.escapeCardMarkdown(request.target)}`
|
|
1867
|
+
});
|
|
1868
|
+
if (status === "pending") elements.push({
|
|
1869
|
+
tag: "column_set",
|
|
1870
|
+
columns: [
|
|
1871
|
+
this.buildApprovalButton("批准", "primary", {
|
|
1872
|
+
requestId: request.id,
|
|
1873
|
+
taskId: request.taskId,
|
|
1874
|
+
decision: "approved"
|
|
1875
|
+
}),
|
|
1876
|
+
this.buildApprovalButton("本会话允许", "default", {
|
|
1877
|
+
requestId: request.id,
|
|
1878
|
+
taskId: request.taskId,
|
|
1879
|
+
decision: "approved",
|
|
1880
|
+
comment: "approved-for-session"
|
|
1881
|
+
}),
|
|
1882
|
+
this.buildApprovalButton("拒绝", "danger", {
|
|
1883
|
+
requestId: request.id,
|
|
1884
|
+
taskId: request.taskId,
|
|
1885
|
+
decision: "rejected"
|
|
1886
|
+
})
|
|
1887
|
+
]
|
|
1888
|
+
});
|
|
1889
|
+
else if (decision) elements.push({
|
|
1890
|
+
tag: "markdown",
|
|
1891
|
+
content: `操作人: ${this.escapeCardMarkdown(decision.decidedBy)}\\n时间: ${this.escapeCardMarkdown(decision.decidedAt)}`
|
|
1892
|
+
});
|
|
1893
|
+
return {
|
|
1894
|
+
schema: "2.0",
|
|
1895
|
+
config: {
|
|
1896
|
+
update_multi: true,
|
|
1897
|
+
width_mode: "fill"
|
|
1898
|
+
},
|
|
1899
|
+
body: {
|
|
1900
|
+
direction: "vertical",
|
|
1901
|
+
padding: "10px 12px 10px 12px",
|
|
1902
|
+
elements
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
parseToolChunk(chunk) {
|
|
1907
|
+
const lines = chunk.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1908
|
+
if (lines.length === 0 || !lines[0]?.startsWith("[tool]")) return;
|
|
1909
|
+
const headMatch = lines[0].match(/^\[tool\]\s+(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
1910
|
+
if (!headMatch?.[1]) return;
|
|
1911
|
+
const tool = {
|
|
1912
|
+
id: headMatch[1].trim(),
|
|
1913
|
+
status: headMatch[2]?.trim()
|
|
1914
|
+
};
|
|
1915
|
+
for (const line of lines.slice(1)) {
|
|
1916
|
+
if (line.startsWith("cmd:")) {
|
|
1917
|
+
tool.cmd = line.slice(4).trim();
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (line.startsWith("path:")) {
|
|
1921
|
+
tool.path = line.slice(5).trim();
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
if (line.startsWith("error:")) tool.error = line.slice(6).trim();
|
|
1925
|
+
}
|
|
1926
|
+
return tool;
|
|
1927
|
+
}
|
|
1928
|
+
toToolView(update) {
|
|
1929
|
+
if (!update.toolCallId) return;
|
|
1930
|
+
const cmd = update.command ?? update.query ?? update.url ?? update.title ?? update.toolName;
|
|
1931
|
+
return {
|
|
1932
|
+
id: update.toolCallId,
|
|
1933
|
+
name: update.toolName,
|
|
1934
|
+
status: update.status,
|
|
1935
|
+
cmd,
|
|
1936
|
+
query: update.query,
|
|
1937
|
+
url: update.url,
|
|
1938
|
+
path: update.path,
|
|
1939
|
+
error: update.error
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
buildContentElements(segments, tools) {
|
|
1943
|
+
const elements = [];
|
|
1944
|
+
const maxToolCards = 12;
|
|
1945
|
+
let renderedToolCards = 0;
|
|
1946
|
+
let hiddenToolCards = 0;
|
|
1947
|
+
let lastToolPath;
|
|
1948
|
+
let lastToolSignature;
|
|
1949
|
+
let index = 0;
|
|
1950
|
+
while (index < segments.length) {
|
|
1951
|
+
const segment = segments[index];
|
|
1952
|
+
if (!segment) break;
|
|
1953
|
+
if (segment.type === "text") {
|
|
1954
|
+
if (!segment.content) {
|
|
1955
|
+
index += 1;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
elements.push({
|
|
1959
|
+
tag: "markdown",
|
|
1960
|
+
content: this.escapeCardMarkdown(segment.content)
|
|
1961
|
+
});
|
|
1962
|
+
index += 1;
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
const toolCards = [];
|
|
1966
|
+
while (index < segments.length && segments[index]?.type === "tool") {
|
|
1967
|
+
const current = segments[index];
|
|
1968
|
+
const tool = tools.get(current.id);
|
|
1969
|
+
if (!tool) {
|
|
1970
|
+
index += 1;
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
const statusText = this.formatToolStatus(tool.status);
|
|
1974
|
+
const statusBadge = this.getToolStatusBadge(tool.status);
|
|
1975
|
+
const cmdText = tool.cmd ? this.shorten(tool.cmd, 140) : "(无命令)";
|
|
1976
|
+
const pathText = tool.path ? this.shorten(tool.path, 90) : void 0;
|
|
1977
|
+
const errorText = tool.error ? this.shorten(tool.error, 160) : void 0;
|
|
1978
|
+
const title = tool.name ?? this.getToolDisplayTitle(tool.cmd);
|
|
1979
|
+
const signature = `${statusText}|${cmdText}|${pathText ?? ""}|${errorText ?? ""}`;
|
|
1980
|
+
if (signature === lastToolSignature) {
|
|
1981
|
+
index += 1;
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
lastToolSignature = signature;
|
|
1985
|
+
if (renderedToolCards >= maxToolCards) {
|
|
1986
|
+
hiddenToolCards += 1;
|
|
1987
|
+
index += 1;
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
const cardElements = [
|
|
1991
|
+
{
|
|
1992
|
+
tag: "div",
|
|
1993
|
+
text: {
|
|
1994
|
+
tag: "plain_text",
|
|
1995
|
+
content: `${statusBadge} ${title}`
|
|
1996
|
+
}
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
tag: "div",
|
|
2000
|
+
text: {
|
|
2001
|
+
tag: "plain_text",
|
|
2002
|
+
content: cmdText
|
|
2003
|
+
}
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
tag: "markdown",
|
|
2007
|
+
content: `<font color='grey'>状态:${this.escapeCardMarkdown(statusText)}</font>`
|
|
2008
|
+
}
|
|
2009
|
+
];
|
|
2010
|
+
if (pathText && pathText !== lastToolPath) {
|
|
2011
|
+
cardElements.push({
|
|
2012
|
+
tag: "markdown",
|
|
2013
|
+
content: `<font color='grey'>↳ ${this.escapeCardMarkdown(pathText)}</font>`
|
|
2014
|
+
});
|
|
2015
|
+
lastToolPath = pathText;
|
|
2016
|
+
}
|
|
2017
|
+
if (errorText) cardElements.push({
|
|
2018
|
+
tag: "div",
|
|
2019
|
+
text: {
|
|
2020
|
+
tag: "plain_text",
|
|
2021
|
+
content: `❗ ${errorText}`,
|
|
2022
|
+
text_size: "cus-1"
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
toolCards.push({
|
|
2026
|
+
tag: "interactive_container",
|
|
2027
|
+
has_border: true,
|
|
2028
|
+
corner_radius: "10px",
|
|
2029
|
+
padding: "8px 10px 8px 10px",
|
|
2030
|
+
margin: "4px 0 4px 0",
|
|
2031
|
+
elements: cardElements
|
|
2032
|
+
});
|
|
2033
|
+
renderedToolCards += 1;
|
|
2034
|
+
index += 1;
|
|
2035
|
+
}
|
|
2036
|
+
if (toolCards.length > 0) elements.push({
|
|
2037
|
+
tag: "collapsible_panel",
|
|
2038
|
+
header: { title: {
|
|
2039
|
+
tag: "plain_text",
|
|
2040
|
+
content: `🛠️ 工具调用(${toolCards.length})`
|
|
2041
|
+
} },
|
|
2042
|
+
elements: toolCards
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
if (hiddenToolCards > 0) elements.push({
|
|
2046
|
+
tag: "markdown",
|
|
2047
|
+
content: `…已折叠 ${hiddenToolCards} 条工具调用`
|
|
2048
|
+
});
|
|
2049
|
+
return elements;
|
|
2050
|
+
}
|
|
2051
|
+
buildApprovalButton(label, type, value) {
|
|
2052
|
+
return {
|
|
2053
|
+
tag: "column",
|
|
2054
|
+
width: "weighted",
|
|
2055
|
+
weight: 1,
|
|
2056
|
+
elements: [{
|
|
2057
|
+
tag: "button",
|
|
2058
|
+
type,
|
|
2059
|
+
text: {
|
|
2060
|
+
tag: "plain_text",
|
|
2061
|
+
content: label
|
|
2062
|
+
},
|
|
2063
|
+
behaviors: [{
|
|
2064
|
+
type: "callback",
|
|
2065
|
+
value
|
|
2066
|
+
}]
|
|
2067
|
+
}]
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
buildActionButton(label, type, value) {
|
|
2071
|
+
return {
|
|
2072
|
+
tag: "column",
|
|
2073
|
+
width: "weighted",
|
|
2074
|
+
weight: 1,
|
|
2075
|
+
elements: [{
|
|
2076
|
+
tag: "button",
|
|
2077
|
+
type,
|
|
2078
|
+
text: {
|
|
2079
|
+
tag: "plain_text",
|
|
2080
|
+
content: label
|
|
2081
|
+
},
|
|
2082
|
+
behaviors: [{
|
|
2083
|
+
type: "callback",
|
|
2084
|
+
value
|
|
2085
|
+
}]
|
|
2086
|
+
}]
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
formatToolStatus(status) {
|
|
2090
|
+
if (!status) return "处理中";
|
|
2091
|
+
if (status === "in_progress") return "进行中";
|
|
2092
|
+
if (status === "completed") return "已完成";
|
|
2093
|
+
if (status === "failed") return "失败";
|
|
2094
|
+
return status;
|
|
2095
|
+
}
|
|
2096
|
+
getToolStatusBadge(_status) {
|
|
2097
|
+
return "🛠️";
|
|
2098
|
+
}
|
|
2099
|
+
getToolDisplayTitle(cmd) {
|
|
2100
|
+
if (!cmd) return "步骤";
|
|
2101
|
+
const head = cmd.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
2102
|
+
if (!head) return "步骤";
|
|
2103
|
+
if (head === "cat" || head === "sed" || head === "head" || head === "tail") return "读取";
|
|
2104
|
+
if (head === "ls" || head === "find" || head === "rg" || head === "grep") return "检索";
|
|
2105
|
+
if (head === "git") return "Git";
|
|
2106
|
+
if (head === "npm" || head === "pnpm" || head === "vp") return "命令";
|
|
2107
|
+
return "步骤";
|
|
2108
|
+
}
|
|
2109
|
+
formatAccessMode(mode) {
|
|
2110
|
+
return mode === "full-access" ? "Full Access" : "Standard";
|
|
2111
|
+
}
|
|
2112
|
+
shorten(text, maxLen) {
|
|
2113
|
+
if (text.length <= maxLen) return text;
|
|
2114
|
+
return `${text.slice(0, maxLen)}...`;
|
|
2115
|
+
}
|
|
2116
|
+
escapeCardMarkdown(text) {
|
|
2117
|
+
return text.replace(/\r/g, "").replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
//#endregion
|
|
2121
|
+
//#region src/feishu/command-handler.ts
|
|
2122
|
+
var FeishuCommandHandler = class {
|
|
2123
|
+
constructor(deps) {
|
|
2124
|
+
this.deps = deps;
|
|
2125
|
+
}
|
|
2126
|
+
async handle(params) {
|
|
2127
|
+
const { chatId, workspace, command } = params;
|
|
2128
|
+
if (command.type === "prompt") return {
|
|
2129
|
+
type: "prompt",
|
|
2130
|
+
prompt: command.prompt
|
|
2131
|
+
};
|
|
2132
|
+
if (command.type === "help") {
|
|
2133
|
+
await this.deps.messageClient.sendText(chatId, [
|
|
2134
|
+
"支持指令:",
|
|
2135
|
+
"/help - 展示指令说明",
|
|
2136
|
+
"/new [path] - 新建会话,可选切换工作目录",
|
|
2137
|
+
"/model - 查看并切换当前模型",
|
|
2138
|
+
"/model <name> - 直接切换到指定模型",
|
|
2139
|
+
"/status - 查看当前会话状态",
|
|
2140
|
+
"/stop - 打断当前执行中的任务",
|
|
2141
|
+
"/perm - 查看并切换权限模式"
|
|
2142
|
+
].join("\n"));
|
|
2143
|
+
return { type: "handled" };
|
|
2144
|
+
}
|
|
2145
|
+
if (command.type === "new") {
|
|
2146
|
+
const next = await this.deps.sessionController.startNewConversation({
|
|
2147
|
+
chatId,
|
|
2148
|
+
workspace,
|
|
2149
|
+
agent: "codex",
|
|
2150
|
+
cwd: command.cwd
|
|
2151
|
+
}, this.deps.taskRunner);
|
|
2152
|
+
await this.deps.messageClient.sendText(chatId, `已切换到新会话,session_id: ${next.sessionId}\n工作目录:${next.workspace.cwd}`);
|
|
2153
|
+
return { type: "handled" };
|
|
2154
|
+
}
|
|
2155
|
+
if (command.type === "status") {
|
|
2156
|
+
await this.deps.messageClient.sendText(chatId, [
|
|
2157
|
+
`session_id: ${this.deps.sessionController.getBridgeSessionId(chatId) ?? "未创建"}`,
|
|
2158
|
+
`codex_session_id: ${this.deps.sessionController.getResumeSessionId(chatId) ?? "未创建"}`,
|
|
2159
|
+
`工具目录(cwd): ${workspace.cwd}`,
|
|
2160
|
+
`当前模型: ${this.deps.sessionController.getModel(chatId) ?? "未设置"}`,
|
|
2161
|
+
`权限模式: ${this.deps.sessionController.getAccessMode(chatId)}`
|
|
2162
|
+
].join("\n"));
|
|
2163
|
+
return { type: "handled" };
|
|
2164
|
+
}
|
|
2165
|
+
if (command.type === "stop") {
|
|
2166
|
+
const interrupted = await this.deps.sessionController.interrupt(chatId, this.deps.taskRunner);
|
|
2167
|
+
await this.deps.messageClient.sendText(chatId, interrupted ? "已打断当前任务。" : "当前没有执行中的任务。");
|
|
2168
|
+
return { type: "handled" };
|
|
2169
|
+
}
|
|
2170
|
+
if (command.type === "show-access") {
|
|
2171
|
+
await this.deps.cardActionHandler.sendPermissionCard(chatId);
|
|
2172
|
+
return { type: "handled" };
|
|
2173
|
+
}
|
|
2174
|
+
if (command.type === "model") {
|
|
2175
|
+
if (!command.model) {
|
|
2176
|
+
if (this.deps.sessionController.getAvailableModels(chatId).length === 0) await this.deps.sessionController.startNewConversation({
|
|
2177
|
+
chatId,
|
|
2178
|
+
workspace,
|
|
2179
|
+
agent: "codex"
|
|
2180
|
+
}, this.deps.taskRunner);
|
|
2181
|
+
await this.deps.cardActionHandler.sendModelCard(chatId);
|
|
2182
|
+
return { type: "handled" };
|
|
2183
|
+
}
|
|
2184
|
+
if (this.deps.sessionController.getAvailableModels(chatId).length === 0) await this.deps.sessionController.startNewConversation({
|
|
2185
|
+
chatId,
|
|
2186
|
+
workspace,
|
|
2187
|
+
agent: "codex"
|
|
2188
|
+
}, this.deps.taskRunner);
|
|
2189
|
+
const resolvedModels = this.deps.sessionController.getAvailableModels(chatId);
|
|
2190
|
+
if (!resolvedModels.some((item) => item.id === command.model)) {
|
|
2191
|
+
await this.deps.messageClient.sendText(chatId, `不支持的模型:${command.model}\n可用模型:${resolvedModels.map((item) => item.id).join(", ")}`);
|
|
2192
|
+
return { type: "handled" };
|
|
2193
|
+
}
|
|
2194
|
+
const changed = await this.deps.sessionController.setModel(chatId, command.model, this.deps.taskRunner);
|
|
2195
|
+
await this.deps.messageClient.sendText(chatId, changed ? `已切换模型:${this.deps.sessionController.getModel(chatId)}` : `当前模型已是:${this.deps.sessionController.getModel(chatId)}`);
|
|
2196
|
+
return { type: "handled" };
|
|
2197
|
+
}
|
|
2198
|
+
return { type: "handled" };
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
//#endregion
|
|
2202
|
+
//#region src/feishu/feishu-event-dispatcher.ts
|
|
2203
|
+
function buildFeishuEventDispatcher(deps) {
|
|
2204
|
+
const dispatcher = new lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
|
|
2205
|
+
await deps.onMessageReceived(data);
|
|
2206
|
+
} });
|
|
2207
|
+
registerLoosely(dispatcher, { "card.action.trigger": async (data) => {
|
|
2208
|
+
return await deps.onCardAction(data) ?? {};
|
|
2209
|
+
} });
|
|
2210
|
+
return dispatcher;
|
|
2211
|
+
}
|
|
2212
|
+
function registerLoosely(dispatcher, handles) {
|
|
2213
|
+
dispatcher.register(handles);
|
|
2214
|
+
}
|
|
2215
|
+
//#endregion
|
|
2216
|
+
//#region src/feishu/message-entry-queue.ts
|
|
2217
|
+
var MessageEntryQueue = class {
|
|
2218
|
+
#processingMessageIds = /* @__PURE__ */ new Set();
|
|
2219
|
+
#handledMessageIds = /* @__PURE__ */ new Map();
|
|
2220
|
+
#chatQueues = /* @__PURE__ */ new Map();
|
|
2221
|
+
constructor(dedupeTtlMs = 600 * 1e3) {
|
|
2222
|
+
this.dedupeTtlMs = dedupeTtlMs;
|
|
2223
|
+
}
|
|
2224
|
+
gcHandledMessageIds() {
|
|
2225
|
+
const now = Date.now();
|
|
2226
|
+
for (const [messageId, timestamp] of this.#handledMessageIds.entries()) if (now - timestamp > this.dedupeTtlMs) this.#handledMessageIds.delete(messageId);
|
|
2227
|
+
}
|
|
2228
|
+
tryBegin(messageId) {
|
|
2229
|
+
if (this.#handledMessageIds.has(messageId) || this.#processingMessageIds.has(messageId)) return false;
|
|
2230
|
+
this.#processingMessageIds.add(messageId);
|
|
2231
|
+
return true;
|
|
2232
|
+
}
|
|
2233
|
+
runInChatQueue(chatId, messageId, task) {
|
|
2234
|
+
if (!this.tryBegin(messageId)) return;
|
|
2235
|
+
const queue = (this.#chatQueues.get(chatId) ?? Promise.resolve()).catch(() => {}).then(async () => {
|
|
2236
|
+
await task();
|
|
2237
|
+
});
|
|
2238
|
+
this.#chatQueues.set(chatId, queue);
|
|
2239
|
+
queue.finally(() => {
|
|
2240
|
+
this.complete(messageId);
|
|
2241
|
+
if (this.#chatQueues.get(chatId) === queue) this.#chatQueues.delete(chatId);
|
|
2242
|
+
}).catch(() => void 0);
|
|
2243
|
+
return queue;
|
|
2244
|
+
}
|
|
2245
|
+
complete(messageId) {
|
|
2246
|
+
this.#processingMessageIds.delete(messageId);
|
|
2247
|
+
this.#handledMessageIds.set(messageId, Date.now());
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
//#endregion
|
|
2251
|
+
//#region src/feishu/feishu-card-action-handler.ts
|
|
2252
|
+
var FeishuCardActionHandler = class {
|
|
2253
|
+
#permissionCardMessages = /* @__PURE__ */ new Map();
|
|
2254
|
+
#permissionCardRevisions = /* @__PURE__ */ new Map();
|
|
2255
|
+
#permissionCardUpdatedAt = /* @__PURE__ */ new Map();
|
|
2256
|
+
#handledPermissionCardIds = /* @__PURE__ */ new Map();
|
|
2257
|
+
#modelCardMessages = /* @__PURE__ */ new Map();
|
|
2258
|
+
#modelCardUpdatedAt = /* @__PURE__ */ new Map();
|
|
2259
|
+
#handledModelCardIds = /* @__PURE__ */ new Map();
|
|
2260
|
+
#handledCardActionEventIds = /* @__PURE__ */ new Map();
|
|
2261
|
+
constructor(deps) {
|
|
2262
|
+
this.deps = deps;
|
|
2263
|
+
}
|
|
2264
|
+
async sendPermissionCard(chatId) {
|
|
2265
|
+
this.gcPermissionCardState();
|
|
2266
|
+
const cardId = randomUUID();
|
|
2267
|
+
const version = 1;
|
|
2268
|
+
this.#permissionCardRevisions.set(cardId, version);
|
|
2269
|
+
const card = this.deps.cardRenderer.buildAccessModeCard({
|
|
2270
|
+
cardId,
|
|
2271
|
+
chatId,
|
|
2272
|
+
defaultMode: this.deps.sessionController.getDefaultAccessMode(),
|
|
2273
|
+
overrideMode: this.deps.sessionController.getAccessOverride(chatId),
|
|
2274
|
+
readonly: false
|
|
2275
|
+
});
|
|
2276
|
+
const messageId = await this.deps.messageClient.sendCard(chatId, card);
|
|
2277
|
+
this.#permissionCardMessages.set(cardId, messageId);
|
|
2278
|
+
this.#permissionCardUpdatedAt.set(cardId, Date.now());
|
|
2279
|
+
this.deps.logger.info("permission card sent", {
|
|
2280
|
+
chatId,
|
|
2281
|
+
cardId,
|
|
2282
|
+
version,
|
|
2283
|
+
messageId,
|
|
2284
|
+
defaultMode: this.deps.sessionController.getDefaultAccessMode(),
|
|
2285
|
+
overrideMode: this.deps.sessionController.getAccessOverride(chatId),
|
|
2286
|
+
effectiveMode: this.deps.sessionController.getAccessMode(chatId)
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
async sendModelCard(chatId) {
|
|
2290
|
+
this.gcModelCardState();
|
|
2291
|
+
const cardId = randomUUID();
|
|
2292
|
+
const card = this.deps.cardRenderer.buildModelCard({
|
|
2293
|
+
cardId,
|
|
2294
|
+
chatId,
|
|
2295
|
+
currentModel: this.deps.sessionController.getModel(chatId),
|
|
2296
|
+
models: this.deps.sessionController.getAvailableModels(chatId),
|
|
2297
|
+
readonly: false
|
|
2298
|
+
});
|
|
2299
|
+
const messageId = await this.deps.messageClient.sendCard(chatId, card);
|
|
2300
|
+
this.#modelCardMessages.set(cardId, messageId);
|
|
2301
|
+
this.#modelCardUpdatedAt.set(cardId, Date.now());
|
|
2302
|
+
this.deps.logger.info("model card sent", {
|
|
2303
|
+
chatId,
|
|
2304
|
+
cardId,
|
|
2305
|
+
messageId,
|
|
2306
|
+
currentModel: this.deps.sessionController.getModel(chatId),
|
|
2307
|
+
models: this.deps.sessionController.getAvailableModels(chatId).map((item) => item.id)
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
async handleCardAction(data) {
|
|
2311
|
+
this.deps.logger.info("card action received", {
|
|
2312
|
+
topLevelKeys: Object.keys(data),
|
|
2313
|
+
eventId: this.extractCardActionEventId(data),
|
|
2314
|
+
actionMessageId: this.extractActionMessageId(data)
|
|
2315
|
+
});
|
|
2316
|
+
this.gcHandledCardActionEventIds();
|
|
2317
|
+
const eventId = this.extractCardActionEventId(data);
|
|
2318
|
+
if (eventId && this.#handledCardActionEventIds.has(eventId)) {
|
|
2319
|
+
this.deps.logger.info("card action ignored: duplicated event", { eventId });
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (eventId) this.#handledCardActionEventIds.set(eventId, Date.now());
|
|
2323
|
+
const action = parseCardActionValue(data);
|
|
2324
|
+
if (!action) {
|
|
2325
|
+
this.deps.logger.warn("card action ignored: invalid payload");
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
if (action.type === "access") return this.handleAccessAction(action, data);
|
|
2329
|
+
if (action.type === "model") return this.handleModelAction(action, data);
|
|
2330
|
+
const decision = {
|
|
2331
|
+
requestId: action.requestId,
|
|
2332
|
+
taskId: action.taskId,
|
|
2333
|
+
decision: action.decision,
|
|
2334
|
+
comment: action.comment,
|
|
2335
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2336
|
+
decidedBy: this.extractOperatorId(data)
|
|
2337
|
+
};
|
|
2338
|
+
this.deps.approvalGateway.resolve(decision);
|
|
2339
|
+
this.deps.onApprovalResolved(decision);
|
|
2340
|
+
}
|
|
2341
|
+
async handleModelAction(action, data) {
|
|
2342
|
+
this.gcHandledModelCardIds();
|
|
2343
|
+
const chatId = action.chatId;
|
|
2344
|
+
const actionMessageId = this.extractActionMessageId(data);
|
|
2345
|
+
const availableModels = this.deps.sessionController.getAvailableModels(chatId);
|
|
2346
|
+
if (availableModels.length > 0 && !availableModels.some((item) => item.id === action.model)) {
|
|
2347
|
+
this.deps.logger.warn("model card action ignored: unsupported model", {
|
|
2348
|
+
chatId,
|
|
2349
|
+
cardId: action.cardId,
|
|
2350
|
+
model: action.model
|
|
2351
|
+
});
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
if (this.#handledModelCardIds.has(action.cardId)) {
|
|
2355
|
+
this.deps.logger.info("model card action ignored: card already locked", {
|
|
2356
|
+
chatId,
|
|
2357
|
+
cardId: action.cardId,
|
|
2358
|
+
messageId: actionMessageId
|
|
2359
|
+
});
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
this.#handledModelCardIds.set(action.cardId, Date.now());
|
|
2363
|
+
try {
|
|
2364
|
+
await this.deps.sessionController.setModel(chatId, action.model, this.deps.taskRunner);
|
|
2365
|
+
this.deps.logger.info("model card action applied", {
|
|
2366
|
+
chatId,
|
|
2367
|
+
cardId: action.cardId,
|
|
2368
|
+
model: action.model
|
|
2369
|
+
});
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
this.#handledModelCardIds.delete(action.cardId);
|
|
2372
|
+
this.deps.logger.warn("model card action failed, unlock card", {
|
|
2373
|
+
chatId,
|
|
2374
|
+
cardId: action.cardId,
|
|
2375
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2376
|
+
});
|
|
2377
|
+
throw error;
|
|
2378
|
+
}
|
|
2379
|
+
const card = await this.updateModelCard(chatId, actionMessageId, action.cardId, true, false);
|
|
2380
|
+
if (!card) return;
|
|
2381
|
+
return { card: {
|
|
2382
|
+
type: "raw",
|
|
2383
|
+
data: card
|
|
2384
|
+
} };
|
|
2385
|
+
}
|
|
2386
|
+
async handleAccessAction(action, data) {
|
|
2387
|
+
this.gcHandledPermissionCardIds();
|
|
2388
|
+
const chatId = action.chatId;
|
|
2389
|
+
const actionMessageId = this.extractActionMessageId(data);
|
|
2390
|
+
this.deps.logger.info("permission card action parsed", {
|
|
2391
|
+
chatId,
|
|
2392
|
+
cardId: action.cardId,
|
|
2393
|
+
action: action.action,
|
|
2394
|
+
mode: action.action === "set" ? action.mode : void 0,
|
|
2395
|
+
actionMessageId
|
|
2396
|
+
});
|
|
2397
|
+
if (this.#handledPermissionCardIds.has(action.cardId)) {
|
|
2398
|
+
this.deps.logger.info("permission card action ignored: card already locked", {
|
|
2399
|
+
chatId,
|
|
2400
|
+
messageId: actionMessageId,
|
|
2401
|
+
cardId: action.cardId
|
|
2402
|
+
});
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
this.#handledPermissionCardIds.set(action.cardId, Date.now());
|
|
2406
|
+
try {
|
|
2407
|
+
if (action.action === "clear") await this.deps.sessionController.clearAccessOverride(chatId, this.deps.taskRunner);
|
|
2408
|
+
else await this.deps.sessionController.setAccessMode(chatId, action.mode, this.deps.taskRunner);
|
|
2409
|
+
this.deps.logger.info("permission card action applied", {
|
|
2410
|
+
chatId,
|
|
2411
|
+
cardId: action.cardId,
|
|
2412
|
+
effectiveMode: this.deps.sessionController.getAccessMode(chatId)
|
|
2413
|
+
});
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
this.#handledPermissionCardIds.delete(action.cardId);
|
|
2416
|
+
this.deps.logger.warn("permission card action failed, unlock card", {
|
|
2417
|
+
chatId,
|
|
2418
|
+
cardId: action.cardId,
|
|
2419
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2420
|
+
});
|
|
2421
|
+
throw error;
|
|
2422
|
+
}
|
|
2423
|
+
const card = await this.updatePermissionCard(chatId, actionMessageId, action.cardId, true, void 0, false);
|
|
2424
|
+
if (!card) {
|
|
2425
|
+
this.deps.logger.warn("permission card action applied but card update skipped", {
|
|
2426
|
+
chatId,
|
|
2427
|
+
cardId: action.cardId,
|
|
2428
|
+
actionMessageId
|
|
2429
|
+
});
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
return { card: {
|
|
2433
|
+
type: "raw",
|
|
2434
|
+
data: card
|
|
2435
|
+
} };
|
|
2436
|
+
}
|
|
2437
|
+
async updatePermissionCard(chatId, messageId, cardId, lockAfterPatch = false, readonlyReason, patch = true) {
|
|
2438
|
+
this.gcPermissionCardState();
|
|
2439
|
+
const resolvedCardId = cardId ?? randomUUID();
|
|
2440
|
+
const nextRevision = (this.#permissionCardRevisions.get(resolvedCardId) ?? 1) + 1;
|
|
2441
|
+
this.#permissionCardRevisions.set(resolvedCardId, nextRevision);
|
|
2442
|
+
this.#permissionCardUpdatedAt.set(resolvedCardId, Date.now());
|
|
2443
|
+
const card = this.deps.cardRenderer.buildAccessModeCard({
|
|
2444
|
+
cardId: resolvedCardId,
|
|
2445
|
+
chatId,
|
|
2446
|
+
defaultMode: this.deps.sessionController.getDefaultAccessMode(),
|
|
2447
|
+
overrideMode: this.deps.sessionController.getAccessOverride(chatId),
|
|
2448
|
+
readonly: lockAfterPatch,
|
|
2449
|
+
readonlyReason
|
|
2450
|
+
});
|
|
2451
|
+
const fromActionMessageId = messageId;
|
|
2452
|
+
const fromCardMap = this.#permissionCardMessages.get(resolvedCardId);
|
|
2453
|
+
const targetMessageId = fromActionMessageId ?? fromCardMap;
|
|
2454
|
+
if (!targetMessageId) {
|
|
2455
|
+
this.deps.logger.warn("permission card update skipped: message id not found", {
|
|
2456
|
+
chatId,
|
|
2457
|
+
cardId: resolvedCardId
|
|
2458
|
+
});
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
this.deps.logger.info("permission card patch start", {
|
|
2462
|
+
chatId,
|
|
2463
|
+
cardId: resolvedCardId,
|
|
2464
|
+
version: nextRevision,
|
|
2465
|
+
targetMessageId,
|
|
2466
|
+
targetSource: fromActionMessageId ? "action-message-id" : "card-id-map",
|
|
2467
|
+
lockAfterPatch,
|
|
2468
|
+
overrideMode: this.deps.sessionController.getAccessOverride(chatId),
|
|
2469
|
+
effectiveMode: this.deps.sessionController.getAccessMode(chatId)
|
|
2470
|
+
});
|
|
2471
|
+
try {
|
|
2472
|
+
this.#permissionCardMessages.set(resolvedCardId, targetMessageId);
|
|
2473
|
+
if (patch) {
|
|
2474
|
+
await this.deps.messageClient.patchCard(targetMessageId, card);
|
|
2475
|
+
this.deps.logger.info("permission card patch done", {
|
|
2476
|
+
chatId,
|
|
2477
|
+
cardId: resolvedCardId,
|
|
2478
|
+
version: nextRevision,
|
|
2479
|
+
targetMessageId,
|
|
2480
|
+
lockAfterPatch
|
|
2481
|
+
});
|
|
2482
|
+
} else this.deps.logger.info("permission card patch skipped: callback response mode", {
|
|
2483
|
+
chatId,
|
|
2484
|
+
cardId: resolvedCardId,
|
|
2485
|
+
version: nextRevision,
|
|
2486
|
+
targetMessageId,
|
|
2487
|
+
lockAfterPatch
|
|
2488
|
+
});
|
|
2489
|
+
return card;
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
this.deps.logger.warn("patch permission card failed", {
|
|
2492
|
+
chatId,
|
|
2493
|
+
messageId: targetMessageId,
|
|
2494
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2495
|
+
});
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
async updateModelCard(chatId, messageId, cardId, lockAfterPatch = false, patch = true) {
|
|
2500
|
+
this.gcModelCardState();
|
|
2501
|
+
const resolvedCardId = cardId ?? randomUUID();
|
|
2502
|
+
this.#modelCardUpdatedAt.set(resolvedCardId, Date.now());
|
|
2503
|
+
const card = this.deps.cardRenderer.buildModelCard({
|
|
2504
|
+
cardId: resolvedCardId,
|
|
2505
|
+
chatId,
|
|
2506
|
+
currentModel: this.deps.sessionController.getModel(chatId),
|
|
2507
|
+
models: this.deps.sessionController.getAvailableModels(chatId),
|
|
2508
|
+
readonly: lockAfterPatch
|
|
2509
|
+
});
|
|
2510
|
+
const targetMessageId = messageId ?? this.#modelCardMessages.get(resolvedCardId);
|
|
2511
|
+
if (!targetMessageId) {
|
|
2512
|
+
this.deps.logger.warn("model card update skipped: message id not found", {
|
|
2513
|
+
chatId,
|
|
2514
|
+
cardId: resolvedCardId
|
|
2515
|
+
});
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
try {
|
|
2519
|
+
this.#modelCardMessages.set(resolvedCardId, targetMessageId);
|
|
2520
|
+
if (patch) await this.deps.messageClient.patchCard(targetMessageId, card);
|
|
2521
|
+
return card;
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
this.deps.logger.warn("patch model card failed", {
|
|
2524
|
+
chatId,
|
|
2525
|
+
messageId: targetMessageId,
|
|
2526
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2527
|
+
});
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
extractOperatorId(data) {
|
|
2532
|
+
const operator = data.operator && typeof data.operator === "object" ? data.operator : void 0;
|
|
2533
|
+
const operatorId = operator?.operator_id && typeof operator.operator_id === "object" ? operator.operator_id : void 0;
|
|
2534
|
+
if (typeof operatorId?.open_id === "string") return operatorId.open_id;
|
|
2535
|
+
if (typeof operatorId?.user_id === "string") return operatorId.user_id;
|
|
2536
|
+
return "feishu-user";
|
|
2537
|
+
}
|
|
2538
|
+
extractActionMessageId(data) {
|
|
2539
|
+
const topLevel = typeof data.open_message_id === "string" ? data.open_message_id : void 0;
|
|
2540
|
+
if (topLevel) return topLevel;
|
|
2541
|
+
const eventObj = data.event && typeof data.event === "object" ? data.event : void 0;
|
|
2542
|
+
if (eventObj) {
|
|
2543
|
+
if (typeof eventObj.open_message_id === "string") return eventObj.open_message_id;
|
|
2544
|
+
const contextObj = eventObj.context && typeof eventObj.context === "object" ? eventObj.context : void 0;
|
|
2545
|
+
if (contextObj && typeof contextObj.open_message_id === "string") return contextObj.open_message_id;
|
|
2546
|
+
const messageObj = eventObj.message && typeof eventObj.message === "object" ? eventObj.message : void 0;
|
|
2547
|
+
if (messageObj && typeof messageObj.message_id === "string") return messageObj.message_id;
|
|
2548
|
+
}
|
|
2549
|
+
const messageObj = data.message && typeof data.message === "object" ? data.message : void 0;
|
|
2550
|
+
if (messageObj && typeof messageObj.message_id === "string") return messageObj.message_id;
|
|
2551
|
+
}
|
|
2552
|
+
extractCardActionEventId(data) {
|
|
2553
|
+
if (typeof data.event_id === "string") return data.event_id;
|
|
2554
|
+
const headerObj = data.header && typeof data.header === "object" ? data.header : void 0;
|
|
2555
|
+
if (headerObj && typeof headerObj.event_id === "string") return headerObj.event_id;
|
|
2556
|
+
const eventObj = data.event && typeof data.event === "object" ? data.event : void 0;
|
|
2557
|
+
if (eventObj && typeof eventObj.event_id === "string") return eventObj.event_id;
|
|
2558
|
+
}
|
|
2559
|
+
gcHandledCardActionEventIds() {
|
|
2560
|
+
const expireMs = 6e4;
|
|
2561
|
+
const now = Date.now();
|
|
2562
|
+
for (const [eventId, ts] of this.#handledCardActionEventIds.entries()) if (now - ts > expireMs) this.#handledCardActionEventIds.delete(eventId);
|
|
2563
|
+
}
|
|
2564
|
+
gcHandledPermissionCardIds() {
|
|
2565
|
+
const expireMs = 10 * 6e4;
|
|
2566
|
+
const now = Date.now();
|
|
2567
|
+
for (const [cardId, ts] of this.#handledPermissionCardIds.entries()) if (now - ts > expireMs) this.#handledPermissionCardIds.delete(cardId);
|
|
2568
|
+
}
|
|
2569
|
+
gcHandledModelCardIds() {
|
|
2570
|
+
const expireMs = 10 * 6e4;
|
|
2571
|
+
const now = Date.now();
|
|
2572
|
+
for (const [cardId, ts] of this.#handledModelCardIds.entries()) if (now - ts > expireMs) this.#handledModelCardIds.delete(cardId);
|
|
2573
|
+
}
|
|
2574
|
+
gcPermissionCardState() {
|
|
2575
|
+
const expireMs = 1440 * 60 * 1e3;
|
|
2576
|
+
const now = Date.now();
|
|
2577
|
+
for (const [cardId, ts] of this.#permissionCardUpdatedAt.entries()) if (now - ts > expireMs) {
|
|
2578
|
+
this.#permissionCardUpdatedAt.delete(cardId);
|
|
2579
|
+
this.#permissionCardRevisions.delete(cardId);
|
|
2580
|
+
this.#permissionCardMessages.delete(cardId);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
gcModelCardState() {
|
|
2584
|
+
const expireMs = 1440 * 60 * 1e3;
|
|
2585
|
+
const now = Date.now();
|
|
2586
|
+
for (const [cardId, ts] of this.#modelCardUpdatedAt.entries()) if (now - ts > expireMs) {
|
|
2587
|
+
this.#modelCardUpdatedAt.delete(cardId);
|
|
2588
|
+
this.#modelCardMessages.delete(cardId);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
//#endregion
|
|
2593
|
+
//#region src/feishu/feishu-message-client.ts
|
|
2594
|
+
var FeishuMessageClient = class {
|
|
2595
|
+
constructor(client, logger) {
|
|
2596
|
+
this.client = client;
|
|
2597
|
+
this.logger = logger;
|
|
2598
|
+
}
|
|
2599
|
+
async sendText(chatId, text) {
|
|
2600
|
+
await this.client.im.v1.message.create({
|
|
2601
|
+
params: { receive_id_type: "chat_id" },
|
|
2602
|
+
data: {
|
|
2603
|
+
receive_id: chatId,
|
|
2604
|
+
msg_type: "text",
|
|
2605
|
+
content: JSON.stringify({ text })
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
async sendCard(chatId, card) {
|
|
2610
|
+
const messageId = (await this.client.im.v1.message.create({
|
|
2611
|
+
params: { receive_id_type: "chat_id" },
|
|
2612
|
+
data: {
|
|
2613
|
+
receive_id: chatId,
|
|
2614
|
+
msg_type: "interactive",
|
|
2615
|
+
content: JSON.stringify(card)
|
|
2616
|
+
}
|
|
2617
|
+
})).data?.message_id;
|
|
2618
|
+
if (!messageId) throw new Error("failed to create card message: missing message_id");
|
|
2619
|
+
return messageId;
|
|
2620
|
+
}
|
|
2621
|
+
async patchCard(messageId, card) {
|
|
2622
|
+
await this.client.im.v1.message.patch({
|
|
2623
|
+
path: { message_id: messageId },
|
|
2624
|
+
data: { content: JSON.stringify(card) }
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
async addTypingReaction(messageId) {
|
|
2628
|
+
try {
|
|
2629
|
+
return (await this.client.im.v1.messageReaction.create({
|
|
2630
|
+
path: { message_id: messageId },
|
|
2631
|
+
data: { reaction_type: { emoji_type: "Typing" } }
|
|
2632
|
+
})).data?.reaction_id;
|
|
2633
|
+
} catch (error) {
|
|
2634
|
+
this.logger.warn("add typing reaction failed", {
|
|
2635
|
+
messageId,
|
|
2636
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2637
|
+
});
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
async removeTypingReaction(messageId, reactionId) {
|
|
2642
|
+
await this.client.im.v1.messageReaction.delete({ path: {
|
|
2643
|
+
message_id: messageId,
|
|
2644
|
+
reaction_id: reactionId
|
|
2645
|
+
} });
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
//#endregion
|
|
2649
|
+
//#region src/session/session-state-store.ts
|
|
2650
|
+
var FileSessionStateStore = class {
|
|
2651
|
+
constructor(filePath = resolve(homedir(), ".im-code-agent", "chat-state.json")) {
|
|
2652
|
+
this.filePath = filePath;
|
|
2653
|
+
}
|
|
2654
|
+
async loadState() {
|
|
2655
|
+
const raw = await readFile(this.filePath, "utf8").catch(() => void 0);
|
|
2656
|
+
if (!raw) return {
|
|
2657
|
+
chatCwds: /* @__PURE__ */ new Map(),
|
|
2658
|
+
chatBridgeSessionIds: /* @__PURE__ */ new Map(),
|
|
2659
|
+
chatSessionIds: /* @__PURE__ */ new Map()
|
|
2660
|
+
};
|
|
2661
|
+
const parsed = JSON.parse(raw);
|
|
2662
|
+
return {
|
|
2663
|
+
chatCwds: new Map(Object.entries(parsed.chatCwds ?? {})),
|
|
2664
|
+
chatBridgeSessionIds: new Map(Object.entries(parsed.chatBridgeSessionIds ?? {})),
|
|
2665
|
+
chatSessionIds: new Map(Object.entries(parsed.chatSessionIds ?? {}))
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
async saveState(state) {
|
|
2669
|
+
const payload = {
|
|
2670
|
+
chatCwds: Object.fromEntries(state.chatCwds.entries()),
|
|
2671
|
+
chatBridgeSessionIds: Object.fromEntries(state.chatBridgeSessionIds.entries()),
|
|
2672
|
+
chatSessionIds: Object.fromEntries(state.chatSessionIds.entries())
|
|
2673
|
+
};
|
|
2674
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2675
|
+
const tmpPath = `${this.filePath}.tmp`;
|
|
2676
|
+
await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
|
|
2677
|
+
await rename(tmpPath, this.filePath);
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
//#endregion
|
|
2681
|
+
//#region src/session/session-manager.ts
|
|
2682
|
+
var SessionManager = class {
|
|
2683
|
+
#tasks = /* @__PURE__ */ new Map();
|
|
2684
|
+
createTask(input, workspace) {
|
|
2685
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2686
|
+
const task = {
|
|
2687
|
+
id: crypto.randomUUID(),
|
|
2688
|
+
workspaceId: workspace.id,
|
|
2689
|
+
agent: input.agent,
|
|
2690
|
+
prompt: input.prompt,
|
|
2691
|
+
cwd: workspace.cwd,
|
|
2692
|
+
status: "pending",
|
|
2693
|
+
createdAt: now
|
|
2694
|
+
};
|
|
2695
|
+
this.#tasks.set(task.id, task);
|
|
2696
|
+
return task;
|
|
2697
|
+
}
|
|
2698
|
+
getTask(taskId) {
|
|
2699
|
+
return this.#tasks.get(taskId);
|
|
2700
|
+
}
|
|
2701
|
+
updateTaskStatus(taskId, status) {
|
|
2702
|
+
const task = this.#tasks.get(taskId);
|
|
2703
|
+
if (!task) return;
|
|
2704
|
+
const updatedTask = {
|
|
2705
|
+
...task,
|
|
2706
|
+
status,
|
|
2707
|
+
startedAt: status === "running" ? (/* @__PURE__ */ new Date()).toISOString() : task.startedAt,
|
|
2708
|
+
endedAt: status === "completed" || status === "failed" || status === "cancelled" ? (/* @__PURE__ */ new Date()).toISOString() : task.endedAt
|
|
2709
|
+
};
|
|
2710
|
+
this.#tasks.set(taskId, updatedTask);
|
|
2711
|
+
return updatedTask;
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
//#endregion
|
|
2715
|
+
//#region src/session/task-runner.ts
|
|
2716
|
+
function sameRuntimeArgs(left, right) {
|
|
2717
|
+
if (left.length !== right.length) return false;
|
|
2718
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
|
|
2719
|
+
return true;
|
|
2720
|
+
}
|
|
2721
|
+
var TaskRunner = class {
|
|
2722
|
+
#eventLog = /* @__PURE__ */ new Map();
|
|
2723
|
+
#eventListeners = /* @__PURE__ */ new Map();
|
|
2724
|
+
#conversations = /* @__PURE__ */ new Map();
|
|
2725
|
+
#runningConversations = /* @__PURE__ */ new Set();
|
|
2726
|
+
#taskRuntimeArgs = /* @__PURE__ */ new Map();
|
|
2727
|
+
constructor(sessionManager, agentProcess, logger) {
|
|
2728
|
+
this.sessionManager = sessionManager;
|
|
2729
|
+
this.agentProcess = agentProcess;
|
|
2730
|
+
this.logger = logger;
|
|
2731
|
+
}
|
|
2732
|
+
async startTask(input, workspace, options) {
|
|
2733
|
+
const task = this.sessionManager.createTask(input, workspace);
|
|
2734
|
+
this.#taskRuntimeArgs.set(task.id, options?.runtimeArgs ?? []);
|
|
2735
|
+
this.#eventLog.set(task.id, []);
|
|
2736
|
+
if (options?.onEvent) this.#eventListeners.set(task.id, options.onEvent);
|
|
2737
|
+
this.sessionManager.updateTaskStatus(task.id, "running");
|
|
2738
|
+
try {
|
|
2739
|
+
const { initialization, sessionId } = await this.agentProcess.start({
|
|
2740
|
+
taskId: task.id,
|
|
2741
|
+
agent: task.agent,
|
|
2742
|
+
cwd: task.cwd,
|
|
2743
|
+
runtimeArgs: options?.runtimeArgs
|
|
2744
|
+
});
|
|
2745
|
+
this.recordEvent(task.id, {
|
|
2746
|
+
type: "task.started",
|
|
2747
|
+
taskId: task.id,
|
|
2748
|
+
workspaceId: task.workspaceId,
|
|
2749
|
+
agent: task.agent,
|
|
2750
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2751
|
+
});
|
|
2752
|
+
const promptResult = await this.agentProcess.prompt(task.id, task.prompt);
|
|
2753
|
+
this.logger.info("task started", {
|
|
2754
|
+
taskId: task.id,
|
|
2755
|
+
workspaceId: task.workspaceId,
|
|
2756
|
+
agent: task.agent,
|
|
2757
|
+
protocolVersion: initialization.protocolVersion,
|
|
2758
|
+
sessionId,
|
|
2759
|
+
stopReason: promptResult.stopReason
|
|
2760
|
+
});
|
|
2761
|
+
this.recordEvent(task.id, {
|
|
2762
|
+
type: "task.completed",
|
|
2763
|
+
taskId: task.id,
|
|
2764
|
+
summary: promptResult.stopReason,
|
|
2765
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2766
|
+
});
|
|
2767
|
+
return {
|
|
2768
|
+
taskId: task.id,
|
|
2769
|
+
events: this.flushEvents(task.id)
|
|
2770
|
+
};
|
|
2771
|
+
} catch (error) {
|
|
2772
|
+
const taskError = error instanceof AgentProcessError ? error : new AgentProcessError("agent_session_start_failed", error instanceof Error ? error.message : String(error));
|
|
2773
|
+
this.sessionManager.updateTaskStatus(task.id, "failed");
|
|
2774
|
+
this.logger.error("task failed before prompt completed", {
|
|
2775
|
+
taskId: task.id,
|
|
2776
|
+
code: taskError.code,
|
|
2777
|
+
error: taskError.message
|
|
2778
|
+
});
|
|
2779
|
+
this.recordEvent(task.id, {
|
|
2780
|
+
type: "task.failed",
|
|
2781
|
+
taskId: task.id,
|
|
2782
|
+
code: taskError.code,
|
|
2783
|
+
error: taskError.message,
|
|
2784
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2785
|
+
});
|
|
2786
|
+
return {
|
|
2787
|
+
taskId: task.id,
|
|
2788
|
+
events: this.flushEvents(task.id)
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
async startConversationTask(conversationId, input, workspace, options) {
|
|
2793
|
+
const state = await this.ensureConversation(conversationId, workspace, input.agent, {
|
|
2794
|
+
resumeSessionId: options?.resumeSessionId,
|
|
2795
|
+
runtimeArgs: options?.runtimeArgs
|
|
2796
|
+
}, input.prompt);
|
|
2797
|
+
return this.runConversationPrompt(conversationId, state, input.prompt, options);
|
|
2798
|
+
}
|
|
2799
|
+
async ensureConversation(conversationId, workspace, agent, options, promptForTask) {
|
|
2800
|
+
let state = this.#conversations.get(conversationId);
|
|
2801
|
+
if (state && (state.workspaceId !== workspace.id || state.agent !== agent || state.cwd !== workspace.cwd || !sameRuntimeArgs(state.runtimeArgs, options?.runtimeArgs ?? []))) {
|
|
2802
|
+
await this.resetConversation(conversationId);
|
|
2803
|
+
state = void 0;
|
|
2804
|
+
}
|
|
2805
|
+
if (state) return state;
|
|
2806
|
+
const task = this.sessionManager.createTask({
|
|
2807
|
+
workspaceId: workspace.id,
|
|
2808
|
+
agent,
|
|
2809
|
+
prompt: promptForTask ?? ""
|
|
2810
|
+
}, workspace);
|
|
2811
|
+
const started = await this.agentProcess.start({
|
|
2812
|
+
taskId: task.id,
|
|
2813
|
+
agent,
|
|
2814
|
+
cwd: workspace.cwd,
|
|
2815
|
+
resumeSessionId: options?.resumeSessionId,
|
|
2816
|
+
runtimeArgs: options?.runtimeArgs
|
|
2817
|
+
});
|
|
2818
|
+
state = {
|
|
2819
|
+
taskId: task.id,
|
|
2820
|
+
workspaceId: workspace.id,
|
|
2821
|
+
agent,
|
|
2822
|
+
cwd: workspace.cwd,
|
|
2823
|
+
sessionId: started.sessionId,
|
|
2824
|
+
runtimeArgs: options?.runtimeArgs ?? [],
|
|
2825
|
+
models: started.models
|
|
2826
|
+
};
|
|
2827
|
+
this.#taskRuntimeArgs.set(task.id, state.runtimeArgs);
|
|
2828
|
+
this.#conversations.set(conversationId, state);
|
|
2829
|
+
this.logger.info("conversation created", {
|
|
2830
|
+
conversationId,
|
|
2831
|
+
taskId: state.taskId,
|
|
2832
|
+
workspaceId: state.workspaceId,
|
|
2833
|
+
agent: state.agent,
|
|
2834
|
+
sessionId: started.sessionId,
|
|
2835
|
+
resumed: Boolean(options?.resumeSessionId && started.sessionId === options.resumeSessionId)
|
|
2836
|
+
});
|
|
2837
|
+
return state;
|
|
2838
|
+
}
|
|
2839
|
+
async runConversationPrompt(conversationId, state, prompt, options) {
|
|
2840
|
+
this.#runningConversations.add(conversationId);
|
|
2841
|
+
const result = await this.runPrompt(state.taskId, state.workspaceId, state.agent, prompt, options).finally(() => {
|
|
2842
|
+
this.#runningConversations.delete(conversationId);
|
|
2843
|
+
});
|
|
2844
|
+
if (result.events.some((event) => event.type === "task.failed")) await this.resetConversation(conversationId);
|
|
2845
|
+
return {
|
|
2846
|
+
...result,
|
|
2847
|
+
sessionId: state.sessionId,
|
|
2848
|
+
models: state.models
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
async resetConversation(conversationId) {
|
|
2852
|
+
const state = this.#conversations.get(conversationId);
|
|
2853
|
+
if (!state) return false;
|
|
2854
|
+
this.#conversations.delete(conversationId);
|
|
2855
|
+
this.#taskRuntimeArgs.delete(state.taskId);
|
|
2856
|
+
this.sessionManager.updateTaskStatus(state.taskId, "cancelled");
|
|
2857
|
+
await this.agentProcess.stop(state.taskId);
|
|
2858
|
+
this.logger.info("conversation reset", {
|
|
2859
|
+
conversationId,
|
|
2860
|
+
taskId: state.taskId
|
|
2861
|
+
});
|
|
2862
|
+
return true;
|
|
2863
|
+
}
|
|
2864
|
+
isConversationRunning(conversationId) {
|
|
2865
|
+
return this.#runningConversations.has(conversationId);
|
|
2866
|
+
}
|
|
2867
|
+
hasConversation(conversationId) {
|
|
2868
|
+
return this.#conversations.has(conversationId);
|
|
2869
|
+
}
|
|
2870
|
+
async setConversationModel(conversationId, modelId) {
|
|
2871
|
+
const state = this.#conversations.get(conversationId);
|
|
2872
|
+
if (!state) return false;
|
|
2873
|
+
await this.agentProcess.setSessionModel(state.taskId, modelId);
|
|
2874
|
+
if (state.models) state.models = {
|
|
2875
|
+
...state.models,
|
|
2876
|
+
currentModelId: modelId
|
|
2877
|
+
};
|
|
2878
|
+
return true;
|
|
2879
|
+
}
|
|
2880
|
+
getConversationModels(conversationId) {
|
|
2881
|
+
return this.#conversations.get(conversationId)?.models;
|
|
2882
|
+
}
|
|
2883
|
+
getTask(taskId) {
|
|
2884
|
+
return this.sessionManager.getTask(taskId);
|
|
2885
|
+
}
|
|
2886
|
+
isTaskFullAccess(taskId) {
|
|
2887
|
+
const args = this.#taskRuntimeArgs.get(taskId) ?? [];
|
|
2888
|
+
return this.hasApprovalPolicyNever(args);
|
|
2889
|
+
}
|
|
2890
|
+
handleAgentEvent(event) {
|
|
2891
|
+
if (event.type === "agent.output") {
|
|
2892
|
+
const bridgeEvent = {
|
|
2893
|
+
type: "task.output",
|
|
2894
|
+
taskId: event.taskId,
|
|
2895
|
+
stream: "agent",
|
|
2896
|
+
chunk: event.text,
|
|
2897
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2898
|
+
};
|
|
2899
|
+
this.recordEvent(event.taskId, bridgeEvent);
|
|
2900
|
+
return bridgeEvent;
|
|
2901
|
+
}
|
|
2902
|
+
if (event.type === "agent.approval_requested") {
|
|
2903
|
+
const bridgeEvent = {
|
|
2904
|
+
type: "task.approval_requested",
|
|
2905
|
+
taskId: event.taskId,
|
|
2906
|
+
request: event.request,
|
|
2907
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2908
|
+
};
|
|
2909
|
+
this.recordEvent(event.taskId, bridgeEvent);
|
|
2910
|
+
return bridgeEvent;
|
|
2911
|
+
}
|
|
2912
|
+
if (event.type === "agent.approval_resolved") {
|
|
2913
|
+
const bridgeEvent = {
|
|
2914
|
+
type: "task.approval_resolved",
|
|
2915
|
+
taskId: event.taskId,
|
|
2916
|
+
decision: event.decision,
|
|
2917
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2918
|
+
};
|
|
2919
|
+
this.recordEvent(event.taskId, bridgeEvent);
|
|
2920
|
+
return bridgeEvent;
|
|
2921
|
+
}
|
|
2922
|
+
if (event.type === "agent.tool_update") {
|
|
2923
|
+
const bridgeEvent = {
|
|
2924
|
+
type: "task.tool_update",
|
|
2925
|
+
taskId: event.taskId,
|
|
2926
|
+
update: event.update,
|
|
2927
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2928
|
+
};
|
|
2929
|
+
this.recordEvent(event.taskId, bridgeEvent);
|
|
2930
|
+
return bridgeEvent;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
recordEvent(taskId, event) {
|
|
2934
|
+
const events = this.#eventLog.get(taskId);
|
|
2935
|
+
if (!events) this.#eventLog.set(taskId, [event]);
|
|
2936
|
+
else events.push(event);
|
|
2937
|
+
const listener = this.#eventListeners.get(taskId);
|
|
2938
|
+
if (listener) listener(event);
|
|
2939
|
+
}
|
|
2940
|
+
async runPrompt(taskId, workspaceId, agent, prompt, options) {
|
|
2941
|
+
this.#eventLog.set(taskId, []);
|
|
2942
|
+
if (options?.onEvent) this.#eventListeners.set(taskId, options.onEvent);
|
|
2943
|
+
this.sessionManager.updateTaskStatus(taskId, "running");
|
|
2944
|
+
this.recordEvent(taskId, {
|
|
2945
|
+
type: "task.started",
|
|
2946
|
+
taskId,
|
|
2947
|
+
workspaceId,
|
|
2948
|
+
agent,
|
|
2949
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2950
|
+
});
|
|
2951
|
+
try {
|
|
2952
|
+
const promptResult = await this.agentProcess.prompt(taskId, prompt);
|
|
2953
|
+
this.recordEvent(taskId, {
|
|
2954
|
+
type: "task.completed",
|
|
2955
|
+
taskId,
|
|
2956
|
+
summary: promptResult.stopReason,
|
|
2957
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2958
|
+
});
|
|
2959
|
+
} catch (error) {
|
|
2960
|
+
const taskError = error instanceof AgentProcessError ? error : new AgentProcessError("agent_session_start_failed", error instanceof Error ? error.message : String(error));
|
|
2961
|
+
this.sessionManager.updateTaskStatus(taskId, "failed");
|
|
2962
|
+
this.logger.error("task failed before prompt completed", {
|
|
2963
|
+
taskId,
|
|
2964
|
+
code: taskError.code,
|
|
2965
|
+
error: taskError.message
|
|
2966
|
+
});
|
|
2967
|
+
this.recordEvent(taskId, {
|
|
2968
|
+
type: "task.failed",
|
|
2969
|
+
taskId,
|
|
2970
|
+
code: taskError.code,
|
|
2971
|
+
error: taskError.message,
|
|
2972
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
return {
|
|
2976
|
+
taskId,
|
|
2977
|
+
events: this.flushEvents(taskId)
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
flushEvents(taskId) {
|
|
2981
|
+
const events = this.#eventLog.get(taskId) ?? [];
|
|
2982
|
+
this.#eventLog.delete(taskId);
|
|
2983
|
+
this.#eventListeners.delete(taskId);
|
|
2984
|
+
return events;
|
|
2985
|
+
}
|
|
2986
|
+
hasApprovalPolicyNever(args) {
|
|
2987
|
+
for (let index = 0; index < args.length - 1; index += 1) if (args[index] === "-c" && args[index + 1] === "approval_policy=\"never\"") return true;
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
//#endregion
|
|
2992
|
+
//#region src/feishu/session-controller.ts
|
|
2993
|
+
var FeishuSessionController = class {
|
|
2994
|
+
#chatCwds = /* @__PURE__ */ new Map();
|
|
2995
|
+
#chatBridgeSessionIds = /* @__PURE__ */ new Map();
|
|
2996
|
+
#chatSessionIds = /* @__PURE__ */ new Map();
|
|
2997
|
+
#chatAccessModes = /* @__PURE__ */ new Map();
|
|
2998
|
+
#chatCurrentModelIds = /* @__PURE__ */ new Map();
|
|
2999
|
+
#chatAvailableModels = /* @__PURE__ */ new Map();
|
|
3000
|
+
#defaultAccessMode;
|
|
3001
|
+
constructor(workspaces, defaultAccessMode, stateStore = new FileSessionStateStore()) {
|
|
3002
|
+
this.workspaces = workspaces;
|
|
3003
|
+
this.stateStore = stateStore;
|
|
3004
|
+
this.#defaultAccessMode = defaultAccessMode;
|
|
3005
|
+
}
|
|
3006
|
+
async restore() {
|
|
3007
|
+
const persisted = await this.stateStore.loadState();
|
|
3008
|
+
let sanitized = false;
|
|
3009
|
+
const fallbackCwd = this.workspaces[0]?.cwd;
|
|
3010
|
+
for (const [chatId, cwd] of persisted.chatCwds.entries()) if (await this.isDirectory(cwd)) this.#chatCwds.set(chatId, cwd);
|
|
3011
|
+
else if (fallbackCwd) {
|
|
3012
|
+
this.#chatCwds.set(chatId, fallbackCwd);
|
|
3013
|
+
sanitized = true;
|
|
3014
|
+
}
|
|
3015
|
+
for (const [chatId, bridgeSessionId] of persisted.chatBridgeSessionIds.entries()) this.#chatBridgeSessionIds.set(chatId, bridgeSessionId);
|
|
3016
|
+
for (const [chatId, sessionId] of persisted.chatSessionIds.entries()) this.#chatSessionIds.set(chatId, sessionId);
|
|
3017
|
+
if (sanitized) await this.persist();
|
|
3018
|
+
return {
|
|
3019
|
+
persistedChats: this.#chatCwds.size,
|
|
3020
|
+
persistedSessions: this.#chatSessionIds.size
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
async resolveValidatedWorkspace(chatId) {
|
|
3024
|
+
const workspace = this.resolveWorkspace(chatId);
|
|
3025
|
+
if (!workspace) return;
|
|
3026
|
+
if (await this.isDirectory(workspace.cwd)) return workspace;
|
|
3027
|
+
const fallback = this.workspaces[0];
|
|
3028
|
+
if (!fallback) return workspace;
|
|
3029
|
+
await this.setChatCwd(chatId, fallback.cwd);
|
|
3030
|
+
await this.clearSession(chatId);
|
|
3031
|
+
return {
|
|
3032
|
+
...fallback,
|
|
3033
|
+
cwd: fallback.cwd
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
getResumeSessionId(chatId) {
|
|
3037
|
+
return this.#chatSessionIds.get(chatId);
|
|
3038
|
+
}
|
|
3039
|
+
getBridgeSessionId(chatId) {
|
|
3040
|
+
return this.#chatBridgeSessionIds.get(chatId);
|
|
3041
|
+
}
|
|
3042
|
+
async setSessionId(chatId, sessionId) {
|
|
3043
|
+
if (!sessionId) {
|
|
3044
|
+
this.#chatBridgeSessionIds.delete(chatId);
|
|
3045
|
+
this.#chatSessionIds.delete(chatId);
|
|
3046
|
+
} else {
|
|
3047
|
+
this.#chatBridgeSessionIds.set(chatId, randomUUID());
|
|
3048
|
+
this.#chatSessionIds.set(chatId, sessionId);
|
|
3049
|
+
}
|
|
3050
|
+
await this.persist();
|
|
3051
|
+
}
|
|
3052
|
+
async clearSession(chatId) {
|
|
3053
|
+
this.#chatBridgeSessionIds.delete(chatId);
|
|
3054
|
+
this.#chatSessionIds.delete(chatId);
|
|
3055
|
+
this.#chatCurrentModelIds.delete(chatId);
|
|
3056
|
+
this.#chatAvailableModels.delete(chatId);
|
|
3057
|
+
await this.persist();
|
|
3058
|
+
}
|
|
3059
|
+
getAccessMode(chatId) {
|
|
3060
|
+
return this.#chatAccessModes.get(chatId) ?? this.#defaultAccessMode;
|
|
3061
|
+
}
|
|
3062
|
+
getDefaultAccessMode() {
|
|
3063
|
+
return this.#defaultAccessMode;
|
|
3064
|
+
}
|
|
3065
|
+
getAccessOverride(chatId) {
|
|
3066
|
+
return this.#chatAccessModes.get(chatId);
|
|
3067
|
+
}
|
|
3068
|
+
getModel(chatId) {
|
|
3069
|
+
return this.#chatCurrentModelIds.get(chatId);
|
|
3070
|
+
}
|
|
3071
|
+
async setModel(chatId, model, taskRunner) {
|
|
3072
|
+
if (!taskRunner.hasConversation(chatId)) return false;
|
|
3073
|
+
if (this.getModel(chatId) === model) return false;
|
|
3074
|
+
if (!await taskRunner.setConversationModel(chatId, model)) return false;
|
|
3075
|
+
const models = taskRunner.getConversationModels(chatId);
|
|
3076
|
+
this.updateModelState(chatId, models);
|
|
3077
|
+
return true;
|
|
3078
|
+
}
|
|
3079
|
+
getAvailableModels(chatId) {
|
|
3080
|
+
return this.#chatAvailableModels.get(chatId) ?? [];
|
|
3081
|
+
}
|
|
3082
|
+
updateModelState(chatId, state) {
|
|
3083
|
+
if (!state) return;
|
|
3084
|
+
this.#chatCurrentModelIds.set(chatId, state.currentModelId);
|
|
3085
|
+
this.#chatAvailableModels.set(chatId, state.availableModels);
|
|
3086
|
+
}
|
|
3087
|
+
async setAccessMode(chatId, mode, taskRunner) {
|
|
3088
|
+
if (this.getAccessMode(chatId) === mode) return false;
|
|
3089
|
+
if (mode === this.#defaultAccessMode) this.#chatAccessModes.delete(chatId);
|
|
3090
|
+
else this.#chatAccessModes.set(chatId, mode);
|
|
3091
|
+
await this.resetConversation(chatId, taskRunner, { keepAccessOverride: true });
|
|
3092
|
+
return true;
|
|
3093
|
+
}
|
|
3094
|
+
async clearAccessOverride(chatId, taskRunner) {
|
|
3095
|
+
if (!this.#chatAccessModes.has(chatId)) return false;
|
|
3096
|
+
this.#chatAccessModes.delete(chatId);
|
|
3097
|
+
await this.resetConversation(chatId, taskRunner);
|
|
3098
|
+
return true;
|
|
3099
|
+
}
|
|
3100
|
+
async interrupt(chatId, taskRunner) {
|
|
3101
|
+
if (!taskRunner.isConversationRunning(chatId)) return false;
|
|
3102
|
+
await this.resetConversation(chatId, taskRunner, { keepAccessOverride: true });
|
|
3103
|
+
return true;
|
|
3104
|
+
}
|
|
3105
|
+
async resetConversation(chatId, taskRunner, options) {
|
|
3106
|
+
await taskRunner.resetConversation(chatId);
|
|
3107
|
+
this.#chatBridgeSessionIds.delete(chatId);
|
|
3108
|
+
this.#chatSessionIds.delete(chatId);
|
|
3109
|
+
this.#chatCurrentModelIds.delete(chatId);
|
|
3110
|
+
this.#chatAvailableModels.delete(chatId);
|
|
3111
|
+
if (!options?.keepAccessOverride) this.#chatAccessModes.delete(chatId);
|
|
3112
|
+
await this.persist();
|
|
3113
|
+
}
|
|
3114
|
+
async startNewConversation(options, taskRunner) {
|
|
3115
|
+
await this.resetConversation(options.chatId, taskRunner);
|
|
3116
|
+
if (options.cwd) await this.setChatCwd(options.chatId, options.cwd);
|
|
3117
|
+
else await this.setChatCwd(options.chatId, options.workspace.cwd);
|
|
3118
|
+
const nextWorkspace = {
|
|
3119
|
+
...options.workspace,
|
|
3120
|
+
cwd: options.cwd ?? options.workspace.cwd
|
|
3121
|
+
};
|
|
3122
|
+
const conversation = await taskRunner.ensureConversation(options.chatId, nextWorkspace, options.agent);
|
|
3123
|
+
this.updateModelState(options.chatId, conversation.models);
|
|
3124
|
+
this.#chatBridgeSessionIds.set(options.chatId, randomUUID());
|
|
3125
|
+
this.#chatSessionIds.set(options.chatId, conversation.sessionId);
|
|
3126
|
+
await this.persist();
|
|
3127
|
+
return {
|
|
3128
|
+
sessionId: conversation.sessionId,
|
|
3129
|
+
workspace: nextWorkspace
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
resolveWorkspace(chatId) {
|
|
3133
|
+
const baseWorkspace = this.workspaces[0];
|
|
3134
|
+
if (!baseWorkspace) return;
|
|
3135
|
+
const cwd = this.#chatCwds.get(chatId) ?? baseWorkspace.cwd;
|
|
3136
|
+
return {
|
|
3137
|
+
...baseWorkspace,
|
|
3138
|
+
cwd
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
async setChatCwd(chatId, cwd) {
|
|
3142
|
+
this.#chatCwds.set(chatId, cwd);
|
|
3143
|
+
await this.persist();
|
|
3144
|
+
}
|
|
3145
|
+
async isDirectory(path) {
|
|
3146
|
+
const dirStat = await stat(path).catch(() => void 0);
|
|
3147
|
+
return Boolean(dirStat?.isDirectory());
|
|
3148
|
+
}
|
|
3149
|
+
async persist() {
|
|
3150
|
+
await this.stateStore.saveState({
|
|
3151
|
+
chatCwds: this.#chatCwds,
|
|
3152
|
+
chatBridgeSessionIds: this.#chatBridgeSessionIds,
|
|
3153
|
+
chatSessionIds: this.#chatSessionIds
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
//#endregion
|
|
3158
|
+
//#region src/feishu/feishu-gateway.ts
|
|
3159
|
+
function buildCodexRuntimeArgs(mode) {
|
|
3160
|
+
const args = [];
|
|
3161
|
+
if (mode === "full-access") args.push("-c", "approval_policy=\"never\"", "-c", "sandbox_mode=\"danger-full-access\"");
|
|
3162
|
+
return args;
|
|
3163
|
+
}
|
|
3164
|
+
var FeishuGateway = class {
|
|
3165
|
+
#wsClient;
|
|
3166
|
+
#eventDispatcher;
|
|
3167
|
+
#messageQueue = new MessageEntryQueue();
|
|
3168
|
+
#sessionController;
|
|
3169
|
+
#cardRenderer = new FeishuCardRenderer();
|
|
3170
|
+
#messageClient;
|
|
3171
|
+
#cardActionHandler;
|
|
3172
|
+
#commandHandler;
|
|
3173
|
+
#approvalCards = /* @__PURE__ */ new Map();
|
|
3174
|
+
#patchMinIntervalMs = 280;
|
|
3175
|
+
constructor(config, workspaces, taskRunner, approvalGateway, logger, yoloMode = false) {
|
|
3176
|
+
this.taskRunner = taskRunner;
|
|
3177
|
+
this.approvalGateway = approvalGateway;
|
|
3178
|
+
this.logger = logger;
|
|
3179
|
+
this.#sessionController = new FeishuSessionController(workspaces, yoloMode ? "full-access" : "standard");
|
|
3180
|
+
this.#messageClient = new FeishuMessageClient(new lark.Client({
|
|
3181
|
+
appId: config.appId,
|
|
3182
|
+
appSecret: config.appSecret
|
|
3183
|
+
}), this.logger);
|
|
3184
|
+
this.#cardActionHandler = new FeishuCardActionHandler({
|
|
3185
|
+
sessionController: this.#sessionController,
|
|
3186
|
+
taskRunner: this.taskRunner,
|
|
3187
|
+
approvalGateway: this.approvalGateway,
|
|
3188
|
+
messageClient: this.#messageClient,
|
|
3189
|
+
cardRenderer: this.#cardRenderer,
|
|
3190
|
+
logger: this.logger,
|
|
3191
|
+
onApprovalResolved: (decision) => {
|
|
3192
|
+
this.patchApprovalCardByDecision(decision);
|
|
3193
|
+
}
|
|
3194
|
+
});
|
|
3195
|
+
this.#commandHandler = new FeishuCommandHandler({
|
|
3196
|
+
sessionController: this.#sessionController,
|
|
3197
|
+
cardActionHandler: this.#cardActionHandler,
|
|
3198
|
+
messageClient: this.#messageClient,
|
|
3199
|
+
taskRunner: this.taskRunner
|
|
3200
|
+
});
|
|
3201
|
+
this.#wsClient = new lark.WSClient({
|
|
3202
|
+
appId: config.appId,
|
|
3203
|
+
appSecret: config.appSecret,
|
|
3204
|
+
loggerLevel: lark.LoggerLevel.info
|
|
3205
|
+
});
|
|
3206
|
+
this.#eventDispatcher = buildFeishuEventDispatcher({
|
|
3207
|
+
onMessageReceived: async (data) => {
|
|
3208
|
+
await this.handleIncomingMessage(data);
|
|
3209
|
+
},
|
|
3210
|
+
onCardAction: async (data) => {
|
|
3211
|
+
return this.#cardActionHandler.handleCardAction(data ?? {});
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
this.approvalGateway.onResolved((snapshot) => {
|
|
3215
|
+
this.patchApprovalCard(snapshot);
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
async start() {
|
|
3219
|
+
const restored = await this.#sessionController.restore();
|
|
3220
|
+
await this.#wsClient.start({ eventDispatcher: this.#eventDispatcher });
|
|
3221
|
+
this.logger.info("feishu gateway started", restored);
|
|
3222
|
+
}
|
|
3223
|
+
async handleIncomingMessage(data) {
|
|
3224
|
+
this.#messageQueue.gcHandledMessageIds();
|
|
3225
|
+
const messageId = data.message.message_id;
|
|
3226
|
+
const queue = this.#messageQueue.runInChatQueue(data.message.chat_id, messageId, async () => {
|
|
3227
|
+
await this.processMessage(data);
|
|
3228
|
+
});
|
|
3229
|
+
if (!queue) {
|
|
3230
|
+
this.logger.info("feishu duplicate message ignored", { messageId });
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
queue.catch((error) => {
|
|
3234
|
+
this.logger.error("feishu message process failed", {
|
|
3235
|
+
messageId,
|
|
3236
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3237
|
+
});
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
async processMessage(data) {
|
|
3241
|
+
if (data.sender.sender_type !== "user") return;
|
|
3242
|
+
const chatId = data.message.chat_id;
|
|
3243
|
+
if (data.message.message_type !== "text") {
|
|
3244
|
+
await this.#messageClient.sendText(chatId, "只支持文本消息。");
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
const workspace = await this.#sessionController.resolveValidatedWorkspace(chatId);
|
|
3248
|
+
if (!workspace) {
|
|
3249
|
+
await this.#messageClient.sendText(chatId, "未配置可用工作区。");
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
const rawPrompt = (parseContent(data.message.content).text ?? "").trim();
|
|
3253
|
+
if (!rawPrompt) {
|
|
3254
|
+
await this.#messageClient.sendText(chatId, "消息内容为空。");
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
let command;
|
|
3258
|
+
try {
|
|
3259
|
+
command = await parseUserCommand(rawPrompt, workspace.cwd);
|
|
3260
|
+
} catch (error) {
|
|
3261
|
+
await this.#messageClient.sendText(chatId, `命令解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
const result = await this.#commandHandler.handle({
|
|
3265
|
+
chatId,
|
|
3266
|
+
workspace,
|
|
3267
|
+
command
|
|
3268
|
+
});
|
|
3269
|
+
if (result.type === "handled") return;
|
|
3270
|
+
await this.runConversationTask({
|
|
3271
|
+
chatId,
|
|
3272
|
+
messageId: data.message.message_id,
|
|
3273
|
+
workspace,
|
|
3274
|
+
prompt: result.prompt
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
async runConversationTask(params) {
|
|
3278
|
+
const streamer = new TaskCardStreamer({
|
|
3279
|
+
chatId: params.chatId,
|
|
3280
|
+
logger: this.logger,
|
|
3281
|
+
renderer: this.#cardRenderer,
|
|
3282
|
+
sendCard: async (chatId, card) => this.#messageClient.sendCard(chatId, card),
|
|
3283
|
+
patchCard: async (messageId, card) => this.#messageClient.patchCard(messageId, card),
|
|
3284
|
+
patchMinIntervalMs: this.#patchMinIntervalMs
|
|
3285
|
+
});
|
|
3286
|
+
let typingReactionId;
|
|
3287
|
+
try {
|
|
3288
|
+
typingReactionId = await this.#messageClient.addTypingReaction(params.messageId);
|
|
3289
|
+
const result = await this.taskRunner.startConversationTask(params.chatId, {
|
|
3290
|
+
workspaceId: params.workspace.id,
|
|
3291
|
+
agent: "codex",
|
|
3292
|
+
prompt: params.prompt
|
|
3293
|
+
}, params.workspace, {
|
|
3294
|
+
runtimeArgs: buildCodexRuntimeArgs(this.#sessionController.getAccessMode(params.chatId)),
|
|
3295
|
+
resumeSessionId: this.#sessionController.getResumeSessionId(params.chatId),
|
|
3296
|
+
onEvent: (event) => {
|
|
3297
|
+
this.onTaskEvent(params.chatId, event, streamer);
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
3300
|
+
if (result.sessionId && this.#sessionController.getResumeSessionId(params.chatId) !== result.sessionId) await this.#sessionController.setSessionId(params.chatId, result.sessionId);
|
|
3301
|
+
this.#sessionController.updateModelState(params.chatId, result.models);
|
|
3302
|
+
const failed = result.events.find((event) => event.type === "task.failed");
|
|
3303
|
+
const completed = result.events.find((event) => event.type === "task.completed");
|
|
3304
|
+
if (failed) streamer.markFailed(failed.error);
|
|
3305
|
+
else if (completed) streamer.markCompleted(completed.summary);
|
|
3306
|
+
await streamer.finalize();
|
|
3307
|
+
} catch (error) {
|
|
3308
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3309
|
+
this.logger.error("run conversation task failed", {
|
|
3310
|
+
chatId: params.chatId,
|
|
3311
|
+
messageId: params.messageId,
|
|
3312
|
+
error: errorMessage
|
|
3313
|
+
});
|
|
3314
|
+
streamer.markFailed(errorMessage);
|
|
3315
|
+
await streamer.finalize().catch((finalizeError) => {
|
|
3316
|
+
this.logger.error("finalize failed after task error", {
|
|
3317
|
+
chatId: params.chatId,
|
|
3318
|
+
messageId: params.messageId,
|
|
3319
|
+
error: finalizeError instanceof Error ? finalizeError.message : String(finalizeError)
|
|
3320
|
+
});
|
|
3321
|
+
});
|
|
3322
|
+
} finally {
|
|
3323
|
+
if (typingReactionId) await this.#messageClient.removeTypingReaction(params.messageId, typingReactionId).catch(() => {});
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
onTaskEvent(chatId, event, streamer) {
|
|
3327
|
+
if (event.type === "task.output") {
|
|
3328
|
+
streamer.handleOutputChunk(event.chunk);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
if (event.type === "task.tool_update") {
|
|
3332
|
+
streamer.handleToolUpdate(event.update);
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
if (event.type === "task.approval_requested") {
|
|
3336
|
+
this.sendApprovalCard(chatId, event.request).catch((error) => {
|
|
3337
|
+
this.logger.error("send approval card failed", {
|
|
3338
|
+
taskId: event.taskId,
|
|
3339
|
+
requestId: event.request.id,
|
|
3340
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3341
|
+
});
|
|
3342
|
+
});
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
if (event.type === "task.approval_resolved") {
|
|
3346
|
+
if (shouldPatchApprovalSummary(event.decision)) this.patchApprovalCardByDecision(event.decision);
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
if (event.type === "task.failed") {
|
|
3350
|
+
streamer.markFailed(event.error);
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
if (event.type === "task.completed") streamer.markCompleted(event.summary);
|
|
3354
|
+
}
|
|
3355
|
+
async sendApprovalCard(chatId, request) {
|
|
3356
|
+
this.gcApprovalCards();
|
|
3357
|
+
const messageId = await this.#messageClient.sendCard(chatId, this.#cardRenderer.buildApprovalCard(request, "pending"));
|
|
3358
|
+
this.#approvalCards.set(request.id, {
|
|
3359
|
+
chatId,
|
|
3360
|
+
messageId,
|
|
3361
|
+
updatedAtMs: Date.now()
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
3364
|
+
async patchApprovalCard(snapshot) {
|
|
3365
|
+
this.gcApprovalCards();
|
|
3366
|
+
const binding = this.#approvalCards.get(snapshot.request.id);
|
|
3367
|
+
if (!binding) return;
|
|
3368
|
+
binding.updatedAtMs = Date.now();
|
|
3369
|
+
await this.#messageClient.patchCard(binding.messageId, this.#cardRenderer.buildApprovalCard(snapshot.request, snapshot.status, snapshot.decision)).catch((error) => {
|
|
3370
|
+
this.logger.warn("patch approval card failed", {
|
|
3371
|
+
requestId: snapshot.request.id,
|
|
3372
|
+
messageId: binding.messageId,
|
|
3373
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3374
|
+
});
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
async patchApprovalCardByDecision(decision) {
|
|
3378
|
+
this.gcApprovalCards();
|
|
3379
|
+
const binding = this.#approvalCards.get(decision.requestId);
|
|
3380
|
+
if (!binding) return;
|
|
3381
|
+
binding.updatedAtMs = Date.now();
|
|
3382
|
+
const title = decision.decision === "approved" ? "已批准" : "已拒绝";
|
|
3383
|
+
await this.#messageClient.patchCard(binding.messageId, this.#cardRenderer.buildApprovalSummaryCard(title, decision)).catch((error) => {
|
|
3384
|
+
this.logger.warn("patch approval card failed", {
|
|
3385
|
+
requestId: decision.requestId,
|
|
3386
|
+
messageId: binding.messageId,
|
|
3387
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3388
|
+
});
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
gcApprovalCards() {
|
|
3392
|
+
const ttlMs = 1440 * 60 * 1e3;
|
|
3393
|
+
const now = Date.now();
|
|
3394
|
+
for (const [requestId, binding] of this.#approvalCards.entries()) if (now - binding.updatedAtMs > ttlMs) this.#approvalCards.delete(requestId);
|
|
3395
|
+
}
|
|
3396
|
+
};
|
|
3397
|
+
//#endregion
|
|
3398
|
+
//#region src/utils/logger.ts
|
|
3399
|
+
function log(level, message, context) {
|
|
3400
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3401
|
+
const levelLabel = level.toUpperCase();
|
|
3402
|
+
const contextText = context ? ` ${formatContext(context)}` : "";
|
|
3403
|
+
console.log(`[${timestamp}] ${levelLabel} ${message}${contextText}`);
|
|
3404
|
+
}
|
|
3405
|
+
function formatContext(context) {
|
|
3406
|
+
return Object.entries(context).map(([key, value]) => `${key}=${stringifyValue(value)}`).join(" ");
|
|
3407
|
+
}
|
|
3408
|
+
function stringifyValue(value) {
|
|
3409
|
+
if (typeof value === "string") return value;
|
|
3410
|
+
if (value === void 0) return "undefined";
|
|
3411
|
+
if (value === null) return "null";
|
|
3412
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
3413
|
+
try {
|
|
3414
|
+
return JSON.stringify(value);
|
|
3415
|
+
} catch {
|
|
3416
|
+
return "[unserializable]";
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
function createLogger() {
|
|
3420
|
+
return {
|
|
3421
|
+
info: (message, context) => log("info", message, context),
|
|
3422
|
+
warn: (message, context) => log("warn", message, context),
|
|
3423
|
+
error: (message, context) => log("error", message, context)
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
//#endregion
|
|
3427
|
+
//#region src/index.ts
|
|
3428
|
+
async function startBridge() {
|
|
3429
|
+
const config = await loadConfig();
|
|
3430
|
+
const logger = createLogger();
|
|
3431
|
+
const approvalGateway = new ApprovalGateway(new ApprovalStore(), logger);
|
|
3432
|
+
const sessionManager = new SessionManager();
|
|
3433
|
+
let taskRunner;
|
|
3434
|
+
taskRunner = new TaskRunner(sessionManager, new AgentProcess(config, logger, (event) => {
|
|
3435
|
+
const bridgeEvent = taskRunner.handleAgentEvent(event);
|
|
3436
|
+
if (!bridgeEvent) return;
|
|
3437
|
+
logger.info("bridge event emitted", bridgeEvent);
|
|
3438
|
+
}, createPermissionRequestHandler({
|
|
3439
|
+
workspaces: config.workspaces,
|
|
3440
|
+
approvalGateway,
|
|
3441
|
+
getTaskRunner: () => taskRunner
|
|
3442
|
+
})), logger);
|
|
3443
|
+
const feishuGateway = config.feishu ? new FeishuGateway(config.feishu, config.workspaces, taskRunner, approvalGateway, logger, config.yoloMode) : void 0;
|
|
3444
|
+
if (feishuGateway) await feishuGateway.start();
|
|
3445
|
+
logger.info("bridge started", {
|
|
3446
|
+
workspaceCount: config.workspaces.length,
|
|
3447
|
+
feishuEnabled: Boolean(feishuGateway),
|
|
3448
|
+
yoloMode: config.yoloMode
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
//#endregion
|
|
3452
|
+
export { startBridge as t };
|