Files
Agent-System/internal/server/server.go
T

787 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"chat-agent-system/internal/app"
"chat-agent-system/internal/app/chatflow"
"chat-agent-system/internal/app/intentflow"
"chat-agent-system/internal/app/stageflow"
"chat-agent-system/internal/artifactvault"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/chat"
"chat-agent-system/internal/config"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/freedoc"
"chat-agent-system/internal/handoff"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/maintenance"
"chat-agent-system/internal/notification"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/sysadmin"
"chat-agent-system/internal/taskrun"
"chat-agent-system/internal/workflowdef"
"chat-agent-system/internal/workspace"
)
// Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务。
// Pool 与领域服务装配归属 app.Runtime;本结构仅保留 HTTP 适配所需句柄。
// 残留具体领域字段见 short-term 任务记录(增量收口,本批先去掉 *pgxpool.Pool)。
type Server struct {
rt *app.Runtime
cfg *config.Config
svc *bizdev.Service
pwSvc *projectworkflow.Service
rec intent.Recognizer
engine *coordinator.CoordinatorEngine
authSvc *auth.Service
sessions map[string]*chat.Session
sessionsMu sync.RWMutex
taskRuns *taskrun.Manager
authorizer *authz.Authorizer
authzSvc *authz.Service
wfDefSvc *workflowdef.Service
instSvc *projectinstance.Service
notifySvc *notification.Service
supplementImpact SupplementImpactService
resolver *workspace.Resolver
wsCap *workspace.Capability
cmds *taskrun.Commands
chatLog chatflow.ChatLog
assembler chatflow.ContextAssembler
stageflow *stageflow.UseCase
gov *docgov.GovernanceEngine
rev *docrevision.Service
ctxMgr *ctxmgr.ContextManager
cas *storage.CAS
freeDoc *freedoc.Service
kb *kb.Client
sysadmin *sysadmin.Service
maintenance *maintenance.Service
listener net.Listener
lastMaintToken string
wsReg *workspace.FileRegistry
wsGCStop chan struct{}
chatTurnSweepStop chan struct{}
httpServer *http.Server
intentPlans intentflow.Service
planAccessFn func(ctx context.Context, projectID, userID int64) error
planSessionOwnerFn func(ctx context.Context, sessionID string) (int64, error)
// FAV stubs (U7 incremental): nil / off unless FAV_PUBLISHER_ENABLED.
vaultReg artifactvault.Registry
vaultStore *artifactvault.FilesystemStore
vaultPub *artifactvault.Publisher
vaultIngest *artifactvault.Ingest
vaultPublisherStarted int32
vaultStop chan struct{}
reminderStop chan struct{}
loginLimiter *loginRateLimiter
healthMu sync.Mutex
healthCache healthSnapshot
eventsAlive func() bool
}
// SetEventsAlive wires an optional in-process events dispatcher liveness probe for /ready.
func (s *Server) SetEventsAlive(fn func() bool) {
if s == nil {
return
}
s.eventsAlive = fn
}
// NewFromRuntime attaches the HTTP adapter to an already-wired composition root.
func NewFromRuntime(rt *app.Runtime) *Server {
if rt == nil {
panic("server: nil runtime")
}
s := &Server{
rt: rt,
cfg: rt.Cfg,
svc: rt.Biz,
pwSvc: rt.PW,
rec: rt.Rec,
engine: rt.Engine,
authSvc: rt.Auth,
sessions: make(map[string]*chat.Session),
taskRuns: rt.TaskRuns,
authorizer: rt.Authorizer,
authzSvc: rt.Authz,
wfDefSvc: rt.WFDef,
instSvc: rt.Inst,
notifySvc: rt.Notify,
supplementImpact: newSupplementImpactAdapter(rt.SupplementImpact),
resolver: rt.Resolver,
wsCap: rt.WSCap,
cmds: rt.Commands,
stageflow: rt.Stageflow,
gov: rt.Gov,
rev: rt.Rev,
ctxMgr: rt.Ctx,
cas: rt.CAS,
freeDoc: rt.FreeDoc,
kb: rt.KB,
sysadmin: rt.SysAdmin,
maintenance: rt.Maintenance,
wsReg: workspaceRegistryFor(rt.Cfg),
intentPlans: rt.IntentPlans,
// 登录/注册限流:10 次 / 5 分钟 / (IP+username) 滑动窗口
loginLimiter: newLoginRateLimiter(10, 5*time.Minute),
}
s.chatLog = chatflow.NewAdapter(s.ctxMgr, s.resolveStageAccess, s.failClosedAuthz())
s.assembler = chatflow.NewAssembler(coordinator.NewContextAssembler(), s.chatLog)
rt.ChatLog = s.chatLog
rt.Assembler = s.assembler
if s.instSvc != nil && s.instSvc.Available() && s.svc != nil {
s.svc.SetStageAccessGate(newBizdevStageGate(s.instSvc, s.pwSvc))
}
s.initTaskRunExecutors()
s.initArtifactVault()
s.wireFinalArtifactHooks()
s.startVaultPublisher()
s.startChatTurnSweeper()
// Durable notify is live via events/docnotify consumer; do not assign OnOutbox here
// (see internal/app.bindDocumentNotify and unhook_test).
return s
}
// NewServer 构造函数(测试兼容:内部走 app.Assemble)
func NewServer(cfg *config.Config, svc *bizdev.Service) *Server {
rt, err := app.Assemble(cfg, svc)
if err != nil {
panic("server: runtime init failed: " + err.Error())
}
return NewFromRuntime(rt)
}
// AuthService 获取认证服务
func (s *Server) AuthService() *auth.Service {
return s.authSvc
}
// BizdevService 获取业务交付服务
func (s *Server) BizdevService() *bizdev.Service {
return s.svc
}
// TaskRuns 获取长任务管理器(测试与运维钩子)。
func (s *Server) TaskRuns() *taskrun.Manager {
return s.taskRuns
}
// SetIntentPlansForTest swaps the intent-plan service (Wave 2 restart/refresh harness only).
func (s *Server) SetIntentPlansForTest(plans intentflow.Service) {
if s == nil {
return
}
s.intentPlans = plans
}
// ProjectWorkflowService 获取项目多工作流控制服务
func (s *Server) ProjectWorkflowService() *projectworkflow.Service {
return s.pwSvc
}
// Routes 构建 HTTP 路由分发器
func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
// 1. 健康检查与系统自检
mux.HandleFunc("GET /health", s.handleHealth)
mux.HandleFunc("GET /api/v1/health", s.handleHealth)
mux.HandleFunc("GET /ready", s.handleReadiness)
mux.HandleFunc("GET /api/v1/ready", s.handleReadiness)
// FAV status stub (publisher/degraded default off; no ingest)
mux.HandleFunc("GET /api/v1/vault/status", s.handleVaultStatus)
// 2. 项目与多工作流控制面
mux.HandleFunc("POST /api/v1/projects", s.handleCreateProject)
mux.HandleFunc("POST /api/v1/projects/creation-preview", s.handleProjectCreationPreview)
mux.HandleFunc("GET /api/v1/project-types/catalog", s.handleProjectTypesCatalog)
mux.HandleFunc("GET /api/v1/compliance-profiles/catalog", s.handleComplianceProfilesCatalog)
mux.HandleFunc("GET /api/v1/projects", s.handleListProjects)
mux.HandleFunc("GET /api/v1/projects/{id}", s.handleGetProjectByID)
mux.HandleFunc("GET /api/v1/projects/{id}/members", s.handleProjectMembers)
mux.HandleFunc("POST /api/v1/projects/{id}/members", s.handleProjectMembers)
mux.HandleFunc("PUT /api/v1/projects/{id}/members", s.handleProjectMembers)
mux.HandleFunc("DELETE /api/v1/projects/{id}/members", s.handleProjectMembers)
mux.HandleFunc("GET /api/v1/projects/{id}/workflows", s.handleListProjectWorkflows)
mux.HandleFunc("PUT /api/v1/projects/{id}/active-workflow", s.handleActivateWorkflow)
mux.HandleFunc("GET /api/v1/projects/{id}/settings", s.handleGetProjectSettings)
mux.HandleFunc("GET /api/v1/projects/{id}/workflow-plan/default", s.handleGetDefaultWorkflowPlan)
mux.HandleFunc("PUT /api/v1/projects/{id}/workflow-plan", s.handleSaveWorkflowPlan)
mux.HandleFunc("PATCH /api/v1/projects/{id}/doc-tasks/{taskKey}/duties", s.handleReassignDocTaskDuties)
mux.HandleFunc("POST /api/v1/projects/{id}/activation-preview", s.handleActivationPreview)
mux.HandleFunc("POST /api/v1/projects/{id}/workflow-instances/activate", s.handleActivateWorkflowInstance)
mux.HandleFunc("GET /api/v1/workflow-instances/{id}", s.handleGetWorkflowInstance)
mux.HandleFunc("GET /api/v1/workflow-instances/{id}/stages/{stage}/readiness", s.handleGetStageReadiness)
mux.HandleFunc("POST /api/v1/workflow-instances/{id}/stages/{stage}/advance", s.handleAdvanceStage)
mux.HandleFunc("GET /api/v1/workflow-instances/{id}/termination-preview", s.handleTerminationPreview)
mux.HandleFunc("POST /api/v1/workflow-instances/{id}/terminate", s.handleTerminateWorkflowInstance)
mux.HandleFunc("GET /api/v1/workflows", s.handleListWorkflowDefinitions)
mux.HandleFunc("GET /api/v1/workflows/catalog", s.handleWorkflowCatalog)
mux.HandleFunc("POST /api/v1/projects/{id}/workflows/{workflow_key}/run", s.handleRunProjectWorkflow)
mux.HandleFunc("GET /api/v1/projects/{id}/workflows/{workflow_key}/runs", s.handleListProjectWorkflowRuns)
mux.HandleFunc("GET /api/v1/projects/{id}/lineage", s.handleGetProjectLineage)
mux.HandleFunc("GET /api/v1/projects/{id}/trace", s.handleProjectTrace)
mux.HandleFunc("POST /api/v1/projects/{id}/trace/index", s.handleProjectTraceIndex)
mux.HandleFunc("POST /api/v1/projects/{id}/trace/analyze", s.handleProjectTraceAnalyze)
mux.HandleFunc("POST /api/v1/projects/{id}/trace/edges/{edgeID}/decision", s.handleProjectTraceDecision)
mux.HandleFunc("POST /api/v1/projects/{id}/trace/reopen", s.handleProjectTraceReopen)
mux.HandleFunc("POST /api/v1/projects/{id}/trace/reopen/{requestID}/approve", s.handleProjectTraceReopenApprove)
mux.HandleFunc("POST /api/v1/projects/{id}/trace/recheck/{markID}/clear", s.handleProjectTraceRecheckClear)
mux.HandleFunc("GET /api/v1/projects/{id}/documents", s.handleListProjectWorkflowDocuments)
mux.HandleFunc("GET /api/v1/projects/{id}/notifications", s.handleListProjectNotifications)
mux.HandleFunc("GET /api/v1/notifications", s.handleListUserNotifications)
mux.HandleFunc("GET /api/v1/supplement-impacts/{batch_id}", s.handleGetSupplementImpact)
mux.HandleFunc("POST /api/v1/supplement-impacts/{batch_id}/decision", s.handleDecideSupplementImpact)
// 3. 对话协同服务 (标准 JSON 阻塞)
mux.HandleFunc("POST /api/v1/chat", s.handleChat)
// 4. 对话协同服务 (核心 SSE 流式长连接)
mux.HandleFunc("POST /api/v1/chat/stream", s.handleChatStream)
mux.HandleFunc("GET /api/v1/chat/stream", s.handleChatStream)
mux.HandleFunc("GET /api/v1/chat/turns/{client_turn_id}", s.handleGetChatTurn)
mux.HandleFunc("GET "+intentflow.PathActivePlan, s.handleGetActiveIntentPlan)
mux.HandleFunc("GET "+intentflow.PathPlanByID, s.handleGetIntentPlan)
mux.HandleFunc("POST "+intentflow.PathPlanDecisions, s.handleIntentPlanDecision)
// 5. 独立任务 / 阶段执行
mux.HandleFunc("POST /api/v1/tasks/run", s.handleRunTask)
mux.HandleFunc("POST /api/v1/tasks/run/stream", s.handleRunTaskStream)
mux.HandleFunc("GET /api/v1/tasks/run/stream", s.handleRunTaskStream)
mux.HandleFunc("POST /api/v1/tasks/runs", s.handleCreateTaskRun)
mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}", s.handleGetTaskRun)
mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}/events", s.handleTaskRunEvents)
mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/cancel", s.handleCancelTaskRun)
mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/resume", s.handleResumeTaskRun)
mux.HandleFunc("POST /api/v1/admin/"+workspace.WorkspacesDirName+"/reconcile", s.handleReconcileWorkspace)
mux.HandleFunc("GET /api/v1/admin/"+workspace.WorkspacesDirName, s.handleAdminWorkspaces)
mux.HandleFunc("POST /api/v1/admin/"+workspace.WorkspacesDirName+"/{id}/cleanup", s.handleAdminWorkspaceCleanup)
mux.HandleFunc("POST /api/v1/projects/{id}/assets", s.handleUploadProjectAsset)
mux.HandleFunc("GET /api/v1/projects/{id}/assets", s.handleListProjectAssets)
mux.HandleFunc("GET /api/v1/projects/{id}/assets/{asset_id}/raw", s.handleGetProjectAssetRaw)
mux.HandleFunc("POST /api/v1/projects/{id}/assets/{asset_id}/reparse", s.handleReparseProjectAsset)
// 6. 多级上下文与偏好记忆管理
mux.HandleFunc("GET /api/v1/context/user", s.handleGetUserContext)
mux.HandleFunc("PUT /api/v1/context/user/preference", s.handleUpdateUserPreference)
mux.HandleFunc("GET /api/v1/context/project", s.handleGetProjectContext)
// 7. 历史多轮会话管理与标杆案例语义检索
mux.HandleFunc("GET /api/v1/chat/sessions", s.handleListChatSessions)
mux.HandleFunc("POST /api/v1/chat/sessions", s.handleUpsertChatSession)
mux.HandleFunc("GET /api/v1/chat/messages", s.handleGetChatMessages)
mux.HandleFunc("GET /api/v1/chat/messages/window", s.handleGetChatWindow)
mux.HandleFunc("GET /api/v1/chat/messages/day", s.handleGetChatDayMessages)
mux.HandleFunc("GET /api/v1/chat/dates", s.handleListChatDates)
mux.HandleFunc("GET /api/v1/chat/categories", s.handleListChatCategories)
mux.HandleFunc("GET /api/v1/chat/search", s.handleChatSearch)
mux.HandleFunc("GET /api/v1/chat/digests", s.handleListDailyDigests)
mux.HandleFunc("POST /api/v1/chat/digests/generate", s.handleGenerateDailyDigest)
mux.HandleFunc("POST /api/v1/chat/messages/import", s.handleImportChatMessages)
// P3: 未读提醒 & 聊天导出
mux.HandleFunc("GET /api/v1/chat/unread", s.handleChatUnread)
mux.HandleFunc("POST /api/v1/chat/read", s.handleChatRead)
mux.HandleFunc("GET /api/v1/chat/export/messages", s.handleChatExportMessages)
mux.HandleFunc("POST /api/v1/chat/recategorize", s.handleChatRecategorize)
mux.HandleFunc("GET /api/v1/context/cases/recommend", s.handleRecommendCases)
// 7.1 单次助手回复导出与快照
mux.HandleFunc("GET /api/v1/chat/messages/{message_id}/export-capability", s.handleGetExportCapability)
mux.HandleFunc("GET /api/v1/chat/messages/{message_id}/export-source", s.handleGetExportSource)
mux.HandleFunc("POST /api/v1/chat/messages/{message_id}/pin-export", s.handlePinExport)
// 7.2 自由私密文档与项目共享参考
mux.HandleFunc("POST /api/v1/freeform-documents", s.handleCreateFreeformDoc)
mux.HandleFunc("POST /api/v1/freeform-documents/from-message", s.handleCreateFreeformDocFromMessage)
mux.HandleFunc("GET /api/v1/freeform-documents", s.handleListFreeformDocs)
mux.HandleFunc("GET /api/v1/freeform-documents/{doc_id}", s.handleGetFreeformDoc)
mux.HandleFunc("PUT /api/v1/freeform-documents/{doc_id}", s.handleUpdateFreeformDoc)
mux.HandleFunc("POST /api/v1/freeform-documents/{doc_id}/share", s.handleShareFreeformDoc)
mux.HandleFunc("POST /api/v1/freeform-documents/{doc_id}/unshare", s.handleUnshareFreeformDoc)
mux.HandleFunc("DELETE /api/v1/freeform-documents/{doc_id}", s.handleDeleteFreeformDoc)
// 8. 受控文档编制、审核、批准生命周期与认领治理
mux.HandleFunc("GET /api/v1/docgov/document", s.handleDocGovGetDocument)
mux.HandleFunc("GET /api/v1/docgov/documents/{doc_id}/content", s.handleDocGovGetDocumentContent)
mux.HandleFunc("POST /api/v1/docgov/claim", s.handleDocGovClaim)
mux.HandleFunc("POST /api/v1/docgov/submit-review", s.handleDocGovSubmitReview)
mux.HandleFunc("POST /api/v1/docgov/review-decision", s.handleDocGovReviewDecision)
mux.HandleFunc("POST /api/v1/docgov/approver-decision", s.handleDocGovApproverDecision)
mux.HandleFunc("POST /api/v1/docgov/proposal", s.handleDocGovProposeModification)
mux.HandleFunc("POST /api/v1/docgov/proposal/decide", s.handleDocGovDecideProposal)
mux.HandleFunc("GET /api/v1/docgov/comments", s.handleDocGovListComments)
mux.HandleFunc("POST /api/v1/docgov/comments", s.handleDocGovAddComment)
mux.HandleFunc("GET /api/v1/docgov/audit-logs", s.handleDocGovListAuditLogs)
mux.HandleFunc("POST /api/v1/docgov/content", s.handleDocGovSaveContent)
mux.HandleFunc("POST /api/v1/docgov/lease/acquire", s.handleDocGovAcquireLease)
mux.HandleFunc("POST /api/v1/docgov/lease/heartbeat", s.handleDocGovHeartbeatLease)
mux.HandleFunc("POST /api/v1/docgov/lease/release", s.handleDocGovReleaseLease)
// 8.1 文档版本 / 修订 ledger
s.registerDocumentRevisionRoutes(mux)
// 9. 用户认证、安全改密与超级管理员后台接口
mux.HandleFunc("POST /api/v1/auth/login", s.handleAuthLogin)
mux.HandleFunc("POST /api/v1/auth/register", s.handleAuthRegister)
mux.HandleFunc("GET /api/v1/auth/public-config", s.handleAuthPublicConfig)
mux.HandleFunc("POST /api/v1/auth/logout", s.handleAuthLogout)
mux.HandleFunc("POST /api/v1/auth/change-password", s.handleAuthChangePassword)
mux.HandleFunc("GET /api/v1/auth/me", s.handleAuthMe)
mux.HandleFunc("GET /api/v1/me/profile", s.handleMeProfile)
mux.HandleFunc("PATCH /api/v1/me/profile", s.handleMeProfile)
mux.HandleFunc("GET /api/v1/me/preferences", s.handleMePreferences)
mux.HandleFunc("PUT /api/v1/me/preferences", s.handleMePreferences)
mux.HandleFunc("GET /api/v1/users/directory", s.handleUserDirectory)
mux.HandleFunc("GET /api/v1/auth/sessions", s.handleAuthSessions)
mux.HandleFunc("GET /api/v1/admin/permissions", s.handleAdminPermissions)
mux.HandleFunc("DELETE /api/v1/auth/sessions/{id}", s.handleAuthSessionByID)
mux.HandleFunc("GET /api/v1/admin/roles", s.handleAdminRoles)
mux.HandleFunc("POST /api/v1/admin/roles", s.handleAdminRoles)
mux.HandleFunc("PATCH /api/v1/admin/roles/{id}", s.handleAdminRoleByID)
mux.HandleFunc("DELETE /api/v1/admin/roles/{id}", s.handleAdminRoleByID)
mux.HandleFunc("GET /api/v1/admin/workflows", s.handleAdminWorkflows)
mux.HandleFunc("POST /api/v1/admin/workflows", s.handleAdminWorkflows)
mux.HandleFunc("GET /api/v1/admin/workflows/{id}", s.handleAdminWorkflowByID)
mux.HandleFunc("PATCH /api/v1/admin/workflows/{id}/versions/{version}", s.handleAdminWorkflowVersion)
mux.HandleFunc("POST /api/v1/admin/workflows/{id}/versions/{version}/validate", s.handleAdminWorkflowValidate)
mux.HandleFunc("POST /api/v1/admin/workflows/{id}/versions/{version}/publish", s.handleAdminWorkflowPublish)
mux.HandleFunc("POST /api/v1/admin/workflows/{id}/versions/{version}/retire", s.handleAdminWorkflowRetire)
mux.HandleFunc("GET /api/v1/admin/templates", s.handleAdminTemplates)
mux.HandleFunc("POST /api/v1/admin/templates", s.handleAdminTemplates)
mux.HandleFunc("GET /api/v1/admin/templates/{key}/versions", s.handleAdminTemplateVersions)
mux.HandleFunc("GET /api/v1/admin/users", s.handleAdminUsers)
mux.HandleFunc("POST /api/v1/admin/users", s.handleAdminUsers)
mux.HandleFunc("PUT /api/v1/admin/users/{id}/role", s.handleAdminUpdateRole)
mux.HandleFunc("PUT /api/v1/admin/users/{id}/status", s.handleAdminUpdateStatus)
mux.HandleFunc("PATCH /api/v1/admin/users/{id}/profile", s.handleAdminUpdateProfile)
mux.HandleFunc("POST /api/v1/admin/users/{id}/reset-password", s.handleAdminResetPassword)
mux.HandleFunc("GET /api/v1/admin/config", s.handleAdminConfig)
mux.HandleFunc("PUT /api/v1/admin/config", s.handleAdminConfig)
// 9.1 系统管理台:细粒度 system:* + 白名单设置 + 策略审计
mux.HandleFunc("GET /api/v1/admin/system/overview", s.handleAdminSystemOverview)
mux.HandleFunc("GET /api/v1/admin/system/kb", s.handleAdminSystemKB)
mux.HandleFunc("GET /api/v1/admin/system/features", s.handleAdminSystemFeatures)
mux.HandleFunc("GET /api/v1/admin/system/settings", s.handleAdminSystemSettings)
mux.HandleFunc("GET /api/v1/admin/system/settings/{key}/audits", s.handleAdminSystemSettingAudits)
mux.HandleFunc("PUT /api/v1/admin/system/settings/{key}", s.handleAdminSystemSettingByKey)
mux.HandleFunc("GET /api/v1/maintenance/status", s.handleMaintenanceStatus)
mux.HandleFunc("GET /api/v1/admin/maintenance", s.handleAdminMaintenanceList)
mux.HandleFunc("POST /api/v1/admin/maintenance", s.handleAdminMaintenanceSchedule)
mux.HandleFunc("POST /api/v1/admin/maintenance/{id}/cancel", s.handleAdminMaintenanceCancel)
// 10. 现代 SPA 前端页面与静态资产托管
mux.HandleFunc("GET /", s.serveSPA)
return s.corsMiddleware(s.accessLogMiddleware(s.bodyLimitMiddleware(s.maintenanceMiddleware(s.authMiddleware(mux)))))
}
// Start 启动 HTTP 守护进程(支持 AGENT_INHERIT_FD 继承 listener)
func (s *Server) Start() error {
s.startWorkspaceGC()
addr := fmt.Sprintf("%s:%d", s.cfg.HTTPHost, s.cfg.HTTPPort)
ln, err := handoff.ListenOrAdopt(addr)
if err != nil {
return err
}
s.listener = ln
_ = handoff.SignalReady()
// 若从维护公告恢复,标记窗口 ended 并清理 token
s.finishMaintenanceResume()
if s.maintenance != nil {
s.maintenance.SetHandoff(func(ctx context.Context, w maintenance.Window) error {
return s.performMaintenanceHandoff(w)
})
s.maintenance.Start(context.Background())
}
s.httpServer = &http.Server{
Addr: addr,
Handler: s.Routes(),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 60 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second,
}
return s.httpServer.Serve(ln)
}
func (s *Server) finishMaintenanceResume() {
if os.Getenv("MAINT_RESUME") != "1" || s.maintenance == nil {
return
}
id, _ := strconv.ParseInt(os.Getenv("MAINT_WINDOW_ID"), 10, 64)
if id > 0 {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _ = s.maintenance.MarkEnded(ctx, id)
}
dataDir := config.DefaultDataDir()
if s.cfg != nil && s.cfg.DataDir != "" {
dataDir = s.cfg.DataDir
}
_ = os.Remove(filepath.Join(dataDir, "maintenance-token"))
_ = os.Unsetenv("MAINT_RESUME")
_ = os.Unsetenv("MAINT_WINDOW_ID")
}
// Shutdown 优雅停机
func (s *Server) Shutdown(ctx context.Context) error {
if s.wsGCStop != nil {
close(s.wsGCStop)
s.wsGCStop = nil
}
if s.reminderStop != nil {
close(s.reminderStop)
s.reminderStop = nil
}
if s.maintenance != nil {
s.maintenance.Stop()
}
if s.vaultStop != nil {
close(s.vaultStop)
s.vaultStop = nil
}
if s.chatTurnSweepStop != nil {
close(s.chatTurnSweepStop)
s.chatTurnSweepStop = nil
}
if s.taskRuns != nil {
_ = s.taskRuns.WaitIdle(ctx)
}
if s.rt != nil {
_ = s.rt.Stop(ctx)
}
if s.httpServer != nil {
return s.httpServer.Shutdown(ctx)
}
return nil
}
// CleanStaleSessions 清理超过空闲 TTL 的内存会话 (释放内存,后续请求可通过 RestoreSessionContext 自动从 DB 恢复)
func (s *Server) CleanStaleSessions(ttl time.Duration) int {
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
return s.cleanStaleSessionsLocked(ttl)
}
func (s *Server) cleanStaleSessionsLocked(ttl time.Duration) int {
if ttl <= 0 {
ttl = 120 * time.Minute
}
now := time.Now()
cleaned := 0
for id, sess := range s.sessions {
if sess == nil || now.Sub(sess.LastAccessed()) > ttl {
delete(s.sessions, id)
cleaned++
}
}
return cleaned
}
const maxLiveSessions = 4096
func (s *Server) enforceSessionCapLocked() {
for len(s.sessions) >= maxLiveSessions {
var oldestID string
var oldest time.Time
found := false
for id, sess := range s.sessions {
if sess == nil {
delete(s.sessions, id)
continue
}
at := sess.LastAccessed()
if !found || at.Before(oldest) {
found = true
oldest = at
oldestID = id
}
}
if !found || oldestID == "" {
return
}
delete(s.sessions, oldestID)
}
}
// getOrCreateSession 获取或动态构建会话。callerUserID>0 时校验持久层归属,不匹配返回 ErrSessionOwnerMismatch。
func (s *Server) getOrCreateSession(
ctx context.Context,
callerUserID int64,
sessionID string,
projectName string,
customerName string,
deviceType string,
intendedUse string,
targetMarket string,
workDir string,
userName string,
userRole string,
) (*chat.Session, error) {
if sessionID == "" {
sessionID = fmt.Sprintf("sess_%d", time.Now().UnixNano())
}
// 归属校验必须先于任何缓存命中/持久层灌入
if callerUserID > 0 && s.ctxMgr != nil {
dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
owner, err := s.ctxMgr.GetSessionOwnerID(dbCtx, sessionID)
cancel()
if err == nil && owner > 0 && owner != callerUserID {
return nil, ctxmgr.ErrSessionOwnerMismatch
}
}
s.sessionsMu.RLock()
existing, exists := s.sessions[sessionID]
s.sessionsMu.RUnlock()
if exists && existing != nil {
if callerUserID > 0 && existing.UserID > 0 && existing.UserID != callerUserID {
return nil, ctxmgr.ErrSessionOwnerMismatch
}
existing.TouchLastAccessed()
return existing, nil
}
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
// 双重检查
if existing, exists := s.sessions[sessionID]; exists && existing != nil {
if callerUserID > 0 && existing.UserID > 0 && existing.UserID != callerUserID {
return nil, ctxmgr.ErrSessionOwnerMismatch
}
existing.TouchLastAccessed()
return existing, nil
}
// 惰性清理,并在仍超过硬上限时按最近访问淘汰,避免随机 session id 撑爆内存。
// 会话 id 仍由客户端提供:历史恢复按该 id 查找,不能改成只接受服务端签发。
if len(s.sessions) > 200 {
s.cleanStaleSessionsLocked(120 * time.Minute)
}
s.enforceSessionCapLocked()
// 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3)
var restored *ctxmgr.RestoredSessionContext
if s.ctxMgr != nil {
dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
r, err := s.ctxMgr.RestoreSessionContext(dbCtx, sessionID)
cancel()
if err == nil && r != nil && len(r.Messages) > 0 {
restored = r
}
}
if userName == "" {
userName = "用户"
}
if userRole == "" {
userRole = "engineer"
}
u := coordinator.UserInfo{
ID: "u_" + sessionID,
Name: userName,
Role: userRole,
ProjectRole: "composer",
}
var userCtx *coordinator.UserProjectContext
if workDir != "" {
assembler := coordinator.NewContextAssembler()
userCtx, _ = assembler.AssembleFromWorkspace(workDir, u)
}
if userCtx == nil {
if projectName == "" {
projectName = "医疗器械研发项目"
}
if customerName == "" {
customerName = "水木东方客户"
}
userCtx = &coordinator.UserProjectContext{
SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()),
AssembledAt: time.Now(),
User: u,
WorkDir: workDir,
Project: coordinator.ProjectInfo{
Name: projectName,
CustomerName: customerName,
DeviceType: deviceType,
IntendedUse: intendedUse,
TargetMarket: targetMarket,
CurrentWorkflow: "intake",
CurrentStage: bizdev.StageV01_Triage,
},
BlackboardFacts: make(map[string]string),
AvailableActions: coordinator.ComputeAvailableActions(bizdev.StageV01_Triage, u.ProjectRole),
}
if targetMarket != "" || deviceType != "" {
fill := map[string]string{}
if targetMarket != "" {
fill["target_market"] = targetMarket
}
if deviceType != "" {
fill["device_type"] = deviceType
}
_ = coordinator.ApplyContextUpdate(userCtx, coordinator.ContextUpdate{
ExpectedVersion: userCtx.Version,
BlackboardFill: fill,
})
}
}
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
sess.CtxMgr = s.ctxMgr
env := "dev"
if s.cfg != nil && s.cfg.Env != "" {
env = s.cfg.Env
}
// Adapters live in internal/app; use server fields so SetIntentPlansForTest stays effective.
sess.ChatLog = app.AdaptChatMemory(s.chatLog)
sess.Actions = &chat.ActionServices{
DocGov: s.gov,
TaskRuns: app.AdaptChatTaskStarter(s.taskRuns),
IntentPlans: app.AdaptChatIntentPlans(s.intentPlans),
Environment: env,
}
if s.vaultIngest != nil {
ingest := s.vaultIngest
sess.OnFinalArtifact = func(ctx context.Context, projectID int64, logical, eventID string, msgID int64, policy string, payload []byte) {
now := time.Now().UTC()
_ = ingest.AssistantReply(ctx, projectID, logical, eventID, msgID, "", policy, &now, payload)
}
}
// 若从持久层恢复了会话历史,回填消息列表、事实黑板与近期对话 (G3)。
// 正文注入走 ChatLog.Restore,避免把未授权消息写进 Prompt 窗口。
if restored != nil {
if restored.UserID > 0 {
sess.UserID = restored.UserID
}
if restored.ProjectID > 0 {
sess.SetProjectID(restored.ProjectID)
}
if len(restored.Facts) > 0 {
_ = coordinator.ApplyContextUpdate(userCtx, coordinator.ContextUpdate{
ExpectedVersion: userCtx.Version,
BlackboardFill: restored.Facts,
})
}
if s.chatLog == nil {
for _, rm := range restored.Messages {
sess.AppendMessage(chat.Message{
ID: fmt.Sprintf("msg_%d", rm.ID),
Role: rm.Role,
Content: rm.Content,
Intent: intent.IntentType(rm.Intent),
Branch: intent.ResolutionBranch(rm.Branch),
Timestamp: rm.CreatedAt,
})
}
sess.SyncRecentDialogue()
}
if sess.UserID > 0 && sess.CtxMgr != nil {
dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
guidance, err := sess.CtxMgr.CompilePromptGuidance(dbCtx, sess.UserID)
cancel()
if err == nil && guidance != "" {
_ = coordinator.ApplyContextUpdate(userCtx, coordinator.ContextUpdate{
ExpectedVersion: userCtx.Version,
SetPersonalizedGuidance: true,
PersonalizedGuidance: guidance,
})
}
}
}
// 新建会话时立即绑定调用者。NewSession 历史占位 UserID=1/ProjectID=1 不能当作已绑定归属。
if restored == nil {
sess.SetProjectID(0)
if callerUserID > 0 {
sess.UserID = callerUserID
}
} else if restored.UserID <= 0 && callerUserID > 0 {
sess.UserID = callerUserID
}
sess.TouchLastAccessed()
s.sessions[sessionID] = sess
return sess, nil
}
func (s *Server) hydrateVisibleHistory(ctx context.Context, sess *chat.Session, viewerUserID int64) {
if s.chatLog == nil || sess == nil || viewerUserID <= 0 {
return
}
if len(sess.SnapshotMessages()) > 0 {
return
}
restored, err := s.chatLog.Restore(ctx, chatflow.Principal{UserID: viewerUserID, Action: chatflow.ActionAssemble}, sess.ID)
if err != nil || restored == nil {
return
}
for _, rm := range restored.Messages {
sess.AppendMessage(chat.Message{
ID: fmt.Sprintf("msg_%d", rm.ID),
Role: rm.Role,
Content: rm.Content,
Intent: intent.IntentType(rm.Intent),
Branch: intent.ResolutionBranch(rm.Branch),
Timestamp: rm.CreatedAt,
})
}
sess.SyncRecentDialogue()
}
func (s *Server) taskUseCase() *stageflow.UseCase {
if s == nil {
return stageflow.New(nil)
}
if s.stageflow != nil {
return s.stageflow
}
return stageflow.New(s.svc)
}
func (s *Server) corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
applySecurityHeaders(w)
s.writeCORS(w, r)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, X-Session-Token, X-Requested-With")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}