Skill: 1688aiinfra/1688-item-title-optimizer (version 0.1.0)

--- name: 1688-item-title-optimizer description: | 1688 商品标题智能优化助手 —— 自动并发执行两种优化算法生成结果。 工具能力：添加热词优化（快速、基于规则）和 LLM 深度重写（高质量、自然流畅），支持用户偏好参数。 触发词：优化标题、标题优化、改标题、重写标题、商品标题、标题改写。 metadata: openclaw: emoji: "✏️" requires: bins: - python interactions: - name: open_tab_select_product type: open_tab selectionType: shop_backend description: "当用户未提供商品ID时，直接输出该JSON唤起商品选择页面，流程结束，禁止反问用户" required_data: url: "商品选择页面URL" pageTitle: "选择商品" pageDescription: "选择商品优化标题" icon: "图标URL" - name: confirm_apply_title type: card selectionType: requirement description: "用户选定标题后，确认是否应用到商品" required_data: questions: "展示选定标题并询问是否应用" - name: select_items_to_optimize type: table selectionType: product description: "当用户传入≥3个商品ID时，在左侧弹出表格让用户筛选要优化的商品" required_data: title: "表格标题" columns: "列定义数组：商品ID、当前标题" rows: "商品列表，每项包含 itemId、title" - name: title_comparison_card type: table selectionType: title_plan description: "两种优化方案生成后，在左侧弹出表格供用户选择新标题。3列（方案+属性+内容），每个成功方案4个维度各占一行（两个都成功=8行，一个成功一个失败=仅展示成功方案的4行，两个都失败=不弹表格直接提示重试）。利用端侧 show_interaction.table v2 协议的 mergedColumns + groupBy + selectionGranularity:group + selectionMode:single 实现方案列相邻同值合并 + 组级互斥单选，用户勾选整组后点击「采用此方案」按钮" required_data: title: "表格标题：请选择新标题 + 商品名称（商品ID）" columns: "固定3列：plan（方案标识，宽80，不传 editable）、field（属性标签，宽140，必须显式声明 editable:false 否则会被端侧误渲染为可编辑，用户实测确认）、value（内容，宽620，不传 editable，由行级 rows[i].editable 控制）。配合行级 editable：仅新标题行 rows[i] 加 editable:true 时该 cell 可编辑，其他 cell 全部只读" mergedColumns: "[\"plan\"]。协议硬约束：列必须存在；不能含列级 editable:true 的列。本场景 value 已不开列级 editable，但仍只合并 plan，因为 value 每行内容都不同没有相邻同值" groupBy: "\"plan\"。selectionGranularity=group 时必填，按plan相邻同值切分组" selectionGranularity: "\"group\"。勾选粒度按组（每组组首行渲染一个checkbox，rowSpan=组大小）" selectionMode: "\"single\"。互斥单选：选新组自动取消旧组；点已选项=清空；空选=跳过。与 selectionGranularity 正交" actions: "[{key:adopt, label:采用此方案, variant:primary, description:...}]，覆盖默认「确认选择」" rows: "4行或8行（rows.length≤10，超过会触发分页关闭合并）。仅填入成功方案的行，失败方案不展示。两个都成功=8行，一个成功一个失败=4行，两个都失败=不弹表格。同方案的4行 plan 必须填相同值且连续排列：方案名称、新标题（仅这行 rows[i].editable:true，可cell内编辑）、生成逻辑及优化说明、预估曝光变化" respond_contract: "selectedRows 始终回传展开后的N行（与 multiple+row 同构），Agent 按 plan 分组、按 field=新标题 取最终value（含编辑），空选视为跳过。无论端侧是否识别行级 editable，Agent 必须软兜底：只采用「新标题」行的编辑值，其他 3 行编辑显式忽略" --- # 1688-item-title-optimizer — 商品标题智能优化 ## 技能概述 1688 商品标题智能优化助手。自动并发执行两种优化算法生成结果：1) 添加热词优化（快速、基于规则）2) LLM 深度重写（高质量、自然流畅）。只需提供商品 ID，即可同时获得两种优化方案供对比选择。支持用户偏好参数（如"加入'防潮'单词"）。 ## 使用场景 - 商品标题修改、改写、优化 - 新品发布，需要高质量标题 - 批量优化商品标题 - 用户有特定优化偏好（如指定关键词、风格） ## CLI 命令 ### configure — 配置 AK ```bash # 查看 AK 状态 python3 {baseDir}/cli.py configure # 设置 AK python3 {baseDir}/cli.py configure YOUR_AK ``` 配置网关鉴权所需的 AK。所有操作命令都依赖 AK，首次使用前需先配置。 ### optimize_title — 添加热词优化（方式A） ```bash python3 {baseDir}/cli.py optimize_title --item_id <商品ID> ``` 基于规则和统计的标题优化，保留原标题结构，快速添加高价值热搜词。 ### optimize_title_llm — LLM 深度重写（方式B） ```bash # 基础调用 python3 {baseDir}/cli.py optimize_title_llm --item_id <商品ID> # 带用户偏好 python3 {baseDir}/cli.py optimize_title_llm --item_id <商品ID> --preference "加入防潮单词" ``` 基于大语言模型的智能标题重写，全面改写标题，支持用户偏好定制。 ### get_keyword_info — 获取关键词信息 ```bash # 基础调用 python3 {baseDir}/cli.py get_keyword_info --item_id <商品ID> # 添加自定义关键词 python3 {baseDir}/cli.py get_keyword_info --item_id <商品ID> --custom_keywords "保温杯;不锈钢;便携" ``` 获取标题优化所需的全部关键词数据（热搜词、曝光词、类目信息）。 ### get_tokenizers — 获取分词器列表 ```bash python3 {baseDir}/cli.py get_tokenizers ``` 获取所有可用的分词器列表及说明。 ## ⚠️ 重要：技能skill 使用规范 ### 规则2：获取到商品ID后，自动并发执行两种优化 当用户请求标题优化时，**必须按以下步骤执行，关键节点需等待用户确认后再继续**： 1. **⏸️ 参数要求（硬性阻断点）**：如果用户未提供商品 ID，**严禁**弹 card 询问，必须按规则1输出JSON 2. **⏸️ 多商品澄清（≥3 个商品时必须触发）**： - 如果用户一次传入 **3 个及以上商品 ID**，**必须先触发** `select_items_to_optimize` 交互，让用户筛选需要优化的商品 - 展示所有商品 ID 及其当前标题（如能获取），让用户选择要优化的商品 - **必须等待用户选择后再继续**，禁止自动对所有商品执行优化 - 如果用户传入 1-2 个商品，可直接跳过此步骤进入下一步 3. **自动执行**：收到用户的优化请求后（或用户在澄清点选定商品后），**必须自动并发调用**两种优化命令 4. **并发调用**：在同一个消息中同时发起两个工具调用： - `optimize_title`（方式A：添加热词优化） - `optimize_title_llm`（方式B：AI深度重写） 5. **提取偏好**：如果用户在请求中提到特殊要求（如"加入'防潮'单词"），提取偏好并传入 `optimize_title_llm` 的 `--preference` 参数 6. **展示结果：触发 `title_comparison_card`**（`type: table`，在左侧弹出表格） - 3 列（方案 + 属性 + 内容），每个方案 4 行（方案名称、新标题、生成逻辑及优化说明、预估曝光变化） - 利用 `mergedColumns: ["plan"]` + `selectionGranularity: "group"` + `groupBy: "plan"` + `selectionMode: "single"` 实现方案列合并单元格 + 组级单选勾选 - 通过 `actions` 配置一个 `key: "adopt"` / `label: "采用此方案"` / `variant: "primary"` 的按钮替代默认"确认选择" - **rows 的 `plan` 字段：同组所有 4 行都填相同方案名**（如全部填"方案A"或全部填"方案B"），端侧依靠相同值进行合并和分组 7. **⏸️ 应用确认** - 向用户展示选定的新标题，与原标题对比 - 触发 `confirm_apply_title` 交互，让用户选择： - ✅ 确认，应用到商品标题 - ✏️ 我想手动微调后再应用（用户可输入修改后的标题） - 💾 仅记录，暂不应用 - **必须等待用户确认后再继续**，禁止自动跳过 8. **应用到商品**： - 如果用户确认应用，且存在技能 `1688-item-one-click`，则调用技能更新商品标题 - 如果用户手动微调了标题，使用微调后的版本应用 **具体的交互组件数据结构请查阅 [`references/interaction-specs.md`](./references/interaction-specs.md) 中对应交互的章节。** ### 规则3：结果展示规范（左侧 Table 表格） 展示时必须使用 `title_comparison_card`（**`type: table`**）交互组件，在左侧弹出表格，3 列（方案 + 属性 + 内容），每个方案 4 行，方案列合并为大单元格，组级互斥单选勾选。 > ⚠️ **端侧版本依赖（v2 协议）**：本交互依赖 `mergedColumns` / `selectionGranularity` / `selectionMode` / `groupBy` 4 个 v2 字段，**仅在已升级到 v2 的客户端**才会生效。当前 1688 工作台 / 找工厂客户端尚未升级，会**忽略**这 4 个字段，退化为默认的 `row + multiple`（每行一个 checkbox、可任意多勾、无方案列合并）。 > > **Agent 必须知道**： > - **payload 不要为兼容降级而修改**——协议字段写法是正确的，等客户端升级即可自动生效 > - **看到「每行一个 checkbox」不是 payload 错了**，是端侧降级渲染 > - 处理 `selectedRows` 时**必须用下方"用户回传后处理"统一兼容算法**，不能假设回传一定是同一方案的 4 行 > - 完整端侧能力对比与降级表见 `references/interaction-specs.md` 中"端侧版本依赖"小节 **展示规范**（`title_comparison_card`）： 1. **`title`**：格式为"请选择新标题 — 商品名称（商品ID）" 2. **`columns` 固定 3 列**： - `plan`（方案标识列，宽 80px，**不传** `editable`） - `field`（属性标签列，宽 140px，**必须显式声明** `editable: false`）—— ⚠️ 用户实测：省略 `editable` 字段会让端侧把该列误渲染为可编辑，**显式写 `false`** 才能保证只读 - `value`（内容列，宽 620px，**列级不传** `editable`，由行级 `rows[i].editable: true` 仅在"新标题"行开启） ⚠️ **行级 editable 协议（2026-05 用户实测有效）**：1688 端侧官方曾答复"列级 editable 不支持行级控制"，但**用户实测确认**端侧已支持 `rows[i].editable: true` 行级控制。本场景配合"`field` 列显式 `editable: false` + `value` 列不传 + 仅新标题行 `rows[i].editable: true`"的组合写法，达到"仅新标题行可编辑、其他所有 cell 都只读"的预期效果。 **双层保护（防御性设计）**：即使端侧某天回退、行级 editable 失效，Agent 在读取回传时**仍必须**软兜底：只采用「新标题」行的编辑值，其他 3 行编辑显式忽略（详见下方"用户回传后处理"第 6 步），保证业务正确性不依赖于端侧能力 3. **v2 协议合并/勾选字段（4 个，缺一不可）**： - `mergedColumns: ["plan"]`：方案列按**相邻同值**合并为大 rowSpan 单元格 - `groupBy: "plan"`：按方案字段切分相邻分组（`selectionGranularity:"group"` 时必填） - `selectionGranularity: "group"`：勾选**单位**为组（每组组首行渲染一个 checkbox，整组共用） - `selectionMode: "single"`：勾选**数量**为单选（方案 A / B 互斥；选新组自动取消旧组；点已选项 = 清空；空选 = 跳过） 4. **协议硬约束**（违反会被主进程 validator 拒绝 / 端侧渲染异常）： - `mergedColumns` 中的列**不能是列级 `editable: true`** —— 本场景 `value` 列已改为不开启列级 editable（用行级 `rows[i].editable` 替代），所以约束自动满足；仍只合并 `plan`，因为 `value` 每行内容不同没有相邻同值可合并 - **`field` 列必须显式 `editable: false`**（不可省略）—— 用户实测：省略时端侧会把该列误渲染为可编辑 - `rows.length ≤ 10` —— 超过会触发端侧分页并自动关闭合并；本场景最多 8 行（仅成功方案入表），安全 - 同方案的所有行 `plan` 字段必须**填相同值且连续排列**（不能"方案A、方案B、方案A"这样交错），否则相邻同值合并失效 5. **`actions` 自定义按钮**：配置 `[{ key: "adopt", label: "采用此方案", variant: "primary", description: "..." }]` 替代默认"确认选择" 6. **`rows` 仅填入成功方案的行（4 行或 8 行，严禁填入失败方案的行）**：两个都成功 = 方案 A 4 行 + 方案 B 4 行共 8 行；一个成功一个失败 = 仅成功方案的 4 行（**禁止**为失败方案填占位行）；两个都失败 = 不弹表格。每方案 4 个属性维度，**行级 editable 配置**（仅"新标题"行设 `editable: true`，端侧识别后这 3 行渲染只读）： - 方案名称（**不设** `editable`，端侧默认只读；即使端侧不识别行级 editable，Agent 也忽略此行编辑值） - **新标题**（**设** `"editable": true`，可 cell 内编辑；**用户编辑会被采用**为最终标题） - 生成逻辑及优化说明（**不设** `editable`，端侧默认只读；Agent 完全不读此行编辑值） - 预估曝光变化（**不设** `editable`，端侧默认只读；Agent 完全不读此行编辑值） 7. **生成逻辑维度构造**（写入"生成逻辑及优化说明"行的 `value`，按热度 `weight` 标注）： - 🔥 热词（tag=`热词`）：词名(热度:weight值) - 📈 流量获取（tag=`时间词`/`修饰词`） - ✨ 吸引力（tag=`场景词`/`风格词`） - 👀 买家关注（tag=`属性词`/`材质词`/`品类词`/`功能词`） 8. **📈 曝光量变化预测（必须）**：基于热词数据给出"+X% ~ +Y%"区间，写入"预估曝光变化"行的 `value`，必须附带"实际效果受类目竞争、商品权重、市场环境等多因素影响，仅供参考"免责说明（具体规则见 `references/interaction-specs.md` 中"曝光量变化预测规则"小节） 9. **部分失败处理（严禁展示失败方案）**：若任一方案 CLI 返回 `success: false` 或异常，**直接跳过该方案**，rows 中**仅填入成功方案的 4 行**，**禁止**为失败方案填入任何占位行/兜底行。具体规则： - **一个成功一个失败** → `title_comparison_card` 只展示成功方案的 4 行（`rows.length = 4`）；由于只有 1 个方案可选，用户直接勾选该方案即可；在对话中简要告知用户另一方案本次未生成成功 - **两个都失败** → **不弹** `title_comparison_card` 表格；直接在对话中告知用户"两种优化方案均未生成成功，建议稍后重试"，并提示可重新触发优化 - **两个都成功** → 正常展示 8 行（不变） **用户回传后处理**（统一兼容 v2 客户端 与 未升级客户端，**无需事先判断端侧版本**，按此顺序执行）： > **前置要求**：触发本交互之前，Agent 必须确保 `optimize_title`（方案A）和 `optimize_title_llm`（方案B）的 CLI 完整返回 JSON **仍可在当前对话上下文中访问**（用于降级场景下重新弹窗时重构 payload，**禁止**为此重新调用 CLI）。 1. **空选判定**：`selectedRows.length === 0` → 视为跳过，**禁止**进入 `confirm_apply_title`，应回退询问"是否重新生成 / 结束优化" 2. **按 `plan` 字段分组聚合**：`groups = group_by(selectedRows, row => row.plan)` 3. **跨方案混勾的兜底**（分组数 ≥ 2，仅未升级客户端可能出现）：**严禁**用"取行数最多的方案"等启发式猜测算法。必须： 1. 给用户对话提示："检测到您勾选了多个方案的行（方案 A：N₁ 行 / 方案 B：N₂ 行），由于本次只能采用一个方案，请在重新弹出的表格中**仅勾选您想采用的那一个方案**，再点「采用此方案」" 2. **重新触发 `title_comparison_card`**，用前置小节提到的成功方案的原始 CLI 返回值**重新构造 payload**（仅填入成功方案的行；**禁止**重新调用 `optimize_title` / `optimize_title_llm`） 3. 等待新一轮回传，从第 1 步重新走 4. **缺字段的兜底**（分组数 = 1 但唯一方案的行集合中缺少 `"方案名称"` 或 `"新标题"`，仅未升级客户端可能出现）： 1. 给用户对话提示："您勾选的行不完整（缺少 `<缺失的 field 列表>`），请在重新弹出的表格中**勾选包含「方案名称」和「新标题」的完整行集合**" 2. **重新触发 `title_comparison_card`**（同第 3 步：用原始 CLI 返回值重构 payload，**禁止**重调 CLI） 3. 等待新一轮回传，从第 1 步重新走 5. **（已移除）**：由于失败方案不再进入表格，无需在回传后识别失败方案。直接进入第 6 步 6. **读取最终新标题（仅采用「新标题」行的编辑值）**：在唯一选中方案的行集合里找 `field === "新标题"` 的行（第 4 步已保证此行存在），取其 `value`（**可能已被用户在 cell 内编辑**，以此为准，**禁止**回退读 CLI 原始 `new_title`，**禁止**用其他 field 的 value 当标题）。 - ⚠️ **关于其他 3 行的用户编辑**：协议层已通过**不在这 3 行设置** `editable: true` 让端侧渲染为只读。但万一端侧暂不识别 `rows[i].editable` 退化为"全部 cell 可编辑"，Agent 仍须**显式忽略**这些行的编辑值（双层保护）： - 方案名称行的 value 仅用于"识别选中方案"，**不参与最终标题构造** - 生成逻辑 / 预估曝光变化 行的 value **完全不读**，仅作 UI 展示 - **严禁**因用户改了其他行就把它当成新标题写入 `confirm_apply_title` 7. **进入应用确认**：携带"方案标识（来自分组的唯一 key）+ 最终新标题（仅来自「新标题」行的编辑值）+ 商品 ID + 商品原标题"触发 `confirm_apply_title` > **设计原则**：上述算法只依赖 `selectedRows` 的扁平结构 + `plan`/`field` 字段语义，不依赖端侧"组"/"单选"实现细节。在 v2 客户端上第 3、4 步的兜底永不触发（端侧已保证完整性），算法退化为"取 `groups` 唯一 key + 找新标题行"的极简路径；在未升级客户端上兜底按需启用，依靠 context 中已有的 CLI 原始返回值重构 payload 重新弹窗，**不再调用任何 CLI**。**待 1688 客户端升级到 v2 后无需任何回滚**。完整算法说明见 `references/interaction-specs.md` 中"Agent 处理逻辑"小节。 ### 规则4：用户偏好提取 如果用户在优化请求中提到特殊要求，需要提取并传入 `--preference` 参数： **识别偏好的关键词**： - "加入xxx"、"添加xxx"、"包含xxx" → 提取关键词 - "突出xxx"、"强调xxx"、"体现xxx" → 提取特点要求 - "要xxx风格"、"面向xxx" → 提取风格要求 **示例**： ``` 用户："优化商品831034165952的标题，加入'防潮'这个词" ↓ 提取：preference = "加入'防潮'单词" ↓ 调用：cli.py optimize_title_llm --item_id 831034165952 --preference "加入'防潮'单词" ``` ## 安全声明 | 风险级别 | 命令 | Agent 行为 | |---------|------|----------| | 只读 | configure | 可直接执行，无需确认 | | 只读 | optimize_title | 可直接执行，无需确认 | | 只读 | optimize_title_llm | 可直接执行，无需确认 | | 只读 | get_keyword_info | 可直接执行，无需确认 | | 只读 | get_tokenizers | 可直接执行，无需确认 | > 所有命令均为只读操作，不会修改商品标题。优化结果仅供参考，需用户确认后手动应用。 ## 异常处理 任何命令输出 `success: false` 时： 1. **先输出 `markdown` 字段**（已包含用户可读的错误描述） 2. **再根据关键词追加引导**： | markdown 关键词 | Agent 额外动作 | |----------------|--------------| | "AK 未配置" 或 "签名无效" 或 "401" | 提示用户当前发送能力所需鉴权未就绪，请补充有效 AK 或检查鉴权配置后重试 | | "限流" 或 "429" | 建议用户等待 1-2 分钟后重试 | | 其他 | 仅输出 markdown 即可 | ## 环境变量（.env） 项目根目录的 `.env` 文件存储 skill 基础信息，供埋点上报等模块读取。发布到不同环境时可直接替换该文件中的变量值。 | 变量 | 默认值 | 说明 | |------|--------|------| | `SKILL_NAME` | `1688-item-title-optimizer` | skill 名称 | | `SKILL_VERSION` | `1.0.0` | skill 版本号 | | `SKILL_CHANNEL` | `clawhub` | 发布渠道 | > 已存在的系统环境变量优先级高于 `.env`，CI/CD 注入的变量不会被覆盖。 ## 埋点上报 每次 CLI 命令执行时，自动向 skill 网关上报一次调用记录，用于统计 skill 调用次数。 - **实现位置**：`scripts/_tracker.py` → `report_skill_usage()`，在 `cli.py` 的 `main()` 中每次命令执行后自动调用 - **上报接口**：`POST /api/reportSkillsUsage/1.0.0` - **上报参数**： | 参数 | 值来源 | 说明 | |------|--------|------| | `apiName` | 固定 `null` | 固定传 null | | `skillsName` | `.env` `SKILL_NAME` | skill 名称 | | `version` | `.env` `SKILL_VERSION` | skill 版本号 | | `scene` | 固定 `CLI` | 固定值 | | `channel` | `.env` `SKILL_CHANNEL` | 发布渠道 | - **失败处理**：上报失败静默忽略，不影响主流程 ## 输出格式 采用标准 JSON 输出： ```json { "success": true, "markdown": "✅ 标题优化完成", "data": { "item_id": 831034165952, "old_title": "304不锈钢水杯", "new_title": "304不锈钢保温杯便携大容量", "optimize_reason": "添加热词:保温杯,便携,大容量", "new_title_words": [...], "other_words": [...] } } ``` ## 两种优化方式对比 | 特性 | optimize_title（方式A） | optimize_title_llm（方式B） | |------|----------------------|--------------------------| | 优化方式 | 规则 + 统计 | LLM 深度重写 | | 优化质量 | ★★★★☆ | ★★★★★ | | 优化速度 | < 1 秒 | 2-5 秒 | | 标题自然度 | 较自然 | 非常自然 | | 成本 | 低 | 较高 | | 偏好支持 | ❌ | ✅ | | 适用场景 | 快速优化、批量处理 | 深度优化、新品发布 | ### 规则5：曝光量变化预测 展示优化结果时，**必须**对每种方案给出曝光量变化预估。预估规则如下： 1. **热词数量变化**：新标题相比原标题每新增 1 个类目热搜词（tag=`热词`），预估曝光提升 5%-15% 2. **热词权重参考**：`new_title_words` 和 `other_words` 中的 `weight`（权重值）、`min_rnk`（搜索排名）可辅助判断词的流量价值——权重越高、排名越靠前，预估提升越大 3. **年份更新加成**：如果新标题包含当前年份词（如"2026新款"），预估额外提升 3%-8% 曝光 4. **综合预估**：给出一个保守区间（如 "+10% ~ +25%"），并标注免责说明 **免责说明（必须展示）**： > ⚠️ 曝光预估基于关键词热度数据，实际效果受类目竞争、商品权重、市场环境等多因素影响，仅供参考。 **示例**： | 方案 | 新增热词数 | 年份更新 | 预估曝光变化 | |------|----------|---------|------------| | 方式A | +3 个热词 | 无 | +15% ~ +25% | | 方式B | +4 个热词 | 含"2026新款" | +20% ~ +35% | ## 使用原则 1. 所有命令均为只读操作，不会修改商品标题 2. 优化结果仅供参考，需用户确认后手动应用 3. 收到优化请求后，应同时调用两种方式并展示结果 4. 如果用户有特殊偏好，应提取并传入 `--preference` 参数 5. 传入 ≥3 个商品时，必须先触发 `select_items_to_optimize` 交互让用户筛选 6. 结果展示必须使用 `title_comparison_card` 表格（3列×每方案4行，方案列合并单元格 + 组级单选），标注生成逻辑及优化说明（含热度）和曝光预估 ## 执行前置（首次命中能力时必须） - 如果用户没有提供商品ID（上下文里没有商品ID），直接按 [`references/interaction-specs.md`](./references/interaction-specs.md) 中 `open_tab_select_product` 的数据结构输出 JSON，流程结束，**不允许反问用户，不允许输出其他内容** - 首次执行 `optimize_title` 前：先完整阅读 `capabilities/optimize_title.md` - 首次执行 `optimize_title_llm` 前：先完整阅读 `capabilities/optimize_title_llm.md` - 首次执行 `get_keyword_info` 前：先完整阅读 `capabilities/get_keyword_info.md` - 首次执行 `get_tokenizers` 前：先完整阅读 `capabilities/get_tokenizers.md` ## Agent 执行检查清单 在执行优化时，请确认： - [ ] 提取商品ID - [ ] 若商品数 ≥ 3，已触发 `select_items_to_optimize` 交互并等待用户选择 - [ ] 已识别并提取用户偏好（如果有） - [ ] 并发调用 `optimize_title` 和 `optimize_title_llm` 命令 - [ ] 等待两个结果都返回 - [ ] 使用 `title_comparison_card` 表格展示两个结果（含生成逻辑 + 曝光预估） - [ ] 已给出每种方案的曝光量变化预估及免责说明

path: capabilities/get_keyword_info.md
content: # get_keyword_info — 获取关键词信息

## 功能说明

获取标题优化所需的全部关键词数据，包括类目热搜词、高曝光词、类目信息和商品属性。支持用户自定义关键词输入。

## 前置条件

- 已配置 AK（通过 `cli.py configure YOUR_AK` 或设置环境变量 `ALI_1688_AK`）

## CLI 调用

```bash
# 基础调用
python3 {baseDir}/cli.py get_keyword_info --item_id <商品ID>

# 添加自定义关键词
python3 {baseDir}/cli.py get_keyword_info --item_id <商品ID> --custom_keywords "保温杯;不锈钢;便携"
```

**参数说明**：

| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `--item_id` | int | 是 | 商品ID |
| `--include_expo_words` | flag | 否 | 包含高曝光词（默认 True） |
| `--include_hot_words` | flag | 否 | 包含类目热搜词（默认 True） |
| `--custom_keywords` | str | 否 | 自定义关键词，分号分隔 |

## 返回数据说明

| 字段 | 类型 | 说明 |
|------|------|------|
| item_id | int | 商品ID |
| cate_id | int | 类目ID |
| cate_name | String | 类目名称 |
| hot_words | Array | 类目热搜词列表 |
| expo_words | Object | 高曝光词及曝光量 |
| custom_keywords | Array | 用户自定义关键词 |
| cpv | String | 商品属性 |
| original_title | String | 原标题 |

## 输出格式

### 成功输出

```json
{
  "success": true,
  "markdown": "✅ 关键词信息获取成功",
  "data": {
    "item_id": 123456789,
    "cate_id": 50000001,
    "cate_name": "保温杯/保温瓶",
    "hot_words": ["不锈钢", "保温杯", "便携", "大容量"],
    "expo_words": {"保温": 150, "水杯": 120},
    "custom_keywords": ["保温杯", "不锈钢", "大容量", "便携"],
    "cpv": "材质:不锈钢;容量:500ml",
    "original_title": "304不锈钢水杯"
  }
}
```

## 异常处理

| 异常场景 | 处理方式 |
|---------|---------|
| AK 未配置 | 提示用户配置 AK |
| 商品ID 未提供 | 提示用户提供 --item_id 参数 |
| 签名无效（401） | 提示用户检查 AK 是否有效 |
| 请求被限流（429） | 建议用户等待 1-2 分钟后重试 |


path: capabilities/get_tokenizers.md
content: # get_tokenizers — 获取分词器列表

## 功能说明

获取所有可用的分词器列表及说明。根据用户需求或 prompt 选择合适的分词器。如果用户没有指定，选择默认分词器（列表第一个）。

## 前置条件

- 已配置 AK（通过 `cli.py configure YOUR_AK` 或设置环境变量 `ALI_1688_AK`）

## CLI 调用

```bash
python3 {baseDir}/cli.py get_tokenizers
```

**无参数**，返回所有可用分词器列表。

## 返回数据说明

成功时返回 `data` 对象，包含 `tokenizers` 数组：

| 字段 | 类型 | 说明 |
|------|------|------|
| tokenizer | String | 分词器类型标识 |
| desc | String | 分词器描述 |

## 输出格式

### 成功输出

```json
{
  "success": true,
  "markdown": "✅ 分词器列表获取成功",
  "data": {
    "tokenizers": [
      {"tokenizer": "qwen-flash", "desc": "使用qwen-flash模型进行分词"}
    ]
  }
}
```

## 异常处理

| 异常场景 | 处理方式 |
|---------|---------|
| AK 未配置 | 提示用户配置 AK |
| 签名无效（401） | 提示用户检查 AK 是否有效 |
| 请求被限流（429） | 建议用户等待 1-2 分钟后重试 |

## 使用说明

1. 获取分词器列表后，根据用户需求选择合适的分词器
2. 如果用户没有指定，默认使用列表中的第一个分词器
3. 选定的分词器类型可传入 `optimize_title` 的 `--tokenizer_type` 参数


path: capabilities/optimize_title.md
content: # optimize_title — 添加热词优化（规则版）

## 功能说明

基于规则和统计的标题优化方式。保留原标题结构，通过删除低效词和添加高价值热搜词来优化标题。成本低、速度快，适合快速批量处理。

## 前置条件

- 已配置 AK（通过 `cli.py configure YOUR_AK` 或设置环境变量 `ALI_1688_AK`）

## CLI 调用

```bash
python3 {baseDir}/cli.py optimize_title --item_id <商品ID>
```

**参数说明**：

| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `--item_id` | int | 是 | 商品ID |

## 返回数据说明

成功时返回 `data` 对象，包含以下字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| item_id | int | 商品ID |
| old_title | String | 原标题（⚠️ 必须展示） |
| new_title | String | 优化后的标题（⚠️ 必须展示） |
| optimize_reason | String | 优化说明 |
| new_title_words | Array | 标题词列表，包含每个词的标签和类型 |
| other_words | Array | 推荐词列表，未使用的高价值热词 |

## 输出格式

### 成功输出

```json
{
  "success": true,
  "markdown": "✅ 标题优化完成（添加热词方式）",
  "data": {
    "item_id": 831034165952,
    "old_title": "304不锈钢水杯",
    "new_title": "304不锈钢保温杯便携大容量",
    "optimize_reason": "添加热词:保温杯,便携,大容量",
    "new_title_words": [
      {"word": "304", "tag": "属性词", "type": null},
      {"word": "不锈钢", "tag": "属性词", "type": null},
      {"word": "保温杯", "tag": "热词", "type": "add"}
    ],
    "other_words": [
      {"word": "大容量", "tag": "热词", "description": "类目热搜词，排名5"}
    ]
  }
}
```

### 失败输出

```json
{
  "success": false,
  "markdown": "❌ AK 未配置\n\n运行: `cli.py configure YOUR_AK`",
  "data": {}
}
```

## 异常处理

| 异常场景 | 处理方式 |
|---------|---------|
| AK 未配置 | 提示用户配置 AK |
| 商品ID 未提供 | 提示用户提供 --item_id 参数 |
| 签名无效（401） | 提示用户检查 AK 是否有效 |
| 请求被限流（429） | 建议用户等待 1-2 分钟后重试 |
| 服务异常（500） | 提示用户稍后重试 |

## 展示规范

展示时必须：
1. 同时显示原标题（old_title）和新标题（new_title）
2. 显示优化说明（optimize_reason）
3. 如有推荐词（other_words），可一并展示供参考


path: capabilities/optimize_title_llm.md
content: # optimize_title_llm — LLM 深度重写

## 功能说明

基于大语言模型的智能标题重写方式。LLM 深度理解商品特征后全面改写标题，生成更自然流畅的标题，自动更新年份、融入热词。支持用户偏好定制（如"加入'防潮'单词"）。

## 前置条件

- 已配置 AK（通过 `cli.py configure YOUR_AK` 或设置环境变量 `ALI_1688_AK`）

## CLI 调用

```bash
# 基础调用
python3 {baseDir}/cli.py optimize_title_llm --item_id <商品ID>

# 带用户偏好
python3 {baseDir}/cli.py optimize_title_llm --item_id <商品ID> --preference "加入防潮单词"
```

**参数说明**：

| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `--item_id` | int | 是 | 商品ID |
| `--preference` | str | 否 | 用户偏好，自然语言描述优化偏好 |

## 用户偏好参数 (preference)

`preference` 参数允许用户通过自然语言指定优化偏好，LLM 会在优化标题时考虑这些偏好。

**支持的偏好类型**：

- **关键词偏好**："加入'防潮'单词"、"添加'防摔'和'耐用'关键词"
- **特点强调**："突出材质特点"、"强调便携性"、"体现性价比"
- **风格偏好**："标题要简洁"、"面向年轻消费者"
- **组合偏好**："加入'防潮'单词，同时突出材质特点"

**Agent 偏好提取关键词**：

| 用户表述 | 偏好类型 |
|---------|---------|
| "加入xxx"、"添加xxx"、"包含xxx" | 关键词偏好 |
| "突出xxx"、"强调xxx"、"体现xxx" | 特点强调 |
| "要xxx风格"、"面向xxx" | 风格偏好 |

## 返回数据说明

成功时返回 `data` 对象，包含以下字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| item_id | int | 商品ID |
| old_title | String | 原标题（⚠️ 必须展示） |
| new_title | String | 优化后的标题（⚠️ 必须展示） |
| new_title_words | Array | 标题词列表，包含每个词的标签和描述 |
| other_words | Array | 推荐词列表，未使用的高价值热词 |

## 输出格式

### 成功输出

```json
{
  "success": true,
  "markdown": "✅ 标题优化完成（LLM深度重写）",
  "data": {
    "item_id": 831034165952,
    "old_title": "304不锈钢水杯",
    "new_title": "2026新款304不锈钢保温杯便携大容量运动水杯",
    "new_title_words": [
      {"word": "2026", "tag": "时间词", "type": null, "description": "自动更新至当前年份"},
      {"word": "新款", "tag": "修饰词", "type": null},
      {"word": "304", "tag": "属性词", "type": null},
      {"word": "不锈钢", "tag": "材质词", "type": null},
      {"word": "保温杯", "tag": "热词", "type": null, "description": "类目热搜词"},
      {"word": "便携", "tag": "热词", "type": null, "description": "类目热搜词"},
      {"word": "大容量", "tag": "热词", "type": null, "description": "类目热搜词"},
      {"word": "运动", "tag": "热词", "type": null, "description": "类目热搜词"},
      {"word": "水杯", "tag": "品类词", "type": null}
    ],
    "other_words": [
      {"word": "户外", "tag": "热词", "weight": 8.5, "min_rnk": 12, "description": "类目热搜词，排名12，建议添加"}
    ]
  }
}
```

### 失败输出

```json
{
  "success": false,
  "markdown": "❌ AK 未配置\n\n运行: `cli.py configure YOUR_AK`",
  "data": {}
}
```

## 异常处理

| 异常场景 | 处理方式 |
|---------|---------|
| AK 未配置 | 提示用户配置 AK |
| 商品ID 未提供 | 提示用户提供 --item_id 参数 |
| 签名无效（401） | 提示用户检查 AK 是否有效 |
| 请求被限流（429） | 建议用户等待 1-2 分钟后重试 |
| 服务异常（500） | 提示用户稍后重试 |

## 展示规范

展示时必须：
1. **必须同时显示**原标题（old_title）和新标题（new_title）
2. **禁止只显示新标题**
3. 如有推荐词（other_words），可一并展示供参考
4. 如用户提供了偏好，需在展示中标注"已考虑您的偏好"

## 与 optimize_title 的对比

| 特性 | optimize_title_llm | optimize_title |
|------|-------------------|---------------|
| 优化方式 | LLM 深度重写 | 规则 + 统计 |
| 优化质量 | ★★★★★ | ★★★★☆ |
| 优化速度 | 2-5 秒 | < 1 秒 |
| 标题自然度 | 非常自然 | 较自然 |
| 成本 | 较高 | 低 |
| 偏好支持 | ✅ 支持 preference | ❌ 不支持 |


path: cli.py
content: #!/usr/bin/env python3
"""
1688-item-title-optimizer —— 商品标题智能优化 CLI 统一入口

Usage:
    python3 cli.py <command> [options]

Commands（更多参数见项目根目录 SKILL.md）:
    configure                      配置 AK（查看状态/设置/重置）
    optimize_title                 添加热词优化标题（规则版，快速）
    optimize_title_llm             LLM深度重写优化标题（高质量，支持偏好）
    get_keyword_info               获取商品关键词信息（热搜词、曝光词等）
    get_tokenizers                 获取可用的分词器列表

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

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)


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 = f"capabilities.{name}.cmd"
        try:
            mod = importlib.import_module(module_path)
            cmd_name = getattr(mod, 'COMMAND_NAME', name)
            commands[cmd_name] = module_path
        except Exception:
            pass

    return commands


def _usage(commands: dict):
    lines = ["**1688-item-title-optimizer 用法**\n", "```"]
    for name in sorted(commands):
        try:
            mod = importlib.import_module(commands[name])
            desc = getattr(mod, 'COMMAND_DESC', '')
            lines.append(f"python3 cli.py {name:<24} {desc}")
        except Exception:
            lines.append(f"python3 cli.py {name}")
    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]

    sys.argv = [f"cli.py {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/title_optimizer_qa.md
content: ```
## 常见问题

### Q1: 为什么要并发执行两种方式？

**A**:

- 让用户直接看到两种优化风格的对比
- 方式A保留原结构，方式B全面改写，各有优势
- 用户可以根据实际需求选择最合适的标题
- 并发执行节省时间，两个结果几乎同时返回

### Q2: 用户偏好只对方式B生效吗？

**A**:

- 是的，`preference` 参数只传给 `optimize_title_llm`（方式B）
- 方式A基于规则和系统推荐，不支持自定义偏好
- 如果用户有特殊要求，通常方式B的结果会更符合需求

### Q3: 如果两个结果都不满意怎么办？

**A**:

- 可以调整偏好参数，重新优化
- 可以基于推荐词列表手动调整
- 可以尝试不同的偏好描述

### Q4: 并发调用会增加成本吗？

**A**:

- 方式A成本很低（规则计算）
- 方式B成本较高（调用LLM）
- 总成本主要取决于方式B
- 但能让用户一次看到两种风格，提升选择效率

### Q5: 会自动应用优化后的标题吗？

**A**:

- 不会自动应用
- 只提供优化建议和新标题
- 需要用户选择后确认应用
- 确保用户完全掌控标题修改

---

```


path: references/title_wo_llm_SKILL.md
content: ---
name: title_wo_llm
description: 1688商品标题优化助手，无需LLM即可智能优化标题。通过三步流程（获取关键词信息 → 获取分词器列表并选择 → 使用选定分词器优化标题）提升标题质量。支持自定义关键词、选择分词器。使用场景：优化商品标题、自定义关键词优化。
---

# 标题优化助手 (Title Optimization Assistant)

为 1688 商品提供智能标题优化服务，基于规则和统计方法（无需 LLM），帮助商家快速提升标题质量、增加曝光和转化。

## 核心优化流程

标题优化采用三步流程，确保优化质量和效率：

### 步骤1：获取关键词信息
使用 `get_keyword_info` 获取优化所需的所有数据：
- 类目热搜词（基于真实搜索数据）
- 高曝光词（该商品的历史曝光关键词）
- 类目信息和商品属性

### 步骤2：获取分词器列表并选择
使用 `get_tokenizers` 获取所有可用的分词器：
- 获取所有预定义的分词器类型和说明
- 根据用户需求或prompt选择合适的分词器
- 如果用户没有指定，选择默认分词器（列表第一个）

### 步骤3：调用优化服务
使用 `optimize_title` 生成优化后的标题：
- 删除重复词和低频词
- 添加高相关性热搜词
- 生成优化说明和推荐词

## 快速开始

### 完整优化流程示例

```bash
# 步骤1：获取关键词信息
python3 scripts/interface.py --function get_keyword_info --item_id 123456789

# 步骤2：获取分词器列表
python3 scripts/interface.py --function get_tokenizers

# 步骤3：优化标题
python3 scripts/interface.py --function optimize_title --item_id 123456789 --use_llm
```

### 一键优化（跳过步骤1-2）

如果不需要查看中间结果，可直接调用优化服务：

```bash
python3 scripts/interface.py --function optimize_title --item_id 123456789
```

## 功能详解

### 1. get_keyword_info - 获取关键词信息

获取标题优化所需的全部关键词数据。**支持用户自定义关键词输入。**

**命令行：**
```bash
python3 scripts/interface.py --function get_keyword_info --item_id <商品ID> [--include_expo_words] [--include_hot_words] [--custom_keywords "关键词1;关键词2;关键词3"]
```

**参数：**
- `--item_id` (必需): 商品ID
- `--include_expo_words`: 包含高曝光词（默认True）
- `--include_hot_words`: 包含类目热搜词（默认True）
- `--custom_keywords` (可选): 用户自定义关键词，**使用分号分隔**，例如："保温杯;不锈钢;便携"

**使用示例：**
```bash
# 基础用法：获取系统推荐的关键词
python3 scripts/interface.py --function get_keyword_info --item_id 123456789

# 添加自定义关键词
python3 scripts/interface.py --function get_keyword_info --item_id 123456789 --custom_keywords "保温杯;不锈钢;大容量;便携"

# 只使用自定义关键词（不获取系统推荐）
python3 scripts/interface.py --function get_keyword_info --item_id 123456789 --custom_keywords "保温杯;不锈钢" --no-include_hot_words
```

**返回结果：**
```json
{
  "success": true,
  "data": {
    "item_id": 123456789,
    "cate_id": 50000001,
    "cate_name": "保温杯/保温瓶",
    "hot_words": ["不锈钢", "保温杯", "便携", "大容量"],
    "expo_words": {"保温": 150, "水杯": 120},
    "custom_keywords": ["保温杯", "不锈钢", "大容量", "便携"],
    "cpv": "材质:不锈钢;容量:500ml",
    "original_title": "304不锈钢水杯"
  }
}
```

### 2. get_tokenizers - 选择分词器

获取所有分词器列表，并根据用户prompt选择分词器。如果用户没有对分词器进行描述，则选择默认分词器（第一个分词器）。

**命令行：**
```bash
python3 scripts/interface.py --function get_tokenizers
```

**参数：**
（空）

**返回结果：**
[{"tokenizer": "qwen-flash", "desc": "使用qwen-flash模型进行分词"}]

获取上述结果后，按照用户prompt选择最匹配的分词器。

### 3. optimize_title - 优化标题

执行标题优化，生成优化后的标题。

**命令行：**
```bash
python3 scripts/interface.py --function optimize_title --item_id <商品ID> [--use_llm]
```

**参数：**
- `--item_id` (必需): 商品ID
- `--use_llm`: 使用LLM进行热词相关性判断（可选，提升准确性但增加耗时）

**返回结果：**
```json
{
  "success": true,
  "data": {
    "item_id": 123456789,
    "old_title": "304不锈钢水杯",
    "new_title": "304不锈钢保温杯便携",
    "optimize_reason": "添加热词:保温杯,便携",
    "new_title_words": [
      {"word": "304", "tag": "属性词", "type": null},
      {"word": "不锈钢", "tag": "属性词", "type": null},
      {"word": "保温杯", "tag": "热词", "type": "add"}
    ],
    "other_words": [
      {"word": "大容量", "tag": "热词", "description": "类目热搜词，排名5"}
    ]
  }
}
```

## 使用场景

### 场景1：新商品发布
为新上架商品生成优质标题

```bash
# 步骤1：获取关键词信息
python3 scripts/interface.py --function get_keyword_info --item_id 123456789

# 步骤2：获取分词器列表
python3 scripts/interface.py --function get_tokenizers
# 假设返回: [{"tokenizer": "qwen-flash", "desc": "..."}, {"tokenizer": "jieba", "desc": "..."}]
# 用户根据描述选择分词器，例如选择 "qwen-flash"

# 步骤3：使用选定的分词器优化标题
python3 scripts/interface.py --function optimize_title --item_id 123456789 --tokenizer_type qwen-flash --use_llm
```

### 场景2：自定义关键词优化
商家想使用特定的关键词优化标题（例如品牌词、活动词等）

```bash
# 使用自定义关键词
python3 scripts/interface.py --function get_keyword_info --item_id 123456789 \
  --custom_keywords "双十一;爆款;旗舰店;限时特惠"

# 然后进行优化
python3 scripts/interface.py --function optimize_title --item_id 123456789
```

**使用自定义关键词的优势：**
- 可以添加品牌词、活动词等特殊关键词
- 适合有特定营销需求的场景
- 结合系统推荐词和自定义词，实现精准优化

## 优化策略

### 删除策略
- **重复词**：标题中出现多次的词
- **低频词**：类目中搜索量极低的词
- **无效词**：对搜索无帮助的词

### 添加策略
- **高热度词**：类目热搜词，排名靠前
- **高曝光词**：该商品历史高曝光的词
- **高相关性**：与商品强相关的词（使用LLM判断）
- **自定义词**：用户指定的关键词（品牌词、活动词等）

### 长度控制
优化后标题长度不超过 60 个字符（中文按2计，英文按1计）

## 技术特点

### 无需LLM的快速优化
- 基于规则和统计的方法
- 优化速度快（< 1秒）
- 成本低，适合大规模应用

### 可选LLM增强
- 使用 `--use_llm` 参数启用
- 提升热词相关性判断准确率
- 耗时增加约0.5-1秒

### 数据驱动
- IGrpah图数据库：类目热搜词、词频统计
- Hologres数据仓库：商品曝光数据
- 实时计算：相似度分析、权重计算

path: scripts/_auth.py
content: #!/usr/bin/env python3
"""
1688-item-title-optimizer 认证模块
"""

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 _const import SKILL_VERSION, SKILL_NAME, OPENCLAW_CONFIG_PATH


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(SKILL_NAME, {})
        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:
    """
    规范化资源路径

    例如：/api/v1/user?name=张三&age=20
    转换为：/api/v1/user?age=20&name=%E5%BC%A0%E4%B8%89
    """
    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(f"{encoded_key}={encoded_val}")

    return f"{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 += f"{key.lower()}:{csk_headers[key].strip()}\n"

    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,
        **csk_headers,
    }

    return headers


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

    Args:
        method: HTTP 方法
        uri: 请求 URI
        body: 请求体（JSON字符串）

    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/_const.py
content: #!/usr/bin/env python3
"""
1688-item-title-optimizer 全局常量
"""

import os
from pathlib import Path

# Skill 版本
SKILL_VERSION = "1.0.0"

# Skill 名称
SKILL_NAME = "1688-item-title-optimizer"

# 后端 API 工具码（与旧 skill 保持一致）
TOOL_CODE = "1688_item_title_optimizer_interface"

# ── OpenClaw 配置文件路径──────────────────────────────────────────────────────
# 优先读取 OPENCLAW_CONFIG_DIR 环境变量，默认 ~/.openclaw
OPENCLAW_CONFIG_PATH: Path = Path(
    os.environ.get("OPENCLAW_CONFIG_DIR", Path.home() / ".openclaw")
) / "openclaw.json"


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 AuthError(SkillError):
    """AK 无效 / 签名失败 / 未配置 (401)"""

    def __init__(self, message: str = "AK 无效或已过期"):
        super().__init__(message, code=401)


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)


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