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:
commit
0c45b86d67
29 files changed
+3789
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user