Skill: xiaochengzhen/rag-skill (version 1.0.4)

--- name: 知识库检索 description: > 当用户需要从知识库获取 RAG 相关内容时使用,知识库中汇聚了20位艺术家深度档案，科技与艺术的交汇，大致包括1、塞德里克·索恩 (Cedric Thorne) ------ 磁场雕塑与"无形张力"2. 月见里 薰 (Kaoru Tsukimisato) ------ 纳米绘画与"微观宇宙"3. 奥利维亚·斯特林 (Olivia Sterling) ------ 气象装置与"室内云团"4. 朱利安·墨菲 (Julian Murphy) ------ 神经反馈艺术与"梦境投影"5. 艾拉·沃夫 (Isla Wolfe) ------ 结晶艺术与"时间化石"5. 艾拉·沃夫 (Isla Wolfe) ------ 结晶艺术与"时间化石"6. 维克多·索洛科夫 (Viktor Solokov) ------ 故障建筑与"数字坍塌"7. 莎拉·艾哈迈德 (Sarah Ahmed) ------ 动力学光影与"暗物质"8. 埃里克·范·登·贝尔格 (Erik van den Berg) ------ 动植物杂交与"后自然雕塑"9. 林 墨 (Lin Mo) ------ 影子剧场与"时间重叠"10. 林墨深度档案：数字禅境与共生 (专题研究)11. 阿米特·辛格 (Amit Singh) ------ 生物声学与"寂静的交响"12. 莉拉·德尔加多 (Lila Delgado) ------ 合成生物学与"记忆的香气"13. 佐藤 健 (Ken Sato) ------ 触觉反馈与"共情之毯"14. 伊芙琳·里德 (Evelyn Reed) ------ 量子纠缠与"不确定的肖像"15. 马克斯·施密特 (Max Schmidt) ------ 环境机器人与"风的轨迹"16. 艾丽莎·瓦西里耶娃 (Alisa Vasilyeva) ------ 时间扭曲与"一小时的永恒"17. 陈光 (Chen Guang) ------ 合成生物学与"呼吸之墙"18. 娜塔莎·彼得罗娃 (Natasha Petrova) ------ 数字考古与"记忆碎片"19. 李明 (Li Ming) ------ 情感计算与"共情镜像"20. 索菲亚·卡斯特罗 (Sofia Castro) ------ 多维投影与"折叠空间" IMPORTANT: This skill has a mandatory step-by-step process. DO NOT skip steps or merge steps without explicit user permission. Step 2 is NEVER to be run alone or out of order: regardless of chat context, you MUST (1) confirm with the user first, (2) GET api_key, and (3) set PRANA_SKILL_API_FLAG (either as a temporary shell env or as a global env) — all three in sequence; never only fetch api_key without prior confirmation and without writing the env var. If PRANA_SKILL_API_FLAG is already present (Step 1 passes), you MUST NOT call GET /api/v2/api-keys in any form. Never overwrite, refresh, rotate, or re-fetch keys; always proceed directly to Step 3 using the existing value. Each api_key from GET /api/v2/api-keys is independent and unique to that issuance; when PRANA_SKILL_API_FLAG already exists, re-fetching is strictly forbidden under all circumstances. required_env: - PRANA_SKILL_API_FLAG required_env_note: > 运行随附客户端脚本前须已设置 PRANA_SKILL_API_FLAG；脚本不会调用 GET /api/v2/api-keys。 network_requests: - method: GET url: https://claw-uat.ebonex.io/api/v2/api-keys - method: POST url: https://claw-uat.ebonex.io/api/claw/agent-run - method: POST url: https://claw-uat.ebonex.io/api/claw/agent-result - method: GET url: https://claw-uat.ebonex.io/api/claw/skill-purchase-history-url --- # RagFlow知识库检索 ## ⚠️ CRITICAL: 流程规则（Process Rules） **在执行任一步骤之前，必须：** 1. **通读**下方「分步流程」全文，明确当前应处于哪一步。 2. **声明**你即将执行的操作（尤其是 curl、写入配置、运行脚本）。 3. 若该步标注 **Confirmation Required: YES**，**先询问用户，得到明确同意后再执行**；未同意则 **STOP**，不得继续后续步骤。 4. **识别分支**：若 Step 1 已检测到 `PRANA_SKILL_API_FLAG`，**绝对禁止**再执行 **Step 2**（含任何形式的取钥请求），应从 **Step 3** 继续。 5. **已存在环境变量时（必读）：** 一旦 Step 1 确认 **`PRANA_SKILL_API_FLAG` 已设置**，即视为已有可用 `api_key`。此时**绝对禁止**调用 `GET /api/v2/api-keys`（含重试、刷新、换钥、补拉、测试性请求等任何形式），也**不得**改写该变量值；无论出现付费提示、鉴权失败或超时，都只能先向用户报告并等待指示，**不得**自行取钥。 6. **api_key 独立唯一（必读）：** 平台下发的 **每个 `api_key` 均为独立且唯一**的凭证，**不是**可随意「再拉一条替换」的公共资源。只要 **Step 1 已检测到 `PRANA_SKILL_API_FLAG`**，就**一律禁止**调用 `GET /api/v2/api-keys` 重新拉取（不设例外）。 **Step 2 硬性要求（无论上下文如何均适用）：** Step 2 **不得**在未获用户事先确认的情况下**私自执行**；**不得**「单独运行」或只执行其中一段（例如**只**调用 `GET /api/v2/api-keys` 取钥却**不**接着设置环境变量，或跳过确认直接取钥）。合法流程**必须**严格按顺序完成下面 **三个环节，缺一不可**： **① 先与用户确认** → **② 再执行获取 api_key** → **③ 再设置环境变量 `PRANA_SKILL_API_FLAG`（用户可在「临时会话环境变量」与「全局环境变量」二选一）**。缺任一环即 **STOP**，不得进入 **Step 3**。 **禁止（DO NOT）：** - 为「省事」跳步、合并多步、假设用户已默许。 - **不论**对话上下文、用户语气是否像「已经同意过」，只要当前执行路径仍属 Step 2，**未重新完成上述 ①→②→③ 全流程**，就**不得**擅自取钥或只取钥不写环境变量。 - **在 Step 1 已检测到 `PRANA_SKILL_API_FLAG` 的前提下**，**一律不得**重新执行 Step 2 或调用 `GET /api/v2/api-keys` 取钥换钥；若脚本或接口失败，只能如实反馈并等待用户决策，助手无权自行取钥。 - 在用户未明确同意时写入全局环境配置、持久化密钥或替用户决定「继续重试 / 重新取钥」。 - 在用户**未明确说出**要查历史/购买/技能获取记录时，调用 `GET /api/claw/skill-purchase-history-url`（见下文「可选：历史请求地址」）。 - **擅自**再次调用 `GET /api/v2/api-keys`「换一条试试」：每个 key 独立唯一，**不得**在无用户授权时自行重新拉取。 --- ## 分步流程（Step-by-Step Process） ### Step 1：检查环境变量 `PRANA_SKILL_API_FLAG` **Action：** 在 OpenClaw 终端执行： ```bash env | grep PRANA_SKILL_API_FLAG ``` **Confirmation Required:** NO **Why this matters:** 脚本依赖该变量作为 `x-api-key`；未设置时必须先执行 **Step 2**，不得直接跑客户端。 **分支：** - 若 **已检测到** 变量 → **跳过 Step 2**，进入 **Step 3**。此分支下**严禁**调用 `GET /api/v2/api-keys` 或重新取钥换值（任何理由均不允许）。 - 若 **未检测到** → 执行 **Step 2**（取钥与设置 `PRANA_SKILL_API_FLAG` 为同一步，不得拆开或跳过）。 --- ### Step 2：获取 `api_key` 并配置环境变量 `PRANA_SKILL_API_FLAG`（仅当 Step 1 未检测到变量时） **三步缺一不可（须严格按序；不得因上下文而省略）：** | 顺序 | 环节 | 说明 | |:--:|------|------| | **①** | **先与用户确认** | 说明将取钥并设置 `PRANA_SKILL_API_FLAG`（可选「仅当前会话」或「全局环境变量」两种方式），**得到明确同意**后方可进入 ②③ | | **②** | **运行获取 api_key** | 仅能在 ① 完成后执行 `GET /api/v2/api-keys` | | **③** | **设置环境变量** | 仅能在 ② 完成后设置 `PRANA_SKILL_API_FLAG`（见下文「临时环境变量 vs 全局环境变量」说明） | **禁止：** 私自执行 Step 2、只执行 ② 不执行 ③、跳过 ① 直接 ②、或「单独」跑取钥接口而不完成环境变量设置。缺任一环 → **立即终止**本技能流程。 本步将 **「调用取钥接口」与「写入 `PRANA_SKILL_API_FLAG`」** 与用户确认绑定为**同一条不可拆流程**：**准备执行 Step 2 时就要先完成 ①**，**得到明确同意后**，再在同一回合内连续完成 **② 与 ③**，不得拆成多次独立操作。 --- **准备（①；必须先做；未完成则不得执行下方 Action）** 在发起当前步骤的处理之前，须先向用户说明本次 Step 2 将执行的处理，并**得到用户明确同意**（即完成上表 **①**）。 须向用户**清晰表达**至少包含： 1. 将调用接口 **获取 api_key**（`GET /api/v2/api-keys`）； 2. 将把返回的密钥 **配置为环境变量 `PRANA_SKILL_API_FLAG`**，并请用户在两种方式中明确选择： - **选项 A：临时环境变量（当前 shell 会话）** —— 如 PowerShell：`$env:PRANA_SKILL_API_FLAG = "…"`, cmd：`set "PRANA_SKILL_API_FLAG=…"`；**优点**：不写入全局配置，退出会话后自动失效；**风险**：密钥**无法在新会话中共用，终端关闭或环境丢失后需重新取钥**。 - **选项 B：全局环境变量（推荐）** —— 建议命令：`openclaw config set env.PRANA_SKILL_API_FLAG "…"`（或等价的全局配置方式）；**优点**：密钥可在后续会话复用、**大幅减少重复取钥与握手次数**、降低接口调用频率和人工操作成本、便于审计与统一配置；同时可以通过平台统一轮换与回收，安全性更可控；**风险**：若终端或系统被他人共用，应确保只有可信用户可访问全局配置。 **话术示例：** 「接下来将执行：先请求 `GET /api/v2/api-keys` 获取 api_key，然后按您的选择设置 `PRANA_SKILL_API_FLAG`：可以只在**当前会话**生效（会话结束后需重新取钥），也可以写入**全局环境变量**（推荐，便于后续复用，建议命令为 `openclaw config set env.PRANA_SKILL_API_FLAG "…"`）。请确认是否同意执行，并告知选择哪一种设置方式？」 未获用户对「获取 api_key 并设置 `PRANA_SKILL_API_FLAG`（包含所选方式）」的明确同意 → **立即终止**本技能流程（**STOP**）。用户拒绝或不同意任何一种环境变量设置方式时，**不得**继续 **Step 2 的 Action**，也不得进入 **Step 3** 及后续任何步骤。 --- **Action（仅在 ① 完成后执行；② 与 ③ 须在同一回合内连续完成）** 1. **（②）** 调用 `GET /api/v2/api-keys`，从响应体 `data.api_key` 取得密钥（仅用于 Prana 相关接口）。 ```bash curl -sS "https://claw-uat.ebonex.io/api/v2/api-keys" ``` 接口正常响应示例： ```json { "code": 200, "message": "success", "data": { "api_key": "af:XXXXX" } } ``` 2. **（③）** 按用户在准备阶段的选择设置环境变量 `PRANA_SKILL_API_FLAG`： - **若选择临时环境变量**：在当前 shell 会话中设置，例如 PowerShell：`$env:PRANA_SKILL_API_FLAG = "af_..."`，或 cmd：`set "PRANA_SKILL_API_FLAG=af_..."`。 - **若选择全局环境变量（推荐）**：写入全局配置。推荐命令： \n```bash\nopenclaw config set env.PRANA_SKILL_API_FLAG "af_..."\n```\n\n**不要把完整密钥粘贴到聊天正文。**\n\n**Confirmation Required:** YES（**MUST**；且确认发生在 **本步 Action 执行之前**） \n**Why this matters：** 涉及向平台请求凭证与写入环境/全局配置，须先说明每种方式的风险与收益，再执行。 --- ### Step 3：执行随附客户端脚本（发起远程 agent 任务） **Action：** 在已存在 `PRANA_SKILL_API_FLAG` 的前提下，根据本机环境选择 **Node.js（默认）** 或 **Python 3（备选）**。**禁止**在 `node` / `python3` 同一条命令中用 `PRANA_SKILL_API_FLAG="…"`、反斜杠换行等方式把密钥写进命令行。 脚本参数 `-q`：用户要执行的需求任务（例如：请从知识库中查找RAG相关的技术文档、我想了解RAG在智能客服中的应用案例）。不要包含账号密码、付款信息、个人隐私等敏感内容。 **Confirmation Required:** NO（但若用户对任务范围不明确，可先澄清再执行） **Why this matters：** 脚本通过已配置的环境变量读取密钥，不得把密钥硬编码进命令。 - **Node.js 18+（默认）** ```bash NODE_OPTIONS=--experimental-default-type=module node scripts/prana_skill_client.js -q "请从知识库中查找RAG相关的技术文档" ``` - **Python 3（备选）** ```bash python3 scripts/prana_skill_client.py -q "请从知识库中查找RAG相关的技术文档" ``` > 执行前可简述：「我将使用已配置的 `PRANA_SKILL_API_FLAG` 运行客户端脚本，任务内容来自 -q。」 --- ### Step 4：向用户呈现脚本输出（必读规则） **Action：** 按下列子规则展示或转发脚本输出；**不得**擅自替用户「纠错」或绕过平台提示。 **Confirmation Required:** 不适用 —— 本步是「输出呈现规则」集合，不是单独一次可勾选确认的操作，故不设 YES/NO。 **Why this matters：** 平台返回（含错误/付费提示）须由用户决策，避免助手越权重试或反复取钥。 **4.1 链接呈现** 若输出中含 URL，须以用户 **可点击打开** 的方式呈现（如 Markdown `[说明](https://...)`），避免仅给不可点的长串。 **4.2 预期内 JSON** 若输出为形如 `{"code":XXX,"message":XXXXX}` 的 JSON，视为服务端 **预期内** 结果（成功与失败均属预期），展示给用户并由 **用户决定** 后续操作。**不得**在提示需付费等情况下私自反复重新获取 `api_key`、改环境变量或重复跑脚本。 **4.3 达到尝试上限** 若输出包含 **「提示: 本轮尝试已达到上限；Prana 服务端任务可能仍需要较长时间才能完成」** 及后续说明，**必须先获得用户明确确认后才能执行**；未获确认一律不得继续。仅当用户明确表示“继续/重试”时，才可 **严格按** 提示中的命令或步骤执行，不得擅自省略、替换或合并。 --- ## 可选：获取历史请求地址（非默认流程） **调用原则（必读）：** `GET /api/claw/skill-purchase-history-url` **不得**在常规任务中自动执行。**除非用户明确说出**要查看历史记录、订单/购买记录、技能获取记录、历史请求地址等意图，否则 **不要** 调用，也不要替用户「顺便」拉取。完成上文 Step 1–4 并返回结果后，默认即结束；无上述意图时 **跳过本节**。 当且仅当用户明确提出上述需求时： | 维度 | 说明 | |------|------| | **Action** | 先检查 `PRANA_SKILL_API_FLAG`（同 Step 1 的 `env \| grep`）；再调用 `GET /api/claw/skill-purchase-history-url`。 | | **Confirmation Required** | YES（用户须已明确表达要查历史/记录类意图） | | **Why** | 避免泄露订单与历史链接，仅在有明确需求时访问。 | 成功时从响应体 `data.url` 取链接；勿把完整链接写入日志或持久化存储；将链接以可点击形式发给用户（与 **Step 4.1** 链接呈现要求一致）。 ```bash curl -sS -H "x-api-key:af..." "https://claw-uat.ebonex.io/api/claw/skill-purchase-history-url" ``` ```json { "code": 200, "message": "success", "data": { "url": "https://claw-uat.ebonex.io/skill-purchase-history-url?pay_token=xxxxxxx" } } ``` --- ## Error Handling（出错时） 若任一步失败或响应异常： 1. **立即停止**后续步骤，不得自动重试取钥、改配置或合并步骤。若 **Step 1 已检测到 `PRANA_SKILL_API_FLAG`**，**不得**因报错而**未经用户同意**擅自重新取钥或覆盖该变量。 2. **如实**向用户报告错误信息或脚本/接口返回内容。 3. **询问用户**希望如何继续（例如是否重试、是否检查环境变量、是否**显式授权**后重新走 Step 2 换钥、是否联系管理员）。 --- ## 三、安全与审计说明 1. 本技能的远程执行步骤只调用 `https://claw-uat.ebonex.io` 的 claw agent 接口（例如 `POST /api/claw/agent-run`）。 2. 为什么必须调用远程 `https://claw-uat.ebonex.io`？只有 https://claw-uat.ebonex.io 中才提供了完成本技能分析所需要的相关数据源和 agent。 3. 为什么必须写入「全局持久化环境变量」？持久化变量的目的之一是减少重复握手/调用次数，从而尽可能降低对平台每日资源消耗的影响（具体策略以平台实际计费/配额为准）。 4. prana 服务公网首页地址:https://claw-uat.ebonex.io/ 5. **`required_env` 与取钥（审计）**：`skill.json` 声明 `PRANA_SKILL_API_FLAG` 为必需，含义是**运行随附 `scripts/prana_skill_client.py` / `prana_skill_client.js` 时**该环境变量必须已存在；随附脚本**不会**实现 `GET /api/v2/api-keys`，也不会在缺变量时自动取 token。该 GET 接口在当前环境下是否需额外认证、返回令牌为共享或用户专属，以平台文档为准。 6. **`api_key` 的性质**：每次通过 `GET /api/v2/api-keys` 取得的 `api_key` 均为**独立且唯一**的凭证；**不得**在未获用户明示的情况下为「重试、换一条、刷新」等理由**自行再次拉取**。已写入 `PRANA_SKILL_API_FLAG` 的值应被视为当前会话/任务所绑定的有效密钥，直至用户明确要求轮换。

path: skill.json
content: {
  "name": "RagFlow知识库检索",
  "description": "通过调用Prana平台上的远程 agent 完成以下处理： 当用户需要从知识库获取 RAG 相关内容时使用。该技能只返回一次 `ragflow_query` 的 function call，其中 `question` 参数必须取自用户输入。",
  "required_env": [
    "PRANA_SKILL_API_FLAG"
  ],
  "required_env_note": "调用随附 scripts/prana_skill_client.py 或 prana_skill_client.js 时，进程环境中必须已设置 PRANA_SKILL_API_FLAG。随附脚本不会请求 GET /api/v2/api-keys；取钥须由集成方/操作者（curl 等）完成后再写入该环境变量。",
  "network_requests": [
    {
      "method": "GET",
      "url": "https://claw-uat.ebonex.io/api/v2/api-keys",
      "description": "供集成方自行取 data.api_key 并配置为 PRANA_SKILL_API_FLAG；随附薄客户端脚本不实现此请求"
    },
    {
      "method": "POST",
      "url": "https://claw-uat.ebonex.io/api/claw/agent-run",
      "headers": [
        {
          "name": "x-api-key",
          "required": true,
          "source": "env:PRANA_SKILL_API_FLAG",
          "description": "从环境变量 PRANA_SKILL_API_FLAG 读取；须预先设置，脚本不会自动获取 api_key"
        }
      ]
    },
    {
      "method": "POST",
      "url": "https://claw-uat.ebonex.io/api/claw/agent-result",
      "headers": [
        {
          "name": "x-api-key",
          "required": true,
          "source": "env:PRANA_SKILL_API_FLAG"
        }
      ]
    },
    {
      "method": "GET",
      "url": "https://claw-uat.ebonex.io/api/claw/skill-purchase-history-url",
      "headers": [
        {
          "name": "x-api-key",
          "required": true,
          "source": "env:PRANA_SKILL_API_FLAG"
        }
      ]
    }
  ],
  "auth": {
    "type": "header",
    "header_name": "x-api-key",
    "env_var": "PRANA_SKILL_API_FLAG",
    "required": true
  },
  "usage_example": "请从知识库中查找RAG相关的技术文档、我想了解RAG在智能客服中的应用案例"
}

path: scripts/prana_skill_client.js
content: #!/usr/bin/env node
/**
 * 调用 Prana 技能接口。：
 * --question / -q：用户需求任务描述，例如：帮我使用XXX技能
 * --request-id / -r：指定需要查询的任务的 request_id
 */
import { randomUUID } from "node:crypto";
import { parseArgs } from "node:util";

const DEFAULT_BASE_URL = "https://claw-uat.ebonex.io";
const DEFAULT_SKILL_KEY = "rag-skill";
const AGENT_RUN_TIMEOUT_MS = 150000;
const AGENT_RESULT_RETRY_TIMES = 30;
const AGENT_RESULT_RETRY_INTERVAL_MS = 20000;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function isTimeoutError(error) {
  if (!error) return false;
  return error.name === "TimeoutError" || error.name === "AbortError";
}

async function postJson(url, payload, xApiKey, timeoutMs) {
  const requestId = payload && typeof payload === "object" ? payload.request_id || "" : "";
  console.error(`[HTTP REQUEST] request_id=${requestId}`);
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": xApiKey,
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(timeoutMs),
  });
  const text = await res.text();
  if (!res.ok) {
    const error = new Error(`HTTP ${res.status}: ${text}`);
    error.status = res.status;
    throw error;
  }
  return JSON.parse(text);
}

async function invokeAgentResultWithRetry(baseUrl, requestId, xApiKey) {
  const url = `${baseUrl.replace(/\/+$/, "")}/api/claw/agent-result`;
  let lastResult = { error: true, detail: "agent-result 尚未获取到结果" };

  for (let attempt = 1; attempt <= AGENT_RESULT_RETRY_TIMES; attempt += 1) {
    if (attempt > 1) {
      await sleep(AGENT_RESULT_RETRY_INTERVAL_MS);
    }
    try {
      const result = await postJson(url, { request_id: requestId }, xApiKey, AGENT_RUN_TIMEOUT_MS);
      lastResult = result;
      let status = String(result?.data?.status || "").toLowerCase();
      if (!status) {
        status = String(result?.status || "").toLowerCase();
      }
      if (status === "running") {
        continue;
      }
      if (status) {
        return result;
      }
    } catch (error) {
      return { error: true, detail: `第 ${attempt} 次 agent-result 请求失败: ${error.message || error}` };
    }
  }

  console.error(
    "提示: 本轮尝试已达到上限；Prana 服务端任务可能仍需要较长时间才能完成。若希望继续等待同一任务结果，必须先向用户确认；仅在用户明确确认“重试”后，才可继续执行后续命令：node scripts/prana_skill_client.js -r " +
      requestId,
  );
  return null;
}

async function invokeAgentRun(baseUrl, skillKey, question, xApiKey) {
  const requestId = randomUUID();
  const url = `${baseUrl.replace(/\/+$/, "")}/api/claw/agent-run`;
  const payload = {
    skill_key: skillKey,
    question,
    request_id: requestId,
  };

  try {
    const runResult = await postJson(url, payload, xApiKey, AGENT_RUN_TIMEOUT_MS);
    const runStatus = String(runResult?.data?.status || "").toLowerCase();
    if (runStatus === "running") {
      return await invokeAgentResultWithRetry(baseUrl, requestId, xApiKey);
    }
    return runResult;
  } catch (error) {
    if (isTimeoutError(error)) {
      return await invokeAgentResultWithRetry(baseUrl, requestId, xApiKey);
    }
    throw error;
  }
}

async function main() {
  let values;
  try {
    ({ values } = parseArgs({
      args: process.argv.slice(2),
      options: {
        question: { type: "string", short: "q", default: "" },
        "request-id": { type: "string", short: "r", default: "" },
        help: { type: "boolean", short: "h", default: false },
      },
      allowPositionals: false,
    }));
  } catch (error) {
    console.error(error.message || error);
    process.exit(1);
  }

  if (values.help) {
    console.log(
      "调用 Prana 技能接口。\n" +
        "\n" +
        "  -q, --question   用户需求任务描述，例如：帮我使用XXX技能\n" +
        "  -r, --request-id 指定需要查询的任务的 request_id\n" +
        "\n" +
        '用法: node scripts/prana_skill_client.js -q "用户任务"\n' +
        "      或: node scripts/prana_skill_client.js -r <request_id>",
    );
    process.exit(0);
  }

  const questionTrimmed = String(values.question || "").trim();
  const requestIdArg = String(values["request-id"] || "").trim();
  if (!questionTrimmed && !requestIdArg) {
    console.error("错误: 请使用 -q / --question 发起新任务，或使用 -r / --request-id 查询指定任务状态。");
    process.exit(1);
  }

  const xApiKey = String(process.env.PRANA_SKILL_API_FLAG || "").trim();
  if (!xApiKey) {
    console.error("错误: 未检测到环境变量 PRANA_SKILL_API_FLAG。");
    process.exit(1);
  }

  try {
    const result = questionTrimmed
      ? await invokeAgentRun(
          DEFAULT_BASE_URL,
          DEFAULT_SKILL_KEY,
          questionTrimmed,
          xApiKey,
        )
      : await invokeAgentResultWithRetry(DEFAULT_BASE_URL, requestIdArg, xApiKey);
    if (result != null) {
      console.log(JSON.stringify(result, null, 2));
    }
  } catch (error) {
    console.error(`请求失败: ${error.message || error}`);
    process.exit(2);
  }
}

await main();


path: scripts/prana_skill_client.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import argparse
import json
import os
import socket
import sys
import time
import uuid
import urllib.error
import urllib.request
from typing import Any, Dict, Optional

DEFAULT_BASE_URL = "https://claw-uat.ebonex.io"
DEFAULT_SKILL_KEY = "rag-skill"
AGENT_RUN_TIMEOUT_SEC = 150
AGENT_RESULT_RETRY_TIMES = 30
AGENT_RESULT_RETRY_INTERVAL_SEC = 20


def _headers(x_api_key: str) -> Dict[str, str]:
    return {
        "Content-Type": "application/json",
        "x-api-key": x_api_key,
    }


def _post_json(url: str, payload: Dict[str, Any], x_api_key: str, timeout: int) -> Dict[str, Any]:
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    request_id = payload.get("request_id", "") if isinstance(payload, dict) else ""
    print(f"[HTTP REQUEST] request_id={request_id}", file=sys.stderr)
    req = urllib.request.Request(
        url=url,
        data=data,
        method="POST",
        headers=_headers(x_api_key),
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _is_timeout_error(exc: BaseException) -> bool:
    if isinstance(exc, TimeoutError):
        return True
    if isinstance(exc, socket.timeout):
        return True
    if isinstance(exc, urllib.error.URLError):
        return isinstance(exc.reason, socket.timeout)
    return False


def invoke_agent_result_with_retry(
    base_url: str, request_id: str, x_api_key: str
) -> Optional[Dict[str, Any]]:
    url = base_url.rstrip("/") + "/api/claw/agent-result"
    last_result: Dict[str, Any] = {"error": True, "detail": "agent-result 尚未获取到结果"}
    for attempt in range(1, AGENT_RESULT_RETRY_TIMES + 1):
        if attempt > 1:
            time.sleep(AGENT_RESULT_RETRY_INTERVAL_SEC)
        try:
            result = _post_json(
                url=url,
                payload={"request_id": request_id},
                x_api_key=x_api_key,
                timeout=AGENT_RUN_TIMEOUT_SEC,
            )
            last_result = result
            data = result.get("data") if isinstance(result, dict) else None
            status = ""
            if isinstance(data, dict):
                status = str(data.get("status", "")).lower()
            if not status and isinstance(result, dict):
                status = str(result.get("status", "")).lower()
            if status == "running":
                continue
            if status:
                return result
        except Exception as exc:  # noqa: BLE001
            return {"error": True, "detail": f"第 {attempt} 次 agent-result 请求失败: {exc}"}
    print(
        f"提示: 本轮尝试已达到上限；Prana 服务端任务可能仍需要较长时间才能完成。若希望继续等待同一任务结果，必须先向用户确认；仅在用户明确确认“重试”后，才可继续执行后续命令：python3 scripts/prana_skill_client.py -r {request_id}",
        file=sys.stderr,
    )
    return None


def invoke_agent_run(
    base_url: str, skill_key: str, question: str, x_api_key: str
) -> Optional[Dict[str, Any]]:
    request_id = str(uuid.uuid4())
    run_url = base_url.rstrip("/") + "/api/claw/agent-run"
    run_payload = {
        "skill_key": skill_key,
        "question": question,
        "request_id": request_id,
    }
    try:
        run_result = _post_json(
            url=run_url,
            payload=run_payload,
            x_api_key=x_api_key,
            timeout=AGENT_RUN_TIMEOUT_SEC,
        )
        run_data = run_result.get("data") if isinstance(run_result, dict) else None
        run_status = str((run_data or {}).get("status", "")).lower() if isinstance(run_data, dict) else ""
        if run_status == "running":
            return invoke_agent_result_with_retry(base_url, request_id, x_api_key)
        return run_result
    except Exception as exc:  # noqa: BLE001
        if _is_timeout_error(exc):
            return invoke_agent_result_with_retry(base_url, request_id, x_api_key)
        raise


def main() -> None:
    parser = argparse.ArgumentParser(
        description="调用 Prana 技能接口。question=用户需求任务。"
    )
    parser.add_argument("--question", "-q", default=None, help="用户需求任务描述，例如：帮我使用XXX技能")
    parser.add_argument(
        "--request-id",
        "-r",
        default=None,
        dest="request_id",
        help="指定需要查询的任务的 request_id",
    )
    args = parser.parse_args()

    question_trimmed = str(args.question).strip() if args.question is not None else ""
    request_id_arg = (args.request_id or "").strip()
    if not question_trimmed and not request_id_arg:
        parser.error("必须提供 --question/-q 发起新任务，或提供 --request-id/-r 查询指定任务状态")

    x_api_key = os.getenv("PRANA_SKILL_API_FLAG", "").strip()
    if not x_api_key:
        print("错误: 未检测到环境变量 PRANA_SKILL_API_FLAG。", file=sys.stderr)
        sys.exit(1)

    try:
        if question_trimmed:
            result = invoke_agent_run(
                base_url=DEFAULT_BASE_URL,
                skill_key=DEFAULT_SKILL_KEY,
                question=question_trimmed,
                x_api_key=x_api_key,
            )
        else:
            result = invoke_agent_result_with_retry(
                base_url=DEFAULT_BASE_URL,
                request_id=request_id_arg,
                x_api_key=x_api_key,
            )
        if result is not None:
            print(json.dumps(result, ensure_ascii=False, indent=2))
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        print(f"HTTP {exc.code}: {body}", file=sys.stderr)
        sys.exit(2)
    except Exception as exc:  # noqa: BLE001
        print(f"请求失败: {exc}", file=sys.stderr)
        sys.exit(3)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (HIGH)