9.6 KiB
Raw Blame History

name description version tags category
pccs-deploy-patterns PCCS/Sage应用部署模式——Menu/i18n/DSPY/RBAC/CRUD子表/主题语言切换的完整参考 1.0.0
deploy
sage
pccs
patterns
pitfall
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.appbodybody.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 个文件:

  1. wwwroot/i18n/language.ui — 从 Sage 复制,点击弹出 menu.ui
  2. wwwroot/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')"}
        ]
    }
}
  1. 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.jsonwebsite.startswiths 加前缀路由到 ahserver 内置 registerfunction

"startswiths": [
  {"leading": "/i18n_getmsgs", "registerfunction": "i18n"}
]

ahserver 内置 i18n registerfunctionahserver/globalEnv.pyrf.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 不是 otextText 的初始文本用 textotext 是 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"
    }]
}

子表 JSONcompute_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/内存)不要存在父表 — 从节点动态聚合
  • descriptiontext 类型(不是 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 控件(非 Treeicon 不用 emojitarget 用 app. 前缀
  • Header: 🌓主题 + en语言 + 👤用户面板
  • i18n: language.ui + menu.ui + RBAC any
  • Stats API 返回 widgettype 卡片格式
  • DSPY 零 f-string、零非法 import
  • add/update DSPY 有 _text + datetime 清理
  • RBAC: 写操作 logined、只读 anyRedis FLUSHDB + 重启
  • CRUD 子表 subtables + data_filter
  • 概览 page 免登录CRUD 需登录
  • 浏览器控制台零 401/403/500
  • 所有改动已 commit + push