@modelzen/feishu-codex-bridge 0.1.0 → 0.1.2

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 +20 -21
  2. package/dist/cli.js +96 -23
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -32,33 +32,17 @@
32
32
 
33
33
  ---
34
34
 
35
- ## ⚠️ 安全须知(务必先读)
36
-
37
- 本机器人调用 Codex 时固定使用 **`approvalPolicy: "never"` + `sandbox: "danger-full-access"`** —— 即 **无任何人工审批、对磁盘完全访问**。这意味着:
38
-
39
- > **任何能在项目群里给机器人发消息的人,都能在你这台机器上、以你的身份、在该项目目录里执行任意命令(读写文件、联网、运行脚本)。**
40
-
41
- 因此:
42
-
43
- - 只把**你信任的人**拉进项目群;
44
- - 在**你自己掌控的机器/账号**上运行,最好是隔离的开发机或容器;
45
- - 绑定的项目目录里不要放你不愿被读写的敏感数据;
46
- - 它不是多租户托管服务,是给你(和你信任的小团队)自用的桥。
47
-
48
- ---
49
-
50
35
  ## 📦 前置条件
51
36
 
52
37
  | 依赖 | 说明 | 获取方式 |
53
38
  |------|------|----------|
54
39
  | **Node.js ≥ 20** | 运行时 | <https://nodejs.org> 或 `nvm install 20` |
55
- | **Codex CLI** | 后端,bridge 会 spawn `codex app-server` | `npm i -g @openai/codex`,或安装 Codex.app,或用环境变量 `CODEX_BIN` 指向已有二进制 |
56
- | **Codex 已登录** | app-server 需要 `~/.codex/auth.json` | 运行 `codex login` |
57
- | **飞书 / Lark 账号** | 且该租户允许「扫码创建应用」(个人/开发者租户一般可以;部分企业租户由管理员限制) | 首次 `run` 时扫码即可创建 |
58
- | **lark-cli**(可选,但用「文档评论回复」**强烈建议装**) | 仅「文档评论回复」用到:要回答「总结本文 / 这段写得对吗」这类**需要读文档正文**的问题时,Codex 靠 `lark-cli` 去读文档(回复本身由桥用 SDK 以机器人身份发,不依赖它)。不装也能跑,但机器人只能凭评论里给到的上下文作答,读不到正文。 | 安装并 `lark-cli auth login` 登录(与本机 lark-* 技能同款的那个 lark-cli),确保 Codex 能在 PATH 上直接调用 |
40
+ | **Codex CLI** | 后端,bridge 会 spawn `codex app-server` | `npm i -g @openai/codex`,或装 Codex.app,或用 `CODEX_BIN` 指向已有二进制 |
41
+ | **Codex 已登录** | app-server 需要 `~/.codex/auth.json` | `codex login` |
42
+ | **飞书 / Lark 账号** | 租户需允许「扫码创建应用」(个人/开发者租户一般可以) | 首次 `run` 时扫码创建 |
43
+ | **lark-cli**(可选) | 仅「文档评论回复」需读文档正文时用到;不装也能跑,只是读不到正文 | `lark-cli auth login`,确保在 PATH |
59
44
 
60
- > 机器人**收发消息、回卡片、发评论回复**全部走 `@larksuiteoapi/node-sdk` 长连接,核心功能**不依赖** `lark-cli`。`lark-cli` 只是「文档评论回复」里让 Codex **读文档正文**的途径——见上表。
61
- > ⚠️ `lark-cli` 以**你的用户身份**登录,所以 Codex 只应用它来**读**;prompt 已明确禁止 Codex 用它发评论(否则评论会署成你本人,而不是机器人)。
45
+ > 收发消息、回卡片、发评论回复均走 `@larksuiteoapi/node-sdk` 长连接,**不依赖** `lark-cli`。⚠️ `lark-cli` 以**你的身份**登录,仅供 Codex **读**文档;prompt 已禁止用它发评论(否则评论会署你本人)。
62
46
 
63
47
  ---
64
48
 
@@ -239,6 +223,21 @@ src/
239
223
 
240
224
  ---
241
225
 
226
+ ## ⚠️ 安全须知
227
+
228
+ 本机器人调用 Codex 时固定使用 **`approvalPolicy: "never"` + `sandbox: "danger-full-access"`** —— 即 **无任何人工审批、对磁盘完全访问**。这意味着:
229
+
230
+ > **任何能在项目群里给机器人发消息的人,都能在你这台机器上、以你的身份、在该项目目录里执行任意命令(读写文件、联网、运行脚本)。**
231
+
232
+ 因此:
233
+
234
+ - 只把**你信任的人**拉进项目群;
235
+ - 在**你自己掌控的机器/账号**上运行,最好是隔离的开发机或容器;
236
+ - 绑定的项目目录里不要放你不愿被读写的敏感数据;
237
+ - 它不是多租户托管服务,是给你(和你信任的小团队)自用的桥。
238
+
239
+ ---
240
+
242
241
  ## ❓ 故障排查
243
242
 
244
243
  | 现象 | 排查 |
package/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // src/cli/index.ts
2
+ import { readFileSync as readFileSync2 } from "fs";
3
+ import { dirname as dirname8, resolve as resolve3 } from "path";
4
+ import { fileURLToPath as fileURLToPath2 } from "url";
2
5
  import { Command } from "commander";
3
6
 
4
7
  // src/cli/commands/doctor.ts
@@ -525,7 +528,7 @@ async function spawnExecProvider(pc, ref) {
525
528
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
526
529
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
527
530
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
528
- return new Promise((resolve3, reject) => {
531
+ return new Promise((resolve4, reject) => {
529
532
  const env = {};
530
533
  if (pc.passEnv) for (const k of pc.passEnv) {
531
534
  const v = process.env[k];
@@ -570,7 +573,7 @@ async function spawnExecProvider(pc, ref) {
570
573
  try {
571
574
  const parsed = JSON.parse(stdout);
572
575
  const value = parsed.values?.[ref.id];
573
- if (typeof value === "string") return resolve3(value);
576
+ if (typeof value === "string") return resolve4(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) {
@@ -1097,7 +1100,7 @@ var AsyncQueue = class {
1097
1100
  continue;
1098
1101
  }
1099
1102
  if (this.closed) return;
1100
- const next = await new Promise((resolve3) => this.waiters.push(resolve3));
1103
+ const next = await new Promise((resolve4) => this.waiters.push(resolve4));
1101
1104
  if (next.done) return;
1102
1105
  yield next.value;
1103
1106
  }
@@ -1148,8 +1151,8 @@ var AppServerClient = class {
1148
1151
  const id = ++this.nextId;
1149
1152
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1150
1153
  `;
1151
- return new Promise((resolve3, reject) => {
1152
- this.pending.set(id, { resolve: resolve3, reject });
1154
+ return new Promise((resolve4, reject) => {
1155
+ this.pending.set(id, { resolve: resolve4, reject });
1153
1156
  this.child.stdin.write(payload, (err) => {
1154
1157
  if (err) {
1155
1158
  this.pending.delete(id);
@@ -1173,14 +1176,14 @@ var AppServerClient = class {
1173
1176
  const child = this.child;
1174
1177
  if (!child || child.exitCode !== null) return;
1175
1178
  child.kill("SIGTERM");
1176
- await new Promise((resolve3) => {
1179
+ await new Promise((resolve4) => {
1177
1180
  const t = setTimeout(() => {
1178
1181
  if (child.exitCode === null) child.kill("SIGKILL");
1179
- resolve3();
1182
+ resolve4();
1180
1183
  }, graceMs);
1181
1184
  child.once("exit", () => {
1182
1185
  clearTimeout(t);
1183
- resolve3();
1186
+ resolve4();
1184
1187
  });
1185
1188
  });
1186
1189
  }
@@ -1299,12 +1302,12 @@ var APPROVAL_POLICY = "never";
1299
1302
  var SANDBOX = "danger-full-access";
1300
1303
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1301
1304
  function withDeadline(p, ms, label) {
1302
- return new Promise((resolve3, reject) => {
1305
+ return new Promise((resolve4, reject) => {
1303
1306
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1304
1307
  p.then(
1305
1308
  (v) => {
1306
1309
  clearTimeout(t);
1307
- resolve3(v);
1310
+ resolve4(v);
1308
1311
  },
1309
1312
  (e) => {
1310
1313
  clearTimeout(t);
@@ -1344,11 +1347,11 @@ var CodexThread = class {
1344
1347
  if (self.model) params.model = self.model;
1345
1348
  if (self.effort) params.effort = self.effort;
1346
1349
  let startError;
1347
- const startFailed = new Promise((resolve3) => {
1350
+ const startFailed = new Promise((resolve4) => {
1348
1351
  self.client.request("turn/start", params).then(void 0, (err) => {
1349
1352
  startError = err instanceof Error ? err : new Error(String(err));
1350
1353
  log.fail("agent", startError, { phase: "turn/start" });
1351
- resolve3("start-failed");
1354
+ resolve4("start-failed");
1352
1355
  });
1353
1356
  });
1354
1357
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -2358,9 +2361,14 @@ var RC = {
2358
2361
  };
2359
2362
  var REASONING_MAX = 1500;
2360
2363
  var COLLAPSE_TOOL_THRESHOLD = 3;
2364
+ var PROCESS_BODY_BUDGET = 22e3;
2361
2365
  function buildRunCard(rc) {
2362
2366
  const state = rc.rs;
2363
2367
  const running = state.terminal === "running";
2368
+ const elements = running ? renderRunning(state, rc) : renderTerminal(state, rc);
2369
+ return card(elements, { streaming: running, summary: summaryText(state) });
2370
+ }
2371
+ function renderRunning(state, rc) {
2364
2372
  const elements = [];
2365
2373
  const reasoning = reasoningContent(state);
2366
2374
  if (reasoning) elements.push(reasoningPanel(reasoning, state.reasoningActive));
@@ -2369,23 +2377,79 @@ function buildRunCard(rc) {
2369
2377
  if (group.kind === "text") {
2370
2378
  if (group.content.trim()) elements.push(md(group.content));
2371
2379
  } else {
2372
- elements.push(...renderToolGroup(group.tools, !running));
2380
+ elements.push(...renderToolGroup(group.tools, false));
2373
2381
  }
2374
2382
  }
2383
+ if (state.footer) elements.push(footerStatus(state.footer));
2384
+ if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2385
+ return elements;
2386
+ }
2387
+ function renderTerminal(state, rc) {
2388
+ const elements = [];
2389
+ const answerIdx = lastTextIndex(state.blocks);
2390
+ const answer = answerIdx >= 0 ? state.blocks[answerIdx].content.trim() : "";
2391
+ const processBlocks = state.blocks.filter((_, i) => i !== answerIdx);
2392
+ const blocks = rc.showTools === false ? processBlocks.filter((b) => b.kind !== "tool") : processBlocks;
2393
+ const reasoning = reasoningContent(state);
2394
+ const processEls = buildProcessBody(reasoning, blocks);
2395
+ if (processEls.length > 0) {
2396
+ const toolCount = blocks.reduce((n, b) => b.kind === "tool" ? n + 1 : n, 0);
2397
+ elements.push(
2398
+ collapsiblePanelEl({
2399
+ title: processTitle(Boolean(reasoning), toolCount),
2400
+ expanded: false,
2401
+ border: "grey",
2402
+ elements: processEls
2403
+ })
2404
+ );
2405
+ }
2406
+ if (answer) elements.push(md(answer));
2375
2407
  if (state.terminal === "interrupted") {
2376
2408
  elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
2377
2409
  } else if (state.terminal === "idle_timeout") {
2378
2410
  elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
2379
2411
  } else if (state.terminal === "error" && state.errorMsg) {
2380
2412
  elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
2381
- } else if (state.terminal === "done" && elements.length === 0) {
2413
+ } else if (state.terminal === "done" && !answer) {
2382
2414
  elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
2383
2415
  }
2384
- if (running) {
2385
- if (state.footer) elements.push(footerStatus(state.footer));
2386
- if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2416
+ return elements;
2417
+ }
2418
+ function lastTextIndex(blocks) {
2419
+ for (let i = blocks.length - 1; i >= 0; i--) {
2420
+ const b = blocks[i];
2421
+ if (b && b.kind === "text" && b.content.trim()) return i;
2387
2422
  }
2388
- return card(elements, { streaming: running, summary: summaryText(state) });
2423
+ return -1;
2424
+ }
2425
+ function buildProcessBody(reasoning, blocks) {
2426
+ const rich = processElements(reasoning, blocks, false);
2427
+ if (estimateSize2(rich) <= PROCESS_BODY_BUDGET) return rich;
2428
+ return processElements(reasoning, blocks, true);
2429
+ }
2430
+ function processElements(reasoning, blocks, compactTools) {
2431
+ const out = [];
2432
+ if (reasoning) out.push(reasoningPanel(reasoning, false));
2433
+ for (const group of groupBlocks(blocks)) {
2434
+ if (group.kind === "text") {
2435
+ if (group.content.trim()) out.push(md(group.content));
2436
+ } else {
2437
+ out.push(...renderToolGroup(group.tools, true, compactTools));
2438
+ }
2439
+ }
2440
+ return out;
2441
+ }
2442
+ function processTitle(hasReasoning, toolCount) {
2443
+ const parts = [];
2444
+ if (hasReasoning) parts.push("\u{1F9E0} \u601D\u8003");
2445
+ if (toolCount > 0) parts.push(`\u{1F9F0} ${toolCount} \u4E2A\u5DE5\u5177\u8C03\u7528`);
2446
+ const detail = parts.length > 0 ? `\uFF1A${parts.join(" \xB7 ")}` : "";
2447
+ return `\u{1F5C2} **\u8FC7\u7A0B${detail}**\uFF08\u70B9\u51FB\u5C55\u5F00\uFF09`;
2448
+ }
2449
+ function estimateSize2(els) {
2450
+ let n = 0;
2451
+ for (const el of els) n += JSON.stringify(el).length;
2452
+ return n;
2389
2453
  }
2390
2454
  function buildRunCardPlain(rc) {
2391
2455
  return buildRunCard({ ...rc, cardKey: void 0 });
@@ -2405,8 +2469,9 @@ function* groupBlocks(blocks) {
2405
2469
  }
2406
2470
  if (toolBuf.length > 0) yield { kind: "tools", tools: toolBuf };
2407
2471
  }
2408
- function renderToolGroup(tools, finalized) {
2472
+ function renderToolGroup(tools, finalized, compact = false) {
2409
2473
  if (tools.length === 0) return [];
2474
+ if (compact) return [collapsedToolSummary(tools, true)];
2410
2475
  if (tools.length < COLLAPSE_TOOL_THRESHOLD) {
2411
2476
  return tools.map((t) => toolPanel(t, false));
2412
2477
  }
@@ -4553,21 +4618,29 @@ async function secretsRemove(id) {
4553
4618
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
4554
4619
  }
4555
4620
  function readStdin() {
4556
- return new Promise((resolve3) => {
4621
+ return new Promise((resolve4) => {
4557
4622
  let data = "";
4558
4623
  if (process.stdin.isTTY) {
4559
- resolve3("");
4624
+ resolve4("");
4560
4625
  return;
4561
4626
  }
4562
4627
  process.stdin.setEncoding("utf8");
4563
4628
  process.stdin.on("data", (c) => data += c);
4564
- process.stdin.on("end", () => resolve3(data));
4629
+ process.stdin.on("end", () => resolve4(data));
4565
4630
  });
4566
4631
  }
4567
4632
 
4568
4633
  // src/cli/index.ts
4569
4634
  var program = new Command();
4570
- program.name("feishu-codex-bridge").description("\u628A\u98DE\u4E66/Lark \u6865\u63A5\u5230\u672C\u673A Codex\uFF08\u9879\u76EE=\u7FA4, \u8BDD\u9898=\u4F1A\u8BDD\uFF09").version("0.0.1");
4635
+ function readVersion() {
4636
+ try {
4637
+ const pkgPath = resolve3(dirname8(fileURLToPath2(import.meta.url)), "..", "package.json");
4638
+ return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "0.0.0";
4639
+ } catch {
4640
+ return "0.0.0";
4641
+ }
4642
+ }
4643
+ 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());
4571
4644
  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 () => {
4572
4645
  await runRun();
4573
4646
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",