# bronya2 **Repository Path**: SystemLight/bronya2 ## Basic Information - **Project Name**: bronya2 - **Description**: bronya2:一个轻量级开源工具集,专注于高效数据处理与自动化任务,适合开发者快速构建稳定可靠的脚本系统。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-21 - **Last Updated**: 2026-09-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 布洛尼亚2 (Bronya2 Work Panel) > 一个基于 Electron + Vue 3 + Vite 构建的二次元风格桌面工作面板应用,集成每日流程管理、日程安排、程序员工具、脑图浏览和个性化配置。 ## 项目总览 bronya2 仓库是一个 monorepo,包含 5 个子项目,各自独立部署/运行,通过协议协作。PC / Web / Server 共享同一批业务 service 与同一数据库文件 `~/.bronya2/bronya2.db`;task-server 与 deploy-server 是远端辅助服务,分别承接构建执行与部署执行。 | 子项目 | 形态 | 主要技术 | 部署位置 | 详见 | | --- | --- | --- | --- | --- | | PC | Electron 桌面应用 | Electron + Vue 3 | 用户本机 | [一、PC 版](#一pc-版electron) | | Web | 浏览器 SPA | Vue 3 + Vite | 服务端静态托管 | [二、Web 版](#二web-版浏览器--express) | | Server | Web 服务端 | Express + TS | 服务器 Node ≥24 | [三、Server](#三serverweb-服务端) | | task-server | 远程构建执行服务 | Node 24 / Python 3.8+ | 构建机 | [四、task-server](#四task-server远程构建执行服务) | | deploy-server | 远程部署服务 | Python 3.4 / 3.7 标准库 | 业务服务器 | [五、deploy-server](#五deploy-server远程部署服务) | ### 协作关系图 ``` ┌──────────────────────────────────┐ │ bronya 主项目 │ │ (PC / Web / Server 共享 services + DB) │ │ - envUrls 配置 │ │ - BuildExecutor 接口 │ └──────┬───────────────────────┬───┘ │ │ uploadUrl / deployUrl build.executor = remote │ │ ┌──────────────▼──────────┐ ┌────────▼─────────┐ │ deploy-server │ │ task-server │ │ (Python 3.4/3.7 标准库) │ │ (Node / Python) │ │ - /bronya/upload │ │ - /build/exec │ │ - /bronya/deploy │ │ - SSE 日志流 │ │ - /adapter/* │ │ - /build/cancel │ │ - /log/* │ └────────────────────┘ └──────────────────────────┘ ``` ## 开发环境 - PowerBy: [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue/tree/main) - Windows: 11 25H2 (OS内部版本 26200.9168) - VisualStudio: 2026 - Windows Sdk: 10.0.26100.8249 - Python: 3.14.6 - NodeJs: 24.18.1 - Electron: 42.1.0 - Vite: 8.0.16 - [nodejs](https://nodejs.org/zh-cn/download) - [electronjs](https://releases.electronjs.org/release) ## 技术栈 | 层级 | 技术 | 版本 | 说明 | | ------- | -------------------------------- | ------- | ------------------------------------- | | 桌面框架 | Electron | 42.1.0 | 跨平台桌面应用容器 | | 构建工具 | Vite | 8.2.2 | 前端构建 + electron 插件 | | 前端框架 | Vue | 3.5.35 | Composition API + SFC | | 类型系统 | TypeScript | 6.0.3 | 全栈类型安全 | | UI 组件库 | Element Plus | 2.14.4 | 企业级 UI 组件 | | 图标库 | @element-plus/icons-vue | 2.3.2 | Element Plus 图标 | | 代码编辑器 | monaco-editor | 0.56.0 | VS Code 同款编辑器内核 | | SQL 语言支持 | monaco-sql-languages + sql-formatter | 1.2.1 / 15.9.0 | 数据查询 SQL 编辑器方言高亮/格式化(禁用 worker 依赖的补全/校验,Monaco 0.56 API 不兼容) | | XML 格式化 | xml-formatter | 3.7.0 | XML pretty print(共享 `utils/format.ts` formatXml + Monaco XML provider) | | Markdown 渲染 | marked + marked-highlight | 18.x / 2.x | AI 对话回复 Markdown 解析 | | 代码高亮 | highlight.js | 11.12.0 | AI 对话代码块语法高亮 | | 编辑器构建 | @dvaji/vite-plugin-monaco-editor | 2.0.0 | Vite 8 Monaco 插件 | | 本地数据库 | better-sqlite3 | 13.0.3 | 同步 SQLite 驱动 | | zip 压缩 | archiver | 8.0.0 | 构建产物流式 zip 打包(构建调试) | | 开机启动 | auto-launch | 5.0.6 | 跨平台开机自启 | | CSV 解析 | papaparse | 5.7.0 | TFS 导出 CSV 解析(需求分析域) | | 图片元数据 | exiftool-vendored | 38.1.0 | EXIF / XMP / IPTC 读取与编辑(内置工具-图片元数据) | | 图片缩放 | sharp | 0.35.4 | libvips 重采样与去白底(内置工具-图片缩放) | | 拼音搜索 | pinyin-pro | 3.29.3 | 快捷搜索拼音/首字母/全拼模糊匹配 + 命中区间高亮 | | Live2D | pixi.js + pixi-live2d-display | 6.5.10 / 0.4.0 | 右下角看板娘渲染层(Cubism2 模型,运行时 core 脚本置于 public/live2d) | | 样式预处理 | Sass | 1.103.1 | SCSS 变量 + 主题 token | | 服务端框架 | Express | 5.1.0 | Web 版 HTTP 服务端 | | 跨域 | cors | 2.8.5 | Web 版 CORS 支持 | | TS 运行 | tsx | 4.19.4 | 服务端 TypeScript 直接运行(dev/start) | | 并行启动 | concurrently | 9.1.2 | Web 版前后端并行开发 | ## 快速开始 ```bash # 安装依赖 npm install # —— PC 版(Electron)—— # 开发模式启动 npm run dev # 类型检查 + 构建 + 打包(产出 release// 安装包) npm run build # 仅预览渲染层 npm run preview # —— Web 版(浏览器 + Express 服务端)—— # 并行启动前端开发服务器(5173) 与后端服务(3210),浏览器访问 http://localhost:5173 npm run dev:web # 仅启动后端服务(自动托管 dist-web,生产用) npm run start:server # Web 版打包(产物输出 dist-web) npm run build:web # 服务端打包(vite 内置 tsc 类型检查插件 + SSR 编译,产物输出 dist-server 部署包) npm run build:server # 运行服务端部署包(dist-server/index.js,生产用) npm run start:server:prod ``` ## 项目目录结构 ``` bronya2/ ├── electron/ # Electron 主进程 & 预加载层 │ ├── main/ # 主进程源码 │ │ ├── config/ │ │ │ └── index.ts # 应用配置常量(窗口尺寸、数据目录名、schema 版本等) │ │ ├── utils/ │ │ │ ├── paths.ts # 路径工具:应用数据目录 ~/.bronya2、数据库路径、构建产物默认输出目录、临时凭证目录 │ │ │ └── logger.ts # 轻量日志器(info/warn/error/debug,本地时区时间戳) │ │ ├── services/ # 业务服务层(直接操作数据库) │ │ │ ├── database.ts # SQLite 连接管理 + schema 初始化 + 表结构创建 │ │ │ ├── settings.ts # 配置服务(基于 configs 表的 key-value 存储) │ │ │ ├── task.ts # 任务服务(每日流程 CRUD + 流程推进 + 预设种子 + 指标 CRUD 与完成强校验) │ │ │ ├── metric.ts # 任务指标执行服务(截图验证/应用启动/网页截图/系统操作) │ │ │ ├── schedule.ts # 日程与待办服务 │ │ │ ├── build.ts # 构建服务(构建项目 CRUD + git 分支查询 + git 操作:pull(分叉冲突带人工解决)/列出分支/切换分支/新建分支/推送分支/合并分支(带冲突人工解决) + 一键构建编排:git pull/打包指令/archiver 压缩/汇总 zip) │ │ │ ├── deploy.ts # 部署服务(远程环境列表 + 产物 zip 2MB 分块上传 + 触发远程部署,原生 fetch,地址取自 envUrls) │ │ │ ├── envUrls.ts # 环境远程服务配置服务(configs 表 env.serviceUrls 存取,环境可动态增删改,代码不内置地址,构建/挡板/日志共用) │ │ │ ├── shieldData.ts # 挡板数据服务(挡板环境列表 + 读取/保存挡板服务模拟数据,原生 fetch 真实接口,地址取自 envUrls) │ │ │ ├── system.ts # 系统信息服务(平台信息 / 打开URL路径 / 执行系统命令 / 切换内外网代理) │ │ │ ├── logService.ts # 日志服务(远程日志列表查询 + 流式下载落盘 + 读取内容到编辑器,协议同第一代 bronya,地址取自 envUrls) │ │ │ ├── editorFiles.ts # 编辑器文件服务(标签页本地文件关联持久化:恢复/失效清理/落盘/upsert/状态同步/存在性检查) │ │ │ ├── mindMap.ts # 脑图服务(数据目录下 .km 文件列举 + 读取 + 路径校验防目录穿越 + kityminder 入口 URL 解析) │ │ │ ├── requirementAnalysis.ts # 需求分析服务(CSV 解析 + 按需求号汇总 + 手动输入需求号双向差集,纯数据加工) │ │ │ ├── designDocuments.ts # 设计挂接服务(CSV 任务 + 扫描目录 readdir 匹配名称含 ID 的文档,区分版本缺失/未挂接) │ │ │ ├── tray.ts # 托盘服务(托盘图标 + 右键菜单 + 退出状态标记) │ │ │ └── autoLaunch.ts # 开机启动服务(auto-launch 封装) │ │ ├── ipc/ │ │ │ ├── handlers/ # IPC handler 按域拆分 │ │ │ │ ├── system.ts # 系统域 + 窗口控制域 handler │ │ │ │ ├── settings.ts # 设置域 handler │ │ │ │ ├── task.ts # 任务域 handler │ │ │ │ ├── schedule.ts # 日程域 handler │ │ │ │ ├── log.ts # 日志域 handler │ │ │ │ ├── build.ts # 构建域 handler(项目 CRUD/分支查询/一键构建/环境列表/远程上传/触发部署 + 进度推送) │ │ │ │ ├── shield.ts # 挡板域 handler(环境列表/读取/保存挡板数据) │ │ │ │ ├── editor.ts # 编辑器文件域 handler(恢复/保存对话框+落盘入库/状态同步/存在性检查) │ │ │ │ ├── mindMap.ts # 脑图域 handler(列举/读取 .km 文件 + kityminder URL + 数据目录存取) │ │ │ │ ├── requirementAnalysis.ts # 需求分析域 handler(CSV 文件读取 + 调用分析服务) │ │ │ │ ├── designDocuments.ts # 设计挂接域 handler(CSV 文件读取 + 调用挂接分析服务) │ │ │ │ └── database.ts # 数据库域 handler │ │ │ └── index.ts # 汇总注册所有 handler │ │ ├── window/ │ │ │ └── mainWindow.ts # 主窗口创建(无边框 frame:false + 自绘标题栏) │ │ └── index.ts # 主进程入口:生命周期 + DB初始化 + IPC注册 + bronya2 本地文件协议 │ └── preload/ # 预加载层 │ ├── apis/ # 各域 preload API │ │ ├── system.ts # 系统域 + 窗口控制 │ │ ├── settings.ts # 设置域 │ │ ├── task.ts # 任务域 │ │ ├── schedule.ts # 日程域 │ │ ├── log.ts # 日志域 │ │ ├── build.ts # 构建域(含 build:progress 进度事件订阅、远程上传/部署) │ │ ├── shield.ts # 挡板域 │ │ ├── editor.ts # 编辑器文件域 │ │ ├── mindMap.ts # 脑图域(kityminder URL + .km 文件列举/读取 + 数据目录存取) │ │ ├── requirementAnalysis.ts # 需求分析域(CSV 导入 + 手动需求号比对分析) │ │ ├── designDocuments.ts # 设计挂接域(CSV 任务 + 扫描目录挂接检查) │ │ └── database.ts # 数据库域 │ └── index.ts # preload 入口:汇总暴露 + loading 占位 ├── src/ # 渲染进程(Vue 3 SPA) │ ├── main.ts # 渲染入口:挂载 Element Plus + 根组件 │ ├── App.vue # 根组件:布局壳 + 全屏背景层 + 动态面板出口 │ ├── vite-env.d.ts # 环境类型声明 + window API 类型补全 │ ├── router/ # 自实现面板路由(不使用 vue-router) │ │ ├── panels.ts # 面板注册表(key/title/icon/component) │ │ └── index.ts # 路由状态管理:currentKey + switchPanel │ ├── api/ # 渲染端双栈 API 包装层(PC 走 IPC / Web 走 HTTP) │ │ ├── runtime.ts # 平台检测:isElectron(运行时探测 window.systemApi) │ │ ├── http.ts # HTTP 客户端(fetch 封装 + SSE 进度流),Web 端通道 │ │ ├── system.ts # 系统域 API(系统信息 / 打开URL路径 / 执行系统命令 / 切换内外网代理 / 窗口控制) │ │ ├── settings.ts # 设置域 API │ │ ├── task.ts # 任务域 API │ │ ├── schedule.ts # 日程域 API │ │ ├── onlineTools.ts # 在线工具域 API │ │ ├── log.ts # 日志域 API │ │ ├── build.ts # 构建域 API │ │ ├── shield.ts # 挡板域 API │ │ ├── editor.ts # 编辑器文件域 API │ │ ├── mindMap.ts # 脑图域 API(kityminder URL + .km 文件列举/读取 + 数据目录存取,PC 走 IPC / Web 走 HTTP) │ │ ├── requirementAnalysis.ts # 需求分析域 API(CSV 导入 + 手动需求号比对,PC 走 IPC / Web 走 HTTP) │ │ ├── designDocuments.ts # 设计挂接域 API(CSV 任务 + 扫描目录挂接检查,PC 走 IPC / Web 走 HTTP) │ │ ├── database.ts # 数据库域 API │ │ ├── aiChat.ts # AI 对话 API(SSE 流式请求 + 配置入库 aiChat.config + 角色性格 + 提示语模板 aiChat.templates,纯前端 fetch) │ │ └── index.ts # 统一出口 │ ├── composables/ # Vue Composables │ │ ├── monaco.ts # Monaco 公共能力(主题/语言注册 + useMonacoTheme + 共享编辑器选项) │ │ ├── useTheme.ts # 主题切换 + 背景图 + 透明度(持久化);导出 bgLayerStyle computed 与 bgLayerStyleWith(alpha)(背景图+主题遮罩样式,App 外壳与锁屏共用,锁屏传入固定轻 alpha) │ │ ├── useLockScreen.ts # 锁屏状态(模块级单例 ref:locked / lock / unlock) │ │ ├── useSidebar.ts # 侧边栏宽度 + 收缩状态 + 二级分组展开折叠(持久化) │ │ ├── useOpenInEditor.ts # 代码编辑器桥接:openInEditor 公共方法(外部模块跳转编辑器打开内容) │ │ ├── useIpc.ts # IPC 调用封装(callIpc / callIpcSilent) │ │ └── useQuickSearch.ts # 全局快捷搜索状态 + 双击 Shift 监听 + 内置工具触发通道 + openQuickSearch(prefill) 预填关键字 │ ├── components/ # 公共组件 │ │ ├── icons/ │ │ │ └── QyIcon.vue # 统一图标组件(EP 图标 + 内联 SVG 回退) │ │ ├── common/ │ │ │ ├── QyCard.vue # 玻璃拟态卡片 │ │ │ ├── QyPanelHero.vue # 面板标题 banner 统一组件(图标+标题+副标题+右侧操作插槽,颜色全走主题 token) │ │ │ └── QySmartInput.vue # 灵动输入框(el-input + suffix 星光按钮,内嵌 AI 提示语模板选择与处理) │ │ ├── feedback/ │ │ │ ├── QyQuickSearch.vue # 全局快捷搜索弹窗(双击 Shift 唤起,搜索菜单/在线/本地/内置工具;输入 > 前缀进入 AI 对话模式;支持打开时预填关键字,如看板娘气泡预填 "> ") │ │ │ └── QyLockScreen.vue # 锁屏浮层(幕布自上而下扣落 + 数字时钟 HH:MM:SS + 日期星期,上拖解锁;详见「核心模块」) │ │ ├── business/ │ │ │ └── QyLive2DMascot.vue # 右下角 Live2D 看板娘(pixi.js 6 + pixi-live2d-display Cubism2;画布加载后按像素包围盒贴合人物;视线跟随 + 点击命中区域触发 tap_* 动作 + 双击人物直达快捷搜索 AI 对话 + 拖拽人物仅左右移动(bottom 锚点固定,夹取在视口 8px 边距内,刷新回默认位);消息气泡:15s 空闲冒泡循环,攒 3-5 句小 tips 后触发一次动作+台词,点击气泡同样直达 AI 对话;气泡空闲时头顶常驻「双击和我聊聊呀~」可爱提示胶囊;悬浮右上角小叉叉临时隐藏,刷新后按设置恢复;App.vue 异步组件挂载) │ │ └── layout/ │ │ ├── QyTitleBar.vue # 自定义无边框标题栏(Logo + 窗口控制按钮) │ │ └── QySideNav.vue # 侧边导航(可拖拽宽度 + 可收缩 + 收缩态悬停 flyout + 二级分组展开折叠 + 复位) │ ├── views/ # 面板视图 │ │ ├── Dashboard/index.vue # 仪表盘(快捷入口搜索过滤 + 回车跳转) │ │ ├── DailyProcess/ # 每日流程(任务与指标强绑定 + 顺序执行) │ │ │ ├── index.vue # 面板主组件 │ │ │ └── components/ # 页面私有对话框(TaskEditDialog 指标配置 / TaskRunDialog 指标执行) │ │ ├── Schedule/index.vue # 日程安排 │ │ ├── ProgrammerTools/index.vue # 程序员工具(在线/本地/内置三大分类 tab,支持拖拽调整顺序并持久化到 localStorage) │ │ ├── CodeEditor/index.vue # 代码编辑器(Monaco 多标签多语言,含 XML;本地文件关联持久化 + 待保存态 + 标签右键打开所在目录 + openInEditor 外部打开) │ │ ├── LogList/index.vue # 日志中心(远程日志列表 + 搜索 + 操作栏「查看」跳转编辑器以待保存页签查看 /「下载」落盘本地,Web 端浏览器直接下载) │ │ ├── BuildDebug/ # 构建调试(前后端项目一键打包 + 远程上传/触发部署 + 清理残留) │ │ │ ├── index.vue # 面板主组件(项目多选/一键构建/远程上传/触发部署/清理残留/状态与耗时展示/日志) │ │ │ └── components/ # 页面私有对话框(ProjectEditDialog 项目配置、CleanConfirmDialog 清理确认、EnvDetail 环境详情、StageFlowDialog 阶段跃迁工作流、MergeConflictDialog 合并分支冲突解决) │ │ ├── ShieldData/ # 挡板数据配置(读取/修改/保存服务器挡板服务模拟数据,结构沿用第一代 bronya) │ │ │ ├── index.vue # 面板主组件(环境加载/保存 + 可视化表格与 Monaco JSON 全文双视图 + 接口条目增删改) │ │ │ └── components/ # 页面私有组件(JsonEditor Monaco JSON 编辑、EntryDialog 接口条目编辑、MatcherDialog 匹配器编辑) │ │ ├── MindMap/index.vue # 脑图(kityminder 思维导图浏览,左侧 .km 文件列表 + 右侧 iframe 加载,postMessage 传递 JSON 数据) │ │ ├── DataAnalysis/ # 数据分析(子 tab:需求分析 / 设计挂接) │ │ │ ├── index.vue # 面板主组件(hero + 子 tab 导航) │ │ │ └── components/ # 页面私有组件(RequirementAnalysis 需求分析、DesignDocuments 设计挂接) │ │ └── Settings/index.vue # 设置(含 Maven / Node.js / Git 环境配置 + 脑图数据目录) │ ├── types/ # 共享类型定义 │ │ ├── domain.ts # 业务领域模型 + 默认设置 + 预设任务 │ │ └── ipc.ts # IPC channel 常量 + 请求/响应类型 + API 接口 │ ├── assets/ │ │ ├── images/ # 图片资源(bronya.png, bg.png) │ │ └── styles/ # 全局样式 │ │ ├── variables.scss # 主题色彩 token + 设计常量(间距/圆角/阴影) │ │ ├── animations.scss # 通用动画关键帧 │ │ └── global.scss # 全局重置 + 滚动条 + 选中文本 │ └── utils/ │ ├── format.ts # 日期/文本/JSON/XML 格式化工具(formatJson/minifyJson/formatXml/minifyXml 共享) │ ├── fuzzyMatch.ts # 拼音模糊匹配 + 命中区间切分(pinyin-pro 的 match(),供快捷搜索高亮渲染) │ ├── markdown.ts # Markdown 渲染(marked + highlight.js,代码块注入复制按钮) │ └── path.ts # 本地路径工具:目录名/文件名解析 + 绝对路径转 bronya2:// 协议 URL ├── public/ # 静态资源(图标等) │ ├── bronya.ico # Windows 任务栏图标 │ ├── bronya.png # 备用图标 │ └── live2d/ # 看板娘资源(Cubism2 原生运行时 live2d.min.js + bronya_1 模型:moc/贴图/动作) ├── server/ # Web 版服务端(Express,复用主进程 services) │ ├── index.ts # 入口:初始化数据库 + 监听端口(默认 3210,PORT 可覆盖) │ ├── app.ts # 组装:CORS / JSON / 请求日志 / 路由挂载 / 静态托管 dist-web + /kityminder / SPA fallback │ ├── tsconfig.json # 服务端独立 TS 配置(vue-tsc 不检查该目录) │ ├── lib/ │ │ ├── response.ts # 统一响应 ok()/fail(),结构与 IpcResponse 一致 │ │ └── sse.ts # SSE 辅助(构建/上传/部署进度推送) │ └── routes/ # 按业务域拆分,与 electron/main/ipc/handlers/ 一一对应 │ ├── settings.ts # 设置域(含开机启动、环境服务配置) │ ├── tasks.ts # 任务域(含指标管理,指标执行降级) │ ├── schedules.ts # 日程与待办域 │ ├── onlineTools.ts # 在线工具域 │ ├── database.ts # 数据库域 │ ├── editor.ts # 编辑器文件域 │ ├── logs.ts # 日志域(下载经本地中转,Web 端 download-file 流式透传给浏览器) │ ├── build.ts # 构建域(run/upload/deploy 为 SSE 进度流) │ ├── shield.ts # 挡板数据域 │ ├── mindMap.ts # 脑图域(.km 文件列举/读取 + 数据目录存取,kityminder 静态托管由 app.ts 注册) │ ├── requirementAnalysis.ts # 需求分析域(按用户选中的 filePath 读 CSV,任意服务器路径 → 调用分析服务返回三区块结果) │ └── designDocuments.ts # 设计挂接域(读 CSV + 扫描服务端目录匹配挂接文档,返回正常/未挂接两区块) ├── extra/ # 随包发布的独立资源(不参与 Vite 构建,由 electron-builder files 打包 / server 静态托管) │ └── kityminder/ # kityminder 思维导图(百度脑图同款,PC 走 bronya2://file 协议 / Web 走 /kityminder 静态托管) │ ├── index.html # 入口(支持 ?fromParent=1 模式,监听 parent.postMessage 加载 JSON 数据) │ └── ... # kityminder 自身静态资源(js/css/图片等) ├── vite.config.ts # Vite 配置(按 --mode 区分 PC / Web,含 electron 插件与 /api 代理) ├── vite.config.server.ts # 服务端独立构建配置(SSR 模式编译 server 层 → dist-server 部署包) ├── package.json └── README.md ``` --- ## 一、PC 版(Electron) ### PC 版快速开始 ```bash # 开发模式启动 npm run dev # 类型检查 + 构建 + 打包(产出 release// 安装包) npm run build # 仅预览渲染层 npm run preview ``` ### PC 版发布打包 `npm run build` 末尾执行 electron-builder,按 `electron-builder.json` 的 `win.target` 同时产出两种 Windows x64 安装包到 `release//`: | target | 产物 | 说明 | | ---------- | --------------------------------------- | -------------------- | | `nsis` | `bronya-Windows--Setup.exe` | 安装版(非一键安装,可选安装目录) | | `portable` | `bronya-Windows--Portable.exe` | 免安装单文件版(运行时自解压到临时目录) | ### 架构设计 #### 三层架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 渲染进程 (Vue 3 SPA) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Dashboard │ │DailyProcess│ │ Schedule │ │ Settings │ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ └──────────┬───┴──────────────┴─────────────┘ │ │ api/ (IPC 包装层) │ │ composables/ (状态管理) │ └──────────────────────┬──────────────────────────────────────┘ │ contextBridge (安全隔离) ┌──────────────────────┴──────────────────────────────────────┐ │ Preload (预加载层) │ │ 暴露 systemApi / settingsApi / taskApi 等 │ └──────────────────────┬──────────────────────────────────────┘ │ ipcRenderer.invoke / ipcMain.handle ┌──────────────────────┴──────────────────────────────────────┐ │ 主进程 (Electron Main) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ IPC Handlers│ │ Services │ │ Database │ │ Window │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` #### IPC 通信流程 ``` 渲染进程调用 Preload 暴露 主进程处理 api/systemApi.getInfo() → window.systemApi.getInfo() → ipcMain.handle('system:get-info') ↑ ↑ ↓ types/ipc.ts preload/apis/ ipc/handlers/ (类型定义) (contextBridge) (业务逻辑 + 返回 IpcResponse) ``` #### 双版本架构(PC / Web) 项目同时支持两种运行形态,共用同一渲染层代码与同一批业务 service: | 版本 | 开发命令 | 构建命令 | 渲染层通道 | 业务层 | | -------------------- | ----------------- | ------------------- | ------------------------------------------ | -------------------------- | | PC 版(Electron) | `npm run dev` | `npm run build` | `src/api/*` → preload → IPC → handler | `electron/main/services/*` | | Web 版(浏览器 + Express) | `npm run dev:web` | `npm run build:web` | `src/api/*` → HTTP/SSE → `server/routes/*` | **复用同一批 services** | - **业务逻辑只写一份**:`electron/main/services/` 是唯一业务实现,IPC handler(PC)与 HTTP route(Web)都是薄适配层 - 平台检测在运行时完成(`src/api/runtime.ts` 的 `isElectron = !!window.systemApi`),同一份渲染层代码无需区分构建目标即可双端运行 - Web 端与 PC 端共享同一数据库文件 `~/.bronya2/bronya2.db`,数据无缝互通 - 构建进度(`build:run`/`upload`/`deploy`)在 Web 端经 SSE 推送,由 `src/api/http.ts` 统一消费 ### PC 端独立 CLI 工具:bronya2-cli.exe(pkg 打包) > 写库命令**不再混在 Electron 主进程里**,而是基于 `pkg` 单独打包成一个轻量可执行文件 `bronya2-cli.exe`,与 GUI 安装包**一起发布**但完全解耦: > - 调用它不会启动任何窗口,执行完毕立即退出 > - 退出码可用于 git hook / CI 判定成功失败 > - GUI 主进程彻底"纯 GUI",启动窗口时不再解析任何写库参数 #### 发布位置 打包 / 安装后的目录结构: ``` <项目源码>/ extra/bin/ ├── index.cjs # CLI 入口源码 ├── bronya2-cli.exe # pkg 打包后的单文件可执行 └── better_sqlite3.node # better-sqlite3 win32-x64 原生驱动(pkg 不能内嵌 .node,需随 exe 一起发布) <安装后路径>/ 布洛尼亚2-Setup.exe / 布洛尼亚2-Portable.exe └── resources/extra/bin/ ← electron-builder 通过 extraResources 自动把 extra/ 原样打进安装包 ├── bronya2-cli.exe └── better_sqlite3.node ``` 开发态单独构建 CLI: ```bash # 默认使用 package.json.pkg.targets(当前默认 node22-win-x64) npm run build:cli # 需要 node26 时可用环境变量覆盖(需 @yao-pkg/pkg 支持该目标) # CMD: set PKG_TARGET=node26-win-x64 && npm run build:cli # PowerShell: $env:PKG_TARGET="node26-win-x64" ; npm run build:cli ``` 构建桌面安装包时 CLI 会**自动**一起打包并发布: ```bash npm run build:pc # 等价链路:vue-tsc + vite build → npm run build:cli → electron-builder(extraResources 含 extra/bin) ``` #### 调用方式(CMD) ```bat REM ====== 安装后路径(最常用): "C:\Program Files\布洛尼亚2\resources\extra\bin\bronya2-cli.exe" ^ --commit-hash=a1b2c3d4e5f6 ^ --commit-content="fix: 修复登录页白屏问题" ^ --commit-lines="+128/-24" ^ --commit-repos=bronya2 ^ --commit-date=2026-09-03 REM ====== 开发态(还没安装,直接跑源码目录里 build:cli 产出的那份): "C:\Users\Syste\Developer\WebstormProjects\bronya2\extra\bin\bronya2-cli.exe" ^ --commit-hash=abc123 ^ --commit-content="feat: 新增提交记录面板" ^ --commit-lines=+256 ^ --commit-repos=bronya2 ``` #### 参数列表 | 参数 | 必填 | 退出码影响 | 说明 | | -------------------- | -- | ----- | ------------------------------------------------------ | | `--commit-hash` | ✅ | 缺 → 2 | 提交哈希 | | `--commit-content` | ✅ | 缺 → 2 | 提交信息(message) | | `--commit-lines` | ✅ | 缺 → 2 | 变更行数 | | `--commit-repos` | ✅ | 缺 → 2 | 仓库名 | | `--commit-date` | ⬚ | — | 提交日期 `YYYY-MM-DD`,未填默认当天 | | `--commit-timestamp` | ⬚ | — | 提交时间戳(unix 秒字符串),未填默认当前时间 | | `--help` / `-h` | ⬚ | → 0 | 打印使用说明 | **环境变量**: | 变量名 | 说明 | | --------------- | -------------------------------------------------------------------- | | `BRONYA2_DB_PATH` | 自定义数据库路径。未填时与 GUI 保持同一位置:`%USERPROFILE%\.bronya2\bronya2.db`(推荐留默认,GUI 才能读到数据) | | `PKG_CACHE_PATH` | 构建 CLI 时 pkg 的下载缓存目录(默认项目根下 `.pkg-cache`,避免 Windows 权限问题) | #### 退出码约定 | 退出码 | 含义 | | --- | -------------------------------------------- | | 0 | 成功(inserted 或 duplicate skipped,两者都算幂等成功) | | 1 | 运行时错误(.node 缺失 / DB 打不开 / DB 执行异常 / 原生驱动 ABI 不匹配) | | 2 | 参数错误(缺少必填项 / 用法错误) | #### 写入规则 / 与前端联动 - 同一条 `hash` 多次写入自动幂等跳过(第二次会打印 `SKIP duplicate hash=...`,退出码仍为 0) - CLI 首次运行会 `CREATE TABLE IF NOT EXISTS commit_records + 索引`,不依赖 GUI 先启动 - 前端「仪表盘」展示所有 `copied=0` 的记录,复制后自动标记已复制并从主页消失 - 「提交记录」面板支持时间范围、快捷日期(今天/昨天/本周/本月)、关键词搜索、状态筛选(全部/未复制/已复制),可按哈希跳转详情页(地址模板在「设置 → 系统」里配置,支持 `{hash}` 占位符);提供「一键全开」按钮按当前筛选条件批量打开详情页,超过 30 条提示收窄筛选条件 #### 典型集成:Git post-commit hook 将提交自动塞进 Bronya2 的本地数据库(每次 commit 完静默写一条,不弹窗): ```bat REM 在 %REPO%\.git\hooks\post-commit.bat 里写: @echo off chcp 65001 >nul setlocal for /f %%h in ('git rev-parse HEAD') do set "HASH=%%h" for /f "delims=" %%m in ('git log -1 --pretty^=%%s') do set "MSG=%%m" for /f %%l in ('git diff --stat HEAD~1..HEAD ^| findstr /r "file.*changed" ^| findstr /o "." ') do set "LINES=%%l" set "REPOS=%~n0" REM 安装后 bronya2-cli.exe 的路径;按实际安装目录改 set "BRONYA_CLI=C:\Program Files\布洛尼亚2\resources\extra\bin\bronya2-cli.exe" "%BRONYA_CLI%" --commit-hash="%HASH%" --commit-content="%MSG%" --commit-lines="%LINES%" --commit-repos="%REPOS%" exit /b %ERRORLEVEL% ``` #### 常见问题 1. **报错:找不到 better_sqlite3.node** 请确认 `bronya2-cli.exe` 同级目录里存在 `better_sqlite3.node`。安装包内两者本就一起发布;若自行拷贝部署,请成对拷贝。 2. **`unable to open database file`** - 可能当前账号对 `%USERPROFILE%\.bronya2\` 没有写入权限 → 用管理员身份试一次;或 - 设置 `BRONYA2_DB_PATH` 指向一个可写路径(注意 GUI 也要用同一个值才能读到)。 3. **`Unknown target: node26-win-x64`** `@yao-pkg/pkg` 目前对 node26 支持仍在推进,改用稳定版目标即可:`set PKG_TARGET=node22-win-x64 && npm run build:cli`(better-sqlite3 v13 的 .node 基于 N-API,跨 Node 小版本完全兼容)。 下载时网络原因可能下载不下来node编译包,可以手动下载,或者等一等下载 ### 构建调试:Git Hook 注入(提交记录自动推送) 在「构建调试」面板中,每个项目卡片新增 **Git Hook** 按钮,点击后自动识别当前运行端并注入 `post-commit` 钩子到该项目仓库的 `.git/hooks/post-commit`: | 运行端 | 注入的推送方式 | | ------ | -------------- | | PC 版(Electron) | 钩子调用本地 `bronya2-cli.exe`(`--commit-hash/content/lines/repos`),写入本地数据库 | | Web 版(服务端) | 钩子 `curl` 调用 `POST /api/commit-records/push`(地址取注入时的 `serverUrl`,缺省由服务端从请求推导),写入服务端数据库 | 行为约定: - 注入前弹**预览确认框**:展示目标 hook 文件路径与将写入的脚本内容,脚本内容支持一键复制;点击「确定注入」才写文件,取消则不注入 - 钩子为 `sh` 脚本(Git for Windows 同样适用),每次提交后静默推送,**失败不影响提交**(`|| exit 0`) - 提交信息为 UTF-8 中文时完整保留(服务端钩子通过 stdin 管道 `--data-binary @-` 传 JSON,绕开 Windows 命令行参数转码) - 提交哈希用 `git rev-parse HEAD`,变更行数用 `git show --numstat`,仓库名取构建项目名 - 非 Git 仓库 / 找不到构建项目会返回明确错误;PC 端未生成 `bronya2-cli.exe` 时提示先执行 `npm run build:cli` - 项目加载时自动检测 hook 注入状态:读取 `post-commit` 文件内容,**仅当包含 Bronya2 注入标记(`由 Bronya2 构建调试注入`)时**按钮变绿,用户手写的 hook 不会被误判 ### 预设标准流程任务 应用首次打开某日时自动创建 4 条预设任务: | 序号 | 标题 | 描述 | | -- | ---- | ---------------------- | | 0 | 上班打卡 | 到达工位后第一时间完成上班打卡 | | 1 | 写TFS | 编写今日 TFS 任务计划,明确当日工作内容 | | 2 | 发邮件 | 处理并回复当日邮件,必要时发送工作汇报 | | 3 | 下班打卡 | 结束当日工作前完成下班打卡 | ### PC 端窗口配置 - `frame: false`:无边框窗口 - `autoHideMenuBar: true`:隐藏菜单栏 - 默认尺寸:1280 x 820,最小 960 x 640 - `contextIsolation: true` + `sandbox: true`:安全隔离 - 图标加载优先 `.ico`,回退 `.png` ### 核心模块(PC 端独有 + 渲染层共享) #### 主题系统 (useTheme) - 四套主题(樱花 sakura / 天蓝 ocean / 星暮 twilight / 暗夜 dark),通过 CSS 自定义属性 + `data-theme` 属性切换 - 背景图支持自定义,无自定义时回退到 `bg.png` - 自定义背景存储为本地绝对路径,渲染时经 `toLocalAssetUrl`(`src/utils/path.ts`)转为 `bronya2://` 协议 URL 加载 - 背景透明度可调 (0-100%),通过 `--bg-overlay` CSS 变量控制叠加层 - 主题色 token 定义在 `variables.scss`,Element Plus 变量同步覆盖 - 状态持久化到 `configs` 表(`theme.mode` / `theme.background` / `theme.bgOpacity`),并镜像到 `localStorage`(`bronya2.theme.*`)供 `index.html` 预热脚本秒读防 FOUC #### 本地文件协议 (bronya2) - 主进程在 `electron/main/index.ts` 注册自定义协议 `bronya2://`(与应用同名,app ready 前 `registerSchemesAsPrivileged`,ready 后 `protocol.handle`) - URL 约定:`bronya2://file/<编码后的绝对路径>`,handler 解析 pathname 后经 `net.fetch` 以文件流返回 - 用途:渲染进程在沙箱 + CSP 约束下安全加载本地文件(如自定义背景图),规避 `file://` 跨源限制 #### 侧边栏 (useSidebar + QySideNav) - 可拖拽调整宽度(180px ~ 360px,默认 220px) - 可收缩为图标模式(56px) - 二级分组(概览/日常管理/开发工具/系统设置)可点击标题展开/折叠,带 `grid-template-rows` 高度过渡动画与箭头旋转指示(过渡属性显式声明,避免 `transition: all` 造成卡顿) - 收缩态(图标模式)下仅展示一级分组图标,鼠标悬停时在右侧悬浮出实色背景 flyout 展示该分组的二级菜单(避免透明底与下层内容重叠);点击分组图标直接跳转该分组首个面板 - 当前激活面板所在分组的二级菜单不可见时(侧栏收缩或分组被折叠),一级分组标题/图标高亮显示,指示当前面板归属 - 展开宽度后恢复就地二级菜单的展开/折叠展示,各分组展开状态不受收缩操作影响 - 从其他面板跳转到被折叠分组内的面板时,自动展开对应分组 - 复位按钮:一键恢复默认宽度 - 状态持久化到浏览器 `localStorage`(`bronya2.sidebar.width` / `bronya2.sidebar.collapsed` / `bronya2.sidebar.groups`),不再入库 - CSS 过渡动画:`width 0.28s cubic-bezier(0.4, 0, 0.2, 1)` #### 无边框窗口 (QyTitleBar) - `frame: false` 全平台自绘标题栏 - `-webkit-app-region: drag` 实现拖拽移动窗口 - 窗口控制按钮:最小化 / 最大化-还原 / 关闭,使用内联 SVG 图标 - 品牌 Logo 使用 `bronya.png`,任务栏图标使用 `bronya.ico` - 主题切换 + 邮件快捷入口集成在标题栏 - 锁屏入口:标题栏「打开邮件软件」旁的锁形按钮(`useLockScreen.lock()`),触发全屏锁屏浮层 #### 锁屏浮层 (QyLockScreen) - 幕布从屏幕上方扣落(CSS keyframes,顶部锚点 + 轻微回弹),解锁时向上滑出;背景复用 `useTheme.bgLayerStyleWith(0.15)`(与主界面同一张背景图,但遮罩不跟随设置里的透明度,固定 15% 轻遮罩保证背景清晰显示) - 数字时钟:中央 `HH:MM:SS` 大字(tabular-nums 等宽数字),`setInterval` 每秒刷新 `now` ref,仅在锁屏期间运行;下方玻璃胶囊显示日期 + 星期(`Intl.DateTimeFormat('zh-CN')`,如「2026年9月22日 · 星期二」) - 解锁引导:底部三枚向上箭头(内联 SVG chevron)循环缓动上浮、错峰淡入淡出(`animation-delay: calc((var(--i) - 1) * 0.35s)`) - 解锁方式:按住幕布向上拖动(Pointer Events + setPointerCapture,`dragY` 仅取负值即只响应上拖),超过视口高度 22% 松手滑出解锁(内联 transform 过渡到 `-100vh` 后延时 `unlock()`),未到阈值松手弹回原位;点击/按键不再解锁,`touch-action: none` 防手势干扰 - 状态为 `src/composables/useLockScreen.ts` 模块级单例 ref;浮层常驻 App.vue,`` 仅负责入场扣落动画(出场由拖拽内联 transform 完成),`z-index: $z-lock(9000)` 盖过 Element Plus 弹窗 #### 托盘与关闭确认 (tray 服务) - 主窗口 `close` 事件被拦截(`mainWindow.ts`):非真正退出时不销毁窗口,经 `window:close-requested` 推送渲染端弹出 `ElMessageBox` 确认(最小化到托盘 / 直接关闭 / Esc 取消) - 真正退出标记:`services/tray.ts` 维护 `quitting` 状态(`markQuitting` / `isQuitting`),托盘菜单退出与 `window:quit` 均先置位再 `app.quit()`,关闭拦截据此放行 - 托盘图标使用 `bronya.ico`(回退 `bronya.png`),右键菜单:显示主窗口 / 最小化到托盘 / 退出布洛尼亚2;左键单击与双击均唤起主窗口 - 窗口隐藏后应用驻留后台;重复启动(`second-instance`)时从托盘恢复显示主窗口 #### 面板路由 (usePanelRouter) - 自实现路由,不依赖 vue-router - `PANELS` 数组注册所有面板(key/title/subtitle/icon/component) - `currentKey` ref 维护当前激活面板 - `` 动态渲染,Transition 过渡动画 - 顶部页签栏(App.vue,风格对齐 vue3-element-admin):每页签显示面板图标 + 标题,激活态带主题色圆点;pinned 页签(仪表盘)无关闭按钮,其余可点 × 或鼠标中键关闭;右键页签或点击栏右端下拉按钮弹出统一操作菜单(刷新 / 关闭 / 关闭其他 / 关闭左侧 / 关闭右侧 / 关闭所有,按 pinned 与位置自动置灰);「刷新」经 keep-alive include 临时剔除当前面板名实现缓存销毁重建(`closeAll` 定义在 `src/router/index.ts`,仅保留 pinned);横向溢出滚动条默认隐藏、悬浮页签栏时才出现 #### 图标系统 (QyIcon) - 15 个常用图标映射到 `@element-plus/icons-vue`(folder/refresh/plus/check 等) - 自定义图标保留内联 SVG(mail/skip/sakura/sparkles/edit/code/warning/up/down/settings 等) - 自动回退:有 EP 映射用 EP 组件,无映射则用内联 SVG #### 程序员工具面板 (ProgrammerTools) - 顶部三个分类 tab:**在线工具 / 本地工具 / 内置工具**,可拖拽调整顺序,顺序持久化到 localStorage 键 `bronya2.programmerTools.tabOrder`(仅前端 UI 偏好,无需跨端同步,缺失或非法时回退默认顺序并补齐新增 tab) - 各 tab 下的**卡片同样支持拖拽调整顺序**: - **在线工具 / 本地工具**:拖拽后直接调用 `onlineToolApi.reorder` / `localToolApi.reorder`(IPC channel `online-tools:reorder` / `local-tools:reorder`,Web 端走同名 HTTP),把新顺序写入 SQLite `sortOrder` 字段;与已有管理弹窗「上移 / 下移」按钮共用同一落库逻辑 - **内置工具**:硬编码清单(加密解密 / JSON·XML / 正则可视化 / 接口请求 / 图片元数据 / 图片缩放),无 DB 表,顺序持久化到 localStorage 键 `bronya2.programmerTools.builtinOrder`(缺失或非法时回退默认顺序并补齐新增工具);清单同时硬编码在 BuiltinToolsTab、ProgrammerTools/index、QyQuickSearch 三处,新增内置工具需三处同步 - 拖拽实现采用**原生 HTML5 drag**(`draggable=true` + `dragstart/dragover/drop/dragend`),不引入 SortableJS,避免在响应式数组上同步 DOM 的常见坑;卡片拖拽源半透明、悬停目标粉色边框高亮;搜索中(输入框有内容)禁用在线 / 本地工具卡片拖拽,避免在过滤子集上重排产生混淆 - 内置工具点击进入对应子视图,自带返回按钮 #### 加密解密工具 (CryptoTool) - 位置:程序员工具 → 内置工具 → 加密解密(`src/views/ProgrammerTools/components/CryptoTool.vue`) - 七大子模块:Base64 编解码 / 哈希(MD5/SHA-1/SHA-256/SHA-512/CRC32)/ 对称加密(AES·DES·3DES + ECB/CBC/CFB/OFB/CTR/GCM)/ RSA 加解密 / 签名验签(RSA·DSA·ECDSA)/ 密钥对生成 / 国密算法(SM2·SM3·SM4) - 核心实现基于 Node 内置 `crypto` 模块 + `zlib.crc32` + `sm-crypto`(`electron/main/services/cryptoTool.ts`,PC IPC handler 与 Web HTTP route 共用) - **国密算法(SM2/SM3/SM4)**:基于 `sm-crypto` 纯 JS 实现,支持 SM2 加解密 / SM2 签名验签 / SM2 密钥对生成(C1C3C2 与 C1C2C3 两种密文顺序、DER 或裸 hex 签名编码、可选 SM3 杂凑与自定义 userId)/ SM3 杂凑(hex 或 base64 输出)/ SM4 对称加解密(ECB·CBC、PKCS7 或无填充、密钥与 IV 支持 hex/utf8/base64 三种格式输入、输入输出可分别指定 hex/base64 编码);密钥与 IV 提供实时字节长度提示,与对称加密面板保持一致;Web 端部署包需携带 `sm-crypto`(已在 `vite.config.server.ts` 的 `RUNTIME_DEPS` 登记) - **错误处理**: - 主进程 `cryptoTool` service 在校验失败时抛出**带字段名的明确错误**(如 `[cipher.key] AES 密钥长度必须为 16 / 24 / 32 字节(对应 AES-128 / AES-192 / AES-256),当前为 X 字节`),IPC handler 与 HTTP route 统一捕获为 `{success: false, error}` 响应 - 前端 `safeCall` 包装层将后端 `error` 写入 `lastError` 响应式状态,并在页面顶部以**毛玻璃红色错误卡片**持续展示最近一次错误,同时通过 `ElMessage` 短促弹出 - 对称加密面板提供**前置字节长度提示**:实时显示密钥当前字节数与期望值(AES 16/24/32、DES 8、3DES 24),长度不匹配时输入框红色描边;IV 字段实时提示期望长度 - **历史抽屉**:完整保留每条操作的输入 / 输出 / IV / AuthTag / 公钥 / 私钥、操作参数(算法/模式/编码/填充等)、时间戳(`YYYY/MM/DD HH:mm:ss`)与失败时的完整错误信息;点击条目**可展开详情**,每个代码块支持单独复制;最多保留 50 条;操作状态(成功/失败)以圆点徽章区分 #### 正则可视化工具 (RegexVisualizer) - 位置:程序员工具 → 内置工具 → 正则可视化(`src/views/ProgrammerTools/components/RegexVisualizer.vue`) - 基于 `@wzo/regex-diagram`(纯 TypeScript、框架无关的正则铁路图渲染库),解析正则表达式为 AST → 布局 → 渲染 SVG 铁路图 - **正则输入**:`/pattern/flags` 字面量风格输入,标志(g/i/m/s/u/y)以可点击 chip 切换;内置 5 个常用示例(邮箱/手机号/IPv4/日期/颜色值)一键加载 - **铁路图可视化**:实时渲染正则结构的 SVG 铁路图(railroad diagram),支持横向滚动;语法/语义错误时以红色提示卡片展示错误信息 - **测试匹配**:输入测试文本后实时高亮所有匹配项,列出每个匹配的文本、位置与捕获分组(`$1`、`$2`…);HTML 转义防注入 - **npm 发布修补**:`@wzo/regex-diagram` 的 npm 发布缺少 `dist` 产物,通过 Vite alias + tsconfig paths 直接解析其 `src/index.ts` TypeScript 源码 #### 接口请求工具 (ApiTool) - 位置:程序员工具 → 内置工具 → 接口请求(`src/views/ProgrammerTools/components/ApiTool.vue`,键值对编辑子组件 `ApiKeyValueRows.vue`,通用 Monaco 代码块子组件 `ApiCodeBlock.vue`) - **Postman 风格双栏布局**:左栏项目 / 接口树(项目可展开,method 彩色标签;项目行悬停显示新建 / 导出 / 编辑 / 删除,接口行悬停显示重命名 / 删除);右栏请求行(方法选择 + URL + 保存 + 预览 + 发送)、Params/Headers/Body 标签页、响应面板(状态码 / 耗时 / 体积 / 编码 + 响应体 Monaco 高亮 + 响应头表) - **集合持久化**:项目与接口保存在 `~/.bronya2/api/data.json`(单文件 JSON,原子写,损坏转存不丢数据),不进 SQLite;项目 `baseUrl` 与接口相对路径在打开时拼接、保存时反推相对路径 - **Monaco 编辑器集成**:请求体(JSON/raw)与响应体统一使用 `ApiCodeBlock` 组件(基于项目共享的 `monaco.ts` composable),JSON / XML 支持格式化(统一走 `editor.action.formatDocument` + 共享 provider,XML 基于 xml-formatter,见依赖表),格式化反馈统一为 ElMessage + 前后内容比较;JSON 支持压缩(`minifyJson` 共享工具);语言自动检测;响应体根据 `content-type` 自动识别 json/xml/html - **Headers 双模式编辑**:支持表格 KV 编辑和原始文本粘贴模式(`Key: Value` 每行一条),原始文本模式下点击「应用到表格」解析为 KV 行;请求行「预览」按钮弹出原始 HTTP 请求报文(包含起始行、Host、所有启用的 headers、form 表单已编码 body) - **编码支持(iconv-lite)**:请求体可选择 `utf-8` / `gbk` 编码发送(GBK 时用 iconv-lite 编码为 Buffer),响应体可指定 `utf-8` / `gbk` / 自动(默认自动:先看用户指定,再看响应头 `content-type` 的 `charset`,都没就 utf-8);响应实际编码在响应头区域显示为 tag;响应体原始字节(base64)随结果返回,切换「响应解码」选项时前端用原始字节实时重解码(无需重发请求) - **请求由后端发出**:渲染层传 `{ method, url, headers, bodyType, body, requestEncoding?, responseEncoding?, timeoutMs? }`,PC 主进程 / Web 服务端用 Node 全局 `fetch` 发起(绕开浏览器同源限制),`AbortController` 超时控制;Query 参数仅启用且键非空的行生效;Web 端部署包需携带 `iconv-lite`(已在 `vite.config.server.ts` 的 `RUNTIME_DEPS` 登记) - **OpenAPI 互转(仅 JSON,零新增依赖)**:导入兼容 OpenAPI 3.x 与 Swagger 2.0,本地解析 JSON Schema `$ref`(环检测 + 深度限制)生成请求体示例;导出固定 OpenAPI 3.0.0,产物可直接被 Postman Import;YAML 明确不支持,读取到非 JSON 时提示先转 JSON - **双端文件选择 / 保存约定(不使用浏览器原生选择 / 下载)**:PC 端导入走 `settingsApi.selectFile` 系统对话框、导出走 `selectDir` + `fsApi.saveTextFile`;Web 端统一走自建资源管理器 `FileBrowserDialog`(file 模式选导入源、dir 模式选导出目录)+ 同一个 `fsApi.saveTextFile` 写盘;导入源文件与导出目录分别缓存到 localStorage 键 `bronya2.apiTool.importPath` / `bronya2.apiTool.exportDir`,作为 PC 对话框 `defaultPath` 与资源管理器 `initialPath` 回填,进入面板时用 `fsApi.exists` 校验并清除失效缓存 #### 图片元数据工具 (ImageMetaTool) - 位置:程序员工具 → 内置工具 → 图片元数据(`src/views/ProgrammerTools/components/ImageMetaTool.vue`,字段表与校验器 `imageMetaSpec.ts`,service `electron/main/services/imageMeta.ts`,IPC handler `electron/main/ipc/handlers/imageMeta.ts`,preload `electron/preload/apis/imageMeta.ts`,Web route `server/routes/imageMeta.ts`,渲染层双栈包装 `src/api/imageMeta.ts`) - 基于 `exiftool-vendored`(v38.1.0,singleton 模式)实现 EXIF / XMP / IPTC 元数据读取与编辑:`exiftool.read()` 返回 Tags(含 `ExifDateTime` 等自定义类,service 层 `JSON.parse(JSON.stringify(tags))` 剥离为纯 JSON);`exiftool.write(file, tags)` 校验 tag 名并回写 - **字段表设计(6 分组 / 46 字段)**:basic(Make/Model/Software/Artist 等 EXIF ASCII)/ camera(LensMake/LensModel/BodySerialNumber 等)/ exposure(FNumber/ExposureTime/ISO/FocalLength 等 rational)/ date(DateTimeOriginal/CreateDate/ModifyDate 等)/ gps(GPSLatitude/Longitude/Altitude + 时间戳)/ description(Title/Description/Creator/Rights/Headline/Country/State/City/Location/Subject 等 XMP/IPTC + EXIF 图像描述 + PNG 文本描述);每字段含 `key` / `zh` / `en` / `group` / `format` / 可选 `enumValues` / `hint`,前端按分组渲染可编辑表单 - **描述信息容器 tabs**:description 分组按元数据容器拆为独立 tab——`EXIF · ASCII`(eXIf,拒中文)/ `IPTC · Latin-1`(拒中文)/ `XMP · UTF-8`(支持中文);PNG 文件额外显示 `PNG · tEXt` tab,字段 key 原样 `PNG:Description` 直接读写 PNG 的 tEXt/iTXt chunk(exiftool 对非 Latin-1 内容自动写 iTXt,中文实测可往返)。注意 PNG 的 `PNG:Description`(tEXt chunk)与 `EXIF:ImageDescription`(eXIf chunk)是**互相独立的存储**,同一张 PNG 可同时存在不同值,因此 service 的 `KEY_NORMALIZE` **不做** `PNG:Description → EXIF:ImageDescription` 回填(回填会导致输入框显示 tEXt 值、保存却只写 eXIf chunk,tEXt 原值残留成矛盾脏数据) - **XMP 字段命名空间归属(易错)**:`Title/Description/Subject/Creator/Rights` 属 dc(`XMP-dc:*`);`Headline/Country/State/City` 属 **photoshop**(`XMP-photoshop:*`,注意不是 Iptc4xmpCore——该命名空间只有 `Location/CountryCode/Scene/SubjectCode` 等,错写 `XMP-iptcCore:Country` 会被 exiftool 以 `doesn't exist or isn't writable` 静默丢弃,曾导致 PNG 的国家/州省/城市保存后丢失);`Location` 才是 `XMP-iptcCore:Location`。PNG/WebP 读取时 exiftool 不拆 namespace(返回 `XMP:City`/`XMP:State` 简写),由 service `KEY_NORMALIZE` 规范化为完整 key - **写入静默失败防护**:exiftool 对不存在/不可写的 tag 不以非零退出,只在 WriteTaskResult.warnings 提示(`updated` 照常 +1);`writeImageMeta` 必须扫描 warnings 中的 `doesn't exist or isn't writable` / `is not defined` / `Nothing to do` / `not in PrintConv` / `more than one PrintConv` 并抛错,由 IPC/HTTP 包装成 `{success:false}` 提示前端,不能只看进程退出码 - **数字枚举 tag 必须用 `#` 后缀写入**:exiftool 写枚举 tag 时默认按 PrintConv 打印名做反向模糊匹配,纯数字值会被误当名称片段——实测 `-Orientation=6` 报 `matches more than one PrintConv`(撞 Rotate 90/270 CW)、`-Orientation=1` 撞 "Rotate 180" 静默写成 3,其余值 `Nothing to do` 保持原值。数字编码枚举(`NUMERIC_ENUM_TAGS` 集合,目前仅 `EXIF:Orientation`)写入时由 `writeImageMeta` 自动把 key 改写为 `EXIF:Orientation#`(tag 级 `-n`,JPG/PNG 实测 1/3/6/8 精确往返);打印名枚举(`ResolutionUnit`=inches/cm/none、`WhiteBalance`=Auto/Manual)**禁止**加后缀,否则名称无法转换;新增数字枚举字段时同步登记集合 - **中英文校验规则**:EXIF ASCII 字段(Make/Model/Software/LensMake/LensModel 等)用 `/^[\x00-\x7F]*$/` 拒中文,违反时输入框红色描边并阻塞保存;XMP / IPTC 描述类字段(Title/Description/Creator/Rights/Headline/Country/State/City/Location/Subject)允许中文录入;日期字段用 `el-date-picker type="datetime" value-format="YYYY:MM:DD HH:mm:ss"` 直接输出 exiftool 标准格式(兼容 `YYYY:MM:DD HH:mm:ss[.ss][±TZ]`);rational 字段用 `/^-?\d+([/.]\d+)?$/`;GPS Latitude 范围 ±90、Longitude ±180 - **GPS 写入展开**:前端只填单十进制数(含正负号),service 层在 `writeImageMeta` 内自动展开为「绝对值 + 方向参考」(LatitudeRef `N`/`S`、LongitudeRef `E`/`W`、AltitudeRef `0`/`1`),保持字段表简单(一个字段一个输入框) - **状态管理**:读取后克隆一份 `original` 作为基线,编辑用 `draft`;`dirty` 计算字段级差异,`errors` 收集校验失败项;底部「保存」按钮在 dirty 且无 error 时可用,「放弃」按钮重置 draft 回 original;支持空值(写 `null` 即删除该 tag) - **双端文件选择约定**:PC 端走 `settingsApi.selectFile` 系统对话框(图片过滤器 `jpg/jpeg/png/tiff/bmp/gif/webp/heic` 等常见格式),Web 端走 `FileBrowserDialog`(file 模式 + 同款过滤器);路径缓存到 localStorage 键 `bronya2.imageMeta.lastPath`,作为 PC 对话框 `defaultPath` 与资源管理器 `initialPath` 回填,进入面板时用 `fsApi.exists` 校验并清除失效缓存 - **原始元数据折叠区**:已并入「知识点帧图模块」卡片,作为帧图下方的子折叠块,展示 `exiftool.read()` 返回的完整 Tags JSON(含字段表中的可编辑项,key 排序),便于与表单值逐项核对;只读、不参与编辑 - **知识点帧图模块**:分组表单上方默认折叠的知识卡片(表头与展开内容同卡一体,展开时表头下加分隔线,无间隙)。展开后按格式(JPEG / PNG / WebP / TIFF / PSD / BMP / GIF)切换查看物理层帧结构——以 IP 帧风格网格表呈现:顶部偏移标尺(文件开头 → 结尾)+ 每行三格「序号 / 物理段名(等宽字体白字)/ 存放字段」,段名格为实色底白字,与图例色块完全一致(--color-layer-fill-* 填充 token,暗色主题自动加深保证白字对比度);图例下方有跨格式提示「新版 IPTC Photo Metadata(IPTC Core / Extension)存放于 XMP XML,IPTC IIM 为旧版容器」,三处 IIM 段(JPEG APP13 / TIFF IPTC 目录 / PSD IPTC 资源)字段格交叉指向 XMP 段;PNG 的 iTXt chunk 字段格直接注明「新版 IPTC Photo Metadata(IPTC Core / Extension)随 XMP 存放于此」,帧表底部以肯定句式说明「PNG 支持新版 IPTC(随 XMP 存于 iTXt,keyword = XML:com.adobe.xmp),不支持旧版 IIM」,如 JPEG `APP1` 放 EXIF、`APP13` 放 IPTC;BMP / GIF 附无元数据容器提示。当前读取图片格式对应的 tab 带金色星标(悬停提示「当前读取图片的格式」)。「全部原始元数据」(exiftool 读出的全部标签,剔除 SourceFile/errors/warnings)作为知识点卡片内的子折叠块置于帧图下方,默认收起,且仅在帧图 tab 为当前图片实际格式时展示(切到其他格式 tab 时隐藏并提示切回;无帧图的格式如 HEIC/RAW 提示无法归类);标签按当前帧图的物理段归类分组(段头带与图例同款实心色点与 tint 底色、计数胶囊,各组独立折叠默认收起,切换格式 tab 时复位),与上方帧表逐段对照 - **Web 端部署包依赖**:需携带 `exiftool-vendored`(已在 `vite.config.server.ts` 的 `RUNTIME_DEPS` 登记);其 `optionalDependencies`(`exiftool-vendored.exe` Windows 二进制 / `exiftool-vendored.pl` Perl 源码)随 `npm install` 自动拉取,无需显式登记 #### 图片缩放工具 (ImageResizeTool) - 位置:程序员工具 → 内置工具 → 图片缩放(`src/views/ProgrammerTools/components/ImageResizeTool.vue`,service `electron/main/services/imageTool.ts`,IPC handler `electron/main/ipc/handlers/imageTool.ts`,preload `electron/preload/apis/imageTool.ts`,Web route `server/routes/imageTool.ts`,渲染层双栈包装 `src/api/imageTool.ts`,自检脚本 `scripts/check-image-tool.ts`) - 处理全部在后端 sharp(libvips 8.18)完成,PC 走 IPC(`image-tool:info` / `image-tool:process`)、Web 走 HTTP(`GET /api/image-tool/info` / `POST /api/image-tool/process`),两端复用同一 service;sharp 走 Node-API 预编译二进制,Electron 42 主进程实测免 rebuild 直接加载 - **三步流水**:去白底(逐像素 alpha 化)→ 重采样缩放(`fit` = fill 拉伸 / contain 补透明边 / cover 裁切;`kernel` = lanczos3 / lanczos2 / mitchell / linear / nearest,**sharp 无 box 核**,libvips 大幅缩小时内部先 reduce 再套核)→ 编码(PNG / WebP / JPEG)。2000×2000 → 20×20 实测约 30ms,4M 像素泛洪约 200ms - **去白底两档**:`edge` 从四边向内泛洪,只去除与边界连通的近白区,**主体内部包裹的白色(眼白 / 白衣 / 高光)保留**(实测中心白圆 alpha 仍 255);`global` 全图近白一律去除,适合纯白底图标。判定用 `255 - min(r,g,b)` 的距白程度,不看色相;`tolerance` 为阈值、`feather` 为软过渡带宽,过渡带内像素按 alpha 反解白底污染(`前景 = (观测 - (1-a)*255)/a`),否则缩到小尺寸会留一圈白晕 - **重采样底噪清理**:lanczos 负瓣会在去底轮廓外留下 alpha 约 8~15 的一圈淡色环。因去底前 alpha 只有 0/255 两态,`ALPHA_FLOOR`(20)以下的低 alpha 一律归零,不影响原图自带软阴影的图(非去底路径不做此处理) - **目标体积二分**:`maxBytes` 超限且在 `[1, quality]` 内二找首个达标的质量(上限 8 轮)。PNG 的唯一缩量手段是调色板量化(`palette + quality`,`quality=100` 时保持无损);JPEG/WebP 直接降 quality。结果 `quality` 字段回传实际使用值 - **预览与导出同源**:一次 `process` 调用即可,不带 `outputDir` 为预览、带 `outputDir` 时同时落盘并回 `savedPath`,避免「看到的」与「导出的」两次编码不一致;预览走 350ms 防抖自动触发。产物超过 `PREVIEW_MAX_BYTES`(6MB)时不回传 base64,前端提示直接导出查看。导出文件名 `filename` 强制 basename 防目录穿越(与 `fsExplorer.saveTextFile` 同约定),JPEG 无透明通道故 `flatten` 白底 - **边界与限制**:输入上限 25M 像素(约 5000×5000),输出边长上限 4096 且超出即由 `clampInt` 夹到上限(与前端 `el-input-number` 的 max 一致);**HEIC/AVIF 写入不支持**(libvips 未编 heif 解码,图片元数据工具能吃 HEIC 但本工具不能,界面已标注);泛洪用 DFS 显栈 + 入栈即标记,病态棋盘格白底下栈仍可能 O(N),实测吃紧再换扫描线填充 - **双端文件选择约定**:PC 端走 `settingsApi.selectFile` / `selectDir` 系统对话框,Web 端复用同一个 `FileBrowserDialog`(`browserMode` 在选源文件 file 与选导出目录 dir 之间切换,`pendingAction` 暂存待执行的导出动作);源图路径缓存 localStorage 键 `bronya2.imageResize.lastPath`、导出目录缓存 `bronya2.imageResize.outputDir`,进入面板时用 `fsApi.exists` 校验并清除失效缓存 - **自检**:`npx tsx scripts/check-image-tool.ts`(15 项断言,覆盖两种去底模式、100:1 缩放、体积二分、JPEG 铺白、contain 补边、落盘、错误路径与超限夹取,全通过退出码 0) - **Web 端部署包依赖**:需携带 `sharp`(已加入 `vite.config.server.ts` 的 `RUNTIME_DEPS`);其平台二进制 `@img/sharp-win32-x64` 等走 `optionalDependencies` 由 npm 按目标平台自动拉取,**离线 `npm pack` 分发时构建机与部署机平台需一致**,否则部署端需联网补装对应平台包 #### 任务指标体系 (DailyProcess 增强) - **任务与指标强绑定**:每个任务可配置 0~N 个指标,全部指标验证通过后任务才允许标记 `done`;任务按顺序逐项执行,仅当前任务可完成/跳过 - **预设指标类型**: - `screenshot` 截图验证:全屏用 `desktopCapturer` 捕获;区域唤起系统截图工具(win32 `ms-screenclip:` + 剪贴板轮询,darwin `screencapture -i`),取消/超时视为未完成 - `launch_app` 应用启动:`shell.openPath` 拉起 + `tasklist`/`ps` 进程检测,未检测到时回退渲染端人工确认 - `web_screenshot` 网页访问并截图:隐藏 `BrowserWindow` 加载 URL 后 `capturePage` 存证 - `system_action` 系统操作: - `shutdown` / `restart`:`shutdown /s|/r /t 0`,渲染端 `ElMessageBox` 二次确认 - `intranet` / `external`:复用设置面板的 `toggleNetwork`(`netsh` 启停网卡 + 注册表切换代理),网卡名从 `net.intranetAdapter` / `net.externalAdapter` 读取,渲染端二次确认后执行 - **凭证存储**:截图等证据保存到 `~/.bronya2/temp/YYYY-MM-DD/`,按日期自动建文件夹 - **交互**:任务行「编辑」按钮打开 TaskEditDialog 配置指标;执行时 TaskRunDialog 逐项引导验证,全部通过后「完成并下一步」 #### 构建调试 (BuildDebug + build 服务) - **多构建环境**:进入构建调试先展示环境卡片列表(毛玻璃卡片平铺,展示环境名 / 描述 / 项目数 / 项目名标签),点击卡片进入该环境的构建调试详情;支持新增 / 编辑 / 删除 / 复制环境,复制时经 SQL 整行深拷贝把该环境下全部项目配置一并带过来(新环境名「源名 副本」);新项目首次进入自动创建一个空的「默认环境」,删除环境时至少保留一个; 各环境项目列表与构建选项完全独立(`build_envs` + `build_projects.env_id`,联合唯一 `(env_id, folder_path)`,同一文件夹可在不同环境各配置一份);上次进入的环境 ID 记于 localStorage `buildDebug.lastEnvId`,下次打开构建调试默认直接进入该环境,详情页「返回」按钮回到环境列表;每个环境的导入导出相互独立(导出 JSON 含 `env` 元信息,导入仅全量同步目标环境) - **Web 端环境可见性(IP 黑白名单)**:环境编辑弹窗中可配置 `ipWhitelist` / `ipBlacklist`(input-tag 手动录入多个 IP),该配置区仅在 Web 端且以本机 127.0.0.1 访问时可见可配置,Electron 端与非本机访问均不展示;**本机 127.0.0.1 始终全部可见不受管控**,其余 IP 白名单非空时仅白名单 IP 可见该环境(优先级高于黑名单),白名单为空时黑名单内 IP 不可见,两者均空则所有 IP 可见;服务端在环境列表 / 项目列表 / 构建执行等接口统一拦截,非本机请求无法篡改名单(见 8b 节) - **构建分支白名单**:环境构建控制区可用 input-tag 配置 `allowedBranches`,留空不校验;支持 `*` 通配(如 `release/*` 匹配 `release/v1.0`、`release/hotfix/x`,`feature-*` 匹配所有 feature- 前缀分支);**校验在后端 `runBuild` 服务统一强制执行**(IPC 与 Web SSE 同一路径,无法绕过前端直调 API),构建发起时实时执行 `git rev-parse --abbrev-ref HEAD` 读取各 Git 项目本地当前分支(不依赖页面缓存分支),不匹配时本次构建不获取锁、不执行,返回 `branchViolations` 由前端弹窗提示「非发布指定分支」,确认后带 `branchCheckConfirmed: true` 重新发起放行、取消则中止;非 Git 仓库不参与校验 - 添加前端 / 后端项目:指定项目文件夹即可加入(项目归属当前所在环境),配置项目类型、打包指令(后端默认 `mvn package`、前端默认 `npm run build`,可修改)、 输出产物列表(`artifacts` 数组,默认 1 条;前端 monorepo / 后端父子模块可配置多条分别压缩,每条独立配置输出路径、zip 名称、压缩方式与根目录/文件重命名), 路径默认前端 `dist`、后端 `target/{项目名}.jar`(选择文件夹后自动填充,切换类型时同步切换默认值,可修改),压缩方式(`contents` 仅目录内容 / `dir` 包含文件夹 / `flat` 仅文件,默认前端 `dir`、后端 `flat`)与根目录/文件重命名(`zipRootName`,如把 `target` 重命名为 `app`),持久化到 `build_projects` 表; 编辑对话框底部「高级配置」(默认折叠)可按项目覆盖环境:前端项目可指定 node.exe / npm,后端项目可指定 mvn / settings.xml / 本地仓库 / Maven JVM 参数,留空的字段回退使用设置面板中的全局环境配置;另可配置构建命令的**执行窗口**(`execWindow`:默认 `default` 隐藏控制台窗口、stdout/stderr 经管道实时回传面板;`newWindow` 新开独立控制台窗口执行命令,构建输出只在新窗口中可见,面板不接收流式日志、仅等待进程退出码判定成败,本地执行器与 node24 / python / python-pty 远程服务均支持;历史配置项 `windowsHide` 已移除)、**进程优先级**(`execPriority`:`default` 标准优先级 / `high` 高优先级加快编译,子进程派生的编译器会继承,默认 `default`)与 spawn 的 `env` 参数(`envMode`:system 传递构造环境 / none 不传递,默认 system),持久化于 `env_overrides` 字段 - **Windows 电源节流(Power Throttling)会显著拖慢构建子进程——重要,排查「构建莫名变慢」时优先检查此项** - 背景:Windows 10 1709+ 内置电源节流(基于 EcoQoS):系统把判定为「后台」的进程标记后,会优先把它调度到能效核(E-core)、限制 CPU 频率并缩短运行时间片以省电。它与进程优先级是**两套正交的机制**——`execPriority='high'`(HIGH_PRIORITY_CLASS)只改变线程的调度排队顺序,**不能解除**电源节流;被节流的高优先级进程依然跑在被限频的核心上,实测对构建效率影响非常明显 - bronya 的构建子进程特别容易被判定为后台:默认模式 `windowsHide:true` 无窗口、无前台焦点;新窗口模式 `detached` 的独立控制台不被持续交互;远程构建时 node24 / python 任务服务本身就是无窗口服务进程;当 bronya 主窗口切到后台(切到 IDE、浏览器)时,整个进程树都可能被标记 - 典型现象:构建耗时明显变长(笔记本电池供电时最严重),设高优先级也不提速;接通电源、或把相关窗口保持在前台时速度恢复;同一份代码在不同电源状态下耗时差异很大 - 诊断方法:任务管理器 →「详细信息」标签页 → 右键列标题 →「选择列」→ 勾选「电源节流」(Power throttling)→ 查看构建中的 node.exe / java.exe / cmd.exe 行,显示「已启用」即正在被限速;也可直接接通电源再构建对比耗时快速验证 - 彻底关闭(**全局生效,需权衡**:关闭后系统不再对任何后台进程限速,笔记本电池续航可能明显下降,台式机 / 长期接通电源场景基本无副作用),注册表与组策略二选一: - **注册表**:定位到 `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power`,新建 `PowerThrottling` 项,在其中创建名为 `PowerThrottlingOff` 的 DWORD(32 位)值并设为 `1`,然后重启电脑 - **组策略(专业版 / 企业版)**:运行 `gpedit.msc`,导航到「计算机配置 > 管理模板 > 系统 > 电源管理 > 电源节流设置」,启用「关闭电源节流」策略 - 不想全局关闭时的缓解建议:构建时接通电源、保持 bronya 窗口在前台(有前台窗口的进程通常不被节流)、使用「新窗口」模式并偶尔点击该控制台窗口使其保持前台属性 - 项目搜索筛选:项目列表头部提供搜索输入框,按项目名称 / 文件夹路径 / 打包指令模糊匹配(不区分大小写),筛选外的已选项目保持选中状态;无匹配结果时显示空状态 - 批量修改:选中 2 个以上同类型项目时「批量修改」按钮可用,打开 BatchEditDialog 按字段勾选应用打包指令 / 拷留清理路径 / 环境覆盖(未勾选的字段保持各项目原值),确认后经 `build:project-batch-update` 一次性更新 - 多选项目后一键构建:多项目**并发**执行(各项目互不等待),流程为 `git pull`(可选开关)→ 打包指令 → 各输出产物分别压缩为 `/.zip`(一个项目可配置多个产物,各自独立压缩;压缩方式可配置:`contents` 仅打包目录内容、`dir` 目录以文件夹形式打包、`flat` 单文件直接打包,`dir`/`flat` 模式可用 `zipRootName` 重命名根目录/文件), 可选将全部选中项目的产物 zip 再汇总打包为一个最终 zip(zip 内根层级为与 zip 同名的文件夹,各产物 zip 置于其中); **构建会话按 sessionId 隔离,支持多会话并发**:构建中仍可勾选其他未在构建 / 未被锁定的项目继续发起构建(忙碌项目的勾选框与构建按钮禁用),同一项目由项目锁互斥不会重复构建;「取消构建」按本视图发起的会话逐个取消(`build:cancel` 携带 sessionId,仅终止目标会话子进程,Windows 下 taskkill /T 杀整棵进程树),各项目以「构建已取消」收尾,不会误杀其他用户 / 窗口的构建; 构建控制卡片顶部提供**静默模式**总开关(全局设置 `build.performanceMode`,settings key 为历史遗留保留,默认关):开启后 git pull 与打包指令子进程以 `stdio: 'ignore'` 启动——不建立 stdout/stderr 管道、不读取/解码输出、不推送 `log` 事件,省去管道转发与 UTF-8/GBK 编码开销,前端无流式日志(「查看日志」按钮自然隐藏),仅保留阶段切换(pull/build/zip)与每个项目的最终成败结果;**代价**:失败时错误信息只有退出码(如「命令执行失败(退出码 1)」),看不到 mvn/npm 的详细报错,排查问题需关闭开关重跑一次;分支查询等需要命令输出文本的 `runShellCapture` 不受影响; 构建中切换页签会弹确认「存在未构建完成的项目,切换后将终止构建」,确认后取消构建并切换(基于 `src/router` 的切换守卫 `registerSwitchGuard`); Web 端构建中刷新 / 关闭页面时浏览器弹出原生提示,并按本视图发起的会话逐个发 keepalive 请求(携带 sessionId)尽力通知服务端终止构建,不影响其他用户的构建会话 - 环境注入:执行 mvn 时自动注入设置中的 mvn 路径、`-s settings.xml`、`-Dmaven.repo.local` 仓库;执行 npm 时注入 npm 路径并将 node.exe 所在目录加入 PATH;git 操作使用设置中的 git 路径; 其中 mvn/npm 的可执行文件配置为完整路径时直接使用该路径(Windows 下 `.cmd` 由 ComSpec 包装启动,退出码经 `cmd /s /c` 与 `windowsVerbatimArguments` 正确传递,符号链接如 nvm 目录在解析 npm-cli.js 时按真实路径处理),配置的 node.exe 所在目录同时加入 PATH 供构建脚本内部派生的子进程使用; 子进程以**参数数组形式** spawn(`shell:false`),按可执行文件类型分流: **npm 快路径** —— 解析 `npm-cli.js`,直接 `spawn(node, [npm-cli.js, ...args])` 跳过 `npm.cmd` 与其内部的 prefix 检测 node fork(实测从 ~318ms 降至 ~164ms,约 1.94x 提升),找不到关键文件时 fallback 到 `npm.cmd`;执行用的 node 优先级为**显式配置(单项目高级配置 > 全局 `env.nodePath`)> npm 同目录 node.exe > PATH 中的 node**,与 fallback 路径(配置的 node 目录会被加到 PATH 最前面)口径一致; **mvn** —— 用 `whichWithExt` 解析 `mvn.cmd` 完整路径,经 `ComSpec /d /s /c` 包装(mvn.cmd 内部需解析 MAVEN_HOME/classpath,重新实现风险大,保留经 cmd.exe 解释); **git** —— 裸名 `git` 直接 `spawn('git', args, {shell:false})`,libuv 在 PATH 中查找 `git.exe`(实测 59ms); **其他 CLI(nrm / npx / yarn / pnpm 等)** —— 单条命令在 Windows 下先用 `whichWithExt` 按 PATH+PATHEXT 解析完整路径:全局 npm 安装的命令本体是 `.cmd` 垫片(裸名直跑会 ENOENT),解析后自动走 comspec 包装;真 `.exe`(如 git/node)解析后直跑; 参数以数组形式传递不再需要 shell 引号转义;**检测到 shell 元字符(`&&` / `||` / `|` / `;` / 重定向 `<` `>`)时自动切换为整串系统 shell 执行**(`spawn(..., {shell: true})`,Windows 走 cmd.exe、Unix 走 /bin/sh),因此 `nrm use taobao && npm i && npm run build`、`npm run build > build.log` 这类复杂脚本照常可用——但该模式不享受 npm 快路径与 mvn `-s`/`-Dmaven.repo.local` 参数注入(`MAVEN_OPTS` 环境变量仍生效),需要注入配置时请将命令拆为单条 mvn/npm 指令; Windows 下子进程输出解码优先 UTF-8(严格流式)失败时整流回退 GBK,避免中文日志乱码; MAVEN_OPTS 不再自动追加 `-Dfile.encoding=UTF-8`,如需 UTF-8 编码输出请在 MAVEN_OPTS 中自行配置 - 状态展示:待构建灰色 / 构建中橙色 / 成功绿色 / 失败红色(项目卡片左侧色条 + 状态标签),实时展示耗时(60 秒内显示「N 秒」,超过 60 秒友好显示「N 分 N 秒」)与当前阶段(拉取代码 / 构建 / 压缩产物) - 预估构建完成时间:列表 / 卡片模式均在项目信息区展示带时钟图标的预估耗时,**不区分静默模式与普通模式统一统计**——每次成功构建后将耗时写入 `build_project_durations` 表,单一项目最多保留最近 10 条(超出删最早),预估为这 10 条(不足则按实际条数)的平均值;无历史记录时显示「暂无法估算完成时间」;待构建时显示「预估耗时 N 分 N 秒」,构建中动态显示「预计还需 N · HH:MM 完成」(已超时则「即将完成」) - 日志:主进程经 `build:progress` 实时推送命令输出,失败自动展开日志面板,可手动展开/收起;成功后「打开产物」定位到产物 zip 所在目录(多产物时所有 zip 均在同一输出目录,取首个 zip 定位目录;PC 端打开目录,Web 端经自构建资源管理器 FileBrowserDialog 浏览且文件行可下载(`fsApi.download`,服务端限 `.bronya2` 目录内);「打开目录」按钮 Web 端同样走资源管理器查看) - 分支展示:项目卡片展示所在 git 仓库当前分支(`git rev-parse --abbrev-ref HEAD`),「刷新分支」按钮带独立 loading 状态;非 git 仓库显示「非 Git 仓库」 - Git 操作菜单:每个项目(列表 / 卡片模式均支持)提供「Git」下拉菜单,包含「拉取代码」「切换分支」「阶段跃迁」: - 拉取代码:经 `build:git-pull` 在项目目录下独立执行 `git pull`(不参与一键构建的进度推送与取消),成功后自动刷新分支显示;本地与远程分叉产生合并冲突时(仓库留在 MERGING 状态)返回 `data.conflictFiles`,阶段跃迁工作流内自动弹冲突解决弹窗人工处理,Git 菜单单独「拉取代码」则仅提示「存在 N 个冲突文件,需人工解决」(可在阶段跃迁中重跑 pull 步骤解决); - 切换分支:打开带搜索的分支选择弹窗(`BranchSelector` 公共组件),经 `build:git-branches` 列出本地 + 远程分支(返回 `local` + `remote` + 合并去重的 `branches` + 当前 `current`),选中后经 `build:git-switch` 执行 `git checkout `,成功后刷新分支显示;切换成功后自动执行一次项目级残留清理(仅清理项目 `clean_paths` 配置的路径,不涉及 zip 输出目录),无残留时静默,有删除时消息提示。弹窗内带「切换后清理本地多余分支」开关(`branchDialogPrune`,localStorage `bronya2.buildDebug.pruneAfterSwitch` 持久化、默认开启),开启时本次切换成功后异步执行 `pruneLocalBranches`(仅删除远程仍存在且非当前的本地分支,未推送工作不会丢) - 阶段跃迁(`StageFlowDialog`):可编排的 Git 自动化工作流。左侧目录维护多个指令集(新建 / 删除,按项目独立持久化到 `build_projects.stage_flows` 列);左侧每个指令集行尾常驻展示「锁定 / 解锁」「复制步骤」「粘贴步骤」「删除」四个按钮(原生 `title` 提示,非 el-tooltip;常驻占位不与名称重叠,名称过长自动 ellipsis 并悬浮显示完整名)——锁定后指令集只读不可编辑(名称 / 步骤 / 变量均禁用、拖拽禁用、粘贴与删除禁用),仅可执行,行首显示锁定图标提示状态,解锁需二次确认避免误操作;复制把当前指令集的全部步骤暂存到组件级剪贴板,粘贴把剪贴板步骤深拷贝(生成新 id)追加到目标指令集末尾,实现跨工作流快速搬运;右侧编辑当前指令集名称与步骤,指令源包含切换分支 / 拉取分支 / 清理残留 / 新建分支(`git branch`)/ 推送分支(`git push -u origin HEAD`)/ 清理分支(`pruneLocalBranches`,删除本地除远程仍存在且非当前的分支)/ 合并分支(`git merge --no-ff `,强制产生 merge commit,对齐 IDEA 行为)七类,SortableJS 拖拽克隆加入、拖拽排序、单步删除、单步忽略 / 恢复(忽略后步骤保留在列表中但划掉展示、运行时跳过,不参与分支参数与变量校验,点恢复可重新启用);切换 / 新建 / 合并步骤的分支参数为支持手动输入的分支下拉,清理残留步骤提供「额外项目」多选下拉(列出同环境其他项目、排除当前项目自身),执行时当前项目恒参与清理、选中项目的 `clean_paths` 残留一并清理(`extraProjectIds` 持久化在步骤中,已删除的项目 id 由后端自动跳过),每个指令块悬浮显示 `title` 说明其功能与对应 git 命令。指令集下方折叠面板「变量」可声明工作流级命名变量(`StageFlowVariable`:name + value 纯文本输入,不限定用途——分支名 / 路径 / tag 名均可),步骤分支字段通过 `{{变量名}}` 占位符引用变量,运行前校验未定义变量与重名变量并提示,运行时执行器先把 `{{name}}` 替换为变量值再下发到 git 命令,未匹配到变量名的占位符原样保留并阻止执行。工作流编辑后防抖 500ms 经 `build:stage-flows-save` 落库(Web 端 `POST /build/projects/:id/stage-flows`,带环境可见性守卫),关闭弹窗时 flush 末次变更;打开时从项目 `stageFlows` 初始化,无配置则为空(不预置默认模板,由用户自行新建指令集编排),空数组不立即落库避免污染数据。运行时严格串行(上一步成功才执行下一步),逐步回写状态:等待序号 → 执行中转圈 → 成功勾选 / 失败红叉并立即停止、提示真实 git stderr;执行期间禁用拖拽与编辑;新建 / 推送 / 合并同样走项目操作锁(`git-create` / `git-push` / `git-merge`),Web 端经 `/build/git/create`、`/build/projects/:id/git-push`、`/build/git/merge`(带环境可见性守卫) - 合并 / 拉取冲突解决(`MergeConflictDialog`):当 `merge` 步骤的 `git merge --no-ff` 或 `pull` 步骤的 `git pull`(本地与远程分叉时)产生冲突时,自动弹出基于 `monaco-editor-merge-conflict`(VS Code 风格)的冲突解决弹窗(pull 与 merge 后仓库同处 MERGE 状态,解决链路完全复用)。左侧列出全部冲突文件(已解决带 ✓ 标记),右侧 Monaco 编辑器内联高亮 `<<<<<<<` / `=======` / `>>>>>>>` 冲突块并提供「Accept Current / Accept Incoming / Accept Both」动作按钮;单文件内多冲突块时,文件头右侧提供「上一个 / 下一个冲突」导航按钮 + `当前 n / 共 m` 计数器,编辑器初始化自动聚焦第一个冲突块,每解决一个冲突块后自动跳到下一个未解决块(解决的是最后一个则回退到最后一个未解决),避免长文件多冲突时漏看;用户逐个文件解决完所有冲突后,自动调 `gitMergeAdd` 写回文件内容 + `git add` 标记已解决,全部文件解决完点「完成合并」调 `gitMergeCommit`(`git commit --no-edit`,使用 `.git/MERGE_MSG` 默认 message `Merge branch 'xxx'`)完成合并、工作流继续后续步骤;点「取消合并」调 `gitMergeAbort`(`git merge --abort` 完全还原工作区与 index),工作流停止,错误信息为「人工取消合并」。主题复用 Bronya Monaco 主题(`useMonacoTheme('solid')`) - 分支名快捷点选:列表 / 卡片模式下,Git 项目的分支名称均显示为小手可点击,点击直接打开「切换分支」弹窗(构建中 / 拉取中 / 被锁定 / 非 Git 仓库不可点);卡片模式分支名下划线带收缩动效——鼠标悬停时下划线从两边向中间收缩变短直至消失,移开后从中间向两边变长出现(伪元素 `::after` + `scaleX` 0.3s) - 项目按钮精简:列表模式保留常用操作(构建 / Git / 编辑)直接展示,其余(启动项目 / Git Hook / 清理残留 / 查看日志 / 打开目录 / 打开产物 / 删除)收纳到「更多」下拉菜单,避免按钮过多造成视觉拥挤;卡片模式同样新增 Git 下拉按钮 - 项目操作锁(多人并发控制):数据库新增 `build_project_locks` 表(`project_id` 主键 + 外键级联、`locked_by` 持有者、`operation` 操作类型、`locked_at` 时间戳),构建 / git pull / 切换分支 / 新建分支 / 推送分支 / 合并分支 / 清理操作执行前批量或单项加锁(`INSERT OR IGNORE` 原子抢占,避免先查后插竞态),操作结束(含异常 / 取消)在 `finally` 中释放; - 锁冲突时后端返回明确提示(含持有者与操作类型),前端展示「锁定」标签(hover 显示持有者与操作),并禁用对应项目的构建 / Git / 清理按钮; - PC 端持有者为主机名,Web 端为客户端 IP;前端每 5 秒轮询 `build:locks` 刷新锁状态,构建 / git / 清理结束后也主动刷新 - 锁定持有者 IP 名称映射(Web 端专属,配置入口在「设置 → IP 映射」,PC 端隐藏):可为 IP 配置中文名称(configs 表 `build.ipNames` 键,JSON 数组 `[{ip, name}]`),锁定标签 hover 时持有者有映射则显示「名称(IP)」、无映射仅显示 IP(前端渲染时查询映射,锁冲突报错等服务端文案仍为原始 IP);PC 端持有者为主机名,不加载该映射 - 操作日志带操作者 IP:构建会话(`[session] start`)、取消构建、远程上传 / 触发部署、阶段跃迁各 Git 操作(pull / switch / create / push / prune / merge / merge-abort / merge-add / merge-commit)与清理的执行日志均记录 `ip=`(或消息内「操作 IP」);Web 端取客户端连接 IP(归一化 `::ffff:` 前缀),PC 端 locker 为主机名时兜底取本机局域网 IPv4(`operatorIpOf`,`electron/main/services/build.ts`)。上传 / 部署的操作 IP 同时经 SSE 进度流显示在控制区日志面板 - 构建域操作留痕:一键构建 / 远程上传 / 触发部署三类操作落库 `build_operation_logs` 表(v11 起,`createOperationLog`;上传 / 部署按前端传入的 `envId` 归属环境,v12 起 `detail` 列记录动作补充环境信息——构建环境名 / 远程环境名),构建调试面板「操作日志」按钮打开弹窗分页查看(操作人 / 操作动作+环境信息 / 操作时间,10/20/50 每页,操作人按「设置 - IP 映射」显示中文名),支持「清除日志」全量删除本环境记录 - 输出目录、构建前 git pull 开关、汇总打包开关与汇总 zip 名称均为**环境级构建选项**,持久化到 `build_envs.options`(JSON;v6 前存于 `configs` 表的 `build.outputDir` / `build.gitPull` / `build.finalZipEnabled` / `build.finalZipName` 首次启动时迁移到默认环境),各环境独立配置;zip 压缩基于 `archiver` 的 `ZipArchive` 流式写入 - 输出目录支持手动指定与清空(输入框可清除),留空时自动兜底到默认产物目录 `~/.bronya2/build-output`(`getBuildOutputDir`,不存在自动创建),主进程 `build:run` 对空目录同样兜底 - 远程上传 / 触发部署:控制区提供远程环境选择(仅显示已配置地址的动态环境,在设置面板「环境服务配置」维护)与「一键构建 / 远程上传 / 触发部署」三个操作按钮;选中的远程环境随环境持久化到 `build_envs.options.deployEnvCode`(失效自动兜底首个),下次进入该环境自动回填; 上传将产物 zip 按 2MB 分块顺序上传(协议同第一代 bronya),部署经 `deployUrl` POST 触发,分块进度日志实时展示在控制区日志面板; **流程串联**:勾选汇总打包且汇总成功后询问「是否上传到远程环境」(确认即打开远程上传弹窗 FinalZipBatchDialog);任一路径上传成功后询问「是否触发远程部署」(确认即直接触发,不再经按钮的二次确认);按钮手动触发部署仍保留确认弹窗 - 汇总产物获取:远程上传弹窗(FinalZipBatchDialog)批次列表每行提供「打开 / 上传此批 / 删除」操作——「打开」在 PC 端经 `systemApi.openPath` 直接以资源管理器打开该汇总 zip; Web 端打开自建资源管理器 FileBrowserDialog(浏览定位到 zip 所在目录),其新增 `downloadable` 模式下文件行显示「下载」按钮, 命中批次记录的文件走 `GET /api/build/final-zips/:id/download`(按 id 查 DB 取 `final_zip_path` 流式回传,路径不接受客户端入参,防路径穿越),其余文件回退 `fsApi.download`(`/api/fs/download`,限 `.bronya2` 目录内) - 清理残留:项目编辑时可配置多个残留文件夹/文件(`clean_paths` 字段,相对项目目录,如 target / dist);多选项目后点击「清理残留」, CleanConfirmDialog 先经 `build:clean-preview` 拉取路径明细展示(可选同时清理 zip 输出目录中的产物包,切换实时刷新明细), 确认后经 `build:clean` 递归删除;主进程对逃逸出项目目录的路径做安全拦截,不存在的路径静默跳过 #### 挡板数据 (ShieldData + shieldData 服务) - 管理服务器挡板服务的模拟数据配置(数据结构沿用第一代 bronya):`{ [接口名]: MockRow[] }`,每条记录含匹配器(`'*'` 任意匹配 或 `{key, value}` 键值对,支持 string/number/boolean/null 类型)、请求方法(GET/POST)、请求体 / 响应体(JSON)、备注 - 环境加载 / 保存:选择挡板环境(仅已配置任意地址的动态环境可选)后经 `shield:data-load` 读取 / `shield:data-save` 保存,加载后 Hero 区展示当前来源环境 - 双视图编辑:可视化表格(按接口分组,支持接口名/备注搜索、添加/编辑/删除条目)与 Monaco JSON 全文编辑(复用代码编辑器的 bronya 主题),两种视图实时互通 - 条目编辑:EntryDialog 维护接口名与多条模拟记录(折叠面板逐条展示,响应体 `message === '模拟数据未维护'` 自动标记「未维护」);匹配器经 MatcherDialog 键值对表格维护,请求体/响应体经 Monaco JSON 弹窗编辑(保存前校验 JSON 合法性) - 读写走挡板服务真实接口(协议同第一代 bronya):读取 POST `getMockDataUrl`,保存 POST `postMockDataUrl`;环境列表由 `envUrls` 服务提供(仅已配置任意地址的环境可选,设置面板「环境服务配置」维护) #### 日志中心 (LogList + logService) - 浏览远程服务器日志文件列表(协议同第一代 bronya):选择环境(仅已配置地址的环境可选)与应用名称(来自设置面板「日志应用名称」动态配置,初始为空)后点击「获取列表」,表格展示文件名、大小、创建时间、修改时间 - 点击「下载」:PC 端选择本地保存目录后经 `log:download` POST `{app, name}` 流式下载落盘,完成后自动打开保存目录;Web 端经 `GET /logs/download-file` 由服务端流式透传(不落盘)触发浏览器直接下载到本地 - 列表/下载地址由 `envUrls` 服务提供(设置面板「环境服务配置」可视化维护,持久化于 configs 表 `env.serviceUrls`) - IPC 链路:`src/api/log.ts` → preload `logApi` → `log:list` / `log:download` → `services/logService.ts` #### 代码编辑器 (CodeEditor + Monaco Editor) - 基于 **monaco-editor 0.56** + **@dvaji/vite-plugin-monaco-editor 2**(Vite 8 兼容版本) - Vite 插件通过 transformIndexHtml 自动注入 `MonacoEnvironment.getWorker`,无需手动配置 worker - 默认启用语言 workers:`editorWorkerService / css / html / json / typescript`(JS 共享 TS worker) - 面板提供: - **多标签页**:可新建 / 切换 / 关闭 / 双击重命名,允许关闭全部标签(空态兜底),未保存关闭前二次确认;右键标签弹出菜单:打开文件所在目录(经 `system:open-path` 调起资源管理器,未关联本地文件时置灰)/ 关闭标签页 - **多语言模板**:JavaScript / TypeScript / HTML / CSS / JSON / XML / Markdown / Vue / 纯文本,每种语言自带示例模板与主题色 - **快捷操作**:格式化 / 复制全部 / 清空 / 保存;XML 格式化基于 `xml-formatter` 注册的 Monaco `DocumentFormattingEditProvider`(Monaco 未内建 XML formatter),非法 XML 会提示无法格式化 - **本地持久化**:标签页可关联本地文件(`editor_files` 表记录路径/展示名/语言/来源/顺序/激活态);未关联或本地文件缺失的标签显示「待保存」徽章,保存时主进程弹保存对话框(已关联路径则直接重写)落盘并入库;下次启动经 `editor:files-restore` 按原顺序重载内容并恢复激活标签,本地文件已缺失的记录自动清理;关闭已保存标签同步删除库内记录(本地文件保留) - **Web 端保存/回显**:服务端 `GET /editor/files` 调用 `restoreEditorFiles` 读取本地文件内容回显;保存时若未关联本地文件,前端通过自建资源管理器 `FileBrowserDialog` 选定保存目录并 `ElMessageBox.prompt` 输入文件名,拼出绝对路径后随 `payload.filePath` 提交 `POST /editor/files/save`,服务端 `writeEditorFile` 落盘 + `upsertEditorFile` 入库(filePath 为空时返回 canceled,与 PC 端取消保存对话框行为一致) - **外部打开桥接**:`openInEditor(filePath, fileName?, content?, language?, source?)`(`composables/useOpenInEditor.ts`)供其他模块(日志中心、配置预览等)跳转到本面板打开内容;请求写入模块级待处理队列,面板挂载恢复持久化标签后消费(外部来源内容默认待保存、按来源路径去重),并按扩展名推断语言预设 - **主题同步**:自动跟随 `useTheme` 的 `data-theme` 明暗模式,为亮色(樱花)/ 海蓝 / 暗夜(午夜星蓝)各定义了 `bronya-*` 自定义主题(另有透明底色的 `-card` 变体透出外层玻璃拟态) - **状态栏**:语言胶囊 / 光标行列 / 行数 / 字符数 / 关联本地路径 / 编码 - Monaco model 使用 `inmemory:///bronya-editor/` 虚拟 URI,切换 Tab 复用 model;面板卸载时统一 dispose 释放内存 #### 快捷搜索 (useQuickSearch + QyQuickSearch) - 双击 Shift 唤起全局快捷搜索弹窗(300ms 内两次按下 Shift),Esc 关闭;监听器在 `App.vue` onMounted 注册一次,随应用生命周期常驻,弹窗打开期间不再响应 Shift 双击(让位给输入大写字母) - 搜索范围(关键字不区分大小写;由 `utils/fuzzyMatch.ts` 包装 `pinyin-pro` 的 `match()`,统一支持 **直接子串 / 拼音首字母 / 全拼** 三种策略,命中字符在标题/副标题上以粉色背景高亮,类似 uTools 体验): - **菜单**:所有已注册面板(`panels`,按平台过滤 webOnly),命中后调用 `openPanel(key)` 切换面板 - **在线工具**:弹窗打开时异步 `onlineToolApi.list()` 拉取,命中后直接 `systemApi.openExternal(url)` 打开外链 - **本地工具**:异步 `localToolApi.list()` 拉取,命中后直接 `systemApi.openPath(path)` 打开本地资源 - **内置工具**:硬编码 3 条(加密解密 / JSON·XML / 正则可视化),命中后 `requestBuiltinTool(key)` + `openPanel('programmer-tools')`,ProgrammerTools 面板在 `onMounted` / `onActivated` 时通过 `consumePendingBuiltinTool()` 读取并清空待触发 key,自动切换到内置工具 tab 并打开对应子视图 - 键盘导航:↑↓ 移动选中项(自动滚动到可视区),回车触发动作,Esc 关闭;鼠标 hover 也会同步选中索引 - **常用项排序**:结果始终按使用频率降序排列(高频在前,同频率保留原类别 + fuzzyMatch 命中顺序,V8 sort 稳定)——刚打开弹窗即见常用,搜索时同样命中的项中常用项也会自动上浮,未使用项保留原类别顺序在后;点击触发时记录频率到 localStorage 键 `bronya2.quickSearch.usage`(key 形如 `${kind}:${id}`,如 `panel:dashboard`) - 跨组件触发通道:`pendingBuiltinTool` 模块级单例 ref(`useQuickSearch.ts`),快捷搜索侧 `requestBuiltinTool` 写入、ProgrammerTools 侧 `consumePendingBuiltinTool` 读取并清空,无需引入事件总线或 Pinia store - **预填关键字**:`openQuickSearch(prefill)` 支持打开时预填输入框(模块级 `quickSearchPrefill` 单例 ref,弹窗 watch 显隐时消费后清空);看板娘气泡点击即预填 `"> "` 直达 AI 对话模式,动画结束后自动聚焦输入框等待提问 - 弹窗使用 `ElDialog`(无标题栏、点击遮罩可关闭、`append-to-body`),样式覆盖 EP 默认背景为玻璃拟态(`--color-surface` + `backdrop-filter: blur(16px)` + 粉色阴影),保持与全局二次元视觉一致 #### AI 对话(快捷搜索内嵌) - 在快捷搜索输入框中以 `>` 前缀开头即进入 AI 对话模式,输入框图标切换为对话气泡,placeholder 提示「输入问题后回车发送」 - 对话模式下:回车发送问题、流式展示回答(打字机光标 + 思考中动画),Esc 停止当前请求(请求中)或关闭弹窗;支持「发送 / 停止」按钮 - 回复内容以 Markdown 渲染(`marked` + `highlight.js`),支持标题 / 列表 / 表格 / 引用 / 行内代码;代码块带语言标签与「复制」按钮(点击复制代码到剪贴板,复制成功后按钮变为「已复制」) - 卡通形象使用 `bronya.png` 圆形头像,气泡分左右两栏(用户右 / 布洛尼亚左),整体玻璃拟态风格 - 单次问答无上下文:每次请求仅携带当前用户消息(`message` + `messages` 均为单条 user 消息),`chatid` 置空由服务端生成 - 流式解析参考 SSE 协议:按 `\n\n` 切分事件,取 `data:` 行 JSON 的 `choices[0].delta.content` 增量拼接,同时记录 `chatid`;流结束后若配置了清理地址则异步 POST 清理 - **角色性格模板**:内置 6 套二次元风格角色问答模板(深海鲸鱼娘 / 云絮精灵 / 林间守泉小妖 / 星尘信使 / 旧书页灵 / 雾岛茶灵),每套包含人设关键词、专属意象与开场—正文—收尾的回答结构;选定后 `streamChat` 会将角色人设指令与用户提问整合拼接为单条消息发送(因目标大模型不支持上下文,无法用 system message 分离人设)。默认性格不拼接任何额外语料,原汁原味发送用户提问内容 - **提示语模板(可复用,与性格独立)**:设置 → AI 对话 中「提示语模板」折叠区可自定义多条模板(标题 + 提示语),提示语中用 `{content}` 作为待处理内容占位符(保存时强制校验必须包含);模板存于 configs 表 `aiChat.templates` 键。使用模板发请求时 `streamChat` 以 `skipPersona` 模式调用,**不混入任何角色性格 prompt**,两套机制完全独立。灵动输入框组件 `src/components/common/QySmartInput.vue`:el-input + suffix 星光按钮(sparkles 图标)+ 弹出模板列表,选择模板后 AI 处理输入内容并自动回写;首个落地点为每日流程「录入任务并复制」对话框的任务名列 - 配置持久化于 `configs` 表 `aiChat.config` 键(JSON 字符串),随设置导出/导入自动迁移,在「设置 → AI 对话」中填写: - **角色性格**:单选切换 7 种角色(默认性格 + 6 套模板),切换后须点击保存生效 - **请求地址**:SSE 接口完整 URL - **Account**:请求体 `account` 字段 - **清理地址**:支持 `{chatid}` 占位符,流结束后 POST 到此地址(留空不清理) - **模型名**:请求体 `model` 字段,默认 `my-local-model` - 实现位于 `src/api/aiChat.ts`(纯前端 fetch,PC / Web 通用,不经 IPC),角色模板数据与 `getPersona` 查找函数同文件维护,导出于 `src/api/index.ts` --- ## 二、Web 版(浏览器 + Express) ### Web 版快速开始 ```bash # 并行启动前端开发服务器(5173) 与后端服务(3210),浏览器访问 http://localhost:5173 npm run dev:web # 仅启动后端服务(自动托管 dist-web,生产用) npm run start:server # Web 版打包(产物输出 dist-web) npm run build:web # 服务端打包(vite 内置 tsc 类型检查插件 + SSR 编译,产物输出 dist-server 部署包) npm run build:server # 运行服务端部署包(dist-server/index.js,生产用) npm run start:server:prod ``` ### 运行方式 | 场景 | 命令 | 说明 | | ------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | 开发 | `npm run dev:web` | concurrently 并行启动 vite(5173) 与服务端(3210),浏览器访问 由 vite 代理转发 | | 构建 | `npm run build:web` | vue-tsc 类型检查 + vite 构建,产物输出 `dist-web`(相对 base,可部署到任意子路径) | | 服务端打包 | `npm run build:server` | vite 集成 tsc 类型检查插件 + SSR 构建,产物输出 `dist-server`(Node 直接可运行,含 bin 命令、shebang 与随包 web/) | | 运行 | `npm run start:server` | tsx 运行 CLI 入口(`server/cli.ts`),启动服务并自动打开浏览器 | | 运行部署包 | `npm run start:server:prod` | `node dist-server/index.js`,运行编译后的 CLI 产物(默认打开浏览器,`--no-open` 关闭) | | 服务端开发 | `npm run dev:server` | tsx watch 热重载运行 CLI 入口 | | 打包离线安装包 | `npm run build:server:pack` | 一键:build:web + build:server + `npm install --omit=dev` + `npm pack`,产出 `dist-server/bronya2-server-*.tgz`(bundled 依赖随包,目标机离线全局安装) | | 仅打包 | `npm run pack:server` | 在 `dist-server` 下执行 `npm pack`(前提:node\_modules 已安装) | ### Web 端能力降级 以下能力仅 Electron 可用,Web 端已显式降级(不伪装成功): | 能力 | Web 端行为 | | ----------------------- | -------------------------------------------------------------------------------- | | 文件/目录选择对话框 | 返回 `{ canceled: true, filePaths: [] }`;业务交互改用自建资源管理器 `FileBrowserDialog`(选服务器路径) | | 指标自动执行(截图/启动应用/系统操作/取消) | 返回错误「Web 端暂不支持,请使用 PC 端操作」 | | 开机自启、托盘、窗口控制 | **PC 端独有功能,服务端不提供服务**;如遇此类需求,提示「这是 PC 端独有功能」即可 | | 自定义本地背景图(`bronya2://`) | `toLocalAssetUrl` 仅放行 http(s) URL,本地路径回退默认背景 | | 打开本地路径/外部程序 | `openExternal` 用 `window.open`;`openPath` 走服务端 `/fs/open` 接口(手写跨平台实现,不依赖 open 库) | > **服务端范围约定**:服务端不追求与 PC 端完全兼容,**PC 端独有功能(如开机自启、托盘、窗口控制、本地文件系统等)不提供服务端实现**——不添加路由、不引入对应 npm 包(如 auto-launch)、不接入 service。修改时如遇此类功能,向用户提示「这是 PC 端独有功能」即可。 --- ## 三、Server(Web 服务端) ### 简介 `server/` 层复用主进程 `electron/main/services/*` 同一批业务实现(业务逻辑只写一份),以 Express 框架组装为 Web 服务端:`server/index.ts` 初始化数据库并监听端口(默认 3210,`PORT` 可覆盖),`server/app.ts` 组装 CORS / JSON / 请求日志 / 路由挂载 / 静态托管 `dist-web` + `/kityminder` / SPA fallback,`server/routes/*` 按业务域拆分,与 `electron/main/ipc/handlers/` 一一对应。PC 端走 IPC,Web 端走 HTTP/SSE,两者共享同一批 services 与同一数据库文件 `~/.bronya2/bronya2.db`,数据无缝互通。 ### Web 版生产部署(Node 直接运行) `npm run build:web` 产出 `dist-web/`,由服务端静态托管;部署时执行 `npm run start:server` 启动服务(默认端口 3210,`PORT` 环境变量可覆盖)。服务端与 Web 版产物不进入 PC 安装包(`electron-builder.json` 仅打包 `dist`/`dist-electron`,express 等服务端依赖位于 devDependencies)。 `npm run build:server` 将 `server/` 层及其复用的纯 Node services 编译为 **Node 可直接部署的自包含产物** `dist-server/`: ``` dist-server/ ├── index.js # 编译后的 CLI 入口(ESM bundle,业务代码内联,第三方依赖 external,含 shebang) ├── package.json # 运行时依赖清单(npm install --omit=dev 即可安装)+ bin 命令(bronya2)+ npm start 脚本 └── web/ # Web 静态产物(构建时自动从 dist-web 复制,随包自包含托管) ``` 部署步骤(服务器安装 Node ≥ 24 后): ```bash # 本地构建(dist-web 存在时自动随包输出 web/,实现自包含托管) npm run build:server # 上传 dist-server/ 到服务器,安装运行时依赖并启动 cd dist-server npm install --omit=dev npm start # 或 node index.js --no-open(不自动打开浏览器) ``` - 默认监听 `0.0.0.0:3210`,`PORT` / `HOST` 环境变量可覆盖 - 数据仍存于 `~/.bronya2/bronya2.db`,与 PC 版 / 开发模式数据互通 - 运行时依赖(express、cors、better-sqlite3、archiver、winston)已写入产物 `package.json`,无需完整源码或 devDependencies - 开机自启等 **PC 端独有功能服务端不提供**(不引入 auto-launch 等依赖),服务端不追求与 PC 端完全兼容 #### 全局安装为 Web UI 命令行面板 `dist-server` 产物内置 CLI 入口,可安装为全局命令 `bronya2`:任意目录运行即启动服务并自动打开浏览器访问面板。 ```bash # 本地构建 npm run build:web # 生成 dist-web(首次需要) npm run build:server # 组装 dist-server(含 bin 命令与 shebang) # 安装运行时依赖,再全局链接 cd dist-server npm install --omit=dev npm link # 或 npm i -g . (会创建全局 bronya2 命令) # 使用:任意目录直接启动,自动打开浏览器 bronya2 # 常用参数 bronya2 --port 8080 # 指定端口 bronya2 --db ~/my.db # 指定数据库文件(默认 ~/.bronya2/bronya2.db) bronya2 --no-open # 启动后不自动打开浏览器 bronya2 --help # 查看完整帮助 bronya2 --version ``` - 未指定端口且默认 3210 被占用时,会自动递增寻找可用端口并在启动横幅中打印实际地址 - 打开浏览器为跨平台实现(Windows `start` / macOS `open` / Linux `xdg-open`),服务进程不阻塞终端 - 卸载全局命令:`npm unlink -g`(或 `npm uninstall -g bronya2-server`) - 注意:`better-sqlite3` 为原生模块,全局链接(`npm link`)不会重复编译,若使用 `npm i -g .` 需要本机具备编译环境 ##### 离线分发(npm pack 打包,目标机器无需联网) `dist-server` 的 `package.json` 已声明 `bundleDependencies`,构建机把运行时依赖装好后执行 `npm pack`,依赖(含其依赖树)会随 tarball 输出到 `node_modules`,接收方全局安装时直接从包内解压、**无需联网**: ```bash # 构建机(联网):一键构建 Web + 服务端 + 安装运行时依赖 + npm pack npm run build:server:pack # 生成 dist-server/bronya2-server-1.0.0.tgz # 若 dist-server 已构建且 node_modules 已安装,可只执行打包 npm run pack:server # 在 dist-server 下执行 npm pack # 将 .tgz 拷贝到目标电脑(同平台,见注意事项),完全离线安装 npm install -g ./bronya2-server-1.0.0.tgz bronya2 # 直接使用 ``` 注意事项: - `better-sqlite3` 为原生模块,二进制随构建平台打包,**请分发到与构建机相同的平台**(如 Windows x64 构建 → Windows x64 目标机) - 目标机需安装 Node ≥ 24(`engines` 声明);bundled 依赖由 npm 从包内解析,不请求 registry - 卸载:`npm uninstall -g bronya2-server` ### Vite 配置 (vite.config.server.ts) 服务端独立构建配置,以 SSR 模式将 `server/cli.ts`(CLI 入口,复用 `server/index.ts` 的 `startServer` 与纯 Node services)编译为 `dist-server/index.js`(ESM bundle,第三方依赖 external),`closeBundle` 钩子随包组装部署产物: | 行为 | 说明 | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | 类型检查 | 内置插件在构建开始前执行 `tsc -p server/tsconfig.json --noEmit`(server 层及其 services 依赖),失败即终止构建 | | 编译 | Vite SSR build(Rolldown 引擎,`rolldownOptions` 配置输出),`target: node24`,第三方依赖保持 external(部署端 npm install 提供) | | Web 产物 | 存在 `dist-web/index.html` 时自动复制为 `dist-server/web`,服务端优先自包含托管 | | CLI 产物 | 为 `index.js` 注入 `#!/usr/bin/env node` shebang,`package.json` 写入 `bin: { bronya2: "index.js" }`,全局链接后可运行 `bronya2` 命令 | | 离线打包 | `package.json` 声明 `bundleDependencies`(express/cors/better-sqlite3/archiver/winston),构建机 `npm install --omit=dev` 后 `npm pack` 将依赖随 tarball 输出,目标机离线安装 | | 部署清单 | 生成 `dist-server/package.json`:运行时依赖(express/cors/better-sqlite3/archiver/winston)+ `npm start` | ### 服务端 HTTP 接口:提交记录推送(与 CLI 同构) Web 服务端通过 HTTP 接口提供与 `bronya2-cli.exe` **同构**的提交记录写入能力,供远程机器 / git hook / 脚本调用(数据写入同一个 `~/.bronya2/bronya2.db`,前端照常展示)。 **接口**:`POST /api/commit-records/push`,JSON 请求体: | 字段 | 必填 | 说明 | | -------------------- | -- | -------------------------------------------------------------- | | `hash` | ✅ | 提交哈希 | | `content` | ✅ | 提交信息(message) | | `lines` | ✅ | 变更行数 | | `repos` | ✅ | 仓库名 | | `createDateText` | ⬚ | 提交日期 `YYYY-MM-DD`,未填默认当天(等价 CLI 的 `--commit-date`) | | `createDateTimestamp`| ⬚ | 提交时间戳(unix 秒字符串),未填默认当前时间(等价 CLI 的 `--commit-timestamp`) | **行为**(与 CLI 一致): - 同一 `hash` 重复推送自动幂等跳过,返回 `{success: true, data: {id, inserted: false}}` - 缺少必填字段返回 `{success: false, error: '缺少必填字段:必须同时提供 hash, content, lines, repos'}` - 成功返回 `{success: true, data: {id, inserted: true}}` ```bash curl -X POST http://:3210/api/commit-records/push -H "Content-Type: application/json" -d '{"hash":"a1b2c3d4","content":"fix: 修复登录页白屏问题","lines":"+128/-24","repos":"bronya2","createDateText":"2026-09-03"}' ``` ### HTTP API 与 IPC 对照 Web 端 `src/api/*` 与 PC 端共用同一套方法签名与 `IpcResponse` 响应结构,HTTP 端点与 IPC channel 一一对应: | 业务域 | HTTP 端点(前缀 `/api`) | IPC channel | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | 系统/窗口 | `GET /system/info` | `system:*` / `window:*` | | 设置 | `GET/PUT /settings[/:key]`、`GET /settings/service-urls`、`GET /settings/file-as-data-url?path=`(Web 端背景图经 FileBrowserDialog 选中的服务器图片读为 data URL,仅限图片扩展名且 ≤10MB;与 IPC `settings:read-file-as-data-url` 复用同一 service)(开机自启为 PC 端独有功能,服务端不提供 `/launch` 接口) | `settings:*` | | 任务 | `GET/POST /tasks`、`PUT/DELETE /tasks/:id`、`POST /tasks/reorder`、`POST /tasks/reset-day`、`GET /tasks/next`、`POST /tasks/:taskId/metrics`、`POST /tasks/metrics/:id/run\|confirm`、`POST /tasks/metrics/cancel` | `task:*` | | 日程/待办 | `GET/POST /schedules`、`PUT/DELETE /schedules/:id`、`GET/POST /todos`、`PUT/DELETE /todos/:id` | `schedule:*` / `todo:*` | | 在线工具 | `GET/POST /online-tools`、`PUT/DELETE /online-tools/:id`、`POST /online-tools/reorder\|reset` | `online-tools:*` | | 数据库 | `GET /database/status`、`POST /database/init` | `database:*` | | 编辑器文件 | `GET /editor/files`、`POST /editor/files/save\|sync\|check` | `editor:*` | | 日志 | `GET /logs`、`POST /logs/download`、`GET /logs/download-file`(流式透传浏览器下载) | `log:*` | | 构建 | `GET/POST /build/projects`、`PUT/DELETE /build/projects/:id`、`GET /build/default-output-dir`、`POST /build/branches\|env-list\|clean-preview\|clean`、`POST /build/run\|upload\|deploy`(SSE 进度流)、`GET /build/final-zips/:id/download`(汇总批次 zip 浏览器下载)、`GET /build/op-logs`、`POST /build/op-logs/clear`(操作日志分页查询 / 全量清除) | `build:*` | | 挡板 | `GET /shield/env-list`、`GET/PUT /shield/data` | `shield:*` | | 脑图 | `GET /mind/list`、`GET /mind/file?name=`、`GET /mind/data-dir`、`PUT /mind/data-dir` | `mind:*` | | 需求分析 | `POST /requirement-analysis`(body: `{ filePath, manualNumbers }`,filePath 为 FileBrowserDialog 选中的服务器本地路径,服务端直接读取,不限目录) | `requirement-analysis:*`| | 设计挂接 | `POST /design-documents`(body: `{ filePath, scanFolder, docUrlPrefix }`,CSV 路径与扫描目录均为 FileBrowserDialog 选中的服务器本地路径,服务端读取并扫描目录,不限目录) | `design-documents:*`| | 接口请求 | `GET /api-tool/projects`、`POST/PUT /api-tool/projects[/:id]`、`DELETE /api-tool/projects/:id`、`POST/PUT /api-tool/endpoints`、`DELETE /api-tool/endpoints`、`POST /api-tool/send`、`POST /api-tool/import-openapi`、`POST /api-tool/export-openapi` | `api-tool:*` | > 所有端点返回 `{ success, data?, error? }` 结构;异常统一由服务端 `wrap()` 捕获为失败响应,不抛 500 页。 --- ## 四、task-server(远程构建执行服务) ### 简介 task-server 是 bronya2 的远程构建执行服务,接收主项目推送的构建命令,在远端机器执行 npm/mvn 等打包命令,通过 SSE 推送实时日志,支持进程树级取消。bronya 主项目通过 `BuildExecutor` 接口的 `RemoteBuildExecutor`(HTTP+SSE)实现调用,与本地 `LocalBuildExecutor` 切换由全局配置 `build.executor` 控制。task-server 与 bronya 之间只传 bin/args,不在 task-server 内解析命令(命令解析统一在 bronya 的 `buildCommandLine` 完成)。 ### 同构双实现(三实现) task-server 提供三种语言实现,API 完全同构,bronya 侧无需区分: - **node24**(`task-server/node24/server.js`):纯 Node + Express,Node 18+ 兼容(无单独 node18 目录) - **python**(`task-server/python/server.py`):aiohttp + asyncio,Python 3.8+(唯一第三方依赖 aiohttp) - **python-pty**(`task-server/python-pty/server.py`):aiohttp + pywinpty,Windows 终端模拟(用于 newWindow 模式真实控制台输出) - python 版依赖见各目录 `requirements.txt`(python 仅 aiohttp>=4.0;python-pty 为 aiohttp>=4.0 + pywin32>=2.0) - 每个实现是独立项目,无外部依赖,各自带 `package.json` 或 `requirements.txt` ### 启动 ```bash # Node 版 cd task-server/node24 npm install npm start # 默认 0.0.0.0:9528,PORT/HOST 环境变量可覆盖 # 或 npm run dev 热重载 # Python 版 cd task-server/python pip install -r requirements.txt PORT=9528 python server.py # Python PTY 版(Windows) cd task-server/python-pty pip install -r requirements.txt python server.py ``` ### API | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | /api/v1/build/exec | 启动构建,返回 buildId | | GET | /api/v1/build/:id/stream | SSE 日志流(log/exit/error 事件) | | POST | /api/v1/build/:id/cancel | 取消构建(进程树 kill) | | GET | /api/v1/build/:id/status | 查询状态 | | GET | /api/v1/health | 健康检查 | ### 实现要点 - SSE 用 aiohttp 的 `StreamResponse` 自然支持长连接(Python 版);Node 版用 express + SSE middleware - Python `decode_output` 使用 **stateful decoder**(create_output_decoder 工厂返回闭包)处理 GBK 输出;无状态解码会在「GBK 字节恰好是有效 UTF-8 但产生错误字符」时失败 - 不能用 Python `http.server` + 阻塞读实现 SSE——会导致连接泄漏与进程锁;必须用 aiohttp + `asyncio.Queue` - 远程构建服务**不做命令解析**,只接收 bronya 预解析的 bin/args;`envMode='system'` 时使用 bronya 提供的环境变量,非 `'system'` 时继承远程服务自身环境 - 取消构建必须杀整棵进程树(Windows `taskkill /T /F`),释放所有资源;SSE 连接断开时不能阻塞进程退出 - `newWindow` 模式:Node 用 `detached: true, stdio: 'ignore'`;Python 用 `CREATE_NEW_CONSOLE`;Windows 上必须 `shell: true` 才能让 cmd.exe 创建可见控制台并绑定输出流 ### 与主项目协作 bronya 主项目通过 `BuildExecutor` 接口抽象构建执行: - `LocalBuildExecutor`:本地 `node:child_process` 直接 spawn - `RemoteBuildExecutor`:HTTP + SSE 调用 task-server - 切换由全局配置 `build.executor`(`local` / `remote`)控制,远程服务在全局配置中维护 name/host/port - 多会话并发:bronya 支持同时发起多批构建,同一项目由项目锁互斥;取消按 sessionId 隔离(一个用户/窗口不能杀另一个的构建) - 构建进度事件携带 sessionId,renderer 只消费本视图发起的会话(IPC 广播到所有窗口) --- ## 五、deploy-server(远程部署服务) 独立部署在远端服务器上的 Python 单文件服务,接收主项目推送的构建产物 zip、解压替换并重启服务,同时复用第一代 bronya 的挡板数据 / 日志接口协议。源码位于 `deploy-server/bronya.py`。 ### 运行环境与开发约束(重要) - **业务服务器上同时存在 Python 3.4.6 与 Python 3.7.9 两套解释器,且无法 `pip install` 任何第三方依赖**——`bronya.py` 必须只写两者都能运行的代码,语法与 API 一律取版本下界 **3.4** - 仅可使用以下标准库:`argparse / base64 / cgi / json / logging / logging.handlers / os / re / shutil / signal / subprocess / time / traceback / datetime / http.server / threading / urllib.parse` - 禁用 Python 3.5+ 才有的语法与标准库能力,常见清单: - `f-string`(3.6)、变量注解 `x: int = 1`(3.6)、`async def / await`(3.5) - `subprocess.run`(3.5)、`Popen`/`communicate` 的 `timeout=`(3.5)、`Popen` 的 `encoding=` / `errors=` / `text=`(3.6) - `json.JSONDecodeError` 顶层别名(3.5)——代码固定使用 3.3 起就存在的 `json.decoder.JSONDecodeError` - `dict` 的插入有序保证(3.7,3.6 仅是 CPython 实现细节)、`dataclasses`(3.7)、`datetime.fromisoformat`(3.7)、`time.time_ns`(3.7)、`os.PathLike` 与 `pathlib` 新增 API(3.6) - 函数签名的参数注解 `def f(x: str)` 自 Python 3.0 起即可用,允许保留;但不引入 `typing` 模块 - 不得引入 `flask / fastapi / aiohttp / requests` 等任何第三方包;HTTP 服务使用 `http.server.HTTPServer + BaseHTTPRequestHandler`,multipart 表单解析使用 `cgi.FieldStorage`,分块上传/日志下载均以原生 `os / shutil / open` 手写实现 - 提交前用 vermin 做语法兼容回归(本机任意 3.x 解释器即可解析):`pip install vermin` 后执行 `vermin --all deploy-server/bronya.py`,输出的 `Minimum required versions` 必须 **≤ 3.4**;当前实测为 3.3,3.4.6 与 3.7.9 均可运行 - vermin 只覆盖语法与部分库特征,命令与行为层面仍需自测:改完在 3.4 / 3.7 解释器下分别跑 `-c token`、`-c build`、`-c server`(含上传/部署/日志下载链路),避免悄悄引入高版本特性导致远端启动失败 ### 文件结构 ``` deploy-server/ └── bronya.py # 单文件服务(HTTP + 部署 + 挡板 + 日志 + token 工具) ``` ### 启动 ```bash # 启动 HTTP 服务(默认监听 8099,binding_ip 限定本机 127.0.0.1,经 X-Real-IP 头校验) python bronya.py -e trial -c server python bronya.py -e freeze -c server # 同步执行一次部署(解压启动目录下的 dist.zip → 备份 → 替换 → 重启),不启动 HTTP # 注意:所有路径以启动时的 cwd 为基准,需在 bronya 工作目录内执行 python bronya.py -e trial -c build python bronya.py -e freeze -c build # 生成 / 校验访问 token(三层 base64,有效时间 +3 小时) python bronya.py -e trial -c token # 生成 python bronya.py -e trial -c token -t # 校验 ``` | 参数 | 说明 | | --- | --- | | `-e / --env` | 必填,`trial`(测试环境)/ `freeze`(冻结环境),决定加载 `trial_config` 还是 `freeze_config` | | `-c / --command` | 必填,`server` 启动 HTTP 服务 / `build` 同步执行一次部署 / `token` 生成或校验 token | | `-t / --token` | 可选,配合 `-c token` 校验已有 token,不传则为生成 | ### 配置 `trial_config` 与 `freeze_config` 结构相同,按环境维护前端 / 后端项目的部署元数据: ```python { 'web': { 'web-app': {'placement_point': '/path/to/web'} }, 'server': { 'customer-server': { 'placement_point': '/path/to/server', 'port': '8080', 'log_folder': '/path/to/logs' } }, 'licence': 382 } ``` - `placement_point`:产物在远端的落点目录 - `port`:后端服务监听端口(部署后用于 `ss -ltnp` 定位端口占用进程的 pid 并 `SIGKILL`,随后 `sh start.sh` 重启) - `log_folder`:日志文件目录,供 `/log/list` 与 `/log/download` 使用 - `licence`:发布版本号,部署前与 `dist/license.txt` 内容比对,低于配置值拒绝部署 ### HTTP 接口 所有接口仅放行 `binding_ip` 中的 IP(默认 `127.0.0.1`,通过 `X-Real-IP` 头校验),其他来源直接返回 `ok(原 IP)` 拒绝服务。 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/bronya/upload` | 分块上传构建产物 zip,表单字段 `file / name / block / total / md5`(与第一代 bronya 协议一致);首块清空 `lisys_dist_temp`,末块合并分块并 `move` 到工作目录覆盖 `dist.zip` | | POST | `/bronya/deploy` | 触发 `AppBuildTool().start()` 异步部署:清理旧 dist → `unzip -o -q <启动目录>/dist.zip -d <启动目录>` → 遍历 dist 下各项目 zip → `*-app` 前端复制 + 备份 + 解压替换、`*-server` 后端复制 jar + 备份原 jar + 解压 + kill 端口占用进程 + `sh start.sh` 重启 → 清理 dist | | POST | `/adapter/get` | 读取挡板服务模拟数据(`./mock-data.json`,不存在自动初始化为 `{}`) | | POST | `/adapter/post` | 保存挡板服务模拟数据(覆盖写入 `./mock-data.json`) | | POST | `/log/list` | 列出指定应用的日志文件信息(文件名 / 友好大小 / 创建时间 / 修改时间),body `{app}`,取 `config['server'][app]['log_folder']` | | POST | `/log/download` | 流式下载日志文件,body `{app, name}`,经 `safe_join` 校验路径不得逃逸 `log_folder` | | GET/POST | `/test/play` | 测试回显接口,返回方法 / 请求路径 / 请求头 / 请求体 / 响应头 | | GET/POST | 其他 | 通用挡板模拟:按 `real_path` 在 `mock-data.json` 中匹配(`matcher: '*'` 任意匹配 或 `{key, value}` 键值对集合全部命中),未维护返回 `模拟数据未维护` | 响应统一为 `{'status': 200, 'msg': 'ok'/'error', 'data': ...}` 结构。 ### 与主项目的协作链路 1. 主项目「构建调试」面板一键构建后产出 `dist.zip`,通过 `envUrls` 服务配置的 `uploadUrl`(指向 `/bronya/upload`)按 **2MB 分块**顺序上传(表单字段与第一代协议一致:`file / name / block / total / md5`) 2. 上传完成后经 `deployUrl`(指向 `/bronya/deploy`)POST 触发远端部署,主项目仅判定 `status===200` 即视为触发成功 3. `ShieldData` 面板经 `getMockDataUrl / postMockDataUrl` 读写挡板数据,`LogList` 面板经日志接口列/下载日志——三者地址均来自 `envUrls` 服务(设置面板「环境服务配置」可视化维护),deploy-server 与第一代 bronya 保持协议同构 ### 日志 服务日志写入 `bronya.log`(`TimedRotatingFileHandler` 按天滚动并保留 30 天,UTF-8);控制台仅输出 `ERROR` 及以上级别。 - 行格式:`线程名 - 2026-09-20 17:46:40.445 - 级别 - [标签] 动作 ## 字段=值 ## 字段=值`。`fiber` 起的线程以 `函数名@时分秒` 命名,同一轮部署的所有日志共享线程名,可据此串联排查 - 字段统一采用 `键=值`(`端口=` / `pid=` / `目录=` / `大小=` / `返回码=` / `耗时=` / `命令=`),避免只有动作词、缺少上下文 - `BronyaStep(标签, 动作, *字段)` 上下文管理器统一输出 `开始-动作` / `完成-动作(耗时)` / `失败-动作(异常堆栈)`;`log.ex` 用于 except 块内附带堆栈 - 长报文(响应体、堆栈以外的参数)由 `preview` 截断到 512 字符并保留原始长度,避免日志被大 payload 撑爆 - 系统命令(`unzip` / `rm`)统一走 `run_command()`:`stdin=/dev/null`、stdout 与 stderr 合并捕获、默认 600s 超时(超时抛 `TimeoutExpired`),返回码与输出落盘(非 0 记 `ERROR`);`sh start.sh` 用 `subprocess.call` 保留输出到服务 stdout,同样置空 stdin - 未配置的项目、被拒绝的 IP、未命中模拟规则等异常分支记为 `WARNING` ### 注意事项 - `binding_ip` 默认仅 `127.0.0.1`,如需主项目跨机推送,请在 `bronya.py` 中加入远端 IP;生产建议保持本机回环,主项目与 deploy-server 同机部署 - `find_port_pid` 优先使用 `ss -H -ltnp "sport = :端口"`(非 root 下不会像 `netstat` 那样混入 `Not all processes could be identified` 提示行),`ss` 不可用时回退 `netstat -ltnp` 并只取 `LISTEN` 行、端口精确匹配且 pid 为纯数字;仅适用于 Linux,`rm / unzip / kill / sh start.sh` 等系统调用同样面向 Linux 服务器。若目标进程属于其他用户,非 root 仍取不到 pid(返回空列表),此时需以 root 运行或以同用户启动服务 - 部署命令一律不能依赖终端:服务多以 `nohup`/后台方式拉起,stdin 非终端。`unzip` 缺少 `-o` 时遇到同名文件会逐条询问 `replace xxx? [y]es, [n]o, [A]ll...`,永远等不到回答导致部署卡死,因此解压固定使用 `unzip -o -q` - 路径全部锚定启动目录(`cwd_path`):`dist.zip`、`dist/`、`mock-data.json`、`lisys_dist_temp`、`license.txt` 均为绝对路径常量(`dist_zip_path` / `dist_path` / `mock_data_path` / `upload_folder`)。因为 `render_server` 与 `reboot_process` 会 `os.chdir` 到 `placement_point`,且 cwd 是进程级共享状态,用相对路径会让第二轮部署或挡板接口读到错误目录 - 远端 Python 固定为 **3.4.6 / 3.7.9** 两个版本,只能使用 3.4 兼容 API:无 `subprocess.run`(3.5+)、`Popen` 无 `encoding=`/`errors=`(3.6+)、`communicate()` 无 `timeout=`(3.5+)、无 f-string(3.6+)。`run_command` 因此基于 `Popen` + `Timer` 实现并手动 `decode('utf-8', 'replace')` - 两个版本共用同一份代码,任何 3.7 才有的写法(`dict` 有序遍历、`dataclasses`、`fromisoformat`)都会让 3.4 机器直接 `SyntaxError` / `AttributeError`,因此不做「按解释器分支」的兼容层,统一按下界 3.4 编写 - 控制台 handler 输出到 `stderr`,3.4 与 3.7 的 `stderr` 都以 `backslashreplace` 兜底:locale 非 UTF-8 时中文日志会转义成 `\uXXXX` 而不会抛异常,`bronya.log` 文件固定 UTF-8 不受影响;需要控制台可读时以 `LANG=zh_CN.UTF-8`(或 `PYTHONIOENCODING=utf-8`)启动 - `cgi` 模块在 Python 3.13 中已移除,如未来远端 Python 升级,需同步迁移 multipart 解析实现 --- ## 数据库设计(共享) ### 存储位置 数据库文件路径:`~/.bronya2/bronya2.db` 除 SQLite 外,少量不适合关系建模的数据以单文件 JSON 落盘在 `~/.bronya2/` 子目录(原子写 tmp + rename,损坏时转存 `.corrupt-<时间戳>` 不丢数据): - `~/.bronya2/api/data.json`:程序员工具「接口请求」的项目 / 接口集合,结构 `{ version: 1, projects: ApiProject[] }` ### Schema 版本管理 | 表名 | 用途 | | ---------------- | ------------------------ | | `schema_version` | 记录数据库 schema 版本,用于增量升级判断 | > 1.0 发布前所有表结构变更已统一合并为版本 1。后续变更: > - `v2`:新增 `task_metric_templates` 表(按任务标题持久化指标模板,新建同名任务自动继承) > - `v3`:新增 `commit_records` 表(代码提交记录,支持 CLI 写入、前端展示/复制/历史查询)+ 新配置键 `commit.detailUrl` > - `v4`:`build_projects` 表新增 `launch_command` 列(启动项目命令行,留空不展示「启动项目」按钮) > - `v5`:新增 `build_project_locks` 表(项目操作锁,构建 / git / 清理期间锁定项目,防止多人并发冲突) > - `v6`:新增 `build_envs` 表(构建调试多环境);`build_projects` 新增 `env_id` 列,唯一约束由 `folder_path` 改为 `(env_id, folder_path)`(旧表自动重建迁移,历史项目全部归属默认环境);环境级构建选项改存 `build_envs.options`,旧 `configs` 的 `build.*` 键首次启动时迁移到默认环境 > - `v7`:新增 `build_project_durations` 表(构建耗时历史),按项目 + 静默/普通模式各自保留最近 10 次成功构建耗时,用于列表 / 卡片模式展示预估构建完成时间 > - `v8`:`build_project_durations` 去掉 `mode` 列,耗时预估不再区分静默 / 普通模式,每项目统一保留最近 10 次成功构建(旧表自动重建,两种模式历史记录合并后每项目保留最近 10 条) > - `v9`:新增 `build_final_zips` 表(汇总批次记录),按 `env_id` 持久化每次构建产出的最终汇总 zip,含触发客户端 IP 与项目名,支持分页查询 > - `v10`:`build_projects` 表新增 `stage_flows` 列(阶段跃迁工作流 JSON 数组),从浏览器 localStorage 迁移到数据库,按项目独立保存 > - `v11`:新增 `build_operation_logs` 表(构建域操作日志),按 `env_id` 记录一键构建 / 远程上传 / 触发部署的操作者 IP 与时间,支持分页查询与全量清除(构建调试「操作日志」弹窗);环境删除时随 `deleteBuildEnv` 级联清理 > - `v12`:`build_operation_logs` 表新增 `detail` 列(操作动作补充环境信息:一键构建记构建环境名,远程上传 / 触发部署记远程环境名),旧表启动时自动补列 > > 当前 `CURRENT_SCHEMA_VERSION = 12`,首次启动时自动建表并按需按版本迁移。 ### 表结构 #### 1. users(用户信息) | 字段 | 类型 | 约束 | 说明 | | ----------- | ------- | ------------------- | ----- | | id | INTEGER | PK AUTOINCREMENT | 主键 | | name | TEXT | NOT NULL DEFAULT '' | 用户名 | | avatar | TEXT |
| 头像路径 | | email | TEXT |
| 邮箱 | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | #### 2. configs(配置表 — key-value 存储) | 字段 | 类型 | 约束 | 说明 | | ----------- | ------- | ---------------- | ------------------ | | id | INTEGER | PK AUTOINCREMENT | 主键 | | key | TEXT | NOT NULL UNIQUE | 配置键名 | | value | TEXT |
| 配置值 | | category | TEXT |
| 分类(theme/app/env/build/mindMap/misc) | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_configs_key(key)`, `idx_configs_category(category)` **已知配置键**: | Key | 分类 | 说明 | 默认值 | | ----------------------- | ----- | ---------------------------------------------------- | --------------- | | `theme.mode` | theme | 主题模式 | `light` | | `theme.background` | theme | 自定义背景图路径 | `''` | | `theme.bgOpacity` | theme | 背景叠加层透明度 (0-100) | `82` | | `app.autoLaunch` | app | 开机启动 | `false` | | `app.mailClient` | app | 邮件软件路径 | `''` | | `env.mvnCmd` | env | Maven 的 mvn 可执行文件路径 | `''` | | `env.mavenSettings` | env | Maven settings.xml 路径(打包时 -s 注入) | `''` | | `env.mvnRepo` | env | Maven 本地仓库路径(打包时 -Dmaven.repo.local 注入) | `''` | | `env.mavenOpts` | env | mvn JVM 内存参数(构建时以 MAVEN\_OPTS 环境变量注入) | `-Xms2g -Xmx4g` | | `env.nodePath` | env | node.exe 路径(构建时所在目录加入 PATH) | `''` | | `env.npmCmd` | env | npm 可执行文件路径(构建时所在目录加入 PATH,命令头以裸命令名 npm 执行) | `''` | | `env.gitCmd` | env | git 可执行文件路径(git pull / 分支查询) | `''` | | `env.powershellCmd` | env | powershell 可执行文件路径(指标执行器截图/系统操作,未配置时用系统 PATH 的 powershell.exe) | `''` | | `env.serviceUrls` | env | 环境远程服务配置(`ServiceEnvConfig[]` JSON,环境可动态增删改,代码不内置地址) | `''` | | `env.logApps` | env | 日志中心可选应用名称(`string[]` JSON,可动态增删改) | `''` | | ~~`build.outputDir`~~ | build | 【v6 起迁移至 `build_envs.options`】构建产物 zip 输出目录,仅首次升级时读取并迁移到默认环境,之后不再使用 | `''` | | ~~`build.gitPull`~~ | build | 【v6 起迁移至 `build_envs.options`】构建前是否先执行 git pull,同上仅迁移用 | `'false'` | | ~~`build.finalZipEnabled`~~ | build | 【v6 起迁移至 `build_envs.options`】是否开启汇总打包,同上仅迁移用 | `'false'` | | ~~`build.finalZipName`~~ | build | 【v6 起迁移至 `build_envs.options`】汇总 zip 文件名,同上仅迁移用 | `'dist'` | | `build.performanceMode` | build | 静默模式总开关(settings key 为历史遗留保留):`'true'` 时构建子进程 `stdio: 'ignore'`,不建立输出管道、不推送流式日志,仅回阶段与最终成败(失败只能看到退出码,无详细报错) | `'false'` | | `commit.detailUrl` | commit | 提交详情跳转地址模板,支持 `{hash}` 占位符,如 `https://github.com/user/repo/commit/{hash}` | `''` | | `tfs.idUrl` | tfs | 需求单详情跳转地址模板,数据分析 → 需求分析结果中点击 ID 跳转,支持 `{id}` 占位符,如 `https://tfs.example.com/tfs/Collection/_workitems?id={id}` | `''` | | `tfs.docUrlPrefix` | tfs | 设计文档远程地址前缀,数据分析 → 设计挂接结果中「远程」链接前缀,自动拼接 `/开发版本号/文档名` | `''` | | `mindMap.dataDir` | mindMap | 脑图数据目录(存放 `.km` 文件的本地目录,脑图面板列举该目录下所有 `.km` 文件) | `''` | | `web.ipWhitelist` | web | Web 端全局访问 IP 白名单(`string[]` JSON,仅 Web 端本机 127.0.0.1 可配置;非空时仅名单内 IP 可访问本站点,优先级高于黑名单;本机回环始终放行) | `''` | | `web.ipBlacklist` | web | Web 端全局访问 IP 黑名单(`string[]` JSON,白名单为空时生效,名单内 IP 不可访问;两者均空=不启用访问控制) | `''` | | `web.panelAcl` | web | Web 端菜单级 IP 名单(`Record` JSON,随 `/settings/web-acl` 读写;仅控制菜单显隐与前端跳转,不做接口校验;本机回环与 pinned 仪表盘不受限) | `''` | #### 3. tasks(每日流程任务) | 字段 | 类型 | 约束 | 说明 | | --------------- | ------- | -------------------------- | ----------------- | | id | INTEGER | PK AUTOINCREMENT | 主键 | | title | TEXT | NOT NULL | 任务标题 | | description | TEXT |
| 任务描述 | | is\_preset | INTEGER | NOT NULL DEFAULT 0 | 是否预设任务 (0/1) | | sort\_order | INTEGER | NOT NULL DEFAULT 0 | 排序序号 | | status | TEXT | NOT NULL DEFAULT 'pending' | 状态 | | scheduled\_date | TEXT | NOT NULL | 计划日期 (YYYY-MM-DD) | | completed\_at | INTEGER |
| 完成时间戳 | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_tasks_date(scheduled_date)`, `idx_tasks_status(status)`, `idx_tasks_sort(scheduled_date, sort_order)` **status 枚举值**: `pending` | `in_progress` | `done` | `skipped` #### 4. task\_metrics(任务指标) | 字段 | 类型 | 约束 | 说明 | | ------------ | ------- | -------------------------- | ------------------------ | | id | INTEGER | PK AUTOINCREMENT | 主键 | | task\_id | INTEGER | FK → tasks.id | 关联任务 (ON DELETE CASCADE) | | type | TEXT | NOT NULL | 指标类型 | | params | TEXT | NOT NULL DEFAULT '{}' | 指标参数(JSON) | | sort\_order | INTEGER | NOT NULL DEFAULT 0 | 排序序号 | | status | TEXT | NOT NULL DEFAULT 'pending' | 验证状态 | | evidence | TEXT |
| 凭证(截图路径 / 说明文本) | | verified\_at | INTEGER |
| 通过时间戳 | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_task_metrics_task(task_id)` **type 枚举值**: `screenshot` | `launch_app` | `web_screenshot` | `system_action` | `manual_input` | `clipboard_image` **status 枚举值**: `pending` | `passed` | `failed` **params 字段约定**: | type | 参数 | 说明 | | ------------------ | ------------- | --------------------------- | | `screenshot` | `captureMode` | `fullscreen` / `region` | | `launch_app` | `appPath` | 目标应用可执行文件路径 | | `web_screenshot` | `url` | 目标网页 URL | | `system_action` | `action` | `shutdown` / `restart` / `intranet` / `external`(内外网切换复用设置面板的 `toggleNetwork`,网卡名从 `net.intranetAdapter` / `net.externalAdapter` 读取) | | `manual_input` | `taskCount` | 需录入任务条数(确定后复制到剪切板) | | `clipboard_image` | — | 无参数,执行时从剪贴板粘贴图片(Ctrl+V)作为凭证 | #### 5. schedules(日程) | 字段 | 类型 | 约束 | 说明 | | ----------- | ------- | -------------------------- | ----- | | id | INTEGER | PK AUTOINCREMENT | 主键 | | title | TEXT | NOT NULL | 日程标题 | | description | TEXT |
| 日程描述 | | start\_time | TEXT | NOT NULL | 开始时间 | | end\_time | TEXT |
| 结束时间 | | category | TEXT |
| 分类 | | priority | TEXT | NOT NULL DEFAULT 'medium' | 优先级 | | status | TEXT | NOT NULL DEFAULT 'pending' | 状态 | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_schedules_start(start_time)`, `idx_schedules_status(status)` **priority 枚举值**: `low` | `medium` | `high` **status 枚举值**: `pending` | `done` | `cancelled` #### 6. todos(待办事项) | 字段 | 类型 | 约束 | 说明 | | ------------- | ------- | -------------------------- | ------------------------- | | id | INTEGER | PK AUTOINCREMENT | 主键 | | schedule\_id | INTEGER | FK → schedules.id | 关联日程 (ON DELETE SET NULL) | | title | TEXT | NOT NULL | 待办标题 | | description | TEXT |
| 待办描述 | | due\_date | TEXT |
| 截止日期 | | priority | TEXT | NOT NULL DEFAULT 'medium' | 优先级 | | status | TEXT | NOT NULL DEFAULT 'pending' | 状态 | | completed\_at | INTEGER |
| 完成时间戳 | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_todos_schedule(schedule_id)`, `idx_todos_due(due_date)`, `idx_todos_status(status)` **status 枚举值**: `pending` | `done` #### 7. editor\_files(编辑器文件) | 字段 | 类型 | 约束 | 说明 | | ------------ | ------- | ---------------------------- | ------------------ | | id | INTEGER | PK AUTOINCREMENT | 主键 | | file\_path | TEXT | NOT NULL UNIQUE | 关联的本地文件绝对路径 | | name | TEXT | NOT NULL | 标签页展示文件名 | | language | TEXT | NOT NULL DEFAULT 'plaintext' | monaco language id | | source | TEXT |
| 来源描述(如 `log`) | | source\_path | TEXT |
| 来源路径(如远程服务器日志路径) | | sort\_order | INTEGER | NOT NULL DEFAULT 0 | 标签顺序 | | is\_active | INTEGER | NOT NULL DEFAULT 0 | 上次会话激活标签 (0/1) | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_editor_files_sort(sort_order)` **生命周期约定**: 记录仅在编辑器「保存」落盘时创建;关闭已保存标签时删除对应记录(本地文件保留);启动恢复与运行期存在性检查时, 本地文件已缺失的记录自动清理(对应标签转为待保存状态)。 #### 8. build\_projects(构建项目) | 字段 | 类型 | 约束 | 说明 | | -------------- | ------- | -------------------------- | ------------------------------------------------------------------------------------------------ | | id | INTEGER | PK AUTOINCREMENT | 主键 | | env\_id | INTEGER | NOT NULL DEFAULT 1 | 所属构建环境 ID(FK → build\_envs(id);v6 起新增,旧表重建时历史项目全部填 1) | | name | TEXT | NOT NULL | 项目名称 | | folder\_path | TEXT | NOT NULL,联合唯一 `(env_id, folder_path)` | 项目文件夹绝对路径(同一构建环境内唯一;不同环境可添加同一文件夹) | | project\_type | TEXT | NOT NULL DEFAULT 'backend' | 项目类型(frontend / backend) | | build\_command | TEXT | NOT NULL | 打包指令(如 mvn package / npm run build) | | artifacts | TEXT | NOT NULL DEFAULT '\[]' | 输出产物列表(JSON 数组,每条含 outputPath / zipName / zipMode / zipRootName;monorepo / 父子模块可配置多条分别压缩,默认 1 条) | | clean\_paths | TEXT | NOT NULL DEFAULT '\[]' | 清理残留的文件夹/文件列表(JSON 数组,相对项目目录) | | env\_overrides | TEXT | NOT NULL DEFAULT '{}' | 单项目高级配置(环境覆盖,JSON 对象;前端 nodePath/npmCmd,后端 mvnCmd/mavenSettings/mvnRepo/mavenOpts,spawn 参数 execWindow/execPriority/envMode),留空字段回退全局配置 | | launch\_command | TEXT | NOT NULL DEFAULT '' | 启动项目命令行(如 `code .` / `webstorm .`),留空表示不展示「启动项目」按钮;点击按钮时在项目目录下 detached spawn 执行 | | stage\_flows | TEXT | NOT NULL DEFAULT '\[]' | 阶段跃迁工作流(JSON 数组,每条含 id / name / steps[step] / variables?[name+value] / locked?[bool],按项目独立保存的 Git 指令编排;v10 起新增;variables 字段后续加入,旧数据无该字段按空数组处理;step 可选 ignored? 标记临时忽略、clean 步骤可选 extraProjectIds?(同环境额外清理项目 id 列表)、flow 可选 locked? 标记锁定只读,旧数据无这几个字段按 false/空处理) | | sort\_order | INTEGER | NOT NULL DEFAULT 0 | 排序序号(环境内排序) | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_build_projects_sort(sort_order)`、`idx_build_projects_env(env_id)` > 历史版本曾使用 `output_path` / `zip_name` / `zip_mode` / `zip_root_name` 四个单产物字段,1.0 起统一合并为 `artifacts` JSON 数组以支持多产物;首次启动时自动从旧字段迁移回填。 > v6 起 `folder_path` 唯一约束改为 `(env_id, folder_path)` 联合唯一(SQLite 不支持 ALTER 约束,迁移采用建新表 → 拷贝数据 → 删旧表 → 重命名的标准重建方案)。 #### 8b. build\_envs(构建环境 — schema v6) 构建调试多环境:每个环境拥有独立的项目列表与构建选项,进入构建调试先展示环境卡片列表,点击进入该环境详情。 | 字段 | 类型 | 约束 | 说明 | | ----------- | ------- | --------------------- | ------------------------------------------------------------------ | | id | INTEGER | PK AUTOINCREMENT | 主键 | | name | TEXT | NOT NULL | 环境名称 | | description | TEXT | NOT NULL DEFAULT '' | 环境描述 | | options | TEXT | NOT NULL DEFAULT '{}' | 环境级构建选项 JSON:`outputDir` 输出目录 / `gitPull` 构建前拉取 / `finalZipEnabled` 汇总打包开关 / `finalZipName` 汇总 zip 名 / `allowedBranches` 构建分支白名单 / `deployEnvCode` 已选远程上传/部署环境 code / `ipWhitelist` `ipBlacklist` Web 端环境可见 IP 名单(v12 起移除 `pruneLocalBranches` 环境级字段,改由切换分支弹窗内联开关 + localStorage `bronya2.buildDebug.pruneAfterSwitch` 控制默认开启) | | sort\_order | INTEGER | NOT NULL DEFAULT 0 | 排序序号 | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | > - 新项目首次使用时自动创建一个空的「默认环境」(id=1),环境选项从旧版 `configs` 的 `build.*` 键迁移 > - 复制环境为整行 SQL 深拷贝(环境 + 其下全部项目配置),新环境名为「源名 副本」;删除环境事务内级联删除其项目与锁,且至少保留一个环境 > - 环境列表附带 `projectCount` / `projectNames` 聚合字段(不入库,查询时按 env_id 统计)供卡片展示 > - 导入导出按环境隔离:导出 JSON 结构 `{version: '2.0', env: {name, description, options}, projects}`,导入仅全量同步目标环境内项目 > - **Web 端环境可见性(IP 黑白名单)**:仅 Web 端生效,Electron 端不过滤。**本机回环地址(127.0.0.1)始终全部可见,不受黑白名单管控**;其余 IP 规则为白名单优先——`ipWhitelist` 非空时仅名单内 IP 可见该环境(黑名单被忽略);白名单为空时 `ipBlacklist` 生效(名单内 IP 不可见);两者均为空时所有 IP 可见。名单配置入口仅对**本机 127.0.0.1 访问**的环境编辑弹窗展示(`GET /api/system/client-info` 判定 localhost),非本机请求携带名单字段会被服务端强制忽略 / 回填库内值;环境列表、项目列表、构建执行等环境作用域接口均按规则拦截。IP 归一化处理 `::ffff:127.0.0.1` 与 `::1`,且不信任 `X-Forwarded-For`(未开启 trust proxy) > - **Web 端全局访问 IP 黑白名单**(`configs` 表 `web.ipWhitelist` / `web.ipBlacklist`,仅 Web 端生效):与上面的环境级 ACL 同规则,但作用于整个 Web 站点全部接口与 SPA 入口,判定逻辑集中在 `server/lib/webAcl.ts`(`webAclState`)。**本机 127.0.0.1 始终放行不受管控**;其他 IP 命中拒绝条件时,`/api/*` 请求由服务端 ACL 中间件直接返回 403 JSON(仅含客户端 IP),HTML/SPA 请求由中间件直接返回内联「禁止访问」页(可爱风、仅展示客户端 IP 与提示语,不进 SPA)。**被拒客户端一律不下发黑白名单 / 菜单名单内容**(403 响应与预检端点均只回 `denied: true` + 客户端 IP,避免泄露管理员配置)。两者均空=不启用访问控制。配置入口在「设置 → Web 访问」面板,仅 `isWeb && webClientLocalhost`(即 Web 端本机 127.0.0.1 访问)可见可配置,PC 端无此能力也无限制。预检端点 `GET /api/health`、`GET /api/system/client-info`、`GET /api/settings/web-acl` 始终放行(供 SPA 自检渲染禁止访问页);`PUT /api/settings/web-acl` 仅本机可写,非本机请求不回写也不回显名单。SPA 在挂载时也会自检(兜底,应对 ACL 配置变更后已加载的 SPA),直接使用 `GET /api/settings/web-acl` 返回的 `denied` 字段,命中则渲染 `QyForbiddenView`。Settings 界面以 `QyTagTextarea` 录入名单,并提供「复制清单」(逗号分隔)与「粘贴回填」(支持逗号 / 换行 / 空白分隔并合并去重)按钮 > - **Web 端菜单级 IP 黑白名单**(`configs` 表 `web.panelAcl`,JSON `Record`,仅 Web 端生效):在「设置 → Web 访问」面板按菜单(面板)逐个配置,规则同全局名单(白名单非空仅白名单可见,否则黑名单内不可见,两者均空不限制),**本机 127.0.0.1 始终可见全部菜单,pinned 仪表盘不参与配置**。与全局 ACL 的区别:菜单级名单**只做前端可见性控制、不做接口级校验**——命中后该菜单从侧边栏分组(整组菜单都被隐藏时分组也不渲染)中消失,全局快捷搜索(双击 Shift)不返回该菜单结果、仪表盘快捷入口 / 统计卡 / 卡片「查看全部」等跳转入口同步隐藏,`switchPanel` / `openPanel` 统一守卫兜底确保任何入口都无法切到隐藏面板;已打开的隐藏面板 tab 与当前面板在 SPA 启动载入名单时自动收敛(移除 tab / 切回首个可用面板)。配置随 `GET/PUT /api/settings/web-acl` 的 `panels` 字段一并读写,入库前经服务端统一归一化去重并丢弃名单均空的条目 #### 9. build\_project\_locks(项目操作锁 — schema v5) 构建 / git pull / 切换分支 / 新建分支 / 推送分支 / 合并分支 / 清理期间锁定项目,防止多人并发操作同一项目;操作结束后释放。 | 字段 | 类型 | 约束 | 说明 | | ----------- | ------- | ------------------------------- | --------------------------------------------- | | project\_id | INTEGER | PK, FK → build\_projects(id) ON DELETE CASCADE | 锁定的项目 ID(一个项目同时最多一把锁) | | locked\_by | TEXT | NOT NULL | 锁持有者标识(PC 端主机名,Web 端客户端 IP) | | operation | TEXT | NOT NULL | 锁定的操作类型:`build` / `git-pull` / `git-switch` / `clean` | | locked\_at | INTEGER | NOT NULL | 加锁时间戳 | #### 9b. build\_project\_durations(构建耗时历史 — schema v7 引入,v8 去模式化) 记录每个项目的成功构建耗时,用于列表 / 卡片模式展示预估构建完成时间。单一项目最多保留最近 10 条,超出时删除最早记录(v8 起不区分静默 / 普通模式,统一统计)。 | 字段 | 类型 | 约束 | 说明 | | ----------- | ------- | ------------------------------- | --------------------------------------------- | | id | INTEGER | PK AUTOINCREMENT | 主键 | | project\_id | INTEGER | NOT NULL, FK → build\_projects(id) ON DELETE CASCADE | 项目 ID | | duration\_ms| INTEGER | NOT NULL | 本次成功构建耗时(毫秒),仅记录成功构建 | | created\_at | INTEGER | NOT NULL | 记录时间戳 | **索引**: `idx_build_durations_project_created(project_id, created_at)` #### 10. commit\_records(代码提交记录 — schema v3) 支持通过命令行参数写入,主页展示未复制记录,复制后归档到历史记录中查询。 | 字段 | 类型 | 约束 | 说明 | | ---------------------- | ------- | ---------------------- | ----------------------------------------------------- | | id | INTEGER | PK AUTOINCREMENT | 主键 | | create\_date\_text | TEXT | NOT NULL | 提交日期 YYYY-MM-DD | | create\_date\_timestamp| TEXT | NOT NULL | 提交时间戳(unix 秒字符串) | | hash | TEXT | NOT NULL UNIQUE | 提交哈希(唯一,冲突时忽略重复写入) | | content | TEXT | NOT NULL DEFAULT '' | 提交信息 | | lines | TEXT | NOT NULL DEFAULT '' | 变更行数 | | repos | TEXT | NOT NULL DEFAULT '' | 仓库名 | | copied | INTEGER | NOT NULL DEFAULT 0 | 是否已复制(0=未复制/主页展示,1=已复制/仅在历史记录中可见) | | created\_at | INTEGER | NOT NULL | 创建时间戳 | | updated\_at | INTEGER | NOT NULL | 更新时间戳 | **索引**: `idx_commit_records_date(create_date_text)`, `idx_commit_records_copied(copied)` --- ## IPC API 接口文档(共享) ### 统一响应格式 所有 IPC 调用返回 `IpcResponse`: ```typescript interface IpcResponse { success: boolean data?: T error?: string } ``` ### IPC Channel 总表 #### 系统域 + 窗口控制域 | Channel | 方法 | 参数 | 返回 data | 说明 | | ------------------------ | ------------------------------ | --------------------- | ------------------------ | ------------------ | | `system:get-info` | `systemApi.getInfo()` | `{ detail: boolean }` | `SystemGetInfoResult` | 获取系统信息 | | `system:open-external` | `systemApi.openExternal()` | `url: string` | `null` | 系统浏览器打开 URL | | `system:open-path` | `systemApi.openPath()` | `p: string` | `null` | 资源管理器打开路径 | | `system:launch-app` | `systemApi.launchApp()` | `command: string` | `null` | 启动 Windows 应用(detached spawn 脱离 Electron,仅 Windows) | | `system:toggle-network` | `systemApi.toggleNetwork()` | `payload: SystemToggleNetworkPayload` | `null` | 切换内外网:netsh 网卡启用/禁用 + reg.exe 代理开关,仅 Windows | | `system:get-network-adapters` | `systemApi.getNetworkAdapters()` | — | `NetworkAdapterInfo[]` | 扫描网络适配器列表(netsh interface show interface),每项含 `name` 与 `ipv4`(来自 `os.networkInterfaces()`,禁用/无 IPv4 地址时为 `null`),仅 Windows | | `window:minimize` | `systemApi.minimize()` | — | `null` | 最小化窗口 | | `window:toggle-maximize` | `systemApi.toggleMaximize()` | — | `{ maximized: boolean }` | 切换最大化/还原 | | `window:close` | `systemApi.close()` | — | `null` | 关闭窗口(被拦截后推送关闭确认) | | `window:hide-to-tray` | `systemApi.hideToTray()` | — | `null` | 隐藏窗口到托盘 | | `window:quit` | `systemApi.quit()` | — | `null` | 真正退出应用 | | `window:close-requested` | `systemApi.onCloseRequested()` | —(主进程推送) | — | 关闭确认请求事件(订阅返回取消函数) | **SystemGetInfoResult**: ```typescript { platform: string // 'win32' | 'darwin' | 'linux' arch: string // 'x64' | 'arm64' ... cpus: number // CPU 核心数 hostname: string // 主机名 appVersion: string // 应用版本 electronVersion: string // Electron 版本 nodeVersion: string // Node.js 版本 userInfoHome: string // 用户主目录 } ``` #### 设置域 | Channel | 方法 | 参数 | 返回 data | 说明 | | -------------------------------- | --------------------------------- | ---------------------------------- | ------------------------------ | ------------------------------------------ | | `settings:get-all` | `settingsApi.getAll()` | — | `Record` | 读取所有配置 | | `settings:get` | `settingsApi.get()` | `{ key: string }` | `string` | 读取单个配置 | | `settings:set` | `settingsApi.set()` | `{ key, value }` | `{ key, value }` | 写入配置 (UPSERT) | | `settings:get-launch` | `settingsApi.getLaunch()` | — | `{ enabled: boolean }` | 查询开机启动状态 | | `settings:set-launch` | `settingsApi.setLaunch()` | `{ enabled: boolean }` | `{ enabled: boolean }` | 设置开机启动 | | `settings:select-file` | `settingsApi.selectFile()` | `{ title, filters, defaultPath? }` | `{ canceled, filePaths }` | 文件选择对话框(`defaultPath` 可选,传入已存在路径时直接定位到该路径) | | `settings:select-dir` | `settingsApi.selectDir()` | `{ title, defaultPath? }` | `{ canceled, filePaths }` | 目录选择对话框(`defaultPath` 可选,传入已存在路径时直接定位到该目录) | | `settings:read-file-as-data-url` | `settingsApi.readFileAsDataUrl()` | `{ path }` | `{ dataUrl: string }` | 将本地图片读取为 data URL(背景图内嵌存储,PC / Web 双端可展示) | | `settings:get-service-urls` | `settingsApi.getServiceUrls()` | — | `{ envs: ServiceEnvConfig[] }` | 查询全部环境配置(环境可动态增删改,未配置的地址为空字符串) | > **注意**: 调用 `settingsApi.set({ key: 'app.autoLaunch', value: 'true' })` 时,主进程会自动同步到系统开机启动设置。 #### 任务域 | Channel | 方法 | 参数 | 返回 data | 说明 | | --------------------- | --------------------------- | ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------ | | `task:list` | `taskApi.list()` | `{ date: string }` | `Task[]` | 列出某日全部任务(附带指标) | | `task:create` | `taskApi.create()` | `TaskCreatePayload` | `Task` | 创建任务 | | `task:update` | `taskApi.update()` | `TaskUpdatePayload` | `Task` | 更新任务(done/skipped 受顺序与指标约束) | | `task:delete` | `taskApi.delete()` | `id: number` | `null` | 删除任务(级联删除指标) | | `task:reorder` | `taskApi.reorder()` | `{ orders: {id,sortOrder}[] }` | `null` | 批量调整排序 | | `task:next` | `taskApi.next()` | `{ date: string }` | `Task \| null` | 推进到下一个任务(当前任务指标需全部通过) | | `task:reset-day` | `taskApi.resetDay()` | `{ date: string }` | `Task[]` | 重置某日全部任务与指标,同步清空当日关联的子任务历史(manual\_input\_tasks)与录入草稿(manual\_input\_drafts),保持子任务与主任务状态一致 | | `task:metrics-save` | `taskApi.saveMetrics()` | `TaskMetricsSavePayload` | `TaskMetric[]` | 整体保存任务指标配置 | | `task:metric-run` | `taskApi.runMetric()` | `{ metricId: number }` | `TaskMetricRunResult` | 执行指标动作并返回验证结果 | | `task:metric-cancel` | `taskApi.cancelMetricRun()` | — | `null` | 取消区域截图等待 | | `task:metric-confirm` | `taskApi.confirmMetric()` | `TaskMetricConfirmPayload` | `TaskMetric` | 人工确认指标通过/失败 | | `task:metric-save-clipboard-image` | `taskApi.saveClipboardImage()` | `TaskMetricSaveClipboardImagePayload` | `TaskMetric` | 粘贴剪贴板图片作为指标凭证(clipboard\_image 指标) | **TaskCreatePayload**: ```typescript { title: string description ? : string isPreset ? : boolean date: string // YYYY-MM-DD sortOrder ? : number } ``` **TaskUpdatePayload**: ```typescript { id: number title ? : string description ? : string status ? : string // 'pending' | 'in_progress' | 'done' | 'skipped' sortOrder ? : number } ``` **task:next 流程**: 若当前有 `in_progress` 任务,先校验其全部指标已通过(否则返回错误),标记为 `done`,然后找到下一个 `pending` 任务置为 `in_progress`。若无 pending 任务则返回 `null`。 **顺序执行约束**: `task:update` 将任务置为 `done` / `skipped` 时,主进程校验该任务必须为当前任务(`in_progress` 或首个 `pending`),且 `done` 要求全部指标 `passed`,否则返回错误。 **TaskMetricRunResult**: ```typescript { metric: TaskMetric // 执行后的指标(needsConfirm 时为原状态) needsConfirm: boolean // 为 true 时需渲染端弹窗人工确认 message: string // 结果描述 } ``` #### 日程域 | Channel | 方法 | 参数 | 返回 data | 说明 | | ----------------- | ------------------------------ | ------------------------ | ------------ | --------- | | `schedule:list` | `scheduleApi.listSchedules()` | `{ startDate, endDate }` | `Schedule[]` | 查询时间范围内日程 | | `schedule:create` | `scheduleApi.createSchedule()` | `ScheduleCreatePayload` | `Schedule` | 创建日程 | | `schedule:update` | `scheduleApi.updateSchedule()` | `ScheduleUpdatePayload` | `Schedule` | 更新日程 | | `schedule:delete` | `scheduleApi.deleteSchedule()` | `id: number` | `null` | 删除日程 | | `todo:list` | `scheduleApi.listTodos()` | `TodoListPayload` | `Todo[]` | 查询待办 | | `todo:create` | `scheduleApi.createTodo()` | `TodoCreatePayload` | `Todo` | 创建待办 | | `todo:update` | `scheduleApi.updateTodo()` | `TodoUpdatePayload` | `Todo` | 更新待办 | | `todo:delete` | `scheduleApi.deleteTodo()` | `id: number` | `null` | 删除待办 | #### 数据库域 | Channel | 方法 | 参数 | 返回 data | 说明 | | ----------------- | ---------------------- | -- | ---------------------- | ------- | | `database:status` | `databaseApi.status()` | — | `DatabaseStatusResult` | 查询数据库状态 | | `database:init` | `databaseApi.init()` | — | `DatabaseStatusResult` | 手动触发初始化 | #### 编辑器文件域(代码编辑器本地文件关联持久化) | Channel | 方法 | 参数 | 返回 data | 说明 | | ---------------------- | -------------------------- | ------------------------- | -------------------------- | ------------------------------------------------------ | | `editor:files-restore` | `editorApi.restoreFiles()` | — | `EditorFilesRestoreResult` | 恢复持久化编辑器文件(自动清理本地文件缺失的记录) | | `editor:file-save` | `editorApi.saveFile()` | `EditorFileSavePayload` | `EditorFileSaveResult` | 内容落盘 + upsert 记录(未关联路径时弹保存对话框,用户取消返回 `canceled: true`) | | `editor:files-sync` | `editorApi.syncState()` | `EditorFilesStatePayload` | `null` | 同步已保存标签状态(名称/顺序/激活,删除已关闭标签的记录) | | `editor:files-check` | `editorApi.checkFiles()` | `{ filePaths: string[] }` | `EditorFilesCheckResult` | 检查本地文件存在性(清理缺失记录) | #### 日志域(远程服务器日志列表 / 下载,协议同第一代 bronya) | Channel | 方法 | 参数 | 返回 data | 说明 | | -------------- | ------------------- | --------------------------------- | ----------------------- | ---------------------------------------------------------- | | `log:list` | `logApi.list()` | `{ envCode, app }` | `LogFileItem[]` | 获取指定环境/应用的日志文件列表(POST body `{app}`,服务端返回 `{status, data}`) | | `log:download` | `logApi.download()` | `{ envCode, app, name, saveDir }` | `{ savedPath: string }` | 下载日志文件流式落盘到指定目录(POST body `{app, name}`) | | `log:read` | `logApi.read()` | `{ envCode, app, name }` | `{ content: string }` | 读取日志文件文本内容(POST body `{app, name}`,用于日志中心「查看」在编辑器预览) | **LogFileItem**(字段与服务端返回保持一致): ```typescript { name: string // 文件名 size: string // 文件大小(服务端返回的展示文本) create_time: string // 创建时间(服务端返回的展示文本) modify_time: string // 修改时间(服务端返回的展示文本) } ``` > 日志列表/下载地址来自 `envUrls` 服务(设置面板「环境服务配置」可视化维护,持久化于 configs 表 `env.serviceUrls`), > 数据获取集中在 `electron/main/services/logService.ts` 的 provider 层,HTTP 基于 Node 原生 `fetch`。 #### 构建域(构建调试:前后端项目一键打包) | Channel | 方法 | 参数 | 返回 data | 说明 | | ---------------------------- | -------------------------------- | -------------------------------- | ------------------------------ | ----------------------------------------------------- | | `build:envs` | `buildApi.listEnvs()` | — | `BuildEnv[]` | 列出全部构建环境(附带 projectCount / projectNames;首次调用自动种子默认环境) | | `build:env-create` | `buildApi.createEnv()` | `BuildEnvCreatePayload` | `BuildEnv` | 新增构建环境(空环境,选项为默认值) | | `build:env-update` | `buildApi.updateEnv()` | `BuildEnvUpdatePayload` | `BuildEnv` | 更新环境名称 / 描述 / 构建选项(仅给出的字段更新) | | `build:env-delete` | `buildApi.deleteEnv()` | `id: number` | `null` | 删除环境(事务内级联删除其项目与锁,至少保留一个环境) | | `build:env-duplicate` | `buildApi.duplicateEnv()` | `id: number` | `BuildEnv` | 复制环境(SQL 整行深拷贝环境与全部项目,新环境名「源名 副本」) | | `build:project-list` | `buildApi.listProjects()` | `envId: number` | `BuildProject[]` | 列出指定构建环境下的构建项目 | | `build:project-create` | `buildApi.createProject()` | `BuildProjectCreatePayload`(含 `envId`) | `BuildProject` | 添加构建项目(校验文件夹存在与同环境内去重) | | `build:project-update` | `buildApi.updateProject()` | `BuildProjectUpdatePayload` | `BuildProject` | 更新构建项目 | | `build:project-batch-update` | `buildApi.batchUpdateProjects()` | `BuildProjectBatchUpdatePayload` | `BuildProject[]` | 批量更新同类型项目的相同配置(打包指令 / 清理路径 / 环境覆盖),仅应用 updates 中给出的字段 | | `build:project-delete` | `buildApi.deleteProject()` | `id: number` | `null` | 删除构建项目 | | `build:project-launch` | `buildApi.launchProject()` | `id: number` | `null` | 启动项目:在项目目录下 detached spawn 执行 launchCommand(如 `code {projectPath}` / `webstorm {projectPath}` 打开 IDE),支持 `{projectPath}` 占位符替换为项目目录;PC 端走主进程 IPC,Web 端走 `POST /build/projects/:id/launch`(服务端 Node spawn) | | `build:branches` | `buildApi.branches()` | `{ folderPaths: string[] }` | `{ items: BuildBranchItem[] }` | 查询各目录 git 分支(非 git 仓库 isGitRepo: false) | | `build:git-pull` | `buildApi.gitPull()` | `projectId: number` | `GitMergeResult` | 单项目 git pull 拉取最新代码(独立于一键构建流程);本地与远程分叉冲突时 `success=false` 但 `data.conflictFiles` 含冲突文件列表,由渲染层弹 `MergeConflictDialog` 人工解决(与 git-merge 同构) | | `build:git-branches` | `buildApi.gitBranches()` | `folderPath: string` | `{ isGitRepo, current, branches: string[], local?: string[], remote?: string[] }` | 列出项目所有分支(本地 `local` + 远程 `remote` 去掉 `origin/` 前缀、过滤 `*/HEAD`,并合并去重为 `branches`)与当前分支 | | `build:git-switch` | `buildApi.gitSwitch()` | `{ projectId, branch }` | `null` | 切换分支:`git checkout ` | | `build:git-merge` | `buildApi.gitMerge()` | `{ projectId, branch }` | `GitMergeResult` | 合并分支:`git merge --no-ff ` 强制产生 merge commit;冲突时 `success=false` 但 `data.conflictFiles` 含冲突文件列表(含 `<<<<<<<` / `=======` / `>>>>>>>` 标记的完整内容),由渲染层弹 `MergeConflictDialog` 人工解决 | | `build:git-merge-abort` | `buildApi.gitMergeAbort()` | `projectId: number` | `null` | 取消合并:`git merge --abort` 完全还原工作区与 index(人工取消合并时调用,工作流停止) | | `build:git-merge-add` | `buildApi.gitMergeAdd()` | `{ projectId, path, content }` | `null` | 写入已解决的冲突文件并 `git add` 标记已解决(防目录穿越校验,仅允许写入项目目录内文件) | | `build:git-merge-commit` | `buildApi.gitMergeCommit()` | `projectId: number` | `GitMergeResult`(含 `mergeCommit`) | 提交合并:`git commit --no-edit` 使用 `.git/MERGE_MSG` 默认 message(`Merge branch 'xxx'`),成功后返回新 commit hash | | `build:locks` | `buildApi.locks()` | — | `{ locks: Record }` | 查询所有项目的锁定状态(projectId → 锁记录) | | `build:durations` | `buildApi.durations()` | `{ projectIds: number[] }` | `{ items: BuildDurationItem[] }` | 批量查询多项目的平均构建耗时(最近 10 次成功构建,不区分静默 / 普通模式),用于预估构建完成时间 | | `build:run` | `buildApi.run()` | `BuildRunPayload`(可带 `branchCheckConfirmed`) | `BuildRunResult`(拦截时返回 `branchViolations`) | 一键构建(多项目并发执行,完成后返回汇总;输出目录留空时兜底默认产物目录;构建前后端实时校验环境分支白名单) | | `build:cancel` | `buildApi.cancel(sessionId?)` | `sessionId?: string` | `null` | 取消构建:传 sessionId 仅取消该会话(多会话并发),不传取消全部(Windows 下 taskkill /T 杀整棵进程树) | | `build:default-output-dir` | `buildApi.defaultOutputDir()` | — | `string` | 构建产物默认输出目录 \~/.bronya2/build-output(自动创建) | | `build:env-list` | `buildApi.envList()` | — | `{ envs: DeployEnv[] }` | 远程部署环境列表(仅已配置任意地址的动态环境,在设置面板「环境服务配置」维护) | | `build:upload` | `buildApi.upload()` | `BuildUploadPayload` | `BuildUploadResult` | 产物 zip 分块上传到远程环境(分块日志经 build:progress 推送) | | `build:deploy` | `buildApi.deploy()` | `BuildDeployPayload` | `BuildDeployResult` | 触发远程环境部署 | | `build:clean-preview` | `buildApi.cleanPreview()` | `BuildCleanPayload` | `BuildCleanPreviewResult` | 预览清理残留的路径明细(供确认弹窗展示) | | `build:clean` | `buildApi.clean()` | `BuildCleanPayload` | `BuildCleanResult` | 执行清理:删除残留路径与可选的 zip 输出包 | | `build:progress` | `buildApi.onProgress()` | —(主进程推送) | — | 构建进度事件(订阅返回取消函数) | **BuildRunPayload**: ```typescript { sessionId: string // 构建会话 ID(前端生成),多会话并发时按会话取消 projectIds: number[] // 选中构建的项目 gitPull: boolean // 构建前是否先执行 git pull outputDir: string // zip 产物输出目录 finalZip: { enabled: boolean; name: string } // 是否汇总打包为最终 zip 及其名称 } ``` **BuildProgressEvent**(`build:progress` 推送): `{ projectId: number | null, type: 'phase' | 'log' | 'success' | 'failed' | 'finished', phase?, message?, elapsedMs?, zipPath?, finalZipPath? }`。 `projectId` 为 `null` 时表示整体阶段(final-zip / finished)。 **构建流程**: 多项目**并发**执行(各项目互不等待):`git pull`(可选)→ 打包指令(自动注入 mvn/settings.xml/仓库与 npm 路径,单项目高级配置优先于设置面板全局配置)→ 将输出产物压缩为 `/.zip`(压缩方式可配置:`contents` 仅打包目录内容、`dir` 目录以文件夹形式打包、`flat` 单文件直接打包,`dir`/`flat` 模式可用 `zipRootName` 重命名根目录/文件); 全部完成后可选将各项目产物 zip 合并为最终 zip(zip 根层级内置与 zip 同名(不含 .zip)的文件夹,各产物 zip 置于其中)。单项目失败标记红色,不影响其他项目。构建中可经 `build:cancel`(携带 sessionId)取消对应会话:仅终止该会话的构建子进程,各项目以「构建已取消」失败收尾并跳过最终汇总 zip;不传 sessionId 时取消全部会话。 **远程上传 / 部署**: 上传目标优先取最终汇总 zip,未汇总时上传各构建成功项目的产物 zip;上传协议与第一代 bronya 一致(2MB 分块顺序上传,表单字段 `file/name/block/total/md5`,服务端返回 `status === 200` 视为成功),HTTP 基于 Node 原生 `fetch/FormData/Blob`;触发部署为对 `deployUrl` 的 POST 请求。环境地址由 `envUrls` 服务提供(代码不内置任何地址,环境列表可动态增删改,需在设置面板「环境服务配置」维护,持久化于 configs 表 `env.serviceUrls`;地址为空时操作会提示未配置)。进度阶段 `BuildPhase` 扩展了 `upload` / `deploy`,日志同样经 `build:progress` 推送(`projectId: null`)。 #### 挡板域(挡板数据配置:读取 / 修改 / 保存服务器挡板服务的模拟数据) | Channel | 方法 | 参数 | 返回 data | 说明 | | ------------------ | ---------------------- | ----------------------- | --------------------------- | ---------------------------- | | `shield:env-list` | `shieldApi.envList()` | — | `{ envs: ShieldEnvItem[] }` | 挡板环境列表(与构建部署环境同源,已配置任意地址的环境) | | `shield:data-load` | `shieldApi.loadData()` | `ShieldDataLoadPayload` | `{ data: MockDataMap }` | 读取指定环境的挡板数据 | | `shield:data-save` | `shieldApi.saveData()` | `ShieldDataSavePayload` | `null` | 保存挡板数据到指定环境 | **MockDataMap**(挡板数据结构,沿用第一代 bronya): `{ [接口名]: MockRow[] }`,每条 `MockRow` 含 `matcher`(`'*'` 匹配任意请求 或 `{key, value}[]` 键值对)、`method`(GET/POST)、`requestBody` / `responseText`(JSON 对象)、`remark`(备注)。 > 读写走挡板服务真实接口(协议同第一代 bronya):读取 POST `getMockDataUrl` 返回 `{status: 200, data}`,保存 POST `postMockDataUrl` body 为完整 JSON; > 地址由 `envUrls` 服务提供,可在设置面板「环境服务配置」修改。 #### 脑图域(kityminder 思维导图浏览:.km 文件列举 / 读取 + 数据目录存取) | Channel | 方法 | 参数 | 返回 data | 说明 | | ------------------------ | --------------------------- | --------------------- | ------------------------ | ------------------------------------------- | | `mind:list` | `mindApi.list()` | — | `MindListResult` | 列举当前数据目录下所有 `.km` 文件(未配置目录时返回空列表 + dir 空字符串) | | `mind:read` | `mindApi.read()` | `MindReadPayload` | `MindReadResult` | 读取指定 `.km` 文件原始 JSON 文本(路径在数据目录内校验,防目录穿越) | | `mind:get-kityminder-url` | `mindApi.getKityminderUrl()` | — | `string` | kityminder 入口 index.html 访问 URL(PC 走 `bronya2://file` / Web 走 `/kityminder`) | | `mind:get-data-dir` | `mindApi.getDataDir()` | — | `string` | 读取当前配置的脑图数据目录(settings.mindMap.dataDir) | | `mind:set-data-dir` | `mindApi.setDataDir()` | `MindSetDataDirPayload` | `MindSetDataDirResult` | 持久化数据目录到 configs 表(PC 端 dir 为空时弹系统目录选择对话框) | **MindListResult**: `{ dir: string, files: MindMapMeta[] }`,`MindMapMeta`: `{ name: string, size: number, mtime: number }`。 **MindReadResult**: `{ name: string, json: string }`(`.km` 文件原始文本,由 iframe 内 kityminder `importJson` 解析)。 > 脑图面板以 iframe 加载 kityminder 入口(URL 带 `?fromParent=1` 参数),通过 `window.postMessage` 向 iframe 下发 `{type: 'loadMindMap', json, fileName}` 消息传递 JSON 数据,避免跨域读取本地文件; > iframe 内 kityminder 加载完成后向 `parent` 回送 `{type: 'mindMapReady'}`,父窗口据此下发待发送文件。 > 数据目录可在「设置 → 数据 → 脑图数据目录」配置,也可在脑图面板内点击「切换数据目录」临时切换(PC 走系统对话框 / Web 走 FileBrowserDialog)。 #### 需求分析域(CSV 导入 + 手动需求号比对) | Channel | 方法 | 参数 | 返回 data | 说明 | | ------------------------------ | ----------------------------------- | ------------------------------- | ---------------------------- | ------------------------------------------ | | `requirement-analysis:analyze` | `requirementAnalysisApi.analyze()` | `RequirementAnalysisPayload` | `RequirementAnalysisResult` | 解析 CSV 需求数据 + 手动需求号,返回下发需求 / 差异需求 / 未匹配三区块 | **RequirementAnalysisPayload**: `{ filePath: string, manualNumbers: string[] }`(PC 端 `filePath` 为系统对话框选中的本地路径,由主进程读文件;Web 端为自建资源管理器 FileBrowserDialog 选中的服务端路径,可浏览任意盘符/目录,由服务端直接读取,与 PC 端 selectFile 行为对齐)。 结果区复制/导出:复制需求号+需求名走 `copyToClipboard`;导出 CSV **不使用浏览器原生下载**——PC 端经 `settingsApi.selectDir` 选目录、Web 端经 FileBrowserDialog `mode="dir"` 选服务器目录,统一调 `fsApi.saveTextFile({dir, filename, content})` 写盘(IPC `fs:save-text-file` / HTTP `POST /fs/save-text`,`filename` 强制 basename 防穿越,返回落盘绝对路径)。 **RequirementAnalysisResult**: ```typescript { issuedDemands: IssuedRequirement[] // 下发需求(CSV 按需求号汇总,合并 ID / 人员 / 版本 / 日期) differenceDemands: IssuedRequirement[] // 差异需求(CSV 存在但手动未输入的需求号) notFoundNumbers: string[] // 未匹配(手动输入但 CSV 不存在的需求号) } ``` `IssuedRequirement`: `{ requirementNumber, ids: string[], title, createDate, acceptDate, integratedVersion, developmentVersion, members: string[] }`(需求号从标题 `空格前第一段 split('-')[0]` 解析)。 #### 设计挂接域(CSV 任务 + 扫描目录,检查设计文档挂接) | Channel | 方法 | 参数 | 返回 data | 说明 | | ------------------------------ | ---------------------------------- | --------------------------- | -------------------------- | -------------------------------------------------------- | | `design-documents:analyze` | `designDocumentsApi.analyze()` | `DesignDocumentsPayload` | `DesignDocumentsResult` | 扫描 `scanFolder/<开发版本号>/` 下名称含工作项 ID 的文档,返回正常挂接 / 未挂接两区块 | **DesignDocumentsPayload**: `{ filePath: string, scanFolder: string, docUrlPrefix: string }`(`filePath` 为 TFS 导出 CSV 路径;`scanFolder` 为扫描根目录,其下按开发版本号建子目录;`docUrlPrefix` 为设置项 `tfs.docUrlPrefix` 远程地址前缀,可为空)。PC 端两个路径均由系统对话框选择,Web 端由 FileBrowserDialog(file / dir 模式)选择服务端路径。 **DesignDocumentsResult**: ```typescript { normalContent: DesignDocItem[] // 已挂接(版本目录存在且找到名称含 ID 的文档,带本地目录 + 远程地址) questionContent: DesignDocItem[] // 未挂接(reason: 'version-missing' 版本目录不存在 | 'doc-missing' 未找到挂接文档) } ``` `DesignDocItem`: `{ id, title, developVersion, docName, url, folderPath, reason }`。远程地址 = `docUrlPrefix/开发版本号/文档名`;「本地」经 `fsApi.open` 打开版本目录,「远程」经 `systemApi.openExternal` 打开。 #### 接口请求域(Postman 风格接口调试 + 集合持久化 + OpenAPI 互转) 集合数据以单文件 JSON 存于 `~/.bronya2/api/data.json`(不进 SQLite);HTTP 请求由主进程 / 服务端用 Node 全局 `fetch` 发出(绕开浏览器 CORS),`AbortController` 控制超时(默认 30s,钳制 1s~300s)。 | Channel | 方法 | 参数 | 返回 data | 说明 | | --- | --- | --- | --- | --- | | `api-tool:projects-list` | `apiToolApi.listProjects()` | 无 | `ApiProject[]` | 列出全部项目(含 endpoints) | | `api-tool:project-create` | `apiToolApi.createProject()` | `ApiProjectCreatePayload` | `ApiProject` | 新建项目(name/baseUrl/description) | | `api-tool:project-update` | `apiToolApi.updateProject()` | `ApiProjectUpdatePayload` | `ApiProject` | 更新项目元信息 | | `api-tool:project-delete` | `apiToolApi.deleteProject()` | `id: string` | `void` | 删除项目及其全部接口 | | `api-tool:endpoint-create` | `apiToolApi.createEndpoint()` | `ApiEndpointCreatePayload` | `ApiEndpoint` | 项目下新建接口 | | `api-tool:endpoint-update` | `apiToolApi.updateEndpoint()` | `ApiEndpointUpdatePayload` | `ApiEndpoint` | 更新接口(方法/路径/参数/请求体) | | `api-tool:endpoint-delete` | `apiToolApi.deleteEndpoint()` | `ApiEndpointIdPayload` | `void` | 删除单个接口 | | `api-tool:send` | `apiToolApi.send()` | `ApiSendPayload` | `ApiSendResult` | 服务端发起真实 HTTP 请求,返回状态码/响应头/响应体/耗时/大小/原始字节(base64,供响应解码实时切换) | | `api-tool:import-openapi` | `apiToolApi.importOpenApi()` | `ApiImportOpenApiPayload` | `ApiProject` | 读取 OpenAPI 3.x / Swagger 2.0 **JSON** 文件转成项目(仅支持 JSON,YAML 需先转 JSON) | | `api-tool:export-openapi` | `apiToolApi.exportOpenApi()` | `projectId: string` | `ApiExportOpenApiResult` | 项目导出为 OpenAPI 3.0.0 JSON(`{ fileName, json }`,写盘走 `fsApi.saveTextFile`) | `ApiEndpoint`: `{ id, projectId, name, method, path, query: ApiKeyValue[], headers: ApiKeyValue[], bodyType: 'none'|'json'|'form'|'raw', body, createdAt, updatedAt }`,`ApiKeyValue = { key, value, enabled }`。导入兼容 OpenAPI 3.x(servers/requestBody)与 Swagger 2.0(schemes+host+basePath、in:body/in:formData、consumes),含 JSON Schema `$ref` 本地解析(`#/components/schemas`、`#/definitions`,带环检测与深度限制);导出固定 3.0.0,同 path 多 method 合并为一个 pathItem,可被 Postman 直接 Import。 **DatabaseStatusResult**: ```typescript { initialized: boolean schemaVersion: number dbPath: string } ``` --- ## 主题色彩 Token(共享) 定义在 `src/assets/styles/variables.scss`: | Token | 亮色值 | 暗色值 | 用途 | | ----------------------- | ------------------------ | ------------------------ | ------- | | `--color-primary` | `#ffb7c5` | `#ff9bb0` | 樱花粉主色 | | `--color-primary-light` | `#ffd6e0` | `#ffc4d4` | 主色浅 | | `--color-primary-deep` | `#ff8fab` | `#ff7a96` | 主色深 | | `--color-surface` | `rgba(255,255,255,0.65)` | `rgba(56,48,60,0.62)` | 卡片/面板背景 | | `--color-border` | `rgba(255,183,197,0.28)` | `rgba(255,183,197,0.22)` | 边框 | | `--color-text` | `#5a4a52` | `#e8dfe5` | 主文本 | --- ## 构建配置(共享) ### Vite 配置 (vite.config.ts) `defineConfig(({mode}) => ...)` 根据打包/开发版本参数(`--mode`)加载对应配置: | mode | 用途 | 产物/行为 | | --------------- | ----- | ----------------------------------------------------------------------------------- | | 默认 / `electron` | PC 版 | 输出 `dist`(electron-builder 打包);加载 `electronSimple` 插件构建 main/preload | | `web` | Web 版 | 输出 `dist-web`;跳过 electron 插件;相对 base `./`;开发/预览时 `/api` 代理到 `http://localhost:3210` | - `@` 别名指向 `./src`;`@wzo/regex-diagram` 别名指向其 `src/index.ts`(npm 发布缺少 dist 产物,直接解析 TypeScript 源码) - SCSS 预处理器:`api: 'modern-compiler'`,`loadPaths` 包含 `./src` - `electronSimple` 插件:main + preload 双入口构建(仅 PC 模式加载) - `@dvaji/vite-plugin-monaco-editor`:Monaco Editor worker 自动打包与注入(Vite 8 兼容 fork) - 开发模式 sourcemap 开启,生产模式 minify --- ## 开发指南 ### 新增 IPC 通道 1. **定义类型** — `src/types/ipc.ts`:添加 channel 常量 + payload/result 类型 + API 接口方法 2. **主进程 handler** — `electron/main/ipc/handlers/`:实现业务逻辑 3. **Preload 暴露** — `electron/preload/apis/`:通过 `contextBridge` 暴露方法 4. **渲染端包装** — `src/api/`:双栈包装(PC 走 IPC / Web 走 HTTP,参考现有模块结构) 5. **类型声明** — `src/vite-env.d.ts`:补充 `window` 类型声明(如需) 6. **Web 端同步(可选)** — `server/routes/` 添加对应路由(薄适配层:参数校验 + service + `ok()/fail()`)并在 `server/app.ts` 挂载;Electron 专属能力按上文「Web 端能力降级」处理 ### 新增数据库表 1. 在 `electron/main/config/index.ts` 递增 `CURRENT_SCHEMA_VERSION` 2. 在 `electron/main/services/database.ts` 添加 `createXxxTable()` 函数 3. 在 `initDatabase()` 事务中调用新函数 4. 在 `src/types/domain.ts` 定义对应的 TypeScript 接口 5. 在 `electron/main/services/` 添加对应 service 层 ### 新增面板视图 1. 在 `src/views/` 下创建新的 `XxxPanel/index.vue` 2. 在 `src/router/panels.ts` 的 `PANELS` 数组中注册 3. 在 `QyIcon.vue` 的 `EP_ICON_MAP` 或 `ICON_PATHS` 中添加对应图标 --- ## Issue / 踩坑记录 > 本章节汇集项目开发过程中遇到的实际问题、根因分析与解决方案。修改相关模块前请先阅读对应章节。 ### Monaco 编辑器踩坑 > 本项目已有两个 Monaco 封装:`JsonEditor.vue`(挡板数据)和 `ApiCodeBlock.vue`(接口请求工具)。下次改任何一个之前先读这份。 #### 坑 1:`v-show` 父容器导致 editor 初始化拿到 0 尺寸 **现象**:组件挂在 `v-show="tab === 'xxx'"` 的容器里,且默认 tab 不是这个 → `onMounted` 时 `container.offsetHeight === 0` → editor 渲染成一条线。 **教训**: - `automaticLayout: true` 只能处理 resizeObserver 能检测到的尺寸变化,**0 → 非 0 的突变它不一定能捕获** - 必须加 `ResizeObserver` 监听 container,debounce 50ms 后 `editor.layout()` - init 后立即 `setTimeout(() => editor.layout(), 0)` 再兜底一次 #### 坑 2:`position: fixed` 被祖先 `backdrop-filter` 捕获 **现象**:全屏用 `position: fixed; top:0; left:0; width:100vw; height:100vh`,结果只覆盖了面板区域而不是视口。 **根因**:CSS 规范里,`backdrop-filter`(以及 `transform`、`filter` 等)会创建新的 **containing block** → 后代 `position: fixed` 不再相对视口,而是相对这个祖先。`!important` 和非 scoped style 都救不了。 **唯一解法**:**Teleport 到 ``**。覆盖层作为 body 直接子元素,body 没有 backdrop-filter,`position: fixed` 就真的 relative to viewport。 #### 坑 3:全屏切换用 CSS class 搬 DOM 的 Monaco 稳定性 **现象**:用 `setContainer()` 把同一个 editor 在"正常容器"和"全屏容器"之间搬来搬去 → 渲染错乱 / 空白 / layout 失效。 **根因**:Monaco 对 DOM 搬移 + v-if/v-show 切换行为极不稳定,特别是涉及 `setModel` 与内部 layout 的时序。 **解法**:Teleport 方案里用**两个独立 editor** 共享**同一个 model**: - 原 editor(常驻)绑正常区域 - fsEditor(按需创建/销毁)绑 Teleport 全屏容器 - Monaco 原生支持多 editor 绑同一 `ITextModel`,内容双向同步自动工作 #### 坑 4:`height: 100%` 在 flex 链上的解析条件 **现象**:响应体 editor 设 `height="100%"` 但高度为 0;请求体 editor 设 `height="160px"` 却正常。 **根因**:父容器是 `display:flex; flex-direction:column; flex:1` 时,子元素如果没有 `flex:1`,只取内容高度。`height:100%` 无法从未明确赋值的 flex 父得到参考高度。 **法则**: - 需要撑满 flex 列剩余空间的子元素 → `flex:1; min-height:0` - 需要固定高度的子元素 → 显式 `height: 160px`(`flex:1` 存在时会被覆盖) - **两者兼容**:根元素始终写 `flex:1; min-height:0`,让容器靠 `height:100%` 或固定值生效 #### 坑 5:Vue scoped CSS 与全屏样式 **现象**:全屏 CSS 写在 `