Version: 0.9.75.dev.260505
后端: 1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent - 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge - 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段 - 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流 - 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
This commit is contained in:
397
backend/services/agent/tools/taskclass_result/common.go
Normal file
397
backend/services/agent/tools/taskclass_result/common.go
Normal file
@@ -0,0 +1,397 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
114
backend/services/agent/tools/taskclass_result/types.go
Normal file
114
backend/services/agent/tools/taskclass_result/types.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package taskclass_result
|
||||
|
||||
const (
|
||||
// ViewTypeWriteResult 固定为任务类写入结果卡片的前端识别类型。
|
||||
ViewTypeWriteResult = "taskclass.write_result"
|
||||
|
||||
// ViewVersionWriteResult 固定为当前任务类写入结果结构版本。
|
||||
ViewVersionWriteResult = 1
|
||||
|
||||
// 这里不依赖父包状态常量,避免子包反向 import tools 形成循环依赖。
|
||||
StatusDone = "done"
|
||||
StatusFailed = "failed"
|
||||
StatusBlocked = "blocked"
|
||||
)
|
||||
|
||||
// WriteResultView 是子包暴露给父包 adapter 的纯展示结构。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只承载 view_type / version / collapsed / expanded 四段展示数据;
|
||||
// 2. 不负责 ToolExecutionResult、SSE、timeline 等父包协议;
|
||||
// 3. collapsed / expanded 继续保留 map 形态,便于父包直接桥接。
|
||||
type WriteResultView struct {
|
||||
ViewType string `json:"view_type"`
|
||||
Version int `json:"version"`
|
||||
Collapsed map[string]any `json:"collapsed"`
|
||||
Expanded map[string]any `json:"expanded"`
|
||||
}
|
||||
|
||||
// MetricField 是 collapsed.metrics 的轻量键值结构。
|
||||
type MetricField struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// KVField 是 expanded.kv section 的轻量键值结构。
|
||||
type KVField struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ItemView 是 expanded.items / section.items 的通用结构。
|
||||
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"`
|
||||
}
|
||||
|
||||
// UpsertResult 承载写入 observation 里可稳定提取的结果字段。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只描述 upsert_task_class 的结果,不承载请求参数;
|
||||
// 2. ValidationIssues 仅用于展示校验失败原因,不负责重新校验;
|
||||
// 3. Error / ErrorCode 保持和 observation 一致,避免展示层发明新语义。
|
||||
type UpsertResult struct {
|
||||
Tool string
|
||||
Success bool
|
||||
TaskClassID int
|
||||
Created bool
|
||||
Error string
|
||||
ErrorCode string
|
||||
ValidationOK bool
|
||||
ValidationIssues []string
|
||||
}
|
||||
|
||||
// RequestSummary 描述写入请求中适合前端展示的稳定字段摘要。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只保留卡片展示稳定需要的信息,不回传完整原始 args;
|
||||
// 2. RequestedID 表示调用方请求更新的任务类 ID,不等同于持久化后的真实 ID;
|
||||
// 3. Items 已经是展示层可直接消费的扁平摘要,不再承担业务校验职责。
|
||||
type RequestSummary struct {
|
||||
RequestedID int
|
||||
Name string
|
||||
Mode string
|
||||
StartDate string
|
||||
EndDate string
|
||||
SubjectType string
|
||||
DifficultyLevel string
|
||||
CognitiveIntensity string
|
||||
TotalSlots int
|
||||
AllowFillerCourse bool
|
||||
Strategy string
|
||||
ExcludedSlots []int
|
||||
ExcludedDaysOfWeek []int
|
||||
Source string
|
||||
Items []TaskClassItemSummary
|
||||
}
|
||||
|
||||
// TaskClassItemSummary 描述单个任务项的展示摘要。
|
||||
type TaskClassItemSummary struct {
|
||||
ID int
|
||||
Order int
|
||||
Content string
|
||||
EmbeddedWeek int
|
||||
EmbeddedDay int
|
||||
EmbeddedSectionFrom int
|
||||
EmbeddedSectionTo int
|
||||
}
|
||||
|
||||
// BuildUpsertTaskClassViewInput 是 upsert_task_class 卡片 builder 的输入。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. Observation 必须原样传入,供 raw_text 保留原始结果;
|
||||
// 2. MachinePayload 只作为调试/后续交互的隐藏字段,不参与标题摘要计算;
|
||||
// 3. Status 由父包 adapter 传入,子包只负责标准化,不重新推断工具执行链路。
|
||||
type BuildUpsertTaskClassViewInput struct {
|
||||
Status string
|
||||
Observation string
|
||||
Result UpsertResult
|
||||
Request RequestSummary
|
||||
MachinePayload map[string]any
|
||||
}
|
||||
242
backend/services/agent/tools/taskclass_result/write.go
Normal file
242
backend/services/agent/tools/taskclass_result/write.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package taskclass_result
|
||||
|
||||
import "fmt"
|
||||
|
||||
// BuildUpsertTaskClassView 把 upsert_task_class 的稳定结果摘要转成任务类写入卡片。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先基于父包传入的 Status/Result 生成折叠态标题、摘要和稳定指标,保证成功/失败都能快速扫读;
|
||||
// 2. 再把任务类字段、配置、任务项列表和失败原因拆成 kv/items/callout section,避免前端继续回退 raw_text;
|
||||
// 3. raw_text 与 machine_payload 始终保留,便于模型链路、调试链路和后续交互共用同一份 observation 语义。
|
||||
func BuildUpsertTaskClassView(input BuildUpsertTaskClassViewInput) WriteResultView {
|
||||
status := normalizeStatus(input.Status)
|
||||
if status == "" {
|
||||
if input.Result.Success {
|
||||
status = StatusDone
|
||||
} else {
|
||||
status = StatusFailed
|
||||
}
|
||||
}
|
||||
|
||||
items := buildTaskClassItemViews(input.Request.Items)
|
||||
sections := buildUpsertSections(input.Result, input.Request, items, status)
|
||||
|
||||
return buildWriteResultView(
|
||||
status,
|
||||
buildUpsertTitle(input.Result, status),
|
||||
buildUpsertSubtitle(input.Result, input.Request, status),
|
||||
buildUpsertMetrics(input.Result, input.Request),
|
||||
items,
|
||||
sections,
|
||||
input.Observation,
|
||||
input.MachinePayload,
|
||||
)
|
||||
}
|
||||
|
||||
func buildUpsertTitle(result UpsertResult, status string) string {
|
||||
if normalizeStatus(status) != StatusDone {
|
||||
return "任务类写入失败"
|
||||
}
|
||||
if result.Created {
|
||||
return "任务类已创建"
|
||||
}
|
||||
return "任务类已更新"
|
||||
}
|
||||
|
||||
func buildUpsertSubtitle(result UpsertResult, request RequestSummary, status string) string {
|
||||
name := fallbackText(request.Name, "未命名任务类")
|
||||
itemCount := len(request.Items)
|
||||
if normalizeStatus(status) == StatusDone {
|
||||
action := "更新"
|
||||
if result.Created {
|
||||
action = "创建"
|
||||
}
|
||||
return fmt.Sprintf("已%s「%s」,共 %d 项任务", action, name, itemCount)
|
||||
}
|
||||
|
||||
if len(result.ValidationIssues) > 0 {
|
||||
return fmt.Sprintf("「%s」校验未通过:%s", name, result.ValidationIssues[0])
|
||||
}
|
||||
if result.Error != "" {
|
||||
return fmt.Sprintf("「%s」写入失败:%s", name, result.Error)
|
||||
}
|
||||
return fmt.Sprintf("「%s」写入失败,请查看详情", name)
|
||||
}
|
||||
|
||||
func buildUpsertMetrics(result UpsertResult, request RequestSummary) []MetricField {
|
||||
action := "更新"
|
||||
if result.Created {
|
||||
action = "创建"
|
||||
}
|
||||
if !result.Success && request.RequestedID == 0 {
|
||||
action = "创建尝试"
|
||||
}
|
||||
if !result.Success && request.RequestedID > 0 {
|
||||
action = "更新尝试"
|
||||
}
|
||||
|
||||
return []MetricField{
|
||||
buildMetric("任务类数量", "1 个"),
|
||||
buildMetric("任务项数量", fmt.Sprintf("%d 项", len(request.Items))),
|
||||
buildMetric("来源", formatSourceCN(request.Source)),
|
||||
buildMetric("写入方式", action),
|
||||
}
|
||||
}
|
||||
|
||||
func buildUpsertSections(
|
||||
result UpsertResult,
|
||||
request RequestSummary,
|
||||
items []ItemView,
|
||||
status string,
|
||||
) []map[string]any {
|
||||
sections := []map[string]any{
|
||||
buildResultCallout(result, request, status),
|
||||
buildKVSection("任务类字段", buildTaskClassFields(result, request)),
|
||||
buildKVSection("排程配置", buildTaskClassConfigFields(request)),
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
sections = append(sections, buildItemsSection("任务项列表", items))
|
||||
} else {
|
||||
sections = append(sections, buildCalloutSection(
|
||||
"任务项列表",
|
||||
"当前没有可展示的任务项。",
|
||||
"info",
|
||||
[]string{"如果这是一次失败写入,请优先检查 task_class.items 或顶层 items 入参是否完整。"},
|
||||
))
|
||||
}
|
||||
|
||||
if len(result.ValidationIssues) > 0 {
|
||||
sections = append(sections, buildCalloutSection(
|
||||
"校验失败原因",
|
||||
"请求参数未通过后端校验。",
|
||||
"warning",
|
||||
normalizeStringSlice(result.ValidationIssues),
|
||||
))
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func buildResultCallout(result UpsertResult, request RequestSummary, status string) map[string]any {
|
||||
if normalizeStatus(status) == StatusDone {
|
||||
action := "更新"
|
||||
if result.Created {
|
||||
action = "创建"
|
||||
}
|
||||
detailLines := []string{
|
||||
fmt.Sprintf("任务类:%s", fallbackText(request.Name, "未命名任务类")),
|
||||
fmt.Sprintf("任务类 ID:%d", resolveDisplayTaskClassID(result, request)),
|
||||
fmt.Sprintf("任务项数量:%d 项", len(request.Items)),
|
||||
}
|
||||
return buildCalloutSection(
|
||||
"写入结果",
|
||||
fmt.Sprintf("已%s任务类,结果可直接用于后续排程。", action),
|
||||
"success",
|
||||
detailLines,
|
||||
)
|
||||
}
|
||||
|
||||
reason := result.Error
|
||||
if len(result.ValidationIssues) > 0 {
|
||||
reason = result.ValidationIssues[0]
|
||||
}
|
||||
if reason == "" {
|
||||
reason = "写入流程未返回明确失败原因,请查看原始 observation。"
|
||||
}
|
||||
return buildCalloutSection(
|
||||
"写入失败",
|
||||
reason,
|
||||
"danger",
|
||||
[]string{
|
||||
fmt.Sprintf("来源:%s", formatSourceCN(request.Source)),
|
||||
fmt.Sprintf("任务类:%s", fallbackText(request.Name, "未命名任务类")),
|
||||
fmt.Sprintf("任务项数量:%d 项", len(request.Items)),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func buildTaskClassFields(result UpsertResult, request RequestSummary) []KVField {
|
||||
return []KVField{
|
||||
buildKVField("任务类 ID", fmt.Sprintf("%d", resolveDisplayTaskClassID(result, request))),
|
||||
buildKVField("名称", fallbackText(request.Name, "未命名任务类")),
|
||||
buildKVField("模式", formatModeCN(request.Mode)),
|
||||
buildKVField("日期范围", formatDateRangeCN(request.StartDate, request.EndDate)),
|
||||
buildKVField("学科类型", formatSubjectTypeCN(request.SubjectType)),
|
||||
buildKVField("难度等级", formatLevelCN(request.DifficultyLevel)),
|
||||
buildKVField("认知强度", formatLevelCN(request.CognitiveIntensity)),
|
||||
buildKVField("来源", formatSourceCN(request.Source)),
|
||||
}
|
||||
}
|
||||
|
||||
func buildTaskClassConfigFields(request RequestSummary) []KVField {
|
||||
return []KVField{
|
||||
buildKVField("总节数", fmt.Sprintf("%d", request.TotalSlots)),
|
||||
buildKVField("允许补位课程", formatBoolCN(request.AllowFillerCourse)),
|
||||
buildKVField("推进策略", formatStrategyCN(request.Strategy)),
|
||||
buildKVField("排除半天块", formatIntListCN(request.ExcludedSlots, "无", func(value int) string {
|
||||
return fmt.Sprintf("第%d块", value)
|
||||
})),
|
||||
buildKVField("排除星期", formatIntListCN(request.ExcludedDaysOfWeek, "无", formatWeekdayCN)),
|
||||
}
|
||||
}
|
||||
|
||||
func buildTaskClassItemViews(items []TaskClassItemSummary) []ItemView {
|
||||
if len(items) == 0 {
|
||||
return make([]ItemView, 0)
|
||||
}
|
||||
out := make([]ItemView, 0, len(items))
|
||||
for _, item := range items {
|
||||
detailLines := []string{
|
||||
"内容:" + fallbackText(item.Content, "未填写内容"),
|
||||
"嵌入时间:" + formatEmbeddedTimeCN(item),
|
||||
}
|
||||
if item.ID > 0 {
|
||||
detailLines = append(detailLines, fmt.Sprintf("任务项 ID:%d", item.ID))
|
||||
}
|
||||
out = append(out, buildItem(
|
||||
truncateText(item.Content, 28),
|
||||
fmt.Sprintf("第 %d 项", maxInt(item.Order, 0)),
|
||||
buildTaskClassItemTags(item),
|
||||
detailLines,
|
||||
map[string]any{
|
||||
"id": item.ID,
|
||||
"order": item.Order,
|
||||
"embedded_week": item.EmbeddedWeek,
|
||||
"embedded_day": item.EmbeddedDay,
|
||||
"section_from": item.EmbeddedSectionFrom,
|
||||
"section_to": item.EmbeddedSectionTo,
|
||||
},
|
||||
))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildTaskClassItemTags(item TaskClassItemSummary) []string {
|
||||
tags := []string{fmt.Sprintf("顺序 %d", maxInt(item.Order, 0))}
|
||||
if item.EmbeddedWeek > 0 && item.EmbeddedDay > 0 {
|
||||
tags = append(tags, formatEmbeddedTimeCN(item))
|
||||
} else {
|
||||
tags = append(tags, "未指定嵌入时间")
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func resolveDisplayTaskClassID(result UpsertResult, request RequestSummary) int {
|
||||
if result.TaskClassID > 0 {
|
||||
return result.TaskClassID
|
||||
}
|
||||
return request.RequestedID
|
||||
}
|
||||
|
||||
func maxInt(values ...int) int {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
best := values[0]
|
||||
for _, value := range values[1:] {
|
||||
if value > best {
|
||||
best = value
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
Reference in New Issue
Block a user