package server import ( "fmt" "net/http" "strconv" "strings" "chat-agent-system/internal/replyexport" ) // handleGetExportCapability 获取单条消息权威导出能力 // GET /api/v1/chat/messages/{message_id}/export-capability func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } if s.ctxMgr == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized") return } msgID := extractMessageIDFromPath(r, "export-capability") if msgID <= 0 { writeJSONResponse(w, http.StatusBadRequest, map[string]string{ "error_code": "INVALID_MESSAGE_ID", "message": "无效的消息ID", }) return } msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID) if err != nil || msg == nil { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "MESSAGE_NOT_FOUND", "message": "指定消息不存在或已被删除", }) return } if !s.assertMessageOwnedByUser(w, r, msg, user.ID) { return } cap, ok := msg.ExportCapability.(replyexport.MessageExportCapability) if !ok { // 动态降级重新评估 hasCard := false if msg.Metadata != nil { if _, exists := msg.Metadata["interactive_card"]; exists { hasCard = true } } _, cap = replyexport.EvaluatePolicy(replyexport.PolicyOptions{ Role: msg.Role, Status: "completed", Content: msg.Content, HasInteractiveCard: hasCard, Revision: msg.ContentRevision, }) } resp := map[string]any{ "message_id": msg.ID, "policy": cap.Policy, "formats": cap.Formats, "body_selector": "content_only", "content_revision": cap.ContentRevision, "content_sha256": cap.ContentSha256, } if cap.ReasonCode != "" { resp["reason_code"] = cap.ReasonCode } writeJSONResponse(w, http.StatusOK, resp) } // handleGetExportSource 获取单条消息受控不可变正文快照 // GET /api/v1/chat/messages/{message_id}/export-source?content_revision= func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } if s.ctxMgr == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized") return } msgID := extractMessageIDFromPath(r, "export-source") if msgID <= 0 { writeJSONResponse(w, http.StatusBadRequest, map[string]string{ "error_code": "INVALID_MESSAGE_ID", "message": "无效的消息ID", }) return } msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID) if err != nil || msg == nil { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "MESSAGE_NOT_FOUND", "message": "指定消息不存在或已被删除", }) return } if !s.assertMessageOwnedByUser(w, r, msg, user.ID) { return } cap, ok := msg.ExportCapability.(replyexport.MessageExportCapability) if !ok { hasCard := false if msg.Metadata != nil { if _, exists := msg.Metadata["interactive_card"]; exists { hasCard = true } } _, cap = replyexport.EvaluatePolicy(replyexport.PolicyOptions{ Role: msg.Role, Status: "completed", Content: msg.Content, HasInteractiveCard: hasCard, Revision: msg.ContentRevision, }) } if cap.Policy == replyexport.PolicyNone { writeJSONResponse(w, http.StatusBadRequest, map[string]string{ "error_code": "MESSAGE_NOT_EXPORTABLE", "message": "该消息不支持导出为文档", }) return } // 校验 revision(若客户端传入了指定版本) if reqRevStr := r.URL.Query().Get("content_revision"); reqRevStr != "" { if reqRev, err := strconv.Atoi(reqRevStr); err == nil && reqRev > 0 { if reqRev != cap.ContentRevision { writeJSONResponse(w, http.StatusConflict, map[string]string{ "error_code": "MESSAGE_REVISION_CHANGED", "message": "消息内容版本已更新,请重新加载最新快照", }) return } } } title := replyexport.DeriveTitle(msg.Content, msg.CreatedAt) snapshotID := fmt.Sprintf("msg:%d:rev:%d", msg.ID, cap.ContentRevision) w.Header().Set("Cache-Control", "private, no-store") writeJSONResponse(w, http.StatusOK, map[string]any{ "snapshot_id": snapshotID, "message_id": msg.ID, "content_revision": cap.ContentRevision, "content_sha256": cap.ContentSha256, "title": title, "markdown": msg.Content, "capability": cap, }) } // extractMessageIDFromPath 从路由参数或 URL 路径中提取 message_id func extractMessageIDFromPath(r *http.Request, actionName string) int64 { // 优先尝试 Go 1.22 PathValue val := r.PathValue("message_id") if val != "" { id, err := strconv.ParseInt(val, 10, 64) if err == nil && id > 0 { return id } } // 降级:从路径分割提取 /api/v1/chat/messages/{id}/{action} parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") for i, p := range parts { if p == "messages" && i+1 < len(parts) { id, err := strconv.ParseInt(parts[i+1], 10, 64) if err == nil && id > 0 { return id } } } return 0 }