336 lines
12 KiB
Go
336 lines
12 KiB
Go
package coordinator
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/intent"
|
|
"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"`
|
|
CurrentWorkflow string `json:"current_workflow"`
|
|
CurrentStage bizdev.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"`
|
|
}
|
|
|
|
// UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费)
|
|
type UserProjectContext struct {
|
|
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:"-"`
|
|
// PersonalizedGuidance 用户工作习惯/偏好注入语(由 ctxmgr 编译,供对话 LLM 使用)
|
|
PersonalizedGuidance string `json:"-"`
|
|
// UpgradeOffered 路由升级建议冷却标记(每会话至多主动建议一次,防打扰——对齐"参谋不强推"人设)
|
|
UpgradeOffered bool `json:"-"`
|
|
}
|
|
|
|
// ContextAssembler 上下文组装服务
|
|
type ContextAssembler struct{}
|
|
|
|
func NewContextAssembler() *ContextAssembler {
|
|
return &ContextAssembler{}
|
|
}
|
|
|
|
// ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合
|
|
func ComputeAvailableActions(stage bizdev.MaturityStage, pRole string) []string {
|
|
actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"}
|
|
|
|
switch stage {
|
|
case bizdev.StageV01_Triage:
|
|
actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility")
|
|
case bizdev.StageV05_Feasibility:
|
|
actions = append(actions,
|
|
"TRIGGER_TASK:research",
|
|
"TRIGGER_TASK:feasibility_reg",
|
|
"TRIGGER_TASK:prelim_design",
|
|
"ADVANCE_STAGE:V1.0_quoting",
|
|
)
|
|
case bizdev.StageV10_Quoting:
|
|
actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL")
|
|
default:
|
|
actions = append(actions, "ADVANCE_STAGE:V0.1_triage")
|
|
}
|
|
|
|
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 bizdev.MaturityStage, projectName string) *intent.PendingExpectation {
|
|
if projectName == "" {
|
|
projectName = "本项目"
|
|
}
|
|
|
|
var options []intent.ExpectationOption
|
|
var snippet string
|
|
|
|
switch stage {
|
|
case bizdev.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(bizdev.StageV05_Feasibility),
|
|
},
|
|
},
|
|
{
|
|
OptionID: "2",
|
|
Title: "补充或微调事实信息",
|
|
Description: "随时告知我客户的最新的预算、目标市场或使用场景",
|
|
Intent: intent.IntentSupplementInfo,
|
|
},
|
|
{
|
|
OptionID: "3",
|
|
Title: "查阅当前立项成熟度与对外沟通建议",
|
|
Description: "获取进展研判与对外商务沟通话术",
|
|
Intent: intent.IntentQueryProgress,
|
|
Slots: map[string]string{
|
|
intent.SlotQueryTopic: "all",
|
|
},
|
|
},
|
|
}
|
|
|
|
case bizdev.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(bizdev.StageV10_Quoting),
|
|
},
|
|
},
|
|
{
|
|
OptionID: "2",
|
|
Title: "微调设计方案或技术参数",
|
|
Description: "进一步指定元器件品牌、特殊工艺或交付节点",
|
|
Intent: intent.IntentSupplementInfo,
|
|
},
|
|
{
|
|
OptionID: "3",
|
|
Title: "审阅初步系统架构图与法规分类论证说明",
|
|
Description: "查看技术路线比选报告与检验标准分析",
|
|
Intent: intent.IntentQueryProgress,
|
|
Slots: map[string]string{
|
|
intent.SlotQueryTopic: "progress",
|
|
},
|
|
},
|
|
}
|
|
|
|
case bizdev.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(bizdev.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 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") {
|
|
ctx.Project.CurrentStage = bizdev.StageV05_Feasibility
|
|
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V1.0") {
|
|
ctx.Project.CurrentStage = bizdev.StageV10_Quoting
|
|
}
|
|
|
|
if ctx.Project.CurrentStage == "" {
|
|
ctx.Project.CurrentStage = bizdev.StageV01_Triage
|
|
}
|
|
ctx.AvailableActions = ComputeAvailableActions(ctx.Project.CurrentStage, user.ProjectRole)
|
|
|
|
// 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
|
|
}
|