本文引用的文件
本文聚焦系统启动后用于清理以下资源的定时任务:Agent 工作空间相关数据、临时批次目录、对话附件孤儿、以及索引残留。文档说明这些任务的调度配置、执行频率、工作空间的目录结构和生命周期,并给出重试/告警策略、运维调整建议与常见故障排查方法。
需要特别说明的是:当前代码库中并未实现“下载日志表定期自动删除”的独立定时任务;知识库下载记录 knowledge_doc_download_log 仅作为审计字段存在。因此“下载日志清理”在现有实现中属于未覆盖项,本文会明确标注其现状与排查建议。
系统涉及三类与“工作空间/临时文件/索引”相关的存储位置:
| 类型 | 根路径或隔离维度 | 主要用途 | 是否由定时任务清理 |
|---|---|---|---|
| Agent 工作空间 | {cwd}/.agentscope/users/{userId}/workspaces/{workspaceId}/agents/{agentRowId} |
Agent 会话、技能、记忆、子智能体等运行时数据 | 无直接定时清理;受 Agent 生命周期和会话管理约束 |
| 清洗临时批次 | {user.dir}/QingJian/tmp/{batchId} |
Excel/CSV 流式清洗过程中的中间批次目录 | 是,按修改时间超过 24 小时兜底清扫 |
| 对话附件 | {user.dir}/QingJian/chat-attachments/{userId}/{caseId}/{sessionId} + 向量表 |
输入框上传的文件、sidecar、RAG 向量块 | 是,按“上传后超期仍未关联消息”判定为孤儿清理 |
| DuckDB 案件数据源 | 内存注册表 + 每案 DuckDB 实例 | 案件级数据库连接与缓存 | 是,按 token 失效+无在途任务回收 |
| 知识库下载日志 | PG 表 knowledge_doc_download_log |
记录谁何时下载了哪个文档 | 无自动清理任务 |
graph TB
A["应用根目录<br/>System.getProperty('user.dir')"] --> B["QingJian<br/>运行目录"]
B --> C["tmp<br/>清洗临时批次"]
B --> D["chat-attachments<br/>对话附件"]
A --> E[".agentscope<br/>Agent 运行时数据"]
E --> F["users/{userId}<br/>用户级隔离"]
F --> G["workspaces/{workspaceId}<br/>工作空间隔离"]
G --> H["agents/{agentRowId}<br/>Agent 隔离"]
图表来源
章节来源
系统使用 Spring @Scheduled 实现多个清理类任务。调度线程池默认大小为 4,以避免单个批量任务拖慢 SSE 心跳等其他调度任务。
| 任务 | 触发方式 | 默认间隔 | 初始延迟 | 作用范围 | 关键配置项 |
|---|---|---|---|---|---|
| 临时批次目录清扫 | fixedDelayString |
21600000 ms(约 6 小时) | 600000 ms(10 分钟) | QingJian/tmp 下直接子目录 |
zsjz.tmp.sweep-interval-ms、zsjz.tmp.sweep-initial-delay-ms |
| DuckDB 案件数据源回收 | fixedDelayString |
300000 ms(5 分钟) | 60000 ms(1 分钟) | 已打开但未归属有效会话的案件 | zsjz.duckdb.reaper-interval-ms、zsjz.duckdb.reaper-initial-delay-ms |
| 对话附件孤儿清理 | fixedDelayString |
3600000 ms(1 小时) | 600000 ms(10 分钟) | chat_attachment 中未关联消息且超期的附件 |
zsjz.chat-attachment.purge-interval-ms、zsjz.chat-attachment.purge-initial-delay-ms |
| 知识库下载日志清理 | 未发现定时任务 | 不适用 | 不适用 | knowledge_doc_download_log |
无 |
调度线程池配置位于 spring.task.scheduling.pool.size,默认值为 4。该值影响所有 @Scheduled 任务并发能力,若某个清理任务耗时过长,应优先检查该任务本身,而不是盲目扩大线程池。
章节来源
Agent 工作空间采用三维隔离:用户 × 工作空间 × Agent。解析结果为:
{cwd}/.agentscope/users/{userId}/workspaces/{workspaceId}/agents/{agentRowId}
其中:
| 层级 | 含义 | 是否可被通用清理脚本删除 |
|---|---|---|
.agentscope |
Agent 运行时根目录 | 不应直接删除,否则丢失用户技能、记忆与会话 |
users/{userId} |
用户级数据,包括技能仓库 | 仅可删除对应用户的目录,不能跨用户清理 |
workspaces/{workspaceId} |
业务工作空间,通常等于 caseId | 删除前应确认该案件不再被任何会话或 Agent 引用 |
agents/{agentRowId} |
具体 Agent 实例数据,包含会话、事件转录、标准槽位 | 不应由外部脚本直接删除 |
Agent 首次创建工作空间时,WorkspaceScaffolder 会写入 AGENTS.md、tools.json、skills/、subagents/、memory/ 等模板内容,并保证“不存在才写入”,避免覆盖已有数据。
flowchart TD
Start["创建 Agent 工作空间"] --> Check["检查工作空间目录是否存在"]
Check --> Exists{"已存在?"}
Exists -->|是| Noop["跳过覆盖,保持已有数据"]
Exists -->|否| Create["创建 AGENTS.md、tools.json、skills、subagents、memory 等"]
Create --> Done["完成初始化"]
Noop --> Done
图表来源
章节来源
清洗临时目录位于 QingJian/tmp,每个数据清洗批次以 batchId 命名一个子目录。正常流程会在清洗回调中删除本批次目录;但中途失败、取消或异常退出的批次可能留下残留。
该目录与 Agent 工作空间不同:它不是按用户/会话隔离的工作空间,而是清洗流水线共享的临时目录,因此清理策略基于“目录最后修改时间超过 24 小时”。
章节来源
对话附件落盘路径为:
{user.dir}/QingJian/chat-attachments/{userId}/{caseId}/{sessionId}/{雪花}_{safeName}
附件元数据保存在 chat_attachment 表中,支持三路处理:小内容内联、大表格 Python 直读、大文档 RAG 向量化。向量索引按案件分表,并在检索时强制按会话过滤。
章节来源
Electron 主进程提供上传清鉴数据源的能力,它会:
QingJian。QingJian/workspace 下扫描下一层文件夹。这说明桌面客户端对 workspace 的使用方式是“按目录枚举上传”,而不是由 Electron 负责清理。
章节来源
该任务由 DmService.sweepStaleTmpBatches 实现,核心逻辑如下:
flowchart TD
Start["定时任务开始"] --> ReadRoot["读取 QingJian/tmp 根目录"]
ReadRoot --> HasDirs{"是否有子目录?"}
HasDirs -->|否| End["结束"]
HasDirs -->|是| Iterate["遍历 tmp 下的直接子目录"]
Iterate --> CheckTime{"最后修改时间是否早于 24 小时前?"}
CheckTime -->|否| Next["跳过该目录"]
CheckTime -->|是| Delete["删除该批次目录"]
Next --> More{"还有目录?"}
Delete --> More
More -->|是| Iterate
More -->|否| End
图表来源
需要注意:
tmp 根目录下的直接子目录,不会删除 tmp 根目录本身。章节来源
CaseDataSourceReaper 是兜底回收器,解决 sa-token Redis 会话自然过期不触发监听器的问题。它的策略非常保守:
只有两者都满足“会话失效 + 无在途任务”才关闭数据源并清理缓存。
sequenceDiagram
participant Scheduler as "调度器"
participant Reaper as "CaseDataSourceReaper"
participant Registry as "CaseDataSourceRegistry"
participant Token as "Sa-Token"
participant Probes as "在途任务探针"
Scheduler->>Reaper: 定时触发 reap()
Reaper->>Registry: 获取已打开案件列表
loop 每个案件
Reaper->>Registry: 查询归属 userId 与 ownerToken
alt 有 ownerToken
Reaper->>Token: 校验 token 是否有效
Token-->>Reaper: 有效/无效
else 老数据无 ownerToken
Reaper->>Token: 按用户查剩余 token
end
Reaper->>Probes: 检查案件是否在途
alt 会话仍有效 或 有在途任务
Reaper->>Reaper: 跳过回收
else 会话失效 且 无在途任务
Reaper->>Registry: 关闭案件数据源
Reaper->>Registry: 清理案件缓存
end
end
图表来源
该任务的关键设计原则是“宁可多留,不可误杀”。当 token 状态检查或探针异常时,它选择跳过回收,而不是释放可能仍被使用的案件数据源。
章节来源
ChatAttachmentService.purgeOrphans 清理上传后超期仍未关联消息的附件。默认保留时间为 7 天(orphanHours=168),但实际截止时间取“当前时间减去 max(24, orphanHours)”,即至少保留 24 小时。
清理流程:
link_message_id 为空且创建时间早于截止时间的附件。单条附件清理异常会被捕获并记录日志,不会中断其他附件清理。
flowchart TD
Start["孤儿附件清理任务"] --> Query["查询未关联消息且超期的附件"]
Query --> HasOrphans{"是否有孤儿?"}
HasOrphans -->|否| End["结束"]
HasOrphans -->|是| Loop["逐个孤儿附件"]
Loop --> AtomicDelete["条件删除:仍未关联消息且超期"]
AtomicDelete --> Deleted{"DB 删除成功?"}
Deleted -->|否| Next["跳过,可能刚被关联"]
Deleted -->|是| CleanIndex["删除向量索引"]
CleanIndex --> CleanFile["删除原件 sidecar 文件"]
CleanFile --> Next
Next --> More{"还有孤儿?"}
More -->|是| Loop
More -->|否| Log["记录清理数量"]
Log --> End
图表来源
章节来源
系统中与“索引”相关的清理主要有两类:
| 索引类型 | 清理时机 | 说明 |
|---|---|---|
| 对话附件 RAG 向量 | 附件孤儿清理、删除案件附件时 | 调用 AttachmentDocIndexer.deleteAttachmentVectors 删除对应向量行 |
| AI 清洗模板向量索引 | 模板变更增量更新、启动时重建、手工重建接口 | 由 aiclean 模块管理,不属于通用工作空间清理 |
| 知识库文档向量 | 删除知识库、撤销附件索引时 | 与知识库生命周期绑定 |
目前未见“周期性扫描磁盘索引目录并清理孤立索引文件”的通用任务。若出现索引残留,通常应先确认对应的数据库记录是否仍存在;若记录已删而索引未删,则属于幂等删除缺失或外部清理未覆盖。
章节来源
| 清理任务 | 失败处理策略 | 是否重试 | 风险点 |
|---|---|---|---|
| 临时批次目录清扫 | 未对单目录删除做 try/catch;删除失败可能中断后续目录 | 无 | 某个目录占用导致整轮清扫停止 |
| DuckDB 数据源回收 | token 检查失败、探针异常均按“仍在用”保守跳过 | 下次调度周期重试 | 回收变慢,但安全性高 |
| 对话附件孤儿清理 | 单条附件清理异常被捕获,继续处理下一条 | 无立即重试,靠下次调度周期 | 文件/向量删除失败会留下磁盘残留 |
| 知识库下载日志 | 无自动清理逻辑 | 不适用 | 长期积累可能增长日志表 |
当前清理任务主要通过日志输出信息,例如:
仓库中未发现专门的清理任务指标上报或告警规则。运维侧建议结合应用日志、磁盘监控与数据库行数变化进行观察。
章节来源
可通过环境变量或配置文件调整以下参数:
| 目标 | 配置键 | 默认值 | 建议 |
|---|---|---|---|
| 临时批次目录清扫间隔 | zsjz.tmp.sweep-interval-ms |
21600000 | 高频上传场景可适当缩短,但不要小于 1 小时 |
| 临时批次目录清扫初始延迟 | zsjz.tmp.sweep-initial-delay-ms |
600000 | 一般保持 10 分钟即可 |
| DuckDB 数据源回收间隔 | zsjz.duckdb.reaper-interval-ms |
300000 | 高并发长驻会话环境可缩短至 1~2 分钟,但会增加 token 校验开销 |
| DuckDB 数据源回收初始延迟 | zsjz.duckdb.reaper-initial-delay-ms |
60000 | 一般保持 1 分钟 |
| 附件孤儿清理间隔 | zsjz.chat-attachment.purge-interval-ms |
3600000 | 高频上传场景可缩短至 15~30 分钟 |
| 附件孤儿清理初始延迟 | zsjz.chat-attachment.purge-initial-delay-ms |
600000 | 一般保持 10 分钟 |
| 附件孤儿保留小时数 | zsjz.chat-attachment.orphan-hours |
168 | 不建议低于 24 小时,避免用户暂存输入框后发送被误删 |
注意:zsjz.chat-attachment.purge-interval-ms 控制“多久扫一次”,而 orphan-hours 控制“多久算孤儿”。二者要分开理解。
重点监控以下目录:
| 目录 | 监控重点 |
|---|---|
QingJian/tmp |
是否存在大量超过 24 小时的批次目录 |
QingJian/chat-attachments |
是否堆积未关联消息的大文件、sidecar 或向量索引 |
.agentscope |
是否随用户/Agent 数量线性增长 |
PG 中 knowledge_doc_download_log |
下载日志是否持续快速膨胀 |
推荐指标:
QingJian/tmp 目录数量与总大小。chat_attachment 表中 link_message_id IS NULL 的记录数。对于临时批次目录或附件文件无法删除的情况,建议按以下步骤定位:
lsof 或 fuser 查找占用文件的进程。.agentscope 根目录。QingJian/workspace,因为它是业务 workspace 入口,可能被桌面端或 Agent 使用。QingJian/tmp 前,先确认没有正在执行的清洗任务。chat-attachments 前,先确认 chat_attachment 中是否仍有活跃记录。可能原因:
排查步骤:
spring.task.scheduling.pool.size 是否过小。章节来源
典型表现:
排查步骤:
QingJian、.agentscope、tmp、chat-attachments 具有读写权限。章节来源
典型表现:
排查步骤:
RUN_QINGJIAN_PATH 指向 QingJian。tmp 目录。QingJian。chat-attachments 是否在首次上传附件时被创建。章节来源
当前代码对“误删”有一定防护:
但仍需注意:
chat-attachments 或 workspace,不会经过业务归属校验。建议:
.agentscope。章节来源
当前系统的清理能力集中在三个方向:
QingJian/tmp 下的批次子目录,适合缓解中途失败导致的目录膨胀。Agent 工作空间本身没有独立的定时清理任务,其生命周期由 Agent 运行时、用户、工作空间和会话共同决定。知识库下载日志也没有自动清理任务。
运维上最关键的实践是:先理解每条清理规则的边界,再调整间隔和阈值;不要直接删除 workspace、.agentscope 或 QingJian 根目录;把清理任务日志纳入监控,重点关注无法删除、孤儿数量增长和调度线程池饱和等问题。