Files
Agent-System/internal/squad/workspace.go
T
agent-bot 2c75dcaf69
CI / go-vet (pull_request) Successful in 13s
CI / short-tests-report (pull_request) Failing after 35s
CI / backup-restore-drill (pull_request) Successful in 36s
CI / web (pull_request) Successful in 32s
refactor(squad): AssetProvider port replaces direct template dependency
KTD1 fifth step (behavior preserved):
- squad.AssetProvider{DocTemplate,RenderDoc,AgentInstruction,
  AgentInstructionFile} injected via Options.Assets into Service,
  WorkspaceEngine and AdaptivePlanner
- bizdev templateAssetProvider maps port to internal/template (assembly side)
- nil-provider paths degrade to the same fallbacks the old error branches
  used (placeholder template, role header) — fail-visible, no guessing
- squad -> template import eliminated; arch edge moved bizdev->template
- verified: build/vet/arch green, squad+bizdev short tests pass
2026-09-29 06:29:51 +00:00

283 lines
7.9 KiB
Go

package squad
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// WorkspacePrepareRequest 工作区准备请求
type WorkspacePrepareRequest struct {
WorkDir string
SessionID FlexSessionID
ProjectID int64
TaskKey string
StageTitle string
ProjectName string
CustomerName string
DeviceType string
IntendedUse string
TargetMarket string
Backend string
TemplateContent string
Materials []MaterialItem
KnowledgeHits []KnowledgeHit
PriorDocs []PriorDocItem
UserContext string
ProjectContext string
AgentContext string
MemoryBankFiles map[string]string
// DialoguePackage is optional layered project dialogue (digest/turns/facts).
DialoguePackage *DialoguePackage
CustomAgents []AgentSpec
WorkflowSteps []StepSpec
InitialFields map[string]string
Metadata map[string]string
}
// DialoguePackage is the squad-facing dialogue injection payload.
type DialoguePackage struct {
DigestMD string
TurnsMD string
FactsJSON []byte
ConfirmationsMD string
TurnCount int
ExcludedPrivate int
ExcludedNoAccess int
ExcludedNoise int
}
// Workspace 表示已初始化的工作区句柄
type Workspace struct {
WorkDir string
Meta WorkspaceMetadata
Squad SquadConfig
Workflow WorkflowPlan
}
// WorkspaceEngine 工作区生命周期与沙箱构建引擎
type WorkspaceEngine struct {
assets AssetProvider
}
func NewWorkspaceEngine(assets AssetProvider) *WorkspaceEngine {
return &WorkspaceEngine{assets: assets}
}
// PrepareWorkspace 初始化自包含工作区目录
func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Workspace, error) {
workDir := req.WorkDir
if workDir == "" {
ts := time.Now().UnixNano()
workDir = filepath.Join("data", "dev", "workspaces", "bizdev", fmt.Sprintf("task_%s_%d", req.TaskKey, ts))
}
if err := os.MkdirAll(workDir, 0o755); err != nil {
return nil, fmt.Errorf("create workspace root dir: %w", err)
}
now := time.Now()
wsID := fmt.Sprintf("ws_%s_%d", req.TaskKey, now.UnixNano())
if req.Metadata != nil && req.Metadata["id"] != "" {
wsID = req.Metadata["id"]
}
// 1. 初始化 workspace.json
meta := WorkspaceMetadata{
ID: wsID,
SessionID: req.SessionID,
ProjectID: req.ProjectID,
TaskKey: req.TaskKey,
StageTitle: req.StageTitle,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Status: "created",
Backend: req.Backend,
WorkDir: workDir,
CreatedAt: now,
UpdatedAt: now,
Metadata: req.Metadata,
}
if err := writeJSON(filepath.Join(workDir, "workspace.json"), meta); err != nil {
return nil, fmt.Errorf("write workspace.json: %w", err)
}
// 2. 初始化自适应小队与工作流规划
var squadCfg SquadConfig
var wf WorkflowPlan
tmplContent := req.TemplateContent
if len(req.WorkflowSteps) > 0 {
agents := req.CustomAgents
if len(agents) == 0 {
agents = DefaultSquadAgents(req.TaskKey)
}
squadCfg = SquadConfig{
SquadName: fmt.Sprintf("%s_squad", req.TaskKey),
Version: "1.0.0",
Agents: agents,
}
steps := req.WorkflowSteps
for idx := range steps {
if steps[idx].Status == "" {
steps[idx].Status = "pending"
}
}
wf = WorkflowPlan{
WorkflowID: fmt.Sprintf("wf_%s_%d", req.TaskKey, now.UnixNano()),
TaskKey: req.TaskKey,
Title: req.StageTitle,
Status: "pending",
CurrentStepIndex: 0,
Steps: steps,
}
} else {
planner := NewAdaptivePlanner(nil, e.assets)
pRes, pErr := planner.Plan(PlannerRequest{
TaskKey: req.TaskKey,
Title: req.StageTitle,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Materials: req.Materials,
KnowledgeHits: req.KnowledgeHits,
Fields: req.InitialFields,
CustomSteps: req.WorkflowSteps,
})
if pErr == nil && pRes != nil {
squadCfg = pRes.Squad
wf = pRes.Workflow
if strings.TrimSpace(tmplContent) == "" {
tmplContent = pRes.TemplateContent
}
} else {
agents := req.CustomAgents
if len(agents) == 0 {
agents = DefaultSquadAgents(req.TaskKey)
}
squadCfg = SquadConfig{
SquadName: fmt.Sprintf("%s_squad", req.TaskKey),
Version: "1.0.0",
Agents: agents,
}
steps := req.WorkflowSteps
if len(steps) == 0 {
steps = DefaultWorkflowSteps(req.TaskKey)
}
wf = WorkflowPlan{
WorkflowID: fmt.Sprintf("wf_%s_%d", req.TaskKey, now.UnixNano()),
Status: "pending",
CurrentStepIndex: 0,
Steps: steps,
}
}
}
if err := writeJSON(filepath.Join(workDir, "squad.json"), squadCfg); err != nil {
return nil, fmt.Errorf("write squad.json: %w", err)
}
if err := writeJSON(filepath.Join(workDir, "workflow.json"), wf); err != nil {
return nil, fmt.Errorf("write workflow.json: %w", err)
}
// 4. 写入自描述模板 template.md
if strings.TrimSpace(tmplContent) == "" && e.assets != nil {
var err error
tmplContent, err = e.assets.DocTemplate(req.TaskKey)
if err != nil {
tmplContent = ""
}
}
if strings.TrimSpace(tmplContent) == "" {
tmplContent = fmt.Sprintf("# %s\n\n## 1. 结论\n\n待确认\n\n## 2. 正文\n\n待确认\n", req.StageTitle)
}
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": now.Format("2006-01-02 15:04:05"),
"snapshot_id": wsID,
}
for k, v := range req.InitialFields {
vars[k] = v
}
var renderedTmpl string
if e.assets != nil {
renderedTmpl = e.assets.RenderDoc(tmplContent, vars)
} else {
renderedTmpl = tmplContent
}
if err := os.WriteFile(filepath.Join(workDir, "template.md"), []byte(renderedTmpl), 0o644); err != nil {
return nil, fmt.Errorf("write template.md: %w", err)
}
// 5. 初始化 blackboard.json
bb := BlackboardState{
Facts: make([]FactItem, 0),
Gaps: make([]GapItem, 0),
Implications: make([]ImplicationItem, 0),
Assumptions: make([]AssumptionItem, 0),
Fields: make(map[string]string),
UpdatedAt: now,
}
if req.CustomerName != "" {
bb.Fields["customer_name"] = req.CustomerName
bb.Facts = append(bb.Facts, FactItem{
ID: "fact_01",
Claim: "客户名称为 " + req.CustomerName,
Source: "用户输入",
Verified: true,
})
}
if req.ProjectName != "" {
bb.Fields["project_name"] = req.ProjectName
}
if req.DeviceType != "" {
bb.Fields["device_type"] = req.DeviceType
}
if req.IntendedUse != "" {
bb.Fields["intended_use"] = req.IntendedUse
}
if req.TargetMarket != "" {
bb.Fields["target_market"] = req.TargetMarket
}
for k, v := range req.InitialFields {
bb.Fields[k] = v
}
if err := writeJSON(filepath.Join(workDir, "blackboard.json"), bb); err != nil {
return nil, fmt.Errorf("write blackboard.json: %w", err)
}
// 6. 构造自描述上下文体系 context/
if err := e.buildContextSystem(workDir, req); err != nil {
return nil, fmt.Errorf("build context system: %w", err)
}
// 7. 创建子工作区 tasks/<sub_dir> 并注入 TASK.md 与 AGENTS.md
if err := e.buildSubWorkspaces(workDir, wf, squadCfg, renderedTmpl); err != nil {
return nil, fmt.Errorf("build sub workspaces: %w", err)
}
// 8. 创建 artifacts/ 与基础日志
_ = os.MkdirAll(filepath.Join(workDir, "artifacts"), 0o755)
logEntry := fmt.Sprintf("[%s] 工作区创建完成: ID=%s, Task=%s\n", now.Format(time.RFC3339), wsID, req.TaskKey)
_ = os.WriteFile(filepath.Join(workDir, "execution.log"), []byte(logEntry), 0o644)
return &Workspace{
WorkDir: workDir,
Meta: meta,
Squad: squadCfg,
Workflow: wf,
}, nil
}