## 把任务描述成状态和阶段

状态回答“任务现在是什么情况”，阶段回答“正在做哪一步”。不要用按钮是否变灰代替任务状态，也不要用一个无限旋转图标代替所有阶段信息。

| 状态 | 进入时做什么 | 可离开的方向 |
| --- | --- | --- |
| `idle` | 等待输入和明确开始 | preparing / failed |
| `preparing` | 检查输入、工作区和必要配置 | running / failed / cancelled |
| `running` | 显示当前步骤、实际日志和产出 | completed / failed / cancelling |
| `cancelling` | 阻止新步骤，等待真实停止 | cancelled / failed |
| `completed` | 展示已经核对并保存的成果 | 新运行 |
| `failed` / `cancelled` | 保留有效输入、日志和文件 | 明确重试、继续或新运行 |

这些状态是组件可采用的内部设计，并不是所有组件已经共同实现的通用任务服务。

## 给每次运行一个身份

开始时生成新的 `runId`。所有异步步骤记录这个身份，返回后先核对它是否仍然有效，再更新界面或调度下一阶段。

```javascript
async function runStage(state, runId, stage, work) {
  const current = () => !state.disposed && state.runId === runId;
  if (!current()) return { stale: true };
  state.stage = stage;
  try {
    const value = await work();
    return current() ? { stale: false, value } : { stale: true };
  } catch (error) {
    if (!current()) return { stale: true };
    throw new Error(stage + '失败：' + String(error.message || error));
  }
}
```

调用者遇到 `stale: true` 必须结束旧运行，不能继续保存或启动后面的任务。运行身份只能阻止后续回写，不能撤回已经完成的磁盘写入。

## 防止重复开始

输入快照、运行身份和占用状态应在第一个异步等待之前建立。运行中重复点击应返回或提示已有任务，不能再启动同一组计算。

对会写同一文件的操作，还需要协调输出目录与文件归属；单纯禁用一个按钮，不能防住其他入口同时覆盖文件。

## 进度必须有实际依据

可计量的分批处理可以显示 `processed / total`。未知总行数、等待模型响应或等待外部程序启动时，显示阶段、活动状态和已知日志即可。

不要把网络字节数假装成整个工作流完成度，也不要让进度先走到 100% 再开始保存。任务完成应对应最终产出已经保存和必要结果已经核对。

## 超时不是总时长限制

将检查工作区、连接服务、启动程序和保存文件分别设置边界。错误明确到具体阶段，例如“模型连接超时”“Python 工具未启动”或“保存结果失败”。

`Promise.race` 可以让等待超时，但本身不会取消后台操作。若采用它，还要处理旧操作晚到及真实停止。正常运行较久的计算不应被一个过短的启动计时器误判。

## 取消分成两个动作

1. 让当前运行身份失效，阻止旧回调继续影响界面。
2. 对已经开始的网络、工具或进程调用真实取消机制，并确认其状态。

资料统计示例只处理本地批次，递增 runId 后就不会进入下一批。换成 Python、编译或模型请求后，不能照搬这一结论宣称后台已经停止。

## 错误与重试

错误记录至少包含阶段和经过脱敏的原因。保留用户输入与已完成产出，不在 `finally` 中清空所有状态。重试当前阶段、从检查点继续和重新开始应是三个明确选择。

重试要有触发条件和次数边界。路径错误、版本不足和缺少接口不能靠无限重试解决。
