Files
weichen dc29b84cdf feat(ctx): inject cleaned project dialogue into squad and assistant
Add dialoguectx packaging, conflictresolve adjudication with HITL routing,
workspace context/dialogue/ injection (default on), and goose 00071.
2026-09-24 17:04:04 +00:00

226 lines
6.4 KiB
Go
Raw Permalink 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 dialoguectx
import (
"encoding/json"
"fmt"
"strings"
"time"
"unicode/utf8"
)
func renderTurnsMD(turns []RichDialogueTurn, from, to time.Time) string {
var sb strings.Builder
sb.WriteString("# 项目成员对话近窗(带元数据原文)\n\n")
if len(turns) == 0 {
sb.WriteString("本窗口内无授权可见的共享对话。\n")
return sb.String()
}
sb.WriteString(fmt.Sprintf("- 覆盖窗口: %s → %s\n", from.Format("2006-01-02 15:04"), to.Format("2006-01-02 15:04")))
sb.WriteString(fmt.Sprintf("- 条数: %d\n\n", len(turns)))
for _, t := range turns {
roleLabel := "用户"
if t.Role == "assistant" {
roleLabel = "助手"
}
identity := t.SpeakerName
parts := make([]string, 0, 3)
if t.SystemRole != "" {
parts = append(parts, t.SystemRole)
}
if t.ProjectRole != "" {
parts = append(parts, t.ProjectRole)
}
if t.DocDuty != "" {
parts = append(parts, "duty:"+t.DocDuty)
}
if len(parts) > 0 {
identity = fmt.Sprintf("%s(%s)", t.SpeakerName, strings.Join(parts, "/"))
}
stage := ""
if t.StageKey != "" {
stage = " stage=" + t.StageKey
}
sb.WriteString(fmt.Sprintf("[%s] %s %s:%s %s\n",
t.CreatedAt.Format("2006-01-02 15:04"), identity, roleLabel, stage, t.Content))
}
return sb.String()
}
func renderDigestMD(turns []RichDialogueTurn, pkg *Package) string {
var sb strings.Builder
sb.WriteString("# 项目对话纪要\n\n")
sb.WriteString("权威度:`collab_dialogue`(低于黑板事实与已批准文档;冲突时不得静默采信对话版本)\n\n")
if pkg != nil {
sb.WriteString(fmt.Sprintf("- 装配时间: %s\n", pkg.AssembledAt.Format(time.RFC3339)))
sb.WriteString(fmt.Sprintf("- 纳入轮次: %d(私有排除 %d / 阶段不可见 %d / 噪声 %d)\n",
pkg.TurnCount, pkg.ExcludedPrivate, pkg.ExcludedNoAccess, pkg.ExcludedNoise))
if !pkg.CoveredFrom.IsZero() {
sb.WriteString(fmt.Sprintf("- 覆盖窗口: %s → %s\n",
pkg.CoveredFrom.Format("2006-01-02"), pkg.CoveredTo.Format("2006-01-02")))
}
sb.WriteString("\n")
}
if len(turns) == 0 {
sb.WriteString("暂无可用共享对话纪要。\n")
return sb.String()
}
// Day buckets: take first user utterance snippet per day as headline proxy.
byDay := map[string][]string{}
dayOrder := []string{}
for _, t := range turns {
day := t.DayKey
if day == "" {
day = t.CreatedAt.Format("2006-01-02")
}
if _, ok := byDay[day]; !ok {
dayOrder = append(dayOrder, day)
}
if t.Role != "user" {
continue
}
if len(byDay[day]) >= 3 {
continue
}
byDay[day] = append(byDay[day], fmt.Sprintf("%s: %s", t.SpeakerName, truncateRunes(t.Content, 64)))
}
sb.WriteString("## 按日要点\n\n")
for _, day := range dayOrder {
pts := byDay[day]
if len(pts) == 0 {
sb.WriteString(fmt.Sprintf("- **%s**: (仅助手/系统可见消息)\n", day))
continue
}
sb.WriteString(fmt.Sprintf("- **%s**: %s\n", day, strings.Join(pts, ";")))
}
out := sb.String()
if utf8.RuneCountInString(out) > MaxDigestRunes {
out = truncateRunes(out, MaxDigestRunes)
}
return out
}
// Known blackboard-style keys we try to extract from user utterances.
var factKeyPatterns = []struct {
Key string
Markers []string
}{
{"device_type", []string{"器械类型", "产品类型", "device_type", "设备类型"}},
{"target_market", []string{"目标市场", "target_market", "市场定位"}},
{"intended_use", []string{"预期用途", "intended_use", "适用场景"}},
{"customer_name", []string{"客户名称", "客户是", "customer_name"}},
{"budget", []string{"预算", "budget"}},
{"classification", []string{"注册分类", "管理类别", "分类界定"}},
}
func extractFacts(turns []RichDialogueTurn) []DialogueFact {
out := make([]DialogueFact, 0)
seen := map[string]struct{}{}
for _, t := range turns {
if t.Role != "user" {
continue
}
content := strings.TrimSpace(t.Content)
for _, pat := range factKeyPatterns {
val, ok := extractAfterMarker(content, pat.Markers)
if !ok {
continue
}
dedupe := pat.Key + "=" + val
if _, exists := seen[dedupe]; exists {
continue
}
seen[dedupe] = struct{}{}
out = append(out, DialogueFact{
Key: pat.Key,
Value: val,
SourceMessageID: t.MessageID,
SpeakerID: t.SpeakerID,
SpeakerName: t.SpeakerName,
StatedAt: t.CreatedAt,
Authority: AuthorityCollabDialogue,
ExplicitEvidence: strings.Contains(content, "材料") || strings.Contains(content, "〔") || strings.Contains(content, "决定"),
})
}
}
return out
}
func extractAfterMarker(content string, markers []string) (string, bool) {
for _, m := range markers {
idx := strings.Index(content, m)
if idx < 0 {
continue
}
rest := strings.TrimSpace(content[idx+len(m):])
rest = strings.TrimLeft(rest, "::=是为 ")
rest = strings.TrimSpace(rest)
if rest == "" {
continue
}
// Cut at sentence boundary.
for _, sep := range []string{"。", ";", ";", "\n", ",且", "并"} {
if i := strings.Index(rest, sep); i > 0 {
rest = rest[:i]
break
}
}
rest = truncateRunes(strings.TrimSpace(rest), 80)
if rest == "" {
continue
}
return rest, true
}
return "", false
}
// FactsJSON returns pretty-printed facts payload.
func FactsJSON(facts []DialogueFact) []byte {
if facts == nil {
facts = []DialogueFact{}
}
b, _ := json.MarshalIndent(facts, "", " ")
return b
}
// RenderAssistantSnippet builds a short MemoryComposer fragment (≤400 runes).
func RenderAssistantSnippet(pkg *Package) string {
if pkg == nil || pkg.TurnCount == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("【项目成员协同近况】(权威度低于黑板事实;冲突时以黑板为准)\n")
// First ~3 digest day lines.
lines := strings.Split(pkg.DigestMD, "\n")
n := 0
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "- **") {
continue
}
sb.WriteString(line)
sb.WriteString("\n")
n++
if n >= 3 {
break
}
}
if len(pkg.Facts) > 0 {
sb.WriteString("- 对话沉淀事实候选: ")
parts := make([]string, 0, 4)
for i, f := range pkg.Facts {
if i >= 4 {
break
}
parts = append(parts, fmt.Sprintf("%s=%s(%s)", f.Key, f.Value, f.SpeakerName))
}
sb.WriteString(strings.Join(parts, "; "))
sb.WriteString("\n")
}
out := sb.String()
if utf8.RuneCountInString(out) > 400 {
out = truncateRunes(out, 400)
}
return out
}