---
name: baoyu-cloudbase-workbench
description: CloudBase 部署工作台：静态托管+匿名鉴权+权限体系+飞书免登。
category: productivity
---

# 宝锐工作台 CloudBase 部署与权限体系

## 架构概览

```
CloudBase 静态托管（CDN + HTTPS）
  ├── index.html → 销售工作台（从根目录部署，避免中文 URL 路径）
  ├── admin.html → 权限管理页面
  ├── 数据/*.json → 业务数据（每 2h 由 cb_sync.sh 从 MCP 同步）
  └── logo.png
  +
CloudBase 文档数据库
  └── user_roles 集合 → 23 人权限配置
  +
本地 Python 服务器（端口 8765）
  └── /api/feishu/auth → 飞书 OAuth 免登中转
```

## 快速命令

### 环境变量
```bash
CB_ENV=bier-sales-d0gatbvlx288724e9
CB_URL=https://bier-sales-d0gatbvlx288724e9-1458438710.tcloudbaseapp.com
```

### 部署 HTML
```bash
cp 工具/销售工作台.html /tmp/index.html
tcb hosting deploy /tmp/index.html index.html -e $CB_ENV
tcb hosting deploy /tmp/index.html 工具/index.html -e $CB_ENV
```

### 数据库操作
```bash
# 查询数量
python3 -c "import json;c=json.dumps([{'TableName':'user_roles','CommandType':'COMMAND','Command':json.dumps({'count':'user_roles','query':{}})}]);open('/tmp/q.json','w').write(c)"
tcb db nosql execute -e $CB_ENV --command "$(cat /tmp/q.json)"

# 全量替换（先清空再插入）
python3 -c "
import json
users = json.load(open('user_config.json'))['users']
clear = json.dumps([{'TableName':'user_roles','CommandType':'COMMAND','Command':json.dumps({'delete':'user_roles','deletes':[{'q':{},'limit':0}]})}])
insert = json.dumps([{'TableName':'user_roles','CommandType':'INSERT','Command':json.dumps({'insert':'user_roles','documents':users},ensure_ascii=False)}],ensure_ascii=False)
open('/tmp/clear.json','w').write(clear)
open('/tmp/insert.json','w').write(insert)
"
tcb db nosql execute -e $CB_ENV --command "$(cat /tmp/clear.json)"
tcb db nosql execute -e $CB_ENV --command "$(cat /tmp/insert.json)"
```

## user_roles 权限模型

| role | 数据范围 | Tab可见 |
|------|---------|---------|
| admin | 全公司 | 全部 |
| manager | 本部门（dept 过滤） | 全部（含看板） |
| sales | 本人（salesperson 过滤） | 隐藏看板 |

### 前端登录流程

```javascript
// 1. 匿名登录 CloudBase
cbApp.auth({ persistence: 'local' }).anonymousAuthProvider().signIn()

// 2. 加载用户列表（必须在匿名登录完成后）
cbDb.collection('user_roles').limit(100).get()

// 3. 用户选姓名 + 输密码 → 验证
cbDb.collection('user_roles').where({ name }).get()
  → 比 password 字段 → finishLogin(user)

// 4. 存储完整用户对象
state.user = { name, role, dept, tabs, email }
localStorage.setItem(ROLE_KEY, JSON.stringify(state.user))
```

### 数据过滤

```javascript
function getFilteredCustomers() {
  if (role === 'admin') return custs;
  if (role === 'manager') return custs.filter(c => c.dept === dept);
  return custs.filter(c => c.salesperson === name);
}
```

### Tab 过滤

```javascript
function renderNav() {
  var visible = TABS.filter(function(t) {
    if (u.role === 'admin') return true;
    if (t.id === 'dashboard' && !tabs.dashboard) return false;
    return true;
  });
}
```

## 飞书免登（OAuth）

### 本地服务器端点（/api/feishu/auth）

```python
if path == '/api/feishu/auth':
    code = payload.get('code')
    # 1. app_access_token
    # 2. POST open-apis/authen/v1/oidc/access_token
    # 3. 返回 {ok:true, name:'张三'}
```

### 前端检测

```javascript
function init() {
  var code = new URLSearchParams(window.location.search).get('code');
  if (code) { feishuAutoLogin(code); return; }
}
function feishuAutoLogin(code) {
  fetch('http://127.0.0.1:8765/api/feishu/auth', {
    method:'POST', body: JSON.stringify({code})
  }).then(r => r.json()).then(data => {
    cbDb.collection('user_roles').where({name:data.name}).get()
      .then(res => finishLogin(res.data[0]));
  }).catch(() => showLoginForm());
}
```

## 🚨 登录故障排查

当用户反馈"密码登录不了"时，按以下步骤排查。

### 1. 确认姓名下拉框选择（最高频！）
登录页面有一个 **姓名下拉框**（`<select id="loginRole">`），默认选中第一人（通常是陈溪子）。用户往往不选人就直接输密码，导致用陈溪子的密码校验自己的账号 → 报"密码错误"。

**验证方法**：在浏览器 Console 里直接调用登录函数验证：
```javascript
// 设置正确的用户名和密码后直接触发登录
document.getElementById('loginRole').value = '刘新元';
document.getElementById('loginPwd').value = 'liuxinyuan';
doLogin();  // 观察错误提示
```

### 2. 确认数据库密码值（含隐藏字符检查）
```bash
# 查询并检查密码字节（排除零宽字符、BOM 等）
tcb db nosql execute --command '[
  {"TableName":"user_roles","CommandType":"QUERY",
   "Command":"{\"find\":\"user_roles\",\"filter\":{\"name\":\"刘新元\"},\"limit\":1}"}
]' --env-id bier-sales-d0gatbvlx288724e9 --json > /tmp/pwd_check.json

python3 -c "
import json
with open('/tmp/pwd_check.json') as f:
    content = f.read()
idx = content.index('{')
data = json.loads(content[idx:])
user = data['data']['results'][0][0]
pwd = user['pwd']
print(f'pwd: [{pwd}]')
print(f'hex: {pwd.encode(\"utf-8\").hex()}')
print(f'len: {len(pwd)}')
"
```
预期输出：`hex: 6c697578696e7975616e`（"liuxinyuan"），长度为 10。任何偏差说明数据库存储有问题。

### 3. 确认 user_roles 集合权限
集合必须设为 **"所有用户可读"**，否则匿名登录后 `collection('user_roles').get()` 返回空数组。

### 4. 桌面端无法登录但手机端正常（浏览器环境差异）

**症状**：同一账号，手机（飞书内嵌浏览器）能登录，桌面 Chrome/Safari 报"密码错误"。

**常见原因**：
- **密码管理器自动填充**：桌面浏览器的 iCloud 钥匙串/Chrome 密码管理器可能自动填充了旧密码或额外字符。清空密码框，手动逐字输入测试。
- **飞书桌面端内置浏览器**：飞书桌面 App 的内嵌 Chromium 有限制。用 **独立浏览器无痕窗口** 打开测试。
- **浏览器扩展干扰**：广告拦截器、隐私扩展可能阻止 CloudBase SDK 加载或匿名认证。

**浏览器 Console 深度诊断**：
```javascript
// 1. 检查 CloudBase SDK
typeof cloudbase !== 'undefined'  // 应为 true
typeof cbDb !== 'undefined'       // 应为 true

// 2. 检查匿名认证状态
cbApp.auth({ persistence: 'local' }).getLoginState()
  .then(s => console.log('auth:', s ? 'OK' : 'FAIL'))

// 3. 直接测试数据库查询
cbDb.collection('user_roles').where({ name: '刘新元' }).get()
  .then(r => console.log('query OK:', r.data.length, 'records'))
  .catch(e => console.error('query FAIL:', e.message))

// 4. 模拟登录逻辑（在 Console 中观察每一步）
var pwd = document.getElementById('loginPwd').value;
console.log('input pwd:', JSON.stringify(pwd), 'len:', pwd.length);
// 对比数据库返回的 user.pwd
```

完整诊断流程详见 `references/login-diagnostic-flow.md`。

## 🔴 避坑手册

| 陷阱 | 修复 |
|------|------|
| 登录报"密码错误"但密码实际正确 | ① 检查姓名下拉框是否选了正确的人 ② 手动输入密码（排除密码管理器） ③ 用独立浏览器无痕窗口 ④ Console 深度诊断 |
| 桌面不能登录但手机正常 | 密码管理器自动填充、飞书桌面端内置浏览器限制、浏览器扩展干扰 |
| tcb fn deploy 需交互式选择 | 走控制台手动操作 |
| CloudBase URL 含中文 → 飞书校验失败 | 部署到根目录 index.html |
| tcb fn trigger 只支持 cron | HTTP 触发器必须控制台手动创建 |
| 体验版限制 2 个授权用户 | 匿名登录 + 数据库验证绕过 |
| MongoDB 命令 JSON 嵌套引号 | 先写文件再用 `$(cat file.json)` |
| loadUserList 在匿名登录前调用 | 移到 signIn().then() 块内 |
| AUTH[state.user].name → undefined | state.user 已是对象，直接 .name |
| state.user === 'gm' → 永假 | 改为 state.user.role === 'admin' |

## 飞书活动导航

不依赖 MCP 写权限。URL 跳转飞书原生页面：

| 跳转目标 | URL |
|----------|-----|
| 活动主页 | `https://project.feishu.cn/xsguanli/sj/homepage` |
| 新建活动 | `https://project.feishu.cn/baoyu/workitem/create?project_key=6593cd71471290e3cc6be6e6&work_item_type_key=65ae1e5d44338dbe7c39a29a` |
