docs: 新增项目规范体系与前端一键部署文档

This commit is contained in:
sugar1569 2026-09-23 17:14:07 +08:00
parent 8071c277b6
commit 05a184ead9
67 changed files with 3415 additions and 2 deletions

View File

@ -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 热更新可能失效,修改文件后页面若未更新,需手动重启服务。
## 八、登录账号

View 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` 契约。
- 触发升级的改动可生成可追溯、可校验、经过验证的候选升级包。
- 自动检查通过;未通过项、动态验证缺口和需负责人决策的内容明确列出。

View File

@ -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/` 下按主题新增文件,并在此处登记。

View File

@ -0,0 +1,28 @@
# {{APP_ID}} 核心功能开发
> TODO:按当前代码补全,删除所有 TODO。每个核心功能都要能定位到入口与验证方式。
## 核心功能清单
| 功能 | 入口 | 关键文件 | 验证方式 |
| --- | --- | --- | --- |
| TODO | TODO | TODO | TODO |
## 功能详解
### TODO:功能名称
- 入口:TODO(页面、路由或 API 路径)
- 业务层:TODO
- 数据模型:TODO(表或字段、迁移位置)
- 扩展点:TODO(插件、事件、钩子或配置)
- 依赖服务:TODO
- 测试与验证命令:TODO
## 开发流程
TODO:新增一个功能时按顺序要改哪些层,各层对应的命令与检查项。
## 常见坑
TODO:该端特有的约束、易错点和已知限制。

View File

@ -0,0 +1,29 @@
# {{APP_ID}} 项目目录说明
> TODO:按当前代码补全,删除所有 TODO。只写 `{{APP_PATH}}` 的事实,不写计划。
## 目录树
TODO:列出 `{{APP_PATH}}` 下的目录与关键文件,标注每项职责。
## 逐目录职责
| 目录 | 职责 | 禁止放置 |
| --- | --- | --- |
| TODO | TODO | TODO |
## 关键入口
TODO:启动入口、路由注册、配置文件、依赖清单、构建配置的真实路径。
## 请求与数据链路
TODO:从请求或页面进入,到业务处理、数据访问和外部依赖的调用链,逐步给出真实文件位置。
## 依赖边界
TODO:本端可依赖哪些模块、不可反向依赖哪些模块、与哪些服务或应用交互。
## 变更影响面
TODO:改动本端目录时需要同步检查的 API、权限、数据库、测试、文档与升级包。

View File

@ -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。静态检查不等于运行验证;升级包未完成隔离升级测试时只能标记为候选。

View File

@ -0,0 +1,14 @@
---
name: api-and-data
description: API 契约、数据库、迁移、事件和兼容性规则
---
# API 与数据规则
- 请求校验、成功响应、错误码、分页、排序和过滤语义在项目内统一。
- 高风险写接口和回调必须具备幂等、权限和重放防护。
- OpenAPI/schema 等机器契约优先作为客户端类型和契约测试来源。
- 表、字段、金额、时间、状态、软删除和多租户语义统一;索引对应真实查询。
- 迁移只向前追加,已发布迁移不可回改;UPDATE/DELETE 必须有精确条件。
- 新安装 schema 与所有历史迁移的最终结果一致,破坏性变更有兼容窗口和恢复计划。

View File

@ -0,0 +1,14 @@
---
name: architecture
description: 项目目录、模块边界与依赖方向强制规则
---
# 架构与目录规则
- AI 规则正文只维护在 `.crmeb/`。
- 后端业务代码位于 `backend/`,前端应用位于 `frontend/<应用名>/`。
- 模块公开入口和依赖方向必须明确;禁止跨模块引用内部实现或形成循环依赖。
- 领域逻辑不依赖 UI、Web 框架、数据库驱动或具体第三方 SDK。
- 共享代码只有存在两个以上真实消费者时才抽取,并具有明确所有者。
- 目录迁移必须同步 import、构建、测试、Docker、CI、部署和文档,并作为可回滚的独立变更。

View File

@ -0,0 +1,14 @@
---
name: backend
description: 后端分层、事务、缓存、队列与外部依赖规则
---
# 后端规则
- 协议层只做输入适配、鉴权入口和输出映射;业务规则进入用例或领域层。
- 持久化和第三方调用通过明确接口隔离,领域层不依赖具体实现。
- 事务位于业务用例边界,避免在事务中执行不受控网络调用。
- 写操作定义并发、幂等和重试;缓存定义 key、TTL、失效责任和一致性。
- 队列与任务定义超时、退避、死信、补偿和重复消费策略。
- 错误分层映射,结构化日志脱敏并携带请求或任务关联 ID。

View File

@ -0,0 +1,14 @@
---
name: coding
description: 跨技术栈代码质量、命名、类型、注释与依赖规则
---
# 代码规范
- 遵守项目格式化、lint、类型和框架工具;不引入并行风格。
- 名称表达领域含义,避免无上下文缩写、布尔歧义和伪通用工具类。
- 函数和模块保持单一职责,副作用、输入、输出和失败方式清晰。
- 优先类型和可执行契约,不用注释重复代码;注释解释原因、不变量和风险。
- 禁止吞异常、调试输出、硬编码秘密、无边界查询和未经说明的全局状态。
- 新依赖需说明用途、许可证与维护状态,并写入正确 manifest 和 lockfile。

View File

@ -0,0 +1,15 @@
---
name: documentation
description: 项目事实、AI 规则、过程记录和测试索引边界规则
---
# 文档规则
- `help/docs/` 描述当前项目事实,`.crmeb/` 维护 AI 规则和技能。
- `tests/` 保存方案、决策和验收过程,并提供测试统一入口;过程文档与测试索引同目录,不另建平行的过程文档目录。
- 同一事实只有一个权威正文,其他入口只摘要和链接。
- 命令写明执行目录、前置条件、成功信号和已验证环境。
- 代码、配置和可执行结果优先于旧文档;冲突时修正文档并记录证据。
- 新增或移动文档后检查链接、索引和易漂移事实。
- 后端及每个独立前端应用必须有就近 `README.md`,并链接到 `help/docs/applications/<应用ID>.md`;不能用一份总说明替代多个应用。
- 代码变更影响应用职责、版本、命令、配置、端口、路由/API、数据、构建或发布方式时,同一变更检查并更新对应应用说明;不影响时记录已检查无需更新。

View File

@ -0,0 +1,14 @@
---
name: frontend
description: 前端应用、组件、状态、接口、体验与构建规则
---
# 前端规则
- 可独立构建的应用位于 `frontend/<应用名>/`,共享包通过 workspace 显式依赖。
- 页面负责场景编排,业务逻辑进入 service、store、hook 或 composable。
- 请求、响应、路由、组件属性和事件保持类型安全,并与 API 契约同步。
- 页面覆盖 loading、empty、error、无权限和成功反馈,危险操作必须确认。
- 使用设计令牌与共享组件,满足响应式、键盘操作和可访问名称要求。
- 构建变量不得承载秘密;发布物从锁定依赖的干净环境生成。

View File

@ -0,0 +1,15 @@
---
name: fullstack
description: 前端到后端、API、权限、数据库和升级交付的跨层同步规则
---
# 全栈协作规则
- 从前端 API 函数追踪 request 封装、后端路由、Controller、Service、Repository 和 migration,不根据字段名猜实现。
- 前端校验负责体验,服务端负责安全校验、权限、所有权和业务不变量。
- 新增持久化字段同步 migration、Model、Service、API schema、前端类型、表单、列表、测试和升级包。
- 列表接口由后端分页、校验筛选和限制排序;数据库索引对应实际查询。
- 权限同步菜单、路由、中间件、角色数据和前端控制,隐藏按钮不能替代服务端鉴权。
- 手工改库只能用于经授权的临时诊断,正式交付必须通过可追踪迁移。
- 对不熟悉后端的协作者,用页面/API语言解释影响,提供真实路径和最小验证命令。

View File

@ -0,0 +1,14 @@
---
name: git-and-release
description: Git 变更边界、提交、版本、构建与发布规则
---
# Git 与发布规则
- 修改前检查工作区,保留用户已有变更;提交按主题分离。
- 不提交秘密、运行数据、缓存、依赖目录、临时产物和本地 IDE 配置。
- 版本号来自唯一来源,已发布 tag、manifest 和迁移不可回改。
- 构建和发布从干净提交执行,制品记录来源提交和校验和。
- 生成本地制品不等于授权提交、推送、打 tag、发布或上传。
- 发布说明包含变更、兼容性、迁移、备份、回滚边界和验证结果。

View File

@ -0,0 +1,14 @@
---
name: security
description: 凭据、鉴权、输入、上传、日志和供应链安全规则
---
# 安全与日志规则
- 密钥、令牌、Cookie、真实账号和生产数据不得进入仓库、日志或升级包。
- 服务端对每个资源执行认证、授权和对象所有权校验。
- SQL 参数化;上传校验内容、大小、类型、位置和访问权限。
- 回调和支付验证签名、时效、重放与幂等。
- 日志结构化且脱敏,异常响应不泄露堆栈和内部配置。
- 依赖使用锁文件与可信源;高风险漏洞必须在发布前处理或明确阻断。

View File

@ -0,0 +1,14 @@
---
name: testing
description: 测试分层、回归、数据隔离与质量门禁规则
---
# 测试规则
- 新功能和缺陷修复覆盖业务不变量、权限、失败路径和回归场景。
- 单元、集成、契约、端到端和生命周期测试职责分明,不互相冒充。
- 测试数据可重复创建和清理,不依赖个人环境或生产数据。
- `check/test/build` 在本地与 CI 使用同一入口和锁定依赖。
- 被跳过、环境受限或未运行的测试必须明确报告,不能写成通过。
- 升级相关变更至少验证最近稳定版到目标版,声明跨级支持时验证完整迁移链。

View File

@ -0,0 +1,14 @@
---
name: upgrade
description: 增量升级包、迁移、校验、备份和失败恢复规则
---
# 升级包规则
- 后端、前端、数据库或运行配置发生可交付变化时判断升级包触发条件。
- 核心升级包与插件/扩展包分离,按所有权收集文件和迁移。
- manifest 记录版本范围、来源提交、组件、依赖、迁移、备份和回滚边界。
- 数据迁移顺序稳定、尽可能幂等;已发布版本和迁移不可回改。
- 包含文件清单与 SHA-256,禁止秘密、运行数据、上传文件和数据库备份。
- 未完成旧版隔离升级、健康检查和失败恢复验证时只能标记为候选包。

View File

@ -0,0 +1,7 @@
---
description: 项目统一工程规范入口;处理任何代码、文档、测试或发布任务时使用
alwaysApply: true
---
项目规则唯一正文是 @../../.crmeb/AGENTS.md 。先读取该入口及其为当前任务指定的规则,不在 Cursor 目录维护第二份规则。修改后执行 `.crmeb/project.json` 声明的最小验证命令,并准确报告未验证项。

View File

@ -0,0 +1,4 @@
# GitHub Copilot 项目指令
开始修改前先读取 [.crmeb/AGENTS.md](../.crmeb/AGENTS.md)。项目规则正文只维护在 `.crmeb/`,本文件不复制规则。按任务读取对应规则,保留已有工作区改动,并执行 `.crmeb/project.json` 声明的检查、测试和构建命令。

View File

@ -0,0 +1,6 @@
# AI 开发入口
开始处理本仓库任务前,读取 [.crmeb/AGENTS.md](.crmeb/AGENTS.md),再按其索引加载与目标文件和任务相关的规则。
项目事实以代码、可执行配置和正式文档为准。修改前保留已有工作区改动,完成后执行 `.crmeb/project.json` 声明的最小验证命令,并准确报告未验证项。

View File

@ -0,0 +1,6 @@
@.crmeb/AGENTS.md
# Claude Code 入口
项目规则正文只维护在 `.crmeb/`。先执行上面的导入,再按 `.crmeb/AGENTS.md` 的索引加载与当前任务有关的规则,不复制或另建第二套规范。

View File

@ -0,0 +1,6 @@
@./.crmeb/AGENTS.md
# Gemini CLI 入口
项目规则正文只维护在 `.crmeb/`。先加载上面的项目规则,再按任务选择相关规则;修改后运行项目契约声明的最小验证命令。

View File

@ -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)

View File

@ -0,0 +1,4 @@
# 项目文档
本目录描述项目当前已实现事实。应用 Skill 后,根据代码和配置补齐项目概览、架构、开发运行、API、数据库、Docker、部署、运维和升级文档;不保留未经验证的模板结论。

View File

@ -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.

View File

@ -0,0 +1,5 @@
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
& node (Join-Path $scriptDir 'scripts/project-command.mjs') @args
exit $LASTEXITCODE

View File

@ -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" "$@"

View File

@ -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)
}

View File

@ -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)

View File

@ -0,0 +1,14 @@
# 过程文档与测试索引
本目录同时承担两项职责:保存方案、决策、迁移记录和验收报告等过程文档,并提供测试统一索引。
```text
tests/
README.md 本文件:过程文档导航 + 测试索引
plans/ 待评审、进行中的方案
reports/ 检查、验收、回归报告
```
过程文档顶部标注性质(待评审 / 已实施 / 已归档)与核对日期;已实施事实必须同步到正式文档或 `.crmeb/` 规则,过程记录不得冒充当前实现。失效后删除。
测试索引登记各应用的静态检查、单元、集成、契约、端到端和生命周期测试位置、命令、前置依赖与覆盖边界。测试脚本按端落位,本目录不存放测试脚本。

View File

@ -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 用仓库上下文发起一次规则相关问题,确认读取仓库级指令。
若无法实际启动某工具,只能报告“入口静态有效”,不得声称已验证该工具发现成功。

View File

@ -0,0 +1,32 @@
# API 与数据库规范
## API 契约
- HTTP/API 风格、版本策略和基础路径在项目中唯一定义。
- 请求校验覆盖类型、长度、格式、枚举、范围和业务权限;未知字段策略明确。
- 成功与错误响应稳定,错误含机器码和可安全展示的信息,不泄露内部异常。
- 列表统一分页、排序和过滤语义;大结果集禁止无边界返回。
- 创建、支付、回调和重试接口定义幂等键或等价机制。
- OpenAPI、GraphQL schema、protobuf 等机器契约是客户端类型和契约测试的优先来源。
- 破坏性变更先提供兼容窗口、弃用说明和升级路径。
## 数据库
- 表、字段、约束和索引遵守项目语言和数据库约定,并在规则中明确。
- 主键、时间、金额、状态、软删除和多租户字段必须有统一类型与语义。
- 索引来自实际查询模式;唯一业务约束优先由数据库保障。
- 迁移只向前追加,已发布迁移不回改;大表采用分阶段兼容迁移。
- UPDATE/DELETE 有精确条件;批量操作评估锁、事务大小和恢复方式。
- 新安装 schema、种子和所有历史迁移必须得到一致结果。
## 契约联动
数据结构变化必须同步检查:Model/Entity、Repository、Service、API schema、前端类型、表单、列表、导入导出、权限、缓存、事件、测试、新安装和升级迁移。
## 验证
- 校验迁移顺序、重复执行行为和失败恢复。
- 对关键查询查看执行计划或等价分析,确认索引命中和结果边界。
- 契约测试覆盖必填/可选字段、错误、分页、权限和版本兼容。
- 禁止把生产数据复制到开发或测试环境,脱敏数据也需遵守授权和保留策略。

View File

@ -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、配置、数据库、异步进程、测试或部署方式时,同一变更检查并更新对应说明。
- 旧说明与代码不一致时,以代码、配置和实际执行结果为准,并明确尚未动态验证的内容。

View File

@ -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 或定期检查发现链接、命令和事实漂移。
建议在贡献规范中加入“变更是否需要更新文档”的检查项,但不要承诺不存在的自动化流程。

View File

@ -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/路径静态核对和文档链接检查,并明确未完成运行验证。

View File

@ -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、权限、数据、构建产物或发布方式时,同一变更必须检查并更新对应应用说明及文档索引。纯内部重构若不改变上述事实,可以不改正文,但交付报告应说明已检查且无需更新。

View File

@ -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 的影响 | 最小可实施动作 |
结尾给出建议实施顺序和验证计划。没有证据的推测单独标注为“待确认”。

View File

@ -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 语言解释后端层级与数据库影响,并给出真实文件定位;
- 明确哪些值由前端提交、后端验证、后端计算和数据库约束;
- 优先修改迁移与代码,不指导手工改库作为正式方案;
- 给出最小可执行验证命令和请求示例,但不暴露真实凭据;
- 说明本次是否涉及权限、缓存、异步任务、历史数据和升级包;
- 不要求用户先掌握后端框架术语才提供可落地方案。

View File

@ -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 地址、构建产物、目标平台或发布流程时,同步更新该应用说明和正式文档索引。
- 文档中的版本与命令必须来自当前代码并经过静态或运行验证,不沿用另一个前端项目的配置。

View File

@ -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. 升级包静态检查与旧版升级验证。
交付时逐项报告实际执行结果。只通过静态检查不能声明功能、部署或升级已验证。

View File

@ -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`;无法生成时必须作为未完成阻塞报告,不能声明规范建设完成。

View File

@ -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 与升级包说明。

View File

@ -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 参数、健康检查、优雅关闭和容器内存边界进入运行文档。

View File

@ -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/`,升级包是否带依赖由部署模型明确决定。

View File

@ -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 不满足版本时可使用项目开发镜像,但须说明实际验证环境。

View File

@ -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 和静态资源处理进入运行说明。
- 测试隔离时区、事件循环、数据库事务和外部服务。

View File

@ -0,0 +1,9 @@
# React Profile
- React 应用位于 `frontend/<应用名>/`,从 manifest、lockfile 和构建框架配置核对版本。
- 组件保持纯渲染;副作用只处理外部同步并明确依赖、取消和清理。
- props、事件、路由、API 和表单保持类型安全,避免把派生值复制进 state。
- 服务端缓存、全局客户端状态和组件局部状态使用不同层,避免单一 store 承载一切。
- 标准检查映射 ESLint、TypeScript、单元/组件/E2E 测试和生产构建。
- SSR/CSR 边界、环境变量暴露、hydration、错误边界和静态资源路径进入开发文档。

View File

@ -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、证书、平台密钥和生产域名不得用个人或真实秘密模板覆盖。

View File

@ -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 和静态部署路径进入开发文档。

View File

@ -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 应调用同一标准命令,不维护另一套命令清单。
- 修改应用、服务或命令时,同一变更更新项目契约并运行检查脚本。

View File

@ -0,0 +1,33 @@
# 安全与可观测性
## 安全底线
- 秘钥、令牌、私钥、Cookie、真实账号和生产数据不得入库、日志或升级包。
- 服务端对每个资源执行认证、授权和对象所有权校验,不能依赖前端隐藏按钮。
- SQL 使用参数绑定;上传校验类型、大小、扩展名、存储位置和访问权限。
- 回调、支付和 webhook 验证签名、时间窗、重放与幂等。
- 外部 URL、命令、模板和反序列化输入按风险防 SSRF、注入、路径穿越和代码执行。
- 依赖使用锁文件和可信源,定期扫描漏洞;高风险依赖升级需回归与发布说明。
- 默认最小权限,开发、测试、生产账号和网络隔离。
## 日志与追踪
- 使用结构化日志,统一时间、级别、服务、环境、request/trace/job ID、动作和结果字段。
- 日志写业务标识的安全形式,不记录完整请求体、认证头和敏感个人信息。
- HTTP、队列、定时任务和外部调用传播关联 ID;异常保留可定位根因的内部上下文。
- 定义日志保留、轮转和访问权限,调试日志不得永久开启。
## 指标与健康检查
- 区分 liveness、readiness 和业务 smoke;健康检查不执行高成本写操作。
- 监控请求率、错误率、延迟、资源、数据库、缓存、队列积压和任务失败。
- 为关键业务结果定义指标和告警,告警包含影响、检查入口和处理手册链接。
- 发布与升级标记进入日志/指标,便于关联异常变化。
## 性能与可靠性
- 查询分页、缓存失效、事务时长、超时、重试、熔断和限流有明确策略。
- 重试只用于可重试错误并采用退避与上限;非幂等操作不可盲目重试。
- 外部依赖故障有降级或清晰失败,不无限等待或吞错。
- 性能优化必须有基线和验证数据,不以复杂缓存替代正确性。

View 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
已完成项目文档体系的审计与完善:
- 新增/修复:列出入口和职责变化。
- 事实核对:列出版本、命令、路径、路由等证据来源。
- 已运行验证:逐条列命令和结果。
- 未运行验证:列原因和影响范围。
- 待确认:只列必须由项目负责人决策的事项。
```

View File

@ -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. 只有全部门禁通过后才允许发布阶段。
报告缓存命中和跳过原因。缺少秘密或依赖导致的跳过不能写成通过。

View File

@ -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、迁移概要、备份要求、人工步骤与未验证项。
- 未通过隔离升级测试、含未解决高风险迁移或无法确定来源版本时阻止正式发布。

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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 和持久化验证。')

View File

@ -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。')

View 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`)
}

View File

@ -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
只有明确选择容器环境时使用:

View File

@ -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)。

View File

View File

View File

@ -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 后台进程

View File

@ -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 准备代码与依赖

View File

@ -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