@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.
- package/README.md +11 -2
- package/dist/cli.js +496 -222
- package/package.json +3 -2
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,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((
|
|
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
|
|
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((
|
|
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((
|
|
1152
|
-
this.pending.set(id, { resolve:
|
|
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((
|
|
1181
|
+
await new Promise((resolve5) => {
|
|
1177
1182
|
const t = setTimeout(() => {
|
|
1178
1183
|
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1179
|
-
|
|
1184
|
+
resolve5();
|
|
1180
1185
|
}, graceMs);
|
|
1181
1186
|
child.once("exit", () => {
|
|
1182
1187
|
clearTimeout(t);
|
|
1183
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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, "&").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
|
+
|
|
2788
3131
|
// src/project/registry.ts
|
|
2789
|
-
import { mkdir as
|
|
2790
|
-
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";
|
|
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
|
|
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
|
|
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
|
|
2848
|
-
import { existsSync as
|
|
2849
|
-
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";
|
|
2850
3193
|
|
|
2851
3194
|
// src/project/git-info.ts
|
|
2852
|
-
import { execFile } from "child_process";
|
|
2853
|
-
import { promisify } from "util";
|
|
2854
|
-
var execFileAsync =
|
|
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 :
|
|
2988
|
-
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}`);
|
|
2989
3332
|
blank = false;
|
|
2990
3333
|
} else {
|
|
2991
|
-
cwd =
|
|
2992
|
-
await
|
|
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"
|
|
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
|
|
3021
|
-
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";
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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((
|
|
4881
|
+
return new Promise((resolve5) => {
|
|
4619
4882
|
let data = "";
|
|
4620
4883
|
if (process.stdin.isTTY) {
|
|
4621
|
-
|
|
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", () =>
|
|
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
|
-
|
|
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.
|
|
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",
|