按桌面客户端统一方案 v0.1 + agent_management Sub Mode Runtime 对接,强制全量统一,不留兼容。
命名统一(强制,无兼容):
- 全仓 agnet/Agnet/AGNET → agent/Agent/AGENT:后端 Go(路由 /api/agent/*、env AGENT_*、
结构体/函数、19 个文件改名)、前端(agent-console/agent-hub、/api/agent 调用、i18n)、
DB(表 agent_*、列 agent_id)、compose/.env、文档、脚本。
- DB 加幂等迁移 renameAgnetTablesToAgent():启动时 rename 老 agnet_* 表/列,保住生产数据。
统一方案核心(10 项):
- callback 统一 /api/agent/callbacks/runtime-events(路由/广播URL/函数名)。
- artifact 兜底判定改用 Runtime 权威信号 metadata.synthesized(§7.2)+ 结构化 artifact_type。
- Manager→Runtime 路径对齐 /api/agent/sub-agile/deployments(§2.2),{deployment_id} 回退 swarm_id。
- 状态裁决 display_status:Manager 唯一裁判,completed 无有效产物→needs_codegen/
completed_without_deliverable(§10.6),接入 detail/timeline/workflow。
- GET /api/heicode/capabilities 能力发现(§6)。
- 模型策略 per_role(role_models)+ 收集 allowed_model_ids(§9)。
- resource_binding_id→secret_ref 服务端解析,客户端不再 inline secret_ref(§17.6)。
- 客户端统一路由层 /api/heicode/sub-agile|swarm/*(task≡deployment,复用控制面)+ workflow 投影。
- 日志分层 user_logs/debug_logs(§13)。
验证:go build ./... + go test(controller/router/model/middleware)全绿;前端 tsc -b + rsbuild build 通过。
待部署:VM .env 的 AGNET_*→AGENT_*;启动迁移自动 rename 表;其他三仓库需同步切到 /api/agent。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
5.4 KiB
Go
149 lines
5.4 KiB
Go
package model
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/heicode/manager/common"
|
|
)
|
|
|
|
// AgentAuditEvent is the persistent audit-trail row for the Agent
|
|
// control-plane. Each row records one observable transition in the
|
|
// Agent lifecycle — deployment accepted, instance phase changed, SK
|
|
// snapshot refreshed, etc. — so admins can answer the "who / when /
|
|
// for which task / against which resource / with what result" set of
|
|
// questions even after the Manager container restarts.
|
|
//
|
|
// Before this table existed the control-plane stashed events in an
|
|
// in-process `map[string][]agentEvent` (controller/agent_control_plane
|
|
// .go:219). Every restart wiped audit history — unacceptable for a
|
|
// product where the audit page is part of the security story.
|
|
//
|
|
// Schema notes
|
|
// - All string columns are bounded varchar to keep MySQL happy.
|
|
// - `OccurredAt` is unix milliseconds so cross-DB ORDER BY / range
|
|
// queries are trivial without a TIMESTAMP-with-timezone dance.
|
|
// - `DetailsJSON` is a free-form payload bucket for fields we don't
|
|
// want to promote to first-class columns yet (e.g. failure_reason,
|
|
// phase, callback_url). Add a real column when a field is queried
|
|
// enough to need an index.
|
|
// - Indexes are picked for the dashboard queries: filter by user_id,
|
|
// filter by deployment, time-range scan.
|
|
//
|
|
// Cross-DB compatibility (CLAUDE.md Rule 2): GORM AutoMigrate maps the
|
|
// tags to the correct types on SQLite / MySQL / PostgreSQL. No raw
|
|
// SQL. No DB-specific column types.
|
|
type AgentAuditEvent struct {
|
|
Id int `gorm:"primaryKey" json:"id"`
|
|
EventID string `gorm:"type:varchar(64);uniqueIndex" json:"event_id"`
|
|
Event string `gorm:"type:varchar(64);index" json:"event"`
|
|
Actor string `gorm:"type:varchar(64)" json:"actor"`
|
|
Resource string `gorm:"type:varchar(128);index" json:"resource"`
|
|
UserID string `gorm:"type:varchar(64);index" json:"user_id"`
|
|
ChannelID string `gorm:"type:varchar(64)" json:"channel_id"`
|
|
BindingScope string `gorm:"type:varchar(64);index" json:"binding_scope"`
|
|
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
|
|
CorrelationID string `gorm:"type:varchar(64)" json:"correlation_id"`
|
|
RequestID string `gorm:"type:varchar(64)" json:"request_id"`
|
|
Result string `gorm:"type:varchar(16)" json:"result"`
|
|
SchemaVersion int `gorm:"default:1" json:"schema_version"`
|
|
OccurredAt int64 `gorm:"bigint;index" json:"occurred_at"`
|
|
DetailsJSON string `gorm:"type:text" json:"details_json,omitempty"`
|
|
}
|
|
|
|
// TableName pins the migration target so we can rename the Go struct
|
|
// later without breaking the deployed schema.
|
|
func (AgentAuditEvent) TableName() string {
|
|
return "agent_audit_events"
|
|
}
|
|
|
|
// InsertAgentAuditEvent best-effort persists one audit row. Callers
|
|
// invoke this in a hot path (right after mutating a deployment), so:
|
|
// - errors are logged but never returned — the audit write must NOT
|
|
// fail the user-facing API
|
|
// - DB is nil-guarded so unit tests / partial-init binaries don't
|
|
// panic on a missing connection
|
|
func InsertAgentAuditEvent(evt *AgentAuditEvent) {
|
|
if DB == nil || evt == nil {
|
|
return
|
|
}
|
|
if evt.OccurredAt == 0 {
|
|
evt.OccurredAt = time.Now().UnixMilli()
|
|
}
|
|
if evt.SchemaVersion == 0 {
|
|
evt.SchemaVersion = 1
|
|
}
|
|
if evt.Result == "" {
|
|
evt.Result = "ok"
|
|
}
|
|
if err := DB.Create(evt).Error; err != nil {
|
|
common.SysLog("InsertAgentAuditEvent: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// ListAgentAuditEventsFilter narrows the audit query to a slice of
|
|
// dashboard relevant rows. Zero-value fields are ignored — callers
|
|
// pass only the filters they care about.
|
|
type ListAgentAuditEventsFilter struct {
|
|
UserID string
|
|
BindingScope string
|
|
DeploymentID string
|
|
EventLike string // prefix filter on Event column, e.g. "deployment."
|
|
FromMs int64 // inclusive
|
|
ToMs int64 // exclusive; zero == no upper bound
|
|
Limit int // defaults to 200
|
|
Offset int
|
|
}
|
|
|
|
// ListAgentAuditEvents pages over audit rows ordered newest-first.
|
|
// Used by the /api/agent/audit-logs endpoint and the future task-
|
|
// scoped audit drawer.
|
|
func ListAgentAuditEvents(f ListAgentAuditEventsFilter) ([]AgentAuditEvent, int64, error) {
|
|
if DB == nil {
|
|
return nil, 0, nil
|
|
}
|
|
q := DB.Model(&AgentAuditEvent{})
|
|
if f.UserID != "" {
|
|
q = q.Where("user_id = ?", f.UserID)
|
|
}
|
|
if f.BindingScope != "" {
|
|
q = q.Where("binding_scope = ?", f.BindingScope)
|
|
}
|
|
if f.DeploymentID != "" {
|
|
q = q.Where("deployment_id = ?", f.DeploymentID)
|
|
}
|
|
if f.EventLike != "" {
|
|
q = q.Where("event LIKE ?", f.EventLike+"%")
|
|
}
|
|
if f.FromMs > 0 {
|
|
q = q.Where("occurred_at >= ?", f.FromMs)
|
|
}
|
|
if f.ToMs > 0 {
|
|
q = q.Where("occurred_at < ?", f.ToMs)
|
|
}
|
|
var total int64
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
limit := f.Limit
|
|
if limit <= 0 || limit > 1000 {
|
|
limit = 200
|
|
}
|
|
var items []AgentAuditEvent
|
|
err := q.Order("occurred_at desc, id desc").Limit(limit).Offset(f.Offset).Find(&items).Error
|
|
return items, total, err
|
|
}
|
|
|
|
// ListAgentAuditEventsByDeployment is the hot path for the deployment
|
|
// detail drawer — returns all events for one deployment in chronological
|
|
// order so the timeline reads top-to-bottom.
|
|
func ListAgentAuditEventsByDeployment(deploymentID string) ([]AgentAuditEvent, error) {
|
|
if DB == nil || deploymentID == "" {
|
|
return nil, nil
|
|
}
|
|
var items []AgentAuditEvent
|
|
err := DB.Where("deployment_id = ?", deploymentID).
|
|
Order("occurred_at asc, id asc").
|
|
Find(&items).Error
|
|
return items, err
|
|
}
|