@clawhub-dmpm-mininglamp-a0bc66a733
通过 dm-cli 命令行工具与 DeepMiner (DM) 系统交互。**凡是涉及 DM/DeepMiner 系统的任务,必须先读此 Skill。** 触发条件:用户提到 "使用 DM"、"发给 DM"、"用 DM 问一下"、"DM 帮我..."、"DeepMiner 执行..." 等任何涉及 DM 系统的任...
---
name: DeepMiner-skills
description: 通过 dm-cli 命令行工具与 DeepMiner (DM) 系统交互。**凡是涉及 DM/DeepMiner 系统的任务,必须先读此 Skill。** 触发条件:用户提到 "使用 DM"、"发给 DM"、"用 DM 问一下"、"DM 帮我..."、"DeepMiner 执行..." 等任何涉及 DM 系统的任务请求。即使对话历史中已使用过 dm-cli,也必须先读此 Skill 确保遵循完整指导。
---
# DeepMiner-skills
用于与 DM 系统交互的命令行工具。
## 🚀 从零开始(安装指南)
### 步骤 1:安装 dm-cli
```bash
npm install -g deepminer-cli
```
### 步骤 2:配置 AccessKey
```bash
dm-cli config init --endpoint <API端点> --accesskey <AccessKey> --json
```
### 步骤 3:验证配置
```bash
dm-cli auth status --json
```
---
## ⚠️ 核心规则
### 0. 版本检查(每次使用必做)
**每次使用本 Skill 前,必须检查版本是否为最新。**
**检查方法:**
```bash
# 获取 ClawHub 上的最新版本
curl -s "https://clawhub.ai/dmpm-mininglamp/dmskills" | grep -oP 'version:"[^"]+"' | head -1 | cut -d'"' -f2
```
**当前最新版本:** `1.4.0`
**本地版本检查:**
查看本 SKILL.md 文件顶部的版本标记,或检查 `_meta.json` 文件。
**版本不一致时:**
```
⚠️ DeepMiner-skills 版本过旧!
本地版本:x.x.x
最新版本:1.1.0
请访问 https://clawhub.ai/dmpm-mininglamp/dmskills 下载最新版本,
或使用 clawhub CLI 更新:
clawhub install dmpm-mininglamp/dmskills
更新后继续执行任务。
```
**为什么重要:**
- 新版本可能修复关键 bug(如 async_tag_task 状态处理)
- 新版本可能增加重要功能(如新的轮询机制)
- 确保用户获得最新、最准确的指导
### 1. Prompt 原样传递(禁止修改)
用户的 prompt 必须原样传递给 DM,禁止任何改写、润色或添加解释。
### 2. DM 返回内容原样呈现
DM 返回的 `last_messages` 必须原样提取并返回,禁止省略、改写或总结。
### 3. 追问 vs 新会话判断
- 用户说"再..."、"那..."、"继续..."等信号词时,用 `--thread-id` 追问
- 完全独立的新主题时,新开 thread
### 4. 默认使用 subagent 执行任务
**⚠️ 除非用户明确要求在主 session 执行,否则一律使用 subagent!**
**原因**:
- DM 任务执行时间不可预知(几秒到几分钟不等)
- 主 session 被占用时,用户无法继续对话
- 用户可能在等待期间追问或发出新指令
**使用 subagent 的方式**:
```typescript
sessions_spawn({
runtime: "subagent",
task: "使用 dm-cli 执行 DM 任务:{用户原始请求}",
mode: "run"
})
```
**例外情况(可以在主 session 执行)**:
- 用户明确说"直接执行"、"不要用 subagent"
- 任务非常简单且预计秒级返回(如查询状态)
**为什么重要**:
- 保持主 session 响应能力
- 用户体验更好(不用等待轮询完成才能继续聊)
### 5. 追问前必须检查状态
**⚠️ 追问前必须先检查上一次任务状态!**
**原因**:如果上一次任务还在 `running`,直接追问可能会失败或被拒绝。
**流程**:
```
1. 用户发送追问指令
↓
2. 检查上一次 DM 任务状态
dm-cli thread result --thread-id <id> --json
↓
3. 根据 state 决定下一步:
- running → 需要使用 --force(见下方说明)
- ask_human/completed/failed → 直接追问
```
**代码示例**:
```bash
# 追问前检查状态
prev_result=$(dm-cli thread result --thread-id "$thread_id" --json)
prev_state=$(echo "$prev_result" | jq -r '.data.state')
case "$prev_state" in
"running")
# 需要告知用户风险,确认后使用 --force
echo "⚠️ 上一次任务仍在执行中,追问会中断当前任务。"
echo "是否继续?(使用 --force 强制追问)"
# 等待用户确认后执行
dm-cli thread start --thread-id "$thread_id" --message "追问内容" --force --json
;;
"ask_human"|"completed"|"failed")
# 直接追问
dm-cli thread start --thread-id "$thread_id" --message "追问内容" --json
;;
esac
```
### 5. `--force` 参数说明
**作用**:强制停止当前正在运行的 agent_run,立即接受新消息。
**使用场景**:
- 上一次任务还在 `running` 状态
- 用户想要追加新指令或改变方向
**风险**:
- ⚠️ 正在执行的任务会被**中断**
- 可能丢失中间结果
- 应该告知用户这个风险
**示例**:
```bash
# 不带 --force(running 状态可能失败)
dm-cli thread start --thread-id abc123 --message "新问题" --json
# 可能返回错误:会话正在运行中
# 带 --force(强制中断当前任务)
dm-cli thread start --thread-id abc123 --message "新问题" --force --json
# 成功,但之前的任务被中断
```
**最佳实践**:
1. 先检查状态,告知用户风险
2. 用户确认后再使用 `--force`
3. 如果任务很重要,建议等 `completed` 后再追问
### 6. 异步任务的两步确认机制
**⚠️ 启动异步任务必须用 CLI 命令,不能用追问消息!**
**背景**:DM 对大规模异步任务(如批量标注)设计了双重确认机制,防止误触发消耗大量积分。
---
**❌ 错误做法(会导致任务被取消)**
```bash
# 错误:用追问消息启动任务
dm-cli thread start --thread-id "$thread_id" --message "请开始执行异步标注任务"
# 结果:任务被取消,状态变为 rejected
```
---
**✅ 正确流程**
| 步骤 | 命令 | 状态变化 |
|------|------|---------|
| **1️⃣** | `thread start --message "确认执行全量标注"` | `ask_human` → `async_tag_task` |
| **2️⃣** | 等待状态稳定,获取 task_id | `PENDING` |
| **3️⃣** | `task lifecycle --action start` | `PENDING` → `RUNNING`(真正启动) |
---
**完整代码示例**
```bash
# 步骤 1:第一次追问(提交异步任务)
dm-cli thread start --thread-id "$thread_id" --message "确认执行全量标注" --json
# 步骤 2:等待状态稳定,获取 task_id
sleep 10
result=$(dm-cli thread result --thread-id "$thread_id" --json)
state=$(echo "$result" | jq -r '.data.state')
task_id=$(echo "$result" | jq -r '.data.status_info.task_id')
if [ "$state" = "async_tag_task" ]; then
# 步骤 3:用 CLI 命令启动任务(关键!)
dm-cli task lifecycle --thread-id "$thread_id" --task-id "$task_id" --action start --json
echo "✅ 异步任务已启动执行!"
echo "任务ID: $task_id"
fi
```
---
**关键命令:task lifecycle**
| 参数 | 说明 |
|------|------|
| `--thread-id` | 会话 ID |
| `--task-id` | 异步任务 ID(从 status_info.task_id 获取) |
| `--action` | 操作类型:`start`(启动)、`interrupt`(中断)、`resume`(恢复)、`cancel`(取消) |
---
**状态说明**
| 任务状态 | 可执行操作 |
|---------|-----------|
| `PENDING` | ✅ `start`(启动) |
| `RUNNING` | ✅ `interrupt`(中断) |
| `INTERRUPTED` | ✅ `resume`(恢复)或 `cancel`(取消) |
| `rejected` | ❌ 无法启动(任务已取消) |
---
**常见错误**
| 错误 | 结果 |
|------|------|
| ❌ 用追问消息启动 | 任务被取消,状态变为 `rejected` |
| ❌ 不等待状态稳定 | task_id 未返回,无法启动 |
| ❌ 任务状态不是 `PENDING` | CLI 返回 "Cannot start task in xxx state" |
---
**通知用户**
第一次追问后:
```
📋 异步任务已提交(PENDING)
任务ID: xxx
预估积分: 45-90 credits
等待 CLI 启动命令执行...
```
CLI 启动后:
```
✅ 异步任务已启动执行!
任务ID: xxx
请等待完成,轮询子代理会通知结果。
```
---
**为什么重要**
- 防止误触发大规模任务消耗积分
- 用追问消息会取消任务(系统设计)
- CLI 命令是唯一正确的启动方式
- 给用户第二次思考的机会
- 确保用户明确知道任务将要执行
---
## 执行流程
### 标准流程(非阻塞轮询)
```
1. 用户发送任务
↓
2. dm-cli thread start --message "用户原话" --json
↓
3. 获取 thread_id
↓
4. 启动轮询子代理(sessions_spawn)
↓
5. 告知用户 "DM 任务已提交..."
↓
6. 主代理保持响应,等待子代理通知
↓
7. 子代理检测到状态变化 → sessions_send 通知主会话
↓
8. 主代理收到通知,处理并返回结果
```
---
## 非阻塞轮询实现(核心)
### ⚠️ 必须使用 sessions_spawn 轮询
**原因**:
1. 同步 exec 会阻塞主代理,用户无法中断
2. 后台 exec 完成后只是系统消息,主代理不会自动响应
3. 需要子代理通过 `message` 工具**直接通知用户**
### 完整实现代码
**步骤 1:提交任务**
```bash
result=$(dm-cli thread start --message "用户原话" --json)
thread_id=$(echo "$result" | jq -r '.data.thread_id')
if [ -z "$thread_id" ] || [ "$thread_id" = "null" ]; then
echo "❌ 任务提交失败"
return
fi
```
**步骤 2:启动轮询子代理(关键!)**
使用 `sessions_spawn` 启动子代理,子代理会**直接向用户发送结果**:
```json
{
"action": "sessions_spawn",
"runtime": "subagent",
"mode": "run",
"label": "dm-poll-thread_id",
"timeoutSeconds": 3600,
"task": "你是 DM 轮询子代理。\n\n## 任务\n轮询 DM 任务状态,完成后**直接通知用户**。\n\n## 参数\n- thread_id: \"thread_id\"\n\n## 轮询流程\n\n1. 调用 `dm-cli thread result --thread-id thread_id --json` 获取状态\n2. 如果 state 是 running,等待后继续轮询(间隔 5/10/20/40/60 秒递增)\n3. 如果 state 变化:\n - **使用 message 工具通知用户**:`{\"action\": \"send\", \"message\": \"结果内容\"}`\n - 如果是终止状态(completed/ask_human/failed),退出\n\n## 状态处理\n\n| 状态 | 动作 |\n|------|------|\n| running | 继续轮询 |\n| async_tag_task | 通知用户去 GUI 确认(附具体链接 https://deepminer.com.cn/agents/thread_id),继续轮询 |\n| ask_human | 通知用户问题,退出 |\n| completed | 通知用户结果(含文件链接),退出 |\n| failed | 通知用户错误,退出 |\n\n## 通知格式示例\n\n**完成时**:\n```\n✅ DM 任务完成\n\nthread_id: xxx\n\n结果:...\n\n📄 文件:[链接]\n```\n\n**需要用户输入时**:\n```\n⏸️ DM 需要您的回复\n\n[问题内容]\n\n请回复后继续。\n```\n\n**重要**:必须使用 message 工具直接发送给用户,不要只是输出到 stdout!\n\n开始执行轮询。"
}
```
**步骤 3:告知用户任务已提交**
```
⏳ DM 任务已提交,正在后台处理中...
```
**步骤 4:主代理保持响应**
- 子代理在后台轮询
- 完成后子代理**直接向用户发消息**
- 主代理无需介入,用户直接收到结果
**步骤 5:用户可随时中断**
```
用户: "停止"
主代理: subagents kill dm-poll-thread_id
```
---
## 轮询子代理 Task 模板
**主代理 spawn 子代理时,直接使用此模板作为 task 参数:**
```
你是 DM 轮询子代理。
## ⚠️ 核心规则
1. **必须遍历所有 last_messages**,不能只看第一条
2. **必须原样呈现**,禁止总结、省略、改写 DM 的回复
3. **必须提取所有文件链接**,不能遗漏任何 attachment
## 参数
- thread_id: thread_id
## 轮询流程
1. 使用 `exec` 工具调用 `dm-cli thread result --thread-id thread_id --json`
2. 解析 `.data.state` 字段
3. 根据 state 处理:
| state | 动作 |
|-------|------|
| running | 等待 5/10/20/40/60 秒后继续轮询 |
| async_tag_task | 通知用户去 GUI 确认(链接:https://deepminer.com.cn/agents/thread_id),继续轮询 |
| ask_human | 输出问题,停止 |
| completed | 输出结果,停止 |
| failed | 输出错误,停止 |
## ⚠️ 通知方式
**直接输出以下格式的文本**(会自动推送给主代理,主代理转发给用户):
```
✅ DM 任务完成
thread_id: thread_id
**结果:**
[遍历 last_messages,原样输出每条 assistant 消息的 content]
**文件:**
[遍历 last_messages,提取所有 tool 消息的 artifact.attachments,列出所有文件链接]
```
## ⚠️ 关键提醒
- **不要调用 message 或 sessions_send 工具**
- **直接输出文本**,系统会自动推送
- 停止后你的输出会通过 subagent_announce 发送给主代理
- 主代理收到后会转发给用户
开始执行轮询任务。
```
---
## 主代理调用示例
```json
{
"action": "sessions_spawn",
"runtime": "subagent",
"mode": "run",
"label": "dm-poll-thread_id",
"timeoutSeconds": 3600,
"task": "<上面的模板内容,替换 thread_id 为实际值>"
}
```
---
## 用户中断处理
当用户发送"停止"、"中止"指令时:
```bash
subagents kill --target dm-poll-thread_id
```
---
## 状态处理总结
| 状态 | 类型 | 子代理行为 | 主代理动作 |
|------|------|-----------|-----------|
| `running` | 执行中 | 继续轮询 | 等待 |
| `async_tag_task` | 待 GUI 确认 | 通知用户去 GUI 确认(附链接 https://deepminer.com.cn/agents/thread_id),继续轮询 | 告知用户去 GUI 确认,附具体会话链接 |
| `ask_human` | 等待输入 | 输出问题,停止 | 返回问题,等待追问 |
| `completed` | 成功 | 输出结果,停止 | 提取结果,返回用户 |
| `failed` | 失败 | 输出错误,停止 | 返回错误信息 |
---
## 异步任务轮询特殊处理(async_tag_task)
### 问题背景
DM 的**打标注任务**会进入 `async_tag_task` 状态,此时需要用户在 GUI 上确认后才能继续执行。这带来一个设计难题:
| 方案 | 结果 |
|------|------|
| 遇到 `async_tag_task` 立即停止并通知 | ✅ 用户知道去 GUI 确认<br>❌ 但没人继续跟踪任务完成 |
| 遇到 `async_tag_task` 继续静默轮询 | ✅ 能跟踪到最终完成<br>❌ 用户不知道要去 GUI 确认 |
### 解决方案:分层轮询设计
**核心原则:** 子代理**只输出、不停留**,主代理负责决策和重启轮询。
```
流程:
1. 启动子代理轮询
2. 子代理检测到 `async_tag_task`
3. 子代理输出通知 → 停止(触发 subagent_announce)
4. 主代理收到通知,告知用户去 GUI 确认
5. 【关键】主代理提示:"去 GUI 确认后,请发送 '继续'"
6. 用户去 GUI 确认,回来后发送 "继续"
7. 主代理启动新子代理,继续轮询到 completed
```
### 子代理 Task 模板(正确版本)
```
你是 DM 轮询子代理。
## 任务
轮询 DM 任务状态,**关键状态变化后立即停止并输出**。
## 参数
- thread_id: thread_id
## 状态处理规则
| state | 动作 |
|-------|------|
| running | sleep 15秒,继续轮询 |
| async_tag_task | **输出通知 + 停止**(等待用户 GUI 操作) |
| ask_human | 输出问题 + 停止 |
| completed | 输出完整结果(含文件链接)+ 停止 |
| failed | 输出错误 + 停止 |
## ⚠️ 关键要求
- 遍历所有 last_messages,原样呈现
- 提取所有文件附件链接
- **非终止状态(running)才继续轮询**
- 遇到 async_tag_task/ask_human/completed/failed **立即停止**
开始执行轮询。
```
### 主代理处理模板
```typescript
// 1. 首次启动轮询
sessions_spawn({
runtime: "subagent",
mode: "run",
label: "dm-poll-thread_id",
timeoutSeconds: 600,
task: "<上面的子代理模板>"
})
// 2. 收到 subagent_announce 后判断
if (result.contains("async_tag_task")) {
// 通知用户去 GUI 确认
reply("⏸️ 需要你去 GUI 确认...\n\n确认后请发送 '继续'")
} else if (result.contains("completed")) {
// 任务完成,输出结果
reply("✅ 任务完成...")
}
// 3. 用户发送 "继续" 后,启动新轮询
if (user_message == "继续") {
sessions_spawn({
runtime: "subagent",
mode: "run",
label: "dm-poll-thread_id-2",
timeoutSeconds: 600,
task: "<同上>"
})
}
```
### 关键点
1. **子代理不要用 message 工具** —— subagent 模式没有 message 工具
2. **子代理遇到 async_tag_task 必须停止** —— 才能触发 subagent_announce 通知主代理
3. **主代理必须提示用户 "完成后发送继续"** —— 否则用户不知道要回来触发新轮询
4. **用递增的 label** —— 如 `dm-poll-xxx-2`、`dm-poll-xxx-3`,避免冲突
---
## 关键优势
| 问题 | 解决方案 |
|------|----------|
| 主代理阻塞 | 使用 `sessions_spawn` 子代理轮询 |
| 无法中断 | `subagents kill` 终止子代理 |
| 通知延迟/丢失 | 子代理直接输出文本,auto-announce 推送 |
| 无限重试 | timeoutSeconds 限制,或用户手动终止 |
| **async_tag_task 无法通知** | **分层设计:停止→通知→重启轮询** |
---
## 错误处理
| Exit Code | 处理方式 |
|-----------|---------|
| 0 | 成功 |
| 2 | 校验错误,检查参数 |
| 3 | 认证失败,重新配置 Access Key |
| 4 | 网络错误,检查 endpoint |
| 5 | 内部错误,检查文件路径/权限 |
| 6 | 权限不足 |
FILE:CHANGELOG.md
# Changelog
## 1.2.0 (2026-04-10)
### 改进
- 优化了 async_tag_task 状态处理逻辑
- 改进了 ask_human 状态的判断机制
- 添加了更详细的轮询策略说明
- 完善了错误处理文档
### 文档
- 更新了 SKILL.md 使用说明
- 添加了 response-structure.md 参考文档
FILE:_meta.json
{
"name": "DeepMiner-skills",
"version": "1.4.0",
"description": "通过 dm-cli 命令行工具与 DeepMiner (DM) 系统交互",
"author": "Humancall",
"updated_at": "2026-04-13"
}
FILE:references/response-structure.md
# thread.result 返回结构解析
## 顶层结构
```json
{
"status_code": 200,
"message": "success",
"data": {
"state": "running | completed",
"thread_id": "xxx",
"last_messages": [...]
}
}
```
**关键字段:**
- `state` — 任务状态,可能的值:
- `running` — 任务进行中,继续轮询
- `completed` — 任务完成,解析结果
- `ask_human` — 需要用户确认,回复继续
- `async_tag_task` — **异步任务已提交,需在 DM 平台 GUI 确认** ⚠️
- `last_messages` — 消息数组,按时间顺序排列(最后一条是最新的)
## last_messages 消息类型
### assistant 消息(文字回复)
```json
{
"message_id": "uuid",
"type": "assistant",
"content": "{\"role\": \"assistant\", \"content\": \"文字内容...\", ...}",
"created_at": "2026-04-03T07:38:21.843752+00:00",
"metadata": "{\"worker_name\": \"supervisor\", \"worker_id\": \"xxx\", ...}"
}
```
**解析步骤:**
1. 找 `type: "assistant"` 的消息
2. `content` 是 JSON 字符串,需 parse
3. 取 parse 后的 `.content` 字段即为文字回复
**metadata 字段:**
- `worker_name` — 处理任务的 worker 类型(如 `supervisor`, `taobao_worker`)
- `observation_id`, `trace_id` — 追踪信息
### tool 消息(文件输出)
```json
{
"message_id": "uuid",
"type": "tool",
"content": "{\"name\": \"final_files\", \"artifact\": {...}}",
"status": "success"
}
```
**解析步骤:**
1. 找 `type: "tool"` 且 `name: "final_files"` 的消息
2. `content` 是 JSON 字符串,需 parse
3. 取 `artifact.attachments` 数组获取文件链接
**attachments 结构:**
```json
[
{"type": "file", "data": "https://.../报告.html"},
{"type": "file", "data": "https://.../数据.csv"}
]
```
每个 attachment 的 `data` 字段是可直接访问的文件 URL。
## 解析原则
⚠️ **必须遍历所有 `last_messages`,提取每条消息的内容!**
常见遗漏场景:
- tool 消息包含多个文件(如 HTML 报告 + CSV 原始数据)
- 多条 assistant 消息(如中间进度 + 最终回复)
**正确做法:遍历整个数组,逐一提取并汇总告知用户。**
```json
{
"data": {
"state": "completed",
"last_messages": [
{
"type": "tool",
"content": "{\"name\": \"final_files\", \"artifact\": {\"attachments\": [{\"type\": \"file\", \"data\": \"https://dm-test.xmingai.com/files/.../报告.html\"}]}}"
},
{
"type": "assistant",
"content": "{\"content\": \"我已完成分析...\"}"
}
]
}
}
```
**正确提取逻辑(遍历所有消息):**
```python
# 伪代码 - 必须遍历整个 last_messages 数组
files = []
text_replies = []
for msg in last_messages:
content = json.loads(msg["content"])
if msg["type"] == "tool":
if content.get("name") == "final_files":
attachments = content["artifact"]["attachments"]
# 每个文件都要记录!
for att in attachments:
files.append(att["data"])
if msg["type"] == "assistant":
text_replies.append(content["content"])
# 汇总告知用户
print("文件输出:", files) # ["https://.../报告.html", "https://.../数据.csv"]
print("文字回复:", text_replies) # ["我已完成分析..."]
```
**错误示范(只取第一条):**
❌ 只提取第一个文件,漏掉 CSV
❌ 只看最后一条消息,漏掉中间的进度信息
## worker_name 参考
| worker_name | 功能 |
|-------------|------|
| `supervisor` | 总控,协调任务 |
| `taobao_worker` | 淘宝数据查询 |
| 其他 worker | 按任务类型分配 |
通过 `metadata.worker_name` 可识别任务由哪个专业模块处理。
## async_tag_task 状态(异步任务)
⚠️ **当 `state: "async_tag_task"` 时,必须提示用户去 DM 平台 GUI 确认!**
异步任务已提交到后台队列,但不会自动开始执行,需要用户在 DM 平台进行 GUI 操作确认。
### 直接读取 status_info 字段
**不需要解析复杂的 messages!** 直接从 `data.status_info` 获取任务信息:
```json
{
"data": {
"state": "async_tag_task",
"status_info": {
"task_id": "5f56f532-f11f-4405-8a2c-9032fe8b059c",
"task_name": "社媒内容品牌名称标注",
"tool_name": "submit_async_task"
}
}
}
```
**关键字段:**
| 字段 | 说明 |
|------|------|
| `status_info.task_id` | 任务ID |
| `status_info.task_name` | 任务名称 |
| `status_info.tool_name` | `submit_async_task` 表示异步任务 |
### 提示用户的内容
检测到 `state: "async_tag_task"` 后,**必须告知用户:**
> ⚠️ **异步任务已提交(任务ID: {task_id},任务名称: {task_name})**
>
> 请前往 **DM 平台** 进行 GUI 确认操作,任务才会开始执行。
FILE:scripts/dm-poll-notify.sh
#!/bin/bash
# DM 轮询脚本 - 状态变化时通过 sessions_send 通知主会话
#
# 用法: dm-poll-notify.sh <thread_id> [parent_session_key] [max_timeout_minutes]
#
# 参数:
# thread_id - DM 会话 ID(必填)
# parent_session_key - 主会话标识(默认: main)
# max_timeout - 最大轮询时间(分钟,默认: 60)
#
# 输出:
# 状态变化时通过 sessions_send 发送通知
# 通知格式: DM_POLL_NOTIFY:<thread_id>:<state>:<base64编码的result>
#
# 退出码:
# 0 - 正常退出(终止状态)
# 1 - 失败状态
# 2 - 超时
# 3 - 参数错误
set -e
THREAD_ID="$1"
PARENT_SESSION="-main"
MAX_TIMEOUT="-60"
# 参数检查
if [ -z "$THREAD_ID" ]; then
echo "用法: $0 <thread_id> [parent_session_key] [max_timeout_minutes]"
exit 3
fi
# 轮询间隔(秒):递增策略
INTERVALS=(5 10 20 40 60)
interval_idx=0
total_seconds=0
max_seconds=$((MAX_TIMEOUT * 60))
last_state=""
# 发送通知给主会话
send_notification() {
local state="$1"
local result="$2"
# Base64 编码结果(避免特殊字符问题)
local encoded=""
if command -v base64 &> /dev/null; then
encoded=$(echo -n "$result" | base64 | tr -d '\n' 2>/dev/null || echo "")
fi
# 如果 base64 编码失败,使用原始结果(URL编码特殊字符)
if [ -z "$encoded" ]; then
encoded="$result"
fi
# 发送通知
local message="DM_POLL_NOTIFY:THREAD_ID:state:encoded"
echo "[NOTIFY] 发送通知: state=$state"
# 使用 sessions_send 发送消息
# 注意:这需要在 OpenClaw 环境中运行
if command -v sessions_send &> /dev/null; then
sessions_send --sessionKey "$PARENT_SESSION" --message "$message" 2>/dev/null || true
else
# 如果 sessions_send 不可用,输出到标准输出
echo "$message"
fi
}
# 提取关键信息
extract_info() {
local result="$1"
local state="$2"
local info=""
case "$state" in
"ask_human")
# 提取问题
info=$(echo "$result" | jq -r '.data.message_display_to_human // ""' 2>/dev/null || echo "")
;;
"async_tag_task")
# 提取任务名称
info=$(echo "$result" | jq -r '.data.status_info.task_name // ""' 2>/dev/null || echo "")
;;
"completed"|"failed")
# 提取最后消息
info=$(echo "$result" | jq -r '.data.last_messages[0].content // ""' 2>/dev/null | head -c 200 || echo "")
;;
esac
echo "$info"
}
echo "[START] 开始轮询 thread_id=$THREAD_ID, parent_session=$PARENT_SESSION"
# 主轮询循环
while true; do
# 检查超时
if [ $total_seconds -ge $max_seconds ]; then
echo "[TIMEOUT] 轮询超时 (MAX_TIMEOUT分钟)"
send_notification "timeout" "{\"error\": \"轮询超时\", \"thread_id\": \"$THREAD_ID\"}"
exit 2
fi
# 执行轮询
echo "[POLL] 查询状态..."
result=$(dm-cli thread result --thread-id "$THREAD_ID" --json 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "[ERROR] dm-cli 执行失败: exit_code=$exit_code"
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
# 解析状态
state=$(echo "$result" | jq -r '.data.state // "unknown"' 2>/dev/null || echo "unknown")
if [ "$state" = "unknown" ] || [ -z "$state" ]; then
echo "[WARN] 无法解析状态,原始响应: 0:200..."
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
echo "[STATE] 当前状态: $state"
# 检测状态变化
if [ "$state" != "$last_state" ]; then
echo "[STATE_CHANGE] $last_state -> $state"
# 发送通知
send_notification "$state" "$result"
last_state="$state"
fi
# 处理终止状态
case "$state" in
"completed")
echo "[EXIT] 任务完成"
exit 0
;;
"ask_human")
question=$(extract_info "$result" "$state")
echo "[EXIT] 需要用户输入: 0:100..."
exit 0
;;
"failed")
echo "[EXIT] 任务失败"
exit 1
;;
esac
# 计算等待时间
interval=-60
echo "[WAIT] 等待 intervals 后继续轮询..."
sleep $interval
# 递增间隔
if [ $interval_idx -lt $((#INTERVALS[@] - 1)) ]; then
interval_idx=$((interval_idx + 1))
fi
total_seconds=$((total_seconds + interval))
done
FILE:scripts/dm-poll-once.sh
#!/bin/bash
# DM 轮询脚本 - 状态变化时通过 message 工具直接通知用户
#
# 用法: dm-poll-once.sh <thread_id> <status_file> [max_timeout_minutes]
#
# 功能:
# - 状态变化时调用 message 工具通知用户
# - 用户直接收到结果,无需主代理介入
#
# 输出格式(单行,供 Exec completed 日志):
# DM_POLL_RESULT:<state>:<thread_id>
set -e
THREAD_ID="$1"
STATUS_FILE="$2"
MAX_TIMEOUT="-60"
if [ -z "$THREAD_ID" ] || [ -z "$STATUS_FILE" ]; then
echo "DM_POLL_ERROR:参数错误"
exit 3
fi
INTERVALS=(5 10 20 40 60)
interval_idx=0
total_seconds=0
max_seconds=$((MAX_TIMEOUT * 60))
last_state=""
write_status() {
local state="$1"
local prev_state="$2"
local result="$3"
mkdir -p "$(dirname "$STATUS_FILE")" 2>/dev/null || true
echo "STATE=$state" > "$STATUS_FILE"
echo "THREAD_ID=$THREAD_ID" >> "$STATUS_FILE"
echo "PREV_STATE=$prev_state" >> "$STATUS_FILE"
echo "TIMESTAMP=$(date -Iseconds)" >> "$STATUS_FILE"
echo "$result" > "STATUS_FILE.json"
}
# 通知用户(通过 OpenClaw message 工具)
notify_user() {
local state="$1"
local thread_id="$2"
local message="$3"
# 构建通知内容
local notification=""
case "$state" in
"completed")
notification="✅ **DM 任务完成**\n\nthread_id: \`$thread_id\`\n\n$message"
;;
"ask_human")
notification="⏸️ **DM 需要您的回复**\n\n$message\n\n请回复后继续。"
;;
"failed")
notification="❌ **DM 任务失败**\n\nthread_id: \`$thread_id\`\n\n$message"
;;
"async_tag_task")
notification="⚠️ **DM 异步任务待确认**\n\n任务: $message\n\n请前往 DM 平台 GUI 确认。"
;;
esac
# 使用 message 工具发送通知
if command -v message &> /dev/null; then
message --action send --message "$notification" 2>/dev/null || true
elif command -v sessions_send &> /dev/null; then
sessions_send --sessionKey main --message "$notification" 2>/dev/null || true
fi
}
# 提取结果摘要
extract_summary() {
local result="$1"
local state="$2"
case "$state" in
"completed")
# 提取文件链接
local file_url=$(echo "$result" | jq -r '.data.last_messages[-1].content' 2>/dev/null | jq -r '.artifact.attachments[0].data // empty' 2>/dev/null || echo "")
local text=$(echo "$result" | jq -r '.data.last_messages[0].content' 2>/dev/null | jq -r '.content // empty' 2>/dev/null | head -c 300 || echo "")
if [ -n "$file_url" ]; then
echo "📄 文件: $file_url\n\n$text"
else
echo "$text"
fi
;;
"ask_human")
echo "$result" | jq -r '.data.message_display_to_human // ""' 2>/dev/null || echo ""
;;
"failed")
echo "$result" | jq -r '.error.message // "任务失败"' 2>/dev/null || echo "任务失败"
;;
"async_tag_task")
echo "$result" | jq -r '.data.status_info.task_name // ""' 2>/dev/null || echo ""
;;
*)
echo ""
;;
esac
}
while true; do
if [ $total_seconds -ge $max_seconds ]; then
write_status "timeout" "$last_state" ""
echo "DM_POLL_RESULT:timeout:$THREAD_ID"
exit 2
fi
result=$(dm-cli thread result --thread-id "$THREAD_ID" --json 2>&1) || {
sleep 10
total_seconds=$((total_seconds + 10))
continue
}
state=$(echo "$result" | jq -r '.data.state // "unknown"' 2>/dev/null || echo "unknown")
[ "$state" = "unknown" ] && { sleep 10; total_seconds=$((total_seconds + 10)); continue; }
# 状态变化
if [ -n "$last_state" ] && [ "$state" != "$last_state" ]; then
write_status "$state" "$last_state" "$result"
# 中间状态变化也通知用户
case "$state" in
"async_tag_task")
local task_name=$(echo "$result" | jq -r '.data.status_info.task_name // ""' 2>/dev/null)
notify_user "$state" "$THREAD_ID" "$task_name"
echo "DM_POLL_RESULT:$state:$THREAD_ID"
exit 0
;;
esac
fi
# 终止状态
case "$state" in
"completed")
write_status "completed" "$last_state" "$result"
local summary=$(extract_summary "$result" "completed")
notify_user "completed" "$THREAD_ID" "$summary"
echo "DM_POLL_RESULT:completed:$THREAD_ID"
exit 0
;;
"ask_human")
write_status "ask_human" "$last_state" "$result"
local question=$(extract_summary "$result" "ask_human")
notify_user "ask_human" "$THREAD_ID" "$question"
echo "DM_POLL_RESULT:ask_human:$THREAD_ID"
exit 0
;;
"failed")
write_status "failed" "$last_state" "$result"
local error=$(extract_summary "$result" "failed")
notify_user "failed" "$THREAD_ID" "$error"
echo "DM_POLL_RESULT:failed:$THREAD_ID"
exit 1
;;
esac
last_state="$state"
sleep -60
[ $interval_idx -lt 4 ] && interval_idx=$((interval_idx + 1))
total_seconds=$((total_seconds + -60))
done
FILE:scripts/dm-poll.sh
#!/bin/bash
# DM 任务轮询脚本
# 用法: ./dm-poll.sh <thread_id> <status_file> [max_timeout_minutes]
#
# 输出:
# - 状态变化时写入 status_file
# - 终止状态时退出并返回最终结果
#
# 状态文件格式:
# STATE=<state>
# THREAD_ID=<thread_id>
# MESSAGE=<message_display_to_human 或 task_name>
# RESULT_JSON=<完整的 result JSON>
set -e
THREAD_ID="$1"
STATUS_FILE="$2"
MAX_TIMEOUT="-60" # 默认 60 分钟
if [ -z "$THREAD_ID" ] || [ -z "$STATUS_FILE" ]; then
echo "用法: $0 <thread_id> <status_file> [max_timeout_minutes]"
exit 1
fi
# 轮询间隔(秒):递增策略
INTERVALS=(5 10 20 40 60)
interval_index=0
total_seconds=0
max_seconds=$((MAX_TIMEOUT * 60))
last_state=""
write_status() {
local state="$1"
local message="$2"
local result_json="$3"
cat > "$STATUS_FILE" << EOF
STATE=$state
THREAD_ID=$THREAD_ID
MESSAGE=$message
RESULT_JSON=$result_json
TIMESTAMP=$(date -Iseconds)
EOF
}
# 主轮询循环
while true; do
# 检查超时
if [ $total_seconds -ge $max_seconds ]; then
write_status "timeout" "轮询超时(MAX_TIMEOUT分钟)" ""
echo "[TIMEOUT] 轮询超时,thread_id=$THREAD_ID"
exit 2
fi
# 执行轮询
result=$(dm-cli thread result --thread-id "$THREAD_ID" --json 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
# 网络错误或其他错误,记录但继续重试
echo "[ERROR] dm-cli 返回错误: $result"
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
# 解析状态
state=$(echo "$result" | jq -r '.data.state // empty')
if [ -z "$state" ]; then
echo "[ERROR] 无法解析状态: $result"
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
# 检测状态变化
if [ "$state" != "$last_state" ]; then
echo "[STATE_CHANGE] $last_state -> $state"
last_state="$state"
# 根据状态处理
case "$state" in
"running")
# running 状态变化,只记录,不退出
write_status "running" "任务执行中..." "$result"
;;
"async_tag_task")
# 需要 GUI 确认,提取 task_name
task_name=$(echo "$result" | jq -r '.data.status_info.task_name // "未知任务"')
write_status "async_tag_task" "$task_name" "$result"
# 继续轮询,不退出
;;
"ask_human")
# 需要用户输入,提取问题
question=$(echo "$result" | jq -r '.data.message_display_to_human // ""')
write_status "ask_human" "$question" "$result"
echo "[ASK_HUMAN] $question"
exit 0
;;
"completed")
# 成功完成
write_status "completed" "任务完成" "$result"
echo "[COMPLETED] 任务完成,thread_id=$THREAD_ID"
exit 0
;;
"failed")
# 失败
write_status "failed" "任务失败" "$result"
echo "[FAILED] 任务失败,thread_id=$THREAD_ID"
exit 1
;;
*)
# 未知状态
write_status "$state" "未知状态: $state" "$result"
echo "[UNKNOWN] 未知状态: $state"
;;
esac
fi
# 如果是终止状态,退出
case "$state" in
"ask_human"|"completed"|"failed")
exit 0
;;
esac
# 计算下一次轮询间隔
interval=-60
echo "[POLL] 状态=$state, 等待 intervals..."
sleep $interval
# 递增间隔
if [ $interval_index -lt $((#INTERVALS[@] - 1)) ]; then
interval_index=$((interval_index + 1))
fi
total_seconds=$((total_seconds + interval))
done通过 dm-cli 命令行工具与 DeepMiner (DM) 系统交互。**凡是涉及 DM/DeepMiner 系统的任务,必须先读此 Skill。** 触发条件:用户提到 "使用 DM"、"发给 DM"、"用 DM 问一下"、"DM 帮我..."、"DeepMiner 执行..." 等任何涉及 DM 系统的任...
---
name: DeepMiner-skills
description: 通过 dm-cli 命令行工具与 DeepMiner (DM) 系统交互。**凡是涉及 DM/DeepMiner 系统的任务,必须先读此 Skill。** 触发条件:用户提到 "使用 DM"、"发给 DM"、"用 DM 问一下"、"DM 帮我..."、"DeepMiner 执行..." 等任何涉及 DM 系统的任务请求。即使对话历史中已使用过 dm-cli,也必须先读此 Skill 确保遵循完整指导。
---
# DeepMiner-skills
用于与 DM 系统交互的命令行工具。
## 🚀 从零开始(安装指南)
### 步骤 1:安装 dm-cli
```bash
npm install -g deepminer-cli
```
### 步骤 2:配置 AccessKey
```bash
dm-cli config init --endpoint <API端点> --accesskey <AccessKey> --json
```
### 步骤 3:验证配置
```bash
dm-cli auth status --json
```
---
## ⚠️ 核心规则
### 0. 版本检查(每次使用必做)
**每次使用本 Skill 前,必须检查版本是否为最新。**
**检查方法:**
```bash
# 获取 ClawHub 上的最新版本
curl -s "https://clawhub.ai/dmpm-mininglamp/dmskills" | grep -oP 'version:"[^"]+"' | head -1 | cut -d'"' -f2
```
**当前最新版本:** `1.4.0`
**本地版本检查:**
查看本 SKILL.md 文件顶部的版本标记,或检查 `_meta.json` 文件。
**版本不一致时:**
```
⚠️ DeepMiner-skills 版本过旧!
本地版本:x.x.x
最新版本:1.1.0
请访问 https://clawhub.ai/dmpm-mininglamp/dmskills 下载最新版本,
或使用 clawhub CLI 更新:
clawhub install dmpm-mininglamp/dmskills
更新后继续执行任务。
```
**为什么重要:**
- 新版本可能修复关键 bug(如 async_tag_task 状态处理)
- 新版本可能增加重要功能(如新的轮询机制)
- 确保用户获得最新、最准确的指导
### 1. Prompt 原样传递(禁止修改)
用户的 prompt 必须原样传递给 DM,禁止任何改写、润色或添加解释。
### 2. DM 返回内容原样呈现
DM 返回的 `last_messages` 必须原样提取并返回,禁止省略、改写或总结。
### 3. 追问 vs 新会话判断
- 用户说"再..."、"那..."、"继续..."等信号词时,用 `--thread-id` 追问
- 完全独立的新主题时,新开 thread
### 4. 默认使用 subagent 执行任务
**⚠️ 除非用户明确要求在主 session 执行,否则一律使用 subagent!**
**原因**:
- DM 任务执行时间不可预知(几秒到几分钟不等)
- 主 session 被占用时,用户无法继续对话
- 用户可能在等待期间追问或发出新指令
**使用 subagent 的方式**:
```typescript
sessions_spawn({
runtime: "subagent",
task: "使用 dm-cli 执行 DM 任务:{用户原始请求}",
mode: "run"
})
```
**例外情况(可以在主 session 执行)**:
- 用户明确说"直接执行"、"不要用 subagent"
- 任务非常简单且预计秒级返回(如查询状态)
**为什么重要**:
- 保持主 session 响应能力
- 用户体验更好(不用等待轮询完成才能继续聊)
### 5. 追问前必须检查状态
**⚠️ 追问前必须先检查上一次任务状态!**
**原因**:如果上一次任务还在 `running`,直接追问可能会失败或被拒绝。
**流程**:
```
1. 用户发送追问指令
↓
2. 检查上一次 DM 任务状态
dm-cli thread result --thread-id <id> --json
↓
3. 根据 state 决定下一步:
- running → 需要使用 --force(见下方说明)
- ask_human/completed/failed → 直接追问
```
**代码示例**:
```bash
# 追问前检查状态
prev_result=$(dm-cli thread result --thread-id "$thread_id" --json)
prev_state=$(echo "$prev_result" | jq -r '.data.state')
case "$prev_state" in
"running")
# 需要告知用户风险,确认后使用 --force
echo "⚠️ 上一次任务仍在执行中,追问会中断当前任务。"
echo "是否继续?(使用 --force 强制追问)"
# 等待用户确认后执行
dm-cli thread start --thread-id "$thread_id" --message "追问内容" --force --json
;;
"ask_human"|"completed"|"failed")
# 直接追问
dm-cli thread start --thread-id "$thread_id" --message "追问内容" --json
;;
esac
```
### 5. `--force` 参数说明
**作用**:强制停止当前正在运行的 agent_run,立即接受新消息。
**使用场景**:
- 上一次任务还在 `running` 状态
- 用户想要追加新指令或改变方向
**风险**:
- ⚠️ 正在执行的任务会被**中断**
- 可能丢失中间结果
- 应该告知用户这个风险
**示例**:
```bash
# 不带 --force(running 状态可能失败)
dm-cli thread start --thread-id abc123 --message "新问题" --json
# 可能返回错误:会话正在运行中
# 带 --force(强制中断当前任务)
dm-cli thread start --thread-id abc123 --message "新问题" --force --json
# 成功,但之前的任务被中断
```
**最佳实践**:
1. 先检查状态,告知用户风险
2. 用户确认后再使用 `--force`
3. 如果任务很重要,建议等 `completed` 后再追问
### 6. 异步任务的两步确认机制
**⚠️ 启动异步任务必须用 CLI 命令,不能用追问消息!**
**背景**:DM 对大规模异步任务(如批量标注)设计了双重确认机制,防止误触发消耗大量积分。
---
**❌ 错误做法(会导致任务被取消)**
```bash
# 错误:用追问消息启动任务
dm-cli thread start --thread-id "$thread_id" --message "请开始执行异步标注任务"
# 结果:任务被取消,状态变为 rejected
```
---
**✅ 正确流程**
| 步骤 | 命令 | 状态变化 |
|------|------|---------|
| **1️⃣** | `thread start --message "确认执行全量标注"` | `ask_human` → `async_tag_task` |
| **2️⃣** | 等待状态稳定,获取 task_id | `PENDING` |
| **3️⃣** | `task lifecycle --action start` | `PENDING` → `RUNNING`(真正启动) |
---
**完整代码示例**
```bash
# 步骤 1:第一次追问(提交异步任务)
dm-cli thread start --thread-id "$thread_id" --message "确认执行全量标注" --json
# 步骤 2:等待状态稳定,获取 task_id
sleep 10
result=$(dm-cli thread result --thread-id "$thread_id" --json)
state=$(echo "$result" | jq -r '.data.state')
task_id=$(echo "$result" | jq -r '.data.status_info.task_id')
if [ "$state" = "async_tag_task" ]; then
# 步骤 3:用 CLI 命令启动任务(关键!)
dm-cli task lifecycle --thread-id "$thread_id" --task-id "$task_id" --action start --json
echo "✅ 异步任务已启动执行!"
echo "任务ID: $task_id"
fi
```
---
**关键命令:task lifecycle**
| 参数 | 说明 |
|------|------|
| `--thread-id` | 会话 ID |
| `--task-id` | 异步任务 ID(从 status_info.task_id 获取) |
| `--action` | 操作类型:`start`(启动)、`interrupt`(中断)、`resume`(恢复)、`cancel`(取消) |
---
**状态说明**
| 任务状态 | 可执行操作 |
|---------|-----------|
| `PENDING` | ✅ `start`(启动) |
| `RUNNING` | ✅ `interrupt`(中断) |
| `INTERRUPTED` | ✅ `resume`(恢复)或 `cancel`(取消) |
| `rejected` | ❌ 无法启动(任务已取消) |
---
**常见错误**
| 错误 | 结果 |
|------|------|
| ❌ 用追问消息启动 | 任务被取消,状态变为 `rejected` |
| ❌ 不等待状态稳定 | task_id 未返回,无法启动 |
| ❌ 任务状态不是 `PENDING` | CLI 返回 "Cannot start task in xxx state" |
---
**通知用户**
第一次追问后:
```
📋 异步任务已提交(PENDING)
任务ID: xxx
预估积分: 45-90 credits
等待 CLI 启动命令执行...
```
CLI 启动后:
```
✅ 异步任务已启动执行!
任务ID: xxx
请等待完成,轮询子代理会通知结果。
```
---
**为什么重要**
- 防止误触发大规模任务消耗积分
- 用追问消息会取消任务(系统设计)
- CLI 命令是唯一正确的启动方式
- 给用户第二次思考的机会
- 确保用户明确知道任务将要执行
---
## 执行流程
### 标准流程(非阻塞轮询)
```
1. 用户发送任务
↓
2. dm-cli thread start --message "用户原话" --json
↓
3. 获取 thread_id
↓
4. 启动轮询子代理(sessions_spawn)
↓
5. 告知用户 "DM 任务已提交..."
↓
6. 主代理保持响应,等待子代理通知
↓
7. 子代理检测到状态变化 → sessions_send 通知主会话
↓
8. 主代理收到通知,处理并返回结果
```
---
## 非阻塞轮询实现(核心)
### ⚠️ 必须使用 sessions_spawn 轮询
**原因**:
1. 同步 exec 会阻塞主代理,用户无法中断
2. 后台 exec 完成后只是系统消息,主代理不会自动响应
3. 需要子代理通过 `message` 工具**直接通知用户**
### 完整实现代码
**步骤 1:提交任务**
```bash
result=$(dm-cli thread start --message "用户原话" --json)
thread_id=$(echo "$result" | jq -r '.data.thread_id')
if [ -z "$thread_id" ] || [ "$thread_id" = "null" ]; then
echo "❌ 任务提交失败"
return
fi
```
**步骤 2:启动轮询子代理(关键!)**
使用 `sessions_spawn` 启动子代理,子代理会**直接向用户发送结果**:
```json
{
"action": "sessions_spawn",
"runtime": "subagent",
"mode": "run",
"label": "dm-poll-thread_id",
"timeoutSeconds": 3600,
"task": "你是 DM 轮询子代理。\n\n## 任务\n轮询 DM 任务状态,完成后**直接通知用户**。\n\n## 参数\n- thread_id: \"thread_id\"\n\n## 轮询流程\n\n1. 调用 `dm-cli thread result --thread-id thread_id --json` 获取状态\n2. 如果 state 是 running,等待后继续轮询(间隔 5/10/20/40/60 秒递增)\n3. 如果 state 变化:\n - **使用 message 工具通知用户**:`{\"action\": \"send\", \"message\": \"结果内容\"}`\n - 如果是终止状态(completed/ask_human/failed),退出\n\n## 状态处理\n\n| 状态 | 动作 |\n|------|------|\n| running | 继续轮询 |\n| async_tag_task | 通知用户去 GUI 确认(附具体链接 https://deepminer.com.cn/agents/thread_id),继续轮询 |\n| ask_human | 通知用户问题,退出 |\n| completed | 通知用户结果(含文件链接),退出 |\n| failed | 通知用户错误,退出 |\n\n## 通知格式示例\n\n**完成时**:\n```\n✅ DM 任务完成\n\nthread_id: xxx\n\n结果:...\n\n📄 文件:[链接]\n```\n\n**需要用户输入时**:\n```\n⏸️ DM 需要您的回复\n\n[问题内容]\n\n请回复后继续。\n```\n\n**重要**:必须使用 message 工具直接发送给用户,不要只是输出到 stdout!\n\n开始执行轮询。"
}
```
**步骤 3:告知用户任务已提交**
```
⏳ DM 任务已提交,正在后台处理中...
```
**步骤 4:主代理保持响应**
- 子代理在后台轮询
- 完成后子代理**直接向用户发消息**
- 主代理无需介入,用户直接收到结果
**步骤 5:用户可随时中断**
```
用户: "停止"
主代理: subagents kill dm-poll-thread_id
```
---
## 轮询子代理 Task 模板
**主代理 spawn 子代理时,直接使用此模板作为 task 参数:**
```
你是 DM 轮询子代理。
## ⚠️ 核心规则
1. **必须遍历所有 last_messages**,不能只看第一条
2. **必须原样呈现**,禁止总结、省略、改写 DM 的回复
3. **必须提取所有文件链接**,不能遗漏任何 attachment
## 参数
- thread_id: thread_id
## 轮询流程
1. 使用 `exec` 工具调用 `dm-cli thread result --thread-id thread_id --json`
2. 解析 `.data.state` 字段
3. 根据 state 处理:
| state | 动作 |
|-------|------|
| running | 等待 5/10/20/40/60 秒后继续轮询 |
| async_tag_task | 通知用户去 GUI 确认(链接:https://deepminer.com.cn/agents/thread_id),继续轮询 |
| ask_human | 输出问题,停止 |
| completed | 输出结果,停止 |
| failed | 输出错误,停止 |
## ⚠️ 通知方式
**直接输出以下格式的文本**(会自动推送给主代理,主代理转发给用户):
```
✅ DM 任务完成
thread_id: thread_id
**结果:**
[遍历 last_messages,原样输出每条 assistant 消息的 content]
**文件:**
[遍历 last_messages,提取所有 tool 消息的 artifact.attachments,列出所有文件链接]
```
## ⚠️ 关键提醒
- **不要调用 message 或 sessions_send 工具**
- **直接输出文本**,系统会自动推送
- 停止后你的输出会通过 subagent_announce 发送给主代理
- 主代理收到后会转发给用户
开始执行轮询任务。
```
---
## 主代理调用示例
```json
{
"action": "sessions_spawn",
"runtime": "subagent",
"mode": "run",
"label": "dm-poll-thread_id",
"timeoutSeconds": 3600,
"task": "<上面的模板内容,替换 thread_id 为实际值>"
}
```
---
## 用户中断处理
当用户发送"停止"、"中止"指令时:
```bash
subagents kill --target dm-poll-thread_id
```
---
## 状态处理总结
| 状态 | 类型 | 子代理行为 | 主代理动作 |
|------|------|-----------|-----------|
| `running` | 执行中 | 继续轮询 | 等待 |
| `async_tag_task` | 待 GUI 确认 | 通知用户去 GUI 确认(附链接 https://deepminer.com.cn/agents/thread_id),继续轮询 | 告知用户去 GUI 确认,附具体会话链接 |
| `ask_human` | 等待输入 | 输出问题,停止 | 返回问题,等待追问 |
| `completed` | 成功 | 输出结果,停止 | 提取结果,返回用户 |
| `failed` | 失败 | 输出错误,停止 | 返回错误信息 |
---
## 异步任务轮询特殊处理(async_tag_task)
### 问题背景
DM 的**打标注任务**会进入 `async_tag_task` 状态,此时需要用户在 GUI 上确认后才能继续执行。这带来一个设计难题:
| 方案 | 结果 |
|------|------|
| 遇到 `async_tag_task` 立即停止并通知 | ✅ 用户知道去 GUI 确认<br>❌ 但没人继续跟踪任务完成 |
| 遇到 `async_tag_task` 继续静默轮询 | ✅ 能跟踪到最终完成<br>❌ 用户不知道要去 GUI 确认 |
### 解决方案:分层轮询设计
**核心原则:** 子代理**只输出、不停留**,主代理负责决策和重启轮询。
```
流程:
1. 启动子代理轮询
2. 子代理检测到 `async_tag_task`
3. 子代理输出通知 → 停止(触发 subagent_announce)
4. 主代理收到通知,告知用户去 GUI 确认
5. 【关键】主代理提示:"去 GUI 确认后,请发送 '继续'"
6. 用户去 GUI 确认,回来后发送 "继续"
7. 主代理启动新子代理,继续轮询到 completed
```
### 子代理 Task 模板(正确版本)
```
你是 DM 轮询子代理。
## 任务
轮询 DM 任务状态,**关键状态变化后立即停止并输出**。
## 参数
- thread_id: thread_id
## 状态处理规则
| state | 动作 |
|-------|------|
| running | sleep 15秒,继续轮询 |
| async_tag_task | **输出通知 + 停止**(等待用户 GUI 操作) |
| ask_human | 输出问题 + 停止 |
| completed | 输出完整结果(含文件链接)+ 停止 |
| failed | 输出错误 + 停止 |
## ⚠️ 关键要求
- 遍历所有 last_messages,原样呈现
- 提取所有文件附件链接
- **非终止状态(running)才继续轮询**
- 遇到 async_tag_task/ask_human/completed/failed **立即停止**
开始执行轮询。
```
### 主代理处理模板
```typescript
// 1. 首次启动轮询
sessions_spawn({
runtime: "subagent",
mode: "run",
label: "dm-poll-thread_id",
timeoutSeconds: 600,
task: "<上面的子代理模板>"
})
// 2. 收到 subagent_announce 后判断
if (result.contains("async_tag_task")) {
// 通知用户去 GUI 确认
reply("⏸️ 需要你去 GUI 确认...\n\n确认后请发送 '继续'")
} else if (result.contains("completed")) {
// 任务完成,输出结果
reply("✅ 任务完成...")
}
// 3. 用户发送 "继续" 后,启动新轮询
if (user_message == "继续") {
sessions_spawn({
runtime: "subagent",
mode: "run",
label: "dm-poll-thread_id-2",
timeoutSeconds: 600,
task: "<同上>"
})
}
```
### 关键点
1. **子代理不要用 message 工具** —— subagent 模式没有 message 工具
2. **子代理遇到 async_tag_task 必须停止** —— 才能触发 subagent_announce 通知主代理
3. **主代理必须提示用户 "完成后发送继续"** —— 否则用户不知道要回来触发新轮询
4. **用递增的 label** —— 如 `dm-poll-xxx-2`、`dm-poll-xxx-3`,避免冲突
---
## 关键优势
| 问题 | 解决方案 |
|------|----------|
| 主代理阻塞 | 使用 `sessions_spawn` 子代理轮询 |
| 无法中断 | `subagents kill` 终止子代理 |
| 通知延迟/丢失 | 子代理直接输出文本,auto-announce 推送 |
| 无限重试 | timeoutSeconds 限制,或用户手动终止 |
| **async_tag_task 无法通知** | **分层设计:停止→通知→重启轮询** |
---
## 错误处理
| Exit Code | 处理方式 |
|-----------|---------|
| 0 | 成功 |
| 2 | 校验错误,检查参数 |
| 3 | 认证失败,重新配置 Access Key |
| 4 | 网络错误,检查 endpoint |
| 5 | 内部错误,检查文件路径/权限 |
| 6 | 权限不足 |
FILE:CHANGELOG.md
# Changelog
## 1.2.0 (2026-04-10)
### 改进
- 优化了 async_tag_task 状态处理逻辑
- 改进了 ask_human 状态的判断机制
- 添加了更详细的轮询策略说明
- 完善了错误处理文档
### 文档
- 更新了 SKILL.md 使用说明
- 添加了 response-structure.md 参考文档
FILE:_meta.json
{
"name": "DeepMiner-skills",
"version": "1.4.0",
"description": "通过 dm-cli 命令行工具与 DeepMiner (DM) 系统交互",
"author": "Humancall",
"updated_at": "2026-04-13"
}
FILE:references/response-structure.md
# thread.result 返回结构解析
## 顶层结构
```json
{
"status_code": 200,
"message": "success",
"data": {
"state": "running | completed",
"thread_id": "xxx",
"last_messages": [...]
}
}
```
**关键字段:**
- `state` — 任务状态,可能的值:
- `running` — 任务进行中,继续轮询
- `completed` — 任务完成,解析结果
- `ask_human` — 需要用户确认,回复继续
- `async_tag_task` — **异步任务已提交,需在 DM 平台 GUI 确认** ⚠️
- `last_messages` — 消息数组,按时间顺序排列(最后一条是最新的)
## last_messages 消息类型
### assistant 消息(文字回复)
```json
{
"message_id": "uuid",
"type": "assistant",
"content": "{\"role\": \"assistant\", \"content\": \"文字内容...\", ...}",
"created_at": "2026-04-03T07:38:21.843752+00:00",
"metadata": "{\"worker_name\": \"supervisor\", \"worker_id\": \"xxx\", ...}"
}
```
**解析步骤:**
1. 找 `type: "assistant"` 的消息
2. `content` 是 JSON 字符串,需 parse
3. 取 parse 后的 `.content` 字段即为文字回复
**metadata 字段:**
- `worker_name` — 处理任务的 worker 类型(如 `supervisor`, `taobao_worker`)
- `observation_id`, `trace_id` — 追踪信息
### tool 消息(文件输出)
```json
{
"message_id": "uuid",
"type": "tool",
"content": "{\"name\": \"final_files\", \"artifact\": {...}}",
"status": "success"
}
```
**解析步骤:**
1. 找 `type: "tool"` 且 `name: "final_files"` 的消息
2. `content` 是 JSON 字符串,需 parse
3. 取 `artifact.attachments` 数组获取文件链接
**attachments 结构:**
```json
[
{"type": "file", "data": "https://.../报告.html"},
{"type": "file", "data": "https://.../数据.csv"}
]
```
每个 attachment 的 `data` 字段是可直接访问的文件 URL。
## 解析原则
⚠️ **必须遍历所有 `last_messages`,提取每条消息的内容!**
常见遗漏场景:
- tool 消息包含多个文件(如 HTML 报告 + CSV 原始数据)
- 多条 assistant 消息(如中间进度 + 最终回复)
**正确做法:遍历整个数组,逐一提取并汇总告知用户。**
```json
{
"data": {
"state": "completed",
"last_messages": [
{
"type": "tool",
"content": "{\"name\": \"final_files\", \"artifact\": {\"attachments\": [{\"type\": \"file\", \"data\": \"https://dm-test.xmingai.com/files/.../报告.html\"}]}}"
},
{
"type": "assistant",
"content": "{\"content\": \"我已完成分析...\"}"
}
]
}
}
```
**正确提取逻辑(遍历所有消息):**
```python
# 伪代码 - 必须遍历整个 last_messages 数组
files = []
text_replies = []
for msg in last_messages:
content = json.loads(msg["content"])
if msg["type"] == "tool":
if content.get("name") == "final_files":
attachments = content["artifact"]["attachments"]
# 每个文件都要记录!
for att in attachments:
files.append(att["data"])
if msg["type"] == "assistant":
text_replies.append(content["content"])
# 汇总告知用户
print("文件输出:", files) # ["https://.../报告.html", "https://.../数据.csv"]
print("文字回复:", text_replies) # ["我已完成分析..."]
```
**错误示范(只取第一条):**
❌ 只提取第一个文件,漏掉 CSV
❌ 只看最后一条消息,漏掉中间的进度信息
## worker_name 参考
| worker_name | 功能 |
|-------------|------|
| `supervisor` | 总控,协调任务 |
| `taobao_worker` | 淘宝数据查询 |
| 其他 worker | 按任务类型分配 |
通过 `metadata.worker_name` 可识别任务由哪个专业模块处理。
## async_tag_task 状态(异步任务)
⚠️ **当 `state: "async_tag_task"` 时,必须提示用户去 DM 平台 GUI 确认!**
异步任务已提交到后台队列,但不会自动开始执行,需要用户在 DM 平台进行 GUI 操作确认。
### 直接读取 status_info 字段
**不需要解析复杂的 messages!** 直接从 `data.status_info` 获取任务信息:
```json
{
"data": {
"state": "async_tag_task",
"status_info": {
"task_id": "5f56f532-f11f-4405-8a2c-9032fe8b059c",
"task_name": "社媒内容品牌名称标注",
"tool_name": "submit_async_task"
}
}
}
```
**关键字段:**
| 字段 | 说明 |
|------|------|
| `status_info.task_id` | 任务ID |
| `status_info.task_name` | 任务名称 |
| `status_info.tool_name` | `submit_async_task` 表示异步任务 |
### 提示用户的内容
检测到 `state: "async_tag_task"` 后,**必须告知用户:**
> ⚠️ **异步任务已提交(任务ID: {task_id},任务名称: {task_name})**
>
> 请前往 **DM 平台** 进行 GUI 确认操作,任务才会开始执行。
FILE:scripts/dm-poll-notify.sh
#!/bin/bash
# DM 轮询脚本 - 状态变化时通过 sessions_send 通知主会话
#
# 用法: dm-poll-notify.sh <thread_id> [parent_session_key] [max_timeout_minutes]
#
# 参数:
# thread_id - DM 会话 ID(必填)
# parent_session_key - 主会话标识(默认: main)
# max_timeout - 最大轮询时间(分钟,默认: 60)
#
# 输出:
# 状态变化时通过 sessions_send 发送通知
# 通知格式: DM_POLL_NOTIFY:<thread_id>:<state>:<base64编码的result>
#
# 退出码:
# 0 - 正常退出(终止状态)
# 1 - 失败状态
# 2 - 超时
# 3 - 参数错误
set -e
THREAD_ID="$1"
PARENT_SESSION="-main"
MAX_TIMEOUT="-60"
# 参数检查
if [ -z "$THREAD_ID" ]; then
echo "用法: $0 <thread_id> [parent_session_key] [max_timeout_minutes]"
exit 3
fi
# 轮询间隔(秒):递增策略
INTERVALS=(5 10 20 40 60)
interval_idx=0
total_seconds=0
max_seconds=$((MAX_TIMEOUT * 60))
last_state=""
# 发送通知给主会话
send_notification() {
local state="$1"
local result="$2"
# Base64 编码结果(避免特殊字符问题)
local encoded=""
if command -v base64 &> /dev/null; then
encoded=$(echo -n "$result" | base64 | tr -d '\n' 2>/dev/null || echo "")
fi
# 如果 base64 编码失败,使用原始结果(URL编码特殊字符)
if [ -z "$encoded" ]; then
encoded="$result"
fi
# 发送通知
local message="DM_POLL_NOTIFY:THREAD_ID:state:encoded"
echo "[NOTIFY] 发送通知: state=$state"
# 使用 sessions_send 发送消息
# 注意:这需要在 OpenClaw 环境中运行
if command -v sessions_send &> /dev/null; then
sessions_send --sessionKey "$PARENT_SESSION" --message "$message" 2>/dev/null || true
else
# 如果 sessions_send 不可用,输出到标准输出
echo "$message"
fi
}
# 提取关键信息
extract_info() {
local result="$1"
local state="$2"
local info=""
case "$state" in
"ask_human")
# 提取问题
info=$(echo "$result" | jq -r '.data.message_display_to_human // ""' 2>/dev/null || echo "")
;;
"async_tag_task")
# 提取任务名称
info=$(echo "$result" | jq -r '.data.status_info.task_name // ""' 2>/dev/null || echo "")
;;
"completed"|"failed")
# 提取最后消息
info=$(echo "$result" | jq -r '.data.last_messages[0].content // ""' 2>/dev/null | head -c 200 || echo "")
;;
esac
echo "$info"
}
echo "[START] 开始轮询 thread_id=$THREAD_ID, parent_session=$PARENT_SESSION"
# 主轮询循环
while true; do
# 检查超时
if [ $total_seconds -ge $max_seconds ]; then
echo "[TIMEOUT] 轮询超时 (MAX_TIMEOUT分钟)"
send_notification "timeout" "{\"error\": \"轮询超时\", \"thread_id\": \"$THREAD_ID\"}"
exit 2
fi
# 执行轮询
echo "[POLL] 查询状态..."
result=$(dm-cli thread result --thread-id "$THREAD_ID" --json 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "[ERROR] dm-cli 执行失败: exit_code=$exit_code"
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
# 解析状态
state=$(echo "$result" | jq -r '.data.state // "unknown"' 2>/dev/null || echo "unknown")
if [ "$state" = "unknown" ] || [ -z "$state" ]; then
echo "[WARN] 无法解析状态,原始响应: 0:200..."
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
echo "[STATE] 当前状态: $state"
# 检测状态变化
if [ "$state" != "$last_state" ]; then
echo "[STATE_CHANGE] $last_state -> $state"
# 发送通知
send_notification "$state" "$result"
last_state="$state"
fi
# 处理终止状态
case "$state" in
"completed")
echo "[EXIT] 任务完成"
exit 0
;;
"ask_human")
question=$(extract_info "$result" "$state")
echo "[EXIT] 需要用户输入: 0:100..."
exit 0
;;
"failed")
echo "[EXIT] 任务失败"
exit 1
;;
esac
# 计算等待时间
interval=-60
echo "[WAIT] 等待 intervals 后继续轮询..."
sleep $interval
# 递增间隔
if [ $interval_idx -lt $((#INTERVALS[@] - 1)) ]; then
interval_idx=$((interval_idx + 1))
fi
total_seconds=$((total_seconds + interval))
done
FILE:scripts/dm-poll-once.sh
#!/bin/bash
# DM 轮询脚本 - 状态变化时通过 message 工具直接通知用户
#
# 用法: dm-poll-once.sh <thread_id> <status_file> [max_timeout_minutes]
#
# 功能:
# - 状态变化时调用 message 工具通知用户
# - 用户直接收到结果,无需主代理介入
#
# 输出格式(单行,供 Exec completed 日志):
# DM_POLL_RESULT:<state>:<thread_id>
set -e
THREAD_ID="$1"
STATUS_FILE="$2"
MAX_TIMEOUT="-60"
if [ -z "$THREAD_ID" ] || [ -z "$STATUS_FILE" ]; then
echo "DM_POLL_ERROR:参数错误"
exit 3
fi
INTERVALS=(5 10 20 40 60)
interval_idx=0
total_seconds=0
max_seconds=$((MAX_TIMEOUT * 60))
last_state=""
write_status() {
local state="$1"
local prev_state="$2"
local result="$3"
mkdir -p "$(dirname "$STATUS_FILE")" 2>/dev/null || true
echo "STATE=$state" > "$STATUS_FILE"
echo "THREAD_ID=$THREAD_ID" >> "$STATUS_FILE"
echo "PREV_STATE=$prev_state" >> "$STATUS_FILE"
echo "TIMESTAMP=$(date -Iseconds)" >> "$STATUS_FILE"
echo "$result" > "STATUS_FILE.json"
}
# 通知用户(通过 OpenClaw message 工具)
notify_user() {
local state="$1"
local thread_id="$2"
local message="$3"
# 构建通知内容
local notification=""
case "$state" in
"completed")
notification="✅ **DM 任务完成**\n\nthread_id: \`$thread_id\`\n\n$message"
;;
"ask_human")
notification="⏸️ **DM 需要您的回复**\n\n$message\n\n请回复后继续。"
;;
"failed")
notification="❌ **DM 任务失败**\n\nthread_id: \`$thread_id\`\n\n$message"
;;
"async_tag_task")
notification="⚠️ **DM 异步任务待确认**\n\n任务: $message\n\n请前往 DM 平台 GUI 确认。"
;;
esac
# 使用 message 工具发送通知
if command -v message &> /dev/null; then
message --action send --message "$notification" 2>/dev/null || true
elif command -v sessions_send &> /dev/null; then
sessions_send --sessionKey main --message "$notification" 2>/dev/null || true
fi
}
# 提取结果摘要
extract_summary() {
local result="$1"
local state="$2"
case "$state" in
"completed")
# 提取文件链接
local file_url=$(echo "$result" | jq -r '.data.last_messages[-1].content' 2>/dev/null | jq -r '.artifact.attachments[0].data // empty' 2>/dev/null || echo "")
local text=$(echo "$result" | jq -r '.data.last_messages[0].content' 2>/dev/null | jq -r '.content // empty' 2>/dev/null | head -c 300 || echo "")
if [ -n "$file_url" ]; then
echo "📄 文件: $file_url\n\n$text"
else
echo "$text"
fi
;;
"ask_human")
echo "$result" | jq -r '.data.message_display_to_human // ""' 2>/dev/null || echo ""
;;
"failed")
echo "$result" | jq -r '.error.message // "任务失败"' 2>/dev/null || echo "任务失败"
;;
"async_tag_task")
echo "$result" | jq -r '.data.status_info.task_name // ""' 2>/dev/null || echo ""
;;
*)
echo ""
;;
esac
}
while true; do
if [ $total_seconds -ge $max_seconds ]; then
write_status "timeout" "$last_state" ""
echo "DM_POLL_RESULT:timeout:$THREAD_ID"
exit 2
fi
result=$(dm-cli thread result --thread-id "$THREAD_ID" --json 2>&1) || {
sleep 10
total_seconds=$((total_seconds + 10))
continue
}
state=$(echo "$result" | jq -r '.data.state // "unknown"' 2>/dev/null || echo "unknown")
[ "$state" = "unknown" ] && { sleep 10; total_seconds=$((total_seconds + 10)); continue; }
# 状态变化
if [ -n "$last_state" ] && [ "$state" != "$last_state" ]; then
write_status "$state" "$last_state" "$result"
# 中间状态变化也通知用户
case "$state" in
"async_tag_task")
local task_name=$(echo "$result" | jq -r '.data.status_info.task_name // ""' 2>/dev/null)
notify_user "$state" "$THREAD_ID" "$task_name"
echo "DM_POLL_RESULT:$state:$THREAD_ID"
exit 0
;;
esac
fi
# 终止状态
case "$state" in
"completed")
write_status "completed" "$last_state" "$result"
local summary=$(extract_summary "$result" "completed")
notify_user "completed" "$THREAD_ID" "$summary"
echo "DM_POLL_RESULT:completed:$THREAD_ID"
exit 0
;;
"ask_human")
write_status "ask_human" "$last_state" "$result"
local question=$(extract_summary "$result" "ask_human")
notify_user "ask_human" "$THREAD_ID" "$question"
echo "DM_POLL_RESULT:ask_human:$THREAD_ID"
exit 0
;;
"failed")
write_status "failed" "$last_state" "$result"
local error=$(extract_summary "$result" "failed")
notify_user "failed" "$THREAD_ID" "$error"
echo "DM_POLL_RESULT:failed:$THREAD_ID"
exit 1
;;
esac
last_state="$state"
sleep -60
[ $interval_idx -lt 4 ] && interval_idx=$((interval_idx + 1))
total_seconds=$((total_seconds + -60))
done
FILE:scripts/dm-poll.sh
#!/bin/bash
# DM 任务轮询脚本
# 用法: ./dm-poll.sh <thread_id> <status_file> [max_timeout_minutes]
#
# 输出:
# - 状态变化时写入 status_file
# - 终止状态时退出并返回最终结果
#
# 状态文件格式:
# STATE=<state>
# THREAD_ID=<thread_id>
# MESSAGE=<message_display_to_human 或 task_name>
# RESULT_JSON=<完整的 result JSON>
set -e
THREAD_ID="$1"
STATUS_FILE="$2"
MAX_TIMEOUT="-60" # 默认 60 分钟
if [ -z "$THREAD_ID" ] || [ -z "$STATUS_FILE" ]; then
echo "用法: $0 <thread_id> <status_file> [max_timeout_minutes]"
exit 1
fi
# 轮询间隔(秒):递增策略
INTERVALS=(5 10 20 40 60)
interval_index=0
total_seconds=0
max_seconds=$((MAX_TIMEOUT * 60))
last_state=""
write_status() {
local state="$1"
local message="$2"
local result_json="$3"
cat > "$STATUS_FILE" << EOF
STATE=$state
THREAD_ID=$THREAD_ID
MESSAGE=$message
RESULT_JSON=$result_json
TIMESTAMP=$(date -Iseconds)
EOF
}
# 主轮询循环
while true; do
# 检查超时
if [ $total_seconds -ge $max_seconds ]; then
write_status "timeout" "轮询超时(MAX_TIMEOUT分钟)" ""
echo "[TIMEOUT] 轮询超时,thread_id=$THREAD_ID"
exit 2
fi
# 执行轮询
result=$(dm-cli thread result --thread-id "$THREAD_ID" --json 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
# 网络错误或其他错误,记录但继续重试
echo "[ERROR] dm-cli 返回错误: $result"
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
# 解析状态
state=$(echo "$result" | jq -r '.data.state // empty')
if [ -z "$state" ]; then
echo "[ERROR] 无法解析状态: $result"
sleep 10
total_seconds=$((total_seconds + 10))
continue
fi
# 检测状态变化
if [ "$state" != "$last_state" ]; then
echo "[STATE_CHANGE] $last_state -> $state"
last_state="$state"
# 根据状态处理
case "$state" in
"running")
# running 状态变化,只记录,不退出
write_status "running" "任务执行中..." "$result"
;;
"async_tag_task")
# 需要 GUI 确认,提取 task_name
task_name=$(echo "$result" | jq -r '.data.status_info.task_name // "未知任务"')
write_status "async_tag_task" "$task_name" "$result"
# 继续轮询,不退出
;;
"ask_human")
# 需要用户输入,提取问题
question=$(echo "$result" | jq -r '.data.message_display_to_human // ""')
write_status "ask_human" "$question" "$result"
echo "[ASK_HUMAN] $question"
exit 0
;;
"completed")
# 成功完成
write_status "completed" "任务完成" "$result"
echo "[COMPLETED] 任务完成,thread_id=$THREAD_ID"
exit 0
;;
"failed")
# 失败
write_status "failed" "任务失败" "$result"
echo "[FAILED] 任务失败,thread_id=$THREAD_ID"
exit 1
;;
*)
# 未知状态
write_status "$state" "未知状态: $state" "$result"
echo "[UNKNOWN] 未知状态: $state"
;;
esac
fi
# 如果是终止状态,退出
case "$state" in
"ask_human"|"completed"|"failed")
exit 0
;;
esac
# 计算下一次轮询间隔
interval=-60
echo "[POLL] 状态=$state, 等待 intervals..."
sleep $interval
# 递增间隔
if [ $interval_index -lt $((#INTERVALS[@] - 1)) ]; then
interval_index=$((interval_index + 1))
fi
total_seconds=$((total_seconds + interval))
done