#!/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+i18n(W1 规范)。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 内嵌