From 2a6dd442bb0c069bf5eb56f9d2c17a2eb2edc9a3 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 13:44:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E6=96=B0=E5=A2=9E=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E5=88=97=E8=A1=A8=E3=80=81=E5=A4=9A=E8=BD=AE=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E6=B6=88=E6=81=AF=E6=9F=A5=E8=AF=A2=E4=B8=8E=E6=A0=87?= =?UTF-8?q?=E6=9D=86=E6=A1=88=E4=BE=8B=E8=AF=AD=E4=B9=89=E6=A3=80=E7=B4=A2?= =?UTF-8?q?=20RESTful=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- cmd/server/main.go | 3 + internal/server/chat_history_api_test.go | 117 +++++++++++++++++++++++ internal/server/handlers.go | 116 ++++++++++++++++++++++ internal/server/server.go | 7 +- 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 internal/server/chat_history_api_test.go diff --git a/cmd/server/main.go b/cmd/server/main.go index e0cbd898..fe09d5a9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -57,6 +57,9 @@ func main() { fmt.Println(" • [GET] /api/v1/context/user - 获取单用户私有上下文与习惯偏好画像") fmt.Println(" • [PUT] /api/v1/context/user/preference - 整理或更新用户个性化产出风格") fmt.Println(" • [GET] /api/v1/context/project - 获取项目多方协同黑板事实与里程碑决策流") + fmt.Println(" • [GET] /api/v1/chat/sessions - 分页查询用户/项目历史会话列表") + fmt.Println(" • [GET] /api/v1/chat/messages - 分页拉取指定会话多轮历史消息记录") + fmt.Println(" • [GET] /api/v1/context/cases/recommend - 往期受控标杆案例与历史报价语义自推荐") fmt.Println(" • [GET] /api/v1/docgov/document - 查询受控文档治理状态看板") fmt.Println(" • [POST] /api/v1/docgov/claim - 查阅并认领受控文档 (正式稿方可认领)") fmt.Println(" • [POST] /api/v1/docgov/proposal/decide - 编制负责人拍板同意/拒绝变更提案") diff --git a/internal/server/chat_history_api_test.go b/internal/server/chat_history_api_test.go new file mode 100644 index 00000000..44c4493c --- /dev/null +++ b/internal/server/chat_history_api_test.go @@ -0,0 +1,117 @@ +package server_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "chat-agent-system/internal/bizdev" + "chat-agent-system/internal/config" + "chat-agent-system/internal/ctxmgr" + "chat-agent-system/internal/docgov" + "chat-agent-system/internal/server" +) + +func TestServer_ChatHistoryAndCaseRecommendationAPIs(t *testing.T) { + cfg := &config.Config{ + HTTPHost: "127.0.0.1", + HTTPPort: 8888, + DataDir: t.TempDir(), + } + svc := bizdev.NewService(cfg) + srv := server.NewServer(cfg, svc) + handler := srv.Routes() + + ctx := context.Background() + sessID := "sess_api_history_001" + userID := int64(108) + + // 1. 模拟写入对话消息 + _ = svc.ContextManager().RecordUserMessage(ctx, sessID, userID, "user", "我们准备立项一款便携无线掌上超声,预算大约150万", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil) + _ = svc.ContextManager().RecordUserMessage(ctx, sessID, 0, "assistant", "已为您建立超声设备立项基准,预估研发周期6个月", "ADVANCE_STAGE", "EXPECTATION_HIT", nil) + + // 2. 测试 GET /api/v1/chat/sessions + reqSessions := httptest.NewRequest("GET", "/api/v1/chat/sessions?user_id=108", nil) + rrSessions := httptest.NewRecorder() + handler.ServeHTTP(rrSessions, reqSessions) + + if rrSessions.Code != http.StatusOK { + t.Fatalf("GET /api/v1/chat/sessions expected 200, got %d: %s", rrSessions.Code, rrSessions.Body.String()) + } + + var sessionList []ctxmgr.ChatSessionSummary + if err := json.Unmarshal(rrSessions.Body.Bytes(), &sessionList); err != nil { + t.Fatalf("unmarshal sessions failed: %v", err) + } + if len(sessionList) != 1 || sessionList[0].SessionID != sessID { + t.Fatalf("unexpected sessions result: %+v", sessionList) + } + t.Logf("✓ 成功查询到历史会话列表: 包含会话=%s, 消息数=%d", sessionList[0].SessionID, sessionList[0].MessageCount) + + // 3. 测试 GET /api/v1/chat/messages + reqMsgs := httptest.NewRequest("GET", "/api/v1/chat/messages?session_id="+sessID, nil) + rrMsgs := httptest.NewRecorder() + handler.ServeHTTP(rrMsgs, reqMsgs) + + if rrMsgs.Code != http.StatusOK { + t.Fatalf("GET /api/v1/chat/messages expected 200, got %d: %s", rrMsgs.Code, rrMsgs.Body.String()) + } + + var msgList []ctxmgr.ChatMessageRecord + if err := json.Unmarshal(rrMsgs.Body.Bytes(), &msgList); err != nil { + t.Fatalf("unmarshal messages failed: %v", err) + } + if len(msgList) != 2 { + t.Fatalf("expected 2 messages, got %d", len(msgList)) + } + t.Logf("✓ 成功分页获取会话消息历史: 轮次数=%d, 首条内容=%s", len(msgList), msgList[0].Content) + + // 4. 预置一个受控文档终态并索引为标杆案例 + now := time.Now() + doc := &docgov.DocumentRecord{ + DocID: "doc_benchmark_api_01", + ProjectID: 501, + TaskKey: "feasibility_reg", + Title: "无线掌上超声探头可行性评估报告", + Stage: "V0.5_feasibility", + CurrentState: docgov.StateApproved, + IsLocked: true, + ApprovedAt: &now, + AuthorName: "王总工", + } + docBody := `# 无线掌上超声探头可行性评估报告 + +## 概述与器械分类 +主要用于临床床旁急诊腹部与浅表超声扫查,属于二类或三类医疗器械。 + +## 关键性能与型检预估 +符合 GB 9706.1-2020 和 YY 0644 标准要求,探头无线射频满足 SRRC 与无线电合规要求。` + + _, err := svc.ContextManager().IndexApprovedDocument(ctx, doc, docBody) + if err != nil { + t.Fatalf("IndexApprovedDocument failed: %v", err) + } + + // 5. 测试 GET /api/v1/context/cases/recommend + reqCase := httptest.NewRequest("GET", "/api/v1/context/cases/recommend?query=无线超声探头型检与注册要求&limit=3", nil) + rrCase := httptest.NewRecorder() + handler.ServeHTTP(rrCase, reqCase) + + if rrCase.Code != http.StatusOK { + t.Fatalf("GET /api/v1/context/cases/recommend expected 200, got %d: %s", rrCase.Code, rrCase.Body.String()) + } + + var cases []ctxmgr.CaseRecommendationItem + if err := json.Unmarshal(rrCase.Body.Bytes(), &cases); err != nil { + t.Fatalf("unmarshal cases failed: %v", err) + } + if len(cases) == 0 { + t.Fatalf("expected recommended cases, got 0") + } + + t.Logf("✓ 成功通过 API 检索到标杆案例推荐: 案例=%s, 匹配章节=%s, 得分=%.2f, 提要=%s", + cases[0].ArtifactTitle, cases[0].SectionTitle, cases[0].SimilarityScore, cases[0].SummarySnippet) +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index d6925893..0f361f5a 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -591,3 +591,119 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques writeJSONResponse(w, http.StatusOK, logs) } + +// ----------------------------------------------------------------------------- +// 历史多轮会话管理与标杆案例语义检索 RESTful 控制器 +// ----------------------------------------------------------------------------- + +// handleListChatSessions 分页查询用户/项目关联的历史会话列表 +func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) { + if s.svc == nil || s.svc.ContextManager() == nil { + http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) + return + } + + userIDStr := r.URL.Query().Get("user_id") + userID, _ := strconv.ParseInt(userIDStr, 10, 64) + projectIDStr := r.URL.Query().Get("project_id") + projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) + + limitStr := r.URL.Query().Get("limit") + limit, _ := strconv.Atoi(limitStr) + if limit <= 0 { + limit = 20 + } + offsetStr := r.URL.Query().Get("offset") + offset, _ := strconv.Atoi(offsetStr) + if offset < 0 { + offset = 0 + } + + sessions, err := s.svc.ContextManager().ListChatSessions(r.Context(), ctxmgr.SessionFilter{ + UserID: userID, + ProjectID: projectID, + Limit: limit, + Offset: offset, + }) + if err != nil { + http.Error(w, "list chat sessions failed: "+err.Error(), http.StatusInternalServerError) + return + } + + writeJSONResponse(w, http.StatusOK, sessions) +} + +// handleGetChatMessages 分页获取指定会话的历史多轮消息记录 +func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) { + if s.svc == nil || s.svc.ContextManager() == nil { + http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) + return + } + + sessionID := r.URL.Query().Get("session_id") + if sessionID == "" { + http.Error(w, "missing session_id query param", http.StatusBadRequest) + return + } + + limitStr := r.URL.Query().Get("limit") + limit, _ := strconv.Atoi(limitStr) + if limit <= 0 { + limit = 50 + } + offsetStr := r.URL.Query().Get("offset") + offset, _ := strconv.Atoi(offsetStr) + if offset < 0 { + offset = 0 + } + + msgs, err := s.svc.ContextManager().GetChatHistory(r.Context(), sessionID, limit, offset) + if err != nil { + http.Error(w, "get chat messages failed: "+err.Error(), http.StatusInternalServerError) + return + } + + writeJSONResponse(w, http.StatusOK, msgs) +} + +// handleRecommendCases 基于语义向量检索往期相似标杆案例 +func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) { + if s.svc == nil || s.svc.ContextManager() == nil { + http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) + return + } + + query := r.URL.Query().Get("query") + if query == "" { + http.Error(w, "missing query param", http.StatusBadRequest) + return + } + + taskKey := r.URL.Query().Get("task_key") + excludeDoc := r.URL.Query().Get("exclude_doc") + limitStr := r.URL.Query().Get("limit") + limit, _ := strconv.Atoi(limitStr) + if limit <= 0 { + limit = 3 + } + + minScoreStr := r.URL.Query().Get("min_score") + minScore, _ := strconv.ParseFloat(minScoreStr, 64) + if minScore <= 0 { + minScore = 0.50 + } + + cases, err := s.svc.ContextManager().SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{ + Query: query, + TaskKey: taskKey, + ExcludeDoc: excludeDoc, + Limit: limit, + MinScore: minScore, + }) + if err != nil { + http.Error(w, "search similar cases failed: "+err.Error(), http.StatusInternalServerError) + return + } + + writeJSONResponse(w, http.StatusOK, cases) +} diff --git a/internal/server/server.go b/internal/server/server.go index 1a4245f0..7297aa9e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -75,7 +75,12 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("PUT /api/v1/context/user/preference", s.handleUpdateUserPreference) mux.HandleFunc("GET /api/v1/context/project", s.handleGetProjectContext) - // 7. 受控文档编制、审核、批准生命周期与认领治理 + // 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)