docs(plan): 设备&会话管理 实现计划 P1-P6(md执行源 + html阅读版 + 登记 + 链接)

新增 docs/superpowers/plans/2026-06-29-device-session-management.md(执行真相,
checkbox) + docs/device-session-management-plan.html(阅读版);登记进 index.html
「实现计划」;总方案顶部加「→ 实现计划」链接。P1 设备注册打通→P2 sessions表+
在线→P3 强制退出/清除→P4 每设备流量→P5 2FA(future)→P6 UI重做。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 00:15:35 +08:00
parent 7fd2fed441
commit 889cff4556
4 changed files with 208 additions and 1 deletions
+1 -1
View File
@@ -90,7 +90,7 @@
<a class="back" href="index.html">← 文档索引</a>
<h1>设备 &amp; 会话管理 + 每设备流量归因<span class="small">(设计方案)</span></h1>
<p class="sub">Pangolin · 「我的设备」管理 + 控制面会话 + 数据面按设备记账 · 2026-06-28</p>
<p class="sub">Pangolin · 「我的设备」管理 + 控制面会话 + 数据面按设备记账 · 2026-06-28 · <a href="device-session-management-plan.html"><b>→ 实现计划 P1P6</b></a></p>
<div class="lead">
一句话:<b>后端绝大部分已经建好,只是「没接线」</b><code>devices</code> 表、每设备数据面凭证 <code>dp_uuid</code>
+134
View File
@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备 & 会话管理 + 每设备流量归因(实现计划 P1–P6)</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:920px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:29px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
.sub{color:var(--fg2);font-size:15px;margin:0 0 28px}
h2{font-size:20px;margin:42px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
.tag.future{background:rgba(168,175,189,.16);color:var(--fg2)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin:14px 0}
.card.p{border-left:3px solid var(--accent)}
.card h3{margin:0 0 4px;font-size:17px;color:#fff}
.card .goal{color:var(--accent2);font-size:13.5px;margin:0 0 8px}
ul{padding-left:20px;margin:8px 0}
li{margin:4px 0}
li.acc{list-style:none;margin-left:-20px;margin-top:8px;color:var(--ok);font-weight:600;font-size:13.5px}
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
.small{color:var(--fg2);font-size:13px}
a{color:var(--accent2)}
.back{display:inline-block;margin-bottom:24px;font-size:13px}
hr{border:none;border-top:1px solid var(--border);margin:36px 0}
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="index.html">← 文档索引</a>
<h1>设备 &amp; 会话管理 + 每设备流量归因 · 实现计划<span class="small"> P1P6</span></h1>
<p class="sub">阅读版 · 执行真相源(含 checkbox)为 <code>docs/superpowers/plans/2026-06-29-device-session-management.md</code> · 2026-06-29</p>
<div class="lead">
设计见 <a href="device-session-management-design.html">设备 &amp; 会话管理 总方案</a>。核心仍是「<b>打通已建未接线的链路 + 新增 sessions 表 + UI 重做</b>」,拆成 6 期、每期一刀可独立验收。
约束:SQL 中性方言(<code>dialect.Upsert</code>、无 <code>NOW()</code>);UI 走 token 单源;不碰 CI-gated golden 的 Linux 基线;每期 build+test 过即 commit。
</div>
<h2>已核的代码事实(计划依据)</h2>
<ul>
<li><code>devices.Service.RegisterIfAbsent</code> 已完整实现但<b>无人调用</b><code>normalizePlatform</code> 拒 linux<code>DeleteDevice</code> 已调 <code>CredentialRevoker</code>(当前 NoopRevoker<b>按 user 不按 device</b>)。</li>
<li><code>TokenManager.Issue/Refresh</code> 内部生成 refresh JTI、<b>不外露</b> → 绑 session 须 surface JTI。</li>
<li><code>auth.Handler</code> 的 login/register 请求体需加 <code>device</code> 字段;<code>clientIP(r)</code> 已有(供 session IP)。</li>
<li><code>auth.NewService(...)</code> 是注入点;<code>nodeSvc.Hub()</code> 将充当 per-device 凭证吊销器。</li>
<li>客户端依赖:<code>flutter_secure_storage</code><code>package_info_plus</code>✓,<b><code>uuid</code><code>device_info_plus</code></b>device_id 现硬编码 <code>'mac-001'</code></li>
</ul>
<h2>分期</h2>
<div class="card p">
<h3>P1 · 设备注册打通(登录即注册设备)</h3>
<p class="goal">目标:登录/注册成功即写 devices 表 → 「我的设备」显示真实设备。</p>
<ul>
<li>客户端加依赖 <code>uuid</code> + <code>device_info_plus</code>;新 <code>device_identity.dart</code>(生成+持久 device_id、设备名/平台、客户端版本);弃用硬编码 <code>'mac-001'</code></li>
<li>login/register 请求体加 <code>device:{id,name,platform,client_version}</code></li>
<li>后端 <code>auth/handler.go</code> 加 device 字段;<code>auth/service.go</code> 成功签发后调 <code>RegisterIfAbsent</code>(注入 <code>DeviceRegistrar</code> 接口,返回 deviceID 供 P2);<code>normalizePlatform</code> 加 linux<code>main.go</code> 注入。</li>
<li class="acc">✓ 验收:登录后 <code>GET /v1/me/devices</code> 非空、含真实设备名/平台</li>
</ul>
</div>
<div class="card p">
<h3>P2 · sessions 表 + 在线/最后登录</h3>
<p class="goal">目标:会话落库绑设备;列表显示在线/客户端版本/最后登录。</p>
<ul>
<li>migration <code>000016</code>:建 sessions 表;devices 加 <code>client_version</code>+<code>totp_trusted_until</code>;唯一键 <code>UNIQUE(uuid)</code><code>UNIQUE(user_id,uuid)</code>platform CHECK 加 linux。</li>
<li><code>server/internal/sessions/</code> StoreCreate/RotateJTI/RevokeByJTI/RevokeByDevice/…)。</li>
<li><code>TokenManager</code> 外露 JTI<code>auth.Service</code> 注入 sessions:登录 Create、刷新 RotateJTI+last_active、登出 RevokeByJTI。</li>
<li><code>ReportUsage</code> touch <code>devices.last_seen</code>(在线判定);<code>ListDevices</code> 返回 online/last_login/client_version;客户端 Device model 加字段。</li>
<li class="acc">✓ 验收:列表显示在线/版本/最后登录;停 agent ~3min 转离线</li>
</ul>
</div>
<div class="card p">
<h3>P3 · 两个操作(强制退出 + 清除增强)</h3>
<p class="goal">目标:强制退出(吊销会话留设备)/ 清除(移除+吊销会话+吊销 dp_uuid)。</p>
<ul>
<li>新端点 <code>POST /v1/me/devices/{uuid}/logout</code>ForceLogoutRevokeByDevice + 逐 jti Revoke)。</li>
<li><code>DeleteDevice</code> 增强:吊销 session<code>CredentialRevoker</code><b>per-device</b><code>RevokeDevice(dpUUID)</code><code>nodes.Hub</code> 实现,<code>main.go</code> 替 NoopRevoker)。</li>
<li>客户端 <code>account_api.forceLogout</code> + provider。</li>
<li class="acc">✓ 验收:强制退出→refresh 失败需重登;清除→设备消失 + 数据面凭证作废</li>
</ul>
</div>
<div class="card p">
<h3>P4 · 每设备流量验证(打通 usage_device_daily</h3>
<p class="goal">目标:每设备流量真正写库 + 统计页设备下拉真数据。</p>
<ul>
<li><code>ConnectNode</code> 兜底 <code>RegisterIfAbsent</code>;端到端确认 <code>EnsureDeviceDpUUID</code>→per-device dp_uuid→<code>ReportUsage</code><code>AccumulateDeviceUsage</code> 填表。</li>
<li>统计页设备下拉接真实设备 + <code>deviceUsageProvider</code>(呼应统计整改 #10)。</li>
<li class="acc">✓ 验收:统计页按设备能看到分流量</li>
</ul>
</div>
<div class="card">
<h3>P5 · 2FA 信任设备 <span class="tag future">FUTURE · 本轮不编码</span></h3>
<p class="small" style="margin:4px 0 0">占位:清除登录信息联动 <code>totp_trusted_until</code>(列已在 P2 预留),重登需重做 2FA。待 2FA 主流程就绪后单独排。</p>
</div>
<div class="card p">
<h3>P6 · UI 重做(DevicesScreen,五端)</h3>
<p class="goal">目标:按总方案 §7 视觉稿落地「我的设备」交互。</p>
<ul>
<li><code>DevicesScreen</code> 重做:名称/平台·版本/在线绿点/最后登录 + 「本机」标 + 行尾 ⋯ 菜单(强制退出/清除·危险红)+ 危险二次确认弹窗;颜色/字号走 token。</li>
<li>宽/窄屏适配;空/加载态;l10nzh+en)新增文案;DevicesScreen 新 goldenMac 生成,非 CI-gated)。</li>
<li class="acc">✓ 验收:截图与 §7 视觉稿一致,五端生效(Flutter 单码)</li>
</ul>
</div>
<hr>
<h2>验收(整体)</h2>
<ul>
<li>后端 <code>go build ./... &amp;&amp; go test ./...</code>(含 sqlite 实库)+ <code>./server/run_sqlite_test.sh</code>;客户端 <code>flutter analyze</code> + <code>flutter test</code></li>
<li>端到端(本机 + pangolin1):登录→设备列表;停 agent→离线;强制退出→refresh 失败;清除→消失+凭证作废;走流量→<code>usage_device_daily</code> 有行。</li>
<li><span class="tag warn">生产</span> 线上 <code>000016</code> 迁移在 pangolin1 跑 <code>pangolin-migrate up</code> —— 生产写操作,执行前单独向用户确认。</li>
</ul>
<p class="small">关联:<a href="device-session-management-design.html">设备 &amp; 会话管理 总方案</a> · <a href="stats-overhaul-plan.html">统计体系整改(#5</a>P4 设备下拉)。</p>
</div>
</body>
</html>
+5
View File
@@ -66,6 +66,11 @@
</a>
<h2>实现计划 / Plans</h2>
<a class="doc" href="device-session-management-plan.html">
<div class="t">设备 &amp; 会话管理 + 每设备流量归因 实现计划(P1–P6)<span class="tag html">HTML</span></div>
<div class="d">阅读版;执行真相源为 <code>docs/superpowers/plans/2026-06-29-device-session-management.md</code>(含 checkbox)。P1 设备注册打通 → P2 sessions表+在线/最后登录 → P3 强制退出/清除 → P4 每设备流量 → P5 2FA信任(future) → P6 UI重做。</div>
<div class="path">docs/device-session-management-plan.html</div>
</a>
<a class="doc" href="superpowers/plans/2026-06-22-android-client.md">
<div class="t">Android 客户端实现计划 <span class="tag md">MD</span></div>
<div class="d">11 个任务:Go 1.24.3 工具链 → 构建 libbox.aar → 解析真实 API → Dart 接线 → 修原生编译 → 模拟器端到端 → 统计 → 切节点 → KillSwitch → 真机验证。</div>
@@ -0,0 +1,68 @@
# 设备 & 会话管理 + 每设备流量归因 —— 实现计划(P1–P6)
> 执行真相源(checkbox)。设计见 `docs/device-session-management-design.html`;阅读版 `docs/device-session-management-plan.html`。
> SQL 走中性方言(`dialect.Upsert`、无 `NOW()`/`UTC_TIMESTAMP()`);UI 走 token 单源;不碰 CI-gated golden 的 Linux 基线。每期 build+test 过即 commit。
## 计划文档(先产出)
- [x] `docs/superpowers/plans/2026-06-29-device-session-management.md`(本文件)
- [ ] `docs/device-session-management-plan.html`(阅读版)
- [ ] 登记 `docs/index.html`「实现计划」
- [ ] 总方案 `device-session-management-design.html` 加「→ 实现计划」链接
## P1 · 设备注册打通(登录即注册设备)
- [ ] 客户端加依赖 `uuid` + `device_info_plus``client/pubspec.yaml`
- [ ]`client/lib/services/device_identity.dart``deviceId()`secure storage 读;无则 `Uuid().v4()` 写;读失败 ≠ 没有,不重生成)、`deviceName()`/`platform()``clientVersion()`
- [ ] 弃用 `connection_provider.dart``_kDeviceId='mac-001'` → 用 `device_identity`
- [ ] `auth_api.dart` login/register 请求体加 `device:{id,name,platform,client_version}`
- [ ] `auth/handler.go``loginRequest`/`registerRequest``Device deviceMeta`
- [ ] `auth/service.go` Login/Register:成功签发后调 `DeviceRegistrar.RegisterIfAbsent`consumer-side 接口;`devices.Service` 实现;`RegisterIfAbsent` 增返回 `deviceID`);`MaxDevices` 由 plan 解析
- [ ] `normalizePlatform``linux`
- [ ] `main.go` 构造 `devices.Service` 注入 `authSvc`
- [ ] 测试:`device_identity` 单测;auth 集成(登录后 devices 落行)
- [ ] 验收:登录后 `GET /v1/me/devices` 非空
## P2 · sessions 表 + 在线/最后登录
- [ ] migration `000016_sessions_and_device_meta`mysql+sqlite,含 down):建 sessions 表;devices 加 `client_version`+`totp_trusted_until`;唯一键 `UNIQUE(uuid)``UNIQUE(user_id,uuid)`platform CHECK 加 `linux`
- [ ]`server/internal/sessions/``Store`Create/RotateJTI/RevokeByJTI/RevokeByDevice/ActiveByUserWithDevice/LastLoginByDevice
- [ ] `TokenManager.Issue`/`Refresh` 外露 refresh JTI
- [ ] `auth.Service` 注入 `sessions.Store`Login/Register `Create`Refresh `RotateJTI`+last_activeLogout `RevokeByJTI`
- [ ] `nodes/handler_grpc.go: ReportUsage` deviceID>0 时 touch `devices.last_seen`
- [ ] `devices.Service.ListDevices`+API struct+store join:增 `client_version`/`online`/`last_login`
- [ ] 客户端 `Device` model 加 `clientVersion`/`online`/`lastLogin``account_api` 解析
- [ ] 测试:sessions storeJTI surfaceReportUsage touchListDevices 计算;migration sqlite 实库
- [ ] 验收:列表显示在线/版本/最后登录;停 agent ~3min 转离线
## P3 · 两个操作(强制退出 + 清除增强)
- [ ] 新端点 `POST /v1/me/devices/{uuid}/logout`handler + `Service.ForceLogout`):`sessions.RevokeByDevice` + 逐 jti `TokenManager.Revoke`
- [ ] `DeleteDevice` 增强:加 `sessions.RevokeByDevice``CredentialRevoker` 改 per-device
- [ ] `CredentialRevoker` 接口改 `RevokeDevice(ctx, dpUUID)``nodes.Hub` 实现;`main.go` 注入 hub 替 NoopRevoker
- [ ] 客户端 `account_api.forceLogout` + `devicesProvider.forceLogout`
- [ ] 测试:ForceLogout 删 jti+标 revoked_atDeleteDevice per-device revoke
- [ ] 验收:强制退出→refresh 失败;清除→设备消失+凭证作废
## P4 · 每设备流量验证
- [ ] `ConnectNode` 兜底 `RegisterIfAbsent`device 缺失时)
- [ ] 端到端确认 `usage_device_daily` 填充
- [ ] 统计页设备下拉接真实设备 + `deviceUsageProvider`#10
- [ ] 测试:集成(注册→连接→上报→有行);统计页设备下拉 widget
- [ ] 验收:统计页按设备看分流量
## P5 · (未来)2FA 信任设备 —— 本轮不编码
- 占位:清除联动 `totp_trusted_until`P2 已预留列)。
## P6 · UI 重做(DevicesScreen,五端)
- [ ] `DevicesScreen` 重做:名称/平台·版本/在线绿点/最后登录 + 本机标 + 行尾 ⋯ 菜单 + 危险确认弹窗;走 token
- [ ] 宽/窄屏适配;空/加载态
- [ ] l10n`app_text.dart`+`strings_{zh,en}.dart` 新增文案
- [ ] goldenDevicesScreen 新 goldenMac 生成,非 CI-gated
- [ ] 测试:widget(在线/离线/菜单/确认/调用)
- [ ] 验收:与 §7 视觉稿一致,五端生效
## Verification
- 后端 `go build ./... && go test ./...`(含 sqlite 实库)+ `./server/run_sqlite_test.sh`
- 客户端 `flutter analyze` + `flutter test`
- 端到端:登录→设备列表;停 agent→离线;强制退出→refresh 失败;清除→消失+凭证作废;走流量→`usage_device_daily` 有行
- **线上 000016 迁移在 pangolin1 跑 `pangolin-migrate up` —— 生产写,执行前单独确认**
## 不在本轮
- P5 只占位;存量空 devices 无需回填(新登录自然铺开)