From 14566d9866b3b2a7f954ec29f86e10280ab3f02a Mon Sep 17 00:00:00 2001 From: dannydeng Date: Wed, 22 Apr 2026 16:22:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(prototypes):=20=E9=87=8D=E6=9E=84=E5=8E=9F?= =?UTF-8?q?=E5=9E=8B=E5=B7=A5=E5=85=B7=E5=B9=B6=E6=96=B0=E5=A2=9E=E5=B1=95?= =?UTF-8?q?=E5=8E=85=E9=A6=96=E9=A1=B5=E4=B8=8E=E6=B7=B1=E5=BA=A6=E5=8E=9F?= =?UTF-8?q?=E5=9E=8B=20--story=3D133683187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...gine-admin-interaction-audit-2026-04-20.md | 612 +++++ .ai/skills/prototype-generator/SKILL.md | 138 +- .flake8 | 3 + .gitignore | 4 +- docs/plans/2026-03-23-prototype-toolkit.md | 879 ++++++ .../plans/2026-04-10-prototype-bk-standard.md | 846 ++++++ ...-bkflow-engine-admin-prototype-overhaul.md | 812 ++++++ .../2026-04-22-prototype-showcase-homepage.md | 640 +++++ .../2026-03-23-prototype-toolkit-design.md | 434 +++ ...2026-04-10-prototype-bk-standard-design.md | 371 +++ ...-engine-admin-prototype-overhaul-design.md | 517 ++++ ...4-22-prototype-showcase-homepage-design.md | 385 +++ prototypes/README.md | 114 + prototypes/assets/bkflow-prototype.css | 2363 +++++++++++++++++ prototypes/assets/bkflow-prototype.js | 277 ++ prototypes/assets/bkflow-showcase.css | 832 ++++++ prototypes/assets/bkflow-showcase.js | 114 + prototypes/assets/icons/bkflow-logo.svg | 8 + prototypes/examples/README.md | 28 + prototypes/examples/component-showcase.html | 609 +++++ prototypes/features/README.md | 55 + prototypes/features/_legacy/README.md | 28 + .../_legacy/node-output-viewer/README.md | 9 + .../_legacy/node-output-viewer/index.html | 646 +++++ .../_legacy/sops-open-plugin/README.md | 13 + .../_legacy/sops-open-plugin/index.html | 101 + .../_legacy/sops-open-plugin/main-flow.html | 343 +++ .../space-open-plugin-management.html | 343 +++ .../task-plugin-error-state.html | 281 ++ .../template-plugin-selection.html | 362 +++ .../_legacy/space-variable-manage/README.md | 9 + .../_legacy/space-variable-manage/index.html | 651 +++++ .../CHANGELOG.md | 6 + .../README.md | 50 + .../feature.meta.json | 22 + .../index.html | 278 ++ .../pages/plugin/plugin-list.html | 452 ++++ .../pages/space/credential-list.html | 256 ++ .../pages/space/debug-task-list.html | 199 ++ .../pages/space/decision-editor.html | 158 ++ .../pages/space/decision-list.html | 121 + .../pages/space/flow-debug.html | 219 ++ .../pages/space/flow-edit.html | 356 +++ .../pages/space/flow-view.html | 172 ++ .../pages/space/label-list.html | 243 ++ .../pages/space/space-config.html | 210 ++ .../pages/space/statistics-exception.html | 178 ++ .../pages/space/task-detail-complete.html | 372 +++ .../pages/space/task-detail-failed.html | 389 +++ .../pages/space/task-list.html | 167 ++ .../pages/space/template-list.html | 433 +++ .../pages/system/module-config-list.html | 458 ++++ .../pages/system/space-config-list.html | 451 ++++ prototypes/masters/README.md | 28 + prototypes/masters/_shared/README.md | 28 + prototypes/masters/_shared/plugin-shell.html | 83 + prototypes/masters/_shared/space-shell.html | 121 + prototypes/masters/_shared/system-shell.html | 91 + prototypes/masters/config-page/README.md | 12 + .../masters/config-page/states/dirty.html | 115 + prototypes/masters/config-page/template.html | 125 + prototypes/masters/decision-editor/README.md | 12 + .../masters/decision-editor/template.html | 122 + prototypes/masters/engine-panel/README.md | 12 + prototypes/masters/engine-panel/template.html | 161 ++ prototypes/masters/flow-editor/README.md | 13 + .../states/mock-debug-confirm.html | 85 + .../flow-editor/states/node-selected.html | 117 + .../flow-editor/states/publish-confirm.html | 88 + .../flow-editor/states/save-ready.html | 96 + prototypes/masters/flow-editor/template.html | 314 +++ prototypes/masters/list-page/README.md | 13 + .../list-page/states/bulk-actions.html | 116 + .../masters/list-page/states/empty.html | 55 + .../masters/list-page/states/error.html | 55 + prototypes/masters/list-page/template.html | 131 + prototypes/masters/overlays/README.md | 26 + prototypes/masters/overlays/dialogs.html | 84 + prototypes/masters/overlays/sidesliders.html | 76 + prototypes/masters/task-detail/README.md | 14 + .../masters/task-detail/states/complete.html | 136 + .../masters/task-detail/states/empty-log.html | 108 + .../masters/task-detail/states/failed.html | 139 + prototypes/masters/task-detail/template.html | 312 +++ prototypes/serve.py | 578 ++++ prototypes/showcase_catalog.py | 167 ++ tests/interface/prototypes/__init__.py | 1 + .../test_bkflow_engine_admin_feature.py | 206 ++ .../prototypes/test_flow_editor_contract.py | 149 ++ .../prototypes/test_master_contract.py | 269 ++ tests/interface/prototypes/test_serve.py | 167 ++ .../prototypes/test_showcase_catalog.py | 176 ++ .../prototypes/test_showcase_homepage.py | 446 ++++ .../prototypes/test_structure_contract.py | 219 ++ .../prototypes/test_task_detail_contract.py | 193 ++ 95 files changed, 23402 insertions(+), 74 deletions(-) create mode 100644 .ai/docs/specs/bkflow-engine-admin-interaction-audit-2026-04-20.md create mode 100644 docs/plans/2026-03-23-prototype-toolkit.md create mode 100644 docs/plans/2026-04-10-prototype-bk-standard.md create mode 100644 docs/plans/2026-04-21-bkflow-engine-admin-prototype-overhaul.md create mode 100644 docs/plans/2026-04-22-prototype-showcase-homepage.md create mode 100644 docs/specs/2026-03-23-prototype-toolkit-design.md create mode 100644 docs/specs/2026-04-10-prototype-bk-standard-design.md create mode 100644 docs/specs/2026-04-21-bkflow-engine-admin-prototype-overhaul-design.md create mode 100644 docs/specs/2026-04-22-prototype-showcase-homepage-design.md create mode 100644 prototypes/README.md create mode 100644 prototypes/assets/bkflow-prototype.css create mode 100644 prototypes/assets/bkflow-prototype.js create mode 100644 prototypes/assets/bkflow-showcase.css create mode 100644 prototypes/assets/bkflow-showcase.js create mode 100644 prototypes/assets/icons/bkflow-logo.svg create mode 100644 prototypes/examples/README.md create mode 100644 prototypes/examples/component-showcase.html create mode 100644 prototypes/features/README.md create mode 100644 prototypes/features/_legacy/README.md create mode 100644 prototypes/features/_legacy/node-output-viewer/README.md create mode 100644 prototypes/features/_legacy/node-output-viewer/index.html create mode 100644 prototypes/features/_legacy/sops-open-plugin/README.md create mode 100644 prototypes/features/_legacy/sops-open-plugin/index.html create mode 100644 prototypes/features/_legacy/sops-open-plugin/main-flow.html create mode 100644 prototypes/features/_legacy/sops-open-plugin/space-open-plugin-management.html create mode 100644 prototypes/features/_legacy/sops-open-plugin/task-plugin-error-state.html create mode 100644 prototypes/features/_legacy/sops-open-plugin/template-plugin-selection.html create mode 100644 prototypes/features/_legacy/space-variable-manage/README.md create mode 100644 prototypes/features/_legacy/space-variable-manage/index.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/CHANGELOG.md create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/README.md create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/feature.meta.json create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/index.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/plugin/plugin-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/credential-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/debug-task-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-editor.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-debug.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-edit.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-view.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/label-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/space-config.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/statistics-exception.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-complete.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-failed.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/template-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/module-config-list.html create mode 100644 prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/space-config-list.html create mode 100644 prototypes/masters/README.md create mode 100644 prototypes/masters/_shared/README.md create mode 100644 prototypes/masters/_shared/plugin-shell.html create mode 100644 prototypes/masters/_shared/space-shell.html create mode 100644 prototypes/masters/_shared/system-shell.html create mode 100644 prototypes/masters/config-page/README.md create mode 100644 prototypes/masters/config-page/states/dirty.html create mode 100644 prototypes/masters/config-page/template.html create mode 100644 prototypes/masters/decision-editor/README.md create mode 100644 prototypes/masters/decision-editor/template.html create mode 100644 prototypes/masters/engine-panel/README.md create mode 100644 prototypes/masters/engine-panel/template.html create mode 100644 prototypes/masters/flow-editor/README.md create mode 100644 prototypes/masters/flow-editor/states/mock-debug-confirm.html create mode 100644 prototypes/masters/flow-editor/states/node-selected.html create mode 100644 prototypes/masters/flow-editor/states/publish-confirm.html create mode 100644 prototypes/masters/flow-editor/states/save-ready.html create mode 100644 prototypes/masters/flow-editor/template.html create mode 100644 prototypes/masters/list-page/README.md create mode 100644 prototypes/masters/list-page/states/bulk-actions.html create mode 100644 prototypes/masters/list-page/states/empty.html create mode 100644 prototypes/masters/list-page/states/error.html create mode 100644 prototypes/masters/list-page/template.html create mode 100644 prototypes/masters/overlays/README.md create mode 100644 prototypes/masters/overlays/dialogs.html create mode 100644 prototypes/masters/overlays/sidesliders.html create mode 100644 prototypes/masters/task-detail/README.md create mode 100644 prototypes/masters/task-detail/states/complete.html create mode 100644 prototypes/masters/task-detail/states/empty-log.html create mode 100644 prototypes/masters/task-detail/states/failed.html create mode 100644 prototypes/masters/task-detail/template.html create mode 100644 prototypes/serve.py create mode 100644 prototypes/showcase_catalog.py create mode 100644 tests/interface/prototypes/__init__.py create mode 100644 tests/interface/prototypes/test_bkflow_engine_admin_feature.py create mode 100644 tests/interface/prototypes/test_flow_editor_contract.py create mode 100644 tests/interface/prototypes/test_master_contract.py create mode 100644 tests/interface/prototypes/test_serve.py create mode 100644 tests/interface/prototypes/test_showcase_catalog.py create mode 100644 tests/interface/prototypes/test_showcase_homepage.py create mode 100644 tests/interface/prototypes/test_structure_contract.py create mode 100644 tests/interface/prototypes/test_task_detail_contract.py diff --git a/.ai/docs/specs/bkflow-engine-admin-interaction-audit-2026-04-20.md b/.ai/docs/specs/bkflow-engine-admin-interaction-audit-2026-04-20.md new file mode 100644 index 0000000000..afce1971e8 --- /dev/null +++ b/.ai/docs/specs/bkflow-engine-admin-interaction-audit-2026-04-20.md @@ -0,0 +1,612 @@ +# BKFlow Engine Admin 交互审计 + +日期:2026-04-20 + +环境:`内部测试环境(已脱敏)` + +范围:`bkflow_engine_admin` 入口下可见的 `空间管理 / 系统管理 / 我的插件` 交互;重点记录导航方式、页面容器形态、主操作反馈、危险操作确认、空态与异常态,并补充一次真实流程的“创建 -> 编辑配置 -> 发布 -> 调试执行”深链路。 + +证据目录:`本地采集截图(未纳入版本库)` + +## 1. 审计结论 + +这个入口不是单一的“列表页壳子”,而是三类交互容器混用: + +1. 原地弹窗/确认框 +2. 右侧滑出面板 +3. 整页跳转或跨模块跳转 + +现有 `prototypes/base.html` 更接近“固定导航 + 简单列表页”的静态外壳,但真实页面里至少还存在以下关键差异: + +1. 顶栏一级导航会切换整套路由和左侧菜单,而不是只切内容区。 +2. `空间管理` 的二级菜单主要依赖 `activeTab` 查询参数切换。 +3. 主按钮的行为并不统一。 + `流程 -> 创建流程` 是弹窗。 + `任务/调试任务 -> 引擎操作` 是跨到 `/enginePanel/...`。 + `决策表 -> 新建` 是跳到独立编辑页 `/admin_decision/...`。 +4. 列表页并不只是“表格 + 操作列”。 + 真实页面大量使用固定操作列、空态、状态标签、链接跳转、确认弹窗、只读内容弹窗、代码编辑器和侧滑表单。 +5. 配置页不是普通表单。 + `空间配置` 是大段代码编辑器 + 若干枚举/文本控件,`保存` 默认禁用,`恢复默认值` 有危险确认。 + +## 2. 全局导航与路由 + +### 顶栏一级导航 + +| 一级导航 | 真实路由 | 左侧菜单形态 | 备注 | +| --- | --- | --- | --- | +| 空间管理 | `/bkflow_engine_admin/?space_id=171...` | 8 个二级菜单 | 以 `activeTab` 切页 | +| 系统管理 | `/bkflow_engine_admin/system/` | `空间配置 / 模块配置 / 运营统计` | 独立 path,不再依赖 `space_id` | +| 我的插件 | `/bkflow_engine_admin/plugin/` | 仅 `蓝鲸插件` | 独立 path | + +### 空间管理二级菜单 + +| 菜单 | 路由形态 | 页面容器主形态 | +| --- | --- | --- | +| 流程 | `/bkflow_engine_admin/?space_id=171` | 列表 + 弹窗 + 侧滑 + 确认框 | +| 任务 | `...?activeTab=task` | 空列表 + 跨模块跳转 | +| 调试任务 | `...?activeTab=mockTask` | 列表 + 详情跳转 + 删除确认 | +| 决策表 | `...?activeTab=decisionTable` | 空列表 + 独立编辑页 | +| 空间配置 | `...?activeTab=config` | 大配置表单 | +| 凭证管理 | `...?activeTab=credential` | 列表 + 新建侧滑 + 查看内容弹窗 | +| 运营统计 | `...?activeTab=statistics` | 当前环境中主体未正常渲染 | +| 标签管理 | `...?activeTab=labelManage` | 列表 + 新增弹窗 | + +### 三种典型跳转 + +| 触发动作 | 目标路由 | 交互含义 | +| --- | --- | --- | +| `任务/调试任务 -> 引擎操作` | `/enginePanel/171/?type=task|mockTask` | 跨模块但保留同一产品壳 | +| `调试任务 -> 任务名称` | `/taskflow/execute/171/?instanceId=&type=mock` | 跳到调试任务执行详情 | +| `决策表 -> 新建` | `/admin_decision/?space_id=171` | 跳到独立编辑器 | + +## 3. 页面逐项记录 + +### 3.1 空间管理 / 流程 + +路由:`/bkflow_engine_admin/?space_id=171` + +证据: + +- `00-space-template-home.png` +- `01-space-template-create-dialog.png` +- `02-space-template-create-validation.png` +- `03-space-template-create-task-sideslider.png` +- `05-space-template-copy-confirm.png` +- `06-space-template-delete-confirm.png` + +观察: + +| 项目 | 细节 | +| --- | --- | +| 顶部主按钮 | `创建流程` | +| 搜索/筛选 | 单搜索框,placeholder 为 `ID/流程名称/标签/创建人/更新人/启用/所属作用域类型/所属作用域值` | +| 表格 | 固定操作列;行操作包含 `新建任务 / 复制 / 删除` | +| 分页 | 底部分页,显示总数、每页条数、页码 | +| 行标题 | 流程名称可点击 | + +关键交互: + +1. `创建流程` + 打开居中弹窗,不跳页。 + 字段包含 `流程名称`、`所属作用域类型`、`所属作用域值`、`标签`。 + 底部按钮为 `提交 / 取消`。 +2. 空提交校验 + `流程名称` 必填,空提交后出现内联错误态。 +3. `新建任务` + 打开右侧侧滑。 + 默认自动带出任务名称,例如 `<流程名>_YYYYMMDDHHMMSS`。 + 支持 `表单模式 / json模式` 切换。 + 底部按钮为 `提交 / 取消`。 +4. `复制` + 不是直接执行。 + 先弹确认框,文案包含“是否复制该流程?” + 附加提示:关联 mock 数据不会同步复制,带决策表节点的流程暂不支持复制。 + 还有一个额外勾选项 `复制包含的子流程`。 +5. `删除` + 弹确认框,文案形如 `确认删除流程""?`。 + +对原型优化的启发: + +1. `流程` 页必须支持“同页多层容器”。 + 主列表、创建弹窗、新建任务侧滑、复制确认、删除确认是并存的。 +2. 现有基座里的“创建流程 = 跳页”或“所有主操作都走同一种浮层”都不够贴近真实页面。 + +#### 深链路补充:真实创建一条流程并进入配置页 + +审计对象: + +- 流程 ID:`2166` +- 流程名称:`audit-deep-dive-20260420` + +深链路证据: + +- `90-template-edit-node-selected.png` +- `91-template-node-config-panel.png` +- `92-template-global-vars-popup.png` +- `93-template-save-enabled-after-node-confirm.png` +- `94-template-debug-save-confirm.png` +- `95-template-debug-execute-error-modal.png` + +实际链路: + +1. 在流程列表点击 `创建流程`,仅填写流程名称 `audit-deep-dive-20260420` 即可创建,创建成功后不跳页。 +2. 列表立即回到第一页顶部,新增行直接插入最上方,列表总数由 `57` 增为 `58`。 +3. 点击流程名称跳转到只读页 `/template/view/2166/`,顶部标题为 `查看流程`,此时展示版本区和 `编辑` 按钮。 +4. 点击 `编辑` 后进入 `/template/edit/2166/`,页面标题切为 `编辑流程`,顶部操作区出现 `保存 / 调试 / 发布`。 + +编辑页的关键交互: + +1. 单击画布节点,只会选中节点并显示节点周边的悬浮工具条,不会立刻打开配置表单。 +2. 双击节点后,右侧才会打开 `节点配置` 抽屉。 +3. `节点配置` 不是简化表单,而是按 `基础信息 / 输入参数 / 输出参数` 分组;同屏包含: + `标准插件` 重选 + `插件版本` + `节点名称` + `失败处理` 多选能力位(`AS / MS / MR / AR`) + `超时控制` 开关 + 输入参数编辑框 + 输出参数表格与“勾选为全局变量” +4. 抽屉顶部还有 `全局变量` 入口,但它不是切整页,而是展开一个浮层式全局变量表格;已内置 `_system.task_name / _system.task_id / _system.task_start_time / _system.operator` 等系统变量。 +5. 节点抽屉底部存在一层局部提交。 + 先点 `确定`,只是把节点配置写回画布。 + 此时顶部全局 `保存` 才会从禁用变为可点。 +6. 顶部再点一次 `保存`,才真正保存整个流程定义。 + 保存后顶部 `保存` 会重新变灰,`发布` 才从禁用切为可点。 + +发布交互: + +1. 点击 `发布` 不会直接提交,而是弹出 `发布流程` 对话框。 +2. 对话框内带默认 `版本号`,实际观测到默认值为 `1.0.0`,并提供 `版本描述` 输入框。 +3. 发布成功后页面回到 `/template/view/2166/`,标题回到 `查看流程`,顶部出现版本徽标 `1.0.0` 与 `最新` 标识。 + +调试执行交互: + +1. 编辑页点击 `调试` 会跳到独立调试路由 `/template/mock/true/2166/`,不是在当前编辑器内切一个 Tab。 +2. 调试页顶部按钮为 `退出调试 / 保存 / 执行调试`,节点点击后会打开 `设置 Mock 数据` 抽屉。 +3. 该抽屉包含 `Mock数据 / 插件配置 / 流程控制选项` 三个分组;`Mock数据` 分组支持 `新增数据方案`,新增后会生成默认方案卡片 `Mock 数据方案1`。 +4. 点击顶部 `执行调试` 时不会直接运行,而是先弹确认框:`确定保存Mock数据并去执行调试?` +5. 确认后会新开一个标签页 `/template/mock/execute/true/2166/`,而不是覆盖当前调试页。 +6. 新标签页标题为 `调试执行`,表单区包含: + 自动生成的调试任务名称,实际观测值为 `audit-deep-dive-20260420_调试任务_20260420203115` + `填写调试入参` 区 + `历史调试` 侧栏 +7. 当前环境下,在 `调试执行` 页点击 `执行` 后没有进入成功态,而是弹出全局异常弹层:`应用出现异常`。 + 这说明执行链路在该环境中至少存在一条异常路径,原型不能只表达“成功执行”单一状态。 + +### 3.2 空间管理 / 任务 + +路由:`/bkflow_engine_admin/?space_id=171&activeTab=task` + +证据: + +- `10-space-task-empty-state.png` +- `11-engine-panel-task-request-form.png` + +观察: + +| 项目 | 细节 | +| --- | --- | +| 默认态 | 列表空态 | +| 顶部主按钮 | `引擎操作` | +| 搜索/筛选 | placeholder 为 `ID/任务名称/标签/创建人/执行人/所属模板 ID/所属作用域类型/所属作用域值` | +| 表格 | 当前空间此页无数据,展示 `暂无数据` | + +关键交互: + +1. `引擎操作` + 不开弹窗。 + 直接跳到 `/enginePanel/171/?type=task`。 +2. `/enginePanel/171/?type=task` + 页面主体是“请求 / 响应”双栏式接口调试表单。 + 包含 `引擎版本`、`请求资源`、`请求方法`、`Query`、`Body`。 + `发送请求` 默认禁用,`重置` 可点。 + +对原型优化的启发: + +1. `任务` 页不是简单列表详情页。 + 主按钮会跨到另一套请求调试页面。 +2. 基座需要支持“列表页跳到接口调试页”的页面切换模型。 + +### 3.3 空间管理 / 调试任务 + +路由:`/bkflow_engine_admin/?space_id=171&activeTab=mockTask` + +证据: + +- `12-space-mocktask-list.png` +- `13-space-mocktask-delete-confirm.png` +- `14-engine-panel-mocktask-request-form.png` + +观察: + +| 项目 | 细节 | +| --- | --- | +| 数据态 | 有真实列表数据 | +| 状态展示 | 使用状态图标 + 文案,如 `完成 / 失败 / 未执行` | +| 任务名称 | 链接跳转到 `/taskflow/execute/171/?instanceId=&type=mock` | +| 行操作 | 仅 `删除` | +| 分页 | 有总数、每页条数、页码 | + +关键交互: + +1. `删除` + 弹确认框,文案形如 `确认删除任务""?`。 +2. `引擎操作` + 跳到 `/enginePanel/171/?type=mockTask`。 + 与普通 `任务` 复用同一套请求页,只是 `type` 不同。 + +对原型优化的启发: + +1. `任务` 和 `调试任务` 在视觉结构上相似,但数据态和跳转目标不同。 +2. 状态列需要支持图标 + 颜色,不适合只用纯文本标签占位。 + +#### 深链路补充:调试任务执行详情页 + +审计对象: + +- 任务 ID:`1192795` +- 任务名称:`audit-deep-dive-20260420_调试任务_20260420203821` +- 路由:`/taskflow/execute/171/?instanceId=1192795&type=mock` + +证据: + +- `96-task-execute-detail-node-detail.png` +- `97-task-execute-detail-call-log.png` +- `98-task-execute-detail-config-snapshot.png` + +实际观察: + +1. 从 `调试任务` 列表点击任务名称,进入的不是流程模板页,而是独立的执行详情页,浏览器标题直接变成任务名称。 +2. 页头信息结构与流程编辑页明显不同: + 左侧是 `任务执行` + 任务名称 + 运行状态 `完成` + 右侧没有 `保存 / 调试 / 发布` + 但提供一个回链图标,可跳回源模板 `/template/view/2166/` +3. 中央仍是流程画布,但语义已从“可编排”切成“可观察”。 + 流程编辑页里,单击节点只会选中,双击才打开配置抽屉。 + 任务详情页里,单击节点就直接打开 `节点详情` 面板。 +4. 任务详情页点击节点后,没有出现编辑页里的节点悬浮工具条,也没有任何可修改控件;节点详情面板默认是只读分析态。 +5. 节点详情面板顶部会列出当前任务运行过的节点轨迹,例如 `开始节点 / 消息展示 / 结束节点`,选中节点后会显示该节点本次运行状态。 + +节点详情页签: + +1. `执行记录` + 展示 `异常信息 / 执行信息 / 输入参数 / 输出参数`。 + 实测这条任务显示: + `暂无异常` + 开始/结束时间 + 耗时 `小于 1 秒` + 输入参数 `展示内容 = audit message 20260420` + 输出参数 `_result / _loop / _inner_loop` +2. `配置快照` + 展示的是任务执行当时的节点配置快照,而不是当前模板最新可编辑配置。 + 内容包括 `标准插件 / 插件版本 / 节点名称 / 是否可选 / 失败处理 / 输入参数 / 输出参数`。 + 这说明执行详情页承担“事后审计”职责,不是“继续编辑”职责。 +3. `操作历史` + 当前这条任务对应节点下为空表,显示 `暂无数据`。 + 说明该页需要兼容“有分页表格”和“空表格”两种运行态。 +4. `调用日志` + 使用代码查看器样式展示运行日志,包含行号和较长的 pipeline trace 文本。 + 这里不是简单文本块,存在日志浏览器式的阅读体验。 + +与流程编辑页的直接差异: + +1. 流程编辑页是“配置中心”,任务详情页是“执行取证中心”。 +2. 流程编辑页顶部核心动作是 `保存 / 调试 / 发布`;任务详情页顶部核心信息是 `任务名 + 状态 + 模板回链`。 +3. 流程编辑页节点面板强调“基础信息、输入参数、输出参数的编辑”;任务详情页节点面板强调“执行记录、配置快照、操作历史、调用日志的查看”。 +4. 流程编辑页需要双击节点进入编辑;任务详情页单击节点即可查看详情。 +5. 流程编辑页里配置变化会驱动按钮状态切换;任务详情页没有这类保存状态机,而是运行信息与只读日志。 + +### 3.4 空间管理 / 决策表 + +路由: + +- 列表:`/bkflow_engine_admin/?space_id=171&activeTab=decisionTable` +- 新建页:`/admin_decision/?space_id=171` + +证据: + +- `20-space-decision-empty-state.png` +- `21-decision-editor-new.png` + +观察: + +| 项目 | 细节 | +| --- | --- | +| 列表态 | 当前空间列表为空 | +| 主按钮 | `新建` | +| 空态 | `暂无数据` | + +关键交互: + +1. `新建` + 直接跳到独立编辑页 `/admin_decision/?space_id=171`。 +2. 新建页结构 + 顶部有返回箭头和标题 `新建决策表`。 + 分为 `基础信息` 与 `规则配置` 两块。 + 基础信息包含 `名称`、`关联流程`、`描述`。 + 规则配置支持 `条件字段 / 结果字段`,并有空态提示。 + 底部按钮为 `提交 / 取消 / 调试`。 +3. `取消` + 返回决策表列表页。 + +对原型优化的启发: + +1. 决策表应建模为“列表页 + 专用编辑器”,而不是列表页上的简单弹层。 + +### 3.5 空间管理 / 空间配置 + +路由:`/bkflow_engine_admin/?space_id=171&activeTab=config` + +证据: + +- `30-space-config-code-form.png` +- `31-space-config-restore-confirm.png` + +观察: + +| 项目 | 细节 | +| --- | --- | +| 页面形态 | 单页长表单,无 Tab | +| 控件类型 | 文本输入、下拉选择、代码编辑器、JSON 文本框 | +| 编辑器 | 多个 Monaco 风格代码块,带折叠/展开、工具图标 | +| 按钮区 | `保存`、`恢复默认值` | +| 保存态 | 初始为禁用 | + +关键交互: + +1. `保存` + 默认禁用,推断需要在有改动后才可提交。 +2. `恢复默认值` + 弹危险确认框:`确认将所有配置项恢复为默认值?` +3. 真实字段密度很高。 + 例如 `API 插件配置`、`空间管理员`、`流程版本控制`、`Token过期时间`、`画布模式`、`网关表达式` 等。 + +对原型优化的启发: + +1. 基座需要支持代码编辑器占位,而不是普通 textarea。 +2. 配置页要体现“保存按钮默认禁用”和“恢复默认值危险确认”。 + +### 3.6 空间管理 / 凭证管理 + +路由:`/bkflow_engine_admin/?space_id=171&activeTab=credential` + +证据: + +- `40-space-credential-list.png` +- `41-space-credential-create-sideslider.png` +- `42-space-credential-view-content.png` + +观察: + +| 项目 | 细节 | +| --- | --- | +| 顶部主按钮 | `新建` | +| 列表 | 有真实数据 | +| 行操作 | `编辑 / 查看内容 / 删除` | +| 类型列 | 如 `蓝鲸应用认证`、`蓝鲸 Access Token 认证` | + +关键交互: + +1. `新建` + 打开右侧侧滑,不跳页。 + 表单包含 `名称`、`类型`、`描述`、`开放范围` 等。 + `类型` 不是 select,而是横向切换项。 + 当前可见类型有 `蓝鲸应用认证 / 蓝鲸 Access Token 认证 / Basic Auth / 自定义`。 +2. `查看内容` + 打开只读弹窗。 + 内容以代码块形式展示,敏感字段做掩码,例如 `"access_token": "*********"`. + +对原型优化的启发: + +1. 凭证页是很典型的“列表 + 创建侧滑 + 查看弹窗”的组合。 +2. 类型切换应做成明显的选项组,不应简化成普通下拉。 + +### 3.7 空间管理 / 运营统计 + +路由:`/bkflow_engine_admin/?space_id=171&activeTab=statistics` + +证据: + +- `50-space-statistics-blank.png` + +观察: + +1. 路由成功切换到 `activeTab=statistics`。 +2. 左侧菜单仍在,但主内容区在当前环境中几乎为空。 +3. 浏览器 console 里出现了来自外部可视化资源的日志,说明此页可能依赖另一套统计应用或延迟加载资源。 + +结论: + +1. 当前环境下,这一页应标记为“入口可见,但主体未稳定渲染”。 +2. 原型基座优化时不要把它简单当成普通列表页处理。 + +### 3.8 空间管理 / 标签管理 + +路由:`/bkflow_engine_admin/?space_id=171&activeTab=labelManage` + +证据: + +- `60-space-label-empty-list.png` +- `61-space-label-create-dialog.png` + +观察: + +1. 首次快照出现过空态,后续数据正常加载为 4 条,说明这里存在异步加载延迟。 +2. 列表字段包含 `标签名称`、`标签描述`、`标签范围`、`标签引用`、`系统默认标签`。 +3. 支持层级标签,名称可包含 `/`,例如 `新标签/第三个二级标签`。 + +关键交互: + +1. `新增标签` + 打开弹窗。 + 字段包含 `标签名称`、`标签颜色`、`标签描述`、`标签范围`。 + 范围使用多选式切换,当前可选 `任务 / 流程`。 +2. 行操作 + 包含 `编辑 / 删除`。 + +对原型优化的启发: + +1. 标签管理不是纯空态页,真实页面会展示树形/层级标签语义。 +2. 需要支持颜色选择器占位。 + +### 3.9 系统管理 / 空间配置 + +路由:`/bkflow_engine_admin/system/` + +证据: + +- `70-system-space-list.png` + +观察: + +1. 顶栏切到 `系统管理` 后,左侧菜单变为 `空间配置 / 模块配置 / 运营统计`。 +2. 当前默认页是平台级 `空间配置` 列表,不再绑定具体 `space_id`。 +3. 顶部有 `新增`。 +4. 行操作只有 `编辑`。 +5. 列表字段偏平台管理属性,如 `空间ID`、`空间名称`、`应用ID`、`平台提供服务的地址`、`空间创建的方式`。 + +对原型优化的启发: + +1. 一级导航切换后,侧栏信息架构要整体变化,而不是局部替换。 + +### 3.10 系统管理 / 模块配置 + +路由:`/bkflow_engine_admin/system/?activeTab=module` + +证据: + +- `71-system-module-list.png` + +观察: + +1. 仍是列表页,顶部有 `新增`。 +2. 行操作为 `编辑 / 删除`。 +3. 字段集中在模块注册信息,如 `模块code`、`模块提供的地址`、`模块的token`、`模块类型`、`隔离类型`。 + +对原型优化的启发: + +1. 系统管理下至少有两种列表页,但信息密度和列宽分配不同于空间管理。 + +### 3.11 我的插件 / 蓝鲸插件 + +路由:`/bkflow_engine_admin/plugin/` + +证据: + +- `80-plugin-list.png` +- `81-plugin-authorize-dialog.png` + +观察: + +1. 左侧只有一个菜单 `蓝鲸插件`。 +2. 顶部没有“新增”,只有搜索。 +3. 列表核心字段包括 `插件名称`、`code`、`管理员`、`授权状态`、`使用范围`、`授权状态修改时间`、`授权状态修改人`。 +4. 行操作根据授权状态变化: + 已授权:`编辑使用范围 / 取消授权` + 未授权:`编辑使用范围 / 授权` + +关键交互: + +1. `授权` + 弹确认框,标题为 `确认授权?` + 文案会说明授权后的使用含义及执行人信息真实性风险。 +2. `编辑使用范围` + 可见但本次未深入执行,建议后续原型优化时保留为独立弹层能力。 + +对原型优化的启发: + +1. 这个页面的“操作按钮随状态变化”是重要差异,不能做成固定按钮文案。 + +## 4. 真实页面里的交互模式归纳 + +### 4.1 容器模式 + +| 模式 | 真实示例 | +| --- | --- | +| 居中弹窗 | 创建流程、复制确认、删除确认、标签新增、插件授权、凭证查看内容 | +| 右侧侧滑 | 流程新建任务、凭证新建 | +| 独立编辑页 | 决策表新建页 | +| 画布编排页 | `/template/view/:id`、`/template/edit/:id` | +| 跨模块请求页 | `enginePanel` | +| 调试执行页 | `/template/mock/true/:id`、`/template/mock/execute/true/:id` | +| 详情执行页 | `taskflow/execute/...` | + +### 4.2 统一规律 + +1. 危险操作基本都有二次确认。 +2. 主操作反馈并不统一,必须按业务页定制。 +3. 真实页面普遍依赖路由表达状态。 + `activeTab` + `/system/` + `/plugin/` + `/enginePanel/...` + `/admin_decision/...` + `/template/view/...` + `/template/edit/...` + `/template/mock/...` +4. 空态在多个页面真实存在。 + 任务 + 决策表 + 统计异常页 + 标签管理首屏异步空态 +5. 流程编排页存在明显的“两段式保存”。 + 节点抽屉先 `确定` + 顶部再 `保存` + 最后才允许 `发布` +6. 调试执行不是当前页内就地完成。 + 先保存 Mock 配置 + 再确认 + 再新开标签页执行 +7. 任务详情页和流程编辑页虽然都复用了流程画布,但交互语义完全不同。 + 编辑页是“编排” + 执行页是“观察” + 同一套画布不能只换标题就复用 + +## 5. 直接指导原型工具优化的差距清单 + +1. `prototypes/base.html` 目前的导航是静态内容切换,缺少真实页面里的“一级导航切路由、二级菜单切 activeTab”的表达。 +2. 现有基座缺少对“同一页面里弹窗、侧滑、确认框并存”的结构支持。 +3. 真实页面大量出现固定操作列、状态标签、空态、分页、行跳转,基座里目前表达偏弱。 +4. 配置页需要代码编辑器、禁用态保存按钮、恢复默认值确认,不能只用普通输入框替代。 +5. 决策表、引擎操作、任务执行详情都不是普通页面切换,应在原型工具中抽成独立页面模版。 +6. 插件授权、凭证查看内容、标签新增这类“轻量业务弹窗”值得做成可复用模版。 +7. 流程编排页需要新增一种高复杂度模版。 + 中央画布 + 节点选中悬浮工具条 + 右侧配置抽屉 + 顶部全局操作条 +8. 原型基座目前缺少“局部确认改变全局保存态”的表达。 + 真实页面里节点抽屉的 `确定` 不等于全局保存,必须单独建模。 +9. 真实调试执行页包含“保存 Mock 数据确认 -> 新开标签页 -> 执行结果/异常”链路。 + 这比普通提交弹窗复杂得多,不能压平为一个 `Run` 按钮。 +10. 任务详情页不能套用流程编辑页原型。 + 它需要专门的“执行详情模版”: + 页头状态区 + 模板回链 + 只读节点详情 + 执行记录/配置快照/操作历史/调用日志四类内容区 + +## 6. 后续优化建议 + +建议后续从 `master` 切一个 worktree 后,先按下面顺序改原型基座: + +1. 先重做全局导航骨架。 + 支持 `空间管理 / 系统管理 / 我的插件` 三套导航语义。 +2. 再补三类页面模版。 + 列表页 + 配置页 + 独立编辑页 +3. 再补三类浮层模版。 + 确认弹窗 + 业务表单弹窗 + 右侧侧滑 +4. 最后再把 `流程 / 调试任务 / 空间配置 / 凭证管理 / 插件` 这些高频页映射到新基座上。 + +## 7. 备注 + +1. 本次补充审计实际创建并发布了一条专用流程 `audit-deep-dive-20260420`(ID `2166`),用于记录真实编排页和调试链路;未对其他现有业务数据做破坏性修改。 +2. 所有危险操作仍然停在确认层或审计专用对象范围内,未执行删除、授权、恢复默认值等影响现有数据的最终提交动作。 +3. `运营统计` 在当前环境里未正常渲染,需在后续优化时单列处理。 +4. `标签管理` 首次快照出现空态,但后续列表成功渲染,说明该页存在异步加载时序差异。 +5. 新流程的调试执行链路已走到 `/template/mock/execute/true/2166/`,但当前环境点击 `执行` 后出现 `应用出现异常` 弹层,因此本次记录到了异常路径,没有记录到成功执行结果页。 diff --git a/.ai/skills/prototype-generator/SKILL.md b/.ai/skills/prototype-generator/SKILL.md index 76d0375409..ad05b205b1 100644 --- a/.ai/skills/prototype-generator/SKILL.md +++ b/.ai/skills/prototype-generator/SKILL.md @@ -21,65 +21,71 @@ description: 独立的原型生成工具。用自然语言描述需求,AI 澄 ## 完整工作流 -1. **启动预览服务器** - - 检查是否已有 `prototypes/serve.py` 在运行(可查看 Cursor 的 **terminals** 状态或本机进程是否监听默认端口 **9080**)。 +1. **启动预览服务器** + - 检查是否已有 `prototypes/serve.py` 在运行(可查看 Cursor 的 **terminals** 状态或本机进程是否监听默认端口 **9080**)。 - 若未运行,在仓库根下执行: ```bash cd prototypes && python serve.py & ``` - 默认地址:`http://localhost:9080/`(根目录即 `prototypes/`)。 -2. **检查 `base.html`** - - 读取 `prototypes/base.html`。 - - **若不存在**:进入「首次使用引导」(见下文),向用户确认后生成 `base.html`,保存到 `prototypes/base.html`,并让用户在浏览器中打开 `http://localhost:9080/base.html` 预览确认。 - -3. **需求澄清**(生成或大幅改版前必须完成,勿跳过) - 逐项提问并记录答案,例如: - - 这个页面要展示什么信息? - - 有哪些数据字段?(表格列 / 表单字段) - - 需要哪些操作?(新建、编辑、删除、启用/停用等) - - 交互方式?(搜索、筛选、侧滑编辑、弹窗确认、Tab 切换等) - - 多个页面之间是否有关联、如何跳转? +2. **确认展厅首页合同** + - 根路径 `/` 是中文「BKFlow 原型展厅」首页。 + - 首页展示来源是 `prototypes/features//feature.meta.json`,而不是简单目录索引。 + - 读取现有 feature 的 `README.md` 和 `feature.meta.json`,理解其首页卡片、代表页与排序约定。 + +3. **参考蓝鲸设计规范(设计输入,不是运行时依赖)** + - 生成或大幅调整原型前,优先把 `bkui-knowledge` 作为设计参考源:如果当前会话能访问 `bkui-knowledge` MCP / connector,先读取相关页面模式、组件 API、交互规范和示例。 + - 若无法访问 `bkui-knowledge`,退回读取本仓库的 `prototypes/examples/component-showcase.html`、相关 `prototypes/masters/` 母版,以及真实前端里相近页面的布局模式。 + - 参考结果只用于决定页面结构、组件选择、交互模式、间距层级和状态表达;最终产物仍必须落在 `prototypes/features//` 或 `prototypes/masters/`,使用 `assets/` 中的静态 CSS/JS。 + - 不要因为参考 `bkui-knowledge` 而引入 Vue / React / bkui-vue CDN / npm 构建链路,也不要恢复 `prototypes/output/`、`prototypes/metadata/`、`prototypes/cache/` 作为版本库产物。 + - 若发现当前 `assets/` 或 `masters/` 与蓝鲸标准差距明显,优先沉淀成共享样式或母版能力,再让 feature 页面复用。 + +4. **需求澄清**(生成或大幅改版前必须完成,勿跳过) + 逐项提问并记录答案,例如: + - 这个页面要展示什么信息? + - 有哪些数据字段?(表格列 / 表单字段) + - 需要哪些操作?(新建、编辑、删除、启用/停用等) + - 交互方式?(搜索、筛选、侧滑编辑、弹窗确认、Tab 切换等) + - 多个页面之间是否有关联、如何跳转? - (可选)后端是否已有 Model / Serializer?若用户**主动提供**文件路径,可按「后端模型读取」一节做字段提取;否则仅凭描述推断。 -4. **生成原型** - - 先阅读 `prototypes/examples/component-showcase.html`(组件与 class 参考)和当前 `prototypes/base.html`(外壳)。 - - 复制 `base.html` 结构,在 `
` … `
` 内填充页面主体;**不要**把侧滑、弹窗等浮层塞进 `main` 内部(见「生成规则」)。 - - 将完整 HTML 保存到 **`prototypes/output/.html`**。 - - **路径修正**:`base.html` 位于 `prototypes/` 根目录,资源用 `assets/...`;`output/` 下页面多一层目录,**必须**改为 `../assets/bkflow-prototype.css`、`../assets/bkflow-prototype.js` 及 `../assets/icons/...` 等。 - -5. **告知预览** - - 告诉用户打开: - `http://localhost:9080/output/.html` - -6. **迭代** - - 根据反馈修改;新版本命名为 `-v2.html`、`-v3.html` … 保留旧文件便于对比(与「版本管理」规则一致)。 - -## 生成规则(MUST) - -- **先读** `prototypes/examples/component-showcase.html`,了解可用样式与结构模式。 -- **先读** `prototypes/base.html`,保持导航、布局与资源引用方式一致(再在 output 中做 `../assets/` 修正)。 -- **CSS class** 一律使用工具包约定的 **`bk-` 前缀**(见下方速查表)。 -- **交互** 仅通过 **`data-*` 属性** 声明,由 `assets/bkflow-prototype.js` 驱动;支持的属性与用法如下(实现以仓库内 JS 为准): - - | 属性 | 用法说明 | - |------|----------| - | `data-open=""` | 点击打开对应 `id` 的侧滑(`.bk-sideslider`)或弹窗(`.bk-dialog`);目标元素需带 `id` 且通过 `.is-show` 控制显示(脚本会添加该类)。 | - | `data-close` | 点击后关闭最近的侧滑/弹窗;点击 `.bk-sideslider-mask` / `.bk-dialog-mask` 也会关闭。 | - | `data-tab=""` | 放在 `.bk-tab` 内的 `.bk-tab-item` 上;切换对应 `id` 的 `.bk-tab-panel` 可见性,并切换 `active` 样式。 | - | `data-sortable` | 放在表头 `th` 上;点击按该列文本排序(中英文数字),并切换升序/降序指示类。 | - | `data-filter=""` | 放在输入框等元素上;`input` 时用输入值对 `id` 为 `tableId` 的表格 `tbody` 行做包含匹配过滤。 | - | `data-collapse=""` | 点击切换 `id` 对应元素的 `hidden` 状态。 | - | `data-dropdown=""` | 点击切换 `id` 对应下拉容器(如 `.bk-select-dropdown`)的 `is-show`。 | - | `data-href=""` | 点击后 `window.location.href` 跳转(如 `../output/other.html`)。 | - | `data-notify=""` | 点击后顶部绿色 Toast 提示,约 2 秒消失。 | - | `data-required` | 在表单控件上;提交 `.bk-form` 时若为空则给 `.bk-form-item` 加 `is-error` 并阻止提交。 | - | `data-pattern=""` | 同上,用正则校验控件值。 | - | `data-step=""` | 在 `.bk-steps-container` 内切换 `.bk-step-panel` 的显示(`hidden`),用于步骤条/向导式布局。 | - -- **Mock 数据** 应贴近真实业务语义(流程名、空间、任务状态等),避免「测试1」「aaa」「foo」等占位。 -- **版本文件**:`xxx.html` → `xxx-v2.html` → `xxx-v3.html`。 -- **浮层位置**:所有侧滑、弹窗等**必须**放在 `` 之后、``(闭合 `.bk-layout`)之前或按 `base.html` 同级结构闭合前,且**在** ` +""" + + +class PrototypeHandler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(ROOT), **kwargs) + + def do_GET(self): + if self.path == '/api/mtime': + self._handle_mtime() + elif self.path == '/': + self._handle_index() + else: + super().do_GET() + + def _handle_mtime(self): + mtime = 0 + for d in ['output', 'examples']: + dirpath = ROOT / d + if dirpath.exists(): + for f in dirpath.glob('*.html'): + mtime = max(mtime, f.stat().st_mtime) + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + self.wfile.write(json.dumps({'mtime': mtime}).encode()) + + def _handle_index(self): + html_parts = ['', + 'BKFlow Prototypes', + '', + '

BKFlow Prototypes

'] + + for section, dirname in [('Output', 'output'), ('Examples', 'examples')]: + dirpath = ROOT / dirname + files = sorted(dirpath.glob('*.html')) if dirpath.exists() else [] + if files: + html_parts.append(f'

{section}

') + for f in files: + rel = f'/{dirname}/{f.name}' + html_parts.append(f'{f.name}') + + if not (ROOT / 'output').exists() or not list((ROOT / 'output').glob('*.html')): + html_parts.append('

No prototypes yet. Use the prototype-generator skill to create one.

') + + html_parts.append('') + content = '\n'.join(html_parts).encode() + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(content) + + def end_headers(self): + if hasattr(self, '_headers_buffer'): + content_type = '' + for line in self._headers_buffer: + if b'Content-Type' in line and b'text/html' in line: + content_type = 'html' + break + super().end_headers() + + def send_response_only(self, code, message=None): + super().send_response_only(code, message) + + def copyfile(self, source, outputfile): + """Inject auto-reload script into HTML responses.""" + import io + content = source.read() + if b'' in content: + content = content.replace(b'', + AUTO_RELOAD_SCRIPT.encode() + b'') + outputfile.write(content) + + +def main(): + parser = argparse.ArgumentParser(description='BKFlow Prototype Preview Server') + parser.add_argument('--port', type=int, default=9080) + parser.add_argument('--host', default='0.0.0.0') + args = parser.parse_args() + + server = HTTPServer((args.host, args.port), PrototypeHandler) + print(f'\033[32m✓ Serving prototypes at http://localhost:{args.port}\033[0m') + print(f'\033[32m✓ Auto-reload enabled\033[0m') + print(f' Press Ctrl+C to stop') + try: + server.serve_forever() + except KeyboardInterrupt: + print('\nStopped.') + + +if __name__ == '__main__': + main() +``` + +- [ ] **Step 3: 验证服务器启动** + +```bash +cd prototypes && python serve.py & +sleep 2 +curl -s http://localhost:9080/ | head -5 +curl -s http://localhost:9080/api/mtime +kill %1 +``` + +Expected: 索引页 HTML 和 `{"mtime": 0}` JSON 响应。 + +- [ ] **Step 4: Commit** + +```bash +git add prototypes/ +git commit -m "feat(prototypes): 初始化原型工具包目录结构和预览服务器 --story=" +``` + +--- + +## Task 2: CSS 设计系统 — 变量 + 重置 + 基础样式 + +**Files:** +- Create: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 创建 CSS 文件,写入变量和重置样式** + +CSS 变量从 bkflow 前端 `frontend/src/scss/config.scss` 和 `app.scss` 提取。 + +内容包括: +- `:root` CSS 变量(主色调、文本、背景、边框、圆角、阴影、字体、布局) +- Box model reset(`*, *::before, *::after { box-sizing: border-box }`) +- Body 基础样式(字体、颜色、背景) +- 基础排版(h1-h6、p、a、code) +- 滚动条样式(匹配 bkflow 的 `mixins/scrollbar.scss`) + +所有值必须使用 `var()` 引用 CSS 变量,不允许写死。 + +写入 `prototypes/assets/bkflow-prototype.css`。 + +- [ ] **Step 2: 创建临时测试页面验证基础样式** + +创建 `prototypes/output/test-base.html`,包含基础排版元素(h1-h6、段落、链接、code),用浏览器打开验证字体和颜色是否与 bkflow 一致。 + +- [ ] **Step 3: 验证后删除测试页面,Commit** + +```bash +rm prototypes/output/test-base.html +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 设计系统 - 变量、重置和基础排版 --story=" +``` + +--- + +## Task 3: CSS 设计系统 — Button + Tag + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Button 组件样式** + +参考 bk-magic-vue 的 Button 组件,实现以下 class: +- `.bk-button` — 基础按钮(白底灰边框) +- `.bk-button-primary` — 主要按钮(蓝底白字) +- `.bk-button-success` — 成功按钮(绿底白字) +- `.bk-button-warning` — 警告按钮(黄底白字) +- `.bk-button-danger` — 危险按钮(红底白字) +- `.bk-button-text` — 文字按钮(无边框) +- `.bk-button-small` — 小尺寸 +- `.bk-button-large` — 大尺寸 +- `.bk-button-disabled` / `.bk-button[disabled]` — 禁用态 +- 包含 hover、active 状态 + +关键样式值:高度 32px、font-size 14px、padding 0 15px、border-radius `var(--bk-radius)`。 + +- [ ] **Step 2: 实现 Tag 组件样式** + +- `.bk-tag` — 基础标签 +- `.bk-tag-success` / `.bk-tag-warning` / `.bk-tag-danger` / `.bk-tag-info` — 状态标签 +- `.bk-tag-closable` — 可关闭标签(带 × 图标) + +- [ ] **Step 3: 创建测试页面验证,然后 Commit** + +创建临时 `prototypes/output/test-button-tag.html` 验证所有按钮和标签变体,确认后删除,commit。 + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Button 和 Tag --story=" +``` + +--- + +## Task 4: CSS 设计系统 — Input + Form/FormItem + Search + Switcher + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Input 组件** + +- `.bk-input` — 基础输入框(高度 32px,border `var(--bk-border-form)`) +- `.bk-input-large` / `.bk-input-small` — 尺寸变体 +- `.bk-input[disabled]` — 禁用态 +- `.bk-input:focus` — 聚焦态(border 变为 `var(--bk-primary)`) +- `.bk-textarea` — 多行文本框 +- `.bk-input-group` — 输入框组(前/后缀) + +- [ ] **Step 2: 实现 Form/FormItem 组件** + +- `.bk-form` — 表单容器 +- `.bk-form-item` — 表单项(包含 label + 控件 + 错误提示) +- `.bk-form-item .bk-form-label` — 标签(width 100px,右对齐) +- `.bk-form-item .bk-form-content` — 内容区(margin-left 120px) +- `.bk-form-item .bk-form-error` — 错误提示(红色 12px) +- `.bk-form-item.is-required .bk-form-label::before` — 必填星号 +- `.bk-form-item.is-error .bk-input` — 错误态输入框(红色边框) +- `.bk-form-vertical` — 纵向布局变体(label 在上方) + +- [ ] **Step 3: 实现 Search 组件** + +- `.bk-search-input` — 搜索输入框(带搜索图标占位) + +- [ ] **Step 4: 实现 Switcher 组件** + +- `.bk-switcher` — 开关(36px × 20px 圆角胶囊) +- `.bk-switcher.is-checked` — 开启态(蓝色背景) +- `.bk-switcher[disabled]` — 禁用态 + +- [ ] **Step 5: 验证并 Commit** + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Input, Form, Search, Switcher --story=" +``` + +--- + +## Task 5: CSS 设计系统 — Select + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Select 组件** + +- `.bk-select` — 下拉选择框容器 +- `.bk-select-trigger` — 触发器(看起来像 Input,右侧带下箭头) +- `.bk-select-dropdown` — 下拉面板(白底,shadow,默认 display:none) +- `.bk-select-dropdown.is-show` — 展开态 +- `.bk-select-option` — 选项行(hover 高亮) +- `.bk-select-option.is-selected` — 选中态(蓝色文字 + 勾选图标) +- `.bk-select-option.is-disabled` — 禁用态 +- `.bk-select[disabled]` — 整体禁用 +- `.bk-select .bk-select-placeholder` — placeholder 文字(灰色) + +- [ ] **Step 2: 验证并 Commit** + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Select --story=" +``` + +--- + +## Task 6: CSS 设计系统 — Table + Pagination + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Table 组件** + +- `.bk-table` — 表格容器(width 100%,border-collapse) +- `.bk-table thead` — 表头(背景 `#fafbfc`,字色 `var(--bk-text-secondary)`,font-size 12px) +- `.bk-table th` — 表头单元格(padding 12px 16px,border-bottom) +- `.bk-table td` — 数据单元格(padding 12px 16px,border-bottom `var(--bk-border-light)`) +- `.bk-table tr:hover td` — 行 hover(背景 `var(--bk-bg-hover)`) +- `.bk-table-empty` — 空状态行(居中灰色文字,高度 280px) +- `.bk-table .bk-table-sort` — 排序图标样式 +- `.bk-table .bk-table-action` — 操作列链接样式(蓝色文字) + +- [ ] **Step 2: 实现 Pagination 组件** + +- `.bk-pagination` — 分页容器(右对齐,padding 10px 20px) +- `.bk-pagination-item` — 页码(宽高 32px,居中) +- `.bk-pagination-item.active` — 当前页(蓝底白字) +- `.bk-pagination-item:hover` — hover 态 +- `.bk-pagination-prev` / `.bk-pagination-next` — 前/后翻页 +- `.bk-pagination-total` — 总数文字("共 X 条") + +- [ ] **Step 3: 验证并 Commit** + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Table 和 Pagination --story=" +``` + +--- + +## Task 7: CSS 设计系统 — Tab + Card + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Tab 组件** + +- `.bk-tab` — Tab 容器 +- `.bk-tab-header` — Tab 头部(底部 border) +- `.bk-tab-item` — Tab 项(padding 8px 16px,cursor pointer) +- `.bk-tab-item.active` — 激活态(蓝色文字 + 底部 2px 蓝色 border) +- `.bk-tab-item:hover` — hover 态 +- `.bk-tab-panel` — Tab 面板容器 +- `.bk-tab-panel[hidden]` — 隐藏的面板(display: none) + +- [ ] **Step 2: 实现 Card 组件** + +- `.bk-card` — 卡片容器(白底、border、border-radius、shadow) +- `.bk-card-header` — 卡片头部(底部 border) +- `.bk-card-body` — 卡片内容区(padding) +- `.bk-card-footer` — 卡片底部 + +- [ ] **Step 3: 验证并 Commit** + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Tab 和 Card --story=" +``` + +--- + +## Task 8: CSS 设计系统 — Dialog + Sideslider + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Dialog 组件** + +- `.bk-dialog` — 弹窗容器(fixed 全屏遮罩,默认 display:none) +- `.bk-dialog.is-show` — 显示态 +- `.bk-dialog-mask` — 半透明黑色遮罩 +- `.bk-dialog-content` — 弹窗内容(居中白色卡片,max-width 480px) +- `.bk-dialog-header` — 头部(标题 + 关闭按钮,padding 16px 24px,border-bottom) +- `.bk-dialog-body` — 主体内容(padding 24px) +- `.bk-dialog-footer` — 底部按钮区(padding 16px 24px,右对齐) + +- [ ] **Step 2: 实现 Sideslider 组件** + +- `.bk-sideslider` — 侧滑容器(fixed 全屏遮罩,默认 display:none) +- `.bk-sideslider.is-show` — 显示态 +- `.bk-sideslider-mask` — 半透明遮罩 +- `.bk-sideslider-content` — 侧滑面板(固定右侧,width 640px,白底,shadow) +- `.bk-sideslider-header` — 头部(深色背景 #333,白色文字,关闭按钮) +- `.bk-sideslider-body` — 主体内容(height calc(100% - header - footer),overflow-y auto) +- `.bk-sideslider-footer` — 底部按钮区(border-top,padding) +- `.bk-sideslider-close` — 关闭按钮 + +CSS transition:Dialog 淡入,Sideslider 从右侧滑入。 + +- [ ] **Step 3: 验证并 Commit** + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Dialog 和 Sideslider --story=" +``` + +--- + +## Task 9: CSS 设计系统 — Navigation + Layout + 辅助组件 + +**Files:** +- Modify: `prototypes/assets/bkflow-prototype.css` + +- [ ] **Step 1: 实现 Navigation 组件** + +参考 bkflow 的 `Navigation.vue`(深色侧边栏 `#1f2738`,顶部 header 48px)。 + +- `.bk-navigation` — 导航容器 +- `.bk-navigation-header` — 顶部区域(logo + 产品名,高度 `var(--bk-header-height)`) +- `.bk-navigation-menu` — 菜单列表 +- `.bk-navigation-menu-item` — 菜单项(padding,hover 高亮) +- `.bk-navigation-menu-item.active` — 当前激活项(左侧蓝色指示条) + +- [ ] **Step 2: 实现布局原语** + +- `.bk-layout` — 页面骨架(flex,min-width `var(--bk-min-width)`,height 100vh) +- `.bk-content` — 内容区(flex 1,overflow-y auto,padding 20px,background `var(--bk-bg-body)`) +- `.bk-toolbar` — 操作栏(flex,justify-content space-between,margin-bottom 16px) +- `.bk-aside` — 侧边面板(width 240px,border-right) +- `.bk-split` — 分栏布局(flex) +- `.bk-info-card` — KV 信息卡片 +- `.bk-steps-container` — 步骤容器 +- `.bk-canvas` — 画布区占位 + +- [ ] **Step 3: 实现辅助组件** + +- `.bk-breadcrumb` — 面包屑容器(flex,gap 8px) +- `.bk-breadcrumb-item` — 面包屑项 + 分隔符 +- `.bk-loading` — 加载状态(居中旋转动画) +- `.bk-exception` — 异常/空状态(居中图标 + 文字,高度 280px) +- `.bk-grid-row` / `.bk-grid-col` — 栅格行列(flex,支持 col-1 到 col-24) + +- [ ] **Step 4: 验证并 Commit** + +```bash +git add prototypes/assets/bkflow-prototype.css +git commit -m "feat(prototypes): CSS 组件 - Navigation, Layout 原语和辅助组件 --story=" +``` + +--- + +## Task 10: JS 声明式交互库 + +**Files:** +- Create: `prototypes/assets/bkflow-prototype.js` + +- [ ] **Step 1: 实现核心框架和 open/close 交互** + +初始化函数在 DOMContentLoaded 时扫描所有 `data-*` 属性并绑定事件。 + +实现: +- `data-open="target-id"` — 给目标元素添加 `is-show` class +- `data-close` — 从最近的 `.bk-dialog` 或 `.bk-sideslider` 祖先移除 `is-show` class +- 点击遮罩层(`.bk-dialog-mask` / `.bk-sideslider-mask`)关闭浮层 + +```javascript +(function() { + 'use strict'; + document.addEventListener('DOMContentLoaded', function() { + initOpen(); + initClose(); + initTabs(); + initSort(); + initFilter(); + initCollapse(); + initDropdown(); + initSteps(); + initHref(); + initNotify(); + initValidation(); + }); + + function initOpen() { /* data-open */ } + function initClose() { /* data-close + mask click */ } + // ... 下面各 step 实现 +})(); +``` + +- [ ] **Step 2: 实现 Tab 切换交互** + +- `data-tab="panel-id"` — 点击时: + 1. 同级 `.bk-tab-item` 移除 `active` class + 2. 当前元素添加 `active` class + 3. 同容器内所有 `.bk-tab-panel` 设为 `hidden` + 4. 目标 panel 移除 `hidden` + +- [ ] **Step 3: 实现表格排序交互** + +- `data-sortable` 在 `` 上 — 点击时: + 1. 获取该列索引 + 2. 读取 `` 所有行 + 3. 按该列文本内容排序(切换升序/降序) + 4. 重新插入 DOM + 5. 更新排序指示器样式 + +- [ ] **Step 4: 实现搜索过滤交互** + +- `data-filter="table-id"` 在 `` 上 — 输入时: + 1. 获取输入值(小写) + 2. 遍历目标表格的 `` 所有行 + 3. 行内文本不包含输入值的行设为 `display:none` + +- [ ] **Step 5: 实现折叠、下拉、步骤、跳转、通知交互** + +- `data-collapse="panel-id"` — toggle 目标的 `is-collapsed` class +- `data-dropdown="menu-id"` — toggle 目标的 `is-show` class + 点击其他区域关闭 +- `data-step="step-id"` — 切换步骤面板可见性 +- `data-href="page.html"` — `window.location.href` 跳转 +- `data-notify="消息"` — 创建一个临时通知元素,3 秒后自动移除 +- `data-required` 在 `` / ` + + +
输入框组 Input Group
+
+
+ https:// + +
+
+ + + +
+

4. 表单 Form

+ +
水平表单(默认)
+
+
+
+ +
+ +
用户名不能为空
+
+
+
+ +
+ +
+
+
+ +
+
+
+ 请选择角色 +
+
+
管理员
+
开发者
+
访客
+
+
+
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+
+ +
垂直表单
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
校验状态
+
+
+
+ +
+ +
此字段为必填项
+
+
+
+ +
+ +
+
+
+
+
+ + +
+

5. 搜索 Search

+
+ +
+
+ + +
+

6. 开关 Switcher

+
+
+ 关闭: + + 开启: + + 禁用: + + 禁用(开): + +
+
+
+ + +
+

7. 下拉选择 Select

+ +
+
+
+
收起状态
+
+
+ 请选择 +
+
+
选项一
+
选项二
+
选项三
+
+
+
+
+
展开状态(手动 is-show)
+
+
+ 请选择 +
+
+
选项 A
+
选项 B(已选)
+
选项 C
+
选项 D(禁用)
+
+
+
+
+
+
+ + +
+

8. 表格 Table

+ +
基础表格(带排序、悬浮高亮、操作按钮)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID名称类型创建时间操作
1001生产部署流程流程2026-03-20 10:00:00 + + + +
1002测试环境回收定时2026-03-19 15:30:00 + + + +
1003权限审批流程审批2026-03-18 09:15:00 + + + +
+
+ +
空状态
+
+ + + + + + + + + + + + + +
名称状态操作
暂无数据
+
+
+ + +
+

9. 分页 Pagination

+
+
+ 共 128 条 + « + 1 + 2 + 3 + 4 + 5 + » +
+
+
+ + +
+

10. 标签页 Tab

+
+
+
+ 标签一 + 标签二 + 标签三 +
+
+

这是标签一的内容区域。可以放置任何组件和布局。

+
+ + +
+
+
+ + +
+

11. 卡片 Card

+
+
+
卡片标题
+
+

卡片内容区域,可以放置文本、表单、列表等各种内容。

+
+ +
+
+
统计信息
+
+
1,284
+
本月执行任务总数
+
+
+
+
+ + +
+

12. 对话框 Dialog

+
+ +
+
+ +
+
+
+
+ 确认删除 + +
+
+ 确定要删除该条记录吗?此操作不可撤销。 +
+ +
+
+ + +
+

13. 侧滑 Sideslider

+
+ +
+
+ +
+
+
+
+ 侧滑面板标题 + +
+
+

侧滑面板可以放置表单、详情信息、日志等内容。

+

从右侧滑出,宽度 640px,带过渡动画。

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ + +
+

14. 面包屑 Breadcrumb

+
+
+ 首页 + 任务管理 + 任务详情 +
+
+
+ + +
+

15. 加载 Loading

+
+
+
+
+ + +
+

16. 异常 Exception

+
+
+ + + + + Empty + + 暂无数据 +
+
+
+ + +
+

17. 布局原语

+ +
操作栏 Toolbar
+
+
+
+ +
+
+ 筛选类型 +
+
+
+
+ + +
+
+
+ +
信息卡片 Info Card
+
+
+
+
创建人
+
admin
+
+
+
创建时间
+
2026-03-20 14:30:00
+
+
+
状态
+
运行中
+
+
+
所属空间
+
prod-space
+
+
+
执行时长
+
12.3s
+
+
+
关联模板
+
标准部署流程
+
+
+
+ +
24 列栅格 Grid
+
+
+
col-24
+
+
+
col-12
+
col-12
+
+
+
col-8
+
col-8
+
col-8
+
+
+
col-6
+
col-6
+
col-6
+
col-6
+
+
+
+ + + + diff --git a/prototypes/features/README.md b/prototypes/features/README.md new file mode 100644 index 0000000000..3a3568219d --- /dev/null +++ b/prototypes/features/README.md @@ -0,0 +1,55 @@ +# features + +## Purpose + +`prototypes/features/` holds one directory per feature slug. Each feature directory should bundle the pages needed for a single spec/plan pairing tied to a feature slug. + +## Contract + +- `prototypes/features/` is organized by feature slug. +- Each feature slug maps to one spec and one plan. +- Put delivery pages, feature notes, and change logs here. +- Do not put long-lived masters or shared shells here. +- `feature.meta.json` is the homepage showcase contract for each feature slug. +- `README.md` remains the narrative documentation for the feature directory. +- When creating a new feature, keep `README.md`, `index.html`, and `feature.meta.json` in sync. +- Keep `feature.meta.json` up to date so the showcase homepage can auto-discover the feature card, representative pages, and ordering without extra wiring. + +## `feature.meta.json` Schema + +```json +{ + "title": "feature 标题", + "summary": "一句话说明这个 feature", + "status": "可评审", + "tags": ["标签1", "标签2"], + "coverTheme": "mixed-admin", + "order": 10, + "featuredPages": [ + { + "path": "pages/space/flow-edit.html", + "title": "流程编辑页", + "summary": "展示该页的核心能力", + "pageType": "流程编辑" + } + ] +} +``` + +`featuredPages[*].path` 必须指向当前 feature 目录下真实存在的 HTML 文件,首页会拿它作为代表页入口。 + +## What goes here + +- Feature-specific prototype pages. +- Lightweight feature notes and change logs. +- Pages that are meant to answer one concrete requirement. + +## What should not go here + +- Long-lived master patterns. +- Shared shell fragments. +- Miscellaneous examples that are not tied to a feature slug. + +## Relationship to `docs/specs` and `docs/plans` + +Each feature directory should be traceable back to one `docs/specs/` document and one `docs/plans/` document. That relationship should stay explicit so the prototype work can be reviewed against the requested scope. diff --git a/prototypes/features/_legacy/README.md b/prototypes/features/_legacy/README.md new file mode 100644 index 0000000000..9a380de271 --- /dev/null +++ b/prototypes/features/_legacy/README.md @@ -0,0 +1,28 @@ +# _legacy + +## Purpose + +`prototypes/features/_legacy/` is the temporary archive for older feature material that is being phased out of the new structure. It is not the active source of truth. + +## Contract + +- `prototypes/features/_legacy/` is archive-only and is not the active source of truth. +- Use it only for migrated historical feature material. +- New work belongs in `prototypes/features//`. +- Do not put shared masters or delivery pages here. + +## What goes here + +- Old feature pages during migration. +- Historical artifacts that still need a place while we clean up the tree. +- Transition-only content that is no longer the active source of truth. + +## What should not go here + +- New feature work. +- Shared masters. +- Anything that should be treated as a current feature directory. + +## Relationship to `docs/specs` and `docs/plans` + +Legacy content may point back to old `docs/specs/` or `docs/plans/` entries, but it should not become the place where new work starts. New work belongs in `prototypes/features//`. diff --git a/prototypes/features/_legacy/node-output-viewer/README.md b/prototypes/features/_legacy/node-output-viewer/README.md new file mode 100644 index 0000000000..94b4779a06 --- /dev/null +++ b/prototypes/features/_legacy/node-output-viewer/README.md @@ -0,0 +1,9 @@ +# node-output-viewer + +## Purpose + +This folder archives the historical node output viewer proposal that previously lived under `prototypes/output/node-output-viewer-proposal-v1.html`. + +## Contents + +- `index.html` preserves the migrated prototype page. diff --git a/prototypes/features/_legacy/node-output-viewer/index.html b/prototypes/features/_legacy/node-output-viewer/index.html new file mode 100644 index 0000000000..caa54fec6f --- /dev/null +++ b/prototypes/features/_legacy/node-output-viewer/index.html @@ -0,0 +1,646 @@ + + + + + + 节点输出结构化展示方案 v1 + + + + +
+
+

节点输出结构化展示方案 v1

+

+ 目标是把 BKFlow 的“节点输出”从当前偏字符串/原始 JSON 的阅读体验,升级成 + 协议驱动的结构化展示: + 插件声明输出 schema、viewer 和字段示例数据,平台在模板编辑态、配置快照和执行记录三处统一消费。 +

+
+ 第一版覆盖:模板编辑态 + 配置快照 + 执行记录 + viewer:平台内置类型 + 兼容旧插件:默认 auto viewer + 示例数据:字段级 example + Raw 视图保留 +
+
+ +
+

方案对比

+

推荐方案会把“定义协议”和“展示能力”一起补齐

+
+
+
+

方案 A:纯前端自动识别

+

不改插件协议,前端根据 value / type / schema 猜测展示方式。

+
    +
  • 优点:开发快,改动小
  • +
  • 缺点:结果不稳定,模板编辑态看不到示例预览
  • +
  • 适合:临时增强执行记录页
  • +
+
+ +
+

方案 C:自定义 renderer

+

插件声明自己的专属输出渲染器,由平台按 renderer key 动态装配。

+
    +
  • 优点:扩展性强
  • +
  • 缺点:第一版复杂度高,平台治理成本重
  • +
  • 适合:第二阶段以后补充少量特例
  • +
+
+
+ +
+

推荐协议

+

后端主导,前端可少量覆盖;旧插件未声明时回退到 auto

+
+
{ + "key": "data", + "name": "响应内容", + "type": "object", + "schema": { + "description": "HTTP 请求响应内容", + "properties": { + "status": { "type": "string" }, + "items": { "type": "array" } + } + }, + "viewer": { + "type": "table", + "options": { + "columns": [ + { "key": "id", "label": "ID" }, + { "key": "name", "label": "名称" }, + { "key": "status", "label": "状态", "viewer": "badge" } + ], + "default_mode": "friendly", + "raw_enabled": true + } + }, + "example": { + "items": [ + { "id": "vm-01", "name": "db-prod-1", "status": "running" }, + { "id": "vm-02", "name": "db-prod-2", "status": "stopped" } + ] + } +}
+ +
+

第一版内置 viewer

+

不开放任意自定义 JS,只做平台受控 viewer

+
+
+
+

基础类

+

覆盖大部分旧插件和普通字段,未声明时优先走 auto。

+
+ auto + text + badge + link + markdown +
+
+
+

结构化类

+

面向 object / array / api data,解决“能看见 JSON,但不容易读”的问题。

+
+ json-tree + kv + table + log +
+
+
+

媒体类

+

作为补充,统一支持一些可直接感知的资源类型。

+
+ image + raw +
+
+
+ +
+

三处页面落点

+

模板编辑态看“定义与示例”,配置快照看“定义快照”,执行记录看“真实值 + Raw”

+
+
+
+
+

模板编辑态:节点输出定义

+ 输出列表内联示例预览 +
+
+
+ 节点输出 + 支持勾选为全局变量 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
名称KEYviewer示例变量
响应内容datatable查看示例未勾选
状态码status_codebadge查看示例${status_code}
+
+
+ data · 示例预览 +
+ Friendly + Raw +
+
+
+ + + + + + + + + + + + + + + + + + + + +
ID名称状态
vm-01db-prod-1running
vm-02db-prod-2stopped
+
编辑器里可直接知道:这个字段运行后会默认按表格展示,而不是只能猜它是 object。
+
+
+
+
+
+ +
+
+

配置快照:输出协议快照

+ 重点看定义,不看真实值 +
+
+
+ 输出配置快照 +
+ 定义 + 示例 +
+
+
+ + + + + + + + + + + + + + + + + + + + +
字段类型viewer
dataobjecttable
status_codeintbadge
+
+
+ viewer 配置 + 用于解释为什么执行记录这样展示 +
+
+
{ + "data": { + "viewer": "table", + "columns": ["id", "name", "status"] + }, + "status_code": { + "viewer": "badge" + } +}
+
+
+
+
+
+ +
+
+

执行记录:真实输出值

+ Friendly / Raw 双视图 +
+
+
+ 输出参数 +
+ Friendly + Raw +
+
+
+ + + + + + + + + + + + + + + + + + + + +
参数名Key
状态码status_code200
响应内容data +
+ items +
[0] id: vm-01
+
[0] name: db-prod-1
+
[0] status: running
+
+
+
+ 旧插件如果没声明 viewer,也能先走 auto:object 用 json-tree,array<object> 尝试表格化,始终保留 Raw 视图。 +
+
+
+
+
+ + +
+ + diff --git a/prototypes/features/_legacy/sops-open-plugin/README.md b/prototypes/features/_legacy/sops-open-plugin/README.md new file mode 100644 index 0000000000..77b2025e76 --- /dev/null +++ b/prototypes/features/_legacy/sops-open-plugin/README.md @@ -0,0 +1,13 @@ +# sops-open-plugin + +## Purpose + +This folder archives the historical Standard Ops open-plugin prototype pages that used to live under `prototypes/output/`. + +## Contents + +- `index.html` is the archive landing page. +- `main-flow.html` preserves the main flow prototype. +- `space-open-plugin-management.html` preserves the space management prototype. +- `task-plugin-error-state.html` preserves the task error state prototype. +- `template-plugin-selection.html` preserves the template plugin selection prototype. diff --git a/prototypes/features/_legacy/sops-open-plugin/index.html b/prototypes/features/_legacy/sops-open-plugin/index.html new file mode 100644 index 0000000000..84f7582087 --- /dev/null +++ b/prototypes/features/_legacy/sops-open-plugin/index.html @@ -0,0 +1,101 @@ + + + + + + sops-open-plugin archive - BKFlow 原型 + + + + +
+
+

sops-open-plugin archive

+

+ This folder keeps the migrated historical open-plugin prototype pages that used to live in `prototypes/output/`. + The original HTML has been preserved under feature-scoped legacy paths so the archive stays understandable. +

+
+ +
+ +

Main flow

+

Former `sops-open-plugin-main-flow.html`.

+
+ +

Space management

+

Former `sops-open-plugin-space-open-plugin-management.html`.

+
+ +

Task error state

+

Former `sops-open-plugin-task-plugin-error-state.html`.

+
+ +

Template plugin selection

+

Former `sops-open-plugin-template-plugin-selection.html`.

+
+
+
+ + diff --git a/prototypes/features/_legacy/sops-open-plugin/main-flow.html b/prototypes/features/_legacy/sops-open-plugin/main-flow.html new file mode 100644 index 0000000000..805326a850 --- /dev/null +++ b/prototypes/features/_legacy/sops-open-plugin/main-flow.html @@ -0,0 +1,343 @@ + + + + + + 标准运维开放插件主流程 - BKFlow 原型 + + + + +
+
+
+ BKFlow +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ space_admin +
+
+
+
+ + +
+
+ 首页 + 标准运维开放插件接入 + 主流程 +
+ +
+
标准运维开放插件在 BKFlow 的主流程
+
+ 这组原型把治理、配置和任务异常解释串成一条完整路径。主目标不是新增一种插件类型,而是让用户继续以 API 插件心智使用标准运维开放出来的内置插件和第三方插件,同时把空间级治理和版本可用性说清楚。 +
+ +
+
+
1
+
空间管理员开放插件
+
在“开放插件管理”里查看来源、搜索插件、对当前已发现插件进行逐个开启或批量开启。
+
+ +
+
+
+
+
2
+
模板作者选择插件与版本
+
在模板节点配置里按普通 API 插件方式选择来源、插件和显式版本字段,并感知当前版本是否仍可新用。
+
+ +
+
+
+
+
3
+
任务页展示执行结果或异常
+
启动任务后统一进入任务页。如果插件未开放、版本失效或来源不可达,任务页通过结构化错误卡片解释原因并给出建议动作。
+
+ +
+
+
+
+ +
+
+
当前空间已接入来源
+
1 个
+
标准运维开放插件来源已完成同步,当前已发现 24 个开放插件,其中 9 个已开启。
+
+
+
模板侧新增心智
+
1 个字段
+
模板编辑只新增显式 `plugin_version` 字段,其余仍维持普通 API 插件配置方式。
+
+
+
任务异常承载位置
+
任务页
+
启动前不做复杂拦截,所有插件不可用类问题统一沉淀到任务页解释。
+
+
+ +
治理摘要
+
+
+
开放来源摘要
+ + + + + + + + + + + + + + + + + +
来源状态插件数最近同步
标准运维开放插件可用242026-04-20 11:48
+
+
+
开放规则
+
+ 1. 新同步进来的插件默认关闭。
+ 2. “一键全开”只作用于当前已发现插件。
+ 3. 已下线插件保留历史记录,但不可继续新用。
+ 4. 来源级紧急停用属于增强态预留。 +
+
+
+ +
主线与异常线
+
+
+
+ 主线 + 标准接入流程 +
+
+ 空间管理员开启“云作业执行脚本”插件 → 模板作者选择版本 `1.4.2` → 用户启动任务 → 任务页展示成功状态与插件版本快照。 +
+
+
+
+ 异常线 + 版本失效流程 +
+
+ 历史模板仍引用 `1.2.0` → 来源目录已不再提供该版本 → 用户启动任务 → 任务页展示“版本不可用”错误卡片,并引导返回模板切换版本。 +
+
+
+ + +
+
+
+ + + + diff --git a/prototypes/features/_legacy/sops-open-plugin/space-open-plugin-management.html b/prototypes/features/_legacy/sops-open-plugin/space-open-plugin-management.html new file mode 100644 index 0000000000..13e0768746 --- /dev/null +++ b/prototypes/features/_legacy/sops-open-plugin/space-open-plugin-management.html @@ -0,0 +1,343 @@ + + + + + + 开放插件管理 - BKFlow 原型 + + + + +
+
+
+ BKFlow +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ space_admin +
+
+
+
+ + +
+
+ 首页 + 空间管理 + 开放插件 +
+ + + +
+
+
+
标准运维开放插件
+ 可用 +
+
+ 已发现 24 个插件,已开启 9 个。
+ 类型同时包含标准运维内置插件与第三方插件。
+ 当前空间仍可继续同步该来源的新目录。 +
+
+ + +
+
+
+
+
增强态:来源紧急停用
+ 预留 +
+
+ 当开放层整体不可用时,管理员可按来源批量关闭当前来源下的插件,避免模板继续新建任务。 +
+
+ +
+
+
+
+
规则摘要
+ 说明 +
+
+ 1. 新增插件默认关闭。
+ 2. 一键全开只作用于当前已发现插件。
+ 3. 已下线插件仅保留记录,不可继续新用。 +
+
+
+ +
+ 你可以对单个插件开关,也可以对“当前列表中已发现的插件”执行一键全开。后续同步进来的新插件不会自动继承这次批量动作,仍然默认关闭。 +
+ +
+
+ +
+
+ 全部来源 +
+
+
+
+ 全部类型 +
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
插件名来源别名插件类型当前状态版本概况最近同步时间操作
云作业执行脚本[标准运维内置插件]内置已开启默认 1.4.2 / 最新 1.5.02026-04-20 11:48 +
+ + +
+
批量更新主机属性[标准运维第三方插件]第三方已开启默认 2.1.0 / 最新 2.2.12026-04-20 11:48 +
+ + +
+
节点巡检报告汇总[标准运维第三方插件]第三方未开启默认 1.0.3 / 最新 1.1.02026-04-20 11:48 +
+ + +
+
集群扩容预检查[标准运维内置插件]内置已下线历史版本 1.2.0 已不可新用2026-04-18 19:20 +
+ + +
+
标准运维回调压测[标准运维第三方插件]第三方来源异常目录已同步,执行状态待恢复2026-04-20 11:48 +
+ + +
+
+ +
+
+ 增强态示意: 当来源进入“紧急停用”状态时,当前空间下该来源的所有插件会统一转成不可继续新用,但仍保留历史模板与历史任务的回看能力。 +
+
+ 页面约束: 这里控制的是空间级开放状态。模板保存、任务创建和任务启动仍由服务端基于当前状态再次校验,不会只靠前端列表展示拦截。 +
+
+
+
+
+ + + + diff --git a/prototypes/features/_legacy/sops-open-plugin/task-plugin-error-state.html b/prototypes/features/_legacy/sops-open-plugin/task-plugin-error-state.html new file mode 100644 index 0000000000..74363d0017 --- /dev/null +++ b/prototypes/features/_legacy/sops-open-plugin/task-plugin-error-state.html @@ -0,0 +1,281 @@ + + + + + + 任务页插件异常提示 - BKFlow 原型 + + + + +
+
+
+ BKFlow +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ task_operator +
+
+
+
+ + +
+
+ 首页 + 任务 + 任务详情 +
+ +
+
+
生产集群执行脚本任务 #20260420-0042
+
+ 来源模板:生产环境脚本执行 / 当前节点:云作业执行脚本 / 启动时间:2026-04-20 14:18:03 +
+
+
执行异常
+
+ +
+
+
+
插件版本不可用,任务未能继续执行
+
+ 当前任务引用的开放插件版本已不再由来源目录提供。BKFlow 仍保留了历史配置快照用于回看,但该版本不能继续用于新的执行请求,因此任务页展示结构化异常说明,并引导返回模板侧处理。 +
+
+
+
当前插件
+
云作业执行脚本
+
+
+
当前版本
+
1.2.0
+
+
+
当前来源
+
标准运维开放插件
+
+
+
当前影响
+
该任务无法继续执行,但历史节点配置仍可回看
+
+
+
+ +
+
+
建议动作
+ 任务页只解释,不原地修复 +
+
+
+
返回模板切换到可用版本
+
模板作者需要将当前节点从 `1.2.0` 切换到目录仍可用的版本,例如 `1.4.2` 或 `1.5.0`,并重新确认参数。
+ +
+
+
检查空间是否仍开放该插件
+
如果插件本身还在来源目录中,也可能是当前空间关闭了该插件,需回到空间开放插件管理页确认。
+ +
+
+
联系来源维护人或空间管理员
+
若来源目录刚完成版本清理、来源当前不可达,或插件能力本身已下线,需要联系管理员或维护人确认处理路径。
+ +
+
+
+
+ +
+
+
+
历史快照说明
+ 只读 +
+
+
+ 该任务创建时已经固化了插件引用快照和 schema 快照,因此当前页面仍能解释“当时为什么能保存、现在为什么不能继续新建任务”。这里展示的是历史视角,不代表该版本现在仍可用于新任务。 +
+
+
+ +
+
+
异常分类示意
+ 增强态预留 +
+
+
+
空间未开放
+
插件仍可用,但当前空间未开启该插件。
+
+
+
来源不可达
+
开放来源当前异常,执行链路无法继续调用标准运维开放层。
+
+
+
插件已下线
+
插件或版本已从目录彻底移除,只保留历史记录。
+
+
+
+
+
+
+
+
+ + + + diff --git a/prototypes/features/_legacy/sops-open-plugin/template-plugin-selection.html b/prototypes/features/_legacy/sops-open-plugin/template-plugin-selection.html new file mode 100644 index 0000000000..b6820e2597 --- /dev/null +++ b/prototypes/features/_legacy/sops-open-plugin/template-plugin-selection.html @@ -0,0 +1,362 @@ + + + + + + 模板插件选择与版本展示 - BKFlow 原型 + + + + +
+
+
+ BKFlow +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ template_owner +
+
+
+
+ + +
+
+ 首页 + 流程 + 模板编辑 + 节点插件配置 +
+ + + +
+
+
+
+
插件选择
+ 普通 API 插件体验 +
+
+
+
+ +
+
+
+ 标准运维开放插件 +
+
+
+
+
+ +
+
+
+ 作业执行 +
+
+
+
+
+ +
+
+
+ 云作业执行脚本 +
+
+
+
+
+ +
+
+
+ 1.4.2 +
+
+
+
+
+ +
+ 当前节点保留普通 API 插件配置方式。版本字段始终显式展示,用来说明模板最终绑定的是哪个开放插件版本。 +
+
+
+ +
+
+
参数配置
+ 按当前版本 schema 渲染 +
+
+
+
业务 ID
+
+
+
+
执行账号
+
+
+
+
脚本内容
+
+
+
+
超时时间
+
+
+
+
+ +
+
+
历史模板回看态
+ 只读示意 +
+
+
+ 当历史模板引用的版本已经不再开放时,页面仍会展示当时保存下来的插件引用快照和 schema 快照,但对应字段进入只读态,用户需要切换到可用版本后才能继续新建任务。 +
+
+
+
+ +
+
+
+
插件摘要
+ 当前可用 +
+
+
+
+
来源别名
+
[标准运维内置插件]
+
+
+
插件类型
+
内置插件
+
+
+
默认版本
+
1.4.2
+
+
+
最新版本
+
1.5.0
+
+
+
+ 当前空间已开放该插件,且版本 `1.4.2` 仍可继续新用。若切换到更新版本,需要重新确认参数兼容性。 +
+
+
+ +
+
+
版本列表
+ 增强态预留 +
+
+
+
+ 1.5.0
+ 新增脚本执行结果归档字段,推荐新模板使用。 +
+ 可用 +
+
+
+ 1.4.2
+ 当前模板绑定版本。 +
+ 当前版本 +
+
+
+ 1.2.0
+ 历史模板常见版本,目录已不再开放。 +
+ 不可用 +
+
+
+ +
+
+
失效版本提示
+ 异常态 +
+
+
+ 当前模板若仍绑定版本 `1.2.0`,页面应允许查看历史配置,但不能继续直接新建任务。用户需要先切换到可用版本,例如 `1.4.2` 或 `1.5.0`,再重新确认参数。 +
+
+
+
+
+
+
+
+ + + + diff --git a/prototypes/features/_legacy/space-variable-manage/README.md b/prototypes/features/_legacy/space-variable-manage/README.md new file mode 100644 index 0000000000..927203570b --- /dev/null +++ b/prototypes/features/_legacy/space-variable-manage/README.md @@ -0,0 +1,9 @@ +# space-variable-manage + +## Purpose + +This folder archives the historical space variable management prototype that previously lived under `prototypes/output/space-variable-manage.html`. + +## Contents + +- `index.html` preserves the migrated prototype page. diff --git a/prototypes/features/_legacy/space-variable-manage/index.html b/prototypes/features/_legacy/space-variable-manage/index.html new file mode 100644 index 0000000000..0999c1d1ab --- /dev/null +++ b/prototypes/features/_legacy/space-variable-manage/index.html @@ -0,0 +1,651 @@ + + + + + + 空间变量管理 - BKFlow 原型 + + + + +
+
+
+ BKFlow +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ 用户名 +
+
+
+ +
+ + +
+

空间配置

+ + +
+
+ 基本信息 + 功能开关 + 变量管理 +
+ + + + + + + + +
+ + +
+ 作用域: + 全部 + 空间级 + Scope 级 + +
+ + +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
变量名KEY类型变量值作用域描述创建人更新时间操作
BK_APP_CODE${_space.BK_APP_CODE}stringbkflow-eng-svc空间级应用标识,用于 API 鉴权admin2026-03-20 14:30 + + +
BK_APP_SECRET${_space.BK_APP_SECRET}password••••••••空间级应用密钥,加密存储admin2026-03-20 14:30 + + +
NOTIFY_RECEIVERS${_space.NOTIFY_RECEIVERS}json["admin","devops"]空间级默认通知接收人列表admin2026-03-19 10:15 + + +
DEPLOY_ENV${_scope.DEPLOY_ENV}stringproductionbiz = 2部署环境标识devops2026-03-18 16:42 + + +
MAX_RETRY${_scope.MAX_RETRY}number3biz = 2节点失败最大重试次数devops2026-03-18 16:42 + + +
ENABLE_APPROVAL${_scope.ENABLE_APPROVAL}booleantruebiz = 5是否启用审批节点pm_wang2026-03-17 09:20 + + +
CMDB_API_URL${_space.CMDB_API_URL}stringhttps://cmdb.example.com/api/v3空间级CMDB 接口地址admin2026-03-15 11:08 + + +
+ +
+ 共 7 条 + 1 +
+
+
+ + +
+

▼ 以下为「任务详情」页面中,空间变量在全局变量面板里的展示效果预览(与系统变量、用户变量并列展示)

+
+
全局变量
+ + +
+
${_system}
+
<SystemObject {task_space_id: 237, task_scope_type: 'biz', task_scope_value: '2', operator: 'admin', task_id: 8821, task_name: '日常巡检任务_2026-03-23 10:00:00', credentials: [{bk_app_code: 'bkflow-eng-svc', bk_app_secret: '******'}]}>
+
+ + +
+
${input}
+
{"target_hosts": ["10.0.1.1", "10.0.1.2"], "check_items": ["cpu", "memory", "disk"]}
+
+ + +
+
${_space.BK_APP_CODE} 空间变量
+
bkflow-eng-svc
+
+
+
${_space.BK_APP_SECRET} 空间变量
+
••••••••
+
+
+
${_space.NOTIFY_RECEIVERS} 空间变量
+
["admin","devops"]
+
+
+
${_space.CMDB_API_URL} 空间变量
+
https://cmdb.example.com/api/v3
+
+ +
+
${_scope.DEPLOY_ENV} scope 变量 · biz=2
+
production
+
+
+
+ +
+
+
+ + +
+
+
+
+ 新建变量 + +
+
+
+
+ +
+ +
变量名不能为空
+
+
+
+ +
+
+
+ 请选择类型 +
+
+
string(字符串)
+
number(数字)
+
boolean(布尔)
+
password(密码)
+
json(JSON 对象)
+
+
+
+
+
+ +
+ +
变量值不能为空
+
+
+
+ +
+
+
+ 请选择作用域 +
+
+
空间级(所有流程共享)
+
Scope 级(按业务划分)
+
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ 编辑变量 + +
+
+
+
+ +
+ +
变量名创建后不可修改
+
+
+
+ +
+
+
+ string(字符串) +
+
+
string(字符串)
+
number(数字)
+
boolean(布尔)
+
password(密码)
+
json(JSON 对象)
+
+
+
+
+
+ +
+ +
+
+
+ +
+ +
作用域创建后不可修改
+
+
+
+ +
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ 确认删除 + +
+
+

删除后该变量将从所有关联流程中移除,已运行的任务不受影响。

+

此操作不可撤销,请确认。

+
+ +
+
+ + + + + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/CHANGELOG.md b/prototypes/features/bkflow-engine-admin-prototype-overhaul/CHANGELOG.md new file mode 100644 index 0000000000..36f810eee0 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 2026-04-21 +- Initialized the `bkflow-engine-admin-prototype-overhaul` feature directory. +- Added the feature README with spec/plan traceability, masters used, and grouped page inventory. +- Added the feature entry `index.html` for quick preview scanning. diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/README.md b/prototypes/features/bkflow-engine-admin-prototype-overhaul/README.md new file mode 100644 index 0000000000..0fb6b1207e --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/README.md @@ -0,0 +1,50 @@ +# BKFlow Engine Admin Prototype Overhaul + +## Related Docs +- Spec: `docs/specs/2026-04-21-bkflow-engine-admin-prototype-overhaul-design.md` +- Plan: `docs/plans/2026-04-21-bkflow-engine-admin-prototype-overhaul.md` + +## Purpose +This feature bundles the full BKFlow engine admin prototype overhaul into one traceable delivery set. The goal is to rebuild the admin experience as a complete `space / system / plugin` prototype family, with the real navigation, list, editor, detail, and debug states represented in one feature directory. + +## Masters Used +- `prototypes/masters/_shared/space-shell.html` +- `prototypes/masters/_shared/system-shell.html` +- `prototypes/masters/_shared/plugin-shell.html` +- `prototypes/masters/list-page/template.html` +- `prototypes/masters/config-page/template.html` +- `prototypes/masters/flow-editor/template.html` +- `prototypes/masters/task-detail/template.html` +- `prototypes/masters/decision-editor/template.html` +- `prototypes/masters/engine-panel/template.html` +- `prototypes/masters/overlays/dialogs.html` +- `prototypes/masters/overlays/sidesliders.html` + +## Page Entries + +### Space +- `pages/space/template-list.html` +- `pages/space/flow-view.html` +- `pages/space/flow-edit.html` +- `pages/space/flow-debug.html` +- `pages/space/task-list.html` +- `pages/space/debug-task-list.html` +- `pages/space/task-detail-complete.html` +- `pages/space/task-detail-failed.html` +- `pages/space/decision-list.html` +- `pages/space/decision-editor.html` +- `pages/space/space-config.html` +- `pages/space/credential-list.html` +- `pages/space/label-list.html` +- `pages/space/statistics-exception.html` + +### System +- `pages/system/space-config-list.html` +- `pages/system/module-config-list.html` + +### Plugin +- `pages/plugin/plugin-list.html` + +## Notes +- `index.html` is the feature entry point for previewing the page inventory. +- `CHANGELOG.md` keeps only lightweight initialization and follow-up notes. diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/feature.meta.json b/prototypes/features/bkflow-engine-admin-prototype-overhaul/feature.meta.json new file mode 100644 index 0000000000..e5c70f9223 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/feature.meta.json @@ -0,0 +1,22 @@ +{ + "title": "BKFlow 引擎管理后台原型重构", + "summary": "覆盖 Space / System / Plugin 三个入口,包含流程、任务、决策与配置链路。", + "status": "可评审", + "tags": ["流程编辑", "任务详情", "系统配置"], + "coverTheme": "mixed-admin", + "order": 10, + "featuredPages": [ + { + "path": "pages/space/flow-edit.html", + "title": "流程编辑页", + "summary": "画布编排、节点抽屉与两段式保存。", + "pageType": "流程编辑" + }, + { + "path": "pages/space/task-detail-complete.html", + "title": "任务详情页", + "summary": "执行状态、节点观察与日志查看。", + "pageType": "任务详情" + } + ] +} diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/index.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/index.html new file mode 100644 index 0000000000..cb6dfcd55e --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/index.html @@ -0,0 +1,278 @@ + + + + + + BKFlow Engine Admin Prototype Overhaul + + + + + +
+
+ BKFlow + BKFlow 原型 · Feature 入口 +
+

bkflow-engine-admin-prototype-overhaul

+

+ 这一组页面对应 BKFlow engine admin 原型重建的完整交付集,覆盖 space / system / plugin 三个入口域, + 用于快速检查页面清单、打开单页原型,并保持与 spec、plan、masters 的回链一致。 +

+
+
Spec docs/specs/2026-04-21-bkflow-engine-admin-prototype-overhaul-design.md
+
Plan docs/plans/2026-04-21-bkflow-engine-admin-prototype-overhaul.md
+
Masters 11 referenced
+
Scope 17 pages
+
+
+ +
+
+
+

Space

+ 14 pages +
+

空间侧原型覆盖流程、任务、决策、配置、凭证、标签和异常态,适合按页面逐个预览和对照审计结果。

+ +
+ +
+
+

System

+ 2 pages +
+

系统侧原型只保留跨空间管理的两个列表入口,适合快速定位配置分发和模块配置的预览页。

+ +
+ +
+
+

Plugin

+ 1 page +
+

插件侧原型只保留插件列表入口,便于和空间、系统视图并列查看。

+ +
+ + +
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/plugin/plugin-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/plugin/plugin-list.html new file mode 100644 index 0000000000..2565b3ec9e --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/plugin/plugin-list.html @@ -0,0 +1,452 @@ + + + + + + BKFlow 原型 - 我的插件 / 插件管理列表 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ plugin_admin + +
+
+
+ +
+ + +
+
+
+
+ 首页 + 我的插件 + 插件管理列表 +
+ + + +
+
+
插件总数
+
24
+
包含内置插件和第三方插件,均在同一列表中管理。
+
+
+
已授权
+
9
+
已授权插件可在模板和流程编辑器中直接使用。
+
+
+
待授权
+
6
+
待授权项需管理员确认使用范围后才能启用。
+
+
+
已停用
+
3
+
停用插件保留历史记录,避免误操作再次启用。
+
+
+ +
+
+
+
插件清单
+
按照插件名称、code、授权状态和管理员维度查看管理权限,支持调整使用范围和授权状态。
+
+
+
+
+ +
+
全部授权状态
+
+
+
全部使用范围
+
+
+
+ + +
+
+ +
+ 全部 + 已授权 + 待授权 + 授权中 + 已停用 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
插件名称code管理员授权状态使用范围修改时间修改人操作
云作业执行脚本bk-script-runnerplatform_ops已授权标准运维 / 作业执行2026-04-20 11:48admin + + + +
定时器增强bk-cron-extendflow_center待授权bkflow-eng-svc / bkflow-test2026-04-19 16:32karen + + + +
跨空间通知cross-space-notifysecurity_audit已停用生产空间2026-04-18 09:15liwei + + + +
实例参数校验instance-params-checkplatform_governance授权中全局2026-04-21 09:20system-bot + + + +
三方回调适配器third-party-webhookintegration_team已授权研发 / 灰度2026-04-17 14:06anna + + + +
+
+ +
+ 共 24 条 + « + 1 + 2 + 3 + » +
+
+
+ + +
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/credential-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/credential-list.html new file mode 100644 index 0000000000..bbef0f9551 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/credential-list.html @@ -0,0 +1,256 @@ + + + + + + + 空间管理 - 凭证管理 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+

凭证管理

+

列表页 + 新建侧滑 + 查看内容弹窗,类型切换使用显式选项组而不是普通下拉。

+
+
+ +
+
+ +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型开放范围创建人更新时间操作
bkapp-prod-release蓝鲸应用认证空间内可见admin2026-04-20 19:32:15 + + + +
bkapi-access-token蓝鲸 Access Token 认证仅管理员ops_admin2026-04-19 13:40:22 + + + +
notify-basic-authBasic Auth全局共享flow_bot2026-04-17 08:11:07 + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+

查看内容

+ +
+
+
{ + "bk_app_code": "bkflow-eng-svc", + "bk_app_secret": "*********", + "access_token": "*********", + "expires_in": 7200 +}
+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/debug-task-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/debug-task-list.html new file mode 100644 index 0000000000..9ac037b5fb --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/debug-task-list.html @@ -0,0 +1,199 @@ + + + + + + + 空间管理 - 调试任务 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
+
A
+ admin +
+
+
+
+ + +
+
+
+
+
+

调试任务

+

与任务页共享相似的列表骨架,但按钮、状态和任务详情跳转都指向 mockTask 链路。

+
+ +
+ +
+
+
+ +
+
+ 当前列表映射 `/taskflow/execute/:spaceId?type=mock` 详情页 +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
任务 ID任务名称执行状态源流程创建人创建时间操作
1192795audit-deep-dive-20260420_调试任务_20260420203821完成audit-deep-dive-20260420admin2026-04-20 20:38:21
1192803audit-deep-dive-20260420_调试任务_20260420204106失败audit-deep-dive-20260420admin2026-04-20 20:41:06
1192812mock-api-regression_调试任务_20260421100219未执行mock-api-regressiontester2026-04-21 10:02:19
+
+ +
+ 共 3 条 + « + 1 + » +
+
+
+
+
+
+
+ +
+
+
+
+

删除调试任务

+ +
+
+

确认删除任务 "audit-deep-dive-20260420_调试任务_20260420203821"?

+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-editor.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-editor.html new file mode 100644 index 0000000000..3573b9bd9c --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-editor.html @@ -0,0 +1,158 @@ + + + + + + + 空间管理 - 新建决策表 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+ ← 返回决策表列表 +

新建决策表

+

独立编辑页,底部动作固定为 `提交 / 取消 / 调试`,不在列表页原地完成编辑。

+
+
+ +
+
基础信息
+
+
+ +
+
+
+ +
audit-deep-dive-20260420
+
+
+ +
+
+
+
+ +
+
规则配置
+
+
+ 暂无规则 +
先补充条件字段和结果字段,再逐条录入决策规则。
+
+ + +
+
+ + +
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-list.html new file mode 100644 index 0000000000..d29804d085 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/decision-list.html @@ -0,0 +1,121 @@ + + + + + + + 空间管理 - 决策表 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+

决策表

+

列表页保持空态,主按钮跳转到独立编辑器,而不是在当前页打开简单弹层。

+
+
+ 新建 +
+
+
+
+
+ +
+
+ 独立路由语义:/admin_decision/?space_id=171 +
+
+
+
+ 暂无数据 +
当前空间还没有任何决策表。审计中这里的「新建」直接跳到专用编辑器。
+ +
+
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-debug.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-debug.html new file mode 100644 index 0000000000..a85b787c80 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-debug.html @@ -0,0 +1,219 @@ + + + + + + + 空间管理 - 调试流程 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+
+ ← 退出调试 +
+

调试流程

+

audit-deep-dive-20260420 · `/template/mock/true/2166/`

+
+
+ 调试模式 + Mock + 节点点击后进入 Mock 数据配置 +
+
+
+ 退出调试 + + +
+
+ +
+ + +
+
+
+ Mock 调试画布 + 单击节点就打开右侧 Mock 数据配置;这里不承载流程编排。 +
+
+ 历史调试 12 次 + Mock 数据方案1 +
+
+ +
+
+
开始
+ + + + + +
结束
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+

执行调试

+ +
+
+

确定保存Mock数据并去执行调试?

+

真实链路会新开标签页进入 `/template/mock/execute/true/2166/`,当前环境继续执行后可能弹出「应用出现异常」。

+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-edit.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-edit.html new file mode 100644 index 0000000000..0b888f9b83 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-edit.html @@ -0,0 +1,356 @@ + + + + + + + 空间管理 - 编辑流程 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+
+ ← 返回流程列表 +
+

编辑流程

+

audit-deep-dive-20260420 · 发布蓝鲸作业审批链

+
+
+ 流程版本 + 草稿 + 最新发布 1.0.0 +
+
+
+ + + 调试 +
+
+ +
+ + +
+
+
+ 流程画布 + 单击查看节点悬浮工具条,双击进入配置抽屉。 +
+
+ - + 100% + + +
+
+ +
+
+
+ 开始 +
+ + + + + +
+ 结束 +
+
+ +
+ 节点悬浮工具条 + + + + +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+

全局变量

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
变量名说明示例值
_system.task_name调度时自动注入的任务名称audit-deep-dive-20260420
_system.task_id系统生成的任务 ID17100002166
_system.task_start_time任务启动时间2026-04-20 20:31:15
_system.operator当前操作人admin
+
+ +
+
+ +
+
+
+
+

发布流程

+ +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-view.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-view.html new file mode 100644 index 0000000000..2b63a2774c --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/flow-view.html @@ -0,0 +1,172 @@ + + + + + + + 空间管理 - 查看流程 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+
+ ← 返回流程列表 +
+

查看流程

+

audit-deep-dive-20260420 · 已发布版本只读观察页

+
+
+ 流程版本 + 已发布 + 1.0.0 最新 +
+
+
+ + 编辑 +
+
+ +
+ + +
+
+
+ 流程画布 + 查看页保留版本上下文和节点关系,但不出现编辑工具条。 +
+
+ 只读 + 1.0.0 +
+
+ +
+
+
开始
+ + + + + +
结束
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/label-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/label-list.html new file mode 100644 index 0000000000..2964d1bf79 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/label-list.html @@ -0,0 +1,243 @@ + + + + + + + 空间管理 - 标签管理 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+

标签管理

+

列表页保留层级标签语义和异步加载后的数据态,而不是只做一个纯空页面。

+
+
+ +
+
+ +
+
+
+ +
+
+ 首次快照可能短暂出现空态,随后异步加载层级标签数据 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
标签名称标签描述标签范围标签引用系统默认标签操作
发布/生产生产发布链路通用标签流程12 次
任务/回归巡检回归场景和巡检任务共用任务7 次
新标签/第三个二级标签用于演示层级名称与颜色选择器任务, 流程4 次
mock/debug专用于 mockTask 与调试链路任务9 次
+
+
+
+
+
+
+
+ +
+
+
+
+

新增标签

+ +
+
+
+
+ + +
+
+ +
+ + + + + +
+
+
+ + +
+
+ +
+ 任务 + 流程 +
+
+
+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/space-config.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/space-config.html new file mode 100644 index 0000000000..15a5f74f0a --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/space-config.html @@ -0,0 +1,210 @@ + + + + + + + 空间管理 - 空间配置 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+

空间配置

+

使用配置页母版表达长表单、代码编辑器和「保存默认禁用 / 恢复默认值需确认」的状态机。

+
+
+ + + +
+
+ +
+
基础配置
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
高级编排模式
+
+
+
+
+ +
+
API 插件配置
+
真实页面使用 Monaco 风格代码编辑器。这里保留多段代码块和字段密度,方便后续与真实配置清单一一映射。
+
{ + "plugin_base_url": "https://bkapi.example.com/bkflow/plugins", + "request_timeout": 30, + "allowed_scopes": ["biz", "project"], + "credential_types": [ + "bk_app", + "bk_access_token", + "basic_auth", + "custom" + ] +}
+
+ +
+
网关表达式
+
expression: + match: "space_id == 171" + allow: + - "template.view" + - "template.edit" + - "task.execute" +fallback: + policy: "deny" + message: "space gateway rule blocked"
+
+ +
+
默认 Mock / 调试配置
+
{ + "mock_history_limit": 20, + "allow_debug_execute": true, + "default_task_name_rule": "{{ template_name }}_调试任务_{{ timestamp }}", + "global_vars": [ + "_system.task_name", + "_system.task_id", + "_system.operator" + ] +}
+
+
+
+
+
+
+ +
+
+
+
+

恢复默认值

+ +
+
+

确认将所有配置项恢复为默认值?

+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/statistics-exception.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/statistics-exception.html new file mode 100644 index 0000000000..73a69f2845 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/statistics-exception.html @@ -0,0 +1,178 @@ + + + + + + + 空间管理 - 运营统计异常 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+

运营统计

+

审计结论是:路由能进入,但当前环境主体未稳定渲染。这里刻意做成显式异常页,而不是伪造一套正常统计图。

+
+
+ + 查看调试任务 +
+
+ +
+
+
近 7 日执行量
+ 1,284 +
+
+
失败率
+ 2.3% +
+
+
平均耗时
+ 46s +
+
+ +
+
+
● 统计应用异常
+

运营统计主体加载失败

+

`activeTab=statistics` 已切换成功,但主内容区在 stag 环境里几乎为空。审计时 console 持续出现外部可视化资源日志,因此该页需要单独建模为“入口存在但内容异常”的失败态。

+
+ + +
+
+
[statistics-loader] +route: /bkflow_engine_admin/?space_id=171&activeTab=statistics +status: PARTIAL_RENDER +message: external dashboard bundle did not mount + +console: +[info] trying to bootstrap chart app +[warn] missing runtime chunk for dashboard vendor +[error] chart root remains empty after hydration timeout
+
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-complete.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-complete.html new file mode 100644 index 0000000000..5894c6cb2b --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-complete.html @@ -0,0 +1,372 @@ + + + + + + + 空间管理 - 调试任务详情(完成) + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+
+ ← 返回调试任务列表 +
+ 任务执行 +
+

audit-deep-dive-20260420_调试任务_20260420203821

+ 完成 +
+

源调试模板 /template/mock/true/2166/ · 执行实例 #1192795 · 2026-04-20 20:38:21

+
+
+
+
+ 观察模式 + 只读观察 +
+
+ 耗时 + 小于 1 秒 +
+
+
+ +
+
+
+
+ 执行画布 + 节点单击后直接打开 `节点详情`,用于只读观察,不提供编辑控件。 +
+
+ 只读观察 + 100% +
+
+ +
+
+
+ 开始 +
+ + + + + +
+ 结束 +
+
+
+
+ + +
+
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-failed.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-failed.html new file mode 100644 index 0000000000..d9b038b546 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-detail-failed.html @@ -0,0 +1,389 @@ + + + + + + + 空间管理 - 调试任务详情(失败) + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
A
admin
+
+
+
+ + +
+
+
+
+
+
+ ← 返回调试任务列表 +
+ 任务执行 +
+

audit-deep-dive-20260420_调试任务_20260420204106

+ 失败 +
+

源调试模板 /template/mock/true/2166/ · 执行实例 #1192803 · 2026-04-20 20:41:06

+
+
+
+
+ 观察模式 + 失败取证 +
+
+ 终止节点 + 发布蓝鲸作业 +
+
+
+ +
+
+
+
+ 执行画布 + 失败态仍是只读观察页,单击节点直接切换节点详情与日志,不出现编辑控件。 +
+
+ 失败链路 + 100% +
+
+ +
+
+
+ 开始 +
+ + + + + +
+ 结束 +
+
+
+
+ + +
+
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-list.html new file mode 100644 index 0000000000..555f8f151b --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/task-list.html @@ -0,0 +1,167 @@ + + + + + + + 空间管理 - 任务 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
+
A
+ admin +
+
+
+
+ + +
+
+
+
+
+

任务

+

延续列表页母版,但主动作不是弹层而是跳到独立的引擎操作页 `/enginePanel/171/?type=task`。

+
+ +
+ +
+
+
+
+ +
+
+ 当前空间暂无正式任务实例 +
+
+
+ + + + + + + + + + + + + + + + + +
任务 ID任务名称执行状态所属模板 ID执行人开始时间更新时间
+
+
+ 暂无数据 +
当前空间没有正式任务记录,可通过「引擎操作」跳转到 task 类型请求面板发起排查。
+
+
+
+
+ + +
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/template-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/template-list.html new file mode 100644 index 0000000000..f59aaa1824 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/space/template-list.html @@ -0,0 +1,433 @@ + + + + + + + 空间管理 - 流程 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+ +
+
A
+ admin +
+
+
+ +
+ + +
+
+
+
+
+

流程

+

组合自空间管理壳子与列表页母版,保留真实审计中的「创建流程 / 新建任务 / 复制 / 删除」语义。

+
+
+ + +
+
+ +
+
+
+ +
+
+ 共 58 条,当前空间开启流程版本控制 +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
流程 ID流程名称状态当前版本所属作用域创建人更新人更新时间操作
2166 +
+ audit-deep-dive-20260420 + 最新创建后直接插回列表顶部,用于发布与调试链路审计 +
+
已发布 + + 1.0.0 + 最新 + + biz · 20050001adminadmin2026-04-20 20:31:15 + + + +
2147 +
+ blueking-release-prod + 生产环境应用发布与回滚编排 +
+
草稿 + + 1.3.2 + 待发布 + + cmdb_biz · 42ops_adminflow_bot2026-04-21 09:42:08 + + + +
2119 +
+ mock-api-regression + 用于联调 mockTask / decisionTable 能力的回归流 +
+
停用 + + 0.9.4 + 历史 + + project · bkflow-engine-admintestertester2026-04-18 16:10:55 + + + +
+
+ +
+ 共 58 条 + « + 1 + 2 + 3 + » +
+
+
+
+
+
+
+ +
+
+
+
+

创建流程

+ +
+
+
+
+ +
+ +
+
+
+ +
+
+
biz
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+

复制流程

+ +
+
+

是否复制该流程?

+
+ 关联 mock 数据不会同步复制,带决策表节点的流程暂不支持复制。
+ 当前页保留“复制包含的子流程”这一真实审计中的额外选项。 +
+ +
+ +
+
+ +
+
+
+
+

删除流程

+ +
+
+

确认删除流程 "audit-deep-dive-20260420"?

+
+ +
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/module-config-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/module-config-list.html new file mode 100644 index 0000000000..a47c516aa0 --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/module-config-list.html @@ -0,0 +1,458 @@ + + + + + + BKFlow 原型 - 系统管理 / 模块配置列表 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ admin_ops + +
+
+
+ +
+ + +
+
+
+
+ 首页 + 系统管理 + 模块配置列表 +
+ + + +
+
+
受管模块
+
36
+
涵盖核心流程、通知、调度、审计等模块。
+
+
+
待同步项
+
8
+
模块版本升级后尚未完成空间级分发。
+
+
+
冲突项
+
4
+
包含字段覆盖、版本不一致和权限不匹配。
+
+
+
只读基线
+
12
+
基线项只能查看,发布动作由平台审计统一控制。
+
+
+ +
+
+
+
模块配置清单
+
按模块追踪配置版本、同步状态、审核状态和最后一次发布结果,方便快速发布或回滚。
+
+
+
+
+ +
+
全部同步状态
+
+
+
全部审核状态
+
+
+
+ + +
+
+ +
+ 全部 + 已同步 + 待同步 + 冲突 + 已回滚 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模块名称模块编码配置版本适用范围同步状态审核状态最近更新时间负责人操作
审批引擎approval-enginev2.8.4系统级已同步已审批2026-04-20 18:40platform_ops + + + +
调度器scheduler-corev1.12.0全空间待同步待审批2026-04-21 08:12scheduler_bot + + + +
插件中心plugin-centerv3.1.2指定空间 8 个冲突待处理2026-04-19 16:21integrate_team + + + +
审计中心audit-hubv1.4.6系统级已回滚已审批2026-04-18 13:07audit_admin + + + +
模板引擎template-runtimev2.0.1全空间已同步已审批2026-04-17 20:16template_bot + + + +
+
+ +
+ 共 36 条 + « + 1 + 2 + 3 + » +
+
+
+ + +
+
+
+
+
+
+ + diff --git a/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/space-config-list.html b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/space-config-list.html new file mode 100644 index 0000000000..c9182c6a7b --- /dev/null +++ b/prototypes/features/bkflow-engine-admin-prototype-overhaul/pages/system/space-config-list.html @@ -0,0 +1,451 @@ + + + + + + BKFlow 原型 - 系统管理 / 空间配置列表 + + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ admin_audit + +
+
+
+ +
+ + +
+
+
+
+ 首页 + 系统管理 + 空间配置列表 +
+ + + +
+
+
受管空间
+
128
+
覆盖生产、灰度和演练空间,均可单独查看配置差异。
+
+
+
待审批变更
+
19
+
包含新建、修改和恢复默认值三类审计动作。
+
+
+
默认值保护项
+
42
+
默认值条目只读展示,恢复默认值后会回到保护态。
+
+
+
已冻结项
+
6
+
冻结项仅允许审计负责人解锁并重新发起审批。
+
+
+ +
+
+
+
空间配置清单
+
按空间查看配置项、当前值、默认值和审计状态,支持编辑、审批和恢复默认值。
+
+
+
+
+ +
+
全部审批状态
+
+
+
全部配置来源
+
+
+
+ + +
+
+ +
+ 全部 + 已生效 + 待审批 + 默认值 + 已冻结 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
配置名称所属空间值类型当前值 / 默认值审计状态更新时间更新人操作
task_timeout_secondsbkflow-eng-svc-prod整数30 / 30已生效2026-04-20 18:24platform_ops + + + +
notify_webhookbkflow-financeURLhttps://hooks.example.com/bkflow / https://hooks.example.com/bkflow待审批2026-04-21 09:08audit_admin + + + +
daily_publish_windowbkflow-ops-qa时间段02:00-04:00 / 01:00-05:00已冻结2026-04-19 15:42release_bot + + + +
debug_modebkflow-test布尔关闭 / 关闭默认值2026-04-18 11:05system_bot + + +
max_retry_countbkflow-shared整数5 / 3已生效2026-04-17 20:16admin_ops + + + +
+
+ +
+ 共 128 条 + « + 1 + 2 + 3 + » +
+
+
+ + +
+
+
+
+
+
+ + diff --git a/prototypes/masters/README.md b/prototypes/masters/README.md new file mode 100644 index 0000000000..2a7a6514bc --- /dev/null +++ b/prototypes/masters/README.md @@ -0,0 +1,28 @@ +# masters + +## Purpose + +`prototypes/masters/` is the long-lived source of truth for page-type masters and state templates. Put full page-type masters, reusable interaction shells, and stable state templates here. + +## Contract + +- `prototypes/masters/` is the source of truth for complete page-type masters and state templates. +- Use this directory for reusable full pages, interaction shells, and stable states. +- Do not put `_shared/` pages here. +- See `docs/specs/` and `docs/plans/` for feature scope; this tree stays reusable. + +## What goes here + +- Page-type masters that can be reused by multiple features. +- Shared shell fragments that are stable enough to become defaults. +- Reference states that help future feature pages stay consistent. + +## What should not go here + +- One-off feature pages tied to a single spec or plan. +- Historical snapshots that only exist for archive purposes. +- Temporary experiments that are not ready to be treated as a master. + +## Relationship to `docs/specs` and `docs/plans` + +`docs/specs/` defines the target behavior and `docs/plans/` tracks implementation steps for a single change. This directory holds the reusable master material that those documents can point to, but it should remain independent from any one spec or plan slug. diff --git a/prototypes/masters/_shared/README.md b/prototypes/masters/_shared/README.md new file mode 100644 index 0000000000..5a1d7ae1bc --- /dev/null +++ b/prototypes/masters/_shared/README.md @@ -0,0 +1,28 @@ +# _shared + +## Purpose + +`prototypes/masters/_shared/` stores shared shells and navigation scaffolding that multiple masters can reuse. It is not a page-type master and does not preview independently. + +## Contract + +- `prototypes/masters/_shared/` is shared scaffolding only and is not a page-type master. +- It does not preview independently. +- Keep navigation shells, structural fragments, and other reuse-only pieces here. +- Do not put delivery pages or feature-specific masters here. + +## What goes here + +- Shared layout shells. +- Cross-master navigation structure. +- Common structural fragments that are not business-specific. + +## What should not go here + +- Feature-specific pages. +- Archived examples. +- Anything that only makes sense for a single spec or plan. + +## Relationship to `docs/specs` and `docs/plans` + +`docs/specs/` and `docs/plans/` define the work that consumes these shared shells. The shared directory should stay generic so it can support many specs and plans without being rewritten each time. diff --git a/prototypes/masters/_shared/plugin-shell.html b/prototypes/masters/_shared/plugin-shell.html new file mode 100644 index 0000000000..54bb71acca --- /dev/null +++ b/prototypes/masters/_shared/plugin-shell.html @@ -0,0 +1,83 @@ + + + + + + BKFlow 原型 - 插件管理壳子 + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ 用户名 + +
+
+
+ +
+ + +
+
+

插件侧栏

+
+

共享壳子占位

+

插件管理壳子只保留顶栏、侧栏和占位内容。

+
+ + + +
+

插件管理壳子同样只包含通用结构与占位内容。

+
+
+
+
+
+ + diff --git a/prototypes/masters/_shared/space-shell.html b/prototypes/masters/_shared/space-shell.html new file mode 100644 index 0000000000..7c3f2f9256 --- /dev/null +++ b/prototypes/masters/_shared/space-shell.html @@ -0,0 +1,121 @@ + + + + + + BKFlow 原型 - 空间管理壳子 + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ 用户名 + +
+
+
+ +
+ + +
+
+

空间选择区

+
+

共享壳子占位

+

这里只保留壳子结构与可复用的通用交互,不放业务表格。

+
+ + + + +
+

页面只承载共享壳子、一级导航状态和占位内容。

+
+
+ + + + + + +
+
+
+ + + + diff --git a/prototypes/masters/_shared/system-shell.html b/prototypes/masters/_shared/system-shell.html new file mode 100644 index 0000000000..ee1412aa78 --- /dev/null +++ b/prototypes/masters/_shared/system-shell.html @@ -0,0 +1,91 @@ + + + + + + BKFlow 原型 - 系统管理壳子 + + + + +
+
+
+ BKFlow + BKFlow 原型 +
+
+
    +
  • 空间管理
  • +
  • 系统管理
  • +
  • 我的插件
  • +
+
+
U
+ 用户名 + +
+
+
+ +
+ + +
+
+

系统管理侧栏

+
+

共享壳子占位

+

系统管理壳子只展示顶栏、侧栏和占位内容。

+
+ + + +
+

系统管理壳子也只保留共享结构与占位内容。

+
+
+ + +
+
+
+ + diff --git a/prototypes/masters/config-page/README.md b/prototypes/masters/config-page/README.md new file mode 100644 index 0000000000..4e027a7f7f --- /dev/null +++ b/prototypes/masters/config-page/README.md @@ -0,0 +1,12 @@ +# config-page + +## Purpose + +`prototypes/masters/config-page/` is the reusable master for long configuration forms. It emphasizes a multi-section form, a code or editor block, a disabled save state in the baseline template, and a dirty state for unsaved changes. + +## What it covers + +- Long-running admin forms with several field groups. +- A code block or editor-style area for structured configuration. +- Disabled save behavior in the default template. +- Dirty-state treatment when the form has unsaved changes. diff --git a/prototypes/masters/config-page/states/dirty.html b/prototypes/masters/config-page/states/dirty.html new file mode 100644 index 0000000000..e68264005d --- /dev/null +++ b/prototypes/masters/config-page/states/dirty.html @@ -0,0 +1,115 @@ + + + + + + 配置页 - 脏状态 + + + + +
+ + +
+
基础配置
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
代码配置
+
+
+ +
+ +
+
+
+
+
runtime:
+  retries: 5
+  timeout_seconds: 45
+  env:
+    - name: BKFLOW_DEBUG
+      value: "true"
+
+ + +
+
+ + + + diff --git a/prototypes/masters/config-page/template.html b/prototypes/masters/config-page/template.html new file mode 100644 index 0000000000..9f4d167fb7 --- /dev/null +++ b/prototypes/masters/config-page/template.html @@ -0,0 +1,125 @@ + + + + + + 配置页 Master - BKFlow 原型 + + + + +
+

配置页 Master

+

用于多字段配置、结构化代码编辑和保存状态控制的长表单模板。

+ +
+
基础配置
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
代码配置
+
+
+ +
+ +
+
+
+
+
runtime:
+  retries: 3
+  timeout_seconds: 30
+  env:
+    - name: BKFLOW_DEBUG
+      value: "false"
+  resources:
+    cpu: "500m"
+    memory: "512Mi"
+
+ + +
+
+ + + + diff --git a/prototypes/masters/decision-editor/README.md b/prototypes/masters/decision-editor/README.md new file mode 100644 index 0000000000..a0bf404601 --- /dev/null +++ b/prototypes/masters/decision-editor/README.md @@ -0,0 +1,12 @@ +# decision-editor + +## Purpose + +`prototypes/masters/decision-editor/` is the reusable master for decision rule editing flows. It combines basic information, a rule configuration area, an empty-state treatment for rules, and the bottom action bar. + +## What it covers + +- Basic metadata and naming fields. +- Rule configuration and rule list layout. +- Empty-state presentation when no rules exist yet. +- Save and cancel actions anchored at the bottom. diff --git a/prototypes/masters/decision-editor/template.html b/prototypes/masters/decision-editor/template.html new file mode 100644 index 0000000000..f57f433e61 --- /dev/null +++ b/prototypes/masters/decision-editor/template.html @@ -0,0 +1,122 @@ + + + + + + 决策编辑器 Master - BKFlow 原型 + + + + +
+

决策编辑器 Master

+

用于配置决策表的基础信息、规则和底部动作区。

+ +
+
基础信息
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
规则配置
+
+
+ 暂无规则 +
当前还没有任何规则,先创建第一条规则开始配置。
+
+ + +
+
+ +
底部操作
+ +
+
+ + + + diff --git a/prototypes/masters/engine-panel/README.md b/prototypes/masters/engine-panel/README.md new file mode 100644 index 0000000000..ba407c2002 --- /dev/null +++ b/prototypes/masters/engine-panel/README.md @@ -0,0 +1,12 @@ +# engine-panel + +## Purpose + +`prototypes/masters/engine-panel/` is the reusable master for request/response debugging panels. It groups the request area, parameter area, response area, and the send/reset action bar into one static reference. + +## What it covers + +- Request body or request preview areas. +- Parameter and variable configuration sections. +- Response output presentation. +- Primary send action and reset action with a disabled send baseline. diff --git a/prototypes/masters/engine-panel/template.html b/prototypes/masters/engine-panel/template.html new file mode 100644 index 0000000000..72f2e3a245 --- /dev/null +++ b/prototypes/masters/engine-panel/template.html @@ -0,0 +1,161 @@ + + + + + + 调试面板 Master - BKFlow 原型 + + + + +
+

调试面板 Master

+

用于查看请求、调整参数、发送调用并阅读响应结果。

+ +
+
+
+
请求区
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
参数区
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+ +
+
响应区
+
+HTTP/1.1 200 OK +Content-Type: application/json + +{ + "code": 0, + "message": "success", + "data": { + "task_id": "task-1024", + "status": "running" + } +} +
+ +
+
headers:
+  x-request-id: req-9f4f
+  x-env: prod
+  x-cost-ms: 42
+
+
+
+
+ + + + diff --git a/prototypes/masters/flow-editor/README.md b/prototypes/masters/flow-editor/README.md new file mode 100644 index 0000000000..0dc8935b54 --- /dev/null +++ b/prototypes/masters/flow-editor/README.md @@ -0,0 +1,13 @@ +# flow-editor + +## Purpose + +`prototypes/masters/flow-editor/` is the reusable master for graph-style flow editing pages. It mirrors the audited BKFlow interaction split between the canvas, a double-click node configuration drawer, floating overlays, and the save / publish / debug header actions. + +## What it covers + +- A header with back entry, flow title, and version context. +- Canvas-first editing with single-click selection and a nearby floating toolbar. +- A double-click node configuration drawer grouped into `基础信息 / 输入参数 / 输出参数`. +- A global variables floating layer opened from the drawer instead of a full-page switch. +- Separate local drawer confirm and global save / publish / debug header actions. diff --git a/prototypes/masters/flow-editor/states/mock-debug-confirm.html b/prototypes/masters/flow-editor/states/mock-debug-confirm.html new file mode 100644 index 0000000000..51a2511d8c --- /dev/null +++ b/prototypes/masters/flow-editor/states/mock-debug-confirm.html @@ -0,0 +1,85 @@ + + + + + + 流程编辑状态 - 调试确认 + + + + +
+
+
+ ← 返回流程列表 +
+

编辑流程

+

audit-deep-dive-20260420 · 调试前确认保存 Mock 数据

+
+
+ 流程版本 + 草稿 + 最新发布 1.0.0 +
+
+
+ + + +
+
+ +
+ + +
+
+
+ 流程画布 + 调试会进入独立链路,这里先表达前置确认。 +
+
+
+
+
+
+ +
+
+
+
+

执行调试

+ +
+
+

确定保存Mock数据并去执行调试?

+

确认后会进入独立 mock 路由,并在新的执行环境里继续填写入参与查看历史调试。

+
+ +
+
+ +
+
+
+
+

发布流程

+ +
+
+
+ + diff --git a/prototypes/masters/flow-editor/states/node-selected.html b/prototypes/masters/flow-editor/states/node-selected.html new file mode 100644 index 0000000000..1573afee6c --- /dev/null +++ b/prototypes/masters/flow-editor/states/node-selected.html @@ -0,0 +1,117 @@ + + + + + + 流程编辑状态 - 节点已选中 + + + + +
+
+
+ ← 返回流程列表 +
+

编辑流程

+

audit-deep-dive-20260420 · 单击节点后的悬浮工具条

+
+
+ 流程版本 + 草稿 + 最新发布 1.0.0 +
+
+
+ + + +
+
+ +
+ + +
+
+
+ 流程画布 + 选中节点后展示节点悬浮工具条。 +
+
+
+
+
开始
+ + + + + +
结束
+
+ +
+ 发布蓝鲸作业 + + + + +
+
+
+
+
+ + + +
+
+
+
+

发布流程

+ +
+
+
+ +
+
+
+
+

执行调试

+ +
+
+
+ + diff --git a/prototypes/masters/flow-editor/states/publish-confirm.html b/prototypes/masters/flow-editor/states/publish-confirm.html new file mode 100644 index 0000000000..dcce3f2f71 --- /dev/null +++ b/prototypes/masters/flow-editor/states/publish-confirm.html @@ -0,0 +1,88 @@ + + + + + + 流程编辑状态 - 发布确认 + + + + +
+
+
+ ← 返回流程列表 +
+

编辑流程

+

audit-deep-dive-20260420 · 全局保存完成后进入发布确认

+
+
+ 流程版本 + 草稿 + 最新发布 1.0.0 +
+
+
+ + + +
+
+ +
+ + +
+
+
+ 流程画布 + 画布已保存,发布按钮进入可操作态。 +
+
+
+
+
+
+ +
+
+
+
+

发布流程

+ +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+

执行调试

+ +
+
+
+ + diff --git a/prototypes/masters/flow-editor/states/save-ready.html b/prototypes/masters/flow-editor/states/save-ready.html new file mode 100644 index 0000000000..b03ba516f3 --- /dev/null +++ b/prototypes/masters/flow-editor/states/save-ready.html @@ -0,0 +1,96 @@ + + + + + + 流程编辑状态 - 可保存 + + + + +
+
+
+ ← 返回流程列表 +
+

编辑流程

+

audit-deep-dive-20260420 · 抽屉局部确定后启用顶部保存

+
+
+ 流程版本 + 草稿 + 最新发布 1.0.0 +
+
+
+ + + +
+
+ +
+ + +
+
+
+ 流程画布 + 局部确认完成后,继续点击页面顶栏执行真正保存。 +
+
+
+
+
开始
+ + + + + +
结束
+
+ +
+ 发布蓝鲸作业 + + + + +
+
+
+
+
+ +
+
+
+
+

发布流程

+ +
+
+
+ +
+
+
+
+

执行调试

+ +
+
+
+ + diff --git a/prototypes/masters/flow-editor/template.html b/prototypes/masters/flow-editor/template.html new file mode 100644 index 0000000000..0e7b88a075 --- /dev/null +++ b/prototypes/masters/flow-editor/template.html @@ -0,0 +1,314 @@ + + + + + + 流程编辑 Master - BKFlow 原型 + + + + +
+
+
+ ← 返回流程列表 +
+

编辑流程

+

audit-deep-dive-20260420 · 发布蓝鲸作业审批链

+
+
+ 流程版本 + 草稿 + 最新发布 1.0.0 +
+
+
+ + + +
+
+ +
+ + +
+
+
+ 流程画布 + 单击查看节点悬浮工具条,双击进入配置抽屉。 +
+
+ - + 100% + + +
+
+ +
+
+
+ 开始 +
+ + + + + +
+ 结束 +
+
+ +
+ 节点悬浮工具条 + + + + +
+
+
+
+
+ + + +
+
+
+
+

全局变量

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
变量名说明示例值
_system.task_name调度时自动注入的任务名称audit-deep-dive-20260420
_system.task_id系统生成的任务 ID17100002166
_system.task_start_time任务启动时间2026-04-20 20:31:15
_system.operator当前操作人admin
+
+ +
+
+ +
+
+
+
+

发布流程

+ +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+

执行调试

+ +
+
+

确定保存Mock数据并去执行调试?

+

该 master 用确认层表达真实产品跳转到 `/template/mock/...` 前的最后一步校验。

+
+ +
+
+ + diff --git a/prototypes/masters/list-page/README.md b/prototypes/masters/list-page/README.md new file mode 100644 index 0000000000..bf1604458a --- /dev/null +++ b/prototypes/masters/list-page/README.md @@ -0,0 +1,13 @@ +# list-page + +## Purpose + +`prototypes/masters/list-page/` is the reusable master for list-oriented admin pages. It covers the standard toolbar, search and filter controls, status and operation columns, pagination, and the common empty, error, and bulk-action states. + +## What it covers + +- Table-based list pages with a clear toolbar. +- Search and filter placement near the table header. +- Status and operation columns for row-level actions. +- Pagination and selection-heavy bulk-action variants. +- Separate static states for empty results and loading failures. diff --git a/prototypes/masters/list-page/states/bulk-actions.html b/prototypes/masters/list-page/states/bulk-actions.html new file mode 100644 index 0000000000..fed060dc53 --- /dev/null +++ b/prototypes/masters/list-page/states/bulk-actions.html @@ -0,0 +1,116 @@ + + + + + + 列表页 - 批量操作状态 + + + + +
+
+
已选 3 条,可执行批量操作。
+
+ + + +
+
+ +
+
+
+ +
+
+ 筛选状态 +
+
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
选择名称类型状态更新时间操作
prod-api-key凭证启用2026-04-18 10:12
staging-webhook回调待审核2026-04-17 17:40
backup-rule规则禁用2026-04-15 09:08
+
+ +
+ 共 38 条 + « + 1 + 2 + » +
+
+
+ + + + diff --git a/prototypes/masters/list-page/states/empty.html b/prototypes/masters/list-page/states/empty.html new file mode 100644 index 0000000000..f94bebb40f --- /dev/null +++ b/prototypes/masters/list-page/states/empty.html @@ -0,0 +1,55 @@ + + + + + + 列表页 - 空状态 + + + + +
+
+
+ +
+
+ 筛选状态 +
+
+
+
+ +
+
+ +
+
+
+ 暂无数据 +
当前筛选条件下没有匹配结果。
+
+ + +
+
+
+
+ + + + diff --git a/prototypes/masters/list-page/states/error.html b/prototypes/masters/list-page/states/error.html new file mode 100644 index 0000000000..fba369560a --- /dev/null +++ b/prototypes/masters/list-page/states/error.html @@ -0,0 +1,55 @@ + + + + + + 列表页 - 错误状态 + + + + +
+
+
+ +
+
+ 筛选状态 +
+
+
+
+ +
+
+ +
+
+
!
+ 加载失败 +
列表数据获取失败,请稍后重试或检查网络。
+
+ + +
+
+
+
+ + + + diff --git a/prototypes/masters/list-page/template.html b/prototypes/masters/list-page/template.html new file mode 100644 index 0000000000..3aff875bfb --- /dev/null +++ b/prototypes/masters/list-page/template.html @@ -0,0 +1,131 @@ + + + + + + 列表页 Master - BKFlow 原型 + + + + +
+

列表页 Master

+

适合管理台中的列表、检索和批量操作场景。

+ +
+
+
+ +
+
+ 筛选状态 +
+
+
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
选择名称类型状态更新时间操作
prod-api-key凭证启用2026-04-18 10:12 + + +
staging-webhook回调待审核2026-04-17 17:40 + + +
backup-rule规则禁用2026-04-15 09:08 + + +
+
+ +
+ 共 38 条 + « + 1 + 2 + 3 + » +
+ +
上方 toolbar 适合放搜索和筛选,表格固定展示状态列与操作列。
+
+
+ + + + diff --git a/prototypes/masters/overlays/README.md b/prototypes/masters/overlays/README.md new file mode 100644 index 0000000000..12b6ff3b97 --- /dev/null +++ b/prototypes/masters/overlays/README.md @@ -0,0 +1,26 @@ +# overlays + +## Purpose + +`prototypes/masters/overlays/` collects reusable floating-layer masters for confirmations, error prompts, and side panels. + +## Contract + +- `dialogs.html` covers confirm and exception dialogs that can be reused by multiple features. +- `sidesliders.html` covers edit and detail side sliders. +- Keep this directory focused on floating-layer patterns only. + +## What goes here + +- Delete confirmations. +- Publish confirmations. +- Exception or warning prompts. +- Form edit side sliders. +- Read-only detail side sliders. + +## What should not go here + +- Page shells. +- Table/list content. +- Feature-specific business flow pages. + diff --git a/prototypes/masters/overlays/dialogs.html b/prototypes/masters/overlays/dialogs.html new file mode 100644 index 0000000000..c5bc842392 --- /dev/null +++ b/prototypes/masters/overlays/dialogs.html @@ -0,0 +1,84 @@ + + + + + + BKFlow 原型 - dialogs 母版 + + + + +
+

Dialogs 母版

+ +
+

删除确认

+

用于危险删除动作的标准确认弹窗。

+ +
+ +
+

发布确认

+

用于发布前的版本确认与描述补充。

+ +
+ +
+

异常提示

+

用于执行或保存失败时的全局异常提示。

+ +
+
+ +
+
+
+
+

删除确认

+ +
+
+ 确认删除该流程吗?删除后无法恢复。 +
+ +
+
+ +
+
+
+
+

发布确认

+ +
+
+

确认发布当前版本吗?

+

可在这里补充版本号、描述和校验信息。

+
+ +
+
+ +
+
+
+
+

异常提示

+ +
+
+ 应用出现异常,请稍后重试或联系管理员。 +
+ +
+
+ + diff --git a/prototypes/masters/overlays/sidesliders.html b/prototypes/masters/overlays/sidesliders.html new file mode 100644 index 0000000000..1a201a62ce --- /dev/null +++ b/prototypes/masters/overlays/sidesliders.html @@ -0,0 +1,76 @@ + + + + + + BKFlow 原型 - sidesliders 母版 + + + + +
+

Sidesliders 母版

+ +
+

表单编辑侧滑

+

用于右侧编辑表单,支持提交、取消和局部状态反馈。

+ +
+ +
+

只读详情侧滑

+

用于展示只读详情、日志快照或执行记录。

+ +
+
+ + + + + + diff --git a/prototypes/masters/task-detail/README.md b/prototypes/masters/task-detail/README.md new file mode 100644 index 0000000000..d9db6ea587 --- /dev/null +++ b/prototypes/masters/task-detail/README.md @@ -0,0 +1,14 @@ +# task-detail + +## Purpose + +`prototypes/masters/task-detail/` is the reusable master for the audited BKFlow execution detail route. It represents a read-only execution canvas with single-click node detail opening, a right-side audit surface, and configuration snapshot captured at execution time. + +## What it covers + +- An execution detail route header with `任务执行`, task name, status, and source-template backlink. +- A read-only execution canvas that stays distinct from `flow-editor/`. +- Single-click node detail opening for analysis instead of double-click editing. +- Observation tabs for `执行记录 / 配置快照 / 操作历史 / 调用日志`. +- A code-style log viewer and empty states for audit trails. +- Explicit separation from editing semantics: no save / publish / debug header actions. diff --git a/prototypes/masters/task-detail/states/complete.html b/prototypes/masters/task-detail/states/complete.html new file mode 100644 index 0000000000..256f2d700e --- /dev/null +++ b/prototypes/masters/task-detail/states/complete.html @@ -0,0 +1,136 @@ + + + + + + 任务详情完成态 - BKFlow 原型 + + + + +
+
+
+ ← 返回源模板 +
+ 任务执行 +
+

audit-deep-dive-20260420_调试任务_20260420203821

+ 完成 +
+

源模板 /template/view/2166/ · 执行实例 #1192795

+
+
+
+
+ 观察模式 + 只读观察 +
+
+
+ +
+
+
+
+ 执行画布 + 成功态展示已完成的节点运行轨迹。 +
+
+ 完成 + 100% +
+
+ +
+
+
+ 开始 +
+ + + +
+ 结束 +
+
+
+
+ + +
+
+ + diff --git a/prototypes/masters/task-detail/states/empty-log.html b/prototypes/masters/task-detail/states/empty-log.html new file mode 100644 index 0000000000..c292ecde2d --- /dev/null +++ b/prototypes/masters/task-detail/states/empty-log.html @@ -0,0 +1,108 @@ + + + + + + 任务详情空日志态 - BKFlow 原型 + + + + +
+
+
+ ← 返回源模板 +
+ 任务执行 +
+

audit-deep-dive-20260420_调试任务_20260420203821

+ 完成 +
+

源模板 /template/view/2166/ · 执行实例 #1192795

+
+
+
+
+ 日志状态 + 采集为空 +
+
+
+ +
+
+
+
+ 执行画布 + 调用日志为空时,仍保留代码查看器风格的阅读区域。 +
+
+ +
+
+
+ 开始 +
+ + + +
+ 结束 +
+
+
+
+ + +
+
+ + diff --git a/prototypes/masters/task-detail/states/failed.html b/prototypes/masters/task-detail/states/failed.html new file mode 100644 index 0000000000..eed2355c92 --- /dev/null +++ b/prototypes/masters/task-detail/states/failed.html @@ -0,0 +1,139 @@ + + + + + + 任务详情失败态 - BKFlow 原型 + + + + +
+
+
+ ← 返回源模板 +
+ 任务执行 +
+

audit-deep-dive-20260420_调试任务_20260420204109

+ 失败 +
+

源模板 /template/view/2166/ · 执行实例 #1192801

+
+
+
+
+ 失败原因 + 参数渲染异常 +
+
+
+ +
+
+
+
+ 执行画布 + 失败态强调异常节点与调用痕迹。 +
+
+ 失败 + 100% +
+
+ +
+
+
+ 开始 +
+ + + +
+ 结束 +
+
+
+
+ + +
+
+ + diff --git a/prototypes/masters/task-detail/template.html b/prototypes/masters/task-detail/template.html new file mode 100644 index 0000000000..fe51504e1b --- /dev/null +++ b/prototypes/masters/task-detail/template.html @@ -0,0 +1,312 @@ + + + + + + 任务详情 Master - BKFlow 原型 + + + + +
+
+
+ ← 返回源模板 +
+ 任务执行 +
+

audit-deep-dive-20260420_调试任务_20260420203821

+ 完成 +
+

源模板 /template/view/2166/ · 执行实例 #1192795 · 2026-04-20 20:38:21

+
+
+
+
+ 观察模式 + 只读观察 +
+
+ 耗时 + 小于 1 秒 +
+
+
+ +
+
+
+
+ 执行画布 + 节点单击后直接打开 `节点详情`,用于只读观察,不提供编辑控件。 +
+
+ 只读观察 + 100% +
+
+ +
+
+
+ 开始 +
+ + + + + +
+ 结束 +
+
+
+
+ + +
+
+ + diff --git a/prototypes/serve.py b/prototypes/serve.py new file mode 100644 index 0000000000..c606926fc5 --- /dev/null +++ b/prototypes/serve.py @@ -0,0 +1,578 @@ +#!/usr/bin/env python3 +"""Zero-dependency static preview server for BKFlow prototypes (stdlib only).""" + +from __future__ import annotations + +import argparse +from datetime import datetime +import json +from html import escape +import os +import sys +from http import HTTPStatus +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import Union +from urllib.parse import unquote, urlparse + +try: + from prototypes.showcase_catalog import collect_feature_catalog, collect_showcase_stats +except ImportError: # pragma: no cover - fallback for direct `cd prototypes && python serve.py` + from showcase_catalog import collect_feature_catalog, collect_showcase_stats + +PROTOTYPES_ROOT = os.path.dirname(os.path.abspath(__file__)) +ASSETS_DIR = os.path.join(PROTOTYPES_ROOT, "assets") +MASTERS_DIR = os.path.join(PROTOTYPES_ROOT, "masters") +FEATURES_DIR = os.path.join(PROTOTYPES_ROOT, "features") +EXAMPLES_DIR = os.path.join(PROTOTYPES_ROOT, "examples") +DEFAULT_COVER_THEME = "mixed-admin" +SUPPORTED_COVER_THEMES = { + "flow-editor": "Flow Editor", + "task-detail": "Task Detail", + "list-management": "List Management", + "config-panel": "Config Panel", + "mixed-admin": "Mixed Admin", +} + +# Injected before or ; polls /api/mtime every 1s and reloads on change. +_RELOAD_SCRIPT = ( + b"" +) + + +def _is_safe_relpath(rel: str) -> bool: + if not rel or rel.startswith("/"): + return False + parts = rel.replace("\\", "/").split("/") + return ".." not in parts + + +def _under_root(full: str, root: str) -> bool: + full_abs = os.path.abspath(full) + root_abs = os.path.abspath(root) + try: + os.path.commonpath([full_abs, root_abs]) + except ValueError: + return False + return full_abs == root_abs or full_abs.startswith(root_abs + os.sep) + + +def latest_mtime_under(bases: tuple[str, ...]) -> float: + """Latest mtime among non-hidden files under given directories.""" + latest = 0.0 + for base in bases: + if not os.path.isdir(base): + continue + for dirpath, dirnames, filenames in os.walk(base): + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + try: + dir_mtime = os.path.getmtime(dirpath) + except OSError: + dir_mtime = None + if dir_mtime is not None and dir_mtime > latest: + latest = dir_mtime + for name in filenames: + if name.startswith("."): + continue + path = os.path.join(dirpath, name) + try: + m = os.path.getmtime(path) + except OSError: + continue + if m > latest: + latest = m + return latest + + +def list_html_files() -> list[tuple[str, str]]: + """Return [(section, url_path), ...] for *.html under masters/features/examples.""" + found: list[tuple[str, str]] = [] + mapping = (("masters", MASTERS_DIR), ("features", FEATURES_DIR), ("examples", EXAMPLES_DIR)) + for section, base in mapping: + if not os.path.isdir(base): + continue + for dirpath, dirnames, filenames in os.walk(base): + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + for name in sorted(filenames): + if not name.lower().endswith(".html"): + continue + full = os.path.join(dirpath, name) + rel = os.path.relpath(full, PROTOTYPES_ROOT) + url_path = rel.replace(os.sep, "/") + found.append((section, url_path)) + found.sort(key=lambda x: (x[0], x[1])) + return found + + +def inject_reload_script(html: bytes) -> bytes: + low = html.lower() + idx = low.rfind(b"") + if idx != -1: + return html[:idx] + _RELOAD_SCRIPT + html[idx:] + idx = low.rfind(b"") + if idx != -1: + return html[:idx] + _RELOAD_SCRIPT + html[idx:] + return html + _RELOAD_SCRIPT + + +def _format_timestamp(value: float) -> str: + if not value: + return "暂无更新" + return datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M") + + +def _format_stat_value(value: object) -> str: + if value is None: + return "-" + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + return str(value) + + +def _normalize_cover_theme(theme: object) -> str: + value = str(theme or DEFAULT_COVER_THEME).strip().lower().replace("_", "-") + return value if value in SUPPORTED_COVER_THEMES else DEFAULT_COVER_THEME + + +def _collect_filter_options(features: list[dict]) -> tuple[list[str], list[str]]: + statuses: list[str] = [] + tags: list[str] = [] + for feature in features: + status = str(feature.get("status") or "").strip() + if status and status not in statuses: + statuses.append(status) + feature_tags = feature.get("tags", []) + if not isinstance(feature_tags, list): + continue + for tag in feature_tags: + tag_value = str(tag).strip() + if tag_value and tag_value not in tags: + tags.append(tag_value) + return statuses, tags[:6] + + +def _render_cover_theme_preview(theme: str) -> str: + normalized = _normalize_cover_theme(theme) + if normalized == "flow-editor": + return ( + '
' + '
' + '' + '
' + '表单' + '' + '生成' + '' + '审核' + "
" + "
" + f'{SUPPORTED_COVER_THEMES[normalized]}' + "
" + ) + if normalized == "task-detail": + return ( + '
' + '
' + '
' + '' + "
" + '
' + '' + '' + '' + "
" + "
" + f'{SUPPORTED_COVER_THEMES[normalized]}' + "
" + ) + if normalized == "list-management": + return ( + '
' + '
' + '
' + '' + "
" + '
' + '' + '' + '' + '' + "
" + f'{SUPPORTED_COVER_THEMES[normalized]}' + "
" + ) + if normalized == "config-panel": + return ( + '
' + '
' + '' + '' + '' + '' + '' + "
" + f'{SUPPORTED_COVER_THEMES[normalized]}' + "
" + ) + return ( + '
' + '
' + '' + '' + '' + "
" + f'{SUPPORTED_COVER_THEMES[normalized]}' + "
" + ) + + +def _render_filter_buttons(label: str, options: list[str], group: str) -> str: + buttons = [ + f'' + ] + for option in options: + buttons.append( + f'' + ) + return "".join(buttons) + + +def _render_feature_pages(feature: dict) -> str: + slug = escape(str(feature.get("slug") or "")) + pages = feature.get("featured_pages", []) + if not pages: + return "

暂无代表页面。

" + items = [] + for page in pages: + title = escape(str(page.get("title") or page.get("path") or "")) + summary = escape(str(page.get("summary") or "")) + path = escape(str(page.get("path") or "")) + items.append( + "
  • " + f'{title}' + f'{summary or path}' + "
  • " + ) + return "
      " + "".join(items) + "
    " + + +def _render_feature_card(feature: dict) -> str: + slug = escape(str(feature.get("slug") or "")) + title = escape(str(feature.get("title") or slug)) + summary = escape(str(feature.get("summary") or "")) + status = escape(str(feature.get("status") or "")) + cover_theme = _normalize_cover_theme(feature.get("cover_theme") or feature.get("coverTheme")) + cover_label = escape(cover_theme.replace("-", " ").title()) + updated_at = _format_timestamp(float(feature.get("updated_at") or 0)) + page_count = _format_stat_value(feature.get("page_count")) + tags = feature.get("tags", []) + tag_values: list[str] = [] + search_terms = [title, summary, status, slug] + tag_html = "" + if isinstance(tags, list) and tags: + tag_values = [str(tag).strip() for tag in tags if str(tag).strip()] + search_terms.extend(tag_values) + tag_html = "
    " + "".join( + f'{escape(tag)}' for tag in tag_values + ) + "
    " + featured_pages = feature.get("featured_pages", []) + if isinstance(featured_pages, list): + for page in featured_pages: + if not isinstance(page, dict): + continue + page_title = str(page.get("title") or page.get("path") or "").strip() + page_summary = str(page.get("summary") or "").strip() + page_type = str(page.get("page_type") or "").strip() + if page_title: + search_terms.append(page_title) + if page_summary: + search_terms.append(page_summary) + if page_type: + search_terms.append(page_type) + return ( + f'
    ' + f"{_render_cover_theme_preview(cover_theme)}" + f'

    {title}

    {status}
    ' + f'

    {cover_label}

    ' + f"

    {summary}

    " + f"
    " + f"
    Slug
    {slug}
    " + f"
    页面数
    {page_count}
    " + f"
    最近更新
    {updated_at}
    " + f"
    " + f"{tag_html}" + f'' + f"{_render_feature_pages(feature)}" + "
    " + ) + + +def _render_recent_updates(features: list[dict]) -> str: + if not features: + return '

    暂无最近更新的原型。

    ' + items = [] + for feature in sorted(features, key=lambda item: (item.get("updated_at") or 0, str(item.get("slug") or "")), reverse=True)[:3]: + title = escape(str(feature.get("title") or feature.get("slug") or "")) + summary = escape(str(feature.get("summary") or "")) + updated_at = _format_timestamp(float(feature.get("updated_at") or 0)) + items.append( + "
  • " + f'{title}' + f"{summary}" + f"{updated_at}" + "
  • " + ) + return "
      " + "".join(items) + "
    " + + +def _render_representative_pages(features: list[dict]) -> str: + groups = [] + for feature in features: + feature_title = escape(str(feature.get("title") or feature.get("slug") or "")) + feature_summary = escape(str(feature.get("summary") or "")) + page_items = [] + for page in feature.get("featured_pages", [])[:2]: + if not isinstance(page, dict): + continue + page_title = escape(str(page.get("title") or page.get("path") or "")) + page_summary = escape(str(page.get("summary") or "")) + page_type = escape(str(page.get("page_type") or "")) + page_path = escape(str(page.get("path") or "")) + meta_bits = [f'所属 Feature:{feature_title}'] + if page_type: + meta_bits.append(f'{page_type}') + if page_summary: + meta_bits.append(f'{page_summary}') + page_items.append( + "
  • " + f'{page_title}' + f'
    {"".join(meta_bits)}
    ' + "
  • " + ) + if not page_items: + continue + groups.append( + "
    " + '
    ' + '

    所属 Feature

    ' + f"

    {feature_title}

    " + f'

    {feature_summary}

    ' + "
    " + '
      ' + + "".join(page_items) + + "
    " + "
    " + ) + if not groups: + return '

    暂无代表页面。

    ' + return "
    " + "".join(groups) + "
    " + + +def _render_tool_resources() -> str: + resources = [ + ("masters/", "长期母版入口"), + ("features/", "需求原型目录"), + ("examples/component-showcase.html", "组件参考样例"), + ("serve.py", "本地预览服务器"), + ("showcase_catalog.py", "展厅数据聚合"), + ] + items = "".join( + f'
  • {escape(path)}{escape(label)}
  • ' + for path, label in resources + ) + return f'
      {items}
    ' + + +def _render_homepage() -> bytes: + features = collect_feature_catalog(FEATURES_DIR) + stats = collect_showcase_stats(FEATURES_DIR, MASTERS_DIR) + statuses, tags = _collect_filter_options(features) + stat_items = [ + ("Feature 目录", stats.get("feature_count")), + ("Feature 页面", stats.get("feature_page_count")), + ("母版页面", stats.get("master_page_count")), + ("HTML 总数", stats.get("html_page_count")), + ] + stat_html = "".join( + f'
    {escape(label)}{escape(_format_stat_value(value))}
    ' + for label, value in stat_items + ) + featured_cards = "".join(_render_feature_card(feature) for feature in features) or "" + feature_empty = '' + feature_summary = f'

    当前展示 {len(features)} 个 Feature

    ' + status_filters = _render_filter_buttons("全部状态", statuses, "status") + tag_filters = _render_filter_buttons("全部标签", tags, "tag") + recent_updates = _render_recent_updates(features) + representative_pages = _render_representative_pages(features) + + lines = [ + "", + '', + "", + '', + "BKFlow 原型展厅", + '', + '', + "", + "", + '
    ', + '
    ', + '

    Prototype Showcase

    ', + "

    BKFlow 原型展厅

    ", + "

    features 是首页主角,masters 和 examples 则作为次级资源区。你可以先看最近更新,再进入具体 Feature,最后打开代表页面或工具资源继续搭建。

    ", + '', + f'
    {stat_html}
    ', + "
    ", + '
    ', + "

    最近更新

    ", + recent_updates, + "
    ", + '
    ', + "

    Feature 展区

    ", + '
    ', + '", + '
    ', + '
    ', + '状态', + f'
    {status_filters}
    ', + "
    ", + '
    ', + '标签', + f'
    {tag_filters}
    ', + "
    ", + "
    ", + "
    ", + feature_summary, + '
    ', + featured_cards, + "
    ", + feature_empty, + "
    ", + '
    ', + "

    代表页面

    ", + representative_pages, + "
    ", + '
    ', + "

    工具资源

    ", + _render_tool_resources(), + "
    ", + "
    ", + "", + "", + ] + return inject_reload_script("\n".join(lines).encode("utf-8")) + + +def _json_mtime_value(m: float) -> Union[int, float]: + """Encode mtime: integer 0 for no files, else preserve sub-second if needed.""" + if m == 0.0: + return 0 + return int(m) if m == int(m) else m + + +class PrototypeRequestHandler(SimpleHTTPRequestHandler): + """Serves prototypes/, index at /, mtime API, HTML auto-reload injection.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=PROTOTYPES_ROOT, **kwargs) + + def log_message(self, format: str, *args) -> None: # noqa: A003 + sys.stderr.write("{} - - [{}] {}\n".format(self.address_string(), self.log_date_time_string(), format % args)) + + def do_GET(self) -> None: # noqa: N802 + parsed = urlparse(self.path) + path = unquote(parsed.path) + norm = path if path.endswith("/") or path == "" else path.rstrip("/") + if path == "/" or path == "": + self._send_index() + return + if norm == "/api/mtime": + self._send_mtime_json() + return + + rel = path.lstrip("/") + if not _is_safe_relpath(rel): + self.send_error(HTTPStatus.FORBIDDEN) + return + full = os.path.normpath(os.path.join(PROTOTYPES_ROOT, rel)) + if os.path.isdir(full): + index_html = os.path.join(full, "index.html") + if os.path.isfile(index_html): + self._send_html_with_reload(index_html) + return + if not _under_root(full, PROTOTYPES_ROOT) or not os.path.isfile(full): + super().do_GET() + return + if full.lower().endswith(".html"): + self._send_html_with_reload(full) + return + super().do_GET() + + def _send_index(self) -> None: + body = _render_homepage() + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _send_mtime_json(self) -> None: + m = latest_mtime_under((MASTERS_DIR, FEATURES_DIR, EXAMPLES_DIR, ASSETS_DIR)) + payload = json.dumps({"mtime": _json_mtime_value(m)}).encode("utf-8") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def _send_html_with_reload(self, full_path: str) -> None: + try: + with open(full_path, "rb") as f: + raw = f.read() + except OSError: + self.send_error(HTTPStatus.NOT_FOUND) + return + body = inject_reload_script(raw) + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Serve BKFlow prototypes with live reload.") + parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)") + parser.add_argument("--port", type=int, default=9080, help="Port (default: 9080)") + args = parser.parse_args() + server = HTTPServer((args.host, args.port), PrototypeRequestHandler) + print( + f"Serving prototypes from {PROTOTYPES_ROOT} at http://{args.host}:{args.port}/", + file=sys.stderr, + ) + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down.", file=sys.stderr) + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/prototypes/showcase_catalog.py b/prototypes/showcase_catalog.py new file mode 100644 index 0000000000..bbb51a6524 --- /dev/null +++ b/prototypes/showcase_catalog.py @@ -0,0 +1,167 @@ +"""Catalog helpers for the BKFlow prototypes showcase homepage.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +DEFAULT_STATUS = "待补充" +DEFAULT_SUMMARY = "尚未补充首页摘要" +DEFAULT_THEME = "mixed-admin" + +_DEFAULT_ORDER = 10**9 +_REPRESENTATIVE_PAGE_LIMIT = 2 + + +def _safe_read_json(path: Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError, UnicodeDecodeError): + return {} + return data if isinstance(data, dict) else {} + + +def _humanize_slug(slug: str) -> str: + return slug.replace("-", " ").replace("_", " ").strip().title() or slug + + +def _iter_feature_dirs(features_dir: Path) -> list[Path]: + if not features_dir.is_dir(): + return [] + return [ + item + for item in sorted(features_dir.iterdir(), key=lambda path: path.name) + if item.is_dir() and not item.name.startswith("_") and not item.name.startswith(".") + ] + + +def _iter_html_pages(root: Path) -> list[Path]: + if not root.is_dir(): + return [] + pages = [path for path in root.rglob("*.html") if path.is_file()] + pages.sort(key=lambda path: str(path.relative_to(root)).replace(os.sep, "/")) + return pages + + +def _latest_mtime(paths: list[Path]) -> float: + latest = 0.0 + for path in paths: + try: + mtime = path.stat().st_mtime + except OSError: + continue + if mtime > latest: + latest = mtime + return latest + + +def _resolve_feature_page(feature_dir: Path, rel_path: str) -> Path | None: + candidate = (feature_dir / rel_path).resolve() + try: + feature_root = feature_dir.resolve() + except OSError: + return None + try: + candidate.relative_to(feature_root) + except ValueError: + return None + if not candidate.is_file(): + return None + return candidate + + +def _normalize_feature_page(feature_dir: Path, page: dict[str, Any]) -> dict[str, Any] | None: + rel_path = str(page.get("path") or "").strip() + if not rel_path: + return None + resolved = _resolve_feature_page(feature_dir, rel_path) + if resolved is None: + return None + title = str(page.get("title") or resolved.stem).strip() or resolved.stem + summary = str(page.get("summary") or "").strip() + page_type = str(page.get("pageType") or page.get("page_type") or "").strip() + normalized = { + "path": rel_path.replace("\\", "/"), + "title": title, + "summary": summary, + "page_type": page_type, + } + return normalized + + +def _default_feature_pages(feature_dir: Path) -> list[dict[str, Any]]: + pages = [] + for path in _iter_html_pages(feature_dir)[:_REPRESENTATIVE_PAGE_LIMIT]: + rel_path = path.relative_to(feature_dir).as_posix() + pages.append( + { + "path": rel_path, + "title": path.stem, + "summary": "", + "page_type": "", + } + ) + return pages + + +def collect_feature_catalog(features_dir: str) -> list[dict]: + """Collect normalized showcase data for feature directories.""" + + base_dir = Path(features_dir) + catalog: list[dict[str, Any]] = [] + + for feature_dir in _iter_feature_dirs(base_dir): + meta_path = feature_dir / "feature.meta.json" + meta = _safe_read_json(meta_path) if meta_path.is_file() else {} + html_pages = _iter_html_pages(feature_dir) + valid_featured_pages: list[dict[str, Any]] = [] + + featured_pages = meta.get("featuredPages", []) + for page in featured_pages if isinstance(featured_pages, list) else []: + if not isinstance(page, dict): + continue + normalized_page = _normalize_feature_page(feature_dir, page) + if normalized_page is not None: + valid_featured_pages.append(normalized_page) + + if not valid_featured_pages: + valid_featured_pages = _default_feature_pages(feature_dir) + + catalog.append( + { + "slug": feature_dir.name, + "title": str(meta.get("title") or _humanize_slug(feature_dir.name)).strip() or feature_dir.name, + "summary": str(meta.get("summary") or DEFAULT_SUMMARY).strip() or DEFAULT_SUMMARY, + "status": str(meta.get("status") or DEFAULT_STATUS).strip() or DEFAULT_STATUS, + "cover_theme": str(meta.get("coverTheme") or DEFAULT_THEME).strip() or DEFAULT_THEME, + "order": meta.get("order") if isinstance(meta.get("order"), int) else _DEFAULT_ORDER, + "tags": list(meta.get("tags")) if isinstance(meta.get("tags"), list) else [], + "page_count": len(html_pages), + "updated_at": _latest_mtime(html_pages + [meta_path] if meta_path.is_file() else html_pages), + "featured_pages": valid_featured_pages, + } + ) + + catalog.sort(key=lambda item: (item["order"], -item["updated_at"], item["slug"])) + return catalog + + +def collect_showcase_stats(features_dir: str, masters_dir: str) -> dict: + """Collect lightweight showcase summary metrics.""" + + features_root = Path(features_dir) + masters_root = Path(masters_dir) + feature_dirs = _iter_feature_dirs(features_root) + feature_pages = [path for feature_dir in feature_dirs for path in _iter_html_pages(feature_dir)] + meta_files = [feature_dir / "feature.meta.json" for feature_dir in feature_dirs if (feature_dir / "feature.meta.json").is_file()] + master_pages = _iter_html_pages(masters_root) + + return { + "feature_count": len(feature_dirs), + "feature_page_count": len(feature_pages), + "master_page_count": len(master_pages), + "html_page_count": len(feature_pages) + len(master_pages), + "updated_at": max(_latest_mtime(feature_pages), _latest_mtime(meta_files), _latest_mtime(master_pages)), + } diff --git a/tests/interface/prototypes/__init__.py b/tests/interface/prototypes/__init__.py new file mode 100644 index 0000000000..242f53f75c --- /dev/null +++ b/tests/interface/prototypes/__init__.py @@ -0,0 +1 @@ +"""Prototype interface tests.""" diff --git a/tests/interface/prototypes/test_bkflow_engine_admin_feature.py b/tests/interface/prototypes/test_bkflow_engine_admin_feature.py new file mode 100644 index 0000000000..911df99cee --- /dev/null +++ b/tests/interface/prototypes/test_bkflow_engine_admin_feature.py @@ -0,0 +1,206 @@ +import re +from pathlib import Path +from typing import Dict, Optional, Tuple + +ROOT = Path("prototypes/features/bkflow-engine-admin-prototype-overhaul") +README = ROOT / "README.md" +INDEX = ROOT / "index.html" +ASSET_JS = Path("prototypes/assets/bkflow-prototype.js") + +SPACE_PAGES = [ + "template-list.html", + "flow-view.html", + "flow-edit.html", + "flow-debug.html", + "task-list.html", + "debug-task-list.html", + "task-detail-complete.html", + "task-detail-failed.html", + "decision-list.html", + "decision-editor.html", + "space-config.html", + "credential-list.html", + "label-list.html", + "statistics-exception.html", +] +SYSTEM_PAGES = [ + "space-config-list.html", + "module-config-list.html", +] +PLUGIN_PAGES = [ + "plugin-list.html", +] + +EXPECTED_PAGE_FILES = { + "Space": [ROOT / "pages" / "space" / name for name in SPACE_PAGES], + "System": [ROOT / "pages" / "system" / name for name in SYSTEM_PAGES], + "Plugin": [ROOT / "pages" / "plugin" / name for name in PLUGIN_PAGES], +} + +EXPECTED_SECTION_LINKS = { + "Space": [f"./pages/space/{name}" for name in SPACE_PAGES], + "System": [f"./pages/system/{name}" for name in SYSTEM_PAGES], + "Plugin": [f"./pages/plugin/{name}" for name in PLUGIN_PAGES], +} + +CRITICAL_INDEX_LINKS = [ + "./pages/space/flow-edit.html", + "./pages/space/task-detail-complete.html", + "./pages/system/module-config-list.html", + "./pages/plugin/plugin-list.html", +] + +REPRESENTATIVE_PAGES = [ + ROOT / "pages" / "space" / "flow-edit.html", + ROOT / "pages" / "space" / "task-detail-complete.html", + ROOT / "pages" / "system" / "module-config-list.html", + ROOT / "pages" / "plugin" / "plugin-list.html", +] + +TOP_NAV_LANDING_LINKS = { + "space": { + "space": "template-list.html", + "system": "../system/space-config-list.html", + "plugin": "../plugin/plugin-list.html", + }, + "system": { + "space": "../space/template-list.html", + "system": "space-config-list.html", + "plugin": "../plugin/plugin-list.html", + }, + "plugin": { + "space": "../space/template-list.html", + "system": "../system/space-config-list.html", + "plugin": "plugin-list.html", + }, +} + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def extract_sections(index_text: str) -> dict[str, str]: + pattern = re.compile( + r'
    .*?

    (Space|System|Plugin)

    .*?
      (.*?)
    ', + re.S, + ) + return {title: body for title, body in pattern.findall(index_text)} + + +def extract_top_nav_links(page_text: str) -> Dict[str, Tuple[Optional[str], bool]]: + items: Dict[str, Tuple[Optional[str], bool]] = {} + pattern = re.compile( + r'[^>]*)class="[^"]*bk-navigation-header-nav-item[^"]*"(?P[^>]*)>(?P.*?)', + re.S, + ) + for match in pattern.finditer(page_text): + attrs = match.group(0) + group_match = re.search(r'data-nav-group="([^"]+)"', attrs) + if not group_match: + continue + nav_link_match = re.search(r'data-nav-link="([^"]+)"', attrs) + items[group_match.group(1)] = ( + nav_link_match.group(1) if nav_link_match else None, + re.search(r'class="[^"]*\bactive\b', attrs) is not None, + ) + return items + + +def count_readme_masters(readme_text: str) -> int: + match = re.search(r"## Masters Used\n(.*?)\n## Page Entries", readme_text, re.S) + assert match, "Masters Used section missing" + return len(re.findall(r"^- `", match.group(1), re.M)) + + +def test_feature_readme_links_spec_and_plan(): + text = read_text(README) + + assert "Spec: `docs/specs/2026-04-21-bkflow-engine-admin-prototype-overhaul-design.md`" in text + assert "Plan: `docs/plans/2026-04-21-bkflow-engine-admin-prototype-overhaul.md`" in text + + +def test_feature_index_groups_pages_by_space_system_and_plugin(): + text = read_text(INDEX) + sections = extract_sections(text) + + assert set(sections) == {"Space", "System", "Plugin"} + + for group, expected_links in EXPECTED_SECTION_LINKS.items(): + section = sections[group] + for link in expected_links: + assert link in section, (group, link) + + +def test_expected_feature_pages_exist(): + for group, paths in EXPECTED_PAGE_FILES.items(): + for path in paths: + assert path.is_file(), (group, path) + + +def test_index_links_to_critical_representative_pages(): + text = read_text(INDEX) + + for href in CRITICAL_INDEX_LINKS: + assert href in text, href + + +def test_representative_pages_define_cross_page_top_nav_targets_and_shared_js_supports_them(): + js = read_text(ASSET_JS) + + for token in [ + 'getAttribute("data-nav-link")', + "window.location.href = navLink", + 'navItem.classList.contains("active")', + ]: + assert token in js + + for path in REPRESENTATIVE_PAGES: + page_text = read_text(path) + nav_items = extract_top_nav_links(page_text) + group_name = path.parent.name + + assert set(nav_items) == {"space", "system", "plugin"}, path + assert sum(1 for _, is_active in nav_items.values() if is_active) == 1, path + + for group, expected_link in TOP_NAV_LANDING_LINKS[group_name].items(): + actual_link, _ = nav_items[group] + assert actual_link == expected_link, (path, group, actual_link, expected_link) + + +def test_debug_task_detail_pages_preserve_debug_route_backlink_and_source_template_context(): + expected_tokens = [ + 'class="bk-task-detail-back" href="debug-task-list.html">← 返回调试任务列表', + '源调试模板 /template/mock/true/2166/', + ] + + for path in [ + ROOT / "pages" / "space" / "task-detail-complete.html", + ROOT / "pages" / "space" / "task-detail-failed.html", + ]: + text = read_text(path) + for token in expected_tokens: + assert token in text, (path, token) + + +def test_index_hero_master_count_matches_feature_readme_inventory(): + readme_text = read_text(README) + index_text = read_text(INDEX) + + master_count = count_readme_masters(readme_text) + + assert master_count == 11 + assert f"Masters {master_count} referenced" in index_text + + +def test_representative_pages_use_nested_asset_paths(): + expected_tokens = [ + 'href="../../../../assets/bkflow-prototype.css"', + 'src="../../../../assets/bkflow-prototype.js"', + 'src="../../../../assets/icons/bkflow-logo.svg"', + ] + + for path in REPRESENTATIVE_PAGES: + text = read_text(path) + for token in expected_tokens: + assert token in text, (path, token) diff --git a/tests/interface/prototypes/test_flow_editor_contract.py b/tests/interface/prototypes/test_flow_editor_contract.py new file mode 100644 index 0000000000..64a9b4a5d5 --- /dev/null +++ b/tests/interface/prototypes/test_flow_editor_contract.py @@ -0,0 +1,149 @@ +from pathlib import Path +import re + + +ROOT = Path("prototypes") +FLOW_EDITOR = ROOT / "masters" / "flow-editor" +STATES = FLOW_EDITOR / "states" +ASSETS = ROOT / "assets" + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def test_flow_editor_master_files_exist(): + required = [ + FLOW_EDITOR / "README.md", + FLOW_EDITOR / "template.html", + STATES / "node-selected.html", + STATES / "save-ready.html", + STATES / "publish-confirm.html", + STATES / "mock-debug-confirm.html", + ] + + for path in required: + assert path.is_file(), path + + +def test_flow_editor_readme_describes_canvas_drawer_and_release_flow(): + text = read_text(FLOW_EDITOR / "README.md") + + for token in [ + "graph-style flow editing pages", + "single-click selection", + "double-click node configuration drawer", + "global variables floating layer", + "save / publish / debug header actions", + ]: + assert token in text + + +def test_flow_editor_template_covers_header_canvas_toolbar_drawer_and_confirm_layers(): + template = read_text(FLOW_EDITOR / "template.html") + + for token in [ + "编辑流程", + "返回流程列表", + "流程版本", + "草稿", + "1.0.0", + "保存", + "发布", + "调试", + "流程画布", + "节点悬浮工具条", + "节点配置", + "基础信息", + "输入参数", + "输出参数", + "全局变量", + "_system.task_name", + "_system.task_id", + "版本描述", + "确定保存Mock数据并去执行调试?", + 'data-node-select="flow-editor-main"', + 'data-node-toolbar="#flow-node-toolbar"', + 'data-dblopen="flow-node-config-drawer"', + 'data-save-target="#flow-editor-save"', + 'data-disable-target="#flow-editor-save"', + 'data-enable-target="#flow-editor-publish"', + 'data-open="flow-publish-dialog"', + 'data-open="flow-mock-debug-dialog"', + 'data-open="flow-global-vars-dialog"', + ]: + assert token in template + + +def test_flow_editor_state_pages_capture_selected_save_ready_publish_and_mock_debug_steps(): + node_selected = read_text(STATES / "node-selected.html") + save_ready = read_text(STATES / "save-ready.html") + publish_confirm = read_text(STATES / "publish-confirm.html") + mock_debug_confirm = read_text(STATES / "mock-debug-confirm.html") + + assert "bk-flow-node is-selected" in node_selected + assert "bk-flow-node-toolbar is-visible" in node_selected + assert "flow-node-config-drawer" in node_selected + assert "bk-sideslider is-show" not in node_selected + assert 'data-open="flow-mock-debug-dialog"' in node_selected + assert ">发布蓝鲸作业" in node_selected + assert ">节点悬浮工具条" not in node_selected + + assert "bk-save-button is-enabled" in save_ready + assert 'id="flow-editor-publish"' in save_ready + assert "is-disabled" in save_ready + + assert 'id="flow-publish-dialog"' in publish_confirm + assert "bk-dialog is-show" in publish_confirm + assert "1.0.0" in publish_confirm + assert "版本描述" in publish_confirm + + assert 'id="flow-mock-debug-dialog"' in mock_debug_confirm + assert "bk-dialog is-show" in mock_debug_confirm + assert "确定保存Mock数据并去执行调试?" in mock_debug_confirm + assert 'data-open="flow-mock-debug-dialog"' in mock_debug_confirm + + publish_button = re.search( + r']*id="flow-editor-publish"[^>]*>', + mock_debug_confirm, + re.DOTALL, + ) + assert publish_button, mock_debug_confirm + assert "is-disabled" in publish_button.group(0) + assert "disabled" in publish_button.group(0) + + +def test_shared_assets_expose_minimal_flow_editor_hooks(): + css = read_text(ASSETS / "bkflow-prototype.css") + js = read_text(ASSETS / "bkflow-prototype.js") + + for token in [ + ".bk-flow-editor", + ".bk-flow-editor-header", + ".bk-flow-canvas", + ".bk-flow-node", + ".bk-flow-node-toolbar", + ".bk-flow-node-toolbar.is-visible", + ".bk-flow-vars-float", + ]: + assert token in css + + for token in [ + "data-node-select", + "data-node-toolbar", + "data-dblopen", + "data-enable-target", + "data-disable-target", + 'removeAttribute("disabled")', + 'setAttribute("disabled", "disabled")', + ]: + assert token in js + + assert "node.closest(" in js + assert ".bk-flow-editor" in js + assert ".bk-flow-editor-shell" in js + assert "root.querySelectorAll(" in js + assert '[data-node-select="' in js + assert "toolbarSelector" in js + assert "document.querySelectorAll('[data-node-select=\"" not in js + assert "document.querySelectorAll(toolbarSelector)" not in js diff --git a/tests/interface/prototypes/test_master_contract.py b/tests/interface/prototypes/test_master_contract.py new file mode 100644 index 0000000000..def54a5a16 --- /dev/null +++ b/tests/interface/prototypes/test_master_contract.py @@ -0,0 +1,269 @@ +from pathlib import Path +import re + + +ROOT = Path("prototypes") +SHARED = ROOT / "masters" / "_shared" +OVERLAYS = ROOT / "masters" / "overlays" +MASTERS = ROOT / "masters" +ASSETS = ROOT / "assets" +LIST_MASTER = MASTERS / "list-page" +CONFIG_MASTER = MASTERS / "config-page" +ENGINE_MASTER = MASTERS / "engine-panel" +DECISION_MASTER = MASTERS / "decision-editor" + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def test_shared_shells_and_overlay_files_exist(): + required = [ + SHARED / "space-shell.html", + SHARED / "system-shell.html", + SHARED / "plugin-shell.html", + OVERLAYS / "README.md", + OVERLAYS / "dialogs.html", + OVERLAYS / "sidesliders.html", + ] + + for path in required: + assert path.is_file(), path + + +def test_shared_shells_keep_common_header_and_distinct_sidebars(): + shells = { + "space": SHARED / "space-shell.html", + "system": SHARED / "system-shell.html", + "plugin": SHARED / "plugin-shell.html", + } + + for name, path in shells.items(): + text = read_text(path) + assert "', + "搜索", + "筛选", + "状态", + "操作", + 'class="bk-pagination"', + ]: + assert token in template + + assert "暂无数据" in empty_state + assert "加载失败" in error_state + for state_text in [empty_state, error_state]: + for token in [ + '
    ', + '(.*?)
    ', template) + dirty_titles = re.findall(r'
    (.*?)
    ', dirty_state) + template_labels = re.findall(r'', template) + dirty_labels = re.findall(r'', dirty_state) + + for token in [ + "基础配置", + "代码配置", + "", encoding="utf-8") + monkeypatch.setattr(serve, f"{section.upper()}_DIR", str(root)) + + items = serve.list_html_files() + sections = {section for section, _ in items} + + assert sections == {"masters", "features", "examples"} + + +def test_latest_mtime_scans_new_roots(tmp_path, monkeypatch): + serve = _load_serve() + for section in ["masters", "features", "examples", "assets"]: + root = tmp_path / section + root.mkdir() + (root / "index.html").write_text("", encoding="utf-8") + monkeypatch.setattr(serve, f"{section.upper()}_DIR", str(root)) + + mtime = serve.latest_mtime_under((serve.MASTERS_DIR, serve.FEATURES_DIR, serve.EXAMPLES_DIR, serve.ASSETS_DIR)) + + assert isinstance(mtime, float) + + +def test_directory_url_injects_reload(tmp_path, monkeypatch): + serve = _load_serve() + root = tmp_path / "prototypes" + foo = root / "masters" / "foo" + foo.mkdir(parents=True) + (foo / "index.html").write_text("foo", encoding="utf-8") + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(root / "masters")) + monkeypatch.setattr(serve, "FEATURES_DIR", str(root / "features")) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(root / "examples")) + monkeypatch.setattr(serve, "ASSETS_DIR", str(root / "assets")) + + captured = {} + + def fake_send_html_with_reload(self, full_path): + captured["path"] = full_path + self.wfile.write(serve.inject_reload_script(b"foo")) + + monkeypatch.setattr(serve.PrototypeRequestHandler, "_send_html_with_reload", fake_send_html_with_reload) + + handler = _make_handler(serve) + handler.path = "/masters/foo/" + serve.PrototypeRequestHandler.do_GET(handler) + body = handler.wfile.getvalue().decode("utf-8") + + assert "foo" in body + assert "fetch('/api/mtime')" in body + assert captured["path"].endswith("masters/foo/index.html") + + +def test_root_index_only_lists_active_sections_even_if_output_exists(tmp_path, monkeypatch): + serve = _load_serve() + root = tmp_path / "prototypes" + masters = root / "masters" + features = root / "features" + examples = root / "examples" + output = root / "output" + assets = root / "assets" + for directory in [masters, features, examples, output, assets]: + directory.mkdir(parents=True) + (output / "legacy.html").write_text("retired output", encoding="utf-8") + + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(masters)) + monkeypatch.setattr(serve, "FEATURES_DIR", str(features)) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(examples)) + monkeypatch.setattr(serve, "ASSETS_DIR", str(assets)) + + handler = _make_handler(serve) + serve.PrototypeRequestHandler._send_index(handler) + body = handler.wfile.getvalue().decode("utf-8") + + assert body.startswith("") + assert "BKFlow 原型展厅" in body + assert "Feature 展区" in body + assert "代表页面" in body + assert "工具资源" in body + assert "masters/" in body + assert "features/" in body + assert "examples/component-showcase.html" in body + assert "fetch('/api/mtime')" in body + assert "retired output" not in body + assert "/output/legacy.html" not in body + + +def test_api_mtime_ignores_retired_output_directory(tmp_path, monkeypatch): + serve = _load_serve() + root = tmp_path / "prototypes" + masters = root / "masters" + features = root / "features" + examples = root / "examples" + output = root / "output" + assets = root / "assets" + for directory in [masters, features, examples, output, assets]: + directory.mkdir(parents=True) + (masters / "foo.html").write_text("", encoding="utf-8") + (output / "legacy.html").write_text("", encoding="utf-8") + (assets / "style.css").write_text("body{}", encoding="utf-8") + + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(masters)) + monkeypatch.setattr(serve, "FEATURES_DIR", str(features)) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(examples)) + monkeypatch.setattr(serve, "ASSETS_DIR", str(assets)) + + mtimes = { + str(masters): 10.0, + str(features): 11.0, + str(examples): 12.0, + str(output): 13.0, + str(assets): 14.0, + str(masters / "foo.html"): 20.0, + str(output / "legacy.html"): 50.0, + str(assets / "style.css"): 40.0, + } + + def fake_getmtime(path): + return mtimes[str(path)] + + monkeypatch.setattr(serve.os.path, "getmtime", fake_getmtime) + + handler = _make_handler(serve) + serve.PrototypeRequestHandler._send_mtime_json(handler) + first = json.loads(handler.wfile.getvalue().decode("utf-8")) + (output / "legacy.html").unlink() + del mtimes[str(output / "legacy.html")] + mtimes[str(output)] = 99.0 + handler = _make_handler(serve) + serve.PrototypeRequestHandler._send_mtime_json(handler) + second = json.loads(handler.wfile.getvalue().decode("utf-8")) + + assert first["mtime"] == 40 + assert second["mtime"] == 40 diff --git a/tests/interface/prototypes/test_showcase_catalog.py b/tests/interface/prototypes/test_showcase_catalog.py new file mode 100644 index 0000000000..b49f044a4f --- /dev/null +++ b/tests/interface/prototypes/test_showcase_catalog.py @@ -0,0 +1,176 @@ +import importlib.util +import json +from pathlib import Path + + +def _load_catalog(): + path = Path("prototypes/showcase_catalog.py") + spec = importlib.util.spec_from_file_location("prototype_showcase_catalog", path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _touch_html(path: Path, content: str = "") -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def test_collect_feature_catalog_reads_meta_and_featured_pages(tmp_path): + """有元数据时应读取首页展示字段并跳过无效代表页。""" + catalog = _load_catalog() + feature_dir = tmp_path / "features" / "demo-feature" + _touch_html(feature_dir / "pages" / "flow-edit.html") + _touch_html(feature_dir / "pages" / "task-detail-complete.html") + _touch_html(feature_dir / "pages" / "unused.html") + (feature_dir / "feature.meta.json").write_text( + json.dumps( + { + "title": "示例原型", + "summary": "覆盖流程编辑与任务详情。", + "status": "可评审", + "tags": ["流程编辑", "任务详情"], + "coverTheme": "mixed-admin", + "order": 7, + "featuredPages": [ + { + "path": "pages/flow-edit.html", + "title": "流程编辑页", + "summary": "画布与节点抽屉。", + "pageType": "流程编辑", + }, + { + "path": "pages/task-detail-complete.html", + "title": "任务详情页", + "summary": "执行状态与日志。", + "pageType": "任务详情", + }, + { + "path": "pages/missing.html", + "title": "应被跳过", + "summary": "不存在的页面。", + "pageType": "流程编辑", + }, + ], + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + items = catalog.collect_feature_catalog(str(tmp_path / "features")) + + assert items[0]["title"] == "示例原型" + assert items[0]["status"] == "可评审" + assert items[0]["cover_theme"] == "mixed-admin" + assert items[0]["page_count"] == 3 + assert [page["title"] for page in items[0]["featured_pages"]] == ["流程编辑页", "任务详情页"] + assert items[0]["featured_pages"][0]["page_type"] == "流程编辑" + + +def test_collect_feature_catalog_falls_back_when_meta_missing(tmp_path): + """缺少元数据时应使用默认展示信息与代表页。""" + catalog = _load_catalog() + feature_dir = tmp_path / "features" / "demo-fallback" + _touch_html(feature_dir / "pages" / "alpha.html") + _touch_html(feature_dir / "pages" / "beta.html") + _touch_html(feature_dir / "pages" / "gamma.html") + + items = catalog.collect_feature_catalog(str(tmp_path / "features")) + + assert items[0]["slug"] == "demo-fallback" + assert items[0]["status"] == "待补充" + assert items[0]["summary"] == "尚未补充首页摘要" + assert items[0]["cover_theme"] == "mixed-admin" + assert items[0]["page_count"] == 3 + assert [page["path"] for page in items[0]["featured_pages"]] == ["pages/alpha.html", "pages/beta.html"] + + +def test_collect_feature_catalog_sorts_by_order_then_updated_at_desc_and_skips_legacy(tmp_path): + """应按 order 升序、更新时间降序排序,并跳过 _legacy。""" + catalog = _load_catalog() + features_dir = tmp_path / "features" + + older = features_dir / "alpha" + newer = features_dir / "beta" + legacy = features_dir / "_legacy" / "old" + + _touch_html(older / "pages" / "index.html") + _touch_html(newer / "pages" / "index.html") + _touch_html(legacy / "pages" / "index.html") + + (older / "feature.meta.json").write_text( + json.dumps({"title": "Alpha", "order": 1}, ensure_ascii=False), encoding="utf-8" + ) + (newer / "feature.meta.json").write_text( + json.dumps({"title": "Beta", "order": 1}, ensure_ascii=False), encoding="utf-8" + ) + (legacy / "feature.meta.json").write_text( + json.dumps({"title": "Legacy", "order": 0}, ensure_ascii=False), encoding="utf-8" + ) + + old_time = 1_700_000_000 + new_time = 1_700_000_100 + Path(older / "pages" / "index.html").touch() + Path(newer / "pages" / "index.html").touch() + import os + + os.utime(older, (old_time, old_time)) + os.utime(older / "pages" / "index.html", (old_time, old_time)) + os.utime(older / "feature.meta.json", (old_time, old_time)) + os.utime(newer, (new_time, new_time)) + os.utime(newer / "pages" / "index.html", (new_time, new_time)) + os.utime(newer / "feature.meta.json", (new_time, new_time)) + + items = catalog.collect_feature_catalog(str(features_dir)) + + assert [item["slug"] for item in items] == ["beta", "alpha"] + assert all(item["slug"] != "_legacy" for item in items) + + +def test_collect_feature_catalog_falls_back_when_meta_is_not_object(tmp_path): + """元数据不是 JSON object 时应回退到默认展示信息。""" + catalog = _load_catalog() + feature_dir = tmp_path / "features" / "demo-bad-meta" + _touch_html(feature_dir / "pages" / "alpha.html") + _touch_html(feature_dir / "pages" / "beta.html") + feature_dir.mkdir(parents=True, exist_ok=True) + (feature_dir / "feature.meta.json").write_text(json.dumps(["not", "an", "object"]), encoding="utf-8") + + items = catalog.collect_feature_catalog(str(tmp_path / "features")) + + assert items[0]["slug"] == "demo-bad-meta" + assert items[0]["status"] == "待补充" + assert items[0]["summary"] == "尚未补充首页摘要" + assert [page["path"] for page in items[0]["featured_pages"]] == ["pages/alpha.html", "pages/beta.html"] + + +def test_collect_showcase_stats_counts_meta_updates(tmp_path): + """首页统计的更新时间应纳入 feature.meta.json 的变更。""" + catalog = _load_catalog() + features_dir = tmp_path / "features" + masters_dir = tmp_path / "masters" + feature_dir = features_dir / "demo-feature" + + _touch_html(feature_dir / "pages" / "alpha.html") + feature_dir.mkdir(parents=True, exist_ok=True) + meta_path = feature_dir / "feature.meta.json" + meta_path.write_text(json.dumps({"title": "Demo"}, ensure_ascii=False), encoding="utf-8") + _touch_html(masters_dir / "template.html") + + html_time = 1_700_000_000 + meta_time = 1_700_000_200 + import os + + os.utime(feature_dir / "pages" / "alpha.html", (html_time, html_time)) + os.utime(meta_path, (meta_time, meta_time)) + os.utime(masters_dir / "template.html", (html_time, html_time)) + + stats = catalog.collect_showcase_stats(str(features_dir), str(masters_dir)) + + assert stats["feature_count"] == 1 + assert stats["feature_page_count"] == 1 + assert stats["master_page_count"] == 1 + assert stats["html_page_count"] == 2 + assert stats["updated_at"] == meta_time diff --git a/tests/interface/prototypes/test_showcase_homepage.py b/tests/interface/prototypes/test_showcase_homepage.py new file mode 100644 index 0000000000..72ba6095ca --- /dev/null +++ b/tests/interface/prototypes/test_showcase_homepage.py @@ -0,0 +1,446 @@ +from __future__ import annotations + +import importlib.util +import subprocess +from textwrap import dedent +from io import BytesIO +from pathlib import Path + + +def _load_serve(): + path = Path("prototypes/serve.py") + spec = importlib.util.spec_from_file_location("prototype_serve", path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _make_handler(serve): + handler = object.__new__(serve.PrototypeRequestHandler) + handler.wfile = BytesIO() + handler.send_response = lambda *args, **kwargs: None + handler.send_header = lambda *args, **kwargs: None + handler.end_headers = lambda *args, **kwargs: None + return handler + + +def _write_feature(root, slug, meta, pages): + feature_dir = root / slug + feature_dir.mkdir() + (feature_dir / "feature.meta.json").write_text(meta, encoding="utf-8") + for rel_path, content in pages.items(): + page_path = feature_dir / rel_path + page_path.parent.mkdir(parents=True, exist_ok=True) + page_path.write_text(content, encoding="utf-8") + return feature_dir + + +def _run_showcase_js_behavior() -> dict: + script = dedent( + r""" + const fs = require('fs'); + const vm = require('vm'); + + function makeClassList(initial) { + const tokens = new Set((initial || '').split(/\s+/).filter(Boolean)); + return { + toggle(token, force) { + const shouldHave = force === undefined ? !tokens.has(token) : !!force; + if (shouldHave) tokens.add(token); else tokens.delete(token); + }, + contains(token) { return tokens.has(token); }, + toString() { return Array.from(tokens).join(' '); }, + }; + } + + function createElement(tag, attrs = {}, text = '') { + const el = { + tagName: tag.toUpperCase(), + attrs: { ...attrs }, + children: [], + parentNode: null, + textContent: text, + hidden: false, + value: attrs.value || '', + classList: makeClassList(attrs.class || ''), + listeners: {}, + setAttribute(name, value) { this.attrs[name] = String(value); }, + getAttribute(name) { return this.attrs[name]; }, + addEventListener(type, cb) { + (this.listeners[type] = this.listeners[type] || []).push(cb); + }, + dispatchEvent(event) { + (this.listeners[event.type] || []).forEach((cb) => cb(event)); + }, + contains(node) { + if (node === this) return true; + return this.children.some((child) => child.contains && child.contains(node)); + }, + closest(selector) { + return matches(this, selector) ? this : null; + }, + querySelector(selector) { + return queryAll(this, selector)[0] || null; + }, + querySelectorAll(selector) { + return queryAll(this, selector); + }, + }; + el.className = attrs.class || ''; + return el; + } + + function matches(el, selector) { + const active = selector.includes('.is-active'); + if (active && !el.classList.contains('is-active')) return false; + const attrMatch = selector.match(/\[([^\]=]+)(?:="([^"]*)")?\]/); + if (!attrMatch) return false; + const name = attrMatch[1]; + const expected = attrMatch[2]; + if (!(name in el.attrs)) return false; + return expected === undefined || String(el.attrs[name]) === expected; + } + + function queryAll(root, selector) { + const results = []; + (function walk(node) { + if (matches(node, selector)) results.push(node); + node.children.forEach(walk); + })(root); + return results; + } + + const search = createElement('input', { 'data-showcase-search': '', type: 'search' }); + const summary = createElement('p', { 'data-showcase-summary': '' }, ''); + const emptyState = createElement('p', { 'data-showcase-empty': '' }, ''); + const cardOne = createElement('article', { + 'data-feature-card': '', + 'data-status': '进行中', + 'data-tags': '代码生成,流程编排', + 'data-search-index': '流程总览 overview 总览页 详情页', + }); + const cardTwo = createElement('article', { + 'data-feature-card': '', + 'data-status': '已完成', + 'data-tags': '资源库', + 'data-search-index': '资源中心 resource', + }); + const statusGroup = createElement('div', { 'data-showcase-filter-group': 'status' }); + const statusAll = createElement('button', { 'data-showcase-filter': 'all', class: 'is-active' }); + const statusLive = createElement('button', { 'data-showcase-filter': '进行中' }); + statusGroup.children = [statusAll, statusLive]; + statusAll.parentNode = statusGroup; + statusLive.parentNode = statusGroup; + const tagGroup = createElement('div', { 'data-showcase-filter-group': 'tag' }); + const tagAll = createElement('button', { 'data-showcase-filter': 'all', class: 'is-active' }); + const tagCode = createElement('button', { 'data-showcase-filter': '代码生成' }); + tagGroup.children = [tagAll, tagCode]; + tagAll.parentNode = tagGroup; + tagCode.parentNode = tagGroup; + const root = createElement('main', { 'data-showcase-root': '' }); + root.children = [search, summary, emptyState, cardOne, cardTwo, statusGroup, tagGroup]; + [search, summary, emptyState, cardOne, cardTwo, statusGroup, tagGroup].forEach((child) => { child.parentNode = root; }); + + const document = { + readyState: 'complete', + querySelector(selector) { + return selector === '[data-showcase-root]' ? root : null; + }, + addEventListener() {}, + }; + + const context = { + document, + console, + setInterval, + clearInterval, + location: { reload() {} }, + fetch() { return Promise.resolve({ json() { return Promise.resolve({ mtime: 1 }); } }); }, + }; + + vm.runInNewContext(fs.readFileSync('prototypes/assets/bkflow-showcase.js', 'utf8'), context); + + const initial = { + summary: summary.textContent, + cardOneHidden: cardOne.hidden, + cardTwoHidden: cardTwo.hidden, + emptyHidden: emptyState.hidden, + }; + + search.value = '资源'; + search.dispatchEvent({ type: 'input', target: search }); + const afterSearch = { + summary: summary.textContent, + cardOneHidden: cardOne.hidden, + cardTwoHidden: cardTwo.hidden, + emptyHidden: emptyState.hidden, + }; + + statusGroup.dispatchEvent({ type: 'click', target: statusLive }); + const afterFilter = { + summary: summary.textContent, + cardOneHidden: cardOne.hidden, + cardTwoHidden: cardTwo.hidden, + emptyHidden: emptyState.hidden, + }; + + process.stdout.write(JSON.stringify({ initial, afterSearch, afterFilter })); + """ + ) + completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) + return __import__("json").loads(completed.stdout) + + +def test_root_showcase_homepage_renders_chinese_hero_sections_and_cta(tmp_path, monkeypatch): + """Root homepage should render the showcase layout and consume catalog data.""" + + serve = _load_serve() + root = tmp_path / "prototypes" + masters = root / "masters" + features = root / "features" + examples = root / "examples" + assets = root / "assets" + for directory in [masters, features, examples, assets]: + directory.mkdir(parents=True) + + _write_feature( + features, + "demo-feature", + """ + { + "title": "流程总览", + "summary": "首页主角 feature", + "status": "进行中", + "coverTheme": "flow-editor", + "tags": ["代码生成", "流程编排"], + "order": 1, + "featuredPages": [ + {"path": "overview.html", "title": "总览页", "pageType": "流程编辑"}, + {"path": "details.html", "title": "详情页", "pageType": "任务详情"}, + {"path": "card-only.html", "title": "卡片页", "pageType": "列表管理"} + ] + } + """.strip(), + { + "overview.html": "overview", + "details.html": "details", + "card-only.html": "card", + }, + ) + + (masters / "list-page.html").write_text("master", encoding="utf-8") + (examples / "component-showcase.html").write_text("showcase", encoding="utf-8") + (assets / "bkflow-prototype.css").write_text("body{}", encoding="utf-8") + + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(masters)) + monkeypatch.setattr(serve, "FEATURES_DIR", str(features)) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(examples)) + monkeypatch.setattr(serve, "ASSETS_DIR", str(assets)) + + handler = object.__new__(serve.PrototypeRequestHandler) + handler.wfile = BytesIO() + handler.send_response = lambda *args, **kwargs: None + handler.send_header = lambda *args, **kwargs: None + handler.end_headers = lambda *args, **kwargs: None + + serve.PrototypeRequestHandler._send_index(handler) + body = handler.wfile.getvalue().decode("utf-8") + + assert "BKFlow 原型展厅" in body + assert "最近更新" in body + assert "流程总览" in body + assert 'href="/assets/bkflow-showcase.css"' in body + assert 'src="/assets/bkflow-showcase.js"' in body + assert 'data-showcase-search' in body + assert 'data-showcase-filter="all"' in body + assert 'data-feature-card' in body + assert 'data-status="进行中"' in body + assert 'data-tags="代码生成,流程编排"' in body + assert 'data-cover-theme="flow-editor"' in body + assert 'href="/features/demo-feature/card-only.html"' in body + assert "代表页面" in body + assert "流程编辑" in body + assert "任务详情" in body + assert "列表管理" in body + assert "工具资源" in body + assert 'href="#feature-showcase"' in body + assert "查看 Feature 展区" in body + assert 'data-showcase-empty' in body + + +def test_root_showcase_homepage_falls_back_to_default_cover_theme_when_missing(tmp_path, monkeypatch): + """Missing coverTheme metadata should fall back to the default showcase theme.""" + + serve = _load_serve() + root = tmp_path / "prototypes" + masters = root / "masters" + features = root / "features" + examples = root / "examples" + assets = root / "assets" + for directory in [masters, features, examples, assets]: + directory.mkdir(parents=True) + + feature_dir = features / "demo-feature" + feature_dir.mkdir() + (feature_dir / "feature.meta.json").write_text( + """ + { + "title": "流程总览", + "summary": "缺省主题回退", + "status": "进行中", + "tags": ["代码生成"], + "featuredPages": [{"path": "overview.html", "title": "总览页"}] + } + """.strip(), + encoding="utf-8", + ) + (feature_dir / "overview.html").write_text("overview", encoding="utf-8") + + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(masters)) + monkeypatch.setattr(serve, "FEATURES_DIR", str(features)) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(examples)) + monkeypatch.setattr(serve, "ASSETS_DIR", str(assets)) + + handler = _make_handler(serve) + serve.PrototypeRequestHandler._send_index(handler) + body = handler.wfile.getvalue().decode("utf-8") + + assert 'data-cover-theme="mixed-admin"' in body + assert "Mixed Admin" in body + + +def test_root_showcase_homepage_keeps_spec_cover_theme_variants_and_labels(tmp_path, monkeypatch): + """Spec coverTheme variants should render distinct previews instead of collapsing to the default theme.""" + + serve = _load_serve() + root = tmp_path / "prototypes" + masters = root / "masters" + features = root / "features" + examples = root / "examples" + assets = root / "assets" + for directory in [masters, features, examples, assets]: + directory.mkdir(parents=True) + + for order, (slug, theme, title) in enumerate( + [ + ("task-detail-feature", "task-detail", "任务详情"), + ("list-management-feature", "list-management", "列表管理"), + ("config-panel-feature", "config-panel", "配置面板"), + ], + start=1, + ): + _write_feature( + features, + slug, + f""" + {{ + "title": "{title}", + "summary": "{theme} 主题校验", + "status": "进行中", + "coverTheme": "{theme}", + "order": {order}, + "featuredPages": [ + {{"path": "page.html", "title": "页面", "pageType": "{title}"}} + ] + }} + """.strip(), + {"page.html": "page"}, + ) + + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(masters)) + monkeypatch.setattr(serve, "FEATURES_DIR", str(features)) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(examples)) + monkeypatch.setattr(serve, "ASSETS_DIR", str(assets)) + + handler = _make_handler(serve) + serve.PrototypeRequestHandler._send_index(handler) + body = handler.wfile.getvalue().decode("utf-8") + + assert 'data-cover-theme="task-detail"' in body + assert 'data-cover-theme="list-management"' in body + assert 'data-cover-theme="config-panel"' in body + assert 'cover-preview--task-detail' in body + assert 'cover-preview--list-management' in body + assert 'cover-preview--config-panel' in body + assert "Task Detail" in body + assert "List Management" in body + assert "Config Panel" in body + assert "code-console" not in body + assert "dashboard" not in body + assert "mobile-first" not in body + + +def test_root_showcase_homepage_representative_pages_show_page_type_and_feature(tmp_path, monkeypatch): + """Representative pages should stay grouped by feature and expose page type context.""" + + serve = _load_serve() + root = tmp_path / "prototypes" + masters = root / "masters" + features = root / "features" + examples = root / "examples" + assets = root / "assets" + for directory in [masters, features, examples, assets]: + directory.mkdir(parents=True) + + _write_feature( + features, + "flow-feature", + """ + { + "title": "流程总览", + "summary": "代表页面要带上下文", + "status": "进行中", + "coverTheme": "flow-editor", + "order": 1, + "featuredPages": [ + {"path": "overview.html", "title": "总览页", "pageType": "流程编辑"}, + {"path": "detail.html", "title": "详情页", "pageType": "任务详情"} + ] + } + """.strip(), + { + "overview.html": "overview", + "detail.html": "detail", + }, + ) + + monkeypatch.setattr(serve, "PROTOTYPES_ROOT", str(root)) + monkeypatch.setattr(serve, "MASTERS_DIR", str(masters)) + monkeypatch.setattr(serve, "FEATURES_DIR", str(features)) + monkeypatch.setattr(serve, "EXAMPLES_DIR", str(examples)) + monkeypatch.setattr(serve, "ASSETS_DIR", str(assets)) + + handler = _make_handler(serve) + serve.PrototypeRequestHandler._send_index(handler) + body = handler.wfile.getvalue().decode("utf-8") + + assert "代表页面" in body + assert "流程总览" in body + assert "流程编辑" in body + assert "任务详情" in body + assert "所属 Feature" in body + assert 'href="/features/flow-feature/overview.html"' in body + assert 'href="/features/flow-feature/detail.html"' in body + + +def test_bkflow_showcase_js_updates_hidden_summary_and_empty_state(tmp_path): + """Search and filter interactions should update the lightweight showcase state.""" + + result = _run_showcase_js_behavior() + + assert result["initial"]["summary"] == "当前展示 2 个 Feature" + assert result["initial"]["cardOneHidden"] is False + assert result["initial"]["cardTwoHidden"] is False + assert result["initial"]["emptyHidden"] is True + assert result["afterSearch"]["summary"] == "当前展示 1 个 Feature" + assert result["afterSearch"]["cardOneHidden"] is True + assert result["afterSearch"]["cardTwoHidden"] is False + assert result["afterSearch"]["emptyHidden"] is True + assert result["afterFilter"]["summary"] == "当前没有匹配的 Feature" + assert result["afterFilter"]["cardOneHidden"] is True + assert result["afterFilter"]["cardTwoHidden"] is True + assert result["afterFilter"]["emptyHidden"] is False diff --git a/tests/interface/prototypes/test_structure_contract.py b/tests/interface/prototypes/test_structure_contract.py new file mode 100644 index 0000000000..d338a40f3d --- /dev/null +++ b/tests/interface/prototypes/test_structure_contract.py @@ -0,0 +1,219 @@ +import json +from pathlib import Path +import re + + +def test_required_root_directories_exist(): + required = [ + Path("prototypes/masters"), + Path("prototypes/masters/_shared"), + Path("prototypes/features"), + Path("prototypes/features/_legacy"), + Path("prototypes/examples"), + ] + + for path in required: + assert path.exists(), path + + +def test_required_readmes_exist(): + required = [ + Path("prototypes/masters/README.md"), + Path("prototypes/masters/_shared/README.md"), + Path("prototypes/features/README.md"), + Path("prototypes/features/_legacy/README.md"), + Path("prototypes/examples/README.md"), + Path("prototypes/features/_legacy/sops-open-plugin/README.md"), + Path("prototypes/features/_legacy/space-variable-manage/README.md"), + Path("prototypes/features/_legacy/node-output-viewer/README.md"), + ] + + for path in required: + assert path.is_file(), path + + +def test_showcase_homepage_contract_resources_and_meta_shape(): + required = [ + Path("prototypes/assets/bkflow-showcase.css"), + Path("prototypes/assets/bkflow-showcase.js"), + ] + + for path in required: + assert path.is_file(), path + + features_root = Path("prototypes/features") + feature_dirs = [ + path + for path in sorted(features_root.iterdir(), key=lambda item: item.name) + if path.is_dir() and not path.name.startswith("_") and not path.name.startswith(".") + ] + + assert feature_dirs, "expected at least one active feature directory" + + for feature_dir in feature_dirs: + meta_path = feature_dir / "feature.meta.json" + assert meta_path.is_file(), meta_path + + meta = json.loads(meta_path.read_text(encoding="utf-8")) + expected_keys = {"title", "summary", "status", "tags", "coverTheme", "order", "featuredPages"} + + assert isinstance(meta, dict), feature_dir + for key in expected_keys: + assert key in meta, (feature_dir, key) + + assert isinstance(meta["title"], str) and meta["title"].strip(), feature_dir + assert isinstance(meta["summary"], str) and meta["summary"].strip(), feature_dir + assert isinstance(meta["status"], str) and meta["status"].strip(), feature_dir + assert isinstance(meta["tags"], list) and meta["tags"], feature_dir + assert all(isinstance(tag, str) and tag.strip() for tag in meta["tags"]), feature_dir + assert isinstance(meta["coverTheme"], str) and meta["coverTheme"].strip(), feature_dir + assert isinstance(meta["order"], int), feature_dir + assert isinstance(meta["featuredPages"], list) and meta["featuredPages"], feature_dir + + for page in meta["featuredPages"]: + assert isinstance(page, dict), (feature_dir, page) + for key in ["path", "title", "summary", "pageType"]: + assert key in page, (feature_dir, key) + assert isinstance(page[key], str) and page[key].strip(), (feature_dir, key) + feature_root = feature_dir.resolve() + resolved = (feature_dir / page["path"]).resolve() + assert feature_root in resolved.parents or resolved == feature_root, (feature_dir, page["path"]) + assert resolved.is_file(), (feature_dir, page["path"]) + assert resolved.suffix == ".html", (feature_dir, page["path"]) + + +def test_examples_only_keep_reference_pages(): + example_dir = Path("prototypes/examples") + files = sorted(path.name for path in example_dir.iterdir() if path.is_file()) + assert files == ["README.md", "component-showcase.html"], files + + +def test_legacy_archive_contains_migrated_pages(): + expected = [ + Path("prototypes/features/_legacy/sops-open-plugin/index.html"), + Path("prototypes/features/_legacy/sops-open-plugin/main-flow.html"), + Path("prototypes/features/_legacy/sops-open-plugin/space-open-plugin-management.html"), + Path("prototypes/features/_legacy/sops-open-plugin/task-plugin-error-state.html"), + Path("prototypes/features/_legacy/sops-open-plugin/template-plugin-selection.html"), + Path("prototypes/features/_legacy/space-variable-manage/README.md"), + Path("prototypes/features/_legacy/space-variable-manage/index.html"), + Path("prototypes/features/_legacy/node-output-viewer/README.md"), + Path("prototypes/features/_legacy/node-output-viewer/index.html"), + ] + + for path in expected: + assert path.is_file(), path + + +def test_legacy_archive_relative_links_resolve(): + archive_root = Path("prototypes/features/_legacy") + pattern = re.compile(r'(?:href|data-href)="([^"#][^"]*)"') + + for html_path in archive_root.rglob("*.html"): + text = html_path.read_text(encoding="utf-8") + for target in pattern.findall(text): + if target.startswith(("http://", "https://", "mailto:", "javascript:")): + continue + resolved = (html_path.parent / target).resolve() + assert resolved.exists(), (html_path, target) + + +def test_obsolete_flat_pages_are_removed(): + removed = [ + Path("prototypes/base.html"), + Path("prototypes/output/.gitkeep"), + Path("prototypes/output/sops-open-plugin-main-flow.html"), + Path("prototypes/output/sops-open-plugin-space-open-plugin-management.html"), + Path("prototypes/output/sops-open-plugin-task-plugin-error-state.html"), + Path("prototypes/output/sops-open-plugin-template-plugin-selection.html"), + Path("prototypes/output/space-variable-manage.html"), + Path("prototypes/output/node-output-viewer-proposal-v1.html"), + Path("prototypes/examples/composite-page.html"), + Path("prototypes/examples/detail-page.html"), + Path("prototypes/examples/flow-edit.html"), + Path("prototypes/examples/form-slider.html"), + Path("prototypes/examples/list-page.html"), + Path("prototypes/examples/tab-page.html"), + Path("prototypes/examples/task-detail.html"), + ] + + for path in removed: + assert not path.exists(), path + + +def test_retired_directories_are_removed(): + removed = [ + Path("prototypes/cache"), + Path("prototypes/metadata"), + Path("prototypes/renderers"), + Path("prototypes/output"), + ] + + for path in removed: + assert not path.exists(), path + + +def test_directory_readmes_describe_contract(): + checks = { + Path("prototypes/masters/README.md"): [ + "source of truth", + "page-type masters", + "stable states", + "_shared", + "docs/specs", + "docs/plans", + ], + Path("prototypes/masters/_shared/README.md"): [ + "shared scaffolding", + "does not preview independently", + "reuse-only", + "Do not put delivery pages", + ], + Path("prototypes/features/README.md"): [ + "feature slug", + "spec and one plan", + "feature.meta.json", + "homepage showcase contract", + ], + Path("prototypes/features/_legacy/README.md"): [ + "archive-only", + "migrated historical feature material", + "prototypes/features//", + "Do not put shared masters", + ], + Path("prototypes/examples/README.md"): [ + "reference only", + "component-showcase.html", + "not for delivery", + "docs/specs", + "docs/plans", + ], + Path(".ai/skills/prototype-generator/SKILL.md"): [ + "BKFlow 原型展厅", + "feature.meta.json", + "features//", + "首页展示所需的卡片信息", + "不要只生成 `README.md` 和 `index.html`", + ], + } + + for path, expected_snippets in checks.items(): + lines = path.read_text(encoding="utf-8").splitlines() + text = "\n".join(lines) + for line in expected_snippets: + assert line in text, (path, line) + + +def test_prototypes_readme_describes_showcase_homepage_sources(): + text = Path("prototypes/README.md").read_text(encoding="utf-8") + expected_snippets = [ + "BKFlow 原型展厅", + "根路径 `/`", + "feature.meta.json", + "首页元数据", + "首页展示来源", + "不再把根首页描述成简单目录索引", + ] + + for snippet in expected_snippets: + assert snippet in text, snippet diff --git a/tests/interface/prototypes/test_task_detail_contract.py b/tests/interface/prototypes/test_task_detail_contract.py new file mode 100644 index 0000000000..577017b5d5 --- /dev/null +++ b/tests/interface/prototypes/test_task_detail_contract.py @@ -0,0 +1,193 @@ +from pathlib import Path +import re + + +ROOT = Path("prototypes") +TASK_DETAIL = ROOT / "masters" / "task-detail" +STATES = TASK_DETAIL / "states" +ASSETS = ROOT / "assets" + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def extract_section(html: str, section_id: str) -> str: + match = re.search( + rf']*id="{re.escape(section_id)}"[^>]*>(.*?)
    ', + html, + re.DOTALL, + ) + assert match, section_id + return match.group(1) + + +def test_task_detail_master_files_exist(): + required = [ + TASK_DETAIL / "README.md", + TASK_DETAIL / "template.html", + STATES / "complete.html", + STATES / "failed.html", + STATES / "empty-log.html", + ] + + for path in required: + assert path.is_file(), path + + +def test_task_detail_readme_describes_execution_audit_surface_and_flow_editor_boundary(): + text = read_text(TASK_DETAIL / "README.md") + + for token in [ + "execution detail route", + "read-only execution canvas", + "single-click node detail opening", + "configuration snapshot captured at execution time", + "distinct from `flow-editor/`", + "no save / publish / debug header actions", + ]: + assert token in text + + +def test_task_detail_template_covers_execution_header_backlink_read_only_canvas_and_observation_tabs(): + template = read_text(TASK_DETAIL / "template.html") + + for token in [ + "任务执行", + "audit-deep-dive-20260420_调试任务_20260420203821", + "完成", + "/template/view/2166/", + "源模板", + "执行画布", + "只读观察", + "节点详情", + "执行记录", + "配置快照", + "操作历史", + "调用日志", + "开始节点 / 消息展示 / 结束节点", + "异常信息", + "执行信息", + "输入参数", + "输出参数", + "标准插件", + "插件版本", + "节点名称", + "失败处理", + "暂无数据", + "pipeline trace", + "line-numbers", + 'data-node-select="task-detail-main"', + 'data-node-open="#task-detail-panel"', + 'data-node-detail-source="', + 'data-detail-source-group="task-detail-panel"', + 'data-detail-source-id="', + 'data-tab-group="task-detail-panel"', + 'data-tab-target="task-detail-records"', + 'data-tab-target="task-detail-snapshot"', + 'data-tab-target="task-detail-history"', + 'data-tab-target="task-detail-call-log"', + ]: + assert token in template + + for forbidden in [ + ">保存", + ">发布", + ">调试", + "节点悬浮工具条", + "data-dblopen", + ">复制", + ">连线", + ">删除", + ]: + assert forbidden not in template + + node_sources = set(re.findall(r'data-node-detail-source="([^"]+)"', template)) + panel_sources = set(re.findall(r'data-detail-source-id="([^"]+)"', template)) + + assert len(node_sources) >= 2 + assert node_sources == panel_sources + assert 'data-node-status="未执行"' in template + assert "bk-task-status is-neutral" in template + + records_panel = extract_section(template, "task-detail-records") + call_log_panel = extract_section(template, "task-detail-call-log") + + assert "bk-log-viewer" not in records_panel + assert "bk-log-viewer" in call_log_panel + + +def test_task_detail_state_pages_cover_complete_failed_and_empty_log_audit_semantics(): + complete = read_text(STATES / "complete.html") + failed = read_text(STATES / "failed.html") + empty_log = read_text(STATES / "empty-log.html") + + for token in [ + "完成", + "bk-task-status is-success", + "暂无异常", + "调用日志", + ]: + assert token in complete + + complete_records = extract_section(complete, "task-detail-records") + complete_call_log = extract_section(complete, "task-detail-call-log") + assert "bk-log-viewer" not in complete_records + assert "bk-log-viewer" in complete_call_log + + for token in [ + "失败", + "bk-task-status is-failed", + "异常信息", + "Traceback", + "ValueError", + "失败原因", + "调用日志", + ]: + assert token in failed + + failed_records = extract_section(failed, "task-detail-records") + failed_call_log = extract_section(failed, "task-detail-call-log") + assert "bk-log-viewer" not in failed_records + assert "bk-log-viewer" in failed_call_log + + for token in [ + "调用日志", + "暂无调用日志", + "日志查看器", + "bk-log-empty", + ]: + assert token in empty_log + + +def test_shared_assets_expose_minimal_task_detail_hooks(): + css = read_text(ASSETS / "bkflow-prototype.css") + js = read_text(ASSETS / "bkflow-prototype.js") + + for token in [ + ".bk-task-detail", + ".bk-task-detail-header", + ".bk-task-detail-panel", + ".bk-task-status", + ".bk-task-detail-node-trail", + ".bk-task-detail-section", + ".bk-log-viewer", + ".bk-log-empty", + ".bk-task-status.is-neutral", + ]: + assert token in css + + for token in [ + "data-node-open", + "data-node-detail-source", + "data-detail-source-group", + "data-detail-source-id", + "data-tab-group", + "data-tab-target", + "statusClassMap", + "is-neutral", + '[data-tab-group="', + 'panel.classList.add("is-open")', + "hidden = panel.id !== targetId", + ]: + assert token in js