9.0 KiB
会议纪要分享:落地与上线步骤(2026-09-23)
App 里「分享 → 分享链接」发出去的那条链接,之前从来没有打开过:域名解析着但没有落地页, 而且链接里带的是用户的完整登录态。这一轮把两件事一起做掉了。
一、改了什么
1. 链接里不再是登录 JWT
| 之前 | 现在 | |
|---|---|---|
| URL 最后一段 | storage.read("logintoken"),即 Authorization 头里那个 JWT |
服务端签发的分享票据 |
| 拿到链接能做什么 | 调任何业务接口、看该账号全部纪要、消费算力 | 只能读这一条纪要 |
| 失效手段 | 只能改密码 | 默认 7 天自动过期 |
票据是自包含的 HMAC 签名串,不落库:
<payload>.<sig>
payload = base64url(JSON{"i":记录id,"u":owner uid,"e":到期unix秒})
sig = base64url(HMAC-SHA256(shareKey, payload))
shareKey = HMAC-SHA256(FIELD_ENCRYPT_KEY, "echomeet.share.v1")
签名密钥从 FIELD_ENCRYPT_KEY 派生而不是直接用:HMAC 单向,从派生 key 推不回原 key,
两个用途不共用同一份秘密。选它是因为它已经在每台机的 .env 里,上线不需要新增任何配置项。
⚠️ 代价:票据签发后无法单独撤销,只能等过期或换 FIELD_ENCRYPT_KEY(那会让全部票据失效)。
这是刻意接受的取舍——要撤销能力时补一张吊销表即可,票据格式不用变。
2. 新增两个接口(modules/echomeet/api_sharelink.go)
| 接口 | 登录 | 说明 |
|---|---|---|
echomeet_createsharelink |
要 | 入参 {id, expiredays},回 {ticket, expiretime}。签发前先校验这条记录是本人的 |
echomeet_getsharedrecord |
不要 | 入参 {ticket},回落地页要展示的只读字段 |
第二个必须在 gateway 的 WhiteList 里——打开链接的人本来就没登录。已加进
gateway.yaml.example,⚠️ 各机上手工维护的
confs/gateway.yaml 要同步加这一行,漏了就是 code:18,表现为「分享链接打开一片空白」。
响应刻意不含 uid / filepath / taskid / logid / *_svc_id:这是任何人凭链接就能
打开的接口,多回一个字段就是多泄一份内部信息。
3. 落地页嵌进了 gateway 二进制
modules/gateway/share_page.html 经 go:embed 打进二进制,路由 GET /share/*any。
为什么不做成静态文件下发:页面必须跟 echomeet_getsharedrecord 的协议同步,
版本对不上就是白页。嵌进来意味着「部署了新镜像 = 页面也是新的」,服务器上不用维护任何文件。
⚠️ 路由必须是 catch-all。真实链接形如 /share/ym.ymaikj.com/ymaikj.com/42/<ticket>,
路径里的段是数据不是文件路径,用 gin.Static 会去磁盘找同名文件,一律 404。
二、上线状态(2026-09-23 实测)
| 步骤 | 状态 |
|---|---|
镜像 starpivot-app:0.2.5 构建并推送 |
✅ |
正式机 ym-a11 部署(0.2.4 → 0.2.5) |
✅ 容器 healthy,日志无 panic |
confs/gateway.yaml 加 echomeet_getsharedrecord 白名单 |
✅ 已改并重启(备份 gateway.yaml.bak-20260923) |
confs/home.yaml 的 AppStoreBundleID 改正 |
✅ 顺带修掉(备份 home.yaml.bak-20260923) |
NPM 加 share.ymaikj.com 反代 + 证书 |
❌ 待办,见下 |
容器内实测:
/share/... → HTTP 200, 15042 bytes, <title>会议纪要</title>
echomeet_getsharedrecord + 假票据 → {"code":20,"msg":"分享链接无效或已过期"} ← 白名单生效
echomeet_createsharelink 未登录 → {"code":18,"msg":"NoLogin"} ← 路由已注册
只剩一步:NPM 反代
share.ymaikj.com 的 DNS 已指向 47.116.104.181,但 NPM 上还没有它的 proxy host
(现有四条:console / deepglass / mcp / ym)。这一步要登录 NPM 后台操作:
- Proxy Hosts → Add Proxy Host
- Domain Names:
share.ymaikj.com - Scheme
http,Forward Hostnameym-a11,Forward Port7100 - 打开 Block Common Exploits
- Domain Names:
- SSL 标签页 → Request a new SSL Certificate
- ⚠️ 现有证书都是单域名的(
npm-1…npm-mcp,没有通配符),所以必须给share.ymaikj.com单独签一张 - 勾 Force SSL、HTTP/2
- 不要开 "Use a DNS Challenge",保持默认的 HTTP-01 即可
- ⚠️ 现有证书都是单域名的(
⚠️ 这台机上签证书用的是 HTTP-01(webroot),不是 DNS challenge。
2026-09-23 实测:5 张现存证书的 /etc/letsencrypt/renewal/npm-*.conf 全部是
authenticator = webroot,容器里也没装 certbot-dns-aliyun
(certbot plugins 里搜不到 dns- 开头的任何东西)。最近那张 mcp.ymaikj.com
就是这么签成的。所以不需要阿里云 AK/SK、不需要填 Propagation Seconds、
不需要先装插件。运维-容器断网与免费证书排查.md
那篇写的 DNS challenge 是给灵谱那台机的,别照搬到这里。
HTTP-01 的唯一前置是 80 端口从公网能打到这台机,且该域名的 DNS 已生效——
两条都已满足(NPM 监听 0.0.0.0:80,share.ymaikj.com 解析到 47.116.104.181)。
⚠️ 顺序不能反:必须先保存 proxy host、再去签证书。证书验证时 Let's Encrypt 会来访
http://share.ymaikj.com/.well-known/acme-challenge/...,没有这条 proxy host,
nginx 上就没有匹配该 server_name 的 80 端口配置,验证必然失败。
配完验收:
curl -sI https://share.ymaikj.com/share/ym.ymaikj.com/ymaikj.com/1/x | head -1 # 期望 200
然后 App 里打开一条纪要 → 分享 → 分享链接 → 发给自己 → 点开。
三、原始上线步骤(留档)
1. 部署新镜像
cd deploy/app
DEPLOY_KEY=$HOME/Documents/keys/loginscre.pem ./prod-build.sh <版本号>
DEPLOY_KEY=$HOME/Documents/keys/loginscre.pem ./prod-deploy.sh ym along <版本号>
2. 补 gateway 白名单(容易漏,漏了整条链路不通)
正式机 47.116.104.181 上 /home/work/ym-a11/confs/gateway.yaml,在 WhiteList 下加:
- echomeet_getsharedrecord
改完 docker restart ym-a11 即可——confs/ 是以卷挂进容器的,进程重启就会重读 yaml。
⚠️ 「必须重建容器」那条规矩只适用于改 .env(环境变量在容器创建时注入,restart 不会重读)。
3. NPM 加一条反代
share.ymaikj.com → ym-a11:7100,申请证书。
证书走阿里云 DNS challenge,踩坑记录见 运维-容器断网与免费证书排查.md:
Propagation Seconds 必须填整数(如 120),插件要先手动
docker exec <NPM> pip install -i https://mirrors.aliyun.com/pypi/simple certbot-dns-aliyun。
4. 验收
# ① 落地页出得来(应返回 200 + HTML)
curl -sI https://share.ymaikj.com/share/ym.ymaikj.com/ymaikj.com/1/x | head -3
# ② 免登录接口通了(应返回 code:20 链接无效,而**不是** code:18 NoLogin)
curl -sX POST https://ym.ymaikj.com/api/home/echomeet_getsharedrecord \
-H 'Content-Type: application/json' -d '{"ticket":"bad"}'
拿到 code:18 = 第 2 步的白名单没生效。拿到 code:20 = 通了(票据确实是假的)。
③ 真机:App 里打开一条纪要 → 分享 → 分享链接 → 发给自己 → 点开。
5. 客户端要发版
链接格式变了(最后一段 token → ticket),旧包分享出来的链接落地页打不开 (它会拿旧 JWT 当票据送上去,签名校验必然失败,显示「链接已失效」)。
这不构成兼容问题:分享功能此前从来没有真正可用过,外面不存在能打开的老链接。
四、落地页里两处不能动的地方
后端域名白名单
var ALLOWED_HOSTS = ['ym.ymaikj.com', 'ym-dev.ymaikj.com'];
serverHost 是从 URL 路径里读出来的。不校验就等于:任何人构造一条
/share/<攻击者域名>/.../<票据> 的链接,就能让打开它的人的浏览器向攻击者的服务器发请求,
顺带把票据送过去。既是钓鱼跳板也是票据泄漏。新增环境要在这里加一行。
先转义再套 Markdown 正则
总结是 Markdown(与客户端 _exportMarkdownPdf 同源),页面里有个极简渲染器。
它先 esc() 再套正则——内容来自用户录音与模型输出,顺序反了就是 XSS。
页面刻意不引第三方 Markdown 库:这页要在国内手机、可能很差的网络下秒开, 为渲染几段标题列表去拉 CDN 不值得,而且 CDN 被墙时整页就废了。
五、还没做
- 票据吊销:见上文取舍。
- 国际化:落地页是中文的。分享出去的对象大概率与创建者同语言,先不做。
web.ymaikj.com:App 里「EAIMAR Web」入口指向它,apps/web仍是空壳,点开打不开。 与本次分享链路无关,但它俩是同一类「域名解析了、东西没做」的欠账。