diff --git a/cmd/bizdev-cli/main.go b/cmd/bizdev-cli/main.go index f43e52f2..fc6d4624 100644 --- a/cmd/bizdev-cli/main.go +++ b/cmd/bizdev-cli/main.go @@ -41,6 +41,10 @@ func main() { os.Exit(1) } + if !res.Success { + fmt.Fprintf(os.Stderr, "警告: 后端执行未完全成功 [%s]: %s\n", res.TaskKey, res.ErrorReason) + } + fmt.Printf("=== 任务执行成功 [%s] ===\n", res.TaskKey) fmt.Printf("耗时: %v\n", res.Duration) fmt.Printf("工作区: %s\n", res.WorkDir) diff --git a/go.mod b/go.mod index a2995d40..5b7456b8 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,13 @@ module chat-agent-system go 1.27.1 +require github.com/xuri/excelize/v2 v2.11.0 + require ( github.com/richardlehane/mscfb v1.0.7 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/xuri/efp v0.0.1 // indirect - github.com/xuri/excelize/v2 v2.11.0 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/net v0.56.0 // indirect diff --git a/go.sum b/go.sum index 44cf10f8..3ea667a0 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= @@ -12,7 +18,11 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBL github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/agent/cursor.go b/internal/agent/cursor.go index 2edbc224..809c52f0 100644 --- a/internal/agent/cursor.go +++ b/internal/agent/cursor.go @@ -24,11 +24,12 @@ func (c *CursorCLIBackend) Execute(ctx context.Context, req Request) (*Result, e } payload, _ := json.Marshal(map[string]any{ - "workspace": req.WorkDir, - "api_key": c.cfg.CursorAPIKey, - "model": c.cfg.CursorModel, - "prompt": req.Prompt, - "targets": req.Targets, + "workspace": req.WorkDir, + "api_key": c.cfg.CursorAPIKey, + "model": c.cfg.CursorModel, + "prompt": req.Prompt, + "instructions": req.Instructions, + "targets": req.Targets, }) bridgeBin := c.cfg.CursorBridgeBin diff --git a/internal/agent/piagent.go b/internal/agent/piagent.go index 752bfbdd..0ee03b61 100644 --- a/internal/agent/piagent.go +++ b/internal/agent/piagent.go @@ -24,11 +24,12 @@ func (p *PiAgentBackend) Execute(ctx context.Context, req Request) (*Result, err } payload, _ := json.Marshal(map[string]any{ - "workspace": req.WorkDir, - "model": p.cfg.PiAgentModel, - "provider": p.cfg.PiAgentProvider, - "prompt": req.Prompt, - "targets": req.Targets, + "workspace": req.WorkDir, + "model": p.cfg.PiAgentModel, + "provider": p.cfg.PiAgentProvider, + "prompt": req.Prompt, + "instructions": req.Instructions, + "targets": req.Targets, }) bridgeBin := p.cfg.PiAgentBridgeBin @@ -51,6 +52,7 @@ func (p *PiAgentBackend) Execute(ctx context.Context, req Request) (*Result, err "PI_AGENT_BIN="+p.cfg.PiAgentBin, "PI_AGENT_MODEL="+p.cfg.PiAgentModel, "PI_AGENT_PROVIDER="+p.cfg.PiAgentProvider, + "PI_AGENT_SYSTEM_PROMPT_FILE=AGENTS.md", ) var stdout, stderr bytes.Buffer diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 6e372094..bacbdd04 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -69,6 +69,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun return nil, err } + snapshotTs := time.Now().UnixNano() vars := map[string]string{ "project_name": req.ProjectName, "product_name": req.ProjectName, @@ -77,6 +78,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun "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 @@ -86,6 +88,9 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun 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) @@ -111,6 +116,17 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun 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"]) @@ -147,13 +163,13 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun }) } - return &TaskRunResult{ - TaskKey: req.TaskKey, - ArtifactTitle: taskArtifactTitle(req.TaskKey), - MarkdownBody: markdownBody, - Attachments: attachments, - WorkDir: workDir, - BackendUsed: backendName, - Duration: time.Since(start), - }, nil + 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) + + return res, nil } diff --git a/internal/bizdev/types.go b/internal/bizdev/types.go index f27dff77..aec6f4ab 100644 --- a/internal/bizdev/types.go +++ b/internal/bizdev/types.go @@ -46,6 +46,8 @@ type TaskRunResult struct { WorkDir string `json:"work_dir"` BackendUsed string `json:"backend_used"` Duration time.Duration `json:"duration"` + Success bool `json:"success"` + ErrorReason string `json:"error_reason"` } type BizdevService interface { diff --git a/internal/quote/quote.go b/internal/quote/quote.go index 2f8e91c5..60711131 100644 --- a/internal/quote/quote.go +++ b/internal/quote/quote.go @@ -80,6 +80,15 @@ func CalculateAndFill(in Input, templateXlsx string, outPath string) (*Summary, _ = f.SetCellValue(sheet, "B4", in.ProductName) _ = f.SetCellValue(sheet, "B5", totalPrice) + _ = f.SetCellValue(sheet, "A7", "报价代码") + _ = f.SetCellValue(sheet, "B7", "服务阶段与内容") + _ = f.SetCellValue(sheet, "D7", "人年(PM)") + _ = f.SetCellValue(sheet, "E7", "基础成本(元)") + _ = f.SetCellValue(sheet, "F7", "缓冲金(20%)") + _ = f.SetCellValue(sheet, "G7", "毛利(40%)") + _ = f.SetCellValue(sheet, "H7", "税费(7%)") + _ = f.SetCellValue(sheet, "J7", "综合报价(元)") + writeSummaryLine(f, sheet, 8, summary.Lines[0]) writeSummaryLine(f, sheet, 9, summary.Lines[1]) writeSummaryLine(f, sheet, 10, summary.Lines[2]) @@ -93,6 +102,8 @@ func CalculateAndFill(in Input, templateXlsx string, outPath string) (*Summary, totalTax += line.Tax } writeSummaryLine(f, sheet, 18, SummaryLine{ + Code: "总计", + Name: "全阶段综合报价", PersonYear: totalPersonYear, Cost: totalCost, Buffer: totalBuffer, @@ -113,6 +124,8 @@ func CalculateAndFill(in Input, templateXlsx string, outPath string) (*Summary, } func writeSummaryLine(f *excelize.File, sheet string, row int, line SummaryLine) { + _ = f.SetCellValue(sheet, fmt.Sprintf("A%d", row), line.Code) + _ = f.SetCellValue(sheet, fmt.Sprintf("B%d", row), line.Name) _ = f.SetCellValue(sheet, fmt.Sprintf("D%d", row), line.PersonYear) _ = f.SetCellValue(sheet, fmt.Sprintf("E%d", row), line.Cost) _ = f.SetCellValue(sheet, fmt.Sprintf("F%d", row), line.Buffer) diff --git a/internal/quote/quote_test.go b/internal/quote/quote_test.go index 1c7e6101..d6ae43c6 100644 --- a/internal/quote/quote_test.go +++ b/internal/quote/quote_test.go @@ -6,6 +6,7 @@ import ( "testing" "chat-agent-system/internal/quote" + "github.com/xuri/excelize/v2" ) func TestCDOQuoteGeneration(t *testing.T) { @@ -39,4 +40,27 @@ func TestCDOQuoteGeneration(t *testing.T) { if len(summary.Notes) != 7 { t.Errorf("expected 7 statutory notes, got %d", len(summary.Notes)) } + + xf, err := excelize.OpenFile(outXlsx) + if err != nil { + t.Fatalf("failed to reopen output xlsx: %v", err) + } + defer xf.Close() + + sheet := "报价模型" + assertCell := func(cell, want string) { + got, err := xf.GetCellValue(sheet, cell) + if err != nil { + t.Fatalf("failed to read cell %s: %v", cell, err) + } + if got != want { + t.Errorf("cell %s: expected %q, got %q", cell, want, got) + } + } + + assertCell("A7", "报价代码") + assertCell("A8", "ID2") + assertCell("B8", "产品规划") + assertCell("A18", "总计") + assertCell("B18", "全阶段综合报价") } diff --git a/internal/template/render.go b/internal/template/render.go index 297449b9..420876b4 100644 --- a/internal/template/render.go +++ b/internal/template/render.go @@ -1,14 +1,21 @@ package template import ( + "regexp" "strings" ) +var unresolvedPlaceholderPattern = regexp.MustCompile(`\{\{[a-zA-Z0-9_]+\}\}`) + func RenderDoc(tmpl string, vars map[string]string) string { res := tmpl for k, v := range vars { placeholder := "{{" + k + "}}" res = strings.ReplaceAll(res, placeholder, v) } + // Any variable that wasn't provided leaves a raw {{placeholder}} in the + // output. Replace those with a clear "待确认" marker instead of leaking + // template syntax into delivered documents. + res = unresolvedPlaceholderPattern.ReplaceAllString(res, "待确认") return res } diff --git a/tools/cursor-bridge/index.mjs b/tools/cursor-bridge/index.mjs index 5f3bfb7d..2ac0fff8 100755 --- a/tools/cursor-bridge/index.mjs +++ b/tools/cursor-bridge/index.mjs @@ -8,6 +8,7 @@ * "api_key": "...", * "model": "auto", * "prompt": "...", + * "instructions": "...", * "targets": ["docs/RD-UN.md"] * } * @@ -81,6 +82,7 @@ async function main() { const apiKey = sanitizeApiKey(req.api_key || process.env.CURSOR_API_KEY || ""); const model = (req.model || process.env.CURSOR_MODEL || "auto").trim(); const prompt = req.prompt || ""; + const instructions = req.instructions || ""; const targets = Array.isArray(req.targets) ? req.targets : []; if (!workspace || !existsSync(workspace)) { @@ -106,10 +108,12 @@ async function main() { return; } + const fullPrompt = instructions ? `${instructions}\n\n---\n\n${prompt}` : prompt; + let agentId = ""; let runId = ""; try { - const result = await Agent.prompt(prompt, { + const result = await Agent.prompt(fullPrompt, { apiKey, model: { id: model }, local: {