blog-assets 桶创建与配置方案

1. 目标

在现有 MinIO 服务上新增一个 blog-assets 桶,用于存放博客/知识库的非图片附件(trace 文件、zip 包、PDF 等)。

1.1 核心动机

当前 fetch-feishu skill 爬取飞书文档时:

  • 图片:可以上传到 minio.898311.xyz/upload/api/upload(只接受图片格式)
  • 非图片附件(.pftrace、.zip、.tar.gz、.pdf 等):无法上传,只能存本地 attachments/ 目录

实际案例(触发本方案的场景):

附件: 3 个已下载到本地
├─ attachments/perfetto_1779783908.pftrace (1.7MB, 原始分析 trace)
├─ attachments/perfetto_trace_reproduce.pftrace (427MB, 开关复现 trace)
└─ attachments/analysis_skill.zip (20KB, 分析 skill 包)

图床服务只允许图片格式上传,trace/zip 文件无法上传到 blogfile 桶,
所以附件保存在本地 attachments/ 子目录并在文档中用相对路径引用

目标:让 fetch-feishu skill 能把这些附件上传到 blog-assets 桶,生成公网可访问的 URL,替代本地相对路径引用。

1.2 核心要求

  • 匿名可下载:任何人通过 URL 直接访问,无需凭证
  • 上传需认证:复用现有 uploadrw0511a 账号写入
  • 内外网统一:通过现有 frpc 隧道,公网自动可用
  • skill 可调用:提供简单的 mc cp 命令即可完成上传,不依赖图床 API

2. 前置条件

执行前确认以下条件满足:

条件确认方式
MinIO 服务运行中systemctl status miniomc ls local
mc 已安装且 local alias 可用mc ls local 返回桶列表
uploadrw0511a 账号存在mc admin user list local 中可见
策略文件位置已知仓库内 main/blog/devops/misc/minio-upload-rw-policy.json
公网 MinIO API 入口已确认使用 http://tx2.898311.xyz:9010,不要使用 minio.898311.xyz
现有大文件 MinIO 工作流已稳定参考 minio_migration_plan.mdpublic_minio_remote_clone_plan.mdremote_bootstrap_validation.md

2.1 本方案复用的既有能力

blog-assets 不是单独新造一套对象存储链路,而是复用当前已经验证通过的 MinIO 基础设施和运维规范:

  • 公网 API 入口已经固定为 http://tx2.898311.xyz:9010
  • 远程访问主模型已经固定为:mc + 受限账号 + 环境变量自动配置 alias
  • uploadrw0511a 已经是现有上传/恢复链路使用的普通账号模型
  • mc cp + mc stat 已经是仓库大文件上传流程的既有约束

这里复用的是:

  • MinIO 基础设施
  • 认证与连接方式
  • 运维命令与校验方式

这里复用的是:

  • generalandroid 的业务职责定义
  • generalandroid 的对象归属边界
  • “超大附件回退到 generalandroid” 这类旧思路

因此本方案的重点不是证明“能不能接入 MinIO”,而是补齐 blog-assets 的目录规范、权限边界、缓存策略、生命周期和验收闭环。

2.2 为什么要单独扩展 blog-assets

当前 GitLab 仓库和备份体系已经承接了大量历史大文件,继续把飞书附件、博客 trace、zip、pdf 直接放入仓库,会继续放大:

  • 仓库工作区体积
  • .git 历史对象体积
  • GitLab 备份体积
  • clone / pull 的传输压力

因此 blog-assets 的定位应该明确为:

  • 用于博客 / 知识库附件的公开分发桶
  • 尽量让文档附件走对象存储,而不是继续进 Git / GitLab 备份链路
  • generalandroid 完全职责分离:generalandroid 继续服务仓库大文件迁移与恢复,blog-assets 负责博客 / 文档附件分发
  • 即使是超大附件,只要语义上属于博客 / 知识库附件,也仍然进入 blog-assets,不回退到 generalandroid
  • 判定上传目标只看内容归属,不看文件大小

3. 执行步骤

步骤 1:创建桶

mc mb local/blog-assets

预期输出:

Bucket created successfully `local/blog-assets`.

验证:

mc ls local | grep blog-assets

步骤 2:设置匿名下载权限

mc anonymous set download local/blog-assets

预期输出:

Access permission for `local/blog-assets` is set to `download`

验证(用 curl 测试匿名访问能力):

# 先上传一个测试文件
echo "blog-assets-public-test" | mc pipe local/blog-assets/test/hello.txt
 
# 用 curl 匿名下载(不带任何认证头)
curl -s http://127.0.0.1:9000/blog-assets/test/hello.txt

预期返回:

blog-assets-public-test

验证公网访问(如果当前机器可以访问公网入口):

curl -s http://tx2.898311.xyz:9010/blog-assets/test/hello.txt

预期返回同上。

清理测试文件:

mc rm local/blog-assets/test/hello.txt

步骤 3:更新 uploadrw0511a 策略文件

编辑策略文件,将 blog-assets 加入桶级和对象级权限列表。

策略文件位置(仓库内):

main/blog/devops/misc/minio-upload-rw-policy.json

更新后的完整内容:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": [
        "arn:aws:s3:::generalandroid",
        "arn:aws:s3:::blogfile",
        "arn:aws:s3:::upload",
        "arn:aws:s3:::tmp-image",
        "arn:aws:s3:::blog-assets"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts"
      ],
      "Resource": [
        "arn:aws:s3:::generalandroid/*",
        "arn:aws:s3:::blogfile/*",
        "arn:aws:s3:::upload/*",
        "arn:aws:s3:::tmp-image/*",
        "arn:aws:s3:::blog-assets/*"
      ]
    }
  ]
}

步骤 4:应用策略到 MinIO

mc admin policy create local upload-rw-policy main/blog/devops/misc/minio-upload-rw-policy.json

如果策略已存在会自动覆盖(create = create or replace)。

预期输出:

Created policy `upload-rw-policy` successfully.

确认策略已关联到用户:

mc admin policy entities local --user uploadrw0511a

如果输出中没有 upload-rw-policy,则手动关联:

mc admin policy attach local upload-rw-policy --user uploadrw0511a

步骤 5:验证 uploadrw0511a 对新桶的读写能力

用 uploadrw alias 验证(如果本机已有 uploadrw alias):

# 列桶
mc ls uploadrw/blog-assets
 
# 上传
echo "uploadrw-write-test" | mc pipe uploadrw/blog-assets/test/write-check.txt
 
# 读取
mc cat uploadrw/blog-assets/test/write-check.txt
 
# 删除
mc rm uploadrw/blog-assets/test/write-check.txt

如果本机没有 uploadrw alias,可以临时创建:

mc alias set uploadrw http://tx2.898311.xyz:9010 uploadrw0511a <SECRET_KEY>

步骤 6:验证权限边界未被放大

确认 uploadrw0511a 对未授权桶仍然拒绝:

mc ls uploadrw/blogimg

预期输出:

ERROR ... Access Denied.

步骤 7:验证正式前缀上传链路

除了测试目录,还要至少验证一次正式前缀写入,确保后续 skill / 文档流程可直接复用。

推荐验证:

printf 'blog-assets-feishu-check\n' | mc pipe local/blog-assets/feishu/acceptance-check/readme.txt
mc stat local/blog-assets/feishu/acceptance-check/readme.txt
curl -s http://tx2.898311.xyz:9010/blog-assets/feishu/acceptance-check/readme.txt
mc rm local/blog-assets/feishu/acceptance-check/readme.txt

预期:

  • mc pipe 成功
  • mc stat 成功
  • curl 返回 blog-assets-feishu-check
  • 清理成功

步骤 8:更新自动化与使用约束

完成桶和策略配置后,还要补齐使用约束,避免后续重新回到“随意上传、覆盖、不可追溯”的状态。

至少确认以下事项:

  1. 文档和 skill 统一使用 http://tx2.898311.xyz:9010 作为公网访问前缀
  2. 自动化脚本或 skill 可先复用现有环境变量:
    • GENERALANDROID_MC_ALIAS
    • GENERALANDROID_MC_ENDPOINT
    • GENERALANDROID_MC_ACCESS_KEY
    • GENERALANDROID_MC_SECRET_KEY 这些变量只表示 MinIO 连接配置,不表示对象应上传到 generalandroid 桶;后续如需彻底消歧义,可再改为更中性的命名。
  3. 上传完成后必须执行 mc stat 再输出 URL
  4. 临时验收对象统一放 test/acceptance-check/,完成后立即删除
  5. 不要把 minio.898311.xyz 写成 mc / SDK endpoint

4. 最终验证清单

全部执行完成后,逐条检查:

#检查项命令预期结果
1桶已创建mc ls local | grep blog-assets列出 blog-assets/
2匿名可下载curl -s http://127.0.0.1:9000/blog-assets/<test-file>返回文件内容
3公网可访问curl -s http://tx2.898311.xyz:9010/blog-assets/<test-file>返回文件内容
4uploadrw 可写mc pipe uploadrw/blog-assets/test.txt成功
5uploadrw 可删mc rm uploadrw/blog-assets/test.txt成功
6权限边界未扩大mc ls uploadrw/blogimgAccess Denied

5. 使用方式

5.1 对象 key 规划

blog-assets 采用“单桶 + 分类前缀”模式,不再按多桶拆分。

推荐固定为三类主前缀:

blog-assets/
├── feishu/
│   └── <document_id>/
├── blog/
│   └── <yyyy>/<mm>/
└── shared/
    └── <topic>/

对应约束:

  • feishu/:飞书文档抓取产生的非图片附件,按 document_id 隔离
  • blog/:普通博客文章附件,按年月归档
  • shared/:长期复用的公共资源,按主题归档
  • test/:仅用于验收或临时调试,不属于正式内容前缀

5.2 对象命名规则

对象命名优先保证:

  1. 来源可追溯
  2. URL 可读
  3. 不依赖覆盖同名对象更新内容
  4. 能避免缓存脏读和同名冲突

建议规则:

  • 保留原始文件名的主体语义
  • 文件名中的空格统一替换为 _
  • 避免目录名和文件名中出现 shell 特殊字符
  • 如果存在重名风险,在文件名末尾追加短 hash 或时间戳

示例:

blog-assets/feishu/UziqdXdeEo/perfetto_trace_reproduce-7f3a2c1d.pftrace
blog-assets/blog/2026/05/renderdoc-analysis-v2.pdf
blog-assets/shared/perfetto/analysis_skill-a13d90be.zip

禁止把“覆盖上传同名文件”作为缓存失效手段。需要更新内容时,应直接生成新对象 key。

5.3 上传文件

飞书附件推荐:

mc cp /path/to/file.pftrace local/blog-assets/feishu/<document_id>/file.pftrace
mc stat local/blog-assets/feishu/<document_id>/file.pftrace

普通博客附件推荐:

mc cp /path/to/attachment.pdf local/blog-assets/blog/2026/05/attachment.pdf
mc stat local/blog-assets/blog/2026/05/attachment.pdf

公共共享资源推荐:

mc cp /path/to/archive.zip local/blog-assets/shared/perfetto/archive.zip
mc stat local/blog-assets/shared/perfetto/archive.zip

上传动作完成后,必须再执行一次 mc stat 确认对象真实存在、size 正常,再把 URL 写入文档或脚本输出。

5.4 在文档中引用

Markdown 中直接使用公网 URL:

[分析报告.pdf](http://tx2.898311.xyz:9010/blog-assets/blog/2026/05/analysis-report.pdf)

对于 trace / zip / 大附件,建议写明文件大小或用途:

[perfetto_trace_reproduce.pftrace (427MB)](http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/perfetto_trace_reproduce.pftrace)

5.5 缓存策略

当前阶段先稳定对象路径和命名规则,再考虑 CDN。

建议约束:

  • 稳定附件默认按“可长缓存”设计
  • 内容变更通过生成新对象 key实现,而不是覆盖原对象
  • 文档中一旦引用外链,尽量不要复用同名对象承载不同内容
  • 如果后续接 CDN,也继续复用“对象 key 版本化”的方式做缓存失效

5.6 生命周期与清理策略

当前建议按前缀区分保留策略:

前缀默认策略说明
feishu/长期保留,后续按引用关系治理先保证链接稳定,再考虑去重或清理
blog/长期保留已发布文档的附件不应随意删除
shared/长期保留公共复用资源,优先稳定
test/短期保留验收完成后立即清理

在自动生命周期规则真正落地前,至少保证:

  • 所有 test/ 对象在验收后立即删除
  • 临时实验对象不要混入 blog/ / shared/ 正式前缀
  • 对正式资源的清理必须以“是否仍被引用”为前提,而不是只看上传时间
  • 超大附件如果属于博客 / 知识库附件,也继续留在 blog-assets 对应前缀,不迁回 generalandroid

6. 回滚方案

如果需要撤销本次操作:

# 删除桶(桶必须为空才能删除)
mc rb local/blog-assets --force
 
# 恢复旧策略(去掉 blog-assets 相关行后重新应用)
mc admin policy create local upload-rw-policy <旧策略文件路>

7. 与现有架构的关系

用途访问方式
generalandroid仓库大文件迁移托管 / 研发资产恢复;不承接博客 / 知识库发布附件需认证(mc alias)
blogfile已有博客文件需认证
upload通用上传需认证
tmp-image临时图片需认证
blog-assets(新)博客 / 知识库附件分发(含超大附件)匿名可下载,上传需认证

上传目标判定规则:只看内容归属,不看文件大小。凡属于博客 / 知识库对外引用附件,即使超大,也进入 blog-assetsgeneralandroid 不作为此类附件的兜底桶。

8. fetch-feishu skill 改造方案

8.1 当前行为(改造前)

飞书文档附件(非图片)
  → media_download 下载到本地 /tmp/feishu_images/
  → 无法上传图床(格式限制)
  → 保存到 attachments/ 子目录
  → 文档中用相对路径引用

8.2 改造后行为

飞书文档附件(非图片)
  → media_download 下载到本地 /tmp/feishu_attachments/
  → mc cp 上传到 blog-assets 桶
  → 获得公网 URL
  → 文档中用公网 URL 引用

8.3 skill 中需要修改的部分

fetch-feishu.md 中新增附件处理步骤,位于图片处理之后。

新增:附件上传命令

# 上传附件到 blog-assets(需要 mc 已配置 alias)
mc cp /tmp/feishu_attachments/<文件> local/blog-assets/feishu/<document_id>/<文件>
 
# 返回的公网 URL 格式:
# http://tx2.898311.xyz:9010/blog-assets/feishu/<document_id>/<文件名>

新增:skill 输出中的附件引用格式

对于不同类型的附件,在 Markdown 中用不同方式引用:

# PDF / 文档类 → 链接
[分析报告.pdf](http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/分析报告.pdf)
 
# Trace / 数据文件 → 链接 + 大小标注
[perfetto_trace.pftrace (427MB)](http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/perfetto_trace.pftrace)
 
# Zip / 压缩包 → 链接 + 说明
[analysis_skill.zip (20KB)](http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/analysis_skill.zip)

修改:skill 的”供其他 skill 调用”部分

新增附件上传命令说明:

- 附件上传命令(非图片文件:trace、zip、pdf 等):
  ```bash
  mc cp <附件路径> local/blog-assets/feishu/<document_id>/<文件名>

公网访问 URL:http://tx2.898311.xyz:9010/blog-assets/feishu/<document_id>/<文件名>


### 8.4 目录规范

blog-assets/ └── feishu/ └── <document_id>/ ← 以飞书文档 ID 为目录名 ├── perfetto_xxx.pftrace ├── analysis_skill.zip └── report.pdf


用 document_id 作为子目录好处:
- 天然去重(同一文档多次爬取不会冲突)
- 方便追溯来源
- 清理时按文档维度操作

### 8.5 skill 可用的上传接口

桶创建完成后,skill 有以下方式完成附件上传:

#### 方式 A:mc cp(推荐,最简单)

```bash
mc cp <本地文件路径> local/blog-assets/feishu/<document_id>/<文件名>

返回公网 URL:

http://tx2.898311.xyz:9010/blog-assets/feishu/<document_id>/<文件名>

完整示例:

# 上传
mc cp /tmp/feishu_attachments/perfetto_trace.pftrace local/blog-assets/feishu/UziqdXdeEo/perfetto_trace.pftrace
 
# 验证上传成功
mc stat local/blog-assets/feishu/UziqdXdeEo/perfetto_trace.pftrace

适用场景:执行 skill 的机器已安装 mc 且 alias 已配置。

方式 B:curl + S3 PUT(无需 mc,适合轻量环境)

因为桶设置了 public download,但上传仍需认证。可用 S3 签名方式 PUT:

# 需要 ACCESS_KEY 和 SECRET_KEY
ENDPOINT="http://tx2.898311.xyz:9010"
BUCKET="blog-assets"
OBJECT_KEY="feishu/<document_id>/<文件名>"
FILE="/tmp/feishu_attachments/<文件名>"
 
# 使用 mc 内置的 pipe 能力(比手写签名简单)
mc pipe local/${BUCKET}/${OBJECT_KEY} < "${FILE}"

或者直接用 Python(如果 skill 在 Python 环境中执行):

import subprocess, os
 
def upload_to_blog_assets(local_path, document_id, filename, alias="local"):
    """上传文件到 blog-assets 桶,返回公网 URL"""
    object_key = f"blog-assets/feishu/{document_id}/{filename}"
    cmd = ["mc", "cp", local_path, f"{alias}/{object_key}"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode == 0:
        return f"http://tx2.898311.xyz:9010/{object_key}"
    return None

方式 C:扩展现有上传页 API(可选,需改服务端)

当前图床 API 接口格式:

curl -sS -X POST https://minio.898311.xyz/upload/api/upload \
  -F "file=@<文件路径>" -F "bucket=blog-assets" | jq -r '.url'

但当前服务端 只允许图片格式。如果后续扩展上传页支持任意文件类型 + blog-assets 桶,则 skill 可以统一用这个 HTTP API,无需 mc

当前推荐方式 Amc cp 最简单可靠,且执行 skill 的机器(本机)已有 mc

接口对比表

方式命令依赖适用场景
A: mc cpmc cp <file> local/blog-assets/...mc + alias本机 skill 执行(推荐
B: mc pipemc pipe local/blog-assets/... < filemc + aliasstdin 管道场景
C: curl PUT需手写 AWS4 签名curl无 mc 环境(复杂,不推荐)
D: 上传页 APIcurl -F file=@... -F bucket=blog-assetscurl需服务端改造后可用

skill 改造后的附件上传伪代码

# 在 fetch-feishu skill 的附件处理步骤中:
 
DOCUMENT_ID="<从步骤1解析出的document_id>"
ATTACH_DIR="/tmp/feishu_attachments"
 
for file in ${ATTACH_DIR}/*; do
  filename=$(basename "$file")
  # 上传
  mc cp "$file" "local/blog-assets/feishu/${DOCUMENT_ID}/${filename}"
  # 生成 URL
  URL="http://tx2.898311.xyz:9010/blog-assets/feishu/${DOCUMENT_ID}/${filename}"
  # 在 Markdown 中替换为 URL 引用
  echo "[${filename}](${URL})"
done

8.6 skill 改造的前置条件

条件说明
blog-assets 桶已创建本文档步骤 1-6 全部完成
执行 skill 的机器有 mclocal alias 或环境变量已配置
mc cp 可成功写入 blog-assets步骤 5 验证通过

8.7 兜底逻辑

如果 mc 不可用或上传失败:

  • 回退到当前行为(本地 attachments/ + 相对路径引用)
  • 在输出中标注哪些附件未能上传

8.8 改造后的 skill 输出示例

文档已保存: /home/zbc/pangu/GeneralAndroid/main/爬取的文章/<标题>.md
- 标题: xxx
- 字数: xxx
- 图片: 3/3 张已上传图床
- 附件: 3/3 个已上传 blog-assets
  ├─ perfetto_1779783908.pftrace → http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/perfetto_1779783908.pftrace
  ├─ perfetto_trace_reproduce.pftrace → http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/perfetto_trace_reproduce.pftrace
  └─ analysis_skill.zip → http://tx2.898311.xyz:9010/blog-assets/feishu/abc123/analysis_skill.zip

9. 完整实施顺序

  1. 服务器操作(本文档步骤 1-8):建桶 → 设匿名 → 更新策略 → 验证 → 补齐使用约束
  2. skill 改造(本文档 §8):修改 fetch-feishu.md,新增附件上传逻辑
  3. 验证闭环:爬一篇带附件的飞书文档,确认附件公网 URL 可访问且上传后有 mc stat 校验

10. 验收标准

本方案完成后,至少要满足以下验收项:

#验收项通过标准
1blog-assets 桶已创建mc ls local 可见 blog-assets/
2匿名下载生效本地和公网 curl 都能读取测试对象
3uploadrw0511a 可读写删mc ls / mc pipe / mc cat / mc rm 均成功
4权限边界未扩大对未授权桶仍返回 Access Denied
5正式前缀可用feishu/blog/ 前缀上传、读取、清理闭环通过
6URL 规范统一文档与脚本都使用 http://tx2.898311.xyz:9010
7上传校验规则落实所有示例都体现“上传后必须 mc stat
8清理规则明确验收测试对象会被删除,不混入正式前缀

11. 已完成事项 / 待完成事项

11.1 本次文档已完成

  • 明确 blog-assets 的定位为公开附件分发桶
  • 补充公网 endpoint 统一约束
  • 补充单桶 + 分类前缀目录规范
  • 补充对象命名规则、缓存规则、生命周期规则
  • 补充正式前缀验收与自动化约束
  • 明确上传后必须 mc stat 校验

11.2 仍需实际执行的事项

以下事项当前只是写入方案,还没有被实际执行

  1. 在 MinIO 上真实创建 blog-assets
  2. blog-assets 应用匿名下载权限
  3. 修改并重新应用 main/blog/devops/misc/minio-upload-rw-policy.json
  4. 验证 uploadrw0511ablog-assets 的真实读写删能力
  5. 验证正式前缀和公网 URL 的真实可用性
  6. 修改 fetch-feishu skill,把非图片附件切换到 blog-assets

如果要继续推进到“方案落地完成”,下一步应直接执行这些命令并逐项验收,而不是只停留在文档层。

12. 后续可选增强

当前方案已满足基本需求。后续如有需要可继续扩展:

  1. 大文件上传治理:为 blog-assets 单独定义超大附件命名规范、上传校验和分目录策略,但仍然留在 blog-assets
  2. CDN 缓存:在 frpc 前加一层 Cloudflare 或 Nginx 缓存
  3. HTTPS:如果需要 HTTPS URL,在反代层加证书(目前 tx2.898311.xyz:9010 是 HTTP)
  4. 生命周期规则:对临时测试文件设置自动过期清理
  5. 上传页扩展:如果图床 minio.898311.xyz 的上传 API 也支持非图片格式,则可以不走 mc 而直接走 HTTP API