后端: 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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
654 lines
18 KiB
Go
654 lines
18 KiB
Go
package toolcontextresult
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
const (
|
||
ViewTypeContextResult = "tool.context_result"
|
||
ViewVersionContextResult = 1
|
||
StatusDone = "done"
|
||
StatusFailed = "failed"
|
||
)
|
||
|
||
// ContextResultView 仅承载 context 工具卡片的纯展示数据。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责输出 view_type / version / collapsed / expanded 四段展示结构;
|
||
// 2. 不依赖父包 ToolExecutionResult,避免形成反向 import;
|
||
// 3. 不改写 ObservationText,原始文本由父包原样挂到 expanded.raw_text。
|
||
type ContextResultView struct {
|
||
ViewType string `json:"view_type"`
|
||
Version int `json:"version"`
|
||
Collapsed map[string]any `json:"collapsed"`
|
||
Expanded map[string]any `json:"expanded"`
|
||
}
|
||
|
||
type MetricField struct {
|
||
Label string `json:"label"`
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
type KVField struct {
|
||
Label string `json:"label"`
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
type ItemView struct {
|
||
Title string `json:"title"`
|
||
Subtitle string `json:"subtitle"`
|
||
Tags []string `json:"tags"`
|
||
DetailLines []string `json:"detail_lines"`
|
||
Meta map[string]any `json:"meta,omitempty"`
|
||
}
|
||
|
||
// ContextToolsAddPayload 对齐 context_tools_add observation 的机器字段。
|
||
type ContextToolsAddPayload struct {
|
||
Tool string `json:"tool"`
|
||
Success bool `json:"success"`
|
||
Action string `json:"action"`
|
||
Domain string `json:"domain,omitempty"`
|
||
Packs []string `json:"packs,omitempty"`
|
||
Mode string `json:"mode,omitempty"`
|
||
Message string `json:"message,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
ErrorCode string `json:"error_code,omitempty"`
|
||
}
|
||
|
||
// ContextToolsRemovePayload 对齐 context_tools_remove observation 的机器字段。
|
||
type ContextToolsRemovePayload struct {
|
||
Tool string `json:"tool"`
|
||
Success bool `json:"success"`
|
||
Action string `json:"action"`
|
||
Domain string `json:"domain,omitempty"`
|
||
Packs []string `json:"packs,omitempty"`
|
||
All bool `json:"all,omitempty"`
|
||
Message string `json:"message,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
ErrorCode string `json:"error_code,omitempty"`
|
||
}
|
||
|
||
// BuildAddView 生成 context_tools_add 的结构化卡片。
|
||
func BuildAddView(payload ContextToolsAddPayload, observation string) ContextResultView {
|
||
status := statusFromSuccess(payload.Success)
|
||
summary := buildAddSummary(payload)
|
||
detailLines := buildAddDetailLines(payload)
|
||
item := BuildItem(
|
||
fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域"),
|
||
summary,
|
||
buildAddTags(payload),
|
||
detailLines,
|
||
map[string]any{
|
||
"action": payload.Action,
|
||
"domain": strings.TrimSpace(payload.Domain),
|
||
"mode": strings.TrimSpace(payload.Mode),
|
||
},
|
||
)
|
||
|
||
sections := []map[string]any{
|
||
buildContextCalloutSection(
|
||
fallbackText(buildAddCalloutTitle(payload), "工具域变更"),
|
||
summary,
|
||
toneFromSuccess(payload.Success),
|
||
detailLines,
|
||
),
|
||
BuildKVSection("当前工具区参数", []KVField{
|
||
BuildKVField("工具域", fallbackText(ResolveDomainLabelCN(payload.Domain), "未指定")),
|
||
BuildKVField("工具包", buildAddPackField(payload)),
|
||
BuildKVField("注入模式", fallbackText(ResolveModeLabelCN(payload.Mode), "未指定")),
|
||
BuildKVField("清空全部", "否"),
|
||
BuildKVField("动作", fallbackText(ResolveActionLabelCN(payload.Action), strings.TrimSpace(payload.Action))),
|
||
}),
|
||
BuildItemsSection("变更摘要", "", []ItemView{item}),
|
||
}
|
||
if !payload.Success {
|
||
appendErrorSection(§ions, payload.Error, payload.ErrorCode)
|
||
}
|
||
|
||
return ContextResultView{
|
||
ViewType: ViewTypeContextResult,
|
||
Version: ViewVersionContextResult,
|
||
Collapsed: map[string]any{
|
||
"title": buildAddCollapsedTitle(payload),
|
||
"subtitle": summary,
|
||
"status": status,
|
||
"status_label": resolveStatusLabelCN(status),
|
||
"metrics": buildMetrics(
|
||
BuildMetric("域", fallbackText(shortDomainLabel(payload.Domain), "未指定")),
|
||
BuildMetric("包", fmt.Sprintf("%d 个", len(payload.Packs))),
|
||
BuildMetric("模式", fallbackText(ResolveModeLabelCN(payload.Mode), "未指定")),
|
||
),
|
||
},
|
||
Expanded: map[string]any{
|
||
"items": []map[string]any{item.Map()},
|
||
"sections": sections,
|
||
"raw_text": observation,
|
||
"machine_payload": buildAddMachinePayload(payload),
|
||
},
|
||
}
|
||
}
|
||
|
||
// BuildRemoveView 生成 context_tools_remove 的结构化卡片。
|
||
func BuildRemoveView(payload ContextToolsRemovePayload, observation string) ContextResultView {
|
||
status := statusFromSuccess(payload.Success)
|
||
summary := buildRemoveSummary(payload)
|
||
detailLines := buildRemoveDetailLines(payload)
|
||
item := BuildItem(
|
||
buildRemoveItemTitle(payload),
|
||
summary,
|
||
buildRemoveTags(payload),
|
||
detailLines,
|
||
map[string]any{
|
||
"action": payload.Action,
|
||
"domain": strings.TrimSpace(payload.Domain),
|
||
"all": payload.All,
|
||
},
|
||
)
|
||
|
||
sections := []map[string]any{
|
||
buildContextCalloutSection(
|
||
fallbackText(buildRemoveCalloutTitle(payload), "工具域变更"),
|
||
summary,
|
||
toneFromSuccess(payload.Success),
|
||
detailLines,
|
||
),
|
||
BuildKVSection("当前工具区参数", []KVField{
|
||
BuildKVField("工具域", buildRemoveDomainField(payload)),
|
||
BuildKVField("工具包", buildRemovePackField(payload)),
|
||
BuildKVField("注入模式", "不适用"),
|
||
BuildKVField("清空全部", formatBoolCN(payload.All)),
|
||
BuildKVField("动作", fallbackText(ResolveActionLabelCN(payload.Action), strings.TrimSpace(payload.Action))),
|
||
}),
|
||
BuildItemsSection("变更摘要", "", []ItemView{item}),
|
||
}
|
||
if !payload.Success {
|
||
appendErrorSection(§ions, payload.Error, payload.ErrorCode)
|
||
}
|
||
|
||
return ContextResultView{
|
||
ViewType: ViewTypeContextResult,
|
||
Version: ViewVersionContextResult,
|
||
Collapsed: map[string]any{
|
||
"title": buildRemoveCollapsedTitle(payload),
|
||
"subtitle": summary,
|
||
"status": status,
|
||
"status_label": resolveStatusLabelCN(status),
|
||
"metrics": buildMetrics(
|
||
BuildMetric("范围", buildRemoveMetricScope(payload)),
|
||
BuildMetric("包", fmt.Sprintf("%d 个", len(payload.Packs))),
|
||
BuildMetric("动作", fallbackText(ResolveActionLabelCN(payload.Action), strings.TrimSpace(payload.Action))),
|
||
),
|
||
},
|
||
Expanded: map[string]any{
|
||
"items": []map[string]any{item.Map()},
|
||
"sections": sections,
|
||
"raw_text": observation,
|
||
"machine_payload": buildRemoveMachinePayload(payload),
|
||
},
|
||
}
|
||
}
|
||
|
||
func ResolveDomainLabelCN(domain string) string {
|
||
switch strings.ToLower(strings.TrimSpace(domain)) {
|
||
case "schedule":
|
||
return "排程工具域"
|
||
case "taskclass":
|
||
return "任务类工具域"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func ResolvePackLabelCN(pack string) string {
|
||
switch strings.ToLower(strings.TrimSpace(pack)) {
|
||
case "core":
|
||
return "固定 core"
|
||
case "mutation":
|
||
return "排程改写"
|
||
case "analyze":
|
||
return "健康分析"
|
||
case "detail_read":
|
||
return "明细读取"
|
||
case "deep_analyze":
|
||
return "深度分析"
|
||
case "queue":
|
||
return "队列微调"
|
||
case "web":
|
||
return "网页能力"
|
||
default:
|
||
return strings.TrimSpace(pack)
|
||
}
|
||
}
|
||
|
||
func FormatPacksCN(packs []string) string {
|
||
if len(packs) == 0 {
|
||
return "无"
|
||
}
|
||
parts := make([]string, 0, len(packs))
|
||
for _, pack := range packs {
|
||
label := strings.TrimSpace(ResolvePackLabelCN(pack))
|
||
if label == "" {
|
||
continue
|
||
}
|
||
parts = append(parts, label)
|
||
}
|
||
if len(parts) == 0 {
|
||
return "无"
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
func ResolveModeLabelCN(mode string) string {
|
||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||
case "replace":
|
||
return "替换"
|
||
case "merge":
|
||
return "合并"
|
||
default:
|
||
return strings.TrimSpace(mode)
|
||
}
|
||
}
|
||
|
||
func ResolveActionLabelCN(action string) string {
|
||
switch strings.ToLower(strings.TrimSpace(action)) {
|
||
case "activate":
|
||
return "激活"
|
||
case "deactivate":
|
||
return "移除域"
|
||
case "deactivate_packs":
|
||
return "移除包"
|
||
case "clear_all":
|
||
return "清空全部"
|
||
case "reject":
|
||
return "拒绝"
|
||
default:
|
||
return strings.TrimSpace(action)
|
||
}
|
||
}
|
||
|
||
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, summary string, items []ItemView) map[string]any {
|
||
normalized := make([]map[string]any, 0, len(items))
|
||
for _, item := range items {
|
||
normalized = append(normalized, item.Map())
|
||
}
|
||
return map[string]any{
|
||
"type": "items",
|
||
"title": strings.TrimSpace(title),
|
||
"summary": strings.TrimSpace(summary),
|
||
"items": normalized,
|
||
}
|
||
}
|
||
|
||
func BuildKVSection(title string, fields []KVField) map[string]any {
|
||
normalized := 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
|
||
}
|
||
normalized = append(normalized, map[string]any{
|
||
"label": label,
|
||
"value": value,
|
||
})
|
||
}
|
||
return map[string]any{
|
||
"type": "kv",
|
||
"title": strings.TrimSpace(title),
|
||
"fields": normalized,
|
||
}
|
||
}
|
||
|
||
func buildContextCalloutSection(title string, summary string, tone string, detailLines []string) map[string]any {
|
||
return map[string]any{
|
||
"type": "callout",
|
||
"title": strings.TrimSpace(title),
|
||
"summary": strings.TrimSpace(summary),
|
||
"tone": strings.TrimSpace(tone),
|
||
"detail_lines": normalizeStringSlice(detailLines),
|
||
}
|
||
}
|
||
|
||
func appendErrorSection(target *[]map[string]any, reason string, errorCode string) {
|
||
lines := make([]string, 0, 2)
|
||
if strings.TrimSpace(reason) != "" {
|
||
lines = append(lines, strings.TrimSpace(reason))
|
||
}
|
||
if strings.TrimSpace(errorCode) != "" {
|
||
lines = append(lines, fmt.Sprintf("错误码:%s", strings.TrimSpace(errorCode)))
|
||
}
|
||
*target = append(*target, buildContextCalloutSection("失败原因", fallbackText(reason, "工具调用失败"), "danger", lines))
|
||
}
|
||
|
||
func buildAddCollapsedTitle(payload ContextToolsAddPayload) string {
|
||
if !payload.Success {
|
||
return "激活工具域失败"
|
||
}
|
||
label := ResolveDomainLabelCN(payload.Domain)
|
||
if label == "" {
|
||
return "已激活工具域"
|
||
}
|
||
return fmt.Sprintf("已激活%s", label)
|
||
}
|
||
|
||
func buildRemoveCollapsedTitle(payload ContextToolsRemovePayload) string {
|
||
if !payload.Success {
|
||
return "移除工具域失败"
|
||
}
|
||
if payload.All {
|
||
return "已清空业务工具域"
|
||
}
|
||
if len(payload.Packs) > 0 {
|
||
return "已移除工具包"
|
||
}
|
||
label := ResolveDomainLabelCN(payload.Domain)
|
||
if label == "" {
|
||
return "已移除工具域"
|
||
}
|
||
return fmt.Sprintf("已移除%s", label)
|
||
}
|
||
|
||
func buildAddCalloutTitle(payload ContextToolsAddPayload) string {
|
||
if payload.Success {
|
||
return "动态工具区已更新"
|
||
}
|
||
return "激活失败"
|
||
}
|
||
|
||
func buildRemoveCalloutTitle(payload ContextToolsRemovePayload) string {
|
||
if payload.Success {
|
||
return "动态工具区已更新"
|
||
}
|
||
return "移除失败"
|
||
}
|
||
|
||
func buildAddSummary(payload ContextToolsAddPayload) string {
|
||
if !payload.Success {
|
||
return fallbackText(payload.Error, "激活工具域失败")
|
||
}
|
||
domainLabel := fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域")
|
||
packsText := formatPackFieldText(payload.Packs)
|
||
modeLabel := fallbackText(ResolveModeLabelCN(payload.Mode), "替换")
|
||
if len(payload.Packs) == 0 {
|
||
return fmt.Sprintf("%s已激活,模式=%s,仅保留固定 core。", domainLabel, modeLabel)
|
||
}
|
||
return fmt.Sprintf("%s已激活,模式=%s,启用 %s。", domainLabel, modeLabel, packsText)
|
||
}
|
||
|
||
func buildRemoveSummary(payload ContextToolsRemovePayload) string {
|
||
if !payload.Success {
|
||
return fallbackText(payload.Error, "移除工具域失败")
|
||
}
|
||
if payload.All {
|
||
return "已清空全部业务工具域,仅保留 context 管理工具。"
|
||
}
|
||
domainLabel := fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域")
|
||
if len(payload.Packs) > 0 {
|
||
return fmt.Sprintf("已从%s移除 %s。", domainLabel, FormatPacksCN(payload.Packs))
|
||
}
|
||
return fmt.Sprintf("已移除%s。", domainLabel)
|
||
}
|
||
|
||
func buildAddDetailLines(payload ContextToolsAddPayload) []string {
|
||
lines := []string{
|
||
fmt.Sprintf("工具域:%s", fallbackText(ResolveDomainLabelCN(payload.Domain), "未指定")),
|
||
fmt.Sprintf("工具包:%s", buildAddPackField(payload)),
|
||
fmt.Sprintf("注入模式:%s", fallbackText(ResolveModeLabelCN(payload.Mode), "未指定")),
|
||
}
|
||
if strings.TrimSpace(payload.Message) != "" {
|
||
lines = append(lines, strings.TrimSpace(payload.Message))
|
||
}
|
||
if !payload.Success && strings.TrimSpace(payload.Error) != "" {
|
||
lines = append(lines, fmt.Sprintf("失败原因:%s", strings.TrimSpace(payload.Error)))
|
||
}
|
||
return lines
|
||
}
|
||
|
||
func buildRemoveDetailLines(payload ContextToolsRemovePayload) []string {
|
||
lines := []string{
|
||
fmt.Sprintf("工具域:%s", buildRemoveDomainField(payload)),
|
||
fmt.Sprintf("工具包:%s", buildRemovePackField(payload)),
|
||
fmt.Sprintf("清空全部:%s", formatBoolCN(payload.All)),
|
||
}
|
||
if strings.TrimSpace(payload.Message) != "" {
|
||
lines = append(lines, strings.TrimSpace(payload.Message))
|
||
}
|
||
if !payload.Success && strings.TrimSpace(payload.Error) != "" {
|
||
lines = append(lines, fmt.Sprintf("失败原因:%s", strings.TrimSpace(payload.Error)))
|
||
}
|
||
return lines
|
||
}
|
||
|
||
func buildAddTags(payload ContextToolsAddPayload) []string {
|
||
tags := []string{
|
||
fallbackText(ResolveActionLabelCN(payload.Action), "激活"),
|
||
}
|
||
if modeLabel := strings.TrimSpace(ResolveModeLabelCN(payload.Mode)); modeLabel != "" {
|
||
tags = append(tags, modeLabel)
|
||
}
|
||
if len(payload.Packs) > 0 {
|
||
tags = append(tags, fmt.Sprintf("%d 个包", len(payload.Packs)))
|
||
}
|
||
return normalizeStringSlice(tags)
|
||
}
|
||
|
||
func buildRemoveTags(payload ContextToolsRemovePayload) []string {
|
||
tags := []string{
|
||
fallbackText(ResolveActionLabelCN(payload.Action), "移除"),
|
||
}
|
||
if payload.All {
|
||
tags = append(tags, "全部")
|
||
}
|
||
if len(payload.Packs) > 0 {
|
||
tags = append(tags, fmt.Sprintf("%d 个包", len(payload.Packs)))
|
||
}
|
||
return normalizeStringSlice(tags)
|
||
}
|
||
|
||
func buildRemoveItemTitle(payload ContextToolsRemovePayload) string {
|
||
if payload.All {
|
||
return "全部业务工具域"
|
||
}
|
||
return fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域")
|
||
}
|
||
|
||
func buildRemoveDomainField(payload ContextToolsRemovePayload) string {
|
||
if payload.All {
|
||
return "全部业务工具域"
|
||
}
|
||
return fallbackText(ResolveDomainLabelCN(payload.Domain), "未指定")
|
||
}
|
||
|
||
func buildRemoveMetricScope(payload ContextToolsRemovePayload) string {
|
||
if payload.All {
|
||
return "全部"
|
||
}
|
||
return fallbackText(shortDomainLabel(payload.Domain), "单域")
|
||
}
|
||
|
||
func buildAddMachinePayload(payload ContextToolsAddPayload) map[string]any {
|
||
return map[string]any{
|
||
"tool": payload.Tool,
|
||
"success": payload.Success,
|
||
"action": strings.TrimSpace(payload.Action),
|
||
"domain": strings.TrimSpace(payload.Domain),
|
||
"packs": append([]string(nil), payload.Packs...),
|
||
"mode": strings.TrimSpace(payload.Mode),
|
||
"message": strings.TrimSpace(payload.Message),
|
||
"error": strings.TrimSpace(payload.Error),
|
||
"error_code": strings.TrimSpace(payload.ErrorCode),
|
||
}
|
||
}
|
||
|
||
func buildRemoveMachinePayload(payload ContextToolsRemovePayload) map[string]any {
|
||
return map[string]any{
|
||
"tool": payload.Tool,
|
||
"success": payload.Success,
|
||
"action": strings.TrimSpace(payload.Action),
|
||
"domain": strings.TrimSpace(payload.Domain),
|
||
"packs": append([]string(nil), payload.Packs...),
|
||
"all": payload.All,
|
||
"message": strings.TrimSpace(payload.Message),
|
||
"error": strings.TrimSpace(payload.Error),
|
||
"error_code": strings.TrimSpace(payload.ErrorCode),
|
||
}
|
||
}
|
||
|
||
func buildMetrics(metrics ...MetricField) []map[string]any {
|
||
normalized := 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
|
||
}
|
||
normalized = append(normalized, map[string]any{
|
||
"label": label,
|
||
"value": value,
|
||
})
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
func toneFromSuccess(success bool) string {
|
||
if success {
|
||
return "info"
|
||
}
|
||
return "danger"
|
||
}
|
||
|
||
func statusFromSuccess(success bool) string {
|
||
if success {
|
||
return StatusDone
|
||
}
|
||
return StatusFailed
|
||
}
|
||
|
||
func resolveStatusLabelCN(status string) string {
|
||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||
case StatusDone:
|
||
return "已完成"
|
||
default:
|
||
return "失败"
|
||
}
|
||
}
|
||
|
||
func shortDomainLabel(domain string) string {
|
||
switch strings.ToLower(strings.TrimSpace(domain)) {
|
||
case "schedule":
|
||
return "排程"
|
||
case "taskclass":
|
||
return "任务类"
|
||
default:
|
||
return strings.TrimSpace(domain)
|
||
}
|
||
}
|
||
|
||
func formatPackFieldText(packs []string) string {
|
||
if len(packs) == 0 {
|
||
return "无"
|
||
}
|
||
return FormatPacksCN(packs)
|
||
}
|
||
|
||
func buildAddPackField(payload ContextToolsAddPayload) string {
|
||
if len(payload.Packs) == 0 {
|
||
return "仅固定 core"
|
||
}
|
||
return FormatPacksCN(payload.Packs)
|
||
}
|
||
|
||
func buildRemovePackField(payload ContextToolsRemovePayload) string {
|
||
if len(payload.Packs) == 0 {
|
||
if payload.All {
|
||
return "全部清空"
|
||
}
|
||
if strings.EqualFold(strings.TrimSpace(payload.Action), "deactivate") {
|
||
return "未指定(按整域处理)"
|
||
}
|
||
return "无"
|
||
}
|
||
return FormatPacksCN(payload.Packs)
|
||
}
|
||
|
||
func formatBoolCN(value bool) string {
|
||
if value {
|
||
return "是"
|
||
}
|
||
return "否"
|
||
}
|
||
|
||
func fallbackText(text string, fallback string) string {
|
||
if strings.TrimSpace(text) == "" {
|
||
return strings.TrimSpace(fallback)
|
||
}
|
||
return strings.TrimSpace(text)
|
||
}
|
||
|
||
func normalizeStringSlice(values []string) []string {
|
||
if len(values) == 0 {
|
||
return nil
|
||
}
|
||
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 nil
|
||
}
|
||
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] = value
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (view ItemView) Map() map[string]any {
|
||
item := map[string]any{
|
||
"title": strings.TrimSpace(view.Title),
|
||
"subtitle": strings.TrimSpace(view.Subtitle),
|
||
"tags": normalizeStringSlice(view.Tags),
|
||
"detail_lines": normalizeStringSlice(view.DetailLines),
|
||
}
|
||
if len(view.Meta) > 0 {
|
||
item["meta"] = cloneAnyMap(view.Meta)
|
||
}
|
||
return item
|
||
}
|