# LISP 任务：提交、回执与恢复

`com.cad.task` 在兼容 CAD 的 COM 命令通道之上执行 AutoLISP，并通过本地文件返回数据。它不依赖 Autos、特定窗口类名、Visual LISP COM 函数接口或 .NET 插件。**回执确认的是程序执行和序列化结果，不代替几何或业务验收。**

## 建议入口

- `cad.CreateLispTask(code,args,doc,options)`：先创建任务，不提交。自动化优先使用，可在产生副作用前保存任务路径。
- `cad.SubmitLisp(...)`：创建并尝试提交，返回 `task,err`。即使提交报错，也可能返回任务对象，应保留它。
- `cad.RunLisp(...)`：创建、保存journal、唯一提交并等待，返回result,err,task。结果含taskPath/elapsed及实际journal；journal写失败不提交。
- `cad.ResumeLisp(path,timeout,doc)`：恢复原回执，返回同一元组；省略doc可离线。
- `com.cad.task.open(path[,doc])`：跨线程、跨会话恢复任务。读取回执不需要 CAD 正在运行。

增强实例入口可省略doc，此时只使用已有活动图纸，不自动新建；底层task.create仍要求显式doc。`code` 是原始 LISP 源码，不是 aardio 模板，也不是文件名。最后一个表达式的值为结果，`args` 是 LISP 中的局部变量。不要使用 `ac$` 前缀的内部变量，也不要在任务中切换/关闭目标图纸或嵌套提交任务。

## 最小完整示例

```aardio
import com.cad;
import com.cad.task;
var cad = com.cad();
if(!cad) error("无法连接或启动 CAD");
if(!cad.IsQuiescent()) error("CAD 当前忙碌，请先检查原任务");
var doc = cad.Documents.Add(); // 本例明确新建测试图纸。
var task,err = cad.CreateLispTask(
    `(list (cons "text" (car args)) (cons "answer" (+ (cadr args) 5)))`,
    ["中文",7],doc
);
if(!task) error(err);
// 在提交前持久化路径；进程意外退出也能重新查回执。
var recoveryFile = io.appData("aardio/com.cad/last-task.txt",task.path);
print("任务目录",task.path);
var sent,err = task.submit();
if(!sent) {
    print("提交未确认，勿重发",err,task.path);
    return;
}
var result,err = task.wait();
print(result,err); // completed 时 data 为 {text="中文";answer=12}
```

恢复时只读原任务，不另建 ID 重试：

```aardio
import com.cad.task;
var path,err = string.load(io.appData("aardio/com.cad/last-task.txt"));
if(!path) error(err);
var task,err = com.cad.task.open(path);
if(!task) error(err);
var result,err = task.wait(5000);
print(result,err);
```

`read()` 和默认的 `wait()` 只读取任务目录，不读取 COM 文档；省略 doc 打开的任务始终可以离线等回执。`wait()` 泵送窗口/STA 消息，不使用长时间纯 sleep。只有创建时显式设 `nudge=true` 且仍持有原 doc 时，wait 才可能刷新命令队列（见下文）。收到回执后 CAD 仍可能正在退出包装器；后续 COM 查询如被暂时拒绝，可有限重试无副作用查询，不重放任务源码。

RunLisp与技能调用同一com.cad.task.run/runTask，journal位于 `%LOCALAPPDATA%\aardio\com.cad\journal`。底层create只指定doc时不猜产品，默认post/utf8；通常使用增强实例入口。详见[兼容说明](compatibility.html)。

## 状态与返回值

| state | 意义 |
| --- | --- |
| prepared | 已创建，本机尚无提交标记 |
| submitted | 已认领一次提交机会；不证明 CAD 已接收或执行 |
| running | CAD 已写 started 标记，尚无终态回执 |
| completed | 执行与结果序列化成功，`ok=true`、`terminal=true` |
| error | parse / execute / serialize / transport 等阶段失败，`terminal=true` |
| rejected | CAD 端发现不是原目标文档或实例 |
| expired | 超过最迟开始时间，未进入用户源码 |
| cancelled | CAD 在执行用户源码前接受了取消请求 |
| timeout | 本次 wait 等待超时；`terminal=false`，`observedState` 保留实际快照 |

`read()` 失败返回 `null,err`。`wait()` 对非成功终态返回 `result,err`；超时也返回快照及错误说明，而不是伪造终态。请检查 `result.ok` 和 `result.terminal`，不要只检查 result 是否非 null。CAD 端语法检查会拒绝括号不完整、未闭合字符串/块注释、额外右括号，避免原始源码破坏传输表达式。

## 超时、去重与取消

- 同一路径的 `submit()` 只有一次提交认领。提交标记使用独占创建，多对象/线程/进程争用时只有一个可派发；该认领内对明确的 COM 拒绝接收可有限重试。
- CAD 侧再次遇到同一请求的 started/result 标记会跳过执行。不要删除标记、复制命令后改请求 ID、或通过新建任务来“补发”未知操作。
- 仅 0x80010001 / 0x8001010A 这两类已识别的拒绝接收在同一次认领内重试，预算3秒。其他 COM 异常（包括普通 null 方法错误）保留提交标记，不重试、不推断没有执行；继续查原回执。
- `timeout` 只限制等待回执；`startTimeout` 限制从创建任务到 CAD 开始执行的时间。两者都不强行中断已开始的 LISP。
- `cancel()` 仅写执行前取消请求。返回 true 表示文件写入成功，只有 cancelled 回执才确认 CAD 接受。已经 running 的任务可以继续完成。
- 出错可能已有部分绘图。不是事务，不自动 UNDO，不保证跨进程崩溃的 exactly-once。
- Esc 是另一条明确授权的 UI 恢复路径，见[恢复与图像反馈](automation.html)。发送 Esc 不等于任务取消回执，更不等于回滚。

## options

| 字段 | 默认值 | 说明 |
| --- | --- | --- |
| directory | `%LOCALAPPDATA%\aardio\com.cad\tasks` | 其下创建唯一请求目录；应使用 CAD 和调用方都可读写的本地路径 |
| method | AutoCAD/浩辰post，中望send | 创建时按增强实例ProgId选择，显式值优先；提交失败后不换方法重放 |
| timeout | 10000 | wait 默认等待毫秒数，0 表示立即查询；最大 86400000 |
| startTimeout | 30000 | 从创建时计算的最迟开始毫秒数；最大 86400000 |
| nudge | false | true 才允许 wait 自动刷新未开始的 Post 队列；需要原 doc，会同步调用 SendCommand，可能阻塞 |
| nudgeDelay | 800 | 显式启用 nudge 时，从本次 wait 开始等候的毫秒数；0有效，最大86400000 |
| resultEncoding | AutoCAD utf8，中望/浩辰 ansi | UTF-8要求支持三参数open；ANSI受代码页限制，先验证文字往返 |
| codepage | 本机系统 ANSI 代码页 | `resultEncoding="ansi"` 时用于解码，应与 CAD 输出一致 |
| idleTimeout | 3000 | RunLisp/runTask前后有限等空闲，0不额外等待；不能打断阻塞COM |

**method=send 的 COM 调用本身可能阻塞；wait 的超时无法打断一个尚未返回的 COM 方法。PostCommand 也不构成硬实时保证。** 强隔离要求应由调用方使用独立工作进程实现，而不是让底层库绑定 Autos。对不兼容的 CAD 先做纯查询小探针，不静默降级或修改安全设置。

## 队列刷新是显式选项，不是默认兜底

`PostCommand` 排队到空闲时处理；向非活动文档提交时可能使目标文档成为活动文档，不保证保持当前文档不变。submitted 未开始也可能是交互输入、模态窗口、许可证或系统阻塞，不能一概归因命令通道。优先查原任务及环境，不重发业务源码。

确需刷新时，`com.cad.task.open(path,doc)` 得到原任务，再调用 `task.nudge()`。它只对仍 submitted 的 post 任务发送 `(princ)` 空操作；核对文档名称、文档/应用 HWND，要求原目标仍活动且 CAD 空闲。不激活其他文档，不发送任务源码；成功后记录 nudged。

创建时 `nudge=true` 可让每次 wait 在达到 nudgeDelay 后最多尝试一次；已有 nudged 标记不再刷新。默认false，旧请求缺少该字段同样不刷新。`wait(0)` 仅查询，不刷新；超时快照可能含 nudgeError 说明未能刷新的原因。成功刷新也不代表业务完成，仍以原终态回执为准。

**nudge 使用同步 SendCommand，可能驱动该实例中其他已排队命令；COM 调用未返回时，wait 超时无法中断它。** 如要求纯异步等待，请保持默认值。刷新不等于 Esc、任务取消或回滚。

## 性能：区分 CAD 内执行和端到端耗时

减少跨进程往返有利于批量工作，但不能把 LISP 循环耗时等同于任务总耗时。2026-09-19 本机 AutoCAD 2026 对照：均新建300个相同的圆、返回300个Handle、缓存ModelSpace、统一重生成一次，三轮交错测量：

- 优化前完整任务：5.008 / 4.563 / 4.878秒。
- 相邻短行合并后完整任务：2.497 / 1.620 / 1.590秒；中位数约为原来的三分之一。命令从180行减到49行，不增加缓存服务或CAD插件。
- 外部COM循环：5.006 / 4.849 / 5.107秒（包含逐个获取Handle）。

这里完整任务从创建到收到回执计时；当时版本LISP内绘图及重生成仅47–110毫秒，不能将它当作端到端耗时。这是2026-09-19传输优化时的runner与数据；后续增加结果类型支持后命令长度不同，不能把旧基准当作后续版本的新测量。

这是单机测量，不是性能保证。完整任务还包含代码生成、提交、CAD 排队、序列化和回执等待。适合批量合并操作，不宜把每个实体都拆成一次任务；少量对象可直接 COM。不要为了缩短等待时间而取消完成确认，也不要把 method=send 当作无条件更快、更安全的替代。

## 数据协议和限制

- 数字、字符串、T、nil、列表、点对、实体名可返回。实体名转为 `{handle="..."}`；句柄必须与原文档配对使用。
- UTF-16 输入先统一为 UTF-8 再分块；Unicode 模式下返回的代理对以 JSON Unicode 转义保留，支持中文及非 BMP 字符。ANSI 模式仍受所选代码页限制，不能表示的字符不会因 JSON 封装而恢复。
- 所有元素都是“字符串键 . 值”的列表转为 JSON 对象，其他正规列表转为数组；重复对象键拒绝序列化。
- nil 转 JSON null，T 转 true。LISP 中 false 与 nil、空列表没有独立区分；aardio 的 null 字段/数组项也不能完整保留。需要保留结构时显式使用标签对象。
- 具有 Handle 的 VLA-OBJECT 转为 `{handle="..."}`；应用对象等没有 Handle 的对象仍需自行提取属性。VARIANT 自动解包；SAFEARRAY 转正规数组；PICKSET 转 `[{handle="..."},…]`，受相同结果项数限制；普通符号转名称字符串，T/nil 规则不变。结果解析只用 JSON，不 eval 返回文本。
- 实数用 AutoLISP `rtos` 的科学计数模式、精度参数16输出，并去除尾数中冗余的零；**不是所有 CAD 版本的 IEEE 754 无损往返承诺**。几何检查按单位/量级用容差，精确ID用字符串。配套 JSON 库修复了长科学计数字面值被错误转为 size64 的问题；集成时同步更新 `lib/JSON/_.aardio`，纯大整数字面值的既有处理保留。
- 每次源码和参数各限制 128 KiB；结果嵌套最多 48 层/50000 个项，主机拒绝超过 8 MiB 的回执。结果并非流式传输，不适合大图像或巨大数组。
- 内嵌 NUL、循环数据、不支持的数据类型拒绝传输。二进制数据走文件，回传路径和必要元信息。
- 传输保留 CR，数据字符串按字符边界分块并用 strcat 组合。库将相邻短行合并到约160字节以内，减少命令行 Enter 开销；较长原分块独立保留，并仍检查768字节硬预算。两者都不是CAD官方上限，不能据此把整个命令压成长行。用户源码先编码为字符串，不因传输折行被改写；回执、去重与超时行为不变。

## 目录和恢复边界

目录保存 request.json、command.txt、submitted、started、result.json，以及可选 cancel/submit-error.txt/nudged。命令文件在发送前校验哈希，结果先关闭临时文件再改名发布。

目录内含代码、参数和结果，可能包含敏感信息；由调用方控制访问与保留期限。只在确认终态且不再需要审计/恢复后主动清理。禁止自动删除未知任务的目录。硬盘损坏、文件被手工修改、CAD 退出、身份句柄复用等不在“只执行一次”的保证范围内。

文档绑定同时检查文档名称、文档 HWND、应用 HWND；不依赖标题和窗口类名。关闭重开图纸、另存改名或重启 CAD 后，不应提交之前 prepared 的任务，应先人工/业务核对。若原任务 submitted/running，始终以核对回执和实际图纸为先，不能直接重放。
