Version: 0.6.6.dev.260317
✨ feat(task,agent): 新增任务完成接口,并打通聊天全链路 Token 记账 - ✅ 新增“标记任务为完成”接口,并补充幂等保护,避免重复完成导致状态污染 - 📊 为聊天链路补充 Token 统计能力: - 流式主对话链路直接读取模型 `usage` - Agent 链路通过 `Eino callback + ctx` 聚合 `Generate usage` - 在流式场景下补齐缺失的 `usage` 数据 - 🧾 按口径 B 完成 Token 落库: - 用户消息 `token` 记为 `0` - 助手消息记录本轮总 `token` - 持久化时同步更新 `chat_histories.tokens_consumed`、`agent_chats.tokens_total`、`users.token_usage` - 🔄 异步标题生成产生的 Token 通过 Outbox 事件完成账本增量调整,保证统计口径一致 - 📝 同步更新 `AGENTS.md` 与 `.gitignore` - 📚 小幅更新 README 说明文档
This commit is contained in:
@@ -33,22 +33,32 @@ func (r *AgentDAO) WithTx(tx *gorm.DB) *AgentDAO {
|
||||
// 失败处理:
|
||||
// 1. 任一步骤失败都返回 error;
|
||||
// 2. 若调用方处于事务中,返回 error 会触发事务回滚。
|
||||
func (a *AgentDAO) saveChatHistoryCore(ctx context.Context, userID int, conversationID string, role, message string) error {
|
||||
func (a *AgentDAO) saveChatHistoryCore(ctx context.Context, userID int, conversationID string, role, message string, tokensConsumed int) error {
|
||||
// 0. token 入库前兜底:负数统一归零,避免异常值污染累计统计。
|
||||
if tokensConsumed < 0 {
|
||||
tokensConsumed = 0
|
||||
}
|
||||
|
||||
// 1. 先写 chat_histories 原始消息。
|
||||
userChat := model.ChatHistory{
|
||||
UserID: userID,
|
||||
MessageContent: &message,
|
||||
Role: &role,
|
||||
ChatID: conversationID,
|
||||
TokensConsumed: tokensConsumed,
|
||||
}
|
||||
if err := a.db.WithContext(ctx).Create(&userChat).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 再更新会话统计(message_count +1, last_message_at=now)。
|
||||
// 2. 再更新会话统计:
|
||||
// 2.1 message_count +1,保持和 chat_histories 行数口径一致;
|
||||
// 2.2 tokens_total 累加本条消息 token;
|
||||
// 2.3 last_message_at 刷新为当前时间,供会话排序使用。
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"message_count": gorm.Expr("message_count + ?", 1),
|
||||
"tokens_total": gorm.Expr("tokens_total + ?", tokensConsumed),
|
||||
"last_message_at": &now,
|
||||
}
|
||||
result := a.db.WithContext(ctx).Model(&model.AgentChat{}).
|
||||
@@ -61,6 +71,23 @@ func (a *AgentDAO) saveChatHistoryCore(ctx context.Context, userID int, conversa
|
||||
// 会话不存在时直接失败,避免出现“孤儿历史消息”。
|
||||
return fmt.Errorf("conversation not found when updating stats: user_id=%d chat_id=%s", userID, conversationID)
|
||||
}
|
||||
|
||||
// 3. 最后更新 users.token_usage(同一事务内):
|
||||
// 3.1 只在 tokensConsumed>0 时执行,避免无意义写入;
|
||||
// 3.2 和 chat_histories/agent_chats 放在同一事务里,保证统计口径原子一致;
|
||||
// 3.3 若用户行不存在则返回错误,触发事务回滚,防止出现“会话统计成功但用户统计丢失”。
|
||||
if tokensConsumed > 0 {
|
||||
userUpdate := a.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", userID).
|
||||
Update("token_usage", gorm.Expr("token_usage + ?", tokensConsumed))
|
||||
if userUpdate.Error != nil {
|
||||
return userUpdate.Error
|
||||
}
|
||||
if userUpdate.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found when updating token usage: user_id=%d", userID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,8 +96,8 @@ func (a *AgentDAO) saveChatHistoryCore(ctx context.Context, userID int, conversa
|
||||
// 设计目的:
|
||||
// 1. 给服务层组合多个 DAO 操作时复用,避免嵌套事务;
|
||||
// 2. 让 outbox 消费处理器可以和业务写入共享同一个 tx。
|
||||
func (a *AgentDAO) SaveChatHistoryInTx(ctx context.Context, userID int, conversationID string, role, message string) error {
|
||||
return a.saveChatHistoryCore(ctx, userID, conversationID, role, message)
|
||||
func (a *AgentDAO) SaveChatHistoryInTx(ctx context.Context, userID int, conversationID string, role, message string, tokensConsumed int) error {
|
||||
return a.saveChatHistoryCore(ctx, userID, conversationID, role, message, tokensConsumed)
|
||||
}
|
||||
|
||||
// SaveChatHistory 在同步直写路径下写入聊天历史。
|
||||
@@ -78,9 +105,58 @@ func (a *AgentDAO) SaveChatHistoryInTx(ctx context.Context, userID int, conversa
|
||||
// 说明:
|
||||
// 1. 该方法会自行开启事务;
|
||||
// 2. 内部复用 saveChatHistoryCore,确保和 SaveChatHistoryInTx 的业务口径完全一致。
|
||||
func (a *AgentDAO) SaveChatHistory(ctx context.Context, userID int, conversationID string, role, message string) error {
|
||||
func (a *AgentDAO) SaveChatHistory(ctx context.Context, userID int, conversationID string, role, message string, tokensConsumed int) error {
|
||||
return a.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return a.WithTx(tx).saveChatHistoryCore(ctx, userID, conversationID, role, message)
|
||||
return a.WithTx(tx).saveChatHistoryCore(ctx, userID, conversationID, role, message, tokensConsumed)
|
||||
})
|
||||
}
|
||||
|
||||
// adjustTokenUsageCore 在同一事务语义下做“会话+用户”token 账本增量调整。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只更新 agent_chats.tokens_total 与 users.token_usage;
|
||||
// 2. 不写 chat_histories(消息落库由 SaveChatHistory* 路径负责);
|
||||
// 3. deltaTokens<=0 时视为无操作,直接返回。
|
||||
func (a *AgentDAO) adjustTokenUsageCore(ctx context.Context, userID int, conversationID string, deltaTokens int) error {
|
||||
if deltaTokens <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 1. 先更新会话累计 token。
|
||||
chatUpdate := a.db.WithContext(ctx).
|
||||
Model(&model.AgentChat{}).
|
||||
Where("user_id = ? AND chat_id = ?", userID, conversationID).
|
||||
Update("tokens_total", gorm.Expr("tokens_total + ?", deltaTokens))
|
||||
if chatUpdate.Error != nil {
|
||||
return chatUpdate.Error
|
||||
}
|
||||
if chatUpdate.RowsAffected == 0 {
|
||||
return fmt.Errorf("conversation not found when adjusting tokens: user_id=%d chat_id=%s", userID, conversationID)
|
||||
}
|
||||
|
||||
// 2. 再更新用户累计 token。
|
||||
userUpdate := a.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", userID).
|
||||
Update("token_usage", gorm.Expr("token_usage + ?", deltaTokens))
|
||||
if userUpdate.Error != nil {
|
||||
return userUpdate.Error
|
||||
}
|
||||
if userUpdate.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found when adjusting token usage: user_id=%d", userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdjustTokenUsageInTx 在调用方已开启事务时执行 token 账本增量调整。
|
||||
func (a *AgentDAO) AdjustTokenUsageInTx(ctx context.Context, userID int, conversationID string, deltaTokens int) error {
|
||||
return a.adjustTokenUsageCore(ctx, userID, conversationID, deltaTokens)
|
||||
}
|
||||
|
||||
// AdjustTokenUsage 在同步路径下执行 token 账本增量调整(内部自带事务)。
|
||||
func (a *AgentDAO) AdjustTokenUsage(ctx context.Context, userID int, conversationID string, deltaTokens int) error {
|
||||
return a.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return a.WithTx(tx).adjustTokenUsageCore(ctx, userID, conversationID, deltaTokens)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user