Files
Agent-System/internal/server/freeform_doc_api.go
T
WeiCHEN 1fc7d0d01f feat(export): 实现单次智能体回复导出、私密文档保存与隐私防泄露全套闭环 (Phase 0 - Phase 2)
- Phase 0: 实现服务端回复导出策略矩阵引擎 (replyexport.EvaluatePolicy)、数据库迁移 (2026-09-11-message-export-policy.sql)、SSE done 事件挂载与能力探测 API
- Phase 1: 构建前端 ReplyDocumentIR 语法树解析与渲染管道,支持 Markdown / DOCX 文件下载及带格式富文本剪贴板复制
- Phase 2: 实现私密自由文档 (freeform_documents) 领域服务、PostgreSQL 乐观锁版本管理、ACL 隐私隔离、一键共享与右侧栏编辑联动
- 专项修复:
  1. 修复后台 API 路由注册并加入双向拉取同步,确保私密保存实时进入我的私密列表
  2. 实现轻量回复专属 DOCX 生成与静默下载逻辑 (downloadDocxReply)
  3. 将 LaTeX 公式转为标准 MathML 矢量标记,支持粘贴到 Microsoft Word / WPS 自动识别为原生公式
2026-09-13 02:50:21 +00:00

367 lines
10 KiB
Go

package server
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"chat-agent-system/internal/freedoc"
)
func extractUserID(r *http.Request) int {
if headerVal := r.Header.Get("X-User-ID"); headerVal != "" {
if id, err := strconv.Atoi(headerVal); err == nil && id > 0 {
return id
}
}
if qVal := r.URL.Query().Get("user_id"); qVal != "" {
if id, err := strconv.Atoi(qVal); err == nil && id > 0 {
return id
}
}
return 1 // 默认当前用户
}
func extractDocIDFromPath(r *http.Request, actionSuffix string) string {
path := r.URL.Path
if actionSuffix != "" {
suffix := "/" + actionSuffix
if strings.HasSuffix(path, suffix) {
path = strings.TrimSuffix(path, suffix)
}
}
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) >= 4 && parts[0] == "api" && parts[1] == "v1" && parts[2] == "freeform-documents" {
return parts[3]
}
return r.PathValue("doc_id")
}
// POST /api/v1/freeform-documents/from-message
func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
var req freedoc.CreateFromMessageRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_BODY",
"message": "请求体格式错误",
})
return
}
detail, isExisting, err := s.svc.FreeformDoc().CreateFromMessage(r.Context(), userID, req)
if err != nil {
if errors.Is(err, freedoc.ErrMessageNotExportable) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{
"error_code": "MESSAGE_NOT_EXPORTABLE",
"message": "该消息不具备导出或保存资格(纯卡片或未完成)",
})
return
}
if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{
"error_code": "MESSAGE_NOT_FOUND",
"message": "指定源消息不存在",
})
return
}
if errors.Is(err, freedoc.ErrConflict) {
writeJSONResponse(w, http.StatusConflict, map[string]any{
"error_code": "CONTENT_SHA_MISMATCH",
"message": "消息内容哈希校验冲突",
})
return
}
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"error_code": "SAVE_FAILED",
"message": err.Error(),
})
return
}
status := http.StatusCreated
if isExisting {
status = http.StatusOK
}
writeJSONResponse(w, status, map[string]any{
"code": 0,
"message": "已保存至我的私密",
"data": detail,
"is_existing": isExisting,
})
}
// GET /api/v1/freeform-documents
func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
space := r.URL.Query().Get("space")
if space == "" {
space = "my_private"
}
projectID := 0
if pStr := r.URL.Query().Get("project_id"); pStr != "" {
projectID, _ = strconv.Atoi(pStr)
}
list, err := s.svc.FreeformDoc().ListDocuments(r.Context(), userID, projectID, space)
if err != nil {
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"error_code": "LIST_FAILED",
"message": err.Error(),
})
return
}
if list == nil {
list = []freedoc.FreeformDocSummary{}
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"code": 0,
"data": list,
})
}
// GET /api/v1/freeform-documents/{doc_id}
func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
docID := extractDocIDFromPath(r, "")
if docID == "" {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_DOC_ID",
"message": "缺少文档ID",
})
return
}
detail, err := s.svc.FreeformDoc().GetDocument(r.Context(), userID, docID)
if err != nil {
if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{
"error_code": "DOC_NOT_FOUND",
"message": "文档不存在或已被删除",
})
return
}
if errors.Is(err, freedoc.ErrForbidden) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{
"error_code": "DOC_ACCESS_FORBIDDEN",
"message": "该文档为他人私密文档,严格禁止跨用户访问",
})
return
}
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"error_code": "GET_FAILED",
"message": err.Error(),
})
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"code": 0,
"data": detail,
})
}
// PUT /api/v1/freeform-documents/{doc_id}
func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
docID := extractDocIDFromPath(r, "")
if docID == "" {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_DOC_ID",
"message": "缺少文档ID",
})
return
}
var req freedoc.UpdateFreeformDocRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_BODY",
"message": "请求体格式错误",
})
return
}
// 优先提取 HTTP If-Match 标头作为乐观并发校验
if ifMatch := r.Header.Get("If-Match"); ifMatch != "" {
cleanMatch := strings.Trim(ifMatch, "\"")
if v, err := strconv.Atoi(cleanMatch); err == nil && v > 0 {
req.ExpectedVersion = &v
} else {
req.ExpectedSHA256 = &cleanMatch
}
}
detail, err := s.svc.FreeformDoc().UpdateDocument(r.Context(), userID, docID, req)
if err != nil {
if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{
"error_code": "DOC_NOT_FOUND",
"message": "文档不存在",
})
return
}
if errors.Is(err, freedoc.ErrForbidden) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{
"error_code": "DOC_ACCESS_FORBIDDEN",
"message": "无权修改此文档",
})
return
}
if errors.Is(err, freedoc.ErrConflict) {
writeJSONResponse(w, http.StatusConflict, map[string]any{
"error_code": "CONCURRENCY_CONFLICT",
"message": "文档已被他人或新版本更新,请刷新重试",
})
return
}
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"error_code": "UPDATE_FAILED",
"message": err.Error(),
})
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"code": 0,
"message": "保存成功",
"data": detail,
})
}
// POST /api/v1/freeform-documents/{doc_id}/share
func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
docID := extractDocIDFromPath(r, "share")
if docID == "" {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_DOC_ID",
"message": "缺少文档ID",
})
return
}
doc, err := s.svc.FreeformDoc().ShareDocument(r.Context(), userID, docID)
if err != nil {
if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
return
}
if errors.Is(err, freedoc.ErrForbidden) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{"error_code": "FORBIDDEN", "message": "仅文档所有者可共享文档"})
return
}
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{"error_code": "SHARE_FAILED", "message": err.Error()})
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"code": 0,
"message": "已共享至项目参考库",
"data": doc,
})
}
// POST /api/v1/freeform-documents/{doc_id}/unshare
func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
docID := extractDocIDFromPath(r, "unshare")
if docID == "" {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_DOC_ID",
"message": "缺少文档ID",
})
return
}
doc, err := s.svc.FreeformDoc().UnshareDocument(r.Context(), userID, docID)
if err != nil {
if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
return
}
if errors.Is(err, freedoc.ErrForbidden) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{"error_code": "FORBIDDEN", "message": "仅文档所有者可撤回共享"})
return
}
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{"error_code": "UNSHARE_FAILED", "message": err.Error()})
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"code": 0,
"message": "已撤回为个人私密",
"data": doc,
})
}
// DELETE /api/v1/freeform-documents/{doc_id}
func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.FreeformDoc() == nil {
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return
}
userID := extractUserID(r)
docID := extractDocIDFromPath(r, "")
if docID == "" {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
"error_code": "INVALID_DOC_ID",
"message": "缺少文档ID",
})
return
}
err := s.svc.FreeformDoc().DeleteDocument(r.Context(), userID, docID)
if err != nil {
if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
return
}
if errors.Is(err, freedoc.ErrForbidden) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{"error_code": "FORBIDDEN", "message": "仅文档所有者可删除"})
return
}
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{"error_code": "DELETE_FAILED", "message": err.Error()})
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"code": 0,
"message": "已移至回收站",
})
}