292 lines
8.4 KiB
Go
292 lines
8.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"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/intent"
|
|
)
|
|
|
|
// Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务
|
|
type Server struct {
|
|
cfg *config.Config
|
|
svc *bizdev.Service
|
|
rec intent.Recognizer
|
|
engine *coordinator.CoordinatorEngine
|
|
authSvc *auth.Service
|
|
sessions map[string]*chat.Session
|
|
sessionsMu sync.RWMutex
|
|
httpServer *http.Server
|
|
}
|
|
|
|
// NewServer 构造函数
|
|
func NewServer(cfg *config.Config, svc *bizdev.Service) *Server {
|
|
if cfg == nil {
|
|
cfg, _ = config.Load("")
|
|
}
|
|
if svc == nil {
|
|
svc = bizdev.NewService(cfg)
|
|
}
|
|
|
|
rec := intent.NewRecognizer()
|
|
coordEngine := coordinator.NewCoordinatorEngine(svc)
|
|
authStore := auth.NewMemoryStore(cfg)
|
|
authSvc := auth.NewService(authStore, cfg)
|
|
|
|
s := &Server{
|
|
cfg: cfg,
|
|
svc: svc,
|
|
rec: rec,
|
|
engine: coordEngine,
|
|
authSvc: authSvc,
|
|
sessions: make(map[string]*chat.Session),
|
|
}
|
|
return s
|
|
}
|
|
|
|
// AuthService 获取认证服务
|
|
func (s *Server) AuthService() *auth.Service {
|
|
return s.authSvc
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 2. 项目状态与会话查询
|
|
mux.HandleFunc("GET /api/v1/projects", s.handleGetProject)
|
|
|
|
// 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)
|
|
|
|
// 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)
|
|
|
|
// 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("GET /api/v1/chat/messages", s.handleGetChatMessages)
|
|
mux.HandleFunc("GET /api/v1/context/cases/recommend", s.handleRecommendCases)
|
|
|
|
// 8. 受控文档编制、审核、批准生命周期与认领治理
|
|
mux.HandleFunc("GET /api/v1/docgov/document", s.handleDocGovGetDocument)
|
|
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)
|
|
|
|
// 9. 用户认证、安全改密与超级管理员后台接口
|
|
mux.HandleFunc("POST /api/v1/auth/login", s.handleAuthLogin)
|
|
mux.HandleFunc("POST /api/v1/auth/register", s.handleAuthRegister)
|
|
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/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(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",
|
|
"../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,
|
|
}
|
|
|
|
return s.httpServer.ListenAndServe()
|
|
}
|
|
|
|
// Shutdown 优雅停机
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
if s.httpServer != nil {
|
|
return s.httpServer.Shutdown(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
return existing
|
|
}
|
|
|
|
s.sessionsMu.Lock()
|
|
defer s.sessionsMu.Unlock()
|
|
// 双重检查
|
|
if existing, exists := s.sessions[sessionID]; exists && existing != nil {
|
|
return existing
|
|
}
|
|
|
|
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()
|
|
}
|
|
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) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|