pipeline-app/scripts/gen_home_index.py

320 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""生成产线平台新 index.ui登录→工作台原 index.ui 内容未登录→后现代主页bricks 声明式)。
用法: python3 scripts/gen_home_index.py (在 pipeline-app 仓库根目录跑)"""
import json, os, io
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IDX = os.path.join(ROOT, 'wwwroot', 'index.ui')
with io.open(IDX, encoding='utf-8') as f:
old = f.read()
# 幂等若已是双分支版本先抽出工作台分支old再重拼门户分支
PREFIX = '{% if get_user() %}\n'
MARK = '\n{% else %}\n{'
if old.startswith(PREFIX):
i = old.rfind(MARK)
assert i > 0, 'generated file but marker not found'
old = old[len(PREFIX):i] + '\n'
else:
assert '{% if get_user() %}' not in old, 'unexpected mixed file'
def T(s, css=None, **kw):
"""TextBase 三件套otext+text+i18nW1 规范。binds/id 必须在 widget 顶层bricks 不读 options.binds"""
o = {'otext': s, 'text': s, 'i18n': True, 'halign': 'left'}
if css:
o['css'] = css
top = {k: kw.pop(k) for k in ('binds', 'id') if k in kw}
o.update(kw)
w = {'widgettype': 'Text', 'options': o}
w.update(top)
return w
def TN(s, css=None, **kw):
"""不翻译的 Text数字/英文装饰字。binds/id 同样提到顶层。"""
o = {'text': s, 'halign': 'left'}
if css:
o['css'] = css
top = {k: kw.pop(k) for k in ('binds', 'id') if k in kw}
o.update(kw)
w = {'widgettype': 'Text', 'options': o}
w.update(top)
return w
def login_bind():
return {'wid': 'self', 'event': 'click', 'actiontype': 'urlwidget', 'target': 'self',
'options': {'url': "{{entire_url('/rbac/user/login.ui')}}"}}
def scroll_script(anchor_id):
return ("var e=document.getElementById(%s);if(e){e.scrollIntoView({behavior:'smooth',block:'start'});}"
% json.dumps(anchor_id))
def btn(label, css, bind=None, script=None, small_id=None):
w = {'widgettype': 'Button', 'options': {'label': label, 'css': css}}
if small_id:
w['id'] = small_id
b = bind if bind is not None else {
'wid': 'self', 'event': 'click', 'actiontype': 'script', 'target': 'self', 'script': script}
w['binds'] = [b]
return w
THEME_SCRIPT = ("var h=document.documentElement;var t=h.getAttribute('data-theme')||'light';"
"var n=t=='light'?'dark':'light';h.setAttribute('data-theme',n);"
"try{localStorage.setItem('pipeline_ui_theme',n);}catch(e){}"
"if(window.applyPipelineTheme){window.applyPipelineTheme(n);}")
# ---------- header ----------
header = {
'widgettype': 'HBox',
'options': {'css': 'pl-header', 'width': '100%', 'padding': '0 clamp(20px,6vw,90px)',
'alignItems': 'center', 'gap': '14px', 'cheight': 3.2},
'subwidgets': [
{'widgettype': 'VBox', 'options': {'css': 'pl-logo-box', 'gap': '2px', 'alignItems': 'flex-start'},
'subwidgets': [T('产线平台', 'pl-logo'), TN('PIPELINE PLATFORM', 'pl-logo-sub')]},
{'widgettype': 'Filler'},
T('能力', 'pl-nav', binds=[
{'wid': 'self', 'event': 'click', 'actiontype': 'script', 'target': 'self',
'script': scroll_script('sec_caps')}]),
T('产线', 'pl-nav', binds=[
{'wid': 'self', 'event': 'click', 'actiontype': 'script', 'target': 'self',
'script': scroll_script('sec_pipes')}]),
T('流程', 'pl-nav', binds=[
{'wid': 'self', 'event': 'click', 'actiontype': 'script', 'target': 'self',
'script': scroll_script('sec_flow')}]),
btn('🌙', 'pl-btn-ghost theme-icon-btn', script=THEME_SCRIPT, small_id='theme_toggle_btn'),
{'widgettype': 'urlwidget', 'options': {'url': "{{entire_url('i18n/language.ui')}}"}},
btn('登录 / 注册', 'pl-btn pl-btn-sm', bind=login_bind()),
],
}
# ---------- hero ----------
hero = {
'widgettype': 'VBox',
'options': {'css': 'pl-hero', 'width': '100%'},
'subwidgets': [
{'widgettype': 'VBox', 'options': {'css': 'pl-hero-deco'},
'subwidgets': [
{'widgettype': 'VBox', 'options': {'css': 'pl-grid'}},
{'widgettype': 'VBox', 'options': {'css': 'pl-orb pl-orb-1'}},
{'widgettype': 'VBox', 'options': {'css': 'pl-orb pl-orb-2'}},
{'widgettype': 'VBox', 'options': {'css': 'pl-orb pl-orb-3'}},
{'widgettype': 'VBox', 'options': {'css': 'pl-noise'}},
]},
{'widgettype': 'VBox',
'options': {'css': 'pl-hero-content', 'width': '100%', 'gap': '22px', 'alignItems': 'flex-start'},
'subwidgets': [
T('AI 原生 · 产线操作系统', 'pl-badge pl-rise pl-d1'),
T('把业务装进产线', 'pl-h1 pl-stroke pl-rise pl-d2'),
T('让 Agent 自主运转', 'pl-h1 pl-grad pl-rise pl-d2'),
T('产线平台是 Agent 驱动的企业智能产线操作系统:商机、投标、开发一键开工,'
'大模型智能体自主推进任务,人类门禁把关质量,产出即产品、跑完即记账。',
'pl-sub pl-rise pl-d3', wrap=True),
{'widgettype': 'HBox',
'options': {'gap': '14px', 'flexWrap': 'wrap', 'alignItems': 'center'},
'subwidgets': [
btn('登录 / 注册', 'pl-btn pl-rise pl-d4', bind=login_bind()),
btn('查看平台能力', 'pl-btn-ghost pl-rise pl-d4', script=scroll_script('sec_caps')),
]},
{'widgettype': 'HBox',
'options': {'css': 'pl-rise pl-d5', 'gap': '40px', 'flexWrap': 'wrap',
'padding': '18px 0 0 0', 'alignItems': 'flex-start'},
'subwidgets': [
{'widgettype': 'VBox', 'options': {'gap': '2px'},
'subwidgets': [TN('5', 'pl-stat-n'), T('产线矩阵', 'pl-stat-t')]},
{'widgettype': 'VBox', 'options': {'gap': '2px'},
'subwidgets': [TN('30+', 'pl-stat-n'), T('功能模块', 'pl-stat-t')]},
{'widgettype': 'VBox', 'options': {'gap': '2px'},
'subwidgets': [TN('24×7', 'pl-stat-n'), T('自主运转', 'pl-stat-t')]},
{'widgettype': 'VBox', 'options': {'gap': '2px'},
'subwidgets': [TN('4', 'pl-stat-n'), T('语言国际化', 'pl-stat-t')]},
]},
]},
],
}
# ---------- marquee ----------
mq_items = ['商机挖掘', '投标生产', '软件开发', '模型网关', '产品商品化',
'计费记账', '供销协同', '知识库', '人机协同门禁']
def mq_track():
return {'widgettype': 'HBox', 'options': {'css': 'pl-mq-track'},
'subwidgets': [T(x, 'pl-mq-item') for x in mq_items * 2]}
marquee = {'widgettype': 'HBox', 'options': {'css': 'pl-marquee', 'width': '100%'},
'subwidgets': [mq_track()]}
# ---------- capabilities (bento) ----------
def card(icon, title, desc, tag, lg=False, flex='1 1 260px'):
css = 'pl-card pl-card-lg' if lg else 'pl-card'
subs = []
if tag:
subs.append(TN(tag, 'pl-card-tag'))
subs.append(TN(icon, 'pl-card-icon'))
subs.append(T(title, 'pl-card-t'))
subs.append(T(desc, 'pl-card-d', wrap=True))
return {'widgettype': 'VBox',
'options': {'css': css, 'flex': flex, 'gap': '10px', 'alignItems': 'flex-start'},
'subwidgets': subs}
caps = {
'id': 'sec_caps',
'widgettype': 'VBox',
'options': {'css': 'pl-sec', 'width': '100%', 'gap': '18px', 'alignItems': 'flex-start'},
'subwidgets': [
TN('CAPABILITIES', 'pl-kicker'),
T('平台能力', 'pl-h2'),
T('不是聊天机器人,是一条条能自主干活、能被审计、能直接计费的智能产线。', 'pl-sec-sub', wrap=True),
{'widgettype': 'HBox',
'options': {'width': '100%', 'flexWrap': 'wrap', 'gap': '16px', 'padding': '18px 0 0 0',
'alignItems': 'stretch'},
'subwidgets': [
card('🤖', 'Agent 驱动产线引擎',
'会话式开工一句话启动流程Agent 自动拆解任务、调用大模型、产出交付件;'
'LLM 三层容错兜底,产线不因单点模型故障停摆。', 'CORE', lg=True, flex='2 2 540px'),
card('🛰', '模型网关',
'多供应商大模型统一接入:余额门禁、会话粘性轮询、故障自动路由、按调用出账,密钥不出进程。', 'GATEWAY'),
card('🛒', '产品商品化',
'资源→产品→定价→折扣→订购→计费全链路;显示价按「真实定价×客户折扣」计算,跑产线即消费、即记账。', 'COMMERCE'),
card('🔔', '平台待办中枢',
'全局异步通知中枢:门禁、确认、上传等产线事件统一冒泡为人类待办,同一事项只发一次、处理完自动补位。', 'HUMAN LOOP'),
card('🛡', '人机协同门禁',
'PM 审核、QC 质检、人工确认层层把关——机器的速度,人的判断,审计日志全程可追溯。', 'GATE'),
card('🔐', '安全与多租户',
'RBAC 细粒度路径权限、机构隔离、短令牌内部代理、登录审计与失败锁定。', 'SECURITY'),
card('🌐', '国际化与双主题',
'中 / 英 / 日 / 韩四语实时切换,明暗双主题一键换装,桌面与手机 H5 同构。', 'I18N'),
]},
],
}
# ---------- pipelines ----------
def pipe(no, name, desc, live):
tag = T('运行中' if live else '规划中', 'pl-tag ' + ('pl-tag-live' if live else 'pl-tag-soon'))
return {'widgettype': 'HBox',
'options': {'css': 'pl-step', 'width': '100%', 'gap': '20px', 'alignItems': 'center',
'flexWrap': 'wrap'},
'subwidgets': [
TN(no, 'pl-step-n'),
{'widgettype': 'VBox',
'options': {'css': 'filler', 'flex': '1 1 320px', 'gap': '4px', 'minWidth': '240px'},
'subwidgets': [T(name, 'pl-step-t'), T(desc, 'pl-step-d', wrap=True)]},
tag,
]}
pipes = {
'id': 'sec_pipes',
'widgettype': 'VBox',
'options': {'css': 'pl-sec', 'width': '100%', 'gap': '16px', 'alignItems': 'flex-start'},
'subwidgets': [
TN('PIPELINES', 'pl-kicker'),
T('产线矩阵', 'pl-h2'),
T('每条产线都是一个完整的业务闭环:从情报输入到可交付产出,全程 Agent 推进、人类门禁把关。', 'pl-sec-sub', wrap=True),
{'widgettype': 'VBox',
'options': {'width': '100%', 'gap': '12px', 'padding': '18px 0 0 0'},
'subwidgets': [
pipe('01', '商机产线', '招标情报采集 → 商机研判 → 研发报告 → 人工确认,把市场信号变成可跟进的商机。', True),
pipe('02', '投标产线', '标书智能编制:逐项应答评分标准、技术/商务分册生成、材料缺口清单、多轮 QC 门禁。', True),
pipe('03', '开发产线', 'SDLC 任务链:需求 → 设计 → 开发 → 测试 → 部署PM 审核门控 + 交付件管理。', True),
pipe('04', '运维产线', '面向 FOMS 的运维自动化:告警处置、巡检报告、变更编排。', False),
pipe('05', '营销产线', '内容生产与投放自动化:素材生成、渠道分发、效果回收。', False),
]},
],
}
# ---------- flow ----------
def step(no, title, desc):
return {'widgettype': 'VBox',
'options': {'css': 'pl-card', 'flex': '1 1 190px', 'gap': '8px', 'alignItems': 'flex-start',
'minHeight': '170px'},
'subwidgets': [TN(no, 'pl-step-n'), T(title, 'pl-card-t'), T(desc, 'pl-card-d', wrap=True)]}
flow = {
'id': 'sec_flow',
'widgettype': 'VBox',
'options': {'css': 'pl-sec', 'width': '100%', 'gap': '16px', 'alignItems': 'flex-start'},
'subwidgets': [
TN('HOW IT WORKS', 'pl-kicker'),
T('五步跑通一条产线', 'pl-h2'),
{'widgettype': 'HBox',
'options': {'width': '100%', 'flexWrap': 'wrap', 'gap': '14px', 'padding': '18px 0 0 0',
'alignItems': 'stretch'},
'subwidgets': [
step('01', '订阅产线', '在产品商城选购产线订阅,账号权益即时生效。'),
step('02', '发起会话', '进入产线工作台,用一句话下达任务。'),
step('03', 'Agent 自主执行', '任务拆解、模型调用、交付件产出全程自动推进,可随时插话纠偏。'),
step('04', '人类门禁', '关键节点冒泡为平台待办,人看过正文再放行。'),
step('05', '交付与记账', '产出归档、按量出账,消费明细随时可查。'),
]},
],
}
# ---------- CTA + footer ----------
cta = {
'widgettype': 'VBox',
'options': {'css': 'pl-cta', 'width': 'auto', 'gap': '16px', 'alignItems': 'center'},
'subwidgets': [
T('准备好把业务装上产线了吗?', 'pl-cta-t'),
T('注册账号,订阅第一条产线,让 Agent 今天就开始干活。', 'pl-cta-d'),
btn('立即注册', 'pl-btn', bind=login_bind()),
],
}
footer = {
'widgettype': 'HBox',
'options': {'css': 'pl-footer', 'width': '100%', 'justifyContent': 'space-between',
'alignItems': 'center', 'flexWrap': 'wrap', 'gap': '8px'},
'subwidgets': [
T('© 2026 开元云(北京)科技有限公司', 'pl-footer-t'),
TN('PIPELINE PLATFORM · BUILT WITH BRICKS', 'pl-footer-t'),
],
}
landing = {
'id': 'app',
'widgettype': 'VBox',
'options': {'css': 'pl-page', 'width': '100%', 'height': '100%', 'padding': '0',
'overflow': 'hidden'},
'subwidgets': [
{'widgettype': 'VBox',
'options': {'css': 'pl-scroll', 'width': '100%', 'flex': '1 1 auto', 'minHeight': '0',
'overflowY': 'auto', 'alignItems': 'flex-start'},
'subwidgets': [header, hero, marquee, caps, pipes, flow, cta, footer]},
],
# 注册成功register.dspy dispatch user_logined后整页刷新 → 服务端渲染工作台分支。
# 密码登录up_login.dspy manual走自身的 dismissed→in-place 渲染 /index.ui不经此事件无双重刷新。
'binds': [
{'wid': 'app', 'event': 'user_logined', 'actiontype': 'script', 'target': 'self',
'script': 'window.location.reload();'},
],
}
landing_txt = json.dumps(landing, ensure_ascii=False, indent=1)
# 防 .ui 内嵌 <script> 提前闭合(本页面没有 Html 控件,保险检查)
assert '<script' not in landing_txt
new = ('{% if get_user() %}\n' + old + '\n{% else %}\n' + landing_txt + '\n{% endif %}\n')
with io.open(IDX, 'w', encoding='utf-8') as f:
f.write(new)
print('index.ui written, %d bytes' % len(new.encode('utf-8')))
# 收集需要 i18n 的 keyotext 全集 + Button label
keys = []
def walk(w):
if isinstance(w, dict):
o = w.get('options', {})
if isinstance(o, dict):
if o.get('i18n') and o.get('otext'):
keys.append(o['otext'])
if w.get('widgettype') == 'Button' and o.get('label') and not o['label'].startswith('🌙'):
keys.append(o['label'])
for v in w.values():
walk(v)
elif isinstance(w, list):
for v in w:
walk(v)
walk(landing)
seen = set()
uniq = [k for k in keys if not (k in seen or seen.add(k))]
with io.open('/tmp/home_i18n_keys.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(uniq))
print('%d unique i18n keys -> /tmp/home_i18n_keys.txt' % len(uniq))
for k in uniq:
print(' -', k)