Files
Agent-System/internal/server/handlers.go
T

930 lines
29 KiB
Go

package server
import (
"encoding/json"
"errors"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/squad"
)
// HealthResponse 健康检查响应
type HealthResponse struct {
Status string `json:"status"`
Env string `json:"env"`
AgentBackend string `json:"agent_backend"`
Version string `json:"version"`
SharedKBPort int `json:"shared_kb_port"`
}
// ChatRequest 对话请求
type ChatRequest struct {
SessionID string `json:"session_id"`
Message string `json:"message"`
ProjectName string `json:"project_name,omitempty"`
CustomerName string `json:"customer_name,omitempty"`
DeviceType string `json:"device_type,omitempty"`
IntendedUse string `json:"intended_use,omitempty"`
TargetMarket string `json:"target_market,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
UserName string `json:"user_name,omitempty"`
UserRole string `json:"user_role,omitempty"`
UserID int64 `json:"user_id,omitempty"`
ProjectID int64 `json:"project_id,omitempty"`
}
// TaskRunRequestDTO 独立任务运行请求
type TaskRunRequestDTO struct {
TaskKey string `json:"task_key"`
Stage string `json:"stage,omitempty"` // 若指定 stage,则执行成熟度阶段跃迁
ProjectName string `json:"project_name"`
CustomerName string `json:"customer_name"`
DeviceType string `json:"device_type"`
IntendedUse string `json:"intended_use"`
TargetMarket string `json:"target_market"`
Fields map[string]string `json:"fields,omitempty"`
Backend string `json:"backend,omitempty"`
AutoRepair bool `json:"auto_repair"`
UserID int64 `json:"user_id,omitempty"`
ProjectID int64 `json:"project_id,omitempty"`
DocID string `json:"doc_id,omitempty"`
AuthorID int64 `json:"author_id,omitempty"`
AuthorName string `json:"author_name,omitempty"`
ReviewerID int64 `json:"reviewer_id,omitempty"`
ReviewerName string `json:"reviewer_name,omitempty"`
ApproverID int64 `json:"approver_id,omitempty"`
ApproverName string `json:"approver_name,omitempty"`
ForceDirectEdit bool `json:"force_direct_edit,omitempty"`
CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"`
WorkflowKey string `json:"workflow_key,omitempty"`
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
resp := HealthResponse{
Status: "ok",
Env: s.cfg.Env,
AgentBackend: s.cfg.AgentBackend,
Version: "v1.0",
SharedKBPort: s.cfg.KbDbPort,
}
writeJSONResponse(w, http.StatusOK, resp)
}
func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
http.Error(w, "missing session_id query param", http.StatusBadRequest)
return
}
sess := s.getOrCreateSession(sessionID, "", "", "", "", "", "", "", "")
if sess == nil || sess.UserCtx == nil {
http.Error(w, "session or project context not found", http.StatusNotFound)
return
}
writeJSONResponse(w, http.StatusOK, sess.UserCtx)
}
func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
var req ChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if req.Message == "" {
http.Error(w, "message cannot be empty", http.StatusBadRequest)
return
}
sess := s.getOrCreateSession(
req.SessionID,
req.ProjectName,
req.CustomerName,
req.DeviceType,
req.IntendedUse,
req.TargetMarket,
req.WorkDir,
req.UserName,
req.UserRole,
)
if req.UserID > 0 {
sess.UserID = req.UserID
}
if req.ProjectID > 0 {
sess.ProjectID = req.ProjectID
}
resp, err := sess.SendMessage(r.Context(), req.Message)
if err != nil {
http.Error(w, "process chat message failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, resp)
}
func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
var req TaskRunRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
// 0. 如果关联了项目,在 projectworkflow 服务中记录运行实例
var runRecord *projectworkflow.WorkflowRun
if s.pwSvc != nil && req.ProjectID > 0 {
wfKey := req.WorkflowKey
if wfKey == "" {
if p, _ := s.pwSvc.GetProject(r.Context(), req.ProjectID, req.UserID); p != nil {
wfKey = p.ActiveWorkflowKey
}
}
if wfKey == "" {
wfKey = "main"
}
manifest := map[string]interface{}{
"fields": req.Fields,
"doc_id": req.DocID,
}
runRecord, _ = s.pwSvc.CreateWorkflowRun(r.Context(), &projectworkflow.WorkflowRun{
ProjectID: req.ProjectID,
WorkflowKey: wfKey,
Status: "running",
StartedBy: &req.UserID,
StageKey: req.Stage,
TaskKey: req.TaskKey,
InputManifest: manifest,
})
}
// 1. 如果指定了 stage,则执行成熟度阶段编排
if req.Stage != "" {
stageRes, err := s.svc.ExecuteStage(r.Context(), bizdev.StageRunRequest{
Stage: bizdev.MaturityStage(req.Stage),
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Fields: req.Fields,
Backend: req.Backend,
AutoRepair: req.AutoRepair,
UserID: req.UserID,
ProjectID: req.ProjectID,
DocID: req.DocID,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
ReviewerID: req.ReviewerID,
ReviewerName: req.ReviewerName,
ApproverID: req.ApproverID,
ApproverName: req.ApproverName,
ForceDirectEdit: req.ForceDirectEdit,
})
if err != nil {
if runRecord != nil {
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error())
}
writeTaskError(w, err, stageRes)
return
}
if runRecord != nil && stageRes != nil {
outDocID := ""
outSummary := stageRes.StageTitle
if stageRes.PrimaryArtifact != nil {
if stageRes.PrimaryArtifact.DocRecord != nil {
outDocID = stageRes.PrimaryArtifact.DocRecord.DocID
}
outSummary = stageRes.PrimaryArtifact.ArtifactTitle
}
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "completed", outDocID, outSummary, "")
}
writeJSONResponse(w, http.StatusOK, stageRes)
return
}
// 2. 执行单任务
if req.TaskKey == "" {
req.TaskKey = "intake"
}
taskRes, err := s.svc.ExecuteTask(r.Context(), bizdev.TaskRunRequest{
TaskKey: req.TaskKey,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Fields: req.Fields,
Backend: req.Backend,
AutoRepair: req.AutoRepair,
UserID: req.UserID,
ProjectID: req.ProjectID,
DocID: req.DocID,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
ReviewerID: req.ReviewerID,
ReviewerName: req.ReviewerName,
ApproverID: req.ApproverID,
ApproverName: req.ApproverName,
ForceDirectEdit: req.ForceDirectEdit,
CustomSteps: req.CustomSteps,
})
if err != nil {
if runRecord != nil {
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error())
}
writeTaskError(w, err, taskRes)
return
}
if runRecord != nil && taskRes != nil {
outDocID := ""
if taskRes.DocRecord != nil {
outDocID = taskRes.DocRecord.DocID
}
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "completed", outDocID, taskRes.ArtifactTitle, "")
}
// 隐藏不必要的中间大体积字节
if taskRes.WorkDir != "" {
taskRes.WorkDir = filepath.Clean(taskRes.WorkDir)
}
writeJSONResponse(w, http.StatusOK, taskRes)
}
func writeTaskError(w http.ResponseWriter, err error, payload any) {
status := http.StatusInternalServerError
if errors.Is(err, docgov.ErrDocumentBusy) {
status = http.StatusConflict
} else if errors.Is(err, docgov.ErrDocumentLocked) || errors.Is(err, docgov.ErrForceEditNotAllowed) {
status = http.StatusConflict
} else if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) {
status = http.StatusBadRequest
}
if payload != nil {
writeJSONResponse(w, status, payload)
return
}
http.Error(w, err.Error(), status)
}
func writeDocGovError(w http.ResponseWriter, err error) {
var busy *docgov.DocumentBusyError
if errors.As(err, &busy) {
writeJSONResponse(w, http.StatusConflict, map[string]any{
"error": "document_busy",
"detail": busy,
})
return
}
if errors.Is(err, docgov.ErrDocumentBusy) {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if errors.Is(err, docgov.ErrDocumentLocked) {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if errors.Is(err, docgov.ErrDocumentNotFound) {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if errors.Is(err, docgov.ErrLeaseNotHeld) || errors.Is(err, docgov.ErrLeaseExpired) {
http.Error(w, err.Error(), http.StatusConflict)
return
}
http.Error(w, err.Error(), http.StatusBadRequest)
}
func writeJSONResponse(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}
// -----------------------------------------------------------------------------
// 多级上下文与偏好记忆 RESTful 控制器
// -----------------------------------------------------------------------------
// handleGetUserContext 获取单用户私有上下文与个性化偏好
func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) {
userIDStr := r.URL.Query().Get("user_id")
projectIDStr := r.URL.Query().Get("project_id")
sessionID := r.URL.Query().Get("session_id")
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
if userID <= 0 {
userID = 1
}
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
if s.svc == nil || s.svc.ContextManager() == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return
}
uCtx, err := s.svc.ContextManager().GetUserContext(r.Context(), projectID, userID, sessionID)
if err != nil {
http.Error(w, "get user context failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, uCtx)
}
// UpdateUserPreferenceRequest 更新用户工作偏好请求
type UpdateUserPreferenceRequest struct {
UserID int64 `json:"user_id"`
CommunicationStyle ctxmgr.CommunicationStyle `json:"communication_style"`
FocusAreas []string `json:"focus_areas"`
OutputFormatPref string `json:"output_format_pref"`
CustomInstructions string `json:"custom_instructions"`
}
// handleUpdateUserPreference 定期整理或前端保存用户习惯偏好
func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Request) {
var req UpdateUserPreferenceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if req.UserID <= 0 {
http.Error(w, "user_id must be positive", http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.ContextManager() == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return
}
pref := &ctxmgr.UserPreference{
UserID: req.UserID,
CommunicationStyle: req.CommunicationStyle,
FocusAreas: req.FocusAreas,
OutputFormatPref: req.OutputFormatPref,
CustomInstructions: req.CustomInstructions,
}
if err := s.svc.ContextManager().UpdateUserPreference(r.Context(), pref); err != nil {
http.Error(w, "save user preference failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{"status": "ok", "user_id": req.UserID})
}
// handleGetProjectContext 获取项目级多方协同事实黑板与里程碑决策流
func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request) {
projectIDStr := r.URL.Query().Get("project_id")
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
if projectID <= 0 {
http.Error(w, "missing or invalid project_id query param", http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.ContextManager() == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return
}
pCtx, err := s.svc.ContextManager().GetProjectContext(r.Context(), projectID)
if err != nil {
http.Error(w, "get project context failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, pCtx)
}
// -----------------------------------------------------------------------------
// 受控文档编制、审核、批准与认领生命周期 RESTful 控制器
// -----------------------------------------------------------------------------
// handleDocGovGetDocument 查询指定受控文档详情与治理状态
func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request) {
docID := r.URL.Query().Get("doc_id")
if docID == "" {
http.Error(w, "missing doc_id query param", http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
doc, err := s.svc.DocGov().GetDocument(r.Context(), docID)
if err != nil {
http.Error(w, "get document failed: "+err.Error(), http.StatusNotFound)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
// DocGovClaimRequest 认领文档请求
type DocGovClaimRequest struct {
DocID string `json:"doc_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
UserRole string `json:"user_role"` // author, reviewer, approver
}
// handleDocGovClaim 查阅并认领受控文档 (非模板状态查看方可认领)
func (s *Server) handleDocGovClaim(w http.ResponseWriter, r *http.Request) {
var req DocGovClaimRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if req.DocID == "" {
http.Error(w, "doc_id cannot be empty", http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
doc, err := s.svc.DocGov().RecordDocumentViewed(r.Context(), req.DocID, req.UserID, docgov.UserRole(req.UserRole), req.UserName)
if err != nil {
http.Error(w, "claim document failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
// DocGovSubmitReviewRequest 送审请求
type DocGovSubmitReviewRequest struct {
DocID string `json:"doc_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
}
// handleDocGovSubmitReview 编制人将文档送审 (drafting -> reviewing, 正文受控锁定)
func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request) {
var req DocGovSubmitReviewRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
doc, err := s.svc.DocGov().SubmitForReview(r.Context(), req.DocID, req.UserID, docgov.RoleAuthor, req.UserName)
if err != nil {
http.Error(w, "submit review failed: "+err.Error(), http.StatusBadRequest)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
// DocGovDecisionRequest 审核/批准决议请求
type DocGovDecisionRequest struct {
DocID string `json:"doc_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
Action string `json:"action"` // approve 或 reject
Comment string `json:"comment,omitempty"`
Reason string `json:"reason,omitempty"` // 驳回时必填
RollbackTo string `json:"rollback_to,omitempty"` // 批准人驳回时可选: drafting 或 reviewing
}
// handleDocGovReviewDecision 审核人决议 (通过流转至 approving,驳回必须填理由并回退 drafting 解锁)
func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Request) {
var req DocGovDecisionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
action := strings.ToLower(strings.TrimSpace(req.Action))
var doc *docgov.DocumentRecord
var err error
if action == "approve" {
doc, err = s.svc.DocGov().ReviewApprove(r.Context(), req.DocID, req.UserID, docgov.RoleReviewer, req.UserName, req.Comment)
} else if action == "reject" {
reason := req.Reason
if reason == "" {
reason = req.Comment
}
doc, err = s.svc.DocGov().ReviewReject(r.Context(), req.DocID, req.UserID, docgov.RoleReviewer, req.UserName, reason)
} else {
http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest)
return
}
if err != nil {
http.Error(w, "review decision failed: "+err.Error(), http.StatusBadRequest)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
// handleDocGovApproverDecision 批准人决议 (通过流转至 approved 终态归档,驳回必填理由并回退)
func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Request) {
var req DocGovDecisionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
action := strings.ToLower(strings.TrimSpace(req.Action))
var doc *docgov.DocumentRecord
var err error
if action == "approve" {
doc, err = s.svc.DocGov().ApproverApprove(r.Context(), req.DocID, req.UserID, docgov.RoleApprover, req.UserName, req.Comment)
} else if action == "reject" {
reason := req.Reason
if reason == "" {
reason = req.Comment
}
rollbackState := docgov.DocumentState(req.RollbackTo)
doc, err = s.svc.DocGov().ApproverReject(r.Context(), req.DocID, req.UserID, docgov.RoleApprover, req.UserName, reason, rollbackState)
} else {
http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest)
return
}
if err != nil {
http.Error(w, "approver decision failed: "+err.Error(), http.StatusBadRequest)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
// DecideProposalRequest 拍板变更提案请求
type DecideProposalRequest struct {
ProposalID string `json:"proposal_id"`
AuthorID int64 `json:"author_id"`
Approve bool `json:"approve"`
Reason string `json:"reason"`
}
// handleDocGovDecideProposal 编制负责人拍板决定是否采纳变更提案
func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Request) {
var req DecideProposalRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
proposal, err := s.svc.DocGov().DecideModificationProposal(r.Context(), req.ProposalID, req.AuthorID, req.Approve, req.Reason)
if err != nil {
http.Error(w, "decide proposal failed: "+err.Error(), http.StatusBadRequest)
return
}
writeJSONResponse(w, http.StatusOK, proposal)
}
// handleDocGovListComments 查询批注与修改建议清单
func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request) {
docID := r.URL.Query().Get("doc_id")
if docID == "" {
http.Error(w, "missing doc_id query param", http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
comments, err := s.svc.DocGov().ListComments(r.Context(), docID)
if err != nil {
http.Error(w, "list comments failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, comments)
}
// AddCommentRequestDTO 新增批注请求
type AddCommentRequestDTO struct {
DocID string `json:"doc_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
UserRole string `json:"user_role"`
TargetSection string `json:"target_section,omitempty"`
CommentType string `json:"comment_type,omitempty"`
Content string `json:"content"`
}
// handleDocGovAddComment 审核人/批准人/编制人新增章节批注或修改建议
func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request) {
var req AddCommentRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
cType := docgov.CommentType(req.CommentType)
if cType == "" {
cType = docgov.CommentGeneral
}
comment, err := s.svc.DocGov().AddComment(
r.Context(), req.DocID, req.UserID, req.UserName,
docgov.UserRole(req.UserRole), req.TargetSection, cType, req.Content,
)
if err != nil {
writeDocGovError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, comment)
}
// handleDocGovListAuditLogs 查询文档全生命周期审计轨迹流
func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Request) {
docID := r.URL.Query().Get("doc_id")
if docID == "" {
http.Error(w, "missing doc_id query param", http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
logs, err := s.svc.DocGov().ListAuditLogs(r.Context(), docID)
if err != nil {
http.Error(w, "list audit logs failed: "+err.Error(), http.StatusInternalServerError)
return
}
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)
}
// -----------------------------------------------------------------------------
// 文档正文保存与编辑租约
// -----------------------------------------------------------------------------
type SaveContentRequestDTO struct {
DocID string `json:"doc_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
UserRole string `json:"user_role"`
MarkdownBody string `json:"markdown_body"`
ExpectedSHA256 string `json:"expected_sha256,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
}
func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request) {
var req SaveContentRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
role := docgov.UserRole(req.UserRole)
if role == "" {
role = docgov.RoleAuthor
}
res, err := s.svc.DocGov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
DocID: req.DocID,
ActorID: req.UserID,
ActorRole: role,
ActorName: req.UserName,
MarkdownBody: req.MarkdownBody,
ExpectedSHA256: req.ExpectedSHA256,
WorkDirOverride: req.WorkDir,
})
if err != nil {
writeDocGovError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, res)
}
type LeaseRequestDTO struct {
DocID string `json:"doc_id"`
HolderType string `json:"holder_type"`
HolderID string `json:"holder_id"`
WorkDir string `json:"work_dir,omitempty"`
TTLSeconds int `json:"ttl_seconds,omitempty"`
}
func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request) {
var req LeaseRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
ttl := time.Duration(req.TTLSeconds) * time.Second
doc, err := s.svc.DocGov().AcquireLease(r.Context(), docgov.LeaseAcquireRequest{
DocID: req.DocID,
HolderType: docgov.LeaseHolderType(req.HolderType),
HolderID: req.HolderID,
WorkDir: req.WorkDir,
TTL: ttl,
})
if err != nil {
writeDocGovError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Request) {
var req LeaseRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
ttl := time.Duration(req.TTLSeconds) * time.Second
doc, err := s.svc.DocGov().HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl)
if err != nil {
writeDocGovError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request) {
var req LeaseRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if s.svc == nil || s.svc.DocGov() == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return
}
doc, err := s.svc.DocGov().ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID)
if err != nil {
writeDocGovError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}