@modelzen/feishu-codex-bridge 0.2.1-win → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +261 -352
- package/package.json +1 -3
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ function bridgeVersion() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// src/cli/commands/doctor.ts
|
|
18
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
18
19
|
import { existsSync as existsSync3 } from "fs";
|
|
19
20
|
import { homedir as homedir2 } from "os";
|
|
20
21
|
import { join as join4 } from "path";
|
|
@@ -268,56 +269,27 @@ async function moveIfExists(src, dest) {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
// src/agent/codex-appserver/locate.ts
|
|
272
|
+
import { execFileSync } from "child_process";
|
|
271
273
|
import { existsSync as existsSync2 } from "fs";
|
|
272
|
-
import {
|
|
273
|
-
|
|
274
|
-
// src/platform/spawn.ts
|
|
275
|
-
import crossSpawn from "cross-spawn";
|
|
276
|
-
function spawnProcess(command, args = [], options = {}) {
|
|
277
|
-
return crossSpawn(command, [...args], options);
|
|
278
|
-
}
|
|
279
|
-
function spawnProcessSync(command, args = [], options = {}) {
|
|
280
|
-
return crossSpawn.sync(command, [...args], options);
|
|
281
|
-
}
|
|
282
|
-
function mergeProcessEnv(base = process.env, overrides = {}) {
|
|
283
|
-
const out = { ...base };
|
|
284
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
285
|
-
for (const existing of Object.keys(out)) {
|
|
286
|
-
if (existing.toLowerCase() === key.toLowerCase()) delete out[existing];
|
|
287
|
-
}
|
|
288
|
-
if (value !== void 0) out[key] = value;
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// src/agent/codex-appserver/locate.ts
|
|
294
|
-
var IS_WIN = process.platform === "win32";
|
|
274
|
+
import { join as join3 } from "path";
|
|
295
275
|
function resolveCodexBin() {
|
|
296
276
|
const env = process.env.CODEX_BIN;
|
|
297
277
|
if (env && existsSync2(env)) return env;
|
|
298
278
|
const onPath = which("codex");
|
|
299
279
|
if (onPath) return onPath;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
280
|
+
const priv = join3(paths.codexCliBinDir, "codex");
|
|
281
|
+
if (existsSync2(priv)) return priv;
|
|
303
282
|
const appBundle = "/Applications/Codex.app/Contents/Resources/codex";
|
|
304
283
|
if (process.platform === "darwin" && existsSync2(appBundle)) return appBundle;
|
|
305
284
|
return null;
|
|
306
285
|
}
|
|
307
|
-
function execCandidates(dir, base) {
|
|
308
|
-
const exact = join3(dir, base);
|
|
309
|
-
if (!IS_WIN || extname(base)) return [exact];
|
|
310
|
-
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
311
|
-
return [exact, ...exts.map((e) => join3(dir, base + e.toLowerCase()))];
|
|
312
|
-
}
|
|
313
286
|
function which(cmd) {
|
|
314
287
|
try {
|
|
315
|
-
const
|
|
288
|
+
const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
|
|
316
289
|
encoding: "utf8",
|
|
317
290
|
stdio: ["ignore", "pipe", "ignore"]
|
|
318
291
|
});
|
|
319
|
-
|
|
320
|
-
const first = res.stdout.split("\n").map((l) => l.trim()).find(Boolean);
|
|
292
|
+
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
321
293
|
return first && existsSync2(first) ? first : null;
|
|
322
294
|
} catch {
|
|
323
295
|
return null;
|
|
@@ -325,9 +297,7 @@ function which(cmd) {
|
|
|
325
297
|
}
|
|
326
298
|
function codexVersion(bin) {
|
|
327
299
|
try {
|
|
328
|
-
|
|
329
|
-
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
330
|
-
return res.stdout.trim();
|
|
300
|
+
return execFileSync(bin, ["--version"], { encoding: "utf8" }).trim();
|
|
331
301
|
} catch {
|
|
332
302
|
return null;
|
|
333
303
|
}
|
|
@@ -385,9 +355,7 @@ ${failed === 0 ? "\u5168\u90E8\u901A\u8FC7 \u2713" : `${failed} \u9879\u9700\u59
|
|
|
385
355
|
}
|
|
386
356
|
function tryExec(cmd, args) {
|
|
387
357
|
try {
|
|
388
|
-
|
|
389
|
-
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
390
|
-
return res.stdout.trim();
|
|
358
|
+
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
391
359
|
} catch {
|
|
392
360
|
return null;
|
|
393
361
|
}
|
|
@@ -570,7 +538,7 @@ async function spawnExecProvider(pc, ref) {
|
|
|
570
538
|
const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
|
|
571
539
|
const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
|
|
572
540
|
const providerName = ref.provider ?? DEFAULT_PROVIDER;
|
|
573
|
-
return new Promise((
|
|
541
|
+
return new Promise((resolve6, reject) => {
|
|
574
542
|
const env = {};
|
|
575
543
|
if (pc.passEnv) for (const k of pc.passEnv) {
|
|
576
544
|
const v = process.env[k];
|
|
@@ -615,7 +583,7 @@ async function spawnExecProvider(pc, ref) {
|
|
|
615
583
|
try {
|
|
616
584
|
const parsed = JSON.parse(stdout);
|
|
617
585
|
const value = parsed.values?.[ref.id];
|
|
618
|
-
if (typeof value === "string") return
|
|
586
|
+
if (typeof value === "string") return resolve6(value);
|
|
619
587
|
const err = parsed.errors?.[ref.id]?.message;
|
|
620
588
|
reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
|
|
621
589
|
} catch (err) {
|
|
@@ -1163,6 +1131,7 @@ ${rule}`);
|
|
|
1163
1131
|
import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
|
|
1164
1132
|
|
|
1165
1133
|
// src/agent/codex-appserver/app-server-client.ts
|
|
1134
|
+
import { spawn as spawn3 } from "child_process";
|
|
1166
1135
|
var AsyncQueue = class {
|
|
1167
1136
|
items = [];
|
|
1168
1137
|
waiters = [];
|
|
@@ -1183,7 +1152,7 @@ var AsyncQueue = class {
|
|
|
1183
1152
|
continue;
|
|
1184
1153
|
}
|
|
1185
1154
|
if (this.closed) return;
|
|
1186
|
-
const next = await new Promise((
|
|
1155
|
+
const next = await new Promise((resolve6) => this.waiters.push(resolve6));
|
|
1187
1156
|
if (next.done) return;
|
|
1188
1157
|
yield next.value;
|
|
1189
1158
|
}
|
|
@@ -1205,9 +1174,9 @@ var AppServerClient = class {
|
|
|
1205
1174
|
}
|
|
1206
1175
|
/** spawn + initialize handshake. Throws if spawn/handshake fails. */
|
|
1207
1176
|
async connect() {
|
|
1208
|
-
const child =
|
|
1177
|
+
const child = spawn3(this.opts.bin, ["app-server", "--listen", "stdio://"], {
|
|
1209
1178
|
cwd: this.opts.cwd,
|
|
1210
|
-
env:
|
|
1179
|
+
env: { ...process.env, ...this.opts.env, FEISHU_CODEX_BRIDGE: "1" },
|
|
1211
1180
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1212
1181
|
});
|
|
1213
1182
|
this.child = child;
|
|
@@ -1234,8 +1203,8 @@ var AppServerClient = class {
|
|
|
1234
1203
|
const id = ++this.nextId;
|
|
1235
1204
|
const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
|
|
1236
1205
|
`;
|
|
1237
|
-
return new Promise((
|
|
1238
|
-
this.pending.set(id, { resolve:
|
|
1206
|
+
return new Promise((resolve6, reject) => {
|
|
1207
|
+
this.pending.set(id, { resolve: resolve6, reject });
|
|
1239
1208
|
this.child.stdin.write(payload, (err) => {
|
|
1240
1209
|
if (err) {
|
|
1241
1210
|
this.pending.delete(id);
|
|
@@ -1258,36 +1227,15 @@ var AppServerClient = class {
|
|
|
1258
1227
|
this.closed = true;
|
|
1259
1228
|
const child = this.child;
|
|
1260
1229
|
if (!child || child.exitCode !== null) return;
|
|
1261
|
-
if (process.platform === "win32" && child.pid) {
|
|
1262
|
-
await new Promise((resolve7) => {
|
|
1263
|
-
let settled = false;
|
|
1264
|
-
const done = () => {
|
|
1265
|
-
if (settled) return;
|
|
1266
|
-
settled = true;
|
|
1267
|
-
clearTimeout(t);
|
|
1268
|
-
resolve7();
|
|
1269
|
-
};
|
|
1270
|
-
const t = setTimeout(done, graceMs);
|
|
1271
|
-
child.once("exit", done);
|
|
1272
|
-
spawnProcess("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }).on(
|
|
1273
|
-
"error",
|
|
1274
|
-
() => {
|
|
1275
|
-
child.kill();
|
|
1276
|
-
done();
|
|
1277
|
-
}
|
|
1278
|
-
);
|
|
1279
|
-
});
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
1230
|
child.kill("SIGTERM");
|
|
1283
|
-
await new Promise((
|
|
1231
|
+
await new Promise((resolve6) => {
|
|
1284
1232
|
const t = setTimeout(() => {
|
|
1285
1233
|
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1286
|
-
|
|
1234
|
+
resolve6();
|
|
1287
1235
|
}, graceMs);
|
|
1288
1236
|
child.once("exit", () => {
|
|
1289
1237
|
clearTimeout(t);
|
|
1290
|
-
|
|
1238
|
+
resolve6();
|
|
1291
1239
|
});
|
|
1292
1240
|
});
|
|
1293
1241
|
}
|
|
@@ -1420,12 +1368,12 @@ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
|
|
|
1420
1368
|
].join("\n");
|
|
1421
1369
|
var READ_HISTORY_TIMEOUT_MS = 2e4;
|
|
1422
1370
|
function withDeadline(p, ms, label) {
|
|
1423
|
-
return new Promise((
|
|
1371
|
+
return new Promise((resolve6, reject) => {
|
|
1424
1372
|
const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
1425
1373
|
p.then(
|
|
1426
1374
|
(v) => {
|
|
1427
1375
|
clearTimeout(t);
|
|
1428
|
-
|
|
1376
|
+
resolve6(v);
|
|
1429
1377
|
},
|
|
1430
1378
|
(e) => {
|
|
1431
1379
|
clearTimeout(t);
|
|
@@ -1465,11 +1413,11 @@ var CodexThread = class {
|
|
|
1465
1413
|
if (self.model) params.model = self.model;
|
|
1466
1414
|
if (self.effort) params.effort = self.effort;
|
|
1467
1415
|
let startError;
|
|
1468
|
-
const startFailed = new Promise((
|
|
1416
|
+
const startFailed = new Promise((resolve6) => {
|
|
1469
1417
|
self.client.request("turn/start", params).then(void 0, (err) => {
|
|
1470
1418
|
startError = err instanceof Error ? err : new Error(String(err));
|
|
1471
1419
|
log.fail("agent", startError, { phase: "turn/start" });
|
|
1472
|
-
|
|
1420
|
+
resolve6("start-failed");
|
|
1473
1421
|
});
|
|
1474
1422
|
});
|
|
1475
1423
|
const stream2 = self.client.stream()[Symbol.asyncIterator]();
|
|
@@ -2013,8 +1961,8 @@ function card(elements, opts = {}) {
|
|
|
2013
1961
|
if (opts.streaming) {
|
|
2014
1962
|
config.streaming_mode = true;
|
|
2015
1963
|
config.streaming_config = {
|
|
2016
|
-
print_frequency_ms: { default:
|
|
2017
|
-
print_step: { default:
|
|
1964
|
+
print_frequency_ms: { default: 25 },
|
|
1965
|
+
print_step: { default: 6 },
|
|
2018
1966
|
print_strategy: "fast"
|
|
2019
1967
|
};
|
|
2020
1968
|
}
|
|
@@ -2036,6 +1984,9 @@ function card(elements, opts = {}) {
|
|
|
2036
1984
|
function md(content) {
|
|
2037
1985
|
return { tag: "markdown", content };
|
|
2038
1986
|
}
|
|
1987
|
+
function mdStream(content, elementId) {
|
|
1988
|
+
return { tag: "markdown", element_id: elementId, content };
|
|
1989
|
+
}
|
|
2039
1990
|
function image(imgKey, alt = "") {
|
|
2040
1991
|
return {
|
|
2041
1992
|
tag: "img",
|
|
@@ -2585,6 +2536,7 @@ function escapeInline2(s) {
|
|
|
2585
2536
|
var RC = {
|
|
2586
2537
|
stop: "run.stop"
|
|
2587
2538
|
};
|
|
2539
|
+
var ANSWER_EID = "answer";
|
|
2588
2540
|
var REASONING_MAX = 1500;
|
|
2589
2541
|
var COLLAPSE_TOOL_THRESHOLD = 3;
|
|
2590
2542
|
var PROCESS_BODY_BUDGET = 22e3;
|
|
@@ -2598,14 +2550,19 @@ function renderRunning(state, rc) {
|
|
|
2598
2550
|
const elements = [];
|
|
2599
2551
|
const reasoning = reasoningContent(state);
|
|
2600
2552
|
if (reasoning) elements.push(reasoningPanel(reasoning, state.reasoningActive));
|
|
2601
|
-
const
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2553
|
+
const showTools = rc.showTools !== false;
|
|
2554
|
+
const tools = [];
|
|
2555
|
+
const textParts = [];
|
|
2556
|
+
for (const b of state.blocks) {
|
|
2557
|
+
if (b.kind === "tool") {
|
|
2558
|
+
if (showTools) tools.push(b.tool);
|
|
2559
|
+
} else if (b.content.trim()) {
|
|
2560
|
+
textParts.push(b.content);
|
|
2607
2561
|
}
|
|
2608
2562
|
}
|
|
2563
|
+
if (tools.length > 0) elements.push(...renderToolGroup(tools, false));
|
|
2564
|
+
const answer = textParts.join("\n\n");
|
|
2565
|
+
if (answer) elements.push(mdStream(answer, ANSWER_EID));
|
|
2609
2566
|
if (state.footer) elements.push(footerStatus(state.footer));
|
|
2610
2567
|
if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
|
|
2611
2568
|
return elements;
|
|
@@ -2752,16 +2709,105 @@ function truncate4(s, n) {
|
|
|
2752
2709
|
}
|
|
2753
2710
|
|
|
2754
2711
|
// src/card/run-card-stream.ts
|
|
2755
|
-
var STREAM_THROTTLE_MS =
|
|
2712
|
+
var STREAM_THROTTLE_MS = 150;
|
|
2756
2713
|
var RunCardStream = class {
|
|
2757
2714
|
cardId = "";
|
|
2758
2715
|
_messageId = "";
|
|
2759
2716
|
seq = 0;
|
|
2760
2717
|
lastPush = 0;
|
|
2761
2718
|
lastContent = "";
|
|
2719
|
+
// Per-turn push counters — surfaced via stats() for the stream.timing log.
|
|
2720
|
+
pushCount = 0;
|
|
2721
|
+
cardPushes = 0;
|
|
2722
|
+
// whole-card card.update (structure)
|
|
2723
|
+
elPushes = 0;
|
|
2724
|
+
// element cardElement.content (answer typewriter)
|
|
2725
|
+
totalRttMs = 0;
|
|
2726
|
+
maxRttMs = 0;
|
|
2727
|
+
// Coalesced streaming. The consume loop records the latest {card, answerEid} in
|
|
2728
|
+
// `pending`; a single pump() drains it — NON-BLOCKING, so consuming codex events
|
|
2729
|
+
// never stalls on a round-trip (awaiting each push serially drained the backlog
|
|
2730
|
+
// at one event per ~RTT → "已回完、飞书还在慢慢打字"). The pump pushes only the most
|
|
2731
|
+
// recent snapshot per round-trip: when only the answer text grew it streams that
|
|
2732
|
+
// via cardElement.content (typewriter), otherwise it whole-card updates.
|
|
2733
|
+
pending = null;
|
|
2734
|
+
pumpChannel = null;
|
|
2735
|
+
pumpPromise = null;
|
|
2736
|
+
// Baselines for the pump's route decision (structure unchanged + answer grew?).
|
|
2737
|
+
lastStructureSig = "";
|
|
2738
|
+
lastAnswerText = "";
|
|
2762
2739
|
get messageId() {
|
|
2763
2740
|
return this._messageId;
|
|
2764
2741
|
}
|
|
2742
|
+
/** Push counts (whole-card vs element) + round-trip stats for this card. */
|
|
2743
|
+
stats() {
|
|
2744
|
+
return {
|
|
2745
|
+
pushCount: this.pushCount,
|
|
2746
|
+
cardPushes: this.cardPushes,
|
|
2747
|
+
elPushes: this.elPushes,
|
|
2748
|
+
totalRttMs: this.totalRttMs,
|
|
2749
|
+
maxRttMs: this.maxRttMs
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Record the latest card and ensure the pump is running. Returns immediately;
|
|
2754
|
+
* calls that arrive while a push is in flight collapse into a single push of
|
|
2755
|
+
* the most recent card once that round-trip completes. Use this from the
|
|
2756
|
+
* event-consume loop instead of awaiting {@link streamCard} per event.
|
|
2757
|
+
*/
|
|
2758
|
+
streamCoalesced(channel, fullCard, answerEid) {
|
|
2759
|
+
this.pending = { card: fullCard, answerEid };
|
|
2760
|
+
this.pumpChannel = channel;
|
|
2761
|
+
if (!this.pumpPromise) this.pumpPromise = this.pump();
|
|
2762
|
+
}
|
|
2763
|
+
/** Await any in-flight coalesced push so the final streaming frame lands (and
|
|
2764
|
+
* `seq` stays ordered) before the terminal update. Call after the loop ends. */
|
|
2765
|
+
async drain() {
|
|
2766
|
+
if (this.pumpPromise) await this.pumpPromise;
|
|
2767
|
+
}
|
|
2768
|
+
async pump() {
|
|
2769
|
+
try {
|
|
2770
|
+
while (this.pending && this.pumpChannel) {
|
|
2771
|
+
const { card: card2, answerEid } = this.pending;
|
|
2772
|
+
this.pending = null;
|
|
2773
|
+
const t0 = Date.now();
|
|
2774
|
+
const answer = answerEid ? answerContent(card2, answerEid) : null;
|
|
2775
|
+
const sig = structureSig(card2, answerEid);
|
|
2776
|
+
if (answerEid && answer !== null && sig === this.lastStructureSig && answer !== this.lastAnswerText && answer.startsWith(this.lastAnswerText)) {
|
|
2777
|
+
await this.streamElement(this.pumpChannel, answerEid, answer);
|
|
2778
|
+
this.lastAnswerText = answer;
|
|
2779
|
+
} else {
|
|
2780
|
+
await this.streamCard(this.pumpChannel, card2, true);
|
|
2781
|
+
this.lastStructureSig = sig;
|
|
2782
|
+
this.lastAnswerText = answer ?? "";
|
|
2783
|
+
}
|
|
2784
|
+
const gap = STREAM_THROTTLE_MS - (Date.now() - t0);
|
|
2785
|
+
if (this.pending && gap > 0) await new Promise((r) => setTimeout(r, gap));
|
|
2786
|
+
}
|
|
2787
|
+
} finally {
|
|
2788
|
+
this.pumpPromise = null;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
/** Element-level streaming push (cardkit.v1.cardElement.content): the answer
|
|
2792
|
+
* element's accumulated full text. Feishu diffs the prefix and types the delta
|
|
2793
|
+
* per the card's streaming_config. Needs streaming_mode on the card. */
|
|
2794
|
+
async streamElement(channel, elementId, content) {
|
|
2795
|
+
if (!this.cardId) return;
|
|
2796
|
+
const t0 = Date.now();
|
|
2797
|
+
try {
|
|
2798
|
+
await channel.rawClient.cardkit.v1.cardElement.content({
|
|
2799
|
+
path: { card_id: this.cardId, element_id: elementId },
|
|
2800
|
+
data: { content, sequence: ++this.seq, uuid: `e_${this.cardId}_${this.seq}` }
|
|
2801
|
+
});
|
|
2802
|
+
const rtt = Date.now() - t0;
|
|
2803
|
+
this.pushCount++;
|
|
2804
|
+
this.elPushes++;
|
|
2805
|
+
this.totalRttMs += rtt;
|
|
2806
|
+
if (rtt > this.maxRttMs) this.maxRttMs = rtt;
|
|
2807
|
+
} catch (err) {
|
|
2808
|
+
log.fail("card", err, { phase: "run-stream-el", cardId: this.cardId, seq: this.seq });
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2765
2811
|
/** Create the entity from the initial (running) card and send a message
|
|
2766
2812
|
* referencing it by card_id. Returns the carrier message id. */
|
|
2767
2813
|
async create(channel, chatId, initialCard, opts) {
|
|
@@ -2803,11 +2849,17 @@ var RunCardStream = class {
|
|
|
2803
2849
|
if (!force && now - this.lastPush < STREAM_THROTTLE_MS) return;
|
|
2804
2850
|
this.lastPush = now;
|
|
2805
2851
|
this.lastContent = data;
|
|
2852
|
+
const t0 = Date.now();
|
|
2806
2853
|
try {
|
|
2807
2854
|
await channel.rawClient.cardkit.v1.card.update({
|
|
2808
2855
|
path: { card_id: this.cardId },
|
|
2809
2856
|
data: { card: { type: "card_json", data }, sequence: ++this.seq, uuid: `s_${this.cardId}_${this.seq}` }
|
|
2810
2857
|
});
|
|
2858
|
+
const rtt = Date.now() - t0;
|
|
2859
|
+
this.pushCount++;
|
|
2860
|
+
this.cardPushes++;
|
|
2861
|
+
this.totalRttMs += rtt;
|
|
2862
|
+
if (rtt > this.maxRttMs) this.maxRttMs = rtt;
|
|
2811
2863
|
} catch (err) {
|
|
2812
2864
|
log.fail("card", err, { phase: "run-stream", cardId: this.cardId, seq: this.seq });
|
|
2813
2865
|
}
|
|
@@ -2837,10 +2889,25 @@ var RunCardStream = class {
|
|
|
2837
2889
|
}
|
|
2838
2890
|
}
|
|
2839
2891
|
};
|
|
2892
|
+
function answerContent(card2, eid) {
|
|
2893
|
+
const els = card2.body?.elements;
|
|
2894
|
+
if (!Array.isArray(els)) return null;
|
|
2895
|
+
for (const el of els) {
|
|
2896
|
+
if (el && el.element_id === eid) return typeof el.content === "string" ? el.content : "";
|
|
2897
|
+
}
|
|
2898
|
+
return null;
|
|
2899
|
+
}
|
|
2900
|
+
function structureSig(card2, eid) {
|
|
2901
|
+
const body = card2.body;
|
|
2902
|
+
const els = body?.elements;
|
|
2903
|
+
if (!eid || !Array.isArray(els)) return JSON.stringify(card2);
|
|
2904
|
+
const blanked = els.map((el) => el && el.element_id === eid ? { ...el, content: "" } : el);
|
|
2905
|
+
return JSON.stringify({ ...card2, body: { ...body, elements: blanked } });
|
|
2906
|
+
}
|
|
2840
2907
|
|
|
2841
2908
|
// src/card/outbound-images.ts
|
|
2842
2909
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
2843
|
-
import { extname
|
|
2910
|
+
import { extname, isAbsolute, resolve as resolve2, sep } from "path";
|
|
2844
2911
|
var MAX_IMAGES = 9;
|
|
2845
2912
|
var MAX_BYTES = 10 * 1024 * 1024;
|
|
2846
2913
|
var DOWNLOAD_TIMEOUT_MS = 1e4;
|
|
@@ -2906,7 +2973,7 @@ async function loadLocal(src, cwd) {
|
|
|
2906
2973
|
log.warn("outbound", "image-outside-cwd", { src: src.slice(0, 80) });
|
|
2907
2974
|
return { cacheKey: `local:${abs}` };
|
|
2908
2975
|
}
|
|
2909
|
-
const ext =
|
|
2976
|
+
const ext = extname(abs).slice(1).toLowerCase();
|
|
2910
2977
|
if (!ALLOWED_EXT.has(ext)) {
|
|
2911
2978
|
log.warn("outbound", "image-ext", { ext, src: src.slice(0, 80) });
|
|
2912
2979
|
return { cacheKey: `local:${abs}` };
|
|
@@ -3437,40 +3504,29 @@ function buildGroupSettingsCard(project) {
|
|
|
3437
3504
|
}
|
|
3438
3505
|
|
|
3439
3506
|
// src/service/update.ts
|
|
3440
|
-
import { execFile, spawn as
|
|
3441
|
-
import { existsSync as
|
|
3442
|
-
import { dirname as
|
|
3443
|
-
import { fileURLToPath as
|
|
3507
|
+
import { execFile, spawn as spawn5 } from "child_process";
|
|
3508
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
3509
|
+
import { dirname as dirname7, join as join8, resolve as resolve4 } from "path";
|
|
3510
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3444
3511
|
import { promisify } from "util";
|
|
3445
3512
|
|
|
3446
3513
|
// src/service/launchd.ts
|
|
3447
|
-
import { spawn as
|
|
3514
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
3448
3515
|
import { existsSync as existsSync4 } from "fs";
|
|
3449
|
-
import { mkdir as
|
|
3516
|
+
import { appendFile, mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
3450
3517
|
import { homedir as homedir3, userInfo as userInfo2 } from "os";
|
|
3451
|
-
import { dirname as dirname6, join as
|
|
3518
|
+
import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
|
|
3452
3519
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3520
|
+
var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
|
|
3521
|
+
function launchAgentPlistPath() {
|
|
3522
|
+
return join7(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
3523
|
+
}
|
|
3457
3524
|
function serviceStdoutPath() {
|
|
3458
3525
|
return join7(paths.appDir, "service.log");
|
|
3459
3526
|
}
|
|
3460
3527
|
function serviceStderrPath() {
|
|
3461
3528
|
return join7(paths.appDir, "service.err.log");
|
|
3462
3529
|
}
|
|
3463
|
-
async function ensureLogFiles() {
|
|
3464
|
-
await mkdir5(paths.appDir, { recursive: true });
|
|
3465
|
-
await appendFile(serviceStdoutPath(), "");
|
|
3466
|
-
await appendFile(serviceStderrPath(), "");
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
// src/service/launchd.ts
|
|
3470
|
-
var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
|
|
3471
|
-
function launchAgentPlistPath() {
|
|
3472
|
-
return join8(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
3473
|
-
}
|
|
3474
3530
|
function resolveCliBinPath() {
|
|
3475
3531
|
const distDir = dirname6(fileURLToPath2(import.meta.url));
|
|
3476
3532
|
return resolve3(distDir, "..", "bin", "feishu-codex-bridge.mjs");
|
|
@@ -3513,7 +3569,7 @@ function buildPlist() {
|
|
|
3513
3569
|
}
|
|
3514
3570
|
async function installLaunchd() {
|
|
3515
3571
|
const plistPath = launchAgentPlistPath();
|
|
3516
|
-
await
|
|
3572
|
+
await mkdir5(dirname6(plistPath), { recursive: true });
|
|
3517
3573
|
await ensureLogFiles();
|
|
3518
3574
|
await writeFile5(plistPath, buildPlist(), "utf8");
|
|
3519
3575
|
if (isLoaded()) {
|
|
@@ -3551,10 +3607,9 @@ function statusLaunchd() {
|
|
|
3551
3607
|
const raw = result.stdout || result.stderr;
|
|
3552
3608
|
const parsed = parseLaunchdStatus(raw);
|
|
3553
3609
|
return {
|
|
3554
|
-
platformName: "launchd (macOS)",
|
|
3555
3610
|
installed: existsSync4(launchAgentPlistPath()),
|
|
3556
|
-
|
|
3557
|
-
|
|
3611
|
+
loaded: result.ok,
|
|
3612
|
+
plistPath: launchAgentPlistPath(),
|
|
3558
3613
|
stdoutPath: serviceStdoutPath(),
|
|
3559
3614
|
stderrPath: serviceStderrPath(),
|
|
3560
3615
|
pid: parsed.pid,
|
|
@@ -3566,7 +3621,7 @@ async function tailLaunchdLogs(follow) {
|
|
|
3566
3621
|
await ensureLogFiles();
|
|
3567
3622
|
const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
|
|
3568
3623
|
await new Promise((resolvePromise, reject) => {
|
|
3569
|
-
const child =
|
|
3624
|
+
const child = spawn4("tail", args, { stdio: "inherit" });
|
|
3570
3625
|
child.on("error", reject);
|
|
3571
3626
|
child.on("close", (code) => {
|
|
3572
3627
|
if (code === 0 || follow && code === null) {
|
|
@@ -3597,6 +3652,11 @@ async function waitUntilUnloaded(timeoutMs = 5e3) {
|
|
|
3597
3652
|
}
|
|
3598
3653
|
throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
|
|
3599
3654
|
}
|
|
3655
|
+
async function ensureLogFiles() {
|
|
3656
|
+
await mkdir5(paths.appDir, { recursive: true });
|
|
3657
|
+
await appendFile(serviceStdoutPath(), "");
|
|
3658
|
+
await appendFile(serviceStderrPath(), "");
|
|
3659
|
+
}
|
|
3600
3660
|
function userTarget() {
|
|
3601
3661
|
return `gui/${userInfo2().uid}`;
|
|
3602
3662
|
}
|
|
@@ -3617,218 +3677,29 @@ function launchctlError(command, result) {
|
|
|
3617
3677
|
return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
|
|
3618
3678
|
}
|
|
3619
3679
|
|
|
3620
|
-
// src/service/schtasks.ts
|
|
3621
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
3622
|
-
import { createReadStream, existsSync as existsSync5, statSync } from "fs";
|
|
3623
|
-
import { mkdir as mkdir7, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "fs/promises";
|
|
3624
|
-
import { dirname as dirname7, join as join9, resolve as resolve4 } from "path";
|
|
3625
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3626
|
-
var WINDOWS_TASK_NAME = "feishu-codex-bridge";
|
|
3627
|
-
function launcherCmdPath() {
|
|
3628
|
-
return join9(paths.appDir, "service-launcher.cmd");
|
|
3629
|
-
}
|
|
3630
|
-
function resolveCliBinPath2() {
|
|
3631
|
-
const distDir = dirname7(fileURLToPath3(import.meta.url));
|
|
3632
|
-
return resolve4(distDir, "..", "bin", "feishu-codex-bridge.mjs");
|
|
3633
|
-
}
|
|
3634
|
-
function buildLauncherCmd() {
|
|
3635
|
-
const nodePath = process.execPath;
|
|
3636
|
-
const cliBinPath = resolveCliBinPath2();
|
|
3637
|
-
const pathEnv = process.env.PATH ?? "";
|
|
3638
|
-
return [
|
|
3639
|
-
"@echo off",
|
|
3640
|
-
`set "PATH=${pathEnv}"`,
|
|
3641
|
-
`"${nodePath}" "${cliBinPath}" run >> "${serviceStdoutPath()}" 2>> "${serviceStderrPath()}"`,
|
|
3642
|
-
""
|
|
3643
|
-
].join("\r\n");
|
|
3644
|
-
}
|
|
3645
|
-
function runSchtasks(args) {
|
|
3646
|
-
const r = spawnSync2("schtasks", args, { encoding: "utf8" });
|
|
3647
|
-
return {
|
|
3648
|
-
ok: r.status === 0,
|
|
3649
|
-
status: r.status,
|
|
3650
|
-
stdout: r.stdout ?? "",
|
|
3651
|
-
stderr: r.stderr ?? ""
|
|
3652
|
-
};
|
|
3653
|
-
}
|
|
3654
|
-
function schtasksError(command, r) {
|
|
3655
|
-
const out = [r.stderr.trim(), r.stdout.trim()].filter(Boolean).join("\n");
|
|
3656
|
-
return new Error(`${command} \u5931\u8D25\uFF08exit ${r.status ?? "unknown"}\uFF09${out ? `\uFF1A${out}` : ""}`);
|
|
3657
|
-
}
|
|
3658
|
-
async function writeLauncherCmd() {
|
|
3659
|
-
const cmdPath = launcherCmdPath();
|
|
3660
|
-
await mkdir7(dirname7(cmdPath), { recursive: true });
|
|
3661
|
-
await ensureLogFiles();
|
|
3662
|
-
await writeFile6(cmdPath, buildLauncherCmd(), "utf8");
|
|
3663
|
-
}
|
|
3664
|
-
async function installSchtask() {
|
|
3665
|
-
await writeLauncherCmd();
|
|
3666
|
-
const create = runSchtasks([
|
|
3667
|
-
"/Create",
|
|
3668
|
-
"/F",
|
|
3669
|
-
"/SC",
|
|
3670
|
-
"ONLOGON",
|
|
3671
|
-
"/RL",
|
|
3672
|
-
"LIMITED",
|
|
3673
|
-
"/TN",
|
|
3674
|
-
WINDOWS_TASK_NAME,
|
|
3675
|
-
"/TR",
|
|
3676
|
-
`"${launcherCmdPath()}"`
|
|
3677
|
-
]);
|
|
3678
|
-
if (!create.ok) throw schtasksError("schtasks /Create", create);
|
|
3679
|
-
const run = runSchtasks(["/Run", "/TN", WINDOWS_TASK_NAME]);
|
|
3680
|
-
if (!run.ok) throw schtasksError("schtasks /Run", run);
|
|
3681
|
-
return statusSchtask();
|
|
3682
|
-
}
|
|
3683
|
-
async function uninstallSchtask() {
|
|
3684
|
-
runSchtasks(["/End", "/TN", WINDOWS_TASK_NAME]);
|
|
3685
|
-
const del = runSchtasks(["/Delete", "/F", "/TN", WINDOWS_TASK_NAME]);
|
|
3686
|
-
if (!del.ok && isTaskRegistered()) throw schtasksError("schtasks /Delete", del);
|
|
3687
|
-
if (existsSync5(launcherCmdPath())) await rm3(launcherCmdPath(), { force: true });
|
|
3688
|
-
}
|
|
3689
|
-
async function restartSchtask() {
|
|
3690
|
-
if (!isTaskRegistered()) {
|
|
3691
|
-
throw new Error(`\u8BA1\u5212\u4EFB\u52A1\u672A\u5B89\u88C5\uFF1A${WINDOWS_TASK_NAME}\uFF08\u5148\u8FD0\u884C \`feishu-codex-bridge start\`\uFF09`);
|
|
3692
|
-
}
|
|
3693
|
-
runSchtasks(["/End", "/TN", WINDOWS_TASK_NAME]);
|
|
3694
|
-
await waitUntilStopped();
|
|
3695
|
-
const run = runSchtasks(["/Run", "/TN", WINDOWS_TASK_NAME]);
|
|
3696
|
-
if (!run.ok) throw schtasksError("schtasks /Run", run);
|
|
3697
|
-
return statusSchtask();
|
|
3698
|
-
}
|
|
3699
|
-
function statusSchtask() {
|
|
3700
|
-
const installed = isTaskRegistered();
|
|
3701
|
-
const raw = installed ? describeTask() : "";
|
|
3702
|
-
return {
|
|
3703
|
-
platformName: "Task Scheduler (Windows)",
|
|
3704
|
-
installed,
|
|
3705
|
-
running: installed && /Status:\s+Running/i.test(raw),
|
|
3706
|
-
servicePath: WINDOWS_TASK_NAME,
|
|
3707
|
-
stdoutPath: serviceStdoutPath(),
|
|
3708
|
-
stderrPath: serviceStderrPath(),
|
|
3709
|
-
// `Process ID:` only appears in verbose output while the task is running.
|
|
3710
|
-
pid: raw.match(/Process ID:\s*(\d+)/i)?.[1],
|
|
3711
|
-
// `Last Result: 0` ⇒ last run succeeded. Surface it as the exit code.
|
|
3712
|
-
lastExit: raw.match(/Last Result:\s*(-?\d+)/i)?.[1],
|
|
3713
|
-
raw
|
|
3714
|
-
};
|
|
3715
|
-
}
|
|
3716
|
-
function isTaskRegistered() {
|
|
3717
|
-
const r = spawnSync2("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME], {
|
|
3718
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
3719
|
-
});
|
|
3720
|
-
return r.status === 0;
|
|
3721
|
-
}
|
|
3722
|
-
function schtaskRunning() {
|
|
3723
|
-
if (!isTaskRegistered()) return false;
|
|
3724
|
-
return /Status:\s+Running/i.test(describeTask());
|
|
3725
|
-
}
|
|
3726
|
-
function describeTask() {
|
|
3727
|
-
const r = runSchtasks(["/Query", "/V", "/FO", "LIST", "/TN", WINDOWS_TASK_NAME]);
|
|
3728
|
-
return r.stdout || r.stderr || "";
|
|
3729
|
-
}
|
|
3730
|
-
async function waitUntilStopped(timeoutMs = 5e3) {
|
|
3731
|
-
const deadline = Date.now() + timeoutMs;
|
|
3732
|
-
while (Date.now() < deadline) {
|
|
3733
|
-
if (!schtaskRunning()) return true;
|
|
3734
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
3735
|
-
}
|
|
3736
|
-
return false;
|
|
3737
|
-
}
|
|
3738
|
-
async function tailSchtaskLogs(follow) {
|
|
3739
|
-
await ensureLogFiles();
|
|
3740
|
-
const files = [serviceStdoutPath(), serviceStderrPath()];
|
|
3741
|
-
for (const f of files) {
|
|
3742
|
-
const tail = await lastLines(f, 100);
|
|
3743
|
-
if (tail) process.stdout.write(`
|
|
3744
|
-
===== ${f} =====
|
|
3745
|
-
${tail}
|
|
3746
|
-
`);
|
|
3747
|
-
}
|
|
3748
|
-
if (!follow) return;
|
|
3749
|
-
const offsets = new Map(files.map((f) => [f, fileSize(f)]));
|
|
3750
|
-
await new Promise((resolvePromise) => {
|
|
3751
|
-
const onSigint = () => {
|
|
3752
|
-
clearInterval(timer);
|
|
3753
|
-
process.off("SIGINT", onSigint);
|
|
3754
|
-
resolvePromise();
|
|
3755
|
-
};
|
|
3756
|
-
process.on("SIGINT", onSigint);
|
|
3757
|
-
const timer = setInterval(() => {
|
|
3758
|
-
for (const f of files) {
|
|
3759
|
-
const size = fileSize(f);
|
|
3760
|
-
const from = offsets.get(f) ?? 0;
|
|
3761
|
-
if (size > from) {
|
|
3762
|
-
offsets.set(f, size);
|
|
3763
|
-
createReadStream(f, { start: from, end: size - 1, encoding: "utf8" }).pipe(process.stdout, {
|
|
3764
|
-
end: false
|
|
3765
|
-
});
|
|
3766
|
-
} else if (size < from) {
|
|
3767
|
-
offsets.set(f, size);
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
}, 700);
|
|
3771
|
-
});
|
|
3772
|
-
}
|
|
3773
|
-
function fileSize(file) {
|
|
3774
|
-
try {
|
|
3775
|
-
return statSync(file).size;
|
|
3776
|
-
} catch {
|
|
3777
|
-
return 0;
|
|
3778
|
-
}
|
|
3779
|
-
}
|
|
3780
|
-
async function lastLines(file, n) {
|
|
3781
|
-
try {
|
|
3782
|
-
const text = await readFile7(file, "utf8");
|
|
3783
|
-
const lines = text.split("\n");
|
|
3784
|
-
return lines.slice(-n - 1).join("\n").trimEnd();
|
|
3785
|
-
} catch {
|
|
3786
|
-
return "";
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
|
|
3790
3680
|
// src/service/adapter.ts
|
|
3791
3681
|
function getServiceAdapter() {
|
|
3792
|
-
if (process.platform
|
|
3793
|
-
|
|
3794
|
-
install: installLaunchd,
|
|
3795
|
-
uninstall: uninstallLaunchd,
|
|
3796
|
-
status: async () => statusLaunchd(),
|
|
3797
|
-
restart: restartLaunchd,
|
|
3798
|
-
logs: tailLaunchdLogs
|
|
3799
|
-
};
|
|
3800
|
-
}
|
|
3801
|
-
if (process.platform === "win32") {
|
|
3802
|
-
return {
|
|
3803
|
-
install: installSchtask,
|
|
3804
|
-
uninstall: uninstallSchtask,
|
|
3805
|
-
status: async () => statusSchtask(),
|
|
3806
|
-
restart: restartSchtask,
|
|
3807
|
-
logs: tailSchtaskLogs
|
|
3808
|
-
};
|
|
3682
|
+
if (process.platform !== "darwin") {
|
|
3683
|
+
throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
|
|
3809
3684
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
if (process.platform === "win32") return schtaskRunning();
|
|
3818
|
-
} catch {
|
|
3819
|
-
}
|
|
3820
|
-
return false;
|
|
3685
|
+
return {
|
|
3686
|
+
install: installLaunchd,
|
|
3687
|
+
uninstall: uninstallLaunchd,
|
|
3688
|
+
status: async () => statusLaunchd(),
|
|
3689
|
+
restart: restartLaunchd,
|
|
3690
|
+
logs: tailLaunchdLogs
|
|
3691
|
+
};
|
|
3821
3692
|
}
|
|
3822
3693
|
|
|
3823
3694
|
// src/service/update.ts
|
|
3824
3695
|
var execFileP = promisify(execFile);
|
|
3825
3696
|
var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3826
3697
|
function pkgRoot() {
|
|
3827
|
-
return
|
|
3698
|
+
return resolve4(dirname7(fileURLToPath3(import.meta.url)), "..");
|
|
3828
3699
|
}
|
|
3829
3700
|
function pkgJson() {
|
|
3830
3701
|
try {
|
|
3831
|
-
return JSON.parse(readFileSync2(
|
|
3702
|
+
return JSON.parse(readFileSync2(join8(pkgRoot(), "package.json"), "utf8"));
|
|
3832
3703
|
} catch {
|
|
3833
3704
|
return {};
|
|
3834
3705
|
}
|
|
@@ -3840,7 +3711,7 @@ function packageName() {
|
|
|
3840
3711
|
return pkgJson().name ?? "@modelzen/feishu-codex-bridge";
|
|
3841
3712
|
}
|
|
3842
3713
|
function isDevSource() {
|
|
3843
|
-
return
|
|
3714
|
+
return existsSync5(join8(pkgRoot(), ".git"));
|
|
3844
3715
|
}
|
|
3845
3716
|
function isNewer(a, b) {
|
|
3846
3717
|
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -3863,7 +3734,7 @@ async function latestVersion() {
|
|
|
3863
3734
|
async function installLatest(opts = {}) {
|
|
3864
3735
|
const target = `${packageName()}@latest`;
|
|
3865
3736
|
return await new Promise((resolveP) => {
|
|
3866
|
-
const child =
|
|
3737
|
+
const child = spawn5(NPM, ["install", "-g", target], {
|
|
3867
3738
|
stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
|
|
3868
3739
|
});
|
|
3869
3740
|
let out = "";
|
|
@@ -3879,16 +3750,20 @@ async function installLatest(opts = {}) {
|
|
|
3879
3750
|
});
|
|
3880
3751
|
}
|
|
3881
3752
|
function daemonRunning() {
|
|
3882
|
-
|
|
3753
|
+
try {
|
|
3754
|
+
return statusLaunchd().loaded;
|
|
3755
|
+
} catch {
|
|
3756
|
+
return false;
|
|
3757
|
+
}
|
|
3883
3758
|
}
|
|
3884
3759
|
async function restartDaemon() {
|
|
3885
3760
|
await getServiceAdapter().restart();
|
|
3886
3761
|
}
|
|
3887
3762
|
|
|
3888
3763
|
// src/project/lifecycle.ts
|
|
3889
|
-
import { mkdir as
|
|
3890
|
-
import { existsSync as
|
|
3891
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3764
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
3765
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3766
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
|
|
3892
3767
|
|
|
3893
3768
|
// src/project/git-info.ts
|
|
3894
3769
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -4023,12 +3898,12 @@ async function onboardGroup(channel, project) {
|
|
|
4023
3898
|
// src/project/lifecycle.ts
|
|
4024
3899
|
async function resolveCwd(name, existingPath) {
|
|
4025
3900
|
if (existingPath) {
|
|
4026
|
-
const cwd2 = isAbsolute2(existingPath) ? existingPath :
|
|
4027
|
-
if (!
|
|
3901
|
+
const cwd2 = isAbsolute2(existingPath) ? existingPath : resolve5(existingPath);
|
|
3902
|
+
if (!existsSync6(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
|
|
4028
3903
|
return { cwd: cwd2, blank: false };
|
|
4029
3904
|
}
|
|
4030
|
-
const cwd =
|
|
4031
|
-
await
|
|
3905
|
+
const cwd = join9(paths.projectsRootDir, name);
|
|
3906
|
+
await mkdir6(cwd, { recursive: true });
|
|
4032
3907
|
return { cwd, blank: true };
|
|
4033
3908
|
}
|
|
4034
3909
|
async function createProject(channel, input2) {
|
|
@@ -4095,12 +3970,12 @@ async function leaveChat(channel, chatId) {
|
|
|
4095
3970
|
}
|
|
4096
3971
|
|
|
4097
3972
|
// src/bot/session-store.ts
|
|
4098
|
-
import { mkdir as
|
|
4099
|
-
import { dirname as
|
|
3973
|
+
import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "fs/promises";
|
|
3974
|
+
import { dirname as dirname8 } from "path";
|
|
4100
3975
|
var FILE_VERSION3 = 1;
|
|
4101
3976
|
async function read2() {
|
|
4102
3977
|
try {
|
|
4103
|
-
const text = await
|
|
3978
|
+
const text = await readFile7(paths.sessionsFile, "utf8");
|
|
4104
3979
|
const parsed = JSON.parse(text);
|
|
4105
3980
|
return Array.isArray(parsed.sessions) ? parsed.sessions : [];
|
|
4106
3981
|
} catch (err) {
|
|
@@ -4109,10 +3984,10 @@ async function read2() {
|
|
|
4109
3984
|
}
|
|
4110
3985
|
}
|
|
4111
3986
|
async function write2(sessions) {
|
|
4112
|
-
await
|
|
3987
|
+
await mkdir7(dirname8(paths.sessionsFile), { recursive: true });
|
|
4113
3988
|
const tmp = `${paths.sessionsFile}.tmp-${process.pid}`;
|
|
4114
3989
|
const body = { version: FILE_VERSION3, sessions };
|
|
4115
|
-
await
|
|
3990
|
+
await writeFile6(tmp, `${JSON.stringify(body, null, 2)}
|
|
4116
3991
|
`, "utf8");
|
|
4117
3992
|
await rename5(tmp, paths.sessionsFile);
|
|
4118
3993
|
}
|
|
@@ -4156,8 +4031,8 @@ async function handleDmConsole(channel, cfg, msg) {
|
|
|
4156
4031
|
}
|
|
4157
4032
|
|
|
4158
4033
|
// src/bot/media.ts
|
|
4159
|
-
import { mkdir as
|
|
4160
|
-
import { join as
|
|
4034
|
+
import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat3 } from "fs/promises";
|
|
4035
|
+
import { join as join10 } from "path";
|
|
4161
4036
|
var MAX_IMAGES2 = 9;
|
|
4162
4037
|
var MEDIA_TTL_MS = 60 * 6e4;
|
|
4163
4038
|
var EXT_BY_CONTENT_TYPE = {
|
|
@@ -4186,7 +4061,7 @@ async function collectInboundImages(channel, msg) {
|
|
|
4186
4061
|
if (refs.length === 0) return [];
|
|
4187
4062
|
await pruneOldMedia();
|
|
4188
4063
|
try {
|
|
4189
|
-
await
|
|
4064
|
+
await mkdir8(paths.mediaDir, { recursive: true });
|
|
4190
4065
|
} catch {
|
|
4191
4066
|
}
|
|
4192
4067
|
const out = [];
|
|
@@ -4262,7 +4137,7 @@ async function downloadOne(channel, ref, index) {
|
|
|
4262
4137
|
params: { type: "image" }
|
|
4263
4138
|
});
|
|
4264
4139
|
const ext = extFromHeaders(res.headers);
|
|
4265
|
-
const file =
|
|
4140
|
+
const file = join10(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
|
|
4266
4141
|
await res.writeFile(file);
|
|
4267
4142
|
return file;
|
|
4268
4143
|
} catch (err) {
|
|
@@ -4296,10 +4171,10 @@ async function pruneOldMedia() {
|
|
|
4296
4171
|
}
|
|
4297
4172
|
const cutoff = Date.now() - MEDIA_TTL_MS;
|
|
4298
4173
|
for (const name of entries) {
|
|
4299
|
-
const file =
|
|
4174
|
+
const file = join10(paths.mediaDir, name);
|
|
4300
4175
|
try {
|
|
4301
4176
|
const st = await stat3(file);
|
|
4302
|
-
if (st.mtimeMs < cutoff) await
|
|
4177
|
+
if (st.mtimeMs < cutoff) await rm3(file, { force: true });
|
|
4303
4178
|
} catch {
|
|
4304
4179
|
}
|
|
4305
4180
|
}
|
|
@@ -5348,11 +5223,29 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
|
5348
5223
|
},
|
|
5349
5224
|
stopSignal
|
|
5350
5225
|
);
|
|
5226
|
+
const tStart = Date.now();
|
|
5227
|
+
let firstEvAt = 0;
|
|
5228
|
+
let firstTextAt = 0;
|
|
5229
|
+
let lastEvAt = tStart;
|
|
5230
|
+
let evCount = 0;
|
|
5231
|
+
let textChars = 0;
|
|
5351
5232
|
for await (const ev of guarded) {
|
|
5233
|
+
const tEv = Date.now();
|
|
5234
|
+
if (!firstEvAt) firstEvAt = tEv;
|
|
5235
|
+
const et = ev.type;
|
|
5236
|
+
if (et === "text_delta") {
|
|
5237
|
+
if (!firstTextAt) firstTextAt = tEv;
|
|
5238
|
+
const d = ev.delta;
|
|
5239
|
+
if (typeof d === "string") textChars += d.length;
|
|
5240
|
+
}
|
|
5241
|
+
lastEvAt = tEv;
|
|
5242
|
+
evCount++;
|
|
5352
5243
|
render.apply(ev);
|
|
5353
5244
|
rc.rs = render.snapshot();
|
|
5354
|
-
|
|
5245
|
+
stream2.streamCoalesced(channel, buildRunCard(rc), ANSWER_EID);
|
|
5355
5246
|
}
|
|
5247
|
+
const doneAt = Date.now();
|
|
5248
|
+
await stream2.drain();
|
|
5356
5249
|
state.interrupt = void 0;
|
|
5357
5250
|
const killed = interrupted || timedOut;
|
|
5358
5251
|
if (timedOut) render.timeout(Math.max(1, Math.round(idleMs / 6e4)));
|
|
@@ -5373,6 +5266,25 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
|
5373
5266
|
rc.images = await uploadOutboundImages(channel, imgSources, opts.cwd ?? fallbackCwd);
|
|
5374
5267
|
}
|
|
5375
5268
|
await stream2.updateCard(channel, buildRunCard(rc));
|
|
5269
|
+
{
|
|
5270
|
+
const terminalAt = Date.now();
|
|
5271
|
+
const st = stream2.stats();
|
|
5272
|
+
log.info("stream", "timing", {
|
|
5273
|
+
firstEv: firstEvAt ? firstEvAt - tStart : -1,
|
|
5274
|
+
firstText: firstTextAt ? firstTextAt - tStart : -1,
|
|
5275
|
+
lastEv: lastEvAt - tStart,
|
|
5276
|
+
done: doneAt - tStart,
|
|
5277
|
+
terminal: terminalAt - tStart,
|
|
5278
|
+
doneToTerminal: terminalAt - doneAt,
|
|
5279
|
+
events: evCount,
|
|
5280
|
+
textChars,
|
|
5281
|
+
pushes: st.pushCount,
|
|
5282
|
+
cardPushes: st.cardPushes,
|
|
5283
|
+
elPushes: st.elPushes,
|
|
5284
|
+
rttAvg: st.pushCount ? Math.round(st.totalRttMs / st.pushCount) : 0,
|
|
5285
|
+
rttMax: st.maxRttMs
|
|
5286
|
+
});
|
|
5287
|
+
}
|
|
5376
5288
|
runsByCard.delete(cardMsgId);
|
|
5377
5289
|
promoteCard(finalMsgId, rc);
|
|
5378
5290
|
for (const fence of fences) {
|
|
@@ -5634,7 +5546,7 @@ async function startBridge(opts) {
|
|
|
5634
5546
|
|
|
5635
5547
|
// src/core/single-instance.ts
|
|
5636
5548
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
|
|
5637
|
-
import { dirname as
|
|
5549
|
+
import { dirname as dirname9 } from "path";
|
|
5638
5550
|
var BridgeAlreadyRunningError = class extends Error {
|
|
5639
5551
|
constructor(pid) {
|
|
5640
5552
|
super(
|
|
@@ -5663,7 +5575,7 @@ function acquireSingleInstanceLock(appId) {
|
|
|
5663
5575
|
} catch (err) {
|
|
5664
5576
|
if (err instanceof BridgeAlreadyRunningError) throw err;
|
|
5665
5577
|
}
|
|
5666
|
-
mkdirSync2(
|
|
5578
|
+
mkdirSync2(dirname9(file), { recursive: true });
|
|
5667
5579
|
const record = { pid: process.pid, appId, startedAt: Date.now() };
|
|
5668
5580
|
writeFileSync(file, `${JSON.stringify(record)}
|
|
5669
5581
|
`, "utf8");
|
|
@@ -5732,14 +5644,12 @@ async function runStart() {
|
|
|
5732
5644
|
return;
|
|
5733
5645
|
}
|
|
5734
5646
|
const status = await getServiceAdapter().install();
|
|
5735
|
-
console.log(
|
|
5736
|
-
process.platform === "win32" ? "\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF08\u767B\u5F55\u81EA\u542F\uFF1B\u6CE8\u610F\uFF1AWindows \u8BA1\u5212\u4EFB\u52A1\u65E0\u5D29\u6E83\u81EA\u52A8\u62C9\u8D77\uFF09\u3002" : "\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"
|
|
5737
|
-
);
|
|
5647
|
+
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");
|
|
5738
5648
|
printStatus(status);
|
|
5739
5649
|
}
|
|
5740
5650
|
async function runStop() {
|
|
5741
5651
|
await getServiceAdapter().uninstall();
|
|
5742
|
-
console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u505C\u6B62\uFF0C\u5E76\u5DF2\u5173\u95ED\u81EA\u542F\uFF08\u5DF2\u79FB\u9664\
|
|
5652
|
+
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");
|
|
5743
5653
|
}
|
|
5744
5654
|
async function runRestart() {
|
|
5745
5655
|
const status = await getServiceAdapter().restart();
|
|
@@ -5753,18 +5663,17 @@ async function runLogs(follow) {
|
|
|
5753
5663
|
await getServiceAdapter().logs(follow);
|
|
5754
5664
|
}
|
|
5755
5665
|
function printStatus(status) {
|
|
5756
|
-
console.log(`
|
|
5757
|
-
console.log(`path: ${status.servicePath}`);
|
|
5666
|
+
console.log(`plist: ${status.plistPath}`);
|
|
5758
5667
|
console.log(`installed: ${status.installed ? "yes" : "no"}`);
|
|
5759
|
-
console.log(`
|
|
5668
|
+
console.log(`loaded: ${status.loaded ? "yes" : "no"}`);
|
|
5760
5669
|
console.log(`pid: ${status.pid ?? "-"}`);
|
|
5761
5670
|
console.log(`last exit: ${status.lastExit ?? "-"}`);
|
|
5762
5671
|
console.log(`stdout: ${status.stdoutPath}`);
|
|
5763
5672
|
console.log(`stderr: ${status.stderrPath}`);
|
|
5764
5673
|
if (!status.installed) {
|
|
5765
5674
|
console.log("\u63D0\u793A\uFF1A\u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF0C\u8FD0\u884C `feishu-codex-bridge start`\u3002");
|
|
5766
|
-
} else if (!status.
|
|
5767
|
-
console.log("\u63D0\u793A\
|
|
5675
|
+
} else if (!status.loaded) {
|
|
5676
|
+
console.log("\u63D0\u793A\uFF1Aplist \u5DF2\u5B58\u5728\uFF0C\u4F46 launchd \u5F53\u524D\u672A\u52A0\u8F7D\uFF08\u8BD5\u8BD5 `restart`\uFF09\u3002");
|
|
5768
5677
|
}
|
|
5769
5678
|
}
|
|
5770
5679
|
|
|
@@ -5817,7 +5726,7 @@ async function runUpdate(opts = {}) {
|
|
|
5817
5726
|
}
|
|
5818
5727
|
|
|
5819
5728
|
// src/cli/commands/bot.ts
|
|
5820
|
-
import { rm as
|
|
5729
|
+
import { rm as rm4 } from "fs/promises";
|
|
5821
5730
|
async function runBotInit(name) {
|
|
5822
5731
|
if (!ensureCodex()) {
|
|
5823
5732
|
process.exitCode = 1;
|
|
@@ -5872,7 +5781,7 @@ async function runBotRm(name) {
|
|
|
5872
5781
|
}
|
|
5873
5782
|
const after = await removeBot(bot2.appId);
|
|
5874
5783
|
await removeSecret(secretKeyForApp(bot2.appId));
|
|
5875
|
-
await
|
|
5784
|
+
await rm4(botDir(bot2.appId), { recursive: true, force: true });
|
|
5876
5785
|
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`);
|
|
5877
5786
|
if (after.bots.length === 0) {
|
|
5878
5787
|
console.log(" \u5DF2\u65E0\u4EFB\u4F55\u673A\u5668\u4EBA\uFF0C`bot init` \u91CD\u65B0\u521B\u5EFA\u3002");
|
|
@@ -5930,15 +5839,15 @@ async function secretsRemove(id) {
|
|
|
5930
5839
|
console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
|
|
5931
5840
|
}
|
|
5932
5841
|
function readStdin() {
|
|
5933
|
-
return new Promise((
|
|
5842
|
+
return new Promise((resolve6) => {
|
|
5934
5843
|
let data = "";
|
|
5935
5844
|
if (process.stdin.isTTY) {
|
|
5936
|
-
|
|
5845
|
+
resolve6("");
|
|
5937
5846
|
return;
|
|
5938
5847
|
}
|
|
5939
5848
|
process.stdin.setEncoding("utf8");
|
|
5940
5849
|
process.stdin.on("data", (c) => data += c);
|
|
5941
|
-
process.stdin.on("end", () =>
|
|
5850
|
+
process.stdin.on("end", () => resolve6(data));
|
|
5942
5851
|
});
|
|
5943
5852
|
}
|
|
5944
5853
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelzen/feishu-codex-bridge",
|
|
3
|
-
"version": "0.2.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,11 +30,9 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@larksuiteoapi/node-sdk": "^1.65.0",
|
|
32
32
|
"commander": "^12.1.0",
|
|
33
|
-
"cross-spawn": "^7.0.6",
|
|
34
33
|
"qrcode-terminal": "^0.12.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
|
-
"@types/cross-spawn": "^6.0.6",
|
|
38
36
|
"@types/node": "^22.10.0",
|
|
39
37
|
"@types/qrcode-terminal": "^0.12.2",
|
|
40
38
|
"tsup": "^8.3.5",
|