feat(service): integrate squad service and ports into bizdev domain service

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-13 02:50:18 +00:00
committed by weichen
co-authored by Cursor
parent 434ce844f8
commit 3c2d0ca94e
4 changed files with 553 additions and 266 deletions
+11 -85
View File
@@ -1,108 +1,34 @@
package bizdev
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"chat-agent-system/internal/quote"
"chat-agent-system/internal/squad"
)
// FormatMoney formats a currency amount for display in markdown (no grouping separators).
func FormatMoney(v float64) string {
if v == 0 {
return "0"
}
if v == float64(int64(v)) {
return strconv.FormatInt(int64(v), 10)
}
return strconv.FormatFloat(v, 'f', 2, 64)
return squad.FormatMoney(v)
}
// RenderCDOQuoteTable builds the pricing table from quote summary lines and total.
func RenderCDOQuoteTable(summary *quote.Summary) string {
var b strings.Builder
b.WriteString("| 报价代码 | 服务内容 | 人数 | 人年 | 成本 | 加Buffer20% | 加毛利40% | 加税7% | 设备设施等费用 | 税后价格 |\n")
b.WriteString("|---|---|---:|---:|---:|---:|---:|---:|---:|---:|\n")
var totalPersonYear, totalCost, totalBuffer, totalMargin, totalTax, totalPrice float64
for _, line := range summary.Lines {
fmt.Fprintf(&b, "| %s | %s | — | %s | %s | %s | %s | %s | — | %s |\n",
line.Code, line.Name,
FormatMoney(line.PersonYear),
FormatMoney(line.Cost),
FormatMoney(line.Buffer),
FormatMoney(line.Margin),
FormatMoney(line.Tax),
FormatMoney(line.Price),
)
totalPersonYear += line.PersonYear
totalCost += line.Cost
totalBuffer += line.Buffer
totalMargin += line.Margin
totalTax += line.Tax
totalPrice += line.Price
}
fmt.Fprintf(&b, "| | **总计** | — | **%s** | **%s** | **%s** | **%s** | **%s** | — | **%s** |\n",
FormatMoney(totalPersonYear),
FormatMoney(totalCost),
FormatMoney(totalBuffer),
FormatMoney(totalMargin),
FormatMoney(totalTax),
FormatMoney(summary.TotalPrice),
)
return b.String()
return squad.RenderCDOQuoteTable(summary)
}
// BackfillCDOQuoteMarkdown replaces placeholder sections with computed pricing from summary.
func BackfillCDOQuoteMarkdown(body string, summary *quote.Summary) string {
conclusion := fmt.Sprintf(
"按公司 CDO 定价核算模板计算,税后合计 **%s 元**(含 Buffer 20%%、毛利 40%%、税 7%%)。数字与配套 Excel「报价模型」一致。",
FormatMoney(summary.TotalPrice),
)
body = replaceSection(body, "## 结论", conclusion)
tableIntro := "表中「人年」列与 Excel 模板同一列,对应模型投入人月合计(工时/160)。\n\n"
body = replaceSection(body, "## 报价模型", tableIntro+RenderCDOQuoteTable(summary))
return body
}
func replaceSection(body, heading, content string) string {
start := strings.Index(body, heading)
if start == -1 {
return body
}
rest := body[start+len(heading):]
nextHeading := strings.Index(rest, "\n## ")
if nextHeading == -1 {
return body[:start] + heading + "\n\n" + content + "\n"
}
end := start + len(heading) + nextHeading
return body[:start] + heading + "\n\n" + content + "\n" + body[end:]
return squad.BackfillCDOQuoteMarkdown(body, summary)
}
// WriteMaterials persists uploaded material text into workspace materials_text/.
func WriteMaterials(workDir string, materials []MaterialInput) error {
if len(materials) == 0 {
return nil
}
dir := filepath.Join(workDir, "materials_text")
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create materials_text: %w", err)
}
var items []squad.MaterialItem
for _, m := range materials {
name := filepath.Base(m.Filename)
if name == "" || name == "." {
name = "material.txt"
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, m.Content, 0o644); err != nil {
return fmt.Errorf("write material %s: %w", name, err)
}
items = append(items, squad.MaterialItem{
Filename: m.Filename,
Content: m.Content,
})
}
return nil
cm := squad.NewContextManager()
return cm.InjectMaterials(workDir, items)
}
+88 -148
View File
@@ -2,174 +2,114 @@ package bizdev
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/config"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/quote"
"chat-agent-system/internal/template"
"chat-agent-system/internal/squad"
)
type Service struct {
cfg *config.Config
kb *kb.Client
cfg *config.Config
kb *kb.Client
squad *squad.Service
}
func NewService(cfg *config.Config) *Service {
return &Service{
cfg: cfg,
kb: kb.NewClient(cfg),
cfg: cfg,
kb: kb.NewClient(cfg),
squad: squad.NewService(cfg),
}
}
var SupportedTasks = []TaskMeta{
{Key: "intake", Title: "用户初始需求收集", ArtifactTitle: "用户初始需求收集表"},
{Key: "research", Title: "调研", ArtifactTitle: "调研报告"},
{Key: "feasibility_dev", Title: "设计开发可行性评估", ArtifactTitle: "设计开发可行性评估报告"},
{Key: "feasibility_reg", Title: "注册路径可行性评估", ArtifactTitle: "注册路径可行性评估报告"},
{Key: "user_requirements", Title: "用户需求表", ArtifactTitle: "用户需求表"},
{Key: "prelim_design", Title: "系统初步设计方案", ArtifactTitle: "系统初步设计方案"},
{Key: "cdo_quote", Title: "CDO报价单", ArtifactTitle: "CDO报价单(含定价核算 Excel)"},
}
var SupportedTasks = squad.SupportedTasks
func (s *Service) ListTasks() []TaskMeta {
return SupportedTasks
}
func taskArtifactTitle(taskKey string) string {
for _, t := range SupportedTasks {
if t.Key == taskKey {
return t.ArtifactTitle
}
}
return taskKey
return s.squad.ListTasks()
}
func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error) {
start := time.Now()
workDir := req.WorkDir
if workDir == "" {
ts := time.Now().UnixNano()
workDir = filepath.Join(s.cfg.DataDir, "workspaces", "bizdev", fmt.Sprintf("%s_%d", req.TaskKey, ts))
}
if err := os.MkdirAll(workDir, 0o755); err != nil {
return nil, fmt.Errorf("create workdir: %w", err)
}
if err := WriteMaterials(workDir, req.Materials); err != nil {
return nil, err
}
tmpl, err := template.GetDocTemplate(req.TaskKey)
if err != nil {
return nil, err
}
snapshotTs := time.Now().UnixNano()
vars := map[string]string{
"project_name": req.ProjectName,
"product_name": req.ProjectName,
"customer_name": req.CustomerName,
"device_type": req.DeviceType,
"intended_use": req.IntendedUse,
"target_market": req.TargetMarket,
"generated_at": time.Now().Format("2006-01-02 15:04:05"),
"snapshot_id": fmt.Sprintf("snap_%s_%d", req.TaskKey, snapshotTs),
}
for k, v := range req.Fields {
vars[k] = v
}
renderedTmpl := template.RenderDoc(tmpl, vars)
_ = os.WriteFile(filepath.Join(workDir, "template.md"), []byte(renderedTmpl), 0o644)
inst, _ := template.GetAgentInstruction("writer")
_ = os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(inst), 0o644)
// Some backends look for a different instructions filename; write both
// unconditionally so persona/role guidance reaches whichever bridge runs.
_ = os.WriteFile(filepath.Join(workDir, "INSTRUCTIONS.md"), []byte(inst), 0o644)
taskPrompt := fmt.Sprintf("# 任务执行: %s\n\n请按 template.md 的指导撰写正文并交付到 output.md。\n", req.TaskKey)
_ = os.WriteFile(filepath.Join(workDir, "TASK.md"), []byte(taskPrompt), 0o644)
backendName := req.Backend
if backendName == "" {
backendName = s.cfg.AgentBackend
}
backend := agent.NewBackend(backendName, s.cfg)
timeout := req.Timeout
if timeout == 0 {
timeout = s.cfg.AgentTimeout
}
backendRes, err := backend.Execute(ctx, agent.Request{
WorkDir: workDir,
Prompt: taskPrompt,
Instructions: inst,
Targets: []string{"output.md"},
Timeout: timeout,
})
if err != nil {
return nil, fmt.Errorf("backend execute: %w", err)
}
res := &TaskRunResult{
Success: true,
}
if backendRes != nil && !backendRes.Success {
res.Success = false
res.ErrorReason = backendRes.ErrorReason
if backendName != "stub" {
return nil, fmt.Errorf("backend execution failed: %s", backendRes.ErrorReason)
}
}
markdownBody := renderedTmpl
if backendRes != nil && len(backendRes.Patches["output.md"]) > 0 && backendName != "stub" {
markdownBody = string(backendRes.Patches["output.md"])
}
var attachments []Attachment
if req.TaskKey == "cdo_quote" {
xlsxPath := filepath.Join(workDir, "CDO报价核算-V01.xlsx")
in := quote.Input{
CustomerName: req.CustomerName,
ProductName: req.ProjectName,
Hours: map[string]float64{
"结构工程师": 320,
"硬件工程师": 320,
"软件工程师": 480,
"项目经理": 160,
},
}
summary, err := quote.CalculateAndFill(in, "templates/xlsx/cdo_quote.xlsx", xlsxPath)
if err != nil {
return nil, fmt.Errorf("cdo quote: %w", err)
}
markdownBody = BackfillCDOQuoteMarkdown(markdownBody, summary)
_ = os.WriteFile(filepath.Join(workDir, "cdo_quote.md"), []byte(markdownBody), 0o644)
data, err := os.ReadFile(xlsxPath)
if err != nil {
return nil, fmt.Errorf("read cdo quote xlsx: %w", err)
}
attachments = append(attachments, Attachment{
Filename: "CDO报价核算-V01.xlsx",
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Data: data,
// 转换 MaterialInput 到 squad.MaterialItem
var materials []squad.MaterialItem
for _, m := range req.Materials {
materials = append(materials, squad.MaterialItem{
Filename: m.Filename,
Content: m.Content,
})
}
res.TaskKey = req.TaskKey
res.ArtifactTitle = taskArtifactTitle(req.TaskKey)
res.MarkdownBody = markdownBody
res.Attachments = attachments
res.WorkDir = workDir
res.BackendUsed = backendName
res.Duration = time.Since(start)
// 自动补充知识库检索 (如针对 research 与 feasibility_reg)
hits := req.KnowledgeHits
if len(hits) == 0 && (req.TaskKey == "research" || req.TaskKey == "feasibility_reg") {
if kbHits, err := s.kb.Search(ctx, req.ProjectName+" "+req.DeviceType, 5); err == nil && len(kbHits) > 0 {
for _, kh := range kbHits {
hits = append(hits, squad.KnowledgeHit{
DocCode: kh.DocCode,
Category: kh.Category,
Title: kh.Title,
Content: kh.Content,
Source: kh.Source,
})
}
}
}
return res, nil
squadReq := squad.SquadRunRequest{
TaskKey: req.TaskKey,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Fields: req.Fields,
Materials: materials,
Backend: req.Backend,
Timeout: req.Timeout,
WorkDir: req.WorkDir,
CustomSteps: req.CustomSteps,
AutoRepair: req.AutoRepair,
KnowledgeHits: hits,
PriorDocs: req.PriorDocs,
}
res, err := s.squad.ExecuteTask(ctx, squadReq)
if err != nil {
if res != nil {
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
WorkDir: res.WorkDir,
BackendUsed: res.BackendUsed,
Duration: res.Duration,
Success: false,
ErrorReason: res.ErrorReason,
}, err
}
return nil, err
}
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
MarkdownBody: res.MarkdownBody,
ExtractedData: res.ExtractedData,
Attachments: res.Attachments,
WorkDir: res.WorkDir,
BackendUsed: res.BackendUsed,
Duration: res.Duration,
Success: res.Success,
ErrorReason: res.ErrorReason,
QualityReport: res.QualityReport,
Manifest: res.Manifest,
}, nil
}
// InspectWorkspace 查看指定工作区的当前执行状态、黑板数据与各专员产出
func (s *Service) InspectWorkspace(ctx context.Context, workDir string) (*squad.WorkspaceState, error) {
return s.squad.InspectWorkspace(ctx, workDir)
}
// ResumeOrModifyWorkspace 动态调整任务并继续执行
func (s *Service) ResumeOrModifyWorkspace(ctx context.Context, req squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error) {
return s.squad.ResumeOrModifyWorkspace(ctx, req)
}
+34 -33
View File
@@ -3,54 +3,55 @@ package bizdev
import (
"context"
"time"
"chat-agent-system/internal/squad"
)
type TaskMeta struct {
Key string `json:"key"`
Title string `json:"title"`
ArtifactTitle string `json:"artifact_title"`
Description string `json:"description"`
}
type TaskMeta = squad.TaskMeta
type MaterialInput struct {
Filename string `json:"filename"`
Content []byte `json:"content"`
}
type TaskRunRequest struct {
TaskKey string `json:"task_key"`
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"`
Materials []MaterialInput `json:"materials"`
Backend string `json:"backend"`
Timeout time.Duration `json:"timeout"`
WorkDir string `json:"work_dir"`
}
type Attachment = squad.Attachment
type Attachment struct {
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Data []byte `json:"data"`
type TaskRunRequest struct {
TaskKey string `json:"task_key"`
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"`
Materials []MaterialInput `json:"materials"`
Backend string `json:"backend"`
Timeout time.Duration `json:"timeout"`
WorkDir string `json:"work_dir"`
CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"`
AutoRepair bool `json:"auto_repair,omitempty"`
KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"`
PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"`
}
type TaskRunResult struct {
TaskKey string `json:"task_key"`
ArtifactTitle string `json:"artifact_title"`
MarkdownBody string `json:"markdown_body"`
ExtractedData map[string]string `json:"extracted_data"`
Attachments []Attachment `json:"attachments"`
WorkDir string `json:"work_dir"`
BackendUsed string `json:"backend_used"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
ErrorReason string `json:"error_reason"`
TaskKey string `json:"task_key"`
ArtifactTitle string `json:"artifact_title"`
MarkdownBody string `json:"markdown_body"`
ExtractedData map[string]string `json:"extracted_data"`
Attachments []Attachment `json:"attachments"`
WorkDir string `json:"work_dir"`
BackendUsed string `json:"backend_used"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
ErrorReason string `json:"error_reason"`
QualityReport *squad.QualityReport `json:"quality_report,omitempty"`
Manifest *squad.Manifest `json:"manifest,omitempty"`
}
type BizdevService interface {
ListTasks() []TaskMeta
ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error)
InspectWorkspace(ctx context.Context, workDir string) (*squad.WorkspaceState, error)
ResumeOrModifyWorkspace(ctx context.Context, req squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error)
}
+420
View File
@@ -0,0 +1,420 @@
package squad
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/config"
"chat-agent-system/internal/quote"
)
// TaskMeta 任务元信息
type TaskMeta struct {
Key string `json:"key"`
Title string `json:"title"`
ArtifactTitle string `json:"artifact_title"`
Description string `json:"description"`
}
// Attachment 附加产物文件 (如 Excel)
type Attachment struct {
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Data []byte `json:"data"`
}
// SquadRunRequest 运行请求
type SquadRunRequest struct {
TaskKey string `json:"task_key"`
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"`
Materials []MaterialItem `json:"materials"`
Backend string `json:"backend"`
Timeout time.Duration `json:"timeout"`
WorkDir string `json:"work_dir"`
CustomSteps []StepSpec `json:"custom_steps"`
AutoRepair bool `json:"auto_repair"`
KnowledgeHits []KnowledgeHit `json:"knowledge_hits"`
PriorDocs []PriorDocItem `json:"prior_docs"`
StepNotify func(StepEvent) `json:"-"`
}
// SquadRunResult 运行结果
type SquadRunResult struct {
TaskKey string `json:"task_key"`
ArtifactTitle string `json:"artifact_title"`
MarkdownBody string `json:"markdown_body"`
ExtractedData map[string]string `json:"extracted_data"`
Attachments []Attachment `json:"attachments"`
WorkDir string `json:"work_dir"`
BackendUsed string `json:"backend_used"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
ErrorReason string `json:"error_reason,omitempty"`
QualityReport *QualityReport `json:"quality_report,omitempty"`
Manifest *Manifest `json:"manifest,omitempty"`
}
// WorkspaceState 用于观察工作区当前完整状态
type WorkspaceState struct {
WorkDir string `json:"work_dir"`
Metadata WorkspaceMetadata `json:"metadata"`
Squad SquadConfig `json:"squad"`
Workflow WorkflowPlan `json:"workflow"`
Blackboard BlackboardState `json:"blackboard"`
DeliverableContent string `json:"deliverable_content"`
Manifest *Manifest `json:"manifest,omitempty"`
}
// ModifyWorkflowRequest 动态调整现有工作区任务
type ModifyWorkflowRequest struct {
WorkDir string `json:"work_dir"`
AddSteps []StepSpec `json:"add_steps,omitempty"`
RemoveStepIDs []string `json:"remove_step_ids,omitempty"`
UpdateSteps []StepSpec `json:"update_steps,omitempty"`
Backend string `json:"backend,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
AutoRepair bool `json:"auto_repair,omitempty"`
StepNotify func(StepEvent) `json:"-"`
}
// SquadService 小队领域服务核心接口
type SquadService interface {
ListTasks() []TaskMeta
PlanWorkflow(ctx context.Context, req SquadRunRequest) (*WorkflowPlan, error)
ExecuteWorkflow(ctx context.Context, wsDir string, backend string, opts ExecuteOptions) (*SquadRunResult, error)
ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadRunResult, error)
InspectWorkspace(ctx context.Context, workDir string) (*WorkspaceState, error)
ResumeOrModifyWorkspace(ctx context.Context, req ModifyWorkflowRequest) (*SquadRunResult, error)
}
// Service SquadService 实现
type Service struct {
cfg *config.Config
engine *WorkspaceEngine
driver *WorkflowDriver
gate *QualityGate
}
func NewService(cfg *config.Config) *Service {
engine := NewWorkspaceEngine()
return &Service{
cfg: cfg,
engine: engine,
driver: NewWorkflowDriver(engine),
gate: NewQualityGate(),
}
}
var SupportedTasks = []TaskMeta{
{Key: "intake", Title: "用户初始需求收集", ArtifactTitle: "用户初始需求收集表", Description: "梳理客户原始需求、提炼事实清单与缺口"},
{Key: "research", Title: "调研", ArtifactTitle: "调研报告", Description: "市场竞品同维度对比、法规要点与技术方案初筛"},
{Key: "feasibility_dev", Title: "设计开发可行性评估", ArtifactTitle: "设计开发可行性评估报告", Description: "工程化差距、技术路线与系统架构可行性推导"},
{Key: "feasibility_reg", Title: "注册路径可行性评估", ArtifactTitle: "注册路径可行性评估报告", Description: "分类建议、临床评价策略与主要标准检测规划"},
{Key: "user_requirements", Title: "用户需求表", ArtifactTitle: "用户需求表", Description: "用户角色画像与可追溯需求条目清单"},
{Key: "prelim_design", Title: "系统初步设计方案", ArtifactTitle: "系统初步设计方案", Description: "子系统架构边界、数据接口与Mermaid上下文流向"},
{Key: "cdo_quote", Title: "CDO报价单", ArtifactTitle: "CDO报价单(含定价核算 Excel)", Description: "基于公司定价模型的工时拆解、成本核算与法定说明"},
}
func (s *Service) ListTasks() []TaskMeta {
return SupportedTasks
}
func TaskArtifactTitle(taskKey string) string {
for _, t := range SupportedTasks {
if t.Key == taskKey {
return t.ArtifactTitle
}
}
return taskKey
}
// PlanWorkflow 动态规划生成任务工作流
func (s *Service) PlanWorkflow(ctx context.Context, req SquadRunRequest) (*WorkflowPlan, error) {
steps := req.CustomSteps
if len(steps) == 0 {
steps = DefaultWorkflowSteps(req.TaskKey)
}
now := time.Now()
return &WorkflowPlan{
WorkflowID: fmt.Sprintf("wf_%s_%d", req.TaskKey, now.UnixNano()),
Status: "pending",
CurrentStepIndex: 0,
Steps: steps,
}, nil
}
// ExecuteTask 便捷门面:一步构建工作区、调度动态工作流、进行CDO核算与终审质量门禁
func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadRunResult, error) {
start := time.Now()
workDir := req.WorkDir
if workDir == "" {
ts := time.Now().UnixNano()
workDir = filepath.Join(s.cfg.DataDir, "workspaces", "bizdev", fmt.Sprintf("%s_%d", req.TaskKey, ts))
}
backendName := req.Backend
if backendName == "" {
backendName = s.cfg.AgentBackend
}
// 1. 初始化工作区目录体系
wsReq := WorkspacePrepareRequest{
WorkDir: workDir,
TaskKey: req.TaskKey,
StageTitle: TaskArtifactTitle(req.TaskKey),
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Backend: backendName,
Materials: req.Materials,
KnowledgeHits: req.KnowledgeHits,
PriorDocs: req.PriorDocs,
WorkflowSteps: req.CustomSteps,
InitialFields: req.Fields,
}
_, err := s.engine.PrepareWorkspace(wsReq)
if err != nil {
return nil, fmt.Errorf("prepare workspace: %w", err)
}
// 2. 若为 CDO 报价任务,先执行 Excel 报价核算引擎并回填
var attachments []Attachment
if req.TaskKey == "cdo_quote" {
att, err := s.handleCDOQuotePreparation(workDir, req)
if err == nil && att != nil {
attachments = append(attachments, *att)
}
}
// 3. 执行动态工作流调度
backend := agent.NewBackend(backendName, s.cfg)
timeout := req.Timeout
if timeout <= 0 {
timeout = s.cfg.AgentTimeout
}
execOpts := ExecuteOptions{
Timeout: timeout,
StepNotify: req.StepNotify,
}
execRes, err := s.driver.Execute(ctx, workDir, backend, execOpts)
if err != nil {
return &SquadRunResult{
TaskKey: req.TaskKey,
ArtifactTitle: TaskArtifactTitle(req.TaskKey),
WorkDir: workDir,
BackendUsed: backendName,
Duration: time.Since(start),
Success: false,
ErrorReason: err.Error(),
}, err
}
// 4. 自动化质量门禁与修复 (QualityGate)
maxRetries := 0
if req.AutoRepair {
maxRetries = 2
}
report, err := s.gate.EnsureQualityOrRepair(ctx, workDir, req.TaskKey, s.driver, backend, maxRetries, execOpts)
if err != nil {
// 记录门禁错误但容错返回当前正文
report = &QualityReport{Passed: false, Violations: []string{err.Error()}}
}
manifest, _ := s.gate.GenerateManifest(workDir, req.TaskKey, *report)
// 5. 读取最终交付正文与黑板抽取数据
finalOutPath := filepath.Join(workDir, "output.md")
finalBytes, _ := os.ReadFile(finalOutPath)
if backendName == "stub" {
finalBytes, _ = os.ReadFile(filepath.Join(workDir, "template.md"))
_ = os.WriteFile(finalOutPath, finalBytes, 0o644)
}
bb, _ := s.driver.LoadBlackboard(workDir)
var extractedData map[string]string
if bb != nil {
extractedData = bb.Fields
}
return &SquadRunResult{
TaskKey: req.TaskKey,
ArtifactTitle: TaskArtifactTitle(req.TaskKey),
MarkdownBody: string(finalBytes),
ExtractedData: extractedData,
Attachments: attachments,
WorkDir: workDir,
BackendUsed: backendName,
Duration: time.Since(start),
Success: execRes.Success,
QualityReport: report,
Manifest: manifest,
}, nil
}
// ExecuteWorkflow 执行既有工作区工作流
func (s *Service) ExecuteWorkflow(ctx context.Context, wsDir string, backendName string, opts ExecuteOptions) (*SquadRunResult, error) {
start := time.Now()
if backendName == "" {
backendName = s.cfg.AgentBackend
}
backend := agent.NewBackend(backendName, s.cfg)
execRes, err := s.driver.Execute(ctx, wsDir, backend, opts)
if err != nil {
return nil, err
}
wsMeta, _ := s.loadWorkspaceMeta(wsDir)
finalBytes, _ := os.ReadFile(filepath.Join(wsDir, "output.md"))
bb, _ := s.driver.LoadBlackboard(wsDir)
return &SquadRunResult{
TaskKey: wsMeta.TaskKey,
ArtifactTitle: TaskArtifactTitle(wsMeta.TaskKey),
MarkdownBody: string(finalBytes),
ExtractedData: bb.Fields,
WorkDir: wsDir,
BackendUsed: backendName,
Duration: time.Since(start),
Success: execRes.Success,
}, nil
}
// InspectWorkspace 查看指定工作区的当前执行状态、黑板数据与各专员产出
func (s *Service) InspectWorkspace(ctx context.Context, workDir string) (*WorkspaceState, error) {
meta, err := s.loadWorkspaceMeta(workDir)
if err != nil {
return nil, fmt.Errorf("load workspace meta: %w", err)
}
squadCfg, err := s.driver.loadSquadConfig(workDir)
if err != nil {
return nil, fmt.Errorf("load squad config: %w", err)
}
wf, err := s.driver.LoadWorkflow(workDir)
if err != nil {
return nil, fmt.Errorf("load workflow: %w", err)
}
bb, err := s.driver.LoadBlackboard(workDir)
if err != nil {
return nil, fmt.Errorf("load blackboard: %w", err)
}
outPath := filepath.Join(workDir, "output.md")
data, _ := os.ReadFile(outPath)
return &WorkspaceState{
WorkDir: workDir,
Metadata: meta,
Squad: squadCfg,
Workflow: *wf,
Blackboard: *bb,
DeliverableContent: string(data),
}, nil
}
// ResumeOrModifyWorkspace 在现有工作区基础上,动态添加/重跑/修改任务并继续执行
func (s *Service) ResumeOrModifyWorkspace(ctx context.Context, req ModifyWorkflowRequest) (*SquadRunResult, error) {
for _, add := range req.AddSteps {
if err := s.driver.AddStep(req.WorkDir, add); err != nil {
return nil, fmt.Errorf("add step %s: %w", add.ID, err)
}
}
for _, removeID := range req.RemoveStepIDs {
if err := s.driver.RemoveStep(req.WorkDir, removeID); err != nil {
return nil, fmt.Errorf("remove step %s: %w", removeID, err)
}
}
for _, update := range req.UpdateSteps {
if err := s.driver.UpdateStep(req.WorkDir, update); err != nil {
return nil, fmt.Errorf("update step %s: %w", update.ID, err)
}
}
backendName := req.Backend
if backendName == "" {
backendName = s.cfg.AgentBackend
}
timeout := req.Timeout
if timeout <= 0 {
timeout = s.cfg.AgentTimeout
}
opts := ExecuteOptions{
Timeout: timeout,
StepNotify: req.StepNotify,
}
return s.ExecuteWorkflow(ctx, req.WorkDir, backendName, opts)
}
func (s *Service) handleCDOQuotePreparation(workDir string, req SquadRunRequest) (*Attachment, error) {
xlsxPath := filepath.Join(workDir, "CDO报价核算-V01.xlsx")
artifactXlsxPath := filepath.Join(workDir, "artifacts", "CDO报价核算-V01.xlsx")
in := quote.Input{
CustomerName: req.CustomerName,
ProductName: req.ProjectName,
Hours: map[string]float64{
"结构工程师": 320,
"硬件工程师": 320,
"软件工程师": 480,
"项目经理": 160,
},
}
summary, err := quote.CalculateAndFill(in, "templates/xlsx/cdo_quote.xlsx", xlsxPath)
if err != nil {
return nil, err
}
data, err := os.ReadFile(xlsxPath)
if err != nil {
return nil, err
}
_ = os.WriteFile(artifactXlsxPath, data, 0o644)
// 回填 template.md 中的报价数据
tmplPath := filepath.Join(workDir, "template.md")
if tmplBytes, err := os.ReadFile(tmplPath); err == nil {
filled := BackfillCDOQuoteMarkdown(string(tmplBytes), summary)
_ = os.WriteFile(tmplPath, []byte(filled), 0o644)
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte(filled), 0o644)
}
return &Attachment{
Filename: "CDO报价核算-V01.xlsx",
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Data: data,
}, nil
}
func (s *Service) loadWorkspaceMeta(wsDir string) (WorkspaceMetadata, error) {
p := filepath.Join(wsDir, "workspace.json")
var m WorkspaceMetadata
data, err := os.ReadFile(p)
if err != nil {
return m, err
}
err = json.Unmarshal(data, &m)
return m, err
}