WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
package workbench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||||
"github.com/Tencent/WeKnora/internal/application/repository"
|
||||
apperrors "github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func actionName(action string) string {
|
||||
if action == "review-settings" {
|
||||
return "保存质检规则与风格"
|
||||
}
|
||||
if name := modelActionNames[action]; name != "" {
|
||||
return name
|
||||
}
|
||||
if name := map[string]string{"generate": "生成内容", "generate-day": "生成每日连线框架", "decompose": "拆解内容段", "import-image": "识别导图图片", "revise": "按建议修改", "review": "跨稿质检", "compose": "组合成稿", "accept": "采用内容段", "skip": "跳过内容段", "confirm": "确认训练营方案", "confirm-day": "确认每日框架", "create": "创建", "update": "修改", "delete": "删除", "cancel": "取消任务", "pause": "暂停任务", "resume": "继续任务"}[action]; name != "" {
|
||||
return name
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
var modelActionNames = map[string]string{
|
||||
"knowledge_classification_recommend": "AI 推荐知识分类",
|
||||
"digital_human_distillation": "蒸馏数字人",
|
||||
"digital_human_trial": "数字人版本试写",
|
||||
"document_summary": "生成资料摘要",
|
||||
"document_auto_tag": "自动分类知识资料",
|
||||
"question_generation": "生成资料问题",
|
||||
"knowledge_qa": "知识问答",
|
||||
"agent_round": "Agent 推理与回答",
|
||||
"agent_context_compaction": "整理对话上下文",
|
||||
"follow_up.suggestions": "生成后续问题建议",
|
||||
"web_fetch_summary": "生成网页摘要",
|
||||
"query_rewrite": "优化检索问题",
|
||||
"data_analysis_plan": "生成数据分析计划",
|
||||
"entity_extraction": "提取知识实体",
|
||||
"wiki_page_modify": "修改 Wiki 页面",
|
||||
"wiki_chunk_citation": "整理 Wiki 来源引用",
|
||||
"wiki_candidate_slug": "整理 Wiki 页面名称",
|
||||
"wiki_summary": "生成 Wiki 摘要",
|
||||
"wiki_knowledge_extract": "提取 Wiki 知识",
|
||||
"wiki_taxonomy_plan": "规划 Wiki 分类",
|
||||
"wiki_deduplication": "合并 Wiki 重复内容",
|
||||
"wiki_index_intro": "生成 Wiki 目录介绍",
|
||||
"wiki_generation": "生成 Wiki 内容",
|
||||
}
|
||||
|
||||
func operationsScope(ctx context.Context, db *gorm.DB, adminAll bool) (*gorm.DB, error) {
|
||||
t, u, admin, e := identity(ctx)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
q := db.Where("tenant_id = ?", t)
|
||||
if !admin || !adminAll {
|
||||
q = q.Where("owner_id = ?", u)
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (h *Handler) Balance(c *gin.Context) {
|
||||
t, u, _, e := identity(c.Request.Context())
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
row := Balance{TenantID: t, UserID: u}
|
||||
e = h.service.db.WithContext(c.Request.Context()).Where("tenant_id=? AND user_id=?", t, u).Take(&row).Error
|
||||
if e != nil && !errors.Is(e, gorm.ErrRecordNotFound) {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, gin.H{"balance": row.Balance, "available": max(int64(0), row.Balance-row.Reserved), "reserved": row.Reserved})
|
||||
}
|
||||
|
||||
func (h *Handler) Accounts(c *gin.Context) {
|
||||
t, _, admin, e := identity(c.Request.Context())
|
||||
if e != nil || !admin {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
rows := []struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Balance int64 `json:"balance"`
|
||||
Reserved int64 `json:"reserved"`
|
||||
}{}
|
||||
e = repository.TenantUsersQuery(h.service.db.WithContext(c.Request.Context()), t).
|
||||
Select("m.user_id, u.username AS user_name, COALESCE(b.balance,0) AS balance, COALESCE(b.reserved,0) AS reserved").
|
||||
Joins("LEFT JOIN workbench_balances b ON b.tenant_id=m.tenant_id AND b.user_id=m.user_id").
|
||||
Order("u.username,u.id").Scan(&rows).Error
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, rows)
|
||||
}
|
||||
|
||||
func (s *Service) grant(ctx context.Context, user string, amount int64, reason, key string) (*Transaction, error) {
|
||||
t, actor, admin, e := identity(ctx)
|
||||
if e != nil || !admin {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
if user == "" || amount <= 0 || amount > 1_000_000_000_000 || len(key) < 8 || len(key) > 120 || len([]rune(reason)) > 1000 {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
var result Transaction
|
||||
e = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var member types.TenantMember
|
||||
if e := repository.TenantUsersQuery(tx, t).Select("m.*").Where("m.user_id=?", user).Take(&member).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
if e := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&Balance{TenantID: t, UserID: user}).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
var account Balance
|
||||
if e := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("tenant_id=? AND user_id=?", t, user).Take(&account).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
key = "grant:" + key
|
||||
e := tx.Where("tenant_id=? AND idempotency_key=?", t, key).Take(&result).Error
|
||||
if e == nil {
|
||||
if result.UserID != user || result.Delta != amount || result.Reason != reason {
|
||||
return ErrConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(e, gorm.ErrRecordNotFound) {
|
||||
return e
|
||||
}
|
||||
if account.Balance > 9_000_000_000_000_000-amount {
|
||||
return ErrInvalid
|
||||
}
|
||||
result = Transaction{ID: uuid.NewString(), TenantID: t, UserID: user, Delta: amount, Kind: "credit", Reason: reason, ActorID: actor, IdempotencyKey: key, Metadata: JSON{}, CreatedAt: time.Now()}
|
||||
if e = tx.Create(&result).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
return tx.Model(&account).Updates(map[string]any{"balance": account.Balance + amount, "updated_at": time.Now()}).Error
|
||||
})
|
||||
return &result, e
|
||||
}
|
||||
|
||||
func (h *Handler) GrantBalance(c *gin.Context) {
|
||||
var body struct {
|
||||
UserID string `json:"user_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
Reason string `json:"reason"`
|
||||
Key string `json:"idempotency_key"`
|
||||
}
|
||||
if e := c.ShouldBindJSON(&body); e != nil {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
row, e := h.service.grant(c.Request.Context(), body.UserID, body.Amount, strings.TrimSpace(body.Reason), body.Key)
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, row)
|
||||
}
|
||||
|
||||
func operationsPage(c *gin.Context) (int, int) {
|
||||
p, _ := strconv.Atoi(c.Query("page"))
|
||||
n, _ := strconv.Atoi(c.Query("page_size"))
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if n < 1 {
|
||||
n = 50
|
||||
}
|
||||
return min(p, 100000), min(n, 200)
|
||||
}
|
||||
|
||||
func (h *Handler) OperationRecords(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
t, u, admin, e := identity(ctx)
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
kind := c.Param("recordKind")
|
||||
page, size := operationsPage(c)
|
||||
db := h.service.db.WithContext(ctx)
|
||||
var rows any
|
||||
var q *gorm.DB
|
||||
switch kind {
|
||||
case "transactions":
|
||||
if !admin {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
rows = &[]Transaction{}
|
||||
q = db.Model(&Transaction{}).Where("tenant_id=?", t)
|
||||
if user := c.Query("user_id"); user != "" {
|
||||
q = q.Where("user_id=?", user)
|
||||
}
|
||||
case "usage":
|
||||
if !admin {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
rows = &[]Usage{}
|
||||
q = db.Model(&Usage{}).Where("tenant_id=?", t)
|
||||
if user := c.Query("user_id"); user != "" {
|
||||
q = q.Where("owner_id=?", user)
|
||||
}
|
||||
case "audit", "system":
|
||||
if !admin {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
rows = &[]OperationLog{}
|
||||
q = db.Model(&OperationLog{}).Where("tenant_id=? AND kind=?", t, kind)
|
||||
if kind == "system" {
|
||||
q = q.Where("level NOT IN ?", []string{"warn", "warning"})
|
||||
}
|
||||
if search := strings.TrimSpace(c.Query("q")); search != "" {
|
||||
q = q.Where("message LIKE ? OR action LIKE ? OR owner_name LIKE ? OR trace_id LIKE ?", "%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
case "consumption":
|
||||
if !accesspolicy.Allowed(ctx, "knowledge.read") {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
rows = &[]Consumption{}
|
||||
q = db.Model(&Consumption{}).Where("tenant_id=?", t)
|
||||
if !admin || c.Query("all_users") != "true" {
|
||||
q = q.Where("owner_id=?", u)
|
||||
}
|
||||
if kb := c.Query("kb_id"); kb != "" {
|
||||
q = q.Where("kb_id=?", kb)
|
||||
}
|
||||
if state := c.Query("status"); state != "" {
|
||||
q = q.Where("status=?", state)
|
||||
}
|
||||
case "memories":
|
||||
if !accesspolicy.Allowed(ctx, "agents.read") {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
rows = &[]PendingMemory{}
|
||||
q = db.Model(&PendingMemory{}).Where("tenant_id=? AND owner_id=?", t, u)
|
||||
if state := c.Query("status"); state != "" {
|
||||
q = q.Where("status=?", state)
|
||||
}
|
||||
default:
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
var total int64
|
||||
if e = q.Count(&total).Error; e == nil {
|
||||
e = q.Order("created_at DESC,id DESC").Offset((page - 1) * size).Limit(size).Find(rows).Error
|
||||
}
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if memories, ok := rows.(*[]PendingMemory); ok {
|
||||
h.service.decorateLegacyMemories(ctx, *memories)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": rows, "total": total})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateConsumption(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
t, u, _, e := identity(ctx)
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if !accesspolicy.Allowed(ctx, "knowledge.consumption") {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []string `json:"ids"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
if c.ShouldBindJSON(&body) != nil || len(body.IDs) < 1 || len(body.IDs) > 1000 || (body.Action != "open" && body.Action != "ban") {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
e = h.service.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var rows []Consumption
|
||||
if e := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("tenant_id=? AND owner_id=? AND id IN ?", t, u, body.IDs).Find(&rows).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
if len(rows) != len(body.IDs) {
|
||||
return ErrForbidden
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.Status == "reserved" && r.LeaseUntil != nil && r.LeaseUntil.After(time.Now()) {
|
||||
return fmt.Errorf("%w: 资料正在生成中使用,请等待任务结束", ErrConflict)
|
||||
}
|
||||
}
|
||||
return tx.Model(&Consumption{}).Where("tenant_id=? AND owner_id=? AND id IN ?", t, u, body.IDs).Updates(map[string]any{"status": "open", "banned": body.Action == "ban", "job_id": "", "lease_until": nil, "updated_at": time.Now()}).Error
|
||||
})
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, gin.H{"updated": len(body.IDs)})
|
||||
}
|
||||
|
||||
func (h *Handler) ReviewMemory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
t, u, _, e := identity(ctx)
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if !accesspolicy.Allowed(ctx, "agents.memory") {
|
||||
respondError(c, ErrForbidden)
|
||||
return
|
||||
}
|
||||
var body memoryReviewRequest
|
||||
if c.ShouldBindJSON(&body) != nil || (body.Status != "accepted" && body.Status != "rejected") {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
var row PendingMemory
|
||||
e = h.service.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if e := tx.Where("id=? AND tenant_id=? AND owner_id=?", c.Param("id"), t, u).Take(&row).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
if body.ReconfirmLegacy {
|
||||
return h.service.reconfirmLegacyMemory(ctx, tx, &row, body)
|
||||
}
|
||||
if body.Status == "accepted" {
|
||||
if e := lockContentFeedbackSource(tx, &row); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
if e := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=? AND tenant_id=? AND owner_id=?", c.Param("id"), t, u).Take(&row).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
if row.Status != "pending" {
|
||||
return ErrConflict
|
||||
}
|
||||
if body.Content != "" {
|
||||
row.Content = strings.TrimSpace(body.Content)
|
||||
}
|
||||
if row.Content == "" || len([]rune(row.Content)) > 12000 {
|
||||
return ErrInvalid
|
||||
}
|
||||
if body.Status == "accepted" {
|
||||
if len([]rune(row.Content)) > types.MemoryContentMaxRunes {
|
||||
return fmt.Errorf("%w: 长期记忆最多 %d 字,请精简后再确认;当前候选仍保留", ErrInvalid, types.MemoryContentMaxRunes)
|
||||
}
|
||||
if h.service.rememberInTransaction == nil {
|
||||
return fmt.Errorf("%w: 记忆服务未初始化", ErrInvalid)
|
||||
}
|
||||
item := types.MemoryItem{Kind: types.MemoryKindFact, Content: row.Content, Origin: types.MemoryOriginExplicit, Importance: 3}
|
||||
if strings.HasPrefix(row.SourceRef, contentMemorySourcePrefix) {
|
||||
item.Kind = types.MemoryKindPreference
|
||||
item.SourceMessageID = row.ID
|
||||
}
|
||||
stored, err := h.service.rememberInTransaction(ctx, tx, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stored == nil || stored.Status != types.MemoryStatusActive {
|
||||
return fmt.Errorf("%w: 记忆尚未写入,请重试", ErrInvalid)
|
||||
}
|
||||
// Approval history must show the sanitized content actually retained.
|
||||
row.Content = stored.Content
|
||||
}
|
||||
row.Status, row.ReviewedBy, row.UpdatedAt = body.Status, u, time.Now()
|
||||
return tx.Save(&row).Error
|
||||
})
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, row)
|
||||
}
|
||||
|
||||
func (s *Service) recordOperation(ctx context.Context, row OperationLog) error {
|
||||
t, ok := types.TenantIDFromContext(ctx)
|
||||
if !ok || t == 0 {
|
||||
return ErrForbidden
|
||||
}
|
||||
u, _ := types.UserIDFromContext(ctx)
|
||||
row.ID, row.TenantID, row.OwnerID, row.CreatedAt = uuid.NewString(), t, u, time.Now()
|
||||
if u != "" && !types.IsSyntheticUserID(u) {
|
||||
row.OwnerName = u
|
||||
var actor struct{ Username string }
|
||||
if e := s.db.WithContext(ctx).Table("users").Select("username").Where("id=?", u).Take(&actor).Error; e == nil && actor.Username != "" {
|
||||
row.OwnerName = actor.Username
|
||||
}
|
||||
} else {
|
||||
row.OwnerName = "系统任务"
|
||||
}
|
||||
if row.Metadata == nil {
|
||||
row.Metadata = JSON{}
|
||||
}
|
||||
return s.db.WithContext(ctx).Create(&row).Error
|
||||
}
|
||||
|
||||
// The outer ErrorHandler may not have rendered a pending error yet.
|
||||
func operationHTTPStatus(c *gin.Context) int {
|
||||
if c.Writer.Written() || len(c.Errors) == 0 {
|
||||
return c.Writer.Status()
|
||||
}
|
||||
var appError *apperrors.AppError
|
||||
if errors.As(c.Errors.Last().Err, &appError) && appError.HTTPCode >= 400 && appError.HTTPCode <= 599 {
|
||||
return appError.HTTPCode
|
||||
}
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// Request logs store route names and safe diagnostics, never request bodies or keys.
|
||||
func (h *Handler) OperationAudit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
started := time.Now()
|
||||
c.Next()
|
||||
ctx := c.Request.Context()
|
||||
_, _, _, e := identity(ctx)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
status := operationHTTPStatus(c)
|
||||
write := c.Request.Method != "GET" && c.Request.Method != "HEAD" && c.Request.Method != "OPTIONS"
|
||||
if !write && status < 400 {
|
||||
return
|
||||
}
|
||||
if strings.Contains(path, "/notifications/") || strings.Contains(path, "/auth/") {
|
||||
return
|
||||
}
|
||||
action := map[string]string{"POST": "创建或执行", "PUT": "修改", "PATCH": "修改", "DELETE": "删除", "GET": "访问"}[c.Request.Method]
|
||||
module := "系统"
|
||||
for _, entry := range [][2]string{{"training-maps", "训练营导图"}, {"content-tasks", "内容任务"}, {"digital-humans", "数字人"}, {"knowledge", "知识库"}, {"agents", "Agent"}, {"skills", "技能"}, {"memory", "记忆"}, {"access", "访问控制"}, {"models", "模型"}, {"flows", "画布"}, {"operations", "业务管理"}} {
|
||||
if strings.Contains(c.Request.URL.Path, entry[0]) {
|
||||
module = entry[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if a := c.Param("action"); a != "" {
|
||||
action = actionName(a)
|
||||
}
|
||||
message := module + ":" + action
|
||||
level := "info"
|
||||
if status >= 400 {
|
||||
level = "warn"
|
||||
if status >= 500 {
|
||||
level = "error"
|
||||
}
|
||||
message += fmt.Sprintf("失败(HTTP %d)", status)
|
||||
}
|
||||
trace := c.Writer.Header().Get("X-Request-ID")
|
||||
if trace == "" {
|
||||
trace = uuid.NewString()
|
||||
}
|
||||
finish, cancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
|
||||
defer cancel()
|
||||
row := OperationLog{Kind: "audit", Level: level, Action: action, Message: message, ResourceID: c.Param("id"), TraceID: trace, Status: status, Metadata: JSON{"method": c.Request.Method, "route": path, "duration_ms": time.Since(started).Milliseconds()}}
|
||||
if status >= 400 {
|
||||
row.Metadata["error_class"] = fmt.Sprintf("http_%d", status)
|
||||
if last := c.Errors.Last(); last != nil {
|
||||
for key, value := range jobErrorMetadata(last.Err) {
|
||||
row.Metadata[key] = value
|
||||
}
|
||||
row.Message += ";" + row.Metadata["error_message"].(string)
|
||||
}
|
||||
}
|
||||
if e = h.service.recordOperation(finish, row); e != nil {
|
||||
logJobFailure(finish, nil, "audit", e)
|
||||
}
|
||||
if status >= 400 {
|
||||
row.Kind = "system"
|
||||
if status == 403 {
|
||||
row.Message = "权限不足:" + module + " / " + action
|
||||
if label := c.GetString("required_permission_label"); label != "" {
|
||||
row.Message = "权限不足:" + label
|
||||
}
|
||||
}
|
||||
if e = h.service.recordOperation(finish, row); e != nil {
|
||||
logJobFailure(finish, nil, "system_log", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user