소스 검색

feat: 发布打包 + 清除全部内部泄漏

新增 scripts/pack-release.mjs:生成可直接上传 SkillHub 的 zip
  - 打包前自检密钥/内网IP/内部路径,命中即失败
  - 排除内部生成脚本、缓存、.git

安全修复:
- 删除 skill-video-lapian/scripts/publish_via_obs.py(含云机明文 root 密码、内部 obsutil 路径、主账套信息)
- 内部 gen-*.py/sh 脚本归档到 .legacy,不进包
- 根 SKILL.md 重写:13 技能清单 + 品牌统一 + 本地文件不再分散

发布包:release/fmode-skills-1.1.0-20261031.zip(343KB,107 文件,敏感自检 0)
liuyuyang 1 일 전
부모
커밋
d2ec66177b

+ 1 - 7
.gitignore

@@ -1,12 +1,6 @@
-# 打包缓存(各技能源仓库的本地 clone)
 .cache/
-
-# 旧版遗留(已归档,不入库)
 .legacy/
-
-# 依赖
+release/
 node_modules/
-
-# 系统文件
 .DS_Store
 *.log

+ 71 - 0
SKILL.md

@@ -0,0 +1,71 @@
+---
+slug: fmode-skill-fmode-skills
+displayName: fmode-skills
+version: 1.1.0
+name: fmode-skills
+description: |
+  Fmode 技能库(Meta 技能):一次安装,全部子技能自动落盘。
+  包含 13 个可直接调用的能力技能:对象存储上传与公开分享、视觉分析、录音转写、
+  图片生成、产品研发方法、HTML 长页报告、异构协同调度、交付物上报、运行日志上报、
+  短视频拉片、视频剪辑、Lit 无构建前端开发、网页智能体开发。
+  安装后自动按宿主分发到 ~/.workbuddy/skills/ 或 ~/.claude/skills/。
+  触发词: 安装技能、技能库、Fmode 技能、数字分身、云资源、发布上线、对象存储、
+  视觉识别、录音转写、拉片、短视频、报告、前端开发、智能体开发
+tags: [FmodeAgent, FmodeCode, 技能库, 安装器, meta, 技能集合]
+platform: Both
+level: 系统级
+category: 平台基础设施
+icon: 📦
+license: MPL-2.0
+author: Fmode (未来飞马)
+---
+
+# Fmode 技能库(fmode-skills)
+
+一次安装,13 个能力技能自动落盘到你的 Agent 技能目录。
+
+## 安装
+
+```bash
+# 全部分发到检测到的宿主(WorkBuddy / FmodeCode / FmodeAgent)
+node scripts/installer.mjs --all
+
+# 只检查现状(不改动)
+node scripts/installer.mjs --check
+
+# 指定宿主
+node scripts/installer.mjs --host cc      # 只装到 ~/.claude/skills/
+node scripts/installer.mjs --host wb      # 只装到 ~/.workbuddy/skills/
+```
+
+## 包含的技能
+
+| 技能 | 能力 |
+|---|---|
+| `fmode-storage` | 大文件上传对象存储,拿公开分享链接(免密钥) |
+| `fmode-vision` | 图片/视频视觉分析(结构化输出) |
+| `fmode-listen` | 录音转写(多语种、说话人分离) |
+| `fmode-image` | 生成架构图/场景图/插图 |
+| `fmode-product-lab` | 新品研发:VOC+KANO+市场+定位 |
+| `fmode-report-publish` | 咨询师级 HTML 长页报告工程 |
+| `fmode-heterarchy` | 异构任务协同调度 |
+| `fmode-deliverable` | 交付物自动上报 |
+| `fmode-agent-log` | 运行日志与性能上报 |
+| `fmode-video-lapian` | 短视频逐帧拉片拆解 |
+| `fmode-video-editing` | 视频剪辑成片 |
+| `fmode-code-lit` | Lit 原生 ESM 无构建前端开发 |
+| `fmode-code-agent` | 纯 JS 网页版智能体开发 |
+
+## 调用方式
+
+每个子技能自带 `SKILL.md`(含触发词,Agent 可通过自然语言命中)与 `scripts/`。
+装好后直接用自然语言描述任务即可,Agent 会自动匹配对应技能。
+
+## 依赖说明
+
+全部技能只依赖「公开 HTTP 接口 + 用户自有 token(环境变量)+ Agent 原生能力」,
+不依赖任何桌面软件,不索取任何云厂商密钥。
+
+## License
+
+MPL-2.0 © 2026 Fmode (未来飞马)

+ 1 - 1
dist/skills-index.json

@@ -1,7 +1,7 @@
 {
   "name": "fmode-skills",
   "version": "1.2.0",
-  "builtAt": "2026-10-31T01:56:15.080Z",
+  "builtAt": "2026-10-31T03:00:32.873Z",
   "skills": [
     {
       "name": "fmode-storage",

+ 0 - 114
dist/skills/fmode-video-lapian/scripts/publish_via_obs.py

@@ -1,114 +0,0 @@
-#!/usr/bin/env python3
-"""publish_via_obs.py — publish.py 的跳板机备用通道。
-
-适用场景: /api/storage/credentials 与 /api/storage/upload 均 404(未部署)、
-容器无 CLOUD_SDK_AK/SK、无 obsutil 时 —— 借道 server.fmode.cn(云服务器)上的
-obsutil(自带 nkkj 主账套 AK/SK)上传到 storage-s3-nkkj 桶。
-
-用法: python3 publish_via_obs.py --workdir /tmp/lapian [--date 20260829]
-产物清单与 publish.py 完全一致, 上传后逐个 HEAD 校验, 写 workdir/publish.json。
-"""
-import argparse, datetime, json, os, sys, tarfile, time, urllib.request
-import paramiko
-
-CLOUD = {"host": "139.159.253.131", "user": "root", "password": "bofang666."}
-OBSUTIL = "/opt/obsutil/obsutil_linux_amd64_5.4.11/obsutil"
-BUCKET = "storage-s3-nkkj"
-ENDPOINT = "obs.cn-north-4.myhuaweicloud.com"
-# 对外链接 host 规范(2026-08-30 硬性): 必须用 s3.fmode.cn, 禁用 OBS 原生域名
-# (浏览器只有经 s3.fmode.cn 反代才能正常加载报告的相对路径资源)
-PUB = "https://s3.fmode.cn"
-
-CT = {".html": "text/html; charset=utf-8", ".json": "application/json",
-      ".mp4": "video/mp4", ".wav": "audio/wav", ".jpg": "image/jpeg"}
-
-
-def main():
-    ap = argparse.ArgumentParser()
-    ap.add_argument("--workdir", required=True)
-    ap.add_argument("--date", default=datetime.date.today().strftime("%Y%m%d"))
-    ap.add_argument("--sub", default="", help="视频子目录(默认取meta.json的aweme_id, 防同日多视频覆盖)")
-    args = ap.parse_args()
-    wd = args.workdir
-    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-    from common import get_userid
-    userid = get_userid()
-    sub = args.sub
-    if not sub:
-        try:
-            sub = (json.load(open(f"{wd}/meta.json")) or {}).get("aweme_id") or "video"
-        except Exception:
-            sub = "video"
-    base = f"user/{userid}/report/lapian/douyin/{args.date}/{sub}"
-
-    # 打包清单(与 publish.py 一致)
-    files = ["report.html", "video.mp4", "audio.wav", "transcript.json"]
-    files = [f for f in files if os.path.exists(f"{wd}/{f}")]
-    files += [f"frames/{f}" for f in sorted(os.listdir(f"{wd}/frames"))] if os.path.isdir(f"{wd}/frames") else []
-    pkg = "/tmp/lapian-pkg.tar.gz"
-    asset_map = []
-    for n in ("analysis.json", "comments.json", "meta.json", "llm_out.json"):
-        if os.path.exists(f"{wd}/{n}"):
-            asset_map.append((f"{wd}/{n}", f"assets/{n}"))
-    with tarfile.open(pkg, "w:gz") as tf:
-        for rel in files:
-            tf.add(f"{wd}/{rel}", arcname=rel)
-        for local, arc in asset_map:
-            tf.add(local, arcname=arc)
-    files += [arc for _, arc in asset_map]
-    print(f"[pkg] {len(files)} files, {os.path.getsize(pkg)/1048576:.1f} MB")
-
-    # SFTP(带重试, banner 抖动常见)
-    ssh = paramiko.SSHClient()
-    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-    for attempt in range(4):
-        try:
-            ssh.connect(CLOUD["host"], 22, CLOUD["user"], CLOUD["password"],
-                        timeout=30, banner_timeout=30)
-            break
-        except Exception as ex:
-            print(f"[ssh] attempt {attempt+1} failed: {type(ex).__name__}, retry 15s")
-            time.sleep(15)
-    else:
-        sys.exit("[ssh] all retries failed")
-    sftp = ssh.open_sftp()
-    sftp.put(pkg, "/tmp/lapian-pkg.tar.gz")
-    sftp.close()
-
-    remote = f"""
-set -e
-rm -rf /tmp/lapian-pkg && mkdir -p /tmp/lapian-pkg
-tar -xzf /tmp/lapian-pkg.tar.gz -C /tmp/lapian-pkg
-cd /tmp/lapian-pkg
-for f in {' '.join(files)}; do
-  if {OBSUTIL} cp "$f" "obs://{BUCKET}/{base}/$f" -acl=public-read -e={ENDPOINT} -j=5 > /tmp/obs_last.log 2>&1; then
-    echo "UP OK $f"
-  else
-    echo "UP FAIL $f: $(tail -2 /tmp/obs_last.log)"
-  fi
-done"""
-    _, stdout, stderr = ssh.exec_command(remote, timeout=600)
-    out = stdout.read().decode(errors="replace")
-    print(out)
-    ssh.close()
-    fails = [l for l in out.splitlines() if "UP FAIL" in l]
-
-    # 公网校验
-    bad = []
-    for rel in files:
-        try:
-            req = urllib.request.Request(f"{PUB}/{base}/{rel}", method="HEAD")
-            with urllib.request.urlopen(req, timeout=25) as r:
-                if r.status != 200:
-                    bad.append((rel, r.status))
-        except Exception as ex:
-            bad.append((rel, str(ex)[:60]))
-    report_url = f"{PUB}/{base}/report.html"
-    print(f"\n[verify] {len(files)-len(bad)}/{len(files)} OK", f"fails={bad}" if bad else "ALL PASS")
-    json.dump({"report_url": report_url, "prefix": base, "fails": bad + fails},
-              open(f"{wd}/publish.json", "w"), ensure_ascii=False, indent=2)
-    print("REPORT URL:", report_url)
-
-
-if __name__ == "__main__":
-    main()

+ 12 - 4
package.json

@@ -12,10 +12,18 @@
     "build": "node scripts/build-dist.mjs",
     "build:dry": "node scripts/build-dist.mjs --dry-run",
     "verify": "node scripts/verify-refs.mjs",
-    "clean": "rm -rf dist"
+    "clean": "rm -rf dist",
+    "pack": "node scripts/pack-release.mjs",
+    "release": "node scripts/build-dist.mjs && node scripts/verify-refs.mjs && node scripts/pack-release.mjs"
   },
   "keywords": [
-    "fmode", "harness", "skillhub", "agent-skills",
-    "未来飞马", "技能库", "智能体技能", "数字分身"
+    "fmode",
+    "harness",
+    "skillhub",
+    "agent-skills",
+    "未来飞马",
+    "技能库",
+    "智能体技能",
+    "数字分身"
   ]
-}
+}

+ 0 - 121
scripts/gen-app-skills.py

@@ -1,121 +0,0 @@
-#!/usr/bin/env python3
-"""批量生成应用层子技能(ESM 结构,含 references 指向 VOC 包)"""
-import os
-
-BASE = '/opt/data/projects/fmode-skills/skills'
-
-skills = [
-    {
-        "dir": "fmode-voc-intelligence",
-        "slug": "fmode-skill-fmode-voc-intelligence",
-        "version": "0.3.38",
-        "title": "VOC 用户经营洞察",
-        "summary": "小红书/抖音/视频号 VOC 情报:趋势情报、问题池、单点深挖、竞品图谱、7天内容计划、口播脚本、成本测算、用量账单。",
-        "triggers": "VOC、小红书趋势、抖音趋势、视频号、问题池、竞品、内容计划、口播脚本、用户声音、经营洞察、趋势情报",
-        "sub_skills": ["xiaohongshu-trend-intelligence", "douyin-trend-intelligence", "voc-issue-pool", "voc-problem-deep-dive", "voc-competitor-map", "voc-content-plan", "voc-speaking-script", "voc-cost-controller", "voc-api-catalog", "voc-business-workflow", "fmode-image-analysis"],
-        "src": "npm @vocmarket/voc-skill 0.3.38",
-    },
-    {
-        "dir": "fmode-tihao",
-        "slug": "fmode-skill-fmode-tihao",
-        "version": "1.0.19",
-        "title": "提号 SOP 达人/博主研究",
-        "summary": "达人/博主候选研究:读取brief、调用VOC电商代理、豆包视觉分析、达人排名、导出商务可用名单。",
-        "triggers": "提号、达人、博主、创作者研究、brief、商务名单、红人、kol",
-        "sub_skills": ["tihao-creator-sourcing"],
-        "src": "npm @vocmarket/tihao 1.0.19",
-    },
-    {
-        "dir": "fmode-pay",
-        "slug": "fmode-skill-fmode-pay",
-        "version": "0.1.0",
-        "title": "微信特约商户自动化进件",
-        "summary": "微信服务商特约商户进件:资料收集 → APIv3 签名 → 云函数提交 → 二维码签约 → 归档。",
-        "triggers": "微信商户、进件、特约商户、fmode-pay、商户开通、支付开通",
-        "sub_skills": ["wxpay-merchant-apply", "wxpay-merchant-status", "wxpay-merchant-upload", "wxpay-merchant-archive"],
-        "src": "本次 fmode-pay 开发",
-    },
-    {
-        "dir": "fmode-courseware",
-        "slug": "fmode-skill-fmode-courseware",
-        "version": "0.1.0",
-        "title": "课程课件生产标准",
-        "summary": "课件/报告 HTML 生产标准:委派 FmodeCode、基准学习、素材归档、证据链、分层验收。",
-        "triggers": "课件、课程、导入课、课纲、验收、skill-present",
-        "sub_skills": ["skill-present-course-production", "html-course-delivery", "html-report-delivery"],
-        "src": "skill-present 抽取",
-    },
-    {
-        "dir": "fmode-report-publish",
-        "slug": "fmode-skill-fmode-report-publish",
-        "version": "0.1.0",
-        "title": "HTML 报告制作发布",
-        "summary": "网页报告交付:手机侧边栏 + 区块化 + 数据优先,交付到 S3 公开 URL。",
-        "triggers": "出报告、HTML报告、可视化报告、S3、报告交付、发布链接",
-        "sub_skills": ["html-report-delivery", "fmode-storage"],
-        "src": "Hermes html-report-delivery 抽取",
-    },
-    {
-        "dir": "fmode-product-lab",
-        "slug": "fmode-skill-fmode-product-lab",
-        "version": "0.2.0",
-        "title": "新品研发实验室",
-        "summary": "新品研发:VOC 采集 + KANO + 市场分析 + 竞争 + 定位 + 错位竞争全流程。",
-        "triggers": "新品研发、产品开发、健康快消、VOC、KANO、市场分析、定位、竞品",
-        "sub_skills": ["product-lab-workflow"],
-        "src": "npm fmode-product-lab 0.2.0",
-    },
-]
-
-for s in skills:
-    d = os.path.join(BASE, s['dir'])
-    os.makedirs(os.path.join(d, 'scripts'), exist_ok=True)
-    os.makedirs(os.path.join(d, 'references'), exist_ok=True)
-
-    sub_lines = "\n".join(f"- `{sub}`" for sub in s['sub_skills'])
-    ref_lines = f"- 原始来源: {s['src']}"
-
-    skill_md = f"""---
-slug: {s['slug']}
-displayName: {s['dir']}
-version: {s['version']}
-name: {s['dir']}
-description: {s['summary']} 触发词:{s['triggers']}
-tags: [FmodeAgent, FmodeCode, 应用级, {s['dir'].replace('fmode-','')}]
-platform: Both
-level: 应用级
-category: 平台基础设施
-icon: 🏢
-license: MPL-2.0
-author: Fmode (未来飞马)
----
-
-# {s['title']}
-
-## 触发词
-
-{s['triggers']}
-
-## 能力分解
-
-{s['sub_skills']}
-
-{ref_lines}
-
-## 使用规则
-
-- 用户只需要自然语言描述业务场景,不要强迫了解参数
-- 未明确要求真实采集前,先使用演示样例(sample);明确 live 时再检查凭证/余额
-- 首轮输出采用"初步判断、机会假设、待校准"口径,附证据
-
-## License
-
-MPL-2.0 © 2026 Fmode (未来飞马)
-"""
-    with open(os.path.join(d, 'SKILL.md'), 'w', encoding='utf-8') as f:
-        f.write(skill_md)
-    with open(os.path.join(d, 'references', 'source.md'), 'w', encoding='utf-8') as f:
-        f.write(f"# {s['title']} 参考\n\n{ref_lines}\n")
-    print(f"✅ 生成 {s['dir']}")
-
-print("🎉 应用层子技能生成完毕")

+ 0 - 22
scripts/gen-dirs.sh

@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-# 批量生成 fmode-skills 服务层子技能骨架(ESM)
-set -e
-cd /opt/data/projects/fmode-skills/skills
-
-# 定义每个子技能: 目录|name|description(触发词+行为)
-SKILLS='
-accounts|fmode-account|fmode账号。手机验证码注册/登录/sessionToken管理。发送验证码、校验登录、校验会话、保存token到受保护profile
-enterprise|fmode-enterprise|fmode企业。企业开通状态查询、独立部署sub-standalone申请、Company订阅状态。手机号注册后确认是否在企业
-cloud-functions|fmode-cloud-functions|fmode云函数。在Function合同表部署业务云函数并通过/api/functions调用。动态函数源进Function合同表,不建用户维护服务器
-cloud-resources|fmode-cloud-resources|fmode云资源。领取/部署到华为云。调/api/functions+deploy/huaweicloud拿到平台签发STS应用到用户项目
-static-publish|fmode-static-publish|fmode静态发布。前后端构建产物发布到OBS+CDN拿公开URL。调deploy/huaweicloud+huaweicloud-cdn-refresh,obsutil同步
-git-archive|fmode-git-archive|fmode Git归档。项目源码归档到Fmode Git(git.fmode.cn)。建gogs账号personal token建仓推代码,验证SHA
-'
-
-echo "$SKILLS" | while IFS='|' read -r _name name desc; do
-  [ -z "$name" ] && continue
-  dir="$name"
-  mkdir -p "$dir/scripts" "$dir/references"
-  echo "=== 生成 $dir ==="
-done
-echo "✅ 目录骨架完成"

+ 0 - 37
scripts/gen-inventory.py

@@ -1,37 +0,0 @@
-#!/usr/bin/env python3
-"""重生成 inventory (剔除 ffmpeg/qiwei 后) + 更新文档描述"""
-import os, json, datetime
-
-BASE = '/opt/data/projects/fmode-skills/skills'
-
-# 删除旧的 inventory 保留目录
-all_skills = sorted(os.listdir(BASE))
-all_skills = [s for s in all_skills if os.path.isdir(os.path.join(BASE, s))]
-
-inventory = {
-    "name": "fmode-skills",
-    "version": "0.2.0",
-    "generatedAt": datetime.datetime.now().isoformat(),
-    "format": "esm",
-    "architecture": "纯接口驱动: 子技能只依赖公开HTTP接口+用户token(env)+agent原生工具, 不依赖任何需另行安装的软件/本地二进制",
-    "skills": [],
-}
-for name in all_skills:
-    full = os.path.join(BASE, name)
-    slug=version=level=summary=""
-    try:
-        md = open(os.path.join(full,'SKILL.md'),encoding='utf-8').read()
-        fm = md.split('---')[1]
-        for line in fm.split('\n'):
-            if line.startswith('slug:'): slug = line.split(':',1)[1].strip()
-            if line.startswith('version:'): version = line.split(':',1)[1].strip()
-            if line.startswith('level:'): level = line.split(':',1)[1].strip()
-            if line.startswith('description:'): summary = line.split(':',1)[1].strip()[:60]
-    except: pass
-    inventory["skills"].append({"name":name,"slug":slug or f"fmode-skill-{name}","version":version,"level":level,"summary":summary})
-
-with open(os.path.join(BASE,'inventory.json'),'w',encoding='utf-8') as f:
-    json.dump(inventory, f, ensure_ascii=False, indent=2)
-print(f"✅ inventory.json 更新: {len(inventory['skills'])} 个子技能")
-for s in inventory['skills']:
-    print(f"  {s['name']} [{s['level']}] v{s['version']}")

+ 0 - 86
scripts/gen-published-skills.py

@@ -1,86 +0,0 @@
-#!/usr/bin/env python3
-"""生成服务层已发布技能(fmode-vision/listen/ffmpeg/qiwei/storage) + inventory.json"""
-import os, json
-
-BASE = '/opt/data/projects/fmode-skills/skills'
-
-skills = [
-    {"dir":"fmode-vision","slug":"fmode-skill-fmode-vision","ver":"0.1.1","title":"视觉识别","summary":"图片/视频结构化视觉分析:宿主多模态优先,回落 Fmode API(豆包/Doubao)。","triggers":"看图、识图、截图分析、UI截图、视觉识别、图片文字、图像分析、doubao、vision","src":"npm fmode-vision","endpoint":"POST https://api.fmode.cn/v1/chat/completions"},
-    {"dir":"fmode-listen","slug":"fmode-skill-fmode-listen","ver":"0.1.2","title":"录音转写","summary":"录音/视频音轨转文字(讯飞 LFASR × Fmode 网关),多语种/方言/说话人分离,服务端计费。","triggers":"转写、录音、语音转文字、ASR、讯飞、说话人分离、meeting转写","src":"npm fmode-listen","endpoint":"POST https://server.fmode.cn/api/listen/transcribe"},
-    {"dir":"fmode-ffmpeg","slug":"fmode-skill-fmode-ffmpeg","ver":"0.1.1","title":"音视频处理","summary":"FFmpeg 音视频处理:抽音轨、转码、压缩、裁剪、截图。捆绑静态二进制零系统依赖。","triggers":"ffmpeg、抽音轨、转码、压缩、裁剪视频、截图、音视频处理、media","src":"npm fmode-ffmpeg","endpoint":"CLI 本地执行"},
-    {"dir":"fmode-qiwei","slug":"fmode-skill-fmode-qiwei","ver":"0.5.2","title":"企微网关 SDK","summary":"企业微信网关:消息收发、通讯录、应用管理封装。","triggers":"企微、企业微信、WeCom、qiwei、公众号、消息推送、通讯录","src":"npm fmode-qiwei","endpoint":"gateway"},
-    {"dir":"fmode-storage","slug":"fmode-skill-fmode-storage","ver":"1.1.0","title":"对象存储与公开分享","summary":"二进制大文件上云,本地零占用,一键生成公开分享链接(S3/OBS)。","triggers":"上传、存储、对象存储、S3、OBS、分享链接、公开URL、托管文件","src":"skill-storage 1.1.0","endpoint":"https://s3.fmode.cn/<key>"},
-]
-
-inven = []
-for s in skills:
-    d = os.path.join(BASE, s['dir'])
-    os.makedirs(os.path.join(d,'scripts'),exist_ok=True)
-    os.makedirs(os.path.join(d,'references'),exist_ok=True)
-    skill_md = f"""---
-slug: {s['slug']}
-displayName: {s['dir']}
-version: {s['ver']}
-name: {s['dir']}
-description: {s['summary']} 触发词:{s['triggers']}
-tags: [FmodeAgent, FmodeCode, 服务级, {s['dir'].replace('fmode-','')}]
-platform: Both
-level: 服务级
-category: 平台基础设施
-icon: 🛠️
-license: MPL-2.0
-author: Fmode (未来飞马)
----
-
-# {s['title']}
-
-## 触发词
-{s['triggers']}
-
-## 快速开始
-```bash
-node scripts/index.mjs <action>
-```
-或按 {s['src']} 的官方命令(`npx --yes {s['src'].split(' ')[-1]}@latest workspace`)。
-
-## 端点
-- {s['endpoint']}
-
-## 来源
-- {s['src']}
-
-## License
-MPL-2.0 © 2026 Fmode (未来飞马)
-"""
-    open(os.path.join(d,'SKILL.md'),'w',encoding='utf-8').write(skill_md)
-    open(os.path.join(d,'references','source.md'),'w',encoding='utf-8').write(f"# 来源\n{s['src']}\n端点: {s['endpoint']}\n")
-    inven.append({"name":s['dir'],"slug":s['slug'],"version":s['ver'],"tier":"service","source":s['src']})
-    print(f"✅ {s['dir']}")
-
-# 汇总全部技能到 inventory.json
-all_skills = sorted(os.listdir(BASE))
-inventory = {
-    "name": "fmode-skills",
-    "version": "0.1.0",
-    "generatedAt": __import__('datetime').datetime.now().isoformat(),
-    "format": "esm",
-    "skills": [],
-}
-for name in all_skills:
-    full = os.path.join(BASE, name)
-    if not os.path.isdir(full): continue
-    # 读 SKILL.md frontmatter 的 slug/version/level
-    slug=version=level=""
-    try:
-        md = open(os.path.join(full,'SKILL.md'),encoding='utf-8').read()
-        for line in md.split('\n')[:20]:
-            if line.startswith('slug:'): slug = line.split(':',1)[1].strip()
-            if line.startswith('version:'): version = line.split(':',1)[1].strip()
-            if line.startswith('level:'): level = line.split(':',1)[1].strip()
-    except: pass
-    inventory["skills"].append({"name":name,"slug":slug or f"fmode-skill-{name}","version":version,"level":level})
-
-with open('/opt/data/projects/fmode-skills/skills/inventory.json','w',encoding='utf-8') as f:
-    json.dump(inventory, f, ensure_ascii=False, indent=2)
-print(f"\n📊 inventory.json 生成: {len(inventory['skills'])} 个子技能")
-print("🎉 服务层已发布技能补齐")

+ 0 - 209
scripts/gen-service-skills.py

@@ -1,209 +0,0 @@
-#!/usr/bin/env python3
-"""批量生成 fmode-skills 服务层子技能 ESM 结构(统一规范)"""
-import os, textwrap
-
-BASE = '/opt/data/projects/fmode-skills/skills'
-
-skills = [
-    {
-        "dir": "fmode-account",
-        "name": "fmode-account",
-        "slug": "fmode-skill-fmode-account",
-        "version": "0.1.0",
-        "title": "Fmode 账号(手机验证码注册/登录)",
-        "summary": "Fmode 账号注册登录:手机验证码 → sessionToken → 企业开通。数字分身初始化第一步。",
-        "triggers": "注册、登录、验证码、sessionToken、账号、手机号、数字分身初始化、fmod账号",
-        "endpoints": [
-            ("POST https://server.fmode.cn/api/apig/message", "发送验证码 {mobile,company}"),
-            ("GET https://server.fmode.cn/api/fmode/mobile", "验证码登录 {mobile,code} → data.token/data.newapiToken"),
-            ("GET https://server.fmode.cn/parse/users/me", "校验会话 Bearer token"),
-        ],
-        "keypoints": ["token 走受保护 profile(0700),不进聊天/project/Git/日志", "验证码只用于立即登录请求", "Windows 用当前用户目录,POSIX 0700/0600"],
-    },
-    {
-        "dir": "fmode-enterprise",
-        "name": "fmode-enterprise",
-        "slug": "fmode-skill-fmode-enterprise",
-        "version": "0.1.0",
-        "title": "Fmode 企业(开通状态/独立部署)",
-        "summary": "Fmode 企业状态:查询订阅、申请独立部署、轮询开通。数字分身企业开通。",
-        "triggers": "企业、开通、订阅、enterprise、Company、独立部署、sub、trial",
-        "endpoints": [
-            ("POST https://server.fmode.cn/api/fmode/enterprise/subscription/status", "查企业状态 {sessionToken}"),
-            ("POST https://server.fmode.cn/api/fmode/enterprise/subscription/trial-standalone", "申请独立部署 {name,sessionToken}"),
-            ("POST https://server.fmode.cn/api/fmode/enterprise/subscription/provision/retry", "重试开通(仅失败后)"),
-        ],
-        "keypoints": ["不在企业时问人名(非企业名)再trial-standalone", "仅 provision pending/running 时轮询状态", "绝不索要数据库URI"],
-    },
-    {
-        "dir": "fmode-cloud-functions",
-        "name": "fmode-cloud-functions",
-        "slug": "fmode-skill-fmode-cloud-functions",
-        "version": "0.1.0",
-        "title": "Fmode 云函数(Function 合同 + /api/functions)",
-        "summary": "Fmode 云函数部署与调用:Function 合同表存动态函数源,/api/functions 发布执行。",
-        "triggers": "云函数、Function、/api/functions、后端函数、业务接口、动态函数",
-        "endpoints": [
-            ("POST https://server.fmode.cn/api/functions", "调用/注册云函数 {token, path, params}"),
-        ],
-        "keypoints": ["CRUD 不建用户维护服务器,动态函数进 Function 合同表", "handler(request,response) 签名 + 会话鉴权 + 租户校验 + 幂等", "masterKey/dbURI/平台session 绝不进浏览器 bundle"],
-    },
-    {
-        "dir": "fmode-cloud-resources",
-        "name": "fmode-cloud-resources",
-        "slug": "fmode-skill-fmode-cloud-resources",
-        "version": "0.1.0",
-        "title": "Fmode 云资源(华为云 STS 部署)",
-        "summary": "Fmode 云资源领取:平台签发项目级 STS,华为云 OBS 部署,无需用户华为账号。",
-        "triggers": "云资源、领取、部署、华为云、STS、obs、huaweicloud、deploy",
-        "endpoints": [
-            ("POST https://server.fmode.cn/api/apig/deploy/huaweicloud", "取 STS {token, projectId} → accessKey/secretKey/securityToken/expiresAt/obsPath"),
-        ],
-        "keypoints": ["STS 平台签发,非用户华为账号,用户无需华为账号/服务器", "过期(<5min)丢弃整套后重取,最多重试2次", "绝不替换为永久密钥"],
-    },
-    {
-        "dir": "fmode-static-publish",
-        "name": "fmode-static-publish",
-        "slug": "fmode-skill-fmode-static-publish",
-        "version": "0.1.0",
-        "title": "Fmode 静态发布(OBS+CDN 公开 URL)",
-        "summary": "Fmode 前后端发布:构建 → 注册 Project → STS → obsutil 同步 OBS → CDN 刷新 → 验证公开 URL。",
-        "triggers": "发布、部署、上线、静态站、前端、课件、CDN、obsutil、public URL、huaweicloud-cdn-refresh",
-        "endpoints": [
-            ("POST https://server.fmode.cn/api/functions", "注册 Project {token,path:/fmode/project-register,params:{name,version,gitUrl}}"),
-            ("POST https://server.fmode.cn/api/apig/deploy/huaweicloud", "取 STS {token,projectId}"),
-            ("POST https://server.fmode.cn/api/apig/deploy/huaweicloud-cdn-refresh", "CDN 刷新 {token,projectId,path}"),
-        ],
-        "keypoints": ["注册Project→构建→扫token→取STS→上传obsPath→CDN刷新→验证URL 逐门禁报告", "obsPath 必须是 dev/<projectId>/", "会话过期先重播登录再取新STS,不求华为凭据"],
-    },
-    {
-        "dir": "fmode-git-archive",
-        "name": "fmode-git-archive",
-        "slug": "fmode-skill-fmode-git-archive",
-        "version": "0.1.0",
-        "title": "Fmode Git 归档(Gitea + 源码入库)",
-        "summary": "Fmode 项目归档:源码进 git.fmode.cn,建 Gogs 账号/个人 token/建仓/推送/验证 SHA。",
-        "triggers": "归档、git、Gogs、Gitea、git.fmode.cn、仓库、源码入库、git archive",
-        "endpoints": [
-            ("POST https://server.fmode.cn/api/functions", "Gogs admin proxy 建用户 {token,path:/gogs/admin/proxy,params:{method:POST,path:/admin/users,...}}"),
-            ("POST https://git.fmode.cn/api/v1/users/<username>/tokens", "创建个人 token"),
-            ("POST https://git.fmode.cn/api/v1/user/repos", "创建私有仓库"),
-        ],
-        "keypoints": ["用户名强制 fmode-<phone>,请求值忽略占位", "token 绝不进 remote URL,用 HTTPS + token 密码", "排除 .env/密钥/构建产物/日志", "git ls-remote 验证远端 SHA,发布与归档分开报告"],
-    },
-]
-
-# 通用 ESM 脚本模板(fetch 封装)
-FETCH_TMPL = '''#!/usr/bin/env node
-/**
- * {title} — 辅助脚本(零 npm 依赖,Node>=18 自带 fetch)
- * Copyright (c) 未来飞马  MPL-2.0
- *
- * 用法:node index.mjs <action> [jsonPayload]
- */
-const BASE = "https://server.fmode.cn";
-
-async function call(method, path, body, token) {{
-  const headers = {{ "content-type": "application/json" }};
-  if (token) headers["authorization"] = "Bearer " + token;
-  const res = await fetch(BASE + path, {{
-    method, headers, body: body ? JSON.stringify(body) : undefined,
-  }});
-  const text = await res.text();
-  // 脱敏输出:token/密钥不打印
-  const safe = text.replace(/(token|authorization|accessKey|secretKey|securityToken)["'\\s:]+([^",}}]+)/gi, "$1:***");
-  console.log(safe);
-  return res;
-}}
-
-const [action, raw] = process.argv.slice(2);
-let payload = {{}};
-try {{ if (raw) payload = JSON.parse(raw); }} catch {{ payload = {{ _raw: raw }}; }}
-
-{actions}
-
-'''
-
-def actions_for(epoints):
-    lines = []
-    for ep in epoints:
-        method, path = ep[0].split(' ')
-        parts = path.replace('https://server.fmode.cn','').strip()
-        # 生成一个如 call("<METHOD>","<PATH>", payload) 的示例
-        lines.append(f'// 示例端点: {method} {ep[1]}')
-    return "\n".join(lines)
-
-for s in skills:
-    d = os.path.join(BASE, s['dir'])
-    os.makedirs(os.path.join(d, 'scripts'), exist_ok=True)
-    os.makedirs(os.path.join(d, 'references'), exist_ok=True)
-
-    # SKILL.md
-    ep_lines = "\n".join(f"| `{m} {p}` | {desc} |" for m, p, desc in [
-        (m, p, dsc) for m, p, dsc in [(ep[0].split(' ')[0], ep[0].split(' ')[1], ep[1]) for ep in s['endpoints']]
-    ])
-    kp_lines = "\n".join(f"- ⚠️ {k}" for k in s['keypoints'])
-
-    skill_md = f"""---
-slug: {s['slug']}
-displayName: {s['dir']}
-version: {s['version']}
-name: {s['dir']}
-description: {s['summary']} 触发词:{s['triggers']}
-tags: [FmodeAgent, FmodeCode, 服务级, {s['dir'].replace('fmode-','')}]
-platform: Both
-level: 服务级
-category: 平台基础设施
-icon: ⚙️
-license: MPL-2.0
-author: Fmode (未来飞马)
----
-
-# {s['title']}
-
-## 触发词
-
-{s['triggers']}
-
-## 快速开始(ESM)
-
-```bash
-node scripts/index.mjs <action> '{{"...": ...}}'
-```
-
-或 Agent 直接按流程调用下述端点。
-
-## 端点清单
-
-| 端点 | 作用 |
-|------|------|
-{ep_lines}
-
-## 关键纪律
-
-{kp_lines}
-
-## References
-
-- 详见 `references/` 目录
-- 完整平台规范见 skill-core-guide(`fmode-skills` 自带)
-
-## License
-
-MPL-2.0 © 2026 Fmode (未来飞马)
-"""
-    with open(os.path.join(d, 'SKILL.md'), 'w', encoding='utf-8') as f:
-        f.write(skill_md)
-
-    # scripts/index.mjs(零依赖 fetch 封装)
-    fetch_js = FETCH_TMPL.format(title=s['title'], actions=actions_for(s['endpoints']))
-    with open(os.path.join(d, 'scripts', 'index.mjs'), 'w', encoding='utf-8') as f:
-        f.write(fetch_js)
-
-    # references/endpoints.md
-    with open(os.path.join(d, 'references', 'endpoints.md'), 'w', encoding='utf-8') as f:
-        f.write(f"# {s['title']} 端点参考\n\n" + ep_lines + "\n")
-
-    print(f"✅ 生成 {s['dir']}")
-
-print("🎉 服务层子技能生成完毕")

+ 165 - 0
scripts/pack-release.mjs

@@ -0,0 +1,165 @@
+#!/usr/bin/env node
+/**
+ * pack-release.mjs —— 生成可直接上传 SkillHub 的发布 zip
+ *
+ * 产物:release/fmode-skills-<version>-<date>.zip
+ * 内容:dist/ 全部技能 + 根 SKILL.md + README + manifest(不含内部脚本、不含缓存)
+ *
+ * 上传方式:SkillHub 后台「上传技能包」→ 选这个 zip
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(__dirname, '..');
+const DIST = path.join(ROOT, 'dist');
+const RELEASE = path.join(ROOT, 'release');
+
+const C = { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' };
+
+if (!fs.existsSync(DIST)) {
+  console.error('dist 不存在,先跑 npm run build');
+  process.exit(1);
+}
+
+// 版本
+const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
+const version = pkg.version || '1.0.0';
+const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+const stamp = `${version}-${date}`;
+
+fs.mkdirSync(RELEASE, { recursive: true });
+const stageDir = path.join(RELEASE, `.stage-${stamp}`);
+fs.rmSync(stageDir, { recursive: true, force: true });
+fs.mkdirSync(stageDir, { recursive: true });
+
+/** 递归复制,跳过 .git / 缓存 / 内部脚本 */
+const SKIP = [
+  /(^|\/)\.git(\/|$)/, /(^|\/)__pycache__(\/|$)/, /(^|\/)node_modules(\/|$)/,
+  /\.pyc$/, /\.log$/, /\.bak/, /\.DS_Store$/,
+  /(^|\/)gen-.*\.(py|sh)$/,      // 内部生成脚本
+  /(^|\/)\.legacy(\/|$)/,
+  /(^|\/)\.cache(\/|$)/,
+];
+
+function copyTree(src, dest, rel = '') {
+  fs.mkdirSync(dest, { recursive: true });
+  for (const e of fs.readdirSync(src, { withFileTypes: true })) {
+    const relPath = rel ? `${rel}/${e.name}` : e.name;
+    if (SKIP.some(re => re.test(relPath))) continue;
+    const s = path.join(src, e.name);
+    const d = path.join(dest, e.name);
+    if (e.isDirectory()) copyTree(s, d, relPath);
+    else if (e.isFile()) fs.copyFileSync(s, d);
+  }
+}
+
+// 1. 复制 dist 里的技能
+copyTree(DIST, stageDir);
+
+// 2. 复制根级 meta(SKILL.md / README / manifest)
+for (const f of ['SKILL.md', 'README.md', 'skill-package-manifest.json']) {
+  const src = path.join(ROOT, f);
+  if (fs.existsSync(src)) fs.copyFileSync(src, path.join(stageDir, f));
+}
+
+// 3. 生成/更新 package.json(SkillHub 读这个)
+const outPkg = {
+  name: 'fmode-skills',
+  version,
+  description: pkg.description,
+  type: 'module',
+  license: pkg.license || 'MPL-2.0',
+  keywords: pkg.keywords,
+  engines: pkg.engines,
+  scripts: {
+    install: 'node scripts/installer.mjs --all',
+    check: 'node scripts/installer.mjs --check',
+  },
+};
+fs.writeFileSync(path.join(stageDir, 'package.json'), JSON.stringify(outPkg, null, 2) + '\n');
+
+// 4. 检查 installer 是否存在(技能安装入口)
+const installerSrc = path.join(ROOT, 'scripts', 'installer.mjs');
+if (fs.existsSync(installerSrc)) {
+  fs.mkdirSync(path.join(stageDir, 'scripts'), { recursive: true });
+  fs.copyFileSync(installerSrc, path.join(stageDir, 'scripts', 'installer.mjs'));
+}
+
+// 5. 统计 & 自检
+function walk(d, out = []) {
+  for (const e of fs.readdirSync(d, { withFileTypes: true })) {
+    const p = path.join(d, e.name);
+    if (e.isDirectory()) walk(p, out);
+    else out.push(p);
+  }
+  return out;
+}
+const files = walk(stageDir);
+const skills = fs.existsSync(path.join(stageDir, 'skills'))
+  ? fs.readdirSync(path.join(stageDir, 'skills')).filter(n =>
+      fs.statSync(path.join(stageDir, 'skills', n)).isDirectory())
+  : [];
+
+// 自检:密钥模式
+const SECRET_RE = [
+  /\bsk-[a-zA-Z0-9]{20,}/,
+  /\br:[a-z0-9]{20,}/,
+  /AKIA[0-9A-Z]{16}/,
+  /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
+  /(192\.168\.[0-9]{1,3}\.[0-9]{1,3})/,
+  /(139\.159\.253\.131|172\.0\.0\.100)/,
+  /(db\.fmode\.cn|userland\.fmode\.cn)/,
+];
+let leaks = 0;
+for (const f of files) {
+  if (!/\.(md|json|mjs|js|sh|py|txt|html|css)$/i.test(f)) continue;
+  const c = fs.readFileSync(f, 'utf8');
+  for (const re of SECRET_RE) {
+    if (re.test(c)) {
+      console.log(`${C.r}✗ 疑似敏感${C.x} ${path.relative(stageDir, f)} → ${re}`);
+      leaks++;
+    }
+  }
+}
+
+// 6. 打 zip
+const zipName = `fmode-skills-${stamp}.zip`;
+const zipPath = path.join(RELEASE, zipName);
+fs.rmSync(zipPath, { force: true });
+
+try {
+  execFileSync('zip', ['-rq', zipName, '.'], { cwd: stageDir, stdio: 'pipe' });
+} catch (e) {
+  // 无 zip 命令,用 Node 的 python 兜底
+  try {
+    execFileSync('python3', ['-c', `
+import zipfile, os
+z = zipfile.ZipFile(${JSON.stringify(zipPath)}, 'w', zipfile.ZIP_DEFLATED)
+for root, dirs, fs in os.walk(${JSON.stringify(stageDir)}):
+    for f in fs:
+        p = os.path.join(root, f)
+        z.write(p, os.path.relpath(p, ${JSON.stringify(stageDir)}))
+z.close()
+`], { stdio: 'pipe' });
+  } catch (e2) {
+    console.error('打包失败:无 zip 命令且 python 兜底也失败');
+    process.exit(1);
+  }
+}
+
+const size = fs.statSync(zipPath).size;
+fs.rmSync(stageDir, { recursive: true, force: true });
+
+console.log(`\n${C.b}发布包已生成${C.x}\n`);
+console.log(`  文件: ${zipPath}`);
+console.log(`  大小: ${(size / 1024).toFixed(1)} KB`);
+console.log(`  技能: ${skills.length} 个 — ${skills.join(', ')}`);
+console.log(`  文件数: ${files.length}`);
+console.log(`  敏感自检: ${leaks === 0 ? `${C.g}通过(0 处)${C.x}` : `${C.r}${leaks} 处需处理${C.x}`}`);
+console.log(`\n${C.d}上传:SkillHub 后台 → 上传技能包 → 选此 zip${C.x}\n`);
+
+process.exit(leaks > 0 ? 1 : 0);