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:
458
backend/newAgent/tools/taskclass_result_handlers.go
Normal file
458
backend/newAgent/tools/taskclass_result_handlers.go
Normal file
@@ -0,0 +1,458 @@
|
||||
package newagenttools
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
taskclassresult "github.com/LoveLosita/smartflow/backend/newAgent/tools/taskclass_result"
|
||||
)
|
||||
|
||||
type taskClassUpsertExecutionInput struct {
|
||||
Result taskClassUpsertToolResult
|
||||
Normalized *TaskClassUpsertInput
|
||||
}
|
||||
|
||||
// NewTaskClassUpsertToolHandler 返回 upsert_task_class 的结构化结果 handler。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责参数解析、校验、调用依赖与包装结构化结果;
|
||||
// 2. 不改变既有写库语义、confirm 语义与 observation JSON 合约;
|
||||
// 3. 老实现暂以 legacy 函数保留,便于本轮并行迁移后回溯与对照。
|
||||
func NewTaskClassUpsertToolHandler(deps TaskClassWriteDeps) ToolHandler {
|
||||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||
_ = state
|
||||
|
||||
if deps.UpsertTaskClass == nil {
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: false,
|
||||
Validation: taskClassValidationResult{OK: false, Issues: []string{"任务类写库依赖未注入"}},
|
||||
Error: "任务类写库依赖未注入",
|
||||
ErrorCode: "dependency_missing",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
userID, ok := readUpsertUserID(args["_user_id"])
|
||||
if !ok || userID <= 0 {
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: false,
|
||||
Validation: taskClassValidationResult{OK: false, Issues: []string{"无法识别用户身份"}},
|
||||
Error: "工具调用失败:无法识别用户身份",
|
||||
ErrorCode: "missing_user_id",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
input, parseErr := parseTaskClassUpsertInput(args)
|
||||
if parseErr != nil {
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: false,
|
||||
Validation: taskClassValidationResult{OK: false, Issues: []string{parseErr.Error()}},
|
||||
Error: parseErr.Error(),
|
||||
ErrorCode: "invalid_args",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
issues := validateTaskClassUpsertRequest(input.Request, input.ID)
|
||||
if len(issues) > 0 {
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: false,
|
||||
Validation: taskClassValidationResult{OK: false, Issues: issues},
|
||||
Error: strings.Join(issues, ";"),
|
||||
ErrorCode: "validation_failed",
|
||||
},
|
||||
Normalized: &input,
|
||||
})
|
||||
}
|
||||
|
||||
result, err := deps.UpsertTaskClass(userID, input)
|
||||
if err != nil {
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: false,
|
||||
Validation: taskClassValidationResult{OK: false, Issues: []string{"持久化写入失败"}},
|
||||
Error: err.Error(),
|
||||
ErrorCode: "persist_failed",
|
||||
},
|
||||
Normalized: &input,
|
||||
})
|
||||
}
|
||||
if result.TaskClassID <= 0 {
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: false,
|
||||
Validation: taskClassValidationResult{OK: false, Issues: []string{"未返回有效 task_class_id"}},
|
||||
Error: "写入后未返回有效 task_class_id",
|
||||
ErrorCode: "invalid_persist_result",
|
||||
},
|
||||
Normalized: &input,
|
||||
})
|
||||
}
|
||||
|
||||
return buildTaskClassUpsertExecutionResult(args, taskClassUpsertExecutionInput{
|
||||
Result: taskClassUpsertToolResult{
|
||||
Tool: "upsert_task_class",
|
||||
Success: true,
|
||||
TaskClassID: result.TaskClassID,
|
||||
Created: result.Created,
|
||||
Validation: taskClassValidationResult{OK: true, Issues: []string{}},
|
||||
Error: "",
|
||||
ErrorCode: "",
|
||||
},
|
||||
Normalized: &input,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// buildTaskClassUpsertExecutionResult 负责把 upsert_task_class 的原始 observation 包装成结构化卡片。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先沿用 LegacyResult 保留 observation、参数预览、错误码提取等既有链路能力;
|
||||
// 2. 再把规范化请求摘要和写入结果投影到 taskclass_result 子包,避免展示层反向依赖父包;
|
||||
// 3. 最后只替换 ResultView / Summary,不改写写库语义、confirm 语义和原始错误文本。
|
||||
func buildTaskClassUpsertExecutionResult(
|
||||
args map[string]any,
|
||||
input taskClassUpsertExecutionInput,
|
||||
) ToolExecutionResult {
|
||||
observation := marshalTaskClassUpsertResult(input.Result)
|
||||
legacy := LegacyResult("upsert_task_class", args, observation)
|
||||
|
||||
requestSummary := buildTaskClassRequestSummary(args, input.Normalized)
|
||||
view := taskclassresult.BuildUpsertTaskClassView(taskclassresult.BuildUpsertTaskClassViewInput{
|
||||
Status: legacy.Status,
|
||||
Observation: observation,
|
||||
Result: taskclassresult.UpsertResult{
|
||||
Tool: strings.TrimSpace(input.Result.Tool),
|
||||
Success: input.Result.Success,
|
||||
TaskClassID: input.Result.TaskClassID,
|
||||
Created: input.Result.Created,
|
||||
Error: strings.TrimSpace(input.Result.Error),
|
||||
ErrorCode: strings.TrimSpace(input.Result.ErrorCode),
|
||||
ValidationOK: input.Result.Validation.OK,
|
||||
ValidationIssues: append([]string(nil), input.Result.Validation.Issues...),
|
||||
},
|
||||
Request: requestSummary,
|
||||
MachinePayload: buildTaskClassMachinePayload(input.Result, requestSummary),
|
||||
})
|
||||
return buildTaskClassWriteExecutionResult(legacy, args, view)
|
||||
}
|
||||
|
||||
// buildTaskClassWriteExecutionResult 负责把子包纯展示视图包回父包统一协议。
|
||||
func buildTaskClassWriteExecutionResult(
|
||||
legacy ToolExecutionResult,
|
||||
args map[string]any,
|
||||
view taskclassresult.WriteResultView,
|
||||
) 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"] = result.ObservationText
|
||||
}
|
||||
|
||||
viewType := strings.TrimSpace(view.ViewType)
|
||||
if viewType == "" {
|
||||
viewType = taskclassresult.ViewTypeWriteResult
|
||||
}
|
||||
version := view.Version
|
||||
if version <= 0 {
|
||||
version = taskclassresult.ViewVersionWriteResult
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func buildTaskClassRequestSummary(
|
||||
args map[string]any,
|
||||
normalized *TaskClassUpsertInput,
|
||||
) taskclassresult.RequestSummary {
|
||||
summary := buildTaskClassRequestSummaryFromArgs(args)
|
||||
if normalized == nil {
|
||||
return summary
|
||||
}
|
||||
|
||||
summary.RequestedID = normalized.ID
|
||||
summary.Name = strings.TrimSpace(normalized.Request.Name)
|
||||
summary.Mode = strings.TrimSpace(normalized.Request.Mode)
|
||||
summary.StartDate = strings.TrimSpace(normalized.Request.StartDate)
|
||||
summary.EndDate = strings.TrimSpace(normalized.Request.EndDate)
|
||||
summary.SubjectType = strings.TrimSpace(normalized.Request.SubjectType)
|
||||
summary.DifficultyLevel = strings.TrimSpace(normalized.Request.DifficultyLevel)
|
||||
summary.CognitiveIntensity = strings.TrimSpace(normalized.Request.CognitiveIntensity)
|
||||
summary.TotalSlots = normalized.Request.Config.TotalSlots
|
||||
summary.AllowFillerCourse = normalized.Request.Config.AllowFillerCourse
|
||||
summary.Strategy = strings.TrimSpace(normalized.Request.Config.Strategy)
|
||||
summary.ExcludedSlots = cloneIntSlice(normalized.Request.Config.ExcludedSlots)
|
||||
summary.ExcludedDaysOfWeek = cloneIntSlice(normalized.Request.Config.ExcludedDaysOfWeek)
|
||||
summary.Source = strings.TrimSpace(normalized.Source)
|
||||
summary.Items = buildTaskClassItemsSummary(normalized.Request.Items)
|
||||
return summary
|
||||
}
|
||||
|
||||
func buildTaskClassRequestSummaryFromArgs(args map[string]any) taskclassresult.RequestSummary {
|
||||
summary := taskclassresult.RequestSummary{
|
||||
RequestedID: readOptionalInt(args, "id"),
|
||||
Source: readOptionalString(args, "source"),
|
||||
Items: make([]taskclassresult.TaskClassItemSummary, 0),
|
||||
ExcludedSlots: make([]int, 0),
|
||||
ExcludedDaysOfWeek: make([]int, 0),
|
||||
}
|
||||
|
||||
taskClassMap, _ := readAnyMap(args["task_class"])
|
||||
if taskClassMap == nil {
|
||||
return summary
|
||||
}
|
||||
|
||||
summary.Name = strings.TrimSpace(readAnyString(taskClassMap["name"]))
|
||||
summary.Mode = strings.TrimSpace(readAnyString(taskClassMap["mode"]))
|
||||
summary.StartDate = strings.TrimSpace(readAnyString(taskClassMap["start_date"]))
|
||||
summary.EndDate = strings.TrimSpace(readAnyString(taskClassMap["end_date"]))
|
||||
summary.SubjectType = strings.TrimSpace(readAnyString(taskClassMap["subject_type"]))
|
||||
summary.DifficultyLevel = strings.TrimSpace(readAnyString(taskClassMap["difficulty_level"]))
|
||||
summary.CognitiveIntensity = strings.TrimSpace(readAnyString(taskClassMap["cognitive_intensity"]))
|
||||
|
||||
configMap, _ := readAnyMap(taskClassMap["config"])
|
||||
summary.TotalSlots = readAnyInt(configMap["total_slots"])
|
||||
summary.AllowFillerCourse = readAnyBool(configMap["allow_filler_course"])
|
||||
summary.Strategy = strings.TrimSpace(readAnyString(configMap["strategy"]))
|
||||
summary.ExcludedSlots = readAnyIntSlice(configMap["excluded_slots"])
|
||||
summary.ExcludedDaysOfWeek = readAnyIntSlice(configMap["excluded_days_of_week"])
|
||||
|
||||
rawItems := taskClassMap["items"]
|
||||
if topLevelItems, exists := args["items"]; exists {
|
||||
rawItems = topLevelItems
|
||||
}
|
||||
summary.Items = buildTaskClassItemsSummaryFromRaw(rawItems)
|
||||
return summary
|
||||
}
|
||||
|
||||
func buildTaskClassItemsSummary(items []model.UserAddTaskClassItemRequest) []taskclassresult.TaskClassItemSummary {
|
||||
if len(items) == 0 {
|
||||
return make([]taskclassresult.TaskClassItemSummary, 0)
|
||||
}
|
||||
out := make([]taskclassresult.TaskClassItemSummary, 0, len(items))
|
||||
for _, item := range items {
|
||||
summary := taskclassresult.TaskClassItemSummary{
|
||||
ID: item.ID,
|
||||
Order: item.Order,
|
||||
Content: strings.TrimSpace(item.Content),
|
||||
}
|
||||
if item.EmbeddedTime != nil {
|
||||
summary.EmbeddedWeek = item.EmbeddedTime.Week
|
||||
summary.EmbeddedDay = item.EmbeddedTime.DayOfWeek
|
||||
summary.EmbeddedSectionFrom = item.EmbeddedTime.SectionFrom
|
||||
summary.EmbeddedSectionTo = item.EmbeddedTime.SectionTo
|
||||
}
|
||||
out = append(out, summary)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildTaskClassItemsSummaryFromRaw(raw any) []taskclassresult.TaskClassItemSummary {
|
||||
rawList, ok := raw.([]any)
|
||||
if !ok || len(rawList) == 0 {
|
||||
return make([]taskclassresult.TaskClassItemSummary, 0)
|
||||
}
|
||||
out := make([]taskclassresult.TaskClassItemSummary, 0, len(rawList))
|
||||
for index, row := range rawList {
|
||||
itemMap, ok := row.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
summary := taskclassresult.TaskClassItemSummary{
|
||||
ID: readAnyInt(itemMap["id"]),
|
||||
Order: maxPositiveInt(readAnyInt(itemMap["order"]), index+1),
|
||||
Content: strings.TrimSpace(firstNonEmptyString(
|
||||
readAnyString(itemMap["content"]),
|
||||
readAnyString(itemMap["description"]),
|
||||
readAnyString(itemMap["title"]),
|
||||
readAnyString(itemMap["name"]),
|
||||
)),
|
||||
}
|
||||
if embeddedMap, ok := readAnyMap(itemMap["embedded_time"]); ok {
|
||||
summary.EmbeddedWeek = readAnyInt(embeddedMap["week"])
|
||||
summary.EmbeddedDay = readAnyInt(embeddedMap["day_of_week"])
|
||||
summary.EmbeddedSectionFrom = readAnyInt(embeddedMap["section_from"])
|
||||
summary.EmbeddedSectionTo = readAnyInt(embeddedMap["section_to"])
|
||||
}
|
||||
out = append(out, summary)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildTaskClassMachinePayload(
|
||||
result taskClassUpsertToolResult,
|
||||
request taskclassresult.RequestSummary,
|
||||
) map[string]any {
|
||||
return map[string]any{
|
||||
"parsed_result": map[string]any{
|
||||
"tool": strings.TrimSpace(result.Tool),
|
||||
"success": result.Success,
|
||||
"task_class_id": result.TaskClassID,
|
||||
"created": result.Created,
|
||||
"validation": map[string]any{
|
||||
"ok": result.Validation.OK,
|
||||
"issues": append([]string(nil), result.Validation.Issues...),
|
||||
},
|
||||
"error": strings.TrimSpace(result.Error),
|
||||
"error_code": strings.TrimSpace(result.ErrorCode),
|
||||
},
|
||||
"input_summary": map[string]any{
|
||||
"requested_id": request.RequestedID,
|
||||
"name": request.Name,
|
||||
"mode": request.Mode,
|
||||
"start_date": request.StartDate,
|
||||
"end_date": request.EndDate,
|
||||
"subject_type": request.SubjectType,
|
||||
"difficulty_level": request.DifficultyLevel,
|
||||
"cognitive_intensity": request.CognitiveIntensity,
|
||||
"total_slots": request.TotalSlots,
|
||||
"allow_filler_course": request.AllowFillerCourse,
|
||||
"strategy": request.Strategy,
|
||||
"excluded_slots": cloneIntSlice(request.ExcludedSlots),
|
||||
"excluded_days_of_week": cloneIntSlice(request.ExcludedDaysOfWeek),
|
||||
"source": request.Source,
|
||||
"items": buildTaskClassItemMachinePayload(request.Items),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildTaskClassItemMachinePayload(items []taskclassresult.TaskClassItemSummary) []map[string]any {
|
||||
if len(items) == 0 {
|
||||
return make([]map[string]any, 0)
|
||||
}
|
||||
out := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, map[string]any{
|
||||
"id": item.ID,
|
||||
"order": item.Order,
|
||||
"content": item.Content,
|
||||
"embedded_week": item.EmbeddedWeek,
|
||||
"embedded_day": item.EmbeddedDay,
|
||||
"section_from": item.EmbeddedSectionFrom,
|
||||
"section_to": item.EmbeddedSectionTo,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func readOptionalString(args map[string]any, key string) string {
|
||||
if len(args) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(readAnyString(args[key]))
|
||||
}
|
||||
|
||||
func readOptionalInt(args map[string]any, key string) int {
|
||||
if len(args) == 0 {
|
||||
return 0
|
||||
}
|
||||
return readAnyInt(args[key])
|
||||
}
|
||||
|
||||
func readAnyInt(raw any) int {
|
||||
value, ok := readUpsertInt(raw)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func readAnyBool(raw any) bool {
|
||||
value, ok := raw.(bool)
|
||||
return ok && value
|
||||
}
|
||||
|
||||
func readAnyIntSlice(raw any) []int {
|
||||
switch typed := raw.(type) {
|
||||
case []int:
|
||||
return cloneIntSlice(typed)
|
||||
case []any:
|
||||
out := make([]int, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
value, ok := readUpsertInt(item)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return make([]int, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneIntSlice(values []int) []int {
|
||||
if len(values) == 0 {
|
||||
return make([]int, 0)
|
||||
}
|
||||
out := make([]int, len(values))
|
||||
copy(out, values)
|
||||
return out
|
||||
}
|
||||
|
||||
func maxPositiveInt(left int, right int) int {
|
||||
if left <= 0 {
|
||||
return right
|
||||
}
|
||||
if right <= 0 {
|
||||
return left
|
||||
}
|
||||
if left > right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
Reference in New Issue
Block a user