@modelzen/feishu-codex-bridge 0.2.0 → 0.2.1-win

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +342 -95
  2. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -15,7 +15,6 @@ function bridgeVersion() {
15
15
  }
16
16
 
17
17
  // src/cli/commands/doctor.ts
18
- import { execFileSync as execFileSync2 } from "child_process";
19
18
  import { existsSync as existsSync3 } from "fs";
20
19
  import { homedir as homedir2 } from "os";
21
20
  import { join as join4 } from "path";
@@ -269,27 +268,56 @@ async function moveIfExists(src, dest) {
269
268
  }
270
269
 
271
270
  // src/agent/codex-appserver/locate.ts
272
- import { execFileSync } from "child_process";
273
271
  import { existsSync as existsSync2 } from "fs";
274
- import { join as join3 } from "path";
272
+ import { extname, join as join3 } from "path";
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";
275
295
  function resolveCodexBin() {
276
296
  const env = process.env.CODEX_BIN;
277
297
  if (env && existsSync2(env)) return env;
278
298
  const onPath = which("codex");
279
299
  if (onPath) return onPath;
280
- const priv = join3(paths.codexCliBinDir, "codex");
281
- if (existsSync2(priv)) return priv;
300
+ for (const cand of execCandidates(paths.codexCliBinDir, "codex")) {
301
+ if (existsSync2(cand)) return cand;
302
+ }
282
303
  const appBundle = "/Applications/Codex.app/Contents/Resources/codex";
283
304
  if (process.platform === "darwin" && existsSync2(appBundle)) return appBundle;
284
305
  return null;
285
306
  }
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
+ }
286
313
  function which(cmd) {
287
314
  try {
288
- const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
315
+ const res = spawnProcessSync(IS_WIN ? "where" : "which", [cmd], {
289
316
  encoding: "utf8",
290
317
  stdio: ["ignore", "pipe", "ignore"]
291
318
  });
292
- const first = out.split("\n").map((l) => l.trim()).find(Boolean);
319
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
320
+ const first = res.stdout.split("\n").map((l) => l.trim()).find(Boolean);
293
321
  return first && existsSync2(first) ? first : null;
294
322
  } catch {
295
323
  return null;
@@ -297,7 +325,9 @@ function which(cmd) {
297
325
  }
298
326
  function codexVersion(bin) {
299
327
  try {
300
- return execFileSync(bin, ["--version"], { encoding: "utf8" }).trim();
328
+ const res = spawnProcessSync(bin, ["--version"], { encoding: "utf8" });
329
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
330
+ return res.stdout.trim();
301
331
  } catch {
302
332
  return null;
303
333
  }
@@ -355,7 +385,9 @@ ${failed === 0 ? "\u5168\u90E8\u901A\u8FC7 \u2713" : `${failed} \u9879\u9700\u59
355
385
  }
356
386
  function tryExec(cmd, args) {
357
387
  try {
358
- return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
388
+ const res = spawnProcessSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
389
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
390
+ return res.stdout.trim();
359
391
  } catch {
360
392
  return null;
361
393
  }
@@ -538,7 +570,7 @@ async function spawnExecProvider(pc, ref) {
538
570
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
539
571
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
540
572
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
541
- return new Promise((resolve6, reject) => {
573
+ return new Promise((resolve7, reject) => {
542
574
  const env = {};
543
575
  if (pc.passEnv) for (const k of pc.passEnv) {
544
576
  const v = process.env[k];
@@ -583,7 +615,7 @@ async function spawnExecProvider(pc, ref) {
583
615
  try {
584
616
  const parsed = JSON.parse(stdout);
585
617
  const value = parsed.values?.[ref.id];
586
- if (typeof value === "string") return resolve6(value);
618
+ if (typeof value === "string") return resolve7(value);
587
619
  const err = parsed.errors?.[ref.id]?.message;
588
620
  reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
589
621
  } catch (err) {
@@ -1131,7 +1163,6 @@ ${rule}`);
1131
1163
  import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
1132
1164
 
1133
1165
  // src/agent/codex-appserver/app-server-client.ts
1134
- import { spawn as spawn3 } from "child_process";
1135
1166
  var AsyncQueue = class {
1136
1167
  items = [];
1137
1168
  waiters = [];
@@ -1152,7 +1183,7 @@ var AsyncQueue = class {
1152
1183
  continue;
1153
1184
  }
1154
1185
  if (this.closed) return;
1155
- const next = await new Promise((resolve6) => this.waiters.push(resolve6));
1186
+ const next = await new Promise((resolve7) => this.waiters.push(resolve7));
1156
1187
  if (next.done) return;
1157
1188
  yield next.value;
1158
1189
  }
@@ -1174,9 +1205,9 @@ var AppServerClient = class {
1174
1205
  }
1175
1206
  /** spawn + initialize handshake. Throws if spawn/handshake fails. */
1176
1207
  async connect() {
1177
- const child = spawn3(this.opts.bin, ["app-server", "--listen", "stdio://"], {
1208
+ const child = spawnProcess(this.opts.bin, ["app-server", "--listen", "stdio://"], {
1178
1209
  cwd: this.opts.cwd,
1179
- env: { ...process.env, ...this.opts.env, FEISHU_CODEX_BRIDGE: "1" },
1210
+ env: mergeProcessEnv(process.env, { ...this.opts.env, FEISHU_CODEX_BRIDGE: "1" }),
1180
1211
  stdio: ["pipe", "pipe", "pipe"]
1181
1212
  });
1182
1213
  this.child = child;
@@ -1203,8 +1234,8 @@ var AppServerClient = class {
1203
1234
  const id = ++this.nextId;
1204
1235
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1205
1236
  `;
1206
- return new Promise((resolve6, reject) => {
1207
- this.pending.set(id, { resolve: resolve6, reject });
1237
+ return new Promise((resolve7, reject) => {
1238
+ this.pending.set(id, { resolve: resolve7, reject });
1208
1239
  this.child.stdin.write(payload, (err) => {
1209
1240
  if (err) {
1210
1241
  this.pending.delete(id);
@@ -1227,15 +1258,36 @@ var AppServerClient = class {
1227
1258
  this.closed = true;
1228
1259
  const child = this.child;
1229
1260
  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
+ }
1230
1282
  child.kill("SIGTERM");
1231
- await new Promise((resolve6) => {
1283
+ await new Promise((resolve7) => {
1232
1284
  const t = setTimeout(() => {
1233
1285
  if (child.exitCode === null) child.kill("SIGKILL");
1234
- resolve6();
1286
+ resolve7();
1235
1287
  }, graceMs);
1236
1288
  child.once("exit", () => {
1237
1289
  clearTimeout(t);
1238
- resolve6();
1290
+ resolve7();
1239
1291
  });
1240
1292
  });
1241
1293
  }
@@ -1368,12 +1420,12 @@ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1368
1420
  ].join("\n");
1369
1421
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1370
1422
  function withDeadline(p, ms, label) {
1371
- return new Promise((resolve6, reject) => {
1423
+ return new Promise((resolve7, reject) => {
1372
1424
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1373
1425
  p.then(
1374
1426
  (v) => {
1375
1427
  clearTimeout(t);
1376
- resolve6(v);
1428
+ resolve7(v);
1377
1429
  },
1378
1430
  (e) => {
1379
1431
  clearTimeout(t);
@@ -1413,11 +1465,11 @@ var CodexThread = class {
1413
1465
  if (self.model) params.model = self.model;
1414
1466
  if (self.effort) params.effort = self.effort;
1415
1467
  let startError;
1416
- const startFailed = new Promise((resolve6) => {
1468
+ const startFailed = new Promise((resolve7) => {
1417
1469
  self.client.request("turn/start", params).then(void 0, (err) => {
1418
1470
  startError = err instanceof Error ? err : new Error(String(err));
1419
1471
  log.fail("agent", startError, { phase: "turn/start" });
1420
- resolve6("start-failed");
1472
+ resolve7("start-failed");
1421
1473
  });
1422
1474
  });
1423
1475
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -2788,7 +2840,7 @@ var RunCardStream = class {
2788
2840
 
2789
2841
  // src/card/outbound-images.ts
2790
2842
  import { readFile as readFile5, stat as stat2 } from "fs/promises";
2791
- import { extname, isAbsolute, resolve as resolve2, sep } from "path";
2843
+ import { extname as extname2, isAbsolute, resolve as resolve2, sep } from "path";
2792
2844
  var MAX_IMAGES = 9;
2793
2845
  var MAX_BYTES = 10 * 1024 * 1024;
2794
2846
  var DOWNLOAD_TIMEOUT_MS = 1e4;
@@ -2854,7 +2906,7 @@ async function loadLocal(src, cwd) {
2854
2906
  log.warn("outbound", "image-outside-cwd", { src: src.slice(0, 80) });
2855
2907
  return { cacheKey: `local:${abs}` };
2856
2908
  }
2857
- const ext = extname(abs).slice(1).toLowerCase();
2909
+ const ext = extname2(abs).slice(1).toLowerCase();
2858
2910
  if (!ALLOWED_EXT.has(ext)) {
2859
2911
  log.warn("outbound", "image-ext", { ext, src: src.slice(0, 80) });
2860
2912
  return { cacheKey: `local:${abs}` };
@@ -3385,29 +3437,40 @@ function buildGroupSettingsCard(project) {
3385
3437
  }
3386
3438
 
3387
3439
  // src/service/update.ts
3388
- import { execFile, spawn as spawn5 } from "child_process";
3389
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3390
- import { dirname as dirname7, join as join8, resolve as resolve4 } from "path";
3391
- import { fileURLToPath as fileURLToPath3 } from "url";
3440
+ import { execFile, spawn as spawn4 } from "child_process";
3441
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
3442
+ import { dirname as dirname8, join as join10, resolve as resolve5 } from "path";
3443
+ import { fileURLToPath as fileURLToPath4 } from "url";
3392
3444
  import { promisify } from "util";
3393
3445
 
3394
3446
  // src/service/launchd.ts
3395
- import { spawn as spawn4, spawnSync } from "child_process";
3447
+ import { spawn as spawn3, spawnSync } from "child_process";
3396
3448
  import { existsSync as existsSync4 } from "fs";
3397
- import { appendFile, mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
3449
+ import { mkdir as mkdir6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
3398
3450
  import { homedir as homedir3, userInfo as userInfo2 } from "os";
3399
- import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
3451
+ import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
3400
3452
  import { fileURLToPath as fileURLToPath2 } from "url";
3401
- var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
3402
- function launchAgentPlistPath() {
3403
- return join7(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
3404
- }
3453
+
3454
+ // src/service/common.ts
3455
+ import { appendFile, mkdir as mkdir5 } from "fs/promises";
3456
+ import { join as join7 } from "path";
3405
3457
  function serviceStdoutPath() {
3406
3458
  return join7(paths.appDir, "service.log");
3407
3459
  }
3408
3460
  function serviceStderrPath() {
3409
3461
  return join7(paths.appDir, "service.err.log");
3410
3462
  }
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
+ }
3411
3474
  function resolveCliBinPath() {
3412
3475
  const distDir = dirname6(fileURLToPath2(import.meta.url));
3413
3476
  return resolve3(distDir, "..", "bin", "feishu-codex-bridge.mjs");
@@ -3450,7 +3513,7 @@ function buildPlist() {
3450
3513
  }
3451
3514
  async function installLaunchd() {
3452
3515
  const plistPath = launchAgentPlistPath();
3453
- await mkdir5(dirname6(plistPath), { recursive: true });
3516
+ await mkdir6(dirname6(plistPath), { recursive: true });
3454
3517
  await ensureLogFiles();
3455
3518
  await writeFile5(plistPath, buildPlist(), "utf8");
3456
3519
  if (isLoaded()) {
@@ -3488,9 +3551,10 @@ function statusLaunchd() {
3488
3551
  const raw = result.stdout || result.stderr;
3489
3552
  const parsed = parseLaunchdStatus(raw);
3490
3553
  return {
3554
+ platformName: "launchd (macOS)",
3491
3555
  installed: existsSync4(launchAgentPlistPath()),
3492
- loaded: result.ok,
3493
- plistPath: launchAgentPlistPath(),
3556
+ running: result.ok,
3557
+ servicePath: launchAgentPlistPath(),
3494
3558
  stdoutPath: serviceStdoutPath(),
3495
3559
  stderrPath: serviceStderrPath(),
3496
3560
  pid: parsed.pid,
@@ -3502,7 +3566,7 @@ async function tailLaunchdLogs(follow) {
3502
3566
  await ensureLogFiles();
3503
3567
  const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
3504
3568
  await new Promise((resolvePromise, reject) => {
3505
- const child = spawn4("tail", args, { stdio: "inherit" });
3569
+ const child = spawn3("tail", args, { stdio: "inherit" });
3506
3570
  child.on("error", reject);
3507
3571
  child.on("close", (code) => {
3508
3572
  if (code === 0 || follow && code === null) {
@@ -3533,11 +3597,6 @@ async function waitUntilUnloaded(timeoutMs = 5e3) {
3533
3597
  }
3534
3598
  throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
3535
3599
  }
3536
- async function ensureLogFiles() {
3537
- await mkdir5(paths.appDir, { recursive: true });
3538
- await appendFile(serviceStdoutPath(), "");
3539
- await appendFile(serviceStderrPath(), "");
3540
- }
3541
3600
  function userTarget() {
3542
3601
  return `gui/${userInfo2().uid}`;
3543
3602
  }
@@ -3558,29 +3617,218 @@ function launchctlError(command, result) {
3558
3617
  return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
3559
3618
  }
3560
3619
 
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
+
3561
3790
  // src/service/adapter.ts
3562
3791
  function getServiceAdapter() {
3563
- if (process.platform !== "darwin") {
3564
- throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
3792
+ if (process.platform === "darwin") {
3793
+ return {
3794
+ install: installLaunchd,
3795
+ uninstall: uninstallLaunchd,
3796
+ status: async () => statusLaunchd(),
3797
+ restart: restartLaunchd,
3798
+ logs: tailLaunchdLogs
3799
+ };
3565
3800
  }
3566
- return {
3567
- install: installLaunchd,
3568
- uninstall: uninstallLaunchd,
3569
- status: async () => statusLaunchd(),
3570
- restart: restartLaunchd,
3571
- logs: tailLaunchdLogs
3572
- };
3801
+ if (process.platform === "win32") {
3802
+ return {
3803
+ install: installSchtask,
3804
+ uninstall: uninstallSchtask,
3805
+ status: async () => statusSchtask(),
3806
+ restart: restartSchtask,
3807
+ logs: tailSchtaskLogs
3808
+ };
3809
+ }
3810
+ throw new Error(
3811
+ "service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u540E\u53F0\u670D\u52A1\uFF08\u4EC5 macOS launchd / Windows \u8BA1\u5212\u4EFB\u52A1\uFF09\u3002\u8BF7\u7528 `feishu-codex-bridge run` \u524D\u53F0\u8FD0\u884C" + (process.platform === "linux" ? "\uFF1BLinux systemd \u652F\u6301\u540E\u7EED\u63D0\u4F9B\u3002" : "\u3002")
3812
+ );
3813
+ }
3814
+ function isServiceRunning() {
3815
+ try {
3816
+ if (process.platform === "darwin") return isLoaded();
3817
+ if (process.platform === "win32") return schtaskRunning();
3818
+ } catch {
3819
+ }
3820
+ return false;
3573
3821
  }
3574
3822
 
3575
3823
  // src/service/update.ts
3576
3824
  var execFileP = promisify(execFile);
3577
3825
  var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
3578
3826
  function pkgRoot() {
3579
- return resolve4(dirname7(fileURLToPath3(import.meta.url)), "..");
3827
+ return resolve5(dirname8(fileURLToPath4(import.meta.url)), "..");
3580
3828
  }
3581
3829
  function pkgJson() {
3582
3830
  try {
3583
- return JSON.parse(readFileSync2(join8(pkgRoot(), "package.json"), "utf8"));
3831
+ return JSON.parse(readFileSync2(join10(pkgRoot(), "package.json"), "utf8"));
3584
3832
  } catch {
3585
3833
  return {};
3586
3834
  }
@@ -3592,7 +3840,7 @@ function packageName() {
3592
3840
  return pkgJson().name ?? "@modelzen/feishu-codex-bridge";
3593
3841
  }
3594
3842
  function isDevSource() {
3595
- return existsSync5(join8(pkgRoot(), ".git"));
3843
+ return existsSync6(join10(pkgRoot(), ".git"));
3596
3844
  }
3597
3845
  function isNewer(a, b) {
3598
3846
  const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -3615,7 +3863,7 @@ async function latestVersion() {
3615
3863
  async function installLatest(opts = {}) {
3616
3864
  const target = `${packageName()}@latest`;
3617
3865
  return await new Promise((resolveP) => {
3618
- const child = spawn5(NPM, ["install", "-g", target], {
3866
+ const child = spawn4(NPM, ["install", "-g", target], {
3619
3867
  stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
3620
3868
  });
3621
3869
  let out = "";
@@ -3631,20 +3879,16 @@ async function installLatest(opts = {}) {
3631
3879
  });
3632
3880
  }
3633
3881
  function daemonRunning() {
3634
- try {
3635
- return statusLaunchd().loaded;
3636
- } catch {
3637
- return false;
3638
- }
3882
+ return isServiceRunning();
3639
3883
  }
3640
3884
  async function restartDaemon() {
3641
3885
  await getServiceAdapter().restart();
3642
3886
  }
3643
3887
 
3644
3888
  // src/project/lifecycle.ts
3645
- import { mkdir as mkdir6 } from "fs/promises";
3646
- import { existsSync as existsSync6 } from "fs";
3647
- import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
3889
+ import { mkdir as mkdir8 } from "fs/promises";
3890
+ import { existsSync as existsSync7 } from "fs";
3891
+ import { isAbsolute as isAbsolute2, join as join11, resolve as resolve6 } from "path";
3648
3892
 
3649
3893
  // src/project/git-info.ts
3650
3894
  import { execFile as execFile2 } from "child_process";
@@ -3779,12 +4023,12 @@ async function onboardGroup(channel, project) {
3779
4023
  // src/project/lifecycle.ts
3780
4024
  async function resolveCwd(name, existingPath) {
3781
4025
  if (existingPath) {
3782
- const cwd2 = isAbsolute2(existingPath) ? existingPath : resolve5(existingPath);
3783
- if (!existsSync6(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
4026
+ const cwd2 = isAbsolute2(existingPath) ? existingPath : resolve6(existingPath);
4027
+ if (!existsSync7(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
3784
4028
  return { cwd: cwd2, blank: false };
3785
4029
  }
3786
- const cwd = join9(paths.projectsRootDir, name);
3787
- await mkdir6(cwd, { recursive: true });
4030
+ const cwd = join11(paths.projectsRootDir, name);
4031
+ await mkdir8(cwd, { recursive: true });
3788
4032
  return { cwd, blank: true };
3789
4033
  }
3790
4034
  async function createProject(channel, input2) {
@@ -3851,12 +4095,12 @@ async function leaveChat(channel, chatId) {
3851
4095
  }
3852
4096
 
3853
4097
  // src/bot/session-store.ts
3854
- import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3855
- import { dirname as dirname8 } from "path";
4098
+ import { mkdir as mkdir9, readFile as readFile8, rename as rename5, writeFile as writeFile7 } from "fs/promises";
4099
+ import { dirname as dirname9 } from "path";
3856
4100
  var FILE_VERSION3 = 1;
3857
4101
  async function read2() {
3858
4102
  try {
3859
- const text = await readFile7(paths.sessionsFile, "utf8");
4103
+ const text = await readFile8(paths.sessionsFile, "utf8");
3860
4104
  const parsed = JSON.parse(text);
3861
4105
  return Array.isArray(parsed.sessions) ? parsed.sessions : [];
3862
4106
  } catch (err) {
@@ -3865,10 +4109,10 @@ async function read2() {
3865
4109
  }
3866
4110
  }
3867
4111
  async function write2(sessions) {
3868
- await mkdir7(dirname8(paths.sessionsFile), { recursive: true });
4112
+ await mkdir9(dirname9(paths.sessionsFile), { recursive: true });
3869
4113
  const tmp = `${paths.sessionsFile}.tmp-${process.pid}`;
3870
4114
  const body = { version: FILE_VERSION3, sessions };
3871
- await writeFile6(tmp, `${JSON.stringify(body, null, 2)}
4115
+ await writeFile7(tmp, `${JSON.stringify(body, null, 2)}
3872
4116
  `, "utf8");
3873
4117
  await rename5(tmp, paths.sessionsFile);
3874
4118
  }
@@ -3912,8 +4156,8 @@ async function handleDmConsole(channel, cfg, msg) {
3912
4156
  }
3913
4157
 
3914
4158
  // src/bot/media.ts
3915
- import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat3 } from "fs/promises";
3916
- import { join as join10 } from "path";
4159
+ import { mkdir as mkdir10, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
4160
+ import { join as join12 } from "path";
3917
4161
  var MAX_IMAGES2 = 9;
3918
4162
  var MEDIA_TTL_MS = 60 * 6e4;
3919
4163
  var EXT_BY_CONTENT_TYPE = {
@@ -3942,7 +4186,7 @@ async function collectInboundImages(channel, msg) {
3942
4186
  if (refs.length === 0) return [];
3943
4187
  await pruneOldMedia();
3944
4188
  try {
3945
- await mkdir8(paths.mediaDir, { recursive: true });
4189
+ await mkdir10(paths.mediaDir, { recursive: true });
3946
4190
  } catch {
3947
4191
  }
3948
4192
  const out = [];
@@ -4018,7 +4262,7 @@ async function downloadOne(channel, ref, index) {
4018
4262
  params: { type: "image" }
4019
4263
  });
4020
4264
  const ext = extFromHeaders(res.headers);
4021
- const file = join10(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
4265
+ const file = join12(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
4022
4266
  await res.writeFile(file);
4023
4267
  return file;
4024
4268
  } catch (err) {
@@ -4052,10 +4296,10 @@ async function pruneOldMedia() {
4052
4296
  }
4053
4297
  const cutoff = Date.now() - MEDIA_TTL_MS;
4054
4298
  for (const name of entries) {
4055
- const file = join10(paths.mediaDir, name);
4299
+ const file = join12(paths.mediaDir, name);
4056
4300
  try {
4057
4301
  const st = await stat3(file);
4058
- if (st.mtimeMs < cutoff) await rm3(file, { force: true });
4302
+ if (st.mtimeMs < cutoff) await rm4(file, { force: true });
4059
4303
  } catch {
4060
4304
  }
4061
4305
  }
@@ -5390,7 +5634,7 @@ async function startBridge(opts) {
5390
5634
 
5391
5635
  // src/core/single-instance.ts
5392
5636
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
5393
- import { dirname as dirname9 } from "path";
5637
+ import { dirname as dirname10 } from "path";
5394
5638
  var BridgeAlreadyRunningError = class extends Error {
5395
5639
  constructor(pid) {
5396
5640
  super(
@@ -5419,7 +5663,7 @@ function acquireSingleInstanceLock(appId) {
5419
5663
  } catch (err) {
5420
5664
  if (err instanceof BridgeAlreadyRunningError) throw err;
5421
5665
  }
5422
- mkdirSync2(dirname9(file), { recursive: true });
5666
+ mkdirSync2(dirname10(file), { recursive: true });
5423
5667
  const record = { pid: process.pid, appId, startedAt: Date.now() };
5424
5668
  writeFileSync(file, `${JSON.stringify(record)}
5425
5669
  `, "utf8");
@@ -5488,12 +5732,14 @@ async function runStart() {
5488
5732
  return;
5489
5733
  }
5490
5734
  const status = await getServiceAdapter().install();
5491
- 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");
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
+ );
5492
5738
  printStatus(status);
5493
5739
  }
5494
5740
  async function runStop() {
5495
5741
  await getServiceAdapter().uninstall();
5496
- 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");
5742
+ console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u505C\u6B62\uFF0C\u5E76\u5DF2\u5173\u95ED\u81EA\u542F\uFF08\u5DF2\u79FB\u9664\u670D\u52A1\u5B9A\u4E49\uFF09\u3002");
5497
5743
  }
5498
5744
  async function runRestart() {
5499
5745
  const status = await getServiceAdapter().restart();
@@ -5507,17 +5753,18 @@ async function runLogs(follow) {
5507
5753
  await getServiceAdapter().logs(follow);
5508
5754
  }
5509
5755
  function printStatus(status) {
5510
- console.log(`plist: ${status.plistPath}`);
5756
+ console.log(`service: ${status.platformName}`);
5757
+ console.log(`path: ${status.servicePath}`);
5511
5758
  console.log(`installed: ${status.installed ? "yes" : "no"}`);
5512
- console.log(`loaded: ${status.loaded ? "yes" : "no"}`);
5759
+ console.log(`running: ${status.running ? "yes" : "no"}`);
5513
5760
  console.log(`pid: ${status.pid ?? "-"}`);
5514
5761
  console.log(`last exit: ${status.lastExit ?? "-"}`);
5515
5762
  console.log(`stdout: ${status.stdoutPath}`);
5516
5763
  console.log(`stderr: ${status.stderrPath}`);
5517
5764
  if (!status.installed) {
5518
5765
  console.log("\u63D0\u793A\uFF1A\u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF0C\u8FD0\u884C `feishu-codex-bridge start`\u3002");
5519
- } else if (!status.loaded) {
5520
- console.log("\u63D0\u793A\uFF1Aplist \u5DF2\u5B58\u5728\uFF0C\u4F46 launchd \u5F53\u524D\u672A\u52A0\u8F7D\uFF08\u8BD5\u8BD5 `restart`\uFF09\u3002");
5766
+ } else if (!status.running) {
5767
+ console.log("\u63D0\u793A\uFF1A\u670D\u52A1\u5DF2\u6CE8\u518C\u4F46\u5F53\u524D\u672A\u8FD0\u884C\uFF08\u8BD5\u8BD5 `restart`\uFF09\u3002");
5521
5768
  }
5522
5769
  }
5523
5770
 
@@ -5570,7 +5817,7 @@ async function runUpdate(opts = {}) {
5570
5817
  }
5571
5818
 
5572
5819
  // src/cli/commands/bot.ts
5573
- import { rm as rm4 } from "fs/promises";
5820
+ import { rm as rm5 } from "fs/promises";
5574
5821
  async function runBotInit(name) {
5575
5822
  if (!ensureCodex()) {
5576
5823
  process.exitCode = 1;
@@ -5625,7 +5872,7 @@ async function runBotRm(name) {
5625
5872
  }
5626
5873
  const after = await removeBot(bot2.appId);
5627
5874
  await removeSecret(secretKeyForApp(bot2.appId));
5628
- await rm4(botDir(bot2.appId), { recursive: true, force: true });
5875
+ await rm5(botDir(bot2.appId), { recursive: true, force: true });
5629
5876
  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`);
5630
5877
  if (after.bots.length === 0) {
5631
5878
  console.log(" \u5DF2\u65E0\u4EFB\u4F55\u673A\u5668\u4EBA\uFF0C`bot init` \u91CD\u65B0\u521B\u5EFA\u3002");
@@ -5683,15 +5930,15 @@ async function secretsRemove(id) {
5683
5930
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
5684
5931
  }
5685
5932
  function readStdin() {
5686
- return new Promise((resolve6) => {
5933
+ return new Promise((resolve7) => {
5687
5934
  let data = "";
5688
5935
  if (process.stdin.isTTY) {
5689
- resolve6("");
5936
+ resolve7("");
5690
5937
  return;
5691
5938
  }
5692
5939
  process.stdin.setEncoding("utf8");
5693
5940
  process.stdin.on("data", (c) => data += c);
5694
- process.stdin.on("end", () => resolve6(data));
5941
+ process.stdin.on("end", () => resolve7(data));
5695
5942
  });
5696
5943
  }
5697
5944
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-win",
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,9 +30,11 @@
30
30
  "dependencies": {
31
31
  "@larksuiteoapi/node-sdk": "^1.65.0",
32
32
  "commander": "^12.1.0",
33
+ "cross-spawn": "^7.0.6",
33
34
  "qrcode-terminal": "^0.12.0"
34
35
  },
35
36
  "devDependencies": {
37
+ "@types/cross-spawn": "^6.0.6",
36
38
  "@types/node": "^22.10.0",
37
39
  "@types/qrcode-terminal": "^0.12.2",
38
40
  "tsup": "^8.3.5",