chore: add bug wiki docs, refresh workspace, livechat, docker build, and totp cargo
zhenyi committed
Modified app/livechat/src/main.rs app/livechat/src/main.rs +8 −1 View app/livechat/src/main.rs
@@ -138,7 +138,14 @@
138138 ));
139139 let hub = ConnectionHub::new(app.gateway.max_connections_per_user);
140140 let gateway = GatewayState::new(chat.clone(), hub, app.gateway.clone());
141 let live = LiveService::new(store, producer, kv, jwt, app.live.clone());
141 let live = LiveService::new(
142 store,
143 producer,
144 kv,
145 jwt,
146 app.live.clone(),
147 Arc::new(app.rate_limit.clone()),
148 );
142149
143150 let room_worker = chat.spawn_room_worker()?;
144151 let user_worker = chat.spawn_user_worker()?;
Modified docker/build.sh docker/build.sh +2 −1 View docker/build.sh
@@ -8,7 +8,7 @@
88
99SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1010ROOT_DIR="$(dirname "${SCRIPT_DIR}")"
11APPS=(apihost githook gitpro livechat mails)
11APPS=(apihost githook gitpro livechat mails lprovider)
1212
1313if command -v docker >/dev/null 2>&1; then
1414 RUNTIME="${RUNTIME:-docker}"
@@ -24,6 +24,7 @@
2424 [apihost]=8080
2525 [gitpro]=8080/2222
2626 [livechat]=8081
27 [lprovider]=8080/9090/9443
2728)
2829
2930detect_base_image() {
Modified lib/totp/Cargo.toml lib/totp/Cargo.toml +2 −2 View lib/totp/Cargo.toml
@@ -2,7 +2,6 @@
22name = "totp"
33version = "1.94.0"
44edition = "2024"
5workspace = true
65
76[dependencies]
87base64 = { workspace = true }
@@ -11,4 +10,5 @@
1110sha1 = { workspace = true }
1211hmac = { workspace = true }
1312aes-gcm = { workspace = true }
14thiserror = { workspace = true }
No newline at end of file
13thiserror = { workspace = true }
14time = { workspace = true }
No newline at end of file
Added wiki/git_bug.md wiki/git_bug.md +98 −0 View wiki/git_bug.md
@@ -0,0 +1,98 @@
1# dvcs 代码审查问题清单
2
3审查范围:`dvcs/` 下 7 个 crate(codesearch、git、gitauth、gitlfs、gitsmart、gitssh、gitsync),共 145 个 .rs 文件约 12000 行。按严重度分级:S = 安全/可被外部触发,B = 功能性 Bug,P = 性能,Q = 质量/死代码。
4
5## 1. 安全(S)
6
7| # | 位置 | 问题 |
8| --- | --- | --- |
9| S1 | `git/patch/usecase.rs:103-105` | 用户输入的 `base` 未校验直接拼进 `duct::cmd("git", ["fetch", "-q", repo_path, base])`,可注入 git 选项(如 `--upload-pack=<任意程序>`)→ 本地任意命令执行(RCE)。 |
10| S2 | `git/pack/usecase.rs:51` | `pack_id` 未校验拼接 `objects/pack/{id}.pack`,`../../` 路径穿越,可将任意文件当 pack 喂给 index-pack。 |
11| S3 | `gitsmart/pre_receive.rs:183` + `handler/smart.rs:87` | `parse_receive_pack_refs` 解析失败返回空 Vec,空结果直接跳过 pre-receive 校验(fail-open);且 40 位长度校验跳过 SHA-256 仓库的 64 位 OID,恶意客户端可绕过分支保护/强推限制。 |
12| S4 | `gitssh/handler/channel.rs:209-218`、`handler/command.rs:135-142` | 实际 spawn git 的两处未应用 `build_git_command`(git_service.rs:92)的加固环境:`GIT_CONFIG_NOSYSTEM`、`GIT_NO_REPLACE_OBJECTS`、`GIT_CONFIG_GLOBAL/SYSTEM=/dev/null`。服务端 git 会读系统/全局 gitconfig(如 `uploadpack.packObjectsHook` 可指向任意命令)。且该函数是死代码,加固从未生效。 |
13| S5 | `git/gitcli.rs:88` | askpass 脚本权限 0o600 无执行位,exec 必失败 → Token 认证不可用;且 token 经环境变量传给所有子进程。 |
14| S6 | `git/validate.rs:54-66` | `allowed_remote_scheme` 只查 scheme,ssh 可达内网(SSRF 不完整);short_name 允许前导 `-`,为 git 参数选项注入留口。 |
15| S7 | `gitsmart/handler/lfs.rs:17`、`smart.rs:14` | body 上限 8GiB/1GiB,gzip 解压后可达 8GB,单请求内存 DoS 面。 |
16| S8 | `gitauth/service.rs:153` | access key 用 `==` 比较哈希,非时序安全比较。 |
17
18**修复状态:S1-S8 全部 ✅ 已修复**(S1 base 先 `rev_parse_single` 解析为 OID 再校验,fetch 用 hex 防选项注入 / S2 pack_id 40/64-hex 校验防路径穿越 / S3 `parse_receive_pack_refs` 改返回 `Result` fail-closed 且支持 64 位 OID / S4 `apply_hardened_env` 落地两处 spawn git / S5 askpass 权限 0o600→0o700 / S6 拒绝前导 `-` + `is_private_host` 内网阻断 / S7 body 上限 8GiB→64MB、1GiB→512MB / S8 改 `constant_time_eq` 恒定时间比较)。
19
20## 2. 严重 Bug(B1)
21
22| # | 位置 | 问题 |
23| --- | --- | --- |
24| B1 | `gitsmart/pre_receive.rs:177` | `is_fast_forward` 仅当 `old==new` 时返回 true(零 OID 已在调用方排除),真实快进推送被误判为强推,`enable_force_push=false` 时被拒绝。 |
25| B2 | `gitsmart/push.rs:150` | `f` panic 时 `heartbeat` 被 unwinding 丢弃,JoinHandle drop 后心跳任务脱离并持有 `stop_clone`,永久每 40s 续租锁 → 该仓库 push 锁永久死锁。 |
26| B3 | `gitsync/sync.rs:67-76` | 心跳用 `watch::channel(())`,失败时 `tx.send(())` 不会唤醒 `rx.changed()`(值未变化不通知),锁丢失信号永远传不到,两个 worker 并发 sync 同一仓库。 |
27| B4 | `git/object/cache.rs:150`、`lfs/usecase.rs:148` | 缓存同步封装在 async 上下文(GraphQL resolver)里 `Handle::block_on` 直接 panic(tokio 禁止);`archive/usecase.rs:26` runtime 内 `Builder::build()` 同样 panic,首次取缓存即崩溃。 |
28| B5 | `gitsync/worker.rs:222-229,184-188` | 锁冲突 handler 返回 `Ok`(消息已 ack),重发/投 DLQ 失败仅记日志,任务永久丢失;一般性失败超过 `subscribe_max_retries` 直接 drop,且消费者 `enable.auto.commit=true`,偏移照常提交,无 DLQ 兜底。 |
29| B6 | `gitsync/stats.rs:22` + `worker.rs:152-159` | `repo` 模型在加锁前加载,`repo.nums_commits + new_commits` 用陈旧基线覆盖写,并发任务导致 commit 计数回退(lost update)。 |
30| B7 | `git/commit/types.rs:39` | 负数时间戳 `as u64` 回绕,`UNIX_EPOCH + Duration` 溢出 panic,恶意 commit 可触发。 |
31| B8 | `git/signature/usecase.rs:113-116` | 验证 tag 时 signed data 是含签名本身的完整序列化,签名验证恒失败。 |
32| B9 | `git/remote/usecase.rs:73` | push 默认 refspec `+refs/heads/*:refs/heads/*`,默认强制覆盖远端。 |
33| B10 | `gitssh/handler/command.rs:109-118` | `invalid_command_error` 发送 channel_failure 后仍返回 `russh::Error::IO`,会终止整个会话而非仅关闭该通道,客户端一次非法命令直接断连。 |
34| B11 | `gitsmart/handler/lfs.rs:31` | batch 端点无条件要求 Write 权限,`operation=Download` 的只读 LFS 获取(clone/checkout)被拒绝。 |
35| B12 | `gitsmart/handler/mod.rs:155` | `url_decode` 对非字符边界切片会 panic;且 `byte as char` 是 latin-1 而非 UTF-8 解码。 |
36| B13 | `git/refs/usecase.rs:63-65` | 遇符号引用(如 `refs/remotes/origin/HEAD`)整表报错而非跳过,`refs_list`/`branches` 全挂。 |
37
38**修复状态:B1-B13 全部 ✅ 已修复**(B1 `is_fast_forward` 改 fail-closed:无法证明快进即视为 force,注释说明 pre-receive 阶段对象未入库无法 merge-base,并新增 `zero_oid`/`oid_len_of` 支持 SHA-256 / B2 Heartbeat 实现 `Drop` 内 `abort()`,panic 逃逸时心跳停止不再永久续租锁 / B3 `watch(false)` + 失败 `send(true)`、`HeartbeatGuard` Drop abort / B4 `block_on_safe`:MultiThread flavor 用 `block_in_place`,否则新建 runtime / B5 统一 `requeue_or_dlq`:attempts 超限送 DLQ、入队失败返回 Err / B6 事务内重读 fresh model 再 `+new_commits` / B7 负时间戳 `checked_sub` 防溢出 / B8 tag 验证用不含签名的重序列化 / B9 refspec 去掉 `+` 强推前缀 / B10 失败仅关通道不返回 Err 断会话 / B11 LFS batch 先解码再按 `operation` 区分权限 / B12 `url_decode` 按字节解析 %XX + UTF-8 / B13 符号引用跳过不整表报错)。
39
40## 3. 中等 Bug(B2)
41
42| # | 位置 | 问题 |
43| --- | --- | --- |
44| B14 | `git/gc/usecase.rs:59` | `Write::write_buf(&repo,...)` 把对象真实写回 odb,"fsck" 实际是写操作且膨胀松对象。 |
45| B15 | `git/graphql/types.rs:608-612` | Tag 类型判定反了:annotated tag 指向 commit 时标成 Lightweight。 |
46| B16 | `git/graphql/query.rs:156-158` | 默认 log 忽略 `after` 游标;`log/usecase.rs:51` 返回 hex 游标而 `parse_cursor` 按 usize 解析,分页游标无法回传。 |
47| B17 | `git/diff/usecase.rs:208-212` | short_stat 加/删恒 0,`comparison` 恒返回 Identical;`diff/usecase.rs:146,231` 删除文件的 name/head_path 为空串。 |
48| B18 | `git/tag/usecase.rs:13-38` | 非 `refs/` 前缀的 revision 一律当 tag 名,无法按 SHA/分支打 tag。 |
49| B19 | `git/archive/usecase.rs:50 vs 68` | 读 `object.pool.*` 写 `{prefix}.*`,缓存键不一致永远 miss。 |
50| B20 | `git/pack/usecase.rs:34` | object_count 用 stdout 行数(实为 1 行 pack 路径),非对象数。 |
51| B21 | `git/conflict/usecase.rs:96-145,236-254` | `conflict_check` 仅做 base..head 单边 diff,未做三方比较;`conflict_detail` 丢弃结果恒返回空。 |
52| B22 | `git/tree/usecase.rs:200-231` | `--name-only` 输出特殊文件名带引号未解引用,last_updated 匹配不上回退默认值。 |
53| B23 | `gitsmart/pre_receive.rs:23` | `accepted_only` 用 `find` 消费迭代器,全部 Accept 时返回空 Vec(当前调用方丢弃返回值才未触发)。 |
54| B24 | `gitsmart/protocol/rpc.rs:119` | 客户端断开时子进程 git 不 kill、reaper 不 await,产生僵尸进程;stdin 任务 JoinHandle 直接丢弃。 |
55| B25 | `gitsmart/push.rs:184` | 心跳 renew 返回 `Ok(false)`(锁已易主)时仍继续循环不退出。 |
56| B26 | `gitssh/handler/channel.rs:29-45` | EOF 路径取走 state 后不删除 map 条目,仅 close 移除;客户端半关闭时条目残留泄漏。 |
57| B27 | `gitssh/handler/command.rs:83` | `run_advertise(...).unwrap_or_default()` 吞掉 git 失败,客户端收到空 advertisement 和 channel_success,错误难以诊断。 |
58| B28 | `gitsmart/handler/mod.rs:57` | HEAD 请求不满足 `is_get`,`info/refs` 的 HEAD 返回 400。 |
59| B29 | `gitsmart/protocol/pktline.rs:9` | `debug_assert!` 在 release 下失效,超长行写出 5 位十六进制长度破坏协议帧。 |
60| B30 | `gitssh/error.rs:32` | `SmartError::Conflict` 映射为 `AuthRejected`,语义不符。 |
61
62**修复状态:B14-B30 全部 ✅ 已修复**(B14 fsck 改 `compute_hash` 纯内存校验不写 odb / B15 Tag 类型直接判 Annotated / B16 HEAD 分支补 skip、log 游标改数字语义 / B17 deleted 文件 name 用 base_path、files_changed 非空路径、short_stat 用 `--numstat` 解析 / B18 `resolve_commit` 不再强制加 `refs/tags/` 前缀 / B19 archive 读缓存改 `get_sync` 统一键空间 / B20 对象数从 `.idx` `num_objects()` 读取 / B21 提取 `threeway_conflicts` 三方比较 + `conflict_detail` 填充 DiffFile / B22 新增 `unquote_path` 解 C 转义 / B23 `accepted_only` 循环收集遇 Reject 即返回 Err / B24 rpc 流 `Drop` 中 abort stdin_task + `start_kill` 子进程 / B25 renew `Ok(false)|Err(_)` 即 break / B26 `channel_eof_impl` 用 `remove` 删 map 条目 / B27 `run_advertise` 改 match,失败发错误信息 / B28 `info/refs` 支持 HEAD(`GET||HEAD`)/ B29 `debug_assert!`→`assert!` / B30 Conflict 映射改 `Internal`)。
63
64## 4. 性能(P)
65
66| # | 位置 | 问题 |
67| --- | --- | --- |
68| P1 | `gitsync/sync.rs:122-123` | 每次 push 同步串行跑 fsck+gc,大仓库长时间阻塞单个消费者。 |
69| P2 | `gitsync/languages.rs:102-110` | `timeout` 包裹 `spawn_blocking`,超时后阻塞任务仍在后台跑不被回收;`detect_languages` 在 DB 事务内执行(sync.rs:118),慢仓库长时间持锁持事务。 |
70| P3 | `gitsync/commits.rs:85-98` | 每次 sync 全量 `SELECT sha` 载入内存,大仓库内存/DB 压力无界增长;`commits.rs:166-169` `LowerEmail` IN 列表可达 2 万,逼近 PG 绑定参数上限。 |
71| P4 | `gitsync/worker.rs:221` | `sleep(5s)` 在 handler 内执行阻塞该消费者。 |
72| P5 | `git/analysis/usecase.rs:92-95` | 语言统计对每个 blob 全量解压仅取长度;`tree/usecase.rs:21` 每 entry 一次 odb 查找。 |
73| P6 | `git/blob/usecase.rs` + `graphql/types.rs:365-377` | 100MB 级 blob 全量字节以不可过期 TTL 进内存缓存(OOM 风险),且 base64 双份拷贝。 |
74| P7 | `git/graphql/query.rs:148` | tags 一次拉全部再内存分页;`tag/usecase.rs:101-107` 每 tag 一次 find_reference。 |
75| P8 | `gitssh/handler/channel.rs:8,229` | `MAX_SSH_CHANNEL_BUF = 1GiB` 每通道且无全局限制;`wait_with_output` 将全部 stdout+stderr 读入内存,大 fetch/push 内存耗尽 DoS。 |
76| P9 | `gitsmart/protocol/rpc.rs:136` | `run_rpc_bytes` 将整个 pack 输出缓冲进内存且无上限。 |
77
78**修复状态:P8 ✅ 已修复**(`MAX_SSH_CHANNEL_BUF` 1GiB→64MB + `MAX_GIT_OUTPUT_BUF` 256MB 上限受控读取 + body 上限下调);**P1-P7、P9 ⏸ 推迟**(性能项按 §6 第三梯队"逐步优化",本轮不强制)。
79
80## 5. 质量 / 死代码(Q)
81
82- `gitsync/post_receive.rs:27-29`:已存在用户 hook 时静默跳过不链式调用,capture 永远为空、push 不入队,无告警。
83- `gitssh/handler/channel.rs:119-124`:`prepare_post_receive` 失败仅 warn,push 继续执行。
84- `gitssh/handler/auth.rs:35-37`:每次公钥认证全量加载用户所有 key 再逐个比对,无 key 数量上限。
85- `gitssh/git_service.rs:74`:路径校验 `repo.contains("/../")` 未覆盖 `"a/.."` 结尾形式(当前由 DB 解析 UUID 拼接,风险低)。
86- `gitlfs` lock/unlock/cleanup、`git` pipeline 全模块、hook pre_receive、log_graph lanes、submodule url、`inject_repo`(query.rs:43)、`gitsync/webhook.rs` 出站 dispatch 均为占位/Noop 实现,应显式拒绝而非静默返回成功。
87- `codesearch/lib.rs` 为空 crate;`gitauth/permission.rs:31` `channel_role=None`、`gitlfs/store.rs:8` `SizeMismatch`/`presign_*`、`gitssh/handler/mod.rs:207` `_PathBuf` 等死代码。
88- `git` 的 `walk_loose_objects` 与 `count_loose_objects` 重复实现。
89- `gitauth/basic.rs:13`:`strip_prefix("Basic ")` 大小写敏感,违反 RFC 7617。
90- `gitsync/lock.rs:54`:lock value 含 `process::id()`,无实例标识,多副本排障困难。
91
92**修复状态**:✅ 已修:post_receive 已有 hook 时 `error!` 告警;`gitauth/permission.rs:31` 死代码删除;`NoopPushBackend` 改显式拒绝(无 Redis 时 push 报错而非静默无锁);`NoopWebhookTx` 改 `warn!` 日志;`gitssh build_git_command` 死代码删除(改用 `build_git_advertise_command`);LFS batch 权限按 operation 区分;pipeline 已确认显式 `Err` 拒绝;codesearch 空 crate 与 `store.rs` 确认无害。⏸ 低优先遗留:auth.rs key 全量加载无上限、`git_service.rs` 的 `"a/.."` 结尾路径、`prepare_post_receive` 失败仅 warn、`mod.rs:207` `_PathBuf` 死代码、`walk_loose_objects`/`count_loose_objects` 重复、`basic.rs` `strip_prefix` 大小写敏感(RFC 7617)、lock value 实例标识。
93
94## 6. 修复优先级建议
95
961. **第一梯队(阻断生产)**:S1 参数注入、S2 路径穿越、B2 push 锁永久死锁、S3/B1 pre-receive fail-open 与快进误判、B4 block_on panic、B3+B5+B6 gitsync 任务丢失链。
972. **第二梯队**:S4 环境加固落地、B10 断连、S7/P8 内存上限、B9 默认强推 refspec、B14 fsck 写操作。
983. **第三梯队**:占位模块显式拒绝、死代码清理、性能项逐步优化。
Modified wiki/llmprovider.md wiki/llmprovider.md +109 −61 View wiki/llmprovider.md
@@ -1,5 +1,24 @@
11# LLM Provider 规划
22
3## 目录
4
5- 1. 目标
6- 2. 当前代码现状
7- 3. 推荐架构
8- 4. 数据模型与职责划分
9- 5. 对外 HTTP API(OpenAI-compatible,仅外部客户端使用)
10- 6. 内部 RPC 接口(gRPC)
11- 7. 路由选择与会话亲和性
12- 8. 审计与计费(严格实现)
13- 9. 统一 200K 上下文策略
14- 10. 安全与运维
15- 11. 计划修改的文件
16- 12. 实施阶段
17- 13. 验证标准
18- 14. 决策记录(已确认)
19
20---
21
322## 1. 目标
423
524完成 `agent/lprovider` 模块,基于 Axum + tonic 构建本平台统一的 LLM Provider 服务,**作为独立集群进程运行**(多实例水平扩展,对外无差别),具备以下能力:
@@ -22,6 +41,8 @@
2241- **统一内部模型别名固定为 `auto`**:调用方无需关心具体上游模型,由 lprovider 内置选择器根据请求能力、token 预算、积分单价、优先级和钱包余额,自动挑选最优上游与 Key。
2342- 多实例集群部署:任一实例可独立处理请求;共享状态经 Redis + DB 协调;管理面写操作需选主或对等互斥。
2443
44---
45
2546## 2. 当前代码现状
2647
2748### 2.1 Workspace 与 lprovider
@@ -33,7 +54,7 @@
3354- 当前 Axum 版本为 `0.8.9`,Tokio 使用 full features。
3455- Workspace 已有 `serde`、`serde_json`、`futures`、`reqwest`、`tracing` 等依赖。
3556- Workspace 已有 `redis = "1.5.0"`(含 tokio-comp / cluster-async / sentinel),封装于 `lib/cache::RedisCache` 与 `RedisConn`。
36- Workspace 已有 `lib/db`(基于 sea-orm 2.x,schema-sync 迁移机制,见 §11),可复用。
57- Workspace 已有 `lib/db`(基于 sea-orm 2.x,通过 schema-sync 机制同步 §4.3 表结构,文件清单见 §11),可复用。
3758- Workspace **暂无 tonic/gRPC 依赖**,需新增。
3859- Workspace **暂无 `lib/secret` 模块**:本文档假设的凭据引用解析、envelope 加密与 mTLS 证书注入能力需要**新建该 crate**(见 §11 / §12 阶段一)。
3960
@@ -60,6 +81,8 @@
6081- `lib/webapi/services.rs` 与 `lib/webapi/router.rs` 当前没有 lprovider 状态和路由。
6182- `app/apihost` CLI 当前提供 `--config`、`--bind` 和 `--port` 参数。
6283
84---
85
6386## 3. 推荐架构
6487
6588### 3.1 运行形态(独立集群进程)
@@ -67,7 +90,7 @@
6790`agent/lprovider` 作为**独立集群进程**运行:
6891
6992- 同一二进制可在多台主机 / 多个 Pod 启动多个实例,对外通过 L4/L7 负载均衡器(Envoy / Nginx / K8s Service / 云厂商 LB)统一暴露。
70- 每个实例都同时承担:**对外 HTTP(OpenAI-compatible)** + **内部 RPC(gRPC 管理)** 两个端口。
93- 每个实例都同时承担三个端口:**对外 HTTP(OpenAI-compatible,8080)**、**内部 RPC AI(gRPC,9090,无鉴权)**、**管理 RPC(gRPC + 强制 mTLS,9443)**。
7194- 实例之间**不直接通信**:共享状态完全由 Redis(会话亲和、冷却)与 DB(审计/计费)协调;配置文件由外部配置中心下发或由管理 RPC 写本地后经集群滚动重启同步。
7295- 启动形式:
7396
@@ -75,7 +98,7 @@
7598cargo run -p lprovider -- \
7699 --config /etc/lprovider/config.yaml \
77100 --bind 0.0.0.0 --port 8080 \
78 --admin-bind 0.0.0.0 --admin-port 9090 \
101 --admin-bind 0.0.0.0 --admin-port 9443 \
79102 --instance-id $(hostname) \
80103 --cluster-name lprovider-prod
81104```
@@ -106,6 +129,8 @@
106129 └── tracing
107130```
108131
132---
133
109134## 4. 数据模型与职责划分
110135
111136**上游数据完全由配置文件承载,不写入数据库**。会话亲和性绑定由 Redis 承载。**落库的只有:审计、计费、钱包(余额与流水)、租户额度、管理操作审计**,且要求严格。
@@ -119,13 +144,15 @@
119144| 平台统一模型别名 → 上游映射 | 是 | 否 | 否 |
120145| 路由权重 | 是 | 否 | 否 |
121146| 上游 Key 明文 / 引用 | 是(引用) | 否 | 否 |
122| 运行时 Key 冷却状态 | 否 | 是(短期) | 否(最终落库用于审计) |
147| 运行时 Key 冷却状态 | 否 | 是(短期) | 否 |
123148| 会话亲和性绑定 | 否 | 是 | 否 |
124149| 调用审计 / 计费 | 否 | 否 | 是 |
125150| 钱包余额 / 流水 | 否 | 否 | 是 |
126151| 租户 / 主体额度 | 否 | 否 | 是 |
127152| 管理操作审计 | 否 | 否 | 是 |
128153
154> 平台统一模型别名当前**仅保留 `auto`**;请求在 `model` 字段填写非 `auto` 的值时,按上游 `code` 直填处理(跳过自动选择,见 §7.1 / §7.2),不视为别名解析。
155
129156### 4.2 Redis 键空间(会话亲和性)
130157
131158参考 [DeepSeek KV Cache 文档](https://api-docs.deepseek.com/zh-cn/guides/kv_cache):上游按“缓存前缀单元(cache prefix unit)”匹配,前缀必须**完整**命中才能复用 KV 缓存;命中情况通过响应 `usage.prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` 反馈;缓存为“尽力而为”,几小时到几天被淘汰。
@@ -134,16 +161,17 @@
134161
135162| 键 | 类型 | 值 / TTL |
136163| ----------------------------------------------- | ------ | ------------------------------------------------------- |
137| `lprovider:bind:{model_alias}:{session_key}` | STRING | JSON `{upstream_code, key_label, model, prefix_hash, bound_at, last_hit_tokens, hit_count}` |
138| TTL | | 默认 `default_ttl_seconds = 3600`,命中 ≥ 50% 续约一周期 |
164| `lprovider:bind:{tenant_id}:{model_alias}:{session_key}` | STRING | JSON `{upstream_code, key_label, model, prefix_hash, bound_at, last_hit_tokens, hit_count}`(键含 `tenant_id`,跨租户隔离) |
139165| `lprovider:bind_idx:upstream:{upstream_code}` | SET | 该上游下当前活跃绑定 session_key(用于上游失效时批量清理) |
140166| `lprovider:bind_idx:key:{upstream_code}:{key_label}` | SET | 该 Key 下当前活跃绑定 session_key |
141167
168绑定 TTL:默认 `default_ttl_seconds = 3600`,命中 ≥ 50% 续约一个周期(§7.7.4);到期由 Redis TTL 自动删除。
169
142170失效与清理:
143171
144- 后台定时任务(每分钟)扫描 `lprovider:bind:*` 并清理 `expires_at < now()` 的绑定。
172- 绑定到期由 Redis TTL 自动删除;后台定时任务(每分钟)扫描 `lprovider:bind:*`,淘汰长期无命中的绑定并同步维护 `bind_idx`。
145173- 当某个 `(upstream_code, key_label)` 触发 cooldown 或被管理接口禁用时,按 `bind_idx` 批量删除对应绑定。
146- 管理接口 `invalidate_binding(model_alias, session_key)` 强制失效。
174- 管理接口 `invalidate_binding(tenant_id, model_alias, session_key)` 强制失效。
147175
148176### 4.3 数据库表(审计、计费、钱包、额度)
149177
@@ -253,7 +281,7 @@
253281| `last_consume_at` | TIMESTAMPTZ | |
254282| `created_at`/`updated_at` | TIMESTAMPTZ | |
255283
256唯一约束:`(owner_type, owner_id, wallet_id)`;查询时优先按 `wallet_id` 命中。
284唯一约束:`(owner_type, owner_id)`(一个主体至多一个钱包;`wallet_id` 为主键,无需重复进唯一约束);查询时优先按 `wallet_id` 命中。
257285
258286状态迁移(仅管理 RPC 可变更状态,请求路径不改状态):
259287
@@ -295,8 +323,6 @@
295323| `operator` | TEXT | |
296324| `created_at` | TIMESTAMPTZ | |
297325
298额度检查在请求进入路由前完成,超额请求直接返回 `QuotaExceeded` 错误。
299
300326#### 4.3.6 租户 / 主体额度表 `llm_tenant_quota`
301327
302328额度数据**落库**(区别于上游配置不落库),由 `LproviderAdmin.UpsertTenantQuota` 管理:
@@ -314,7 +340,7 @@
314340| `enabled` | BOOLEAN NOT NULL| |
315341| `created_at`/`updated_at` | TIMESTAMPTZ | |
316342
317唯一约束:`(tenant_id, subject_type, subject_id)`。额度消耗量实时来自 `llm_billing_aggregate`(当月周期)或 `llm_call_log` 增量查询,不单独维护计数器表。
343唯一约束:`(tenant_id, subject_type, subject_id)`。额度消耗量实时来自 `llm_billing_aggregate`(当月周期)或 `llm_call_log` 增量查询,不单独维护计数器表。额度检查在请求进入路由前完成,超额请求直接返回 `QuotaExceeded` 错误。
318344
319345#### 4.3.7 管理操作审计表 `lprovider_admin_audit`
320346
@@ -364,8 +390,9 @@
364390 # 整体计费采用积分制,1 point = 1 积分(整数,避免浮点精度问题)
365391 # 单价单位:积分 / 1K token;调用时按实际 token 数换算为积分(向上取整,见 §8.3)
366392 # key 是实际路由到的上游 code(upstreams[*].code),不是 alias;
367 # auto 模式下由选择器选中的上游直接作为 pricing key,确保计费维度与路由维度一致。
368 upstreams:
393 # auto 模式下由选择器选中的上游直接作为 rates key,确保计费维度与路由维度一致。
394 # 注意:此处是单价映射(rates),与 lprovider.upstreams 的上游列表是两个不同层级,勿混淆。
395 rates:
369396 openai-gpt4o:
370397 points_per_1k_input_tokens: 100
371398 points_per_1k_output_tokens: 300
@@ -440,6 +467,8 @@
440467
441468凭据通过 `credential_ref` 引用环境变量、Secret 或 KMS,运行时由 `lib/secret` 解密;绝不明文落盘或不加脱敏地输出日志。
442469
470---
471
443472## 5. 对外 HTTP API(OpenAI-compatible,仅外部客户端使用)
444473
445474对外 HTTP 仅服务**外部客户端**(第三方网关、浏览器直接调用、用户脚本等),协议与 OpenAI Chat Completions API 兼容;平台内部服务统一使用 §6.1 的 `LproviderAI` gRPC。
@@ -478,6 +507,8 @@
478507- 集群模式下 L4/L7 LB 应基于 `/health/ready` 摘除异常实例。
479508- **上游可用性独立于 `/health/ready`**:所有上游同时进入 cooldown 不应让 LB 摘除全部实例;上游可用性通过单独的 `RuntimeStatus.upstream_summary`(gRPC 查询)与告警机制反映。
480509
510---
511
481512## 6. 内部 RPC 接口(gRPC)
482513
483514内部 RPC 通过 **gRPC(tonic)** 暴露,**按服务分端口**与对外 HTTP 完全隔离:
@@ -593,7 +624,7 @@
593624
594625message WalletRef {
595626 int64 wallet_id = 1; // 调用方显式选择的钱包 id
596 // 服务端会校验(与 §7.5.1 / §10.2 R-1 口径一致):
627 // 服务端会校验(与 §7.6.1 / §10.2 R-1 口径一致):
597628 // 1. wallet 存在且 status='active'
598629 // 2. owner 属于调用方 tenant_id(或调用方为 admin)
599630 // 3. 最坏情况预校验:balance_points >= estimated_max_cost
@@ -1093,7 +1124,7 @@
10931124
10941125### 6.5 凭据回显规则
10951126
1096`GetUpstreamKey` 响应**只返回** `credential_ref` 和 `key_fingerprint`,绝不返回明文或可解密的密文。`GetConfig` 默认脱敏(`mask_secrets=true`),所有 `credential_ref` 引用的真实值在响应中替换为 `***`。轮换密钥必须通过 `RotateUpstreamKey` RPC 完成(首版可选;密钥轮换可由外部 secret manager 触发)。
1127`GetUpstreamKey` 响应**只返回** `credential_ref` 和 `key_fingerprint`,绝不返回明文或可解密的密文。`GetConfig` 默认脱敏(`mask_secrets=true`),所有 `credential_ref` 引用的真实值在响应中替换为 `***`。密钥轮换由外部 secret manager 负责(决策 #14);lprovider 仅消费 `credential_ref`,无需提供轮换 RPC。
10971128
10981129### 6.6 集群模式下的配置与重启协调
10991130
@@ -1108,17 +1139,19 @@
11081139- **配置漂移防护**:每个实例启动时记录 `loaded_sha256` 到 Redis `lprovider:config_sha:{instance_id}`;运维平台可对比所有实例的 SHA,发现漂移时触发统一重载。
11091140- **重启标识**:实例启动时将 `(instance_id, started_at, pid, listen_addr)` 写入 Redis `lprovider:instances:{cluster_name}`(TTL 24h),用于 `ListInstances` 查询与存活检测。
11101141
1142---
1143
11111144## 7. 路由选择与会话亲和性
11121145
1113### 7.0 HTTP 与 RPC 共用同一选择器与运行时
1146### 7.1 概述:HTTP 与 RPC 共用同一选择器与运行时
11141147
1115- 对外 HTTP(OpenAI-compatible)与内部 RPC(`LproviderAI`)共用 §7.1 ~ §7.7 的能力筛选 / 加权轮转 / 亲和绑定 / 降级轮转 / 配额账本,避免双实现。
1148- 对外 HTTP(OpenAI-compatible)与内部 RPC(`LproviderAI`)共用 §7.2 ~ §7.8 的能力筛选 / 加权轮转 / 亲和绑定 / 降级轮转 / 配额账本,避免双实现。
11161149- HTTP 路径在边缘做 OpenAI ↔ `ChatRequestMsg` 转换、JSON ↔ SSE 适配;RPC 路径直接消费 proto 类型。
11171150- HTTP 与 RPC 路径的请求都进入同一 `llm_call_log` 与配额账本,但 `caller` 维度(`http` / `service_name`)分开统计,便于内部成本归因。
1118- 亲和绑定以 `model_alias` 为键,跨 HTTP / RPC 共享;同一租户不同入口的会话可复用上游 KV 缓存。
1119- **默认 `model_alias = auto`**,由 §7.1 选择器自动挑选上游;调用方也可在 `model` 字段直接填写某个已声明的上游 `code`(如 `openai-gpt4o`)跳过自动选择。
1151- 亲和绑定以 `tenant_id` + `model_alias` 为键,跨 HTTP / RPC 共享;同一租户不同入口的会话可复用上游 KV 缓存,跨租户互不可见。
1152- **默认 `model_alias = auto`**,由 §7.2 选择器自动挑选上游;调用方也可在 `model` 字段直接填写某个已声明的上游 `code`(如 `openai-gpt4o`)跳过自动选择。
11201153
1121### 7.1 自动模型选择器(`auto`)
1154### 7.2 自动模型选择器(`auto`)
11221155
11231156`auto` 是保留的内部模型别名,触发如下自动选择流程:
11241157
@@ -1154,10 +1187,10 @@
11541187 d. 默认 BALANCED
11551188 │
11561189 ▼
11575. 在选中的上游内通过 §7.4 加权 Round Robin 选出具体 Key
11905. 在选中的上游内通过 §7.5 加权 Round Robin 选出具体 Key
11581191 │
11591192 ▼
11606. 调用 + 亲和绑定(§7.6)+ 降级轮转(§7.7)
11936. 调用 + 亲和绑定(§7.7)+ 降级轮转(§7.8)
11611194```
11621195
11631196要点:
@@ -1168,7 +1201,7 @@
11681201- 调用方在 `model` 字段直接填 `openai-gpt4o` 时跳过自动选择,但仍走能力筛选、加权 Round Robin、亲和、降级轮转。
11691202- 自动选择结果(`chosen_strategy` / `score_breakdown` / `tier_*`)写入 `llm_call_log`,便于事后审计「为什么这次请求被路由到这个上游」。
11701203
1171### 7.2 统一模型别名 → 上游池
1204### 7.3 统一模型别名 → 上游池
11721205
11731206候选实例属性:
11741207
@@ -1180,7 +1213,7 @@
11801213- 最大上下文与最大输出能力。
11811214- 当前 in-flight 数、连续失败次数、冷却截止时间(Redis 短期状态 + 定期回写到内存快照)。
11821215
1183### 7.3 运行时快照
1216### 7.4 运行时快照
11841217
11851218采用 `Arc<tokio::sync::RwLock<...>>` 管理运行时状态:
11861219
@@ -1189,7 +1222,7 @@
11891222- 配置变更时构造完整新快照并整体替换。
11901223- 请求持有当前快照的 `Arc`,配置热更新不影响已开始的请求。
11911224
1192### 7.4 能力自动筛选
1225### 7.5 能力自动筛选
11931226
11941227每次路由前根据请求推导所需能力集合:
11951228
@@ -1211,11 +1244,11 @@
121112445. 剩余候选按加权 Round Robin 选取;权重相等时取 in-flight 最小者打破平局。
121212456. 候选为空时返回 `ProviderUnavailable` 错误。
12131246
1214### 7.5 计费:钱包校验与扣减
1247### 7.6 计费:钱包校验与扣减
12151248
12161249请求进入路由前根据 `WalletSelector` 模式分流:
12171250
1218#### 7.5.1 HTTP 路径(强制钱包)
1251#### 7.6.1 HTTP 路径(强制钱包)
12191252
122012531. 从 `X-Wallet-Id` header 解析 `wallet_id`;缺失或非法立即返回 `WalletRequired`(HTTP 401/402),**HTTP 不允许匿名**。
122112542. 查询 `llm_wallet`,校验 `status='active'`、`owner` 属于调用方 `tenant_id`(admin 调用方跳过 owner 校验)。
@@ -1223,37 +1256,37 @@
122312564. 调用完成后**仅实扣**:成功请求按实际 `points_total` 一次性原子扣减(不预扣、不补扣);失败请求**不扣减**、不计费。
122412575. 实扣 SQL:`UPDATE llm_wallet SET balance_points = balance_points - $actual, total_consumed = total_consumed + $actual, last_consume_at = now() WHERE wallet_id=$id AND status='active' AND balance_points >= $actual RETURNING balance_after`;扣减失败(并发耗尽余额)记录 `llm_call_log.status='error'` 与 `error_kind='wallet_debit_failed'`,不再二次重试,上游成本损失按 R-1 告警人工对账。
12251258
1226#### 7.5.2 内部 RPC AI 路径(钱包或匿名)
1259#### 7.6.2 内部 RPC AI 路径(钱包或匿名)
12271260
122812611. 从 `ChatRequestMsg.wallet_selector` 解析模式:
1229 - `Anonymous`:跳到 §7.5.3。
1262 - `Anonymous`:跳到 §7.6.3。
12301263 - `Wallet`:按 HTTP 相同流程校验与扣减。
123112642. 匿名时同样校验 `tenant_id` 一致,但**不查询 `llm_wallet`、不扣减**。
123212653. 内部 RPC **不做调用方鉴权**(信任集群内部网络);调用方由请求中 `tenant_id` + `caller_service` 标识,跨租户访问在 owner 校验阶段拒绝。
12331266
1234#### 7.5.3 匿名模式(仅内部 RPC AI)
1267#### 7.6.3 匿名模式(仅内部 RPC AI)
12351268
12361269- 不扣减、不计费、不校验钱包。
12371270- 在 `llm_call_log` 写 `anonymous=true`、`wallet_id=NULL`、`points_total=0`、`points_rate_*` 仍记录用于成本可视化。
12381271- 上游真实消耗仍计入平台总成本;运营通过单独报表(`total_cost_by_upstream`)观察匿名流量占比。
12391272- **无独立速率限制**(信任内部调用方);仅依赖 `subject_path` 告警 + 全平台配额告警。
12401273
1241#### 7.5.4 钱包 owner 校验
1274#### 7.6.4 钱包 owner 校验
12421275
12431276- 调用方为 admin 时跳过 owner 校验但仍校验 status 与余额。
12441277- 调用方非 admin 时,必须满足 `wallet.tenant_id == caller.tenant_id` **且** (`wallet.owner_type == 'user' && wallet.owner_id == caller.user_id` **或** `wallet.owner_type == 'space' && caller 是该 space 的成员 / 管理员,通过 `lib/space` 实时校验`)。
12451278- 跨租户访问钱包一律拒绝并记录 `lprovider_admin_audit` 与 `llm_call_log` 错误。
12461279
1247#### 7.5.5 并发安全
1280#### 7.6.5 并发安全
12481281
12491282- 钱包扣减完全依赖 SQL 条件 `balance_points >= $delta` + `RETURNING balance_after`,由数据库原子保证;不在应用层做分布式锁。
12501283- 同一 `request_id` 的 `consume` 流水通过 `llm_wallet_ledger (request_id, kind)` 唯一约束防止重复落账。
12511284
1252### 7.6 会话亲和性(基于上游 KV 缓存 + Redis)
1285### 7.7 会话亲和性(基于上游 KV 缓存 + Redis)
12531286
1254参考 [DeepSeek KV Cache](https://api-docs.deepseek.com/zh-cn/guides/kv_cache) 语义:上游按“缓存前缀单元”匹配,请求前缀必须**完整**命中才能复用 KV 缓存;命中情况通过响应 `usage.prompt_cache_hit_tokens` 反馈;缓存为“尽力而为”,数小时到数天被淘汰。
1287复用 §4.2 所述的 KV 缓存语义:上游按“缓存前缀单元”匹配,请求前缀必须**完整**命中才能复用 KV 缓存;命中情况通过响应 `usage.prompt_cache_hit_tokens` 反馈;缓存为“尽力而为”,数小时到数天被淘汰。
12551288
1256#### 7.6.1 会话键 `session_key` 计算
1289#### 7.7.1 会话键 `session_key` 计算
12571290
12581291```text
12591292session_key = SHA256(
@@ -1267,20 +1300,21 @@
12671300- `prefix_tokens` 取 `system` 消息 + 最早的若干 `user` 消息,按 `prefix_token_budget`(默认 512)截断。
12681301- 当 `prefix_tokens` 长度 < `min_prefix_tokens`(默认 64)时**不建立绑定**,走普通轮转。
12691302- 客户端可通过 `X-Session-Key` header 显式覆盖亲和键;覆盖值必须做格式校验(长度、字符集),避免注入。
1303- 绑定键包含 `tenant_id`(`lprovider:bind:{tenant_id}:{model_alias}:{session_key}`),不同租户的亲和绑定天然隔离,防止跨租户共享或污染。
12701304
1271#### 7.6.2 命中流程
1305#### 7.7.2 命中流程
12721306
12731. 收到请求后计算 `session_key`,并按模型别名构造候选集(见 §7.4)。
12742. 在 Redis `lprovider:bind:{model_alias}:{session_key}` 查询:
13071. 收到请求后计算 `session_key`,并按模型别名构造候选集(见 §7.5)。
13082. 在 Redis `lprovider:bind:{tenant_id}:{model_alias}:{session_key}` 查询:
12751309 - **命中且 `(upstream_code, key_label)` 仍在候选集**:复用。
12761310 - **命中但实例已不可用**:回退普通轮转,并异步删除该绑定。
1277 - **未命中**:走 §7.4 的加权轮转;调用成功后建立绑定。
12783. 调用完成后依据响应 `prompt_cache_hit_tokens` 调整绑定(见 §7.6.4)。
1311 - **未命中**:走 §7.5 的加权轮转;调用成功后建立绑定。
13123. 调用完成后依据响应 `prompt_cache_hit_tokens` 调整绑定(见 §7.7.4)。
12791313
1280#### 7.6.3 绑定建立(Redis Lua 原子)
1314#### 7.7.3 绑定建立(Redis Lua 原子)
12811315
12821316```lua
1283-- KEYS[1] = lprovider:bind:{model_alias}:{session_key}
1317-- KEYS[1] = lprovider:bind:{tenant_id}:{model_alias}:{session_key}
12841318-- KEYS[2] = lprovider:bind_idx:upstream:{upstream_code}
12851319-- KEYS[3] = lprovider:bind_idx:key:{upstream_code}:{key_label}
12861320-- ARGV = payload_json, ttl_seconds, expire_at_unix, session_key
@@ -1290,26 +1324,26 @@
12901324return 1
12911325```
12921326
1293#### 7.6.4 命中续约
1327#### 7.7.4 命中续约
12941328
12951329调用完成后读取上游响应 `usage.prompt_cache_hit_tokens`:
12961330
1297- `cache_hit_tokens / (cache_hit_tokens + cache_miss_tokens) >= 0.5`:
1331- `cache_hit_tokens + cache_miss_tokens > 0` 且 `cache_hit_tokens / (cache_hit_tokens + cache_miss_tokens) >= 0.5`(两者均为 0 时不续约、保持现有绑定):
12981332 - 续约 TTL 到 `now() + default_ttl_seconds`。
12991333 - `last_hit_tokens = cache_hit_tokens`,`hit_count += 1`。
13001334- `cache_hit_tokens == 0` 且 `cache_miss_tokens > min_prefix_tokens`:
13011335 - 视为缓存已被淘汰,缩短 TTL 至 `default_ttl_seconds / 4`,下一次请求重新轮转。
13021336- 持续命中失败且 `hit_count < 3` 时,由后台清理任务直接删除该绑定。
13031337
1304#### 7.6.5 失效与清理
1338#### 7.7.5 失效与清理
13051339
1306- 后台定时任务(每分钟)清理 `expires_at < now()` 的绑定。
1340- 绑定到期由 Redis TTL 自动删除;后台定时任务(每分钟)扫描 `lprovider:bind:*`,清理长期无命中的绑定。
13071341- 当 `(upstream_code, key_label)` 触发 cooldown 或被管理接口禁用时,按 `bind_idx` 批量删除对应绑定。
1308- 管理接口 `invalidate_binding(model_alias, session_key)` 强制失效。
1342- 管理接口 `invalidate_binding(tenant_id, model_alias, session_key)` 强制失效。
13091343
1310### 7.7 降级轮转
1344### 7.8 降级轮转
13111345
1312- 非流式请求在首次响应前发生 retryable 错误时,切换到 §7.4 候选集的下一个实例,并把失败 Key 的 Redis 状态写为 `cooldown_until = now() + cooldown_seconds`(默认 30s)。
1346- 非流式请求在首次响应前发生 retryable 错误时,切换到 §7.5 候选集的下一个实例,并把失败 Key 的 Redis 状态写为 `cooldown_until = now() + cooldown_seconds`(默认 30s)。
13131347- 重试应限制最大尝试次数(默认 2 次额外尝试)。
13141348- 仅对 `LlmError` 判定为 retryable 的错误执行切换:网络、限流和部分 5xx。
13151349- 认证错误、请求参数错误、模型不存在、上下文超限、租户超额不能盲目重试。
@@ -1317,6 +1351,8 @@
13171351- 流式请求一旦已输出内容,不能透明切换并重新发送完整结果;应结束流并返回可诊断错误。
13181352- 可复用现有 `FallbackProvider` 的 retryable 判断和错误语义,实例选择、冷却、权重和亲和由 lprovider 自己管理。
13191353
1354---
1355
13201356## 8. 审计与计费(严格实现)
13211357
13221358### 8.1 审计范围
@@ -1326,13 +1362,13 @@
13261362### 8.2 审计字段保证
13271363
13281364- `request_id`:HTTP 路径在服务端入口生成并贯穿全链路(写入响应 header `X-Request-Id`);RPC 路径优先使用调用方提供的 `request_id`,留空时服务端生成。
1329- `tenant_id` 为**必填**维度:HTTP 路径取自 `X-Tenant-Id` header(缺失拒绝),RPC 路径取自 `ChatRequestMsg.tenant_id`(零值拒绝)。**不存在「匿名租户」回退**——`anonymous=true` 仅出现在内部 RPC AI 显式选择 `AnonymousRef` 的场景(§7.5.3),且此时 `wallet_id=NULL`、`tenant_id` 仍必须提供。
1365- `tenant_id` 为**必填**维度:HTTP 路径取自 `X-Tenant-Id` header(缺失拒绝),RPC 路径取自 `ChatRequestMsg.tenant_id`(零值拒绝)。**不存在「匿名租户」回退**——`anonymous=true` 仅出现在内部 RPC AI 显式选择 `AnonymousRef` 的场景(§7.6.3),且此时 `wallet_id=NULL`、`tenant_id` 仍必须提供。
13301366- 错误信息按 `error_kind` 分类存储,`error_message` 截断到 512 字符并脱敏,禁止包含 API Key 或 session_key。
13311367- 上游响应原始体**不写入审计表**,只写入聚合指标与 token 数。
13321368
13331369### 8.3 计费规则(积分制)
13341370
1335- 计费模型由 `lprovider.pricing.upstreams[upstream_code]` 配置:输入、输出、缓存命中各自的积分单价(单位:积分 / 1K token)。key 与路由维度(`upstreams[*].code`)一一对应,确保 auto 模式选中上游即可查到单价。
1371- 计费模型由 `lprovider.pricing.rates[upstream_code]` 配置:输入、输出、缓存命中各自的积分单价(单位:积分 / 1K token)。key 与路由维度(`upstreams[*].code`)一一对应,确保 auto 模式选中上游即可查到单价。
13361372- 1 point = 1 积分,整数计算;**向上取整**(避免小数 token 被免费化):
13371373
13381374```text
@@ -1347,7 +1383,7 @@
13471383
13481384- 单价变更后历史请求仍按请求发生时的快照单价计费(审计表中持久化 `points_rate_*`)。
13491385- **计费在请求成功完成时计算(流式取最后一个 `usage` 事件);失败请求不扣减、不计入 `points_total`,但计入 `error_count`,便于后续统计浪费率**。
1350- 计费完成后**仅一次性原子扣减**(见 §7.5.1);不预扣、不补扣。
1386- 计费完成后**仅一次性原子扣减**(见 §7.6.1);不预扣、不补扣。
13511387- 业务侧可配置 `points_per_1k_cache_hit_tokens = 0` 实现"命中不计费"。
13521388- 钱包余额(`llm_wallet.balance_points`)通过原子扣减实现;超额请求按余额是否充足直接拒绝(HTTP 401/402);匿名请求不消耗钱包。
13531389
@@ -1364,6 +1400,8 @@
13641400- 上游整体错误率超过 5% 时,发送告警并记录到审计。
13651401- 匿名调用占比超过阈值(默认总流量 30%)时单独告警,提示可能被滥用。
13661402
1403---
1404
13671405## 9. 统一 200K 上下文策略
13681406
13691407### 9.1 网关上限
@@ -1404,6 +1442,8 @@
14041442
14051443需要明确输入 token 估算误差、安全余量以及 `max_tokens` 未提供时的默认值,避免边界请求在后端被拒绝。
14061444
1445---
1446
14071447## 10. 安全与运维
14081448
14091449### 10.1 网络分段(防御深度)
@@ -1419,8 +1459,8 @@
14191459
14201460下列风险在 v1 中**显式接受**,需要在生产环境监控并规划后续版本处理:
14211461
1422- **R-1:钱包余额不足时上游成本已发生**(§7.5.1 仅实扣 + 上游已完成)。v1 缓解措施:
1423 - 请求入口做**最坏情况预校验**:`estimated_max_cost = (estimated_input_tokens + max_output_tokens) * max(points_per_1k_*) / 1000`,超过余额立即 `InsufficientBalance` 返回;不发起上游调用(已纳入 §7.5.1 步骤 3 为强制流程,非可选缓解)。
1462- **R-1:钱包余额不足时上游成本已发生**(§7.6.1 仅实扣 + 上游已完成)。v1 缓解措施:
1463 - 请求入口做**最坏情况预校验**:`estimated_max_cost = (estimated_input_tokens + max_output_tokens) * max(points_per_1k_*) / 1000`,超过余额立即 `InsufficientBalance` 返回;不发起上游调用(已纳入 §7.6.1 步骤 3 为强制流程,非可选缓解)。
14241464 - 真正的硬性预留(pre-debit + 实际结算)纳入 v2,需要事务、补偿流与钱包锁的完整设计。
14251465 - 上游成本无法回收时记录 `llm_call_log.error_kind='wallet_debit_failed'`,触发告警并由人工对账。
14261466- **R-2:内部 RPC 身份伪造**(决策 #18)。调用方可在请求里填任意 `tenant_id` + 任意 `wallet_id`。v1 缓解措施:
@@ -1428,7 +1468,7 @@
14281468 - 由 `caller_service_tenant_acl` 配置项(白名单)做应用层校验:每个 `caller_service` 允许代表的 `tenant_id` 列表;非法组合返回 `PermissionDenied`。
14291469 - 真正的密码学绑定(mTLS 客户端证书 → tenant 映射)纳入 v2,需客户端证书 CN/SAN 体系。
14301470- **R-3:auto 模式选中的上游不在 pricing 表**。v1 缓解措施:
1431 - `pricing.upstreams` 必须覆盖所有 `enabled` 上游,启动时校验;缺失时启动失败。
1471 - `pricing.rates` 必须覆盖所有 `enabled` 上游,启动时校验;缺失时启动失败。
14321472 - `auto` 选中后查不到单价时 fallback 到 `default_per_1k` 全局兜底(也可配)。
14331473- **R-4:Cancel 与计费交互**(决策 #20)。由于不主动中断上游请求,被取消的请求若上游仍完整生成完毕,视为成功调用、正常扣积分。客户端若中途断开:
14341474 - 上游已完成 → 按成功扣积分(用户在响应中断开仍需付费)。
@@ -1439,7 +1479,7 @@
14391479
14401480首版至少包含:
14411481
1442- 对外 HTTP 可选 Bearer Token 认证。
1482- 对外 HTTP Bearer Token 认证(决策 #4,默认启用;可通过 `lprovider.auth.bearer_token` 配置开关关闭,关闭后仅限受信网络,见 §5.1 / §10.1)。
14431483- 管理 RPC 强制 mTLS。
14441484- 认证失败返回统一错误,不泄露 Provider 凭据。
14451485- API Key 通过 `lib/secret` 在运行时按引用解密,绝不明文落盘或日志输出。
@@ -1452,7 +1492,9 @@
14521492- 关闭时停止接收新请求,等待正在运行的请求在超时内结束。
14531493- **长流式 / 双向流的优雅关闭**:drain 阶段对 `ChatStream` 单向流,等待事件自然结束或客户端断开;对 `ChatStreamBidirectional` 双向流,drain 时立即返回错误事件 `StreamEventMsg { event_type="server_draining" }`,通知客户端主动重连到其他实例(客户端基于 `X-Instance-Id` header 或服务发现重路由);超过 `drain_timeout_seconds`(默认 30s)后强制断开。
14541494
1455限流、配额、计费按 §8 实现;其他细粒度租户认证复用平台能力。
1495限流与计费按 §8 实现,配额检查见 §4.3.6 / §7.6,告警见 §8.5;其他细粒度租户认证复用平台能力。
1496
1497---
14561498
14571499## 11. 计划修改的文件
14581500
@@ -1513,6 +1555,8 @@
15131555- 租户额度检查与告警测试。
15141556- 优雅关闭和并发请求测试。
15151557
1558---
1559
15161560## 12. 实施阶段
15171561
15181562### 阶段一:配置、依赖与服务骨架
@@ -1552,11 +1596,13 @@
15521596
15531597### 阶段五:安全、测试与运行验证
15541598
1555- 接入对外 HTTP Bearer Token 与 gRPC 鉴权。
1599- 接入对外 HTTP Bearer Token 与 Admin 服务强制 mTLS(AI 推理 RPC 保持无鉴权,见决策 #18)。
15561600- 完成能力筛选、会话亲和、降级轮转、上下文边界、并发、计费准确性测试。
15571601- 验证健康检查、优雅关闭和流式错误语义。
15581602- 执行 `cargo fmt`、`cargo check`、测试和 Clippy。
15591603
1604---
1605
15601606## 13. 验证标准
15611607
15621608实现完成后至少满足:
@@ -1588,13 +1634,15 @@
15881634- 钱包原子扣减依赖 SQL 条件与 `RETURNING balance_after`,并发安全由数据库保证;同一 request_id 的 consume/refund 不可重复落账。
15891635- 钱包 owner 校验:非 admin 调用方必须满足 tenant + owner 一致;跨租户访问一律拒绝。
15901636- 匿名仅允许内部 RPC AI;审计记录 `anonymous=true`、`wallet_id=NULL`、`points_total=0`,仍记录 usage / token / 缓存命中用于成本可视化。
1591- 匿名无独立速率限制(信任内部调用方);仅依赖 wallet 余额扣减与告警。
1637- 匿名无独立速率限制(信任内部调用方);仅依赖 `subject_path` 告警与全平台配额告警(§7.6.3)。
15921638- 内部 RPC AI 推理调用**不要求鉴权**(信任集群内部网络);调用方由 tenant_id + caller_service 标识即可。
1593- 审计与聚合按 `(owner_type, owner_id, wallet_id)` 与 `subject_path` 维度产出报告。
1639- 审计与聚合按 `(subject_type, subject_id)` 与 `subject_path` 维度产出报告(聚合维度与 §4.3.2 一致)。
15941640- live/ready 检查语义正确,ready 包含 Redis 与 DB 连通性。
15951641- 服务可以响应终止信号并优雅退出。
15961642- `cargo check`、测试、格式检查和 Clippy 通过。
15971643
1644---
1645
15981646## 14. 决策记录(已确认)
15991647
16001648以下决策已在 2026-08-12 由架构评审确认,作为实现基准:
Added wiki/room_bug.md wiki/room_bug.md +135 −0 View wiki/room_bug.md
@@ -0,0 +1,135 @@
1# chat 模块代码审查报告
2
3> 审查范围:`chat/` 下 4 个 crate(storagelayer / chatlayer / glayer / live),共 88 个 .rs 文件约 9500 行。
4
5## 概览
6
7| Crate | 职责 |
8|---|---|
9| storagelayer | 存储底层:models / repository / mapper / blob,权限校验核心 |
10| chatlayer | 领域层:actor / consumer / events / projection / media / service |
11| glayer | HTTP / WS 网关层:handler / ws / hub / router / protocol |
12| live | 音视频会议层:handler / service / token / webhook / client |
13
14整体架构清晰、分层合理。权限校验集中在 repository 层(ctx 强制校验 membership),事件流转采用 Kafka at-least-once + Redis `SET NX` dedup 兜底,附件上传有危险类型拦截(防存储型 XSS),LiveKit 令牌签发与 webhook 校验标准。
15
16---
17
18## 高危
19
20### 1. 群聊消息扇出是死代码,普通消息无法送达接收方
21
22`chatlayer/projection/room.rs:161-168`:
23
24```rust
25if cmd.sender_id != ctx.user_id.unwrap_or(Uuid::nil()) {
26 out.push(ChatUserEvent::MessageDelivered { user_id: cmd.sender_id, ... });
27}
28```
29
30`ctx` 由 `ChatStorageCtx::new(envelope.actor_id)` 构造(room.rs:24),而 `MessageCreated` 的 `actor_id` 恒等于发送者(service.rs `EventEnvelope::new(identity.user_id, ...)`),`cmd.sender_id` 也是发送者。因此 `cmd.sender_id != ctx.user_id` **恒为 false**,且 `user_id` 目标写的是 `cmd.sender_id`(发送者自己)。
31
32结果:`MessageDelivered` 永远不会被产出(全仓 grep 仅此一处 producer),普通群聊消息的接收方收不到 `NewMessage` / `UnreadChanged`,未读数不递增、无实时推送。目前只有被 `@提及` 的用户能通过 `MentionReceived` 收到事件(room.rs:169-179)。
33
34**这是核心投递/未读功能失效。**
35
36修复方向:应遍历会话成员(`store.conversations().list_member_user_ids`),对每个「非发送者」成员各发一条 `MessageDelivered{user_id: member}`,或修正条件与目标字段语义。
37
38---
39
40## 中危
41
42### 2. JWT 失效被错误映射为 403 而非 401
43
44`chatlayer/auth_bridge.rs` 的 `require_identity` 把 `ChatLayerError::Jwt(_)` map 成 `PermissionDenied`,导致 `glayer/error.rs` 里 `ChatLayerError::Jwt(_) => StatusCode::UNAUTHORIZED` 这条分支永远走不到,token 过期/无效实际返回 403 FORBIDDEN。语义上应为 401。
45
46### 3. LiveKit webhook 事件用 `Uuid::nil()` 作 actor,部分投影静默失败
47
48`live/service.rs:327`:`EventEnvelope::new(Uuid::nil(), envelope_payload)`。投影层 `projection/live.rs` 用 `ChatStorageCtx::new(actor_id)` 即 `Some(nil)`,随后:
49
50- `apply_participant_joined` → `meet_participants().join` 内部 `require_self`(`caller != input.user_id`)→ nil ≠ 真实用户 → **PermissionDenied**;
51- `apply_meet_ended` → `mark_ended` 里 `created_by != caller(nil)` → `require_channel_admin(nil)` → **PermissionDenied**;
52- 仅 `apply_participant_left`(`leave` 无 self 校验)存活。
53
54错误被 actor 层 `tracing::error` + 吞掉。后果:webhook 作为 LiveKit 断连兜底的参与人追踪「加入/结束」分支失效(直接断线离开仍能记录)。建议 webhook 投影走系统/匿名专用上下文路径,或为这类事件单独绕过 self/admin 校验。
55
56### 4. `AttachmentPath::as_key()` 非幂等(每次生成新 UUID)
57
58`storagelayer/blob/media.rs`:key = `{owner}/{owner_id}/{Uuid::now_v7()}-{filename}`。`ChatMediaStore` trait 的 `put_attachment_bytes` 返回 `AttachmentPath`,而 `get_attachment_bytes` / `delete_attachment` / `presign_download(&AttachmentPath)` 会重新调用 `as_key()` 得到**不同 key**,按重建路径使用时会定位到错误对象。
59
60主流程(`chatlayer/media.rs`)因把 `key` 字符串持久化进 Redis draft 并统一走 `object_path(key)`(media.rs:208/241/267/287)而规避了此坑,但该 trait 本身是个隐患 API。
61
62### 5. webhook 去重键用常量 `sub`,可能误吞事件
63
64`live/webhook.rs` 校验用自定义 `nh`(hex) 字段 + `claims.sub` 做 `SET NX 300s` 去重。LiveKit webhook JWT 的 `sub` 通常是**每个 receiver 固定的常量**,并非每次投递唯一;300s 窗口内后续不同事件会被误吞。应改用 `jti`(WebhookClaims 目前**没有 jti 字段**)或事件 id 去重。
65
66另外 `nh = hex(SHA256(body))` 非 LiveKit 标准的 base64 声明,需确认 LiveKit 端配置一致,否则校验会全部失败。
67
68---
69
70## 低危
71
72| # | 位置 | 问题 |
73|---|---|---|
74| 6 | `glayer/auth.rs` | `split_whitespace` 只取前两词,`"Bearer abc extra"` 会取 `abc` 而忽略多余 token,未拒绝畸形头 |
75| 7 | `mapper/attachment.rs::attach_to_message` | 仅 `require_uploader_or_admin` + 附件 owner 为 nil,未校验上传者是否为目标消息所属会话成员——可把自己的 draft 附件挂到任意已知 `message_id`(含自己不在其中的会话)下 |
76| 8 | `mapper/message.rs::find_by_id` | 不存在返回 `MessageNotFound`、存在但非成员返回 `PermissionDenied`,可枚举区分消息是否存在;`exists()` 忽略 ctx |
77| 9 | `mapper/message.rs` 游标分页 | `list_by_conversation` 的 cursor 消息未校验归属同一 chat,跨会话游标可能跳页/重复(结果仍按 chat 过滤,无越权泄露) |
78| 10 | `actor/runtime.rs` ActorRegistry | 旧 actor 结束时 `remove(key)` 可能误删同 key 新 spawn 的 sender,后续 dispatch 重复建 actor(经典竞态,低概率) |
79| 11 | `chatlayer/media.rs::finalize_upload` | `draft.size` 用客户端传入的 `observed_size` 而非 `head` 真实值,附件元数据 size 可被谎报 |
80| 12 | typing 回环 | `glayer/ws.rs::handle_typing` 既本地 `broadcast_typing` 又 `publish_typing`;TypingConsumer 用 Broadcast 订阅会在源节点再次广播,同节点成员收到重复 Typing 帧(发送者已跳过,纯展示型) |
81| 13 | `live/service.rs` | `RateLimitProvider` 的 `rate_limit_settings` 用 `default()`,未接实际配置,限流可能未生效 |
82| 14 | `live/service.rs::join_room` | 并发首次 join 同 channel 均见 `None` → 双 `create`,依赖 DB 唯一约束兜底 |
83| 15 | `chatlayer/service.rs::send_message` | `Fresh \| InFlight` 都继续 publish,InFlight 会重复 publish;已被 `projection/room.rs` 的 `is_resolved` 兜底,不产生重复消息,仅多余一次 publish |
84
85---
86
87## 待确认 / 设计问题
88
89- **XSS 转义策略**:`sanitize_message_body` 把 `< > & " '` 转义为 HTML 实体。若客户端按纯文本渲染则属过度转义/双重编码;若按 HTML 渲染则正确。需确认前端渲染方式。
90- **`NewMessage` view 事件 body 为空串**(`projection/user.rs`),客户端需凭 `message_id` 回查消息体——设计选择,但会增加一次往返。
91
92---
93
94## 做得好的部分
95
96- **权限集中在 repository 层**:`require_member`(Private=creator/counterpart、Group=space active member)、`require_admin`(查 space_member_roles + manage_members 权限)、`require_sender_or_admin`、`require_uploader_or_admin` 等,`storagelayer/auth.rs` 实现扎实。
97- **幂等与兜底**:room/user/live actor 统一 dedup(Redis `SET NX chat:evt:{event_id}`)+ Kafka at-least-once + 错误吞掉由重投兜底;`apply_message_created` 先查 `is_resolved` 再 create+resolve(化解 InFlight 双 publish)。
98- **附件安全**:`is_dangerous_upload` 拦截 HTML/SVG/JS/XML/PHP(防存储型 XSS);presign 用 owner_hint 作 nonce `SET NX` 防复用;size_hint 上限校验;finalize 校验 `draft.sender_id == 调用者`。
99- **LiveKit 集成**:`live/token.rs` 令牌 HS256 标准(iss=api_key, sub, video grants, nbf/exp/iat/jti),api_key 空→NotConfigured;`live/client.rs` admin_token 60s 现签;handler 全部用 `AuthUser` claims.sub 提取 user_id;`/live/webhook` 为 public 不 ratelimit(合理);`WebhookVerification | NotConfigured` 回 200+`{"ok":false}` 避免 LiveKit 重试。
100- **网关层**:`glayer/error.rs` 5xx 不泄漏内部错误;ws 握手超时/认证失败/超限分别 close 4001/4002/4003;hub 用 std Mutex + try_send,慢消费者直接剔除;pump ping/pong 心跳;`messages.rs` 校验路径 conversation_id 与 body 一致。
101- **事件流**:producer 分区键 room=conversation_id、user=user_id、live=channel_id,topic 后缀 v1;事件定义 serde tag=type snake_case 一致;映射层 `Uuid::now_v7`;错误枚举齐全。
102
103---
104
105## 修复优先级建议
106
1071. 第 1 项(高危,核心功能)→ 第 2/3 项(权限语义与 webhook 兜底)→ 第 4/5 项(数据完整性/安全校验)→ 其余按需。
108
109---
110
111## 修复记录(2026-08-13)
112
113以下问题已修复,验证:`cargo check/clippy/fmt` 与 `cargo test` 全部通过,chat 相关 4 crate + livechat 无新增告警。
114
115| # | 问题 | 修复方式 | 涉及文件 |
116|---|------|---------|---------|
117| 1 | 群聊消息扇出死代码(高危) | 改为遍历 `list_member_user_ids`,对每个非发送者成员发 `MessageDelivered`;被直接 @ 的用户跳过(由 `MentionReceived` 覆盖,避免未读数重复 +1) | `chat/chatlayer/projection/room.rs` |
118| 2 | JWT 失效映射 403 而非 401 | `require_identity` 直接透传 `ChatLayerError::Jwt`(不再 map 成 PermissionDenied),glayer `Jwt→401` 分支恢复可达 | `chat/chatlayer/auth_bridge.rs` |
119| 3 | webhook 事件用 nil actor 导致加入/结束分支失效 | 投影层新增 `effective_actor`:actor 为 nil 时回退到 payload 主体(participant user_id / meet.created_by);无已知创建者且无 meet 行的 room.started 安全跳过 | `chat/chatlayer/projection/live.rs` |
120| 4 | `AttachmentPath::as_key` 非幂等 | `AttachmentPath` 内聚 `key` 字段,构造时生成一次,`as_key()` 返回 `&str` | `chat/storagelayer/blob/media.rs`、`blob/object_store.rs`、`chatlayer/media.rs` |
121| 5 | webhook 去重用常量 sub | `WebhookClaims` 增加可选 `jti`;去重键优先 `jti`,否则回退 body 哈希 `nh`(每次投递唯一) | `chat/live/webhook.rs`、`live/service.rs` |
122| 6 | Bearer 头畸形未拒绝 | 解析后若仍有剩余 token 则返回 401 | `chat/glayer/auth.rs` |
123| 7 | attach_to_message 缺会话校验 | 取消息行后 `require_member(message.chat_id)`,上传者必须是目标消息所属会话成员 | `chat/storagelayer/mapper/attachment.rs` |
124| 8 | find_by_id 消息存在性枚举 | 不存在返回 `Ok(None)`(与 PermissionDenied 区分不再可枚举);`exists()` 加成员校验,非成员返回 false | `chat/storagelayer/mapper/message.rs` |
125| 9 | list_by_conversation cursor 未校验归属 | cursor 查询追加 `chat_id.eq(chat_id)` 过滤,不属于该会话返回 InvalidInput | `chat/storagelayer/mapper/message.rs` |
126| 10 | ActorRegistry 竞态 | registry 值升级为 `ActorEntry{tx, generation}`,actor 退出清理时按 generation 比对,不再误删新 spawn 的 sender | `chat/chatlayer/actor/runtime.rs` |
127| 11 | finalize_upload 用客户端 observed_size | draft 与 AttachmentMeta 的 size 改用 `head` 真实值;observed_size 仅保留范围校验 | `chat/chatlayer/media.rs` |
128| 12 | typing 回环重复广播 | 有队列时只走 Kafka 发布(TypingConsumer 全节点广播,含本节点);无队列时才本地广播;`publish_typing` 返回是否已发布 | `chat/glayer/ws.rs`、`glayer/state.rs` |
129| 13 | rate_limit_settings 用 default() | `LiveSettings` 新增 `rate_limit: RateLimitConfig`,`LiveService::new` 接收 `Arc<RateLimitConfig>`,livechat main 传入 `app.rate_limit` | `lib/config/src/live.rs`、`chat/live/service.rs`、`app/livechat/src/main.rs` |
130| 14 | join_room 并发首次 join 双 create 竞态 | create 失败后重新 `find_active_by_channel`,存在则按加入处理,否则返回 Conflict | `chat/live/service.rs` |
131| 15 | send_message InFlight 重复 publish | `InFlight` 直接返回 `AcceptedMsg`(不重复 publish;投影层 is_resolved 兜底保留) | `chat/chatlayer/service.rs` |
132
133### 未改动项(待前端确认,设计问题)
134- #16 XSS 转义策略(HTML 实体转义 vs 纯文本渲染)——需确认客户端渲染方式。
135- #17 NewMessage view 事件 body 为空串——客户端需回查消息体,属设计选择。
Modified Cargo.lock Cargo.lock +261 −2 View Cargo.lock
@@ -625,6 +625,7 @@
625625 "thiserror 2.0.19",
626626 "time",
627627 "tokio",
628 "totp",
628629 "tracing",
629630 "url",
630631 "utoipa",
@@ -2243,6 +2244,12 @@
22432244checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
22442245
22452246[[package]]
2247name = "fixedbitset"
2248version = "0.5.7"
2249source = "registry+https://github.com/rust-lang/crates.io-index"
2250checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
2251
2252[[package]]
22462253name = "flate2"
22472254version = "1.1.9"
22482255source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2271,6 +2278,12 @@
22712278
22722279[[package]]
22732280name = "foldhash"
2281version = "0.1.5"
2282source = "registry+https://github.com/rust-lang/crates.io-index"
2283checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
2284
2285[[package]]
2286name = "foldhash"
22742287version = "0.2.0"
22752288source = "registry+https://github.com/rust-lang/crates.io-index"
22762289checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
@@ -3726,13 +3739,22 @@
37263739
37273740[[package]]
37283741name = "hashbrown"
3742version = "0.15.5"
3743source = "registry+https://github.com/rust-lang/crates.io-index"
3744checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
3745dependencies = [
3746 "foldhash 0.1.5",
3747]
3748
3749[[package]]
3750name = "hashbrown"
37293751version = "0.16.1"
37303752source = "registry+https://github.com/rust-lang/crates.io-index"
37313753checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
37323754dependencies = [
37333755 "allocator-api2",
37343756 "equivalent",
3735 "foldhash",
3757 "foldhash 0.2.0",
37363758]
37373759
37383760[[package]]
@@ -3743,7 +3765,7 @@
37433765dependencies = [
37443766 "allocator-api2",
37453767 "equivalent",
3746 "foldhash",
3768 "foldhash 0.2.0",
37473769]
37483770
37493771[[package]]
@@ -3939,6 +3961,19 @@
39393961]
39403962
39413963[[package]]
3964name = "hyper-timeout"
3965version = "0.5.2"
3966source = "registry+https://github.com/rust-lang/crates.io-index"
3967checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
3968dependencies = [
3969 "hyper",
3970 "hyper-util",
3971 "pin-project-lite",
3972 "tokio",
3973 "tower-service",
3974]
3975
3976[[package]]
39423977name = "hyper-util"
39433978version = "0.1.20"
39443979source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4668,8 +4703,43 @@
46684703name = "lprovider"
46694704version = "1.94.0"
46704705dependencies = [
4706 "aes-gcm",
4707 "async-trait",
46714708 "axum",
4709 "base64 0.23.1",
4710 "bytes",
4711 "cache",
4712 "clap",
4713 "config 1.94.0",
4714 "db",
4715 "futures",
4716 "hex",
4717 "http",
4718 "llm",
4719 "metrics",
4720 "prost",
4721 "prost-types",
4722 "rand 0.10.2",
4723 "redis",
4724 "reqwest 0.13.4",
4725 "sea-orm",
4726 "secret",
4727 "serde",
4728 "serde_json",
4729 "serde_yaml",
4730 "sha2 0.10.9",
4731 "thiserror 2.0.19",
4732 "time",
46724733 "tokio",
4734 "tokio-stream",
4735 "tokio-util",
4736 "tonic",
4737 "tonic-health",
4738 "tonic-prost",
4739 "tonic-prost-build",
4740 "tracing",
4741 "tracing-subscriber",
4742 "uuid",
46734743]
46744744
46754745[[package]]
@@ -4936,6 +5006,12 @@
49365006]
49375007
49385008[[package]]
5009name = "multimap"
5010version = "0.10.1"
5011source = "registry+https://github.com/rust-lang/crates.io-index"
5012checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
5013
5014[[package]]
49395015name = "native-tls"
49405016version = "0.2.18"
49415017source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5646,6 +5722,17 @@
56465722]
56475723
56485724[[package]]
5725name = "petgraph"
5726version = "0.8.3"
5727source = "registry+https://github.com/rust-lang/crates.io-index"
5728checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
5729dependencies = [
5730 "fixedbitset",
5731 "hashbrown 0.15.5",
5732 "indexmap",
5733]
5734
5735[[package]]
56495736name = "pgvector"
56505737version = "0.4.2"
56515738source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5877,6 +5964,16 @@
58775964]
58785965
58795966[[package]]
5967name = "prettyplease"
5968version = "0.2.37"
5969source = "registry+https://github.com/rust-lang/crates.io-index"
5970checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
5971dependencies = [
5972 "proc-macro2",
5973 "syn 2.0.119",
5974]
5975
5976[[package]]
58805977name = "primefield"
58815978version = "0.14.0"
58825979source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6017,6 +6114,27 @@
60176114]
60186115
60196116[[package]]
6117name = "prost-build"
6118version = "0.14.4"
6119source = "registry+https://github.com/rust-lang/crates.io-index"
6120checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
6121dependencies = [
6122 "heck 0.5.0",
6123 "itertools 0.14.0",
6124 "log",
6125 "multimap",
6126 "petgraph",
6127 "prettyplease",
6128 "prost",
6129 "prost-types",
6130 "pulldown-cmark",
6131 "pulldown-cmark-to-cmark",
6132 "regex",
6133 "syn 2.0.119",
6134 "tempfile",
6135]
6136
6137[[package]]
60206138name = "prost-derive"
60216139version = "0.14.4"
60226140source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6030,6 +6148,15 @@
60306148]
60316149
60326150[[package]]
6151name = "prost-types"
6152version = "0.14.4"
6153source = "registry+https://github.com/rust-lang/crates.io-index"
6154checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
6155dependencies = [
6156 "prost",
6157]
6158
6159[[package]]
60336160name = "ptr_meta"
60346161version = "0.1.4"
60356162source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6050,6 +6177,26 @@
60506177]
60516178
60526179[[package]]
6180name = "pulldown-cmark"
6181version = "0.13.4"
6182source = "registry+https://github.com/rust-lang/crates.io-index"
6183checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
6184dependencies = [
6185 "bitflags 2.13.1",
6186 "memchr",
6187 "unicase",
6188]
6189
6190[[package]]
6191name = "pulldown-cmark-to-cmark"
6192version = "22.0.1"
6193source = "registry+https://github.com/rust-lang/crates.io-index"
6194checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60"
6195dependencies = [
6196 "pulldown-cmark",
6197]
6198
6199[[package]]
60536200name = "queue"
60546201version = "1.94.0"
60556202dependencies = [
@@ -6823,6 +6970,7 @@
68236970checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
68246971dependencies = [
68256972 "aws-lc-rs",
6973 "log",
68266974 "once_cell",
68276975 "ring",
68286976 "rustls-pki-types",
@@ -7168,6 +7316,21 @@
71687316]
71697317
71707318[[package]]
7319name = "secret"
7320version = "1.94.0"
7321dependencies = [
7322 "aes-gcm",
7323 "base64 0.23.1",
7324 "hex",
7325 "rand 0.10.2",
7326 "serde",
7327 "serde_json",
7328 "sha2 0.10.9",
7329 "thiserror 2.0.19",
7330 "tracing",
7331]
7332
7333[[package]]
71717334name = "security-framework"
71727335version = "3.7.0"
71737336source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7297,6 +7460,19 @@
72977460]
72987461
72997462[[package]]
7463name = "serde_yaml"
7464version = "0.9.34+deprecated"
7465source = "registry+https://github.com/rust-lang/crates.io-index"
7466checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
7467dependencies = [
7468 "indexmap",
7469 "itoa",
7470 "ryu",
7471 "serde",
7472 "unsafe-libyaml",
7473]
7474
7475[[package]]
73007476name = "serdect"
73017477version = "0.4.3"
73027478source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8229,6 +8405,7 @@
82298405 "futures-core",
82308406 "pin-project-lite",
82318407 "tokio",
8408 "tokio-util",
82328409]
82338410
82348411[[package]]
@@ -8308,21 +8485,57 @@
83088485checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
83098486dependencies = [
83108487 "async-trait",
8488 "axum",
83118489 "base64 0.22.1",
83128490 "bytes",
8491 "flate2",
8492 "h2",
83138493 "http",
83148494 "http-body",
83158495 "http-body-util",
8496 "hyper",
8497 "hyper-timeout",
8498 "hyper-util",
83168499 "percent-encoding",
83178500 "pin-project",
8501 "rustls-native-certs",
8502 "socket2",
83188503 "sync_wrapper",
8504 "tokio",
8505 "tokio-rustls",
83198506 "tokio-stream",
8507 "tower",
83208508 "tower-layer",
83218509 "tower-service",
83228510 "tracing",
83238511]
83248512
83258513[[package]]
8514name = "tonic-build"
8515version = "0.14.6"
8516source = "registry+https://github.com/rust-lang/crates.io-index"
8517checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322"
8518dependencies = [
8519 "prettyplease",
8520 "proc-macro2",
8521 "quote",
8522 "syn 2.0.119",
8523]
8524
8525[[package]]
8526name = "tonic-health"
8527version = "0.14.6"
8528source = "registry+https://github.com/rust-lang/crates.io-index"
8529checksum = "fcfab99db777fba2802f0dfa861d1628d1ae916fb199d29819941f139ae85082"
8530dependencies = [
8531 "prost",
8532 "tokio",
8533 "tokio-stream",
8534 "tonic",
8535 "tonic-prost",
8536]
8537
8538[[package]]
83268539name = "tonic-prost"
83278540version = "0.14.6"
83288541source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8334,6 +8547,36 @@
83348547]
83358548
83368549[[package]]
8550name = "tonic-prost-build"
8551version = "0.14.6"
8552source = "registry+https://github.com/rust-lang/crates.io-index"
8553checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27"
8554dependencies = [
8555 "prettyplease",
8556 "proc-macro2",
8557 "prost-build",
8558 "prost-types",
8559 "quote",
8560 "syn 2.0.119",
8561 "tempfile",
8562 "tonic-build",
8563]
8564
8565[[package]]
8566name = "totp"
8567version = "1.94.0"
8568dependencies = [
8569 "aes-gcm",
8570 "base64 0.23.1",
8571 "hmac 0.12.1",
8572 "rand 0.10.2",
8573 "sha1 0.10.7",
8574 "sha2 0.10.9",
8575 "thiserror 2.0.19",
8576 "time",
8577]
8578
8579[[package]]
83378580name = "tower"
83388581version = "0.5.3"
83398582source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8341,9 +8584,12 @@
83418584dependencies = [
83428585 "futures-core",
83438586 "futures-util",
8587 "indexmap",
83448588 "pin-project-lite",
8589 "slab",
83458590 "sync_wrapper",
83468591 "tokio",
8592 "tokio-util",
83478593 "tower-layer",
83488594 "tower-service",
83498595 "tracing",
@@ -8504,6 +8750,12 @@
85048750]
85058751
85068752[[package]]
8753name = "unicase"
8754version = "2.9.0"
8755source = "registry+https://github.com/rust-lang/crates.io-index"
8756checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
8757
8758[[package]]
85078759name = "unicode-bidi"
85088760version = "0.3.18"
85098761source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8559,6 +8811,12 @@
85598811]
85608812
85618813[[package]]
8814name = "unsafe-libyaml"
8815version = "0.2.11"
8816source = "registry+https://github.com/rust-lang/crates.io-index"
8817checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
8818
8819[[package]]
85628820name = "unsafe-libyaml-norway"
85638821version = "0.2.15"
85648822source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8620,6 +8878,7 @@
86208878 "thiserror 2.0.19",
86218879 "time",
86228880 "tokio",
8881 "totp",
86238882 "tracing",
86248883 "url",
86258884 "utoipa",
Modified Cargo.toml Cargo.toml +10 −0 View Cargo.toml
@@ -35,6 +35,7 @@
3535 "lib/rate_limit",
3636 "lib/repo",
3737 "lib/search",
38 "lib/secret",
3839 "lib/space",
3940 "lib/storage",
4041 "lib/totp",
@@ -81,6 +82,7 @@
8182project = { path = "lib/project" }
8283payment = { path = "lib/payment" }
8384search = { path = "lib/search" }
85secret = { path = "lib/secret" }
8486webapi = { path = "lib/webapi" }
8587webhook = { path = "lib/webhook" }
8688obs = { path = "lib/obs" }
@@ -107,6 +109,7 @@
107109object_store = { version = "0.14", default-features = false, features = ["aws", "fs"] }
108110url = "2"
109111serde_json = "1"
112serde_yaml = "0.9"
110113axum = "0.8.9"
111114axum-extra = { version = "0.12.6", features = ["cookie"] }
112115reqwest = { version = "0.13.4", default-features = false, features = ["default-tls", "json", "form", "stream"] }
@@ -144,3 +147,10 @@
144147opentelemetry-appender-tracing = { version = "=0.31.0" }
145148tracing-opentelemetry = { version = "=0.32.0" }
146149metrics-exporter-opentelemetry = { version = "=0.2.1" }
150tonic = { version = "0.14.2", features = ["tls-ring", "tls-native-roots", "gzip"] }
151tonic-build = "0.14.2"
152tonic-health = "0.14.0"
153tonic-prost = "0.14.6"
154tonic-prost-build = "0.14.6"
155prost = "0.14.1"
156prost-types = "0.14.1"