Merge remote-tracking branch 'upstream/main' into main (保留常驻通知与顺延迁移版本)

This commit is contained in:
✨await
2026-09-17 23:25:50 +08:00
120 changed files with 9314 additions and 1625 deletions

View File

@@ -22,7 +22,7 @@ func RegisterAgentRoutes(r *gin.RouterGroup, svc *service.Service) {
return
}
capabilities := service.CloudAgentCapabilitySetInfo()
ok(c, gin.H{"version": 2, "permissionModes": []string{"read_only", "request_approval", "auto"}, "contextScopes": []string{"canvas"}, "skills": true, "writeTools": true, "billing": "fixed_request", "maxHistoryPairs": 0, "maxHistoryBytes": 64000, "maxSteps": 0, "tools": service.CloudAgentSupportedToolNames(), "capabilitySetVersion": capabilities.Version, "capabilitySetHash": capabilities.Hash, "nodeTypes": capabilities.Nodes})
ok(c, gin.H{"version": 2, "permissionModes": []string{"read_only", "request_approval", "auto"}, "contextScopes": []string{"canvas"}, "skills": true, "writeTools": true, "billing": "fixed_request", "maxHistoryPairs": 10, "maxHistoryBytes": 64000, "maxSteps": 0, "tools": service.CloudAgentSupportedToolNames(), "capabilitySetVersion": capabilities.Version, "capabilitySetHash": capabilities.Hash, "nodeTypes": capabilities.Nodes})
})
// Profiles are durable preference data, not an authorization surface. The
// service validates scope ownership and the compiler injects the effective
@@ -130,6 +130,50 @@ func RegisterAgentRoutes(r *gin.RouterGroup, svc *service.Service) {
r.POST("/agent/runs", create)
// Each additional message starts a new immutable turn and returns its ID.
r.POST("/agent/runs/:id/messages", create)
r.POST("/agent/runs/:id/interjections", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
policy, available := loadRuntimePolicy(c, svc)
if !available || !enforceRateLimit(c, "tasks:"+user.ID, policy.Request.TaskCreatePerMinute, time.Minute) {
return
}
if _, err = svc.CloudAgentRun(user.ID, c.Param("id")); err != nil {
failService(c, err)
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 64<<10)
var req struct {
Text string `json:"text"`
MessageID string `json:"messageId"`
}
decoder := json.NewDecoder(c.Request.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
if req.Text != "" && (!utf8.ValidString(req.Text) || utf8.RuneCountInString(req.Text) > 4000 || strings.ContainsRune(req.Text, 0)) {
fail(c, http.StatusBadRequest, errors.New("text 无效"))
return
}
if req.MessageID != "" && (!utf8.ValidString(req.MessageID) || utf8.RuneCountInString(req.MessageID) > 160) {
fail(c, http.StatusBadRequest, errors.New("messageId 无效"))
return
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
fail(c, http.StatusBadRequest, errors.New("请求必须只包含一个 JSON 对象"))
return
}
pending, err := svc.InterjectCloudAgent(user.ID, c.Param("id"), req.MessageID, req.Text)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"accepted": true, "pending": pending})
})
r.GET("/agent/runs/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {

View File

@@ -0,0 +1,236 @@
package handler
import (
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"strings"
"infinite-canvas/backend/internal/service"
"github.com/gin-gonic/gin"
)
func RegisterAgentMemoryRoutes(r *gin.RouterGroup, svc *service.Service) {
r.GET("/agent/memories", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
limit, _ := strconv.Atoi(c.Query("limit"))
memories, err := svc.UserAgentMemories(user.ID, strings.TrimSpace(c.Query("status")), limit)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"memories": memories})
})
r.POST("/agent/memories", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.AgentMemoryRequest
if err := decodeAgentJSON(c, &req, 64<<10); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
view, err := svc.CreateUserAgentMemory(user.ID, req)
if err != nil {
failService(c, err)
return
}
ok(c, view)
})
r.GET("/agent/memories/export", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
bundle, err := svc.ExportUserAgentMemories(user.ID)
if err != nil {
failService(c, err)
return
}
ok(c, bundle)
})
r.POST("/agent/memories/import", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var bundle service.AgentMemoryBundle
if err := decodeAgentJSON(c, &bundle, 512<<10); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
result, err := svc.ImportUserAgentMemories(user.ID, bundle)
if err != nil {
failService(c, err)
return
}
ok(c, result)
})
r.GET("/agent/memories/settings", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
view, err := svc.UserAgentMemoryCompact(user.ID)
if err != nil {
failService(c, err)
return
}
ok(c, view)
})
r.PATCH("/agent/memories/settings", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.AgentMemorySettingRequest
if err := decodeAgentJSON(c, &req, 8<<10); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
view, err := svc.UpdateUserAgentMemorySetting(user.ID, req)
if err != nil {
failService(c, err)
return
}
ok(c, view)
})
r.POST("/agent/memories/compact", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.AgentMemoryCompactRequest
if err := decodeAgentJSON(c, &req, 8<<10); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
view, err := svc.CompactUserAgentMemories(user.ID, req)
if err != nil {
failService(c, err)
return
}
ok(c, view)
})
r.PATCH("/agent/memories/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.AgentMemoryRequest
if err := decodeAgentJSON(c, &req, 64<<10); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
view, err := svc.UpdateUserAgentMemory(user.ID, c.Param("id"), req)
if err != nil {
failService(c, err)
return
}
ok(c, view)
})
r.POST("/agent/memories/:id/decide", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var request struct {
Decision string `json:"decision"`
}
if err := decodeAgentJSON(c, &request, 4<<10); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
if err := svc.DecideUserAgentMemory(user.ID, c.Param("id"), request.Decision); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"id": c.Param("id"), "decision": request.Decision})
})
r.DELETE("/agent/memories/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.DeleteUserAgentMemory(user.ID, c.Param("id")); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"id": c.Param("id")})
})
}
func RegisterAgentLessonAdminRoutes(r *gin.RouterGroup, svc *service.Service) {
admin := r.Group("/admin/agent-lessons")
admin.GET("", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.RequireAdmin(user); err != nil {
failService(c, err)
return
}
limit, _ := strconv.Atoi(c.Query("limit"))
lessons, err := svc.AdminAgentLessons(
strings.TrimSpace(c.Query("status")),
strings.TrimSpace(c.Query("userId")),
strings.TrimSpace(c.Query("keyword")),
limit,
)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"lessons": lessons})
})
admin.DELETE("/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.RequireAdmin(user); err != nil {
failService(c, err)
return
}
if err := svc.AdminDeleteAgentLesson(c.Param("id")); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"id": c.Param("id")})
})
}
func decodeAgentJSON(c *gin.Context, dest any, maxBytes int64) error {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes)
decoder := json.NewDecoder(c.Request.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(dest); err != nil {
return err
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return errors.New("请求必须只包含一个 JSON 对象")
}
return nil
}

View File

@@ -15,8 +15,10 @@ func RegisterCanvasAPI(api *gin.RouterGroup, svc *service.Service) {
RegisterAppearanceRoutes(api, svc)
RegisterFeatureAvailabilityRoutes(api, svc)
RegisterAgentRoutes(api, svc)
RegisterAgentMemoryRoutes(api, svc)
RegisterResponseInterceptionRoutes(api, svc)
RegisterAdminRoutes(api, svc)
RegisterAgentLessonAdminRoutes(api, svc)
RegisterAdminAnalyticsRoutes(api, svc)
RegisterAdminStorageRoutes(api, svc)
RegisterAdminUpdateRoutes(api, svc)

View File

@@ -40,7 +40,7 @@ paths:
summary: 读取云端 Agent 能力
responses:
"200":
description: 信封 data 包含 version、permissionModes、contextScopes、skills、writeTools、billing、maxHistoryPairs、maxSteps、maxHistoryBytes、capabilitySetVersion、capabilitySetHash、nodeTypes、tools;maxHistoryPairs 和 maxSteps 为 0 表示无固定轮数上限,历史仍受 maxHistoryBytes 限制
description: 信封 data 包含 version、permissionModes、contextScopes、skills、writeTools、billing、maxHistoryPairs、maxSteps、maxHistoryBytes、capabilitySetVersion、capabilitySetHash、nodeTypes、tools;maxHistoryPairs 为跨轮保留的用户对话轮数,maxSteps 为 0 表示无固定模型调用上限,历史仍受 maxHistoryBytes 限制
/agent/profile:
get:
tags: [agent]
@@ -78,6 +78,204 @@ paths:
description: 返回更新后的生效偏好视图
"409":
description: 偏好 revision 已变化,须重新读取后保存
/agent/memories:
get:
tags: [agent]
summary: 列出当前用户的 Agent 记忆
parameters:
- in: query
name: status
schema:
type: string
enum: [pending, approved, rejected]
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 500
responses:
"200":
description: 信封 data.memories
"401":
$ref: "#/components/responses/Error"
post:
tags: [agent]
summary: 手动添加一条个人记忆(直接批准)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [topic, category, situation]
properties:
topic: { type: string }
category: { type: string }
situation: { type: string }
lesson: { type: string }
source: { type: string }
steps:
type: array
items:
type: object
properties:
tool: { type: string }
action: { type: string }
note: { type: string }
responses:
"200":
$ref: "#/components/responses/Envelope"
"400":
$ref: "#/components/responses/Error"
"401":
$ref: "#/components/responses/Error"
/agent/memories/export:
get:
tags: [agent]
summary: 导出当前用户的 Agent 记忆
responses:
"200":
description: 信封 data 为 version/kind/exportedAt/memories
"401":
$ref: "#/components/responses/Error"
/agent/memories/import:
post:
tags: [agent]
summary: 导入 Agent 记忆 JSON
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
version: { type: integer }
kind: { type: string }
memories: { type: array }
responses:
"200":
description: 信封 data 含 imported、merged、skipped
"400":
$ref: "#/components/responses/Error"
"401":
$ref: "#/components/responses/Error"
/agent/memories/settings:
get:
tags: [agent]
summary: 读取当前用户的记忆压缩设置与最近一次压缩状态
responses:
"200":
description: 信封 data 含 compactInterval、模型字段、lastStatus、summary
"401":
$ref: "#/components/responses/Error"
patch:
tags: [agent]
summary: 更新记忆压缩周期和文本模型
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
compactInterval:
type: string
enum: [off, daily, weekly, monthly]
logicalModelId: { type: string }
channelId: { type: string }
channelModelKey: { type: string }
model: { type: string }
responses:
"200":
$ref: "#/components/responses/Envelope"
"400":
$ref: "#/components/responses/Error"
"401":
$ref: "#/components/responses/Error"
/agent/memories/compact:
post:
tags: [agent]
summary: 提交一次个人记忆压缩任务(走用户文本模型计费)
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
logicalModelId: { type: string }
channelId: { type: string }
channelModelKey: { type: string }
model: { type: string }
responses:
"200":
description: 信封 data 为压缩设置视图,lastStatus 为 queued
"400":
$ref: "#/components/responses/Error"
"401":
$ref: "#/components/responses/Error"
/agent/memories/{id}:
parameters:
- in: path
name: id
required: true
schema: { type: string }
patch:
tags: [agent]
summary: 更新一条属于当前用户的记忆
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
"200":
$ref: "#/components/responses/Envelope"
"401":
$ref: "#/components/responses/Error"
"404":
$ref: "#/components/responses/Error"
delete:
tags: [agent]
summary: 删除一条属于当前用户的记忆
responses:
"200":
$ref: "#/components/responses/Envelope"
"401":
$ref: "#/components/responses/Error"
"404":
$ref: "#/components/responses/Error"
/agent/memories/{id}/decide:
post:
tags: [agent]
summary: 批准或拒绝一条属于当前用户的待审记忆
parameters:
- in: path
name: id
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [decision]
properties:
decision:
type: string
enum: [approve, reject]
responses:
"200":
$ref: "#/components/responses/Envelope"
"400":
$ref: "#/components/responses/Error"
"401":
$ref: "#/components/responses/Error"
"404":
$ref: "#/components/responses/Error"
/agent/runs:
post:
tags: [agent]
@@ -123,6 +321,33 @@ paths:
description: 信封 data.run(新的 ID)
"409":
description: 父轮仍在执行或幂等键冲突
/agent/runs/{id}/interjections:
parameters:
- $ref: "#/components/parameters/AgentRunID"
post:
tags: [agent]
summary: 向正在运行的 Agent 轮次插入一句话
description: 不新建轮次、不产生新的扣费身份。插话排进当前运行,在下一次模型调用前送达;运行已结束返回 409。
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [text, messageId]
properties:
text:
type: string
maxLength: 4000
messageId:
type: string
maxLength: 160
responses:
"200":
description: 信封 data.accepted 与 data.pending
"409":
description: 本轮已结束,应改为新一轮发送
/agent/runs/{id}/cancel:
parameters:
- $ref: "#/components/parameters/AgentRunID"
@@ -267,6 +492,55 @@ paths:
$ref: "#/components/responses/Error"
"404":
$ref: "#/components/responses/Error"
/admin/agent-lessons:
get:
tags: [admin]
summary: 按用户筛选查看个人 Agent 记忆
parameters:
- in: query
name: status
schema:
type: string
enum: [pending, approved, rejected]
- in: query
name: userId
schema:
type: string
- in: query
name: keyword
schema:
type: string
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 200
responses:
"200":
description: 信封 data.lessons 含解析后的 steps、authorUserId、authorUsername、authorDisplayName
"401":
$ref: "#/components/responses/Error"
"403":
$ref: "#/components/responses/Error"
/admin/agent-lessons/{id}:
delete:
tags: [admin]
summary: 删除一条用户的 Agent 记忆
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
"200":
$ref: "#/components/responses/Envelope"
"401":
$ref: "#/components/responses/Error"
"403":
$ref: "#/components/responses/Error"
"404":
$ref: "#/components/responses/Error"
/auth/session:
get:
tags: [auth]