refactor(squad): quote preparation moves to Core via TaskPreparer hook
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:
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user