ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SKills实战 - 之将自己编写的 Skill 连同依赖一键打包,方便其他团队一键 import 运行(TaoToken 统一 Key 接入版)

SKills实战 - 之将自己编写的 Skill 连同依赖一键打包,方便其他团队一键 import 运行(TaoToken 统一 Key 接入版) 1. 为什么 Skill 交付总在“最后一公里”翻车你写了一个能自动解析财报、生成周报的 Skill本地跑得飞起。同事拉过去ModuleNotFoundError、manifest.yaml校验失败、模板文件路径 404三连击直接劝退。问题不在代码而在交付形态——你给的是“一堆文件”不是“一个可 import 的包”。Skill 的本质是给 Codex CLI 这类智能体运行时调用的原子能力单元它需要 manifest 元数据、入口代码、静态资源、依赖清单四件套齐全且路径关系严格自洽。跨团队交付时接收方环境里装了什么版本的 pandas、有没有那个模板目录你完全不可控。手动pip install加复制粘贴等于把环境一致性赌在对方运维水平上。我试过最省事的做法把 Skill 目录连同requirements.txt打成一个.tar.gz接收方解压后一条命令装依赖、一条命令 import 进 Codex CLI中间不需要问任何问题。这篇文章就把这套打包骨架和 Codex CLI 侧的接入配置完整拆开包括怎么让所有调用统一走 TaoToken 的 Key/API 通道避免每个团队各自管一堆密钥。适合谁看手里有自研 Skill、需要交付给其他团队或部署到多台机器的 Python 开发者正在用 Codex CLI 做 Agent 编排、想把 Skill 当资产管理的工程师。2. 前置准备TaoToken 统一 Key 与 Codex CLI 环境在打包之前先把“调用通道”这件事定下来。Skill 打包解决的是代码和依赖的交付但 Skill 被 Codex CLI 调用时底层模型请求走哪条通道、用哪个 Key如果不统一跨团队交付后对方还得自己配一遍密钥等于没交付完整。TaoToken 在这里的角色是统一 API 入口你申请一个 KeyCodex CLI 的模型请求通过https://taotoken.net/api发出团队内所有 Skill 共用这一套凭证和计费。这样打包出去的 Skill 不绑定任何个人密钥接收方只需要在本地配置一次。2.1 获取 API Key访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进入控制台在 API Keys 页面创建一个新 Key。建议按团队或项目维度创建方便后续用量归因。创建入口https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewriteutm_content拿到形如sk-xxxxxxxx的 Key 后不要写进代码或打包进 Skill而是放在 Codex CLI 的配置文件里下一节会给具体片段。2.2 Codex CLI 安装与版本确认确保本地 Codex CLI 可用codex --version # 预期输出类似codex-cli 0.9.x如果还没装按官方文档走一遍即可。装完后先确认它能读到配置文件后面接入 TaoToken 通道时要用。2.3 目录约定本文假设你的 Skill 源码目录长这样后面所有配置都基于这个结构my_finance_skill/ ├── manifest.yaml ├── skill_code.py ├── templates/ │ └── report_template.json └── README.mdmanifest.yaml是 Codex CLI 识别 Skill 的入口元数据skill_code.py是实际被调用的逻辑templates/放静态资源。打包工具要做的就是把这个目录变成自包含的.tar.gz。3. 可复制的打包骨架pyproject 入口文件 依赖解析这一节给的是能直接抄的配置。核心思路用pyproject.toml声明打包工具自身的元信息用入口脚本build_skill.py完成依赖解析、资源收集、压缩三步。3.1 pyproject.toml在打包工具项目根目录建pyproject.toml[build-system] requires [setuptools68, wheel] build-backend setuptools.build_meta [project] name skill-bundler version 0.1.0 description 将自研 Skill 连同依赖打包为可 import 的 tar.gz requires-python 3.10 dependencies [ PyYAML6.0, ] [project.scripts] skill-bundler skill_bundler.cli:main [tool.setuptools.packages.find] where [src]这里只依赖PyYAML因为要解析manifest.yaml。依赖解析部分我们用标准库ast加subprocess调pip freeze不引入额外包减少打包工具自身的环境负担。3.2 入口文件依赖解析与资源收集建src/skill_bundler/cli.py核心逻辑分四块。先看依赖解析用 AST 扫出顶层 import再和当前环境的pip freeze结果匹配版本import ast import sys import subprocess import os import fnmatch import shutil import tarfile import tempfile import json import datetime import yaml IGNORE_PATTERNS [ *.pyc, __pycache__, .git, .DS_Store, *.swp, .idea, .vscode, venv, .venv, node_modules, ] def extract_imports(file_path): with open(file_path, r, encodingutf-8) as f: tree ast.parse(f.read()) imports [] for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: name alias.name.split(.)[0] if name not in imports: imports.append(name) elif isinstance(node, ast.ImportFrom): if node.module: name node.module.split(.)[0] if name not in imports: imports.append(name) return imports def installed_packages(): result subprocess.run( [sys.executable, -m, pip, freeze], capture_outputTrue, textTrue, ) pkgs {} for line in result.stdout.splitlines(): if in line: name, ver line.split(, 1) pkgs[name.strip().lower()] ver.strip() return pkgs def generate_requirements(skill_dir): py_files [] for root, _, files in os.walk(skill_dir): for f in files: if f.endswith(.py): py_files.append(os.path.join(root, f)) all_imports [] for pf in py_files: all_imports.extend(extract_imports(pf)) installed installed_packages() lines [] for imp in sorted(set(all_imports)): if imp in sys.stdlib_module_names: continue if imp.lower() in installed: lines.append(f{imp}{installed[imp.lower()]}) else: lines.append(imp) return \n.join(lines)这段代码的关键点sys.stdlib_module_names过滤标准库避免把os、json写进 requirementspip freeze匹配版本保证接收方装到和你一致的版本减少“在我机器上能跑”的问题。3.3 资源收集与忽略规则def should_ignore(path): basename os.path.basename(path) for pattern in IGNORE_PATTERNS: if fnmatch.fnmatch(basename, pattern): return True if pattern in path.split(os.sep): return True return False def collect_resources(src_dir, dst_dir): for root, dirs, files in os.walk(src_dir): dirs[:] [d for d in dirs if not should_ignore(d)] for f in files: src_path os.path.join(root, f) rel os.path.relpath(src_path, src_dir) if should_ignore(rel): continue dst_path os.path.join(dst_dir, rel) os.makedirs(os.path.dirname(dst_path), exist_okTrue) shutil.copy2(src_path, dst_path)dirs[:]这行是原地修改让os.walk跳过整个忽略目录比逐个文件判断高效。3.4 打包主流程def build_bundle(skill_dir, output_path): tmp tempfile.mkdtemp() bundle_name os.path.basename(skill_dir.rstrip(/)) out_dir os.path.join(tmp, bundle_name) os.makedirs(out_dir) try: req generate_requirements(skill_dir) with open(os.path.join(out_dir, requirements.txt), w, encodingutf-8) as f: f.write(req) manifest_src os.path.join(skill_dir, manifest.yaml) with open(manifest_src, r, encodingutf-8) as f: manifest yaml.safe_load(f) for field in (name, version, description, tools): if field not in manifest: raise ValueError(fmanifest.yaml 缺少字段: {field}) with open(os.path.join(out_dir, manifest.yaml), w, encodingutf-8) as f: yaml.dump(manifest, f, allow_unicodeTrue) collect_resources(skill_dir, out_dir) meta { exported_at: datetime.datetime.now().isoformat(), source: os.path.abspath(skill_dir), version: manifest.get(version, unknown), } with open(os.path.join(out_dir, .skill_metadata.json), w, encodingutf-8) as f: json.dump(meta, f, indent2, ensure_asciiFalse) with tarfile.open(output_path, w:gz) as tar: tar.add(out_dir, arcnamebundle_name) print(f打包完成: {output_path}) finally: shutil.rmtree(tmp, ignore_errorsTrue) def main(): import argparse parser argparse.ArgumentParser() parser.add_argument(skill_dir) parser.add_argument(--output, defaultdist) args parser.parse_args() os.makedirs(args.output, exist_okTrue) name os.path.basename(args.skill_dir.rstrip(/)) build_bundle(args.skill_dir, os.path.join(args.output, f{name}.tar.gz)) if __name__ __main__: main()跑起来python -m skill_bundler.cli ./my_finance_skill --output ./dist # 输出打包完成: ./dist/my_finance_skill.tar.gz到这里Skill 包已经自包含manifest、代码、模板、精确版本的 requirements 全在里面。4. Codex CLI 侧配置settings.json 与 config.toml 接入 TaoToken接收方拿到.tar.gz后需要两件事装依赖、把 Skill 注册进 Codex CLI。同时Codex CLI 的模型请求要指向 TaoToken 的统一通道。4.1 settings.json 片段Codex CLI 的settings.json通常放在~/.codex/settings.json。加入 API 通道配置{ api: { base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, timeout_seconds: 60 }, skills: { search_paths: [ ./skills, ~/.codex/skills ] } }api_key_env指向环境变量避免把 Key 明文写进配置文件。接收方在 shell 里设置export TAOTOKEN_API_KEYsk-你的Key4.2 config.toml 片段如果你的 Codex CLI 版本用config.toml对应配置[api] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY timeout_seconds 60 [skills] search_paths [./skills, ~/.codex/skills]两个文件按你本地 Codex CLI 实际读取的那个来配不要同时写避免优先级混乱。4.3 导入 Skill 包接收方操作tar -xzf my_finance_skill.tar.gz cd my_finance_skill pip install -r requirements.txt然后把解压后的目录放到search_paths覆盖的路径下比如./skills/my_finance_skill。Codex CLI 启动时会扫描该目录读取manifest.yaml完成注册。5. 验证请求跑通最小调用链路配置完成后用最小动作验证整条链路Codex CLI 能发现 Skill、能通过 TaoToken 通道发出模型请求、Skill 逻辑能执行。5.1 确认 Skill 被识别codex skill list # 预期输出包含my_finance_skill v1.0.0如果列表里没有检查search_paths路径是否正确、manifest.yaml的name字段是否存在。5.2 发起一次真实调用codex run 用 my_finance_skill 分析 AAPL 最近一个月走势这条命令会触发 Codex CLI 通过https://taotoken.net/api发送模型请求模型决定调用my_finance_skill的analyze_stock工具Skill 代码执行后返回结果。5.3 验证 API 通道单独测一下通道是否通curl -s https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY | head -c 300返回模型列表 JSON 即通道正常。如果返回 401检查 Key 是否设置正确返回超时检查网络到taotoken.net的连通性。5.4 验证依赖完整性在干净环境里验证打包是否真的自包含python -m venv /tmp/clean_env source /tmp/clean_env/bin/activate pip install -r requirements.txt python -c import skill_code; print(import ok)import ok说明依赖清单没有遗漏。这一步是跨团队交付的底线建议每次打包后都跑一遍。6. 本篇常见错排查6.1 ModuleNotFoundError: No module named xxx最常见。原因通常是 AST 解析漏掉了动态 import比如importlib.import_module(yfinance)这种写法 AST 扫不到。解决办法在 Skill 目录下放一个extra_requirements.txt打包时合并进去。def merge_extra_requirements(skill_dir, base_req): extra_path os.path.join(skill_dir, extra_requirements.txt) if os.path.exists(extra_path): with open(extra_path, r, encodingutf-8) as f: extra f.read().strip() return base_req \n extra return base_req6.2 manifest.yaml 校验失败Codex CLI 对 manifest 的 schema 有要求。常见错误tools字段写成了字典而不是列表、file_paths里的路径是绝对路径。打包时统一转成相对路径for tool in manifest.get(tools, []): if file_paths in tool: tool[file_paths] [os.path.basename(p) for p in tool[file_paths]]6.3 模板文件路径 404Skill 代码里如果用了open(templates/report_template.json)这种相对路径运行时的工作目录不一定是 Skill 根目录。改成基于__file__解析import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) template_path os.path.join(BASE_DIR, templates, report_template.json)6.4 Codex CLI 读不到 Skill按顺序查search_paths是否包含解压目录的父级manifest.yaml是否在 Skill 根目录name字段是否有非法字符。可以用codex skill validate ./my_finance_skill单独校验。6.5 API 请求 401 或超时401 检查TAOTOKEN_API_KEY环境变量是否在当前 shell 生效echo $TAOTOKEN_API_KEY确认。超时检查base_url是否写成了https://taotoken.net/api注意不要多加/v1路径由 Codex CLI 拼接。如果团队内多人共用建议在 TaoToken 控制台按成员创建独立 Key方便排查和用量管理。7. 把 Skill 当资产管起来打包只是第一步。真正让 Skill 在团队间流动起来还需要两件事一是把.tar.gz传到内部制品库或 Git 仓库的 Release 里带上版本号二是把 Codex CLI 的接入配置写成团队模板新成员 clone 下来改一个环境变量就能跑。TaoToken 的统一 Key 通道在这里省掉的是“每个 Skill 各自配密钥”的重复劳动。你打包出去的 Skill 不携带任何凭证接收方只需要在 Codex CLI 配置里指向同一个base_url用自己环境里的 Key 即可。这样 Skill 包可以公开在团队内分发密钥管理收敛到 TaoToken 控制台一处。下一步动作拿你手头最成熟的那个 Skill跑一遍python -m skill_bundler.cli把生成的.tar.gz发给一个同事让他按第 4、5 节的步骤走一遍。如果他能不问你任何问题就跑通这套交付流程就算立住了。
返回列表