mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 21:40:50 +00:00
docs: 新增项目规范体系与前端一键部署文档
This commit is contained in:
parent
8071c277b6
commit
05a184ead9
@ -32,7 +32,11 @@
|
||||
./run.sh delete # 停止并清理所有数据(危险)
|
||||
./run.sh logs # 查看日志
|
||||
./run.sh ps # 查看容器状态
|
||||
./run.sh # 交互式菜单
|
||||
./run.sh admin:dev # 管理端前端开发测试启动(Vite 1619,/adminapi 代理 8011)
|
||||
./run.sh admin:build # 管理端 build:all(主包+扩展 UMD)并发布到 backend/public/admin
|
||||
./run.sh uni:dev # uniapp H5 开发测试启动(默认端口 5173)
|
||||
./run.sh uni:build # uniapp H5 构建并发布到 backend/public(index.html、assets、pages、static)
|
||||
./run.sh # 交互式菜单(含 11/12 管理端、21/22 uniapp 前端命令)
|
||||
```
|
||||
|
||||
`install` 会删除 `backend/public/install.lock`、清空 `backend/runtime`、`docker/mysql/data` 等数据目录,完成后访问 **http://localhost:8011** 进入安装向导。
|
||||
@ -130,6 +134,15 @@ location /ws {
|
||||
| `frontend/admin` | Vue 3 + Element Plus + Vite | 管理后台,构建产物部署到 `/admin`;二开用 `scripts/build-extensions.js` 构建插件 UMD 扩展 |
|
||||
| `frontend/uni-app` | uni-app CLI(Vue 3) | 用户端(H5/小程序/APP),用 `npm run dev:h5` 等 CLI 命令运行 |
|
||||
|
||||
前端也可用仓库根 `run.sh` 一键执行(自动补装 `node_modules`,等价于进入对应目录执行 npm 命令):
|
||||
|
||||
| 菜单 | 命令 | 作用 | 产物 / 地址 |
|
||||
|------|------|------|------------|
|
||||
| 11 | `./run.sh admin:dev` | 管理端开发测试启动 | http://localhost:1619 |
|
||||
| 12 | `./run.sh admin:build` | `npm run build:all`(主包 + 扩展 UMD)后发布 | `backend/public/admin/` |
|
||||
| 21 | `./run.sh uni:dev` | uniapp H5 开发测试启动 | http://localhost:5173 |
|
||||
| 22 | `./run.sh uni:build` | `npm run build:h5` 后发布 | `backend/public/`(index.html + assets/pages/static,PHP 侧文件不受影响) |
|
||||
|
||||
**注意**:前端 dev server 热更新可能失效,修改文件后页面若未更新,需手动重启服务。
|
||||
|
||||
## 八、登录账号
|
||||
|
||||
156
.crmeb/skills/project-standard-bootstrap/SKILL.md
Normal file
156
.crmeb/skills/project-standard-bootstrap/SKILL.md
Normal file
@ -0,0 +1,156 @@
|
||||
---
|
||||
name: project-standard-bootstrap
|
||||
description: 审计、建立或修复项目统一工程规范,使全栈项目具备一致的 .crmeb AI 规则、统一技能包骨架、按后端与各前端目录拆分的架构认知和核心功能开发文档、主流 AI 工具入口、backend/frontend 目录、标准命令、Docker 本地开发、跨层开发指引、测试 CI、文档和自动升级包。适用于新项目初始化、旧项目规范化、AI 接手治理、前端补齐后端与数据库能力、UniApp 多端开发和快速运行;不用于只润色单篇文档或绕过目标仓库已有约束。
|
||||
---
|
||||
|
||||
# 项目统一工程规范
|
||||
|
||||
把目标项目建设成“目录统一、命令统一、规则可发现、全栈协作完整、环境可复现、变更可验证、交付可升级”的工程系统。统一的是项目契约和质量底线;语言、框架及部署实现由真实技术栈决定。
|
||||
|
||||
## 工作模式
|
||||
|
||||
- **仅审计**:运行只读审计并输出 P0/P1/P2 差距,不修改文件。
|
||||
- **初始化**:为新项目生成缺失的目录、`.crmeb/` 基线、项目契约,以及按代码目录拆分的架构认知与开发文档技能。
|
||||
- **审计并完善**:对已有项目增量补齐规范、技能包骨架、目录说明、脚本、文档和验证入口。
|
||||
- **刷新维护**:修复事实漂移、失效命令、规则冲突、目录说明过期、迁移断链和交付缺口。
|
||||
|
||||
用户只要求分析时不得修改。用户要求建立、完善、统一或升级时,完成实施与验证闭环。
|
||||
|
||||
## 不可妥协的统一契约
|
||||
|
||||
1. AI 规则正文只维护在 `.crmeb/`;根 `AGENTS.md` 等工具入口只做薄指针。
|
||||
2. `.crmeb/skills/` 采用统一技能包骨架:每个技能目录为 `SKILL.md` + `references/`,`assets/`、`scripts/`、`agents/` 按需存在;`SKILL.md` 只承载索引与最小流程,正文放 `references/`,不得在技能根目录散落规则正文。
|
||||
3. 架构认知与核心功能开发文档必须按代码目录拆分:后端一个技能,每个可独立构建的前端应用各一个技能,与 `.crmeb/project.json` 的 `applications` 对齐;每个目录必须有「项目目录说明」,并登记到 `.crmeb/AGENTS.md` 的技能索引,使 AI 无需遍历全仓即可快速了解架构。
|
||||
4. 后端业务代码位于 `backend/`;可独立构建的前端位于 `frontend/<应用名>/`。
|
||||
5. 知识目录职责分离:当前事实在 `help/docs/`,AI 规则在 `.crmeb/`,过程文档与测试索引统一在 `tests/`(`plans/`、`reports/`、可分发模板);过程目录名由契约 `directories.process` 声明,审计、初始化与检查脚本读取该字段,不硬编码目录名。
|
||||
6. `.crmeb/project.json` 记录应用、服务、端口和标准命令映射,并由检查脚本防漂移。
|
||||
7. 项目提供 `doctor/setup/dev/stop/status/logs/check/test/build` 等价命令;实现可映射到现有 Make、Task、npm、Gradle、Compose 或脚本。
|
||||
8. 全栈变更同步检查数据、后端、API、权限、前端、测试、文档、迁移和升级包。
|
||||
9. 除“仅审计”外,必须根据当前项目后端真实运行环境自动生成或完善根 `docker-compose.yml`;同时生成所需 Dockerfile、配置和 `run.sh`,挂载本地源码、隔离依赖和数据卷,并区分安全停止与破坏性重置。
|
||||
10. 规则区分通用底线、技术栈 Profile 和项目专属约定,不把单一框架习惯强加给所有项目。
|
||||
11. 已发布版本、迁移和升级清单不可回改;可交付变更必须判断升级包触发条件。
|
||||
12. 不覆盖现有修改、不泄露凭据、不擅自清库、发布、推送或连接生产环境。
|
||||
13. Codex、Claude Code、Cursor、GitHub Copilot、Gemini CLI 等工具通过各自发现入口读取同一 `.crmeb` 真源,不复制规则正文。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 1. 发现与审计
|
||||
|
||||
先读取目标仓库所有适用的 `AGENTS.md`、README、贡献指南、manifest、lockfile、容器、CI、迁移和测试配置,并检查 Git 状态。运行:
|
||||
|
||||
```bash
|
||||
node <skill>/scripts/audit-project-standard.mjs <repository>
|
||||
node <skill>/scripts/analyze-project-environment.mjs <repository> --write
|
||||
```
|
||||
|
||||
环境分析器只读取 manifest、lockfile、非敏感配置样例和现有容器配置,不读取真实 `.env`。它输出运行时、应用、依赖服务、Docker 现状和动态文档缺口;结果必须由代码入口复核。事实以代码和可执行配置为准,旧文档只作线索。详细证据账本和优先级方法见 [文档与事实治理](references/documentation-governance.md)。
|
||||
|
||||
### 2. 建立项目契约
|
||||
|
||||
读取 [项目契约与标准命令](references/project-contract.md),建立 `.crmeb/project.json`。新项目或确认需要初始化时可先运行:
|
||||
|
||||
```bash
|
||||
node <skill>/scripts/init-project-standard.mjs <repository> --dry-run
|
||||
node <skill>/scripts/init-project-standard.mjs <repository>
|
||||
```
|
||||
|
||||
初始化工具只创建缺失项,不覆盖现有文件。生成后必须根据真实项目补全命令、应用、服务和端口,禁止把草稿契约当成已完成。
|
||||
|
||||
### 3. 建立规则体系
|
||||
|
||||
按任务读取相关参考,不要一次加载全部:
|
||||
|
||||
- 目录、模块边界和迁移:[目录与架构](references/directory-and-architecture.md)
|
||||
- 全栈功能修改:[全栈开发流程](references/fullstack-workflow.md)
|
||||
- 前端开发者跨到后端和数据库:[前端到后端与数据库](references/frontend-backend-database-bridge.md)
|
||||
- 后端分层、事务、任务:[后端规范](references/backend-standard.md)
|
||||
- 前端应用、状态、UI:[前端规范](references/frontend-standard.md)
|
||||
- API、数据库和契约:[API 与数据库](references/api-and-database.md)
|
||||
- 安全、日志、性能和运维:[安全与可观测性](references/security-and-observability.md)
|
||||
- 测试、质量门禁和 CI:[测试与 CI](references/testing-and-ci.md)
|
||||
- Docker 本地运行:[Docker 本地开发](references/docker-local-development.md)
|
||||
- 项目文档完整度:[文档覆盖矩阵](references/documentation-coverage.md)
|
||||
- 版本迁移和升级包:[自动升级包](references/upgrade-package.md)
|
||||
- 最终验收和旧项目迁移:[迁移与验收](references/migration-and-acceptance.md)
|
||||
- 多 AI 工具发现与单一真源:[AI 工具适配](references/ai-tool-adapters.md)
|
||||
|
||||
把通用规则模板从 `assets/project-template/.crmeb/` 增量适配到目标项目。规则必须结合真实框架补充,不能保留与实现冲突的模板语句。
|
||||
|
||||
### 4. 生成架构认知与目录说明技能
|
||||
|
||||
系统必须自动、快速地让 AI 与新成员了解项目架构,不依赖人工口述或全仓遍历。在 `.crmeb/skills/` 下按代码目录建立技能,目录职责与分层方法见 [目录与架构](references/directory-and-architecture.md):
|
||||
|
||||
- **按目录拆分**:后端对应一个技能;每个可独立构建的前端应用各对应一个技能(如管理端、移动端、Web 端),与 `.crmeb/project.json` 的 `applications` 一一对应,技能名可从应用 ID 派生。
|
||||
- **每个目录必须有「项目目录说明」**:目录树、逐目录职责、关键文件与入口、请求与数据流转链路、依赖边界、禁止放置的内容。
|
||||
- **每个目录必须有核心功能开发文档**:该端核心功能的入口、路由或 API、业务层、数据模型、扩展点、依赖服务,以及测试与验证命令。
|
||||
- **统一骨架**:技能目录为 `SKILL.md` + `references/`;`SKILL.md` 只做索引与最小流程,正文按主题拆到 `references/`,需要时再补 `assets/`、`scripts/`、`agents/`。
|
||||
- **骨架来源与元数据**:从 `assets/app-skill-skeleton/` 复制并替换占位符(`{{SKILL_NAME}}`、`{{APP_ID}}`、`{{APP_PATH}}`、`{{RUNTIME}}`);`SKILL.md` frontmatter 必须声明 `name`(与目录同名)、`description`、`appPath`、`directoryGuide`、`coreFeatures`,后两者指向技能内真实存在的文档。
|
||||
|
||||
初始化脚本会按 `applications` 自动生成技能骨架并登记 `.crmeb/AGENTS.md` 技能索引。脚本只搭骨架,正文必须由 AI 按当前代码补全;占位符或 TODO 未清除会被校验标为未完成。内容必须从当前代码、路由、manifest 和测试核对,禁止照搬模板或旧文档;架构事实变化时同步更新对应目录说明。已存在的技能只做增量补全,不覆盖用户已有改动。
|
||||
|
||||
### 5. 加载技术栈 Profile
|
||||
|
||||
只加载检测到的技术栈:
|
||||
|
||||
- PHP:[profiles/php.md](references/profiles/php.md)
|
||||
- Node.js 后端:[profiles/node.md](references/profiles/node.md)
|
||||
- Python:[profiles/python.md](references/profiles/python.md)
|
||||
- Java:[profiles/java.md](references/profiles/java.md)
|
||||
- Go:[profiles/go.md](references/profiles/go.md)
|
||||
- Vue:[profiles/vue.md](references/profiles/vue.md)
|
||||
- React:[profiles/react.md](references/profiles/react.md)
|
||||
- UniApp:[profiles/uniapp.md](references/profiles/uniapp.md)
|
||||
|
||||
Profile 用于发现权威版本、命令、常见目录和验证工具,不替代项目自身配置。
|
||||
|
||||
### 6. 强制生成 Docker 本地运行环境
|
||||
|
||||
初始化、审计并完善或刷新维护时,不论仓库原来是否存在 Docker,都必须先从后端 manifest、lockfile、版本文件、启动入口、扩展依赖、配置样例和外部服务提取真实运行要求,然后自动生成或语义完善根 `docker-compose.yml`。不得要求使用者手工选择通用 PHP、Node、Python、Java 或 Go 模板。
|
||||
|
||||
先预览,再写入缺失文件:
|
||||
|
||||
```bash
|
||||
node <skill>/scripts/generate-docker-environment.mjs <repository>
|
||||
node <skill>/scripts/generate-docker-environment.mjs <repository> --write-missing
|
||||
```
|
||||
|
||||
脚本只负责有把握的机械生成。如果脚本无法识别某种框架、启动命令、扩展或服务,AI 必须继续检查技术栈 Profile、代码入口、CI 和已有运行脚本,亲自完成 `docker-compose.yml`,不能把“脚本不支持”作为交付结果。已有 Compose 必须保留有效配置并按当前后端环境更新;不能仅因为文件已经存在就跳过。
|
||||
|
||||
最终 Compose 至少覆盖后端应用、实际必需的数据库/缓存、真实后台进程和健康检查;前端是否进入 Compose 由当前项目开发方式决定。Dockerfile、Nginx、入口脚本等辅助代码统一放入 `docker/`,根 `run.sh` 提供统一命令。生成后补全 `.crmeb/project.json` 的 Docker、服务、命令和文档字段。
|
||||
|
||||
### 7. 文档、运行与交付
|
||||
|
||||
使用 [文档模板](references/templates.md) 和 [文档覆盖矩阵](references/documentation-coverage.md),根据环境分析结果建立根入口、项目概览、开发配置、架构、前后端、API、数据库、测试、Docker、部署、运维、安全、排障、备份恢复和升级文档。正式事实、AI 规则、过程记录和测试索引职责分离:过程记录写入契约 `directories.process` 声明的目录(缺省 `tests/`,即与测试索引同目录);不得用空壳文档冒充完成。
|
||||
|
||||
必须扫描 `.crmeb/project.json` 和环境分析器识别出的每个后端、管理端、Web、UniApp、小程序或其他独立应用,逐一根据当前 manifest、构建配置、入口/路由、环境样例、API 客户端、测试和 CI 更新应用目录 `README.md` 与 `help/docs/applications/<应用ID>.md`。不能只更新总 README 或用一份前端说明覆盖多个项目。代码变更后重新核对受影响应用;无需改文档时也要在交付报告中说明检查依据。同一应用在 `.crmeb/skills/` 下的目录说明与核心功能开发文档,必须与该应用的 `README.md`、`help/docs/applications/<应用ID>.md` 事实一致,三者不得互相矛盾。
|
||||
|
||||
Docker 环境必须至少验证 Compose 解析、镜像构建、服务健康、源码挂载、前后端访问、测试和数据保留。触发升级交付时,生成 manifest、迁移、发布 payload、校验和和回滚说明,并在隔离环境验证旧版到新版。
|
||||
|
||||
### 8. 机器验收
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
node <skill>/scripts/check-project-standard.mjs <repository>
|
||||
```
|
||||
|
||||
再执行 `.crmeb/project.json` 声明的 `check`、`test`、`build` 和必要 smoke 命令。根据风险逐级报告文本检查、静态检查、构建测试、运行验证和生命周期验证,不得夸大覆盖范围。
|
||||
|
||||
检查脚本已校验技能骨架、`appPath` 应用覆盖、`directoryGuide` 与 `coreFeatures` 的存在性及索引登记;文档内容是否与当前代码一致仍需逐项核对,占位符或 TODO 未清除即视为未完成,不得用脚本通过代替。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 新开发者或 AI 能在 5 分钟内找到规则、配置和最短启动路径。
|
||||
- `.crmeb/skills/` 每个技能符合统一骨架(`SKILL.md` + `references/`);后端与每个独立前端应用都有对应技能,技能与 `.crmeb/project.json` 的 `applications` 一一对应。
|
||||
- 每个代码目录都有经当前代码核对的「项目目录说明」与核心功能开发文档;新 AI 会话无需遍历全仓即可定位任一核心功能的入口链路。
|
||||
- 同一应用的技能文档、应用目录 `README.md` 与 `help/docs/applications/<应用ID>.md` 事实一致,没有跨应用混用。
|
||||
- 标准命令可从仓库根执行或有明确映射,失败信息能定位缺少的环境或服务。
|
||||
- 后端、前端、API 和数据库改动有一致的全栈同步与测试清单。
|
||||
- Docker 日常启动不删除数据,破坏性重置需要明确动作和确认。
|
||||
- 非“仅审计”任务已经根据当前后端运行环境自动生成或更新根 `docker-compose.yml`;缺失该文件或仅保留通用占位模板不得声明完成。
|
||||
- `docker-compose.yml`、`run.sh` 和 `docker/` 与当前项目真实运行时、版本、服务、端口和进程一致,不保留未经核对的模板值。
|
||||
- 文档覆盖由项目能力动态计算;数据库、UniApp、worker、升级或多前端存在时,对应文档不可缺省。
|
||||
- 后端及每个独立前端应用都有从当前代码核对过的就近 README 和详细说明,命令、端口、API、构建与发布事实没有跨项目混用。
|
||||
- CI 执行与本地相同的 `check/test/build` 契约。
|
||||
- 触发升级的改动可生成可追溯、可校验、经过验证的候选升级包。
|
||||
- 自动检查通过;未通过项、动态验证缺口和需负责人决策的内容明确列出。
|
||||
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: {{SKILL_NAME}}
|
||||
description: {{APP_ID}} 应用的目录说明与核心功能开发文档,供 AI 在修改 {{APP_PATH}} 前快速了解该端架构、入口链路与验证命令
|
||||
appPath: {{APP_PATH}}
|
||||
directoryGuide: references/项目目录说明.md
|
||||
coreFeatures:
|
||||
- references/核心功能开发.md
|
||||
---
|
||||
|
||||
# {{APP_ID}} 应用开发技能
|
||||
|
||||
本技能覆盖 `{{APP_PATH}}`(运行时:{{RUNTIME}})。只描述该端事实,不混入其他应用。
|
||||
|
||||
## 读取顺序
|
||||
|
||||
1. [项目目录说明](references/项目目录说明.md):目录树、逐目录职责、关键入口、请求与数据链路、依赖边界。
|
||||
2. [核心功能开发](references/核心功能开发.md):核心功能的入口、路由或 API、业务层、数据模型、扩展点、测试与验证命令。
|
||||
|
||||
## 使用要求
|
||||
|
||||
- 两份文档必须从当前代码、路由、manifest 和测试核对,禁止照搬模板或旧文档。
|
||||
- 修改 `{{APP_PATH}}` 后同步更新受影响条目;事实变化未同步即视为未完成。
|
||||
- 与 `{{APP_PATH}}/README.md`、`help/docs/applications/{{APP_ID}}.md` 事实保持一致,三者不得互相矛盾。
|
||||
- 需要更细的专题时,在本技能 `references/` 下按主题新增文件,并在此处登记。
|
||||
@ -0,0 +1,28 @@
|
||||
# {{APP_ID}} 核心功能开发
|
||||
|
||||
> TODO:按当前代码补全,删除所有 TODO。每个核心功能都要能定位到入口与验证方式。
|
||||
|
||||
## 核心功能清单
|
||||
|
||||
| 功能 | 入口 | 关键文件 | 验证方式 |
|
||||
| --- | --- | --- | --- |
|
||||
| TODO | TODO | TODO | TODO |
|
||||
|
||||
## 功能详解
|
||||
|
||||
### TODO:功能名称
|
||||
|
||||
- 入口:TODO(页面、路由或 API 路径)
|
||||
- 业务层:TODO
|
||||
- 数据模型:TODO(表或字段、迁移位置)
|
||||
- 扩展点:TODO(插件、事件、钩子或配置)
|
||||
- 依赖服务:TODO
|
||||
- 测试与验证命令:TODO
|
||||
|
||||
## 开发流程
|
||||
|
||||
TODO:新增一个功能时按顺序要改哪些层,各层对应的命令与检查项。
|
||||
|
||||
## 常见坑
|
||||
|
||||
TODO:该端特有的约束、易错点和已知限制。
|
||||
@ -0,0 +1,29 @@
|
||||
# {{APP_ID}} 项目目录说明
|
||||
|
||||
> TODO:按当前代码补全,删除所有 TODO。只写 `{{APP_PATH}}` 的事实,不写计划。
|
||||
|
||||
## 目录树
|
||||
|
||||
TODO:列出 `{{APP_PATH}}` 下的目录与关键文件,标注每项职责。
|
||||
|
||||
## 逐目录职责
|
||||
|
||||
| 目录 | 职责 | 禁止放置 |
|
||||
| --- | --- | --- |
|
||||
| TODO | TODO | TODO |
|
||||
|
||||
## 关键入口
|
||||
|
||||
TODO:启动入口、路由注册、配置文件、依赖清单、构建配置的真实路径。
|
||||
|
||||
## 请求与数据链路
|
||||
|
||||
TODO:从请求或页面进入,到业务处理、数据访问和外部依赖的调用链,逐步给出真实文件位置。
|
||||
|
||||
## 依赖边界
|
||||
|
||||
TODO:本端可依赖哪些模块、不可反向依赖哪些模块、与哪些服务或应用交互。
|
||||
|
||||
## 变更影响面
|
||||
|
||||
TODO:改动本端目录时需要同步检查的 API、权限、数据库、测试、文档与升级包。
|
||||
@ -0,0 +1,41 @@
|
||||
# 项目 AI 开发规范
|
||||
|
||||
本目录是项目 AI 规则唯一正文来源。根级工具入口只指向本文,不复制规则。
|
||||
|
||||
## 处理任务前
|
||||
|
||||
1. 读取 `.crmeb/project.json`,确认应用、服务、路径和标准命令。
|
||||
2. 根据任务加载相关规则与技能,不整目录读取;改某端前先读该端技能的项目目录说明。
|
||||
3. 检查 Git 状态,区分用户已有改动与本任务改动。
|
||||
4. 从代码和配置核对事实,不把旧文档当作实现证据。
|
||||
|
||||
## 规则索引
|
||||
|
||||
| 规则 | 何时读取 |
|
||||
| --- | --- |
|
||||
| [架构与目录](rules/architecture.md) | 新增模块、调整依赖或移动目录 |
|
||||
| [代码规范](rules/coding.md) | 修改任何代码 |
|
||||
| [后端规范](rules/backend.md) | 修改 `backend/` |
|
||||
| [前端规范](rules/frontend.md) | 修改 `frontend/` |
|
||||
| [API 与数据](rules/api-and-data.md) | 修改接口、数据库、事件或消息 |
|
||||
| [安全与日志](rules/security.md) | 涉及鉴权、输入、日志、外部系统 |
|
||||
| [测试规范](rules/testing.md) | 新增功能、修复缺陷或改测试 |
|
||||
| [Git 与发布](rules/git-and-release.md) | 提交、构建、升级或发布 |
|
||||
| [文档规范](rules/documentation.md) | 新增或修改文档 |
|
||||
| [升级包规范](rules/upgrade.md) | 可交付代码、数据或前端产物变化 |
|
||||
| [全栈协作](rules/fullstack.md) | 前端功能涉及 API、权限、后端或数据库时 |
|
||||
|
||||
## 技能索引
|
||||
|
||||
按代码目录拆分,后端与每个独立前端应用各一个技能;项目目录说明和核心功能开发文档都在技能 `references/` 内。技能与 `.crmeb/project.json` 的 `applications` 一一对应。
|
||||
|
||||
| 技能 | 何时读取 |
|
||||
| --- | --- |
|
||||
|
||||
## 跨层协作
|
||||
|
||||
主要负责前端的开发者提出全栈需求时,先从前端 API 路径追踪到后端路由、业务层、数据访问和迁移。解释每层影响并给出真实文件位置;禁止用手工改数据库替代迁移,也不能因为前端隐藏按钮就省略服务端权限校验。
|
||||
|
||||
## 完成要求
|
||||
|
||||
执行项目契约中的 `check`、相关 `test`、必要 `build` 和 smoke。静态检查不等于运行验证;升级包未完成隔离升级测试时只能标记为候选。
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: api-and-data
|
||||
description: API 契约、数据库、迁移、事件和兼容性规则
|
||||
---
|
||||
|
||||
# API 与数据规则
|
||||
|
||||
- 请求校验、成功响应、错误码、分页、排序和过滤语义在项目内统一。
|
||||
- 高风险写接口和回调必须具备幂等、权限和重放防护。
|
||||
- OpenAPI/schema 等机器契约优先作为客户端类型和契约测试来源。
|
||||
- 表、字段、金额、时间、状态、软删除和多租户语义统一;索引对应真实查询。
|
||||
- 迁移只向前追加,已发布迁移不可回改;UPDATE/DELETE 必须有精确条件。
|
||||
- 新安装 schema 与所有历史迁移的最终结果一致,破坏性变更有兼容窗口和恢复计划。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: architecture
|
||||
description: 项目目录、模块边界与依赖方向强制规则
|
||||
---
|
||||
|
||||
# 架构与目录规则
|
||||
|
||||
- AI 规则正文只维护在 `.crmeb/`。
|
||||
- 后端业务代码位于 `backend/`,前端应用位于 `frontend/<应用名>/`。
|
||||
- 模块公开入口和依赖方向必须明确;禁止跨模块引用内部实现或形成循环依赖。
|
||||
- 领域逻辑不依赖 UI、Web 框架、数据库驱动或具体第三方 SDK。
|
||||
- 共享代码只有存在两个以上真实消费者时才抽取,并具有明确所有者。
|
||||
- 目录迁移必须同步 import、构建、测试、Docker、CI、部署和文档,并作为可回滚的独立变更。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: backend
|
||||
description: 后端分层、事务、缓存、队列与外部依赖规则
|
||||
---
|
||||
|
||||
# 后端规则
|
||||
|
||||
- 协议层只做输入适配、鉴权入口和输出映射;业务规则进入用例或领域层。
|
||||
- 持久化和第三方调用通过明确接口隔离,领域层不依赖具体实现。
|
||||
- 事务位于业务用例边界,避免在事务中执行不受控网络调用。
|
||||
- 写操作定义并发、幂等和重试;缓存定义 key、TTL、失效责任和一致性。
|
||||
- 队列与任务定义超时、退避、死信、补偿和重复消费策略。
|
||||
- 错误分层映射,结构化日志脱敏并携带请求或任务关联 ID。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: coding
|
||||
description: 跨技术栈代码质量、命名、类型、注释与依赖规则
|
||||
---
|
||||
|
||||
# 代码规范
|
||||
|
||||
- 遵守项目格式化、lint、类型和框架工具;不引入并行风格。
|
||||
- 名称表达领域含义,避免无上下文缩写、布尔歧义和伪通用工具类。
|
||||
- 函数和模块保持单一职责,副作用、输入、输出和失败方式清晰。
|
||||
- 优先类型和可执行契约,不用注释重复代码;注释解释原因、不变量和风险。
|
||||
- 禁止吞异常、调试输出、硬编码秘密、无边界查询和未经说明的全局状态。
|
||||
- 新依赖需说明用途、许可证与维护状态,并写入正确 manifest 和 lockfile。
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: documentation
|
||||
description: 项目事实、AI 规则、过程记录和测试索引边界规则
|
||||
---
|
||||
|
||||
# 文档规则
|
||||
|
||||
- `help/docs/` 描述当前项目事实,`.crmeb/` 维护 AI 规则和技能。
|
||||
- `tests/` 保存方案、决策和验收过程,并提供测试统一入口;过程文档与测试索引同目录,不另建平行的过程文档目录。
|
||||
- 同一事实只有一个权威正文,其他入口只摘要和链接。
|
||||
- 命令写明执行目录、前置条件、成功信号和已验证环境。
|
||||
- 代码、配置和可执行结果优先于旧文档;冲突时修正文档并记录证据。
|
||||
- 新增或移动文档后检查链接、索引和易漂移事实。
|
||||
- 后端及每个独立前端应用必须有就近 `README.md`,并链接到 `help/docs/applications/<应用ID>.md`;不能用一份总说明替代多个应用。
|
||||
- 代码变更影响应用职责、版本、命令、配置、端口、路由/API、数据、构建或发布方式时,同一变更检查并更新对应应用说明;不影响时记录已检查无需更新。
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: frontend
|
||||
description: 前端应用、组件、状态、接口、体验与构建规则
|
||||
---
|
||||
|
||||
# 前端规则
|
||||
|
||||
- 可独立构建的应用位于 `frontend/<应用名>/`,共享包通过 workspace 显式依赖。
|
||||
- 页面负责场景编排,业务逻辑进入 service、store、hook 或 composable。
|
||||
- 请求、响应、路由、组件属性和事件保持类型安全,并与 API 契约同步。
|
||||
- 页面覆盖 loading、empty、error、无权限和成功反馈,危险操作必须确认。
|
||||
- 使用设计令牌与共享组件,满足响应式、键盘操作和可访问名称要求。
|
||||
- 构建变量不得承载秘密;发布物从锁定依赖的干净环境生成。
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: fullstack
|
||||
description: 前端到后端、API、权限、数据库和升级交付的跨层同步规则
|
||||
---
|
||||
|
||||
# 全栈协作规则
|
||||
|
||||
- 从前端 API 函数追踪 request 封装、后端路由、Controller、Service、Repository 和 migration,不根据字段名猜实现。
|
||||
- 前端校验负责体验,服务端负责安全校验、权限、所有权和业务不变量。
|
||||
- 新增持久化字段同步 migration、Model、Service、API schema、前端类型、表单、列表、测试和升级包。
|
||||
- 列表接口由后端分页、校验筛选和限制排序;数据库索引对应实际查询。
|
||||
- 权限同步菜单、路由、中间件、角色数据和前端控制,隐藏按钮不能替代服务端鉴权。
|
||||
- 手工改库只能用于经授权的临时诊断,正式交付必须通过可追踪迁移。
|
||||
- 对不熟悉后端的协作者,用页面/API语言解释影响,提供真实路径和最小验证命令。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: git-and-release
|
||||
description: Git 变更边界、提交、版本、构建与发布规则
|
||||
---
|
||||
|
||||
# Git 与发布规则
|
||||
|
||||
- 修改前检查工作区,保留用户已有变更;提交按主题分离。
|
||||
- 不提交秘密、运行数据、缓存、依赖目录、临时产物和本地 IDE 配置。
|
||||
- 版本号来自唯一来源,已发布 tag、manifest 和迁移不可回改。
|
||||
- 构建和发布从干净提交执行,制品记录来源提交和校验和。
|
||||
- 生成本地制品不等于授权提交、推送、打 tag、发布或上传。
|
||||
- 发布说明包含变更、兼容性、迁移、备份、回滚边界和验证结果。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: security
|
||||
description: 凭据、鉴权、输入、上传、日志和供应链安全规则
|
||||
---
|
||||
|
||||
# 安全与日志规则
|
||||
|
||||
- 密钥、令牌、Cookie、真实账号和生产数据不得进入仓库、日志或升级包。
|
||||
- 服务端对每个资源执行认证、授权和对象所有权校验。
|
||||
- SQL 参数化;上传校验内容、大小、类型、位置和访问权限。
|
||||
- 回调和支付验证签名、时效、重放与幂等。
|
||||
- 日志结构化且脱敏,异常响应不泄露堆栈和内部配置。
|
||||
- 依赖使用锁文件与可信源;高风险漏洞必须在发布前处理或明确阻断。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: testing
|
||||
description: 测试分层、回归、数据隔离与质量门禁规则
|
||||
---
|
||||
|
||||
# 测试规则
|
||||
|
||||
- 新功能和缺陷修复覆盖业务不变量、权限、失败路径和回归场景。
|
||||
- 单元、集成、契约、端到端和生命周期测试职责分明,不互相冒充。
|
||||
- 测试数据可重复创建和清理,不依赖个人环境或生产数据。
|
||||
- `check/test/build` 在本地与 CI 使用同一入口和锁定依赖。
|
||||
- 被跳过、环境受限或未运行的测试必须明确报告,不能写成通过。
|
||||
- 升级相关变更至少验证最近稳定版到目标版,声明跨级支持时验证完整迁移链。
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
---
|
||||
name: upgrade
|
||||
description: 增量升级包、迁移、校验、备份和失败恢复规则
|
||||
---
|
||||
|
||||
# 升级包规则
|
||||
|
||||
- 后端、前端、数据库或运行配置发生可交付变化时判断升级包触发条件。
|
||||
- 核心升级包与插件/扩展包分离,按所有权收集文件和迁移。
|
||||
- manifest 记录版本范围、来源提交、组件、依赖、迁移、备份和回滚边界。
|
||||
- 数据迁移顺序稳定、尽可能幂等;已发布版本和迁移不可回改。
|
||||
- 包含文件清单与 SHA-256,禁止秘密、运行数据、上传文件和数据库备份。
|
||||
- 未完成旧版隔离升级、健康检查和失败恢复验证时只能标记为候选包。
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
description: 项目统一工程规范入口;处理任何代码、文档、测试或发布任务时使用
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
项目规则唯一正文是 @../../.crmeb/AGENTS.md 。先读取该入口及其为当前任务指定的规则,不在 Cursor 目录维护第二份规则。修改后执行 `.crmeb/project.json` 声明的最小验证命令,并准确报告未验证项。
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
# GitHub Copilot 项目指令
|
||||
|
||||
开始修改前先读取 [.crmeb/AGENTS.md](../.crmeb/AGENTS.md)。项目规则正文只维护在 `.crmeb/`,本文件不复制规则。按任务读取对应规则,保留已有工作区改动,并执行 `.crmeb/project.json` 声明的检查、测试和构建命令。
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
# AI 开发入口
|
||||
|
||||
开始处理本仓库任务前,读取 [.crmeb/AGENTS.md](.crmeb/AGENTS.md),再按其索引加载与目标文件和任务相关的规则。
|
||||
|
||||
项目事实以代码、可执行配置和正式文档为准。修改前保留已有工作区改动,完成后执行 `.crmeb/project.json` 声明的最小验证命令,并准确报告未验证项。
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
@.crmeb/AGENTS.md
|
||||
|
||||
# Claude Code 入口
|
||||
|
||||
项目规则正文只维护在 `.crmeb/`。先执行上面的导入,再按 `.crmeb/AGENTS.md` 的索引加载与当前任务有关的规则,不复制或另建第二套规范。
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
@./.crmeb/AGENTS.md
|
||||
|
||||
# Gemini CLI 入口
|
||||
|
||||
项目规则正文只维护在 `.crmeb/`。先加载上面的项目规则,再按任务选择相关规则;修改后运行项目契约声明的最小验证命令。
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
# 项目入口
|
||||
|
||||
本仓库采用统一工程规范。项目业务介绍、真实技术栈和访问地址应在初始化后补充到本页。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
./run.sh doctor
|
||||
./run.sh setup
|
||||
./run.sh dev
|
||||
./run.sh status
|
||||
```
|
||||
|
||||
Windows PowerShell 使用对应的 `./run.ps1` 命令。标准命令的真实实现记录在 `.crmeb/project.json`。
|
||||
|
||||
## 导航
|
||||
|
||||
- AI 开发规则:[.crmeb/AGENTS.md](.crmeb/AGENTS.md)
|
||||
- 项目正式文档:[help/docs/README.md](help/docs/README.md)
|
||||
- 过程文档与测试索引:[tests/README.md](tests/README.md)
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
# 项目文档
|
||||
|
||||
本目录描述项目当前已实现事实。应用 Skill 后,根据代码和配置补齐项目概览、架构、开发运行、API、数据库、Docker、部署、运维和升级文档;不保留未经验证的模板结论。
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
# Project AI entrypoints
|
||||
|
||||
Project rules: .crmeb/AGENTS.md
|
||||
Machine-readable project contract: .crmeb/project.json
|
||||
Human project documentation: help/docs/README.md
|
||||
|
||||
Always read the project rules before changing code. Tool-specific files are discovery adapters only; `.crmeb/` is the single source of truth.
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
& node (Join-Path $scriptDir 'scripts/project-command.mjs') @args
|
||||
exit $LASTEXITCODE
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
exec node "$script_dir/scripts/project-command.mjs" "$@"
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const composeFile = path.join(root, 'docker-compose.yml')
|
||||
const action = process.argv[2]
|
||||
const args = process.argv.slice(3)
|
||||
|
||||
function run(command, commandArgs, options = {}) {
|
||||
const result = spawnSync(command, commandArgs, { cwd: root, env: process.env, stdio: 'inherit', ...options })
|
||||
if (result.error) {
|
||||
console.error(result.error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
if (result.status !== 0) process.exit(result.status ?? 1)
|
||||
}
|
||||
|
||||
function compose(commandArgs) {
|
||||
if (!fs.existsSync(composeFile)) {
|
||||
console.error('缺少 docker-compose.yml,请先生成或完善 Docker 本地环境。')
|
||||
process.exit(2)
|
||||
}
|
||||
run('docker', ['compose', '-f', composeFile, ...commandArgs])
|
||||
}
|
||||
|
||||
function requireService(value) {
|
||||
if (!value || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value)) {
|
||||
console.error('请提供合法的 Compose 服务名。')
|
||||
process.exit(2)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'doctor':
|
||||
run('docker', ['version'])
|
||||
run('docker', ['compose', 'version'])
|
||||
compose(['config', '--quiet'])
|
||||
break
|
||||
case 'setup':
|
||||
compose(['build'])
|
||||
break
|
||||
case 'dev':
|
||||
compose(['up', '-d', '--build', '--remove-orphans'])
|
||||
compose(['ps'])
|
||||
break
|
||||
case 'restart':
|
||||
compose(args.length ? ['restart', ...args.map(requireService)] : ['restart'])
|
||||
break
|
||||
case 'stop':
|
||||
compose(['stop'])
|
||||
break
|
||||
case 'down':
|
||||
compose(['down', '--remove-orphans'])
|
||||
break
|
||||
case 'status':
|
||||
compose(['ps'])
|
||||
break
|
||||
case 'logs':
|
||||
compose(['logs', '--tail', '200', '-f', ...args.map(requireService)])
|
||||
break
|
||||
case 'shell': {
|
||||
const service = requireService(args[0])
|
||||
compose(['exec', service, 'sh'])
|
||||
break
|
||||
}
|
||||
case 'reset-data':
|
||||
if (!args.includes('--yes')) {
|
||||
console.error('该操作会删除本地 Compose 数据卷。确认后使用:./run.sh reset-data --yes')
|
||||
process.exit(2)
|
||||
}
|
||||
compose(['down', '--volumes', '--remove-orphans'])
|
||||
break
|
||||
default:
|
||||
console.error('用法:node scripts/docker-project.mjs <doctor|setup|dev|restart|stop|down|status|logs|shell|reset-data> [服务名|--yes]')
|
||||
process.exit(2)
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const contractPath = path.join(root, '.crmeb/project.json')
|
||||
const requested = process.argv[2]
|
||||
const aliases = {
|
||||
'reset-data': 'resetData',
|
||||
'upgrade-plan': 'upgradePlan',
|
||||
'upgrade-package': 'upgradePackage',
|
||||
'upgrade-verify': 'upgradeVerify',
|
||||
}
|
||||
const commandName = aliases[requested] || requested
|
||||
|
||||
if (!requested || ['-h', '--help', 'help'].includes(requested)) {
|
||||
console.log('用法:./run.sh <doctor|setup|dev|restart|stop|down|status|logs|shell|check|test|smoke|build|reset-data|upgrade-plan|upgrade-package|upgrade-verify> [参数]')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(contractPath)) {
|
||||
console.error('缺少 .crmeb/project.json,请先完成项目规范初始化。')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
let contract
|
||||
try {
|
||||
contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`无法解析 .crmeb/project.json:${error.message}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const command = contract.commands?.[commandName]
|
||||
if (!command) {
|
||||
console.error(`标准命令尚未配置:${requested}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const rawArgs = process.argv.slice(3)
|
||||
for (const value of rawArgs) {
|
||||
if (!/^[A-Za-z0-9._/@:+-]+$/.test(value)) {
|
||||
console.error(`参数包含不允许的字符:${value}`)
|
||||
process.exit(2)
|
||||
}
|
||||
}
|
||||
const extraArgs = rawArgs.join(' ')
|
||||
const result = spawnSync(extraArgs ? `${command} ${extraArgs}` : command, {
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
if (result.error) {
|
||||
console.error(result.error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
process.exit(result.status ?? 1)
|
||||
@ -0,0 +1,14 @@
|
||||
# 过程文档与测试索引
|
||||
|
||||
本目录同时承担两项职责:保存方案、决策、迁移记录和验收报告等过程文档,并提供测试统一索引。
|
||||
|
||||
```text
|
||||
tests/
|
||||
README.md 本文件:过程文档导航 + 测试索引
|
||||
plans/ 待评审、进行中的方案
|
||||
reports/ 检查、验收、回归报告
|
||||
```
|
||||
|
||||
过程文档顶部标注性质(待评审 / 已实施 / 已归档)与核对日期;已实施事实必须同步到正式文档或 `.crmeb/` 规则,过程记录不得冒充当前实现。失效后删除。
|
||||
|
||||
测试索引登记各应用的静态检查、单元、集成、契约、端到端和生命周期测试位置、命令、前置依赖与覆盖边界。测试脚本按端落位,本目录不存放测试脚本。
|
||||
@ -0,0 +1,38 @@
|
||||
# AI 工具适配
|
||||
|
||||
## 单一真源原则
|
||||
|
||||
所有项目规则正文维护在 `.crmeb/AGENTS.md`、`.crmeb/rules/` 和 `.crmeb/skills/`。工具专属文件只负责发现和导入,不复制长期规则,避免 Codex、Claude、Cursor、Copilot 和 Gemini 得到不同结论。
|
||||
|
||||
## 默认适配矩阵
|
||||
|
||||
| 工具 | 仓库入口 | 适配方式 |
|
||||
| --- | --- | --- |
|
||||
| Codex 及 AGENTS 兼容工具 | `AGENTS.md` | Markdown 薄入口指向 `.crmeb/AGENTS.md` |
|
||||
| Claude Code | `CLAUDE.md` | 使用 `@.crmeb/AGENTS.md` 导入真源 |
|
||||
| Cursor | `.cursor/rules/project-standard.mdc` | `alwaysApply: true`,引用 `.crmeb/AGENTS.md` |
|
||||
| GitHub Copilot | `.github/copilot-instructions.md` | 仓库级指令要求先读取 `.crmeb/AGENTS.md` |
|
||||
| Gemini CLI | `GEMINI.md` | 使用 `@./.crmeb/AGENTS.md` 导入真源 |
|
||||
| 通用抓取或其他工具 | `llms.txt` | 只给项目入口和 `.crmeb` 真源路径 |
|
||||
|
||||
这些路径依据各工具官方发现机制设置:[Codex AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md)、[Claude Code memory](https://code.claude.com/docs/en/memory)、[Cursor Rules](https://docs.cursor.com/context/rules)、[GitHub Copilot instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions-in-your-ide/add-repository-instructions-in-your-ide)、[Gemini CLI context](https://google-gemini.github.io/gemini-cli/docs/cli/gemini-md.html)。
|
||||
|
||||
## 生成要求
|
||||
|
||||
- 初始化时创建缺失入口,不覆盖项目已有工具指令。
|
||||
- 已有入口包含项目专属内容时,保留内容并增加指向 `.crmeb` 的引导;冲突条款需人工或 AI 语义合并。
|
||||
- 默认使用入库薄文件而不是软链接,保证 Windows 和压缩包场景可用;项目明确采用软链接时验证 Git 与目标工具能正确发现。
|
||||
- 工具专属入口保持简短,避免每次会话加载完整规则库;具体规则由 `.crmeb/AGENTS.md` 按任务路由。
|
||||
- 子目录需要特殊约定时,优先使用工具支持的作用域机制或子目录入口,但正文仍链接到 `.crmeb/scopes/`。
|
||||
|
||||
## 验证
|
||||
|
||||
1. 静态检查所有入口存在且包含 `.crmeb/AGENTS.md`。
|
||||
2. Codex/AGENTS 工具确认根入口生效。
|
||||
3. Claude Code 用 `/context` 检查 `CLAUDE.md` 和导入文件。
|
||||
4. Gemini CLI 用 `/memory show` 或 `/memory list` 检查 `GEMINI.md`。
|
||||
5. Cursor 在 Rules 设置或上下文中确认项目规则生效。
|
||||
6. Copilot 用仓库上下文发起一次规则相关问题,确认读取仓库级指令。
|
||||
|
||||
若无法实际启动某工具,只能报告“入口静态有效”,不得声称已验证该工具发现成功。
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
# API 与数据库规范
|
||||
|
||||
## API 契约
|
||||
|
||||
- HTTP/API 风格、版本策略和基础路径在项目中唯一定义。
|
||||
- 请求校验覆盖类型、长度、格式、枚举、范围和业务权限;未知字段策略明确。
|
||||
- 成功与错误响应稳定,错误含机器码和可安全展示的信息,不泄露内部异常。
|
||||
- 列表统一分页、排序和过滤语义;大结果集禁止无边界返回。
|
||||
- 创建、支付、回调和重试接口定义幂等键或等价机制。
|
||||
- OpenAPI、GraphQL schema、protobuf 等机器契约是客户端类型和契约测试的优先来源。
|
||||
- 破坏性变更先提供兼容窗口、弃用说明和升级路径。
|
||||
|
||||
## 数据库
|
||||
|
||||
- 表、字段、约束和索引遵守项目语言和数据库约定,并在规则中明确。
|
||||
- 主键、时间、金额、状态、软删除和多租户字段必须有统一类型与语义。
|
||||
- 索引来自实际查询模式;唯一业务约束优先由数据库保障。
|
||||
- 迁移只向前追加,已发布迁移不回改;大表采用分阶段兼容迁移。
|
||||
- UPDATE/DELETE 有精确条件;批量操作评估锁、事务大小和恢复方式。
|
||||
- 新安装 schema、种子和所有历史迁移必须得到一致结果。
|
||||
|
||||
## 契约联动
|
||||
|
||||
数据结构变化必须同步检查:Model/Entity、Repository、Service、API schema、前端类型、表单、列表、导入导出、权限、缓存、事件、测试、新安装和升级迁移。
|
||||
|
||||
## 验证
|
||||
|
||||
- 校验迁移顺序、重复执行行为和失败恢复。
|
||||
- 对关键查询查看执行计划或等价分析,确认索引命中和结果边界。
|
||||
- 契约测试覆盖必填/可选字段、错误、分页、权限和版本兼容。
|
||||
- 禁止把生产数据复制到开发或测试环境,脱敏数据也需遵守授权和保留策略。
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
# 后端通用规范
|
||||
|
||||
## 架构边界
|
||||
|
||||
- 依赖方向必须单向且可描述,领域层不依赖 Web、数据库驱动或具体第三方 SDK。
|
||||
- Controller/Handler 负责协议适配、鉴权入口和参数转换,不承载核心业务。
|
||||
- Service/UseCase 编排业务和事务;Repository/DAO 隔离持久化;外部服务通过 Adapter/Gateway 隔离。
|
||||
- 跨模块调用通过公开契约、事件或接口,不引用对方内部实现。
|
||||
- 框架确有不同最佳实践时,在 `.crmeb/rules/backend.md` 写清实际分层和例外。
|
||||
|
||||
## 数据与事务
|
||||
|
||||
- 事务边界位于业务用例层,事务内避免不受控网络调用和长耗时处理。
|
||||
- 写操作明确并发、幂等和重试语义;支付、库存、回调、任务尤其如此。
|
||||
- 查询必须分页,避免 N+1、无边界全表加载和 `SELECT *` 类不稳定契约。
|
||||
- 缓存必须定义 key、TTL、失效责任和一致性策略,不把缓存当权威数据源。
|
||||
|
||||
## 输入、错误和日志
|
||||
|
||||
- 所有外部输入在边界校验、归一化和限流;内部方法仍维护业务不变量。
|
||||
- 业务错误、校验错误、权限错误、依赖故障和未知异常分层映射。
|
||||
- 不吞异常、不空 catch、不把堆栈或敏感数据返回客户端。
|
||||
- 结构化日志包含请求/任务关联 ID、模块、动作和结果;密码、令牌、Cookie、身份证件等必须脱敏。
|
||||
|
||||
## 异步任务
|
||||
|
||||
- 队列消息和事件 payload 版本化,消费者可幂等处理和安全重试。
|
||||
- 定时任务和 worker 明确超时、重试、退避、死信和人工补偿。
|
||||
- 进程入口纳入标准 status/logs/healthcheck,升级后验证重启与积压情况。
|
||||
|
||||
## 验证
|
||||
|
||||
- 领域规则优先单元测试;数据库和外部适配使用集成测试。
|
||||
- 权限、并发、幂等、事务回滚、重试和失败补偿必须覆盖高风险路径。
|
||||
- 使用该语言 Profile 中项目真实存在的 lint、类型、测试和构建工具。
|
||||
|
||||
## 后端应用说明文档
|
||||
|
||||
- 后端目录必须维护就近 `README.md`,并链接到 `help/docs/applications/<后端ID>.md` 的详细说明;多后端应用或服务要分别建档。
|
||||
- 根据当前 manifest/lockfile、框架入口、路由、配置样例、迁移、worker/scheduler、测试和 CI 核对技术栈、版本、分层、命令、端口、依赖服务和健康信号。
|
||||
- 修改运行时、扩展、启动入口、路由/API、配置、数据库、异步进程、测试或部署方式时,同一变更检查并更新对应说明。
|
||||
- 旧说明与代码不一致时,以代码、配置和实际执行结果为准,并明确尚未动态验证的内容。
|
||||
@ -0,0 +1,156 @@
|
||||
# 目录与架构
|
||||
|
||||
本文给出可迁移的职责模型。目标项目已有成熟约定时,应映射职责而不是机械改名。
|
||||
|
||||
## 四层职责模型
|
||||
|
||||
| 层级 | 回答的问题 | 典型内容 | 生命周期 |
|
||||
| --- | --- | --- | --- |
|
||||
| 项目事实层 | 项目当前如何组成、运行和部署 | 项目概览、架构、开发、API、数据、部署、运维 | 随代码同步更新 |
|
||||
| AI 协作层 | AI 应如何理解、修改和验证 | `AGENTS.md`、规则、技能、目录约定、验证要求 | 随工程规范更新 |
|
||||
| 过程记录层 | 为什么改、如何迁移、验收结果是什么 | 方案、ADR、评审、迁移记录、检查报告 | 完成后归档或删除 |
|
||||
| 测试验证层 | 如何证明系统仍然正确 | 测试地图、测试数据、执行命令、CI 说明、覆盖边界 | 随测试体系更新 |
|
||||
|
||||
同一事实只在一个层级拥有正文。例如:
|
||||
|
||||
- “Node 版本要求”归开发运行指南;AI 规则只链接过去。
|
||||
- “目录禁止放什么”归工程或 AI 规则;根 README 只给入口。
|
||||
- “某重构尚未实施”归方案;落地后把当前结果更新到正式文档。
|
||||
- “如何执行集成测试”归测试索引;方案只记录为什么增加它。
|
||||
|
||||
## 推荐目录
|
||||
|
||||
没有既有约定时,可从以下最小结构开始:
|
||||
|
||||
```text
|
||||
repository/
|
||||
README.md 项目单一入口
|
||||
AGENTS.md AI 发现入口,保持简短
|
||||
backend/ 全部后端代码与后端测试
|
||||
frontend/ 全部前端应用与前端共享包
|
||||
admin/ 示例:管理端(按真实应用命名)
|
||||
web/ 示例:桌面 Web 端
|
||||
mobile/ 示例:移动端/H5/跨端应用
|
||||
help/docs/
|
||||
README.md 正式文档索引
|
||||
overview.md 产品定位、角色、能力边界
|
||||
architecture.md 系统结构、模块关系、关键链路
|
||||
development.md 环境、安装、配置、启动、构建
|
||||
testing.md 测试地图、命令、依赖与覆盖边界
|
||||
api.md API 约定或权威 API 文档入口
|
||||
database.md 数据模型、迁移、备份恢复边界
|
||||
deployment.md 部署、升级、回滚、健康检查
|
||||
operations.md 日志、监控、任务、常见故障
|
||||
docker-development.md Docker 本地开发、调试与数据维护
|
||||
upgrade.md 升级包生成、安装、验证与失败恢复
|
||||
.crmeb/ AI 规则、技能与子智能体唯一来源
|
||||
AGENTS.md 规则和技能索引
|
||||
rules/
|
||||
skills/
|
||||
agents/
|
||||
tests/ 过程记录层与测试验证层同目录,名称由契约 directories.process 声明(缺省 tests/)
|
||||
README.md 过程文档导航 + 测试统一入口;正文可链接 help/docs/testing.md
|
||||
plans/
|
||||
decisions/
|
||||
reports/
|
||||
docker/ 镜像与服务配置,不存放业务源码
|
||||
compose.yaml 本地开发服务编排入口
|
||||
upgrades/
|
||||
versions/ 版本迁移与升级定义真源
|
||||
packages/ 自动生成的升级包;通常由 .gitignore 排除
|
||||
```
|
||||
|
||||
只创建有真实内容和明确维护者的文件。小型项目可合并 `overview`、`architecture` 和 `development`;无数据库或部署职责的项目不应制造空文档。
|
||||
|
||||
## 代码目录统一规范
|
||||
|
||||
### 后端 `backend/`
|
||||
|
||||
- 所有服务端业务代码、依赖清单、迁移和后端测试统一位于 `backend/`。
|
||||
- 单体项目直接在 `backend/` 内按框架惯例组织;多服务项目使用 `backend/apps/<服务名>/`,共享库使用 `backend/packages/` 或目标语言的标准 workspace 结构。
|
||||
- 不在仓库根目录散落 `api/`、`server/`、`service/` 等与 `backend/` 并列的后端业务目录。
|
||||
- 后端命令必须说明是在仓库根还是 `backend/` 执行;Docker 容器内工作目录也应映射到该边界。
|
||||
|
||||
### 前端 `frontend/`
|
||||
|
||||
- 所有浏览器端、管理端、H5、移动端、桌面端等前端工程统一位于 `frontend/`。
|
||||
- 每个可独立安装、构建或发布的客户端使用 `frontend/<应用名>/`,例如 `frontend/admin/`、`frontend/web/`、`frontend/mobile/`。
|
||||
- 多应用共享组件和 SDK 使用 `frontend/packages/` 或 `frontend/shared/`;其依赖关系由 workspace 配置显式声明。
|
||||
- 不在根目录散落 `admin/`、`web/`、`h5/`,也不把前端构建产物放进 `backend/` 源码。若部署必须复制静态产物,应由构建或发布脚本完成并记录来源。
|
||||
|
||||
### 已有项目迁移
|
||||
|
||||
新项目直接遵守上述命名。已有项目目录不一致时,先盘点以下引用再迁移:
|
||||
|
||||
- import、autoload、workspace 与构建入口;
|
||||
- Docker build context、volume 和工作目录;
|
||||
- CI、部署、Nginx、进程管理与脚本路径;
|
||||
- 文档、测试、代码生成器和 IDE 配置。
|
||||
|
||||
目录迁移应作为独立变更,保留可回滚方案并运行构建、测试和启动验证。用户只要求完善文档时,只记录目标结构和迁移方案,不擅自移动业务代码。
|
||||
|
||||
## 入口设计
|
||||
|
||||
### 根 README
|
||||
|
||||
根 README 是导航页,不是所有内容的复制品。它至少应让读者快速得到:
|
||||
|
||||
1. 项目解决什么问题;
|
||||
2. 主要目录或子项目是什么;
|
||||
3. 最短可运行路径是什么;
|
||||
4. 开发、测试、部署、贡献和 AI 规则在哪里;
|
||||
5. 哪些事项必须先阅读。
|
||||
|
||||
### AI 入口
|
||||
|
||||
根 `AGENTS.md` 或工具发现文件应保持短小,只负责:
|
||||
|
||||
- 指向规则唯一来源;
|
||||
- 说明更深目录存在作用域更小的规则;
|
||||
- 告诉 AI 如何选择技能和验证方式;
|
||||
- 禁止明显高风险行为。
|
||||
|
||||
不要在多个工具目录复制整套规则。工具必须使用不同发现路径时,优先使用链接、薄入口或生成校验机制,并明确唯一维护源。
|
||||
|
||||
AI 规则正文统一维护在 `.crmeb/`:
|
||||
|
||||
- `.crmeb/AGENTS.md`:项目总入口与索引;
|
||||
- `.crmeb/rules/`:违反即错误的强制规则;
|
||||
- `.crmeb/skills/`:任务操作流程和专项手册;
|
||||
- `.crmeb/agents/`:可选的专项角色说明;
|
||||
- `.crmeb/scopes/`:可选的子项目或目录补充约定。
|
||||
|
||||
根 `AGENTS.md`、`CLAUDE.md`、`llms.txt` 等仅作为发现入口并指向 `.crmeb/AGENTS.md`,不复制正文。若目标工具需要专属目录,使用薄入口或软链接,并提供链接完整性检查。
|
||||
|
||||
默认生成 Codex/AGENTS、Claude Code、Cursor、GitHub Copilot 和 Gemini CLI 的薄入口,具体路径和验证方式见 [AI 工具适配](ai-tool-adapters.md)。跨平台项目默认使用真实薄文件,只有确认 Git 与开发环境支持时才使用软链接。
|
||||
|
||||
### 文档索引
|
||||
|
||||
按用户任务组织导航,例如“第一次运行”“理解架构”“开发接口”“写测试”“部署排障”,而不是只按字母或文件名罗列。
|
||||
|
||||
## 权威来源矩阵
|
||||
|
||||
为易漂移事实定义权威来源:
|
||||
|
||||
| 事实 | 推荐权威来源 | 文档写法 |
|
||||
| --- | --- | --- |
|
||||
| 语言和框架版本 | manifest、lockfile、运行镜像 | 标注来源,避免凭记忆填写 |
|
||||
| 启动/构建命令 | package scripts、Makefile、任务配置 | 给出执行目录和前置依赖 |
|
||||
| 服务端口 | 环境样例、容器、服务配置 | 区分默认值和可覆盖值 |
|
||||
| API 路由 | 路由定义、OpenAPI 生成物 | 链接权威定义,避免手抄全量接口 |
|
||||
| 数据表和迁移 | migration/schema | 自动生成或校验数量与关键对象 |
|
||||
| 模块/插件清单 | 注册器、模块目录、配置 | 用脚本校验,说明启用条件 |
|
||||
| 测试范围 | 测试配置、目录、CI | 说明已覆盖和未覆盖部分 |
|
||||
| 部署流程 | 实际部署配置、流水线 | 区分本地示例与生产流程 |
|
||||
| Docker 本地开发 | `compose.yaml`、Dockerfile、环境样例 | 说明源码挂载、端口、健康检查、数据卷和重建条件 |
|
||||
| 版本与升级 | 版本文件、发布标签、迁移和升级 manifest | 已发布记录不可回改,生成包与真源可追溯 |
|
||||
|
||||
## 生命周期
|
||||
|
||||
1. 设计阶段:方案进入过程记录层,明确状态、负责人或核对日期。
|
||||
2. 实施阶段:代码、测试和正式文档在同一变更中更新。
|
||||
3. 验收阶段:报告记录已执行验证、结果和未覆盖项。
|
||||
4. 完成阶段:把已落地事实合并到正式文档;过程文档归档或删除。
|
||||
5. 维护阶段:CI 或定期检查发现链接、命令和事实漂移。
|
||||
|
||||
建议在贡献规范中加入“变更是否需要更新文档”的检查项,但不要承诺不存在的自动化流程。
|
||||
@ -0,0 +1,215 @@
|
||||
# Docker 本地开发规范
|
||||
|
||||
本规范用于建立可复现的本地容器开发环境。除用户明确要求“仅审计”外,Skill 必须读取当前项目后端实际技术栈、依赖清单和启动入口,自动生成或完善根 `docker-compose.yml`;不要把示例端口、镜像或命令当成项目事实。
|
||||
|
||||
## 目标结构
|
||||
|
||||
```text
|
||||
repository/
|
||||
docker-compose.yml 本地开发统一 Compose 入口
|
||||
run.sh macOS/Linux 标准命令入口
|
||||
run.ps1 Windows 标准命令入口(需要支持时)
|
||||
.env.docker.example Compose 变量样例,不含真实秘密
|
||||
backend/ 后端源码、依赖清单、迁移
|
||||
frontend/
|
||||
<app-name>/ 可独立运行的前端应用
|
||||
docker/ 只保存 Docker 配置和脚本,不保存运行数据
|
||||
backend/Dockerfile.dev 后端开发镜像
|
||||
frontend/<app>/Dockerfile.dev 前端开发镜像(确有容器化需要时)
|
||||
nginx/ 本地反向代理配置(按需)
|
||||
scripts/ 容器入口和健康检查脚本(按需)
|
||||
help/docs/docker-development.md 使用者文档
|
||||
```
|
||||
|
||||
根 `docker-compose.yml` 和 `run.sh` 是所有项目统一入口,后端统一映射到 `backend/`,前端统一映射到 `frontend/<应用名>/`。旧项目尚未完成目录迁移时,先在 `.crmeb/project.json` 记录兼容路径并制定迁移计划,不得为了形式统一直接移动大量源码。生产镜像和部署编排应与本地开发配置分开,避免源码挂载、调试扩展和开发端口进入生产。
|
||||
|
||||
## 自动分析与生成
|
||||
|
||||
先运行环境分析器,核对后端运行时和版本、包管理器、启动命令、全部前端、数据库/缓存/队列、端口、worker/scheduler/WebSocket、现有 Docker 文件和配置样例。只读取非敏感样例,不读取或复制真实 `.env`。
|
||||
|
||||
生成器分两步运行:默认只预览;`--write-missing` 只创建缺失文件。已有 `docker-compose.yml`、`run.sh` 或 `docker/` 配置必须保留并语义合并。机械脚本无法可靠识别运行时、启动命令或服务时应停止猜测,但 AI 必须继续读取代码入口、技术栈 Profile、CI 和现有脚本完成生成;除非项目本身缺少必要事实且已经形成明确阻塞,否则不得把“待确认”当作最终结果。禁止用 `sleep infinity`、无效端口或虚假的 healthy 状态掩盖缺口。
|
||||
|
||||
## 后端环境到 Compose 的强制映射
|
||||
|
||||
生成 `docker-compose.yml` 前必须建立证据映射:
|
||||
|
||||
| 后端事实 | Compose/Docker 输出 |
|
||||
| --- | --- |
|
||||
| 语言及版本约束 | 固定到兼容的基础镜像版本和构建参数 |
|
||||
| manifest、lockfile、系统扩展 | Dockerfile 的依赖安装,依赖卷及重建条件 |
|
||||
| Web/CLI 启动入口 | `command`、工作目录、监听地址、应用端口和健康检查 |
|
||||
| 数据库、缓存、队列、对象存储 | 对应服务、版本、健康检查、网络、非敏感变量和具名卷 |
|
||||
| worker、scheduler、WebSocket | 独立 Compose 服务,复用后端镜像和源码但使用真实命令 |
|
||||
| migration、seed、初始化脚本 | 首次启动说明和可重复执行的初始化入口,不塞进普通重启 |
|
||||
| 上传、日志和持久化目录 | 明确开发卷、权限和保留策略,不提交运行数据 |
|
||||
|
||||
PHP 必须继续核对 Composer `ext-*`、FPM/CLI 和 Web 入口;Node 必须核对 Node 版本、包管理器、lockfile 与真实 script;Python 必须核对依赖管理器和 ASGI/WSGI 入口;Java 必须核对 JDK、Maven/Gradle 与启动产物;Go 必须核对 Go 版本、构建入口和运行参数。框架 Profile 只能帮助发现事实,不能替代项目分析。
|
||||
|
||||
## Compose 必备信息
|
||||
|
||||
根据项目实际需要定义服务,常见职责如下:
|
||||
|
||||
| 服务 | 代码/数据来源 | 必须说明 |
|
||||
| --- | --- | --- |
|
||||
| `backend` | `backend/` 源码挂载 | 工作目录、启动命令、应用端口、健康检查 |
|
||||
| `frontend-<app>` | `frontend/<app>/` 源码挂载 | 包管理器、开发命令、HMR 端口、API 地址 |
|
||||
| `db` | 命名数据卷 | 数据库版本、库名、健康检查、初始化/迁移方式 |
|
||||
| `redis` | 命名数据卷或临时卷 | 版本、端口、持久化策略 |
|
||||
| `proxy` | `docker/nginx/` 等配置 | 对外入口、转发目标、静态文件来源 |
|
||||
| worker/scheduler | 与后端相同源码或镜像 | 命令、依赖服务、重启策略、日志位置 |
|
||||
|
||||
必须满足以下约束:
|
||||
|
||||
1. 使用明确的镜像版本,不使用不可追溯的 `latest` 作为长期基线。
|
||||
2. `docker-compose.yml` 中的 build context、Dockerfile、working directory 与 `backend/`、`frontend/` 目录一致。
|
||||
3. 开发容器挂载本地源码,实现代码修改立即生效;不要把宿主机的依赖目录直接覆盖容器依赖。
|
||||
4. 依赖目录使用容器命名卷,例如 `/workspace/backend/vendor`、`/workspace/frontend/admin/node_modules`,或使用经验证的宿主机策略。
|
||||
5. 数据库、缓存等有状态服务使用具名卷;清理卷是破坏性操作,文档必须单独警告。
|
||||
6. 密码和令牌通过未入库的 `.env` 注入,仓库只提供 `.env.example` 与非敏感默认值。
|
||||
7. 服务依赖优先结合 `healthcheck` 判断就绪;`depends_on` 的启动顺序不等于依赖服务已经可用。
|
||||
8. 应用监听容器内 `0.0.0.0`,端口映射可由环境变量覆盖,并在文档中列出默认值。
|
||||
9. macOS/Windows 说明文件监听、权限和卷性能差异;Linux 说明 UID/GID 映射策略。
|
||||
10. 本地 Compose 不复用生产数据库、生产凭据或生产网络。
|
||||
|
||||
## 源码挂载模式
|
||||
|
||||
Compose 配置应表达以下关系,具体镜像和命令从项目事实生成:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/backend/Dockerfile.dev
|
||||
working_dir: /workspace/backend
|
||||
volumes:
|
||||
- ./backend:/workspace/backend
|
||||
- backend-dependencies:/workspace/backend/vendor
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
frontend-admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/frontend/admin/Dockerfile.dev
|
||||
working_dir: /workspace/frontend/admin
|
||||
volumes:
|
||||
- ./frontend/admin:/workspace/frontend/admin
|
||||
- frontend-admin-dependencies:/workspace/frontend/admin/node_modules
|
||||
environment:
|
||||
FILE_WATCH_POLLING: "false"
|
||||
|
||||
volumes:
|
||||
backend-dependencies:
|
||||
frontend-admin-dependencies:
|
||||
database-data:
|
||||
```
|
||||
|
||||
上例只展示目录和卷策略。生成目标项目配置时必须替换为真实的依赖目录、环境变量和服务命令;非 PHP 后端不应保留 `vendor`,非 Node 前端不应保留 `node_modules`。仅在原生文件监听失效时才为对应平台启用轮询,避免无谓 CPU 消耗。
|
||||
|
||||
## 必须生成的本地运行文档
|
||||
|
||||
`help/docs/docker-development.md` 至少包含以下章节。
|
||||
|
||||
### 1. 适用范围
|
||||
|
||||
- Docker 是默认开发方式、可选兼容方式还是仅用于某些依赖;
|
||||
- 已验证的 Docker Engine、Compose 与 CPU 架构;
|
||||
- 与宿主机直接运行方式的区别和不能覆盖的场景。
|
||||
|
||||
### 2. 服务与端口
|
||||
|
||||
列出服务名、容器端口、宿主机默认端口、健康检查、数据卷和用途。明确端口由哪个环境变量覆盖,避免只写一个可能冲突的固定值。
|
||||
|
||||
### 3. 首次启动
|
||||
|
||||
按可执行顺序记录:
|
||||
|
||||
1. 检查 Docker 与 Compose 版本;
|
||||
2. 从 `.env.example` 创建本地 `.env`;
|
||||
3. 构建开发镜像;
|
||||
4. 启动基础设施并等待健康;
|
||||
5. 安装后端和前端依赖;
|
||||
6. 创建数据库、执行迁移或导入最小数据;
|
||||
7. 启动应用、worker 和 scheduler;
|
||||
8. 通过健康检查、页面或最小 API 验证成功。
|
||||
|
||||
每一步都要写明执行目录和成功信号。只有实际存在且验证过的命令才能进入文档。
|
||||
|
||||
### 4. 日常开发
|
||||
|
||||
文档应给出项目真实对应命令:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
docker compose logs -f backend
|
||||
docker compose exec backend <project-command>
|
||||
docker compose exec frontend-admin <project-command>
|
||||
docker compose stop
|
||||
docker compose down
|
||||
```
|
||||
|
||||
同时说明:
|
||||
|
||||
- 修改后端和前端源码是否立即生效;
|
||||
- 新增依赖后在容器内执行什么命令、何时需要重建镜像;
|
||||
- 如何运行 lint、类型检查、单元测试、集成测试和构建;
|
||||
- 如何进入容器、查看日志、检查端口和健康状态;
|
||||
- worker、scheduler、WebSocket 等进程如何重启。
|
||||
|
||||
`<project-command>` 只是编写模板时的标记,交付到目标项目文档前必须替换为真实命令,不能原样保留。
|
||||
|
||||
### 5. 数据库与持久化
|
||||
|
||||
- 说明初始化、迁移、种子、备份和恢复命令;
|
||||
- 标明数据库卷名称和数据保留行为;
|
||||
- `docker compose down` 与 `docker compose down -v` 的区别必须写清楚;
|
||||
- 删除数据卷、重建数据库或执行不可逆迁移前必须获得明确授权;
|
||||
- 测试数据和开发账号不得包含生产数据或真实个人信息。
|
||||
|
||||
### 6. 重建与清理
|
||||
|
||||
按影响分级:
|
||||
|
||||
| 操作 | 适用场景 | 数据影响 |
|
||||
| --- | --- | --- |
|
||||
| 重启服务 | 配置或进程异常 | 不删除数据 |
|
||||
| 重建指定镜像 | Dockerfile、系统扩展变化 | 通常保留命名卷 |
|
||||
| 重装应用依赖卷 | lockfile 或依赖损坏 | 删除依赖缓存,不删除数据库 |
|
||||
| 删除数据库卷 | 确认要全量重置本地数据 | 不可恢复,必须显著警告 |
|
||||
|
||||
给出精确的项目命令,禁止把删除卷作为普通故障排查第一步。
|
||||
|
||||
### 7. 常见故障
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- 端口占用和端口覆盖;
|
||||
- 镜像架构不兼容;
|
||||
- 文件权限或 UID/GID;
|
||||
- macOS/Windows 文件监听与卷性能;
|
||||
- 数据库“已启动但未就绪”;
|
||||
- 宿主机与容器访问地址混用;
|
||||
- 源码已更新但容器使用旧镜像/旧依赖卷;
|
||||
- Compose 项目名不同导致连接到另一组容器或数据卷。
|
||||
|
||||
每个故障写“症状、检查命令、原因、处理、验证结果”,不要只给破坏性重置命令。
|
||||
|
||||
## 验证要求
|
||||
|
||||
至少完成并如实报告:
|
||||
|
||||
1. `docker compose config` 能解析当前配置;
|
||||
2. 开发镜像能构建,或明确未构建原因;
|
||||
3. 必要服务达到 healthy/running;
|
||||
4. 本地源码修改能在容器内可见,热更新按技术栈生效;
|
||||
5. 后端健康检查或最小 API 可访问;
|
||||
6. 每个前端应用至少能启动或构建;
|
||||
7. 测试命令能在文档指定目录执行;
|
||||
8. 停止再启动后,有状态数据按文档预期保留。
|
||||
|
||||
不能启动完整环境时,至少执行 Compose 解析、Dockerfile/路径静态核对和文档链接检查,并明确未完成运行验证。
|
||||
@ -0,0 +1,58 @@
|
||||
# 项目文档覆盖矩阵
|
||||
|
||||
文档不是固定数量的空文件,而是由项目能力动态生成的可运行知识地图。环境分析器给出 `documentation.required` 和 `missing`,AI 再以源码、配置和实际验证补全内容。
|
||||
|
||||
## 所有项目必备
|
||||
|
||||
| 文档 | 必须回答 |
|
||||
| --- | --- |
|
||||
| `README.md` | 项目用途、组成、五分钟启动、标准命令、文档导航 |
|
||||
| `help/docs/README.md` | 按角色和任务导航全部正式文档 |
|
||||
| `project-overview.md` | 用户、边界、主要能力、外部系统和非目标 |
|
||||
| `architecture.md` | 应用/模块、调用链、进程、数据流、关键决策和扩展点 |
|
||||
| `development.md` | 版本、依赖安装、配置、初始化、启动和成功信号 |
|
||||
| `configuration.md` | 变量名、用途、必填性、默认值来源、覆盖顺序和秘密管理 |
|
||||
| `api.md` | 协议、鉴权、错误、分页、幂等、版本和契约来源 |
|
||||
| `testing.md` | 静态、单元、集成、端到端、冒烟、测试数据和覆盖边界 |
|
||||
| `deployment.md` | 构建物、环境、部署步骤、健康检查和回滚入口 |
|
||||
| `operations.md` | 进程、日志、指标、告警、任务、容量和日常操作 |
|
||||
| `troubleshooting.md` | 已验证的症状、检查、原因、处理和验证结果 |
|
||||
| `security.md` | 权限、秘密、敏感数据、依赖风险和安全响应 |
|
||||
| `upgrade-and-rollback.md` | 版本源、升级路径、迁移、备份、验证、失败恢复 |
|
||||
|
||||
## 按能力强制增加
|
||||
|
||||
| 检测到的能力 | 必备文档 |
|
||||
| --- | --- |
|
||||
| 后端应用 | 后端目录 `README.md` + `help/docs/applications/<后端ID>.md`:职责、入口、分层、任务、异常、事务、命令和调试 |
|
||||
| 每个独立前端应用 | `frontend/<应用名>/README.md` + `help/docs/applications/<应用ID>.md`:职责、页面/路由、状态、API 客户端、环境、命令、构建和发布 |
|
||||
| UniApp | `uniapp-development.md`:平台矩阵、条件编译、权限、分包、真机与发布 |
|
||||
| 数据库 | `database-and-migrations.md`、`backup-and-recovery.md` |
|
||||
| Docker 本地环境 | `docker-development.md`,并与根 compose/run 逐项一致 |
|
||||
| worker/queue/scheduler/WebSocket | 在架构、开发和运维文档分别说明启动、健康、日志和重启 |
|
||||
| 多租户、支付、对象存储、搜索等 | 增加专项边界、数据、安全、故障和测试说明 |
|
||||
| 可安装升级包 | 升级清单、包格式、校验和、连续迁移与隔离验证 |
|
||||
|
||||
## 内容质量门槛
|
||||
|
||||
每份正式文档必须链接证据来源,命令写明执行目录、前置条件、成功信号和副作用。待确认内容标注状态和负责人;计划不得写成已实现。禁止真实凭据、生产数据、机器私有绝对路径和无证据版本号。
|
||||
|
||||
文档完成不以“文件存在”为标准。至少执行链接检查、事实静态检查和项目标准检查;运行、部署、备份恢复或升级未实际验证时,必须明确验证等级和缺口。
|
||||
|
||||
## 逐应用说明文档
|
||||
|
||||
每个可独立安装、运行、测试、构建或发布的后端/前端应用都必须有自己的说明,不能用一份笼统的“前端开发”文档代替多个应用。应用目录内的 `README.md` 是开发者就近入口,详细事实文档放在 `help/docs/applications/<应用ID>.md`;两者应以链接分工,避免复制整段内容。
|
||||
|
||||
逐应用说明至少由当前代码核对以下事实:
|
||||
|
||||
1. 应用用途、用户、边界以及与其他应用的关系;
|
||||
2. 运行时、框架、包管理器和版本来源文件;
|
||||
3. 目录入口、路由/页面或请求入口、状态与 API 客户端;
|
||||
4. 安装、配置、开发、测试、构建、预览和发布命令;
|
||||
5. 环境变量名称、默认端口、代理、依赖服务和健康信号;
|
||||
6. 构建产物、部署目标、平台差异以及当前限制;
|
||||
7. 常见调试入口、日志、测试位置和关联正式文档。
|
||||
|
||||
检查顺序是 manifest/lockfile → 构建配置 → 代码入口/路由 → 环境样例 → API 客户端与后端路由 → 测试/CI → 现有说明。旧 README 与代码冲突时按代码和可执行结果更新,不能反过来照抄旧说明。
|
||||
|
||||
代码变更影响应用职责、目录、运行时版本、命令、配置、端口、路由、API、权限、数据、构建产物或发布方式时,同一变更必须检查并更新对应应用说明及文档索引。纯内部重构若不改变上述事实,可以不改正文,但交付报告应说明已检查且无需更新。
|
||||
@ -0,0 +1,146 @@
|
||||
# 文档与事实治理
|
||||
|
||||
## 1. 先建立证据账本
|
||||
|
||||
对每项关键事实记录四个字段:
|
||||
|
||||
| 事实 | 证据 | 验证方式 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 运行时版本 | manifest、lockfile、容器镜像 | 读取约束并与实际环境比较 | 已核对 / 冲突 / 未验证 |
|
||||
| 启动命令 | scripts、Makefile、compose | 在正确目录执行或静态解析 | 已运行 / 仅静态 |
|
||||
| API 前缀 | 路由注册文件 | 搜索注册链路,必要时列路由 | 已核对 / 待运行 |
|
||||
| 数据结构 | migrations、schema | 解析迁移或连接测试库 | 已核对 / 环境受限 |
|
||||
|
||||
先收集证据,再决定写什么。遇到冲突时报告冲突并以可执行配置为优先,不应选择看起来更合理的旧描述。
|
||||
|
||||
## 2. 项目发现顺序
|
||||
|
||||
### 仓库与规则
|
||||
|
||||
- 根目录、Git 状态、远程托管平台;
|
||||
- 所有适用的 `AGENTS.md`、贡献指南和目录规则;
|
||||
- workspace/monorepo 配置和各子项目边界;
|
||||
- 当前未提交修改,避免覆盖或误归因。
|
||||
|
||||
### 技术栈与依赖
|
||||
|
||||
- manifest、lockfile、运行镜像和版本文件;
|
||||
- 后端、前端、移动端、插件、任务进程等入口;
|
||||
- 包管理器、构建工具、代码生成、数据库和缓存;
|
||||
- 生产依赖与仅开发依赖的区别。
|
||||
|
||||
### 可执行工作流
|
||||
|
||||
- 安装、初始化、启动、停止、构建、测试、lint、类型检查;
|
||||
- 环境变量样例、配置覆盖顺序、默认端口;
|
||||
- 数据库创建、迁移、种子、升级、备份和回滚;
|
||||
- 容器、CI、发布、健康检查和日志位置。
|
||||
|
||||
项目存在 Dockerfile 或 Compose 时,还要确认它是生产构建、本地开发还是两者兼有,并检查源码是否挂载、依赖目录如何隔离、服务是否有健康检查。详细检查使用 [Docker 本地开发规范](docker-local-development.md)。
|
||||
|
||||
### 系统结构
|
||||
|
||||
- 请求入口、路由、控制器/服务/数据访问层;
|
||||
- 前端路由、状态管理、API 客户端和构建产物;
|
||||
- 异步任务、定时器、WebSocket、缓存、文件存储;
|
||||
- 插件或模块的发现、安装、启停、卸载和权限边界。
|
||||
|
||||
### 验证资产
|
||||
|
||||
- 单元、集成、端到端、契约、冒烟测试;
|
||||
- fixture、测试数据和依赖服务;
|
||||
- CI 实际执行的检查;
|
||||
- 本地无法覆盖的动态路径。
|
||||
|
||||
### 版本与升级资产
|
||||
|
||||
- 唯一版本源、发布标签、历史版本和允许升级范围;
|
||||
- 数据库迁移、基础数据迁移、升级日志和失败恢复机制;
|
||||
- 后端发布物、各前端应用构建产物及配置覆盖边界;
|
||||
- 现有升级器、manifest、校验和、备份与回滚能力;
|
||||
- 核心升级包与插件/扩展包的所有权边界。
|
||||
|
||||
需要生成或修复升级交付时使用 [自动升级包规范](upgrade-package.md),不得仅依据工作区全部改动直接压缩。
|
||||
|
||||
## 3. 差距评估
|
||||
|
||||
按以下维度给出“完整 / 部分 / 缺失 / 冲突”,每条结论必须附证据:
|
||||
|
||||
1. 发现性:根入口是否能到达所有关键文档;
|
||||
2. 正确性:版本、路径、命令、端口、路由是否与代码一致;
|
||||
3. 可运行性:新环境是否有从零到运行的闭环;
|
||||
4. 可开发性:架构、目录、规则和扩展方式是否明确;
|
||||
5. 可验证性:测试层级、命令、成功信号和限制是否明确;
|
||||
6. 可运维性:部署、升级、回滚、日志、监控是否覆盖;
|
||||
7. AI 友好性:规则是否可发现、范围是否清楚、验证是否可执行;
|
||||
8. 可维护性:是否存在重复真相源、失效链接和自动校验缺口。
|
||||
9. 目录一致性:AI 规则、后端与前端是否分别归入 `.crmeb/`、`backend/`、`frontend/`,不一致时是否有安全迁移方案。
|
||||
10. 升级能力:旧版本能否通过连续迁移、安全文件替换和验证步骤升级,失败时是否可停止并恢复。
|
||||
11. 文档覆盖:按 [项目文档覆盖矩阵](documentation-coverage.md) 检查通用文档与数据库、UniApp、异步进程、Docker、升级等条件文档,防止只补 README 和启动说明。
|
||||
|
||||
## 4. 实施策略
|
||||
|
||||
### 先修 P0
|
||||
|
||||
- 错误或危险命令;
|
||||
- 泄露的凭据或真实环境数据;
|
||||
- 根入口和实际项目完全脱节;
|
||||
- 多份文档对同一事实给出冲突结论;
|
||||
- 把待实施方案写成当前能力。
|
||||
|
||||
### 再补最短闭环
|
||||
|
||||
以新接手者路径实施:
|
||||
|
||||
1. 从根 README 识别项目;
|
||||
2. 安装正确版本的运行环境和依赖;
|
||||
3. 从样例建立本地配置;
|
||||
4. 启动最小依赖和应用;
|
||||
5. 确认健康状态或首个可见页面;
|
||||
6. 运行最小测试或静态检查;
|
||||
7. 找到架构、接口、数据和贡献规则。
|
||||
|
||||
命令块附近要写明执行目录、前置服务、预期结果。若某一步只适用于特定平台或版本,应紧邻命令说明。
|
||||
|
||||
### 最后治理漂移
|
||||
|
||||
优先校验最容易变化且能低成本提取的事实:
|
||||
|
||||
- Markdown 内部链接与路径;
|
||||
- 运行时版本和包管理器;
|
||||
- 公开脚本名称;
|
||||
- 模块、插件、数据库迁移或路由的关键清单;
|
||||
- 根入口是否链接到规则和测试索引。
|
||||
|
||||
校验脚本应失败得清楚:指出文档值、实际值和证据位置。不要只检查“文件存在”,也不要为不可稳定解析的内容制造脆弱正则。
|
||||
|
||||
## 5. 验证分级
|
||||
|
||||
| 级别 | 含义 | 可声明的结论 |
|
||||
| --- | --- | --- |
|
||||
| L0 文本检查 | 格式、链接、路径、拼写 | 文档结构与引用有效 |
|
||||
| L1 静态事实检查 | 从配置/代码解析版本、脚本、路由等 | 文档与当前源码一致 |
|
||||
| L2 构建/测试 | 安装、lint、类型检查、单测、构建 | 对应检查在当前环境通过 |
|
||||
| L3 运行验证 | 启动服务、健康检查、集成/冒烟测试 | 指定运行链路已实际验证 |
|
||||
| L4 生命周期验证 | 安装、升级、回滚、插件启停/卸载 | 指定生命周期场景已验证 |
|
||||
|
||||
报告必须使用实际达到的级别。L0/L1 不能写成“项目可正常运行”,单元测试通过也不能替代部署或升级验证。
|
||||
|
||||
## 6. 环境与安全边界
|
||||
|
||||
- 不读取或输出真实密钥、令牌、Cookie、个人数据;环境示例只保留占位值。
|
||||
- 不因文档检查擅自连接生产库、运行生产迁移、删除数据或发布版本。
|
||||
- 不默认容器存在或能启动;先检查运行时、镜像和服务状态。
|
||||
- 不把本机偶然路径写成所有开发者的固定要求,除非项目本身规定如此。
|
||||
- 添加 CI 前先确认托管平台和现有流水线;非 GitHub 项目不要擅自添加 GitHub Actions。
|
||||
- 依赖不可用时,完成安全的静态验证并明确动态覆盖缺口。
|
||||
|
||||
## 7. 审计输出格式
|
||||
|
||||
仅审计时,推荐用表格输出:
|
||||
|
||||
| 优先级 | 问题 | 证据 | 影响 | 建议 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P0/P1/P2 | 明确且可复现的问题 | 文件、配置或命令结果 | 对运行/开发/AI 的影响 | 最小可实施动作 |
|
||||
|
||||
结尾给出建议实施顺序和验证计划。没有证据的推测单独标注为“待确认”。
|
||||
@ -0,0 +1,100 @@
|
||||
# 前端到后端与数据库
|
||||
|
||||
本指南帮助以页面开发为主的开发者和 AI 安全完成全栈改动。目标不是要求前端绕过后端直接改库,而是能追踪数据来源、理解影响并使用迁移和测试完成闭环。
|
||||
|
||||
## 先建立请求链路
|
||||
|
||||
```text
|
||||
页面/组件
|
||||
→ 前端 API 函数与类型
|
||||
→ HTTP 请求封装、认证和 base URL
|
||||
→ 后端路由与中间件
|
||||
→ Controller/Handler 输入校验
|
||||
→ Service/UseCase 业务规则与事务
|
||||
→ Repository/DAO/ORM
|
||||
→ 数据库表、索引与迁移
|
||||
→ 统一响应、错误码和前端状态
|
||||
```
|
||||
|
||||
排查或开发时沿这条链路双向追踪,记录每层真实文件。不要只从页面字段名猜测数据库字段,也不要从表结构直接推断接口权限和业务语义。
|
||||
|
||||
## 快速找到后端实现
|
||||
|
||||
1. 从前端 API 函数找到请求方法和相对路径。
|
||||
2. 确认 request 封装添加的 API 前缀、版本、租户和认证头。
|
||||
3. 在后端路由中搜索静态路径片段;动态参数按路由声明核对。
|
||||
4. 继续定位 Controller/Handler、输入校验和权限中间件。
|
||||
5. 从 Service/UseCase 找到业务规则、事务、事件和缓存失效。
|
||||
6. 从 Repository/DAO/ORM 找到表、字段、关联和查询条件。
|
||||
7. 从 migration/schema 确认结构真源,不以运行库临时状态代替迁移。
|
||||
|
||||
找不到时优先查看路由列表命令、OpenAPI、框架注册器和模块清单,不要新建重复接口。
|
||||
|
||||
## 前端常见任务的全链路清单
|
||||
|
||||
### 新增一个字段
|
||||
|
||||
- 明确字段属于展示、提交、数据库持久化还是计算结果。
|
||||
- 数据库存储时新增迁移、索引/约束和新安装 schema;设置安全默认值及历史数据策略。
|
||||
- 更新 Entity/Model、允许字段、Repository、Service 和序列化。
|
||||
- 更新请求校验、API schema、响应类型、前端类型、表单和展示。
|
||||
- 覆盖新建、编辑、详情、列表、导入导出和权限场景。
|
||||
- 将迁移和受影响构建产物加入升级包。
|
||||
|
||||
### 新增列表和筛选
|
||||
|
||||
- 后端实现分页、排序白名单、筛选校验和权限范围。
|
||||
- 查询避免 N+1;为实际筛选和排序组合评估索引。
|
||||
- 响应返回稳定的列表和总数契约。
|
||||
- 前端在筛选变化时重置分页,防止重复请求并处理空、错和加载结束状态。
|
||||
- 测试无数据、大数据量、非法筛选、越权和边界页。
|
||||
|
||||
### 新增或编辑表单
|
||||
|
||||
- 前端校验改善体验,后端校验才是安全边界。
|
||||
- 后端重新计算价格、权限、所有权和不可由客户端信任的数据。
|
||||
- 防止重复提交;高风险写操作使用幂等键、版本号或唯一约束。
|
||||
- 事务覆盖必须同时成功的写入,外部调用失败有补偿策略。
|
||||
|
||||
### 权限和菜单
|
||||
|
||||
- 前端路由、菜单和按钮控制只负责体验,服务端接口仍必须鉴权和校验对象所有权。
|
||||
- 权限标识、角色种子、路由中间件和前端指令保持同一稳定 key。
|
||||
- 新安装数据、存量升级迁移和卸载/回滚边界同步处理。
|
||||
|
||||
## 数据库最低知识
|
||||
|
||||
- **主键**:稳定标识记录,不把可变化的展示值当主键。
|
||||
- **唯一约束**:从数据库层阻止重复业务记录,应用仍需返回友好错误。
|
||||
- **索引**:加速特定查询但增加写入和存储成本,按查询模式设计。
|
||||
- **事务**:保证一组数据库操作整体成功或失败,不自动覆盖外部网络调用。
|
||||
- **迁移**:可追踪地改变结构或基础数据;已发布迁移不能回改。
|
||||
- **外键/软删除**:遵守项目既有策略,删除前确认关联、审计和恢复需求。
|
||||
- **NULL 与默认值**:区分未知、未填写和空值;不要依赖前端补默认值。
|
||||
|
||||
禁止在共享、测试或生产数据库手工改表来代替迁移。需要查看数据时先确认环境和只读范围;SQL UPDATE/DELETE 必须带精确条件并先用 SELECT 验证影响集合。
|
||||
|
||||
## 常见错误定位
|
||||
|
||||
| 现象 | 优先检查 |
|
||||
| --- | --- |
|
||||
| 401 | token 是否发送、是否过期、认证中间件和环境域名 |
|
||||
| 403 | 角色权限、对象所有权、租户范围;不是简单隐藏按钮 |
|
||||
| 404 | API 前缀、版本、路由方法、动态参数和代理转发 |
|
||||
| 422/参数错误 | 前后端字段名、类型、必填、枚举和嵌套结构 |
|
||||
| 500 | 后端结构化日志、关联 ID、异常链和数据库错误;不要只在前端重试 |
|
||||
| 列表重复/缺失 | 分页基准、排序稳定性、筛选重置和并发请求覆盖 |
|
||||
| 保存成功但页面不变 | 响应契约、缓存失效、状态更新和返回页面刷新 |
|
||||
| 本地可用、容器失败 | base URL、容器服务名、端口、CORS、挂载和环境变量 |
|
||||
|
||||
## AI 输出要求
|
||||
|
||||
当请求来自主要做前端的开发者时,AI 应:
|
||||
|
||||
- 用页面和 API 语言解释后端层级与数据库影响,并给出真实文件定位;
|
||||
- 明确哪些值由前端提交、后端验证、后端计算和数据库约束;
|
||||
- 优先修改迁移与代码,不指导手工改库作为正式方案;
|
||||
- 给出最小可执行验证命令和请求示例,但不暴露真实凭据;
|
||||
- 说明本次是否涉及权限、缓存、异步任务、历史数据和升级包;
|
||||
- 不要求用户先掌握后端框架术语才提供可落地方案。
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
# 前端通用规范
|
||||
|
||||
## 应用与目录
|
||||
|
||||
- 每个可独立安装、运行和发布的客户端位于 `frontend/<应用名>/`。
|
||||
- 共享组件、类型和 SDK 进入显式 workspace package,不通过跨应用相对路径偷渡。
|
||||
- 页面负责场景编排,业务逻辑进入 composable/hook/service/store;基础组件不依赖具体业务模块。
|
||||
- API client 统一处理 base URL、鉴权、超时、取消、错误映射和响应解包。
|
||||
- 前端需求涉及新增字段、筛选、权限、持久化或后端错误时,使用 [前端到后端与数据库](frontend-backend-database-bridge.md) 追踪完整链路,不在页面层猜测或绕过后端。
|
||||
- 检测到 `@dcloudio/*`、`pages.json` 或 UniApp manifest 时,同时读取 [UniApp Profile](profiles/uniapp.md)。
|
||||
|
||||
## 类型与状态
|
||||
|
||||
- 接口请求、响应、路由参数、组件属性和事件保持类型安全;避免无理由的 `any` 或类型断言。
|
||||
- 服务端状态优先由请求缓存层管理;跨页面客户端状态才进入全局 store。
|
||||
- 派生状态使用计算值,不维护可推导的重复副本;副作用明确生命周期和清理。
|
||||
- 接口契约变化同步类型、调用方、mock、测试和兼容策略。
|
||||
|
||||
## 页面体验
|
||||
|
||||
- 页面必须处理 loading、empty、error、无权限、离线/超时和成功反馈。
|
||||
- 表单有标签、校验、提交中防重复、错误定位和键盘操作。
|
||||
- 列表有分页或虚拟化策略,搜索与筛选状态可恢复;危险操作二次确认。
|
||||
- 使用项目设计令牌和共享组件,支持响应式、深色模式及目标浏览器范围。
|
||||
- 交互元素具备可访问名称、焦点状态和合理对比度;图片和图标提供替代信息。
|
||||
|
||||
## 构建与发布
|
||||
|
||||
- `.env` 只存本地非入库配置,公开构建变量不得承载秘密。
|
||||
- lockfile 必须与选定包管理器一致;禁止混用造成依赖解析漂移。
|
||||
- 构建产物从干净依赖生成并写入升级包或部署制品,不把开发源码冒充线上产物。
|
||||
- 每个应用独立记录 dev、typecheck、lint、test、build 和 preview/smoke 命令。
|
||||
|
||||
## 每个前端应用的说明文档
|
||||
|
||||
- 每个 `frontend/<应用名>/` 必须维护就近 `README.md`,并链接到 `help/docs/applications/<应用ID>.md` 的详细说明。
|
||||
- 不能只维护一份总的“前端开发文档”;管理端、Web、H5、UniApp、小程序、桌面端等独立项目分别核对 manifest、lockfile、构建配置、路由/页面、环境变量、API 客户端、测试和发布脚本。
|
||||
- 新增或修改命令、端口、代理、路由、权限、API 地址、构建产物、目标平台或发布流程时,同步更新该应用说明和正式文档索引。
|
||||
- 文档中的版本与命令必须来自当前代码并经过静态或运行验证,不沿用另一个前端项目的配置。
|
||||
@ -0,0 +1,47 @@
|
||||
# 全栈开发流程
|
||||
|
||||
## 开始前
|
||||
|
||||
1. 读取 `.crmeb/AGENTS.md`、相关规则和应用作用域。
|
||||
2. 用 `.crmeb/project.json` 定位应用、命令和依赖服务。
|
||||
3. 检查工作区现有改动,确定本任务基线和修改边界。
|
||||
4. 明确用户角色、权限、业务不变量、验收场景和非目标。
|
||||
|
||||
## 变更链路
|
||||
|
||||
按实际影响选择,不机械制造无关层:
|
||||
|
||||
1. **领域与数据**:业务规则、状态机、表结构、索引、迁移和新安装 schema。
|
||||
2. **后端**:路由、输入校验、权限、Service、Repository/DAO、事务、事件和任务。
|
||||
3. **API 契约**:请求、响应、错误码、分页、幂等、版本和 OpenAPI。
|
||||
4. **前端接口层**:类型、API client、错误映射和兼容处理。
|
||||
5. **前端体验**:路由、权限、状态、页面、组件、加载/空/错状态和可访问性。
|
||||
6. **异步与运维**:队列、定时任务、缓存失效、日志、指标和告警。
|
||||
7. **测试**:领域单测、接口集成、前端组件、关键 E2E 和回归场景。
|
||||
8. **文档与配置**:环境变量、开发运行、API、部署和运维说明。
|
||||
9. **升级交付**:迁移、构建产物、manifest、校验和、备份及回滚边界。
|
||||
|
||||
## 接口变更同步清单
|
||||
|
||||
- 路由和权限注册;
|
||||
- 请求校验、响应类型、错误码和示例;
|
||||
- OpenAPI 或其他契约真源;
|
||||
- 前端 API client、类型和调用方;
|
||||
- 缓存 key、事件 payload、队列消息和 webhook;
|
||||
- mock、fixture、契约测试和兼容窗口;
|
||||
- 发布说明和升级依赖。
|
||||
|
||||
删除或重命名字段前先确认所有消费者。跨版本部署时采用先扩展、再迁移、最后收缩的兼容策略。
|
||||
|
||||
## 最小验证顺序
|
||||
|
||||
1. 针对改动的格式、lint、类型和静态架构检查;
|
||||
2. 后端领域与接口测试;
|
||||
3. 前端组件、类型检查和构建;
|
||||
4. 数据迁移幂等与新安装一致性;
|
||||
5. Docker 或宿主机 smoke;
|
||||
6. 关键角色的端到端路径;
|
||||
7. 升级包静态检查与旧版升级验证。
|
||||
|
||||
交付时逐项报告实际执行结果。只通过静态检查不能声明功能、部署或升级已验证。
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
# 迁移与验收
|
||||
|
||||
## 已有项目迁移顺序
|
||||
|
||||
1. 先审计并冻结事实,不移动目录、不改命令。
|
||||
2. 建立 `.crmeb/` 真源和根级薄入口,不复制多套规则。
|
||||
3. 生成 `.crmeb/project.json`,把现有命令映射到统一契约。
|
||||
4. 补齐 `check/test/build` 门禁,再处理 Docker 和快速启动。
|
||||
5. 规划 `backend/`、`frontend/` 迁移,逐项更新 import、autoload、workspace、CI、Docker、部署和文档。
|
||||
6. 目录迁移作为独立变更,运行全量构建、测试和 smoke,并保留回滚点。
|
||||
7. 最后接入升级包与发布流程,验证旧版到新版链路。
|
||||
|
||||
用户只要求文档或规范时,不擅自移动业务代码。无法一次迁移的项目可用兼容入口过渡,但必须记录淘汰计划,避免长期双路径。
|
||||
|
||||
## 入口与导航
|
||||
|
||||
- [ ] 根 README 能说明项目定位、组成和最短启动路径。
|
||||
- [ ] 开发、架构、测试、部署和 AI 规则都能从根入口到达。
|
||||
- [ ] 文档索引按任务组织,没有孤立的关键文档。
|
||||
- [ ] 内部链接、相对路径和标题锚点全部有效。
|
||||
- [ ] 后端及每个独立前端应用都有就近 README,并链接到各自的 `help/docs/applications/<应用ID>.md`。
|
||||
|
||||
## 事实准确性
|
||||
|
||||
- [ ] 语言、框架和工具版本来自 manifest、lockfile、镜像或版本配置。
|
||||
- [ ] 安装、启动、构建、测试命令在正确目录存在。
|
||||
- [ ] 端口、路由/API 前缀、模块和数据结构与当前代码一致。
|
||||
- [ ] 文档明确区分默认值、示例值和环境覆盖值。
|
||||
- [ ] 待实施方案没有写成已具备能力。
|
||||
- [ ] 易漂移数字或清单有来源,能自动生成或校验时不靠手工维护。
|
||||
- [ ] 已逐应用检查 manifest/lockfile、构建配置、入口/路由、环境样例、API 客户端、测试和 CI,并据此更新说明。
|
||||
- [ ] 本次代码变更影响的应用文档已同步;无需更新的应用已记录检查结论和依据。
|
||||
|
||||
## 可运行与可开发
|
||||
|
||||
- [ ] 新环境的前置依赖、配置、初始化、启动和成功信号完整。
|
||||
- [ ] 命令标注执行目录、前置服务和平台限制。
|
||||
- [ ] 后端业务代码统一位于 `backend/`,没有并列散落的后端业务目录。
|
||||
- [ ] 每个可独立构建的前端应用位于 `frontend/<应用名>/`,共享包边界明确。
|
||||
- [ ] 核心目录、模块关系和关键调用链足以支持定位改动。
|
||||
- [ ] API、数据库、插件/扩展机制等项目关键领域有权威入口。
|
||||
- [ ] 常见问题包含症状、原因和验证过的处理方式。
|
||||
|
||||
## AI 协作
|
||||
|
||||
- [ ] `.crmeb/` 是 AI 规则、技能和子智能体的唯一正文来源。
|
||||
- [ ] 根 AI 入口短小,能指向 `.crmeb/AGENTS.md`。
|
||||
- [ ] `CLAUDE.md`、`GEMINI.md`、Cursor Rules、Copilot instructions 和 `llms.txt` 都是薄入口,没有复制规则正文。
|
||||
- [ ] 静态检查确认所有工具入口存在并指向 `.crmeb/AGENTS.md`;已实际验证的工具与仅静态适配的工具分别报告。
|
||||
- [ ] 规则说明作用域、禁止事项、代码风格和最小验证要求。
|
||||
- [ ] 子目录有特殊约定时能被 AI 发现。
|
||||
- [ ] 不同工具入口没有复制多份长期维护正文。
|
||||
- [ ] AI 能区分正式事实、过程记录和测试说明。
|
||||
|
||||
## 前端跨层与 UniApp
|
||||
|
||||
- [ ] 前端开发指南能从 API 函数追踪到请求封装、后端路由、业务层、数据访问和迁移。
|
||||
- [ ] 新字段、列表筛选、表单和权限均有前后端、数据库、测试及升级包同步清单。
|
||||
- [ ] 文档明确禁止用手工改库替代迁移,也不把前端隐藏控件当作服务端鉴权。
|
||||
- [ ] 存在 UniApp 时登记真实平台脚本、API 地址链路、认证封装、pages/分包和条件编译约定。
|
||||
- [ ] UniApp 通用改动至少验证 H5 和目标小程序/App 平台;无法真机验证时明确限制。
|
||||
- [ ] 升级交付区分 H5 产物、小程序审核、App 资源包和原生安装包。
|
||||
|
||||
## Docker 本地开发
|
||||
|
||||
- [ ] 已从当前后端的版本、manifest/lockfile、扩展、启动入口和依赖服务生成证据映射。
|
||||
- [ ] 根 `docker-compose.yml` 已自动生成或按当前环境语义更新,不是未替换的通用模板。
|
||||
- [ ] `docker-compose.yml`、Dockerfile、工作目录与 `backend/`、`frontend/` 的实际结构一致。
|
||||
- [ ] 文档列出服务、端口、健康检查、源码挂载、依赖卷和数据卷。
|
||||
- [ ] 首次启动包含配置、构建、依赖安装、数据库初始化和成功验证。
|
||||
- [ ] 日常开发包含启动、停止、日志、容器内命令、测试和进程重启。
|
||||
- [ ] 源码改动能在容器内可见,热更新或需要重启的边界明确。
|
||||
- [ ] lockfile、Dockerfile 或系统依赖变化后的重建方式明确。
|
||||
- [ ] `down` 与 `down -v` 的数据影响明确,删除数据卷有显著警告。
|
||||
- [ ] macOS、Windows、Linux 的监听、权限或架构差异按实际情况说明。
|
||||
- [ ] 至少执行 `docker compose config`;未构建或未启动时说明原因。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- [ ] 测试地图列出层级、目录、命令、依赖和覆盖范围。
|
||||
- [ ] 文档检查至少覆盖格式、链接和引用路径。
|
||||
- [ ] 关键事实检查能报告文档值、实际值和证据位置。
|
||||
- [ ] 实际执行的 lint、测试、构建或运行验证有结果记录。
|
||||
- [ ] 未执行项及其原因明确,没有夸大验证范围。
|
||||
|
||||
## 自动升级包
|
||||
|
||||
- [ ] 已定义唯一版本源、目标版本、允许来源版本和版本递增规则。
|
||||
- [ ] 后端、前端、数据库、配置和插件变更能被正确分类。
|
||||
- [ ] 核心升级包不混入独立插件/扩展文件,插件包不修改无所有权的核心数据。
|
||||
- [ ] manifest 包含来源提交、组件、运行要求、迁移、备份、回滚边界和人工步骤。
|
||||
- [ ] 文件清单和 SHA-256 由构建结果生成并能完整复验。
|
||||
- [ ] 包内没有 `.env`、密钥、日志、缓存、上传文件、数据库备份或本地运行数据。
|
||||
- [ ] 数据迁移可追踪、顺序稳定、尽可能幂等,已发布迁移没有被回改。
|
||||
- [ ] 新安装结果与执行全部存量迁移后的最终结构一致。
|
||||
- [ ] 所有受影响的前端应用生成发布构建产物,而不是把开发源码冒充线上产物。
|
||||
- [ ] 至少验证最近稳定版到目标版;声明跨级升级时验证完整迁移链。
|
||||
- [ ] 失败不会写入成功记录,备份、停止和恢复路径有验证或明确限制。
|
||||
- [ ] 交付报告包含版本、包路径、来源提交、SHA-256、迁移概要和未验证项。
|
||||
|
||||
## 安全与维护
|
||||
|
||||
- [ ] 文档和示例不含真实密钥、令牌、Cookie、个人数据或生产地址凭据。
|
||||
- [ ] 数据迁移、删除、发布、回滚等高风险操作带有边界和前置条件。
|
||||
- [ ] CI 配置与项目实际托管平台和现有流水线一致。
|
||||
- [ ] 每类文档有清晰职责和生命周期,不存在重复真相源。
|
||||
- [ ] 改动未覆盖用户已有修改,`git diff --check` 通过。
|
||||
|
||||
## 完成判定
|
||||
|
||||
以下条件同时满足才能声明“文档体系已完善”:
|
||||
|
||||
1. P0 问题全部解决或明确阻塞;
|
||||
2. 从零运行、理解架构、修改代码、执行测试四条核心路径都有入口;
|
||||
3. 关键事实已达到至少静态核对级别;
|
||||
4. 所有实际验证与未验证项如实报告;
|
||||
5. 没有为追求目录完整而留下空文档或虚构内容。
|
||||
6. 触发升级交付的变更已经生成候选升级包;未完成动态验证时没有标记为正式可发布。
|
||||
7. 非“仅审计”任务已生成或完善根 `docker-compose.yml`;无法生成时必须作为未完成阻塞报告,不能声明规范建设完成。
|
||||
@ -0,0 +1,9 @@
|
||||
# Go Profile
|
||||
|
||||
- 从 `go.mod`、toolchain、CI 和镜像核对 Go 版本;依赖由 `go.sum` 锁定。
|
||||
- 后端位于 `backend/`;多模块仓库使用明确的 `go.work` 和模块边界。
|
||||
- 遵循 `gofmt`/`goimports`,错误保留上下文并使用 `errors.Is/As` 等可判定机制。
|
||||
- 标准检查映射 `go vet`、项目 lint、`go test ./...`、race 测试和构建。
|
||||
- Context、超时、goroutine 生命周期、连接关闭和信号处理必须可验证。
|
||||
- 静态二进制、CGO 和目标架构进入 Docker 与升级包说明。
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# Java Profile
|
||||
|
||||
- 从 Maven/Gradle 配置、toolchain、wrapper 和镜像核对 JDK 与构建工具版本。
|
||||
- 后端位于 `backend/`;必须使用仓库内 Maven/Gradle Wrapper,避免本机版本漂移。
|
||||
- 多模块依赖方向在构建文件中显式声明,领域层不依赖 Web 或持久化实现。
|
||||
- 标准检查映射 formatter、Checkstyle/SpotBugs、单测、集成测试和 assemble/build。
|
||||
- 配置按 profile/environment 隔离,密钥不进入 `application*.yml`。
|
||||
- 数据库迁移、JVM 参数、健康检查、优雅关闭和容器内存边界进入运行文档。
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# Node.js Profile
|
||||
|
||||
- 从 `package.json` engines/packageManager、lockfile、版本文件和容器镜像核对 Node 与包管理器。
|
||||
- 只使用 lockfile 对应的 npm、pnpm、Yarn 或 Bun;CI 使用冻结锁文件安装。
|
||||
- Node 后端位于 `backend/`,多包项目的 workspace 边界必须显式配置。
|
||||
- 标准检查映射项目实际 scripts:lint、typecheck、test、build、start/dev。
|
||||
- 生产运行不依赖开发服务器;处理信号、优雅关闭、健康检查和未捕获异常。
|
||||
- 不提交 `node_modules/`,升级包是否带依赖由部署模型明确决定。
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# PHP Profile
|
||||
|
||||
- 从 `composer.json`、`composer.lock`、容器镜像和 `composer check-platform-reqs` 核对 PHP 与扩展版本。
|
||||
- 后端根目录统一为 `backend/`,Composer 命令在该目录执行;`vendor/` 默认不入库。
|
||||
- 使用项目既有的 PSR、框架分层和自动加载约定,不凭经验改 namespace。
|
||||
- 标准检查优先映射 Composer scripts、PHP-CS-Fixer/PHPCS、PHPStan/Psalm、PHPUnit/Pest 和框架命令。
|
||||
- 迁移、队列、定时任务、长驻进程和 OPcache 重载必须进入运行与升级说明。
|
||||
- 宿主 PHP 不满足版本时可使用项目开发镜像,但须说明实际验证环境。
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# Python Profile
|
||||
|
||||
- 从 `pyproject.toml`、lockfile、`.python-version`、tox/nox 和镜像核对 Python 版本与依赖工具。
|
||||
- 后端位于 `backend/`,虚拟环境和缓存不入库;应用包采用明确的 `src` 或框架结构。
|
||||
- 依赖必须锁定;开发、测试和生产组边界清楚。
|
||||
- 标准检查映射 Ruff/Black、mypy/pyright、pytest、构建和框架健康检查中的实际工具。
|
||||
- 数据库迁移、ASGI/WSGI、worker、scheduler 和静态资源处理进入运行说明。
|
||||
- 测试隔离时区、事件循环、数据库事务和外部服务。
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# React Profile
|
||||
|
||||
- React 应用位于 `frontend/<应用名>/`,从 manifest、lockfile 和构建框架配置核对版本。
|
||||
- 组件保持纯渲染;副作用只处理外部同步并明确依赖、取消和清理。
|
||||
- props、事件、路由、API 和表单保持类型安全,避免把派生值复制进 state。
|
||||
- 服务端缓存、全局客户端状态和组件局部状态使用不同层,避免单一 store 承载一切。
|
||||
- 标准检查映射 ESLint、TypeScript、单元/组件/E2E 测试和生产构建。
|
||||
- SSR/CSR 边界、环境变量暴露、hydration、错误边界和静态资源路径进入开发文档。
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
# UniApp Profile
|
||||
|
||||
## 识别与目录
|
||||
|
||||
- UniApp 应用统一位于 `frontend/uni-app/`;多套独立 UniApp 应用使用 `frontend/<应用名>/` 并在项目契约分别登记。
|
||||
- 从 `package.json` 的 `@dcloudio/*`、Vite 配置、`pages.json` 和 `manifest.json` 判断 CLI 工程、Vue 版本和目标平台。
|
||||
- 所有 `@dcloudio/*` 依赖保持同一发行版本,升级时整体同步并用 lockfile 固定。
|
||||
- 先读取 package scripts 获取真实的 H5、小程序和 App 开发/构建命令,不假设项目一定使用 HBuilderX 或某个包管理器。
|
||||
|
||||
## 页面与平台边界
|
||||
|
||||
- 页面必须登记在 `pages.json` 的主包或分包;控制主包体积并记录分包边界。
|
||||
- 页面生命周期从 UniApp API 获取,组件生命周期遵循对应 Vue 版本;不要混用导致重复请求或状态丢失。
|
||||
- 导航、请求、存储、上传、权限和系统能力优先使用 `uni.*` 跨端 API。
|
||||
- 浏览器 DOM、微信 API、App 原生能力等必须放入正确的条件编译块,块保持成对和平衡。
|
||||
- 使用 `rpx`、安全区和触屏交互规范;权限、定位、相机、相册等能力同步隐私声明与平台配置。
|
||||
|
||||
## 接口与认证
|
||||
|
||||
- 追踪“环境变量 → 构建配置 → API base URL → request 封装 → 后端路由”的完整链路。
|
||||
- 明确 token header、登录过期、刷新/重新登录、语言、租户、设备等公共请求信息。
|
||||
- API 函数只传领域路径和参数,统一封装响应、错误、超时、取消、上传及未登录处理。
|
||||
- 前端使用匿名标记前,必须确认后端路由和中间件也允许匿名。
|
||||
- 后端下发的状态、按钮、页面配置和组件 key 是跨端契约,不能在页面再次硬编码另一套映射。
|
||||
|
||||
## 多端验证矩阵
|
||||
|
||||
| 改动 | 最低验证 |
|
||||
| --- | --- |
|
||||
| 通用页面、组件、API | H5 + 需求指定的至少一个小程序或 App 端 |
|
||||
| H5 DOM、登录、分享 | 普通浏览器;相关时验证微信内浏览器 |
|
||||
| 微信/支付宝等小程序能力 | 对应平台开发者工具和真实权限配置 |
|
||||
| App 原生能力 | 受影响的 Android/iOS 平台,缺少真机时明确限制 |
|
||||
| `pages.json`、manifest、条件编译 | 所有受影响的平台构建 |
|
||||
|
||||
同时覆盖未登录、已登录、token 过期、慢请求、空数据、后端错误、返回页面刷新和重复提交。
|
||||
|
||||
## 构建与升级交付
|
||||
|
||||
- H5 产物可按部署模型进入核心升级包,并记录 base path、API 地址和缓存刷新方式。
|
||||
- 微信、支付宝等小程序通常需要重新上传和审核;升级包 manifest 必须提示人工发布步骤,不能声称覆盖已发布小程序。
|
||||
- App 本地资源包、热更新包、原生安装包和应用商店发布是不同制品,分别记录版本、平台和签名要求。
|
||||
- `manifest.json`、App ID、证书、平台密钥和生产域名不得用个人或真实秘密模板覆盖。
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# Vue Profile
|
||||
|
||||
- Vue 应用位于 `frontend/<应用名>/`,从 package manifest、lockfile 和 Vite/Nuxt 配置核对版本。
|
||||
- Vue 3 优先 Composition API 与 `<script setup>`;以项目现有 TypeScript/JavaScript策略为准。
|
||||
- props、emits、slots、template refs 和路由参数保持类型安全;避免解构破坏响应性。
|
||||
- 服务端状态、全局状态和局部状态职责分离;Pinia 等 store 不承载页面瞬时状态。
|
||||
- 标准检查映射 ESLint、vue-tsc、Vitest、组件/E2E 测试和生产构建。
|
||||
- 环境变量公开边界、base path、代理、HMR 和静态部署路径进入开发文档。
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
# 项目契约与标准命令
|
||||
|
||||
`.crmeb/project.json` 是人和 AI 的机器可读入口,用于描述项目组成和命令映射。manifest、lockfile、路由、Compose 等仍是具体事实真源;项目契约负责把这些入口串起来并由检查脚本防漂移。
|
||||
|
||||
项目进入 `active` 前必须已经根据当前后端运行环境生成或完善根 `docker-compose.yml`,并设置 `docker.enabled: true`。`docker.enabled: false` 只允许存在于初始化过程的 `draft` 契约或明确的“仅审计”结果中。
|
||||
|
||||
## 必备结构
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"status": "active",
|
||||
"project": {
|
||||
"name": "example",
|
||||
"type": "fullstack",
|
||||
"versionSource": "backend/VERSION"
|
||||
},
|
||||
"directories": {
|
||||
"backend": "backend",
|
||||
"frontend": "frontend",
|
||||
"docs": "help/docs",
|
||||
"process": "tests",
|
||||
"tests": "tests",
|
||||
"upgrades": "upgrades"
|
||||
},
|
||||
"applications": [],
|
||||
"services": [],
|
||||
"docker": {
|
||||
"enabled": true,
|
||||
"composeFile": "docker-compose.yml",
|
||||
"configRoot": "docker"
|
||||
},
|
||||
"documentation": {
|
||||
"required": ["README.md", "help/docs/development.md", "help/docs/docker-development.md"]
|
||||
},
|
||||
"commands": {
|
||||
"doctor": "node scripts/docker-project.mjs doctor",
|
||||
"setup": "node scripts/docker-project.mjs setup",
|
||||
"dev": "node scripts/docker-project.mjs dev",
|
||||
"restart": "node scripts/docker-project.mjs restart",
|
||||
"stop": "node scripts/docker-project.mjs stop",
|
||||
"down": "node scripts/docker-project.mjs down",
|
||||
"status": "node scripts/docker-project.mjs status",
|
||||
"logs": "node scripts/docker-project.mjs logs",
|
||||
"shell": "node scripts/docker-project.mjs shell",
|
||||
"check": "node scripts/check-project.mjs",
|
||||
"test": "node scripts/test-project.mjs",
|
||||
"smoke": "node scripts/smoke-project.mjs",
|
||||
"build": "node scripts/build-project.mjs",
|
||||
"resetData": "node scripts/docker-project.mjs reset-data",
|
||||
"upgradePlan": "node scripts/upgrade.mjs plan",
|
||||
"upgradePackage": "node scripts/upgrade.mjs package",
|
||||
"upgradeVerify": "node scripts/upgrade.mjs verify"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
示例只展示映射方式,必须替换为目标项目真实存在的命令。根 `run.sh` / `run.ps1` 调用通用执行器,再由执行器读取这里的底层命令,禁止把命令反向映射到 `run.sh` 造成递归。`status: draft` 表示仍需人工或 AI 补全;只有命令存在且通过验证后才能改为 `active`。
|
||||
|
||||
`directories` 记录知识目录归属:`docs` 是当前事实,`process` 是方案、评审与验收等过程文档目录,`tests` 是测试索引与编排入口。过程文档与测试索引统一在 `tests/`,因此 `process` 缺省即为 `tests`;确有独立过程文档目录的项目才单独声明,审计与检查脚本必须读取该字段,不得硬编码目录名。
|
||||
|
||||
## Applications
|
||||
|
||||
每个可独立运行或构建的应用记录:
|
||||
|
||||
- `id`:稳定标识;
|
||||
- `kind`:backend、admin、web、mobile、worker 等;
|
||||
- `path`:相对仓库根路径;
|
||||
- `runtime`:运行时与版本来源文件;
|
||||
- `devCommand`、`testCommand`、`buildCommand`;
|
||||
- `healthcheck` 或成功信号;
|
||||
- `dependsOn`:数据库、缓存或其他应用 ID。
|
||||
|
||||
## Services
|
||||
|
||||
数据库、缓存、消息队列、对象存储、搜索等服务记录:
|
||||
|
||||
- 服务 ID、用途、实现和版本来源;
|
||||
- 容器内端口与宿主机默认端口;
|
||||
- 健康检查;
|
||||
- 数据卷和持久化策略;
|
||||
- 是否为开发必需;
|
||||
- 配置变量名,不记录真实秘密。
|
||||
|
||||
## 标准命令语义
|
||||
|
||||
| 命令 | 统一语义 |
|
||||
| --- | --- |
|
||||
| `doctor` | 只读检查运行时、Docker、端口、配置和依赖,不修改系统 |
|
||||
| `setup` | 创建本地配置并安装依赖;破坏性初始化必须拆成单独命令 |
|
||||
| `dev` | 启动完整本地开发环境,重复执行应安全 |
|
||||
| `restart` | 重启全部或指定服务,不删除数据 |
|
||||
| `stop` | 停止开发环境但保留数据和依赖缓存 |
|
||||
| `down` | 删除本地容器和网络但保留具名数据卷 |
|
||||
| `status` | 展示应用、服务、端口和健康状态 |
|
||||
| `logs` | 查看或跟随日志,可选择具体服务 |
|
||||
| `shell` | 进入明确指定的开发容器 |
|
||||
| `check` | 格式、lint、类型、架构、文档和配置静态门禁 |
|
||||
| `test` | 执行项目默认测试集合,并说明外部依赖 |
|
||||
| `smoke` | 对运行中的关键页面、API 和后台进程做最小可用验证 |
|
||||
| `build` | 从干净依赖生成所有发布产物 |
|
||||
| `upgrade-*` | 规划、生成和验证升级包;不得隐式发布 |
|
||||
|
||||
破坏性操作使用明显名称,例如 `reset-data`、`delete-volumes`,要求交互确认或显式 `--yes`。不得让 `setup`、`dev`、`stop` 默认清库。
|
||||
|
||||
## 防漂移
|
||||
|
||||
- 契约中的路径必须存在;可选应用需显式标注 `enabled: false`。
|
||||
- 命令必须能从仓库根执行,或通过脚本稳定切换目录。
|
||||
- 端口和版本应引用配置来源;环境可覆盖时记录变量名。
|
||||
- CI 和 README 应调用同一标准命令,不维护另一套命令清单。
|
||||
- 修改应用、服务或命令时,同一变更更新项目契约并运行检查脚本。
|
||||
@ -0,0 +1,33 @@
|
||||
# 安全与可观测性
|
||||
|
||||
## 安全底线
|
||||
|
||||
- 秘钥、令牌、私钥、Cookie、真实账号和生产数据不得入库、日志或升级包。
|
||||
- 服务端对每个资源执行认证、授权和对象所有权校验,不能依赖前端隐藏按钮。
|
||||
- SQL 使用参数绑定;上传校验类型、大小、扩展名、存储位置和访问权限。
|
||||
- 回调、支付和 webhook 验证签名、时间窗、重放与幂等。
|
||||
- 外部 URL、命令、模板和反序列化输入按风险防 SSRF、注入、路径穿越和代码执行。
|
||||
- 依赖使用锁文件和可信源,定期扫描漏洞;高风险依赖升级需回归与发布说明。
|
||||
- 默认最小权限,开发、测试、生产账号和网络隔离。
|
||||
|
||||
## 日志与追踪
|
||||
|
||||
- 使用结构化日志,统一时间、级别、服务、环境、request/trace/job ID、动作和结果字段。
|
||||
- 日志写业务标识的安全形式,不记录完整请求体、认证头和敏感个人信息。
|
||||
- HTTP、队列、定时任务和外部调用传播关联 ID;异常保留可定位根因的内部上下文。
|
||||
- 定义日志保留、轮转和访问权限,调试日志不得永久开启。
|
||||
|
||||
## 指标与健康检查
|
||||
|
||||
- 区分 liveness、readiness 和业务 smoke;健康检查不执行高成本写操作。
|
||||
- 监控请求率、错误率、延迟、资源、数据库、缓存、队列积压和任务失败。
|
||||
- 为关键业务结果定义指标和告警,告警包含影响、检查入口和处理手册链接。
|
||||
- 发布与升级标记进入日志/指标,便于关联异常变化。
|
||||
|
||||
## 性能与可靠性
|
||||
|
||||
- 查询分页、缓存失效、事务时长、超时、重试、熔断和限流有明确策略。
|
||||
- 重试只用于可重试错误并采用退避与上限;非幂等操作不可盲目重试。
|
||||
- 外部依赖故障有降级或清晰失败,不无限等待或吞错。
|
||||
- 性能优化必须有基线和验证数据,不以复杂缓存替代正确性。
|
||||
|
||||
248
.crmeb/skills/project-standard-bootstrap/references/templates.md
Normal file
248
.crmeb/skills/project-standard-bootstrap/references/templates.md
Normal file
@ -0,0 +1,248 @@
|
||||
# 可复用模板
|
||||
|
||||
以下模板是内容骨架,不是必须保留的章节。填写前先用代码和配置核对事实;不适用的章节直接删除。
|
||||
|
||||
## 根 README
|
||||
|
||||
```markdown
|
||||
# 项目名称
|
||||
|
||||
一句话说明项目面向谁、解决什么问题。
|
||||
|
||||
## 项目组成
|
||||
|
||||
用短表格说明主要应用/服务、目录和职责。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置环境
|
||||
|
||||
列出版本要求,并注明来源文件。
|
||||
|
||||
### 本地运行
|
||||
|
||||
1. 安装依赖:命令、执行目录。
|
||||
2. 创建配置:从哪个示例复制、哪些变量必填。
|
||||
3. 初始化依赖:数据库/缓存/迁移命令。
|
||||
4. 启动应用:命令和默认访问地址。
|
||||
5. 验证成功:健康检查、页面或最小测试。
|
||||
|
||||
## 常用命令
|
||||
|
||||
开发、构建、测试、lint、类型检查;只列真实存在的命令。
|
||||
|
||||
## 文档导航
|
||||
|
||||
- 第一次开发:链接
|
||||
- 理解架构:链接
|
||||
- API 与数据:链接
|
||||
- 测试与验证:链接
|
||||
- 部署与运维:链接
|
||||
- AI 协作规则:链接
|
||||
|
||||
## 安全提示
|
||||
|
||||
仅保留对本项目真实且重要的提示。
|
||||
```
|
||||
|
||||
## 正式文档索引
|
||||
|
||||
```markdown
|
||||
# 项目文档
|
||||
|
||||
本文档描述当前已实现状态。方案和历史记录位于过程文档目录。
|
||||
|
||||
## 按任务查找
|
||||
|
||||
| 你要做什么 | 阅读内容 | 适用范围 |
|
||||
| --- | --- | --- |
|
||||
| 从零运行项目 | 开发运行指南 | 全体开发者 |
|
||||
| 理解系统边界 | 项目概览、架构 | 开发与维护者 |
|
||||
| 开发接口或页面 | API、前端/后端约定 | 对应子项目 |
|
||||
| 修改数据结构 | 数据库与迁移 | 后端与运维 |
|
||||
| 编写或运行测试 | 测试索引 | 开发与 CI |
|
||||
| 部署和排障 | 部署、运维 | 运维与负责人 |
|
||||
```
|
||||
|
||||
## 开发运行指南
|
||||
|
||||
```markdown
|
||||
# 开发运行指南
|
||||
|
||||
## 环境要求
|
||||
|
||||
列出语言、包管理器、数据库、缓存、容器及版本证据。
|
||||
|
||||
## 目录与应用入口
|
||||
|
||||
说明各命令应在哪个目录执行。后端统一位于 `backend/`;每个前端应用统一位于 `frontend/<应用名>/`。
|
||||
|
||||
## 安装依赖
|
||||
|
||||
给出可复制命令、前置条件和成功信号。
|
||||
|
||||
## 配置
|
||||
|
||||
列出环境变量名、用途、是否必填、默认值来源;不包含真实秘密。
|
||||
|
||||
## 初始化与启动
|
||||
|
||||
按实际依赖顺序列步骤,区分本地和容器方式。
|
||||
|
||||
Docker 本地开发使用独立的 `docker-development.md`,覆盖服务/端口、首次启动、源码挂载、日常命令、依赖重建、数据库持久化和故障排查。
|
||||
|
||||
## 构建与测试
|
||||
|
||||
列出项目真实脚本,并链接测试索引。
|
||||
|
||||
## 常见问题
|
||||
|
||||
只收录已复现的问题、症状、原因和验证过的处理方式。
|
||||
```
|
||||
|
||||
## 独立应用说明
|
||||
|
||||
为后端及每个 `frontend/<应用名>/` 分别生成应用说明,不能复制同一份模板后只改标题。应用目录 `README.md` 保持短小,提供应用定位、最短运行命令、验证信号和详细文档链接;`help/docs/applications/<应用ID>.md` 记录从当前代码核对出的完整事实。
|
||||
|
||||
```markdown
|
||||
# 应用名称
|
||||
|
||||
## 用途、用户与边界
|
||||
## 技术栈、版本及证据来源
|
||||
## 目录、入口、路由/页面与关键模块
|
||||
## 配置、环境变量、端口和依赖服务
|
||||
## 安装、开发、检查、测试、构建和预览
|
||||
## API、鉴权、状态与错误处理
|
||||
## 构建产物、部署/发布目标与平台差异
|
||||
## 调试、日志、常见故障和已知限制
|
||||
## 关联架构、数据库、Docker 与测试文档
|
||||
```
|
||||
|
||||
生成或更新前必须检查该应用自己的 manifest、lockfile、构建配置、入口/路由、环境样例、API 客户端、测试和 CI;不得从另一个前端或后端应用推断命令与端口。
|
||||
|
||||
## AI 入口
|
||||
|
||||
```markdown
|
||||
# AI 开发入口
|
||||
|
||||
处理本仓库任务前:
|
||||
|
||||
1. 阅读规则唯一来源:链接。
|
||||
2. 按目标文件所在目录继续读取更具体的作用域规则。
|
||||
3. 从技能索引选择与任务匹配的技能。
|
||||
4. 修改前检查工作区状态,保留已有改动。
|
||||
5. 完成后执行该模块规定的最小验证,并准确报告未覆盖项。
|
||||
|
||||
项目事实以正式文档和可执行配置为准;待实施方案不得视为当前能力。
|
||||
```
|
||||
|
||||
AI 入口保持简洁。编码细则、目录规则、前端/后端约定和专项工作流分别进入规则或技能文件。
|
||||
|
||||
规则正文固定维护在 `.crmeb/`。根级及工具专属入口只指向 `.crmeb/AGENTS.md`,不得复制规则全文。
|
||||
|
||||
同时生成并校验 `AGENTS.md`、`CLAUDE.md`、`GEMINI.md`、`.cursor/rules/project-standard.mdc`、`.github/copilot-instructions.md` 和 `llms.txt`;入口内容和发现机制见 [AI 工具适配](ai-tool-adapters.md)。
|
||||
|
||||
## Docker 本地开发文档
|
||||
|
||||
使用 [Docker 本地开发规范](docker-local-development.md) 中的完整要求生成 `help/docs/docker-development.md`。建议结构:
|
||||
|
||||
```markdown
|
||||
# Docker 本地开发
|
||||
|
||||
## 适用范围与已验证版本
|
||||
## 服务、端口、健康检查与数据卷
|
||||
## 首次启动
|
||||
## 日常启动、停止与日志
|
||||
## 本地源码挂载与热更新
|
||||
## 安装依赖与重建镜像
|
||||
## 数据库初始化、迁移、备份与恢复
|
||||
## 在容器内运行测试和调试
|
||||
## 安全清理与数据保留
|
||||
## 常见故障
|
||||
## 与生产环境的差异
|
||||
```
|
||||
|
||||
不要只写 `docker compose up -d`。每个命令都要说明执行目录、前置条件、成功信号和数据影响。
|
||||
|
||||
完整项目还应按 [文档覆盖矩阵](documentation-coverage.md) 生成项目概览、配置、前后端、API、数据库、部署、运维、安全、排障、备份恢复和升级回滚文档。只创建标题或保留占位符不算完成;不适用的文档应在索引中说明原因,而不是静默遗漏。
|
||||
|
||||
## 升级与发布文档
|
||||
|
||||
需要支持存量项目升级时,使用 [自动升级包规范](upgrade-package.md) 生成升级定义、打包命令和验证流程。正式文档建议包含:
|
||||
|
||||
```markdown
|
||||
# 升级与回滚
|
||||
|
||||
## 版本策略与唯一版本源
|
||||
## 支持的来源版本和升级路径
|
||||
## 升级包目录、manifest 与校验和
|
||||
## 生成升级计划和升级包
|
||||
## 升级前预检与备份
|
||||
## 安装升级包
|
||||
## 数据库迁移与基础数据处理
|
||||
## 升级后健康检查
|
||||
## 失败停止、恢复与回滚边界
|
||||
## 插件/扩展包与核心升级包边界
|
||||
## 发布记录和制品留存
|
||||
```
|
||||
|
||||
不得把“覆盖文件并执行 SQL”作为完整升级说明。文档必须包含来源版本限制、备份、校验、失败处理和实际验证范围。
|
||||
|
||||
## 测试索引
|
||||
|
||||
```markdown
|
||||
# 测试与验证
|
||||
|
||||
## 测试地图
|
||||
|
||||
| 层级 | 目录/配置 | 命令 | 前置依赖 | 覆盖范围 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 静态检查 | ... | ... | ... | ... |
|
||||
| 单元测试 | ... | ... | ... | ... |
|
||||
| 集成测试 | ... | ... | ... | ... |
|
||||
| 端到端/冒烟 | ... | ... | ... | ... |
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
从低成本到高成本说明执行顺序和失败定位方式。
|
||||
|
||||
## 测试数据与清理
|
||||
|
||||
说明 fixture、测试账号、隔离策略和安全清理方式,不放真实凭据。
|
||||
|
||||
## 已知覆盖边界
|
||||
|
||||
明确哪些模块或生命周期尚未自动验证。
|
||||
```
|
||||
|
||||
## 过程文档头部
|
||||
|
||||
```markdown
|
||||
# 方案或报告名称
|
||||
|
||||
> 性质:待评审 / 实施中 / 已实施 / 已归档
|
||||
> 最近核对:YYYY-MM-DD
|
||||
> 适用范围:目录、模块或版本
|
||||
|
||||
## 背景与证据
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
## 方案或检查结果
|
||||
|
||||
## 验收标准
|
||||
|
||||
## 风险、回滚与未覆盖项
|
||||
```
|
||||
|
||||
## 文档变更交付摘要
|
||||
|
||||
```markdown
|
||||
已完成项目文档体系的审计与完善:
|
||||
|
||||
- 新增/修复:列出入口和职责变化。
|
||||
- 事实核对:列出版本、命令、路径、路由等证据来源。
|
||||
- 已运行验证:逐条列命令和结果。
|
||||
- 未运行验证:列原因和影响范围。
|
||||
- 待确认:只列必须由项目负责人决策的事项。
|
||||
```
|
||||
@ -0,0 +1,40 @@
|
||||
# 测试与 CI 规范
|
||||
|
||||
## 测试地图
|
||||
|
||||
| 层级 | 目标 | 典型对象 |
|
||||
| --- | --- | --- |
|
||||
| 静态检查 | 快速阻止格式、类型、依赖和文档错误 | lint、typecheck、架构、配置、链接 |
|
||||
| 单元测试 | 验证纯业务规则和边界条件 | domain、service、utility、composable |
|
||||
| 集成测试 | 验证数据库、缓存、队列和外部适配 | repository、API、migration、adapter |
|
||||
| 契约测试 | 保证生产者与消费者兼容 | OpenAPI/schema、事件、消息、webhook |
|
||||
| 端到端/冒烟 | 验证关键用户链路和部署结果 | 登录、核心交易、管理操作、健康检查 |
|
||||
| 生命周期测试 | 验证安装、升级、回滚和插件启停 | 新安装、旧版升级、失败恢复 |
|
||||
|
||||
## 统一门禁
|
||||
|
||||
- `check`:格式、lint、类型、架构、文档、配置与密钥扫描。
|
||||
- `test`:默认可在开发机和 CI 运行的测试集合;慢测试可拆分但需登记。
|
||||
- `build`:从干净依赖生成所有发布应用和制品。
|
||||
- CI 必须调用与本地相同的标准命令,不在流水线复制另一套隐藏逻辑。
|
||||
|
||||
## 测试要求
|
||||
|
||||
- 测试数据可重复创建和清理,不依赖开发者个人数据库状态。
|
||||
- 外部服务默认使用 fake、sandbox 或受控测试实例;真实写入需明确授权。
|
||||
- 时间、随机、网络和并发行为可控,失败信息包含场景和关键上下文。
|
||||
- 修复缺陷时增加能在修复前失败、修复后通过的回归测试。
|
||||
- 覆盖率只作信号,核心业务不变量和失败路径优先于追求数字。
|
||||
|
||||
## CI 顺序
|
||||
|
||||
1. 安装锁定依赖并验证 lockfile;
|
||||
2. 静态 `check`;
|
||||
3. 单元和集成 `test`;
|
||||
4. 全量 `build`;
|
||||
5. Docker/制品 smoke;
|
||||
6. 迁移和升级包验证;
|
||||
7. 只有全部门禁通过后才允许发布阶段。
|
||||
|
||||
报告缓存命中和跳过原因。缺少秘密或依赖导致的跳过不能写成通过。
|
||||
|
||||
@ -0,0 +1,171 @@
|
||||
# 自动升级包规范
|
||||
|
||||
本规范用于把已经完成并验证的程序变更自动归档为可追溯、可校验、可安装的增量升级包。它不是完整源码备份,也不替代正式发布流程。
|
||||
|
||||
## 触发判定
|
||||
|
||||
以下变更通常触发升级包:
|
||||
|
||||
- `backend/` 中的业务代码、配置模板、命令或运行资源;
|
||||
- `frontend/<应用名>/` 中需要发布的代码,包内通常收录构建产物;
|
||||
- 数据库结构、基础数据、权限、菜单、任务、配置项等迁移;
|
||||
- 反向代理、进程、容器或部署配置的可交付变化;
|
||||
- API 契约变化要求后端和前端同步升级。
|
||||
|
||||
纯文档、测试源码、CI 配置、开发过程记录通常不触发程序升级包。项目拥有独立插件或扩展体系时,插件自身文件和迁移必须进入插件包,不得混入核心升级包;核心新增扩展契约时才把核心部分放入升级包,并声明所需插件版本。
|
||||
|
||||
开始打包前使用 `git diff --name-status`、任务基线提交和构建清单确定本次真实变更。工作区含有其他人的未提交改动时,不得把整个工作区直接归档。
|
||||
|
||||
## 统一目录
|
||||
|
||||
```text
|
||||
upgrades/
|
||||
versions/
|
||||
<target-version>/
|
||||
manifest.json 升级定义与约束真源
|
||||
CHANGELOG.md 面向使用者的变化和人工步骤
|
||||
migrations/ 数据结构和基础数据迁移
|
||||
hooks/ preflight/apply/postcheck/rollback(按需)
|
||||
packages/
|
||||
<project>-<from>-to-<target>.zip
|
||||
```
|
||||
|
||||
`versions/` 中的文本定义和迁移应入库;`packages/` 是可重复生成的发布产物,通常由 `.gitignore` 排除并交给制品仓库保存。目标项目已有成熟升级目录时保留其结构,但必须满足同等职责。
|
||||
|
||||
生成的压缩包建议采用以下结构:
|
||||
|
||||
```text
|
||||
manifest.json
|
||||
CHANGELOG.md
|
||||
checksums.sha256
|
||||
payload/
|
||||
backend/ 后端源码或编译产物,按技术栈决定
|
||||
frontend/<app>/ 各前端应用的发布构建产物
|
||||
config/ 允许覆盖的非敏感配置模板
|
||||
migrations/
|
||||
hooks/
|
||||
```
|
||||
|
||||
不得打包 `.env`、密钥、日志、缓存、runtime、上传文件、数据库备份、IDE 配置、测试数据、宿主机依赖目录或用户生成内容。是否包含 `vendor`、`node_modules`、二进制依赖由项目部署模型决定,并在 manifest 中明确;默认不包含。
|
||||
|
||||
## Manifest 必备字段
|
||||
|
||||
`manifest.json` 至少记录:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"project": "project-id",
|
||||
"version": "1.4.0",
|
||||
"from_versions": [">=1.3.0 <1.4.0"],
|
||||
"created_at": "ISO-8601 UTC time",
|
||||
"source_commit": "full git commit id",
|
||||
"components": ["backend", "frontend/admin", "database"],
|
||||
"requires": {
|
||||
"runtime": {},
|
||||
"database": {},
|
||||
"extensions": []
|
||||
},
|
||||
"migrations": [],
|
||||
"hooks": {},
|
||||
"backup": {
|
||||
"required": true,
|
||||
"scope": []
|
||||
},
|
||||
"rollback": {
|
||||
"supported": false,
|
||||
"instructions": "CHANGELOG.md"
|
||||
},
|
||||
"breaking_changes": [],
|
||||
"manual_steps": [],
|
||||
"files": []
|
||||
}
|
||||
```
|
||||
|
||||
字段值必须由版本配置、Git、构建结果和迁移定义生成,不能保留示例值。`from_versions` 应明确允许的来源版本;跨多个版本时必须证明迁移链连续。`files` 记录相对路径、类型、大小和 SHA-256,压缩包根目录另生成 `checksums.sha256`。
|
||||
|
||||
## 版本规则
|
||||
|
||||
1. 从项目唯一版本源读取当前版本,并与发布标签、manifest、前后端版本字段核对。
|
||||
2. 优先使用项目已经声明的 SemVer 或版本策略,不擅自发明编号规则。
|
||||
3. 已发布或已登记为执行成功的版本定义、迁移和 manifest 禁止回改;新增内容必须创建更高版本。
|
||||
4. 若存在尚未发布的当前开发版本,可在不破坏已验证迁移顺序的前提下合并;否则创建新版本。
|
||||
5. 无法唯一判断目标版本或升级基线时,先生成升级计划和变更清单,不得冒充最终发布包。
|
||||
6. 版本提升、Git tag、发布和上传制品是不同动作;生成本地升级包不自动授权推送或发布。
|
||||
|
||||
## 数据迁移
|
||||
|
||||
- 迁移按稳定顺序执行,每项有唯一 ID,升级器记录执行状态。
|
||||
- 建表、加字段、加索引和写基础数据应尽可能幂等;重复执行不能造成重复数据或不可控失败。
|
||||
- UPDATE/DELETE 必须有精确条件,并在需要时记录影响行数上限或预检结果。
|
||||
- 大表变更采用分阶段兼容方案,避免长事务锁表;先兼容读写,再迁移数据,最后清理旧结构。
|
||||
- 新安装 schema 与存量升级迁移必须得到相同的最终结构和基础数据。
|
||||
- 不可逆迁移必须在 manifest 标记、强制备份并提供人工恢复方法;不得伪造 rollback 脚本。
|
||||
- 迁移涉及插件或扩展数据时遵守其所有权边界,不直接修改不属于核心的私有数据。
|
||||
|
||||
## 自动生成流程
|
||||
|
||||
目标项目应提供统一命令,命令名可映射到现有构建系统:
|
||||
|
||||
```bash
|
||||
./run.sh upgrade-plan <target-version>
|
||||
./run.sh upgrade-package <target-version>
|
||||
./run.sh upgrade-verify <package-path>
|
||||
```
|
||||
|
||||
自动流程必须:
|
||||
|
||||
1. 确认基线版本、目标版本、源码提交和工作区范围;
|
||||
2. 收集任务相关文件,区分核心、插件、文档、测试和发布产物;
|
||||
3. 检查数据库或基础数据变化是否拥有迁移,并检查迁移链连续性;
|
||||
4. 执行后端构建或依赖准备,构建所有受影响的前端应用;
|
||||
5. 只复制发布所需文件,保持相对路径、文件权限和大小写;
|
||||
6. 生成 manifest、CHANGELOG、文件清单和 SHA-256;
|
||||
7. 扫描禁止内容和疑似密钥,确认包内没有路径穿越或指向包外的软链接;
|
||||
8. 生成确定性压缩包,同一提交和配置重复构建应得到相同文件清单;
|
||||
9. 执行静态校验和隔离升级测试;
|
||||
10. 输出版本、来源提交、包路径、校验和、覆盖组件和未验证项。
|
||||
|
||||
构建脚本不得使用整个仓库复制后再靠黑名单删除的方式出包;应由 manifest 和受影响组件白名单收集文件,降低混入凭据与运行数据的风险。
|
||||
|
||||
## 安装器执行顺序
|
||||
|
||||
通用安全顺序为:
|
||||
|
||||
1. 解析并校验 manifest schema、项目 ID、来源版本和运行环境;
|
||||
2. 验证包级及逐文件校验和;
|
||||
3. 检查磁盘空间、目录权限、依赖服务和维护模式能力;
|
||||
4. 按 manifest 创建代码、配置和数据库备份;
|
||||
5. 进入维护模式或停止写入流量;
|
||||
6. 解压到临时目录,拒绝绝对路径、`..` 路径穿越和危险软链接;
|
||||
7. 原子替换或分阶段部署后端及前端产物;
|
||||
8. 执行迁移和必要的数据处理;
|
||||
9. 清理缓存、重载进程并运行 postcheck;
|
||||
10. 健康检查成功后退出维护模式并写入升级日志;
|
||||
11. 失败时停止后续步骤,按声明恢复文件和数据,保留可排障日志。
|
||||
|
||||
不同技术栈可调整文件与迁移先后,但必须避免新代码与旧数据库长期不兼容。使用滚动发布时,迁移应同时兼容升级中的新旧实例。
|
||||
|
||||
## 验证矩阵
|
||||
|
||||
| 验证 | 最低要求 |
|
||||
| --- | --- |
|
||||
| 静态包检查 | manifest schema、允许来源版本、文件清单、禁止内容、路径安全、SHA-256 全部通过 |
|
||||
| 构建验证 | 所有受影响的后端和前端产物从干净环境生成 |
|
||||
| 新安装一致性 | 新安装与执行全部迁移后的 schema/基础数据一致 |
|
||||
| 幂等性 | 可重复的迁移在临时库连续执行两次结果稳定;一次性迁移能被升级日志可靠跳过 |
|
||||
| 单级升级 | 最近稳定版到目标版成功,健康检查和核心 smoke 通过 |
|
||||
| 跨级升级 | 声明支持的最低版本沿完整迁移链升级成功 |
|
||||
| 失败恢复 | 至少验证一个迁移或 postcheck 失败场景,确认不会标记为成功且备份可用 |
|
||||
| 重启验证 | 升级后重启应用、worker、scheduler 等进程仍正常 |
|
||||
|
||||
若无法运行数据库、构建或完整升级环境,包只能标记为“候选/未完成动态验证”,不能宣称可发布。
|
||||
|
||||
## CI 与交付门禁
|
||||
|
||||
- 合并前校验版本单调递增、迁移链连续、manifest 合法、禁止文件和校验和。
|
||||
- 发布流水线从干净提交重新构建升级包,不信任开发机生成的未知产物。
|
||||
- 制品仓库保存压缩包、SHA-256、来源提交、构建日志和验证结果。
|
||||
- 发布说明必须报告目标版本、允许来源版本、包路径或制品地址、SHA-256、迁移概要、备份要求、人工步骤与未验证项。
|
||||
- 未通过隔离升级测试、含未解决高风险迁移或无法确定来源版本时阻止正式发布。
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const write = args.includes('--write')
|
||||
const target = path.resolve(args.find(arg => !arg.startsWith('--')) || process.cwd())
|
||||
|
||||
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
||||
console.error(`目标仓库不存在或不是目录:${target}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const exists = relative => fs.existsSync(path.join(target, relative))
|
||||
const read = (relative) => {
|
||||
try { return fs.readFileSync(path.join(target, relative), 'utf8') }
|
||||
catch { return '' }
|
||||
}
|
||||
const json = (relative) => {
|
||||
try { return JSON.parse(read(relative)) }
|
||||
catch { return null }
|
||||
}
|
||||
const first = values => values.find(Boolean) || ''
|
||||
const versionFromRange = value => String(value || '').match(/\d+(?:\.\d+){0,2}/)?.[0] || ''
|
||||
|
||||
function packageManager(base) {
|
||||
if (exists(path.join(base, 'pnpm-lock.yaml'))) return 'pnpm'
|
||||
if (exists(path.join(base, 'yarn.lock'))) return 'yarn'
|
||||
if (exists(path.join(base, 'bun.lockb')) || exists(path.join(base, 'bun.lock'))) return 'bun'
|
||||
if (exists(path.join(base, 'package-lock.json'))) return 'npm'
|
||||
return 'npm-install'
|
||||
}
|
||||
|
||||
function nodeApp(relative, kind) {
|
||||
const manifest = json(path.join(relative, 'package.json')) || {}
|
||||
const deps = { ...(manifest.dependencies || {}), ...(manifest.devDependencies || {}) }
|
||||
const framework = first([
|
||||
deps['@dcloudio/uni-app'] && 'uniapp', deps.nuxt && 'nuxt', deps.next && 'next',
|
||||
deps.vue && 'vue', deps.react && 'react', deps.express && 'express', deps.fastify && 'fastify',
|
||||
deps['@nestjs/core'] && 'nestjs',
|
||||
]) || 'node'
|
||||
const scripts = manifest.scripts || {}
|
||||
const manager = packageManager(relative)
|
||||
const commandManager = manager === 'npm-install' ? 'npm' : manager
|
||||
const run = name => commandManager === 'npm' ? `npm run ${name}` : `${commandManager} ${name}`
|
||||
const devScript = first(['dev', 'serve', 'start', 'dev:h5', 'dev:mp-weixin'].map(name => scripts[name] && run(name)))
|
||||
const buildScript = first(['build', 'build:h5'].map(name => scripts[name] && run(name)))
|
||||
const configSource = first(['vite.config.ts', 'vite.config.js', 'vite.config.mjs', 'nuxt.config.ts', 'next.config.js'].map(file => exists(path.join(relative, file)) && path.join(relative, file)))
|
||||
const scriptPort = Object.values(scripts).join('\n').match(/--port(?:=|\s+)(\d+)/)?.[1]
|
||||
const configPort = configSource && read(configSource).match(/\bport\s*:\s*(\d+)/)?.[1]
|
||||
return {
|
||||
id: relative.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '') || kind,
|
||||
kind, path: relative || '.', runtime: 'node', framework, packageManager: manager,
|
||||
version: versionFromRange(manifest.engines?.node), versionSource: `${relative ? `${relative}/` : ''}package.json`,
|
||||
port: Number(scriptPort || configPort) || null,
|
||||
portSource: scriptPort ? `${relative ? `${relative}/` : ''}package.json#scripts` : configPort ? configSource : '',
|
||||
commands: { install: manager === 'npm' ? 'npm ci' : manager === 'npm-install' ? 'npm install' : `${manager} install --frozen-lockfile`, dev: devScript, build: buildScript },
|
||||
}
|
||||
}
|
||||
|
||||
function detectBackend() {
|
||||
const bases = exists('backend') ? ['backend'] : ['']
|
||||
for (const base of bases) {
|
||||
const prefix = base ? `${base}/` : ''
|
||||
if (exists(`${prefix}composer.json`)) {
|
||||
const manifest = json(`${prefix}composer.json`) || {}
|
||||
const require = manifest.require || {}
|
||||
return {
|
||||
id: 'backend', kind: 'backend', path: base || '.', runtime: 'php',
|
||||
framework: require['topthink/framework'] ? 'thinkphp' : require['laravel/framework'] ? 'laravel' : require['symfony/framework-bundle'] ? 'symfony' : 'php',
|
||||
version: versionFromRange(require.php), versionSource: `${prefix}composer.json`,
|
||||
extensions: Object.keys(require).filter(name => name.startsWith('ext-')).map(name => name.slice(4)).sort(),
|
||||
commands: { install: 'composer install', dev: '', build: '' },
|
||||
}
|
||||
}
|
||||
if (exists(`${prefix}package.json`)) return nodeApp(base, 'backend')
|
||||
if (exists(`${prefix}pyproject.toml`) || exists(`${prefix}requirements.txt`)) {
|
||||
const source = exists(`${prefix}pyproject.toml`) ? `${prefix}pyproject.toml` : `${prefix}requirements.txt`
|
||||
return { id: 'backend', kind: 'backend', path: base || '.', runtime: 'python', framework: 'python', version: '', versionSource: source, commands: { install: exists(`${prefix}requirements.txt`) ? 'pip install -r requirements.txt' : 'pip install -e .', dev: '', build: '' } }
|
||||
}
|
||||
if (exists(`${prefix}pom.xml`) || exists(`${prefix}build.gradle`) || exists(`${prefix}build.gradle.kts`)) {
|
||||
const source = first(['pom.xml', 'build.gradle', 'build.gradle.kts'].map(file => exists(`${prefix}${file}`) && `${prefix}${file}`))
|
||||
return { id: 'backend', kind: 'backend', path: base || '.', runtime: 'java', framework: 'java', version: '', versionSource: source, commands: { install: '', dev: exists(`${prefix}mvnw`) ? './mvnw spring-boot:run' : exists(`${prefix}gradlew`) ? './gradlew bootRun' : '', build: '' } }
|
||||
}
|
||||
if (exists(`${prefix}go.mod`)) return { id: 'backend', kind: 'backend', path: base || '.', runtime: 'go', framework: 'go', version: read(`${prefix}go.mod`).match(/^go\s+([^\s]+)/m)?.[1] || '', versionSource: `${prefix}go.mod`, commands: { install: 'go mod download', dev: 'go run .', build: 'go build ./...' } }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function detectFrontends() {
|
||||
const roots = []
|
||||
const candidates = ['frontend', 'admin', 'web', 'uniapp', 'mobile', 'apps']
|
||||
for (const candidate of candidates) {
|
||||
if (!exists(candidate)) continue
|
||||
if (exists(`${candidate}/package.json`)) roots.push(candidate)
|
||||
for (const entry of fs.readdirSync(path.join(target, candidate), { withFileTypes: true })) {
|
||||
const relative = `${candidate}/${entry.name}`
|
||||
if (entry.isDirectory() && exists(`${relative}/package.json`)) roots.push(relative)
|
||||
}
|
||||
}
|
||||
return [...new Set(roots)].filter(relative => relative !== 'backend').map(relative => nodeApp(relative, 'frontend'))
|
||||
}
|
||||
|
||||
const backend = detectBackend()
|
||||
const frontends = detectFrontends()
|
||||
const evidence = [
|
||||
'docker-compose.yml', 'compose.yaml', 'compose.yml', '.env.example', '.env.sample',
|
||||
backend?.versionSource, ...frontends.map(app => app.versionSource),
|
||||
].filter(relative => relative && exists(relative))
|
||||
const searchable = evidence.map(read).join('\n').toLowerCase()
|
||||
const composerRequire = backend?.runtime === 'php' ? (json(path.join(backend.path === '.' ? '' : backend.path, 'composer.json'))?.require || {}) : {}
|
||||
const services = []
|
||||
if (/mysql|mariadb|db_host|database_url/.test(searchable) || composerRequire['ext-pdo_mysql']) services.push({ id: 'db', engine: 'mysql', defaultPort: 3306, required: true })
|
||||
else if (/postgres|pgsql|postgis/.test(searchable) || composerRequire['ext-pdo_pgsql']) services.push({ id: 'db', engine: 'postgres', defaultPort: 5432, required: true })
|
||||
if (/redis|redis_host/.test(searchable) || composerRequire['ext-redis']) services.push({ id: 'redis', engine: 'redis', defaultPort: 6379, required: true })
|
||||
|
||||
const existingDocker = ['docker-compose.yml', 'compose.yaml', 'compose.yml'].filter(exists)
|
||||
const warnings = []
|
||||
if (!backend) warnings.push('未识别后端运行时;生成 Docker 前必须定位真实启动入口。')
|
||||
if (backend?.path === '.') warnings.push('后端当前位于仓库根目录;应先记录兼容映射,再规划迁移到 backend/,禁止直接移动。')
|
||||
if (backend && !backend.commands.dev && backend.runtime !== 'php') warnings.push('未能可靠识别后端开发启动命令。')
|
||||
if (!services.length) warnings.push('未从非敏感样例和依赖清单识别数据库或缓存;请核对外部服务。')
|
||||
|
||||
const requiredDocs = [
|
||||
'README.md', 'help/docs/README.md', 'help/docs/project-overview.md', 'help/docs/architecture.md',
|
||||
'help/docs/development.md', 'help/docs/configuration.md', 'help/docs/api.md', 'help/docs/testing.md',
|
||||
'help/docs/deployment.md', 'help/docs/operations.md', 'help/docs/troubleshooting.md',
|
||||
'help/docs/security.md', 'help/docs/upgrade-and-rollback.md',
|
||||
]
|
||||
if (backend) {
|
||||
requiredDocs.push('help/docs/backend-development.md', `help/docs/applications/${backend.id}.md`)
|
||||
if (backend.path !== '.') requiredDocs.push(`${backend.path}/README.md`)
|
||||
}
|
||||
if (frontends.length) requiredDocs.push('help/docs/frontend-development.md')
|
||||
for (const app of frontends) {
|
||||
requiredDocs.push(`${app.path}/README.md`, `help/docs/applications/${app.id}.md`)
|
||||
}
|
||||
if (frontends.some(app => app.framework === 'uniapp')) requiredDocs.push('help/docs/uniapp-development.md')
|
||||
if (services.some(service => service.id === 'db')) requiredDocs.push('help/docs/database-and-migrations.md', 'help/docs/backup-and-recovery.md')
|
||||
requiredDocs.push('help/docs/docker-development.md')
|
||||
|
||||
const result = {
|
||||
schemaVersion: 1,
|
||||
repository: target,
|
||||
generatedAt: new Date().toISOString(),
|
||||
backend,
|
||||
frontends,
|
||||
services,
|
||||
docker: { existing: existingDocker, configRootExists: exists('docker'), rootEntrypoint: 'docker-compose.yml' },
|
||||
documentation: { required: [...new Set(requiredDocs)], missing: [...new Set(requiredDocs)].filter(relative => !exists(relative)) },
|
||||
warnings,
|
||||
evidence,
|
||||
}
|
||||
|
||||
if (write) {
|
||||
const destination = path.join(target, '.crmeb/environment.generated.json')
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.writeFileSync(destination, `${JSON.stringify(result, null, 2)}\n`)
|
||||
console.error(`已写入 ${path.relative(target, destination)}`)
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { collectFindings, detectApplications, pathExists, printFindings } from './lib.mjs'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const json = args.includes('--json')
|
||||
const targetArg = args.find(arg => !arg.startsWith('--')) || process.cwd()
|
||||
const target = path.resolve(targetArg)
|
||||
|
||||
if (!pathExists(target) || !fs.statSync(target).isDirectory()) {
|
||||
console.error(`目标仓库不存在或不是目录:${target}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const findings = collectFindings(target)
|
||||
const detected = detectApplications(target)
|
||||
const result = { repository: target, detectedApplications: detected, findings }
|
||||
|
||||
if (json) console.log(JSON.stringify(result, null, 2))
|
||||
else {
|
||||
console.log(`项目规范审计:${target}`)
|
||||
if (detected.length) {
|
||||
console.log('检测到的应用:')
|
||||
for (const app of detected) console.log(`- ${app.id}: ${app.path} (${app.runtime})`)
|
||||
}
|
||||
else console.log('未从 backend/frontend 检测到应用 manifest。')
|
||||
console.log('')
|
||||
printFindings(findings)
|
||||
}
|
||||
|
||||
// 审计用于收集差距,不以发现问题作为进程失败。
|
||||
process.exit(0)
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { collectFindings, collectMarkdownLinkFindings, pathExists, printFindings } from './lib.mjs'
|
||||
|
||||
const target = path.resolve(process.argv[2] || process.cwd())
|
||||
if (!pathExists(target) || !fs.statSync(target).isDirectory()) {
|
||||
console.error(`目标仓库不存在或不是目录:${target}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const findings = [...collectFindings(target), ...collectMarkdownLinkFindings(target)]
|
||||
printFindings(findings)
|
||||
process.exit(findings.some(item => item.severity === 'error') ? 1 : 0)
|
||||
|
||||
@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const writeMissing = args.includes('--write-missing')
|
||||
const target = path.resolve(args.find(arg => !arg.startsWith('--')) || process.cwd())
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const analysisRun = spawnSync(process.execPath, [path.join(scriptDir, 'analyze-project-environment.mjs'), target], { encoding: 'utf8' })
|
||||
if (analysisRun.status !== 0) {
|
||||
process.stderr.write(analysisRun.stderr)
|
||||
process.exit(analysisRun.status ?? 1)
|
||||
}
|
||||
const analysis = JSON.parse(analysisRun.stdout)
|
||||
const backend = analysis.backend
|
||||
|
||||
if (!backend) {
|
||||
console.error('无法可靠识别后端运行时,未生成 Docker 文件。请先根据分析报告补充真实后端入口。')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const projectName = path.basename(target).toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'application'
|
||||
const backendPath = backend.path === '.' ? '.' : `./${backend.path}`
|
||||
const db = analysis.services.find(item => item.id === 'db')
|
||||
const hasRedis = analysis.services.some(item => item.id === 'redis')
|
||||
const services = []
|
||||
const volumes = new Set()
|
||||
|
||||
if (db?.engine === 'mysql') {
|
||||
services.push(` db:
|
||||
image: mysql:8.4
|
||||
environment:
|
||||
MYSQL_DATABASE: \${DB_DATABASE:-app}
|
||||
MYSQL_USER: \${DB_USERNAME:-app}
|
||||
MYSQL_PASSWORD: \${DB_PASSWORD:-app-local-only}
|
||||
MYSQL_ROOT_PASSWORD: \${DB_ROOT_PASSWORD:-root-local-only}
|
||||
ports:
|
||||
- "\${DB_PORT_FORWARD:-3306}:3306"
|
||||
volumes:
|
||||
- database-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20`)
|
||||
volumes.add('database-data')
|
||||
}
|
||||
if (db?.engine === 'postgres') {
|
||||
services.push(` db:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: \${DB_DATABASE:-app}
|
||||
POSTGRES_USER: \${DB_USERNAME:-app}
|
||||
POSTGRES_PASSWORD: \${DB_PASSWORD:-app-local-only}
|
||||
ports:
|
||||
- "\${DB_PORT_FORWARD:-5432}:5432"
|
||||
volumes:
|
||||
- database-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20`)
|
||||
volumes.add('database-data')
|
||||
}
|
||||
if (hasRedis) {
|
||||
services.push(` redis:
|
||||
image: redis:7.4-alpine
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- "\${REDIS_PORT_FORWARD:-6379}:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20`)
|
||||
volumes.add('redis-data')
|
||||
}
|
||||
|
||||
const dependencies = [db && 'db', hasRedis && 'redis'].filter(Boolean)
|
||||
const dependsBlock = dependencies.length
|
||||
? `\n depends_on:\n${dependencies.map(id => ` ${id}:\n condition: service_healthy`).join('\n')}`
|
||||
: ''
|
||||
let dockerfile = ''
|
||||
let appService = ''
|
||||
let proxyService = ''
|
||||
|
||||
if (backend.runtime === 'php') {
|
||||
const phpVersion = backend.version || '8.2'
|
||||
dockerfile = `ARG PHP_VERSION=${phpVersion}\nFROM php:\${PHP_VERSION}-fpm-bookworm\n\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends git unzip libzip-dev libpq-dev \\\n && docker-php-ext-install pdo_mysql pdo_pgsql zip bcmath pcntl \\\n && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer\nWORKDIR /workspace/backend\nCMD ["php-fpm"]\n`
|
||||
const phpExtensions = new Set((backend.extensions || []).filter(name => name !== 'json'))
|
||||
if (db?.engine === 'mysql') phpExtensions.add('pdo_mysql')
|
||||
if (db?.engine === 'postgres') phpExtensions.add('pdo_pgsql')
|
||||
const buildableExtensions = [...phpExtensions].filter(name => ['bcmath', 'curl', 'gd', 'intl', 'mbstring', 'opcache', 'pcntl', 'pdo_mysql', 'pdo_pgsql', 'simplexml', 'soap', 'sockets', 'zip'].includes(name))
|
||||
const aptPackages = new Set(['git', 'unzip'])
|
||||
if (phpExtensions.has('curl')) aptPackages.add('libcurl4-openssl-dev')
|
||||
if (phpExtensions.has('gd')) ['libfreetype6-dev', 'libjpeg62-turbo-dev', 'libpng-dev'].forEach(item => aptPackages.add(item))
|
||||
if (phpExtensions.has('intl')) aptPackages.add('libicu-dev')
|
||||
if (phpExtensions.has('mbstring')) aptPackages.add('libonig-dev')
|
||||
if (phpExtensions.has('pdo_pgsql')) aptPackages.add('libpq-dev')
|
||||
if (phpExtensions.has('simplexml') || phpExtensions.has('soap')) aptPackages.add('libxml2-dev')
|
||||
if (phpExtensions.has('zip')) aptPackages.add('libzip-dev')
|
||||
const installSteps = [` && apt-get install -y --no-install-recommends ${[...aptPackages].join(' ')}`]
|
||||
if (phpExtensions.has('gd')) installSteps.push(' && docker-php-ext-configure gd --with-freetype --with-jpeg')
|
||||
if (buildableExtensions.length) installSteps.push(` && docker-php-ext-install ${buildableExtensions.join(' ')}`)
|
||||
if (phpExtensions.has('redis')) installSteps.push(' && pecl install redis', ' && docker-php-ext-enable redis')
|
||||
installSteps.push(' && rm -rf /var/lib/apt/lists/*')
|
||||
dockerfile = [
|
||||
`ARG PHP_VERSION=${phpVersion}`,
|
||||
'FROM php:${PHP_VERSION}-fpm-bookworm',
|
||||
'',
|
||||
'RUN apt-get update \\',
|
||||
installSteps.join(' \\\n'),
|
||||
'',
|
||||
'COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer',
|
||||
'WORKDIR /workspace/backend',
|
||||
'CMD ["php-fpm"]',
|
||||
'',
|
||||
].join('\n')
|
||||
appService = ` backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/backend/Dockerfile.dev
|
||||
args:
|
||||
PHP_VERSION: \${PHP_VERSION:-${phpVersion}}
|
||||
working_dir: /workspace/backend
|
||||
volumes:
|
||||
- ${backendPath}:/workspace/backend
|
||||
- backend-dependencies:/workspace/backend/vendor
|
||||
${db || hasRedis ? ` environment:\n${db ? ' DB_HOST: db\n' : ''}${hasRedis ? ' REDIS_HOST: redis\n' : ''}` : ''}${dependsBlock}`
|
||||
proxyService = ` proxy:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "\${APP_PORT:-8080}:80"
|
||||
volumes:
|
||||
- ${backendPath}:/workspace/backend:ro
|
||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 12`
|
||||
volumes.add('backend-dependencies')
|
||||
}
|
||||
else if (backend.runtime === 'node' && backend.commands.dev) {
|
||||
const nodeVersion = backend.version || '22'
|
||||
const dependencyPath = '/workspace/backend/node_modules'
|
||||
dockerfile = `ARG NODE_VERSION=${nodeVersion}\nFROM node:\${NODE_VERSION}-alpine\nRUN corepack enable\nWORKDIR /workspace/backend\nCMD ["sh"]\n`
|
||||
appService = ` backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/backend/Dockerfile.dev
|
||||
args:
|
||||
NODE_VERSION: \${NODE_VERSION:-${nodeVersion}}
|
||||
working_dir: /workspace/backend
|
||||
command: ["sh", "-lc", "${backend.commands.install} && ${backend.commands.dev}"]
|
||||
ports:
|
||||
- "\${BACKEND_PORT:-3000}:3000"
|
||||
volumes:
|
||||
- ${backendPath}:/workspace/backend
|
||||
- backend-dependencies:${dependencyPath}
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
${db ? ' DB_HOST: db\n' : ''}${hasRedis ? ' REDIS_HOST: redis\n' : ''}${dependsBlock}`
|
||||
volumes.add('backend-dependencies')
|
||||
}
|
||||
else {
|
||||
console.error(`已识别 ${backend.runtime},但缺少可验证的通用开发启动模板或启动命令;未生成可能误导的 Compose。`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
services.push(appService)
|
||||
if (proxyService) services.push(proxyService)
|
||||
const frontendFiles = new Map()
|
||||
analysis.frontends.filter(app => app.commands.dev).forEach((app, index) => {
|
||||
const appName = app.path.split('/').pop().toLowerCase().replace(/[^a-z0-9_-]+/g, '-')
|
||||
const serviceName = `frontend-${appName}`
|
||||
const port = app.port || 5173 + index
|
||||
const portVariable = `FRONTEND_${appName.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_PORT`
|
||||
const nodeVersion = app.version || '22'
|
||||
const hostSuffix = ['vue', 'react', 'uniapp'].includes(app.framework) ? ` -- --host 0.0.0.0 --port ${port}` : ''
|
||||
services.push(` ${serviceName}:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/frontend/${appName}/Dockerfile.dev
|
||||
args:
|
||||
NODE_VERSION: \${NODE_VERSION:-${nodeVersion}}
|
||||
working_dir: /workspace/frontend/${appName}
|
||||
command: ["sh", "-lc", "${app.commands.install} && ${app.commands.dev}${hostSuffix}"]
|
||||
ports:
|
||||
- "\${${portVariable}:-${port}}:${port}"
|
||||
volumes:
|
||||
- ./${app.path}:/workspace/frontend/${appName}
|
||||
- ${serviceName}-dependencies:/workspace/frontend/${appName}/node_modules
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: ${port}`)
|
||||
volumes.add(`${serviceName}-dependencies`)
|
||||
frontendFiles.set(`docker/frontend/${appName}/Dockerfile.dev`, `ARG NODE_VERSION=${nodeVersion}\nFROM node:\${NODE_VERSION}-alpine\nRUN corepack enable\nWORKDIR /workspace/frontend/${appName}\nCMD ["sh"]\n`)
|
||||
})
|
||||
const compose = `name: \${COMPOSE_PROJECT_NAME:-${projectName}}\n\nservices:\n${services.join('\n\n')}\n\nvolumes:\n${[...volumes].map(name => ` ${name}:`).join('\n')}\n`
|
||||
const nginx = `server {\n listen 80;\n server_name _;\n root /workspace/backend/public;\n index index.php index.html;\n\n location / { try_files $uri $uri/ /index.php?$query_string; }\n location ~ \\.php$ {\n include fastcgi_params;\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n fastcgi_pass backend:9000;\n }\n}\n`
|
||||
const frontendEnv = analysis.frontends.filter(app => app.commands.dev).map((app, index) => `FRONTEND_${app.path.split('/').pop().toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_PORT=${app.port || 5173 + index}`).join('\n')
|
||||
const envExample = `COMPOSE_PROJECT_NAME=${projectName}\nAPP_PORT=8080\nBACKEND_PORT=3000\n${frontendEnv}${frontendEnv ? '\n' : ''}DB_DATABASE=app\nDB_USERNAME=app\nDB_PASSWORD=app-local-only\nDB_ROOT_PASSWORD=root-local-only\nDB_PORT_FORWARD=${db?.defaultPort || 3306}\nREDIS_PORT_FORWARD=6379\n`
|
||||
const dockerReadme = `# Docker 本地开发配置\n\n- 统一入口:仓库根目录 \`docker-compose.yml\`、\`run.sh\`。\n- 本目录只保存镜像、代理、入口和健康检查配置,不提交运行数据或日志。\n- 数据使用 Compose 具名卷;日常 \`stop/down\` 不删除数据。\n- 完整命令、端口、初始化、备份恢复和排障见 \`help/docs/docker-development.md\`。\n`
|
||||
const dockerDoc = `# Docker 本地开发\n\n> 由项目环境分析生成基线;启用前必须核对运行时扩展、应用环境变量、迁移和健康接口。\n\n## 已识别环境\n\n- 后端:${backend.runtime} / ${backend.framework},路径 \`${backend.path}\`,版本证据 \`${backend.versionSource}\`。\n- 基础服务:${analysis.services.map(item => item.engine).join('、') || '未识别'}。\n- Compose 入口:\`docker-compose.yml\`。\n\n## 首次启动\n\n1. 将 \`.env.docker.example\` 中需要的非敏感配置合并到本地 \`.env\`,不要覆盖已有配置。\n2. 运行 \`./run.sh doctor\`,成功信号是 Docker、Compose 与配置解析均通过。\n3. 运行 \`./run.sh setup\` 构建镜像。\n4. 根据项目真实机制执行数据库迁移或最小数据初始化。\n5. 运行 \`./run.sh dev\`,再运行 \`./run.sh status\`。\n6. 访问 \`http://localhost:\${APP_PORT:-8080}\` 或项目健康接口验证。\n\n## 日常命令\n\n- \`./run.sh dev\`:构建并启动;保留数据。\n- \`./run.sh restart [service]\`:重启全部或指定服务。\n- \`./run.sh logs [service]\`:跟随日志。\n- \`./run.sh shell <service>\`:进入容器。\n- \`./run.sh stop\`:停止但保留容器和数据。\n- \`./run.sh down\`:删除容器和网络,保留具名卷。\n- \`./run.sh reset-data --yes\`:删除本地具名卷,不可恢复。\n\n## 必须补充并验证\n\n- 应用所需系统扩展、环境变量、内部/外部端口和健康接口;\n- 数据库迁移、种子、开发账号、备份与恢复命令;\n- 每个前端、worker、scheduler、WebSocket 和队列进程;\n- 源码热更新、测试、构建、停止再启动后的数据持久化;\n- macOS、Windows、Linux 的权限与文件监听差异。\n`
|
||||
|
||||
const templateRoot = path.resolve(scriptDir, '../assets/project-template')
|
||||
const files = new Map([
|
||||
['docker-compose.yml', compose],
|
||||
['.env.docker.example', envExample],
|
||||
['docker/backend/Dockerfile.dev', dockerfile],
|
||||
['docker/README.md', dockerReadme],
|
||||
['help/docs/docker-development.md', dockerDoc],
|
||||
['run.sh', fs.readFileSync(path.join(templateRoot, 'run.sh'), 'utf8')],
|
||||
['scripts/project-command.mjs', fs.readFileSync(path.join(templateRoot, 'scripts/project-command.mjs'), 'utf8')],
|
||||
['scripts/docker-project.mjs', fs.readFileSync(path.join(templateRoot, 'scripts/docker-project.mjs'), 'utf8')],
|
||||
])
|
||||
if (backend.runtime === 'php') files.set('docker/nginx/default.conf', nginx)
|
||||
for (const [relative, content] of frontendFiles) files.set(relative, content)
|
||||
|
||||
const created = []
|
||||
const retained = []
|
||||
for (const [relative, content] of files) {
|
||||
const destination = path.join(target, relative)
|
||||
if (fs.existsSync(destination)) {
|
||||
retained.push(relative)
|
||||
continue
|
||||
}
|
||||
created.push(relative)
|
||||
if (writeMissing) {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.writeFileSync(destination, content)
|
||||
if (relative === 'run.sh') fs.chmodSync(destination, 0o755)
|
||||
}
|
||||
}
|
||||
|
||||
let contractUpdated = false
|
||||
const contractPath = path.join(target, '.crmeb/project.json')
|
||||
if (writeMissing && fs.existsSync(contractPath)) {
|
||||
try {
|
||||
const contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'))
|
||||
contract.docker = { ...(contract.docker || {}), enabled: true, composeFile: 'docker-compose.yml', configRoot: 'docker' }
|
||||
contract.applications ||= []
|
||||
for (const app of [backend, ...analysis.frontends]) {
|
||||
const existing = contract.applications.find(item => item.id === app.id)
|
||||
if (!existing) contract.applications.push(app)
|
||||
else {
|
||||
for (const [key, value] of Object.entries(app)) {
|
||||
if (existing[key] === undefined || existing[key] === '' || existing[key] === 'unknown') existing[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
contract.services ||= []
|
||||
for (const service of analysis.services) {
|
||||
if (!contract.services.some(item => item.id === service.id)) contract.services.push(service)
|
||||
}
|
||||
contract.documentation ||= {}
|
||||
contract.documentation.required = [...new Set([...(contract.documentation.required || []), ...analysis.documentation.required])]
|
||||
contract.commands ||= {}
|
||||
const dockerCommands = {
|
||||
doctor: 'node scripts/docker-project.mjs doctor', setup: 'node scripts/docker-project.mjs setup',
|
||||
dev: 'node scripts/docker-project.mjs dev', restart: 'node scripts/docker-project.mjs restart',
|
||||
stop: 'node scripts/docker-project.mjs stop', down: 'node scripts/docker-project.mjs down',
|
||||
status: 'node scripts/docker-project.mjs status', logs: 'node scripts/docker-project.mjs logs',
|
||||
shell: 'node scripts/docker-project.mjs shell', resetData: 'node scripts/docker-project.mjs reset-data',
|
||||
}
|
||||
for (const [name, command] of Object.entries(dockerCommands)) {
|
||||
if (!contract.commands[name]) contract.commands[name] = command
|
||||
}
|
||||
fs.writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`)
|
||||
contractUpdated = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`无法安全合并 .crmeb/project.json:${error.message}`)
|
||||
process.exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(writeMissing ? 'Docker 本地环境生成结果:' : 'Docker 本地环境生成预览:')
|
||||
for (const relative of created) console.log(`- ${writeMissing ? '创建' : '将创建'} ${relative}`)
|
||||
for (const relative of retained) console.log(`- 保留已有 ${relative}(必须语义审计,生成器不会覆盖)`)
|
||||
if (contractUpdated) console.log('- 增量合并 .crmeb/project.json 的 Docker、应用、服务、命令和文档清单')
|
||||
console.log('\n下一步:语义合并已有文件,补全 project.json 命令与文档清单,再依次执行 doctor、compose config、build、dev、smoke 和持久化验证。')
|
||||
@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defaultProcessDirectory, detectApplications, pathExists, requiredCommands, requiredDockerCommands, walkFiles } from './lib.mjs'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const dryRun = args.includes('--dry-run')
|
||||
const targetArg = args.find(arg => !arg.startsWith('--')) || process.cwd()
|
||||
const target = path.resolve(targetArg)
|
||||
|
||||
if (!pathExists(target) || !fs.statSync(target).isDirectory()) {
|
||||
console.error(`目标仓库不存在或不是目录:${target}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const templateRoot = path.resolve(scriptDir, '../assets/project-template')
|
||||
const skillSkeletonRoot = path.resolve(scriptDir, '../assets/app-skill-skeleton')
|
||||
const applications = detectApplications(target)
|
||||
const actions = []
|
||||
|
||||
function ensureDirectory(relative) {
|
||||
const destination = path.join(target, relative)
|
||||
if (pathExists(destination)) return
|
||||
actions.push(`创建目录 ${relative}/`)
|
||||
if (!dryRun) fs.mkdirSync(destination, { recursive: true })
|
||||
}
|
||||
|
||||
for (const relative of [
|
||||
'.crmeb/rules', '.crmeb/skills', '.crmeb/agents', '.crmeb/scopes',
|
||||
'backend', 'frontend', 'help/docs', 'docker', 'tests',
|
||||
`${defaultProcessDirectory}/plans`, `${defaultProcessDirectory}/reports`,
|
||||
'upgrades/versions', 'upgrades/packages',
|
||||
]) ensureDirectory(relative)
|
||||
|
||||
for (const source of walkFiles(templateRoot, { ignored: [] })) {
|
||||
const relative = path.relative(templateRoot, source)
|
||||
const destination = path.join(target, relative)
|
||||
if (pathExists(destination)) {
|
||||
actions.push(`保留已有 ${relative}`)
|
||||
continue
|
||||
}
|
||||
actions.push(`创建文件 ${relative}`)
|
||||
if (!dryRun) {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.copyFileSync(source, destination)
|
||||
if (relative === 'run.sh') fs.chmodSync(destination, 0o755)
|
||||
}
|
||||
}
|
||||
|
||||
/** 把技能骨架占位符替换为目标应用的真实信息 */
|
||||
function renderSkillFile(content, app) {
|
||||
return content
|
||||
.replaceAll('{{SKILL_NAME}}', `${app.id}-dev`)
|
||||
.replaceAll('{{APP_ID}}', app.id)
|
||||
.replaceAll('{{APP_PATH}}', app.path)
|
||||
.replaceAll('{{RUNTIME}}', app.runtime || 'unknown')
|
||||
}
|
||||
|
||||
for (const app of applications) {
|
||||
const skillRelative = `.crmeb/skills/${app.id}-dev`
|
||||
ensureDirectory(skillRelative)
|
||||
for (const source of walkFiles(skillSkeletonRoot, { ignored: [] })) {
|
||||
const relative = path.relative(skillSkeletonRoot, source)
|
||||
const destination = path.join(target, skillRelative, relative)
|
||||
if (pathExists(destination)) {
|
||||
actions.push(`保留已有 ${skillRelative}/${relative}`)
|
||||
continue
|
||||
}
|
||||
actions.push(`创建文件 ${skillRelative}/${relative}`)
|
||||
if (!dryRun) {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.writeFileSync(destination, renderSkillFile(fs.readFileSync(source, 'utf8'), app))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const agentsPath = path.join(target, '.crmeb/AGENTS.md')
|
||||
if (applications.length && pathExists(agentsPath)) {
|
||||
const content = fs.readFileSync(agentsPath, 'utf8')
|
||||
const missing = applications
|
||||
.map(app => ({
|
||||
link: `skills/${app.id}-dev/SKILL.md`,
|
||||
row: `| [${app.id}-dev](skills/${app.id}-dev/SKILL.md) | 修改 \`${app.path}\` 前,先读该端的项目目录说明与核心功能开发 |`,
|
||||
}))
|
||||
.filter(item => !content.includes(item.link))
|
||||
|
||||
if (missing.length) {
|
||||
actions.push(`更新文件 .crmeb/AGENTS.md(登记 ${missing.length} 个技能)`)
|
||||
if (!dryRun) {
|
||||
const lines = content.split(/\r?\n/)
|
||||
const heading = lines.findIndex(line => line.trim() === '## 技能索引')
|
||||
let insertAt
|
||||
if (heading < 0) {
|
||||
lines.push('', '## 技能索引', '', '| 技能 | 何时读取 |', '| --- | --- |')
|
||||
insertAt = lines.length
|
||||
} else {
|
||||
const separator = lines.findIndex((line, index) => index > heading && /^\|\s*-{2,}/.test(line))
|
||||
insertAt = separator >= 0 ? separator + 1 : heading + 1
|
||||
}
|
||||
lines.splice(insertAt, 0, ...missing.map(item => item.row))
|
||||
fs.writeFileSync(agentsPath, `${lines.join('\n').replace(/\n+$/, '')}\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contractPath = path.join(target, '.crmeb/project.json')
|
||||
if (!pathExists(contractPath)) {
|
||||
const commands = Object.fromEntries([...requiredCommands, ...requiredDockerCommands].map(command => [command, '']))
|
||||
const contract = {
|
||||
schemaVersion: 1,
|
||||
status: 'draft',
|
||||
project: { name: path.basename(target), type: 'fullstack', versionSource: '' },
|
||||
directories: {
|
||||
backend: 'backend', frontend: 'frontend', docs: 'help/docs',
|
||||
process: defaultProcessDirectory, tests: 'tests', upgrades: 'upgrades',
|
||||
},
|
||||
applications,
|
||||
services: [],
|
||||
docker: { enabled: false, composeFile: 'docker-compose.yml', configRoot: 'docker' },
|
||||
documentation: {
|
||||
required: [
|
||||
'README.md', 'help/docs/README.md', 'help/docs/development.md',
|
||||
'help/docs/architecture.md', 'help/docs/testing.md',
|
||||
],
|
||||
},
|
||||
commands,
|
||||
}
|
||||
actions.push('创建文件 .crmeb/project.json(draft,需按真实项目补全)')
|
||||
if (!dryRun) fs.writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`)
|
||||
}
|
||||
else actions.push('保留已有 .crmeb/project.json')
|
||||
|
||||
console.log(dryRun ? '项目规范初始化预览:' : '项目规范初始化结果:')
|
||||
for (const action of actions) console.log(`- ${action}`)
|
||||
console.log('\n不会覆盖现有文件。补全 project.json 后运行 check-project-standard.mjs。')
|
||||
356
.crmeb/skills/project-standard-bootstrap/scripts/lib.mjs
Normal file
356
.crmeb/skills/project-standard-bootstrap/scripts/lib.mjs
Normal file
@ -0,0 +1,356 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export const requiredDirectories = ['.crmeb', 'backend', 'frontend', 'help/docs', 'tests']
|
||||
export const defaultProcessDirectory = 'tests'
|
||||
export const requiredCommands = [
|
||||
'doctor', 'setup', 'dev', 'stop', 'status', 'logs', 'check', 'test', 'build',
|
||||
'upgradePlan', 'upgradePackage', 'upgradeVerify',
|
||||
]
|
||||
export const requiredDockerCommands = ['restart', 'down', 'shell', 'smoke', 'resetData']
|
||||
export const requiredAiEntrypoints = [
|
||||
'AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.github/copilot-instructions.md',
|
||||
'.cursor/rules/project-standard.mdc', 'llms.txt',
|
||||
]
|
||||
|
||||
export function pathExists(target) {
|
||||
return fs.existsSync(target)
|
||||
}
|
||||
|
||||
export function readJson(file) {
|
||||
try {
|
||||
return { value: JSON.parse(fs.readFileSync(file, 'utf8')), error: null }
|
||||
}
|
||||
catch (error) {
|
||||
return { value: null, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/** 过程文档目录名以契约为准,缺省回退到 defaultProcessDirectory,不硬编码 */
|
||||
export function resolveProcessDirectory(contract) {
|
||||
const value = contract?.directories?.process
|
||||
if (typeof value !== 'string' || !value.trim()) return defaultProcessDirectory
|
||||
return value.trim().replace(/^\.\//, '').replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
/** 解析 Markdown frontmatter 的简单键值;支持 `key: value` 与 `key:` + `- item` 列表 */
|
||||
export function readFrontmatter(file) {
|
||||
if (!pathExists(file)) return null
|
||||
const match = fs.readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
||||
if (!match) return null
|
||||
const data = {}
|
||||
let currentKey = ''
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const item = line.match(/^\s*-\s+(.+?)\s*$/)
|
||||
if (item && currentKey) {
|
||||
if (!Array.isArray(data[currentKey])) data[currentKey] = data[currentKey] ? [data[currentKey]] : []
|
||||
data[currentKey].push(item[1].replace(/^['"]|['"]$/g, ''))
|
||||
continue
|
||||
}
|
||||
const pair = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/)
|
||||
if (!pair) continue
|
||||
currentKey = pair[1]
|
||||
data[currentKey] = pair[2].trim().replace(/^['"]|['"]$/g, '') || []
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export function walkFiles(root, options = {}) {
|
||||
const ignored = new Set(options.ignored || ['.git', 'node_modules', 'vendor', 'dist', 'build', 'runtime'])
|
||||
const result = []
|
||||
if (!pathExists(root)) return result
|
||||
|
||||
const visit = (current) => {
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
if (ignored.has(entry.name)) continue
|
||||
const absolute = path.join(current, entry.name)
|
||||
if (entry.isDirectory()) visit(absolute)
|
||||
else if (entry.isFile()) result.push(absolute)
|
||||
}
|
||||
}
|
||||
visit(root)
|
||||
return result
|
||||
}
|
||||
|
||||
export function detectApplications(root) {
|
||||
const applications = []
|
||||
const backend = path.join(root, 'backend')
|
||||
if (pathExists(backend)) {
|
||||
let runtime = 'unknown'
|
||||
let versionSource = ''
|
||||
const candidates = [
|
||||
['composer.json', 'php'], ['package.json', 'node'], ['pyproject.toml', 'python'],
|
||||
['pom.xml', 'java'], ['build.gradle', 'java'], ['build.gradle.kts', 'java'], ['go.mod', 'go'],
|
||||
]
|
||||
for (const [file, name] of candidates) {
|
||||
if (pathExists(path.join(backend, file))) {
|
||||
runtime = name
|
||||
versionSource = `backend/${file}`
|
||||
break
|
||||
}
|
||||
}
|
||||
applications.push({ id: 'backend', kind: 'backend', path: 'backend', runtime, versionSource })
|
||||
}
|
||||
|
||||
const frontend = path.join(root, 'frontend')
|
||||
if (pathExists(frontend)) {
|
||||
for (const entry of fs.readdirSync(frontend, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
||||
const relative = `frontend/${entry.name}`
|
||||
const appRoot = path.join(root, relative)
|
||||
if (pathExists(path.join(appRoot, 'package.json')) || pathExists(path.join(appRoot, 'pubspec.yaml'))) {
|
||||
applications.push({
|
||||
id: `frontend-${entry.name}`,
|
||||
kind: 'frontend',
|
||||
path: relative,
|
||||
runtime: pathExists(path.join(appRoot, 'package.json')) ? 'node' : 'flutter',
|
||||
versionSource: pathExists(path.join(appRoot, 'package.json')) ? `${relative}/package.json` : `${relative}/pubspec.yaml`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return applications
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 .crmeb/skills/ 技能包骨架与应用覆盖:
|
||||
* 每个技能为 SKILL.md + references/;声明 appPath 的技能必须提供 directoryGuide 与 coreFeatures;
|
||||
* project.json 的每个应用都要有对应技能。
|
||||
*/
|
||||
export function collectSkillFindings(root, contract) {
|
||||
const findings = []
|
||||
const add = (severity, code, message) => findings.push({ severity, code, message })
|
||||
const skillsRoot = path.join(root, '.crmeb/skills')
|
||||
|
||||
if (!pathExists(skillsRoot)) {
|
||||
add('error', 'missing-skills', '缺少 .crmeb/skills/:架构认知与核心功能开发文档按技能组织,目录不可缺省')
|
||||
return findings
|
||||
}
|
||||
|
||||
const agentsFile = path.join(root, '.crmeb/AGENTS.md')
|
||||
const agentIndex = pathExists(agentsFile) ? fs.readFileSync(agentsFile, 'utf8') : ''
|
||||
const declaredPaths = new Set()
|
||||
let skillCount = 0
|
||||
|
||||
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
||||
skillCount += 1
|
||||
const skillDir = path.join(skillsRoot, entry.name)
|
||||
const skillFile = path.join(skillDir, 'SKILL.md')
|
||||
|
||||
if (!pathExists(skillFile)) {
|
||||
add('error', 'missing-skill-file', `.crmeb/skills/${entry.name}/ 缺少 SKILL.md`)
|
||||
continue
|
||||
}
|
||||
if (!pathExists(path.join(skillDir, 'references'))) {
|
||||
add('error', 'missing-skill-references', `.crmeb/skills/${entry.name}/ 缺少 references/:技能骨架为 SKILL.md + references/`)
|
||||
}
|
||||
if (!agentIndex.includes(`skills/${entry.name}/SKILL.md`)) {
|
||||
add('warning', 'unregistered-skill', `.crmeb/skills/${entry.name}/ 未登记到 .crmeb/AGENTS.md 的技能索引`)
|
||||
}
|
||||
|
||||
const frontmatter = readFrontmatter(skillFile)
|
||||
if (!frontmatter) {
|
||||
add('error', 'missing-skill-frontmatter', `.crmeb/skills/${entry.name}/SKILL.md 缺少 frontmatter`)
|
||||
continue
|
||||
}
|
||||
if (frontmatter.name !== entry.name) {
|
||||
add('error', 'skill-name-mismatch', `.crmeb/skills/${entry.name}/SKILL.md 的 name 必须与目录名一致,当前为 ${frontmatter.name || '(空)'}`)
|
||||
}
|
||||
if (!frontmatter.description) {
|
||||
add('error', 'skill-description', `.crmeb/skills/${entry.name}/SKILL.md 缺少非空 description`)
|
||||
}
|
||||
|
||||
const skillDocs = walkFiles(skillDir).filter(file => file.endsWith('.md'))
|
||||
if (skillDocs.some(file => /\{\{|TODO/.test(fs.readFileSync(file, 'utf8')))) {
|
||||
add('warning', 'skill-placeholder', `.crmeb/skills/${entry.name}/ 仍含 TODO 或占位符:目录说明与核心功能文档尚未按当前代码补全`)
|
||||
}
|
||||
|
||||
const appPath = typeof frontmatter.appPath === 'string' ? frontmatter.appPath.replace(/\/+$/, '') : ''
|
||||
if (!appPath) continue
|
||||
declaredPaths.add(appPath)
|
||||
|
||||
const coreFeatures = Array.isArray(frontmatter.coreFeatures) ? frontmatter.coreFeatures : []
|
||||
if (!frontmatter.directoryGuide) {
|
||||
add('error', 'missing-directory-guide', `.crmeb/skills/${entry.name}/SKILL.md 缺少 directoryGuide:每个目录必须有项目目录说明`)
|
||||
}
|
||||
if (!coreFeatures.length) {
|
||||
add('error', 'missing-core-features', `.crmeb/skills/${entry.name}/SKILL.md 缺少 coreFeatures:每个目录必须有核心功能开发文档`)
|
||||
}
|
||||
for (const relative of [frontmatter.directoryGuide, ...coreFeatures].filter(Boolean)) {
|
||||
if (!pathExists(path.resolve(skillDir, relative))) {
|
||||
add('error', 'missing-skill-doc', `.crmeb/skills/${entry.name}/ 引用的文档不存在:${relative}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skillCount) {
|
||||
add('error', 'empty-skills', '.crmeb/skills/ 下没有技能:后端与每个独立前端应用都需要一个架构认知技能')
|
||||
}
|
||||
|
||||
const applicationPaths = new Set(
|
||||
(contract?.applications || []).map(app => String(app.path || '').replace(/\/+$/, '')).filter(Boolean),
|
||||
)
|
||||
for (const app of contract?.applications || []) {
|
||||
const appPath = String(app.path || '').replace(/\/+$/, '')
|
||||
if (!appPath || declaredPaths.has(appPath)) continue
|
||||
add('error', 'missing-app-skill', `应用 ${app.id || appPath}(${appPath})没有架构认知技能:需要 .crmeb/skills/<技能>/SKILL.md 声明 appPath: ${appPath}`)
|
||||
}
|
||||
for (const appPath of declaredPaths) {
|
||||
if (!applicationPaths.has(appPath)) {
|
||||
add('warning', 'stale-app-skill', `技能声明的 appPath 不在 project.json 的 applications 中:${appPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
export function collectFindings(root) {
|
||||
const findings = []
|
||||
const add = (severity, code, message) => findings.push({ severity, code, message })
|
||||
|
||||
const contractPath = path.join(root, '.crmeb/project.json')
|
||||
const contractResult = pathExists(contractPath) ? readJson(contractPath) : { value: null, error: null }
|
||||
const processDirectory = resolveProcessDirectory(contractResult.value)
|
||||
|
||||
for (const relative of new Set([...requiredDirectories, processDirectory])) {
|
||||
if (!pathExists(path.join(root, relative))) add('error', 'missing-directory', `缺少统一目录:${relative}/`)
|
||||
}
|
||||
|
||||
const rootAgents = path.join(root, 'AGENTS.md')
|
||||
if (!pathExists(rootAgents)) add('error', 'missing-entry', '缺少根 AGENTS.md')
|
||||
else if (!fs.readFileSync(rootAgents, 'utf8').includes('.crmeb/AGENTS.md')) {
|
||||
add('error', 'invalid-entry', '根 AGENTS.md 未指向 .crmeb/AGENTS.md')
|
||||
}
|
||||
|
||||
if (!pathExists(path.join(root, '.crmeb/AGENTS.md'))) add('error', 'missing-ai-source', '缺少 .crmeb/AGENTS.md')
|
||||
|
||||
for (const relative of requiredAiEntrypoints) {
|
||||
const entry = path.join(root, relative)
|
||||
if (!pathExists(entry)) {
|
||||
add('error', 'missing-ai-entry', `缺少 AI 工具入口:${relative}`)
|
||||
continue
|
||||
}
|
||||
if (!fs.readFileSync(entry, 'utf8').includes('.crmeb/AGENTS.md')) {
|
||||
add('error', 'invalid-ai-entry', `AI 工具入口未指向 .crmeb/AGENTS.md:${relative}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathExists(contractPath)) {
|
||||
add('error', 'missing-contract', '缺少 .crmeb/project.json')
|
||||
return findings
|
||||
}
|
||||
|
||||
const { value: contract, error } = contractResult
|
||||
if (error) {
|
||||
add('error', 'invalid-contract', `.crmeb/project.json 无法解析:${error}`)
|
||||
return findings
|
||||
}
|
||||
|
||||
if (contract.schemaVersion !== 1) add('error', 'contract-schema', 'project.json schemaVersion 必须为 1')
|
||||
if (contract.status !== 'active') add('warning', 'contract-draft', 'project.json 尚未标记为 active')
|
||||
if (contract.status === 'active' && !contract.docker?.enabled) {
|
||||
add('error', 'docker-required', 'active 项目必须根据当前后端环境提供根 docker-compose.yml,并在 project.json 启用 docker')
|
||||
}
|
||||
if (!contract.project?.name) add('error', 'project-name', 'project.json 缺少 project.name')
|
||||
|
||||
const commandsToCheck = contract.docker?.enabled
|
||||
? [...requiredCommands, ...requiredDockerCommands]
|
||||
: requiredCommands
|
||||
for (const command of commandsToCheck) {
|
||||
const value = contract.commands?.[command]
|
||||
if (!value) {
|
||||
add('error', 'missing-command', `project.json 缺少标准命令:${command}`)
|
||||
continue
|
||||
}
|
||||
const parts = value.trim().split(/\s+/).map(part => part.replace(/^['"]|['"]$/g, ''))
|
||||
const executable = parts[0]
|
||||
if ((executable.startsWith('./') || executable.startsWith('../')) && !pathExists(path.resolve(root, executable))) {
|
||||
add('error', 'missing-command-entry', `标准命令 ${command} 的本地入口不存在:${executable}`)
|
||||
}
|
||||
if (['node', 'php', 'python', 'python3', 'bash', 'sh'].includes(executable)) {
|
||||
const script = parts[1]
|
||||
if (script && (script.startsWith('./') || script.startsWith('../') || script.includes('/')) && !pathExists(path.resolve(root, script))) {
|
||||
add('error', 'missing-command-script', `标准命令 ${command} 的脚本不存在:${script}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const app of contract.applications || []) {
|
||||
if (!app.id || !app.path) add('error', 'invalid-application', 'applications 条目必须包含 id 和 path')
|
||||
else if (!pathExists(path.join(root, app.path))) add('error', 'missing-application-path', `应用路径不存在:${app.path}`)
|
||||
}
|
||||
|
||||
findings.push(...collectSkillFindings(root, contract))
|
||||
|
||||
for (const relative of contract.documentation?.required || []) {
|
||||
if (!pathExists(path.join(root, relative))) {
|
||||
add('error', 'missing-documentation', `project.json 声明的必备文档不存在:${relative}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (contract.docker?.enabled) {
|
||||
for (const relative of ['docker-compose.yml', 'run.sh', 'docker/README.md', 'help/docs/docker-development.md']) {
|
||||
if (!pathExists(path.join(root, relative))) add('error', 'missing-docker-asset', `Docker 本地开发缺少:${relative}`)
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
export function collectMarkdownLinkFindings(root) {
|
||||
const findings = []
|
||||
const { value: contract } = readJson(path.join(root, '.crmeb/project.json'))
|
||||
const roots = [...new Set(['.crmeb', 'help/docs', 'tests', resolveProcessDirectory(contract)])]
|
||||
const files = [
|
||||
path.join(root, 'README.md'), path.join(root, 'AGENTS.md'), path.join(root, 'CLAUDE.md'),
|
||||
path.join(root, 'GEMINI.md'), path.join(root, '.github/copilot-instructions.md'),
|
||||
]
|
||||
for (const relative of roots) {
|
||||
for (const file of walkFiles(path.join(root, relative))) {
|
||||
if (file.endsWith('.md')) files.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
const linkPattern = /\[[^\]]*\]\(([^)]+)\)/g
|
||||
for (const file of [...new Set(files)].filter(pathExists)) {
|
||||
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/)
|
||||
let fenced = false
|
||||
lines.forEach((line, index) => {
|
||||
if (/^\s*```/.test(line)) {
|
||||
fenced = !fenced
|
||||
return
|
||||
}
|
||||
if (fenced) return
|
||||
for (const match of line.matchAll(linkPattern)) {
|
||||
const raw = match[1].trim().replace(/^<|>$/g, '')
|
||||
if (!raw || /^(https?:|mailto:|#|codex:)/i.test(raw)) continue
|
||||
const clean = decodeURIComponent(raw.split('#')[0].split('?')[0])
|
||||
if (!clean) continue
|
||||
const target = path.resolve(path.dirname(file), clean)
|
||||
if (!pathExists(target)) {
|
||||
findings.push({
|
||||
severity: 'error',
|
||||
code: 'broken-link',
|
||||
message: `${path.relative(root, file)}:${index + 1} 链接目标不存在:${raw}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
export function printFindings(findings) {
|
||||
if (!findings.length) {
|
||||
console.log('✅ 项目统一规范检查通过')
|
||||
return
|
||||
}
|
||||
for (const finding of findings) {
|
||||
const icon = finding.severity === 'error' ? '❌' : '⚠️'
|
||||
console.log(`${icon} [${finding.code}] ${finding.message}`)
|
||||
}
|
||||
const errors = findings.filter(item => item.severity === 'error').length
|
||||
const warnings = findings.length - errors
|
||||
console.log(`\n汇总:${errors} error / ${warnings} warning`)
|
||||
}
|
||||
11
README.md
11
README.md
@ -21,6 +21,17 @@ php think run --host 127.0.0.1 --port 8011
|
||||
|
||||
数据库和 Redis 连接以 `backend/.env` 为准,宿主机常见端口为 MySQL `3306`、Redis `6379`。首次启动后访问 **http://localhost:8011** 进入安装向导。
|
||||
|
||||
## 前端一键命令
|
||||
|
||||
```bash
|
||||
./run.sh admin:dev # 管理端开发测试启动(端口 1619)
|
||||
./run.sh admin:build # 管理端编译打包并发布到 backend/public/admin
|
||||
./run.sh uni:dev # 移动端 H5 开发测试启动
|
||||
./run.sh uni:build # 移动端 H5 编译打包并发布到 backend/public
|
||||
```
|
||||
|
||||
也可直接执行 `./run.sh` 打开交互式菜单(前端命令对应编号 11/12/21/22)。
|
||||
|
||||
## 可选 Docker Compose
|
||||
|
||||
只有明确选择容器环境时使用:
|
||||
|
||||
@ -264,6 +264,15 @@ php think workerman start --d # WebSocket 长连接网关
|
||||
./run.sh logs # 查看日志
|
||||
```
|
||||
|
||||
**前端一键命令(run.sh)**
|
||||
|
||||
```bash
|
||||
./run.sh admin:dev # 管理端开发测试启动(端口 1619)
|
||||
./run.sh admin:build # 管理端编译打包并发布到 backend/public/admin
|
||||
./run.sh uni:dev # 移动端 H5 开发测试启动
|
||||
./run.sh uni:build # 移动端 H5 编译打包并发布到 backend/public
|
||||
```
|
||||
|
||||
> 默认访问地址:`http://localhost:8011`,管理后台 `http://localhost:8011/admin`。
|
||||
> 数据库连接等配置以 `backend/.env` 为准;详细部署说明见 [部署指南](help/docs/部署指南.md)。
|
||||
|
||||
|
||||
@ -121,6 +121,8 @@ npm run build:all
|
||||
|
||||
`build:ext` 构建可由管理端动态加载的扩展包;修改插件页面后不能只构建主应用。
|
||||
|
||||
仓库根 `./run.sh admin:dev` / `./run.sh admin:build` 是上述 `dev` 与 `build:all` + 发布的一键封装;后者清空 `backend/public/admin` 后写入新产物。
|
||||
|
||||
### 3.3 用户端
|
||||
|
||||
```bash
|
||||
@ -143,6 +145,8 @@ npm run build:app
|
||||
|
||||
平台标识与权限位于 `src/manifest.json`,页面注册位于 `src/pages.json`。产物在 `dist/{dev,build}/{platform}`,小程序使用对应开发者工具打开目标目录,并检查条件编译后的 `Form-type`。
|
||||
|
||||
仓库根 `./run.sh uni:dev` / `./run.sh uni:build` 是 H5 `dev:h5` / `build:h5` 的一键封装;发布时清理 `backend/public/{assets,pages,static}` 后覆盖拷贝 `dist/build/h5`,再访问 `http://localhost:8011`。
|
||||
|
||||
不要把生产 AppSecret、证书或私钥写入前端工程;前端代码和小程序包对用户可见。
|
||||
|
||||
### 3.4 后台进程
|
||||
|
||||
@ -61,6 +61,13 @@
|
||||
|
||||
`run.sh delete` 会清理数据,只有确认环境可销毁时才能使用。
|
||||
|
||||
前端构建发布(产物写入程序 public 目录):
|
||||
|
||||
```bash
|
||||
./run.sh admin:build # 管理端 build:all(主包 + 扩展 UMD)→ backend/public/admin
|
||||
./run.sh uni:build # 用户端 H5 构建 → backend/public(index.html、assets、pages、static)
|
||||
```
|
||||
|
||||
## 3. 手动部署
|
||||
|
||||
### 3.1 准备代码与依赖
|
||||
|
||||
@ -144,7 +144,7 @@ crmebbz/
|
||||
├── .crmeb/ # AI 规则、技能、角色与各端约定的唯一维护目录
|
||||
├── AGENTS.md # 引导读取 .crmeb/AGENTS.md 的开发入口
|
||||
├── docker-compose.yml # 本地容器编排
|
||||
└── run.sh # Docker 管理脚本
|
||||
└── run.sh # Docker 管理与前端一键开发/发布脚本
|
||||
```
|
||||
|
||||
## 7. 技术栈摘要
|
||||
@ -223,6 +223,17 @@ npm run dev
|
||||
|
||||
开发服务器默认端口为 `1619`,并把 `/adminapi` 代理到 `http://localhost:8011`。
|
||||
|
||||
前端各端也可用仓库根 `run.sh` 一键执行(`node_modules` 缺失时自动 `npm install`):
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---|---|
|
||||
| `./run.sh admin:dev` | 管理端开发测试启动(端口 `1619`),等价 `npm run dev` |
|
||||
| `./run.sh admin:build` | 管理端 `build:all`(主包 + 扩展 UMD)并发布到 `backend/public/admin` |
|
||||
| `./run.sh uni:dev` | 用户端 H5 开发测试启动,等价 `npm run dev:h5` |
|
||||
| `./run.sh uni:build` | 用户端 H5 构建并发布到 `backend/public`(PHP 侧文件不受影响) |
|
||||
|
||||
交互式菜单(直接执行 `./run.sh`)对应编号为 `11/12/21/22`。
|
||||
|
||||
### 8.4 后台进程
|
||||
|
||||
```bash
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user