@modelzen/feishu-codex-bridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/bin/feishu-codex-bridge.mjs +2 -0
- package/dist/cli.js +4621 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +224 -0
- package/package.json +66 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4621 @@
|
|
|
1
|
+
// src/cli/index.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/cli/commands/doctor.ts
|
|
5
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
6
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7
|
+
import { homedir as homedir2 } from "os";
|
|
8
|
+
import { join as join4 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/config/paths.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var appDir = join(homedir(), ".feishu-codex-bridge");
|
|
14
|
+
var larkCliDir = join(appDir, "lark-cli");
|
|
15
|
+
var codexCliDir = join(appDir, "codex-cli");
|
|
16
|
+
var currentBotDir = appDir;
|
|
17
|
+
function botDir(appId) {
|
|
18
|
+
return join(appDir, "bots", appId);
|
|
19
|
+
}
|
|
20
|
+
function useBotDir(appId) {
|
|
21
|
+
currentBotDir = botDir(appId);
|
|
22
|
+
}
|
|
23
|
+
var paths = {
|
|
24
|
+
appDir,
|
|
25
|
+
cacheDir: appDir,
|
|
26
|
+
/** bot 注册表:保存的全部 bot + 当前选中的 appId */
|
|
27
|
+
botsFile: join(appDir, "bots.json"),
|
|
28
|
+
/** app id / 租户 / 偏好(当前 bot;不含明文密钥) */
|
|
29
|
+
get configFile() {
|
|
30
|
+
return join(currentBotDir, "config.json");
|
|
31
|
+
},
|
|
32
|
+
/** thread(话题) → codex thread_id + cwd + 会话级配置(当前 bot) */
|
|
33
|
+
get sessionsFile() {
|
|
34
|
+
return join(currentBotDir, "sessions.json");
|
|
35
|
+
},
|
|
36
|
+
/** project(群) → cwd + 默认参数 注册表(当前 bot) */
|
|
37
|
+
get projectsFile() {
|
|
38
|
+
return join(currentBotDir, "projects.json");
|
|
39
|
+
},
|
|
40
|
+
/** 在跑的 start 进程注册中心(同 App 冲突检测;当前 bot) */
|
|
41
|
+
get processesFile() {
|
|
42
|
+
return join(currentBotDir, "processes.json");
|
|
43
|
+
},
|
|
44
|
+
secretsFile: join(appDir, "secrets.enc"),
|
|
45
|
+
keystoreSaltFile: join(appDir, ".keystore.salt"),
|
|
46
|
+
npmCacheDir: join(appDir, "npm-cache"),
|
|
47
|
+
/** 空白项目默认落地目录 */
|
|
48
|
+
projectsRootDir: join(appDir, "projects"),
|
|
49
|
+
larkCliDir,
|
|
50
|
+
larkCliBinDir: join(larkCliDir, "node_modules", ".bin"),
|
|
51
|
+
codexCliDir,
|
|
52
|
+
codexCliBinDir: join(codexCliDir, "node_modules", ".bin"),
|
|
53
|
+
/**
|
|
54
|
+
* Thin shell wrapper that lark-cli invokes to resolve secrets from the
|
|
55
|
+
* bridge's encrypted store. Written user-owned and non-symlinked so it
|
|
56
|
+
* passes lark-cli's AssertSecurePath audit.
|
|
57
|
+
*/
|
|
58
|
+
secretsGetterScript: join(appDir, "secrets-getter"),
|
|
59
|
+
mediaDir: join(appDir, "media")
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/config/bots.ts
|
|
63
|
+
import { existsSync } from "fs";
|
|
64
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
65
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
66
|
+
|
|
67
|
+
// src/config/store.ts
|
|
68
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
69
|
+
import { dirname } from "path";
|
|
70
|
+
|
|
71
|
+
// src/config/schema.ts
|
|
72
|
+
function isComplete(cfg) {
|
|
73
|
+
const app = cfg.accounts?.app;
|
|
74
|
+
return Boolean(app?.id && hasSecret(app?.secret) && app?.tenant);
|
|
75
|
+
}
|
|
76
|
+
function hasSecret(s) {
|
|
77
|
+
if (!s) return false;
|
|
78
|
+
if (typeof s === "string") return s.length > 0;
|
|
79
|
+
return Boolean(s.source && s.id);
|
|
80
|
+
}
|
|
81
|
+
function isSecretRef(s) {
|
|
82
|
+
return typeof s === "object" && s !== null;
|
|
83
|
+
}
|
|
84
|
+
function secretKeyForApp(appId) {
|
|
85
|
+
return `app-${appId}`;
|
|
86
|
+
}
|
|
87
|
+
function getShowToolCalls(cfg) {
|
|
88
|
+
return cfg.preferences?.showToolCalls !== false;
|
|
89
|
+
}
|
|
90
|
+
function getMaxConcurrentRuns(cfg) {
|
|
91
|
+
const raw = cfg.preferences?.maxConcurrentRuns;
|
|
92
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) return 10;
|
|
93
|
+
return Math.min(Math.floor(raw), 50);
|
|
94
|
+
}
|
|
95
|
+
function getPendingPolicy(cfg) {
|
|
96
|
+
return cfg.preferences?.pendingPolicy === "queue" ? "queue" : "steer";
|
|
97
|
+
}
|
|
98
|
+
function getRunIdleTimeoutMs(cfg) {
|
|
99
|
+
const raw = cfg.preferences?.runIdleTimeoutSeconds;
|
|
100
|
+
if (raw === 0) return void 0;
|
|
101
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return 12e4;
|
|
102
|
+
const clamped = Math.min(Math.max(Math.floor(raw), 10), 1800);
|
|
103
|
+
return clamped * 1e3;
|
|
104
|
+
}
|
|
105
|
+
function isUserAllowed(cfg, senderId) {
|
|
106
|
+
const list = cfg.preferences?.access?.allowedUsers;
|
|
107
|
+
if (!list || list.length === 0) return true;
|
|
108
|
+
return list.includes(senderId);
|
|
109
|
+
}
|
|
110
|
+
function isChatAllowed(cfg, chatId) {
|
|
111
|
+
const list = cfg.preferences?.access?.allowedChats;
|
|
112
|
+
if (!list || list.length === 0) return true;
|
|
113
|
+
return list.includes(chatId);
|
|
114
|
+
}
|
|
115
|
+
function isAdmin(cfg, senderId) {
|
|
116
|
+
const list = cfg.preferences?.access?.admins;
|
|
117
|
+
if (!list || list.length === 0) return true;
|
|
118
|
+
return list.includes(senderId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/config/store.ts
|
|
122
|
+
async function loadConfig(path = paths.configFile) {
|
|
123
|
+
try {
|
|
124
|
+
const text = await readFile(path, "utf8");
|
|
125
|
+
return JSON.parse(text);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err.code === "ENOENT") return {};
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function buildEncryptedAccountConfig(appId, tenant, preferences) {
|
|
132
|
+
const wrapperPath = await ensureSecretsGetterWrapper();
|
|
133
|
+
return {
|
|
134
|
+
accounts: {
|
|
135
|
+
app: {
|
|
136
|
+
id: appId,
|
|
137
|
+
secret: { source: "exec", provider: "bridge", id: secretKeyForApp(appId) },
|
|
138
|
+
tenant
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
secrets: {
|
|
142
|
+
providers: {
|
|
143
|
+
bridge: { source: "exec", command: wrapperPath, args: [] }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
...preferences ? { preferences } : {}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function ensureSecretsGetterWrapper() {
|
|
150
|
+
const wrapperPath = paths.secretsGetterScript;
|
|
151
|
+
const node = process.execPath;
|
|
152
|
+
const bridgeEntry = process.argv[1] ?? "";
|
|
153
|
+
const sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
154
|
+
const content = `#!/bin/sh
|
|
155
|
+
# Auto-generated by feishu-codex-bridge. Do not edit.
|
|
156
|
+
exec ${sq(node)} ${sq(bridgeEntry)} secrets get "$@"
|
|
157
|
+
`;
|
|
158
|
+
await mkdir(dirname(wrapperPath), { recursive: true });
|
|
159
|
+
const tmp = `${wrapperPath}.tmp-${process.pid}`;
|
|
160
|
+
await writeFile(tmp, content, "utf8");
|
|
161
|
+
await chmod(tmp, 448);
|
|
162
|
+
await rename(tmp, wrapperPath);
|
|
163
|
+
return wrapperPath;
|
|
164
|
+
}
|
|
165
|
+
async function saveConfig(cfg, path = paths.configFile) {
|
|
166
|
+
await mkdir(dirname(path), { recursive: true });
|
|
167
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
168
|
+
await writeFile(tmp, `${JSON.stringify(cfg, null, 2)}
|
|
169
|
+
`, "utf8");
|
|
170
|
+
await chmod(tmp, 384);
|
|
171
|
+
await rename(tmp, path);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/config/bots.ts
|
|
175
|
+
var EMPTY = { version: 1, bots: [] };
|
|
176
|
+
async function loadBots() {
|
|
177
|
+
try {
|
|
178
|
+
const text = await readFile2(paths.botsFile, "utf8");
|
|
179
|
+
const reg = JSON.parse(text);
|
|
180
|
+
return { version: 1, current: reg.current, bots: Array.isArray(reg.bots) ? reg.bots : [] };
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err.code === "ENOENT") return { ...EMPTY };
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function saveBots(reg) {
|
|
187
|
+
await mkdir2(dirname2(paths.botsFile), { recursive: true });
|
|
188
|
+
const tmp = `${paths.botsFile}.tmp-${process.pid}`;
|
|
189
|
+
await writeFile2(tmp, `${JSON.stringify(reg, null, 2)}
|
|
190
|
+
`, "utf8");
|
|
191
|
+
await chmod2(tmp, 384);
|
|
192
|
+
await rename2(tmp, paths.botsFile);
|
|
193
|
+
}
|
|
194
|
+
async function ensureRegistry() {
|
|
195
|
+
if (existsSync(paths.botsFile)) return loadBots();
|
|
196
|
+
const flatConfigPath = join2(paths.appDir, "config.json");
|
|
197
|
+
const flat = await loadConfig(flatConfigPath);
|
|
198
|
+
if (!isComplete(flat)) return { ...EMPTY };
|
|
199
|
+
const { id: appId, tenant } = flat.accounts.app;
|
|
200
|
+
const dest = botDir(appId);
|
|
201
|
+
await mkdir2(dest, { recursive: true });
|
|
202
|
+
for (const file of ["config.json", "projects.json", "sessions.json", "processes.json"]) {
|
|
203
|
+
await moveIfExists(join2(paths.appDir, file), join2(dest, file));
|
|
204
|
+
}
|
|
205
|
+
const reg = {
|
|
206
|
+
version: 1,
|
|
207
|
+
current: appId,
|
|
208
|
+
bots: [{ name: "default", appId, tenant, createdAt: nowMs() }]
|
|
209
|
+
};
|
|
210
|
+
await saveBots(reg);
|
|
211
|
+
return reg;
|
|
212
|
+
}
|
|
213
|
+
function findBot(reg, nameOrAppId) {
|
|
214
|
+
return reg.bots.find((b) => b.name === nameOrAppId || b.appId === nameOrAppId);
|
|
215
|
+
}
|
|
216
|
+
function currentBot(reg) {
|
|
217
|
+
return reg.current ? reg.bots.find((b) => b.appId === reg.current) : void 0;
|
|
218
|
+
}
|
|
219
|
+
async function addBot(entry) {
|
|
220
|
+
const reg = await loadBots();
|
|
221
|
+
reg.bots = reg.bots.filter((b) => b.appId !== entry.appId);
|
|
222
|
+
reg.bots.push(entry);
|
|
223
|
+
if (!reg.current) reg.current = entry.appId;
|
|
224
|
+
await saveBots(reg);
|
|
225
|
+
return reg;
|
|
226
|
+
}
|
|
227
|
+
async function setCurrent(appId) {
|
|
228
|
+
const reg = await loadBots();
|
|
229
|
+
reg.current = appId;
|
|
230
|
+
await saveBots(reg);
|
|
231
|
+
}
|
|
232
|
+
async function removeBot(appId) {
|
|
233
|
+
const reg = await loadBots();
|
|
234
|
+
reg.bots = reg.bots.filter((b) => b.appId !== appId);
|
|
235
|
+
if (reg.current === appId) reg.current = reg.bots[0]?.appId;
|
|
236
|
+
await saveBots(reg);
|
|
237
|
+
return reg;
|
|
238
|
+
}
|
|
239
|
+
function uniqueName(reg, desired) {
|
|
240
|
+
const base = slugify(desired) || "bot";
|
|
241
|
+
if (!reg.bots.some((b) => b.name === base)) return base;
|
|
242
|
+
for (let i = 2; ; i++) {
|
|
243
|
+
const candidate = `${base}-${i}`;
|
|
244
|
+
if (!reg.bots.some((b) => b.name === candidate)) return candidate;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function slugify(s) {
|
|
248
|
+
return s.trim().toLowerCase().replace(/[^a-z0-9一-龥]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
249
|
+
}
|
|
250
|
+
function nowMs() {
|
|
251
|
+
return Date.now();
|
|
252
|
+
}
|
|
253
|
+
async function moveIfExists(src, dest) {
|
|
254
|
+
if (!existsSync(src)) return;
|
|
255
|
+
await rename2(src, dest);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/agent/codex-appserver/locate.ts
|
|
259
|
+
import { execFileSync } from "child_process";
|
|
260
|
+
import { existsSync as existsSync2 } from "fs";
|
|
261
|
+
import { join as join3 } from "path";
|
|
262
|
+
function resolveCodexBin() {
|
|
263
|
+
const env = process.env.CODEX_BIN;
|
|
264
|
+
if (env && existsSync2(env)) return env;
|
|
265
|
+
const onPath = which("codex");
|
|
266
|
+
if (onPath) return onPath;
|
|
267
|
+
const priv = join3(paths.codexCliBinDir, "codex");
|
|
268
|
+
if (existsSync2(priv)) return priv;
|
|
269
|
+
const appBundle = "/Applications/Codex.app/Contents/Resources/codex";
|
|
270
|
+
if (process.platform === "darwin" && existsSync2(appBundle)) return appBundle;
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function which(cmd) {
|
|
274
|
+
try {
|
|
275
|
+
const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
|
|
276
|
+
encoding: "utf8",
|
|
277
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
278
|
+
});
|
|
279
|
+
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
280
|
+
return first && existsSync2(first) ? first : null;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function codexVersion(bin) {
|
|
286
|
+
try {
|
|
287
|
+
return execFileSync(bin, ["--version"], { encoding: "utf8" }).trim();
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/cli/commands/doctor.ts
|
|
294
|
+
async function runDoctor() {
|
|
295
|
+
const checks = [];
|
|
296
|
+
const codexBin = resolveCodexBin();
|
|
297
|
+
if (codexBin) {
|
|
298
|
+
const v = codexVersion(codexBin) ?? "unknown";
|
|
299
|
+
checks.push({ name: "codex CLI", ok: true, detail: `${v} (${codexBin})` });
|
|
300
|
+
} else {
|
|
301
|
+
checks.push({
|
|
302
|
+
name: "codex CLI",
|
|
303
|
+
ok: false,
|
|
304
|
+
detail: "\u672A\u627E\u5230\u3002\u8BBE\u7F6E CODEX_BIN\uFF0C\u6216\u5B89\u88C5 @openai/codex\uFF0C\u6216\u88C5 Codex.app"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const codexAuth = join4(process.env.CODEX_HOME ?? join4(homedir2(), ".codex"), "auth.json");
|
|
308
|
+
checks.push(
|
|
309
|
+
existsSync3(codexAuth) ? { name: "codex \u767B\u5F55", ok: true, detail: codexAuth } : { name: "codex \u767B\u5F55", ok: false, detail: "\u672A\u767B\u5F55\uFF0C\u8FD0\u884C `codex login`" }
|
|
310
|
+
);
|
|
311
|
+
const larkVer = tryExec("lark-cli", ["--version"]);
|
|
312
|
+
checks.push(
|
|
313
|
+
larkVer ? { name: "lark-cli", ok: true, detail: larkVer } : { name: "lark-cli", ok: false, detail: "\u672A\u627E\u5230\uFF08onboarding \u4F1A\u88C5\u5230\u79C1\u6709\u76EE\u5F55\uFF09" }
|
|
314
|
+
);
|
|
315
|
+
const reg = await ensureRegistry();
|
|
316
|
+
const cur = currentBot(reg);
|
|
317
|
+
if (cur) useBotDir(cur.appId);
|
|
318
|
+
if (cur && existsSync3(paths.configFile)) {
|
|
319
|
+
checks.push({
|
|
320
|
+
name: "bridge \u914D\u7F6E",
|
|
321
|
+
ok: true,
|
|
322
|
+
detail: `\u5F53\u524D\u673A\u5668\u4EBA\u300C${cur.name}\u300D(${cur.appId}) \u5171 ${reg.bots.length} \u4E2A`
|
|
323
|
+
});
|
|
324
|
+
} else if (cur) {
|
|
325
|
+
checks.push({ name: "bridge \u914D\u7F6E", ok: false, detail: `\u914D\u7F6E\u6587\u4EF6\u7F3A\u5931\uFF1A${paths.configFile}` });
|
|
326
|
+
} else {
|
|
327
|
+
checks.push({
|
|
328
|
+
name: "bridge \u914D\u7F6E",
|
|
329
|
+
ok: false,
|
|
330
|
+
detail: "\u672A\u914D\u7F6E\uFF0C\u8FD0\u884C `feishu-codex-bridge run`\uFF08\u6216 `bot init`\uFF09\u626B\u7801\u521B\u5EFA"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
console.log("\n\u{1FA7A} feishu-codex-bridge \u81EA\u68C0\n");
|
|
334
|
+
for (const c of checks) {
|
|
335
|
+
console.log(` ${c.ok ? "\u2705" : "\u274C"} ${c.name.padEnd(12)} ${c.detail}`);
|
|
336
|
+
}
|
|
337
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
338
|
+
console.log(`
|
|
339
|
+
${failed === 0 ? "\u5168\u90E8\u901A\u8FC7 \u2713" : `${failed} \u9879\u9700\u5904\u7406`}
|
|
340
|
+
`);
|
|
341
|
+
process.exitCode = failed === 0 ? 0 : 1;
|
|
342
|
+
}
|
|
343
|
+
function tryExec(cmd, args) {
|
|
344
|
+
try {
|
|
345
|
+
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/bot/onboarding.ts
|
|
352
|
+
import { createInterface } from "readline/promises";
|
|
353
|
+
|
|
354
|
+
// src/config/keystore.ts
|
|
355
|
+
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "crypto";
|
|
356
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
357
|
+
import { hostname, userInfo } from "os";
|
|
358
|
+
import { dirname as dirname3 } from "path";
|
|
359
|
+
var KEY_LEN = 32;
|
|
360
|
+
var IV_LEN = 12;
|
|
361
|
+
var TAG_LEN = 16;
|
|
362
|
+
var PBKDF2_ITER = 1e5;
|
|
363
|
+
var FILE_VERSION = 1;
|
|
364
|
+
var EMPTY2 = { version: FILE_VERSION, entries: {} };
|
|
365
|
+
async function readStore() {
|
|
366
|
+
try {
|
|
367
|
+
const text = await readFile3(paths.secretsFile, "utf8");
|
|
368
|
+
const parsed = JSON.parse(text);
|
|
369
|
+
if (parsed?.version !== FILE_VERSION || !parsed.entries) return { ...EMPTY2 };
|
|
370
|
+
return { version: parsed.version, entries: { ...parsed.entries } };
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err.code === "ENOENT") return { ...EMPTY2 };
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function writeStore(store) {
|
|
377
|
+
await mkdir3(dirname3(paths.secretsFile), { recursive: true });
|
|
378
|
+
const tmp = `${paths.secretsFile}.tmp-${process.pid}`;
|
|
379
|
+
await writeFile3(tmp, `${JSON.stringify(store, null, 2)}
|
|
380
|
+
`, "utf8");
|
|
381
|
+
await chmod3(tmp, 384);
|
|
382
|
+
await rename3(tmp, paths.secretsFile);
|
|
383
|
+
}
|
|
384
|
+
async function loadOrCreateSalt() {
|
|
385
|
+
try {
|
|
386
|
+
const buf = await readFile3(paths.keystoreSaltFile);
|
|
387
|
+
if (buf.length === KEY_LEN) return buf;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (err.code !== "ENOENT") throw err;
|
|
390
|
+
}
|
|
391
|
+
const salt = randomBytes(KEY_LEN);
|
|
392
|
+
await mkdir3(dirname3(paths.keystoreSaltFile), { recursive: true });
|
|
393
|
+
const tmp = `${paths.keystoreSaltFile}.tmp-${process.pid}`;
|
|
394
|
+
await writeFile3(tmp, salt);
|
|
395
|
+
await chmod3(tmp, 384);
|
|
396
|
+
await rename3(tmp, paths.keystoreSaltFile);
|
|
397
|
+
return salt;
|
|
398
|
+
}
|
|
399
|
+
async function deriveKey() {
|
|
400
|
+
const salt = await loadOrCreateSalt();
|
|
401
|
+
const seed = `${hostname()}|${userInfo().username}`;
|
|
402
|
+
return pbkdf2Sync(seed, salt, PBKDF2_ITER, KEY_LEN, "sha256");
|
|
403
|
+
}
|
|
404
|
+
function encrypt(key, plaintext) {
|
|
405
|
+
const iv = randomBytes(IV_LEN);
|
|
406
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
407
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
408
|
+
const tag = cipher.getAuthTag();
|
|
409
|
+
return { iv: iv.toString("base64"), data: enc.toString("base64"), tag: tag.toString("base64") };
|
|
410
|
+
}
|
|
411
|
+
function decrypt(key, env) {
|
|
412
|
+
const iv = Buffer.from(env.iv, "base64");
|
|
413
|
+
const data = Buffer.from(env.data, "base64");
|
|
414
|
+
const tag = Buffer.from(env.tag, "base64");
|
|
415
|
+
if (iv.length !== IV_LEN) throw new Error("invalid IV length");
|
|
416
|
+
if (tag.length !== TAG_LEN) throw new Error("invalid auth tag length");
|
|
417
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
418
|
+
decipher.setAuthTag(tag);
|
|
419
|
+
const dec = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
420
|
+
return dec.toString("utf8");
|
|
421
|
+
}
|
|
422
|
+
async function getSecret(id) {
|
|
423
|
+
const store = await readStore();
|
|
424
|
+
const env = store.entries[id];
|
|
425
|
+
if (!env) return void 0;
|
|
426
|
+
const key = await deriveKey();
|
|
427
|
+
return decrypt(key, env);
|
|
428
|
+
}
|
|
429
|
+
async function setSecret(id, plaintext) {
|
|
430
|
+
const key = await deriveKey();
|
|
431
|
+
const env = encrypt(key, plaintext);
|
|
432
|
+
const store = await readStore();
|
|
433
|
+
store.entries[id] = env;
|
|
434
|
+
await writeStore(store);
|
|
435
|
+
}
|
|
436
|
+
async function removeSecret(id) {
|
|
437
|
+
const store = await readStore();
|
|
438
|
+
if (!(id in store.entries)) return false;
|
|
439
|
+
delete store.entries[id];
|
|
440
|
+
await writeStore(store);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
async function listSecretIds() {
|
|
444
|
+
const store = await readStore();
|
|
445
|
+
return Object.keys(store.entries);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/config/secret-resolver.ts
|
|
449
|
+
import { spawn } from "child_process";
|
|
450
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
451
|
+
import { join as join5 } from "path";
|
|
452
|
+
var ENV_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
|
453
|
+
var DEFAULT_PROVIDER = "default";
|
|
454
|
+
var DEFAULT_EXEC_TIMEOUT_MS = 5e3;
|
|
455
|
+
var DEFAULT_EXEC_MAX_OUTPUT = 64 * 1024;
|
|
456
|
+
async function resolveAppSecret(cfg) {
|
|
457
|
+
const appId = cfg.accounts.app.id;
|
|
458
|
+
return resolveSecretInput(cfg.accounts.app.secret, cfg.secrets, appId);
|
|
459
|
+
}
|
|
460
|
+
async function resolveSecretInput(input2, secretsCfg, appId) {
|
|
461
|
+
if (!input2) throw new Error("app secret is missing");
|
|
462
|
+
if (typeof input2 === "string") return resolvePlainOrTemplate(input2);
|
|
463
|
+
if (!isSecretRef(input2)) throw new Error(`unsupported secret form: ${JSON.stringify(input2)}`);
|
|
464
|
+
switch (input2.source) {
|
|
465
|
+
case "env":
|
|
466
|
+
return resolveEnvRef(input2, lookupProvider(secretsCfg, input2));
|
|
467
|
+
case "file":
|
|
468
|
+
return resolveFileRef(input2, lookupProvider(secretsCfg, input2));
|
|
469
|
+
case "exec":
|
|
470
|
+
return resolveExecRef(input2, lookupProvider(secretsCfg, input2), appId);
|
|
471
|
+
default:
|
|
472
|
+
throw new Error(`unknown secret source: ${input2.source}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function resolvePlainOrTemplate(value) {
|
|
476
|
+
if (!value) throw new Error("app secret is empty");
|
|
477
|
+
const m = ENV_TEMPLATE_RE.exec(value);
|
|
478
|
+
if (m) {
|
|
479
|
+
const name = m[1];
|
|
480
|
+
const v = process.env[name];
|
|
481
|
+
if (!v) throw new Error(`env var ${name} referenced by secret is not set`);
|
|
482
|
+
return v;
|
|
483
|
+
}
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
function lookupProvider(secretsCfg, ref) {
|
|
487
|
+
if (!secretsCfg?.providers) return void 0;
|
|
488
|
+
const name = ref.provider ?? secretsCfg.defaults?.[ref.source] ?? DEFAULT_PROVIDER;
|
|
489
|
+
return secretsCfg.providers[name];
|
|
490
|
+
}
|
|
491
|
+
function resolveEnvRef(ref, pc) {
|
|
492
|
+
if (pc?.allowlist && pc.allowlist.length > 0 && !pc.allowlist.includes(ref.id)) {
|
|
493
|
+
throw new Error(`env var ${ref.id} is not allowlisted in provider`);
|
|
494
|
+
}
|
|
495
|
+
const v = process.env[ref.id];
|
|
496
|
+
if (!v) throw new Error(`env var ${ref.id} is not set`);
|
|
497
|
+
return v;
|
|
498
|
+
}
|
|
499
|
+
async function resolveFileRef(ref, pc) {
|
|
500
|
+
const path = pc?.path ? join5(pc.path, ref.id) : ref.id;
|
|
501
|
+
return (await readFile4(path, "utf8")).trim();
|
|
502
|
+
}
|
|
503
|
+
async function resolveExecRef(ref, pc, appId) {
|
|
504
|
+
if (!pc?.command) throw new Error("exec provider missing `command`");
|
|
505
|
+
if (isSelfBridgeCommand(pc.command, pc.args)) {
|
|
506
|
+
const candidate = await getSecret(ref.id);
|
|
507
|
+
if (candidate !== void 0) return candidate;
|
|
508
|
+
const conventional = secretKeyForApp(appId);
|
|
509
|
+
const fallback = await getSecret(conventional);
|
|
510
|
+
if (fallback !== void 0) return fallback;
|
|
511
|
+
throw new Error(`keystore has no entry for "${ref.id}" or "${conventional}"`);
|
|
512
|
+
}
|
|
513
|
+
return spawnExecProvider(pc, ref);
|
|
514
|
+
}
|
|
515
|
+
function isSelfBridgeCommand(command, args) {
|
|
516
|
+
if (command === paths.secretsGetterScript) return true;
|
|
517
|
+
if (args && args.length >= 2) {
|
|
518
|
+
const a = args[args.length - 2];
|
|
519
|
+
const b = args[args.length - 1];
|
|
520
|
+
if (a === "secrets" && b === "get") return true;
|
|
521
|
+
}
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
async function spawnExecProvider(pc, ref) {
|
|
525
|
+
const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
|
|
526
|
+
const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
|
|
527
|
+
const providerName = ref.provider ?? DEFAULT_PROVIDER;
|
|
528
|
+
return new Promise((resolve3, reject) => {
|
|
529
|
+
const env = {};
|
|
530
|
+
if (pc.passEnv) for (const k of pc.passEnv) {
|
|
531
|
+
const v = process.env[k];
|
|
532
|
+
if (v) env[k] = v;
|
|
533
|
+
}
|
|
534
|
+
if (pc.env) Object.assign(env, pc.env);
|
|
535
|
+
const child = spawn(pc.command, pc.args ?? [], { env, stdio: ["pipe", "pipe", "pipe"] });
|
|
536
|
+
let stdout = "", stderr = "", truncated = false, settled = false;
|
|
537
|
+
const timer = setTimeout(() => {
|
|
538
|
+
if (settled) return;
|
|
539
|
+
settled = true;
|
|
540
|
+
child.kill("SIGKILL");
|
|
541
|
+
reject(new Error(`exec provider timed out after ${timeoutMs}ms`));
|
|
542
|
+
}, timeoutMs);
|
|
543
|
+
child.stdout.on("data", (chunk) => {
|
|
544
|
+
if (truncated) return;
|
|
545
|
+
if (stdout.length + chunk.length > maxOutput) {
|
|
546
|
+
truncated = true;
|
|
547
|
+
child.kill("SIGKILL");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
stdout += chunk.toString("utf8");
|
|
551
|
+
});
|
|
552
|
+
child.stderr.on("data", (chunk) => {
|
|
553
|
+
stderr += chunk.toString("utf8");
|
|
554
|
+
});
|
|
555
|
+
child.on("error", (err) => {
|
|
556
|
+
if (settled) return;
|
|
557
|
+
settled = true;
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
reject(new Error(`exec provider failed to start: ${err.message}`));
|
|
560
|
+
});
|
|
561
|
+
child.on("close", (code) => {
|
|
562
|
+
if (settled) return;
|
|
563
|
+
settled = true;
|
|
564
|
+
clearTimeout(timer);
|
|
565
|
+
if (truncated) return reject(new Error(`exec provider stdout exceeded ${maxOutput} bytes`));
|
|
566
|
+
if (code !== 0) {
|
|
567
|
+
const detail = stderr.trim() ? `: ${stderr.trim().slice(0, 200)}` : "";
|
|
568
|
+
return reject(new Error(`exec provider exited with code ${code}${detail}`));
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const parsed = JSON.parse(stdout);
|
|
572
|
+
const value = parsed.values?.[ref.id];
|
|
573
|
+
if (typeof value === "string") return resolve3(value);
|
|
574
|
+
const err = parsed.errors?.[ref.id]?.message;
|
|
575
|
+
reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
|
|
576
|
+
} catch (err) {
|
|
577
|
+
reject(new Error(`exec provider returned invalid JSON: ${err.message}`));
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
child.stdin.end(JSON.stringify({ protocolVersion: 1, provider: providerName, ids: [ref.id] }));
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/bot/wizard.ts
|
|
585
|
+
import { registerApp } from "@larksuiteoapi/node-sdk";
|
|
586
|
+
import qrcode from "qrcode-terminal";
|
|
587
|
+
async function runRegistrationWizard() {
|
|
588
|
+
console.log("\n\u672A\u68C0\u6D4B\u5230\u98DE\u4E66\u5E94\u7528\u914D\u7F6E\uFF0C\u8FDB\u5165\u626B\u7801\u521B\u5EFA\u5411\u5BFC\u3002\n");
|
|
589
|
+
const result = await registerApp({
|
|
590
|
+
onQRCodeReady: (info) => {
|
|
591
|
+
console.log("\u8BF7\u7528\u98DE\u4E66 App \u626B\u63CF\u4EE5\u4E0B\u4E8C\u7EF4\u7801\u5B8C\u6210\u5E94\u7528\u521B\u5EFA\uFF1A\n");
|
|
592
|
+
qrcode.generate(info.url, { small: true });
|
|
593
|
+
const mins = Math.max(1, Math.round(info.expireIn / 60));
|
|
594
|
+
console.log(`
|
|
595
|
+
\u4E8C\u7EF4\u7801\u6709\u6548\u671F\uFF1A\u7EA6 ${mins} \u5206\u949F`);
|
|
596
|
+
console.log(`\u4E5F\u53EF\u4EE5\u76F4\u63A5\u5728\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A${info.url}
|
|
597
|
+
`);
|
|
598
|
+
},
|
|
599
|
+
onStatusChange: (info) => {
|
|
600
|
+
if (info.status === "domain_switched") {
|
|
601
|
+
console.log("\u8BC6\u522B\u5230\u56FD\u9645\u7248\u79DF\u6237\uFF0C\u5DF2\u5207\u6362\u5230 larksuite.com \u57DF\u540D\u3002");
|
|
602
|
+
} else if (info.status === "slow_down") {
|
|
603
|
+
console.log("\u8F6E\u8BE2\u901F\u5EA6\u8FC7\u5FEB\uFF0C\u5DF2\u81EA\u52A8\u964D\u901F\u3002");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
const tenant = result.user_info?.tenant_brand ?? "feishu";
|
|
608
|
+
const operatorOpenId = result.user_info?.open_id;
|
|
609
|
+
console.log("\n\u2713 \u5E94\u7528\u521B\u5EFA\u6210\u529F");
|
|
610
|
+
console.log(` App ID: ${result.client_id}`);
|
|
611
|
+
console.log(` Tenant: ${tenant}`);
|
|
612
|
+
const cfg = {
|
|
613
|
+
accounts: { app: { id: result.client_id, secret: result.client_secret, tenant } }
|
|
614
|
+
};
|
|
615
|
+
if (operatorOpenId) {
|
|
616
|
+
cfg.preferences = { access: { admins: [operatorOpenId] } };
|
|
617
|
+
console.log(` Admin: ${operatorOpenId} (\u4F60\u81EA\u5DF1\uFF0C\u5DF2\u81EA\u52A8\u52A0\u5165\u7BA1\u7406\u5458\u540D\u5355)`);
|
|
618
|
+
} else {
|
|
619
|
+
console.log(
|
|
620
|
+
" \u26A0\uFE0F \u672A\u62FF\u5230\u626B\u7801\u7528\u6237\u7684 open_id\uFF1B\u7BA1\u7406\u5458\u5217\u8868\u7559\u7A7A = \u6240\u6709\u7528\u6237\u90FD\u80FD\u79C1\u804A\u5EFA\u9879\u76EE\u3002\n \u53EF\u7A0D\u540E\u5728 /config \u624B\u52A8\u8BBE\u7F6E\u7BA1\u7406\u5458\u3002"
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
console.log("");
|
|
624
|
+
return cfg;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/config/scopes.ts
|
|
628
|
+
var REQUIRED_SCOPES = [
|
|
629
|
+
// Feishu has split the old umbrella scopes (`im:chat`, `im:message`) into
|
|
630
|
+
// fine-grained ones; new apps can only be granted the fine-grained names, so
|
|
631
|
+
// we list those — not the umbrellas (which would be un-grantable + would make
|
|
632
|
+
// the scope check false-positive).
|
|
633
|
+
"im:message.group_at_msg:readonly",
|
|
634
|
+
// @bot messages in project groups
|
|
635
|
+
"im:message.group_msg",
|
|
636
|
+
// ALL group messages (高敏感) — required for 免@ (respond without @)
|
|
637
|
+
"im:message.p2p_msg:readonly",
|
|
638
|
+
// DM console messages
|
|
639
|
+
"im:message:send_as_bot",
|
|
640
|
+
// reply_in_thread / send cards
|
|
641
|
+
"im:message.pins:write_only",
|
|
642
|
+
// Pin the welcome/command card to the group's Pins tab (im.v1.pin.create) — NOTE: plural `pins`
|
|
643
|
+
"im:message.reactions:write_only",
|
|
644
|
+
// ⏳/🫳 run-status emoji reactions (best-effort) — NOTE: plural `reactions`
|
|
645
|
+
"im:resource",
|
|
646
|
+
// upload/download images & resources
|
|
647
|
+
"im:chat:create",
|
|
648
|
+
// create the project group
|
|
649
|
+
"im:chat:update",
|
|
650
|
+
// transfer ownership on unbind
|
|
651
|
+
"im:chat.announcement:read",
|
|
652
|
+
// read group announcement blocks (list)
|
|
653
|
+
"im:chat.announcement:write_only",
|
|
654
|
+
// write group announcement blocks (create/delete)
|
|
655
|
+
"im:chat.top_notice:write_only",
|
|
656
|
+
// pin the announcement to the top banner
|
|
657
|
+
"im:chat.tabs:write_only",
|
|
658
|
+
// add the "👈 查看可使用的命令" chat tab on group create
|
|
659
|
+
"cardkit:card:write"
|
|
660
|
+
// interactive button cards (CardKit entities)
|
|
661
|
+
];
|
|
662
|
+
var COMMENT_SCOPES = [
|
|
663
|
+
"docs:document.comment:read",
|
|
664
|
+
"docs:document.comment:create",
|
|
665
|
+
"wiki:wiki:readonly"
|
|
666
|
+
];
|
|
667
|
+
var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES];
|
|
668
|
+
var HOSTS = {
|
|
669
|
+
feishu: "open.feishu.cn",
|
|
670
|
+
lark: "open.larksuite.com"
|
|
671
|
+
};
|
|
672
|
+
function buildScopeGrantUrl(appId, tenant, scopes = GRANT_SCOPES) {
|
|
673
|
+
const host = HOSTS[tenant];
|
|
674
|
+
const q = encodeURIComponent(scopes.join(","));
|
|
675
|
+
return `https://${host}/app/${encodeURIComponent(appId)}/auth?q=${q}`;
|
|
676
|
+
}
|
|
677
|
+
function buildEventConfigUrl(appId, tenant) {
|
|
678
|
+
return `https://${HOSTS[tenant]}/app/${encodeURIComponent(appId)}/event`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/utils/feishu-auth.ts
|
|
682
|
+
var ENDPOINTS = {
|
|
683
|
+
feishu: "https://open.feishu.cn",
|
|
684
|
+
lark: "https://open.larksuite.com"
|
|
685
|
+
};
|
|
686
|
+
async function validateAppCredentials(appId, appSecret, tenant) {
|
|
687
|
+
const base = ENDPOINTS[tenant];
|
|
688
|
+
let resp;
|
|
689
|
+
try {
|
|
690
|
+
resp = await fetch(`${base}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
691
|
+
method: "POST",
|
|
692
|
+
headers: { "Content-Type": "application/json" },
|
|
693
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret })
|
|
694
|
+
});
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return { ok: false, reason: `\u7F51\u7EDC\u9519\u8BEF\uFF1A${err instanceof Error ? err.message : String(err)}` };
|
|
697
|
+
}
|
|
698
|
+
if (!resp.ok) return { ok: false, reason: `HTTP ${resp.status}` };
|
|
699
|
+
let data;
|
|
700
|
+
try {
|
|
701
|
+
data = await resp.json();
|
|
702
|
+
} catch {
|
|
703
|
+
return { ok: false, reason: "\u54CD\u5E94\u4E0D\u662F\u5408\u6CD5 JSON" };
|
|
704
|
+
}
|
|
705
|
+
if (data.code !== 0 || !data.tenant_access_token) {
|
|
706
|
+
return { ok: false, reason: `code=${data.code ?? "?"} msg=${data.msg ?? "<no msg>"}` };
|
|
707
|
+
}
|
|
708
|
+
const token = data.tenant_access_token;
|
|
709
|
+
const info = await fetchBotInfo(base, token).catch(() => void 0);
|
|
710
|
+
const missingScopes = await fetchMissingScopes(base, token).catch(() => void 0);
|
|
711
|
+
return { ok: true, botName: info?.bot?.app_name, botOpenId: info?.bot?.open_id, missingScopes };
|
|
712
|
+
}
|
|
713
|
+
async function fetchBotInfo(base, token) {
|
|
714
|
+
const resp = await fetch(`${base}/open-apis/bot/v3/info`, {
|
|
715
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
716
|
+
});
|
|
717
|
+
if (!resp.ok) return void 0;
|
|
718
|
+
return await resp.json();
|
|
719
|
+
}
|
|
720
|
+
async function fetchMissingScopes(base, token) {
|
|
721
|
+
const resp = await fetch(`${base}/open-apis/application/v6/scopes`, {
|
|
722
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
723
|
+
});
|
|
724
|
+
if (!resp.ok) return void 0;
|
|
725
|
+
const body = await resp.json();
|
|
726
|
+
if (!body.data?.scopes) return void 0;
|
|
727
|
+
const granted = new Set(body.data.scopes.filter((s) => s.grant_status === 1).map((s) => s.scope_name));
|
|
728
|
+
return REQUIRED_SCOPES.filter((s) => !granted.has(s));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/utils/open-url.ts
|
|
732
|
+
import { spawn as spawn2 } from "child_process";
|
|
733
|
+
import { platform } from "os";
|
|
734
|
+
function openUrl(url) {
|
|
735
|
+
if (!process.stdout.isTTY) return false;
|
|
736
|
+
let cmd;
|
|
737
|
+
let args;
|
|
738
|
+
switch (platform()) {
|
|
739
|
+
case "darwin":
|
|
740
|
+
cmd = "open";
|
|
741
|
+
args = [url];
|
|
742
|
+
break;
|
|
743
|
+
case "win32":
|
|
744
|
+
cmd = "cmd";
|
|
745
|
+
args = ["/c", "start", "", url];
|
|
746
|
+
break;
|
|
747
|
+
default:
|
|
748
|
+
cmd = "xdg-open";
|
|
749
|
+
args = [url];
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
753
|
+
child.on("error", () => {
|
|
754
|
+
});
|
|
755
|
+
child.unref();
|
|
756
|
+
return true;
|
|
757
|
+
} catch {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/core/logger.ts
|
|
763
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
764
|
+
import { createWriteStream, mkdirSync } from "fs";
|
|
765
|
+
import { open, readdir, rm, stat } from "fs/promises";
|
|
766
|
+
import { join as join6 } from "path";
|
|
767
|
+
var LOG_RETENTION_DAYS = Math.max(
|
|
768
|
+
1,
|
|
769
|
+
Number(process.env.FEISHU_CODEX_LOG_DAYS ?? 7) || 7
|
|
770
|
+
);
|
|
771
|
+
var STDOUT_INFO_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
772
|
+
"ws.connected",
|
|
773
|
+
"ws.reconnecting",
|
|
774
|
+
"ws.reconnected",
|
|
775
|
+
"intake.enter",
|
|
776
|
+
"intake.recv",
|
|
777
|
+
"intake.reject",
|
|
778
|
+
"card.final",
|
|
779
|
+
"card.config",
|
|
780
|
+
"card.action",
|
|
781
|
+
"card.launch",
|
|
782
|
+
"agent.spawn",
|
|
783
|
+
"agent.exit"
|
|
784
|
+
]);
|
|
785
|
+
var als = new AsyncLocalStorage();
|
|
786
|
+
var stream = null;
|
|
787
|
+
var currentDate = "";
|
|
788
|
+
function todayKey() {
|
|
789
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
790
|
+
}
|
|
791
|
+
function logsDir() {
|
|
792
|
+
return join6(paths.appDir, "logs");
|
|
793
|
+
}
|
|
794
|
+
function getStream() {
|
|
795
|
+
const today = todayKey();
|
|
796
|
+
if (stream && currentDate === today) return stream;
|
|
797
|
+
if (stream) {
|
|
798
|
+
try {
|
|
799
|
+
stream.end();
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
mkdirSync(logsDir(), { recursive: true });
|
|
805
|
+
stream = createWriteStream(join6(logsDir(), `${today}.log`), { flags: "a" });
|
|
806
|
+
currentDate = today;
|
|
807
|
+
return stream;
|
|
808
|
+
} catch {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
813
|
+
"ts",
|
|
814
|
+
"level",
|
|
815
|
+
"phase",
|
|
816
|
+
"event",
|
|
817
|
+
"traceId",
|
|
818
|
+
"chatId",
|
|
819
|
+
"msgId"
|
|
820
|
+
]);
|
|
821
|
+
function emit(level, phase, event, fields = {}) {
|
|
822
|
+
const ctx = als.getStore() ?? {};
|
|
823
|
+
const entry = {
|
|
824
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
825
|
+
level,
|
|
826
|
+
phase,
|
|
827
|
+
event,
|
|
828
|
+
...ctx
|
|
829
|
+
};
|
|
830
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
831
|
+
if (RESERVED_KEYS.has(k)) {
|
|
832
|
+
entry[`_${k}`] = v;
|
|
833
|
+
} else {
|
|
834
|
+
entry[k] = v;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const s = getStream();
|
|
838
|
+
if (s) {
|
|
839
|
+
try {
|
|
840
|
+
s.write(`${JSON.stringify(entry)}
|
|
841
|
+
`);
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const showOnStdout = level !== "info" || STDOUT_INFO_ALLOWLIST.has(`${phase}.${event}`);
|
|
846
|
+
if (!showOnStdout) return;
|
|
847
|
+
const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
848
|
+
fn(formatStdout(level, phase, event, ctx, fields));
|
|
849
|
+
}
|
|
850
|
+
function formatStdout(level, phase, event, ctx, fields) {
|
|
851
|
+
if (phase === "ws") {
|
|
852
|
+
if (event === "connected") {
|
|
853
|
+
const bot2 = fields.bot ?? "-";
|
|
854
|
+
const appId = fields.appId ? ` (${fields.appId})` : "";
|
|
855
|
+
return `\u2713 \u5DF2\u8FDE\u63A5 bot: ${bot2}${appId}`;
|
|
856
|
+
}
|
|
857
|
+
if (event === "reconnecting") return "\u21BB \u6B63\u5728\u91CD\u8FDE\u2026";
|
|
858
|
+
if (event === "reconnected") return "\u2713 \u5DF2\u91CD\u8FDE";
|
|
859
|
+
if (event === "fail") return `\u2717 WS \u9519\u8BEF: ${fields.err ?? ""}`;
|
|
860
|
+
}
|
|
861
|
+
if (phase === "intake" && event === "enter") {
|
|
862
|
+
const c = ctx.chatId ? ctx.chatId.slice(-6) : "-";
|
|
863
|
+
const sender = fields.sender ?? "-";
|
|
864
|
+
const preview = fields.preview ?? "";
|
|
865
|
+
return `\u25B8 ${fields.chatType ?? "?"}/${c} ${sender}: ${preview}`;
|
|
866
|
+
}
|
|
867
|
+
if (phase === "card" && event === "final") {
|
|
868
|
+
const c = ctx.chatId ? ctx.chatId.slice(-6) : "-";
|
|
869
|
+
const t = fields.terminal;
|
|
870
|
+
const mark = t === "done" ? "\u2713" : t === "interrupted" ? "\u23F9" : "\u2717";
|
|
871
|
+
return ` ${mark} ${c} ${t}`;
|
|
872
|
+
}
|
|
873
|
+
const ctxBits = [];
|
|
874
|
+
if (ctx.traceId) ctxBits.push(`t=${ctx.traceId}`);
|
|
875
|
+
if (ctx.chatId) ctxBits.push(`c=${ctx.chatId.slice(-6)}`);
|
|
876
|
+
const ctxStr = ctxBits.length > 0 ? ` ${ctxBits.join(" ")}` : "";
|
|
877
|
+
const summary = formatFields(fields);
|
|
878
|
+
const tag = level === "error" ? "\u2717" : level === "warn" ? "\u26A0" : "\xB7";
|
|
879
|
+
return `${tag} [${phase}.${event}]${ctxStr}${summary ? ` ${summary}` : ""}`;
|
|
880
|
+
}
|
|
881
|
+
function formatFields(fields) {
|
|
882
|
+
const keys = Object.keys(fields);
|
|
883
|
+
if (keys.length === 0) return "";
|
|
884
|
+
const parts = [];
|
|
885
|
+
for (const k of keys) {
|
|
886
|
+
const v = fields[k];
|
|
887
|
+
if (v === void 0 || v === null) continue;
|
|
888
|
+
if (k === "stack") continue;
|
|
889
|
+
if (typeof v === "string") {
|
|
890
|
+
parts.push(`${k}=${v.length > 80 ? `${v.slice(0, 80)}\u2026` : v}`);
|
|
891
|
+
} else if (typeof v === "number" || typeof v === "boolean") {
|
|
892
|
+
parts.push(`${k}=${v}`);
|
|
893
|
+
} else {
|
|
894
|
+
try {
|
|
895
|
+
const str = JSON.stringify(v);
|
|
896
|
+
parts.push(`${k}=${str.length > 80 ? `${str.slice(0, 80)}\u2026` : str}`);
|
|
897
|
+
} catch {
|
|
898
|
+
parts.push(`${k}=?`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return parts.join(" ");
|
|
903
|
+
}
|
|
904
|
+
var log = {
|
|
905
|
+
info(phase, event, fields) {
|
|
906
|
+
emit("info", phase, event, fields);
|
|
907
|
+
},
|
|
908
|
+
warn(phase, event, fields) {
|
|
909
|
+
emit("warn", phase, event, fields);
|
|
910
|
+
},
|
|
911
|
+
fail(phase, err, fields) {
|
|
912
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
913
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
914
|
+
const apiData = err?.response?.data;
|
|
915
|
+
const apiStatus = err?.response?.status;
|
|
916
|
+
emit("error", phase, "fail", {
|
|
917
|
+
...fields,
|
|
918
|
+
err: message,
|
|
919
|
+
apiStatus,
|
|
920
|
+
apiData,
|
|
921
|
+
stack
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
function withTrace(ctx, fn) {
|
|
926
|
+
const traceId = ctx.traceId ?? newTraceId();
|
|
927
|
+
return als.run({ ...ctx, traceId }, fn);
|
|
928
|
+
}
|
|
929
|
+
function newTraceId() {
|
|
930
|
+
return Math.random().toString(36).slice(2, 10);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/bot/onboarding.ts
|
|
934
|
+
function ensureCodex() {
|
|
935
|
+
if (resolveCodexBin()) return true;
|
|
936
|
+
console.error(
|
|
937
|
+
"\u2717 \u672A\u627E\u5230 codex CLI\u3002\u8BF7\u5148\u5B89\u88C5 Codex \u5E76\u767B\u5F55\uFF1A\n \u2022 \u5B89\u88C5\uFF1Anpm i -g @openai/codex\uFF08\u6216\u5B89\u88C5 Codex.app\uFF0C\u6216\u7528 CODEX_BIN \u6307\u5411\u5DF2\u6709\u4E8C\u8FDB\u5236\uFF09\n \u2022 \u767B\u5F55\uFF1Acodex login\n \u7136\u540E\u91CD\u8DD1\u3002\u53EF\u5148\u7528 `feishu-codex-bridge doctor` \u81EA\u68C0\u3002"
|
|
938
|
+
);
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
async function ensureOnboarded(opts = {}) {
|
|
942
|
+
if (!ensureCodex()) return null;
|
|
943
|
+
const reg = await ensureRegistry();
|
|
944
|
+
const entry = currentBot(reg);
|
|
945
|
+
if (!entry) {
|
|
946
|
+
if (!opts.allowCreate) {
|
|
947
|
+
console.error("\u2717 \u5C1A\u672A\u914D\u7F6E\u4EFB\u4F55\u98DE\u4E66\u673A\u5668\u4EBA\u3002\u8BF7\u5148\u8FD0\u884C `feishu-codex-bridge bot init`\uFF08\u6216\u524D\u53F0 `run`\uFF09\u626B\u7801\u521B\u5EFA\u3002");
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
return registerNewBot("default");
|
|
951
|
+
}
|
|
952
|
+
useBotDir(entry.appId);
|
|
953
|
+
const cfg = await loadConfig();
|
|
954
|
+
if (!isComplete(cfg)) {
|
|
955
|
+
console.error(`\u2717 \u5F53\u524D\u673A\u5668\u4EBA\u300C${entry.name}\u300D(${entry.appId}) \u914D\u7F6E\u7F3A\u5931\u6216\u635F\u574F\u3002\u53EF \`bot rm ${entry.name}\` \u540E\u91CD\u65B0 \`bot init\`\u3002`);
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
const r = await validateAndReport(cfg);
|
|
959
|
+
if (r === null) return null;
|
|
960
|
+
return { cfg, secret: r.secret, missingScopes: r.missingScopes };
|
|
961
|
+
}
|
|
962
|
+
async function registerNewBot(desiredName) {
|
|
963
|
+
if (!process.stdout.isTTY) {
|
|
964
|
+
console.error(
|
|
965
|
+
"\u2717 \u5F53\u524D\u4E0D\u662F\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\uFF0C\u65E0\u6CD5\u626B\u7801\u521B\u5EFA\u98DE\u4E66\u5E94\u7528\u3002\n \u8BF7\u5728\u7EC8\u7AEF\u524D\u53F0\u8FD0\u884C `feishu-codex-bridge bot init`\uFF08\u6216 `run` / `start`\uFF09\u626B\u7801 onboarding\u3002"
|
|
966
|
+
);
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
const wizardCfg = await runRegistrationWizard();
|
|
970
|
+
const app = wizardCfg.accounts.app;
|
|
971
|
+
if (typeof app.secret !== "string") {
|
|
972
|
+
console.error("\u2717 \u5411\u5BFC\u672A\u8FD4\u56DE\u660E\u6587\u5BC6\u94A5\uFF0C\u65E0\u6CD5\u7EE7\u7EED\u3002");
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const v = await validateAppCredentials(app.id, app.secret, app.tenant);
|
|
976
|
+
if (!v.ok) {
|
|
977
|
+
console.error(`\u2717 \u5E94\u7528\u51ED\u636E\u6821\u9A8C\u5931\u8D25\uFF1A${v.reason}`);
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
await setSecret(secretKeyForApp(app.id), app.secret);
|
|
981
|
+
useBotDir(app.id);
|
|
982
|
+
const cfg = await buildEncryptedAccountConfig(app.id, app.tenant, wizardCfg.preferences);
|
|
983
|
+
await saveConfig(cfg);
|
|
984
|
+
const reg = await loadBots();
|
|
985
|
+
const name = uniqueName(reg, desiredName ?? v.botName ?? "default");
|
|
986
|
+
await addBot({ name, appId: app.id, tenant: app.tenant, botName: v.botName, createdAt: Date.now() });
|
|
987
|
+
console.log(`\u2713 \u5DF2\u521B\u5EFA\u673A\u5668\u4EBA\u300C${name}\u300D bot: ${v.botName ?? "-"} appId: ${app.id}`);
|
|
988
|
+
log.info("onboard", "bot-created", { name, appId: app.id, bot: v.botName ?? null });
|
|
989
|
+
showScopeGrant(cfg, v.missingScopes);
|
|
990
|
+
const secret = await resolveAppSecret(cfg);
|
|
991
|
+
return { cfg, secret, missingScopes: v.missingScopes };
|
|
992
|
+
}
|
|
993
|
+
async function validateAndReport(cfg) {
|
|
994
|
+
const secret = await resolveAppSecret(cfg);
|
|
995
|
+
const v = await validateAppCredentials(cfg.accounts.app.id, secret, cfg.accounts.app.tenant);
|
|
996
|
+
if (!v.ok) {
|
|
997
|
+
console.error(`\u2717 \u5E94\u7528\u51ED\u636E\u6821\u9A8C\u5931\u8D25\uFF1A${v.reason}`);
|
|
998
|
+
console.error(" \u5E94\u7528\u53EF\u80FD\u88AB\u7981\u7528/\u672A\u53D1\u5E03\uFF1B\u53EF\u91CD\u8DD1 `feishu-codex-bridge bot init` \u91CD\u65B0\u626B\u7801\u3002");
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
console.log(`\u2713 \u51ED\u636E\u6821\u9A8C\u901A\u8FC7 bot: ${v.botName ?? "-"} appId: ${cfg.accounts.app.id}`);
|
|
1002
|
+
log.info("onboard", "credentials-ok", { appId: cfg.accounts.app.id, bot: v.botName ?? null });
|
|
1003
|
+
showScopeGrant(cfg, v.missingScopes);
|
|
1004
|
+
return { secret, missingScopes: v.missingScopes };
|
|
1005
|
+
}
|
|
1006
|
+
async function confirmReadyForDaemon(result) {
|
|
1007
|
+
if (!process.stdin.isTTY) return true;
|
|
1008
|
+
const { app } = result.cfg.accounts;
|
|
1009
|
+
let missing = result.missingScopes;
|
|
1010
|
+
while (missing && missing.length > 0) {
|
|
1011
|
+
const url = buildScopeGrantUrl(app.id, app.tenant);
|
|
1012
|
+
console.log(`
|
|
1013
|
+
\u23F3 \u8FD8\u5DEE ${missing.length} \u9879\u6743\u9650\u672A\u5F00\u901A\uFF0C\u540E\u53F0\u670D\u52A1\u6682\u4E0D\u5B89\u88C5\u3002`);
|
|
1014
|
+
console.log(` \u5F00\u901A\u9875\uFF1A${url}`);
|
|
1015
|
+
await promptEnter(" \u5728\u6D4F\u89C8\u5668\u52FE\u9009\u5168\u90E8\u6743\u9650\u5E76\u786E\u8BA4\u540E\uFF0C\u6309 Enter \u91CD\u65B0\u68C0\u6D4B\uFF08Ctrl+C \u53D6\u6D88\uFF09\u2026 ");
|
|
1016
|
+
const v = await validateAppCredentials(app.id, result.secret, app.tenant);
|
|
1017
|
+
if (!v.ok) {
|
|
1018
|
+
console.error(`\u2717 \u51ED\u636E\u6821\u9A8C\u5931\u8D25\uFF1A${v.reason}`);
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
missing = v.missingScopes;
|
|
1022
|
+
if (missing && missing.length > 0) console.log(` \u4ECD\u7F3A\uFF1A${missing.join(" ")}`);
|
|
1023
|
+
}
|
|
1024
|
+
console.log("\u2713 \u6743\u9650\u5DF2\u5F00\u901A\u3002");
|
|
1025
|
+
const eventUrl = buildEventConfigUrl(app.id, app.tenant);
|
|
1026
|
+
const opened = openUrl(eventUrl);
|
|
1027
|
+
console.log("\n\u6700\u540E\u8FD9\u51E0\u6B65\u98DE\u4E66\u6CA1\u6709 API/\u6DF1\u94FE\u53EF\u4EE3\u529E\uFF08\u8FDE\u67E5\u8BE2\u8BA2\u9605\u72B6\u6001\u7684\u63A5\u53E3\u90FD\u6CA1\u6709\uFF09\uFF0C\u9700\u4F60\u624B\u52A8\u70B9\uFF1A\n");
|
|
1028
|
+
console.log(` \u30101\u3011\u4E8B\u4EF6\u4E0E\u56DE\u8C03\uFF08${opened ? "\u5DF2\u81EA\u52A8\u6253\u5F00" : "\u6253\u5F00\u4E0B\u9762\u94FE\u63A5"}\uFF09\uFF1A${eventUrl}`);
|
|
1029
|
+
console.log(" \u8FD9\u9875\u9876\u90E8\u6709\u4E09\u4E2A\u6807\u7B7E\uFF1A\u300C\u4E8B\u4EF6\u914D\u7F6E\u300D\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u300C\u52A0\u5BC6\u7B56\u7565\u300D\u3002");
|
|
1030
|
+
console.log(" \u2022 \u5207\u5230\u300C\u4E8B\u4EF6\u914D\u7F6E\u300D\u6807\u7B7E \u2192 \u300C\u8BA2\u9605\u65B9\u5F0F\u300D\u6539\u300C\u957F\u8FDE\u63A5\u300D\u2192 \u70B9\u300C\u6DFB\u52A0\u4E8B\u4EF6\u300D\u641C\u5E76\u52FE\u9009\uFF1A");
|
|
1031
|
+
console.log(" im.message.receive_v1\uFF08\u63A5\u6536\u6D88\u606F\uFF09\u3001application.bot.menu_v6\uFF08\u673A\u5668\u4EBA\u83DC\u5355\uFF09");
|
|
1032
|
+
console.log(" \u2022 \uFF08\u53EF\u9009\uFF09\u60F3\u8981\u300C\u5728\u98DE\u4E66\u6587\u6863\u8BC4\u8BBA\u91CC @\u673A\u5668\u4EBA\u5C31\u81EA\u52A8\u56DE\u590D\u300D\uFF0C\u518D\u52A0\u8FD9\u4E00\u4E2A\u4E8B\u4EF6\uFF1A");
|
|
1033
|
+
console.log(" drive.notice.comment_add_v1\uFF08\u4E91\u6587\u6863\u65B0\u589E\u8BC4\u8BBA\uFF09");
|
|
1034
|
+
console.log(" \u5B83\u4F9D\u8D56\u300C\u6587\u6863\u8BC4\u8BBA\u300D\u6743\u9650\uFF08docs:document.comment:read / :create\uFF0C\u6388\u6743\u94FE\u63A5\u5DF2\u9884\u52FE\u9009\uFF09\uFF1B\u4E0D\u52A0\u5219\u8BE5\u529F\u80FD\u9759\u9ED8\u5173\u95ED\u3002");
|
|
1035
|
+
console.log(" \u2022 \u5207\u5230\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u6807\u7B7E \u2192 \u300C\u8BA2\u9605\u65B9\u5F0F\u300D\u6539\u300C\u957F\u8FDE\u63A5\u300D\u2192 \u70B9\u300C\u6DFB\u52A0\u56DE\u8C03\u300D\u52FE\u9009\uFF1A");
|
|
1036
|
+
console.log(" card.action.trigger\uFF08\u5361\u7247\u56DE\u4F20\u4EA4\u4E92\uFF09");
|
|
1037
|
+
console.log(" \u26A0\uFE0F \u5B83\u662F\u300C\u56DE\u8C03\u300D\u4E0D\u662F\u300C\u4E8B\u4EF6\u300D\u2014\u2014\u5728\u4E0A\u9762\u300C\u6DFB\u52A0\u4E8B\u4EF6\u300D\u91CC\u641C\u4E0D\u5230\uFF0C\u5FC5\u987B\u5207\u5230\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u8FD9\u4E2A\u6807\u7B7E\u3002");
|
|
1038
|
+
console.log(" \u30102\u3011\u5DE6\u4FA7\u680F\u300C\u5E94\u7528\u53D1\u5E03 \u2192 \u7248\u672C\u7BA1\u7406\u4E0E\u53D1\u5E03\u300D\u2192 \u521B\u5EFA\u4E00\u4E2A\u7248\u672C\u5E76\u53D1\u5E03\u3002");
|
|
1039
|
+
console.log("\n \uFF08\u4FDD\u5B58\u300C\u957F\u8FDE\u63A5\u300D\u8BA2\u9605\u65B9\u5F0F\u8981\u6C42\u957F\u8FDE\u63A5\u5728\u7EBF\uFF1B\u82E5\u63D0\u793A\u8FDE\u63A5\u672A\u5EFA\u7ACB\uFF0C");
|
|
1040
|
+
console.log(" \u5148\u5F00\u53E6\u4E00\u4E2A\u7EC8\u7AEF\u8DD1 `feishu-codex-bridge run` \u628A\u6865\u8FDE\u4E0A\uFF0C\u518D\u56DE\u8FD9\u9875\u4FDD\u5B58\u3002\uFF09");
|
|
1041
|
+
await promptEnter("\n\u4EE5\u4E0A\u90FD\u70B9\u5B8C\u540E\u6309 Enter \u5B89\u88C5\u540E\u53F0\u670D\u52A1\uFF08Ctrl+C \u53D6\u6D88\uFF09\u2026 ");
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
async function promptEnter(message) {
|
|
1045
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1046
|
+
try {
|
|
1047
|
+
return await rl.question(message);
|
|
1048
|
+
} finally {
|
|
1049
|
+
rl.close();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function showScopeGrant(cfg, missingScopes) {
|
|
1053
|
+
if (missingScopes && missingScopes.length > 0) {
|
|
1054
|
+
const url = buildScopeGrantUrl(cfg.accounts.app.id, cfg.accounts.app.tenant);
|
|
1055
|
+
const rule = "\u2500".repeat(64);
|
|
1056
|
+
const opened = openUrl(url);
|
|
1057
|
+
console.log(`
|
|
1058
|
+
${rule}`);
|
|
1059
|
+
console.log(`\u26A0\uFE0F \u8FD8\u5DEE ${missingScopes.length} \u9879\u6743\u9650\u672A\u5F00\u901A \u2014\u2014 \u4E0D\u5F00\u901A\u5219\u6536\u4E0D\u5230\u6D88\u606F\u3001\u53D1\u4E0D\u51FA\u5361\u7247`);
|
|
1060
|
+
console.log(" \u98DE\u4E66\u6CA1\u6709\u300C\u626B\u7801\u5373\u6388\u6743\u300D\u7684\u63A5\u53E3\uFF0C\u53EA\u80FD\u5728\u6D4F\u89C8\u5668\u5F00\u901A\uFF08\u5373\u65F6\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\uFF09\uFF1A");
|
|
1061
|
+
console.log(
|
|
1062
|
+
opened ? "\n \u{1F310} \u5DF2\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\u6388\u6743\u9875\u3002\u82E5\u6CA1\u5F39\u51FA\uFF0C\u624B\u52A8\u590D\u5236\u4E0B\u9762\u94FE\u63A5\u6253\u5F00\uFF1A" : "\n \u{1F310} \u5728\u6D4F\u89C8\u5668\u6253\u5F00\u4E0B\u9762\u94FE\u63A5\uFF0C\u52FE\u9009\u5168\u90E8\u6743\u9650 \u2192 \u786E\u8BA4\uFF1A"
|
|
1063
|
+
);
|
|
1064
|
+
console.log(`
|
|
1065
|
+
\u{1F449} ${url}
|
|
1066
|
+
`);
|
|
1067
|
+
console.log(` \uFF08\u672C\u6B21\u7F3A\u5931\uFF1A${missingScopes.join(" ")}\uFF09`);
|
|
1068
|
+
console.log(`${rule}
|
|
1069
|
+
`);
|
|
1070
|
+
} else if (missingScopes === void 0) {
|
|
1071
|
+
log.info("onboard", "scope-check-skipped", { reason: "scope list unavailable" });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/bot/bridge.ts
|
|
1076
|
+
import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
|
|
1077
|
+
|
|
1078
|
+
// src/agent/codex-appserver/app-server-client.ts
|
|
1079
|
+
import { spawn as spawn3 } from "child_process";
|
|
1080
|
+
var AsyncQueue = class {
|
|
1081
|
+
items = [];
|
|
1082
|
+
waiters = [];
|
|
1083
|
+
closed = false;
|
|
1084
|
+
push(item) {
|
|
1085
|
+
const w = this.waiters.shift();
|
|
1086
|
+
if (w) w({ value: item, done: false });
|
|
1087
|
+
else this.items.push(item);
|
|
1088
|
+
}
|
|
1089
|
+
close() {
|
|
1090
|
+
this.closed = true;
|
|
1091
|
+
while (this.waiters.length) this.waiters.shift()({ value: void 0, done: true });
|
|
1092
|
+
}
|
|
1093
|
+
async *[Symbol.asyncIterator]() {
|
|
1094
|
+
while (true) {
|
|
1095
|
+
if (this.items.length) {
|
|
1096
|
+
yield this.items.shift();
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (this.closed) return;
|
|
1100
|
+
const next = await new Promise((resolve3) => this.waiters.push(resolve3));
|
|
1101
|
+
if (next.done) return;
|
|
1102
|
+
yield next.value;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
var AppServerClient = class {
|
|
1107
|
+
constructor(opts) {
|
|
1108
|
+
this.opts = opts;
|
|
1109
|
+
}
|
|
1110
|
+
opts;
|
|
1111
|
+
child = null;
|
|
1112
|
+
buf = "";
|
|
1113
|
+
nextId = 0;
|
|
1114
|
+
pending = /* @__PURE__ */ new Map();
|
|
1115
|
+
notifications = new AsyncQueue();
|
|
1116
|
+
closed = false;
|
|
1117
|
+
get pid() {
|
|
1118
|
+
return this.child?.pid;
|
|
1119
|
+
}
|
|
1120
|
+
/** spawn + initialize handshake. Throws if spawn/handshake fails. */
|
|
1121
|
+
async connect() {
|
|
1122
|
+
const child = spawn3(this.opts.bin, ["app-server", "--listen", "stdio://"], {
|
|
1123
|
+
cwd: this.opts.cwd,
|
|
1124
|
+
env: { ...process.env, ...this.opts.env, FEISHU_CODEX_BRIDGE: "1" },
|
|
1125
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1126
|
+
});
|
|
1127
|
+
this.child = child;
|
|
1128
|
+
log.info("agent", "spawn", { pid: child.pid ?? null, cwd: this.opts.cwd });
|
|
1129
|
+
child.stdout.on("data", (d) => this.onStdout(d));
|
|
1130
|
+
child.stderr.on("data", (d) => {
|
|
1131
|
+
const line = d.toString("utf8").trim();
|
|
1132
|
+
if (line) log.warn("agent", "stderr", { line: line.slice(0, 200) });
|
|
1133
|
+
});
|
|
1134
|
+
child.on("exit", (code, signal) => {
|
|
1135
|
+
log.info("agent", "exit", { pid: child.pid ?? null, code, signal });
|
|
1136
|
+
this.failAllPending(new Error(`app-server exited (code=${code} signal=${signal})`));
|
|
1137
|
+
this.notifications.close();
|
|
1138
|
+
});
|
|
1139
|
+
child.on("error", (err) => this.failAllPending(err));
|
|
1140
|
+
await this.request("initialize", {
|
|
1141
|
+
clientInfo: { name: this.opts.clientName ?? "feishu-codex-bridge", version: "0.0.1" },
|
|
1142
|
+
capabilities: null
|
|
1143
|
+
});
|
|
1144
|
+
this.notify("initialized");
|
|
1145
|
+
}
|
|
1146
|
+
request(method, params) {
|
|
1147
|
+
if (this.closed || !this.child) return Promise.reject(new Error("app-server client closed"));
|
|
1148
|
+
const id = ++this.nextId;
|
|
1149
|
+
const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
|
|
1150
|
+
`;
|
|
1151
|
+
return new Promise((resolve3, reject) => {
|
|
1152
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
1153
|
+
this.child.stdin.write(payload, (err) => {
|
|
1154
|
+
if (err) {
|
|
1155
|
+
this.pending.delete(id);
|
|
1156
|
+
reject(err);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
notify(method, params) {
|
|
1162
|
+
if (this.closed || !this.child) return;
|
|
1163
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params: params ?? {} })}
|
|
1164
|
+
`);
|
|
1165
|
+
}
|
|
1166
|
+
/** async-iterate server notifications (closes when the process exits). */
|
|
1167
|
+
stream() {
|
|
1168
|
+
return this.notifications;
|
|
1169
|
+
}
|
|
1170
|
+
async close(graceMs = 4e3) {
|
|
1171
|
+
if (this.closed) return;
|
|
1172
|
+
this.closed = true;
|
|
1173
|
+
const child = this.child;
|
|
1174
|
+
if (!child || child.exitCode !== null) return;
|
|
1175
|
+
child.kill("SIGTERM");
|
|
1176
|
+
await new Promise((resolve3) => {
|
|
1177
|
+
const t = setTimeout(() => {
|
|
1178
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1179
|
+
resolve3();
|
|
1180
|
+
}, graceMs);
|
|
1181
|
+
child.once("exit", () => {
|
|
1182
|
+
clearTimeout(t);
|
|
1183
|
+
resolve3();
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
onStdout(d) {
|
|
1188
|
+
this.buf += d.toString("utf8");
|
|
1189
|
+
let nl;
|
|
1190
|
+
while ((nl = this.buf.indexOf("\n")) !== -1) {
|
|
1191
|
+
const line = this.buf.slice(0, nl);
|
|
1192
|
+
this.buf = this.buf.slice(nl + 1);
|
|
1193
|
+
if (!line.trim()) continue;
|
|
1194
|
+
this.handleLine(line);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
handleLine(line) {
|
|
1198
|
+
let msg;
|
|
1199
|
+
try {
|
|
1200
|
+
msg = JSON.parse(line);
|
|
1201
|
+
} catch {
|
|
1202
|
+
log.warn("agent", "nonjson", { line: line.slice(0, 120) });
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (typeof msg.id === "number" && ("result" in msg || "error" in msg) && !("method" in msg)) {
|
|
1206
|
+
const p = this.pending.get(msg.id);
|
|
1207
|
+
if (!p) return;
|
|
1208
|
+
this.pending.delete(msg.id);
|
|
1209
|
+
if ("error" in msg && msg.error) {
|
|
1210
|
+
const e = msg.error;
|
|
1211
|
+
p.reject(new Error(e.message ?? "JSON-RPC error"));
|
|
1212
|
+
} else {
|
|
1213
|
+
p.resolve(msg.result);
|
|
1214
|
+
}
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (typeof msg.id === "number" && typeof msg.method === "string") {
|
|
1218
|
+
this.child?.stdin.write(
|
|
1219
|
+
`${JSON.stringify({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "not handled" } })}
|
|
1220
|
+
`
|
|
1221
|
+
);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof msg.method === "string") {
|
|
1225
|
+
this.notifications.push(msg);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
failAllPending(err) {
|
|
1229
|
+
for (const p of this.pending.values()) p.reject(err);
|
|
1230
|
+
this.pending.clear();
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
// src/agent/codex-appserver/event-map.ts
|
|
1235
|
+
function mapNotification(n) {
|
|
1236
|
+
switch (n.method) {
|
|
1237
|
+
case "thread/started":
|
|
1238
|
+
return { type: "system", threadId: n.params.thread.id };
|
|
1239
|
+
case "turn/started":
|
|
1240
|
+
return { type: "turn_started", turnId: n.params.turn.id };
|
|
1241
|
+
case "item/agentMessage/delta":
|
|
1242
|
+
return { type: "text_delta", itemId: n.params.itemId, delta: n.params.delta };
|
|
1243
|
+
case "item/reasoning/textDelta":
|
|
1244
|
+
return { type: "thinking_delta", itemId: n.params.itemId, delta: n.params.delta };
|
|
1245
|
+
case "item/started":
|
|
1246
|
+
return mapItemStart(n.params.item);
|
|
1247
|
+
case "item/completed":
|
|
1248
|
+
return mapItemComplete(n.params.item);
|
|
1249
|
+
case "turn/completed":
|
|
1250
|
+
return { type: "done", turnId: n.params.turn.id };
|
|
1251
|
+
case "error":
|
|
1252
|
+
return { type: "error", message: n.params.error.message, willRetry: n.params.willRetry };
|
|
1253
|
+
default:
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function mapItemStart(item) {
|
|
1258
|
+
switch (item.type) {
|
|
1259
|
+
case "commandExecution":
|
|
1260
|
+
return { type: "tool_use", itemId: item.id, title: item.command, detail: String(item.cwd) };
|
|
1261
|
+
case "fileChange":
|
|
1262
|
+
return { type: "tool_use", itemId: item.id, title: "\u7F16\u8F91\u6587\u4EF6" };
|
|
1263
|
+
case "webSearch":
|
|
1264
|
+
return { type: "tool_use", itemId: item.id, title: "\u8054\u7F51\u641C\u7D22" };
|
|
1265
|
+
case "mcpToolCall":
|
|
1266
|
+
case "dynamicToolCall":
|
|
1267
|
+
return { type: "tool_use", itemId: item.id, title: "\u5DE5\u5177\u8C03\u7528" };
|
|
1268
|
+
default:
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function mapItemComplete(item) {
|
|
1273
|
+
switch (item.type) {
|
|
1274
|
+
case "agentMessage":
|
|
1275
|
+
return { type: "text", itemId: item.id, text: item.text };
|
|
1276
|
+
case "reasoning": {
|
|
1277
|
+
const text = item.content.length ? item.content.join("\n") : item.summary.join("\n");
|
|
1278
|
+
return { type: "thinking", itemId: item.id, text };
|
|
1279
|
+
}
|
|
1280
|
+
case "commandExecution":
|
|
1281
|
+
return {
|
|
1282
|
+
type: "tool_result",
|
|
1283
|
+
itemId: item.id,
|
|
1284
|
+
output: item.aggregatedOutput ?? void 0,
|
|
1285
|
+
exitCode: item.exitCode
|
|
1286
|
+
};
|
|
1287
|
+
case "fileChange":
|
|
1288
|
+
case "webSearch":
|
|
1289
|
+
case "mcpToolCall":
|
|
1290
|
+
case "dynamicToolCall":
|
|
1291
|
+
return { type: "tool_result", itemId: item.id };
|
|
1292
|
+
default:
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/agent/codex-appserver/backend.ts
|
|
1298
|
+
var APPROVAL_POLICY = "never";
|
|
1299
|
+
var SANDBOX = "danger-full-access";
|
|
1300
|
+
var READ_HISTORY_TIMEOUT_MS = 2e4;
|
|
1301
|
+
function withDeadline(p, ms, label) {
|
|
1302
|
+
return new Promise((resolve3, reject) => {
|
|
1303
|
+
const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
1304
|
+
p.then(
|
|
1305
|
+
(v) => {
|
|
1306
|
+
clearTimeout(t);
|
|
1307
|
+
resolve3(v);
|
|
1308
|
+
},
|
|
1309
|
+
(e) => {
|
|
1310
|
+
clearTimeout(t);
|
|
1311
|
+
reject(e);
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
function toUserInput(input2) {
|
|
1317
|
+
const out = [];
|
|
1318
|
+
if (input2.text) out.push({ type: "text", text: input2.text, text_elements: [] });
|
|
1319
|
+
for (const path of input2.images ?? []) out.push({ type: "localImage", path });
|
|
1320
|
+
return out;
|
|
1321
|
+
}
|
|
1322
|
+
var CodexThread = class {
|
|
1323
|
+
constructor(client, codexThreadId, model, effort) {
|
|
1324
|
+
this.client = client;
|
|
1325
|
+
this.codexThreadId = codexThreadId;
|
|
1326
|
+
this.model = model;
|
|
1327
|
+
this.effort = effort;
|
|
1328
|
+
}
|
|
1329
|
+
client;
|
|
1330
|
+
codexThreadId;
|
|
1331
|
+
model;
|
|
1332
|
+
effort;
|
|
1333
|
+
currentTurnId;
|
|
1334
|
+
runStreamed(input2, turn) {
|
|
1335
|
+
const self = this;
|
|
1336
|
+
this.currentTurnId = void 0;
|
|
1337
|
+
if (turn?.model) this.model = turn.model;
|
|
1338
|
+
if (turn?.effort) this.effort = turn.effort;
|
|
1339
|
+
async function* gen() {
|
|
1340
|
+
const params = {
|
|
1341
|
+
threadId: self.codexThreadId,
|
|
1342
|
+
input: toUserInput(input2)
|
|
1343
|
+
};
|
|
1344
|
+
if (self.model) params.model = self.model;
|
|
1345
|
+
if (self.effort) params.effort = self.effort;
|
|
1346
|
+
let startError;
|
|
1347
|
+
const startFailed = new Promise((resolve3) => {
|
|
1348
|
+
self.client.request("turn/start", params).then(void 0, (err) => {
|
|
1349
|
+
startError = err instanceof Error ? err : new Error(String(err));
|
|
1350
|
+
log.fail("agent", startError, { phase: "turn/start" });
|
|
1351
|
+
resolve3("start-failed");
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
const stream2 = self.client.stream()[Symbol.asyncIterator]();
|
|
1355
|
+
while (true) {
|
|
1356
|
+
const step = await Promise.race([stream2.next(), startFailed]);
|
|
1357
|
+
if (step === "start-failed") {
|
|
1358
|
+
yield { type: "error", message: startError?.message ?? "turn/start \u8BF7\u6C42\u5931\u8D25", willRetry: false };
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (step.done) return;
|
|
1362
|
+
const ev = mapNotification(step.value);
|
|
1363
|
+
if (!ev) continue;
|
|
1364
|
+
if (ev.type === "turn_started") self.currentTurnId = ev.turnId;
|
|
1365
|
+
yield ev;
|
|
1366
|
+
if (ev.type === "done") return;
|
|
1367
|
+
if (ev.type === "error" && !ev.willRetry) return;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return { events: gen(), turnId: () => self.currentTurnId };
|
|
1371
|
+
}
|
|
1372
|
+
async steer(input2, expectedTurnId) {
|
|
1373
|
+
await this.client.request("turn/steer", {
|
|
1374
|
+
threadId: this.codexThreadId,
|
|
1375
|
+
expectedTurnId,
|
|
1376
|
+
input: toUserInput(input2)
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
async abort(turnId) {
|
|
1380
|
+
await this.client.request("turn/interrupt", { threadId: this.codexThreadId, turnId });
|
|
1381
|
+
}
|
|
1382
|
+
async close() {
|
|
1383
|
+
await this.client.close();
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
var CodexAppServerBackend = class {
|
|
1387
|
+
id = "codex-appserver";
|
|
1388
|
+
displayName = "Codex (app-server)";
|
|
1389
|
+
modelCache = null;
|
|
1390
|
+
async isAvailable() {
|
|
1391
|
+
const bin = resolveCodexBin();
|
|
1392
|
+
return bin !== null && codexVersion(bin) !== null;
|
|
1393
|
+
}
|
|
1394
|
+
async listModels() {
|
|
1395
|
+
if (this.modelCache) return this.modelCache;
|
|
1396
|
+
const bin = resolveCodexBin();
|
|
1397
|
+
if (!bin) return STATIC_MODELS;
|
|
1398
|
+
const client = new AppServerClient({ bin, cwd: process.cwd(), clientName: "feishu-codex-bridge-models" });
|
|
1399
|
+
try {
|
|
1400
|
+
await client.connect();
|
|
1401
|
+
const res = await client.request("model/list", { limit: 50 });
|
|
1402
|
+
const models = (res.data ?? []).map(mapModel);
|
|
1403
|
+
this.modelCache = models.length ? models : STATIC_MODELS;
|
|
1404
|
+
return this.modelCache;
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
log.fail("agent", err, { phase: "model/list" });
|
|
1407
|
+
return STATIC_MODELS;
|
|
1408
|
+
} finally {
|
|
1409
|
+
await client.close();
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async listThreads(cwd, limit = 15) {
|
|
1413
|
+
const bin = resolveCodexBin();
|
|
1414
|
+
if (!bin) return [];
|
|
1415
|
+
const client = new AppServerClient({ bin, cwd, clientName: "feishu-codex-bridge-threads" });
|
|
1416
|
+
try {
|
|
1417
|
+
await client.connect();
|
|
1418
|
+
const res = await client.request("thread/list", {
|
|
1419
|
+
cwd,
|
|
1420
|
+
limit,
|
|
1421
|
+
sortKey: "created_at",
|
|
1422
|
+
sortDirection: "desc"
|
|
1423
|
+
});
|
|
1424
|
+
return (res.data ?? []).filter((t) => !t.ephemeral).map((t) => ({
|
|
1425
|
+
codexThreadId: t.id,
|
|
1426
|
+
preview: t.preview ?? "",
|
|
1427
|
+
createdAt: t.createdAt ?? 0,
|
|
1428
|
+
updatedAt: t.updatedAt ?? t.createdAt ?? 0,
|
|
1429
|
+
name: t.name ?? void 0
|
|
1430
|
+
}));
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
log.fail("agent", err, { phase: "thread/list" });
|
|
1433
|
+
return [];
|
|
1434
|
+
} finally {
|
|
1435
|
+
await client.close();
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async readHistory(cwd, codexThreadId, maxTurns = 10) {
|
|
1439
|
+
const empty = { turns: [], totalTurns: 0 };
|
|
1440
|
+
const bin = resolveCodexBin();
|
|
1441
|
+
if (!bin) return empty;
|
|
1442
|
+
const client = new AppServerClient({ bin, cwd, clientName: "feishu-codex-bridge-history" });
|
|
1443
|
+
try {
|
|
1444
|
+
const read3 = (async () => {
|
|
1445
|
+
await client.connect();
|
|
1446
|
+
return client.request("thread/read", { threadId: codexThreadId, includeTurns: true });
|
|
1447
|
+
})();
|
|
1448
|
+
read3.catch(() => void 0);
|
|
1449
|
+
const res = await withDeadline(read3, READ_HISTORY_TIMEOUT_MS, "thread/read");
|
|
1450
|
+
const thread = res.thread;
|
|
1451
|
+
const all = (Array.isArray(thread?.turns) ? thread.turns : []).map(mapTurn).filter((t) => t.userText || t.assistantText || t.tools.length);
|
|
1452
|
+
const totalTurns = all.length;
|
|
1453
|
+
const turns = totalTurns > maxTurns ? all.slice(totalTurns - maxTurns) : all;
|
|
1454
|
+
return {
|
|
1455
|
+
turns,
|
|
1456
|
+
totalTurns,
|
|
1457
|
+
name: thread?.name ?? void 0,
|
|
1458
|
+
preview: thread?.preview ?? void 0,
|
|
1459
|
+
createdAt: thread?.createdAt,
|
|
1460
|
+
updatedAt: thread?.updatedAt
|
|
1461
|
+
};
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
log.fail("agent", err, { phase: "thread/read", codexThreadId });
|
|
1464
|
+
return empty;
|
|
1465
|
+
} finally {
|
|
1466
|
+
await client.close();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async startThread(opts) {
|
|
1470
|
+
const client = await this.spawn(opts.cwd);
|
|
1471
|
+
const res = await client.request("thread/start", {
|
|
1472
|
+
cwd: opts.cwd,
|
|
1473
|
+
approvalPolicy: APPROVAL_POLICY,
|
|
1474
|
+
sandbox: SANDBOX,
|
|
1475
|
+
...opts.model ? { model: opts.model } : {}
|
|
1476
|
+
});
|
|
1477
|
+
return new CodexThread(client, res.thread.id, opts.model, opts.effort);
|
|
1478
|
+
}
|
|
1479
|
+
async resumeThread(opts) {
|
|
1480
|
+
const client = await this.spawn(opts.cwd);
|
|
1481
|
+
const res = await client.request("thread/resume", {
|
|
1482
|
+
threadId: opts.codexThreadId,
|
|
1483
|
+
cwd: opts.cwd,
|
|
1484
|
+
approvalPolicy: APPROVAL_POLICY,
|
|
1485
|
+
sandbox: SANDBOX,
|
|
1486
|
+
...opts.model ? { model: opts.model } : {}
|
|
1487
|
+
});
|
|
1488
|
+
return new CodexThread(client, res.thread.id, opts.model, opts.effort);
|
|
1489
|
+
}
|
|
1490
|
+
async spawn(cwd) {
|
|
1491
|
+
const bin = resolveCodexBin();
|
|
1492
|
+
if (!bin) throw new Error("codex CLI not found (set CODEX_BIN or install @openai/codex)");
|
|
1493
|
+
const client = new AppServerClient({ bin, cwd });
|
|
1494
|
+
await client.connect();
|
|
1495
|
+
return client;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
function isBoilerplateUserText(text) {
|
|
1499
|
+
const t = text.trimStart();
|
|
1500
|
+
return t.startsWith("<environment_context>") || t.startsWith("# AGENTS.md instructions");
|
|
1501
|
+
}
|
|
1502
|
+
function mapTurn(turn) {
|
|
1503
|
+
const userParts = [];
|
|
1504
|
+
const assistantParts = [];
|
|
1505
|
+
const reasoningParts = [];
|
|
1506
|
+
const tools = [];
|
|
1507
|
+
for (const item of turn.items ?? []) {
|
|
1508
|
+
switch (item.type) {
|
|
1509
|
+
case "userMessage": {
|
|
1510
|
+
const text = item.content.map((c) => c.type === "text" ? c.text : c.type === "mention" ? `@${c.name}` : "").join("").trim();
|
|
1511
|
+
if (text && !isBoilerplateUserText(text)) userParts.push(text);
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
case "agentMessage":
|
|
1515
|
+
if (item.text.trim()) assistantParts.push(item.text);
|
|
1516
|
+
break;
|
|
1517
|
+
case "reasoning": {
|
|
1518
|
+
const r = (item.content.length ? item.content : item.summary).join("\n").trim();
|
|
1519
|
+
if (r) reasoningParts.push(r);
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
case "commandExecution":
|
|
1523
|
+
tools.push({
|
|
1524
|
+
title: item.command,
|
|
1525
|
+
output: item.aggregatedOutput ?? void 0,
|
|
1526
|
+
exitCode: item.exitCode,
|
|
1527
|
+
failed: item.status === "failed" || item.status === "declined" || (item.exitCode ?? 0) !== 0
|
|
1528
|
+
});
|
|
1529
|
+
break;
|
|
1530
|
+
case "fileChange":
|
|
1531
|
+
tools.push({ title: "\u7F16\u8F91\u6587\u4EF6", failed: item.status === "failed" || item.status === "declined" });
|
|
1532
|
+
break;
|
|
1533
|
+
case "webSearch":
|
|
1534
|
+
tools.push({ title: `\u8054\u7F51\u641C\u7D22\uFF1A${item.query}` });
|
|
1535
|
+
break;
|
|
1536
|
+
case "mcpToolCall":
|
|
1537
|
+
tools.push({ title: `${item.server} / ${item.tool}`, failed: item.status === "failed" || Boolean(item.error) });
|
|
1538
|
+
break;
|
|
1539
|
+
case "dynamicToolCall":
|
|
1540
|
+
tools.push({ title: item.tool, failed: item.status === "failed" || item.success === false });
|
|
1541
|
+
break;
|
|
1542
|
+
// plan / contextCompaction / review-mode / image* — omitted from the digest
|
|
1543
|
+
default:
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
userText: userParts.join("\n\n"),
|
|
1549
|
+
assistantText: assistantParts.join("\n\n"),
|
|
1550
|
+
reasoning: reasoningParts.join("\n\n"),
|
|
1551
|
+
tools,
|
|
1552
|
+
startedAt: turn.startedAt ?? void 0
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function mapModel(m) {
|
|
1556
|
+
return {
|
|
1557
|
+
id: m.id,
|
|
1558
|
+
displayName: m.displayName ?? m.id,
|
|
1559
|
+
description: m.description ?? "",
|
|
1560
|
+
hidden: m.hidden ?? false,
|
|
1561
|
+
isDefault: m.isDefault ?? false,
|
|
1562
|
+
supportedEfforts: (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort),
|
|
1563
|
+
defaultEffort: m.defaultReasoningEffort ?? "medium"
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
var STATIC_MODELS = [
|
|
1567
|
+
{
|
|
1568
|
+
id: "gpt-5.5",
|
|
1569
|
+
displayName: "GPT-5.5",
|
|
1570
|
+
description: "\u9ED8\u8BA4\u6A21\u578B",
|
|
1571
|
+
hidden: false,
|
|
1572
|
+
isDefault: true,
|
|
1573
|
+
supportedEfforts: ["low", "medium", "high"],
|
|
1574
|
+
defaultEffort: "medium"
|
|
1575
|
+
}
|
|
1576
|
+
];
|
|
1577
|
+
|
|
1578
|
+
// src/agent/index.ts
|
|
1579
|
+
function createBackend() {
|
|
1580
|
+
return new CodexAppServerBackend();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/card/dispatcher.ts
|
|
1584
|
+
var CardDispatcher = class {
|
|
1585
|
+
constructor(channel, cfg) {
|
|
1586
|
+
this.channel = channel;
|
|
1587
|
+
this.cfg = cfg;
|
|
1588
|
+
}
|
|
1589
|
+
channel;
|
|
1590
|
+
cfg;
|
|
1591
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1592
|
+
/** Register a handler for an action id. Last registration wins. */
|
|
1593
|
+
on(actionId, handler) {
|
|
1594
|
+
this.handlers.set(actionId, handler);
|
|
1595
|
+
return this;
|
|
1596
|
+
}
|
|
1597
|
+
/** Bound handler suitable for `channel.on('cardAction', ...)`. */
|
|
1598
|
+
handle = async (evt) => {
|
|
1599
|
+
const value = evt.action?.value ?? {};
|
|
1600
|
+
const actionId = typeof value.a === "string" ? value.a : void 0;
|
|
1601
|
+
if (!actionId) {
|
|
1602
|
+
log.info("card", "action-unkeyed", { tag: evt.action?.tag });
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const handler = this.handlers.get(actionId);
|
|
1606
|
+
if (!handler) {
|
|
1607
|
+
log.info("card", "action-nohandler", { actionId });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const formValue = evt.raw?.action?.form_value;
|
|
1611
|
+
await withTrace({ chatId: evt.chatId, msgId: evt.messageId }, async () => {
|
|
1612
|
+
log.info("card", "action", { actionId, by: evt.operator?.openId?.slice(-6) });
|
|
1613
|
+
try {
|
|
1614
|
+
await handler({
|
|
1615
|
+
channel: this.channel,
|
|
1616
|
+
cfg: this.cfg,
|
|
1617
|
+
evt,
|
|
1618
|
+
actionId,
|
|
1619
|
+
option: evt.action?.option,
|
|
1620
|
+
value,
|
|
1621
|
+
formValue
|
|
1622
|
+
});
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
log.fail("card", err, { actionId });
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
};
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
// src/card/managed.ts
|
|
1631
|
+
var byMessageId = /* @__PURE__ */ new Map();
|
|
1632
|
+
var renderToken = 0;
|
|
1633
|
+
function stampRenderToken(card2) {
|
|
1634
|
+
const token = (++renderToken).toString(36);
|
|
1635
|
+
const visit = (node) => {
|
|
1636
|
+
if (Array.isArray(node)) {
|
|
1637
|
+
node.forEach(visit);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (!node || typeof node !== "object") return;
|
|
1641
|
+
const obj = node;
|
|
1642
|
+
const behaviors = obj.behaviors;
|
|
1643
|
+
if (Array.isArray(behaviors)) {
|
|
1644
|
+
for (const b of behaviors) {
|
|
1645
|
+
if (b && typeof b === "object" && b.type === "callback") {
|
|
1646
|
+
const v = b.value;
|
|
1647
|
+
if (v && typeof v === "object") v.__r = token;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
for (const k of Object.keys(obj)) visit(obj[k]);
|
|
1652
|
+
};
|
|
1653
|
+
visit(card2);
|
|
1654
|
+
}
|
|
1655
|
+
async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread = false) {
|
|
1656
|
+
stampRenderToken(card2);
|
|
1657
|
+
const created = await channel.rawClient.cardkit.v1.card.create({
|
|
1658
|
+
data: { type: "card_json", data: JSON.stringify(card2) }
|
|
1659
|
+
});
|
|
1660
|
+
const cardId = created.data?.card_id;
|
|
1661
|
+
if (!cardId) {
|
|
1662
|
+
throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
|
|
1663
|
+
}
|
|
1664
|
+
const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
|
|
1665
|
+
let messageId;
|
|
1666
|
+
if (replyTo) {
|
|
1667
|
+
const sent = await channel.rawClient.im.v1.message.reply({
|
|
1668
|
+
path: { message_id: replyTo },
|
|
1669
|
+
data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
|
|
1670
|
+
});
|
|
1671
|
+
messageId = sent.data?.message_id;
|
|
1672
|
+
} else {
|
|
1673
|
+
const sent = await channel.rawClient.im.v1.message.create({
|
|
1674
|
+
params: { receive_id_type: "chat_id" },
|
|
1675
|
+
data: { receive_id: chatId, msg_type: "interactive", content }
|
|
1676
|
+
});
|
|
1677
|
+
messageId = sent.data?.message_id;
|
|
1678
|
+
}
|
|
1679
|
+
if (!messageId) {
|
|
1680
|
+
throw new Error("send card-by-reference returned no message_id");
|
|
1681
|
+
}
|
|
1682
|
+
byMessageId.set(messageId, { cardId, sequence: 0 });
|
|
1683
|
+
return { messageId, cardId };
|
|
1684
|
+
}
|
|
1685
|
+
async function updateManagedCard(channel, messageId, card2) {
|
|
1686
|
+
const entry = byMessageId.get(messageId);
|
|
1687
|
+
if (!entry) {
|
|
1688
|
+
log.info("card", "managed-update-no-entry", { messageId, known: byMessageId.size });
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1691
|
+
stampRenderToken(card2);
|
|
1692
|
+
const data = JSON.stringify(card2);
|
|
1693
|
+
const push = async () => {
|
|
1694
|
+
entry.sequence += 1;
|
|
1695
|
+
await channel.rawClient.cardkit.v1.card.update({
|
|
1696
|
+
path: { card_id: entry.cardId },
|
|
1697
|
+
data: { card: { type: "card_json", data }, sequence: entry.sequence, uuid: `u_${entry.cardId}_${entry.sequence}` }
|
|
1698
|
+
});
|
|
1699
|
+
};
|
|
1700
|
+
try {
|
|
1701
|
+
await push();
|
|
1702
|
+
return true;
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
log.fail("card", err, { phase: "managed-update", cardId: entry.cardId, seq: entry.sequence, retry: true });
|
|
1705
|
+
await new Promise((r) => setTimeout(r, 3200));
|
|
1706
|
+
try {
|
|
1707
|
+
await push();
|
|
1708
|
+
return true;
|
|
1709
|
+
} catch (err2) {
|
|
1710
|
+
log.fail("card", err2, { phase: "managed-update-retry", cardId: entry.cardId, seq: entry.sequence });
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/card/run-state.ts
|
|
1717
|
+
var initialState = {
|
|
1718
|
+
blocks: [],
|
|
1719
|
+
reasoning: [],
|
|
1720
|
+
reasoningActive: false,
|
|
1721
|
+
footer: "thinking",
|
|
1722
|
+
terminal: "running"
|
|
1723
|
+
};
|
|
1724
|
+
function reasoningContent(state) {
|
|
1725
|
+
return state.reasoning.map((r) => r.text).filter((t) => t.trim()).join("\n\n");
|
|
1726
|
+
}
|
|
1727
|
+
function finalMessageText(state) {
|
|
1728
|
+
for (let i = state.blocks.length - 1; i >= 0; i--) {
|
|
1729
|
+
const b = state.blocks[i];
|
|
1730
|
+
if (b && b.kind === "text" && b.content.trim()) return b.content.trim();
|
|
1731
|
+
}
|
|
1732
|
+
return "";
|
|
1733
|
+
}
|
|
1734
|
+
function closeStreamingText(blocks) {
|
|
1735
|
+
return blocks.map((b) => b.kind === "text" && b.streaming ? { ...b, streaming: false } : b);
|
|
1736
|
+
}
|
|
1737
|
+
function upsertText(blocks, id, mutate) {
|
|
1738
|
+
const idx = blocks.findIndex((b) => b.kind === "text" && b.id === id);
|
|
1739
|
+
if (idx === -1) {
|
|
1740
|
+
return [...blocks, { kind: "text", id, content: mutate(""), streaming: true }];
|
|
1741
|
+
}
|
|
1742
|
+
const prev = blocks[idx];
|
|
1743
|
+
const next = { ...prev, content: mutate(prev.content) };
|
|
1744
|
+
return [...blocks.slice(0, idx), next, ...blocks.slice(idx + 1)];
|
|
1745
|
+
}
|
|
1746
|
+
function upsertReasoning(items, id, mutate) {
|
|
1747
|
+
const idx = items.findIndex((r) => r.id === id);
|
|
1748
|
+
if (idx === -1) return [...items, { id, text: mutate("") }];
|
|
1749
|
+
const prev = items[idx];
|
|
1750
|
+
const next = { id: prev.id, text: mutate(prev.text) };
|
|
1751
|
+
return [...items.slice(0, idx), next, ...items.slice(idx + 1)];
|
|
1752
|
+
}
|
|
1753
|
+
function reduce(state, evt) {
|
|
1754
|
+
switch (evt.type) {
|
|
1755
|
+
case "text_delta":
|
|
1756
|
+
return {
|
|
1757
|
+
...state,
|
|
1758
|
+
blocks: upsertText(state.blocks, evt.itemId, (prev) => prev + evt.delta),
|
|
1759
|
+
reasoningActive: false,
|
|
1760
|
+
footer: "streaming"
|
|
1761
|
+
};
|
|
1762
|
+
case "text": {
|
|
1763
|
+
const idx = state.blocks.findIndex((b) => b.kind === "text" && b.id === evt.itemId);
|
|
1764
|
+
const blocks = idx === -1 ? [...state.blocks, { kind: "text", id: evt.itemId, content: evt.text, streaming: false }] : [
|
|
1765
|
+
...state.blocks.slice(0, idx),
|
|
1766
|
+
{ kind: "text", id: evt.itemId, content: evt.text, streaming: false },
|
|
1767
|
+
...state.blocks.slice(idx + 1)
|
|
1768
|
+
];
|
|
1769
|
+
return { ...state, blocks, reasoningActive: false };
|
|
1770
|
+
}
|
|
1771
|
+
case "thinking_delta":
|
|
1772
|
+
return {
|
|
1773
|
+
...state,
|
|
1774
|
+
reasoning: upsertReasoning(state.reasoning, evt.itemId, (prev) => prev + evt.delta),
|
|
1775
|
+
reasoningActive: true,
|
|
1776
|
+
footer: state.footer === "streaming" ? state.footer : "thinking"
|
|
1777
|
+
};
|
|
1778
|
+
case "thinking":
|
|
1779
|
+
return {
|
|
1780
|
+
...state,
|
|
1781
|
+
reasoning: upsertReasoning(state.reasoning, evt.itemId, () => evt.text)
|
|
1782
|
+
};
|
|
1783
|
+
case "tool_use": {
|
|
1784
|
+
const tool = {
|
|
1785
|
+
id: evt.itemId,
|
|
1786
|
+
title: evt.title,
|
|
1787
|
+
detail: evt.detail,
|
|
1788
|
+
status: "running"
|
|
1789
|
+
};
|
|
1790
|
+
return {
|
|
1791
|
+
...state,
|
|
1792
|
+
blocks: [...closeStreamingText(state.blocks), { kind: "tool", tool }],
|
|
1793
|
+
reasoningActive: false,
|
|
1794
|
+
footer: "tool_running"
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
case "tool_result": {
|
|
1798
|
+
const isError = evt.exitCode != null && evt.exitCode !== 0;
|
|
1799
|
+
const blocks = state.blocks.map((b) => {
|
|
1800
|
+
if (b.kind !== "tool" || b.tool.id !== evt.itemId) return b;
|
|
1801
|
+
return {
|
|
1802
|
+
...b,
|
|
1803
|
+
tool: {
|
|
1804
|
+
...b.tool,
|
|
1805
|
+
status: isError ? "error" : "done",
|
|
1806
|
+
output: evt.output,
|
|
1807
|
+
exitCode: evt.exitCode
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
});
|
|
1811
|
+
return { ...state, blocks };
|
|
1812
|
+
}
|
|
1813
|
+
case "error":
|
|
1814
|
+
return { ...state, terminal: "error", errorMsg: evt.message, footer: null };
|
|
1815
|
+
case "done":
|
|
1816
|
+
return {
|
|
1817
|
+
...state,
|
|
1818
|
+
blocks: closeStreamingText(state.blocks),
|
|
1819
|
+
reasoningActive: false,
|
|
1820
|
+
terminal: "done",
|
|
1821
|
+
footer: null
|
|
1822
|
+
};
|
|
1823
|
+
default:
|
|
1824
|
+
return state;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
function markInterrupted(state) {
|
|
1828
|
+
return {
|
|
1829
|
+
...state,
|
|
1830
|
+
blocks: closeStreamingText(state.blocks),
|
|
1831
|
+
reasoningActive: false,
|
|
1832
|
+
terminal: "interrupted",
|
|
1833
|
+
footer: null
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
function markIdleTimeout(state, minutes) {
|
|
1837
|
+
return {
|
|
1838
|
+
...state,
|
|
1839
|
+
blocks: closeStreamingText(state.blocks),
|
|
1840
|
+
reasoningActive: false,
|
|
1841
|
+
terminal: "idle_timeout",
|
|
1842
|
+
footer: null,
|
|
1843
|
+
idleTimeoutMinutes: minutes
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function finalizeIfRunning(state) {
|
|
1847
|
+
if (state.terminal !== "running") return state;
|
|
1848
|
+
return {
|
|
1849
|
+
...state,
|
|
1850
|
+
blocks: closeStreamingText(state.blocks),
|
|
1851
|
+
reasoningActive: false,
|
|
1852
|
+
terminal: "done",
|
|
1853
|
+
footer: null
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/card/run-render.ts
|
|
1858
|
+
var RunRender = class {
|
|
1859
|
+
state = initialState;
|
|
1860
|
+
/** when false, tool blocks are dropped from the rendered card (pref) */
|
|
1861
|
+
showTools = true;
|
|
1862
|
+
apply(ev) {
|
|
1863
|
+
this.state = reduce(this.state, ev);
|
|
1864
|
+
}
|
|
1865
|
+
/** Current structured state for rendering. */
|
|
1866
|
+
snapshot() {
|
|
1867
|
+
return this.state;
|
|
1868
|
+
}
|
|
1869
|
+
/** Lifecycle terminal, for the run loop's status/logging. */
|
|
1870
|
+
terminal() {
|
|
1871
|
+
return this.state.terminal;
|
|
1872
|
+
}
|
|
1873
|
+
/** Mark the run as watchdog-killed (idle timeout). */
|
|
1874
|
+
timeout(minutes) {
|
|
1875
|
+
this.state = markIdleTimeout(this.state, minutes);
|
|
1876
|
+
}
|
|
1877
|
+
/** Mark the run as user-interrupted (⏹). */
|
|
1878
|
+
interrupt() {
|
|
1879
|
+
this.state = markInterrupted(this.state);
|
|
1880
|
+
}
|
|
1881
|
+
/** Force a terminal state if the stream ended without done/error. */
|
|
1882
|
+
finalize() {
|
|
1883
|
+
this.state = finalizeIfRunning(this.state);
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// src/card/cards.ts
|
|
1888
|
+
function card(elements, opts = {}) {
|
|
1889
|
+
const config = { update_multi: true };
|
|
1890
|
+
if (opts.streaming) {
|
|
1891
|
+
config.streaming_mode = true;
|
|
1892
|
+
config.streaming_config = {
|
|
1893
|
+
print_frequency_ms: { default: 70 },
|
|
1894
|
+
print_step: { default: 1 },
|
|
1895
|
+
print_strategy: "fast"
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
if (opts.summary) config.summary = { content: opts.summary };
|
|
1899
|
+
const obj = {
|
|
1900
|
+
schema: "2.0",
|
|
1901
|
+
config,
|
|
1902
|
+
body: { elements }
|
|
1903
|
+
};
|
|
1904
|
+
if (opts.header) {
|
|
1905
|
+
obj.header = {
|
|
1906
|
+
template: opts.header.template ?? "blue",
|
|
1907
|
+
title: { tag: "plain_text", content: opts.header.title },
|
|
1908
|
+
...opts.header.subtitle ? { subtitle: { tag: "plain_text", content: opts.header.subtitle } } : {}
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
return obj;
|
|
1912
|
+
}
|
|
1913
|
+
function md(content) {
|
|
1914
|
+
return { tag: "markdown", content };
|
|
1915
|
+
}
|
|
1916
|
+
function note(content) {
|
|
1917
|
+
return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: "grey" } };
|
|
1918
|
+
}
|
|
1919
|
+
function hr() {
|
|
1920
|
+
return { tag: "hr" };
|
|
1921
|
+
}
|
|
1922
|
+
function noteMd(content) {
|
|
1923
|
+
return { tag: "markdown", content, text_size: "notation" };
|
|
1924
|
+
}
|
|
1925
|
+
function collapsiblePanel(opts) {
|
|
1926
|
+
return {
|
|
1927
|
+
tag: "collapsible_panel",
|
|
1928
|
+
expanded: opts.expanded,
|
|
1929
|
+
header: {
|
|
1930
|
+
title: { tag: "markdown", content: opts.title },
|
|
1931
|
+
vertical_align: "center",
|
|
1932
|
+
icon: { tag: "standard_icon", token: "down-small-ccm_outlined", size: "16px 16px" },
|
|
1933
|
+
icon_position: "follow_text",
|
|
1934
|
+
icon_expanded_angle: -180
|
|
1935
|
+
},
|
|
1936
|
+
border: { color: opts.border, corner_radius: "5px" },
|
|
1937
|
+
vertical_spacing: "8px",
|
|
1938
|
+
padding: "8px 8px 8px 8px",
|
|
1939
|
+
elements: [{ tag: "markdown", content: opts.body, text_size: "notation" }]
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function collapsiblePanelEl(opts) {
|
|
1943
|
+
return {
|
|
1944
|
+
tag: "collapsible_panel",
|
|
1945
|
+
expanded: opts.expanded,
|
|
1946
|
+
header: {
|
|
1947
|
+
title: { tag: "markdown", content: opts.title },
|
|
1948
|
+
vertical_align: "center",
|
|
1949
|
+
icon: { tag: "standard_icon", token: "down-small-ccm_outlined", size: "16px 16px" },
|
|
1950
|
+
icon_position: "follow_text",
|
|
1951
|
+
icon_expanded_angle: -180
|
|
1952
|
+
},
|
|
1953
|
+
border: { color: opts.border, corner_radius: "5px" },
|
|
1954
|
+
vertical_spacing: "8px",
|
|
1955
|
+
padding: "8px 8px 8px 8px",
|
|
1956
|
+
elements: opts.elements
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function actions(items) {
|
|
1960
|
+
return {
|
|
1961
|
+
tag: "column_set",
|
|
1962
|
+
flex_mode: "flow",
|
|
1963
|
+
horizontal_spacing: "small",
|
|
1964
|
+
columns: items.map((it) => ({ tag: "column", width: "auto", elements: [it] }))
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
function button(label, value, type = "default") {
|
|
1968
|
+
return {
|
|
1969
|
+
tag: "button",
|
|
1970
|
+
text: { tag: "plain_text", content: label },
|
|
1971
|
+
type,
|
|
1972
|
+
behaviors: [{ type: "callback", value }]
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function linkButton(label, url, type = "default") {
|
|
1976
|
+
return {
|
|
1977
|
+
tag: "button",
|
|
1978
|
+
text: { tag: "plain_text", content: label },
|
|
1979
|
+
type,
|
|
1980
|
+
behaviors: [{ type: "open_url", default_url: url }]
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function input(opts) {
|
|
1984
|
+
return {
|
|
1985
|
+
tag: "input",
|
|
1986
|
+
name: opts.name,
|
|
1987
|
+
...opts.label ? { label: { tag: "plain_text", content: opts.label } } : {},
|
|
1988
|
+
...opts.placeholder ? { placeholder: { tag: "plain_text", content: opts.placeholder } } : {},
|
|
1989
|
+
...opts.value ? { default_value: opts.value } : {},
|
|
1990
|
+
required: Boolean(opts.required)
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function form(name, elements) {
|
|
1994
|
+
return { tag: "form", name, elements };
|
|
1995
|
+
}
|
|
1996
|
+
function submitButton(label, value, type = "primary", name = "submit") {
|
|
1997
|
+
return {
|
|
1998
|
+
tag: "button",
|
|
1999
|
+
name,
|
|
2000
|
+
text: { tag: "plain_text", content: label },
|
|
2001
|
+
type,
|
|
2002
|
+
form_action_type: "submit",
|
|
2003
|
+
behaviors: [{ type: "callback", value }]
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
function selectStatic(opts) {
|
|
2007
|
+
return {
|
|
2008
|
+
tag: "select_static",
|
|
2009
|
+
placeholder: { tag: "plain_text", content: opts.placeholder },
|
|
2010
|
+
...opts.initial ? { initial_option: opts.initial } : {},
|
|
2011
|
+
options: opts.options.map((o) => ({
|
|
2012
|
+
text: { tag: "plain_text", content: o.label },
|
|
2013
|
+
value: o.value
|
|
2014
|
+
})),
|
|
2015
|
+
behaviors: [{ type: "callback", value: { a: opts.actionId } }]
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/card/command-cards.ts
|
|
2020
|
+
var MC = {
|
|
2021
|
+
model: "model.set",
|
|
2022
|
+
effort: "model.effort"
|
|
2023
|
+
};
|
|
2024
|
+
var RES = {
|
|
2025
|
+
pick: "resume.pick"
|
|
2026
|
+
};
|
|
2027
|
+
var EFFORT_LABEL = {
|
|
2028
|
+
none: "\u65E0",
|
|
2029
|
+
minimal: "\u6781\u7B80",
|
|
2030
|
+
low: "\u4F4E",
|
|
2031
|
+
medium: "\u4E2D",
|
|
2032
|
+
high: "\u9AD8",
|
|
2033
|
+
xhigh: "\u6781\u9AD8"
|
|
2034
|
+
};
|
|
2035
|
+
function buildModelCard(state) {
|
|
2036
|
+
const visible = state.models.filter((m) => !m.hidden);
|
|
2037
|
+
const cur = state.models.find((m) => m.id === state.model);
|
|
2038
|
+
const efforts = cur?.supportedEfforts.length ? cur.supportedEfforts : ["low", "medium", "high"];
|
|
2039
|
+
const elements = [
|
|
2040
|
+
md("\u{1F9E0} **\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6**"),
|
|
2041
|
+
note("\u9009\u62E9\u540E\u4E0B\u4E00\u8F6E\u751F\u6548"),
|
|
2042
|
+
hr(),
|
|
2043
|
+
actions([
|
|
2044
|
+
selectStatic({
|
|
2045
|
+
actionId: MC.model,
|
|
2046
|
+
placeholder: "\u9009\u62E9\u6A21\u578B",
|
|
2047
|
+
initial: state.model,
|
|
2048
|
+
options: visible.map((m) => ({ label: m.displayName, value: m.id }))
|
|
2049
|
+
}),
|
|
2050
|
+
selectStatic({
|
|
2051
|
+
actionId: MC.effort,
|
|
2052
|
+
placeholder: "effort",
|
|
2053
|
+
initial: state.effort,
|
|
2054
|
+
options: efforts.map((e) => ({ label: `effort\uFF1A${EFFORT_LABEL[e]}`, value: e }))
|
|
2055
|
+
})
|
|
2056
|
+
])
|
|
2057
|
+
];
|
|
2058
|
+
if (state.note) elements.push(note(state.note));
|
|
2059
|
+
return card(elements, { summary: "\u6A21\u578B\u8BBE\u7F6E" });
|
|
2060
|
+
}
|
|
2061
|
+
var RESUME_TITLE_MAX = 30;
|
|
2062
|
+
function buildResumeCard(state) {
|
|
2063
|
+
const elements = [md("\u{1F558} **\u6062\u590D\u5386\u53F2\u4F1A\u8BDD**"), note(metaNote(state)), hr()];
|
|
2064
|
+
if (state.threads.length === 0) {
|
|
2065
|
+
elements.push(md("_\u8BE5\u76EE\u5F55\u4E0B\u8FD8\u6CA1\u6709\u5386\u53F2\u4F1A\u8BDD\u3002\u76F4\u63A5 @\u6211 \u5373\u53EF\u65B0\u5EFA\u3002_"));
|
|
2066
|
+
} else {
|
|
2067
|
+
elements.push(note("\u70B9\u4E00\u6761\u5373\u6062\u590D \u2014\u2014 \u5728\u65B0\u8BDD\u9898\u91CC\u6253\u5F00\u5386\u53F2\u3001\u53EF\u76F4\u63A5\u7EE7\u7EED\u3002"));
|
|
2068
|
+
for (const t of state.threads) {
|
|
2069
|
+
const title = (t.name?.trim() || t.preview.trim() || "(\u65E0\u6458\u8981)").replace(/\s+/g, " ");
|
|
2070
|
+
const label = `\u21A9\uFE0F ${pickerTime(t.updatedAt || t.createdAt)} \xB7 ${truncate(title, RESUME_TITLE_MAX)}`;
|
|
2071
|
+
elements.push(actions([button(label, { a: RES.pick, t: t.codexThreadId })]));
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return card(elements, { summary: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD" });
|
|
2075
|
+
}
|
|
2076
|
+
function buildResumeLaunchingCard(state) {
|
|
2077
|
+
return card([md("\u23F3 \u6B63\u5728\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u2026"), note(metaNote(state))], { summary: "\u6062\u590D\u4E2D" });
|
|
2078
|
+
}
|
|
2079
|
+
function buildResumeDoneCard(state) {
|
|
2080
|
+
return card([md("\u2705 \u5DF2\u6062\u590D \u2014\u2014 \u5DF2\u5728\u4E0A\u65B9\u65B0\u8BDD\u9898\u6253\u5F00\uFF0C\u53EF\u76F4\u63A5\u7EE7\u7EED\u3002"), note(metaNote(state))], { summary: "\u5DF2\u6062\u590D" });
|
|
2081
|
+
}
|
|
2082
|
+
function buildResumeErrorCard(state, message) {
|
|
2083
|
+
return card([md(`\u274C \u6062\u590D\u5931\u8D25\uFF1A${truncate(message, 200)}`), note(metaNote(state))], { summary: "\u6062\u590D\u5931\u8D25" });
|
|
2084
|
+
}
|
|
2085
|
+
function metaNote(state) {
|
|
2086
|
+
const parts = [`\u{1F4C2} \`${state.cwd}\``];
|
|
2087
|
+
if (state.projectName) parts.unshift(`\u{1F4C1} ${state.projectName}`);
|
|
2088
|
+
return parts.join(" ");
|
|
2089
|
+
}
|
|
2090
|
+
function truncate(s, n) {
|
|
2091
|
+
const t = s.trim();
|
|
2092
|
+
return t.length > n ? `${t.slice(0, n)}\u2026` : t;
|
|
2093
|
+
}
|
|
2094
|
+
function relativeTime(unixSeconds) {
|
|
2095
|
+
if (!unixSeconds) return "\u672A\u77E5\u65F6\u95F4";
|
|
2096
|
+
const ms = unixSeconds < 1e12 ? unixSeconds * 1e3 : unixSeconds;
|
|
2097
|
+
const diff = Date.now() - ms;
|
|
2098
|
+
const min = Math.floor(diff / 6e4);
|
|
2099
|
+
if (min < 1) return "\u521A\u521A";
|
|
2100
|
+
if (min < 60) return `${min} \u5206\u949F\u524D`;
|
|
2101
|
+
const hr2 = Math.floor(min / 60);
|
|
2102
|
+
if (hr2 < 24) return `${hr2} \u5C0F\u65F6\u524D`;
|
|
2103
|
+
const day = Math.floor(hr2 / 24);
|
|
2104
|
+
if (day < 30) return `${day} \u5929\u524D`;
|
|
2105
|
+
return new Date(ms).toLocaleDateString("zh-CN");
|
|
2106
|
+
}
|
|
2107
|
+
function pickerTime(unixSeconds) {
|
|
2108
|
+
if (!unixSeconds) return "\u672A\u77E5\u65F6\u95F4";
|
|
2109
|
+
const ms = unixSeconds < 1e12 ? unixSeconds * 1e3 : unixSeconds;
|
|
2110
|
+
const min = Math.floor((Date.now() - ms) / 6e4);
|
|
2111
|
+
if (min < 1) return "\u521A\u521A";
|
|
2112
|
+
if (min < 60) return `${min}\u5206\u949F\u524D`;
|
|
2113
|
+
const d = new Date(ms);
|
|
2114
|
+
const now = /* @__PURE__ */ new Date();
|
|
2115
|
+
const p2 = (n) => String(n).padStart(2, "0");
|
|
2116
|
+
const hm = `${p2(d.getHours())}:${p2(d.getMinutes())}`;
|
|
2117
|
+
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
|
|
2118
|
+
if (sameDay) return `\u4ECA\u5929 ${hm}`;
|
|
2119
|
+
const md2 = `${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
|
|
2120
|
+
return d.getFullYear() === now.getFullYear() ? `${md2} ${hm}` : `${d.getFullYear()}-${md2} ${hm}`;
|
|
2121
|
+
}
|
|
2122
|
+
function buildHelpCard(scope) {
|
|
2123
|
+
const elements = [];
|
|
2124
|
+
if (scope === "single") {
|
|
2125
|
+
elements.push(
|
|
2126
|
+
md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"),
|
|
2127
|
+
hr(),
|
|
2128
|
+
md(
|
|
2129
|
+
"\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u4EA4\u7ED9\u6211\u5904\u7406\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
|
|
2130
|
+
)
|
|
2131
|
+
);
|
|
2132
|
+
} else if (scope === "topic") {
|
|
2133
|
+
elements.push(
|
|
2134
|
+
md("\u{1F9F5} **\u8BDD\u9898\u5185** \u2014 \u6BCF\u4E2A\u8BDD\u9898\u662F\u4E00\u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
|
|
2135
|
+
hr(),
|
|
2136
|
+
md(
|
|
2137
|
+
"\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
|
|
2138
|
+
),
|
|
2139
|
+
note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
|
|
2140
|
+
);
|
|
2141
|
+
} else {
|
|
2142
|
+
elements.push(
|
|
2143
|
+
md("\u{1F465} **\u4E3B\u7FA4\u533A** \u2014 @\u6211\u5F00\u8BDD\u9898\uFF0C\u6BCF\u4E2A\u8BDD\u9898\u662F\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
|
|
2144
|
+
hr(),
|
|
2145
|
+
md(
|
|
2146
|
+
"\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB\n\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/model` \u2192 \u9700\u8981\u5728\u8BDD\u9898\u91CC\u7528\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
|
|
2147
|
+
)
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
|
|
2151
|
+
}
|
|
2152
|
+
function buildWelcomeCard(kind, docUrl) {
|
|
2153
|
+
const elements = [
|
|
2154
|
+
md("\u{1F44B} **\u6B22\u8FCE\u4F7F\u7528 Codex Bridge** \u2014 \u672C\u7FA4\u5DF2\u7ED1\u5B9A\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5728\u7FA4\u91CC\u5C31\u80FD\u9A71\u52A8\u672C\u673A Codex \u5E72\u6D3B\u3002"),
|
|
2155
|
+
hr()
|
|
2156
|
+
];
|
|
2157
|
+
if (kind === "single") {
|
|
2158
|
+
elements.push(
|
|
2159
|
+
md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4**\uFF08\u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\uFF09"),
|
|
2160
|
+
md(
|
|
2161
|
+
"\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u4EA4\u7ED9\u6211\u5904\u7406\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/help` \u2192 \u547D\u4EE4\u901F\u67E5\u5361"
|
|
2162
|
+
)
|
|
2163
|
+
);
|
|
2164
|
+
} else {
|
|
2165
|
+
elements.push(
|
|
2166
|
+
md("\u{1F465} **\u4E3B\u7FA4\u533A**"),
|
|
2167
|
+
md(
|
|
2168
|
+
"\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB\uFF08\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF09\n\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09"
|
|
2169
|
+
),
|
|
2170
|
+
md("\u{1F9F5} **\u8BDD\u9898\u5185**"),
|
|
2171
|
+
md("\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"),
|
|
2172
|
+
note("\u4EFB\u610F\u573A\u666F\u53D1 `/help` \u770B\u5F53\u524D\u53EF\u7528\u547D\u4EE4\u3002")
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
if (docUrl) {
|
|
2176
|
+
elements.push(hr(), actions([linkButton("\u{1F4D6} \u67E5\u770B\u5B8C\u6574\u4F7F\u7528\u624B\u518C", docUrl, "primary")]));
|
|
2177
|
+
}
|
|
2178
|
+
return card(elements, { header: { title: "\u{1F916} \u672C\u7FA4\u4F7F\u7528\u8BF4\u660E", template: "turquoise" }, summary: "\u672C\u7FA4\u4F7F\u7528\u8BF4\u660E" });
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/card/history-card.ts
|
|
2182
|
+
var USER_MAX = 300;
|
|
2183
|
+
var ASSIST_MAX = 800;
|
|
2184
|
+
var REASON_MAX = 600;
|
|
2185
|
+
var TOOL_TITLE_MAX = 90;
|
|
2186
|
+
var TOOLS_BODY_MAX = 700;
|
|
2187
|
+
var TOOLS_MAX_LINES = 12;
|
|
2188
|
+
var PREVIEW_MAX = 160;
|
|
2189
|
+
var TITLE_Q_MAX = 56;
|
|
2190
|
+
var PANEL_SHELL = 360;
|
|
2191
|
+
var BODY_BUDGET = 18e3;
|
|
2192
|
+
function buildHistoryCard(state) {
|
|
2193
|
+
const { history } = state;
|
|
2194
|
+
const elements = [metaNote2(state)];
|
|
2195
|
+
if (history.turns.length === 0) {
|
|
2196
|
+
elements.push(
|
|
2197
|
+
hr(),
|
|
2198
|
+
md("_\u8FD9\u4E2A\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u663E\u793A\u7684\u5386\u53F2\uFF08\u53EF\u80FD\u662F\u7A7A\u4F1A\u8BDD\u6216\u521A\u521B\u5EFA\uFF09\u3002_"),
|
|
2199
|
+
hr(),
|
|
2200
|
+
resumedFooter()
|
|
2201
|
+
);
|
|
2202
|
+
return card(elements, { header: header(state), summary: "\u5DF2\u6062\u590D\u5386\u53F2\u4F1A\u8BDD" });
|
|
2203
|
+
}
|
|
2204
|
+
const dropped = history.totalTurns - history.turns.length;
|
|
2205
|
+
if (dropped > 0) {
|
|
2206
|
+
elements.push(note(`\u4EC5\u663E\u793A\u6700\u8FD1 ${history.turns.length} \u8F6E\uFF0C\u66F4\u65E9\u7684 ${dropped} \u8F6E Codex \u4ECD\u4FDD\u7559\u5728\u4E0A\u4E0B\u6587\u4E2D\u3002`));
|
|
2207
|
+
}
|
|
2208
|
+
elements.push(hr());
|
|
2209
|
+
const panels = [];
|
|
2210
|
+
let used = 0;
|
|
2211
|
+
for (let i = history.turns.length - 1; i >= 0; i--) {
|
|
2212
|
+
const turn = history.turns[i];
|
|
2213
|
+
if (!turn) continue;
|
|
2214
|
+
const title = turnTitle(turn);
|
|
2215
|
+
const body = turnBody(turn);
|
|
2216
|
+
const size = estimateSize(body) + PANEL_SHELL;
|
|
2217
|
+
if (used + size > BODY_BUDGET && panels.length > 0) {
|
|
2218
|
+
const stub = "_\uFF08\u5185\u5BB9\u5DF2\u7701\u7565\uFF0C\u5386\u53F2\u8F83\u957F\uFF09_";
|
|
2219
|
+
panels.push(collapsiblePanel({ title, expanded: false, border: "grey", body: stub }));
|
|
2220
|
+
used += title.length + stub.length + PANEL_SHELL;
|
|
2221
|
+
} else {
|
|
2222
|
+
panels.push(collapsiblePanelEl({ title, expanded: false, border: "grey", elements: body }));
|
|
2223
|
+
used += size;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
panels.reverse();
|
|
2227
|
+
elements.push(...panels);
|
|
2228
|
+
const last = history.turns[history.turns.length - 1];
|
|
2229
|
+
const leftOff = last ? last.assistantText || last.userText || (last.tools.at(-1)?.title ?? "") : "";
|
|
2230
|
+
if (leftOff.trim()) {
|
|
2231
|
+
elements.push(hr(), noteMd(`\u{1F4CD} **\u4E0A\u6B21\u505C\u5728**\uFF1A${truncateTail(leftOff, PREVIEW_MAX)}`));
|
|
2232
|
+
} else {
|
|
2233
|
+
elements.push(hr());
|
|
2234
|
+
}
|
|
2235
|
+
elements.push(resumedFooter());
|
|
2236
|
+
return card(elements, { header: header(state), summary: `\u5DF2\u6062\u590D\u5386\u53F2\u4F1A\u8BDD \xB7 \u5171 ${history.totalTurns} \u8F6E` });
|
|
2237
|
+
}
|
|
2238
|
+
function header(state) {
|
|
2239
|
+
const { history } = state;
|
|
2240
|
+
const bits = [
|
|
2241
|
+
history.name?.trim() || state.projectName,
|
|
2242
|
+
`\u5171 ${history.totalTurns} \u8F6E`,
|
|
2243
|
+
history.updatedAt ? relativeTime(history.updatedAt) : void 0
|
|
2244
|
+
].filter(Boolean);
|
|
2245
|
+
return { title: "\u{1F558} \u5DF2\u6062\u590D\u5386\u53F2\u4F1A\u8BDD", template: "turquoise", subtitle: bits.join(" \xB7 ") };
|
|
2246
|
+
}
|
|
2247
|
+
function metaNote2(state) {
|
|
2248
|
+
const parts = [`\u{1F4C2} \`${state.cwd}\``];
|
|
2249
|
+
if (state.projectName) parts.unshift(`\u{1F4C1} ${state.projectName}`);
|
|
2250
|
+
return note(parts.join(" "));
|
|
2251
|
+
}
|
|
2252
|
+
function resumedFooter() {
|
|
2253
|
+
return md("\u2705 **\u4F1A\u8BDD\u5DF2\u6062\u590D** \u2014\u2014 \u76F4\u63A5\u53D1\u6D88\u606F\u5373\u53EF\u7EE7\u7EED\u3002");
|
|
2254
|
+
}
|
|
2255
|
+
function turnTitle(turn) {
|
|
2256
|
+
if (turn.userText.trim()) return `\u{1F464} ${escapeInline(truncate2(oneLine(turn.userText), TITLE_Q_MAX))}`;
|
|
2257
|
+
return "\u2699\uFE0F \u7CFB\u7EDF / \u5DE5\u5177\u8C03\u7528";
|
|
2258
|
+
}
|
|
2259
|
+
function turnBody(turn) {
|
|
2260
|
+
const out = [];
|
|
2261
|
+
if (turn.userText.trim()) out.push(md(`**\u{1F464} \u4F60**
|
|
2262
|
+
${truncate2(turn.userText, USER_MAX)}`));
|
|
2263
|
+
if (turn.assistantText.trim()) out.push(md(`**\u{1F916} Codex**
|
|
2264
|
+
${truncate2(turn.assistantText, ASSIST_MAX)}`));
|
|
2265
|
+
if (!turn.assistantText.trim() && !turn.userText.trim() && turn.tools.length) {
|
|
2266
|
+
out.push(noteMd("_\uFF08\u4EC5\u5DE5\u5177\u8C03\u7528\uFF0C\u65E0\u6587\u672C\u56DE\u590D\uFF09_"));
|
|
2267
|
+
}
|
|
2268
|
+
const detail = [];
|
|
2269
|
+
if (turn.reasoning.trim()) detail.push(md(`\u{1F9E0} **\u601D\u8003**
|
|
2270
|
+
${truncate2(turn.reasoning, REASON_MAX)}`));
|
|
2271
|
+
if (turn.tools.length) detail.push(md(toolsBlock(turn.tools)));
|
|
2272
|
+
if (detail.length) {
|
|
2273
|
+
out.push(collapsiblePanelEl({ title: detailTitle(turn), expanded: false, border: "blue", elements: detail }));
|
|
2274
|
+
}
|
|
2275
|
+
return out;
|
|
2276
|
+
}
|
|
2277
|
+
function detailTitle(turn) {
|
|
2278
|
+
const parts = [];
|
|
2279
|
+
if (turn.reasoning.trim()) parts.push("\u{1F9E0} \u601D\u8003");
|
|
2280
|
+
if (turn.tools.length) parts.push(`\u{1F9F0} ${turn.tools.length} \u4E2A\u5DE5\u5177`);
|
|
2281
|
+
return `\u{1F50E} ${parts.join(" \xB7 ")}`;
|
|
2282
|
+
}
|
|
2283
|
+
function toolsBlock(tools) {
|
|
2284
|
+
const lines = [`\u{1F9F0} **\u5DE5\u5177\u8C03\u7528\uFF08${tools.length}\uFF09**`];
|
|
2285
|
+
let body = 0;
|
|
2286
|
+
let shown = 0;
|
|
2287
|
+
for (const t of tools) {
|
|
2288
|
+
if (shown >= TOOLS_MAX_LINES || body >= TOOLS_BODY_MAX) {
|
|
2289
|
+
lines.push(`_\u2026\u8FD8\u6709 ${tools.length - shown} \u4E2A_`);
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
const line = toolLine(t);
|
|
2293
|
+
lines.push(line);
|
|
2294
|
+
body += line.length;
|
|
2295
|
+
shown += 1;
|
|
2296
|
+
}
|
|
2297
|
+
return lines.join("\n");
|
|
2298
|
+
}
|
|
2299
|
+
function toolLine(t) {
|
|
2300
|
+
const title = escapeInline(truncate2(oneLine(t.title), TOOL_TITLE_MAX));
|
|
2301
|
+
const mark = t.failed ? " \u2717" : "";
|
|
2302
|
+
const exit = t.exitCode != null && t.exitCode !== 0 ? ` (exit ${t.exitCode})` : "";
|
|
2303
|
+
return `- \`${title}\`${mark}${exit}`;
|
|
2304
|
+
}
|
|
2305
|
+
function estimateSize(els) {
|
|
2306
|
+
let n = 0;
|
|
2307
|
+
for (const el of els) n += JSON.stringify(el).length;
|
|
2308
|
+
return n;
|
|
2309
|
+
}
|
|
2310
|
+
function oneLine(s) {
|
|
2311
|
+
return s.replace(/\s+/g, " ").trim();
|
|
2312
|
+
}
|
|
2313
|
+
function escapeInline(s) {
|
|
2314
|
+
return s.replace(/`/g, "");
|
|
2315
|
+
}
|
|
2316
|
+
function truncate2(s, n) {
|
|
2317
|
+
const t = s.trim();
|
|
2318
|
+
return t.length > n ? `${t.slice(0, n)}\u2026` : t;
|
|
2319
|
+
}
|
|
2320
|
+
function truncateTail(s, n) {
|
|
2321
|
+
const t = oneLine(s);
|
|
2322
|
+
return t.length > n ? `\u2026${t.slice(t.length - n)}` : t;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/card/tool-render.ts
|
|
2326
|
+
var HEADER_TITLE_MAX = 80;
|
|
2327
|
+
var OUTPUT_MAX = 1200;
|
|
2328
|
+
var BODY_TOTAL_MAX = 2500;
|
|
2329
|
+
function toolHeaderText(tool) {
|
|
2330
|
+
const icon = tool.status === "done" ? "\u2705" : tool.status === "error" ? "\u274C" : "\u23F3";
|
|
2331
|
+
return `${icon} **${escapeInline2(truncate3(tool.title, HEADER_TITLE_MAX))}**`;
|
|
2332
|
+
}
|
|
2333
|
+
function toolBodyMd(tool) {
|
|
2334
|
+
if (!tool.output) {
|
|
2335
|
+
return tool.status === "running" ? "_\u8FD0\u884C\u4E2D\u2026_" : "";
|
|
2336
|
+
}
|
|
2337
|
+
const out = truncate3(tool.output, OUTPUT_MAX);
|
|
2338
|
+
const label = tool.status === "error" ? "Error" : "Output";
|
|
2339
|
+
const body = `**${label}**
|
|
2340
|
+
\`\`\`
|
|
2341
|
+
${out}
|
|
2342
|
+
\`\`\``;
|
|
2343
|
+
if (body.length <= BODY_TOTAL_MAX) return body;
|
|
2344
|
+
return `${body.slice(0, BODY_TOTAL_MAX)}\u2026
|
|
2345
|
+
|
|
2346
|
+
_\uFF08\u5DF2\u622A\u65AD\uFF0C\u5B8C\u6574\u5185\u5BB9\u89C1\u65E5\u5FD7\uFF09_`;
|
|
2347
|
+
}
|
|
2348
|
+
function truncate3(s, max) {
|
|
2349
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
2350
|
+
}
|
|
2351
|
+
function escapeInline2(s) {
|
|
2352
|
+
return s.replace(/\s+/g, " ").trim();
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/card/run-card.ts
|
|
2356
|
+
var RC = {
|
|
2357
|
+
stop: "run.stop"
|
|
2358
|
+
};
|
|
2359
|
+
var REASONING_MAX = 1500;
|
|
2360
|
+
var COLLAPSE_TOOL_THRESHOLD = 3;
|
|
2361
|
+
function buildRunCard(rc) {
|
|
2362
|
+
const state = rc.rs;
|
|
2363
|
+
const running = state.terminal === "running";
|
|
2364
|
+
const elements = [];
|
|
2365
|
+
const reasoning = reasoningContent(state);
|
|
2366
|
+
if (reasoning) elements.push(reasoningPanel(reasoning, state.reasoningActive));
|
|
2367
|
+
const blocks = rc.showTools === false ? state.blocks.filter((b) => b.kind !== "tool") : state.blocks;
|
|
2368
|
+
for (const group of groupBlocks(blocks)) {
|
|
2369
|
+
if (group.kind === "text") {
|
|
2370
|
+
if (group.content.trim()) elements.push(md(group.content));
|
|
2371
|
+
} else {
|
|
2372
|
+
elements.push(...renderToolGroup(group.tools, !running));
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (state.terminal === "interrupted") {
|
|
2376
|
+
elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
|
|
2377
|
+
} else if (state.terminal === "idle_timeout") {
|
|
2378
|
+
elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
|
|
2379
|
+
} else if (state.terminal === "error" && state.errorMsg) {
|
|
2380
|
+
elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
|
|
2381
|
+
} else if (state.terminal === "done" && elements.length === 0) {
|
|
2382
|
+
elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
|
|
2383
|
+
}
|
|
2384
|
+
if (running) {
|
|
2385
|
+
if (state.footer) elements.push(footerStatus(state.footer));
|
|
2386
|
+
if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
|
|
2387
|
+
}
|
|
2388
|
+
return card(elements, { streaming: running, summary: summaryText(state) });
|
|
2389
|
+
}
|
|
2390
|
+
function buildRunCardPlain(rc) {
|
|
2391
|
+
return buildRunCard({ ...rc, cardKey: void 0 });
|
|
2392
|
+
}
|
|
2393
|
+
function* groupBlocks(blocks) {
|
|
2394
|
+
let toolBuf = [];
|
|
2395
|
+
for (const b of blocks) {
|
|
2396
|
+
if (b.kind === "tool") {
|
|
2397
|
+
toolBuf.push(b.tool);
|
|
2398
|
+
} else {
|
|
2399
|
+
if (toolBuf.length > 0) {
|
|
2400
|
+
yield { kind: "tools", tools: toolBuf };
|
|
2401
|
+
toolBuf = [];
|
|
2402
|
+
}
|
|
2403
|
+
yield { kind: "text", content: b.content };
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
if (toolBuf.length > 0) yield { kind: "tools", tools: toolBuf };
|
|
2407
|
+
}
|
|
2408
|
+
function renderToolGroup(tools, finalized) {
|
|
2409
|
+
if (tools.length === 0) return [];
|
|
2410
|
+
if (tools.length < COLLAPSE_TOOL_THRESHOLD) {
|
|
2411
|
+
return tools.map((t) => toolPanel(t, false));
|
|
2412
|
+
}
|
|
2413
|
+
if (finalized) return [collapsedToolSummary(tools, true)];
|
|
2414
|
+
const prior = tools.slice(0, -1);
|
|
2415
|
+
const latest = tools[tools.length - 1];
|
|
2416
|
+
const out = [];
|
|
2417
|
+
if (prior.length > 0) out.push(collapsedToolSummary(prior, false));
|
|
2418
|
+
if (latest) out.push(toolPanel(latest, true));
|
|
2419
|
+
return out;
|
|
2420
|
+
}
|
|
2421
|
+
function reasoningPanel(content, active) {
|
|
2422
|
+
return collapsiblePanel({
|
|
2423
|
+
title: active ? "\u{1F9E0} **\u601D\u8003\u4E2D**" : "\u{1F9E0} **\u601D\u8003\u5B8C\u6210\uFF0C\u70B9\u51FB\u67E5\u770B**",
|
|
2424
|
+
expanded: active,
|
|
2425
|
+
border: "grey",
|
|
2426
|
+
body: truncate4(content, REASONING_MAX)
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
function toolPanel(tool, expanded) {
|
|
2430
|
+
return collapsiblePanel({
|
|
2431
|
+
title: toolHeaderText(tool),
|
|
2432
|
+
expanded,
|
|
2433
|
+
border: tool.status === "error" ? "red" : "grey",
|
|
2434
|
+
body: toolBodyMd(tool) || "_\u65E0\u8F93\u51FA_"
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
function collapsedToolSummary(tools, finalized) {
|
|
2438
|
+
const suffix = finalized ? "\uFF08\u5DF2\u7ED3\u675F\uFF09" : "";
|
|
2439
|
+
return collapsiblePanel({
|
|
2440
|
+
title: `\u2615 **${tools.length} \u4E2A\u5DE5\u5177\u8C03\u7528${suffix}**`,
|
|
2441
|
+
expanded: false,
|
|
2442
|
+
border: "blue",
|
|
2443
|
+
body: tools.map((t) => `- ${toolHeaderText(t)}`).join("\n")
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
function footerStatus(status) {
|
|
2447
|
+
const text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
|
|
2448
|
+
return noteMd(text);
|
|
2449
|
+
}
|
|
2450
|
+
function summaryText(state) {
|
|
2451
|
+
if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
|
|
2452
|
+
if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
|
|
2453
|
+
if (state.terminal === "error") return "\u51FA\u9519";
|
|
2454
|
+
if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
|
|
2455
|
+
if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
|
|
2456
|
+
if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
|
|
2457
|
+
return "\u601D\u8003\u4E2D";
|
|
2458
|
+
}
|
|
2459
|
+
function truncate4(s, n) {
|
|
2460
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/card/run-card-stream.ts
|
|
2464
|
+
var STREAM_THROTTLE_MS = 250;
|
|
2465
|
+
var RunCardStream = class {
|
|
2466
|
+
cardId = "";
|
|
2467
|
+
_messageId = "";
|
|
2468
|
+
seq = 0;
|
|
2469
|
+
lastPush = 0;
|
|
2470
|
+
lastContent = "";
|
|
2471
|
+
get messageId() {
|
|
2472
|
+
return this._messageId;
|
|
2473
|
+
}
|
|
2474
|
+
/** Create the entity from the initial (running) card and send a message
|
|
2475
|
+
* referencing it by card_id. Returns the carrier message id. */
|
|
2476
|
+
async create(channel, chatId, initialCard, opts) {
|
|
2477
|
+
const created = await channel.rawClient.cardkit.v1.card.create({
|
|
2478
|
+
data: { type: "card_json", data: JSON.stringify(initialCard) }
|
|
2479
|
+
});
|
|
2480
|
+
const cardId = created.data?.card_id;
|
|
2481
|
+
if (!cardId) {
|
|
2482
|
+
throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
|
|
2483
|
+
}
|
|
2484
|
+
this.cardId = cardId;
|
|
2485
|
+
this.lastContent = JSON.stringify(initialCard);
|
|
2486
|
+
const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
|
|
2487
|
+
let messageId;
|
|
2488
|
+
if (opts.replyTo) {
|
|
2489
|
+
const r = await channel.rawClient.im.v1.message.reply({
|
|
2490
|
+
path: { message_id: opts.replyTo },
|
|
2491
|
+
data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
|
|
2492
|
+
});
|
|
2493
|
+
messageId = r.data?.message_id;
|
|
2494
|
+
} else {
|
|
2495
|
+
const r = await channel.rawClient.im.v1.message.create({
|
|
2496
|
+
params: { receive_id_type: "chat_id" },
|
|
2497
|
+
data: { receive_id: chatId, msg_type: "interactive", content }
|
|
2498
|
+
});
|
|
2499
|
+
messageId = r.data?.message_id;
|
|
2500
|
+
}
|
|
2501
|
+
if (!messageId) throw new Error("run card send returned no message_id");
|
|
2502
|
+
this._messageId = messageId;
|
|
2503
|
+
return messageId;
|
|
2504
|
+
}
|
|
2505
|
+
/** Throttled whole-card stream update. Skips identical/too-soon pushes;
|
|
2506
|
+
* `force` flushes regardless (still de-duped on content). */
|
|
2507
|
+
async streamCard(channel, fullCard, force = false) {
|
|
2508
|
+
if (!this.cardId) return;
|
|
2509
|
+
const data = JSON.stringify(fullCard);
|
|
2510
|
+
if (data === this.lastContent) return;
|
|
2511
|
+
const now = Date.now();
|
|
2512
|
+
if (!force && now - this.lastPush < STREAM_THROTTLE_MS) return;
|
|
2513
|
+
this.lastPush = now;
|
|
2514
|
+
this.lastContent = data;
|
|
2515
|
+
try {
|
|
2516
|
+
await channel.rawClient.cardkit.v1.card.update({
|
|
2517
|
+
path: { card_id: this.cardId },
|
|
2518
|
+
data: { card: { type: "card_json", data }, sequence: ++this.seq, uuid: `s_${this.cardId}_${this.seq}` }
|
|
2519
|
+
});
|
|
2520
|
+
} catch (err) {
|
|
2521
|
+
log.fail("card", err, { phase: "run-stream", cardId: this.cardId, seq: this.seq });
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
/** Forced whole-card replace for structural/terminal updates. A terminal
|
|
2525
|
+
* card built with streaming off clears the typewriter cursor. */
|
|
2526
|
+
async updateCard(channel, fullCard) {
|
|
2527
|
+
if (!this.cardId) return;
|
|
2528
|
+
const data = JSON.stringify(fullCard);
|
|
2529
|
+
this.lastContent = data;
|
|
2530
|
+
const push = async () => {
|
|
2531
|
+
await channel.rawClient.cardkit.v1.card.update({
|
|
2532
|
+
path: { card_id: this.cardId },
|
|
2533
|
+
data: { card: { type: "card_json", data }, sequence: ++this.seq, uuid: `u_${this.cardId}_${this.seq}` }
|
|
2534
|
+
});
|
|
2535
|
+
};
|
|
2536
|
+
try {
|
|
2537
|
+
await push();
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
log.fail("card", err, { phase: "run-update", cardId: this.cardId, seq: this.seq, retry: true });
|
|
2540
|
+
await new Promise((r) => setTimeout(r, 3200));
|
|
2541
|
+
try {
|
|
2542
|
+
await push();
|
|
2543
|
+
} catch (err2) {
|
|
2544
|
+
log.fail("card", err2, { phase: "run-update-retry", cardId: this.cardId, seq: this.seq });
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
|
|
2550
|
+
// src/card/dm-cards.ts
|
|
2551
|
+
function openChatUrl(chatId) {
|
|
2552
|
+
return `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(chatId)}`;
|
|
2553
|
+
}
|
|
2554
|
+
var DM = {
|
|
2555
|
+
menu: "dm.menu",
|
|
2556
|
+
newProject: "dm.newProject",
|
|
2557
|
+
newProjectSubmit: "dm.newProject.submit",
|
|
2558
|
+
projects: "dm.projects",
|
|
2559
|
+
settings: "dm.settings",
|
|
2560
|
+
doctor: "dm.doctor",
|
|
2561
|
+
reconnect: "dm.reconnect",
|
|
2562
|
+
rmConfirm: "dm.rmConfirm",
|
|
2563
|
+
rmDo: "dm.rmDo",
|
|
2564
|
+
rmCancel: "dm.rmCancel",
|
|
2565
|
+
setTools: "dm.set.tools",
|
|
2566
|
+
setWatchdog: "dm.set.watchdog",
|
|
2567
|
+
setPending: "dm.set.pending",
|
|
2568
|
+
setConcurrency: "dm.set.concurrency"
|
|
2569
|
+
};
|
|
2570
|
+
var GS = {
|
|
2571
|
+
setNoMention: "gs.noMention"
|
|
2572
|
+
};
|
|
2573
|
+
function kindLabel(kind) {
|
|
2574
|
+
return kind === "single" ? "\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4" : "\u{1F465} \u591A\u8BDD\u9898\u7FA4";
|
|
2575
|
+
}
|
|
2576
|
+
function buildDmMenuCard() {
|
|
2577
|
+
return card(
|
|
2578
|
+
[
|
|
2579
|
+
md("\u79C1\u804A\u7528\u4E8E**\u5EFA\u9879\u76EE\u548C\u7BA1\u7406**\uFF1B\u5177\u4F53\u4EFB\u52A1\u8BF7\u5230\u9879\u76EE\u7FA4\u91CC @\u6211\u3002"),
|
|
2580
|
+
hr(),
|
|
2581
|
+
actions([
|
|
2582
|
+
button("\u2795 \u65B0\u5EFA\u9879\u76EE", { a: DM.newProject }, "primary"),
|
|
2583
|
+
button("\u{1F4C1} \u9879\u76EE\u5217\u8868", { a: DM.projects }),
|
|
2584
|
+
button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.settings })
|
|
2585
|
+
]),
|
|
2586
|
+
actions([
|
|
2587
|
+
button("\u{1FA7A} \u8BCA\u65AD", { a: DM.doctor }),
|
|
2588
|
+
button("\u{1F504} \u91CD\u8FDE", { a: DM.reconnect })
|
|
2589
|
+
])
|
|
2590
|
+
],
|
|
2591
|
+
{ header: { title: "\u{1F916} Codex Bridge \u7BA1\u7406\u53F0", template: "blue" } }
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
function buildNewProjectFormCard(opts = {}) {
|
|
2595
|
+
const elements = [];
|
|
2596
|
+
if (opts.error) elements.push(md(`\u274C **\u521B\u5EFA\u5931\u8D25**\uFF1A${opts.error}`));
|
|
2597
|
+
elements.push(
|
|
2598
|
+
md("\u586B\u9879\u76EE\u540D\uFF08\u5FC5\u586B\uFF09\u3002**\u6587\u4EF6\u5939\u8DEF\u5F84\u7559\u7A7A** = \u81EA\u52A8\u5728\u9ED8\u8BA4\u4F4D\u7F6E\u65B0\u5EFA\u4E00\u4E2A\u7A7A\u767D\u9879\u76EE\uFF1B**\u586B\u7EDD\u5BF9\u8DEF\u5F84** = \u7528\u7535\u8111\u4E0A\u5DF2\u6709\u7684\u6587\u4EF6\u5939\u3002"),
|
|
2599
|
+
form("new_project", [
|
|
2600
|
+
input({ name: "name", label: "\u9879\u76EE\u540D", placeholder: "my-app", value: opts.name, required: true }),
|
|
2601
|
+
input({ name: "cwd", label: "\u6587\u4EF6\u5939\u8DEF\u5F84\uFF08\u9009\u586B\uFF0C\u7559\u7A7A\u81EA\u52A8\u65B0\u5EFA\uFF09", placeholder: "/Users/you/code/my-app", value: opts.cwd }),
|
|
2602
|
+
note("\u9009\u7FA4\u7C7B\u578B(\u76F4\u63A5\u70B9\u5BF9\u5E94\u6309\u94AE\u521B\u5EFA)\uFF1A\u{1F465} \u591A\u8BDD\u9898\u7FA4 = @\u6211\u5F00\u8BDD\u9898\u3001\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF1B\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4 = \u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\u3001\u8FDE\u7EED\u4E0A\u4E0B\u6587\u3002"),
|
|
2603
|
+
actions([
|
|
2604
|
+
submitButton("\u{1F465} \u521B\u5EFA\xB7\u591A\u8BDD\u9898\u7FA4", { a: DM.newProjectSubmit, kind: "multi" }, "primary", "submit_multi"),
|
|
2605
|
+
submitButton("\u{1F4AC} \u521B\u5EFA\xB7\u5355\u4F1A\u8BDD\u7FA4", { a: DM.newProjectSubmit, kind: "single" }, "primary", "submit_single")
|
|
2606
|
+
]),
|
|
2607
|
+
actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
|
|
2608
|
+
])
|
|
2609
|
+
);
|
|
2610
|
+
return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "turquoise" } });
|
|
2611
|
+
}
|
|
2612
|
+
function buildNewProjectDoneCard(p) {
|
|
2613
|
+
const elements = [
|
|
2614
|
+
md(`\u2705 \u5DF2\u521B\u5EFA\u9879\u76EE **${p.name}**${p.blank ? " _(\u7A7A\u767D\u9879\u76EE)_" : ""}`),
|
|
2615
|
+
note(`\u{1F4C2} \`${p.cwd}\` \xB7 ${kindLabel(p.kind)}`),
|
|
2616
|
+
md(p.chatId ? "\u7FA4\u5DF2\u5EFA\u597D \u{1F449} \u53BB\u9879\u76EE\u7FA4\u91CC **@\u6211** \u5E72\u6D3B\u3002" : "\u53D1\u6211\u4EFB\u610F\u6D88\u606F\u53EF\u518D\u6B21\u6253\u5F00\u7BA1\u7406\u53F0\u3002")
|
|
2617
|
+
];
|
|
2618
|
+
if (p.chatId) elements.push(actions([linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary")]));
|
|
2619
|
+
return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "green" } });
|
|
2620
|
+
}
|
|
2621
|
+
function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
|
|
2622
|
+
if (projects.length === 0) {
|
|
2623
|
+
return card(
|
|
2624
|
+
[md("\u8FD8\u6CA1\u6709\u9879\u76EE\u3002\u70B9 **\u2795 \u65B0\u5EFA\u9879\u76EE** \u6216\u76F4\u63A5\u53D1\u6211\u4E00\u4E2A\u9879\u76EE\u540D\u3002"), actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])],
|
|
2625
|
+
{ header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } }
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
const elements = [];
|
|
2629
|
+
for (const p of projects) {
|
|
2630
|
+
elements.push(md(`**${p.name}**${p.blank ? " _(\u7A7A\u767D)_" : ""}`));
|
|
2631
|
+
elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
|
|
2632
|
+
elements.push(
|
|
2633
|
+
note(
|
|
2634
|
+
p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)} \xB7 \u514D@\uFF1A${p.noMention ?? true ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
|
|
2635
|
+
)
|
|
2636
|
+
);
|
|
2637
|
+
const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
|
|
2638
|
+
if (sessions.length === 0) {
|
|
2639
|
+
elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
|
|
2640
|
+
} else {
|
|
2641
|
+
const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
2642
|
+
for (const s of sorted) {
|
|
2643
|
+
const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 40);
|
|
2644
|
+
elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
const row = [];
|
|
2648
|
+
if (p.chatId) row.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId)));
|
|
2649
|
+
row.push(button("\u{1F5D1} \u5220\u9664", { a: DM.rmConfirm, n: p.name }, "danger"));
|
|
2650
|
+
elements.push(actions(row));
|
|
2651
|
+
elements.push(hr());
|
|
2652
|
+
}
|
|
2653
|
+
elements.push(note(`\u5171 ${projects.length} \u4E2A\u9879\u76EE`));
|
|
2654
|
+
elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
|
|
2655
|
+
return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
|
|
2656
|
+
}
|
|
2657
|
+
function buildRmConfirmCard(name) {
|
|
2658
|
+
return card(
|
|
2659
|
+
[
|
|
2660
|
+
md(`\u786E\u5B9A\u5220\u9664\u9879\u76EE **${name}**\uFF1F`),
|
|
2661
|
+
note("\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002"),
|
|
2662
|
+
actions([
|
|
2663
|
+
button("\u2705 \u786E\u8BA4\u5220\u9664", { a: DM.rmDo, n: name }, "danger"),
|
|
2664
|
+
button("\u53D6\u6D88", { a: DM.rmCancel })
|
|
2665
|
+
])
|
|
2666
|
+
],
|
|
2667
|
+
{ header: { title: "\u{1F5D1} \u5220\u9664\u9879\u76EE", template: "red" } }
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
function optionRow(label, actionId, current, opts) {
|
|
2671
|
+
return [
|
|
2672
|
+
md(label),
|
|
2673
|
+
actions(opts.map((o) => button(o.label, { a: actionId, v: o.value }, o.value === current ? "primary" : "default")))
|
|
2674
|
+
];
|
|
2675
|
+
}
|
|
2676
|
+
function buildSettingsCard(cfg) {
|
|
2677
|
+
const watchdogSec = cfg.preferences?.runIdleTimeoutSeconds ?? 120;
|
|
2678
|
+
return card(
|
|
2679
|
+
[
|
|
2680
|
+
md("**\u5168\u5C40\u8BBE\u7F6E**\uFF08\u7BA1\u7406\u5458\uFF09"),
|
|
2681
|
+
...optionRow("\u{1F527} \u5DE5\u5177\u8C03\u7528", DM.setTools, getShowToolCalls(cfg) ? "on" : "off", [
|
|
2682
|
+
{ label: "\u663E\u793A", value: "on" },
|
|
2683
|
+
{ label: "\u9690\u85CF", value: "off" }
|
|
2684
|
+
]),
|
|
2685
|
+
...optionRow("\u23F1 \u5047\u6B7B\u8D85\u65F6", DM.setWatchdog, String(watchdogSec), [
|
|
2686
|
+
{ label: "\u5173\u95ED", value: "0" },
|
|
2687
|
+
{ label: "60\u79D2", value: "60" },
|
|
2688
|
+
{ label: "120\u79D2", value: "120" },
|
|
2689
|
+
{ label: "300\u79D2", value: "300" }
|
|
2690
|
+
]),
|
|
2691
|
+
...optionRow("\u{1F4E5} \u8FD0\u884C\u4E2D\u65B0\u6D88\u606F", DM.setPending, getPendingPolicy(cfg), [
|
|
2692
|
+
{ label: "\u5F15\u5BFC", value: "steer" },
|
|
2693
|
+
{ label: "\u6392\u961F", value: "queue" }
|
|
2694
|
+
]),
|
|
2695
|
+
...optionRow("\u26A1 \u5E76\u53D1\u4E0A\u9650", DM.setConcurrency, String(getMaxConcurrentRuns(cfg)), [
|
|
2696
|
+
{ label: "1", value: "1" },
|
|
2697
|
+
{ label: "5", value: "5" },
|
|
2698
|
+
{ label: "10", value: "10" },
|
|
2699
|
+
{ label: "20", value: "20" }
|
|
2700
|
+
]),
|
|
2701
|
+
note("\u26A0\uFE0F \u5047\u6B7B\u8D85\u65F6 / \u5E76\u53D1\u4E0A\u9650 \u6539\u540E\u9700**\u91CD\u542F**\u751F\u6548\uFF1B\u5DE5\u5177\u663E\u793A / \u8FD0\u884C\u4E2D\u65B0\u6D88\u606F \u5373\u65F6\u751F\u6548\u3002"),
|
|
2702
|
+
actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
|
|
2703
|
+
],
|
|
2704
|
+
{ header: { title: "\u2699\uFE0F \u8BBE\u7F6E", template: "blue" } }
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
function buildGroupSettingsCard(project) {
|
|
2708
|
+
const kind = project.kind ?? "multi";
|
|
2709
|
+
const noMention = project.noMention ?? true;
|
|
2710
|
+
const scopeNote = kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u7684\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002";
|
|
2711
|
+
return card(
|
|
2712
|
+
[
|
|
2713
|
+
md(`**\u7FA4\u8BBE\u7F6E** \xB7 ${project.name}`),
|
|
2714
|
+
note(`\u7FA4\u7C7B\u578B(\u5EFA\u7FA4\u65F6\u5B9A\uFF0C\u4E0D\u53EF\u6539)\uFF1A${kindLabel(kind)}`),
|
|
2715
|
+
...optionRow("\u270B \u514D@\uFF08\u4E0D\u7528 @ \u4E5F\u56DE\u590D\uFF09", GS.setNoMention, noMention ? "on" : "off", [
|
|
2716
|
+
{ label: "\u5F00", value: "on" },
|
|
2717
|
+
{ label: "\u5173", value: "off" }
|
|
2718
|
+
]),
|
|
2719
|
+
note(scopeNote),
|
|
2720
|
+
note("\u26A0\uFE0F \u514D@ \u9700\u5E94\u7528\u5DF2\u5F00\u901A\u300C\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\u300D(im:message.group_msg)\u6743\u9650\uFF0C\u5426\u5219\u6536\u4E0D\u5230\u975E @ \u6D88\u606F\u3002")
|
|
2721
|
+
],
|
|
2722
|
+
{ header: { title: "\u2699\uFE0F \u7FA4\u8BBE\u7F6E", template: "blue" } }
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/project/registry.ts
|
|
2727
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
2728
|
+
import { dirname as dirname4 } from "path";
|
|
2729
|
+
var FILE_VERSION2 = 1;
|
|
2730
|
+
async function read() {
|
|
2731
|
+
try {
|
|
2732
|
+
const text = await readFile5(paths.projectsFile, "utf8");
|
|
2733
|
+
const parsed = JSON.parse(text);
|
|
2734
|
+
return Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
2735
|
+
} catch (err) {
|
|
2736
|
+
if (err.code === "ENOENT") return [];
|
|
2737
|
+
throw err;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
async function write(projects) {
|
|
2741
|
+
await mkdir4(dirname4(paths.projectsFile), { recursive: true });
|
|
2742
|
+
const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
|
|
2743
|
+
const body = { version: FILE_VERSION2, projects };
|
|
2744
|
+
await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
|
|
2745
|
+
`, "utf8");
|
|
2746
|
+
await rename4(tmp, paths.projectsFile);
|
|
2747
|
+
}
|
|
2748
|
+
async function listProjects() {
|
|
2749
|
+
return read();
|
|
2750
|
+
}
|
|
2751
|
+
async function getProjectByChatId(chatId) {
|
|
2752
|
+
return (await read()).find((p) => p.chatId === chatId);
|
|
2753
|
+
}
|
|
2754
|
+
async function getProjectByName(name) {
|
|
2755
|
+
return (await read()).find((p) => p.name === name);
|
|
2756
|
+
}
|
|
2757
|
+
async function addProject(p) {
|
|
2758
|
+
const projects = await read();
|
|
2759
|
+
if (projects.some((x) => x.name === p.name)) {
|
|
2760
|
+
throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
|
|
2761
|
+
}
|
|
2762
|
+
projects.push(p);
|
|
2763
|
+
await write(projects);
|
|
2764
|
+
}
|
|
2765
|
+
async function updateProject(name, patch) {
|
|
2766
|
+
const projects = await read();
|
|
2767
|
+
const p = projects.find((x) => x.name === name);
|
|
2768
|
+
if (!p) return;
|
|
2769
|
+
const target = p;
|
|
2770
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
2771
|
+
if (v !== void 0) target[k] = v;
|
|
2772
|
+
}
|
|
2773
|
+
await write(projects);
|
|
2774
|
+
}
|
|
2775
|
+
async function removeProject(name) {
|
|
2776
|
+
const projects = await read();
|
|
2777
|
+
const idx = projects.findIndex((p) => p.name === name);
|
|
2778
|
+
if (idx === -1) return void 0;
|
|
2779
|
+
const [removed] = projects.splice(idx, 1);
|
|
2780
|
+
await write(projects);
|
|
2781
|
+
return removed;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// src/project/lifecycle.ts
|
|
2785
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
2786
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2787
|
+
import { isAbsolute, join as join7, resolve } from "path";
|
|
2788
|
+
|
|
2789
|
+
// src/project/git-info.ts
|
|
2790
|
+
import { execFile } from "child_process";
|
|
2791
|
+
import { promisify } from "util";
|
|
2792
|
+
var execFileAsync = promisify(execFile);
|
|
2793
|
+
async function currentBranch(cwd) {
|
|
2794
|
+
try {
|
|
2795
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
2796
|
+
cwd,
|
|
2797
|
+
timeout: 3e3
|
|
2798
|
+
});
|
|
2799
|
+
const b = stdout.trim();
|
|
2800
|
+
return b && b !== "HEAD" ? b : null;
|
|
2801
|
+
} catch {
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// src/project/announcement.ts
|
|
2807
|
+
var PAGE_BLOCK_TYPE = 1;
|
|
2808
|
+
var TEXT_BLOCK_TYPE = 2;
|
|
2809
|
+
var LATEST_REVISION = -1;
|
|
2810
|
+
function buildAnnouncementLine(project, branch) {
|
|
2811
|
+
const parts = [`\u{1F4C1} ${project.name}`];
|
|
2812
|
+
if (!project.blank) parts.push(`\u{1F4C2} ${project.cwd}`);
|
|
2813
|
+
if (branch) parts.push(`\u{1F33F} ${branch}`);
|
|
2814
|
+
return parts.join(" \xB7 ");
|
|
2815
|
+
}
|
|
2816
|
+
async function withRetry(fn, attempts = 5) {
|
|
2817
|
+
let lastErr;
|
|
2818
|
+
for (let i = 0; i < attempts; i++) {
|
|
2819
|
+
try {
|
|
2820
|
+
return await fn();
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
lastErr = err;
|
|
2823
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 800 * (i + 1)));
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
throw lastErr;
|
|
2827
|
+
}
|
|
2828
|
+
async function writeAnnouncement(channel, chatId, line) {
|
|
2829
|
+
const docx = channel.rawClient.docx.v1;
|
|
2830
|
+
await withRetry(async () => {
|
|
2831
|
+
const listed = await docx.chatAnnouncementBlock.list({ path: { chat_id: chatId } });
|
|
2832
|
+
const items = listed.data?.items ?? [];
|
|
2833
|
+
const page = items.find((b) => b.block_type === PAGE_BLOCK_TYPE);
|
|
2834
|
+
if (!page?.block_id) throw new Error("\u7FA4\u516C\u544A\u7F3A\u5C11 page block");
|
|
2835
|
+
const existing = page.children?.length ?? 0;
|
|
2836
|
+
if (existing > 0) {
|
|
2837
|
+
await docx.chatAnnouncementBlockChildren.batchDelete({
|
|
2838
|
+
path: { chat_id: chatId, block_id: page.block_id },
|
|
2839
|
+
params: { revision_id: LATEST_REVISION },
|
|
2840
|
+
data: { start_index: 0, end_index: existing }
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
await docx.chatAnnouncementBlockChildren.create({
|
|
2844
|
+
path: { chat_id: chatId, block_id: page.block_id },
|
|
2845
|
+
params: { revision_id: LATEST_REVISION },
|
|
2846
|
+
data: {
|
|
2847
|
+
index: 0,
|
|
2848
|
+
children: [{ block_type: TEXT_BLOCK_TYPE, text: { elements: [{ text_run: { content: line } }] } }]
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
async function pinAnnouncement(channel, chatId) {
|
|
2854
|
+
await withRetry(
|
|
2855
|
+
() => channel.rawClient.im.v1.chatTopNotice.putTopNotice({
|
|
2856
|
+
path: { chat_id: chatId },
|
|
2857
|
+
data: { chat_top_notice: [{ action_type: "2" }] }
|
|
2858
|
+
})
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
async function setAnnouncement(channel, project) {
|
|
2862
|
+
const branch = await currentBranch(project.cwd);
|
|
2863
|
+
await writeAnnouncement(channel, project.chatId, buildAnnouncementLine(project, branch));
|
|
2864
|
+
try {
|
|
2865
|
+
await pinAnnouncement(channel, project.chatId);
|
|
2866
|
+
} catch (err) {
|
|
2867
|
+
log.fail("project", err, { phase: "announcement-pin" });
|
|
2868
|
+
}
|
|
2869
|
+
await updateProject(project.name, { branch: branch ?? void 0 });
|
|
2870
|
+
}
|
|
2871
|
+
async function refreshBranch(channel, project) {
|
|
2872
|
+
const branch = await currentBranch(project.cwd);
|
|
2873
|
+
if ((branch ?? "\u2014") === (project.branch ?? "\u2014")) return;
|
|
2874
|
+
log.info("project", "branch-change", { name: project.name, from: project.branch ?? "\u2014", to: branch ?? "\u2014" });
|
|
2875
|
+
try {
|
|
2876
|
+
await writeAnnouncement(channel, project.chatId, buildAnnouncementLine(project, branch));
|
|
2877
|
+
await updateProject(project.name, { branch: branch ?? void 0 });
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
log.fail("project", err, { phase: "announcement-patch" });
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/project/onboarding.ts
|
|
2884
|
+
var HELP_DOC_URL = "https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g";
|
|
2885
|
+
async function onboardGroup(channel, project) {
|
|
2886
|
+
const kind = project.kind ?? "multi";
|
|
2887
|
+
const chatId = project.chatId;
|
|
2888
|
+
try {
|
|
2889
|
+
const content = JSON.stringify(buildWelcomeCard(kind, HELP_DOC_URL || void 0));
|
|
2890
|
+
const sent = await channel.rawClient.im.v1.message.create({
|
|
2891
|
+
params: { receive_id_type: "chat_id" },
|
|
2892
|
+
data: { receive_id: chatId, msg_type: "interactive", content }
|
|
2893
|
+
});
|
|
2894
|
+
const messageId = sent.data?.message_id;
|
|
2895
|
+
if (messageId) {
|
|
2896
|
+
await channel.rawClient.im.v1.pin.create({ data: { message_id: messageId } });
|
|
2897
|
+
log.info("project", "onboard-pin", { name: project.name });
|
|
2898
|
+
}
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
log.fail("project", err, { phase: "onboard-welcome" });
|
|
2901
|
+
}
|
|
2902
|
+
if (HELP_DOC_URL) {
|
|
2903
|
+
try {
|
|
2904
|
+
await channel.rawClient.im.v1.chatTab.create({
|
|
2905
|
+
path: { chat_id: chatId },
|
|
2906
|
+
data: {
|
|
2907
|
+
chat_tabs: [{ tab_name: "\u{1F448} \u4F7F\u7528\u8BF4\u660E", tab_type: "url", tab_content: { url: HELP_DOC_URL } }]
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
log.info("project", "onboard-tab", { name: project.name });
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
log.fail("project", err, { phase: "onboard-tab" });
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// src/project/lifecycle.ts
|
|
2918
|
+
async function createProject(channel, input2) {
|
|
2919
|
+
const name = input2.name.trim();
|
|
2920
|
+
if (!name) throw new Error("\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2921
|
+
if (await getProjectByName(name)) throw new Error(`\u9879\u76EE\u540D\u300C${name}\u300D\u5DF2\u5B58\u5728\uFF0C\u6362\u4E2A\u540D\u6216\u7528 /projects \u770B\u5DF2\u6709\u7684`);
|
|
2922
|
+
let cwd;
|
|
2923
|
+
let blank;
|
|
2924
|
+
if (input2.existingPath) {
|
|
2925
|
+
cwd = isAbsolute(input2.existingPath) ? input2.existingPath : resolve(input2.existingPath);
|
|
2926
|
+
if (!existsSync4(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
|
|
2927
|
+
blank = false;
|
|
2928
|
+
} else {
|
|
2929
|
+
cwd = join7(paths.projectsRootDir, name);
|
|
2930
|
+
await mkdir5(cwd, { recursive: true });
|
|
2931
|
+
blank = true;
|
|
2932
|
+
}
|
|
2933
|
+
const res = await channel.rawClient.im.v1.chat.create({
|
|
2934
|
+
params: { user_id_type: "open_id", set_bot_manager: true },
|
|
2935
|
+
data: { name, user_id_list: [input2.ownerOpenId] }
|
|
2936
|
+
});
|
|
2937
|
+
const chatId = res.data?.chat_id;
|
|
2938
|
+
if (!chatId) throw new Error(`\u5EFA\u7FA4\u5931\u8D25\uFF1A${JSON.stringify(res).slice(0, 200)}`);
|
|
2939
|
+
const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi" };
|
|
2940
|
+
await addProject(project);
|
|
2941
|
+
log.info("project", "create", { name, chatId, cwd, blank });
|
|
2942
|
+
await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
|
|
2943
|
+
await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
|
|
2944
|
+
return project;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// src/project/group-ops.ts
|
|
2948
|
+
async function transferOwnership(channel, chatId, toOpenId) {
|
|
2949
|
+
await channel.rawClient.im.v1.chat.update({
|
|
2950
|
+
path: { chat_id: chatId },
|
|
2951
|
+
params: { user_id_type: "open_id" },
|
|
2952
|
+
data: { owner_id: toOpenId }
|
|
2953
|
+
});
|
|
2954
|
+
log.info("project", "owner-transfer", { chatId: chatId.slice(-6), to: toOpenId.slice(-6) });
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
// src/bot/session-store.ts
|
|
2958
|
+
import { mkdir as mkdir6, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
2959
|
+
import { dirname as dirname5 } from "path";
|
|
2960
|
+
var FILE_VERSION3 = 1;
|
|
2961
|
+
async function read2() {
|
|
2962
|
+
try {
|
|
2963
|
+
const text = await readFile6(paths.sessionsFile, "utf8");
|
|
2964
|
+
const parsed = JSON.parse(text);
|
|
2965
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions : [];
|
|
2966
|
+
} catch (err) {
|
|
2967
|
+
if (err.code === "ENOENT") return [];
|
|
2968
|
+
throw err;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
async function write2(sessions) {
|
|
2972
|
+
await mkdir6(dirname5(paths.sessionsFile), { recursive: true });
|
|
2973
|
+
const tmp = `${paths.sessionsFile}.tmp-${process.pid}`;
|
|
2974
|
+
const body = { version: FILE_VERSION3, sessions };
|
|
2975
|
+
await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
|
|
2976
|
+
`, "utf8");
|
|
2977
|
+
await rename5(tmp, paths.sessionsFile);
|
|
2978
|
+
}
|
|
2979
|
+
async function listSessions() {
|
|
2980
|
+
return read2();
|
|
2981
|
+
}
|
|
2982
|
+
async function getSession(threadId) {
|
|
2983
|
+
return (await read2()).find((s) => s.threadId === threadId);
|
|
2984
|
+
}
|
|
2985
|
+
async function upsertSession(rec) {
|
|
2986
|
+
const sessions = await read2();
|
|
2987
|
+
const idx = sessions.findIndex((s) => s.threadId === rec.threadId);
|
|
2988
|
+
if (idx === -1) sessions.push(rec);
|
|
2989
|
+
else sessions[idx] = rec;
|
|
2990
|
+
await write2(sessions);
|
|
2991
|
+
}
|
|
2992
|
+
async function patchSession(threadId, patch) {
|
|
2993
|
+
const sessions = await read2();
|
|
2994
|
+
const rec = sessions.find((s) => s.threadId === threadId);
|
|
2995
|
+
if (!rec) return;
|
|
2996
|
+
const target = rec;
|
|
2997
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
2998
|
+
if (v !== void 0) target[k] = v;
|
|
2999
|
+
}
|
|
3000
|
+
rec.updatedAt = Date.now();
|
|
3001
|
+
await write2(sessions);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// src/bot/dm-console.ts
|
|
3005
|
+
async function handleDmConsole(channel, cfg, msg) {
|
|
3006
|
+
await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3007
|
+
if (!isAdmin(cfg, msg.senderId)) {
|
|
3008
|
+
log.info("console", "deny", { sender: msg.senderId.slice(-6) });
|
|
3009
|
+
await channel.send(msg.chatId, { markdown: "\u26D4 \u4EC5\u7BA1\u7406\u5458\u53EF\u5728\u79C1\u804A\u91CC\u7BA1\u7406\u9879\u76EE\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
await sendManagedCard(channel, msg.chatId, buildDmMenuCard(), msg.messageId).catch(
|
|
3013
|
+
(err) => log.fail("console", err, { cmd: "menu-send" })
|
|
3014
|
+
);
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// src/bot/comments.ts
|
|
3019
|
+
var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
|
|
3020
|
+
var REPLY_MAX_CHARS = 2e3;
|
|
3021
|
+
function apiErrCode(err) {
|
|
3022
|
+
return err?.response?.data?.code;
|
|
3023
|
+
}
|
|
3024
|
+
async function resolveComment(channel, evt) {
|
|
3025
|
+
if (!SUPPORTED_FILE_TYPES.has(evt.fileType)) return null;
|
|
3026
|
+
const direct = {
|
|
3027
|
+
fileToken: evt.fileToken,
|
|
3028
|
+
fileType: evt.fileType
|
|
3029
|
+
};
|
|
3030
|
+
const directCtx = await fetchCommentContext(channel, direct, evt).catch((err) => {
|
|
3031
|
+
if (apiErrCode(err) === 1069307) log.warn("comment", "no-access", { token: direct.fileToken });
|
|
3032
|
+
else log.fail("comment", err, { step: "fetch-direct" });
|
|
3033
|
+
return null;
|
|
3034
|
+
});
|
|
3035
|
+
if (directCtx?.question) return { target: direct, ctx: directCtx };
|
|
3036
|
+
const wiki = await resolveWikiNode(channel, evt.fileToken);
|
|
3037
|
+
if (wiki) {
|
|
3038
|
+
const wikiCtx = await fetchCommentContext(channel, wiki, evt).catch((err) => {
|
|
3039
|
+
log.fail("comment", err, { step: "fetch-wiki" });
|
|
3040
|
+
return null;
|
|
3041
|
+
});
|
|
3042
|
+
if (wikiCtx?.question) return { target: wiki, ctx: wikiCtx };
|
|
3043
|
+
}
|
|
3044
|
+
return null;
|
|
3045
|
+
}
|
|
3046
|
+
async function resolveWikiNode(channel, fileToken) {
|
|
3047
|
+
try {
|
|
3048
|
+
const r = await channel.rawClient.wiki.v2.space.getNode({ params: { token: fileToken } });
|
|
3049
|
+
const node = r?.data?.node;
|
|
3050
|
+
if (node?.obj_token && node.obj_type && SUPPORTED_FILE_TYPES.has(node.obj_type)) {
|
|
3051
|
+
log.info("comment", "wiki-resolved", { objToken: node.obj_token, objType: node.obj_type });
|
|
3052
|
+
return { fileToken: node.obj_token, fileType: node.obj_type };
|
|
3053
|
+
}
|
|
3054
|
+
} catch {
|
|
3055
|
+
}
|
|
3056
|
+
return null;
|
|
3057
|
+
}
|
|
3058
|
+
async function fetchCommentContext(channel, target, evt) {
|
|
3059
|
+
let replies = [];
|
|
3060
|
+
let quote;
|
|
3061
|
+
let isWhole = false;
|
|
3062
|
+
try {
|
|
3063
|
+
const r = await channel.rawClient.drive.v1.fileComment.get({
|
|
3064
|
+
params: { file_type: target.fileType },
|
|
3065
|
+
path: { file_token: target.fileToken, comment_id: evt.commentId }
|
|
3066
|
+
});
|
|
3067
|
+
replies = r?.data?.reply_list?.replies ?? [];
|
|
3068
|
+
quote = r?.data?.quote || void 0;
|
|
3069
|
+
isWhole = Boolean(r?.data?.is_whole);
|
|
3070
|
+
} catch (err) {
|
|
3071
|
+
log.warn("comment", "get-failed-fallback-list", { code: apiErrCode(err) });
|
|
3072
|
+
const found = await findCommentViaList(channel, target, evt.commentId);
|
|
3073
|
+
replies = found?.reply_list?.replies ?? [];
|
|
3074
|
+
quote = found?.quote || void 0;
|
|
3075
|
+
isWhole = Boolean(found?.is_whole);
|
|
3076
|
+
}
|
|
3077
|
+
const targetReply = (evt.replyId ? replies.find((rr) => rr.reply_id === evt.replyId) : void 0) ?? replies[replies.length - 1];
|
|
3078
|
+
const question = elementsToText(targetReply?.content?.elements ?? []);
|
|
3079
|
+
return { question, quote, isWhole, targetReplyId: targetReply?.reply_id };
|
|
3080
|
+
}
|
|
3081
|
+
function elementsToText(elements) {
|
|
3082
|
+
return elements.map((el) => {
|
|
3083
|
+
if (el.type === "text_run") return el.text_run?.text ?? "";
|
|
3084
|
+
if (el.type === "docs_link") return el.docs_link?.url ?? "";
|
|
3085
|
+
return "";
|
|
3086
|
+
}).join("").trim();
|
|
3087
|
+
}
|
|
3088
|
+
async function findCommentViaList(channel, target, commentId) {
|
|
3089
|
+
let pageToken;
|
|
3090
|
+
for (let page = 0; page < 10; page++) {
|
|
3091
|
+
const r = await channel.rawClient.drive.v1.fileComment.list({
|
|
3092
|
+
params: {
|
|
3093
|
+
file_type: target.fileType,
|
|
3094
|
+
page_size: 100,
|
|
3095
|
+
...pageToken ? { page_token: pageToken } : {}
|
|
3096
|
+
},
|
|
3097
|
+
path: { file_token: target.fileToken }
|
|
3098
|
+
});
|
|
3099
|
+
const items = r?.data?.items ?? [];
|
|
3100
|
+
const hit = items.find((it) => it.comment_id === commentId);
|
|
3101
|
+
if (hit) return hit;
|
|
3102
|
+
if (!r?.data?.has_more || !r.data.page_token) {
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
pageToken = r.data.page_token;
|
|
3106
|
+
}
|
|
3107
|
+
log.warn("comment", "comment-not-found-after-paging", { commentId, pagesScanned: 10 });
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
3110
|
+
var DOC_HOSTS = {
|
|
3111
|
+
feishu: "feishu.cn",
|
|
3112
|
+
lark: "larksuite.com"
|
|
3113
|
+
};
|
|
3114
|
+
function buildCommentPrompt(target, ctx, tenant) {
|
|
3115
|
+
const docUrl = `https://${DOC_HOSTS[tenant]}/${target.fileType}/${target.fileToken}`;
|
|
3116
|
+
const parts = [];
|
|
3117
|
+
parts.push("\u6211\u5728\u98DE\u4E66\u4E91\u6587\u6863\u7684\u8BC4\u8BBA\u91CC\u88AB @\u4E86\uFF0C\u9700\u8981\u4F60\u56DE\u7B54\u8BC4\u8BBA\u4E2D\u7684\u95EE\u9898\u3002\u6587\u6863\u4FE1\u606F\uFF1A");
|
|
3118
|
+
parts.push(`- \u94FE\u63A5\uFF1A${docUrl}`);
|
|
3119
|
+
parts.push(`- file_token\uFF1A${target.fileToken}`);
|
|
3120
|
+
parts.push(`- \u7C7B\u578B\uFF1A${target.fileType}`);
|
|
3121
|
+
parts.push(`- \u8BC4\u8BBA\u8303\u56F4\uFF1A${ctx.isWhole ? "\u5168\u6587\u8BC4\u8BBA\uFF08\u9488\u5BF9\u6574\u7BC7\u6587\u6863\uFF09" : "\u884C\u5185\u8BC4\u8BBA\uFF08\u9488\u5BF9\u9009\u4E2D\u7684\u6587\u5B57\uFF09"}`);
|
|
3122
|
+
if (ctx.quote) {
|
|
3123
|
+
parts.push("");
|
|
3124
|
+
parts.push(`\u7528\u6237\u9009\u4E2D\u7684\u539F\u6587\uFF1A
|
|
3125
|
+
> ${ctx.quote.replace(/\n/g, "\n> ")}`);
|
|
3126
|
+
}
|
|
3127
|
+
parts.push("");
|
|
3128
|
+
parts.push(`\u7528\u6237\u7684\u95EE\u9898\uFF1A${ctx.question}`);
|
|
3129
|
+
parts.push("");
|
|
3130
|
+
parts.push(
|
|
3131
|
+
`\u5982\u679C\u56DE\u7B54\u9700\u8981\u6587\u6863\u6B63\u6587\u5185\u5BB9\uFF0C\u53EF\u7528 lark-cli \u53EA\u8BFB\u5730\u83B7\u53D6\uFF08\u4EC5\u7528\u4E8E\u8BFB\u53D6\uFF0C\u4E0D\u8981\u7528\u5B83\u5199\u4EFB\u4F55\u4E1C\u897F\uFF09\uFF1A
|
|
3132
|
+
lark-cli docs +fetch --doc ${target.fileToken} --api-version v2`
|
|
3133
|
+
);
|
|
3134
|
+
parts.push("");
|
|
3135
|
+
parts.push("\u3010\u975E\u5E38\u91CD\u8981\uFF0C\u52A1\u5FC5\u9075\u5B88\u3011");
|
|
3136
|
+
parts.push(
|
|
3137
|
+
"1. \u4E0D\u8981\u81EA\u5DF1\u53BB\u53D1\u8868 / \u56DE\u590D / \u4FEE\u6539\u4EFB\u4F55\u98DE\u4E66\u8BC4\u8BBA\u6216\u6587\u6863\uFF08\u4E5F\u4E0D\u8981\u7528 lark-cli \u6216\u4EFB\u4F55\u5DE5\u5177\u53BB\u53D1\u8BC4\u8BBA\uFF09\u2014\u2014\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u628A\u4F60\u4E0B\u9762\u7ED9\u51FA\u7684\u6700\u7EC8\u56DE\u590D\u53D1\u5230\u8FD9\u6761\u8BC4\u8BBA\u91CC\uFF0C\u4F60\u53EA\u7BA1\u628A\u7B54\u6848\u5199\u51FA\u6765\u3002"
|
|
3138
|
+
);
|
|
3139
|
+
parts.push("2. \u53EA\u8F93\u51FA\u8981\u53D1\u7ED9\u7528\u6237\u7684\u300C\u6700\u7EC8\u7B54\u6848\u300D\u672C\u8EAB\uFF0C\u4E0D\u8981\u590D\u8FF0\u5206\u6790\u8FC7\u7A0B\u3001\u6B65\u9AA4\u3001\u6216\u300C\u6211\u73B0\u5728\u53BB\u2026\u300D\u8FD9\u7C7B\u8BF4\u660E\u3002");
|
|
3140
|
+
parts.push(
|
|
3141
|
+
"3. \u7528\u7EAF\u6587\u672C\uFF0C\u4E0D\u8981\u7528 markdown \u6807\u8BB0\uFF08\u4E0D\u8981 ** __ # - * > ` \u4E4B\u7C7B\uFF09\uFF0C\u4E0D\u8981\u4EE3\u7801\u5757\uFF1B\u8BC4\u8BBA\u6846\u4E0D\u6E32\u67D3 markdown\uFF0C\u4F1A\u539F\u6837\u663E\u793A\u8FD9\u4E9B\u7B26\u53F7\u3002\u56DE\u7B54\u7B80\u6D01\u76F4\u63A5\u3002"
|
|
3142
|
+
);
|
|
3143
|
+
return parts.join("\n");
|
|
3144
|
+
}
|
|
3145
|
+
function stripMarkdown(s) {
|
|
3146
|
+
return s.replace(/```[a-zA-Z]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/(?<![*\w])\*(?!\s)([^*\n]+?)(?<!\s)\*(?!\w)/g, "$1").replace(/(?<![_\w])_(?!\s)([^_\n]+?)(?<!\s)_(?!\w)/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/^[-*]\s+/gm, "").replace(/^>\s?/gm, "");
|
|
3147
|
+
}
|
|
3148
|
+
async function postCommentReply(channel, target, evt, text) {
|
|
3149
|
+
try {
|
|
3150
|
+
await channel.rawClient.drive.v1.fileCommentReply.create({
|
|
3151
|
+
params: { file_type: target.fileType },
|
|
3152
|
+
path: { file_token: target.fileToken, comment_id: evt.commentId },
|
|
3153
|
+
data: { content: { elements: [{ type: "text_run", text_run: { text } }] } }
|
|
3154
|
+
});
|
|
3155
|
+
log.info("comment", "replied", { mode: "in-thread" });
|
|
3156
|
+
return;
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
if (apiErrCode(err) !== 1069302) throw err;
|
|
3159
|
+
log.warn("comment", "reply-rejected-fallback-create", { code: 1069302 });
|
|
3160
|
+
}
|
|
3161
|
+
if (target.fileType !== "doc" && target.fileType !== "docx") {
|
|
3162
|
+
log.warn("comment", "whole-doc-reply-unsupported", { fileType: target.fileType });
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
await channel.rawClient.drive.v1.fileComment.create({
|
|
3166
|
+
params: { file_type: target.fileType },
|
|
3167
|
+
path: { file_token: target.fileToken },
|
|
3168
|
+
data: {
|
|
3169
|
+
reply_list: { replies: [{ content: { elements: [{ type: "text_run", text_run: { text } }] } }] }
|
|
3170
|
+
}
|
|
3171
|
+
});
|
|
3172
|
+
log.info("comment", "replied", { mode: "new-top-level" });
|
|
3173
|
+
}
|
|
3174
|
+
async function addCommentReaction(channel, target, replyId) {
|
|
3175
|
+
return commentReaction(channel, target, replyId, "add");
|
|
3176
|
+
}
|
|
3177
|
+
async function removeCommentReaction(channel, target, replyId) {
|
|
3178
|
+
await commentReaction(channel, target, replyId, "delete");
|
|
3179
|
+
}
|
|
3180
|
+
async function commentReaction(channel, target, replyId, action) {
|
|
3181
|
+
try {
|
|
3182
|
+
await channel.rawClient.drive.v2.commentReaction.updateReaction({
|
|
3183
|
+
params: { file_type: target.fileType },
|
|
3184
|
+
path: { file_token: target.fileToken },
|
|
3185
|
+
data: { action, reply_id: replyId, reaction_type: "Typing" }
|
|
3186
|
+
});
|
|
3187
|
+
log.info("comment", `reaction-${action}`, { fileToken: target.fileToken, replyId });
|
|
3188
|
+
return true;
|
|
3189
|
+
} catch (err) {
|
|
3190
|
+
log.warn("comment", `reaction-${action}-failed`, {
|
|
3191
|
+
replyId,
|
|
3192
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3193
|
+
});
|
|
3194
|
+
return false;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// src/bot/watchdog.ts
|
|
3199
|
+
async function* withIdleTimeout(source, idleMs, onTimeout, stop) {
|
|
3200
|
+
if ((!idleMs || idleMs <= 0) && !stop) {
|
|
3201
|
+
yield* source;
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const iter = source[Symbol.asyncIterator]();
|
|
3205
|
+
const stopRace = stop?.then(() => "__stop__");
|
|
3206
|
+
while (true) {
|
|
3207
|
+
let timer;
|
|
3208
|
+
const races = [iter.next()];
|
|
3209
|
+
if (idleMs && idleMs > 0) {
|
|
3210
|
+
races.push(new Promise((res) => {
|
|
3211
|
+
timer = setTimeout(() => res("__idle__"), idleMs);
|
|
3212
|
+
}));
|
|
3213
|
+
}
|
|
3214
|
+
if (stopRace) races.push(stopRace);
|
|
3215
|
+
const raced = await Promise.race(races);
|
|
3216
|
+
if (timer) clearTimeout(timer);
|
|
3217
|
+
if (raced === "__idle__") {
|
|
3218
|
+
onTimeout();
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
if (raced === "__stop__") return;
|
|
3222
|
+
const r = raced;
|
|
3223
|
+
if (r.done) return;
|
|
3224
|
+
yield r.value;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
var Semaphore = class {
|
|
3228
|
+
constructor(max) {
|
|
3229
|
+
this.max = max;
|
|
3230
|
+
}
|
|
3231
|
+
max;
|
|
3232
|
+
active = 0;
|
|
3233
|
+
waiters = [];
|
|
3234
|
+
/** True if acquire() would grant a slot without queueing. */
|
|
3235
|
+
hasFree() {
|
|
3236
|
+
return this.active < this.max;
|
|
3237
|
+
}
|
|
3238
|
+
async acquire() {
|
|
3239
|
+
if (this.active >= this.max) {
|
|
3240
|
+
await new Promise((res) => this.waiters.push(res));
|
|
3241
|
+
}
|
|
3242
|
+
this.active++;
|
|
3243
|
+
let released = false;
|
|
3244
|
+
return () => {
|
|
3245
|
+
if (released) return;
|
|
3246
|
+
released = true;
|
|
3247
|
+
this.active--;
|
|
3248
|
+
const next = this.waiters.shift();
|
|
3249
|
+
if (next) next();
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
};
|
|
3253
|
+
|
|
3254
|
+
// src/bot/handle-message.ts
|
|
3255
|
+
function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
3256
|
+
const backend = createBackend();
|
|
3257
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
3258
|
+
const active = /* @__PURE__ */ new Map();
|
|
3259
|
+
const docLocks = /* @__PURE__ */ new Map();
|
|
3260
|
+
const sema = new Semaphore(getMaxConcurrentRuns(cfg));
|
|
3261
|
+
const idleMs = getRunIdleTimeoutMs(cfg) ?? 0;
|
|
3262
|
+
const resumePending = /* @__PURE__ */ new Map();
|
|
3263
|
+
const modelPending = /* @__PURE__ */ new Map();
|
|
3264
|
+
const runsByCard = /* @__PURE__ */ new Map();
|
|
3265
|
+
const runCards = /* @__PURE__ */ new Map();
|
|
3266
|
+
const runStreams = /* @__PURE__ */ new Map();
|
|
3267
|
+
const lastRunCard = /* @__PURE__ */ new Map();
|
|
3268
|
+
let modelsCache = null;
|
|
3269
|
+
async function listModels() {
|
|
3270
|
+
if (!modelsCache) modelsCache = await backend.listModels();
|
|
3271
|
+
return modelsCache;
|
|
3272
|
+
}
|
|
3273
|
+
function pickDefault(models) {
|
|
3274
|
+
const def = models.find((m) => m.isDefault && !m.hidden) ?? models.find((m) => !m.hidden) ?? models[0];
|
|
3275
|
+
return { model: def?.id ?? "gpt-5.5", effort: def?.defaultEffort ?? "medium" };
|
|
3276
|
+
}
|
|
3277
|
+
async function addReaction(messageId, emoji) {
|
|
3278
|
+
try {
|
|
3279
|
+
const r = await channel.rawClient.im.v1.messageReaction.create({
|
|
3280
|
+
path: { message_id: messageId },
|
|
3281
|
+
data: { reaction_type: { emoji_type: emoji } }
|
|
3282
|
+
});
|
|
3283
|
+
return r.data?.reaction_id;
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
log.fail("card", err, { phase: "reaction-add", emoji });
|
|
3286
|
+
return void 0;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function removeReaction(messageId, reactionId) {
|
|
3290
|
+
void channel.rawClient.im.v1.messageReaction.delete({ path: { message_id: messageId, reaction_id: reactionId } }).catch((err) => log.fail("card", err, { phase: "reaction-del" }));
|
|
3291
|
+
}
|
|
3292
|
+
function runReaction(messageId, queued) {
|
|
3293
|
+
let chain = addReaction(messageId, queued ? "OneSecond" : "Typing");
|
|
3294
|
+
let phase = queued ? 0 : 1;
|
|
3295
|
+
const swap = (emoji) => {
|
|
3296
|
+
chain = chain.then(async (prevId) => {
|
|
3297
|
+
if (prevId) removeReaction(messageId, prevId);
|
|
3298
|
+
return addReaction(messageId, emoji);
|
|
3299
|
+
});
|
|
3300
|
+
};
|
|
3301
|
+
return {
|
|
3302
|
+
started: () => {
|
|
3303
|
+
if (phase < 1) {
|
|
3304
|
+
phase = 1;
|
|
3305
|
+
swap("Typing");
|
|
3306
|
+
}
|
|
3307
|
+
},
|
|
3308
|
+
done: () => {
|
|
3309
|
+
if (phase < 2) {
|
|
3310
|
+
phase = 2;
|
|
3311
|
+
chain = chain.then((prevId) => {
|
|
3312
|
+
if (prevId) removeReaction(messageId, prevId);
|
|
3313
|
+
return void 0;
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
const onMessage = async (msg) => {
|
|
3320
|
+
log.info("intake", "recv", {
|
|
3321
|
+
chatType: msg.chatType,
|
|
3322
|
+
mentionedBot: msg.mentionedBot,
|
|
3323
|
+
threadId: msg.threadId ?? null,
|
|
3324
|
+
preview: msg.content.slice(0, 40)
|
|
3325
|
+
});
|
|
3326
|
+
if (msg.chatType === "p2p") {
|
|
3327
|
+
await handleDmConsole(channel, cfg, msg);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
const project = await getProjectByChatId(msg.chatId);
|
|
3331
|
+
if (!msg.mentionedBot && !(project && shouldRespondWithoutMention(project, msg))) return;
|
|
3332
|
+
if (!isChatAllowed(cfg, msg.chatId) || !isUserAllowed(cfg, msg.senderId)) {
|
|
3333
|
+
log.info("intake", "reject", { reason: "not_allowed", chatId: msg.chatId.slice(-6) });
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
const text = msg.content.trim();
|
|
3337
|
+
const cmd = parseCommand(text);
|
|
3338
|
+
if ((project?.kind ?? "multi") === "single") {
|
|
3339
|
+
if (cmd === "help") {
|
|
3340
|
+
await postHelpCard(msg, "single");
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (cmd === "settings") {
|
|
3344
|
+
await postGroupSettings(msg, project);
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
if (cmd === "model") {
|
|
3348
|
+
await postModelCard(msg, msg.chatId);
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
handleTurn(msg, text, msg.chatId, true, project);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
if (msg.threadId) {
|
|
3355
|
+
if (cmd === "help") {
|
|
3356
|
+
await postHelpCard(msg, "topic", true);
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
if (cmd === "model") {
|
|
3360
|
+
await postModelCard(msg, msg.threadId);
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
handleTurn(msg, text, msg.threadId, false, project);
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
if (cmd === "help") {
|
|
3367
|
+
await postHelpCard(msg, "main");
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
if (cmd === "resume") {
|
|
3371
|
+
await postResumeCard(msg);
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
if (cmd === "settings") {
|
|
3375
|
+
await postGroupSettings(msg, project);
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
if (cmd === "model") {
|
|
3379
|
+
await channel.send(msg.chatId, { markdown: "`/model` \u9700\u8981\u5728\u8BDD\u9898\u91CC\u4F7F\u7528\uFF08\u5148 @\u6211 \u5F00\u4E2A\u8BDD\u9898\uFF09\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
startTopicDirectly(msg, text, project);
|
|
3383
|
+
};
|
|
3384
|
+
function parseCommand(text) {
|
|
3385
|
+
const m = /^\/(\w+)/.exec(text);
|
|
3386
|
+
const name = m?.[1]?.toLowerCase();
|
|
3387
|
+
return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
|
|
3388
|
+
}
|
|
3389
|
+
function shouldRespondWithoutMention(project, msg) {
|
|
3390
|
+
if (!(project.noMention ?? true)) return false;
|
|
3391
|
+
if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
|
|
3392
|
+
if ((project.kind ?? "multi") === "single") return true;
|
|
3393
|
+
return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
|
|
3394
|
+
}
|
|
3395
|
+
async function postGroupSettings(msg, project) {
|
|
3396
|
+
if (!isAdmin(cfg, msg.senderId)) {
|
|
3397
|
+
await channel.send(msg.chatId, { markdown: "\u4EC5\u7BA1\u7406\u5458\u53EF\u6539\u7FA4\u8BBE\u7F6E\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
if (!project) {
|
|
3401
|
+
await channel.send(msg.chatId, { markdown: "\u672C\u7FA4\u672A\u7ED1\u5B9A\u9879\u76EE\uFF0C\u8BF7\u5148\u5728\u79C1\u804A\u91CC\u65B0\u5EFA\u9879\u76EE\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3405
|
+
await sendManagedCard(channel, msg.chatId, buildGroupSettingsCard(project), msg.messageId);
|
|
3406
|
+
log.info("card", "group-settings", { project: project.name });
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
async function handleTurn(msg, text, sessionKey, flat, project) {
|
|
3410
|
+
const existing = active.get(sessionKey);
|
|
3411
|
+
if (existing) {
|
|
3412
|
+
if (getPendingPolicy(cfg) === "steer" && existing.run && existing.thread) {
|
|
3413
|
+
const tid = existing.run.turnId();
|
|
3414
|
+
if (tid) {
|
|
3415
|
+
try {
|
|
3416
|
+
await existing.thread.steer({ text }, tid);
|
|
3417
|
+
log.info("intake", "steer", { tid });
|
|
3418
|
+
return;
|
|
3419
|
+
} catch (err) {
|
|
3420
|
+
log.warn("intake", "steer-failed", { err: String(err) });
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
existing.queue.push(text);
|
|
3425
|
+
log.info("intake", "queued", { depth: existing.queue.length });
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
const reserved = { queue: [], requesterOpenId: msg.senderId };
|
|
3429
|
+
active.set(sessionKey, reserved);
|
|
3430
|
+
void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3431
|
+
const reaction = runReaction(msg.messageId, !sema.hasFree());
|
|
3432
|
+
try {
|
|
3433
|
+
let thread = await resolveThread(sessionKey, msg.chatId);
|
|
3434
|
+
if (!thread) {
|
|
3435
|
+
const cwd = project?.cwd ?? fallbackCwd;
|
|
3436
|
+
thread = await backend.startThread({ cwd });
|
|
3437
|
+
sessions.set(sessionKey, thread);
|
|
3438
|
+
await upsertSession({
|
|
3439
|
+
threadId: sessionKey,
|
|
3440
|
+
chatId: msg.chatId,
|
|
3441
|
+
cwd,
|
|
3442
|
+
codexThreadId: thread.codexThreadId,
|
|
3443
|
+
summary: text.slice(0, 80),
|
|
3444
|
+
createdAt: Date.now(),
|
|
3445
|
+
updatedAt: Date.now()
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
reserved.thread = thread;
|
|
3449
|
+
await launchRun(
|
|
3450
|
+
{
|
|
3451
|
+
chatId: msg.chatId,
|
|
3452
|
+
replyTo: msg.messageId,
|
|
3453
|
+
replyInThread: !flat,
|
|
3454
|
+
flat,
|
|
3455
|
+
thread,
|
|
3456
|
+
firstText: text,
|
|
3457
|
+
knownThreadId: sessionKey,
|
|
3458
|
+
requesterOpenId: msg.senderId
|
|
3459
|
+
},
|
|
3460
|
+
reaction
|
|
3461
|
+
);
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
active.delete(sessionKey);
|
|
3464
|
+
reaction.done();
|
|
3465
|
+
log.fail("intake", err);
|
|
3466
|
+
await channel.send(msg.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId, replyInThread: !flat }).catch(() => void 0);
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
async function resolveThread(threadId, chatId) {
|
|
3471
|
+
const live = sessions.get(threadId);
|
|
3472
|
+
if (live) return live;
|
|
3473
|
+
const rec = await getSession(threadId);
|
|
3474
|
+
if (!rec) return void 0;
|
|
3475
|
+
try {
|
|
3476
|
+
const resumed = await backend.resumeThread({
|
|
3477
|
+
cwd: rec.cwd,
|
|
3478
|
+
codexThreadId: rec.codexThreadId,
|
|
3479
|
+
model: rec.model,
|
|
3480
|
+
effort: rec.effort
|
|
3481
|
+
});
|
|
3482
|
+
sessions.set(threadId, resumed);
|
|
3483
|
+
return resumed;
|
|
3484
|
+
} catch (err) {
|
|
3485
|
+
log.fail("agent", err, { phase: "resume-on-turn", threadId });
|
|
3486
|
+
const project = await getProjectByChatId(chatId);
|
|
3487
|
+
const cwd = project?.cwd ?? rec.cwd ?? fallbackCwd;
|
|
3488
|
+
const fresh = await backend.startThread({ cwd, model: rec.model, effort: rec.effort });
|
|
3489
|
+
sessions.set(threadId, fresh);
|
|
3490
|
+
return fresh;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
function startTopicDirectly(msg, text, project) {
|
|
3494
|
+
void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3495
|
+
const reaction = runReaction(msg.messageId, !sema.hasFree());
|
|
3496
|
+
const cwd = project?.cwd ?? fallbackCwd;
|
|
3497
|
+
if (project) void refreshBranch(channel, project).catch(() => void 0);
|
|
3498
|
+
const { model, effort } = pickDefault(await listModels());
|
|
3499
|
+
let thread;
|
|
3500
|
+
try {
|
|
3501
|
+
thread = await backend.startThread({ cwd, model, effort });
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
reaction.done();
|
|
3504
|
+
log.fail("card", err, { phase: "start-topic" });
|
|
3505
|
+
await channel.send(msg.chatId, { markdown: `\u274C \u542F\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
const firstText = text || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
|
|
3509
|
+
log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort });
|
|
3510
|
+
await launchRun(
|
|
3511
|
+
{
|
|
3512
|
+
chatId: msg.chatId,
|
|
3513
|
+
replyTo: msg.messageId,
|
|
3514
|
+
replyInThread: true,
|
|
3515
|
+
thread,
|
|
3516
|
+
firstText,
|
|
3517
|
+
model,
|
|
3518
|
+
effort,
|
|
3519
|
+
cwd,
|
|
3520
|
+
summary: text.slice(0, 80) || "(\u7A7A)",
|
|
3521
|
+
requesterOpenId: msg.senderId
|
|
3522
|
+
},
|
|
3523
|
+
reaction,
|
|
3524
|
+
() => reaction.done()
|
|
3525
|
+
// topic created → ✅ DONE (don't wait for the reply)
|
|
3526
|
+
);
|
|
3527
|
+
}).catch((err) => log.fail("intake", err));
|
|
3528
|
+
}
|
|
3529
|
+
async function postResumeCard(msg) {
|
|
3530
|
+
await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3531
|
+
const project = await getProjectByChatId(msg.chatId);
|
|
3532
|
+
const cwd = project?.cwd ?? fallbackCwd;
|
|
3533
|
+
const threads = await backend.listThreads(cwd);
|
|
3534
|
+
const state = {
|
|
3535
|
+
chatId: msg.chatId,
|
|
3536
|
+
originalMsgId: msg.messageId,
|
|
3537
|
+
requesterOpenId: msg.senderId,
|
|
3538
|
+
cwd,
|
|
3539
|
+
projectName: project?.name,
|
|
3540
|
+
threads,
|
|
3541
|
+
createdAt: Date.now()
|
|
3542
|
+
};
|
|
3543
|
+
const res = await sendManagedCard(channel, msg.chatId, buildResumeCard(state), msg.messageId);
|
|
3544
|
+
pruneResumePending();
|
|
3545
|
+
resumePending.set(res.messageId, state);
|
|
3546
|
+
log.info("card", "resume", { project: project?.name ?? "(unregistered)", threads: threads.length });
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
async function postModelCard(msg, sessionKey) {
|
|
3550
|
+
await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3551
|
+
const [models, rec] = await Promise.all([listModels(), getSession(sessionKey)]);
|
|
3552
|
+
const def = pickDefault(models);
|
|
3553
|
+
const state = {
|
|
3554
|
+
chatId: msg.chatId,
|
|
3555
|
+
threadId: sessionKey,
|
|
3556
|
+
requesterOpenId: msg.senderId,
|
|
3557
|
+
models,
|
|
3558
|
+
model: rec?.model ?? def.model,
|
|
3559
|
+
effort: rec?.effort ?? def.effort,
|
|
3560
|
+
createdAt: Date.now()
|
|
3561
|
+
};
|
|
3562
|
+
const res = await sendManagedCard(channel, msg.chatId, buildModelCard(state), msg.messageId, true);
|
|
3563
|
+
pruneModelPending();
|
|
3564
|
+
modelPending.set(res.messageId, state);
|
|
3565
|
+
log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
async function postHelpCard(msg, scope, inThread = false) {
|
|
3569
|
+
await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
|
|
3570
|
+
await sendManagedCard(channel, msg.chatId, buildHelpCard(scope), msg.messageId, inThread).catch(
|
|
3571
|
+
(err) => log.fail("card", err, { cmd: "help", scope })
|
|
3572
|
+
);
|
|
3573
|
+
log.info("card", "help", { scope });
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
const dispatcher = new CardDispatcher(channel, cfg);
|
|
3577
|
+
const PENDING_TTL_MS = 30 * 6e4;
|
|
3578
|
+
const CARD_SETTLE_MS = 500;
|
|
3579
|
+
const settleUpdate = (msgId, c, fallbackChatId) => {
|
|
3580
|
+
const armedAt = Date.now();
|
|
3581
|
+
void (async () => {
|
|
3582
|
+
await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
|
|
3583
|
+
const card2 = typeof c === "function" ? await c() : c;
|
|
3584
|
+
const ok = await updateManagedCard(channel, msgId, card2);
|
|
3585
|
+
log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
|
|
3586
|
+
if (!ok && fallbackChatId) {
|
|
3587
|
+
await sendManagedCard(channel, fallbackChatId, card2).catch(
|
|
3588
|
+
(err) => log.fail("console", err, { phase: "settle-fallback" })
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
})();
|
|
3592
|
+
};
|
|
3593
|
+
function pruneResumePending() {
|
|
3594
|
+
const now = Date.now();
|
|
3595
|
+
for (const [k, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k);
|
|
3596
|
+
}
|
|
3597
|
+
function pruneModelPending() {
|
|
3598
|
+
const now = Date.now();
|
|
3599
|
+
for (const [k, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k);
|
|
3600
|
+
}
|
|
3601
|
+
function authPending(map, evt) {
|
|
3602
|
+
const state = map.get(evt.messageId);
|
|
3603
|
+
if (!state) return void 0;
|
|
3604
|
+
if (Date.now() - state.createdAt > PENDING_TTL_MS) {
|
|
3605
|
+
map.delete(evt.messageId);
|
|
3606
|
+
return void 0;
|
|
3607
|
+
}
|
|
3608
|
+
const op = evt.operator?.openId ?? "";
|
|
3609
|
+
if (op !== state.requesterOpenId || !isChatAllowed(cfg, state.chatId) || !isUserAllowed(cfg, op)) {
|
|
3610
|
+
log.info("card", "action-denied", { reason: "not-allowed" });
|
|
3611
|
+
return void 0;
|
|
3612
|
+
}
|
|
3613
|
+
return state;
|
|
3614
|
+
}
|
|
3615
|
+
dispatcher.on(MC.model, ({ evt, option }) => {
|
|
3616
|
+
const state = authPending(modelPending, evt);
|
|
3617
|
+
if (!state || !option) return;
|
|
3618
|
+
settleUpdate(evt.messageId, async () => {
|
|
3619
|
+
state.model = option;
|
|
3620
|
+
const m = state.models.find((x) => x.id === option);
|
|
3621
|
+
if (m && m.supportedEfforts.length && !m.supportedEfforts.includes(state.effort)) {
|
|
3622
|
+
state.effort = m.defaultEffort;
|
|
3623
|
+
}
|
|
3624
|
+
await patchSession(state.threadId, { model: state.model, effort: state.effort });
|
|
3625
|
+
state.note = `\u2705 \u5DF2\u5207\u6362\u6A21\u578B\u300C${m?.displayName ?? option}\u300D\uFF0C\u4E0B\u4E00\u8F6E\u751F\u6548`;
|
|
3626
|
+
return buildModelCard(state);
|
|
3627
|
+
});
|
|
3628
|
+
}).on(MC.effort, ({ evt, option }) => {
|
|
3629
|
+
const state = authPending(modelPending, evt);
|
|
3630
|
+
if (!state || !option) return;
|
|
3631
|
+
settleUpdate(evt.messageId, async () => {
|
|
3632
|
+
state.effort = option;
|
|
3633
|
+
await patchSession(state.threadId, { effort: state.effort });
|
|
3634
|
+
state.note = "\u2705 \u5DF2\u8BBE\u7F6E effort\uFF0C\u4E0B\u4E00\u8F6E\u751F\u6548";
|
|
3635
|
+
return buildModelCard(state);
|
|
3636
|
+
});
|
|
3637
|
+
}).on(RES.pick, ({ evt, value }) => {
|
|
3638
|
+
const state = authPending(resumePending, evt);
|
|
3639
|
+
const codexThreadId = typeof value.t === "string" ? value.t : void 0;
|
|
3640
|
+
if (!state || !codexThreadId || state.launching) return;
|
|
3641
|
+
state.launching = true;
|
|
3642
|
+
settleUpdate(evt.messageId, buildResumeLaunchingCard(state));
|
|
3643
|
+
void resumeFromCard(evt, state, codexThreadId);
|
|
3644
|
+
});
|
|
3645
|
+
const runAllowed = (evt) => isChatAllowed(cfg, evt.chatId) && isUserAllowed(cfg, evt.operator?.openId ?? "");
|
|
3646
|
+
const runOwnerOrAdmin = (evt, ownerOpenId) => {
|
|
3647
|
+
if (!runAllowed(evt)) return false;
|
|
3648
|
+
const op = evt.operator?.openId ?? "";
|
|
3649
|
+
return op === ownerOpenId || isAdmin(cfg, op);
|
|
3650
|
+
};
|
|
3651
|
+
dispatcher.on(RC.stop, ({ evt, value }) => {
|
|
3652
|
+
const key = typeof value.m === "string" ? value.m : evt.messageId;
|
|
3653
|
+
const st = runsByCard.get(key);
|
|
3654
|
+
if (!st || !runOwnerOrAdmin(evt, st.requesterOpenId)) return;
|
|
3655
|
+
st.interrupt?.();
|
|
3656
|
+
log.info("card", "action", { actionId: "run.stop", stopped: Boolean(st.interrupt) });
|
|
3657
|
+
});
|
|
3658
|
+
const dmAdmin = (openId) => isAdmin(cfg, openId ?? "");
|
|
3659
|
+
const patch = (evt, c) => settleUpdate(evt.messageId, c, evt.chatId);
|
|
3660
|
+
function applyPref(evt, mut) {
|
|
3661
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
3662
|
+
const prefs = { ...cfg.preferences ?? {} };
|
|
3663
|
+
mut(prefs);
|
|
3664
|
+
cfg.preferences = prefs;
|
|
3665
|
+
void saveConfig(cfg).catch((err) => log.fail("console", err, { phase: "save-config" }));
|
|
3666
|
+
patch(evt, buildSettingsCard(cfg));
|
|
3667
|
+
}
|
|
3668
|
+
const freshMenu = (evt) => {
|
|
3669
|
+
patch(evt, buildDmMenuCard());
|
|
3670
|
+
};
|
|
3671
|
+
const renderProjectList = async () => {
|
|
3672
|
+
const [projects, sessions2] = await Promise.all([listProjects(), listSessions()]);
|
|
3673
|
+
const byChat = /* @__PURE__ */ new Map();
|
|
3674
|
+
for (const s of sessions2) {
|
|
3675
|
+
const arr = byChat.get(s.chatId);
|
|
3676
|
+
if (arr) arr.push(s);
|
|
3677
|
+
else byChat.set(s.chatId, [s]);
|
|
3678
|
+
}
|
|
3679
|
+
return buildProjectListCard(projects, byChat);
|
|
3680
|
+
};
|
|
3681
|
+
dispatcher.on(DM.menu, ({ evt }) => {
|
|
3682
|
+
if (dmAdmin(evt.operator?.openId)) freshMenu(evt);
|
|
3683
|
+
}).on(DM.newProject, ({ evt }) => {
|
|
3684
|
+
if (dmAdmin(evt.operator?.openId)) patch(evt, buildNewProjectFormCard());
|
|
3685
|
+
}).on(DM.newProjectSubmit, ({ evt, formValue, value }) => {
|
|
3686
|
+
const op = evt.operator?.openId;
|
|
3687
|
+
if (!dmAdmin(op)) return;
|
|
3688
|
+
const name = String(formValue?.name ?? "").trim();
|
|
3689
|
+
const cwdIn = String(formValue?.cwd ?? "").trim();
|
|
3690
|
+
const kind = value.kind === "single" ? "single" : "multi";
|
|
3691
|
+
void (async () => {
|
|
3692
|
+
let result;
|
|
3693
|
+
if (!name) result = buildNewProjectFormCard({ cwd: cwdIn, error: "\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3694
|
+
else if (!op) result = buildNewProjectFormCard({ name, cwd: cwdIn, error: "\u65E0\u6CD5\u8BC6\u522B\u64CD\u4F5C\u8005\u8EAB\u4EFD" });
|
|
3695
|
+
else {
|
|
3696
|
+
try {
|
|
3697
|
+
const p = await createProject(channel, { name, ownerOpenId: op, existingPath: cwdIn || void 0, kind });
|
|
3698
|
+
log.info("console", "new-project", { name: p.name, blank: p.blank });
|
|
3699
|
+
result = buildNewProjectDoneCard(p);
|
|
3700
|
+
} catch (err) {
|
|
3701
|
+
result = buildNewProjectFormCard({ name, cwd: cwdIn, error: err instanceof Error ? err.message : String(err) });
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
await sendManagedCard(channel, evt.chatId, result).catch(
|
|
3705
|
+
(e) => log.fail("console", e, { phase: "new-project-result" })
|
|
3706
|
+
);
|
|
3707
|
+
})();
|
|
3708
|
+
}).on(DM.projects, ({ evt }) => {
|
|
3709
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
3710
|
+
patch(evt, renderProjectList);
|
|
3711
|
+
}).on(DM.settings, async ({ evt }) => {
|
|
3712
|
+
if (dmAdmin(evt.operator?.openId)) await patch(evt, buildSettingsCard(cfg));
|
|
3713
|
+
}).on(DM.doctor, async ({ evt }) => {
|
|
3714
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
3715
|
+
const ok = await backend.isAvailable().catch(() => false);
|
|
3716
|
+
const conn = channel.getConnectionStatus?.()?.state ?? "unknown";
|
|
3717
|
+
await channel.send(evt.chatId, { markdown: `\u{1FA7A} **\u8BCA\u65AD**
|
|
3718
|
+
- codex: ${ok ? "\u2705 \u53EF\u7528" : "\u274C \u4E0D\u53EF\u7528\uFF08\u68C0\u67E5 CODEX_BIN/PATH\uFF09"}
|
|
3719
|
+
- \u957F\u8FDE\u63A5: ${conn}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
3720
|
+
}).on(DM.reconnect, async ({ evt }) => {
|
|
3721
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
3722
|
+
const conn = channel.getConnectionStatus?.()?.state ?? "unknown";
|
|
3723
|
+
await channel.send(evt.chatId, { markdown: `\u{1F504} \u957F\u8FDE\u63A5\u72B6\u6001\uFF1A**${conn}**
|
|
3724
|
+
SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8BF7\u5728\u7EC8\u7AEF\u91CD\u8DD1 \`feishu-codex-bridge run\`\uFF08\u524D\u53F0\uFF09\u6216 \`feishu-codex-bridge restart\`\uFF08\u540E\u53F0\u5B88\u62A4\uFF09\u3002` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
3725
|
+
}).on(DM.rmConfirm, async ({ evt, value }) => {
|
|
3726
|
+
const name = typeof value.n === "string" ? value.n : void 0;
|
|
3727
|
+
if (!dmAdmin(evt.operator?.openId) || !name) return;
|
|
3728
|
+
await patch(evt, buildRmConfirmCard(name));
|
|
3729
|
+
}).on(DM.rmCancel, ({ evt }) => {
|
|
3730
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
3731
|
+
patch(evt, renderProjectList);
|
|
3732
|
+
}).on(DM.rmDo, ({ evt, value }) => {
|
|
3733
|
+
const name = typeof value.n === "string" ? value.n : void 0;
|
|
3734
|
+
const op = evt.operator?.openId;
|
|
3735
|
+
if (!dmAdmin(op) || !name) return;
|
|
3736
|
+
patch(evt, async () => {
|
|
3737
|
+
const removed = await removeProject(name);
|
|
3738
|
+
let transferred = false;
|
|
3739
|
+
if (removed?.chatId && op) {
|
|
3740
|
+
transferred = await transferOwnership(channel, removed.chatId, op).then(() => true).catch((err) => {
|
|
3741
|
+
log.fail("console", err, { phase: "owner-transfer" });
|
|
3742
|
+
return false;
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
log.info("console", "rm", { name, transferred });
|
|
3746
|
+
const tail = transferred ? "\u7FA4\u4E3B\u5DF2\u8F6C\u7ED9\u4F60 \u2192 \u8BF7\u5728\u98DE\u4E66\u91CC**\u81EA\u884C\u89E3\u6563\u8BE5\u7FA4**\uFF08\u673A\u5668\u4EBA\u4E0D\u4E3B\u52A8\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u7FA4\u4E3B\u8F6C\u8BA9\u5931\u8D25\uFF08\u53EF\u80FD bot \u975E\u7FA4\u4E3B\uFF09\uFF0C\u8BF7\u7528\u300C\u{1F6AA} \u7FA4\u7BA1\u7406\u300D\u624B\u52A8\u8F6C\u8BA9\u540E\u89E3\u6563\u3002";
|
|
3747
|
+
await channel.send(evt.chatId, { markdown: `\u2705 \u5DF2\u5220\u9664\u9879\u76EE\u300C${name}\u300D\uFF08\u89E3\u7ED1\uFF0C\u672A\u5220\u4EE3\u7801\u76EE\u5F55\uFF09\u3002
|
|
3748
|
+
${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
3749
|
+
return renderProjectList();
|
|
3750
|
+
});
|
|
3751
|
+
}).on(DM.setTools, ({ evt, value }) => {
|
|
3752
|
+
applyPref(evt, (p) => p.showToolCalls = value.v === "on");
|
|
3753
|
+
}).on(DM.setWatchdog, ({ evt, value }) => {
|
|
3754
|
+
const n = Number(value.v);
|
|
3755
|
+
if (Number.isFinite(n)) applyPref(evt, (p) => p.runIdleTimeoutSeconds = n);
|
|
3756
|
+
}).on(DM.setPending, ({ evt, value }) => {
|
|
3757
|
+
if (value.v === "steer" || value.v === "queue") applyPref(evt, (p) => p.pendingPolicy = value.v);
|
|
3758
|
+
}).on(DM.setConcurrency, ({ evt, value }) => {
|
|
3759
|
+
const n = Number(value.v);
|
|
3760
|
+
if (Number.isFinite(n)) applyPref(evt, (p) => p.maxConcurrentRuns = n);
|
|
3761
|
+
}).on(GS.setNoMention, ({ evt, value }) => {
|
|
3762
|
+
if (!isAdmin(cfg, evt.operator?.openId ?? "")) return;
|
|
3763
|
+
const on = value.v === "on";
|
|
3764
|
+
patch(evt, async () => {
|
|
3765
|
+
const project = await getProjectByChatId(evt.chatId);
|
|
3766
|
+
if (project) {
|
|
3767
|
+
await updateProject(project.name, { noMention: on });
|
|
3768
|
+
log.info("console", "group-nomention", { project: project.name, on });
|
|
3769
|
+
return buildGroupSettingsCard({ ...project, noMention: on });
|
|
3770
|
+
}
|
|
3771
|
+
return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", noMention: on });
|
|
3772
|
+
});
|
|
3773
|
+
});
|
|
3774
|
+
async function resumeFromCard(evt, state, codexThreadId) {
|
|
3775
|
+
try {
|
|
3776
|
+
const history = await backend.readHistory(state.cwd, codexThreadId);
|
|
3777
|
+
resumePending.delete(evt.messageId);
|
|
3778
|
+
let bound = false;
|
|
3779
|
+
await withTrace({ chatId: state.chatId, msgId: state.originalMsgId }, async () => {
|
|
3780
|
+
const cardState = { cwd: state.cwd, projectName: state.projectName, history };
|
|
3781
|
+
const sent = await sendManagedCard(channel, state.chatId, buildHistoryCard(cardState), state.originalMsgId, true);
|
|
3782
|
+
let tid;
|
|
3783
|
+
for (let attempt = 0; attempt < 4 && !tid; attempt++) {
|
|
3784
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, 500));
|
|
3785
|
+
tid = await getThreadId(channel, sent.messageId);
|
|
3786
|
+
}
|
|
3787
|
+
if (tid) {
|
|
3788
|
+
const now = Date.now();
|
|
3789
|
+
await upsertSession({
|
|
3790
|
+
threadId: tid,
|
|
3791
|
+
chatId: state.chatId,
|
|
3792
|
+
cwd: state.cwd,
|
|
3793
|
+
codexThreadId,
|
|
3794
|
+
summary: history.name || history.preview || "(\u6062\u590D\u4F1A\u8BDD)",
|
|
3795
|
+
createdAt: now,
|
|
3796
|
+
updatedAt: now
|
|
3797
|
+
});
|
|
3798
|
+
bound = true;
|
|
3799
|
+
} else {
|
|
3800
|
+
log.warn("card", "resume-no-threadid", { messageId: sent.messageId });
|
|
3801
|
+
}
|
|
3802
|
+
log.info("card", "resume-done", { codexThreadId, threadId: tid ?? null, bound, turns: history.totalTurns });
|
|
3803
|
+
});
|
|
3804
|
+
settleUpdate(
|
|
3805
|
+
evt.messageId,
|
|
3806
|
+
bound ? buildResumeDoneCard(state) : buildResumeErrorCard(state, "\u5DF2\u5EFA\u8BDD\u9898\u4F46\u672A\u80FD\u7ED1\u5B9A\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u65B0 /resume")
|
|
3807
|
+
);
|
|
3808
|
+
} catch (err) {
|
|
3809
|
+
state.launching = false;
|
|
3810
|
+
log.fail("card", err, { phase: "resume-launch" });
|
|
3811
|
+
settleUpdate(evt.messageId, buildResumeErrorCard(state, err instanceof Error ? err.message : String(err)));
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
async function launchRun(opts, reaction, onTopicCreated) {
|
|
3815
|
+
const release = await sema.acquire();
|
|
3816
|
+
reaction?.started();
|
|
3817
|
+
let firstCardSent = false;
|
|
3818
|
+
let activeKey = opts.knownThreadId ?? `pending:${opts.replyTo}`;
|
|
3819
|
+
let topicThreadId = opts.knownThreadId;
|
|
3820
|
+
const state = active.get(activeKey) ?? { queue: [], requesterOpenId: opts.requesterOpenId };
|
|
3821
|
+
state.thread = opts.thread;
|
|
3822
|
+
if (opts.requesterOpenId) state.requesterOpenId = opts.requesterOpenId;
|
|
3823
|
+
active.set(activeKey, state);
|
|
3824
|
+
if (opts.knownThreadId) sessions.set(opts.knownThreadId, opts.thread);
|
|
3825
|
+
const persist = async (threadId) => {
|
|
3826
|
+
await upsertSession({
|
|
3827
|
+
threadId,
|
|
3828
|
+
chatId: opts.chatId,
|
|
3829
|
+
cwd: opts.cwd ?? fallbackCwd,
|
|
3830
|
+
codexThreadId: opts.thread.codexThreadId,
|
|
3831
|
+
model: opts.model,
|
|
3832
|
+
effort: opts.effort,
|
|
3833
|
+
summary: opts.summary ?? opts.firstText.slice(0, 80),
|
|
3834
|
+
createdAt: Date.now(),
|
|
3835
|
+
updatedAt: Date.now()
|
|
3836
|
+
}).catch(() => void 0);
|
|
3837
|
+
};
|
|
3838
|
+
const promoteCard = (cardMsgId, rc) => {
|
|
3839
|
+
if (!topicThreadId) return;
|
|
3840
|
+
const prev = lastRunCard.get(topicThreadId);
|
|
3841
|
+
if (prev && prev !== cardMsgId) {
|
|
3842
|
+
const prevState = runCards.get(prev);
|
|
3843
|
+
const prevStream = runStreams.get(prev);
|
|
3844
|
+
if (prevState && prevStream) void prevStream.updateCard(channel, buildRunCardPlain(prevState));
|
|
3845
|
+
runCards.delete(prev);
|
|
3846
|
+
runStreams.delete(prev);
|
|
3847
|
+
}
|
|
3848
|
+
lastRunCard.set(topicThreadId, cardMsgId);
|
|
3849
|
+
runCards.set(cardMsgId, rc);
|
|
3850
|
+
};
|
|
3851
|
+
let curCardKey;
|
|
3852
|
+
try {
|
|
3853
|
+
let turnText = opts.firstText;
|
|
3854
|
+
let replyTo = opts.replyTo;
|
|
3855
|
+
let replyInThread = opts.flat ? false : opts.replyInThread ?? Boolean(opts.knownThreadId);
|
|
3856
|
+
for (; ; ) {
|
|
3857
|
+
const rec = topicThreadId ? await getSession(topicThreadId) : void 0;
|
|
3858
|
+
const turnModel = rec?.model ?? opts.model;
|
|
3859
|
+
const turnEffort = rec?.effort ?? opts.effort;
|
|
3860
|
+
const run = opts.thread.runStreamed({ text: turnText }, { model: turnModel, effort: turnEffort });
|
|
3861
|
+
state.run = run;
|
|
3862
|
+
const render = new RunRender();
|
|
3863
|
+
render.showTools = getShowToolCalls(cfg);
|
|
3864
|
+
let cardMsgId;
|
|
3865
|
+
const rc = {
|
|
3866
|
+
rs: render.snapshot(),
|
|
3867
|
+
requesterOpenId: opts.requesterOpenId,
|
|
3868
|
+
showTools: render.showTools
|
|
3869
|
+
};
|
|
3870
|
+
const adoptThreadId = async (messageId) => {
|
|
3871
|
+
if (activeKey.startsWith("pending:")) {
|
|
3872
|
+
const tid = await getThreadId(channel, messageId);
|
|
3873
|
+
if (tid) {
|
|
3874
|
+
active.delete(activeKey);
|
|
3875
|
+
active.set(tid, state);
|
|
3876
|
+
sessions.set(tid, opts.thread);
|
|
3877
|
+
activeKey = tid;
|
|
3878
|
+
topicThreadId = tid;
|
|
3879
|
+
rc.threadId = tid;
|
|
3880
|
+
await persist(tid);
|
|
3881
|
+
}
|
|
3882
|
+
} else {
|
|
3883
|
+
topicThreadId = activeKey;
|
|
3884
|
+
rc.threadId = activeKey;
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
const stream2 = new RunCardStream();
|
|
3888
|
+
cardMsgId = await stream2.create(channel, opts.chatId, buildRunCard(rc), { replyTo, replyInThread });
|
|
3889
|
+
curCardKey = cardMsgId;
|
|
3890
|
+
rc.cardKey = cardMsgId;
|
|
3891
|
+
runsByCard.set(cardMsgId, state);
|
|
3892
|
+
runStreams.set(cardMsgId, stream2);
|
|
3893
|
+
await adoptThreadId(cardMsgId);
|
|
3894
|
+
if (!firstCardSent) {
|
|
3895
|
+
firstCardSent = true;
|
|
3896
|
+
try {
|
|
3897
|
+
onTopicCreated?.();
|
|
3898
|
+
} catch {
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
let timedOut = false;
|
|
3902
|
+
let interrupted = false;
|
|
3903
|
+
let resolveStop;
|
|
3904
|
+
const stopSignal = new Promise((res) => {
|
|
3905
|
+
resolveStop = res;
|
|
3906
|
+
});
|
|
3907
|
+
state.interrupt = () => {
|
|
3908
|
+
if (interrupted) return;
|
|
3909
|
+
interrupted = true;
|
|
3910
|
+
resolveStop();
|
|
3911
|
+
};
|
|
3912
|
+
const guarded = withIdleTimeout(
|
|
3913
|
+
run.events,
|
|
3914
|
+
idleMs,
|
|
3915
|
+
() => {
|
|
3916
|
+
timedOut = true;
|
|
3917
|
+
},
|
|
3918
|
+
stopSignal
|
|
3919
|
+
);
|
|
3920
|
+
for await (const ev of guarded) {
|
|
3921
|
+
render.apply(ev);
|
|
3922
|
+
rc.rs = render.snapshot();
|
|
3923
|
+
await stream2.streamCard(channel, buildRunCard(rc));
|
|
3924
|
+
}
|
|
3925
|
+
state.interrupt = void 0;
|
|
3926
|
+
const killed = interrupted || timedOut;
|
|
3927
|
+
if (timedOut) render.timeout(Math.max(1, Math.round(idleMs / 6e4)));
|
|
3928
|
+
else if (interrupted) render.interrupt();
|
|
3929
|
+
else render.finalize();
|
|
3930
|
+
rc.rs = render.snapshot();
|
|
3931
|
+
if (killed) {
|
|
3932
|
+
void opts.thread.close().catch(() => void 0);
|
|
3933
|
+
if (topicThreadId) sessions.delete(topicThreadId);
|
|
3934
|
+
}
|
|
3935
|
+
const finalMsgId = cardMsgId;
|
|
3936
|
+
await adoptThreadId(finalMsgId);
|
|
3937
|
+
rc.cardKey = finalMsgId;
|
|
3938
|
+
await stream2.updateCard(channel, buildRunCard(rc));
|
|
3939
|
+
runsByCard.delete(cardMsgId);
|
|
3940
|
+
promoteCard(finalMsgId, rc);
|
|
3941
|
+
if (topicThreadId) await patchSession(topicThreadId, { updatedAt: Date.now() });
|
|
3942
|
+
replyTo = finalMsgId;
|
|
3943
|
+
replyInThread = !opts.flat;
|
|
3944
|
+
log.info("card", "final", { terminal: render.terminal() });
|
|
3945
|
+
if (killed) break;
|
|
3946
|
+
if (state.queue.length === 0) break;
|
|
3947
|
+
turnText = state.queue.shift();
|
|
3948
|
+
}
|
|
3949
|
+
} catch (err) {
|
|
3950
|
+
log.fail("intake", err);
|
|
3951
|
+
await channel.send(opts.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: opts.replyTo, replyInThread: !opts.flat }).catch(() => void 0);
|
|
3952
|
+
} finally {
|
|
3953
|
+
active.delete(activeKey);
|
|
3954
|
+
if (curCardKey) runsByCard.delete(curCardKey);
|
|
3955
|
+
reaction?.done();
|
|
3956
|
+
release();
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
const onComment = async (evt) => {
|
|
3960
|
+
await withTrace({ chatId: "comment" }, async () => {
|
|
3961
|
+
log.info("comment", "enter", {
|
|
3962
|
+
doc: evt.fileToken,
|
|
3963
|
+
fileType: evt.fileType,
|
|
3964
|
+
commentId: evt.commentId,
|
|
3965
|
+
replyId: evt.replyId ?? null,
|
|
3966
|
+
mentionedBot: evt.mentionedBot,
|
|
3967
|
+
sender: evt.operator.openId
|
|
3968
|
+
});
|
|
3969
|
+
if (!evt.mentionedBot) return log.info("comment", "skip", { reason: "not-mentioned" });
|
|
3970
|
+
if (!SUPPORTED_FILE_TYPES.has(evt.fileType))
|
|
3971
|
+
return log.info("comment", "skip", { reason: "unsupported-fileType", fileType: evt.fileType });
|
|
3972
|
+
if (!isUserAllowed(cfg, evt.operator.openId))
|
|
3973
|
+
return log.info("comment", "skip", { reason: "not-allowed" });
|
|
3974
|
+
const resolved = await resolveComment(channel, evt);
|
|
3975
|
+
if (!resolved) return log.info("comment", "skip", { reason: "no-target-or-empty" });
|
|
3976
|
+
const { target, ctx } = resolved;
|
|
3977
|
+
log.info("comment", "parsed", { isWhole: ctx.isWhole, hasQuote: Boolean(ctx.quote) });
|
|
3978
|
+
const prompt = buildCommentPrompt(target, ctx, cfg.accounts.app.tenant);
|
|
3979
|
+
const sessionKey = `doc:${evt.fileToken}`;
|
|
3980
|
+
const reacted = ctx.targetReplyId ? await addCommentReaction(channel, target, ctx.targetReplyId) : false;
|
|
3981
|
+
try {
|
|
3982
|
+
await withDocLock(sessionKey, async () => {
|
|
3983
|
+
const release = await sema.acquire();
|
|
3984
|
+
try {
|
|
3985
|
+
const thread = await resolveDocThread(sessionKey, ctx.question);
|
|
3986
|
+
const rec = await getSession(sessionKey);
|
|
3987
|
+
const run = thread.runStreamed({ text: prompt }, { model: rec?.model, effort: rec?.effort });
|
|
3988
|
+
let state = initialState;
|
|
3989
|
+
let timedOut = false;
|
|
3990
|
+
const guarded = withIdleTimeout(run.events, idleMs, () => {
|
|
3991
|
+
timedOut = true;
|
|
3992
|
+
});
|
|
3993
|
+
for await (const ev of guarded) state = reduce(state, ev);
|
|
3994
|
+
if (timedOut) {
|
|
3995
|
+
const tid = run.turnId();
|
|
3996
|
+
if (tid) void thread.abort(tid).catch(() => void 0);
|
|
3997
|
+
void thread.close().catch(() => void 0);
|
|
3998
|
+
sessions.delete(sessionKey);
|
|
3999
|
+
} else {
|
|
4000
|
+
await patchSession(sessionKey, { updatedAt: Date.now() });
|
|
4001
|
+
}
|
|
4002
|
+
let reply = stripMarkdown(finalMessageText(state)).trim();
|
|
4003
|
+
if (state.terminal === "error" && state.errorMsg) reply = `\u26A0\uFE0F \u51FA\u9519\u4E86\uFF1A${state.errorMsg}`;
|
|
4004
|
+
if (!reply) reply = timedOut ? "\uFF08\u5904\u7406\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5\u6216\u628A\u95EE\u9898\u95EE\u5F97\u66F4\u5177\u4F53\u4E9B\uFF09" : "\uFF08\u6CA1\u6709\u53EF\u56DE\u590D\u7684\u5185\u5BB9\uFF09";
|
|
4005
|
+
if (reply.length > REPLY_MAX_CHARS) reply = `${reply.slice(0, REPLY_MAX_CHARS - 1)}\u2026`;
|
|
4006
|
+
await postCommentReply(channel, target, evt, reply).catch(
|
|
4007
|
+
(err) => log.fail("comment", err, { step: "postCommentReply" })
|
|
4008
|
+
);
|
|
4009
|
+
log.info("comment", "done", { terminal: state.terminal, timedOut, len: reply.length });
|
|
4010
|
+
} finally {
|
|
4011
|
+
release();
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
4014
|
+
} catch (err) {
|
|
4015
|
+
log.fail("comment", err, { step: "run" });
|
|
4016
|
+
} finally {
|
|
4017
|
+
if (reacted && ctx.targetReplyId)
|
|
4018
|
+
await removeCommentReaction(channel, target, ctx.targetReplyId).catch(() => void 0);
|
|
4019
|
+
}
|
|
4020
|
+
}).catch((err) => log.fail("comment", err));
|
|
4021
|
+
};
|
|
4022
|
+
function withDocLock(key, fn) {
|
|
4023
|
+
const prev = docLocks.get(key) ?? Promise.resolve();
|
|
4024
|
+
const run = prev.then(fn, fn);
|
|
4025
|
+
const tail = run.then(
|
|
4026
|
+
() => void 0,
|
|
4027
|
+
() => void 0
|
|
4028
|
+
);
|
|
4029
|
+
docLocks.set(key, tail);
|
|
4030
|
+
void tail.then(() => {
|
|
4031
|
+
if (docLocks.get(key) === tail) docLocks.delete(key);
|
|
4032
|
+
});
|
|
4033
|
+
return run;
|
|
4034
|
+
}
|
|
4035
|
+
async function resolveDocThread(sessionKey, question) {
|
|
4036
|
+
const live = sessions.get(sessionKey);
|
|
4037
|
+
if (live) return live;
|
|
4038
|
+
const rec = await getSession(sessionKey);
|
|
4039
|
+
if (rec) {
|
|
4040
|
+
try {
|
|
4041
|
+
const resumed = await backend.resumeThread({
|
|
4042
|
+
cwd: rec.cwd,
|
|
4043
|
+
codexThreadId: rec.codexThreadId,
|
|
4044
|
+
model: rec.model,
|
|
4045
|
+
effort: rec.effort
|
|
4046
|
+
});
|
|
4047
|
+
sessions.set(sessionKey, resumed);
|
|
4048
|
+
return resumed;
|
|
4049
|
+
} catch (err) {
|
|
4050
|
+
log.fail("agent", err, { phase: "comment-resume", sessionKey });
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const { model, effort } = pickDefault(await listModels());
|
|
4054
|
+
const fresh = await backend.startThread({ cwd: fallbackCwd, model, effort });
|
|
4055
|
+
sessions.set(sessionKey, fresh);
|
|
4056
|
+
await upsertSession({
|
|
4057
|
+
threadId: sessionKey,
|
|
4058
|
+
chatId: sessionKey,
|
|
4059
|
+
cwd: fallbackCwd,
|
|
4060
|
+
codexThreadId: fresh.codexThreadId,
|
|
4061
|
+
model,
|
|
4062
|
+
effort,
|
|
4063
|
+
summary: question.slice(0, 80),
|
|
4064
|
+
createdAt: Date.now(),
|
|
4065
|
+
updatedAt: Date.now()
|
|
4066
|
+
});
|
|
4067
|
+
return fresh;
|
|
4068
|
+
}
|
|
4069
|
+
async function shutdown() {
|
|
4070
|
+
const live = [...sessions.values()];
|
|
4071
|
+
sessions.clear();
|
|
4072
|
+
await Promise.allSettled(live.map((t) => t.close()));
|
|
4073
|
+
log.info("bridge", "shutdown", { closed: live.length });
|
|
4074
|
+
}
|
|
4075
|
+
return { onMessage, onComment, dispatcher, shutdown };
|
|
4076
|
+
}
|
|
4077
|
+
async function getThreadId(channel, messageId) {
|
|
4078
|
+
try {
|
|
4079
|
+
const res = await channel.rawClient.im.v1.message.get({ path: { message_id: messageId } });
|
|
4080
|
+
const items = res.data?.items;
|
|
4081
|
+
const tid = items?.[0]?.thread_id;
|
|
4082
|
+
if (!tid) log.warn("intake", "threadid-missing", { messageId });
|
|
4083
|
+
return tid;
|
|
4084
|
+
} catch (err) {
|
|
4085
|
+
log.warn("intake", "threadid-lookup-failed", { messageId, err: String(err) });
|
|
4086
|
+
return void 0;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
// src/bot/bridge.ts
|
|
4091
|
+
async function startBridge(opts) {
|
|
4092
|
+
const app = opts.cfg.accounts.app;
|
|
4093
|
+
const channel = createLarkChannel({
|
|
4094
|
+
appId: app.id,
|
|
4095
|
+
appSecret: opts.appSecret,
|
|
4096
|
+
domain: app.tenant === "lark" ? Domain.Lark : Domain.Feishu,
|
|
4097
|
+
source: "feishu-codex-bridge",
|
|
4098
|
+
// surface raw events so card-action handlers can read form submissions
|
|
4099
|
+
// (action.form_value) — used by the new-project form.
|
|
4100
|
+
includeRawEvent: true,
|
|
4101
|
+
// Deliver ALL group messages (not just @bot) to `onMessage`. The SDK's
|
|
4102
|
+
// PolicyGate otherwise drops non-@ group messages with reason 'no_mention'
|
|
4103
|
+
// before they reach us, which would make 免@ impossible. We turn the SDK
|
|
4104
|
+
// filter off and let our per-project gate (shouldRespondWithoutMention in
|
|
4105
|
+
// handle-message) be the single source of truth for 免@. Non-@ delivery
|
|
4106
|
+
// still requires the im:message.group_msg scope (Feishu-side push).
|
|
4107
|
+
policy: { requireMention: false }
|
|
4108
|
+
});
|
|
4109
|
+
const orchestrator = createOrchestrator(channel, opts.cfg, opts.fallbackCwd);
|
|
4110
|
+
channel.on("message", orchestrator.onMessage);
|
|
4111
|
+
channel.on("cardAction", orchestrator.dispatcher.handle);
|
|
4112
|
+
channel.on("comment", orchestrator.onComment);
|
|
4113
|
+
channel.on("reject", (evt) => log.info("intake", "reject", { reason: evt.reason, msgId: evt.messageId }));
|
|
4114
|
+
channel.on("error", (err) => log.fail("ws", err));
|
|
4115
|
+
channel.on("reconnecting", () => log.info("ws", "reconnecting"));
|
|
4116
|
+
channel.on("reconnected", () => log.info("ws", "reconnected"));
|
|
4117
|
+
await channel.connect();
|
|
4118
|
+
log.info("ws", "connected", { appId: app.id, fallbackCwd: opts.fallbackCwd });
|
|
4119
|
+
let closed = false;
|
|
4120
|
+
const shutdown = async () => {
|
|
4121
|
+
if (closed) return;
|
|
4122
|
+
closed = true;
|
|
4123
|
+
await orchestrator.shutdown();
|
|
4124
|
+
await channel.disconnect().catch((err) => log.fail("ws", err, { phase: "disconnect" }));
|
|
4125
|
+
};
|
|
4126
|
+
return { channel, shutdown };
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// src/core/single-instance.ts
|
|
4130
|
+
import { mkdirSync as mkdirSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
4131
|
+
import { dirname as dirname6 } from "path";
|
|
4132
|
+
var BridgeAlreadyRunningError = class extends Error {
|
|
4133
|
+
constructor(pid) {
|
|
4134
|
+
super(
|
|
4135
|
+
`\u53E6\u4E00\u4E2A bridge \u8FDB\u7A0B\u5DF2\u5728\u8FD0\u884C (PID ${pid})\u3002\u5148\u505C\u6389\u5B83\uFF08kill ${pid}\uFF09\u518D\u542F\u52A8\u2014\u2014\u4E24\u4E2A\u8FDB\u7A0B\u4F1A\u62A2\u540C\u4E00 App \u7684\u5361\u7247\u56DE\u8C03\uFF0C\u5BFC\u81F4\u6309\u94AE\u65F6\u7075\u65F6\u4E0D\u7075\u3002`
|
|
4136
|
+
);
|
|
4137
|
+
this.pid = pid;
|
|
4138
|
+
this.name = "BridgeAlreadyRunningError";
|
|
4139
|
+
}
|
|
4140
|
+
pid;
|
|
4141
|
+
};
|
|
4142
|
+
function isAlive(pid) {
|
|
4143
|
+
try {
|
|
4144
|
+
process.kill(pid, 0);
|
|
4145
|
+
return true;
|
|
4146
|
+
} catch {
|
|
4147
|
+
return false;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
function acquireSingleInstanceLock(appId) {
|
|
4151
|
+
const file = paths.processesFile;
|
|
4152
|
+
try {
|
|
4153
|
+
const rec = JSON.parse(readFileSync(file, "utf8"));
|
|
4154
|
+
if (rec.pid && rec.pid !== process.pid && rec.appId === appId && isAlive(rec.pid)) {
|
|
4155
|
+
throw new BridgeAlreadyRunningError(rec.pid);
|
|
4156
|
+
}
|
|
4157
|
+
} catch (err) {
|
|
4158
|
+
if (err instanceof BridgeAlreadyRunningError) throw err;
|
|
4159
|
+
}
|
|
4160
|
+
mkdirSync2(dirname6(file), { recursive: true });
|
|
4161
|
+
const record = { pid: process.pid, appId, startedAt: Date.now() };
|
|
4162
|
+
writeFileSync(file, `${JSON.stringify(record)}
|
|
4163
|
+
`, "utf8");
|
|
4164
|
+
const release = () => {
|
|
4165
|
+
try {
|
|
4166
|
+
const rec = JSON.parse(readFileSync(file, "utf8"));
|
|
4167
|
+
if (rec.pid === process.pid) unlinkSync(file);
|
|
4168
|
+
} catch {
|
|
4169
|
+
}
|
|
4170
|
+
};
|
|
4171
|
+
process.once("exit", release);
|
|
4172
|
+
return release;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// src/cli/commands/run.ts
|
|
4176
|
+
async function runRun() {
|
|
4177
|
+
const ready = await ensureOnboarded({ allowCreate: true });
|
|
4178
|
+
if (!ready) {
|
|
4179
|
+
process.exitCode = 1;
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
const { cfg, secret } = ready;
|
|
4183
|
+
let releaseLock;
|
|
4184
|
+
try {
|
|
4185
|
+
releaseLock = acquireSingleInstanceLock(cfg.accounts.app.id);
|
|
4186
|
+
} catch (err) {
|
|
4187
|
+
if (err instanceof BridgeAlreadyRunningError) {
|
|
4188
|
+
console.error(`\u2717 ${err.message}`);
|
|
4189
|
+
log.info("run", "already-running", { pid: err.pid });
|
|
4190
|
+
process.exitCode = 1;
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
throw err;
|
|
4194
|
+
}
|
|
4195
|
+
const fallbackCwd = process.env.FEISHU_CODEX_CWD || process.cwd();
|
|
4196
|
+
console.log("\n\u6B63\u5728\u542F\u52A8\u957F\u8FDE\u63A5 bot\u2026");
|
|
4197
|
+
console.log("\u79C1\u804A\u6211 `/new <\u540D>` \u5EFA\u9879\u76EE\uFF1B\u5728\u9879\u76EE\u7FA4\u91CC @\u6211 \u5E72\u6D3B\u3002Ctrl+C \u9000\u51FA\u3002\n");
|
|
4198
|
+
const handle = await startBridge({ cfg, appSecret: secret, fallbackCwd });
|
|
4199
|
+
let stopping = false;
|
|
4200
|
+
const stop = (sig) => {
|
|
4201
|
+
if (stopping) return;
|
|
4202
|
+
stopping = true;
|
|
4203
|
+
console.log(`
|
|
4204
|
+
\u6536\u5230 ${sig}\uFF0C\u6B63\u5728\u4F18\u96C5\u9000\u51FA\uFF08\u5173\u95ED\u6240\u6709 codex \u4F1A\u8BDD\uFF09\u2026`);
|
|
4205
|
+
void handle.shutdown().catch((err) => log.fail("run", err, { phase: "shutdown" })).finally(() => {
|
|
4206
|
+
releaseLock();
|
|
4207
|
+
process.exit(0);
|
|
4208
|
+
});
|
|
4209
|
+
};
|
|
4210
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
4211
|
+
process.once(sig, () => stop(sig));
|
|
4212
|
+
}
|
|
4213
|
+
await new Promise(() => {
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
// src/service/launchd.ts
|
|
4218
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
4219
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4220
|
+
import { appendFile, mkdir as mkdir7, rm as rm2, writeFile as writeFile6 } from "fs/promises";
|
|
4221
|
+
import { homedir as homedir3, userInfo as userInfo2 } from "os";
|
|
4222
|
+
import { dirname as dirname7, join as join8, resolve as resolve2 } from "path";
|
|
4223
|
+
import { fileURLToPath } from "url";
|
|
4224
|
+
var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
|
|
4225
|
+
function launchAgentPlistPath() {
|
|
4226
|
+
return join8(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
4227
|
+
}
|
|
4228
|
+
function serviceStdoutPath() {
|
|
4229
|
+
return join8(paths.appDir, "service.log");
|
|
4230
|
+
}
|
|
4231
|
+
function serviceStderrPath() {
|
|
4232
|
+
return join8(paths.appDir, "service.err.log");
|
|
4233
|
+
}
|
|
4234
|
+
function resolveCliBinPath() {
|
|
4235
|
+
const distDir = dirname7(fileURLToPath(import.meta.url));
|
|
4236
|
+
return resolve2(distDir, "..", "bin", "feishu-codex-bridge.mjs");
|
|
4237
|
+
}
|
|
4238
|
+
function escapeXml(value) {
|
|
4239
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4240
|
+
}
|
|
4241
|
+
function buildPlist() {
|
|
4242
|
+
const nodePath = process.execPath;
|
|
4243
|
+
const cliBinPath = resolveCliBinPath();
|
|
4244
|
+
const pathEnv = process.env.PATH ?? "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
4245
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
4246
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4247
|
+
<plist version="1.0">
|
|
4248
|
+
<dict>
|
|
4249
|
+
<key>Label</key>
|
|
4250
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
4251
|
+
<key>ProgramArguments</key>
|
|
4252
|
+
<array>
|
|
4253
|
+
<string>${escapeXml(nodePath)}</string>
|
|
4254
|
+
<string>${escapeXml(cliBinPath)}</string>
|
|
4255
|
+
<string>run</string>
|
|
4256
|
+
</array>
|
|
4257
|
+
<key>RunAtLoad</key>
|
|
4258
|
+
<true/>
|
|
4259
|
+
<key>KeepAlive</key>
|
|
4260
|
+
<true/>
|
|
4261
|
+
<key>StandardOutPath</key>
|
|
4262
|
+
<string>${escapeXml(serviceStdoutPath())}</string>
|
|
4263
|
+
<key>StandardErrorPath</key>
|
|
4264
|
+
<string>${escapeXml(serviceStderrPath())}</string>
|
|
4265
|
+
<key>EnvironmentVariables</key>
|
|
4266
|
+
<dict>
|
|
4267
|
+
<key>PATH</key>
|
|
4268
|
+
<string>${escapeXml(pathEnv)}</string>
|
|
4269
|
+
</dict>
|
|
4270
|
+
</dict>
|
|
4271
|
+
</plist>
|
|
4272
|
+
`;
|
|
4273
|
+
}
|
|
4274
|
+
async function installLaunchd() {
|
|
4275
|
+
const plistPath = launchAgentPlistPath();
|
|
4276
|
+
await mkdir7(dirname7(plistPath), { recursive: true });
|
|
4277
|
+
await ensureLogFiles();
|
|
4278
|
+
await writeFile6(plistPath, buildPlist(), "utf8");
|
|
4279
|
+
if (isLoaded()) {
|
|
4280
|
+
const bootout = runLaunchctl(["bootout", serviceTarget()]);
|
|
4281
|
+
if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
|
|
4282
|
+
await waitUntilUnloaded();
|
|
4283
|
+
}
|
|
4284
|
+
const bootstrap = runLaunchctl(["bootstrap", userTarget(), plistPath]);
|
|
4285
|
+
if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
|
|
4286
|
+
return statusLaunchd();
|
|
4287
|
+
}
|
|
4288
|
+
async function uninstallLaunchd() {
|
|
4289
|
+
if (isLoaded()) {
|
|
4290
|
+
const bootout = runLaunchctl(["bootout", serviceTarget()]);
|
|
4291
|
+
if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
|
|
4292
|
+
await waitUntilUnloaded();
|
|
4293
|
+
}
|
|
4294
|
+
await rm2(launchAgentPlistPath(), { force: true });
|
|
4295
|
+
}
|
|
4296
|
+
async function restartLaunchd() {
|
|
4297
|
+
if (!existsSync5(launchAgentPlistPath())) {
|
|
4298
|
+
throw new Error(`launchd service \u672A\u5B89\u88C5\uFF1A${launchAgentPlistPath()}`);
|
|
4299
|
+
}
|
|
4300
|
+
if (isLoaded()) {
|
|
4301
|
+
const bootout = runLaunchctl(["bootout", serviceTarget()]);
|
|
4302
|
+
if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
|
|
4303
|
+
await waitUntilUnloaded();
|
|
4304
|
+
}
|
|
4305
|
+
const bootstrap = runLaunchctl(["bootstrap", userTarget(), launchAgentPlistPath()]);
|
|
4306
|
+
if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
|
|
4307
|
+
return statusLaunchd();
|
|
4308
|
+
}
|
|
4309
|
+
function statusLaunchd() {
|
|
4310
|
+
const result = runLaunchctl(["print", serviceTarget()]);
|
|
4311
|
+
const raw = result.stdout || result.stderr;
|
|
4312
|
+
const parsed = parseLaunchdStatus(raw);
|
|
4313
|
+
return {
|
|
4314
|
+
installed: existsSync5(launchAgentPlistPath()),
|
|
4315
|
+
loaded: result.ok,
|
|
4316
|
+
plistPath: launchAgentPlistPath(),
|
|
4317
|
+
stdoutPath: serviceStdoutPath(),
|
|
4318
|
+
stderrPath: serviceStderrPath(),
|
|
4319
|
+
pid: parsed.pid,
|
|
4320
|
+
lastExit: parsed.lastExit,
|
|
4321
|
+
raw
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
async function tailLaunchdLogs(follow) {
|
|
4325
|
+
await ensureLogFiles();
|
|
4326
|
+
const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
|
|
4327
|
+
await new Promise((resolvePromise, reject) => {
|
|
4328
|
+
const child = spawn4("tail", args, { stdio: "inherit" });
|
|
4329
|
+
child.on("error", reject);
|
|
4330
|
+
child.on("close", (code) => {
|
|
4331
|
+
if (code === 0 || follow && code === null) {
|
|
4332
|
+
resolvePromise();
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
reject(new Error(`tail \u9000\u51FA\u7801 ${code ?? "unknown"}`));
|
|
4336
|
+
});
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
function parseLaunchdStatus(text) {
|
|
4340
|
+
return {
|
|
4341
|
+
pid: text.match(/\bpid\s*=\s*(\d+)/)?.[1],
|
|
4342
|
+
lastExit: text.match(/last exit code\s*=\s*(-?\d+)/i)?.[1]
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
function isLoaded() {
|
|
4346
|
+
const result = spawnSync("launchctl", ["print", serviceTarget()], {
|
|
4347
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
4348
|
+
});
|
|
4349
|
+
return result.status === 0;
|
|
4350
|
+
}
|
|
4351
|
+
async function waitUntilUnloaded(timeoutMs = 5e3) {
|
|
4352
|
+
const deadline = Date.now() + timeoutMs;
|
|
4353
|
+
while (Date.now() < deadline) {
|
|
4354
|
+
if (!isLoaded()) return;
|
|
4355
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
|
|
4356
|
+
}
|
|
4357
|
+
throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
|
|
4358
|
+
}
|
|
4359
|
+
async function ensureLogFiles() {
|
|
4360
|
+
await mkdir7(paths.appDir, { recursive: true });
|
|
4361
|
+
await appendFile(serviceStdoutPath(), "");
|
|
4362
|
+
await appendFile(serviceStderrPath(), "");
|
|
4363
|
+
}
|
|
4364
|
+
function userTarget() {
|
|
4365
|
+
return `gui/${userInfo2().uid}`;
|
|
4366
|
+
}
|
|
4367
|
+
function serviceTarget() {
|
|
4368
|
+
return `${userTarget()}/${LAUNCHD_LABEL}`;
|
|
4369
|
+
}
|
|
4370
|
+
function runLaunchctl(args) {
|
|
4371
|
+
const result = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
4372
|
+
return {
|
|
4373
|
+
ok: result.status === 0,
|
|
4374
|
+
status: result.status,
|
|
4375
|
+
stdout: result.stdout ?? "",
|
|
4376
|
+
stderr: result.stderr ?? ""
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
function launchctlError(command, result) {
|
|
4380
|
+
const output = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
4381
|
+
return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
// src/service/adapter.ts
|
|
4385
|
+
function getServiceAdapter() {
|
|
4386
|
+
if (process.platform !== "darwin") {
|
|
4387
|
+
throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
|
|
4388
|
+
}
|
|
4389
|
+
return {
|
|
4390
|
+
install: installLaunchd,
|
|
4391
|
+
uninstall: uninstallLaunchd,
|
|
4392
|
+
status: async () => statusLaunchd(),
|
|
4393
|
+
restart: restartLaunchd,
|
|
4394
|
+
logs: tailLaunchdLogs
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
// src/cli/commands/daemon.ts
|
|
4399
|
+
async function runStart() {
|
|
4400
|
+
const ready = await ensureOnboarded({ allowCreate: true });
|
|
4401
|
+
if (!ready) {
|
|
4402
|
+
process.exitCode = 1;
|
|
4403
|
+
return;
|
|
4404
|
+
}
|
|
4405
|
+
if (!await confirmReadyForDaemon(ready)) {
|
|
4406
|
+
process.exitCode = 1;
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
const status = await getServiceAdapter().install();
|
|
4410
|
+
console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF08\u5F00\u673A\u81EA\u542F\u3001\u5D29\u6E83\u81EA\u52A8\u62C9\u8D77\uFF09\u3002");
|
|
4411
|
+
printStatus(status);
|
|
4412
|
+
}
|
|
4413
|
+
async function runStop() {
|
|
4414
|
+
await getServiceAdapter().uninstall();
|
|
4415
|
+
console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u505C\u6B62\uFF0C\u5E76\u5DF2\u5173\u95ED\u5F00\u673A\u81EA\u542F\uFF08\u5DF2\u79FB\u9664 launchd plist\uFF09\u3002");
|
|
4416
|
+
}
|
|
4417
|
+
async function runRestart() {
|
|
4418
|
+
const status = await getServiceAdapter().restart();
|
|
4419
|
+
console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u91CD\u542F\u3002");
|
|
4420
|
+
printStatus(status);
|
|
4421
|
+
}
|
|
4422
|
+
async function runStatus() {
|
|
4423
|
+
printStatus(await getServiceAdapter().status());
|
|
4424
|
+
}
|
|
4425
|
+
async function runLogs(follow) {
|
|
4426
|
+
await getServiceAdapter().logs(follow);
|
|
4427
|
+
}
|
|
4428
|
+
function printStatus(status) {
|
|
4429
|
+
console.log(`plist: ${status.plistPath}`);
|
|
4430
|
+
console.log(`installed: ${status.installed ? "yes" : "no"}`);
|
|
4431
|
+
console.log(`loaded: ${status.loaded ? "yes" : "no"}`);
|
|
4432
|
+
console.log(`pid: ${status.pid ?? "-"}`);
|
|
4433
|
+
console.log(`last exit: ${status.lastExit ?? "-"}`);
|
|
4434
|
+
console.log(`stdout: ${status.stdoutPath}`);
|
|
4435
|
+
console.log(`stderr: ${status.stderrPath}`);
|
|
4436
|
+
if (!status.installed) {
|
|
4437
|
+
console.log("\u63D0\u793A\uFF1A\u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF0C\u8FD0\u884C `feishu-codex-bridge start`\u3002");
|
|
4438
|
+
} else if (!status.loaded) {
|
|
4439
|
+
console.log("\u63D0\u793A\uFF1Aplist \u5DF2\u5B58\u5728\uFF0C\u4F46 launchd \u5F53\u524D\u672A\u52A0\u8F7D\uFF08\u8BD5\u8BD5 `restart`\uFF09\u3002");
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
// src/cli/commands/bot.ts
|
|
4444
|
+
import { rm as rm3 } from "fs/promises";
|
|
4445
|
+
async function runBotInit(name) {
|
|
4446
|
+
if (!ensureCodex()) {
|
|
4447
|
+
process.exitCode = 1;
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
4450
|
+
const result = await registerNewBot(name);
|
|
4451
|
+
if (!result) {
|
|
4452
|
+
process.exitCode = 1;
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
console.log("\n\u4E0B\u4E00\u6B65\uFF08\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u540E\u53F0\uFF0C\u9700\u624B\u52A8\u4E00\u6B21 https://open.feishu.cn/app \uFF09\uFF1A");
|
|
4456
|
+
console.log(" 1) \u4E8B\u4EF6\u4E0E\u56DE\u8C03 \u2192 \u957F\u8FDE\u63A5 \u2192 \u8BA2\u9605\uFF1Aim.message.receive_v1 / card.action.trigger / application.bot.menu_v6");
|
|
4457
|
+
console.log(" 2) \u521B\u5EFA\u5E76\u53D1\u5E03\u5E94\u7528\u7248\u672C");
|
|
4458
|
+
console.log("\n`bot list` \u67E5\u770B\u5168\u90E8\uFF1B`bot use <\u540D>` \u5207\u6362\u5F53\u524D\uFF1B`run` \u524D\u53F0\u8DD1 / `start` \u540E\u53F0\u5E38\u9A7B\u3002\n");
|
|
4459
|
+
}
|
|
4460
|
+
async function runBotList() {
|
|
4461
|
+
const reg = await loadBots();
|
|
4462
|
+
if (reg.bots.length === 0) {
|
|
4463
|
+
console.log("\uFF08\u8FD8\u6CA1\u6709\u6CE8\u518C\u4EFB\u4F55\u98DE\u4E66\u673A\u5668\u4EBA\u3002\u8FD0\u884C `feishu-codex-bridge bot init` \u521B\u5EFA\u3002\uFF09");
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
console.log("\n\u5DF2\u6CE8\u518C\u7684\u98DE\u4E66\u673A\u5668\u4EBA\uFF1A\n");
|
|
4467
|
+
for (const b of reg.bots) {
|
|
4468
|
+
const cur = b.appId === reg.current ? "\u{1F449}" : " ";
|
|
4469
|
+
console.log(`${cur} ${b.name.padEnd(16)} ${b.appId} [${b.tenant}]${b.botName ? ` ${b.botName}` : ""}`);
|
|
4470
|
+
}
|
|
4471
|
+
console.log("\n\u{1F449} = \u5F53\u524D\u9009\u4E2D\uFF08run / start \u542F\u52A8\u7684\u5C31\u662F\u5B83\uFF09\u3002`bot use <\u540D>` \u5207\u6362\u3002\n");
|
|
4472
|
+
}
|
|
4473
|
+
async function runBotUse(name) {
|
|
4474
|
+
const reg = await loadBots();
|
|
4475
|
+
const bot2 = findBot(reg, name);
|
|
4476
|
+
if (!bot2) {
|
|
4477
|
+
console.error(`\u2717 \u627E\u4E0D\u5230\u673A\u5668\u4EBA\u300C${name}\u300D\u3002\u5DF2\u6CE8\u518C\uFF1A${botNames(reg.bots)}`);
|
|
4478
|
+
process.exitCode = 1;
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
if (reg.current === bot2.appId) {
|
|
4482
|
+
console.log(`\u300C${bot2.name}\u300D\u5DF2\u7ECF\u662F\u5F53\u524D\u673A\u5668\u4EBA\u3002`);
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
await setCurrent(bot2.appId);
|
|
4486
|
+
console.log(`\u2713 \u5F53\u524D\u673A\u5668\u4EBA \u2192 \u300C${bot2.name}\u300D(${bot2.appId})\u3002\u524D\u53F0 \`run\` \u76F4\u63A5\u751F\u6548\uFF1B\u540E\u53F0\u8BF7 \`restart\`\u3002`);
|
|
4487
|
+
}
|
|
4488
|
+
async function runBotRm(name) {
|
|
4489
|
+
const reg = await loadBots();
|
|
4490
|
+
const bot2 = findBot(reg, name);
|
|
4491
|
+
if (!bot2) {
|
|
4492
|
+
console.error(`\u2717 \u627E\u4E0D\u5230\u673A\u5668\u4EBA\u300C${name}\u300D\u3002\u5DF2\u6CE8\u518C\uFF1A${botNames(reg.bots)}`);
|
|
4493
|
+
process.exitCode = 1;
|
|
4494
|
+
return;
|
|
4495
|
+
}
|
|
4496
|
+
const after = await removeBot(bot2.appId);
|
|
4497
|
+
await removeSecret(secretKeyForApp(bot2.appId));
|
|
4498
|
+
await rm3(botDir(bot2.appId), { recursive: true, force: true });
|
|
4499
|
+
console.log(`\u2713 \u5DF2\u79FB\u9664\u673A\u5668\u4EBA\u300C${bot2.name}\u300D(${bot2.appId})\uFF1A\u6CE8\u518C\u8868 + \u5BC6\u94A5 + \u72B6\u6001\u76EE\u5F55(projects/sessions)\u3002`);
|
|
4500
|
+
if (after.bots.length === 0) {
|
|
4501
|
+
console.log(" \u5DF2\u65E0\u4EFB\u4F55\u673A\u5668\u4EBA\uFF0C`bot init` \u91CD\u65B0\u521B\u5EFA\u3002");
|
|
4502
|
+
} else if (after.current) {
|
|
4503
|
+
const cur = after.bots.find((b) => b.appId === after.current);
|
|
4504
|
+
if (cur) console.log(` \u5F53\u524D\u673A\u5668\u4EBA\u73B0\u4E3A\u300C${cur.name}\u300D\u3002`);
|
|
4505
|
+
} else {
|
|
4506
|
+
console.log(" \u5F53\u524D\u673A\u5668\u4EBA\u672A\u8BBE\u7F6E\uFF0C\u7528 `bot use <\u540D>` \u9009\u62E9\u3002");
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
function botNames(bots) {
|
|
4510
|
+
return bots.map((b) => b.name).join(", ") || "\uFF08\u65E0\uFF09";
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
// src/cli/commands/secrets.ts
|
|
4514
|
+
async function secretsGet() {
|
|
4515
|
+
const input2 = await readStdin();
|
|
4516
|
+
let ids = [];
|
|
4517
|
+
try {
|
|
4518
|
+
const req = JSON.parse(input2);
|
|
4519
|
+
ids = Array.isArray(req.ids) ? req.ids : [];
|
|
4520
|
+
} catch {
|
|
4521
|
+
process.stdout.write(JSON.stringify({ values: {}, errors: { _: { message: "invalid request JSON" } } }));
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
const values = {};
|
|
4525
|
+
const errors = {};
|
|
4526
|
+
for (const id of ids) {
|
|
4527
|
+
try {
|
|
4528
|
+
const v = await getSecret(id);
|
|
4529
|
+
if (v !== void 0) values[id] = v;
|
|
4530
|
+
else errors[id] = { message: "not found" };
|
|
4531
|
+
} catch (err) {
|
|
4532
|
+
errors[id] = { message: err instanceof Error ? err.message : String(err) };
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
process.stdout.write(JSON.stringify({ values, errors }));
|
|
4536
|
+
}
|
|
4537
|
+
async function secretsSet(id) {
|
|
4538
|
+
const value = (await readStdin()).trim();
|
|
4539
|
+
if (!value) {
|
|
4540
|
+
console.error("\u65E0\u8F93\u5165\uFF1A\u628A\u5BC6\u94A5\u901A\u8FC7 stdin \u4F20\u5165\uFF0C\u5982 `echo <secret> | feishu-codex-bridge secrets set <id>`");
|
|
4541
|
+
process.exitCode = 1;
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
await setSecret(id, value);
|
|
4545
|
+
console.log(`\u2713 \u5DF2\u5B58\u50A8\u5BC6\u94A5: ${id}`);
|
|
4546
|
+
}
|
|
4547
|
+
async function secretsList() {
|
|
4548
|
+
const ids = await listSecretIds();
|
|
4549
|
+
console.log(ids.length ? ids.join("\n") : "(\u7A7A)");
|
|
4550
|
+
}
|
|
4551
|
+
async function secretsRemove(id) {
|
|
4552
|
+
const ok = await removeSecret(id);
|
|
4553
|
+
console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
|
|
4554
|
+
}
|
|
4555
|
+
function readStdin() {
|
|
4556
|
+
return new Promise((resolve3) => {
|
|
4557
|
+
let data = "";
|
|
4558
|
+
if (process.stdin.isTTY) {
|
|
4559
|
+
resolve3("");
|
|
4560
|
+
return;
|
|
4561
|
+
}
|
|
4562
|
+
process.stdin.setEncoding("utf8");
|
|
4563
|
+
process.stdin.on("data", (c) => data += c);
|
|
4564
|
+
process.stdin.on("end", () => resolve3(data));
|
|
4565
|
+
});
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
// src/cli/index.ts
|
|
4569
|
+
var program = new Command();
|
|
4570
|
+
program.name("feishu-codex-bridge").description("\u628A\u98DE\u4E66/Lark \u6865\u63A5\u5230\u672C\u673A Codex\uFF08\u9879\u76EE=\u7FA4, \u8BDD\u9898=\u4F1A\u8BDD\uFF09").version("0.0.1");
|
|
4571
|
+
program.command("run").description("\u524D\u53F0\u542F\u52A8 bot\uFF08\u6CA1\u914D\u7F6E\u5219\u5148\u626B\u7801 init\uFF1BCtrl+C \u4F18\u96C5\u9000\u51FA\uFF09").action(async () => {
|
|
4572
|
+
await runRun();
|
|
4573
|
+
});
|
|
4574
|
+
program.command("start").description("\u540E\u53F0 daemon \u542F\u52A8\uFF08\u88C5 launchd \u5F00\u673A\u81EA\u542F\uFF1B\u6CA1\u914D\u7F6E\u5219\u5148\u626B\u7801 init\uFF09").action(async () => {
|
|
4575
|
+
await runStart();
|
|
4576
|
+
});
|
|
4577
|
+
program.command("stop").description("\u505C\u6B62\u540E\u53F0 daemon \u5E76\u5173\u95ED\u5F00\u673A\u81EA\u542F").action(async () => {
|
|
4578
|
+
await runStop();
|
|
4579
|
+
});
|
|
4580
|
+
program.command("restart").description("\u91CD\u542F\u540E\u53F0 daemon").action(async () => {
|
|
4581
|
+
await runRestart();
|
|
4582
|
+
});
|
|
4583
|
+
program.command("status").description("\u540E\u53F0 daemon \u72B6\u6001\uFF08pid / \u65E5\u5FD7\u8DEF\u5F84 / \u4E0A\u6B21\u9000\u51FA\u7801\uFF09").action(async () => {
|
|
4584
|
+
await runStatus();
|
|
4585
|
+
});
|
|
4586
|
+
program.command("logs").description("\u67E5\u770B\u540E\u53F0 daemon \u65E5\u5FD7").option("-f, --follow", "\u6301\u7EED\u8DDF\u968F\u65E5\u5FD7").action(async (options) => {
|
|
4587
|
+
await runLogs(Boolean(options.follow));
|
|
4588
|
+
});
|
|
4589
|
+
var bot = program.command("bot").description("\u98DE\u4E66\u673A\u5668\u4EBA\u7BA1\u7406\uFF08\u591A\u673A\u5668\u4EBA\uFF09");
|
|
4590
|
+
bot.command("init [name]").description("\u6CE8\u518C\u4E00\u4E2A\u98DE\u4E66\u673A\u5668\u4EBA\u5E76\u6388\u6743\uFF08\u53EF\u9009\u77ED\u540D\uFF09").action(async (name) => {
|
|
4591
|
+
await runBotInit(name);
|
|
4592
|
+
});
|
|
4593
|
+
bot.command("list").description("\u5217\u51FA\u5DF2\u6CE8\u518C\u7684\u98DE\u4E66\u673A\u5668\u4EBA").action(async () => {
|
|
4594
|
+
await runBotList();
|
|
4595
|
+
});
|
|
4596
|
+
bot.command("use <name>").description("\u9009\u62E9 run / start \u542F\u52A8\u65F6\u4F7F\u7528\u7684\u673A\u5668\u4EBA").action(async (name) => {
|
|
4597
|
+
await runBotUse(name);
|
|
4598
|
+
});
|
|
4599
|
+
bot.command("rm <name>").description("\u79FB\u9664\u4E00\u4E2A\u673A\u5668\u4EBA\u914D\u7F6E").action(async (name) => {
|
|
4600
|
+
await runBotRm(name);
|
|
4601
|
+
});
|
|
4602
|
+
program.command("doctor").description("\u672C\u5730\u81EA\u68C0\uFF1Acodex / \u767B\u5F55 / lark-cli / \u5F53\u524D\u673A\u5668\u4EBA\u914D\u7F6E").action(async () => {
|
|
4603
|
+
await runDoctor();
|
|
4604
|
+
});
|
|
4605
|
+
var secrets = program.command("secrets", { hidden: true }).description("\u672C\u5730\u52A0\u5BC6\u5BC6\u94A5\u5E93\uFF08\u5185\u90E8\uFF09");
|
|
4606
|
+
secrets.command("get").description("exec-provider \u7AEF\u70B9\uFF08\u4ECE stdin \u8BFB JSON \u8BF7\u6C42\uFF09").action(async () => {
|
|
4607
|
+
await secretsGet();
|
|
4608
|
+
});
|
|
4609
|
+
secrets.command("set <id>").description("\u5B58\u50A8\u5BC6\u94A5\uFF08\u503C\u4ECE stdin \u8BFB\uFF09").action(async (id) => {
|
|
4610
|
+
await secretsSet(id);
|
|
4611
|
+
});
|
|
4612
|
+
secrets.command("list").description("\u5217\u51FA\u5BC6\u94A5 id").action(async () => {
|
|
4613
|
+
await secretsList();
|
|
4614
|
+
});
|
|
4615
|
+
secrets.command("remove <id>").description("\u5220\u9664\u5BC6\u94A5").action(async (id) => {
|
|
4616
|
+
await secretsRemove(id);
|
|
4617
|
+
});
|
|
4618
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
4619
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
4620
|
+
process.exit(1);
|
|
4621
|
+
});
|