# lark-codex-bridge

把飞书 / Lark 消息和本地 Codex CLI 打通的轻量 bot，用一条命令起服务，扫码绑应用，在飞书里和 Codex 对话、让它读图 / 改代码。

> 说明：本项目 fork 自 [feishu-claude-code-bridge](https://github.com/zarazhangrui/feishu-claude-code-bridge)，在其飞书桥接、会话管理、交互卡片、配置向导等能力基础上，将本地 Agent 后端从 Claude Code 改为 Codex CLI，并更名为 `lark-codex-bridge`。

[English README](./README.md)

## 能干什么

- 在飞书（私聊直接发；群里 `@bot`）把消息转给本地的 `codex` CLI，Codex 在你指定的工作目录里工作
- **流式卡片**：Codex 的文本和工具调用实时出现在同一张卡片上，不用傻等
- **会话延续**：每个 chat 独立 session，对话能接着上次说
- **抢占 + 批处理**：中途发新消息会打断旧任务；快速连发几条会合并成一次请求
- **多工作空间**：`/ws` 切换不同项目，session 自己重置
- **图片 / 文件**：直接发给 bot，Codex 会读本地下载的文件路径
- **卡片按钮**：`/help` `/ws list` `/status` 返回交互卡片，点按钮直接操作

## 前置条件

- Node.js **≥ 20**
- `codex` CLI 已安装并登录：https://developers.openai.com/codex
- 一个飞书 / Lark PersonalAgent 应用（首次启动的扫码向导能帮你创建）

## 安装

```bash
npm i -g lark-codex-bridge
# 或
pnpm add -g lark-codex-bridge
```

## 首次启动

```bash
lark-codex-bridge start
```

第一次跑会检测到没配置应用，**自动进入扫码向导**：

1. 终端渲染一个二维码
2. 用飞书 App 扫码
3. 选择 / 创建 PersonalAgent 应用
4. 成功后凭据写入 `~/.lark-codex/config.json`

### 开放平台补齐 scope 和事件订阅

向导只负责创建应用，平台侧还需要手动确认：

**权限 scope**：
- `im:message`
- `im:message:send_as_bot`
- `im:resource`

**事件订阅（使用长连接接收）**：
- `im.message.receive_v1`
- `card.action.trigger`
- `im.message.reaction.created_v1` / `deleted_v1`（可选）
- `im.chat.member.bot.added_v1`（可选）

启用以后再次 `lark-codex-bridge start`，看到 `✓ 已连接` 就可以在飞书里找 bot 对话了。

## 命令速查

### 宿主 CLI

```
lark-codex-bridge start [-c <config>]   启动 bot
lark-codex-bridge ps                    列出本机所有正在跑的 start 进程
lark-codex-bridge stop <id|#>           终止指定 start 进程（SIGTERM，2s 后 SIGKILL）
lark-codex-bridge --help                列所有命令
```

> 多开同一个 app 时，开放平台会把事件随机推到其中一个长连接。`start` 启动前会检测同 app 已有的进程，TTY 下提示 `[c]ontinue / [k]ill old / [a]bort` 三选；非 TTY 只 warn 并继续。

其它命令（`status` / `doctor` / `handover` / `workspace` / `service`）是占位，后续版本补。

### 在飞书里用的斜杠命令

| 命令 | 作用 |
|---|---|
| `/new` `/reset` | 清空当前 chat 的会话 |
| `/cd <path>` | 切换工作目录（会重置 session） |
| `/ws list` | 列所有命名工作空间（卡片 + 按钮） |
| `/ws save <name>` | 把当前 cwd 存为命名工作空间 |
| `/ws use <name>` | 切换到命名工作空间 |
| `/ws remove <name>` | 删除命名工作空间 |
| `/status` | 当前 cwd / session / agent（卡片 + 按钮） |
| `/config` | 调整偏好（消息回复方式、工具调用显示等） |
| `/stop` | 终止当前正在跑的 run（也可点卡片底部 ⏹ 终止 按钮） |
| `/timeout [N\|off\|default]` | 当前 session 的 idle 探活（分钟）；`/config` 改全局默认。详见下方"常见问题 — Codex 子进程假死" |
| `/ps` | 列出本机所有 start 进程，标识当前回复的是哪个 |
| `/exit <id\|#>` | 终止指定 start 进程（自己 = graceful 退出；他人 = SIGTERM） |
| `/reconnect` | 强制重连 WebSocket（网络抖动后 bot 没反应时用） |
| `/doctor [描述]` | 把最近运行日志和你的描述喂给 Codex，自助诊断卡住 / 异常的原因 |
| `/help` | 帮助卡片 |
| 其它 `/xxx` | 原样交给 Codex |

**消息策略**：私聊 = 不需要 @，任何消息都回；**群（含话题群）= 默认要 @bot 才回**（0.1.22 起的新默认），不 @ 时 bot 完全沉默；@全员永远不响应；云文档评论必须 @bot。要恢复"群里也不强制 @"的老行为：`/config` → "群里需要 @ bot" → 选"否"。

## 数据目录

| 路径 | 内容 |
|---|---|
| `~/.lark-codex/config.json` | 应用凭据（App ID / Secret），权限 600 |
| `~/.lark-codex/sessions.json` | 每个 chat / 话题 的 Codex thread id + cwd（+ 可选的 `/timeout` 覆盖） |
| `~/.lark-codex/workspaces.json` | 工作空间映射 |
| `~/.lark-codex/processes.json` | 当前在跑的 start 进程注册中心（`ps`/`stop` 用），死进程会被自动清理 |
| `~/.lark-codex/media/<chatId>/` | 下载的图片 / 文件，24h 自动清理 |
| `~/.lark-codex/logs/YYYY-MM-DD.log` | 结构化运行日志（JSON line），按天滚动；启动时清理超过 7 天的老文件（`LARK_CODEX_LOG_DAYS` 环境变量可改）；`/doctor` 命令读它做诊断 |

> 升级自 0.1.11 之前的版本？跑一次 `lark-codex-bridge migrate` —— 自动把 `~/.config/lark-codex-bridge/` 和 `~/.cache/lark-codex-bridge/` 下的内容搬到新位置，并把 `config.json` 升级到新结构。

## Codex 运行配置

在飞书里发 `/config` 可以调整 Codex 子进程参数，改完从下一次 run 开始生效：

- `codexBinary`：Codex CLI 命令或绝对路径，默认 `codex`
- `model`：传给 `codex exec --model`；留空则使用 Codex 默认配置
- `profile` / `profileV2`：分别对应 `--profile` 和 `--profile-v2`
- `sandbox`：`workspace-write`（默认）、`read-only` 或 `danger-full-access`
- `skipGitRepoCheck`：默认开启，允许 `/cd` 到非 Git 目录
- `search`：开启后以顶层 `codex --search exec ...` 方式启用 Web Search

也可以直接写 `~/.lark-codex/config.json` 的 `preferences.agent`：

```json
{
  "preferences": {
    "agent": {
      "provider": "codex",
      "codexBinary": "codex",
      "sandbox": "workspace-write",
      "skipGitRepoCheck": true,
      "search": false
    }
  }
}
```

## 访问控制（可选）

默认 bot 是"开放"的：任何能找到它的人都能私聊它，群里 @bot 就触发响应。**个人自己用 / 给朋友用，这就够了**——但如果想给团队用、或者怕在大群里被滥用，可以在飞书里发 `/config`，调下面三栏中的一栏或几栏。

### 几种典型用法

**只让我自己用**

`/config` 表单里：
- "用户白名单"：填你自己的 `open_id`
- 其它两栏留空

之后非你发的消息会被 bot 静默丢弃——bot 不会回"你没权限"之类的话，免得暴露它存在。

**只让一小群同事用**

- "用户白名单"：填同事们的 `open_id`，英文逗号分隔
- 其它两栏留空

**bot 只在指定工作群里干活**

私聊不受影响；群里只有名单上的群才触发响应：
- "群白名单"：填想让 bot 工作的群 `chat_id`，英文逗号分隔
- 私聊**永远**不受此约束——意味着你随时能 DM bot 调配置

**谁都能跟 bot 聊，但只有我能改设置**

- "管理员"：填你自己的 `open_id`
- 其它两栏留空

下次别人发 `/account` `/config` `/exit` `/reconnect` `/doctor` `/cd` `/ws` 这些敏感命令，会收到 `❌ 此命令仅管理员可用`。普通对话（让 bot 帮忙做事）不受影响。

**完全收紧**

三栏全填。`/config` 表单会拦下常见误配——比如管理员名单里没把你自己加进去、群白名单里没包含当前会话，提交时会被拒绝并提示原因，不会让你不小心把自己锁在外面。

### 怎么找 `open_id` 和 `chat_id`

最快的办法：让目标用户给 bot 发一条任意消息（群的话就 @bot 一下），然后在终端：

```bash
grep '"event":"enter"' ~/.lark-codex/logs/$(date +%Y-%m-%d).log | tail -5
```

每一行都带 `chatId`（= 群或私聊 ID）和 `senderId`（= 用户 `open_id`），照着复制就行。

也可以查飞书开放平台的"获取用户信息"API，但要先给你的应用加 `contact:user` scope，没必要为了几个 ID 折腾。

### 几点提醒

- 改完 `/config` **下一条消息**就生效，不用重启
- 把任何一栏设成**空字符串** = 不限制（不是"一个都不允许"）
- 想从某种受限状态回到"完全开放"，把对应栏目清空再提交即可
- 私聊不受"群白名单"约束——这是设计上故意的：万一你不小心把所有群都锁死了，**回到 bot 的私聊里发 `/config` 就能解锁**

### 高级：直接改配置文件

不太想登飞书也可以，`/config` 表单背后写的是 `~/.lark-codex/config.json` 的 `preferences.access`：

```json
{
  "preferences": {
    "access": {
      "allowedUsers": ["ou_xxxxxxxxxxxxx"],
      "allowedChats": ["oc_xxxxxxxxxxxxx"],
      "admins":       ["ou_xxxxxxxxxxxxx"]
    }
  }
}
```

手改完之后**重启 bridge** 或者**找一个被允许的会话发 `/reconnect`** 让新配置生效。日常调整还是用 `/config` 表单更省事，直接改文件主要用在"部署脚本里预填"之类的场景。

## 常见问题

**Codex 挂住不回复**：通常是 `codex` CLI 本身没登录，或者 session 指向了不存在的 cwd。发 `/status` 看当前状态；`/new` 重开会话往往就好。

**Codex 子进程假死（卡片停在最后一帧不动）**：支持 idle 探活：Codex 一段时间没输出就被 SIGTERM kill，卡片末尾会标 "⏱ N 分钟无响应，已自动终止"。默认关闭。开启方式：`/config` 设全局值（分钟），或 `/timeout 10` 只对当前 session 生效；`/timeout off` 关掉某个 session 的探活；`/timeout default` 清掉 session 覆盖回退到全局。

**图片发过去 Codex 说看不到**：确认当前 Codex CLI 支持 `codex exec --image`，并查看日志里的附件下载路径。

## 维护者发布

本项目通过 GitHub Actions + npm Trusted Publishing 自动发布，流程见 [docs/release.md](./docs/release.md)。

## 许可

[MIT](./LICENSE)
