@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.
- package/README.md +11 -2
- package/dist/cli.js +489 -226
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# feishu-codex-bridge
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
|
|
4
|
+
[](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
|
|
5
|
+
[](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
|
|
6
|
+
[](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
|
|
3
|
-
import { dirname as
|
|
4
|
-
import { fileURLToPath as
|
|
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((
|
|
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
|
|
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((
|
|
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((
|
|
1155
|
-
this.pending.set(id, { resolve:
|
|
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((
|
|
1181
|
+
await new Promise((resolve5) => {
|
|
1180
1182
|
const t = setTimeout(() => {
|
|
1181
1183
|
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1182
|
-
|
|
1184
|
+
resolve5();
|
|
1183
1185
|
}, graceMs);
|
|
1184
1186
|
child.once("exit", () => {
|
|
1185
1187
|
clearTimeout(t);
|
|
1186
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
2793
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
2851
|
-
import { existsSync as
|
|
2852
|
-
import { isAbsolute, join as
|
|
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 =
|
|
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 :
|
|
2991
|
-
if (!
|
|
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 =
|
|
2995
|
-
await
|
|
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"
|
|
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
|
|
3024
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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((
|
|
4881
|
+
return new Promise((resolve5) => {
|
|
4622
4882
|
let data = "";
|
|
4623
4883
|
if (process.stdin.isTTY) {
|
|
4624
|
-
|
|
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", () =>
|
|
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 =
|
|
4638
|
-
return JSON.parse(
|
|
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);
|