后端: 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 迁移面
367 lines
10 KiB
Go
367 lines
10 KiB
Go
package agentshared
|
||
|
||
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
|
||
}
|
||
}
|