# ADR-0009:把平台登录从"只在聊天里"搬进设置页 > **这个文件解决什么问题**:本插件早期刻意**不**在设置界面收集平台凭据(账号页只读,凭据一律在聊天里由模型转交工具)。现在改成界面里可以登录,安全承诺跟着变了。这里记录为什么改、改的边界在哪、以及为守住这条边界做了什么。 - **Status**:Accepted - **Date**:2026-09-26 - **相关**:[ADR-0006](0006-api-and-origin.md)(同源 API 与 Origin 校验)、[ADR-0007](0007-open-source-boundary.md)(闭源子进程边界)、[`docs/verify-platform-login.md`](../verify-platform-login.md)(验证证据) --- ## Context 0.1/0.2 的形态是:设置界面里的「账号总览」**只读**,任何登录动作都在聊天里让模型调用工具完成。当时给出的理由是"避免把凭据收集到设置界面"。 真实使用后这个理由站不住了: 1. **聊天不是表单。** 让用户在自己的对话里打出美的账号密码,凭据会同时进入:模型上下文、会话记录、任何日志或导出。设置界面里的密码框(`type="password"`、提交即清空、走同源 POST)反而**暴露面更小**。 2. **涂鸦之外的四家没法在聊天里可靠走完。** 米家 OAuth 的回调地址是一段 URL、华为是"账号密码 → 设备挑战码"两步且需要区分两种失败、美的/易微联有区域/云端枚举 —— 这些让用户用自然语言描述,全靠模型理解,成功率低且每次不一样。用户最初的需求原话就是"目前是显示如下文字,用户没办法操作"。 3. **参考实现(`app/feyagate-desktop`)本来就是图形化登录页。** 这是产品的既有形态,不是新发明。 ## Decision **登录动作搬进设置页新增的「平台登录」标签,聊天的工具调用保持可用**(两条路调同一批子进程工具,不新增接口)。 同一批子进程工具,插件只做翻译与呈现。为守住"设置页不成为凭据仓库"这条线,同时接受四条硬约束: 1. **凭据只经过本机回环,插件不留存。** 账号/密码/验证码在宿主半侧只存在于一次函数调用的参数里,直接转发给 `127.0.0.1:<子进程端口>`;**不写文件、不写日志缓冲、不回显**。`scripts/check-platform-auth.mjs` 用真实 API handler 断言密码不出现在插件日志、`/logs` 响应、`state.json`、`/account/overview` 四处。 2. **写操作一律 Origin 校验**(沿用 [ADR-0006](0006-api-and-origin.md) 的 `checkOrigin`);登录、退出、提交验证码都是 POST,来自其他 Origin 的请求直接 403。只有二维码图片/文本是 GET(聊天里 `` 要用,本来也没有凭据)。 3. **能力由子进程协商,插件不写死平台清单。** `GET /auth/capabilities` 把子进程 `tools/list` 与本 UI 认识的授权工具求交集。后台服务没有的平台(本机 v1.2.19 没有华为)就如实显示"本构建不支持",而不是给一个必然失败的按钮;后台服务升级到 v1.2.20 后该卡片自动出现,**插件不需要跟着发版**。 4. **失败要说清是哪种失败。** 授权过期(`capability_denied`)与密码错误、验证码错误与"验证码已通过只是换令牌失败"(`retry_without_code`)都分流成不同文案 —— 否则用户会反复重输一个正确的密码或白费一个验证码。 ## Consequences **好的:** - 五家平台都能点着走完;涂鸦二维码不再依赖客户端会不会渲染图片;米家把"粘贴整段回调地址"这一唯一的操作负担包成了一个输入框(插件自己从 URL 里抠 `code`)。 - 凭据暴露面从"模型上下文 + 会话记录"缩小到"一次本机 HTTP 请求"。 - 能力协商让插件**跨版本**可用:它同时适配还没有华为工具的旧后台服务与将来新增平台的新后台服务。 **代价与仍然存在的风险:** - 插件仍然**经手**明文凭据(只是不留存)。威胁模型里"本机进程被攻破"这一档没有变化 —— 绑 `127.0.0.1` 是唯一防线,把 `bindAddress` 改成 `0.0.0.0` 时界面会给警告,但**登录功能的前提仍然是只绑回环**。 - 子进程自己会把美的/易微联的账号口令**落盘到它自己的 token 文件**(上游实现如此,`~/.dsh/dsh-feyagate/data/`),这份明文不在本插件的责任范围内 —— 但用户问"我的密码存在哪"时必须如实回答,README 与使用手册都写了。 - 米家**没有**退出登录:上游 `XiaomiProvider::logout()` 有实现但既无 MCP 工具也无 REST 路由。界面不提供该按钮,并显示原因(501),而不是假装成功。 ## Alternatives considered - **保持只在聊天里登录。** 被真实使用否决(见 Context)——用户拿不到可操作的入口就是原需求里的故障。 - **界面只做跳转,凭据仍由聊天收集。** 半成品:用户还是要先学会怎么跟模型描述账号密码,且凭据照样进模型上下文。 - **插件自己存一份凭据/令牌。** 直接否决:子进程已经是凭据的唯一所有者(它要自己刷新令牌),插件再存一份只会制造第二份明文和一致性问题。 - **顺手用 REST 形态调华为**(`/api/v1/platform/huawei/*`)。否决:桌面端驱动的是 MCP 工具(`auth/huawei_login` / `auth/huawei_challenge`),REST 那套没人调用、且只在 ≥v1.2.20 存在;多一条需要版本探测的路径没有收益。