9.6 KiB
| name | description | version | tags | category | |||||
|---|---|---|---|---|---|---|---|---|---|
| pccs-deploy-patterns | PCCS/Sage应用部署模式——Menu/i18n/DSPY/RBAC/CRUD子表/主题语言切换的完整参考 | 1.0.0 |
|
devops |
PCCS/Sage 应用部署模式
PCCS 项目上实际踩过的所有坑和正确模式。每个新应用部署前逐项检查。
1. 侧边栏 Menu 控件
不用 Tree 做菜单 — 用 Menu 控件,参照 Sage global_menu.ui。
{
"widgettype": "Menu",
"id": "sidebar_menu",
"options": {
"menuitem_css": "menuitem",
"items": [
{"name": "dashboard", "label": "📊 概览", "icon": "", "url": "{{entire_url('/pccs/overview.ui')}}", "target": "app.main_content"},
{"name": "pool", "label": "📦 算力池管理", "icon": "", "items": [
{"name": "pool_list", "label": "算力池列表", "url": "{{entire_url('/pcpool/compute_pool_list/index.ui')}}", "target": "app.main_content"}
]}
]
}
}
关键点:
| 陷阱 | 正确做法 |
|---|---|
icon: "📊" |
icon: "" — emoji 会被当作 URL 路径(/📊 → 401) |
target: "main_content" |
target: "app.main_content" — app 前缀从 body 级 DOM 搜索 |
| Tree 控件 + textField/label 坑 | 直接用 Menu 控件 |
子菜单 children |
子菜单用嵌套 items |
target 解析原理:bricks.getWidgetById('app.pccs_main_content', menuWidget) 拆分 "app.pccs_main_content" → 先解析 "app" = bricks.app(body),再 body.querySelector('#pccs_main_content')。这个路径可以跨 DOM 兄弟节点找到 target,避免 Menu 和主内容区是兄弟节点时 closest/querySelector 都找不到的问题。
2. Header 布局
参照 Sage index.ui:
[品牌] [Filler] [🌓主题] [🌐语言] [👤用户]
// 主题切换
{"widgettype": "Button", "id": "theme_toggle_btn",
"options": {"label": "🌓", "width": "36px", "height": "36px", "padding": "0"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "app",
"script": "var h=document.documentElement;var t=h.getAttribute('data-theme')||'light';var n=t=='light'?'dark':'light';h.setAttribute('data-theme',n);localStorage.setItem('pccs-theme',n);var b=bricks.getWidgetById('theme_toggle_btn',bricks.app);if(b){b.opts.label=n=='light'?'🌙':'☀️';b.dom_element.textContent=b.opts.label}"}]},
// 语言切换
{"widgettype": "urlwidget", "options": {"url": "{{entire_url('i18n/language.ui')}}"},
// 用户面板
{"widgettype": "urlwidget", "options": {"url": "{{entire_url('/rbac/user/user_panel.ui')}}"}}
注意:Button 更新文字用 b.dom_element.textContent(不是 b.refresh(),不存在这个方法)。
3. 语言切换
需要 3 个文件:
wwwroot/i18n/language.ui— 从 Sage 复制,点击弹出 menu.uiwwwroot/i18n/menu.ui— 语言选择 Menu:
{
"widgettype": "Menu",
"options": {
"cwidth": 11,
"items": [
{"name": "zh", "label": "中文", "script": "bricks.app.change_language('zh')"},
{"name": "en", "label": "English", "script": "bricks.app.change_language('en')"}
]
}
}
- RBAC
any权限 —/i18n/menu.ui需要匿名访问
方法差异:PCCS 的 bricks.js 版本使用 bricks.app.change_language('zh'),不是 Sage 的 this.change_language('zh')(Sage 的 Menu 原型有该方法,PCCS 没有)。Sage/pipeline 的 menu.ui 用 target:"app" + item script:"this.change_language('zh')"(this 指向 app)。
i18n_getmsgs 无后缀文件坑(关键,新平台必踩):bricks 默认请求 /i18n_getmsgs(无后缀)。website.processors 只按扩展名(.dspy/.ui/.xterm…)匹配,无后缀文件不命中任何 processor,被当静态文件返回 shebang 源码,前端 get_lang_dic() 报 "#!/usr/bin"... is not valid JSON,翻译全不加载(但 change_language 本身正常,语言代码仍会切)。修复:config.json 的 website.startswiths 加前缀路由到 ahserver 内置 registerfunction:
"startswiths": [
{"leading": "/i18n_getmsgs", "registerfunction": "i18n"}
]
ahserver 内置 i18n registerfunction(ahserver/globalEnv.py 里 rf.register('i18n', i18n))读 wwwroot/{i18n_path}/{lang}/i18n.json 直接返回 JSON。sage 的 config.json 有此配置;pipeline 等新平台接入 i18n 时容易漏,症状就是翻译加载失败但语言切换按钮能弹能切。
language.ui 完整结构(Text 按钮 + 点击 Popup 弹 menu.ui + lang 事件更新显示):
{"widgettype": "Text", "id": "language",
"options": {"tip": "切换语言", "text": "🌐", "css": "clickable", "cfontsize": 1.2},
"binds": [
{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "Popup",
"popup_options": {"eventpos": true, "cwidth": 11, "cheight": 10, "dismiss_events": ["command"]},
"options": {"url": "{{entire_url('menu.ui')}}"}},
{"wid": "app", "event": "lang", "actiontype": "script", "target": "self",
"script": "this.set_text(bricks.app.lang)"}
]}
text 不是 otext:Text 的初始文本用 text;otext 是 i18n 文本,需配 i18n:true,否则渲染为空(DOM 里 #language textContent 为空,点不到)。lang 事件更新用 set_text(bricks.app.lang)。
验证:curl 'http://host/i18n_getmsgs?lang=en&i18n=i18n' 应返回 JSON(非 shebang);浏览器点 🌐 → 弹语言菜单 → 选 en → bricks.app.lang==='en' 且 🌐 文本变 "en"。
4. DSPY 规则
| 规则 | 说明 |
|---|---|
| 禁止 f-string | 导致 unterminated string literal,用 + 拼接 |
禁止 import os |
os 未预加载 |
add/update 清理 _text |
for k in list(ns.keys()): if k.endswith('_text'): ns.pop(k) |
| add/update 清理空 datetime | for k in list(ns.keys()): if k.endswith('_at') or k.endswith('_time') or k=='last_heartbeat': if ns[k]=='' or ns[k] is None: ns[k]=None |
| CRUD stub 必须真操作 DB | 不能只 return {'status': 'ok'} |
| Stats API 返回 widgettype | 不能裸 {status: 'ok'},要用 HBox + VBox 卡片返回 |
5. 概览卡片格式
Stats API 返回 HBox + 4 列 VBox 卡片:
return {
'widgettype': 'HBox',
'options': {'gap': '16px'},
'subwidgets': [
{'widgettype': 'VBox', 'options': {'bgcolor': '#eff6ff', 'padding': '16px', 'width': '25%', 'border': '1px solid #dbeafe', 'borderRadius': '8px'},
'subwidgets': [
{'widgettype': 'Text', 'options': {'otext': '算力池', 'cfontsize': 0.7, 'color': '#64748b'}},
{'widgettype': 'Text', 'options': {'otext': str(count) + ' 个', 'cfontsize': 2, 'fontWeight': 'bold', 'color': '#2563eb'}},
{'widgettype': 'Text', 'options': {'otext': str(active) + ' 运行中', 'cfontsize': 0.7, 'color': '#94a3b8'}}
]},
# ... 3 more cards
]
}
4 卡配色:算力池=蓝(#eff6ff/#2563eb)、CPU=紫(#f5f3ff/#7c3aed)、GPU=绿(#ecfdf5/#059669)、内存=橙(#fff7ed/#ea580c)
6. RBAC 权限
必须 redis-cli FLUSHDB + 重启 才能让 DB 权限改动生效(RBAC 有 Redis 缓存)。
角色分配原则:
| 角色 | 用途 | 示例 |
|---|---|---|
any |
匿名只读 | stats, get_.dspy, index.ui, menu.ui, i18n/ |
logined |
需登录的写操作 | create, update, delete, deploy, allocate |
any 必须覆盖的路径:
- Bricks 静态资源:
/bricks/**,/favicon.ico,/bricks/imgs/**,/bricks/3parties/** - i18n:
/i18n_getmsgs,/i18n/language.ui,/i18n/menu.ui - RBAC login:
/rbac/login.css - 概览 stats:
/pcpool/api/pool_stats.dspy,/pcc/api/cluster_status_all.dspy, 等
7. CRUD 子表
JSON 配置(父表 compute_pool_list):
"params": {
"subtables": [{
"field": "pool_id",
"title": "算力节点",
"url": "{{entire_url('../compute_node_list')}}",
"subtable": "compute_node"
}]
}
子表 JSON(compute_node_list)需要 data_filter:
"params": {
"data_filter": {"AND": [{"field": "pool_id", "op": "=", "var": "pool_id"}]}
}
框架自动通过 params_mapping: {mapping: {id: pool_id}} 传递父 ID → 子表。
注意:toolbar.tools: [] 空数组会阻止框架生成默认行操作按钮 — 应删除 tools 键或设为 null。
8. 模型设计
- 资源字段(CPU/GPU/内存)不要存在父表 — 从节点动态聚合
description用text类型(不是str)- 存储容量同理 — 从导出/挂载统计
- 聚合在 DSPY 里实时查询子表
SUM(CASE WHEN status='allocated' THEN ...)
9. 开发流程
标准流程:本地改 → commit → push → 服务器 pull → pip install → 重启
禁止 SSH 直改服务器源文件!
# 服务器拉取
cd /d/pccs/pkgs/模块名 && git pull && /d/pccs/py3/bin/pip install . -q
# 重启
pkill -f pccs.py; nohup bash /d/pccs/start.sh > /d/pccs/logs/nohup.log 2>&1 &
部署验证清单
- Menu 控件(非 Tree),icon 不用 emoji,target 用 app. 前缀
- Header: 🌓主题 + en语言 + 👤用户面板
- i18n: language.ui + menu.ui + RBAC any
- Stats API 返回 widgettype 卡片格式
- DSPY 零 f-string、零非法 import
- add/update DSPY 有 _text + datetime 清理
- RBAC: 写操作 logined、只读 any,Redis FLUSHDB + 重启
- CRUD 子表 subtables + data_filter
- 概览 page 免登录,CRUD 需登录
- 浏览器控制台零 401/403/500
- 所有改动已 commit + push