Version: 0.9.80.dev.260506

后端:
1. LLM 独立服务与统一计费出口落地:新增 `cmd/llm`、`client/llm` 与 `services/llm/rpc`,补齐 BillingContext、CreditBalanceGuard、价格规则解析、stream usage 归集与 `credit.charge.requested` outbox 发布,active-scheduler / agent / course / memory / gateway fallback 全部改走 llm zrpc,不再各自本地初始化模型。
2. TokenStore 收口为 Credit 权威账本:新增 credit account / ledger / product / order / price-rule / reward-rule 能力与 Redis 快照缓存,扩展 tokenstore rpc/client 支撑余额快照、消耗看板、商品、订单、流水、价格规则和奖励规则,并接入 LLM charge 事件消费完成 Credit 扣费落账。
3. 计费旧链路下线与网关切口切换:`/token-store` 语义整体切到 `/credit-store`,agent chat 移除旧 TokenQuotaGuard,userauth 的 CheckTokenQuota / AdjustTokenUsage 改为废弃,聊天历史落库不再同步旧 token 额度账本,course 图片解析请求补 user_id 进入新计费口径。

前端:
4. 计划广场从 mock 数据切到真实接口:新增 forum api/types,首页支持真实列表、标签、搜索、防抖、点赞、导入和发布计划,详情页补齐帖子详情、评论树、回复和删除评论链路,同时补上“至少一个标签”的前后端约束与默认标签兜底。
5. 商店页切到 Credit 体系并重做展示:顶部改为余额 + Credit/Token 消耗看板,支持 24h/7d/30d/all 周期切换;套餐区展示原价与当前价;历史区改为当前用户 Credit 流水并支持查看更多,整体视觉和交互同步收口。

仓库:
6. 配置与本地启动体系补齐 llm / outbox 编排:`config.example.yaml` 增加 llm rpc 和统一 outbox service 配置,`dev-common.ps1` 把 llm 纳入多服务依赖并自动建 Kafka topic,`docker-compose.yml` 同步初始化 agent/task/memory/active-scheduler/notification/taskclass-forum/llm/token-store 全量 outbox topic。
This commit is contained in:
Losita
2026-05-06 20:16:53 +08:00
parent 7d324b77aa
commit 61db646805
104 changed files with 9527 additions and 3925 deletions

View File

@@ -0,0 +1,602 @@
# 统一出口 Credit 计费最终计划
## 1. 文档目的
本计划用于把当前系统从“各业务服务进程内直接持有 LLM 组件、调用结束后再走旧 token 累加”的模式,重构为:
1. `LLM` 成为真正的独立服务统一掌管模型调用入口、出口、usage 采集与计费事件发布。
2. `TokenStore` 服务切换为 Credit 语义权威服务,掌管余额、商品、订单、流水与最终扣费落账。
3. 前端商店直接消费 Credit 语义接口,并能展示“当前登录用户自己的 Credit 流水”。
这份计划改成“三步走”版本,便于直接按阶段批准与执行。
---
## 2. 总体结论
### 2.1 核心结论
1. `backend/services/llm` 不能再以“进程内共享组件”存在,必须升级为独立服务。
2. 所有用户态模型调用统一经过 `LLM RPC`,由 `LLM` 服务统一采集 usage。
3. `LLM` 服务自己做同步准入 `CreditBalanceGuard`,并使用 Redis 余额快照提速。
4. `LLM` 服务不做同步扣费,也不允许裸 goroutine 异步扣费。
5. `LLM` 服务在拿到最终 usage 后,把 `credit.charge.requested` 写入 **自己的 outbox 表**
6. `TokenStore` 服务异步消费扣费事件,更新 Credit 余额与流水。
7. 旧 token 累加链路、旧 token 商店链路和旧 `/token-store` 接口本轮直接删除,不保留兼容层。
### 2.2 关键原则
1. **统一出口**:所有 LLM 计费只在 `LLM` 服务出口发生,不允许业务层各自重复扣费。
2. **强制入参**:不能只靠 `Metadata` 约定传 `user_id`,必须改 RPC/函数签名,让漏传在编译期报错。
3. **准入同步、结算异步**:调用模型前同步做 `CreditBalanceGuard`,拿到最终 usage 后通过 outbox 异步结算。
4. **旧商店直删**:旧 token 商店前后端都未真正接线,本轮不做 token/credit 双轨并存。
5. **调用面兼容优先**`LLM RPC` 可以新增内部协议,但 `backend/client/llm` 对业务侧暴露的调用方式应尽量贴近当前 `services/llm`,把迁移成本压到最低。
---
## 3. 为什么要这样改
### 3.1 当前最大问题
1. `services/llm` 名字像服务,实际上不是服务,而是多个进程直接 new 的本地组件。
2. 计费边界分散,谁发起调用谁就得自己记账,很容易遗漏。
3. 当前只有旧 `token_usage` 累加,没有人民币成本与 Credit 扣费真相。
4. `userauth` 当前承担的是 `token quota` 门禁,不是 Credit 余额门禁。
5. 旧 token 商店前后端都没有真正接上,因此没有保留兼容层的价值。
### 3.2 这次改完后的好处
1. LLM 调用入口统一。
2. usage 采集统一。
3. 余额准入统一。
4. 扣费事件发布统一。
5. TokenStore 只做 Credit 权威账本,不再和模型调用散乱耦合。
---
## 4. 三步走总览
### 第一步:把 LLM 变成真正独立服务
目标:
1. 所有模型调用统一改走 `LLM RPC`
2. `LLM` 服务拥有自己的 `CreditBalanceGuard`
3. `LLM` 服务拥有自己的 outbox 表
### 第二步:把 TokenStore 改造成 Credit 权威服务
目标:
1. 所有余额、流水、商品、订单都切成 Credit 语义
2. 异步消费 `credit.charge.requested`
3. 提供“查看自己流水”的正式接口
### 第三步:切 Gateway 与前端,并删旧链路
目标:
1. 上线 `/credit-store/*`
2. 商店页接 Credit 真接口与流水列表
3. 删掉旧 token 累加和旧 token 商店链路
---
## 5. 第一步:把 LLM 变成真正独立服务
## 5.1 这一步的目标
把当前“各进程内直接持有 LLM 组件”的模式,改成“统一调 LLM 服务”。
## 5.2 新增/改造内容
### 5.2.1 新增独立 LLM 进程与 RPC
新增:
1. `backend/cmd/llm/main.go`
2. `backend/client/llm/client.go`
3. `backend/services/llm/rpc/pb/llm.proto`
4. `backend/services/llm/rpc/handler.go`
5. `backend/services/llm/rpc/server.go`
6. `backend/services/llm/dao/connect.go`
说明:
1. 这样 `services/llm` 放在 `services` 目录下才真正名副其实。
2. 业务服务以后全部只依赖 `client/llm`,不再直接 import `services/llm` 的本地组件。
### 5.2.2 统一 RPC 能力,但业务侧调用面尽量保持原状
服务间 `LLM RPC` 至少提供三类底层能力:
1. `GenerateText`
2. `StreamText`
3. `GenerateResponsesText`
但是对业务代码,不建议直接散落调用底层 RPC 方法名,而是由 `backend/client/llm` 继续暴露与当前本地 `llm client` 基本一致的门面:
1. `(*Client).GenerateText(ctx, messages, options)`
2. `GenerateJSON[T](ctx, client, messages, options)`
3. `(*Client).Stream(ctx, messages, options)`
4. `(*ArkResponsesClient).GenerateText(ctx, messages, options)`
5. `GenerateArkResponsesJSON[T](ctx, client, messages, options)`
迁移目标:
1. 业务层尽量只替换 client 来源,不重写 prompt 组织方式。
2. `GenerateOptions``ArkResponsesOptions` 现有字段语义尽量不变。
3. `generateJson / stream / responses json` 这些现有能力名和使用习惯尽量延续,避免把改造面扩大成“全链路重写调用协议”。
使用分工:
1. `GenerateText`memory、active-scheduler、agent 非流式调用
2. `StreamText`agent chat / plan / execute / deliver
3. `GenerateResponsesText`course 图片解析
### 5.2.3 统一 BillingContext但不要污染现有 GenerateOptions
所有请求都必须携带:
```go
type BillingContext struct {
UserID int
EventID string
Scene string
RequestID string
ConversationID string
ModelAlias string
SkipCharge bool
}
```
要求:
1. `UserID` 必须明确,普通用户态调用禁止省略。
2. `EventID` 必须稳定,作为幂等扣费键。
3. `Scene` 必须明确,方便价格表与审计。
4. `SkipCharge` 只允许系统内部场景显式声明。
5. `BillingContext` 不再塞进 `GenerateOptions.Metadata` 里,避免继续靠弱约定传关键字段。
6. `GenerateOptions``ArkResponsesOptions` 继续只承载模型行为参数,例如 `Temperature / MaxTokens / Thinking`
落地建议:
1. `client/llm` 对调用方继续保留现有 `GenerateText / GenerateJSON / Stream / Responses` 风格方法。
2. 计费必填信息通过独立的调用包装层传入,例如“绑定过 BillingContext 的 client”或“显式 request 参数对象”,但要由 `client/llm` 吸收复杂度。
3. 调用方改造目标应控制在“补一处 BillingContext 组装 + 替换 client 初始化来源”,而不是每个业务点重写整段调用代码。
### 5.2.4 LLM 服务自己的 CreditBalanceGuard
这一步必须同时完成,否则独立 LLM 服务没有准入价值。
Guard 设计要求:
1. 模型真正发起前同步执行。
2. 不直接查 MySQL 真相表,必须走 Redis 快照优先。
3. 缓存 miss 或过期时,再回源 `TokenStore RPC`
建议缓存 key
1. `smartflow:credit_balance_snapshot:{user_id}`
2. `smartflow:credit_balance_blocked:{user_id}`
执行顺序:
1. 先查 `blocked`
2. 再查 `snapshot`
3. miss 时调用 TokenStore 获取余额快照
4. 若余额不足,写短 TTL 的 `blocked`
### 5.2.5 LLM 服务自己的 outbox
这一步也必须同时完成。
新增:
1. `llm_outbox_messages`
并在 outbox 目录注册:
1. `ServiceLLM`
2. `ServiceNameLLM`
3. `smartflow.llm.outbox`
4. `smartflow-llm-outbox-consumer`
涉及文件:
1. `backend/shared/infra/outbox/service_catalog.go`
2. `backend/shared/infra/outbox/service_route.go`
3. `backend/services/llm/dao/connect.go`
### 5.2.6 LLM 服务统一 usage 采集
`LLM` 服务内部统一输出:
```go
type BillingUsage struct {
InputTokens int64
OutputTokens int64
CachedTokens int64
ReasoningTokens int64
TotalTokens int64
ModelName string
ProviderName string
}
```
### 5.2.7 LLM 服务只发 charge 事件,不同步扣费
新增事件:
1. `credit.charge.requested`
事件载荷至少包含:
1. `event_id`
2. `user_id`
3. `scene`
4. `request_id`
5. `conversation_id`
6. `provider_name`
7. `model_name`
8. `input_tokens`
9. `output_tokens`
10. `cached_tokens`
11. `reasoning_tokens`
12. `rmb_cost_micros`
13. `credit_cost`
14. `triggered_at`
时机:
1. 拿到最终 usage
2. 算出本次 `credit_cost`
3. 写入 `llm_outbox_messages`
4. 结果即可返回调用方
## 5.3 这一步涉及哪些调用方改造
以下服务都要从“本地 import `services/llm`”改成“调用 `client/llm`”:
1. `agent`
2. `memory`
3. `active-scheduler`
4. `course`
---
## 6. 第二步:把 TokenStore 改造成 Credit 权威服务
## 6.1 这一步的目标
`TokenStore` 不再只是“充值/奖励入账中心”,而是:
1. Credit 余额真相
2. Credit 商品与订单中心
3. Credit 流水中心
4. LLM 扣费事件的最终结算方
## 6.2 新的 Credit 数据表
本轮不复用旧 `token_*` 表,直接建立纯 Credit 语义的新表:
1. `credit_products`
2. `credit_orders`
3. `credit_accounts`
4. `credit_ledger`
5. `credit_price_rules`
6. `credit_reward_rules`
### 6.2.1 `credit_products`
用途:商店展示的 Credit 商品
核心字段:
1. `sku`
2. `name`
3. `description`
4. `credit_amount`
5. `price_cent`
6. `currency`
7. `badge`
8. `status`
9. `sort_order`
### 6.2.2 `credit_orders`
用途:购买订单与 mock paid 状态机
核心字段:
1. `order_no`
2. `user_id`
3. `product_id`
4. `product_snapshot_json`
5. `quantity`
6. `credit_amount`
7. `amount_cent`
8. `status`
9. `payment_mode`
10. `idempotency_key`
11. `paid_at`
12. `credited_at`
### 6.2.3 `credit_accounts`
用途:余额真相与门禁回源表
核心字段:
1. `user_id`
2. `balance`
3. `total_recharged`
4. `total_rewarded`
5. `total_consumed`
6. `version`
### 6.2.4 `credit_ledger`
用途:统一正负流水,也是“查看自己流水”的唯一真相源
核心字段:
1. `event_id`
2. `user_id`
3. `direction`
4. `source`
5. `scene`
6. `description`
7. `credit_delta`
8. `balance_after`
9. `input_tokens`
10. `output_tokens`
11. `cached_tokens`
12. `reasoning_tokens`
13. `rmb_cost_micros`
14. `created_at`
### 6.2.5 `credit_price_rules`
用途:模型价格表
核心字段:
1. `provider_name`
2. `model_name`
3. `scene`
4. `input_price_micros_per_1k`
5. `output_price_micros_per_1k`
6. `cached_price_micros_per_1k`
7. `reasoning_price_micros_per_1k`
8. `credits_per_yuan`
9. `status`
### 6.2.6 `credit_reward_rules`
用途:社区奖励规则
核心字段:
1. `source`
2. `name`
3. `credit_amount`
4. `status`
5. `config_json`
## 6.3 TokenStore 如何消费 charge 事件
新增消费逻辑:
1. 订阅 `credit.charge.requested`
2. 幂等校验 `event_id`
3. 在事务中写 `credit_ledger`
4. 扣减 `credit_accounts.balance`
5. 刷新或删除 Redis 余额快照
## 6.4 用户查看自己流水接口
这是本轮必须落的能力。
对外新增:
1. `ListCreditTransactions`
要求:
1. 明确用于“当前登录用户查看自己的 Credit 流水”
2. 必须覆盖购买入账、社区奖励入账、LLM 消费扣费三类记录
HTTP 接口:
1. `GET /api/v1/credit-store/transactions`
必须满足:
1. 只返回当前登录用户自己的 Credit 流水
2. 支持分页:`page``page_size`
3. 支持可选筛选:`direction``source``scene`
4. 返回字段至少包含:
- `transaction_id`
- `event_id`
- `direction`
- `source`
- `scene`
- `description`
- `credit_delta`
- `balance_after`
- `created_at`
## 6.5 这一步要删除什么旧东西
直接删除:
1. `token_products`
2. `token_orders`
3. `token_grants`
4. `token_reward_rules`
---
## 7. 第三步:切 Gateway/前端并删旧链路
## 7.1 这一步的目标
1. 上线 `/credit-store/*`
2. 商店页接 Credit 真接口
3. 删掉旧 token 累加和旧 token 商店链路
## 7.2 Gateway 切换
只保留 Credit 语义路由:
1. `GET /api/v1/credit-store/summary`
2. `GET /api/v1/credit-store/products`
3. `POST /api/v1/credit-store/orders`
4. `POST /api/v1/credit-store/orders/:order_id/mock-paid`
5. `GET /api/v1/credit-store/transactions`
直接删除:
1. `/token-store/*`
2. `backend/gateway/api/tokenstoreapi/*`
## 7.3 前端商店页切换
商店页必须改成:
1.`/credit-store/*`
2. 新增“我的 Credit 流水”展示区
3. 流水数据源只允许来自 `GET /api/v1/credit-store/transactions`
4. 至少展示:
- 流水类型
- 流水说明
- Credit 增减值
- 流水发生时间
- 可选余额快照
## 7.4 旧 token 累加机制下线
直接删除:
1. `backend/services/runtime/eventsvc/chat_token_usage_adjust.go`
2. `backend/services/runtime/model/agent.go``ChatTokenUsageAdjustPayload`
3. `backend/services/agent/sv/agent_graph.go` 中旧 token 调整调用
4. `backend/services/agent/sv/agent_meta.go` 中旧 token 调整调用
5. `backend/services/runtime/dao/agent.go``tokens_total` 累加写路径
6. `backend/gateway/middleware/token_quota_guard.go` 在聊天主链的使用
7. `userauth``CheckTokenQuota / AdjustTokenUsage` 的计费用途
8. `agent_chats.tokens_total` 字段
---
## 8. 这三步分别改哪些地方
### 第一步改动面
1. `backend/services/llm/*`
2. `backend/client/llm/*`
3. `backend/cmd/llm/*`
4. `backend/shared/infra/outbox/service_catalog.go`
5. `backend/shared/infra/outbox/service_route.go`
6. `agent / memory / active-scheduler / course` 的所有本地 LLM 调用点
### 第二步改动面
1. `backend/services/tokenstore/*`
2. `backend/client/tokenstore/*`
3. TokenStore 的 outbox consumer
4. Redis 余额快照与失效逻辑
### 第三步改动面
1. `backend/gateway/*`
2. `frontend/src/views/StoreView.vue`
3. 可能新增商店流水列表组件
4. `runtime/userauth` 旧 token 累加链删除
---
## 9. 验证清单
### 9.1 第一步验证
1. 业务服务代码里不再直接 import `services/llm`
2. 所有模型调用都改经 `LLM RPC`
3. 所有请求都必须带 `BillingContext`
4. `CreditBalanceGuard` 命中缓存、miss 回源、余额不足封禁都通过测试
5. `llm_outbox_messages` 能正常写入 `credit.charge.requested`
### 9.2 第二步验证
1. `TokenStore` 能幂等消费 `credit.charge.requested`
2. `credit_accounts` 余额更新正确
3. `credit_ledger` 流水写入正确
4. `ListCreditTransactions` 返回当前用户自己的流水
### 9.3 第三步验证
1. 商店页能展示 Credit 概览
2. 商店页能展示商品
3. 商店页能展示“我的 Credit 流水”
4. 全文搜索确认以下旧链路不再出现在主路径:
- `PublishChatTokenUsageAdjustRequested`
- `AdjustTokenUsage(`
- `CheckTokenQuota(`
- `TokenQuotaGuard`
- `tokens_total +`
- `tokenstoreapi`
- `/token-store`
- `token_products`
- `token_orders`
- `token_grants`
- `token_reward_rules`
---
## 10. 风险与取舍
### 10.1 这次改造面确实更大
因为这次不是只改计费,而是顺带把 `LLM` 从“共享组件”升级成真正独立服务,所以会涉及:
1. 新 RPC
2. 新 client
3. 新 outbox
4. 业务服务调用方式切换
### 10.2 但长期结构更对
收益是:
1. LLM 调用入口统一
2. usage 采集统一
3. 准入 guard 统一
4. 计费事件发布统一
5. TokenStore 只做账本,不再和模型调用散乱耦合
---
## 11. 服务影响总表
### 必改服务
1. `backend/services/llm`
2. `backend/services/tokenstore`
3. `backend/gateway`
4. `backend/services/agent`
5. `backend/services/course`
6. `backend/services/memory`
7. `backend/services/active_scheduler`
### 新增
1. `backend/cmd/llm`
2. `backend/client/llm`
3. `backend/services/llm/rpc/*`
4. `llm_outbox_messages`
### 后续前端接入
1. `frontend/src/views/StoreView.vue`
2. 若商店页拆组件,则补充独立的 Credit 流水列表组件