Files
WeiCHENandCursor f4e4357ddb fix(agent): Chat Agent 全链路 P0-P5 整改
封堵鉴权旁路与导出 IDOR,DocGov 改服务端身份;对话层 PendingAction 接真实 docgov/taskrun;修复流式租约假死与会话级任务流;加固配置/doc_id/NULL;补齐 slog 与测试;前端 ErrorBoundary 与受控文档重试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:22 +00:00

190 lines
5.2 KiB
Go

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.svc == nil || s.svc.ContextManager() == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
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.svc.ContextManager().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.svc == nil || s.svc.ContextManager() == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
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.svc.ContextManager().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
}