refactor: update user identifiers from string to number across multiple files
ZhenYi committed
This change is very large, so some diffs are not shown. Clone the repository to see the full change.
Deleted .opencode/goal-log.jsonl .opencode/goal-log.jsonl +0 −525
@@ -1,525 +0,0 @@
1{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":1,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792827632882,"roundsStarted":0,"waitUntil":0,"usage":{"inputTokens":0,"outputTokens":0,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
2{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834366939,"expiresAt":1792834366939,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
3{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792834366941,"roundsStarted":1,"waitUntil":0,"usage":{"inputTokens":0,"outputTokens":0,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
4{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834366943,"expiresAt":1792834426943,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
5{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792834366944,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
6{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":713976,"outputTokens":807,"cost":0},"at":1792834387388,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
7{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":716702,"outputTokens":243,"cost":0},"at":1792834397171,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
8{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834397172,"expiresAt":1792834457172,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
9{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":717431,"outputTokens":414,"cost":0},"at":1792834408245,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
10{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":718059,"outputTokens":829,"cost":0},"at":1792834423346,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
11{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":722046,"outputTokens":289,"cost":0},"at":1792834433352,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
12{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834433353,"expiresAt":1792834493352,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
13{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":722372,"outputTokens":95,"cost":0},"at":1792834440310,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
14{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":723400,"outputTokens":586,"cost":0},"at":1792834452189,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
15{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":726034,"outputTokens":456,"cost":0},"at":1792834464422,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
16{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834464423,"expiresAt":1792834524423,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
17{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":726649,"outputTokens":112,"cost":0},"at":1792834473344,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
18{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":728351,"outputTokens":179,"cost":0},"at":1792834482159,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
19{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":728989,"outputTokens":143,"cost":0},"at":1792834491488,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
20{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":729453,"outputTokens":132,"cost":0},"at":1792834499626,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
21{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834499627,"expiresAt":1792834559627,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
22{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":730449,"outputTokens":137,"cost":0},"at":1792834508741,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
23{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":730772,"outputTokens":208,"cost":0},"at":1792834517366,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
24{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":731008,"outputTokens":165,"cost":0},"at":1792834528614,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
25{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":731201,"outputTokens":126,"cost":0},"at":1792834535451,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
26{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834535452,"expiresAt":1792834595452,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
27{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":731359,"outputTokens":160,"cost":0},"at":1792834542984,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
28{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":731549,"outputTokens":64,"cost":0},"at":1792834550603,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
29{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":731760,"outputTokens":176,"cost":0},"at":1792834557684,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
30{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":731966,"outputTokens":239,"cost":0},"at":1792834566774,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
31{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834566775,"expiresAt":1792834626775,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
32{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":732232,"outputTokens":117,"cost":0},"at":1792834575560,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
33{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":733192,"outputTokens":722,"cost":0},"at":1792834587346,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
34{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":733953,"outputTokens":63,"cost":0},"at":1792834594065,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
35{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":734092,"outputTokens":241,"cost":0},"at":1792834603455,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
36{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834603456,"expiresAt":1792834663456,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
37{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":734783,"outputTokens":136,"cost":0},"at":1792834612064,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
38{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":735552,"outputTokens":485,"cost":0},"at":1792834623179,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
39{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":736410,"outputTokens":126,"cost":0},"at":1792834631732,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
40{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":736622,"outputTokens":165,"cost":0},"at":1792834640617,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
41{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834640618,"expiresAt":1792834700618,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
42{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":736873,"outputTokens":240,"cost":0},"at":1792834651190,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
43{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":737658,"outputTokens":1797,"cost":0},"at":1792834670166,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
44{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":739496,"outputTokens":292,"cost":0},"at":1792834680509,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
45{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834680510,"expiresAt":1792834740510,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
46{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":739897,"outputTokens":184,"cost":0},"at":1792834710007,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
47{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":740241,"outputTokens":1032,"cost":0},"at":1792834725514,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
48{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834725515,"expiresAt":1792834785515,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
49{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":741314,"outputTokens":173,"cost":0},"at":1792834749846,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
50{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":741671,"outputTokens":125,"cost":0},"at":1792834758501,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
51{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834758502,"expiresAt":1792834818502,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
52{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":741818,"outputTokens":306,"cost":0},"at":1792834768426,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
53{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":742460,"outputTokens":323,"cost":0},"at":1792834777879,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
54{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":742813,"outputTokens":899,"cost":0},"at":1792834789969,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
55{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834789970,"expiresAt":1792834849970,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
56{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":743740,"outputTokens":186,"cost":0},"at":1792834803294,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
57{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":744727,"outputTokens":252,"cost":0},"at":1792834812310,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
58{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":745443,"outputTokens":501,"cost":0},"at":1792834822531,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
59{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834822532,"expiresAt":1792834882532,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
60{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":745974,"outputTokens":144,"cost":0},"at":1792834836616,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
61{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":746775,"outputTokens":82,"cost":0},"at":1792834844793,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
62{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":747046,"outputTokens":141,"cost":0},"at":1792834852437,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
63{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":747217,"outputTokens":144,"cost":0},"at":1792834876594,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
64{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834876595,"expiresAt":1792834936595,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
65{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":747716,"outputTokens":136,"cost":0},"at":1792834883784,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
66{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":747882,"outputTokens":144,"cost":0},"at":1792834909348,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
67{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834909350,"expiresAt":1792834969350,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
68{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":748386,"outputTokens":290,"cost":0},"at":1792834921164,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
69{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":748897,"outputTokens":112,"cost":0},"at":1792834929035,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
70{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":749039,"outputTokens":136,"cost":0},"at":1792834953903,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
71{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834953905,"expiresAt":1792835013904,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
72{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":749195,"outputTokens":127,"cost":0},"at":1792834962576,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
73{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":749339,"outputTokens":881,"cost":0},"at":1792834978108,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
74{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":752361,"outputTokens":318,"cost":0},"at":1792834988772,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
75{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792834988774,"expiresAt":1792835048773,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
76{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":753803,"outputTokens":611,"cost":0},"at":1792835002221,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
77{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":754738,"outputTokens":210,"cost":0},"at":1792835012175,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
78{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":755493,"outputTokens":151,"cost":0},"at":1792835020893,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
79{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835020895,"expiresAt":1792835080894,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
80{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":755700,"outputTokens":134,"cost":0},"at":1792835029619,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
81{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":756819,"outputTokens":263,"cost":0},"at":1792835039079,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
82{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":757121,"outputTokens":141,"cost":0},"at":1792835048149,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
83{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":758479,"outputTokens":185,"cost":0},"at":1792835056878,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
84{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835056879,"expiresAt":1792835116879,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
85{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":758842,"outputTokens":168,"cost":0},"at":1792835065785,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
86{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":759395,"outputTokens":164,"cost":0},"at":1792835075303,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
87{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":759616,"outputTokens":772,"cost":0},"at":1792835087107,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
88{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835087109,"expiresAt":1792835147109,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
89{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":760429,"outputTokens":65,"cost":0},"at":1792835094956,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
90{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":760561,"outputTokens":654,"cost":0},"at":1792835105903,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
91{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":761250,"outputTokens":460,"cost":0},"at":1792835115395,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
92{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":761745,"outputTokens":182,"cost":0},"at":1792835124222,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
93{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835124224,"expiresAt":1792835184223,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
94{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":761941,"outputTokens":80,"cost":0},"at":1792835132540,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
95{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":762121,"outputTokens":367,"cost":0},"at":1792835141028,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
96{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":762517,"outputTokens":304,"cost":0},"at":1792835151124,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
97{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":762939,"outputTokens":173,"cost":0},"at":1792835159032,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
98{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835159034,"expiresAt":1792835219033,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
99{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":763147,"outputTokens":330,"cost":0},"at":1792835168179,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
100{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":763508,"outputTokens":113,"cost":0},"at":1792835176170,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
101{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":763652,"outputTokens":150,"cost":0},"at":1792835184041,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
102{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":763833,"outputTokens":116,"cost":0},"at":1792835191345,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
103{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835191346,"expiresAt":1792835251346,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
104{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":763980,"outputTokens":289,"cost":0},"at":1792835201308,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
105{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":764670,"outputTokens":451,"cost":0},"at":1792835211281,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
106{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":765152,"outputTokens":560,"cost":0},"at":1792835223430,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
107{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835223432,"expiresAt":1792835283432,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
108{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":766218,"outputTokens":344,"cost":0},"at":1792835232010,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
109{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":766593,"outputTokens":145,"cost":0},"at":1792835241655,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
110{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":767007,"outputTokens":126,"cost":0},"at":1792835249892,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
111{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":767164,"outputTokens":846,"cost":0},"at":1792835263015,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
112{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835263016,"expiresAt":1792835323016,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
113{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":768024,"outputTokens":405,"cost":0},"at":1792835274932,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
114{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":768558,"outputTokens":70,"cost":0},"at":1792835283837,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
115{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":769026,"outputTokens":247,"cost":0},"at":1792835309739,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
116{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835309740,"expiresAt":1792835369740,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
117{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":770110,"outputTokens":102,"cost":0},"at":1792835318374,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
118{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":771869,"outputTokens":278,"cost":0},"at":1792835327870,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
119{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":772313,"outputTokens":194,"cost":0},"at":1792835337355,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
120{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":774232,"outputTokens":1304,"cost":0},"at":1792835353475,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
121{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835353477,"expiresAt":1792835413477,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
122{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":775578,"outputTokens":66,"cost":0},"at":1792835361740,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
123{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":775704,"outputTokens":112,"cost":0},"at":1792835370940,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
124{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":776790,"outputTokens":953,"cost":0},"at":1792835384016,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
125{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835384017,"expiresAt":1792835444017,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
126{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":777784,"outputTokens":170,"cost":0},"at":1792835409527,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
127{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":778449,"outputTokens":157,"cost":0},"at":1792835418300,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
128{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835418301,"expiresAt":1792835478301,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
129{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":779087,"outputTokens":132,"cost":0},"at":1792835425744,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
130{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":779250,"outputTokens":180,"cost":0},"at":1792835433496,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
131{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":779459,"outputTokens":259,"cost":0},"at":1792835443681,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
132{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":780802,"outputTokens":286,"cost":0},"at":1792835456969,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
133{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835456971,"expiresAt":1792835516970,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
134{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":782741,"outputTokens":232,"cost":0},"at":1792835467412,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
135{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":783514,"outputTokens":538,"cost":0},"at":1792835478861,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
136{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":784895,"outputTokens":421,"cost":0},"at":1792835490664,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
137{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835490665,"expiresAt":1792835550665,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
138{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":785375,"outputTokens":137,"cost":0},"at":1792835499850,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
139{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":785704,"outputTokens":108,"cost":0},"at":1792835510556,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
140{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":786126,"outputTokens":402,"cost":0},"at":1792835519684,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
141{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":786557,"outputTokens":133,"cost":0},"at":1792835527957,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
142{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835527959,"expiresAt":1792835587959,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
143{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":786728,"outputTokens":154,"cost":0},"at":1792835535882,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
144{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":786911,"outputTokens":411,"cost":0},"at":1792835547207,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
145{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":787997,"outputTokens":239,"cost":0},"at":1792835558017,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
146{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835558019,"expiresAt":1792835618019,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
147{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":788669,"outputTokens":3749,"cost":0},"at":1792835588576,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
148{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835588577,"expiresAt":1792835648577,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
149{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":792448,"outputTokens":229,"cost":0},"at":1792835598054,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
150{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":793145,"outputTokens":145,"cost":0},"at":1792835608168,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
151{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":793393,"outputTokens":3444,"cost":0},"at":1792835638244,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
152{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835638245,"expiresAt":1792835698245,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
153{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":796879,"outputTokens":171,"cost":0},"at":1792835653689,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
154{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":798582,"outputTokens":228,"cost":0},"at":1792835664556,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
155{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":799248,"outputTokens":820,"cost":0},"at":1792835680660,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
156{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835680661,"expiresAt":1792835740661,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
157{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":800580,"outputTokens":214,"cost":0},"at":1792835691444,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
158{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":801133,"outputTokens":1173,"cost":0},"at":1792835709293,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
159{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":802347,"outputTokens":170,"cost":0},"at":1792835721404,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
160{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835721405,"expiresAt":1792835781405,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
161{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":803469,"outputTokens":135,"cost":0},"at":1792835730648,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
162{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":804130,"outputTokens":260,"cost":0},"at":1792835742112,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
163{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":804724,"outputTokens":863,"cost":0},"at":1792835755930,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
164{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835755931,"expiresAt":1792835815931,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
165{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":805629,"outputTokens":171,"cost":0},"at":1792835771263,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
166{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":806889,"outputTokens":208,"cost":0},"at":1792835782146,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
167{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":807272,"outputTokens":536,"cost":0},"at":1792835793568,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
168{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835793570,"expiresAt":1792835853569,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
169{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":807850,"outputTokens":171,"cost":0},"at":1792835809423,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
170{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":809126,"outputTokens":197,"cost":0},"at":1792835820274,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
171{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":809336,"outputTokens":122,"cost":0},"at":1792835829771,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
172{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835829772,"expiresAt":1792835889772,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
173{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":809601,"outputTokens":190,"cost":0},"at":1792835839538,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
174{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":809958,"outputTokens":828,"cost":0},"at":1792835853245,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
175{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":810827,"outputTokens":170,"cost":0},"at":1792835880216,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
176{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835880218,"expiresAt":1792835940218,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
177{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":811032,"outputTokens":122,"cost":0},"at":1792835890523,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
178{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":811264,"outputTokens":349,"cost":0},"at":1792835923080,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
179{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835923082,"expiresAt":1792835983081,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
180{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":811633,"outputTokens":85,"cost":0},"at":1792835933731,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
181{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":814201,"outputTokens":896,"cost":0},"at":1792835951236,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
182{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":816021,"outputTokens":587,"cost":0},"at":1792835969181,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
183{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792835969182,"expiresAt":1792836029182,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
184{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":817587,"outputTokens":242,"cost":0},"at":1792835980438,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
185{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":818068,"outputTokens":1089,"cost":0},"at":1792835996698,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
186{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":819179,"outputTokens":1086,"cost":0},"at":1792836015495,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
187{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836015496,"expiresAt":1792836075496,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
188{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":820288,"outputTokens":375,"cost":0},"at":1792836089682,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
189{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836089684,"expiresAt":1792836149683,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
190{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":820729,"outputTokens":215,"cost":0},"at":1792836104776,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
191{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":820964,"outputTokens":83,"cost":0},"at":1792836115945,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
192{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":822732,"outputTokens":823,"cost":0},"at":1792836129665,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
193{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836129666,"expiresAt":1792836189666,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
194{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":823577,"outputTokens":784,"cost":0},"at":1792836143352,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
195{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":824399,"outputTokens":126,"cost":0},"at":1792836159863,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
196{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836159866,"expiresAt":1792836219865,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
197{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":824550,"outputTokens":215,"cost":0},"at":1792836178313,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
198{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":824786,"outputTokens":843,"cost":0},"at":1792836193145,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
199{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836193147,"expiresAt":1792836253146,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
200{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":825648,"outputTokens":359,"cost":0},"at":1792836204603,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
201{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":826730,"outputTokens":441,"cost":0},"at":1792836216548,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
202{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":827713,"outputTokens":594,"cost":0},"at":1792836233025,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
203{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836233026,"expiresAt":1792836293026,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
204{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":828344,"outputTokens":119,"cost":0},"at":1792836260870,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
205{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":828511,"outputTokens":615,"cost":0},"at":1792836275325,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
206{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836275328,"expiresAt":1792836335328,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
207{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":829174,"outputTokens":2469,"cost":0},"at":1792836300148,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
208{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":831665,"outputTokens":354,"cost":0},"at":1792836338226,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
209{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836338228,"expiresAt":1792836398227,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
210{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":832689,"outputTokens":166,"cost":0},"at":1792836348258,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
211{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":833091,"outputTokens":188,"cost":0},"at":1792836358666,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
212{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":833296,"outputTokens":216,"cost":0},"at":1792836373229,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
213{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836373231,"expiresAt":1792836433230,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
214{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":834955,"outputTokens":731,"cost":0},"at":1792836386676,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
215{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":835723,"outputTokens":123,"cost":0},"at":1792836400272,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
216{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":836031,"outputTokens":164,"cost":0},"at":1792836409309,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
217{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836409311,"expiresAt":1792836469311,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
218{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":836217,"outputTokens":95,"cost":0},"at":1792836422364,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
219{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":836389,"outputTokens":167,"cost":0},"at":1792836431703,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
220{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":836578,"outputTokens":95,"cost":0},"at":1792836445051,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
221{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836445053,"expiresAt":1792836505053,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
222{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":836690,"outputTokens":249,"cost":0},"at":1792836455811,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
223{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":837768,"outputTokens":1478,"cost":0},"at":1792836475241,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
224{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836475243,"expiresAt":1792836535242,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
225{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":839287,"outputTokens":181,"cost":0},"at":1792836501592,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
226{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":840253,"outputTokens":126,"cost":0},"at":1792836511066,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
227{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836511067,"expiresAt":1792836571067,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
228{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":840523,"outputTokens":168,"cost":0},"at":1792836520154,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
229{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":840722,"outputTokens":184,"cost":0},"at":1792836548209,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
230{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836548211,"expiresAt":1792836608211,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
231{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":840923,"outputTokens":499,"cost":0},"at":1792836561276,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
232{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":843209,"outputTokens":460,"cost":0},"at":1792836573853,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
233{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":844273,"outputTokens":1313,"cost":0},"at":1792836591414,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
234{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836591415,"expiresAt":1792836651415,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
235{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":845608,"outputTokens":133,"cost":0},"at":1792836603421,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
236{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":845756,"outputTokens":323,"cost":0},"at":1792836614558,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
237{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":846101,"outputTokens":1427,"cost":0},"at":1792836633794,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
238{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836633797,"expiresAt":1792836693796,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
239{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":847564,"outputTokens":122,"cost":0},"at":1792836663349,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
240{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":847936,"outputTokens":150,"cost":0},"at":1792836676016,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
241{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836676020,"expiresAt":1792836736019,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
242{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":848109,"outputTokens":99,"cost":0},"at":1792836693450,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
243{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":848228,"outputTokens":100,"cost":0},"at":1792836721191,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
244{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836721193,"expiresAt":1792836781193,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
245{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":848345,"outputTokens":353,"cost":0},"at":1792836744595,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
246{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":848808,"outputTokens":168,"cost":0},"at":1792836756891,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
247{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836756894,"expiresAt":1792836816893,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
248{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":849406,"outputTokens":201,"cost":0},"at":1792836766408,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
249{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":849630,"outputTokens":116,"cost":0},"at":1792836792028,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
250{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836792031,"expiresAt":1792836852030,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
251{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":195141,"outputTokens":395,"cost":0},"at":1792836868802,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
252{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836868804,"expiresAt":1792836928804,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
253{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":196135,"outputTokens":120,"cost":0},"at":1792836873975,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
254{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":199449,"outputTokens":969,"cost":0},"at":1792836885855,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
255{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":204961,"outputTokens":1009,"cost":0},"at":1792836903080,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
256{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836903083,"expiresAt":1792836963082,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
257{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":208080,"outputTokens":199,"cost":0},"at":1792836910437,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
258{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":210022,"outputTokens":708,"cost":0},"at":1792836936138,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
259{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836936141,"expiresAt":1792836996140,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
260{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":212737,"outputTokens":1128,"cost":0},"at":1792836951238,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
261{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":216851,"outputTokens":559,"cost":0},"at":1792836961499,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
262{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":221012,"outputTokens":825,"cost":0},"at":1792836972885,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
263{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792836972888,"expiresAt":1792837032887,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
264{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":224081,"outputTokens":419,"cost":0},"at":1792836980761,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
265{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":224995,"outputTokens":103,"cost":0},"at":1792836985837,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
266{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":225482,"outputTokens":91,"cost":0},"at":1792836990765,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
267{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":225719,"outputTokens":1405,"cost":0},"at":1792837007260,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
268{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837007263,"expiresAt":1792837067262,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
269{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":228283,"outputTokens":2584,"cost":0},"at":1792837028360,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
270{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":230923,"outputTokens":399,"cost":0},"at":1792837052643,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
271{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837052646,"expiresAt":1792837112645,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
272{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":232322,"outputTokens":865,"cost":0},"at":1792837061406,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
273{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":233226,"outputTokens":88,"cost":0},"at":1792837070389,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
274{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":233569,"outputTokens":989,"cost":0},"at":1792837085871,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
275{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837085878,"expiresAt":1792837145877,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
276{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":235510,"outputTokens":1411,"cost":0},"at":1792837121593,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
277{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837121596,"expiresAt":1792837181595,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
278{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":236961,"outputTokens":172,"cost":0},"at":1792837159034,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
279{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837159037,"expiresAt":1792837219036,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
280{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":237511,"outputTokens":109,"cost":0},"at":1792837167194,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
281{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":237741,"outputTokens":249,"cost":0},"at":1792837177447,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
282{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":238019,"outputTokens":555,"cost":0},"at":1792837188131,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
283{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":238601,"outputTokens":144,"cost":0},"at":1792837246353,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
284{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837246357,"expiresAt":1792837306356,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
285{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":238761,"outputTokens":679,"cost":0},"at":1792837255802,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
286{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":239460,"outputTokens":347,"cost":0},"at":1792837264983,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
287{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":241142,"outputTokens":2635,"cost":0},"at":1792837287638,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
288{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837287641,"expiresAt":1792837347640,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
289{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":243815,"outputTokens":169,"cost":0},"at":1792837306060,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
290{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":244188,"outputTokens":236,"cost":0},"at":1792837334585,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
291{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837334588,"expiresAt":1792837394587,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
292{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":245262,"outputTokens":538,"cost":0},"at":1792837344176,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
293{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":246580,"outputTokens":868,"cost":0},"at":1792837354099,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
294{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":247489,"outputTokens":108,"cost":0},"at":1792837368317,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
295{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837368349,"expiresAt":1792837428348,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
296{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":247678,"outputTokens":62,"cost":0},"at":1792837375285,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
297{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":247967,"outputTokens":582,"cost":0},"at":1792837382509,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
298{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":248573,"outputTokens":108,"cost":0},"at":1792837396273,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
299{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":248699,"outputTokens":122,"cost":0},"at":1792837442046,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
300{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837442050,"expiresAt":1792837502049,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
301{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":248874,"outputTokens":581,"cost":0},"at":1792837451238,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
302{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":251991,"outputTokens":61,"cost":0},"at":1792837457530,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
303{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":254300,"outputTokens":86,"cost":0},"at":1792837464510,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
304{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":257256,"outputTokens":303,"cost":0},"at":1792837471695,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
305{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":257952,"outputTokens":515,"cost":0},"at":1792837478774,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
306{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837478777,"expiresAt":1792837538776,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
307{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":258618,"outputTokens":196,"cost":0},"at":1792837484683,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
308{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":259392,"outputTokens":338,"cost":0},"at":1792837493504,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
309{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":261197,"outputTokens":2139,"cost":0},"at":1792837505467,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
310{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":263377,"outputTokens":171,"cost":0},"at":1792837542336,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
311{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837542340,"expiresAt":1792837602339,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
312{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":263850,"outputTokens":188,"cost":0},"at":1792837548753,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
313{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":264176,"outputTokens":446,"cost":0},"at":1792837586274,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
314{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837586277,"expiresAt":1792837646276,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
315{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":264642,"outputTokens":2072,"cost":0},"at":1792837596667,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
316{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":266752,"outputTokens":169,"cost":0},"at":1792837607441,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
317{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":266945,"outputTokens":2946,"cost":0},"at":1792837620667,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
318{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837620671,"expiresAt":1792837680670,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
319{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":269930,"outputTokens":133,"cost":0},"at":1792837677276,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
320{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837677280,"expiresAt":1792837737279,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
321{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":270087,"outputTokens":153,"cost":0},"at":1792837685735,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
322{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":270862,"outputTokens":323,"cost":0},"at":1792837693989,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
323{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":271269,"outputTokens":168,"cost":0},"at":1792837702013,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
324{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":271476,"outputTokens":133,"cost":0},"at":1792837714579,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
325{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837714582,"expiresAt":1792837774581,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
326{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":271695,"outputTokens":155,"cost":0},"at":1792837756873,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
327{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837756876,"expiresAt":1792837816875,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
328{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":272474,"outputTokens":124,"cost":0},"at":1792837767401,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
329{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":272963,"outputTokens":286,"cost":0},"at":1792837785419,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
330{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":273269,"outputTokens":1260,"cost":0},"at":1792837796629,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
331{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837796632,"expiresAt":1792837856631,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
332{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":274567,"outputTokens":134,"cost":0},"at":1792837808634,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
333{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":274789,"outputTokens":221,"cost":0},"at":1792837813550,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
334{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":275069,"outputTokens":206,"cost":0},"at":1792837818641,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
335{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":275296,"outputTokens":109,"cost":0},"at":1792837829910,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
336{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837829914,"expiresAt":1792837889913,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
337{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":275489,"outputTokens":197,"cost":0},"at":1792837834643,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
338{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":275726,"outputTokens":63,"cost":0},"at":1792837839932,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
339{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":276007,"outputTokens":121,"cost":0},"at":1792837844247,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
340{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":276149,"outputTokens":109,"cost":0},"at":1792837861194,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
341{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837861198,"expiresAt":1792837921197,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
342{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":276275,"outputTokens":101,"cost":0},"at":1792837867511,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
343{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":279905,"outputTokens":485,"cost":0},"at":1792837880409,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
344{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":281465,"outputTokens":126,"cost":0},"at":1792837886840,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
345{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":282283,"outputTokens":547,"cost":0},"at":1792837897387,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
346{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837897391,"expiresAt":1792837957390,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
347{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":284350,"outputTokens":1689,"cost":0},"at":1792837911495,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
348{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":286080,"outputTokens":173,"cost":0},"at":1792837953767,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
349{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837953770,"expiresAt":1792838013770,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
350{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":286300,"outputTokens":136,"cost":0},"at":1792837960089,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
351{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":286756,"outputTokens":283,"cost":0},"at":1792837964966,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
352{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":287068,"outputTokens":2084,"cost":0},"at":1792837983104,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
353{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":289189,"outputTokens":170,"cost":0},"at":1792837994604,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
354{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792837994607,"expiresAt":1792838054606,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
355{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":289383,"outputTokens":3034,"cost":0},"at":1792838018471,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
356{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":292455,"outputTokens":134,"cost":0},"at":1792838046013,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
357{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838046016,"expiresAt":1792838106015,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
358{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":292702,"outputTokens":322,"cost":0},"at":1792838060734,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
359{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":293151,"outputTokens":62,"cost":0},"at":1792838066515,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
360{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":293428,"outputTokens":320,"cost":0},"at":1792838079797,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
361{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838079801,"expiresAt":1792838139800,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
362{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":293898,"outputTokens":509,"cost":0},"at":1792838094991,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
363{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":294427,"outputTokens":172,"cost":0},"at":1792838101582,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
364{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":296751,"outputTokens":63,"cost":0},"at":1792838106908,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
365{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":299369,"outputTokens":63,"cost":0},"at":1792838113383,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
366{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838113387,"expiresAt":1792838173386,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
367{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":302070,"outputTokens":1113,"cost":0},"at":1792838129076,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
368{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":304434,"outputTokens":608,"cost":0},"at":1792838183094,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
369{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838183098,"expiresAt":1792838243097,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
370{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":305071,"outputTokens":272,"cost":0},"at":1792838193291,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
371{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":305401,"outputTokens":136,"cost":0},"at":1792838199511,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
372{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":306782,"outputTokens":638,"cost":0},"at":1792838207768,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
373{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":307458,"outputTokens":62,"cost":0},"at":1792838213112,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
374{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838213115,"expiresAt":1792838273115,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
375{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":307536,"outputTokens":1771,"cost":0},"at":1792838229236,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
376{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":309345,"outputTokens":188,"cost":0},"at":1792838236348,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
377{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":310385,"outputTokens":237,"cost":0},"at":1792838243687,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
378{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838243690,"expiresAt":1792838303689,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
379{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":311111,"outputTokens":1149,"cost":0},"at":1792838254782,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
380{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":312301,"outputTokens":3004,"cost":0},"at":1792838278984,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
381{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838278987,"expiresAt":1792838338986,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
382{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":315344,"outputTokens":190,"cost":0},"at":1792838290153,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
383{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":315768,"outputTokens":166,"cost":0},"at":1792838298480,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
384{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":316529,"outputTokens":459,"cost":0},"at":1792838305189,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
385{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":317026,"outputTokens":134,"cost":0},"at":1792838322738,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
386{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838322744,"expiresAt":1792838382742,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
387{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":317246,"outputTokens":485,"cost":0},"at":1792838339776,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
388{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":317751,"outputTokens":414,"cost":0},"at":1792838348135,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
389{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":319739,"outputTokens":193,"cost":0},"at":1792838355046,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
390{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838355050,"expiresAt":1792838415049,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
391{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":322036,"outputTokens":204,"cost":0},"at":1792838362520,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
392{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":324256,"outputTokens":1746,"cost":0},"at":1792838376335,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
393{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":326040,"outputTokens":2738,"cost":0},"at":1792838396207,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
394{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838396210,"expiresAt":1792838456209,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
395{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":328817,"outputTokens":161,"cost":0},"at":1792838412411,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
396{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":329017,"outputTokens":190,"cost":0},"at":1792838419933,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
397{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":330185,"outputTokens":472,"cost":0},"at":1792838512869,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
398{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838512872,"expiresAt":1792838572872,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
399{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":330686,"outputTokens":119,"cost":0},"at":1792838521515,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
400{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":335540,"outputTokens":171,"cost":0},"at":1792838528953,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
401{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":335729,"outputTokens":471,"cost":0},"at":1792838535512,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
402{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":336239,"outputTokens":63,"cost":0},"at":1792838542123,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
403{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":2},"delta":{"inputTokens":336318,"outputTokens":736,"cost":0},"at":1792838551117,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
404{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838551161,"expiresAt":1792838611160,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
405{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838551164,"expiresAt":1792838551164,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
406{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792838551168,"roundsStarted":2,"waitUntil":0,"usage":{"inputTokens":173603273,"outputTokens":134502,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
407{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838551172,"expiresAt":1792838611171,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
408{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792838551177,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
409{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":337444,"outputTokens":1124,"cost":0},"at":1792838566201,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
410{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":339391,"outputTokens":578,"cost":0},"at":1792838576076,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
411{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":340216,"outputTokens":290,"cost":0},"at":1792838582002,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
412{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838582005,"expiresAt":1792838642004,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
413{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":340529,"outputTokens":60,"cost":0},"at":1792838587400,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
414{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":340826,"outputTokens":335,"cost":0},"at":1792838592810,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
415{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":341200,"outputTokens":970,"cost":0},"at":1792838601904,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
416{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":342211,"outputTokens":1606,"cost":0},"at":1792838615391,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
417{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838615396,"expiresAt":1792838675395,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
418{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":343859,"outputTokens":189,"cost":0},"at":1792838655664,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
419{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838655668,"expiresAt":1792838715667,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
420{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":344101,"outputTokens":552,"cost":0},"at":1792838727019,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
421{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838727023,"expiresAt":1792838787022,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
422{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":344673,"outputTokens":630,"cost":0},"at":1792838780037,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
423{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838780040,"expiresAt":1792838840039,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
424{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":345377,"outputTokens":416,"cost":0},"at":1792838786974,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
425{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":345833,"outputTokens":125,"cost":0},"at":1792838799506,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
426{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":345985,"outputTokens":566,"cost":0},"at":1792838810464,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
427{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838810469,"expiresAt":1792838870467,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
428{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":348055,"outputTokens":1422,"cost":0},"at":1792838824294,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
429{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":349518,"outputTokens":183,"cost":0},"at":1792838877921,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
430{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838877925,"expiresAt":1792838937924,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
431{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":349743,"outputTokens":228,"cost":0},"at":1792838885618,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
432{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":350937,"outputTokens":385,"cost":0},"at":1792838894933,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
433{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":351518,"outputTokens":589,"cost":0},"at":1792838904681,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
434{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":353427,"outputTokens":374,"cost":0},"at":1792838913072,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
435{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838913075,"expiresAt":1792838973075,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
436{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":353950,"outputTokens":1261,"cost":0},"at":1792838927484,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
437{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":355250,"outputTokens":150,"cost":0},"at":1792838980888,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
438{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792838980891,"expiresAt":1792839040890,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
439{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":355605,"outputTokens":753,"cost":0},"at":1792838997589,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
440{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":356538,"outputTokens":519,"cost":0},"at":1792839006191,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
441{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":357095,"outputTokens":149,"cost":0},"at":1792839164215,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
442{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839164219,"expiresAt":1792839224218,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
443{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":357267,"outputTokens":182,"cost":0},"at":1792839177041,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
444{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":357793,"outputTokens":188,"cost":0},"at":1792839184686,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
445{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":358371,"outputTokens":391,"cost":0},"at":1792839196096,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
446{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839196100,"expiresAt":1792839256099,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
447{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":358900,"outputTokens":72,"cost":0},"at":1792839206134,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
448{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":361073,"outputTokens":213,"cost":0},"at":1792839214688,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
449{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":364734,"outputTokens":518,"cost":0},"at":1792839231773,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
450{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839231777,"expiresAt":1792839291776,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
451{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":366519,"outputTokens":203,"cost":0},"at":1792839244253,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
452{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":368066,"outputTokens":497,"cost":0},"at":1792839302882,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
453{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839302885,"expiresAt":1792839362884,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
454{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":368869,"outputTokens":806,"cost":0},"at":1792839345692,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
455{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839345697,"expiresAt":1792839405696,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
456{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":369713,"outputTokens":799,"cost":0},"at":1792839360170,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
457{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":370550,"outputTokens":543,"cost":0},"at":1792839375905,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
458{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839375909,"expiresAt":1792839435908,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
459{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":371132,"outputTokens":200,"cost":0},"at":1792839436346,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
460{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839436350,"expiresAt":1792839496349,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
461{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":3},"delta":{"inputTokens":371436,"outputTokens":371,"cost":0},"at":1792839694404,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
462{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792839694408,"expiresAt":1792839754407,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
463{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792840404181,"expiresAt":1792840404181,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
464{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":4,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792840404186,"roundsStarted":3,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
465{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792840404190,"expiresAt":1792840464189,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
466{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792840404195,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
467{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792841110694,"expiresAt":1792841110694,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
468{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":5,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792841110699,"roundsStarted":4,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
469{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792841110704,"expiresAt":1792841170703,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
470{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792841110708,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
471{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792841814115,"expiresAt":1792841814115,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
472{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":6,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792841814119,"roundsStarted":5,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
473{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792841814123,"expiresAt":1792841874122,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
474{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792841814128,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
475{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792842522751,"expiresAt":1792842522751,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
476{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":7,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792842522754,"roundsStarted":6,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
477{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792842522758,"expiresAt":1792842582757,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
478{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792842522763,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
479{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792843223626,"expiresAt":1792843223626,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
480{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":8,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792843223630,"roundsStarted":7,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
481{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792843223634,"expiresAt":1792843283633,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
482{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792843223639,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
483{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792843931394,"expiresAt":1792843931394,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
484{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":9,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792843931398,"roundsStarted":8,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
485{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792843931403,"expiresAt":1792843991402,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
486{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792843931410,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
487{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792844633341,"expiresAt":1792844633341,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
488{"kind":"goal/change","version":1,"operation":"continue","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"active","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792844633347,"roundsStarted":9,"waitUntil":0,"usage":{"inputTokens":186680977,"outputTokens":152939,"cost":0}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
489{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792844633352,"expiresAt":1792844693351,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
490{"kind":"goal/session","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","sessionID":"ses_e93679c00ffe4y1dZna72QPquP","at":1792844633358,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
491{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":374907,"outputTokens":242,"cost":0},"at":1792845231175,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
492{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845231180,"expiresAt":1792845291179,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
493{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":375504,"outputTokens":190,"cost":0},"at":1792845245279,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
494{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":375829,"outputTokens":129,"cost":0},"at":1792845254895,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
495{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":376117,"outputTokens":61,"cost":0},"at":1792845259884,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
496{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":376288,"outputTokens":561,"cost":0},"at":1792845278776,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
497{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845278778,"expiresAt":1792845338778,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
498{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":376920,"outputTokens":808,"cost":0},"at":1792845290021,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
499{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":378232,"outputTokens":1954,"cost":0},"at":1792845307910,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
500{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":380226,"outputTokens":182,"cost":0},"at":1792845336686,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
501{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845336689,"expiresAt":1792845396688,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
502{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":380574,"outputTokens":144,"cost":0},"at":1792845342138,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
503{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":380792,"outputTokens":272,"cost":0},"at":1792845369819,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
504{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845369821,"expiresAt":1792845429821,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
505{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":381083,"outputTokens":1105,"cost":0},"at":1792845380424,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
506{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":382228,"outputTokens":186,"cost":0},"at":1792845420774,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
507{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845420778,"expiresAt":1792845480777,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
508{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":382446,"outputTokens":361,"cost":0},"at":1792845622850,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
509{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845622855,"expiresAt":1792845682854,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
510{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":383434,"outputTokens":221,"cost":0},"at":1792845633558,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
511{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":384479,"outputTokens":116,"cost":0},"at":1792845668161,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
512{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845668164,"expiresAt":1792845728163,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
513{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":384726,"outputTokens":149,"cost":0},"at":1792845680706,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
514{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":385018,"outputTokens":169,"cost":0},"at":1792845698906,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
515{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845698908,"expiresAt":1792845758908,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
516{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":386015,"outputTokens":856,"cost":0},"at":1792845714463,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
517{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":387140,"outputTokens":437,"cost":0},"at":1792845725063,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
518{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":388090,"outputTokens":596,"cost":0},"at":1792845734088,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
519{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845734091,"expiresAt":1792845794090,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
520{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":389185,"outputTokens":788,"cost":0},"at":1792845743128,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
521{"kind":"goal/change","version":1,"operation":"usage","ref":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":10},"delta":{"inputTokens":390011,"outputTokens":146,"cost":0},"at":1792845779730,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
522{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845779733,"expiresAt":1792845839732,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
523{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792845787807,"expiresAt":1792845787807,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
524{"kind":"goal/change","version":1,"operation":"complete","goal":{"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","revision":11,"objective":"按 docs/oauth.md 执行迁移,从 B0.5 开始逐批次落地:B0.5 删除 app/adminhost、lib/admin、lib/admin-rpc 并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫;随后 B0(lib/kenauth+基础设施)、B1(用户 ID 全量 i64)、B2.0–B2.8(lib/jwt、webapi 接入、OIDC/refresh/logout、全局替换、删旧登录、删 session crate、im 服务令牌、前端、Helm 配置)、B3、B4、B5。每个批次结束必须保持 cargo check --workspace 通过,最终全仓无 session_layer / lib/session / admin_rpc 引用。","phase":"complete","maxGoalRounds":40,"maxTurnsWithoutToolCall":2,"tokenBudget":0,"createdAt":1792827632882,"updatedAt":1792847660642,"roundsStarted":9,"waitUntil":0,"usage":{"inputTokens":195080221,"outputTokens":162612,"cost":0},"completion":{"summary":"GitData→KenOAuth 认证/会话/space 迁移按 docs/oauth.md 的 B0.5–B5 批次落地完成:认证与令牌由 KenOAuth 唯一持有,GitData 仅用 lib/jwt 验签;用户主键全量 i64;space 写操作全部穿透到 KenOAuth 并以本地投影读;session/session-layer/admin/admin-rpc/adminhost 已从代码与工作区移除。B4 完成了 space/member/ban/group/invite/join-rule/join-request 的 gRPC 写透与投影、apply_space_join_rules 投影、CheckPermissions(300s Redis);K4 补齐 join 字段、email 绑定与多角色邀请;B2.8 补齐 /readyz KenOAuth 探针、JWKS 失败告警、OIDC id_token 的 nonce/at_hash/sub 校验。范围说明:docs §K2/K3/K5 与 §9 中标注“代理”的自助端点(TOTP 注册、session/access key/ssh key/deletion 管理)不在本目标枚举的批次内,已在 docs/oauth.md「已知待收尾」记录;全链路运行时冒烟因本机无 Docker/Postgres/KenOAuth 运行环境未执行。","criteria":[{"criterion":"B0.5:删除 app/adminhost、lib/admin、lib/admin-rpc,并为 /api/v1/internal/* 加 x-gitdata-service-key 守卫","evidence":["lib 目录清单不含 admin、admin-rpc、session、session-layer;app 目录清单不含 adminhost","lib/webapi/src/lib.rs 定义 SERVICE_KEY_HEADER=\"x-gitdata-service-key\"、constant_time_eq 与 require_service_key 中间件,并在构建路由时挂载"]},{"criterion":"B0:lib/kenauth + OAuth 基础设施落地","evidence":["lib/kenauth 存在(client.rs、proto/、拦截器 x-rpc-key 与 gitdata-request-id)","config/default.toml 与 config/local.toml 含 [oauth] 与 [internal] service_key","docker-compose.dev.yml 含 oauth profile 服务;charts/core/templates/oauth.yaml 存在;helm lint charts/core 通过"]},{"criterion":"B1:用户 ID 全量 i64","evidence":["lib/models/src/users/user.rs:pub uid: i64","lib/jwt/src/extract.rs:fn uid() -> Option<i64>(claims.sub 为 i64)","cargo check --workspace 通过"]},{"criterion":"B2.0–B2.8:JWT 验签、OIDC 登录/回调/续期/登出、旧登录与 session crate 删除、im 服务令牌、前端 401 续期、健康探针与 id_token 校验","evidence":["lib/jwt 提供 EdDSA/JWKS 缓存、AccessClaims/ServiceClaims/IdClaims、Session 提取器与 cookie","lib/auth/src/routes.rs 暴露 GET /auth/login、GET /auth/callback、POST /auth/refresh、POST /auth/logout","lib/jwt/src/verifier.rs 与 lib/auth/src/oauth/callback.rs 校验 id_token nonce、at_hash(SHA-512 左半)与 sub","app/im/src/gateway.rs 调用 introspect_token;web/src/shared/api/index.ts 对 token_expired 做单飞 refresh","lib/export/src/health.rs 的 /readyz 通过 kenauth::check_health(gRPC health)探测 KenOAuth","工作区无 lib/session、lib/session-layer;代码无 session_layer 引用"]},{"criterion":"B3:AccessKey/SSH 公钥校验改 gRPC,删除旧凭据模型","evidence":["lib/git 使用 client.lookup_ssh_key / verify_access_key(gRPC)","lib/models/src/users 不再包含 user_access_keys、user_ssh_keys、user_sessions 模型"]},{"criterion":"B4:space/member/ban/group/invite/join 写入穿透 KenOAuth,权限判定走 CheckPermissions","evidence":["lib/space/src/sync.rs 提供 upsert_space/remove_space/upsert_member/remove_member/bump_members","spaces.rs/members.rs/bans.rs/groups.rs/invites.rs/join_rules.rs/join_requests.rs 均在写前调用 kenauth 客户端","lib/space/src/share/authz.rs 使用 check_permissions + Redis 300s 缓存(key 含 space.version),owner/system 短路","apply_space_join_rules 通过 EnsureBotMember(role_ids) 投影到 KenOAuth"]},{"criterion":"K4:组织扩展字段、邀请 email、多角色邀请、bot 会籍","evidence":["KenOAuth organizations 增加 join_policy/allow_join/require_email_verified/require_mfa/welcome_message/member_must_enable_2fa;organization_members 增加 last_active_at;roles 增加 color/emoji/icon_hash/position;invites 增加 email/extra_role_ids","CreateOrganization/UpdateOrganization 支持 join 字段;CreateInvite 支持 email 与 role_ids;redeem_invite 校验 email 并绑定全部角色","EnsureBotMember 支持 role_keys 与 role_ids;gitdata proto 副本已同步"]},{"criterion":"B5:路由/死代码/依赖清理,openapi 与 web 客户端重新生成,静态检查通过","evidence":["cargo run -p openapi -- --check 输出 openapi.json is up to date","bun run --cwd web gen:api 重新生成客户端;bun run --cwd web build 与 lint 通过","移除未使用依赖(lib/auth sha1/ssh-key、lib/export jsonwebtoken/rand、lib/ci auth、lib/ci-protocol serde_yaml_ng/thiserror、lib/git ssh-key 及 workspace 未使用项)后 cargo check 通过"]},{"criterion":"最终验收:全仓代码无 session_layer / lib/session / admin_rpc 引用,cargo check --workspace 通过","evidence":["对 app/lib/web/charts/config/scripts 的 .rs/.ts/.tsx/.toml/.yaml/.json 检索 session_layer|lib/session|lib\\session|admin_rpc|admin-rpc|adminhost:0 命中(docs/oauth.md 保留历史计划文字)","lib 目录无 session/session-layer/admin/admin-rpc;app 目录无 adminhost","cargo check --workspace 退出码 0"]}],"checks":[{"command":"cargo check --workspace --quiet","result":"passed","exitCode":0},{"command":"cargo check --quiet (C:\\工程\\KenOAuth, KENOAUTH_SKIP_WEB_ASSETS=1)","result":"passed","exitCode":0},{"command":"cargo run -p openapi -- --check","result":"passed","exitCode":0},{"command":"bun run --cwd web build","result":"passed","exitCode":0},{"command":"bun run --cwd web lint","result":"passed","exitCode":0},{"command":"helm lint charts/core","result":"passed","exitCode":0},{"command":"grep session_layer|lib/session|admin_rpc|admin-rpc|adminhost over code/config","result":"passed","exitCode":0},{"command":"docker compose -f docker-compose.dev.yml config -q","result":"not-run","explanation":"Docker CLI 未安装;compose 文件保持 B0 已落地的 oauth profile 配置,未在本轮改动"},{"command":"端到端运行时冒烟(注册→登录→建仓→push→issue→chat→CI→AI review→MCP)","result":"not-run","explanation":"本机无 Docker/Postgres/Redis/NATS 与 KenOAuth 运行实例;已完成编译、OpenAPI 一致性、前端构建/lint 与 Helm lint 的静态验证。后续 K2/K3/K5 自助端点与运行时冒烟已记录在 docs/oauth.md「已知待收尾」"}]}},"session":"ses_e93679c00ffe4y1dZna72QPquP"}
525{"kind":"goal/lease","version":1,"id":"goal-b26bd861-766d-4ac6-99d5-44a8e7109d44","holder":"1e0e7dfb-081e-41cc-a092-01c68db46c3a","at":1792847672615,"expiresAt":1792847672615,"session":"ses_e93679c00ffe4y1dZna72QPquP"}
Modified charts/core/templates/_helpers.tpl charts/core/templates/_helpers.tpl +0 −60 View charts/core/templates/_helpers.tpl
@@ -66,70 +66,10 @@
6666 value: {{ .Values.githost.httpPort | quote }}
6767- name: APP_IM__PORT
6868 value: {{ .Values.im.port | quote }}
69- name: APP_MCP__PORT
70 value: {{ .Values.mcp.port | quote }}
7169- name: APP_HEALTH__ENABLED
7270 value: {{ .Values.health.enabled | quote }}
7371- name: APP_HEALTH__SYNCHOST__PORT
7472 value: {{ .Values.health.synchostPort | quote }}
75- name: APP_HEALTH__MAILS__PORT
76- name: APP_HEALTH__AIHOST__PORT
77 value: {{ .Values.health.aihostPort | quote }}
78{{- if .Values.ai.enabled }}
79- name: APP_AI__ENABLED
80 value: "true"
81- name: APP_AI__BASE_URL
82 value: {{ .Values.ai.baseUrl | quote }}
83- name: APP_AI__MODEL
84 value: {{ .Values.ai.model | quote }}
85- name: APP_AI__REVIEW_ENABLED
86 value: {{ .Values.ai.reviewEnabled | quote }}
87- name: APP_AI__SUMMARY_ENABLED
88 value: {{ .Values.ai.summaryEnabled | quote }}
89- name: APP_AI__RELEASE_NOTES_ENABLED
90 value: {{ .Values.ai.releaseNotesEnabled | quote }}
91- name: APP_AI__QUEUE_TOPIC
92 value: {{ .Values.ai.queueTopic | quote }}
93- name: APP_AI__DLQ_TOPIC
94 value: {{ .Values.ai.dlqTopic | quote }}
95- name: APP_AI__MAX_ATTEMPTS
96 value: {{ .Values.ai.maxAttempts | quote }}
97- name: APP_AI__REQUEST_TIMEOUT_SECS
98 value: {{ .Values.ai.requestTimeoutSecs | quote }}
99- name: APP_AI__CONTEXT_MAX_BYTES
100 value: {{ .Values.ai.contextMaxBytes | quote }}
101- name: APP_AI__MAX_FINDINGS
102 value: {{ .Values.ai.maxFindings | quote }}
103- name: APP_AI__MAX_OUTPUT_TOKENS
104 value: {{ .Values.ai.maxOutputTokens | quote }}
105- name: APP_AI__THINKING_BUDGET
106 value: {{ .Values.ai.thinkingBudget | quote }}
107- name: APP_AI__TEMPERATURE
108 value: {{ .Values.ai.temperature | quote }}
109- name: APP_AI__SUMMARY_COMMIT_LIMIT
110 value: {{ .Values.ai.summaryCommitLimit | quote }}
111- name: APP_AI__BOT_USERNAME
112 value: {{ .Values.ai.botUsername | quote }}
113- name: APP_AI__BOT_DISPLAY_NAME
114 value: {{ .Values.ai.botDisplayName | quote }}
115- name: APP_AI__OUTPUT_LANGUAGE
116 value: {{ .Values.ai.outputLanguage | quote }}
117- name: APP_AIHOST__ENABLED
118 value: {{ .Values.aihost.enabled | quote }}
119- name: APP_AIHOST__PORT
120 value: {{ .Values.aihost.port | quote }}
121- name: APP_AIHOST__CONCURRENCY
122 value: {{ .Values.aihost.concurrency | quote }}
123- name: APP_AIHOST__POLL_INTERVAL_SECS
124 value: {{ .Values.aihost.pollIntervalSecs | quote }}
125- name: APP_AIHOST__RETRY_BACKOFF_BASE_SECS
126 value: {{ .Values.aihost.retryBackoffBaseSecs | quote }}
127{{- else }}
128- name: APP_AI__ENABLED
129 value: "false"
130- name: APP_AIHOST__ENABLED
131 value: "false"
132{{- end }}
13373{{- if .Values.githost.ssh.enabled }}
13474- name: APP_GIT__SSH__PORT
13575 value: {{ .Values.githost.ssh.containerPort | quote }}
Modified charts/core/values.yaml charts/core/values.yaml +0 −73 View charts/core/values.yaml
@@ -177,21 +177,10 @@
177177 limits: { memory: 1Gi }
178178 extraEnv: []
179179
180# sandbox 运行事件流(SSE)由 sandbox-agent 自己在 sandbox-agent.gitdata.ai 暴露,
181# 平台侧只配置 APP_SANDBOX_RPC__FANOUT_ENDPOINT,因此这里不再有 chart 资源。
182
183 enabled: true
184 replicaCount: 1
185 resources:
186 requests: { cpu: 50m, memory: 64Mi }
187 limits: { memory: 256Mi }
188 extraEnv: []
189
190180# worker 类程序的独立健康端点(apihost/githost/im 挂在自己业务端口上)
191181health:
192182 enabled: true
193183 synchostPort: 3002
194 aihostPort: 3006
195184
196185im:
197186 enabled: true
@@ -216,68 +205,6 @@
216205 extraEnv: []
217206 pdb:
218207 enabled: true
219 minAvailable: 1
220
221# ---------------------------------------------------------------------------
222# AI:评审 / 仓库总结 / release 文案。启用前必须提供 OpenAI 兼容端点。
223# 密钥走 secret.env(APP_AI__API_KEY),不要写在这里。
224# ---------------------------------------------------------------------------
225ai:
226 # 非敏感默认值;enabled/base_url/model 等按需在 values 覆盖
227 enabled: false
228 reviewEnabled: true
229 summaryEnabled: true
230 releaseNotesEnabled: true
231 queueTopic: "gitdata:ai"
232 dlqTopic: "gitdata:ai:dlq"
233 maxAttempts: 3
234 requestTimeoutSecs: 120
235 contextMaxBytes: 524288
236 maxFindings: 20
237 maxOutputTokens: 4096
238 thinkingBudget: 0
239 temperature: "0.2"
240 summaryCommitLimit: 50
241 botUsername: "ai-reviewer"
242 botDisplayName: "AI Reviewer"
243 outputLanguage: "zh-CN"
244
245aihost:
246 enabled: true
247 replicaCount: 1
248 port: 3006
249 concurrency: 2
250 pollIntervalSecs: 5
251 retryBackoffBaseSecs: 2
252 resources:
253 requests: { cpu: 100m, memory: 256Mi }
254 limits: { memory: 1Gi }
255 extraEnv: []
256
257# ---------------------------------------------------------------------------
258# MCP:独立服务(Streamable HTTP),需要挂载 repos PVC(仓库读取走本地文件系统)。
259# 鉴权:用户访问密钥 + 内部系统密钥(仅内网来源);配置见 APP_MCP__*。
260# ---------------------------------------------------------------------------
261mcp:
262 enabled: true
263 replicaCount: 2
264 port: 3007
265 resources:
266 requests: { cpu: 100m, memory: 128Mi }
267 limits: { memory: 512Mi }
268 extraEnv: []
269 # 独立域名 Ingress(同源提供 /v1 端点与 /healthz)
270 ingress:
271 enabled: false
272 host: mcp.gitdata.example.com
273 className: ""
274 annotations: {}
275 tls:
276 enabled: false
277 secretName: ""
278 clusterIssuer: ""
279 pdb:
280 enabled: false
281208 minAvailable: 1
282209
283210# ---------------------------------------------------------------------------
Modified charts/prod/values/prod.yaml charts/prod/values/prod.yaml +0 −75 View charts/prod/values/prod.yaml
@@ -38,31 +38,6 @@
3838 # 用跨命名空间 FQDN:runner 池在 gitdata-runners 命名空间,这个 URL 会随 claim
3939 # 响应下发(lib/ci/src/actions_payload.rs:87 runtime_url),必须两边都能解析。
4040 APP_CI__RUNTIME_BASE_URL: "http://gitdata-webhook.gitdata.svc.cluster.local:3005"
41 # MCP 独立服务(app/mcp,域名 mcp.gitdata.ai,端点 /v1):
42 # - ALLOWED_HOSTS 必须包含对外域名,否则 rmcp 默认只放行 loopback(fail-closed)。
43 # - 内部系统密钥仅当请求来源 IP 落在 INTERNAL_CIDRS 内才被接受;TRUSTED_PROXIES
44 # 必须覆盖 ingress 出口地址(traefik 节点/Pod CIDR),否则外部请求的来源会以
45 # traefik Pod IP 参与内网判定。
46 # - 内部密钥是机密:APP_MCP__INTERNAL_KEYS 放在 Gitea Secret PROD_SECRET_VALUES,
47 # 形如 '[{name="gitdata-agent",token_hash="<sha256 hex>"}]',不要写在本文件。
48 # - in-cluster Host 也必须列进来:sandbox 运行时按 RUNTIME_URL 访问本端点,rmcp
49 # 的 DNS-rebinding 保护会校验 Host 头(非白名单直接 403)。
50 APP_MCP__ALLOWED_HOSTS: '["mcp.gitdata.ai","gitdata-mcp.gitdata.svc.cluster.local"]'
51 # sandbox 运行时访问本端点的地址(随 run 下发给运行时,带空间令牌)。
52 APP_MCP__RUNTIME_URL: "http://gitdata-mcp.gitdata.svc.cluster.local:3007/v1"
53 APP_MCP__INTERNAL_CIDRS: '["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"]'
54 APP_MCP__TRUSTED_PROXIES: '["10.0.0.0/8"]'
55 # 平台 → 独立 sandbox-agent 运行时(agent-sandbox-system/sandbox-agent-server gRPC 8091):
56 # agents 对话与运行的后端。TRUSTED_KEY 是机密,放在 Gitea Secret PROD_SECRET_VALUES
57 # (APP_SANDBOX_RPC__TRUSTED_KEY),必须与 sandbox-agent 的 server.auth.trustedKeys
58 # 一致,否则 gRPC 返回 Unauthenticated。
59 APP_SANDBOX_RPC__ENABLED: "true"
60 APP_SANDBOX_RPC__ENDPOINT: "http://sandbox-agent-server.agent-sandbox-system.svc.cluster.local:8091"
61 # 浏览器直连的事件流网关:sandbox-agent 自己暴露的 fanout 域名(跨源,靠
62 # server.corsOrigins 放行 https://gitdata.ai);留空则前端回退轮询(streamsAvailable=false)。
63 APP_SANDBOX_RPC__FANOUT_ENDPOINT: "https://sandbox-agent.gitdata.ai"
64 APP_SANDBOX_RPC__REQUEST_TIMEOUT_SECS: "30"
65 APP_SANDBOX_RPC__STREAM_TOKEN_TTL_SECS: "600"
6641 RUST_LOG: "info"
6742 OTEL_TRACES_SAMPLER: "parentbased_traceidratio"
6843 OTEL_TRACES_SAMPLER_ARG: "0.2"
@@ -130,20 +105,6 @@
130105 annotations:
131106 metallb.universe.tf/loadBalancerIPs: "47.239.98.8"
132107
133
134
135mcp:
136 enabled: true
137 replicaCount: 2
138 ingress:
139 enabled: true
140 host: mcp.gitdata.ai
141 className: traefik
142 tls:
143 enabled: true
144 secretName: ""
145 clusterIssuer: cloudflare-acme-cluster-issuer
146
147108webhook:
148109 enabled: true
149110 ingress:
@@ -155,40 +116,6 @@
155116 secretName: ""
156117 clusterIssuer: cloudflare-acme-cluster-issuer
157118
158ai:
159 enabled: true
160 baseUrl: "https://api.commandcode.ai/provider/v1"
161 model: "deepseek/deepseek-v4.1-flash"
162 reviewEnabled: true
163 summaryEnabled: true
164 releaseNotesEnabled: true
165 queueTopic: "gitdata:ai"
166 dlqTopic: "gitdata:ai:dlq"
167 maxAttempts: 3
168 requestTimeoutSecs: 600
169 contextMaxBytes: 524288
170 maxFindings: 20
171 maxOutputTokens: 4096
172 thinkingBudget: 2048
173 temperature: "0.2"
174 summaryCommitLimit: 50
175 botUsername: "ai-reviewer"
176 botDisplayName: "AI Reviewer"
177 outputLanguage: "zh-CN"
178
179
180aihost:
181 enabled: true
182 replicaCount: 1
183 port: 3006
184 concurrency: 2
185 pollIntervalSecs: 5
186 retryBackoffBaseSecs: 2
187 resources:
188 requests: { cpu: 100m, memory: 256Mi }
189 limits: { memory: 1Gi }
190 extraEnv: []
191
192119persistence:
193120 repos:
194121 enabled: true
@@ -210,8 +137,6 @@
210137searchhost:
211138 enabled: true
212139 replicaCount: 1
213
214 enabled: true
215140
216141im:
217142 enabled: true
Modified config/default.toml config/default.toml +0 −74 View config/default.toml
@@ -180,24 +180,7 @@
180180max_reaction_emoji_len = 32
181181max_mentions_per_message = 20
182182
183[agents]
184max_pagination = 100
185default_pagination = 20
186max_collection_name_len = 100
187max_agent_name_len = 100
188max_instructions_len = 32768
189# Lifetime of the per-run space token handed to the sandbox runtime when the
190# agent chat "MCP" option is enabled (seconds).
191mcp_token_ttl_secs = 86400
192183
193[sandbox_rpc]
194enabled = false
195endpoint = "http://127.0.0.1:8091"
196fanout_endpoint = "http://127.0.0.1:8090"
197trusted_key = ""
198request_timeout_secs = 30
199stream_token_ttl_secs = 600
200
201184[git]
202185repo_root = "/opt/gitdata/repos"
203186objects_root = "/opt/gitdata/objects"
@@ -294,9 +277,6 @@
294277[health.synchost]
295278port = 3002
296279
297
298[health.aihost]
299port = 3006
300280[ci]
301281enabled = false
302282secret_encryption_key = ""
@@ -325,36 +305,6 @@
325305port = 3005
326306public_base_url = ""
327307
328[ai]
329enabled = false
330base_url = ""
331api_key = ""
332model = ""
333review_enabled = true
334summary_enabled = true
335release_notes_enabled = true
336queue_topic = "gitdata:ai"
337dlq_topic = "gitdata:ai:dlq"
338max_attempts = 3
339request_timeout_secs = 120
340context_max_bytes = 524288
341max_findings = 20
342max_output_tokens = 4096
343thinking_budget = 0
344temperature = 0.2
345summary_commit_limit = 50
346bot_username = "ai-reviewer"
347bot_display_name = "AI Reviewer"
348output_language = "zh-CN"
349
350[aihost]
351enabled = true
352bind_addr = "0.0.0.0"
353port = 3006
354concurrency = 2
355poll_interval_secs = 5
356retry_backoff_base_secs = 2
357
358308[runner]
359309server_url = "http://127.0.0.1:3005"
360310registration_token = ""
@@ -388,30 +338,6 @@
388338retry_backoff_base_secs = 30
389339request_timeout_secs = 10
390340ttl_secs = 86400
391
392[mcp]
393enabled = true
394bind_addr = "0.0.0.0"
395port = 3007
396# URL the sandbox-agent runtime uses to reach this MCP endpoint. Empty keeps
397# the agent chat "MCP" option unavailable. Dev sets it in config/local.toml;
398# production must point it at the runtime-reachable address.
399runtime_url = ""
400# MCP endpoint Host header allowlist. Empty keeps the rmcp default (loopback only).
401# Production must set e.g. ["mcp.gitdata.ai"].
402allowed_hosts = []
403# Only requests whose resolved client IP falls inside these CIDRs can use internal keys.
404internal_cidrs = ["127.0.0.1/32", "::1/128"]
405# Proxies whose X-Forwarded-For header is trusted (e.g. the ingress pod/node CIDR).
406trusted_proxies = []
407
408# Internal system keys ([[mcp.internal_keys]]). Set exactly one of token or token_hash
409# (token_hash = sha256_hex(token), recommended). These act as the platform AI bot with
410# full permissions and are accepted only from internal_cidrs.
411# [[mcp.internal_keys]]
412# name = "gitdata-agent"
413# token_hash = ""
414# enabled = true
415341
416342[livekit]
417343enabled = false
Modified docs/oauth.md docs/oauth.md +11 −17 View docs/oauth.md
@@ -126,9 +126,9 @@
126126
127127```
128128登录:
129 浏览器 → /api/v1/auth/login → 302 KenOAuth /oauth/authorize (PKCE)
129 浏览器 → /api/v1/auth/login → 302 KenOAuth /api/v1/oauth/authorize (PKCE)
130130 → /api/v1/auth/callback?code
131 → 后端 POST /oauth/token
131 → 后端 POST /api/v1/oauth/token
132132 → 验 id_token(JWKS + nonce)
133133 → Set-Cookie:
134134 gitdata_session = access_token (600s, Path=/)
@@ -142,7 +142,7 @@
142142过期:
143143 API 401 token_expired
144144 → POST /api/v1/auth/refresh(Redis 单飞锁)
145 → POST /oauth/token (refresh_token, 轮换)
145 → POST /api/v1/oauth/token (refresh_token, 轮换)
146146 → 覆盖两个 cookie → 重放原请求
147147```
148148
@@ -361,7 +361,7 @@
361361GET /api/v1/auth/login?redirect=/app
362362 1. 校验 redirect 为站内相对路径
363363 2. 生成 state / nonce / code_verifier,写 Redis 600s
364 3. 302 {issuer}/oauth/authorize
364 3. 302 {issuer}/api/v1/oauth/authorize
365365 ?response_type=code
366366 &client_id=gitdata-web
367367 &redirect_uri={redirect_uri}
@@ -370,7 +370,7 @@
370370
371371GET /api/v1/auth/callback?code&state
372372 1. state 校验并删除
373 2. POST {issuer}/oauth/token (authorization_code + code_verifier)
373 2. POST {issuer}/api/v1/oauth/token (authorization_code + code_verifier)
374374 3. 验 id_token:JWKS、iss、aud、exp、nonce、at_hash
375375 4. 验 access_token:JWKS、iss、aud、client_id、typ、exp
376376 5. sub 解析为 i64
@@ -396,7 +396,7 @@
396396 1. 读 gitdata_refresh;缺失 → 401 unauthenticated
397397 2. 从 access token 读 sid(签名有效即可,忽略 exp)
398398 3. Redis SET oauth:refresh:{sid} NX EX 10
399 4. POST {issuer}/oauth/token (grant_type=refresh_token)
399 4. POST {issuer}/api/v1/oauth/token (grant_type=refresh_token)
400400 5. 覆盖 gitdata_session / gitdata_refresh
401401 6. 204
402402 - invalid_grant → 清两个 cookie → 401 refresh_expired
@@ -523,7 +523,7 @@
523523}
524524```
525525
526验收:连续两次 `EnsureBotUser("ai-reviewer")` 返回同一 `user_id`。
526验收:连续两次 `EnsureBotUser("service-bot")` 返回同一 `user_id`。
527527
528528### K2 协议接受(1 人日)
529529
@@ -894,12 +894,6 @@
894894
895895自定义角色 = 原 `space_groups`;`self_assignable` = 原 `join_group_self`。
896896
897#### B4.5 Bot
898
899- `lib/agent/src/bot.rs` → `EnsureBotUser(system=true)` + 本地 upsert。
900- `lib/mcp-api/src/space_token.rs` → `EnsureBotUser` + `EnsureBotMember`。
901- `export::authz::set_system_uid(i64)` / `register_space_bot(i64)` 保留。
902
903897验收:建 space → 成员/角色/封禁/邀请/加入申请全链路;24 个跨域 `space_members` 查询正常。
904898
905899### B5 收尾(4 人日)
@@ -909,7 +903,7 @@
9099033. `lib/user`、`lib/auth` 死代码清理。
9109044. `openapi.json` + `web/src/shared/api` 最终生成;`openapi-check` 通过。
9119055. Helm/compose/justfile 最终校验。
9126. 全链路冒烟:注册 → 登录 → 建组织 → 邀请 → 建仓库 → push → issue → chat → CI → AI review → MCP。
9066. 全链路冒烟:注册 → 登录 → 建组织 → 邀请 → 建仓库 → push → issue → chat → CI。
913907
914908---
915909
@@ -997,8 +991,8 @@
997991client_id = "gitdata-web"
998992client_secret = ""
999993redirect_uri = "http://localhost:5173/api/v1/auth/callback"
1000token_endpoint = "" # 空 = {issuer}/oauth/token
1001jwks_url = "" # 空 = {issuer}/.well-known/jwks.json
994token_endpoint = "http://127.0.0.1:18081/api/v1/oauth/token"
995jwks_url = "http://127.0.0.1:18081/.well-known/jwks.json"
1002996audience = "gitdata.ai"
1003997grpc_endpoint = "http://127.0.0.1:18082"
1004998rpc_key = ""
@@ -1138,7 +1132,7 @@
11381132- [ ] `client_id != gitdata-web` 的令牌被拒绝
11391133- [ ] access token 过期后 refresh 成功,原请求重放成功
11401134- [ ] refresh 轮换后旧 token 重放被拒绝
1141- [ ] 并发 10 个刷新只触发一次 `/oauth/token`
1135- [ ] 并发 10 个刷新只触发一次 `/api/v1/oauth/token`
11421136- [ ] 吊销会话后 60s 内 401
11431137- [ ] `im` 用 service token 握手成功,`jti` 重放被拒
11441138
Added lib/auth/src/handlers/access_keys.rs lib/auth/src/handlers/access_keys.rs +223 −0 View lib/auth/src/handlers/access_keys.rs
@@ -0,0 +1,223 @@
1use axum::Json;
2use axum::extract::{Path, Query, State};
3use axum::http::HeaderMap;
4use export::pagination::{Page, PageQuery, paginate};
5use export::rest::{AppResponseData, NoData};
6use jwt::Session;
7use jwt::share::session::require_uid;
8use kenauth::proto::common::v1::{AccessKeyInfo, PageRequest, TokenScope};
9use models::users::access_scope::AccessKeyScope;
10use serde::{Deserialize, Serialize};
11use time::OffsetDateTime;
12use utoipa::ToSchema;
13use uuid::Uuid;
14
15use crate::errors::ApiError;
16use crate::services::AuthService;
17use crate::share::rpc;
18
19const SCOPE_PAGE_SIZE: i32 = 100;
20
21#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
22pub struct CreateAccessKeyCmd {
23 pub name: String,
24 pub scope: AccessKeyScope,
25 #[serde(
26 default,
27 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
28 )]
29 pub expires_at: Option<OffsetDateTime>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
33pub struct AccessKeyDto {
34 pub id: Uuid,
35 pub name: String,
36 pub scope: AccessKeyScope,
37 pub created_at: OffsetDateTime,
38 pub expires_at: Option<OffsetDateTime>,
39 pub last_used_at: Option<OffsetDateTime>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
43pub struct CreateAccessKeyResp {
44 pub key: AccessKeyDto,
45 pub token: String,
46}
47
48fn scopes_of(scope: AccessKeyScope) -> Vec<i32> {
49 match scope {
50 AccessKeyScope::Read => vec![TokenScope::RepoRead as i32],
51 AccessKeyScope::Write => vec![TokenScope::RepoRead as i32, TokenScope::RepoWrite as i32],
52 }
53}
54
55fn scope_of(info: &AccessKeyInfo) -> AccessKeyScope {
56 if info.scopes.contains(&(TokenScope::RepoWrite as i32)) {
57 AccessKeyScope::Write
58 } else {
59 AccessKeyScope::Read
60 }
61}
62
63fn key_dto(info: AccessKeyInfo) -> Result<AccessKeyDto, ApiError> {
64 let created_at = info
65 .created_at
66 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
67 .ok_or_else(|| ApiError::internal("kenoauth returned a key without created_at"))?;
68 Ok(AccessKeyDto {
69 id: Uuid::parse_str(&info.access_key_id)
70 .map_err(|_| ApiError::internal("kenoauth returned a malformed key id"))?,
71 scope: scope_of(&info),
72 name: info.name,
73 created_at,
74 expires_at: info
75 .expires_at
76 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos)),
77 last_used_at: info
78 .last_used_at
79 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos)),
80 })
81}
82
83#[utoipa::path(
84 get,
85 path = "/auth/access-keys",
86 tag = "security",
87 description = "列出当前账号的访问令牌(不含令牌明文)。",
88 params(PageQuery),
89 responses(
90 (status = 200, description = "access keys", body = AppResponseData<Page<AccessKeyDto>>),
91 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
92 )
93)]
94pub async fn list_access_keys(
95 State(service): State<AuthService>,
96 session: Session,
97 Query(query): Query<PageQuery>,
98) -> Result<AppResponseData<Page<AccessKeyDto>>, ApiError> {
99 let uid = require_uid(&session)?;
100 let client = rpc::client(&service.state)?;
101 let listed = client
102 .list_access_keys(
103 uid,
104 false,
105 PageRequest {
106 page_size: SCOPE_PAGE_SIZE,
107 page_token: String::new(),
108 },
109 )
110 .await
111 .map_err(|error| ApiError::internal(error.to_string()))?;
112 let mut keys = Vec::with_capacity(listed.keys.len());
113 for info in listed.keys {
114 keys.push(key_dto(info)?);
115 }
116 Ok(AppResponseData::ok(paginate(keys, &query)))
117}
118
119#[utoipa::path(
120 post,
121 path = "/auth/access-keys",
122 tag = "security",
123 description = "为当前账号签发新的访问令牌,令牌明文只在本响应出现一次。",
124 request_body = CreateAccessKeyCmd,
125 responses(
126 (status = 200, description = "access key created", body = AppResponseData<CreateAccessKeyResp>),
127 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
128 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
129 )
130)]
131pub async fn create_access_key(
132 State(service): State<AuthService>,
133 session: Session,
134 headers: HeaderMap,
135 Json(cmd): Json<CreateAccessKeyCmd>,
136) -> Result<AppResponseData<CreateAccessKeyResp>, ApiError> {
137 let uid = require_uid(&session)?;
138 let client = rpc::client(&service.state)?;
139 let expires_in_secs = match cmd.expires_at {
140 Some(expires_at) => (expires_at - OffsetDateTime::now_utc()).whole_seconds(),
141 None => 0,
142 };
143 let created = client
144 .create_access_key(
145 uid,
146 cmd.name.trim(),
147 "",
148 scopes_of(cmd.scope),
149 expires_in_secs,
150 &rpc::client_ip(&headers),
151 )
152 .await
153 .map_err(|error| ApiError::bad_request(error.to_string()))?;
154 let key = created
155 .key
156 .ok_or_else(|| ApiError::internal("kenoauth returned no key"))?;
157 Ok(AppResponseData::ok(CreateAccessKeyResp {
158 key: key_dto(key)?,
159 token: created.token,
160 }))
161}
162
163#[utoipa::path(
164 delete,
165 path = "/auth/access-keys/{key_id}",
166 tag = "security",
167 description = "吊销指定访问令牌;令牌不存在或不属于当前账号时返回 404。",
168 params(("key_id" = Uuid, Path)),
169 responses(
170 (status = 200, description = "access key revoked", body = AppResponseData<NoData>),
171 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
172 (status = 404, description = "not found", body = AppResponseData<NoData>)
173 )
174)]
175pub async fn delete_access_key(
176 State(service): State<AuthService>,
177 session: Session,
178 Path(key_id): Path<Uuid>,
179) -> Result<AppResponseData<NoData>, ApiError> {
180 let uid = require_uid(&session)?;
181 let client = rpc::client(&service.state)?;
182 let revoked = client
183 .revoke_access_key(uid, &key_id.to_string(), "user_revoked")
184 .await
185 .map_err(|error| ApiError::internal(error.to_string()))?;
186 if !revoked.revoked && !revoked.already_revoked {
187 return Err(ApiError::not_found("access key not found"));
188 }
189 Ok(AppResponseData::empty())
190}
191
192#[utoipa::path(
193 post,
194 path = "/auth/access-keys/{key_id}/rotate",
195 tag = "auth",
196 description = "为指定访问令牌换发新明文,保留其编号、名称与范围。",
197 params(("key_id" = Uuid, Path)),
198 responses(
199 (status = 200, description = "access key rotated", body = AppResponseData<CreateAccessKeyResp>),
200 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
201 (status = 404, description = "not found", body = AppResponseData<NoData>)
202 )
203)]
204pub async fn rotate_access_key(
205 State(service): State<AuthService>,
206 session: Session,
207 headers: HeaderMap,
208 Path(key_id): Path<Uuid>,
209) -> Result<AppResponseData<CreateAccessKeyResp>, ApiError> {
210 let uid = require_uid(&session)?;
211 let client = rpc::client(&service.state)?;
212 let rotated = client
213 .rotate_access_key(uid, &key_id.to_string(), &rpc::client_ip(&headers))
214 .await
215 .map_err(|error| ApiError::not_found("access key not found"))?;
216 let key = rotated
217 .key
218 .ok_or_else(|| ApiError::internal("kenoauth returned no key"))?;
219 Ok(AppResponseData::ok(CreateAccessKeyResp {
220 key: key_dto(key)?,
221 token: rotated.token,
222 }))
223}
No newline at end of file
Added lib/auth/src/handlers/devices.rs lib/auth/src/handlers/devices.rs +184 −0 View lib/auth/src/handlers/devices.rs
@@ -0,0 +1,184 @@
1use axum::extract::{Query, State};
2use axum::response::Json;
3use export::pagination::PageQuery;
4use export::rest::AppResponseData;
5use jwt::Session;
6use jwt::share::session::require_uid;
7use kenauth::proto::common::v1::PageRequest;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use time::OffsetDateTime;
11use utoipa::ToSchema;
12use uuid::Uuid;
13
14use crate::errors::ApiError;
15use crate::services::AuthService;
16use crate::share::rpc;
17
18const SESSION_PAGE_SIZE: i32 = 100;
19
20#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
21pub struct DeviceDto {
22 pub id: Uuid,
23 pub device_name: Option<String>,
24 pub device_type: Option<String>,
25 pub os: Option<String>,
26 pub is_trusted: bool,
27 pub first_seen_at: OffsetDateTime,
28 pub last_seen_at: OffsetDateTime,
29}
30
31struct DeviceRow {
32 id: Uuid,
33 device_name: Option<String>,
34 device_type: Option<String>,
35 os: Option<String>,
36 first_seen_at: OffsetDateTime,
37 last_seen_at: OffsetDateTime,
38}
39
40fn os_of(user_agent: &str) -> Option<String> {
41 let name = if user_agent.contains("Android") {
42 "Android"
43 } else if user_agent.contains("iPhone") || user_agent.contains("iPad") {
44 "iOS"
45 } else if user_agent.contains("Windows") {
46 "Windows"
47 } else if user_agent.contains("Macintosh") || user_agent.contains("Mac OS X") {
48 "macOS"
49 } else if user_agent.contains("Linux") {
50 "Linux"
51 } else {
52 return None;
53 };
54 Some(name.to_string())
55}
56
57fn device_type_of(user_agent: &str) -> Option<String> {
58 let kind = if user_agent.contains("iPad") {
59 "tablet"
60 } else if user_agent.contains("Android") || user_agent.contains("iPhone") {
61 "mobile"
62 } else if user_agent.is_empty() {
63 "unknown"
64 } else {
65 "desktop"
66 };
67 Some(kind.to_string())
68}
69
70fn agent_of(user_agent: &str) -> Option<String> {
71 let name = if user_agent.contains("Edg/") {
72 "Edge"
73 } else if user_agent.contains("Firefox/") {
74 "Firefox"
75 } else if user_agent.contains("Chrome/") {
76 "Chrome"
77 } else if user_agent.contains("Safari/") {
78 "Safari"
79 } else if user_agent.contains("git/") {
80 "Git"
81 } else {
82 return None;
83 };
84 Some(name.to_string())
85}
86
87fn device_name_of(user_agent: &str) -> Option<String> {
88 let agent = agent_of(user_agent);
89 let os = os_of(user_agent);
90 match (agent, os) {
91 (Some(agent), Some(os)) => Some(format!("{agent} on {os}")),
92 (Some(agent), None) => Some(agent),
93 (None, Some(os)) => Some(os),
94 (None, None) => None,
95 }
96}
97
98fn stable_id(user_agent: &str) -> Uuid {
99 let digest = Sha256::digest(user_agent.as_bytes());
100 let mut bytes = [0u8; 16];
101 bytes.copy_from_slice(&digest[..16]);
102 bytes[6] = (bytes[6] & 0x0f) | 0x40;
103 bytes[8] = (bytes[8] & 0x3f) | 0x80;
104 Uuid::from_bytes(bytes)
105}
106
107#[utoipa::path(
108 get,
109 path = "/me/devices",
110 tag = "account",
111 description = "按会话的用户代理归并出当前账号出现过的设备及其最近活跃时间。",
112 params(PageQuery),
113 responses(
114 (status = 200, description = "devices derived from active sessions", body = AppResponseData<Vec<DeviceDto>>),
115 (status = 401, description = "authentication required", body = AppResponseData<Vec<DeviceDto>>)
116 )
117)]
118pub async fn list_devices(
119 State(service): State<AuthService>,
120 session: Session,
121 Query(query): Query<PageQuery>,
122) -> Result<Json<AppResponseData<Vec<DeviceDto>>>, ApiError> {
123 let uid = require_uid(&session)?;
124 let client = rpc::client(&service.state)?;
125 let listed = client
126 .list_user_sessions(
127 uid,
128 false,
129 true,
130 PageRequest {
131 page_size: SESSION_PAGE_SIZE,
132 page_token: String::new(),
133 },
134 )
135 .await
136 .map_err(|error| ApiError::internal(error.to_string()))?;
137
138 let mut rows: Vec<DeviceRow> = Vec::new();
139 for info in listed.sessions {
140 let Some(created_at) = info
141 .created_at
142 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
143 else {
144 continue;
145 };
146 let last_seen_at = info
147 .last_seen_at
148 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
149 .unwrap_or(created_at);
150 let id = stable_id(&info.user_agent);
151 match rows.iter_mut().find(|row| row.id == id) {
152 Some(row) => {
153 row.first_seen_at = row.first_seen_at.min(created_at);
154 row.last_seen_at = row.last_seen_at.max(last_seen_at);
155 }
156 None => rows.push(DeviceRow {
157 id,
158 device_name: device_name_of(&info.user_agent),
159 device_type: device_type_of(&info.user_agent),
160 os: os_of(&info.user_agent),
161 first_seen_at: created_at,
162 last_seen_at,
163 }),
164 }
165 }
166
167 rows.sort_by(|left, right| right.last_seen_at.cmp(&left.last_seen_at));
168 let (limit, offset) = query.page();
169 let devices = rows
170 .into_iter()
171 .skip(offset as usize)
172 .take(limit as usize)
173 .map(|row| DeviceDto {
174 id: row.id,
175 device_name: row.device_name,
176 device_type: row.device_type,
177 os: row.os,
178 is_trusted: false,
179 first_seen_at: row.first_seen_at,
180 last_seen_at: row.last_seen_at,
181 })
182 .collect();
183 Ok(Json(AppResponseData::ok(devices)))
184}
No newline at end of file
Modified lib/auth/src/handlers/jwt_request.rs lib/auth/src/handlers/jwt_request.rs +18 −8 View lib/auth/src/handlers/jwt_request.rs
@@ -1,17 +1,16 @@
11use axum::extract::{Query, State};
2use axum::response::Json;
32use serde::{Deserialize, Serialize};
43use utoipa::ToSchema;
54
65use crate::errors::ApiError;
6use export::rest::{AppResponseData, NoData};
77use crate::services::AuthService;
88use jwt::Session;
99use jwt::share::session::require_uid;
1010
11#[derive(Debug, Deserialize)]
11#[derive(Debug, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
1212pub struct JwtRequestQuery {
13 #[serde(default)]
14 pub appid: Option<String>,
13 pub appid: String,
1514}
1615
1716#[derive(Debug, Serialize, ToSchema)]
@@ -20,14 +19,25 @@
2019 pub expires_in_secs: i64,
2120}
2221
22#[utoipa::path(
23 get,
24 path = "/auth/jwt-request",
25 tag = "auth",
26 description = "为站内实时连接签发短期服务令牌。",
27 params(JwtRequestQuery),
28 responses(
29 (status = 200, description = "jwt issued", body = AppResponseData<JwtRequestResp>),
30 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
31 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
32 )
33)]
2334pub async fn jwt_request(
2435 State(service): State<AuthService>,
2536 session: Session,
2637 Query(query): Query<JwtRequestQuery>,
27) -> Result<Json<JwtRequestResp>, ApiError> {
38) -> Result<AppResponseData<JwtRequestResp>, ApiError> {
2839 let uid = require_uid(&session)?;
29 let appid = query.appid.as_deref().unwrap_or("chat");
30 if appid != "chat" {
40 if query.appid != "chat" {
3141 return Err(ApiError::bad_request("unsupported appid"));
3242 }
3343 let client = service
@@ -45,7 +55,7 @@
4555 )
4656 .await
4757 .map_err(|error| ApiError::internal(error.to_string()))?;
48 Ok(Json(JwtRequestResp {
58 Ok(AppResponseData::ok(JwtRequestResp {
4959 jwt: issued.service_token,
5060 expires_in_secs: 900,
5161 }))
Modified lib/auth/src/handlers/mod.rs lib/auth/src/handlers/mod.rs +6 −0 View lib/auth/src/handlers/mod.rs
@@ -1,15 +1,21 @@
11pub mod accept_agreement;
2pub mod access_keys;
23pub mod agreement_page;
34pub mod create_report;
45pub mod delete_notification;
6pub mod devices;
7pub mod password;
58pub mod get_preferences;
69pub mod get_user;
710pub mod jwt_request;
811pub mod list_agreements;
912pub mod list_notifications;
1013pub mod mark_notifications_read;
14pub mod sessions;
15pub mod ssh_keys;
1116pub mod me;
1217pub mod unread_notifications_count;
1318pub mod update_preferences;
19pub mod two_factor;
1420pub mod update_profile;
1521pub mod users;
No newline at end of file
Added lib/auth/src/handlers/password.rs lib/auth/src/handlers/password.rs +52 −0 View lib/auth/src/handlers/password.rs
@@ -0,0 +1,52 @@
1use axum::Json;
2use axum::extract::State;
3use axum::http::HeaderMap;
4use export::rest::{AppResponseData, NoData};
5use jwt::Session;
6use jwt::share::session::require_uid;
7use serde::Deserialize;
8use utoipa::ToSchema;
9
10use crate::errors::ApiError;
11use crate::services::AuthService;
12use crate::share::rpc;
13
14#[derive(Debug, Deserialize, ToSchema)]
15pub struct ChangePasswordCmd {
16 pub old_password: String,
17 pub new_password: String,
18}
19
20#[utoipa::path(
21 post,
22 path = "/auth/password/change",
23 tag = "auth",
24 description = "修改当前账号口令,成功后吊销除当前会话以外的全部会话。",
25 request_body = ChangePasswordCmd,
26 responses(
27 (status = 200, description = "password changed", body = AppResponseData<NoData>),
28 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
29 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
30 )
31)]
32pub async fn change_password(
33 State(service): State<AuthService>,
34 session: Session,
35 headers: HeaderMap,
36 Json(cmd): Json<ChangePasswordCmd>,
37) -> Result<AppResponseData<NoData>, ApiError> {
38 let uid = require_uid(&session)?;
39 let client = rpc::client(&service.state)?;
40 let keep = session.session_id().unwrap_or_default();
41 client
42 .change_password(
43 uid,
44 &rpc::digest(&cmd.old_password),
45 &rpc::digest(&cmd.new_password),
46 keep,
47 &rpc::client_ip(&headers),
48 )
49 .await
50 .map_err(|error| ApiError::bad_request(error.to_string()))?;
51 Ok(AppResponseData::empty())
52}
No newline at end of file
Added lib/auth/src/handlers/sessions.rs lib/auth/src/handlers/sessions.rs +197 −0 View lib/auth/src/handlers/sessions.rs
@@ -0,0 +1,197 @@
1use axum::extract::{Path, Query, State};
2use export::pagination::{Page, PageQuery, paginate};
3use export::rest::{AppResponseData, NoData};
4use jwt::Session;
5use jwt::share::session::require_uid;
6use kenauth::proto::common::v1::{PageRequest, SessionInfo};
7use kenauth::proto::v1::ListUserSessionsResponse;
8use serde::{Deserialize, Serialize};
9use time::OffsetDateTime;
10use utoipa::ToSchema;
11use uuid::Uuid;
12
13use crate::errors::ApiError;
14use crate::services::AuthService;
15use crate::share::rpc;
16
17const SESSION_PAGE_SIZE: i32 = 100;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
20pub enum SessionKind {
21 WebLogin,
22 BearerAccess,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
26pub struct SessionDto {
27 pub id: Uuid,
28 pub kind: SessionKind,
29 pub client_ip_hash: Option<String>,
30 pub user_agent_hash: Option<String>,
31 pub device_id: Option<Uuid>,
32 pub last_seen_at: OffsetDateTime,
33 pub created_at: OffsetDateTime,
34 pub expires_at: OffsetDateTime,
35 pub is_revoked: bool,
36 pub revoked_at: Option<OffsetDateTime>,
37 pub is_current: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
41pub struct CountResp {
42 pub count: i64,
43}
44
45#[derive(Debug, Deserialize, utoipa::IntoParams)]
46pub struct SessionQuery {
47 pub kind: Option<SessionKind>,
48 pub include_revoked: Option<bool>,
49 pub limit: Option<u64>,
50 pub offset: Option<u64>,
51}
52
53impl SessionQuery {
54 fn page(&self) -> PageQuery {
55 PageQuery {
56 limit: self.limit,
57 offset: self.offset,
58 sort: None,
59 }
60 }
61}
62
63pub(crate) fn session_list(
64 listed: ListUserSessionsResponse,
65 current: Option<&str>,
66) -> Result<Vec<SessionDto>, ApiError> {
67 let mut out = Vec::with_capacity(listed.sessions.len());
68 for info in listed.sessions {
69 out.push(session_dto(info, current)?);
70 }
71 Ok(out)
72}
73
74fn session_dto(info: SessionInfo, current: Option<&str>) -> Result<SessionDto, ApiError> {
75 let created_at = info
76 .created_at
77 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
78 .ok_or_else(|| ApiError::internal("kenoauth returned a session without created_at"))?;
79 let last_seen_at = info
80 .last_seen_at
81 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
82 .unwrap_or(created_at);
83 let expires_at = info
84 .expires_at
85 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
86 .unwrap_or(created_at);
87 Ok(SessionDto {
88 id: Uuid::parse_str(&info.session_id)
89 .map_err(|_| ApiError::internal("kenoauth returned a malformed session id"))?,
90 kind: SessionKind::WebLogin,
91 client_ip_hash: hash_field(&info.client_ip),
92 user_agent_hash: hash_field(&info.user_agent),
93 device_id: None,
94 is_current: current == Some(info.session_id.as_str()),
95 is_revoked: info.revoked_at.is_some(),
96 revoked_at: info
97 .revoked_at
98 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos)),
99 last_seen_at,
100 created_at,
101 expires_at,
102 })
103}
104
105pub(crate) fn hash_field(value: &str) -> Option<String> {
106 if value.trim().is_empty() {
107 None
108 } else {
109 Some(rpc::digest(value))
110 }
111}
112
113#[utoipa::path(
114 get,
115 path = "/me/sessions",
116 tag = "user",
117 description = "列出当前账号的登录会话,并标记当前会话。",
118 params(SessionQuery),
119 responses(
120 (status = 200, description = "sessions", body = AppResponseData<Page<SessionDto>>),
121 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
122 )
123)]
124pub async fn list_sessions(
125 State(service): State<AuthService>,
126 session: Session,
127 Query(query): Query<SessionQuery>,
128) -> Result<AppResponseData<Page<SessionDto>>, ApiError> {
129 let uid = require_uid(&session)?;
130 let client = rpc::client(&service.state)?;
131 let listed = client
132 .list_user_sessions(
133 uid,
134 query.include_revoked.unwrap_or(false),
135 true,
136 PageRequest {
137 page_size: SESSION_PAGE_SIZE,
138 page_token: String::new(),
139 },
140 )
141 .await
142 .map_err(|error| ApiError::internal(error.to_string()))?;
143 let sessions = session_list(listed, session.session_id())?;
144 Ok(AppResponseData::ok(paginate(sessions, &query.page())))
145}
146
147#[utoipa::path(
148 delete,
149 path = "/me/sessions",
150 tag = "user",
151 description = "吊销当前账号除当前会话以外的全部会话。",
152 responses(
153 (status = 200, description = "sessions revoked", body = AppResponseData<CountResp>),
154 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
155 )
156)]
157pub async fn revoke_all_sessions(
158 State(service): State<AuthService>,
159 session: Session,
160) -> Result<AppResponseData<CountResp>, ApiError> {
161 let uid = require_uid(&session)?;
162 let client = rpc::client(&service.state)?;
163 let revoked = client
164 .revoke_user_sessions(uid, session.session_id().unwrap_or_default(), "user_logout", false)
165 .await
166 .map_err(|error| ApiError::internal(error.to_string()))?;
167 Ok(AppResponseData::ok(CountResp {
168 count: i64::from(revoked.revoked_count),
169 }))
170}
171
172#[utoipa::path(
173 delete,
174 path = "/me/sessions/{session_id}",
175 tag = "user",
176 description = "吊销指定会话;会话不存在或不属于当前账号时返回 404。",
177 params(("session_id" = Uuid, Path)),
178 responses(
179 (status = 200, description = "session revoked", body = AppResponseData<NoData>),
180 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
181 (status = 404, description = "not found", body = AppResponseData<NoData>)
182 )
183)]
184pub async fn revoke_session(
185 State(service): State<AuthService>,
186 session: Session,
187 Path(session_id): Path<Uuid>,
188) -> Result<AppResponseData<NoData>, ApiError> {
189 let uid = require_uid(&session)?;
190 let client = rpc::client(&service.state)?;
191 let revoked = client
192 .revoke_session(uid, &session_id.to_string(), "user_revoked")
193 .await
194 .map_err(|error| ApiError::internal(error.to_string()))?;
195 let _ = revoked;
196 Ok(AppResponseData::empty())
197}
No newline at end of file
Added lib/auth/src/handlers/ssh_keys.rs lib/auth/src/handlers/ssh_keys.rs +165 −0 View lib/auth/src/handlers/ssh_keys.rs
@@ -0,0 +1,165 @@
1use axum::Json;
2use axum::extract::{Path, Query, State};
3use axum::http::HeaderMap;
4use export::pagination::{Page, PageQuery, paginate};
5use export::rest::{AppResponseData, NoData};
6use jwt::Session;
7use jwt::share::session::require_uid;
8use kenauth::proto::common::v1::{PageRequest, SshKeyAlgorithm, SshKeyInfo};
9use serde::{Deserialize, Serialize};
10use time::OffsetDateTime;
11use utoipa::ToSchema;
12use uuid::Uuid;
13
14use crate::errors::ApiError;
15use crate::services::AuthService;
16use crate::share::rpc;
17
18const KEY_PAGE_SIZE: i32 = 100;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
21#[serde(rename_all = "snake_case")]
22pub enum SshKeyType {
23 SshRsa,
24 SshEd25519,
25 EcdsaSha2Nistp256,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
29pub struct SshKeyDto {
30 pub id: Uuid,
31 pub name: String,
32 pub key_type: SshKeyType,
33 pub fingerprint: String,
34 pub created_at: OffsetDateTime,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
38pub struct AddSshKeyCmd {
39 pub name: String,
40 pub content: String,
41}
42
43fn key_type_of(algorithm: i32) -> SshKeyType {
44 match SshKeyAlgorithm::try_from(algorithm) {
45 Ok(SshKeyAlgorithm::Rsa) => SshKeyType::SshRsa,
46 Ok(SshKeyAlgorithm::Ecdsa) => SshKeyType::EcdsaSha2Nistp256,
47 _ => SshKeyType::SshEd25519,
48 }
49}
50
51fn key_dto(info: SshKeyInfo) -> Result<SshKeyDto, ApiError> {
52 let created_at = info
53 .created_at
54 .and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
55 .ok_or_else(|| ApiError::internal("kenoauth returned a key without created_at"))?;
56 Ok(SshKeyDto {
57 id: Uuid::parse_str(&info.key_id)
58 .map_err(|_| ApiError::internal("kenoauth returned a malformed key id"))?,
59 key_type: key_type_of(info.algorithm),
60 name: info.name,
61 fingerprint: info.fingerprint,
62 created_at,
63 })
64}
65
66#[utoipa::path(
67 get,
68 path = "/auth/ssh-keys",
69 tag = "security",
70 description = "列出当前账号已登记的 SSH 公钥。",
71 params(PageQuery),
72 responses(
73 (status = 200, description = "ssh public keys", body = AppResponseData<Page<SshKeyDto>>),
74 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
75 )
76)]
77pub async fn list_ssh_keys(
78 State(service): State<AuthService>,
79 session: Session,
80 Query(query): Query<PageQuery>,
81) -> Result<AppResponseData<Page<SshKeyDto>>, ApiError> {
82 let uid = require_uid(&session)?;
83 let client = rpc::client(&service.state)?;
84 let listed = client
85 .list_user_ssh_keys(
86 uid,
87 false,
88 PageRequest {
89 page_size: KEY_PAGE_SIZE,
90 page_token: String::new(),
91 },
92 )
93 .await
94 .map_err(|error| ApiError::internal(error.to_string()))?;
95 let mut keys = Vec::with_capacity(listed.keys.len());
96 for info in listed.keys {
97 keys.push(key_dto(info)?);
98 }
99 Ok(AppResponseData::ok(paginate(keys, &query)))
100}
101
102#[utoipa::path(
103 post,
104 path = "/auth/ssh-keys",
105 tag = "security",
106 description = "登记一把新的 SSH 公钥,用于 git over SSH。",
107 request_body = AddSshKeyCmd,
108 responses(
109 (status = 200, description = "ssh public key added", body = AppResponseData<SshKeyDto>),
110 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
111 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
112 )
113)]
114pub async fn add_ssh_key(
115 State(service): State<AuthService>,
116 session: Session,
117 headers: HeaderMap,
118 Json(cmd): Json<AddSshKeyCmd>,
119) -> Result<AppResponseData<SshKeyDto>, ApiError> {
120 let uid = require_uid(&session)?;
121 let client = rpc::client(&service.state)?;
122 let added = client
123 .add_ssh_key(
124 uid,
125 cmd.name.trim(),
126 cmd.content.trim(),
127 "",
128 &rpc::client_ip(&headers),
129 )
130 .await
131 .map_err(|error| ApiError::bad_request(error.to_string()))?;
132 let key = added
133 .key
134 .ok_or_else(|| ApiError::internal("kenoauth returned no key"))?;
135 Ok(AppResponseData::ok(key_dto(key)?))
136}
137
138#[utoipa::path(
139 delete,
140 path = "/auth/ssh-keys/{key_id}",
141 tag = "security",
142 description = "吊销指定 SSH 公钥;密钥不存在或不属于当前账号时返回 404。",
143 params(("key_id" = Uuid, Path)),
144 responses(
145 (status = 200, description = "ssh public key revoked", body = AppResponseData<NoData>),
146 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
147 (status = 404, description = "not found", body = AppResponseData<NoData>)
148 )
149)]
150pub async fn delete_ssh_key(
151 State(service): State<AuthService>,
152 session: Session,
153 Path(key_id): Path<Uuid>,
154) -> Result<AppResponseData<NoData>, ApiError> {
155 let uid = require_uid(&session)?;
156 let client = rpc::client(&service.state)?;
157 let revoked = client
158 .revoke_ssh_key(uid, &key_id.to_string(), "user_revoked", false)
159 .await
160 .map_err(|error| ApiError::internal(error.to_string()))?;
161 if !revoked.revoked {
162 return Err(ApiError::not_found("ssh key not found"));
163 }
164 Ok(AppResponseData::empty())
165}
No newline at end of file
Added lib/auth/src/handlers/two_factor.rs lib/auth/src/handlers/two_factor.rs +225 −0 View lib/auth/src/handlers/two_factor.rs
@@ -0,0 +1,225 @@
1use axum::Json;
2use axum::extract::State;
3use axum::http::HeaderMap;
4use export::rest::{AppResponseData, NoData};
5use jwt::Session;
6use jwt::share::session::require_uid;
7use kenauth::proto::common::v1::MfaStatus;
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11use crate::errors::ApiError;
12use crate::services::AuthService;
13use crate::share::rpc;
14
15#[derive(Debug, Serialize, ToSchema)]
16pub struct TotpStatusResp {
17 pub enabled: bool,
18}
19
20#[derive(Debug, Serialize, ToSchema)]
21pub struct TotpSetupResp {
22 pub secret: String,
23 pub otpauth_uri: String,
24}
25
26#[derive(Debug, Deserialize, ToSchema)]
27pub struct TotpEnableCmd {
28 pub code: String,
29 pub password: String,
30}
31
32#[derive(Debug, Deserialize, ToSchema)]
33pub struct TotpCodeCmd {
34 pub code: String,
35}
36
37#[derive(Debug, Deserialize, ToSchema)]
38pub struct RegenerateRecoveryCodesCmd {
39 pub password: String,
40}
41
42#[derive(Debug, Serialize, ToSchema)]
43pub struct RecoveryCodesResp {
44 pub total: u32,
45 pub used: u32,
46 pub codes: Vec<String>,
47}
48
49#[utoipa::path(
50 get,
51 path = "/auth/2fa/status",
52 tag = "auth",
53 description = "查询当前账号是否已启用两步验证。",
54 responses(
55 (status = 200, description = "two-factor status", body = AppResponseData<TotpStatusResp>),
56 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
57 )
58)]
59pub async fn totp_status(
60 State(service): State<AuthService>,
61 session: Session,
62) -> Result<AppResponseData<TotpStatusResp>, ApiError> {
63 let uid = require_uid(&session)?;
64 let client = rpc::client(&service.state)?;
65 let status = client
66 .get_mfa_status(uid)
67 .await
68 .map_err(|error| ApiError::internal(error.to_string()))?;
69 let enabled = status
70 .factors
71 .iter()
72 .any(|factor| factor.status == MfaStatus::Enabled as i32);
73 Ok(AppResponseData::ok(TotpStatusResp { enabled }))
74}
75
76#[utoipa::path(
77 post,
78 path = "/auth/2fa/setup",
79 tag = "auth",
80 description = "生成新的 TOTP 密钥并返回 otpauth URI,密钥明文只返回一次。",
81 responses(
82 (status = 200, description = "totp secret provisioned", body = AppResponseData<TotpSetupResp>),
83 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
84 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
85 )
86)]
87pub async fn totp_setup(
88 State(service): State<AuthService>,
89 session: Session,
90 headers: HeaderMap,
91) -> Result<AppResponseData<TotpSetupResp>, ApiError> {
92 let uid = require_uid(&session)?;
93 let client = rpc::client(&service.state)?;
94 let configured = client
95 .configure_totp(uid, "TOTP", &rpc::client_ip(&headers))
96 .await
97 .map_err(|error| ApiError::bad_request(error.to_string()))?;
98 Ok(AppResponseData::ok(TotpSetupResp {
99 secret: configured.secret,
100 otpauth_uri: configured.otpauth_uri,
101 }))
102}
103
104#[utoipa::path(
105 post,
106 path = "/auth/2fa/enable",
107 tag = "auth",
108 description = "用动态码与账号口令确认启用两步验证,返回一次性恢复码。",
109 request_body = TotpEnableCmd,
110 responses(
111 (status = 200, description = "two-factor enabled and recovery codes issued", body = AppResponseData<RecoveryCodesResp>),
112 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
113 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
114 )
115)]
116pub async fn totp_enable(
117 State(service): State<AuthService>,
118 session: Session,
119 headers: HeaderMap,
120 Json(cmd): Json<TotpEnableCmd>,
121) -> Result<AppResponseData<RecoveryCodesResp>, ApiError> {
122 let uid = require_uid(&session)?;
123 let client = rpc::client(&service.state)?;
124 let enabled = client
125 .enable_totp(
126 uid,
127 cmd.code.trim(),
128 &rpc::digest(&cmd.password),
129 &rpc::client_ip(&headers),
130 )
131 .await
132 .map_err(|error| ApiError::bad_request(error.to_string()))?;
133 let total = enabled.recovery_codes.len() as u32;
134 Ok(AppResponseData::ok(RecoveryCodesResp {
135 total,
136 used: 0,
137 codes: enabled.recovery_codes,
138 }))
139}
140
141#[utoipa::path(
142 post,
143 path = "/auth/2fa/disable",
144 tag = "auth",
145 description = "用动态码或一枚恢复码关闭两步验证,并吊销全部恢复码。",
146 request_body = TotpCodeCmd,
147 responses(
148 (status = 200, description = "two-factor disabled", body = AppResponseData<NoData>),
149 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
150 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
151 )
152)]
153pub async fn totp_disable(
154 State(service): State<AuthService>,
155 session: Session,
156 headers: HeaderMap,
157 Json(cmd): Json<TotpCodeCmd>,
158) -> Result<AppResponseData<NoData>, ApiError> {
159 let uid = require_uid(&session)?;
160 let client = rpc::client(&service.state)?;
161 client
162 .disable_totp(uid, cmd.code.trim(), &rpc::client_ip(&headers))
163 .await
164 .map_err(|error| ApiError::bad_request(error.to_string()))?;
165 Ok(AppResponseData::empty())
166}
167
168#[utoipa::path(
169 get,
170 path = "/auth/2fa/recovery-codes",
171 tag = "auth",
172 description = "查看恢复码批次用量。明文只在生成时返回一次,这里不会回显。",
173 responses(
174 (status = 200, description = "recovery code usage", body = AppResponseData<RecoveryCodesResp>),
175 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
176 )
177)]
178pub async fn recovery_codes(
179 State(service): State<AuthService>,
180 session: Session,
181) -> Result<AppResponseData<RecoveryCodesResp>, ApiError> {
182 let uid = require_uid(&session)?;
183 let client = rpc::client(&service.state)?;
184 let status = client
185 .get_mfa_status(uid)
186 .await
187 .map_err(|error| ApiError::internal(error.to_string()))?;
188 Ok(AppResponseData::ok(RecoveryCodesResp {
189 total: status.recovery_codes_total,
190 used: status.recovery_codes_used,
191 codes: Vec::new(),
192 }))
193}
194
195#[utoipa::path(
196 put,
197 path = "/auth/2fa/recovery-codes",
198 tag = "auth",
199 description = "校验账号口令后重建恢复码批次,返回新的一批明文。",
200 request_body = RegenerateRecoveryCodesCmd,
201 responses(
202 (status = 200, description = "recovery codes regenerated", body = AppResponseData<RecoveryCodesResp>),
203 (status = 400, description = "request failed validation", body = AppResponseData<NoData>),
204 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
205 )
206)]
207pub async fn regenerate_recovery_codes(
208 State(service): State<AuthService>,
209 session: Session,
210 headers: HeaderMap,
211 Json(cmd): Json<RegenerateRecoveryCodesCmd>,
212) -> Result<AppResponseData<RecoveryCodesResp>, ApiError> {
213 let uid = require_uid(&session)?;
214 let client = rpc::client(&service.state)?;
215 let regenerated = client
216 .regenerate_recovery_codes(uid, &rpc::digest(&cmd.password), &rpc::client_ip(&headers))
217 .await
218 .map_err(|error| ApiError::bad_request(error.to_string()))?;
219 let total = regenerated.recovery_codes.len() as u32;
220 Ok(AppResponseData::ok(RecoveryCodesResp {
221 total,
222 used: 0,
223 codes: regenerated.recovery_codes,
224 }))
225}
No newline at end of file
Modified lib/auth/src/oauth/login.rs lib/auth/src/oauth/login.rs +80 −8 View lib/auth/src/oauth/login.rs
@@ -3,6 +3,8 @@
33use serde::Deserialize;
44use url::form_urlencoded;
55
6use config::oauth::OAuthConfig;
7
68use crate::errors::ApiError;
79use crate::oauth::{LoginState, pkce_challenge, random_token, state};
810use crate::services::AuthService;
@@ -15,6 +17,57 @@
1517 pub silent: Option<u8>,
1618}
1719
20#[derive(Debug, Deserialize)]
21pub struct SignupQuery {
22 #[serde(default)]
23 pub redirect: Option<String>,
24}
25
26/// 注册表单已迁移到 KenOAuth:跳它的 `/register`。
27///
28/// 不能直接跳 `/register` 了事——那样注册完的人没有会话,也没有回到 GitData 的路径。
29/// 这里先按注册前的 `/auth/login` 跑一遍握手(PKCE + nonce 存 Redis),
30/// 把授权地址作为 `callback_url` 交给 KenOAuth:用户在那里注册并登录后,
31/// `/success` 页用 `location.replace` 回到授权端点,走正常 callback 落地。
32///
33/// 多出来的那一份 state 不会被 `take()`,600s 后自然过期。
34pub async fn signup(
35 State(service): State<AuthService>,
36 Query(query): Query<SignupQuery>,
37) -> Result<Response, ApiError> {
38 let config = &service.config().oauth;
39 if !config.enabled {
40 return Err(ApiError::not_found("oauth is disabled"));
41 }
42 let redirect = sanitize_redirect(query.redirect.as_deref());
43 let state_token = random_token(32);
44 let verifier = random_token(48);
45 let nonce = random_token(16);
46 state::store(
47 &service,
48 &state_token,
49 &LoginState {
50 redirect: redirect.clone(),
51 verifier: verifier.clone(),
52 nonce: nonce.clone(),
53 },
54 )
55 .await
56 .map_err(ApiError::from)?;
57
58 // 注册后直接拿授权码,省掉一次「请再点一次登录」。
59 let authorization = authorize_url(config, &state_token, &verifier, &nonce, false);
60
61 let mut serializer = form_urlencoded::Serializer::new(String::new());
62 serializer.append_pair("callback_url", &authorization);
63 let location = format!(
64 "{}/register?{}",
65 config.issuer.trim_end_matches('/'),
66 serializer.finish()
67 );
68 Ok(Redirect::to(&location).into_response())
69}
70
1871pub async fn login(
1972 State(service): State<AuthService>,
2073 Query(query): Query<LoginQuery>,
@@ -39,26 +92,45 @@
3992 .await
4093 .map_err(ApiError::from)?;
4194
95 let location = authorize_url(
96 config,
97 &state_token,
98 &verifier,
99 &nonce,
100 query.silent == Some(1),
101 );
102 Ok(Redirect::to(&location).into_response())
103}
104
105fn authorize_url(
106 config: &OAuthConfig,
107 state_token: &str,
108 verifier: &str,
109 nonce: &str,
110 silent: bool,
111) -> String {
42112 let mut serializer = form_urlencoded::Serializer::new(String::new());
43113 serializer.append_pair("response_type", "code");
44114 serializer.append_pair("client_id", &config.client_id);
45115 serializer.append_pair("redirect_uri", &config.redirect_uri);
46116 serializer.append_pair("scope", "openid profile email");
47 serializer.append_pair("state", &state_token);
48 serializer.append_pair("nonce", &nonce);
117 serializer.append_pair("state", state_token);
118 serializer.append_pair("nonce", nonce);
49119 serializer.append_pair("code_challenge_method", "S256");
50 serializer.append_pair("code_challenge", &pkce_challenge(&verifier));
51 if query.silent == Some(1) {
120 serializer.append_pair("code_challenge", &pkce_challenge(verifier));
121 if silent {
52122 serializer.append_pair("prompt", "none");
53123 }
54
55 let location = format!("{}?{}", config.authorization_endpoint(), serializer.finish());
56 Ok(Redirect::to(&location).into_response())
124 format!("{}?{}", config.authorization_endpoint(), serializer.finish())
57125}
58126
127/// 未指定去向时的落点:登录/注册完成直接进工作区。
128/// `/app` 在路由里会再跳一次 `/app/@me`。
129pub const DEFAULT_REDIRECT: &str = "/app";
130
59131fn sanitize_redirect(raw: Option<&str>) -> String {
60132 match raw.map(str::trim) {
61133 Some(value) if value.starts_with('/') && !value.starts_with("//") => value.to_string(),
62 _ => "/".to_string(),
134 _ => DEFAULT_REDIRECT.to_string(),
63135 }
64136}
No newline at end of file
Modified lib/auth/src/share/mod.rs lib/auth/src/share/mod.rs +1 −0 View lib/auth/src/share/mod.rs
@@ -1,5 +1,6 @@
11pub mod crypto;
22pub mod guard;
3pub mod rpc;
34pub mod types;
45pub mod validate;
56
Added lib/auth/src/share/rpc.rs lib/auth/src/share/rpc.rs +46 −0 View lib/auth/src/share/rpc.rs
@@ -0,0 +1,46 @@
1use std::sync::Arc;
2
3use axum::http::HeaderMap;
4use export::AppState;
5use kenauth::KenAuthClient;
6use sha2::{Digest, Sha256};
7use time::OffsetDateTime;
8
9use crate::errors::ApiError;
10
11pub fn client(state: &AppState) -> Result<&Arc<KenAuthClient>, ApiError> {
12 state
13 .kenauth
14 .as_ref()
15 .ok_or_else(|| ApiError::internal("kenoauth client is not configured"))
16}
17
18pub fn client_ip(headers: &HeaderMap) -> String {
19 headers
20 .get("x-forwarded-for")
21 .and_then(|value| value.to_str().ok())
22 .and_then(|value| value.split(',').next())
23 .map(str::trim)
24 .filter(|value| !value.is_empty())
25 .or_else(|| {
26 headers
27 .get("x-real-ip")
28 .and_then(|value| value.to_str().ok())
29 .map(str::trim)
30 })
31 .unwrap_or_default()
32 .to_string()
33}
34
35pub fn digest(password: &str) -> String {
36 let hash = Sha256::digest(password.as_bytes());
37 let mut out = String::with_capacity(64);
38 for byte in hash {
39 out.push_str(&format!("{byte:02x}"));
40 }
41 out
42}
43pub fn datetime(seconds: i64, nanos: i32) -> Option<OffsetDateTime> {
44 OffsetDateTime::from_unix_timestamp_nanos(i128::from(seconds) * 1_000_000_000 + i128::from(nanos))
45 .ok()
46}
Modified lib/auth/src/openapi.rs lib/auth/src/openapi.rs +42 −0 View lib/auth/src/openapi.rs
@@ -32,6 +32,25 @@
3232 handlers::unread_notifications_count::unread_notifications_count,
3333 handlers::update_preferences::update_preferences,
3434 handlers::update_profile::update_profile,
35 handlers::jwt_request::jwt_request,
36 handlers::password::change_password,
37 handlers::two_factor::totp_status,
38 handlers::two_factor::totp_setup,
39 handlers::two_factor::totp_enable,
40 handlers::two_factor::totp_disable,
41 handlers::two_factor::recovery_codes,
42 handlers::two_factor::regenerate_recovery_codes,
43 handlers::access_keys::list_access_keys,
44 handlers::access_keys::create_access_key,
45 handlers::access_keys::delete_access_key,
46 handlers::access_keys::rotate_access_key,
47 handlers::ssh_keys::list_ssh_keys,
48 handlers::ssh_keys::add_ssh_key,
49 handlers::ssh_keys::delete_ssh_key,
50 handlers::sessions::list_sessions,
51 handlers::sessions::revoke_all_sessions,
52 handlers::sessions::revoke_session,
53 handlers::devices::list_devices,
3554 ),
3655 components(schemas(
3756 export::pagination::PageQuery,
@@ -54,6 +73,29 @@
5473 ReportDto,
5574 UnreadCountResp,
5675 NotificationType,
76 handlers::jwt_request::JwtRequestQuery,
77 handlers::jwt_request::JwtRequestResp,
78 handlers::password::ChangePasswordCmd,
79 handlers::two_factor::TotpStatusResp,
80 handlers::two_factor::TotpSetupResp,
81 handlers::two_factor::TotpEnableCmd,
82 handlers::two_factor::TotpCodeCmd,
83 handlers::two_factor::RegenerateRecoveryCodesCmd,
84 handlers::two_factor::RecoveryCodesResp,
85 handlers::access_keys::AccessKeyDto,
86 handlers::access_keys::CreateAccessKeyCmd,
87 handlers::access_keys::CreateAccessKeyResp,
88 models::users::access_scope::AccessKeyScope,
89 export::pagination::Page<handlers::access_keys::AccessKeyDto>,
90 handlers::ssh_keys::SshKeyDto,
91 handlers::ssh_keys::SshKeyType,
92 handlers::ssh_keys::AddSshKeyCmd,
93 export::pagination::Page<handlers::ssh_keys::SshKeyDto>,
94 handlers::sessions::SessionDto,
95 handlers::sessions::SessionKind,
96 handlers::sessions::CountResp,
97 export::pagination::Page<handlers::sessions::SessionDto>,
98 handlers::devices::DeviceDto,
5799 ))
58100)]
59101pub struct AuthApiDoc;
No newline at end of file
Modified lib/auth/src/routes.rs lib/auth/src/routes.rs +50 −1 View lib/auth/src/routes.rs
@@ -1,6 +1,6 @@
11use axum::Router;
22use axum::middleware;
3use axum::routing::{delete, get, patch, post, put};
3use axum::routing::{delete, get, post, put};
44
55use crate::handlers::accept_agreement::accept_agreement;
66use crate::handlers::agreement_page::agreement_page;
@@ -22,6 +22,7 @@
2222pub fn router(service: AuthService) -> Router<AuthService> {
2323 let public = Router::new()
2424 .route("/auth/login", get(oauth::login::login))
25 .route("/auth/signup", get(oauth::login::signup))
2526 .route("/auth/callback", get(oauth::callback::callback))
2627 .route("/auth/refresh", post(oauth::refresh::refresh))
2728 .route("/auth/logout", post(oauth::logout::logout))
@@ -56,6 +57,54 @@
5657 put(crate::handlers::mark_notifications_read::mark_notifications_read),
5758 )
5859 .route("/me/reports", post(create_report))
60 .route(
61 "/auth/password/change",
62 post(crate::handlers::password::change_password),
63 )
64 .route("/auth/2fa/status", get(crate::handlers::two_factor::totp_status))
65 .route("/auth/2fa/setup", post(crate::handlers::two_factor::totp_setup))
66 .route("/auth/2fa/enable", post(crate::handlers::two_factor::totp_enable))
67 .route("/auth/2fa/disable", post(crate::handlers::two_factor::totp_disable))
68 .route(
69 "/auth/2fa/recovery-codes",
70 get(crate::handlers::two_factor::recovery_codes)
71 .put(crate::handlers::two_factor::regenerate_recovery_codes),
72 )
73 .route(
74 "/auth/access-keys",
75 get(crate::handlers::access_keys::list_access_keys)
76 .post(crate::handlers::access_keys::create_access_key),
77 )
78 .route(
79 "/auth/access-keys/{key_id}",
80 delete(crate::handlers::access_keys::delete_access_key),
81 )
82 .route(
83 "/auth/access-keys/{key_id}/rotate",
84 post(crate::handlers::access_keys::rotate_access_key),
85 )
86 .route(
87 "/auth/ssh-keys",
88 get(crate::handlers::ssh_keys::list_ssh_keys)
89 .post(crate::handlers::ssh_keys::add_ssh_key),
90 )
91 .route(
92 "/auth/ssh-keys/{key_id}",
93 delete(crate::handlers::ssh_keys::delete_ssh_key),
94 )
95 .route(
96 "/me/sessions",
97 get(crate::handlers::sessions::list_sessions)
98 .delete(crate::handlers::sessions::revoke_all_sessions),
99 )
100 .route(
101 "/me/sessions/{session_id}",
102 delete(crate::handlers::sessions::revoke_session),
103 )
104 .route(
105 "/me/devices",
106 get(crate::handlers::devices::list_devices),
107 )
59108 .route_layer(middleware::from_fn_with_state(service, auth_guard));
60109
61110 public.merge(authed)
Modified lib/config/src/oauth.rs lib/config/src/oauth.rs +3 −3 View lib/config/src/oauth.rs
@@ -74,7 +74,7 @@
7474 if !self.token_endpoint.is_empty() {
7575 return self.token_endpoint.clone();
7676 }
77 format!("{}/oauth/token", self.issuer.trim_end_matches('/'))
77 format!("{}/api/v1/oauth/token", self.issuer.trim_end_matches('/'))
7878 }
7979
8080 pub fn jwks_url(&self) -> String {
@@ -88,11 +88,11 @@
8888 }
8989
9090 pub fn authorization_endpoint(&self) -> String {
91 format!("{}/oauth/authorize", self.issuer.trim_end_matches('/'))
91 format!("{}/api/v1/oauth/authorize", self.issuer.trim_end_matches('/'))
9292 }
9393
9494 pub fn end_session_endpoint(&self) -> String {
95 format!("{}/oauth/logout", self.issuer.trim_end_matches('/'))
95 format!("{}/api/v1/oauth/logout", self.issuer.trim_end_matches('/'))
9696 }
9797
9898 pub fn is_valid(&self) -> Result<(), String> {
Modified lib/kenauth/proto/v1/access_key.proto lib/kenauth/proto/v1/access_key.proto +37 −0 View lib/kenauth/proto/v1/access_key.proto
@@ -16,6 +16,16 @@
1616 // 校验 access key 明文。热路径:结果按摘要缓存 60s,吊销会主动失效。
1717 rpc VerifyAccessKey(VerifyAccessKeyRequest) returns (VerifyAccessKeyResponse);
1818
19 // 签发一枚用户令牌。明文只在本次响应出现一次,库内只存摘要。
20 // 供外部账户服务在“创建访问令牌”入口调用;上限与 scope 校验同 HTTP 入口。
21 // mutation: request_id 通过 gitdata-request-id 头携带
22 rpc CreateAccessKey(CreateAccessKeyRequest) returns (CreateAccessKeyResponse);
23
24 // 为已签发的令牌换一枚新的明文(保留编号、名称与 scope,摘要与指纹重算)。
25 // 已吊销或不存在的令牌返回 NOT_FOUND。明文只返回一次。
26 // mutation: request_id 通过 gitdata-request-id 头携带
27 rpc RotateAccessKey(RotateAccessKeyRequest) returns (RotateAccessKeyResponse);
28
1929 // 列出某用户的令牌(不含明文)。默认不含已吊销。
2030 rpc ListAccessKeys(ListAccessKeysRequest) returns (ListAccessKeysResponse);
2131
@@ -43,6 +53,33 @@
4353 int32 current_security_version = 5;
4454}
4555
56message CreateAccessKeyRequest {
57 int64 user_id = 1;
58 string name = 2;
59 string description = 3;
60 repeated auth.common.v1.TokenScope scopes = 4;
61 // 0 = 永不过期。超出服务端允许区间时返回 INVALID_ARGUMENT。
62 int64 expires_in_secs = 5;
63 string client_ip = 6;
64}
65
66message CreateAccessKeyResponse {
67 auth.common.v1.AccessKeyInfo key = 1;
68 // 令牌明文,形如 "gd_" + 32 字节随机数的 base64url;只返回一次。
69 string token = 2;
70}
71
72message RotateAccessKeyRequest {
73 int64 user_id = 1;
74 string access_key_id = 2;
75 string client_ip = 3;
76}
77
78message RotateAccessKeyResponse {
79 auth.common.v1.AccessKeyInfo key = 1;
80 string token = 2;
81}
82
4683message ListAccessKeysRequest {
4784 int64 user_id = 1;
4885 bool include_revoked = 2;
Modified lib/kenauth/proto/v1/identity.proto lib/kenauth/proto/v1/identity.proto +23 −1 View lib/kenauth/proto/v1/identity.proto
@@ -6,7 +6,7 @@
66import "common/v1/types.proto";
77
88// 身份校验入口。调用方:api(用户接口前置)、git / ssh(HTTP 与 SSH 认证)、
9// chat / realtime / sfu(连接建立时)、issue / pr / object / notify / webhook。
9// chat / realtime / sfu(连接建立时)、issue / pr / agent / object / notify / webhook。
1010//
1111// 安全边界:
1212// * 凭据原文只在请求里出现,auth 立即摘要后丢弃,绝不回显、绝不落日志。
@@ -36,6 +36,11 @@
3636
3737 // 按用户名解析或创建机器人账号。同一用户名重复调用返回同一 user_id。
3838 rpc EnsureBotUser(EnsureBotUserRequest) returns (EnsureBotUserResponse);
39
40 // 校验旧口令后改写口令摘要,推进 security_version 并吊销全部会话(可保留调用方当前会话)。
41 // 口令只以客户端摘要(sha256 hex)形式出现,本服务不接触明文。
42 // mutation: request_id 通过 gitdata-request-id 头携带
43 rpc ChangePassword(ChangePasswordRequest) returns (ChangePasswordResponse);
3944}
4045
4146message AuthenticateRequest {
@@ -108,3 +113,20 @@
108113 auth.common.v1.UserPublic user = 1;
109114 bool created = 2;
110115}
116
117message ChangePasswordRequest {
118 int64 user_id = 1;
119 // 旧口令的客户端摘要(sha256 hex,64 字符)。未设口令的账号留空。
120 string old_password_hash = 2;
121 // 新口令的客户端摘要(sha256 hex,64 字符)。
122 string new_password_hash = 3;
123 // 非空时保留该会话不吊销;调用方传自己的 session_id。
124 string keep_session_id = 4;
125 string client_ip = 5;
126}
127
128message ChangePasswordResponse {
129 bool changed = 1;
130 int32 security_version = 2;
131 uint32 revoked_sessions = 3;
132}
Modified lib/kenauth/proto/v1/mfa.proto lib/kenauth/proto/v1/mfa.proto +68 −1 View lib/kenauth/proto/v1/mfa.proto
@@ -9,7 +9,9 @@
99// 多因子状态与校验。调用方:api(登录第二因子步骤)、auth 自身(登录用例内部复用)。
1010//
1111// 安全边界:TOTP 密钥(user_mfa.secret_ciphertext)永不出 auth,也不提供读取密钥的 RPC;
12// 绑定与解绑由用户 HTTP 接口完成,密钥明文只在绑定响应里出现一次。
12// 密钥明文只在 ConfigureTotp 的响应里出现一次,之后只以密文形式留存。
13// 绑定与解绑入口(ConfigureTotp / EnableTotp / DisableTotp / RegenerateRecoveryCodes)
14// 供外部账户服务的“两步验证”页面调用;口令只以客户端摘要形式出现。
1315// VerifyTotp 与 RedeemRecoveryCode 都是 mutation:服务端按 user_mfa.last_totp_step 自行防重放,
1416// 并消费一次性凭据,因此必须携带 request_id 以便审计。
1517service MfaService {
@@ -23,6 +25,25 @@
2325 // 消费一枚恢复码。成功即标记 consumed_at,幂等重复调用返回 RECOVERY_CODE_INVALID。
2426 // mutation: request_id 通过 gitdata-request-id 头携带
2527 rpc RedeemRecoveryCode(RedeemRecoveryCodeRequest) returns (RedeemRecoveryCodeResponse);
28
29 // 生成新的 TOTP 密钥并以 pending 状态落库,返回密钥明文与 otpauth URI 供验证器录入。
30 // 该用户已有 pending 因子时原地替换;已有 enabled 因子时返回 ALREADY_EXISTS。
31 // mutation: request_id 通过 gitdata-request-id 头携带
32 rpc ConfigureTotp(ConfigureTotpRequest) returns (ConfigureTotpResponse);
33
34 // 校验动态码与账号口令摘要,把 pending 因子置为 enabled 并签发一批恢复码。
35 // 恢复码明文只在本响应出现一次;同用户此前未消费的恢复码会被吊销。
36 // mutation: request_id 通过 gitdata-request-id 头携带
37 rpc EnableTotp(EnableTotpRequest) returns (EnableTotpResponse);
38
39 // 校验动态码或恢复码后吊销该用户的 TOTP 因子,并吊销全部恢复码。
40 // 没有 enabled 因子时返回 disabled=false 而不报错(幂等)。
41 // mutation: request_id 通过 gitdata-request-id 头携带
42 rpc DisableTotp(DisableTotpRequest) returns (DisableTotpResponse);
43
44 // 校验账号口令摘要后重建恢复码批次,返回新的一批明文。
45 // mutation: request_id 通过 gitdata-request-id 头携带
46 rpc RegenerateRecoveryCodes(RegenerateRecoveryCodesRequest) returns (RegenerateRecoveryCodesResponse);
2647}
2748
2849message GetMfaStatusRequest {
@@ -34,6 +55,8 @@
3455 bool login_requires_mfa = 1;
3556 repeated Factor factors = 2;
3657 uint32 recovery_codes_remaining = 3; // 未使用且未过期的备份码数量
58 uint32 recovery_codes_total = 4; // 当前批次总数(含已消费)
59 uint32 recovery_codes_used = 5; // 当前批次已消费数量
3760
3861 message Factor {
3962 string factor_id = 1; // user_mfa.id
@@ -75,3 +98,47 @@
7598 auth.common.v1.Code reason = 2;
7699 uint32 remaining_codes = 3;
77100}
101
102message ConfigureTotpRequest {
103 int64 user_id = 1;
104 string name = 2; // 因子显示名,空则用 "TOTP"
105 string client_ip = 3;
106}
107
108message ConfigureTotpResponse {
109 string factor_id = 1; // user_mfa.id
110 string secret = 2; // base32 明文,只返回一次
111 string otpauth_uri = 3; // otpauth://totp/...
112}
113
114message EnableTotpRequest {
115 int64 user_id = 1;
116 string code = 2; // 6 位动态码
117 string password_hash = 3; // 账号口令的客户端摘要(sha256 hex),用于二次确认
118 string client_ip = 4;
119}
120
121message EnableTotpResponse {
122 repeated string recovery_codes = 1;
123 google.protobuf.Timestamp enabled_at = 2;
124}
125
126message DisableTotpRequest {
127 int64 user_id = 1;
128 string code = 2; // 6 位动态码,或一枚恢复码
129 string client_ip = 3;
130}
131
132message DisableTotpResponse {
133 bool disabled = 1; // false = 该用户没有启用中的 TOTP 因子
134}
135
136message RegenerateRecoveryCodesRequest {
137 int64 user_id = 1;
138 string password_hash = 2; // 账号口令的客户端摘要(sha256 hex)
139 string client_ip = 3;
140}
141
142message RegenerateRecoveryCodesResponse {
143 repeated string recovery_codes = 1;
144}
Modified lib/kenauth/proto/v1/ssh_key.proto lib/kenauth/proto/v1/ssh_key.proto +18 −0 View lib/kenauth/proto/v1/ssh_key.proto
@@ -13,6 +13,11 @@
1313 // 按 fingerprint 或公钥原文定位用户。热路径:按 fingerprint 缓存 300s,吊销主动失效。
1414 rpc LookupSshKey(LookupSshKeyRequest) returns (LookupSshKeyResponse);
1515
16 // 为某用户登记一枚公钥。同一 fingerprint 已存在时返回 ALREADY_EXISTS。
17 // 供外部账户服务在“添加 SSH 密钥”入口调用。
18 // mutation: request_id 通过 gitdata-request-id 头携带
19 rpc AddSshKey(AddSshKeyRequest) returns (AddSshKeyResponse);
20
1621 // 列出某用户的公钥。默认不含已吊销。
1722 rpc ListUserSshKeys(ListUserSshKeysRequest) returns (ListUserSshKeysResponse);
1823
@@ -36,6 +41,19 @@
3641 int32 current_security_version = 4;
3742}
3843
44message AddSshKeyRequest {
45 int64 user_id = 1;
46 string name = 2;
47 // 单行 OpenSSH 公钥。无法解析或算法不支持时返回 INVALID_ARGUMENT。
48 string public_key = 3;
49 string description = 4;
50 string client_ip = 5;
51}
52
53message AddSshKeyResponse {
54 auth.common.v1.SshKeyInfo key = 1;
55}
56
3957message ListUserSshKeysRequest {
4058 int64 user_id = 1;
4159 bool include_revoked = 2;
Modified lib/kenauth/src/client.rs lib/kenauth/src/client.rs +292 −0 View lib/kenauth/src/client.rs
@@ -196,6 +196,298 @@
196196 )
197197 }
198198
199#[allow(clippy::too_many_arguments)]
200 pub async fn change_password(
201 &self,
202 user_id: i64,
203 old_password_hash: &str,
204 new_password_hash: &str,
205 keep_session_id: &str,
206 client_ip: &str,
207 ) -> Result<proto::v1::ChangePasswordResponse, KenAuthError> {
208 let mut client = self.identity();
209 client
210 .change_password(self.write_request(proto::v1::ChangePasswordRequest {
211 user_id,
212 old_password_hash: old_password_hash.to_string(),
213 new_password_hash: new_password_hash.to_string(),
214 keep_session_id: keep_session_id.to_string(),
215 client_ip: client_ip.to_string(),
216 }))
217 .await
218 .map_err(KenAuthError::from_status)
219 .map(|response| response.into_inner())
220 }
221
222 pub async fn get_mfa_status(
223 &self,
224 user_id: i64,
225 ) -> Result<proto::v1::GetMfaStatusResponse, KenAuthError> {
226 let mut client = self.mfa();
227 client
228 .get_mfa_status(self.read_request(proto::v1::GetMfaStatusRequest { user_id }))
229 .await
230 .map_err(KenAuthError::from_status)
231 .map(|response| response.into_inner())
232 }
233
234 pub async fn configure_totp(
235 &self,
236 user_id: i64,
237 name: &str,
238 client_ip: &str,
239 ) -> Result<proto::v1::ConfigureTotpResponse, KenAuthError> {
240 let mut client = self.mfa();
241 client
242 .configure_totp(self.write_request(proto::v1::ConfigureTotpRequest {
243 user_id,
244 name: name.to_string(),
245 client_ip: client_ip.to_string(),
246 }))
247 .await
248 .map_err(KenAuthError::from_status)
249 .map(|response| response.into_inner())
250 }
251
252 pub async fn enable_totp(
253 &self,
254 user_id: i64,
255 code: &str,
256 password_hash: &str,
257 client_ip: &str,
258 ) -> Result<proto::v1::EnableTotpResponse, KenAuthError> {
259 let mut client = self.mfa();
260 client
261 .enable_totp(self.write_request(proto::v1::EnableTotpRequest {
262 user_id,
263 code: code.to_string(),
264 password_hash: password_hash.to_string(),
265 client_ip: client_ip.to_string(),
266 }))
267 .await
268 .map_err(KenAuthError::from_status)
269 .map(|response| response.into_inner())
270 }
271
272 pub async fn disable_totp(
273 &self,
274 user_id: i64,
275 code: &str,
276 client_ip: &str,
277 ) -> Result<proto::v1::DisableTotpResponse, KenAuthError> {
278 let mut client = self.mfa();
279 client
280 .disable_totp(self.write_request(proto::v1::DisableTotpRequest {
281 user_id,
282 code: code.to_string(),
283 client_ip: client_ip.to_string(),
284 }))
285 .await
286 .map_err(KenAuthError::from_status)
287 .map(|response| response.into_inner())
288 }
289
290 pub async fn regenerate_recovery_codes(
291 &self,
292 user_id: i64,
293 password_hash: &str,
294 client_ip: &str,
295 ) -> Result<proto::v1::RegenerateRecoveryCodesResponse, KenAuthError> {
296 let mut client = self.mfa();
297 client
298 .regenerate_recovery_codes(
299 self.write_request(proto::v1::RegenerateRecoveryCodesRequest {
300 user_id,
301 password_hash: password_hash.to_string(),
302 client_ip: client_ip.to_string(),
303 }),
304 )
305 .await
306 .map_err(KenAuthError::from_status)
307 .map(|response| response.into_inner())
308 }
309
310 pub async fn list_access_keys(
311 &self,
312 user_id: i64,
313 include_revoked: bool,
314 page: proto::common::v1::PageRequest,
315 ) -> Result<proto::v1::ListAccessKeysResponse, KenAuthError> {
316 let mut client = self.access_key();
317 client
318 .list_access_keys(self.read_request(proto::v1::ListAccessKeysRequest {
319 user_id,
320 include_revoked,
321 page: Some(page),
322 }))
323 .await
324 .map_err(KenAuthError::from_status)
325 .map(|response| response.into_inner())
326 }
327
328 #[allow(clippy::too_many_arguments)]
329 pub async fn create_access_key(
330 &self,
331 user_id: i64,
332 name: &str,
333 description: &str,
334 scopes: Vec<i32>,
335 expires_in_secs: i64,
336 client_ip: &str,
337 ) -> Result<proto::v1::CreateAccessKeyResponse, KenAuthError> {
338 let mut client = self.access_key();
339 client
340 .create_access_key(self.write_request(proto::v1::CreateAccessKeyRequest {
341 user_id,
342 name: name.to_string(),
343 description: description.to_string(),
344 scopes,
345 expires_in_secs,
346 client_ip: client_ip.to_string(),
347 }))
348 .await
349 .map_err(KenAuthError::from_status)
350 .map(|response| response.into_inner())
351 }
352
353 pub async fn rotate_access_key(
354 &self,
355 user_id: i64,
356 access_key_id: &str,
357 client_ip: &str,
358 ) -> Result<proto::v1::RotateAccessKeyResponse, KenAuthError> {
359 let mut client = self.access_key();
360 client
361 .rotate_access_key(self.write_request(proto::v1::RotateAccessKeyRequest {
362 user_id,
363 access_key_id: access_key_id.to_string(),
364 client_ip: client_ip.to_string(),
365 }))
366 .await
367 .map_err(KenAuthError::from_status)
368 .map(|response| response.into_inner())
369 }
370
371 pub async fn revoke_access_key(
372 &self,
373 user_id: i64,
374 access_key_id: &str,
375 reason: &str,
376 ) -> Result<proto::v1::RevokeAccessKeyResponse, KenAuthError> {
377 let mut client = self.access_key();
378 client
379 .revoke_access_key(self.write_request(proto::v1::RevokeAccessKeyRequest {
380 user_id,
381 access_key_id: access_key_id.to_string(),
382 reason: reason.to_string(),
383 }))
384 .await
385 .map_err(KenAuthError::from_status)
386 .map(|response| response.into_inner())
387 }
388
389 pub async fn list_user_ssh_keys(
390 &self,
391 user_id: i64,
392 include_revoked: bool,
393 page: proto::common::v1::PageRequest,
394 ) -> Result<proto::v1::ListUserSshKeysResponse, KenAuthError> {
395 let mut client = self.ssh_key();
396 client
397 .list_user_ssh_keys(self.read_request(proto::v1::ListUserSshKeysRequest {
398 user_id,
399 include_revoked,
400 page: Some(page),
401 }))
402 .await
403 .map_err(KenAuthError::from_status)
404 .map(|response| response.into_inner())
405 }
406
407 pub async fn add_ssh_key(
408 &self,
409 user_id: i64,
410 name: &str,
411 public_key: &str,
412 description: &str,
413 client_ip: &str,
414 ) -> Result<proto::v1::AddSshKeyResponse, KenAuthError> {
415 let mut client = self.ssh_key();
416 client
417 .add_ssh_key(self.write_request(proto::v1::AddSshKeyRequest {
418 user_id,
419 name: name.to_string(),
420 public_key: public_key.to_string(),
421 description: description.to_string(),
422 client_ip: client_ip.to_string(),
423 }))
424 .await
425 .map_err(KenAuthError::from_status)
426 .map(|response| response.into_inner())
427 }
428
429 pub async fn revoke_ssh_key(
430 &self,
431 user_id: i64,
432 key_id: &str,
433 reason: &str,
434 bump_security_version: bool,
435 ) -> Result<proto::v1::RevokeSshKeyResponse, KenAuthError> {
436 let mut client = self.ssh_key();
437 client
438 .revoke_ssh_key(self.write_request(proto::v1::RevokeSshKeyRequest {
439 user_id,
440 key_id: key_id.to_string(),
441 reason: reason.to_string(),
442 bump_security_version,
443 }))
444 .await
445 .map_err(KenAuthError::from_status)
446 .map(|response| response.into_inner())
447 }
448
449 pub async fn list_user_sessions(
450 &self,
451 user_id: i64,
452 include_revoked: bool,
453 include_expired: bool,
454 page: proto::common::v1::PageRequest,
455 ) -> Result<proto::v1::ListUserSessionsResponse, KenAuthError> {
456 let mut client = self.session();
457 client
458 .list_user_sessions(self.read_request(proto::v1::ListUserSessionsRequest {
459 user_id,
460 include_revoked,
461 include_expired,
462 page: Some(page),
463 }))
464 .await
465 .map_err(KenAuthError::from_status)
466 .map(|response| response.into_inner())
467 }
468
469 pub async fn revoke_user_sessions(
470 &self,
471 user_id: i64,
472 except_session_id: &str,
473 reason: &str,
474 bump_security_version: bool,
475 ) -> Result<proto::v1::RevokeUserSessionsResponse, KenAuthError> {
476 let mut client = self.session();
477 client
478 .revoke_user_sessions(
479 self.write_request(proto::v1::RevokeUserSessionsRequest {
480 user_id,
481 except_session_id: except_session_id.to_string(),
482 reason: reason.to_string(),
483 bump_security_version,
484 }),
485 )
486 .await
487 .map_err(KenAuthError::from_status)
488 .map(|response| response.into_inner())
489 }
490
199491 pub fn read_request<T>(&self, message: T) -> Request<T> {
200492 let mut request = Request::new(message);
201493 request.set_timeout(self.read_timeout);
Modified lib/models/schema_extras.sql lib/models/schema_extras.sql +8 −4 View lib/models/schema_extras.sql
@@ -35,8 +35,6 @@
3535-- ---------------------------------------------------------------------------
3636CREATE INDEX IF NOT EXISTS idx_comment_parent_id ON comment (parent_id);
3737CREATE INDEX IF NOT EXISTS idx_git_commit_status_sha ON git_commit_status (repo_id, sha);
38CREATE INDEX IF NOT EXISTS idx_user_session_user_created ON user_session (user_id, created_at DESC);
39CREATE INDEX IF NOT EXISTS idx_user_session_expires ON user_session (expires_at);
4038
4139-- Single-column indexes for reserved project_id columns (parity with the
4240-- historical project module; columns are kept until project is restored).
@@ -226,8 +224,14 @@
226224CREATE INDEX IF NOT EXISTS idx_repo_webhook_delivery_due
227225 ON repo_webhook_delivery (status, next_retry_at);
228226
229UPDATE user_access_key SET scope = 'write' WHERE scope IN ('repo', 'all');
230UPDATE user_access_key SET scope = 'read' WHERE scope NOT IN ('read', 'write');
227-- user_access_key is owned by KenOAuth now; GitData only normalises legacy rows
228-- left in a database that still has the table.
229DO $$ BEGIN
230 IF to_regclass('user_access_key') IS NOT NULL THEN
231 UPDATE user_access_key SET scope = 'write' WHERE scope IN ('repo', 'all');
232 UPDATE user_access_key SET scope = 'read' WHERE scope NOT IN ('read', 'write');
233 END IF;
234END $$;
231235
232236-- ---------------------------------------------------------------------------
233237-- Pull Request domain (lib/pulls): list/filter lookups and inline-comment
Modified scripts/dev.ps1 scripts/dev.ps1 +25 −6 View scripts/dev.ps1
@@ -6,10 +6,23 @@
66$ErrorActionPreference = 'Stop'
77
88$Root = Split-Path -Parent $PSScriptRoot
9
10# 二进制目录跟随 CARGO_TARGET_DIR(cargo 约定:相对路径按调用时的工作目录解析,
11# just 在仓库根目录调用 cargo,所以这里也按 $Root 解析)。
12# $DevDir 保持在仓库内的 target\dev:日志 / pid / 本地仓库不随 target 目录搬家。
13function Resolve-TargetDir {
14 $raw = $env:CARGO_TARGET_DIR
15 if (-not $raw) { return (Join-Path $Root 'target') }
16 if ($raw -eq '~') { return $HOME }
17 if ($raw.StartsWith('~\') -or $raw.StartsWith('~/')) { return (Join-Path $HOME $raw.Substring(2)) }
18 if ([System.IO.Path]::IsPathRooted($raw)) { return $raw }
19 return (Join-Path $Root $raw)
20}
21$TargetDir = Resolve-TargetDir
922$DevDir = Join-Path $Root 'target\dev'
1023$LogDir = Join-Path $DevDir 'logs'
1124$PidDir = Join-Path $DevDir 'pids'
12$BinDir = Join-Path $Root 'target\debug'
25$BinDir = Join-Path $TargetDir 'debug'
1326$DevHostKey = Join-Path $DevDir 'ssh_host_ed25519'
1427
1528$Services = [ordered]@{
@@ -121,10 +134,13 @@
121134 if (-not $proc) { return $false }
122135 $path = ''
123136 try { $path = $proc.Path } catch { }
124 if ($path -and $path.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) { return $true }
137 if ($path) {
138 if ($path.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) { return $true }
139 if ($path.StartsWith($BinDir, [StringComparison]::OrdinalIgnoreCase)) { return $true }
140 }
125141 try {
126142 $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$ProcId" -ErrorAction SilentlyContinue).CommandLine
127 if ($cmd -and $cmd.Contains($Root)) { return $true }
143 if ($cmd -and ($cmd.Contains($Root) -or $cmd.Contains($BinDir))) { return $true }
128144 } catch { }
129145 return $false
130146}
@@ -155,7 +171,7 @@
155171 Ensure-Dir
156172 Set-ServiceEnv
157173 $exe = Join-Path $BinDir 'migrate.exe'
158 if (-not (Test-Path -LiteralPath $exe)) { throw '缺少 target\debug\migrate.exe,请先执行 just build' }
174 if (-not (Test-Path -LiteralPath $exe)) { throw "缺少 $exe,请先执行 just build" }
159175 Write-Info '[migrate] 同步数据库 schema ...'
160176 $proc = Start-Process -FilePath $exe -WorkingDirectory $Root -PassThru -Wait `
161177 -RedirectStandardOutput (Join-Path $LogDir 'migrate.log') `
@@ -167,6 +183,9 @@
167183
168184function Start-Daemon {
169185 param([string]$Name, [string]$Exe, [string]$Arguments, [string]$WorkingDirectory, [string]$Out, [string]$Err)
186 # 启动器由 cmd.exe 解释,必须用系统 ANSI 编码(本机为 GBK)写,
187 # 不能用 -Encoding ascii:仓库路径含非 ASCII(如 C:\工程)会被写成 ?,
188 # cmd.exe 找不到 exe,服务静默起不来。
170189 $launcher = Join-Path $PidDir "$Name.cmd"
171190 $lines = @(
172191 '@echo off'
@@ -174,7 +193,7 @@
174193 'cd /d "' + $WorkingDirectory + '"'
175194 '"' + $Exe + '" ' + $Arguments + ' 1> "' + $Out + '" 2> "' + $Err + '"'
176195 )
177 Set-Content -LiteralPath $launcher -Value $lines -Encoding ascii
196 Set-Content -LiteralPath $launcher -Value $lines -Encoding Default
178197
179198 $proc = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', ('"{0}"' -f $launcher)) `
180199 -WorkingDirectory $WorkingDirectory -WindowStyle Hidden -PassThru
@@ -305,7 +324,7 @@
305324 $ok = $false
306325 }
307326 }
308 Write-Info '== 后端二进制 =='
327 Write-Info "== 后端二进制 ($BinDir) =="
309328 foreach ($name in $Backends) {
310329 $exe = Join-Path $BinDir ($Services[$name].Exe + '.exe')
311330 if (Test-Path -LiteralPath $exe) { Write-Info (" {0,-12} OK" -f $name) } else { Write-Warn (" {0,-12} 未构建(just build)" -f $name); $ok = $false }
Modified scripts/dev.sh scripts/dev.sh +18 −3 View scripts/dev.sh
@@ -5,10 +5,24 @@
55set -u
66
77ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
9# 二进制目录跟随 CARGO_TARGET_DIR(cargo 约定:相对路径按调用时的工作目录解析,
10# just 在仓库根目录调用 cargo,所以这里也按 ROOT 解析)。
11# DEV_DIR 保持在仓库内的 target/dev:日志 / pid / 本地仓库不随 target 目录搬家。
12if [ -n "${CARGO_TARGET_DIR:-}" ]; then
13 case "$CARGO_TARGET_DIR" in
14 /*) TARGET_DIR="$CARGO_TARGET_DIR" ;;
15 "~") TARGET_DIR="$HOME" ;;
16 "~/"*) TARGET_DIR="$HOME/${CARGO_TARGET_DIR#\~/}" ;;
17 *) TARGET_DIR="$ROOT/$CARGO_TARGET_DIR" ;;
18 esac
19else
20 TARGET_DIR="$ROOT/target"
21fi
822DEV_DIR="$ROOT/target/dev"
923LOG_DIR="$DEV_DIR/logs"
1024PID_DIR="$DEV_DIR/pids"
11BIN_DIR="$ROOT/target/debug"
25BIN_DIR="$TARGET_DIR/debug"
1226DEV_HOST_KEY="$DEV_DIR/ssh_host_ed25519"
1327COMPOSE_FILE="$ROOT/docker-compose.dev.yml"
1428
@@ -254,7 +268,7 @@
254268 fi
255269 done
256270
257 info '== 后端二进制 =='
271 info "== 后端二进制 ($BIN_DIR) =="
258272 for name in $BACKENDS; do
259273 if [ -x "$BIN_DIR/$name" ]; then
260274 printf ' %-12s OK\n' "$name"
@@ -380,7 +394,7 @@
380394 ensure_path
381395 service_env || return 1
382396 if [ ! -x "$BIN_DIR/migrate" ]; then
383 fail ' 缺少 target/debug/migrate,请先执行 just build'
397 fail " 缺少 $BIN_DIR/migrate,请先执行 just build"
384398 return 1
385399 fi
386400 info '[migrate] 同步数据库 schema ...'
@@ -541,6 +555,7 @@
541555 esac
542556 fi
543557 case "$(ps -o command= -p "$owner" 2>/dev/null)" in
558 *"$BIN_DIR"*) kill_tree "$owner"; info "[$name] 清理残留监听 $port (pid $owner)"; return 0 ;;
544559 *"$ROOT"*) kill_tree "$owner"; info "[$name] 清理残留监听 $port (pid $owner)" ;;
545560 *) warn "[$name] 端口 $port 仍被 pid $owner 占用(非本仓库进程,未处理)" ;;
546561 esac
Modified web/src/app/routes/deferred-routes.tsx web/src/app/routes/deferred-routes.tsx +18 −0 View web/src/app/routes/deferred-routes.tsx
@@ -32,6 +32,24 @@
3232 return <RoutePending />
3333}
3434
35/**
36 * 注册表单也在 KenOAuth:把 `redirect` 原样转给后端,后端拼好跳转地址。
37 * 全页跳转是必须的(目标在另一个源),所以和 `LoginRedirectRoute` 一样放在
38 * 免检的路由层里。
39 */
40export function SignupRedirectRoute() {
41 useEffect(() => {
42 const redirect = new URLSearchParams(window.location.search).get('redirect')
43 const query = redirect ? `?redirect=${encodeURIComponent(sanitizeRedirect(redirect))}` : ''
44 window.location.replace(`/api/v1/auth/signup${query}`)
45 }, [])
46 return <RoutePending />
47}
48
49function sanitizeRedirect(raw: string): string {
50 return raw.startsWith('/') && !raw.startsWith('//') ? raw : '/app'
51}
52
3553export function SpaceInviteRoute() {
3654 return <DeferredRoute><SpaceInvitePage /></DeferredRoute>
3755}
Modified web/src/app/routes/router.tsx web/src/app/routes/router.tsx +2 −0 View web/src/app/routes/router.tsx
@@ -4,6 +4,7 @@
44 AgreementRoute,
55 LoginRedirectRoute,
66 NotFoundRoute,
7 SignupRedirectRoute,
78 SpaceInviteRoute,
89 WorkspaceRoute,
910} from './deferred-routes'
@@ -44,6 +45,7 @@
4445 },
4546 { path: '/auth', element: <Navigate to="/auth/login" replace /> },
4647 { path: '/auth/login', element: <LoginRedirectRoute /> },
48 { path: '/auth/signup', element: <SignupRedirectRoute /> },
4749 { path: '/spaces/invites/accept', element: <SpaceInviteRoute /> },
4850 { path: '/agreements/:key', element: <AgreementRoute /> },
4951 { path: '*', element: <NotFoundRoute /> },
Modified web/src/entities/chat/model/api.ts web/src/entities/chat/model/api.ts +7 −7 View web/src/entities/chat/model/api.ts
@@ -78,10 +78,10 @@
7878 Apis.chat.list_room_members({ pathParams: { room_id: roomId }, params: { limit: 200 } }),
7979 )
8080
81export const addRoomMembers = (roomId: string, userIds: string[]) =>
81export const addRoomMembers = (roomId: string, userIds: number[]) =>
8282 nothing(Apis.chat.add_room_members({ pathParams: { room_id: roomId }, data: { user_ids: userIds } }))
8383
84export const kickRoomMember = (roomId: string, userId: string) =>
84export const kickRoomMember = (roomId: string, userId: number) =>
8585 nothing(Apis.chat.kick_room_member({ pathParams: { room_id: roomId, user_id: userId } }))
8686
8787/** Per-viewer channel preferences (mute / favorite / hidden / notification level). */
@@ -101,7 +101,7 @@
101101
102102/* -------------------------------------------------------------------- dms */
103103
104export const openDm = (peerUserId: string) =>
104export const openDm = (peerUserId: number) =>
105105 unwrapApi<DmBrief>(
106106 Apis.chat.open_dm({
107107 pathParams: { limit: null, offset: null, sort: null },
@@ -119,7 +119,7 @@
119119 afterSeq?: number
120120 limit?: number
121121 threadId?: string
122 authorId?: string
122 authorId?: number
123123}
124124
125125/** Ascending by `seq`; `has_more` means *older* rows exist (before_seq path). */
@@ -340,7 +340,7 @@
340340export interface SearchQuery {
341341 q: string
342342 roomId?: string
343 authorId?: string
343 authorId?: number
344344 limit?: number
345345 offset?: number
346346}
@@ -373,7 +373,7 @@
373373}
374374
375375export interface SpaceMemberOption extends SpaceMemberRow {
376 user_id: string
376 user_id: number
377377}
378378
379379/**
@@ -400,7 +400,7 @@
400400 Apis.chat.list_space_presences({ pathParams: { space_name: spaceName } }),
401401 )
402402
403export const getUserPresence = (userId: string) =>
403export const getUserPresence = (userId: number) =>
404404 unwrapApi<Presence>(Apis.chat.get_presence({ pathParams: { user_id: userId } }))
405405
406406/* ------------------------------------------------------------------ emojis */
Modified web/src/entities/chat/model/chat.ts web/src/entities/chat/model/chat.ts +1 −1 View web/src/entities/chat/model/chat.ts
@@ -79,7 +79,7 @@
7979 /** Idempotency key sent as `nonce`: a retry can never duplicate the post. */
8080 nonce: string
8181 room_id: string
82 author_id: string
82 author_id: number
8383 content: string
8484 created_at: string
8585 reply_to_id?: string | null
Modified web/src/entities/chat/model/voice-api.ts web/src/entities/chat/model/voice-api.ts +5 −5 View web/src/entities/chat/model/voice-api.ts
@@ -70,23 +70,23 @@
7070 )
7171}
7272
73export function approveVoiceHand(roomId: string, userId: string): Promise<VoiceModeration> {
73export function approveVoiceHand(roomId: string, userId: number): Promise<VoiceModeration> {
7474 return unwrapApi(
7575 Apis.chat.approve_hand({ pathParams: { room_id: roomId, user_id: userId } }),
7676 )
7777}
7878
79export function denyVoiceHand(roomId: string, userId: string): Promise<VoiceModeration> {
79export function denyVoiceHand(roomId: string, userId: number): Promise<VoiceModeration> {
8080 return unwrapApi(Apis.chat.deny_hand({ pathParams: { room_id: roomId, user_id: userId } }))
8181}
8282
83export function revokeVoiceHand(roomId: string, userId: string): Promise<VoiceModeration> {
83export function revokeVoiceHand(roomId: string, userId: number): Promise<VoiceModeration> {
8484 return unwrapApi(Apis.chat.revoke_hand({ pathParams: { room_id: roomId, user_id: userId } }))
8585}
8686
8787export function moderateVoiceMute(
8888 roomId: string,
89 userId: string,
89 userId: number,
9090 muted: boolean,
9191): Promise<VoiceModeration> {
9292 return unwrapApi(
@@ -97,7 +97,7 @@
9797 )
9898}
9999
100export function kickVoiceParticipant(roomId: string, userId: string): Promise<VoiceModeration> {
100export function kickVoiceParticipant(roomId: string, userId: number): Promise<VoiceModeration> {
101101 return unwrapApi(
102102 Apis.chat.kick_participant({ pathParams: { room_id: roomId, user_id: userId } }),
103103 )
Modified web/src/entities/issue/model/issue.ts web/src/entities/issue/model/issue.ts +3 −3 View web/src/entities/issue/model/issue.ts
@@ -107,7 +107,7 @@
107107 /** `none` is a UI-only token meaning "no milestone" (filtered client-side). */
108108 milestoneIds?: string[]
109109 assigneeIds?: string[]
110 authorId?: string
110 authorId?: number
111111 sort?: IssueSort
112112 limit?: number
113113 offset?: number
@@ -147,7 +147,7 @@
147147 priority?: IssuePriority
148148 due_at?: string | null
149149 milestone_id?: string | null
150 assignee_ids?: string[]
150 assignee_ids?: number[]
151151 label_ids?: string[]
152152 /** Draft upload ids from `POST /issues/attachments` to link to the new issue. */
153153 attachment_ids?: string[]
@@ -165,6 +165,6 @@
165165 priority?: IssuePriority
166166 due_at?: string | null
167167 milestone_id?: string | null
168 assignee_ids?: string[]
168 assignee_ids?: number[]
169169 label_ids?: string[]
170170}
Modified web/src/entities/pull-request/model/api.ts web/src/entities/pull-request/model/api.ts +1 −1 View web/src/entities/pull-request/model/api.ts
@@ -262,7 +262,7 @@
262262 }),
263263 )
264264
265export const removeRequestedReviewer = (ref: PullRef, userId: string) =>
265export const removeRequestedReviewer = (ref: PullRef, userId: number) =>
266266 ignore(
267267 Apis.reviews.delete_requested_reviewer({ pathParams: { ...pathOf(ref), user_id: userId } }),
268268 )
Modified web/src/entities/repo/model/protection.ts web/src/entities/repo/model/protection.ts +3 −3 View web/src/entities/repo/model/protection.ts
@@ -73,7 +73,7 @@
7373 space: string,
7474 repo: string,
7575 policyId: string,
76 cmd: { kind: WhitelistKind; user_id?: string | null; group_id?: string | null },
76 cmd: { kind: WhitelistKind; user_id?: number | null; group_id?: string | null },
7777) =>
7878 unwrapApi<BranchWhitelistRow>(
7979 Apis.branch_protection.add_branch_whitelist({
@@ -94,7 +94,7 @@
9494 Apis.tag_protection.list_tag_whitelists({ pathParams: { space_name: space, repo_name: repo, policy_id: policyId } }),
9595 ).then(rows => rows ?? [])
9696
97export const addTagWhitelist = (space: string, repo: string, policyId: string, userId: string) =>
97export const addTagWhitelist = (space: string, repo: string, policyId: string, userId: number) =>
9898 unwrapApi<TagWhitelistRow>(
9999 Apis.tag_protection.add_tag_whitelist({
100100 pathParams: { space_name: space, repo_name: repo, policy_id: policyId },
@@ -102,7 +102,7 @@
102102 }),
103103 )
104104
105export const removeTagWhitelist = (space: string, repo: string, policyId: string, userId: string) =>
105export const removeTagWhitelist = (space: string, repo: string, policyId: string, userId: number) =>
106106 unwrapApi<null>(
107107 Apis.tag_protection.remove_tag_whitelist({
108108 pathParams: { space_name: space, repo_name: repo, policy_id: policyId, user_id: userId },
Modified web/src/features/chat/model/transcript.ts web/src/features/chat/model/transcript.ts +1 −1 View web/src/features/chat/model/transcript.ts
@@ -367,7 +367,7 @@
367367export function findEchoedPending(
368368 pending: readonly PendingMessage[],
369369 message: ChatMessage,
370 viewerId?: string,
370 viewerId?: number,
371371): PendingMessage | undefined {
372372 if (pending.length === 0 || message.author_type !== 'User') return undefined
373373 if (viewerId && message.author_id !== viewerId) return undefined
Modified web/src/features/chat/model/use-room-data.ts web/src/features/chat/model/use-room-data.ts +4 −4 View web/src/features/chat/model/use-room-data.ts
@@ -40,7 +40,7 @@
4040 /** Thread indexed by its root message, so a row knows its own topic. */
4141 threadOf: (messageId: string) => ThreadBrief | undefined
4242 /** Display name for a user id, resolved from the room's member list. */
43 nameOf: (userId: string | null | undefined) => string
43 nameOf: (userId: number | null | undefined) => string
4444}
4545
4646export interface UseRoomDataOptions {
@@ -146,7 +146,7 @@
146146 }, [threads.data])
147147
148148 const users = useMemo(() => {
149 const map = new Map<string, string>()
149 const map = new Map<number, string>()
150150 for (const member of members.data ?? []) {
151151 map.set(member.user.user_id, member.nick || member.user.display_name || member.user.username)
152152 }
@@ -156,9 +156,9 @@
156156 const threadOf = useCallback((messageId: string) => byRoot.get(messageId), [byRoot])
157157
158158 const nameOf = useCallback(
159 (userId: string | null | undefined) => {
159 (userId: number | null | undefined) => {
160160 if (!userId) return t('common.state.unknownUser')
161 return users.get(userId) ?? t('shell.chat.userFallback', { id: userId.slice(0, 8) })
161 return users.get(userId) ?? t('shell.chat.userFallback', { id: String(userId).slice(0, 8) })
162162 },
163163 [users],
164164 )
Modified web/src/features/chat/model/use-room-stream.ts web/src/features/chat/model/use-room-stream.ts +4 −4 View web/src/features/chat/model/use-room-stream.ts
@@ -55,7 +55,7 @@
5555 /** Set to read one thread instead of the room's main stream. */
5656 threadId?: string | null
5757 /** Local user id: identifies your own posts and ignores your typing echo. */
58 viewerId?: string
58 viewerId?: number
5959 /** Stops all traffic (no room selected, voice channel, outage gate open …). */
6060 enabled?: boolean
6161 /** The unread cursor moved: the shell refreshes its sidebar badges. */
@@ -82,7 +82,7 @@
8282 /** Connection lifecycle of the shared socket. */
8383 status: ReturnType<typeof useRoomWatch>
8484 /** Ids currently typing in this scope (your own echo already filtered out). */
85 typingUserIds: string[]
85 typingUserIds: number[]
8686 /** Seq the viewer last read; the「新消息」divider sits right after it. */
8787 lastReadSeq: number
8888 /** True while the viewport is pinned to the newest row. */
@@ -429,7 +429,7 @@
429429 localId: `local:${nonce}`,
430430 nonce,
431431 room_id: roomId,
432 author_id: viewerId ?? '',
432 author_id: viewerId ?? 0,
433433 content: body,
434434 created_at: new Date().toISOString(),
435435 reply_to_id: replyToId ?? null,
@@ -720,7 +720,7 @@
720720 return {
721721 state,
722722 status,
723 typingUserIds: typing.map(indicator => indicator.user_id),
723 typingUserIds: typing.map(indicator => Number(indicator.user_id)),
724724 lastReadSeq,
725725 atBottom,
726726 missedCount,
Modified web/src/features/chat/ui/voice/voice-avatar-tile.tsx web/src/features/chat/ui/voice/voice-avatar-tile.tsx +1 −1 View web/src/features/chat/ui/voice/voice-avatar-tile.tsx
@@ -94,7 +94,7 @@
9494 {/* Sized by CSS against the window, which the caller can resize. */}
9595 <UserAvatar
9696 user={{ display_name: name, username: name, avatar_url: avatarUrl ?? '' }}
97 id={participant.identity}
97 id={Number(participant.identity) || 0}
9898 className="voice-avatar-face"
9999 />
100100 <span className="voice-avatar-name">
Modified web/src/features/chat/ui/voice/voice-danmaku-history.tsx web/src/features/chat/ui/voice/voice-danmaku-history.tsx +2 −2 View web/src/features/chat/ui/voice/voice-danmaku-history.tsx
@@ -11,7 +11,7 @@
1111
1212export interface VoiceDanmakuHistoryProps {
1313 messages: ChatMessage[]
14 nameOf: (userId: string) => string
14 nameOf: (userId: number) => string
1515 hasOlder: boolean
1616 onLoadOlder: () => void
1717 flying: boolean
@@ -96,7 +96,7 @@
9696 {items.map(message => (
9797 <div key={String(message.id)} className="voice-danmaku-history-row">
9898 <span className="voice-danmaku-history-author">
99 {message.author?.display_name || nameOf(String(message.author_id))}
99 {message.author?.display_name || nameOf(message.author_id)}
100100 </span>
101101 <span className="voice-danmaku-history-text">{message.content.trim()}</span>
102102 <time className="voice-danmaku-history-time">{fmtRelative(message.created_at)}</time>
Modified web/src/features/chat/ui/voice/voice-danmaku.tsx web/src/features/chat/ui/voice/voice-danmaku.tsx +2 −2 View web/src/features/chat/ui/voice/voice-danmaku.tsx
@@ -20,7 +20,7 @@
2020 messages: ChatMessage[]
2121 /** True once the first page loaded: everything up to it is history. */
2222 seeded: boolean
23 nameOf: (userId: string) => string
23 nameOf: (userId: number) => string
2424}
2525
2626interface Bullet {
@@ -111,7 +111,7 @@
111111 if (now - born >= CROSS_MS) continue
112112 bullets.push({
113113 id,
114 author: message.author?.display_name || nameOf(String(message.author_id)),
114 author: message.author?.display_name || nameOf(message.author_id),
115115 text: message.content.trim(),
116116 lane: laneFor(id, lanes),
117117 })
Modified web/src/features/chat/ui/voice/voice-room-panel.tsx web/src/features/chat/ui/voice/voice-room-panel.tsx +5 −5 View web/src/features/chat/ui/voice/voice-room-panel.tsx
@@ -62,8 +62,8 @@
6262 danmakuSeeded: boolean
6363 danmakuHasOlder: boolean
6464 onLoadOlderDanmaku: () => void
65 nameOf: (userId: string) => string
66 viewerId?: string
65 nameOf: (userId: number) => string
66 viewerId?: number
6767 canModerate: boolean
6868}
6969
@@ -270,7 +270,7 @@
270270 }, [roomId, refreshHands, t])
271271
272272 const decide = useCallback(
273 async (userId: string, action: 'approve' | 'deny' | 'revoke') => {
273 async (userId: number, action: 'approve' | 'deny' | 'revoke') => {
274274 setError(null)
275275 try {
276276 if (action === 'approve') await approveVoiceHand(roomId, userId)
@@ -285,7 +285,7 @@
285285 )
286286
287287 const onMute = useCallback(
288 (userId: string, muted: boolean) => {
288 (userId: number, muted: boolean) => {
289289 setError(null)
290290 void moderateVoiceMute(roomId, userId, muted)
291291 .then(() => refreshHands())
@@ -297,7 +297,7 @@
297297 )
298298
299299 const onKick = useCallback(
300 (userId: string) => {
300 (userId: number) => {
301301 setError(null)
302302 void kickVoiceParticipant(roomId, userId).catch(cause =>
303303 setError(cause instanceof Error ? cause.message : t('chat.voice.handDecideFailed')),
Modified web/src/features/chat/ui/voice/voice-tiles.tsx web/src/features/chat/ui/voice/voice-tiles.tsx +12 −12 View web/src/features/chat/ui/voice/voice-tiles.tsx
@@ -44,15 +44,15 @@
4444
4545interface BoardProps {
4646 roomId: string
47 viewerId?: string
47 viewerId?: number
4848 canModerate: boolean
4949 layout: VoiceTileLayout
5050 /** Server-side moderator mutes, keyed by user id (identity === user id). */
5151 mutedIds: Set<string>
5252 /** Avatar per user id, from the presence snapshot (identity === user id). */
5353 avatarUrls: Map<string, string | null>
54 onMute: (userId: string, muted: boolean) => void
55 onKick: (userId: string) => void
54 onMute: (userId: number, muted: boolean) => void
55 onKick: (userId: number) => void
5656}
5757
5858/** Microphone reference per member, so a camera tile can still show "muted". */
@@ -89,11 +89,11 @@
8989}: {
9090 trackRef: TrackReferenceOrPlaceholder
9191 roomId: string
92 viewerId?: string
92 viewerId?: number
9393 canModerate: boolean
9494 moderatorMuted: boolean
95 onMute: (userId: string, muted: boolean) => void
96 onKick: (userId: string) => void
95 onMute: (userId: number, muted: boolean) => void
96 onKick: (userId: number) => void
9797}) {
9898 const { t } = useT()
9999 const room = useRoomContext()
@@ -101,7 +101,7 @@
101101 const identity = trackRef.participant.identity
102102 const pinned = usePinnedTracks(layoutContext)
103103 const isPinned = pinned.some(entry => entry.participant.identity === identity)
104 const isSelf = identity === viewerId
104 const isSelf = identity === String(viewerId ?? '')
105105 const remembered = useMemo(() => rememberedVolume(roomId, identity), [roomId, identity])
106106 const [volume, setVolume] = useState(remembered)
107107
@@ -149,11 +149,11 @@
149149 <button
150150 type="button"
151151 title={moderatorMuted ? t('chat.voice.forceUnmute') : t('chat.voice.forceMute')}
152 onClick={() => onMute(identity, !moderatorMuted)}
152 onClick={() => onMute(Number(identity) || 0, !moderatorMuted)}
153153 >
154154 {moderatorMuted ? <Mic className="h-3.5 w-3.5" /> : <MicOff className="h-3.5 w-3.5" />}
155155 </button>
156 <button type="button" title={t('chat.voice.kick')} onClick={() => onKick(identity)}>
156 <button type="button" title={t('chat.voice.kick')} onClick={() => onKick(Number(identity) || 0)}>
157157 <LogOut className="h-3.5 w-3.5" />
158158 </button>
159159 </>
@@ -298,7 +298,7 @@
298298}
299299
300300/** Join/leave chatter: the visual tile set cannot announce it to a screen reader. */
301function VoiceAnnouncements({ viewerId }: { viewerId?: string }) {
301function VoiceAnnouncements({ viewerId }: { viewerId?: number }) {
302302 const { t } = useT()
303303 const participants = useParticipants()
304304 const known = useRef<Map<string, string> | null>(null)
@@ -311,13 +311,13 @@
311311 return
312312 }
313313 for (const participant of participants) {
314 if (participant.identity === viewerId) continue
314 if (participant.identity === String(viewerId ?? '')) continue
315315 if (!known.current.has(participant.identity)) {
316316 setMessage(t('chat.voice.aria.joined', { name: participant.name || participant.identity }))
317317 }
318318 }
319319 for (const [identity, name] of known.current) {
320 if (!ids.includes(identity) && identity !== viewerId) {
320 if (!ids.includes(identity) && identity !== String(viewerId ?? '')) {
321321 setMessage(t('chat.voice.aria.left', { name }))
322322 }
323323 }
Modified web/src/features/chat/ui/chat-room.tsx web/src/features/chat/ui/chat-room.tsx +3 −3 View web/src/features/chat/ui/chat-room.tsx
@@ -104,7 +104,7 @@
104104export interface ChatRoomProps {
105105 spaceName: string
106106 roomId: string
107 viewerId?: string
107 viewerId?: number
108108 /** Sidebar row, used only until the authoritative room row arrives. */
109109 hint?: { name?: string; topic?: string | null; kind?: RoomKindName; peer?: UserInfo | null }
110110 /** Replaces the plain title heading with an interactive node (e.g. the asset search menu). */
@@ -321,7 +321,7 @@
321321 /* -------------------------------------------------------- interactions */
322322
323323 const nameOf = useCallback(
324 (userId: string | null | undefined) => {
324 (userId: number | null | undefined) => {
325325 if (!userId) return t('common.state.unknownUser')
326326 if (userId === viewerId) return t('shell.chat.me')
327327 const member = (data.members.data ?? []).find(row => row.user.user_id === userId)
@@ -330,7 +330,7 @@
330330 return (
331331 author?.display_name ||
332332 author?.username ||
333 t('shell.chat.userFallback', { id: userId.slice(0, 8) })
333 t('shell.chat.userFallback', { id: String(userId).slice(0, 8) })
334334 )
335335 },
336336 [data.members.data, stream.state.rows, t, viewerId],
Modified web/src/features/chat/ui/parts.tsx web/src/features/chat/ui/parts.tsx +7 −7 View web/src/features/chat/ui/parts.tsx
@@ -48,14 +48,14 @@
4848 className = '',
4949}: {
5050 user?: Pick<UserInfo, 'avatar_url' | 'display_name' | 'username'> | null
51 id: string
51 id: number
5252 size?: number
5353 className?: string
5454}) {
5555 // "this exact URL failed to load" is the state, so a new avatar needs no
5656 // effect to clear a previous failure.
5757 const [failedUrl, setFailedUrl] = useState<string | null>(null)
58 const label = user?.display_name || user?.username || id
58 const label = user?.display_name || user?.username || String(id)
5959 const src = user?.avatar_url && failedUrl !== user.avatar_url ? user.avatar_url : null
6060 if (src) {
6161 return (
@@ -78,8 +78,8 @@
7878 className={`grid shrink-0 place-content-center rounded-full font-semibold text-white ${className}`}
7979 style={
8080 className
81 ? { backgroundColor: colorFor(id || '0') }
82 : { width: size, height: size, backgroundColor: colorFor(id || '0'), fontSize: size * 0.4 }
81 ? { backgroundColor: colorFor(String(id || 0)) }
82 : { width: size, height: size, backgroundColor: colorFor(String(id || 0)), fontSize: size * 0.4 }
8383 }
8484 >
8585 {initialsOf(user)}
@@ -307,7 +307,7 @@
307307/** Inline markdown + @mention chips. Returns plain strings where nothing matched. */
308308function renderInline(
309309 text: string,
310 mentionIds: Set<string>,
310 mentionIds: Set<number>,
311311 isSelf: boolean,
312312 embeds: Map<string, MessageEmbed> = new Map(),
313313): ReactNode[] {
@@ -480,7 +480,7 @@
480480const MENTION = /@[\w.\-\u4e00-\u9fa5]+/g
481481
482482/** Replace bare @tokens inside text nodes with highlighted chips. */
483function mentionChips(nodes: ReactNode[], mentionIds: Set<string>, isSelf: boolean): ReactNode[] {
483function mentionChips(nodes: ReactNode[], mentionIds: Set<number>, isSelf: boolean): ReactNode[] {
484484 const out: ReactNode[] = []
485485 let key = 0
486486 for (const node of nodes) {
@@ -526,7 +526,7 @@
526526 suffix = null,
527527}: {
528528 message: ChatMessage
529 viewerId?: string
529 viewerId?: number
530530 compact?: boolean
531531 /** 追加在正文最后一段文本末尾的行内内容(“已编辑”等标记)。 */
532532 suffix?: ReactNode
Modified web/src/features/chat/ui/side-panel.tsx web/src/features/chat/ui/side-panel.tsx +11 −11 View web/src/features/chat/ui/side-panel.tsx
@@ -89,11 +89,11 @@
8989 thread: ThreadBrief
9090 /** Root message, when it is part of the loaded window (or fetched for the panel). */
9191 root?: ChatMessage
92 viewerId?: string
92 viewerId?: number
9393 spaceName?: string
9494 members: RoomMember[]
9595 customEmojis?: CustomEmoji[]
96 nameOf: (userId: string | null | undefined) => string
96 nameOf: (userId: number | null | undefined) => string
9797 interactions: MessageInteractions
9898 /** A message to reveal inside this thread (deep link from global search). */
9999 jumpId?: string
@@ -239,7 +239,7 @@
239239 onChanged,
240240}: {
241241 thread: ThreadBrief
242 viewerId?: string
242 viewerId?: number
243243 canManage: boolean
244244 onChanged: () => void
245245}) {
@@ -329,7 +329,7 @@
329329}: {
330330 pins: ChatMessage[]
331331 loading: boolean
332 nameOf: (userId: string | null | undefined) => string
332 nameOf: (userId: number | null | undefined) => string
333333 onJump: (messageId: string) => void
334334 onUnpin: (message: ChatMessage) => void
335335 onClose: () => void
@@ -423,7 +423,7 @@
423423 loading: boolean
424424 error?: unknown
425425 presences: Presence[]
426 viewerId?: string
426 viewerId?: number
427427 canManage: boolean
428428 onDm: (user: UserInfo) => void
429429 onChanged: () => void
@@ -432,7 +432,7 @@
432432 const { t, tc } = useT()
433433 const [adding, setAdding] = useState(false)
434434 const online = useMemo(() => {
435 const map = new Map<string, Presence>()
435 const map = new Map<number, Presence>()
436436 for (const presence of presences) map.set(presence.user_id, presence)
437437 return map
438438 }, [presences])
@@ -588,12 +588,12 @@
588588}: {
589589 roomId: string
590590 spaceName: string
591 existing: Set<string>
591 existing: Set<number>
592592 onDone: () => void
593593 onCancel: () => void
594594}) {
595595 const { t } = useT()
596 const [picked, setPicked] = useState<string[]>([])
596 const [picked, setPicked] = useState<number[]>([])
597597 const [busy, setBusy] = useState(false)
598598 const [error, setError] = useState<string | null>(null)
599599 const people = useAsync(() => listSpaceMembers(spaceName), [spaceName], {
@@ -625,7 +625,7 @@
625625 )
626626 }
627627 />
628 <span className="truncate">{member.user?.display_name || member.user?.username || member.user_id}</span>
628 <span className="truncate">{member.user?.display_name || member.user?.username || String(member.user_id)}</span>
629629 <span className="min-w-0 flex-1 truncate text-[11px] text-shell-ink-muted">
630630 @{member.user?.username}
631631 </span>
@@ -697,7 +697,7 @@
697697 onToggleScope: (value: boolean) => void
698698 onJump: (hit: SearchHit) => void
699699 onClose: () => void
700 nameOf: (userId: string | null | undefined) => string
700 nameOf: (userId: number | null | undefined) => string
701701 roomLabel: string
702702}) {
703703 const { t } = useT()
@@ -789,7 +789,7 @@
789789}: {
790790 threads: ThreadBrief[]
791791 loading: boolean
792 nameOf: (userId: string | null | undefined) => string
792 nameOf: (userId: number | null | undefined) => string
793793 onOpen: (thread: ThreadBrief) => void
794794 onClose: () => void
795795}) {
Modified web/src/features/chat/ui/system-lines.ts web/src/features/chat/ui/system-lines.ts +1 −1 View web/src/features/chat/ui/system-lines.ts
@@ -9,7 +9,7 @@
99
1010export function systemLine(
1111 message: ChatMessage,
12 nameOf: (userId: string | null | undefined) => string,
12 nameOf: (userId: number | null | undefined) => string,
1313): string {
1414 const who = nameOf(message.author_id)
1515 if (message.author_type !== 'User') {
Modified web/src/features/chat/ui/transcript.tsx web/src/features/chat/ui/transcript.tsx +7 −7 View web/src/features/chat/ui/transcript.tsx
@@ -84,12 +84,12 @@
8484 /** False for a read-only room: no reply/thread composer at all. */
8585 canParticipate: boolean
8686 threadOf: (messageId: string) => ThreadBrief | undefined
87 nameOf: (userId: string | null | undefined) => string
87 nameOf: (userId: number | null | undefined) => string
8888}
8989
9090export interface TranscriptProps {
9191 state: TranscriptState
92 viewerId?: string
92 viewerId?: number
9393 lastReadSeq: number
9494 missedCount: number
9595 listRef: React.RefObject<HTMLDivElement | null>
@@ -341,7 +341,7 @@
341341 nameOf,
342342}: {
343343 message: ChatMessage
344 nameOf: (userId: string | null | undefined) => string
344 nameOf: (userId: number | null | undefined) => string
345345}) {
346346 return (
347347 <div className="flex items-start gap-2 px-3 py-0.5 text-[12.5px] text-shell-ink-muted">
@@ -369,7 +369,7 @@
369369}: {
370370 message: ChatMessage
371371 grouped: boolean
372 viewerId?: string
372 viewerId?: number
373373 state: TranscriptState
374374 interactions: MessageInteractions
375375 editing: boolean
@@ -533,7 +533,7 @@
533533 onJump,
534534}: {
535535 message: ChatMessage
536 nameOf: (userId: string | null | undefined) => string
536 nameOf: (userId: number | null | undefined) => string
537537 onJump: () => void
538538}) {
539539 const { t } = useT()
@@ -696,7 +696,7 @@
696696 onDismiss,
697697}: {
698698 item: PendingMessage
699 viewerId?: string
699 viewerId?: number
700700 onPreview: (attachment: Attachment) => void
701701 onRetry: (localId: string) => void
702702 onDismiss: (localId: string) => void
@@ -705,7 +705,7 @@
705705 const failed = item.state === 'failed'
706706 return (
707707 <div className="chat-pending-row flex gap-3 px-3 pb-0.5 pt-2.5">
708 <UserAvatar id={viewerId ?? ''} size={40} />
708 <UserAvatar id={viewerId ?? 0} size={40} />
709709 <div className="min-w-0 flex-1">
710710 <div className="flex items-baseline gap-2">
711711 <span className="text-[15px] font-semibold">{t('chat.transcript.mine')}</span>
Modified web/src/features/issue-board/ui/board-filters.tsx web/src/features/issue-board/ui/board-filters.tsx +2 −2 View web/src/features/issue-board/ui/board-filters.tsx
@@ -278,7 +278,7 @@
278278 <FilterSection
279279 title={t('issue.board.filter.assignee')}
280280 options={members.map(user => ({
281 value: user.user_id,
281 value: String(user.user_id),
282282 label: user.display_name || user.username,
283283 hint: user.username ? `@${user.username}` : undefined,
284284 }))}
@@ -344,7 +344,7 @@
344344 view={view}
345345 labelName={id => labels.find(label => label.id === id)?.name}
346346 milestoneName={id => milestones.find(item => item.id === id)?.name}
347 memberName={id => members.find(user => user.user_id === id)?.display_name}
347 memberName={id => members.find(user => String(user.user_id) === id)?.display_name}
348348 onFilter={onFilter}
349349 />
350350 <button type="button" className="board-filter-clear" tabIndex={filtered ? undefined : -1} onClick={onClear}>
Modified web/src/features/issue-board/ui/issue-detail.tsx web/src/features/issue-board/ui/issue-detail.tsx +5 −5 View web/src/features/issue-board/ui/issue-detail.tsx
@@ -109,7 +109,7 @@
109109 /** Bumped by the page after a board-level change; refetches this issue. */
110110 reloadKey: number
111111 data: BoardData
112 viewerId?: string
112 viewerId?: number
113113 onBack: () => void
114114 onChanged: () => void
115115 /** Open the edit dialog prefilled with this issue. */
@@ -419,7 +419,7 @@
419419 memberName={id =>
420420 (data.members.data?.items ?? [])
421421 .map(item => item.user)
422 .find(user => user?.user_id === id)?.display_name ?? undefined
422 .find(user => String(user?.user_id) === id)?.display_name ?? undefined
423423 }
424424 />
425425 ),
@@ -828,8 +828,8 @@
828828 onChange,
829829}: {
830830 members: { user?: import('@/entities/issue').UserDto | null }[]
831 selected: string[]
832 onChange: (ids: string[]) => void
831 selected: number[]
832 onChange: (ids: number[]) => void
833833}) {
834834 const { t } = useT()
835835 const [open, setOpen] = useState(false)
@@ -1044,7 +1044,7 @@
10441044 node: import('@/entities/issue').CommentNode
10451045 boardRef: BoardRef
10461046 issueId: string
1047 viewerId?: string
1047 viewerId?: number
10481048 onReload: () => void
10491049}) {
10501050 const comment: IssueComment = node.comment
Modified web/src/features/issue-board/ui/issue-form-modal.tsx web/src/features/issue-board/ui/issue-form-modal.tsx +5 −5 View web/src/features/issue-board/ui/issue-form-modal.tsx
@@ -80,7 +80,7 @@
8080 const [milestoneId, setMilestoneId] = useState('')
8181 const [due, setDue] = useState('')
8282 const [labelIds, setLabelIds] = useState<string[]>([])
83 const [assigneeIds, setAssigneeIds] = useState<string[]>([])
83 const [assigneeIds, setAssigneeIds] = useState<number[]>([])
8484 const [drafts, setDrafts] = useState<DraftAttachment[]>([])
8585 const [preview, setPreview] = useState(false)
8686 const [busy, setBusy] = useState(false)
@@ -418,7 +418,7 @@
418418}
419419
420420/** 多选 chip:收起时是一枚胶囊,展开后在上方列出可勾选项。 */
421function PickerChip({
421function PickerChip<Id extends string | number>({
422422 icon,
423423 label,
424424 options,
@@ -429,9 +429,9 @@
429429}: {
430430 icon: ReactNode
431431 label: string
432 options: Array<{ id: string; name: string; color?: string; user?: UserDto }>
433 selected: string[]
434 onToggle: (id: string) => void
432 options: Array<{ id: Id; name: string; color?: string; user?: UserDto }>
433 selected: readonly Id[]
434 onToggle: (id: Id) => void
435435 empty: string
436436 align?: 'left' | 'right'
437437}) {
Modified web/src/features/pull-request/ui/pull-detail.tsx web/src/features/pull-request/ui/pull-detail.tsx +2 −2 View web/src/features/pull-request/ui/pull-detail.tsx
@@ -552,7 +552,7 @@
552552 type="button"
553553 aria-label={t('pull.detail.remove', { name: userName(row.user) })}
554554 className="text-shell-ink-muted hover:text-[#b42318]"
555 onClick={() => void removeRequestedReviewer(refObj, row.user?.user_id ?? '').then(onChanged).catch(() => undefined)}
555 onClick={() => void removeRequestedReviewer(refObj, row.user?.user_id ?? 0).then(onChanged).catch(() => undefined)}
556556 >
557557 ×
558558 </button>
@@ -608,7 +608,7 @@
608608 type="button"
609609 aria-label={t('pull.detail.remove', { name: userName(row.user) })}
610610 className="text-shell-ink-muted hover:text-[#b42318]"
611 onClick={() => void removeAssignee(refObj, row.user?.username ?? row.user?.user_id ?? '').then(onChanged).catch(() => undefined)}
611 onClick={() => void removeAssignee(refObj, row.user?.username ?? String(row.user?.user_id ?? '')).then(onChanged).catch(() => undefined)}
612612 >
613613 ×
614614 </button>
Modified web/src/features/repo-settings/ui/sections/protection-settings.tsx web/src/features/repo-settings/ui/sections/protection-settings.tsx +6 −5 View web/src/features/repo-settings/ui/sections/protection-settings.tsx
@@ -182,7 +182,7 @@
182182 const { t } = useT()
183183 const [tick, setTick] = useState(0)
184184 const [kind, setKind] = useState<WhitelistKind>('Push')
185 const [userId, setUserId] = useState('')
185 const [userId, setUserId] = useState(0)
186186 const [error, setError] = useState<string | null>(null)
187187 const lists = useAsync(() => listBranchWhitelists(space, repo, policy.id), [space, repo, policy.id, tick])
188188 const users = useAsync(() => listUserOptions(space), [space])
@@ -194,7 +194,7 @@
194194 setError(null)
195195 try {
196196 await addBranchWhitelist(space, repo, policy.id, { kind, user_id: userId })
197 setUserId('')
197 setUserId(0)
198198 setTick(n => n + 1)
199199 } catch (f) {
200200 setError(f instanceof Error ? f.message : t('repo.protection.addFailed'))
@@ -220,7 +220,7 @@
220220 {KIND_LABEL[value]}
221221 </label>
222222 ))}
223 <select value={userId} onChange={event => setUserId(event.target.value)} className={`h-8 flex-1 min-w-[140px] ${modalInputClass}`}>
223 <select value={userId} onChange={event => setUserId(Number(event.target.value))} className={`h-8 flex-1 min-w-[140px] ${modalInputClass}`}>
224224 <option value="">{t('repo.protection.selectUser')}</option>
225225 {options.map(user => (
226226 <option key={user.user_id} value={user.user_id}>{t('repo.protection.userOption', { name: user.display_name || user.username, username: user.username })}</option>
@@ -254,7 +254,7 @@
254254 const { t } = useT()
255255 const [policyId, setPolicyId] = useState('')
256256 const [tick, setTick] = useState(0)
257 const [userId, setUserId] = useState('')
257 const [userId, setUserId] = useState(0)
258258 const [error, setError] = useState<string | null>(null)
259259 const rowsState = useAsync(
260260 () => (policyId ? listTagWhitelists(space, repo, policyId) : Promise.resolve([] as TagWhitelistRow[])),
@@ -268,6 +268,7 @@
268268 setError(null)
269269 try {
270270 await addTagWhitelist(space, repo, policyId, userId)
271 setUserId(0)
271272 setTick(n => n + 1)
272273 } catch (f) {
273274 setError(f instanceof Error ? f.message : t('repo.protection.addFailed'))
@@ -290,7 +291,7 @@
290291 {policyId ? (
291292 <div className="mt-2">
292293 <div className="flex flex-wrap items-center gap-2 text-[12px]">
293 <select value={userId} onChange={event => setUserId(event.target.value)} className={`h-8 flex-1 min-w-[140px] ${modalInputClass}`}>
294 <select value={userId} onChange={event => setUserId(Number(event.target.value))} className={`h-8 flex-1 min-w-[140px] ${modalInputClass}`}>
294295 <option value="">{t('repo.protection.selectUser')}</option>
295296 {(users.data ?? []).map(user => (
296297 <option key={user.user_id} value={user.user_id}>{t('repo.protection.userOption', { name: user.display_name || user.username, username: user.username })}</option>
Modified web/src/features/space-management/model/space-admin.ts web/src/features/space-management/model/space-admin.ts +9 −9 View web/src/features/space-management/model/space-admin.ts
@@ -105,7 +105,7 @@
105105 }),
106106 )
107107
108export const getSpaceMember = (spaceName: string, userId: string) =>
108export const getSpaceMember = (spaceName: string, userId: number) =>
109109 unwrapApi<SpaceMember>(
110110 Apis.space.get_member({ pathParams: { space_name: spaceName, user_id: userId } }),
111111 )
@@ -113,7 +113,7 @@
113113export const addSpaceMember = (spaceName: string, cmd: AddMemberCmd) =>
114114 unwrapApi<SpaceMember>(Apis.space.add_member({ pathParams: { space_name: spaceName }, data: cmd }))
115115
116export const updateSpaceMember = (spaceName: string, userId: string, cmd: UpdateMemberCmd) =>
116export const updateSpaceMember = (spaceName: string, userId: number, cmd: UpdateMemberCmd) =>
117117 unwrapApi<SpaceMember>(
118118 Apis.space.update_member({ pathParams: { space_name: spaceName, user_id: userId }, data: cmd }),
119119 )
@@ -122,7 +122,7 @@
122122export const updateMySpaceMembership = (spaceName: string, cmd: UpdateMemberCmd) =>
123123 unwrapApi<SpaceMember>(Apis.space.update_self_member({ pathParams: { space_name: spaceName }, data: cmd }))
124124
125export const removeSpaceMember = (spaceName: string, userId: string) =>
125export const removeSpaceMember = (spaceName: string, userId: number) =>
126126 unwrapApi<null>(
127127 Apis.space.remove_member({ pathParams: { space_name: spaceName, user_id: userId } }),
128128 )
@@ -151,7 +151,7 @@
151151 )
152152
153153/** Group membership is a set write, not a field on the member row. */
154export const assignSpaceGroup = (spaceName: string, userId: string, groupId: string) =>
154export const assignSpaceGroup = (spaceName: string, userId: number, groupId: string) =>
155155 unwrapApi<null>(
156156 Apis.space.assign_group({
157157 pathParams: { space_name: spaceName, user_id: userId },
@@ -159,7 +159,7 @@
159159 }),
160160 )
161161
162export const unassignSpaceGroup = (spaceName: string, userId: string, groupId: string) =>
162export const unassignSpaceGroup = (spaceName: string, userId: number, groupId: string) =>
163163 unwrapApi<null>(
164164 Apis.space.unassign_group({
165165 pathParams: { space_name: spaceName, user_id: userId, group_id: groupId },
@@ -266,12 +266,12 @@
266266export const banSpaceUser = (spaceName: string, cmd: BanUserCmd) =>
267267 unwrapApi<SpaceBan>(Apis.space.ban_user({ pathParams: { space_name: spaceName }, data: cmd }))
268268
269export const updateSpaceBan = (spaceName: string, userId: string, cmd: UpdateBanCmd) =>
269export const updateSpaceBan = (spaceName: string, userId: number, cmd: UpdateBanCmd) =>
270270 unwrapApi<SpaceBan>(
271271 Apis.space.update_ban({ pathParams: { space_name: spaceName, user_id: userId }, data: cmd }),
272272 )
273273
274export const unbanSpaceUser = (spaceName: string, userId: string) =>
274export const unbanSpaceUser = (spaceName: string, userId: number) =>
275275 unwrapApi<null>(Apis.space.unban_user({ pathParams: { space_name: spaceName, user_id: userId } }))
276276
277277/* ----------------------------------------------- channel permission edits */
@@ -309,7 +309,7 @@
309309export const setDirUserOverwrite = (
310310 spaceName: string,
311311 dirId: string,
312 userId: string,
312 userId: number,
313313 cmd: { allow_mask: number; deny_mask: number },
314314) =>
315315 unwrapApi<DirUserOverwrite>(
@@ -319,7 +319,7 @@
319319 }),
320320 )
321321
322export const removeDirUserOverwrite = (spaceName: string, dirId: string, userId: string) =>
322export const removeDirUserOverwrite = (spaceName: string, dirId: string, userId: number) =>
323323 unwrapApi<null>(
324324 Apis.space.remove_dir_user_overwrite({
325325 pathParams: { space_name: spaceName, dir_id: dirId, user_id: userId },
Modified web/src/features/space-management/ui/space-settings/channel-permissions-section.tsx web/src/features/space-management/ui/space-settings/channel-permissions-section.tsx +6 −6 View web/src/features/space-management/ui/space-settings/channel-permissions-section.tsx
@@ -94,19 +94,19 @@
9494 )
9595 const refresh = () => setTick(t => t + 1)
9696
97 const setStance = (kind: 'group' | 'user', id: string, stance: Stance) =>
97 const setStance = (kind: 'group' | 'user', id: string | number, stance: Stance) =>
9898 void run(async () => {
9999 if (!active) return
100100 if (stance === 'inherit') {
101 if (kind === 'group') await removeDirGroupOverwrite(spaceName, active, id)
102 else await removeDirUserOverwrite(spaceName, active, id)
101 if (kind === 'group') await removeDirGroupOverwrite(spaceName, active, String(id))
102 else await removeDirUserOverwrite(spaceName, active, Number(id))
103103 } else {
104104 const mask = {
105105 allow_mask: stance === 'allow' ? PERM_DIR_VISIBLE : 0,
106106 deny_mask: stance === 'deny' ? PERM_DIR_VISIBLE : 0,
107107 }
108 if (kind === 'group') await setDirGroupOverwrite(spaceName, active, id, mask)
109 else await setDirUserOverwrite(spaceName, active, id, mask)
108 if (kind === 'group') await setDirGroupOverwrite(spaceName, active, String(id), mask)
109 else await setDirUserOverwrite(spaceName, active, Number(id), mask)
110110 }
111111 setRevision(r => r + 1)
112112 }, t('space.channelPermissions.saved'))
@@ -345,7 +345,7 @@
345345 dirId: string
346346 busy: boolean
347347 disabled: boolean
348 current: { user_id: string }[]
348 current: { user_id: number }[]
349349 onApplied: () => void
350350}) {
351351 const { t } = useT()
Modified web/src/features/space-management/ui/space-settings/props.ts web/src/features/space-management/ui/space-settings/props.ts +1 −1 View web/src/features/space-management/ui/space-settings/props.ts
@@ -6,7 +6,7 @@
66 /** Authoritative `GET /spaces/{name}` row, loaded once by the modal. */
77 space: SpaceDto
88 perms: Set<string>
9 viewerId?: string
9 viewerId?: number
1010 /** `PATCH/DELETE /spaces/{name}` are owner-only on the server. */
1111 isOwner: boolean
1212 /** The space row changed (name, icon, …) — refetch the rail and header. */
Modified web/src/features/space-management/ui/space-settings/roles-section.tsx web/src/features/space-management/ui/space-settings/roles-section.tsx +5 −5 View web/src/features/space-management/ui/space-settings/roles-section.tsx
@@ -225,7 +225,7 @@
225225}: {
226226 spaceName: string
227227 group: SpaceGroup
228 memberOptions: { value: string; label: string }[]
228 memberOptions: { value: number; label: string }[]
229229 onSaved: () => void
230230}) {
231231 const [name, setName] = useState(group.name)
@@ -234,7 +234,7 @@
234234 const [position, setPosition] = useState(String(group.position))
235235 const [perms, setPerms] = useState<Set<string>>(() => new Set(group.permissions))
236236 const [query, setQuery] = useState('')
237 const [target, setTarget] = useState('')
237 const [target, setTarget] = useState(0)
238238 const { t } = useT()
239239 const { run, busy, notice, error } = useSave()
240240 const dirty =
@@ -366,7 +366,7 @@
366366 <select
367367 className={`${inputCls} h-9 w-auto max-w-[220px]`}
368368 value={target}
369 onChange={e => setTarget(e.target.value)}
369 onChange={e => setTarget(Number(e.target.value))}
370370 aria-label={t('space.roles.selectMember')}
371371 >
372372 <option value="">{t('space.roles.selectMemberPlaceholder')}</option>
@@ -383,7 +383,7 @@
383383 onClick={() =>
384384 void run(async () => {
385385 await assignSpaceGroup(spaceName, target, group.id)
386 setTarget('')
386 setTarget(0)
387387 onSaved()
388388 }, t('space.roles.granted'))
389389 }
@@ -397,7 +397,7 @@
397397 onClick={() =>
398398 void run(async () => {
399399 await unassignSpaceGroup(spaceName, target, group.id)
400 setTarget('')
400 setTarget(0)
401401 onSaved()
402402 }, t('space.roles.revoked'))
403403 }
Modified web/src/features/space-management/ui/space-header.tsx web/src/features/space-management/ui/space-header.tsx +1 −1 View web/src/features/space-management/ui/space-header.tsx
@@ -47,7 +47,7 @@
4747
4848export interface SpaceHeaderProps {
4949 mySpace: MySpace
50 viewerId?: string
50 viewerId?: number
5151 /** A room/repo/board was created — sidebar lists should refetch. */
5252 onListsChanged: () => void
5353 /** Opened 社区设置: the overlay lives in the shell, outside the sidebar. */
Modified web/src/features/space-management/ui/space-settings-modal.tsx web/src/features/space-management/ui/space-settings-modal.tsx +1 −1 View web/src/features/space-management/ui/space-settings-modal.tsx
@@ -23,7 +23,7 @@
2323 open: boolean
2424 onClose: () => void
2525 mySpace: MySpace
26 viewerId?: string
26 viewerId?: number
2727 /** Deep-linked section to select on open (e.g. from a join-request notice). */
2828 initialSection?: string
2929 /** The space row changed — the rail tile and header need the fresh name/icon. */
Modified web/src/features/user-settings/model/api.ts web/src/features/user-settings/model/api.ts +0 −19 View web/src/features/user-settings/model/api.ts
@@ -16,11 +16,9 @@
1616import type {
1717 AccessKey,
1818 AccessKeyPage,
19 AddEmailResult,
2019 AvatarInfo,
2120 CountResult,
2221 DeviceRow,
23 EmailRow,
2422 RecoveryCodes,
2523 SessionPage,
2624 SshKey,
@@ -84,23 +82,6 @@
8482export const revokeAllSessions = () =>
8583 unwrapApi<CountResult>(Apis.user.revoke_all_sessions())
8684export const listDevices = () => unwrapApi<DeviceRow[]>(Apis.account.list_devices({ params: {} }))
87export const renameDevice = (deviceId: string, name: string) =>
88 unwrapApi<DeviceRow>(
89 Apis.account.update_device({ pathParams: { device_id: deviceId }, data: { device_name: name } }),
90 )
91export const deleteDevice = (deviceId: string) =>
92 unwrapApi<null>(Apis.account.delete_device({ pathParams: { device_id: deviceId } }))
93
94/* ---- emails --------------------------------------------------------------- */
95export const listEmails = () => unwrapApi<EmailRow[]>(Apis.user.list_emails())
96export const addEmail = (email: string) =>
97 unwrapApi<AddEmailResult>(Apis.user.add_email({ data: { email } }))
98export const verifyAddEmail = (email: string, code: string) =>
99 unwrapApi<EmailRow>(Apis.user.verify_add_email({ data: { email, code } }))
100export const deleteEmail = (emailId: string) =>
101 unwrapApi<null>(Apis.user.delete_email({ pathParams: { email_id: emailId } }))
102export const setPrimaryEmail = (emailId: string) =>
103 unwrapApi<EmailRow>(Apis.user.set_primary_email({ pathParams: { email_id: emailId } }))
10485
10586/* ---- preferences ---------------------------------------------------------- */
10687/** Canonical home of the preference endpoints; features/space-management keeps
Modified web/src/features/user-settings/model/user-settings.ts web/src/features/user-settings/model/user-settings.ts +0 −4 View web/src/features/user-settings/model/user-settings.ts
@@ -4,7 +4,6 @@
44 * feature may not import types from a page slice.
55 */
66import type {
7 AppResponseData_AddEmailResp,
87 AppResponseData_AvatarDto,
98 AppResponseData_CountResp,
109 AppResponseData_MeResp,
@@ -17,7 +16,6 @@
1716 AppResponseData_UserPreferencesDto,
1817 AppResponseData_UserPublic,
1918 AppResponseData_Vec_DeviceDto,
20 AppResponseData_Vec_EmailDto,
2119 AppResponseData_Vec_SuspensionDto,
2220} from '@/shared/api'
2321
@@ -28,9 +26,7 @@
2826export type RecoveryCodes = NonNullable<AppResponseData_RecoveryCodesResp['data']>
2927export type TotpSetup = NonNullable<AppResponseData_TotpSetupResp['data']>
3028export type TotpStatus = NonNullable<AppResponseData_TotpStatusResp['data']>
31export type AddEmailResult = NonNullable<AppResponseData_AddEmailResp['data']>
3229export type DeviceRow = NonNullable<AppResponseData_Vec_DeviceDto['data']>[number]
33export type EmailRow = NonNullable<AppResponseData_Vec_EmailDto['data']>[number]
3430export type SuspensionRow = NonNullable<AppResponseData_Vec_SuspensionDto['data']>[number]
3531export type SessionPage = NonNullable<AppResponseData_Page_SessionDto['data']>
3632export type AccessKeyPage = NonNullable<AppResponseData_Page_AccessKeyDto['data']>
Modified web/src/features/user-settings/ui/sections/account-section.tsx web/src/features/user-settings/ui/sections/account-section.tsx +1 −1 View web/src/features/user-settings/ui/sections/account-section.tsx
@@ -113,7 +113,7 @@
113113 <dl className="mb-8 space-y-1.5 text-[12.5px] text-shell-ink-muted">
114114 <div className="flex gap-2">
115115 <dt>{t('settings.account.userId')}</dt>
116 <dd className="flex items-center gap-2 font-mono text-shell-ink">{me.user.user_id} <CopyButton text={me.user.user_id} /></dd>
116 <dd className="flex items-center gap-2 font-mono text-shell-ink">{me.user.user_id} <CopyButton text={String(me.user.user_id)} /></dd>
117117 </div>
118118 <div className="flex gap-2">
119119 <dt>{t('settings.account.createdAt')}</dt>
Deleted web/src/features/user-settings/ui/sections/emails-section.tsx web/src/features/user-settings/ui/sections/emails-section.tsx +0 −49
@@ -1,49 +0,0 @@
1import { useState } from 'react'
2import { useT } from '@/shared/i18n'
3import { Busy, Button, SaveBar, SectionTitle, inputCls, useSave } from '@/shared/ui'
4import { settingsKey, useAsync } from '@/shared/lib'
5import * as api from '../../model/api'
6
7export function EmailsSection() {
8 const { t } = useT()
9 const [tick, setTick] = useState(0)
10 const emails = useAsync(api.listEmails, [tick], { cacheKey: settingsKey('emails') })
11 const { busy, notice, error, run } = useSave()
12 const [newEmail, setNewEmail] = useState('')
13 const [pending, setPending] = useState<string | null>(null)
14 const [verifyCode, setVerifyCode] = useState('')
15
16 return (
17 <section>
18 <SectionTitle title={t('userSettings.emails.title')} sub={t('userSettings.emails.sub')} />
19 {emails.loading ? <Busy /> : (
20 <div className="mb-6 space-y-2">
21 {(emails.data ?? []).map(email => (
22 <div key={email.id} className="settings-card flex flex-wrap items-center gap-3 px-4 py-3">
23 <span className="text-[13.5px] font-semibold">{email.email}</span>
24 {email.is_primary ? <span className="rounded-full bg-shell-active px-2 py-0.5 text-[11px]">{t('userSettings.emails.primary')}</span> : null}
25 {email.is_activated ? <span className="rounded-full bg-[#23a55a]/15 px-2 py-0.5 text-[11px] text-[#23a55a]">{t('userSettings.emails.verified')}</span> : <span className="rounded-full bg-[#f0b232]/15 px-2 py-0.5 text-[11px] text-[#8a6100]">{t('userSettings.emails.pending')}</span>}
26 <div className="ml-auto flex gap-2">
27 {!email.is_primary ? (
28 <Button variant="secondary" className="h-8 text-[12.5px]" disabled={busy} onClick={() => void run(async () => { await api.setPrimaryEmail(email.id); setTick(t => t + 1) }, t('userSettings.emails.primaryChanged'))}>{t('userSettings.emails.setPrimary')}</Button>
29 ) : null}
30 <Button variant="secondary" className="h-8 text-[12.5px]" disabled={busy} onClick={() => void run(async () => { await api.deleteEmail(email.id); setTick(t => t + 1) }, t('userSettings.emails.deleted'))}>{t('common.action.delete')}</Button>
31 </div>
32 </div>
33 ))}
34 </div>
35 )}
36 <div className="flex max-w-[620px] gap-2">
37 <input className={inputCls} type="email" placeholder={t('userSettings.emails.newPlaceholder')} value={newEmail} onChange={e => setNewEmail(e.target.value)} />
38 <Button disabled={busy || !newEmail} onClick={() => void run(async () => { const result = await api.addEmail(newEmail); if (result.pending) setPending(newEmail); setTick(t => t + 1) }, t('userSettings.emails.added'))}>{t('userSettings.emails.add')}</Button>
39 </div>
40 {pending ? (
41 <div className="mt-3 flex max-w-[620px] gap-2">
42 <input className={inputCls} placeholder={t('userSettings.emails.codePlaceholder', { email: pending })} value={verifyCode} onChange={e => setVerifyCode(e.target.value)} />
43 <Button disabled={busy || !verifyCode} onClick={() => void run(async () => { await api.verifyAddEmail(pending, verifyCode); setPending(null); setVerifyCode(''); setNewEmail(''); setTick(t => t + 1) }, t('userSettings.emails.verifiedNotice'))}>{t('userSettings.emails.verify')}</Button>
44 </div>
45 ) : null}
46 <div className="mt-4"><SaveBar busy={busy} notice={notice} error={error} label={t('userSettings.emails.refresh')} onSave={() => setTick(t => t + 1)} /></div>
47 </section>
48 )
49}
Modified web/src/features/user-settings/ui/sections/sessions-section.tsx web/src/features/user-settings/ui/sections/sessions-section.tsx +1 −14 View web/src/features/user-settings/ui/sections/sessions-section.tsx
@@ -1,6 +1,6 @@
11import { useState } from 'react'
22import { useT } from '@/shared/i18n'
3import { Busy, Button, SaveBar, SectionTitle, fmtDate, inputCls, useSave } from '@/shared/ui'
3import { Busy, Button, SaveBar, SectionTitle, fmtDate, useSave } from '@/shared/ui'
44import { settingsKey, useAsync } from '@/shared/lib'
55import * as api from '../../model/api'
66
@@ -12,7 +12,6 @@
1212 return { devices, sessions: sessions.items ?? [] }
1313 }, [tick], { cacheKey: settingsKey('sessions') })
1414 const { busy, notice, error, run } = useSave()
15 const [names, setNames] = useState<Record<string, string>>({})
1615 const refresh = () => setTick(t => t + 1)
1716
1817 return (
@@ -29,18 +28,6 @@
2928 <div key={device.id} className="settings-card flex flex-wrap items-center gap-3 px-4 py-3">
3029 <span className="min-w-[160px] text-[13.5px] font-semibold">{device.device_name || device.device_type || t('userSettings.sessions.unknownDevice')}</span>
3130 <span className="text-[12px] text-shell-ink-muted">{device.os ?? '—'} · {t('userSettings.sessions.recently', { when: fmtDate(device.last_seen_at) })}</span>
32 <input
33 className={`${inputCls} ml-auto h-8 max-w-[180px]`}
34 placeholder={device.device_name || t('userSettings.sessions.renamePlaceholder')}
35 value={names[device.id] ?? ''}
36 onChange={e => setNames(prev => ({ ...prev, [device.id]: e.target.value }))}
37 />
38 <Button variant="secondary" className="h-8 text-[12.5px]" disabled={busy} onClick={() => void run(async () => { await api.renameDevice(device.id, names[device.id] ?? ''); refresh() }, t('userSettings.sessions.renamed'))}>
39 {t('userSettings.sessions.rename')}
40 </Button>
41 <Button variant="secondary" className="h-8 text-[12.5px]" disabled={busy} onClick={() => void run(async () => { await api.deleteDevice(device.id); refresh() }, t('userSettings.sessions.deviceDeleted'))}>
42 {t('common.action.delete')}
43 </Button>
4431 </div>
4532 ))}
4633 </div>
Modified web/src/features/user-settings/ui/user-settings-modal.tsx web/src/features/user-settings/ui/user-settings-modal.tsx +1 −4 View web/src/features/user-settings/ui/user-settings-modal.tsx
@@ -1,5 +1,5 @@
11import { useState } from 'react'
2import { Activity, Bell, Eye, IdCard, KeyRound, Mail, MonitorSmartphone, Palette, ShieldCheck, Terminal, UserRound } from 'lucide-react'
2import { Activity, Bell, Eye, IdCard, KeyRound, MonitorSmartphone, Palette, ShieldCheck, Terminal, UserRound } from 'lucide-react'
33import { useT } from '@/shared/i18n'
44import { SettingsModalShell, type SettingsSection } from '@/shared/ui'
55import type { Me } from '../model/user-settings'
@@ -7,7 +7,6 @@
77import { ProfileSection } from './sections/profile-section'
88import { SecuritySection } from './sections/security-section'
99import { SessionsSection } from './sections/sessions-section'
10import { EmailsSection } from './sections/emails-section'
1110import { PrivacySection } from './sections/privacy-section'
1211import { NotificationsSection } from './sections/notifications-section'
1312import { AppearanceSection } from './sections/appearance-section'
@@ -34,7 +33,6 @@
3433 { id: 'profile', label: t('settings.section.profile'), icon: IdCard, group: t('settings.group.account') },
3534 { id: 'security', label: t('settings.section.security'), icon: ShieldCheck, group: t('settings.group.account') },
3635 { id: 'sessions', label: t('settings.section.sessions'), icon: MonitorSmartphone, group: t('settings.group.account') },
37 { id: 'emails', label: t('settings.section.emails'), icon: Mail, group: t('settings.group.account') },
3836 { id: 'privacy', label: t('settings.section.privacy'), icon: Eye, group: t('settings.group.preferences') },
3937 { id: 'notifications', label: t('settings.section.notifications'), icon: Bell, group: t('settings.group.preferences') },
4038 { id: 'appearance', label: t('settings.section.appearance'), icon: Palette, group: t('settings.group.preferences') },
@@ -57,7 +55,6 @@
5755 {section === 'profile' ? <ProfileSection me={me} onMeChanged={onMeChanged} /> : null}
5856 {section === 'security' ? <SecuritySection /> : null}
5957 {section === 'sessions' ? <SessionsSection /> : null}
60 {section === 'emails' ? <EmailsSection /> : null}
6158 {section === 'privacy' ? <PrivacySection /> : null}
6259 {section === 'notifications' ? <NotificationsSection /> : null}
6360 {section === 'appearance' ? <AppearanceSection /> : null}
Modified web/src/pages/space-invite/ui/space-invite-page.tsx web/src/pages/space-invite/ui/space-invite-page.tsx +8 −1 View web/src/pages/space-invite/ui/space-invite-page.tsx
@@ -116,7 +116,14 @@
116116 {t('space.invite.accept.login')}
117117 <ArrowRight size={15} aria-hidden />
118118 </Button>
119 <Button variant="secondary" size="lg" className="w-full" onClick={() => navigate('/auth/signup')}>
119 <Button
120 variant="secondary"
121 size="lg"
122 className="w-full"
123 onClick={() =>
124 navigate(`/auth/signup?redirect=${encodeURIComponent(location.pathname + location.search)}`)
125 }
126 >
120127 {t('space.invite.accept.signup')}
121128 </Button>
122129 </div>
Modified web/src/pages/workspace/model/workspace-data.ts web/src/pages/workspace/model/workspace-data.ts +1 −1 View web/src/pages/workspace/model/workspace-data.ts
@@ -76,7 +76,7 @@
7676 // repopulates it (login pages also set the owner eagerly, see auth flow).
7777 const meUserId = me.data?.user.user_id
7878 useEffect(() => {
79 if (meUserId) void setCacheOwner(meUserId)
79 if (meUserId) void setCacheOwner(String(meUserId))
8080 }, [meUserId])
8181
8282 // The interface language follows the account: `users.language` wins over
Modified web/src/pages/workspace/ui/board/board-view.tsx web/src/pages/workspace/ui/board/board-view.tsx +1 −1 View web/src/pages/workspace/ui/board/board-view.tsx
@@ -32,7 +32,7 @@
3232 boardId: string
3333 /** Row from the sidebar's board list — titles the frame before the fetch lands. */
3434 boardHint?: Board
35 viewerId?: string
35 viewerId?: number
3636 /** AppShell slots owned by the workspace frame. */
3737 shell: { rail?: React.ReactNode; sideHeader?: React.ReactNode; navPinned?: React.ReactNode; nav?: React.ReactNode; sideFooter?: React.ReactNode }
3838 /** Sidebar counts (open/closed issues) come from the shell's own query. */
Modified web/src/pages/workspace/ui/chat/chat-view.tsx web/src/pages/workspace/ui/chat/chat-view.tsx +1 −1 View web/src/pages/workspace/ui/chat/chat-view.tsx
@@ -16,7 +16,7 @@
1616export interface ChatViewProps {
1717 spaceName: string
1818 roomId: string
19 viewerId?: string
19 viewerId?: number
2020 /** Sidebar rows: they title the frame before `GET /chat/rooms/{id}` lands. */
2121 roomHint?: RoomBrief
2222 dmHint?: DmBrief
Modified web/src/pages/workspace/ui/repo/repo-view.tsx web/src/pages/workspace/ui/repo/repo-view.tsx +1 −1 View web/src/pages/workspace/ui/repo/repo-view.tsx
@@ -30,7 +30,7 @@
3030 * id; repo settings are owner-only server-side, so the tab needs it too. */
3131 mySpace?: MySpace
3232 /** Signed-in account id, compared against the space owner. */
33 viewerId?: string
33 viewerId?: number
3434 /** AppShell slots owned by the workspace frame (rail / nav / footer). */
3535 shell: { rail?: React.ReactNode; sideHeader?: React.ReactNode; navPinned?: React.ReactNode; nav?: React.ReactNode; sideFooter?: React.ReactNode }
3636}
Modified web/src/pages/workspace/ui/members-view.tsx web/src/pages/workspace/ui/members-view.tsx +1 −1 View web/src/pages/workspace/ui/members-view.tsx
@@ -221,5 +221,5 @@
221221}
222222
223223function memberName(member: SpaceMember): string {
224 return member.alias_name || member.user?.display_name || member.user?.username || member.user?.user_id || '—'
224 return member.alias_name || member.user?.display_name || member.user?.username || String(member.user?.user_id ?? '') || '—'
225225}
Modified web/src/pages/workspace/ui/user-panel.tsx web/src/pages/workspace/ui/user-panel.tsx +1 −1 View web/src/pages/workspace/ui/user-panel.tsx
@@ -147,7 +147,7 @@
147147 </div>
148148 <button
149149 type="button"
150 onClick={() => void copyText(me.user.user_id)}
150 onClick={() => void copyText(String(me.user.user_id))}
151151 className="flex w-full items-center gap-2 px-3 py-2 text-left text-[13px] transition-colors hover:bg-shell-active"
152152 >
153153 {copied ? (
Modified web/src/shared/api/apiDefinitions.ts web/src/shared/api/apiDefinitions.ts +12 −35 View web/src/shared/api/apiDefinitions.ts
@@ -23,17 +23,19 @@
2323- `POST /auth/logout` is idempotent: repeating it always succeeds with `200`.
2424
2525### Authentication
26- `session_cookie`: primary web session auth (cookie `gitdata_session`).
27- `basic_access_key`: HTTP Basic auth with an access key created via
28 `POST /auth/access-keys` (id as user, secret as password). **Scope: git
29 repository endpoints only** (`/repos/*`, `/graphql`); it is NOT accepted by
30 chat/issues/space/user endpoints.
26- `session_cookie`: KenOAuth access token carried in the `gitdata_session`
27 cookie. `GET /auth/login` starts the OIDC authorization-code + PKCE flow;
28 `POST /auth/refresh` rotates the access/refresh pair; `POST /auth/logout`
29 revokes the KenOAuth session and clears both cookies.
30- `basic_access_key`: HTTP Basic auth with a KenOAuth-issued access key.
31 **Scope: git repository endpoints only** (`/repos/*`, `/graphql`); it is NOT
32 accepted by chat/issues/space/user endpoints.
3133
3234### URL & parameter style
3335- Paths are `kebab-case`, plural nouns, nesting expresses ownership.
3436- Query/path/JSON field names are `snake_case`; enum values are `snake_case`.
35- All endpoints are versioned under `/api/v1`. `/api/v1/internal/*` requires a
36 session belonging to a configured platform admin and is NOT documented here.
37- All endpoints are versioned under `/api/v1`. `/api/v1/internal/*` is guarded
38 by the `x-gitdata-service-key` header and is NOT documented here.
3739- State-transition actions use `POST {resource}/{id}/{verb}` (close/reopen);
3840 idempotent whole-resource state uses `PUT` (default branch, pin, read-state);
3941 partial updates use `PATCH`.
@@ -53,10 +55,9 @@
5355Example: `{"milestone_id": null}` unassigns the milestone; omitting
5456`milestone_id` leaves it untouched.
5557
56### 2FA login exchange
57`POST /auth/login` responds with `needs_2fa` and an opaque one-time
58`challenge` string; pass it to `POST /auth/login/2fa` together with the
59TOTP/recovery code. The challenge is single-use and short-lived.
58### 2FA
59TOTP enrollment, recovery codes and sensitive-operation verification are
60handled by KenOAuth; GitData only proxies the account-management endpoints.
6061
6162### Operations
6263- `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
@@ -109,24 +110,10 @@
109110 'security.list_access_keys': ['GET', '/auth/access-keys'],
110111 'security.create_access_key': ['POST', '/auth/access-keys'],
111112 'security.delete_access_key': ['DELETE', '/auth/access-keys/{key_id}'],
112 'auth.update_access_key': ['PATCH', '/auth/access-keys/{key_id}'],
113113 'auth.rotate_access_key': ['POST', '/auth/access-keys/{key_id}/rotate'],
114114 'account.list_agreements': ['GET', '/auth/agreements'],
115 'auth.request_email_change': ['POST', '/auth/email/change'],
116 'auth.confirm_email_change': ['POST', '/auth/email/change/confirm'],
117 'auth.cancel_email_change': ['DELETE', '/auth/email/change/{ticket_id}'],
118 'auth.reclaim': ['POST', '/auth/email/reclaim'],
119 'auth.resend_code': ['POST', '/auth/email/resend'],
120 'auth.verify_email': ['POST', '/auth/email/verify'],
121115 'auth.jwt_request': ['GET', '/auth/jwt-request'],
122 'auth.login': ['POST', '/auth/login'],
123 'auth.login_2fa': ['POST', '/auth/login/2fa'],
124 'auth.logout': ['POST', '/auth/logout'],
125116 'auth.change_password': ['POST', '/auth/password/change'],
126 'auth.forgot_password': ['POST', '/auth/password/forgot'],
127 'auth.reset_password': ['POST', '/auth/password/reset'],
128 'auth.pow_challenge': ['GET', '/auth/pow/challenge'],
129 'auth.register': ['POST', '/auth/register'],
130117 'security.list_ssh_keys': ['GET', '/auth/ssh-keys'],
131118 'security.add_ssh_key': ['POST', '/auth/ssh-keys'],
132119 'security.delete_ssh_key': ['DELETE', '/auth/ssh-keys/{key_id}'],
@@ -331,17 +318,7 @@
331318 'user.get_avatar': ['GET', '/me/avatar'],
332319 'user.upload_avatar': ['PUT', '/me/avatar'],
333320 'user.delete_avatar': ['DELETE', '/me/avatar'],
334 'deletion.deletion_status': ['GET', '/me/deletion'],
335 'deletion.request_deletion': ['POST', '/me/deletion'],
336 'deletion.cancel_deletion': ['DELETE', '/me/deletion'],
337321 'account.list_devices': ['GET', '/me/devices'],
338 'account.delete_device': ['DELETE', '/me/devices/{device_id}'],
339 'account.update_device': ['PATCH', '/me/devices/{device_id}'],
340 'user.list_emails': ['GET', '/me/emails'],
341 'user.add_email': ['POST', '/me/emails'],
342 'user.verify_add_email': ['POST', '/me/emails/verify'],
343 'user.delete_email': ['DELETE', '/me/emails/{email_id}'],
344 'user.set_primary_email': ['PATCH', '/me/emails/{email_id}/primary'],
345322 'user.list_memes': ['GET', '/me/memes'],
346323 'user.create_meme': ['POST', '/me/memes'],
347324 'user.update_meme': ['PUT', '/me/memes/{meme_id}'],
Modified web/src/shared/api/createApis.ts web/src/shared/api/createApis.ts +12 −11 View web/src/shared/api/createApis.ts
@@ -22,17 +22,19 @@
2222- `POST /auth/logout` is idempotent: repeating it always succeeds with `200`.
2323
2424### Authentication
25- `session_cookie`: primary web session auth (cookie `gitdata_session`).
26- `basic_access_key`: HTTP Basic auth with an access key created via
27 `POST /auth/access-keys` (id as user, secret as password). **Scope: git
28 repository endpoints only** (`/repos/*`, `/graphql`); it is NOT accepted by
29 chat/issues/space/user endpoints.
25- `session_cookie`: KenOAuth access token carried in the `gitdata_session`
26 cookie. `GET /auth/login` starts the OIDC authorization-code + PKCE flow;
27 `POST /auth/refresh` rotates the access/refresh pair; `POST /auth/logout`
28 revokes the KenOAuth session and clears both cookies.
29- `basic_access_key`: HTTP Basic auth with a KenOAuth-issued access key.
30 **Scope: git repository endpoints only** (`/repos/*`, `/graphql`); it is NOT
31 accepted by chat/issues/space/user endpoints.
3032
3133### URL & parameter style
3234- Paths are `kebab-case`, plural nouns, nesting expresses ownership.
3335- Query/path/JSON field names are `snake_case`; enum values are `snake_case`.
34- All endpoints are versioned under `/api/v1`. `/api/v1/internal/*` requires a
35 session belonging to a configured platform admin and is NOT documented here.
36- All endpoints are versioned under `/api/v1`. `/api/v1/internal/*` is guarded
37 by the `x-gitdata-service-key` header and is NOT documented here.
3638- State-transition actions use `POST {resource}/{id}/{verb}` (close/reopen);
3739 idempotent whole-resource state uses `PUT` (default branch, pin, read-state);
3840 partial updates use `PATCH`.
@@ -52,10 +54,9 @@
5254Example: `{"milestone_id": null}` unassigns the milestone; omitting
5355`milestone_id` leaves it untouched.
5456
55### 2FA login exchange
56`POST /auth/login` responds with `needs_2fa` and an opaque one-time
57`challenge` string; pass it to `POST /auth/login/2fa` together with the
58TOTP/recovery code. The challenge is single-use and short-lived.
57### 2FA
58TOTP enrollment, recovery codes and sensitive-operation verification are
59handled by KenOAuth; GitData only proxies the account-management endpoints.
5960
6061### Operations
6162- `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
Modified web/src/shared/api/globals.d.ts web/src/shared/api/globals.d.ts View web/src/shared/api/globals.d.ts
Diff too large to display.
Modified web/src/shared/api/index.ts web/src/shared/api/index.ts +5 −2 View web/src/shared/api/index.ts
@@ -23,8 +23,11 @@
2323}
2424
2525function redirectToLogin() {
26 if (window.location.pathname.startsWith('/auth')) return;
27 window.location.assign('/api/v1/auth/login?silent=1');
26 const { pathname, search } = window.location;
27 if (pathname.startsWith('/auth')) return;
28 // 带着当前位置重登,回来后还停在原来的页面;入口页不重复带参数。
29 const target = pathname === '/' || pathname === '/app' ? '' : `&redirect=${encodeURIComponent(pathname + search)}`;
30 window.location.assign(`/api/v1/auth/login?silent=1${target}`);
2831}
2932
3033const retryAdapter: typeof rawAdapter = (elements, method) => {
Modified web/src/shared/i18n/messages/en/auth.ts web/src/shared/i18n/messages/en/auth.ts +0 −24 View web/src/shared/i18n/messages/en/auth.ts
@@ -18,30 +18,6 @@
1818 'auth.login.resendVerify': 'Resend verification email',
1919 'auth.login.noAccount': 'No account yet?',
2020 'auth.login.requestAccess': 'Create account',
21 'auth.signup.crumb': 'Create account',
22 'auth.signup.hint': 'You must be {age}+ · password at least 8 characters',
23 'auth.signup.heading': 'Create your account',
24 'auth.signup.subheading': 'One account for repositories, boards and team chat.',
25 'auth.signup.username': 'Username (optional)',
26 'auth.signup.email': 'Email',
27 'auth.signup.password': 'Password',
28 'auth.signup.passwordHint': 'At least 8 characters with upper case, lower case and a digit',
29 'auth.signup.dob': 'Date of birth',
30 'auth.signup.dobRequired': '{age}+ required',
31 'auth.signup.day': 'Day',
32 'auth.signup.month': 'Month',
33 'auth.signup.year': 'Year',
34 'auth.signup.termsAria': 'Agree to terms',
35 'auth.signup.termsPrefix': 'I agree to the',
36 'auth.signup.terms': 'Terms',
37 'auth.signup.and': 'and',
38 'auth.signup.privacy': 'Privacy Policy',
39 'auth.signup.submit': 'Create account',
40 'auth.signup.haveAccount': 'Already have an account?',
41 'auth.signup.signIn': 'Sign in',
42 'auth.signup.error.terms': 'Please accept the Terms and Privacy Policy to continue.',
43 'auth.signup.error.dobIncomplete': 'Please complete your date of birth.',
44 'auth.signup.error.age': 'You must be at least {age} years old to sign up.',
4521 'auth.forgot.crumb': 'Recover access',
4622 'auth.forgot.hint': 'Reset links expire after 30 minutes',
4723 'auth.forgot.heading': 'Forgot password?',
Modified web/src/shared/i18n/messages/zh-CN/auth.ts web/src/shared/i18n/messages/zh-CN/auth.ts +0 −24 View web/src/shared/i18n/messages/zh-CN/auth.ts
@@ -18,30 +18,6 @@
1818 'auth.login.resendVerify': '重发验证邮件',
1919 'auth.login.noAccount': '还没有账号?',
2020 'auth.login.requestAccess': '创建账号',
21 'auth.signup.crumb': '创建账号',
22 'auth.signup.hint': '注册需年满 {age} 周岁 · 密码至少 8 位',
23 'auth.signup.heading': '创建账号',
24 'auth.signup.subheading': '一个账号,进入仓库、看板与团队协作。',
25 'auth.signup.username': '用户名(选填)',
26 'auth.signup.email': '邮箱',
27 'auth.signup.password': '密码',
28 'auth.signup.passwordHint': '至少 8 位,需含大写、小写字母和数字',
29 'auth.signup.dob': '出生日期',
30 'auth.signup.dobRequired': '需年满 {age} 周岁',
31 'auth.signup.day': '日',
32 'auth.signup.month': '月',
33 'auth.signup.year': '年',
34 'auth.signup.termsAria': '同意条款',
35 'auth.signup.termsPrefix': '我同意',
36 'auth.signup.terms': '服务条款',
37 'auth.signup.and': '与',
38 'auth.signup.privacy': '隐私政策',
39 'auth.signup.submit': '创建账号',
40 'auth.signup.haveAccount': '已有账号?',
41 'auth.signup.signIn': '登录',
42 'auth.signup.error.terms': '请先同意服务条款与隐私政策。',
43 'auth.signup.error.dobIncomplete': '请补全出生日期。',
44 'auth.signup.error.age': '注册需年满 {age} 周岁。',
4521 'auth.forgot.crumb': '找回访问',
4622 'auth.forgot.hint': '重置链接 30 分钟内有效',
4723 'auth.forgot.heading': '忘记密码?',
Modified web/openapi.json web/openapi.json View web/openapi.json
Diff too large to display.
Modified justfile justfile +6 −0 View justfile
@@ -8,6 +8,10 @@
88# just check -> just infra -> just build -> just up
99# 常用:just up / just down / just status / just tail apihost
1010#
11# 二进制目录跟随 cargo 的 CARGO_TARGET_DIR(just build 与 just up 一致读同一个变量):
12# CARGO_TARGET_DIR=/tmp/kenapi just up
13# 日志 / pid / 本地 git 仓库仍固定在 target/dev,不随 CARGO_TARGET_DIR 搬家。
14#
1115# 依赖工具:cargo、bun、ssh-keygen;Windows 额外需要 WSL 的 Ubuntu 发行版,
1216# macOS/Linux 额外需要 docker + docker compose。
1317# 需要 just >= 1.21。
@@ -49,6 +53,7 @@
4953 @{{ invoke }}"{{ dev }}" migrate
5054
5155# 构建全部后端二进制(web 前端由 build.rs 构建并内嵌)
56# 遵循 CARGO_TARGET_DIR;just up 从同一目录取二进制。
5257build:
5358 @cargo build --bins
5459
@@ -57,6 +62,7 @@
5762 @cargo build --release --bins
5863
5964# 启动全部服务(后端 + web)
65# 二进制目录 = $CARGO_TARGET_DIR/debug(未设置则 target/debug)。
6066up *svc='':
6167 @{{ invoke }}"{{ dev }}" up {{ svc }}
6268
Modified openapi.json openapi.json View openapi.json
Diff too large to display.