这份文档写给所有人:不懂技术的同事、新来的同事、来看系统的领导。 上篇用大白话说清它是什么、为什么需要它;下篇给要上手的人讲清怎么用、出问题怎么办。
任务执行 Agent 是一个"只会干活、不会思考"的程序。
别人把活交给它("把这个料号的资料导出来"),它就自动打开设计软件、把资料导出来、把结果回报给交活的人。 全程不需要人坐在电脑前点鼠标。
它不认识任何给它派活的系统。它只认一套固定的"交活格式"—— 所以谁来派活都行,只要按格式说清楚就行。
| 它是什么 | 它不是什么 |
|---|---|
| ✅ 一个执行者(干活的) | ❌ 不是决策者:不判断该不该出这份资料 |
| ✅ 一个通用网关:谁来派活都行 | ❌ 不认识 MES/E系统/网页系统,只认协议 |
| ✅ 一个常驻服务:7×24 待命 | ❌ 不需要人守着、不需要点界面 |
先说清"原来是怎么干的":
这样的做法有几个绕不开的麻烦:
| 麻烦 | 实际后果 |
|---|---|
| 要靠人记着 | 忙起来就漏;夜班/周末没人就停摆 |
| 要人守着电脑 | 一个料号导出要几分钟,人只能干等 |
| 步骤多、易错 | 选错层、选错类型、忘了某个类型 |
| 出问题没人知道 | 软件卡住了,没人发现,队列全堵着 |
| 无法追溯 | 谁什么时候出的、为什么失败,说不清 |
整个无人值守输出系统里,有三个角色:
完整的一条流水线长这样(Agent 在中间那一段):
把一件事从头跟到尾,你就完全懂它了:
| 步骤 | 发生了什么 | 记录在哪 |
|---|---|---|
| 1 | 接到活。有人往它这儿丢了一条任务:"用 output_engine 脚本,给料号 <料号> 出「内层LDI」,输出到 …内层LDI 目录" |
任务进入队列,状态 queued 排队中 |
| 2 | 先验身份。检查两件事:① 派活的这个系统有没有权限派这种活;② 这个脚本在不在白名单里。 任一不满足 → 直接拒绝,不执行。 | 拒绝则记录原因 |
| 3 | 排队等工位。出资料用的 InCAMPro 很吃资源,同一时刻只允许一个人用;其他类型的脚本最多 3 个同时跑。 | 状态 running 执行中 |
| 4 | 开工准备。给这一个任务单独准备一套干净的工作环境(独立账号文件、独立临时目录), 保证它跟别人互不干扰。 | 日志逐条记录 |
| 5 | 真的去干活。启动 InCAMPro → 脚本按料号检查资料是否齐全(料号在不在、层别对不对、审核状态是否 OK) → 全部通过才导出资料。 | 每个任务的独立日志文件 |
| 6 | 交卷。脚本把结果写成一份标准回执(成功 / 业务失败+原因 / 无需处理)。 | 回执文件 + 入库 |
| 7 | 回报。把结果主动发回给派活的系统;必要的还会推企微通知到人。 | 状态 success / failed / 跳过 |
| 8 | 清理。关掉这次会话、删掉临时环境、留好日志档案(按日期归档,保留 30 天)。 | 日志归档 |
| 保障 | 大白话解释 |
|---|---|
| ① 只做教过的活 (白名单) |
它不能执行任意命令。只有提前登记在"允许清单"里的几个脚本能跑。 别人派一个清单外的脚本 → 直接拒。这是防止它被滥用、被误用的第一道闸。 |
| ② 认人下菜 (按系统授权) |
每个来派活的系统有专属口令,并且能派的活不一样。 比如"出正式资料"这种重活,只有 E系统 那边能派;网页端只能派些轻量的活。 |
| ③ 一人一间房 (并发隔离) |
两个任务同时跑不会互相踩:各自用独立的账号文件与临时目录, 跑完立刻清理(避免残留敏感文件)。 |
| ④ 到点必须停 (多重超时) |
任何脚本都有最长执行时间;超了就连同它的所有子进程一起掐掉, 绝不允许"赖着不走"把后面的活全堵死。 |
| ⑤ 卡住会被拍醒 (自愈) |
如果某个任务真的卡死了,系统能分辨出"是真卡住"还是"只是慢", 只把真卡住的干掉重来;同时它自己会向巡检程序"报到",长时间没动静会被自动重启。 |
| ⑥ 全程留痕 (可追溯) |
每条任务都有:什么时候收的、谁派的、什么时候开始、结果如何、失败原因、 完整日志。出问题能查、能复盘。 |
| 你想知道 | 去哪儿看 |
|---|---|
| 这个活干完了没 | 派活的那套系统(如前端任务中心)会显示状态:排队中 / 执行中 / 成功 / 失败 / 跳过 |
| 为什么失败了 | 回执里的原因(中文说明,比如"未检测到第二阶段制作电子check_list") |
| 它到底跑了什么 | 每个任务都有一份独立日志,可按任务号下载查看 |
| 现在忙不忙 | 查它的健康接口,能看到排队/执行数量 |
| 要不要通知人 | 可以配置:哪些结果发企业微信、发给谁(支持按人/部门自动查) |
| 别指望它 | 因为 | 该找谁 |
|---|---|---|
| 判断"这个料号该不该出资料" | 那是主管(V2)的职责 | 无人值守系统 V2 |
| 补齐缺失的资料 | 它只会导出,不会改设计数据 | CAM 制作/审核人员 |
| 自动修好业务性失败 | 业务失败重试一百次也还是失败(资料本身不全) | 人工补齐资料 |
| 执行未登记的脚本 | 白名单机制,安全要求 | 走流程申请加白名单 |
| 猜到"你想干什么" | 它只认协议,不猜意图 | 按协议把任务说清楚 |
自动任务Agent/
├── run.py 启动入口(起 web 服务 + 调度)
├── agent.service systemd 单元(开机自启 / 崩溃自动拉起)
├── config.json ★ 全部配置:白名单、权限、调度、渠道、企微(改完热加载,不用重启)
├── secrets.json ★ 敏感信息(口令 / 企微密钥)—— 权限 600,不进版本库
├── requirements.txt 依赖
│
├── agent/ ★ 框架代码
│ ├── app.py 对外 HTTP 接口(收任务/查任务/取消/通知/健康)
│ ├── scheduler.py 调度器:3 个 worker、并发控制、心跳与进展打点
│ ├── executor.py 执行器:起 InCAMPro/命令、超时、活动性探针、清理
│ ├── parser.py 结果解析:把脚本返回翻译成统一状态与中文原因
│ ├── callback.py 回传:HTTP 回调外部系统 + 企微通知
│ ├── db.py 任务库(SQLite)读写与原子领取
│ ├── task_api.py ★ 给业务脚本用的两行接口 get_args() / return_result()
│ ├── log.py 日志:agent.log 轮转 + 每任务档案 + 过期清理
│ ├── inbox.py 文件渠道(往目录丢文件也算派活)
│ ├── redis_channel.py Redis 渠道(预留)
│ └── wechat.py 企微发送(群机器人 / 应用消息 / 通讯录解析)
│
├── scripts/ 真实业务脚本(如 close_genesis.py)
├── tests/ 演示/测试脚本(example_task / demo_task)
├── docs/ ★ 详细文档(协议设计 / 使用手册 / 对接指南 / 本文)
├── logs/ agent.log + tasks/按日期的任务日志
└── data/ agent.db(任务库)+ results/(参数与结果文件)+ 心跳文件
| 模块 | 一句话职责 | 行数 |
|---|---|---|
executor.py | 真正动手的:起会话、喂参数、收结果、超时杀、活动性判卡死 | 627 |
wechat.py | 发企微(姓名→用户自动解析、卡片、附件) | 507 |
scheduler.py | 调度:领任务、并发闸门、重试、心跳/进展 | 382 |
task_api.py | 业务脚本接口(脚本只需两行代码就能对接) | 238 |
app.py | HTTP 接口层 | 223 |
db.py / parser.py / callback.py | 存储 / 结果翻译 / 回传 | 216 / 158 / 99 |
POST http://<agent主机>:8130/task
Header: Authorization: Bearer <你系统的口令>
Body:
{
"script": "output_engine", // 要跑哪个脚本(必须在白名单里)
"args": { "job": "<料号>", "type": "内层LDI", "path": "/.../内层LDI", "mode": "测试" },
"timeout": 14400, // 可选,不填用脚本默认
"callback_url": "http://.../回调", // 可选,干完主动通知你
"notify": { "on": ["failed"] } // 可选,哪些结果推企微
}
| 方式 | 怎么用 | 适合 |
|---|---|---|
| 主动回调 | 投任务时给 callback_url,任务到终态时 Agent 主动 POST 结果过去;失败会重试 3 次(间隔 5 秒) | 推荐,最省事 |
| 主动查询 | 随时 GET /task/{任务号} 查状态与结果;也可以 GET /task?status=queued&limit=100 列清单 | 兜底 / 批量对账 |
from task_api import get_args, return_result
args = get_args() # ① 拿到派活方传的参数
# ... 在这里干你的活 ...
return_result("0", "OK", {"summary": "导出完成"}) # ② 交卷(code="0" 表示成功)
| 接口 | 作用 |
|---|---|
get_args() | 按任务号读出参数 JSON(参数走文件传递,命令行只传任务号,避免解析歧义) |
return_result(code, message, data) | 写标准回执文件。code="0"=成功;非 0 = 业务码(Agent 会查字典翻译成中文原因) |
| 接口 | 用途 |
|---|---|
POST /task/{id}/cancel | 取消任务:按登记好的进程组号精确杀整棵进程树,不误伤别的会话 |
GET /task/{id}/log | 下载该任务的完整日志 |
POST /notify | 不经过任务,直接推一条企微消息(支持文本 / 图文卡片 / 附件) |
GET /health | 健康检查(给监控用) |
none(不需要回传)/
success(已成功通知外部系统)/ failed(通知失败,重试 3 次仍失败)。
| 类别 | 长什么样 | 含义与谁负责 |
|---|---|---|
| 业务码 | 纯数字 如 43 |
料号资料本身的问题,Agent 无能为力,需人工去补资料。 例: 43 = 未检测到第二阶段制作电子 check_list |
| 系统码 | E_ 开头如 E_PROC_1 |
Agent/进程层面的问题,通常可自动重试或需运维介入。 例:脚本异常退出、结果文件损坏、超时 |
| 码 | 类别 | 中文含义 |
|---|---|---|
1 | 已处理 | 处理 OK |
12 | 不处理 | 未识别到对应层别,无需输出 |
24 | 不处理 | 未识别到外防文,只出内层 AOI / 内层 LDI |
5 | 不处理 | 输出资料包含周期,需手动输出 |
6 / 61 / 62 | 不处理 | 文字喷印条件不满足(未获取到文字设备/文字层别) |
2 | 异常 | CAM 资料库没有此料号 |
21 / 22 | 异常 | 料号被锁定 / 输出路径创建失败 |
3 / 31 / 32 / 33 | 异常 | 锁放系统无数据 / CAM 制作·审核·输出锁定状态不 OK |
4 / 41–48 | 异常 | 电子 check_list 缺失或检测不 OK(分第一/第二阶段、制作/审核) |
51 / 63 | 异常 | 未检测到料号周期 / 料号包含周期限制,需人工处理 |
9000 | 异常 | 未知原因(会带上脚本的真实报错信息) |
1 / 成功 才是真的把资料导出来了。
| 码 | 含义 | 系统会怎么做 |
|---|---|---|
E_INCAM_TIMEOUT | 会话超时未交卷 | 杀进程组 → 可自动重试 |
E_PROC_<退出码> | 脚本/进程异常退出 | 记录退出码与输出尾部 → 可自动重试 |
E_RESULT_MISSING | 脚本正常退出但没交卷(没调 return_result) | 按失败处理(防"假成功") |
E_RESULT_INVALID | 回执文件损坏 | 按失败处理 |
E_WHITELIST | 脚本不在白名单 | 直接拒绝 |
E_LICENSE | 等 InCAMPro 窗口许可超时被停止 | 释放会话 → 可自动重试 |
config.json → whitelist 里登记过的脚本能被调用,
且每个脚本的路径、类型、最长执行时间都在配置里写死。
| 脚本名 | 类型 | 最长执行 | 用途 |
|---|---|---|---|
output_engine | InCAM 会话 | 4 小时 | 真正出资料(主业务) |
old_jobs_backup | InCAM 会话 | 1 小时 | 旧料号备份(按退出码判成败) |
close_genesis | InCAM 会话 | 5 分钟 | 关闭 Genesis 会话 |
example_task | 普通命令 | 2 分钟 | 演示/自检 |
demo_task | 普通命令 | 2 分钟 | 演示/自检 |
| 派活方 | 允许调用的脚本 |
|---|---|
mes(E系统侧) | old_jobs_backup、close_genesis、output_engine |
web(网页侧) | example_task、demo_task、close_genesis |
wechatrobot | example_task、demo_task、close_genesis |
secrets.json(权限 600),与配置文件分离。
| 参数 | 当前值 | 说人话 |
|---|---|---|
scheduler.workers | 3 | 同时有 3 个"工人"在待命领活 |
max_incam_concurrent | 1 | InCAMPro 会话同一时刻只允许 1 个(抢许可、抢显示,必须串行) |
max_cmd_concurrent | 3 | 普通命令类脚本最多 3 个并行 |
poll_interval_sec | 3 | 每个工人每 3 秒问一次"有活吗" |
max_retry | 3 | 环境性问题最多自动重试 3 次 |
领任务用的是一条原子操作:update ... where id=? and status='queued'。
意思是"只有它还是排队状态我才领走"——3 个工人同时抢同一条,数据库保证只有一个成功。
.agent_<任务号>),账号文件互不覆盖;任务结束立刻删除,不留明文口令。| 层 | 机制 | 作用 |
|---|---|---|
| 1 | 交卷即收工 | 脚本交卷那一刻,给会话 10 秒自然退出;不退就掐掉整个进程组 |
| 2 | 总超时 | 超过该脚本的最长执行时间(出资料是 4 小时)→ 连同子进程一起掐 |
| 3 | 活动性看门狗 | 见下(这是 2026-09 新增的关键能力) |
| 证据 | 真卡住 | 慢但在算 |
|---|---|---|
| 会话树的 CPU 时间是否增长 | 完全不动(在等锁) | 持续增长(在算) |
| InCAMPro 自己的日志是否在长 | 停止 | 持续增长(每条命令都写一行) |
只要任一在动 → 判定"在推进",放它继续跑;全部静止超过阈值 → 才判卡死,杀掉重来, 并留一份证据快照供复盘。
Agent 启动会话后 5 秒内会核对一次:子进程到底有没有真的跑起来(比对它的命令行是否已变)。 如果 5 秒后它还停在"没启动"的状态 → 判定启动失败,立即收掉,不用白等 4 小时超时。
| 情况 | 会不会重试 | 为什么 |
|---|---|---|
| 脚本超时 / 进程异常退出 / 结果文件缺失或损坏 | 会(最多 3 次,每次间隔递增) | 属于"环境性问题",重试可能自愈 |
| 业务码失败(如 43 = 缺 check_list) | 不会(在 Agent 这层) | 资料本身不全,重试没用 |
| "不处理"类结果 | 不会 | 这是正常的业务结论,不是错 |
| 信号 | 文件 | 回答的问题 | 什么时候更新 |
|---|---|---|---|
| 心跳(活性) | data/scheduler_heartbeat | "这个程序还活着吗" | 调度循环每轮都更新(含空闲) |
| 进展(推进) | data/scheduler_progress | "任务真的在往前走吗" | 只在真有推进时更新(领到任务/有输出/子进程结束/写终态) |
为什么要分开:曾经只有心跳,而心跳被写在"等待循环"里——所以任务卡死时心跳照样新鲜, 系统以为一切正常。现在"心跳新鲜 + 进展陈旧"就是卡死的典型特征。
| 触发条件 | 动作 | 会不会打扰正常任务 |
|---|---|---|
| 启动校验失败(子进程没起来) | 5 秒内判失败并清理 | 不会(确定性判据) |
| 活动性证据全部静止超阈值 | 杀该会话 → 可重试 + 留证据快照 | 不会(慢但有活动的任务被放行) |
| 心跳长时间未更新 且队列有任务 | 巡检自动 重启 Agent 服务;重启后把卡住的执行中任务重新排队 | 重启有冷却,5 分钟内不重复 |
| 进展陈旧(队列有任务却零推进) | 只告警(附活动性证据,判定"偏向卡死/偏向慢") | 不会(避免误杀慢任务) |
任务一开始执行,Agent 就把它启动的整个进程组的编号记下来。要取消时按这个编号"整组杀"—— 保证不残留孤儿进程,也不会误杀别的会话。
# 看状态
systemctl status task-agent
systemctl is-active task-agent
# 重启(改代码后需要;改 config.json 不用重启,热加载)
systemctl restart task-agent
# 看实时日志
journalctl -u task-agent -f
# 看进程日志文件
tail -f 自动任务Agent/logs/agent.log
# 健康接口
curl http://127.0.0.1:8130/health
| 你看到的现象 | 多半是什么 | 怎么办 |
|---|---|---|
| 任务一直"排队中"不开始 | ① 前面有出资料的任务在跑(InCAMPro 串行,正常排队)② 服务停了 ③ 服务被暂停 | 先看队列里有没有 running 的;再看服务状态 |
| 任务卡在"执行中"很久 | 可能真卡住,也可能只是慢 | 看两个时间戳:心跳 vs 进展。心跳新鲜但进展陈旧 = 卡死特征。再看该会话 CPU/日志是否在动 |
| 失败原因是一串中文(无 E_) | 业务失败:料号资料不全 | 去补资料,补完让上游重派;反复重试无意义 |
| 结果是"不处理 / 跳过" | 正常:按业务规则这批不该出 | 不用处理 |
失败原因是 E_PROC_... |
脚本/进程异常退出 | 看该任务日志的报错尾巴;环境性问题会自动重试 |
失败原因是 E_RESULT_MISSING |
脚本没按协议交卷(漏调 return_result) |
找脚本作者修;这是防"假成功"的保护 |
| 外部系统收不到结果 | 回传失败(对方地址不通 / 超时) | 看任务的回传状态;Agent 已重试 3 次,仍失败需检查对方服务 |
| 被拒绝:"脚本不在白名单" | 该脚本未登记,或派活方没有该脚本权限 | 走流程申请加白名单 / 加权限 |
| 企微没收到通知 | 该任务/该状态未开启通知,或企微配置问题 | 检查任务的 notify 配置与企微凭证 |
| 项目 | 当前情况(2026-09-13 实测) |
|---|---|
| 任务库文件 | data/agent.db ≈ 4.7 GB(SQLite) |
| 任务总数 | 约 86,000 条 |
| 其中成功 | 1,855 条 |
| 其中失败 | 83,598 条(绝大多数是业务失败被上游反复重排,以及测试料号) |
| 近 7 天每天新增 | 1,700 ~ 4,600 条 |
| 成功任务平均耗时 | 约 3.8 分钟 |
| 日志保留 | 任务日志 30 天、结果文件 7 天,到期自动清理 |
| 你会听到的词 | 其实就是 |
|---|---|
| Agent / 执行 Agent | 干活的程序,本文主角(端口 8130) |
| V2 / 无人值守系统 | 派活的主管(决定该出哪些资料) |
| E系统 / MES | 上游订单/审核系统,料号状态的来源 |
| InCAMPro | 真正用来导出 PCB 设计资料的商业软件 |
| 料号 / Job | 一块板子的编号(如 <料号>) |
| 资料类型 | 要出哪一类资料,如 内层LDI / 外层AOI / 防焊DI |
| 白名单 | "允许执行的脚本清单"(安全闸门) |
| 回调(callback) | 干完主动把结果发回给派活方 |
| 队列 / 排队 | 活太多时按顺序等工位 |
| 串行 / 并发 | 串行=一次只干一个;并发=可以同时干几个 |
| 业务跳过 / 不处理 | 正常结果:按规则这批不该出 |
| 心跳(heartbeat) | 程序"我还活着"的信号 |
| 进展(progress) | "活真的在往前推"的信号 |
| 自愈 | 卡住了能自己发现、自己恢复 |
| 伪终端(pty) | 给后台程序模拟一个"人在终端"的环境 |
| 进程组(pgid) | 一次执行启动的所有相关进程的"户口",便于整组清理 |
| 超时(timeout) | 超过规定时间还没干完,强制停止 |
不会。V2 只产生任务(写工单),Agent 只执行任务(干活)。V2 不直接操作 InCAMPro,Agent 不判断该不该出资料。 一条活只由 Agent 执行一次("原子领取"保证不会两个工人抢同一条)。
因为 InCAMPro 需要独占窗口许可与显示,多开会互相干扰导致失败。所以出资料的任务必须排队串行。 这也是为什么成功任务平均要 3.8 分钟——时间主要花在等工位 + 软件启动 + 逐项检查资料。
分两种:
① 失败原因是一串中文(如"未检测到第二阶段制作电子 check_list")→ 属于料号资料不全,需要人工去补资料;
系统再怎么重试都不会成功。
② 失败原因是 E_ 开头(进程异常/超时等)→ 系统会自动重试,通常不用管;连续失败才需要找运维。
"不处理"是由业务脚本按规则判断得出的结论,例如:没有文字层别(所以不出文字喷印)、 没有外防文(所以只出内层)、料号没有对应工具层(所以这类资料无需输出)。 这些是设计好的行为,不是漏做。
三个限制:① 只能跑白名单里的脚本,不能执行任意命令;② 每个任务用独立的临时目录,跑完立刻清理,不碰别人的; ③ 收工时按进程组号精确清理,不会误杀其他会话。此外它的所有配置与密钥分离存放,权限收紧。
不用。自动分层处理:
① 单任务卡死 → 活动性探针识别后杀掉重来;
② 整个服务没响应(心跳停)→ 巡检自动重启服务,并把卡住的执行中任务重新排队;
③ 队列有任务但长时间零推进 → 告警通知人来看(附证据判断是"卡死"还是"只是慢")。
看上游系统的任务列表(状态=成功 表示已导出)。想知道细节就按任务号查 Agent:
GET /task/{任务号} 看结果与原因,GET /task/{任务号}/log 看完整执行日志。
需要两步:① 把脚本登记进 config.json 的白名单(含路径、类型、超时);
② 给需要调用它的系统在 client_permissions 里开权限。
脚本本身要按协议写:get_args() 取参数、return_result() 交卷。
配置改完热加载生效,不用重启。
docs/ 里还有三份专业文档(面向不同读者):
| 文档 | 给谁 | 内容 |
|---|---|---|
任务执行Agent-使用手册.md | 运维 / 管理员 / 脚本作者 | 部署、配置、白名单、脚本开发、企微、FAQ |
任务执行Agent-外部系统对接指南.md | 对接开发 | 协议字段、状态机、错误码、Python/Java/JS/C# 示例 |
任务执行Agent-标准化协议设计.md | 技术底稿 | 四个标准化边界的协议定稿 |