@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.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/dist/cli.js +41 -26
  3. 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
- - **可常驻**:macOS 下可注册成 launchd 后台服务,开机自启。
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 # 装 launchd 并启动:开机自启、崩溃自动拉起、关终端照跑
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
- > ⚠️ **后台 daemon 必须全局安装(`npm i -g`),不要用 npx**:launchd plist 里硬编码了 CLI 路径,而 npx 的临时缓存(`~/.npm/_npx/...`)会被清理,缓存一没服务就起不来。前台 `run` npx 没问题(单次进程)。
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 启动(装 launchd 开机自启;阻塞到授权完成)
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 = spawn(pc.command, pc.args ?? [], { env, stdio: ["pipe", "pipe", "pipe"] });
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 as spawn2 } from "child_process";
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 = spawn2(cmd, args, { stdio: "ignore", detached: true });
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 spawn3, spawnSync } from "child_process";
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 = spawn3("tail", args, { stdio: "inherit" });
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 spawn4, spawnSync as spawnSync2 } from "child_process";
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 = spawn4(process.execPath, [resolveCliBinPath(), "run"], {
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 execFileP = promisify(execFile);
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
- try {
4128
- const { stdout } = await execFileP(NPM, ["view", packageName(), "version"], { timeout: 2e4 });
4129
- const v = stdout.trim();
4130
- return /^\d+\.\d+\.\d+/.test(v) ? v : null;
4131
- } catch {
4132
- return null;
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 = spawn5(NPM, ["install", "-g", target], {
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 as execFile2 } from "child_process";
4167
- import { promisify as promisify2 } from "util";
4168
- var execFileAsync = promisify2(execFile2);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.2.3-win",
3
+ "version": "0.3.0",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {