package service import ( "log" "strconv" "time" "git.gozeros.com/wsk/eightbf-analysis-go/internal/domain" "git.gozeros.com/wsk/eightbf-analysis-go/internal/repo" ) // DemoWorker 假进度推进(仅 ANALYSIS_MODE=demo)。 type DemoWorker struct { Repo *repo.JobRepo Every time.Duration } var demoLines = []string{ "【演示】我先说观点:当前应先收窄目标,再谈方案细节。", "【演示】节奏上建议短周期验证,避免一次铺太开。", "【演示】依赖和数据口径要尽早对齐,否则后面容易返工。", "【演示】成本先压到能证明价值的最小投入。", "【演示】主要风险是范围蔓延和决策偏慢,需要明确拍板人。", "【演示】我同意形成一版可执行共识,可以进入总结。", } const charsPerTick = 8 func (w *DemoWorker) Start() { if w.Every <= 0 { w.Every = 400 * time.Millisecond } go func() { t := time.NewTicker(w.Every) defer t.Stop() for range t.C { list, err := w.Repo.FindActiveJobs() if err != nil { continue } for _, job := range list { if err := w.advance(job); err != nil { log.Printf("demo worker advance failed jobId=%s: %v", job.ID, err) job.Status = domain.StatusError job.Phase = "error" job.Error = err.Error() _ = w.Repo.UpdateProgress(job) } } } }() } func (w *DemoWorker) advance(job *domain.AnalysisJob) error { if ok, _ := w.Repo.IsCancelFlag(job.ID); ok { finishCurrentBubble(job, "cancelled") job.Status = domain.StatusCancelled job.Phase = "cancelled" job.Error = "cancelled" p := job.EnsureProgress() p["message"] = "cancelled" return w.Repo.UpdateProgress(job) } if job.Status == domain.StatusAwaitingInput { return nil } agents := job.Agents if agents == nil { agents = []map[string]interface{}{} } agentCount := len(agents) if agentCount < 1 { agentCount = 1 } dialogues := job.EnsureDialogues() progress := job.EnsureProgress() if job.Status == domain.StatusQueued { job.Status = domain.StatusRunning job.Phase = "analyzing" job.Cycle = 1 progress["percent"] = 5 progress["message"] = "席位开始发言" progress["lineIndex"] = 0 progress["charIndex"] = 0 applyAgentStatus(agents, 0, "speaking") job.ActiveAgentID = agentIDAt(agents, 0) job.Agents = agents openDemoBubble(job, dialogues, agents, 0, "席位输出中…") // openDemoBubble 已写回 job.Dialogues;切勿再用旧 dialogues 切片盖掉新气泡 return w.Repo.UpdateProgress(job) } lineIndex := toIntVal(progress["lineIndex"], 0) charIndex := toIntVal(progress["charIndex"], 0) if lineIndex < len(demoLines) { speakerIdx := lineIndex % agentCount wantAgent := agentIDAt(agents, speakerIdx) if job.ActiveAgentID != wantAgent || (charIndex == 0 && !hasStreaming(dialogues)) { if charIndex == 0 { finishCurrentBubble(job, "") for i := range agents { st := "listening" if i == speakerIdx { st = "speaking" } applyAgentStatus(agents, i, st) } job.ActiveAgentID = wantAgent openDemoBubble(job, dialogues, agents, speakerIdx, "席位输出中…") dialogues = job.Dialogues } } line := demoLines[lineIndex] end := charIndex + charsPerTick if end > len([]rune(line)) { end = len([]rune(line)) } rs := []rune(line) if end > charIndex { appendStreaming(dialogues, string(rs[charIndex:end])) charIndex = end } if charIndex >= len(rs) { appendStreaming(dialogues, "\n\n") finishCurrentBubble(job, "") lineIndex++ charIndex = 0 } totalChars := 0 for _, l := range demoLines { totalChars += len([]rune(l)) } doneChars := 0 for i := 0; i < lineIndex && i < len(demoLines); i++ { doneChars += len([]rune(demoLines[i])) } doneChars += charIndex percent := 5 + (doneChars*85)/max(totalChars, 1) if percent > 90 { percent = 90 } progress["lineIndex"] = lineIndex progress["charIndex"] = charIndex progress["percent"] = percent progress["message"] = "发言中 " + itoa(percent) + "%" job.Cycle = 1 + lineIndex/max(agentCount, 1) job.ConsensusPercentage = min(95, percent) job.Agents = agents job.Dialogues = dialogues return w.Repo.UpdateProgress(job) } finishCurrentBubble(job, "") summary := "【演示总结】以上均为占位发言,用于验证「关浏览器后任务仍继续 + 页面流式展示」。" + "接入真实模型后,这里会换成总控根据各席发言生成的真正总结。" + "(job=" + job.ID + ")" dialogues = append(dialogues, map[string]interface{}{ "id": "dlg_" + job.ID + "_leader", "role": "leader", "sender": "总控协调", "agentId": "coordinator", "content": summary, "status": "done", "phaseLabel": "全案盖印", }) for i := range agents { applyAgentStatus(agents, i, "done") } job.ActiveAgentID = "" job.Agents = agents job.Status = domain.StatusDone job.Phase = "done" job.ConsensusPercentage = 100 progress["percent"] = 100 progress["message"] = "completed" job.Dialogues = dialogues job.Result = map[string]interface{}{ "summary": summary, "consensusPercentage": 100, "recommendations": []string{"确认产品边界与里程碑", "补充关键风险与资源假设", "安排下一轮执行评审"}, } return w.Repo.UpdateProgress(job) } func openDemoBubble(job *domain.AnalysisJob, dialogues, agents []map[string]interface{}, idx int, phase string) { bubble := map[string]interface{}{ "id": "dlg_" + job.ID + "_" + itoa(len(dialogues)+1), "role": "special", "sender": agentNameAt(agents, idx), "agentId": agentIDAt(agents, idx), "content": "", "status": "streaming", "phaseLabel": phase, } job.Dialogues = append(dialogues, bubble) } func finishCurrentBubble(job *domain.AnalysisJob, override string) { for i := len(job.Dialogues) - 1; i >= 0; i-- { b := job.Dialogues[i] if asStr(b["status"]) == "streaming" { st := "done" if override != "" { st = override } b["status"] = st b["phaseLabel"] = "" return } } } func appendStreaming(dialogues []map[string]interface{}, chunk string) { for i := len(dialogues) - 1; i >= 0; i-- { b := dialogues[i] if asStr(b["status"]) == "streaming" { b["content"] = asStr(b["content"]) + chunk return } } } func hasStreaming(dialogues []map[string]interface{}) bool { for i := len(dialogues) - 1; i >= 0; i-- { if asStr(dialogues[i]["status"]) == "streaming" { return true } } return false } func agentIDAt(agents []map[string]interface{}, idx int) string { if len(agents) == 0 { return "" } return asStr(agents[idx%len(agents)]["id"]) } func agentNameAt(agents []map[string]interface{}, idx int) string { if len(agents) == 0 { return "席位" } a := agents[idx%len(agents)] if n := asStr(a["name"]); n != "" { return n } return asStr(a["id"]) } func applyAgentStatus(agents []map[string]interface{}, idx int, status string) { if idx < 0 || idx >= len(agents) { return } agents[idx]["status"] = status } func toIntVal(v interface{}, def int) int { switch t := v.(type) { case int: return t case int64: return int(t) case float64: return int(t) default: return def } } func itoa(n int) string { return strconv.Itoa(n) } func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b }