chore: 初始化 eightbf-analysis-go(对齐 Java 会商作业服务)

Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gozerosandCursor committed 2026-11-03 00:13:46 +08:00
commit 0c45b86d67
29 files changed
+3789

No files matched your search

+277
View File
@@ -0,0 +1,277 @@
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
}