Skill: 1688aiinfra/1688-cowboy (version 1.0.0)

--- name: 1688客户接待助手 version: 1.0.0 description: | 接待助手 Skill。商家在牛顿端的对话入口，负责招聘接待助手、查看工作日报、解读接待数据、培训知识库；调整接待范围 / 暂停接待统一跳转到管理页面。 接待助手是平台预设的 AI 业务员，名字固定为「接待助手」，不可修改。 触发词：接待助手、业务员、招聘、招接待助手、看日报、接待、转人工、培训、知识库、待完善、调接待范围、改买家等级、改 L 等级、暂停接待、恢复接待、管理接待助手、配触达。 metadata: openclaw: requires: bins: - python3 # 风险命令二次确认 # 这些命令一旦被 AI 跳过 SKILL.md 软约束直接调起，将造成商家配置 / 凭证被篡改； # 通过 risk_command_hooks 让端侧弹窗强制商家显式确认，AI 无法绕过。 risk_command_hooks: # 牛仔配置写入类（改买家等级 / 暂停 / 恢复） # 同时覆盖 cli.py 入口 + cmd.py 直调两条路径，防止 AI 绕开统一入口 # ⚠️ 特别说明：create 不在本表，原因是 Step 4 商家点击「确认模拟效果」已是显式确认， # 且同商家仅能 create 一次（网关硬约束），再弹一次弹窗会导致招聘体验出现重复确认。 - "(cli\\.py\\s+cowboy_config|cowboy_config/cmd\\.py)\\s+(update|pause|resume)" # 知识库答案写入 - "(cli\\.py\\s+knowledge_answer|knowledge_answer/cmd\\.py)\\b" # AK 凭证写入（带任意非空参数才会触发写入分支；查询分支不会进入二次确认） - "(cli\\.py\\s+configure|configure/cmd\\.py)\\s+\\S+" interactions: - name: hire_step1_intro type: rich_card selectionType: hire_intro description: "招聘 Step 1：接待助手自我介绍 rich_card。商家点击唯一按钮 `start`（'好的，开始面试'）即进入 Step 2 站内授权。仅用于首次招聘剧情流，不调后端接口；端侧不识别 rich_card 时可回退 card 渲染。🔴 **数据与示例强一致**：本卡下发的所有字段值（`title` / `submittedTitle` / `iconUrl` / `body` / `buttons[]` 所有 key/label/submittedLabel/description/variant）必须**与 `references/interaction-specs.md` §1 「完整数据示例」字面一致**，禁止改写、禁止凭印象拼装、禁止与 specs 示例出现任何字面偏差（空格 / 标点 / 换行符都不能动）" required_data: fields: | 以 specs §1 「完整数据示例」为唯一权威源，调用前 MUST 先读： - `title` / `submittedTitle` / `iconUrl` / `body` / `buttons[1]` **全部必填**，字面与 specs §1 完全一致 - `submittedTitle` 是本卡特例（仅 Step 1 需要），用于区分面试前 / 面试后两种标题状态 - `iconUrl` 是固定 CDN 链接，缺失会导致 UI 标题前无 icon - 唯一按钮 key=`start` / label='好的，开始面试' / submittedLabel='进入面试流程' / description='用户同意开始面试' / variant=`primary` - 🔴 任何字段与 specs §1 示例不一致 = 违规；调用前 MUST 先读 specs §1 才能拼参 - name: hire_step2_platform_auth type: rich_card selectionType: platform_auth description: "招聘 Step 2：信息读取确认 rich_card。商家点击「确认」后进入站内授权页完成授权动作、主 Agent 推进至 Step 3。本步不调任何后端接口、不含 cowboy_config 任何子命令" required_data: fields: "title='信息读取确认' / body（列出 4 项读取范围） / iconUrl（必填，固定 CDN: O1CN0132cHKR1UMQUfVTUWV_...32-32.png）/ buttons[1]；唯一按钮 key=`confirm`(确认, variant=`primary`，进 Step 3 唯一开关)；本卡 **不需要** `submittedTitle`（仅 Step 1 需要）；字面与 specs §2 完全一致，调用前 MUST 先读 specs §2" - name: hire_step3_buyer_levels type: card selectionType: 买家等级 description: "招聘 Step 3：接待范围配置卡（单题多选）。商家从 `L0~L6` 共 7 个买家等级中勾选愿意由接待助手接待的范围（直接对应子账号 `allowBuyerLevelList` 字段 / `cowboy_config create --levels` 入参）；主 Agent 取该题答案逐项映射为 L 等级后**仅暂存到会话上下文，不调任何后端接口**（`cowboy_config create` 按约拖到 Step 4），推进至 Step 4 hire_step4_sim_dialog。接待需求 / 接待时间 / 红线场景均为平台默认不可改，**不进本卡题目**" required_data: fields: "showSkipButton=false / allowCustomOption=false（必须显式为 false，禁开「自定义选项」，商家自填项会破坏 `--levels` 映射） / confirmedHeaderLabel='确认模拟效果' / confirmedActionLabel='开始上岗！' / questions[1]（仅「哪些买家让我接待」一题，不可增删）" questions: | Q1 "哪些买家让我接待" 多选必填，7 选项（L0买家/L1买家/L2买家/L3买家/L4买家/L5买家/L6买家）——直接逐项映射为 `--levels` 入参的唯一来源；至少勾 1 项 - name: hire_step4_sim_dialog type: conversation_sim_card selectionType: sim_dialog description: "招聘 Step 4：模拟对话效果确认卡。商家预览接待助手对常见买家问题（一件代发/批量折扣/发货时效等）的拟答效果。**Q 由前端预置**（simQuestions 固定不变）；**A 由主 Agent 在 show_interaction 之前并行调 `cli.py test_chat --query {label}` 实时生成**，按 actionType 三类映射：answer→message 原文 / human→「转人工」 / ask→「询问买家」。商家点「确认模拟效果」后主 Agent **此时才调** `cowboy_config create --levels {Step 3 暂存值}`，一步完成子账号创建 + 买家等级写入，成功即将 status 写为 `active`、正式上岗" required_data: fields: "title='模拟对话' / submittedTitle='确认模拟效果' / iconUrl（必填，固定 CDN: O1CN014o8zCI1aoGFy2oaRS_...66-63.png） / simQuestions[2~6]{key,label} / defaultSelectedQuestion / buyerLabel='买家' / agentLabel='接待员' / conversations[]{question,answer} / confirmButtonLabel='确认模拟效果'；调用前 MUST 先读 specs §4" - name: manage_reception type: open_tab selectionType: reception_management description: "打开「接待管理」Tab。招聘完成后所有配置修改入口（改 L 等级范围 / 暂停 / 恢复接待 / 查看授权范围只读 / 看质检状况）统一通过本交互打开端侧组件页（componentKey=`reception_management`）。fire-and-forget，调用后立即返回、不阻塞后续工具调用；**主 Agent 在对话里不再发起任何 `cowboy_config update / pause / resume` 写入**，写入由页面内部完成" required_data: fields: "type='open_tab' / pageTitle='接待专员' / pageDescription='查看接待管理和质检状况' / componentKey='reception_management'（无 url，由 componentKey 路由到端侧已注册组件）" --- # reception-assistant 主 Agent 是商家在牛顿端的**唯一对话入口**，所有商家发起的消息由本 Skill 处理或编排。 **云端接待助手 Agent 是子 Agent**，由本 Skill 在需要时调用 / 配置 / 引导，**主 Agent 不直接接买家询盘**。 统一入口：`python3 {baseDir}/cli.py <command> [options]` ## 路径要求 - 执行脚本时必须使用绝对路径，即 `python3 /完整路径/cli.py <command>` - `{baseDir}` 是 Skill 所在目录的绝对路径，**不要使用相对路径** --- ## 使用流程 · 前置条件（全局，最高优先级） **未完成招聘配置前，除 `hire-reception` 外的所有命令一律阻断，引导商家先走招聘流程。** ### 如何获取 `agent_status` `agent_status` **统一通过 `cowboy_config load` 获取**（CLI：`python3 {baseDir}/cli.py cowboy_config load`）。 返回的 `data.status` 即为 `agent_status`，取值范围： | status | 含义 | 路由行为 | | ----------- | --------- | --------------------------------- | | `not_hired` | 未招聘（未安装） | 走 `hire-reception` 4 步剧情对话流 | | `active` | 正常运行 | 正常路由 | | `paused` | 已暂停 | 正常路由（首页实时卡片展示“已暂停接待中”） | > 备注：`cowboy_config` 是为路由判断服务的底层依赖能力，不出现在商家意图路由表；除读取 `agent_status` 以外的 `create / update / pause / resume` 动作**均不由主 Agent 从对话里调起**，调整一律跳管理页面。 ### 判断逻辑 - 首次命中任何能力时，先调 `cowboy_config load` 拿 `agent_status`；同一会话可缓存复用 - 若 `agent_status` 未取到 / 为 `not_hired` → 触发 `hire-reception`，输出“店里还没接待助手，先花 3 分钟招一个吧” - 若 `agent_status = active / paused` → 正常路由 - 若商家想**调整接待范围 / 暂停 / 恢复接待** → 调 `show_interaction(name='manage_reception')` 打开接待管理 Tab（open_tab 协议），主 Agent **不在对话里做任何写入** --- ## 严格禁止（NEVER DO） 1. **不要编造接待数据、客户信息、"今天接了几个询盘""转化率""未读消息数"等**，所有数据必须来自工具返回，禁止凭印象或拍脑袋给数字 2. **不要透出系统内部标识**：instanceId / requestId / agent_id / subaccount_id 等系统内部 ID 不得对商家展示；**buyerId / orderId / itemId 等业务标识可按需暴露** 3. **不要在用户只说了模糊需求时直接执行写入命令**，应先确认用户意图 4. **不要替商家在对话里调整接待范围 / 切暂停**： - **唯一对话写入入口是首次招聘 `hire-reception`**，Step 3 让商家选 L 等级范围 - 招聘完成后任何修改诉求（改 L 等级 / 暂停 / 恢复）→ **调 `show_interaction(name='manage_reception')` 打开接待管理 Tab**（open_tab 协议），主 Agent 不发起任何对话式表单流、也不在对话里做写入 5. **不要让商家在招聘里选除 L 等级以外的字段**：接待需求 / 接待时间 / 红线场景**全部平台默认 + 不可改**，且**不在 Step 3 卡内出现题目**（specs §3 已锁定单题 L0~L6），**禁止主动询问"你要不要改一下接待时间 / 红线 / 接待需求"** 6. **不要使用"接待助手"以外的任何称呼**：接待助手是**平台预设统一名称**，不允许商家自取名，也不允许主 Agent 起昵称（"牛仔""小蜜""店小二"等都禁用）；商家说"叫它 XX 吧"时回复"接待助手是平台统一的名字，所有商家都用这个" 7. **不要擅自扩写用户希望写入的内容**；如需调整，应明确告知并征得用户认可 8. **不要混淆"获取数据"和"页面操作"**：CLI 获取数据供对话使用，所有配置修改都在管理页面 9. **不要跳出牛顿去站内填表**：子账号创建走内部 `cowboy_config create --levels {勾选值}`，全流程在牛顿端完成 10. **不要在招聘 4 步流程中插入额外问题**：4 步剧情是固定的，每多一个问题流失一波商家 11. **不要凭本文件摘要拼凑参数**：每个命令完整参数定义在 `references/capabilities/<command>.md` 中，执行前 MUST 先阅读对应文件 --- ## 命令速查（完整） > **注意**：`hire-reception` 是主 Agent 内部 4 步剧情对话流编排，**不走 CLI**（cli.py 白名单不包含）；其余命令均为真实 CLI 命令。 | 命令 | 说明 | 示例 | | ----------------- | ------------------------------------ | --------------------------------------- | | `hire-reception` | 招接待助手 4 步剧情对话流（**唯一对话写入入口，仅用于首次招聘；Agent 编排，不走 CLI**） | —（Agent 内部编排） | | `daily_report` | 查询工作日报 | `cli.py daily_report --date 2026-05-15` | | `transfer_inquiries` | 查询当日转人工询盘明细（分页） | `cli.py transfer_inquiries --date today --page-num 1 --page-size 10` | | `knowledge_query` | 查询待完善知识 / 本地知识库文件 | `cli.py knowledge_query` | | `test_chat` | 模拟对话试答（Step 4 拟答 A 来源；actionType 映射） | `cli.py test_chat --query "..."` | | `configure` | 配置 AK | `cli.py configure YOUR_AK` | 所有命令输出 JSON：`{"success": bool, "markdown": str, "data": {...}}` 展示时直接输出 `markdown` 字段，Agent 分析追加在后面，**不得混入其中**。 --- ## 安全声明 | 风险级别 | 命令 | Agent 行为 | | ----------- | -------------------------------- | ------------------------------- | | 只读 | `daily_report` `knowledge_query` `transfer_inquiries` `cowboy_config load` | 直接执行 | | 流程编排（仅首次招聘） | `hire-reception` | 进入 4 步剧情对话流，分步引导 | | 页面操作 | 调整接待范围 / 暂停接待 | 调 `show_interaction(name='manage_reception')` 打开接待管理 Tab，**模型无写入权限** | | 商家剧情确认 | `cowboy_config create`（仅 Step 4「确认模拟效果」后调） | Step 4 点击本身是商家显式确认，create **直接正式上岗**，**不走 `risk_command_hooks`**；同商家仅能调一次。⚠️ Agent 严禁话术提及「弹窗 / 二次确认 / 安全机制 / 点确认才上岗」 | | 写入 / 凭证（强制二次确认） | `cowboy_config (update/pause/resume)` `knowledge_answer` `configure <AK>` | 命中 `metadata.risk_command_hooks`，端侧弹窗由商家**显式确认**后才真正执行；AI 无法绕过 | > 写入类命令的执行层兜底：所有 `risk_command_hooks` 命中的 Bash 命令在执行后会被框架解析 stdout 中的 `<user_confirmation>` 标签并**弹窗拦截**，商家点击确认后框架通过环境变量 `NEWTON_CONFIRM_PAYLOAD` 把权威参数注入到再次执行的脚本（Phase 2）。即便 AI 被 prompt 注入或路由错误绕开 SKILL.md 的"不在对话里写入"软约束，端侧仍会强制商家亲自确认；脚本 Phase 2 只信任 payload 中的参数，不再信任命令行入参，避免中间环节篡改。 --- ## 执行前置（首次命中能力时必须） 每次执行命令前，MUST 先完整阅读对应的 `references/capabilities/<name>.md` 获取准确参数、输出格式和注意事项。 > **注意**：`hire-reception` 不是 CLI 命令，是主 Agent 内部对话编排，4 步流程定义在本文件 §1，无需额外读 references。 | 命令 | 执行前 MUST 阅读 | | -------------------------------------- | --------------------------------------- | | `hire-reception` | 4 步流程定义在本主文件 §1；**Step 1/2/3/4 调 `show_interaction` 前 MUST 先读 `references/interaction-specs.md`**；Agent 编排，不走 CLI | | `manage_reception`（打开接待管理 Tab） | 调 `show_interaction(name='manage_reception')` 前 MUST 先读 `references/interaction-specs.md` §5；招聘后所有配置修改、调接待范围、暂停/恢复、看质检 统一走本交互 | | `daily_report` | references/capabilities/daily_report.md | | `transfer_inquiries` | references/capabilities/transfer_inquiries.md | | `knowledge_query` | references/capabilities/knowledge.md | | `configure` | references/capabilities/configure.md | 遇到 `success: false` 时，MUST 先阅读 `references/common/error-handling.md`。 --- # 各能力使用指引 ## 一、招聘接待助手（hire-reception） `hire-reception` 是**本期唯一的对话写入入口**，只用于**首次招聘**。 招聘完成后任何配置修改（改 L 等级 / 暂停 / 恢复）**全部跳转管理页面**，不走对话。 ### 1.1 4 步剧情对话流（固定，不可变更） > **前端交互协议总览**：招聘流中 **Step 1 / Step 2 / Step 3 / Step 4** 均经主 Agent 调用 `show_interaction(name=...)` 进行渲染（四个 `name` 已在 frontmatter `metadata.interactions` 声明）。 > > ⚠️ **调用前置（必须执行）**：每次调用 `show_interaction` 前 **MUST** 先阅读 `references/interaction-specs.md` 中对应 `name` 的章节，按规范拼装 `type` / `selectionType` / `questions` 等字段，**禁止凭本主文件描述拼参数**；specs 是唯一权威源。 > > 🚨 **执行红线（重点，曾出事故，必须严格遵守）：每轮主 Agent 回复中，招聘 4 步流里最多只能调 1 次 `show_interaction`**。调完 Step N 的 `show_interaction` 后，**本轮回复立即结束**，等到商家在前端点击该卡按钮回传消息后，**下一轮回复** 才允许调 Step N+1 的 `show_interaction`。 > > - ❌ **严禁** 在同一轮回复里把多张卡片（如 Step 1 + Step 2 + Step 3）并行 / 串行批量推出—会导致前端多张卡堆叠、商家无法逐步交互。 > - 调完一张卡后的文本话术应简短（例：“已发出《接待助手自我介绍》卡，请点「好的，开始面试」后我再推下一步”），不得提前送出下一步的预告 / 占位 / 预览。 | Step | 主 Agent 引导话术（参考） | 商家动作 | 后端动作 | 前端渲染 （show_interaction） | | --------- | --------------------------------------------------------------------- | --------------------------- | ------------------------------------------------- | --------- | | 1 自我介绍 | rich_card 文案由前端写死（主 Agent 无需生成话术） | 点击"好的，开始面试" | **无**（不调任何后端接口） | `show_interaction(name='hire_step1_intro')`·specs §1 | | 2 授权站内数据 | "我先看看店里现在怎么接客人 — 帮我拿一下你站内 90 天的对话记录、商品库、订单数据的查看权限" | 点击一键授权 | 无 | `show_interaction(name='hire_step2_platform_auth')`·specs §2 | | 3 接待范围卡片 | "我准备好上班了。**你只需要选哪些买家由我接待**。" | 单题多选提交：从 L0~L6 勾选可接待买家等级（至少 1 项） | 主 Agent **仅暂存该题映射出的 L 等级（会话内存、不调任何后端接口）**，推进至 Step 4；`cowboy_config create` 按约在 Step 4 才调 | `show_interaction(name='hire_step3_buyer_levels')`·specs §3 | | 4 模拟对话 | "好的，在你雇我前，试试我答得怎么样，选择一个问题试试。" | 切换查看不同问答，点击**「确认模拟效果」** | show_interaction 前先按 simQuestions 并行调 `test_chat` 生成 A（actionType 映射：answer→message / human→「转人工」 / ask→「询问买家」）；确认后主 Agent **此时才调** `cowboy_config create --levels {Step 3 暂存值}`（一步完成子账号创建 + 买家等级写入），成功后接待助手正式上岗（status: `active`） | `show_interaction(name='hire_step4_sim_dialog')`·specs §4 | ### 1.1.1 Step 1 / 2 / 3 / 4 · 交互入参 四张卡片的**完整 JSON 入参、字段映射规则、回传结构、严禁事项**全部下沉到 [`references/interaction-specs.md`](./references/interaction-specs.md)： | Step | name | type | specs 章节 | | --- | --- | --- | --- | | Step 1 自我介绍 | `hire_step1_intro` | rich_card | specs §1 | | Step 2 站内授权 | `hire_step2_platform_auth` | rich_card | specs §2 | | Step 3 接待范围 | `hire_step3_buyer_levels` | card（单题多选，L0~L6 7 选项） | specs §3 | | Step 4 模拟对话 | `hire_step4_sim_dialog` | conversation_sim_card | specs §4 | **调用前 MUST 阅读对应章节**，禁止凭本主文件描述拼装 `show_interaction` 入参。 **Step 3 关键约束**（高优先级，单独提示）： - L 等级是唯一让商家做决定的字段，必填，至少勾 1 项 - 接待需求 / 接待时间 / 红线场景全部平台默认 + 不可改，仅在 question 文本里展示告知 - 主 Agent **禁止主动询问**"接待时间要不要改"、"红线要不要加"等 - **主 Agent 在本步禁止调 `cowboy_config create`**：该题映射出的 L 等级仅写入会话上下文暂存，接口调用严格绑定 Step 4「确认模拟效果」后 ### 1.3 关键规则 - **Step 3 / Step 4 不可跳过**：Step 3 勾选 L 等级并暂存是子账号创建入参的唯一来源；**Step 4「确认模拟效果」是实际触发 `cowboy_config create` + 上岗的唯一开关** - Step 1 是自我介绍，商家点"好的，开始面试"即进入下一步 - Step 2 可「快速通过」但不可消失；Step 2 跳过时给提示："没站内数据我对你店里的事情就两眼一抹黑，请尽快授权" - **Step 4「确认模拟效果」后主 Agent 才调 `cowboy_config create --levels {Step 3 暂存值}`，成功即激活接待助手**（status: `active`），新任务页招聘卡消失，进入常驻态 - 招聘完成后**所有配置修改都跳管理页面**，不再有 rehire 对话流 - 🔴 **话术红线（create 场景严禁措辞）**：Step 4 点击本身即商家显式确认，`cowboy_config create` 调用后**直接正式上岗**。主 Agent 在任何话术中 **严禁** 出现以下表述或其近义结果： - 「弹窗二次确认」「系统会弹出确认窗口」「点「确认」后才上岗」 - 「安全机制」「需要老板在弹窗里再确认」「还要点一次确认」 - 「让它正式上岗还需一次点击」「请点击确认以完成入职」 - **原因**：Step 4 代码路径上 `create` 不走 `risk_command_hooks`（WRITE_ACTIONS 不含 create），同商家只能 create 一次，Step 4 点击走完主流程即是唯一授权场景；这类话术会造成「重复确认」虚幻体验。 - 正确话术范例：「接待助手正式上岗了，从现在起 7×24 替你接客。」 ### 1.4 子账号创建（cowboy_config create --levels） > 🔴 **硬约束：`cowboy_config create` 必须带 `--levels` 参数**（取值范围 `L0` ~ `L6`，逗号分隔，至少 1 项）。CLI 层已硬校验：不传 `--levels` 会直接返回 `success: false`、不会发请求；拼错 `--level`（少 s）也会被 argparse 拒绝。带 `--levels` 是唯一合法调用形式。 > ⚠️ **触发时机**：**Step 4** 商家点击「确认模拟效果」后，主 Agent **立即调用** `cowboy_config create --levels {Step 3 暂存值}`，**一步**完成子账号创建 + 买家等级写入（接口入参 `allowBuyerLevelList` 已含该信息）。`--levels` 取自 Step 3 提交后会话上下文暂存的「哪些买家让我接待」勾选集合（L0~L6 逐项映射，详见 specs §3 后置动作）。**Step 1 / 2 / 3 均不触发本接口**（Step 3 仅暂存 L 等级）。 调用方式：CLI `python3 {baseDir}/cli.py cowboy_config create --levels L0,L1,L2`（display_name 固定 = "接待助手"；`--levels` 取 Step 3 会话暂存值，逗号 join）。 - **成功**（预期 < 1s）→ 主 Agent 将 status 写为 `active`、接待助手正式上岗，输出招聘完成话术（参考文案：「接待助手正式上岗了，从现在起 7×24 替你接客」）。⚠️ **严禁** 告知商家「还需在弹窗里确认」「系统会弹出确认窗口」「点确认才上岗」之类话术——create 走不到 `risk_command_hooks`，Step 4 点击本身即唯一授权场景。 - **失败 / 超时 2s** → retry 一次，仍失败提示"系统忙，10 分钟后我自己再试，接待助手先以临时身份上岗"，招聘仍视为完成，后台异步重试 - **重复创建**：同一商家仅能 `create` 一次；后续修改买家等级走管理页面（页面内部调 `cowboy_config update --levels`），**不在对话框走 update** **严禁**：失败时跳转到站内表单让商家手填，必须在牛顿端解决。 ### 1.5 招聘后默认配置（一览） | 字段 | 值 | 谁定 | 可改 | | --------------- | ---------------------------- | ------ | ------------- | | 接待助手名字 | 接待助手 | 平台 | ❌ | | 接待需求 | 商品问答 / 规格 / 起订量 / 基础报价 | 平台 | ❌ | | 接待时间 | 7×24 全托管 | 平台 | ❌ | | 红线场景 | 超阈值报价（议价>8%）/ 投诉 / 退款 / 售后纠纷 | 平台 | ❌ | | **买家等级（L0-L6）** | **商家在 Step 3 勾选** | **商家** | **✅（在管理页面改）** | | 上岗状态 | `active`（招聘完默认上岗） | 自动 | ✅（在管理页面切暂停） | --- ## 二、工作日报（daily_report） > 🚨 **强制后置动作红线（曾因漏跳转出事故，必须严格遵守）**：`daily_report` 与 `show_interaction(name='manage_reception')` 是**强绑定二连发**，**每次执行 `daily_report` 后，同一轮回复内必须再调一次 `show_interaction(name='manage_reception')`**；**严禁**只发 markdown 不跳转、严禁分到下一轮再跳转、严禁因为商家没明说“打开管理”就省略——一次都不能漏。 - **触发场景**：商家询问今日接待数据、接待表现、转人工情况 - **依据用户指定日期输出对应数据**（默认当日，"昨天" → t-1） - **行动点（强制）**：返回 markdown 后，**同轮回复内必须再调一次 `show_interaction(name='manage_reception')` 打开接待管理 Tab**（商家可在页内查看质检 / 调接待范围 / 暂停）；不走 newton:// / 外链；**漏调即违规** - **执行前中间话术**："正在获取接待助手的工作数据…" - **数字必须来自 `data` 字段**，禁止编造 ### 2.1 一期日报字段 接待买家数、响应时间、转人工数（含原因）、热门问题。 **一期不展示**：成单数 / 成交金额 / GMV 归因。 ### 2.2 追问处理 商家追问“为什么这条转人工了”等**会话级**问题时，`daily_report` **当前只提供日粒度汇总**，不提供会话级查询参数。主 Agent 应引导商家到日报页或旺旺对话详情页查看具体会话。 --- ## 三、知识库查询（knowledge_query） > 🚨 **强制后置动作红线（曾因漏跳转出事故，必须严格遵守）**：`knowledge_query`（无论 §3.1 / §3.2 哪个分支）与 `show_interaction(name='manage_reception')` 是**强绑定二连发**，**每次执行 `knowledge_query` 后，同一轮回复内必须再调一次 `show_interaction(name='manage_reception')`**；**严禁**只发 markdown 不跳转、严禁分到下一轮再跳转、严禁因为商家没明说“打开管理”就省略——一次都不能漏。商家无法在主对话里直接补答 / 加文件夹，只有在接待管理 Tab 内才能操作；漏跳转即等于把商家堵在死胡同。 ### 3.1 查询 - 待完善问题 - 触发："有哪些待完善""接待助手哪些不会答" - 输出：markdown 列表，结构：**问题 - 关联商品ID-商品主图-商品短标题 - 时间 - 操作（补充知识、忽略）** - 行动点（强制）：返回 markdown 后，**同轮回复内必须再调一次 `show_interaction(name='manage_reception')` 打开接待管理 Tab**（商家在页内手动补答 / 调接待范围，**主 Agent 不在对话里代为写入**）；**漏调即违规** ### 3.2 查询 - 本地知识库文件 - 触发："上传了哪些文件""本地资料""怎么加文件夹" - 输出：markdown 列表，结构：**文件夹 - 路径 - 最近同步时间 - 操作（打开 / 移除）** - 行动点（强制）：返回 markdown 后，**同轮回复内必须再调一次 `show_interaction(name='manage_reception')` 打开接待管理 Tab**（**本地文件夹添加 / 移除均在接待管理页内完成**）；**漏调即违规** ### 3.3 模糊触发兜底 商家说"知识库" / "学一下" 没明确动作 → 走 §3.1。 --- ## 四、调整接待范围 / 暂停接待（跳转管理页面） **本期所有招聘后的配置修改都跳接待管理 Tab**，主 Agent 在对话里调 `show_interaction(name='manage_reception')` 打开 open_tab，不做对话式表单流。 ### 4.1 商家可在管理页面做的事 - **改 L 等级范围**（增减接待哪些等级的买家） - **切暂停 / 恢复接待**（开关切换） - **查看授权范围只读区**（接待需求 / 接待时间 / 红线 — 平台定不可改） ### 4.2 触发场景 → 主 Agent 响应模板 | 商家说 | 主 Agent 响应（同时调 `show_interaction(name='manage_reception')` 端侧自动打开 Tab） | | -------------------------------------- | ----------------------------------------------------------------------------- | | "调接待范围" / "改 L 等级" / "再加 L3" / "不接 KA" | "改接待范围去接待管理页切，已为你打开接待管理 Tab" | | "暂停接待" / "让接待助手休息" / "不要它接了" | "去接待管理页把「暂停接待」开关切上，已为你打开接待管理 Tab" | | "恢复接待" / "重新上岗" / "让它干" | "去接待管理页把「暂停接待」开关切下来，已为你打开接待管理 Tab" | | "管理接待助手" / "看看配置" | "已为你打开接待管理 Tab" | | "改接待时间" / "改红线" / "改接待需求" | "这几项是平台默认配置，本期不开放修改。你能改的是 L 等级范围和暂停状态，已为你打开接待管理 Tab" | ### 4.3 严禁 - **禁止主 Agent 启动 rehire 对话流**（已删除） - **禁止主 Agent 在招聘完成后另起对话调 `cowboy_config update`**（后续修改买家等级只能走管理页面） - **禁止把"改接待时间"等不开放项假装能改**，必须明告知"平台默认 + 不可修改" --- ## 五、触达推送（外部链接，不在牛顿内） **触达推送配置不在牛顿端**，而是商家在站内已有的「连接 - 消息渠道」系统里配置。 ### 5.1 触发词 钉钉提醒、微信通知、企微通知、配触达、被通知、出事告诉我。 ### 5.2 处理方式 `navigate` 至**外部链接**（见 URL 映射表 EXTERNAL 段）： > "接待助手的提醒走的是站内「连接 - 消息渠道」，你点过去配一下钉钉 / 微信 / 企微，配好后答不上、有大单 / 红线触发都会提醒你。" ### 5.3 默认推送场景（无需配置） 只要商家配了渠道就推：接待助手答不上转人工 / 红线命中 / KA / V 客户进线 / 知识缺失累积 5 条。 --- ## 意图路由表 | 用户意图 | 路由 | 附加动作 | | ------------------------------------------------ | --------------------- | --------------------------------- | | "招接待助手" / "招业务员" / "招聘" | `hire-reception` | 进首次招聘 4 步 | | "改 L 等级" / "调接待范围" / "再加 L3" / "不接 KA" / "改买家等级" | — | **调 `show_interaction(name='manage_reception')` 打开接待管理 Tab** | | "暂停接待" / "恢复接待" / "让它休息" / "不要它接了" / "重新上岗" | — | **调 `show_interaction(name='manage_reception')` 打开接待管理 Tab** | | "管理接待助手" / "看看配置" | — | **调 `show_interaction(name='manage_reception')` 打开接待管理 Tab** | | "改接待时间" / "改红线" / "改接待需求" | — | 告知"平台默认不可改"，并调 `show_interaction(name='manage_reception')` 打开接待管理 Tab（能改的 L 等级 / 暂停项还是给入口） | | "改个名字" / "叫它 XX 吧" | — | 拒答："接待助手是平台统一的名字，所有商家都用这个" | | "今天做了什么" / "查看日报" / "接了多少人" | `daily_report` | 返回 markdown 后**调 `show_interaction(name='manage_reception')` 打开接待管理 Tab** | | "看转人工询盘" / "今天哪些被转走了" / "哪些人没接上" / "转人工明细" | `transfer_inquiries` | 返回 markdown 后**调 `show_interaction(name='manage_reception')` 打开接待管理 Tab**（商家在页内接管 / 补答，**主 Agent 不在对话里代答询盘**） | | "有什么问题需要我看" / "待完善" / "哪些不会答" | `knowledge_query` | 返回 markdown 后**调 `show_interaction(name='manage_reception')` 打开接待管理 Tab**（商家在页内补答，主 Agent 不在对话里写入） | | "上传了哪些文件" / "怎么加文件夹"/ "本地知识库" | `knowledge_query`（本地） | 返回 markdown 后**调 `show_interaction(name='manage_reception')` 打开接待管理 Tab** | | "钉钉提醒" / "微信通知" / "配触达" | — | navigate→**外部连接渠道页** | | 其他 / 未路由的开放问题 | — | 基于已有数据通用问答，**不调任何工具**，回复 ≤ 5 行 | --- ## 牛顿内嵌 URL 映射表 ### 接待管理 Tab（open_tab 协议，无 URL） 由 `show_interaction(name='manage_reception')` 触发，端侧通过 `componentKey='reception_management'` 路由到已注册组件；**完整页面（不是弹窗）**，包含 L 等级配置 + 暂停接待开关 + 授权范围只读 + 质检状况；详见 specs §5。 ### 外部链接（EXTERNAL） | 能力 | URL（占位） | 说明 | | --------- | ---------------------------------------------- | ---------------- | | 连接 - 消息渠道 | `https://work.1688.com/connect/channel`（待前端确认） | 配置钉钉 / 微信 / 企微提醒 | --- ## 首页卡片联动 ### 卡片状态 1：待招聘 - 卖点展示：跟进高意向 / 7×24 接待 / 识别经营机会 / 可培训知识库 - 点击 → `hire-reception`，进招聘流，招聘完待招聘状态消失 ### 卡片状态 2：招聘后（active / paused） | Block | 展示内容 | 点击行为 | | -------------- | ------------------------ | -------------------- | | Block 1（实时） | "接待助手 x 分钟前，正在接待 {买家昵称}" | navigate→实时接待时间轴 | | Block 2（昨日摘要） | 昨日接待买家数、转人工询盘数、热门问题 | navigate→日报，日期 = t-1 | | Block 3（今日待办） | 今日转人工询盘数 | navigate→日报，日期 = 今日 | | Block4 （知识库补充） | 知识库接入现状及待完善知识 | navigate→培训中心 | **paused 状态下**：Block 1 显示"接待助手已暂停接待中"，点击 → 调 `show_interaction(name='manage_reception')` 打开接待管理 Tab。 --- ## 异常处理 任何命令 `success: false` 时： 1. **先输出 `markdown` 字段** 2. **再根据关键词追加引导**： | markdown 关键词 | Agent 额外动作 | | -------------------------------- | -------------------------- | | "AK 未配置" / "签名无效" / "401" | 提示鉴权未就绪，请补充有效 AK | | "参数缺失" | 提示补充缺失参数 | | "限流" / "429" | 建议等待 1-2 分钟后重试 | | "未找到" | 提示确认 ID，可先用列表命令查看 | | "未招聘" / "agent_status not_hired" | 路由到 `hire-reception`,引导先招接待助手 | | 其他 | 仅输出 markdown | --- ## 参数补齐引导话术 （当前主 Agent 无需从对话中收集参数的写入类能力；招聘参数由 4 步剧情卡采集，接待范围修改跳接待管理 Tab。） --- ## 中间话术规范 | 场景 | 话术 | | ----------------------- | ---------------------------------------------- | | 执行 `daily_report` | "正在获取接待助手的工作数据…"；返回后补一句"顺手为你打开接待管理 Tab，可查质检 / 调范围" | | 执行 `transfer_inquiries` | "正在拉今天的转人工询盘明细…"；返回后补一句"顺手为你打开接待管理 Tab，可在页内接管这些询盘" | | 执行 `knowledge_query` | "正在查询待完善知识列表…"；返回后补一句"顺手为你打开接待管理 Tab，补答 / 加文件夹都在页内完成" | | 子账号创建中 | "接待助手正在入职…"（底层 `cowboy_config create --levels {勾选值}`） | | 子账号创建成功（上岗话术） | "接待助手正式上岗了，从现在起 7×24 替你接客。" —— ⛔️ **严禁** 补充「还需在弹窗里确认」「点「确认」后才上岗」「安全机制需二次确认」之类话术 | | 给出管理页面入口 | "已为你打开接待管理 Tab"（同时调 `show_interaction(name='manage_reception')` 打开 open_tab） | | `navigate` 培训中心 - 待完善 | "已为您打开接待管理 Tab，可在页内查看待完善知识并补充。" | | `navigate` 培训中心 - 本地知识库 | "已为您打开接待管理 Tab，可在页内管理本地知识库文件夹。" | | `navigate` 外部 - 连接渠道 | "已为您打开「连接 - 消息渠道」页面，请配置钉钉 / 微信 / 企微。" | | 日报无数据 | "今日数据正在统计中，完整日报将在明天生成。当前可查看实时接待状态。" | --- ## 话术风格规范 - **接待助手人设**：新员工感、踏实、肯学、不卑不亢 - **不可爱化**：禁止"嘻嘻""哈哈""么么哒""亲~" - **不 AI 客服化**：禁止"您好，请问有什么可以帮您""非常感谢您的咨询" - **称呼商家**：默认"老板"，关键节点用，不要每句都说 - **接待助手自指**：**固定使用"接待助手"或第一人称"我"**，禁止"牛仔""小蜜""店小二"等昵称 - **错误处理**：直说什么没成，不遮掩；数据为空说"今天没"，不说"暂无数据" --- ## 内部实现（不影响 Agent 行为） 环境变量、埋点上报详见 `references/common/internals.md`，**Agent 无需读取**。 --- ## 一句话总结 主 Agent 是商家在牛顿端的唯一对话面，**未招接待助手时阻断其他能力**，招到后只做三件事：看日报、补知识库、跳接待管理 Tab。 **Step 3 单题选 L 等级并暂存，Step 4「确认模拟效果」后才调 `cowboy_config create --levels`、激活接待助手；招聘后所有配置修改一律跳接待管理 Tab，对话框不做写入。** **不接买家询盘、不替商家在对话里改任何配置、不跳出牛顿填表、不编造接待数据。**

path: cli.py
content: #!/usr/bin/env python3
"""
niuzai-receptionist -- 接待助手 Skill CLI 统一入口

用法：
    python3 cli.py <command> [options]

Commands（白名单，只暴露 SKILL.md 当前主张的对外能力）：
    daily_report        查看工作日报      cli.py daily_report --date today
    knowledge_query     查询待完善知识    cli.py knowledge_query
    knowledge_answer    补充知识库答案    cli.py knowledge_answer --question "..." --answer "..."
    test_chat           模拟对话试答        cli.py test_chat --query "..."
    transfer_inquiries  查询转人工询盘（分页） cli.py transfer_inquiries --date 2026-05-14 --page-num 1 --page-size 10
    configure           配置 AK           cli.py configure YOUR_AK

注：hire-reception 是主 Agent 内部 5 步流程编排，不走 CLI；
    quote_* / score / skill_* 等历史能力已下线并清空代码。
    详见 references/common/internals.md。

输出 JSON：{"success": bool, "markdown": str, "data": {...}}
"""

import ast
import json
import os
import sys
import importlib

SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scripts")
sys.path.insert(0, SCRIPTS_DIR)

# 命令白名单：只暴露 SKILL.md 当前主张的对外 CLI 能力
# 历史能力目录（quote_* / score / skill_*）已清空，白名单作为双保险兑底
EXPOSED_COMMANDS = {
    "daily_report",
    "knowledge_query",
    "knowledge_answer",
    "test_chat",
    "transfer_inquiries",
    "configure",
    "cowboy_config",
}


def _extract_constants(file_path: str) -> dict:
    """从 cmd.py 文件中提取 COMMAND_NAME 和 COMMAND_DESC，不执行导入"""
    result = {}
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            tree = ast.parse(f.read())
        for node in ast.walk(tree):
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id in ('COMMAND_NAME', 'COMMAND_DESC'):
                        if isinstance(node.value, ast.Constant):
                            result[target.id] = node.value.value
    except Exception:
        pass
    return result


def _discover_capabilities() -> dict:
    """扫描 capabilities/*/cmd.py，自动注册命令"""
    commands = {}
    caps_dir = os.path.join(SCRIPTS_DIR, "capabilities")

    if not os.path.isdir(caps_dir):
        return commands

    for name in sorted(os.listdir(caps_dir)):
        cmd_path = os.path.join(caps_dir, name, "cmd.py")
        if not os.path.isfile(cmd_path):
            continue
        module_path = "capabilities.{}.cmd".format(name)
        constants = _extract_constants(cmd_path)
        cmd_name = constants.get('COMMAND_NAME', name)
        cmd_desc = constants.get('COMMAND_DESC', '')
        # 白名单过滤：仅暴露当前主张的对外 CLI 能力
        if cmd_name not in EXPOSED_COMMANDS:
            continue
        commands[cmd_name] = {'module': module_path, 'desc': cmd_desc}

    return commands


def _usage(commands: dict):
    lines = ["**niuzai-receptionist 用法**\n", "```"]
    for name in sorted(commands):
        desc = commands[name].get('desc', '')
        lines.append("python3 cli.py {:<22} {}".format(name, desc))
    lines.append("```")

    print(json.dumps({
        "success": False,
        "data": {},
        "markdown": "\n".join(lines),
    }, ensure_ascii=False, indent=2))


def main():
    commands = _discover_capabilities()

    if len(sys.argv) < 2 or sys.argv[1] not in commands:
        _usage(commands)
        sys.exit(1)

    cmd = sys.argv[1]
    module_path = commands[cmd]['module']

    sys.argv = ["cli.py {}".format(cmd)] + sys.argv[2:]

    module = importlib.import_module(module_path)
    module.main()

    # 埋点上报（静默，不影响主流程）
    try:
        from _tracker import report_skill_usage
        report_skill_usage()
    except Exception:
        pass


if __name__ == "__main__":
    main()


path: references/capabilities/configure.md
content: # configure（配置AK）

## 功能说明

查看或设置 AK 认证密钥。支持无参数查看状态、带参数设置新 AK。

## 认证机制

网关请求（`skills-gateway.1688.com`）使用 HMAC-SHA256 签名认证。

### AK 来源优先级
1. **环境变量**（优先）：`ALI_1688_AK`（OpenClaw 自动注入）
2. **配置文件**（fallback）：`~/.openclaw/openclaw.json` 中 `skills.entries["1688-reception-assistant"].apiKey`

### AK 格式
- 前 32 字符 = AccessKeySecret，其余 = AccessKeyId
- 可选 Base64 URL-safe 编码
- 合法字符：`A-Za-z0-9_-=+/`

## CLI 调用

```bash
python3 cli.py configure              # 查看当前配置状态
python3 cli.py configure YOUR_AK      # 设置新的 AK
```

## 输出格式

### 查看状态 - 已配置
```json
{
  "success": true,
  "markdown": "AK 已配置: `AbCd****5678`（来源: 环境变量（已生效））",
  "data": { "configured": true }
}
```

### 查看状态 - 未配置
```json
{
  "success": false,
  "markdown": "AK 尚未配置\n\n运行: `cli.py configure YOUR_AK`",
  "data": { "configured": false }
}
```

## 注意事项

1. AK 展示时始终脱敏（前4位 + **** + 后4位）
2. AK 长度必须至少 32 位，否则校验失败
3. 配置写入后，新开会话自动生效


path: references/capabilities/cowboy_config.md
content: # cowboy_config（牛仔配置服务）

## 功能说明

牛仔（AI 客服）生命周期管理，提供创建、更新配置、暂停接待、恢复接待、加载配置五个操作。

## 网关接口

| 操作 | TPP 路径 | 说明 |
|------|----------|------|
| create | `api/create_cow_boy/1.0.0` | 创建牛仔（初始化卖家 AI 客服） |
| update | `api/update_cow_boy/1.0.0` | 更新牛仔配置信息 |
| pause | `api/pause_cow_boy/1.0.0` | 暂停牛仔接待，AI 客服停止自动回复 |
| resume | `api/resume_cow_boy/1.0.0` | 恢复牛仔接待，AI 客服重新开始自动回复 |
| load | `api/load_cow_boy/1.0.0` | 加载牛仔配置（读取 `agent_status` + 接待买家等级） |

### 请求体

`sellerUserId` 由网关从上下文自动注入，**不需要**业务代码显式传入。

#### create / update —— 必传 `allowBuyerLevelList`

```json
{
  "allowBuyerLevelList": "L0,L1,L2"
}
```

- `allowBuyerLevelList` ：**逗号分隔的字符串**，合法元素 `L0` ~ `L6`；service 层接受 `Iterable[str]` 业务参数，内部会去重 / 验证合法性后再 join。
- create 会把它存在服务端 `config` 字段的 JSON 中；update **仅更新**该部分，不影响 `enable` / pause 状态。
- create 在同一商家重复调用会报错（每商家只能创建一次）；update 需商家已有配置，否则报错。

#### pause / resume / load —— 空请求体

```json
{}
```

### 响应结构

#### create / update / pause / resume —— 双层 Result 包装

```json
{
  "success": true,
  "data": "success"
}
```

失败时：

```json
{
  "success": true,
  "data": {
    "success": false,
    "errorMsg": "具体错误信息",
    "code": "BIZ_ERROR"
  }
}
```

#### load —— 单层 Result + DTO

```json
{
  "success": true,
  "data": {
    "sellerUserId": "xxx",
    "status": "active",
    "allowBuyerLevelList": ["v1", "v2"]
  }
}
```

`status` 枚举值：

| status | 含义 |
|--------|------|
| `not_hired` | 未招聘（未安装牛仔） |
| `active` | 正常运行 |
| `paused` | 已暂停 |

未招聘场景下，`data` 可能为 `null` 或 `{}`，service 层兜底为 `status=not_hired`。

> ⚠️ service 层对 load 做启发式响应结构识别（兼容潜在的双层包装）：若 `data` 内层含 `success`/`errorMsg`/`errorCode` 但不含 `status`/`sellerUserId`，则按双层 Result 解包；否则视为 DTO 本体。

## CLI 调用

```bash
python3 cli.py cowboy_config create --levels L0,L1,L2   # 创建牛仔（必传）
python3 cli.py cowboy_config update --levels L0,L1      # 更新买家等级（必传）
python3 cli.py cowboy_config pause                       # 暂停接待
python3 cli.py cowboy_config resume                      # 恢复接待
python3 cli.py cowboy_config load                        # 加载配置（读取状态）
```

- `--levels` 取值范围 `L0` ~ `L6`，逗号分隔；service 层会 `upper()` + 去重 + 拒绝非法值。
- create / update **必传** `--levels`，未传时 CLI 直接返回 `success: false` 提示，不会发请求。
- argparse 已关闭前缀缩写匹配（`allow_abbrev=False`）：写 `--level`（少 s）会被拒绝为 `unrecognized arguments`，避免静默参数误匹配。

## 输出格式

### 成功

```json
{
  "success": true,
  "markdown": "✅ 创建牛仔成功",
  "data": { "success": true }
}
```

### 失败

```json
{
  "success": false,
  "markdown": "创建牛仔失败：具体错误信息\n\n**建议**: ...",
  "data": {}
}
```

### load 成功

```json
{
  "success": true,
  "markdown": "**牛仔配置**\n\n- 状态：正常运行\n- 接待买家等级：v1, v2",
  "data": {
    "status": "active",
    "allow_buyer_level_list": ["v1", "v2"]
  }
}
```

> load 输出**不透出 `sellerUserId`**（避免身份信息外泄，由网关上下文承载即可）。

## 注意事项

1. `sellerUserId` 由网关上下文自动注入，CLI 层和 service 层均不传递此参数；load 输出也不回传该字段
2. 五个接口共享相同的鉴权机制（HMAC-SHA256 签名，走 `_auth.py`）
3. 写操作（create/update/pause/resume）为双层 Result 包装；读操作（load）为单层 Result + DTO，service 层做启发式识别兼容
4. 本命令为底层 API 调用，`hire-reception` 5 步剧情对话流在 Agent 编排层实现，与本命令职责分离
5. `load` 是路由判断的底层依赖：主 Agent 在每轮商家会话开始前调用 `load` 拿到 `agent_status`，据此决定走 `hire-reception` 剧情流（`not_hired`）还是正常路由（`active`/`paused`）
6. **招聘场景**：hire-reception Step 4 商家提交接待范围卡后（Q3「哪些买家不让我接」反推 L 等级），主 Agent **仅将推导出的 L 等级写入会话上下文暂存**（建议 key：`hire.reception_levels`），**不调本接口**、不激活接待助手，直接推进至 Step 5 模拟对话；Step 5 商家点「确认模拟效果」后，主 Agent **此时才立即调** `cowboy_config create --levels {Step 4 暂存值}`，一步完成子账号创建 + 买家等级写入，接口成功后接待助手 status 写为 `active`、正式上岗；**后续**调为 `update --levels` 在管理页面同步买家等级，不在主对话框发起


path: references/capabilities/daily_report.md
content: # daily_report（工作日报）

## 功能说明

查询接待助手的工作日报数据。当前为统一模式：按日期查询各项接待指标。

## CLI 调用

```bash
python3 cli.py daily_report --date today
python3 cli.py daily_report --date 2026-05-14
```

### TPP 接口

`POST /api/cowboy_daily_report/1.0.0`

请求体：
```json
{"date": "2026-05-14"}
```

### 完整参数表

| 参数     | 简写 | 说明                          | 默认值 |
| -------- | ---- | ----------------------------- | ------ |
| `--date` | `-d` | 查询日期：'today' 或 YYYY-MM-DD | today  |

## 输出格式

```json
{
  "success": true,
  "markdown": "## 接待助手工作日报（2026-05-14）\n\n| 指标 | 数值 | 较昨日 |\n...",
  "data": {
    "date": "2026-05-14",
    "elapsed_seconds": 0.3,
    "metrics": [
      {"name": "接待人数", "value": "15", "unit": "人", "yoy": "+12.5%"},
      {"name": "成交订单", "value": "3", "unit": "笔", "yoy": "-25.0%"},
      {"name": "成交客户", "value": "3", "unit": "人", "yoy": "-25.0%"},
      {"name": "询盘数量", "value": "28", "unit": "个", "yoy": "+8.3%"},
      {"name": "平均响应时间", "value": "45", "unit": "秒", "yoy": "-10.0%"},
      {"name": "转人工次数", "value": "5", "unit": "次", "yoy": "+25.0%"}
    ]
  }
}
```

### 字段映射（网关 → 项目内部）

| 网关字段 | 项目内部字段 | 说明 |
|----------|-------------|------|
| `metricName` | `name` | 指标名称 |
| `metricValue` | `value` | 指标数值 |
| `unit` | `unit` | 单位 |
| `yesterdayPercentage` | `yoy` | 同比变化（较昨日） |

## 注意事项

1. **双层 Result 包装**：网关返回的 `data` 字段是内层 `{"data": [...DTO...]}`，需剥两层才能拿到真正的 metrics 列表
2. **日期格式**：仅接受 ISO 格式 `YYYY-MM-DD`，斜杠格式 `YYYY/MM/DD` 不合法
3. **sellerUserId 不需传递**：网关会根据 AK 自动注入实际调用方 userId
4. metrics 列表为空时展示"今天没数据，可能日报还在统计中"

## 关联页面

执行 `daily_report` 后，Agent 可同时引导商家打开日报详情页查看。


path: references/capabilities/knowledge.md
content: # 知识库管理 (knowledge_query / knowledge_answer)

## 概述

两个命令共同管理接待助手的知识缺口：
- `knowledge_query`：查询接待助手接待中无法回答或转人工的问题列表
- `knowledge_answer`：为指定问题补充标准答案，自动写入云端知识库

## knowledge_query

### 入参

| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `--page` | int | 否 | 1 | 页码 |
| `--page-size` | int | 否 | 10 | 每页条数 |

### TPP 接口

`POST /api/cowboy_knowledge_query/1.0.0`

请求体（项目内部 page → 网关字段 pageNum）：
```json
{"pageNum": 1, "pageSize": 10}
```

返回字段（网关双层 Result 包装，外层 success + 内层 AiSellerCcPageResult）：
```json
{
  "success": true,
  "data": {
    "data": [
      {
        "id": "TQR001",
        "query": "这款手机支持5G网络吗？",
        "createTime": "2026-05-14 10:30:00",
        "relatedProducts": [
          {
            "productId": "PROD001",
            "productName": "iPhone 15 Pro Max",
            "productImage": "https://example.com/products/iphone15promax.jpg"
          }
        ]
      }
    ],
    "total": 15,
    "pageNum": 1,
    "totalPages": 2
  }
}
```

### 字段映射（网关 → 项目内部）

| 网关字段 | 项目内部字段 | 说明 |
|----------|-------------|------|
| `id` | `question_id` | 问题 ID |
| `query` | `question` | 问题内容 |
| `createTime` | `create_time` | 问题产生时间 |
| `relatedProducts` | `products` | 关联商品列表 |
| `relatedProducts[].productId` | `products[].id` | 商品 ID |
| `relatedProducts[].productName` | `products[].name` | 商品名称 |
| `relatedProducts[].productImage` | `products[].image` | 商品图片 |

### 输出格式

Markdown 列表展示，每条包含：问题内容、问题ID、关联商品名称、时间。
支持分页提示（当有下一页时提示 `--page N`）。

---

## knowledge_answer

### 入参

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `--question` | str | 是 | 问题内容 |
| `--answer` | str | 是 | 答案内容 |
| `--question-id` | str | 否 | 问题 ID（可选；不传则自动生成 UUID） |

### TPP 接口

`POST /api/cowboy_knowledge_answer/1.0.0`

请求体（项目内部 question_id → 网关字段 recordId，question → query）：
```json
{
  "recordId": "TQR001",
  "query": "这款手机支持5G网络吗？",
  "answer": "是的，iPhone 15 Pro Max 支持全球主流5G频段"
}
```

返回字段（网关双层 Result 包装，外层 success + 内层 AiSellerCcResult\<Boolean\>）：
```json
{
  "success": true,
  "data": {
    "success": true,
    "data": true
  }
}
```

内层 `data` 为 Boolean：
- `true`：答案成功入库
- `false`：网关受理但入库未成功（检查 query 与 question_id 是否匹配）

### 写入规则

- 提交后该问题从待完善列表移除
- 答案自动写入云端知识库
- 接待助手下次遇到类似问题时将使用该答案回复

### 注意事项

1. **双层 Result 包装**：所有 1688 网关接口返回都是外层 Result + 内层业务壳，代码需剥两层
2. **字段名边界映射**：项目内部统一使用 question_id / question，仅在构造请求 body 时映射为网关字段名（recordId / query）
3. **UUID 自动生成**：当 `--question-id` 未传时，service 层本地生成 uuid 作为 recordId

### 关联页面

执行 `knowledge_query` 后，Agent 应同时引导商家打开培训中心 - 待完善知识页，供其在页面中直接操作。


path: references/capabilities/score.md
content: # 技能掌握度评分 (score)

## 概述

展示牛仔的综合技能掌握度评分，帮助商家了解牛仔当前的能力水平和提升方向。

## 入参

无（直接执行即可）

## TPP 接口

`POST /api/CowboyScore/1.0.0`

请求体：
```json
{}
```

返回字段：
```json
{
  "success": true,
  "data": {
    "totalScore": 72,
    "dimensions": [
      {
        "name": "知识库完善度",
        "score": 65,
        "description": "知识库覆盖了65%的常见问题"
      },
      {
        "name": "技能流程覆盖",
        "score": 80,
        "description": "已创建4个技能流程覆盖主要场景"
      },
      {
        "name": "报价准确率",
        "score": 70,
        "description": "近7天报价确认通过率70%"
      },
      {
        "name": "独立接待率",
        "score": 75,
        "description": "近7天75%的对话无需转人工"
      }
    ],
    "suggestions": [
      "补充「物流相关」类问题答案可提升知识库完善度约10分",
      "创建「催发货应对」技能可减少约15%的转人工率",
      "调整报价Skill的利润率区间可提升报价确认通过率"
    ]
  }
}
```

## 评分维度说明

| 维度 | 计算规则 |
|------|----------|
| 知识库完善度 | 已有答案的问题数 / 所有出现过的问题数 × 100 |
| 技能流程覆盖 | 已创建技能涵盖的业务场景数 / 店铺实际出现的场景数 × 100 |
| 报价准确率 | 近7天报价确认通过数 / 报价总数 × 100 |
| 独立接待率 | 近7天无转人工对话数 / 总对话数 × 100 |

## 输出格式

Markdown 表格展示各维度分数 + 文字列出提升建议。

## 使用时机

- 商家询问"牛仔表现怎么样"
- 商家询问"评分"、"掌握度"、"完善度"
- 日报中综合评价的补充详情


path: references/capabilities/task_config.md
content: # 分活配置（管理牛仔弹窗）

## 页面定位

商家通过弹窗式配置面板，管理牛仔的接待工作范围。配置修改后实时生效。

## 页面内容

### 基本状态栏
- 牛仔头像 + 当前状态（「接待中」绿色 /「已暂停」灰色）
- 暂停/恢复接待 切换按钮

### 四维度配置面板

#### 1. 需求种类（多选）

基于近30天询盘自动统计的业务类型：

| 常见类型 | 说明 |
|----------|------|
| 采购询价 | 买家直接询问产品价格 |
| 定制需求 | 买家有定制化要求 |
| 物流查询 | 询问发货方式和费用 |
| 售后咨询 | 退换货/售后投诉 |
| 普通闲聊 | 无明确采购意向的对话 |

默认全选。取消勾选后，该类询盘不交给牛仔处理，直接进入人工队列。

#### 2. 接待时间段

| 预设选项 | 时间 |
|----------|------|
| 全天候 | 00:00 ~ 24:00（默认） |
| 工作时间 | 09:00 ~ 18:00 |
| 仅非工作时间 | 18:00 ~ 09:00 |
| 仅周末 | 周六、周日全天 |
| 自定义 | 允许多时段组合 |

非接待时间段内，牛仔不主动应答，买家消息进入人工队列。

#### 3. 买家等级筛选

| 等级 | 说明 |
|------|------|
| KA客户 | 大客户，默认不交给牛仔，由人工接待 |
| V等级高 | V3及以上，可配置是否由牛仔接待 |
| 普通买家 | 默认由牛仔接待 |
| 新买家 | 默认由牛仔接待 |

#### 4. 红线场景

以下场景一旦触发，牛仔立即转人工（默认全部勾选）：

| 红线 | 说明 |
|------|------|
| 投诉升级 | 买家明确表示要投诉 |
| 退款争议 | 涉及退款金额分歧 |
| 法律合规 | 涉及合同、法律条款 |
| 情绪激动 | 买家用词激烈 |

### 底部数据概况
- 近7天接待总量、牛仔接待占比、转人工率
- 用于帮助商家判断配置是否合理

## Agent 引导要点

当用户说"调整分活配置"、"暂停牛仔"时，Agent 应：
1. 引导文案："您可以调整牛仔的接待范围，包括需求种类、工作时段、客户等级和红线场景。"
2. 跳转到管理牛仔弹窗
3. 跳转后话术："已打开管理牛仔配置面板。您可以修改接待规则，修改后实时生效。"


path: references/capabilities/training_kb.md
content: # 培训中心 - 本地知识库

## 页面定位

商家可添加本地文件夹路径，牛仔自动解析其中文档并同步到云端知识库（OSS），支持 PDF、Word、Excel、图片。

## 页面内容

### 文件夹路径列表

每条路径展示：

| 字段 | 说明 |
|------|------|
| 文件夹名称 | 用户自定义的名称 |
| 完整路径 | 本地文件夹的绝对路径 |
| 文件数量 | 该文件夹下的文档数 |
| 上次同步时间 | 最后一次同步完成的时间 |
| 同步状态 | 「已同步」（绿色）/ 「同步中」（蓝色旋转图标）|

### 操作按钮

| 操作 | 说明 |
|------|------|
| 打开 | 在系统文件管理器中打开对应文件夹路径 |
| 移除 | 二次确认弹窗 → 确认后从列表移除，牛仔停止同步该目录 |
| 添加文件夹路径 | 展开表单，填写名称和路径后确认添加，添加后自动开启同步云端 |

### 支持的文件格式
- PDF、Word(.doc/.docx)、Excel(.xls/.xlsx)、图片(jpg/png)、TXT

## Agent 引导要点

当用户说"添加知识库"、"同步文件"时，Agent 应：
1. 引导文案："您可以指定本地文件夹，牛仔会自动解析文档并同步到云端。"
2. 跳转到该页面
3. 跳转后话术："已打开本地知识库管理页面。您可以添加文件夹路径，牛仔会自动解析同步。"


path: references/capabilities/transfer_inquiries.md
content: # transfer_inquiries（转人工询盘明细查询）

## 功能说明

按日期分页查询当日转人工的询盘明细记录（接待助手答不上来、主动转给商家处理的会话）。与 `daily_report` 关系：`daily_report` 只给「转人工次数」的**汇总值**，本命令给**逐条明细**（什么时间 / 哪个买家 / 询盘总结 / 转人工原因）。

## CLI 调用

```bash
python3 cli.py transfer_inquiries                                  # 默认 today / 第 1 页 / 每页 10 条
python3 cli.py transfer_inquiries --date 2026-05-14
python3 cli.py transfer_inquiries --date today --page-num 2 --page-size 20
```

### TPP 接口

`POST /api/query_transfer_inquiries/1.0.0`

请求体：
```json
{"date": "2026-05-14", "pageNum": 1, "pageSize": 10}
```

### 完整参数表

| 参数            | 简写 | 说明                              | 默认值 |
| --------------- | ---- | --------------------------------- | ------ |
| `--date`        | `-d` | 查询日期：'today' 或 YYYY-MM-DD   | today  |
| `--page-num`    | `-p` | 页码，>=1                         | 1      |
| `--page-size`   | `-s` | 每页大小，1~100                   | 10     |

## 输出格式

```json
{
  "success": true,
  "markdown": "## 转人工询盘（2026-05-14）\n\n> 共 **5** 条 · 第 1 / 1 页（每页 10 条）\n\n| 时间 | 买家 | 公司 | 询盘总结 | 转人工原因 |\n...",
  "data": {
    "date": "2026-05-14",
    "elapsed_seconds": 0.4,
    "page_num": 1,
    "page_size": 10,
    "total": 5,
    "total_pages": 1,
    "inquiries": [
      {
        "id": "INQ001",
        "inquiry_time": "2026-05-14 10:30:00",
        "inquiry_summary": "询问商品材质和是否能定制 logo",
        "transfer_reason": "涉及定制需求，超出标准接待范围",
        "buyer_nickname": "张三",
        "buyer_login_id": "zhangsan",
        "buyer_company_name": "上海科技有限公司",
        "buyer_tags": "VIP客户, 老客户",
        "buyer_avatar": "https://..."
      }
    ]
  }
}
```

### 字段映射（网关 → 项目内部）

| 网关字段             | 项目内部字段           | 说明              |
| -------------------- | ---------------------- | ----------------- |
| `id`                 | `id`                   | 询盘 ID           |
| `inquiryTime`        | `inquiry_time`         | 转人工发生时间    |
| `inquirySummary`     | `inquiry_summary`      | 询盘总结          |
| `transferReason`     | `transfer_reason`      | 转人工原因        |
| `buyerNickname`      | `buyer_nickname`       | 买家昵称          |
| `buyerLoginId`       | `buyer_login_id`       | 买家登录 ID       |
| `buyerCompanyName`   | `buyer_company_name`   | 买家公司名        |
| `buyerTags`          | `buyer_tags`           | 买家标签          |
| `buyerAvatar`        | `buyer_avatar`         | 买家头像 URL      |
| `buyerLoginIdEncode` | —（**不暴露**）        | 系统内部编码值    |

## 注意事项

1. **双层 Result 包装**：网关返回的 `data` 字段是内层 `AiSellerCcPageResult { data: [...DTO], total, pageNum, pageSize, totalPages }`，需剥两层才能拿到真正的 DTO 列表
2. **日期格式**：仅接受 ISO 格式 `YYYY-MM-DD`，斜杠格式不合法
3. **sellerUserId 不需传递**：网关会根据 AK 自动注入实际调用方 userId
4. **`buyerLoginIdEncode` 严禁向商家展示**：属于系统内部编码值，不得透出
5. 空列表时输出"该日期暂无转人工询盘"，不要编造数据
6. 分页边界：`pageNum >= 1`、`pageSize` 在 1~100 之间，超出 CLI 层直接拒绝

## 关联页面

商家看完转人工明细后，常见追问是"我要在哪里接手 / 补答这些询盘"——主 Agent 应同时调 `show_interaction(name='manage_reception')` 打开接待管理 Tab，让商家在页内手动接管 / 调整接待范围；**主 Agent 不在对话里代回询盘**。


path: references/common/error-handling.md
content: # 通用错误处理

## 异常体系

```
SkillError (基类, code=500)
+-- AuthError      (AK 无效/签名失败/未配置, code=401)
+-- ParamError     (请求参数不合法, code=400)
+-- RateLimitError (请求被限流, code=429)
+-- ServiceError   (服务端异常/网络异常, code=500)
+-- TimeoutError   (请求超时, code=504)
```

## 错误识别与 Agent 应对

命令输出 `success: false` 时，先输出 `markdown` 字段，再根据关键词引导：

| markdown 关键词    | Agent 应对                                     |
| ------------------ | ---------------------------------------------- |
| "AK 未配置"        | 运行 `cli.py configure YOUR_AK` 或检查环境变量 |
| "认证失败" / "401" | AK 无效或过期，需重新配置                      |
| "限流" / "429"     | 等待 1-2 分钟重试                              |
| "超时" / "504"     | 接口较慢，已内置自动重试，可稍后再试           |
| "连接错误"         | 检查网络                                       |
| 其他               | 仅输出 markdown 即可                           |

## 输出格式

所有异常统一转为标准 JSON：
```json
{
  "success": false,
  "markdown": "错误描述（用户可读）",
  "data": {}
}
```

## 环境变量覆盖

| 环境变量               | 说明                  | 默认值 |
| ---------------------- | --------------------- | ------ |
| `COWBOY_REPORT_TIMEOUT` | 工作日报接口超时(秒)  | 30     |


path: references/common/internals.md
content: # 内部实现（仅供研发 / 运维参考，Agent 无需读取）

> 本文件记录的全部是部署、签名、埋点等"幕后"细节。
> 这些内容不会出现在与商家的对话里，主 Agent 在执行命令时也不需要理解它们。
> 如果只是想知道某个能力对外行为，请回到 `SKILL.md` 主文件 / `references/capabilities/<command>.md`。

---

## 1. 目录与代码结构

```
1688-cowboy/
├── SKILL.md                       # 主入口，Agent 唯一行为契约
├── cli.py                         # CLI 统一入口，按白名单加载 capability
├── references/
│   ├── capabilities/<cmd>.md      # 每个对外命令的参数 / 接口定义
│   └── common/                    # 通用规则（错误处理 / 内部实现）
└── scripts/
    ├── _auth.py                   # AK 提取 + HMAC-SHA256 签名
    ├── _http.py                   # 网关 POST 封装 + 重试 + 错误映射
    ├── _errors.py                 # SkillError 体系
    ├── _output.py                 # 统一 JSON 输出 {success, markdown, data}
    ├── _tracker.py                # skill 调用埋点
    ├── settings.py                # 配置常量 + 路径 + AK fallback 路径
    └── capabilities/<cmd>/        # 每个 CLI 命令的 cmd.py + service.py
```

`cli.py` 启动时通过 `_discover_capabilities()` 扫描 `scripts/capabilities/<name>/cmd.py`，但只暴露 **白名单** 内的命令。白名单未列入的目录视为历史代码残留，不对外可见。

---

## 2. 命令白名单（与 SKILL.md 命令速查保持一致）

| 命令 | 后端目录 | 上线状态 |
|------|----------|----------|
| `daily_report` | `scripts/capabilities/daily_report/` | ✅ |
| `knowledge_query` | `scripts/capabilities/knowledge_query/` | ✅ |
| `knowledge_answer` | `scripts/capabilities/knowledge_answer/` | ✅ |
| `configure` | `scripts/capabilities/configure/` | ✅ |

> `hire-reception` 是主 Agent 内部流程编排（5 步剧情），不走 CLI，没有对应后端目录。
>
> `quote_*` / `score` / `skill_*` 目录为历史能力残留，不在白名单内，CLI 不会暴露；可在后续清理迭代中删除目录与对应 references。

---

## 3. 网关与签名

- **网关地址**：`https://skills-gateway.1688.com/`（见 `scripts/_http.py` `BASE_URL`）
- **签名算法**：HMAC-SHA256，规范见 `scripts/_auth.py` `build_signature`
- **请求头**：
  - `x-csk-ak` / `x-csk-time` / `x-csk-nonce` / `x-csk-content-md5` / `x-csk-version`
  - `x-csk-sign`（基于 method + content-md5 + content-type + timestamp + 规范化请求头 + 规范化资源路径计算）
- **重试策略**：仅对 `ConnectionError` / `Timeout` / 502 / 504 重试，最多 3 次，指数退避（封顶 10s）
- **错误映射**：401→`AuthError("签名无效")`、429→`RateLimitError("限流")`、400→`ParamError`、其他 5xx→`ServiceError`

---

## 4. AK 读取与 fallback

读取顺序见 `scripts/_auth.py` `get_ak_from_env`：

1. 环境变量 `ALI_1688_AK`（OpenClaw 注入）
2. 配置文件 `${OPENCLAW_CONFIG_DIR:-~/.openclaw}/openclaw.json` 中 `skills.entries["1688-reception-assistant"].apiKey`（Gateway 未重启时 fallback）
3. 都没有 → 返回 `None`，调用方抛 `AuthError("AK 未配置")`

`extract_ak_keys` 约定：原始字符串前 32 位是 `AccessKeySecret`，第 33 位起是 `AccessKeyID`；输入若是 base64url 编码会先解码再切。

---

## 5. 接口路径常量（`scripts/settings.py`）

| 常量 | 路径 |
|------|------|
| `DAILY_REPORT_PATH` | `api/CowboyDailyReport/1.0.0` |
| `KNOWLEDGE_QUERY_PATH` | `api/CowboyKnowledgeQuery/1.0.0` |
| `KNOWLEDGE_ANSWER_PATH` | `api/CowboyKnowledgeAnswer/1.0.0` |

> `SKILL_LIST_PATH` / `SKILL_CREATE_PATH` / `SKILL_TOGGLE_PATH` / `QUOTE_LIST_PATH` / `QUOTE_CONFIRM_PATH` / `SCORE_PATH` 仍保留在常量里以兼容旧脚本，但 CLI 不再暴露对应命令。

---

## 6. 埋点上报（`scripts/_tracker.py`）

- 时机：`cli.py main()` 命令执行完毕后异步触发 `report_skill_usage()`
- 接口：`POST /api/reportSkillsUsage/1.0.0`
- 上报字段：`skillsName=niuzai-receptionist` / `version=settings.SKILL_VERSION` / `scene=CLI` / `channel=${SKILL_CHANNEL:-clawhub}`
- **失败静默**，不影响主流程；只记 `logger.debug`

---

## 7. 环境变量一览

| 变量 | 默认值 | 用途 |
|------|--------|------|
| `ALI_1688_AK` | — | 必需，AK 鉴权（OpenClaw 注入） |
| `OPENCLAW_CONFIG_DIR` | `~/.openclaw` | AK fallback 配置目录 |
| `COWBOY_API_TIMEOUT` | `30` | 网关接口超时秒数 |
| `SKILL_CHANNEL` | `clawhub` | 埋点 channel 字段 |

---

## 8. 输出契约（`scripts/_output.py`）

所有 capability 最终通过统一的 JSON 打印：

```json
{"success": true, "markdown": "...", "data": {...}}
```

- `markdown`：直接给商家看的可读内容
- `data`：结构化字段，供主 Agent 做二次解读 / 分析
- `success=false` 时 `markdown` 必须自带可读错误描述；Agent 处理见 `references/common/error-handling.md`


path: requirements.txt
content: requests>=2.28.0


path: scripts/_auth.py
content: #!/usr/bin/env python3
"""
AK 认证模块

提供 AK 提取、HMAC-SHA256 签名生成功能。
复用买家侧认证机制，统一网关签名规范。
"""

import hashlib
import hmac
import base64
import time
import uuid
import json
import os
from typing import Optional, Dict, Tuple
from urllib.parse import urlparse, parse_qs, quote
from settings import settings, OPENCLAW_CONFIG_PATH

SKILL_VERSION = settings.SKILL_VERSION


def extract_ak_keys(raw_input: str) -> Tuple[Optional[str], Optional[str]]:
    """
    从原始输入中提取 AccessKeyID 和 AccessKeySecret

    Args:
        raw_input: ALI_1688_AK 环境变量的值

    Returns:
        (access_key_id, access_key_secret) 或 (None, None) 如果无效
    """
    try:
        decoded = base64.urlsafe_b64decode(raw_input).decode("utf-8")
        if decoded:
            raw_input = decoded
    except Exception:
        pass

    if not raw_input or len(raw_input) < 32:
        return None, None

    [REDACTED_SECRET]
    [REDACTED_SECRET]

    return access_key_id, access_key_secret


def _get_ak_raw_from_config() -> Optional[str]:
    """从 OPENCLAW_CONFIG_PATH 读取 AK（Gateway 未重启时的 fallback）"""
    if not OPENCLAW_CONFIG_PATH.exists():
        return None
    try:
        with open(OPENCLAW_CONFIG_PATH, "r", encoding="utf-8") as f:
            config = json.load(f)
        entries = config.get("skills", {}).get("entries", {})
        skill = entries.get("1688-cowboy", {})
        ak = skill.get("apiKey") or skill.get("env", {}).get("ALI_1688_AK", "")
        return ak if ak else None
    except Exception:
        return None


def get_ak_from_env() -> Tuple[Optional[str], Optional[str]]:
    """读取 AK：优先环境变量（OpenClaw 注入），其次配置文件（Gateway 未重启时 fallback）"""
    raw_input = os.environ.get("ALI_1688_AK") or _get_ak_raw_from_config()
    if not raw_input:
        return None, None
    return extract_ak_keys(raw_input)


def get_content_md5(body: str) -> str:
    """计算 body 的 MD5 并 Base64 编码"""
    if not body:
        return ""
    md5_obj = hashlib.md5(body.encode('utf-8'))
    return base64.b64encode(md5_obj.digest()).decode('utf-8')


def get_canonicalized_resource(uri: str) -> str:
    """规范化资源路径"""
    parsed_uri = urlparse(uri)
    path = parsed_uri.path
    query = parsed_uri.query

    if not query:
        return path

    params = parse_qs(query)
    sorted_keys = sorted(params.keys())

    canonical_query = []
    for key in sorted_keys:
        values = sorted(params[key])
        for value in values:
            encoded_key = quote(key, safe='')
            encoded_val = quote(value, safe='')
            canonical_query.append("{}={}".format(encoded_key, encoded_val))

    return "{}?{}".format(path, "&".join(canonical_query))


def build_signature(
    method: str,
    uri: str,
    body: str,
    content_type: str,
    ak_id: str,
    ak_secret: str
) -> Dict[str, str]:
    """
    构建带签名的请求头

    Args:
        method: HTTP 方法 (GET/POST)
        uri: 请求路径（包含查询参数）
        body: 请求体 JSON 字符串
        content_type: Content-Type
        ak_id: Access Key ID
        ak_secret: Access Key Secret

    Returns:
        完整的请求头字典
    """
    timestamp = str(int(time.time()))
    nonce = uuid.uuid4().hex[:8]
    content_md5 = get_content_md5(body)

    csk_headers = {
        "x-csk-ak": ak_id,
        "x-csk-time": timestamp,
        "x-csk-nonce": nonce,
        "x-csk-content-md5": content_md5,
        "x-csk-version": SKILL_VERSION,
    }

    sorted_csk_keys = sorted(csk_headers.keys())
    canonicalized_headers = ""
    for key in sorted_csk_keys:
        canonicalized_headers += "{}:{}\n".format(key.lower(), csk_headers[key].strip())

    string_to_sign = (
        method.upper() + "\n" +
        content_md5 + "\n" +
        content_type + "\n" +
        timestamp + "\n" +
        canonicalized_headers +
        get_canonicalized_resource(uri)
    )

    signature = hmac.new(
        ak_secret.encode('utf-8'),
        string_to_sign.encode('utf-8'),
        hashlib.sha256
    ).digest()
    sign_base64 = base64.b64encode(signature).decode('utf-8')

    headers = {
        "Content-Type": content_type,
        "x-csk-sign": sign_base64,
    }
    headers.update(csk_headers)

    return headers


def get_auth_headers(method: str, uri: str, body: str = "") -> Optional[Dict[str, str]]:
    """
    获取认证头（便捷函数）

    Returns:
        请求头字典，如果 AK 未配置则返回 None
    """
    ak_id, ak_secret = get_ak_from_env()

    if not ak_id or not ak_secret:
        return None

    return build_signature(
        method=method,
        uri=uri,
        body=body,
        content_type="application/json",
        ak_id=ak_id,
        ak_secret=ak_secret,
    )


path: scripts/_errors.py
content: #!/usr/bin/env python3
"""
统一异常体系

所有 capability 的 service 层抛出此处定义的异常，
由 cmd 层统一捕获并转为 {success: false, markdown, data} 输出。
"""


class SkillError(Exception):
    """所有 skill 异常的基类"""

    def __init__(self, message: str, code: int = 500, data: dict = None):
        super().__init__(message)
        self.message = message
        self.code = code
        self.data = data or {}


class ParamError(SkillError):
    """请求参数不合法 (400)"""

    def __init__(self, message: str = "请求参数不合法"):
        super().__init__(message, code=400)


class RateLimitError(SkillError):
    """请求被限流 (429)"""

    def __init__(self, message: str = "请求被限流，请稍后重试"):
        super().__init__(message, code=429)


class ServiceError(SkillError):
    """服务端异常 / 网络异常 (500)"""

    def __init__(self, message: str = "服务异常，请稍后重试"):
        super().__init__(message, code=500)


class AuthError(SkillError):
    """认证失败 (401)"""

    def __init__(self, message: str = "认证失败，请检查 AK 配置"):
        super().__init__(message, code=401)


class TimeoutError(SkillError):
    """请求超时"""

    def __init__(self, message: str = "请求超时，请稍后重试"):
        super().__init__(message, code=504)


path: scripts/_http.py
content: #!/usr/bin/env python3
"""
通用 HTTP 客户端

职责：签名注入、自动重试、统一错误映射。
所有 capability 的 service 层通过 api_post(path, body) 调用网关 API。
网关地址：
预发：https://skills-gateway.1688.com/
线上：https://skills-gateway.1688.com
"""

import json
import sys
import time
from functools import wraps

import requests

from _auth import get_auth_headers
from _errors import AuthError, ParamError, RateLimitError, ServiceError, TimeoutError

# 上线前改为线上网关
BASE_URL = "https://skills-gateway.1688.com/"
MAX_RETRIES = 3
RETRY_DELAY_BASE = 1


class _RetryableError(Exception):
    """内部重试信号，不对外暴露"""
    pass


def _with_retry(max_retries: int = MAX_RETRIES):
    """重试 ConnectionError / Timeout / _RetryableError，其余异常直接传播"""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except (requests.exceptions.ConnectionError,
                        requests.exceptions.Timeout,
                        _RetryableError) as e:
                    last_exc = e
                    delay = min(RETRY_DELAY_BASE * (2 ** attempt), 10)
                    print("请求异常(尝试{}/{}): {}, {}s后重试".format(
                        attempt + 1, max_retries, e, delay), file=sys.stderr)
                    if attempt < max_retries - 1:
                        time.sleep(delay)
            raise ServiceError("请求异常，已重试{}次: {}".format(max_retries, last_exc))
        return wrapper
    return decorator


def _handle_http_error(e: requests.exceptions.HTTPError):
    """HTTP 状态码 -> SkillError，502/504 触发重试"""
    status = e.response.status_code if e.response is not None else None
    if status == 401:
        raise AuthError("签名无效或已过期（401）")
    if status == 429:
        raise RateLimitError("请求被限流（429），请稍后重试")
    if status == 400:
        raise ParamError("请求参数不合法（400）")
    if status in (502, 504):
        raise _RetryableError("网关超时（{}），将自动重试".format(status))
    raise ServiceError("HTTP 错误 {}".format(status))


def _handle_biz_error(result: dict):
    """业务错误（HTTP 200 但 success=false）-> SkillError"""
    msg = result.get("msgInfo") or result.get("message") or "未知业务错误"
    raise ServiceError(str(msg))


@_with_retry()
def _api_post_impl(path: str, body: dict = None, timeout: int = 30, raw_body: str = None) -> dict:
    """内部实现，带重试"""
    return _do_post(path, body, timeout, raw_body)


def _do_post(path: str, body: dict = None, timeout: int = 30, raw_body: str = None) -> dict:
    """
    POST 请求网关 API 核心逻辑

    Args:
        path:    API 路径（拼接到 BASE_URL 后面）
        body:    请求体 dict（与 raw_body 二选一）
        timeout: 超时秒数
        raw_body: 已序列化的请求体字符串（跳过 json.dumps）

    Returns:
        API 响应 dict

    Raises:
        ParamError / RateLimitError / ServiceError
    """
    url = "{}{}".format(BASE_URL, path)
    body_str = raw_body if raw_body is not None else json.dumps(body or {}, ensure_ascii=False)

    # AK 签名认证
    headers = get_auth_headers("POST", path, body_str)
    if not headers:
        raise AuthError("AK 未配置")

    try:
        resp = requests.post(url, headers=headers, data=body_str.encode("utf-8"), timeout=timeout)
        resp.raise_for_status()
    except requests.exceptions.HTTPError as e:
        _handle_http_error(e)
    except requests.exceptions.Timeout:
        raise TimeoutError("请求超时")

    try:
        result = resp.json()
    except json.JSONDecodeError:
        raise ServiceError("响应格式异常，无法解析 JSON")

    # 业务层错误检测
    if isinstance(result, dict) and result.get("success") is False:
        _handle_biz_error(result)

    return result


def api_post(path: str, body: dict = None, timeout: int = 30, raw_body: str = None, retry: bool = True) -> dict:
    """
    POST 请求网关 API

    Args:
        path:    API 路径（拼接到 BASE_URL 后面）
        body:    请求体 dict（与 raw_body 二选一）
        timeout: 超时秒数
        raw_body: 已序列化的请求体字符串（跳过 json.dumps）
        retry:   是否启用自动重试（默认 True）

    Returns:
        API 响应 dict
    """
    if retry:
        return _api_post_impl(path=path, body=body, timeout=timeout, raw_body=raw_body)
    return _do_post(path=path, body=body, timeout=timeout, raw_body=raw_body)


path: scripts/_output.py
content: #!/usr/bin/env python3
"""
统一输出工具

所有 cmd.py 通过此模块输出 JSON，保证格式一致。
输出格式：{"success": bool, "markdown": str, "data": {...}}
"""

import json

from _errors import SkillError, AuthError, ParamError, RateLimitError, TimeoutError


def make_output(success: bool, markdown: str, data: dict) -> dict:
    return {"success": success, "markdown": markdown, "data": data}


def print_output(success: bool, markdown: str, data: dict):
    """打印标准 JSON 输出"""
    print(json.dumps(make_output(success, markdown, data), ensure_ascii=False, indent=2))


# 错误类型 -> 行动建议
_ERROR_HINTS = {
    AuthError: "请检查 ALI_1688_AK 环境变量是否已配置，或确认 AK 是否有效。",
    ParamError: "请检查传入的参数是否正确。",
    RateLimitError: "请等待 1-2 分钟后重试。",
    TimeoutError: "接口响应较慢，已内置自动重试。如仍失败，请稍后再试。",
}


def print_error(e: Exception, default_data: dict = None):
    """将异常转为标准错误输出并打印，自动附加行动建议"""
    if isinstance(e, SkillError):
        msg = "{}".format(e.message)
        # 按异常子类匹配行动建议
        for err_type, hint in _ERROR_HINTS.items():
            if isinstance(e, err_type):
                msg += "\n\n**建议**: {}".format(hint)
                break
    elif isinstance(e, ValueError):
        msg = "参数错误：{}".format(e)
    else:
        msg = "操作失败：{}\n\n**建议**: 如反复出现，请检查网络连接或稍后重试。".format(e)
    print_output(False, msg, default_data or {})


path: scripts/_risk_guard.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
风险命令二次确认工具 -- 配合 SKILL.md `metadata.risk_command_hooks` 使用

协议参考：钩子_skill_hook_接入协议.md §一 BashRiskCheckHook

用途：
    所有写入 / 状态变更 / 凭证类命令在真正执行前，必须经过商家端侧弹窗显式确认，
    防止 AI 被 prompt 注入或路由错误时跳过 SKILL.md 软约束直接触发后端写入。

两阶段协议：
    Phase 1（首次调用）：脚本输出 <user_confirmation>{message,payload}</user_confirmation>
                       端侧解析后弹窗，由商家点击确认。
    Phase 2（点击确认后）：框架将 payload 写入临时文件，把路径放入环境变量
                       NEWTON_CONFIRM_PAYLOAD，再次执行同一脚本；脚本读取后
                       按 payload 真正执行业务逻辑。

权威值优先：Phase 2 内部所有业务参数都应**只**从 payload 读取，避免命令行参数
           在两阶段之间被任何中间环节篡改。
"""

import json
import os
import sys
from typing import Any, Dict, Optional

_CONFIRM_ENV = "NEWTON_CONFIRM_PAYLOAD"


def get_confirmed_payload() -> Optional[Dict[str, Any]]:
    """读取 Phase 2 注入的 payload。未注入或解析失败返回 None。"""
    payload_file = os.environ.get(_CONFIRM_ENV, "").strip()
    if not payload_file:
        return None
    try:
        with open(payload_file, "r", encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, dict):
            return data
    except Exception:
        return None
    return None


def emit_confirmation(
    message: str,
    payload: Dict[str, Any],
    *,
    preview_markdown: str = "",
) -> None:
    """Phase 1：先输出标准 JSON（供 LLM 看到"等待商家确认"），再输出协议
    要求的 <user_confirmation> 标签（端侧弹窗使用）。

    调用方在调用本函数后应**立即返回**，不要继续执行真实业务逻辑。

    Args:
        message: 展示给商家的确认提示（必填）
        payload: Phase 2 真正执行时所需的完整业务参数（必填，必须可 JSON 序列化）
        preview_markdown: 可选，给 LLM 看到的标准 markdown；缺省时复用 message
    """
    if not message:
        raise ValueError("emit_confirmation: message 不能为空")
    if payload is None:
        raise ValueError("emit_confirmation: payload 不能为 None")

    print(json.dumps({
        "success": False,
        "markdown": preview_markdown or "等待商家确认：{}".format(message),
        "data": {"awaiting_confirmation": True},
    }, ensure_ascii=False, indent=2))

    confirm_obj = {"message": message, "payload": payload}
    sys.stdout.write("<user_confirmation>{}</user_confirmation>\n".format(
        json.dumps(confirm_obj, ensure_ascii=False)
    ))
    sys.stdout.flush()


path: scripts/_tracker.py
content: #!/usr/bin/env python3
"""
Skill 埋点上报

职责：每次 CLI 命令执行时，向 skill 网关上报一次调用记录，用于统计 skill 调用次数。
上报失败不影响主流程，静默处理。
"""

import logging
import os

from settings import settings

logger = logging.getLogger("cowboy_tracker")

# 发布渠道（可通过环境变量覆盖）
_CHANNEL = os.environ.get("SKILL_CHANNEL", settings.SKILL_CHANNEL)


def report_skill_usage() -> None:
    """
    上报 skill 调用次数到网关。

    调用时机：每次 CLI 命令执行时调用一次（在 cli.py 的 main() 中触发）。
    失败时静默处理，不抛出异常，不影响主流程。
    """
    try:
        from _http import api_post

        api_post(
            "/api/reportSkillsUsage/1.0.0",
            {
                "apiName": None,
                "skillsName": settings.SKILL_NAME,
                "version": settings.SKILL_VERSION,
                "scene": "CLI",
                "channel": _CHANNEL,
            },
            retry=False,
        )
    except Exception as exc:
        logger.debug("埋点上报失败（已忽略）: %s", exc)


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