Skill: cyuxlif/andonq (version 2.0.0)

--- name: AndonQ description: AndonQ 腾讯云智能客服"领域虾" — 不切窗口、不排队，即刻获得腾讯云全产品线专业解答。支持工单查询（列表/详情/流水）、集团/MC 工单与需求单管理、腾讯云全产品线智能问答、云产品资源查询等。当用户查询工单、查看工单详情、咨询腾讯云产品问题、查询集团(360)工单/需求单、或查询腾讯云资源信息时使用。 description_zh: "腾讯云智能客服领域虾，支持工单、需求单、云产品问答、资源查询" description_en: "Tencent Cloud smart customer service — tickets, stories, product Q&A, resource query" version: 2.0.0 allowed-tools: Read,Write,Bash,Grep metadata: openclaw: emoji: "☁️" requires: bins: - python3 permissions: - "network:https://andon.cloud.tencent.com" - "network:https://cloud.tencent.com" security: data_handling: "OAuth2 临时码仅保存在本地 ~/.andonq/auth.json（权限 0600），仅在调用 AndonQ 网关时通过 HTTPS 的 X-TANDON-CODE 请求头传输；不写入任何日志，不持久化其他用户数据" --- # ☁️ AndonQ — 腾讯云智能客服"领域虾" ## 零、自我介绍 当用户询问"你是谁"、"andonq 是什么"等**身份相关问题**时，**必须**使用以下固定内容回答（保持 emoji 和格式）： > Hi，我是 > **AndonQ** — 腾讯云智能客服"领域虾" > > 我能帮您： > 🎫 **工单一体化**：查工单列表、工单详情、工单流水，集团（360）工单、MC 工单一站式搞定 > 📋 **需求单管理**：集团需求单查询、需求单详情一键查看 > 🤖 **智能问答**：腾讯云全产品线（CVM、轻量、COS、CDN、数据库、VPC……）专业解答 > ☁️ **资源查询**：云产品资源盘点、配置查看 > > 不切窗口、不排队，即刻获得腾讯云全产品线专业解答。 > > **AndonQ: Just Ask AndonQ！** ### 0.1 功能查询（动态） 当用户询问"你有哪些功能"、"你能做什么"、"支持哪些能力"、"功能列表"等**功能范围相关问题**时，**必须通过 ChatCompletionsAndonQ 接口动态查询**，不可仅依赖本文档中的静态描述。 **触发关键词**：有哪些功能、能做什么、支持什么、功能列表、能力清单、都能干啥 **执行方式**： ```bash SID=$(python3 -c 'import uuid;print(uuid.uuid4())') python3 {baseDir}/scripts/andon_sse_api.py 'AndonQ有哪些功能和能力' "$SID" ``` 若功能查询发生在多轮对话中，则复用首轮传入的 session_id（见 §3.3）。 **原因**：ChatCompletionsAndonQ 接口的功能会持续更新迭代，接口自身最清楚当前支持哪些能力。通过动态查询可确保向用户展示的功能列表始终是最新、最完整的，无需 Skill 侧同步更新。 **展示规则**： 1. 先展示固定的身份介绍（上方自我介绍内容） 2. 再展示从接口动态获取的功能列表 3. 接口返回失败时，兜底展示本文档 4.1 节中的静态功能场景列表 --- 核心能力：通过 **OAuth2 临时码鉴权**调用 ChatCompletionsAndonQ 接口，以 SSE 流式对话方式统一承载工单查询、需求单查询、云产品问答、云资源查询等所有 AndonQ 能力。 --- ## 一、鉴权方式 使用 **OAuth2 临时码鉴权**。用户在浏览器完成腾讯云账号授权后，把页面展示的临时码绑定到本地（`~/.andonq/auth.json`），后续调用接口时 Skill 自动将临时码放在 `X-TANDON-CODE` 请求头中发送，**在授权有效期内可跨会话复用**。 ### 1.1 首次使用引导（用户未绑定临时码时必须执行） 当 `scripts/check_env.py` 返回码为 `2`，或 `andon_sse_api.py` 退出码为 `1` 且 `stderr` 提示需要授权（含授权 URL / Step 1/2/3 引导文案）时，**必须**按以下三步引导用户，并且**把授权链接以可点击的 Markdown 链接形式展示**（格式：`[打开授权页面](授权URL)`），而不是让用户手动复制： **Step 1**：获取一次性授权链接并展示给用户（带随机 state，防 CSRF） ```bash python3 {baseDir}/scripts/andon_auth.py --authorize-url ``` 脚本返回 `{"success": true, "authorize_url": "https://cloud.tencent.com/open/authorize?..."}`。**必须**用以下 Markdown 格式展示给用户，便于直接点击： > 请点击 [打开 AndonQ 授权页面]({authorize_url}) 完成腾讯云账号登录。 **Step 2**：用户在授权页面登录成功后，复制页面上展示的临时码，原样粘贴即可（脚本会自动识别并提取有效部分）。 **Step 3**：绑定临时码（推荐让用户直接把临时码发给 AI，由 Skill 调用 CLI 一键保存） ```bash python3 {baseDir}/scripts/andon_auth.py --save '<用户粘贴的内容>' ``` 脚本会自动识别并从粘贴内容中提取有效 token，写入 `~/.andonq/auth.json`（权限 0600），返回 `{"success": true, "token_masked": "****xxxx", "obtained_at": <Unix 秒级时间戳>}`。绑定成功后**授权有效期内**所有调用均可复用，无需重复绑定。 > **⚠️ 重要规则**： > - **严禁让用户把临时码在公共场合明文暴露**，展示给用户时始终使用 `token_masked`（末 4 位可见） > - **严禁自行编造或猜测临时码**，必须由用户从授权页面复制 > - 临时码**不是**腾讯云 AK/SK，不需要用户配置任何环境变量 ### 1.2 临时码存储 | 项 | 值 | |----|----| | 存储路径 | `~/.andonq/auth.json` | | 文件权限 | `0600` | | 字段 | `{"token": "<临时码>", "obtained_at": <Unix 秒级时间戳>}` | | 传输方式 | HTTPS 请求头 `X-TANDON-CODE: <临时码>` | | 有效期 | 腾讯云 OAuth2 授权有效期内可跨会话复用；失效后接口返回 401/403，需重新授权并绑定 | --- ## 二、前置检查（初始化工作流） **每次对话的首次操作前必须先执行环境检测**（含版本检查）。同一对话中后续操作无需重复执行。 ### 2.1 初始化工作流 ```bash python3 {baseDir}/scripts/check_env.py ``` 脚本会依次执行以下检测： 1. 检查 Python 版本（需要 3.7+） 2. 检查 Skill 版本更新（读取本地 `_meta.json` 版本，与远端最新版本对比） 3. 检查本地 OAuth2 临时码是否已绑定（读取 `~/.andonq/auth.json`） 根据返回码判断状态： - `0` = 环境就绪，可以正常使用所有功能 - `1` = Python 版本不满足要求 → 提示用户升级 Python - `2` = OAuth2 临时码未配置 → 走 §1.1 首次使用引导 **版本更新提示策略**： `scripts/check_env.py` 输出中若包含 `发现新版本` 关键词，说明有更新可用。此时**不阻断功能使用**，按以下规则处理： 1. **首次提醒**：在本次回答**末尾**附加一条更新提示（不影响正常功能回答）： > 💡 AndonQ 有新版本可用（{当前版本} → {最新版本}），建议前往 SkillHub 或 ClawHub 更新以获得最新功能。 2. **同一对话不重复提醒**：首次提醒后，同一对话中后续回答不再附加更新提示 3. **不阻断任何功能**：无论是否有新版本，所有功能正常可用 检查结果会保存到 `~/.andonq/version_check_cache.json` 供参考。网络不可用或远端接口异常时版本检查会被跳过（不影响后续流程）。可通过 `--skip-update` 参数主动跳过。 ### 2.2 静默模式（供脚本内部调用） ```bash python3 {baseDir}/scripts/check_env.py --quiet ``` 静默模式下仅输出错误信息，适合其他脚本调用获取环境状态。 ### 2.3 跳过版本检查 ```bash python3 {baseDir}/scripts/check_env.py --skip-update ``` 跳过远端版本对比，直接进行后续环境检测。适用于离线环境或已知无需更新的场景。可与 `--quiet` 组合使用（仅限 check_env.py）。 ### 2.4 交互式绑定临时码 ```bash python3 {baseDir}/scripts/check_env.py --bind-code ``` 在终端交互式地打印授权链接并读取用户粘贴的临时码，写入 `~/.andonq/auth.json`。**通常由用户自行在终端运行**；AI 侧一键绑定请使用 §1.1 Step 3 的 `andon_auth.py --save` 方式。 --- ## 三、API 调用方式 **所有用户问题统一通过 ChatCompletionsAndonQ SSE 流式接口处理**，使用独立调用脚本： ```bash python3 {baseDir}/scripts/andon_sse_api.py '<question>' [session_id] ``` - `question`：用户问题（必填，对应请求体 `Content` 字段） - `session_id`：会话 ID（UUID v4，**调用方必须显式传入**） - **首次对话**：Skill 生成一个新的 UUID v4 并传入（例如 `python3 -c 'import uuid;print(uuid.uuid4())'`），脚本会把本次使用的 session_id 回显到 **stderr 首行**：`[session] <uuid>` - **追问**：**必须**使用首次调用时传入的同一个 session_id（从本对话上下文中回忆首轮传入的值；若不确定，可从上一轮脚本 stderr 首行的 `[session] <uuid>` 回显中读取），而不是脚本或后端返回值里的其他 session_id - 如不传入，脚本会兜底生成一个 UUID v4 并同样回显到 stderr，但**强烈不推荐省略**：Skill 必须自行生成并传入，保证多轮上下文可靠延续 示例（session_id 为调用方事先生成并在同一对话全程复用的 UUID v4，见 §3.3）： ```bash # 首轮：生成新 UUID 并打进命令 SID=$(python3 -c 'import uuid;print(uuid.uuid4())') python3 {baseDir}/scripts/andon_sse_api.py '查询我的工单' "$SID" # 同一对话的后续追问：复用首轮的 SID python3 {baseDir}/scripts/andon_sse_api.py '工单 202604010721 的详情' "$SID" python3 {baseDir}/scripts/andon_sse_api.py '详细说说' "$SID" ``` ### 3.1 协议同意（子账号级，永久一次） 同一子账号**首次**调用 ChatCompletionsAndonQ 时，接口会返回《腾讯云 AndonQ 软件许可及服务协议》的同意请求。**同意是子账号级别的永久操作，一个子账号只需同意一次，之后所有对话均不再弹出**。 **判断方式**：接口返回的 `content` 中包含 `AndonQ服务协议` 或 `请先阅读并同意` 关键词。 **处理流程**： 1. **展示协议提示**：将接口返回的 content **原样展示**给用户，**不得省略协议链接、不得改写、不得摘要** 2. **等待用户同意**：用户回复「同意」后，将「同意」作为问题发送给同一 SessionID 的 ChatCompletionsAndonQ 接口： ```bash python3 {baseDir}/scripts/andon_sse_api.py '同意' '<同一SessionID>' ``` 3. **同意成功**：接口返回欢迎信息，该子账号后续所有对话均可正常使用，不会再触发协议同意 4. **用户拒绝**：不发送同意，提示用户必须同意协议后才能使用 AndonQ > **⚠️ 重要**： > - **严禁自动发送「同意」**，必须用户明确知晓并同意后才能发送 > - 同意请求使用的 SessionID 必须与触发协议的首次调用保持一致 ### 3.2 输出透传与错误处理 **stdout 约定**：脚本输出的正文为后端原样 Markdown，**Skill 侧不做任何后处理**，直接将 stdout 内容展示给用户即可。 **stderr 约定**： - 首行固定为 `[session] <uuid>`（本次实际使用的 session_id，便于追问复用；展示用户时请忽略） - 调用失败时追加一段**完整的人话错误引导**（如「OAuth2 临时码失效，请打开授权页面 xxx…Step 1/2/3」），Skill **直接按这些提示文案引导用户**，不需要自行翻译或构造另一套话术 **退出码约定**：`0` = 成功；`1` = 失败（参数缺失 / 授权失效 / 网络错误 / 运行错误） #### 3.2.1 stdout 编码问题兜底（避免 Bash 工具读取正文） 默认直接读取并展示 `stdout`。如果调用脚本时发现读取 `stdout` 存在编码问题，例如中文乱码、非 ASCII 字符被替换、Markdown 内容损坏、emoji 丢失，或无法稳定读取完整接口返回，则**不要继续通过 Bash 工具读取正文**，必须改用文件中转： 1. 将接口返回正文写入 UTF-8 临时文件 2. `stdout` 只输出文件路径、状态或其他 ASCII 安全信息，避免正文继续经过 Bash/终端输出链路 3. Agent 使用文件读取能力读取该临时文件内容，不要再通过 `cat`、重定向回读等 Bash 方式读取正文 4. 将读取到的内容原样展示给用户；展示后可清理临时文件 该策略只用于绕过 Agent 环境对 `stdout` 的编码或截断问题，不改变后端返回内容，也不做摘要、翻译或改写。 ### 3.3 SessionID 管理（强约束） `SessionID` 控制多轮对话上下文，**完全由调用方掌控**。Skill **必须**按以下规则执行，否则多轮对话会退化为每轮新会话，上下文丢失。 #### 3.3.1 生成与传入规则（MUST） 1. **首次对话必须主动生成 UUID v4 并显式传入**。推荐方式： ```bash SID=$(python3 -c 'import uuid;print(uuid.uuid4())') python3 {baseDir}/scripts/andon_sse_api.py '<用户首轮问题>' "$SID" ``` **禁止**省略 session_id 参数依赖脚本兜底（兜底值虽会回显，但会诱导 Skill 遗忘）。 2. **追问必须传入同一个 session_id**：从下方 3.3.2 的持久化手段中读取，而不是重新生成。 3. **不采纳后端返回的 session_id**：后端可能回显 `im:clawith-gateway:...` 等自定义格式，必须忽略。 4. **用户明确要求"新对话/重新开始/换个话题"时**：重新生成一个新 UUID v4，并在后续所有调用中改用新值。 #### 3.3.2 跨轮次复用机制 Skill 本身无状态，跨轮次复用 session_id 依赖以下两个来源（按优先级）： 1. **AI 自身的对话上下文**（主）：首次调用时传入的 session_id 会作为命令参数出现在 AI 的工具调用记录中，后续轮次可直接从本对话上下文中回忆。 2. **脚本 stderr 回显**（兜底）：脚本每次运行都会把本次实际使用的 session_id 打印到 **stderr 首行**：`[session] <uuid>`。若 AI 不确定上一轮传入的 session_id，可用正则 `^\[session\] (\S+)` 从上一轮的 stderr 输出中提取。 #### 3.3.3 判定追问 vs 新对话 仅以下情形视为"新对话"，需重新生成 session_id： - 用户明确说"新对话"/"重新开始"/"换个话题"/"忘掉之前的" 其余情况（包括话题微调、追问细节、要求改写格式等）一律视为追问，**必须复用**首轮传入的 session_id。 > ⚠️ **关键**：SessionID 一旦改变，服务端视为全新对话，不包含任何历史上下文。同一对话中的所有调用必须传入相同的 session_id。 --- ## 四、接口说明 ### 4.1 ChatCompletionsAndonQ（全局对话） 所有用户问题统一通过此接口处理。使用前**必须先加载接口文档**：`{baseDir}/references/api/ChatCompletionsAndonQ.md` | 参数 | 值 | |------|------| | method | `POST` | | url | `https://andon.cloud.tencent.com/api/v1/gateway/chat-completions-andonq` | | 鉴权 header | `X-TANDON-CODE: <临时码>` | | 请求体 | `{"content":"<用户问题>","session_id":"<uuid>"}` | | 响应格式 | SSE 流式（`message.delta` / `message.completed` / `run.progress` / `run.completed` / `run.error`） | | action | `ChatCompletionsAndonQ` | > **⚠️ 动态能力**：此接口的功能会持续更新迭代。当用户询问"有哪些功能"时，**必须通过此接口动态查询**（见 0.1 节），不可仅依赖下方静态列表。 已知支持的功能场景（兜底参考，实际以接口返回为准）： - **工单查询**：工单列表、工单详情、工单流水（MC 工单 + 集团（360）工单统一入口） - **需求单查询**：集团需求单列表、需求单详情 - **智能客服问答**：腾讯云全产品线专业问答（CVM、轻量应用服务器、COS、CDN、CLB、VPC、CBS、CAM、数据库、缓存、中间件、Serverless 等所有云产品） - **云产品资源查询**：实例、地域、安全组、负载均衡、云硬盘等资源信息 --- ## 五、注意事项 1. **临时码安全**：严禁将临时码明文硬编码在代码或脚本中；严禁让用户在公开/群聊等场合暴露临时码；展示时一律脱敏（末 4 位可见） 2. **授权有效期**：临时码在腾讯云 OAuth2 授权有效期内可跨会话复用；接口返回 401/403 时必须引导用户重新走 §1.1 授权流程 3. **跨平台支持**：所有脚本均使用纯 Python 实现，支持 Windows / Linux / macOS 4. **SessionID 管理**：**必须**由 Skill 自行生成 UUID v4 并显式传入脚本（禁止依赖脚本兜底）；追问时**必须**复用首轮传入的 session_id（从本对话上下文回忆，或从上一轮脚本 stderr 首行的 `[session] <uuid>` 回显中读取）。详见 §3.3 5. **SSE 超时**：默认超时 600 秒 6. **输出透传**：脚本 `stdout` 为 SSE 正文（原样 Markdown），直接展示给用户；Skill 不做任何后处理，所有业务逻辑由后端 ChatCompletionsAndonQ 承载 7. **错误引导**：脚本退出码非 0 时 `stderr` 会输出完整的人话提示（如授权失效的 Step 1/2/3 + 授权 URL），**直接按 stderr 提示文案引导用户**，无需自行构造话术 --- ## 六、安全与权限声明 ### 6.1 所需凭证 本 Skill 不使用任何环境变量凭证，仅使用 OAuth2 临时码： | 项 | 说明 | |----|------| | 存储文件 | `~/.andonq/auth.json`（权限 0600，仅当前用户可读写） | | 字段 | `{"token": "<临时码>", "obtained_at": <Unix 秒级时间戳>}` | | 获取方式 | 用户在腾讯云 OAuth2 授权页面登录后复制，由用户发给 AI 或在终端执行 `--save` 命令写入 | | 传输方式 | HTTPS 请求头 `X-TANDON-CODE: <临时码>`，仅发送到 `andon.cloud.tencent.com` 网关 | ### 6.2 网络访问范围 本 Skill 仅连接以下官方域名： | 域名 | 用途 | |------|------| | `andon.cloud.tencent.com` | ChatCompletionsAndonQ 网关（全局对话） | | `cloud.tencent.com` | OAuth2 授权页面（仅构造 URL，Skill 不代为发起请求） | ### 6.3 数据安全 - **临时码处理**：仅保存在本地 `~/.andonq/auth.json`（权限 0600）；仅通过 HTTPS 请求头发送给 AndonQ 网关；展示时一律脱敏 - **无额外持久化**：除临时码文件与版本检查缓存（`~/.andonq/version_check_cache.json`）外，不创建其他配置文件、不缓存用户对话数据 - **SSL 验证**：所有 HTTPS 请求启用完整 SSL 证书验证 - **纯 Python 实现**：无需 curl、openssl、jq 等外部依赖 --- ## 七、参考文档 使用接口前，**建议先加载对应的接口文档**获取完整参数说明和展示规则： - **ChatCompletionsAndonQ 全局对话**：`{baseDir}/references/api/ChatCompletionsAndonQ.md` — 接口参数、SSE 事件、协议同意流程、SessionID 管理

path: references/api/ChatCompletionsAndonQ.md
content: # ChatCompletionsAndonQ — AndonQ 全局对话

AndonQ 全局对话交互接口（SSE 流式输出），核心能力（工单/需求单查询、云产品问答、云资源查询等）由后端统一承载。

## 接口定义

| 项 | 值 |
|------|------|
| URL | `https://andon.cloud.tencent.com/api/v1/gateway/chat-completions-andonq` |
| method | `POST` |
| Content-Type | `application/json` |
| Accept | `text/event-stream` |
| 鉴权 Header | `X-TANDON-CODE: <OAuth2 临时码>` |
| action | `ChatCompletionsAndonQ` |

## 请求参数（请求体 JSON）

| 参数 | 必选 | 类型 | 描述 |
|------|------|------|------|
| `content` | 是 | String | 用户问题，如 `查询我的工单` |
| `session_id` | 是 | String | 会话 ID（UUID v4），同一对话必须保持不变 |

### curl 示例

```bash
curl -XPOST 'https://andon.cloud.tencent.com/api/v1/gateway/chat-completions-andonq' \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -H 'X-TANDON-CODE: <你的临时码>' \
  --data '{"content":"查询下我的轻量服务器","session_id":"<UUID v4>"}' \
  -N
```

## 调用示例（脚本）

```bash
python3 {baseDir}/scripts/andon_sse_api.py '查询我的工单'
python3 {baseDir}/scripts/andon_sse_api.py '详细说说' '<首轮使用的 session_id>'
```

## 鉴权（OAuth2 临时码）

### 授权流程

1. **浏览器打开授权页面并登录**（腾讯云开放平台）；授权 URL 形式如下，其中 `state` 每次由脚本随机生成（UUID）用于防 CSRF。终端中该 URL 独占一行，支持 Cmd/Ctrl 点击直接打开，无需手动复制：

   ```
   https://cloud.tencent.com/open/authorize?app_id=100048267608&redirect_url=http%3A%2F%2Fandon.qq.com%2Foauth%2Faq%2Fcallback&scope=login&state=<随机 uuid>
   ```

   实际请通过 `python3 {baseDir}/scripts/check_env.py --bind-code` 打印出的 URL打开，不要复用旧 URL。

2. **登录成功后**，复制页面上展示的临时码。

3. **绑定临时码**（交互式粘贴并落盘）：

   ```bash
   python3 {baseDir}/scripts/check_env.py --bind-code
   ```

### 临时码本地存储

- **路径**：`~/.andonq/auth.json`
- **权限**：`0600`（仅当前用户可读写）
- **字段**：
  ```json
  {
    "token": "<临时码>",
    "obtained_at": 1761227278
  }
  ```
  `obtained_at` 为 Unix 秒级时间戳（整数）。旧版本 ISO8601 字符串会被自动兼容读取。
- **跨会话**：在临时码授权有效期内，新开会话、历史被压缩时脚本均会从该文件读取并复用；临时码过期后需重新授权并绑定

### 调用阶段

每次调用 `andon_sse_api.py` 时，脚本自动从 `~/.andonq/auth.json` 读取 `token`，并作为 HTTP Header `X-TANDON-CODE` 发送至网关。

### 临时码失效

当接口返回 `HTTP 401 / 403`，脚本会把错误统一映射为 `AuthCodeInvalid`，提示用户重新授权并执行 `--bind-code` 绑定。

> **AK/SK 已废弃**：本版本起不再使用腾讯云 API AK/SK 签名；`TENCENTCLOUD_SECRET_ID` / `TENCENTCLOUD_SECRET_KEY` / `TENCENTCLOUD_TOKEN` 环境变量无需配置（保留也不会影响本 Skill 运行）。

## 返回格式

脚本自动解析 SSE 流并汇总为统一 JSON：

```json
{
  "success": true,
  "action": "ChatCompletionsAndonQ",
  "data": {
    "content": "当前账号下共有 **15** 个工单...",
    "is_final": true
  },
  "requestId": "d72bal4g699bmj4h7gs0"
}
```

## 返回字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `content` | String | Markdown 格式回答（原样返回，不做任何后处理） |
| `is_final` | Boolean | 是否为最终结果（收到 `run.completed` 或 `message.completed` 时为 `true`） |

## SSE 响应事件（对齐 OpenClaw gwproto.StreamEvent，脚本已自动解析）

| 事件 `type` | 脚本行为 | 关键字段 |
|------|------|------|
| `message.delta` | 追加 `delta` 到 content 缓冲；实时 flush 到 stdout | `delta` |
| `message.completed` | 使用 `reply` 覆写 content（保证完整） | `reply` |
| `run.progress` | 输出 `[stage] summary` 到 stderr | `stage`、`summary` |
| `run.completed` | 正常结束流，返回成功结果 | — |
| `run.error` | 映射为统一错误 `{code, message}` 返回 | `error.type`、`error.message` |
| `[DONE]`（哨兵） | 兜底结束读流 | — |

其他事件类型（如 `public.delta`、`thought.delta` 等）会被静默忽略。

## 错误响应

```json
{
  "success": false,
  "action": "ChatCompletionsAndonQ",
  "error": {
    "code": "AuthCodeInvalid",
    "message": "OAuth2 临时码已失效或无权限，请重新授权..."
  },
  "requestId": ""
}
```

### 常见错误码

| 错误码 | 含义 | 解决方案 |
|--------|------|----------|
| `MissingAuthCode` | 未找到 `~/.andonq/auth.json` 或为空 | 执行 `python3 scripts/check_env.py --bind-code` 按提示完成授权与粘贴 |
| `AuthCodeInvalid` | 临时码失效（HTTP 401 / 403） | 重新访问授权页面登录，再 `--bind-code` 绑定 |
| `NetworkError` | 网络异常 | 检查网络连通性 |
| `HTTPError` | HTTP 状态码非 200 | 参考 message 排查 |
| `RunError` | 后端 `run.error` | 参考 message 排查 |

## 展示规则

- `content` 为 Markdown 原文，可直接展示给用户
- 严禁自行对 `content` 做链接替换、URL 改写等后处理，所有业务逻辑由后端 ChatCompletionsAndonQ 承载
- 多轮对话必须复用同一 `session_id`，改变 `session_id` 视为开启新对话

## 协议同意（子账号级，永久一次）

同一子账号**首次**调用 ChatCompletionsAndonQ 时，接口会返回《腾讯云 AndonQ 软件许可及服务协议》的同意请求。

**判断方式**：`content` 中包含 `软件许可及服务协议` 或 `请先阅读并同意` 关键词。

**处理流程**：

1. 直接将 `content` 展示给用户（含协议链接）
2. 用户回复「同意」后，以**同一 session_id** 发送「同意」：
   ```bash
   python3 {baseDir}/scripts/andon_sse_api.py '同意' '<同一SessionID>'
   ```
3. 同意成功后，该子账号后续所有对话均可正常使用

> **⚠️ 严禁自动发送「同意」**，必须由用户明确同意后才能发送。

## 频率限制

- 接口限制 20 次/秒（维度：API + 接入地域 + 子账号）

## SessionID 管理

调用方（Skill）**必须**显式生成并传入 `session_id`，**不要依赖脚本兜底**；脚本每次运行会把本次使用的 `session_id` 回显到 **stderr 首行** `[session] <uuid>`，供调用方读取复用。

| 场景 | session_id 处理 |
|------|---------------|
| **首次对话** | 调用方生成一个新的 UUID v4 并显式传入（例如 `python3 -c 'import uuid;print(uuid.uuid4())'`） |
| **同一对话追问** | **必须**沿用首次调用时传入的 session_id（从本对话上下文中回忆；若不确定，可从上一轮脚本 stderr 首行的 `[session] <uuid>` 回显中提取），**不采纳服务端返回的 session_id** |
| **用户要求新对话 / 重新开始** | 重新生成一个新的 UUID v4，并在后续所有调用中改用新值 |

> ⚠️ session_id 一旦改变，服务端视为全新对话，不包含任何历史上下文。


path: scripts/andon_auth.py
content: #!/usr/bin/env python3
"""
AndonQ OAuth2 临时码本地存储与授权 URL 生成模块

职责：
    1) 提供统一的 OAuth2 授权 URL 构造（带随机 state，防 CSRF）
    2) 提供 ~/.andonq/auth.json 的读写接口（临时码落盘与加载）

存储格式（~/.andonq/auth.json，权限 0600）:
    {"token": "<临时码>", "obtained_at": <Unix 秒级时间戳>}
    兼容读取：旧版 ISO8601 字符串会自动解析为秒级整数

本模块无项目内依赖，可被 check_env.py / scripts/andon_sse_api.py 等同时 import。

作为模块导入:
    from andon_auth import (
        AUTH_HEADER, AUTH_FILE, CONFIG_DIR,
        build_authorize_url, load_auth_code, save_auth_code,
    )

作为 CLI 调用（供 AI Agent 直接保存用户粘贴的临时码）:
    python3 andon_auth.py --save '<用户粘贴的临时码>'   # 保存到 ~/.andonq/auth.json，输出单行 JSON
    python3 andon_auth.py --authorize-url                # 仅打印带随机 state 的授权 URL
"""

import json
import os
import re
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlencode

# 匹配授权页面直接复制的整行格式："X-<token>-TANDON-CODE"
# 仅提取中间那段纯 token
_TOKEN_INLINE_RE = re.compile(r"^X-(.+?)-TANDON-CODE$", re.IGNORECASE)

def _extract_token(raw: str) -> str:
    """从用户输入中提取纯 token。

    容错两种格式：
      1) 整行复制格式： "X-<token>-TANDON-CODE"    -> 返回 <token>
      2) 纯 token：          "<token>"                  -> 原样返回

    前后空白会被 trim；输入为空时返回空串。
    """
    if not raw:
        return ""
    s = raw.strip()
    m = _TOKEN_INLINE_RE.match(s)
    if m:
        return m.group(1).strip()
    return s

# ---------------------------------------------------------------------------
# 存储路径 & HTTP Header 名称
# ---------------------------------------------------------------------------
CONFIG_DIR = Path.home() / ".andonq"
AUTH_FILE = CONFIG_DIR / "auth.json"
AUTH_HEADER = "X-TANDON-CODE"

# ---------------------------------------------------------------------------
# OAuth2 授权端点（腾讯云开放平台）
# ---------------------------------------------------------------------------
AUTHORIZE_ENDPOINT = "https://cloud.tencent.com/open/authorize"
AUTHORIZE_APP_ID = "100048267608"
AUTHORIZE_REDIRECT_URL = "http://andon.qq.com/oauth/aq/callback"
AUTHORIZE_SCOPE = "login"


def build_authorize_url() -> str:
    """构造一个带随机 state 的 OAuth2 授权 URL（防 CSRF）。

    state 每次调用均使用新的 uuid4.hex。如需稳定 URL
    （如打印后多次复用），请先缓存返回值。
    """
    params = {
        "app_id": AUTHORIZE_APP_ID,
        "redirect_url": AUTHORIZE_REDIRECT_URL,
        "scope": AUTHORIZE_SCOPE,
        "state": uuid.uuid4().hex,
    }
    return f"{AUTHORIZE_ENDPOINT}?{urlencode(params)}"


def _coerce_obtained_at(raw) -> int:
    """将 auth.json 中的 obtained_at 字段归一化为秒级整数时间戳。

    兼容以下形式：
      - int / float：当作 Unix 时间戳（float 截断为秒）
      - ISO8601 字符串（旧版本写入）：解析为秒级时间戳
      - 其他无法识别：返回 0
    """
    if isinstance(raw, bool):
        return 0
    if isinstance(raw, (int, float)):
        try:
            return int(raw)
        except (ValueError, OverflowError):
            return 0
    if isinstance(raw, str):
        s = raw.strip()
        if not s:
            return 0
        # 纯数字字符串（历史上可能有手工填写）
        if s.lstrip("-").isdigit():
            try:
                return int(s)
            except ValueError:
                return 0
        # ISO8601 旧格式兼容
        try:
            # Python 3.7+ 不支持带 Z 后缀，统一替换为 +00:00
            normalized = s.replace("Z", "+00:00")
            dt = datetime.fromisoformat(normalized)
            if dt.tzinfo is None:
                dt = dt.replace(tzinfo=timezone.utc)
            return int(dt.timestamp())
        except (ValueError, TypeError):
            return 0
    return 0

def load_auth_code() -> tuple:
    """
    从 ~/.andonq/auth.json 读取临时码及绑定时间。

    Returns:
        tuple[str, int]: (token, obtained_at)
            - [REDACTED_SECRET]
            - obtained_at: Unix 秒级时间戳整数；不存在/读取失败/无法解析时为 0
    """
    if not AUTH_FILE.exists():
        return "", 0
    try:
        data = json.loads(AUTH_FILE.read_text(encoding="utf-8"))
        # 已落盘数据上做一次兑底提取，兼容人工编辑或旧版本直接写入的原始复制格式
        [REDACTED_SECRET]"token") or "")
        obtained_at = _coerce_obtained_at(data.get("obtained_at"))
        return token, obtained_at
    except (json.JSONDecodeError, OSError):
        return "", 0


def save_auth_code(token: str) -> None:
    """
    将临时码写入 ~/.andonq/auth.json（权限 0600）。

    Args:
        [REDACTED_SECRET]
               - "<token>"                    （纯 token）
               - "X-<token>-TANDON-CODE"      （整行复制格式，自动提取中间 token）
    """
    [REDACTED_SECRET])
    if not token:
        raise ValueError("临时码不能为空")
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    payload = {
        "token": token,
        "obtained_at": int(time.time()),  # Unix 秒级时间戳
    }
    AUTH_FILE.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    try:
        os.chmod(AUTH_FILE, 0o600)
    except OSError:
        # Windows 上 chmod 可能不生效，忽略即可
        pass


# ---------------------------------------------------------------------------
# CLI 入口：供 AI Agent 直接一行命令保存用户粘贴的临时码
#
# 用法:
#     python3 andon_auth.py --save '<用户粘贴的临时码>'
#     python3 andon_auth.py --authorize-url            # 打印带随机 state 的授权 URL
#
# 输出格式（stdout 单行 JSON）:
#     {"success": true,  "token_masked": "****xxxx", "obtained_at": <Unix 秒级时间戳>}
#     {"success": false, "error": "<reason>"}
# ---------------------------------------------------------------------------
def _mask(token: str, visible: int = 4) -> str:
    """脱敏：保留末尾 visible 位，其余用 * 填充"""
    if len(token) <= visible:
        return "*" * len(token)
    return "*" * (len(token) - visible) + token[-visible:]


def _cli_save(raw_token: str) -> int:
    try:
        save_auth_code(raw_token)
    except ValueError as e:
        print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False))
        return 2
    except OSError as e:
        print(json.dumps({"success": False, "error": f"写入失败: {e}"}, ensure_ascii=False))
        return 2

    token, obtained_at = load_auth_code()
    print(json.dumps({
        "success": True,
        "token_masked": _mask(token),
        "obtained_at": obtained_at,
        "auth_file": str(AUTH_FILE),
    }, ensure_ascii=False))
    return 0


def _cli_print_url() -> int:
    print(json.dumps({"success": True, "authorize_url": build_authorize_url()},
                     ensure_ascii=False))
    return 0


def _main(argv: list) -> int:
    if len(argv) >= 2 and argv[1] == "--save":
        if len(argv) < 3:
            print(json.dumps({"success": False,
                              "error": "缺少参数：python3 andon_auth.py --save '<临时码>'"},
                             ensure_ascii=False))
            return 2
        return _cli_save(argv[2])
    if len(argv) >= 2 and argv[1] == "--authorize-url":
        return _cli_print_url()
    print(json.dumps({
        "success": False,
        "error": "用法: python3 andon_auth.py --save '<临时码>' | --authorize-url",
    }, ensure_ascii=False))
    return 2


if __name__ == "__main__":
    import sys
    sys.exit(_main(sys.argv))


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