模块介绍 / 今日规划 / 下班日报 / 团队摘要 生成成功后没有上报,也几乎不写「AI」日志。

流结束时的 usage 用了非阻塞发送,通道一满就被丢掉;很多服务商默认还不在流里带 usage。
现在每次真正打到模型的调用都会:

在本地日志(分类 AI,消息 AI 调用完成)写下 scene / prompt / completion / cached
登录后同步上报到后台日汇总
This commit is contained in:
李琦
2026-08-17 14:00:47 +08:00
parent 83e69317d4
commit e71ce451ff
23 changed files with 1363 additions and 93 deletions

25
ai.go
View File

@@ -258,9 +258,7 @@ func (a *App) runAIStream(ctx context.Context, provider ai.Provider, convID int6
a.emit("ai:stream", aiStreamEvent{ConversationID: convID, Error: e.Error(), Done: true})
return
}
if usage != nil {
go a.reportAIUsage(provider.Name(), usage.PromptTokens, usage.CompletionTokens, usage.Estimated)
}
a.recordAIUsage(provider.Name(), "chat", usage, msgs, sb.String())
a.store.TouchAIConversation(convID, provider.Name())
a.emit("ai:stream", aiStreamEvent{ConversationID: convID, Done: true, MessageID: m.ID})
}
@@ -339,6 +337,21 @@ func (a *App) aiProvider() (ai.Provider, error) {
return ai.New(st.AIProvider, key)
}
// recordAIUsage 写入本地「AI」生成记录并上报云端日汇总。usage 为空时按消息粗估,避免生成成功却没有记录。
func (a *App) recordAIUsage(provider, scene string, usage *ai.Usage, msgs []ai.Message, completion string) {
if usage == nil {
usage = ai.EstimateUsage(msgs, completion)
}
cache := "none"
if usage.CachedTokens > 0 {
cache = fmt.Sprintf("hit:%d", usage.CachedTokens)
}
a.store.Log("info", "AI", "AI 调用完成", fmt.Sprintf(
"scene=%s provider=%s prompt=%d completion=%d cached=%d cache=%s estimated=%v",
scene, provider, usage.PromptTokens, usage.CompletionTokens, usage.CachedTokens, cache, usage.Estimated))
go a.reportAIUsage(provider, usage.PromptTokens, usage.CompletionTokens, usage.CachedTokens, usage.Estimated)
}
// aiSummarySem 全局串行生成,避免批量分析后并发打爆免费额度接口。
var aiSummarySem = make(chan struct{}, 1)
@@ -501,6 +514,7 @@ func (a *App) generateAISummaries(projectID int64, kinds ...string) {
// 无数据的全局简报直接落固定文案,不浪费一次注定编造的模型调用。
if content, ok := a.staticDayBrief(kind, st.Locale); ok {
if e := a.store.SaveAISummary(projectID, kind, "system", content); e == nil {
a.store.Log("info", "AI", "AI 调用完成", fmt.Sprintf("scene=summary:%s cache=local provider=system", kind))
a.emit("ai:summary", aiSummaryEvent{ProjectID: projectID, Kind: kind})
}
continue
@@ -518,8 +532,12 @@ func (a *App) generateAISummaries(projectID int64, kinds ...string) {
continue
}
var sb strings.Builder
var usage *ai.Usage
var streamErr error
for chunk := range stream {
if chunk.Usage != nil {
usage = chunk.Usage
}
if chunk.Err != nil {
streamErr = chunk.Err
break
@@ -536,6 +554,7 @@ func (a *App) generateAISummaries(projectID int64, kinds ...string) {
a.emit("ai:summary", aiSummaryEvent{ProjectID: projectID, Kind: kind, Error: msg})
continue
}
a.recordAIUsage(provider.Name(), "summary:"+kind, usage, msgs, sb.String())
if e := a.store.SaveAISummary(projectID, kind, provider.Name(), dedentCommon(stripFence(strings.TrimSpace(sb.String())))); e != nil {
a.store.Log("warning", "AI", "AI 模块介绍保存失败", e.Error())
continue