Skip to content

Commit be6eac4

Browse files
authored
Merge pull request #561 from Cai-Tang-www/feat/feishu-2
feat(feishu): 接入 Gateway-backed 飞书 Adapter(Webhook + SDK 长连接)
2 parents be46f74 + ef9922a commit be6eac4

21 files changed

Lines changed: 2803 additions & 529 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ NeoCode 是一个运行在本地开发环境中的 AI Coding Agent。
5555
- Skills 系统:为不同任务启用专用行为和流程。
5656
- MCP 接入:通过 MCP stdio server 扩展外部工具能力。
5757
- Gateway 模式:通过本地 JSON-RPC / SSE / WebSocket 接口连接桌面端、脚本和第三方客户端。
58+
- Feishu Adapter:支持 Webhook 与 SDK 长连接接入,并用单张状态卡片持续回传 run 状态。
5859

5960
---
6061

@@ -254,6 +255,7 @@ go run ./cmd/neocode --session <session_id>
254255
- [MCP 工具接入(Guide)](https://neocode-docs.pages.dev/guide/mcp)
255256
- [Skills 使用(Guide)](https://neocode-docs.pages.dev/guide/skills)
256257
- [飞书远程接入配置(Guide)](https://neocode-docs.pages.dev/guide/feishu-remote-setup)
258+
- [飞书本地 SDK 长连接(免公网,个人开发推荐)](https://neocode-docs.pages.dev/guide/feishu-remote-setup)
257259
- [Hooks 使用(Guide)](https://neocode-docs.pages.dev/guide/hooks)
258260
- [工具与权限(Guide)](https://neocode-docs.pages.dev/guide/tools-permissions)
259261
- [Runtime / Provider 事件流(Repo Doc)](docs/runtime-provider-event-flow.md)

docs/examples/feishu.yaml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
feishu:
22
enabled: true
3+
ingress: "webhook" # webhook | sdk
34
app_id: "cli_xxx"
4-
app_secret: "cli_secret_xxx"
5+
# 群聊 @ 命中建议配置 bot 身份标识(至少一个)
6+
bot_user_id: "ou_xxx"
7+
bot_open_id: "ou_xxx"
58
verify_token: "verify_token_xxx"
6-
signing_secret: "signing_secret_xxx"
79
insecure_skip_signature_verify: false
810
adapter:
911
listen: "127.0.0.1:18080"
@@ -17,3 +19,23 @@ feishu:
1719
gateway:
1820
listen: "127.0.0.1:8080"
1921
token_file: "~/.neocode/auth.json"
22+
23+
# SDK 本地模式最小示例(无需公网回调):
24+
# feishu:
25+
# enabled: true
26+
# ingress: "sdk"
27+
# app_id: "cli_xxx"
28+
# bot_user_id: "ou_xxx"
29+
# bot_open_id: "ou_xxx"
30+
# request_timeout_sec: 8
31+
# idempotency_ttl_sec: 600
32+
# reconnect_backoff_min_ms: 500
33+
# reconnect_backoff_max_ms: 10000
34+
# rebind_interval_sec: 15
35+
# gateway:
36+
# listen: "127.0.0.1:8080"
37+
# token_file: "~/.neocode/auth.json"
38+
39+
# 运行前请设置环境变量:
40+
# export FEISHU_APP_SECRET="cli_secret_xxx"
41+
# export FEISHU_SIGNING_SECRET="signing_secret_xxx" # 仅 webhook 必需

docs/guides/configuration.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,12 @@ go run ./cmd/neocode --workdir /path/to/workspace
349349
```yaml
350350
feishu:
351351
enabled: true
352+
ingress: "webhook" # webhook | sdk
352353
app_id: "cli_xxx"
353-
app_secret: "cli_secret_xxx"
354+
# 群聊 @ 命中建议至少配置一个
355+
bot_user_id: "ou_xxx"
356+
bot_open_id: "ou_xxx"
354357
verify_token: "verify_token_xxx"
355-
signing_secret: "signing_secret_xxx"
356358
insecure_skip_signature_verify: false
357359
adapter:
358360
listen: "127.0.0.1:18080"
@@ -371,6 +373,8 @@ feishu:
371373
启动命令:
372374

373375
```bash
376+
export FEISHU_APP_SECRET="cli_secret_xxx"
377+
export FEISHU_SIGNING_SECRET="signing_secret_xxx" # 仅 webhook 模式需要
374378
neocode feishu-adapter
375379
```
376380

docs/guides/feishu-adapter.md

Lines changed: 73 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,107 @@
1-
# Feishu Adapter 接入指南(Phase 1
1+
# Feishu Adapter 接入指南(#554 + #557
22

3-
本文档描述 #554 Phase 1 的最小可用接入方式:通过 `neocode feishu-adapter` 把飞书消息桥接到现有 Gateway。
3+
本文档说明 NeoCode Feishu Adapter 的两种入站模式:
44

5-
## 1. 边界说明
5+
- `webhook`:飞书回调到开发者服务(通常需要公网 HTTPS)。
6+
- `sdk`:本机 Adapter 通过飞书 SDK 长连接接收事件(无需公网回调)。
67

7-
- 本阶段只实现云端控制面桥接(Adapter)。
8-
- 不新增任何 Gateway RPC action。
9-
- 仅复用
8+
## 1. 架构边界
9+
10+
- Adapter 不直接调用 runtime 私有接口,只复用 Gateway 已有协议
1011
- `gateway.authenticate`
1112
- `gateway.bindStream`
1213
- `gateway.run`
1314
- `gateway.resolvePermission`
14-
- `gateway.event`(通知)
15-
- 不依赖 `gateway.createSession`
16-
- 真正“云端飞书 -> 用户本机执行”的主动长连通道属于 #555,不在本阶段内。
17-
18-
## 2. 会话与运行 ID 规则
19-
20-
- `session_id = "feishu_" + stableHash(chat_id)`
21-
- `run_id = "feishu_" + stableHash(message_id)`
15+
- `gateway.event`
16+
- 会话与运行 ID 保持实现一致:
17+
- `session_id = "feishu_" + stableHash(chat_id)`
18+
- `run_id = "feishu_" + stableHash(message_id)`
19+
- #557 只新增 SDK 入站,不包含 #555 Local Runner 主动长连。
2220

23-
这两个 ID 都是确定性生成,便于幂等和追踪。
21+
## 2. 事件执行顺序
2422

25-
## 3. 事件处理顺序
26-
27-
飞书消息进入后,适配器固定执行:
23+
每条消息都按固定顺序执行:
2824

2925
1. `authenticate`
3026
2. `bindStream(session_id, run_id)`
3127
3. `run(session_id, run_id, input_text)`
32-
4. 持续消费 `gateway.event`
28+
4. 持续消费 `gateway.event` 并回传飞书
3329

34-
说明:必须先 `bindStream``run`,避免丢失 run 早期事件。
30+
## 3. 配置示例
3531

36-
## 4. 幂等与重放防护
32+
完整示例见 [../examples/feishu.yaml](../examples/feishu.yaml)
3733

38-
- 消息事件去重键:`event_id + message_id`(TTL 内只执行一次 run)。
39-
- 卡片回调去重键:`request_id + decision`(TTL 内只提交一次审批)。
40-
- 去重在 `run` 被网关受理后才标记成功;若受理失败会释放去重键,允许飞书重试恢复。
34+
关键字段:
4135

42-
## 5. 审批闭环
36+
- `feishu.ingress``webhook``sdk`,默认 `webhook`
37+
- `feishu.app_id`
38+
- `FEISHU_APP_SECRET`(固定环境变量,启动时必检)
39+
- `feishu.bot_user_id` / `feishu.bot_open_id`(群聊 @ 命中建议至少配置一个)
40+
- `feishu.verify_token`
41+
- `FEISHU_SIGNING_SECRET`(仅 `webhook` 强制,固定环境变量)
4342

44-
Phase 1 仅支持最小动作:
43+
## 4. 启动方式
4544

46-
- `allow_once`
47-
- `reject`
45+
### 4.1 Webhook 模式(#554
4846

49-
当收到 `permission_requested` 事件时,适配器发送最小审批卡片;用户点击后回调 `gateway.resolvePermission`
47+
```bash
48+
neocode feishu-adapter --ingress webhook
49+
```
50+
51+
通常还会覆盖地址参数:
52+
53+
```bash
54+
neocode feishu-adapter \
55+
--ingress webhook \
56+
--listen 127.0.0.1:18080 \
57+
--event-path /feishu/events \
58+
--card-path /feishu/cards
59+
```
5060

51-
## 6. 安全要求
61+
### 4.2 SDK 模式(#557,本地无公网)
5262

53-
适配器日志不会打印以下敏感信息:
63+
```bash
64+
export FEISHU_APP_SECRET="cli_secret_xxx"
65+
neocode feishu-adapter --ingress sdk
66+
```
5467

55-
- `app_secret`
56-
- `verify_token`
57-
- `signing_secret`
58-
- `gateway token`
59-
- `Authorization`
68+
SDK 模式下不要求公网回调地址,不要求 `adapter.listen/event_path/card_path`
69+
如果缺少 `FEISHU_APP_SECRET`,启动会直接失败,避免把明文 secret 落到 `config.yaml`
6070

61-
默认要求签名校验与回调 token 校验都开启:
71+
## 5. 群聊触发规则
6272

63-
- `verify_token` 必填
64-
- `signing_secret` 必填
73+
- 私聊:默认处理。
74+
- 群聊:必须 `@` 当前 bot 才会触发 run,建议配置 `bot_user_id``bot_open_id` 作为命中目标。
75+
- 任意 mention(@其他用户)不会触发 NeoCode。
6576

66-
仅在联调场景可显式设置 `insecure_skip_signature_verify=true` 跳过签名校验(不建议生产使用)。
77+
## 6. 幂等与重试
6778

68-
群聊消息默认仅在检测到 `@` 机器人时受理;私聊消息默认受理。
79+
- 消息去重键:`event_id + message_id`
80+
- 卡片去重键:`request_id + decision`
81+
- 仅当 `gateway.run` 成功受理后才标记成功;
82+
-`run` 失败会释放去重状态,Webhook 返回 `HTTP 500`,SDK 长连接回调返回失败 ACK,允许飞书重试恢复。
6983

70-
用户可见错误仅返回摘要,不回传内部堆栈。
84+
## 6.1 轻量级状态卡片
7185

72-
## 7. 启动方式
86+
- 每个 run 会创建一个独立状态卡片,并复用同一个 `card_id` 更新:
87+
- `任务`
88+
- `状态``thinking / planning / running`
89+
- `审批``none / pending / approved / rejected`
90+
- `结果``pending / success / failure`
91+
- `permission_requested``hook_notification(async_rewake)``run_done``run_error` 都会更新同一张卡片,不额外刷多条进度文本。
92+
- 最终完成/失败仅更新卡片结果与摘要区,不额外回传独立文本消息。
7393

74-
```bash
75-
neocode feishu-adapter
76-
```
94+
## 7. 审批能力边界
7795

78-
也可以通过命令行参数覆盖配置:
96+
- Webhook 模式:支持卡片回调 -> `resolvePermission`
97+
- SDK 模式:优先支持 SDK 卡片动作事件;若租户侧不可用,支持文本审批降级:
98+
- `允许 <request_id>`
99+
- `拒绝 <request_id>`
79100

80-
```bash
81-
neocode feishu-adapter \
82-
--listen 127.0.0.1:18080 \
83-
--event-path /feishu/events \
84-
--card-path /feishu/cards \
85-
--app-id xxx \
86-
--app-secret yyy \
87-
--verify-token zzz \
88-
--signing-secret sss
89-
```
101+
以上两种路径都复用 `gateway.resolvePermission`,不新增 Gateway action。
90102

91-
## 8. 配置示例
103+
## 8. 安全要求
92104

93-
参考 [../examples/feishu.yaml](../examples/feishu.yaml)
105+
- 默认启用签名校验(Webhook);
106+
- 日志不会输出 `app_secret`、签名密钥、gateway token、Authorization 等敏感信息;
107+
- 用户侧只回关键状态(受理、权限请求、完成、失败),不暴露内部堆栈和控制面细节。

go.mod

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,28 @@ module neo-code
33
go 1.25.0
44

55
require (
6+
github.com/Masterminds/semver/v3 v3.4.0
67
github.com/Microsoft/go-winio v0.6.2
78
github.com/anthropics/anthropic-sdk-go v1.37.0
89
github.com/atotto/clipboard v0.1.4
910
github.com/charmbracelet/bubbles v1.0.0
1011
github.com/charmbracelet/bubbletea v1.3.10
1112
github.com/charmbracelet/glamour v1.0.0
1213
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
14+
github.com/charmbracelet/x/ansi v0.11.6
15+
github.com/creack/pty v1.1.24
1316
github.com/creativeprojects/go-selfupdate v1.5.2
17+
github.com/larksuite/oapi-sdk-go/v3 v3.6.1
1418
github.com/openai/openai-go/v3 v3.32.0
19+
github.com/pmezard/go-difflib v1.0.0
1520
github.com/prometheus/client_golang v1.23.2
21+
github.com/sahilm/fuzzy v0.1.1
1622
github.com/spf13/cobra v1.10.2
1723
github.com/spf13/viper v1.21.0
1824
golang.design/x/clipboard v0.7.1
1925
golang.org/x/net v0.52.0
2026
golang.org/x/sys v0.42.0
27+
golang.org/x/term v0.41.0
2128
google.golang.org/genai v1.54.0
2229
gopkg.in/yaml.v3 v3.0.1
2330
modernc.org/sqlite v1.48.2
@@ -29,28 +36,26 @@ require (
2936
cloud.google.com/go/compute/metadata v0.5.0 // indirect
3037
code.gitea.io/sdk/gitea v0.22.1 // indirect
3138
github.com/42wim/httpsig v1.2.3 // indirect
32-
github.com/Masterminds/semver/v3 v3.4.0 // indirect
3339
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
3440
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
3541
github.com/aymerick/douceur v0.2.0 // indirect
3642
github.com/beorn7/perks v1.0.1 // indirect
3743
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3844
github.com/charmbracelet/colorprofile v0.4.3 // indirect
3945
github.com/charmbracelet/harmonica v0.2.0 // indirect
40-
github.com/charmbracelet/x/ansi v0.11.6 // indirect
4146
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
4247
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
4348
github.com/charmbracelet/x/term v0.2.2 // indirect
4449
github.com/clipperhouse/displaywidth v0.11.0 // indirect
4550
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
46-
github.com/creack/pty v1.1.24 // indirect
4751
github.com/davidmz/go-pageant v1.0.2 // indirect
4852
github.com/dlclark/regexp2 v1.11.5 // indirect
4953
github.com/dustin/go-humanize v1.0.1 // indirect
5054
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
5155
github.com/fsnotify/fsnotify v1.9.0 // indirect
5256
github.com/go-fed/httpsig v1.1.0 // indirect
5357
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
58+
github.com/gogo/protobuf v1.3.2 // indirect
5459
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
5560
github.com/google/go-cmp v0.7.0 // indirect
5661
github.com/google/go-github/v74 v74.0.0 // indirect
@@ -76,14 +81,12 @@ require (
7681
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
7782
github.com/ncruces/go-strftime v1.0.0 // indirect
7883
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
79-
github.com/pmezard/go-difflib v1.0.0 // indirect
8084
github.com/prometheus/client_model v0.6.2 // indirect
8185
github.com/prometheus/common v0.66.1 // indirect
8286
github.com/prometheus/procfs v0.16.1 // indirect
8387
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
8488
github.com/rivo/uniseg v0.4.7 // indirect
8589
github.com/sagikazarmark/locafero v0.11.0 // indirect
86-
github.com/sahilm/fuzzy v0.1.1 // indirect
8790
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
8891
github.com/spf13/afero v1.15.0 // indirect
8992
github.com/spf13/cast v1.10.0 // indirect
@@ -107,7 +110,6 @@ require (
107110
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
108111
golang.org/x/oauth2 v0.34.0 // indirect
109112
golang.org/x/sync v0.20.0 // indirect
110-
golang.org/x/term v0.41.0 // indirect
111113
golang.org/x/text v0.35.0 // indirect
112114
golang.org/x/time v0.14.0 // indirect
113115
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect

0 commit comments

Comments
 (0)