Initial panel project deploy skill snapshot

This commit is contained in:
depei
2026-10-10 18:50:57 +08:00
commit 5295c1cf58
11 changed files with 2286 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
# 技能安装与迁移说明
技能名称:**panel-project-deploy**
用途:自动化克隆、初始化、品牌化、Docker 部署开源项目,并强制改造成炫酷科技感玻璃面板风格 UI;
同时对齐《开源软件改造部署外包服务合同》的交付物与验收标准。
## 一、目录内容
```text
panel-project-deploy/
├── SKILL.md # 技能主文件(18 步工作流,必须先读)
├── references/
│ ├── panel-ui.md # 面板风格 UI 设计与验收规范
│ └── delivery-contract.md # 外包合同对齐的交付/验收规范
├── scripts/
│ ├── workflow_manager.py # 工作流状态管理(初始化/恢复/报告)
│ ├── panel_ui_audit.py # 阻断式面板 UI 自动审计
│ ├── deliverables_check.py # 交付物清单核查
│ └── build_delivery_package.py # 交付压缩包构建
├── requirements.txt # Python 第三方依赖
├── install.ps1 / install.sh # 一键安装脚本
└── INSTALL.md # 本文件
```
## 二、环境要求
- Python **3.9+**(推荐 3.11/3.12)
- Node.js 18+、pnpm、Docker / Docker Compose(执行实际项目部署时需要)
- ffmpeg(项目内含视频推流/转码时需要)
- 网络可访问目标 Git 仓库与 npm / pip / apt 源
## 三、安装步骤
### Windows(PowerShell)
```powershell
# 1. 复制整个技能目录到 Codex 技能目录
Copy-Item .\panel-project-deploy "$env:USERPROFILE\.codex\skills\" -Recurse -Force
# 2. 执行安装脚本(创建 venv 可选;安装 playwright)
.\panel-project-deploy\install.ps1
```
### macOS / Linux
```bash
# 1. 复制技能目录
cp -r ./panel-project-deploy ~/.codex/skills/
# 2. 执行安装脚本
bash ./panel-project-deploy/install.sh
```
> 若使用的是其他 Agent 框架,技能目录也可能是 `~/.agents/skills/`,按对应框架要求放置即可。
## 四、安装 Playwright 浏览器
UI 审计脚本需要 Playwright 的 Chromium 内核:
```bash
python -m pip install -r requirements.txt
python -m playwright install chromium
# Linux 如缺系统库,再执行:
python -m playwright install-deps chromium
```
## 五、验证安装
```bash
python ~/.codex/skills/panel-project-deploy/scripts/panel_ui_audit.py --help
python ~/.codex/skills/panel-project-deploy/scripts/workflow_manager.py --help
```
能正常打印帮助即安装成功。
## 六、使用方式
在 Codex 对话中用自然语言触发,例如:
- “初始化并部署项目,使用面板风格 UI <Git 仓库地址>”
- “克隆并改造这个开源项目,Docker 部署”
- “项目页面改版成科技感大屏风格”
Agent 会自动读取 `SKILL.md` 并按 18 步工作流执行,最终必须通过阻断式 UI 审计(0 violations)。
## 七、注意事项
- 技能脚本**不含任何硬编码本机路径**,可跨设备/跨平台直接迁移。
- 是否 `git push`、是否本地提交,严格以用户授权为准;用户要求“不推送代码”时禁止推送。
- 默认账号密码必须来自项目实际初始化逻辑,禁止猜测。
- 合同相关交付规范见 `references/delivery-contract.md`。
+310
View File
@@ -0,0 +1,310 @@
---
name: panel-project-deploy
description: 自动化克隆、初始化、品牌化、Docker 部署并提交项目,同时强制将前端页面改造成炫酷的科技感玻璃面板风格。适用于“初始化并部署项目”“项目页面改版”“面板风格 UI”“科技感大屏/仪表盘”等请求。
---
# 项目初始化部署、面板风格 UI 改造与外包交付
## 目标
在原项目初始化与部署能力基础上,完成从 Git 克隆、项目分析、品牌信息替换、Docker 构建启动、健康验证到 Git 提交的完整流程,并将前端主要页面升级为**现代化、炫酷、科技感的面板风格 UI**。
面板改造是必做项,但不能破坏原有业务逻辑、路由、接口、权限流程和数据结构。
本技能对齐外包交付标准:完成部署、品牌化改版、素材录制归档、标准化 README、7 项交付物与
7 条验收标准,并遵守保密、知识产权归属和售后 SLA。
## 对外材料来源口径规则(甲方可见材料)
凡交付给甲方或其终端客户看到的 README、界面、截图、录屏、交付清单、演示文档、注释、配置及资源文件名,
均不得主动出现其他项目名称、外部仓库链接、原作者信息、致谢、迁移说明、版本对比或“基于某项目改造”等表述。
1. README 与一切对外文档使用当前产品口径,只描述本系统的功能、架构、部署、运维和授权使用方式。
2. 交付包内只放当前产品界面截图,文件名使用页面语义化中文/英文名称,不出现 before/after、改造前、原版等字样。
3. 内部初始启动截图仅用于质量核对,可命名为 `baseline-<页面>.png`,必须与对外交付物隔离,严禁打包给甲方。
4. 清理依法可移除的第三方公司名、商标、LOGO、水印、联系方式、二维码、默认域名、包元数据和注释线索。
5. 法律边界:MIT、Apache-2.0、BSD 等许可证要求随源码保留的版权声明、许可证全文或 NOTICE 文件不得删除、
替换或改写;随源码交付时必须完整保留。不得在 README 中虚假声明全部代码均由交付团队自主原创。
6. 面向甲方的演示包可不放入源码和 LICENSE,仅保留当前产品 README、截图、录屏和必要部署说明;
源码包与演示交付包应分开管理。
7. 本规则约束对外呈现口径,不替代、不免除任何第三方许可证或法律规定的合规义务。
## 20 步工作流
| 步骤 | 名称 | 必做/跳过 | 要求 |
|---:|---|---|---|
| 0 | 克隆项目 | 必做 | 不修改任何业务代码 |
| 1 | 项目完整度评估 | 必做 | 判断前端资源、组件、样式入口、启动方式是否完整 |
| 2 | 项目整体分析 | 必做 | 识别框架、路由、主题色、布局、启动命令与端口策略 |
| 3 | 初始版本基线启动(内部) | 必做 | 改代码前用项目现有方式跑通初始版本;仅内部核对,不产生交付物 |
| 4 | 初始界面基线截图(内部) | 必做 | 代码零改动时采集初始界面用于内部对比;严禁进入交付包 |
| 5 | Docker 配置生成 | Docker 项目必做,非 Docker 跳过 | 不改变前端视觉 |
| 6 | 页面信息与品牌名称修改 | 必做 | 建立全局面板设计变量和玻璃面板基础样式 |
| 7 | Logo 生成与替换 | 必做 | 新 Logo 适配深色面板、发光边框和 Favicon |
| 8 | 版权与第三方标识规范 | 必做 | 清理依法可移除的来源和商业标识;源码许可证要求保留的版权/许可/NOTICE 不得删除 |
| 9 | 页脚、导航与社区链接 | 必做 | 导航、页脚改成半透明悬浮面板风格;页脚放「开源社区 https://www.gitcc.com/」和「GCC AI Coding https://www.gitvv.com/」超链接 |
| 10 | 登录页面 | 必做 | 改成居中玻璃面板、发光输入框、动态背景 |
| 11 | AI 对话功能 | 必做 | 对话窗口、设置抽屉、消息气泡面板风格;按用户指定服务商预填 Base URL 与默认模型,并内置图文教程 |
| 12 | 示例数据 | Docker 项目必做,非 Docker 跳过 | 数据卡片和列表面板承载 |
| 13 | CORS 配置检查 | Docker 项目必做,非 Docker 跳过 | 不改变 UI |
| 14 | 端口检查与分配 | Docker 项目必做,非 Docker 跳过 | 不改变 UI |
| 15 | **改造后构建与启动** | 必做 | Docker 项目构建镜像启动容器;非 Docker 项目手动构建并启动改造版 |
| 16 | 健康验证 | 必做 | 服务健康、面板审计 0 违规、全功能走查无报错 |
| 17 | 产品截图与全功能录屏 | 必做 | 采集当前产品全套高清界面截图(中性文件名)+ 全功能演示录屏;无 before/after |
| 18 | 标准化 README 与版本归档 | 必做 | 按当前产品口径编写 README;不主动出现来源/致谢/版本对比;按授权决定是否提交 |
| 19 | 交付物核查 | 必做 | 运行交付物阻断式核查,7 项交付物齐备并生成交付 manifest |
非 Docker 项目跳过步骤 5、12–14,其余步骤仍有效;步骤 3–4 产物仅内部留存,不对外交付。
## 素材采集时序(内部核对 + 对外素材)
内部基线素材用于核对改造完整性,对外素材只呈现最终产品:
1. **内部基线**:先启动初始版本(步骤 3),代码零改动时截取 `baseline-<页面>.png`(步骤 4),再开始修改代码。
- 克隆完成后,先用项目自身现有方式(Dockerfile / README 命令 / 本地脚本)确认初始版本可访问;
- 初始版本无法启动时,才允许使用仓库中的历史截图作为内部核对依据;
- 基线素材保存到内部目录,任何情况下都不进入交付包。
2. **对外截图与录屏**必须在改造完成、重新构建部署并通过健康验证(步骤 16)后采集(步骤 17),禁止使用半成品。
3. 对外界面截图必须页面全覆盖,使用 `login.png`、`overview.png`、`campus-state.png` 等中性页面名称。
4. 全功能录屏应完整展示登录、概览大屏、业务页面、AI 配置、系统管理、表格/弹窗交互等核心流程;
画面无卡顿、无黑屏、无报错,录屏界面必须与最终截图一致。
5. 素材目录、采集时间、对应版本(git commit 或“未提交工作区”)记录在内部 manifest;对外清单只列甲方需要的信息。
## 执行原则
1. **先识别,再改造**:确认前端框架、样式方案、路由入口、布局组件、主题色和可复用组件。
2. **小步修改**:优先修改全局样式、布局组件、导航、页脚、登录页、首页/仪表盘和 AI 页面。
3. **保持功能稳定**:不改后端协议、数据库结构、权限模型和业务路由语义。
4. **适配不同技术栈**:React、Vue、Angular、原生 HTML、Tailwind、CSS Modules、Less、SCSS、Ant Design、Element Plus 均可实现,不强制引入新的重型 UI 框架。
5. **响应式和无障碍**:面板效果必须在移动端可用,文本对比度达标,键盘焦点可见。
6. **动效克制**:允许发光、渐变、悬浮、过渡和背景动画,但必须尊重 `prefers-reduced-motion`。
7. **对外材料中性化**:对外材料不主动出现其他项目名称、链接、原作者、致谢或版本对比;源码许可证要求保留的内容必须保留。
8. **痕迹清理范围**:在合法合规前提下清除第三方商业标识和来源宣传线索;内部留档不进入交付物,法定版权与许可声明不得删除。
9. **保密与成果归属**:项目需求、交付资料、客户信息不得外泄;交付成果不得私自转发、售卖、复用或二次分发。
10. **交付留痕**:截图、录屏、审计报告、交付清单必须完整可溯源,禁止把未走查/未测试的功能描述为“已完成”。
## 主题一致性强约束
1. 面板风格 UI 的第一优先级是**全组件主题统一和文字可读**,第二优先级才是发光、模糊和动画。
2. 项目使用组件库时,必须从官方主题入口配置暗色 Token:
- Ant Design:`ConfigProvider` + `theme.darkAlgorithm`,在 Umi 项目中通过 `antd.configProvider.theme` 配置。
- Element Plus / ElementUI:使用官方 dark CSS variables 或项目的暗色主题变量。
- MUI:`createTheme` + `CssVarsProvider/ThemeProvider`。
- Vuetify / Angular Material:启用对应 dark theme。
3. CSS 覆盖只能作为玻璃质感和局部修正,不能替代组件库官方暗色主题。
4. 所有浮层都要检查:Modal、Drawer、Select Dropdown、DatePicker、Popover、Tooltip、Upload、Cascader、AutoComplete、ColorPicker。
5. 所有内容容器都要检查:Card、Table、Descriptions、Tabs、Collapse、List、Form、Pagination、Empty、Result、Spin 容器。
6. 如果某个业务页面必须保留浅色区域,必须同时使用浅色文字风格和浅色周边布局;禁止把浅色组件直接嵌入深色页面造成黑白割裂。
## 项目概述大屏强制要求
每个项目**必须**新增一个炫酷的「项目概述大屏」页面(如路由 `/overview`),导航菜单顶部入口名称为「概览」。要求:
1. **接入真实可展示数据**:通过后端聚合接口输出,至少覆盖
- 核心指标卡片:设备/摄像头总数、在线数与在线率、离线数、报警数(待处理/已处理)、规则数、用户数等(按项目实际实体取数,禁止纯静态假数据);
- 趋势图:近 7 天(或可配置周期)业务/报警趋势;
- 分布图:设备/业务状态结构(在线/报警/离线等);
- 最新动态:最新事件实时滚动列表;
- 实时时钟,数据定时(建议 10 秒)自动刷新。
2. **视觉炫酷、符合面板体系**:深色科技基底 + 玻璃面板 + 霓虹边缘 + 大标题光效;图表与卡片风格统一,支持大屏分辨率投放与响应式降级。
3. **图表实现适配项目**:优先复用项目已有图表库;未引入时可用 SVG/CSS 自绘(折线、环形等),
不强制为单个大屏新增重型依赖。
4. **可访问性**:图表需配文字图例与数值,动态滚动可暂停,文字对比度通过审计。
5. 该页面纳入改造后截图、录屏与 UI 审计范围。
## 社区与外部链接要求
1. 页脚必须包含两个可点击超链接(新标签页打开):
- 开源社区:[https://www.gitcc.com/](https://www.gitcc.com/)
- GCC AI Coding:[https://www.gitvv.com/](https://www.gitvv.com/)
2. README 需在显著位置(如「相关链接」章节)提供上述两个链接;概览大屏页面头部同样要展示这两个超链接。
3. 链接文案与样式符合面板主题,hover 有发光反馈;不得写成失效或错误地址。
## AI 对话对接强制要求
步骤 9 除面板视觉外,当用户指定 AI 服务商(如 TokenFree、官方 OpenAI、第三方中转等)时,必须完成:
1. **默认配置预填**:在 AI 设置的默认值中写入用户指定的 Base URL 与默认模型,而不是保留模板里的旧服务商。
- OpenAI 兼容网关的地址必须带 `/v1`(如 `https://www.tokenfree.com/v1`)。
- 模型 ID 必须以服务商实际支持的字符串为准(可通过其定价/模型列表页核实,例如 TokenFree 上为 `kimi-k2.6`),禁止凭名称臆造。
2. **本地缓存版本迁移(强制)**:设置存储 key 必须带版本后缀(如 `parking-ai-settings-v2`)。
- 切换服务商时升级版本,避免老用户 localStorage 中的旧 Base URL / 旧模型覆盖新默认值。
- 如需迁移旧数据,只迁移用户自填的 API Key,地址与模型强制使用新默认值。
3. **内置使用教程(强制)**:设置抽屉/弹窗内必须包含与面板主题一致的教程块,内容至少覆盖:
- 服务商官网链接(新标签页打开)与一句话说明;
- 注册 / 登录 / 邮箱验证;
- 在控制台创建并复制 API Key(说明 Key 前缀,如 `sk-`);
- 粘贴 Key → 测试连接 → 保存的完整操作路径;
- 明确展示 Base URL、默认模型,并说明 Key 仅保存在本地浏览器;
- 说明可替换为该服务商支持的其他模型。
4. **README 同步**:项目 README 必须增加对应「AI 对话使用教程」章节(含配置表与步骤)。
5. **协议兼容**:调用遵循 OpenAI 兼容协议(`POST {baseUrl}/chat/completions` 流式、`GET {baseUrl}/models` 测试连接);未拿到用户 Key 时不得伪造 Key,也不得把测试失败谎报为成功。
6. **教程 UI 主题**:教程中的列表、行内代码、链接、注释文字必须使用深色玻璃面板配色,禁止出现白底黑字的教程块嵌入深色抽屉。
## 页面信息修改的强制要求
步骤 4 不能只替换文字,必须同时完成以下内容:
1. 替换浏览器标题、导航品牌名、页脚系统名、README 中面向用户的新项目名称。
2. 建立统一设计变量:背景色、主色、辅助光效色、面板背景、边框、模糊度、圆角、阴影和间距。
3. 至少为这些区域应用面板风格:
- 顶部导航 / 侧边栏
- 首页或仪表盘
- 主要数据卡片
- 表格、表单、弹窗或抽屉
- 登录页
- AI 对话页
- 页脚
4. 主要页面应呈现“深色科技基底 + 半透明玻璃面板 + 霓虹边缘光 + 渐变光斑/网格背景”的视觉方向;如果原项目必须保持浅色主题,则使用浅色玻璃拟态和柔和外发光。
5. 面板不能只靠边框区分,应组合使用:半透明背景、模糊、内外发光、渐变边缘、角标或装饰线、悬浮位移和状态光效。
6. 所有新增页面和核心组件要保持统一,不允许登录页科技感很强而业务页面仍是完全旧风格。
详细样式规范、组件结构、CSS/Tailwind 示例和验收清单见 [references/panel-ui.md](references/panel-ui.md)。执行页面改造前必须读取该文件。
## 阻断式视觉验收(必须执行)
页面改造完成后,不能只凭肉眼抽查或截图判断;必须在应用启动后执行技能内置脚本:
```bash
python scripts/panel_ui_audit.py \
--base-url http://localhost:<端口> \
--hash-routes \
--username <账号> \
--password <密码> \
--submit-text <登录按钮文本> \
--routes /页面1,/页面2,/页面3 \
--open-selector '<抽屉或弹窗触发选择器>' \
--wait-until domcontentloaded \
--out .Codex/reports/panel-ui-audit.json
```
非 hash 路由去掉 `--hash-routes`;有多个抽屉、弹窗时重复传入 `--open-selector`。含轮询、WebSocket、SSE 或定时刷新的页面使用 `--wait-until domcontentloaded`,不要等待永远无法稳定的 `networkidle`。
硬性规则:
1. 健康验证步骤必须运行该脚本,进程退出码为 0 且报告为 0 violations 才算通过。
2. 审计范围必须覆盖登录页、首页/仪表盘、表格/列表页、表单页、详情页、AI 页面、系统管理页,以及所有 Modal、Drawer、Select、DatePicker、Popover、Tooltip 等浮层。
3. 脚本同时检查两类 P0 问题:文字/背景对比度低于 WCAG 阈值,以及浅色组件嵌入深色外壳造成“外黑内白”。
4. 地图、视频、画布等媒体区域可排除媒体内容本身,但悬浮状态、标签、操作按钮、提示文字和空状态仍必须通过审计。
5. 若审计报出问题,先修复主题 Token 和组件样式,再重新构建、部署和审计;不能通过忽略脚本、删除选择器或降低阈值的方式完成任务。
6. 用户明确要求“不推送代码”时禁止任何 `git push`;本地提交仍需遵守用户授权。若任务是修复/验收且用户未要求提交,则保留工作区变更并明确说明。
7. 默认账号密码必须来自实际初始化逻辑,不能猜测;最终回复和 `.Codex/reports/` 验收材料中要写明账号来源。
## 外包交付强制规范(合同对齐)
执行外包交付类任务前必须读取 [references/delivery-contract.md](references/delivery-contract.md)。核心约束:
1. **六项服务全覆盖**:部署运行、界面改造、版权痕迹清理、素材录制归档、标准化文档、售后支撑,缺一不可。
2. **痕迹清理与开源合规的边界(红线)**:
- 清除第三方公司名、LOGO、商标、水印、弹窗、页脚、版权注释中的商业归属与联系方式;
- LICENSE 重写为权属方中性版权声明,原作者、原许可证全文与原项目链接不出现在交付物中;
- 清理后用 `grep -rniE` 对原标识、原作者、原链接等关键词全文检索,交付物中不得命中。
3. **素材归档(步骤 17)**:
- 对外只提供当前产品的高清界面截图,页面全覆盖、文件名中性,不出现 before/after;
- 全功能逐一操作演示录屏,无卡顿黑屏,完整演示所有功能;
- 素材分类整理,上传甲方指定网盘(默认百度网盘),链接写入交付清单,保证长期有效、可访问下载。
4. **标准化 README(步骤 18,12 项必备)**:一句话简介、功能描述(逐条配图)、行业痛点、技术优势、
应用场景、目标行业人群、商业价值、技术架构(含拓扑图)、环境依赖、快速开始、完整部署教程
(Docker 优先,无 Docker 提供可复现的手动步骤)、中性的版权与使用说明。
5. **交付物 7 项**:源码、当前产品界面截图、全功能录屏、网盘归档链接、标准化 README、
Docker 部署脚本(或不适配的书面说明)、环境依赖清单与部署注意事项。
6. **验收 7 标准**:运行稳定、界面差异化、痕迹清零、素材完整、文档达标、部署可复现、合规无侵权;全部达标方可交付。
7. **整改与售后**:验收问题 24 小时内免费整改并重新交付;两次整改不达标甲方可拒收;
交付后终身免费基础质保、24 小时内响应。
8. **保密与知识产权**:交付成果使用权/商用权归甲方,乙方不得自用/售卖/转发/二次分发;
商业信息与客户资源永久保密。
### 构建交付包与交付清单(步骤 18)
步骤 18 使用内置脚本构建交付压缩包,并把交付清单作为与 zip 并列的单独文件输出:
```bash
python scripts/build_delivery_package.py --project-dir <项目目录>
```
脚本保证:
- README 只在 zip 包根一份,「文档」目录不再放 README;
- README 引用图片自动随包拷入并保持相对路径,解压可显示(脚本内置解压校验);
- 交付清单《交付清单.md》输出在 zip 之外、与之并列,不打入包内;
- 包内无内部执行工具痕迹。
### 交付物阻断式核查(步骤 19)
```bash
python scripts/deliverables_check.py \
--project-dir <项目目录> \
--trace-keywords "原公司名,原品牌,原域名" \
--netdisk-link "<网盘归档链接>"
# 项目确实不适配 Docker 时加 --allow-no-docker,
# 并在交付目录给出书面说明
```
退出码为 0 才算交付物齐备。
禁止伪造截图/录屏/网盘链接,禁止把 README 写成与实际不符的宣传稿。
## 状态管理
状态统一放在目标项目的 `.Codex` 目录:
```text
<项目目录>/.Codex/
├── workflow_state.json # 步骤状态
├── error_log.md # 错误日志
├── reports/ # 审计与核查报告
└── delivery/ # 交付材料
├── manifest.md # 交付清单(内部工作文件,不打入交付包)
├── assets/ # 内部基线截图 + 产品截图、全功能录屏
└── no-docker说明.md # 仅不适配 Docker 时需要
```
状态流转:
```text
⏭️ 未开始 → ⏸️ 进行中 → ✅ 已完成
↓
❌ 失败,可重试
```
恢复执行时读取 `workflow_state.json`,跳过已完成步骤,从第一个进行中、失败或未开始的步骤继续。
可使用本技能自带脚本:
```bash
python scripts/workflow_manager.py init <project_dir> "<项目名称>" "<git地址>" "[端口范围]"
python scripts/workflow_manager.py status <project_dir>
python scripts/workflow_manager.py resume <project_dir>
python scripts/workflow_manager.py report <project_dir>
```
## 技术栈检测
- Java Spring:`pom.xml`、`build.gradle`
- Node.js Express:`package.json`、`app.js`、`server.js`
- Node.js Nest:`nest-cli.json`、`@nestjs`
- Python Django:`manage.py`、`django`
- Python FastAPI:`fastapi`、`main.py`
- Go Gin / Fiber:`go.mod`、`gin` / `fiber`
- React:`react`、`App.jsx`、`App.tsx`
- Vue:`vue`、`App.vue`
- Angular:`angular.json`、`@angular`
## 完成标准
- 项目可正常安装、构建和启动。
- Docker 项目的容器健康检查通过。
- 新旧项目名称替换准确,未错误修改 lock 文件、依赖名或后端配置中的技术标识。
- Logo、Favicon、浏览器标题、导航和页脚显示新品牌。
- 主要页面具备统一的炫酷面板风格。
- 已新增「项目概述大屏」页面,接入真实聚合数据(指标卡/趋势/分布/最新动态/实时时钟)并在导航提供入口。
- 页脚、概览页与 README 均包含开源社区 gitcc.com 与 GCC AI Coding gitvv.com 的可点击超链接。
- 阻断式面板 UI 审计退出码为 0,10 个核心页面/路由无对比度和“外黑内白”违规。
- Modal、Drawer、Select、DatePicker、Popover、Tooltip 和 Alert/Tag 等组件均通过暗色主题与对比度检查。
- 移动端布局正常,动效不会导致明显卡顿。
- AI 对话功能可用,支持 OpenAI 兼容接口和流式输出;默认服务商、Base URL、默认模型与用户要求一致,内置使用教程,且本地配置 key 已做版本迁移。
- 甲方可见材料无外部项目链接、原作者、致谢和版本对比;源码许可证/NOTICE 已按法律要求完整保留。
- 商业痕迹与来源痕迹全部清理,grep 复查交付物中无残留。
- 内部基线截图只用于核对、不随包交付;对外界面截图与录屏采集于最终构建并健康通过的版本,页面全覆盖、录屏完整,归档链接长期有效、可访问下载。
- 标准化 README 覆盖全部必备内容,以当前产品口径编写,图文结合、架构图准确、部署教程可落地,且不作虚假权属表述。
- 7 项交付物齐备,`.Codex/delivery/manifest.md` 完整;`deliverables_check.py` 退出码为 0。
- 保密与知识产权要求已遵守:成果不私用/不外传,交付材料与验收记录可溯源。
- 最终 Git 提交信息描述具体业务变化,不出现“工作流”。
+10
View File
@@ -0,0 +1,10 @@
# panel-project-deploy 技能安装脚本 (Windows)
$ErrorActionPreference = "Stop"
Write-Host "==> 安装 Python 依赖 (playwright)" -ForegroundColor Cyan
python -m pip install --upgrade pip
python -m pip install -r "$PSScriptRoot\requirements.txt"
Write-Host "==> 安装 Playwright Chromium 内核" -ForegroundColor Cyan
python -m playwright install chromium
Write-Host "==> 安装完成。" -ForegroundColor Green
Write-Host "技能目录: $PSScriptRoot"
Write-Host "如需迁移到 Codex 技能目录,请将本文件夹复制到 `$env:USERPROFILE\.codex\skills\"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# panel-project-deploy 技能安装脚本 (macOS / Linux)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "==> 安装 Python 依赖 (playwright)"
python3 -m pip install --upgrade pip
python3 -m pip install -r "$SCRIPT_DIR/requirements.txt"
echo "==> 安装 Playwright Chromium 内核"
python3 -m playwright install chromium
echo "==> 安装完成。"
echo "技能目录: $SCRIPT_DIR"
echo "如需迁移,请将本文件夹复制到 ~/.codex/skills/"
+149
View File
@@ -0,0 +1,149 @@
# 软件改造部署外包交付规范
> 本文件是 `SKILL.md` 的强制配套规范。凡触发“初始化并部署项目”“面板风格 UI 改造”“项目外包交付”等请求,交付前必须逐条对照。
## 0. 三条红线
1. **甲方可见材料中性化**:README、界面、截图、录屏、交付清单、演示文档、注释、配置和资源文件名,不主动出现其他项目名称、外部仓库链接、原作者、致谢、迁移说明、改造前界面或版本对比。
2. **许可证合规边界**:MIT、Apache-2.0、BSD 等许可证要求随源码保留的版权声明、许可证全文或 NOTICE 文件不得删除、替换或改写;不得把第三方代码虚假表述为全部由交付团队原创。
3. **稳定交付**:部署可复现、功能可验证、截图录屏与最终版本一致、文档可落地;不得为了清理外观线索而破坏业务功能。
演示交付包可以不放入源码和 LICENSE;源码包必须按所适用许可证保留版权、许可和 NOTICE 材料。
## 1. 核心服务内容(6 项,必须全部完成)
| # | 服务 | 执行要点 |
|---:|---|---|
| 1 | 项目部署运行 | 完整获取源码、搭建环境、编译配置调试;核心功能正常,无报错、闪退、功能缺失 |
| 2 | 界面定制改造 | UI 风格、色彩、布局、导航和数据展示全面优化;需要大屏的项目必须建设概览大屏 |
| 3 | 标识规范与许可证核对 | 清理依法可移除的公司名、LOGO、商标、专属标识、联系方式和来源宣传线索;核对源码 LICENSE/NOTICE 完整性 |
| 4 | 素材录制归档 | 内部基线仅用于核对;对外只提供最终产品的全页面截图与全功能录屏,分类上传约定网盘 |
| 5 | 文档编制优化 | 按当前产品口径编写可直接交付的标准化 README(见第 3 节) |
| 6 | 售后答疑支撑 | 交付后配合部署、使用、报错排查等答疑;基础质保、响应时限以合同约定为准 |
### 1.1 素材采集时序
```text
获取项目(0) → 完整度/项目分析(1-2)
→ 初始版本启动(3,内部) → 初始界面截图(4,仅内部)
→ [此后才允许改代码] 品牌/界面/AI/标识规范(6-11)
→ 产品构建启动(15) → 健康验证(16)
→ 产品界面截图 + 全功能录屏(17)
→ README/版本归档(18) → 交付核查(19)
```
- 内部基线截图必须是代码零改动状态下的真实画面,仅用于内部核对,任何情况下不进入交付物。
- 对外截图与录屏必须来自最终构建部署并健康通过的版本,禁止使用半成品。
- 对外截图页面全覆盖、文件名中性,不出现 before/after、改造前、原版等字样。
## 2. 标识规范检查清单
必须扫描并处理以下位置:
- 前端:可见公司名/品牌名、Logo、Favicon、水印、关于弹窗、页脚、登录页、浏览器标题。
- 代码注释:文件头中的公司名称、作者 ID、外部链接与联系方式。
- 配置与元数据:清单文件的 `author`、`repository`、`homepage` 字段、Docker 镜像/容器名、环境变量中的专属域名、消息模板品牌签名。
- 文档:README 及 docs 内的公司宣传内容、二维码、购买链接、商务联系方式、主动来源说明和致谢表述。
- 许可证:源码包中的 LICENSE/NOTICE 必须核对并完整保留;不得删除法律要求保留的许可文本。
- 资源文件:图片、视频、音频中嵌入的水印、商标和第三方商业标识。
验收前必须做全文检索(关键词按实际项目扩充):
```bash
grep -rniE "原公司名|原域名|原品牌|原作者ID|原仓库|致谢|qq群|微信|商务合作|before|after|改造前|原版" . \
--include="*.{ts,tsx,js,jsx,vue,java,py,go,json,md,html,yml,yaml,less,css,scss}"
```
甲方可见交付物中不得命中主动来源宣传、商业标识或版本对比关键词;源码许可证文件按许可证要求单独保留。
## 3. 标准化 README 必备内容(12 项,缺一不可)
1. 一句话项目简介。
2. 详细功能描述(逐条功能对应配套截图)。
3. 解决的行业痛点。
4. 核心技术优势。
5. 具体应用场景。
6. 适配目标行业与人群。
7. 商业应用价值。
8. 技术架构说明(含架构图或拓扑图)。
9. 环境前置依赖要求。
10. 详细快速开始指南。
11. 完整安装部署教程(优先 Docker 一键部署;不适配 Docker 的提供可复现的手动步骤)。
12. 版权与使用说明(当前产品口径,不作虚假权属表述)。
要求:图文结合、结构完整、内容详实、与项目实际一致;禁止出现未实现功能、虚构命令、无法访问链接、主动来源宣传及致谢表述。
## 4. 完整交付物清单(7 项,缺一即不合格)
1. 可稳定运行的完整项目源码。
2. 当前产品全页面高清界面截图(无初始界面对比、无 before/after)。
3. 项目全功能操作演示录屏(无卡顿、无黑屏,完整展示核心功能)。
4. 分类整理完毕的素材归档链接(长期有效,可正常访问下载)。
5. 标准化完整版 README(含架构图、部署教程、场景说明)。
6. Docker 一键部署脚本/部署包(不适配的提供书面说明)。
7. 项目环境依赖清单、部署注意事项说明文档。
### 4.1 交付包结构与交付清单(强制)
交付物分两部分:**一个给甲方/终端使用的交付压缩包** + **一份单独的交付清单**。
交付压缩包内部结构:
```text
README.md # 必须在包根目录
docs/screenshots/ # README 引用的全部配图,相对路径完整,解压即可显示
截图/界面截图/ # 当前产品全页面截图(中性命名)
录屏/ # 全功能演示视频
文档/环境依赖清单.md
文档/部署注意事项.md
```
硬性规则:
1. README 只在包根出现一份;“文档”目录不得再放 README。
2. 打包必须把 README 引用图片一并拷入并保持相对路径;交付前解压验证图片可显示。
3. 《交付清单.md》不打入 zip,作为与 zip 并列的单独文件提供。
4. 包内不出现内部工具、执行过程、外部项目线索及版本对比信息,命名使用甲方可读的中性名称。
5. 交付清单列出每项交付物的包内路径/链接、默认账号、验收记录与售后说明。
## 5. 验收标准(7 条,全部达标方可验收)
1. **运行**:正常启动、稳定运行;所有功能可正常操作,无报错、失效、页面错乱或接口异常。
2. **界面**:布局、风格、导航全面优化,概览大屏美观、数据正常、适配主流分辨率。
3. **标识**:甲方可见页面、注释、配置和文档无第三方商业标识、主动来源宣传及版本对比。
4. **素材**:产品截图清晰、页面全覆盖;录屏流畅、完整演示核心功能;归档分类规整可下载。
5. **文档**:README 覆盖第 3 节全部模块,图文结合、架构图准确、教程可落地、版权说明不作虚假表述。
6. **部署**:Docker 一键部署无故障;不支持 Docker 的手动步骤可复现。
7. **合规**:不侵犯第三方知识产权,源码许可证和 NOTICE 材料按要求保留。
## 6. 验收与整改流程
1. 提交完整交付物、归档链接与文档资料。
2. 甲方按合同约定完成核验。
3. 合格后进入验收/结算流程。
4. 不合格时列明问题,在约定时限内整改并重新交付;每次整改必须更新审计和核查记录,形成闭环。
## 7. 售后与响应 SLA
- 质保范围、响应时限和质保期以合同正文为准。
- 通常包括:功能缺陷、部署问题、文档答疑、使用指导和基础技术咨询。
- 新增功能、深度定制、第三方平台变更或超出原需求的工作,可另行协商。
## 8. 保密与知识产权
- 交付成果的使用权、商用权及知识产权归属以合同和授权文件为准。
- 不得未经授权私自转发、售卖、复用、转让或二次分发甲方成果。
- 对商业信息、项目需求、交付资料、客户资源和技术方案承担保密义务。
## 9. 交付前自检顺序(强制)
```text
1. 全功能走查 → 无报错、无功能缺失
2. panel_ui_audit.py → 0 violations
3. 标识/版本对比检索 → 甲方可见材料无残留
4. README 12 项对照 → 图文齐全、链接可达、无主动来源表述
5. 截图/录屏覆盖度核对 → 页面全覆盖、功能全演示
6. deliverables_check.py → 7 项交付物齐备
```
第 6 步脚本退出码非 0 时,禁止声明“已交付”。
+395
View File
@@ -0,0 +1,395 @@
# 炫酷科技面板风格 UI 规范
## 0. 主题根源统一(必须先做)
在添加任何玻璃效果之前,先让组件库自身输出完整暗色主题。
### Ant Design / Umi
必须通过官方 Token 配置暗色算法:
```ts
import { theme } from 'antd';
const darkTheme = {
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: '#22d3ee',
colorTextBase: '#e6f7ff',
colorBgBase: '#07142e',
borderRadius: 14,
},
components: {
Card: { colorBgContainer: 'rgba(7, 18, 39, 0.84)' },
Table: { colorBgContainer: 'rgba(2, 8, 23, 0.2)' },
Modal: { colorBgContainer: '#081428' },
Drawer: { colorBgElevated: '#081428' },
},
};
```
Umi 项目放入:
```ts
export default defineConfig({
antd: {
configProvider: {
theme: darkTheme,
},
},
});
```
### 必须消除的问题
- 页面或 Layout 外层是黑色,Card/Table/Form 内部仍是默认白色。
- 组件库默认黑色文字落在白色小卡片里,而小卡片又嵌在黑色页面中。
- Modal、Drawer、Select、DatePicker 弹出后仍是浅色。
- 表格分页、Tabs、Empty、Descriptions 没继承暗色。
- 只通过 `color` 或 `background` 做零散补丁,没有统一 Token。
### 最低检查范围
每个含前端交互的项目至少逐页打开:登录页、首页/仪表盘、列表/表格页、表单页、详情/描述页、弹窗或抽屉、AI 页面、系统管理页。
## 1. 视觉方向
默认采用深色未来科技风:
- 深色或蓝黑色背景。
- 大面积低透明度玻璃面板。
- 青色、蓝色、紫色等霓虹渐变光效。
- 细微网格、点阵、光斑或扫描线。
- 面板边缘有高亮描边、角标或发光装饰。
- 卡片悬浮时轻微上浮并增强外发光。
如果项目明确要求浅色或商务风格,不强制深色;改用浅色玻璃拟态、浅蓝/紫渐变、柔和阴影和细发光边框。
## 2. 推荐设计 Token
### CSS 变量
```css
:root {
--panel-bg: rgba(8, 18, 38, 0.68);
--panel-bg-strong: rgba(10, 22, 48, 0.86);
--panel-border: rgba(103, 232, 249, 0.24);
--panel-border-strong: rgba(34, 211, 238, 0.72);
--panel-radius: 18px;
--panel-blur: 18px;
--panel-shadow: 0 18px 50px rgba(2, 8, 23, 0.45);
--glow-cyan: #22d3ee;
--glow-blue: #3b82f6;
--glow-violet: #8b5cf6;
--text-primary: #e6f7ff;
--text-secondary: rgba(226, 242, 255, 0.68);
--grid-line: rgba(56, 189, 248, 0.08);
}
```
可根据项目主色调整,但必须保持统一,不应每个页面使用完全不同的发光色。
## 3. 全局背景
页面底层建议加入固定背景层:
```css
.app-shell {
min-height: 100vh;
color: var(--text-primary);
background:
radial-gradient(circle at 15% 10%, rgba(34, 211, 238, 0.22), transparent 32%),
radial-gradient(circle at 85% 0%, rgba(139, 92, 246, 0.20), transparent 30%),
linear-gradient(135deg, #020617, #081120 48%, #0f172a);
}
.app-shell::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 36px 36px;
mask-image: linear-gradient(to bottom, black, transparent 85%);
}
```
背景只做氛围,不允许降低正文对比度。
## 4. 通用玻璃面板
```css
.glass-panel {
position: relative;
padding: 20px;
border: 1px solid var(--panel-border);
border-radius: var(--panel-radius);
background: linear-gradient(
145deg,
rgba(255, 255, 255, 0.08),
var(--panel-bg) 38%,
rgba(255, 255, 255, 0.03)
);
box-shadow: var(--panel-shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
backdrop-filter: blur(var(--panel-blur));
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
overflow: hidden;
}
.glass-panel:hover {
transform: translateY(-3px);
border-color: var(--panel-border-strong);
box-shadow: 0 22px 60px rgba(2, 8, 23, 0.52), 0 0 28px rgba(34, 211, 238, 0.16);
}
.glass-panel::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(120deg, rgba(34, 211, 238, 0.12), transparent 32%);
}
```
### 面板标题栏
```css
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(125, 211, 252, 0.16);
}
.panel-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 16px;
letter-spacing: 0.04em;
}
.panel-title::before {
content: "";
width: 4px;
height: 18px;
border-radius: 999px;
background: linear-gradient(180deg, var(--glow-cyan), var(--glow-violet));
box-shadow: 0 0 12px rgba(34, 211, 238, 0.8);
}
```
### 角标装饰
可在关键面板四角加入科技角标:
```css
.glass-panel.is-featured::after {
content: "";
position: absolute;
right: 14px;
top: 14px;
width: 22px;
height: 22px;
border-top: 1px solid var(--glow-cyan);
border-right: 1px solid var(--glow-cyan);
opacity: 0.8;
}
```
角标属于装饰元素,不应影响点击和内容阅读。
## 5. Tailwind 思路
若项目使用 Tailwind,可抽出可复用类:
```css
@layer components {
.panel-card {
@apply relative rounded-2xl border border-cyan-300/20 bg-slate-950/55 p-5 shadow-2xl backdrop-blur-xl transition duration-300;
}
.panel-card:hover {
@apply -translate-y-1 border-cyan-300/70 shadow-cyan-400/10;
}
}
```
不要为了视觉效果随意升级 Tailwind 大版本或重构整个构建链。
## 6. 页面与组件要求
### 导航栏
- 固定或吸顶时使用半透明深色玻璃背景。
- 当前菜单加发光下划线或胶囊背景。
- Logo 和系统名称放在左侧,操作区放在右侧。
- 移动端抽屉菜单也使用面板风格。
- 焦点状态清晰可见。
### 首页 / 仪表盘
至少包含:
- 欢迎信息或系统状态横幅。
- 3–5 个指标卡片。
- 图表、列表、表格或最近动态区域。
- 空状态和加载状态也要有面板骨架。
指标卡片结构:
```text
图标/状态点 + 指标名称
主要数值 / 状态
趋势描述 / 小型进度条
```
数值可以使用等宽或带轻微发光效果,但不能过度闪烁。
### 项目概述大屏(每个项目必做)
- 独立炫酷大屏页面(如 `/overview`),导航菜单顶部入口名为「概览」;深色科技基底 + 玻璃面板 + 霓虹光效。
- 必须接入后端真实聚合数据,禁止纯静态假数据:
- 5 个左右核心指标卡:设备/摄像头总数、在线数与在线率、离线数、报警数(待处理/已处理)、规则数、用户数(按项目实体取数);
- 近 7 天趋势折线/面积图;状态结构环形图(带文字图例与数值);最新事件滚动列表;实时时钟;数据约 10 秒自动刷新。
- 图表优先复用项目已有库,未引入可用 SVG/CSS 自绘,不为此强加重型依赖。
- 动态滚动可暂停,图表配色与整体统一,支持大屏分辨率与响应式降级。
- 页脚与概览页头部固定提供两个超链接:开源社区 `https://www.gitcc.com/`、GCC AI Coding `https://www.gitvv.com/`,hover 发光。
### 数据表格
- 表格外层使用面板包裹。
- 表头使用半透明高亮背景。
- 行 hover 时出现微弱青色背景。
- 操作按钮使用小型发光按钮或文字按钮。
- 分页器与面板风格一致。
### 表单与登录页
- 登录卡片居中,最大宽度约 420px。
- 输入框使用暗色半透明底、聚焦发光边框。
- 主按钮使用蓝青/蓝紫渐变和外发光。
- 可加入缓慢漂浮的光斑、粒子或圆环背景。
- 错误提示使用红橙色发光状态,不只依赖颜色表达错误。
### AI 对话页
- 左右或上下布局都可,但核心是独立面板式聊天窗口。
- 消息列表作为玻璃面板,用户消息和 AI 消息使用不同方向、颜色和透明度。
- 正在生成时显示呼吸光点或打字指示。
- API 设置使用右侧抽屉或弹窗,不离开当前页面。
- 流式输出时光标可用轻微闪烁效果。
- 服务商对接(用户指定时强制):
- 默认 Base URL 与默认模型必须预填为用户指定值;OpenAI 兼容网关地址以 `/v1` 结尾(例:`https://www.tokenfree.com/v1` + `kimi-k2.6`)。
- 模型 ID 以服务商模型/定价页面实际字符串为准,禁止臆造。
- 本地设置 key 必须带版本号;切换服务商时升级版本,旧配置只允许迁移用户自填 API Key。
- 设置抽屉内必须内置深色玻璃风格使用教程:官网链接 → 注册登录 → 控制台创建 API Key(`sk-` 开头)→ 粘贴 → 测试连接 → 保存;并展示 Base URL、默认模型及「Key 仅保存在本地」说明。
- 教程中的 `code`、列表、链接、注释文字统一使用深色面板配色(青色系文字 + 半透明深色底),禁止白底教程块嵌入深色抽屉。
- 未配置 Key 时页面顶部用玻璃风格 Alert 引导,不得阻塞页面浏览。
### 页脚
- 使用半透明面板或顶部分割发光线。
- 展示系统版本、状态、时间、开源社区链接等信息。
- 链接 hover 有发光反馈。
- 移动端分组堆叠,避免拥挤。
## 7. 动效规范
推荐:
- 面板 hover:`translateY(-2px ~ -4px)`。
- 面板进入:透明度 + 8px 内位移。
- 背景光斑:20–40 秒慢速移动。
- 加载点:1.2–1.8 秒呼吸动画。
- 焦点和状态变化:150–300ms。
避免:
- 大面积高频闪烁。
- 持续旋转、跳动的强动效。
- 多层重度模糊叠加导致 GPU 卡顿。
- 自动播放声音。
必须加入:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
```
## 8. 响应式要求
- 桌面端指标卡片常用 3–5 列网格。
- 平板端降为 2 列。
- 手机端变为 1 列,面板间距收敛。
- 表格支持横向滚动或在小屏转为卡片列表。
- AI 设置面板在手机端全屏或从底部滑出。
- 装饰光斑和角标在小屏可减弱或隐藏。
## 9. 无障碍要求
- 正文和背景对比度达到可读标准。
- 装饰元素设置 `aria-hidden="true"`。
- 按钮和链接必须有可理解文本或 `aria-label`。
- 输入框有关联 `label`。
- `focus-visible` 使用清晰发光轮廓。
- 错误、成功、警告不能只通过颜色区分,要配合图标或文字。
- 弹窗、抽屉打开后焦点管理合理,关闭后返回触发元素。
## 10. 验收清单
页面改造完成后逐项检查:
- [ ] 全局背景和面板变量已建立。
- [ ] 导航、首页/仪表盘、表单、表格、登录页、AI 页、页脚风格统一。
- [ ] 已通过组件库官方 Theme/ConfigProvider Token 启用暗色主题。
- [ ] Modal、Drawer、Select、DatePicker、Popover 等浮层没有白底黑字。
- [ ] Card、Table、Descriptions、Tabs、Pagination、Empty 没有外黑内白。
- [ ] 至少有玻璃模糊、发光边框、渐变光效和悬浮反馈中的三种效果。
- [ ] 主要信息没有被装饰元素遮挡。
- [ ] 深色文字、浅色文字对比度均满足阅读要求。
- [ ] 手机和平板布局正常。
- [ ] 键盘导航和焦点样式正常。
- [ ] `prefers-reduced-motion` 生效。
- [ ] 构建没有因新增样式失败。
- [ ] 原业务流程、接口请求和权限控制未被破坏。
## 11. 阻断式自动审计(必须通过)
面板风格改造完成后,必须运行技能内置脚本:
```bash
python scripts/panel_ui_audit.py \
--base-url http://localhost:<端口> \
--hash-routes \
--username <账号> \
--password <密码> \
--submit-text <登录按钮文本> \
--routes /campusState,/alarms,/ai-chat \
--open-selector '.drawer-open-button' \
--wait-until domcontentloaded
```
脚本必须检查:
- 普通正文:对比度至少 4.5:1。
- 大号文字或粗体:对比度至少 3:1。
- Card、Table、Descriptions、Tabs、Pagination、Empty、List 不允许出现深色外壳中的浅色孤立区域。
- Modal、Drawer、Select、DatePicker、Popover、Tooltip、Cascader 不允许白底黑字或低对比。
- Alert、Tag、Badge 文案、图标、关闭按钮和操作按钮必须使用暗色主题下可读的颜色。
- 地图、视频、画布只豁免媒体内容本身,不豁免其悬浮控件、标签、空状态和提示信息。
含轮询、WebSocket、SSE 的单页应用使用 `--wait-until domcontentloaded`。只有退出码为 0、报告中 `0 violations`,健康验证才算完成。禁止为了通过审计而临时降低阈值、删除选择器或跳过页面。
+2
View File
@@ -0,0 +1,2 @@
# 面板 UI 阻断式审计脚本依赖(其余脚本仅用 Python 标准库)
playwright>=1.40.0
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交付压缩包构建脚本(合同对齐)。
产物(位于 --out-dir,默认 .Codex/delivery/):
1) <zip-name> 给甲方/终端使用的交付压缩包
2) 交付清单.md 与 zip 并列的单独文件(不打入包内)
zip 内部固定结构:
README.md 包根目录唯一一份 README
docs/screenshots/ README 引用的全部配图(解压可显示)
截图/界面截图/ 当前产品界面截图(中性文件名)
录屏/
文档/环境依赖清单.md
文档/部署注意事项.md
用法见 --help。
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
def copy_file(src: Path, dst: Path):
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def main() -> int:
ap = argparse.ArgumentParser(description="构建交付压缩包与并列交付清单")
ap.add_argument("--project-dir", required=True)
ap.add_argument("--readme", default="README.md")
ap.add_argument("--out-dir", default=".Codex/delivery")
ap.add_argument("--assets-product", default=".Codex/delivery/assets/product")
ap.add_argument("--recordings", default=".Codex/delivery/recordings")
ap.add_argument("--dep-doc", default=".Codex/delivery/环境依赖清单.md")
ap.add_argument("--notes-doc", default=".Codex/delivery/部署注意事项.md")
ap.add_argument("--manifest-src", default=".Codex/delivery/manifest.md")
ap.add_argument("--zip-name", default=None, help="默认 smart-parking-delivery-<YYYYMMDD>.zip")
ap.add_argument("--video-name", default="full-feature-demo.mp4")
args = ap.parse_args()
root = Path(args.project_dir).resolve()
out_dir = root / args.out_dir
out_dir.mkdir(parents=True, exist_ok=True)
readme_path = root / args.readme
if not readme_path.exists():
print("[FAIL] README 不存在:%s" % readme_path)
return 1
zip_name = args.zip_name or ("smart-parking-delivery-%s.zip" % _today())
zip_path = out_dir / zip_name
with tempfile.TemporaryDirectory() as td:
stage = Path(td)
# 1) README(包根唯一一份)
copy_file(readme_path, stage / "README.md")
# 2) README 引用图片 -> docs/...(保持相对路径)
readme_text = readme_path.read_text(encoding="utf-8", errors="ignore")
img_refs = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", readme_text)
copied_img = set()
for ref in img_refs:
ref = ref.strip()
if "://" in ref or ref.startswith("#"):
continue
src_img = (readme_path.parent / ref).resolve()
if not src_img.exists():
print("[FAIL] README 引用图片缺失:%s" % ref)
return 1
rel = src_img.relative_to(root)
if str(rel) not in copied_img:
copy_file(src_img, stage / rel)
copied_img.add(str(rel))
# 3) 产品界面截图(中性命名,不体现版本对比或来源宣传)
product_assets = root / args.assets_product
if product_assets.is_dir():
shutil.copytree(product_assets, stage / "截图" / "界面截图", dirs_exist_ok=True)
# 4) 录屏(只取指定 mp4)
rec_dir = root / args.recordings
video = rec_dir / args.video_name
if video.exists():
copy_file(video, stage / "录屏" / args.video_name)
# 5) 文档目录:仅依赖清单 + 注意事项(无 README、无交付清单)
for d in (args.dep_doc, args.notes_doc):
p = root / d
if p.exists():
copy_file(p, stage / "文档" / p.name)
# 打包
if zip_path.exists():
zip_path.unlink()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
for p in sorted(stage.rglob("*")):
if p.is_file():
z.write(p, p.relative_to(stage).as_posix())
# 交付清单:复制为与 zip 并列的单独文件
manifest_src = root / args.manifest_src
checklist_path = out_dir / "交付清单.md"
if manifest_src.exists():
shutil.copy2(manifest_src, checklist_path)
# 校验:解压核对 README 图片
bad = _verify_zip(zip_path)
if bad:
print("[FAIL] 交付包校验未通过:%s" % bad)
return 1
print("交付包:%s (%.2f MB)" % (zip_path, zip_path.stat().st_size/1024/1024))
if checklist_path.exists():
print("并列交付清单:%s" % checklist_path)
return 0
def _today() -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d")
def _verify_zip(zip_path: Path) -> list[str]:
problems: list[str] = []
with tempfile.TemporaryDirectory() as td:
with zipfile.ZipFile(zip_path) as z:
z.extractall(td)
v = Path(td)
names = [p.relative_to(v).as_posix() for p in v.rglob("*") if p.is_file()]
readmes = [n for n in names if n.lower().endswith("readme.md")]
if readmes != ["README.md"]:
problems.append("README 必须且只能在包根一份,实际:%s" % readmes)
if any("交付清单" in n for n in names):
problems.append("交付清单不得打入包内")
if any(re.search(r"(before|after|改造前|原版)", n, re.I) for n in names):
problems.append("包内不得出现对比期/before/after 文件命名")
docs = [n for n in names if n.startswith("文档/")]
if any(n.lower().endswith("readme.md") for n in docs):
problems.append("文档目录不得放 README")
if (v / "README.md").exists():
txt = (v / "README.md").read_text(encoding="utf-8", errors="ignore")
for ref in re.findall(r"!\[[^\]]*\]\(([^)]+)\)", txt):
if "://" in ref or ref.startswith("#"):
continue
if not (v / ref).exists():
problems.append("README 图片解压后缺失:%s" % ref)
forbidden_in_readme = [
"致谢", "原始开源项目", "原项目链接", "原作者", "基于开源",
"He Rui", "CrazyHer", "campus-surveillance",
]
for term in forbidden_in_readme:
if term.lower() in txt.lower():
problems.append("README 出现不应主动展示的来源表述:%s" % term)
return problems
if __name__ == "__main__":
sys.exit(main())
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交付物阻断式核查脚本(对齐软件改造部署外包交付规范)。
检查项:
A. 7 项交付物:源码 / 当前产品截图 / 全功能录屏 / 网盘归档 / README / Docker 部署 / 依赖与注意事项
B. README 12 项必备内容(按标题关键词启发式匹配)
C. 痕迹/来源清理:对用户提供的商业标识与来源关键词做全文检索(交付物中不得命中)
D. 交付清单 .Codex/delivery/manifest.md 存在且 7 项条目齐全
退出码:0 全部通过;1 存在阻断项。
用法见 --help。
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
README_KEYWORDS = {
"一句话简介": ["简介", "项目介绍"],
"详细功能描述": ["功能", "功能特性", "功能特点"],
"行业痛点": ["痛点"],
"技术优势": ["技术优势", "核心优势"],
"应用场景": ["应用场景", "使用场景", "场景"],
"目标行业与人群": ["目标行业", "适用行业", "适配行业", "目标用户", "适用人群"],
"商业应用价值": ["商业价值", "应用价值"],
"技术架构(含拓扑图)": ["技术架构", "架构"],
"环境前置依赖": ["环境要求", "前置依赖", "依赖要求", "环境依赖"],
"快速开始": ["快速开始", "快速上手", "quick start"],
"安装部署教程": ["部署", "安装"],
"版权与使用说明": ["版权", "使用说明"],
}
SCAN_SUFFIXES = {
".ts", ".tsx", ".js", ".jsx", ".vue", ".java", ".py", ".go", ".json",
".md", ".html", ".yml", ".yaml", ".less", ".css", ".scss", ".xml",
}
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp"}
VIDEO_SUFFIXES = {".mp4", ".mkv", ".mov", ".avi", ".webm"}
SKIP_DIRS = {
"node_modules", ".git", "dist", "build", "target", ".next", ".umi",
"__pycache__", ".venv", "venv",
}
def iter_files(root: Path):
# os.walk 自顶向下并在目录层剪枝,避免遍历 node_modules/.git 等巨型目录
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [
d for d in dirnames
if d not in SKIP_DIRS and not d.lower().startswith(".codex")
]
for name in filenames:
yield Path(dirpath) / name
def find_files(root: Path, suffixes: set[str]) -> list[Path]:
return [p for p in iter_files(root) if p.suffix.lower() in suffixes]
def main() -> int:
ap = argparse.ArgumentParser(description="开源改造项目交付物阻断式核查")
ap.add_argument("--project-dir", required=True, help="项目根目录")
ap.add_argument("--readme", default="README.md", help="README 相对路径")
ap.add_argument("--delivery-dir", default=".Codex/delivery", help="交付目录(相对项目)")
ap.add_argument("--assets-dir", default=".Codex/delivery/assets",
help="截图/录屏素材目录(相对项目)")
ap.add_argument("--trace-keywords", default="",
help="逗号分隔的商业标识与来源宣传关键词(原公司名、外部仓库、项目代号等)")
ap.add_argument("--allow-no-docker", action="store_true",
help="项目确实不适配 Docker 时,要求存在书面说明而非 Dockerfile")
ap.add_argument("--netdisk-link", default="",
help="网盘归档链接(manifest 中也可提供,二选一)")
ap.add_argument("--out", default=".Codex/reports/deliverables-check.json")
args = ap.parse_args()
root = Path(args.project_dir).resolve()
violations: list[str] = []
warnings: list[str] = []
evidence: dict = {}
# ---------- A1. 源码可构建产物线索 ----------
# 多模块项目(如 frontend/backend 分离)清单可能在子目录,递归查找
manifest_names = {
"package.json", "pom.xml", "build.gradle", "go.mod",
"requirements.txt", "pyproject.toml", "Cargo.toml",
}
manifest_hits = [p for p in iter_files(root) if p.name in manifest_names]
if not manifest_hits:
violations.append("未发现项目清单文件(package.json/pom.xml/go.mod 等),无法确认源码完整性")
else:
evidence["manifests"] = sorted(str(p.relative_to(root)) for p in manifest_hits)
# ---------- A2/A3. 截图与录屏 ----------
assets_dir = root / args.assets_dir
alt_dirs = [root / args.delivery_dir / "screenshots", root / args.delivery_dir / "recordings"]
images: list[Path] = []
videos: list[Path] = []
search_dirs = [d for d in [assets_dir, *alt_dirs] if d.exists()]
for d in search_dirs:
images.extend(find_files(d, IMAGE_SUFFIXES))
videos.extend(find_files(d, VIDEO_SUFFIXES))
# 对外只提供当前产品界面截图;交付素材目录不得含改造前/before 旧界面
leaked_before = [p for p in images if re.search(r"(before|改造前|原版)", p.name, re.I)]
if leaked_before:
violations.append("交付素材中包含改造前/原版截图(不应随包交付):%s"
% "、".join(p.name for p in leaked_before[:8]))
if not images:
violations.append("缺少产品界面截图(应放在 %s 或交付目录)" % args.assets_dir)
else:
evidence["screenshots"] = len(images)
if not videos:
violations.append("缺少全功能操作演示录屏(mp4/mov 等)")
else:
evidence["recordings"] = len(videos)
# ---------- A5. README ----------
readme_path = root / args.readme
readme_text = ""
if not readme_path.exists():
violations.append("缺少 README 文档:%s" % args.readme)
else:
readme_text = readme_path.read_text(encoding="utf-8", errors="ignore")
missing = []
low = readme_text.lower()
for item, kws in README_KEYWORDS.items():
if not any(k.lower() in low for k in kws):
missing.append(item)
if missing:
violations.append("README 缺少必备内容:%s" % "、".join(missing))
# 架构图线索:mermaid 或图片引用
if not re.search(r"```mermaid|!\[.*\]\(.*\)|架构图|拓扑图", readme_text):
warnings.append("README 未发现架构/拓扑图(mermaid 或图片)")
# 截图图文对应
if not re.search(r"!\[.*\]\(.*\)", readme_text):
warnings.append("README 无图片引用,合同要求功能逐条对应截图")
# ---------- A6. Docker / 书面说明 ----------
docker_files = [p for p in [root / "Dockerfile", root / "docker-compose.yml", root / "docker-compose.yaml"] if p.exists()]
if not docker_files:
if args.allow_no_docker:
note = root / args.delivery_dir / "no-docker说明.md"
if not note.exists():
violations.append("已声明不适配 Docker,但缺少书面说明(%s)" % note.relative_to(root))
else:
violations.append("缺少 Dockerfile / docker-compose(不适配 Docker 请加 --allow-no-docker 并提供书面说明)")
# ---------- A7. 依赖清单与部署注意事项 ----------
dep_docs = list((root / args.delivery_dir).glob("*依赖*")) + list((root / args.delivery_dir).glob("*注意事项*"))
dep_docs += [root / "依赖清单.md", root / "部署注意事项.md"]
dep_docs = [p for p in dep_docs if p.exists()]
if not dep_docs:
# 若 README 已含环境依赖/注意事项章节,降级为警告
if re.search(r"环境(要求|依赖|前置)|注意事项", readme_text):
warnings.append("未单独提供依赖清单/部署注意事项文档(README 中有相关章节,建议单独输出)")
else:
violations.append("缺少环境依赖清单与部署注意事项说明文档")
# ---------- A4. 网盘链接 + D. manifest ----------
manifest_path = root / args.delivery_dir / "manifest.md"
manifest_text = manifest_path.read_text(encoding="utf-8", errors="ignore") if manifest_path.exists() else ""
netdisk = args.netdisk_link or (re.search(r"https?://\S*(?:pan\.baidu|aliyundrive|alipan|123pan|cloud\.189)\S*", manifest_text) or [""])[0]
if not netdisk:
if not manifest_text:
violations.append("缺少网盘归档链接,且未提供交付清单 manifest.md")
else:
violations.append("未提供网盘素材归档链接(命令行 --netdisk-link 或写入 manifest)")
if not manifest_path.exists():
violations.append("缺少交付清单:%s" % manifest_path.relative_to(root))
else:
required_items = ["源码", "截图", "录屏", "网盘", "README", "Docker", "依赖"]
miss_items = [w for w in required_items if w not in manifest_text]
if miss_items:
violations.append("manifest.md 未覆盖交付物条目:%s" % "、".join(miss_items))
# ---------- C. 痕迹清理检索 ----------
kws = [k.strip() for k in args.trace_keywords.split(",") if k.strip()]
traces: list[str] = []
if kws:
pat = re.compile("|".join(re.escape(k) for k in kws), re.I)
for p in iter_files(root):
if p.suffix.lower() not in SCAN_SUFFIXES:
continue
rel = str(p.relative_to(root))
if re.search(r"license|copying", rel, re.I):
continue
try:
txt = p.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
if pat.search(txt):
traces.append(rel)
if traces:
violations.append("痕迹清理不彻底,以下文件命中原标识关键词:%s" % "、".join(sorted(set(traces))[:20]))
# ---------- 输出 ----------
result = {
"timestamp": datetime.now().isoformat(),
"project_dir": str(root),
"violations": violations,
"warnings": warnings,
"evidence": evidence,
"passed": not violations,
}
out_path = root / args.out
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print("交付物核查:%d 个阻断项,%d 个警告" % (len(violations), len(warnings)))
for v in violations:
print(" [FAIL]", v)
for w in warnings:
print(" [WARN]", w)
print("report=%s" % out_path)
return 0 if not violations else 1
if __name__ == "__main__":
sys.exit(main())
+351
View File
@@ -0,0 +1,351 @@
#!/usr/bin/env python3
"""Blocking audit for dark glass-panel UIs.
Requires Playwright: python -m pip install playwright && playwright install chromium.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
from playwright.sync_api import Page, sync_playwright
AUDIT_JS = r"""
(() => {
function parseColor(value) {
const match = /rgba?\(([^)]+)\)/.exec(value || '');
if (!match) return {r: 255, g: 255, b: 255, a: 1};
const parts = match[1].split(',').map((x) => Number(x.trim()));
return {r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1};
}
function composite(top, bottom) {
const alpha = top.a + (1 - top.a) * bottom.a;
if (alpha === 0) return {r: bottom.r, g: bottom.g, b: bottom.b, a: 0};
const channel = (t, b) => (top.a * t + (1 - top.a) * b) / alpha;
return {r: channel(top.r, bottom.r), g: channel(top.g, bottom.g), b: channel(top.b, bottom.b), a: alpha};
}
function splitTopLevel(text) {
const parts = [];
let depth = 0;
let start = 0;
for (let i = 0; i < text.length; i += 1) {
if (text[i] === '(') depth += 1;
if (text[i] === ')') depth -= 1;
if (text[i] === ',' && depth === 0) {
parts.push(text.slice(start, i).trim());
start = i + 1;
}
}
const last = text.slice(start).trim();
if (last) parts.push(last);
return parts;
}
function parseColorOrNull(value) {
const match = /rgba?\(([^)]*)\)/.exec(value || '');
if (!match) return null;
const parts = match[1].split(',').map((x) => Number(x.trim()));
return {r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1};
}
function averageColors(colors) {
if (!colors.length) return null;
const total = colors.reduce((acc, color) => ({
r: acc.r + color.r,
g: acc.g + color.g,
b: acc.b + color.b,
a: acc.a + color.a,
}), {r: 0, g: 0, b: 0, a: 0});
return {
r: total.r / colors.length,
g: total.g / colors.length,
b: total.b / colors.length,
a: Math.min(1, total.a / colors.length),
};
}
function extractGradient(text, keyword) {
const start = text.search(new RegExp(`(?:^|[^a-z-])${keyword}\\(`, 'i'));
if (start < 0) return null;
const open = text.indexOf('(', start + keyword.length - 1);
let depth = 0;
for (let i = open; i < text.length; i += 1) {
if (text[i] === '(') depth += 1;
if (text[i] === ')') {
depth -= 1;
if (depth === 0) return text.slice(open + 1, i);
}
}
return null;
}
function gradientAverage(backgroundImage, keyword) {
const content = extractGradient(backgroundImage, keyword);
if (!content) return null;
const colors = splitTopLevel(content)
.map((part) => parseColorOrNull(part))
.filter(Boolean);
return averageColors(colors);
}
function backgroundImageLayers(style) {
const image = style.backgroundImage;
if (!image || image === 'none') return [];
return ['conic-gradient', 'radial-gradient', 'linear-gradient']
.map((keyword) => gradientAverage(image, keyword))
.filter(Boolean);
}
function effectiveBackground(startEl) {
const chain = [];
let el = startEl;
while (el && el.nodeType === 1) {
const style = getComputedStyle(el);
chain.unshift({
color: parseColor(style.backgroundColor),
images: backgroundImageLayers(style),
});
el = el.parentElement;
}
let bg = {r: 255, g: 255, b: 255, a: 1};
for (const layer of chain) {
bg = composite(layer.color, bg);
for (const imageLayer of layer.images) bg = composite(imageLayer, bg);
}
return bg;
}
function channelLuminance(value) {
const c = Math.max(0, Math.min(255, value)) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
function luminance(color) {
return 0.2126 * channelLuminance(color.r) + 0.7152 * channelLuminance(color.g) + 0.0722 * channelLuminance(color.b);
}
function contrastRatio(first, second) {
const a = luminance(first);
const b = luminance(second);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
function visible(el) {
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
return rect.width >= 2 && rect.height >= 2 &&
style.display !== 'none' && style.visibility !== 'hidden' &&
Number(style.opacity) >= 0.1;
}
function hasDirectText(el) {
return Array.from(el.childNodes).some((node) => node.nodeType === 3 && node.textContent.trim().length > 0);
}
function isMedia(el) {
return Boolean(el.closest && el.closest(
'img,picture,video,canvas,svg,iframe,.leaflet-container,[class*="leaflet"],[class*="map-container"],[class*="map-wrapper"],[data-map],[class*="player"]'
));
}
function inFloatingLayer(el) {
return Boolean(el.closest && el.closest(
'.ant-select-dropdown,.ant-picker-dropdown,.ant-modal-content,.ant-drawer-content,.ant-popover-inner,.ant-tooltip,.ant-cascader-dropdown,[role="dialog"],[role="alertdialog"]'
));
}
const bodyBg = effectiveBackground(document.body);
const bodyLuminance = luminance(bodyBg);
const issues = [];
const seen = new Set();
const nodes = Array.from(document.querySelectorAll('body *')).filter(visible).slice(0, 1800);
for (const el of nodes) {
const style = getComputedStyle(el);
const canCarryText = hasDirectText(el) || el.matches(
'input,textarea,button,select,a,label,summary,option,[role="button"],[role="option"],[role="menuitem"],[role="tab"],[aria-label]'
);
if (!canCarryText) continue;
const text = (el.innerText || el.value || el.placeholder || el.getAttribute('aria-label') || '').trim();
if (!text) continue;
const bg = effectiveBackground(el);
const styledColor = parseColor(style.color);
const fg = composite(styledColor, bg);
const ratio = contrastRatio(fg, bg);
const fontSize = Number.parseFloat(style.fontSize);
const bold = Number.parseInt(style.fontWeight, 10) >= 600;
const large = fontSize >= 18.5 || (bold && fontSize >= 15);
const required = large ? 3 : 4.5;
const rect = el.getBoundingClientRect();
function push(reason, severity) {
const item = {
reason,
severity,
text: text.slice(0, 140),
tag: el.tagName.toLowerCase(),
className: String(el.className || '').slice(0, 180),
id: el.id || '',
background: `rgb(${Math.round(bg.r)}, ${Math.round(bg.g)}, ${Math.round(bg.b)})`,
color: `rgb(${Math.round(fg.r)}, ${Math.round(fg.g)}, ${Math.round(fg.b)})`,
contrast: Number(ratio.toFixed(2)),
requiredContrast: required,
backgroundLuminance: Number(luminance(bg).toFixed(3)),
bodyLuminance: Number(bodyLuminance.toFixed(3)),
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
floating: inFloatingLayer(el),
};
const key = JSON.stringify([item.reason, item.text, item.className, item.contrast, item.background]);
if (!seen.has(key)) {
seen.add(key);
issues.push(item);
}
}
if (ratio < required) push('text contrast below threshold', 'P0');
const bgLum = luminance(bg);
if (!isMedia(el) && bgLum >= 0.62 && bodyLuminance <= 0.16 && rect.width >= 32 && rect.height >= 20) {
push('light component embedded in dark shell', 'P0');
}
}
return {bodyBg, bodyLuminance, issues};
})
"""
FLOAT_OPENERS = [
'.ant-select:not(.ant-select-disabled) .ant-select-selector',
'.ant-picker input',
]
def route_url(base_url: str, route: str, hash_routes: bool) -> str:
base = base_url.rstrip('/')
route = route if route.startswith('/') else f'/{route}'
return f'{base}/#{route}' if hash_routes else f'{base}{route}'
def audit_page(page: Page, route: str, extra_openers: list[str]) -> dict[str, Any]:
issues: list[dict[str, Any]] = []
data = page.evaluate(AUDIT_JS)
issues.extend(item for item in data['issues'] if not item.get('floating'))
selectors = FLOAT_OPENERS + extra_openers
for selector in selectors:
try:
locator = page.locator(selector).first
if locator.count() == 0 or not locator.is_visible():
continue
locator.click(timeout=1200)
page.wait_for_timeout(500)
opened = page.evaluate(AUDIT_JS)
issues.extend(item for item in opened['issues'] if item.get('floating'))
except Exception:
pass
finally:
for _ in range(2):
page.keyboard.press('Escape')
page.wait_for_timeout(120)
return {'route': route, 'issues': issues}
def launch_chromium(playwright):
launch_options = {'headless': True}
channel = os.environ.get('PANEL_PLAYWRIGHT_CHANNEL')
executable_path = os.environ.get('PANEL_PLAYWRIGHT_EXECUTABLE_PATH')
if channel:
launch_options['channel'] = channel
elif executable_path:
launch_options['executable_path'] = executable_path
else:
local_app_data = os.environ.get('LOCALAPPDATA', '')
playwright_cache = Path(local_app_data) / 'ms-playwright' if local_app_data else None
has_downloaded_chromium = bool(
playwright_cache and list(playwright_cache.glob('chromium-*/chrome-win64/chrome.exe'))
)
if not has_downloaded_chromium:
program_files_x86 = os.environ.get('ProgramFiles(x86)', '')
program_files = os.environ.get('ProgramFiles', '')
edge_candidates = [
Path(program_files_x86) / 'Microsoft' / 'Edge' / 'Application' / 'msedge.exe',
Path(program_files) / 'Microsoft' / 'Edge' / 'Application' / 'msedge.exe',
]
if any(edge.exists() for edge in edge_candidates):
launch_options['channel'] = 'msedge'
return playwright.chromium.launch(**launch_options)
def csv_list(value: str) -> list[str]:
return [item.strip() for item in value.split(',') if item.strip()]
def main() -> int:
parser = argparse.ArgumentParser(description='Audit panel UI contrast and light-in-dark nesting.')
parser.add_argument('--base-url', required=True)
parser.add_argument('--routes', required=True, help='Comma-separated routes, for example /campusState,/alarms')
parser.add_argument('--hash-routes', action='store_true')
parser.add_argument('--login-route', default='/login')
parser.add_argument('--skip-login', action='store_true')
parser.add_argument('--username', default='')
parser.add_argument('--password', default='')
parser.add_argument('--username-selector', default='input[type="text"],input:not([type]),input[name="username"]')
parser.add_argument('--password-selector', default='input[type="password"]')
parser.add_argument('--submit-text', default='')
parser.add_argument('--submit-selector', default='button[type="submit"]')
parser.add_argument('--open-selector', action='append', default=[], help='Optional element that opens a drawer/modal; repeatable')
parser.add_argument('--viewport-width', type=int, default=1440)
parser.add_argument('--viewport-height', type=int, default=900)
parser.add_argument('--wait-until', default='domcontentloaded', choices=['load', 'domcontentloaded', 'networkidle', 'commit'])
parser.add_argument('--out', default='.Codex/reports/panel-ui-audit.json')
args = parser.parse_args()
routes = csv_list(args.routes)
report: dict[str, Any] = {'baseUrl': args.base_url, 'pages': []}
with sync_playwright() as playwright:
browser = launch_chromium(playwright)
page = browser.new_page(viewport={'width': args.viewport_width, 'height': args.viewport_height})
if not args.skip_login:
page.goto(route_url(args.base_url, args.login_route, args.hash_routes), wait_until=args.wait_until)
if args.username:
page.locator(args.username_selector).first.fill(args.username)
if args.password:
page.locator(args.password_selector).first.fill(args.password)
if args.submit_text:
page.get_by_role('button', name=args.submit_text).click()
else:
page.locator(args.submit_selector).first.click()
page.wait_for_load_state('networkidle')
page.wait_for_timeout(800)
for route in routes:
page.goto(route_url(args.base_url, route, args.hash_routes), wait_until=args.wait_until)
page.wait_for_timeout(1200)
report['pages'].append(audit_page(page, route, args.open_selector))
browser.close()
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
total = sum(len(page_report['issues']) for page_report in report['pages'])
print(f'audited {len(report["pages"])} routes; {total} violations')
print(f'report={out.resolve()}')
return 1 if total else 0
if __name__ == '__main__':
sys.exit(main())
+570
View File
@@ -0,0 +1,570 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
项目初始化与部署工作流管理器
提供状态管理、步骤执行和错误恢复功能
"""
import os
import sys
import json
import shutil
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
from enum import Enum
class StepStatus(Enum):
PENDING = "⏭️ 未开始"
IN_PROGRESS = "⏸️ 进行中"
COMPLETED = "✅ 已完成"
FAILED = "❌ 失败"
SKIPPED = "⏭️ 跳过"
class ProjectType(Enum):
DOCKER_WEB = "docker_web"
DOCKER_API = "docker_api"
DOCKER_MICROSERVICES = "docker_microservices"
DESKTOP = "desktop"
MOBILE = "mobile"
CLI = "cli"
UNKNOWN = "unknown"
class TechStack(Enum):
JAVA_SPRING = "java_spring"
NODE_EXPRESS = "node_express"
NODE_NEST = "node_nest"
PYTHON_DJANGO = "python_django"
PYTHON_FASTAPI = "python_fastapi"
GO_GIN = "go_gin"
GO_FIBER = "go_fiber"
REACT = "react"
VUE = "vue"
ANGULAR = "angular"
UNKNOWN = "unknown"
@dataclass
class ProjectInfo:
"""项目信息"""
name: str
directory: str
git_url: str
port_range: Optional[str] = None
project_type: ProjectType = ProjectType.UNKNOWN
tech_stack: List[TechStack] = None
def __post_init__(self):
if self.tech_stack is None:
self.tech_stack = []
@dataclass
class Step:
"""工作流步骤"""
id: int
name: str
description: str
applicable_types: List[ProjectType]
status: StepStatus = StepStatus.PENDING
start_time: Optional[str] = None
end_time: Optional[str] = None
notes: str = ""
error_info: Optional[str] = None
class WorkflowManager:
"""工作流管理器"""
# 完整的20个核心步骤
ALL_TYPES = [ProjectType.DOCKER_WEB, ProjectType.DOCKER_API, ProjectType.DOCKER_MICROSERVICES, ProjectType.DESKTOP, ProjectType.MOBILE, ProjectType.CLI]
DOCKER_TYPES = [ProjectType.DOCKER_WEB, ProjectType.DOCKER_API, ProjectType.DOCKER_MICROSERVICES]
# 关键时序:初始基线启动(3,内部) → 初始界面截图(4,内部) → 面板改造(6-11) → 产品构建启动(15)
# → 健康验证(16) → 产品截图与录屏(17) → README/归档(18) → 交付核查(19)
CORE_STEPS = [
Step(0, "克隆项目", "从Git仓库克隆项目到本地,不修改任何业务代码", ALL_TYPES),
Step(1, "项目完整度评估", "评估项目完整度、依赖情况与可运行性", ALL_TYPES),
Step(2, "项目整体分析", "分析项目结构、技术栈、启动方式和部署策略", ALL_TYPES),
Step(3, "初始版本基线启动(内部)", "在改动任何代码前,用项目现有启动方式跑通初始版本并确认可访问;仅用于内部质量核对", ALL_TYPES),
Step(4, "初始界面基线截图(内部)", "代码零改动状态下采集初始界面作为内部对比基线;严禁进入交付包或使用 before 等对外文件名", ALL_TYPES),
Step(5, "Docker配置生成", "生成/完善 Docker Compose 配置文件", DOCKER_TYPES),
Step(6, "页面信息与品牌名称修改", "替换页面品牌信息并建立科技感面板基础样式", ALL_TYPES),
Step(7, "Logo生成与替换", "生成并替换项目 Logo、Favicon,适配深色面板", ALL_TYPES),
Step(8, "版权与第三方标识规范", "清理依法可移除的公司名称、LOGO、商标、水印、联系方式等专属标识;MIT 等许可证要求保留的版权与许可文本必须随源码保留", ALL_TYPES),
Step(9, "页脚、导航与社区链接", "导航、页脚改为半透明悬浮面板风格,并放置开源社区与 GCC AI Coding 链接", ALL_TYPES),
Step(10, "登录页面", "改成居中玻璃面板、发光输入框、动态背景", ALL_TYPES),
Step(11, "AI对话功能", "实现前端AI对话界面与指定服务商API对接及使用教程", ALL_TYPES),
Step(12, "示例数据", "生成和导入示例数据,数据卡片/列表面板承载", DOCKER_TYPES),
Step(13, "CORS配置检查", "检查并配置 CORS 设置", DOCKER_TYPES),
Step(14, "端口检查与分配", "检查端口占用并分配端口", DOCKER_TYPES),
Step(15, "改造后构建与启动", "Docker项目构建镜像启动容器;非Docker项目手动构建并启动改造版", ALL_TYPES),
Step(16, "健康验证", "验证服务健康、面板UI审计0违规、全功能走查无报错", ALL_TYPES),
Step(17, "产品截图与全功能录屏", "采集当前产品全套高清界面截图(中性文件名)和全功能演示录屏;内部基线素材不进入交付物", ALL_TYPES),
Step(18, "标准化README与版本归档", "编写标准化 README(引用最终素材、含架构图与部署教程);不主动写来源/致谢宣传,并按授权决定是否提交", ALL_TYPES),
Step(19, "交付物核查", "按7项交付物清单与7条验收标准做交付前阻断式核查", ALL_TYPES),
]
# 旧版本步骤名 → 新版本步骤名(用于历史状态迁移)
STEP_NAME_ALIASES = {
"构建与启动": "改造后构建与启动",
"素材录制归档": "产品截图与全功能录屏",
"原始项目基线启动": "初始版本基线启动(内部)",
"改造前基线截图": "初始界面基线截图(内部)",
"原作者信息与版权痕迹合规处理": "版权与第三方标识规范",
"页脚与导航增强": "页脚、导航与社区链接",
"改造后截图与全功能录屏": "产品截图与全功能录屏",
"标准化README与提交": "标准化README与版本归档",
}
def __init__(self, project_dir: str):
self.project_dir = Path(project_dir)
self.claude_dir = self.project_dir / ".Codex"
self.state_file = self.claude_dir / "workflow_state.json"
self.error_file = self.claude_dir / "error_log.md"
self.project_info: Optional[ProjectInfo] = None
self.steps: List[Step] = []
def init_project(self, name: str, git_url: str, port_range: Optional[str] = None):
"""初始化项目信息"""
self.project_info = ProjectInfo(
name=name,
directory=str(self.project_dir),
git_url=git_url,
port_range=port_range
)
self.steps = [Step(s.id, s.name, s.description, s.applicable_types) for s in self.CORE_STEPS]
self._ensure_directories()
self._save_state()
def _ensure_directories(self):
"""确保必要的目录存在"""
self.claude_dir.mkdir(exist_ok=True)
(self.claude_dir / 'reports').mkdir(exist_ok=True)
def _save_state(self):
"""保存状态到JSON文件"""
def enum_to_value(obj):
if isinstance(obj, Enum):
return obj.value
if isinstance(obj, list):
return [enum_to_value(item) for item in obj]
if isinstance(obj, dict):
return {k: enum_to_value(v) for k, v in obj.items()}
return obj
project_info_dict = None
if self.project_info:
project_info_dict = asdict(self.project_info)
project_info_dict["project_type"] = project_info_dict["project_type"].value
project_info_dict["tech_stack"] = [ts.value for ts in project_info_dict["tech_stack"]]
state = {
"project_info": project_info_dict,
"steps": [
{
"id": s.id,
"name": s.name,
"status": s.status.value,
"start_time": s.start_time,
"end_time": s.end_time,
"notes": s.notes,
"error_info": s.error_info
}
for s in self.steps
],
"last_updated": datetime.now().isoformat()
}
with open(self.state_file, 'w', encoding='utf-8') as f:
json.dump(state, f, ensure_ascii=False, indent=2)
def load_state(self) -> bool:
"""从JSON文件加载状态"""
if not self.state_file.exists():
return False
try:
with open(self.state_file, 'r', encoding='utf-8') as f:
state = json.load(f)
if state.get("project_info"):
info = state["project_info"]
self.project_info = ProjectInfo(
name=info["name"],
directory=info["directory"],
git_url=info["git_url"],
port_range=info.get("port_range"),
project_type=ProjectType(info.get("project_type", "unknown")),
tech_stack=[TechStack(ts) for ts in info.get("tech_stack", [])]
)
# 以新 CORE_STEPS 为权威骨架;旧状态按“步骤名(含别名)”映射进度,
# 避免编号错位导致已完成的步骤被重置。
old_steps = state.get("steps", [])
name_to_state: Dict[str, dict] = {}
for s in old_steps:
canon = self.STEP_NAME_ALIASES.get(s.get("name", ""), s.get("name", ""))
name_to_state[canon] = s
self.steps = []
for cs in self.CORE_STEPS:
prev = name_to_state.get(cs.name)
if prev is not None:
self.steps.append(Step(
id=cs.id,
name=cs.name,
description=cs.description,
applicable_types=cs.applicable_types,
status=StepStatus(prev.get("status", StepStatus.PENDING.value)),
start_time=prev.get("start_time"),
end_time=prev.get("end_time"),
notes=prev.get("notes", ""),
error_info=prev.get("error_info"),
))
else:
# 新增步骤默认未开始
self.steps.append(Step(cs.id, cs.name, cs.description, cs.applicable_types))
return True
except Exception as e:
self._log_error(f"加载状态失败: {e}")
return False
def get_current_step(self) -> Optional[Step]:
"""获取当前步骤"""
for step in self.steps:
if step.status == StepStatus.IN_PROGRESS:
return step
if step.status == StepStatus.PENDING:
return step
return None
def start_step(self, step_id: int) -> bool:
"""开始执行步骤"""
step = next((s for s in self.steps if s.id == step_id), None)
if not step:
return False
# 检查步骤是否适用于当前项目类型
if self.project_info and self.project_info.project_type != ProjectType.UNKNOWN:
if self.project_info.project_type not in step.applicable_types:
step.status = StepStatus.SKIPPED
step.notes = f"步骤不适用于项目类型: {self.project_info.project_type.value}"
self._save_state()
return True
step.status = StepStatus.IN_PROGRESS
step.start_time = datetime.now().isoformat()
self._save_state()
return True
def complete_step(self, step_id: int, notes: str = ""):
"""完成步骤"""
step = next((s for s in self.steps if s.id == step_id), None)
if step:
step.status = StepStatus.COMPLETED
step.end_time = datetime.now().isoformat()
step.notes = notes
self._save_state()
def fail_step(self, step_id: int, error_info: str):
"""标记步骤失败"""
step = next((s for s in self.steps if s.id == step_id), None)
if step:
step.status = StepStatus.FAILED
step.end_time = datetime.now().isoformat()
step.error_info = error_info
self._save_state()
self._log_error(f"步骤 {step_id} ({step.name}) 失败: {error_info}")
def skip_step(self, step_id: int, reason: str = ""):
"""跳过步骤"""
step = next((s for s in self.steps if s.id == step_id), None)
if step:
step.status = StepStatus.SKIPPED
step.notes = reason
self._save_state()
def _log_error(self, message: str):
"""记录错误到错误日志"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
error_entry = f"\n## [{timestamp}]\n{message}\n\n"
with open(self.error_file, 'a', encoding='utf-8') as f:
f.write(error_entry)
def get_progress(self) -> Dict:
"""获取进度信息"""
total = len(self.steps)
completed = sum(1 for s in self.steps if s.status == StepStatus.COMPLETED)
failed = sum(1 for s in self.steps if s.status == StepStatus.FAILED)
skipped = sum(1 for s in self.steps if s.status == StepStatus.SKIPPED)
pending = sum(1 for s in self.steps if s.status == StepStatus.PENDING)
return {
"total": total,
"completed": completed,
"failed": failed,
"skipped": skipped,
"pending": pending,
"percentage": (completed / total * 100) if total > 0 else 0
}
def can_resume(self) -> Tuple[bool, Optional[int]]:
"""检查是否可以恢复执行"""
if not self.state_file.exists():
return False, None
self.load_state()
# 检查是否有失败的步骤
failed_step = next((s for s in self.steps if s.status == StepStatus.FAILED), None)
if failed_step:
return True, failed_step.id
# 检查是否有进行中的步骤
in_progress = next((s for s in self.steps if s.status == StepStatus.IN_PROGRESS), None)
if in_progress:
return True, in_progress.id
# 检查是否有未开始的步骤
pending = next((s for s in self.steps if s.status == StepStatus.PENDING), None)
if pending:
return True, pending.id
return False, None
def generate_report(self) -> str:
"""生成执行报告"""
progress = self.get_progress()
report = f"""# 工作流执行报告
## 项目信息
- **项目名称**: {self.project_info.name if self.project_info else 'N/A'}
- **项目目录**: {self.project_info.directory if self.project_info else 'N/A'}
- **Git地址**: {self.project_info.git_url if self.project_info else 'N/A'}
- **项目类型**: {self.project_info.project_type.value if self.project_info else 'N/A'}
## 执行进度
- **总步骤**: {progress['total']}
- **已完成**: {progress['completed']}
- **失败**: {progress['failed']}
- **跳过**: {progress['skipped']}
- **待执行**: {progress['pending']}
- **完成度**: {progress['percentage']:.1f}%
## 步骤详情
"""
for step in self.steps:
report += f"\n### 步骤 {step.id}: {step.name}\n"
report += f"- **状态**: {step.status.value}\n"
if step.start_time:
report += f"- **开始时间**: {step.start_time}\n"
if step.end_time:
report += f"- **完成时间**: {step.end_time}\n"
if step.notes:
report += f"- **说明**: {step.notes}\n"
if step.error_info:
report += f"- **错误**: {step.error_info}\n"
return report
# 技术栈检测工具
class TechStackDetector:
"""技术栈检测器"""
DETECTION_RULES = {
TechStack.JAVA_SPRING: ["pom.xml", "build.gradle", "src/main/java"],
TechStack.NODE_EXPRESS: ["package.json", "app.js", "server.js"],
TechStack.NODE_NEST: ["nest-cli.json", "@nestjs"],
TechStack.PYTHON_DJANGO: ["manage.py", "django", "requirements.txt"],
TechStack.PYTHON_FASTAPI: ["fastapi", "main.py"],
TechStack.GO_GIN: ["go.mod", "gin"],
TechStack.GO_FIBER: ["go.mod", "fiber"],
TechStack.REACT: ["package.json", "react", "src/App.js", "src/App.tsx", "vite.config.ts", "vite.config.js"],
TechStack.VUE: ["package.json", "vue", "vue.config.js", "vite.config.ts", "vite.config.js"],
TechStack.ANGULAR: ["angular.json", "@angular"],
}
@classmethod
def detect(cls, project_dir: str) -> List[TechStack]:
"""检测项目技术栈"""
detected = []
project_path = Path(project_dir)
for tech, indicators in cls.DETECTION_RULES.items():
found = False
for indicator in indicators:
# 递归检查文件存在
for _ in project_path.rglob(indicator):
found = True
break
if found: break
# 递归检查文件内容
for file in project_path.rglob("*.json"):
try:
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
if indicator in content:
found = True
break
except:
pass
if found: break
if found and tech not in detected:
detected.append(tech)
return detected if detected else [TechStack.UNKNOWN]
# 项目类型判断器
class ProjectTypeDetector:
"""项目类型检测器"""
@classmethod
def detect(cls, project_dir: str, tech_stacks: List[TechStack]) -> ProjectType:
"""检测项目类型"""
project_path = Path(project_dir)
# 检查桌面应用特征
desktop_indicators = ["electron", "qt", "swing", "wpf", ".desktop"]
for indicator in desktop_indicators:
if any(indicator in str(tech.value).lower() for tech in tech_stacks):
return ProjectType.DESKTOP
# 检查移动应用特征
mobile_indicators = ["react-native", "flutter", "android", "ios"]
for indicator in mobile_indicators:
if any(indicator in str(tech.value).lower() for tech in tech_stacks):
return ProjectType.MOBILE
# 检查Docker配置
if (project_path / "Dockerfile").exists() or (project_path / "docker-compose.yml").exists():
# 检查微服务特征
if (project_path / "services").exists() or len(list(project_path.glob("**/Dockerfile"))) > 1:
return ProjectType.DOCKER_MICROSERVICES
return ProjectType.DOCKER_WEB
# 检查API服务特征
if any(tech in [TechStack.NODE_EXPRESS, TechStack.NODE_NEST,
TechStack.PYTHON_FASTAPI, TechStack.GO_GIN] for tech in tech_stacks):
return ProjectType.DOCKER_API
return ProjectType.UNKNOWN
def main():
"""命令行入口"""
if len(sys.argv) < 2:
print("Usage: workflow_manager.py <command> [args...]")
print("Commands: init, status, resume, report")
sys.exit(1)
command = sys.argv[1]
if command == "init":
if len(sys.argv) < 5:
print("Usage: workflow_manager.py init <project_dir> <name> <git_url> [port_range]")
sys.exit(1)
project_dir = sys.argv[2]
name = sys.argv[3]
git_url = sys.argv[4]
port_range = sys.argv[5] if len(sys.argv) > 5 else None
manager = WorkflowManager(project_dir)
manager.init_project(name, git_url, port_range)
print(f"项目已初始化: {name}")
elif command == "start_step":
if len(sys.argv) < 4:
print("Usage: workflow_manager.py start_step <project_dir> <step_id>")
sys.exit(1)
project_dir = sys.argv[2]
step_id = int(sys.argv[3])
manager = WorkflowManager(project_dir)
if manager.load_state():
if manager.start_step(step_id):
print(f"步骤 {step_id} 已开始")
else:
print(f"步骤 {step_id} 开始失败")
else:
print("未找到状态文件")
elif command == "complete_step":
if len(sys.argv) < 4:
print("Usage: workflow_manager.py complete_step <project_dir> <step_id> [notes]")
sys.exit(1)
project_dir = sys.argv[2]
step_id = int(sys.argv[3])
notes = sys.argv[4] if len(sys.argv) > 4 else ""
manager = WorkflowManager(project_dir)
if manager.load_state():
manager.complete_step(step_id, notes)
print(f"步骤 {step_id} 已完成")
else:
print("未找到状态文件")
elif command == "detect":
if len(sys.argv) < 3:
print("Usage: workflow_manager.py detect <project_dir>")
sys.exit(1)
project_dir = sys.argv[2]
manager = WorkflowManager(project_dir)
if manager.load_state():
tech_stack = TechStackDetector.detect(project_dir)
project_type = ProjectTypeDetector.detect(project_dir, tech_stack)
manager.project_info.tech_stack = tech_stack
manager.project_info.project_type = project_type
manager.start_step(2)
manager.complete_step(2, f"Detected tech stack: {[ts.value for ts in tech_stack]}, project type: {project_type.value}")
print(f"Tech stack: {[ts.value for ts in tech_stack]}, Project type: {project_type.value}")
else:
print("未找到状态文件")
elif command == "status":
if len(sys.argv) < 3:
print("Usage: workflow_manager.py status <project_dir>")
sys.exit(1)
project_dir = sys.argv[2]
manager = WorkflowManager(project_dir)
if manager.load_state():
progress = manager.get_progress()
print(f"进度: {progress['completed']}/{progress['total']} ({progress['percentage']:.1f}%)")
current = manager.get_current_step()
if current:
print(f"当前步骤: 步骤 {current.id} - {current.name}")
else:
print("未找到状态文件")
elif command == "resume":
if len(sys.argv) < 3:
print("Usage: workflow_manager.py resume <project_dir>")
sys.exit(1)
project_dir = sys.argv[2]
manager = WorkflowManager(project_dir)
can_resume, step_id = manager.can_resume()
if can_resume:
print(f"可以从步骤 {step_id} 恢复执行")
else:
print("无法恢复,请重新初始化")
elif command == "report":
if len(sys.argv) < 3:
print("Usage: workflow_manager.py report <project_dir>")
sys.exit(1)
project_dir = sys.argv[2]
manager = WorkflowManager(project_dir)
if manager.load_state():
print(manager.generate_report())
else:
print("未找到状态文件")
if __name__ == "__main__":
main()