Skill: 1688aiinfra/1688-source-suppliers (version 1.0.0)

--- name: 1688-source-suppliers version: "1.0.0" description: | 1688找供应商 —— 结合用户需求与关键字查询对应的供应商及工厂信息 核心工具能力：1688供应商查询能力。用于查询1688平台上的供应商及工厂信息。 触发词：找供应商、查供应商、1688供应商、供应商信息、工厂信息、产业带查询。 不触发场景：找商品/选品 → 1688-product-find；比价/换供 → 1688-product-compare；下单付款 → 不处理。 metadata: {"openclaw": {"emoji": "🏭", "requires": {"bins": ["python3"]}, "primaryEnv": "ALI_1688_AK"}} --- # 1688-source-suppliers ## 意图判断 **触发本技能**： - 找供应商、查供应商、1688供应商、供应商信息、工厂信息 - 查询某地区的工厂/产业带（如"常州灯具工厂"、"义乌小商品供应商"） - 寻找某品类的生产厂家/加工厂 ## 严格禁止（NEVER DO） 1. **不编造供应商信息**：禁止凭空生成不存在的公司名称、联系方式或地址 2. **不擅自改写查询关键字**：不得在未征得用户认可的情况下修改用户的查询意图 3. **不混入Agent分析内容**：供应商信息展示时，Agent 分析内容必须追加在最后，不得混入供应商数据中 ## 命令速查 统一入口：`python3 {baseDir}/cli.py <command> [options]` 所有命令输出 JSON：`{"success": bool, "markdown": str, "data": {...}}` | 命令 | 说明 | 示例 | |------|------|------| | `ali_1688_source_suppliers` | 查询1688供应商信息 | `cli.py ali_1688_source_suppliers --query "灯具供应商"` | | `configure` | 配置 AK | `cli.py configure YOUR_AK` | ## 执行前置 首次执行命令前需先完整阅读 `references/capabilities/<命令名>.md`，同一会话内后续重复调用可复用已加载知识。 ## 使用流程 **步骤1：提取查询关键字** - 从用户消息中提取供应商名称或关键字 - 若关键字不明确，使用参数补齐引导话术追问用户： > "查询1688供应商信息需要提供查询关键字。你可以直接告诉我，例如：查询"灯具供应商"或"常州工厂"。" **步骤2：调用工具查询** - 执行命令：`python3 cli.py ali_1688_source_suppliers --query "<查询关键字>"` - 等待工具返回结果 **步骤3：检查输出状态** - 若 `success: true`：输出 `markdown` 字段内容 → 追加引导链接 → 可追加 Agent 分析 - 若 `success: false`：按「异常处理」章节处理 **步骤4：追加引导链接** - 在供应商信息后追加： > 📌 [找更多优质供应商上1688](https://s.1688.com/company/company_search.htm) ## 输出规范 **数据质量要求**： - 返回的供应商记录必须包含以下完整信息，缺少任一字段的记录将被自动过滤： - 公司名称（必填，不能为空） - 合作方式（必填，不能为空） - 服务（必填，不能为空） - Agent在向用户展示供应商信息时，应确保所有推荐的供应商都包含完整的供应商名称 **输出顺序**： 1. 先输出 `markdown` 字段内容（供应商信息） 2. 再追加引导链接 3. 最后可追加 Agent 分析内容 **引导链接格式**： > 📌 [找更多优质供应商上1688](https://s.1688.com/company/company_search.htm) **输出示例**： ```json { "success": true, "markdown": "找到 5 家供应商\n\n---\n\n### 供应商 1\n\n- 公司名称: [中山市致富照明有限公司](https://sale.1688.com/...)\n- 所在地区: 广东中山\n- 合作方式: OEM\n- 服务: 清加工, 包工包料", "data": {...} } ``` ## 异常处理 任何命令输出 `success: false` 时： 1. **先输出 `markdown` 字段**（已包含用户可读的错误描述） 2. **再根据关键词追加引导**： | markdown 关键词 | Agent 额外动作 | |----------------|--------------| | "AK 未配置" 或 "签名无效" 或 "401" | 提示用户当前查询能力所需鉴权未就绪，请运行 `cli.py configure YOUR_AK` 配置 AK 后重试 | | "参数缺失" 或 "query 不能为空" | 提示用户补充查询关键字后重试 | | "限流" 或 "429" | 建议用户等待 1-2 分钟后重试 | | "格式异常" 或 "服务异常" | 提示用户服务暂时不可用，请稍后重试 | | 其他 | 仅输出 markdown 即可 | ## 安全声明 | 风险级别 | 命令 | Agent 行为 | |---------|------|-----------| | **读取** | ali_1688_source_suppliers | 当查询关键字明确时直接执行；信息缺失时先追问补齐 | **全局读取规则**： 1. 查询类操作属于读取，当查询关键字明确时可直接执行 2. 当查询关键字不明确时，先向用户追问补齐后再执行 3. 不擅自扩写、改写用户的查询关键字；如需优化，应明确告知并征得用户认可

path: README.md
content: # 1688找供应商

一个用于查询1688供应商及工厂信息的命令行工具。

## 功能特性

- 根据供应商名称或关键字查询1688供应商信息
- 支持按地区、行业等条件筛选供应商
- 简单易用的CLI命令行界面
- 自动AK管理和签名认证

## 安装

### 前置要求

- Python 3.6 或更高版本
- pip（Python包管理器）

### 安装依赖

```bash
pip install -r requirements.txt
```

## 快速开始

### 1. 配置AK

首次使用前需要配置Access Key（AK）：

```bash
python3 cli.py configure YOUR_ACCESS_KEY
```

### 2. 查询供应商

```bash
# 查询特定供应商
python3 cli.py ali_1688_source_suppliers --query "灯具供应商"

# 使用缩写参数
python3 cli.py ali_1688_source_suppliers -q "常州工厂"
```

## 使用说明

### 命令列表

| 命令 | 说明 |
|------|------|
| `ali_1688_source_suppliers` | 查询1688供应商信息 |
| `configure` | 配置或查看AK状态 |

### 参数说明

#### ali_1688_source_suppliers

| 参数 | 缩写 | 必填 | 说明 |
|------|------|------|------|
| `--query` | `-q` | 是 | 供应商名称或关键字 |

#### configure

| 参数 | 说明 |
|------|------|
| `[AK]` | 可选，提供AK则进行配置，不提供则查看当前状态 |

### 输出格式

所有命令输出均为JSON格式：

```json
{
  "success": true,
  "markdown": "操作说明信息",
  "data": {
    "data": {
      // 具体数据
    }
  }
}
```

## 常见问题

### AK未配置

如果提示"AK 未配置"，请运行：

```bash
python3 cli.py configure YOUR_ACCESS_KEY
```

### AK无效

如果AK无效或已过期，请：

1. 检查AK是否正确
2. 重新配置AK：`python3 cli.py configure YOUR_ACCESS_KEY`
3. 如果配置后仍未生效，尝试重启OpenClaw或执行 `openclaw secrets reload`

### 网络错误

如果遇到网络错误：

1. 检查网络连接
2. 稍后重试
3. 检查防火墙设置

## 项目结构

```
1688-source-suppliers/
├── cli.py                                    # CLI入口
├── SKILL.md                                  # Skill定义
├── requirements.txt                          # Python依赖
├── scripts/                                  # 核心脚本
│   ├── _auth.py                             # 认证模块
│   ├── _http.py                             # HTTP客户端
│   ├── _const.py                            # 全局常量
│   ├── _errors.py                           # 异常定义
│   ├── _output.py                           # 输出工具
│   └── capabilities/                        # 功能实现
│       ├── configure/                       # AK配置
│       └── ali_1688_source_suppliers/       # 供应商查询
└── references/                              # 参考文档
    └── capabilities/
        ├── configure.md
        └── ali_1688_source_suppliers.md
```

## 开发

### 运行测试

```bash
# 查看帮助
python3 cli.py

# 测试配置命令
python3 cli.py configure

# 测试查询命令
python3 cli.py ali_1688_source_suppliers --help
```

### 代码规范

- 遵循PEP 8代码风格
- 使用类型注解
- 添加必要的文档字符串

## 许可证

[许可证信息]

## 联系方式

如有问题或建议，请联系项目维护者。


path: cli.py
content: #!/usr/bin/env python3
"""
1688-source-suppliers —— 1688找供应商 CLI 统一入口

用法：
    python3 cli.py ali_1688_source_suppliers --query "供应商名称"

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

Commands（更多参数见项目根目录 SKILL.md）:
    ali_1688_source_suppliers  找供应商请求    python3 cli.py ali_1688_source_suppliers --query "供应商名称"
    configure     配置 AK        python3 cli.py configure YOUR_AK

输出 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-source-suppliers 用法**\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:<16} {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()


if __name__ == "__main__":
    main()

path: references/capabilities/ali_1688_source_suppliers.md
content: # 1688供应商查询指南

## 功能说明

通过 CLI 调用1688供应商查询接口，返回匹配到的供应商及工厂信息。

## 前置条件

- 已配置 AK（未配置时会提示运行 `cli.py configure YOUR_AK`）

## CLI 调用

```bash
python3 cli.py ali_1688_source_suppliers --query "供应商名称"
```

### 参数说明

| 参数        | 缩写   | 是否必填 | 说明    |
|-----------|------|----------|-------|
| `--query` | `-q` | ✅ 是 | 供应商名称或关键字 |

### 调用示例

```bash
# 查询灯具供应商
python3 cli.py ali_1688_source_suppliers -q "灯具供应商"

# 查询常州地区的工厂
python3 cli.py ali_1688_source_suppliers -q "常州工厂"
```

## 输出格式

### 成功

```json
{
  "success": true,
  "markdown": "供应商信息查询成功",
  "data": {
    "data": {
      // 供应商详细信息
    }
  }
}
```

### 失败 — AK 未配置

```json
{
  "success": false,
  "markdown": "❌ AK 未配置，无法查询1688供应商信息。\n\n运行: `cli.py configure YOUR_AK`"
}
```

### 失败 — 其他异常

```json
{
  "success": false,
  "markdown": "❌ 错误描述信息"
}
```

## Agent 处理流程

```
1. 从用户消息中提取：供应商名称或关键字
2. 执行 cli.py ali_1688_source_suppliers --query <供应商名称>
3. 检查输出：
   - success=true → 告知用户"供应商信息查询成功"
   - success=false → 原样输出错误信息
```

## 异常处理

| 场景 | Agent 应对 |
|------|-----------|
| AK 未配置 | 引导用户执行 `cli.py configure YOUR_AK` 配置 AK |
| 参数缺失（query） | 提示用户补充供应商名称或关键字 |
| 接口返回格式异常 | 提示"格式异常，请稍后重试" |
| 其他运行时异常 | 原样输出错误信息 |

通用 HTTP 异常（400/401/429/500）处理见 `references/common/error-handling.md`。

path: references/capabilities/configure.md
content: # AK 配置指南

## 获取 AK（引导用户）

当用户没有 AK 时，Agent 输出以下引导：

> 请提供您的 AK（Access Key），用于1688供应商查询的鉴权。

## Agent 配置流程（核心）

用户告知 AK 后，Agent 按以下步骤执行：

```
1. 从用户消息中提取 AK 字符串
2. 执行 cli.py configure <AK>
3. 检查输出：success=true → 继续；success=false → 原样输出 markdown 错误信息
4. 配置成功后由 OpenClaw 配置注入生效（不依赖本地会话缓存）；如当前会话仍未生效，提示用户新开会话或执行 `openclaw secrets reload`
5. 继续用户的原始请求（如查询供应商信息）；若用户仅提供了 AK 没有其他请求，告知"配置成功，您可以查询供应商信息了"
```

## CLI 调用

```bash
python3 cli.py configure YOUR_AK_HERE
```

无参数调用可查看当前配置状态：`python3 cli.py configure`

## 异常处理

| 场景 | Agent 应对 |
|------|-----------|
| configure 输出 success=false | 原样输出 markdown 错误信息 |
| 配置成功但后续命令仍报 AK 未配置 | 提示用户新开会话或执行 `openclaw secrets reload`，必要时再重试 configure |
| 用户问"我的 AK 在哪" | 输出上方获取 AK 引导话术 |

通用 HTTP 异常（400/401/429/500）处理见 `references/common/error-handling.md`。

path: requirements.txt
content: requests>=2.31.0


path: scripts/_auth.py
content: #!/usr/bin/env python3
"""
钉钉消息 API 认证模块
"""

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, 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:
        # 当前 AK 规范并不保证一定是 base64，可回退到按长度切分
        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-shopkeeper", {})
        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)
    
    # 对 Key 排序
    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:
        完整的请求头字典
    """
    # A. 准备基础安全参数
    timestamp = str(int(time.time()))
    nonce = uuid.uuid4().hex[:8]
    content_md5 = get_content_md5(body)
    
    # B. 构造自定义 Header
    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,
    }
    
    # C. 生成 CanonicalizedHeaders
    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"
    
    # D. 构造待签名字符串
    string_to_sign = (
        method.upper() + "\n" +
        content_md5 + "\n" +
        content_type + "\n" +
        timestamp + "\n" +
        canonicalized_headers +
        get_canonicalized_resource(uri)
    )
    
    # E. 计算 HMAC-SHA256 签名
    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')
    
    # F. 返回最终 Headers
    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（如 /1688claw/skill/searchoffer）
        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,
    )

# 测试入口
if __name__ == "__main__":
    # 从环境变量获取 AK 进行测试
    import os
    test_ak = os.environ.get("ALI_1688_AK")
    
    if not test_ak:
        print("❌ 请先设置环境变量 ALI_1688_AK")
        print("示例: export ALI_1688_AK=your_ak_here")
        exit(1)
    
    ak_id, ak_secret = extract_ak_keys(test_ak)
    if not ak_id or not ak_secret:
        print("❌ AK 格式不正确")
        exit(1)
    
    print(f"✅ AK ID: {ak_id}")
    print(f"✅ [REDACTED_SECRET]")
    
    # 测试签名生成
    headers = build_signature(
        method="POST",
        uri="/api/1688_source_suppliers/1.0.0",
        body='{"query":"连衣裙"}',
        content_type="application/json",
        ak_id=ak_id,
        ak_secret=ak_secret,
    )
    
    print("\n✅ 签名生成成功！")
    print("请求头包含:")
    for k in headers.keys():
        print(f"  - {k}")

path: scripts/_const.py
content: #!/usr/bin/env python3
"""
1688供应商查询全局常量
"""

import os
from pathlib import Path

# Skill 版本
SKILL_VERSION = "1.0.0"

# ── 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)


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

职责：签名注入、自动重试、统一错误映射。
"""

import os
import json
import re
import time
import logging
from functools import wraps

import requests

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

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('1688_http')

# 支持通过环境变量切换预发/线上环境
# 线上: https://skills-gateway.1688.com
# 预发: https://skills-gateway.1688.com
BASE_URL = os.environ.get("1688_GATEWAY_URL", "https://skills-gateway.1688.com")
MAX_RETRIES = 3
RETRY_DELAY_BASE = 1


def _with_retry(max_retries: int = MAX_RETRIES):
    """仅重试 ConnectionError / Timeout，其余异常直接传播"""
    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) as e:
                    last_exc = e
                    delay = min(RETRY_DELAY_BASE * (2 ** attempt), 10)
                    logger.warning("网络异常(尝试%d/%d): %s, %ds后重试",
                                   attempt + 1, max_retries, e, delay)
                    if attempt < max_retries - 1:
                        time.sleep(delay)
            raise ServiceError(f"网络异常，已重试{max_retries}次: {last_exc}")
        return wrapper
    return decorator


def _handle_http_error(e: requests.exceptions.HTTPError):
    """HTTP 状态码 → SkillError"""
    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）")
    raise ServiceError(f"HTTP 错误 {status}")


def _handle_biz_error(result: dict):
    """业务错误（HTTP 200 但 success=false）→ SkillError"""
    msg_code = str(result.get("msgCode") or "")
    msg_info = result.get("msgInfo")
    code_match = re.search(r"\b(400|401|429|500)\b", msg_code)
    normalized = code_match.group(1) if code_match else ""

    if normalized == "401":
        raise AuthError("签名无效（401）")
    if normalized == "429":
        raise RateLimitError("请求被限流（429）")
    if normalized == "400":
        raise ParamError("请求参数不合法（400）")
    if normalized == "500":
        raise ServiceError("服务异常（500），请稍后重试")

    detail = msg_info or msg_code or "未知业务错误"
    raise ServiceError(str(detail))


@_with_retry()
def api_post_stream(path: str, body: dict = None, timeout: int = 60) -> str:
    """
    流式 POST 请求 1688 API（自动签名 + 重试 + 错误映射）

    Args:
        path:    API 路径，如 /api/1688_source_suppliers/1.0.0
        body:    请求体 dict（会 json.dumps）
        timeout: 超时秒数，默认60秒

    Returns:
        所有chunk数据合并后的完整字符串

    Raises:
        AuthError / ParamError / RateLimitError / ServiceError
    """
    url = f"{BASE_URL}{path}"
    body_str = json.dumps(body or {})

    headers = get_auth_headers("POST", path, body_str)
    if not headers:
        raise AuthError("AK 未配置")

    try:
        resp = requests.post(url, headers=headers, data=body_str, timeout=timeout, stream=True)
        resp.raise_for_status()
    except requests.exceptions.HTTPError as e:
        _handle_http_error(e)

    all_chunks = []
    try:
        for chunk in resp.iter_content(chunk_size=None, decode_unicode=True):
            if chunk:
                all_chunks.append(chunk)
    except Exception as e:
        logger.warning(f"流式数据读取异常: {e}")

    full_content = "".join(all_chunks)
    
    if not full_content:
        raise ServiceError("未获取到流式数据")

    return full_content


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

所有 cmd.py 通过此模块输出 JSON，保证格式一致。
"""

import json

from _errors import SkillError, AuthError

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))

def print_error(e: Exception, default_data: dict = None):
    """将异常转为标准错误输出并打印"""
    if isinstance(e, AuthError):
        msg = f"❌ {e.message}\n\n请运行: `cli.py configure YOUR_AK`"
    elif isinstance(e, SkillError):
        msg = f"❌ {e.message}"
    elif isinstance(e, ValueError):
        msg = f"❌ 参数错误：{e}"
    else:
        msg = f"❌ 操作失败：{e}"
    print_output(False, msg, default_data or {})



path: scripts/capabilities/ali_1688_source_suppliers/cmd.py
content: #!/usr/bin/env python3
"""1688供应商查询 CLI入口"""

COMMAND_NAME = "ali_1688_source_suppliers"
COMMAND_DESC = "查询1688供应商信息"

import os
import sys
import argparse

sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')))

from _auth import get_ak_from_env
from _output import print_output, print_error

from capabilities.ali_1688_source_suppliers.service import query_1688_source_suppliers


def main():
    ak_id, _ = get_ak_from_env()
    if not ak_id:
        print_output(False,
                     "❌ AK 未配置，无法查询1688供应商信息。\n\n运行: `cli.py configure YOUR_AK`",
                     {"data": {}})
        return

    parser = argparse.ArgumentParser(description="供应商信息查询")
    parser.add_argument("--query", "-q", required=True, help="供应商名称")
    
    try:
        args = parser.parse_args()
    except SystemExit:
        # argparse 参数缺失时会调用 sys.exit，捕获后返回标准 JSON 格式
        print_output(False,
                     "❌ 参数缺失：query 不能为空。\n\n请提供查询关键字，例如：\n- `cli.py ali_1688_source_suppliers --query \"灯具供应商\"`\n- `cli.py ali_1688_source_suppliers -q \"常州工厂\"`",
                     {"data": {}})
        return

    try:
        result = query_1688_source_suppliers(args.query)
        markdown_output = result.get("markdown", "未找到供应商信息")
        
        print_output(True, markdown_output, result)
    except Exception as e:
        print_error(e, {"data": {}})


if __name__ == "__main__":
    main()


path: scripts/capabilities/ali_1688_source_suppliers/service.py
content: #!/usr/bin/env python3
"""1688供应商信息"""

import json

from _http import api_post_stream
from _errors import ServiceError


def query_1688_source_suppliers(query: str) -> dict:
    """查询1688供应商信息（流式）"""
    
    full_content = api_post_stream("/api/1688_source_suppliers/1.0.0", {"query": query})
    
    try:
        result = json.loads(full_content)
        
        if not result.get("success"):
            msg_info = result.get("msgInfo", "未知错误")
            raise ServiceError(f"API调用失败: {msg_info}")
            
        factories_info = _extract_factories_from_response(result)
        markdown_output = _generate_markdown_output(factories_info)
        
        return {
            "factories": factories_info,
            "markdown": markdown_output
        }
            
    except json.JSONDecodeError as e:
        raise ServiceError(f"数据解析失败: {e}")


def _parse_json_field(field_value: str) -> list:
    """解析 JSON 数组字段，例如: "[\"OEM\",\"ODM\"]" -> ["OEM", "ODM"]"""
    if not field_value:
        return []
    try:
        result = json.loads(field_value)
        return result if isinstance(result, list) else []
    except (json.JSONDecodeError, TypeError):
        return []


def _find_retrieval_data(data_list: list) -> list:
    """从列表中查找 RETRIEVAL 阶段的数据"""
    for item in data_list:
        if isinstance(item, dict) and item.get("currentPhase") == "RETRIEVAL":
            response_data = item.get("responseData", {})
            if isinstance(response_data, dict) and "data" in response_data:
                factories = response_data["data"]
                if isinstance(factories, list) and len(factories) > 0:
                    return factories
    return []


def _get_factories_data(result: dict) -> list:
    """从API响应中获取工厂数据列表
    
    支持三种数据格式：
    1. 顶层 originResponses 数组
    2. data.result.originResponses 数组
    3. data.result.model 数组
    """
    # 格式1 & 格式2：originResponses
    origin_responses = result.get("originResponses", [])
    if not origin_responses:
        origin_responses = result.get("data", {}).get("result", {}).get("originResponses", [])
    
    factories = _find_retrieval_data(origin_responses)
    if factories:
        return factories
    
    # 格式3：data.result.model
    model = result.get("data", {}).get("result", {}).get("model", [])
    if isinstance(model, list):
        factories = _find_retrieval_data(model)
        if factories:
            return factories
    
    return []


def _extract_factories_from_response(result: dict) -> list:
    """从API响应中提取所有工厂信息"""
    try:
        factories = _get_factories_data(result)
        
        extracted_factories = []
        for factory in factories:
            if not isinstance(factory, dict):
                continue
            
            ext_infos = factory.get("extInfos", {})
            
            # 基础信息
            company_name = factory.get("companyName", "")
            oem_mode = _parse_json_field(ext_infos.get("oem_mode", ""))
            manufacture_type = _parse_json_field(ext_infos.get("manufacture_type", ""))
            
            # 数据质量过滤
            if not company_name or not oem_mode or not manufacture_type:
                continue
            
            # 所在地区
            province = ext_infos.get("reg_prov_name", "")
            city = ext_infos.get("reg_city_name", "")
            location = f"{province}{city}" if province or city else ""
            
            extracted_factories.append({
                "companyName": company_name,
                "companyUrl": factory.get("companyUrl", ""),
                "cooperationMode": oem_mode,
                "services": manufacture_type,
                "location": location,
                "factoryLevel": ext_infos.get("factory_level", ""),
                "factoryType": _parse_json_field(ext_infos.get("factory_type_tag", [])),
                "recTags": _parse_json_field(ext_infos.get("rec_tags", [])),
                "satisfiedRate": ext_infos.get("satisfied_rate_std_001", ""),
                "monthlyOrders": ext_infos.get("pay_ord_byr_cnt_1m_004", 0),
                "supportProofing": ext_infos.get("is_proofing", "") == "Y",
                "score": factory.get("score", 0)
            })
        
        return extracted_factories
        
    except Exception as e:
        raise ServiceError(f"提取工厂信息失败: {e}")


def _generate_markdown_output(factories: list) -> str:
    """生成展示用的Markdown输出，公司名称可点击跳转"""
    if not factories:
        return "未找到供应商信息"
    
    parts = [f"找到 {len(factories)} 家供应商\n"]
    
    for i, f in enumerate(factories, 1):
        lines = [f"---\n\n### 供应商 {i}\n"]
        
        # 公司名称（可点击链接）
        company_name = f.get("companyName", "")
        company_url = f.get("companyUrl", "")
        if company_url and company_name:
            lines.append(f"- 公司名称: [{company_name}]({company_url})")
        else:
            lines.append(f"- 公司名称: {company_name}")
        
        # 所在地区
        location = f.get("location", "")
        if location:
            lines.append(f"- 所在地区: {location}")
        
        # 工厂信息
        factory_info = []
        if f.get("factoryLevel"):
            factory_info.append(f["factoryLevel"])
        factory_type = ", ".join(f.get("factoryType", []))
        if factory_type:
            factory_info.append(factory_type)
        if factory_info:
            lines.append(f"- 工厂信息: {', '.join(factory_info)}")
        
        # 推荐标签
        rec_tags = ", ".join(f.get("recTags", []))
        if rec_tags:
            lines.append(f"- 推荐标签: {rec_tags}")
        
        # 服务指标
        indicators = []
        if f.get("satisfiedRate"):
            indicators.append(f["satisfiedRate"])
        if f.get("monthlyOrders"):
            indicators.append(f"月订单{f['monthlyOrders']}单")
        if f.get("supportProofing"):
            indicators.append("支持打样")
        if indicators:
            lines.append(f"- 服务指标: {', '.join(indicators)}")
        
        # 合作方式和服务
        lines.append(f"- 合作方式: {', '.join(f.get('cooperationMode', []))}")
        lines.append(f"- 服务: {', '.join(f.get('services', []))}")
        
        parts.append("\n".join(lines) + "\n")
    
    return "\n".join(parts)


path: scripts/capabilities/configure/cmd.py
content: #!/usr/bin/env python3
"""AK 配置命令 — CLI 入口"""

COMMAND_NAME = "configure"
COMMAND_DESC = "配置 AK"

import os
import sys

sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')))

from _output import print_output, print_error
from capabilities.configure.service import (
    validate_ak, configure_via_gateway, configure_via_file, check_existing_config,
)


def _mask_ak(ak: str) -> str:
    if len(ak) >= 8:
        return f"{ak[:4]}****{ak[-4:]}"
    return "****"


def main():
    try:
        has_existing, existing_ak = check_existing_config()

        # 无参数 → 查看状态
        if len(sys.argv) < 2:
            if has_existing:
                src = ("环境变量（已生效）" if os.environ.get("ALI_1688_AK")
                       else "OpenClaw 配置（新会话/重载后生效）")
                md = f"✅ AK 已配置: `{_mask_ak(existing_ak)}`（来源: {src}）"
            else:
                md = "❌ 尚未配置 AK\n\n运行: `cli.py configure YOUR_AK`"
            print_output(has_existing, md, {"configured": has_existing})
            return

        ak = sys.argv[1].strip()
        is_valid, error_msg = validate_ak(ak)
        if not is_valid:
            print_output(False, f"❌ {error_msg}", {"configured": False})
            return

        write_ok = configure_via_gateway(ak) or configure_via_file(ak)
        if not write_ok:
            print_output(False,
                         "❌ AK 写入失败（Gateway 不可用且 fallback 被拒绝/失败），请检查 Gateway 状态或文件权限",
                         {"configured": False})
            return

        md = (
            f"✅ AK 已保存: `{_mask_ak(ak)}`\n\n"
            "后续由 OpenClaw 配置注入生效（以配置为准，不使用本地会话缓存）。\n\n"
            "若当前会话仍提示 AK未配置或AK无效，请新开会话或执行：`openclaw secrets reload`（或 `openclaw gateway restart`）"
        )
        print_output(True, md, {"configured": True})
    except Exception as e:
        print_error(e, {"configured": False})


if __name__ == "__main__":
    main()


path: scripts/capabilities/configure/service.py
content: #!/usr/bin/env python3
"""AK 配置服务 — 校验、写入、状态查询"""

import json
import os
import sys
from pathlib import Path
from typing import Tuple

sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')))
from _const import OPENCLAW_CONFIG_PATH as CONFIG_PATH

SKILL_NAME = "1688-shopkeeper"


def validate_ak(ak: str) -> Tuple[bool, str]:
    """校验明文 AK 格式，返回 (is_valid, error_msg)"""
    if not ak:
        return False, "AK 不能为空"
    if len(ak) < 32:
        return False, f"AK 长度不足（当前 {len(ak)}，需要至少 32 位）"
    allowed = set([REDACTED_SECRET])
    if not all(c in allowed for c in ak):
        return False, "AK 包含非法字符"
    return True, ""


def configure_via_gateway(api_key: str) -> bool:
    """通过 OpenClaw Gateway REST API 写入配置（安全，不破坏 JSON5 格式）"""
    try:
        import requests
    except ImportError:
        return False

    gateway_url = os.environ.get("OPENCLAW_GATEWAY_URL", "http://localhost:18789")
    [REDACTED_SECRET]"OPENCLAW_GATEWAY_TOKEN", "")

    payload = {
        "skills": {
            "entries": {
                SKILL_NAME: {
                    "apiKey": api_key
                }
            }
        }
    }

    try:
        headers = {}
        if [REDACTED_SECRET]"Authorization"] = f"Bearer {token}"
        resp = requests.patch(f"{gateway_url}/api/config",
                              headers=headers, json=payload, timeout=5)
        return resp.ok
    except Exception:
        return False


def configure_via_file(api_key: str) -> bool:
    """直接写入 openclaw.json（fallback）"""
    try:
        config: dict = {}
        if CONFIG_PATH.exists():
            try:
                with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                    content = f.read().strip()
                    if content:
                        config = json.loads(content)
            except json.JSONDecodeError:
                return False

        config.setdefault("skills", {})
        config["skills"].setdefault("entries", {})
        config["skills"]["entries"].setdefault(SKILL_NAME, {})
        skill_entry = config["skills"]["entries"][SKILL_NAME]
        # apiKey 作为唯一写入目标，并移除旧格式 env.ALI_1688_AK 避免误导
        skill_entry["apiKey"] = api_key
        if "env" in skill_entry and isinstance(skill_entry["env"], dict):
            skill_entry["env"].pop("ALI_1688_AK", None)
            if not skill_entry["env"]:
                del skill_entry["env"]

        CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
        with open(CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(config, f, ensure_ascii=False, indent=2)

        return True
    except Exception:
        return False


def check_existing_config() -> Tuple[bool, str]:
    """检查是否已有 AK（环境变量优先，其次配置文件）"""
    env_ak = os.environ.get("ALI_1688_AK", "")
    if env_ak:
        return True, env_ak

    if not CONFIG_PATH.exists():
        return False, ""
    try:
        with open(CONFIG_PATH, "r", encoding="utf-8") as f:
            config = json.load(f)
        entries = config.get("skills", {}).get("entries", {})
        skill_entry = entries.get(SKILL_NAME, {})
        [REDACTED_SECRET]"apiKey")
        if isinstance(api_key, str) and [REDACTED_SECRET] True, api_key
        # 兼容旧格式：env.ALI_1688_AK
        legacy = skill_entry.get("env", {}).get("ALI_1688_AK", "")
        if legacy:
            return True, legacy
        return False, ""
    except Exception:
        return False, ""

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