分析时间:2026-07-08
日志文件:/Users/catsleep/Documents/projects/zleap_cli_server/test/07.logs
目标 profile:8753734f-56d4-46ff-9604-ffd2d69b48de
这次不是飞书侧用户授权真正过期或被撤销,而是服务重启后从 Redis 恢复 config.json 时,目标 profile 被同步合并逻辑丢掉了。
目标 profile 的 keychain 信息和用户 token 仍然存在,所以 /api/v1/feishu/info 可以通过 keychain fallback 返回 200;但 /api/v1/feishu/conversation 会先走 ResolveWithRefresh,它必须在 config.json 的 profile 列表里找到该 profile。重启后 config.json 中没有目标 profile,于是直接报:
profile "8753734f-56d4-46ff-9604-ffd2d69b48de" not found
因此表现为“掉授权”,实际根因是 profile 配置丢失。
| 时间 | 事件 | 证据 |
|---|---|---|
| 20:57:12 | 服务启动,从 Redis 同步 lark-cli 文件 | larkcli: synced from redis total=528 dedup=528 written=528 skipped=0 |
| 20:59:57 | 目标 profile 注册成功,写入 app 配置 | registration config saved profile=8753734f-56d4-46ff-9604-ffd2d69b48de app_id=cli_aabb89d590395cd2 |
| 21:00:07 | 目标 profile 登录成功 | login completed profile=8753734f-56d4-46ff-9604-ffd2d69b48de open_id=ou_f76af3445fea8b99961eb47b82ed6fe1 |
| 21:00:09 | 目标 profile 拉会话成功 | /api/v1/feishu/conversation ... status=200 |
| 21:01:30 | 另一个 profile 注册到同一个 app_id | registration config saved profile=a322f95e-4bbb-4086-a068-c92e55ff77b5 app_id=cli_aabb89d590395cd2 |
| 21:01:38 | 该 profile 登录成功,open_id 与目标 profile 相同 | login completed profile=a322f95e-4bbb-4086-a068-c92e55ff77b5 open_id=ou_f76af3445fea8b99961eb47b82ed6fe1 |
| 21:02:18 | 服务停止,同步本地 lark-cli 文件到 Redis | larkcli: stopping auth syncer / larkcli: synced to redis files=536 |
| 21:02:20 | 服务重启,从 Redis 重新拉回 lark-cli 文件 | larkcli: synced from redis total=536 dedup=536 written=536 skipped=0 |
| 21:02:39 | 重启后目标 profile 首次会话请求失败 | conversation token maintenance failed ... profile "...8753734f..." not found |
| 21:04:06 | /feishu/info 仍能从 keychain 找到 app_id | feishu info: resolved from keychain (JSON format) profile=8753734f-... app_id=cli_aabb89d590395cd2 |
目标 profile 在注册和登录后,当时本地进程中的 config.json 能找到它,所以 conversation 正常返回 200。
21:02:18 服务停止并执行 SyncToRedis,21:02:20 新实例从 Redis 恢复文件。恢复后的 config.json 的 available profile 列表里没有目标 profile。
日志中 21:02:39 的 available 列表统计结果:
available_count = 141
8753734f-56d4-46ff-9604-ffd2d69b48de = false
a322f95e-4bbb-4086-a068-c92e55ff77b5 = false
2acc5a28-0ada-4926-a2f5-905595ef251c = true
6f4cffec-162a-4290-b038-8622804f09f0 = true
这说明重启后配置恢复结果只保留了部分新 profile,同 app_id=cli_aabb89d590395cd2 的目标 profile 和 a322f95e-... 都没有被保留。
/feishu/info 还是 200/feishu/info 有额外兜底逻辑,可以从 keychain JSON 格式恢复 app_id/app_secret,所以它能显示 profile 信息:
feishu info: resolved from keychain (JSON format)
但 conversation 的授权维护走的是 ResolveWithRefresh,该路径先依赖 config.json 找 profile,profile 不在 config 里就直接失败。
注册写配置时,是按 profile name 追加 app 配置:
existing.Apps = append(existing.Apps, core.AppConfig{
Name: profileName,
AppId: appID,
AppSecret: secret,
Brand: larkBrand,
Users: []core.AppUser{},
})
位置:feishu_cli/pkg/register/register.go
而服务的 Redis 同步器在合并多个 config.json 时按 AppId 去重:
// Dedup by AppId; fallback to Name when AppId is empty.
if a.AppId != "" {
if seen[a.AppId] {
...
continue
}
seen[a.AppId] = true
}
位置:service_gateway/internal/larkcli/syncer.go
这与实际身份模型冲突:业务请求传的是 profile,CLI 查配置也是优先按 Name/ProfileName 查。多个不同 profile 可以共用同一个 app_id,例如日志中:
8753734f-56d4-46ff-9604-ffd2d69b48de -> cli_aabb89d590395cd2
a322f95e-4bbb-4086-a068-c92e55ff77b5 -> cli_aabb89d590395cd2
452ab633-c35c-48b2-9799-f5622cac3870 -> cli_aabb89d590395cd2
因此 mergeConfigJSON 按 AppId 去重会把不同 profile 当成同一个 app 合并,最终导致其中一些 profile 在重启恢复后丢失。
多个 profile 共用同一个 app_id
↓
注册和登录后,本地 config.json 暂时可用
↓
服务停止时 SyncToRedis 合并 config.json
↓
mergeConfigJSON 按 app_id 去重
↓
不同 profile 被误判为重复 app,被跳过或替换
↓
服务重启后从 Redis 恢复到缺 profile 的 config.json
↓
conversation 调用 ResolveWithRefresh 找不到 profile
↓
返回 not_authenticated,表现为掉授权
修改 service_gateway/internal/larkcli/syncer.go 的 mergeConfigJSON:
Name/ProfileName 作为主去重键。AppId 相同就丢弃不同 profile。Users 更完整或更新时间更新的配置。增加测试覆盖:
输入:
profile A: name=8753734f..., appId=cli_aabb89d590395cd2
profile B: name=a322f95e..., appId=cli_aabb89d590395cd2
期望:
mergeConfigJSON 后 A 和 B 都保留
还应覆盖:
同名 profile + 不同 users:保留 users 更完整的版本
同名 profile + 新旧 config:按更新时间或字段完整度合并
不同 name + 同 appId:必须全部保留
让 maintainFeishuUserToken / ResolveWithRefresh 支持 keychain fallback,至少在 config.json 丢 profile 时能通过 profile-based keychain token 恢复,避免 info 成功但 conversation 失败的不一致体验。
同步器合并 config.json 时建议输出:
before_apps
after_apps
dedup_by
dropped_profiles
duplicate_app_ids
尤其是 dropped_profiles,可以直接暴露这类问题。
短期可以重新 register/login 目标 profile,或者手动把目标 profile 恢复到容器内 /root/.lark-cli/config.json 并同步到 Redis。
但只做恢复不能根治。只要 mergeConfigJSON 继续按 AppId 去重,后续服务重启或多副本同步时仍可能再次丢 profile。