Files
Agent-System/internal/server/server.go
T
2026-09-16 15:46:45 +00:00

640 lines
24 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/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/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/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)
// FAV stubs (U7 incremental): nil / off unless FAV_PUBLISHER_ENABLED.
vaultReg artifactvault.Registry
vaultStore *artifactvault.FilesystemStore
vaultPub *artifactvault.Publisher
vaultPublisherStarted int32
}
// 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()
s.initArtifactVault()
s.startVaultPublisher()
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("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)
})
}