Files
pj0235-eai_agentplatform/eai_agentplatform/backend-go/internal/ai/credits.go
T
eaiadminandClaude Code 8b136d4a10 feat: 专员岗位说明书与技能绑定打通(SY23 P0–P4)
工作台对话此前根本不读专员表:smart_assistant.go 里两条写死的 prompt,
专员表零参与,所以「选哪个专员说话都一样」。本次把
「专员 → 岗位说明书 → 技能」接进 system prompt。

P0 修 bug
  batch/extract 与 contract/review 直接把 nil 当路由传给 AI 层,
  取 primary.RouteID 时空指针,这两个接口必 500。
  llm.go 抽出 buildRouteChain,nil 主路由改为显式报错。

P1 数据层
  specialist 表加 rule_file_markdown(岗位说明书正文)与 allowed_skills
  (绑定技能,顺序即优先级)。写入侧用 model.ValidSkillKeys 校验,
  非法 key / 重复项直接 400,并规范化成紧凑 JSON 落库。
  播种 11 份岗位说明书初稿与技能绑定,沿用「存在则只补空字段」,
  管理员改过的内容不会被重启覆盖。

P2 打通链路
  请求体加 task_id 与 specialist_key。后端按任务反查专员(专员是任务的
  字段,任务优先),查不到安静退回通用助手而不是报错打断对话。
  ai_call_log 加 specialist_key,用来回答「这条回答是谁说的」。

P3 注入 prompt
  system prompt 改为 基础角色 + 【当前专员】+【可用技能】+【岗位说明书】,
  说明书放最后(离用户消息最近,优先级最高)。选了专员就不再自称
  「通用助手」——身份冲突正是老毛病的成因。

P4 前端
  对话带上当前任务与专员;专员详情页显示绑定的技能、读哪些信源、
  可以动什么。

验证
  go build / go vet / go test 全绿,45 项测试通过;前端构建通过。
  另在开发库副本上验过真实升级路径(AutoMigrate 补三列 + 播种补齐
  11 个专员),源库未被改动。
  新增防漂移测试:后端技能清单与前端 availableSkills 不一致即红灯;
  专员改名而说明书没同步也会红灯(这类静默失效最难查)。

注:llm.go / credits.go / seed.go / smart_assistant.go / api/specialist.go
同时含有并行进行中的改动(路由健康上报、清理调试埋点、技能与动作种子),
与本方案交织在同一批行内,无法单独拆出,一并随本次提交。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 21:32:31 +08:00

88 lines
2.6 KiB
Go

package ai
import (
"time"
"gorm.io/gorm"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/store"
)
// ──────────────────────────────────────────────
// 按用户算力点计费
// ──────────────────────────────────────────────
// AI 能力常量
const (
CapabilityAIChat = "ai_chat" // PathCoach 对话(每轮扣 1 点)
CapabilityTextGen = "text_gen" // 快捷动作(情景演练/查佣金/产品对比,扣 1 点)
CapabilityImageGen = "image_gen" // 文生图(按次扣点)
CapabilityEmbed = "embed" // 知识检索内部 embedding(不扣点,仅审计)
CapabilityEssayGrade = "essay_grade" // 简答题 LLM 评分(系统自动,不扣点,仅审计)
)
// CapabilityCredits 各能力扣点成本
var CapabilityCredits = map[string]int{
CapabilityAIChat: 1,
CapabilityTextGen: 1,
CapabilityImageGen: 1,
CapabilityEmbed: 0,
CapabilityEssayGrade: 0,
}
// ComputeCredits 扣点决策:仅「成功」才扣点;失败不扣。
func ComputeCredits(capability string, success bool) int {
if !success {
return 0
}
return CapabilityCredits[capability]
}
// LogEntry 一次 AI 调用的审计入参
type LogEntry struct {
UserID uint
Capability string
// SpecialistKey 本次调用以哪个专员的身份进行;空 = 通用助手
SpecialistKey string
Provider string
AIRouteID string
Model string
TokensInput int
TokensOutput int
Success bool
ErrorMessage string
LatencyMs int
}
// LogCall 写 ai_call_log;成功且需扣点时从用户余额扣点。审计写入失败不阻断主流程。
func LogCall(e LogEntry) {
credits := ComputeCredits(e.Capability, e.Success)
status := "success"
if !e.Success {
status = "failed"
}
rec := model.AiCallLog{
UserID: e.UserID,
Capability: e.Capability,
SpecialistKey: e.SpecialistKey,
Provider: e.Provider,
AIRouteID: e.AIRouteID,
Model: e.Model,
TokensInput: e.TokensInput,
TokensOutput: e.TokensOutput,
CreditsCharged: credits,
Status: status,
ErrorMessage: e.ErrorMessage,
LatencyMs: e.LatencyMs,
CreatedAt: time.Now(),
}
if err := store.DB.Create(&rec).Error; err != nil {
return
}
if credits > 0 {
store.DB.Model(&model.User{}).Where("id = ?", e.UserID).
UpdateColumn("ai_points", gorm.Expr("ai_points - ?", credits))
}
}