Skip to content
Clone

refactor: update user identifiers from string to number across multiple files

ZhenYi committed
parent e34ed7e commit 759ae5e Browse files

82 files changed

+1987 −1175

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 82
Deleted .opencode/goal-log.jsonl
@@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
@@ -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 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 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 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 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 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 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 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 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 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 View web/src/shared/api/apiDefinitions.ts
@@ -23,17 +23,19 @@
2323- &#x60;POST /auth/logout&#x60; is idempotent: repeating it always succeeds with &#x60;200&#x60;.
2424
2525### Authentication
26- &#x60;session_cookie&#x60;: primary web session auth (cookie &#x60;gitdata_session&#x60;).
27- &#x60;basic_access_key&#x60;: HTTP Basic auth with an access key created via
28 &#x60;POST /auth/access-keys&#x60; (id as user, secret as password). **Scope: git
29 repository endpoints only** (&#x60;/repos/*&#x60;, &#x60;/graphql&#x60;); it is NOT accepted by
30 chat/issues/space/user endpoints.
26- &#x60;session_cookie&#x60;: KenOAuth access token carried in the &#x60;gitdata_session&#x60;
27 cookie. &#x60;GET /auth/login&#x60; starts the OIDC authorization-code + PKCE flow;
28 &#x60;POST /auth/refresh&#x60; rotates the access/refresh pair; &#x60;POST /auth/logout&#x60;
29 revokes the KenOAuth session and clears both cookies.
30- &#x60;basic_access_key&#x60;: HTTP Basic auth with a KenOAuth-issued access key.
31 **Scope: git repository endpoints only** (&#x60;/repos/*&#x60;, &#x60;/graphql&#x60;); it is NOT
32 accepted by chat/issues/space/user endpoints.
3133
3234### URL &amp; parameter style
3335- Paths are &#x60;kebab-case&#x60;, plural nouns, nesting expresses ownership.
3436- Query/path/JSON field names are &#x60;snake_case&#x60;; enum values are &#x60;snake_case&#x60;.
35- All endpoints are versioned under &#x60;/api/v1&#x60;. &#x60;/api/v1/internal/*&#x60; requires a
36 session belonging to a configured platform admin and is NOT documented here.
37- All endpoints are versioned under &#x60;/api/v1&#x60;. &#x60;/api/v1/internal/*&#x60; is guarded
38 by the &#x60;x-gitdata-service-key&#x60; header and is NOT documented here.
3739- State-transition actions use &#x60;POST {resource}/{id}/{verb}&#x60; (close/reopen);
3840 idempotent whole-resource state uses &#x60;PUT&#x60; (default branch, pin, read-state);
3941 partial updates use &#x60;PATCH&#x60;.
@@ -53,10 +55,9 @@
5355Example: &#x60;{&quot;milestone_id&quot;: null}&#x60; unassigns the milestone; omitting
5456&#x60;milestone_id&#x60; leaves it untouched.
5557
56### 2FA login exchange
57&#x60;POST /auth/login&#x60; responds with &#x60;needs_2fa&#x60; and an opaque one-time
58&#x60;challenge&#x60; string; pass it to &#x60;POST /auth/login/2fa&#x60; 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- &#x60;GET /healthz&#x60; — liveness probe (no auth, outside &#x60;/api/v1&#x60;).
@@ -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 View web/src/shared/api/createApis.ts
@@ -22,17 +22,19 @@
2222- &#x60;POST /auth/logout&#x60; is idempotent: repeating it always succeeds with &#x60;200&#x60;.
2323
2424### Authentication
25- &#x60;session_cookie&#x60;: primary web session auth (cookie &#x60;gitdata_session&#x60;).
26- &#x60;basic_access_key&#x60;: HTTP Basic auth with an access key created via
27 &#x60;POST /auth/access-keys&#x60; (id as user, secret as password). **Scope: git
28 repository endpoints only** (&#x60;/repos/*&#x60;, &#x60;/graphql&#x60;); it is NOT accepted by
29 chat/issues/space/user endpoints.
25- &#x60;session_cookie&#x60;: KenOAuth access token carried in the &#x60;gitdata_session&#x60;
26 cookie. &#x60;GET /auth/login&#x60; starts the OIDC authorization-code + PKCE flow;
27 &#x60;POST /auth/refresh&#x60; rotates the access/refresh pair; &#x60;POST /auth/logout&#x60;
28 revokes the KenOAuth session and clears both cookies.
29- &#x60;basic_access_key&#x60;: HTTP Basic auth with a KenOAuth-issued access key.
30 **Scope: git repository endpoints only** (&#x60;/repos/*&#x60;, &#x60;/graphql&#x60;); it is NOT
31 accepted by chat/issues/space/user endpoints.
3032
3133### URL &amp; parameter style
3234- Paths are &#x60;kebab-case&#x60;, plural nouns, nesting expresses ownership.
3335- Query/path/JSON field names are &#x60;snake_case&#x60;; enum values are &#x60;snake_case&#x60;.
34- All endpoints are versioned under &#x60;/api/v1&#x60;. &#x60;/api/v1/internal/*&#x60; requires a
35 session belonging to a configured platform admin and is NOT documented here.
36- All endpoints are versioned under &#x60;/api/v1&#x60;. &#x60;/api/v1/internal/*&#x60; is guarded
37 by the &#x60;x-gitdata-service-key&#x60; header and is NOT documented here.
3638- State-transition actions use &#x60;POST {resource}/{id}/{verb}&#x60; (close/reopen);
3739 idempotent whole-resource state uses &#x60;PUT&#x60; (default branch, pin, read-state);
3840 partial updates use &#x60;PATCH&#x60;.
@@ -52,10 +54,9 @@
5254Example: &#x60;{&quot;milestone_id&quot;: null}&#x60; unassigns the milestone; omitting
5355&#x60;milestone_id&#x60; leaves it untouched.
5456
55### 2FA login exchange
56&#x60;POST /auth/login&#x60; responds with &#x60;needs_2fa&#x60; and an opaque one-time
57&#x60;challenge&#x60; string; pass it to &#x60;POST /auth/login/2fa&#x60; 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- &#x60;GET /healthz&#x60; — liveness probe (no auth, outside &#x60;/api/v1&#x60;).
Modified 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 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 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 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 View web/openapi.json

Diff too large to display.

Modified justfile 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 View openapi.json

Diff too large to display.