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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
This commit is contained in:
Losita
2026-04-28 20:22:22 +08:00
parent 1a5b2ecd73
commit d89e2830a9
38 changed files with 9180 additions and 1577 deletions

View File

@@ -0,0 +1,196 @@
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
}