Skill: shawn92/ftshare-news-data (version 1.0.1)

--- name: FTShare-news-data description: 新闻语义搜索技能集（market.ft.tech）。根据搜索文字进行语义搜索，返回相关新闻列表。用户询问语义搜新闻、按关键词搜新闻时使用。数据仅支持当年、最近半个月。 --- # FT 新闻数据 Skills 本 skill 是 `FTShare-news-data` 的**统一路由入口**。 根据用户问题，从下方「能力总览」或「询问方式与子 skill 对应表」匹配对应子 skill，然后通过 `run.py` 执行并解析响应。 > 所有接口均以 `https://market.ft.tech` 为基础域名，本技能集子 skill 无需额外请求头。 --- ## 调用方式（唯一规则） `run.py` 与本文件（`SKILL.md`）位于同一目录。执行时： 1. 取本文件的绝对路径，将末尾 `/SKILL.md` 替换为 `/run.py`，得到 `<RUN_PY>` 。 2. 调用：`python <RUN_PY> <子skill名> [参数...]` ```bash # 示例（<RUN_PY> 为实际绝对路径） python <RUN_PY> semantic-search-news --query 人工智能 python <RUN_PY> semantic-search-news --query 人工智能 --limit 10 --year 2026 python <RUN_PY> semantic-search-news --query 人工智能 --limit 10 --year 2026 --start_time 2026-03-01T00:00:00+08:00 --end_time 2026-03-15T23:59:59+08:00 ``` > `run.py` 内部通过 `__file__` 自定位，无论安装在何处都能正确找到各子 skill 的脚本。 --- ## 新闻 — 询问方式与子 skill 对应表 | 询问方式（用户常说的词） | 子 skill | |------------------------|----------| | **语义搜索新闻**、**按关键词搜新闻**、**搜索相关新闻** | `semantic-search-news` | --- ## 能力总览 - **`semantic-search-news`**：根据搜索文字进行语义搜索，返回相关新闻列表。数据仅支持**当年**、**最近半个月**内的新闻。必填：`--query`（搜索文字）；可选：`--limit`（返回条数，默认 10）、`--year`（年份，仅支持当年）。展示时需包含来源（source_site）与文章链接（article_url），并提示用户数据仅半个月内。 --- ## 使用流程 1. **记录本文件绝对路径**，将 `/SKILL.md` 替换为 `/run.py` 得到 `<RUN_PY>`。 2. **理解用户意图**，从「询问方式与子 skill 对应表」或「能力总览」匹配子 skill 名称。 3. （可选）读取 `sub-skills/<子skill名>/SKILL.md` 了解接口与参数。 4. **执行**：`python <RUN_PY> <子skill名> [参数...]`，获取 JSON 输出。 5. **解析并输出**：按子 skill 要求展示来源、链接，并提示「以下结果仅展示当年、最近半个月以内的新闻。」

path: run.py
content: #!/usr/bin/env python3
"""
FTShare-news-data 统一调度入口（新闻 / 语义搜索）。

用法：
    python run.py <subskill名> [handler参数...]

示例：
    python run.py semantic-search-news --query 人工智能
    python run.py semantic-search-news --query 人工智能 --limit 10 --year 2026
"""
import os
import runpy
import sys

SKILL_ROOT = os.path.dirname(os.path.abspath(__file__))


def _allowed_subskills():
    """仅允许 sub-skills 目录下存在 scripts/handler.py 的名称，防止路径遍历。"""
    sub_skills_dir = os.path.join(SKILL_ROOT, "sub-skills")
    allowed = set()
    if not os.path.isdir(sub_skills_dir):
        return allowed
    for name in os.listdir(sub_skills_dir):
        if os.path.isfile(os.path.join(sub_skills_dir, name, "scripts", "handler.py")):
            allowed.add(name)
    return allowed




def _execute_handler(handler: str, handler_args: list) -> int:
    """在当前进程执行 handler.py，避免 subprocess 告警。"""
    old_cwd = os.getcwd()
    old_argv = sys.argv[:]
    try:
        os.chdir(SKILL_ROOT)
        sys.argv = [handler] + handler_args
        runpy.run_path(handler, run_name="__main__")
        return 0
    except SystemExit as e:
        code = e.code
        if isinstance(code, int):
            return code
        return 1 if code else 0
    finally:
        sys.argv = old_argv
        os.chdir(old_cwd)

def main():
    sub_skills_dir = os.path.join(SKILL_ROOT, "sub-skills")
    allowed = _allowed_subskills()

    if len(sys.argv) < 2:
        print("用法: python run.py <subskill名> [参数...]", file=sys.stderr)
        print("\n可用 subskill：")
        for name in sorted(allowed):
            print(f"  {name}")
        sys.exit(1)

    subskill = sys.argv[1]
    if subskill not in allowed:
        print(f"错误：未找到 subskill '{subskill}'，或名称不合法。", file=sys.stderr)
        sys.exit(1)

    handler = os.path.join(sub_skills_dir, subskill, "scripts", "handler.py")

    exit_code = _execute_handler(handler, sys.argv[2:])
    sys.exit(exit_code)


if __name__ == "__main__":
    main()


path: sub-skills/semantic-search-news/scripts/handler.py
content: #!/usr/bin/env python3
"""语义搜索新闻（market.ft.tech）"""
import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
SAFE_URLOPENER = urllib.request.build_opener()

BASE_URL = "https://market.ft.tech"
BEIJING_TZ = timezone(timedelta(hours=8))


def _normalize_to_beijing_iso(value: str) -> str:
    """
    将输入时间规范为东八区 ISO 8601 字符串。
    - 无时区：按东八区解释
    - 有时区：统一转换到东八区
    """
    raw = value.strip()
    if not raw:
        return raw
    # 兼容 Z 结尾
    if raw.endswith("Z"):
        raw = raw[:-1] + "+00:00"
    dt = datetime.fromisoformat(raw)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=BEIJING_TZ)
    else:
        dt = dt.astimezone(BEIJING_TZ)
    return dt.isoformat()


def main():
    parser = argparse.ArgumentParser(description="语义搜索新闻")
    parser.add_argument(
        "--query",
        required=True,
        help="搜索文字，如：人工智能",
    )
    parser.add_argument(
        "--limit",
        type=int,
        default=10,
        help="返回条数，默认 10",
    )
    parser.add_argument(
        "--year",
        type=int,
        default=None,
        help="年份，仅支持当年，用于限定搜索范围；不传则由服务端决定",
    )
    parser.add_argument(
        "--start_time",
        default=None,
        help="开始时间（含），ISO 8601，如 2026-03-01T00:00:00+08:00",
    )
    parser.add_argument(
        "--end_time",
        default=None,
        help="结束时间（含），ISO 8601，如 2026-03-15T23:59:59+08:00",
    )
    args = parser.parse_args()

    params = {"query": args.query, "limit": args.limit}
    if args.year is not None:
        params["year"] = args.year
    try:
        if args.start_time:
            params["start_time"] = _normalize_to_beijing_iso(args.start_time)
        if args.end_time:
            params["end_time"] = _normalize_to_beijing_iso(args.end_time)
    except ValueError as e:
        print(f"时间格式错误：{e}", file=sys.stderr)
        sys.exit(1)

    url = BASE_URL + "/data/api/v1/market/data/semantic-search-news?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, method="GET")

    try:
        with SAFE_URLOPENER.open(req) as resp:
            data = json.loads(resp.read().decode())
        print(json.dumps(data, ensure_ascii=False, indent=2))
    except urllib.error.HTTPError as e:
        body = e.read().decode()
        print(f"HTTP {e.code}: {body}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


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