Skill: orangeboo/goodsclassify (version 0.1.0)

--- name: goodsclassify-a description: Use this skill for 商品编码, 商品归类, 海关编码, HS编码, 税号, 蓝牙耳机编码, 手机壳编码, 充电器编码. Alias: goods-classify-helper, 商品归类助手. Always execute the local script in skills/goodsclassify-a/scripts/run_goodsclassify.py. Never simulate results. --- # 商品归类助手 ## 强制执行 1. 只要用户在问商品编码、商品归类、海关编码、HS 编码、税号，就必须使用本 skill。 2. 必须执行本地脚本 `skills/goodsclassify-a/scripts/run_goodsclassify.py` 获取结果。 3. 禁止捏造"权限问题"、"没有检测到 skill"、"模拟结果"、"根据经验推测编码"。 4. 禁止在未执行脚本的情况下直接给出任何商品编码结果。 5. 如果脚本要求手机号，就向用户索取手机号；用户回复手机号后，再次执行同一个脚本。 ## 执行命令 在本 skill 目录下运行： ```bash python skills/goodsclassify-a/scripts/run_goodsclassify.py --message "<用户原始消息>" ``` 如果需要显式会话 ID： ```bash python skills/goodsclassify-a/scripts/run_goodsclassify.py --session-id "<当前会话ID>" --message "<用户原始消息>" ``` ## 何时使用 - 查询商品编码 - 查询商品归类 - 查询海关编码 - 查询 HS 编码 - 查询税号 - 查询蓝牙耳机编码 - 查询手机壳编码 - 查询充电器编码 ## 示例 - 通过 goodsclassify-a 这个 skill 告诉我蓝牙耳机的商品编码 - 查一下苹果的手机壳编码 - 华为充电器编码呢 - 这个商品的 HS 编码是多少 - 帮我做商品归类 ```

path: .env
content: FASTGPT_API_URL=http://agent.ccorp.cn:16800/api/v1/chat/completions
AGENT_A_KEY=fastgpt-o89ea0GDqRnxpXGFqTHHo4qis7qKbxme6S1ByFjcBPee9BM1aQmpzyMrN8vv
AGENT_B_KEY=fastgpt-oWZs42uGeyg5yFKaiKXrvBgLTRsmBnbbWhFpaG1KC0q4D69VXmAxOxVH6Mx
AGENT_C_KEY=fastgpt-nMVt377nJLQJA1JGznGhH6zxz14x7S0r1JnORSNGx7AgIsdRcOrPNbD6HRWfXBb

path: README.md
content: # 商品归类助手

这是一个基于 FastGPT 的本地 Skill。

它的流程是：

1. 用户第一次提问商品编码问题时，如果本地 SQLite 里没有该会话的手机号，就先提示输入手机号。
2. 用户发送手机号后，调用 A 智能体判断手机号是否存在。
3. 如果不存在，调用 B 智能体登记手机号。
4. 登记完成后，自动继续处理上一次待回答的商品问题。
5. 同一 `session_id` 后续再提问，直接调用 C 智能体返回商品编码答案。

## 依赖

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

## 配置

支持两种配置方式：

- 一个统一密钥：`FASTGPT_API_KEY`
- 三个独立密钥：`AGENT_A_KEY`、`AGENT_B_KEY`、`AGENT_C_KEY`

还需要：

- `FASTGPT_API_URL`
- 可选：`AGENT_A_ID`、`AGENT_B_ID`、`AGENT_C_ID`

## 本地测试

```bash
python skill.py --session-id test-user-1 --message "查一下苹果的手机壳编码"
python skill.py --session-id test-user-1 --message "13812345678"
python skill.py --session-id test-user-1 --message "华为充电器编码呢？"
```

SQLite 数据文件默认是当前目录下的 `users.db`。


path: config.py
content: import os
from dotenv import load_dotenv

load_dotenv()


class Config:
    API_URL = os.getenv("FASTGPT_API_URL", "").strip()
    REQUEST_TIMEOUT = int(os.getenv("FASTGPT_TIMEOUT", "60").strip() or "60")

    # Allow a shared key or per-agent keys.
    FASTGPT_API_KEY = os.getenv("FASTGPT_API_KEY", "").strip()
    AGENT_A_KEY = os.getenv("AGENT_A_KEY", "").strip()
    AGENT_B_KEY = os.getenv("AGENT_B_KEY", "").strip()
    AGENT_C_KEY = os.getenv("AGENT_C_KEY", "").strip()

    AGENT_A_ID = os.getenv("AGENT_A_ID", "").strip()
    AGENT_B_ID = os.getenv("AGENT_B_ID", "").strip()
    AGENT_C_ID = os.getenv("AGENT_C_ID", "").strip()

    DB_PATH = os.path.join(os.path.dirname(__file__), "users.db")

    @classmethod
    def key_for_agent(cls, agent_name: str) -> str:
        if agent_name == "A":
            return cls.AGENT_A_KEY or cls.FASTGPT_API_KEY
        if agent_name == "B":
            return cls.AGENT_B_KEY or cls.FASTGPT_API_KEY
        if agent_name == "C":
            return cls.AGENT_C_KEY or cls.FASTGPT_API_KEY
        return cls.FASTGPT_API_KEY

    @classmethod
    def validate(cls):
        if not cls.API_URL:
            raise ValueError("FASTGPT_API_URL 未配置")
        if not (cls.key_for_agent("A") and cls.key_for_agent("B") and cls.key_for_agent("C")):
            raise ValueError("请配置 FASTGPT_API_KEY 或 AGENT_A_KEY/AGENT_B_KEY/AGENT_C_KEY")


path: db.py
content: import sqlite3
from datetime import datetime
from typing import Optional

from config import Config


class Database:
    def __init__(self):
        self.db_path = Config.DB_PATH
        self._init_table()

    def _init_table(self):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    session_id TEXT,
                    phone TEXT NOT NULL DEFAULT '',
                    pending_question TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
                """
            )

            columns = {
                row[1]: row for row in conn.execute("PRAGMA table_info(users)").fetchall()
            }
            if "session_id" not in columns:
                conn.execute("ALTER TABLE users ADD COLUMN session_id TEXT")
            if "pending_question" not in columns:
                conn.execute("ALTER TABLE users ADD COLUMN pending_question TEXT")

            conn.execute(
                "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_session_id ON users(session_id)"
            )
            conn.execute("CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone)")

    def get_phone(self, session_id: str) -> Optional[str]:
        with sqlite3.connect(self.db_path) as conn:
            row = conn.execute(
                "SELECT phone FROM users WHERE session_id = ? LIMIT 1",
                (session_id,),
            ).fetchone()
            if not row:
                return None
            return row[0] or None

    def set_pending_question(self, session_id: str, question: str) -> None:
        now = self._now()
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                INSERT INTO users (session_id, phone, pending_question, created_at, updated_at)
                VALUES (?, '', ?, ?, ?)
                ON CONFLICT(session_id) DO UPDATE SET
                    pending_question = excluded.pending_question,
                    updated_at = excluded.updated_at
                """,
                (session_id, question, now, now),
            )

    def pop_pending_question(self, session_id: str) -> Optional[str]:
        with sqlite3.connect(self.db_path) as conn:
            row = conn.execute(
                "SELECT pending_question FROM users WHERE session_id = ? LIMIT 1",
                (session_id,),
            ).fetchone()
            pending = row[0] if row and row[0] else None
            conn.execute(
                "UPDATE users SET pending_question = NULL, updated_at = ? WHERE session_id = ?",
                (self._now(), session_id),
            )
            return pending

    def save_phone(self, session_id: str, phone: str) -> None:
        now = self._now()
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                INSERT INTO users (session_id, phone, created_at, updated_at)
                VALUES (?, ?, ?, ?)
                ON CONFLICT(session_id) DO UPDATE SET
                    phone = excluded.phone,
                    updated_at = excluded.updated_at
                """,
                (session_id, phone, now, now),
            )

    @staticmethod
    def _now() -> str:
        return datetime.now().isoformat(sep=" ", timespec="seconds")


db = Database()


path: fastgpt_client.py
content: import json
import time
import uuid
from typing import Any, Optional

import requests


class FastGPTError(RuntimeError):
    pass


class FastGPTClient:
    def __init__(
        self, api_url: str, api_key: str, agent_id: str = "", timeout: int = 60
    ):
        self.api_url = api_url
        self.[REDACTED_SECRET]
        self.agent_id = agent_id
        self.timeout = timeout
        self.headers = {
            "Authorization": self._normalize_bearer(api_key),
            "Content-Type": "application/json",
        }

    def call(
        self,
        content: str,
        variables: Optional[dict[str, Any]] = None,
        chat_id: Optional[str] = None,
    ) -> str:
        payload_variables = dict(variables or {})
        if self.agent_id:
            payload_variables.setdefault("agent_id", self.agent_id)

        payload: dict[str, Any] = {
            "chatId": chat_id or f"goodsclassify-{int(time.time() * 1000)}",
            "stream": False,
            "detail": False,
            "responseChatItemId": str(uuid.uuid4()),
            "variables": payload_variables,
            "messages": [{"role": "user", "content": content}],
        }
        if self.agent_id:
            payload["appId"] = self.agent_id

        try:
            response = requests.post(
                self.api_url,
                headers=self.headers,
                json=payload,
                timeout=self.timeout,
            )
            response.raise_for_status()
        except requests.Timeout as exc:
            raise FastGPTError("FastGPT request timed out") from exc
        except requests.RequestException as exc:
            raise FastGPTError(f"FastGPT request failed: {exc}") from exc

        try:
            result = response.json()
        except ValueError as exc:
            raise FastGPTError(f"FastGPT returned invalid JSON: {response.text}") from exc

        content_text = self._extract_content(result)
        if content_text is None:
            if isinstance(result, (dict, list)):
                return json.dumps(result, ensure_ascii=False)
            raise FastGPTError(f"Unable to parse FastGPT response: {result}")
        return content_text

    def check_phone_exists(self, phone: str, session_id: str) -> bool:
        response = self.call(
            phone,
            variables={},
            chat_id=f"{session_id}-agent-a",
        )
        parsed = self._try_parse_json(response)
        if isinstance(parsed, list):
            return len(parsed) > 0
        if isinstance(parsed, dict):
            if "exists" in parsed:
                return bool(parsed["exists"])
            data = parsed.get("data")
            if isinstance(data, list):
                return len(data) > 0

        normalized = response.strip().lower()
        return (
            "\u4e0d\u5b58\u5728" not in response
            and ("\u5b58\u5728" in response or "exists" in normalized)
        )

    def save_phone(self, phone: str, session_id: str) -> bool:
        response = self.call(
            phone,
            variables={},
            chat_id=f"{session_id}-agent-b",
        )
        parsed = self._try_parse_json(response)
        if isinstance(parsed, dict):
            affected_rows = parsed.get("affectedRows")
            changed_rows = parsed.get("changedRows")
            if isinstance(affected_rows, int) and affected_rows >= 1:
                return True
            if isinstance(changed_rows, int) and changed_rows >= 1:
                return True
            if "error" in parsed or "errors" in parsed:
                return False
            if "choices" in parsed and "id" in parsed:
                return True

        normalized = response.strip().lower()
        return (
            "\u5931\u8d25" not in response
            and ("\u6210\u529f" in response or "success" in normalized)
        )

    def query_product(self, question: str, session_id: str, phone: str) -> str:
        return self.call(
            question,
            variables={"phone": phone, "session_id": session_id},
            chat_id=f"{session_id}-agent-c",
        )

    @staticmethod
    def _extract_content(result: Any) -> Optional[str]:
        if isinstance(result, dict):
            choices = result.get("choices")
            if isinstance(choices, list) and choices:
                message = choices[0].get("message")
                if isinstance(message, dict):
                    content = message.get("content")
                    if isinstance(content, str) and content.strip():
                        return content.strip()

            for key in ("text", "content", "data", "message"):
                value = result.get(key)
                if isinstance(value, str) and value.strip():
                    return value.strip()

        if isinstance(result, str) and result.strip():
            return result.strip()
        return None

    @staticmethod
    def _normalize_bearer(token: str) -> str:
        cleaned = token.strip()
        if cleaned.lower().startswith("bearer "):
            return cleaned
        return f"Bearer {cleaned}"

    @staticmethod
    def _try_parse_json(text: str) -> Optional[Any]:
        try:
            return json.loads(text)
        except Exception:
            return None


path: requirements.txt
content: requests==2.31.0
python-dotenv==1.0.0

path: skill.py
content: #!/usr/bin/env python3

import argparse
import os
import re
import sys

from config import Config
from db import db
from fastgpt_client import FastGPTClient, FastGPTError


class ProductSkill:
    PHONE_PATTERN = re.compile(r"^1[3-9]\d{9}$")

    def __init__(self):
        Config.validate()
        self.agent_a = FastGPTClient(
            Config.API_URL,
            Config.key_for_agent("A"),
            Config.AGENT_A_ID,
            Config.REQUEST_TIMEOUT,
        )
        self.agent_b = FastGPTClient(
            Config.API_URL,
            Config.key_for_agent("B"),
            Config.AGENT_B_ID,
            Config.REQUEST_TIMEOUT,
        )
        self.agent_c = FastGPTClient(
            Config.API_URL,
            Config.key_for_agent("C"),
            Config.AGENT_C_ID,
            Config.REQUEST_TIMEOUT,
        )

    def handle_message(self, message: str, session_id: str) -> str:
        message = message.strip()
        if not message:
            return "\u95ee\u9898\u4e0d\u80fd\u4e3a\u7a7a\u3002"

        phone = db.get_phone(session_id)
        if not phone:
            return self._handle_first_contact(message, session_id)

        return self._query_product(message, session_id, phone)

    def _handle_first_contact(self, message: str, session_id: str) -> str:
        if not self.is_valid_phone(message):
            db.set_pending_question(session_id, message)
            return "\u8bf7\u5148\u63d0\u4f9b\u624b\u673a\u53f7\uff08\u4ec5\u9996\u6b21\u9700\u8981\uff09"

        registered_message = self._process_phone_registration(message, session_id)
        pending_question = db.pop_pending_question(session_id)
        if not pending_question:
            return (
                f"{registered_message}\n"
                "\u624b\u673a\u53f7\u5df2\u4fdd\u5b58\uff0c\u8bf7\u7ee7\u7eed\u8f93\u5165\u5546\u54c1\u95ee\u9898\u3002"
            )

        try:
            answer = self._query_product(pending_question, session_id, message)
            return f"{registered_message}\n{answer}"
        except FastGPTError:
            return (
                f"{registered_message}\n"
                "\u5df2\u8bb0\u5f55\u60a8\u4e0a\u4e00\u6761\u5546\u54c1\u95ee\u9898\uff0c"
                "\u4f46\u5546\u54c1\u7f16\u7801\u67e5\u8be2\u6682\u65f6\u8d85\u65f6\uff0c"
                "\u8bf7\u76f4\u63a5\u518d\u95ee\u4e00\u6b21\u3002"
            )

    def _process_phone_registration(self, phone: str, session_id: str) -> str:
        exists = self.agent_a.check_phone_exists(phone, session_id)
        if exists:
            db.save_phone(session_id, phone)
            return "\u624b\u673a\u53f7\u9a8c\u8bc1\u901a\u8fc7"

        saved = self.agent_b.save_phone(phone, session_id)
        if not saved:
            raise FastGPTError(
                "\u624b\u673a\u53f7\u767b\u8bb0\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"
            )

        db.save_phone(session_id, phone)
        return "\u624b\u673a\u53f7\u5df2\u767b\u8bb0\uff0c\u611f\u8c22\u53c2\u4e0e"

    def _query_product(self, question: str, session_id: str, phone: str) -> str:
        return self.agent_c.query_product(question, session_id, phone)

    @classmethod
    def is_valid_phone(cls, phone: str) -> bool:
        return bool(cls.PHONE_PATTERN.match(phone))


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="goods classify helper")
    parser.add_argument("--message", required=True, help="current user message")
    parser.add_argument("--session-id", help="user session id")
    return parser.parse_args()


def resolve_session_id(args: argparse.Namespace) -> str:
    if args.session_id:
        return args.session_id

    for key in (
        "OPENCLAW_SESSION_ID",
        "OPENCLAW_CHAT_ID",
        "OPENCLAW_CONVERSATION_ID",
        "LOBSTER_USER_ID",
        "WECHAT_SESSION_ID",
    ):
        value = os.getenv(key, "").strip()
        if value:
            return value

    return "openclaw-default-session"


def main() -> int:
    try:
        args = parse_args()
        skill = ProductSkill()
        session_id = resolve_session_id(args)
        result = skill.handle_message(args.message, session_id)
        print(result)
        return 0
    except ValueError as exc:
        print(f"\u914d\u7f6e\u9519\u8bef: {exc}", file=sys.stderr)
        return 1
    except FastGPTError as exc:
        print(f"\u8bf7\u6c42\u5931\u8d25: {exc}", file=sys.stderr)
        return 1
    except Exception as exc:
        print(f"\u672a\u77e5\u9519\u8bef: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


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