Register newTestServer cleanup and Shutdown WaitIdle so race detector no longer hits RemoveAll on a live DataDir. Co-authored-by: Cursor <cursoragent@cursor.com>
618 lines
23 KiB
Go
618 lines
23 KiB
Go
package server
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"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/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/intent"
|
||
"chat-agent-system/internal/kb"
|
||
"chat-agent-system/internal/notification"
|
||
"chat-agent-system/internal/projectinstance"
|
||
"chat-agent-system/internal/projectworkflow"
|
||
"chat-agent-system/internal/storage"
|
||
"chat-agent-system/internal/taskrun"
|
||
"chat-agent-system/internal/workflowdef"
|
||
"chat-agent-system/internal/workspace"
|
||
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
// Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务
|
||
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
|
||
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
|
||
pool *pgxpool.Pool
|
||
freeDoc *freedoc.Service
|
||
kb *kb.Client
|
||
httpServer *http.Server
|
||
intentPlans intentflow.Service
|
||
planAccessFn func(ctx context.Context, projectID, userID int64) error
|
||
planSessionOwnerFn func(ctx context.Context, sessionID string) (int64, error)
|
||
}
|
||
|
||
// 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,
|
||
resolver: rt.Resolver,
|
||
wsCap: rt.WSCap,
|
||
cmds: rt.Commands,
|
||
stageflow: rt.Stageflow,
|
||
gov: rt.Gov,
|
||
rev: rt.Rev,
|
||
ctxMgr: rt.Ctx,
|
||
cas: rt.CAS,
|
||
pool: rt.Pool,
|
||
freeDoc: rt.FreeDoc,
|
||
kb: rt.KB,
|
||
intentPlans: rt.IntentPlans,
|
||
}
|
||
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()
|
||
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
|
||
}
|
||
|
||
// 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)
|
||
|
||
// 2. 项目与多工作流控制面
|
||
mux.HandleFunc("POST /api/v1/projects", s.handleCreateProject)
|
||
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("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}/documents", s.handleListProjectWorkflowDocuments)
|
||
mux.HandleFunc("GET /api/v1/projects/{id}/notifications", s.handleListProjectNotifications)
|
||
|
||
// 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/workspaces/reconcile", s.handleReconcileWorkspace)
|
||
mux.HandleFunc("POST /api/v1/projects/{id}/assets", s.handleUploadProjectAsset)
|
||
mux.HandleFunc("GET /api/v1/projects/{id}/assets", s.handleListProjectAssets)
|
||
|
||
// 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)
|
||
|
||
// 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/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/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("GET /api/v1/admin/config", s.handleAdminConfig)
|
||
mux.HandleFunc("PUT /api/v1/admin/config", s.handleAdminConfig)
|
||
|
||
// 10. 现代 SPA 前端页面与静态资产托管
|
||
mux.HandleFunc("GET /", s.serveSPA)
|
||
|
||
return s.corsMiddleware(s.bodyLimitMiddleware(s.authMiddleware(mux)))
|
||
}
|
||
|
||
// serveSPA 托管 web/dist 静态产物并提供前端路由自动回退
|
||
func (s *Server) serveSPA(w http.ResponseWriter, r *http.Request) {
|
||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
candidates := []string{
|
||
"web/dist",
|
||
"/app/web/dist",
|
||
"../web/dist",
|
||
"../../web/dist",
|
||
filepath.Join(s.cfg.DataDir, "web/dist"),
|
||
filepath.Join(s.cfg.DataDir, "../web/dist"),
|
||
}
|
||
|
||
var distDir string
|
||
for _, dir := range candidates {
|
||
if fi, err := os.Stat(filepath.Join(dir, "index.html")); err == nil && !fi.IsDir() {
|
||
distDir = dir
|
||
break
|
||
}
|
||
}
|
||
|
||
if distDir == "" {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
reqPath := filepath.Clean(r.URL.Path)
|
||
if reqPath == "/" {
|
||
http.ServeFile(w, r, filepath.Join(distDir, "index.html"))
|
||
return
|
||
}
|
||
|
||
targetFile := filepath.Join(distDir, reqPath)
|
||
if fi, err := os.Stat(targetFile); err == nil && !fi.IsDir() {
|
||
http.ServeFile(w, r, targetFile)
|
||
return
|
||
}
|
||
|
||
// SPA Fallback 默认回退 index.html
|
||
http.ServeFile(w, r, filepath.Join(distDir, "index.html"))
|
||
}
|
||
|
||
// Start 启动 HTTP 守护进程
|
||
func (s *Server) Start() error {
|
||
if s.rt != nil {
|
||
_ = s.rt.Start(context.Background())
|
||
}
|
||
addr := fmt.Sprintf("%s:%d", s.cfg.HTTPHost, s.cfg.HTTPPort)
|
||
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.ListenAndServe()
|
||
}
|
||
|
||
// Shutdown 优雅停机
|
||
func (s *Server) Shutdown(ctx context.Context) error {
|
||
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.LastAccessedAt) > ttl {
|
||
delete(s.sessions, id)
|
||
cleaned++
|
||
}
|
||
}
|
||
return cleaned
|
||
}
|
||
|
||
// getOrCreateSession 获取或动态构建会话
|
||
func (s *Server) getOrCreateSession(
|
||
sessionID string,
|
||
projectName string,
|
||
customerName string,
|
||
deviceType string,
|
||
intendedUse string,
|
||
targetMarket string,
|
||
workDir string,
|
||
userName string,
|
||
userRole string,
|
||
) *chat.Session {
|
||
if sessionID == "" {
|
||
sessionID = fmt.Sprintf("sess_%d", time.Now().UnixNano())
|
||
}
|
||
|
||
s.sessionsMu.RLock()
|
||
existing, exists := s.sessions[sessionID]
|
||
s.sessionsMu.RUnlock()
|
||
if exists && existing != nil {
|
||
existing.LastAccessedAt = time.Now()
|
||
return existing
|
||
}
|
||
|
||
s.sessionsMu.Lock()
|
||
defer s.sessionsMu.Unlock()
|
||
// 双重检查
|
||
if existing, exists := s.sessions[sessionID]; exists && existing != nil {
|
||
existing.LastAccessedAt = time.Now()
|
||
return existing
|
||
}
|
||
|
||
// 惰性内存防泄漏保护 (会话数超过 200 时清理空闲超 120 分钟会话)
|
||
if len(s.sessions) > 200 {
|
||
s.cleanStaleSessionsLocked(120 * time.Minute)
|
||
}
|
||
|
||
// 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3)
|
||
var restored *ctxmgr.RestoredSessionContext
|
||
if s.ctxMgr != nil {
|
||
if r, err := s.ctxMgr.RestoreSessionContext(context.Background(), sessionID); 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.ChatLog = s.chatLog
|
||
sess.CtxMgr = s.ctxMgr
|
||
env := "dev"
|
||
if s.cfg != nil && s.cfg.Env != "" {
|
||
env = s.cfg.Env
|
||
}
|
||
sess.Actions = &chat.ActionServices{
|
||
DocGov: s.gov,
|
||
TaskRuns: s.taskRuns,
|
||
IntentPlans: s.intentPlans,
|
||
Environment: env,
|
||
}
|
||
|
||
// 若从持久层恢复了会话历史,回填消息列表、事实黑板与近期对话 (G3)。
|
||
// 正文注入走 ChatLog.Restore,避免把未授权消息写进 Prompt 窗口。
|
||
if restored != nil {
|
||
if restored.UserID > 0 {
|
||
sess.UserID = restored.UserID
|
||
}
|
||
if restored.ProjectID > 0 {
|
||
sess.ProjectID = 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.Messages = append(sess.Messages, 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 {
|
||
if guidance, err := sess.CtxMgr.CompilePromptGuidance(context.Background(), sess.UserID); err == nil && guidance != "" {
|
||
_ = coordinator.ApplyContextUpdate(userCtx, coordinator.ContextUpdate{
|
||
ExpectedVersion: userCtx.Version,
|
||
SetPersonalizedGuidance: true,
|
||
PersonalizedGuidance: guidance,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
sess.LastAccessedAt = time.Now()
|
||
s.sessions[sessionID] = sess
|
||
return sess
|
||
}
|
||
|
||
func (s *Server) hydrateVisibleHistory(ctx context.Context, sess *chat.Session, viewerUserID int64) {
|
||
if s.chatLog == nil || sess == nil || viewerUserID <= 0 {
|
||
return
|
||
}
|
||
if len(sess.Messages) > 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.Messages = append(sess.Messages, 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) {
|
||
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)
|
||
})
|
||
}
|