Bladeren bron

feat: 新增文档引用完整性校验 + 修复全库引用缺失

核心原则:用户不该持华为云密钥,所有上传走平台内置技能。

新增:scripts/verify-refs.mjs — 扫描 dist 里每份文档的每个文件引用,验证是否真实存在
  - 区分「真实依赖」与「示例性提及」
  - 打包后必跑,确保 Agent 装完技能不缺文件

修复(37 处真实缺失 → 0):
- code-lit: SKILL.md 规则引用路径补全 references/rules/
- report-publish: 自带 tokens.css 权威源,去掉 skill-present 外部依赖
- storage: 项目级上传改用自带 uploader.mjs,移除内部脚本路径和 obsutil 密钥配置
- video-lapian: 移除跳板机/AK-SK/内部桶名,改走平台免密钥通道
- vision: 扫描器补 scripts/prompts 等路径解析

验收:13 技能 / 真实依赖缺失 0 / 触发词 13/13 / 品牌 0 违规
liuyuyang 1 dag geleden
bovenliggende
commit
45f33bad62

+ 1 - 1
dist/skills-index.json

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

+ 4 - 4
dist/skills/fmode-code-lit/SKILL.md

@@ -47,9 +47,9 @@ keywords: [lit, 项目, lit, 组件, WebComponent, Web, Component, 组件库, 
 
 | 需求 | 写在哪 | 生成命令 | 规则篇 |
 |------|--------|----------|--------|
-| 逻辑要在 Node + 多框架复用 | `lib/core/` | `new.mjs core <name>` | `core-library.md` |
-| UI 要跨框架(Lit/Vue/React/原生) | `lib/wc/` | `new.mjs wc <name>` | `web-component-library.md` |
-| 只做本应用的页面 | `src/routes/` | `new.mjs page <name>` | `components.md` |
+| 逻辑要在 Node + 多框架复用 | `lib/core/` | `new.mjs core <name>` | `references/rules/core-library.md` |
+| UI 要跨框架(Lit/Vue/React/原生) | `lib/wc/` | `new.mjs wc <name>` | `references/rules/web-component-library.md` |
+| 只做本应用的页面 | `src/routes/` | `new.mjs page <name>` | `references/rules/components.md` |
 
 - **core 铁律**:不 `import lit`、不碰 `window`/`document`/`fetch`;`now`/`id`/`transport` 等不确定输入**注入**;`node --test` 可测。
 - **wc 铁律**:**必须用 Lit 框架特性写**(`extends LitElement` + `html` + `css`,**禁手写原生 wc**:`extends HTMLElement` / `attachShadow()` / 手写 `observedAttributes`);逻辑来自 core;网络由宿主注入 `transport`;只暴露语义属性 + `composed` 事件 + slot + CSS 变量;标签加统一前缀;`customElements.define` **判重**。
@@ -148,7 +148,7 @@ lit@3.3.3  lit-html@3.3.3  lit-element@4.2.2  @lit/reactive-element@2.1.2
 - **CDN History 回退**:不存在路径返回 `index.html` 且状态码 **200**,**禁 302**(否则地址栏改写、OAuth 回调失败、路由丢失)。
 - `<base href>` 与部署前缀一致;`index.html` 设 `no-cache`。
 
-## 常见坑(详见规则仓 `.claude/rules/pitfalls.md`)
+## 常见坑(详见规则仓 `references/rules/pitfalls.md`)
 
 装饰器 / CORS / MIME 非 JS / lit 多版本混用 / CDN 302 / base href 缺失 / 相对 import 漏 `.js` /
 signal 与 property 混用 / 老浏览器无 Import Map / 循环依赖。

+ 4 - 4
dist/skills/fmode-report-publish/SKILL.md

@@ -28,7 +28,7 @@ keywords: [html-report, long-page, proposal, landing-page, publishing, s3-cdn, 
 
 ### 2.1 主题体系(双主题·139个CSS变量)
 
-参考 `skill-present/lib/theme/tokens.css`,使用 CSS 变量实现深色/浅色双主题切换:
+参考 `assets/theme/tokens.css`(本技能自带权威源),使用 CSS 变量实现深色/浅色双主题切换:
 
 ```css
 /* 深色主题(默认) */
@@ -129,9 +129,9 @@ node uploader.mjs put /path/to/report.html \
 
 | 资源 | 位置 | 说明 |
 |------|------|------|
-| tokens.css (权威源) | `skill-present/lib/theme/tokens.css` | 139个CSS变量双主题系统 |
-| day2am.css | `skill-present/lesson/hundun-aiceo/course/day2/day2am.css` | day2 课件设计体系 |
-| RSI v2 报告 (参考) | `skill-present/lesson/harness-evolution/landing/index.html` | 66KB 最佳实践 |
+| tokens.css (权威源) | `assets/theme/tokens.css`(本技能自带) | 139个CSS变量双主题系统 |
+| day2am.css | 参考实现:day2 课件的设计体系(不随技能分发) | day2 课件设计体系 |
+| RSI v2 报告 (参考) | 参考实现:66KB 最佳实践案例(不随技能分发) | 长页报告范本 |
 | 七层架构图 | `storage/archive/demo/theme1/theme1-explode-7layer.png` | 标准架构图 |
 | client-proposal-craft | 本地技能 `communication/client-proposal-craft` | 客户方案方法论 |
 | skill-storage | `$SKILL_STORAGE_DIR`(默认取 skill-storage 技能的安装目录) | CDN 发布引擎 |

+ 200 - 0
dist/skills/fmode-report-publish/assets/theme/tokens.css

@@ -0,0 +1,200 @@
+/* ============================================================
+ * lib/theme/tokens.css · 混沌AI院设计系统 token 唯一来源
+ * 双主题:暗色(混沌AI院) / 浅色(未来飞马 VI)
+ * 任何文件不得新写色值/字体/圆角常量,一律引用本文件 var(--...)。
+ * 切换方式:<html data-theme="dark" | "light">(由 theme.js 管理)
+ * ============================================================ */
+
+:root {
+  /* ---------- 背景层级(0=最深 3=最浅浮层) ---------- */
+  --bg-0: #0B1017;                 /* 页面深底 */
+  --bg-1: #111925;                 /* 一级浮层 / 区块底 */
+  --bg-2: #171F2B;                 /* 卡片底 */
+  --bg-3: #0D131C;                 /* 浮层 / 弹层底 */
+
+  /* ---------- 文字层级(1=最高 4=最低) ---------- */
+  --text-1: #FBFAF7;               /* 大标题 / 核心结论 */
+  --text-2: #C1C4CB;               /* 主标题 / 正文 */
+  --text-3: #777D85;               /* 说明 / 次级 */
+  --text-4: rgba(255,255,255,.42); /* 弱化 / 页码 / 脚注 */
+
+  /* ---------- 品牌强调 ---------- */
+  --brand-1:    #D5B254;           /* 金:主强调 */
+  --brand-1-hi: #FFC000;           /* 金黄:框线 / 高亮 */
+  --brand-2:    #AF8AE8;           /* 浅紫:次级强调 */
+  --brand-3:    #FF0000;           /* 红:卡点 / 警示 */
+  --brand-ok:   #7FD1A0;           /* 绿:通过 / 已确认 */
+
+  /* ---------- 渐变 ---------- */
+  --gradient-brand: linear-gradient(135deg, #D5B254, #FFC000);
+  --gradient-text:  linear-gradient(135deg, #FFC000, #AF8AE8);
+  --on-gradient:    #0B1017;           /* 品牌渐变底上的文字色(暗=金渐变深字) */
+  --gradient-hero:  radial-gradient(ellipse at 50% 0%, rgba(213,178,84,.10) 0%, transparent 60%),
+                    radial-gradient(ellipse at 80% 85%, rgba(175,138,232,.10) 0%, transparent 55%),
+                    linear-gradient(180deg, #0B1017 0%, #111925 60%, #0D131C 100%);
+
+  /* ---------- 边框 ---------- */
+  --border-subtle: rgba(255,255,255,.07);
+  --border-strong: rgba(213,178,84,.35);
+  --border-focus:  rgba(213,178,84,.6);
+  --border-danger: rgba(255,0,0,.45);        /* 警示红框(卡点 / 翻车) */
+  --border-ok:     rgba(127,209,160,.4);     /* 通过绿框(已确认 / 人味版) */
+
+  /* ---------- 阴影 ---------- */
+  --shadow-card:  none;                       /* 暗色用线框,不用投影 */
+  --shadow-float: 0 8px 30px rgba(0,0,0,.45);
+  --shadow-glow:  0 0 24px rgba(213,178,84,.25);
+
+  /* ---------- 圆角(混沌 = 直角卡片) ---------- */
+  --radius-sm: 3px;  --radius-md: 6px;  --radius-lg: 10px;
+  --radius-card: 0px;
+  --radius-pill: 999px;
+
+  /* ---------- 字体 ---------- */
+  --font-display: "Source Han Serif SC","Noto Serif SC","Songti SC","SimSun",serif;
+  --font-display-weight: 800;
+  --font-body: "Source Han Sans SC","Noto Sans SC","PingFang SC","Microsoft YaHei",system-ui,sans-serif;
+  --font-mono: "SF Mono","Cascadia Code","JetBrains Mono",Consolas,monospace;
+
+  /* ---------- 缓动 ---------- */
+  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);
+  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
+
+  /* ---------- 玻璃拟态 ---------- */
+  --glass-bg:   rgba(23,31,43,.85);
+  --glass-blur: 12px;
+
+  /* ---------- 屏幕 ---------- */
+  --screen-h: 100dvh;
+
+  /* ---------- 组件 token:微信/群聊 ---------- */
+  --chat-bg:          #111B21;
+  --chat-in-bg:       #202C33;   --chat-in-text: #E9EDEF;
+  --chat-out-bg:      #95EC69;   --chat-out-text: #111B21;
+  --chat-avatar-bg:   #5A6570;   --chat-avatar-text: #FFFFFF;
+  --chat-link:        #7FD1A0;
+  --chat-bar-bg:      #0B1017;   --chat-bar-text: #D5B254;
+  --chat-card-bg:     rgba(127,209,160,.06);
+  --chat-card-border: rgba(127,209,160,.4);
+  --chat-time:        #777D85;
+
+  /* ---------- 组件 token:多画像评估 ---------- */
+  /* 画像区分色(avatar 底 / 标签点 / 雷达线),双主题两套值 */
+  --persona-1:  #FFC000;  /* 决策层老板     */
+  --persona-2:  #AF8AE8;  /* 执行层操盘手   */
+  --persona-3:  #FF6B6B;  /* 一线业务经纪人 */
+  --persona-4:  #4FC3F7;  /* 技术 AI 工程师 */
+  --persona-5:  #7FD1A0;  /* 非技术市场 HR  */
+  --persona-6:  #FF9F43;  /* 行业老兵       */
+  --persona-7:  #F78FB3;  /* 课程采购负责人 */
+  --persona-8:  #00E5CC;  /* 挑剔同行讲师   */
+  --persona-9:  #B2B6BF;  /* 预留           */
+  --persona-10: #8BE9FD;  /* 预留           */
+
+  /* 问题优先级徽章色(色点/色条背景),P0 红 / P1 橙 / P2 黄 / P3 绿 */
+  --grade-p0: #FF0000;
+  --grade-p1: #FF9F43;
+  --grade-p2: #FFC000;
+  --grade-p3: #7FD1A0;
+  /* 徽章前景文字(双主题两套值,与背景成对) */
+  --grade-p0-fg: #FFFFFF;
+  --grade-p1-fg: #0B1017;
+  --grade-p2-fg: #0B1017;
+  --grade-p3-fg: #0B1017;
+
+  /* ---------- 组件 token:二维码 ----------
+     扫码器要求「深色模块 + 浅色底」,故双主题同值、不随主题反转。 */
+  --qr-paper: #FFFFFF;
+  --qr-ink:   #0B1017;
+}
+
+/* ============================================================
+ * 浅色 · 未来飞马 VI
+ * 主色:紫 #6C5CE7 / 蓝 #3B82F6 / 青 #0891B2
+ * 背景 #F8F9FC,玻璃卡 + 圆角 16 + 胶囊 50 + 柔和阴影,黑体标题
+ * ============================================================ */
+html[data-theme="light"] {
+  --bg-0: #F8F9FC;
+  --bg-1: #FFFFFF;
+  --bg-2: #F0F2F8;
+  --bg-3: #E9EBF3;
+
+  --text-1: #0F0F23;
+  --text-2: #1A1A2E;
+  --text-3: #4A4A6A;
+  --text-4: #6B7280;
+
+  --brand-1:    #6C5CE7;         /* 主品牌紫 */
+  --brand-1-hi: #3B82F6;         /* 辅蓝 */
+  --brand-2:    #0891B2;         /* 强调青 */
+  --brand-3:    #E5484D;
+  --brand-ok:   #16A34A;
+
+  --gradient-brand: linear-gradient(135deg, #6C5CE7, #3B82F6);
+  --gradient-text:  linear-gradient(135deg, #6C5CE7, #0891B2);
+  --on-gradient:    #FFFFFF;           /* 品牌渐变底上的文字色(浅=紫渐变白字) */
+  --gradient-hero:  linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 50%, #F8F9FC 100%);
+
+  --border-subtle: rgba(15,15,35,.08);
+  --border-strong: rgba(108,92,231,.35);
+  --border-focus:  rgba(108,92,231,.6);
+  --border-danger: rgba(229,72,77,.45);
+  --border-ok:     rgba(22,163,74,.4);
+
+  --shadow-card:  0 4px 24px rgba(0,0,0,.06);
+  --shadow-float: 0 8px 32px rgba(0,0,0,.10);
+  --shadow-glow:  0 0 24px rgba(108,92,231,.20);
+
+  --radius-sm: 8px;  --radius-md: 12px;  --radius-lg: 16px;
+  --radius-card: 16px;
+  --radius-pill: 50px;
+
+  --font-display: "Source Han Sans SC","Noto Sans SC","PingFang SC","Microsoft YaHei",system-ui,sans-serif;
+  --font-display-weight: 700;
+
+  --glass-bg:   rgba(255,255,255,.85);
+  --glass-blur: 20px;
+
+  --chat-bg:          #FFFFFF;
+  --chat-in-bg:       #FFFFFF;   --chat-in-text: #1A1A2E;
+  --chat-out-bg:      #6C5CE7;   --chat-out-text: #FFFFFF;
+  --chat-avatar-bg:   #B9B1F5;   --chat-avatar-text: #FFFFFF;
+  --chat-link:        #6C5CE7;
+  --chat-bar-bg:      #F8F9FC;   --chat-bar-text: #6C5CE7;
+  --chat-card-bg:     rgba(108,92,231,.06);
+  --chat-card-border: rgba(108,92,231,.4);
+  --chat-time:        #9CA3AF;
+
+  /* 多画像评估(浅色:中深色系保证浅底可读) */
+  --persona-1:  #F59E0B;  /* 决策层老板     */
+  --persona-2:  #8B5CF6;  /* 执行层操盘手   */
+  --persona-3:  #E5484D;  /* 一线业务经纪人 */
+  --persona-4:  #3B82F6;  /* 技术 AI 工程师 */
+  --persona-5:  #16A34A;  /* 非技术市场 HR  */
+  --persona-6:  #EA580C;  /* 行业老兵       */
+  --persona-7:  #DB2777;  /* 课程采购负责人 */
+  --persona-8:  #0D9488;  /* 挑剔同行讲师   */
+  --persona-9:  #64748B;  /* 预留           */
+  --persona-10: #0EA5E9;  /* 预留           */
+
+  --grade-p0: #E5484D;
+  --grade-p1: #EA580C;
+  --grade-p2: #D97706;
+  --grade-p3: #16A34A;
+
+  --grade-p0-fg: #FFFFFF;
+  --grade-p1-fg: #FFFFFF;
+  --grade-p2-fg: #FFFFFF;
+  --grade-p3-fg: #FFFFFF;
+
+  /* ---------- 组件 token:二维码(同暗色,扫码器要求深色模块 + 浅色底) ---------- */
+  --qr-paper: #FFFFFF;
+  --qr-ink:   #0B1017;
+}
+
+/* ---------- 主题切换过渡(页面级,轻微即可) ---------- */
+html[data-theme] body,
+html[data-theme] .card,
+html[data-theme] .s-wrap {
+  transition: background-color .3s ease, color .3s ease, border-color .3s ease, box-shadow .3s ease;
+}

+ 250 - 0
dist/skills/fmode-storage/README.md

@@ -0,0 +1,250 @@
+# skill-storage · 对象存储与公开分享
+
+> **未来飞马 — 让AI进化提前发生,让AI落地快人一步**
+
+[![License: MPL-2.0](https://img.shields.io/badge/License-MPL--2.0-brightgreen.svg)](LICENSE)
+[![ESM](https://img.shields.io/badge/module-ESM--only-orange.svg)](#快速开始)
+[![npm](https://img.shields.io/badge/npm-fmode--storage-blue.svg)](https://www.npmjs.com/package/fmode-storage)
+
+---
+
+## 简介
+
+`skill-storage` 是智能体的「仓库管理员」:把二进制大文件(图片 / 音频 / 视频 / 报告 HTML)上传到对象存储,本地磁盘零长期占用,一键生成可转发的公开分享链接。
+
+**默认通道免密钥**:只要环境里有登录态 `sessionToken`,平台云函数即按调用者身份签发上传地址——不用申请 AK/SK、不用初始化。密钥由云函数保管,**永不下发到调用方**。
+
+本技能适用于 **FmodeAgent / FmodeAgent** 平台,开发由 **FmodeCode / FmodeCode** 执行。
+
+本技能以 ESM 原生模块交付,Node.js ≥ 18 直接 `import`,零依赖、零构建。
+
+---
+
+## 核心定位
+
+| 维度 | 说明 |
+|------|------|
+| **解决什么** | 大文件上云 + 公开链接生成:报告发布即分享、素材托管、批量上传 |
+| **不解决什么** | 不做文件转码、不做内容审核、不托管长期私有数据 |
+| **与网盘的区别** | 产出的是**可直接嵌入/转发的公网 URL**,而非需要登录的分享页 |
+| **层级** | 服务级(Platform Services) |
+| **适用平台** | FmodeAgent / FmodeAgent · FmodeCode / FmodeCode |
+
+---
+
+## 核心能力 & 交付物
+
+- 📤 **大文件上传** —— 对象存储直传,本地磁盘零长期占用
+- 🔗 **公开分享链接** —— 上传即得公网 URL,可直接转发或嵌入
+- 📄 **报告发布即分享** —— HTML 报告/课件传完即得可转发链接
+- 🧪 **自检命令** —— `test` 上传→删除探针文件,全链路验证
+- 🧹 **生命周期管理** —— 前缀用量统计、批量清理、ACL 设置
+- 🔐 **凭据零入库** —— 密钥永不落盘、永不进日志、永不进对话
+
+---
+
+## 快速开始
+
+### 零配置用法(0 步,推荐)
+
+```bash
+node skills/fmode-storage/scripts/uploader.mjs put ./report.html --key report/202609/report.html
+# → { "ok": true,
+#     "key": "user/<你的id>/report/202609/report.html",
+#     "url": "https://s3.fmode.cn/user/<你的id>/report/202609/report.html" }
+
+node skills/fmode-storage/scripts/uploader.mjs test    # 自检
+```
+
+`--key` 给相对路径即可,服务端会强制挂到 `user/<你的id>/` 前缀下;省略则自动命名。上传即 `public-read`,**不需要**再调 `setacl`。
+
+### Node.js(ESM)
+
+```javascript
+import { putViaCloudFunction } from './skills/fmode-storage/scripts/uploader.mjs';
+
+const r = await putViaCloudFunction(
+  '/path/report.html',
+  'report/202609/report.html',
+  'report',
+);
+// r = { ok, key, url, bytes, via }
+console.log(r.url);
+```
+
+### 浏览器(原生 ES Module)
+
+```html
+<script type="module">
+  // 浏览器端只消费上传后得到的公开链接,不持有任何密钥
+  const url = 'https://s3.fmode.cn/user/<id>/report/202609/report.html';
+  const res = await fetch(url, { method: 'HEAD' });
+  console.log('published:', res.ok, res.status);
+</script>
+```
+
+### 兜底:需要写平台桶时
+
+需要写**平台桶**(而非个人空间)时,才走 `init` 向导配置 OBS 子账号:
+
+```bash
+node skills/fmode-storage/scripts/uploader.mjs init \
+  --ak <你的AK> --sk <你的SK> \
+  --endpoint obs.cn-north-4.myhuaweicloud.com --bucket <bucket>
+```
+
+---
+
+## 凭据链(命中即用,零密钥入库)
+
+```
+┌─ 第0级【首选·免密钥】sessionToken → 云函数签发预签名 PUT
+│    AK/SK 由云函数保管,调用方只拿 5 分钟有效的上传地址。无需任何本地配置。
+│  第1级 环境变量 OBS_AK/OBS_SK(可选 OBS_ENDPOINT/OBS_BUCKET)
+│  第2级 obsutil config(OBSUTIL_CONFIG 或 ~/.obsutilconfig)
+│  第3级 sessionToken + storageProjectId → 项目级 STS(作用域限定项目前缀)
+│  第4级 项目级 ./.fmode/config.json(obsBucket/obsEndpoint/cdnDomain)
+│  全失败 → 打印初始化向导并退出码 2,绝不伪装成功
+└──────────────────────────────────────────────────────────────────────────┘
+```
+
+- **对象只能写自己的前缀** —— `user/<自己的id>/`,由服务端强制校验,写不了别人的
+- **STS 仅内存持有** —— 临时凭证带过期时间与权限范围,用完即删
+- **诊断输出脱敏** —— `config` 命令的输出绝不含任何密钥本体
+
+> ⚠️ **不要把 AK/SK、sessionToken、STS 写进本仓库或任何对话。**
+
+---
+
+## FAQ
+
+### 技术概念
+
+**Q1:为什么要用对象存储,不直接把文件放在本地?**
+本地文件无法被外部访问,也无法嵌入到别人的文档或聊天里。对象存储提供稳定的公网 URL,磁盘占用为零,且天然适合大文件与高并发下载。
+
+**Q2:STS 临时凭证和 AK/SK 长期密钥有什么区别?**
+AK/SK 是长期身份密钥,泄露即长期风险;STS 是平台签发的**临时**凭证,带过期时间与权限范围,仅内存持有、用完即删。云函数预签名地址同理,有效期仅 5 分钟。优先走这两条临时通道。
+
+**Q3:云函数签发地址,密钥会不会下发到我的容器?**
+不会。真实 AK/SK 只存在于平台云函数侧,调用方拿到的是一个 5 分钟有效的预签名 PUT 地址。密钥永不下发、永不落盘。
+
+**Q4:诊断命令会不会泄露我的密钥?**
+不会。`config` 命令的输出经过脱敏,绝不包含任何密钥本体。
+
+### 开源协议(MPL-2.0)
+
+**Q1:MPL-2.0 协议允许我商用吗?**
+允许。MPL-2.0 允许商用,也可用于闭源产品。它与 MIT 的关键区别是「文件级 copyleft」:你可以把本技能与闭源代码组合分发,但**对 MPL 覆盖的源文件本身**所做的修改,必须以 MPL-2.0 公开。
+
+**Q2:使用本技能需要保留版权声明吗?**
+需要。分发时必须保留原始版权声明与许可证全文,并说明 MPL-2.0 覆盖了哪些文件;若修改了 MPL 覆盖的源文件,需以 MPL-2.0 公开这些文件的源码。
+
+**Q3:我可以把本技能改成别的名字再发布吗?**
+可以修改和再分发,但**不可以**使用「未来飞马」「Harness Loop」「RSI」等商标,也不得使用品牌 Slogan 作为产品名或宣传语。版权许可不等于商标授权,详见 [Trademark Notice](#trademark-notice)。
+
+**Q4:MPL-2.0 协议提供担保吗?**
+不提供。本技能按「原样」提供,不附带任何明示或默示担保。
+
+### 业务用户搜索
+
+**Q1:怎么把大文件传到云端并生成一个公开链接?**
+用 skill-storage。只要环境里有登录态,一条 `put` 命令即可上传并直接拿到可转发的公网 URL,无需配置任何密钥。
+
+**Q2:生成的 HTML 报告怎么发给客户看?**
+用 skill-storage 上传后得到 `https://` 链接,客户点开即看,无需登录、无需下载附件。配合 skill-report-html 可制作咨询师级长页报告。
+
+**Q3:上传大文件会占用我的电脑磁盘吗?**
+不会。文件直传对象存储,本地只在传输过程中临时读取,不做长期留存。
+
+**Q4:支持华为云之外的存储吗?**
+支持。任何兼容 S3 协议的对象存储均可通过 `--endpoint` 指定。
+
+---
+
+## GEO 埋点说明
+
+本技能遵循**隐私优先**的 GEO(生成式引擎优化)埋点规范:
+
+- **默认关闭** —— `geoTracking` 默认为 `false`,不开启即不产生任何上报
+- **显式开启** —— 仅当用户主动设置开启后才会上报
+- **最小采集** —— 只采集地区级别信息(国家/大区),**不采集**城市、IP 地址、设备 ID、经纬度
+- **独立模块** —— 埋点逻辑独立于主技能,可单独移除而不影响功能
+- **不阻塞** —— 上报失败静默降级,绝不阻塞主技能逻辑
+
+---
+
+## License
+
+本技能采用 **Mozilla Public License 2.0(MPL-2.0)** 发布,完整原文见 [LICENSE](LICENSE)。
+
+```
+Mozilla Public License Version 2.0
+
+Copyright (c) 未来飞马
+```
+
+## Trademark Notice
+
+> MPL-2.0 governs copyright for source code only.
+> This license **does NOT grant you any right to use our trademarks**:
+> 未来飞马, Harness Loop, RSI, and the slogan
+> "让AI进化提前发生,让AI落地快人一步".
+>
+> You may not use these trademarks in your product name, marketing,
+> documentation, or public promotion unless you obtain separate written
+> permission from 未来飞马.
+
+---
+
+## 贡献指南
+
+1. **Fork** 本仓库并创建特性分支:`git checkout -b feature/your-idea`
+2. **零依赖优先** —— 优先使用平台内置能力,不引入第三方包
+3. **凭据纪律** —— 任何情况下不得在仓库、Issue、PR 中写入真实密钥
+4. **提交前自检** —— 运行 `npm run smoke` 或 `node skills/fmode-storage/scripts/uploader.mjs test`
+5. **提交 PR** —— 说明动机、变更范围与验证方式
+
+---
+
+## 相关项目
+
+- **Harness Loop** —— 未来飞马技能生态的持续迭代回路
+- **RSI** —— 递归自我改进(Recursive Self-Improvement)机制
+- **FmodeAgent / FmodeAgent · FmodeCode / FmodeCode** —— 本技能的目标运行平台
+
+---
+
+## Changelog
+
+### 1.2.0
+- 许可证由 MIT 切换为 MPL-2.0:LICENSE 全文、package.json / manifest / plugin.json / SKILL.md frontmatter 的 license 字段同步更新
+- 源码头部注释模板改为 MPL-2.0 文案
+- 品牌名统一并列写法:FmodeAgent / FmodeAgent、FmodeCode / FmodeCode
+
+### 1.1.0
+- 按 skill-core-guide v1.1.0 规范改造:品牌 Slogan、GEO 埋点说明、MPL-2.0 协议与商标声明独立小节
+- README 重构为完整结构(简介 → 核心定位 → 快速开始 → FAQ → GEO → 许可 → 贡献指南)
+- 移除内部分析内容(设计文档路径、事故复盘式细节)
+- package.json 补齐中英双语 keywords 与 ESM 元数据
+- 源码头部补齐版权 + 商标注释模板
+
+### 0.4.0
+- **新增第 0 级:云函数预签名直传**,只需登录态 `sessionToken`,调用方无需任何凭据
+- `put` / `test` 优先走第 0 级;第 1–4 级保留为兜底(平台桶等场景)
+- `setacl` 在第 0 级下为 no-op(上传即 `public-read`)
+
+### 0.3.0
+- 凭据链重写为诚实 4 级(env → obsutil config → 平台签发 STS → 项目 config)
+- 新增 `init` 向导与 `test` 自检命令
+- 全链失败时明确报错并退出码 2,不再伪装成功
+
+## 📥 大文件网关(bigfile-gateway)
+
+当聊天通道限制挡住大文件(如企微 20MB)时,生成接收页让对方拖拽直传对象存储:
+
+- `uploader.mjs gateway <outDir>` → 自动注册项目、部署接收页、输出 URL(含 1h 临时授权)
+- `gateway.mjs collect --manifest <outDir>/mint.json --out ~/inbox` → 取回文件
+- 页面不含任何密钥;每文件独立槽位;平时不需要,超限才触发
+
+官网介绍:https://app.fmode.cn/dev/QPeGDMcxAj/

+ 14 - 17
dist/skills/fmode-storage/SKILL.md

@@ -41,23 +41,20 @@ node <skill_dir>/scripts/uploader.mjs test
 
 **不要用 uploader put 走个人级当"项目级"!** uploader put 的首选通道是 level0 cloudfunc(个人空间 user/<id>/)。
 
-项目级 = nova-cloud 桶 `dev/<projectId>/` 前缀,公网 https://app.fmode.cn/dev/<projectId>/<file>。正确流程:
-
-1. 项目未注册先注册:`POST server.fmode.cn/api/functions {token, path:'/fmode/project-register', params:{name:'<slug>'}}` → 返回 projectId(objectId,如 GvsWkfZb36)
-2. 签发项目级 STS:`POST server.fmode.cn/api/apig/deploy/huaweicloud {token, projectId}` → 200 返回 accessKey/secretKey/securityToken + obsPath `obs://nova-cloud/dev/<PID>/`
-3. obsutil ini(无[default]段头,endpoint=obs.cn-south-1配合nova-cloud 桶):
-   ```
-   endpoint=obs.cn-south-1.myhuaweicloud.com
-   ak=...
-   sk=...
-   token=...
-   ```
-4. 上传:`obsutil cp <file> obs://nova-cloud/dev/<PID>/<file> -f -acl=public-read -config=<ini>` (**必须 -config= 指定 ini,不要 obsutil config 全局污染**;不要用 obsutil ls 测试权限——直接 cp 才准,ls 可能因 STS policy 范围报 403)
-5. CDN 刷新 app.fmode.cn 域名组
-
-脚本:`/opt/data/scripts/upload-project.sh <sessionToken> <projectId> <file...>`
-
-认证区身份:sessionToken 用 ~/.fmode/config/user.json(真实用户,userId=liuyuyang),不是 ~/.fmode/config.json。
+项目级 = nova-cloud 桶 `dev/<projectId>/` 前缀,公网 https://app.fmode.cn/dev/<projectId>/<file>。
+
+**用技能自带脚本一行完成(推荐)**:
+
+```bash
+# 项目级上传:脚本自动完成 项目注册 → 平台签发 STS → 直传 OBS → 返回公网 URL
+node <skill_dir>/scripts/uploader.mjs put <文件> --project-slug <你的项目名>
+
+# 指定已有项目
+node <skill_dir>/scripts/uploader.mjs put <文件> --project-id <projectId>
+```
+
+**平台已托管全部密钥**:STS 由平台云函数按你的 sessionToken 签发(5 分钟有效,仅存内存),
+**用户无需申请、无需持有、无需配置任何华为云 AK/SK**,也不要写 obsutil 配置文件。
 
 坑:①赖着 uploader put(level0 个人空间)当"项目级"= 错误;②obsutil 用 `obsutil config -c` 写临时 ini 会污染且报 not well-formed,用 `-config=` 参数指向自立 ini;③deploy STS endpoint 是 obs.cn-south-1(nova-cloud 桶在 south-1,north-4 会 NoSuchBucket/签名不匹配 403);④**`resolveStorageProjectId()` 常命中一个不属于当前用户的陈旧 projectId**(本机 `~/.fmode/config.json` = `GvsWkfZb36`),此时 deploy STS 返回 `{code:403,"没有权限"}`——用 `gateway` 子命令会自动注册/复用自己的项目(`bigfile-gateway`),或显式 `--project-id` 传一个本人有权限的 objectId。
 

+ 4 - 7
dist/skills/fmode-video-lapian/SKILL.md

@@ -28,9 +28,8 @@ export FEME_USERID=sr2WiPsDyQ            # ← 换成你自己的 Parse objectId
 export FEME_SESSION_TOKEN="r:..."        # 飞马会话
 export FEME_NEWAPI_TOKEN="sk-..."        # API计费token(调VOC网关/转写/模型全走它)
 export FEME_STUDIO_URL="http://server.fmode.cn:<studio端口>"   # 自己的studio端点
-# S3 (过渡期可用容器env; studio /api/storage/credentials 上线后自动切换)
-export CLOUD_SDK_AK=...   export CLOUD_SDK_SK=...
-export S3_BUCKET=storage-s3-nkkj  S3_REGION=cn-north-4
+# S3 发布:走平台内置免密钥通道(见 fmode-storage 技能),无需本地 AK/SK
+export S3_BUCKET=<由平台云函数返回>  S3_REGION=cn-north-4
 export S3_ENDPOINT=https://obs.cn-north-4.myhuaweicloud.com
 ```
 
@@ -126,7 +125,6 @@ AIGC 拉片流程(mode=aigc_storyboard 时): 场景检测(`select='gt(scene,0.10
 - fmode-listen 是异步转写, 长音频耐心等(脚本已设 30 分钟超时)
 - 豆包视觉模型: 用 `doubao-seed-2-0-pro-260215`(当前账号唯一可用豆包pro), FmodeCode 系无视觉
 - 同一视频当天重跑: publish.py 幂等覆盖同日期目录, 直接重传即可
-<<<<<<< HEAD
 
 ## 微信视频号(weixin/channels)专用说明 2026-09-03
 
@@ -137,9 +135,8 @@ AIGC 拉片流程(mode=aigc_storyboard 时): 场景检测(`select='gt(scene,0.10
 - **详情接口**: 视频号走 `weixin/channels/fetch_one_video` → 失败降级 `weixin/channels/fetch` → 再失败**降级直连模式**(仅下载视频做拉片,不阻塞流程)
 - **与抖音的差异**: 视频号 meta 字段是 `title/desc/nickname/video_url`(非 aweme 结构),summary 已做归一;**评论区暂无视频号通道**,拉片报告的 VOC 区会标注"该平台暂不支持评论区采集"
 - **实测路径**: 拿到真实视频号链接后先跑一次 `fetch_video.py --url <链接> --out-dir /tmp/test-wx`,确认下载链路通,再给用户出报告
-=======
+
 - **抖音下载铁律(2026-08-29 三次坏文件实测)**: `play_addr` 流对非APP客户端返回**损坏流**(moov 声称259s但 mdat 截断, ffprobe 时长正常但解码报 Invalid NAL unit size)。**必须用 `download_addr.url_list[0]` + curl 带浏览器UA/Referer 下载**(该流是高码率 1958kbps 版本, 时长可能与 play 版差几秒)。**唯一可信校验 = `ffmpeg -xerror -i 全量解码`, ffprobe 时长正常/尾部分段解码都可能假阴性**。下载会话中断后转写/抽帧全部作废重跑(音频只覆盖已下载部分)。
 - **孤儿进程陷阱**: 会话中断后下载进程可能还活着继续写文件, 恢复会话先 `ps aux | grep fetch_video` + 隔几秒对比文件大小, 确认没有写入者再动手, 避免双写或误判。
 - **报告点评 LLM 空响应**: build_report.py 内置 call_llm 走 deepseek-v4-flash 会路由到 reasoning 模型, reasoning 吃满 max_tokens 导致 content 为空。解法: 自己(编排 agent)按 methodology.md 逐段写点评 → 写 `workdir/llm_out.json`(格式: `{"segments":[{"i","techniques","func","comment","reuse"}...], "structure_summary"}`) → build_report.py 自动优先采用(手动 > LLM > 启发式)。
-- **S3 发布的 storage API 是设计稿未部署**(2026-08-29 实测): `/api/storage/credentials`、`/api/storage/upload` 在 443 网关与各 studio 端口全部 404, fmode-identity.json 也无 CLOUD_SDK_AK/SK。**用跳板机通道: `scripts/publish_via_obs.py`** — 容器打包 → SFTP 到 server.fmode.cn(139.159.253.131) → 云端 obsutil(`/opt/obsutil/obsutil_linux_amd64_5.4.11/obsutil`, 自带 nkkj 主账套 AK/SK, `cp -acl=public-read -e=obs.cn-north-4...`, 旗标别加 `-p -f`) → OBS 公网 HEAD 校验。paramiko 连云服务器偶发 SSH banner 瞬断, 脚本已带重试。注意容器 tar 打包时 assets 类文件本地在根目录、需 arcname 映射到 assets/ 前缀。
->>>>>>> d65e9489096639272bb3e0ecf63c77681fe7c2b8
+- **S3 发布改用平台内置通道**:优先用 `fmode-storage` 技能的 `uploader.mjs put`(免密钥,走平台云函数签发上传地址)。**不要**自建跳板机/不要使用任何外部 AK-SK;密钥类信息一律由平台托管,技能不自持也不索取。

+ 2 - 22
package.json

@@ -1,37 +1,17 @@
 {
   "name": "fmode-skills",
-<<<<<<< HEAD
-  "version": "0.3.0",
-  "description": "Fmode 总技能库 —— 一次安装,全部 Fmode 技能自动落盘(数字分身初始化、云资源领取、前后端发布、VOC 情报、企微、支付等)。安装后由 installer.mjs 按宿主分发子技能。",
-=======
-  "version": "1.0.0",
+  "version": "1.1.0",
   "description": "Fmode 技能库源仓库(维护 + 打包)。dist/ 为上传 SkillHub 的产物,由 scripts/build-dist.mjs 从各技能源仓库拉取真实文件生成。",
->>>>>>> 8bd0c5f (refactor: 重构为源库+打包架构 —— dist 由脚本从源仓库拉真实文件生成)
   "type": "module",
   "private": true,
   "license": "MPL-2.0",
-<<<<<<< HEAD
-  "keywords": [
-    "fmode",
-    "harness",
-    "skillhub",
-    "agent-skills",
-    "未来飞马",
-    "技能库",
-    "智能体技能",
-    "数字分身",
-    "云资源",
-    "VOC"
-  ],
-=======
->>>>>>> 8bd0c5f (refactor: 重构为源库+打包架构 —— dist 由脚本从源仓库拉真实文件生成)
   "engines": {
     "node": ">=18"
   },
   "scripts": {
     "build": "node scripts/build-dist.mjs",
     "build:dry": "node scripts/build-dist.mjs --dry-run",
-    "verify": "node scripts/verify-dist.mjs",
+    "verify": "node scripts/verify-refs.mjs",
     "clean": "rm -rf dist"
   },
   "keywords": [

+ 152 - 0
scripts/verify-refs.mjs

@@ -0,0 +1,152 @@
+#!/usr/bin/env node
+/**
+ * verify-refs.mjs v2 —— 检查 dist 里文档引用的文件是否真实存在
+ * 区分:真实依赖(必须存在) vs 示例性提及(可忽略)
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+
+const args = process.argv.slice(2);
+const CWD = process.cwd();
+const SHOW_ALL = args.includes('--all');
+const pathArg = args.find(a => !a.startsWith('--'));
+const DIST = path.resolve(pathArg || path.join(CWD, 'dist'));
+const skillsRoot = path.join(DIST, 'skills');
+
+if (!fs.existsSync(skillsRoot)) {
+  console.error('dist/skills 不存在,先跑 npm run build');
+  process.exit(1);
+}
+
+const C = { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' };
+
+function allMd(dir, out = []) {
+  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+    const p = path.join(dir, e.name);
+    if (e.isDirectory()) allMd(p, out);
+    else if (e.name.endsWith('.md')) out.push(p);
+  }
+  return out;
+}
+
+function extractRefs(text) {
+  const refs = new Set();
+  const patterns = [
+    /`([^`\n]+\.(?:md|mjs|js|cjs|json|sh|py|ts|html|css))`/g,
+    /\]\(\.?\/?([^)\s]+\.(?:md|mjs|js|json|sh|py))\)/g,
+    /(?:详见|参见|见|参考)\s*[`「]?([\w./-]+\.(?:md|mjs|js|json|sh|py))/g,
+  ];
+  for (const re of patterns) {
+    let m;
+    while ((m = re.exec(text)) !== null) {
+      const p = m[1].trim();
+      if (!p.includes(' ')) refs.add(p);
+    }
+  }
+  return [...refs];
+}
+
+/** 判定是否为示例性提及(无需真实存在) */
+function isExample(ref) {
+  // 通配符 / 通配占位
+  if (/[*?{}]/.test(ref)) return true;
+  // 占位符形式
+  if (/<[^>]+>/.test(ref)) return true;
+  // 通用命名示例
+  if (/^(kebab-case|PascalCase|xxx|Xxx|your-|my-|page-)/.test(ref)) return true;
+  // 典型项目入口文件(描述用户项目结构,非技能资产)
+  if (/^(\.?\/)?(index\.html|app\.js|index\.js|main\.js|plain\.html|theme\.css)$/.test(ref)) return true;
+  // 用户项目目录结构示例(技能教用户怎么建,文件由用户项目生成)
+  if (/^(src|lib|components|pages|routes|services|utils|public|assets|dist|build|workdir|user|modules)\//.test(ref)) return true;
+  // 运行时生成物(技能运行时产出,非技能包内资产)
+  if (/^(mint\.json|collect\.sh|url\.txt|report\.html|.*\.log)$/.test(ref)) return true;
+  // 用户环境配置文件(由平台/用户在运行环境生成)
+  if (/(fmode-identity\.json|settings\.local\.json|config\.json|\.fmode\/)/.test(ref)) return true;
+  // 平台内部路径(不属于技能资产)
+  if (/^\/npm\/|^\/index\.html$/.test(ref)) return true;
+  // 用户上传 key 路径示例(uploader --key 的示例值)
+  if (/^[a-z0-9-]+\/(index\.html|.*\.html)$/.test(ref) && !ref.startsWith('references/')) return true;
+  // 代码文件名示例(文档教用户按此命名创建,非技能自带资产)
+  if (/^(validate|machine|format|storage|browser-transport|utils|helpers)\.js$/.test(ref)) return true;
+  if (/^(scripts\/check\.mjs|scripts\/deploy\.sh)$/.test(ref)) return true;
+  // 示例路径(如 ./components/x.js,教用户按此建目录)
+  if (/^\.?\/?(components|routes|services|utils|modules)\/[a-z0-9-]*x?\.js$/.test(ref)) return true;
+  // 历史继承来源说明(如“继承自 xxx-v4.mjs”,旧版本已被替代)
+  if (/^analyze-photos-v\d.*\.mjs$/.test(ref)) return true;
+  // 外部规范/标准文档引用(不在本技能包内分发)
+  if (/^[a-z-]+\/[a-z-]*standard\.md$/.test(ref)) return true;
+  return false;
+}
+
+function isExternal(ref) {
+  if (/^https?:\/\//.test(ref)) return true;
+  if (/^(node:|npm |npx |pip |git )/.test(ref)) return true;
+  if (ref.startsWith('/opt/') || ref.startsWith('/root/') || ref.startsWith('~')) return true; // 环境绝对路径
+  if (ref.startsWith('./.') || /^\.fmode\//.test(ref)) return true;  // 用户环境配置
+  return false;
+}
+
+const results = [];
+
+for (const skill of fs.readdirSync(skillsRoot)) {
+  const skillDir = path.join(skillsRoot, skill);
+  if (!fs.statSync(skillDir).isDirectory()) continue;
+
+  const mds = allMd(skillDir);
+  const critical = [];   // 真实依赖缺失(严重)
+  const examples = [];   // 示例性提及(可忽略)
+  const checked = new Set();
+
+  for (const mdFile of mds) {
+    const text = fs.readFileSync(mdFile, 'utf8');
+    const relDoc = path.relative(skillDir, mdFile);
+    const docDir = path.dirname(mdFile);
+
+    for (const ref of extractRefs(text)) {
+      if (isExternal(ref)) continue;
+      const key = `${relDoc}::${ref}`;
+      if (checked.has(key)) continue;
+      checked.add(key);
+
+      const candidates = [
+        path.resolve(docDir, ref),
+        path.resolve(skillDir, ref),
+        path.resolve(skillDir, 'references', ref),
+        path.resolve(skillDir, 'scripts', ref),
+        path.resolve(skillDir, 'scripts/prompts', ref),
+        path.resolve(skillDir, 'assets', ref),
+        path.resolve(skillDir, 'assets/theme', ref),
+        path.resolve(skillDir, ref.replace(/^references\//, '')),
+      ];
+      const found = candidates.some(c => fs.existsSync(c) && fs.statSync(c).isFile());
+      if (found) continue;
+
+      // CHANGELOG 里的历史文件名不算依赖
+      if (/CHANGELOG/i.test(relDoc)) continue;
+
+      (isExample(ref) ? examples : critical).push({ doc: relDoc, ref });
+    }
+  }
+
+  results.push({ skill, docs: mds.length, checked: checked.size, critical, examples });
+}
+
+let totalCritical = 0;
+console.log(`\n${C.b}文档引用完整性检查${C.x}  (dist/skills)\n`);
+
+for (const r of results) {
+  if (r.critical.length === 0) {
+    const ex = r.examples.length ? ` ${C.d}(示例性提及 ${r.examples.length} 处,可忽略)${C.x}` : '';
+    console.log(`${C.g}✓${C.x} ${r.skill} ${C.d}(${r.docs}篇/${r.checked}处)${C.x}${ex}`);
+  } else {
+    totalCritical += r.critical.length;
+    console.log(`${C.r}✗${C.x} ${r.skill} ${C.d}— ${r.critical.length} 处真实依赖缺失${C.x}`);
+    for (const m of (SHOW_ALL ? r.critical : r.critical.slice(0, 8))) {
+      console.log(`    ${C.r}${m.ref}${C.x}  ${C.d}← ${m.doc}${C.x}`);
+    }
+    if (!SHOW_ALL && r.critical.length > 8) console.log(`    ${C.d}...还有 ${r.critical.length - 8} 处 (--all 查看全部)${C.x}`);
+  }
+}
+
+console.log(`\n${C.b}汇总${C.x}: ${results.length} 技能 | ${C.r}真实依赖缺失 ${totalCritical} 处${C.x}\n`);
+process.exit(totalCritical > 0 ? 1 : 0);