@modelzen/feishu-codex-bridge 0.2.3-win → 0.3.0
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 +8 -5
- package/dist/cli.js +41 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
- **私聊控制台**:私聊机器人弹交互菜单 —— 新建项目、项目列表、设置、诊断、重连。
|
|
34
34
|
- **稳定隔离**:每会话独立 app-server 进程;卡死有 watchdog(默认 120s)→ 终止 → 回收,异常不波及其他群。
|
|
35
35
|
- **本地加密密钥库**:飞书应用密钥用 AES-256-GCM 存在 `~/.feishu-codex-bridge/`,不入仓库、不进环境变量。
|
|
36
|
-
-
|
|
36
|
+
- **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
| 依赖 | 说明 | 获取方式 |
|
|
43
43
|
|------|------|----------|
|
|
44
|
+
| **操作系统** | **macOS / Windows** 均支持;Linux·WSL 为 best-effort(已实现 systemd,未广泛实测) | — |
|
|
44
45
|
| **Node.js ≥ 20** | 运行时 | <https://nodejs.org> 或 `nvm install 20` |
|
|
45
46
|
| **Codex CLI** | 后端,bridge 会 spawn `codex app-server` | `npm i -g @openai/codex`,或装 Codex.app,或用 `CODEX_BIN` 指向已有二进制 |
|
|
46
47
|
| **Codex 已登录** | app-server 需要 `~/.codex/auth.json` | `codex login` |
|
|
@@ -78,7 +79,7 @@ feishu-codex-bridge run
|
|
|
78
79
|
### 3. 后台 daemon(`start` —— 日常这么跑)
|
|
79
80
|
|
|
80
81
|
```bash
|
|
81
|
-
feishu-codex-bridge start #
|
|
82
|
+
feishu-codex-bridge start # 装系统后台服务并启动:开机/登录自启、崩溃自动拉起、关终端照跑
|
|
82
83
|
feishu-codex-bridge status # 状态 / pid / 日志路径 / 上次退出码
|
|
83
84
|
feishu-codex-bridge logs -f # 跟踪日志
|
|
84
85
|
feishu-codex-bridge restart # 重启
|
|
@@ -90,7 +91,9 @@ feishu-codex-bridge update # 更新到最新版(npm i -g)并自动重启
|
|
|
90
91
|
|
|
91
92
|
`start` 会**先在当前终端完成 init**(没配置则扫码),并**阻塞到授权完成**——权限全部开通、且你确认已订阅事件/发布版本——才真正装服务,绝不会装一个收不到消息的空壳。daemon 体跑的就是 `run`。
|
|
92
93
|
|
|
93
|
-
>
|
|
94
|
+
> 🖥 **各平台后台机制**:macOS = launchd 用户服务;**Windows = 登录自启(写 `HKCU\…\Run`,隐藏启动,全程免管理员)**;Linux·WSL = systemd 用户单元(`systemctl --user`,需要 `loginctl enable-linger` 才能登出后续跑;WSL 还需在 `/etc/wsl.conf` 开 `[boot] systemd=true`,否则用前台 `run`)。三者命令一致(`start`/`status`/`stop`/`restart`/`logs`),状态/日志路径统一。
|
|
95
|
+
|
|
96
|
+
> ⚠️ **后台服务必须全局安装(`npm i -g`),不要用 npx**:服务里硬编码了 CLI 路径,而 npx 的临时缓存(`~/.npm/_npx/...`)会被清理,缓存一没服务就起不来。前台 `run` 用 npx 没问题(单次进程)。
|
|
94
97
|
|
|
95
98
|
### 4. 多飞书机器人(可选)
|
|
96
99
|
|
|
@@ -198,7 +201,7 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
|
|
|
198
201
|
|
|
199
202
|
```
|
|
200
203
|
feishu-codex-bridge run 前台启动(没配置先扫码 init;Ctrl+C 优雅退出)
|
|
201
|
-
feishu-codex-bridge start 后台 daemon
|
|
204
|
+
feishu-codex-bridge start 后台 daemon 启动(装系统后台服务、开机/登录自启;阻塞到授权完成)
|
|
202
205
|
feishu-codex-bridge stop|restart|status|logs 后台 daemon 生命周期
|
|
203
206
|
feishu-codex-bridge update 更新到最新版并自动重启 daemon(--check 只查不装)
|
|
204
207
|
feishu-codex-bridge bot init|list|use|rm 多飞书机器人:注册 / 列表 / 切当前 / 移除
|
|
@@ -229,7 +232,7 @@ src/
|
|
|
229
232
|
config/ 加密密钥库、密钥解析、配置存储、多机器人注册表、scope 清单、路径
|
|
230
233
|
core/ watchdog、单实例锁、日志
|
|
231
234
|
cli/ commander 命令(run / start / stop / restart / status / logs / update / bot / doctor / secrets)
|
|
232
|
-
service/ launchd
|
|
235
|
+
service/ 后台服务适配器(launchd / Windows 登录自启 / systemd)+ 跨平台 spawn
|
|
233
236
|
```
|
|
234
237
|
|
|
235
238
|
架构与实现细节见 [`docs/design/feishu-codex-bridge-design.md`](docs/design/feishu-codex-bridge-design.md) 与 [`docs/design/implementation-plan.md`](docs/design/implementation-plan.md)。
|
package/dist/cli.js
CHANGED
|
@@ -274,10 +274,10 @@ import { extname, join as join3 } from "path";
|
|
|
274
274
|
// src/platform/spawn.ts
|
|
275
275
|
import crossSpawn from "cross-spawn";
|
|
276
276
|
function spawnProcess(command, args = [], options = {}) {
|
|
277
|
-
return crossSpawn(command, [...args], options);
|
|
277
|
+
return crossSpawn(command, [...args], { windowsHide: true, ...options });
|
|
278
278
|
}
|
|
279
279
|
function spawnProcessSync(command, args = [], options = {}) {
|
|
280
|
-
return crossSpawn.sync(command, [...args], options);
|
|
280
|
+
return crossSpawn.sync(command, [...args], { windowsHide: true, ...options });
|
|
281
281
|
}
|
|
282
282
|
function mergeProcessEnv(base = process.env, overrides = {}) {
|
|
283
283
|
const out = { ...base };
|
|
@@ -491,7 +491,6 @@ async function listSecretIds() {
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
// src/config/secret-resolver.ts
|
|
494
|
-
import { spawn } from "child_process";
|
|
495
494
|
import { readFile as readFile4 } from "fs/promises";
|
|
496
495
|
import { join as join5 } from "path";
|
|
497
496
|
var ENV_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
|
@@ -577,7 +576,10 @@ async function spawnExecProvider(pc, ref) {
|
|
|
577
576
|
if (v) env[k] = v;
|
|
578
577
|
}
|
|
579
578
|
if (pc.env) Object.assign(env, pc.env);
|
|
580
|
-
const child =
|
|
579
|
+
const child = spawnProcess(pc.command, pc.args ?? [], {
|
|
580
|
+
env,
|
|
581
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
582
|
+
});
|
|
581
583
|
let stdout = "", stderr = "", truncated = false, settled = false;
|
|
582
584
|
const timer = setTimeout(() => {
|
|
583
585
|
if (settled) return;
|
|
@@ -812,7 +814,7 @@ async function fetchGrantedScopes(base, token) {
|
|
|
812
814
|
}
|
|
813
815
|
|
|
814
816
|
// src/utils/open-url.ts
|
|
815
|
-
import { spawn
|
|
817
|
+
import { spawn } from "child_process";
|
|
816
818
|
import { platform } from "os";
|
|
817
819
|
function openUrl(url) {
|
|
818
820
|
if (!process.stdout.isTTY) return false;
|
|
@@ -832,7 +834,7 @@ function openUrl(url) {
|
|
|
832
834
|
args = [url];
|
|
833
835
|
}
|
|
834
836
|
try {
|
|
835
|
-
const child =
|
|
837
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
836
838
|
child.on("error", () => {
|
|
837
839
|
});
|
|
838
840
|
child.unref();
|
|
@@ -3556,14 +3558,12 @@ function buildGroupSettingsCard(project) {
|
|
|
3556
3558
|
}
|
|
3557
3559
|
|
|
3558
3560
|
// src/service/update.ts
|
|
3559
|
-
import { execFile, spawn as spawn5 } from "child_process";
|
|
3560
3561
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
3561
3562
|
import { dirname as dirname9, join as join11, resolve as resolve5 } from "path";
|
|
3562
3563
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3563
|
-
import { promisify } from "util";
|
|
3564
3564
|
|
|
3565
3565
|
// src/service/launchd.ts
|
|
3566
|
-
import { spawn as
|
|
3566
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
3567
3567
|
import { existsSync as existsSync4 } from "fs";
|
|
3568
3568
|
import { mkdir as mkdir6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
3569
3569
|
import { homedir as homedir3, userInfo as userInfo2 } from "os";
|
|
@@ -3741,7 +3741,7 @@ async function tailLaunchdLogs(follow) {
|
|
|
3741
3741
|
await ensureLogFiles();
|
|
3742
3742
|
const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
|
|
3743
3743
|
await new Promise((resolvePromise, reject) => {
|
|
3744
|
-
const child =
|
|
3744
|
+
const child = spawn2("tail", args, { stdio: "inherit" });
|
|
3745
3745
|
child.on("error", reject);
|
|
3746
3746
|
child.on("close", (code) => {
|
|
3747
3747
|
if (code === 0 || follow && code === null) {
|
|
@@ -3793,7 +3793,7 @@ function launchctlError(command, result) {
|
|
|
3793
3793
|
}
|
|
3794
3794
|
|
|
3795
3795
|
// src/service/win-startup.ts
|
|
3796
|
-
import { spawn as
|
|
3796
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
|
|
3797
3797
|
import { openSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
3798
3798
|
import { mkdir as mkdir7, writeFile as writeFile6 } from "fs/promises";
|
|
3799
3799
|
import { join as join9 } from "path";
|
|
@@ -3829,7 +3829,7 @@ function buildLauncherVbs() {
|
|
|
3829
3829
|
function startNow() {
|
|
3830
3830
|
const out = openSync(serviceStdoutPath(), "a");
|
|
3831
3831
|
const err = openSync(serviceStderrPath(), "a");
|
|
3832
|
-
const child =
|
|
3832
|
+
const child = spawn3(process.execPath, [resolveCliBinPath(), "run"], {
|
|
3833
3833
|
detached: true,
|
|
3834
3834
|
windowsHide: true,
|
|
3835
3835
|
stdio: ["ignore", out, err],
|
|
@@ -4093,8 +4093,7 @@ function isServiceRunning() {
|
|
|
4093
4093
|
}
|
|
4094
4094
|
|
|
4095
4095
|
// src/service/update.ts
|
|
4096
|
-
var
|
|
4097
|
-
var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
4096
|
+
var NPM = "npm";
|
|
4098
4097
|
function pkgRoot() {
|
|
4099
4098
|
return resolve5(dirname9(fileURLToPath4(import.meta.url)), "..");
|
|
4100
4099
|
}
|
|
@@ -4124,18 +4123,31 @@ function isNewer(a, b) {
|
|
|
4124
4123
|
return false;
|
|
4125
4124
|
}
|
|
4126
4125
|
async function latestVersion() {
|
|
4127
|
-
|
|
4128
|
-
const
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4126
|
+
const v = await new Promise((resolveP) => {
|
|
4127
|
+
const child = spawnProcess(NPM, ["view", packageName(), "version"], {
|
|
4128
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4129
|
+
});
|
|
4130
|
+
let out = "";
|
|
4131
|
+
const timer = setTimeout(() => {
|
|
4132
|
+
child.kill();
|
|
4133
|
+
resolveP(null);
|
|
4134
|
+
}, 2e4);
|
|
4135
|
+
child.stdout?.on("data", (d) => out += d);
|
|
4136
|
+
child.on("error", () => {
|
|
4137
|
+
clearTimeout(timer);
|
|
4138
|
+
resolveP(null);
|
|
4139
|
+
});
|
|
4140
|
+
child.on("close", (code) => {
|
|
4141
|
+
clearTimeout(timer);
|
|
4142
|
+
resolveP(code === 0 ? out.trim() : null);
|
|
4143
|
+
});
|
|
4144
|
+
});
|
|
4145
|
+
return v && /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
4134
4146
|
}
|
|
4135
4147
|
async function installLatest(opts = {}) {
|
|
4136
4148
|
const target = `${packageName()}@latest`;
|
|
4137
4149
|
return await new Promise((resolveP) => {
|
|
4138
|
-
const child =
|
|
4150
|
+
const child = spawnProcess(NPM, ["install", "-g", target], {
|
|
4139
4151
|
stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
|
|
4140
4152
|
});
|
|
4141
4153
|
let out = "";
|
|
@@ -4163,14 +4175,17 @@ import { existsSync as existsSync7 } from "fs";
|
|
|
4163
4175
|
import { isAbsolute as isAbsolute2, join as join12, resolve as resolve6 } from "path";
|
|
4164
4176
|
|
|
4165
4177
|
// src/project/git-info.ts
|
|
4166
|
-
import { execFile
|
|
4167
|
-
import { promisify
|
|
4168
|
-
var execFileAsync =
|
|
4178
|
+
import { execFile } from "child_process";
|
|
4179
|
+
import { promisify } from "util";
|
|
4180
|
+
var execFileAsync = promisify(execFile);
|
|
4169
4181
|
async function currentBranch(cwd) {
|
|
4170
4182
|
try {
|
|
4171
4183
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
4172
4184
|
cwd,
|
|
4173
|
-
timeout: 3e3
|
|
4185
|
+
timeout: 3e3,
|
|
4186
|
+
// Hide the console window: this runs on every inbound message, and the
|
|
4187
|
+
// Windows background service has no console of its own to inherit.
|
|
4188
|
+
windowsHide: true
|
|
4174
4189
|
});
|
|
4175
4190
|
const b = stdout.trim();
|
|
4176
4191
|
return b && b !== "HEAD" ? b : null;
|