refactor(weixin-ilink): 措辞纠正 —「会话过期」→「会话被 iLink 拒绝」(零逻辑改动)
事实(主人 2026-10-03 确认):iLink 的 bot token 是长期凭据(一个月前扫码的仍可用);errcode=-14 是 iLink 侧拒绝/作废该会话,不是时间性过期;且至今未观测到真实 -14(当日全部日志 0 次)。此前代码把这些写成「过期」,属沿用的旧命名,现纠正。 - 纯改名/文案:ErrSessionExpired→ErrSessionRejected、errSessionExpired→errcodeSessionRejected、expired/expiredToken→rejected/rejectedToken、markExpired/expiredLocked/expiredState→markRejected/rejectedLocked/rejectedState;409 错误码 session_expired→session_rejected - 行为一字未改:显式标记 / Connect 不假 OK / 面板 needsRescan+lastError / 扫码或换新 token 即清除 全部保留 - 微信协议自身的「二维码过期」与 auth 的 JWT/refresh 过期一律未动(那些是真·时间性过期) - Docs/api-client v0.39 补事实说明;顺手收尾 3 处残留注释(admin.go / channelbridge/inbound.go / readloop_test.go)
This commit is contained in:
1 parent
cdc370dc82
commit
a51aadeef4
14 files changed
+205
-192
No files matched your search
+5
-4
@@ -124,7 +124,7 @@
|
||||
| 4 | `/api/v1/admin/plugins` | GET | ✅ | 插件/能力列表(含 MCP/渠道) |
|
||||
| 5 | `/api/v1/admin/plugins/policy` | POST | ✅ | 写工具策略(owner-only,审计) |
|
||||
| 5a | `/api/v1/admin/plugins/installed` | GET | ✅ | **已装运行时插件列表**(owner/admin 读):`{data:[{id,name,kind,transport,endpoint?,command?,args?,apiKey?,env?,createdAt?,live,toolCount}]}`,`apiKey`/敏感 `env` 值**掩码**(如 `****1234`),`live` = 是否已连接注册,`toolCount` = 该 server 注册工具数 |
|
||||
| 6 | `/api/v1/admin/channels` | GET | ✅ | 渠道列表/健康 + 每平台已配账号(脱敏 token 掩码,design/04 §3.1);已连接渠道的账号附 **bot 身份**(`botId`/`botUsername`/`botNickname`,如 KOOK `GET /api/v3/user/me` 自动获取);适配器要求重新授权时附 **`needsRescan`** + **`lastError`**(v0.39,仅微信 iLink 过期 token) |
|
||||
| 6 | `/api/v1/admin/channels` | GET | ✅ | 渠道列表/健康 + 每平台已配账号(脱敏 token 掩码,design/04 §3.1);已连接渠道的账号附 **bot 身份**(`botId`/`botUsername`/`botNickname`,如 KOOK `GET /api/v3/user/me` 自动获取);适配器要求重新授权时附 **`needsRescan`** + **`lastError`**(v0.39,仅微信 iLink 返回 `errcode=-14`、会话被 iLink 侧拒绝这一种) |
|
||||
| 7 | `/api/v1/admin/iot` | GET | ✅ | IoT 设备列表 |
|
||||
| 8 | `/api/v1/admin/users` | GET/POST | ✅(GET) | 用户/角色/API key(GET 已实现;POST 创建用户/签发 key 随 M10 落地) |
|
||||
| 9 | `/api/v1/admin/llm` | GET/POST | ✅ | LLM 多厂商多模型(models.json):GET 列表(APIKey 脱敏,含 `provider` 协议与 `maxTokens` 预算);POST 保存组→落盘 `models.json`→**热更新**运行中的 LLM stack(design/04 §3.1,无需重启) |
|
||||
@@ -160,9 +160,10 @@
|
||||
- `DELETE /admin/automations/{id}` → `200 {data:{id,deleted:true}}`;`404`。
|
||||
- `POST /admin/llm` body `{id?,provider?,baseUrl,apiKey?,model,multimodal?,fallback?,maxTokens?}` → `201 {data:{provider:{id,name,provider,baseUrl,model,multimodal,enabled,fallback?,maxTokens?}, persisted:true, path}}`。**真实持久化**:读-改-写 `models.json`(本地、gitignore),保存组提升为**生效组**(index 0),并**热更新**运行中的 LLM provider(`path` 为落盘文件路径);API key 写入文件但**永不回显/不落日志**。`provider` 为空默认 `openai`;缺 `baseUrl`/`model` 回 `400`;`maxTokens` 为负回 `400`;未注入 LLM stack 回 `500 llm_unavailable`、写盘失败 `500 llm_persist_failed`、热重载失败 `500 llm_reload_failed`。`maxTokens` 缺省/`0` = 未设(回落 `LLM_FALLBACK_MAX_TOKENS` / 适配器默认)。`.env` 为兜底。
|
||||
- `POST /admin/llm/test` body `{provider?,baseUrl,model,apiKey?,prompt?}` → `200 {data:{ok,provider,model,reply,tokens}}`(**真实模型调用**);无效/失败回 `400 invalid_provider` / `500 llm_test_failed`。API key 仅本次请求内使用,不落库、不进 wire。
|
||||
- `POST /admin/channels/open`、`POST /admin/channels/close` body `{name}` → `200 {data:{name,connected}}`(真实 Connect/Disconnect);`404`(未注册渠道)/`409 session_expired`(微信 iLink bot token 已过期,重连无法修复,须先「扫码登录」重新绑定,v0.39)/`409 needs_login`(微信 iLink 尚无 token)/`500`(其它凭据缺失等连接失败)。
|
||||
- `POST /admin/channels/open`、`POST /admin/channels/close` body `{name}` → `200 {data:{name,connected}}`(真实 Connect/Disconnect);`404`(未注册渠道)/`409 session_rejected`(微信 iLink 侧拒绝/作废了该会话,重连无法修复,须先「扫码登录」重新绑定,v0.39)/`409 needs_login`(微信 iLink 尚无 token)/`500`(其它凭据缺失等连接失败)。
|
||||
- `POST /admin/channels/test` body `{platform?,accountId,isGroup}` → `200 {data:{role,owner,isGroup,env,verdict}}`。**结构性判定**:channel 层 actor 元数据 × `configs/users/owner.yaml` 白名单,绝不靠提示词。
|
||||
- **微信 iLink「会话过期」契约**(v0.39,适配器级,非端点):iLink 返回 `errcode=-14`(bot token 过期)时适配器进入**显式过期态**——`IsConnected()` 恒 `false`;`Connect()` **不再返回成功**,而是返回「需要重新扫码」类错误(`POST /admin/channels/open` 对应 `409 session_expired`);`HealthCheck` 同样把「过期」与「未连接」区分开。`GET /admin/channels` 该平台行带 **`needsRescan:true`** + **`lastError`**(人类可读原因,**绝不含 token**)。**清除条件**:扫码登录拿到新 token(`login-status` 报 `confirmed`)或配置里换了新 token(保存后热重载)。过期期间:`login-status` **不**假报 `confirmed` 而是 `failed` + 过期原因(避免面板把死 token 当成功重新落盘);`login-qr` 仍可正常出码(**不**回 `already_logged_in` 挡住重新扫码);`get_bot_qrcode` 不再携带已失效 token(否则服务端会以「已绑定」应答而不发新凭据)。未实现该能力的平台:`needsRescan`/`lastError` **不出现**(`omitempty`),客户端按缺省处理。
|
||||
- **微信 iLink「会话被拒」契约**(v0.39,适配器级,非端点):iLink 返回 `errcode=-14` 时适配器进入**显式被拒态**——`IsConnected()` 恒 `false`;`Connect()` **不再返回成功**,而是返回「需要重新扫码」类错误(`POST /admin/channels/open` 对应 `409 session_rejected`);`HealthCheck` 同样把「被拒」与「未连接」区分开。`GET /admin/channels` 该平台行带 **`needsRescan:true`** + **`lastError`**(人类可读原因,**绝不含 token**)。**清除条件**:扫码登录拿到新 token(`login-status` 报 `confirmed`)或配置里换了新 token(保存后热重载)。被拒期间:`login-status` **不**假报 `confirmed` 而是 `failed` + 被拒原因(避免面板把已作废的凭据当成功重新落盘);`login-qr` 仍可正常出码(**不**回 `already_logged_in` 挡住重新扫码);`get_bot_qrcode` 不再携带已被拒的 token(否则服务端会以「已绑定」应答而不发新凭据)。未实现该能力的平台:`needsRescan`/`lastError` **不出现**(`omitempty`),客户端按缺省处理。
|
||||
- **事实说明(勿误读为「过期」)**:iLink 的 bot token 是**长期凭据**(实测一个月前扫码得到的 token 仍可用),`errcode=-14` 表示 **iLink 侧拒绝/作废了该会话凭据**(解绑、在别处重新登录、协议层拒绝等),**不是时间性过期**;恢复方式为**重新扫码绑定**(或配置换新 token)。当前尚未在真实日志中观测到 `-14`,此处按「被拒」语义处理以防静默变哑。
|
||||
- **KOOK 适配器面板端点**(design/17 §2,**owner-only + 审计**,仅 `kook` 平台实现;其它平台回 `501 not_implemented`,账号未找到回 `404`):这些端点均带可选 `?accountId=` 定位到具体 channels.json 账号(不传=该平台首个账号)。**token/密钥不打印、不落日志**;子项拉取失败不伪造、记入 `errors`。
|
||||
- `GET /admin/channels/{platform}/view?accountId=` → `200 {data:{platform,accountId?,friends?,userChats?,guilds?,channels?,members?,errors?}}`。聚合该账号真实数据(好友/申请/屏蔽、服务器、首个服务器频道与成员、私信会话);`errors` 为 `{子项:原因}` 记录失败的子项。
|
||||
- `GET /admin/channels/{platform}/friends?type=&accountId=` → `200 {data:{friends?,requests?,blocked?}}`。`type` ∈ `friend|request|block`(空=全部),调用 KOOK `GET /friend?type=`。
|
||||
@@ -328,7 +329,7 @@
|
||||
- 语音:上行 `voice_input`(base64 音频或已转写 text),下行 `voice_output`(合成音频 base64 + text)。
|
||||
|
||||
## 5. 变更记录
|
||||
- `v0.39`(**微信 iLink 会话过期可见化 + 不再假 OK**,对客户端**新增两个可选字段、一处错误码**,向后兼容):① **`GET /api/v1/admin/channels` 每平台行新增可选 `needsRescan`(bool,`omitempty`)+ `lastError`(string,`omitempty`)**——仅当适配器**真的**要求重新授权时出现(今天只有微信 iLink 的 `errcode=-14` 过期 token 这一种),取值来自适配器新增的**可选能力接口** `RescanRequired() (bool, string)`,网关**经类型断言**读取(主 `PlatformAdapter` 契约**不变**;未实现该能力的平台字段**缺省不出现**)。用途:把「需要重新扫码」与「主人自己关掉的未连接」区分开——后者仅 `connected:false`。② **`POST /admin/channels/open`** 对「已过期」回 **`409 session_expired`**(message 明确要求用「扫码登录」重新绑定),**不再**吞掉错误回 200;`409 needs_login`(尚无 token,v0.8 起)不变。③ **适配器行为**:iLink `errcode=-14` 进入**显式过期态**——`IsConnected()` 恒 `false`;`Connect()` 返回过期错误(**不再**对已过期 token 报成功);`HealthCheck` 区分「过期」与「未连接」;并以 **`ERROR`** 级日志写明「需要重新扫码登录」(只带 `bot_id` 等非敏感标识,**token 永不入日志**)。过期态在读循环与发送两条路径**都会**置位,并仅在**扫码登录拿到新 token** 或**配置换了新 token**(保存 + 热重载)时清除。④ 扫码登录侧:过期期间 `login-status` 报 **`failed` + 过期原因**(**不再**假报 `confirmed`,否则面板会把死 token 当成功重新落盘);`login-qr` 仍可正常出码(**不**回 `already_logged_in`,否则重新扫码这条路被堵死);`get_bot_qrcode` 不再携带已失效的 token(否则服务端以「已绑定」应答、**不**发新凭据)。**向后兼容**:只新增可选字段与一处更诚实的错误码;不读新字段、不处理 409 的客户端行为不变(此时面板显示「未连接」,仍可用「扫码登录」恢复)。详见 §2 行 6 与「微信 iLink『会话过期』契约」。
|
||||
- `v0.39`(**微信 iLink 会话被拒可见化 + 不再假 OK**,对客户端**新增两个可选字段、一处错误码**,向后兼容):① **`GET /api/v1/admin/channels` 每平台行新增可选 `needsRescan`(bool,`omitempty`)+ `lastError`(string,`omitempty`)**——仅当适配器**真的**要求重新授权时出现(今天只有微信 iLink 返回 `errcode=-14` 这一种),取值来自适配器新增的**可选能力接口** `RescanRequired() (bool, string)`,网关**经类型断言**读取(主 `PlatformAdapter` 契约**不变**;未实现该能力的平台字段**缺省不出现**)。用途:把「需要重新扫码」与「主人自己关掉的未连接」区分开——后者仅 `connected:false`。② **`POST /admin/channels/open`** 对「iLink 已拒绝该会话」回 **`409 session_rejected`**(message 明确要求用「扫码登录」重新绑定),**不再**吞掉错误回 200;`409 needs_login`(尚无 token,v0.8 起)不变。③ **适配器行为**:iLink `errcode=-14` 进入**显式被拒态**——`IsConnected()` 恒 `false`;`Connect()` 返回被拒错误(**不再**对被拒会话报成功);`HealthCheck` 区分「会话被拒」与「未连接」;并以 **`ERROR`** 级日志写明「本次会话已被 iLink 拒绝,需要重新扫码绑定」(只带 `bot_id` 等非敏感标识,**token 永不入日志**)。被拒态在读循环与发送两条路径**都会**置位,并仅在**扫码登录拿到新 token** 或**配置换了新 token**(保存 + 热重载)时清除。④ 扫码登录侧:被拒期间 `login-status` 报 **`failed` + 被拒原因**(**不再**假报 `confirmed`,否则面板会把已作废凭据当成功重新落盘);`login-qr` 仍可正常出码(**不**回 `already_logged_in`,否则重新扫码这条路被堵死);`get_bot_qrcode` 不再携带已被拒的 token(否则服务端以「已绑定」应答、**不**发新凭据)。**向后兼容**:只新增可选字段与一处更诚实的错误码;不读新字段、不处理 409 的客户端行为不变(此时面板显示「未连接」,仍可用「扫码登录」恢复)。**语义澄清(本次仅措辞,不改契约形状、不另起版本号——v0.39 尚未被任何客户端采用)**:iLink 的 bot token 是**长期凭据**(实测一个月前扫码得到的 token 仍可用),`errcode=-14` 表示 **iLink 侧拒绝/作废了该会话凭据**,**不是时间性过期**。详见 §2 行 6 与「微信 iLink『会话被拒』契约」。
|
||||
- `v0.38`(**B4 客户端共享枚举权威表 + 未知值回落约定**,对客户端**新增一个只读 REST 端点**):新增 **`GET /api/v1/meta/enums`**(**任意已认证角色可读**,与 `GET /sessions` 同级;无 token `401`)→ `200 {data:{platforms,moods,modes,unknown}}`,每项 `{id,label}`:① **`platforms`**(6)= `web`/`kook`/`qq-obv11`/`weixin-ilink`/`telegram`/`webhook`——即各内置适配器 `Name()` + Web 主会话来源 `web`;② **`moods`**(5)= `calm`/`happy`/`excited`/`sad`/`angry`,与 persona 情绪状态机内置状态一致(该状态机**可扩展**,故非封闭集合);③ **`modes`**(9)= `chat`/`code`/`home`/`work`/`docs`/`voice`/`vision`/`agents`/`settings`(design/07 §1);④ **`unknown`** 恒 `"display-raw"` = **未知值回落约定机器可读**:客户端遇不在表中的取值**原样显示原始字符串**(**不丢弃、不报错、不占位符**),**服务端新增取值视为非破坏性变更**(forward-compatible)。**单一事实来源在 Go 侧**(`internal/contract/enums`),并有**反漂移测试**断言适配器 `Name()` / 情绪内置状态 / 前端九模式与三张表一致,漏登记即 CI FAIL。Web 侧镜像 `frontend/web/src/lib/enums.ts`(`labelFor` 未知值返回原值),顶栏/侧栏/消息气泡的「心情」标签已改用它渲染。**向后兼容**:仅新增端点,既有契约形状不变。详见 §1 行 15 与 §1.1。
|
||||
- `v0.37`(**D4 未读状态 / 已读游标**,对客户端**新增一个语义字段、一个 REST 端点、一个下行事件**):① `chat_sessions` 新增 **`read_at INTEGER NOT NULL DEFAULT 0`**(迁移 **0010**,down 可逆;**毫秒 Unix 时间戳**,与 `chat_messages.created_at` **同单位**)。② **语义**:**会话级单一已读游标 + 单 owner 账号**——同一账号所有端共用一个游标,**读一处 = 全端已读**(不做 per-device/per-user 游标,与「同一账号所有浏览器都属于同一 owner」一致);`createdAt <= readAt` 的消息算已读,未读数 = `createdAt > readAt` 的条数。③ **`GET /api/v1/sessions` 每项新增恒返回的 `unreadCount`**(整数,0 = 无未读):与 `lastMessage` **同一条 SQL 派生**(关联子查询 `COUNT(*) WHERE session_id = s.id AND created_at > s.read_at`,走已有 `idx_chat_msgs_session_created`,**无 N+1、不新增索引**)。④ **新增 `POST /api/v1/sessions/{id}/read`**(owner/admin,与 `PATCH /sessions/{id}` 同鉴权):body `{before?}`(可选毫秒;缺省= 现在)→ `200 {data:{sessionId,readAt}}`;`before` 为 0/负数/非数字/body 非对象 → `400`,未知会话(含隔离 `sess-xxx`)→ `404`,未注入 HistoryStore → `501`。**游标只前进不后退**(`read_at = MAX(read_at, before)`),乱序/并发/重试请求幂等安全。⑤ **新增下行事件 `session_read`**:`{type:"session_read", sessionId, data:{readAt}}`,全连接广播 + 客户端按 `sessionId` 过滤,用于同账号其它端清红点;失败路径不广播(§3.9)。⑥ **升级回填**:0010 把既有会话游标回填为「该会话现有最新消息时间」,**升级当天不误报未读**(既有历史全部视为已读)。⑦ `HistoryStore`(网关消费侧契约)新增 `MarkSessionRead(ctx, sessionID, before)`;`SessionMeta` 新增 `UnreadCount`。**向后兼容**:仅新增字段/端点/事件,既有契约形状不变;不消费 `unreadCount`/`session_read` 的客户端行为不变。详见 §1 行 4/8a 与 §3.9。
|
||||
- `v0.36`(**A1 流式帧附加会话 / 回合标识**,对客户端**新增两个可选字段、无端点变更**):结构化流帧 `message.start/delta/end`、`thinking.start/delta/end`、`tool_call.start/delta/end`、`tool_execution.update` 现在带 `sessionId` + `turnId`(**向后兼容**:只新增帧字段,`data` 结构与既有事件形状不变)。`sessionId` 由 loop 在**回合边界**从运行上下文注入(模型适配器在 loop 之下、天然不感知会话,保持零值;loop 外无会话上下文时该字段为空);`turnId` 由网关**每回合**新生成(`newTurnID`)并经 `loop.WithTurnID` 挂进运行上下文,`runTurn` 是编排层与 legacy 直连路径**唯一**的回合收口点,两路都覆盖——同一会话同时只有一个 in-flight 回合,故同轮内所有帧的 `turnId` 一致。此前这几帧属于 design/16 §4.2 标注的「若知道就带上」缺口(v0.10 起记录):**多会话并发流式时前端会把别的会话的 delta 接进当前会话气泡**,现在客户端按 `sessionId === activeSessionId` 过滤即可路由到激活会话。`stream_chunk` / `stream_end` 的 `sessionId`/`turnId` 行为不变。详见 §3.1。
|
||||
|
||||
Reference in new issue
Block a user