Просмотр исходного кода

fix: 恢复完整版 SKILL.md(含大文件网关 gateway 章节+坑库)

liuyuyang 5 часов назад
Родитель
Сommit
c45c89a1b8
1 измененных файлов с 105 добавлено и 2 удалено
  1. 105 2
      skills/fmode-storage/SKILL.md

+ 105 - 2
skills/fmode-storage/SKILL.md

@@ -12,7 +12,11 @@ keywords:
   - upload
   - cdn
   - share-link
-version: 1.1.0
+  - bigfile
+  - gateway
+  - presigned
+  - sigv4
+version: 1.2.0
 author: Yuyang001 (FmodeAgent)
 license: MPL-2.0
 copyright: "Copyright (c) 2026 未来飞马 Fmode"
@@ -49,7 +53,30 @@ node <skill_dir>/scripts/uploader.mjs test
 > 省略 `--key` 则自动生成 `user/<你的id>/report/<YYYYMM>/<uuid>-<文件名>`。
 > 上传即 `public-read`,**不需要**再调 `setacl`。
 
-## 凭据链(5 级,命中即用)
+## 项目级 Storage 正确姿势(2026-10-12 实测修复)
+
+**不要用 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。
+
+坑:①赖着 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。
+
 
 ```
 ┌─ 第0级【首选·免密钥】sessionToken → 云函数 fmodeagent-upload-url 签发预签名 PUT
@@ -82,11 +109,87 @@ node <skill_dir>/scripts/uploader.mjs put ./data.bin --key x/data.bin --mime app
 > ⚠️ **覆盖同名 key 后可能"改了没生效"**:CDN 缓存 TTL 为 30 天,且缓存的是**整个响应(含 Content-Type 头)**。
 > 建议换新 key 路径发布(如 `about-me-v2/index.html`)。
 
+## 大文件网关(bigfile-gateway)— 通道超限时生成接收页
+
+**何时用**:主通道被**不可协商的大小上限**挡住时(典型:企微入站 20MB,用户发大文件报「超过 100M 无法发送」,
+文件从未到达智能体)。此时**不要再走聊天通道**——生成一个接收页,用户浏览器**直连对象存储**上传,
+再由 collect 取回。**平时不生成**,只在超限时触发。
+
+> 为什么必须对象存储中转:容器跑在 NAT 内网,**没有公网入口**,不能自建接收服务(踩坑 #4)。
+> 两段式「直传 + 取回」是唯一可行解。
+
+### 命令
+
+```bash
+# 生成 + 部署接收页(默认 20 槽位、1 小时有效),输出页面 URL
+node <skill_dir>/scripts/uploader.mjs gateway <outDir>
+# 等价:node <skill_dir>/scripts/gateway.mjs mint <outDir>
+
+# 常见参数
+node scripts/gateway.mjs mint ./gw --slots 100 --hours 1 \
+     --title "大文件接收网关 · 刘雨飏" \
+     --channel "企微入站" --limit 20MB \
+     --note "企微入站在 20MB 处直接拒收,文件在到达前就已不存在"
+# --project-slug <slug>   复用/注册的项目名(默认 bigfile-gateway,幂等)
+# --project-id <id>       直接指定已有项目 objectId
+# --slot-prefix <p>       槽位键前缀(默认 gateway/<时间戳>)
+# --no-deploy             只本地生成,不部署
+# --json                  机器可读输出
+
+# 取回(扫描到位文件 → 下载 → 台账)
+node scripts/gateway.mjs collect --manifest <outDir>/mint.json            # 只列
+node scripts/gateway.mjs collect --manifest <outDir>/mint.json --out ~/inbox  # 下载
+# 生成的 <outDir>/collect.sh 已封装以上命令:bash collect.sh [--list|--out DIR]
+```
+
+### 流程(5 步)
+
+1. `resolveSessionToken()` 取登录态(复用 uploader 的会话语义)
+2. 云函数 `/fmode/project-register` 注册/复用项目 → `projectId`(幂等)
+3. `POST /api/apig/deploy/huaweicloud {token, projectId}` → **项目级 STS(真身 1h)**
+4. **自己实现 SigV4** 为 N 个槽位各签一条预签名 PUT(host-only 签名,`UNSIGNED-PAYLOAD`),
+   渲染 `templates/gateway.html` → `<outDir>/index.html`,再用同一 STS 把页面传回 `dev/<pid>/bigfile/index.html`
+5. 输出**页面 URL**(`https://app.fmode.cn/dev/<pid>/bigfile/index.html`)+ `mint.json` + `collect.sh`
+
+用户传完 → `collect` 逐槽位 HEAD 探测 → 下载到本地 + 写 `ledger.jsonl`(只记 key/大小/路径)。
+
+### 页面特性
+
+单文件自包含、中文、移动端可用:拖拽/点选多文件、逐文件进度条、完成显示「上传成功 ✓」+ 查看链接;
+**不含任何 AK/SK/token**(只内联当次预签名 URL);文件只写各自槽位(`gateway/<ts>/0000.bin`…),互不影响。
+
+### ⚠️ 踩坑库(实测 2026-10-13,必守)
+
+| # | 坑 / 现象 | 根因 | 本实现的做法 |
+|---|---|---|---|
+| 1 | 不存在对象的公网 URL 被 CDN 回落成 **200 + text/html**(占位页),全被误判「有文件」 | 只看状态码判「到位」 | `looksLikeObject()`:**必须排除 `content-type: text/html`**;实测该桶对缺失 key 返回 404,但仍严格按此判 |
+| 2 | 签名标称 48h,用户**第二天传全失败** | 签名带 `X-Amz-Security-Token`,**STS 真身只 1h**;可用窗口 = min(签名, 1h) | 页面 `--hours` 默认 1、封顶 1h;用户过时须 **重新 mint + 重新部署**,不可沿用旧页 |
+| 3 | 读 `.obsutilconfig` 直接失败 | 里面是**占位符凭据**(`http://your-endpoint` + 假 AK/SK) | 不读它;凭据走项目 STS,签名自实现 |
+| 4 | 想「起 http 服务让用户传」外部连不上 | 容器在 NAT 内网、无公网入口 | 放弃自建接收端,用对象存储中转 |
+| 5 | 用云函数签的 URL 做上传页,用户点开已失效 | 云函数签名 `expires/ttl/expiresIn` 全被忽略,**恒定 282s** | **不用**云函数签名;只用 `/api/functions` 注册项目拿到项目 STS(1h),签名自实现 |
+| 6 | 页面发出后用户传不上去,来回沟通成本极高 | 生成→部署间有多道环节(签名/CORS/key 前缀/CDN) | **生成后必须先真传一个小文件实测**再发用户(本工具不改这条铁律) |
+| 7 | 页面放进站点项目后 key 与预期不符、取回扫不到 | 站点 `dist-deploy` 重写规则给绝对路径加项目前缀 | 页面**不塞独立站点项目**,直接用同一 STS 写入 `dev/<pid>/bigfile/`,key 与预期一致 |
+| 8 | bucket `NoSuchBucket` | **nova-cloud 桶在 cn-south-1**(north-4 无此桶) | endpoint 固定 `obs.cn-south-1.myhuaweicloud.com`,region `cn-south-1` |
+| 9 | 逐槽位扫描想用 `ListObjects` | **STS 策略不允许 List**(实测 403,只允许 prefix 内 GetObject/PutObject) | 取回靠**逐槽位 HEAD 探测**(`app.fmode.cn/<key>`),不依赖列举 |
+
+### 实测(2026-10-13,本机)
+
+- 页面:`https://app.fmode.cn/dev/OtEAl5m4JP/bigfile/index.html`(项目 `bigfile-gateway`→`OtEAl5m4JP`)
+- 真传:浏览器同一路径的预签名 PUT → slot `0000`/`0002` 上传 **200** → 公网 GET 回读 **200** 内容一致(含中文)
+- 取回:`collect --list` 命中 2/2;`collect --out` 下载 2 文件 + `ledger.jsonl` 全部成功
+- SigV4 预签名 PUT/GET、CORS 预检(回显 `content-type,x-obs-acl`)均为 200
+
+### 安全边界
+
+预签名 URL **只对单个 key 有 PUT 权限**(不能读/列/删其他对象)——链接即使公开,别人也只能往那一个空槽位写。
+页面 URL 本身公开:敏感资料用 `--hours 1` 短效并告知用户「用完即焚」。
+
 ## 何时用
 
 - 生成了 HTML 报告/课件要"发给别人看" → `put` 后把 URL 发出
 - 图片/音频/视频素材需要稳定外链 → `put`
 - 本地磁盘被大文件占满 → `put` 完成后可删本地原件
+- **用户发来的文件被通道上限挡住(如企微 20MB)→ `gateway` 生成接收页 + `collect` 取回**
 
 ## 命令