@modelzen/feishu-codex-bridge 0.1.1 → 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 +496 -222
  3. package/package.json +3 -2
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,4 +1,7 @@
1
1
  // src/cli/index.ts
2
+ import { readFileSync as readFileSync3 } from "fs";
3
+ import { dirname as dirname9, resolve as resolve4 } from "path";
4
+ import { fileURLToPath as fileURLToPath3 } from "url";
2
5
  import { Command } from "commander";
3
6
 
4
7
  // src/cli/commands/doctor.ts
@@ -525,7 +528,7 @@ async function spawnExecProvider(pc, ref) {
525
528
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
526
529
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
527
530
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
528
- return new Promise((resolve3, reject) => {
531
+ return new Promise((resolve5, reject) => {
529
532
  const env = {};
530
533
  if (pc.passEnv) for (const k of pc.passEnv) {
531
534
  const v = process.env[k];
@@ -570,7 +573,7 @@ async function spawnExecProvider(pc, ref) {
570
573
  try {
571
574
  const parsed = JSON.parse(stdout);
572
575
  const value = parsed.values?.[ref.id];
573
- if (typeof value === "string") return resolve3(value);
576
+ if (typeof value === "string") return resolve5(value);
574
577
  const err = parsed.errors?.[ref.id]?.message;
575
578
  reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
576
579
  } catch (err) {
@@ -648,6 +651,8 @@ var REQUIRED_SCOPES = [
648
651
  // create the project group
649
652
  "im:chat:update",
650
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)
651
656
  "im:chat.announcement:read",
652
657
  // read group announcement blocks (list)
653
658
  "im:chat.announcement:write_only",
@@ -1097,7 +1102,7 @@ var AsyncQueue = class {
1097
1102
  continue;
1098
1103
  }
1099
1104
  if (this.closed) return;
1100
- const next = await new Promise((resolve3) => this.waiters.push(resolve3));
1105
+ const next = await new Promise((resolve5) => this.waiters.push(resolve5));
1101
1106
  if (next.done) return;
1102
1107
  yield next.value;
1103
1108
  }
@@ -1148,8 +1153,8 @@ var AppServerClient = class {
1148
1153
  const id = ++this.nextId;
1149
1154
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1150
1155
  `;
1151
- return new Promise((resolve3, reject) => {
1152
- this.pending.set(id, { resolve: resolve3, reject });
1156
+ return new Promise((resolve5, reject) => {
1157
+ this.pending.set(id, { resolve: resolve5, reject });
1153
1158
  this.child.stdin.write(payload, (err) => {
1154
1159
  if (err) {
1155
1160
  this.pending.delete(id);
@@ -1173,14 +1178,14 @@ var AppServerClient = class {
1173
1178
  const child = this.child;
1174
1179
  if (!child || child.exitCode !== null) return;
1175
1180
  child.kill("SIGTERM");
1176
- await new Promise((resolve3) => {
1181
+ await new Promise((resolve5) => {
1177
1182
  const t = setTimeout(() => {
1178
1183
  if (child.exitCode === null) child.kill("SIGKILL");
1179
- resolve3();
1184
+ resolve5();
1180
1185
  }, graceMs);
1181
1186
  child.once("exit", () => {
1182
1187
  clearTimeout(t);
1183
- resolve3();
1188
+ resolve5();
1184
1189
  });
1185
1190
  });
1186
1191
  }
@@ -1299,12 +1304,12 @@ var APPROVAL_POLICY = "never";
1299
1304
  var SANDBOX = "danger-full-access";
1300
1305
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1301
1306
  function withDeadline(p, ms, label) {
1302
- return new Promise((resolve3, reject) => {
1307
+ return new Promise((resolve5, reject) => {
1303
1308
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1304
1309
  p.then(
1305
1310
  (v) => {
1306
1311
  clearTimeout(t);
1307
- resolve3(v);
1312
+ resolve5(v);
1308
1313
  },
1309
1314
  (e) => {
1310
1315
  clearTimeout(t);
@@ -1344,11 +1349,11 @@ var CodexThread = class {
1344
1349
  if (self.model) params.model = self.model;
1345
1350
  if (self.effort) params.effort = self.effort;
1346
1351
  let startError;
1347
- const startFailed = new Promise((resolve3) => {
1352
+ const startFailed = new Promise((resolve5) => {
1348
1353
  self.client.request("turn/start", params).then(void 0, (err) => {
1349
1354
  startError = err instanceof Error ? err : new Error(String(err));
1350
1355
  log.fail("agent", startError, { phase: "turn/start" });
1351
- resolve3("start-failed");
1356
+ resolve5("start-failed");
1352
1357
  });
1353
1358
  });
1354
1359
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -2621,6 +2626,8 @@ var DM = {
2621
2626
  settings: "dm.settings",
2622
2627
  doctor: "dm.doctor",
2623
2628
  reconnect: "dm.reconnect",
2629
+ update: "dm.update",
2630
+ updateDo: "dm.update.do",
2624
2631
  rmConfirm: "dm.rmConfirm",
2625
2632
  rmDo: "dm.rmDo",
2626
2633
  rmCancel: "dm.rmCancel",
@@ -2647,12 +2654,91 @@ function buildDmMenuCard() {
2647
2654
  ]),
2648
2655
  actions([
2649
2656
  button("\u{1FA7A} \u8BCA\u65AD", { a: DM.doctor }),
2650
- 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 })
2651
2659
  ])
2652
2660
  ],
2653
2661
  { header: { title: "\u{1F916} Codex Bridge \u7BA1\u7406\u53F0", template: "blue" } }
2654
2662
  );
2655
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
+ }
2656
2742
  function buildNewProjectFormCard(opts = {}) {
2657
2743
  const elements = [];
2658
2744
  if (opts.error) elements.push(md(`\u274C **\u521B\u5EFA\u5931\u8D25**\uFF1A${opts.error}`));
@@ -2785,9 +2871,266 @@ function buildGroupSettingsCard(project) {
2785
2871
  );
2786
2872
  }
2787
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
+
2788
3131
  // src/project/registry.ts
2789
- import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2790
- 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";
2791
3134
  var FILE_VERSION2 = 1;
2792
3135
  async function read() {
2793
3136
  try {
@@ -2800,10 +3143,10 @@ async function read() {
2800
3143
  }
2801
3144
  }
2802
3145
  async function write(projects) {
2803
- await mkdir4(dirname4(paths.projectsFile), { recursive: true });
3146
+ await mkdir5(dirname6(paths.projectsFile), { recursive: true });
2804
3147
  const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
2805
3148
  const body = { version: FILE_VERSION2, projects };
2806
- await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
3149
+ await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
2807
3150
  `, "utf8");
2808
3151
  await rename4(tmp, paths.projectsFile);
2809
3152
  }
@@ -2844,14 +3187,14 @@ async function removeProject(name) {
2844
3187
  }
2845
3188
 
2846
3189
  // src/project/lifecycle.ts
2847
- import { mkdir as mkdir5 } from "fs/promises";
2848
- import { existsSync as existsSync4 } from "fs";
2849
- 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";
2850
3193
 
2851
3194
  // src/project/git-info.ts
2852
- import { execFile } from "child_process";
2853
- import { promisify } from "util";
2854
- var execFileAsync = promisify(execFile);
3195
+ import { execFile as execFile2 } from "child_process";
3196
+ import { promisify as promisify2 } from "util";
3197
+ var execFileAsync = promisify2(execFile2);
2855
3198
  async function currentBranch(cwd) {
2856
3199
  try {
2857
3200
  const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
@@ -2984,20 +3327,25 @@ async function createProject(channel, input2) {
2984
3327
  let cwd;
2985
3328
  let blank;
2986
3329
  if (input2.existingPath) {
2987
- cwd = isAbsolute(input2.existingPath) ? input2.existingPath : resolve(input2.existingPath);
2988
- 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}`);
2989
3332
  blank = false;
2990
3333
  } else {
2991
- cwd = join7(paths.projectsRootDir, name);
2992
- await mkdir5(cwd, { recursive: true });
3334
+ cwd = join9(paths.projectsRootDir, name);
3335
+ await mkdir6(cwd, { recursive: true });
2993
3336
  blank = true;
2994
3337
  }
2995
3338
  const res = await channel.rawClient.im.v1.chat.create({
2996
- params: { user_id_type: "open_id", set_bot_manager: true },
3339
+ params: { user_id_type: "open_id" },
2997
3340
  data: { name, user_id_list: [input2.ownerOpenId] }
2998
3341
  });
2999
3342
  const chatId = res.data?.chat_id;
3000
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" }));
3001
3349
  const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi" };
3002
3350
  await addProject(project);
3003
3351
  log.info("project", "create", { name, chatId, cwd, blank });
@@ -3017,8 +3365,8 @@ async function transferOwnership(channel, chatId, toOpenId) {
3017
3365
  }
3018
3366
 
3019
3367
  // src/bot/session-store.ts
3020
- import { mkdir as mkdir6, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "fs/promises";
3021
- 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";
3022
3370
  var FILE_VERSION3 = 1;
3023
3371
  async function read2() {
3024
3372
  try {
@@ -3031,10 +3379,10 @@ async function read2() {
3031
3379
  }
3032
3380
  }
3033
3381
  async function write2(sessions) {
3034
- await mkdir6(dirname5(paths.sessionsFile), { recursive: true });
3382
+ await mkdir7(dirname7(paths.sessionsFile), { recursive: true });
3035
3383
  const tmp = `${paths.sessionsFile}.tmp-${process.pid}`;
3036
3384
  const body = { version: FILE_VERSION3, sessions };
3037
- await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
3385
+ await writeFile6(tmp, `${JSON.stringify(body, null, 2)}
3038
3386
  `, "utf8");
3039
3387
  await rename5(tmp, paths.sessionsFile);
3040
3388
  }
@@ -3784,6 +4132,54 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3784
4132
  const conn = channel.getConnectionStatus?.()?.state ?? "unknown";
3785
4133
  await channel.send(evt.chatId, { markdown: `\u{1F504} \u957F\u8FDE\u63A5\u72B6\u6001\uFF1A**${conn}**
3786
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
+ })();
3787
4183
  }).on(DM.rmConfirm, async ({ evt, value }) => {
3788
4184
  const name = typeof value.n === "string" ? value.n : void 0;
3789
4185
  if (!dmAdmin(evt.operator?.openId) || !name) return;
@@ -4189,8 +4585,8 @@ async function startBridge(opts) {
4189
4585
  }
4190
4586
 
4191
4587
  // src/core/single-instance.ts
4192
- import { mkdirSync as mkdirSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
4193
- 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";
4194
4590
  var BridgeAlreadyRunningError = class extends Error {
4195
4591
  constructor(pid) {
4196
4592
  super(
@@ -4212,20 +4608,20 @@ function isAlive(pid) {
4212
4608
  function acquireSingleInstanceLock(appId) {
4213
4609
  const file = paths.processesFile;
4214
4610
  try {
4215
- const rec = JSON.parse(readFileSync(file, "utf8"));
4611
+ const rec = JSON.parse(readFileSync2(file, "utf8"));
4216
4612
  if (rec.pid && rec.pid !== process.pid && rec.appId === appId && isAlive(rec.pid)) {
4217
4613
  throw new BridgeAlreadyRunningError(rec.pid);
4218
4614
  }
4219
4615
  } catch (err) {
4220
4616
  if (err instanceof BridgeAlreadyRunningError) throw err;
4221
4617
  }
4222
- mkdirSync2(dirname6(file), { recursive: true });
4618
+ mkdirSync2(dirname8(file), { recursive: true });
4223
4619
  const record = { pid: process.pid, appId, startedAt: Date.now() };
4224
4620
  writeFileSync(file, `${JSON.stringify(record)}
4225
4621
  `, "utf8");
4226
4622
  const release = () => {
4227
4623
  try {
4228
- const rec = JSON.parse(readFileSync(file, "utf8"));
4624
+ const rec = JSON.parse(readFileSync2(file, "utf8"));
4229
4625
  if (rec.pid === process.pid) unlinkSync(file);
4230
4626
  } catch {
4231
4627
  }
@@ -4276,187 +4672,6 @@ async function runRun() {
4276
4672
  });
4277
4673
  }
4278
4674
 
4279
- // src/service/launchd.ts
4280
- import { spawn as spawn4, spawnSync } from "child_process";
4281
- import { existsSync as existsSync5 } from "fs";
4282
- import { appendFile, mkdir as mkdir7, rm as rm2, writeFile as writeFile6 } from "fs/promises";
4283
- import { homedir as homedir3, userInfo as userInfo2 } from "os";
4284
- import { dirname as dirname7, join as join8, resolve as resolve2 } from "path";
4285
- import { fileURLToPath } from "url";
4286
- var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
4287
- function launchAgentPlistPath() {
4288
- return join8(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
4289
- }
4290
- function serviceStdoutPath() {
4291
- return join8(paths.appDir, "service.log");
4292
- }
4293
- function serviceStderrPath() {
4294
- return join8(paths.appDir, "service.err.log");
4295
- }
4296
- function resolveCliBinPath() {
4297
- const distDir = dirname7(fileURLToPath(import.meta.url));
4298
- return resolve2(distDir, "..", "bin", "feishu-codex-bridge.mjs");
4299
- }
4300
- function escapeXml(value) {
4301
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4302
- }
4303
- function buildPlist() {
4304
- const nodePath = process.execPath;
4305
- const cliBinPath = resolveCliBinPath();
4306
- const pathEnv = process.env.PATH ?? "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
4307
- return `<?xml version="1.0" encoding="UTF-8"?>
4308
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4309
- <plist version="1.0">
4310
- <dict>
4311
- <key>Label</key>
4312
- <string>${LAUNCHD_LABEL}</string>
4313
- <key>ProgramArguments</key>
4314
- <array>
4315
- <string>${escapeXml(nodePath)}</string>
4316
- <string>${escapeXml(cliBinPath)}</string>
4317
- <string>run</string>
4318
- </array>
4319
- <key>RunAtLoad</key>
4320
- <true/>
4321
- <key>KeepAlive</key>
4322
- <true/>
4323
- <key>StandardOutPath</key>
4324
- <string>${escapeXml(serviceStdoutPath())}</string>
4325
- <key>StandardErrorPath</key>
4326
- <string>${escapeXml(serviceStderrPath())}</string>
4327
- <key>EnvironmentVariables</key>
4328
- <dict>
4329
- <key>PATH</key>
4330
- <string>${escapeXml(pathEnv)}</string>
4331
- </dict>
4332
- </dict>
4333
- </plist>
4334
- `;
4335
- }
4336
- async function installLaunchd() {
4337
- const plistPath = launchAgentPlistPath();
4338
- await mkdir7(dirname7(plistPath), { recursive: true });
4339
- await ensureLogFiles();
4340
- await writeFile6(plistPath, buildPlist(), "utf8");
4341
- if (isLoaded()) {
4342
- const bootout = runLaunchctl(["bootout", serviceTarget()]);
4343
- if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
4344
- await waitUntilUnloaded();
4345
- }
4346
- const bootstrap = runLaunchctl(["bootstrap", userTarget(), plistPath]);
4347
- if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
4348
- return statusLaunchd();
4349
- }
4350
- async function uninstallLaunchd() {
4351
- if (isLoaded()) {
4352
- const bootout = runLaunchctl(["bootout", serviceTarget()]);
4353
- if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
4354
- await waitUntilUnloaded();
4355
- }
4356
- await rm2(launchAgentPlistPath(), { force: true });
4357
- }
4358
- async function restartLaunchd() {
4359
- if (!existsSync5(launchAgentPlistPath())) {
4360
- throw new Error(`launchd service \u672A\u5B89\u88C5\uFF1A${launchAgentPlistPath()}`);
4361
- }
4362
- if (isLoaded()) {
4363
- const bootout = runLaunchctl(["bootout", serviceTarget()]);
4364
- if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
4365
- await waitUntilUnloaded();
4366
- }
4367
- const bootstrap = runLaunchctl(["bootstrap", userTarget(), launchAgentPlistPath()]);
4368
- if (!bootstrap.ok) throw launchctlError("launchctl bootstrap", bootstrap);
4369
- return statusLaunchd();
4370
- }
4371
- function statusLaunchd() {
4372
- const result = runLaunchctl(["print", serviceTarget()]);
4373
- const raw = result.stdout || result.stderr;
4374
- const parsed = parseLaunchdStatus(raw);
4375
- return {
4376
- installed: existsSync5(launchAgentPlistPath()),
4377
- loaded: result.ok,
4378
- plistPath: launchAgentPlistPath(),
4379
- stdoutPath: serviceStdoutPath(),
4380
- stderrPath: serviceStderrPath(),
4381
- pid: parsed.pid,
4382
- lastExit: parsed.lastExit,
4383
- raw
4384
- };
4385
- }
4386
- async function tailLaunchdLogs(follow) {
4387
- await ensureLogFiles();
4388
- const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
4389
- await new Promise((resolvePromise, reject) => {
4390
- const child = spawn4("tail", args, { stdio: "inherit" });
4391
- child.on("error", reject);
4392
- child.on("close", (code) => {
4393
- if (code === 0 || follow && code === null) {
4394
- resolvePromise();
4395
- return;
4396
- }
4397
- reject(new Error(`tail \u9000\u51FA\u7801 ${code ?? "unknown"}`));
4398
- });
4399
- });
4400
- }
4401
- function parseLaunchdStatus(text) {
4402
- return {
4403
- pid: text.match(/\bpid\s*=\s*(\d+)/)?.[1],
4404
- lastExit: text.match(/last exit code\s*=\s*(-?\d+)/i)?.[1]
4405
- };
4406
- }
4407
- function isLoaded() {
4408
- const result = spawnSync("launchctl", ["print", serviceTarget()], {
4409
- stdio: ["ignore", "ignore", "ignore"]
4410
- });
4411
- return result.status === 0;
4412
- }
4413
- async function waitUntilUnloaded(timeoutMs = 5e3) {
4414
- const deadline = Date.now() + timeoutMs;
4415
- while (Date.now() < deadline) {
4416
- if (!isLoaded()) return;
4417
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
4418
- }
4419
- throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
4420
- }
4421
- async function ensureLogFiles() {
4422
- await mkdir7(paths.appDir, { recursive: true });
4423
- await appendFile(serviceStdoutPath(), "");
4424
- await appendFile(serviceStderrPath(), "");
4425
- }
4426
- function userTarget() {
4427
- return `gui/${userInfo2().uid}`;
4428
- }
4429
- function serviceTarget() {
4430
- return `${userTarget()}/${LAUNCHD_LABEL}`;
4431
- }
4432
- function runLaunchctl(args) {
4433
- const result = spawnSync("launchctl", args, { encoding: "utf8" });
4434
- return {
4435
- ok: result.status === 0,
4436
- status: result.status,
4437
- stdout: result.stdout ?? "",
4438
- stderr: result.stderr ?? ""
4439
- };
4440
- }
4441
- function launchctlError(command, result) {
4442
- const output = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
4443
- return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
4444
- }
4445
-
4446
- // src/service/adapter.ts
4447
- function getServiceAdapter() {
4448
- if (process.platform !== "darwin") {
4449
- throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
4450
- }
4451
- return {
4452
- install: installLaunchd,
4453
- uninstall: uninstallLaunchd,
4454
- status: async () => statusLaunchd(),
4455
- restart: restartLaunchd,
4456
- logs: tailLaunchdLogs
4457
- };
4458
- }
4459
-
4460
4675
  // src/cli/commands/daemon.ts
4461
4676
  async function runStart() {
4462
4677
  const ready = await ensureOnboarded({ allowCreate: true });
@@ -4502,6 +4717,54 @@ function printStatus(status) {
4502
4717
  }
4503
4718
  }
4504
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
+
4505
4768
  // src/cli/commands/bot.ts
4506
4769
  import { rm as rm3 } from "fs/promises";
4507
4770
  async function runBotInit(name) {
@@ -4615,21 +4878,29 @@ async function secretsRemove(id) {
4615
4878
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
4616
4879
  }
4617
4880
  function readStdin() {
4618
- return new Promise((resolve3) => {
4881
+ return new Promise((resolve5) => {
4619
4882
  let data = "";
4620
4883
  if (process.stdin.isTTY) {
4621
- resolve3("");
4884
+ resolve5("");
4622
4885
  return;
4623
4886
  }
4624
4887
  process.stdin.setEncoding("utf8");
4625
4888
  process.stdin.on("data", (c) => data += c);
4626
- process.stdin.on("end", () => resolve3(data));
4889
+ process.stdin.on("end", () => resolve5(data));
4627
4890
  });
4628
4891
  }
4629
4892
 
4630
4893
  // src/cli/index.ts
4631
4894
  var program = new Command();
4632
- program.name("feishu-codex-bridge").description("\u628A\u98DE\u4E66/Lark \u6865\u63A5\u5230\u672C\u673A Codex\uFF08\u9879\u76EE=\u7FA4, \u8BDD\u9898=\u4F1A\u8BDD\uFF09").version("0.0.1");
4895
+ function readVersion() {
4896
+ try {
4897
+ const pkgPath = resolve4(dirname9(fileURLToPath3(import.meta.url)), "..", "package.json");
4898
+ return JSON.parse(readFileSync3(pkgPath, "utf8")).version ?? "0.0.0";
4899
+ } catch {
4900
+ return "0.0.0";
4901
+ }
4902
+ }
4903
+ program.name("feishu-codex-bridge").description("\u628A\u98DE\u4E66/Lark \u6865\u63A5\u5230\u672C\u673A Codex\uFF08\u9879\u76EE=\u7FA4, \u8BDD\u9898=\u4F1A\u8BDD\uFF09").version(readVersion());
4633
4904
  program.command("run").description("\u524D\u53F0\u542F\u52A8 bot\uFF08\u6CA1\u914D\u7F6E\u5219\u5148\u626B\u7801 init\uFF1BCtrl+C \u4F18\u96C5\u9000\u51FA\uFF09").action(async () => {
4634
4905
  await runRun();
4635
4906
  });
@@ -4648,6 +4919,9 @@ program.command("status").description("\u540E\u53F0 daemon \u72B6\u6001\uFF08pid
4648
4919
  program.command("logs").description("\u67E5\u770B\u540E\u53F0 daemon \u65E5\u5FD7").option("-f, --follow", "\u6301\u7EED\u8DDF\u968F\u65E5\u5FD7").action(async (options) => {
4649
4920
  await runLogs(Boolean(options.follow));
4650
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
+ });
4651
4925
  var bot = program.command("bot").description("\u98DE\u4E66\u673A\u5668\u4EBA\u7BA1\u7406\uFF08\u591A\u673A\u5668\u4EBA\uFF09");
4652
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) => {
4653
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.1",
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": {
@@ -24,7 +24,8 @@
24
24
  "typecheck": "tsc --noEmit",
25
25
  "test": "vitest run",
26
26
  "start": "node bin/feishu-codex-bridge.mjs run",
27
- "prepare": "npm run build"
27
+ "prepare": "npm run build",
28
+ "release": "bash scripts/release.sh"
28
29
  },
29
30
  "dependencies": {
30
31
  "@larksuiteoapi/node-sdk": "^1.65.0",