Files
Agent-System/internal/server/server.go
T
weichen 2e56ba9719 feat(docgov): complete review lifecycle gaps for lock/notify/comment/reopen
Defer locked-period supplements to a pending ledger, route duty-holder
notifications, persist stage-scoped comments, and prefer Approver for reopen.
2026-09-25 20:08:56 +00:00

735 lines
29 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/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/handoff"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/maintenance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/taskrun"
"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
sessions map[string]*chat.Session
sessionsMu sync.RWMutex
supplementImpact SupplementImpactService
// intentPlansOverride 仅测试注入用(SetIntentPlansForTest);为空回落 rt.IntentPlans。
intentPlansOverride intentflow.Service
// testOverrides 仅供测试注入替换件;生产路径恒为 nil。
testOverrides *serverTestOverrides
// 后台循环停止控制(backgroundLoop,修 Shutdown 与 loop 的 DATA RACE)。
wsGCLoop backgroundLoop
reminderLoop backgroundLoop
vaultLoop backgroundLoop
chatTurnSweepLoop backgroundLoop
chatLog chatflow.ChatLog
assembler chatflow.ContextAssembler
listener net.Listener
lastMaintToken string
wsReg *workspace.FileRegistry
httpServer *http.Server
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
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,
sessions: make(map[string]*chat.Session),
wsReg: workspaceRegistryFor(rt.Cfg),
// 登录/注册限流: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
}
// newMinimalRuntime 构造只带测试所需字段的 app.Runtime(task_finalize 系列专用)。
func newMinimalRuntime(cfg *config.Config, svc *bizdev.Service, pwSvc *projectworkflow.Service, ctxMgr *ctxmgr.ContextManager) *app.Runtime {
return &app.Runtime{
Cfg: cfg,
Biz: svc,
PW: pwSvc,
Ctx: ctxMgr,
TaskRuns: taskrun.NewManager(pwSvc, ctxMgr),
}
}
// 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.intentPlansOverride = 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("POST /api/v1/docgov/comments/{id}/resolve", s.handleDocGovResolveComment)
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/maintenance/site-status", s.handleMaintenanceSiteStatus)
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 {
s.wsGCLoop.stop()
s.reminderLoop.stop()
if s.maintenance() != nil {
s.maintenance().Stop()
}
s.vaultLoop.stop()
s.chatTurnSweepLoop.stop()
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()),
Workflow: adaptChatWorkflow(s.pwSvc()),
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)
})
}