---
name: baoyu-cloudbase-deployment
description: 宝锐工作台 CloudBase 部署：静态托管、匿名登录鉴权、数据库权限、Cloudflare Tunnel 公网暴露
---

## 触发条件
当用户要求部署/迁移工作台 HTML 页面到公网、配置 CloudBase、接入腾讯云开发静态托管、实现免登或飞书 OAuth 时使用。也用于回答"某功能上线在哪个链接"——先定位并验证链接再答复。

## 〇、快速定位与验证线上链接（回答"功能在哪个链接"时）

1. **找基准域名**：用 `search_files` 搜 `tcloudbaseapp.com`（terminal 里 grep 被 blocklist 硬拦截，不可用）。权威记录处：`数据/cb_sync.log`（每次部署日志末尾的 Visit website 行）和 `产品说明.md`。当前基准域名：`https://bier-sales-d0gatbvlx288724e9-1458438710.tcloudbaseapp.com`。
2. **拼页面路径**：线上路径 = 本地 `~/Desktop/Hermes输出-工作类/` 下的相对路径，如 `工具/客户360档案.html` → `<基准域名>/工具/客户360档案.html`。中文路径直接拼，curl/浏览器自动编码。
3. **给链接前必须 curl 实测**：页面和其数据 JSON 各验一次 200：
   ```bash
   curl -s -o /dev/null -w "%{http_code}\n" "<基准域名>/工具/客户360档案.html"
   curl -s -o /dev/null -w "%{http_code}\n" "<基准域名>/数据/customer_360.json"
   ```
   两个都 200 才把链接发给用户；页面 200 但数据 404 说明只部署了 HTML，按下面第一节补传数据文件。

### ⚠️ 飞书 Open Platform 不接受中文 URL 路径

**症状**：飞书开放平台配置应用首页 URL 含中文路径（如 `/工具/index.html`），提示"地址无效"。

**根因**：飞书后台校验拒绝非 ASCII 路径段。

**修复**：在 CloudBase 上额外部署一份到纯英文路径：

```bash
# 原路径（通过 curl/浏览器可访问，但不能配飞书应用）
/工具/index.html

# 新建纯英文路径副本
tcb hosting deploy "工具/index.html" "workbench/index.html" -e <env-id>

# 飞书应用配置改为
https://<cloudbase-domain>/workbench/index.html
```

两个路径指向同一份文件，后续修改时**两个路径都要部署**以保持一致。

## 一、CloudBase 静态托管

### 部署 HTML + 数据文件
```bash
# 部署主页（始终用显式路径）
tcb hosting deploy index.html /index.html -e <env-id>

# 部署数据文件（保持目录结构）
tcb hosting deploy 数据/shipment_status.json 数据/shipment_status.json -e <env-id>
```

### ⚠️ 必须同步部署全部数据文件（JSON 回退依赖）

页面通过 `../数据/...` 相对路径回退加载 JSON。只部署 HTML 时，这些路径解析到 CDN 上→**全部 404**。**部署完 HTML 后必须同步部署全部数据文件**：

```bash
cd ~/Desktop/Hermes输出-工作类
tcb hosting deploy 数据/ 数据 -e <env-id>   # 递归上传（通常 1000+ 文件）
```

**验证每个回退路径可达**：
```bash
for f in 数据/pricing/_key_customers.json 数据/kpi_dashboard.json; do
  curl -s -o /dev/null -w "%{http_code}" "https://<cloudbase-url>/$f"
done  # 全部应为 200
```

**后续增量更新**：单文件 `tcb hosting deploy 数据/xxx.json 数据/xxx.json -e <env-id>`。

### CDN 缓存
CloudBase CDN 缓存约 5 分钟。部署后如页面未更新：
- 访问时加 `?v=<timestamp>` 参数（实测带 query 仍 200）
- 或用 curl `-H "Cache-Control: no-cache"` 验证

**🔴 覆盖同一路径重部署时，必须主动给带版本号的链接、并把成图直接发给用户。** 路径不变的更新（如 `poster/x.html` 重部署覆盖）用户端浏览器/微信内置浏览器会吃旧缓存，回你一句「**没改变**」——其实他看到的是旧版。此时若当成「上次改动无效」去重改，会把本来已经正确的区块一起改坏。交付固定动作：① 直接 `MEDIA:` 发当版成品图（半尺寸也够看版面）让用户对着成图挑错；② 链接写成 `<URL>?v=YYYYMMDDHHMM`。只报「已部署 HTTP 200」不算交付完成。

### ⚠️ 批量部署的成败按退出码判断，不按回执文案

`tcb hosting deploy` 的成功回执**中英文不定**（`✔ 文件部署成功！` / `✔ File deployment successful!`）。循环里用中文串匹配计数（`case "$out" in *成功*)`）会把英文回执全落到「失败」，报出「15 个全失败」的假警报，而文件其实已上线。修：按退出码计数（`if tcb hosting deploy ... ; then n=$((n+1)); fi`），或只回显不复判。

**验证只看 HTTP 200 不够**——缓存旧版也返回 200。必须拉内容 grep 关键字符串：
```bash
curl -s -H 'Cache-Control: no-cache' "<URL>" | grep -c '<本次新增的关键字符串>'
```
中文路径直拼 curl 一般可用；若返回 404/异常，用 `urllib.parse.quote(path)` 编码后再请求。

## 二、匿名登录鉴权

### 架构
```
页面加载 → CloudBase SDK init → 匿名登录 → 查 user_roles 集合 → 验证密码 → 进系统
```

### 前端 SDK
```html
<script src="https://static.cloudbase.net/cloudbase-js-sdk/latest/cloudbase.full.js"></script>
```

### ⚠️ 关键陷阱：password 字段被 CloudBase 自动屏蔽
CloudBase 数据库会自动把名为 `password` 的字段值显示为 `***`，导致前端读取到 `"***"` 而非实际密码。**必须将密码字段命名为 `pwd` 或 `login_pwd`**。

### 数据库权限（ACL）查询与设置

`user_roles` 集合实际 ACL（2026-08 实测，`DescribeDatabaseACL` 返回）：
```json
{ "read": true, "write": "auth.openid == doc._openid" }
```
即 **所有人可读（含未登录）、仅创建者可写**（`AclTag=CUSTOM`）。之前文档写的 `read:auth!=null, write:auth!=null` 已过时。

**查询集合 ACL**：
```bash
tcb api tcb DescribeDatabaseACL --api-version 2018-06-08 \
  --body '{"EnvId":"<env-id>","CollectionName":"<col>"}' --json
# 返回 data.AclTag + data.Rule
```

**AclTag 预设值**：`PRIVATE`（仅创建者读写）、`READONLY`（所有人读/仅创建者写）、`ADMINWRITE`、`ADMINONLY`、`CUSTOM`（自定义 Rule）。

**⚠️ 新集合默认 PRIVATE**：通过 `tcb db nosql execute` INSERT 创建的集合默认 `AclTag=PRIVATE`。多用户应用（管理员要看员工数据、员工写自己数据）必须改成 `read:true, write:true`，否则管理员读不到别人的记录、跨用户写也失败。

**设置集合 ACL —— ⚠️ 两个 API 易混淆，务必区分**：
- `ModifyDatabaseACL` 只接受**预设标签**（READONLY/PRIVATE/ADMINWRITE/ADMINONLY），**不接受 `Rule` 参数**——传 `Rule` 会报 `The parameter Rule is not recognized`。
- **自定义规则（如 `read:true, write:true`）必须用 `ModifySafeRule`**，`AclTag=CUSTOM` + `Rule`：

```bash
# 自定义规则（所有人可读写，多用户应用的标准做法）
tcb api tcb ModifySafeRule --api-version 2018-06-08 \
  --body '{"EnvId":"<env-id>","CollectionName":"<col>","AclTag":"CUSTOM","Rule":"{\"read\":true,\"write\":true}"}' --json

# 查询确认（自定义规则用 DescribeSafeRule；预设标签用 DescribeDatabaseACL）
tcb api tcb DescribeSafeRule --api-version 2018-06-08 \
  --body '{"EnvId":"<env-id>","CollectionName":"<col>"}' --json
```

**文档型集合 CRUD**（`tcb db nosql execute`，注意 `Command` 字段是二次转义的 JSON 字符串）：
```bash
# 查询
tcb db nosql execute -e <env> --json --command '[{"TableName":"<col>","CommandType":"QUERY","Command":"{\"find\":\"<col>\",\"filter\":{\"name\":\"刘子研\"},\"limit\":10}"}]'
# 插入
tcb db nosql execute -e <env> --command '[{"TableName":"<col>","CommandType":"INSERT","Command":"{\"insert\":\"<col>",\"documents\":[{...}]}"}]'
```

**UPDATE 的 _id 匹配是 ObjectId、必须 `{"$oid":...}`，否则命令假成功（无 error 但数据没变）**——连同 DELETE `limit:0` 清空、QUERY 数条数、`$numberInt` 解包等写入坑，见 `references/tcb-db-write-patterns.md`。

**注意**：`tcb db model list` 只列出新版「数据模型」体系（sys_user/sys_department），**不列出 nosql 文档集合**。查 nosql 集合用 `DescribeDatabaseACL` 或 `tcb db nosql execute`。

多用户应用搭建模板（复用 user_roles 登录 + 新集合 + ACL + 英文路径部署）见 `references/multi-user-app-pattern.md`。

### ⚠️ `read:true` 的二阶风险：全体明文密码可被任何登录用户拖走

`user_roles` 留 `read:true` 是为了让客户端自己查表登录/取名册。**一旦登录改走后端**（`/api/auth/token`、飞书免登也走后端），客户端就不再需要读这个集合——此时还留 `read:true` 等于：任何匿名登录的客户端都能拖走全表 `pwd`（明文）+ open_id。

**锁库四步（顺序不能反，否则锁完立刻全公司登不上）**：
1. 用户管理搬服务端：`/api/users/list`（仅 admin/gm，**剥离 `pwd`**，用 `has_pwd` 表达有无密码）、`/api/users/roster`（全员可读，只回 name/email/dept/role）、`/api/users/save`、`/api/users/reset_pwd`（写本地用户表快照，登录以此为准）。
2. 替换所有客户端直连：管理页的增删改查、主页面取名册（`loadUserList`）都改调上面这些接口，CloudBase 路径只留 try/catch 兜底。
3. 最后锁库：`ModifySafeRule` → `{"read": false, "write": false}`，再用 `DescribeSafeRule` 回读确认（自定义规则只能用 SafeRule 系列，见本节上文）。
4. 顺手消掉登录响应里的 `pwd` 字段（原实现把 `pwd` 一并回传，与公开读叠加就是完整凭据泄露）。

**锁库前先排查还有谁在读**：`grep -l "collection('user_roles')" 工具/*.html` — 备份页/一次性播种页（`*_backup_*.html`、`init_users.html`）可以直接归档，正式页面必须改完再锁。

**验收**：试点账号逐个登录（响应不含 `pwd`）→ 非 admin 读用户表 403 → 未登录读任意接口 401。

单机工具转多人服务的完整配方（按人隔离的工作区、上传→排队→后台工人、带鉴权下载、验收清单）见 `references/multi-user-service-hardening.md`。

### 用户密码默认值
所有用户统一初始密码 `biori2026`。服务器端 `.py` 的 `DEFAULT_USERS` 字典以完整邮箱（如 `liuxinyuan@biori.com`）为 key，密码硬编码校验。

**登录要求**：邮箱必须完整格式 `xxx@biori.com`，仅前缀会导致"邮箱未注册或密码错误"。

### loadUserList 时序
必须在匿名登录成功后再调用 `loadUserList()`。如查询返回空数组（权限不足），需 fallback 到硬编码名单。

## 三、Cloudflare Tunnel（临时公网暴露）

```bash
# 启动 tunnel（每次重启生成新 URL）
cloudflared tunnel --url http://localhost:8765
```

输出示例：
```
https://times-edinburgh-phases-sailing.trycloudflare.com
```

⚠️ Tunnel URL 每次重启会变，仅适合测试。生产环境公网暴露**不要用 Tailscale Funnel**（ts.net 域名在国内 DNS 部分墙 + IP 被墙，远程设备连不上，2026-09-08 实测）——见「七、国内公网暴露方案」。

## 四、飞书 OAuth 免登流程

### 架构
```
飞书打开应用 → 重定向到 CloudBase 页面?code=xxx
  → 页面检测 code → POST 到 tunnel/auth 端点
  → Python 服务器调飞书 API 换用户身份
  → 返回用户名 → 前端查 user_roles → finishLogin
```

### ⚠️ 关键陷阱：飞书桌面端嵌入式浏览器与 CloudBase 匿名鉴权不兼容

**症状**：在飞书桌面 App（非手机）的嵌入式 Chromium 中打开工作台，输入正确密码始终报"密码错误"，但在 Chrome/Safari/飞书手机端正常。

**根因**：飞书桌面端的定制 Chromium 安全策略更严格，CloudBase 匿名登录 `auth.anonymousAuthProvider().signIn()` 虽然能初始化 `cbDb`，但后续 `cbDb.collection().where().get()` 查询可能返回异常数据或比较行为不一致，导致 `user.pwd !== pwd` 恒为 true。

**解决：飞书桌面端自动 OAuth 免登**

前端 `init()` 中检测飞书桌面端（非手机）时，自动跳转飞书 OAuth 授权：

```javascript
var isFeishuMobile = /feishu.*(Mobile|Android|iPhone)/i.test(navigator.userAgent);

function init() {
    // ... cbApp / cbDb 初始化 ...

    // 飞书桌面端 → 跳转 OAuth 免登（绕过 password 校验）
    if (isInFeishu && !isFeishuMobile) {
        var saved = localStorage.getItem(ROLE_KEY);
        if (!saved) {
            var oauthUrl = 'https://open.feishu.cn/open-apis/authen/v1/authorize'
                + '?app_id=' + FEISHU_APP_ID
                + '&redirect_uri=' + encodeURIComponent(WORKBENCH_URL)
                + '&state=login';
            window.location.href = oauthUrl;
            return;
        }
    }

    // 正常流程（手机飞书 / 非飞书浏览器）
    // ...
}
```

**流程**：飞书桌面打开 → 检测飞书环境 → 跳转飞书授权页 → 用户点"授权" → 飞书回调带回 `?code=xxx` → `feishuAutoLogin(code)` → 后端换身份 → `finishLogin`。

**注意**：
- `isFeishuMobile` 正则区分手机端（手机飞书用正常密码登录，不跳 OAuth）
- 已有 localStorage 缓存时跳过 OAuth（已登录过）
- `redirect_uri` 必须是飞书应用后台已配置的安全域名

**⚠️ 边缘情况：OAuth 免登失败回退后用户列表为空**

`feishuAutoLogin` 失败（code过期/网络不通）时会调用 `showLoginForm()` 回退到密码登录。但原 `showLoginForm()` 只显示登录遮罩，不加载用户列表（`loadUserList` 只在 `init()` 的匿名登录成功回调中调用，OAuth 快捷路径跳过了这个回调）。结果：用户看到登录框但下拉无选项。

**修复**：`showLoginForm` 中加兜底检测：

```javascript
function showLoginForm() {
  document.getElementById('loginOverlay').style.display = 'flex';
  document.getElementById('loginLoader').style.display = 'none';
  document.getElementById('loginBtn').disabled = false;
  // ... 其他逻辑 ...
  // 确保用户列表已加载（OAuth 回退路径未走 init() 匿名登录回调）
  var sel = document.getElementById('loginRole');
  if (!sel || sel.options.length === 0) loadUserList();
}
```

### 飞书应用配置
- 重定向 URL / 安全域名: `https://<cloudbase-domain>`
- 权限: `contact:user.base:readonly`（需管理员审批）

### CORS 配置（后端必须）

OAuth 端点必须允许 CloudBase 域名的跨域请求：

```python
def _send_json(self, obj, code=200):
    body = json.dumps(obj, ensure_ascii=False).encode('utf-8')
    self.send_response(code)
    self.send_header('Content-Type', 'application/json; charset=utf-8')
    self.send_header('Content-Length', str(len(body)))
    self.send_header('Access-Control-Allow-Origin', '*')  # ← CORS
    self.end_headers()
    self.wfile.write(body)

def do_OPTIONS(self):
    """处理 CORS 预检请求"""
    self.send_response(204)
    self.send_header('Access-Control-Allow-Origin', '*')
    self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
    self.send_header('Access-Control-Allow-Headers', 'Content-Type')
    self.end_headers()
```

### 端点代码（Python）
```python
@app.route('/api/feishu/auth', methods=['POST'])
def feishu_auth():
    code = request.json.get('code')
    # 1. 获取 app_access_token
    token = requests.post('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal',
        json={'app_id': APP_ID, 'app_secret': APP_SECRET}).json()['app_access_token']
    # 2. 换用户身份
    auth = requests.post('https://open.feishu.cn/open-apis/authen/v1/oidc/access_token',
        json={'grant_type': 'authorization_code', 'code': code},
        headers={'Authorization': f'Bearer {token}'}).json()
    return {'ok': True, 'name': auth['data']['name']}
```

### Tunnel URL 变更检查清单

Tunnel URL 每次重启会变。更新步骤：

1. `kill` 旧 cloudflared 进程 → 重启 → 从输出复制新 URL
   ```bash
   cloudflared tunnel --url http://localhost:8765 2>&1 | tee /tmp/cf_url.txt
   grep -o 'https://[a-z-]*\.trycloudflare\.com' /tmp/cf_url.txt
   ```

2. **下载 CloudBase 当前版本**（当本地无源文件时，或本地 Desktop 源文件读不出——见下方 iCloud 坑）：
   ```bash
   tcb hosting download index.html /tmp/index_workbench.html -e <env-id>
   ```

### ⚠️ Desktop 目录 iCloud「优化存储」导致源文件读不出

**症状**：`ls ~/Desktop/` 或读 `~/Desktop/.../工具/app.html` 内容时挂起 30–180s 超时，但 `stat`（元数据）秒回、`ls ~/` 正常、`head -c 100` 读具体文件内容也卡死。根因：macOS iCloud「优化存储」把 Desktop 文件驱逐到云端、本地只留占位符，读内容触发下载而卡住。

**对策**（不用等下载，立刻绕开）：
1. 从 CloudBase 拉线上版本到 `/tmp` 再读：`tcb hosting download <云路径> /tmp/xxx.html -e <env-id>`（`/tmp` 非 iCloud，可正常 read_file/grep）。
2. 新建产物写到 `~/work/`（非 iCloud 目录）再部署，避免写 Desktop 卡住。
3. 精确 `stat`/`read_file` 单个文件路径通常不卡（只有 `ls` 目录、`cat`/`grep` 读内容才卡），诊断时先 `stat` 确认文件存在再决定是否绕行。

3. 替换文件中所有旧 Tunnel URL 为新 URL（通常 3-4 处）：
   - `feishuAutoLogin` 中的 `AUTH_URLS[0]`
   - `doLogin` 中的 `apiUrls[0]`
   - `API_BASE_URLS[0]`（数据加载）

4. 部署回 CloudBase：
   ```bash
   tcb hosting deploy /tmp/index_workbench.html -e <env-id>
   ```

5. 验证：`curl -H "Cache-Control: no-cache" <cloudbase-url> | grep <new-tunnel-url>`

⚠️ CDN 缓存约 5 分钟。部署后用浏览器无痕窗口 + `?v=<timestamp>` 参数验证。

### ⚠️ `hermes update` 会杀死后台 HTTP 服务

**症状**：`hermes update` 完成后 HTTP 8765 工作台服务被 SIGTERM，页面无法访问。

**根因**：`hermes update` 结束时执行 `gateway restart`，向进程组发送 SIGTERM 清理关联进程，波及 HTTP 服务器。

**修复**：用 tmux 隔离 HTTP 服务，使其不属于 Gateway 进程组：
```bash
tmux new-session -d -s workbench 'cd ~/Desktop/Hermes输出-工作类 && python3 脚本/销售工作台服务.py 8765'
```

`tmux` 会话不受 Gateway 重启影响，`hermes update` 后服务自动存活。

## 五、CloudBase 云函数部署（备选方案）

`tcb fn deploy` 只支持定时触发器，HTTP 触发器需在控制台手动创建。Web 函数需通过控制台或 `tcbr` CLI 部署。

## 六、数据同步

```bash
# cb_sync.sh - 定时从本地拉 MCP 数据 + 推送到 CloudBase
*/2 * * * * bash ~/Desktop/Hermes输出-工作类/脚本/cb_sync.sh
```

脚本流程：MCP pipeline → 生成 JSON → `tcb hosting deploy` 逐文件上传。

## 七、国内公网暴露方案（Tailscale Funnel 不可靠）

**核心事实（2026-09-08 实测）**：工作台 API 之前经 Tailscale Funnel（`*.ts.net`）暴露，但 ts.net 在国内公网**不可靠**：
- **DNS 选择性墙**：阿里 DNS（223.5.5.5）能解析到 103.84.155.x，腾讯 DNS（119.29.29.29）返回 NXDOMAIN。
- **IP 墙**：解析到的 IP（103.84.155.x，Tailscale 香港节点）从国内 SSL 握手被掐断，连不上。
- **云函数探针**：腾讯云云函数解析 ts.net → ENOTFOUND。
- 只有装了 Tailscale 的设备（走 MagicDNS 本地解析）能连 ts.net —— 所以"本机 curl 测正常"是假象，远程设备全连不上。

**症状**：本机 curl 正常，但用户/销售员从其他设备访问时 API 全失败（前端 CHAT_APIS 三条路径全试一遍后报"回答超时"）。

**诊断三连**（确认"远程连不上我的自建服务"）：
1. DNS 对比：`nslookup <域名> 223.5.5.5` vs `nslookup <域名> 119.29.29.29`（结果不一致 = DNS 选择性墙）。
2. 直连 IP：`curl --resolve <域名>:443:<IP> https://<域名>/`（DNS 通了但 IP 连不上 = IP 墙）。
3. 云端探针：部署一个云函数 curl 目标域名（排除本机 MagicDNS/代理干扰）。

**修复（frp 反向隧道，2026-09-08 已落地生产）**：Mac 主动出站连到国内服务器，把 8765 映射出去。完整部署（frps systemd + frpc launchd + Caddy HTTPS + 腾讯云防火墙端口 + 域名 DNS）见 `references/frp-tunnel-deployment.md`。备选：直接用腾讯云轻量服务器的「Hermes Agent」应用镜像把工作台整体搬到云端（线上平台正解，摆脱 Mac + ts.net）。详见 `references/tailscale-funnel-china-access.md`。

**⚠️ curl 默认不走系统代理**：诊断"服务是否可达"时，curl（不加 -x）直连，不代表浏览器行为；浏览器走系统代理（如 mihomo 随云 7897），代理会额外掐断 ts.net SSL。用户机器开了 mihomo 时，须把 `*.ts.net` 加进系统代理绕过列表，否则本机浏览器也连不上 ts.net。

**⚠️ 443 端口 TLS 被运营商动态干扰（2026-09-08 实战，AI 助手"正在连接…×3+超时"的根因）**：
- 症状：`curl https://api.biorisales.site/` → HTTP 000，`openssl s_client` 显示 Client Hello 已发出、0 字节读回、`errno=54`（Connection reset by peer，握手即断）。
- 判别五连测（确认"唯 443 的 TLS 被掐"，而非服务器/代理问题）：① Mac 后端 `curl 127.0.0.1:8765` → 200；② `nc -z` frps 7000 通；③ Caddy active + 服务器本机 `curl -sk --resolve api.biorisales.site:443:127.0.0.1 https://api.biorisales.site/` → 200（Caddy 健康）；④ 外部扫描器连 443 TLS 正常；⑤ **同一 IP 换 7000 端口 TLS 正常、唯 443 被 reset**。⑤ 命中 = 运营商对 443 端口做 TLS 干扰（端口特定、动态出现，非 IP 级、非服务器、非翻墙代理）。
- 修复（绕开 443）：Caddy 加非 443 监听 `https://api.biorisales.site:8443 { reverse_proxy 127.0.0.1:8765 }` → 腾讯云防火墙放行 TCP 8443 → 前端 CHAT_APIS 改为 `['https://api.biorisales.site:8443/api/chat','https://api.biorisales.site/api/chat','/api/chat','http://127.0.0.1:8765/api/chat']`（8443 优先、443 降级）→ `tcb hosting deploy` 两个 HTML。
- 排查前提：先 `scutil --proxy` 确认系统代理已关 + `ps aux|grep mihomo` 无进程（排除代理劫持出站 TLS）。

## 八、敏感数据页面「数据不在源码」（ADMINONLY + 云函数鉴权）

含敏感数据（客户案例/截图）的页面，要求数据不内嵌 HTML 源码、仅授权用户飞书免登后可拉取时：集合设 `ADMINONLY`（仅云函数可读写）+ 云函数做 open_id 白名单校验 + 前端 `callFunction` 拉数 + base64 图片压缩存库。完整模式见 `references/data-isolation-auth-pattern.md`。

**坑**：`tcb db nosql execute --command "$(cat big.json)"` 内联大 JSON（如含 base64 图）会 `Argument list too long`（macOS ARG_MAX ~1MB）→ 单文档逐个插入（每文档 ~150KB 可过）。

**坑2（OAuth code 单次有效）**：飞书授权 code 只能用一次——不能先调 feishuAuth 换身份、再调 getCases 复用同一 code（第二次必失败）。方案：合并成一个云函数（`getCases({code})` 内部完成换身份 + user_roles 白名单校验 + 返回数据）。云函数同步响应上限 ~6MB，大量截图须先 Pillow 压缩（resize 1400px + JPEG q72，11MB→4.8MB）再 base64 存库，才能一次返回。

## 九、CloudBase 对象存储（附件 / 文件上传）

CLI 子命令：`tcb storage upload | download | rm | list | url | detail | get-acl | set-acl | rules`。

```bash
T=/Users/liuxinyuan/.npm-global/lib/node_modules/@cloudbase/cli/dist/standalone/cli.js
E=<env-id>
# 探活 + 列表（返回 data:[] + meta 且无报错 = 存储已开通可用）
node $T storage list <prefix> -e $E --json
# 上传 / 下载
node $T storage upload <本地路径> <cloudPath> -e $E          # 成功回执：✔ 上传文件成功！
node $T storage download <cloudPath> <本地路径> -e $E
# 签发临时访问地址（带 sign + t，expires≈3600 秒）
node $T storage url <cloudPath> -e $E --json                 # → data.url / data.expires
# 删除：必须加 -f，否则交互式确认会把非交互调用挂住
node $T storage rm <cloudPath...> -e $E -f
```

**默认 ACL 是「仅创建者及管理员可读写」**（`storage get-acl` 实测）。这意味着：
- 客户端 JS SDK `uploadFile` 上传的匿名对象，别的登录用户读不到；把 ACL 改成「所有用户可读」又会让带签名可猜的 URL 变成对外泄露通道。
- **有自建登录体系的应用，附件一律服务端上传**：浏览器 multipart 传给 `脚本/销售工作台服务.py`（统一鉴权）→ 落本地 `数据/<业务目录>/` → 服务端 `tcb storage upload` 上桶。读取走自家接口签发临时 URL（`tcb storage url`），**归属鉴权放在自己这层**，桶的 ACL 不动。临时地址约 1 小时有效，不要缓存给前端长期使用。
- 对象路径按 `<应用名>/<记录id>/<文件名>` 组织：中间那段记录 id 就是鉴权键。若记录 id 在上传时尚未生成（上传早于保存），必须在保存时补一份 id↔目录的归属绑定（见 `baoyu-activity-entry` 的附件一节）。

**写入必须回读核对**：上传后 `storage list <prefix>` 数条数、云库文档用 `db nosql execute` QUERY 回读，不能只看 CLI 的 ✔ 回执（回执文案与真实落库是两回事）。自测产生的云端对象/文档要当场删干净（`storage rm ... -f`、DELETE 命令）。
