Files
smartmate/backend/newAgent/tools/schedule_analysis_handlers.go
Losita d89e2830a9 Version: 0.9.52.dev.260428
后端:
1. 工具结果结构化切流继续推进:schedule 读工具改为“父包 adapter + 子包 view builder”,`queue_pop_head` / `queue_skip_head` 脱离 legacy wrapper,`analyze_health` / `analyze_rhythm` 补齐 `schedule.analysis_result` 诊断卡片。
2. 非 schedule 工具补齐专属结果协议:`web_search` / `web_fetch`、`upsert_task_class`、`context_tools_add` / `context_tools_remove` 全部接入结构化 `ResultView`,注册表继续去 legacy wrapper,同时保持原始 `ObservationText` 供模型链路复用。
3. 工具展示细节继续收口:参数本地化补齐 `domain` / `packs` / `mode` / `all`,deliver 阶段补发段落分隔,避免 execute 与总结正文黏连。

前端:
4. `ToolCardRenderer` 升级为多协议通用渲染器,补齐 read / analysis / web / taskclass / context 卡片渲染、参数折叠区、未知协议兜底与操作明细展示。
5. `AssistantPanel` 修正 `tool_result` 结果回填与卡片布局宽度问题,并新增结构化卡片 fixture / mock 调试入口,便于整体验收。

仓库:
6. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
2026-04-28 20:22:22 +08:00

197 lines
6.5 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 newagenttools
import (
"strings"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
scheduleanalysis "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule_analysis"
)
type scheduleAnalyzeObserveFunc func(state *schedule.ScheduleState, args map[string]any) string
type scheduleAnalyzeViewBuilder func(input scheduleAnalysisAdapterInput) scheduleanalysis.AnalysisResultView
// scheduleAnalysisAdapterInput 是父包传给 schedule_analysis 子包前的最小上下文。
//
// 职责边界:
// 1. 只携带展示构造需要的 observation 与已本地化参数字段;
// 2. 不把 ToolExecutionResult / ToolArgumentView 传入子包,避免反向依赖父包;
// 3. ObservationText 必须原样来自底层 schedule.AnalyzeXxx不在 adapter 层改写。
type scheduleAnalysisAdapterInput struct {
ToolName string
Args map[string]any
State *schedule.ScheduleState
ObservationText string
ArgFields []scheduleanalysis.KVField
}
// NewAnalyzeHealthToolHandler 为 analyze_health 生成结构化诊断结果。
func NewAnalyzeHealthToolHandler() ToolHandler {
return newScheduleAnalyzeToolHandler(
"analyze_health",
schedule.AnalyzeHealth,
func(input scheduleAnalysisAdapterInput) scheduleanalysis.AnalysisResultView {
return scheduleanalysis.BuildAnalyzeHealthView(scheduleanalysis.AnalyzeHealthViewInput{
Observation: input.ObservationText,
ArgFields: input.ArgFields,
})
},
)
}
// NewAnalyzeRhythmToolHandler 为 analyze_rhythm 生成结构化诊断结果。
func NewAnalyzeRhythmToolHandler() ToolHandler {
return newScheduleAnalyzeToolHandler(
"analyze_rhythm",
schedule.AnalyzeRhythm,
func(input scheduleAnalysisAdapterInput) scheduleanalysis.AnalysisResultView {
return scheduleanalysis.BuildAnalyzeRhythmView(scheduleanalysis.AnalyzeRhythmViewInput{
Observation: input.ObservationText,
ArgFields: input.ArgFields,
})
},
)
}
// newScheduleAnalyzeToolHandler 统一构造父包 analysis adapter。
//
// 步骤化说明:
// 1. 先调用现有 schedule.AnalyzeXxx确保 state_snapshot / prompt 摘要消费的 JSON 完全不变;
// 2. 再用 LegacyResultWithState 复用父包参数展示、状态判断和错误信息提取;
// 3. 最后调用 schedule_analysis 子包生成纯展示视图,并包回 ToolExecutionResult。
func newScheduleAnalyzeToolHandler(
toolName string,
observe scheduleAnalyzeObserveFunc,
buildView scheduleAnalyzeViewBuilder,
) ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
observation := observe(state, args)
legacy := LegacyResultWithState(toolName, args, state, observation)
input := scheduleAnalysisAdapterInput{
ToolName: toolName,
Args: cloneAnyMap(args),
State: state,
ObservationText: observation,
ArgFields: extractScheduleAnalysisArgumentFields(legacy.ArgumentView),
}
return buildScheduleAnalysisExecutionResult(legacy, args, buildView(input))
}
}
// buildScheduleAnalysisExecutionResult 负责把子包纯展示视图包回父包统一协议。
//
// 职责边界:
// 1. 只做 AnalysisResultView -> ToolDisplayView 的协议桥接;
// 2. 不改写 ObservationText确保主动优化状态快照仍读取原始 JSON
// 3. 错误码与错误文案继续复用父包既有 JSON / 文本解析逻辑。
func buildScheduleAnalysisExecutionResult(
legacy ToolExecutionResult,
args map[string]any,
view scheduleanalysis.AnalysisResultView,
) ToolExecutionResult {
result := legacy
status := normalizeToolStatus(result.Status)
if status == "" {
status = ToolStatusDone
}
if collapsedStatus, ok := readStringAnyMap(view.Collapsed, "status"); ok {
if normalized := normalizeToolStatus(collapsedStatus); normalized != "" {
status = normalized
}
}
collapsed := cloneAnyMap(view.Collapsed)
if collapsed == nil {
collapsed = make(map[string]any)
}
expanded := cloneAnyMap(view.Expanded)
if expanded == nil {
expanded = make(map[string]any)
}
collapsed["status"] = status
if _, exists := collapsed["status_label"]; !exists {
collapsed["status_label"] = resolveToolStatusLabelCN(status)
}
if _, exists := expanded["raw_text"]; !exists {
expanded["raw_text"] = strings.TrimSpace(result.ObservationText)
}
viewType := strings.TrimSpace(view.ViewType)
if viewType == "" {
viewType = scheduleanalysis.ViewTypeAnalysisResult
}
version := view.Version
if version <= 0 {
version = scheduleanalysis.ViewVersionAnalysisResult
}
result.Status = status
result.Success = status == ToolStatusDone
result.ResultView = &ToolDisplayView{
ViewType: viewType,
Version: version,
Collapsed: collapsed,
Expanded: expanded,
}
if title, ok := readStringAnyMap(collapsed, "title"); ok {
result.Summary = title
}
if !result.Success {
errorCode, errorMessage := extractToolErrorInfo(result.ObservationText, status)
if strings.TrimSpace(result.ErrorCode) == "" {
result.ErrorCode = strings.TrimSpace(errorCode)
}
if strings.TrimSpace(result.ErrorMessage) == "" {
result.ErrorMessage = strings.TrimSpace(errorMessage)
}
}
return EnsureToolResultDefaults(result, args)
}
// extractScheduleAnalysisArgumentFields 把父包 ToolArgumentView 投影成子包可消费的 KVField。
//
// 说明:
// 1. 参数字段只做回显,尤其 detail / threshold / hard_categories 不在这里解释为真实生效;
// 2. 子包只接收中文 label/display避免理解父包参数 view 结构;
// 3. 字段缺失时返回空切片,由子包跳过参数 section。
func extractScheduleAnalysisArgumentFields(view *ToolArgumentView) []scheduleanalysis.KVField {
if view == nil || view.Expanded == nil {
return make([]scheduleanalysis.KVField, 0)
}
rawFields, exists := view.Expanded["fields"]
if !exists {
return make([]scheduleanalysis.KVField, 0)
}
fields := make([]scheduleanalysis.KVField, 0)
appendField := func(row map[string]any) {
label, _ := row["label"].(string)
display, _ := row["display"].(string)
label = strings.TrimSpace(label)
display = strings.TrimSpace(display)
if label == "" || display == "" {
return
}
fields = append(fields, scheduleanalysis.BuildKVField(label, display))
}
switch typed := rawFields.(type) {
case []map[string]any:
for _, row := range typed {
appendField(row)
}
case []any:
for _, item := range typed {
row, ok := item.(map[string]any)
if !ok {
continue
}
appendField(row)
}
}
if len(fields) == 0 {
return make([]scheduleanalysis.KVField, 0)
}
return fields
}