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

605 lines
23 KiB
Go

package server
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"chat-agent-system/internal/agent"
"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/intent"
"chat-agent-system/internal/notification"
"chat-agent-system/internal/observability"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/taskrun"
"chat-agent-system/internal/workflowdef"
"chat-agent-system/internal/workspace"
)
// Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务
type Server struct {
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
httpServer *http.Server
}
// NewServer 构造函数
func NewServer(cfg *config.Config, svc *bizdev.Service) *Server {
if cfg == nil {
cfg, _ = config.Load("")
}
if cfg != nil {
observability.InitLogger(cfg.Env)
}
if svc == nil {
var err error
svc, err = bizdev.NewService(cfg)
if err != nil {
panic("server: bizdev service init failed: " + err.Error())
}
}
rec := intent.NewRecognizer()
dialogueBackend := agent.NewBackend(cfg.AgentBackend, cfg)
coordEngine := coordinator.NewCoordinatorEngineWithDialogue(svc, dialogueBackend, cfg.DataDir)
pool := svc.Pool()
stores, storeErr := auth.NewStores(context.Background(), pool, cfg)
if storeErr != nil {
panic("server: auth store init failed: " + storeErr.Error())
}
authSvc := auth.NewService(stores.Auth, stores.Sessions, cfg)
pwSvc := svc.ProjectWorkflow()
if pwSvc == nil {
pwSvc = projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
}
authzSvc := authz.NewService(pool)
wfDefSvc := workflowdef.NewService(pool, svc.CAS())
instStore := projectinstance.NewStore(pool)
var instSvc *projectinstance.Service
if wfDefSvc.Available() {
docChecker := projectinstance.NewDocGovChecker(svc.DocGov())
instSvc = projectinstance.NewService(instStore, wfDefSvc.Store(), pwSvc.Store(), docChecker)
if pool != nil {
instSvc.SetActiveMemberLookup(projectinstance.NewPGMemberLookup(pool))
}
}
taskMgr := taskrun.NewManager(pwSvc, svc.ContextManager())
if pool != nil {
taskMgr.SetStore(taskrun.NewPGStore(pool))
}
s := &Server{
cfg: cfg,
svc: svc,
pwSvc: pwSvc,
rec: rec,
engine: coordEngine,
authSvc: authSvc,
sessions: make(map[string]*chat.Session),
taskRuns: taskMgr,
authorizer: authz.NewAuthorizer(pool),
authzSvc: authzSvc,
wfDefSvc: wfDefSvc,
instSvc: instSvc,
notifySvc: notification.NewService(pool),
resolver: workspace.NewResolver(cfg.DataDir, pool),
}
if instSvc != nil && instSvc.Available() {
svc.SetStageAccessGate(newBizdevStageGate(instSvc, pwSvc))
coordEngine.SetInstanceService(instSvc)
}
s.registerWorkflowApprovalHook()
s.startNotificationReminderLoop()
s.initTaskRunExecutors()
if svc != nil && svc.DocRevision() != nil && s.notifySvc != nil {
rev := svc.DocRevision()
rev.OnOutbox = func(ctx context.Context, eventType string, payload map[string]any) error {
if !s.notifySvc.Available() {
return nil
}
docID, _ := payload["doc_id"].(string)
projectID, _ := payload["project_id"].(int64)
risk, _ := payload["risk"].(string)
var recipient int64
if s.svc.DocGov() != nil {
if doc, err := s.svc.DocGov().GetDocument(ctx, docID); err == nil && doc != nil && doc.AuthorID > 0 {
recipient = doc.AuthorID
projectID = doc.ProjectID
}
}
if recipient == 0 {
return nil
}
p := notification.DocumentEventPayload{
ProjectID: projectID, DocID: docID, RiskLevel: risk,
}
switch eventType {
case notification.EventDocAgentCandidateReady:
if v, ok := payload["candidate_id"].(int64); ok {
p.CandidateID = v
}
_, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocAgentCandidateReady,
"智能体变更待确认", "文档有高风险智能体候选变更,请确认后生效。",
fmt.Sprintf("doc_candidate:%s:%d:%d", docID, p.CandidateID, recipient), recipient, p)
return err
case notification.EventDocAgentVersionCommitted:
if v, ok := payload["version_no"].(int); ok {
p.VersionNo = v
}
_, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocAgentVersionCommitted,
"智能体版本已生效", "新的智能体文档版本已提交。",
fmt.Sprintf("doc_agent_version:%s:%d:%d", docID, p.VersionNo, recipient), recipient, p)
return err
case notification.EventDocDownstreamImpact:
impacted, _ := payload["impacted_doc_id"].(string)
p.ImpactedDocID = impacted
_, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocDownstreamImpact,
"下游文档受影响", "上游文档变更,请复核关联文档。",
fmt.Sprintf("doc_impact:%s:%s:%d", docID, impacted, recipient), recipient, p)
return err
case notification.EventDocChangeReportDegraded:
if v, ok := payload["report_id"].(int64); ok {
p.ReportID = v
}
_, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocChangeReportDegraded,
"变更报告未完整评估", "变更报告降级,受控文档暂不可送审。",
fmt.Sprintf("doc_report_degraded:%s:%d:%d", docID, p.ReportID, recipient), recipient, p)
return err
default:
return nil
}
}
}
return s
}
// 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)
// 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 {
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.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.svc != nil && s.svc.ContextManager() != nil {
if r, err := s.svc.ContextManager().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 != "" {
userCtx.BlackboardFacts["target_market"] = targetMarket
}
if deviceType != "" {
userCtx.BlackboardFacts["device_type"] = deviceType
}
}
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
if s.svc != nil {
sess.CtxMgr = s.svc.ContextManager()
sess.Actions = &chat.ActionServices{
DocGov: s.svc.DocGov(),
Bizdev: s.svc,
TaskRuns: s.taskRuns,
}
}
// 若从持久层恢复了会话历史,回填消息列表、事实黑板与近期对话 (G3)
if restored != nil {
if restored.UserID > 0 {
sess.UserID = restored.UserID
}
if restored.ProjectID > 0 {
sess.ProjectID = restored.ProjectID
}
for k, v := range restored.Facts {
if _, exists := userCtx.BlackboardFacts[k]; !exists {
userCtx.BlackboardFacts[k] = v
}
}
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 != "" {
userCtx.PersonalizedGuidance = guidance
}
}
}
sess.LastAccessedAt = time.Now()
s.sessions[sessionID] = sess
return sess
}
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)
})
}