@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.
- package/README.md +20 -21
- package/dist/cli.js +96 -23
- 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
|
|
56
|
-
| **Codex 已登录** | app-server 需要 `~/.codex/auth.json` |
|
|
57
|
-
| **飞书 / Lark 账号** |
|
|
58
|
-
| **lark-cli
|
|
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
|
-
>
|
|
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((
|
|
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
|
|
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((
|
|
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((
|
|
1152
|
-
this.pending.set(id, { resolve:
|
|
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((
|
|
1179
|
+
await new Promise((resolve4) => {
|
|
1177
1180
|
const t = setTimeout(() => {
|
|
1178
1181
|
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1179
|
-
|
|
1182
|
+
resolve4();
|
|
1180
1183
|
}, graceMs);
|
|
1181
1184
|
child.once("exit", () => {
|
|
1182
1185
|
clearTimeout(t);
|
|
1183
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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,
|
|
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" &&
|
|
2413
|
+
} else if (state.terminal === "done" && !answer) {
|
|
2382
2414
|
elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
|
|
2383
2415
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
|
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((
|
|
4621
|
+
return new Promise((resolve4) => {
|
|
4557
4622
|
let data = "";
|
|
4558
4623
|
if (process.stdin.isTTY) {
|
|
4559
|
-
|
|
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", () =>
|
|
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
|
-
|
|
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.
|
|
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",
|