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

merge: 保留完整版 SKILL.md(含大文件网关章节)

liuyuyang 5 часов назад
Родитель
Сommit
8d9a31c888
3 измененных файлов с 31 добавлено и 107 удалено
  1. 27 0
      CHANGELOG.md
  2. 2 2
      package.json
  3. 2 105
      skills/fmode-storage/SKILL.md

+ 27 - 0
CHANGELOG.md

@@ -0,0 +1,27 @@
+## 1.2.0(2026-09-23)
+
+依据 agent-node-xinting 的缺陷报告(`s3.fmode.cn/user/dhv9yTqK6K/skill-storage-mime-bug-report/report.md`)修复。
+
+- **fix(P0) 透传 `mimeType`**:`putViaCloudFunction` 从不向云函数传 mimeType,导致对象
+  Content-Type 恒为 `application/octet-stream` → 浏览器**下载而非渲染**,HTML 报告分享后打不开。
+  预签名 URL 的签名**绑定 Content-Type**,必须在申请时就传对,PUT 阶段补救会 403。
+- **feat(P0) `guessMime()`**:按扩展名推断 MIME,覆盖 30+ 常见类型
+  (html/css/js/mjs/json/txt/md/csv/xml/svg/png/jpg/gif/webp/ico/pdf/zip/音视频/字体/Office)。
+- **feat(P1) `--mime` 显式覆盖**:应对未知或特殊类型。
+- **fix(P1) `normalizeKey()` 双层前缀防护**:`--key` 若已带 `user/<id>/`,云函数会再补一层
+  变成 `user/<id>/user/<id>/...`。现自动剥离并打印告警。
+- **fix(P1) 覆盖同名 key 时提示 CDN 缓存**:CDN TTL 30 天且缓存**含 Content-Type 头**,
+  覆盖上传后旧链接可能仍返回旧响应头;提示改用新 key 路径发布。
+- **fix(P1) token 解析链补 `fmode-identity.json` 兜底**:容器普遍只有
+  `/opt/data/fmode-identity.json`(字段 `session_token`)带 token,缺此兜底会误报「缺 token」
+  并打印初始化向导退出。实测 34 台容器中 32 台属此情况。
+- **feat(P2) 输出回显 `mimeType`**:便于核对,降低排查成本。
+- **docs(P1) 版本口径统一**;补 CDN 缓存与更新策略说明。
+
+### 线上验收(生产实测)
+
+| 文件 | Content-Type | 结果 |
+|------|--------------|------|
+| `index.html` | `text/html; charset=utf-8` | ✅ 浏览器渲染(修复前 octet-stream → 下载)|
+
+# 更新日志

+ 2 - 2
package.json

@@ -1,7 +1,7 @@
 {
   "name": "fmode-storage",
-  "version": "1.1.0",
-  "description": "对象存储上传/公开分享(OBS / S3 兼容)。首选免密钥通道:sessionToken → 云函数预签名直传(公网 https://s3.fmode.cn/<key>);兜底 4 级凭据链。超级技能(ESM 多端可用),零依赖 Node ≥18。",
+  "version": "1.2.0",
+  "description": "对象存储上传/公开分享技能(华为云 OBS / S3 兼容)。首选免密钥通道:sessionToken → 云函数 fmodeagent-upload-url 预签名直传;自动推断并透传 Content-Type(HTML/CSS/JS/SVG 上传后可浏览器直接渲染,不再触发下载)。零依赖 Node ≥18。",
   "type": "module",
   "main": "./skills/fmode-storage/scripts/uploader.mjs",
   "exports": {

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

@@ -12,11 +12,7 @@ keywords:
   - upload
   - cdn
   - share-link
-  - bigfile
-  - gateway
-  - presigned
-  - sigv4
-version: 1.2.0
+version: 1.1.0
 author: Yuyang001 (FmodeAgent)
 license: MPL-2.0
 copyright: "Copyright (c) 2026 未来飞马 Fmode"
@@ -53,30 +49,7 @@ node <skill_dir>/scripts/uploader.mjs test
 > 省略 `--key` 则自动生成 `user/<你的id>/report/<YYYYMM>/<uuid>-<文件名>`。
 > 上传即 `public-read`,**不需要**再调 `setacl`。
 
-## 项目级 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。
-
+## 凭据链(5 级,命中即用)
 
 ```
 ┌─ 第0级【首选·免密钥】sessionToken → 云函数 fmodeagent-upload-url 签发预签名 PUT
@@ -109,87 +82,11 @@ 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` 取回**
 
 ## 命令