refactor(squad): quote preparation moves to Core via TaskPreparer hook
CI / go-vet (pull_request) Successful in 14s
CI / short-tests-report (pull_request) Failing after 35s
CI / backup-restore-drill (pull_request) Successful in 33s
CI / web (pull_request) Successful in 32s

KTD1 fourth step:
- squad.Options.TaskPreparer + TaskPreparation (Attachment + Backfills +
  MirrorTemplateToOutput); squad stays sole writer of its workspace files
- CDO quote calc/xlsx/backfill implementation moves to bizdev
  (quote_preparer.go); squad/quote_backfill.go relocated to bizdev
- arch path rule enforced the boundary: bizdev may not name/write
  output.md; mirroring decided squad-side via flag
- squad -> quote import eliminated; engine.go delegations now local
- verified: build/vet/arch green, squad+bizdev short tests pass
This commit is contained in:
2026-09-29 06:16:19 +00:00
parent f034a80c09
commit 8c2dd49e80
6 changed files with 114 additions and 71 deletions
@@ -287,7 +287,6 @@ imports:
squad:
- agent
- config
- quote
- stream
- template
- textextract
-16
View File
@@ -1,25 +1,9 @@
package bizdev
import (
"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 {
return squad.FormatMoney(v)
}
// RenderCDOQuoteTable builds the pricing table from quote summary lines and total.
func RenderCDOQuoteTable(summary *quote.Summary) string {
return squad.RenderCDOQuoteTable(summary)
}
// BackfillCDOQuoteMarkdown replaces placeholder sections with computed pricing from summary.
func BackfillCDOQuoteMarkdown(body string, summary *quote.Summary) string {
return squad.BackfillCDOQuoteMarkdown(body, summary)
}
// WriteMaterials persists uploaded material text into workspace materials_text/.
func WriteMaterials(workDir string, materials []MaterialInput) error {
var items []squad.MaterialItem
@@ -1,4 +1,4 @@
package squad
package bizdev
import (
"fmt"
+74
View File
@@ -0,0 +1,74 @@
package bizdev
import (
"os"
"path/filepath"
"chat-agent-system/internal/quote"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/workflowdef"
)
// handleCDOQuotePreparation 为 CDO 报价任务执行 Excel 核算并生成回填内容
// (KTD1:报价领域规则、核算与 xlsx 资产归 Core;只计算,不写 squad 工作区,
// 文件由 squad 经 TaskPreparation.Backfills 统一写入)。
func (s *Service) handleCDOQuotePreparation(workDir string, req squad.SquadRunRequest) (*squad.TaskPreparation, error) {
in := quote.Input{
CustomerName: req.CustomerName,
ProductName: req.ProjectName,
Hours: map[string]float64{
"结构工程师": 320,
"硬件工程师": 320,
"软件工程师": 480,
"项目经理": 160,
},
}
// 核算写入临时文件后读回字节;工作区文件由 squad 写入。
tmp, err := os.CreateTemp("", "cdo-quote-*.xlsx")
if err != nil {
return nil, err
}
tmpPath := tmp.Name()
tmp.Close()
defer os.Remove(tmpPath)
summary, err := quote.CalculateAndFill(in, "templates/xlsx/cdo_quote.xlsx", tmpPath)
if err != nil {
return nil, err
}
data, err := os.ReadFile(tmpPath)
if err != nil {
return nil, err
}
backfills := map[string][]byte{
"CDO报价核算-V01.xlsx": data,
"artifacts/CDO报价核算-V01.xlsx": data,
}
// 回填 template.md 报价占位(模板存在时);output.md 镜像由 squad 决定
tmplBytes, tmplErr := os.ReadFile(filepath.Join(workDir, "template.md"))
mirror := false
if tmplErr == nil {
filled := BackfillCDOQuoteMarkdown(string(tmplBytes), summary)
backfills["template.md"] = []byte(filled)
mirror = true
}
return &squad.TaskPreparation{
Attachment: &squad.Attachment{
Filename: "CDO报价核算-V01.xlsx",
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Data: data,
},
Backfills: backfills,
MirrorTemplateToOutput: mirror,
}, nil
}
// squadTaskPreparer 仅对 CDO 报价任务触发领域准备,其余任务快速跳过。
func (s *Service) squadTaskPreparer(workDir string, req squad.SquadRunRequest) (*squad.TaskPreparation, error) {
if !workflowdef.SameTaskKey(req.TaskKey, "cdo_quote") {
return nil, nil
}
return s.handleCDOQuotePreparation(workDir, req)
}
+9 -8
View File
@@ -193,14 +193,6 @@ func NewService(cfg *config.Config) (*Service, error) {
svc := &Service{
cfg: cfg,
kb: kb.NewClient(cfg),
squad: squad.NewService(squad.Options{
BackendName: cfg.AgentBackend,
BackendTimeout: cfg.AgentTimeout,
DataDir: cfg.DataDir,
NewBackend: func(name string) agent.Backend {
return agent.NewBackend(name, cfg)
},
}),
ctxMgr: ctxMgr,
cas: casStore,
materialIndex: matIndex,
@@ -211,6 +203,15 @@ func NewService(cfg *config.Config) (*Service, error) {
pool: pool,
schemaVersion: schemaVersion,
}
svc.squad = squad.NewService(squad.Options{
BackendName: cfg.AgentBackend,
BackendTimeout: cfg.AgentTimeout,
DataDir: cfg.DataDir,
NewBackend: func(name string) agent.Backend {
return agent.NewBackend(name, cfg)
},
TaskPreparer: svc.squadTaskPreparer,
})
svc.parts = Parts{
Pool: pool,
Gov: docGov,
+30 -45
View File
@@ -10,7 +10,6 @@ import (
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/quote"
"chat-agent-system/internal/workflowdef"
)
@@ -119,6 +118,20 @@ type Options struct {
// NewBackend 由调用方注入的后端工厂(Core 装配:凭证、环境、并发上限
// 留在产品侧)。nil 时返回错误,不静默降级 Stub。
NewBackend func(name string) agent.Backend
// TaskPreparer 为可选的任务准备钩子(KTD1:报价领域规则、核算与 xlsx
// 资产归 Core;Core 只做计算并返回产物,工作区文件由 squad 统一写入)。
// nil 或返回 nil 表示无预处理。
TaskPreparer func(workDir string, req SquadRunRequest) (*TaskPreparation, error)
}
// TaskPreparation 是领域准备钩子的产物:附件与工作区内回写内容。
// 路径均为工作区相对路径;物理写入由 squad 执行(工作区单写者)。
type TaskPreparation struct {
Attachment *Attachment
Backfills map[string][]byte
// MirrorTemplateToOutput 为 true 时,template.md 回填内容同时镜像到
// output.md(工作区产物镜像语义归 squad,Core 不直接命名 output.md)。
MirrorTemplateToOutput bool
}
func (o Options) withDefaults() Options {
@@ -265,12 +278,23 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
return nil, fmt.Errorf("prepare workspace: %w", err)
}
// 2. 若为 CDO 报价任务,先执行 Excel 报价核算引擎并回填
// 2. 任务领域预处理(如 CDO 报价核算+xlsx 回填):Core 计算,squad 写入
var attachments []Attachment
if workflowdef.SameTaskKey(req.TaskKey, "cdo_quote") {
att, err := s.handleCDOQuotePreparation(workDir, req)
if err == nil && att != nil {
attachments = append(attachments, *att)
if s.opts.TaskPreparer != nil {
prep, err := s.opts.TaskPreparer(workDir, req)
if err == nil && prep != nil {
for rel, content := range prep.Backfills {
_ = os.MkdirAll(filepath.Dir(filepath.Join(workDir, rel)), 0o755)
_ = os.WriteFile(filepath.Join(workDir, rel), content, 0o644)
}
if prep.MirrorTemplateToOutput {
if filled, ok := prep.Backfills["template.md"]; ok {
_ = os.WriteFile(filepath.Join(workDir, "output.md"), filled, 0o644)
}
}
if prep.Attachment != nil {
attachments = append(attachments, *prep.Attachment)
}
}
}
@@ -535,45 +559,6 @@ func (s *Service) ResumeOrModifyWorkspace(ctx context.Context, req ModifyWorkflo
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")