这份文档对应 PaperFlow 当前唯一保留的本地联调方案:
webhook + ngrok
如果只是把精读报告创建成飞书文档,不需要 webhook 或 ngrok;请看 feishu-doc-export.md。
最少需要:
FEISHU_APP_ID=cli_xxxxxxxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxx
FEISHU_BOT_NAME=PaperFlow Bot
FEISHU_USER_ID=ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFEISHU_APP_ID / FEISHU_APP_SECRET 在飞书开放平台应用详情页 → 凭证与基础信息 看(详见 §4.1)。
FEISHU_VERIFICATION_TOKEN 在 开发配置 → 事件与回调 → 事件配置 顶部那一栏看(详见 §4.3)。
FEISHU_USER_ID 是你自己的飞书 open_id(私聊 bot 没绑 chat_id 时,bot 会私聊回到这个 open_id)。三种取法挑一种就行:
- 最快:bot 启动后,自己私聊 bot 随便发一句话,本地 webhook 终端会打印一行类似
sender open_id=ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,复制ou_...整段粘到.env。 - 从 API:在浏览器打开 https://open.feishu.cn/api-explorer → 搜
获取用户信息 / Get User Info→ 用应用身份调一次,返回里open_id就是。 - 从飞书客户端:打开飞书设置 → 个人信息页右上角 ⋯,部分版本会显示
User ID,但那个不是 open_id(是user_id)。除非你把 feishu_reporter.py 改成走user_id模式,否则别用这种方式。
注意:bot 通过
FEISHU_USER_ID私聊你之前,你必须先和 bot 互发过至少一条消息(飞书要求"已建立单聊会话"),否则 bot 主动发消息会报99991672 NotFound。
可选(强烈推荐 — URL 永远不变):
NGROK_AUTHTOKEN=xxxxxxxxxxxxxxxx
NGROK_DOMAIN=gap-suffrage-caddie.ngrok-free.devNGROK_DOMAIN 来自 https://dashboard.ngrok.com/domains 点 + New Domain,免费账号送一个永久域名。配置之后下面"配置飞书开放平台"那一步只需做一次。
ngrok version
ngrok config add-authtoken <your-token>token 来自 https://dashboard.ngrok.com/get-started/your-authtoken,每台机器跑一次即可。
python deployments/feishu/webhook-server/start-with-ngrok.py成功后会看到:
- 本地 webhook 地址
- 当前 ngrok 公网地址
- 可直接填写到飞书后台的
Request URL
同时脚本会把地址写到:
data/ngrok_url.txt
data/feishu_request_url.txt
入口:https://open.feishu.cn/app → 在 企业自建应用 / Custom Apps 列表里点中你的 PaperFlow App,进入应用详情页。
后面所有"左侧菜单"指的都是应用详情页左边那一栏,分组依次是:应用信息 / 凭证与基础信息 / 应用能力 / 开发配置 / 应用发布。
左侧菜单 → 凭证与基础信息 / Credentials & Basic Info
| 页面字段 | 复制到 .env 的哪一项 |
|---|---|
| App ID | FEISHU_APP_ID |
| App Secret(点"显示"才能看到) | FEISHU_APP_SECRET |
左侧菜单 → 应用能力 / Add features(不是顶部,是左栏中间的分组)→ 找到 机器人 / Bot 这一行卡片 → 点 添加 / 启用。
启用后这一行右侧会出现"已添加"。没有这一步飞书不会把消息推过来。
左侧菜单 → 开发配置 / Development Config → 事件与回调 / Events & Callbacks → 顶部 Tab 选 事件配置 / Event Configuration。
页面顶部 配置订阅方式 / Subscription Method 那一块:
- 推送方式 / Push Method:选 将事件发送至开发者服务器(HTTP)/ Send to developer server (HTTP)(不是"长连接 WebSocket")
下方出现三个输入框,按下表填:
| 字段(页面原文) | 值 | 说明 |
|---|---|---|
| 请求地址 / Request URL | https://gap-suffrage-caddie.ngrok-free.dev/(直接复制 data/feishu_request_url.txt 整行) |
结尾的 / 不能少 |
| Encrypt Key / 加密 Key | 完全留空,不要点"重置" | PaperFlow webhook 不解密 payload;填了校验必失败 |
| Verification Token / 校验 Token | 一串 16+ 位字符 | 复制这串到 .env 里的 FEISHU_VERIFICATION_TOKEN,两边必须完全一致(含大小写) |
点页面右下角 保存 / Save。飞书会立刻调用一次 Request URL 做校验:
- 成功 → 弹出 "URL 校验通过 / Verification succeeded" 绿条
- 失败 → 红条提示,按下面"常见问题"排查
同一页面继续往下翻,找到 事件 / Events 区块(在 Request URL 下方,有一个表格 + 添加事件 / Add Events 按钮)。
点 添加事件 / Add Events,弹窗里搜索并勾选:
| 事件名(搜索关键词) | 事件 Key | 必需? |
|---|---|---|
| 接收消息 / Receive Messages | im.message.receive_v1 |
必需,没它 bot 收不到任何消息 |
| 消息已读 / Message Read | im.message.message_read_v1 |
可选 |
| 消息表情回复 / Message Reaction Created | im.message.reaction.created_v1 |
想用点赞/点踩当 feedback 时勾 |
每勾一个,弹窗会提示"该事件需要以下权限",直接点确认/添加,权限会自动加进权限申请单。
弹窗关掉后,点表格右下角的 保存 / Save 让订阅生效。
左侧菜单 → 开发配置 / Development Config → 权限管理 / Permissions & Scopes。
页面有一个搜索框,按表格依次搜索 + 点 申请 / Add:
| Scope(搜索这个名字) | 用途 |
|---|---|
im:message |
收发消息 |
im:message:send_as_bot |
以 bot 身份发消息 |
im:resource |
下载用户上传的 PDF |
docx:document |
创建精读报告 docx |
docx:document:readonly |
读取自己创建的报告 |
contact:user.id:readonly |
把 open_id 解析成用户 |
4.4 弹窗自动加进来的权限,这里会显示"待发布"。
申请完必须发版本,否则权限不会真正生效:
- 左侧菜单 → 应用发布 / App Release → 版本管理与发布 / Versions & Releases
- 右上角点 创建版本 / Create Version
- 版本号随便填(如
0.1.0)→ 更新说明随便写 → 提交申请 / Submit - 企业自建应用一般会显示"无需审核,已发布";如果显示"待管理员审核",去飞书管理后台 (https://www.feishu.cn/admin) 自己批准就行
在飞书桌面端:
- 建一个群(或用现有的群)
- 群右上角 ⋯ → 群设置 / Group Settings → 群机器人 / Group Bots → 添加机器人 / Add Bot
- 搜你的 PaperFlow Bot → 添加
bot 进群后,在群里 @它 随便发一句话,本地 webhook 终端会打印一行带 chat_id 的日志(形如 oc_xxxxxxxx)。复制 oc_...,回命令行:
python agents/role-manager/main.py --command "绑定 role1 oc_xxxxxxxx"把 chat_id 写回 data/roles.json。只私聊用 bot 的话这一步可以跳过。
PaperFlow 是多角色系统:每个飞书群对应一个角色(role),bot 收到消息后查 data/roles.json 反查"这个 chat_id 是哪个 role",然后用那个 role 的画像生成推荐 / 报告。
只有一个文件:data/roles.json。结构是:
{
"roles": {
"role1": {
"user_id": "user_role1",
"feishu_chat_id": "oc_aae06b...",
"description": "direction: gui agent, web automation, ..."
},
"role2": { ... }
}
}每条 role 有一个 feishu_chat_id 字段决定它绑哪个群。webhook 每次收消息都会重读这个文件,不用重启。
python -c "import json; r=json.load(open('data/roles.json',encoding='utf-8'))['roles']; [print(f'{n} -> {v[\"feishu_chat_id\"] or \"(empty)\"} | {v.get(\"description\",\"\")[:50]}') for n,v in r.items()]"方式 A:群里 @bot 直接绑(最方便)
在目标群里 @PaperFlow Bot 发:
绑定 role1
bot 会把当前群的 chat_id 写到 role1 的 feishu_chat_id。
方式 B:命令行
python agents/role-manager/main.py --command "绑定 role1 oc_xxxxxxxx"方式 C:直接编辑 JSON
打开 data/roles.json,改对应 role 的 feishu_chat_id 字段保存即可。
路径 1:复用现成 24 个 role
适合:跟实验对齐 / 跑 demo / 想体验不同方向。
操作:在飞书里建若干个群 → 把 bot 拉进每个群 → 在每个群里 @bot 绑定 roleN 即可(每个群只能绑一个 role)。
路径 2:建自己的 role
适合:自己研究方向、不在 24 个预设方向里。
# 建一个新角色
python agents/role-manager/main.py --command "创建角色 alice,研究方向:扩散模型 视频生成"
# 跑一次 profile cold start 给它写初始画像
paperflow profile \
--user-id user_alice \
--natural-language "扩散模型 视频生成"
# 把它绑到飞书群
python agents/role-manager/main.py --command "绑定 alice oc_xxxxxxxx"role-manager/main.py 支持的命令:
| 命令 | 用途 |
|---|---|
创建角色 <name>,研究方向:<keywords> |
新建一个 role |
切换到 <name> |
CLI 模式下切换默认 role(webhook 模式下不影响群) |
删除角色 <name> |
删 role |
绑定 <name> <chat_id> |
把 chat_id 写到 role 上 |
角色列表 |
列出所有 role |
飞书群里回复 1 3、1-5、none、全部、没有 时,webhook 会先根据
chat_id 找到绑定的 role / user_id,然后调用和 CLI 一样的
feedback-agent。因此下面两种方式对同一个 user_id 来说是等价的画像学习信号:
飞书群里回复:1 3
paperflow feedback --user-id user_alice --push-id <latest_push_id> --reply "1 3"二者都会写入 data/paperflow.db,更新用户画像、selected/skipped 行为日志、
drift 状态,并在开启 PAPERFLOW_WIKI_INGEST=true 时同步到本地 wiki。完整闭环见
feedback-loop.md。
飞书界面里看不到 chat_id。最快的办法:bot 进群后,群里 @bot 发任意一句话,本地 webhook 终端会打印:
[INFO] received text from chat_id=oc_aae06b0c... role=role1 ...
oc_... 那串就是这个群的 chat_id。如果 role= 后面是 None,就说明这个群还没绑任何 role,按 §5.3 绑一下就行。
curl http://127.0.0.1:8080/healthcurl http://127.0.0.1:4040/api/tunnels看返回内容里是否有:
localhost:8080
给 bot 私聊或群里 @它 发任意一条文本消息,例如:
推送
如果飞书里能看到你发出的消息,但本地 webhook 没有收到 POST /,通常就是飞书后台还挂着旧的 ngrok 地址(用 NGROK_DOMAIN 静态域名后这个问题不会再出现)。
按这个顺序查:
- webhook 是否已经启动(
curl http://127.0.0.1:8080/health回{"status":"healthy"}) - ngrok 隧道是否真的指向 8080(
curl http://127.0.0.1:4040/api/tunnels看到localhost:8080) - 飞书后台 Encrypt Key 是否留空
- 飞书后台 Verification Token 是否和
.env里的FEISHU_VERIFICATION_TOKEN完全一致(含大小写) - URL 末尾有没有漏
/
免费 ngrok 不带静态域名时 URL 每次重启都会变。在 .env 里加一行 NGROK_DOMAIN=xxx-yyy-zzz.ngrok-free.dev(去 https://dashboard.ngrok.com/domains 领一个免费的)就能锁定 URL。
最常见的几种原因,按出现频率排:
1. 缺权限(最常见):申请了但没"创建版本 → 发布"
scope 申请完只是"待发布",必须发版本权限才生效,否则 bot 调发消息接口会返回 99991663 / 99991668 / NoAccess 之类。
去 应用发布 → 版本管理与发布 → 创建版本 → 提交 → 发布。看终端是否还有 Send message failed: code=99991xxx 这种报错。
2. bot 没启用机器人能力
scope 全了,但没在 应用能力 → 机器人 里启用。终端调 IM 接口返回 230002 或 99991671。回 §4.2 启用一次。
3. 群里 bot 默认只听 @它 的消息
终端日志里能看到 chat_id=oc_xxx 但没有 intent=... / routing to coordinator 这种行 — 说明消息没进 coordinator。原因:群里 bot 的"接收消息范围"默认只接 @它 的消息,你直接发"推送"它收不到。
解决:要么每次 @ 它(@PaperFlow Bot 推送),要么去 事件订阅 / Event Subscriptions 把 接收消息范围 / Message Receive Range 改成"所有消息"。
4. chat_id 没绑到任何 role
终端打 _find_role_by_chat_id ... role=None,coordinator 拿不到 user_id 就早早 return 不回消息。按 §5.3 绑一下:群里 @bot 发 绑定 role1。
5. 私聊场景下 bot 主动发消息要求"已建立单聊会话"
如果你刚建好 bot 没和它互发过任何消息,bot 主动给你 push 会报 99991672 NotFound。先在私聊里随便发一句"hi"建好会话再试。
6. coordinator 内部抛异常但被吞了
终端有 Traceback 就是这种。最常见是 OPENAI_API_KEY 没配 / 配错(默认 LLM provider 是 openai),bot 生成不了内容自然没法回。看终端 traceback 第一行是哪个错对症下药;如果只是想跑通流程不在意推荐质量,临时设:
PAPERFLOW_LLM_PROVIDER=mock PAPERFLOW_EMBED_PROVIDER=hash python deployments/feishu/webhook-server/start-with-ngrok.pymock provider 不调任何外部 API,能快速验证消息回路。
webhook 终端里搜这几个关键词,定位卡在哪一层:
| 关键词 | 含义 |
|---|---|
POST / |
飞书 → webhook 这一段通了 |
chat_id=oc_xxx role=roleN |
role 反查成功 |
intent=daily_push 等 |
coordinator 识别意图成功 |
Send message failed |
bot 调发消息接口失败(通常是权限) |
Traceback |
coordinator 内部炸了,看下一行错误 |