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

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
toolcontextresult "github.com/LoveLosita/smartflow/backend/newAgent/tools/tool_context_result"
)
type contextToolsAddResult struct {
@@ -34,22 +35,22 @@ type contextToolsRemoveResult struct {
// NewContextToolsAddHandler 创建 context_tools_add 工具。
//
// 职责边界:
// 1. 负责校验 domain/mode/packs 并返回结构化结果,不直接修改流程状态
// 2. 真正的激活态写回”由 execute 节点根据工具结果回写 CommonState
// 3. schedule 支持可选 packstaskclass 当前不支持可选 packs
// 1. 这里只负责校验 domain / packs / mode并产出结构化结果
// 2. 不直接改 CommonState真正的激活切流仍由 execute 层读取 observation 后更新快照
// 3. 因为这里拿不到 CommonState所以卡片展示的是“本次工具结果返回的 domain/packs/mode”不是全局最终快照
func NewContextToolsAddHandler() ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
_ = state
domain := NormalizeToolDomain(readContextToolString(args["domain"]))
if domain == "" {
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Error: "参数非法domain 仅支持 schedule/taskclass",
ErrorCode: "invalid_domain",
}))
})
}
mode := strings.ToLower(strings.TrimSpace(readContextToolString(args["mode"])))
@@ -57,52 +58,54 @@ func NewContextToolsAddHandler() ToolHandler {
mode = "replace"
}
if mode != "replace" && mode != "merge" {
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Domain: domain,
Error: "参数非法mode 仅支持 replace/merge",
ErrorCode: "invalid_mode",
}))
})
}
packsRaw := readContextToolStringSlice(args["packs"])
packs, errCode, errText := validateContextPacks(domain, packsRaw, false)
if errCode != "" {
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Domain: domain,
Error: errText,
ErrorCode: errCode,
}))
})
}
// schedule 未显式传 packs 时,默认启用最小可用包mutation + analyze
// 1. schedule 未显式传 packs 时,默认激活最小可用包 mutation+analyze。
// 2. taskclass 当前没有可选包,所以这里会保持空切片,由 execute 层只保留固定 core。
// 3. 这样做可以让 observation 直接表达“本次实际生效的可选包集合”,减少展示层再二次猜测。
if domain == ToolDomainSchedule && len(packsRaw) == 0 {
packs = ResolveEffectiveToolPacks(domain, nil)
}
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: true,
Action: "activate",
Domain: domain,
Packs: packs,
Mode: mode,
Message: "已激活工具域,可继续调用对应业务工具。",
}))
Message: "已激活目标工具域,可继续调用对应业务工具。",
})
}
}
// NewContextToolsRemoveHandler 创建 context_tools_remove 工具。
//
// 职责边界:
// 1. 仅解析 domain/all/packs 语义并返回结构化结果,不直接触碰上下文存储
// 2. all=true 表示清空动态区业务工具domain+packs 表示移除域下指定二级包;
// 3. 仅 schedule 支持按 packs 移除,且 core 不允许显式移除
// 1. 这里只解释 domain / packs / all 的语义并返回结构化结果;
// 2. all=true 表示清空全部业务工具domain+packs 表示移除域下的可选包;
// 3. 实际 CommonState 的域/包更新,仍由 execute 层统一消费 observation 完成
func NewContextToolsRemoveHandler() ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
_ = state
@@ -111,61 +114,160 @@ func NewContextToolsRemoveHandler() ToolHandler {
domainRaw := strings.ToLower(strings.TrimSpace(readContextToolString(args["domain"])))
packsRaw := readContextToolStringSlice(args["packs"])
// 兼容写法domain=all 视为清空全部。
// 兼容写法domain=all 视为清空全部业务工具域
if domainRaw == "all" {
all = true
}
if all {
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "clear_all",
All: true,
Message: "已移除全部业务工具域,仅保留上下文管理工具。",
}))
Message: "已移除全部业务工具域,仅保留 context 管理工具。",
})
}
domain := NormalizeToolDomain(domainRaw)
if domain == "" {
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: false,
Action: "reject",
Error: "参数非法:需提供 domain=schedule/taskclass 或 all=true",
Error: "参数非法:需提供 domain=schedule/taskclass 或 all=true",
ErrorCode: "invalid_domain",
}))
})
}
packs, errCode, errText := validateContextPacks(domain, packsRaw, true)
if errCode != "" {
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: false,
Action: "reject",
Domain: domain,
Error: errText,
ErrorCode: errCode,
}))
})
}
if len(packs) > 0 {
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "deactivate_packs",
Domain: domain,
Packs: packs,
Message: "已移除指定工具包。",
}))
})
}
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "deactivate",
Domain: domain,
Message: "已移除指定工具域。",
}))
})
}
}
func buildContextToolsAddExecutionResult(args map[string]any, payload contextToolsAddResult) ToolExecutionResult {
observation := marshalContextToolsAddResult(payload)
legacy := LegacyResult(ToolNameContextToolsAdd, args, observation)
view := toolcontextresult.BuildAddView(toContextToolsAddPayload(payload), observation)
return buildContextToolExecutionResult(legacy, args, view)
}
func buildContextToolsRemoveExecutionResult(args map[string]any, payload contextToolsRemoveResult) ToolExecutionResult {
observation := marshalContextToolsRemoveResult(payload)
legacy := LegacyResult(ToolNameContextToolsRemove, args, observation)
view := toolcontextresult.BuildRemoveView(toContextToolsRemovePayload(payload), observation)
return buildContextToolExecutionResult(legacy, args, view)
}
// buildContextToolExecutionResult 负责把子包纯展示结构包回 ToolExecutionResult。
//
// 职责边界:
// 1. 只做 ContextResultView -> ToolDisplayView 的协议桥接;
// 2. 不改写 ObservationText确保模型侧仍消费原始 observation JSON
// 3. 错误码与错误文案继续复用父包现有 JSON/text 解析逻辑,避免多套失败判定分叉。
func buildContextToolExecutionResult(
legacy ToolExecutionResult,
args map[string]any,
view toolcontextresult.ContextResultView,
) ToolExecutionResult {
result := legacy
status := normalizeToolStatus(result.Status)
if status == "" {
status = ToolStatusDone
}
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"] = result.ObservationText
}
result.Status = status
result.Success = status == ToolStatusDone
result.ResultView = &ToolDisplayView{
ViewType: strings.TrimSpace(view.ViewType),
Version: view.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)
}
func toContextToolsAddPayload(payload contextToolsAddResult) toolcontextresult.ContextToolsAddPayload {
return toolcontextresult.ContextToolsAddPayload{
Tool: payload.Tool,
Success: payload.Success,
Action: payload.Action,
Domain: payload.Domain,
Packs: append([]string(nil), payload.Packs...),
Mode: payload.Mode,
Message: payload.Message,
Error: payload.Error,
ErrorCode: payload.ErrorCode,
}
}
func toContextToolsRemovePayload(payload contextToolsRemoveResult) toolcontextresult.ContextToolsRemovePayload {
return toolcontextresult.ContextToolsRemovePayload{
Tool: payload.Tool,
Success: payload.Success,
Action: payload.Action,
Domain: payload.Domain,
Packs: append([]string(nil), payload.Packs...),
All: payload.All,
Message: payload.Message,
Error: payload.Error,
ErrorCode: payload.ErrorCode,
}
}