Files
smartmate/backend/newAgent/shared/deadline.go
LoveLosita 73ab0f43aa Version: 0.9.34.dev.260421
后端:
1. 旧 Agent 管线(agent/)全面下线,共享逻辑迁移至 newAgent/
- 删除 backend/agent/ 整个目录(44 个 Go 文件),5 条旧专用流程已由 newAgent 统一 graph 取代
- 共享逻辑迁入 newAgent/:clone(shared/clone.go)、时间解析(shared/deadline.go)、优先级常量(shared/task_priority.go)、TaskQuery 类型(model/taskquery_types.go)、SystemPrompt(prompt/system.go)、Usage 合并(stream/usage.go)
2. service 层清除 agent/ 全部依赖
- 删除 4 个旧流程入口文件(agent_route / agent_quick_note / agent_schedule_plan / agent_schedule_refine)
- agent_task_query.go 删除 runTaskQueryFlow,参数类型切到 newagentmodel
- agent.go / agent_newagent.go / agent_schedule_preview.go / agent_schedule_state.go / cmd/start.go / quicknote.go:agent* 引用全部替换为 newagent*
3. 流式降级回退路径内联到 service 层(agent_stream_fallback.go),消除最后一条 agent/chat 依赖

前端:
1. ScheduleFineTuneModal 幂等键追加 classId 后缀,修复多任务类并行保存 key 重复
2026-04-21 20:10:16 +08:00

367 lines
10 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentshared
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
var (
deadlineLayouts = []string{
time.RFC3339,
"2006-01-02T15:04",
"2006-01-02 15:04:05",
"2006-01-02 15:04",
"2006/01/02 15:04:05",
"2006/01/02 15:04",
"2006.01.02 15:04:05",
"2006.01.02 15:04",
"2006-01-02",
"2006/01/02",
"2006.01.02",
}
deadlineDateOnlyLayouts = map[string]struct{}{
"2006-01-02": {},
"2006/01/02": {},
"2006.01.02": {},
}
clockHMRegex = regexp.MustCompile(`(\d{1,2})\s*[:]\s*(\d{1,2})`)
clockCNRegex = regexp.MustCompile(`(\d{1,2})\s*点\s*(半|(\d{1,2})\s*分?)?`)
ymdRegex = regexp.MustCompile(`(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]?`)
mdRegex = regexp.MustCompile(`(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]?`)
dateSepRegex = regexp.MustCompile(`\d{1,4}\s*[-/.]\s*\d{1,2}(\s*[-/.]\s*\d{1,2})?`)
weekdayRegex = regexp.MustCompile(`(下周|下星期|下礼拜|本周|这周|本星期|这星期|周|星期|礼拜)([一二三四五六日天])`)
relativeTokens = []string{
"今天", "今日", "今晚", "今早", "今晨", "明天", "明日", "后天", "大后天", "昨天", "昨日",
"早上", "早晨", "上午", "中午", "下午", "晚上", "傍晚", "夜里", "凌晨",
}
)
// ParseOptionalDeadline 解析工具输入中的可选截止时间。
func ParseOptionalDeadline(raw string) (*time.Time, error) {
value := normalizeDeadlineInput(raw)
if value == "" {
return nil, nil
}
deadline, hasHint, err := parseDeadlineFromText(value, NowToMinute())
if err != nil {
return nil, err
}
if deadline == nil {
if !hasHint {
return nil, fmt.Errorf("deadline_at 格式不支持: %s", value)
}
return nil, fmt.Errorf("deadline_at 无法解析: %s", value)
}
return deadline, nil
}
// ParseOptionalDeadlineWithNow 在给定时间基准下解析 deadline。
func ParseOptionalDeadlineWithNow(raw string, now time.Time) (*time.Time, error) {
value := normalizeDeadlineInput(raw)
if value == "" {
return nil, nil
}
deadline, _, err := parseDeadlineFromText(value, now)
if err != nil {
return nil, err
}
if deadline == nil {
return nil, fmt.Errorf("deadline_at 格式不支持: %s", value)
}
return deadline, nil
}
func parseDeadlineFromText(value string, now time.Time) (*time.Time, bool, error) {
if strings.TrimSpace(value) == "" {
return nil, false, nil
}
loc := ShanghaiLocation()
now = now.In(loc)
hasHint := hasDeadlineHint(value)
if abs, ok := tryParseAbsoluteDeadline(value, loc); ok {
return abs, true, nil
}
if rel, recognized, err := tryParseRelativeDeadline(value, now, loc); recognized {
if err != nil {
return nil, true, err
}
return rel, true, nil
}
if hasHint {
return nil, true, fmt.Errorf("deadline_at 格式不支持: %s", value)
}
return nil, false, nil
}
func normalizeDeadlineInput(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
replacer := strings.NewReplacer(
"", ":",
"", ",",
"。", ".",
" ", " ",
)
return strings.TrimSpace(replacer.Replace(trimmed))
}
func hasDeadlineHint(value string) bool {
if clockHMRegex.MatchString(value) ||
clockCNRegex.MatchString(value) ||
ymdRegex.MatchString(value) ||
mdRegex.MatchString(value) ||
dateSepRegex.MatchString(value) ||
weekdayRegex.MatchString(value) {
return true
}
for _, token := range relativeTokens {
if strings.Contains(value, token) {
return true
}
}
return false
}
func tryParseAbsoluteDeadline(value string, loc *time.Location) (*time.Time, bool) {
for _, layout := range deadlineLayouts {
var (
parsed time.Time
err error
)
if layout == time.RFC3339 {
parsed, err = time.Parse(layout, value)
if err == nil {
parsed = parsed.In(loc)
}
} else {
parsed, err = time.ParseInLocation(layout, value, loc)
}
if err != nil {
continue
}
if _, dateOnly := deadlineDateOnlyLayouts[layout]; dateOnly {
parsed = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 0, 0, loc)
} else {
parsed = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), 0, 0, loc)
}
return &parsed, true
}
return nil, false
}
func tryParseRelativeDeadline(value string, now time.Time, loc *time.Location) (*time.Time, bool, error) {
baseDate, recognized := inferBaseDate(value, now, loc)
if !recognized {
return nil, false, nil
}
hour, minute, hasExplicitClock, err := extractClock(value)
if err != nil {
return nil, true, err
}
if !hasExplicitClock {
hour, minute = defaultClockByHint(value)
}
deadline := time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), hour, minute, 0, 0, loc)
return &deadline, true, nil
}
func inferBaseDate(value string, now time.Time, loc *time.Location) (time.Time, bool) {
if matched := ymdRegex.FindStringSubmatch(value); len(matched) == 4 {
year, _ := strconv.Atoi(matched[1])
month, _ := strconv.Atoi(matched[2])
day, _ := strconv.Atoi(matched[3])
if isValidDate(year, month, day) {
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, loc), true
}
}
if matched := mdRegex.FindStringSubmatch(value); len(matched) == 3 {
month, _ := strconv.Atoi(matched[1])
day, _ := strconv.Atoi(matched[2])
year := now.Year()
if !isValidDate(year, month, day) {
return time.Time{}, false
}
candidate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, loc)
if candidate.Before(startOfDay(now)) {
year++
if !isValidDate(year, month, day) {
return time.Time{}, false
}
candidate = time.Date(year, time.Month(month), day, 0, 0, 0, 0, loc)
}
return candidate, true
}
if matched := weekdayRegex.FindStringSubmatch(value); len(matched) == 3 {
prefix := matched[1]
target, ok := toWeekday(matched[2])
if ok {
return resolveWeekdayDate(now, prefix, target), true
}
}
today := startOfDay(now)
switch {
case strings.Contains(value, "大后天"):
return today.AddDate(0, 0, 3), true
case strings.Contains(value, "后天"):
return today.AddDate(0, 0, 2), true
case strings.Contains(value, "明天") || strings.Contains(value, "明日"):
return today.AddDate(0, 0, 1), true
case strings.Contains(value, "今天") || strings.Contains(value, "今日") || strings.Contains(value, "今晚") || strings.Contains(value, "今早") || strings.Contains(value, "今晨"):
return today, true
case strings.Contains(value, "昨天") || strings.Contains(value, "昨日"):
return today.AddDate(0, 0, -1), true
default:
return time.Time{}, false
}
}
func extractClock(value string) (int, int, bool, error) {
hour := 0
minute := 0
hasClock := false
if matched := clockHMRegex.FindStringSubmatch(value); len(matched) == 3 {
h, errH := strconv.Atoi(matched[1])
m, errM := strconv.Atoi(matched[2])
if errH != nil || errM != nil {
return 0, 0, true, fmt.Errorf("deadline_at 时间解析失败: %s", value)
}
hour = h
minute = m
hasClock = true
} else if matched := clockCNRegex.FindStringSubmatch(value); len(matched) >= 2 {
h, errH := strconv.Atoi(matched[1])
if errH != nil {
return 0, 0, true, fmt.Errorf("deadline_at 时间解析失败: %s", value)
}
hour = h
minute = 0
hasClock = true
if len(matched) >= 3 {
if matched[2] == "半" {
minute = 30
} else if len(matched) >= 4 && strings.TrimSpace(matched[3]) != "" {
m, errM := strconv.Atoi(strings.TrimSpace(matched[3]))
if errM != nil {
return 0, 0, true, fmt.Errorf("deadline_at 时间解析失败: %s", value)
}
minute = m
}
}
}
if !hasClock {
return 0, 0, false, nil
}
if isPMHint(value) && hour < 12 {
hour += 12
}
if isNoonHint(value) && hour >= 1 && hour <= 10 {
hour += 12
}
if strings.Contains(value, "凌晨") && hour == 12 {
hour = 0
}
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
return 0, 0, true, fmt.Errorf("deadline_at 时间超出范围: %s", value)
}
return hour, minute, true, nil
}
func defaultClockByHint(value string) (int, int) {
switch {
case strings.Contains(value, "凌晨"):
return 1, 0
case strings.Contains(value, "早上") || strings.Contains(value, "早晨") || strings.Contains(value, "上午") || strings.Contains(value, "今早") || strings.Contains(value, "明早"):
return 9, 0
case strings.Contains(value, "中午"):
return 12, 0
case strings.Contains(value, "下午"):
return 15, 0
case strings.Contains(value, "晚上") || strings.Contains(value, "今晚") || strings.Contains(value, "傍晚") || strings.Contains(value, "夜里"):
return 20, 0
default:
return 23, 59
}
}
func isPMHint(value string) bool {
return strings.Contains(value, "下午") || strings.Contains(value, "晚上") || strings.Contains(value, "今晚") || strings.Contains(value, "傍晚")
}
func isNoonHint(value string) bool {
return strings.Contains(value, "中午")
}
func startOfDay(t time.Time) time.Time {
loc := t.Location()
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
}
func isValidDate(year, month, day int) bool {
if month < 1 || month > 12 || day < 1 || day > 31 {
return false
}
candidate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
return candidate.Year() == year && int(candidate.Month()) == month && candidate.Day() == day
}
func toWeekday(chinese string) (time.Weekday, bool) {
switch chinese {
case "一":
return time.Monday, true
case "二":
return time.Tuesday, true
case "三":
return time.Wednesday, true
case "四":
return time.Thursday, true
case "五":
return time.Friday, true
case "六":
return time.Saturday, true
case "日", "天":
return time.Sunday, true
default:
return time.Sunday, false
}
}
func resolveWeekdayDate(now time.Time, prefix string, target time.Weekday) time.Time {
today := startOfDay(now)
weekdayOffset := (int(today.Weekday()) + 6) % 7
weekStart := today.AddDate(0, 0, -weekdayOffset)
targetOffset := (int(target) + 6) % 7
candidateThisWeek := weekStart.AddDate(0, 0, targetOffset)
switch {
case strings.HasPrefix(prefix, "下"):
return candidateThisWeek.AddDate(0, 0, 7)
case strings.HasPrefix(prefix, "本"), strings.HasPrefix(prefix, "这"):
return candidateThisWeek
default:
if candidateThisWeek.Before(today) {
return candidateThisWeek.AddDate(0, 0, 7)
}
return candidateThisWeek
}
}