Version: 0.9.47.dev.260427

后端:
1. execute 节点继续拆职责——超大 execute.go 下沉为 node/execute 子包,按决策流、动作路由、上下文锚点、工具执行、状态快照、工具展示与参数解析拆分;顶层 execute.go 收敛为桥接导出,降低单文件编排/业务/模型/工具逻辑混写
2. 节点公共能力继续沉到 shared——抽出 LLM 纠错回灌、完整上下文调试日志、thinking 开关、统一上下文压缩、可见 assistant 文本持久化等 node_* 公共件,减少 execute 独占实现并为其他节点复用铺路
3. speak 文本整理能力独立收口——新增 speak_text 辅助文件,补齐正文归一化的独立承载,继续收缩 execute 主文件体积

前端:
4. NewAgent 时间线接入 business_card 业务卡片协议——schedule_agent.ts 新增 task_query / task_record 卡片载荷类型与 business_card kind;AssistantPanel 增加业务卡片事件存储、时间线恢复、块渲染分支与 BusinessCardRenderer 接入,同时保留 interrupt / status / tool / reasoning 多块并存
5. 新增任务查询卡片与任务记录卡片组件,并补充 DesignDemo 设计预览页与路由,前端可先行验证 business_card 的视觉与交互落点

文档:
6. 新增 newagent business card 前后端对接说明,明确 timeline kind、payload 结构、卡片分类、前后端发射/渲染约束
This commit is contained in:
LoveLosita
2026-04-27 17:35:55 +08:00
parent 736ba0cff3
commit 0b0ed3c61a
23 changed files with 4528 additions and 2966 deletions

View File

@@ -0,0 +1,332 @@
package newagentexecute
import (
"encoding/json"
"fmt"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
)
func shouldForceFeasibilityNegotiation(
flowState *newagentmodel.CommonState,
registry *newagenttools.ToolRegistry,
toolName string,
) bool {
if flowState == nil || registry == nil {
return false
}
if !flowState.HealthCheckDone || flowState.HealthIsFeasible {
return false
}
if !registry.IsWriteTool(toolName) || !registry.RequiresScheduleState(toolName) {
return false
}
return true
}
func buildInfeasibleNegotiationQuestion(flowState *newagentmodel.CommonState) string {
capacityGap := 0
reasonCode := "capacity_insufficient"
if flowState != nil {
capacityGap = flowState.HealthCapacityGap
if strings.TrimSpace(flowState.HealthReasonCode) != "" {
reasonCode = strings.TrimSpace(flowState.HealthReasonCode)
}
}
return fmt.Sprintf(
"当前计划不可行analyze_health 判断当前约束不可行capacity_gap=%dreason=%s。在继续写操作前请先与用户协商扩展时间窗、放宽约束、缩减范围或预算或接受风险收口。",
capacityGap,
reasonCode,
)
}
func buildInfeasibleBlockedResult(flowState *newagentmodel.CommonState) string {
capacityGap := 0
reasonCode := "capacity_insufficient"
if flowState != nil {
capacityGap = flowState.HealthCapacityGap
if strings.TrimSpace(flowState.HealthReasonCode) != "" {
reasonCode = strings.TrimSpace(flowState.HealthReasonCode)
}
}
return fmt.Sprintf(
"已阻断本次写操作analyze_health 判定当前约束不可行capacity_gap=%dreason=%s。请先与用户协商扩展时间窗 / 放宽约束 / 缩减范围或预算 / 接受风险收口。",
capacityGap,
reasonCode,
)
}
type contextToolsResultEnvelope struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
Mode string `json:"mode,omitempty"`
All bool `json:"all,omitempty"`
}
type analyzeHealthResultEnvelope struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Feasibility *analyzeHealthFeasibilityBrief `json:"feasibility,omitempty"`
Decision *analyzeHealthDecisionBrief `json:"decision,omitempty"`
}
type analyzeHealthFeasibilityBrief struct {
IsFeasible bool `json:"is_feasible"`
CapacityGap int `json:"capacity_gap"`
ReasonCode string `json:"reason_code"`
}
type analyzeHealthDecisionBrief struct {
ShouldContinueOptimize bool `json:"should_continue_optimize"`
PrimaryProblem string `json:"primary_problem,omitempty"`
RecommendedOperation string `json:"recommended_operation,omitempty"`
IsForcedImperfection bool `json:"is_forced_imperfection"`
ImprovementSignal string `json:"improvement_signal,omitempty"`
}
type upsertTaskClassResultEnvelope struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Validation *upsertTaskClassValidationPart `json:"validation,omitempty"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
type upsertTaskClassValidationPart struct {
OK bool `json:"ok"`
Issues []string `json:"issues"`
}
func updateActiveToolDomainSnapshot(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !newagenttools.IsContextManagementTool(toolName) {
return
}
var envelope contextToolsResultEnvelope
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
return
}
if !envelope.Success {
return
}
switch strings.TrimSpace(toolName) {
case newagenttools.ToolNameContextToolsAdd:
domain := newagenttools.NormalizeToolDomain(envelope.Domain)
if domain == "" {
return
}
nextPacks := newagenttools.ResolveEffectiveToolPacks(domain, envelope.Packs)
mode := strings.ToLower(strings.TrimSpace(envelope.Mode))
if mode == "merge" && newagenttools.NormalizeToolDomain(flowState.ActiveToolDomain) == domain {
merged := make([]string, 0, len(flowState.ActiveToolPacks)+len(nextPacks))
seen := make(map[string]struct{}, len(flowState.ActiveToolPacks)+len(nextPacks))
current := newagenttools.ResolveEffectiveToolPacks(domain, flowState.ActiveToolPacks)
for _, pack := range current {
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
merged = append(merged, pack)
}
for _, pack := range nextPacks {
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
merged = append(merged, pack)
}
nextPacks = merged
}
flowState.ActiveToolDomain = domain
flowState.ActiveToolPacks = nextPacks
case newagenttools.ToolNameContextToolsRemove:
if envelope.All {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
domain := newagenttools.NormalizeToolDomain(envelope.Domain)
if domain == "" {
return
}
currentDomain := newagenttools.NormalizeToolDomain(flowState.ActiveToolDomain)
if currentDomain != domain {
return
}
removedPacks := newagenttools.NormalizeToolPacks(domain, envelope.Packs)
if len(removedPacks) == 0 {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
currentEffective := newagenttools.ResolveEffectiveToolPacks(domain, flowState.ActiveToolPacks)
if len(currentEffective) == 0 {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
removedSet := make(map[string]struct{}, len(removedPacks))
for _, pack := range removedPacks {
removedSet[pack] = struct{}{}
}
remaining := make([]string, 0, len(currentEffective))
for _, pack := range currentEffective {
if _, shouldRemove := removedSet[pack]; shouldRemove {
continue
}
remaining = append(remaining, pack)
}
if len(remaining) == 0 {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
flowState.ActiveToolPacks = remaining
}
}
func updateHealthFeasibilitySnapshot(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !strings.EqualFold(strings.TrimSpace(toolName), toolAnalyzeHealth) {
return
}
flowState.HealthCheckDone = false
flowState.HealthIsFeasible = true
flowState.HealthCapacityGap = 0
flowState.HealthReasonCode = ""
var envelope analyzeHealthResultEnvelope
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
return
}
if !envelope.Success || envelope.Feasibility == nil {
return
}
flowState.HealthCheckDone = true
flowState.HealthIsFeasible = envelope.Feasibility.IsFeasible
flowState.HealthCapacityGap = envelope.Feasibility.CapacityGap
flowState.HealthReasonCode = strings.TrimSpace(envelope.Feasibility.ReasonCode)
}
func updateTaskClassUpsertSnapshot(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !strings.EqualFold(strings.TrimSpace(toolName), "upsert_task_class") {
return
}
flowState.TaskClassUpsertLastTried = true
flowState.TaskClassUpsertLastSuccess = false
flowState.TaskClassUpsertLastIssues = nil
var envelope upsertTaskClassResultEnvelope
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
flowState.TaskClassUpsertConsecutiveFailures++
return
}
success := envelope.Success
issues := make([]string, 0)
if envelope.Validation != nil {
issues = append(issues, parseAnyToStringSlice(any(envelope.Validation.Issues))...)
if !envelope.Validation.OK {
success = false
}
}
if !success && strings.TrimSpace(envelope.Error) != "" && len(issues) == 0 {
issues = append(issues, strings.TrimSpace(envelope.Error))
}
issues = uniqueNonEmptyStrings(issues)
flowState.TaskClassUpsertLastSuccess = success
flowState.TaskClassUpsertLastIssues = issues
if success {
flowState.TaskClassUpsertConsecutiveFailures = 0
return
}
flowState.TaskClassUpsertConsecutiveFailures++
}
func uniqueNonEmptyStrings(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
text := strings.TrimSpace(value)
if text == "" {
continue
}
if _, exists := seen[text]; exists {
continue
}
seen[text] = struct{}{}
result = append(result, text)
}
return result
}
func updateHealthSnapshotV2(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !strings.EqualFold(strings.TrimSpace(toolName), toolAnalyzeHealth) {
return
}
prevSignal := strings.TrimSpace(flowState.HealthImprovementSignal)
flowState.HealthCheckDone = false
flowState.HealthIsFeasible = true
flowState.HealthCapacityGap = 0
flowState.HealthReasonCode = ""
flowState.HealthShouldContinueOptimize = false
flowState.HealthTightnessLevel = ""
flowState.HealthPrimaryProblem = ""
flowState.HealthRecommendedOperation = ""
flowState.HealthIsForcedImperfection = false
flowState.HealthImprovementSignal = ""
var envelope struct {
Success bool `json:"success"`
Feasibility *analyzeHealthFeasibilityBrief `json:"feasibility,omitempty"`
Metrics struct {
Tightness *struct {
TightnessLevel string `json:"tightness_level"`
} `json:"tightness,omitempty"`
} `json:"metrics"`
Decision *analyzeHealthDecisionBrief `json:"decision,omitempty"`
}
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
flowState.HealthStagnationCount = 0
return
}
if !envelope.Success || envelope.Feasibility == nil {
flowState.HealthStagnationCount = 0
return
}
flowState.HealthCheckDone = true
flowState.HealthIsFeasible = envelope.Feasibility.IsFeasible
flowState.HealthCapacityGap = envelope.Feasibility.CapacityGap
flowState.HealthReasonCode = strings.TrimSpace(envelope.Feasibility.ReasonCode)
if envelope.Metrics.Tightness != nil {
flowState.HealthTightnessLevel = strings.TrimSpace(envelope.Metrics.Tightness.TightnessLevel)
}
if envelope.Decision != nil {
flowState.HealthShouldContinueOptimize = envelope.Decision.ShouldContinueOptimize
flowState.HealthPrimaryProblem = strings.TrimSpace(envelope.Decision.PrimaryProblem)
flowState.HealthRecommendedOperation = strings.TrimSpace(envelope.Decision.RecommendedOperation)
flowState.HealthIsForcedImperfection = envelope.Decision.IsForcedImperfection
flowState.HealthImprovementSignal = strings.TrimSpace(envelope.Decision.ImprovementSignal)
}
if signal := strings.TrimSpace(flowState.HealthImprovementSignal); signal != "" && prevSignal != "" && signal == prevSignal {
flowState.HealthStagnationCount++
return
}
flowState.HealthStagnationCount = 0
}