@uzooai/anime-ai-cli 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.agents/skills/uzoo-anime-cli/SKILL.md +83 -0
  2. package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +51 -0
  3. package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
  4. package/.agents/skills/uzoo-anime-cli/references/commands.md +250 -0
  5. package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +152 -0
  6. package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +175 -0
  7. package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +67 -0
  8. package/.agents/skills/uzoo-anime-cli/references/material-graph-workflow.md +123 -0
  9. package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +95 -0
  10. package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +94 -0
  11. package/.agents/skills/uzoo-anime-cli/references/remake-workflow.md +54 -0
  12. package/.agents/skills/uzoo-anime-cli/references/repaint-workflow.md +300 -0
  13. package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +41 -0
  14. package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +71 -0
  15. package/CHANGELOG.md +92 -0
  16. package/LICENSE +21 -0
  17. package/README.md +242 -3
  18. package/assets/README.md +4 -0
  19. package/assets/favicon-uzoo.png +0 -0
  20. package/assets/logo-uzoo.svg +7 -0
  21. package/docs/CLI/345/275/223/345/211/215/350/214/203/345/233/264/344/270/216/346/234/254/345/234/260/346/211/223/345/214/205-20261103.md +134 -0
  22. package/docs/CLI/346/216/210/346/235/203/346/216/245/345/217/243/344/270/216/344/273/244/347/211/214/350/207/252/345/212/250/345/210/267/346/226/260/350/256/276/350/256/241.md +540 -0
  23. package/docs/CLI/347/274/272/345/217/243/346/240/270/345/257/271/344/270/216/346/224/266/345/260/276-20261105.md +65 -0
  24. package/docs/CLI/347/275/221/351/241/265/344/277/235/345/255/230/350/212/202/347/202/271/347/274/272/345/244/261/344/272/244/346/216/245.md +52 -0
  25. package/docs/M0-M5/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +89 -0
  26. package/docs/npm/347/272/277/344/270/212/345/217/221/345/270/203/344/270/216/345/256/211/350/243/205/344/275/277/347/224/250.md +151 -0
  27. package/docs//344/270/213/350/275/275/346/216/245/345/217/243/351/200/202/351/205/215/344/270/216/351/252/214/346/224/266.md +107 -0
  28. package/docs//344/270/232/345/212/241/350/203/275/345/212/233/350/241/245/345/205/250/344/270/216Skill/347/273/223/346/236/204/350/247/204/345/210/222.md +311 -0
  29. package/docs//345/205/250/345/261/200/351/231/204/344/273/266/350/265/204/346/272/220/345/234/260/345/235/200/344/270/216/346/250/241/345/236/213/350/257/273/345/217/226/345/267/256/345/274/202.md +47 -0
  30. package/docs//345/206/205/351/203/250Windows/345/256/211/350/243/205/344/270/216/345/215/207/347/272/247/351/252/214/346/224/266/346/270/205/345/215/225.md +34 -0
  31. package/docs//345/233/276/347/211/207/346/212/240/345/233/276/346/216/245/345/205/245/344/270/216/351/252/214/346/224/266-20261104.md +110 -0
  32. package/docs//345/274/200/345/217/221/350/256/241/345/210/222.md +127 -0
  33. package/docs//345/274/202/346/255/245/346/212/240/345/233/276CLI/346/235/203/351/231/220/344/272/244/346/216/245-20261105.md +50 -0
  34. package/docs//345/274/202/346/255/245/346/212/240/345/233/276/347/234/237/345/256/236/351/252/214/346/224/266-20261105.md +44 -0
  35. package/docs//346/226/260/344/274/232/350/257/235/344/273/273/345/212/241/344/272/244/344/273/243-/351/230/266/346/256/2652A.md +79 -0
  36. package/docs//346/226/260/351/203/250/347/275/262/347/224/273/345/270/203/345/206/262/347/252/201/351/252/214/346/224/266-20261102.md +61 -0
  37. package/docs//346/231/256/351/200/232/346/265/217/350/247/210/345/231/250/347/231/273/345/275/225.md +53 -0
  38. package/docs//346/255/243/345/274/217/347/253/231/345/210/207/346/215/242/344/270/216/351/252/214/346/224/266-20261104.md +90 -0
  39. package/docs//347/224/273/345/270/203CLI/344/270/213/344/270/200/351/230/266/346/256/265/345/274/200/345/217/221/350/256/241/345/210/222.md +136 -0
  40. package/docs//347/224/273/345/270/203CLI/351/235/236/350/275/254/347/273/230/345/256/214/345/226/204/350/256/241/345/210/222.md +183 -0
  41. package/docs//347/224/273/345/270/203/350/203/275/345/212/233/345/245/221/347/272/246/346/240/270/345/257/271.md +59 -0
  42. package/docs//347/234/237/345/256/236/344/270/232/345/212/241/346/216/245/345/205/245/344/270/216/346/265/213/350/257/225.md +1004 -0
  43. package/docs//347/264/240/346/235/220/345/205/245/345/272/223/344/270/216/347/224/273/345/270/203/344/277/235/345/255/230/351/227/256/351/242/230/345/256/232/344/275/215.md +52 -0
  44. package/docs//351/230/266/346/256/2653B/351/252/214/346/224/266/344/272/244/346/216/245.md +44 -0
  45. package/docs//351/230/266/346/256/265/344/270/200/351/252/214/346/224/266.md +60 -0
  46. package/docs//351/235/236/350/275/254/347/273/230/346/214/201/347/273/255/345/256/214/345/226/204/344/270/216/351/252/214/346/224/266-20261103.md +76 -0
  47. package/docs//351/235/236/350/275/254/347/273/230/347/254/254/344/272/214/346/211/271/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +39 -0
  48. package/docs//351/235/236/350/275/254/347/273/230/350/241/245/345/205/205/346/235/241/344/273/266/351/252/214/346/224/266-20261103.md +113 -0
  49. package/docs//351/235/236/350/275/254/347/273/230/351/246/226/346/211/271/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +71 -0
  50. package/examples/matting.json +5 -0
  51. package/examples/real-business/image-request.template.json +7 -0
  52. package/examples/real-business/node-patch.template.json +11 -0
  53. package/extensions/uzoo-cli-login/manifest.json +9 -0
  54. package/extensions/uzoo-cli-login/popup.css +1 -0
  55. package/extensions/uzoo-cli-login/popup.html +14 -0
  56. package/extensions/uzoo-cli-login/popup.js +65 -0
  57. package/package.json +56 -6
  58. package/scripts/check-package-resources.mjs +56 -0
  59. package/scripts/check-release.mjs +19 -0
  60. package/scripts/pack-local.mjs +33 -0
  61. package/scripts/release-policy.mjs +22 -0
  62. package/src/agreement-page.js +62 -0
  63. package/src/agreement.js +419 -0
  64. package/src/annotation-media.js +386 -0
  65. package/src/api.js +168 -0
  66. package/src/browser-login.js +13 -0
  67. package/src/callback-page.js +44 -0
  68. package/src/canvas.js +105 -0
  69. package/src/cli-authorization.js +122 -0
  70. package/src/cli.js +459 -0
  71. package/src/download-name.js +17 -0
  72. package/src/draft-finalize.js +229 -0
  73. package/src/generation-graph.js +124 -0
  74. package/src/generation-input.js +472 -0
  75. package/src/graph-disconnect.js +89 -0
  76. package/src/graph.js +431 -0
  77. package/src/group-edit.js +53 -0
  78. package/src/group-layout.js +113 -0
  79. package/src/group-membership.js +126 -0
  80. package/src/history-add.js +119 -0
  81. package/src/image-annotation.js +246 -0
  82. package/src/image-crop.js +283 -0
  83. package/src/image-matting.js +82 -0
  84. package/src/image-media.js +89 -0
  85. package/src/image-output.js +385 -0
  86. package/src/local-output.js +23 -0
  87. package/src/material-download.js +55 -0
  88. package/src/material-save.js +254 -0
  89. package/src/materials.js +147 -0
  90. package/src/matting-background.js +73 -0
  91. package/src/matting-jobs.js +63 -0
  92. package/src/matting-worker.js +49 -0
  93. package/src/matting.js +170 -0
  94. package/src/media-copy.js +53 -0
  95. package/src/media.js +343 -0
  96. package/src/model-options.js +76 -0
  97. package/src/multipart.js +28 -0
  98. package/src/node-copy.js +95 -0
  99. package/src/node-delete.js +49 -0
  100. package/src/node-history.js +231 -0
  101. package/src/normal-login.js +118 -0
  102. package/src/package-info.js +8 -0
  103. package/src/png-alpha.js +60 -0
  104. package/src/protocol.js +83 -0
  105. package/src/remake.js +738 -0
  106. package/src/repaint-archive.js +53 -0
  107. package/src/repaint-delivery.js +783 -0
  108. package/src/repaint-export.js +63 -0
  109. package/src/repaint-graph.js +214 -0
  110. package/src/repaint-history.js +267 -0
  111. package/src/repaint-materials.js +601 -0
  112. package/src/repaint-reverse.js +614 -0
  113. package/src/repaint-sequence.js +132 -0
  114. package/src/repaint-settings.js +576 -0
  115. package/src/repaint-video.js +798 -0
  116. package/src/repaint.js +326 -0
  117. package/src/resource.js +110 -0
  118. package/src/review.js +435 -0
  119. package/src/service.js +85 -0
  120. package/src/store.js +100 -0
  121. package/src/video-clip.js +381 -0
  122. package/src/video-frame.js +296 -0
  123. package/src/workflows.js +214 -0
@@ -0,0 +1,540 @@
1
+ # CLI 授权接口与令牌自动刷新设计
2
+
3
+ 更新日期:2026-10-27。修订:v2,采用网页/CLI 分端独立会话,替代此前的独占交接设计。状态:2A/2B 已在隔离本地验证;3A 已接入并通过模拟验证码的本地联调,真实短信和滑块验收待完成;3B 待实施,线上未启用双端在线。
4
+
5
+ 阶段 2A 实现状态见第 12 节;2B 和 3A 的实现及测试记录见后端 `docs/阶段2B后端授权协议实施记录.md` 和 `docs/阶段3A本地联调记录.md`。以下总体设计应结合实施记录及未完成事项核对。
6
+
7
+ ## 1. 目标与不可变边界
8
+
9
+ - 用户在普通浏览器中完成现有网站登录、短信和滑块验证;无需安装扩展、导入 Cookie 或使用受控浏览器。
10
+ - CLI 接收短期一次性授权码,通过 PKCE 兑换 CLI 会话,安全保存刷新令牌,后续自动刷新 access token。
11
+ - 兼容现有 E1 整包加密、S1 业务签名、JWT、签名会话恢复和刷新令牌轮换。
12
+ - **支持网页与 CLI 独立在线:每个账号保留一个网页会话和一个 CLI 会话。** 两类会话各有 sid、refresh token/JTI 和 S1 密钥,登录、刷新、普通退出互不顶掉;同类新登录仍替换同类旧登录。CLI 被其他 CLI 登录替换后不得自动反抢。
13
+ - 本期只开放固定的 `web` 与 `cli` 两个会话类别,不支持任意多设备、多 CLI 或按浏览器标签页扩容。CLI 仍必须通过对应类别的 active session 检查,不增加永久 API Key。账号禁用、全局撤销和确认的令牌重放按全账号安全策略处理。
14
+ - 按第 10 节先完成兼容会话层,再实现授权接口、网页/CLI 接入和灰度验收。设计变更不等于立即放开生产限制;未完成前保持现有运行行为。
15
+
16
+ ## 2. 已有实现及需要兼容的事实
17
+
18
+ 以下依据工作区源码,不假定线上配置值与默认值相同。
19
+
20
+ 2026-10-26 已将后端 `E:\anime-ai\anime-ai-engine` 的 `cli-liu` 从 `48a0434` 快进至 `origin/develop` 的 `4010259b42afd3d867cdb275276b99f6a22113a5`,并按该基线复核本节和 7.3。该次上游更新未改变核心单会话模型;签名中间件新增的客户端断连处理需在后续改造中保留。此版本记录不代表与生产部署的提交完全相同。
21
+
22
+ | 已有能力 | 当前实现 | 设计影响 |
23
+ |---|---|---|
24
+ | 单处登录 | `IdentitySessionService.issue()` 写 `refresh_jti:{user_id}` 和 `active_session:{user_id}`;JWT 带 `sid` | 保留旧键作为 web 类别;CLI 使用独立状态键,服务端统一路由 |
25
+ | 每次业务鉴权 | `get_current_user()` 比较 JWT `sid` 与当前 active session | 根据已验签的 token 类别核对对应 sid;同类旧 token 即使未到期也拒绝 |
26
+ | 刷新 | `IdentitySessionService.refresh()` 验证 refresh JWT、账号状态、当前 sid 和 JTI,轮换后保持同一 sid | 每类独立续期,只轮换本类 JTI,不创建新登录、不改另一类状态 |
27
+ | 响应丢失宽限 | 旧 JTI 可在受限窗口重取同一次轮换的 refresh token;默认 60 秒、最多 3 次 | CLI 要串行刷新、保存最新凭据;不能无限重试旧 token |
28
+ | 网页令牌交付 | `_issue_auth_response_data()` 从 JSON 移除 refresh token 并写 HttpOnly Cookie | **不能直接用于 CLI 换码/刷新响应**,需单独 formatter |
29
+ | S1 密钥 | 与 user、sid、可选设备绑定;Redis 中 apiSecret 加密保存 | CLI 新 sid 使用新的 CLI 设备绑定,不复制网页签名密钥 |
30
+ | 签名恢复 | `GET /auth/sign-session`,Bearer 校验,恢复已有签名密钥 | 缺少签名密钥时先恢复,不靠 refresh 轮换恢复 |
31
+ | E1 | RSA-OAEP-SHA256 封装随机密钥,HKDF-SHA256 派生 ENC/MAC,AES-256-GCM 加密请求体并 HMAC 签名 | 初次授权申请、换码和 CLI 刷新使用同一套实现 |
32
+ | 响应格式 | `code/message/data/request_id/timestamp`;默认统一 HTTP 状态,也可配置真实 HTTP 状态 | CLI 同时检查 HTTP 与业务 code,不能只看 HTTP 200 |
33
+
34
+ 当前源码默认 access token 有效期 1440 分钟、refresh token 15 天、S1 会话 7200 秒,均由配置决定。文档不将这些默认值写死到 CLI;令牌响应应返回实际期限。刷新是现有滑动续期语义,本期不增加无限期凭据或改变绝对会话期限策略。
35
+
36
+ 源码依据:
37
+
38
+ - [会话签发与刷新](../../anime-ai-engine/app/modules/identity/infrastructure/session.py)
39
+ - [认证依赖与单会话校验](../../anime-ai-engine/app/core/deps.py)
40
+ - [现有认证路由和 Cookie 响应处理](../../anime-ai-engine/app/api/v1/auth.py)
41
+ - [Redis 会话与刷新轮换](../../anime-ai-engine/app/core/redis.py)
42
+ - [S1 签名会话](../../anime-ai-engine/app/core/sign_v2/sessions.py)
43
+ - [E1/S1 中间件](../../anime-ai-engine/app/middleware/sign_v2.py)
44
+ - [请求签名 v2 文档](../../anime-ai-engine/docs/前端对接文档-请求签名v2.md)
45
+
46
+ ## 3. 分端独立在线的交互与状态
47
+
48
+ ```mermaid
49
+ sequenceDiagram
50
+ participant C as CLI
51
+ participant B as 普通浏览器
52
+ participant A as 后端
53
+ C->>C: 启动回环监听,生成 state 和 PKCE
54
+ C->>A: E1 创建授权申请
55
+ A-->>C: authorization_id / authorize_url
56
+ C->>B: 打开 authorize_url
57
+ B->>B: 按现有流程登录,返回授权页
58
+ B->>A: S1 查询申请并明确授权 CLI 独立登录
59
+ A-->>B: 仅返回短期授权码的回调地址
60
+ B->>C: 顶层导航至 127.0.0.1,携带 code/state
61
+ C->>A: E1 授权码 + PKCE verifier 换码
62
+ A->>A: 原子消费授权码,只签发或替换 CLI 类别的 sid
63
+ A-->>C: access/refresh token + CLI S1 会话
64
+ Note over B,A: 网页会话保持有效;网页与 CLI 分别刷新
65
+ C->>A: E1 refresh grant,保持 CLI sid
66
+ A-->>C: 新 access/refresh token,原子保存后继续业务请求
67
+ ```
68
+
69
+ 授权页必须显示当前账号、CLI 名称、设备名称、权限说明和下列影响:
70
+
71
+ > 授权后 CLI 可以独立在线,不会使当前网页下线;若已有其他 CLI 登录,该 CLI 将被替换。同一账号最多保留一个网页会话和一个 CLI 会话。
72
+
73
+ 按钮建议为“授权 CLI 独立登录”和“取消”。不能将独立在线宣传为无限设备在线或更高业务权限。设备名称由 CLI 提供,仅作提示,不能当成服务端证明;网页需转义显示。
74
+
75
+ 授权申请状态:`pending → approved → consumed`;另有 `denied / expired / cancelled`。`approved` 仅表示签发授权码,不创建或替换 CLI 会话。`consumed` 才签发 CLI 会话,必要时替换旧 CLI;取消、关闭浏览器、无效 PKCE 或过期请求不能影响已在线的网页或 CLI。
76
+
77
+ 同一授权申请只绑定一次账号与批准时的网页 sid。换码时再次核对该 sid 仍为当前 web 类别的 active session;若该网页已退出、被其他网页登录替换或被全局撤销,拒绝这次换码。此检查只证明批准仍有效,不将后续 CLI 生命周期绑定到网页:换码成功后的普通网页退出或重新登录不影响 CLI。
78
+
79
+ 若打开授权链接时未登录,正常网页登录只替换旧网页会话,不影响已有 CLI。只有后续用户明确批准且成功换码才可能替换旧 CLI;授权页需在批准时提示该影响。
80
+
81
+ ## 4. 后端接口清单
82
+
83
+ 前缀均为 `/api/v1`。`uzoo-anime-cli` 是登记的公共客户端标识,不是保密认证信息;CLI 不内置 client secret。第一期沿用该账号既有业务权限,不对外承诺尚未实现的细粒度 OAuth scope。
84
+
85
+ | 类型 | 路由 | 调用方 | 认证/保护 | 用途 |
86
+ |---|---|---|---|---|
87
+ | 新增 | `POST /auth/cli/authorizations` | CLI | 强制 E1,限流,无需已登录 | 创建不可变授权申请 |
88
+ | 新增 | `GET /auth/cli/authorizations/{authorization_id}` | 网页 | Bearer + S1,当前网页 sid 有效 | 查询申请并展示确认信息 |
89
+ | 新增 | `POST /auth/cli/authorizations/{authorization_id}/decision` | 网页 | Bearer + S1,沿用 CSRF 规则 | 确认/拒绝并返回服务端构造的回调地址 |
90
+ | 新增 | `POST /auth/cli/token` | CLI | 强制 E1,按 grant 验证,限流 | 授权码兑换及刷新令牌轮换 |
91
+ | 新增 | `POST /auth/cli/revoke` | CLI | 强制 E1,验证当前 CLI refresh 凭据 | 主动撤销当前 CLI 会话 |
92
+ | 复用 | `GET /auth/public-key` | CLI/网页 | 沿用现有公钥读取机制 | E1 公钥与 pkid |
93
+ | 复用 | `GET /auth/sign-session` | CLI | Bearer,保留未强制 S1 的启动能力 | 获取/恢复 CLI sid 的 S1 会话 |
94
+ | 兼容调整 | `POST /auth/refresh` | 原网页 | 原流程保持 | 拒绝 CLI refresh token,防止错误 formatter 丢失 CLI 凭据 |
95
+
96
+ `/auth/cli/token` 采用本项目的 E1 JSON 协议,借鉴授权码/PKCE 机制;不是可直接接入任意通用 OAuth SDK 的标准 form-urlencoded token endpoint。若未来需要标准 OAuth 兼容,应单独定义适配层,不能让同一路由隐式降级接受明文 token。
97
+
98
+ ### 4.1 创建授权申请
99
+
100
+ `POST /auth/cli/authorizations`。下面展示的是 **E1 解密后** 的 JSON,不是网络上发送的明文:
101
+
102
+ ```json
103
+ {
104
+ "client_id": "uzoo-anime-cli",
105
+ "request_id": "本次登录固定的随机UUID",
106
+ "redirect_uri": "http://127.0.0.1:49152/cli/callback/随机路径段",
107
+ "state": "至少32字节随机数的Base64URL",
108
+ "code_challenge": "BASE64URL(SHA256(code_verifier))",
109
+ "code_challenge_method": "S256",
110
+ "device_id": "CLI本机稳定设备标识",
111
+ "device_name": "Liu-PC",
112
+ "session_policy": "web_cli_independent"
113
+ }
114
+ ```
115
+
116
+ 响应 `data`:
117
+
118
+ ```json
119
+ {
120
+ "authorization_id": "car_高熵随机值",
121
+ "authorize_url": "https://uzooai.com/auth/cli/authorize?request_id=car_高熵随机值",
122
+ "expires_in": 300,
123
+ "session_policy": "web_cli_independent"
124
+ }
125
+ ```
126
+
127
+ 服务端限制:只允许登记 client_id,`code_challenge_method` 固定 S256,校验设备标识长度与 Header 一致;`session_policy` 只接受本修订的 `web_cli_independent`;客户端不能通过自填类别或策略参数扩展会话数量。重试使用相同 request_id 和业务参数;E1 外层时间戳/密钥/签名必须重新生成。同 request_id 不同请求内容应拒绝。
128
+
129
+ 回调只接受数值回环地址:第一期仅 `http://127.0.0.1:<非零端口>/cli/callback/<随机段>`。不接受域名、用户信息、fragment、通配符或调用方附加 query;不用 `localhost` 的 DNS 解析作为信任依据,不监听 `0.0.0.0`。端口由 CLI 先绑定成功后写入申请,后续精确匹配完整 redirect_uri。后端只将浏览器重定向到此地址,不主动请求回环地址。
130
+
131
+ ### 4.2 网页查询与批准
132
+
133
+ `GET /auth/cli/authorizations/{id}` 返回申请状态、登记的 CLI 名称、CLI 设备名、期限、固定的双类别在线与同类替换提示和当前账号显示信息。不得返回 PKCE verifier、令牌或签名密钥。授权页从现有登录态获得 Bearer 和内存 S1,不读取/导出 HttpOnly Cookie。
134
+
135
+ `POST /auth/cli/authorizations/{id}/decision` 的 S1 业务 JSON:
136
+
137
+ ```json
138
+ { "decision": "approve", "session_policy": "web_cli_independent" }
139
+ ```
140
+
141
+ 批准时保存用户 id、当前有效网页 sid、申请绑定信息,生成至少 32 字节随机授权码,建议有效期 60 秒且不超过申请剩余期限。服务端返回:
142
+
143
+ ```json
144
+ {
145
+ "status": "approved",
146
+ "redirect_url": "http://127.0.0.1:49152/cli/callback/随机路径段?code=随机授权码&state=原state",
147
+ "expires_in": 60,
148
+ "cli_session_on_exchange": true,
149
+ "web_session_preserved": true
150
+ }
151
+ ```
152
+
153
+ 网页使用顶层导航前往服务端返回的完整地址,不自行拼接用户提供的地址;无需跨域 fetch 本机端口或安装扩展。此时网页会话仍有效。拒绝用 `decision=deny`,使申请失效,并返回带 `error=access_denied&state=...` 的既定回调地址;无效 redirect_uri 从一开始拒绝,不能重定向到未经验证的地址。
154
+
155
+ 重复批准同一申请不得产生多份有效授权码或更换绑定账号。若需要恢复丢失的批准响应,只能在授权码有效期内、原网页 sid 下返回同一次决定;为此缓存的原始授权码须加密保存,索引只保存其哈希。
156
+
157
+ ### 4.3 授权码兑换
158
+
159
+ `POST /auth/cli/token`,E1 解密后:
160
+
161
+ ```json
162
+ {
163
+ "grant_type": "authorization_code",
164
+ "client_id": "uzoo-anime-cli",
165
+ "authorization_id": "car_高熵随机值",
166
+ "code": "本机回调收到的一次性授权码",
167
+ "code_verifier": "仅CLI持有的PKCE原值",
168
+ "redirect_uri": "http://127.0.0.1:49152/cli/callback/随机路径段",
169
+ "device_id": "与申请及Header相同的CLI设备标识",
170
+ "exchange_id": "本次兑换固定的随机UUID"
171
+ }
172
+ ```
173
+
174
+ 先校验申请、码哈希、期限、client_id、设备、redirect_uri、PKCE S256 与批准时绑定的账号/sid;检查账号可用。**浏览器不接触 code_verifier,网页请求中也不传 verifier。** 然后按第 7 节原子创建/替换 CLI 会话:生成新的 CLI sid 和令牌对,只写该用户 CLI 类别的 active session 与 refresh JTI,单次消费授权码;保留网页的 sid、JTI、Cookie 与 S1 密钥。
175
+
176
+ 响应 `data` 示例(令牌/期限为示意,实际值由配置生成):
177
+
178
+ ```json
179
+ {
180
+ "access_token": "access JWT",
181
+ "refresh_token": "refresh JWT",
182
+ "token_type": "bearer",
183
+ "expires_in": 86400,
184
+ "refresh_expires_in": 1296000,
185
+ "session": {
186
+ "session_id": "新的CLI sid",
187
+ "client_type": "cli",
188
+ "client_id": "uzoo-anime-cli",
189
+ "device_id": "CLI设备标识",
190
+ "session_policy": "web_cli_independent"
191
+ },
192
+ "sign": { "keyId": "sess_xxx", "apiSecret": "短期S1密钥", "expiresIn": 7200 },
193
+ "browser_session_replaced": false,
194
+ "cli_session_replaced": false
195
+ }
196
+ ```
197
+
198
+ `cli_session_replaced` 由服务端按是否替换已在线 CLI 返回,示例为首次 CLI 登录;`browser_session_replaced` 固定为 false。
199
+
200
+ 使用专门的 CLI formatter:JSON 保留 refresh_token,不设置浏览器认证 Cookie,不调用会 `pop(refresh_token)` 的网页 formatter。HTTP 必须 HTTPS,并返回 `Cache-Control: no-store`。签名密钥下发若在令牌提交后失败,返回 `sign: null`,由 CLI 先保存令牌、再调用现有 sign-session 自愈;不能因为签名服务短暂失败而丢弃已轮换/已签发凭据。
201
+
202
+ 授权码只能创造一次会话。若响应丢失,允许同一 exchange_id、相同原始业务参数、相同 PKCE 证明在短恢复窗口取回**同一次兑换结果**;不是再次消费授权码或签发新 sid。缓存应加密、短 TTL、限定次数;返回前检查新 sid 仍是当前 CLI 会话且缓存 CLI refresh JTI 仍为当前值。若 CLI 已重新登录或该刷新链已前进,不能返回已失效令牌,更不能覆盖现会话;兑换完成后的普通网页登录变化不使 CLI 兑换恢复失效。不同 exchange_id 或参数复用已消费码一律拒绝。超出恢复窗口后须重新授权;不回滚到旧 CLI sid,也不改变网页会话。
203
+
204
+ ### 4.4 CLI 自动刷新
205
+
206
+ 同一 `POST /auth/cli/token`,E1 解密后:
207
+
208
+ ```json
209
+ {
210
+ "grant_type": "refresh_token",
211
+ "client_id": "uzoo-anime-cli",
212
+ "refresh_token": "本机安全存储中的最新refresh JWT",
213
+ "device_id": "同一CLI设备标识",
214
+ "refresh_request_id": "本次刷新固定的随机UUID"
215
+ }
216
+ ```
217
+
218
+ 不要求有效 access token 或现存 S1 密钥;E1 提供启动时的加密与验签,refresh JWT 及其服务端状态提供用户认证。必须验证 token 类型、签发者/受众、有效期、账号状态、CLI 类别的 active sid、当前 CLI JTI,以及该 sid 的 CLI 类型、client_id、device_id 绑定。网页 refresh token 不能用于 CLI grant,CLI refresh token 也不能走旧网页 Cookie formatter。
219
+
220
+ 复用既有轮换机制,并按第 7 节保留全账号重放处置边界:成功时保持 sid、client/device 绑定,轮换 JTI,返回新 access_token、refresh_token、期限和可恢复的 `sign`。不得调用 `issue()` 生成新的登录来“刷新”,不得替换本类 active sid 与该 refresh token 不相同的会话,不读写 web 类别状态键。现有 refresh 函数只保留 `sid` 的签发逻辑需改为保留经过服务端验证的 CLI 绑定字段;不得盲目拷贝 JWT 中所有 extra claims。
221
+
222
+ CLI 对同一会话串行刷新,刷新前后使用同一套进程锁和系统安全存储。收到新 refresh_token 后先原子保存,再重放原业务请求。签名密钥仅放内存。进程退出期间不需要后台定时续期;下一次命令根据实际 token 有效期或 2010 刷新即可。长期空闲超过现有会话期限仍须重新登录。
223
+
224
+ 响应丢失可使用同一 refresh_request_id 和原 refresh token,在配置的短窗口内取回同一次轮换结果;E1 外层每次重新生成。沿用现有默认 60 秒/3 次的受限恢复边界,服务端结果需与同一个 sid、客户端绑定及当前 JTI 校验。超出宽限或出现真实重放时不延长旧 token 使用期;确认重放后按既有全账号安全策略撤销 web/cli 两类并要求重新登录。仅凭同类 sid 被新登录替换或普通过期不能误判成重放。
225
+
226
+ ### 4.5 撤销
227
+
228
+ `POST /auth/cli/revoke`,E1 解密后:
229
+
230
+ ```json
231
+ {
232
+ "client_id": "uzoo-anime-cli",
233
+ "refresh_token": "当前CLI刷新凭据",
234
+ "device_id": "CLI设备标识"
235
+ }
236
+ ```
237
+
238
+ 验证 CLI 刷新凭据的类型、绑定和当前状态后,按期望 sid/JTI 原子删除当前 CLI 类别的登录状态,吊销该 CLI sid 的 S1 会话并清理相关授权恢复缓存。普通撤销不影响 web;已经被其他 CLI 登录替换的旧 CLI 不能删除新 CLI 或网页会话。重复撤销返回无敏感信息的幂等结果,不暴露其他会话状态。CLI 在服务端响应后清除本机凭据;网络失败时仍可清除本地凭据,但输出需说明远程撤销未确认。
239
+
240
+ ### 4.6 错误响应约定
241
+
242
+ 保留既有统一响应封装与 2010/2011/2012、2200~2205 的含义。新增业务错误的数值 code 由实施时在后端错误目录登记,避免占用已有编号;建议在 `data.error` 返回下列稳定符号,CLI 不依赖中文 message 分支判断。
243
+
244
+ | `data.error` 建议值 | 场景 | CLI 处理 |
245
+ |---|---|---|
246
+ | `invalid_authorization_request` | client、参数、回调或 session_policy 不合法 | 停止,修正配置后创建新申请 |
247
+ | `authorization_expired` | 申请/码到期 | 提示重新运行登录,不替换原会话 |
248
+ | `access_denied` | 用户拒绝 | 正常结束登录,不重试授权 |
249
+ | `invalid_grant` | 无效码、PKCE/绑定不匹配、已消费且不属于合法响应恢复 | 对外统一拒绝,服务端记录脱敏原因;不更改会话 |
250
+ | `exchange_result_unavailable` | 已兑换但短期结果已不可恢复,或其 JTI 已前进 | 提示重新授权;不能重放旧兑换覆盖当前会话 |
251
+ | `session_changed` | 尚未兑换时,批准所依据的网页 sid 已失效 | 拒绝本次授权码兑换,重新取得网页批准;不影响独立在线的 CLI |
252
+ | `unsupported_grant_type` | token grant 不支持 | 客户端版本/协议错误,停止 |
253
+ | `cli_auth_disabled` | 功能未开启 | 提示服务端尚不支持,不退回弱认证接口 |
254
+
255
+ 错误 PKCE 的尝试按申请/IP 限流,达到阈值可使该申请失效,但不得因此吊销用户正常会话。不得在错误响应中泄露授权码是否属于某个其他账号、当前有效 JTI 或其他会话的设备信息。CLI 同时处理业务 code 与代理产生的非 JSON HTTP 错误;不能把 HTML 错误页解析成成功授权。
256
+
257
+ ## 5. E1 / S1 兼容要求
258
+
259
+ E1 和 S1 作用不同:E1 是本期敏感请求的整包加密与请求验签;S1 是业务请求签名,不提供业务请求体加密。沿用 HTTPS 保护传输,不把“有 S1”描述成“所有数据都经过 AES 加密”。
260
+
261
+ CLI 的三个敏感写入口 `authorizations`、`token`、`revoke` 必须明确验证 `request.state.sign_v2.mode == E1`。不能只把路径加到观察模式配置后默认放过无签名请求,也不能要求尚未拿到 S1 的 token 入口必须持有 S1。新增路径不得被宽泛的 `/auth/*` 豁免吞掉。
262
+
263
+ E1 完整流程沿用现有 v2,不新增另一种加密格式:
264
+
265
+ 1. `GET /auth/public-key` 获取 pkid 与 RSA 公钥;按服务器现有多公钥轮换机制选择和刷新缓存。
266
+ 2. 随机 32 字节 requestKey,经 RSA-OAEP-SHA256 封装为 `X-K: E1.{Base64URL(wrappedKey)}`。
267
+ 3. HKDF-SHA256,空 salt,info 分别为 `ENC` 和 `MAC`,各派生 32 字节。
268
+ 4. 随机 12 字节 AES-GCM nonce,AAD 为 `METHOD + LF + RAW_PATH + LF + X-T + LF + X-K`。
269
+ 5. UTF-8 JSON 明文加密为 `data=Base64URL(nonce || ciphertextWithTag)`;外层为 `{"pkid":"...","data":"..."}`。
270
+ 6. 对**实际发送的外层 JSON 字节**求 SHA256,使用现有六行规范串计算 HMAC-SHA256,产生无填充 Base64URL 的 `X-Sign`。
271
+ 7. 服务端先验签、防重放,再检查外层 pkid 与解封密钥 id 一致,并使用现有 `decode_e1_json()` 解密与 schema 校验。不能先消费业务授权码再验 E1。
272
+
273
+ 六行规范串仍为:
274
+
275
+ ```text
276
+ METHOD
277
+ RAW_PATH
278
+ SORTED_QUERY
279
+ BODY_SHA256_HEX
280
+ X-T
281
+ X-K
282
+ ```
283
+
284
+ Query 使用既有 RFC3986 编码、编码后 key 排序、重复 key 保序规则。`X-T` 为毫秒时间戳,时钟窗口、防重放 TTL 和 E1 限流沿用配置。每次传输重试更新 E1 requestKey、nonce、时间戳及签名;grant 的 exchange_id / refresh_request_id 保持不变。这是传输防重放与业务幂等的区别。
285
+
286
+ 浏览器已有登录继续使用 E1 短信登录;授权查询和批准走现有 Bearer/S1/CSRF。CLI 拿到会话后的画布等业务请求继续 S1,并带 CLI 自己的 `X-Device-Id` 和现有 `X-Client-Source: canvas`。CLI 设备 id 不复制浏览器 id;网页批准时使用网页设备,最终 CLI 签名会话绑定 CLI 设备。
287
+
288
+ E1 本身不表示响应体自动加密。本案令牌仍通过 HTTPS JSON 交付;若启用现有 `SIGN_V2_SIGN_RESPONSES`,CLI 应验证 `X-Resp-T/X-Resp-Sign`,E1 响应用本次派生 MAC key,S1 响应用当前 apiSecret。不得宣称现有服务端已经支持本案未实现的响应加密。
289
+
290
+ ## 6. CLI 刷新与失败处理状态机
291
+
292
+ | 事件 | CLI 行为 | 禁止行为 |
293
+ |---|---|---|
294
+ | 本地无登录态 | 执行普通浏览器授权码流程 | 偷读浏览器 Cookie 数据库、重新要求安装扩展 |
295
+ | 有效期接近到期 | 获取会话锁,复查最新本地凭据后进行一次 E1 refresh | 多个命令同时使用旧 refresh token |
296
+ | 业务返回 2010 | 刷新一次;成功保存后按请求安全性重试 | 对刷新接口递归自动刷新 |
297
+ | 返回 2200/2205 且 access 尚有效 | 先 `GET /auth/sign-session`,恢复后重新签名 | 为取签名而轮换 refresh token |
298
+ | 返回 2011 | 清理失效状态,提示重新登录 | 后台无限续期或自动提交新登录 |
299
+ | 返回 2012 | 提示“该 CLI 会话已被其他 CLI 登录替换”,停止刷新和新业务提交;普通网页登录不得触发此错误 | 自动重新授权、抢回另一 CLI 会话 |
300
+ | 刷新响应丢失 | 在短恢复窗口内有限次恢复同一次刷新 | 一直使用旧 token、重新签发 sid |
301
+ | 刷新成功但本地保存失败 | 停止业务调用,明确提示凭据保存失败;只按短恢复协议处理 | 丢掉新 token 后继续用旧 token 轮换 |
302
+ | 生成提交超时 | 保留原 operation / task_id / Idempotency-Key 查询恢复 | 将重新登录或刷新当成再次扣费提交的理由 |
303
+
304
+ 对读请求,凭据恢复后可有限重试;对写请求,必须依据已证明的认证失败或服务端幂等契约决定是否重放。认证恢复不改变阶段一生成操作的幂等规则。
305
+
306
+ ## 7. 分端会话存储、兼容与原子性
307
+
308
+ ### 7.1 固定两类会话与状态键
309
+
310
+ 每个账号最多有两个有效 sid,各类别最多一个。同一网页会话的多个标签页不增加席位;多个 CLI profile、设备或进程也不能通过不同 client_id/device_id 额外增加 CLI 席位。
311
+
312
+ | 状态 | web 类别(保留原键) | cli 类别(新增键) |
313
+ |---|---|---|
314
+ | 当前有效 sid | `active_session:{uid}` | `active_session:{uid}:cli` |
315
+ | 当前 refresh JTI | `refresh_jti:{uid}` | `refresh_jti:{uid}:cli` |
316
+ | 刷新响应恢复 | 原 `refresh_rot:{uid}:{old_jti}`,仍需校验 web sid | `refresh_rot:{uid}:cli:{sid}:{old_jti}` |
317
+ | S1 密钥/映射 | 继续按现有唯一 sid/keyId 存储 | 继续按各自唯一 sid/keyId 存储,不共用网页密钥 |
318
+
319
+ 键名为本修订建议;封装在统一 session store 中,业务调用方不能各自拼接。元数据记录 user、client_type、sid、登记 client_id、device_id 和有效期;它不是第三种登录许可。JWT 沿用 sub/sid/type/jti/iss/aud,新增经服务端签发的 `client_type=web|cli`;refresh 继续保留已验证的 client/device 绑定。
320
+
321
+ 类别只能来自**验证成功的 JWT 与服务端会话记录**。旧 JWT 无 client_type 时按 web 处理,以兼容存量;未知类别、CLI 路由上的缺失类别或元数据不匹配必须拒绝,不能回退至 web。禁止按客户端随意提交的 Header、query、profile 名称或 client_id 字符串直接选择鉴权状态键。
322
+
323
+ ### 7.2 登录、刷新、撤销与安全事件
324
+
325
+ | 事件 | web 状态 | cli 状态 |
326
+ |---|---|---|
327
+ | 新网页登录 | 替换旧 web sid/JTI/签名会话 | 保持 |
328
+ | 授权码成功兑换 | 保持,源网页只用于证明批准有效 | 新建 CLI,或替换旧 CLI sid/JTI/签名会话 |
329
+ | web 刷新/签名恢复 | 只操作 web 同一 sid 的状态 | 保持 |
330
+ | CLI 刷新/签名恢复 | 保持 | 只操作 cli 同一 sid 的状态 |
331
+ | 普通网页退出 | 撤销当前 web sid,按 sid 回填退出日志 | 保持 |
332
+ | CLI revoke/退出 | 保持 | 撤销当前 cli sid,按 sid 回填退出日志 |
333
+ | 当前类别自然过期 | 仅该类别失效 | 另一类别照常独立续期 |
334
+ | 账号禁用/删除、全局撤销、确认的 refresh token 重放 | 两类都撤销,清理各自签名与恢复缓存 | 两类都撤销,不因分端在线降低既有安全处置 |
335
+
336
+ “全部退出”与普通退出应为不同的服务操作。若现有某个调用方本来表达全局安全撤销,应显式路由到全局操作,不能机械地把所有 delete(user_id) 都改成当前端。确认重放前先区分过期、同类登录替换和合法短期响应恢复;旧请求不能误吊销新会话。账号禁用仍在每条鉴权路径检查,后台禁用不能仅依赖某个类别的 Redis 删除是否成功。
337
+
338
+ ### 7.3 统一调整公共鉴权实现,排查独立鉴权路径
339
+
340
+ #### 7.3.1 本节目的与改动边界
341
+
342
+ 目的:网页与 CLI 的 token 进入后端后,均按**服务端可信的客户端类别**查找自身有效会话。避免 CLI 已独立登录,某条认证路径却仍读网页会话键,导致误拒绝;也避免旧 CLI 被替换后,在遗漏的路径继续通过认证。
343
+
344
+ **不是逐个修改所有 API。** 大部分模型、画布、生成、账号等 HTTP 路由已使用 `Depends(get_current_user)`;修改这一个公共依赖及其下层会话查询后,这些路由统一生效。业务参数、响应、计费、任务执行、资源所有权与团队权限不因本节改变。仅当发现路由自行解析 JWT/查询 active session、绕过公共认证时,才单独调整它。
345
+
346
+ 当前代码尚未实现本设计;以下文件和函数已在后端 `4010259` 基线核对,实施前仍应检查实际工作区差异。
347
+
348
+ #### 7.3.2 公共实现清单与调用关系
349
+
350
+ | 公共实现 | 当前使用方式 | 必须调整/核对的内容 | 调用方通常是否逐个修改 |
351
+ |---|---|---|---|
352
+ | `app/core/deps.py::get_current_user()` | 大部分 HTTP 路由使用的 FastAPI 依赖 | JWT 验证后解析类别,调用统一会话验证,填充请求的可信 client_type/sid;保留账号禁用、软删除、account_id 和权限上下文 | 否;只对依赖传播做代表性回归 |
353
+ | `app/modules/identity/application/optional_auth.py::OptionalAccessApplication.resolve()` | 配置、公开展示等接口的可选登录解析 | 使用同一类别/会话验证,旧 CLI 不能被当成有效登录用户;保持无效/无 token 返回匿名的既有契约 | 通常否;调用方数据可见性需测试 |
354
+ | `app/modules/notification/websocket_auth.py::authenticate_notification_socket()` | 通知和画布协作 WebSocket 的公共握手认证 | 根据可信类别验证对应 sid;保留账号状态检查、失败返回与已有握手协议,避免自行另写一套不一致规则 | 握手调用方通常不需重复修改;连接元数据/失效处理按下文检查 |
355
+ | `app/api/v1/auth.py::restore_sign_session()` | `GET /auth/sign-session` 已依赖 get_current_user | 经公共依赖获得当前正确 sid,恢复该 sid 的 S1 密钥;不额外调用 refresh,不借网页密钥给 CLI | 该认证路由单独验证,不复制到业务路由 |
356
+ | `app/middleware/sign_v2.py`、`app/core/sign_v2/sessions.py` | S1 验签及按 sid 的密钥保存/恢复/吊销 | user/sid/设备绑定与新会话类别一致;S1 算法保持,active sid 仍由公共会话校验保证;签名清理不能跨类误删 | 不逐个给 API 增加新签名代码 |
357
+ | 统一 session store(建议新建小模块,复用 `app/core/redis.py`) | 当前多个实现各自调用 get_active_session(user_id) | 集中类别解析、状态键访问、session 验证、TTL 和开关判断;调用方不得各拼键或自行按 Header 推断类别 | 修改公共调用点,业务路由不直接感知 Redis 键名 |
358
+
359
+ 已核对的代表调用链:
360
+
361
+ - 普通 HTTP:模型/画布/生成等路由 → `get_current_user()` → 唯一的公共会话验证。
362
+ - 可选登录:`app/api/v1/configs.py`、`app/api/v1/showcase.py`、`app/modules/featured_case/api.py` → `OptionalAccessApplication.resolve()` → 同一公共会话验证。
363
+ - 通知 WebSocket:`app/api/v1/notifications.py::notifications_ws()` → `authenticate_notification_socket()`。
364
+ - 画布协作 WebSocket:`app/api/v1/canvas_collab_ws.py::canvas_collab_ws()` → **同一个** `authenticate_notification_socket()`;不是在每个 WebSocket 处理函数重复改 token 校验。
365
+ - 签名恢复:`restore_sign_session()` → `get_current_user()` → `ensure_sign_session(session_id=当前sid, ...)`。
366
+
367
+ 登录签发、refresh、logout/revoke、设备列表与登录日志属于第 7.1/7.2/7.4/7.5 的状态读写改造,不应和“所有业务接口鉴权重写”混为一项,也不能只改 get_current_user 就遗漏这些写入口。
368
+
369
+ #### 7.3.3 建议的统一验证步骤
370
+
371
+ 1. 按现有 security helper 验证 JWT 签名、iss/aud、过期时间、token 类型及必要字段。refresh token 不能当业务 access token 使用。
372
+ 2. 从**已验签**的 token 及服务端元数据解析 client_type:旧 token 无字段仅走 web 兼容分支;显式 web/cli 必须与记录一致;未知类别拒绝,CLI 关闭时明确拒绝 cli 类别。不能由 `X-Client-Type`、User-Agent、query 或 CLI profile 名决定。
373
+ 3. 通过 session store 查本类别的 active sid,验证 token sid 与其一致。无会话/到期、同类被替换、账号禁用等继续保持既有错误语义;另一类别的新登录不应导致 2012。
374
+ 4. 按当前入口契约返回认证结果:普通 HTTP 返回用户并绑定可信上下文;optional auth 保持匿名降级语义;WebSocket 保持握手拒绝语义。共享验证结论,不强行让所有入口抛出同一种 HTTP 异常。
375
+ 5. 原有资源所有权、团队角色、节点编辑权限等授权检查继续执行。通过 CLI 认证不代表拥有全部项目或更高权限。
376
+ 6. S1 路径继续检查签名会话的 user/sid/设备;有效签名不能替代当前登录有效性检查,已被同类新登录替换的旧 token/key 组合仍必须被拒绝。
377
+
378
+ 无需把相同的代码粘贴到各路由。统一 helper 只负责必要的会话事实;HTTP、匿名访问与 WebSocket 的错误/上下文适配留在其原有边界。
379
+
380
+ #### 7.3.4 哪些通常不改,哪些需要单独处理
381
+
382
+ 通常不改:已经依赖 get_current_user 的业务 route 参数、模型目录、生成请求/响应结构、任务队列与计费逻辑;已经委托公共 helper 的 optional auth/WS 路由不重复实现类别逻辑。原网站的登录表单、Bearer 传递、Cookie 形式以及 E1/S1 算法保持兼容。
383
+
384
+ 需要单独处理的例外:独立解码 token 后直接信任 sub/sid 的入口、直接读写 active_session/refresh_jti 的代码、自己维护在线会话缓存的长连接,以及用“最近一次登录”替代确切 sid 的退出日志。新 `/auth/cli/*` 路由按第 4 节另行实现;本节不要求阶段 2A 提前完成这些授权接口。
385
+
386
+ `decode_token()` 搜索结果不等于“全部要改”:例如 auth 响应 formatter 读取自身刚签发 JWT 的 sid、S1 中间件对照 Bearer 与签名身份、保留过渡期的 `request_hmac.py`,用途各不相同。逐项记录“修改/验证保持/不涉及”的理由;不因搜索命中就重写旧加密协议。预签名文件 URL、分享凭证等不是用户登录 JWT 的,不能套用 client_type 规则。
387
+
388
+ #### 7.3.5 WebSocket 握手与已建立连接的边界
389
+
390
+ 当前通知/画布协作代码在握手时调用公共认证,循环处理消息时不自动等同于“每次重新鉴权”。因此仅修改公共握手函数,不能宣称已在线的旧连接会在退出/撤销后立即断开。
391
+
392
+ 阶段 2A 需核对连接管理器是否能区分 user/sid/client_type:同用户的网页和 CLI 连接允许共存;某条连接退出不能按 user_id 清掉另一类全部连接。对同类替换和全局撤销,明确并测试已有连接的拒绝后续操作/停止推送策略;需要 sid 元数据、撤销通知或受控周期复验时,作为认证边界的配套改动,而不是重写消息业务。握手失败与已建立连接失效分别验收;未实现主动断开时如实报告实际生效边界,不把连接仍存活当成通过了“即时撤销”。
393
+
394
+ #### 7.3.6 排查方法与阶段 2A 交付物
395
+
396
+ 实施时先用 rg 建调用清单,至少覆盖:
397
+
398
+ ```powershell
399
+ rg -n 'get_active_session|store_active_session|delete_active_session|get_active_session_ttl|refresh_jti:|refresh_rot:' app -g '*.py'
400
+ rg -n 'decode_token\(|HTTPBearer|OptionalAccessApplication|authenticate_notification_socket' app -g '*.py'
401
+ rg -n 'get_current_user|request.state.session_id|revoke_sign_sessions_for_session' app -g '*.py'
402
+ ```
403
+
404
+ 清单区分公共实现、纯委托调用方、独立实现与测试;不能为了让清单变空而批量修改文件。更新源码后重新搜索,确认旧键访问集中到受控的 web 兼容/session store 分支。
405
+
406
+ 阶段 2A 的必要验证与交付:
407
+
408
+ - 公共 helper 在旧 web、显式 web、cli、未知/伪造类别、空/失效/被替换 sid、开关关闭时的结果正确。
409
+ - 用至少一个现有受保护 HTTP 接口证明公共依赖传播生效,同时验证资源权限未放宽,不需要为每个同构业务路由写重复测试。
410
+ - optional auth 的无 token/无效 token 继续按原公开访问语义处理,不能泄露仅登录可见的数据;合法 cli 按账号身份返回。
411
+ - 通知和画布协作握手覆盖两类有效会话、同类替换、全局失效;已建立连接行为单独测试并说明。
412
+ - 签名恢复不轮换 refresh;旧 S1 不绕过 sid,有效 web 与 cli 密钥互不吊销。
413
+ - 提供按“公共文件已改、调用方验证无需改、独立路径需改”的变更清单,附实际测试命令/结果;范围限于公共认证和会话基础,不扩展为全业务 API 重写。
414
+
415
+ ### 7.4 原子性与竞争条件
416
+
417
+ 当前 issue 分别写 JTI 和 active sid,refresh/退出也存在分步读写。应在共享 session store 统一 Lua/CAS 或可靠串行化机制;同时覆盖网页签发、CLI 签发、刷新、普通撤销与全局撤销。不能只改键名后保留旧的无条件写入/删除。
418
+
419
+ 1. CLI 兑换原子核对授权码、批准时的 web sid、账号可用性与授权状态;源 web sid 已失效则拒绝尚未完成的兑换。
420
+ 2. 将 code 的 approved→consumed、exchange_id/请求摘要、**cli 类别**新 sid/JTI、元数据和加密恢复结果一次可靠提交。不得改写 web 键;同时兑换的不同申请按提交顺序同类替换,最终只能一个 CLI 有效。
421
+ 3. 兑换成功后,后续普通 web 退出/刷新/新登录只影响 web。尚未兑换的授权申请仍以其批准的源 web sid 有效为条件。
422
+ 4. 每类 refresh 原子核对自身 sid/JTI 后轮换并续期;旧 refresh 请求不能覆盖同类新登录,更不能访问另一类状态。恢复响应也须匹配类别/sid/client/device/当前 JTI。
423
+ 5. 普通 logout/revoke 与旧签名清理按期望类别/sid/JTI 比较后执行,防止晚到的旧请求撤销新同类会话。登录日志按确切 sid 更新,不能继续只回填“该用户最近一次登录”。
424
+ 6. 全局撤销需与两类会话写入处于同一可靠协调边界:要么先新建后被全局撤销,要么先撤销后旧批准无法再兑换。若采用锁,不能只使用互不协调的 web/cli 两把锁来实现全局操作。
425
+
426
+ JWT 与加密恢复数据应在提交前准备;状态与可恢复结果须处于同一可靠边界。跨 Redis/数据库的审计使用可重试补记,不在长数据库事务中调用外部网络。原有父子表锁顺序和迁移规范继续适用。
427
+
428
+ 现有 `remember_refresh_rotation()` 保存 `new_token`,不能描述为已有加密。新增 CLI 兑换/刷新缓存需加密、短 TTL 并绑定用途、类别、用户、sid 和操作 id;不把令牌写入普通审计表或长期 Redis 缓存。
429
+
430
+ ### 7.5 存量兼容与设备展示
431
+
432
+ 旧 web JWT、Cookie、签名密钥和 Redis 键按原语义继续有效,不要求用户仅因增加 CLI 支持而集体重新登录。新增服务端签发/刷新使用可信 web 类别;缺字段的旧 token 仅通过受控的 web 兼容分支。CLI 必须通过新的授权端点拿到真正独立的 CLI sid;当前扩展导入的网页 token 不能只改本地配置或标签就变成 CLI token。
433
+
434
+ 设备列表读取两类 active sid 与各自 TTL,增加 `client_type` 和可区分的客户端显示信息;设备归并至少包含类别,不能把同机网页与 CLI 合并成一个席位或仅显示最近的一条。返回的 online_count 最大为 2,is_current 由请求自身 sid 判断。撤销/退出日志按 sid 匹配。通常不必改 users 表;是否需为 login_logs 增加 client_type/设备字段或迁移,需在阶段 2A 核对现有字段后确定,不能只依赖 UA 猜类别。
435
+
436
+ 若并发、全局撤销或兼容测试未通过,保持 CLI 功能关闭,不进入真实双端在线灰度;不能以忽略 sid 校验或允许任意设备并行作为兜底。
437
+
438
+ ## 8. Redis 数据、配置与审计建议
439
+
440
+ | 记录 | 建议内容/期限 | 不应保存 |
441
+ |---|---|---|
442
+ | CLI 授权申请 | 随机 id、client/device、回调、state、PKCE challenge、状态、批准 user/sid;默认 300 秒 | PKCE verifier、网页 refresh token |
443
+ | 授权码索引 | 授权码哈希、申请 id、期限、消费信息;默认 60 秒 | 可直接查到的明文授权码 |
444
+ | 兑换恢复 | exchange_id、证明摘要、新 CLI sid/JTI、加密结果;建议至多 60 秒/3 次 | 无限期可重取的凭据 |
445
+ | CLI sid 元数据 | user、client_type、client_id、device、权限版本(如将来有)、会话期限 | 额外绕过对应类别 active sid 的许可 |
446
+ | 刷新恢复 | 类别、old/new JTI、sid、client/device、request_id、次数、加密结果;继承既有宽限边界 | 可跨 sid 使用的旧 token 缓存 |
447
+
448
+ 建议配置:`CLI_AUTH_ENABLED`(默认关闭)、`CLI_SESSION_ENABLED`(默认关闭,控制 CLI 类别签发和鉴权;不影响 web)、`CLI_AUTH_REQUEST_TTL_SECONDS=300`、`CLI_AUTH_CODE_TTL_SECONDS=60`、登记的公共 client_id、规范网站 origin、允许的回环地址格式、按 IP/client/device 的申请和换码限流。刷新有效期与重放宽限默认沿用现有 AUTH/REFRESH 配置,不引入更长的 CLI 特权期限。
449
+
450
+ 审计事件建议登记为申请、批准/拒绝、换码、CLI 类别登录/同类替换、刷新恢复、分类撤销/全局撤销及失败原因。记录 user、client_type、原/新同类 sid、设备、来源 IP、request_id 与脱敏操作标识;新增审计事件名要在现有事件目录登记。网关、API 日志、分析 SDK、异常报告和终端日志统一剔除授权码、verifier、refresh token、access token、apiSecret 及含 code 的完整回调 URL。授权页和回调页设置 no-store、no-referrer,避免第三方脚本收集查询参数。
451
+
452
+ 本机回调监听由 CLI 在打开浏览器前完成,严格核对 Host、端口、随机路径、state 和请求次数;只接收 code/state 或受支持的错误字段,不接收令牌;回调页面只显示“可返回终端”,不回显授权码。绑定失败换端口重新生成完整申请,不能在已有授权申请上临时更换回调。
453
+
454
+ ## 9. 预计代码改动清单
455
+
456
+ 以下仅列后续实施位置,不在本次修改:
457
+
458
+ | 位置 | 预计改动 |
459
+ |---|---|
460
+ | 后端 `app/api/v1/` 与 router 注册 | 独立 CLI auth router,上述新接口,保持现有网页路由兼容 |
461
+ | 后端 `app/schemas/` | E1 解密后的申请、批准、authorization_code/refresh grant、revoke payload 与响应 schema |
462
+ | 后端 identity application/infrastructure | 授权申请服务、双类别 session 绑定、同类会话原子替换、分类刷新/撤销与全局撤销;不要把全部逻辑堆入已受架构行数约束的 AuthService |
463
+ | 后端 `app/core/redis.py` 或独立 session store | 授权码原子消费、类别状态键封装、会话 CAS、短期加密响应恢复、全局撤销协调与旧键兼容 |
464
+ | 后端 E1/S1 与 CSRF 中间件 | 明确新路径的强制模式、密文解包复用、不得误豁免;维持 sign-session 启动能力 |
465
+ | 后端 `app/api/v1/auth.py` | CLI formatter 分离、网页 refresh 的 token 类型分流、普通退出与全局退出区分;不改旧网页 Cookie 交付约定 |
466
+ | 后端普通/可选/WebSocket 鉴权与设备组装 | 全部按可信类别检查 active sid;设备列表支持两类在线;退出日志按 sid 更新 |
467
+ | 后端 config/audit/request logging/tests | 功能开关、限流/TTL、事件登记、敏感字段脱敏、并发与契约测试 |
468
+ | 网页 `/auth/cli/authorize` | 保存授权上下文、正常登录后返回、展示影响并明确确认、导航至服务端返回的回调地址;授权后网页保留自身独立会话 |
469
+ | CLI | PKCE/state、普通浏览器链接、一次性回调服务、E1 client、换码/刷新/撤销、安全存储、错误状态机;正式流程替换扩展桥接 |
470
+
471
+ 网页仍需一个最小授权入口。现有登录地址 `https://uzooai.com/auth/login?redirect=/workspace` 用于普通登录;CLI 授权场景应由授权页将登录成功后的站内 redirect 设置为自身受校验路径并保留申请 id,不能登录后一律丢回 workspace。不得把任意外部回调 URL 直接塞入普通登录页的 redirect;回环目标只由服务端申请记录持有。
472
+
473
+ 如果不修改现有业务前端,可以由后端提供独立授权网页,但它仍是新增网页代码,且必须正确接入当前 Bearer/S1 登录态;浏览器 HttpOnly Cookie 本身不等于现有 `get_current_user()` 所需的 Bearer。不能只新增后端 token 接口就宣称完成网页登录授权。
474
+
475
+ ## 10. 分阶段开发与验收
476
+
477
+ 本节是总体三阶段计划中“阶段二/三”的细分;阶段一现有 demo 不因此具备双端在线。按 **2A → 2B → 3A → 3B** 顺序交付。每一阶段先完成退出条件,再开启下一阶段的对外能力;文档、开发完成、隔离环境验收和生产启用分别记录,不混为同一完成状态。
478
+
479
+ | 子阶段 | 目标 | 修改范围 | 阶段退出条件 |
480
+ |---|---|---|---|
481
+ | 2A:兼容会话层 | web/cli 固定双类别、原子存储与可信鉴权 | 后端 session store、JWT、普通/可选/WebSocket 鉴权、刷新、退出、设备/审计、测试 | 旧网页无感兼容;隔离测试证明两端互不影响,同类仍只一个有效会话;CLI 对外开关仍关闭 |
482
+ | 2B:后端授权协议 | E1 申请/换码/刷新/撤销、PKCE、响应恢复 | 后端新 router/schema/service、中间件配置、限流/日志、契约测试 | 全部授权与竞争测试通过;能用测试客户端独立建立 CLI sid;未接网页时不宣称可网页登录 |
483
+ | 3A:网页与 CLI 接入 | 无扩展的完整普通浏览器授权 | 网页授权页及登录返回上下文、CLI E1/callback/存储/自刷新、帮助与 skill | 测试账号真实网页登录、独立刷新和撤销通过;网页登录保留,CLI 无需 Cookie 导入或扩展 |
484
+ | 3B:兼容回归与灰度 | 完整链路、混合版本约束、手动上线与回滚 | 隔离环境联调、设备展示、支持平台、发布检查与运行文档 | 双端/同端替换/全局撤销/故障恢复矩阵全绿;维护者按发布流程逐步开启并验证回滚 |
485
+
486
+ ### 10.1 阶段 2A:先改会话基础,保持对外兼容
487
+
488
+ - 统一 client_type 的签发、可信解析和类别键访问;网页保留原键,旧 JWT 无类别按 web 处理。清点所有会话读写与安全撤销调用方,禁止遗漏 optional auth/WebSocket。
489
+ - 引入原子签发/刷新/撤销,区分 logout_current 与 revoke_all;确认重放沿用全账号安全处置。设备在线状态支持两类,退出日志按 sid 回填。
490
+ - 用隔离测试凭据验证 web/cli 双类别,不开放公开 CLI 登录;`CLI_SESSION_ENABLED` 和 `CLI_AUTH_ENABLED` 默认关闭。
491
+ - 必测:旧网页登录/刷新/退出不回归;同类二次登录只替换同类;两类同时刷新不冲突;旧请求不删除新状态;账号禁用与全局撤销覆盖两类。
492
+
493
+ 该子阶段主要会话改造粗估 3~5 个开发人日,另预留 1~3 个联调/回归人日,仅作排期参考;不包含后续完整授权接口、网页和 CLI 开发。
494
+
495
+ ### 10.2 阶段 2B:实现授权接口与令牌服务
496
+
497
+ - 实现第 4 节的新接口、固定 session_policy、不可变申请、单次授权码与 PKCE。正常签发只影响 cli 类别,源 web sid 仅用于检查批准仍有效。
498
+ - E1 使用真实字节验证 RSA-OAEP、HKDF、GCM/AAD、pkid、六行规范串、时钟与重放;拒绝无 E1 的敏感请求,测试密钥轮换。
499
+ - 必测:参数/client/device/redirect/state/PKCE 绑定、拒绝/过期/重复批准、并发换码、同 exchange_id 恢复、失败不顶掉现有 CLI 或网页。
500
+ - 测试 CLI refresh 只能进入 CLI grant,网页 refresh 只能进入网页 formatter;两端 JTI、宽限缓存、签名密钥完全独立;真实重放仍触发全账号撤销。
501
+ - 完成加密恢复缓存与敏感字段脱敏。签名会话下发失败不丢令牌,恢复签名不额外轮换 refresh token。
502
+
503
+ ### 10.3 阶段 3A:网页确认、CLI 授权码与自刷新
504
+
505
+ - 授权页展示“网页保持在线、旧 CLI 可能被替换”,正常短信/滑块登录后保留申请上下文,批准后只把 code/state 通过顶层导航交给本机回调。
506
+ - CLI 完成回环监听、PKCE/state、E1、换码、系统安全存储、刷新串行锁、撤销和 2010/2011/2012 状态处理。当前扩展导入的 web token 不自动转换为 CLI token,正式功能需重新授权一次。
507
+ - 真实测试:网页 A 与 CLI B 同时可查询、各自刷新;网页 C 登录只替换 A,CLI D 登录只替换 B;普通 web 退出不影响 B,CLI revoke 不影响 A/C。
508
+ - 验证普通浏览器不依赖扩展、自动化窗口或复制令牌。前端设备页正确显示两类在线;skill 与帮助使用新授权流程。
509
+
510
+ ### 10.4 阶段 3B:回归、灰度、回滚
511
+
512
+ 发布前完整矩阵至少覆盖:
513
+
514
+ 1. 同类多设备并发登录,每类最终最多一个有效 sid/JTI;网页与 CLI 并发刷新互不影响。
515
+ 2. 网页批准后、换码前源 web sid 失效:拒绝换码且不影响已有 CLI;换码成功后普通 web 登录变化不影响 CLI。
516
+ 3. 同类刷新与新登录/退出竞争、全局撤销与换码竞争、过期与重放区分;不能遗留超额席位、恢复被撤销会话或误删新会话。
517
+ 4. 双端令牌/签名过期、刷新响应丢失、短期恢复限额、CLI 保存失败/重启/双进程;S1 仍检查对应类别 sid 与设备。
518
+ 5. 旧网页 JWT/Cookie/旧键兼容;HTTP、optional auth、WebSocket 认证一致;全局账号禁用/注销和安全撤销立即覆盖两类后续鉴权。
519
+ 6. 完成生成预估、幂等提交、等待和下载;重新认证不得自动重复扣费。真实生成与计费按用户授权执行。
520
+
521
+ 先部署兼容代码至**所有**鉴权与会话写入实例,再开启 CLI 类别和授权开关。旧实例不知道 cli 类别,可能将 CLI 按 web 处理或执行用户级删除,因此混合版本期间禁止公开启用;不能以灰度流量碰巧命中新实例代替兼容验证。`CLI_AUTH_ENABLED` 开启前必须先通过 `CLI_SESSION_ENABLED` 的隔离验收,生产由维护者按顺序控制。
522
+
523
+ 回滚顺序:停止新 CLI 授权/换码与新 CLI 刷新,显式撤销现有 CLI 类别会话和未完成授权/恢复记录,停止接受 CLI 类别业务鉴权,确认 web 旧键/会话保持可用后,再回滚不认识 cli 的旧代码。只关闭新授权接口不会自动撤销已签发 CLI token,不可视为完整回滚;回滚不得清空网页状态或整个 Redis。说明 CLI 需在恢复功能后重新授权。
524
+
525
+ 部署由维护者手动执行。本次文档不授权生产发布、重启容器或执行迁移。阶段 2A/2B 的后端测试通过也不代表阶段 3A/3B 已完成;排期和验收逐项记录。
526
+
527
+ ## 11. 相关标准
528
+
529
+ 本方案使用普通外部浏览器与回环回调的设计依据见 [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html#section-7.3);PKCE 的 challenge/verifier 绑定依据见 [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636.html#section-4)。刷新凭据轮换与重放风险参考 [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.14)。E1 封包、S1 签名以及分端在线/同类唯一规则是本项目自身契约,不应标为这些标准已经规定的现成接口。
530
+
531
+
532
+ ## 12. 阶段 2A 实际实施状态(2026-10-27)
533
+
534
+ 后端在 `cli-liu` 的 `4010259` 基线上完成公共兼容会话层;隔离回归 **203 passed**,独立本地 MySQL/Redis/MinIO 与真实 HTTP/S1/通知 WS 冒烟通过。两个 CLI 开关继续默认关闭,没有新增 `/auth/cli/*` 授权接口,没有修改业务前端或 CLI 登录,也没有生产发布。
535
+
536
+ 实际采用第 7.1 节建议的 web/cli 键布局,新增服务端会话元数据与账号 epoch。新轮换恢复记录加密、有界并随替换/退出/全局撤销清理;升级前的旧 web 明文宽限记录仅在原短 TTL 内兼容读取。CLI sid 使用服务端保留的 `cli2a:` 前缀,复用现有 login_logs.session_id 归类离线设备;认证仍以已验签类别和服务端记录为准,无新增 users 字段或迁移。
537
+
538
+ 两个 WS 端点及投递管理器在后续操作/推送前复验,空闲连接可能保留到下次活动;未实现主动瞬时断开。后台禁用、恢复与所有独立会话写入方的联调是公开启用前置条件。2B 必须把授权码消费、源 web sid 与全局 epoch 校验、CLI 席位和兑换恢复一起可靠提交,不能将 2A 的内部 issue 当成完整授权接口。
539
+
540
+ 完整清单与验收证据见 [后端阶段 2A 实施记录](../../anime-ai-engine/docs/阶段2A兼容会话层实施记录.md);本地运行步骤见 [测试部署说明](../../anime-ai-engine/deploy/local-session-2a/README.md)。