后台与定时任务

两套调度共用一个原则:先落库,再排队。后台任务负责「一次性重活」(embedding、图谱、Wiki、看板执行),定时任务负责「周期性重活」(技能、AI 任务、图谱重建、向量重建)。前者用数据库唯一索引 + 内存信号量防重,后者用 NextRunAt 与 cron 表达式决定何时该跑。

入队协调器 信号量串行化 + 批内去重 先落库,再入内存队列 单消费者处理 领取时置 Running 5 类任务分支 巡检与恢复 每 2 分钟扫描一次 超过 5 分钟未更新才恢复 定时任务 Interval 或 5 段 cron 到期由 NextRunAt <= now 判定 后台任务类型:GenerateEmbedding · GenerateKnowledgeItemEmbedding · GraphExtract · KanbanTaskExecute · WikiGenerate 停止时重新排队,普通取消记失败 执行器:Skill · AiTask · GraphRebuild · EmbeddingRegenerate
两条链路共享同一套「落库 + 恢复 + 状态回写」思路:先持久化意图,再执行,最后把结果写回同一行记录。

后台任务模型

字段 / 约束说明
TaskItemTaskType · EntityId · EntityTitle · Status · ErrorMessage · Metadata · StartedAt · CompletedAt · IsDeleted
状态0 排队 · 1 运行中 · 2 已完成 · 3 失败
唯一索引IX_TaskItems_ActiveUniqueness:过滤条件为「排队或运行中且未删除」,因此同一 (TaskType, EntityId) 同时只能有一条活跃任务
索引TaskType · Status · IsDeleted

入队与去重

  1. 用 SemaphoreSlim(1, 1) 把「查重 + 建记录 + 入队」串行化,避免并发入队产生重复。
  2. 批量入队时先在批内按 (TaskType, EntityId) 去重。
  3. 数据库中已有活跃记录(排队或运行中)的目标直接跳过。
  4. 先 SaveChangesAsync 落库,再 QueueAsync 入内存队列。
  5. 消费端领取时把记录置为「运行中」并回写数据库(ClaimAsync)。
队列本身在内存里,数据库才是真实状态:进程重启后靠启动恢复 + 巡检把遗漏的任务捡回来,而不是靠队列持久化。

消费、巡检与恢复

项行为
消费者单个后台服务循环消费队列(无多线程并发消费)
启动恢复启动时先恢复遗留任务(排队 / 卡住的记录)再进入消费循环
巡检频率SweepInterval = 2 分钟
过期判定只有超过 StaleThreshold = 5 分钟未更新的记录才会被恢复——避免把正在跑的长任务误判
服务停止正在执行的任务重置为排队态、清空开始时间、错误信息「服务停止,任务已重新排队」
普通取消状态置失败,错误信息「任务被取消」
执行异常状态置失败,错误信息为异常消息
收尾finally 中无论成败都写回完成时间与更新时间

任务类型

类型做什么
GenerateEmbedding笔记向量化(见 分块与向量索引)
GenerateKnowledgeItemEmbedding文件 / 网址类知识项的向量化
GraphExtract单篇笔记的图谱抽取(见 知识图谱抽取)
KanbanTaskExecute看板任务的自动化执行(见 任务看板与自动化)
WikiGenerateWiki 整套生成,模型 id 通过任务 Metadata 传递(见 Wiki 生成)

定时任务模型与端点

字段 / 端点说明
ScheduledTaskName · Description · TaskKind · TargetId / TargetName · Parameters · ScheduleType · IntervalMinutes · CronExpression · IsEnabled · NextRunAt · LastRunAt · LastStatus · LastError · MaxRetries · RetryCount · TopicId
TaskKindSkill · AiTask · GraphRebuild · EmbeddingRegenerate
ScheduleTypeInterval(分钟间隔)或 Cron(5 段:分 时 日 月 周)
执行记录ScheduledTaskExecution:Status(Running / Success / Failed)· ErrorMessage · Result · RetryAttempt · IsManual · 起止时间
端点GET /api/scheduled-tasks · GET /api/scheduled-tasks/{id} · GET /api/scheduled-tasks/target-options · POST · PUT · DELETE · POST {id}/toggle · POST {id}/run · GET {id}/executions

下次运行时间

模式计算方式
Interval基准取 LastRunAt ?? 当前时间,下次 = 基准 + IntervalMinutes;若结果已过期则滚到「当前时间 + 间隔」,避免补跑一长串历史窗口
Cron用 Cronos 解析 5 段表达式计算下一次时间
到期判定取 IsEnabled && NextRunAt <= now 的任务
立即运行POST {id}/run 把 NextRunAt 置为当前时间交给 Runner 拾取,并返回一条占位执行记录(状态「排队」、IsManual = true)
更新语义保存配置会重置 RetryCount = 0 并重算 NextRunAt
删除语义软删:IsDeleted = true · IsEnabled = false · NextRunAt = null

执行器

类型行为
Skill执行数据库技能或本地技能:TargetId 以 local: 开头时走本地技能(见 扩展与工具)
AiTask解析 Parameters JSON 的 systemPrompt / prompt;不是 JSON 时把全文当提示词;用量来源 scheduled
GraphRebuild遍历全部笔记重建图谱
EmbeddingRegenerate遍历全部笔记重建向量,强制重算(force: true)

校验、失败与观测

  • 创建 / 更新校验:不支持的 TaskKind 拒绝;Cron 模式必须给表达式;Interval 模式间隔必须 > 0;Skill 必须有 TargetId;AiTask 必须有 Parameters。
  • MaxRetries 被钳到不小于 0;下次时间计算异常时记警告并返回空(本次不排期)。
  • 数据库侧:Name / TaskKind / ScheduleType 必填,CronExpression / LastStatus / LastError 有长度限制;查询过滤软删。
  • 观测:任务状态、错误信息、下次运行时间、最近状态都在列表里;执行历史看 /executions(含耗时与是否手动)。
  • 日志主要来自后台服务自身(巡检、恢复、失败),没有统一的定时任务日志前缀。

实作要点

  • 「同一目标只允许一条活跃任务」是刻意的:重复点「重建向量」不会排队十次,只会复用已有任务。
  • 长任务超过 5 分钟不更新会被巡检认为可能卡死;长期运行的任务要保证期间有状态更新,否则进程重启场景下可能被重复投递。
  • Interval 不补跑历史窗口:机器休眠一天再开机,只会从「现在」重新开始排期。
  • 定时任务里的「重建图谱 / 重建向量」是全量操作,跑之前先确认 embedding 维度与模型没换(见 分块与向量索引)。