Files
Agent-System/internal/coordinator/context.go
weichenandCursor 110c2e810c feat(stageflow): own task/stage types so coordinator drops bizdev
Coordinator now takes a StageTaskRunner and injected context/KB ports.
Chatflow assembler reads maturity tokens instead of the orchestration root.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 11:13:17 +00:00

422 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package coordinator
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/squad"
)
// UserInfo 用户在系统与项目内的身份
type UserInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"` // 系统角色 (如 engineer, pm, director)
ProjectRole string `json:"project_role"` // 项目内角色 (如 编制人 composer, 审核人 reviewer, 批准人 approver)
}
// 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"`
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 maturity.MaturityStage `json:"current_stage"`
StageRules []string `json:"stage_rules,omitempty"`
}
// UserDocsInfo 用户相关文档清单与认领状态
type UserDocsInfo struct {
Composed []string `json:"composed"` // 负责编制的文档
Reviewed []string `json:"reviewed"` // 负责审核的文档
Approved []string `json:"approved"` // 负责批准的文档
Claimed []string `json:"claimed"` // 已认领/已打开查阅的文档
}
// PendingItems 待用户处理或决策的待办项
type PendingItems struct {
ConfirmModifications []string `json:"confirm_modifications,omitempty"`
PendingReviews []string `json:"pending_reviews,omitempty"`
PendingApprovals []string `json:"pending_approvals,omitempty"`
MyDraftsInProgress []string `json:"my_drafts_in_progress,omitempty"`
UnreadSupplements []string `json:"unread_supplements,omitempty"`
}
// ReadableDocument 是已通过项目与阶段权限过滤后的只读文档快照。
// Markdown 只用于当轮总结,不写入上下文快照或对话元数据。
type ReadableDocument struct {
DocID string
Title string
TaskKey string
LifecycleState string
Version int
Markdown string
IsTemplateOnly bool
}
// DocumentSummaryReader 由 HTTP 服务端按当前登录用户和项目绑定。
// 协调层不得直接枚举文档,以免绕过阶段可见性规则。
type DocumentSummaryReader func(ctx context.Context, target string) (*ReadableDocument, error)
// UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费)
type UserProjectContext struct {
mu sync.Mutex
Version int64 `json:"version"`
SnapshotID string `json:"snapshot_id"`
AssembledAt time.Time `json:"assembled_at"`
User UserInfo `json:"user"`
Project ProjectInfo `json:"project"`
UserDocs UserDocsInfo `json:"user_docs"`
Pending PendingItems `json:"pending"`
BlackboardFacts map[string]string `json:"blackboard_facts"`
LatestAdvisorBrief *squad.AdvisorBriefing `json:"latest_advisor_brief,omitempty"`
PendingExpectation *intent.PendingExpectation `json:"pending_expectation,omitempty"`
InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"`
AvailableActions []string `json:"available_actions"`
RecentEvents []string `json:"recent_events,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
// RecentDialogue 近轮对话(供 Phase A 对话 LLM 使用,不序列化到快照)
RecentDialogue []DialogueTurn `json:"-"`
// DialogueSummary 早期对话滚动压缩摘要(由 ctxmgr 维护生成,不序列化到快照)
DialogueSummary string `json:"-"`
// TaskState 会话级任务状态(DST,记录 active_task, topic_anchor, collected_slots 等)
TaskState *ctxmgr.ConversationTaskState `json:"task_state,omitempty"`
// PersonalizedGuidance 用户工作习惯/偏好注入语(由 ctxmgr 编译,供对话 LLM 使用)
PersonalizedGuidance string `json:"-"`
// RecalledMemories 按需语义召回的相关历史片段(TopK ≤ 3)
RecalledMemories []ctxmgr.RecalledMemoryItem `json:"-"`
// RecalledMemorySnippet 格式化后的相关历史片段 Prompt 文本
RecalledMemorySnippet string `json:"-"`
// RewrittenQuery 规则改写补全后的完整独立问句(用于检索与 Prompt 双呈现,不进意图识别)
RewrittenQuery string `json:"-"`
// RewriteRules 本轮命中的改写规则列表(如 ellipsis_completion, pronoun_resolution)
RewriteRules []string `json:"-"`
// RegulationHits 知识库法规与强标检索命中项(带 CiteKey 引用键,供对话层精准引用)
RegulationHits []kb.HitItem `json:"-"`
// UpgradeOffered 路由升级建议冷却标记(每会话至多主动建议一次,防打扰——对齐"参谋不强推"人设)
UpgradeOffered bool `json:"-"`
// DocumentSummaryReader 仅用于服务端鉴权后的只读正文访问,不序列化。
DocumentSummaryReader DocumentSummaryReader `json:"-"`
}
// ContextAssembler 上下文组装服务
type ContextAssembler struct{}
func NewContextAssembler() *ContextAssembler {
return &ContextAssembler{}
}
// ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合
func ComputeAvailableActions(stage maturity.MaturityStage, pRole string) []string {
actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"}
switch stage {
case maturity.StageV01_Triage:
actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility")
case maturity.StageV05_Feasibility:
actions = append(actions,
"TRIGGER_TASK:research",
"TRIGGER_TASK:feasibility_reg",
"TRIGGER_TASK:prelim_design",
"ADVANCE_STAGE:V1.0_quoting",
)
case maturity.StageV10_Quoting:
actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL")
default:
actions = append(actions, "ADVANCE_STAGE:V0.1_triage")
}
return actions
}
// ComputeAvailableActionsWithWorkflow 结合激活工作流、成熟度阶段与角色权限计算可用操作集合
func ComputeAvailableActionsWithWorkflow(stage maturity.MaturityStage, pRole string, activeWorkflow string, isCreator bool) []string {
actions := ComputeAvailableActions(stage, pRole)
actions = appendUniqueCapability(actions, "QUERY_WORKFLOW")
if isCreator {
actions = appendUniqueCapability(actions, "SWITCH_WORKFLOW")
}
switch activeWorkflow {
case "product_manager":
actions = appendUniqueCapability(actions, "TRIGGER_TASK:system_reqs", "TRIGGER_WORKFLOW:product_manager")
case "test_manager":
actions = appendUniqueCapability(actions, "TRIGGER_TASK:test_manager", "TRIGGER_WORKFLOW:test_manager")
default:
if activeWorkflow != "" && activeWorkflow != "main" {
actions = appendUniqueCapability(actions, "TRIGGER_WORKFLOW:"+activeWorkflow)
}
}
return actions
}
func appendUniqueCapability(actions []string, keys ...string) []string {
seen := make(map[string]struct{}, len(actions))
for _, a := range actions {
seen[a] = struct{}{}
}
for _, k := range keys {
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
actions = append(actions, k)
}
return actions
}
// ToSnapshot 转换为 L1 意图识别器所需的轻量上下文快照
func (ctx *UserProjectContext) ToSnapshot() *intent.ContextSnapshot {
if ctx == nil {
return nil
}
snap := &intent.ContextSnapshot{
SnapshotID: ctx.SnapshotID,
ProjectName: ctx.Project.Name,
CurrentStage: string(ctx.Project.CurrentStage),
AvailableActions: ctx.AvailableActions,
PendingExpectation: ctx.PendingExpectation,
}
if ctx.User.ProjectRole != "" {
snap.UserDocRole = ctx.User.ProjectRole
} else if ctx.User.Role != "" {
snap.UserDocRole = ctx.User.Role
}
if ctx.InteractiveCard != nil && ctx.InteractiveCard.CurrentStatus == "active" {
snap.ActiveDocID = ctx.InteractiveCard.TargetDocID
if ctx.InteractiveCard.TargetDocID != "" {
snap.DocLifecycleState = "reviewing"
}
}
return snap
}
// BuildNextExpectation 根据当前成熟度阶段与项目名构造参谋下一步建议选项草稿
func BuildNextExpectation(stage maturity.MaturityStage, projectName string) *intent.PendingExpectation {
if projectName == "" {
projectName = "本项目"
}
var options []intent.ExpectationOption
var snippet string
switch stage {
case maturity.StageV01_Triage:
snippet = fmt.Sprintf("项目「%s」当前处于初始摸底期,基准产品画像已梳理完成。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "进入 V0.5 方案成型与法规对齐",
Description: "自动比选技术路线,并梳理分类路径与型检标准",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(maturity.StageV05_Feasibility),
},
},
{
OptionID: "2",
Title: "补充或微调事实信息",
Description: "随时告知我客户的最新的预算、目标市场或使用场景",
Intent: intent.IntentSupplementInfo,
},
{
OptionID: "3",
Title: "查阅当前立项成熟度与对外沟通建议",
Description: "获取进展研判与对外商务沟通话术",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "all",
},
},
}
case maturity.StageV05_Feasibility:
snippet = fmt.Sprintf("项目「%s」已完成方案成型与法规对齐,具备进入商务核算的条件。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "推进到 V1.0 商业锁定阶段",
Description: "生成精细化 CDO 工时与成本核算表,锁定立项商务方案",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(maturity.StageV10_Quoting),
},
},
{
OptionID: "2",
Title: "微调设计方案或技术参数",
Description: "进一步指定元器件品牌、特殊工艺或交付节点",
Intent: intent.IntentSupplementInfo,
},
{
OptionID: "3",
Title: "审阅初步系统架构图与法规分类论证说明",
Description: "查看技术路线比选报告与检验标准分析",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "progress",
},
},
}
case maturity.StageV10_Quoting:
snippet = fmt.Sprintf("项目「%s」已完成商务锁定,报价核算表已就绪。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "重新核算或调整报价单参数",
Description: "调整费率、工时模型或元器件采购预算",
Intent: intent.IntentTriggerTask,
Slots: map[string]string{
intent.SlotTargetTask: "cdo_quote",
},
},
{
OptionID: "2",
Title: "生成面向客户负责人的正式沟通函件",
Description: "草拟发给客户的商务方案总揽与下一步推进排期",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "talk_script",
},
},
}
default:
snippet = fmt.Sprintf("项目「%s」当前处于初始准备阶段。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "启动 V0.1 闪电摸底",
Description: "快速沉淀第一版立项画像并为您准备沟通话术",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(maturity.StageV01_Triage),
},
},
}
}
var defaultOpt *intent.ExpectationOption
if len(options) > 0 {
defaultOpt = &options[0]
}
return &intent.PendingExpectation{
ContextSnippet: snippet,
DefaultOption: defaultOpt,
Options: options,
AllowAffirmative: true,
CreatedAt: time.Now(),
}
}
// AssembleFromWorkspace 基于现有工作区快速组装 UserProjectContext
func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo) (*UserProjectContext, error) {
snapID := fmt.Sprintf("ctx_%d", time.Now().UnixNano())
ctx := &UserProjectContext{
SnapshotID: snapID,
AssembledAt: time.Now(),
User: user,
WorkDir: workDir,
BlackboardFacts: make(map[string]string),
AvailableActions: []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"},
}
// 1. 尝试读取 workspace.json
wsMetaPath := filepath.Join(workDir, "workspace.json")
if data, err := os.ReadFile(wsMetaPath); err == nil {
var wsMeta squad.WorkspaceMetadata
if json.Unmarshal(data, &wsMeta) == nil {
ctx.Project.Name = wsMeta.ProjectName
ctx.Project.CustomerName = wsMeta.CustomerName
ctx.Project.DeviceType = wsMeta.DeviceType
ctx.Project.IntendedUse = wsMeta.IntendedUse
ctx.Project.TargetMarket = wsMeta.TargetMarket
ctx.Project.CurrentWorkflow = wsMeta.TaskKey
}
} else {
// 备选:从目录名推断
baseName := filepath.Base(workDir)
if strings.HasPrefix(baseName, "intake_") {
ctx.Project.CurrentWorkflow = "intake"
}
}
// 2. 尝试读取 blackboard.json
bbPath := filepath.Join(workDir, "blackboard.json")
if data, err := os.ReadFile(bbPath); err == nil {
var bb squad.BlackboardState
if json.Unmarshal(data, &bb) == nil {
for k, v := range bb.Fields {
ctx.BlackboardFacts[k] = v
}
for _, g := range bb.Gaps {
if g.Blocking && !g.Resolved {
ctx.Pending.ConfirmModifications = append(ctx.Pending.ConfirmModifications, g.Item)
}
}
}
}
// 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 = maturity.StageV01_Triage
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.5") {
ctx.Project.CurrentStage = maturity.StageV05_Feasibility
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V1.0") {
ctx.Project.CurrentStage = maturity.StageV10_Quoting
}
if ctx.Project.CurrentStage == "" {
ctx.Project.CurrentStage = maturity.StageV01_Triage
}
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")
if data, err := os.ReadFile(briefPath); err == nil {
var brief squad.AdvisorBriefing
if json.Unmarshal(data, &brief) == nil {
ctx.LatestAdvisorBrief = &brief
}
}
// 5. 若无 advisor_brief.json 但有 output.md,尝试从文本抽取
if ctx.LatestAdvisorBrief == nil {
outPath := filepath.Join(workDir, "output.md")
if outBytes, err := os.ReadFile(outPath); err == nil && len(outBytes) > 0 {
ctx.LatestAdvisorBrief = squad.ExtractAdvisorBriefing(ctx.Project.CurrentWorkflow, string(outBytes), ctx.BlackboardFacts)
ctx.UserDocs.Composed = append(ctx.UserDocs.Composed, ctx.Project.CurrentWorkflow)
}
}
return ctx, nil
}