@modelzen/feishu-codex-bridge 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +11 -2
  2. package/dist/cli.js +489 -226
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # feishu-codex-bridge
2
2
 
3
+ [![npm version](https://badgen.net/npm/v/@modelzen/feishu-codex-bridge)](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
4
+ [![total downloads](https://badgen.net/npm/dt/@modelzen/feishu-codex-bridge)](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
5
+ [![downloads/month](https://badgen.net/npm/dm/@modelzen/feishu-codex-bridge)](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
6
+ [![license](https://badgen.net/npm/license/@modelzen/feishu-codex-bridge)](https://github.com/modelzen/feishu-codex-bridge/blob/main/LICENSE)
7
+
3
8
  > 把飞书 / Lark 桥接到你本机的 [Codex](https://github.com/openai/codex),在群里 @ 机器人就能让 Codex 在指定项目目录里干活,结果以流式 Markdown 卡片实时回到群里。
4
9
  >
5
10
  > **项目 = 群 = 固定工作目录(cwd)**,**话题(thread)= 一个 Codex 会话(session)**。
@@ -78,8 +83,11 @@ feishu-codex-bridge status # 状态 / pid / 日志路径 / 上次退出码
78
83
  feishu-codex-bridge logs -f # 跟踪日志
79
84
  feishu-codex-bridge restart # 重启
80
85
  feishu-codex-bridge stop # 停止并关闭开机自启
86
+ feishu-codex-bridge update # 更新到最新版(npm i -g)并自动重启 daemon(--check 只查不装)
81
87
  ```
82
88
 
89
+ > 💡 升级很省事:装了后台 daemon 的,直接 `feishu-codex-bridge update` 一条命令 = 拉最新版 + 自动 `restart`;也可在**私聊管理台**点 **⬆️ 版本更新** 按钮,机器人自更新后重启服务。
90
+
83
91
  `start` 会**先在当前终端完成 init**(没配置则扫码),并**阻塞到授权完成**——权限全部开通、且你确认已订阅事件/发布版本——才真正装服务,绝不会装一个收不到消息的空壳。daemon 体跑的就是 `run`。
84
92
 
85
93
  > ⚠️ **后台 daemon 必须全局安装(`npm i -g`),不要用 npx**:launchd plist 里硬编码了 CLI 路径,而 npx 的临时缓存(`~/.npm/_npx/...`)会被清理,缓存一没服务就起不来。前台 `run` 用 npx 没问题(单次进程)。
@@ -109,7 +117,7 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
109
117
 
110
118
  启动时若有缺失权限,会**自动打开浏览器**到形如 `https://open.feishu.cn/app/<app_id>/auth?q=...` 的页面(同时在终端打印链接),**一次性勾选全部 → 确认**即可(即时生效、无需重启)。`start`(后台 daemon)会阻塞到这步开通完成才装服务。
111
119
 
112
- 本桥需要的全部权限以 [`src/config/scopes.ts`](src/config/scopes.ts) 的 `REQUIRED_SCOPES` 为权威清单,包含:收群 @ 消息 / 全量群消息(免 @)/ 私聊消息、以机器人身份发消息与回话题、消息置顶、表情回复、上传下载资源、建群 / 转让群主、群公告读写、置顶横幅、群标签页、交互卡片。**这些都在首次开通链接里一并申请,正常用不会再遇到「权限不足」。**
120
+ 本桥需要的全部权限以 [`src/config/scopes.ts`](src/config/scopes.ts) 的 `REQUIRED_SCOPES` 为权威清单,包含:收群 @ 消息 / 全量群消息(免 @)/ 私聊消息、以机器人身份发消息与回话题、消息置顶、表情回复、上传下载资源、建群 / 转让群主 / 设群管理员、群公告读写、置顶横幅、群标签页、交互卡片。**这些都在首次开通链接里一并申请,正常用不会再遇到「权限不足」。**
113
121
 
114
122
  > 「**文档评论回复**」功能另需 `docs:document.comment:read`、`docs:document.comment:create`、`wiki:wiki:readonly` 三项(见 `COMMENT_SCOPES`)。它们**已预勾选进同一个开通链接**,但**不属于** `REQUIRED_SCOPES` —— 不开通也不会卡住后台服务安装,只是该功能静默关闭。
115
123
 
@@ -188,6 +196,7 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
188
196
  feishu-codex-bridge run 前台启动(没配置先扫码 init;Ctrl+C 优雅退出)
189
197
  feishu-codex-bridge start 后台 daemon 启动(装 launchd 开机自启;阻塞到授权完成)
190
198
  feishu-codex-bridge stop|restart|status|logs 后台 daemon 生命周期
199
+ feishu-codex-bridge update 更新到最新版并自动重启 daemon(--check 只查不装)
191
200
  feishu-codex-bridge bot init|list|use|rm 多飞书机器人:注册 / 列表 / 切当前 / 移除
192
201
  feishu-codex-bridge doctor 本地自检:codex / 登录 / lark-cli / 当前机器人
193
202
  ```
@@ -215,7 +224,7 @@ src/
215
224
  project/ 项目注册表、建群/公告/标签页 onboarding、生命周期
216
225
  config/ 加密密钥库、密钥解析、配置存储、多机器人注册表、scope 清单、路径
217
226
  core/ watchdog、单实例锁、日志
218
- cli/ commander 命令(run / start / stop / restart / status / logs / bot / doctor / secrets)
227
+ cli/ commander 命令(run / start / stop / restart / status / logs / update / bot / doctor / secrets)
219
228
  service/ launchd 后台服务
220
229
  ```
221
230
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/cli/index.ts
2
- import { readFileSync as readFileSync2 } from "fs";
3
- import { dirname as dirname8, resolve as resolve3 } from "path";
4
- import { fileURLToPath as fileURLToPath2 } from "url";
2
+ import { readFileSync as readFileSync3 } from "fs";
3
+ import { dirname as dirname9, resolve as resolve4 } from "path";
4
+ import { fileURLToPath as fileURLToPath3 } from "url";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/cli/commands/doctor.ts
@@ -528,7 +528,7 @@ async function spawnExecProvider(pc, ref) {
528
528
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
529
529
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
530
530
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
531
- return new Promise((resolve4, reject) => {
531
+ return new Promise((resolve5, reject) => {
532
532
  const env = {};
533
533
  if (pc.passEnv) for (const k of pc.passEnv) {
534
534
  const v = process.env[k];
@@ -573,7 +573,7 @@ async function spawnExecProvider(pc, ref) {
573
573
  try {
574
574
  const parsed = JSON.parse(stdout);
575
575
  const value = parsed.values?.[ref.id];
576
- if (typeof value === "string") return resolve4(value);
576
+ if (typeof value === "string") return resolve5(value);
577
577
  const err = parsed.errors?.[ref.id]?.message;
578
578
  reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
579
579
  } catch (err) {
@@ -651,6 +651,8 @@ var REQUIRED_SCOPES = [
651
651
  // create the project group
652
652
  "im:chat:update",
653
653
  // transfer ownership on unbind
654
+ "im:chat.managers:write_only",
655
+ // promote the project creator to group admin (im.v1.chat.managers.add_managers)
654
656
  "im:chat.announcement:read",
655
657
  // read group announcement blocks (list)
656
658
  "im:chat.announcement:write_only",
@@ -1100,7 +1102,7 @@ var AsyncQueue = class {
1100
1102
  continue;
1101
1103
  }
1102
1104
  if (this.closed) return;
1103
- const next = await new Promise((resolve4) => this.waiters.push(resolve4));
1105
+ const next = await new Promise((resolve5) => this.waiters.push(resolve5));
1104
1106
  if (next.done) return;
1105
1107
  yield next.value;
1106
1108
  }
@@ -1151,8 +1153,8 @@ var AppServerClient = class {
1151
1153
  const id = ++this.nextId;
1152
1154
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1153
1155
  `;
1154
- return new Promise((resolve4, reject) => {
1155
- this.pending.set(id, { resolve: resolve4, reject });
1156
+ return new Promise((resolve5, reject) => {
1157
+ this.pending.set(id, { resolve: resolve5, reject });
1156
1158
  this.child.stdin.write(payload, (err) => {
1157
1159
  if (err) {
1158
1160
  this.pending.delete(id);
@@ -1176,14 +1178,14 @@ var AppServerClient = class {
1176
1178
  const child = this.child;
1177
1179
  if (!child || child.exitCode !== null) return;
1178
1180
  child.kill("SIGTERM");
1179
- await new Promise((resolve4) => {
1181
+ await new Promise((resolve5) => {
1180
1182
  const t = setTimeout(() => {
1181
1183
  if (child.exitCode === null) child.kill("SIGKILL");
1182
- resolve4();
1184
+ resolve5();
1183
1185
  }, graceMs);
1184
1186
  child.once("exit", () => {
1185
1187
  clearTimeout(t);
1186
- resolve4();
1188
+ resolve5();
1187
1189
  });
1188
1190
  });
1189
1191
  }
@@ -1302,12 +1304,12 @@ var APPROVAL_POLICY = "never";
1302
1304
  var SANDBOX = "danger-full-access";
1303
1305
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1304
1306
  function withDeadline(p, ms, label) {
1305
- return new Promise((resolve4, reject) => {
1307
+ return new Promise((resolve5, reject) => {
1306
1308
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1307
1309
  p.then(
1308
1310
  (v) => {
1309
1311
  clearTimeout(t);
1310
- resolve4(v);
1312
+ resolve5(v);
1311
1313
  },
1312
1314
  (e) => {
1313
1315
  clearTimeout(t);
@@ -1347,11 +1349,11 @@ var CodexThread = class {
1347
1349
  if (self.model) params.model = self.model;
1348
1350
  if (self.effort) params.effort = self.effort;
1349
1351
  let startError;
1350
- const startFailed = new Promise((resolve4) => {
1352
+ const startFailed = new Promise((resolve5) => {
1351
1353
  self.client.request("turn/start", params).then(void 0, (err) => {
1352
1354
  startError = err instanceof Error ? err : new Error(String(err));
1353
1355
  log.fail("agent", startError, { phase: "turn/start" });
1354
- resolve4("start-failed");
1356
+ resolve5("start-failed");
1355
1357
  });
1356
1358
  });
1357
1359
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -2624,6 +2626,8 @@ var DM = {
2624
2626
  settings: "dm.settings",
2625
2627
  doctor: "dm.doctor",
2626
2628
  reconnect: "dm.reconnect",
2629
+ update: "dm.update",
2630
+ updateDo: "dm.update.do",
2627
2631
  rmConfirm: "dm.rmConfirm",
2628
2632
  rmDo: "dm.rmDo",
2629
2633
  rmCancel: "dm.rmCancel",
@@ -2650,12 +2654,91 @@ function buildDmMenuCard() {
2650
2654
  ]),
2651
2655
  actions([
2652
2656
  button("\u{1FA7A} \u8BCA\u65AD", { a: DM.doctor }),
2653
- button("\u{1F504} \u91CD\u8FDE", { a: DM.reconnect })
2657
+ button("\u{1F504} \u91CD\u8FDE", { a: DM.reconnect }),
2658
+ button("\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", { a: DM.update })
2654
2659
  ])
2655
2660
  ],
2656
2661
  { header: { title: "\u{1F916} Codex Bridge \u7BA1\u7406\u53F0", template: "blue" } }
2657
2662
  );
2658
2663
  }
2664
+ var backToMenu = () => actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]);
2665
+ function buildUpdateCard(state) {
2666
+ switch (state.phase) {
2667
+ case "checking":
2668
+ return card([md("\u23F3 \u6B63\u5728\u67E5\u8BE2\u6700\u65B0\u7248\u672C\u2026"), note("\u4ECE npm registry \u62C9\u53D6\u7248\u672C\u4FE1\u606F\uFF0C\u8BF7\u7A0D\u5019\u3002")], {
2669
+ header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "turquoise" }
2670
+ });
2671
+ case "checked": {
2672
+ const cur = state.current ?? "?";
2673
+ if (!state.latest) {
2674
+ return card(
2675
+ [
2676
+ md(`\u5F53\u524D\u7248\u672C\uFF1A**v${cur}**`),
2677
+ md("\u26A0\uFE0F \u67E5\u4E0D\u5230\u6700\u65B0\u7248\u672C\uFF08\u7F51\u7EDC\u6216 npm registry \u95EE\u9898\uFF09\u3002"),
2678
+ actions([button("\u{1F504} \u91CD\u8BD5", { a: DM.update }), button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
2679
+ ],
2680
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "red" } }
2681
+ );
2682
+ }
2683
+ if (!state.hasUpdate) {
2684
+ return card(
2685
+ [md(`\u2705 \u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF1A**v${cur}**`), backToMenu()],
2686
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "green" } }
2687
+ );
2688
+ }
2689
+ const head = [
2690
+ md(`\u53D1\u73B0\u65B0\u7248\u672C \u{1F389}`),
2691
+ note(`\u5F53\u524D v${cur} \u2192 \u6700\u65B0 v${state.latest}`)
2692
+ ];
2693
+ if (state.dev) {
2694
+ return card(
2695
+ [
2696
+ ...head,
2697
+ md("\u68C0\u6D4B\u5230**\u6E90\u7801\u5F00\u53D1\u6A21\u5F0F**\uFF08\u4ED3\u5E93\u5185\u6709 .git\uFF09\u3002\u8BF7\u5728\u7EC8\u7AEF\u7528 `git pull && npm i` \u66F4\u65B0\uFF0C\u800C\u4E0D\u662F\u5168\u5C40\u5B89\u88C5\u3002"),
2698
+ backToMenu()
2699
+ ],
2700
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "orange" } }
2701
+ );
2702
+ }
2703
+ return card(
2704
+ [
2705
+ ...head,
2706
+ note("\u70B9\u300C\u7ACB\u5373\u66F4\u65B0\u300D\u4F1A\u6267\u884C `npm i -g` \u5E76\u81EA\u52A8\u91CD\u542F\u540E\u53F0\u670D\u52A1\uFF08\u7EA6\u6570\u5341\u79D2\uFF09\u3002"),
2707
+ actions([
2708
+ button("\u2B06\uFE0F \u7ACB\u5373\u66F4\u65B0", { a: DM.updateDo }, "primary"),
2709
+ button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })
2710
+ ])
2711
+ ],
2712
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "blue" } }
2713
+ );
2714
+ }
2715
+ case "updating":
2716
+ return card(
2717
+ [
2718
+ md(`\u23F3 \u6B63\u5728\u66F4\u65B0\u5230\u6700\u65B0\u7248\u2026`),
2719
+ note(`\u4ECE v${state.from ?? "?"} \u5347\u7EA7\u4E2D\uFF0C\u4E0B\u8F7D\u5B89\u88C5\u7EA6\u6570\u5341\u79D2\uFF0C\u8BF7\u52FF\u91CD\u590D\u70B9\u51FB\u3002`)
2720
+ ],
2721
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "turquoise" } }
2722
+ );
2723
+ case "done": {
2724
+ const tail = state.willRestart ? note("\u6B63\u5728\u91CD\u542F\u540E\u53F0\u670D\u52A1\u4EE5\u751F\u6548 \u2014\u2014 \u91CD\u542F\u671F\u95F4\u672C\u5361\u7247\u505C\u6B62\u66F4\u65B0\uFF1B\u7A0D\u540E\u53D1\u6211\u4EFB\u610F\u6D88\u606F\u53EF\u91CD\u5F00\u7BA1\u7406\u53F0\u3002") : note("\u524D\u53F0\u6A21\u5F0F\uFF1A\u8BF7\u5728\u7EC8\u7AEF\u624B\u52A8\u91CD\u542F `run` \u8FDB\u7A0B\u4F7F\u65B0\u7248\u672C\u751F\u6548\u3002");
2725
+ return card(
2726
+ [md(`\u2705 \u5DF2\u66F4\u65B0 **v${state.from ?? "?"} \u2192 v${state.to ?? "?"}**`), tail],
2727
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "green" } }
2728
+ );
2729
+ }
2730
+ case "error":
2731
+ return card(
2732
+ [
2733
+ md("\u274C **\u66F4\u65B0\u5931\u8D25**"),
2734
+ state.message ? note(state.message) : note("npm \u5B89\u88C5\u672A\u6210\u529F\u3002"),
2735
+ md("\u53EF\u5728\u7EC8\u7AEF\u624B\u52A8\u6267\u884C\uFF1A`npm i -g @modelzen/feishu-codex-bridge@latest`\uFF08\u5FC5\u8981\u65F6\u52A0 sudo\uFF09\u3002"),
2736
+ actions([button("\u{1F504} \u91CD\u8BD5", { a: DM.update }), button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
2737
+ ],
2738
+ { header: { title: "\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", template: "red" } }
2739
+ );
2740
+ }
2741
+ }
2659
2742
  function buildNewProjectFormCard(opts = {}) {
2660
2743
  const elements = [];
2661
2744
  if (opts.error) elements.push(md(`\u274C **\u521B\u5EFA\u5931\u8D25**\uFF1A${opts.error}`));
@@ -2788,9 +2871,266 @@ function buildGroupSettingsCard(project) {
2788
2871
  );
2789
2872
  }
2790
2873
 
2874
+ // src/service/update.ts
2875
+ import { execFile, spawn as spawn5 } from "child_process";
2876
+ import { existsSync as existsSync5, readFileSync } from "fs";
2877
+ import { dirname as dirname5, join as join8, resolve as resolve2 } from "path";
2878
+ import { fileURLToPath as fileURLToPath2 } from "url";
2879
+ import { promisify } from "util";
2880
+
2881
+ // src/service/launchd.ts
2882
+ import { spawn as spawn4, spawnSync } from "child_process";
2883
+ import { existsSync as existsSync4 } from "fs";
2884
+ import { appendFile, mkdir as mkdir4, rm as rm2, writeFile as writeFile4 } from "fs/promises";
2885
+ import { homedir as homedir3, userInfo as userInfo2 } from "os";
2886
+ import { dirname as dirname4, join as join7, resolve } from "path";
2887
+ import { fileURLToPath } from "url";
2888
+ var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
2889
+ function launchAgentPlistPath() {
2890
+ return join7(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
2891
+ }
2892
+ function serviceStdoutPath() {
2893
+ return join7(paths.appDir, "service.log");
2894
+ }
2895
+ function serviceStderrPath() {
2896
+ return join7(paths.appDir, "service.err.log");
2897
+ }
2898
+ function resolveCliBinPath() {
2899
+ const distDir = dirname4(fileURLToPath(import.meta.url));
2900
+ return resolve(distDir, "..", "bin", "feishu-codex-bridge.mjs");
2901
+ }
2902
+ function escapeXml(value) {
2903
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2904
+ }
2905
+ function buildPlist() {
2906
+ const nodePath = process.execPath;
2907
+ const cliBinPath = resolveCliBinPath();
2908
+ const pathEnv = process.env.PATH ?? "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
2909
+ return `<?xml version="1.0" encoding="UTF-8"?>
2910
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2911
+ <plist version="1.0">
2912
+ <dict>
2913
+ <key>Label</key>
2914
+ <string>${LAUNCHD_LABEL}</string>
2915
+ <key>ProgramArguments</key>
2916
+ <array>
2917
+ <string>${escapeXml(nodePath)}</string>
2918
+ <string>${escapeXml(cliBinPath)}</string>
2919
+ <string>run</string>
2920
+ </array>
2921
+ <key>RunAtLoad</key>
2922
+ <true/>
2923
+ <key>KeepAlive</key>
2924
+ <true/>
2925
+ <key>StandardOutPath</key>
2926
+ <string>${escapeXml(serviceStdoutPath())}</string>
2927
+ <key>StandardErrorPath</key>
2928
+ <string>${escapeXml(serviceStderrPath())}</string>
2929
+ <key>EnvironmentVariables</key>
2930
+ <dict>
2931
+ <key>PATH</key>
2932
+ <string>${escapeXml(pathEnv)}</string>
2933
+ </dict>
2934
+ </dict>
2935
+ </plist>
2936
+ `;
2937
+ }
2938
+ async function installLaunchd() {
2939
+ const plistPath = launchAgentPlistPath();
2940
+ await mkdir4(dirname4(plistPath), { recursive: true });
2941
+ await ensureLogFiles();
2942
+ await writeFile4(plistPath, buildPlist(), "utf8");
2943
+ if (isLoaded()) {
2944
+ const bootout = runLaunchctl(["bootout", serviceTarget()]);
2945
+ if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
2946
+ await waitUntilUnloaded();
2947
+ }
2948
+ const bootstrap = runLaunchctl(["bootstrap", userTarget(), plistPath]);
2949
+ if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
2950
+ return statusLaunchd();
2951
+ }
2952
+ async function uninstallLaunchd() {
2953
+ if (isLoaded()) {
2954
+ const bootout = runLaunchctl(["bootout", serviceTarget()]);
2955
+ if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
2956
+ await waitUntilUnloaded();
2957
+ }
2958
+ await rm2(launchAgentPlistPath(), { force: true });
2959
+ }
2960
+ async function restartLaunchd() {
2961
+ if (!existsSync4(launchAgentPlistPath())) {
2962
+ throw new Error(`launchd service \u672A\u5B89\u88C5\uFF1A${launchAgentPlistPath()}`);
2963
+ }
2964
+ if (isLoaded()) {
2965
+ const bootout = runLaunchctl(["bootout", serviceTarget()]);
2966
+ if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
2967
+ await waitUntilUnloaded();
2968
+ }
2969
+ const bootstrap = runLaunchctl(["bootstrap", userTarget(), launchAgentPlistPath()]);
2970
+ if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
2971
+ return statusLaunchd();
2972
+ }
2973
+ function statusLaunchd() {
2974
+ const result = runLaunchctl(["print", serviceTarget()]);
2975
+ const raw = result.stdout || result.stderr;
2976
+ const parsed = parseLaunchdStatus(raw);
2977
+ return {
2978
+ installed: existsSync4(launchAgentPlistPath()),
2979
+ loaded: result.ok,
2980
+ plistPath: launchAgentPlistPath(),
2981
+ stdoutPath: serviceStdoutPath(),
2982
+ stderrPath: serviceStderrPath(),
2983
+ pid: parsed.pid,
2984
+ lastExit: parsed.lastExit,
2985
+ raw
2986
+ };
2987
+ }
2988
+ async function tailLaunchdLogs(follow) {
2989
+ await ensureLogFiles();
2990
+ const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
2991
+ await new Promise((resolvePromise, reject) => {
2992
+ const child = spawn4("tail", args, { stdio: "inherit" });
2993
+ child.on("error", reject);
2994
+ child.on("close", (code) => {
2995
+ if (code === 0 || follow && code === null) {
2996
+ resolvePromise();
2997
+ return;
2998
+ }
2999
+ reject(new Error(`tail \u9000\u51FA\u7801 ${code ?? "unknown"}`));
3000
+ });
3001
+ });
3002
+ }
3003
+ function parseLaunchdStatus(text) {
3004
+ return {
3005
+ pid: text.match(/\bpid\s*=\s*(\d+)/)?.[1],
3006
+ lastExit: text.match(/last exit code\s*=\s*(-?\d+)/i)?.[1]
3007
+ };
3008
+ }
3009
+ function isLoaded() {
3010
+ const result = spawnSync("launchctl", ["print", serviceTarget()], {
3011
+ stdio: ["ignore", "ignore", "ignore"]
3012
+ });
3013
+ return result.status === 0;
3014
+ }
3015
+ async function waitUntilUnloaded(timeoutMs = 5e3) {
3016
+ const deadline = Date.now() + timeoutMs;
3017
+ while (Date.now() < deadline) {
3018
+ if (!isLoaded()) return;
3019
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
3020
+ }
3021
+ throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
3022
+ }
3023
+ async function ensureLogFiles() {
3024
+ await mkdir4(paths.appDir, { recursive: true });
3025
+ await appendFile(serviceStdoutPath(), "");
3026
+ await appendFile(serviceStderrPath(), "");
3027
+ }
3028
+ function userTarget() {
3029
+ return `gui/${userInfo2().uid}`;
3030
+ }
3031
+ function serviceTarget() {
3032
+ return `${userTarget()}/${LAUNCHD_LABEL}`;
3033
+ }
3034
+ function runLaunchctl(args) {
3035
+ const result = spawnSync("launchctl", args, { encoding: "utf8" });
3036
+ return {
3037
+ ok: result.status === 0,
3038
+ status: result.status,
3039
+ stdout: result.stdout ?? "",
3040
+ stderr: result.stderr ?? ""
3041
+ };
3042
+ }
3043
+ function launchctlError(command, result) {
3044
+ const output = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
3045
+ return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
3046
+ }
3047
+
3048
+ // src/service/adapter.ts
3049
+ function getServiceAdapter() {
3050
+ if (process.platform !== "darwin") {
3051
+ throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
3052
+ }
3053
+ return {
3054
+ install: installLaunchd,
3055
+ uninstall: uninstallLaunchd,
3056
+ status: async () => statusLaunchd(),
3057
+ restart: restartLaunchd,
3058
+ logs: tailLaunchdLogs
3059
+ };
3060
+ }
3061
+
3062
+ // src/service/update.ts
3063
+ var execFileP = promisify(execFile);
3064
+ var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
3065
+ function pkgRoot() {
3066
+ return resolve2(dirname5(fileURLToPath2(import.meta.url)), "..");
3067
+ }
3068
+ function pkgJson() {
3069
+ try {
3070
+ return JSON.parse(readFileSync(join8(pkgRoot(), "package.json"), "utf8"));
3071
+ } catch {
3072
+ return {};
3073
+ }
3074
+ }
3075
+ function currentVersion() {
3076
+ return pkgJson().version ?? "0.0.0";
3077
+ }
3078
+ function packageName() {
3079
+ return pkgJson().name ?? "@modelzen/feishu-codex-bridge";
3080
+ }
3081
+ function isDevSource() {
3082
+ return existsSync5(join8(pkgRoot(), ".git"));
3083
+ }
3084
+ function isNewer(a, b) {
3085
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
3086
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
3087
+ for (let i = 0; i < 3; i++) {
3088
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
3089
+ if (d !== 0) return d > 0;
3090
+ }
3091
+ return false;
3092
+ }
3093
+ async function latestVersion() {
3094
+ try {
3095
+ const { stdout } = await execFileP(NPM, ["view", packageName(), "version"], { timeout: 2e4 });
3096
+ const v = stdout.trim();
3097
+ return /^\d+\.\d+\.\d+/.test(v) ? v : null;
3098
+ } catch {
3099
+ return null;
3100
+ }
3101
+ }
3102
+ async function installLatest(opts = {}) {
3103
+ const target = `${packageName()}@latest`;
3104
+ return await new Promise((resolveP) => {
3105
+ const child = spawn5(NPM, ["install", "-g", target], {
3106
+ stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
3107
+ });
3108
+ let out = "";
3109
+ if (!opts.inherit) {
3110
+ child.stdout?.on("data", (d) => out += d);
3111
+ child.stderr?.on("data", (d) => out += d);
3112
+ }
3113
+ child.on("error", (e) => resolveP({ ok: false, message: e.message }));
3114
+ child.on("close", (code) => {
3115
+ const tail = out.trim().slice(-600);
3116
+ resolveP({ ok: code === 0, message: opts.inherit ? `\u9000\u51FA\u7801 ${code}` : tail || `\u9000\u51FA\u7801 ${code}` });
3117
+ });
3118
+ });
3119
+ }
3120
+ function daemonRunning() {
3121
+ try {
3122
+ return statusLaunchd().loaded;
3123
+ } catch {
3124
+ return false;
3125
+ }
3126
+ }
3127
+ async function restartDaemon() {
3128
+ await getServiceAdapter().restart();
3129
+ }
3130
+
2791
3131
  // src/project/registry.ts
2792
- import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2793
- import { dirname as dirname4 } from "path";
3132
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3133
+ import { dirname as dirname6 } from "path";
2794
3134
  var FILE_VERSION2 = 1;
2795
3135
  async function read() {
2796
3136
  try {
@@ -2803,10 +3143,10 @@ async function read() {
2803
3143
  }
2804
3144
  }
2805
3145
  async function write(projects) {
2806
- await mkdir4(dirname4(paths.projectsFile), { recursive: true });
3146
+ await mkdir5(dirname6(paths.projectsFile), { recursive: true });
2807
3147
  const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
2808
3148
  const body = { version: FILE_VERSION2, projects };
2809
- await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
3149
+ await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
2810
3150
  `, "utf8");
2811
3151
  await rename4(tmp, paths.projectsFile);
2812
3152
  }
@@ -2847,14 +3187,14 @@ async function removeProject(name) {
2847
3187
  }
2848
3188
 
2849
3189
  // src/project/lifecycle.ts
2850
- import { mkdir as mkdir5 } from "fs/promises";
2851
- import { existsSync as existsSync4 } from "fs";
2852
- import { isAbsolute, join as join7, resolve } from "path";
3190
+ import { mkdir as mkdir6 } from "fs/promises";
3191
+ import { existsSync as existsSync6 } from "fs";
3192
+ import { isAbsolute, join as join9, resolve as resolve3 } from "path";
2853
3193
 
2854
3194
  // src/project/git-info.ts
2855
- import { execFile } from "child_process";
2856
- import { promisify } from "util";
2857
- var execFileAsync = promisify(execFile);
3195
+ import { execFile as execFile2 } from "child_process";
3196
+ import { promisify as promisify2 } from "util";
3197
+ var execFileAsync = promisify2(execFile2);
2858
3198
  async function currentBranch(cwd) {
2859
3199
  try {
2860
3200
  const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
@@ -2987,20 +3327,25 @@ async function createProject(channel, input2) {
2987
3327
  let cwd;
2988
3328
  let blank;
2989
3329
  if (input2.existingPath) {
2990
- cwd = isAbsolute(input2.existingPath) ? input2.existingPath : resolve(input2.existingPath);
2991
- if (!existsSync4(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
3330
+ cwd = isAbsolute(input2.existingPath) ? input2.existingPath : resolve3(input2.existingPath);
3331
+ if (!existsSync6(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
2992
3332
  blank = false;
2993
3333
  } else {
2994
- cwd = join7(paths.projectsRootDir, name);
2995
- await mkdir5(cwd, { recursive: true });
3334
+ cwd = join9(paths.projectsRootDir, name);
3335
+ await mkdir6(cwd, { recursive: true });
2996
3336
  blank = true;
2997
3337
  }
2998
3338
  const res = await channel.rawClient.im.v1.chat.create({
2999
- params: { user_id_type: "open_id", set_bot_manager: true },
3339
+ params: { user_id_type: "open_id" },
3000
3340
  data: { name, user_id_list: [input2.ownerOpenId] }
3001
3341
  });
3002
3342
  const chatId = res.data?.chat_id;
3003
3343
  if (!chatId) throw new Error(`\u5EFA\u7FA4\u5931\u8D25\uFF1A${JSON.stringify(res).slice(0, 200)}`);
3344
+ await channel.rawClient.im.v1.chatManagers.addManagers({
3345
+ path: { chat_id: chatId },
3346
+ params: { member_id_type: "open_id" },
3347
+ data: { manager_ids: [input2.ownerOpenId] }
3348
+ }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
3004
3349
  const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi" };
3005
3350
  await addProject(project);
3006
3351
  log.info("project", "create", { name, chatId, cwd, blank });
@@ -3020,8 +3365,8 @@ async function transferOwnership(channel, chatId, toOpenId) {
3020
3365
  }
3021
3366
 
3022
3367
  // src/bot/session-store.ts
3023
- import { mkdir as mkdir6, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "fs/promises";
3024
- import { dirname as dirname5 } from "path";
3368
+ import { mkdir as mkdir7, readFile as readFile6, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3369
+ import { dirname as dirname7 } from "path";
3025
3370
  var FILE_VERSION3 = 1;
3026
3371
  async function read2() {
3027
3372
  try {
@@ -3034,10 +3379,10 @@ async function read2() {
3034
3379
  }
3035
3380
  }
3036
3381
  async function write2(sessions) {
3037
- await mkdir6(dirname5(paths.sessionsFile), { recursive: true });
3382
+ await mkdir7(dirname7(paths.sessionsFile), { recursive: true });
3038
3383
  const tmp = `${paths.sessionsFile}.tmp-${process.pid}`;
3039
3384
  const body = { version: FILE_VERSION3, sessions };
3040
- await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
3385
+ await writeFile6(tmp, `${JSON.stringify(body, null, 2)}
3041
3386
  `, "utf8");
3042
3387
  await rename5(tmp, paths.sessionsFile);
3043
3388
  }
@@ -3787,6 +4132,54 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3787
4132
  const conn = channel.getConnectionStatus?.()?.state ?? "unknown";
3788
4133
  await channel.send(evt.chatId, { markdown: `\u{1F504} \u957F\u8FDE\u63A5\u72B6\u6001\uFF1A**${conn}**
3789
4134
  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);
4135
+ }).on(DM.update, ({ evt }) => {
4136
+ if (!dmAdmin(evt.operator?.openId)) return;
4137
+ void (async () => {
4138
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
4139
+ await updateManagedCard(channel, evt.messageId, buildUpdateCard({ phase: "checking" })).catch(
4140
+ () => void 0
4141
+ );
4142
+ const current = currentVersion();
4143
+ const latest = await latestVersion().catch(() => null);
4144
+ const hasUpdate = !!latest && isNewer(latest, current);
4145
+ log.info("console", "update-check", { current, latest, hasUpdate });
4146
+ await updateManagedCard(
4147
+ channel,
4148
+ evt.messageId,
4149
+ buildUpdateCard({ phase: "checked", current, latest, hasUpdate, dev: isDevSource() })
4150
+ ).catch((e) => log.fail("console", e, { phase: "update-check" }));
4151
+ })();
4152
+ }).on(DM.updateDo, ({ evt }) => {
4153
+ if (!dmAdmin(evt.operator?.openId)) return;
4154
+ void (async () => {
4155
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
4156
+ const from = currentVersion();
4157
+ await updateManagedCard(channel, evt.messageId, buildUpdateCard({ phase: "updating", from })).catch(
4158
+ () => void 0
4159
+ );
4160
+ const res = await installLatest();
4161
+ if (!res.ok) {
4162
+ log.info("console", "update-failed", { from });
4163
+ await updateManagedCard(
4164
+ channel,
4165
+ evt.messageId,
4166
+ buildUpdateCard({ phase: "error", from, message: res.message })
4167
+ ).catch((e) => log.fail("console", e, { phase: "update-error" }));
4168
+ return;
4169
+ }
4170
+ const to = currentVersion();
4171
+ const willRestart = daemonRunning();
4172
+ log.info("console", "update-done", { from, to, willRestart });
4173
+ await updateManagedCard(
4174
+ channel,
4175
+ evt.messageId,
4176
+ buildUpdateCard({ phase: "done", from, to, willRestart })
4177
+ ).catch((e) => log.fail("console", e, { phase: "update-done" }));
4178
+ if (willRestart) {
4179
+ await new Promise((r) => setTimeout(r, 800));
4180
+ await restartDaemon().catch((e) => log.fail("console", e, { phase: "update-restart" }));
4181
+ }
4182
+ })();
3790
4183
  }).on(DM.rmConfirm, async ({ evt, value }) => {
3791
4184
  const name = typeof value.n === "string" ? value.n : void 0;
3792
4185
  if (!dmAdmin(evt.operator?.openId) || !name) return;
@@ -4192,8 +4585,8 @@ async function startBridge(opts) {
4192
4585
  }
4193
4586
 
4194
4587
  // src/core/single-instance.ts
4195
- import { mkdirSync as mkdirSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
4196
- import { dirname as dirname6 } from "path";
4588
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
4589
+ import { dirname as dirname8 } from "path";
4197
4590
  var BridgeAlreadyRunningError = class extends Error {
4198
4591
  constructor(pid) {
4199
4592
  super(
@@ -4215,20 +4608,20 @@ function isAlive(pid) {
4215
4608
  function acquireSingleInstanceLock(appId) {
4216
4609
  const file = paths.processesFile;
4217
4610
  try {
4218
- const rec = JSON.parse(readFileSync(file, "utf8"));
4611
+ const rec = JSON.parse(readFileSync2(file, "utf8"));
4219
4612
  if (rec.pid && rec.pid !== process.pid && rec.appId === appId && isAlive(rec.pid)) {
4220
4613
  throw new BridgeAlreadyRunningError(rec.pid);
4221
4614
  }
4222
4615
  } catch (err) {
4223
4616
  if (err instanceof BridgeAlreadyRunningError) throw err;
4224
4617
  }
4225
- mkdirSync2(dirname6(file), { recursive: true });
4618
+ mkdirSync2(dirname8(file), { recursive: true });
4226
4619
  const record = { pid: process.pid, appId, startedAt: Date.now() };
4227
4620
  writeFileSync(file, `${JSON.stringify(record)}
4228
4621
  `, "utf8");
4229
4622
  const release = () => {
4230
4623
  try {
4231
- const rec = JSON.parse(readFileSync(file, "utf8"));
4624
+ const rec = JSON.parse(readFileSync2(file, "utf8"));
4232
4625
  if (rec.pid === process.pid) unlinkSync(file);
4233
4626
  } catch {
4234
4627
  }
@@ -4279,187 +4672,6 @@ async function runRun() {
4279
4672
  });
4280
4673
  }
4281
4674
 
4282
- // src/service/launchd.ts
4283
- import { spawn as spawn4, spawnSync } from "child_process";
4284
- import { existsSync as existsSync5 } from "fs";
4285
- import { appendFile, mkdir as mkdir7, rm as rm2, writeFile as writeFile6 } from "fs/promises";
4286
- import { homedir as homedir3, userInfo as userInfo2 } from "os";
4287
- import { dirname as dirname7, join as join8, resolve as resolve2 } from "path";
4288
- import { fileURLToPath } from "url";
4289
- var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
4290
- function launchAgentPlistPath() {
4291
- return join8(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
4292
- }
4293
- function serviceStdoutPath() {
4294
- return join8(paths.appDir, "service.log");
4295
- }
4296
- function serviceStderrPath() {
4297
- return join8(paths.appDir, "service.err.log");
4298
- }
4299
- function resolveCliBinPath() {
4300
- const distDir = dirname7(fileURLToPath(import.meta.url));
4301
- return resolve2(distDir, "..", "bin", "feishu-codex-bridge.mjs");
4302
- }
4303
- function escapeXml(value) {
4304
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4305
- }
4306
- function buildPlist() {
4307
- const nodePath = process.execPath;
4308
- const cliBinPath = resolveCliBinPath();
4309
- const pathEnv = process.env.PATH ?? "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
4310
- return `<?xml version="1.0" encoding="UTF-8"?>
4311
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4312
- <plist version="1.0">
4313
- <dict>
4314
- <key>Label</key>
4315
- <string>${LAUNCHD_LABEL}</string>
4316
- <key>ProgramArguments</key>
4317
- <array>
4318
- <string>${escapeXml(nodePath)}</string>
4319
- <string>${escapeXml(cliBinPath)}</string>
4320
- <string>run</string>
4321
- </array>
4322
- <key>RunAtLoad</key>
4323
- <true/>
4324
- <key>KeepAlive</key>
4325
- <true/>
4326
- <key>StandardOutPath</key>
4327
- <string>${escapeXml(serviceStdoutPath())}</string>
4328
- <key>StandardErrorPath</key>
4329
- <string>${escapeXml(serviceStderrPath())}</string>
4330
- <key>EnvironmentVariables</key>
4331
- <dict>
4332
- <key>PATH</key>
4333
- <string>${escapeXml(pathEnv)}</string>
4334
- </dict>
4335
- </dict>
4336
- </plist>
4337
- `;
4338
- }
4339
- async function installLaunchd() {
4340
- const plistPath = launchAgentPlistPath();
4341
- await mkdir7(dirname7(plistPath), { recursive: true });
4342
- await ensureLogFiles();
4343
- await writeFile6(plistPath, buildPlist(), "utf8");
4344
- if (isLoaded()) {
4345
- const bootout = runLaunchctl(["bootout", serviceTarget()]);
4346
- if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
4347
- await waitUntilUnloaded();
4348
- }
4349
- const bootstrap = runLaunchctl(["bootstrap", userTarget(), plistPath]);
4350
- if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
4351
- return statusLaunchd();
4352
- }
4353
- async function uninstallLaunchd() {
4354
- if (isLoaded()) {
4355
- const bootout = runLaunchctl(["bootout", serviceTarget()]);
4356
- if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
4357
- await waitUntilUnloaded();
4358
- }
4359
- await rm2(launchAgentPlistPath(), { force: true });
4360
- }
4361
- async function restartLaunchd() {
4362
- if (!existsSync5(launchAgentPlistPath())) {
4363
- throw new Error(`launchd service \u672A\u5B89\u88C5\uFF1A${launchAgentPlistPath()}`);
4364
- }
4365
- if (isLoaded()) {
4366
- const bootout = runLaunchctl(["bootout", serviceTarget()]);
4367
- if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
4368
- await waitUntilUnloaded();
4369
- }
4370
- const bootstrap = runLaunchctl(["bootstrap", userTarget(), launchAgentPlistPath()]);
4371
- if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
4372
- return statusLaunchd();
4373
- }
4374
- function statusLaunchd() {
4375
- const result = runLaunchctl(["print", serviceTarget()]);
4376
- const raw = result.stdout || result.stderr;
4377
- const parsed = parseLaunchdStatus(raw);
4378
- return {
4379
- installed: existsSync5(launchAgentPlistPath()),
4380
- loaded: result.ok,
4381
- plistPath: launchAgentPlistPath(),
4382
- stdoutPath: serviceStdoutPath(),
4383
- stderrPath: serviceStderrPath(),
4384
- pid: parsed.pid,
4385
- lastExit: parsed.lastExit,
4386
- raw
4387
- };
4388
- }
4389
- async function tailLaunchdLogs(follow) {
4390
- await ensureLogFiles();
4391
- const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
4392
- await new Promise((resolvePromise, reject) => {
4393
- const child = spawn4("tail", args, { stdio: "inherit" });
4394
- child.on("error", reject);
4395
- child.on("close", (code) => {
4396
- if (code === 0 || follow && code === null) {
4397
- resolvePromise();
4398
- return;
4399
- }
4400
- reject(new Error(`tail \u9000\u51FA\u7801 ${code ?? "unknown"}`));
4401
- });
4402
- });
4403
- }
4404
- function parseLaunchdStatus(text) {
4405
- return {
4406
- pid: text.match(/\bpid\s*=\s*(\d+)/)?.[1],
4407
- lastExit: text.match(/last exit code\s*=\s*(-?\d+)/i)?.[1]
4408
- };
4409
- }
4410
- function isLoaded() {
4411
- const result = spawnSync("launchctl", ["print", serviceTarget()], {
4412
- stdio: ["ignore", "ignore", "ignore"]
4413
- });
4414
- return result.status === 0;
4415
- }
4416
- async function waitUntilUnloaded(timeoutMs = 5e3) {
4417
- const deadline = Date.now() + timeoutMs;
4418
- while (Date.now() < deadline) {
4419
- if (!isLoaded()) return;
4420
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
4421
- }
4422
- throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
4423
- }
4424
- async function ensureLogFiles() {
4425
- await mkdir7(paths.appDir, { recursive: true });
4426
- await appendFile(serviceStdoutPath(), "");
4427
- await appendFile(serviceStderrPath(), "");
4428
- }
4429
- function userTarget() {
4430
- return `gui/${userInfo2().uid}`;
4431
- }
4432
- function serviceTarget() {
4433
- return `${userTarget()}/${LAUNCHD_LABEL}`;
4434
- }
4435
- function runLaunchctl(args) {
4436
- const result = spawnSync("launchctl", args, { encoding: "utf8" });
4437
- return {
4438
- ok: result.status === 0,
4439
- status: result.status,
4440
- stdout: result.stdout ?? "",
4441
- stderr: result.stderr ?? ""
4442
- };
4443
- }
4444
- function launchctlError(command, result) {
4445
- const output = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
4446
- return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
4447
- }
4448
-
4449
- // src/service/adapter.ts
4450
- function getServiceAdapter() {
4451
- if (process.platform !== "darwin") {
4452
- throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
4453
- }
4454
- return {
4455
- install: installLaunchd,
4456
- uninstall: uninstallLaunchd,
4457
- status: async () => statusLaunchd(),
4458
- restart: restartLaunchd,
4459
- logs: tailLaunchdLogs
4460
- };
4461
- }
4462
-
4463
4675
  // src/cli/commands/daemon.ts
4464
4676
  async function runStart() {
4465
4677
  const ready = await ensureOnboarded({ allowCreate: true });
@@ -4505,6 +4717,54 @@ function printStatus(status) {
4505
4717
  }
4506
4718
  }
4507
4719
 
4720
+ // src/cli/commands/update.ts
4721
+ async function runUpdate(opts = {}) {
4722
+ const pkg = packageName();
4723
+ const current = currentVersion();
4724
+ console.log(`\u5F53\u524D\u7248\u672C\uFF1Av${current}`);
4725
+ console.log("\u67E5\u8BE2\u6700\u65B0\u7248\u672C\u2026");
4726
+ const latest = await latestVersion();
4727
+ if (!latest) {
4728
+ console.log("\u26A0\uFE0F \u67E5\u4E0D\u5230\u6700\u65B0\u7248\u672C\uFF08\u7F51\u7EDC\u6216 npm registry \u95EE\u9898\uFF09\u3002");
4729
+ process.exitCode = 1;
4730
+ return;
4731
+ }
4732
+ if (!isNewer(latest, current)) {
4733
+ console.log(`\u2713 \u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${current}\uFF09\u3002`);
4734
+ return;
4735
+ }
4736
+ console.log(`\u53D1\u73B0\u65B0\u7248\u672C\uFF1Av${current} \u2192 v${latest}`);
4737
+ if (opts.check) {
4738
+ console.log("\u8FD0\u884C `feishu-codex-bridge update` \u5B89\u88C5\u66F4\u65B0\u3002");
4739
+ return;
4740
+ }
4741
+ if (isDevSource()) {
4742
+ console.log("\u68C0\u6D4B\u5230\u6E90\u7801\u5F00\u53D1\u6A21\u5F0F\uFF08\u4ED3\u5E93\u5185\u6709 .git\uFF09\u3002\u8BF7\u7528\uFF1Agit pull && npm i \u2014\u2014 \u8DF3\u8FC7\u5168\u5C40\u5B89\u88C5\u3002");
4743
+ return;
4744
+ }
4745
+ console.log(`\u5F00\u59CB\u5168\u5C40\u5B89\u88C5\u6700\u65B0\u7248\uFF08npm i -g ${pkg}@latest\uFF09\u2026`);
4746
+ const res = await installLatest({ inherit: true });
4747
+ if (!res.ok) {
4748
+ console.log(`\u274C \u5B89\u88C5\u5931\u8D25\uFF1A${res.message}`);
4749
+ console.log(`\u53EF\u5728\u7EC8\u7AEF\u624B\u52A8\u6267\u884C\uFF1Anpm i -g ${pkg}@latest`);
4750
+ process.exitCode = 1;
4751
+ return;
4752
+ }
4753
+ console.log(`\u2713 \u5DF2\u66F4\u65B0\u5230 v${latest}`);
4754
+ if (daemonRunning()) {
4755
+ console.log("\u91CD\u542F\u540E\u53F0\u670D\u52A1\u4EE5\u52A0\u8F7D\u65B0\u7248\u672C\u2026");
4756
+ try {
4757
+ await restartDaemon();
4758
+ console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u91CD\u542F\uFF0C\u65B0\u7248\u672C\u5DF2\u751F\u6548\u3002");
4759
+ } catch (err) {
4760
+ console.log(`\u26A0\uFE0F \u81EA\u52A8\u91CD\u542F\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
4761
+ console.log("\u8BF7\u624B\u52A8\u6267\u884C\uFF1Afeishu-codex-bridge restart");
4762
+ }
4763
+ } else {
4764
+ console.log("\u672A\u68C0\u6D4B\u5230\u8FD0\u884C\u4E2D\u7684\u540E\u53F0 daemon\uFF1B\u4E0B\u6B21 `start` / `run` \u5373\u7528\u65B0\u7248\u672C\u3002");
4765
+ }
4766
+ }
4767
+
4508
4768
  // src/cli/commands/bot.ts
4509
4769
  import { rm as rm3 } from "fs/promises";
4510
4770
  async function runBotInit(name) {
@@ -4618,15 +4878,15 @@ async function secretsRemove(id) {
4618
4878
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
4619
4879
  }
4620
4880
  function readStdin() {
4621
- return new Promise((resolve4) => {
4881
+ return new Promise((resolve5) => {
4622
4882
  let data = "";
4623
4883
  if (process.stdin.isTTY) {
4624
- resolve4("");
4884
+ resolve5("");
4625
4885
  return;
4626
4886
  }
4627
4887
  process.stdin.setEncoding("utf8");
4628
4888
  process.stdin.on("data", (c) => data += c);
4629
- process.stdin.on("end", () => resolve4(data));
4889
+ process.stdin.on("end", () => resolve5(data));
4630
4890
  });
4631
4891
  }
4632
4892
 
@@ -4634,8 +4894,8 @@ function readStdin() {
4634
4894
  var program = new Command();
4635
4895
  function readVersion() {
4636
4896
  try {
4637
- const pkgPath = resolve3(dirname8(fileURLToPath2(import.meta.url)), "..", "package.json");
4638
- return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "0.0.0";
4897
+ const pkgPath = resolve4(dirname9(fileURLToPath3(import.meta.url)), "..", "package.json");
4898
+ return JSON.parse(readFileSync3(pkgPath, "utf8")).version ?? "0.0.0";
4639
4899
  } catch {
4640
4900
  return "0.0.0";
4641
4901
  }
@@ -4659,6 +4919,9 @@ program.command("status").description("\u540E\u53F0 daemon \u72B6\u6001\uFF08pid
4659
4919
  program.command("logs").description("\u67E5\u770B\u540E\u53F0 daemon \u65E5\u5FD7").option("-f, --follow", "\u6301\u7EED\u8DDF\u968F\u65E5\u5FD7").action(async (options) => {
4660
4920
  await runLogs(Boolean(options.follow));
4661
4921
  });
4922
+ program.command("update").description("\u66F4\u65B0\u5230\u6700\u65B0\u7248\uFF08npm i -g\uFF09\uFF0C\u5E76\u81EA\u52A8\u91CD\u542F\u540E\u53F0 daemon").option("--check", "\u53EA\u68C0\u67E5\u6709\u65E0\u65B0\u7248\uFF0C\u4E0D\u5B89\u88C5").action(async (options) => {
4923
+ await runUpdate({ check: Boolean(options.check) });
4924
+ });
4662
4925
  var bot = program.command("bot").description("\u98DE\u4E66\u673A\u5668\u4EBA\u7BA1\u7406\uFF08\u591A\u673A\u5668\u4EBA\uFF09");
4663
4926
  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) => {
4664
4927
  await runBotInit(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {