后端: 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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
398 lines
8.9 KiB
Go
398 lines
8.9 KiB
Go
package taskclass_result
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// 说明:
|
||
// 1. schedule_read / schedule_analysis 已经各自带有一套卡片 helper;
|
||
// 2. 这一轮只迁 taskclass 写入结果,如果现在强行把前三批 helper 回抽成公共层,会扩大回归面;
|
||
// 3. 因此本包只保留 taskclass.write_result 所需的最小 helper,待非 schedule 主链稳定后再统一评估抽象。
|
||
|
||
func buildWriteResultView(
|
||
status string,
|
||
title string,
|
||
subtitle string,
|
||
metrics []MetricField,
|
||
items []ItemView,
|
||
sections []map[string]any,
|
||
observation string,
|
||
machinePayload map[string]any,
|
||
) WriteResultView {
|
||
normalizedStatus := normalizeStatus(status)
|
||
if normalizedStatus == "" {
|
||
normalizedStatus = StatusDone
|
||
}
|
||
|
||
collapsed := map[string]any{
|
||
"title": strings.TrimSpace(title),
|
||
"subtitle": strings.TrimSpace(subtitle),
|
||
"status": normalizedStatus,
|
||
"status_label": resolveStatusLabelCN(normalizedStatus),
|
||
"metrics": metricListToMaps(metrics),
|
||
}
|
||
expanded := map[string]any{
|
||
"items": itemListToMaps(items),
|
||
"sections": cloneSectionList(sections),
|
||
"raw_text": observation,
|
||
}
|
||
if len(machinePayload) > 0 {
|
||
expanded["machine_payload"] = cloneAnyMap(machinePayload)
|
||
}
|
||
|
||
return WriteResultView{
|
||
ViewType: ViewTypeWriteResult,
|
||
Version: ViewVersionWriteResult,
|
||
Collapsed: collapsed,
|
||
Expanded: expanded,
|
||
}
|
||
}
|
||
|
||
func buildMetric(label string, value string) MetricField {
|
||
return MetricField{
|
||
Label: strings.TrimSpace(label),
|
||
Value: strings.TrimSpace(value),
|
||
}
|
||
}
|
||
|
||
func buildKVField(label string, value string) KVField {
|
||
return KVField{
|
||
Label: strings.TrimSpace(label),
|
||
Value: strings.TrimSpace(value),
|
||
}
|
||
}
|
||
|
||
func buildItem(title string, subtitle string, tags []string, detailLines []string, meta map[string]any) ItemView {
|
||
return ItemView{
|
||
Title: strings.TrimSpace(title),
|
||
Subtitle: strings.TrimSpace(subtitle),
|
||
Tags: normalizeStringSlice(tags),
|
||
DetailLines: normalizeStringSlice(detailLines),
|
||
Meta: cloneAnyMap(meta),
|
||
}
|
||
}
|
||
|
||
func buildItemsSection(title string, items []ItemView) map[string]any {
|
||
return map[string]any{
|
||
"type": "items",
|
||
"title": strings.TrimSpace(title),
|
||
"items": itemListToMaps(items),
|
||
}
|
||
}
|
||
|
||
func buildKVSection(title string, fields []KVField) map[string]any {
|
||
rows := make([]map[string]any, 0, len(fields))
|
||
for _, field := range fields {
|
||
label := strings.TrimSpace(field.Label)
|
||
value := strings.TrimSpace(field.Value)
|
||
if label == "" || value == "" {
|
||
continue
|
||
}
|
||
rows = append(rows, map[string]any{
|
||
"label": label,
|
||
"value": value,
|
||
})
|
||
}
|
||
return map[string]any{
|
||
"type": "kv",
|
||
"title": strings.TrimSpace(title),
|
||
"fields": rows,
|
||
}
|
||
}
|
||
|
||
func buildCalloutSection(title string, subtitle string, tone string, detailLines []string) map[string]any {
|
||
return map[string]any{
|
||
"type": "callout",
|
||
"title": strings.TrimSpace(title),
|
||
"subtitle": strings.TrimSpace(subtitle),
|
||
"tone": strings.TrimSpace(tone),
|
||
"detail_lines": normalizeStringSlice(detailLines),
|
||
}
|
||
}
|
||
|
||
func normalizeStatus(status string) string {
|
||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||
case StatusDone:
|
||
return StatusDone
|
||
case StatusBlocked:
|
||
return StatusBlocked
|
||
case StatusFailed:
|
||
return StatusFailed
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func resolveStatusLabelCN(status string) string {
|
||
switch normalizeStatus(status) {
|
||
case StatusDone:
|
||
return "已完成"
|
||
case StatusBlocked:
|
||
return "已阻断"
|
||
default:
|
||
return "失败"
|
||
}
|
||
}
|
||
|
||
func formatSourceCN(source string) string {
|
||
switch strings.ToLower(strings.TrimSpace(source)) {
|
||
case "chat":
|
||
return "对话"
|
||
case "memory":
|
||
return "记忆"
|
||
case "web":
|
||
return "网页"
|
||
case "":
|
||
return "未标注"
|
||
default:
|
||
return strings.TrimSpace(source)
|
||
}
|
||
}
|
||
|
||
func formatModeCN(mode string) string {
|
||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||
case "auto":
|
||
return "自动排布"
|
||
case "manual":
|
||
return "手动维护"
|
||
default:
|
||
return fallbackText(mode, "未标注")
|
||
}
|
||
}
|
||
|
||
func formatStrategyCN(strategy string) string {
|
||
switch strings.ToLower(strings.TrimSpace(strategy)) {
|
||
case "steady":
|
||
return "稳态推进"
|
||
case "rapid":
|
||
return "快速推进"
|
||
default:
|
||
return fallbackText(strategy, "未标注")
|
||
}
|
||
}
|
||
|
||
func formatSubjectTypeCN(subjectType string) string {
|
||
switch strings.ToLower(strings.TrimSpace(subjectType)) {
|
||
case "quantitative":
|
||
return "计算型"
|
||
case "memory":
|
||
return "记忆型"
|
||
case "reading":
|
||
return "阅读型"
|
||
case "mixed":
|
||
return "混合型"
|
||
default:
|
||
return fallbackText(subjectType, "未标注")
|
||
}
|
||
}
|
||
|
||
func formatLevelCN(level string) string {
|
||
switch strings.ToLower(strings.TrimSpace(level)) {
|
||
case "low":
|
||
return "低"
|
||
case "medium":
|
||
return "中"
|
||
case "high":
|
||
return "高"
|
||
default:
|
||
return fallbackText(level, "未标注")
|
||
}
|
||
}
|
||
|
||
func formatBoolCN(value bool) string {
|
||
if value {
|
||
return "是"
|
||
}
|
||
return "否"
|
||
}
|
||
|
||
func formatDateRangeCN(start string, end string) string {
|
||
start = strings.TrimSpace(start)
|
||
end = strings.TrimSpace(end)
|
||
switch {
|
||
case start != "" && end != "":
|
||
return fmt.Sprintf("%s 至 %s", start, end)
|
||
case start != "":
|
||
return start
|
||
case end != "":
|
||
return end
|
||
default:
|
||
return "未标注"
|
||
}
|
||
}
|
||
|
||
func formatIntListCN(values []int, emptyText string, formatFn func(int) string) string {
|
||
if len(values) == 0 {
|
||
return strings.TrimSpace(emptyText)
|
||
}
|
||
parts := make([]string, 0, len(values))
|
||
for _, value := range values {
|
||
parts = append(parts, formatFn(value))
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
func formatWeekdayCN(day int) string {
|
||
switch day {
|
||
case 1:
|
||
return "周一"
|
||
case 2:
|
||
return "周二"
|
||
case 3:
|
||
return "周三"
|
||
case 4:
|
||
return "周四"
|
||
case 5:
|
||
return "周五"
|
||
case 6:
|
||
return "周六"
|
||
case 7:
|
||
return "周日"
|
||
default:
|
||
return fmt.Sprintf("星期%d", day)
|
||
}
|
||
}
|
||
|
||
func formatEmbeddedTimeCN(item TaskClassItemSummary) string {
|
||
if item.EmbeddedWeek <= 0 || item.EmbeddedDay <= 0 || item.EmbeddedSectionFrom <= 0 || item.EmbeddedSectionTo <= 0 {
|
||
return "未指定"
|
||
}
|
||
return fmt.Sprintf(
|
||
"第%d周 %s 第%d-%d节",
|
||
item.EmbeddedWeek,
|
||
formatWeekdayCN(item.EmbeddedDay),
|
||
item.EmbeddedSectionFrom,
|
||
item.EmbeddedSectionTo,
|
||
)
|
||
}
|
||
|
||
func normalizeStringSlice(values []string) []string {
|
||
if len(values) == 0 {
|
||
return make([]string, 0)
|
||
}
|
||
out := make([]string, 0, len(values))
|
||
for _, value := range values {
|
||
text := strings.TrimSpace(value)
|
||
if text == "" {
|
||
continue
|
||
}
|
||
out = append(out, text)
|
||
}
|
||
if len(out) == 0 {
|
||
return make([]string, 0)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func truncateText(text string, limit int) string {
|
||
runes := []rune(strings.TrimSpace(text))
|
||
if len(runes) == 0 {
|
||
return "未填写内容"
|
||
}
|
||
if limit <= 0 || len(runes) <= limit {
|
||
return string(runes)
|
||
}
|
||
return string(runes[:limit]) + "..."
|
||
}
|
||
|
||
func fallbackText(text string, fallback string) string {
|
||
if strings.TrimSpace(text) == "" {
|
||
return strings.TrimSpace(fallback)
|
||
}
|
||
return strings.TrimSpace(text)
|
||
}
|
||
|
||
func metricListToMaps(metrics []MetricField) []map[string]any {
|
||
if len(metrics) == 0 {
|
||
return make([]map[string]any, 0)
|
||
}
|
||
out := make([]map[string]any, 0, len(metrics))
|
||
for _, metric := range metrics {
|
||
label := strings.TrimSpace(metric.Label)
|
||
value := strings.TrimSpace(metric.Value)
|
||
if label == "" || value == "" {
|
||
continue
|
||
}
|
||
out = append(out, map[string]any{
|
||
"label": label,
|
||
"value": value,
|
||
})
|
||
}
|
||
if len(out) == 0 {
|
||
return make([]map[string]any, 0)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func itemListToMaps(items []ItemView) []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 {
|
||
row := map[string]any{
|
||
"title": strings.TrimSpace(item.Title),
|
||
"subtitle": strings.TrimSpace(item.Subtitle),
|
||
"tags": normalizeStringSlice(item.Tags),
|
||
"detail_lines": normalizeStringSlice(item.DetailLines),
|
||
}
|
||
if len(item.Meta) > 0 {
|
||
row["meta"] = cloneAnyMap(item.Meta)
|
||
}
|
||
out = append(out, row)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cloneSectionList(sections []map[string]any) []map[string]any {
|
||
if len(sections) == 0 {
|
||
return make([]map[string]any, 0)
|
||
}
|
||
out := make([]map[string]any, 0, len(sections))
|
||
for _, section := range sections {
|
||
out = append(out, cloneAnyMap(section))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cloneAnyMap(input map[string]any) map[string]any {
|
||
if len(input) == 0 {
|
||
return nil
|
||
}
|
||
out := make(map[string]any, len(input))
|
||
for key, value := range input {
|
||
out[key] = cloneAnyValue(value)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cloneAnyValue(value any) any {
|
||
switch typed := value.(type) {
|
||
case map[string]any:
|
||
return cloneAnyMap(typed)
|
||
case []map[string]any:
|
||
out := make([]map[string]any, 0, len(typed))
|
||
for _, item := range typed {
|
||
out = append(out, cloneAnyMap(item))
|
||
}
|
||
return out
|
||
case []any:
|
||
out := make([]any, 0, len(typed))
|
||
for _, item := range typed {
|
||
out = append(out, cloneAnyValue(item))
|
||
}
|
||
return out
|
||
case []string:
|
||
out := make([]string, len(typed))
|
||
copy(out, typed)
|
||
return out
|
||
case []int:
|
||
out := make([]int, len(typed))
|
||
copy(out, typed)
|
||
return out
|
||
default:
|
||
return typed
|
||
}
|
||
}
|