Files
eightbf-analysis-go/internal/service/demo_worker.go
T
gozerosandCursor 0c45b86d67 chore: 初始化 eightbf-analysis-go(对齐 Java 会商作业服务)
Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-03 00:13:46 +08:00

278 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}