feat(workflow): Phase 3 Coordinator 上下文、动作路由与工作流运行 API 集成
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,15 +26,18 @@ type UserInfo struct {
|
||||
|
||||
// ProjectInfo 项目与工作流当前基准
|
||||
type ProjectInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
CurrentWorkflow string `json:"current_workflow"`
|
||||
CurrentStage bizdev.MaturityStage `json:"current_stage"`
|
||||
StageRules []string `json:"stage_rules,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
CreatorUserID int64 `json:"creator_user_id,omitempty"`
|
||||
ActiveWorkflowKey string `json:"active_workflow_key,omitempty"`
|
||||
WorkflowSelectionVersion int `json:"workflow_selection_version,omitempty"`
|
||||
CurrentWorkflow string `json:"current_workflow"`
|
||||
CurrentStage bizdev.MaturityStage `json:"current_stage"`
|
||||
StageRules []string `json:"stage_rules,omitempty"`
|
||||
}
|
||||
|
||||
// UserDocsInfo 用户相关文档清单与认领状态
|
||||
@@ -120,6 +124,21 @@ func ComputeAvailableActions(stage bizdev.MaturityStage, pRole string) []string
|
||||
return actions
|
||||
}
|
||||
|
||||
// ComputeAvailableActionsWithWorkflow 结合激活工作流、成熟度阶段与角色权限计算可用操作集合
|
||||
func ComputeAvailableActionsWithWorkflow(stage bizdev.MaturityStage, pRole string, activeWorkflow string, isCreator bool) []string {
|
||||
actions := ComputeAvailableActions(stage, pRole)
|
||||
if isCreator {
|
||||
actions = append(actions, "SWITCH_WORKFLOW")
|
||||
}
|
||||
switch activeWorkflow {
|
||||
case "product_manager":
|
||||
actions = append(actions, "TRIGGER_TASK:system_reqs", "TRIGGER_WORKFLOW:product_manager")
|
||||
case "test_manager":
|
||||
actions = append(actions, "TRIGGER_TASK:test_manager", "TRIGGER_WORKFLOW:test_manager")
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// ToSnapshot 转换为 L1 意图识别器所需的轻量上下文快照
|
||||
func (ctx *UserProjectContext) ToSnapshot() *intent.ContextSnapshot {
|
||||
if ctx == nil {
|
||||
@@ -316,6 +335,9 @@ func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo)
|
||||
}
|
||||
|
||||
// 3. 尝试提取阶段与文档
|
||||
if ctx.Project.ActiveWorkflowKey != "" {
|
||||
ctx.Project.CurrentWorkflow = ctx.Project.ActiveWorkflowKey
|
||||
}
|
||||
if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.1") || ctx.Project.CurrentWorkflow == "intake" {
|
||||
ctx.Project.CurrentStage = bizdev.StageV01_Triage
|
||||
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.5") {
|
||||
@@ -327,7 +349,13 @@ func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo)
|
||||
if ctx.Project.CurrentStage == "" {
|
||||
ctx.Project.CurrentStage = bizdev.StageV01_Triage
|
||||
}
|
||||
ctx.AvailableActions = ComputeAvailableActions(ctx.Project.CurrentStage, user.ProjectRole)
|
||||
|
||||
isCreator := false
|
||||
if user.ID != "" && ctx.Project.CreatorUserID > 0 {
|
||||
uid, _ := strconv.ParseInt(user.ID, 10, 64)
|
||||
isCreator = (uid == ctx.Project.CreatorUserID)
|
||||
}
|
||||
ctx.AvailableActions = ComputeAvailableActionsWithWorkflow(ctx.Project.CurrentStage, user.ProjectRole, ctx.Project.ActiveWorkflowKey, isCreator)
|
||||
|
||||
// 4. 优先尝试直接读取持久化的 advisor_brief.json
|
||||
briefPath := filepath.Join(workDir, "advisor_brief.json")
|
||||
|
||||
@@ -416,11 +416,20 @@ func (m *ContextManager) GetProjectContext(ctx context.Context, projectID int64)
|
||||
|
||||
if m.pool != nil {
|
||||
// 1. 获取项目基准与黑板事实
|
||||
var pCode, pName, cStage string
|
||||
_ = m.pool.QueryRow(ctx, "SELECT code, name, current_stage FROM projects WHERE id = $1", projectID).Scan(&pCode, &pName, &cStage)
|
||||
var pCode, pName, cStage, actWf string
|
||||
var creatorID int64
|
||||
var wfVer int
|
||||
_ = m.pool.QueryRow(ctx, `
|
||||
SELECT code, name, current_stage, COALESCE(creator_user_id, 1),
|
||||
COALESCE(active_workflow_key, 'main'), COALESCE(workflow_selection_version, 1)
|
||||
FROM projects WHERE id = $1
|
||||
`, projectID).Scan(&pCode, &pName, &cStage, &creatorID, &actWf, &wfVer)
|
||||
pCtx.ProjectCode = pCode
|
||||
pCtx.ProjectName = pName
|
||||
pCtx.CurrentStage = cStage
|
||||
pCtx.CreatorUserID = creatorID
|
||||
pCtx.ActiveWorkflowKey = actWf
|
||||
pCtx.WorkflowSelectionVersion = wfVer
|
||||
|
||||
var factsJSON []byte
|
||||
err := m.pool.QueryRow(ctx, "SELECT facts FROM project_blackboards WHERE project_id = $1", projectID).Scan(&factsJSON)
|
||||
|
||||
@@ -81,8 +81,11 @@ type ProjectCollectiveContext struct {
|
||||
ProjectID int64 `json:"project_id"`
|
||||
ProjectCode string `json:"project_code"`
|
||||
ProjectName string `json:"project_name"`
|
||||
CurrentStage string `json:"current_stage"`
|
||||
BlackboardFacts map[string]string `json:"blackboard_facts"`
|
||||
CurrentStage string `json:"current_stage"`
|
||||
CreatorUserID int64 `json:"creator_user_id,omitempty"`
|
||||
ActiveWorkflowKey string `json:"active_workflow_key,omitempty"`
|
||||
WorkflowSelectionVersion int `json:"workflow_selection_version,omitempty"`
|
||||
BlackboardFacts map[string]string `json:"blackboard_facts"`
|
||||
AllActivities []ActivityItem `json:"all_activities"`
|
||||
MilestoneDecisions []ActivityItem `json:"milestone_decisions"`
|
||||
SharedFiles []*storage.FileReference `json:"shared_files"`
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -63,6 +64,7 @@ type TaskRunRequestDTO struct {
|
||||
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) {
|
||||
@@ -137,6 +139,33 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
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{
|
||||
@@ -161,9 +190,23 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
@@ -195,10 +238,21 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -165,3 +167,64 @@ func (s *Server) handleListWorkflowDefinitions(w http.ResponseWriter, r *http.Re
|
||||
defs := s.pwSvc.Catalog().List()
|
||||
writeJSONResponse(w, http.StatusOK, defs)
|
||||
}
|
||||
|
||||
// handleRunProjectWorkflow 针对特定项目与工作流触发运行并落盘运行记录
|
||||
func (s *Server) handleRunProjectWorkflow(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wfKey := r.PathValue("workflow_key")
|
||||
if wfKey == "" {
|
||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
def, ok := s.pwSvc.Catalog().Get(wfKey)
|
||||
if !ok {
|
||||
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
user, _ := s.authenticateUser(r)
|
||||
var uid int64 = 1
|
||||
if user != nil {
|
||||
uid = user.ID
|
||||
}
|
||||
|
||||
var req TaskRunRequestDTO
|
||||
if r.Body != nil && r.ContentLength > 0 {
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
}
|
||||
req.ProjectID = id
|
||||
req.WorkflowKey = wfKey
|
||||
req.UserID = uid
|
||||
if req.TaskKey == "" {
|
||||
req.TaskKey = def.TaskKey
|
||||
}
|
||||
|
||||
bodyBytes, _ := json.Marshal(req)
|
||||
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
s.handleRunTask(w, r)
|
||||
}
|
||||
|
||||
// handleListProjectWorkflowRuns 查询项目某工作流的历史运行记录
|
||||
func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wfKey := r.PathValue("workflow_key")
|
||||
runs, err := s.pwSvc.ListWorkflowRuns(r.Context(), id, wfKey, 20)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if runs == nil {
|
||||
runs = make([]*projectworkflow.WorkflowRun, 0)
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, runs)
|
||||
}
|
||||
|
||||
@@ -132,4 +132,27 @@ func TestProjectWorkflowAPI_Lifecycle(t *testing.T) {
|
||||
err = json.Unmarshal(recDefs.Body.Bytes(), &defs)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, len(defs))
|
||||
|
||||
// 9. 触发工作流运行 -> 200 OK
|
||||
bodyRun, _ := json.Marshal(map[string]any{
|
||||
"task_key": "intake",
|
||||
"project_name": "智能高频电刀",
|
||||
"device_type": "高频手术设备",
|
||||
})
|
||||
reqRun := httptest.NewRequest(http.MethodPost, "/api/v1/projects/1001/workflows/main/run", bytes.NewReader(bodyRun))
|
||||
reqRun.Header.Set("X-User-ID", "2")
|
||||
recRun := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recRun, reqRun)
|
||||
assert.Equal(t, http.StatusOK, recRun.Code)
|
||||
|
||||
// 10. 查询运行历史 -> 200 OK 且包含刚运行的记录
|
||||
reqRuns := httptest.NewRequest(http.MethodGet, "/api/v1/projects/1001/workflows/main/runs", nil)
|
||||
recRuns := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recRuns, reqRuns)
|
||||
assert.Equal(t, http.StatusOK, recRuns.Code)
|
||||
var runsList []*projectworkflow.WorkflowRun
|
||||
err = json.Unmarshal(recRuns.Body.Bytes(), &runsList)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, runsList)
|
||||
assert.Equal(t, "completed", runsList[0].Status)
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ func (s *Server) Routes() http.Handler {
|
||||
mux.HandleFunc("GET /api/v1/projects/{id}/workflows", s.handleListProjectWorkflows)
|
||||
mux.HandleFunc("PUT /api/v1/projects/{id}/active-workflow", s.handleActivateWorkflow)
|
||||
mux.HandleFunc("GET /api/v1/workflows", s.handleListWorkflowDefinitions)
|
||||
mux.HandleFunc("POST /api/v1/projects/{id}/workflows/{workflow_key}/run", s.handleRunProjectWorkflow)
|
||||
mux.HandleFunc("GET /api/v1/projects/{id}/workflows/{workflow_key}/runs", s.handleListProjectWorkflowRuns)
|
||||
|
||||
// 3. 对话协同服务 (标准 JSON 阻塞)
|
||||
mux.HandleFunc("POST /api/v1/chat", s.handleChat)
|
||||
|
||||
Reference in New Issue
Block a user