静态看板发布到阿里云 OSS
并绑定自有域名 · 完整操作指引
从”本地 HTML 看板”到”客户点开即看的 HTTPS 短链”
含工具脚本设计全过程、控制台截图与踩坑实录
| 项目 | 内容 |
|---|---|
| 适用场景 | 把本地做好的静态 HTML 看板(含 ECharts 等)发布到云端,用自有域名给客户一个短的、点开即看的 HTTPS 链接 |
| 后端 | 阿里云 OSS(国内正式交付);脚本同时兼容 Cloudflare R2(公开样板 / 备份) |
| 实战环境 | bucket:accunion-dashboard(华东1 杭州)|域名:kanban.accunion.cn|看板:BOM 成本差异分析 |
| 读者 | 需要复现这套流程的同事 / AI Agent(无需编程经验,照第 5 章命令执行即可;想改脚本看第 4 章) |
| 成文日期 | 2026-09-29 |
| 配套文件 | 工具/看板发布/ 下的 publish.py、build_single.py、secrets_file.py、init.py、密钥.txt、使用说明.md |
阅读顺序建议:第 2 章先建立整体认知 → 第 5 章照着跑通 → 跑通后再回头看第 4 章(脚本设计)和第 7 章(踩坑)。如果只想快速交付,只看第 5 章即可。
第 1 章 这份文档能帮你做什么
1.1 读完你能得到什么
本文完整记录了一次真实交付:把一个 136 KB 的静态 HTML 看板,发布到阿里云对象存储,绑定公司自有二级域名,配上 HTTPS 证书,最终给客户一个形如 https://kanban.accunion.cn/c003/bom 的短链接——客户点开就能看,不用下载、不用登录、地址栏干净。
文档中每一步都附了当时的控制台截图、实际执行的命令、真实输出和踩过的坑。照着做一遍,大约 1~2 小时可以跑通(其中大部分时间在等证书签发)。
1.2 前置条件清单
动手前请先确认以下各项都具备,缺一项都会卡住:
| # | 需要什么 | 说明 / 怎么确认 |
|---|---|---|
| 1 | 一个已备案的域名 | 国内 OSS 绑定自定义域名要求 ICP 备案。本例用 accunion.cn 的二级域名 kanban.accunion.cn。没有备案见 7.2 的替代方案 |
| 2 | 阿里云账号 + 实名认证 | 能进 OSS 控制台即可 |
| 3 | 本机能跑 Python 3.12 | 本文命令里的 Python 路径需替换成你本机的;依赖 oss2 / boto3 装在这个解释器上 |
| 4 | 做好的静态看板目录 | 目录里有 index.html(引用 styles.css / app.js / data.js 等本地文件) |
| 5 | (可选)Node.js | 仅当需要重新生成本文档时才用;发布流程本身不需要 |
1.3 名词表(先把这几个词对齐)
| 名词 | 含义 |
|---|---|
| OSS / Bucket | 阿里云对象存储,以及其中的”桶”。桶是顶层容器,本例只有一个桶 accunion-dashboard,靠路径前缀区分不同客户 |
| Object / Key | 桶里的一个个文件。Key 就是它的路径,如 c003/bom/index.html |
| 前缀 prefix | Key 的前半段。本文规定写成两段”客户码/看板码”,如 c003/bom,它直接就是链接的后半段 |
| 私有 / 公共读 | 私有=必须带签名才能访问;公共读=任何人拿到链接都能看。含真实财务数据一律私有 |
| 签名 URL | 给私有对象生成的临时访问链接,带 OSSAccessKeyId / Expires / Signature 三个参数,到期失效 |
| 单文件 | 把 CSS/JS/数据全部内联进一个 index.html。私有交付必须这样做,否则只签了首页、css/js 全是 403 |
| CNAME | 把自定义域名指向 OSS 的域名解析记录 |
| 短链 / 跳转壳 | 一个公开的极小 HTML,用 iframe 加载私有的真实看板,让客户地址栏保持短链 |
| RAM 用户 | 阿里云的子账号。用它生成 AccessKey,比用主账号 AK 安全得多 |
第 2 章 先想清楚:为什么这么设计
2.1 目标与约束
目标很朴素:让客户在微信/邮件里点一个链接,立刻看到看板。但有三条硬约束:
数据不能公开。看板里是客户真实的成本、供应商、单价,一旦设成公共读,链接被转发就等于泄露。
客户不能折腾。不能让客户下载文件、解压、装环境,也不能让链接长得吓人。
客户会越来越多。一家客户可能有好几个看板,结构必须能横向扩展,不能每家客户建一个桶。
2.2 四种交付方案的取舍
当时对比过四条路,最终选了第 4 条:
| 方案 | 做法 | 问题 | 结论 |
|---|---|---|---|
| ① 直接发文件 | 把 HTML 打包微信发给客户 | 客户要下载、双击打开;更新一次发一次;手机上打不开 | ❌ 体验差 |
| ② 公开直链 | 对象设公共读,直接给 URL | 数据裸奔;且 OSS 默认域名会强制下载(见 7.1) | ❌ 不安全 |
| ③ 私有 + 签名长链接 | 私有对象 + 7 天签名 URL | 安全,但链接一长串(含签名参数、中文还会被编码),且 7 天就得重发一次 | ⚠ 可用但难看 |
| ④ 私有 + 固定短链 | 真实文件私有,另加一个公开”跳转壳”指向它 | 链接固定不变、地址栏干净;真实数据仍受签名保护 | ✅ 最终采用 |
为什么不用”每家客户一个桶”?—— 桶多了权限要配多遍、AK 要授多个、运维成本高。一个桶靠前缀分层(c002/…、c003/…)就够,权限策略也只需写一份、限定这一个桶。
2.3 最终架构(这张表是全文核心)
一个看板在云端其实是 3 个对象,理解了这个,后面所有操作都顺理成章:
| 云端对象 | 权限 | 从哪来 | 作用 |
|---|---|---|---|
| c003/bom/index.html | 私有(签名访问) | 本地 看板/dist/index.html | 真实看板,含全部数据 |
| c003/bom | 公共读 | 脚本自动生成 | 跳转壳:iframe 加载真实页,客户访问入口 |
| c003 | 公共读 | 脚本自动生成 | 客户总入口:列出该客户所有看板 |
对应到链接:
单个看板 → https://kanban.accunion.cn/c003/bom
客户总入口 → https://kanban.accunion.cn/c003
2.4 三个”必须”(这三条是硬道理)
必须打成单文件。私有桶里每个对象都要单独签名,而 index.html 里是相对引用,签了首页也加载不到 css/js/data,页面必白屏。
必须绑自定义域名。OSS 默认域名会强制下载(响应头 x-oss-force-download),客户点了只会下载一个 html 文件。两条绕过办法实测均无效(见 7.1)。
证书必须”部署”,不只是”签发”。证书控制台显示”已签发”不等于生效,必须再点一次”部署到云资源”,否则 HTTPS 报证书不匹配(见 7.3)。这是本次最隐蔽的坑。
第 3 章 阿里云侧准备(控制台操作,带截图)
3.1 创建 RAM 用户并授权(不要动用主账号 AK)
在 RAM 控制台创建用户时会看到下面这个提示。请务必选”RAM 用户 AccessKey”,不要图省事用云账号 AccessKey——后者一旦泄露等于整个阿里云账号失守。

图 1 控制台的 AccessKey 选型提示(红框为当时标注的两条路)
为什么不用 STS Token:STS 临时凭证确实更安全(不用轮换),但要定时刷新 token,对”一个月发一两次看板”的场景太重。RAM 静态 AK + 最小权限策略已经足够。
创建用户(访问方式勾”OpenAPI 调用访问”):

图 2 创建 RAM 用户,勾选 OpenAPI 调用访问
AccessKey Secret 只在创建成功的那一瞬间显示一次,之后再也查不到。当场复制保存,否则只能删掉重建。
然后给这个用户挂一条最小权限策略(只准操作这一个桶,连建桶权限都不给):
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:GetBucketInfo", "oss:ListObjects",
"oss:PutObject", "oss:GetObject",
"oss:DeleteObject", "oss:PutObjectAcl"
],
"Resource": [
"acs:oss:*:*:accunion-dashboard",
"acs:oss:*:*:accunion-dashboard/*"
]
}
]
}
把上面两处 accunion-dashboard 换成你的桶名即可直接粘贴。完整文件见 工具/看板发布/ram-policy-example.json。
3.2 创建 Bucket
OSS 控制台 → 创建 Bucket。本例的关键设置如下:

图 3 创建 Bucket:名称、地域、存储类型

图 4 创建 Bucket:版本控制、读写权限等
| 设置项 | 本例取值 | 为什么 |
|---|---|---|
| 名称 | accunion-dashboard | 全局唯一,建议公司/项目维度命名,不要按客户命名(一个桶装所有客户) |
| 地域 | 华东1(杭州) | 决定 endpoint:oss-cn-hangzhou.aliyuncs.com,必须和密钥文件里的 endpoint 一致 |
| 读写权限 | 私有 | 含真实财务数据,绝不开公共读 |
| 存储类型 | 低频访问 | 看板访问频次低,更便宜;有 30 天最短存储期,看板体量下可忽略 |
| 版本控制 | 不开 | 每次发布都是整文件覆盖,不需要历史版本 |
| 阻止公共访问 | 保持关闭 | ⚠ 短链跳转壳依赖”单对象可设公共读”。此项若开启,短链会失效(详见 7.6) |
建好后可以在”基础信息”里核对一遍:

图 5 Bucket 创建完成,核对地域、读写权限与存储类型
3.3 绑定自定义域名
路径:OSS 控制台 → 选中桶 → 传输管理 → 域名管理 → 绑定域名。
填入你要用的域名,本例是 kanban.accunion.cn。
阿里云会提示去 DNS 加一条 CNAME 记录,按提示值添加即可(本例解析到了 118.31.219.204)。
等解析生效(通常几分钟,最长 24 小时)。可以用 ping 或 nslookup 验证。
验证解析是否生效:
nslookup kanban.accunion.cn
# 或
python -c "import socket; print(socket.gethostbyname('kanban.accunion.cn'))"
3.4 HTTPS 证书:申请 → 签发 → 部署(三步,缺一不可)
在”域名管理”里点证书配置,可以申请阿里云免费 DV 证书(每个域名 20 分钟~数小时签发)。签发完成后,证书列表里会显示”已签发”:

图 6 证书已签发:状态”已签发”,有效期 2026-09-29 ~ 2026-12-28
⚠ 最大的坑来了:看到”已签发”就以为好了 —— 不对。此时访问 https 仍会报证书不匹配(Hostname mismatch),因为 OSS 还在用自己的默认证书。必须再做一步”部署到云资源”。
部署路径(两条都行):
① 数字证书管理服务控制台 → 选中该证书 → 部署 → 云产品部署 → 选 对象存储 OSS / 目标 bucket / 目标域名
② OSS 控制台 → 桶 → 传输管理 → 域名管理 → 点击域名 → 修改证书配置 → 选中这张证书
部署完成后,在证书的”云产品部署记录”里应能看到一条 OSS 记录:

图 7 云产品部署记录:已部署到 OSS / accunion-dashboard / kanban.accunion.cn
这一步目前没有 SDK 接口(oss2 无证书部署 API),只能控制台点。别指望脚本自动化。
3.5 权限排错口诀
第一次跑脚本报权限错时,按这张表判断,能省很多时间:
| 报错 | 真正原因 | 怎么办 |
|---|---|---|
| InvalidAccessKeyId | 密钥本身错了(ID 抄错 / 用错用户的) | 重新核对 AccessKeyId |
| AccessDenied | 密钥对,但这个 RAM 用户没被授权 | 去 RAM 控制台给该用户挂 3.1 的策略 |
| The bucket you access does not belong to you | 也是没授权!OSS 不向无权限者透露桶是否存在 | 同上,别误判成桶名写错 |
| oss:PutBucket 相关 | 策略里没给建桶权限 | 手动建桶即可(推荐),或给策略加 oss:PutBucket |
第 4 章 工具脚本是怎么设计的(给 AI / 想改脚本的人)
4.1 为什么要自己写脚本
OSS 有控制台、有图形化工具,但都不适合这件事:
发布是高频重复动作(每月续期、改数据后重发),点几次鼠标必然出错;
真正麻烦的部分——强制 MIME、Cache-Control、单文件内联、签名后换域名、生成短链壳——控制台根本做不了;
要做到”一次对接,以后一条命令”,就必须脚本化。
最终目标形态:所有配置写在一个文本文件里,以后每次只跑一条命令,传完直接吐出给客户的链接。
4.2 文件清单与各自职责
| 文件 | 职责 | 备注 |
|---|---|---|
| 密钥.txt | 唯一配置源:AK、桶、域名、发布方式、看板登记 | 明文,已 gitignore,勿外传 |
| secrets_file.py | 解析密钥.txt 的纯文本格式(键=值、# 注释、键名不敏感) | 被 init.py 和 publish.py 共用 |
| init.py | 一次性对接向导:测连通、测读写、测签名、写配置 | 有密钥文件时免交互;支持 –yes 无人值守 |
| publish.py | 发布主程序:打包 → 上传 → 签名 → 生成短链/导航页 | 核心文件,日常只用这一个 |
| build_single.py | 把本地 css/js/图片全部内联成一个自包含 HTML | 私有交付必须 |
| 使用说明.md | 完整说明文档 | 命令速查、安全合规、常见报错 |
| ram-policy-example.json | RAM 最小权限策略模板 | 改 2 处桶名即可粘贴 |
| gen_docx.js | 本文档的生成脚本 | 重新生成:node 工具/看板发布/gen_docx.js |
4.3 配置设计:让”密钥.txt”成为唯一配置源
设计原则是:改配置只改这一个文件,不用重新跑 init,也不用碰代码。配置优先级:
环境变量 > 密钥.txt > publish_config.json
这样换机器时用环境变量覆盖,本地改文件即可。密钥.txt 支持这些键(部分):
| 键 | 作用 | 本例取值 |
|---|---|---|
| backend | oss 或 r2 | oss |
| oss.endpoint | 地域对应的 endpoint | https://oss-cn-hangzhou.aliyuncs.com |
| oss.accessKeyId / accessKeySecret | RAM 用户的 AK | (敏感,不在此写出) |
| oss.bucket | 桶名 | accunion-dashboard |
| oss.publicHost | 自定义域名(可带 http:// 前缀降级) | kanban.accunion.cn |
| public / sign / single | 是否公开、签名时效、是否打包单文件 | false / 7d / true |
| short / shortSign / jump | 是否生成短链、短链背后签名时效、是否直接跳转 | true / 30d / false |
| board.<客户>.<看板>.dir | 本地看板目录 | 客户档案/003、…/看板 |
| board.<客户>.<看板>.prefix | 云端前缀=链接后半段 | c003/bom |
键名规范化的坑:解析器会把键名按段规范化(下划线/连字符转驼峰),所以新加驼峰字段(如 shortSign)必须同时登记到 CANON 映射表,否则会被压成全小写而匹配不上。这是实际踩过的(见 7.9)。
4.4 登记设计:客户 / 看板 两级
为了支持”一个客户多个看板”,登记名用点号分层:
board.小园香径.BOM成本差异.dir = 客户档案/003、小园香径独徘徊(个人客户)/2、BOM成本差异分析/看板
board.小园香径.BOM成本差异.prefix = c003/bom
于是命令也支持两级:
publish.py --client 小园香径 # 发该客户全部看板 + 自动生成总入口
publish.py --client 小园香径.BOM成本差异 # 只发其中一个
publish.py --list # 查看已登记了哪些
prefix 必须写成两段”客户码/看板码”,且用纯英文数字。因为它就是链接的后半段,写中文会被浏览器编码成 %E5%B0%8F%E5%9B%AD… 一长串,正是这次要消灭的问题。
4.5 build_single.py:为什么必须内联,以及怎么改通用
私有桶下每个对象都要单独签名,而 index.html 里写的是 这种相对引用——签名只签了首页,浏览器再去请求 css/js 时没有签名,全部 403,页面白屏。解决办法只剩一个:把所有东西塞进一个 HTML。
第一版写死了三个文件名(styles.css / app.js / data.js),结果第二个看板翻车了——它还有一个 143 KB 的地图数据 pakistan.js 没被内联,客户看到的图缺地图。改成”扫描并内联所有本地引用”:
# 不写死文件名:把 index.html 里所有本地 css / js / 图片都内联
def repl_script(m):
u = m.group("u")
p = board_dir / u
if not _is_local(u) or not p.exists():
return m.group(0)
return "<script>\n" + _safe_js(p.read_text(encoding="utf-8")) + "\n</script>"
html = LINK_RE.sub(repl_link, html) # <link href="x.css"> → <style>…</style>
html = SCRIPT_RE.sub(repl_script, html) # <script src="x.js"> → <script>…</script>
html = IMG_RE.sub(repl_img, html) # <img src="x.png"> → data:image/png;base64,…
两个细节:① 内联进 会提前截断标签,要转义;② 图片转 base64。改完后第二个看板单文件从 2487.9 KB 变成 2631.0 KB,正好多了那 143 KB。
4.6 publish.py:主流程与两个关键点
主流程:读配置 → 找看板目录 → 打包单文件 → 上传到
关键点一:上传时必须强制 MIME 和缓存头,否则 OSS 默认给 application/octet-stream,浏览器会拒绝加载 CSS/JS:
self.bucket.put_object_from_file(
key, str(path),
headers={
"Content-Type": "text/html; charset=utf-8", # 必须带 charset
"Cache-Control": "no-cache, max-age=0", # 改完 data.js 客户刷新即生效
})
关键点二:签名后把主机名换成自定义域名。因为 OSS 的 V1 签名不含 host,换域名后签名依然有效;而 R2 的 SigV4 签名绑定 host,不能换(代码里已注明):
def url(self, key, sign_secs=None):
if sign_secs:
u = self.bucket.sign_url("GET", key, sign_secs)
# 绑了自定义域名就把主机名换过去:默认域名会强制下载,必须换
if self.public_host and self.public_host != self.default_host:
parts = urlsplit(u)
u = urlunsplit((self.public_scheme, self.public_host,
parts.path, parts.query, parts.fragment))
return u
4.7 短链:一个公开的”壳”指向私有的真实文件
这是让链接变短、变固定的关键。壳就是一个几十字节的 HTML,用 iframe 加载真实页面,浏览器地址栏始终显示短链,客户看不到签名串:
SHORT_HTML = """<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8">
<title>{title}</title>
<style>html,body{{margin:0;height:100%;overflow:hidden}}</style>
</head><body><iframe src="{url}"></iframe></body></html>
"""
def make_shortcut(backend, short_key, real_url, title, use_jump):
tpl = JUMP_HTML if use_jump else SHORT_HTML
backend.put_bytes(short_key, html.encode("utf-8"),
"text/html; charset=utf-8", public=True)
为什么能这么做:桶的”阻止公共访问”是关闭状态,所以单个对象可以设公共读(已实测确认)。真实看板文件仍然私有,只有壳是公开的——壳里除了一个签名地址什么都没有。
续期逻辑:真实文件的签名默认 30 天(shortSign=30d)。到期后重跑同一条命令,壳里的地址会更新,而给客户的链接一个字都不变。
4.8 无人值守:让 AI 能代替你跑完
用户说”我不懂程序,你来运行工具、一步步向我要我需要的东西”,所以脚本必须能在没有键盘输入的环境下跑完。
加 –yes 参数,所有提问自动选默认值;
判断 _unattended():AUTO_YES 或 sys.stdin 不是 tty 时,所有 ask() 直接短路返回默认。
Windows 的 getpass 会绕过管道直接读键盘——AI 用管道调用时会永久卡住(实测卡过一次)。所以必须先判定无人值守再短路,不能只靠 try/except 兜底。
第 5 章 操作 SOP(照着做就能跑通)
5.1 环境准备
确认 Python 与依赖。注意:依赖装在哪个解释器上,就必须用哪个解释器跑,用错会 ModuleNotFoundError。
# 本例使用系统 Python 3.12(oss2 / boto3 装在这里)
C:/Users/Administrator/AppData/Local/Programs/Python/Python312/python.exe -m pip install oss2 boto3
# 验证
python -c "import oss2, boto3; print(oss2.__version__, boto3.__version__)"
# 期望输出类似:2.19.1 1.43.103
5.2 填写密钥.txt
打开 工具/看板发布/密钥.txt,只需要你手填的部分(其余已按私有交付配好):
backend = oss
oss.endpoint = https://oss-cn-hangzhou.aliyuncs.com
oss.accessKeyId = 你的 RAM 用户 AccessKeyId
oss.accessKeySecret = 你的 RAM 用户 AccessKeySecret
oss.bucket = accunion-dashboard
oss.publicHost = kanban.accunion.cnpublic = false # 私有 + 签名
single = true # 打包成自包含单文件
short = true # 生成固定短链
shortSign = 30d # 短链背后真实文件的签名时效
jump = false # 用 iframe,地址栏保持短链写注释请用独立的 # 行。不要写成 oss.accessKeyId = ← 在这里填 —— 解析器会把提示语当真值(踩过,见 7.10)。
5.3 登记看板(加两行)
board.小园香径.BOM成本差异.dir = 客户档案/003、小园香径独徘徊(个人客户)/2、BOM成本差异分析/看板
board.小园香径.BOM成本差异.prefix = c003/bom
确认登记成功:
python 工具/看板发布/publish.py --list
5.4 首次对接自检(只跑一次)
python 工具/看板发布/init.py
有密钥文件时它是免交互的:读文件 → 缺字段就明确报 → 测连通 → 测读写 → 测签名 → 写配置。期望看到:
✓ bucket 已存在(oss-cn-hangzhou)
✓ 读写权限正常
✓ 签名 URL 可生成
如果报 AccessDenied 或 “does not belong to you”,回看 3.5 的排错表——大概率是 RAM 用户还没挂策略。
5.5 发布(以后每次就这一条)
$PY = "C:/Users/Administrator/AppData/Local/Programs/Python/Python312/python.exe"
& $PY 工具/看板发布/publish.py --client 小园香径
真实输出(本例):
密钥文件:密钥.txt 后端=oss 登记看板 2 个
客户:小园香径 共 1 个看板 后端:阿里云 OSS
■ BOM成本差异 c003/bom/ 1 个文件 私有 + 签名 30d + 固定短链
------------------------------------------------------------
↑ index.html → c003/bom/index.html (136.6 KB)
给客户:https://kanban.accunion.cn/c003/bom
客户总入口(1 个看板):https://kanban.accunion.cn/c003
============================================================
给客户的链接:
BOM成本差异 https://kanban.accunion.cn/c003/bom
(总入口) https://kanban.accunion.cn/c003
先演练(不连服务器、不校验密钥,确认要传什么):
python 工具/看板发布/publish.py --client 小园香径 --dry-run
5.6 验收清单(发布后务必自查)
用下面这段脚本检查,9 项全过才算交付合格:
import urllib.request, re
u = "https://kanban.accunion.cn/c003/bom"
r = urllib.request.urlopen(u, timeout=20); b = r.read()
print("HTTP", r.status, "|", len(b), "字节 |", r.headers.get("Content-Type"))
print("强制下载头:", r.headers.get("x-oss-force-download") or "无 ✅")
print("Content-Disposition:", r.headers.get("Content-Disposition") or "无 ✅")
# 再取壳里的真实地址,确认真实页能加载
| 检查项 | 期望结果 | 不合格说明 |
|---|---|---|
| HTTP 状态 | 200 | 403=签名过期或没权限;404=prefix 写错 |
| Content-Type | text/html; charset=utf-8 | 若缺 charset,中文会乱码 |
| x-oss-force-download | 不存在 | 存在=还在用默认域名,客户会下载文件 |
| Content-Disposition | 不存在 | 同上 |
| 页面大小 | 与本地 dist/index.html 一致 | 不一致=单文件没打包完整(漏内联了某个 js) |
| 含数据变量 | 能搜到 window.COST 之类 | 搜不到=data.js 没内联进去 |
| HTTPS 无告警 | 浏览器显示锁标志 | 报不匹配=证书没”部署”(见 7.3) |
5.7 把链接发给客户
发短链即可。建议同时说明两点:① 链接 30 天内有效,我们每月会续期,链接不变;② 数据保密,请勿转发。
第 6 章 多客户、多看板怎么放
6.1 云端目录结构(一个桶装所有客户)
accunion-dashboard/
├── c002/ ← 客户码
│ ├── sales/index.html 真实看板(私有,签名访问)
│ ├── sales 短链壳(公开读)→ kanban.accunion.cn/c002/sales
│ └── (c002) 客户总入口 → kanban.accunion.cn/c002
├── c003/
│ ├── bom/index.html
│ ├── bom → kanban.accunion.cn/c003/bom
│ └── (c003) → kanban.accunion.cn/c003
└── …以后新客户继续加 c004/、c005/
6.2 本地目录 ↔ 云端的映射(哪些传、哪些不传)
只上传”看板目录”这一层。客户的原始数据、清洗脚本、数据集、需求文档一律不上传——里面是客户敏感底稿,本来也不该出本机。
| 本地 | 云端 | 说明 |
|---|---|---|
| 客户档案/002、…/001、做销售看板/看板/ | c002/sales/ | 目录 ↔ 前缀,整目录对应 |
| └ dist/index.html | c002/sales/index.html | ★ 实际传输物(单文件模式只传这一个) |
| 原始数据/ 、清洗/ 、数据集/ | — | 不上传 |
| 客户档案/003、…/2、BOM成本差异分析/看板/ | c003/bom/ | 同上 |
| 1、客户资料/(含需求文档) | — | 不上传 |
| 客户档案/001、江西元气谷/002、资金看板/ | — | 未登记,内部自用,不上传 |
两种模式的上传规则:
| 模式 | 传什么 | 落到云端 |
|---|---|---|
| 单文件(默认,single=true) | 只传 看板/dist/index.html |
|
| 多文件(–no-single) | 看板目录下所有文件 | |
| 两种都跳过 | dist/、.git/、node_modules/、pycache/ | — |
6.3 新增客户 / 新增看板:只改两行
# 新客户
board.某某公司.成本分析.dir = 客户档案/004、某某公司/1、成本分析/看板
board.某某公司.成本分析.prefix = c004/cost
# 给老客户再加一个看板
board.小园香径.预算执行.dir = 客户档案/003、…/3、预算执行/看板
board.小园香径.预算执行.prefix = c003/budget
登记后 –client 小园香径 会一次发两个,总入口自动列出两项,不用改任何代码。
6.4 日常运维
| 事项 | 频率 | 做法 |
|---|---|---|
| 更新看板数据 | 客户数据更新时 | 替换 data.js → 重跑 publish 命令,客户刷新页面即可 |
| 短链续期 | 每 30 天 | 重跑同一条 publish 命令,客户链接不变 |
| 证书续签 | 到期前(本例 2026-12-28) | 证书控制台续签,并重新”部署到云资源”——只续签不部署会失效 |
| 检查桶内对象 | 偶尔 | publish.py –list 查看登记;OSS 控制台查看实际对象 |
第 7 章 踩坑实录(按踩到的顺序)
这一章是全文最有价值的部分。每一条都是真实发生过、并已验证解决办法的。
7.1 默认域名强制下载(致命)
现象:客户点链接,浏览器直接下载一个 html 文件而不是打开看板。
根因:OSS 默认域名返回的响应头里有 x-oss-force-download: true 和 Content-Disposition: attachment。
试过且无效的两条路:① 签名加 response-content-disposition=inline;② 对象元数据设 Content-Disposition: inline。都会被 OSS 覆盖回 attachment。
解决:绑定自定义域名(需备案)。绑定后这两个头消失,浏览器正常预览。
7.2 没有备案域名怎么办
把单文件 HTML 直接发客户,客户双击用浏览器打开(最省事,但更新麻烦);
接受”下载后打开”(体验差,不推荐);
用境外 bucket(R2 有 r2.dev 域名,但数据出境 + 国内访问慢,只适合公开样板);
先去备案(一两个星期,长期看最划算)。
7.3 HTTPS 证书”已签发”≠ 已生效
现象:证书控制台明明显示”已签发”,浏览器访问 https 却报 Hostname mismatch / CERTIFICATE_VERIFY_FAILED。
根因:签发只是把证书造出来,OSS 并不会自动用它。必须再执行一次”部署到云资源”,把它们关联起来。证书列表里的”已部署”列如果是 –,就是没部署。
解决:数字证书管理服务 → 该证书 → 部署 → 云产品部署 → 选 OSS / 桶 / 域名。oss2 没有这个接口,只能控制台操作。
7.4 部署后仍然时好时坏
现象:部署完成后测 10 次,有 3 次失败,报证书不匹配。
根因:同一个 IP 交替返回两张证书(一张含你的域名、一张是 OSS 默认证书)——证书正在节点间灰度生效。
诊断方法:连续握手多次,比对证书的 SHA256 前缀,如果指纹在两张之间跳变,就是灰度未收敛。
解决:等。本例约 20 分钟后复测 30/30 全通。
7.5 私有交付不打单文件 = 白屏
现象:签名链接能打开,但页面空白,控制台一片 403。
根因:签名只签了 index.html 这一个 key,页面里的相对引用(css/js/data)没有签名,全部被拒。
解决:build_single.py 内联成单文件。脚本已加兜底:非公开且没显式 –no-single 时自动打包。
7.6 ”阻止公共访问”与短链的冲突
现象:短链壳设 public-read 失败。
根因:桶开了”阻止公共访问”,对象级 ACL 会被拒。
本例实况:创建时界面显示”已开启”,但后来 API 实测为 False(关闭),所以单对象可以设公共读,短链才能工作。
结论:短链依赖这个开关为”关闭”。若哪天控制台把它打开,短链壳会失效,需要改回或退回用签名长链接。判断依据以 API 实测为准,不要只看截图。
7.7 配置读了两套,改了不生效
现象:在密钥.txt 里改了 publicHost,发布出来的链接还是默认域名。
根因:load_config() 只读 publish_config.json,没合并密钥文件。
解决:让 load_config 接收已合并的配置作为 base。教训:新增配置源时,要检查所有读配置的函数,不能只改一处。
7.8 布尔参数语义反转
现象:密钥里写 jump = false(想要 iframe 保持地址栏),结果生成的是跳转页。
根因:函数参数名叫 use_iframe,实际接收的却是 jump 的值,判断写成 if use_iframe is False → 语义反了。
解决:参数名与传入值语义必须一致(改成 use_jump 并修正判断)。
7.9 驼峰键名被压成小写
现象:密钥文件里写了 shortSign,解析器认不出来。
根因:键名规范化函数对不含连字符的键会整体小写(shortSign → shortsign),与字段表匹配不上。
解决:新增驼峰字段必须同时登记到 CANON 映射表。
7.10 注释写法导致提示语被当真值
现象:密钥文件里写 oss.accessKeyId = ← 在这里填,脚本把”← 在这里填”当成了密钥。
解决:提示语必须写成独立的 # 注释行;同时解析器加防呆(← 开头或 <…> 包裹的视为空值)。
7.11 旧配置残留干扰合并
现象:登记改成两级后,–client 小园香径 命中的仍是旧的一级登记(旧前缀 003-小园香径)。
根因:publish_config.json 里还留着旧记录,被合并进来了。
解决:清空它的 boards,让密钥.txt 成为唯一登记来源。
7.12 Windows 中文乱码与 BOM
控制台中文乱码 → 脚本开头调 SetConsoleOutputCP(65001);
JSON 读不出来(Unexpected UTF-8 BOM)→ 记事本/PowerShell 写文件会带 BOM,一律用 utf-8-sig 读。
7.13 getpass 在管道下永久卡住
Windows 的 getpass 绕过管道直读键盘,AI 用管道调用脚本会一直等着。必须在调用前判定无人值守并短路,不能只靠 try/except。
7.14 R2 的签名不能换域名
OSS 的 V1 签名不含 host,所以签名后可以换成自定义域名;R2/S3 的 SigV4 签名绑定 host,换了会 SignatureDoesNotMatch。代码里已分别注明,别混用。
第 8 章 附录
附录 A 密钥.txt 完整示例
backend = oss
oss.endpoint = https://oss-cn-hangzhou.aliyuncs.com
oss.accessKeyId = LTAI********************
oss.accessKeySecret = ************************
oss.bucket = accunion-dashboard
oss.publicHost = kanban.accunion.cn
public = false
sign = 7d
single = true
short = true
shortSign = 30d
jump = false
website = false
open = false
board.小园香径.BOM成本差异.dir = 客户档案/003、小园香径独徘徊(个人客户)/2、BOM成本差异分析/看板
board.小园香径.BOM成本差异.prefix = c003/bom
board.巴基斯坦OPPO.销售看板.dir = 客户档案/002、巴基斯坦oppo/001、做销售看板/看板
board.巴基斯坦OPPO.销售看板.prefix = c002/sales
附录 B 常用命令速查
$PY = "C:/Users/Administrator/AppData/Local/Programs/Python/Python312/python.exe"
# 查看已登记的客户 / 看板
& $PY 工具/看板发布/publish.py --list
# 演练(不上传)
& $PY 工具/看板发布/publish.py --client 小园香径 --dry-run
# 发布某个客户的全部看板(最常用)
& $PY 工具/看板发布/publish.py --client 小园香径
# 只发其中一个看板
& $PY 工具/看板发布/publish.py --client 小园香径.BOM成本差异
# 首次对接自检
& $PY 工具/看板发布/init.py
# 临时发一个没登记的目录
& $PY 工具/看板发布/publish.py --dir "<看板目录>" --prefix c999/demo --short
附录 C 本次实战的关键数据
| 项目 | 取值 |
|---|---|
| Bucket | accunion-dashboard(华东1 杭州,私有,低频访问存储) |
| Endpoint | oss-cn-hangzhou.aliyuncs.com |
| 自定义域名 | kanban.accunion.cn(解析 118.31.219.204) |
| HTTPS 证书 | cert-bdo9y / 2753078-cn-hangzhou,2026-09-29 签发,2026-12-28 到期 |
| 看板 003 | c003/bom/index.html 136.6 KB (本地 ↔ 云端逐字节一致) |
| 看板 002 | c002/sales/index.html 2631.0 KB(含 143 KB 地图数据内联) |
| 实测结果 | HTTPS 200|text/html; charset=utf-8|无强制下载头|30/30 次访问成功 |
| 依赖版本 | Python 3.12 oss2 2.19.1 boto3 1.43.103 |
附录 D 参考来源
本文档基于一次完整实战整理,原始记录见本仓库:
聊天对话记录/20260928.md —— 看板发布工具从零到首次上传成功的全过程
聊天对话记录/20260929.md —— 多客户多板结构、短链设计、HTTPS 打通、证书部署
工具/看板发布/使用说明.md —— 命令与参数的完整说明
附件/20260928/、附件/20260929/ —— 本文所用控制台截图原件
本文档可用 node 工具/看板发布/gen_docx.js 重新生成。












暂无评论内容