mirror of
https://github.com/Lydanne/spaceflow.git
synced 2026-03-11 19:52:45 +08:00
chore: 初始化仓库
This commit is contained in:
483
commands/ci-scripts/CHANGELOG.md
Normal file
483
commands/ci-scripts/CHANGELOG.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Changelog
|
||||
|
||||
## [0.19.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.18.0...@spaceflow/ci-scripts@0.19.0) (2026-02-15)
|
||||
|
||||
### 新特性
|
||||
|
||||
* **cli:** 新增 MCP Server 命令并集成 review 扩展的 MCP 工具 ([b794b36](https://git.bjxgj.com/xgj/spaceflow/commit/b794b36d90788c7eb4cbb253397413b4a080ae83))
|
||||
* **cli:** 新增 MCP Server 导出类型支持 ([9568cbd](https://git.bjxgj.com/xgj/spaceflow/commit/9568cbd14d4cfbdedaf2218379c72337af6db271))
|
||||
* **core:** 为所有命令添加 i18n 国际化支持 ([867c5d3](https://git.bjxgj.com/xgj/spaceflow/commit/867c5d3eccc285c8a68803b8aa2f0ffb86a94285))
|
||||
* **core:** 新增 GitLab 平台适配器并完善配置支持 ([47be9ad](https://git.bjxgj.com/xgj/spaceflow/commit/47be9adfa90944a9cb183e03286a7a96fec747f1))
|
||||
* **core:** 新增 Logger 全局日志工具并支持 plain/tui 双模式渲染 ([8baae7c](https://git.bjxgj.com/xgj/spaceflow/commit/8baae7c24139695a0e379e1c874023cd61dfc41b))
|
||||
* **docs:** 新增 VitePress 文档站点并完善项目文档 ([a79d620](https://git.bjxgj.com/xgj/spaceflow/commit/a79d6208e60390a44fa4c94621eb41ae20159e98))
|
||||
* **mcp:** 新增 MCP Inspector 交互式调试支持并优化工具日志输出 ([05fd2ee](https://git.bjxgj.com/xgj/spaceflow/commit/05fd2ee941c5f6088b769d1127cb7c0615626f8c))
|
||||
* **review:** 为 MCP 服务添加 i18n 国际化支持 ([a749054](https://git.bjxgj.com/xgj/spaceflow/commit/a749054eb73b775a5f5973ab1b86c04f2b2ddfba))
|
||||
* **review:** 新增规则级 includes 解析测试并修复文件级/规则级 includes 过滤逻辑 ([4baca71](https://git.bjxgj.com/xgj/spaceflow/commit/4baca71c17782fb92a95b3207f9c61e0b410b9ff))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
* **actions:** 修正 pnpm setup 命令调用方式 ([8f014fa](https://git.bjxgj.com/xgj/spaceflow/commit/8f014fa90b74e20de4c353804d271b3ef6f1288f))
|
||||
* **mcp:** 添加 -y 选项确保 Inspector 自动安装依赖 ([a9201f7](https://git.bjxgj.com/xgj/spaceflow/commit/a9201f74bd9ddc5eba92beaaa676f377842863e0))
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **claude:** 移除 .claude 目录及其 .gitignore 配置文件 ([91916a9](https://git.bjxgj.com/xgj/spaceflow/commit/91916a99f65da31c1d34e6f75b5cbea1d331ba35))
|
||||
* **cli:** 优化依赖安装流程并支持 .spaceflow 目录配置 ([5977631](https://git.bjxgj.com/xgj/spaceflow/commit/597763183eaa61bb024bba2703d75239650b54fb))
|
||||
* **cli:** 拆分 CLI 为独立包并重构扩展加载机制 ([b385d28](https://git.bjxgj.com/xgj/spaceflow/commit/b385d281575f29b823bb6dc4229a396a29c0e226))
|
||||
* **cli:** 移除 ExtensionModule 并优化扩展加载机制 ([8f7077d](https://git.bjxgj.com/xgj/spaceflow/commit/8f7077deaef4e5f4032662ff5ac925cd3c07fdb6))
|
||||
* **cli:** 调整依赖顺序并格式化导入语句 ([32a9c1c](https://git.bjxgj.com/xgj/spaceflow/commit/32a9c1cf834725a20f93b1f8f60b52692841a3e5))
|
||||
* **cli:** 重构 getPluginConfigFromPackageJson 方法以提高代码可读性 ([f5f6ed9](https://git.bjxgj.com/xgj/spaceflow/commit/f5f6ed9858cc4ca670e30fac469774bdc8f7b005))
|
||||
* **cli:** 重构扩展配置格式,支持 flow/command/skill 三种导出类型 ([958dc13](https://git.bjxgj.com/xgj/spaceflow/commit/958dc130621f78bbcc260224da16a5f16ae0b2b1))
|
||||
* **core:** 为 build/clear/commit 命令添加国际化支持 ([de82cb2](https://git.bjxgj.com/xgj/spaceflow/commit/de82cb2f1ed8cef0e446a2d42a1bf1f091e9c421))
|
||||
* **core:** 优化 list 命令输出格式并修复 MCP Inspector 包管理器兼容性 ([a019829](https://git.bjxgj.com/xgj/spaceflow/commit/a019829d3055c083aeb86ed60ce6629d13012d91))
|
||||
* **core:** 将 rspack 配置和工具函数中的 @spaceflow/cli 引用改为 @spaceflow/core ([3c301c6](https://git.bjxgj.com/xgj/spaceflow/commit/3c301c60f3e61b127db94481f5a19307f5ef00eb))
|
||||
* **core:** 将扩展依赖从 @spaceflow/cli 迁移到 @spaceflow/core ([6f9ffd4](https://git.bjxgj.com/xgj/spaceflow/commit/6f9ffd4061cecae4faaf3d051e3ca98a0b42b01f))
|
||||
* **core:** 提取 source 处理和包管理器工具函数到共享模块 ([ab3ff00](https://git.bjxgj.com/xgj/spaceflow/commit/ab3ff003d1cd586c0c4efc7841e6a93fe3477ace))
|
||||
* **core:** 新增 getEnvFilePaths 工具函数统一管理 .env 文件路径优先级 ([809fa18](https://git.bjxgj.com/xgj/spaceflow/commit/809fa18f3d0b8eabcb068988bab53d548eaf03ea))
|
||||
* **core:** 新增远程仓库规则拉取功能并支持 Git API 获取目录内容 ([69ade16](https://git.bjxgj.com/xgj/spaceflow/commit/69ade16c9069f9e1a90b3ef56dc834e33a3c0650))
|
||||
* **core:** 统一 LogLevel 类型定义并支持字符串/数字双模式 ([557f6b0](https://git.bjxgj.com/xgj/spaceflow/commit/557f6b0bc39fcfb0e3f773836cbbf08c1a8790ae))
|
||||
* **core:** 重构配置读取逻辑,新增 ConfigReaderService 并支持 .spaceflowrc 配置文件 ([72e88ce](https://git.bjxgj.com/xgj/spaceflow/commit/72e88ced63d03395923cdfb113addf4945162e54))
|
||||
* **i18n:** 将 locales 导入从命令文件迁移至扩展入口文件 ([0da5d98](https://git.bjxgj.com/xgj/spaceflow/commit/0da5d9886296c4183b24ad8c56140763f5a870a4))
|
||||
* **i18n:** 移除扩展元数据中的 locales 字段并改用 side-effect 自动注册 ([2c7d488](https://git.bjxgj.com/xgj/spaceflow/commit/2c7d488a9dfa59a99b95e40e3c449c28c2d433d8))
|
||||
* **mcp:** 使用 DTO + Swagger 装饰器替代手动 JSON Schema 定义 ([87ec262](https://git.bjxgj.com/xgj/spaceflow/commit/87ec26252dd295536bb090ae8b7e418eec96e1bd))
|
||||
* **mcp:** 升级 MCP SDK API 并优化 Inspector 调试配置 ([176d04a](https://git.bjxgj.com/xgj/spaceflow/commit/176d04a73fbbb8d115520d922f5fedb9a2961aa6))
|
||||
* **mcp:** 将 MCP 元数据存储从 Reflect Metadata 改为静态属性以支持跨模块访问 ([cac0ea2](https://git.bjxgj.com/xgj/spaceflow/commit/cac0ea2029e1b504bc4278ce72b3aa87fff88c84))
|
||||
* **test:** 迁移测试框架从 Jest 到 Vitest ([308f9d4](https://git.bjxgj.com/xgj/spaceflow/commit/308f9d49089019530588344a5e8880f5b6504a6a))
|
||||
* 优化构建流程并调整 MCP/review 日志输出级别 ([74072c0](https://git.bjxgj.com/xgj/spaceflow/commit/74072c04be7a45bfc0ab53b636248fe5c0e1e42a))
|
||||
* 将 .spaceflow/package.json 纳入版本控制并自动添加到根项目依赖 ([ab83d25](https://git.bjxgj.com/xgj/spaceflow/commit/ab83d2579cb5414ee3d78a9768fac2147a3d1ad9))
|
||||
* 将 GiteaSdkModule/GiteaSdkService 重命名为 GitProviderModule/GitProviderService ([462f492](https://git.bjxgj.com/xgj/spaceflow/commit/462f492bc2607cf508c5011d181c599cf17e00c9))
|
||||
* 恢复 pnpm catalog 配置并移除 .spaceflow 工作区导入器 ([217387e](https://git.bjxgj.com/xgj/spaceflow/commit/217387e2e8517a08162e9bcaf604893fd9bca736))
|
||||
* 迁移扩展依赖到 .spaceflow 工作区并移除 pnpm catalog ([c457c0f](https://git.bjxgj.com/xgj/spaceflow/commit/c457c0f8918171f1856b88bc007921d76c508335))
|
||||
* 重构 Extension 安装机制为 pnpm workspace 模式 ([469b12e](https://git.bjxgj.com/xgj/spaceflow/commit/469b12eac28f747b628e52a5125a3d5a538fba39))
|
||||
* 重构插件加载改为扩展模式 ([0e6e140](https://git.bjxgj.com/xgj/spaceflow/commit/0e6e140b19ea2cf6084afc261c555d2083fe04f9))
|
||||
|
||||
### 文档更新
|
||||
|
||||
* **guide:** 更新编辑器集成文档,补充四种导出类型说明和 MCP 注册机制 ([19a7409](https://git.bjxgj.com/xgj/spaceflow/commit/19a7409092c89d002f11ee51ebcb6863118429bd))
|
||||
* **guide:** 更新配置文件位置说明并补充 RC 文件支持 ([2214dc4](https://git.bjxgj.com/xgj/spaceflow/commit/2214dc4e197221971f5286b38ceaa6fcbcaa7884))
|
||||
|
||||
### 测试用例
|
||||
|
||||
* **core:** 新增 GiteaAdapter 完整单元测试并实现自动检测 provider 配置 ([c74f745](https://git.bjxgj.com/xgj/spaceflow/commit/c74f7458aed91ac7d12fb57ef1c24b3d2917c406))
|
||||
* **review:** 新增 DeletionImpactService 测试覆盖并配置 coverage 工具 ([50bfbfe](https://git.bjxgj.com/xgj/spaceflow/commit/50bfbfe37192641f1170ade8f5eb00e0e382af67))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-shell:** released version 0.18.0 [no ci] ([f64fd80](https://git.bjxgj.com/xgj/spaceflow/commit/f64fd8009a6dd725f572c7e9fbf084d9320d5128))
|
||||
* **ci:** 迁移工作流从 Gitea 到 GitHub 并统一环境变量命名 ([57e3bae](https://git.bjxgj.com/xgj/spaceflow/commit/57e3bae635b324c8c4ea50a9fb667b6241fae0ef))
|
||||
* **cli:** released version 0.19.0 [no ci] ([6b63149](https://git.bjxgj.com/xgj/spaceflow/commit/6b631499e2407a1822395d5f40cec2d725331b78))
|
||||
* **config:** 将 git 推送白名单用户从 "Gitea Actions" 改为 "GiteaActions" ([fdbb865](https://git.bjxgj.com/xgj/spaceflow/commit/fdbb865341e6f02b26fca32b54a33b51bee11cad))
|
||||
* **config:** 将 git 推送白名单用户从 github-actions[bot] 改为 Gitea Actions ([9c39819](https://git.bjxgj.com/xgj/spaceflow/commit/9c39819a9f95f415068f7f0333770b92bc98321b))
|
||||
* **config:** 移除 review-spec 私有仓库依赖 ([8ae18f1](https://git.bjxgj.com/xgj/spaceflow/commit/8ae18f13c441b033d1cbc75119695a5cc5cb6a0b))
|
||||
* **core:** released version 0.1.0 [no ci] ([170fa67](https://git.bjxgj.com/xgj/spaceflow/commit/170fa670e98473c2377120656d23aae835c51997))
|
||||
* **core:** 禁用 i18next 初始化时的 locize.com 推广日志 ([a99fbb0](https://git.bjxgj.com/xgj/spaceflow/commit/a99fbb068441bc623efcf15a1dd7b6bd38c05f38))
|
||||
* **deps:** 移除 pnpm catalog 配置并更新依赖锁定 ([753fb9e](https://git.bjxgj.com/xgj/spaceflow/commit/753fb9e3e43b28054c75158193dc39ab4bab1af5))
|
||||
* **docs:** 统一文档脚本命名,为 VitePress 命令添加 docs: 前缀 ([3cc46ea](https://git.bjxgj.com/xgj/spaceflow/commit/3cc46eab3a600290f5064b8270902e586b9c5af4))
|
||||
* **i18n:** 配置 i18n-ally-next 自动提取键名生成策略 ([753c3dc](https://git.bjxgj.com/xgj/spaceflow/commit/753c3dc3f24f3c03c837d1ec2c505e8e3ce08b11))
|
||||
* **i18n:** 重构 i18n 配置并统一 locales 目录结构 ([3e94037](https://git.bjxgj.com/xgj/spaceflow/commit/3e94037fa6493b3b0e4a12ff6af9f4bea48ae217))
|
||||
* **period-summary:** released version 0.18.0 [no ci] ([f0df638](https://git.bjxgj.com/xgj/spaceflow/commit/f0df63804d06f8c75e04169ec98226d7a4f5d7f9))
|
||||
* **publish:** released version 0.20.0 [no ci] ([d347e3b](https://git.bjxgj.com/xgj/spaceflow/commit/d347e3b2041157d8dc6e3ade69b05a481b2ab371))
|
||||
* **review:** released version 0.28.0 [no ci] ([a2d89ed](https://git.bjxgj.com/xgj/spaceflow/commit/a2d89ed5f386eb6dd299c0d0a208856ce267ab5e))
|
||||
* **scripts:** 修正 setup 和 build 脚本的过滤条件,避免重复构建 cli 包 ([ffd2ffe](https://git.bjxgj.com/xgj/spaceflow/commit/ffd2ffedca08fd56cccb6a9fbd2b6bd106e367b6))
|
||||
* **templates:** 新增 MCP 工具插件模板 ([5f6df60](https://git.bjxgj.com/xgj/spaceflow/commit/5f6df60b60553f025414fd102d8a279cde097485))
|
||||
* **workflows:** 为所有 GitHub Actions 工作流添加 GIT_PROVIDER_TYPE 环境变量 ([a463574](https://git.bjxgj.com/xgj/spaceflow/commit/a463574de6755a0848a8d06267f029cb947132b0))
|
||||
* **workflows:** 在发布流程中添加 GIT_PROVIDER_TYPE 环境变量 ([a4bb388](https://git.bjxgj.com/xgj/spaceflow/commit/a4bb3881f39ad351e06c5502df6895805b169a28))
|
||||
* **workflows:** 在发布流程中添加扩展安装步骤 ([716be4d](https://git.bjxgj.com/xgj/spaceflow/commit/716be4d92641ccadb3eaf01af8a51189ec5e9ade))
|
||||
* **workflows:** 将发布流程的 Git 和 NPM 配置从 GitHub 迁移到 Gitea ([6d9acff](https://git.bjxgj.com/xgj/spaceflow/commit/6d9acff06c9a202432eb3d3d5552e6ac972712f5))
|
||||
* **workflows:** 将发布流程的 GITHUB_TOKEN 改为使用 CI_GITEA_TOKEN ([e7fe7b4](https://git.bjxgj.com/xgj/spaceflow/commit/e7fe7b4271802fcdbfc2553b180f710eed419335))
|
||||
* 为所有 commands 包添加 @spaceflow/cli 开发依赖 ([d4e6c83](https://git.bjxgj.com/xgj/spaceflow/commit/d4e6c8344ca736f7e55d7db698482e8fa2445684))
|
||||
* 优化依赖配置并移除 .spaceflow 包依赖 ([be5264e](https://git.bjxgj.com/xgj/spaceflow/commit/be5264e5e0fe1f53bbe3b44a9cb86dd94ab9d266))
|
||||
* 修正 postinstall 脚本命令格式 ([3f0820f](https://git.bjxgj.com/xgj/spaceflow/commit/3f0820f85dee88808de921c3befe2d332f34cc36))
|
||||
* 恢复 pnpm catalog 配置并更新依赖锁定 ([0b2295c](https://git.bjxgj.com/xgj/spaceflow/commit/0b2295c1f906d89ad3ba7a61b04c6e6b94f193ef))
|
||||
* 新增 .spaceflow/pnpm-workspace.yaml 防止被父级 workspace 接管并移除根项目 devDependencies 自动添加逻辑 ([61de3a2](https://git.bjxgj.com/xgj/spaceflow/commit/61de3a2b75e8a19b28563d2a6476158d19f6c5be))
|
||||
* 新增 postinstall 钩子自动执行 setup 脚本 ([64dae0c](https://git.bjxgj.com/xgj/spaceflow/commit/64dae0cb440bd5e777cb790f826ff2d9f8fe65ba))
|
||||
* 移除 postinstall 钩子避免依赖安装时自动执行构建 ([ea1dc85](https://git.bjxgj.com/xgj/spaceflow/commit/ea1dc85ce7d6cf23a98c13e2c21e3c3bcdf7dd79))
|
||||
|
||||
## [0.18.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.17.0...@spaceflow/ci-scripts@0.18.0) (2026-02-04)
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **verbose:** 扩展 verbose 级别支持至 3 ([c1a0808](https://git.bjxgj.com/xgj/spaceflow/commit/c1a080859e5d25ca1eb3dc7e00a67b32eb172635))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-shell:** released version 0.17.0 [no ci] ([a53508b](https://git.bjxgj.com/xgj/spaceflow/commit/a53508b15e4020e3399bae9cc04e730f1539ad8e))
|
||||
* **core:** released version 0.18.0 [no ci] ([c5e973f](https://git.bjxgj.com/xgj/spaceflow/commit/c5e973fbe22c0fcd0d6d3af6e4020e2fbff9d31f))
|
||||
* **period-summary:** released version 0.17.0 [no ci] ([ac4e5b6](https://git.bjxgj.com/xgj/spaceflow/commit/ac4e5b6083773146ac840548a69006f6c4fbac1d))
|
||||
* **publish:** released version 0.19.0 [no ci] ([7a96bca](https://git.bjxgj.com/xgj/spaceflow/commit/7a96bca945434a99f7d051a38cb31adfd2ade5d2))
|
||||
* **review:** released version 0.27.0 [no ci] ([ac3fc5a](https://git.bjxgj.com/xgj/spaceflow/commit/ac3fc5a5d7317d537d0447e05a61bef15a1accbe))
|
||||
|
||||
## [0.17.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.16.0...@spaceflow/ci-scripts@0.17.0) (2026-02-04)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 新增 override 作用域测试,验证 includes 对 override 过滤的影响 ([820e0cb](https://git.bjxgj.com/xgj/spaceflow/commit/820e0cb0f36783dc1c7e1683ad08501e91f094b2))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 从 PR diff 填充缺失的 patch 字段 ([24bfaa7](https://git.bjxgj.com/xgj/spaceflow/commit/24bfaa76f3bd56c8ead307e73e0623a2221c69cf))
|
||||
- **review:** 新增 getFileContents、getChangedFilesBetweenRefs 和 filterIssuesByValidCommits 方法的单元测试 ([7618c91](https://git.bjxgj.com/xgj/spaceflow/commit/7618c91bc075d218b9f51b862e5161d15a306bf8))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **config:** 降低并发数以优化 AI 审查性能 ([052dd72](https://git.bjxgj.com/xgj/spaceflow/commit/052dd728f759da0a31e86a0ad480e9bb35052781))
|
||||
- **review:** 优化 Markdown 格式化器的代码风格和 JSON 数据输出逻辑 ([ca1b0c9](https://git.bjxgj.com/xgj/spaceflow/commit/ca1b0c96d9d0663a8b8dc93b4a9f63d4e5590df0))
|
||||
- **review:** 优化 override 和变更行过滤的日志输出,增强调试信息的可读性 ([9a7c6f5](https://git.bjxgj.com/xgj/spaceflow/commit/9a7c6f5b4ef2b8ae733fa499a0e5ec82feebc1d2))
|
||||
- **review:** 使用 Base64 编码存储审查数据,避免 JSON 格式在 Markdown 中被转义 ([fb91e30](https://git.bjxgj.com/xgj/spaceflow/commit/fb91e30d0979cfe63ed8e7657c578db618b5e783))
|
||||
- **review:** 基于 fileContents 实际 commit hash 验证问题归属,替代依赖 LLM 填写的 commit 字段 ([de3e377](https://git.bjxgj.com/xgj/spaceflow/commit/de3e3771eb85ff93200c63fa9feb38941914a07d))
|
||||
- **review:** 新增测试方法用于验证 PR 审查功能 ([5c57833](https://git.bjxgj.com/xgj/spaceflow/commit/5c578332cedffb7fa7e5ad753a788bcd55595c68))
|
||||
- **review:** 移除 filterNoCommit 配置项,统一使用基于 commit hash 的问题过滤逻辑 ([82429b1](https://git.bjxgj.com/xgj/spaceflow/commit/82429b1072affb4f2b14d52f99887e12184d8218))
|
||||
- **review:** 移除测试方法 testMethod ([21e9938](https://git.bjxgj.com/xgj/spaceflow/commit/21e9938100c5dd7d4eada022441c565b5c41a55a))
|
||||
- **review:** 统一使用 parseLineRange 方法解析行号,避免重复的正则匹配逻辑 ([c64f96a](https://git.bjxgj.com/xgj/spaceflow/commit/c64f96aa2e1a8e22dcd3e31e1a2acc1bb338a1a8))
|
||||
- **review:** 调整 filterIssuesByValidCommits 逻辑,保留无 commit 的 issue 交由 filterNoCommit 配置处理 ([e9c5d47](https://git.bjxgj.com/xgj/spaceflow/commit/e9c5d47aebef42507fd9fcd67e5eab624437e81a))
|
||||
- **review:** 过滤 merge commits,避免在代码审查中处理合并提交 ([d7c647c](https://git.bjxgj.com/xgj/spaceflow/commit/d7c647c33156a58b42bfb45a67417723b75328c6))
|
||||
- **review:** 过滤非 PR commits 的问题,避免 merge commit 引入的代码被审查 ([9e20f54](https://git.bjxgj.com/xgj/spaceflow/commit/9e20f54d57e71725432dfb9e7c943946aa6677d4))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 新增新增文件无 patch 时的测试用例,优化变更行标记逻辑 ([a593f0d](https://git.bjxgj.com/xgj/spaceflow/commit/a593f0d4a641b348f7c9d30b14f639b24c12dcfa))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.16.0 [no ci] ([87fd703](https://git.bjxgj.com/xgj/spaceflow/commit/87fd7030b54d2f614f23e092499c5c51bfc33788))
|
||||
- **core:** released version 0.17.0 [no ci] ([c85a8ed](https://git.bjxgj.com/xgj/spaceflow/commit/c85a8ed88929d867d2d460a44d08d8b7bc4866a2))
|
||||
- **period-summary:** released version 0.16.0 [no ci] ([b214e31](https://git.bjxgj.com/xgj/spaceflow/commit/b214e31221d5afa04481c48d9ddb878644a22ae7))
|
||||
- **publish:** released version 0.18.0 [no ci] ([2f2ce01](https://git.bjxgj.com/xgj/spaceflow/commit/2f2ce01726f7b3e4387e23a17974b58acd3e6929))
|
||||
- **review:** released version 0.20.0 [no ci] ([8b0f82f](https://git.bjxgj.com/xgj/spaceflow/commit/8b0f82f94813c79d579dbae8decb471b20e45e9d))
|
||||
- **review:** released version 0.21.0 [no ci] ([b51a1dd](https://git.bjxgj.com/xgj/spaceflow/commit/b51a1ddcba3e6a4b3b3eb947864e731d8f87d62b))
|
||||
- **review:** released version 0.22.0 [no ci] ([fca3bfc](https://git.bjxgj.com/xgj/spaceflow/commit/fca3bfc0c53253ac78566e88c7e5d31020a3896b))
|
||||
- **review:** released version 0.23.0 [no ci] ([ed5bf22](https://git.bjxgj.com/xgj/spaceflow/commit/ed5bf22819094df070708c2724669d0b5f7b9008))
|
||||
- **review:** released version 0.24.0 [no ci] ([5f1f94e](https://git.bjxgj.com/xgj/spaceflow/commit/5f1f94ee02123baa05802fb2bb038ccf9d50a0cc))
|
||||
- **review:** released version 0.25.0 [no ci] ([69cfeaf](https://git.bjxgj.com/xgj/spaceflow/commit/69cfeaf768e4bf7b2aaba6f089064469338a1ac0))
|
||||
- **review:** released version 0.26.0 [no ci] ([dec9c7e](https://git.bjxgj.com/xgj/spaceflow/commit/dec9c7ec66455cf83588368c930d12510ada6c0f))
|
||||
|
||||
## [0.16.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.15.0...@spaceflow/ci-scripts@0.16.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 新增 Git diff 行号映射工具并优化 Claude 配置 ([88ef340](https://git.bjxgj.com/xgj/spaceflow/commit/88ef3400127fac3ad52fc326ad79fdc7bd058e98))
|
||||
- **review:** 为 execute 方法添加文档注释 ([a21f582](https://git.bjxgj.com/xgj/spaceflow/commit/a21f58290c873fb07789e70c8c5ded2b5874a29d))
|
||||
- **review:** 为 getPrNumberFromEvent 方法添加文档注释 ([54d1586](https://git.bjxgj.com/xgj/spaceflow/commit/54d1586f4558b5bfde81b926c7b513a32e5caf89))
|
||||
- **review:** 优化行号更新统计,分别统计更新和标记无效的问题数量 ([892b8be](https://git.bjxgj.com/xgj/spaceflow/commit/892b8bed8913531a9440579f777b1965fec772e5))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 优化历史 issue commit 匹配逻辑,支持短 SHA 与完整 SHA 的前缀匹配 ([e30c6dd](https://git.bjxgj.com/xgj/spaceflow/commit/e30c6ddefb14ec6631ce341f1d45c59786e94a46))
|
||||
- **review:** 简化历史问题处理策略,将行号更新改为标记变更文件问题为无效 ([5df7f00](https://git.bjxgj.com/xgj/spaceflow/commit/5df7f0087c493e104fe0dc054fd0b6c19ebe3500))
|
||||
- **review:** 简化行号更新逻辑,使用最新 commit diff 替代增量 diff ([6de7529](https://git.bjxgj.com/xgj/spaceflow/commit/6de7529c90ecbcee82149233fc01c393c5c4e7f7))
|
||||
- **review:** 重构行号更新逻辑,使用增量 diff 替代全量 diff ([d4f4304](https://git.bjxgj.com/xgj/spaceflow/commit/d4f4304e1e41614f7be8946d457eea1cf4e202fb))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 添加单元测试以覆盖行号更新逻辑 ([ebf33e4](https://git.bjxgj.com/xgj/spaceflow/commit/ebf33e45c18c910b88b106cdd4cfeb516b3fb656))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **actions:** 增强命令执行日志,输出原始 command 和 args 参数 ([0f0c238](https://git.bjxgj.com/xgj/spaceflow/commit/0f0c238de7d6f10875022f364746cefa56631b7f))
|
||||
- **ci-shell:** released version 0.15.0 [no ci] ([5c0dc0b](https://git.bjxgj.com/xgj/spaceflow/commit/5c0dc0b5482366ccfd7854868d1eb5f306c24810))
|
||||
- **core:** released version 0.16.0 [no ci] ([871f981](https://git.bjxgj.com/xgj/spaceflow/commit/871f981b0b908c981aaef366f2382ec6ca2e2269))
|
||||
- **period-summary:** released version 0.15.0 [no ci] ([3dd72cb](https://git.bjxgj.com/xgj/spaceflow/commit/3dd72cb65a422b5b008a83820e799b810a6d53eb))
|
||||
- **publish:** released version 0.17.0 [no ci] ([8e0d065](https://git.bjxgj.com/xgj/spaceflow/commit/8e0d0654040d6af7e99fa013a8255aa93acbcc3a))
|
||||
- **review:** released version 0.19.0 [no ci] ([0ba5c0a](https://git.bjxgj.com/xgj/spaceflow/commit/0ba5c0a39879b598da2d774acc0834c590ef6d4c))
|
||||
- 在 PR 审查工作流中启用 --filter-no-commit 参数 ([e0024ad](https://git.bjxgj.com/xgj/spaceflow/commit/e0024ad5cb29250b452a841db2ce6ebf84016a2c))
|
||||
- 禁用删除代码分析功能 ([988e3f1](https://git.bjxgj.com/xgj/spaceflow/commit/988e3f156f2ca4e92413bf7a455eba1760ad9eba))
|
||||
|
||||
## [0.15.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.14.0...@spaceflow/ci-scripts@0.15.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 在 Gitea SDK 中新增编辑 Pull Request 的方法 ([a586bf1](https://git.bjxgj.com/xgj/spaceflow/commit/a586bf110789578f23b39d64511229a1e5635dc4))
|
||||
- **core:** 在 Gitea SDK 中新增获取 reactions 的方法 ([9324cf2](https://git.bjxgj.com/xgj/spaceflow/commit/9324cf2550709b8302171e5522d0792c08bc1415))
|
||||
- **review:** 优化 commit author 获取逻辑,支持 committer 作为备选 ([b75b613](https://git.bjxgj.com/xgj/spaceflow/commit/b75b6133e5b8c95580516480315bc979fc6eb59b))
|
||||
- **review:** 优化 commit author 获取逻辑,支持从 Git 原始作者信息中提取 ([10ac821](https://git.bjxgj.com/xgj/spaceflow/commit/10ac8210a4457e0356c3bc1645f54f6f3d8c904c))
|
||||
- **review:** 优化 commit author 获取逻辑,通过 Gitea API 搜索用户以关联 Git 原始作者 ([daa274b](https://git.bjxgj.com/xgj/spaceflow/commit/daa274bba2255e92d1e9a6e049e20846a69e8df7))
|
||||
- **review:** 优化 PR 标题生成的格式要求 ([a4d807d](https://git.bjxgj.com/xgj/spaceflow/commit/a4d807d0a4feee4ccc88c6096e069c6dbb650a03))
|
||||
- **review:** 优化 verbose 参数支持多级别累加,将日志级别扩展为 0-3 级 ([fe4c830](https://git.bjxgj.com/xgj/spaceflow/commit/fe4c830cac137c5502d700d2cd5f22b52a629e5f))
|
||||
- **review:** 优化历史问题的 author 信息填充逻辑 ([b18d171](https://git.bjxgj.com/xgj/spaceflow/commit/b18d171c9352fe5815262d43ffd9cd7751f03a4e))
|
||||
- **review:** 优化审查报告中回复消息的格式显示 ([f478c8d](https://git.bjxgj.com/xgj/spaceflow/commit/f478c8da4c1d7494819672006e3230dbc8e0924d))
|
||||
- **review:** 优化审查报告中的消息展示格式 ([0996c2b](https://git.bjxgj.com/xgj/spaceflow/commit/0996c2b45c9502c84308f8a7f9186e4dbd4164fb))
|
||||
- **review:** 优化问题 author 信息填充时机,统一在所有问题合并后填充 ([ea8c586](https://git.bjxgj.com/xgj/spaceflow/commit/ea8c586fc60061ffd339e85c6c298b905bdfdcd8))
|
||||
- **review:** 优化问题展示和无效标记逻辑 ([e2b45e1](https://git.bjxgj.com/xgj/spaceflow/commit/e2b45e1ec594488bb79f528911fd6009a3213eca))
|
||||
- **review:** 在 fillIssueAuthors 方法中添加详细的调试日志 ([42ab288](https://git.bjxgj.com/xgj/spaceflow/commit/42ab288933296abdeeb3dbbedbb2aecedbea2251))
|
||||
- **review:** 在 syncReactionsToIssues 中添加详细日志并修复团队成员获取逻辑 ([91f166a](https://git.bjxgj.com/xgj/spaceflow/commit/91f166a07c2e43dabd4dd4ac186ec7b5f03dfc71))
|
||||
- **review:** 在审查报告的回复中为用户名添加 @ 前缀 ([bc6186b](https://git.bjxgj.com/xgj/spaceflow/commit/bc6186b97f0764f6335690eca1f8af665f9b7629))
|
||||
- **review:** 在审查问题中添加作者信息填充功能 ([8332dba](https://git.bjxgj.com/xgj/spaceflow/commit/8332dba4bb826cd358dc96db5f9b9406fb23df9b))
|
||||
- **review:** 将审查命令的详细日志参数从 --verbose 简化为 -vv ([5eb320b](https://git.bjxgj.com/xgj/spaceflow/commit/5eb320b92d1f7165052730b2e90eee52367391dd))
|
||||
- **review:** 扩展评审人收集逻辑,支持从 PR 指定的评审人和团队中获取 ([bbd61af](https://git.bjxgj.com/xgj/spaceflow/commit/bbd61af9d3e2b9e1dcf28c5e3867645fdda52e6f))
|
||||
- **review:** 支持 AI 自动生成和更新 PR 标题 ([e02fb02](https://git.bjxgj.com/xgj/spaceflow/commit/e02fb027d525dd3e794d649e6dbc53c99a3a9a59))
|
||||
- **review:** 支持 PR 关闭事件触发审查并自动传递事件类型参数 ([03967d9](https://git.bjxgj.com/xgj/spaceflow/commit/03967d9e860af7da06e3c04539f16c7bb31557ff))
|
||||
- **review:** 支持在审查报告中展示评论的 reactions 和回复记录 ([f4da31a](https://git.bjxgj.com/xgj/spaceflow/commit/f4da31adf6ce412cb0ce27bfe7a1e87e5350e915))
|
||||
- **review:** 移除 handleReview 中的重复 author 填充逻辑 ([e458bfd](https://git.bjxgj.com/xgj/spaceflow/commit/e458bfd0d21724c37fdd4023265d6a2dd1700404))
|
||||
- **review:** 限制 PR 标题自动更新仅在第一轮审查时执行 ([1891cbc](https://git.bjxgj.com/xgj/spaceflow/commit/1891cbc8d85f6eaef9e7107a7f1003bdc654d3a3))
|
||||
- **review:** 默认启用 PR 标题自动更新功能 ([fda6656](https://git.bjxgj.com/xgj/spaceflow/commit/fda6656efaf6479bb398ddc5cb1955142f31f369))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **actions:** 修复日志输出中的 emoji 显示问题,将 <20> 替换为 ℹ️ ([d3cd94a](https://git.bjxgj.com/xgj/spaceflow/commit/d3cd94afa9c6893b923d316fdcb5904f42ded632))
|
||||
- **review:** 修复审查完成日志中的乱码 emoji ([36c1c48](https://git.bjxgj.com/xgj/spaceflow/commit/36c1c48faecda3cc02b9e0b097aebba0a85ea5f8))
|
||||
- **review:** 将 UserInfo 的 id 字段类型从 number 改为 string ([505e019](https://git.bjxgj.com/xgj/spaceflow/commit/505e019c85d559ce1def1350599c1de218f7516a))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.14.0 [no ci] ([c6e4bdc](https://git.bjxgj.com/xgj/spaceflow/commit/c6e4bdca44874739694e3e46998e376779503e53))
|
||||
- **core:** released version 0.15.0 [no ci] ([48f3875](https://git.bjxgj.com/xgj/spaceflow/commit/48f38754dee382548bab968c57dd0f40f2343981))
|
||||
- **period-summary:** released version 0.14.0 [no ci] ([55a72f2](https://git.bjxgj.com/xgj/spaceflow/commit/55a72f2b481e5ded1d9207a5a8d6a6864328d5a0))
|
||||
- **publish:** released version 0.16.0 [no ci] ([e31e46d](https://git.bjxgj.com/xgj/spaceflow/commit/e31e46d08fccb10a42b6579fa042aa6c57d79c8a))
|
||||
- **review:** released version 0.18.0 [no ci] ([d366e3f](https://git.bjxgj.com/xgj/spaceflow/commit/d366e3fa9c1b32369a3d98e56fc873e033d71d00))
|
||||
|
||||
## [0.14.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.13.0...@spaceflow/ci-scripts@0.14.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 统一所有命令的错误处理,添加堆栈信息输出 ([31224a1](https://git.bjxgj.com/xgj/spaceflow/commit/31224a16ce7155402504bd8d3e386e59e47949df))
|
||||
- **review:** 增强错误处理,添加堆栈信息输出 ([e0fb5de](https://git.bjxgj.com/xgj/spaceflow/commit/e0fb5de6bc877d8f0b3dc3c03f8d614320427bf3))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.13.0 [no ci] ([81e7582](https://git.bjxgj.com/xgj/spaceflow/commit/81e75820eb69ca188155e33945111e2b1f6b3012))
|
||||
- **core:** released version 0.14.0 [no ci] ([996dbc6](https://git.bjxgj.com/xgj/spaceflow/commit/996dbc6f80b0d3fb8049df9a9a31bd1e5b5d4b92))
|
||||
- **period-summary:** released version 0.13.0 [no ci] ([1d47460](https://git.bjxgj.com/xgj/spaceflow/commit/1d47460e40ba422a32865ccddd353e089eb91c6a))
|
||||
- **publish:** released version 0.15.0 [no ci] ([4b09122](https://git.bjxgj.com/xgj/spaceflow/commit/4b091227265a57f0a05488749eb4852fb421a06e))
|
||||
- **review:** released version 0.17.0 [no ci] ([9f25412](https://git.bjxgj.com/xgj/spaceflow/commit/9f254121557ae238e32f4093b0c8b5dd8a4b9a72))
|
||||
|
||||
## [0.13.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.12.0...@spaceflow/ci-scripts@0.13.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 为删除影响分析添加文件过滤功能 ([7304293](https://git.bjxgj.com/xgj/spaceflow/commit/73042937c5271ff4b0dcb6cd6d823e5aa0c03e7b))
|
||||
- **review:** 新增过滤无commit问题的选项 ([7a4c458](https://git.bjxgj.com/xgj/spaceflow/commit/7a4c458da03ae4a4646abca7e5f03abc849dc405))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 修复 resolveRef 方法未处理空 ref 参数的问题 ([0824c83](https://git.bjxgj.com/xgj/spaceflow/commit/0824c8392482263036888b2fec95935371d67d4d))
|
||||
- **review:** 修复参数空值检查,增强代码健壮性 ([792a192](https://git.bjxgj.com/xgj/spaceflow/commit/792a192fd5dd80ed1e6d85cd61f6ce997bcc9dd9))
|
||||
- **review:** 修复按指定提交过滤时未处理空值导致的潜在问题 ([5d4d3e0](https://git.bjxgj.com/xgj/spaceflow/commit/5d4d3e0390a50c01309bb09e01c7328b211271b8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.12.0 [no ci] ([274216f](https://git.bjxgj.com/xgj/spaceflow/commit/274216fc930dfbf8390d02e25c06efcb44980fed))
|
||||
- **core:** released version 0.13.0 [no ci] ([e3edde3](https://git.bjxgj.com/xgj/spaceflow/commit/e3edde3e670c79544af9a7249d566961740a2284))
|
||||
- **period-summary:** released version 0.12.0 [no ci] ([38490aa](https://git.bjxgj.com/xgj/spaceflow/commit/38490aa75ab20789c5495a5d8d009867f954af4f))
|
||||
- **publish:** released version 0.14.0 [no ci] ([fe0e140](https://git.bjxgj.com/xgj/spaceflow/commit/fe0e14058a364362d7d218da9b34dbb5d8fb8f42))
|
||||
- **review:** released version 0.13.0 [no ci] ([4214c44](https://git.bjxgj.com/xgj/spaceflow/commit/4214c4406ab5482b151ec3c00da376b1d3d50887))
|
||||
- **review:** released version 0.14.0 [no ci] ([4165b05](https://git.bjxgj.com/xgj/spaceflow/commit/4165b05f8aab90d753193f3c1c2800e7f03ea4de))
|
||||
- **review:** released version 0.15.0 [no ci] ([a2ab86d](https://git.bjxgj.com/xgj/spaceflow/commit/a2ab86d097943924749876769f0a144926178783))
|
||||
- **review:** released version 0.16.0 [no ci] ([64c8866](https://git.bjxgj.com/xgj/spaceflow/commit/64c88666fc7e84ced013198d3a53a8c75c7889eb))
|
||||
|
||||
## [0.12.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.11.0...@spaceflow/ci-scripts@0.12.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 CLI 入口文件添加 Node shebang 支持 ([0d787d3](https://git.bjxgj.com/xgj/spaceflow/commit/0d787d329e69f2b53d26ba04720d60625ca51efd))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.11.0 [no ci] ([cf9e486](https://git.bjxgj.com/xgj/spaceflow/commit/cf9e48666197295f118396693abc08b680b3ddee))
|
||||
- **core:** released version 0.12.0 [no ci] ([1ce5034](https://git.bjxgj.com/xgj/spaceflow/commit/1ce50346d73a1914836333415f5ead9fbfa27be7))
|
||||
- **period-summary:** released version 0.11.0 [no ci] ([b518887](https://git.bjxgj.com/xgj/spaceflow/commit/b518887bddd5a452c91148bac64d61ec64b0b509))
|
||||
- **publish:** released version 0.13.0 [no ci] ([1d308d9](https://git.bjxgj.com/xgj/spaceflow/commit/1d308d9e32c50902dd881144ff541204d368006f))
|
||||
- **review:** released version 0.12.0 [no ci] ([3da605e](https://git.bjxgj.com/xgj/spaceflow/commit/3da605ea103192070f1c63112ad896a33fbc4312))
|
||||
|
||||
## [0.11.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.10.0...@spaceflow/ci-scripts@0.11.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit message 的 scope 处理逻辑 ([42869dd](https://git.bjxgj.com/xgj/spaceflow/commit/42869dd4bde0a3c9bf8ffb827182775e2877a57b))
|
||||
- **core:** 重构 commit 服务并添加结构化 commit message 支持 ([22b4db8](https://git.bjxgj.com/xgj/spaceflow/commit/22b4db8619b0ce038667ab42dea1362706887fc9))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.10.0 [no ci] ([53864b8](https://git.bjxgj.com/xgj/spaceflow/commit/53864b8c2534cae265b8fbb98173a5b909682d4e))
|
||||
- **core:** released version 0.11.0 [no ci] ([f0025c7](https://git.bjxgj.com/xgj/spaceflow/commit/f0025c792e332e8b8752597a27f654c0197c36eb))
|
||||
- **period-summary:** released version 0.10.0 [no ci] ([c1ca3bb](https://git.bjxgj.com/xgj/spaceflow/commit/c1ca3bb67fa7f9dbb4de152f0461d644f3044946))
|
||||
- **publish:** released version 0.12.0 [no ci] ([50e209e](https://git.bjxgj.com/xgj/spaceflow/commit/50e209ebc57504462ed192a0fe22f6f944165fa3))
|
||||
- **review:** released version 0.11.0 [no ci] ([150cd9d](https://git.bjxgj.com/xgj/spaceflow/commit/150cd9df7d380c26e6f3f7f0dfd027022f610e6e))
|
||||
|
||||
## [0.10.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.9.0...@spaceflow/ci-scripts@0.10.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 npm 包名处理逻辑 ([ae23ebd](https://git.bjxgj.com/xgj/spaceflow/commit/ae23ebdc3144b611e1aa8c4e66bf0db074d09798))
|
||||
- **core:** 添加依赖更新功能 ([1a544eb](https://git.bjxgj.com/xgj/spaceflow/commit/1a544eb5e2b64396a0187d4518595e9dcb51d73e))
|
||||
- **review:** 支持绝对路径转换为相对路径 ([9050f64](https://git.bjxgj.com/xgj/spaceflow/commit/9050f64b8ef67cb2c8df9663711a209523ae9d18))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.9.0 [no ci] ([accdda7](https://git.bjxgj.com/xgj/spaceflow/commit/accdda7ee4628dc8447e9a89da6c8101c572cb90))
|
||||
- **core:** released version 0.10.0 [no ci] ([a80d34f](https://git.bjxgj.com/xgj/spaceflow/commit/a80d34fb647e107343a07a8793363b3b76320e81))
|
||||
- **period-summary:** released version 0.9.0 [no ci] ([ac03f9b](https://git.bjxgj.com/xgj/spaceflow/commit/ac03f9bcff742d669c6e8b2f94e40153b6c298f5))
|
||||
- **publish:** released version 0.11.0 [no ci] ([df17cd1](https://git.bjxgj.com/xgj/spaceflow/commit/df17cd1250c8fd8a035eb073d292885a4b1e3322))
|
||||
- **review:** released version 0.10.0 [no ci] ([6465de8](https://git.bjxgj.com/xgj/spaceflow/commit/6465de8751028787efb509670988c62b4dbbdf2a))
|
||||
- **review:** released version 0.9.0 [no ci] ([13dd62c](https://git.bjxgj.com/xgj/spaceflow/commit/13dd62c6f307aa6d3b78c34f485393434036fe59))
|
||||
|
||||
## [0.9.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.8.0...@spaceflow/ci-scripts@0.9.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 npm 包添加 npx 直接执行支持 ([e67a7da](https://git.bjxgj.com/xgj/spaceflow/commit/e67a7da34c4e41408760da4de3a499495ce0df2f))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.8.0 [no ci] ([3102178](https://git.bjxgj.com/xgj/spaceflow/commit/310217827c6ec29294dee5689b2dbb1b66492728))
|
||||
- **core:** released version 0.9.0 [no ci] ([8127211](https://git.bjxgj.com/xgj/spaceflow/commit/812721136828e8c38adf0855fb292b0da89daf1a))
|
||||
- **period-summary:** released version 0.8.0 [no ci] ([44ff3c5](https://git.bjxgj.com/xgj/spaceflow/commit/44ff3c505b243e1291565e354e239ce893e5e47c))
|
||||
- **publish:** released version 0.10.0 [no ci] ([8722ba9](https://git.bjxgj.com/xgj/spaceflow/commit/8722ba9eddb03c2f73539f4e09c504ed9491a5eb))
|
||||
- **review:** released version 0.8.0 [no ci] ([ec6e7e5](https://git.bjxgj.com/xgj/spaceflow/commit/ec6e7e5defd2a5a6349d3530f3b0f4732dd5bb62))
|
||||
|
||||
## [0.8.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.7.0...@spaceflow/ci-scripts@0.8.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit 消息生成器中的 scope 处理逻辑 ([1592079](https://git.bjxgj.com/xgj/spaceflow/commit/1592079edde659fe94a02bb6e2dea555c80d3b6b))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.7.0 [no ci] ([247967b](https://git.bjxgj.com/xgj/spaceflow/commit/247967b30876aae78cfb1f9c706431b5bb9fb57e))
|
||||
- **core:** released version 0.8.0 [no ci] ([625dbc0](https://git.bjxgj.com/xgj/spaceflow/commit/625dbc0206747b21a893ae43032f55d0a068c6fd))
|
||||
- **period-summary:** released version 0.7.0 [no ci] ([8869d58](https://git.bjxgj.com/xgj/spaceflow/commit/8869d5876e86ebe83ae65c3259cd9a7e402257cf))
|
||||
- **publish:** released version 0.9.0 [no ci] ([b404930](https://git.bjxgj.com/xgj/spaceflow/commit/b40493049877c1fd3554d77a14e9bd9ab318e15a))
|
||||
- **review:** released version 0.7.0 [no ci] ([1d195d7](https://git.bjxgj.com/xgj/spaceflow/commit/1d195d74685f12edf3b1f4e13b58ccc3d221fd94))
|
||||
|
||||
## [0.7.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.6.0...@spaceflow/ci-scripts@0.7.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 重构安装服务目录结构和命名 ([50cc900](https://git.bjxgj.com/xgj/spaceflow/commit/50cc900eb864b23f20c5f48dec20d1a754238286))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.6.0 [no ci] ([a2d1239](https://git.bjxgj.com/xgj/spaceflow/commit/a2d12397997b309062a9d93af57a5588cdb82a79))
|
||||
- **core:** released version 0.7.0 [no ci] ([000c53e](https://git.bjxgj.com/xgj/spaceflow/commit/000c53eff80899dbadad8d668a2227921373daad))
|
||||
- **period-summary:** released version 0.6.0 [no ci] ([6648dfb](https://git.bjxgj.com/xgj/spaceflow/commit/6648dfb31b459e8c4522cff342dfa87a4bdaab4b))
|
||||
- **publish:** released version 0.8.0 [no ci] ([d7cd2e9](https://git.bjxgj.com/xgj/spaceflow/commit/d7cd2e9a7af178acdf91f16ae299c82e915db6e6))
|
||||
- **review:** released version 0.6.0 [no ci] ([48a90b2](https://git.bjxgj.com/xgj/spaceflow/commit/48a90b253dbe03f46d26bb88f3e0158193aa1dba))
|
||||
|
||||
## [0.6.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.5.0...@spaceflow/ci-scripts@0.6.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化pnpm包安装逻辑,检测是否为workspace ([6555daf](https://git.bjxgj.com/xgj/spaceflow/commit/6555dafe1f08a244525be3a0345cc585f2552086))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.5.0 [no ci] ([920d9a8](https://git.bjxgj.com/xgj/spaceflow/commit/920d9a8165fe6eabf7a074eb65762f4693883438))
|
||||
- **core:** released version 0.6.0 [no ci] ([21e1ec6](https://git.bjxgj.com/xgj/spaceflow/commit/21e1ec61a2de542e065034f32a259092dd7c0e0d))
|
||||
- **period-summary:** released version 0.5.0 [no ci] ([8e547e9](https://git.bjxgj.com/xgj/spaceflow/commit/8e547e9e6a6496a8c314c06cf6e88c97e623bc2d))
|
||||
- **publish:** released version 0.7.0 [no ci] ([7124435](https://git.bjxgj.com/xgj/spaceflow/commit/712443516845f5bbc097af16ec6e90bb57b69fa3))
|
||||
- **review:** released version 0.5.0 [no ci] ([93c3088](https://git.bjxgj.com/xgj/spaceflow/commit/93c308887040f39047766a789a37d24ac6146359))
|
||||
|
||||
## [0.5.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.4.0...@spaceflow/ci-scripts@0.5.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化包管理器检测与 npm 包处理逻辑 ([63f7fa4](https://git.bjxgj.com/xgj/spaceflow/commit/63f7fa4f55cb41583009b2ea313b5ad327615e52))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 优化配置合并逻辑,添加字段覆盖策略 ([18680e6](https://git.bjxgj.com/xgj/spaceflow/commit/18680e69b0d6e9e05c843ed3f07766830955d658))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.4.0 [no ci] ([7e6bf1d](https://git.bjxgj.com/xgj/spaceflow/commit/7e6bf1dabffc6250b918b89bb850d478d3f4b875))
|
||||
- **core:** released version 0.5.0 [no ci] ([ad20098](https://git.bjxgj.com/xgj/spaceflow/commit/ad20098ef954283dd6d9867a4d2535769cbb8377))
|
||||
- **period-summary:** released version 0.4.0 [no ci] ([ca89a9b](https://git.bjxgj.com/xgj/spaceflow/commit/ca89a9b9436761e210dedfc38fb3c16ef39b0718))
|
||||
- **publish:** released version 0.6.0 [no ci] ([b6d8d09](https://git.bjxgj.com/xgj/spaceflow/commit/b6d8d099fc439ce67f802d56e30dadaa28afed0e))
|
||||
- **review:** released version 0.4.0 [no ci] ([3b5f8a9](https://git.bjxgj.com/xgj/spaceflow/commit/3b5f8a934de5ba4f59e232e1dcbccbdff1b8b17c))
|
||||
- 更新项目依赖锁定文件 ([19d2d1d](https://git.bjxgj.com/xgj/spaceflow/commit/19d2d1d86bb35b8ee5d3ad20be51b7aa68e83eff))
|
||||
- 移除 npm registry 配置文件 ([2d9fac6](https://git.bjxgj.com/xgj/spaceflow/commit/2d9fac6db79e81a09ca8e031190d0ebb2781cc31))
|
||||
- 调整依赖配置并添加npm registry配置 ([a754db1](https://git.bjxgj.com/xgj/spaceflow/commit/a754db1bad1bafcea50b8d2825aaf19457778f2e))
|
||||
|
||||
## [0.4.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.3.0...@spaceflow/ci-scripts@0.4.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整zod依赖的导入来源 ([574eef1](https://git.bjxgj.com/xgj/spaceflow/commit/574eef1910809a72a4b13acd4cb070e12dc42ce8))
|
||||
- **review:** 调整zod依赖的导入路径 ([02014cd](https://git.bjxgj.com/xgj/spaceflow/commit/02014cdab9829df583f0f621150573b8c45a3ad7))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.3.0 [no ci] ([7b25e55](https://git.bjxgj.com/xgj/spaceflow/commit/7b25e557b628fdfa66d7a0be4ee21267906ac15f))
|
||||
- **core:** released version 0.4.0 [no ci] ([bc4cd89](https://git.bjxgj.com/xgj/spaceflow/commit/bc4cd89af70dce052e7e00fe413708790f65be61))
|
||||
- **core:** 调整核心依赖与配置,新增Zod类型系统支持 ([def0751](https://git.bjxgj.com/xgj/spaceflow/commit/def0751577d9f3350494ca3c7bb4a4b087dab05e))
|
||||
- **period-summary:** released version 0.3.0 [no ci] ([7e74c59](https://git.bjxgj.com/xgj/spaceflow/commit/7e74c59d90d88e061e693829f8196834d9858307))
|
||||
- **publish:** released version 0.5.0 [no ci] ([8eecd19](https://git.bjxgj.com/xgj/spaceflow/commit/8eecd19c4dd3fbaa27187a9b24234e753fff5efe))
|
||||
- **review:** released version 0.3.0 [no ci] ([865c6fd](https://git.bjxgj.com/xgj/spaceflow/commit/865c6fdee167df187d1bc107867f842fe25c1098))
|
||||
- 调整项目依赖配置 ([6802386](https://git.bjxgj.com/xgj/spaceflow/commit/6802386f38f4081a3b5d5c47ddc49e9ec2e4f28d))
|
||||
|
||||
## [0.3.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.2.0...@spaceflow/ci-scripts@0.3.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整包变更检测的日志输出格式 ([df35e92](https://git.bjxgj.com/xgj/spaceflow/commit/df35e92d614ce59e202643cf34a0fef2803412a1))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.2.0 [no ci] ([4f5314b](https://git.bjxgj.com/xgj/spaceflow/commit/4f5314b1002b90d7775a5ef51e618a3f227ae5a9))
|
||||
- **core:** released version 0.3.0 [no ci] ([bf8b005](https://git.bjxgj.com/xgj/spaceflow/commit/bf8b005ccbfcdd2061c18ae4ecdd476584ecbb53))
|
||||
- **core:** 调整依赖配置 ([c86534a](https://git.bjxgj.com/xgj/spaceflow/commit/c86534ad213293ee2557ba5568549e8fbcb74ba5))
|
||||
- **period-summary:** released version 0.2.0 [no ci] ([66a4e20](https://git.bjxgj.com/xgj/spaceflow/commit/66a4e209519b64d946ec21b1d1695105fb9de106))
|
||||
- **publish:** released version 0.3.0 [no ci] ([972eca4](https://git.bjxgj.com/xgj/spaceflow/commit/972eca440dd333e8c5380124497c16fe6e3eea6c))
|
||||
- **publish:** released version 0.4.0 [no ci] ([be66220](https://git.bjxgj.com/xgj/spaceflow/commit/be662202c1e9e509368eb683a0d6df3afd876ff8))
|
||||
- **review:** released version 0.2.0 [no ci] ([d0bd3ed](https://git.bjxgj.com/xgj/spaceflow/commit/d0bd3edf364dedc7c077d95801b402d41c3fdd9c))
|
||||
- 调整项目依赖配置 ([f4009cb](https://git.bjxgj.com/xgj/spaceflow/commit/f4009cb0c369b225c356584afb28a7ff5a1a89ec))
|
||||
|
||||
## [0.2.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.1.2...@spaceflow/ci-scripts@0.2.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **publish:** 增强包变更检测的日志输出 ([b89c5cc](https://git.bjxgj.com/xgj/spaceflow/commit/b89c5cc0654713b6482ee591325d4f92ad773600))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复分支锁定时未捕获异常处理器的资源泄漏问题 ([ae326e9](https://git.bjxgj.com/xgj/spaceflow/commit/ae326e95c0cea033893cf084cbf7413fb252bd33))
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **core:** 更新核心框架README文档 ([0d98658](https://git.bjxgj.com/xgj/spaceflow/commit/0d98658f6ab01f119f98d3387fb5651d4d4351a8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-shell:** released version 0.1.2 [no ci] ([bf7977b](https://git.bjxgj.com/xgj/spaceflow/commit/bf7977bed684b557555861b9dc0359eda3c5d476))
|
||||
- **core:** released version 0.2.0 [no ci] ([5a96529](https://git.bjxgj.com/xgj/spaceflow/commit/5a96529cabdce4fb150732b34c55e668c33cb50c))
|
||||
- **period-summary:** released version 0.1.2 [no ci] ([eaf41a0](https://git.bjxgj.com/xgj/spaceflow/commit/eaf41a0149ee4306361ccab0b3878bded79677df))
|
||||
- **publish:** released version 0.1.2 [no ci] ([4786731](https://git.bjxgj.com/xgj/spaceflow/commit/4786731da7a21982dc1e912b1a5002f5ebba9104))
|
||||
- **publish:** released version 0.2.0 [no ci] ([bc30a82](https://git.bjxgj.com/xgj/spaceflow/commit/bc30a82082bba4cc1a66c74c11dc0ad9deef4692))
|
||||
- **review:** released version 0.1.2 [no ci] ([9689d3e](https://git.bjxgj.com/xgj/spaceflow/commit/9689d3e37781ca9ae6cb14d7b12717c061f2919d))
|
||||
- 优化CI工作流的代码检出配置 ([d9740dd](https://git.bjxgj.com/xgj/spaceflow/commit/d9740dd6d1294068ffdcd7a12b61149773866a2a))
|
||||
|
||||
## [0.1.2](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.1.1...@spaceflow/ci-scripts@0.1.2) (2026-01-28)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复预演模式下的交互式提示问题 ([0b785bf](https://git.bjxgj.com/xgj/spaceflow/commit/0b785bfddb9f35e844989bd3891817dc502302f8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **core:** released version 0.1.2 [no ci] ([8292dbe](https://git.bjxgj.com/xgj/spaceflow/commit/8292dbe59a200cc640a95b86afb6451ec0c044ce))
|
||||
|
||||
## [0.1.1](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.1.0...@spaceflow/ci-scripts@0.1.1) (2026-01-28)
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **publish:** 完善发布插件README文档 ([faa57b0](https://git.bjxgj.com/xgj/spaceflow/commit/faa57b095453c00fb3c9a7704bc31b63953c0ac5))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **core:** released version 0.1.1 [no ci] ([0cf3a4d](https://git.bjxgj.com/xgj/spaceflow/commit/0cf3a4d37d7d1460e232dd30bc7ab8dc015ed11f))
|
||||
|
||||
## [0.1.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-scripts@0.0.1...@spaceflow/ci-scripts@0.1.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 添加同步解锁分支方法用于进程退出清理 ([cbec480](https://git.bjxgj.com/xgj/spaceflow/commit/cbec480511e074de3ccdc61226f3baa317cff907))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **core:** released version 0.1.0 [no ci] ([f455607](https://git.bjxgj.com/xgj/spaceflow/commit/f45560735082840410e08e0d8113f366732a1243))
|
||||
|
||||
## 0.0.1 (2026-01-28)
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **core:** released version 0.0.1 [no ci] ([66497d6](https://git.bjxgj.com/xgj/spaceflow/commit/66497d60be04b4756a3362dbec4652177910165c))
|
||||
- 重置所有包版本至 0.0.0 并清理 CHANGELOG 文件 ([f7efaf9](https://git.bjxgj.com/xgj/spaceflow/commit/f7efaf967467f1272e05d645720ee63941fe79be))
|
||||
3
commands/ci-scripts/README.md
Normal file
3
commands/ci-scripts/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# ci-scripts
|
||||
|
||||
在分支锁定/解锁之间执行 JS 语句的 CI 命令。
|
||||
25
commands/ci-scripts/package.json
Normal file
25
commands/ci-scripts/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@spaceflow/ci-scripts",
|
||||
"version": "0.19.0",
|
||||
"description": "Spaceflow CI 脚本插件,用于在分支锁定/解锁之间执行 JS 语句",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"build": "spaceflow build",
|
||||
"dev": "spaceflow dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spaceflow/cli": "workspace:*",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "catalog:",
|
||||
"@nestjs/config": "catalog:",
|
||||
"@spaceflow/core": "workspace:*",
|
||||
"nest-commander": "catalog:"
|
||||
},
|
||||
"spaceflow": {
|
||||
"type": "flow",
|
||||
"entry": "."
|
||||
}
|
||||
}
|
||||
50
commands/ci-scripts/src/ci-scripts.command.ts
Normal file
50
commands/ci-scripts/src/ci-scripts.command.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Command, CommandRunner, Option } from "nest-commander";
|
||||
import { t } from "@spaceflow/core";
|
||||
import { CiScriptsService } from "./ci-scripts.service";
|
||||
|
||||
export interface CiScriptsOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: "ci-script",
|
||||
description: t("ci-scripts:description"),
|
||||
arguments: "<script>",
|
||||
argsDescription: {
|
||||
script: t("ci-scripts:argsDescription.script"),
|
||||
},
|
||||
})
|
||||
export class CiScriptsCommand extends CommandRunner {
|
||||
constructor(protected readonly ciScriptsService: CiScriptsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(passedParams: string[], options: CiScriptsOptions): Promise<void> {
|
||||
const script = passedParams.join(" ");
|
||||
|
||||
if (!script) {
|
||||
console.error(t("ci-scripts:noScript"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`DRY-RUN mode: ${options.dryRun ? "enabled" : "disabled"}`);
|
||||
|
||||
try {
|
||||
const context = this.ciScriptsService.getContextFromEnv(options);
|
||||
await this.ciScriptsService.execute(context, script);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
t("common.executionFailed", { error: error instanceof Error ? error.message : error }),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-d, --dry-run",
|
||||
description: t("common.options.dryRun"),
|
||||
})
|
||||
parseDryRun(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
11
commands/ci-scripts/src/ci-scripts.module.ts
Normal file
11
commands/ci-scripts/src/ci-scripts.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { GitProviderModule, ciConfig } from "@spaceflow/core";
|
||||
import { CiScriptsCommand } from "./ci-scripts.command";
|
||||
import { CiScriptsService } from "./ci-scripts.service";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forFeature(ciConfig), GitProviderModule.forFeature()],
|
||||
providers: [CiScriptsCommand, CiScriptsService],
|
||||
})
|
||||
export class CiScriptsModule {}
|
||||
154
commands/ci-scripts/src/ci-scripts.service.ts
Normal file
154
commands/ci-scripts/src/ci-scripts.service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { GitProviderService, BranchProtection, CiConfig } from "@spaceflow/core";
|
||||
import { CiScriptsOptions } from "./ci-scripts.command";
|
||||
|
||||
// 定义常量表示进程退出码
|
||||
const PROCESS_EXIT_CODE_ERROR = 1;
|
||||
export interface CiScriptsContext extends CiScriptsOptions {
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch: string;
|
||||
}
|
||||
|
||||
export interface CiScriptsResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
protection?: BranchProtection | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CiScriptsService {
|
||||
constructor(
|
||||
protected readonly gitProvider: GitProviderService,
|
||||
protected readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
getContextFromEnv(options: CiScriptsOptions): CiScriptsContext {
|
||||
this.gitProvider.validateConfig();
|
||||
|
||||
const ciConf = this.configService.get<CiConfig>("ci");
|
||||
const repository = ciConf?.repository;
|
||||
const branch = ciConf?.refName;
|
||||
|
||||
if (!repository) {
|
||||
throw new Error("缺少配置 ci.repository (环境变量 GITHUB_REPOSITORY)");
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
throw new Error("缺少配置 ci.refName (环境变量 GITHUB_REF_NAME)");
|
||||
}
|
||||
|
||||
const [owner, repo] = repository.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`ci.repository 格式不正确,期望 "owner/repo",实际: "${repository}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(context: CiScriptsContext, script: string): Promise<void> {
|
||||
try {
|
||||
// 1. 锁定分支
|
||||
await this.handleBegin(context);
|
||||
|
||||
try {
|
||||
// 2. 执行脚本
|
||||
console.log(`🏃 正在执行脚本...`);
|
||||
console.log(`> ${script}`);
|
||||
|
||||
// 包装在 async IIFE 中以支持 await
|
||||
const asyncScript = `(async () => { ${script} })()`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-eval
|
||||
const result = eval(asyncScript);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
|
||||
console.log("✅ 脚本执行成功");
|
||||
} catch (error) {
|
||||
console.error("❌ 脚本执行失败:", error);
|
||||
// 出错时也要尝试解锁
|
||||
await this.handleEnd(context);
|
||||
process.exit(PROCESS_EXIT_CODE_ERROR);
|
||||
}
|
||||
|
||||
// 3. 解锁分支
|
||||
await this.handleEnd(context);
|
||||
} catch (error) {
|
||||
console.error("执行失败:", error instanceof Error ? error.message : error);
|
||||
process.exit(PROCESS_EXIT_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleBegin(context: CiScriptsContext): Promise<CiScriptsResult> {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将锁定分支: ${owner}/${repo}#${branch}`);
|
||||
return {
|
||||
success: true,
|
||||
message: "DRY-RUN: 分支锁定已跳过",
|
||||
protection: null,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`🔒 正在锁定分支: ${owner}/${repo}#${branch}`);
|
||||
|
||||
const protection = await this.gitProvider.lockBranch(owner, repo, branch);
|
||||
|
||||
console.log(`✅ 分支已锁定`);
|
||||
console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
|
||||
console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "分支锁定完成",
|
||||
protection,
|
||||
};
|
||||
}
|
||||
|
||||
protected async handleEnd(context: CiScriptsContext): Promise<CiScriptsResult> {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将解锁分支: ${owner}/${repo}#${branch}`);
|
||||
return {
|
||||
success: true,
|
||||
message: "DRY-RUN: 分支解锁已跳过",
|
||||
protection: null,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`🔓 正在解锁分支: ${owner}/${repo}#${branch}`);
|
||||
|
||||
const protection = await this.gitProvider.unlockBranch(owner, repo, branch);
|
||||
|
||||
if (protection) {
|
||||
console.log(`✅ 分支已解锁`);
|
||||
console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
|
||||
console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "分支解锁完成",
|
||||
protection,
|
||||
};
|
||||
} else {
|
||||
console.log(`✅ 分支本身没有保护规则,无需解锁`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "分支本身没有保护规则,无需解锁",
|
||||
protection: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
commands/ci-scripts/src/index.ts
Normal file
24
commands/ci-scripts/src/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import "./locales";
|
||||
import { SpaceflowExtension, SpaceflowExtensionMetadata, t } from "@spaceflow/core";
|
||||
import { CiScriptsModule } from "./ci-scripts.module";
|
||||
export class CiScriptsExtension implements SpaceflowExtension {
|
||||
getMetadata(): SpaceflowExtensionMetadata {
|
||||
return {
|
||||
name: "ci-scripts",
|
||||
commands: ["ci-script"],
|
||||
configKey: "ci-scripts",
|
||||
version: "1.0.0",
|
||||
description: t("ci-scripts:extensionDescription"),
|
||||
};
|
||||
}
|
||||
|
||||
getModule() {
|
||||
return CiScriptsModule;
|
||||
}
|
||||
}
|
||||
|
||||
export default CiScriptsExtension;
|
||||
|
||||
export * from "./ci-scripts.command";
|
||||
export * from "./ci-scripts.service";
|
||||
export * from "./ci-scripts.module";
|
||||
6
commands/ci-scripts/src/locales/en/ci-scripts.json
Normal file
6
commands/ci-scripts/src/locales/en/ci-scripts.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"description": "Execute JS statements between branch lock/unlock",
|
||||
"argsDescription.script": "JS statement to execute",
|
||||
"noScript": "❌ Please provide a JS statement to execute",
|
||||
"extensionDescription": "CI script command for executing JS statements between branch lock/unlock"
|
||||
}
|
||||
11
commands/ci-scripts/src/locales/index.ts
Normal file
11
commands/ci-scripts/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { addLocaleResources } from "@spaceflow/core";
|
||||
import zhCN from "./zh-cn/ci-scripts.json";
|
||||
import en from "./en/ci-scripts.json";
|
||||
|
||||
/** ci-scripts 命令 i18n 资源 */
|
||||
export const ciScriptsLocales: Record<string, Record<string, string>> = {
|
||||
"zh-CN": zhCN,
|
||||
en,
|
||||
};
|
||||
|
||||
addLocaleResources("ci-scripts", ciScriptsLocales);
|
||||
6
commands/ci-scripts/src/locales/zh-cn/ci-scripts.json
Normal file
6
commands/ci-scripts/src/locales/zh-cn/ci-scripts.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"description": "在分支锁定/解锁之间执行 JS 语句",
|
||||
"argsDescription.script": "要执行的 JS 语句",
|
||||
"noScript": "❌ 请提供要执行的 JS 语句",
|
||||
"extensionDescription": "CI 脚本命令,用于在分支锁定/解锁之间执行 JS 语句"
|
||||
}
|
||||
5
commands/ci-scripts/tsconfig.json
Normal file
5
commands/ci-scripts/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../core/tsconfig.skill.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
487
commands/ci-shell/CHANGELOG.md
Normal file
487
commands/ci-shell/CHANGELOG.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# Changelog
|
||||
|
||||
## [0.19.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.18.0...@spaceflow/ci-shell@0.19.0) (2026-02-15)
|
||||
|
||||
### 新特性
|
||||
|
||||
* **cli:** 新增 MCP Server 命令并集成 review 扩展的 MCP 工具 ([b794b36](https://git.bjxgj.com/xgj/spaceflow/commit/b794b36d90788c7eb4cbb253397413b4a080ae83))
|
||||
* **cli:** 新增 MCP Server 导出类型支持 ([9568cbd](https://git.bjxgj.com/xgj/spaceflow/commit/9568cbd14d4cfbdedaf2218379c72337af6db271))
|
||||
* **core:** 为所有命令添加 i18n 国际化支持 ([867c5d3](https://git.bjxgj.com/xgj/spaceflow/commit/867c5d3eccc285c8a68803b8aa2f0ffb86a94285))
|
||||
* **core:** 新增 GitLab 平台适配器并完善配置支持 ([47be9ad](https://git.bjxgj.com/xgj/spaceflow/commit/47be9adfa90944a9cb183e03286a7a96fec747f1))
|
||||
* **core:** 新增 Logger 全局日志工具并支持 plain/tui 双模式渲染 ([8baae7c](https://git.bjxgj.com/xgj/spaceflow/commit/8baae7c24139695a0e379e1c874023cd61dfc41b))
|
||||
* **docs:** 新增 VitePress 文档站点并完善项目文档 ([a79d620](https://git.bjxgj.com/xgj/spaceflow/commit/a79d6208e60390a44fa4c94621eb41ae20159e98))
|
||||
* **mcp:** 新增 MCP Inspector 交互式调试支持并优化工具日志输出 ([05fd2ee](https://git.bjxgj.com/xgj/spaceflow/commit/05fd2ee941c5f6088b769d1127cb7c0615626f8c))
|
||||
* **review:** 为 MCP 服务添加 i18n 国际化支持 ([a749054](https://git.bjxgj.com/xgj/spaceflow/commit/a749054eb73b775a5f5973ab1b86c04f2b2ddfba))
|
||||
* **review:** 新增规则级 includes 解析测试并修复文件级/规则级 includes 过滤逻辑 ([4baca71](https://git.bjxgj.com/xgj/spaceflow/commit/4baca71c17782fb92a95b3207f9c61e0b410b9ff))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
* **actions:** 修正 pnpm setup 命令调用方式 ([8f014fa](https://git.bjxgj.com/xgj/spaceflow/commit/8f014fa90b74e20de4c353804d271b3ef6f1288f))
|
||||
* **mcp:** 添加 -y 选项确保 Inspector 自动安装依赖 ([a9201f7](https://git.bjxgj.com/xgj/spaceflow/commit/a9201f74bd9ddc5eba92beaaa676f377842863e0))
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **claude:** 移除 .claude 目录及其 .gitignore 配置文件 ([91916a9](https://git.bjxgj.com/xgj/spaceflow/commit/91916a99f65da31c1d34e6f75b5cbea1d331ba35))
|
||||
* **cli:** 优化依赖安装流程并支持 .spaceflow 目录配置 ([5977631](https://git.bjxgj.com/xgj/spaceflow/commit/597763183eaa61bb024bba2703d75239650b54fb))
|
||||
* **cli:** 拆分 CLI 为独立包并重构扩展加载机制 ([b385d28](https://git.bjxgj.com/xgj/spaceflow/commit/b385d281575f29b823bb6dc4229a396a29c0e226))
|
||||
* **cli:** 移除 ExtensionModule 并优化扩展加载机制 ([8f7077d](https://git.bjxgj.com/xgj/spaceflow/commit/8f7077deaef4e5f4032662ff5ac925cd3c07fdb6))
|
||||
* **cli:** 调整依赖顺序并格式化导入语句 ([32a9c1c](https://git.bjxgj.com/xgj/spaceflow/commit/32a9c1cf834725a20f93b1f8f60b52692841a3e5))
|
||||
* **cli:** 重构 getPluginConfigFromPackageJson 方法以提高代码可读性 ([f5f6ed9](https://git.bjxgj.com/xgj/spaceflow/commit/f5f6ed9858cc4ca670e30fac469774bdc8f7b005))
|
||||
* **cli:** 重构扩展配置格式,支持 flow/command/skill 三种导出类型 ([958dc13](https://git.bjxgj.com/xgj/spaceflow/commit/958dc130621f78bbcc260224da16a5f16ae0b2b1))
|
||||
* **core:** 为 build/clear/commit 命令添加国际化支持 ([de82cb2](https://git.bjxgj.com/xgj/spaceflow/commit/de82cb2f1ed8cef0e446a2d42a1bf1f091e9c421))
|
||||
* **core:** 优化 list 命令输出格式并修复 MCP Inspector 包管理器兼容性 ([a019829](https://git.bjxgj.com/xgj/spaceflow/commit/a019829d3055c083aeb86ed60ce6629d13012d91))
|
||||
* **core:** 将 rspack 配置和工具函数中的 @spaceflow/cli 引用改为 @spaceflow/core ([3c301c6](https://git.bjxgj.com/xgj/spaceflow/commit/3c301c60f3e61b127db94481f5a19307f5ef00eb))
|
||||
* **core:** 将扩展依赖从 @spaceflow/cli 迁移到 @spaceflow/core ([6f9ffd4](https://git.bjxgj.com/xgj/spaceflow/commit/6f9ffd4061cecae4faaf3d051e3ca98a0b42b01f))
|
||||
* **core:** 提取 source 处理和包管理器工具函数到共享模块 ([ab3ff00](https://git.bjxgj.com/xgj/spaceflow/commit/ab3ff003d1cd586c0c4efc7841e6a93fe3477ace))
|
||||
* **core:** 新增 getEnvFilePaths 工具函数统一管理 .env 文件路径优先级 ([809fa18](https://git.bjxgj.com/xgj/spaceflow/commit/809fa18f3d0b8eabcb068988bab53d548eaf03ea))
|
||||
* **core:** 新增远程仓库规则拉取功能并支持 Git API 获取目录内容 ([69ade16](https://git.bjxgj.com/xgj/spaceflow/commit/69ade16c9069f9e1a90b3ef56dc834e33a3c0650))
|
||||
* **core:** 统一 LogLevel 类型定义并支持字符串/数字双模式 ([557f6b0](https://git.bjxgj.com/xgj/spaceflow/commit/557f6b0bc39fcfb0e3f773836cbbf08c1a8790ae))
|
||||
* **core:** 重构配置读取逻辑,新增 ConfigReaderService 并支持 .spaceflowrc 配置文件 ([72e88ce](https://git.bjxgj.com/xgj/spaceflow/commit/72e88ced63d03395923cdfb113addf4945162e54))
|
||||
* **i18n:** 将 locales 导入从命令文件迁移至扩展入口文件 ([0da5d98](https://git.bjxgj.com/xgj/spaceflow/commit/0da5d9886296c4183b24ad8c56140763f5a870a4))
|
||||
* **i18n:** 移除扩展元数据中的 locales 字段并改用 side-effect 自动注册 ([2c7d488](https://git.bjxgj.com/xgj/spaceflow/commit/2c7d488a9dfa59a99b95e40e3c449c28c2d433d8))
|
||||
* **mcp:** 使用 DTO + Swagger 装饰器替代手动 JSON Schema 定义 ([87ec262](https://git.bjxgj.com/xgj/spaceflow/commit/87ec26252dd295536bb090ae8b7e418eec96e1bd))
|
||||
* **mcp:** 升级 MCP SDK API 并优化 Inspector 调试配置 ([176d04a](https://git.bjxgj.com/xgj/spaceflow/commit/176d04a73fbbb8d115520d922f5fedb9a2961aa6))
|
||||
* **mcp:** 将 MCP 元数据存储从 Reflect Metadata 改为静态属性以支持跨模块访问 ([cac0ea2](https://git.bjxgj.com/xgj/spaceflow/commit/cac0ea2029e1b504bc4278ce72b3aa87fff88c84))
|
||||
* **test:** 迁移测试框架从 Jest 到 Vitest ([308f9d4](https://git.bjxgj.com/xgj/spaceflow/commit/308f9d49089019530588344a5e8880f5b6504a6a))
|
||||
* 优化构建流程并调整 MCP/review 日志输出级别 ([74072c0](https://git.bjxgj.com/xgj/spaceflow/commit/74072c04be7a45bfc0ab53b636248fe5c0e1e42a))
|
||||
* 将 .spaceflow/package.json 纳入版本控制并自动添加到根项目依赖 ([ab83d25](https://git.bjxgj.com/xgj/spaceflow/commit/ab83d2579cb5414ee3d78a9768fac2147a3d1ad9))
|
||||
* 将 GiteaSdkModule/GiteaSdkService 重命名为 GitProviderModule/GitProviderService ([462f492](https://git.bjxgj.com/xgj/spaceflow/commit/462f492bc2607cf508c5011d181c599cf17e00c9))
|
||||
* 恢复 pnpm catalog 配置并移除 .spaceflow 工作区导入器 ([217387e](https://git.bjxgj.com/xgj/spaceflow/commit/217387e2e8517a08162e9bcaf604893fd9bca736))
|
||||
* 迁移扩展依赖到 .spaceflow 工作区并移除 pnpm catalog ([c457c0f](https://git.bjxgj.com/xgj/spaceflow/commit/c457c0f8918171f1856b88bc007921d76c508335))
|
||||
* 重构 Extension 安装机制为 pnpm workspace 模式 ([469b12e](https://git.bjxgj.com/xgj/spaceflow/commit/469b12eac28f747b628e52a5125a3d5a538fba39))
|
||||
* 重构插件加载改为扩展模式 ([0e6e140](https://git.bjxgj.com/xgj/spaceflow/commit/0e6e140b19ea2cf6084afc261c555d2083fe04f9))
|
||||
|
||||
### 文档更新
|
||||
|
||||
* **guide:** 更新编辑器集成文档,补充四种导出类型说明和 MCP 注册机制 ([19a7409](https://git.bjxgj.com/xgj/spaceflow/commit/19a7409092c89d002f11ee51ebcb6863118429bd))
|
||||
* **guide:** 更新配置文件位置说明并补充 RC 文件支持 ([2214dc4](https://git.bjxgj.com/xgj/spaceflow/commit/2214dc4e197221971f5286b38ceaa6fcbcaa7884))
|
||||
|
||||
### 测试用例
|
||||
|
||||
* **core:** 新增 GiteaAdapter 完整单元测试并实现自动检测 provider 配置 ([c74f745](https://git.bjxgj.com/xgj/spaceflow/commit/c74f7458aed91ac7d12fb57ef1c24b3d2917c406))
|
||||
* **review:** 新增 DeletionImpactService 测试覆盖并配置 coverage 工具 ([50bfbfe](https://git.bjxgj.com/xgj/spaceflow/commit/50bfbfe37192641f1170ade8f5eb00e0e382af67))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.19.0 [no ci] ([9f747c6](https://git.bjxgj.com/xgj/spaceflow/commit/9f747c617b387e105e92b4a5dcd0f5d3cf51c26d))
|
||||
* **ci:** 迁移工作流从 Gitea 到 GitHub 并统一环境变量命名 ([57e3bae](https://git.bjxgj.com/xgj/spaceflow/commit/57e3bae635b324c8c4ea50a9fb667b6241fae0ef))
|
||||
* **cli:** released version 0.19.0 [no ci] ([6b63149](https://git.bjxgj.com/xgj/spaceflow/commit/6b631499e2407a1822395d5f40cec2d725331b78))
|
||||
* **config:** 将 git 推送白名单用户从 "Gitea Actions" 改为 "GiteaActions" ([fdbb865](https://git.bjxgj.com/xgj/spaceflow/commit/fdbb865341e6f02b26fca32b54a33b51bee11cad))
|
||||
* **config:** 将 git 推送白名单用户从 github-actions[bot] 改为 Gitea Actions ([9c39819](https://git.bjxgj.com/xgj/spaceflow/commit/9c39819a9f95f415068f7f0333770b92bc98321b))
|
||||
* **config:** 移除 review-spec 私有仓库依赖 ([8ae18f1](https://git.bjxgj.com/xgj/spaceflow/commit/8ae18f13c441b033d1cbc75119695a5cc5cb6a0b))
|
||||
* **core:** released version 0.1.0 [no ci] ([170fa67](https://git.bjxgj.com/xgj/spaceflow/commit/170fa670e98473c2377120656d23aae835c51997))
|
||||
* **core:** 禁用 i18next 初始化时的 locize.com 推广日志 ([a99fbb0](https://git.bjxgj.com/xgj/spaceflow/commit/a99fbb068441bc623efcf15a1dd7b6bd38c05f38))
|
||||
* **deps:** 移除 pnpm catalog 配置并更新依赖锁定 ([753fb9e](https://git.bjxgj.com/xgj/spaceflow/commit/753fb9e3e43b28054c75158193dc39ab4bab1af5))
|
||||
* **docs:** 统一文档脚本命名,为 VitePress 命令添加 docs: 前缀 ([3cc46ea](https://git.bjxgj.com/xgj/spaceflow/commit/3cc46eab3a600290f5064b8270902e586b9c5af4))
|
||||
* **i18n:** 配置 i18n-ally-next 自动提取键名生成策略 ([753c3dc](https://git.bjxgj.com/xgj/spaceflow/commit/753c3dc3f24f3c03c837d1ec2c505e8e3ce08b11))
|
||||
* **i18n:** 重构 i18n 配置并统一 locales 目录结构 ([3e94037](https://git.bjxgj.com/xgj/spaceflow/commit/3e94037fa6493b3b0e4a12ff6af9f4bea48ae217))
|
||||
* **period-summary:** released version 0.18.0 [no ci] ([f0df638](https://git.bjxgj.com/xgj/spaceflow/commit/f0df63804d06f8c75e04169ec98226d7a4f5d7f9))
|
||||
* **publish:** released version 0.20.0 [no ci] ([d347e3b](https://git.bjxgj.com/xgj/spaceflow/commit/d347e3b2041157d8dc6e3ade69b05a481b2ab371))
|
||||
* **review:** released version 0.28.0 [no ci] ([a2d89ed](https://git.bjxgj.com/xgj/spaceflow/commit/a2d89ed5f386eb6dd299c0d0a208856ce267ab5e))
|
||||
* **scripts:** 修正 setup 和 build 脚本的过滤条件,避免重复构建 cli 包 ([ffd2ffe](https://git.bjxgj.com/xgj/spaceflow/commit/ffd2ffedca08fd56cccb6a9fbd2b6bd106e367b6))
|
||||
* **templates:** 新增 MCP 工具插件模板 ([5f6df60](https://git.bjxgj.com/xgj/spaceflow/commit/5f6df60b60553f025414fd102d8a279cde097485))
|
||||
* **workflows:** 为所有 GitHub Actions 工作流添加 GIT_PROVIDER_TYPE 环境变量 ([a463574](https://git.bjxgj.com/xgj/spaceflow/commit/a463574de6755a0848a8d06267f029cb947132b0))
|
||||
* **workflows:** 在发布流程中添加 GIT_PROVIDER_TYPE 环境变量 ([a4bb388](https://git.bjxgj.com/xgj/spaceflow/commit/a4bb3881f39ad351e06c5502df6895805b169a28))
|
||||
* **workflows:** 在发布流程中添加扩展安装步骤 ([716be4d](https://git.bjxgj.com/xgj/spaceflow/commit/716be4d92641ccadb3eaf01af8a51189ec5e9ade))
|
||||
* **workflows:** 将发布流程的 Git 和 NPM 配置从 GitHub 迁移到 Gitea ([6d9acff](https://git.bjxgj.com/xgj/spaceflow/commit/6d9acff06c9a202432eb3d3d5552e6ac972712f5))
|
||||
* **workflows:** 将发布流程的 GITHUB_TOKEN 改为使用 CI_GITEA_TOKEN ([e7fe7b4](https://git.bjxgj.com/xgj/spaceflow/commit/e7fe7b4271802fcdbfc2553b180f710eed419335))
|
||||
* 为所有 commands 包添加 @spaceflow/cli 开发依赖 ([d4e6c83](https://git.bjxgj.com/xgj/spaceflow/commit/d4e6c8344ca736f7e55d7db698482e8fa2445684))
|
||||
* 优化依赖配置并移除 .spaceflow 包依赖 ([be5264e](https://git.bjxgj.com/xgj/spaceflow/commit/be5264e5e0fe1f53bbe3b44a9cb86dd94ab9d266))
|
||||
* 修正 postinstall 脚本命令格式 ([3f0820f](https://git.bjxgj.com/xgj/spaceflow/commit/3f0820f85dee88808de921c3befe2d332f34cc36))
|
||||
* 恢复 pnpm catalog 配置并更新依赖锁定 ([0b2295c](https://git.bjxgj.com/xgj/spaceflow/commit/0b2295c1f906d89ad3ba7a61b04c6e6b94f193ef))
|
||||
* 新增 .spaceflow/pnpm-workspace.yaml 防止被父级 workspace 接管并移除根项目 devDependencies 自动添加逻辑 ([61de3a2](https://git.bjxgj.com/xgj/spaceflow/commit/61de3a2b75e8a19b28563d2a6476158d19f6c5be))
|
||||
* 新增 postinstall 钩子自动执行 setup 脚本 ([64dae0c](https://git.bjxgj.com/xgj/spaceflow/commit/64dae0cb440bd5e777cb790f826ff2d9f8fe65ba))
|
||||
* 移除 postinstall 钩子避免依赖安装时自动执行构建 ([ea1dc85](https://git.bjxgj.com/xgj/spaceflow/commit/ea1dc85ce7d6cf23a98c13e2c21e3c3bcdf7dd79))
|
||||
|
||||
## [0.18.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.17.0...@spaceflow/ci-shell@0.18.0) (2026-02-04)
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **verbose:** 扩展 verbose 级别支持至 3 ([c1a0808](https://git.bjxgj.com/xgj/spaceflow/commit/c1a080859e5d25ca1eb3dc7e00a67b32eb172635))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.18.0 [no ci] ([e17894a](https://git.bjxgj.com/xgj/spaceflow/commit/e17894a5af53ff040a0a17bc602d232f78415e1b))
|
||||
* **core:** released version 0.18.0 [no ci] ([c5e973f](https://git.bjxgj.com/xgj/spaceflow/commit/c5e973fbe22c0fcd0d6d3af6e4020e2fbff9d31f))
|
||||
* **period-summary:** released version 0.17.0 [no ci] ([ac4e5b6](https://git.bjxgj.com/xgj/spaceflow/commit/ac4e5b6083773146ac840548a69006f6c4fbac1d))
|
||||
* **publish:** released version 0.19.0 [no ci] ([7a96bca](https://git.bjxgj.com/xgj/spaceflow/commit/7a96bca945434a99f7d051a38cb31adfd2ade5d2))
|
||||
* **review:** released version 0.27.0 [no ci] ([ac3fc5a](https://git.bjxgj.com/xgj/spaceflow/commit/ac3fc5a5d7317d537d0447e05a61bef15a1accbe))
|
||||
|
||||
## [0.17.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.16.0...@spaceflow/ci-shell@0.17.0) (2026-02-04)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 新增 override 作用域测试,验证 includes 对 override 过滤的影响 ([820e0cb](https://git.bjxgj.com/xgj/spaceflow/commit/820e0cb0f36783dc1c7e1683ad08501e91f094b2))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 从 PR diff 填充缺失的 patch 字段 ([24bfaa7](https://git.bjxgj.com/xgj/spaceflow/commit/24bfaa76f3bd56c8ead307e73e0623a2221c69cf))
|
||||
- **review:** 新增 getFileContents、getChangedFilesBetweenRefs 和 filterIssuesByValidCommits 方法的单元测试 ([7618c91](https://git.bjxgj.com/xgj/spaceflow/commit/7618c91bc075d218b9f51b862e5161d15a306bf8))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **config:** 降低并发数以优化 AI 审查性能 ([052dd72](https://git.bjxgj.com/xgj/spaceflow/commit/052dd728f759da0a31e86a0ad480e9bb35052781))
|
||||
- **review:** 优化 Markdown 格式化器的代码风格和 JSON 数据输出逻辑 ([ca1b0c9](https://git.bjxgj.com/xgj/spaceflow/commit/ca1b0c96d9d0663a8b8dc93b4a9f63d4e5590df0))
|
||||
- **review:** 优化 override 和变更行过滤的日志输出,增强调试信息的可读性 ([9a7c6f5](https://git.bjxgj.com/xgj/spaceflow/commit/9a7c6f5b4ef2b8ae733fa499a0e5ec82feebc1d2))
|
||||
- **review:** 使用 Base64 编码存储审查数据,避免 JSON 格式在 Markdown 中被转义 ([fb91e30](https://git.bjxgj.com/xgj/spaceflow/commit/fb91e30d0979cfe63ed8e7657c578db618b5e783))
|
||||
- **review:** 基于 fileContents 实际 commit hash 验证问题归属,替代依赖 LLM 填写的 commit 字段 ([de3e377](https://git.bjxgj.com/xgj/spaceflow/commit/de3e3771eb85ff93200c63fa9feb38941914a07d))
|
||||
- **review:** 新增测试方法用于验证 PR 审查功能 ([5c57833](https://git.bjxgj.com/xgj/spaceflow/commit/5c578332cedffb7fa7e5ad753a788bcd55595c68))
|
||||
- **review:** 移除 filterNoCommit 配置项,统一使用基于 commit hash 的问题过滤逻辑 ([82429b1](https://git.bjxgj.com/xgj/spaceflow/commit/82429b1072affb4f2b14d52f99887e12184d8218))
|
||||
- **review:** 移除测试方法 testMethod ([21e9938](https://git.bjxgj.com/xgj/spaceflow/commit/21e9938100c5dd7d4eada022441c565b5c41a55a))
|
||||
- **review:** 统一使用 parseLineRange 方法解析行号,避免重复的正则匹配逻辑 ([c64f96a](https://git.bjxgj.com/xgj/spaceflow/commit/c64f96aa2e1a8e22dcd3e31e1a2acc1bb338a1a8))
|
||||
- **review:** 调整 filterIssuesByValidCommits 逻辑,保留无 commit 的 issue 交由 filterNoCommit 配置处理 ([e9c5d47](https://git.bjxgj.com/xgj/spaceflow/commit/e9c5d47aebef42507fd9fcd67e5eab624437e81a))
|
||||
- **review:** 过滤 merge commits,避免在代码审查中处理合并提交 ([d7c647c](https://git.bjxgj.com/xgj/spaceflow/commit/d7c647c33156a58b42bfb45a67417723b75328c6))
|
||||
- **review:** 过滤非 PR commits 的问题,避免 merge commit 引入的代码被审查 ([9e20f54](https://git.bjxgj.com/xgj/spaceflow/commit/9e20f54d57e71725432dfb9e7c943946aa6677d4))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 新增新增文件无 patch 时的测试用例,优化变更行标记逻辑 ([a593f0d](https://git.bjxgj.com/xgj/spaceflow/commit/a593f0d4a641b348f7c9d30b14f639b24c12dcfa))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.17.0 [no ci] ([31abd3d](https://git.bjxgj.com/xgj/spaceflow/commit/31abd3dcb48e2ddea5175552c0a87c1eaa1e7a41))
|
||||
- **core:** released version 0.17.0 [no ci] ([c85a8ed](https://git.bjxgj.com/xgj/spaceflow/commit/c85a8ed88929d867d2d460a44d08d8b7bc4866a2))
|
||||
- **period-summary:** released version 0.16.0 [no ci] ([b214e31](https://git.bjxgj.com/xgj/spaceflow/commit/b214e31221d5afa04481c48d9ddb878644a22ae7))
|
||||
- **publish:** released version 0.18.0 [no ci] ([2f2ce01](https://git.bjxgj.com/xgj/spaceflow/commit/2f2ce01726f7b3e4387e23a17974b58acd3e6929))
|
||||
- **review:** released version 0.20.0 [no ci] ([8b0f82f](https://git.bjxgj.com/xgj/spaceflow/commit/8b0f82f94813c79d579dbae8decb471b20e45e9d))
|
||||
- **review:** released version 0.21.0 [no ci] ([b51a1dd](https://git.bjxgj.com/xgj/spaceflow/commit/b51a1ddcba3e6a4b3b3eb947864e731d8f87d62b))
|
||||
- **review:** released version 0.22.0 [no ci] ([fca3bfc](https://git.bjxgj.com/xgj/spaceflow/commit/fca3bfc0c53253ac78566e88c7e5d31020a3896b))
|
||||
- **review:** released version 0.23.0 [no ci] ([ed5bf22](https://git.bjxgj.com/xgj/spaceflow/commit/ed5bf22819094df070708c2724669d0b5f7b9008))
|
||||
- **review:** released version 0.24.0 [no ci] ([5f1f94e](https://git.bjxgj.com/xgj/spaceflow/commit/5f1f94ee02123baa05802fb2bb038ccf9d50a0cc))
|
||||
- **review:** released version 0.25.0 [no ci] ([69cfeaf](https://git.bjxgj.com/xgj/spaceflow/commit/69cfeaf768e4bf7b2aaba6f089064469338a1ac0))
|
||||
- **review:** released version 0.26.0 [no ci] ([dec9c7e](https://git.bjxgj.com/xgj/spaceflow/commit/dec9c7ec66455cf83588368c930d12510ada6c0f))
|
||||
|
||||
## [0.16.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.15.0...@spaceflow/ci-shell@0.16.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 新增 Git diff 行号映射工具并优化 Claude 配置 ([88ef340](https://git.bjxgj.com/xgj/spaceflow/commit/88ef3400127fac3ad52fc326ad79fdc7bd058e98))
|
||||
- **review:** 为 execute 方法添加文档注释 ([a21f582](https://git.bjxgj.com/xgj/spaceflow/commit/a21f58290c873fb07789e70c8c5ded2b5874a29d))
|
||||
- **review:** 为 getPrNumberFromEvent 方法添加文档注释 ([54d1586](https://git.bjxgj.com/xgj/spaceflow/commit/54d1586f4558b5bfde81b926c7b513a32e5caf89))
|
||||
- **review:** 优化行号更新统计,分别统计更新和标记无效的问题数量 ([892b8be](https://git.bjxgj.com/xgj/spaceflow/commit/892b8bed8913531a9440579f777b1965fec772e5))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 优化历史 issue commit 匹配逻辑,支持短 SHA 与完整 SHA 的前缀匹配 ([e30c6dd](https://git.bjxgj.com/xgj/spaceflow/commit/e30c6ddefb14ec6631ce341f1d45c59786e94a46))
|
||||
- **review:** 简化历史问题处理策略,将行号更新改为标记变更文件问题为无效 ([5df7f00](https://git.bjxgj.com/xgj/spaceflow/commit/5df7f0087c493e104fe0dc054fd0b6c19ebe3500))
|
||||
- **review:** 简化行号更新逻辑,使用最新 commit diff 替代增量 diff ([6de7529](https://git.bjxgj.com/xgj/spaceflow/commit/6de7529c90ecbcee82149233fc01c393c5c4e7f7))
|
||||
- **review:** 重构行号更新逻辑,使用增量 diff 替代全量 diff ([d4f4304](https://git.bjxgj.com/xgj/spaceflow/commit/d4f4304e1e41614f7be8946d457eea1cf4e202fb))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 添加单元测试以覆盖行号更新逻辑 ([ebf33e4](https://git.bjxgj.com/xgj/spaceflow/commit/ebf33e45c18c910b88b106cdd4cfeb516b3fb656))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **actions:** 增强命令执行日志,输出原始 command 和 args 参数 ([0f0c238](https://git.bjxgj.com/xgj/spaceflow/commit/0f0c238de7d6f10875022f364746cefa56631b7f))
|
||||
- **ci-scripts:** released version 0.16.0 [no ci] ([9ab007d](https://git.bjxgj.com/xgj/spaceflow/commit/9ab007db178878e093ba93ea27c4f05ca813a65d))
|
||||
- **core:** released version 0.16.0 [no ci] ([871f981](https://git.bjxgj.com/xgj/spaceflow/commit/871f981b0b908c981aaef366f2382ec6ca2e2269))
|
||||
- **period-summary:** released version 0.15.0 [no ci] ([3dd72cb](https://git.bjxgj.com/xgj/spaceflow/commit/3dd72cb65a422b5b008a83820e799b810a6d53eb))
|
||||
- **publish:** released version 0.17.0 [no ci] ([8e0d065](https://git.bjxgj.com/xgj/spaceflow/commit/8e0d0654040d6af7e99fa013a8255aa93acbcc3a))
|
||||
- **review:** released version 0.19.0 [no ci] ([0ba5c0a](https://git.bjxgj.com/xgj/spaceflow/commit/0ba5c0a39879b598da2d774acc0834c590ef6d4c))
|
||||
- 在 PR 审查工作流中启用 --filter-no-commit 参数 ([e0024ad](https://git.bjxgj.com/xgj/spaceflow/commit/e0024ad5cb29250b452a841db2ce6ebf84016a2c))
|
||||
- 禁用删除代码分析功能 ([988e3f1](https://git.bjxgj.com/xgj/spaceflow/commit/988e3f156f2ca4e92413bf7a455eba1760ad9eba))
|
||||
|
||||
## [0.15.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.14.0...@spaceflow/ci-shell@0.15.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 在 Gitea SDK 中新增编辑 Pull Request 的方法 ([a586bf1](https://git.bjxgj.com/xgj/spaceflow/commit/a586bf110789578f23b39d64511229a1e5635dc4))
|
||||
- **core:** 在 Gitea SDK 中新增获取 reactions 的方法 ([9324cf2](https://git.bjxgj.com/xgj/spaceflow/commit/9324cf2550709b8302171e5522d0792c08bc1415))
|
||||
- **review:** 优化 commit author 获取逻辑,支持 committer 作为备选 ([b75b613](https://git.bjxgj.com/xgj/spaceflow/commit/b75b6133e5b8c95580516480315bc979fc6eb59b))
|
||||
- **review:** 优化 commit author 获取逻辑,支持从 Git 原始作者信息中提取 ([10ac821](https://git.bjxgj.com/xgj/spaceflow/commit/10ac8210a4457e0356c3bc1645f54f6f3d8c904c))
|
||||
- **review:** 优化 commit author 获取逻辑,通过 Gitea API 搜索用户以关联 Git 原始作者 ([daa274b](https://git.bjxgj.com/xgj/spaceflow/commit/daa274bba2255e92d1e9a6e049e20846a69e8df7))
|
||||
- **review:** 优化 PR 标题生成的格式要求 ([a4d807d](https://git.bjxgj.com/xgj/spaceflow/commit/a4d807d0a4feee4ccc88c6096e069c6dbb650a03))
|
||||
- **review:** 优化 verbose 参数支持多级别累加,将日志级别扩展为 0-3 级 ([fe4c830](https://git.bjxgj.com/xgj/spaceflow/commit/fe4c830cac137c5502d700d2cd5f22b52a629e5f))
|
||||
- **review:** 优化历史问题的 author 信息填充逻辑 ([b18d171](https://git.bjxgj.com/xgj/spaceflow/commit/b18d171c9352fe5815262d43ffd9cd7751f03a4e))
|
||||
- **review:** 优化审查报告中回复消息的格式显示 ([f478c8d](https://git.bjxgj.com/xgj/spaceflow/commit/f478c8da4c1d7494819672006e3230dbc8e0924d))
|
||||
- **review:** 优化审查报告中的消息展示格式 ([0996c2b](https://git.bjxgj.com/xgj/spaceflow/commit/0996c2b45c9502c84308f8a7f9186e4dbd4164fb))
|
||||
- **review:** 优化问题 author 信息填充时机,统一在所有问题合并后填充 ([ea8c586](https://git.bjxgj.com/xgj/spaceflow/commit/ea8c586fc60061ffd339e85c6c298b905bdfdcd8))
|
||||
- **review:** 优化问题展示和无效标记逻辑 ([e2b45e1](https://git.bjxgj.com/xgj/spaceflow/commit/e2b45e1ec594488bb79f528911fd6009a3213eca))
|
||||
- **review:** 在 fillIssueAuthors 方法中添加详细的调试日志 ([42ab288](https://git.bjxgj.com/xgj/spaceflow/commit/42ab288933296abdeeb3dbbedbb2aecedbea2251))
|
||||
- **review:** 在 syncReactionsToIssues 中添加详细日志并修复团队成员获取逻辑 ([91f166a](https://git.bjxgj.com/xgj/spaceflow/commit/91f166a07c2e43dabd4dd4ac186ec7b5f03dfc71))
|
||||
- **review:** 在审查报告的回复中为用户名添加 @ 前缀 ([bc6186b](https://git.bjxgj.com/xgj/spaceflow/commit/bc6186b97f0764f6335690eca1f8af665f9b7629))
|
||||
- **review:** 在审查问题中添加作者信息填充功能 ([8332dba](https://git.bjxgj.com/xgj/spaceflow/commit/8332dba4bb826cd358dc96db5f9b9406fb23df9b))
|
||||
- **review:** 将审查命令的详细日志参数从 --verbose 简化为 -vv ([5eb320b](https://git.bjxgj.com/xgj/spaceflow/commit/5eb320b92d1f7165052730b2e90eee52367391dd))
|
||||
- **review:** 扩展评审人收集逻辑,支持从 PR 指定的评审人和团队中获取 ([bbd61af](https://git.bjxgj.com/xgj/spaceflow/commit/bbd61af9d3e2b9e1dcf28c5e3867645fdda52e6f))
|
||||
- **review:** 支持 AI 自动生成和更新 PR 标题 ([e02fb02](https://git.bjxgj.com/xgj/spaceflow/commit/e02fb027d525dd3e794d649e6dbc53c99a3a9a59))
|
||||
- **review:** 支持 PR 关闭事件触发审查并自动传递事件类型参数 ([03967d9](https://git.bjxgj.com/xgj/spaceflow/commit/03967d9e860af7da06e3c04539f16c7bb31557ff))
|
||||
- **review:** 支持在审查报告中展示评论的 reactions 和回复记录 ([f4da31a](https://git.bjxgj.com/xgj/spaceflow/commit/f4da31adf6ce412cb0ce27bfe7a1e87e5350e915))
|
||||
- **review:** 移除 handleReview 中的重复 author 填充逻辑 ([e458bfd](https://git.bjxgj.com/xgj/spaceflow/commit/e458bfd0d21724c37fdd4023265d6a2dd1700404))
|
||||
- **review:** 限制 PR 标题自动更新仅在第一轮审查时执行 ([1891cbc](https://git.bjxgj.com/xgj/spaceflow/commit/1891cbc8d85f6eaef9e7107a7f1003bdc654d3a3))
|
||||
- **review:** 默认启用 PR 标题自动更新功能 ([fda6656](https://git.bjxgj.com/xgj/spaceflow/commit/fda6656efaf6479bb398ddc5cb1955142f31f369))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **actions:** 修复日志输出中的 emoji 显示问题,将 <20> 替换为 ℹ️ ([d3cd94a](https://git.bjxgj.com/xgj/spaceflow/commit/d3cd94afa9c6893b923d316fdcb5904f42ded632))
|
||||
- **review:** 修复审查完成日志中的乱码 emoji ([36c1c48](https://git.bjxgj.com/xgj/spaceflow/commit/36c1c48faecda3cc02b9e0b097aebba0a85ea5f8))
|
||||
- **review:** 将 UserInfo 的 id 字段类型从 number 改为 string ([505e019](https://git.bjxgj.com/xgj/spaceflow/commit/505e019c85d559ce1def1350599c1de218f7516a))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.15.0 [no ci] ([e314fb1](https://git.bjxgj.com/xgj/spaceflow/commit/e314fb11e7425b27c337d3650857cf3b737051fd))
|
||||
- **core:** released version 0.15.0 [no ci] ([48f3875](https://git.bjxgj.com/xgj/spaceflow/commit/48f38754dee382548bab968c57dd0f40f2343981))
|
||||
- **period-summary:** released version 0.14.0 [no ci] ([55a72f2](https://git.bjxgj.com/xgj/spaceflow/commit/55a72f2b481e5ded1d9207a5a8d6a6864328d5a0))
|
||||
- **publish:** released version 0.16.0 [no ci] ([e31e46d](https://git.bjxgj.com/xgj/spaceflow/commit/e31e46d08fccb10a42b6579fa042aa6c57d79c8a))
|
||||
- **review:** released version 0.18.0 [no ci] ([d366e3f](https://git.bjxgj.com/xgj/spaceflow/commit/d366e3fa9c1b32369a3d98e56fc873e033d71d00))
|
||||
|
||||
## [0.14.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.13.0...@spaceflow/ci-shell@0.14.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 统一所有命令的错误处理,添加堆栈信息输出 ([31224a1](https://git.bjxgj.com/xgj/spaceflow/commit/31224a16ce7155402504bd8d3e386e59e47949df))
|
||||
- **review:** 增强错误处理,添加堆栈信息输出 ([e0fb5de](https://git.bjxgj.com/xgj/spaceflow/commit/e0fb5de6bc877d8f0b3dc3c03f8d614320427bf3))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.14.0 [no ci] ([c536208](https://git.bjxgj.com/xgj/spaceflow/commit/c536208e352baa82e5b56c490ea9df0aff116cb2))
|
||||
- **core:** released version 0.14.0 [no ci] ([996dbc6](https://git.bjxgj.com/xgj/spaceflow/commit/996dbc6f80b0d3fb8049df9a9a31bd1e5b5d4b92))
|
||||
- **period-summary:** released version 0.13.0 [no ci] ([1d47460](https://git.bjxgj.com/xgj/spaceflow/commit/1d47460e40ba422a32865ccddd353e089eb91c6a))
|
||||
- **publish:** released version 0.15.0 [no ci] ([4b09122](https://git.bjxgj.com/xgj/spaceflow/commit/4b091227265a57f0a05488749eb4852fb421a06e))
|
||||
- **review:** released version 0.17.0 [no ci] ([9f25412](https://git.bjxgj.com/xgj/spaceflow/commit/9f254121557ae238e32f4093b0c8b5dd8a4b9a72))
|
||||
|
||||
## [0.13.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.12.0...@spaceflow/ci-shell@0.13.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 为删除影响分析添加文件过滤功能 ([7304293](https://git.bjxgj.com/xgj/spaceflow/commit/73042937c5271ff4b0dcb6cd6d823e5aa0c03e7b))
|
||||
- **review:** 新增过滤无commit问题的选项 ([7a4c458](https://git.bjxgj.com/xgj/spaceflow/commit/7a4c458da03ae4a4646abca7e5f03abc849dc405))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 修复 resolveRef 方法未处理空 ref 参数的问题 ([0824c83](https://git.bjxgj.com/xgj/spaceflow/commit/0824c8392482263036888b2fec95935371d67d4d))
|
||||
- **review:** 修复参数空值检查,增强代码健壮性 ([792a192](https://git.bjxgj.com/xgj/spaceflow/commit/792a192fd5dd80ed1e6d85cd61f6ce997bcc9dd9))
|
||||
- **review:** 修复按指定提交过滤时未处理空值导致的潜在问题 ([5d4d3e0](https://git.bjxgj.com/xgj/spaceflow/commit/5d4d3e0390a50c01309bb09e01c7328b211271b8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.13.0 [no ci] ([021eefd](https://git.bjxgj.com/xgj/spaceflow/commit/021eefdf2ff72d16b36123335548df2d3ad1d6b7))
|
||||
- **core:** released version 0.13.0 [no ci] ([e3edde3](https://git.bjxgj.com/xgj/spaceflow/commit/e3edde3e670c79544af9a7249d566961740a2284))
|
||||
- **period-summary:** released version 0.12.0 [no ci] ([38490aa](https://git.bjxgj.com/xgj/spaceflow/commit/38490aa75ab20789c5495a5d8d009867f954af4f))
|
||||
- **publish:** released version 0.14.0 [no ci] ([fe0e140](https://git.bjxgj.com/xgj/spaceflow/commit/fe0e14058a364362d7d218da9b34dbb5d8fb8f42))
|
||||
- **review:** released version 0.13.0 [no ci] ([4214c44](https://git.bjxgj.com/xgj/spaceflow/commit/4214c4406ab5482b151ec3c00da376b1d3d50887))
|
||||
- **review:** released version 0.14.0 [no ci] ([4165b05](https://git.bjxgj.com/xgj/spaceflow/commit/4165b05f8aab90d753193f3c1c2800e7f03ea4de))
|
||||
- **review:** released version 0.15.0 [no ci] ([a2ab86d](https://git.bjxgj.com/xgj/spaceflow/commit/a2ab86d097943924749876769f0a144926178783))
|
||||
- **review:** released version 0.16.0 [no ci] ([64c8866](https://git.bjxgj.com/xgj/spaceflow/commit/64c88666fc7e84ced013198d3a53a8c75c7889eb))
|
||||
|
||||
## [0.12.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.11.0...@spaceflow/ci-shell@0.12.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 CLI 入口文件添加 Node shebang 支持 ([0d787d3](https://git.bjxgj.com/xgj/spaceflow/commit/0d787d329e69f2b53d26ba04720d60625ca51efd))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.12.0 [no ci] ([097863f](https://git.bjxgj.com/xgj/spaceflow/commit/097863f0c5cc46cb5cb930f14a6f379f60a13f08))
|
||||
- **core:** released version 0.12.0 [no ci] ([1ce5034](https://git.bjxgj.com/xgj/spaceflow/commit/1ce50346d73a1914836333415f5ead9fbfa27be7))
|
||||
- **period-summary:** released version 0.11.0 [no ci] ([b518887](https://git.bjxgj.com/xgj/spaceflow/commit/b518887bddd5a452c91148bac64d61ec64b0b509))
|
||||
- **publish:** released version 0.13.0 [no ci] ([1d308d9](https://git.bjxgj.com/xgj/spaceflow/commit/1d308d9e32c50902dd881144ff541204d368006f))
|
||||
- **review:** released version 0.12.0 [no ci] ([3da605e](https://git.bjxgj.com/xgj/spaceflow/commit/3da605ea103192070f1c63112ad896a33fbc4312))
|
||||
|
||||
## [0.11.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.10.0...@spaceflow/ci-shell@0.11.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit message 的 scope 处理逻辑 ([42869dd](https://git.bjxgj.com/xgj/spaceflow/commit/42869dd4bde0a3c9bf8ffb827182775e2877a57b))
|
||||
- **core:** 重构 commit 服务并添加结构化 commit message 支持 ([22b4db8](https://git.bjxgj.com/xgj/spaceflow/commit/22b4db8619b0ce038667ab42dea1362706887fc9))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.11.0 [no ci] ([d4f5bba](https://git.bjxgj.com/xgj/spaceflow/commit/d4f5bba6f89e9e051dde8d313b6e102c6dadfa41))
|
||||
- **core:** released version 0.11.0 [no ci] ([f0025c7](https://git.bjxgj.com/xgj/spaceflow/commit/f0025c792e332e8b8752597a27f654c0197c36eb))
|
||||
- **period-summary:** released version 0.10.0 [no ci] ([c1ca3bb](https://git.bjxgj.com/xgj/spaceflow/commit/c1ca3bb67fa7f9dbb4de152f0461d644f3044946))
|
||||
- **publish:** released version 0.12.0 [no ci] ([50e209e](https://git.bjxgj.com/xgj/spaceflow/commit/50e209ebc57504462ed192a0fe22f6f944165fa3))
|
||||
- **review:** released version 0.11.0 [no ci] ([150cd9d](https://git.bjxgj.com/xgj/spaceflow/commit/150cd9df7d380c26e6f3f7f0dfd027022f610e6e))
|
||||
|
||||
## [0.10.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.9.0...@spaceflow/ci-shell@0.10.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 npm 包名处理逻辑 ([ae23ebd](https://git.bjxgj.com/xgj/spaceflow/commit/ae23ebdc3144b611e1aa8c4e66bf0db074d09798))
|
||||
- **core:** 添加依赖更新功能 ([1a544eb](https://git.bjxgj.com/xgj/spaceflow/commit/1a544eb5e2b64396a0187d4518595e9dcb51d73e))
|
||||
- **review:** 支持绝对路径转换为相对路径 ([9050f64](https://git.bjxgj.com/xgj/spaceflow/commit/9050f64b8ef67cb2c8df9663711a209523ae9d18))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.10.0 [no ci] ([ca2daad](https://git.bjxgj.com/xgj/spaceflow/commit/ca2daada8b04bbe809e69a3d5bd9373e897c6f40))
|
||||
- **core:** released version 0.10.0 [no ci] ([a80d34f](https://git.bjxgj.com/xgj/spaceflow/commit/a80d34fb647e107343a07a8793363b3b76320e81))
|
||||
- **period-summary:** released version 0.9.0 [no ci] ([ac03f9b](https://git.bjxgj.com/xgj/spaceflow/commit/ac03f9bcff742d669c6e8b2f94e40153b6c298f5))
|
||||
- **publish:** released version 0.11.0 [no ci] ([df17cd1](https://git.bjxgj.com/xgj/spaceflow/commit/df17cd1250c8fd8a035eb073d292885a4b1e3322))
|
||||
- **review:** released version 0.10.0 [no ci] ([6465de8](https://git.bjxgj.com/xgj/spaceflow/commit/6465de8751028787efb509670988c62b4dbbdf2a))
|
||||
- **review:** released version 0.9.0 [no ci] ([13dd62c](https://git.bjxgj.com/xgj/spaceflow/commit/13dd62c6f307aa6d3b78c34f485393434036fe59))
|
||||
|
||||
## [0.9.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.8.0...@spaceflow/ci-shell@0.9.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 npm 包添加 npx 直接执行支持 ([e67a7da](https://git.bjxgj.com/xgj/spaceflow/commit/e67a7da34c4e41408760da4de3a499495ce0df2f))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.9.0 [no ci] ([1b9e816](https://git.bjxgj.com/xgj/spaceflow/commit/1b9e8167bb8fc67fcc439b2ef82e7a63dc323e6d))
|
||||
- **core:** released version 0.9.0 [no ci] ([8127211](https://git.bjxgj.com/xgj/spaceflow/commit/812721136828e8c38adf0855fb292b0da89daf1a))
|
||||
- **period-summary:** released version 0.8.0 [no ci] ([44ff3c5](https://git.bjxgj.com/xgj/spaceflow/commit/44ff3c505b243e1291565e354e239ce893e5e47c))
|
||||
- **publish:** released version 0.10.0 [no ci] ([8722ba9](https://git.bjxgj.com/xgj/spaceflow/commit/8722ba9eddb03c2f73539f4e09c504ed9491a5eb))
|
||||
- **review:** released version 0.8.0 [no ci] ([ec6e7e5](https://git.bjxgj.com/xgj/spaceflow/commit/ec6e7e5defd2a5a6349d3530f3b0f4732dd5bb62))
|
||||
|
||||
## [0.8.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.7.0...@spaceflow/ci-shell@0.8.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit 消息生成器中的 scope 处理逻辑 ([1592079](https://git.bjxgj.com/xgj/spaceflow/commit/1592079edde659fe94a02bb6e2dea555c80d3b6b))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.8.0 [no ci] ([be6273d](https://git.bjxgj.com/xgj/spaceflow/commit/be6273dab7f1c80c58abdb8de6f0eeb986997e28))
|
||||
- **core:** released version 0.8.0 [no ci] ([625dbc0](https://git.bjxgj.com/xgj/spaceflow/commit/625dbc0206747b21a893ae43032f55d0a068c6fd))
|
||||
- **period-summary:** released version 0.7.0 [no ci] ([8869d58](https://git.bjxgj.com/xgj/spaceflow/commit/8869d5876e86ebe83ae65c3259cd9a7e402257cf))
|
||||
- **publish:** released version 0.9.0 [no ci] ([b404930](https://git.bjxgj.com/xgj/spaceflow/commit/b40493049877c1fd3554d77a14e9bd9ab318e15a))
|
||||
- **review:** released version 0.7.0 [no ci] ([1d195d7](https://git.bjxgj.com/xgj/spaceflow/commit/1d195d74685f12edf3b1f4e13b58ccc3d221fd94))
|
||||
|
||||
## [0.7.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.6.0...@spaceflow/ci-shell@0.7.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 重构安装服务目录结构和命名 ([50cc900](https://git.bjxgj.com/xgj/spaceflow/commit/50cc900eb864b23f20c5f48dec20d1a754238286))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.7.0 [no ci] ([ea294e1](https://git.bjxgj.com/xgj/spaceflow/commit/ea294e138c6b15033af85819629727915dfcbf4b))
|
||||
- **core:** released version 0.7.0 [no ci] ([000c53e](https://git.bjxgj.com/xgj/spaceflow/commit/000c53eff80899dbadad8d668a2227921373daad))
|
||||
- **period-summary:** released version 0.6.0 [no ci] ([6648dfb](https://git.bjxgj.com/xgj/spaceflow/commit/6648dfb31b459e8c4522cff342dfa87a4bdaab4b))
|
||||
- **publish:** released version 0.8.0 [no ci] ([d7cd2e9](https://git.bjxgj.com/xgj/spaceflow/commit/d7cd2e9a7af178acdf91f16ae299c82e915db6e6))
|
||||
- **review:** released version 0.6.0 [no ci] ([48a90b2](https://git.bjxgj.com/xgj/spaceflow/commit/48a90b253dbe03f46d26bb88f3e0158193aa1dba))
|
||||
|
||||
## [0.6.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.5.0...@spaceflow/ci-shell@0.6.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化pnpm包安装逻辑,检测是否为workspace ([6555daf](https://git.bjxgj.com/xgj/spaceflow/commit/6555dafe1f08a244525be3a0345cc585f2552086))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.6.0 [no ci] ([d485758](https://git.bjxgj.com/xgj/spaceflow/commit/d48575827941cae6ffc7ae6ba911e5d4cf3bd7fa))
|
||||
- **core:** released version 0.6.0 [no ci] ([21e1ec6](https://git.bjxgj.com/xgj/spaceflow/commit/21e1ec61a2de542e065034f32a259092dd7c0e0d))
|
||||
- **period-summary:** released version 0.5.0 [no ci] ([8e547e9](https://git.bjxgj.com/xgj/spaceflow/commit/8e547e9e6a6496a8c314c06cf6e88c97e623bc2d))
|
||||
- **publish:** released version 0.7.0 [no ci] ([7124435](https://git.bjxgj.com/xgj/spaceflow/commit/712443516845f5bbc097af16ec6e90bb57b69fa3))
|
||||
- **review:** released version 0.5.0 [no ci] ([93c3088](https://git.bjxgj.com/xgj/spaceflow/commit/93c308887040f39047766a789a37d24ac6146359))
|
||||
|
||||
## [0.5.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.4.0...@spaceflow/ci-shell@0.5.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化包管理器检测与 npm 包处理逻辑 ([63f7fa4](https://git.bjxgj.com/xgj/spaceflow/commit/63f7fa4f55cb41583009b2ea313b5ad327615e52))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 优化配置合并逻辑,添加字段覆盖策略 ([18680e6](https://git.bjxgj.com/xgj/spaceflow/commit/18680e69b0d6e9e05c843ed3f07766830955d658))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.5.0 [no ci] ([a87a1da](https://git.bjxgj.com/xgj/spaceflow/commit/a87a1da0490986c46c2a527cda5e7d0df9df6d03))
|
||||
- **core:** released version 0.5.0 [no ci] ([ad20098](https://git.bjxgj.com/xgj/spaceflow/commit/ad20098ef954283dd6d9867a4d2535769cbb8377))
|
||||
- **period-summary:** released version 0.4.0 [no ci] ([ca89a9b](https://git.bjxgj.com/xgj/spaceflow/commit/ca89a9b9436761e210dedfc38fb3c16ef39b0718))
|
||||
- **publish:** released version 0.6.0 [no ci] ([b6d8d09](https://git.bjxgj.com/xgj/spaceflow/commit/b6d8d099fc439ce67f802d56e30dadaa28afed0e))
|
||||
- **review:** released version 0.4.0 [no ci] ([3b5f8a9](https://git.bjxgj.com/xgj/spaceflow/commit/3b5f8a934de5ba4f59e232e1dcbccbdff1b8b17c))
|
||||
- 更新项目依赖锁定文件 ([19d2d1d](https://git.bjxgj.com/xgj/spaceflow/commit/19d2d1d86bb35b8ee5d3ad20be51b7aa68e83eff))
|
||||
- 移除 npm registry 配置文件 ([2d9fac6](https://git.bjxgj.com/xgj/spaceflow/commit/2d9fac6db79e81a09ca8e031190d0ebb2781cc31))
|
||||
- 调整依赖配置并添加npm registry配置 ([a754db1](https://git.bjxgj.com/xgj/spaceflow/commit/a754db1bad1bafcea50b8d2825aaf19457778f2e))
|
||||
|
||||
## [0.4.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.3.0...@spaceflow/ci-shell@0.4.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整zod依赖的导入来源 ([574eef1](https://git.bjxgj.com/xgj/spaceflow/commit/574eef1910809a72a4b13acd4cb070e12dc42ce8))
|
||||
- **review:** 调整zod依赖的导入路径 ([02014cd](https://git.bjxgj.com/xgj/spaceflow/commit/02014cdab9829df583f0f621150573b8c45a3ad7))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.4.0 [no ci] ([364f696](https://git.bjxgj.com/xgj/spaceflow/commit/364f696d0df5d84be915cfaa9202a592073d9b46))
|
||||
- **core:** released version 0.4.0 [no ci] ([bc4cd89](https://git.bjxgj.com/xgj/spaceflow/commit/bc4cd89af70dce052e7e00fe413708790f65be61))
|
||||
- **core:** 调整核心依赖与配置,新增Zod类型系统支持 ([def0751](https://git.bjxgj.com/xgj/spaceflow/commit/def0751577d9f3350494ca3c7bb4a4b087dab05e))
|
||||
- **period-summary:** released version 0.3.0 [no ci] ([7e74c59](https://git.bjxgj.com/xgj/spaceflow/commit/7e74c59d90d88e061e693829f8196834d9858307))
|
||||
- **publish:** released version 0.5.0 [no ci] ([8eecd19](https://git.bjxgj.com/xgj/spaceflow/commit/8eecd19c4dd3fbaa27187a9b24234e753fff5efe))
|
||||
- **review:** released version 0.3.0 [no ci] ([865c6fd](https://git.bjxgj.com/xgj/spaceflow/commit/865c6fdee167df187d1bc107867f842fe25c1098))
|
||||
- 调整项目依赖配置 ([6802386](https://git.bjxgj.com/xgj/spaceflow/commit/6802386f38f4081a3b5d5c47ddc49e9ec2e4f28d))
|
||||
|
||||
## [0.3.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.2.0...@spaceflow/ci-shell@0.3.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整包变更检测的日志输出格式 ([df35e92](https://git.bjxgj.com/xgj/spaceflow/commit/df35e92d614ce59e202643cf34a0fef2803412a1))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.3.0 [no ci] ([9292b52](https://git.bjxgj.com/xgj/spaceflow/commit/9292b524f2b8171f8774fab4e4ef4b32991f5d3d))
|
||||
- **core:** released version 0.3.0 [no ci] ([bf8b005](https://git.bjxgj.com/xgj/spaceflow/commit/bf8b005ccbfcdd2061c18ae4ecdd476584ecbb53))
|
||||
- **core:** 调整依赖配置 ([c86534a](https://git.bjxgj.com/xgj/spaceflow/commit/c86534ad213293ee2557ba5568549e8fbcb74ba5))
|
||||
- **period-summary:** released version 0.2.0 [no ci] ([66a4e20](https://git.bjxgj.com/xgj/spaceflow/commit/66a4e209519b64d946ec21b1d1695105fb9de106))
|
||||
- **publish:** released version 0.3.0 [no ci] ([972eca4](https://git.bjxgj.com/xgj/spaceflow/commit/972eca440dd333e8c5380124497c16fe6e3eea6c))
|
||||
- **publish:** released version 0.4.0 [no ci] ([be66220](https://git.bjxgj.com/xgj/spaceflow/commit/be662202c1e9e509368eb683a0d6df3afd876ff8))
|
||||
- **review:** released version 0.2.0 [no ci] ([d0bd3ed](https://git.bjxgj.com/xgj/spaceflow/commit/d0bd3edf364dedc7c077d95801b402d41c3fdd9c))
|
||||
- 调整项目依赖配置 ([f4009cb](https://git.bjxgj.com/xgj/spaceflow/commit/f4009cb0c369b225c356584afb28a7ff5a1a89ec))
|
||||
|
||||
## [0.2.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.1.2...@spaceflow/ci-shell@0.2.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **publish:** 增强包变更检测的日志输出 ([b89c5cc](https://git.bjxgj.com/xgj/spaceflow/commit/b89c5cc0654713b6482ee591325d4f92ad773600))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复分支锁定时未捕获异常处理器的资源泄漏问题 ([ae326e9](https://git.bjxgj.com/xgj/spaceflow/commit/ae326e95c0cea033893cf084cbf7413fb252bd33))
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **core:** 更新核心框架README文档 ([0d98658](https://git.bjxgj.com/xgj/spaceflow/commit/0d98658f6ab01f119f98d3387fb5651d4d4351a8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.2.0 [no ci] ([716e9ad](https://git.bjxgj.com/xgj/spaceflow/commit/716e9ad0f32bde09c608143da78f0a4299017797))
|
||||
- **core:** released version 0.2.0 [no ci] ([5a96529](https://git.bjxgj.com/xgj/spaceflow/commit/5a96529cabdce4fb150732b34c55e668c33cb50c))
|
||||
- **period-summary:** released version 0.1.2 [no ci] ([eaf41a0](https://git.bjxgj.com/xgj/spaceflow/commit/eaf41a0149ee4306361ccab0b3878bded79677df))
|
||||
- **publish:** released version 0.1.2 [no ci] ([4786731](https://git.bjxgj.com/xgj/spaceflow/commit/4786731da7a21982dc1e912b1a5002f5ebba9104))
|
||||
- **publish:** released version 0.2.0 [no ci] ([bc30a82](https://git.bjxgj.com/xgj/spaceflow/commit/bc30a82082bba4cc1a66c74c11dc0ad9deef4692))
|
||||
- **review:** released version 0.1.2 [no ci] ([9689d3e](https://git.bjxgj.com/xgj/spaceflow/commit/9689d3e37781ca9ae6cb14d7b12717c061f2919d))
|
||||
- 优化CI工作流的代码检出配置 ([d9740dd](https://git.bjxgj.com/xgj/spaceflow/commit/d9740dd6d1294068ffdcd7a12b61149773866a2a))
|
||||
|
||||
## [0.1.2](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.1.1...@spaceflow/ci-shell@0.1.2) (2026-01-28)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复预演模式下的交互式提示问题 ([0b785bf](https://git.bjxgj.com/xgj/spaceflow/commit/0b785bfddb9f35e844989bd3891817dc502302f8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.2 [no ci] ([ab9c100](https://git.bjxgj.com/xgj/spaceflow/commit/ab9c1000bcbe64d8a99ffa6bebb974c024b14325))
|
||||
- **core:** released version 0.1.2 [no ci] ([8292dbe](https://git.bjxgj.com/xgj/spaceflow/commit/8292dbe59a200cc640a95b86afb6451ec0c044ce))
|
||||
|
||||
## [0.1.1](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.1.0...@spaceflow/ci-shell@0.1.1) (2026-01-28)
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **publish:** 完善发布插件README文档 ([faa57b0](https://git.bjxgj.com/xgj/spaceflow/commit/faa57b095453c00fb3c9a7704bc31b63953c0ac5))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.1 [no ci] ([19ca0d8](https://git.bjxgj.com/xgj/spaceflow/commit/19ca0d8461f9537f4318b772cad3ea395d2b3264))
|
||||
- **core:** released version 0.1.1 [no ci] ([0cf3a4d](https://git.bjxgj.com/xgj/spaceflow/commit/0cf3a4d37d7d1460e232dd30bc7ab8dc015ed11f))
|
||||
|
||||
## [0.1.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/ci-shell@0.0.1...@spaceflow/ci-shell@0.1.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 添加同步解锁分支方法用于进程退出清理 ([cbec480](https://git.bjxgj.com/xgj/spaceflow/commit/cbec480511e074de3ccdc61226f3baa317cff907))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.0 [no ci] ([57b3a1c](https://git.bjxgj.com/xgj/spaceflow/commit/57b3a1c826dafd5ec51d68b7471266efd5cc32b2))
|
||||
- **core:** released version 0.1.0 [no ci] ([f455607](https://git.bjxgj.com/xgj/spaceflow/commit/f45560735082840410e08e0d8113f366732a1243))
|
||||
|
||||
## 0.0.1 (2026-01-28)
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.0.1 [no ci] ([b38fb9b](https://git.bjxgj.com/xgj/spaceflow/commit/b38fb9ba56200ced1baf563b097faa8717693783))
|
||||
- **core:** released version 0.0.1 [no ci] ([66497d6](https://git.bjxgj.com/xgj/spaceflow/commit/66497d60be04b4756a3362dbec4652177910165c))
|
||||
- 重置所有包版本至 0.0.0 并清理 CHANGELOG 文件 ([f7efaf9](https://git.bjxgj.com/xgj/spaceflow/commit/f7efaf967467f1272e05d645720ee63941fe79be))
|
||||
3
commands/ci-shell/README.md
Normal file
3
commands/ci-shell/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# ci-shell
|
||||
|
||||
在分支锁定/解锁之间执行 Shell 命令的 CI 命令。
|
||||
25
commands/ci-shell/package.json
Normal file
25
commands/ci-shell/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@spaceflow/ci-shell",
|
||||
"version": "0.19.0",
|
||||
"description": "Spaceflow CI Shell 插件,用于在分支锁定/解锁之间执行 Shell 命令",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"build": "spaceflow build",
|
||||
"dev": "spaceflow dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spaceflow/cli": "workspace:*",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "catalog:",
|
||||
"@nestjs/config": "catalog:",
|
||||
"@spaceflow/core": "workspace:*",
|
||||
"nest-commander": "catalog:"
|
||||
},
|
||||
"spaceflow": {
|
||||
"type": "flow",
|
||||
"entry": "."
|
||||
}
|
||||
}
|
||||
50
commands/ci-shell/src/ci-shell.command.ts
Normal file
50
commands/ci-shell/src/ci-shell.command.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Command, CommandRunner, Option } from "nest-commander";
|
||||
import { t } from "@spaceflow/core";
|
||||
import { CiShellService } from "./ci-shell.service";
|
||||
|
||||
export interface CiShellOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: "ci-shell",
|
||||
description: t("ci-shell:description"),
|
||||
arguments: "<command>",
|
||||
argsDescription: {
|
||||
command: t("ci-shell:argsDescription.command"),
|
||||
},
|
||||
})
|
||||
export class CiShellCommand extends CommandRunner {
|
||||
constructor(protected readonly ciShellService: CiShellService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(passedParams: string[], options: CiShellOptions): Promise<void> {
|
||||
const command = passedParams.join(" ");
|
||||
|
||||
if (!command) {
|
||||
console.error(t("ci-shell:noCommand"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`DRY-RUN mode: ${options.dryRun ? "enabled" : "disabled"}`);
|
||||
|
||||
try {
|
||||
const context = this.ciShellService.getContextFromEnv(options);
|
||||
await this.ciShellService.execute(context, command);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
t("common.executionFailed", { error: error instanceof Error ? error.message : error }),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-d, --dry-run",
|
||||
description: t("common.options.dryRun"),
|
||||
})
|
||||
parseDryRun(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
11
commands/ci-shell/src/ci-shell.module.ts
Normal file
11
commands/ci-shell/src/ci-shell.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { GitProviderModule, ciConfig } from "@spaceflow/core";
|
||||
import { CiShellCommand } from "./ci-shell.command";
|
||||
import { CiShellService } from "./ci-shell.service";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forFeature(ciConfig), GitProviderModule.forFeature()],
|
||||
providers: [CiShellCommand, CiShellService],
|
||||
})
|
||||
export class CiShellModule {}
|
||||
149
commands/ci-shell/src/ci-shell.service.ts
Normal file
149
commands/ci-shell/src/ci-shell.service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { execSync } from "child_process";
|
||||
import { GitProviderService, BranchProtection, CiConfig } from "@spaceflow/core";
|
||||
import { CiShellOptions } from "./ci-shell.command";
|
||||
|
||||
export interface CiShellContext extends CiShellOptions {
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch: string;
|
||||
}
|
||||
|
||||
export interface CiShellResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
protection?: BranchProtection | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CiShellService {
|
||||
constructor(
|
||||
protected readonly gitProvider: GitProviderService,
|
||||
protected readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
getContextFromEnv(options: CiShellOptions): CiShellContext {
|
||||
this.gitProvider.validateConfig();
|
||||
|
||||
const ciConf = this.configService.get<CiConfig>("ci");
|
||||
const repository = ciConf?.repository;
|
||||
const branch = ciConf?.refName;
|
||||
|
||||
if (!repository) {
|
||||
throw new Error("缺少配置 ci.repository (环境变量 GITHUB_REPOSITORY)");
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
throw new Error("缺少配置 ci.refName (环境变量 GITHUB_REF_NAME)");
|
||||
}
|
||||
|
||||
const [owner, repo] = repository.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`ci.repository 格式不正确,期望 "owner/repo",实际: "${repository}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(context: CiShellContext, command: string): Promise<void> {
|
||||
try {
|
||||
// 1. 锁定分支
|
||||
await this.handleBegin(context);
|
||||
|
||||
try {
|
||||
// 2. 执行命令
|
||||
console.log(`🏃 正在执行命令...`);
|
||||
console.log(`> ${command}`);
|
||||
|
||||
if (context.dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 跳过命令执行`);
|
||||
} else {
|
||||
execSync(command, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
console.log("✅ 命令执行成功");
|
||||
} catch (error) {
|
||||
console.error("❌ 命令执行失败:", error);
|
||||
// 出错时也要尝试解锁
|
||||
await this.handleEnd(context);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. 解锁分支
|
||||
await this.handleEnd(context);
|
||||
} catch (error) {
|
||||
console.error("执行失败:", error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleBegin(context: CiShellContext): Promise<CiShellResult> {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将锁定分支: ${owner}/${repo}#${branch}`);
|
||||
return {
|
||||
success: true,
|
||||
message: "DRY-RUN: 分支锁定已跳过",
|
||||
protection: null,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`🔒 正在锁定分支: ${owner}/${repo}#${branch}`);
|
||||
|
||||
const protection = await this.gitProvider.lockBranch(owner, repo, branch);
|
||||
|
||||
console.log(`✅ 分支已锁定`);
|
||||
console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
|
||||
console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "分支锁定完成",
|
||||
protection,
|
||||
};
|
||||
}
|
||||
|
||||
protected async handleEnd(context: CiShellContext): Promise<CiShellResult> {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将解锁分支: ${owner}/${repo}#${branch}`);
|
||||
return {
|
||||
success: true,
|
||||
message: "DRY-RUN: 分支解锁已跳过",
|
||||
protection: null,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`🔓 正在解锁分支: ${owner}/${repo}#${branch}`);
|
||||
|
||||
const protection = await this.gitProvider.unlockBranch(owner, repo, branch);
|
||||
|
||||
if (protection) {
|
||||
console.log(`✅ 分支已解锁`);
|
||||
console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
|
||||
console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "分支解锁完成",
|
||||
protection,
|
||||
};
|
||||
} else {
|
||||
console.log(`✅ 分支本身没有保护规则,无需解锁`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "分支本身没有保护规则,无需解锁",
|
||||
protection: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
commands/ci-shell/src/index.ts
Normal file
24
commands/ci-shell/src/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import "./locales";
|
||||
import { SpaceflowExtension, SpaceflowExtensionMetadata, t } from "@spaceflow/core";
|
||||
import { CiShellModule } from "./ci-shell.module";
|
||||
export class CiShellExtension implements SpaceflowExtension {
|
||||
getMetadata(): SpaceflowExtensionMetadata {
|
||||
return {
|
||||
name: "ci-shell",
|
||||
commands: ["ci-shell"],
|
||||
configKey: "ci-shell",
|
||||
version: "1.0.0",
|
||||
description: t("ci-shell:extensionDescription"),
|
||||
};
|
||||
}
|
||||
|
||||
getModule() {
|
||||
return CiShellModule;
|
||||
}
|
||||
}
|
||||
|
||||
export default CiShellExtension;
|
||||
|
||||
export * from "./ci-shell.command";
|
||||
export * from "./ci-shell.service";
|
||||
export * from "./ci-shell.module";
|
||||
6
commands/ci-shell/src/locales/en/ci-shell.json
Normal file
6
commands/ci-shell/src/locales/en/ci-shell.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"description": "Execute shell commands between branch lock/unlock",
|
||||
"argsDescription.command": "Shell command to execute",
|
||||
"noCommand": "❌ Please provide a shell command to execute",
|
||||
"extensionDescription": "CI shell command for executing shell commands between branch lock/unlock"
|
||||
}
|
||||
11
commands/ci-shell/src/locales/index.ts
Normal file
11
commands/ci-shell/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { addLocaleResources } from "@spaceflow/core";
|
||||
import zhCN from "./zh-cn/ci-shell.json";
|
||||
import en from "./en/ci-shell.json";
|
||||
|
||||
/** ci-shell 命令 i18n 资源 */
|
||||
export const ciShellLocales: Record<string, Record<string, string>> = {
|
||||
"zh-CN": zhCN,
|
||||
en,
|
||||
};
|
||||
|
||||
addLocaleResources("ci-shell", ciShellLocales);
|
||||
6
commands/ci-shell/src/locales/zh-cn/ci-shell.json
Normal file
6
commands/ci-shell/src/locales/zh-cn/ci-shell.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"description": "在分支锁定/解锁之间执行 Shell 命令",
|
||||
"argsDescription.command": "要执行的 Shell 命令",
|
||||
"noCommand": "❌ 请提供要执行的 Shell 命令",
|
||||
"extensionDescription": "CI Shell 命令,用于在分支锁定/解锁之间执行 Shell 命令"
|
||||
}
|
||||
5
commands/ci-shell/tsconfig.json
Normal file
5
commands/ci-shell/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../core/tsconfig.skill.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
491
commands/period-summary/CHANGELOG.md
Normal file
491
commands/period-summary/CHANGELOG.md
Normal file
@@ -0,0 +1,491 @@
|
||||
# Changelog
|
||||
|
||||
## [0.19.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.18.0...@spaceflow/period-summary@0.19.0) (2026-02-15)
|
||||
|
||||
### 新特性
|
||||
|
||||
* **cli:** 新增 MCP Server 命令并集成 review 扩展的 MCP 工具 ([b794b36](https://git.bjxgj.com/xgj/spaceflow/commit/b794b36d90788c7eb4cbb253397413b4a080ae83))
|
||||
* **cli:** 新增 MCP Server 导出类型支持 ([9568cbd](https://git.bjxgj.com/xgj/spaceflow/commit/9568cbd14d4cfbdedaf2218379c72337af6db271))
|
||||
* **core:** 为所有命令添加 i18n 国际化支持 ([867c5d3](https://git.bjxgj.com/xgj/spaceflow/commit/867c5d3eccc285c8a68803b8aa2f0ffb86a94285))
|
||||
* **core:** 新增 GitLab 平台适配器并完善配置支持 ([47be9ad](https://git.bjxgj.com/xgj/spaceflow/commit/47be9adfa90944a9cb183e03286a7a96fec747f1))
|
||||
* **core:** 新增 Logger 全局日志工具并支持 plain/tui 双模式渲染 ([8baae7c](https://git.bjxgj.com/xgj/spaceflow/commit/8baae7c24139695a0e379e1c874023cd61dfc41b))
|
||||
* **docs:** 新增 VitePress 文档站点并完善项目文档 ([a79d620](https://git.bjxgj.com/xgj/spaceflow/commit/a79d6208e60390a44fa4c94621eb41ae20159e98))
|
||||
* **mcp:** 新增 MCP Inspector 交互式调试支持并优化工具日志输出 ([05fd2ee](https://git.bjxgj.com/xgj/spaceflow/commit/05fd2ee941c5f6088b769d1127cb7c0615626f8c))
|
||||
* **review:** 为 MCP 服务添加 i18n 国际化支持 ([a749054](https://git.bjxgj.com/xgj/spaceflow/commit/a749054eb73b775a5f5973ab1b86c04f2b2ddfba))
|
||||
* **review:** 新增规则级 includes 解析测试并修复文件级/规则级 includes 过滤逻辑 ([4baca71](https://git.bjxgj.com/xgj/spaceflow/commit/4baca71c17782fb92a95b3207f9c61e0b410b9ff))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
* **actions:** 修正 pnpm setup 命令调用方式 ([8f014fa](https://git.bjxgj.com/xgj/spaceflow/commit/8f014fa90b74e20de4c353804d271b3ef6f1288f))
|
||||
* **mcp:** 添加 -y 选项确保 Inspector 自动安装依赖 ([a9201f7](https://git.bjxgj.com/xgj/spaceflow/commit/a9201f74bd9ddc5eba92beaaa676f377842863e0))
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **claude:** 移除 .claude 目录及其 .gitignore 配置文件 ([91916a9](https://git.bjxgj.com/xgj/spaceflow/commit/91916a99f65da31c1d34e6f75b5cbea1d331ba35))
|
||||
* **cli:** 优化依赖安装流程并支持 .spaceflow 目录配置 ([5977631](https://git.bjxgj.com/xgj/spaceflow/commit/597763183eaa61bb024bba2703d75239650b54fb))
|
||||
* **cli:** 拆分 CLI 为独立包并重构扩展加载机制 ([b385d28](https://git.bjxgj.com/xgj/spaceflow/commit/b385d281575f29b823bb6dc4229a396a29c0e226))
|
||||
* **cli:** 移除 ExtensionModule 并优化扩展加载机制 ([8f7077d](https://git.bjxgj.com/xgj/spaceflow/commit/8f7077deaef4e5f4032662ff5ac925cd3c07fdb6))
|
||||
* **cli:** 调整依赖顺序并格式化导入语句 ([32a9c1c](https://git.bjxgj.com/xgj/spaceflow/commit/32a9c1cf834725a20f93b1f8f60b52692841a3e5))
|
||||
* **cli:** 重构 getPluginConfigFromPackageJson 方法以提高代码可读性 ([f5f6ed9](https://git.bjxgj.com/xgj/spaceflow/commit/f5f6ed9858cc4ca670e30fac469774bdc8f7b005))
|
||||
* **cli:** 重构扩展配置格式,支持 flow/command/skill 三种导出类型 ([958dc13](https://git.bjxgj.com/xgj/spaceflow/commit/958dc130621f78bbcc260224da16a5f16ae0b2b1))
|
||||
* **core:** 为 build/clear/commit 命令添加国际化支持 ([de82cb2](https://git.bjxgj.com/xgj/spaceflow/commit/de82cb2f1ed8cef0e446a2d42a1bf1f091e9c421))
|
||||
* **core:** 优化 list 命令输出格式并修复 MCP Inspector 包管理器兼容性 ([a019829](https://git.bjxgj.com/xgj/spaceflow/commit/a019829d3055c083aeb86ed60ce6629d13012d91))
|
||||
* **core:** 将 rspack 配置和工具函数中的 @spaceflow/cli 引用改为 @spaceflow/core ([3c301c6](https://git.bjxgj.com/xgj/spaceflow/commit/3c301c60f3e61b127db94481f5a19307f5ef00eb))
|
||||
* **core:** 将扩展依赖从 @spaceflow/cli 迁移到 @spaceflow/core ([6f9ffd4](https://git.bjxgj.com/xgj/spaceflow/commit/6f9ffd4061cecae4faaf3d051e3ca98a0b42b01f))
|
||||
* **core:** 提取 source 处理和包管理器工具函数到共享模块 ([ab3ff00](https://git.bjxgj.com/xgj/spaceflow/commit/ab3ff003d1cd586c0c4efc7841e6a93fe3477ace))
|
||||
* **core:** 新增 getEnvFilePaths 工具函数统一管理 .env 文件路径优先级 ([809fa18](https://git.bjxgj.com/xgj/spaceflow/commit/809fa18f3d0b8eabcb068988bab53d548eaf03ea))
|
||||
* **core:** 新增远程仓库规则拉取功能并支持 Git API 获取目录内容 ([69ade16](https://git.bjxgj.com/xgj/spaceflow/commit/69ade16c9069f9e1a90b3ef56dc834e33a3c0650))
|
||||
* **core:** 统一 LogLevel 类型定义并支持字符串/数字双模式 ([557f6b0](https://git.bjxgj.com/xgj/spaceflow/commit/557f6b0bc39fcfb0e3f773836cbbf08c1a8790ae))
|
||||
* **core:** 重构配置读取逻辑,新增 ConfigReaderService 并支持 .spaceflowrc 配置文件 ([72e88ce](https://git.bjxgj.com/xgj/spaceflow/commit/72e88ced63d03395923cdfb113addf4945162e54))
|
||||
* **i18n:** 将 locales 导入从命令文件迁移至扩展入口文件 ([0da5d98](https://git.bjxgj.com/xgj/spaceflow/commit/0da5d9886296c4183b24ad8c56140763f5a870a4))
|
||||
* **i18n:** 移除扩展元数据中的 locales 字段并改用 side-effect 自动注册 ([2c7d488](https://git.bjxgj.com/xgj/spaceflow/commit/2c7d488a9dfa59a99b95e40e3c449c28c2d433d8))
|
||||
* **mcp:** 使用 DTO + Swagger 装饰器替代手动 JSON Schema 定义 ([87ec262](https://git.bjxgj.com/xgj/spaceflow/commit/87ec26252dd295536bb090ae8b7e418eec96e1bd))
|
||||
* **mcp:** 升级 MCP SDK API 并优化 Inspector 调试配置 ([176d04a](https://git.bjxgj.com/xgj/spaceflow/commit/176d04a73fbbb8d115520d922f5fedb9a2961aa6))
|
||||
* **mcp:** 将 MCP 元数据存储从 Reflect Metadata 改为静态属性以支持跨模块访问 ([cac0ea2](https://git.bjxgj.com/xgj/spaceflow/commit/cac0ea2029e1b504bc4278ce72b3aa87fff88c84))
|
||||
* **test:** 迁移测试框架从 Jest 到 Vitest ([308f9d4](https://git.bjxgj.com/xgj/spaceflow/commit/308f9d49089019530588344a5e8880f5b6504a6a))
|
||||
* 优化构建流程并调整 MCP/review 日志输出级别 ([74072c0](https://git.bjxgj.com/xgj/spaceflow/commit/74072c04be7a45bfc0ab53b636248fe5c0e1e42a))
|
||||
* 将 .spaceflow/package.json 纳入版本控制并自动添加到根项目依赖 ([ab83d25](https://git.bjxgj.com/xgj/spaceflow/commit/ab83d2579cb5414ee3d78a9768fac2147a3d1ad9))
|
||||
* 将 GiteaSdkModule/GiteaSdkService 重命名为 GitProviderModule/GitProviderService ([462f492](https://git.bjxgj.com/xgj/spaceflow/commit/462f492bc2607cf508c5011d181c599cf17e00c9))
|
||||
* 恢复 pnpm catalog 配置并移除 .spaceflow 工作区导入器 ([217387e](https://git.bjxgj.com/xgj/spaceflow/commit/217387e2e8517a08162e9bcaf604893fd9bca736))
|
||||
* 迁移扩展依赖到 .spaceflow 工作区并移除 pnpm catalog ([c457c0f](https://git.bjxgj.com/xgj/spaceflow/commit/c457c0f8918171f1856b88bc007921d76c508335))
|
||||
* 重构 Extension 安装机制为 pnpm workspace 模式 ([469b12e](https://git.bjxgj.com/xgj/spaceflow/commit/469b12eac28f747b628e52a5125a3d5a538fba39))
|
||||
* 重构插件加载改为扩展模式 ([0e6e140](https://git.bjxgj.com/xgj/spaceflow/commit/0e6e140b19ea2cf6084afc261c555d2083fe04f9))
|
||||
|
||||
### 文档更新
|
||||
|
||||
* **guide:** 更新编辑器集成文档,补充四种导出类型说明和 MCP 注册机制 ([19a7409](https://git.bjxgj.com/xgj/spaceflow/commit/19a7409092c89d002f11ee51ebcb6863118429bd))
|
||||
* **guide:** 更新配置文件位置说明并补充 RC 文件支持 ([2214dc4](https://git.bjxgj.com/xgj/spaceflow/commit/2214dc4e197221971f5286b38ceaa6fcbcaa7884))
|
||||
|
||||
### 测试用例
|
||||
|
||||
* **core:** 新增 GiteaAdapter 完整单元测试并实现自动检测 provider 配置 ([c74f745](https://git.bjxgj.com/xgj/spaceflow/commit/c74f7458aed91ac7d12fb57ef1c24b3d2917c406))
|
||||
* **review:** 新增 DeletionImpactService 测试覆盖并配置 coverage 工具 ([50bfbfe](https://git.bjxgj.com/xgj/spaceflow/commit/50bfbfe37192641f1170ade8f5eb00e0e382af67))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.19.0 [no ci] ([9f747c6](https://git.bjxgj.com/xgj/spaceflow/commit/9f747c617b387e105e92b4a5dcd0f5d3cf51c26d))
|
||||
* **ci-shell:** released version 0.19.0 [no ci] ([59ac30d](https://git.bjxgj.com/xgj/spaceflow/commit/59ac30da6802a9493c33e560ea9121d378597e89))
|
||||
* **ci:** 迁移工作流从 Gitea 到 GitHub 并统一环境变量命名 ([57e3bae](https://git.bjxgj.com/xgj/spaceflow/commit/57e3bae635b324c8c4ea50a9fb667b6241fae0ef))
|
||||
* **cli:** released version 0.19.0 [no ci] ([6b63149](https://git.bjxgj.com/xgj/spaceflow/commit/6b631499e2407a1822395d5f40cec2d725331b78))
|
||||
* **config:** 将 git 推送白名单用户从 "Gitea Actions" 改为 "GiteaActions" ([fdbb865](https://git.bjxgj.com/xgj/spaceflow/commit/fdbb865341e6f02b26fca32b54a33b51bee11cad))
|
||||
* **config:** 将 git 推送白名单用户从 github-actions[bot] 改为 Gitea Actions ([9c39819](https://git.bjxgj.com/xgj/spaceflow/commit/9c39819a9f95f415068f7f0333770b92bc98321b))
|
||||
* **config:** 移除 review-spec 私有仓库依赖 ([8ae18f1](https://git.bjxgj.com/xgj/spaceflow/commit/8ae18f13c441b033d1cbc75119695a5cc5cb6a0b))
|
||||
* **core:** released version 0.1.0 [no ci] ([170fa67](https://git.bjxgj.com/xgj/spaceflow/commit/170fa670e98473c2377120656d23aae835c51997))
|
||||
* **core:** 禁用 i18next 初始化时的 locize.com 推广日志 ([a99fbb0](https://git.bjxgj.com/xgj/spaceflow/commit/a99fbb068441bc623efcf15a1dd7b6bd38c05f38))
|
||||
* **deps:** 移除 pnpm catalog 配置并更新依赖锁定 ([753fb9e](https://git.bjxgj.com/xgj/spaceflow/commit/753fb9e3e43b28054c75158193dc39ab4bab1af5))
|
||||
* **docs:** 统一文档脚本命名,为 VitePress 命令添加 docs: 前缀 ([3cc46ea](https://git.bjxgj.com/xgj/spaceflow/commit/3cc46eab3a600290f5064b8270902e586b9c5af4))
|
||||
* **i18n:** 配置 i18n-ally-next 自动提取键名生成策略 ([753c3dc](https://git.bjxgj.com/xgj/spaceflow/commit/753c3dc3f24f3c03c837d1ec2c505e8e3ce08b11))
|
||||
* **i18n:** 重构 i18n 配置并统一 locales 目录结构 ([3e94037](https://git.bjxgj.com/xgj/spaceflow/commit/3e94037fa6493b3b0e4a12ff6af9f4bea48ae217))
|
||||
* **publish:** released version 0.20.0 [no ci] ([d347e3b](https://git.bjxgj.com/xgj/spaceflow/commit/d347e3b2041157d8dc6e3ade69b05a481b2ab371))
|
||||
* **review:** released version 0.28.0 [no ci] ([a2d89ed](https://git.bjxgj.com/xgj/spaceflow/commit/a2d89ed5f386eb6dd299c0d0a208856ce267ab5e))
|
||||
* **scripts:** 修正 setup 和 build 脚本的过滤条件,避免重复构建 cli 包 ([ffd2ffe](https://git.bjxgj.com/xgj/spaceflow/commit/ffd2ffedca08fd56cccb6a9fbd2b6bd106e367b6))
|
||||
* **templates:** 新增 MCP 工具插件模板 ([5f6df60](https://git.bjxgj.com/xgj/spaceflow/commit/5f6df60b60553f025414fd102d8a279cde097485))
|
||||
* **workflows:** 为所有 GitHub Actions 工作流添加 GIT_PROVIDER_TYPE 环境变量 ([a463574](https://git.bjxgj.com/xgj/spaceflow/commit/a463574de6755a0848a8d06267f029cb947132b0))
|
||||
* **workflows:** 在发布流程中添加 GIT_PROVIDER_TYPE 环境变量 ([a4bb388](https://git.bjxgj.com/xgj/spaceflow/commit/a4bb3881f39ad351e06c5502df6895805b169a28))
|
||||
* **workflows:** 在发布流程中添加扩展安装步骤 ([716be4d](https://git.bjxgj.com/xgj/spaceflow/commit/716be4d92641ccadb3eaf01af8a51189ec5e9ade))
|
||||
* **workflows:** 将发布流程的 Git 和 NPM 配置从 GitHub 迁移到 Gitea ([6d9acff](https://git.bjxgj.com/xgj/spaceflow/commit/6d9acff06c9a202432eb3d3d5552e6ac972712f5))
|
||||
* **workflows:** 将发布流程的 GITHUB_TOKEN 改为使用 CI_GITEA_TOKEN ([e7fe7b4](https://git.bjxgj.com/xgj/spaceflow/commit/e7fe7b4271802fcdbfc2553b180f710eed419335))
|
||||
* 为所有 commands 包添加 @spaceflow/cli 开发依赖 ([d4e6c83](https://git.bjxgj.com/xgj/spaceflow/commit/d4e6c8344ca736f7e55d7db698482e8fa2445684))
|
||||
* 优化依赖配置并移除 .spaceflow 包依赖 ([be5264e](https://git.bjxgj.com/xgj/spaceflow/commit/be5264e5e0fe1f53bbe3b44a9cb86dd94ab9d266))
|
||||
* 修正 postinstall 脚本命令格式 ([3f0820f](https://git.bjxgj.com/xgj/spaceflow/commit/3f0820f85dee88808de921c3befe2d332f34cc36))
|
||||
* 恢复 pnpm catalog 配置并更新依赖锁定 ([0b2295c](https://git.bjxgj.com/xgj/spaceflow/commit/0b2295c1f906d89ad3ba7a61b04c6e6b94f193ef))
|
||||
* 新增 .spaceflow/pnpm-workspace.yaml 防止被父级 workspace 接管并移除根项目 devDependencies 自动添加逻辑 ([61de3a2](https://git.bjxgj.com/xgj/spaceflow/commit/61de3a2b75e8a19b28563d2a6476158d19f6c5be))
|
||||
* 新增 postinstall 钩子自动执行 setup 脚本 ([64dae0c](https://git.bjxgj.com/xgj/spaceflow/commit/64dae0cb440bd5e777cb790f826ff2d9f8fe65ba))
|
||||
* 移除 postinstall 钩子避免依赖安装时自动执行构建 ([ea1dc85](https://git.bjxgj.com/xgj/spaceflow/commit/ea1dc85ce7d6cf23a98c13e2c21e3c3bcdf7dd79))
|
||||
|
||||
## [0.18.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.17.0...@spaceflow/period-summary@0.18.0) (2026-02-04)
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **verbose:** 扩展 verbose 级别支持至 3 ([c1a0808](https://git.bjxgj.com/xgj/spaceflow/commit/c1a080859e5d25ca1eb3dc7e00a67b32eb172635))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.18.0 [no ci] ([e17894a](https://git.bjxgj.com/xgj/spaceflow/commit/e17894a5af53ff040a0a17bc602d232f78415e1b))
|
||||
* **ci-shell:** released version 0.18.0 [no ci] ([f64fd80](https://git.bjxgj.com/xgj/spaceflow/commit/f64fd8009a6dd725f572c7e9fbf084d9320d5128))
|
||||
* **core:** released version 0.18.0 [no ci] ([c5e973f](https://git.bjxgj.com/xgj/spaceflow/commit/c5e973fbe22c0fcd0d6d3af6e4020e2fbff9d31f))
|
||||
* **publish:** released version 0.19.0 [no ci] ([7a96bca](https://git.bjxgj.com/xgj/spaceflow/commit/7a96bca945434a99f7d051a38cb31adfd2ade5d2))
|
||||
* **review:** released version 0.27.0 [no ci] ([ac3fc5a](https://git.bjxgj.com/xgj/spaceflow/commit/ac3fc5a5d7317d537d0447e05a61bef15a1accbe))
|
||||
|
||||
## [0.17.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.16.0...@spaceflow/period-summary@0.17.0) (2026-02-04)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 新增 override 作用域测试,验证 includes 对 override 过滤的影响 ([820e0cb](https://git.bjxgj.com/xgj/spaceflow/commit/820e0cb0f36783dc1c7e1683ad08501e91f094b2))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 从 PR diff 填充缺失的 patch 字段 ([24bfaa7](https://git.bjxgj.com/xgj/spaceflow/commit/24bfaa76f3bd56c8ead307e73e0623a2221c69cf))
|
||||
- **review:** 新增 getFileContents、getChangedFilesBetweenRefs 和 filterIssuesByValidCommits 方法的单元测试 ([7618c91](https://git.bjxgj.com/xgj/spaceflow/commit/7618c91bc075d218b9f51b862e5161d15a306bf8))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **config:** 降低并发数以优化 AI 审查性能 ([052dd72](https://git.bjxgj.com/xgj/spaceflow/commit/052dd728f759da0a31e86a0ad480e9bb35052781))
|
||||
- **review:** 优化 Markdown 格式化器的代码风格和 JSON 数据输出逻辑 ([ca1b0c9](https://git.bjxgj.com/xgj/spaceflow/commit/ca1b0c96d9d0663a8b8dc93b4a9f63d4e5590df0))
|
||||
- **review:** 优化 override 和变更行过滤的日志输出,增强调试信息的可读性 ([9a7c6f5](https://git.bjxgj.com/xgj/spaceflow/commit/9a7c6f5b4ef2b8ae733fa499a0e5ec82feebc1d2))
|
||||
- **review:** 使用 Base64 编码存储审查数据,避免 JSON 格式在 Markdown 中被转义 ([fb91e30](https://git.bjxgj.com/xgj/spaceflow/commit/fb91e30d0979cfe63ed8e7657c578db618b5e783))
|
||||
- **review:** 基于 fileContents 实际 commit hash 验证问题归属,替代依赖 LLM 填写的 commit 字段 ([de3e377](https://git.bjxgj.com/xgj/spaceflow/commit/de3e3771eb85ff93200c63fa9feb38941914a07d))
|
||||
- **review:** 新增测试方法用于验证 PR 审查功能 ([5c57833](https://git.bjxgj.com/xgj/spaceflow/commit/5c578332cedffb7fa7e5ad753a788bcd55595c68))
|
||||
- **review:** 移除 filterNoCommit 配置项,统一使用基于 commit hash 的问题过滤逻辑 ([82429b1](https://git.bjxgj.com/xgj/spaceflow/commit/82429b1072affb4f2b14d52f99887e12184d8218))
|
||||
- **review:** 移除测试方法 testMethod ([21e9938](https://git.bjxgj.com/xgj/spaceflow/commit/21e9938100c5dd7d4eada022441c565b5c41a55a))
|
||||
- **review:** 统一使用 parseLineRange 方法解析行号,避免重复的正则匹配逻辑 ([c64f96a](https://git.bjxgj.com/xgj/spaceflow/commit/c64f96aa2e1a8e22dcd3e31e1a2acc1bb338a1a8))
|
||||
- **review:** 调整 filterIssuesByValidCommits 逻辑,保留无 commit 的 issue 交由 filterNoCommit 配置处理 ([e9c5d47](https://git.bjxgj.com/xgj/spaceflow/commit/e9c5d47aebef42507fd9fcd67e5eab624437e81a))
|
||||
- **review:** 过滤 merge commits,避免在代码审查中处理合并提交 ([d7c647c](https://git.bjxgj.com/xgj/spaceflow/commit/d7c647c33156a58b42bfb45a67417723b75328c6))
|
||||
- **review:** 过滤非 PR commits 的问题,避免 merge commit 引入的代码被审查 ([9e20f54](https://git.bjxgj.com/xgj/spaceflow/commit/9e20f54d57e71725432dfb9e7c943946aa6677d4))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 新增新增文件无 patch 时的测试用例,优化变更行标记逻辑 ([a593f0d](https://git.bjxgj.com/xgj/spaceflow/commit/a593f0d4a641b348f7c9d30b14f639b24c12dcfa))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.17.0 [no ci] ([31abd3d](https://git.bjxgj.com/xgj/spaceflow/commit/31abd3dcb48e2ddea5175552c0a87c1eaa1e7a41))
|
||||
- **ci-shell:** released version 0.17.0 [no ci] ([a53508b](https://git.bjxgj.com/xgj/spaceflow/commit/a53508b15e4020e3399bae9cc04e730f1539ad8e))
|
||||
- **core:** released version 0.17.0 [no ci] ([c85a8ed](https://git.bjxgj.com/xgj/spaceflow/commit/c85a8ed88929d867d2d460a44d08d8b7bc4866a2))
|
||||
- **publish:** released version 0.18.0 [no ci] ([2f2ce01](https://git.bjxgj.com/xgj/spaceflow/commit/2f2ce01726f7b3e4387e23a17974b58acd3e6929))
|
||||
- **review:** released version 0.20.0 [no ci] ([8b0f82f](https://git.bjxgj.com/xgj/spaceflow/commit/8b0f82f94813c79d579dbae8decb471b20e45e9d))
|
||||
- **review:** released version 0.21.0 [no ci] ([b51a1dd](https://git.bjxgj.com/xgj/spaceflow/commit/b51a1ddcba3e6a4b3b3eb947864e731d8f87d62b))
|
||||
- **review:** released version 0.22.0 [no ci] ([fca3bfc](https://git.bjxgj.com/xgj/spaceflow/commit/fca3bfc0c53253ac78566e88c7e5d31020a3896b))
|
||||
- **review:** released version 0.23.0 [no ci] ([ed5bf22](https://git.bjxgj.com/xgj/spaceflow/commit/ed5bf22819094df070708c2724669d0b5f7b9008))
|
||||
- **review:** released version 0.24.0 [no ci] ([5f1f94e](https://git.bjxgj.com/xgj/spaceflow/commit/5f1f94ee02123baa05802fb2bb038ccf9d50a0cc))
|
||||
- **review:** released version 0.25.0 [no ci] ([69cfeaf](https://git.bjxgj.com/xgj/spaceflow/commit/69cfeaf768e4bf7b2aaba6f089064469338a1ac0))
|
||||
- **review:** released version 0.26.0 [no ci] ([dec9c7e](https://git.bjxgj.com/xgj/spaceflow/commit/dec9c7ec66455cf83588368c930d12510ada6c0f))
|
||||
|
||||
## [0.16.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.15.0...@spaceflow/period-summary@0.16.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 新增 Git diff 行号映射工具并优化 Claude 配置 ([88ef340](https://git.bjxgj.com/xgj/spaceflow/commit/88ef3400127fac3ad52fc326ad79fdc7bd058e98))
|
||||
- **review:** 为 execute 方法添加文档注释 ([a21f582](https://git.bjxgj.com/xgj/spaceflow/commit/a21f58290c873fb07789e70c8c5ded2b5874a29d))
|
||||
- **review:** 为 getPrNumberFromEvent 方法添加文档注释 ([54d1586](https://git.bjxgj.com/xgj/spaceflow/commit/54d1586f4558b5bfde81b926c7b513a32e5caf89))
|
||||
- **review:** 优化行号更新统计,分别统计更新和标记无效的问题数量 ([892b8be](https://git.bjxgj.com/xgj/spaceflow/commit/892b8bed8913531a9440579f777b1965fec772e5))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 优化历史 issue commit 匹配逻辑,支持短 SHA 与完整 SHA 的前缀匹配 ([e30c6dd](https://git.bjxgj.com/xgj/spaceflow/commit/e30c6ddefb14ec6631ce341f1d45c59786e94a46))
|
||||
- **review:** 简化历史问题处理策略,将行号更新改为标记变更文件问题为无效 ([5df7f00](https://git.bjxgj.com/xgj/spaceflow/commit/5df7f0087c493e104fe0dc054fd0b6c19ebe3500))
|
||||
- **review:** 简化行号更新逻辑,使用最新 commit diff 替代增量 diff ([6de7529](https://git.bjxgj.com/xgj/spaceflow/commit/6de7529c90ecbcee82149233fc01c393c5c4e7f7))
|
||||
- **review:** 重构行号更新逻辑,使用增量 diff 替代全量 diff ([d4f4304](https://git.bjxgj.com/xgj/spaceflow/commit/d4f4304e1e41614f7be8946d457eea1cf4e202fb))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 添加单元测试以覆盖行号更新逻辑 ([ebf33e4](https://git.bjxgj.com/xgj/spaceflow/commit/ebf33e45c18c910b88b106cdd4cfeb516b3fb656))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **actions:** 增强命令执行日志,输出原始 command 和 args 参数 ([0f0c238](https://git.bjxgj.com/xgj/spaceflow/commit/0f0c238de7d6f10875022f364746cefa56631b7f))
|
||||
- **ci-scripts:** released version 0.16.0 [no ci] ([9ab007d](https://git.bjxgj.com/xgj/spaceflow/commit/9ab007db178878e093ba93ea27c4f05ca813a65d))
|
||||
- **ci-shell:** released version 0.16.0 [no ci] ([87fd703](https://git.bjxgj.com/xgj/spaceflow/commit/87fd7030b54d2f614f23e092499c5c51bfc33788))
|
||||
- **core:** released version 0.16.0 [no ci] ([871f981](https://git.bjxgj.com/xgj/spaceflow/commit/871f981b0b908c981aaef366f2382ec6ca2e2269))
|
||||
- **publish:** released version 0.17.0 [no ci] ([8e0d065](https://git.bjxgj.com/xgj/spaceflow/commit/8e0d0654040d6af7e99fa013a8255aa93acbcc3a))
|
||||
- **review:** released version 0.19.0 [no ci] ([0ba5c0a](https://git.bjxgj.com/xgj/spaceflow/commit/0ba5c0a39879b598da2d774acc0834c590ef6d4c))
|
||||
- 在 PR 审查工作流中启用 --filter-no-commit 参数 ([e0024ad](https://git.bjxgj.com/xgj/spaceflow/commit/e0024ad5cb29250b452a841db2ce6ebf84016a2c))
|
||||
- 禁用删除代码分析功能 ([988e3f1](https://git.bjxgj.com/xgj/spaceflow/commit/988e3f156f2ca4e92413bf7a455eba1760ad9eba))
|
||||
|
||||
## [0.15.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.14.0...@spaceflow/period-summary@0.15.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 在 Gitea SDK 中新增编辑 Pull Request 的方法 ([a586bf1](https://git.bjxgj.com/xgj/spaceflow/commit/a586bf110789578f23b39d64511229a1e5635dc4))
|
||||
- **core:** 在 Gitea SDK 中新增获取 reactions 的方法 ([9324cf2](https://git.bjxgj.com/xgj/spaceflow/commit/9324cf2550709b8302171e5522d0792c08bc1415))
|
||||
- **review:** 优化 commit author 获取逻辑,支持 committer 作为备选 ([b75b613](https://git.bjxgj.com/xgj/spaceflow/commit/b75b6133e5b8c95580516480315bc979fc6eb59b))
|
||||
- **review:** 优化 commit author 获取逻辑,支持从 Git 原始作者信息中提取 ([10ac821](https://git.bjxgj.com/xgj/spaceflow/commit/10ac8210a4457e0356c3bc1645f54f6f3d8c904c))
|
||||
- **review:** 优化 commit author 获取逻辑,通过 Gitea API 搜索用户以关联 Git 原始作者 ([daa274b](https://git.bjxgj.com/xgj/spaceflow/commit/daa274bba2255e92d1e9a6e049e20846a69e8df7))
|
||||
- **review:** 优化 PR 标题生成的格式要求 ([a4d807d](https://git.bjxgj.com/xgj/spaceflow/commit/a4d807d0a4feee4ccc88c6096e069c6dbb650a03))
|
||||
- **review:** 优化 verbose 参数支持多级别累加,将日志级别扩展为 0-3 级 ([fe4c830](https://git.bjxgj.com/xgj/spaceflow/commit/fe4c830cac137c5502d700d2cd5f22b52a629e5f))
|
||||
- **review:** 优化历史问题的 author 信息填充逻辑 ([b18d171](https://git.bjxgj.com/xgj/spaceflow/commit/b18d171c9352fe5815262d43ffd9cd7751f03a4e))
|
||||
- **review:** 优化审查报告中回复消息的格式显示 ([f478c8d](https://git.bjxgj.com/xgj/spaceflow/commit/f478c8da4c1d7494819672006e3230dbc8e0924d))
|
||||
- **review:** 优化审查报告中的消息展示格式 ([0996c2b](https://git.bjxgj.com/xgj/spaceflow/commit/0996c2b45c9502c84308f8a7f9186e4dbd4164fb))
|
||||
- **review:** 优化问题 author 信息填充时机,统一在所有问题合并后填充 ([ea8c586](https://git.bjxgj.com/xgj/spaceflow/commit/ea8c586fc60061ffd339e85c6c298b905bdfdcd8))
|
||||
- **review:** 优化问题展示和无效标记逻辑 ([e2b45e1](https://git.bjxgj.com/xgj/spaceflow/commit/e2b45e1ec594488bb79f528911fd6009a3213eca))
|
||||
- **review:** 在 fillIssueAuthors 方法中添加详细的调试日志 ([42ab288](https://git.bjxgj.com/xgj/spaceflow/commit/42ab288933296abdeeb3dbbedbb2aecedbea2251))
|
||||
- **review:** 在 syncReactionsToIssues 中添加详细日志并修复团队成员获取逻辑 ([91f166a](https://git.bjxgj.com/xgj/spaceflow/commit/91f166a07c2e43dabd4dd4ac186ec7b5f03dfc71))
|
||||
- **review:** 在审查报告的回复中为用户名添加 @ 前缀 ([bc6186b](https://git.bjxgj.com/xgj/spaceflow/commit/bc6186b97f0764f6335690eca1f8af665f9b7629))
|
||||
- **review:** 在审查问题中添加作者信息填充功能 ([8332dba](https://git.bjxgj.com/xgj/spaceflow/commit/8332dba4bb826cd358dc96db5f9b9406fb23df9b))
|
||||
- **review:** 将审查命令的详细日志参数从 --verbose 简化为 -vv ([5eb320b](https://git.bjxgj.com/xgj/spaceflow/commit/5eb320b92d1f7165052730b2e90eee52367391dd))
|
||||
- **review:** 扩展评审人收集逻辑,支持从 PR 指定的评审人和团队中获取 ([bbd61af](https://git.bjxgj.com/xgj/spaceflow/commit/bbd61af9d3e2b9e1dcf28c5e3867645fdda52e6f))
|
||||
- **review:** 支持 AI 自动生成和更新 PR 标题 ([e02fb02](https://git.bjxgj.com/xgj/spaceflow/commit/e02fb027d525dd3e794d649e6dbc53c99a3a9a59))
|
||||
- **review:** 支持 PR 关闭事件触发审查并自动传递事件类型参数 ([03967d9](https://git.bjxgj.com/xgj/spaceflow/commit/03967d9e860af7da06e3c04539f16c7bb31557ff))
|
||||
- **review:** 支持在审查报告中展示评论的 reactions 和回复记录 ([f4da31a](https://git.bjxgj.com/xgj/spaceflow/commit/f4da31adf6ce412cb0ce27bfe7a1e87e5350e915))
|
||||
- **review:** 移除 handleReview 中的重复 author 填充逻辑 ([e458bfd](https://git.bjxgj.com/xgj/spaceflow/commit/e458bfd0d21724c37fdd4023265d6a2dd1700404))
|
||||
- **review:** 限制 PR 标题自动更新仅在第一轮审查时执行 ([1891cbc](https://git.bjxgj.com/xgj/spaceflow/commit/1891cbc8d85f6eaef9e7107a7f1003bdc654d3a3))
|
||||
- **review:** 默认启用 PR 标题自动更新功能 ([fda6656](https://git.bjxgj.com/xgj/spaceflow/commit/fda6656efaf6479bb398ddc5cb1955142f31f369))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **actions:** 修复日志输出中的 emoji 显示问题,将 <20> 替换为 ℹ️ ([d3cd94a](https://git.bjxgj.com/xgj/spaceflow/commit/d3cd94afa9c6893b923d316fdcb5904f42ded632))
|
||||
- **review:** 修复审查完成日志中的乱码 emoji ([36c1c48](https://git.bjxgj.com/xgj/spaceflow/commit/36c1c48faecda3cc02b9e0b097aebba0a85ea5f8))
|
||||
- **review:** 将 UserInfo 的 id 字段类型从 number 改为 string ([505e019](https://git.bjxgj.com/xgj/spaceflow/commit/505e019c85d559ce1def1350599c1de218f7516a))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.15.0 [no ci] ([e314fb1](https://git.bjxgj.com/xgj/spaceflow/commit/e314fb11e7425b27c337d3650857cf3b737051fd))
|
||||
- **ci-shell:** released version 0.15.0 [no ci] ([5c0dc0b](https://git.bjxgj.com/xgj/spaceflow/commit/5c0dc0b5482366ccfd7854868d1eb5f306c24810))
|
||||
- **core:** released version 0.15.0 [no ci] ([48f3875](https://git.bjxgj.com/xgj/spaceflow/commit/48f38754dee382548bab968c57dd0f40f2343981))
|
||||
- **publish:** released version 0.16.0 [no ci] ([e31e46d](https://git.bjxgj.com/xgj/spaceflow/commit/e31e46d08fccb10a42b6579fa042aa6c57d79c8a))
|
||||
- **review:** released version 0.18.0 [no ci] ([d366e3f](https://git.bjxgj.com/xgj/spaceflow/commit/d366e3fa9c1b32369a3d98e56fc873e033d71d00))
|
||||
|
||||
## [0.14.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.13.0...@spaceflow/period-summary@0.14.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 统一所有命令的错误处理,添加堆栈信息输出 ([31224a1](https://git.bjxgj.com/xgj/spaceflow/commit/31224a16ce7155402504bd8d3e386e59e47949df))
|
||||
- **review:** 增强错误处理,添加堆栈信息输出 ([e0fb5de](https://git.bjxgj.com/xgj/spaceflow/commit/e0fb5de6bc877d8f0b3dc3c03f8d614320427bf3))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.14.0 [no ci] ([c536208](https://git.bjxgj.com/xgj/spaceflow/commit/c536208e352baa82e5b56c490ea9df0aff116cb2))
|
||||
- **ci-shell:** released version 0.14.0 [no ci] ([c6e4bdc](https://git.bjxgj.com/xgj/spaceflow/commit/c6e4bdca44874739694e3e46998e376779503e53))
|
||||
- **core:** released version 0.14.0 [no ci] ([996dbc6](https://git.bjxgj.com/xgj/spaceflow/commit/996dbc6f80b0d3fb8049df9a9a31bd1e5b5d4b92))
|
||||
- **publish:** released version 0.15.0 [no ci] ([4b09122](https://git.bjxgj.com/xgj/spaceflow/commit/4b091227265a57f0a05488749eb4852fb421a06e))
|
||||
- **review:** released version 0.17.0 [no ci] ([9f25412](https://git.bjxgj.com/xgj/spaceflow/commit/9f254121557ae238e32f4093b0c8b5dd8a4b9a72))
|
||||
|
||||
## [0.13.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.12.0...@spaceflow/period-summary@0.13.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 为删除影响分析添加文件过滤功能 ([7304293](https://git.bjxgj.com/xgj/spaceflow/commit/73042937c5271ff4b0dcb6cd6d823e5aa0c03e7b))
|
||||
- **review:** 新增过滤无commit问题的选项 ([7a4c458](https://git.bjxgj.com/xgj/spaceflow/commit/7a4c458da03ae4a4646abca7e5f03abc849dc405))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 修复 resolveRef 方法未处理空 ref 参数的问题 ([0824c83](https://git.bjxgj.com/xgj/spaceflow/commit/0824c8392482263036888b2fec95935371d67d4d))
|
||||
- **review:** 修复参数空值检查,增强代码健壮性 ([792a192](https://git.bjxgj.com/xgj/spaceflow/commit/792a192fd5dd80ed1e6d85cd61f6ce997bcc9dd9))
|
||||
- **review:** 修复按指定提交过滤时未处理空值导致的潜在问题 ([5d4d3e0](https://git.bjxgj.com/xgj/spaceflow/commit/5d4d3e0390a50c01309bb09e01c7328b211271b8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.13.0 [no ci] ([021eefd](https://git.bjxgj.com/xgj/spaceflow/commit/021eefdf2ff72d16b36123335548df2d3ad1d6b7))
|
||||
- **ci-shell:** released version 0.13.0 [no ci] ([81e7582](https://git.bjxgj.com/xgj/spaceflow/commit/81e75820eb69ca188155e33945111e2b1f6b3012))
|
||||
- **core:** released version 0.13.0 [no ci] ([e3edde3](https://git.bjxgj.com/xgj/spaceflow/commit/e3edde3e670c79544af9a7249d566961740a2284))
|
||||
- **publish:** released version 0.14.0 [no ci] ([fe0e140](https://git.bjxgj.com/xgj/spaceflow/commit/fe0e14058a364362d7d218da9b34dbb5d8fb8f42))
|
||||
- **review:** released version 0.13.0 [no ci] ([4214c44](https://git.bjxgj.com/xgj/spaceflow/commit/4214c4406ab5482b151ec3c00da376b1d3d50887))
|
||||
- **review:** released version 0.14.0 [no ci] ([4165b05](https://git.bjxgj.com/xgj/spaceflow/commit/4165b05f8aab90d753193f3c1c2800e7f03ea4de))
|
||||
- **review:** released version 0.15.0 [no ci] ([a2ab86d](https://git.bjxgj.com/xgj/spaceflow/commit/a2ab86d097943924749876769f0a144926178783))
|
||||
- **review:** released version 0.16.0 [no ci] ([64c8866](https://git.bjxgj.com/xgj/spaceflow/commit/64c88666fc7e84ced013198d3a53a8c75c7889eb))
|
||||
|
||||
## [0.12.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.11.0...@spaceflow/period-summary@0.12.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 CLI 入口文件添加 Node shebang 支持 ([0d787d3](https://git.bjxgj.com/xgj/spaceflow/commit/0d787d329e69f2b53d26ba04720d60625ca51efd))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.12.0 [no ci] ([097863f](https://git.bjxgj.com/xgj/spaceflow/commit/097863f0c5cc46cb5cb930f14a6f379f60a13f08))
|
||||
- **ci-shell:** released version 0.12.0 [no ci] ([274216f](https://git.bjxgj.com/xgj/spaceflow/commit/274216fc930dfbf8390d02e25c06efcb44980fed))
|
||||
- **core:** released version 0.12.0 [no ci] ([1ce5034](https://git.bjxgj.com/xgj/spaceflow/commit/1ce50346d73a1914836333415f5ead9fbfa27be7))
|
||||
- **publish:** released version 0.13.0 [no ci] ([1d308d9](https://git.bjxgj.com/xgj/spaceflow/commit/1d308d9e32c50902dd881144ff541204d368006f))
|
||||
- **review:** released version 0.12.0 [no ci] ([3da605e](https://git.bjxgj.com/xgj/spaceflow/commit/3da605ea103192070f1c63112ad896a33fbc4312))
|
||||
|
||||
## [0.11.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.10.0...@spaceflow/period-summary@0.11.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit message 的 scope 处理逻辑 ([42869dd](https://git.bjxgj.com/xgj/spaceflow/commit/42869dd4bde0a3c9bf8ffb827182775e2877a57b))
|
||||
- **core:** 重构 commit 服务并添加结构化 commit message 支持 ([22b4db8](https://git.bjxgj.com/xgj/spaceflow/commit/22b4db8619b0ce038667ab42dea1362706887fc9))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.11.0 [no ci] ([d4f5bba](https://git.bjxgj.com/xgj/spaceflow/commit/d4f5bba6f89e9e051dde8d313b6e102c6dadfa41))
|
||||
- **ci-shell:** released version 0.11.0 [no ci] ([cf9e486](https://git.bjxgj.com/xgj/spaceflow/commit/cf9e48666197295f118396693abc08b680b3ddee))
|
||||
- **core:** released version 0.11.0 [no ci] ([f0025c7](https://git.bjxgj.com/xgj/spaceflow/commit/f0025c792e332e8b8752597a27f654c0197c36eb))
|
||||
- **publish:** released version 0.12.0 [no ci] ([50e209e](https://git.bjxgj.com/xgj/spaceflow/commit/50e209ebc57504462ed192a0fe22f6f944165fa3))
|
||||
- **review:** released version 0.11.0 [no ci] ([150cd9d](https://git.bjxgj.com/xgj/spaceflow/commit/150cd9df7d380c26e6f3f7f0dfd027022f610e6e))
|
||||
|
||||
## [0.10.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.9.0...@spaceflow/period-summary@0.10.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 npm 包名处理逻辑 ([ae23ebd](https://git.bjxgj.com/xgj/spaceflow/commit/ae23ebdc3144b611e1aa8c4e66bf0db074d09798))
|
||||
- **core:** 添加依赖更新功能 ([1a544eb](https://git.bjxgj.com/xgj/spaceflow/commit/1a544eb5e2b64396a0187d4518595e9dcb51d73e))
|
||||
- **review:** 支持绝对路径转换为相对路径 ([9050f64](https://git.bjxgj.com/xgj/spaceflow/commit/9050f64b8ef67cb2c8df9663711a209523ae9d18))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.10.0 [no ci] ([ca2daad](https://git.bjxgj.com/xgj/spaceflow/commit/ca2daada8b04bbe809e69a3d5bd9373e897c6f40))
|
||||
- **ci-shell:** released version 0.10.0 [no ci] ([53864b8](https://git.bjxgj.com/xgj/spaceflow/commit/53864b8c2534cae265b8fbb98173a5b909682d4e))
|
||||
- **core:** released version 0.10.0 [no ci] ([a80d34f](https://git.bjxgj.com/xgj/spaceflow/commit/a80d34fb647e107343a07a8793363b3b76320e81))
|
||||
- **publish:** released version 0.11.0 [no ci] ([df17cd1](https://git.bjxgj.com/xgj/spaceflow/commit/df17cd1250c8fd8a035eb073d292885a4b1e3322))
|
||||
- **review:** released version 0.10.0 [no ci] ([6465de8](https://git.bjxgj.com/xgj/spaceflow/commit/6465de8751028787efb509670988c62b4dbbdf2a))
|
||||
- **review:** released version 0.9.0 [no ci] ([13dd62c](https://git.bjxgj.com/xgj/spaceflow/commit/13dd62c6f307aa6d3b78c34f485393434036fe59))
|
||||
|
||||
## [0.9.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.8.0...@spaceflow/period-summary@0.9.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 npm 包添加 npx 直接执行支持 ([e67a7da](https://git.bjxgj.com/xgj/spaceflow/commit/e67a7da34c4e41408760da4de3a499495ce0df2f))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.9.0 [no ci] ([1b9e816](https://git.bjxgj.com/xgj/spaceflow/commit/1b9e8167bb8fc67fcc439b2ef82e7a63dc323e6d))
|
||||
- **ci-shell:** released version 0.9.0 [no ci] ([accdda7](https://git.bjxgj.com/xgj/spaceflow/commit/accdda7ee4628dc8447e9a89da6c8101c572cb90))
|
||||
- **core:** released version 0.9.0 [no ci] ([8127211](https://git.bjxgj.com/xgj/spaceflow/commit/812721136828e8c38adf0855fb292b0da89daf1a))
|
||||
- **publish:** released version 0.10.0 [no ci] ([8722ba9](https://git.bjxgj.com/xgj/spaceflow/commit/8722ba9eddb03c2f73539f4e09c504ed9491a5eb))
|
||||
- **review:** released version 0.8.0 [no ci] ([ec6e7e5](https://git.bjxgj.com/xgj/spaceflow/commit/ec6e7e5defd2a5a6349d3530f3b0f4732dd5bb62))
|
||||
|
||||
## [0.8.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.7.0...@spaceflow/period-summary@0.8.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit 消息生成器中的 scope 处理逻辑 ([1592079](https://git.bjxgj.com/xgj/spaceflow/commit/1592079edde659fe94a02bb6e2dea555c80d3b6b))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.8.0 [no ci] ([be6273d](https://git.bjxgj.com/xgj/spaceflow/commit/be6273dab7f1c80c58abdb8de6f0eeb986997e28))
|
||||
- **ci-shell:** released version 0.8.0 [no ci] ([3102178](https://git.bjxgj.com/xgj/spaceflow/commit/310217827c6ec29294dee5689b2dbb1b66492728))
|
||||
- **core:** released version 0.8.0 [no ci] ([625dbc0](https://git.bjxgj.com/xgj/spaceflow/commit/625dbc0206747b21a893ae43032f55d0a068c6fd))
|
||||
- **publish:** released version 0.9.0 [no ci] ([b404930](https://git.bjxgj.com/xgj/spaceflow/commit/b40493049877c1fd3554d77a14e9bd9ab318e15a))
|
||||
- **review:** released version 0.7.0 [no ci] ([1d195d7](https://git.bjxgj.com/xgj/spaceflow/commit/1d195d74685f12edf3b1f4e13b58ccc3d221fd94))
|
||||
|
||||
## [0.7.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.6.0...@spaceflow/period-summary@0.7.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 重构安装服务目录结构和命名 ([50cc900](https://git.bjxgj.com/xgj/spaceflow/commit/50cc900eb864b23f20c5f48dec20d1a754238286))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.7.0 [no ci] ([ea294e1](https://git.bjxgj.com/xgj/spaceflow/commit/ea294e138c6b15033af85819629727915dfcbf4b))
|
||||
- **ci-shell:** released version 0.7.0 [no ci] ([247967b](https://git.bjxgj.com/xgj/spaceflow/commit/247967b30876aae78cfb1f9c706431b5bb9fb57e))
|
||||
- **core:** released version 0.7.0 [no ci] ([000c53e](https://git.bjxgj.com/xgj/spaceflow/commit/000c53eff80899dbadad8d668a2227921373daad))
|
||||
- **publish:** released version 0.8.0 [no ci] ([d7cd2e9](https://git.bjxgj.com/xgj/spaceflow/commit/d7cd2e9a7af178acdf91f16ae299c82e915db6e6))
|
||||
- **review:** released version 0.6.0 [no ci] ([48a90b2](https://git.bjxgj.com/xgj/spaceflow/commit/48a90b253dbe03f46d26bb88f3e0158193aa1dba))
|
||||
|
||||
## [0.6.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.5.0...@spaceflow/period-summary@0.6.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化pnpm包安装逻辑,检测是否为workspace ([6555daf](https://git.bjxgj.com/xgj/spaceflow/commit/6555dafe1f08a244525be3a0345cc585f2552086))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.6.0 [no ci] ([d485758](https://git.bjxgj.com/xgj/spaceflow/commit/d48575827941cae6ffc7ae6ba911e5d4cf3bd7fa))
|
||||
- **ci-shell:** released version 0.6.0 [no ci] ([a2d1239](https://git.bjxgj.com/xgj/spaceflow/commit/a2d12397997b309062a9d93af57a5588cdb82a79))
|
||||
- **core:** released version 0.6.0 [no ci] ([21e1ec6](https://git.bjxgj.com/xgj/spaceflow/commit/21e1ec61a2de542e065034f32a259092dd7c0e0d))
|
||||
- **publish:** released version 0.7.0 [no ci] ([7124435](https://git.bjxgj.com/xgj/spaceflow/commit/712443516845f5bbc097af16ec6e90bb57b69fa3))
|
||||
- **review:** released version 0.5.0 [no ci] ([93c3088](https://git.bjxgj.com/xgj/spaceflow/commit/93c308887040f39047766a789a37d24ac6146359))
|
||||
|
||||
## [0.5.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.4.0...@spaceflow/period-summary@0.5.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化包管理器检测与 npm 包处理逻辑 ([63f7fa4](https://git.bjxgj.com/xgj/spaceflow/commit/63f7fa4f55cb41583009b2ea313b5ad327615e52))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 优化配置合并逻辑,添加字段覆盖策略 ([18680e6](https://git.bjxgj.com/xgj/spaceflow/commit/18680e69b0d6e9e05c843ed3f07766830955d658))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.5.0 [no ci] ([a87a1da](https://git.bjxgj.com/xgj/spaceflow/commit/a87a1da0490986c46c2a527cda5e7d0df9df6d03))
|
||||
- **ci-shell:** released version 0.5.0 [no ci] ([920d9a8](https://git.bjxgj.com/xgj/spaceflow/commit/920d9a8165fe6eabf7a074eb65762f4693883438))
|
||||
- **core:** released version 0.5.0 [no ci] ([ad20098](https://git.bjxgj.com/xgj/spaceflow/commit/ad20098ef954283dd6d9867a4d2535769cbb8377))
|
||||
- **publish:** released version 0.6.0 [no ci] ([b6d8d09](https://git.bjxgj.com/xgj/spaceflow/commit/b6d8d099fc439ce67f802d56e30dadaa28afed0e))
|
||||
- **review:** released version 0.4.0 [no ci] ([3b5f8a9](https://git.bjxgj.com/xgj/spaceflow/commit/3b5f8a934de5ba4f59e232e1dcbccbdff1b8b17c))
|
||||
- 更新项目依赖锁定文件 ([19d2d1d](https://git.bjxgj.com/xgj/spaceflow/commit/19d2d1d86bb35b8ee5d3ad20be51b7aa68e83eff))
|
||||
- 移除 npm registry 配置文件 ([2d9fac6](https://git.bjxgj.com/xgj/spaceflow/commit/2d9fac6db79e81a09ca8e031190d0ebb2781cc31))
|
||||
- 调整依赖配置并添加npm registry配置 ([a754db1](https://git.bjxgj.com/xgj/spaceflow/commit/a754db1bad1bafcea50b8d2825aaf19457778f2e))
|
||||
|
||||
## [0.4.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.3.0...@spaceflow/period-summary@0.4.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整zod依赖的导入来源 ([574eef1](https://git.bjxgj.com/xgj/spaceflow/commit/574eef1910809a72a4b13acd4cb070e12dc42ce8))
|
||||
- **review:** 调整zod依赖的导入路径 ([02014cd](https://git.bjxgj.com/xgj/spaceflow/commit/02014cdab9829df583f0f621150573b8c45a3ad7))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.4.0 [no ci] ([364f696](https://git.bjxgj.com/xgj/spaceflow/commit/364f696d0df5d84be915cfaa9202a592073d9b46))
|
||||
- **ci-shell:** released version 0.4.0 [no ci] ([7e6bf1d](https://git.bjxgj.com/xgj/spaceflow/commit/7e6bf1dabffc6250b918b89bb850d478d3f4b875))
|
||||
- **core:** released version 0.4.0 [no ci] ([bc4cd89](https://git.bjxgj.com/xgj/spaceflow/commit/bc4cd89af70dce052e7e00fe413708790f65be61))
|
||||
- **core:** 调整核心依赖与配置,新增Zod类型系统支持 ([def0751](https://git.bjxgj.com/xgj/spaceflow/commit/def0751577d9f3350494ca3c7bb4a4b087dab05e))
|
||||
- **publish:** released version 0.5.0 [no ci] ([8eecd19](https://git.bjxgj.com/xgj/spaceflow/commit/8eecd19c4dd3fbaa27187a9b24234e753fff5efe))
|
||||
- **review:** released version 0.3.0 [no ci] ([865c6fd](https://git.bjxgj.com/xgj/spaceflow/commit/865c6fdee167df187d1bc107867f842fe25c1098))
|
||||
- 调整项目依赖配置 ([6802386](https://git.bjxgj.com/xgj/spaceflow/commit/6802386f38f4081a3b5d5c47ddc49e9ec2e4f28d))
|
||||
|
||||
## [0.3.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.2.0...@spaceflow/period-summary@0.3.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整包变更检测的日志输出格式 ([df35e92](https://git.bjxgj.com/xgj/spaceflow/commit/df35e92d614ce59e202643cf34a0fef2803412a1))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.3.0 [no ci] ([9292b52](https://git.bjxgj.com/xgj/spaceflow/commit/9292b524f2b8171f8774fab4e4ef4b32991f5d3d))
|
||||
- **ci-shell:** released version 0.3.0 [no ci] ([7b25e55](https://git.bjxgj.com/xgj/spaceflow/commit/7b25e557b628fdfa66d7a0be4ee21267906ac15f))
|
||||
- **core:** released version 0.3.0 [no ci] ([bf8b005](https://git.bjxgj.com/xgj/spaceflow/commit/bf8b005ccbfcdd2061c18ae4ecdd476584ecbb53))
|
||||
- **core:** 调整依赖配置 ([c86534a](https://git.bjxgj.com/xgj/spaceflow/commit/c86534ad213293ee2557ba5568549e8fbcb74ba5))
|
||||
- **publish:** released version 0.3.0 [no ci] ([972eca4](https://git.bjxgj.com/xgj/spaceflow/commit/972eca440dd333e8c5380124497c16fe6e3eea6c))
|
||||
- **publish:** released version 0.4.0 [no ci] ([be66220](https://git.bjxgj.com/xgj/spaceflow/commit/be662202c1e9e509368eb683a0d6df3afd876ff8))
|
||||
- **review:** released version 0.2.0 [no ci] ([d0bd3ed](https://git.bjxgj.com/xgj/spaceflow/commit/d0bd3edf364dedc7c077d95801b402d41c3fdd9c))
|
||||
- 调整项目依赖配置 ([f4009cb](https://git.bjxgj.com/xgj/spaceflow/commit/f4009cb0c369b225c356584afb28a7ff5a1a89ec))
|
||||
|
||||
## [0.2.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.1.2...@spaceflow/period-summary@0.2.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **publish:** 增强包变更检测的日志输出 ([b89c5cc](https://git.bjxgj.com/xgj/spaceflow/commit/b89c5cc0654713b6482ee591325d4f92ad773600))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复分支锁定时未捕获异常处理器的资源泄漏问题 ([ae326e9](https://git.bjxgj.com/xgj/spaceflow/commit/ae326e95c0cea033893cf084cbf7413fb252bd33))
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **core:** 更新核心框架README文档 ([0d98658](https://git.bjxgj.com/xgj/spaceflow/commit/0d98658f6ab01f119f98d3387fb5651d4d4351a8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.2.0 [no ci] ([716e9ad](https://git.bjxgj.com/xgj/spaceflow/commit/716e9ad0f32bde09c608143da78f0a4299017797))
|
||||
- **ci-shell:** released version 0.2.0 [no ci] ([4f5314b](https://git.bjxgj.com/xgj/spaceflow/commit/4f5314b1002b90d7775a5ef51e618a3f227ae5a9))
|
||||
- **core:** released version 0.2.0 [no ci] ([5a96529](https://git.bjxgj.com/xgj/spaceflow/commit/5a96529cabdce4fb150732b34c55e668c33cb50c))
|
||||
- **publish:** released version 0.1.2 [no ci] ([4786731](https://git.bjxgj.com/xgj/spaceflow/commit/4786731da7a21982dc1e912b1a5002f5ebba9104))
|
||||
- **publish:** released version 0.2.0 [no ci] ([bc30a82](https://git.bjxgj.com/xgj/spaceflow/commit/bc30a82082bba4cc1a66c74c11dc0ad9deef4692))
|
||||
- **review:** released version 0.1.2 [no ci] ([9689d3e](https://git.bjxgj.com/xgj/spaceflow/commit/9689d3e37781ca9ae6cb14d7b12717c061f2919d))
|
||||
- 优化CI工作流的代码检出配置 ([d9740dd](https://git.bjxgj.com/xgj/spaceflow/commit/d9740dd6d1294068ffdcd7a12b61149773866a2a))
|
||||
|
||||
## [0.1.2](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.1.1...@spaceflow/period-summary@0.1.2) (2026-01-28)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复预演模式下的交互式提示问题 ([0b785bf](https://git.bjxgj.com/xgj/spaceflow/commit/0b785bfddb9f35e844989bd3891817dc502302f8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.2 [no ci] ([ab9c100](https://git.bjxgj.com/xgj/spaceflow/commit/ab9c1000bcbe64d8a99ffa6bebb974c024b14325))
|
||||
- **ci-shell:** released version 0.1.2 [no ci] ([bf7977b](https://git.bjxgj.com/xgj/spaceflow/commit/bf7977bed684b557555861b9dc0359eda3c5d476))
|
||||
- **core:** released version 0.1.2 [no ci] ([8292dbe](https://git.bjxgj.com/xgj/spaceflow/commit/8292dbe59a200cc640a95b86afb6451ec0c044ce))
|
||||
|
||||
## [0.1.1](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.1.0...@spaceflow/period-summary@0.1.1) (2026-01-28)
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **publish:** 完善发布插件README文档 ([faa57b0](https://git.bjxgj.com/xgj/spaceflow/commit/faa57b095453c00fb3c9a7704bc31b63953c0ac5))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.1 [no ci] ([19ca0d8](https://git.bjxgj.com/xgj/spaceflow/commit/19ca0d8461f9537f4318b772cad3ea395d2b3264))
|
||||
- **ci-shell:** released version 0.1.1 [no ci] ([488a686](https://git.bjxgj.com/xgj/spaceflow/commit/488a6869240151e7d1cf37a3b177897c2b5d5c1e))
|
||||
- **core:** released version 0.1.1 [no ci] ([0cf3a4d](https://git.bjxgj.com/xgj/spaceflow/commit/0cf3a4d37d7d1460e232dd30bc7ab8dc015ed11f))
|
||||
|
||||
## [0.1.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/period-summary@0.0.1...@spaceflow/period-summary@0.1.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 添加同步解锁分支方法用于进程退出清理 ([cbec480](https://git.bjxgj.com/xgj/spaceflow/commit/cbec480511e074de3ccdc61226f3baa317cff907))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.0 [no ci] ([57b3a1c](https://git.bjxgj.com/xgj/spaceflow/commit/57b3a1c826dafd5ec51d68b7471266efd5cc32b2))
|
||||
- **ci-shell:** released version 0.1.0 [no ci] ([2283d9d](https://git.bjxgj.com/xgj/spaceflow/commit/2283d9d69ada1c071bef6c548dc756fe062893bd))
|
||||
- **core:** released version 0.1.0 [no ci] ([f455607](https://git.bjxgj.com/xgj/spaceflow/commit/f45560735082840410e08e0d8113f366732a1243))
|
||||
|
||||
## 0.0.1 (2026-01-28)
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.0.1 [no ci] ([b38fb9b](https://git.bjxgj.com/xgj/spaceflow/commit/b38fb9ba56200ced1baf563b097faa8717693783))
|
||||
- **ci-shell:** released version 0.0.1 [no ci] ([ec2a84b](https://git.bjxgj.com/xgj/spaceflow/commit/ec2a84b298c5fb989951caf42e2b016b3336f6a0))
|
||||
- **core:** released version 0.0.1 [no ci] ([66497d6](https://git.bjxgj.com/xgj/spaceflow/commit/66497d60be04b4756a3362dbec4652177910165c))
|
||||
- 重置所有包版本至 0.0.0 并清理 CHANGELOG 文件 ([f7efaf9](https://git.bjxgj.com/xgj/spaceflow/commit/f7efaf967467f1272e05d645720ee63941fe79be))
|
||||
27
commands/period-summary/README.md
Normal file
27
commands/period-summary/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# @spaceflow/period-summary
|
||||
|
||||
Spaceflow 周期统计命令,根据时间范围统计 PR 贡献情况,按人员汇总并排序。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **时间范围统计**:支持自定义日期范围或预设时间段
|
||||
- **多维度统计**:PR 数量、代码行数、变更文件数、问题数量
|
||||
- **综合评分**:根据代码量和问题数量权衡计算分数
|
||||
- **按人员排序**:按综合分数降序排列贡献者
|
||||
- **多种输出方式**:控制台、GitHub Issue、Markdown 文件
|
||||
|
||||
## 使用方法
|
||||
|
||||
```bash
|
||||
# 使用时间预设 - 统计本周
|
||||
spaceflow period-summary -p this-week
|
||||
|
||||
# 使用时间预设 - 统计上月
|
||||
spaceflow period-summary -p last-month
|
||||
|
||||
# 自定义日期范围
|
||||
spaceflow period-summary -s 2026-01-01 -u 2026-01-18
|
||||
|
||||
# 输出到 GitHub Issue
|
||||
spaceflow period-summary -p last-week -o issue -c
|
||||
```
|
||||
25
commands/period-summary/package.json
Normal file
25
commands/period-summary/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@spaceflow/period-summary",
|
||||
"version": "0.19.0",
|
||||
"description": "Spaceflow 周期统计命令,根据时间范围统计 PR 贡献情况,按人员汇总并排序",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"build": "spaceflow build",
|
||||
"dev": "spaceflow dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spaceflow/cli": "workspace:*",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "catalog:",
|
||||
"@nestjs/config": "catalog:",
|
||||
"@spaceflow/core": "workspace:*",
|
||||
"nest-commander": "catalog:"
|
||||
},
|
||||
"spaceflow": {
|
||||
"type": "flow",
|
||||
"entry": "."
|
||||
}
|
||||
}
|
||||
25
commands/period-summary/src/index.ts
Normal file
25
commands/period-summary/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import "./locales";
|
||||
import { SpaceflowExtension, SpaceflowExtensionMetadata, t } from "@spaceflow/core";
|
||||
import { PeriodSummaryModule } from "./period-summary.module";
|
||||
export class PeriodSummaryExtension implements SpaceflowExtension {
|
||||
getMetadata(): SpaceflowExtensionMetadata {
|
||||
return {
|
||||
name: "period-summary",
|
||||
commands: ["period-summary"],
|
||||
configKey: "period-summary",
|
||||
version: "1.0.0",
|
||||
description: t("period-summary:extensionDescription"),
|
||||
};
|
||||
}
|
||||
|
||||
getModule() {
|
||||
return PeriodSummaryModule;
|
||||
}
|
||||
}
|
||||
|
||||
export default PeriodSummaryExtension;
|
||||
|
||||
export * from "./period-summary.command";
|
||||
export * from "./period-summary.service";
|
||||
export * from "./period-summary.module";
|
||||
export * from "./types";
|
||||
13
commands/period-summary/src/locales/en/period-summary.json
Normal file
13
commands/period-summary/src/locales/en/period-summary.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Period summary - aggregate PR contributions by time range, grouped by author",
|
||||
"options.preset": "Time preset: this-week, last-week, this-month, last-month, last-7-days, last-15-days, last-30-days",
|
||||
"options.since": "Start date (YYYY-MM-DD format), alternative to --preset",
|
||||
"options.until": "End date (YYYY-MM-DD format), defaults to today",
|
||||
"options.repository": "Repository path (owner/repo format), auto-detected from env by default",
|
||||
"options.format": "Output format: table, json, markdown",
|
||||
"options.output": "Output target: console, issue (create GitHub Issue), file (Markdown file)",
|
||||
"options.outputFile": "Output file path (used when --output is file)",
|
||||
"options.verbose": "Verbose log level (1: process logs, 2: detailed logs)",
|
||||
"reportOutput": "✅ Report output to: {{location}}",
|
||||
"extensionDescription": "Period summary - aggregate PR contributions by time range, grouped by author"
|
||||
}
|
||||
11
commands/period-summary/src/locales/index.ts
Normal file
11
commands/period-summary/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { addLocaleResources } from "@spaceflow/core";
|
||||
import zhCN from "./zh-cn/period-summary.json";
|
||||
import en from "./en/period-summary.json";
|
||||
|
||||
/** period-summary 命令 i18n 资源 */
|
||||
export const periodSummaryLocales: Record<string, Record<string, string>> = {
|
||||
"zh-CN": zhCN,
|
||||
en,
|
||||
};
|
||||
|
||||
addLocaleResources("period-summary", periodSummaryLocales);
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "周期统计命令,根据时间范围统计 PR 贡献情况,按人员汇总并排序",
|
||||
"options.preset": "时间预设: this-week(本周), last-week(上周), this-month(本月), last-month(上月), last-7-days, last-15-days, last-30-days",
|
||||
"options.since": "开始日期 (YYYY-MM-DD 格式),与 --preset 二选一",
|
||||
"options.until": "结束日期 (YYYY-MM-DD 格式),默认为今天",
|
||||
"options.repository": "仓库路径 (owner/repo 格式),默认从环境变量获取",
|
||||
"options.format": "输出格式: table, json, markdown",
|
||||
"options.output": "输出目标: console(控制台), issue(创建 GitHub Issue), file(Markdown 文件)",
|
||||
"options.outputFile": "输出文件路径(当 --output 为 file 时使用)",
|
||||
"options.verbose": "详细日志级别 (1: 过程日志, 2: 详细日志)",
|
||||
"reportOutput": "✅ 报告已输出到: {{location}}",
|
||||
"extensionDescription": "周期统计命令,根据时间范围统计 PR 贡献情况,按人员汇总并排序"
|
||||
}
|
||||
128
commands/period-summary/src/period-summary.command.ts
Normal file
128
commands/period-summary/src/period-summary.command.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Command, CommandRunner, Option } from "nest-commander";
|
||||
import { t, normalizeVerbose } from "@spaceflow/core";
|
||||
import { PeriodSummaryService } from "./period-summary.service";
|
||||
import type { PeriodSummaryOptions, OutputTarget, TimePreset } from "./types";
|
||||
import type { VerboseLevel } from "@spaceflow/core";
|
||||
|
||||
/**
|
||||
* 周期统计命令
|
||||
*
|
||||
* 根据时间范围统计 PR 贡献情况,按人员汇总并排序
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo 格式)
|
||||
*/
|
||||
@Command({
|
||||
name: "period-summary",
|
||||
description: t("period-summary:description"),
|
||||
})
|
||||
export class PeriodSummaryCommand extends CommandRunner {
|
||||
constructor(protected readonly periodSummaryService: PeriodSummaryService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(_passedParams: string[], options: PeriodSummaryOptions): Promise<void> {
|
||||
try {
|
||||
const context = this.periodSummaryService.getContextFromOptions(options);
|
||||
const result = await this.periodSummaryService.execute(context);
|
||||
const outputResult = await this.periodSummaryService.outputReport(context, result);
|
||||
if (outputResult.type !== "console" && outputResult.location) {
|
||||
console.log(t("period-summary:reportOutput", { location: outputResult.location }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
t("common.executionFailed", { error: error instanceof Error ? error.message : error }),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-p, --preset <preset>",
|
||||
description: t("period-summary:options.preset"),
|
||||
choices: [
|
||||
"this-week",
|
||||
"last-week",
|
||||
"this-month",
|
||||
"last-month",
|
||||
"last-7-days",
|
||||
"last-15-days",
|
||||
"last-30-days",
|
||||
],
|
||||
})
|
||||
parsePreset(val: string): TimePreset {
|
||||
return val as TimePreset;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-s, --since <date>",
|
||||
description: t("period-summary:options.since"),
|
||||
})
|
||||
parseSince(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-u, --until <date>",
|
||||
description: t("period-summary:options.until"),
|
||||
})
|
||||
parseUntil(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-r, --repository <repo>",
|
||||
description: t("period-summary:options.repository"),
|
||||
})
|
||||
parseRepository(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-c, --ci",
|
||||
description: t("common.options.ci"),
|
||||
})
|
||||
parseCi(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-f, --format <format>",
|
||||
description: t("period-summary:options.format"),
|
||||
choices: ["table", "json", "markdown"],
|
||||
})
|
||||
parseFormat(val: string): "table" | "json" | "markdown" {
|
||||
return val as "table" | "json" | "markdown";
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-o, --output <target>",
|
||||
description: t("period-summary:options.output"),
|
||||
choices: ["console", "issue", "file"],
|
||||
defaultValue: "console",
|
||||
})
|
||||
parseOutput(val: string): OutputTarget {
|
||||
return val as OutputTarget;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--output-file <path>",
|
||||
description: t("period-summary:options.outputFile"),
|
||||
})
|
||||
parseOutputFile(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-v, --verbose [level]",
|
||||
description: t("period-summary:options.verbose"),
|
||||
})
|
||||
parseVerbose(val: string | boolean): VerboseLevel {
|
||||
if (val === true || val === "") {
|
||||
return 1;
|
||||
}
|
||||
const level = parseInt(val as string, 10);
|
||||
return normalizeVerbose(level as VerboseLevel);
|
||||
}
|
||||
}
|
||||
11
commands/period-summary/src/period-summary.module.ts
Normal file
11
commands/period-summary/src/period-summary.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { GitProviderModule, ciConfig } from "@spaceflow/core";
|
||||
import { PeriodSummaryCommand } from "./period-summary.command";
|
||||
import { PeriodSummaryService } from "./period-summary.service";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forFeature(ciConfig), GitProviderModule.forFeature()],
|
||||
providers: [PeriodSummaryCommand, PeriodSummaryService],
|
||||
})
|
||||
export class PeriodSummaryModule {}
|
||||
530
commands/period-summary/src/period-summary.service.ts
Normal file
530
commands/period-summary/src/period-summary.service.ts
Normal file
@@ -0,0 +1,530 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { GitProviderService, shouldLog, normalizeVerbose } from "@spaceflow/core";
|
||||
import type { PullRequest, Issue, CiConfig } from "@spaceflow/core";
|
||||
import type {
|
||||
PeriodSummaryOptions,
|
||||
PeriodSummaryContext,
|
||||
PeriodSummaryResult,
|
||||
PrStats,
|
||||
UserStats,
|
||||
OutputTarget,
|
||||
TimePreset,
|
||||
} from "./types";
|
||||
|
||||
/** 分数权重配置 */
|
||||
const SCORE_WEIGHTS = {
|
||||
/** 每个 PR 的基础分 */
|
||||
prBase: 10,
|
||||
/** 每 100 行新增代码的分数 */
|
||||
additionsPer100: 2,
|
||||
/** 每 100 行删除代码的分数 */
|
||||
deletionsPer100: 1,
|
||||
/** 每个变更文件的分数 */
|
||||
changedFile: 0.5,
|
||||
/** 每个未修复问题的扣分 */
|
||||
issueDeduction: 3,
|
||||
/** 每个已修复问题的加分 */
|
||||
fixedBonus: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* 周期统计服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class PeriodSummaryService {
|
||||
constructor(
|
||||
protected readonly gitProvider: GitProviderService,
|
||||
protected readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 从配置和选项获取执行上下文
|
||||
*/
|
||||
getContextFromOptions(options: PeriodSummaryOptions): PeriodSummaryContext {
|
||||
let owner: string;
|
||||
let repo: string;
|
||||
if (options.repository) {
|
||||
const parts = options.repository.split("/");
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(`仓库格式不正确,期望 "owner/repo",实际: "${options.repository}"`);
|
||||
}
|
||||
owner = parts[0];
|
||||
repo = parts[1];
|
||||
} else {
|
||||
const ciConf = this.configService.get<CiConfig>("ci");
|
||||
const repository = ciConf?.repository;
|
||||
if (!repository) {
|
||||
throw new Error("缺少仓库配置,请通过 --repository 参数或环境变量 GITHUB_REPOSITORY 指定");
|
||||
}
|
||||
const parts = repository.split("/");
|
||||
owner = parts[0];
|
||||
repo = parts[1];
|
||||
}
|
||||
if (options.ci) {
|
||||
this.gitProvider.validateConfig();
|
||||
}
|
||||
const { since, until } = this.resolveDateRange(options);
|
||||
if (since > until) {
|
||||
throw new Error("开始日期不能晚于结束日期");
|
||||
}
|
||||
const output: OutputTarget = options.output ?? "console";
|
||||
if (output === "issue" && !options.ci) {
|
||||
this.gitProvider.validateConfig();
|
||||
}
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
since,
|
||||
until,
|
||||
format: options.format ?? (output === "console" ? "table" : "markdown"),
|
||||
output,
|
||||
outputFile: options.outputFile,
|
||||
verbose: normalizeVerbose(options.verbose),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行周期统计
|
||||
*/
|
||||
async execute(context: PeriodSummaryContext): Promise<PeriodSummaryResult> {
|
||||
const { owner, repo, since, until, verbose } = context;
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(`📊 开始统计 ${owner}/${repo} 的 PR 数据...`);
|
||||
console.log(`📅 时间范围: ${this.formatDate(since)} ~ ${this.formatDate(until)}`);
|
||||
}
|
||||
const allPrs = await this.gitProvider.listAllPullRequests(owner, repo, { state: "closed" });
|
||||
const mergedPrs = allPrs.filter((pr) => {
|
||||
if (!pr.merged_at) return false;
|
||||
const mergedAt = new Date(pr.merged_at);
|
||||
return mergedAt >= since && mergedAt <= until;
|
||||
});
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(`📝 找到 ${mergedPrs.length} 个已合并的 PR`);
|
||||
}
|
||||
const prStatsList: PrStats[] = [];
|
||||
for (const pr of mergedPrs) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 处理 PR #${pr.number}: ${pr.title}`);
|
||||
}
|
||||
const stats = await this.collectPrStats(owner, repo, pr);
|
||||
prStatsList.push(stats);
|
||||
}
|
||||
const userStatsMap = this.aggregateByUser(prStatsList);
|
||||
const sortedUserStats = this.sortUserStats(userStatsMap);
|
||||
return {
|
||||
period: {
|
||||
since: this.formatDate(since),
|
||||
until: this.formatDate(until),
|
||||
},
|
||||
repository: `${owner}/${repo}`,
|
||||
totalPrs: mergedPrs.length,
|
||||
userStats: sortedUserStats,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集单个 PR 的统计数据
|
||||
*/
|
||||
protected async collectPrStats(owner: string, repo: string, pr: PullRequest): Promise<PrStats> {
|
||||
let additions = 0;
|
||||
let deletions = 0;
|
||||
let changedFiles = 0;
|
||||
try {
|
||||
const files = await this.gitProvider.getPullRequestFiles(owner, repo, pr.number!);
|
||||
changedFiles = files.length;
|
||||
for (const file of files) {
|
||||
additions += file.additions ?? 0;
|
||||
deletions += file.deletions ?? 0;
|
||||
}
|
||||
} catch {
|
||||
// 如果获取文件失败,使用默认值
|
||||
}
|
||||
const { issueCount, fixedCount } = await this.extractIssueStats(owner, repo, pr.number!);
|
||||
return {
|
||||
number: pr.number!,
|
||||
title: pr.title ?? "",
|
||||
author: pr.user?.login ?? "unknown",
|
||||
mergedAt: pr.merged_at ?? "",
|
||||
additions,
|
||||
deletions,
|
||||
changedFiles,
|
||||
issueCount,
|
||||
fixedCount,
|
||||
description: this.extractDescription(pr),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PR 评论中提取问题统计
|
||||
*/
|
||||
protected async extractIssueStats(
|
||||
owner: string,
|
||||
repo: string,
|
||||
prNumber: number,
|
||||
): Promise<{ issueCount: number; fixedCount: number }> {
|
||||
try {
|
||||
const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
|
||||
let issueCount = 0;
|
||||
let fixedCount = 0;
|
||||
for (const comment of comments) {
|
||||
const body = comment.body ?? "";
|
||||
const issueMatch = body.match(/发现\s*(\d+)\s*个问题/);
|
||||
if (issueMatch) {
|
||||
issueCount = Math.max(issueCount, parseInt(issueMatch[1], 10));
|
||||
}
|
||||
const fixedMatch = body.match(/已修复[::]\s*(\d+)/);
|
||||
if (fixedMatch) {
|
||||
fixedCount = Math.max(fixedCount, parseInt(fixedMatch[1], 10));
|
||||
}
|
||||
const statsMatch = body.match(/🔴\s*(\d+).*🟡\s*(\d+)/);
|
||||
if (statsMatch) {
|
||||
const errorCount = parseInt(statsMatch[1], 10);
|
||||
const warnCount = parseInt(statsMatch[2], 10);
|
||||
issueCount = Math.max(issueCount, errorCount + warnCount);
|
||||
}
|
||||
}
|
||||
return { issueCount, fixedCount };
|
||||
} catch {
|
||||
return { issueCount: 0, fixedCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PR 提取功能描述
|
||||
*/
|
||||
protected extractDescription(pr: PullRequest): string {
|
||||
if (pr.title) {
|
||||
return pr.title.replace(/^\[.*?\]\s*/, "").trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 按用户聚合统计数据
|
||||
*/
|
||||
protected aggregateByUser(prStatsList: PrStats[]): Map<string, UserStats> {
|
||||
const userMap = new Map<string, UserStats>();
|
||||
for (const pr of prStatsList) {
|
||||
let userStats = userMap.get(pr.author);
|
||||
if (!userStats) {
|
||||
userStats = {
|
||||
username: pr.author,
|
||||
prCount: 0,
|
||||
totalAdditions: 0,
|
||||
totalDeletions: 0,
|
||||
totalChangedFiles: 0,
|
||||
totalIssues: 0,
|
||||
totalFixed: 0,
|
||||
score: 0,
|
||||
features: [],
|
||||
prs: [],
|
||||
};
|
||||
userMap.set(pr.author, userStats);
|
||||
}
|
||||
userStats.prCount++;
|
||||
userStats.totalAdditions += pr.additions;
|
||||
userStats.totalDeletions += pr.deletions;
|
||||
userStats.totalChangedFiles += pr.changedFiles;
|
||||
userStats.totalIssues += pr.issueCount;
|
||||
userStats.totalFixed += pr.fixedCount;
|
||||
if (pr.description) {
|
||||
userStats.features.push(pr.description);
|
||||
}
|
||||
userStats.prs.push(pr);
|
||||
}
|
||||
for (const userStats of userMap.values()) {
|
||||
userStats.score = this.calculateScore(userStats);
|
||||
}
|
||||
return userMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算用户综合分数
|
||||
*/
|
||||
protected calculateScore(stats: UserStats): number {
|
||||
const prScore = stats.prCount * SCORE_WEIGHTS.prBase;
|
||||
const additionsScore = (stats.totalAdditions / 100) * SCORE_WEIGHTS.additionsPer100;
|
||||
const deletionsScore = (stats.totalDeletions / 100) * SCORE_WEIGHTS.deletionsPer100;
|
||||
const filesScore = stats.totalChangedFiles * SCORE_WEIGHTS.changedFile;
|
||||
const unfixedIssues = stats.totalIssues - stats.totalFixed;
|
||||
const issueDeduction = unfixedIssues * SCORE_WEIGHTS.issueDeduction;
|
||||
const fixedBonus = stats.totalFixed * SCORE_WEIGHTS.fixedBonus;
|
||||
const totalScore =
|
||||
prScore + additionsScore + deletionsScore + filesScore - issueDeduction + fixedBonus;
|
||||
return Math.max(0, Math.round(totalScore * 10) / 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分数排序用户统计
|
||||
*/
|
||||
protected sortUserStats(userMap: Map<string, UserStats>): UserStats[] {
|
||||
return Array.from(userMap.values()).sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串
|
||||
*/
|
||||
protected parseDate(dateStr: string, fieldName: string): Date {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error(`${fieldName}格式不正确: "${dateStr}",请使用 YYYY-MM-DD 格式`);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据预设或手动输入解析日期范围
|
||||
*/
|
||||
protected resolveDateRange(options: PeriodSummaryOptions): { since: Date; until: Date } {
|
||||
if (options.preset) {
|
||||
return this.resolvePresetDateRange(options.preset);
|
||||
}
|
||||
if (!options.since) {
|
||||
throw new Error("请指定 --since 参数或使用 --preset 预设时间范围");
|
||||
}
|
||||
const since = this.parseDate(options.since, "开始日期");
|
||||
const until = options.until ? this.parseDate(options.until, "结束日期") : new Date();
|
||||
until.setHours(23, 59, 59, 999);
|
||||
return { since, until };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据预设解析日期范围
|
||||
*/
|
||||
protected resolvePresetDateRange(preset: TimePreset): { since: Date; until: Date } {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
let since: Date;
|
||||
let until: Date;
|
||||
switch (preset) {
|
||||
case "this-week": {
|
||||
const dayOfWeek = today.getDay();
|
||||
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
since = new Date(today);
|
||||
since.setDate(today.getDate() + mondayOffset);
|
||||
until = new Date(today);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
}
|
||||
case "last-week": {
|
||||
const dayOfWeek = today.getDay();
|
||||
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
until = new Date(today);
|
||||
until.setDate(today.getDate() + mondayOffset - 1);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
since = new Date(until);
|
||||
since.setDate(until.getDate() - 6);
|
||||
since.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
}
|
||||
case "this-month": {
|
||||
since = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
until = new Date(today);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
}
|
||||
case "last-month": {
|
||||
since = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||
until = new Date(today.getFullYear(), today.getMonth(), 0);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
}
|
||||
case "last-7-days": {
|
||||
since = new Date(today);
|
||||
since.setDate(today.getDate() - 6);
|
||||
until = new Date(today);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
}
|
||||
case "last-15-days": {
|
||||
since = new Date(today);
|
||||
since.setDate(today.getDate() - 14);
|
||||
until = new Date(today);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
}
|
||||
case "last-30-days": {
|
||||
since = new Date(today);
|
||||
since.setDate(today.getDate() - 29);
|
||||
until = new Date(today);
|
||||
until.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`未知的时间预设: ${preset}`);
|
||||
}
|
||||
return { since, until };
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期为 YYYY-MM-DD
|
||||
*/
|
||||
protected formatDate(date: Date): string {
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化输出结果
|
||||
*/
|
||||
formatOutput(result: PeriodSummaryResult, format: "table" | "json" | "markdown"): string {
|
||||
switch (format) {
|
||||
case "json":
|
||||
return JSON.stringify(result, null, 2);
|
||||
case "markdown":
|
||||
return this.formatMarkdown(result);
|
||||
case "table":
|
||||
default:
|
||||
return this.formatTable(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为表格输出
|
||||
*/
|
||||
protected formatTable(result: PeriodSummaryResult): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("");
|
||||
lines.push(`📊 周期统计报告`);
|
||||
lines.push(`${"─".repeat(60)}`);
|
||||
lines.push(`📦 仓库: ${result.repository}`);
|
||||
lines.push(`📅 周期: ${result.period.since} ~ ${result.period.until}`);
|
||||
lines.push(`📝 合并 PR 数: ${result.totalPrs}`);
|
||||
lines.push("");
|
||||
lines.push(`🏆 贡献者排名`);
|
||||
lines.push(`${"─".repeat(60)}`);
|
||||
const header = [
|
||||
"排名".padEnd(4),
|
||||
"用户".padEnd(15),
|
||||
"PR数".padStart(5),
|
||||
"新增".padStart(8),
|
||||
"删除".padStart(8),
|
||||
"问题".padStart(5),
|
||||
"分数".padStart(8),
|
||||
].join(" │ ");
|
||||
lines.push(header);
|
||||
lines.push("─".repeat(60));
|
||||
result.userStats.forEach((user, index) => {
|
||||
const row = [
|
||||
`#${index + 1}`.padEnd(4),
|
||||
user.username.slice(0, 15).padEnd(15),
|
||||
String(user.prCount).padStart(5),
|
||||
`+${user.totalAdditions}`.padStart(8),
|
||||
`-${user.totalDeletions}`.padStart(8),
|
||||
String(user.totalIssues).padStart(5),
|
||||
user.score.toFixed(1).padStart(8),
|
||||
].join(" │ ");
|
||||
lines.push(row);
|
||||
});
|
||||
lines.push("─".repeat(60));
|
||||
lines.push("");
|
||||
lines.push(`📋 功能摘要`);
|
||||
lines.push(`${"─".repeat(60)}`);
|
||||
for (const user of result.userStats) {
|
||||
if (user.features.length > 0) {
|
||||
lines.push(`\n👤 ${user.username}:`);
|
||||
for (const feature of user.features) {
|
||||
lines.push(` • ${feature}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为 Markdown 输出
|
||||
*/
|
||||
protected formatMarkdown(result: PeriodSummaryResult): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`# 📊 周期统计报告`);
|
||||
lines.push("");
|
||||
lines.push(`- **仓库**: ${result.repository}`);
|
||||
lines.push(`- **周期**: ${result.period.since} ~ ${result.period.until}`);
|
||||
lines.push(`- **合并 PR 数**: ${result.totalPrs}`);
|
||||
lines.push("");
|
||||
lines.push(`## 🏆 贡献者排名`);
|
||||
lines.push("");
|
||||
lines.push(`| 排名 | 用户 | PR数 | 新增 | 删除 | 问题 | 分数 |`);
|
||||
lines.push(`|------|------|------|------|------|------|------|`);
|
||||
result.userStats.forEach((user, index) => {
|
||||
lines.push(
|
||||
`| #${index + 1} | ${user.username} | ${user.prCount} | +${user.totalAdditions} | -${user.totalDeletions} | ${user.totalIssues} | ${user.score.toFixed(1)} |`,
|
||||
);
|
||||
});
|
||||
lines.push("");
|
||||
lines.push(`## 📋 功能摘要`);
|
||||
lines.push("");
|
||||
for (const user of result.userStats) {
|
||||
if (user.features.length > 0) {
|
||||
lines.push(`### 👤 ${user.username}`);
|
||||
lines.push("");
|
||||
for (const feature of user.features) {
|
||||
lines.push(`- ${feature}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出报告到指定目标
|
||||
*/
|
||||
async outputReport(
|
||||
context: PeriodSummaryContext,
|
||||
result: PeriodSummaryResult,
|
||||
): Promise<{ type: OutputTarget; location?: string }> {
|
||||
const content = this.formatOutput(result, context.format);
|
||||
switch (context.output) {
|
||||
case "issue":
|
||||
return this.outputToIssue(context, result, content);
|
||||
case "file":
|
||||
return this.outputToFile(context, result, content);
|
||||
case "console":
|
||||
default:
|
||||
console.log(content);
|
||||
return { type: "console" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出报告到 GitHub Issue
|
||||
*/
|
||||
protected async outputToIssue(
|
||||
context: PeriodSummaryContext,
|
||||
result: PeriodSummaryResult,
|
||||
content: string,
|
||||
): Promise<{ type: OutputTarget; location: string }> {
|
||||
const title = `📊 周期统计报告: ${result.period.since} ~ ${result.period.until}`;
|
||||
const issue: Issue = await this.gitProvider.createIssue(context.owner, context.repo, {
|
||||
title,
|
||||
body: content,
|
||||
});
|
||||
const location = issue.html_url ?? `#${issue.number}`;
|
||||
if (shouldLog(context.verbose, 1)) {
|
||||
console.log(`✅ 已创建 Issue: ${location}`);
|
||||
}
|
||||
return { type: "issue", location };
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出报告到 Markdown 文件
|
||||
*/
|
||||
protected outputToFile(
|
||||
context: PeriodSummaryContext,
|
||||
result: PeriodSummaryResult,
|
||||
content: string,
|
||||
): { type: OutputTarget; location: string } {
|
||||
const filename =
|
||||
context.outputFile ?? `period-summary-${result.period.since}-${result.period.until}.md`;
|
||||
const filepath = join(process.cwd(), filename);
|
||||
writeFileSync(filepath, content, "utf-8");
|
||||
if (shouldLog(context.verbose, 1)) {
|
||||
console.log(`✅ 已保存到文件: ${filepath}`);
|
||||
}
|
||||
return { type: "file", location: filepath };
|
||||
}
|
||||
}
|
||||
115
commands/period-summary/src/types.ts
Normal file
115
commands/period-summary/src/types.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 周期统计命令类型定义
|
||||
*/
|
||||
|
||||
import type { VerboseLevel } from "@spaceflow/core";
|
||||
|
||||
/** 输出目标类型 */
|
||||
export type OutputTarget = "console" | "issue" | "file";
|
||||
|
||||
/** 时间预设类型 */
|
||||
export type TimePreset =
|
||||
| "this-week"
|
||||
| "last-week"
|
||||
| "this-month"
|
||||
| "last-month"
|
||||
| "last-7-days"
|
||||
| "last-15-days"
|
||||
| "last-30-days";
|
||||
|
||||
/** 命令选项 */
|
||||
export interface PeriodSummaryOptions {
|
||||
/** 开始日期 (YYYY-MM-DD) */
|
||||
since?: string;
|
||||
/** 结束日期 (YYYY-MM-DD),默认为今天 */
|
||||
until?: string;
|
||||
/** 时间预设,优先级高于 since/until */
|
||||
preset?: TimePreset;
|
||||
/** 仓库路径 owner/repo,默认从环境变量获取 */
|
||||
repository?: string;
|
||||
/** 是否在 CI 环境中运行 */
|
||||
ci?: boolean;
|
||||
/** 输出格式 */
|
||||
format?: "table" | "json" | "markdown";
|
||||
/** 输出目标:console(控制台)、issue(创建 GitHub Issue)、file(Markdown 文件) */
|
||||
output?: OutputTarget;
|
||||
/** 输出文件路径(当 output 为 file 时使用) */
|
||||
outputFile?: string;
|
||||
/** 详细日志级别 (0: 静默, 1: 过程日志, 2: 详细日志) */
|
||||
verbose?: VerboseLevel;
|
||||
}
|
||||
|
||||
/** 命令执行上下文 */
|
||||
export interface PeriodSummaryContext {
|
||||
owner: string;
|
||||
repo: string;
|
||||
since: Date;
|
||||
until: Date;
|
||||
format: "table" | "json" | "markdown";
|
||||
output: OutputTarget;
|
||||
outputFile?: string;
|
||||
verbose: VerboseLevel;
|
||||
}
|
||||
|
||||
/** 单个 PR 的统计数据 */
|
||||
export interface PrStats {
|
||||
/** PR 编号 */
|
||||
number: number;
|
||||
/** PR 标题 */
|
||||
title: string;
|
||||
/** 作者用户名 */
|
||||
author: string;
|
||||
/** 合并时间 */
|
||||
mergedAt: string;
|
||||
/** 新增行数 */
|
||||
additions: number;
|
||||
/** 删除行数 */
|
||||
deletions: number;
|
||||
/** 变更文件数 */
|
||||
changedFiles: number;
|
||||
/** 扫描发现的问题数 */
|
||||
issueCount: number;
|
||||
/** 已修复的问题数 */
|
||||
fixedCount: number;
|
||||
/** PR 描述/功能摘要 */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** 单个用户的统计数据 */
|
||||
export interface UserStats {
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** PR 数量 */
|
||||
prCount: number;
|
||||
/** 总新增行数 */
|
||||
totalAdditions: number;
|
||||
/** 总删除行数 */
|
||||
totalDeletions: number;
|
||||
/** 总变更文件数 */
|
||||
totalChangedFiles: number;
|
||||
/** 总问题数 */
|
||||
totalIssues: number;
|
||||
/** 总已修复问题数 */
|
||||
totalFixed: number;
|
||||
/** 综合分数 */
|
||||
score: number;
|
||||
/** 功能摘要列表 */
|
||||
features: string[];
|
||||
/** 该用户的 PR 列表 */
|
||||
prs: PrStats[];
|
||||
}
|
||||
|
||||
/** 周期统计结果 */
|
||||
export interface PeriodSummaryResult {
|
||||
/** 统计周期 */
|
||||
period: {
|
||||
since: string;
|
||||
until: string;
|
||||
};
|
||||
/** 仓库信息 */
|
||||
repository: string;
|
||||
/** 总 PR 数 */
|
||||
totalPrs: number;
|
||||
/** 按用户统计(已排序) */
|
||||
userStats: UserStats[];
|
||||
}
|
||||
5
commands/period-summary/tsconfig.json
Normal file
5
commands/period-summary/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../core/tsconfig.skill.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
503
commands/publish/CHANGELOG.md
Normal file
503
commands/publish/CHANGELOG.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# Changelog
|
||||
|
||||
## [0.21.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.20.0...@spaceflow/publish@0.21.0) (2026-02-15)
|
||||
|
||||
### 新特性
|
||||
|
||||
* **cli:** 新增 MCP Server 命令并集成 review 扩展的 MCP 工具 ([b794b36](https://git.bjxgj.com/xgj/spaceflow/commit/b794b36d90788c7eb4cbb253397413b4a080ae83))
|
||||
* **cli:** 新增 MCP Server 导出类型支持 ([9568cbd](https://git.bjxgj.com/xgj/spaceflow/commit/9568cbd14d4cfbdedaf2218379c72337af6db271))
|
||||
* **core:** 为所有命令添加 i18n 国际化支持 ([867c5d3](https://git.bjxgj.com/xgj/spaceflow/commit/867c5d3eccc285c8a68803b8aa2f0ffb86a94285))
|
||||
* **core:** 新增 GitLab 平台适配器并完善配置支持 ([47be9ad](https://git.bjxgj.com/xgj/spaceflow/commit/47be9adfa90944a9cb183e03286a7a96fec747f1))
|
||||
* **core:** 新增 Logger 全局日志工具并支持 plain/tui 双模式渲染 ([8baae7c](https://git.bjxgj.com/xgj/spaceflow/commit/8baae7c24139695a0e379e1c874023cd61dfc41b))
|
||||
* **docs:** 新增 VitePress 文档站点并完善项目文档 ([a79d620](https://git.bjxgj.com/xgj/spaceflow/commit/a79d6208e60390a44fa4c94621eb41ae20159e98))
|
||||
* **mcp:** 新增 MCP Inspector 交互式调试支持并优化工具日志输出 ([05fd2ee](https://git.bjxgj.com/xgj/spaceflow/commit/05fd2ee941c5f6088b769d1127cb7c0615626f8c))
|
||||
* **review:** 为 MCP 服务添加 i18n 国际化支持 ([a749054](https://git.bjxgj.com/xgj/spaceflow/commit/a749054eb73b775a5f5973ab1b86c04f2b2ddfba))
|
||||
* **review:** 新增规则级 includes 解析测试并修复文件级/规则级 includes 过滤逻辑 ([4baca71](https://git.bjxgj.com/xgj/spaceflow/commit/4baca71c17782fb92a95b3207f9c61e0b410b9ff))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
* **actions:** 修正 pnpm setup 命令调用方式 ([8f014fa](https://git.bjxgj.com/xgj/spaceflow/commit/8f014fa90b74e20de4c353804d271b3ef6f1288f))
|
||||
* **mcp:** 添加 -y 选项确保 Inspector 自动安装依赖 ([a9201f7](https://git.bjxgj.com/xgj/spaceflow/commit/a9201f74bd9ddc5eba92beaaa676f377842863e0))
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **claude:** 移除 .claude 目录及其 .gitignore 配置文件 ([91916a9](https://git.bjxgj.com/xgj/spaceflow/commit/91916a99f65da31c1d34e6f75b5cbea1d331ba35))
|
||||
* **cli:** 优化依赖安装流程并支持 .spaceflow 目录配置 ([5977631](https://git.bjxgj.com/xgj/spaceflow/commit/597763183eaa61bb024bba2703d75239650b54fb))
|
||||
* **cli:** 拆分 CLI 为独立包并重构扩展加载机制 ([b385d28](https://git.bjxgj.com/xgj/spaceflow/commit/b385d281575f29b823bb6dc4229a396a29c0e226))
|
||||
* **cli:** 移除 ExtensionModule 并优化扩展加载机制 ([8f7077d](https://git.bjxgj.com/xgj/spaceflow/commit/8f7077deaef4e5f4032662ff5ac925cd3c07fdb6))
|
||||
* **cli:** 调整依赖顺序并格式化导入语句 ([32a9c1c](https://git.bjxgj.com/xgj/spaceflow/commit/32a9c1cf834725a20f93b1f8f60b52692841a3e5))
|
||||
* **cli:** 重构 getPluginConfigFromPackageJson 方法以提高代码可读性 ([f5f6ed9](https://git.bjxgj.com/xgj/spaceflow/commit/f5f6ed9858cc4ca670e30fac469774bdc8f7b005))
|
||||
* **cli:** 重构扩展配置格式,支持 flow/command/skill 三种导出类型 ([958dc13](https://git.bjxgj.com/xgj/spaceflow/commit/958dc130621f78bbcc260224da16a5f16ae0b2b1))
|
||||
* **core:** 为 build/clear/commit 命令添加国际化支持 ([de82cb2](https://git.bjxgj.com/xgj/spaceflow/commit/de82cb2f1ed8cef0e446a2d42a1bf1f091e9c421))
|
||||
* **core:** 优化 list 命令输出格式并修复 MCP Inspector 包管理器兼容性 ([a019829](https://git.bjxgj.com/xgj/spaceflow/commit/a019829d3055c083aeb86ed60ce6629d13012d91))
|
||||
* **core:** 将 rspack 配置和工具函数中的 @spaceflow/cli 引用改为 @spaceflow/core ([3c301c6](https://git.bjxgj.com/xgj/spaceflow/commit/3c301c60f3e61b127db94481f5a19307f5ef00eb))
|
||||
* **core:** 将扩展依赖从 @spaceflow/cli 迁移到 @spaceflow/core ([6f9ffd4](https://git.bjxgj.com/xgj/spaceflow/commit/6f9ffd4061cecae4faaf3d051e3ca98a0b42b01f))
|
||||
* **core:** 提取 source 处理和包管理器工具函数到共享模块 ([ab3ff00](https://git.bjxgj.com/xgj/spaceflow/commit/ab3ff003d1cd586c0c4efc7841e6a93fe3477ace))
|
||||
* **core:** 新增 getEnvFilePaths 工具函数统一管理 .env 文件路径优先级 ([809fa18](https://git.bjxgj.com/xgj/spaceflow/commit/809fa18f3d0b8eabcb068988bab53d548eaf03ea))
|
||||
* **core:** 新增远程仓库规则拉取功能并支持 Git API 获取目录内容 ([69ade16](https://git.bjxgj.com/xgj/spaceflow/commit/69ade16c9069f9e1a90b3ef56dc834e33a3c0650))
|
||||
* **core:** 统一 LogLevel 类型定义并支持字符串/数字双模式 ([557f6b0](https://git.bjxgj.com/xgj/spaceflow/commit/557f6b0bc39fcfb0e3f773836cbbf08c1a8790ae))
|
||||
* **core:** 重构配置读取逻辑,新增 ConfigReaderService 并支持 .spaceflowrc 配置文件 ([72e88ce](https://git.bjxgj.com/xgj/spaceflow/commit/72e88ced63d03395923cdfb113addf4945162e54))
|
||||
* **i18n:** 将 locales 导入从命令文件迁移至扩展入口文件 ([0da5d98](https://git.bjxgj.com/xgj/spaceflow/commit/0da5d9886296c4183b24ad8c56140763f5a870a4))
|
||||
* **i18n:** 移除扩展元数据中的 locales 字段并改用 side-effect 自动注册 ([2c7d488](https://git.bjxgj.com/xgj/spaceflow/commit/2c7d488a9dfa59a99b95e40e3c449c28c2d433d8))
|
||||
* **mcp:** 使用 DTO + Swagger 装饰器替代手动 JSON Schema 定义 ([87ec262](https://git.bjxgj.com/xgj/spaceflow/commit/87ec26252dd295536bb090ae8b7e418eec96e1bd))
|
||||
* **mcp:** 升级 MCP SDK API 并优化 Inspector 调试配置 ([176d04a](https://git.bjxgj.com/xgj/spaceflow/commit/176d04a73fbbb8d115520d922f5fedb9a2961aa6))
|
||||
* **mcp:** 将 MCP 元数据存储从 Reflect Metadata 改为静态属性以支持跨模块访问 ([cac0ea2](https://git.bjxgj.com/xgj/spaceflow/commit/cac0ea2029e1b504bc4278ce72b3aa87fff88c84))
|
||||
* **test:** 迁移测试框架从 Jest 到 Vitest ([308f9d4](https://git.bjxgj.com/xgj/spaceflow/commit/308f9d49089019530588344a5e8880f5b6504a6a))
|
||||
* 优化构建流程并调整 MCP/review 日志输出级别 ([74072c0](https://git.bjxgj.com/xgj/spaceflow/commit/74072c04be7a45bfc0ab53b636248fe5c0e1e42a))
|
||||
* 将 .spaceflow/package.json 纳入版本控制并自动添加到根项目依赖 ([ab83d25](https://git.bjxgj.com/xgj/spaceflow/commit/ab83d2579cb5414ee3d78a9768fac2147a3d1ad9))
|
||||
* 将 GiteaSdkModule/GiteaSdkService 重命名为 GitProviderModule/GitProviderService ([462f492](https://git.bjxgj.com/xgj/spaceflow/commit/462f492bc2607cf508c5011d181c599cf17e00c9))
|
||||
* 恢复 pnpm catalog 配置并移除 .spaceflow 工作区导入器 ([217387e](https://git.bjxgj.com/xgj/spaceflow/commit/217387e2e8517a08162e9bcaf604893fd9bca736))
|
||||
* 迁移扩展依赖到 .spaceflow 工作区并移除 pnpm catalog ([c457c0f](https://git.bjxgj.com/xgj/spaceflow/commit/c457c0f8918171f1856b88bc007921d76c508335))
|
||||
* 重构 Extension 安装机制为 pnpm workspace 模式 ([469b12e](https://git.bjxgj.com/xgj/spaceflow/commit/469b12eac28f747b628e52a5125a3d5a538fba39))
|
||||
* 重构插件加载改为扩展模式 ([0e6e140](https://git.bjxgj.com/xgj/spaceflow/commit/0e6e140b19ea2cf6084afc261c555d2083fe04f9))
|
||||
|
||||
### 文档更新
|
||||
|
||||
* **guide:** 更新编辑器集成文档,补充四种导出类型说明和 MCP 注册机制 ([19a7409](https://git.bjxgj.com/xgj/spaceflow/commit/19a7409092c89d002f11ee51ebcb6863118429bd))
|
||||
* **guide:** 更新配置文件位置说明并补充 RC 文件支持 ([2214dc4](https://git.bjxgj.com/xgj/spaceflow/commit/2214dc4e197221971f5286b38ceaa6fcbcaa7884))
|
||||
|
||||
### 测试用例
|
||||
|
||||
* **core:** 新增 GiteaAdapter 完整单元测试并实现自动检测 provider 配置 ([c74f745](https://git.bjxgj.com/xgj/spaceflow/commit/c74f7458aed91ac7d12fb57ef1c24b3d2917c406))
|
||||
* **review:** 新增 DeletionImpactService 测试覆盖并配置 coverage 工具 ([50bfbfe](https://git.bjxgj.com/xgj/spaceflow/commit/50bfbfe37192641f1170ade8f5eb00e0e382af67))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.19.0 [no ci] ([9f747c6](https://git.bjxgj.com/xgj/spaceflow/commit/9f747c617b387e105e92b4a5dcd0f5d3cf51c26d))
|
||||
* **ci-shell:** released version 0.19.0 [no ci] ([59ac30d](https://git.bjxgj.com/xgj/spaceflow/commit/59ac30da6802a9493c33e560ea9121d378597e89))
|
||||
* **ci:** 迁移工作流从 Gitea 到 GitHub 并统一环境变量命名 ([57e3bae](https://git.bjxgj.com/xgj/spaceflow/commit/57e3bae635b324c8c4ea50a9fb667b6241fae0ef))
|
||||
* **cli:** released version 0.19.0 [no ci] ([6b63149](https://git.bjxgj.com/xgj/spaceflow/commit/6b631499e2407a1822395d5f40cec2d725331b78))
|
||||
* **config:** 将 git 推送白名单用户从 "Gitea Actions" 改为 "GiteaActions" ([fdbb865](https://git.bjxgj.com/xgj/spaceflow/commit/fdbb865341e6f02b26fca32b54a33b51bee11cad))
|
||||
* **config:** 将 git 推送白名单用户从 github-actions[bot] 改为 Gitea Actions ([9c39819](https://git.bjxgj.com/xgj/spaceflow/commit/9c39819a9f95f415068f7f0333770b92bc98321b))
|
||||
* **config:** 移除 review-spec 私有仓库依赖 ([8ae18f1](https://git.bjxgj.com/xgj/spaceflow/commit/8ae18f13c441b033d1cbc75119695a5cc5cb6a0b))
|
||||
* **core:** released version 0.1.0 [no ci] ([170fa67](https://git.bjxgj.com/xgj/spaceflow/commit/170fa670e98473c2377120656d23aae835c51997))
|
||||
* **core:** 禁用 i18next 初始化时的 locize.com 推广日志 ([a99fbb0](https://git.bjxgj.com/xgj/spaceflow/commit/a99fbb068441bc623efcf15a1dd7b6bd38c05f38))
|
||||
* **deps:** 移除 pnpm catalog 配置并更新依赖锁定 ([753fb9e](https://git.bjxgj.com/xgj/spaceflow/commit/753fb9e3e43b28054c75158193dc39ab4bab1af5))
|
||||
* **docs:** 统一文档脚本命名,为 VitePress 命令添加 docs: 前缀 ([3cc46ea](https://git.bjxgj.com/xgj/spaceflow/commit/3cc46eab3a600290f5064b8270902e586b9c5af4))
|
||||
* **i18n:** 配置 i18n-ally-next 自动提取键名生成策略 ([753c3dc](https://git.bjxgj.com/xgj/spaceflow/commit/753c3dc3f24f3c03c837d1ec2c505e8e3ce08b11))
|
||||
* **i18n:** 重构 i18n 配置并统一 locales 目录结构 ([3e94037](https://git.bjxgj.com/xgj/spaceflow/commit/3e94037fa6493b3b0e4a12ff6af9f4bea48ae217))
|
||||
* **period-summary:** released version 0.19.0 [no ci] ([b833948](https://git.bjxgj.com/xgj/spaceflow/commit/b83394888ac47ae8d91bfd9317980f56bd322b34))
|
||||
* **review:** released version 0.28.0 [no ci] ([a2d89ed](https://git.bjxgj.com/xgj/spaceflow/commit/a2d89ed5f386eb6dd299c0d0a208856ce267ab5e))
|
||||
* **scripts:** 修正 setup 和 build 脚本的过滤条件,避免重复构建 cli 包 ([ffd2ffe](https://git.bjxgj.com/xgj/spaceflow/commit/ffd2ffedca08fd56cccb6a9fbd2b6bd106e367b6))
|
||||
* **templates:** 新增 MCP 工具插件模板 ([5f6df60](https://git.bjxgj.com/xgj/spaceflow/commit/5f6df60b60553f025414fd102d8a279cde097485))
|
||||
* **workflows:** 为所有 GitHub Actions 工作流添加 GIT_PROVIDER_TYPE 环境变量 ([a463574](https://git.bjxgj.com/xgj/spaceflow/commit/a463574de6755a0848a8d06267f029cb947132b0))
|
||||
* **workflows:** 在发布流程中添加 GIT_PROVIDER_TYPE 环境变量 ([a4bb388](https://git.bjxgj.com/xgj/spaceflow/commit/a4bb3881f39ad351e06c5502df6895805b169a28))
|
||||
* **workflows:** 在发布流程中添加扩展安装步骤 ([716be4d](https://git.bjxgj.com/xgj/spaceflow/commit/716be4d92641ccadb3eaf01af8a51189ec5e9ade))
|
||||
* **workflows:** 将发布流程的 Git 和 NPM 配置从 GitHub 迁移到 Gitea ([6d9acff](https://git.bjxgj.com/xgj/spaceflow/commit/6d9acff06c9a202432eb3d3d5552e6ac972712f5))
|
||||
* **workflows:** 将发布流程的 GITHUB_TOKEN 改为使用 CI_GITEA_TOKEN ([e7fe7b4](https://git.bjxgj.com/xgj/spaceflow/commit/e7fe7b4271802fcdbfc2553b180f710eed419335))
|
||||
* 为所有 commands 包添加 @spaceflow/cli 开发依赖 ([d4e6c83](https://git.bjxgj.com/xgj/spaceflow/commit/d4e6c8344ca736f7e55d7db698482e8fa2445684))
|
||||
* 优化依赖配置并移除 .spaceflow 包依赖 ([be5264e](https://git.bjxgj.com/xgj/spaceflow/commit/be5264e5e0fe1f53bbe3b44a9cb86dd94ab9d266))
|
||||
* 修正 postinstall 脚本命令格式 ([3f0820f](https://git.bjxgj.com/xgj/spaceflow/commit/3f0820f85dee88808de921c3befe2d332f34cc36))
|
||||
* 恢复 pnpm catalog 配置并更新依赖锁定 ([0b2295c](https://git.bjxgj.com/xgj/spaceflow/commit/0b2295c1f906d89ad3ba7a61b04c6e6b94f193ef))
|
||||
* 新增 .spaceflow/pnpm-workspace.yaml 防止被父级 workspace 接管并移除根项目 devDependencies 自动添加逻辑 ([61de3a2](https://git.bjxgj.com/xgj/spaceflow/commit/61de3a2b75e8a19b28563d2a6476158d19f6c5be))
|
||||
* 新增 postinstall 钩子自动执行 setup 脚本 ([64dae0c](https://git.bjxgj.com/xgj/spaceflow/commit/64dae0cb440bd5e777cb790f826ff2d9f8fe65ba))
|
||||
* 移除 postinstall 钩子避免依赖安装时自动执行构建 ([ea1dc85](https://git.bjxgj.com/xgj/spaceflow/commit/ea1dc85ce7d6cf23a98c13e2c21e3c3bcdf7dd79))
|
||||
|
||||
## [0.20.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.19.0...@spaceflow/publish@0.20.0) (2026-02-04)
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **verbose:** 扩展 verbose 级别支持至 3 ([c1a0808](https://git.bjxgj.com/xgj/spaceflow/commit/c1a080859e5d25ca1eb3dc7e00a67b32eb172635))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.18.0 [no ci] ([e17894a](https://git.bjxgj.com/xgj/spaceflow/commit/e17894a5af53ff040a0a17bc602d232f78415e1b))
|
||||
* **ci-shell:** released version 0.18.0 [no ci] ([f64fd80](https://git.bjxgj.com/xgj/spaceflow/commit/f64fd8009a6dd725f572c7e9fbf084d9320d5128))
|
||||
* **core:** released version 0.18.0 [no ci] ([c5e973f](https://git.bjxgj.com/xgj/spaceflow/commit/c5e973fbe22c0fcd0d6d3af6e4020e2fbff9d31f))
|
||||
* **period-summary:** released version 0.18.0 [no ci] ([f0df638](https://git.bjxgj.com/xgj/spaceflow/commit/f0df63804d06f8c75e04169ec98226d7a4f5d7f9))
|
||||
* **review:** released version 0.27.0 [no ci] ([ac3fc5a](https://git.bjxgj.com/xgj/spaceflow/commit/ac3fc5a5d7317d537d0447e05a61bef15a1accbe))
|
||||
|
||||
## [0.19.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.18.0...@spaceflow/publish@0.19.0) (2026-02-04)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 新增 override 作用域测试,验证 includes 对 override 过滤的影响 ([820e0cb](https://git.bjxgj.com/xgj/spaceflow/commit/820e0cb0f36783dc1c7e1683ad08501e91f094b2))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 从 PR diff 填充缺失的 patch 字段 ([24bfaa7](https://git.bjxgj.com/xgj/spaceflow/commit/24bfaa76f3bd56c8ead307e73e0623a2221c69cf))
|
||||
- **review:** 新增 getFileContents、getChangedFilesBetweenRefs 和 filterIssuesByValidCommits 方法的单元测试 ([7618c91](https://git.bjxgj.com/xgj/spaceflow/commit/7618c91bc075d218b9f51b862e5161d15a306bf8))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **config:** 降低并发数以优化 AI 审查性能 ([052dd72](https://git.bjxgj.com/xgj/spaceflow/commit/052dd728f759da0a31e86a0ad480e9bb35052781))
|
||||
- **review:** 优化 Markdown 格式化器的代码风格和 JSON 数据输出逻辑 ([ca1b0c9](https://git.bjxgj.com/xgj/spaceflow/commit/ca1b0c96d9d0663a8b8dc93b4a9f63d4e5590df0))
|
||||
- **review:** 优化 override 和变更行过滤的日志输出,增强调试信息的可读性 ([9a7c6f5](https://git.bjxgj.com/xgj/spaceflow/commit/9a7c6f5b4ef2b8ae733fa499a0e5ec82feebc1d2))
|
||||
- **review:** 使用 Base64 编码存储审查数据,避免 JSON 格式在 Markdown 中被转义 ([fb91e30](https://git.bjxgj.com/xgj/spaceflow/commit/fb91e30d0979cfe63ed8e7657c578db618b5e783))
|
||||
- **review:** 基于 fileContents 实际 commit hash 验证问题归属,替代依赖 LLM 填写的 commit 字段 ([de3e377](https://git.bjxgj.com/xgj/spaceflow/commit/de3e3771eb85ff93200c63fa9feb38941914a07d))
|
||||
- **review:** 新增测试方法用于验证 PR 审查功能 ([5c57833](https://git.bjxgj.com/xgj/spaceflow/commit/5c578332cedffb7fa7e5ad753a788bcd55595c68))
|
||||
- **review:** 移除 filterNoCommit 配置项,统一使用基于 commit hash 的问题过滤逻辑 ([82429b1](https://git.bjxgj.com/xgj/spaceflow/commit/82429b1072affb4f2b14d52f99887e12184d8218))
|
||||
- **review:** 移除测试方法 testMethod ([21e9938](https://git.bjxgj.com/xgj/spaceflow/commit/21e9938100c5dd7d4eada022441c565b5c41a55a))
|
||||
- **review:** 统一使用 parseLineRange 方法解析行号,避免重复的正则匹配逻辑 ([c64f96a](https://git.bjxgj.com/xgj/spaceflow/commit/c64f96aa2e1a8e22dcd3e31e1a2acc1bb338a1a8))
|
||||
- **review:** 调整 filterIssuesByValidCommits 逻辑,保留无 commit 的 issue 交由 filterNoCommit 配置处理 ([e9c5d47](https://git.bjxgj.com/xgj/spaceflow/commit/e9c5d47aebef42507fd9fcd67e5eab624437e81a))
|
||||
- **review:** 过滤 merge commits,避免在代码审查中处理合并提交 ([d7c647c](https://git.bjxgj.com/xgj/spaceflow/commit/d7c647c33156a58b42bfb45a67417723b75328c6))
|
||||
- **review:** 过滤非 PR commits 的问题,避免 merge commit 引入的代码被审查 ([9e20f54](https://git.bjxgj.com/xgj/spaceflow/commit/9e20f54d57e71725432dfb9e7c943946aa6677d4))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 新增新增文件无 patch 时的测试用例,优化变更行标记逻辑 ([a593f0d](https://git.bjxgj.com/xgj/spaceflow/commit/a593f0d4a641b348f7c9d30b14f639b24c12dcfa))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.17.0 [no ci] ([31abd3d](https://git.bjxgj.com/xgj/spaceflow/commit/31abd3dcb48e2ddea5175552c0a87c1eaa1e7a41))
|
||||
- **ci-shell:** released version 0.17.0 [no ci] ([a53508b](https://git.bjxgj.com/xgj/spaceflow/commit/a53508b15e4020e3399bae9cc04e730f1539ad8e))
|
||||
- **core:** released version 0.17.0 [no ci] ([c85a8ed](https://git.bjxgj.com/xgj/spaceflow/commit/c85a8ed88929d867d2d460a44d08d8b7bc4866a2))
|
||||
- **period-summary:** released version 0.17.0 [no ci] ([ac4e5b6](https://git.bjxgj.com/xgj/spaceflow/commit/ac4e5b6083773146ac840548a69006f6c4fbac1d))
|
||||
- **review:** released version 0.20.0 [no ci] ([8b0f82f](https://git.bjxgj.com/xgj/spaceflow/commit/8b0f82f94813c79d579dbae8decb471b20e45e9d))
|
||||
- **review:** released version 0.21.0 [no ci] ([b51a1dd](https://git.bjxgj.com/xgj/spaceflow/commit/b51a1ddcba3e6a4b3b3eb947864e731d8f87d62b))
|
||||
- **review:** released version 0.22.0 [no ci] ([fca3bfc](https://git.bjxgj.com/xgj/spaceflow/commit/fca3bfc0c53253ac78566e88c7e5d31020a3896b))
|
||||
- **review:** released version 0.23.0 [no ci] ([ed5bf22](https://git.bjxgj.com/xgj/spaceflow/commit/ed5bf22819094df070708c2724669d0b5f7b9008))
|
||||
- **review:** released version 0.24.0 [no ci] ([5f1f94e](https://git.bjxgj.com/xgj/spaceflow/commit/5f1f94ee02123baa05802fb2bb038ccf9d50a0cc))
|
||||
- **review:** released version 0.25.0 [no ci] ([69cfeaf](https://git.bjxgj.com/xgj/spaceflow/commit/69cfeaf768e4bf7b2aaba6f089064469338a1ac0))
|
||||
- **review:** released version 0.26.0 [no ci] ([dec9c7e](https://git.bjxgj.com/xgj/spaceflow/commit/dec9c7ec66455cf83588368c930d12510ada6c0f))
|
||||
|
||||
## [0.18.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.17.0...@spaceflow/publish@0.18.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 新增 Git diff 行号映射工具并优化 Claude 配置 ([88ef340](https://git.bjxgj.com/xgj/spaceflow/commit/88ef3400127fac3ad52fc326ad79fdc7bd058e98))
|
||||
- **review:** 为 execute 方法添加文档注释 ([a21f582](https://git.bjxgj.com/xgj/spaceflow/commit/a21f58290c873fb07789e70c8c5ded2b5874a29d))
|
||||
- **review:** 为 getPrNumberFromEvent 方法添加文档注释 ([54d1586](https://git.bjxgj.com/xgj/spaceflow/commit/54d1586f4558b5bfde81b926c7b513a32e5caf89))
|
||||
- **review:** 优化行号更新统计,分别统计更新和标记无效的问题数量 ([892b8be](https://git.bjxgj.com/xgj/spaceflow/commit/892b8bed8913531a9440579f777b1965fec772e5))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 优化历史 issue commit 匹配逻辑,支持短 SHA 与完整 SHA 的前缀匹配 ([e30c6dd](https://git.bjxgj.com/xgj/spaceflow/commit/e30c6ddefb14ec6631ce341f1d45c59786e94a46))
|
||||
- **review:** 简化历史问题处理策略,将行号更新改为标记变更文件问题为无效 ([5df7f00](https://git.bjxgj.com/xgj/spaceflow/commit/5df7f0087c493e104fe0dc054fd0b6c19ebe3500))
|
||||
- **review:** 简化行号更新逻辑,使用最新 commit diff 替代增量 diff ([6de7529](https://git.bjxgj.com/xgj/spaceflow/commit/6de7529c90ecbcee82149233fc01c393c5c4e7f7))
|
||||
- **review:** 重构行号更新逻辑,使用增量 diff 替代全量 diff ([d4f4304](https://git.bjxgj.com/xgj/spaceflow/commit/d4f4304e1e41614f7be8946d457eea1cf4e202fb))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 添加单元测试以覆盖行号更新逻辑 ([ebf33e4](https://git.bjxgj.com/xgj/spaceflow/commit/ebf33e45c18c910b88b106cdd4cfeb516b3fb656))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **actions:** 增强命令执行日志,输出原始 command 和 args 参数 ([0f0c238](https://git.bjxgj.com/xgj/spaceflow/commit/0f0c238de7d6f10875022f364746cefa56631b7f))
|
||||
- **ci-scripts:** released version 0.16.0 [no ci] ([9ab007d](https://git.bjxgj.com/xgj/spaceflow/commit/9ab007db178878e093ba93ea27c4f05ca813a65d))
|
||||
- **ci-shell:** released version 0.16.0 [no ci] ([87fd703](https://git.bjxgj.com/xgj/spaceflow/commit/87fd7030b54d2f614f23e092499c5c51bfc33788))
|
||||
- **core:** released version 0.16.0 [no ci] ([871f981](https://git.bjxgj.com/xgj/spaceflow/commit/871f981b0b908c981aaef366f2382ec6ca2e2269))
|
||||
- **period-summary:** released version 0.16.0 [no ci] ([b214e31](https://git.bjxgj.com/xgj/spaceflow/commit/b214e31221d5afa04481c48d9ddb878644a22ae7))
|
||||
- **review:** released version 0.19.0 [no ci] ([0ba5c0a](https://git.bjxgj.com/xgj/spaceflow/commit/0ba5c0a39879b598da2d774acc0834c590ef6d4c))
|
||||
- 在 PR 审查工作流中启用 --filter-no-commit 参数 ([e0024ad](https://git.bjxgj.com/xgj/spaceflow/commit/e0024ad5cb29250b452a841db2ce6ebf84016a2c))
|
||||
- 禁用删除代码分析功能 ([988e3f1](https://git.bjxgj.com/xgj/spaceflow/commit/988e3f156f2ca4e92413bf7a455eba1760ad9eba))
|
||||
|
||||
## [0.17.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.16.0...@spaceflow/publish@0.17.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 在 Gitea SDK 中新增编辑 Pull Request 的方法 ([a586bf1](https://git.bjxgj.com/xgj/spaceflow/commit/a586bf110789578f23b39d64511229a1e5635dc4))
|
||||
- **core:** 在 Gitea SDK 中新增获取 reactions 的方法 ([9324cf2](https://git.bjxgj.com/xgj/spaceflow/commit/9324cf2550709b8302171e5522d0792c08bc1415))
|
||||
- **review:** 优化 commit author 获取逻辑,支持 committer 作为备选 ([b75b613](https://git.bjxgj.com/xgj/spaceflow/commit/b75b6133e5b8c95580516480315bc979fc6eb59b))
|
||||
- **review:** 优化 commit author 获取逻辑,支持从 Git 原始作者信息中提取 ([10ac821](https://git.bjxgj.com/xgj/spaceflow/commit/10ac8210a4457e0356c3bc1645f54f6f3d8c904c))
|
||||
- **review:** 优化 commit author 获取逻辑,通过 Gitea API 搜索用户以关联 Git 原始作者 ([daa274b](https://git.bjxgj.com/xgj/spaceflow/commit/daa274bba2255e92d1e9a6e049e20846a69e8df7))
|
||||
- **review:** 优化 PR 标题生成的格式要求 ([a4d807d](https://git.bjxgj.com/xgj/spaceflow/commit/a4d807d0a4feee4ccc88c6096e069c6dbb650a03))
|
||||
- **review:** 优化 verbose 参数支持多级别累加,将日志级别扩展为 0-3 级 ([fe4c830](https://git.bjxgj.com/xgj/spaceflow/commit/fe4c830cac137c5502d700d2cd5f22b52a629e5f))
|
||||
- **review:** 优化历史问题的 author 信息填充逻辑 ([b18d171](https://git.bjxgj.com/xgj/spaceflow/commit/b18d171c9352fe5815262d43ffd9cd7751f03a4e))
|
||||
- **review:** 优化审查报告中回复消息的格式显示 ([f478c8d](https://git.bjxgj.com/xgj/spaceflow/commit/f478c8da4c1d7494819672006e3230dbc8e0924d))
|
||||
- **review:** 优化审查报告中的消息展示格式 ([0996c2b](https://git.bjxgj.com/xgj/spaceflow/commit/0996c2b45c9502c84308f8a7f9186e4dbd4164fb))
|
||||
- **review:** 优化问题 author 信息填充时机,统一在所有问题合并后填充 ([ea8c586](https://git.bjxgj.com/xgj/spaceflow/commit/ea8c586fc60061ffd339e85c6c298b905bdfdcd8))
|
||||
- **review:** 优化问题展示和无效标记逻辑 ([e2b45e1](https://git.bjxgj.com/xgj/spaceflow/commit/e2b45e1ec594488bb79f528911fd6009a3213eca))
|
||||
- **review:** 在 fillIssueAuthors 方法中添加详细的调试日志 ([42ab288](https://git.bjxgj.com/xgj/spaceflow/commit/42ab288933296abdeeb3dbbedbb2aecedbea2251))
|
||||
- **review:** 在 syncReactionsToIssues 中添加详细日志并修复团队成员获取逻辑 ([91f166a](https://git.bjxgj.com/xgj/spaceflow/commit/91f166a07c2e43dabd4dd4ac186ec7b5f03dfc71))
|
||||
- **review:** 在审查报告的回复中为用户名添加 @ 前缀 ([bc6186b](https://git.bjxgj.com/xgj/spaceflow/commit/bc6186b97f0764f6335690eca1f8af665f9b7629))
|
||||
- **review:** 在审查问题中添加作者信息填充功能 ([8332dba](https://git.bjxgj.com/xgj/spaceflow/commit/8332dba4bb826cd358dc96db5f9b9406fb23df9b))
|
||||
- **review:** 将审查命令的详细日志参数从 --verbose 简化为 -vv ([5eb320b](https://git.bjxgj.com/xgj/spaceflow/commit/5eb320b92d1f7165052730b2e90eee52367391dd))
|
||||
- **review:** 扩展评审人收集逻辑,支持从 PR 指定的评审人和团队中获取 ([bbd61af](https://git.bjxgj.com/xgj/spaceflow/commit/bbd61af9d3e2b9e1dcf28c5e3867645fdda52e6f))
|
||||
- **review:** 支持 AI 自动生成和更新 PR 标题 ([e02fb02](https://git.bjxgj.com/xgj/spaceflow/commit/e02fb027d525dd3e794d649e6dbc53c99a3a9a59))
|
||||
- **review:** 支持 PR 关闭事件触发审查并自动传递事件类型参数 ([03967d9](https://git.bjxgj.com/xgj/spaceflow/commit/03967d9e860af7da06e3c04539f16c7bb31557ff))
|
||||
- **review:** 支持在审查报告中展示评论的 reactions 和回复记录 ([f4da31a](https://git.bjxgj.com/xgj/spaceflow/commit/f4da31adf6ce412cb0ce27bfe7a1e87e5350e915))
|
||||
- **review:** 移除 handleReview 中的重复 author 填充逻辑 ([e458bfd](https://git.bjxgj.com/xgj/spaceflow/commit/e458bfd0d21724c37fdd4023265d6a2dd1700404))
|
||||
- **review:** 限制 PR 标题自动更新仅在第一轮审查时执行 ([1891cbc](https://git.bjxgj.com/xgj/spaceflow/commit/1891cbc8d85f6eaef9e7107a7f1003bdc654d3a3))
|
||||
- **review:** 默认启用 PR 标题自动更新功能 ([fda6656](https://git.bjxgj.com/xgj/spaceflow/commit/fda6656efaf6479bb398ddc5cb1955142f31f369))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **actions:** 修复日志输出中的 emoji 显示问题,将 <20> 替换为 ℹ️ ([d3cd94a](https://git.bjxgj.com/xgj/spaceflow/commit/d3cd94afa9c6893b923d316fdcb5904f42ded632))
|
||||
- **review:** 修复审查完成日志中的乱码 emoji ([36c1c48](https://git.bjxgj.com/xgj/spaceflow/commit/36c1c48faecda3cc02b9e0b097aebba0a85ea5f8))
|
||||
- **review:** 将 UserInfo 的 id 字段类型从 number 改为 string ([505e019](https://git.bjxgj.com/xgj/spaceflow/commit/505e019c85d559ce1def1350599c1de218f7516a))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.15.0 [no ci] ([e314fb1](https://git.bjxgj.com/xgj/spaceflow/commit/e314fb11e7425b27c337d3650857cf3b737051fd))
|
||||
- **ci-shell:** released version 0.15.0 [no ci] ([5c0dc0b](https://git.bjxgj.com/xgj/spaceflow/commit/5c0dc0b5482366ccfd7854868d1eb5f306c24810))
|
||||
- **core:** released version 0.15.0 [no ci] ([48f3875](https://git.bjxgj.com/xgj/spaceflow/commit/48f38754dee382548bab968c57dd0f40f2343981))
|
||||
- **period-summary:** released version 0.15.0 [no ci] ([3dd72cb](https://git.bjxgj.com/xgj/spaceflow/commit/3dd72cb65a422b5b008a83820e799b810a6d53eb))
|
||||
- **review:** released version 0.18.0 [no ci] ([d366e3f](https://git.bjxgj.com/xgj/spaceflow/commit/d366e3fa9c1b32369a3d98e56fc873e033d71d00))
|
||||
|
||||
## [0.16.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.15.0...@spaceflow/publish@0.16.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 统一所有命令的错误处理,添加堆栈信息输出 ([31224a1](https://git.bjxgj.com/xgj/spaceflow/commit/31224a16ce7155402504bd8d3e386e59e47949df))
|
||||
- **review:** 增强错误处理,添加堆栈信息输出 ([e0fb5de](https://git.bjxgj.com/xgj/spaceflow/commit/e0fb5de6bc877d8f0b3dc3c03f8d614320427bf3))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.14.0 [no ci] ([c536208](https://git.bjxgj.com/xgj/spaceflow/commit/c536208e352baa82e5b56c490ea9df0aff116cb2))
|
||||
- **ci-shell:** released version 0.14.0 [no ci] ([c6e4bdc](https://git.bjxgj.com/xgj/spaceflow/commit/c6e4bdca44874739694e3e46998e376779503e53))
|
||||
- **core:** released version 0.14.0 [no ci] ([996dbc6](https://git.bjxgj.com/xgj/spaceflow/commit/996dbc6f80b0d3fb8049df9a9a31bd1e5b5d4b92))
|
||||
- **period-summary:** released version 0.14.0 [no ci] ([55a72f2](https://git.bjxgj.com/xgj/spaceflow/commit/55a72f2b481e5ded1d9207a5a8d6a6864328d5a0))
|
||||
- **review:** released version 0.17.0 [no ci] ([9f25412](https://git.bjxgj.com/xgj/spaceflow/commit/9f254121557ae238e32f4093b0c8b5dd8a4b9a72))
|
||||
|
||||
## [0.15.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.14.0...@spaceflow/publish@0.15.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 为删除影响分析添加文件过滤功能 ([7304293](https://git.bjxgj.com/xgj/spaceflow/commit/73042937c5271ff4b0dcb6cd6d823e5aa0c03e7b))
|
||||
- **review:** 新增过滤无commit问题的选项 ([7a4c458](https://git.bjxgj.com/xgj/spaceflow/commit/7a4c458da03ae4a4646abca7e5f03abc849dc405))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 修复 resolveRef 方法未处理空 ref 参数的问题 ([0824c83](https://git.bjxgj.com/xgj/spaceflow/commit/0824c8392482263036888b2fec95935371d67d4d))
|
||||
- **review:** 修复参数空值检查,增强代码健壮性 ([792a192](https://git.bjxgj.com/xgj/spaceflow/commit/792a192fd5dd80ed1e6d85cd61f6ce997bcc9dd9))
|
||||
- **review:** 修复按指定提交过滤时未处理空值导致的潜在问题 ([5d4d3e0](https://git.bjxgj.com/xgj/spaceflow/commit/5d4d3e0390a50c01309bb09e01c7328b211271b8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.13.0 [no ci] ([021eefd](https://git.bjxgj.com/xgj/spaceflow/commit/021eefdf2ff72d16b36123335548df2d3ad1d6b7))
|
||||
- **ci-shell:** released version 0.13.0 [no ci] ([81e7582](https://git.bjxgj.com/xgj/spaceflow/commit/81e75820eb69ca188155e33945111e2b1f6b3012))
|
||||
- **core:** released version 0.13.0 [no ci] ([e3edde3](https://git.bjxgj.com/xgj/spaceflow/commit/e3edde3e670c79544af9a7249d566961740a2284))
|
||||
- **period-summary:** released version 0.13.0 [no ci] ([1d47460](https://git.bjxgj.com/xgj/spaceflow/commit/1d47460e40ba422a32865ccddd353e089eb91c6a))
|
||||
- **review:** released version 0.13.0 [no ci] ([4214c44](https://git.bjxgj.com/xgj/spaceflow/commit/4214c4406ab5482b151ec3c00da376b1d3d50887))
|
||||
- **review:** released version 0.14.0 [no ci] ([4165b05](https://git.bjxgj.com/xgj/spaceflow/commit/4165b05f8aab90d753193f3c1c2800e7f03ea4de))
|
||||
- **review:** released version 0.15.0 [no ci] ([a2ab86d](https://git.bjxgj.com/xgj/spaceflow/commit/a2ab86d097943924749876769f0a144926178783))
|
||||
- **review:** released version 0.16.0 [no ci] ([64c8866](https://git.bjxgj.com/xgj/spaceflow/commit/64c88666fc7e84ced013198d3a53a8c75c7889eb))
|
||||
|
||||
## [0.14.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.13.0...@spaceflow/publish@0.14.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 CLI 入口文件添加 Node shebang 支持 ([0d787d3](https://git.bjxgj.com/xgj/spaceflow/commit/0d787d329e69f2b53d26ba04720d60625ca51efd))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.12.0 [no ci] ([097863f](https://git.bjxgj.com/xgj/spaceflow/commit/097863f0c5cc46cb5cb930f14a6f379f60a13f08))
|
||||
- **ci-shell:** released version 0.12.0 [no ci] ([274216f](https://git.bjxgj.com/xgj/spaceflow/commit/274216fc930dfbf8390d02e25c06efcb44980fed))
|
||||
- **core:** released version 0.12.0 [no ci] ([1ce5034](https://git.bjxgj.com/xgj/spaceflow/commit/1ce50346d73a1914836333415f5ead9fbfa27be7))
|
||||
- **period-summary:** released version 0.12.0 [no ci] ([38490aa](https://git.bjxgj.com/xgj/spaceflow/commit/38490aa75ab20789c5495a5d8d009867f954af4f))
|
||||
- **review:** released version 0.12.0 [no ci] ([3da605e](https://git.bjxgj.com/xgj/spaceflow/commit/3da605ea103192070f1c63112ad896a33fbc4312))
|
||||
|
||||
## [0.13.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.12.0...@spaceflow/publish@0.13.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit message 的 scope 处理逻辑 ([42869dd](https://git.bjxgj.com/xgj/spaceflow/commit/42869dd4bde0a3c9bf8ffb827182775e2877a57b))
|
||||
- **core:** 重构 commit 服务并添加结构化 commit message 支持 ([22b4db8](https://git.bjxgj.com/xgj/spaceflow/commit/22b4db8619b0ce038667ab42dea1362706887fc9))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.11.0 [no ci] ([d4f5bba](https://git.bjxgj.com/xgj/spaceflow/commit/d4f5bba6f89e9e051dde8d313b6e102c6dadfa41))
|
||||
- **ci-shell:** released version 0.11.0 [no ci] ([cf9e486](https://git.bjxgj.com/xgj/spaceflow/commit/cf9e48666197295f118396693abc08b680b3ddee))
|
||||
- **core:** released version 0.11.0 [no ci] ([f0025c7](https://git.bjxgj.com/xgj/spaceflow/commit/f0025c792e332e8b8752597a27f654c0197c36eb))
|
||||
- **period-summary:** released version 0.11.0 [no ci] ([b518887](https://git.bjxgj.com/xgj/spaceflow/commit/b518887bddd5a452c91148bac64d61ec64b0b509))
|
||||
- **review:** released version 0.11.0 [no ci] ([150cd9d](https://git.bjxgj.com/xgj/spaceflow/commit/150cd9df7d380c26e6f3f7f0dfd027022f610e6e))
|
||||
|
||||
## [0.12.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.11.0...@spaceflow/publish@0.12.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 npm 包名处理逻辑 ([ae23ebd](https://git.bjxgj.com/xgj/spaceflow/commit/ae23ebdc3144b611e1aa8c4e66bf0db074d09798))
|
||||
- **core:** 添加依赖更新功能 ([1a544eb](https://git.bjxgj.com/xgj/spaceflow/commit/1a544eb5e2b64396a0187d4518595e9dcb51d73e))
|
||||
- **review:** 支持绝对路径转换为相对路径 ([9050f64](https://git.bjxgj.com/xgj/spaceflow/commit/9050f64b8ef67cb2c8df9663711a209523ae9d18))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.10.0 [no ci] ([ca2daad](https://git.bjxgj.com/xgj/spaceflow/commit/ca2daada8b04bbe809e69a3d5bd9373e897c6f40))
|
||||
- **ci-shell:** released version 0.10.0 [no ci] ([53864b8](https://git.bjxgj.com/xgj/spaceflow/commit/53864b8c2534cae265b8fbb98173a5b909682d4e))
|
||||
- **core:** released version 0.10.0 [no ci] ([a80d34f](https://git.bjxgj.com/xgj/spaceflow/commit/a80d34fb647e107343a07a8793363b3b76320e81))
|
||||
- **period-summary:** released version 0.10.0 [no ci] ([c1ca3bb](https://git.bjxgj.com/xgj/spaceflow/commit/c1ca3bb67fa7f9dbb4de152f0461d644f3044946))
|
||||
- **review:** released version 0.10.0 [no ci] ([6465de8](https://git.bjxgj.com/xgj/spaceflow/commit/6465de8751028787efb509670988c62b4dbbdf2a))
|
||||
- **review:** released version 0.9.0 [no ci] ([13dd62c](https://git.bjxgj.com/xgj/spaceflow/commit/13dd62c6f307aa6d3b78c34f485393434036fe59))
|
||||
|
||||
## [0.11.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.10.0...@spaceflow/publish@0.11.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 npm 包添加 npx 直接执行支持 ([e67a7da](https://git.bjxgj.com/xgj/spaceflow/commit/e67a7da34c4e41408760da4de3a499495ce0df2f))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.9.0 [no ci] ([1b9e816](https://git.bjxgj.com/xgj/spaceflow/commit/1b9e8167bb8fc67fcc439b2ef82e7a63dc323e6d))
|
||||
- **ci-shell:** released version 0.9.0 [no ci] ([accdda7](https://git.bjxgj.com/xgj/spaceflow/commit/accdda7ee4628dc8447e9a89da6c8101c572cb90))
|
||||
- **core:** released version 0.9.0 [no ci] ([8127211](https://git.bjxgj.com/xgj/spaceflow/commit/812721136828e8c38adf0855fb292b0da89daf1a))
|
||||
- **period-summary:** released version 0.9.0 [no ci] ([ac03f9b](https://git.bjxgj.com/xgj/spaceflow/commit/ac03f9bcff742d669c6e8b2f94e40153b6c298f5))
|
||||
- **review:** released version 0.8.0 [no ci] ([ec6e7e5](https://git.bjxgj.com/xgj/spaceflow/commit/ec6e7e5defd2a5a6349d3530f3b0f4732dd5bb62))
|
||||
|
||||
## [0.10.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.9.0...@spaceflow/publish@0.10.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit 消息生成器中的 scope 处理逻辑 ([1592079](https://git.bjxgj.com/xgj/spaceflow/commit/1592079edde659fe94a02bb6e2dea555c80d3b6b))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.8.0 [no ci] ([be6273d](https://git.bjxgj.com/xgj/spaceflow/commit/be6273dab7f1c80c58abdb8de6f0eeb986997e28))
|
||||
- **ci-shell:** released version 0.8.0 [no ci] ([3102178](https://git.bjxgj.com/xgj/spaceflow/commit/310217827c6ec29294dee5689b2dbb1b66492728))
|
||||
- **core:** released version 0.8.0 [no ci] ([625dbc0](https://git.bjxgj.com/xgj/spaceflow/commit/625dbc0206747b21a893ae43032f55d0a068c6fd))
|
||||
- **period-summary:** released version 0.8.0 [no ci] ([44ff3c5](https://git.bjxgj.com/xgj/spaceflow/commit/44ff3c505b243e1291565e354e239ce893e5e47c))
|
||||
- **review:** released version 0.7.0 [no ci] ([1d195d7](https://git.bjxgj.com/xgj/spaceflow/commit/1d195d74685f12edf3b1f4e13b58ccc3d221fd94))
|
||||
|
||||
## [0.9.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.8.0...@spaceflow/publish@0.9.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 重构安装服务目录结构和命名 ([50cc900](https://git.bjxgj.com/xgj/spaceflow/commit/50cc900eb864b23f20c5f48dec20d1a754238286))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.7.0 [no ci] ([ea294e1](https://git.bjxgj.com/xgj/spaceflow/commit/ea294e138c6b15033af85819629727915dfcbf4b))
|
||||
- **ci-shell:** released version 0.7.0 [no ci] ([247967b](https://git.bjxgj.com/xgj/spaceflow/commit/247967b30876aae78cfb1f9c706431b5bb9fb57e))
|
||||
- **core:** released version 0.7.0 [no ci] ([000c53e](https://git.bjxgj.com/xgj/spaceflow/commit/000c53eff80899dbadad8d668a2227921373daad))
|
||||
- **period-summary:** released version 0.7.0 [no ci] ([8869d58](https://git.bjxgj.com/xgj/spaceflow/commit/8869d5876e86ebe83ae65c3259cd9a7e402257cf))
|
||||
- **review:** released version 0.6.0 [no ci] ([48a90b2](https://git.bjxgj.com/xgj/spaceflow/commit/48a90b253dbe03f46d26bb88f3e0158193aa1dba))
|
||||
|
||||
## [0.8.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.7.0...@spaceflow/publish@0.8.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化pnpm包安装逻辑,检测是否为workspace ([6555daf](https://git.bjxgj.com/xgj/spaceflow/commit/6555dafe1f08a244525be3a0345cc585f2552086))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.6.0 [no ci] ([d485758](https://git.bjxgj.com/xgj/spaceflow/commit/d48575827941cae6ffc7ae6ba911e5d4cf3bd7fa))
|
||||
- **ci-shell:** released version 0.6.0 [no ci] ([a2d1239](https://git.bjxgj.com/xgj/spaceflow/commit/a2d12397997b309062a9d93af57a5588cdb82a79))
|
||||
- **core:** released version 0.6.0 [no ci] ([21e1ec6](https://git.bjxgj.com/xgj/spaceflow/commit/21e1ec61a2de542e065034f32a259092dd7c0e0d))
|
||||
- **period-summary:** released version 0.6.0 [no ci] ([6648dfb](https://git.bjxgj.com/xgj/spaceflow/commit/6648dfb31b459e8c4522cff342dfa87a4bdaab4b))
|
||||
- **review:** released version 0.5.0 [no ci] ([93c3088](https://git.bjxgj.com/xgj/spaceflow/commit/93c308887040f39047766a789a37d24ac6146359))
|
||||
|
||||
## [0.7.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.6.0...@spaceflow/publish@0.7.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化包管理器检测与 npm 包处理逻辑 ([63f7fa4](https://git.bjxgj.com/xgj/spaceflow/commit/63f7fa4f55cb41583009b2ea313b5ad327615e52))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 优化配置合并逻辑,添加字段覆盖策略 ([18680e6](https://git.bjxgj.com/xgj/spaceflow/commit/18680e69b0d6e9e05c843ed3f07766830955d658))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.5.0 [no ci] ([a87a1da](https://git.bjxgj.com/xgj/spaceflow/commit/a87a1da0490986c46c2a527cda5e7d0df9df6d03))
|
||||
- **ci-shell:** released version 0.5.0 [no ci] ([920d9a8](https://git.bjxgj.com/xgj/spaceflow/commit/920d9a8165fe6eabf7a074eb65762f4693883438))
|
||||
- **core:** released version 0.5.0 [no ci] ([ad20098](https://git.bjxgj.com/xgj/spaceflow/commit/ad20098ef954283dd6d9867a4d2535769cbb8377))
|
||||
- **period-summary:** released version 0.5.0 [no ci] ([8e547e9](https://git.bjxgj.com/xgj/spaceflow/commit/8e547e9e6a6496a8c314c06cf6e88c97e623bc2d))
|
||||
- **review:** released version 0.4.0 [no ci] ([3b5f8a9](https://git.bjxgj.com/xgj/spaceflow/commit/3b5f8a934de5ba4f59e232e1dcbccbdff1b8b17c))
|
||||
- 更新项目依赖锁定文件 ([19d2d1d](https://git.bjxgj.com/xgj/spaceflow/commit/19d2d1d86bb35b8ee5d3ad20be51b7aa68e83eff))
|
||||
- 移除 npm registry 配置文件 ([2d9fac6](https://git.bjxgj.com/xgj/spaceflow/commit/2d9fac6db79e81a09ca8e031190d0ebb2781cc31))
|
||||
- 调整依赖配置并添加npm registry配置 ([a754db1](https://git.bjxgj.com/xgj/spaceflow/commit/a754db1bad1bafcea50b8d2825aaf19457778f2e))
|
||||
|
||||
## [0.6.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.5.0...@spaceflow/publish@0.6.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整zod依赖的导入来源 ([574eef1](https://git.bjxgj.com/xgj/spaceflow/commit/574eef1910809a72a4b13acd4cb070e12dc42ce8))
|
||||
- **review:** 调整zod依赖的导入路径 ([02014cd](https://git.bjxgj.com/xgj/spaceflow/commit/02014cdab9829df583f0f621150573b8c45a3ad7))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.4.0 [no ci] ([364f696](https://git.bjxgj.com/xgj/spaceflow/commit/364f696d0df5d84be915cfaa9202a592073d9b46))
|
||||
- **ci-shell:** released version 0.4.0 [no ci] ([7e6bf1d](https://git.bjxgj.com/xgj/spaceflow/commit/7e6bf1dabffc6250b918b89bb850d478d3f4b875))
|
||||
- **core:** released version 0.4.0 [no ci] ([bc4cd89](https://git.bjxgj.com/xgj/spaceflow/commit/bc4cd89af70dce052e7e00fe413708790f65be61))
|
||||
- **core:** 调整核心依赖与配置,新增Zod类型系统支持 ([def0751](https://git.bjxgj.com/xgj/spaceflow/commit/def0751577d9f3350494ca3c7bb4a4b087dab05e))
|
||||
- **period-summary:** released version 0.4.0 [no ci] ([ca89a9b](https://git.bjxgj.com/xgj/spaceflow/commit/ca89a9b9436761e210dedfc38fb3c16ef39b0718))
|
||||
- **review:** released version 0.3.0 [no ci] ([865c6fd](https://git.bjxgj.com/xgj/spaceflow/commit/865c6fdee167df187d1bc107867f842fe25c1098))
|
||||
- 调整项目依赖配置 ([6802386](https://git.bjxgj.com/xgj/spaceflow/commit/6802386f38f4081a3b5d5c47ddc49e9ec2e4f28d))
|
||||
|
||||
## [0.5.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.4.0...@spaceflow/publish@0.5.0) (2026-01-28)
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.3.0 [no ci] ([9292b52](https://git.bjxgj.com/xgj/spaceflow/commit/9292b524f2b8171f8774fab4e4ef4b32991f5d3d))
|
||||
- **ci-shell:** released version 0.3.0 [no ci] ([7b25e55](https://git.bjxgj.com/xgj/spaceflow/commit/7b25e557b628fdfa66d7a0be4ee21267906ac15f))
|
||||
- **core:** released version 0.3.0 [no ci] ([bf8b005](https://git.bjxgj.com/xgj/spaceflow/commit/bf8b005ccbfcdd2061c18ae4ecdd476584ecbb53))
|
||||
- **core:** 调整依赖配置 ([c86534a](https://git.bjxgj.com/xgj/spaceflow/commit/c86534ad213293ee2557ba5568549e8fbcb74ba5))
|
||||
- **period-summary:** released version 0.3.0 [no ci] ([7e74c59](https://git.bjxgj.com/xgj/spaceflow/commit/7e74c59d90d88e061e693829f8196834d9858307))
|
||||
- 调整项目依赖配置 ([f4009cb](https://git.bjxgj.com/xgj/spaceflow/commit/f4009cb0c369b225c356584afb28a7ff5a1a89ec))
|
||||
|
||||
## [0.4.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.3.0...@spaceflow/publish@0.4.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整包变更检测的日志输出格式 ([df35e92](https://git.bjxgj.com/xgj/spaceflow/commit/df35e92d614ce59e202643cf34a0fef2803412a1))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **review:** released version 0.2.0 [no ci] ([d0bd3ed](https://git.bjxgj.com/xgj/spaceflow/commit/d0bd3edf364dedc7c077d95801b402d41c3fdd9c))
|
||||
|
||||
## [0.3.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.2.0...@spaceflow/publish@0.3.0) (2026-01-28)
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **core:** 更新核心框架README文档 ([0d98658](https://git.bjxgj.com/xgj/spaceflow/commit/0d98658f6ab01f119f98d3387fb5651d4d4351a8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.2.0 [no ci] ([716e9ad](https://git.bjxgj.com/xgj/spaceflow/commit/716e9ad0f32bde09c608143da78f0a4299017797))
|
||||
- **ci-shell:** released version 0.2.0 [no ci] ([4f5314b](https://git.bjxgj.com/xgj/spaceflow/commit/4f5314b1002b90d7775a5ef51e618a3f227ae5a9))
|
||||
- **core:** released version 0.2.0 [no ci] ([5a96529](https://git.bjxgj.com/xgj/spaceflow/commit/5a96529cabdce4fb150732b34c55e668c33cb50c))
|
||||
- **period-summary:** released version 0.2.0 [no ci] ([66a4e20](https://git.bjxgj.com/xgj/spaceflow/commit/66a4e209519b64d946ec21b1d1695105fb9de106))
|
||||
|
||||
## [0.2.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.1.2...@spaceflow/publish@0.2.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **publish:** 增强包变更检测的日志输出 ([b89c5cc](https://git.bjxgj.com/xgj/spaceflow/commit/b89c5cc0654713b6482ee591325d4f92ad773600))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复分支锁定时未捕获异常处理器的资源泄漏问题 ([ae326e9](https://git.bjxgj.com/xgj/spaceflow/commit/ae326e95c0cea033893cf084cbf7413fb252bd33))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **review:** released version 0.1.2 [no ci] ([9689d3e](https://git.bjxgj.com/xgj/spaceflow/commit/9689d3e37781ca9ae6cb14d7b12717c061f2919d))
|
||||
- 优化CI工作流的代码检出配置 ([d9740dd](https://git.bjxgj.com/xgj/spaceflow/commit/d9740dd6d1294068ffdcd7a12b61149773866a2a))
|
||||
|
||||
## [0.1.2](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.1.1...@spaceflow/publish@0.1.2) (2026-01-28)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复预演模式下的交互式提示问题 ([0b785bf](https://git.bjxgj.com/xgj/spaceflow/commit/0b785bfddb9f35e844989bd3891817dc502302f8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.2 [no ci] ([ab9c100](https://git.bjxgj.com/xgj/spaceflow/commit/ab9c1000bcbe64d8a99ffa6bebb974c024b14325))
|
||||
- **ci-shell:** released version 0.1.2 [no ci] ([bf7977b](https://git.bjxgj.com/xgj/spaceflow/commit/bf7977bed684b557555861b9dc0359eda3c5d476))
|
||||
- **core:** released version 0.1.2 [no ci] ([8292dbe](https://git.bjxgj.com/xgj/spaceflow/commit/8292dbe59a200cc640a95b86afb6451ec0c044ce))
|
||||
- **period-summary:** released version 0.1.2 [no ci] ([eaf41a0](https://git.bjxgj.com/xgj/spaceflow/commit/eaf41a0149ee4306361ccab0b3878bded79677df))
|
||||
|
||||
## [0.1.1](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.1.0...@spaceflow/publish@0.1.1) (2026-01-28)
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **publish:** 完善发布插件README文档 ([faa57b0](https://git.bjxgj.com/xgj/spaceflow/commit/faa57b095453c00fb3c9a7704bc31b63953c0ac5))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.1 [no ci] ([19ca0d8](https://git.bjxgj.com/xgj/spaceflow/commit/19ca0d8461f9537f4318b772cad3ea395d2b3264))
|
||||
- **ci-shell:** released version 0.1.1 [no ci] ([488a686](https://git.bjxgj.com/xgj/spaceflow/commit/488a6869240151e7d1cf37a3b177897c2b5d5c1e))
|
||||
- **core:** released version 0.1.1 [no ci] ([0cf3a4d](https://git.bjxgj.com/xgj/spaceflow/commit/0cf3a4d37d7d1460e232dd30bc7ab8dc015ed11f))
|
||||
- **period-summary:** released version 0.1.1 [no ci] ([b77e96b](https://git.bjxgj.com/xgj/spaceflow/commit/b77e96b1b768efa81d37143101057224fc3cef0f))
|
||||
|
||||
## [0.1.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/publish@0.0.1...@spaceflow/publish@0.1.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 添加同步解锁分支方法用于进程退出清理 ([cbec480](https://git.bjxgj.com/xgj/spaceflow/commit/cbec480511e074de3ccdc61226f3baa317cff907))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.0 [no ci] ([57b3a1c](https://git.bjxgj.com/xgj/spaceflow/commit/57b3a1c826dafd5ec51d68b7471266efd5cc32b2))
|
||||
- **ci-shell:** released version 0.1.0 [no ci] ([2283d9d](https://git.bjxgj.com/xgj/spaceflow/commit/2283d9d69ada1c071bef6c548dc756fe062893bd))
|
||||
- **core:** released version 0.1.0 [no ci] ([f455607](https://git.bjxgj.com/xgj/spaceflow/commit/f45560735082840410e08e0d8113f366732a1243))
|
||||
- **period-summary:** released version 0.1.0 [no ci] ([36fb7a4](https://git.bjxgj.com/xgj/spaceflow/commit/36fb7a486da82e1d8e4b0574c68b4473cd86b28e))
|
||||
|
||||
## 0.0.1 (2026-01-28)
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.0.1 [no ci] ([b38fb9b](https://git.bjxgj.com/xgj/spaceflow/commit/b38fb9ba56200ced1baf563b097faa8717693783))
|
||||
- **ci-shell:** released version 0.0.1 [no ci] ([ec2a84b](https://git.bjxgj.com/xgj/spaceflow/commit/ec2a84b298c5fb989951caf42e2b016b3336f6a0))
|
||||
- **core:** released version 0.0.1 [no ci] ([66497d6](https://git.bjxgj.com/xgj/spaceflow/commit/66497d60be04b4756a3362dbec4652177910165c))
|
||||
- **period-summary:** released version 0.0.1 [no ci] ([7ab3504](https://git.bjxgj.com/xgj/spaceflow/commit/7ab3504750191b88643fe5db6b92bb08acc9ab5d))
|
||||
- 重置所有包版本至 0.0.0 并清理 CHANGELOG 文件 ([f7efaf9](https://git.bjxgj.com/xgj/spaceflow/commit/f7efaf967467f1272e05d645720ee63941fe79be))
|
||||
237
commands/publish/README.md
Normal file
237
commands/publish/README.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# @spaceflow/publish
|
||||
|
||||
Spaceflow CI 发布插件,基于 [release-it](https://github.com/release-it/release-it) 实现自动化版本发布。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **Monorepo 支持**:自动检测变更包,按依赖拓扑顺序发布
|
||||
- **分支保护**:发布期间自动锁定分支,防止其他推送干扰
|
||||
- **Conventional Changelog**:基于 conventional commits 自动生成 CHANGELOG
|
||||
- **GitHub Release**:自动创建 GitHub Release 并上传资产文件
|
||||
- **预发布支持**:支持 rc、beta、alpha 等预发布版本
|
||||
- **进程退出保护**:即使发布失败也能自动解锁分支
|
||||
|
||||
## 安装
|
||||
|
||||
`@spaceflow/publish` 是 `@spaceflow/cli` 的内置命令,安装 CLI 后即可使用:
|
||||
|
||||
```bash
|
||||
# 使用 pnpm
|
||||
pnpm add -D @spaceflow/cli
|
||||
|
||||
pnpm spaceflow install @spaceflow/publish
|
||||
```
|
||||
|
||||
然后在项目根目录创建 `spaceflow.json` 配置文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"publish": {
|
||||
"monorepo": {
|
||||
"enabled": true
|
||||
},
|
||||
"git": {
|
||||
"lockBranch": true,
|
||||
"pushWhitelistUsernames": ["github-actions[bot]"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 命令行参数
|
||||
|
||||
```bash
|
||||
# 发布
|
||||
spaceflow publish [options]
|
||||
|
||||
# 预演
|
||||
spaceflow publish --rehearsal
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
| ------------------------ | ----------------------------------------------- |
|
||||
| `-d, --dry-run` | 仅打印将要执行的操作,不实际执行 |
|
||||
| `-c, --ci` | 在 CI 环境中运行(自动 fetch tags) |
|
||||
| `-p, --prerelease <tag>` | 预发布标签,如 `rc`、`beta`、`alpha`、`nightly` |
|
||||
| `-r, --rehearsal` | 预演模式:执行 hooks 但不修改文件/git |
|
||||
|
||||
## 配置
|
||||
|
||||
在 `spaceflow.config.ts` 中配置 `publish` 插件:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
plugins: {
|
||||
publish: {
|
||||
// Monorepo 发布模式配置
|
||||
monorepo: {
|
||||
enabled: true, // 启用 monorepo 模式
|
||||
propagateDeps: true, // 依赖的包变更时,依赖方也发布
|
||||
},
|
||||
|
||||
// Changelog 配置
|
||||
changelog: {
|
||||
infileDir: ".", // CHANGELOG 文件输出目录
|
||||
preset: {
|
||||
name: "conventionalcommits",
|
||||
type: [
|
||||
{ type: "feat", section: "新功能" },
|
||||
{ type: "fix", section: "Bug 修复" },
|
||||
{ type: "refactor", section: "代码重构" },
|
||||
{ type: "perf", section: "性能优化" },
|
||||
{ type: "docs", section: "文档更新" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// NPM 发布配置
|
||||
npm: {
|
||||
publish: true, // 是否发布到 npm registry
|
||||
packageManager: "pnpm", // 包管理器:npm 或 pnpm
|
||||
registry: "https://registry.npmjs.org",
|
||||
tag: "latest", // npm tag
|
||||
ignoreVersion: true, // 忽略 package.json 中的版本号
|
||||
versionArgs: ["--workspaces false"],
|
||||
publishArgs: [],
|
||||
},
|
||||
|
||||
// GitHub Release 配置
|
||||
release: {
|
||||
host: "https://github.com",
|
||||
assets: [{ path: "dist/*.zip", name: "dist.zip", type: "zip" }],
|
||||
},
|
||||
|
||||
// Git 配置
|
||||
git: {
|
||||
requireBranch: ["main", "dev", "develop"], // 允许发布的分支
|
||||
lockBranch: true, // 发布时锁定分支
|
||||
pushWhitelistUsernames: ["github-actions[bot]"], // 锁定期间允许推送的用户
|
||||
},
|
||||
|
||||
// release-it hooks
|
||||
hooks: {
|
||||
"before:bump": "echo 'Before bump'",
|
||||
"after:bump": ["pnpm build", "pnpm test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 配置项说明
|
||||
|
||||
### monorepo
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| --------------- | --------- | ------- | -------------------------------- |
|
||||
| `enabled` | `boolean` | `false` | 是否启用 monorepo 发布模式 |
|
||||
| `propagateDeps` | `boolean` | `true` | 依赖的包变更时,依赖方是否也发布 |
|
||||
|
||||
### changelog
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | -------- | ----------------------- | ----------------------------- |
|
||||
| `infileDir` | `string` | `"."` | CHANGELOG 文件输出目录 |
|
||||
| `preset.name` | `string` | `"conventionalcommits"` | Changelog preset 名称 |
|
||||
| `preset.type` | `array` | `[]` | Commit type 到 section 的映射 |
|
||||
|
||||
### npm
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ---------------- | ----------------- | ------------------------ | -------------------------------- |
|
||||
| `publish` | `boolean` | `false` | 是否发布到 npm registry |
|
||||
| `packageManager` | `"npm" \| "pnpm"` | `"npm"` | 包管理器 |
|
||||
| `registry` | `string` | - | npm registry 地址 |
|
||||
| `tag` | `string` | `"latest"` | npm tag |
|
||||
| `ignoreVersion` | `boolean` | `true` | 是否忽略 package.json 中的版本号 |
|
||||
| `versionArgs` | `string[]` | `["--workspaces false"]` | npm version 命令额外参数 |
|
||||
| `publishArgs` | `string[]` | `[]` | npm/pnpm publish 命令额外参数 |
|
||||
|
||||
### release
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ---------------- | -------- | ------------- | -------------------- |
|
||||
| `host` | `string` | `"localhost"` | Git 服务器地址 |
|
||||
| `assets` | `array` | `[]` | Release 资产文件列表 |
|
||||
| `assetSourcemap` | `object` | - | Sourcemap 资产配置 |
|
||||
|
||||
### git
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---------- | ---------------------------- | ------------------------ |
|
||||
| `requireBranch` | `string[]` | `["main", "dev", "develop"]` | 允许发布的分支列表 |
|
||||
| `lockBranch` | `boolean` | `true` | 发布时是否锁定分支 |
|
||||
| `pushWhitelistUsernames` | `string[]` | `[]` | 锁定期间允许推送的用户名 |
|
||||
|
||||
### hooks
|
||||
|
||||
支持 release-it 的所有 hooks,如:
|
||||
|
||||
- `before:init` / `after:init`
|
||||
- `before:bump` / `after:bump`
|
||||
- `before:release` / `after:release`
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| `GITHUB_TOKEN` | GitHub API Token |
|
||||
| `GITHUB_REPOSITORY` | 仓库名称(格式:`owner/repo`) |
|
||||
| `GITHUB_REF_NAME` | 当前分支名称 |
|
||||
| `PUBLISH_REHEARSAL` | 预演模式标志(由 `--rehearsal` 自动设置) |
|
||||
|
||||
## CI 工作流示例
|
||||
|
||||
```yaml
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm spaceflow publish --ci
|
||||
```
|
||||
|
||||
## 版本计算
|
||||
|
||||
版本号基于 [Conventional Commits](https://www.conventionalcommits.org/) 自动计算:
|
||||
|
||||
| Commit 类型 | 版本变更 |
|
||||
| ----------------- | ------------- |
|
||||
| `feat` | Minor (0.x.0) |
|
||||
| `fix` | Patch (0.0.x) |
|
||||
| `BREAKING CHANGE` | Major (x.0.0) |
|
||||
| 其他 | Patch (0.0.x) |
|
||||
|
||||
## Monorepo 模式
|
||||
|
||||
启用 `monorepo.enabled: true` 后:
|
||||
|
||||
1. **变更检测**:基于 git diff 检测哪些包有变更
|
||||
2. **依赖传递**:如果包 A 依赖包 B,且 B 有变更,A 也会被标记为需要发布
|
||||
3. **拓扑排序**:被依赖的包先发布,确保依赖关系正确
|
||||
4. **独立版本**:每个包有独立的版本号和 CHANGELOG
|
||||
5. **Tag 格式**:`@scope/package@version`(如 `@spaceflow/cli@1.0.0`)
|
||||
|
||||
## 分支保护机制
|
||||
|
||||
发布期间会自动锁定分支,防止其他推送干扰发布流程:
|
||||
|
||||
1. **锁定**:创建分支保护规则,仅允许白名单用户推送
|
||||
2. **发布**:执行版本更新、CHANGELOG 生成、git tag、npm publish
|
||||
3. **解锁**:删除分支保护规则,恢复正常状态
|
||||
|
||||
即使发布过程中发生错误,也会通过 `process.on('exit')` 确保分支被解锁。
|
||||
30
commands/publish/package.json
Normal file
30
commands/publish/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@spaceflow/publish",
|
||||
"version": "0.21.0",
|
||||
"description": "Spaceflow CI 发布插件,用于在发布流程中锁定/解锁分支",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"build": "spaceflow build",
|
||||
"dev": "spaceflow dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@release-it/conventional-changelog": "^10.0.4",
|
||||
"release-it": "^19.2.2",
|
||||
"release-it-gitea": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spaceflow/cli": "workspace:*",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "catalog:",
|
||||
"@nestjs/config": "catalog:",
|
||||
"@spaceflow/core": "workspace:*",
|
||||
"nest-commander": "catalog:"
|
||||
},
|
||||
"spaceflow": {
|
||||
"type": "flow",
|
||||
"entry": "."
|
||||
}
|
||||
}
|
||||
30
commands/publish/src/index.ts
Normal file
30
commands/publish/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import "./locales";
|
||||
import { SpaceflowExtension, SpaceflowExtensionMetadata, t } from "@spaceflow/core";
|
||||
import { PublishModule } from "./publish.module";
|
||||
import { publishSchema } from "./publish.config";
|
||||
/** publish Extension 元数据 */
|
||||
export const publishMetadata: SpaceflowExtensionMetadata = {
|
||||
name: "publish",
|
||||
commands: ["publish"],
|
||||
configKey: "publish",
|
||||
configSchema: () => publishSchema,
|
||||
version: "1.0.0",
|
||||
description: t("publish:extensionDescription"),
|
||||
};
|
||||
|
||||
export class PublishExtension implements SpaceflowExtension {
|
||||
getMetadata(): SpaceflowExtensionMetadata {
|
||||
return publishMetadata;
|
||||
}
|
||||
|
||||
getModule() {
|
||||
return PublishModule;
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishExtension;
|
||||
|
||||
export * from "./publish.command";
|
||||
export * from "./publish.service";
|
||||
export * from "./publish.module";
|
||||
export * from "./monorepo.service";
|
||||
8
commands/publish/src/locales/en/publish.json
Normal file
8
commands/publish/src/locales/en/publish.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"description": "CI publish command for branch lock/unlock during release",
|
||||
"options.prerelease": "Prerelease tag, e.g. rc, beta, alpha, nightly",
|
||||
"options.rehearsal": "Rehearsal mode: execute hooks but no file/git changes, sets PUBLISH_REHEARSAL=true",
|
||||
"rehearsalMode": "🎭 REHEARSAL mode: hooks will execute, but no file/git changes",
|
||||
"dryRunMode": "🔍 DRY-RUN mode: no actual execution",
|
||||
"extensionDescription": "CI publish command for branch lock/unlock during release"
|
||||
}
|
||||
11
commands/publish/src/locales/index.ts
Normal file
11
commands/publish/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { addLocaleResources } from "@spaceflow/core";
|
||||
import zhCN from "./zh-cn/publish.json";
|
||||
import en from "./en/publish.json";
|
||||
|
||||
/** publish 命令 i18n 资源 */
|
||||
export const publishLocales: Record<string, Record<string, string>> = {
|
||||
"zh-CN": zhCN,
|
||||
en,
|
||||
};
|
||||
|
||||
addLocaleResources("publish", publishLocales);
|
||||
8
commands/publish/src/locales/zh-cn/publish.json
Normal file
8
commands/publish/src/locales/zh-cn/publish.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"description": "CI 发布命令,用于在发布流程中锁定/解锁分支",
|
||||
"options.prerelease": "预发布标签,如 rc、beta、alpha、nightly 等",
|
||||
"options.rehearsal": "预演模式:执行 hooks 但不修改文件/git,设置 PUBLISH_REHEARSAL=true 环境变量",
|
||||
"rehearsalMode": "🎭 REHEARSAL mode: hooks will execute, but no file/git changes",
|
||||
"dryRunMode": "🔍 DRY-RUN mode: no actual execution",
|
||||
"extensionDescription": "CI 发布命令,用于在发布流程中锁定/解锁分支"
|
||||
}
|
||||
376
commands/publish/src/monorepo.service.ts
Normal file
376
commands/publish/src/monorepo.service.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export interface PackageInfo {
|
||||
/** 包目录路径(相对于项目根目录) */
|
||||
dir: string;
|
||||
/** 包名称(从 package.json 读取) */
|
||||
name: string;
|
||||
/** 包版本 */
|
||||
version: string;
|
||||
/** workspace 依赖的包名列表 */
|
||||
workspaceDeps: string[];
|
||||
}
|
||||
|
||||
export interface MonorepoAnalysisResult {
|
||||
/** 所有变更的包 */
|
||||
changedPackages: PackageInfo[];
|
||||
/** 需要发布的包(包含依赖变更的包),按拓扑排序 */
|
||||
packagesToPublish: PackageInfo[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MonorepoService {
|
||||
private readonly cwd: string;
|
||||
|
||||
constructor() {
|
||||
this.cwd = process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 monorepo 变更,返回需要发布的包列表(拓扑排序后)
|
||||
* @param dryRun 是否为 dry-run 模式
|
||||
* @param propagateDeps 是否传递依赖变更(依赖的包变更时,依赖方也发布)
|
||||
*/
|
||||
async analyze(dryRun: boolean, propagateDeps = true): Promise<MonorepoAnalysisResult> {
|
||||
const workspacePackages = this.getWorkspacePackages();
|
||||
const allPackages = this.getAllPackageInfos(workspacePackages);
|
||||
|
||||
// 为每个包单独检测变更(基于各自的最新 tag)
|
||||
const changedPackages = this.getChangedPackages(allPackages, dryRun);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`📦 直接变更的包: ${changedPackages.map((p) => p.name).join(", ") || "无"}`);
|
||||
}
|
||||
|
||||
// 计算依赖传递,找出所有需要发布的包
|
||||
const packagesToPublish = propagateDeps
|
||||
? this.calculateAffectedPackages(changedPackages, allPackages)
|
||||
: changedPackages;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`🔄 需要发布的包(含依赖传递): ${packagesToPublish.map((p) => p.name).join(", ") || "无"}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 拓扑排序
|
||||
const sortedPackages = this.topologicalSort(packagesToPublish, allPackages);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`📋 发布顺序: ${sortedPackages.map((p) => p.name).join(" -> ") || "无"}`);
|
||||
}
|
||||
|
||||
return {
|
||||
changedPackages,
|
||||
packagesToPublish: sortedPackages,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单解析 pnpm-workspace.yaml(只提取 packages 数组)
|
||||
*/
|
||||
private parseSimpleYaml(content: string): { packages?: string[] } {
|
||||
const packages: string[] = [];
|
||||
const lines = content.split("\n");
|
||||
let inPackages = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "packages:") {
|
||||
inPackages = true;
|
||||
continue;
|
||||
}
|
||||
if (inPackages) {
|
||||
if (trimmed.startsWith("- ")) {
|
||||
// 提取包路径,去除引号
|
||||
let pkg = trimmed.slice(2).trim();
|
||||
pkg = pkg.replace(/^["']|["']$/g, "");
|
||||
packages.push(pkg);
|
||||
} else if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("-")) {
|
||||
// 遇到新的顶级 key,停止解析
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { packages: packages.length > 0 ? packages : undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 pnpm-workspace.yaml 读取 workspace 包配置
|
||||
*/
|
||||
private getWorkspacePackages(): string[] {
|
||||
const workspaceFile = join(this.cwd, "pnpm-workspace.yaml");
|
||||
if (!existsSync(workspaceFile)) {
|
||||
throw new Error("未找到 pnpm-workspace.yaml 文件");
|
||||
}
|
||||
|
||||
const content = readFileSync(workspaceFile, "utf-8");
|
||||
const config = this.parseSimpleYaml(content);
|
||||
|
||||
if (!config.packages || !Array.isArray(config.packages)) {
|
||||
throw new Error("pnpm-workspace.yaml 中未配置 packages");
|
||||
}
|
||||
|
||||
return config.packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 workspace 包配置,获取所有实际的包目录
|
||||
*/
|
||||
private expandWorkspacePatterns(patterns: string[]): string[] {
|
||||
const dirs: string[] = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.includes("*")) {
|
||||
// 使用 glob 展开,这里简化处理,只支持 commands/* 这种模式
|
||||
const baseDir = pattern.replace("/*", "");
|
||||
const basePath = join(this.cwd, baseDir);
|
||||
if (existsSync(basePath)) {
|
||||
const { readdirSync, statSync } = require("fs");
|
||||
const entries = readdirSync(basePath) as string[];
|
||||
for (const entry of entries) {
|
||||
const entryPath = join(basePath, entry);
|
||||
if (statSync(entryPath).isDirectory()) {
|
||||
const pkgJson = join(entryPath, "package.json");
|
||||
if (existsSync(pkgJson)) {
|
||||
dirs.push(join(baseDir, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接目录
|
||||
const pkgJson = join(this.cwd, pattern, "package.json");
|
||||
if (existsSync(pkgJson)) {
|
||||
dirs.push(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有包的详细信息(排除私有包)
|
||||
*/
|
||||
private getAllPackageInfos(patterns: string[]): PackageInfo[] {
|
||||
const dirs = this.expandWorkspacePatterns(patterns);
|
||||
const packages: PackageInfo[] = [];
|
||||
|
||||
for (const dir of dirs) {
|
||||
const pkgJsonPath = join(this.cwd, dir, "package.json");
|
||||
if (!existsSync(pkgJsonPath)) continue;
|
||||
|
||||
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
||||
|
||||
// 跳过私有包
|
||||
if (pkgJson.private === true) continue;
|
||||
|
||||
const workspaceDeps = this.extractWorkspaceDeps(pkgJson);
|
||||
|
||||
packages.push({
|
||||
dir,
|
||||
name: pkgJson.name,
|
||||
version: pkgJson.version,
|
||||
workspaceDeps,
|
||||
});
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取包的 workspace 依赖
|
||||
*/
|
||||
private extractWorkspaceDeps(pkgJson: Record<string, unknown>): string[] {
|
||||
const deps: string[] = [];
|
||||
const allDeps = {
|
||||
...(pkgJson.dependencies as Record<string, string> | undefined),
|
||||
...(pkgJson.devDependencies as Record<string, string> | undefined),
|
||||
...(pkgJson.peerDependencies as Record<string, string> | undefined),
|
||||
};
|
||||
|
||||
for (const [name, version] of Object.entries(allDeps)) {
|
||||
if (version && (version.startsWith("workspace:") || version === "*")) {
|
||||
deps.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测每个包的变更(基于各自的最新 tag)
|
||||
*/
|
||||
private getChangedPackages(allPackages: PackageInfo[], dryRun: boolean): PackageInfo[] {
|
||||
const changedPackages: PackageInfo[] = [];
|
||||
|
||||
for (const pkg of allPackages) {
|
||||
const hasChanges = this.hasPackageChanges(pkg);
|
||||
if (hasChanges) {
|
||||
changedPackages.push(pkg);
|
||||
}
|
||||
if (dryRun) {
|
||||
console.log(` ${hasChanges ? "✅" : "⭕"} ${pkg.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
return changedPackages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测单个包是否有变更(基于该包的最新 tag)
|
||||
*/
|
||||
private hasPackageChanges(pkg: PackageInfo): boolean {
|
||||
try {
|
||||
// 获取该包的最新 tag(格式: @scope/pkg@version 或 pkg@version)
|
||||
const tagPattern = `${pkg.name}@*`;
|
||||
const latestTag = execSync(
|
||||
`git describe --tags --abbrev=0 --match "${tagPattern}" 2>/dev/null || echo ''`,
|
||||
{ cwd: this.cwd, encoding: "utf-8" },
|
||||
).trim();
|
||||
|
||||
if (!latestTag) {
|
||||
// 没有 tag,说明是新包,需要发布
|
||||
console.log(`📌 ${pkg.name}: 无 tag,需要发布`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检测从该 tag 到 HEAD,该包目录下是否有变更
|
||||
const diffOutput = execSync(`git diff --name-only "${latestTag}"..HEAD -- "${pkg.dir}"`, {
|
||||
cwd: this.cwd,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const hasChanges = diffOutput.length > 0;
|
||||
if (hasChanges) {
|
||||
console.log(`📌 ${pkg.name}: ${latestTag} -> HEAD 有变更`);
|
||||
console.log(
|
||||
` 变更文件: ${diffOutput.split("\n").slice(0, 3).join(", ")}${diffOutput.split("\n").length > 3 ? "..." : ""}`,
|
||||
);
|
||||
}
|
||||
return hasChanges;
|
||||
} catch (error) {
|
||||
// 出错时保守处理,认为有变更
|
||||
console.log(`📌 ${pkg.name}: 检测出错,保守处理为有变更`);
|
||||
console.log(` 错误: ${error instanceof Error ? error.message : error}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将变更文件映射到包目录
|
||||
*/
|
||||
private mapFilesToPackages(files: string[], patterns: string[]): Set<string> {
|
||||
const packageDirs = this.expandWorkspacePatterns(patterns);
|
||||
const changedPackages = new Set<string>();
|
||||
|
||||
for (const file of files) {
|
||||
for (const dir of packageDirs) {
|
||||
if (file.startsWith(dir + "/") || file === dir) {
|
||||
changedPackages.add(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changedPackages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算受影响的包(包含依赖传递)
|
||||
*/
|
||||
private calculateAffectedPackages(
|
||||
changedPackages: PackageInfo[],
|
||||
allPackages: PackageInfo[],
|
||||
): PackageInfo[] {
|
||||
const changedNames = new Set(changedPackages.map((p) => p.name));
|
||||
const affectedNames = new Set(changedNames);
|
||||
|
||||
// 构建反向依赖图:谁依赖了我
|
||||
const reverseDeps = new Map<string, Set<string>>();
|
||||
for (const pkg of allPackages) {
|
||||
for (const dep of pkg.workspaceDeps) {
|
||||
if (!reverseDeps.has(dep)) {
|
||||
reverseDeps.set(dep, new Set());
|
||||
}
|
||||
reverseDeps.get(dep)!.add(pkg.name);
|
||||
}
|
||||
}
|
||||
|
||||
// BFS 传递依赖
|
||||
const queue = [...changedNames];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
const dependents = reverseDeps.get(current);
|
||||
if (dependents) {
|
||||
for (const dependent of dependents) {
|
||||
if (!affectedNames.has(dependent)) {
|
||||
affectedNames.add(dependent);
|
||||
queue.push(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allPackages.filter((p) => affectedNames.has(p.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拓扑排序:被依赖的包先发布
|
||||
*/
|
||||
private topologicalSort(packages: PackageInfo[], _allPackages: PackageInfo[]): PackageInfo[] {
|
||||
const packageNames = new Set(packages.map((p) => p.name));
|
||||
const nameToPackage = new Map(packages.map((p) => [p.name, p]));
|
||||
|
||||
// 构建依赖图(只考虑待发布包之间的依赖)
|
||||
const inDegree = new Map<string, number>();
|
||||
const graph = new Map<string, string[]>();
|
||||
|
||||
for (const pkg of packages) {
|
||||
inDegree.set(pkg.name, 0);
|
||||
graph.set(pkg.name, []);
|
||||
}
|
||||
|
||||
for (const pkg of packages) {
|
||||
for (const dep of pkg.workspaceDeps) {
|
||||
if (packageNames.has(dep)) {
|
||||
graph.get(dep)!.push(pkg.name);
|
||||
inDegree.set(pkg.name, (inDegree.get(pkg.name) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn's algorithm
|
||||
const queue: string[] = [];
|
||||
for (const [name, degree] of inDegree) {
|
||||
if (degree === 0) {
|
||||
queue.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted: PackageInfo[] = [];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
sorted.push(nameToPackage.get(current)!);
|
||||
|
||||
for (const neighbor of graph.get(current) || []) {
|
||||
const newDegree = (inDegree.get(neighbor) || 0) - 1;
|
||||
inDegree.set(neighbor, newDegree);
|
||||
if (newDegree === 0) {
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sorted.length !== packages.length) {
|
||||
throw new Error("检测到循环依赖,无法确定发布顺序");
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
71
commands/publish/src/publish.command.ts
Normal file
71
commands/publish/src/publish.command.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Command, CommandRunner, Option } from "nest-commander";
|
||||
import { t } from "@spaceflow/core";
|
||||
import { PublishService } from "./publish.service";
|
||||
|
||||
export interface PublishOptions {
|
||||
dryRun: boolean;
|
||||
ci: boolean;
|
||||
prerelease?: string;
|
||||
/** 预演模式:执行 hooks 但不修改文件/git */
|
||||
rehearsal: boolean;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: "publish",
|
||||
description: t("publish:description"),
|
||||
})
|
||||
export class PublishCommand extends CommandRunner {
|
||||
constructor(protected readonly publishService: PublishService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(_passedParams: string[], options: PublishOptions): Promise<void> {
|
||||
if (options.rehearsal) {
|
||||
console.log(t("publish:rehearsalMode"));
|
||||
} else if (options.dryRun) {
|
||||
console.log(t("publish:dryRunMode"));
|
||||
}
|
||||
|
||||
try {
|
||||
const context = this.publishService.getContextFromEnv(options);
|
||||
await this.publishService.execute(context);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
t("common.executionFailed", { error: error instanceof Error ? error.message : error }),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-d, --dry-run",
|
||||
description: t("common.options.dryRun"),
|
||||
})
|
||||
parseDryRun(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-c, --ci",
|
||||
description: t("common.options.ci"),
|
||||
})
|
||||
parseCi(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-p, --prerelease <tag>",
|
||||
description: t("publish:options.prerelease"),
|
||||
})
|
||||
parsePrerelease(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-r, --rehearsal",
|
||||
description: t("publish:options.rehearsal"),
|
||||
})
|
||||
parseRehearsal(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
90
commands/publish/src/publish.config.ts
Normal file
90
commands/publish/src/publish.config.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { z } from "@spaceflow/core";
|
||||
|
||||
/** publish 命令配置 schema */
|
||||
export const publishSchema = z.object({
|
||||
/** monorepo 发布模式配置 */
|
||||
monorepo: z
|
||||
.object({
|
||||
/** 是否启用 monorepo 发布模式 */
|
||||
enabled: z.boolean().default(false),
|
||||
/** 是否传递依赖变更(依赖的包变更时,依赖方也发布) */
|
||||
propagateDeps: z.boolean().default(true),
|
||||
})
|
||||
.optional(),
|
||||
changelog: z
|
||||
.object({
|
||||
/** changelog 文件输出目录 */
|
||||
infileDir: z.string().default(".").optional(),
|
||||
preset: z
|
||||
.object({
|
||||
/** preset 名称,默认 conventionalcommits */
|
||||
name: z.string().default("conventionalcommits").optional(),
|
||||
/** commit type 到 section 的映射 */
|
||||
type: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
section: z.string(),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
/** npm 发布配置 */
|
||||
npm: z
|
||||
.object({
|
||||
/** 是否发布到 npm registry */
|
||||
publish: z.boolean().default(false),
|
||||
/** 包管理器,npm 或 pnpm */
|
||||
packageManager: z.enum(["npm", "pnpm"]).default("npm"),
|
||||
/** npm registry 地址 */
|
||||
registry: z.string().optional(),
|
||||
/** npm tag,如 latest、beta、next */
|
||||
tag: z.string().default("latest"),
|
||||
/** 是否忽略 package.json 中的版本号 */
|
||||
ignoreVersion: z.boolean().default(true),
|
||||
/** npm version 命令额外参数 */
|
||||
versionArgs: z.array(z.string()).default(["--workspaces false"]),
|
||||
/** npm/pnpm publish 命令额外参数 */
|
||||
publishArgs: z.array(z.string()).default([]),
|
||||
})
|
||||
.optional(),
|
||||
release: z
|
||||
.object({
|
||||
host: z.string().default("localhost"),
|
||||
assetSourcemap: z
|
||||
.object({
|
||||
path: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
assets: z
|
||||
.array(
|
||||
z.object({
|
||||
path: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
.optional(),
|
||||
/** git 配置 */
|
||||
git: z
|
||||
.object({
|
||||
/** 允许发布的分支列表 */
|
||||
requireBranch: z.array(z.string()).default(["main", "dev", "develop"]),
|
||||
/** 分支锁定时允许推送的用户名白名单(如 CI 机器人) */
|
||||
pushWhitelistUsernames: z.array(z.string()).default([]),
|
||||
/** 是否在发布时锁定分支 */
|
||||
lockBranch: z.boolean().default(true),
|
||||
})
|
||||
.optional(),
|
||||
/** release-it hooks 配置,如 before:bump, after:bump 等 */
|
||||
hooks: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
|
||||
});
|
||||
|
||||
/** publish 配置类型(从 schema 推导) */
|
||||
export type PublishConfig = z.infer<typeof publishSchema>;
|
||||
12
commands/publish/src/publish.module.ts
Normal file
12
commands/publish/src/publish.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { GitProviderModule, ConfigReaderModule, ciConfig } from "@spaceflow/core";
|
||||
import { PublishCommand } from "./publish.command";
|
||||
import { PublishService } from "./publish.service";
|
||||
import { MonorepoService } from "./monorepo.service";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forFeature(ciConfig), GitProviderModule.forFeature(), ConfigReaderModule],
|
||||
providers: [PublishCommand, PublishService, MonorepoService],
|
||||
})
|
||||
export class PublishModule {}
|
||||
602
commands/publish/src/publish.service.ts
Normal file
602
commands/publish/src/publish.service.ts
Normal file
@@ -0,0 +1,602 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import {
|
||||
GitProviderService,
|
||||
ConfigReaderService,
|
||||
type BranchProtection,
|
||||
type CiConfig,
|
||||
} from "@spaceflow/core";
|
||||
import { type PublishConfig } from "./publish.config";
|
||||
import { MonorepoService, type PackageInfo } from "./monorepo.service";
|
||||
import type { Config } from "release-it";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const releaseItModule = require("release-it") as
|
||||
| { default: (opts: Config & Record<string, unknown>) => Promise<void> }
|
||||
| ((opts: Config & Record<string, unknown>) => Promise<void>);
|
||||
|
||||
const releaseIt = typeof releaseItModule === "function" ? releaseItModule : releaseItModule.default;
|
||||
|
||||
import type { PublishOptions } from "./publish.command";
|
||||
|
||||
export interface PublishContext extends PublishOptions {
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch: string;
|
||||
}
|
||||
|
||||
export interface PublishResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
protection?: BranchProtection | null;
|
||||
}
|
||||
|
||||
interface ReleaseItConfigOptions {
|
||||
dryRun: boolean;
|
||||
prerelease?: string;
|
||||
ci: boolean;
|
||||
/** 预演模式:执行 hooks 但不修改文件/git */
|
||||
rehearsal: boolean;
|
||||
/** 包目录(monorepo 模式)或 "."(单包模式) */
|
||||
pkgDir: string;
|
||||
/** 包名称(monorepo 模式)或 undefined(单包模式) */
|
||||
pkgName?: string;
|
||||
/** package.json 所在目录的名称,只有最后一节 */
|
||||
pkgBase: string;
|
||||
publishConf: PublishConfig;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PublishService {
|
||||
private cleanupOnExit: (() => void) | null = null;
|
||||
private uncaughtExceptionHandler: ((err: Error) => void) | null = null;
|
||||
private branchUnlocked = false;
|
||||
|
||||
constructor(
|
||||
protected readonly gitProvider: GitProviderService,
|
||||
protected readonly configService: ConfigService,
|
||||
protected readonly configReader: ConfigReaderService,
|
||||
protected readonly monorepoService: MonorepoService,
|
||||
) {}
|
||||
|
||||
getContextFromEnv(options: PublishOptions): PublishContext {
|
||||
this.gitProvider.validateConfig();
|
||||
|
||||
const ciConf = this.configService.get<CiConfig>("ci");
|
||||
const repository = ciConf?.repository;
|
||||
const branch = ciConf?.refName;
|
||||
|
||||
if (!repository) {
|
||||
throw new Error("缺少配置 ci.repository (环境变量 GITHUB_REPOSITORY)");
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
throw new Error("缺少配置 ci.refName (环境变量 GITHUB_REF_NAME)");
|
||||
}
|
||||
|
||||
const [owner, repo] = repository.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`ci.repository 格式不正确,期望 "owner/repo",实际: "${repository}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
dryRun: options.dryRun ?? false,
|
||||
prerelease: options.prerelease,
|
||||
ci: options.ci,
|
||||
rehearsal: options.rehearsal ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(context: PublishContext): Promise<PublishResult> {
|
||||
const publishConf = this.configReader.getPluginConfig<PublishConfig>("publish");
|
||||
const monorepoConf = publishConf.monorepo;
|
||||
|
||||
// CI 环境下自动 fetch tags,确保 release-it 能正确计算版本
|
||||
if (context.ci) {
|
||||
await this.ensureTagsFetched();
|
||||
}
|
||||
|
||||
// 检查是否启用 monorepo 模式
|
||||
if (monorepoConf?.enabled) {
|
||||
return this.executeMonorepo(context, publishConf);
|
||||
}
|
||||
|
||||
// 单包发布模式
|
||||
return this.executeSinglePackage(context, publishConf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Monorepo 发布模式:扫描变更包,按依赖顺序发布
|
||||
*/
|
||||
private async executeMonorepo(
|
||||
context: PublishContext,
|
||||
publishConf: PublishConfig,
|
||||
): Promise<PublishResult> {
|
||||
const { dryRun } = context;
|
||||
|
||||
console.log("\n📦 Monorepo 发布模式");
|
||||
console.log("=".repeat(50));
|
||||
|
||||
const propagateDeps = publishConf.monorepo?.propagateDeps ?? true;
|
||||
|
||||
// 分析变更包
|
||||
const analysis = await this.monorepoService.analyze(dryRun, propagateDeps);
|
||||
|
||||
if (analysis.packagesToPublish.length === 0) {
|
||||
console.log("\n✅ 没有需要发布的包");
|
||||
return { success: true, message: "没有需要发布的包" };
|
||||
}
|
||||
|
||||
console.log(`\n🚀 将发布 ${analysis.packagesToPublish.length} 个包`);
|
||||
|
||||
await this.handleBegin(context, publishConf);
|
||||
|
||||
try {
|
||||
// 按顺序发布每个包
|
||||
for (let i = 0; i < analysis.packagesToPublish.length; i++) {
|
||||
const pkg = analysis.packagesToPublish[i];
|
||||
console.log(`\n[${i + 1}/${analysis.packagesToPublish.length}] 发布 ${pkg.name}`);
|
||||
console.log("-".repeat(40));
|
||||
|
||||
await this.executePackageRelease(context, publishConf, pkg);
|
||||
}
|
||||
|
||||
await this.handleEnd(context, publishConf);
|
||||
return {
|
||||
success: true,
|
||||
message: `成功发布 ${analysis.packagesToPublish.length} 个包`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("\n❌ Monorepo 发布失败:", error instanceof Error ? error.message : error);
|
||||
try {
|
||||
await this.handleEnd(context, publishConf);
|
||||
} catch (unlockError) {
|
||||
console.error(
|
||||
"⚠️ 解锁分支失败:",
|
||||
unlockError instanceof Error ? unlockError.message : unlockError,
|
||||
);
|
||||
}
|
||||
return { success: false, message: "Monorepo 发布失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布单个包(monorepo 模式)
|
||||
*/
|
||||
private async executePackageRelease(
|
||||
context: PublishContext,
|
||||
publishConf: PublishConfig,
|
||||
pkg: PackageInfo,
|
||||
): Promise<void> {
|
||||
const { dryRun, prerelease, ci, rehearsal } = context;
|
||||
|
||||
if (rehearsal) {
|
||||
console.log(`🎭 [REHEARSAL] 将发布包: ${pkg.name} (${pkg.dir})`);
|
||||
} else if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将发布包: ${pkg.name} (${pkg.dir})`);
|
||||
}
|
||||
|
||||
const pkgDir = join(process.cwd(), pkg.dir);
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
const config = this.buildReleaseItConfig({
|
||||
dryRun,
|
||||
prerelease,
|
||||
ci,
|
||||
rehearsal,
|
||||
pkgDir,
|
||||
pkgName: pkg.name,
|
||||
pkgBase: pkg.dir.split("/").pop() || pkg.dir,
|
||||
publishConf,
|
||||
});
|
||||
|
||||
// 切换到包目录运行 release-it,确保读取正确的 package.json
|
||||
process.chdir(pkgDir);
|
||||
try {
|
||||
await releaseIt(config);
|
||||
console.log(`✅ ${pkg.name} 发布完成`);
|
||||
} finally {
|
||||
// 恢复原工作目录
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单包发布模式
|
||||
*/
|
||||
private async executeSinglePackage(
|
||||
context: PublishContext,
|
||||
publishConf: PublishConfig,
|
||||
): Promise<PublishResult> {
|
||||
const { dryRun, prerelease, ci, rehearsal } = context;
|
||||
|
||||
await this.handleBegin(context, publishConf);
|
||||
|
||||
try {
|
||||
const config = this.buildReleaseItConfig({
|
||||
dryRun,
|
||||
prerelease,
|
||||
ci,
|
||||
rehearsal,
|
||||
pkgDir: process.cwd(),
|
||||
pkgBase: process.cwd().split("/").pop() || ".",
|
||||
publishConf,
|
||||
});
|
||||
|
||||
await releaseIt(config);
|
||||
} catch (error) {
|
||||
console.error("执行失败:", error instanceof Error ? error.message : error);
|
||||
try {
|
||||
await this.handleEnd(context, publishConf);
|
||||
} catch (unlockError) {
|
||||
console.error(
|
||||
"⚠️ 解锁分支失败:",
|
||||
unlockError instanceof Error ? unlockError.message : unlockError,
|
||||
);
|
||||
}
|
||||
return { success: false, message: "执行失败" };
|
||||
}
|
||||
|
||||
await this.handleEnd(context, publishConf);
|
||||
return { success: true, message: "执行完成", protection: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 release-it 配置(公共方法)
|
||||
*/
|
||||
private buildReleaseItConfig(opts: ReleaseItConfigOptions): Config & Record<string, unknown> {
|
||||
const { dryRun, prerelease, ci, rehearsal, pkgName, pkgBase, publishConf } = opts;
|
||||
const changelogConf = publishConf.changelog;
|
||||
const releaseConf = publishConf.release;
|
||||
const npmConf = publishConf.npm;
|
||||
const gitConf = publishConf.git;
|
||||
|
||||
// 预演模式:设置环境变量,hooks 可以通过它判断当前模式
|
||||
if (rehearsal) {
|
||||
process.env.PUBLISH_REHEARSAL = "true";
|
||||
}
|
||||
|
||||
const isMonorepo = !!pkgName;
|
||||
const tagMatchOpts = !prerelease ? { tagExclude: `*[-]*` } : {};
|
||||
|
||||
// monorepo: @scope/pkg@1.0.0, 单包: v1.0.0
|
||||
const tagPrefix = isMonorepo ? `${pkgName}@` : "v";
|
||||
const tagName = isMonorepo ? `${pkgName}@\${version}` : "v${version}";
|
||||
const releaseName = isMonorepo ? `${pkgName}@\${version}` : "v${version}";
|
||||
const releaseTitle = isMonorepo ? `🎉 ${pkgName}@\${version}` : "🎉 v${version}";
|
||||
// monorepo 模式下在包目录运行,git commitsPath 为 "."
|
||||
const commitsPath = ".";
|
||||
const commitMessage = isMonorepo
|
||||
? `chore(${pkgBase}): released version \${version} [no ci]`
|
||||
: "chore: released version v${version} [no ci]";
|
||||
|
||||
// 预演模式:禁用文件/git 修改,但保留 hooks
|
||||
// dryRun 模式:完全跳过所有操作(包括 hooks)
|
||||
const skipWrite = dryRun || rehearsal;
|
||||
|
||||
return {
|
||||
"dry-run": dryRun,
|
||||
d: dryRun,
|
||||
ci: ci || dryRun, // dry-run 模式也启用 ci 模式,避免交互式提示
|
||||
plugins: {
|
||||
// 预演模式下禁用 changelog 写入
|
||||
...(!skipWrite && changelogConf
|
||||
? {
|
||||
"@release-it/conventional-changelog": {
|
||||
// 现在在包目录下运行,使用相对路径
|
||||
infile: join(
|
||||
changelogConf.infileDir || ".",
|
||||
`CHANGELOG${!prerelease ? "" : "-" + prerelease.toUpperCase()}.md`,
|
||||
),
|
||||
preset: {
|
||||
name: changelogConf.preset?.name || "conventionalcommits",
|
||||
types: changelogConf.preset?.type || [],
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
// 预演模式下禁用 release 创建
|
||||
...(!skipWrite && releaseConf
|
||||
? {
|
||||
"release-it-gitea": {
|
||||
releaseTitle,
|
||||
releaseNotes: this.formatReleaseNotes,
|
||||
assets: this.buildReleaseAssets(releaseConf),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
git: {
|
||||
// 预演模式:禁用 push/commit/tag
|
||||
push: !skipWrite,
|
||||
commit: !skipWrite,
|
||||
tag: !skipWrite,
|
||||
tagName,
|
||||
commitsPath,
|
||||
commitMessage,
|
||||
requireCommits: false,
|
||||
requireCommitsFail: false,
|
||||
getLatestTagFromAllRefs: true,
|
||||
requireBranch: (gitConf?.requireBranch ?? ["main", "dev", "develop"]) as any,
|
||||
requireCleanWorkingDir: !skipWrite,
|
||||
...(isMonorepo ? { tagMatch: `${tagPrefix}*` } : {}),
|
||||
...tagMatchOpts,
|
||||
},
|
||||
// 预演模式:禁用 npm
|
||||
// 如果使用 pnpm,禁用内置 npm 发布,但保留版本更新功能
|
||||
npm: skipWrite
|
||||
? (false as any)
|
||||
: {
|
||||
// pnpm 模式:禁用 publish(通过 hooks 实现),但保留版本更新
|
||||
publish: npmConf?.packageManager === "pnpm" ? false : (npmConf?.publish ?? false),
|
||||
ignoreVersion: npmConf?.ignoreVersion ?? true,
|
||||
tag: prerelease || npmConf?.tag || "latest",
|
||||
versionArgs: npmConf?.versionArgs ?? ["--workspaces false"],
|
||||
publishArgs: npmConf?.publishArgs ?? [],
|
||||
...(npmConf?.registry ? { publishConfig: { registry: npmConf.registry } } : {}),
|
||||
},
|
||||
github: {
|
||||
release: false,
|
||||
releaseName: `Release ${releaseName}`,
|
||||
autoGenerate: true,
|
||||
skipChecks: true,
|
||||
host: releaseConf?.host || "localhost",
|
||||
},
|
||||
// 合并用户 hooks 和内部 pnpm 发布 hook
|
||||
hooks: this.buildHooks({
|
||||
userHooks: publishConf.hooks,
|
||||
npmConf,
|
||||
prerelease,
|
||||
skipWrite,
|
||||
dryRun,
|
||||
rehearsal,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 release notes
|
||||
*/
|
||||
private formatReleaseNotes(t: { changelog: string }): string {
|
||||
const lines = t.changelog.split("\n");
|
||||
const cateLines = lines.filter(
|
||||
(line: string) => line.startsWith("###") || line.startsWith("* "),
|
||||
);
|
||||
|
||||
const cateMap: Record<string, string[]> = {};
|
||||
let currentCate = "";
|
||||
|
||||
cateLines.forEach((line: string) => {
|
||||
if (line.startsWith("###")) {
|
||||
currentCate = line;
|
||||
cateMap[currentCate] = cateMap[currentCate] || [];
|
||||
} else {
|
||||
cateMap[currentCate].push(line);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(cateMap)
|
||||
.map(([cate, catLines]) => `${cate}\n\n${catLines.join("\n")}\n`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 release assets 配置
|
||||
*/
|
||||
private buildReleaseAssets(releaseConf: NonNullable<PublishConfig["release"]>) {
|
||||
const assets = releaseConf.assetSourcemap
|
||||
? [
|
||||
{
|
||||
path: releaseConf.assetSourcemap.path,
|
||||
name: releaseConf.assetSourcemap.name,
|
||||
type: "zip",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
return assets.concat(releaseConf.assets || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 hooks 配置,合并用户 hooks 和内部 pnpm 发布逻辑
|
||||
*/
|
||||
private buildHooks(opts: {
|
||||
userHooks?: Record<string, string | string[]>;
|
||||
npmConf?: PublishConfig["npm"];
|
||||
prerelease?: string;
|
||||
skipWrite: boolean;
|
||||
dryRun: boolean;
|
||||
rehearsal: boolean;
|
||||
}): Record<string, string | string[]> | undefined {
|
||||
const { userHooks, npmConf, prerelease, skipWrite, dryRun, rehearsal } = opts;
|
||||
|
||||
// dryRun 模式下不执行任何 hooks
|
||||
if (dryRun && !rehearsal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 复制用户 hooks
|
||||
let hooks: Record<string, string | string[]> = { ...userHooks };
|
||||
|
||||
// rehearsal 模式下过滤 after 前缀的 hooks(不执行实际的发布后操作)
|
||||
if (rehearsal) {
|
||||
hooks = Object.fromEntries(
|
||||
Object.entries(hooks).filter(([key]) => !key.startsWith("after:")),
|
||||
);
|
||||
// rehearsal 模式下也不添加 pnpm publish
|
||||
return Object.keys(hooks).length > 0 ? hooks : undefined;
|
||||
}
|
||||
|
||||
// 如果使用 pnpm 且需要发布
|
||||
if (npmConf?.packageManager === "pnpm" && npmConf?.publish && !skipWrite) {
|
||||
const tag = prerelease || npmConf.tag || "latest";
|
||||
const publishArgs = npmConf.publishArgs ?? [];
|
||||
const registry = npmConf.registry;
|
||||
|
||||
// 构建 pnpm publish 命令
|
||||
// monorepo 模式下已切换到包目录,不需要 -C 参数
|
||||
let publishCmd = `pnpm publish --tag ${tag} --no-git-checks`;
|
||||
if (registry) {
|
||||
publishCmd += ` --registry ${registry}`;
|
||||
}
|
||||
if (publishArgs.length > 0) {
|
||||
publishCmd += ` ${publishArgs.join(" ")}`;
|
||||
}
|
||||
|
||||
// 合并到 after:bump hook
|
||||
const existingAfterBump = hooks["after:bump"];
|
||||
if (existingAfterBump) {
|
||||
hooks["after:bump"] = Array.isArray(existingAfterBump)
|
||||
? [...existingAfterBump, publishCmd]
|
||||
: [existingAfterBump, publishCmd];
|
||||
} else {
|
||||
hooks["after:bump"] = publishCmd;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(hooks).length > 0 ? hooks : undefined;
|
||||
}
|
||||
|
||||
protected async handleBegin(
|
||||
context: PublishContext,
|
||||
publishConf: PublishConfig,
|
||||
): Promise<PublishResult> {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
const shouldLockBranch = publishConf.git?.lockBranch ?? true;
|
||||
|
||||
if (!shouldLockBranch) {
|
||||
console.log(`⏭️ 跳过分支锁定(已禁用)`);
|
||||
return { success: true, message: "分支锁定已禁用", protection: null };
|
||||
}
|
||||
|
||||
const pushWhitelistUsernames = [...(publishConf.git?.pushWhitelistUsernames ?? [])];
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将锁定分支: ${owner}/${repo}#${branch}`);
|
||||
return { success: true, message: "DRY-RUN: 分支锁定已跳过", protection: null };
|
||||
}
|
||||
|
||||
console.log(`🔒 正在锁定分支: ${owner}/${repo}#${branch}`);
|
||||
const protection = await this.gitProvider.lockBranch(owner, repo, branch, {
|
||||
pushWhitelistUsernames,
|
||||
});
|
||||
console.log(`✅ 分支已锁定`);
|
||||
console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
|
||||
if (pushWhitelistUsernames?.length) {
|
||||
console.log(` 允许推送用户: ${pushWhitelistUsernames.join(", ")}`);
|
||||
} else {
|
||||
console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
|
||||
}
|
||||
|
||||
// 注册进程退出时的清理函数,确保即使 release-it 调用 process.exit() 也能解锁分支
|
||||
this.branchUnlocked = false;
|
||||
this.cleanupOnExit = () => {
|
||||
if (this.branchUnlocked) return;
|
||||
this.branchUnlocked = true;
|
||||
console.log("\n🔓 进程退出,正在同步解锁分支...");
|
||||
try {
|
||||
this.unlockBranchSync(context, publishConf);
|
||||
} catch (e) {
|
||||
console.error("⚠️ 同步解锁分支失败:", e instanceof Error ? e.message : e);
|
||||
}
|
||||
};
|
||||
this.uncaughtExceptionHandler = (err: Error) => {
|
||||
console.error("\n❌ 未捕获的异常:", err.message);
|
||||
if (this.cleanupOnExit) this.cleanupOnExit();
|
||||
process.exit(1);
|
||||
};
|
||||
process.on("exit", this.cleanupOnExit);
|
||||
process.on("SIGINT", this.cleanupOnExit);
|
||||
process.on("SIGTERM", this.cleanupOnExit);
|
||||
process.on("uncaughtException", this.uncaughtExceptionHandler);
|
||||
|
||||
return { success: true, message: "分支锁定完成", protection };
|
||||
}
|
||||
|
||||
protected async handleEnd(
|
||||
context: PublishContext,
|
||||
publishConf: PublishConfig,
|
||||
): Promise<PublishResult> {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
const shouldLockBranch = publishConf.git?.lockBranch ?? true;
|
||||
|
||||
if (!shouldLockBranch) {
|
||||
return { success: true, message: "分支锁定已禁用,无需解锁", protection: null };
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`🔍 [DRY-RUN] 将解锁分支: ${owner}/${repo}#${branch}`);
|
||||
return { success: true, message: "DRY-RUN: 分支解锁已跳过", protection: null };
|
||||
}
|
||||
|
||||
console.log(`🔓 正在解锁分支: ${owner}/${repo}#${branch}`);
|
||||
const protection = await this.gitProvider.unlockBranch(owner, repo, branch);
|
||||
|
||||
// 标记已解锁,防止清理函数重复执行
|
||||
this.branchUnlocked = true;
|
||||
|
||||
// 移除事件监听器
|
||||
if (this.cleanupOnExit) {
|
||||
process.removeListener("exit", this.cleanupOnExit);
|
||||
process.removeListener("SIGINT", this.cleanupOnExit);
|
||||
process.removeListener("SIGTERM", this.cleanupOnExit);
|
||||
this.cleanupOnExit = null;
|
||||
}
|
||||
if (this.uncaughtExceptionHandler) {
|
||||
process.removeListener("uncaughtException", this.uncaughtExceptionHandler);
|
||||
this.uncaughtExceptionHandler = null;
|
||||
}
|
||||
|
||||
if (protection) {
|
||||
console.log(`✅ 分支已解锁`);
|
||||
console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
|
||||
console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
|
||||
return { success: true, message: "分支解锁完成", protection };
|
||||
} else {
|
||||
console.log(`✅ 分支本身没有保护规则,无需解锁`);
|
||||
return { success: true, message: "分支本身没有保护规则,无需解锁", protection: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步解锁分支(用于进程退出时的清理)
|
||||
*/
|
||||
private unlockBranchSync(context: PublishContext, publishConf: PublishConfig): void {
|
||||
const { owner, repo, branch, dryRun } = context;
|
||||
const shouldLockBranch = publishConf.git?.lockBranch ?? true;
|
||||
|
||||
if (!shouldLockBranch || dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gitProvider.unlockBranchSync(owner, repo, branch);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 git tags 已获取(CI 环境中 shallow clone 可能缺失 tags)
|
||||
* 这对于 release-it 正确计算版本号至关重要
|
||||
*/
|
||||
private async ensureTagsFetched(): Promise<void> {
|
||||
try {
|
||||
// 检查是否有 tags
|
||||
const existingTags = execSync("git tag --list 2>/dev/null || echo ''", {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
if (!existingTags) {
|
||||
console.log("🏷️ 正在获取 git tags...");
|
||||
execSync("git fetch --tags --force", { stdio: "inherit" });
|
||||
console.log("✅ Git tags 已获取");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ 获取 git tags 失败:", error instanceof Error ? error.message : error);
|
||||
console.warn(
|
||||
" 版本计算可能不准确,建议在 CI checkout 时添加 fetch-depth: 0 和 fetch-tags: true",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
commands/publish/tsconfig.json
Normal file
5
commands/publish/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../core/tsconfig.skill.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
533
commands/review/CHANGELOG.md
Normal file
533
commands/review/CHANGELOG.md
Normal file
@@ -0,0 +1,533 @@
|
||||
# Changelog
|
||||
|
||||
## [0.29.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.28.0...@spaceflow/review@0.29.0) (2026-02-15)
|
||||
|
||||
### 新特性
|
||||
|
||||
* **cli:** 新增 MCP Server 命令并集成 review 扩展的 MCP 工具 ([b794b36](https://git.bjxgj.com/xgj/spaceflow/commit/b794b36d90788c7eb4cbb253397413b4a080ae83))
|
||||
* **cli:** 新增 MCP Server 导出类型支持 ([9568cbd](https://git.bjxgj.com/xgj/spaceflow/commit/9568cbd14d4cfbdedaf2218379c72337af6db271))
|
||||
* **core:** 为所有命令添加 i18n 国际化支持 ([867c5d3](https://git.bjxgj.com/xgj/spaceflow/commit/867c5d3eccc285c8a68803b8aa2f0ffb86a94285))
|
||||
* **core:** 新增 GitLab 平台适配器并完善配置支持 ([47be9ad](https://git.bjxgj.com/xgj/spaceflow/commit/47be9adfa90944a9cb183e03286a7a96fec747f1))
|
||||
* **core:** 新增 Logger 全局日志工具并支持 plain/tui 双模式渲染 ([8baae7c](https://git.bjxgj.com/xgj/spaceflow/commit/8baae7c24139695a0e379e1c874023cd61dfc41b))
|
||||
* **docs:** 新增 VitePress 文档站点并完善项目文档 ([a79d620](https://git.bjxgj.com/xgj/spaceflow/commit/a79d6208e60390a44fa4c94621eb41ae20159e98))
|
||||
* **mcp:** 新增 MCP Inspector 交互式调试支持并优化工具日志输出 ([05fd2ee](https://git.bjxgj.com/xgj/spaceflow/commit/05fd2ee941c5f6088b769d1127cb7c0615626f8c))
|
||||
* **review:** 为 MCP 服务添加 i18n 国际化支持 ([a749054](https://git.bjxgj.com/xgj/spaceflow/commit/a749054eb73b775a5f5973ab1b86c04f2b2ddfba))
|
||||
* **review:** 新增规则级 includes 解析测试并修复文件级/规则级 includes 过滤逻辑 ([4baca71](https://git.bjxgj.com/xgj/spaceflow/commit/4baca71c17782fb92a95b3207f9c61e0b410b9ff))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
* **actions:** 修正 pnpm setup 命令调用方式 ([8f014fa](https://git.bjxgj.com/xgj/spaceflow/commit/8f014fa90b74e20de4c353804d271b3ef6f1288f))
|
||||
* **mcp:** 添加 -y 选项确保 Inspector 自动安装依赖 ([a9201f7](https://git.bjxgj.com/xgj/spaceflow/commit/a9201f74bd9ddc5eba92beaaa676f377842863e0))
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **claude:** 移除 .claude 目录及其 .gitignore 配置文件 ([91916a9](https://git.bjxgj.com/xgj/spaceflow/commit/91916a99f65da31c1d34e6f75b5cbea1d331ba35))
|
||||
* **cli:** 优化依赖安装流程并支持 .spaceflow 目录配置 ([5977631](https://git.bjxgj.com/xgj/spaceflow/commit/597763183eaa61bb024bba2703d75239650b54fb))
|
||||
* **cli:** 拆分 CLI 为独立包并重构扩展加载机制 ([b385d28](https://git.bjxgj.com/xgj/spaceflow/commit/b385d281575f29b823bb6dc4229a396a29c0e226))
|
||||
* **cli:** 移除 ExtensionModule 并优化扩展加载机制 ([8f7077d](https://git.bjxgj.com/xgj/spaceflow/commit/8f7077deaef4e5f4032662ff5ac925cd3c07fdb6))
|
||||
* **cli:** 调整依赖顺序并格式化导入语句 ([32a9c1c](https://git.bjxgj.com/xgj/spaceflow/commit/32a9c1cf834725a20f93b1f8f60b52692841a3e5))
|
||||
* **cli:** 重构 getPluginConfigFromPackageJson 方法以提高代码可读性 ([f5f6ed9](https://git.bjxgj.com/xgj/spaceflow/commit/f5f6ed9858cc4ca670e30fac469774bdc8f7b005))
|
||||
* **cli:** 重构扩展配置格式,支持 flow/command/skill 三种导出类型 ([958dc13](https://git.bjxgj.com/xgj/spaceflow/commit/958dc130621f78bbcc260224da16a5f16ae0b2b1))
|
||||
* **core:** 为 build/clear/commit 命令添加国际化支持 ([de82cb2](https://git.bjxgj.com/xgj/spaceflow/commit/de82cb2f1ed8cef0e446a2d42a1bf1f091e9c421))
|
||||
* **core:** 优化 list 命令输出格式并修复 MCP Inspector 包管理器兼容性 ([a019829](https://git.bjxgj.com/xgj/spaceflow/commit/a019829d3055c083aeb86ed60ce6629d13012d91))
|
||||
* **core:** 将 rspack 配置和工具函数中的 @spaceflow/cli 引用改为 @spaceflow/core ([3c301c6](https://git.bjxgj.com/xgj/spaceflow/commit/3c301c60f3e61b127db94481f5a19307f5ef00eb))
|
||||
* **core:** 将扩展依赖从 @spaceflow/cli 迁移到 @spaceflow/core ([6f9ffd4](https://git.bjxgj.com/xgj/spaceflow/commit/6f9ffd4061cecae4faaf3d051e3ca98a0b42b01f))
|
||||
* **core:** 提取 source 处理和包管理器工具函数到共享模块 ([ab3ff00](https://git.bjxgj.com/xgj/spaceflow/commit/ab3ff003d1cd586c0c4efc7841e6a93fe3477ace))
|
||||
* **core:** 新增 getEnvFilePaths 工具函数统一管理 .env 文件路径优先级 ([809fa18](https://git.bjxgj.com/xgj/spaceflow/commit/809fa18f3d0b8eabcb068988bab53d548eaf03ea))
|
||||
* **core:** 新增远程仓库规则拉取功能并支持 Git API 获取目录内容 ([69ade16](https://git.bjxgj.com/xgj/spaceflow/commit/69ade16c9069f9e1a90b3ef56dc834e33a3c0650))
|
||||
* **core:** 统一 LogLevel 类型定义并支持字符串/数字双模式 ([557f6b0](https://git.bjxgj.com/xgj/spaceflow/commit/557f6b0bc39fcfb0e3f773836cbbf08c1a8790ae))
|
||||
* **core:** 重构配置读取逻辑,新增 ConfigReaderService 并支持 .spaceflowrc 配置文件 ([72e88ce](https://git.bjxgj.com/xgj/spaceflow/commit/72e88ced63d03395923cdfb113addf4945162e54))
|
||||
* **i18n:** 将 locales 导入从命令文件迁移至扩展入口文件 ([0da5d98](https://git.bjxgj.com/xgj/spaceflow/commit/0da5d9886296c4183b24ad8c56140763f5a870a4))
|
||||
* **i18n:** 移除扩展元数据中的 locales 字段并改用 side-effect 自动注册 ([2c7d488](https://git.bjxgj.com/xgj/spaceflow/commit/2c7d488a9dfa59a99b95e40e3c449c28c2d433d8))
|
||||
* **mcp:** 使用 DTO + Swagger 装饰器替代手动 JSON Schema 定义 ([87ec262](https://git.bjxgj.com/xgj/spaceflow/commit/87ec26252dd295536bb090ae8b7e418eec96e1bd))
|
||||
* **mcp:** 升级 MCP SDK API 并优化 Inspector 调试配置 ([176d04a](https://git.bjxgj.com/xgj/spaceflow/commit/176d04a73fbbb8d115520d922f5fedb9a2961aa6))
|
||||
* **mcp:** 将 MCP 元数据存储从 Reflect Metadata 改为静态属性以支持跨模块访问 ([cac0ea2](https://git.bjxgj.com/xgj/spaceflow/commit/cac0ea2029e1b504bc4278ce72b3aa87fff88c84))
|
||||
* **test:** 迁移测试框架从 Jest 到 Vitest ([308f9d4](https://git.bjxgj.com/xgj/spaceflow/commit/308f9d49089019530588344a5e8880f5b6504a6a))
|
||||
* 优化构建流程并调整 MCP/review 日志输出级别 ([74072c0](https://git.bjxgj.com/xgj/spaceflow/commit/74072c04be7a45bfc0ab53b636248fe5c0e1e42a))
|
||||
* 将 .spaceflow/package.json 纳入版本控制并自动添加到根项目依赖 ([ab83d25](https://git.bjxgj.com/xgj/spaceflow/commit/ab83d2579cb5414ee3d78a9768fac2147a3d1ad9))
|
||||
* 将 GiteaSdkModule/GiteaSdkService 重命名为 GitProviderModule/GitProviderService ([462f492](https://git.bjxgj.com/xgj/spaceflow/commit/462f492bc2607cf508c5011d181c599cf17e00c9))
|
||||
* 恢复 pnpm catalog 配置并移除 .spaceflow 工作区导入器 ([217387e](https://git.bjxgj.com/xgj/spaceflow/commit/217387e2e8517a08162e9bcaf604893fd9bca736))
|
||||
* 迁移扩展依赖到 .spaceflow 工作区并移除 pnpm catalog ([c457c0f](https://git.bjxgj.com/xgj/spaceflow/commit/c457c0f8918171f1856b88bc007921d76c508335))
|
||||
* 重构 Extension 安装机制为 pnpm workspace 模式 ([469b12e](https://git.bjxgj.com/xgj/spaceflow/commit/469b12eac28f747b628e52a5125a3d5a538fba39))
|
||||
* 重构插件加载改为扩展模式 ([0e6e140](https://git.bjxgj.com/xgj/spaceflow/commit/0e6e140b19ea2cf6084afc261c555d2083fe04f9))
|
||||
|
||||
### 文档更新
|
||||
|
||||
* **guide:** 更新编辑器集成文档,补充四种导出类型说明和 MCP 注册机制 ([19a7409](https://git.bjxgj.com/xgj/spaceflow/commit/19a7409092c89d002f11ee51ebcb6863118429bd))
|
||||
* **guide:** 更新配置文件位置说明并补充 RC 文件支持 ([2214dc4](https://git.bjxgj.com/xgj/spaceflow/commit/2214dc4e197221971f5286b38ceaa6fcbcaa7884))
|
||||
|
||||
### 测试用例
|
||||
|
||||
* **core:** 新增 GiteaAdapter 完整单元测试并实现自动检测 provider 配置 ([c74f745](https://git.bjxgj.com/xgj/spaceflow/commit/c74f7458aed91ac7d12fb57ef1c24b3d2917c406))
|
||||
* **review:** 新增 DeletionImpactService 测试覆盖并配置 coverage 工具 ([50bfbfe](https://git.bjxgj.com/xgj/spaceflow/commit/50bfbfe37192641f1170ade8f5eb00e0e382af67))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.19.0 [no ci] ([9f747c6](https://git.bjxgj.com/xgj/spaceflow/commit/9f747c617b387e105e92b4a5dcd0f5d3cf51c26d))
|
||||
* **ci-shell:** released version 0.19.0 [no ci] ([59ac30d](https://git.bjxgj.com/xgj/spaceflow/commit/59ac30da6802a9493c33e560ea9121d378597e89))
|
||||
* **ci:** 迁移工作流从 Gitea 到 GitHub 并统一环境变量命名 ([57e3bae](https://git.bjxgj.com/xgj/spaceflow/commit/57e3bae635b324c8c4ea50a9fb667b6241fae0ef))
|
||||
* **cli:** released version 0.19.0 [no ci] ([6b63149](https://git.bjxgj.com/xgj/spaceflow/commit/6b631499e2407a1822395d5f40cec2d725331b78))
|
||||
* **config:** 将 git 推送白名单用户从 "Gitea Actions" 改为 "GiteaActions" ([fdbb865](https://git.bjxgj.com/xgj/spaceflow/commit/fdbb865341e6f02b26fca32b54a33b51bee11cad))
|
||||
* **config:** 将 git 推送白名单用户从 github-actions[bot] 改为 Gitea Actions ([9c39819](https://git.bjxgj.com/xgj/spaceflow/commit/9c39819a9f95f415068f7f0333770b92bc98321b))
|
||||
* **config:** 移除 review-spec 私有仓库依赖 ([8ae18f1](https://git.bjxgj.com/xgj/spaceflow/commit/8ae18f13c441b033d1cbc75119695a5cc5cb6a0b))
|
||||
* **core:** released version 0.1.0 [no ci] ([170fa67](https://git.bjxgj.com/xgj/spaceflow/commit/170fa670e98473c2377120656d23aae835c51997))
|
||||
* **core:** 禁用 i18next 初始化时的 locize.com 推广日志 ([a99fbb0](https://git.bjxgj.com/xgj/spaceflow/commit/a99fbb068441bc623efcf15a1dd7b6bd38c05f38))
|
||||
* **deps:** 移除 pnpm catalog 配置并更新依赖锁定 ([753fb9e](https://git.bjxgj.com/xgj/spaceflow/commit/753fb9e3e43b28054c75158193dc39ab4bab1af5))
|
||||
* **docs:** 统一文档脚本命名,为 VitePress 命令添加 docs: 前缀 ([3cc46ea](https://git.bjxgj.com/xgj/spaceflow/commit/3cc46eab3a600290f5064b8270902e586b9c5af4))
|
||||
* **i18n:** 配置 i18n-ally-next 自动提取键名生成策略 ([753c3dc](https://git.bjxgj.com/xgj/spaceflow/commit/753c3dc3f24f3c03c837d1ec2c505e8e3ce08b11))
|
||||
* **i18n:** 重构 i18n 配置并统一 locales 目录结构 ([3e94037](https://git.bjxgj.com/xgj/spaceflow/commit/3e94037fa6493b3b0e4a12ff6af9f4bea48ae217))
|
||||
* **period-summary:** released version 0.19.0 [no ci] ([b833948](https://git.bjxgj.com/xgj/spaceflow/commit/b83394888ac47ae8d91bfd9317980f56bd322b34))
|
||||
* **publish:** released version 0.21.0 [no ci] ([18f9b20](https://git.bjxgj.com/xgj/spaceflow/commit/18f9b20f26716c88eb389778912de8e7713bffca))
|
||||
* **scripts:** 修正 setup 和 build 脚本的过滤条件,避免重复构建 cli 包 ([ffd2ffe](https://git.bjxgj.com/xgj/spaceflow/commit/ffd2ffedca08fd56cccb6a9fbd2b6bd106e367b6))
|
||||
* **templates:** 新增 MCP 工具插件模板 ([5f6df60](https://git.bjxgj.com/xgj/spaceflow/commit/5f6df60b60553f025414fd102d8a279cde097485))
|
||||
* **workflows:** 为所有 GitHub Actions 工作流添加 GIT_PROVIDER_TYPE 环境变量 ([a463574](https://git.bjxgj.com/xgj/spaceflow/commit/a463574de6755a0848a8d06267f029cb947132b0))
|
||||
* **workflows:** 在发布流程中添加 GIT_PROVIDER_TYPE 环境变量 ([a4bb388](https://git.bjxgj.com/xgj/spaceflow/commit/a4bb3881f39ad351e06c5502df6895805b169a28))
|
||||
* **workflows:** 在发布流程中添加扩展安装步骤 ([716be4d](https://git.bjxgj.com/xgj/spaceflow/commit/716be4d92641ccadb3eaf01af8a51189ec5e9ade))
|
||||
* **workflows:** 将发布流程的 Git 和 NPM 配置从 GitHub 迁移到 Gitea ([6d9acff](https://git.bjxgj.com/xgj/spaceflow/commit/6d9acff06c9a202432eb3d3d5552e6ac972712f5))
|
||||
* **workflows:** 将发布流程的 GITHUB_TOKEN 改为使用 CI_GITEA_TOKEN ([e7fe7b4](https://git.bjxgj.com/xgj/spaceflow/commit/e7fe7b4271802fcdbfc2553b180f710eed419335))
|
||||
* 为所有 commands 包添加 @spaceflow/cli 开发依赖 ([d4e6c83](https://git.bjxgj.com/xgj/spaceflow/commit/d4e6c8344ca736f7e55d7db698482e8fa2445684))
|
||||
* 优化依赖配置并移除 .spaceflow 包依赖 ([be5264e](https://git.bjxgj.com/xgj/spaceflow/commit/be5264e5e0fe1f53bbe3b44a9cb86dd94ab9d266))
|
||||
* 修正 postinstall 脚本命令格式 ([3f0820f](https://git.bjxgj.com/xgj/spaceflow/commit/3f0820f85dee88808de921c3befe2d332f34cc36))
|
||||
* 恢复 pnpm catalog 配置并更新依赖锁定 ([0b2295c](https://git.bjxgj.com/xgj/spaceflow/commit/0b2295c1f906d89ad3ba7a61b04c6e6b94f193ef))
|
||||
* 新增 .spaceflow/pnpm-workspace.yaml 防止被父级 workspace 接管并移除根项目 devDependencies 自动添加逻辑 ([61de3a2](https://git.bjxgj.com/xgj/spaceflow/commit/61de3a2b75e8a19b28563d2a6476158d19f6c5be))
|
||||
* 新增 postinstall 钩子自动执行 setup 脚本 ([64dae0c](https://git.bjxgj.com/xgj/spaceflow/commit/64dae0cb440bd5e777cb790f826ff2d9f8fe65ba))
|
||||
* 移除 postinstall 钩子避免依赖安装时自动执行构建 ([ea1dc85](https://git.bjxgj.com/xgj/spaceflow/commit/ea1dc85ce7d6cf23a98c13e2c21e3c3bcdf7dd79))
|
||||
|
||||
## [0.28.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.27.0...@spaceflow/review@0.28.0) (2026-02-04)
|
||||
|
||||
### 代码重构
|
||||
|
||||
* **verbose:** 扩展 verbose 级别支持至 3 ([c1a0808](https://git.bjxgj.com/xgj/spaceflow/commit/c1a080859e5d25ca1eb3dc7e00a67b32eb172635))
|
||||
|
||||
### 其他修改
|
||||
|
||||
* **ci-scripts:** released version 0.18.0 [no ci] ([e17894a](https://git.bjxgj.com/xgj/spaceflow/commit/e17894a5af53ff040a0a17bc602d232f78415e1b))
|
||||
* **ci-shell:** released version 0.18.0 [no ci] ([f64fd80](https://git.bjxgj.com/xgj/spaceflow/commit/f64fd8009a6dd725f572c7e9fbf084d9320d5128))
|
||||
* **core:** released version 0.18.0 [no ci] ([c5e973f](https://git.bjxgj.com/xgj/spaceflow/commit/c5e973fbe22c0fcd0d6d3af6e4020e2fbff9d31f))
|
||||
* **period-summary:** released version 0.18.0 [no ci] ([f0df638](https://git.bjxgj.com/xgj/spaceflow/commit/f0df63804d06f8c75e04169ec98226d7a4f5d7f9))
|
||||
* **publish:** released version 0.20.0 [no ci] ([d347e3b](https://git.bjxgj.com/xgj/spaceflow/commit/d347e3b2041157d8dc6e3ade69b05a481b2ab371))
|
||||
|
||||
## [0.27.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.26.0...@spaceflow/review@0.27.0) (2026-02-04)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 从 PR diff 填充缺失的 patch 字段 ([24bfaa7](https://git.bjxgj.com/xgj/spaceflow/commit/24bfaa76f3bd56c8ead307e73e0623a2221c69cf))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **config:** 降低并发数以优化 AI 审查性能 ([052dd72](https://git.bjxgj.com/xgj/spaceflow/commit/052dd728f759da0a31e86a0ad480e9bb35052781))
|
||||
- **review:** 优化 Markdown 格式化器的代码风格和 JSON 数据输出逻辑 ([ca1b0c9](https://git.bjxgj.com/xgj/spaceflow/commit/ca1b0c96d9d0663a8b8dc93b4a9f63d4e5590df0))
|
||||
- **review:** 新增测试方法用于验证 PR 审查功能 ([5c57833](https://git.bjxgj.com/xgj/spaceflow/commit/5c578332cedffb7fa7e5ad753a788bcd55595c68))
|
||||
- **review:** 移除测试方法 testMethod ([21e9938](https://git.bjxgj.com/xgj/spaceflow/commit/21e9938100c5dd7d4eada022441c565b5c41a55a))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 新增新增文件无 patch 时的测试用例,优化变更行标记逻辑 ([a593f0d](https://git.bjxgj.com/xgj/spaceflow/commit/a593f0d4a641b348f7c9d30b14f639b24c12dcfa))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.17.0 [no ci] ([31abd3d](https://git.bjxgj.com/xgj/spaceflow/commit/31abd3dcb48e2ddea5175552c0a87c1eaa1e7a41))
|
||||
- **ci-shell:** released version 0.17.0 [no ci] ([a53508b](https://git.bjxgj.com/xgj/spaceflow/commit/a53508b15e4020e3399bae9cc04e730f1539ad8e))
|
||||
- **core:** released version 0.17.0 [no ci] ([c85a8ed](https://git.bjxgj.com/xgj/spaceflow/commit/c85a8ed88929d867d2d460a44d08d8b7bc4866a2))
|
||||
- **period-summary:** released version 0.17.0 [no ci] ([ac4e5b6](https://git.bjxgj.com/xgj/spaceflow/commit/ac4e5b6083773146ac840548a69006f6c4fbac1d))
|
||||
- **publish:** released version 0.19.0 [no ci] ([7a96bca](https://git.bjxgj.com/xgj/spaceflow/commit/7a96bca945434a99f7d051a38cb31adfd2ade5d2))
|
||||
|
||||
## [0.26.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.25.0...@spaceflow/review@0.26.0) (2026-02-04)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 新增 override 作用域测试,验证 includes 对 override 过滤的影响 ([820e0cb](https://git.bjxgj.com/xgj/spaceflow/commit/820e0cb0f36783dc1c7e1683ad08501e91f094b2))
|
||||
|
||||
## [0.25.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.24.0...@spaceflow/review@0.25.0) (2026-02-04)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 优化 override 和变更行过滤的日志输出,增强调试信息的可读性 ([9a7c6f5](https://git.bjxgj.com/xgj/spaceflow/commit/9a7c6f5b4ef2b8ae733fa499a0e5ec82feebc1d2))
|
||||
|
||||
## [0.24.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.23.0...@spaceflow/review@0.24.0) (2026-02-04)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **review:** 新增 getFileContents、getChangedFilesBetweenRefs 和 filterIssuesByValidCommits 方法的单元测试 ([7618c91](https://git.bjxgj.com/xgj/spaceflow/commit/7618c91bc075d218b9f51b862e5161d15a306bf8))
|
||||
|
||||
## [0.23.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.22.0...@spaceflow/review@0.23.0) (2026-02-03)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 使用 Base64 编码存储审查数据,避免 JSON 格式在 Markdown 中被转义 ([fb91e30](https://git.bjxgj.com/xgj/spaceflow/commit/fb91e30d0979cfe63ed8e7657c578db618b5e783))
|
||||
|
||||
## [0.22.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.21.0...@spaceflow/review@0.22.0) (2026-02-02)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 基于 fileContents 实际 commit hash 验证问题归属,替代依赖 LLM 填写的 commit 字段 ([de3e377](https://git.bjxgj.com/xgj/spaceflow/commit/de3e3771eb85ff93200c63fa9feb38941914a07d))
|
||||
- **review:** 移除 filterNoCommit 配置项,统一使用基于 commit hash 的问题过滤逻辑 ([82429b1](https://git.bjxgj.com/xgj/spaceflow/commit/82429b1072affb4f2b14d52f99887e12184d8218))
|
||||
- **review:** 统一使用 parseLineRange 方法解析行号,避免重复的正则匹配逻辑 ([c64f96a](https://git.bjxgj.com/xgj/spaceflow/commit/c64f96aa2e1a8e22dcd3e31e1a2acc1bb338a1a8))
|
||||
- **review:** 调整 filterIssuesByValidCommits 逻辑,保留无 commit 的 issue 交由 filterNoCommit 配置处理 ([e9c5d47](https://git.bjxgj.com/xgj/spaceflow/commit/e9c5d47aebef42507fd9fcd67e5eab624437e81a))
|
||||
- **review:** 过滤非 PR commits 的问题,避免 merge commit 引入的代码被审查 ([9e20f54](https://git.bjxgj.com/xgj/spaceflow/commit/9e20f54d57e71725432dfb9e7c943946aa6677d4))
|
||||
|
||||
## [0.21.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.20.0...@spaceflow/review@0.21.0) (2026-02-02)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 过滤 merge commits,避免在代码审查中处理合并提交 ([d7c647c](https://git.bjxgj.com/xgj/spaceflow/commit/d7c647c33156a58b42bfb45a67417723b75328c6))
|
||||
|
||||
## [0.20.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.19.0...@spaceflow/review@0.20.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 新增 Git diff 行号映射工具并优化 Claude 配置 ([88ef340](https://git.bjxgj.com/xgj/spaceflow/commit/88ef3400127fac3ad52fc326ad79fdc7bd058e98))
|
||||
- **review:** 为 execute 方法添加文档注释 ([a21f582](https://git.bjxgj.com/xgj/spaceflow/commit/a21f58290c873fb07789e70c8c5ded2b5874a29d))
|
||||
- **review:** 为 getPrNumberFromEvent 方法添加文档注释 ([54d1586](https://git.bjxgj.com/xgj/spaceflow/commit/54d1586f4558b5bfde81b926c7b513a32e5caf89))
|
||||
- **review:** 优化行号更新统计,分别统计更新和标记无效的问题数量 ([892b8be](https://git.bjxgj.com/xgj/spaceflow/commit/892b8bed8913531a9440579f777b1965fec772e5))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **review:** 优化历史 issue commit 匹配逻辑,支持短 SHA 与完整 SHA 的前缀匹配 ([e30c6dd](https://git.bjxgj.com/xgj/spaceflow/commit/e30c6ddefb14ec6631ce341f1d45c59786e94a46))
|
||||
- **review:** 简化历史问题处理策略,将行号更新改为标记变更文件问题为无效 ([5df7f00](https://git.bjxgj.com/xgj/spaceflow/commit/5df7f0087c493e104fe0dc054fd0b6c19ebe3500))
|
||||
- **review:** 简化行号更新逻辑,使用最新 commit diff 替代增量 diff ([6de7529](https://git.bjxgj.com/xgj/spaceflow/commit/6de7529c90ecbcee82149233fc01c393c5c4e7f7))
|
||||
- **review:** 重构行号更新逻辑,使用增量 diff 替代全量 diff ([d4f4304](https://git.bjxgj.com/xgj/spaceflow/commit/d4f4304e1e41614f7be8946d457eea1cf4e202fb))
|
||||
|
||||
### 测试用例
|
||||
|
||||
- **review:** 添加单元测试以覆盖行号更新逻辑 ([ebf33e4](https://git.bjxgj.com/xgj/spaceflow/commit/ebf33e45c18c910b88b106cdd4cfeb516b3fb656))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **actions:** 增强命令执行日志,输出原始 command 和 args 参数 ([0f0c238](https://git.bjxgj.com/xgj/spaceflow/commit/0f0c238de7d6f10875022f364746cefa56631b7f))
|
||||
- **ci-scripts:** released version 0.16.0 [no ci] ([9ab007d](https://git.bjxgj.com/xgj/spaceflow/commit/9ab007db178878e093ba93ea27c4f05ca813a65d))
|
||||
- **ci-shell:** released version 0.16.0 [no ci] ([87fd703](https://git.bjxgj.com/xgj/spaceflow/commit/87fd7030b54d2f614f23e092499c5c51bfc33788))
|
||||
- **core:** released version 0.16.0 [no ci] ([871f981](https://git.bjxgj.com/xgj/spaceflow/commit/871f981b0b908c981aaef366f2382ec6ca2e2269))
|
||||
- **period-summary:** released version 0.16.0 [no ci] ([b214e31](https://git.bjxgj.com/xgj/spaceflow/commit/b214e31221d5afa04481c48d9ddb878644a22ae7))
|
||||
- **publish:** released version 0.18.0 [no ci] ([2f2ce01](https://git.bjxgj.com/xgj/spaceflow/commit/2f2ce01726f7b3e4387e23a17974b58acd3e6929))
|
||||
- 在 PR 审查工作流中启用 --filter-no-commit 参数 ([e0024ad](https://git.bjxgj.com/xgj/spaceflow/commit/e0024ad5cb29250b452a841db2ce6ebf84016a2c))
|
||||
- 禁用删除代码分析功能 ([988e3f1](https://git.bjxgj.com/xgj/spaceflow/commit/988e3f156f2ca4e92413bf7a455eba1760ad9eba))
|
||||
|
||||
## [0.19.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.18.0...@spaceflow/review@0.19.0) (2026-02-02)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 在 Gitea SDK 中新增编辑 Pull Request 的方法 ([a586bf1](https://git.bjxgj.com/xgj/spaceflow/commit/a586bf110789578f23b39d64511229a1e5635dc4))
|
||||
- **core:** 在 Gitea SDK 中新增获取 reactions 的方法 ([9324cf2](https://git.bjxgj.com/xgj/spaceflow/commit/9324cf2550709b8302171e5522d0792c08bc1415))
|
||||
- **review:** 优化 commit author 获取逻辑,支持 committer 作为备选 ([b75b613](https://git.bjxgj.com/xgj/spaceflow/commit/b75b6133e5b8c95580516480315bc979fc6eb59b))
|
||||
- **review:** 优化 commit author 获取逻辑,支持从 Git 原始作者信息中提取 ([10ac821](https://git.bjxgj.com/xgj/spaceflow/commit/10ac8210a4457e0356c3bc1645f54f6f3d8c904c))
|
||||
- **review:** 优化 commit author 获取逻辑,通过 Gitea API 搜索用户以关联 Git 原始作者 ([daa274b](https://git.bjxgj.com/xgj/spaceflow/commit/daa274bba2255e92d1e9a6e049e20846a69e8df7))
|
||||
- **review:** 优化 PR 标题生成的格式要求 ([a4d807d](https://git.bjxgj.com/xgj/spaceflow/commit/a4d807d0a4feee4ccc88c6096e069c6dbb650a03))
|
||||
- **review:** 优化 verbose 参数支持多级别累加,将日志级别扩展为 0-3 级 ([fe4c830](https://git.bjxgj.com/xgj/spaceflow/commit/fe4c830cac137c5502d700d2cd5f22b52a629e5f))
|
||||
- **review:** 优化历史问题的 author 信息填充逻辑 ([b18d171](https://git.bjxgj.com/xgj/spaceflow/commit/b18d171c9352fe5815262d43ffd9cd7751f03a4e))
|
||||
- **review:** 优化审查报告中回复消息的格式显示 ([f478c8d](https://git.bjxgj.com/xgj/spaceflow/commit/f478c8da4c1d7494819672006e3230dbc8e0924d))
|
||||
- **review:** 优化审查报告中的消息展示格式 ([0996c2b](https://git.bjxgj.com/xgj/spaceflow/commit/0996c2b45c9502c84308f8a7f9186e4dbd4164fb))
|
||||
- **review:** 优化问题 author 信息填充时机,统一在所有问题合并后填充 ([ea8c586](https://git.bjxgj.com/xgj/spaceflow/commit/ea8c586fc60061ffd339e85c6c298b905bdfdcd8))
|
||||
- **review:** 优化问题展示和无效标记逻辑 ([e2b45e1](https://git.bjxgj.com/xgj/spaceflow/commit/e2b45e1ec594488bb79f528911fd6009a3213eca))
|
||||
- **review:** 在 fillIssueAuthors 方法中添加详细的调试日志 ([42ab288](https://git.bjxgj.com/xgj/spaceflow/commit/42ab288933296abdeeb3dbbedbb2aecedbea2251))
|
||||
- **review:** 在 syncReactionsToIssues 中添加详细日志并修复团队成员获取逻辑 ([91f166a](https://git.bjxgj.com/xgj/spaceflow/commit/91f166a07c2e43dabd4dd4ac186ec7b5f03dfc71))
|
||||
- **review:** 在审查报告的回复中为用户名添加 @ 前缀 ([bc6186b](https://git.bjxgj.com/xgj/spaceflow/commit/bc6186b97f0764f6335690eca1f8af665f9b7629))
|
||||
- **review:** 在审查问题中添加作者信息填充功能 ([8332dba](https://git.bjxgj.com/xgj/spaceflow/commit/8332dba4bb826cd358dc96db5f9b9406fb23df9b))
|
||||
- **review:** 将审查命令的详细日志参数从 --verbose 简化为 -vv ([5eb320b](https://git.bjxgj.com/xgj/spaceflow/commit/5eb320b92d1f7165052730b2e90eee52367391dd))
|
||||
- **review:** 扩展评审人收集逻辑,支持从 PR 指定的评审人和团队中获取 ([bbd61af](https://git.bjxgj.com/xgj/spaceflow/commit/bbd61af9d3e2b9e1dcf28c5e3867645fdda52e6f))
|
||||
- **review:** 支持 AI 自动生成和更新 PR 标题 ([e02fb02](https://git.bjxgj.com/xgj/spaceflow/commit/e02fb027d525dd3e794d649e6dbc53c99a3a9a59))
|
||||
- **review:** 支持 PR 关闭事件触发审查并自动传递事件类型参数 ([03967d9](https://git.bjxgj.com/xgj/spaceflow/commit/03967d9e860af7da06e3c04539f16c7bb31557ff))
|
||||
- **review:** 支持在审查报告中展示评论的 reactions 和回复记录 ([f4da31a](https://git.bjxgj.com/xgj/spaceflow/commit/f4da31adf6ce412cb0ce27bfe7a1e87e5350e915))
|
||||
- **review:** 移除 handleReview 中的重复 author 填充逻辑 ([e458bfd](https://git.bjxgj.com/xgj/spaceflow/commit/e458bfd0d21724c37fdd4023265d6a2dd1700404))
|
||||
- **review:** 限制 PR 标题自动更新仅在第一轮审查时执行 ([1891cbc](https://git.bjxgj.com/xgj/spaceflow/commit/1891cbc8d85f6eaef9e7107a7f1003bdc654d3a3))
|
||||
- **review:** 默认启用 PR 标题自动更新功能 ([fda6656](https://git.bjxgj.com/xgj/spaceflow/commit/fda6656efaf6479bb398ddc5cb1955142f31f369))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **actions:** 修复日志输出中的 emoji 显示问题,将 <20> 替换为 ℹ️ ([d3cd94a](https://git.bjxgj.com/xgj/spaceflow/commit/d3cd94afa9c6893b923d316fdcb5904f42ded632))
|
||||
- **review:** 修复审查完成日志中的乱码 emoji ([36c1c48](https://git.bjxgj.com/xgj/spaceflow/commit/36c1c48faecda3cc02b9e0b097aebba0a85ea5f8))
|
||||
- **review:** 将 UserInfo 的 id 字段类型从 number 改为 string ([505e019](https://git.bjxgj.com/xgj/spaceflow/commit/505e019c85d559ce1def1350599c1de218f7516a))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.15.0 [no ci] ([e314fb1](https://git.bjxgj.com/xgj/spaceflow/commit/e314fb11e7425b27c337d3650857cf3b737051fd))
|
||||
- **ci-shell:** released version 0.15.0 [no ci] ([5c0dc0b](https://git.bjxgj.com/xgj/spaceflow/commit/5c0dc0b5482366ccfd7854868d1eb5f306c24810))
|
||||
- **core:** released version 0.15.0 [no ci] ([48f3875](https://git.bjxgj.com/xgj/spaceflow/commit/48f38754dee382548bab968c57dd0f40f2343981))
|
||||
- **period-summary:** released version 0.15.0 [no ci] ([3dd72cb](https://git.bjxgj.com/xgj/spaceflow/commit/3dd72cb65a422b5b008a83820e799b810a6d53eb))
|
||||
- **publish:** released version 0.17.0 [no ci] ([8e0d065](https://git.bjxgj.com/xgj/spaceflow/commit/8e0d0654040d6af7e99fa013a8255aa93acbcc3a))
|
||||
|
||||
## [0.18.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.17.0...@spaceflow/review@0.18.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 统一所有命令的错误处理,添加堆栈信息输出 ([31224a1](https://git.bjxgj.com/xgj/spaceflow/commit/31224a16ce7155402504bd8d3e386e59e47949df))
|
||||
- **review:** 增强错误处理,添加堆栈信息输出 ([e0fb5de](https://git.bjxgj.com/xgj/spaceflow/commit/e0fb5de6bc877d8f0b3dc3c03f8d614320427bf3))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.14.0 [no ci] ([c536208](https://git.bjxgj.com/xgj/spaceflow/commit/c536208e352baa82e5b56c490ea9df0aff116cb2))
|
||||
- **ci-shell:** released version 0.14.0 [no ci] ([c6e4bdc](https://git.bjxgj.com/xgj/spaceflow/commit/c6e4bdca44874739694e3e46998e376779503e53))
|
||||
- **core:** released version 0.14.0 [no ci] ([996dbc6](https://git.bjxgj.com/xgj/spaceflow/commit/996dbc6f80b0d3fb8049df9a9a31bd1e5b5d4b92))
|
||||
- **period-summary:** released version 0.14.0 [no ci] ([55a72f2](https://git.bjxgj.com/xgj/spaceflow/commit/55a72f2b481e5ded1d9207a5a8d6a6864328d5a0))
|
||||
- **publish:** released version 0.16.0 [no ci] ([e31e46d](https://git.bjxgj.com/xgj/spaceflow/commit/e31e46d08fccb10a42b6579fa042aa6c57d79c8a))
|
||||
|
||||
## [0.17.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.16.0...@spaceflow/review@0.17.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **core:** 修复 resolveRef 方法未处理空 ref 参数的问题 ([0824c83](https://git.bjxgj.com/xgj/spaceflow/commit/0824c8392482263036888b2fec95935371d67d4d))
|
||||
- **review:** 修复参数空值检查,增强代码健壮性 ([792a192](https://git.bjxgj.com/xgj/spaceflow/commit/792a192fd5dd80ed1e6d85cd61f6ce997bcc9dd9))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.13.0 [no ci] ([021eefd](https://git.bjxgj.com/xgj/spaceflow/commit/021eefdf2ff72d16b36123335548df2d3ad1d6b7))
|
||||
- **ci-shell:** released version 0.13.0 [no ci] ([81e7582](https://git.bjxgj.com/xgj/spaceflow/commit/81e75820eb69ca188155e33945111e2b1f6b3012))
|
||||
- **core:** released version 0.13.0 [no ci] ([e3edde3](https://git.bjxgj.com/xgj/spaceflow/commit/e3edde3e670c79544af9a7249d566961740a2284))
|
||||
- **period-summary:** released version 0.13.0 [no ci] ([1d47460](https://git.bjxgj.com/xgj/spaceflow/commit/1d47460e40ba422a32865ccddd353e089eb91c6a))
|
||||
- **publish:** released version 0.15.0 [no ci] ([4b09122](https://git.bjxgj.com/xgj/spaceflow/commit/4b091227265a57f0a05488749eb4852fb421a06e))
|
||||
|
||||
## [0.16.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.15.0...@spaceflow/review@0.16.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 为删除影响分析添加文件过滤功能 ([7304293](https://git.bjxgj.com/xgj/spaceflow/commit/73042937c5271ff4b0dcb6cd6d823e5aa0c03e7b))
|
||||
|
||||
## [0.15.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.14.0...@spaceflow/review@0.15.0) (2026-01-31)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **review:** 修复按指定提交过滤时未处理空值导致的潜在问题 ([5d4d3e0](https://git.bjxgj.com/xgj/spaceflow/commit/5d4d3e0390a50c01309bb09e01c7328b211271b8))
|
||||
|
||||
## [0.14.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.13.0...@spaceflow/review@0.14.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 新增过滤无commit问题的选项 ([7a4c458](https://git.bjxgj.com/xgj/spaceflow/commit/7a4c458da03ae4a4646abca7e5f03abc849dc405))
|
||||
|
||||
## [0.13.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.12.0...@spaceflow/review@0.13.0) (2026-01-31)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 CLI 入口文件添加 Node shebang 支持 ([0d787d3](https://git.bjxgj.com/xgj/spaceflow/commit/0d787d329e69f2b53d26ba04720d60625ca51efd))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.12.0 [no ci] ([097863f](https://git.bjxgj.com/xgj/spaceflow/commit/097863f0c5cc46cb5cb930f14a6f379f60a13f08))
|
||||
- **ci-shell:** released version 0.12.0 [no ci] ([274216f](https://git.bjxgj.com/xgj/spaceflow/commit/274216fc930dfbf8390d02e25c06efcb44980fed))
|
||||
- **core:** released version 0.12.0 [no ci] ([1ce5034](https://git.bjxgj.com/xgj/spaceflow/commit/1ce50346d73a1914836333415f5ead9fbfa27be7))
|
||||
- **period-summary:** released version 0.12.0 [no ci] ([38490aa](https://git.bjxgj.com/xgj/spaceflow/commit/38490aa75ab20789c5495a5d8d009867f954af4f))
|
||||
- **publish:** released version 0.14.0 [no ci] ([fe0e140](https://git.bjxgj.com/xgj/spaceflow/commit/fe0e14058a364362d7d218da9b34dbb5d8fb8f42))
|
||||
|
||||
## [0.12.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.11.0...@spaceflow/review@0.12.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit message 的 scope 处理逻辑 ([42869dd](https://git.bjxgj.com/xgj/spaceflow/commit/42869dd4bde0a3c9bf8ffb827182775e2877a57b))
|
||||
- **core:** 重构 commit 服务并添加结构化 commit message 支持 ([22b4db8](https://git.bjxgj.com/xgj/spaceflow/commit/22b4db8619b0ce038667ab42dea1362706887fc9))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.11.0 [no ci] ([d4f5bba](https://git.bjxgj.com/xgj/spaceflow/commit/d4f5bba6f89e9e051dde8d313b6e102c6dadfa41))
|
||||
- **ci-shell:** released version 0.11.0 [no ci] ([cf9e486](https://git.bjxgj.com/xgj/spaceflow/commit/cf9e48666197295f118396693abc08b680b3ddee))
|
||||
- **core:** released version 0.11.0 [no ci] ([f0025c7](https://git.bjxgj.com/xgj/spaceflow/commit/f0025c792e332e8b8752597a27f654c0197c36eb))
|
||||
- **period-summary:** released version 0.11.0 [no ci] ([b518887](https://git.bjxgj.com/xgj/spaceflow/commit/b518887bddd5a452c91148bac64d61ec64b0b509))
|
||||
- **publish:** released version 0.13.0 [no ci] ([1d308d9](https://git.bjxgj.com/xgj/spaceflow/commit/1d308d9e32c50902dd881144ff541204d368006f))
|
||||
|
||||
## [0.11.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.10.0...@spaceflow/review@0.11.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 npm 包名处理逻辑 ([ae23ebd](https://git.bjxgj.com/xgj/spaceflow/commit/ae23ebdc3144b611e1aa8c4e66bf0db074d09798))
|
||||
- **core:** 添加依赖更新功能 ([1a544eb](https://git.bjxgj.com/xgj/spaceflow/commit/1a544eb5e2b64396a0187d4518595e9dcb51d73e))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.10.0 [no ci] ([ca2daad](https://git.bjxgj.com/xgj/spaceflow/commit/ca2daada8b04bbe809e69a3d5bd9373e897c6f40))
|
||||
- **ci-shell:** released version 0.10.0 [no ci] ([53864b8](https://git.bjxgj.com/xgj/spaceflow/commit/53864b8c2534cae265b8fbb98173a5b909682d4e))
|
||||
- **core:** released version 0.10.0 [no ci] ([a80d34f](https://git.bjxgj.com/xgj/spaceflow/commit/a80d34fb647e107343a07a8793363b3b76320e81))
|
||||
- **period-summary:** released version 0.10.0 [no ci] ([c1ca3bb](https://git.bjxgj.com/xgj/spaceflow/commit/c1ca3bb67fa7f9dbb4de152f0461d644f3044946))
|
||||
- **publish:** released version 0.12.0 [no ci] ([50e209e](https://git.bjxgj.com/xgj/spaceflow/commit/50e209ebc57504462ed192a0fe22f6f944165fa3))
|
||||
|
||||
## [0.10.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.9.0...@spaceflow/review@0.10.0) (2026-01-29)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **review:** 支持绝对路径转换为相对路径 ([9050f64](https://git.bjxgj.com/xgj/spaceflow/commit/9050f64b8ef67cb2c8df9663711a209523ae9d18))
|
||||
|
||||
## [0.9.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.8.0...@spaceflow/review@0.9.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 为 npm 包添加 npx 直接执行支持 ([e67a7da](https://git.bjxgj.com/xgj/spaceflow/commit/e67a7da34c4e41408760da4de3a499495ce0df2f))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.9.0 [no ci] ([1b9e816](https://git.bjxgj.com/xgj/spaceflow/commit/1b9e8167bb8fc67fcc439b2ef82e7a63dc323e6d))
|
||||
- **ci-shell:** released version 0.9.0 [no ci] ([accdda7](https://git.bjxgj.com/xgj/spaceflow/commit/accdda7ee4628dc8447e9a89da6c8101c572cb90))
|
||||
- **core:** released version 0.9.0 [no ci] ([8127211](https://git.bjxgj.com/xgj/spaceflow/commit/812721136828e8c38adf0855fb292b0da89daf1a))
|
||||
- **period-summary:** released version 0.9.0 [no ci] ([ac03f9b](https://git.bjxgj.com/xgj/spaceflow/commit/ac03f9bcff742d669c6e8b2f94e40153b6c298f5))
|
||||
- **publish:** released version 0.11.0 [no ci] ([df17cd1](https://git.bjxgj.com/xgj/spaceflow/commit/df17cd1250c8fd8a035eb073d292885a4b1e3322))
|
||||
|
||||
## [0.8.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.7.0...@spaceflow/review@0.8.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化 commit 消息生成器中的 scope 处理逻辑 ([1592079](https://git.bjxgj.com/xgj/spaceflow/commit/1592079edde659fe94a02bb6e2dea555c80d3b6b))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.8.0 [no ci] ([be6273d](https://git.bjxgj.com/xgj/spaceflow/commit/be6273dab7f1c80c58abdb8de6f0eeb986997e28))
|
||||
- **ci-shell:** released version 0.8.0 [no ci] ([3102178](https://git.bjxgj.com/xgj/spaceflow/commit/310217827c6ec29294dee5689b2dbb1b66492728))
|
||||
- **core:** released version 0.8.0 [no ci] ([625dbc0](https://git.bjxgj.com/xgj/spaceflow/commit/625dbc0206747b21a893ae43032f55d0a068c6fd))
|
||||
- **period-summary:** released version 0.8.0 [no ci] ([44ff3c5](https://git.bjxgj.com/xgj/spaceflow/commit/44ff3c505b243e1291565e354e239ce893e5e47c))
|
||||
- **publish:** released version 0.10.0 [no ci] ([8722ba9](https://git.bjxgj.com/xgj/spaceflow/commit/8722ba9eddb03c2f73539f4e09c504ed9491a5eb))
|
||||
|
||||
## [0.7.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.6.0...@spaceflow/review@0.7.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 重构安装服务目录结构和命名 ([50cc900](https://git.bjxgj.com/xgj/spaceflow/commit/50cc900eb864b23f20c5f48dec20d1a754238286))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.7.0 [no ci] ([ea294e1](https://git.bjxgj.com/xgj/spaceflow/commit/ea294e138c6b15033af85819629727915dfcbf4b))
|
||||
- **ci-shell:** released version 0.7.0 [no ci] ([247967b](https://git.bjxgj.com/xgj/spaceflow/commit/247967b30876aae78cfb1f9c706431b5bb9fb57e))
|
||||
- **core:** released version 0.7.0 [no ci] ([000c53e](https://git.bjxgj.com/xgj/spaceflow/commit/000c53eff80899dbadad8d668a2227921373daad))
|
||||
- **period-summary:** released version 0.7.0 [no ci] ([8869d58](https://git.bjxgj.com/xgj/spaceflow/commit/8869d5876e86ebe83ae65c3259cd9a7e402257cf))
|
||||
- **publish:** released version 0.9.0 [no ci] ([b404930](https://git.bjxgj.com/xgj/spaceflow/commit/b40493049877c1fd3554d77a14e9bd9ab318e15a))
|
||||
|
||||
## [0.6.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.5.0...@spaceflow/review@0.6.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化pnpm包安装逻辑,检测是否为workspace ([6555daf](https://git.bjxgj.com/xgj/spaceflow/commit/6555dafe1f08a244525be3a0345cc585f2552086))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.6.0 [no ci] ([d485758](https://git.bjxgj.com/xgj/spaceflow/commit/d48575827941cae6ffc7ae6ba911e5d4cf3bd7fa))
|
||||
- **ci-shell:** released version 0.6.0 [no ci] ([a2d1239](https://git.bjxgj.com/xgj/spaceflow/commit/a2d12397997b309062a9d93af57a5588cdb82a79))
|
||||
- **core:** released version 0.6.0 [no ci] ([21e1ec6](https://git.bjxgj.com/xgj/spaceflow/commit/21e1ec61a2de542e065034f32a259092dd7c0e0d))
|
||||
- **period-summary:** released version 0.6.0 [no ci] ([6648dfb](https://git.bjxgj.com/xgj/spaceflow/commit/6648dfb31b459e8c4522cff342dfa87a4bdaab4b))
|
||||
- **publish:** released version 0.8.0 [no ci] ([d7cd2e9](https://git.bjxgj.com/xgj/spaceflow/commit/d7cd2e9a7af178acdf91f16ae299c82e915db6e6))
|
||||
|
||||
## [0.5.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.4.0...@spaceflow/review@0.5.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 优化包管理器检测与 npm 包处理逻辑 ([63f7fa4](https://git.bjxgj.com/xgj/spaceflow/commit/63f7fa4f55cb41583009b2ea313b5ad327615e52))
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **core:** 优化配置合并逻辑,添加字段覆盖策略 ([18680e6](https://git.bjxgj.com/xgj/spaceflow/commit/18680e69b0d6e9e05c843ed3f07766830955d658))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.5.0 [no ci] ([a87a1da](https://git.bjxgj.com/xgj/spaceflow/commit/a87a1da0490986c46c2a527cda5e7d0df9df6d03))
|
||||
- **ci-shell:** released version 0.5.0 [no ci] ([920d9a8](https://git.bjxgj.com/xgj/spaceflow/commit/920d9a8165fe6eabf7a074eb65762f4693883438))
|
||||
- **core:** released version 0.5.0 [no ci] ([ad20098](https://git.bjxgj.com/xgj/spaceflow/commit/ad20098ef954283dd6d9867a4d2535769cbb8377))
|
||||
- **period-summary:** released version 0.5.0 [no ci] ([8e547e9](https://git.bjxgj.com/xgj/spaceflow/commit/8e547e9e6a6496a8c314c06cf6e88c97e623bc2d))
|
||||
- **publish:** released version 0.7.0 [no ci] ([7124435](https://git.bjxgj.com/xgj/spaceflow/commit/712443516845f5bbc097af16ec6e90bb57b69fa3))
|
||||
- 更新项目依赖锁定文件 ([19d2d1d](https://git.bjxgj.com/xgj/spaceflow/commit/19d2d1d86bb35b8ee5d3ad20be51b7aa68e83eff))
|
||||
- 移除 npm registry 配置文件 ([2d9fac6](https://git.bjxgj.com/xgj/spaceflow/commit/2d9fac6db79e81a09ca8e031190d0ebb2781cc31))
|
||||
- 调整依赖配置并添加npm registry配置 ([a754db1](https://git.bjxgj.com/xgj/spaceflow/commit/a754db1bad1bafcea50b8d2825aaf19457778f2e))
|
||||
|
||||
## [0.4.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.3.0...@spaceflow/review@0.4.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整zod依赖的导入来源 ([574eef1](https://git.bjxgj.com/xgj/spaceflow/commit/574eef1910809a72a4b13acd4cb070e12dc42ce8))
|
||||
- **review:** 调整zod依赖的导入路径 ([02014cd](https://git.bjxgj.com/xgj/spaceflow/commit/02014cdab9829df583f0f621150573b8c45a3ad7))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.4.0 [no ci] ([364f696](https://git.bjxgj.com/xgj/spaceflow/commit/364f696d0df5d84be915cfaa9202a592073d9b46))
|
||||
- **ci-shell:** released version 0.4.0 [no ci] ([7e6bf1d](https://git.bjxgj.com/xgj/spaceflow/commit/7e6bf1dabffc6250b918b89bb850d478d3f4b875))
|
||||
- **core:** released version 0.4.0 [no ci] ([bc4cd89](https://git.bjxgj.com/xgj/spaceflow/commit/bc4cd89af70dce052e7e00fe413708790f65be61))
|
||||
- **core:** 调整核心依赖与配置,新增Zod类型系统支持 ([def0751](https://git.bjxgj.com/xgj/spaceflow/commit/def0751577d9f3350494ca3c7bb4a4b087dab05e))
|
||||
- **period-summary:** released version 0.4.0 [no ci] ([ca89a9b](https://git.bjxgj.com/xgj/spaceflow/commit/ca89a9b9436761e210dedfc38fb3c16ef39b0718))
|
||||
- **publish:** released version 0.6.0 [no ci] ([b6d8d09](https://git.bjxgj.com/xgj/spaceflow/commit/b6d8d099fc439ce67f802d56e30dadaa28afed0e))
|
||||
- 调整项目依赖配置 ([6802386](https://git.bjxgj.com/xgj/spaceflow/commit/6802386f38f4081a3b5d5c47ddc49e9ec2e4f28d))
|
||||
|
||||
## [0.3.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.2.0...@spaceflow/review@0.3.0) (2026-01-28)
|
||||
|
||||
### 代码重构
|
||||
|
||||
- **publish:** 调整包变更检测的日志输出格式 ([df35e92](https://git.bjxgj.com/xgj/spaceflow/commit/df35e92d614ce59e202643cf34a0fef2803412a1))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.3.0 [no ci] ([9292b52](https://git.bjxgj.com/xgj/spaceflow/commit/9292b524f2b8171f8774fab4e4ef4b32991f5d3d))
|
||||
- **ci-shell:** released version 0.3.0 [no ci] ([7b25e55](https://git.bjxgj.com/xgj/spaceflow/commit/7b25e557b628fdfa66d7a0be4ee21267906ac15f))
|
||||
- **core:** released version 0.3.0 [no ci] ([bf8b005](https://git.bjxgj.com/xgj/spaceflow/commit/bf8b005ccbfcdd2061c18ae4ecdd476584ecbb53))
|
||||
- **core:** 调整依赖配置 ([c86534a](https://git.bjxgj.com/xgj/spaceflow/commit/c86534ad213293ee2557ba5568549e8fbcb74ba5))
|
||||
- **period-summary:** released version 0.3.0 [no ci] ([7e74c59](https://git.bjxgj.com/xgj/spaceflow/commit/7e74c59d90d88e061e693829f8196834d9858307))
|
||||
- **publish:** released version 0.4.0 [no ci] ([be66220](https://git.bjxgj.com/xgj/spaceflow/commit/be662202c1e9e509368eb683a0d6df3afd876ff8))
|
||||
- **publish:** released version 0.5.0 [no ci] ([8eecd19](https://git.bjxgj.com/xgj/spaceflow/commit/8eecd19c4dd3fbaa27187a9b24234e753fff5efe))
|
||||
- 调整项目依赖配置 ([f4009cb](https://git.bjxgj.com/xgj/spaceflow/commit/f4009cb0c369b225c356584afb28a7ff5a1a89ec))
|
||||
|
||||
## [0.2.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.1.2...@spaceflow/review@0.2.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **publish:** 增强包变更检测的日志输出 ([b89c5cc](https://git.bjxgj.com/xgj/spaceflow/commit/b89c5cc0654713b6482ee591325d4f92ad773600))
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复分支锁定时未捕获异常处理器的资源泄漏问题 ([ae326e9](https://git.bjxgj.com/xgj/spaceflow/commit/ae326e95c0cea033893cf084cbf7413fb252bd33))
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **core:** 更新核心框架README文档 ([0d98658](https://git.bjxgj.com/xgj/spaceflow/commit/0d98658f6ab01f119f98d3387fb5651d4d4351a8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.2.0 [no ci] ([716e9ad](https://git.bjxgj.com/xgj/spaceflow/commit/716e9ad0f32bde09c608143da78f0a4299017797))
|
||||
- **ci-shell:** released version 0.2.0 [no ci] ([4f5314b](https://git.bjxgj.com/xgj/spaceflow/commit/4f5314b1002b90d7775a5ef51e618a3f227ae5a9))
|
||||
- **core:** released version 0.2.0 [no ci] ([5a96529](https://git.bjxgj.com/xgj/spaceflow/commit/5a96529cabdce4fb150732b34c55e668c33cb50c))
|
||||
- **period-summary:** released version 0.2.0 [no ci] ([66a4e20](https://git.bjxgj.com/xgj/spaceflow/commit/66a4e209519b64d946ec21b1d1695105fb9de106))
|
||||
- **publish:** released version 0.2.0 [no ci] ([bc30a82](https://git.bjxgj.com/xgj/spaceflow/commit/bc30a82082bba4cc1a66c74c11dc0ad9deef4692))
|
||||
- **publish:** released version 0.3.0 [no ci] ([972eca4](https://git.bjxgj.com/xgj/spaceflow/commit/972eca440dd333e8c5380124497c16fe6e3eea6c))
|
||||
- 优化CI工作流的代码检出配置 ([d9740dd](https://git.bjxgj.com/xgj/spaceflow/commit/d9740dd6d1294068ffdcd7a12b61149773866a2a))
|
||||
|
||||
## [0.1.2](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.1.1...@spaceflow/review@0.1.2) (2026-01-28)
|
||||
|
||||
### 修复BUG
|
||||
|
||||
- **publish:** 修复预演模式下的交互式提示问题 ([0b785bf](https://git.bjxgj.com/xgj/spaceflow/commit/0b785bfddb9f35e844989bd3891817dc502302f8))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.2 [no ci] ([ab9c100](https://git.bjxgj.com/xgj/spaceflow/commit/ab9c1000bcbe64d8a99ffa6bebb974c024b14325))
|
||||
- **ci-shell:** released version 0.1.2 [no ci] ([bf7977b](https://git.bjxgj.com/xgj/spaceflow/commit/bf7977bed684b557555861b9dc0359eda3c5d476))
|
||||
- **core:** released version 0.1.2 [no ci] ([8292dbe](https://git.bjxgj.com/xgj/spaceflow/commit/8292dbe59a200cc640a95b86afb6451ec0c044ce))
|
||||
- **period-summary:** released version 0.1.2 [no ci] ([eaf41a0](https://git.bjxgj.com/xgj/spaceflow/commit/eaf41a0149ee4306361ccab0b3878bded79677df))
|
||||
- **publish:** released version 0.1.2 [no ci] ([4786731](https://git.bjxgj.com/xgj/spaceflow/commit/4786731da7a21982dc1e912b1a5002f5ebba9104))
|
||||
|
||||
## [0.1.1](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.1.0...@spaceflow/review@0.1.1) (2026-01-28)
|
||||
|
||||
### 文档更新
|
||||
|
||||
- **publish:** 完善发布插件README文档 ([faa57b0](https://git.bjxgj.com/xgj/spaceflow/commit/faa57b095453c00fb3c9a7704bc31b63953c0ac5))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.1 [no ci] ([19ca0d8](https://git.bjxgj.com/xgj/spaceflow/commit/19ca0d8461f9537f4318b772cad3ea395d2b3264))
|
||||
- **ci-shell:** released version 0.1.1 [no ci] ([488a686](https://git.bjxgj.com/xgj/spaceflow/commit/488a6869240151e7d1cf37a3b177897c2b5d5c1e))
|
||||
- **core:** released version 0.1.1 [no ci] ([0cf3a4d](https://git.bjxgj.com/xgj/spaceflow/commit/0cf3a4d37d7d1460e232dd30bc7ab8dc015ed11f))
|
||||
- **period-summary:** released version 0.1.1 [no ci] ([b77e96b](https://git.bjxgj.com/xgj/spaceflow/commit/b77e96b1b768efa81d37143101057224fc3cef0f))
|
||||
- **publish:** released version 0.1.1 [no ci] ([43ba6cb](https://git.bjxgj.com/xgj/spaceflow/commit/43ba6cb565ab84155ddc335b8bf6a72424e99b69))
|
||||
|
||||
## [0.1.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/review@0.0.1...@spaceflow/review@0.1.0) (2026-01-28)
|
||||
|
||||
### 新特性
|
||||
|
||||
- **core:** 添加同步解锁分支方法用于进程退出清理 ([cbec480](https://git.bjxgj.com/xgj/spaceflow/commit/cbec480511e074de3ccdc61226f3baa317cff907))
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.1.0 [no ci] ([57b3a1c](https://git.bjxgj.com/xgj/spaceflow/commit/57b3a1c826dafd5ec51d68b7471266efd5cc32b2))
|
||||
- **ci-shell:** released version 0.1.0 [no ci] ([2283d9d](https://git.bjxgj.com/xgj/spaceflow/commit/2283d9d69ada1c071bef6c548dc756fe062893bd))
|
||||
- **core:** released version 0.1.0 [no ci] ([f455607](https://git.bjxgj.com/xgj/spaceflow/commit/f45560735082840410e08e0d8113f366732a1243))
|
||||
- **period-summary:** released version 0.1.0 [no ci] ([36fb7a4](https://git.bjxgj.com/xgj/spaceflow/commit/36fb7a486da82e1d8e4b0574c68b4473cd86b28e))
|
||||
- **publish:** released version 0.1.0 [no ci] ([0ca1b54](https://git.bjxgj.com/xgj/spaceflow/commit/0ca1b54fd52e1721b5453dc1922c1d5b6a00acf4))
|
||||
|
||||
## 0.0.1 (2026-01-28)
|
||||
|
||||
### 其他修改
|
||||
|
||||
- **ci-scripts:** released version 0.0.1 [no ci] ([b38fb9b](https://git.bjxgj.com/xgj/spaceflow/commit/b38fb9ba56200ced1baf563b097faa8717693783))
|
||||
- **ci-shell:** released version 0.0.1 [no ci] ([ec2a84b](https://git.bjxgj.com/xgj/spaceflow/commit/ec2a84b298c5fb989951caf42e2b016b3336f6a0))
|
||||
- **core:** released version 0.0.1 [no ci] ([66497d6](https://git.bjxgj.com/xgj/spaceflow/commit/66497d60be04b4756a3362dbec4652177910165c))
|
||||
- **period-summary:** released version 0.0.1 [no ci] ([7ab3504](https://git.bjxgj.com/xgj/spaceflow/commit/7ab3504750191b88643fe5db6b92bb08acc9ab5d))
|
||||
- **publish:** released version 0.0.1 [no ci] ([16b0f64](https://git.bjxgj.com/xgj/spaceflow/commit/16b0f647cf7fe23b921947b4a53ac94076bbee9e))
|
||||
- 重置所有包版本至 0.0.0 并清理 CHANGELOG 文件 ([f7efaf9](https://git.bjxgj.com/xgj/spaceflow/commit/f7efaf967467f1272e05d645720ee63941fe79be))
|
||||
40
commands/review/package.json
Normal file
40
commands/review/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@spaceflow/review",
|
||||
"version": "0.29.0",
|
||||
"description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"build": "spaceflow build",
|
||||
"dev": "spaceflow dev",
|
||||
"test": "vitest run",
|
||||
"test:cov": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/testing": "catalog:",
|
||||
"@spaceflow/cli": "workspace:*",
|
||||
"@swc/core": "catalog:",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"unplugin-swc": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "catalog:",
|
||||
"@nestjs/config": "catalog:",
|
||||
"@nestjs/swagger": "catalog:",
|
||||
"@spaceflow/core": "workspace:*",
|
||||
"class-transformer": "catalog:",
|
||||
"class-validator": "catalog:",
|
||||
"nest-commander": "catalog:"
|
||||
},
|
||||
"spaceflow": {
|
||||
"type": "flow",
|
||||
"entry": "."
|
||||
}
|
||||
}
|
||||
364
commands/review/src/README.md
Normal file
364
commands/review/src/README.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Review 模块
|
||||
|
||||
Review 是 spaceflow 的核心模块,提供基于 LLM 的自动化代码审查功能。支持在 GitHub Actions CI 环境中运行,也可在本地命令行使用。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
review/
|
||||
├── index.ts # 模块导出入口
|
||||
├── review.module.ts # NestJS 模块定义
|
||||
├── review.command.ts # CLI 命令定义
|
||||
├── review.service.ts # 核心审查服务
|
||||
├── review.service.spec.ts # 审查服务单元测试
|
||||
├── issue-verify.service.ts # 历史问题验证服务
|
||||
├── issue-verify.service.spec.ts # 验证服务单元测试
|
||||
├── deletion-impact.service.ts # 删除代码影响分析服务
|
||||
├── deletion-impact.service.spec.ts # 删除分析服务单元测试
|
||||
├── parse-title-options.ts # PR 标题参数解析
|
||||
├── parse-title-options.spec.ts # 标题解析单元测试
|
||||
```
|
||||
|
||||
## 模块依赖
|
||||
|
||||
```text
|
||||
ReviewModule
|
||||
├── ConfigModule # 配置管理
|
||||
├── GitProviderModule # Git Provider 适配器
|
||||
├── ClaudeSetupModule # Claude CLI 配置
|
||||
├── ReviewSpecModule # 审查规范加载
|
||||
├── ReviewReportModule # 审查报告格式化
|
||||
├── GitSdkModule # Git 命令封装
|
||||
└── LlmProxyModule # LLM 统一代理(支持 OpenAI/Claude/Gemini)
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. ReviewCommand
|
||||
|
||||
CLI 命令入口,支持以下参数:
|
||||
|
||||
| 参数 | 简写 | 说明 |
|
||||
| --------------------------------- | ---- | ------------------------------------- |
|
||||
| `--dry-run` | `-d` | 仅打印将要执行的操作,不实际提交评论 |
|
||||
| `--ci` | `-c` | 在 CI 环境中运行 |
|
||||
| `--pr-number <number>` | `-p` | PR 编号 |
|
||||
| `--base <ref>` | `-b` | 基准分支/tag |
|
||||
| `--head <ref>` | | 目标分支/tag |
|
||||
| `--verbose [level]` | `-v` | 详细输出 (1: 过程日志, 2: 含提示词) |
|
||||
| `--includes <patterns...>` | `-i` | 文件 glob 过滤模式 |
|
||||
| `--llm-mode <mode>` | `-l` | LLM 模式: claude-code, openai, gemini |
|
||||
| `--files <files...>` | `-f` | 仅审查指定文件 |
|
||||
| `--commits <commits...>` | | 仅审查指定 commits |
|
||||
| `--verify-fixes` | | 验证历史问题是否已修复 |
|
||||
| `--no-verify-fixes` | | 禁用历史问题验证 |
|
||||
| `--verify-concurrency <n>` | | 验证并发数(默认 10) |
|
||||
| `--analyze-deletions` | | 分析删除代码影响 |
|
||||
| `--deletion-only` | | 仅执行删除代码分析 |
|
||||
| `--deletion-analysis-mode <mode>` | | 删除分析模式: openai, claude-code |
|
||||
| `--output-format <format>` | `-o` | 输出格式: markdown, terminal, json |
|
||||
| `--generate-description` | | 使用 AI 生成 PR 功能描述 |
|
||||
|
||||
**环境变量**:
|
||||
|
||||
- `GITHUB_TOKEN`: GitHub API Token
|
||||
- `GITHUB_REPOSITORY`: 仓库名称 (owner/repo 格式)
|
||||
- `GITHUB_REF_NAME`: 当前分支名称
|
||||
- `GITHUB_EVENT_PATH`: 事件文件路径
|
||||
|
||||
### 2. ReviewService
|
||||
|
||||
核心审查服务,主要功能:
|
||||
|
||||
#### 2.1 审查流程
|
||||
|
||||
```text
|
||||
getContextFromEnv() # 从环境变量/配置构建上下文
|
||||
↓
|
||||
execute() # 执行审查主流程
|
||||
↓
|
||||
┌───────────────────────────────────────┐
|
||||
│ 1. 加载审查规范 (ReviewSpec) │
|
||||
│ 2. 获取 PR/分支变更信息 │
|
||||
│ 3. 过滤文件 (files/commits/includes) │
|
||||
│ 4. 获取文件内容并构建行号-commit 映射 │
|
||||
│ 5. 构建审查提示词 │
|
||||
│ 6. 并行调用 LLM 审查各文件 │
|
||||
│ 7. 过滤/格式化审查结果 │
|
||||
│ 8. 验证历史问题修复状态 │
|
||||
│ 9. 提交/更新 PR 评论 │
|
||||
│ 10. 可选:删除代码影响分析 │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 2.2 审查结果 Schema
|
||||
|
||||
```typescript
|
||||
interface ReviewResult {
|
||||
success: boolean;
|
||||
description: string; // PR 功能描述
|
||||
issues: ReviewIssue[]; // 发现的问题列表
|
||||
summary: FileSummary[]; // 各文件审查总结
|
||||
deletionImpact?: DeletionImpactResult; // 删除代码影响
|
||||
round: number; // 审查轮次
|
||||
}
|
||||
|
||||
interface ReviewIssue {
|
||||
file: string; // 文件路径
|
||||
line: string; // 行号 (如 "123" 或 "123-125")
|
||||
ruleId: string; // 规则 ID
|
||||
specFile: string; // 规则来源文件
|
||||
reason: string; // 问题描述
|
||||
suggestion?: string; // 修改建议
|
||||
severity?: "error" | "warn"; // 严重程度
|
||||
commit?: string; // 相关 commit SHA
|
||||
code?: string; // 问题代码片段
|
||||
date?: string; // 发现时间
|
||||
fixed?: string; // 修复时间
|
||||
valid?: "true" | "false"; // 问题有效性
|
||||
round?: number; // 发现轮次
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 关键方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
| ----------------------------- | ------------------------------ |
|
||||
| `getContextFromEnv()` | 从环境变量和配置构建审查上下文 |
|
||||
| `execute()` | 执行完整审查流程 |
|
||||
| `executeDeletionOnly()` | 仅执行删除代码分析 |
|
||||
| `buildReviewPrompt()` | 为每个文件构建审查提示词 |
|
||||
| `runLLMReview()` | 调用 LLM 执行审查 |
|
||||
| `callLLM()` | 并行审查多个文件 |
|
||||
| `reviewSingleFile()` | 审查单个文件 |
|
||||
| `getFileContents()` | 获取文件内容并构建行号映射 |
|
||||
| `buildLineCommitMap()` | 构建行号到 commit 的映射 |
|
||||
| `filterDuplicateIssues()` | 过滤重复问题 |
|
||||
| `postOrUpdateReviewComment()` | 发布/更新 PR 评论 |
|
||||
| `generatePrDescription()` | AI 生成 PR 功能描述 |
|
||||
|
||||
### 3. IssueVerifyService
|
||||
|
||||
历史问题验证服务,用于检查之前发现的问题是否已被修复。
|
||||
|
||||
#### 3.1 验证流程
|
||||
|
||||
```text
|
||||
verifyIssueFixes()
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 跳过已修复/无效的问题 │
|
||||
│ 2. 检查文件是否已删除 │
|
||||
│ 3. 并行调用 LLM 验证各问题 │
|
||||
│ 4. 更新问题的 fixed/valid 状态 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.2 验证结果
|
||||
|
||||
```typescript
|
||||
interface VerifyResult {
|
||||
fixed: boolean; // 问题是否已修复
|
||||
valid: boolean; // 问题是否有效(非误报)
|
||||
reason: string; // 判断依据
|
||||
}
|
||||
```
|
||||
|
||||
### 4. DeletionImpactService
|
||||
|
||||
删除代码影响分析服务,评估删除代码可能带来的风险。
|
||||
|
||||
#### 4.1 分析流程
|
||||
|
||||
```text
|
||||
analyzeDeletionImpact()
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 获取删除的代码块 │
|
||||
│ - Git Provider API 模式:从 PR diff 获取 │
|
||||
│ - Git Diff 模式:本地 git 命令 │
|
||||
│ 2. 查找代码引用关系 (git grep) │
|
||||
│ 3. 调用 LLM 分析影响 │
|
||||
│ - OpenAI 模式:标准 chat │
|
||||
│ - Claude Agent 模式:可用工具 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 4.2 风险等级
|
||||
|
||||
| 等级 | 说明 |
|
||||
| -------- | ---------------------------- |
|
||||
| `high` | 可能导致编译错误或运行时异常 |
|
||||
| `medium` | 可能影响部分功能行为 |
|
||||
| `low` | 影响较小 |
|
||||
| `none` | 无影响(清理无用代码) |
|
||||
|
||||
#### 4.3 分析结果
|
||||
|
||||
```typescript
|
||||
interface DeletionImpactResult {
|
||||
impacts: DeletionImpact[];
|
||||
summary: string;
|
||||
}
|
||||
|
||||
interface DeletionImpact {
|
||||
file: string; // 被删除代码所在文件
|
||||
deletedCode: string; // 删除代码摘要
|
||||
riskLevel: "high" | "medium" | "low" | "none";
|
||||
affectedFiles: string[]; // 可能受影响的文件
|
||||
reason: string; // 影响分析说明
|
||||
suggestion?: string; // 建议处理方式
|
||||
}
|
||||
```
|
||||
|
||||
### 5. parseTitleOptions
|
||||
|
||||
从 PR 标题解析命令参数,支持在标题末尾添加 `[/ai-review ...]` 格式的参数。
|
||||
|
||||
**示例**:
|
||||
|
||||
```text
|
||||
feat: 添加新功能 [/ai-review -l openai -v 2]
|
||||
```
|
||||
|
||||
**支持的参数**:
|
||||
|
||||
- `-l, --llm-mode <mode>`: LLM 模式
|
||||
- `-v, --verbose [level]`: 详细输出级别
|
||||
- `-d, --dry-run`: 仅打印不执行
|
||||
- `-i, --includes <pattern>`: 文件过滤模式
|
||||
- `--verify-fixes` / `--no-verify-fixes`: 历史问题验证
|
||||
- `--analyze-deletions`: 分析删除代码
|
||||
- `--deletion-only`: 仅执行删除分析
|
||||
- `--deletion-analysis-mode <mode>`: 删除分析模式
|
||||
|
||||
## 配置项
|
||||
|
||||
在 `spaceflow.json` 中配置:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
review: {
|
||||
// LLM 模式:claude-code, openai, gemini
|
||||
llmMode: "openai",
|
||||
|
||||
// 文件过滤模式
|
||||
includes: ["**/*.ts", "**/*.js"],
|
||||
|
||||
// 是否验证历史问题
|
||||
verifyFixes: true,
|
||||
verifyFixesConcurrency: 10,
|
||||
|
||||
// 删除代码分析
|
||||
analyzeDeletions: false,
|
||||
deletionAnalysisMode: "openai",
|
||||
|
||||
// 并发和超时配置
|
||||
concurrency: 5,
|
||||
timeout: 60000,
|
||||
retries: 0,
|
||||
retryDelay: 1000,
|
||||
|
||||
// 是否生成 PR 描述
|
||||
generateDescription: false,
|
||||
|
||||
// 是否启用行级评论
|
||||
lineComments: false,
|
||||
|
||||
// OpenAI 配置
|
||||
openai: {
|
||||
apiKey: "sk-xxx",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
model: "gpt-4o",
|
||||
},
|
||||
|
||||
// Claude Code 配置
|
||||
claudeCode: {
|
||||
// Claude CLI 配置
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### CI 环境
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ai-review.yml
|
||||
name: AI Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Review
|
||||
run: npx spaceflow review --ci -l openai
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
### 本地命令行
|
||||
|
||||
```bash
|
||||
# 审查 PR
|
||||
npx spaceflow review -p 123 -l openai
|
||||
|
||||
# 审查两个分支之间的差异
|
||||
npx spaceflow review -b main --head feature/xxx -l openai
|
||||
|
||||
# 仅审查指定文件
|
||||
npx spaceflow review -f src/app.ts -l openai
|
||||
|
||||
# 详细输出
|
||||
npx spaceflow review -p 123 -l openai -v 2
|
||||
|
||||
# 仅分析删除代码
|
||||
npx spaceflow review -p 123 --deletion-only -l openai
|
||||
```
|
||||
|
||||
## 审查规范
|
||||
|
||||
审查规范文件位于 `.spaceflow/review-spec/` 目录,使用 Markdown 格式定义规则。
|
||||
|
||||
详见 [ReviewSpec 模块文档](../../shared/review-spec/README.md)。
|
||||
|
||||
## 输出格式
|
||||
|
||||
### Markdown 格式(PR 评论)
|
||||
|
||||
包含以下部分:
|
||||
|
||||
- PR 功能描述
|
||||
- 问题列表(按严重程度分组)
|
||||
- 各文件审查总结
|
||||
- 删除代码影响分析(如启用)
|
||||
- 隐藏的 JSON 数据(用于增量审查)
|
||||
|
||||
### Terminal 格式
|
||||
|
||||
彩色终端输出,适合本地调试。
|
||||
|
||||
### JSON 格式
|
||||
|
||||
结构化 JSON 输出,适合程序处理。
|
||||
|
||||
## 增量审查
|
||||
|
||||
AI Review 支持增量审查,即在同一个 PR 上多次运行时:
|
||||
|
||||
1. **问题去重**:不会重复报告已存在的问题
|
||||
2. **修复验证**:自动验证历史问题是否已修复
|
||||
3. **轮次追踪**:记录每个问题的发现轮次
|
||||
4. **评论更新**:更新而非新增评论
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Token 限制**:单个文件审查的提示词长度受 LLM token 限制
|
||||
2. **并发控制**:可通过 `concurrency` 配置控制并行审查数量
|
||||
3. **超时处理**:可通过 `timeout` 配置单文件审查超时时间
|
||||
4. **重试机制**:可通过 `retries` 和 `retryDelay` 配置重试策略
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
query: jest.fn(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Mock for json-stringify-pretty-compact ESM module
|
||||
export default function stringify(obj: unknown, options: { indent?: number } = {}): string {
|
||||
return JSON.stringify(obj, null, options.indent || 2);
|
||||
}
|
||||
974
commands/review/src/deletion-impact.service.spec.ts
Normal file
974
commands/review/src/deletion-impact.service.spec.ts
Normal file
@@ -0,0 +1,974 @@
|
||||
import { vi, type Mocked, type Mock } from "vitest";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { LlmProxyService, GitProviderService } from "@spaceflow/core";
|
||||
import { DeletionImpactService } from "./deletion-impact.service";
|
||||
import * as child_process from "child_process";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
||||
query: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("child_process");
|
||||
|
||||
describe("DeletionImpactService", () => {
|
||||
let service: DeletionImpactService;
|
||||
let llmProxyService: Mocked<LlmProxyService>;
|
||||
let gitProvider: Mocked<GitProviderService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockLlmProxyService = {
|
||||
chatStream: vi.fn(),
|
||||
};
|
||||
|
||||
const mockGitProvider = {
|
||||
getPullRequestFiles: vi.fn(),
|
||||
getPullRequestDiff: vi.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DeletionImpactService,
|
||||
{ provide: LlmProxyService, useValue: mockLlmProxyService },
|
||||
{ provide: GitProviderService, useValue: mockGitProvider },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DeletionImpactService>(DeletionImpactService);
|
||||
llmProxyService = module.get(LlmProxyService) as Mocked<LlmProxyService>;
|
||||
gitProvider = module.get(GitProviderService) as Mocked<GitProviderService>;
|
||||
});
|
||||
|
||||
describe("analyzeDeletionImpact", () => {
|
||||
it("should return early if no parameters provided", async () => {
|
||||
const result = await service.analyzeDeletionImpact({}, "openai", 1);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
expect(result.summary).toBe("缺少必要参数");
|
||||
});
|
||||
|
||||
it("should extract blocks and analyze with LLM (Gitea API source)", async () => {
|
||||
const context = {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
prNumber: 123,
|
||||
};
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
filename: "test.ts",
|
||||
patch: "@@ -1,1 +1,0 @@\n-const oldCode = 1;",
|
||||
deletions: 1,
|
||||
},
|
||||
];
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue(mockFiles as any);
|
||||
|
||||
// Mock LLM response
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: {
|
||||
structuredOutput: {
|
||||
impacts: [
|
||||
{
|
||||
file: "test.ts",
|
||||
deletedCode: "const oldCode",
|
||||
riskLevel: "low",
|
||||
affectedFiles: [],
|
||||
reason: "Clean up",
|
||||
suggestion: "None",
|
||||
},
|
||||
],
|
||||
summary: "Safe deletion",
|
||||
},
|
||||
},
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
// Mock git grep (for findCodeReferences)
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
|
||||
process.nextTick(() => {
|
||||
mockProcess.stdout.emit("data", ""); // No references found
|
||||
mockProcess.emit("close", 0);
|
||||
});
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
|
||||
expect(result.impacts).toHaveLength(1);
|
||||
expect(result.summary).toBe("Safe deletion");
|
||||
expect(gitProvider.getPullRequestFiles).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDeletedBlocksFromPatch", () => {
|
||||
it("should correctly parse deleted lines from patch", () => {
|
||||
const patch = "@@ -10,2 +10,1 @@\n-line 1\n-line 2\n+line 1 modified";
|
||||
const blocks = (service as any).parseDeletedBlocksFromPatch("test.ts", patch);
|
||||
|
||||
expect(blocks).toHaveLength(1);
|
||||
expect(blocks[0]).toEqual({
|
||||
file: "test.ts",
|
||||
startLine: 10,
|
||||
endLine: 11,
|
||||
content: "line 1\nline 2",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractIdentifiers", () => {
|
||||
it("should extract function names", () => {
|
||||
const code = "function testFunc() {}\nasync function asyncFunc() {}";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).toContain("testFunc");
|
||||
expect(ids).toContain("asyncFunc");
|
||||
});
|
||||
|
||||
it("should extract class names", () => {
|
||||
const code = "class MyClass {}";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).toContain("MyClass");
|
||||
});
|
||||
|
||||
it("should extract interface names", () => {
|
||||
const code = "interface MyInterface {}";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).toContain("MyInterface");
|
||||
});
|
||||
|
||||
it("should extract type names", () => {
|
||||
const code = "type MyType = string;";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).toContain("MyType");
|
||||
});
|
||||
|
||||
it("should extract exported variable names", () => {
|
||||
const code = "export const MY_CONST = 1;\nexport let myVar = 2;";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).toContain("MY_CONST");
|
||||
expect(ids).toContain("myVar");
|
||||
});
|
||||
|
||||
it("should extract method names", () => {
|
||||
const code = "async getData() {\n return [];\n}";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).toContain("getData");
|
||||
});
|
||||
|
||||
it("should not extract control flow keywords as methods", () => {
|
||||
const code = "if (true) {\n}\nfor (const x of arr) {\n}\nwhile (true) {\n}";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
expect(ids).not.toContain("if");
|
||||
expect(ids).not.toContain("for");
|
||||
expect(ids).not.toContain("while");
|
||||
});
|
||||
|
||||
it("should deduplicate identifiers", () => {
|
||||
const code = "function test() {}\nfunction test() {}";
|
||||
const ids = (service as any).extractIdentifiers(code);
|
||||
const testCount = ids.filter((id: string) => id === "test").length;
|
||||
expect(testCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractDeletedBlocksFromChangedFiles", () => {
|
||||
it("should extract blocks from files with patch", () => {
|
||||
const changedFiles = [
|
||||
{
|
||||
filename: "test.ts",
|
||||
patch: "@@ -5,3 +5,1 @@\n-const a = 1;\n-const b = 2;\n+const c = 3;",
|
||||
},
|
||||
];
|
||||
const blocks = (service as any).extractDeletedBlocksFromChangedFiles(changedFiles);
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(1);
|
||||
expect(blocks[0].file).toBe("test.ts");
|
||||
});
|
||||
|
||||
it("should skip files without filename or patch", () => {
|
||||
const changedFiles = [{ filename: "", patch: "" }, { patch: "some patch" }];
|
||||
const blocks = (service as any).extractDeletedBlocksFromChangedFiles(changedFiles);
|
||||
expect(blocks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should filter out comment-only blocks", () => {
|
||||
const changedFiles = [
|
||||
{
|
||||
filename: "test.ts",
|
||||
patch: "@@ -1,2 +1,0 @@\n-// this is a comment\n-/* another comment */",
|
||||
},
|
||||
];
|
||||
const blocks = (service as any).extractDeletedBlocksFromChangedFiles(changedFiles);
|
||||
expect(blocks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractDeletedBlocksFromDiffText", () => {
|
||||
it("should parse diff text with multiple files", () => {
|
||||
const diffText = `diff --git a/file1.ts b/file1.ts
|
||||
--- a/file1.ts
|
||||
+++ b/file1.ts
|
||||
@@ -1,2 +1,1 @@
|
||||
-const old1 = 1;
|
||||
-const old2 = 2;
|
||||
+const new1 = 1;
|
||||
diff --git a/file2.ts b/file2.ts
|
||||
--- a/file2.ts
|
||||
+++ b/file2.ts
|
||||
@@ -5,1 +5,0 @@
|
||||
-function removed() {}`;
|
||||
const blocks = (service as any).extractDeletedBlocksFromDiffText(diffText);
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should handle empty diff text", () => {
|
||||
const blocks = (service as any).extractDeletedBlocksFromDiffText("");
|
||||
expect(blocks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should skip files without header match", () => {
|
||||
const diffText = "some random text without diff header";
|
||||
const blocks = (service as any).extractDeletedBlocksFromDiffText(diffText);
|
||||
expect(blocks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should save last delete block at end of file", () => {
|
||||
const diffText = `diff --git a/test.ts b/test.ts
|
||||
--- a/test.ts
|
||||
+++ b/test.ts
|
||||
@@ -1,2 +1,0 @@
|
||||
-const a = 1;
|
||||
-const b = 2;`;
|
||||
const blocks = (service as any).extractDeletedBlocksFromDiffText(diffText);
|
||||
expect(blocks).toHaveLength(1);
|
||||
expect(blocks[0].content).toContain("const a = 1;");
|
||||
expect(blocks[0].content).toContain("const b = 2;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterMeaningfulBlocks", () => {
|
||||
it("should keep blocks with meaningful code", () => {
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const result = (service as any).filterMeaningfulBlocks(blocks);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should filter out blocks with only comments", () => {
|
||||
const blocks = [
|
||||
{ file: "test.ts", startLine: 1, endLine: 2, content: "// comment\n* another" },
|
||||
];
|
||||
const result = (service as any).filterMeaningfulBlocks(blocks);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should filter out blocks with only blank lines", () => {
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 2, content: " \n " }];
|
||||
const result = (service as any).filterMeaningfulBlocks(blocks);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDeletedBlocksFromPatch - more branches", () => {
|
||||
it("should handle multiple hunks", () => {
|
||||
const patch = "@@ -1,1 +1,0 @@\n-line1\n@@ -10,1 +9,0 @@\n-line10";
|
||||
const blocks = (service as any).parseDeletedBlocksFromPatch("test.ts", patch);
|
||||
expect(blocks).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should save block when encountering new hunk after deletions", () => {
|
||||
const patch = "@@ -1,2 +1,0 @@\n-line1\n-line2\n@@ -10,1 +8,1 @@\n-old\n+new";
|
||||
const blocks = (service as any).parseDeletedBlocksFromPatch("test.ts", patch);
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should handle added lines breaking delete block", () => {
|
||||
const patch = "@@ -1,3 +1,2 @@\n-deleted1\n+added1\n-deleted2";
|
||||
const blocks = (service as any).parseDeletedBlocksFromPatch("test.ts", patch);
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should save last block at end of patch", () => {
|
||||
const patch = "@@ -1,1 +1,0 @@\n-const x = 1;";
|
||||
const blocks = (service as any).parseDeletedBlocksFromPatch("test.ts", patch);
|
||||
expect(blocks).toHaveLength(1);
|
||||
expect(blocks[0].startLine).toBe(1);
|
||||
expect(blocks[0].endLine).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzeDeletionImpact - more branches", () => {
|
||||
it("should use git-diff source with baseRef/headRef", async () => {
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
|
||||
const context = { baseRef: "main", headRef: "feature" };
|
||||
|
||||
// resolveRef 会调用 git rev-parse,然后 getDeletedCodeBlocks 调用 git diff
|
||||
// 第一次 spawn: rev-parse main
|
||||
// 第二次 spawn: rev-parse feature
|
||||
// 第三次 spawn: git diff
|
||||
let callCount = 0;
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
callCount++;
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
if (callCount <= 2) {
|
||||
// rev-parse 成功
|
||||
proc.stdout.emit("data", "abc123");
|
||||
proc.emit("close", 0);
|
||||
} else {
|
||||
// git diff 返回空
|
||||
proc.stdout.emit("data", "");
|
||||
proc.emit("close", 0);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
expect(result.summary).toBe("没有发现删除的代码");
|
||||
});
|
||||
|
||||
it("should use PR diff API when no patch in files", async () => {
|
||||
const context = { owner: "o", repo: "r", prNumber: 1 };
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([
|
||||
{ filename: "test.ts", deletions: 5 },
|
||||
] as any);
|
||||
gitProvider.getPullRequestDiff.mockResolvedValue(
|
||||
`diff --git a/test.ts b/test.ts
|
||||
--- a/test.ts
|
||||
+++ b/test.ts
|
||||
@@ -1,1 +1,0 @@
|
||||
-const removed = true;`,
|
||||
);
|
||||
|
||||
// Mock git grep for findCodeReferences
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.emit("close", 1); // grep 没找到
|
||||
});
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: {
|
||||
structuredOutput: { impacts: [], summary: "ok" },
|
||||
},
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(gitProvider.getPullRequestDiff).toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle PR diff API failure", async () => {
|
||||
const context = { owner: "o", repo: "r", prNumber: 1 };
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([
|
||||
{ filename: "test.ts", deletions: 5 },
|
||||
] as any);
|
||||
gitProvider.getPullRequestDiff.mockRejectedValue(new Error("API error"));
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
expect(result.summary).toBe("没有发现删除的代码");
|
||||
});
|
||||
|
||||
it("should return early when no deletions in files", async () => {
|
||||
const context = { owner: "o", repo: "r", prNumber: 1 };
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([
|
||||
{ filename: "test.ts", additions: 5, deletions: 0 },
|
||||
] as any);
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should filter blocks by includes", async () => {
|
||||
const context = {
|
||||
owner: "o",
|
||||
repo: "r",
|
||||
prNumber: 1,
|
||||
includes: ["*.service.ts"],
|
||||
};
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([
|
||||
{
|
||||
filename: "test.controller.ts",
|
||||
patch: "@@ -1,1 +1,0 @@\n-const x = 1;",
|
||||
deletions: 1,
|
||||
},
|
||||
] as any);
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should use verbose logging", async () => {
|
||||
const context = { owner: "o", repo: "r", prNumber: 1 };
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([] as any);
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzeWithLLM", () => {
|
||||
it("should handle error event from stream", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "error", message: "LLM failed" };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai");
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
expect(result.summary).toBe("分析返回格式无效");
|
||||
});
|
||||
|
||||
it("should handle invalid result from LLM", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "result", response: { structuredOutput: null } };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai");
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
expect(result.summary).toBe("分析返回格式无效");
|
||||
});
|
||||
|
||||
it("should handle array result from LLM", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "result", response: { structuredOutput: [] } };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai");
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should handle LLM call exception", async () => {
|
||||
llmProxyService.chatStream.mockImplementation(() => {
|
||||
throw new Error("Connection failed");
|
||||
});
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai");
|
||||
expect(result.summary).toBe("LLM 调用失败");
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle non-Error exception", async () => {
|
||||
llmProxyService.chatStream.mockImplementation(() => {
|
||||
throw "string error";
|
||||
});
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai");
|
||||
expect(result.summary).toBe("LLM 调用失败");
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should include references in prompt", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { impacts: [], summary: "ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 5, content: "const x = 1;" }];
|
||||
const refs = new Map([["test.ts:1-5", ["other.ts", "another.ts"]]]);
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai");
|
||||
expect(result.summary).toBe("ok");
|
||||
});
|
||||
|
||||
it("should log prompts with verbose=2", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { impacts: [], summary: "ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithLLM(blocks, refs, "openai", 2);
|
||||
expect(result.summary).toBe("ok");
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzeWithAgent", () => {
|
||||
it("should handle successful agent analysis", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { impacts: [], summary: "agent ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithAgent("claude-code", blocks, refs);
|
||||
expect(result.summary).toBe("agent ok");
|
||||
});
|
||||
|
||||
it("should handle agent error event", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "error", message: "Agent failed" };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithAgent("claude-code", blocks, refs);
|
||||
expect(result.impacts).toHaveLength(0);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle agent call exception", async () => {
|
||||
llmProxyService.chatStream.mockImplementation(() => {
|
||||
throw new Error("Agent connection failed");
|
||||
});
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithAgent("claude-code", blocks, refs);
|
||||
expect(result.summary).toBe("Agent 调用失败");
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle non-Error agent exception", async () => {
|
||||
llmProxyService.chatStream.mockImplementation(() => {
|
||||
throw "agent string error";
|
||||
});
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithAgent("claude-code", blocks, refs);
|
||||
expect(result.summary).toBe("Agent 调用失败");
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle invalid agent result", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "result", response: { structuredOutput: null } };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithAgent("claude-code", blocks, refs);
|
||||
expect(result.summary).toBe("分析返回格式无效");
|
||||
});
|
||||
|
||||
it("should log with verbose=2", async () => {
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { impacts: [], summary: "ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = new Map<string, string[]>();
|
||||
const result = await (service as any).analyzeWithAgent("claude-code", blocks, refs, 2);
|
||||
expect(result.summary).toBe("ok");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRef", () => {
|
||||
it("should return SHA directly for commit hash", async () => {
|
||||
const result = await (service as any).resolveRef("abc1234", 1);
|
||||
expect(result).toBe("abc1234");
|
||||
});
|
||||
|
||||
it("should return origin/ ref directly", async () => {
|
||||
const result = await (service as any).resolveRef("origin/main", 1);
|
||||
expect(result).toBe("origin/main");
|
||||
});
|
||||
|
||||
it("should throw for empty ref", async () => {
|
||||
await expect((service as any).resolveRef("")).rejects.toThrow("ref 参数不能为空");
|
||||
});
|
||||
|
||||
it("should try local branch first", async () => {
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.stdout.emit("data", "abc123");
|
||||
mockProcess.emit("close", 0);
|
||||
});
|
||||
|
||||
const result = await (service as any).resolveRef("main", 1);
|
||||
expect(result).toBe("main");
|
||||
});
|
||||
|
||||
it("should fallback to origin/ when local fails", async () => {
|
||||
let callCount = 0;
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
callCount++;
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
if (callCount === 1) {
|
||||
// rev-parse --verify main 失败
|
||||
proc.stderr.emit("data", "not found");
|
||||
proc.emit("close", 1);
|
||||
} else {
|
||||
// rev-parse --verify origin/main 成功
|
||||
proc.stdout.emit("data", "abc123");
|
||||
proc.emit("close", 0);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const result = await (service as any).resolveRef("main", 1);
|
||||
expect(result).toBe("origin/main");
|
||||
});
|
||||
|
||||
it("should try fetch when both local and origin fail", async () => {
|
||||
let callCount = 0;
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
callCount++;
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
if (callCount <= 2) {
|
||||
// rev-parse 失败
|
||||
proc.stderr.emit("data", "not found");
|
||||
proc.emit("close", 1);
|
||||
} else {
|
||||
// fetch 成功
|
||||
proc.stdout.emit("data", "");
|
||||
proc.emit("close", 0);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const result = await (service as any).resolveRef("develop", 1);
|
||||
expect(result).toBe("origin/develop");
|
||||
});
|
||||
|
||||
it("should return original ref when all attempts fail", async () => {
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
proc.stderr.emit("data", "error");
|
||||
proc.emit("close", 1);
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const result = await (service as any).resolveRef("nonexistent", 1);
|
||||
expect(result).toBe("nonexistent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runGitCommand", () => {
|
||||
it("should resolve with stdout on success", async () => {
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.stdout.emit("data", "output");
|
||||
mockProcess.emit("close", 0);
|
||||
});
|
||||
|
||||
const result = await (service as any).runGitCommand(["status"]);
|
||||
expect(result).toBe("output");
|
||||
});
|
||||
|
||||
it("should reject on non-zero exit code", async () => {
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.stderr.emit("data", "error msg");
|
||||
mockProcess.emit("close", 1);
|
||||
});
|
||||
|
||||
await expect((service as any).runGitCommand(["bad"])).rejects.toThrow("Git 命令失败");
|
||||
});
|
||||
|
||||
it("should reject on spawn error", async () => {
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.emit("error", new Error("spawn failed"));
|
||||
});
|
||||
|
||||
await expect((service as any).runGitCommand(["bad"])).rejects.toThrow("spawn failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCodeReferences", () => {
|
||||
it("should find references using git grep", async () => {
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
proc.stdout.emit("data", "other.ts\nanother.ts\n");
|
||||
proc.emit("close", 0);
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const blocks = [
|
||||
{ file: "test.ts", startLine: 1, endLine: 5, content: "function myFunc() {}" },
|
||||
];
|
||||
const refs = await (service as any).findCodeReferences(blocks);
|
||||
expect(refs.size).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("should skip short identifiers", async () => {
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
proc.emit("close", 1);
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const blocks = [{ file: "test.ts", startLine: 1, endLine: 1, content: "const x = 1;" }];
|
||||
const refs = await (service as any).findCodeReferences(blocks);
|
||||
expect(refs.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle grep errors gracefully", async () => {
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
proc.stderr.emit("data", "error");
|
||||
proc.emit("close", 1);
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const blocks = [
|
||||
{ file: "test.ts", startLine: 1, endLine: 5, content: "function longFuncName() {}" },
|
||||
];
|
||||
const refs = await (service as any).findCodeReferences(blocks);
|
||||
expect(refs.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDeletedCodeBlocks", () => {
|
||||
it("should parse deleted blocks from git diff output", async () => {
|
||||
let callCount = 0;
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
callCount++;
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
if (callCount <= 2) {
|
||||
// resolveRef: rev-parse 成功
|
||||
proc.stdout.emit("data", "abc123");
|
||||
proc.emit("close", 0);
|
||||
} else {
|
||||
// git diff 返回有删除的内容
|
||||
proc.stdout.emit(
|
||||
"data",
|
||||
`diff --git a/test.ts b/test.ts\n--- a/test.ts\n+++ b/test.ts\n@@ -1,3 +1,1 @@\n-const old1 = 1;\n-const old2 = 2;\n+const new1 = 1;\n@@ -10,2 +8,0 @@\n-function removed() {}\n-// end`,
|
||||
);
|
||||
proc.emit("close", 0);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const blocks = await (service as any).getDeletedCodeBlocks("main", "feature", 1);
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should handle diff with last block at end", async () => {
|
||||
let callCount = 0;
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
callCount++;
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
if (callCount <= 2) {
|
||||
proc.stdout.emit("data", "abc123");
|
||||
proc.emit("close", 0);
|
||||
} else {
|
||||
proc.stdout.emit(
|
||||
"data",
|
||||
`diff --git a/test.ts b/test.ts\n--- a/test.ts\n+++ b/test.ts\n@@ -1,2 +1,0 @@\n-const removed1 = true;\n-const removed2 = true;`,
|
||||
);
|
||||
proc.emit("close", 0);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const blocks = await (service as any).getDeletedCodeBlocks("main", "feature");
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzeDeletionImpact - git-diff with blocks", () => {
|
||||
it("should analyze deleted blocks from git diff", async () => {
|
||||
let callCount = 0;
|
||||
(child_process.spawn as Mock).mockImplementation(() => {
|
||||
callCount++;
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.stderr = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
if (callCount <= 2) {
|
||||
proc.stdout.emit("data", "abc123");
|
||||
proc.emit("close", 0);
|
||||
} else if (callCount === 3) {
|
||||
// git diff 返回有删除的内容
|
||||
proc.stdout.emit(
|
||||
"data",
|
||||
`diff --git a/test.ts b/test.ts\n--- a/test.ts\n+++ b/test.ts\n@@ -1,1 +1,0 @@\n-function removedFunc() {}`,
|
||||
);
|
||||
proc.emit("close", 0);
|
||||
} else {
|
||||
// git grep 失败(没有引用)
|
||||
proc.stderr.emit("data", "");
|
||||
proc.emit("close", 1);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
});
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: {
|
||||
structuredOutput: {
|
||||
impacts: [
|
||||
{
|
||||
file: "test.ts",
|
||||
deletedCode: "removedFunc",
|
||||
riskLevel: "low",
|
||||
affectedFiles: [],
|
||||
reason: "safe",
|
||||
},
|
||||
],
|
||||
summary: "safe deletion",
|
||||
},
|
||||
},
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const context = { baseRef: "main", headRef: "feature" };
|
||||
const result = await service.analyzeDeletionImpact(context, "openai", 1);
|
||||
expect(result.impacts.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.summary).toBe("safe deletion");
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzeDeletionImpact - agent mode", () => {
|
||||
it("should use agent mode for claude-code", async () => {
|
||||
const context = { owner: "o", repo: "r", prNumber: 1, analysisMode: "claude-code" as const };
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([
|
||||
{
|
||||
filename: "test.ts",
|
||||
patch: "@@ -1,1 +1,0 @@\n-const removed = true;",
|
||||
deletions: 1,
|
||||
},
|
||||
] as any);
|
||||
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.emit("close", 1);
|
||||
});
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { impacts: [], summary: "agent analysis" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai");
|
||||
expect(result.summary).toBe("agent analysis");
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyzeDeletionImpact - defensive checks", () => {
|
||||
it("should fix invalid impacts array", async () => {
|
||||
const context = { owner: "o", repo: "r", prNumber: 1 };
|
||||
gitProvider.getPullRequestFiles.mockResolvedValue([
|
||||
{
|
||||
filename: "test.ts",
|
||||
patch: "@@ -1,1 +1,0 @@\n-const removed = true;",
|
||||
deletions: 1,
|
||||
},
|
||||
] as any);
|
||||
|
||||
const mockProcess = new EventEmitter() as any;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
(child_process.spawn as Mock).mockReturnValue(mockProcess);
|
||||
process.nextTick(() => {
|
||||
mockProcess.emit("close", 1);
|
||||
});
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { impacts: null, summary: "ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.analyzeDeletionImpact(context, "openai");
|
||||
expect(result.impacts).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
879
commands/review/src/deletion-impact.service.ts
Normal file
879
commands/review/src/deletion-impact.service.ts
Normal file
@@ -0,0 +1,879 @@
|
||||
import {
|
||||
Injectable,
|
||||
LlmProxyService,
|
||||
logStreamEvent,
|
||||
createStreamLoggerState,
|
||||
type LLMMode,
|
||||
type VerboseLevel,
|
||||
shouldLog,
|
||||
type LlmJsonPutSchema,
|
||||
LlmJsonPut,
|
||||
GitProviderService,
|
||||
ChangedFile,
|
||||
} from "@spaceflow/core";
|
||||
import micromatch from "micromatch";
|
||||
import type { DeletionImpactResult } from "./review-spec";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
export interface DeletedCodeBlock {
|
||||
file: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
content: string;
|
||||
commit?: string;
|
||||
}
|
||||
|
||||
const DELETION_IMPACT_SCHEMA: LlmJsonPutSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
impacts: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file: { type: "string", description: "被删除代码所在的文件路径" },
|
||||
deletedCode: { type: "string", description: "被删除的代码片段摘要(前50字符)" },
|
||||
riskLevel: {
|
||||
type: "string",
|
||||
enum: ["high", "medium", "low", "none"],
|
||||
description:
|
||||
"风险等级:high=可能导致功能异常,medium=可能影响部分功能,low=影响较小,none=无影响",
|
||||
},
|
||||
affectedFiles: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "可能受影响的文件列表",
|
||||
},
|
||||
reason: { type: "string", description: "影响分析的详细说明" },
|
||||
suggestion: { type: "string", description: "建议的处理方式" },
|
||||
},
|
||||
required: ["file", "deletedCode", "riskLevel", "affectedFiles", "reason"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
summary: { type: "string", description: "删除代码影响的整体总结" },
|
||||
},
|
||||
required: ["impacts", "summary"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export type DeletionDiffSource = "provider-api" | "git-diff";
|
||||
|
||||
export interface DeletionAnalysisContext {
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
prNumber?: number;
|
||||
baseRef?: string;
|
||||
headRef?: string;
|
||||
/** diff 来源:provider-api 使用 Git Provider API,git-diff 使用本地 git 命令(两点语法) */
|
||||
diffSource?: DeletionDiffSource;
|
||||
/** 分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式(可使用工具) */
|
||||
analysisMode?: LLMMode;
|
||||
/** 文件过滤 glob 模式,与 review.includes 一致 */
|
||||
includes?: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DeletionImpactService {
|
||||
constructor(
|
||||
protected readonly llmProxyService: LlmProxyService,
|
||||
protected readonly gitProvider: GitProviderService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 分析删除代码的影响
|
||||
*/
|
||||
async analyzeDeletionImpact(
|
||||
context: DeletionAnalysisContext,
|
||||
llmMode: LLMMode,
|
||||
verbose?: VerboseLevel,
|
||||
): Promise<DeletionImpactResult> {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(`\n🔍 开始分析删除代码的影响...`);
|
||||
}
|
||||
|
||||
// 1. 获取删除的代码块
|
||||
let deletedBlocks: DeletedCodeBlock[];
|
||||
const diffSource = context.diffSource ?? (context.prNumber ? "provider-api" : "git-diff");
|
||||
|
||||
if (diffSource === "provider-api" && context.prNumber && context.owner && context.repo) {
|
||||
// Git Provider API 模式:使用 API 获取 diff
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📡 使用 Git Provider API 获取 PR #${context.prNumber} 的 diff`);
|
||||
}
|
||||
const changedFiles = await this.gitProvider.getPullRequestFiles(
|
||||
context.owner,
|
||||
context.repo,
|
||||
context.prNumber,
|
||||
);
|
||||
// 检查 changedFiles 是否包含 patch 字段
|
||||
const filesWithPatch = changedFiles.filter((f) => f.patch);
|
||||
const filesWithDeletions = changedFiles.filter((f) => f.deletions && f.deletions > 0);
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(
|
||||
` 📊 共 ${changedFiles.length} 个文件, ${filesWithPatch.length} 个有 patch, ${filesWithDeletions.length} 个有删除`,
|
||||
);
|
||||
}
|
||||
|
||||
if (filesWithPatch.length > 0) {
|
||||
// 有 patch 字段,直接解析
|
||||
deletedBlocks = this.extractDeletedBlocksFromChangedFiles(changedFiles);
|
||||
} else if (filesWithDeletions.length > 0) {
|
||||
// 没有 patch 字段但有删除,使用 PR diff API
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⚠️ API 未返回 patch 字段,使用 PR diff API`);
|
||||
}
|
||||
try {
|
||||
const diffText = await this.gitProvider.getPullRequestDiff(
|
||||
context.owner,
|
||||
context.repo,
|
||||
context.prNumber,
|
||||
);
|
||||
deletedBlocks = this.extractDeletedBlocksFromDiffText(diffText);
|
||||
} catch (e) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ❌ PR diff API 失败: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
deletedBlocks = [];
|
||||
}
|
||||
} else {
|
||||
// 没有删除的文件
|
||||
deletedBlocks = [];
|
||||
}
|
||||
} else if (context.baseRef && context.headRef) {
|
||||
// Git Diff 模式:使用本地 git 命令(两点语法)
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 💻 使用 Git Diff 获取 ${context.baseRef}..${context.headRef} 的差异`);
|
||||
}
|
||||
deletedBlocks = await this.getDeletedCodeBlocks(context.baseRef, context.headRef, verbose);
|
||||
} else {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⚠️ 缺少必要参数,跳过删除分析`);
|
||||
}
|
||||
return { impacts: [], summary: "缺少必要参数" };
|
||||
}
|
||||
if (deletedBlocks.length === 0) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ✅ 没有发现删除的代码`);
|
||||
}
|
||||
return { impacts: [], summary: "没有发现删除的代码" };
|
||||
}
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📦 发现 ${deletedBlocks.length} 个删除的代码块`);
|
||||
}
|
||||
|
||||
// 1.5 使用 includes 过滤文件
|
||||
if (context.includes && context.includes.length > 0) {
|
||||
const beforeCount = deletedBlocks.length;
|
||||
const filenames = deletedBlocks.map((b) => b.file);
|
||||
const matchedFilenames = micromatch(filenames, context.includes);
|
||||
deletedBlocks = deletedBlocks.filter((b) => matchedFilenames.includes(b.file));
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 🔍 Includes 过滤: ${beforeCount} -> ${deletedBlocks.length} 个删除块`);
|
||||
}
|
||||
if (deletedBlocks.length === 0) {
|
||||
return { impacts: [], summary: "过滤后没有需要分析的删除代码" };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取删除代码的引用关系
|
||||
const references = await this.findCodeReferences(deletedBlocks);
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 🔗 找到 ${references.size} 个文件可能引用了被删除的代码`);
|
||||
}
|
||||
|
||||
// 3. 根据分析模式选择不同的分析方法
|
||||
const analysisMode = context.analysisMode ?? "openai";
|
||||
let result: DeletionImpactResult;
|
||||
|
||||
if (["claude-code", "open-code"].includes(analysisMode)) {
|
||||
// Claude Agent 模式:使用工具主动探索代码库
|
||||
result = await this.analyzeWithAgent(analysisMode, deletedBlocks, references, verbose);
|
||||
} else {
|
||||
// OpenAI 模式:标准 chat completion
|
||||
result = await this.analyzeWithLLM(deletedBlocks, references, llmMode, verbose);
|
||||
}
|
||||
|
||||
// 防御性检查:确保 impacts 是数组
|
||||
if (!result.impacts || !Array.isArray(result.impacts)) {
|
||||
result.impacts = [];
|
||||
}
|
||||
|
||||
const highRiskCount = result.impacts.filter((i) => i.riskLevel === "high").length;
|
||||
const mediumRiskCount = result.impacts.filter((i) => i.riskLevel === "medium").length;
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(`\n📊 分析完成: ${highRiskCount} 个高风险, ${mediumRiskCount} 个中风险`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Git Provider API 返回的 ChangedFile 中提取被删除的代码块
|
||||
*/
|
||||
protected extractDeletedBlocksFromChangedFiles(changedFiles: ChangedFile[]): DeletedCodeBlock[] {
|
||||
const deletedBlocks: DeletedCodeBlock[] = [];
|
||||
|
||||
for (const file of changedFiles) {
|
||||
if (!file.filename || !file.patch) continue;
|
||||
|
||||
const blocks = this.parseDeletedBlocksFromPatch(file.filename, file.patch);
|
||||
deletedBlocks.push(...blocks);
|
||||
}
|
||||
|
||||
// 过滤掉空白行和注释行为主的删除块
|
||||
return this.filterMeaningfulBlocks(deletedBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单个文件的 patch 中解析删除的代码块
|
||||
*/
|
||||
protected parseDeletedBlocksFromPatch(filename: string, patch: string): DeletedCodeBlock[] {
|
||||
const deletedBlocks: DeletedCodeBlock[] = [];
|
||||
const lines = patch.split("\n");
|
||||
let currentDeleteBlock: { startLine: number; lines: string[] } | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
||||
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
// 保存之前的删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
}
|
||||
currentDeleteBlock = {
|
||||
startLine: parseInt(hunkMatch[1], 10),
|
||||
lines: [],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// 删除的行(以 - 开头,但不是 ---)
|
||||
if (line.startsWith("-") && !line.startsWith("---") && currentDeleteBlock) {
|
||||
currentDeleteBlock.lines.push(line.slice(1)); // 去掉 - 前缀
|
||||
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||
// 新增行,保存当前删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
currentDeleteBlock = { startLine: currentDeleteBlock.startLine, lines: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
}
|
||||
|
||||
return deletedBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤掉空白行和注释行为主的删除块
|
||||
*/
|
||||
protected filterMeaningfulBlocks(blocks: DeletedCodeBlock[]): DeletedCodeBlock[] {
|
||||
return blocks.filter((block) => {
|
||||
const meaningfulLines = block.content.split("\n").filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
return (
|
||||
trimmed &&
|
||||
!trimmed.startsWith("//") &&
|
||||
!trimmed.startsWith("*") &&
|
||||
!trimmed.startsWith("/*")
|
||||
);
|
||||
});
|
||||
return meaningfulLines.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 diff 文本中提取被删除的代码块
|
||||
*/
|
||||
protected extractDeletedBlocksFromDiffText(diffText: string): DeletedCodeBlock[] {
|
||||
const deletedBlocks: DeletedCodeBlock[] = [];
|
||||
const fileDiffs = diffText.split(/^diff --git /m).filter(Boolean);
|
||||
|
||||
for (const fileDiff of fileDiffs) {
|
||||
// 解析文件名
|
||||
const headerMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)[\r\n]/);
|
||||
if (!headerMatch) continue;
|
||||
|
||||
const filename = headerMatch[1]; // 使用原文件名(a/...)
|
||||
const lines = fileDiff.split("\n");
|
||||
let currentDeleteBlock: { startLine: number; lines: string[] } | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
||||
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
// 保存之前的删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
}
|
||||
currentDeleteBlock = {
|
||||
startLine: parseInt(hunkMatch[1], 10),
|
||||
lines: [],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// 删除的行(以 - 开头,但不是 ---)
|
||||
if (line.startsWith("-") && !line.startsWith("---") && currentDeleteBlock) {
|
||||
currentDeleteBlock.lines.push(line.slice(1)); // 去掉 - 前缀
|
||||
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||
// 新增行,保存当前删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
currentDeleteBlock = { startLine: currentDeleteBlock.startLine, lines: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤掉空白行和注释行为主的删除块
|
||||
return this.filterMeaningfulBlocks(deletedBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 git diff 中提取被删除的代码块
|
||||
*/
|
||||
protected async getDeletedCodeBlocks(
|
||||
baseRef: string,
|
||||
headRef: string,
|
||||
verbose?: VerboseLevel,
|
||||
): Promise<DeletedCodeBlock[]> {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 🔎 分析 ${baseRef}...${headRef} 的删除代码`);
|
||||
}
|
||||
// 尝试解析 ref,支持本地分支、远程分支、commit SHA
|
||||
const resolvedBaseRef = await this.resolveRef(baseRef, verbose);
|
||||
const resolvedHeadRef = await this.resolveRef(headRef, verbose);
|
||||
|
||||
// 使用两点语法 (..) 而非三点语法 (...),避免浅克隆时找不到 merge base
|
||||
// 两点语法直接比较两个 ref 的差异,不需要计算共同祖先
|
||||
const diffOutput = await this.runGitCommand([
|
||||
"diff",
|
||||
"-U0", // 不显示上下文,只显示变更
|
||||
`${resolvedBaseRef}..${resolvedHeadRef}`,
|
||||
]);
|
||||
|
||||
const deletedBlocks: DeletedCodeBlock[] = [];
|
||||
const fileDiffs = diffOutput.split(/^diff --git /m).filter(Boolean);
|
||||
|
||||
for (const fileDiff of fileDiffs) {
|
||||
// 解析文件名
|
||||
const headerMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)[\r\n]/);
|
||||
if (!headerMatch) continue;
|
||||
|
||||
const filename = headerMatch[1]; // 使用原文件名(a/...)
|
||||
const lines = fileDiff.split("\n");
|
||||
let currentDeleteBlock: { startLine: number; lines: string[] } | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
||||
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
// 保存之前的删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
}
|
||||
currentDeleteBlock = {
|
||||
startLine: parseInt(hunkMatch[1], 10),
|
||||
lines: [],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// 删除的行(以 - 开头,但不是 ---)
|
||||
if (line.startsWith("-") && !line.startsWith("---") && currentDeleteBlock) {
|
||||
currentDeleteBlock.lines.push(line.slice(1)); // 去掉 - 前缀
|
||||
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||
// 新增行,保存当前删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
currentDeleteBlock = { startLine: currentDeleteBlock.startLine, lines: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个删除块
|
||||
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
||||
deletedBlocks.push({
|
||||
file: filename,
|
||||
startLine: currentDeleteBlock.startLine,
|
||||
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
||||
content: currentDeleteBlock.lines.join("\n"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤掉空白行和注释行为主的删除块
|
||||
return this.filterMeaningfulBlocks(deletedBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可能引用被删除代码的文件
|
||||
*/
|
||||
protected async findCodeReferences(
|
||||
deletedBlocks: DeletedCodeBlock[],
|
||||
): Promise<Map<string, string[]>> {
|
||||
const references = new Map<string, string[]>();
|
||||
|
||||
for (const block of deletedBlocks) {
|
||||
// 从删除的代码中提取可能的标识符(函数名、类名、变量名等)
|
||||
const identifiers = this.extractIdentifiers(block.content);
|
||||
const fileRefs: string[] = [];
|
||||
|
||||
for (const identifier of identifiers) {
|
||||
if (identifier.length < 3) continue; // 跳过太短的标识符
|
||||
|
||||
try {
|
||||
// 使用 git grep 查找引用
|
||||
const grepOutput = await this.runGitCommand([
|
||||
"grep",
|
||||
"-l", // 只输出文件名
|
||||
"-w", // 全词匹配
|
||||
identifier,
|
||||
"--",
|
||||
"*.ts",
|
||||
"*.js",
|
||||
"*.tsx",
|
||||
"*.jsx",
|
||||
]);
|
||||
|
||||
const files = grepOutput
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((f) => f && f !== block.file);
|
||||
fileRefs.push(...files);
|
||||
} catch {
|
||||
// grep 没找到匹配,忽略
|
||||
}
|
||||
}
|
||||
|
||||
if (fileRefs.length > 0) {
|
||||
const uniqueRefs = [...new Set(fileRefs)];
|
||||
references.set(`${block.file}:${block.startLine}-${block.endLine}`, uniqueRefs);
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从代码中提取标识符
|
||||
*/
|
||||
protected extractIdentifiers(code: string): string[] {
|
||||
const identifiers: string[] = [];
|
||||
|
||||
// 匹配函数定义
|
||||
const funcMatches = code.matchAll(/(?:function|async\s+function)\s+(\w+)/g);
|
||||
for (const match of funcMatches) {
|
||||
identifiers.push(match[1]);
|
||||
}
|
||||
|
||||
// 匹配方法定义
|
||||
const methodMatches = code.matchAll(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/g);
|
||||
for (const match of methodMatches) {
|
||||
if (!["if", "for", "while", "switch", "catch", "function"].includes(match[1])) {
|
||||
identifiers.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配类定义
|
||||
const classMatches = code.matchAll(/class\s+(\w+)/g);
|
||||
for (const match of classMatches) {
|
||||
identifiers.push(match[1]);
|
||||
}
|
||||
|
||||
// 匹配接口定义
|
||||
const interfaceMatches = code.matchAll(/interface\s+(\w+)/g);
|
||||
for (const match of interfaceMatches) {
|
||||
identifiers.push(match[1]);
|
||||
}
|
||||
|
||||
// 匹配类型定义
|
||||
const typeMatches = code.matchAll(/type\s+(\w+)/g);
|
||||
for (const match of typeMatches) {
|
||||
identifiers.push(match[1]);
|
||||
}
|
||||
|
||||
// 匹配 export 的变量/常量
|
||||
const exportMatches = code.matchAll(/export\s+(?:const|let|var)\s+(\w+)/g);
|
||||
for (const match of exportMatches) {
|
||||
identifiers.push(match[1]);
|
||||
}
|
||||
|
||||
return [...new Set(identifiers)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 LLM 分析删除代码的影响
|
||||
*/
|
||||
protected async analyzeWithLLM(
|
||||
deletedBlocks: DeletedCodeBlock[],
|
||||
references: Map<string, string[]>,
|
||||
llmMode: LLMMode,
|
||||
verbose?: VerboseLevel,
|
||||
): Promise<DeletionImpactResult> {
|
||||
const llmJsonPut = new LlmJsonPut<DeletionImpactResult>(DELETION_IMPACT_SCHEMA);
|
||||
|
||||
const systemPrompt = `你是一个代码审查专家,专门分析删除代码可能带来的影响。
|
||||
|
||||
## 任务
|
||||
分析以下被删除的代码块,判断删除这些代码是否会影响到其他功能。
|
||||
|
||||
## 分析要点
|
||||
1. **功能依赖**: 被删除的代码是否被其他模块调用或依赖
|
||||
2. **接口变更**: 删除是否会导致 API 或接口不兼容
|
||||
3. **副作用**: 删除是否会影响系统的其他行为
|
||||
4. **数据流**: 删除是否会中断数据处理流程
|
||||
|
||||
## 风险等级判断标准
|
||||
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
||||
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
||||
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
||||
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
||||
|
||||
## 输出要求
|
||||
- 对每个有风险的删除块给出详细分析
|
||||
- 如果删除是安全的,也要说明原因
|
||||
- 提供具体的建议`;
|
||||
|
||||
const deletedCodeSection = deletedBlocks
|
||||
.map((block, index) => {
|
||||
const refs = references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
||||
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}
|
||||
|
||||
\`\`\`
|
||||
${block.content}
|
||||
\`\`\`
|
||||
|
||||
可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}
|
||||
`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const userPrompt = `## 被删除的代码块
|
||||
|
||||
${deletedCodeSection}
|
||||
|
||||
请分析这些删除操作可能带来的影响。`;
|
||||
|
||||
if (shouldLog(verbose, 2)) {
|
||||
console.log(`\nsystemPrompt:\n----------------\n${systemPrompt}\n----------------`);
|
||||
console.log(`\nuserPrompt:\n----------------\n${userPrompt}\n----------------`);
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = this.llmProxyService.chatStream(
|
||||
[
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
],
|
||||
{
|
||||
adapter: llmMode,
|
||||
jsonSchema: llmJsonPut,
|
||||
verbose,
|
||||
},
|
||||
);
|
||||
|
||||
let result: DeletionImpactResult | undefined;
|
||||
for await (const event of stream) {
|
||||
if (event.type === "result") {
|
||||
result = event.response.structuredOutput as DeletionImpactResult | undefined;
|
||||
} else if (event.type === "error") {
|
||||
console.error(` ❌ 分析失败: ${event.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 防御性检查:确保返回的是有效对象
|
||||
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
||||
return { impacts: [], summary: "分析返回格式无效" };
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(` ❌ LLM 调用失败: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.error(` 堆栈信息:\n${error.stack}`);
|
||||
}
|
||||
} else {
|
||||
console.error(` ❌ LLM 调用失败: ${String(error)}`);
|
||||
}
|
||||
return { impacts: [], summary: "LLM 调用失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Claude Agent 模式分析删除代码的影响
|
||||
* Claude Agent 可以使用工具主动探索代码库,分析更深入
|
||||
*/
|
||||
protected async analyzeWithAgent(
|
||||
analysisMode: LLMMode,
|
||||
deletedBlocks: DeletedCodeBlock[],
|
||||
references: Map<string, string[]>,
|
||||
verbose?: VerboseLevel,
|
||||
): Promise<DeletionImpactResult> {
|
||||
const llmJsonPut = new LlmJsonPut<DeletionImpactResult>(DELETION_IMPACT_SCHEMA);
|
||||
|
||||
const systemPrompt = `你是一个资深代码架构师,擅长分析代码变更的影响范围和潜在风险。
|
||||
|
||||
## 任务
|
||||
深入分析以下被删除的代码块,评估删除操作对代码库的影响。
|
||||
|
||||
## 你的能力
|
||||
你可以使用以下工具来深入分析代码:
|
||||
- **Read**: 读取文件内容,查看被删除代码的完整上下文
|
||||
- **Grep**: 搜索代码库,查找对被删除代码的引用
|
||||
- **Glob**: 查找匹配模式的文件
|
||||
|
||||
## 分析流程
|
||||
1. 首先阅读被删除代码的上下文,理解其功能
|
||||
2. 使用 Grep 搜索代码库中对这些代码的引用
|
||||
3. 分析引用处的代码,判断删除后的影响
|
||||
4. 给出风险评估和建议
|
||||
|
||||
## 风险等级判断标准
|
||||
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
||||
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
||||
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
||||
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
||||
|
||||
## 输出要求
|
||||
- 对每个有风险的删除块给出详细分析
|
||||
- 如果删除是安全的,也要说明原因
|
||||
- 提供具体的建议`;
|
||||
|
||||
const deletedCodeSection = deletedBlocks
|
||||
.map((block, index) => {
|
||||
const refs = references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
||||
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}
|
||||
|
||||
\`\`\`
|
||||
${block.content}
|
||||
\`\`\`
|
||||
|
||||
可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}
|
||||
`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const userPrompt = `## 被删除的代码块
|
||||
|
||||
${deletedCodeSection}
|
||||
|
||||
## 补充说明
|
||||
|
||||
请使用你的工具能力深入分析这些删除操作可能带来的影响。
|
||||
- 如果需要查看更多上下文,请读取相关文件
|
||||
- 如果需要确认引用关系,请搜索代码库
|
||||
- 分析完成后,给出结构化的影响评估`;
|
||||
|
||||
if (shouldLog(verbose, 2)) {
|
||||
console.log(
|
||||
`\n[Agent Mode] systemPrompt:\n----------------\n${systemPrompt}\n----------------`,
|
||||
);
|
||||
console.log(`\n[Agent Mode] userPrompt:\n----------------\n${userPrompt}\n----------------`);
|
||||
}
|
||||
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 🤖 使用 Agent 模式分析(${analysisMode},可使用工具)...`);
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = this.llmProxyService.chatStream(
|
||||
[
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
],
|
||||
{
|
||||
adapter: analysisMode,
|
||||
jsonSchema: llmJsonPut,
|
||||
allowedTools: ["Read", "Grep", "Glob"],
|
||||
verbose,
|
||||
},
|
||||
);
|
||||
|
||||
let result: DeletionImpactResult | undefined;
|
||||
const streamLoggerState = createStreamLoggerState();
|
||||
for await (const event of stream) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
logStreamEvent(event, streamLoggerState);
|
||||
}
|
||||
if (event.type === "result") {
|
||||
result = event.response.structuredOutput as DeletionImpactResult | undefined;
|
||||
} else if (event.type === "error") {
|
||||
console.error(` ❌ 分析失败: ${event.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 防御性检查:确保返回的是有效对象
|
||||
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
||||
return { impacts: [], summary: "分析返回格式无效" };
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(` ❌ Agent 调用失败: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.error(` 堆栈信息:\n${error.stack}`);
|
||||
}
|
||||
} else {
|
||||
console.error(` ❌ Agent 调用失败: ${String(error)}`);
|
||||
}
|
||||
return { impacts: [], summary: "Agent 调用失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 ref,支持本地分支、远程分支、commit SHA
|
||||
* 优先级:本地分支 > origin/分支 > fetch后重试 > 原始值
|
||||
*/
|
||||
protected async resolveRef(ref: string, verbose?: VerboseLevel): Promise<string> {
|
||||
if (!ref) {
|
||||
throw new Error(`resolveRef: ref 参数不能为空。调用栈: ${new Error().stack}`);
|
||||
}
|
||||
// 如果已经是 commit SHA 格式(7-40位十六进制),直接返回
|
||||
if (/^[0-9a-f]{7,40}$/i.test(ref)) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📌 ${ref} 是 commit SHA,直接使用`);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
// 如果已经是 origin/ 格式,直接返回
|
||||
if (ref.startsWith("origin/")) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📌 ${ref} 已是远程分支格式,直接使用`);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
// 尝试解析本地分支
|
||||
try {
|
||||
await this.runGitCommand(["rev-parse", "--verify", ref]);
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📌 ${ref} 解析为本地分支`);
|
||||
}
|
||||
return ref;
|
||||
} catch {
|
||||
// 本地分支不存在,尝试 origin/分支
|
||||
}
|
||||
|
||||
// 尝试 origin/分支
|
||||
try {
|
||||
await this.runGitCommand(["rev-parse", "--verify", `origin/${ref}`]);
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📌 ${ref} 解析为 origin/${ref}`);
|
||||
}
|
||||
return `origin/${ref}`;
|
||||
} catch {
|
||||
// origin/分支也不存在,尝试 fetch
|
||||
}
|
||||
|
||||
// 尝试 fetch 该分支
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⏳ 尝试 fetch ${ref}...`);
|
||||
}
|
||||
try {
|
||||
await this.runGitCommand([
|
||||
"fetch",
|
||||
"origin",
|
||||
`${ref}:refs/remotes/origin/${ref}`,
|
||||
"--depth=1",
|
||||
]);
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 📌 ${ref} fetch 成功,使用 origin/${ref}`);
|
||||
}
|
||||
return `origin/${ref}`;
|
||||
} catch (e) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⚠️ fetch ${ref} 失败: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⚠️ 无法解析 ${ref},使用原始值`);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
protected runGitCommand(args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git", args, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(`Git 命令失败 (${code}): ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
42
commands/review/src/dto/mcp.dto.ts
Normal file
42
commands/review/src/dto/mcp.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
ApiProperty,
|
||||
ApiPropertyOptional,
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
t,
|
||||
} from "@spaceflow/core";
|
||||
|
||||
export class ListRulesInput {
|
||||
@ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export class GetRulesForFileInput {
|
||||
@ApiProperty({ description: t("review:mcp.dto.filePath") })
|
||||
@IsString()
|
||||
filePath!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
cwd?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: t("review:mcp.dto.includeExamples") })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
includeExamples?: boolean;
|
||||
}
|
||||
|
||||
export class GetRuleDetailInput {
|
||||
@ApiProperty({ description: t("review:mcp.dto.ruleId") })
|
||||
@IsString()
|
||||
ruleId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
cwd?: string;
|
||||
}
|
||||
32
commands/review/src/index.ts
Normal file
32
commands/review/src/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import "./locales";
|
||||
import { SpaceflowExtension, SpaceflowExtensionMetadata, t } from "@spaceflow/core";
|
||||
import { ReviewModule } from "./review.module";
|
||||
import { reviewSchema } from "./review.config";
|
||||
/** review Extension 元数据 */
|
||||
export const reviewMetadata: SpaceflowExtensionMetadata = {
|
||||
name: "review",
|
||||
commands: ["review"],
|
||||
configKey: "review",
|
||||
configSchema: reviewSchema,
|
||||
version: "1.0.0",
|
||||
description: t("review:extensionDescription"),
|
||||
};
|
||||
|
||||
export class ReviewExtension implements SpaceflowExtension {
|
||||
getMetadata(): SpaceflowExtensionMetadata {
|
||||
return reviewMetadata;
|
||||
}
|
||||
|
||||
getModule() {
|
||||
return ReviewModule;
|
||||
}
|
||||
}
|
||||
|
||||
export default ReviewExtension;
|
||||
|
||||
export * from "./review.module";
|
||||
export * from "./review.command";
|
||||
export * from "./review.service";
|
||||
export * from "./review.mcp";
|
||||
export * from "./issue-verify.service";
|
||||
export * from "./deletion-impact.service";
|
||||
460
commands/review/src/issue-verify.service.spec.ts
Normal file
460
commands/review/src/issue-verify.service.spec.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { vi, type Mocked } from "vitest";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { LlmProxyService } from "@spaceflow/core";
|
||||
import { ReviewIssue, FileContentsMap, ReviewSpecService } from "./review-spec";
|
||||
import { IssueVerifyService } from "./issue-verify.service";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
||||
query: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("IssueVerifyService", () => {
|
||||
let service: IssueVerifyService;
|
||||
let llmProxyService: Mocked<LlmProxyService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockLlmProxyService = {
|
||||
chatStream: vi.fn(),
|
||||
};
|
||||
|
||||
const mockReviewSpecService = {
|
||||
findRuleById: vi.fn(),
|
||||
buildSpecsSection: vi.fn().mockReturnValue("mock rule specs"),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
IssueVerifyService,
|
||||
{
|
||||
provide: LlmProxyService,
|
||||
useValue: mockLlmProxyService,
|
||||
},
|
||||
{
|
||||
provide: ReviewSpecService,
|
||||
useValue: mockReviewSpecService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<IssueVerifyService>(IssueVerifyService);
|
||||
llmProxyService = module.get(LlmProxyService) as Mocked<LlmProxyService>;
|
||||
});
|
||||
|
||||
it("should return empty array if no issues provided", async () => {
|
||||
const result = await service.verifyIssueFixes([], new Map(), [], "openai");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should skip already fixed issues", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
fixed: "2023-01-01",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const result = await service.verifyIssueFixes(
|
||||
issues,
|
||||
new Map() as FileContentsMap,
|
||||
[],
|
||||
"openai",
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(issues[0]);
|
||||
expect(llmProxyService.chatStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should mark as fixed if file is deleted", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "deleted.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map();
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fixed).toBeDefined();
|
||||
expect(llmProxyService.chatStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call LLM to verify issue fix", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "new content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: true, reason: "Fixed now" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fixed).toBeDefined();
|
||||
expect(llmProxyService.chatStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle LLM saying issue is not fixed", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([
|
||||
["test.ts", [["-------", "still bad content"]]],
|
||||
]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: false, reason: "Still broken" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fixed).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle invalid issue from LLM", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: false, valid: false, reason: "False positive" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].valid).toBe("false");
|
||||
expect(result[0].fixed).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle error in LLM stream", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "error", message: "LLM error" };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(issues[0]); // Returns original issue on error
|
||||
});
|
||||
|
||||
it("should handle exception during LLM call", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
llmProxyService.chatStream.mockImplementation(() => {
|
||||
throw new Error("Critical failure");
|
||||
});
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(issues[0]);
|
||||
});
|
||||
|
||||
it("should skip issues with valid=false", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
valid: "false",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const result = await service.verifyIssueFixes(
|
||||
issues,
|
||||
new Map() as FileContentsMap,
|
||||
[],
|
||||
"openai",
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(issues[0]);
|
||||
expect(llmProxyService.chatStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle non-Error exception during LLM call", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
llmProxyService.chatStream.mockImplementation(() => {
|
||||
throw "string error";
|
||||
});
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(issues[0]);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should return original issue when LLM returns no structured output", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield { type: "result", response: { content: "no json" } };
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(issues[0]);
|
||||
});
|
||||
|
||||
it("should include suggestion in prompt when present", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
suggestion: "fix this way",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: true, valid: true, reason: "ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fixed).toBeDefined();
|
||||
const callArgs = llmProxyService.chatStream.mock.calls[0][0];
|
||||
expect(callArgs[1].content).toContain("fix this way");
|
||||
});
|
||||
|
||||
it("should log verbose messages when verbose=1", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: true, valid: true, reason: "Fixed" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await service.verifyIssueFixes(issues, fileContents, [], "openai", 1);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should log verbose for invalid issues", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: false, valid: false, reason: "Invalid" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await service.verifyIssueFixes(issues, fileContents, [], "openai", 1);
|
||||
const logMessages = consoleSpy.mock.calls.map((c) => c[0]);
|
||||
expect(logMessages.some((m: string) => m.includes("无效问题"))).toBe(true);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should log verbose for unfixed issues", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: false, valid: true, reason: "Still broken" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await service.verifyIssueFixes(issues, fileContents, [], "openai", 1);
|
||||
const logMessages = consoleSpy.mock.calls.map((c) => c[0]);
|
||||
expect(logMessages.some((m: string) => m.includes("未修复"))).toBe(true);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should use ruleInfo when available", async () => {
|
||||
const mockReviewSpecService = (service as any).reviewSpecService;
|
||||
mockReviewSpecService.findRuleById.mockReturnValue({
|
||||
rule: { id: "R1", description: "test rule" },
|
||||
spec: { name: "test-spec" },
|
||||
});
|
||||
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
const specs = [{ name: "test-spec" }] as any;
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: false, valid: true, reason: "ok" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
await service.verifyIssueFixes(issues, fileContents, specs, "openai");
|
||||
expect(mockReviewSpecService.buildSpecsSection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle valid=true and fixed=true result", async () => {
|
||||
const issues: ReviewIssue[] = [
|
||||
{
|
||||
file: "test.ts",
|
||||
line: "10",
|
||||
ruleId: "R1",
|
||||
specFile: "s1.md",
|
||||
reason: "r1",
|
||||
round: 1,
|
||||
} as any,
|
||||
];
|
||||
const fileContents: FileContentsMap = new Map([["test.ts", [["-------", "content"]]]]);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
response: { structuredOutput: { fixed: true, valid: true, reason: "Fixed" } },
|
||||
};
|
||||
})();
|
||||
llmProxyService.chatStream.mockReturnValue(mockStream as any);
|
||||
|
||||
const result = await service.verifyIssueFixes(issues, fileContents, [], "openai");
|
||||
expect(result[0].fixed).toBeDefined();
|
||||
expect(result[0].valid).toBe("true");
|
||||
});
|
||||
});
|
||||
309
commands/review/src/issue-verify.service.ts
Normal file
309
commands/review/src/issue-verify.service.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import {
|
||||
Injectable,
|
||||
LlmProxyService,
|
||||
type LLMMode,
|
||||
type VerboseLevel,
|
||||
shouldLog,
|
||||
type LlmJsonPutSchema,
|
||||
LlmJsonPut,
|
||||
parallel,
|
||||
} from "@spaceflow/core";
|
||||
import {
|
||||
ReviewIssue,
|
||||
ReviewSpec,
|
||||
ReviewRule,
|
||||
ReviewSpecService,
|
||||
FileContentsMap,
|
||||
FileContentLine,
|
||||
} from "./review-spec";
|
||||
|
||||
interface VerifyResult {
|
||||
fixed: boolean;
|
||||
valid: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const TRUE = "true";
|
||||
const FALSE = "false";
|
||||
|
||||
const VERIFY_SCHEMA: LlmJsonPutSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
fixed: {
|
||||
type: "boolean",
|
||||
description: "问题是否已被修复",
|
||||
},
|
||||
valid: {
|
||||
type: "boolean",
|
||||
description: "问题是否有效,有效的条件就是你需要看看代码是否符合规范",
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
description: "判断依据,说明为什么认为问题已修复或仍存在",
|
||||
},
|
||||
},
|
||||
required: ["fixed", "valid", "reason"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class IssueVerifyService {
|
||||
constructor(
|
||||
protected readonly llmProxyService: LlmProxyService,
|
||||
protected readonly reviewSpecService: ReviewSpecService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 验证历史 issues 是否已被修复
|
||||
* 按并发数批量验证,每批并行调用 LLM
|
||||
*/
|
||||
async verifyIssueFixes(
|
||||
existingIssues: ReviewIssue[],
|
||||
fileContents: FileContentsMap,
|
||||
specs: ReviewSpec[],
|
||||
llmMode: LLMMode,
|
||||
verbose?: VerboseLevel,
|
||||
concurrency: number = 10,
|
||||
): Promise<ReviewIssue[]> {
|
||||
if (existingIssues.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(
|
||||
`\n🔍 开始验证 ${existingIssues.length} 个历史问题是否已修复 (并发: ${concurrency})...`,
|
||||
);
|
||||
}
|
||||
|
||||
const verifiedIssues: ReviewIssue[] = [];
|
||||
const llmJsonPut = new LlmJsonPut<VerifyResult>(VERIFY_SCHEMA);
|
||||
|
||||
// 预处理:分离已修复和需要验证的 issues
|
||||
const toVerify: {
|
||||
issue: ReviewIssue;
|
||||
fileContent: FileContentLine[];
|
||||
ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null;
|
||||
}[] = [];
|
||||
for (const issue of existingIssues) {
|
||||
if (issue.fixed) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⏭️ 跳过已修复: ${issue.file}:${issue.line} (${issue.ruleId})`);
|
||||
}
|
||||
verifiedIssues.push(issue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// valid === 'false' 的问题跳过复查(已确认无效的问题无需再次验证)
|
||||
if (issue.valid === "false") {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⏭️ 跳过无效问题: ${issue.file}:${issue.line} (${issue.ruleId})`);
|
||||
}
|
||||
verifiedIssues.push(issue);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileContent = fileContents.get(issue.file);
|
||||
if (fileContent === undefined) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ✅ 文件已删除: ${issue.file}:${issue.line} (${issue.ruleId})`);
|
||||
}
|
||||
verifiedIssues.push({
|
||||
...issue,
|
||||
fixed: new Date().toISOString(),
|
||||
valid: FALSE,
|
||||
reason: "文件已删除",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const ruleInfo = this.reviewSpecService.findRuleById(issue.ruleId, specs);
|
||||
toVerify.push({ issue, fileContent, ruleInfo });
|
||||
}
|
||||
|
||||
// 使用 parallel 库并行处理
|
||||
const executor = parallel({
|
||||
concurrency,
|
||||
onTaskStart: (taskId) => {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` 🔎 验证: ${taskId}`);
|
||||
}
|
||||
},
|
||||
onTaskComplete: (taskId, success) => {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ${success ? "✅" : "❌"} 完成: ${taskId}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const results = await executor.map(
|
||||
toVerify,
|
||||
async ({ issue, fileContent, ruleInfo }) =>
|
||||
this.verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose),
|
||||
({ issue }) => `${issue.file}:${issue.line}`,
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.success && result.result) {
|
||||
verifiedIssues.push(result.result);
|
||||
} else {
|
||||
// 失败时保留原始 issue
|
||||
const originalItem = toVerify.find(
|
||||
(item) => `${item.issue.file}:${item.issue.line}` === result.id,
|
||||
);
|
||||
if (originalItem) {
|
||||
verifiedIssues.push(originalItem.issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fixedCount = verifiedIssues.filter((i) => i.fixed).length;
|
||||
const unfixedCount = verifiedIssues.length - fixedCount;
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(`\n📊 验证完成: ${fixedCount} 个已修复, ${unfixedCount} 个未修复`);
|
||||
}
|
||||
|
||||
return verifiedIssues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单个 issue 是否已修复
|
||||
*/
|
||||
protected async verifySingleIssue(
|
||||
issue: ReviewIssue,
|
||||
fileContent: FileContentLine[],
|
||||
ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null,
|
||||
llmMode: LLMMode,
|
||||
llmJsonPut: LlmJsonPut<VerifyResult>,
|
||||
verbose?: VerboseLevel,
|
||||
): Promise<ReviewIssue> {
|
||||
const verifyPrompt = this.buildVerifyPrompt(issue, fileContent, ruleInfo);
|
||||
|
||||
try {
|
||||
const stream = this.llmProxyService.chatStream(
|
||||
[
|
||||
{ role: "system", content: verifyPrompt.systemPrompt },
|
||||
{ role: "user", content: verifyPrompt.userPrompt },
|
||||
],
|
||||
{
|
||||
adapter: llmMode,
|
||||
jsonSchema: llmJsonPut,
|
||||
verbose,
|
||||
},
|
||||
);
|
||||
|
||||
let result: VerifyResult | undefined;
|
||||
for await (const event of stream) {
|
||||
if (event.type === "result") {
|
||||
result = event.response.structuredOutput as VerifyResult | undefined;
|
||||
} else if (event.type === "error") {
|
||||
console.error(` ❌ 验证失败: ${event.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const updatedIssue: ReviewIssue = {
|
||||
...issue,
|
||||
valid: result.valid ? TRUE : FALSE,
|
||||
};
|
||||
|
||||
if (result.fixed) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ✅ 已修复: ${result.reason}`);
|
||||
}
|
||||
updatedIssue.fixed = new Date().toISOString();
|
||||
} else if (!result.valid) {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ❌ 无效问题: ${result.reason}`);
|
||||
}
|
||||
} else {
|
||||
if (shouldLog(verbose, 1)) {
|
||||
console.log(` ⚠️ 未修复: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedIssue;
|
||||
} else {
|
||||
return issue;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(` ❌ 验证出错: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.error(` 堆栈信息:\n${error.stack}`);
|
||||
}
|
||||
} else {
|
||||
console.error(` ❌ 验证出错: ${String(error)}`);
|
||||
}
|
||||
return issue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建验证单个 issue 是否已修复的 prompt
|
||||
*/
|
||||
protected buildVerifyPrompt(
|
||||
issue: ReviewIssue,
|
||||
fileContent: FileContentLine[],
|
||||
ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null,
|
||||
): { systemPrompt: string; userPrompt: string } {
|
||||
const padWidth = String(fileContent.length).length;
|
||||
const linesWithNumbers = fileContent
|
||||
.map(([, line], index) => `${String(index + 1).padStart(padWidth)}| ${line}`)
|
||||
.join("\n");
|
||||
|
||||
const systemPrompt = `你是一个代码审查专家。你的任务是判断之前发现的一个代码问题:
|
||||
1. 是否有效(是否真的违反了规则)
|
||||
2. 是否已经被修复
|
||||
|
||||
请仔细分析当前的代码内容。
|
||||
|
||||
## 输出要求
|
||||
- valid: 布尔值,true 表示问题有效(代码确实违反了规则),false 表示问题无效(误报)
|
||||
- fixed: 布尔值,true 表示问题已经被修复,false 表示问题仍然存在
|
||||
- reason: 判断依据
|
||||
|
||||
## 判断标准
|
||||
|
||||
### valid 判断
|
||||
- 根据规则 ID 和问题描述,判断代码是否真的违反了该规则
|
||||
- 如果问题描述与实际代码不符,valid 为 false
|
||||
- 如果规则不适用于该代码场景,valid 为 false
|
||||
|
||||
### fixed 判断
|
||||
- 只有当问题所在的代码已被修改,且修改后的代码不再违反规则时,fixed 才为 true
|
||||
- 如果问题所在的代码仍然存在且仍违反规则,fixed 必须为 false
|
||||
- 如果代码行号发生变化但问题本质仍存在,fixed 必须为 false
|
||||
|
||||
## 重要提醒
|
||||
- valid=false 时,fixed 的值无意义(无效问题无需修复)
|
||||
- 请确保 valid 和 fixed 的值与 reason 的描述一致!`;
|
||||
|
||||
// 构建规则定义部分
|
||||
let ruleSection = "";
|
||||
if (ruleInfo) {
|
||||
ruleSection = this.reviewSpecService.buildSpecsSection([ruleInfo.spec]);
|
||||
}
|
||||
|
||||
const userPrompt = `## 规则定义
|
||||
|
||||
${ruleSection}
|
||||
|
||||
## 之前发现的问题
|
||||
|
||||
- **文件**: ${issue.file}
|
||||
- **行号**: ${issue.line}
|
||||
- **规则**: ${issue.ruleId} (来自 ${issue.specFile})
|
||||
- **问题描述**: ${issue.reason}
|
||||
${issue.suggestion ? `- **原建议**: ${issue.suggestion}` : ""}
|
||||
|
||||
## 当前文件内容
|
||||
|
||||
\`\`\`
|
||||
${linesWithNumbers}
|
||||
\`\`\`
|
||||
|
||||
请判断这个问题是否有效,以及是否已经被修复。`;
|
||||
|
||||
return { systemPrompt, userPrompt };
|
||||
}
|
||||
}
|
||||
31
commands/review/src/locales/en/review.json
Normal file
31
commands/review/src/locales/en/review.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"description": "Code review command using LLM for automated PR review",
|
||||
"options.dryRun": "Only print actions without posting comments",
|
||||
"options.prNumber": "PR number, auto-detected from env if not specified",
|
||||
"options.base": "Base branch/tag for diff comparison",
|
||||
"options.head": "Head branch/tag for diff comparison",
|
||||
"options.includes": "File glob patterns to review, e.g. *.ts *.js (can be specified multiple times)",
|
||||
"options.llmMode": "LLM mode: claude-code, openai, gemini",
|
||||
"options.files": "Only review specified files (space-separated)",
|
||||
"options.commits": "Only review specified commits (space-separated)",
|
||||
"options.verifyFixes": "Verify if historical issues are fixed (default from config)",
|
||||
"options.noVerifyFixes": "Disable historical issue verification",
|
||||
"options.verifyConcurrency": "Concurrency for verifying historical issues (default 10)",
|
||||
"options.analyzeDeletions": "Analyze impact of deleted code (true, false, ci, pr, terminal)",
|
||||
"options.deletionAnalysisMode": "Deletion analysis mode: openai (standard) or claude-code (Agent mode with tools)",
|
||||
"options.deletionOnly": "Only run deletion analysis, skip regular code review",
|
||||
"options.outputFormat": "Output format: markdown, terminal, json. Auto-selected if not specified (markdown for PR, terminal for CLI)",
|
||||
"options.generateDescription": "Generate PR description using AI",
|
||||
"options.showAll": "Show all issues found, including those on unchanged lines",
|
||||
"options.eventAction": "PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review",
|
||||
"extensionDescription": "Code review command using LLM for automated PR review",
|
||||
"mcp.serverDescription": "Code review rules query service",
|
||||
"mcp.listRules": "List all code review rules for the current project, returning rule list with ID, title, description, applicable file extensions, etc.",
|
||||
"mcp.getRulesForFile": "Get applicable code review rules for a specific file, filtered by file extension and includes configuration",
|
||||
"mcp.getRuleDetail": "Get full details of a specific rule, including description, example code, etc.",
|
||||
"mcp.ruleNotFound": "Rule {{ruleId}} not found",
|
||||
"mcp.dto.cwd": "Project root directory path, defaults to current working directory",
|
||||
"mcp.dto.filePath": "File path, can be relative or absolute",
|
||||
"mcp.dto.includeExamples": "Whether to include rule example code, defaults to false",
|
||||
"mcp.dto.ruleId": "Rule ID, e.g. JsTs.Naming.FileName"
|
||||
}
|
||||
11
commands/review/src/locales/index.ts
Normal file
11
commands/review/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { addLocaleResources } from "@spaceflow/core";
|
||||
import zhCN from "./zh-cn/review.json";
|
||||
import en from "./en/review.json";
|
||||
|
||||
/** review 命令 i18n 资源 */
|
||||
export const reviewLocales: Record<string, Record<string, string>> = {
|
||||
"zh-CN": zhCN,
|
||||
en,
|
||||
};
|
||||
|
||||
addLocaleResources("review", reviewLocales);
|
||||
31
commands/review/src/locales/zh-cn/review.json
Normal file
31
commands/review/src/locales/zh-cn/review.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"description": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
|
||||
"options.dryRun": "仅打印将要执行的操作,不实际提交评论",
|
||||
"options.prNumber": "PR 编号,如果不指定则从环境变量获取",
|
||||
"options.base": "基准分支/tag,用于比较差异",
|
||||
"options.head": "目标分支/tag,用于比较差异",
|
||||
"options.includes": "要审查的文件 glob 模式,如 *.ts *.js(可多次指定)",
|
||||
"options.llmMode": "使用的 LLM 模式: claude-code, openai, gemini",
|
||||
"options.files": "仅审查指定的文件(空格分隔)",
|
||||
"options.commits": "仅审查指定的 commits(空格分隔)",
|
||||
"options.verifyFixes": "是否验证历史问题是否已修复(默认从配置文件读取)",
|
||||
"options.noVerifyFixes": "禁用历史问题验证",
|
||||
"options.verifyConcurrency": "验证历史问题的并发数(默认 10)",
|
||||
"options.analyzeDeletions": "分析删除代码可能带来的影响 (true, false, ci, pr, terminal)",
|
||||
"options.deletionAnalysisMode": "删除代码分析模式: openai (标准模式) 或 claude-code (Agent 模式,可使用工具)",
|
||||
"options.deletionOnly": "仅执行删除代码分析,跳过常规代码审查",
|
||||
"options.outputFormat": "输出格式: markdown, terminal, json。不指定则智能选择(PR 用 markdown,终端用 terminal)",
|
||||
"options.generateDescription": "使用 AI 生成 PR 功能描述",
|
||||
"options.showAll": "显示所有发现的问题,不过滤非变更行的问题",
|
||||
"options.eventAction": "PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查",
|
||||
"extensionDescription": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
|
||||
"mcp.serverDescription": "代码审查规则查询服务",
|
||||
"mcp.listRules": "获取当前项目的所有代码审查规则,返回规则列表包含 ID、标题、描述、适用的文件扩展名等信息",
|
||||
"mcp.getRulesForFile": "获取某个文件应该使用的代码审查规则,根据文件扩展名和 includes 配置过滤",
|
||||
"mcp.getRuleDetail": "获取某个规则的完整详情,包括描述、示例代码等",
|
||||
"mcp.ruleNotFound": "规则 {{ruleId}} 不存在",
|
||||
"mcp.dto.cwd": "项目根目录路径,默认为当前工作目录",
|
||||
"mcp.dto.filePath": "文件路径,可以是相对路径或绝对路径",
|
||||
"mcp.dto.includeExamples": "是否包含规则示例代码,默认 false",
|
||||
"mcp.dto.ruleId": "规则 ID,如 JsTs.Naming.FileName"
|
||||
}
|
||||
251
commands/review/src/parse-title-options.spec.ts
Normal file
251
commands/review/src/parse-title-options.spec.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { parseTitleOptions } from "./parse-title-options";
|
||||
|
||||
describe("parseTitleOptions", () => {
|
||||
describe("基本解析", () => {
|
||||
it("应该从 PR 标题末尾解析命令参数 (/review)", () => {
|
||||
const title = "feat: 添加新功能 [/review -l openai -v 2]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("openai");
|
||||
expect(options.verbose).toBe(2);
|
||||
});
|
||||
|
||||
it("应该支持旧的 /ai-review 格式", () => {
|
||||
const title = "feat: 添加新功能 [/ai-review -l openai -v 2]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("openai");
|
||||
expect(options.verbose).toBe(2);
|
||||
});
|
||||
|
||||
it("没有命令参数时应返回空对象", () => {
|
||||
const title = "feat: 添加新功能";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options).toEqual({});
|
||||
});
|
||||
|
||||
it("格式不正确时应返回空对象", () => {
|
||||
const title = "feat: 添加新功能 [ai-review -l openai]"; // 缺少 /
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("LLM 模式参数", () => {
|
||||
it("应该解析 -l 短参数", () => {
|
||||
const title = "fix: bug [/ai-review -l claude-code]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("claude-code");
|
||||
});
|
||||
|
||||
it("应该解析 --llm-mode 长参数", () => {
|
||||
const title = "fix: bug [/ai-review --llm-mode gemini]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("gemini");
|
||||
});
|
||||
|
||||
it("无效的 LLM 模式应被忽略", () => {
|
||||
const title = "fix: bug [/ai-review -l invalid-mode]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("详细输出级别参数", () => {
|
||||
it("应该解析 -v 1", () => {
|
||||
const title = "fix: bug [/ai-review -v 1]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verbose).toBe(1);
|
||||
});
|
||||
|
||||
it("应该解析 -v 2", () => {
|
||||
const title = "fix: bug [/ai-review -v 2]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verbose).toBe(2);
|
||||
});
|
||||
|
||||
it("-v 不带值时应默认为 1", () => {
|
||||
const title = "fix: bug [/ai-review -v]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verbose).toBe(1);
|
||||
});
|
||||
|
||||
it("-v 后跟其他参数时应默认为 1", () => {
|
||||
const title = "fix: bug [/ai-review -v -l openai]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verbose).toBe(1);
|
||||
expect(options.llmMode).toBe("openai");
|
||||
});
|
||||
|
||||
it("应该解析 --verbose 长参数", () => {
|
||||
const title = "fix: bug [/ai-review --verbose 2]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verbose).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dry-run 参数", () => {
|
||||
it("应该解析 -d 短参数", () => {
|
||||
const title = "fix: bug [/ai-review -d]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.dryRun).toBe(true);
|
||||
});
|
||||
|
||||
it("应该解析 --dry-run 长参数", () => {
|
||||
const title = "fix: bug [/ai-review --dry-run]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.dryRun).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("includes 参数", () => {
|
||||
it("应该解析 -i 短参数", () => {
|
||||
const title = "fix: bug [/ai-review -i *.ts]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.includes).toEqual(["*.ts"]);
|
||||
});
|
||||
|
||||
it("应该解析多个 includes", () => {
|
||||
const title = "fix: bug [/ai-review -i *.ts -i *.js]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.includes).toEqual(["*.ts", "*.js"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verify-fixes 参数", () => {
|
||||
it("应该解析 --verify-fixes", () => {
|
||||
const title = "fix: bug [/ai-review --verify-fixes]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verifyFixes).toBe(true);
|
||||
});
|
||||
|
||||
it("应该解析 --no-verify-fixes", () => {
|
||||
const title = "fix: bug [/ai-review --no-verify-fixes]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.verifyFixes).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("删除代码分析参数", () => {
|
||||
it("应该解析 --analyze-deletions 无值时默认为 true", () => {
|
||||
const title = "fix: bug [/ai-review --analyze-deletions]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.analyzeDeletions).toBe(true);
|
||||
});
|
||||
|
||||
it("应该解析 --analyze-deletions true", () => {
|
||||
const title = "fix: bug [/ai-review --analyze-deletions true]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.analyzeDeletions).toBe(true);
|
||||
});
|
||||
|
||||
it("应该解析 --analyze-deletions false", () => {
|
||||
const title = "fix: bug [/ai-review --analyze-deletions false]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.analyzeDeletions).toBe(false);
|
||||
});
|
||||
|
||||
it("应该解析 --analyze-deletions ci", () => {
|
||||
const title = "fix: bug [/ai-review --analyze-deletions ci]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.analyzeDeletions).toBe("ci");
|
||||
});
|
||||
|
||||
it("应该解析 --analyze-deletions pr", () => {
|
||||
const title = "fix: bug [/ai-review --analyze-deletions pr]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.analyzeDeletions).toBe("pr");
|
||||
});
|
||||
|
||||
it("应该解析 --analyze-deletions terminal", () => {
|
||||
const title = "fix: bug [/ai-review --analyze-deletions terminal]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.analyzeDeletions).toBe("terminal");
|
||||
});
|
||||
|
||||
it("应该解析 --deletion-only", () => {
|
||||
const title = "fix: bug [/ai-review --deletion-only]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.deletionOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("应该解析 --deletion-analysis-mode", () => {
|
||||
const title = "fix: bug [/ai-review --deletion-analysis-mode claude-code]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.deletionAnalysisMode).toBe("claude-code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("组合参数", () => {
|
||||
it("应该正确解析多个参数组合", () => {
|
||||
const title = "feat: 新功能 [/ai-review -l openai -v 2 -d --no-verify-fixes]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("openai");
|
||||
expect(options.verbose).toBe(2);
|
||||
expect(options.dryRun).toBe(true);
|
||||
expect(options.verifyFixes).toBe(false);
|
||||
});
|
||||
|
||||
it("应该处理带引号的参数值", () => {
|
||||
const title = 'fix: bug [/ai-review -i "src/**/*.ts"]';
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.includes).toEqual(["src/**/*.ts"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("大小写不敏感", () => {
|
||||
it("命令名称应该大小写不敏感", () => {
|
||||
const title = "fix: bug [/AI-REVIEW -l openai]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("openai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("边界情况", () => {
|
||||
it("空标题应返回空对象", () => {
|
||||
const options = parseTitleOptions("");
|
||||
expect(options).toEqual({});
|
||||
});
|
||||
|
||||
it("命令在标题中间也应该被解析", () => {
|
||||
const title = "feat: [/review -l openai] 替换 [/ai-review -l openai] 添加新功能";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("openai");
|
||||
});
|
||||
|
||||
it("多个命令只解析第一个", () => {
|
||||
const title = "feat: [/review -l openai] 替换 [/ai-review -l openai] [/ai-review -l gemini]";
|
||||
const options = parseTitleOptions(title);
|
||||
|
||||
expect(options.llmMode).toBe("openai");
|
||||
});
|
||||
});
|
||||
});
|
||||
185
commands/review/src/parse-title-options.ts
Normal file
185
commands/review/src/parse-title-options.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { LLMMode, VerboseLevel } from "@spaceflow/core";
|
||||
import type { AnalyzeDeletionsMode } from "./review.config";
|
||||
import { normalizeVerbose } from "@spaceflow/core";
|
||||
|
||||
/**
|
||||
* 从 PR 标题中解析的命令参数
|
||||
*/
|
||||
export interface TitleOptions {
|
||||
llmMode?: LLMMode;
|
||||
verbose?: VerboseLevel;
|
||||
dryRun?: boolean;
|
||||
includes?: string[];
|
||||
verifyFixes?: boolean;
|
||||
analyzeDeletions?: AnalyzeDeletionsMode;
|
||||
deletionOnly?: boolean;
|
||||
deletionAnalysisMode?: LLMMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PR 标题中解析命令参数
|
||||
*
|
||||
* 支持的格式:标题末尾 [/review -l openai -v 2]
|
||||
*
|
||||
* 支持的参数:
|
||||
* - `-l, --llm-mode <mode>`: LLM 模式 (claude-code, openai, gemini)
|
||||
* - `-v, --verbose [level]`: 详细输出级别 (1 或 2)
|
||||
* - `-d, --dry-run`: 仅打印不执行
|
||||
* - `-i, --includes <pattern>`: 文件过滤模式
|
||||
* - `--verify-fixes`: 验证历史问题
|
||||
* - `--no-verify-fixes`: 禁用历史问题验证
|
||||
* - `--analyze-deletions`: 分析删除代码
|
||||
* - `--deletion-only`: 仅执行删除代码分析
|
||||
* - `--deletion-analysis-mode <mode>`: 删除分析模式
|
||||
*
|
||||
* @param title PR 标题
|
||||
* @returns 解析出的命令参数,如果没有找到命令则返回空对象
|
||||
*/
|
||||
export function parseTitleOptions(title: string): TitleOptions {
|
||||
const options: TitleOptions = {};
|
||||
|
||||
// 匹配 [/review ...] 或 [/ai-review ...] (保持向后兼容) 格式
|
||||
const match = title.match(/\[\/(review|ai-review)\s+([^\]]+)\]/i);
|
||||
if (!match) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const argsString = match[2].trim();
|
||||
const args = parseArgs(argsString);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
switch (arg) {
|
||||
case "-l":
|
||||
case "--llm-mode": {
|
||||
const value = args[++i];
|
||||
if (value && isValidLLMMode(value)) {
|
||||
options.llmMode = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "-v":
|
||||
case "--verbose": {
|
||||
const nextArg = args[i + 1];
|
||||
if (nextArg && !nextArg.startsWith("-")) {
|
||||
const level = parseInt(nextArg, 10);
|
||||
if (level === 1 || level === 2) {
|
||||
options.verbose = level;
|
||||
i++;
|
||||
} else {
|
||||
options.verbose = normalizeVerbose(1);
|
||||
}
|
||||
} else {
|
||||
options.verbose = normalizeVerbose(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "-d":
|
||||
case "--dry-run":
|
||||
options.dryRun = true;
|
||||
break;
|
||||
|
||||
case "-i":
|
||||
case "--includes": {
|
||||
const value = args[++i];
|
||||
if (value && !value.startsWith("-")) {
|
||||
if (!options.includes) {
|
||||
options.includes = [];
|
||||
}
|
||||
options.includes.push(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "--verify-fixes":
|
||||
options.verifyFixes = true;
|
||||
break;
|
||||
|
||||
case "--no-verify-fixes":
|
||||
options.verifyFixes = false;
|
||||
break;
|
||||
|
||||
case "--analyze-deletions": {
|
||||
const nextArg = args[i + 1];
|
||||
if (nextArg && !nextArg.startsWith("-") && isValidAnalyzeDeletionsMode(nextArg)) {
|
||||
options.analyzeDeletions = parseAnalyzeDeletionsValue(nextArg);
|
||||
i++;
|
||||
} else {
|
||||
options.analyzeDeletions = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "--deletion-only":
|
||||
options.deletionOnly = true;
|
||||
break;
|
||||
|
||||
case "--deletion-analysis-mode": {
|
||||
const value = args[++i];
|
||||
if (value && isValidDeletionAnalysisMode(value)) {
|
||||
options.deletionAnalysisMode = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析参数字符串为数组
|
||||
* 支持引号包裹的参数值
|
||||
*/
|
||||
function parseArgs(argsString: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = "";
|
||||
let inQuote = false;
|
||||
let quoteChar = "";
|
||||
|
||||
for (let i = 0; i < argsString.length; i++) {
|
||||
const char = argsString[i];
|
||||
|
||||
if ((char === '"' || char === "'") && !inQuote) {
|
||||
inQuote = true;
|
||||
quoteChar = char;
|
||||
} else if (char === quoteChar && inQuote) {
|
||||
inQuote = false;
|
||||
quoteChar = "";
|
||||
} else if (char === " " && !inQuote) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function isValidLLMMode(value: string): value is LLMMode {
|
||||
return ["claude-code", "openai", "gemini"].includes(value);
|
||||
}
|
||||
|
||||
function isValidDeletionAnalysisMode(value: string): value is LLMMode {
|
||||
return ["openai", "claude-code"].includes(value);
|
||||
}
|
||||
|
||||
function isValidAnalyzeDeletionsMode(value: string): boolean {
|
||||
return ["true", "false", "ci", "pr", "terminal"].includes(value);
|
||||
}
|
||||
|
||||
function parseAnalyzeDeletionsValue(value: string): AnalyzeDeletionsMode {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
return value as "ci" | "pr" | "terminal";
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { DeletionImpactResult, DeletionImpact } from "../../review-spec/types";
|
||||
|
||||
const RISK_EMOJI: Record<string, string> = {
|
||||
high: "🔴",
|
||||
medium: "🟡",
|
||||
low: "🟢",
|
||||
none: "⚪",
|
||||
};
|
||||
|
||||
const RISK_LABEL: Record<string, string> = {
|
||||
high: "高风险",
|
||||
medium: "中风险",
|
||||
low: "低风险",
|
||||
none: "无风险",
|
||||
};
|
||||
|
||||
export interface DeletionImpactReportOptions {
|
||||
includeJsonData?: boolean;
|
||||
}
|
||||
|
||||
const DELETION_IMPACT_DATA_START = "<!-- spaceflow-deletion-impact-data-start -->";
|
||||
const DELETION_IMPACT_DATA_END = "<!-- spaceflow-deletion-impact-data-end -->";
|
||||
|
||||
export class DeletionImpactFormatter {
|
||||
format(result: DeletionImpactResult, options: DeletionImpactReportOptions = {}): string {
|
||||
const { includeJsonData = true } = options;
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("## 🗑️ 删除代码影响分析\n");
|
||||
|
||||
// 防御性检查:确保 impacts 是数组
|
||||
const impacts = result.impacts && Array.isArray(result.impacts) ? result.impacts : [];
|
||||
|
||||
if (impacts.length === 0) {
|
||||
lines.push("✅ **未发现有风险的代码删除**\n");
|
||||
lines.push(result.summary);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// 统计风险等级
|
||||
const highRisk = impacts.filter((i) => i.riskLevel === "high");
|
||||
const mediumRisk = impacts.filter((i) => i.riskLevel === "medium");
|
||||
const lowRisk = impacts.filter((i) => i.riskLevel === "low");
|
||||
|
||||
lines.push("### 📊 风险概览\n");
|
||||
lines.push(`| 风险等级 | 数量 |`);
|
||||
lines.push(`|----------|------|`);
|
||||
if (highRisk.length > 0) {
|
||||
lines.push(`| ${RISK_EMOJI.high} 高风险 | ${highRisk.length} |`);
|
||||
}
|
||||
if (mediumRisk.length > 0) {
|
||||
lines.push(`| ${RISK_EMOJI.medium} 中风险 | ${mediumRisk.length} |`);
|
||||
}
|
||||
if (lowRisk.length > 0) {
|
||||
lines.push(`| ${RISK_EMOJI.low} 低风险 | ${lowRisk.length} |`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
// 详情折叠
|
||||
lines.push("<details>");
|
||||
lines.push("<summary>📋 点击查看详情</summary>\n");
|
||||
|
||||
// 高风险项详情
|
||||
if (highRisk.length > 0) {
|
||||
lines.push("### 🔴 高风险删除\n");
|
||||
lines.push(this.formatImpactList(highRisk));
|
||||
}
|
||||
|
||||
// 中风险项详情
|
||||
if (mediumRisk.length > 0) {
|
||||
lines.push("### 🟡 中风险删除\n");
|
||||
lines.push(this.formatImpactList(mediumRisk));
|
||||
}
|
||||
|
||||
// 低风险项
|
||||
if (lowRisk.length > 0) {
|
||||
lines.push("### 🟢 低风险删除\n");
|
||||
lines.push(this.formatImpactList(lowRisk));
|
||||
}
|
||||
|
||||
// 总结
|
||||
lines.push("\n### 📝 总结\n");
|
||||
lines.push(result.summary);
|
||||
|
||||
lines.push("\n</details>");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
private formatImpactList(impacts: DeletionImpact[]): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const impact of impacts) {
|
||||
const emoji = RISK_EMOJI[impact.riskLevel] || RISK_EMOJI.none;
|
||||
const label = RISK_LABEL[impact.riskLevel] || "未知";
|
||||
const codePreview =
|
||||
impact.deletedCode.length > 50
|
||||
? impact.deletedCode.slice(0, 50) + "..."
|
||||
: impact.deletedCode;
|
||||
|
||||
lines.push(`#### ${emoji} \`${impact.file}\`\n`);
|
||||
lines.push(`- **风险等级**: ${label}`);
|
||||
lines.push(`- **删除代码**: \`${codePreview.replace(/\n/g, " ")}\``);
|
||||
|
||||
if (impact.affectedFiles.length > 0) {
|
||||
lines.push(`- **受影响文件**:`);
|
||||
for (const file of impact.affectedFiles.slice(0, 5)) {
|
||||
lines.push(` - \`${file}\``);
|
||||
}
|
||||
if (impact.affectedFiles.length > 5) {
|
||||
lines.push(` - ... 还有 ${impact.affectedFiles.length - 5} 个文件`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`- **影响分析**: ${impact.reason}`);
|
||||
|
||||
if (impact.suggestion) {
|
||||
lines.push(`- **建议**: ${impact.suggestion}`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
parse(content: string): DeletionImpactResult | null {
|
||||
const startIndex = content.indexOf(DELETION_IMPACT_DATA_START);
|
||||
const endIndex = content.indexOf(DELETION_IMPACT_DATA_END);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const jsonStart = startIndex + DELETION_IMPACT_DATA_START.length;
|
||||
const jsonContent = content.slice(jsonStart, endIndex).trim();
|
||||
|
||||
try {
|
||||
return JSON.parse(jsonContent) as DeletionImpactResult;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
commands/review/src/review-report/formatters/index.ts
Normal file
4
commands/review/src/review-report/formatters/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./markdown.formatter";
|
||||
export * from "./json.formatter";
|
||||
export * from "./terminal.formatter";
|
||||
export * from "./deletion-impact.formatter";
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ReviewResult } from "../../review-spec/types";
|
||||
import { ReportOptions, ReviewReportFormatter } from "../types";
|
||||
|
||||
export class JsonFormatter implements ReviewReportFormatter {
|
||||
format(result: ReviewResult, _options: ReportOptions = {}): string {
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import { extname } from "path";
|
||||
import {
|
||||
FileSummary,
|
||||
ReviewIssue,
|
||||
ReviewResult,
|
||||
ReviewStats,
|
||||
SEVERITY_EMOJI,
|
||||
} from "../../review-spec/types";
|
||||
import { ParsedReport, ReportOptions, ReviewReportFormatter, ReviewReportParser } from "../types";
|
||||
import { DeletionImpactFormatter } from "./deletion-impact.formatter";
|
||||
|
||||
const REVIEW_DATA_START = "<!-- spaceflow-review-data-start -->";
|
||||
const REVIEW_DATA_END = "<!-- spaceflow-review-data-end -->";
|
||||
|
||||
function formatDateToUTC8(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
return date.toLocaleString("zh-CN", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportParser {
|
||||
static clearReviewData(content: string, replaceData: string): string {
|
||||
return content
|
||||
.replace(new RegExp(`${REVIEW_DATA_START}[\\s\\S]*?${REVIEW_DATA_END}`, "g"), replaceData)
|
||||
.trim();
|
||||
}
|
||||
private readonly deletionImpactFormatter = new DeletionImpactFormatter();
|
||||
|
||||
private formatIssueList(issues: ReviewIssue[]): string {
|
||||
const lines: string[] = [];
|
||||
for (const issue of issues) {
|
||||
const severityEmoji = SEVERITY_EMOJI[issue.severity] || SEVERITY_EMOJI.error;
|
||||
lines.push(`### ${issue.fixed ? "🟢" : severityEmoji} ${issue.file}:${issue.line}\n`);
|
||||
lines.push(`- **问题**: ${issue.reason}`);
|
||||
lines.push(`- **规则**: \`${issue.ruleId}\` (来自 \`${issue.specFile}\`)`);
|
||||
if (issue.commit) {
|
||||
lines.push(`- **Commit**: ${issue.commit}`);
|
||||
}
|
||||
lines.push(`- **开发人员**: ${issue.author ? "@" + issue.author.login : "未知"}`);
|
||||
if (issue.date) {
|
||||
lines.push(`- **发现时间**: ${formatDateToUTC8(issue.date)}`);
|
||||
}
|
||||
if (issue.fixed) {
|
||||
lines.push(`- **修复时间**: ${formatDateToUTC8(issue.fixed)}`);
|
||||
}
|
||||
if (issue.suggestion) {
|
||||
const ext = extname(issue.file).slice(1) || "";
|
||||
const cleanSuggestion = issue.suggestion.replace(/```/g, "//").trim();
|
||||
const lineCount = cleanSuggestion.split("\n").length;
|
||||
lines.push("- **建议**:");
|
||||
if (lineCount < 4) {
|
||||
lines.push(`\`\`\`${ext}`);
|
||||
lines.push(cleanSuggestion);
|
||||
lines.push("```");
|
||||
} else {
|
||||
lines.push("<details>");
|
||||
lines.push("<summary>💡 查看建议</summary>\n");
|
||||
lines.push(`\`\`\`${ext}`);
|
||||
lines.push(cleanSuggestion);
|
||||
lines.push("```");
|
||||
lines.push("\n</details>");
|
||||
}
|
||||
}
|
||||
// 渲染消息(reactions 统计 + replies 详情,合并到一个折叠块)
|
||||
const hasReactions = issue.reactions && issue.reactions.length > 0;
|
||||
const hasReplies = issue.replies && issue.replies.length > 0;
|
||||
if (hasReactions || hasReplies) {
|
||||
const reactionCount = hasReactions
|
||||
? issue.reactions!.reduce((sum, r) => sum + r.users.length, 0)
|
||||
: 0;
|
||||
const replyCount = hasReplies ? issue.replies!.length : 0;
|
||||
const totalCount = reactionCount + replyCount;
|
||||
lines.push("<details>");
|
||||
lines.push(`<summary>💬 消息 (${totalCount} 条)</summary>\n`);
|
||||
// 先显示 reactions 统计(格式:👎(1) 👍(2))
|
||||
if (hasReactions) {
|
||||
const reactionStats = issue.reactions!.map((r) => {
|
||||
const emoji = this.getReactionEmoji(r.content);
|
||||
return `${emoji}(${r.users.length})`;
|
||||
});
|
||||
lines.push(`> ${reactionStats.join(" ")}`);
|
||||
}
|
||||
// 再显示 replies 详情
|
||||
if (hasReplies) {
|
||||
for (const reply of issue.replies!) {
|
||||
const time = reply.createdAt ? formatDateToUTC8(reply.createdAt) : "";
|
||||
lines.push(`> @${reply.user.login} ${time ? `(${time})` : ""}:`);
|
||||
lines.push(`> ${reply.body.split("\n").join("\n> ")}`);
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
lines.push("</details>");
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** 将 reaction content 转换为 emoji */
|
||||
private getReactionEmoji(content: string): string {
|
||||
const emojiMap: Record<string, string> = {
|
||||
"+1": "👍",
|
||||
"-1": "👎",
|
||||
laugh: "😄",
|
||||
hooray: "🎉",
|
||||
confused: "😕",
|
||||
heart: "❤️",
|
||||
rocket: "🚀",
|
||||
eyes: "👀",
|
||||
};
|
||||
return emojiMap[content] || content;
|
||||
}
|
||||
|
||||
private formatFileSummaries(summaries: FileSummary[], issues: ReviewIssue[]): string {
|
||||
if (summaries.length === 0) {
|
||||
return "没有需要审查的文件";
|
||||
}
|
||||
|
||||
const issuesByFile = new Map<string, { resolved: number; unresolved: number }>();
|
||||
for (const issue of issues) {
|
||||
if (issue.valid === "false") continue;
|
||||
const stats = issuesByFile.get(issue.file) || { resolved: 0, unresolved: 0 };
|
||||
if (issue.fixed) {
|
||||
stats.resolved++;
|
||||
} else {
|
||||
stats.unresolved++;
|
||||
}
|
||||
issuesByFile.set(issue.file, stats);
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push("| 文件 | 🟢 | 🔴 | 总结 |");
|
||||
lines.push("|------|----|----|------|");
|
||||
|
||||
for (const fileSummary of summaries) {
|
||||
const stats = issuesByFile.get(fileSummary.file) || { resolved: 0, unresolved: 0 };
|
||||
const summaryText = fileSummary.summary
|
||||
.split("\n")
|
||||
.filter((line) => line.trim())
|
||||
.join("<br>");
|
||||
lines.push(
|
||||
`| \`${fileSummary.file}\` | ${stats.resolved} | ${stats.unresolved} | ${summaryText} |`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
parse(content: string): ParsedReport | null {
|
||||
const startIndex = content.indexOf(REVIEW_DATA_START);
|
||||
const endIndex = content.indexOf(REVIEW_DATA_END);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataStart = startIndex + REVIEW_DATA_START.length;
|
||||
const encodedContent = content.slice(dataStart, endIndex).trim();
|
||||
|
||||
try {
|
||||
// 尝试 Base64 解码,如果失败则尝试直接解析 JSON(兼容旧格式)
|
||||
let jsonContent: string;
|
||||
try {
|
||||
jsonContent = Buffer.from(encodedContent, "base64").toString("utf-8");
|
||||
// 验证解码后是否为有效 JSON
|
||||
JSON.parse(jsonContent);
|
||||
} catch {
|
||||
// Base64 解码失败或解码后不是有效 JSON,尝试直接解析(旧格式)
|
||||
jsonContent = encodedContent;
|
||||
}
|
||||
const parsed = JSON.parse(jsonContent);
|
||||
const hasReanalysisRequest = content.includes("- [x]");
|
||||
|
||||
// 新格式:完整的 ReviewResult
|
||||
return {
|
||||
result: parsed as ReviewResult,
|
||||
hasReanalysisRequest,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
format(result: ReviewResult, options: ReportOptions = {}): string {
|
||||
const {
|
||||
prNumber,
|
||||
includeReanalysisCheckbox = true,
|
||||
includeJsonData = true,
|
||||
reviewCommentMarker,
|
||||
} = options;
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
if (reviewCommentMarker) {
|
||||
lines.push(reviewCommentMarker);
|
||||
}
|
||||
|
||||
const validIssues = result.issues.filter((issue) => issue.valid !== "false");
|
||||
const invalidIssues = result.issues.filter((issue) => issue.valid === "false");
|
||||
|
||||
lines.push("# 🤖 AI 代码审查报告\n");
|
||||
|
||||
// 输出统计信息(如果有)
|
||||
if (result.stats) {
|
||||
lines.push(this.formatStats(result.stats));
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// 输出 PR 功能描述
|
||||
if (result.description) {
|
||||
lines.push("## 📋 功能概述\n");
|
||||
// 将 description 中的标题级别降低(h1->h3, h2->h4, h3->h5 等)
|
||||
const adjustedDescription = result.description.replace(/^(#{1,4})\s/gm, "$1## ");
|
||||
lines.push(adjustedDescription);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## 📝 新增代码分析\n");
|
||||
lines.push("### 📊 审查概览\n");
|
||||
lines.push(this.formatFileSummaries(result.summary, validIssues));
|
||||
lines.push("<details>");
|
||||
lines.push("<summary>📋 点击查看详情</summary>\n");
|
||||
lines.push("### 🐛 详细问题\n");
|
||||
if (validIssues.length === 0 && invalidIssues.length === 0) {
|
||||
lines.push("✅ **未发现问题**\n");
|
||||
} else {
|
||||
if (validIssues.length === 0) {
|
||||
lines.push("✅ **未发现有效问题**\n");
|
||||
} else {
|
||||
lines.push(`⚠️ **发现 ${validIssues.length} 个问题**\n`);
|
||||
lines.push(this.formatIssueList(validIssues));
|
||||
}
|
||||
|
||||
if (invalidIssues.length > 0) {
|
||||
lines.push("\n<details>");
|
||||
lines.push(`<summary>🚫 无效问题 (${invalidIssues.length} 个)</summary>\n`);
|
||||
lines.push(this.formatIssueList(invalidIssues));
|
||||
lines.push("\n</details>");
|
||||
}
|
||||
}
|
||||
lines.push("\n</details>");
|
||||
|
||||
// if (includeReanalysisCheckbox) {
|
||||
// lines.push("\n---");
|
||||
// lines.push("<details>");
|
||||
// lines.push("<summary>🔄 重新分析</summary>\n");
|
||||
// if (prNumber) {
|
||||
// lines.push("- [ ] 勾选此复选框后保存评论,将触发重新分析");
|
||||
// }
|
||||
// lines.push("\n</details>");
|
||||
// }
|
||||
|
||||
// 输出删除代码影响分析报告
|
||||
if (result.deletionImpact) {
|
||||
lines.push("\n---\n");
|
||||
lines.push(this.deletionImpactFormatter.format(result.deletionImpact, { includeJsonData }));
|
||||
}
|
||||
|
||||
if (includeJsonData) {
|
||||
lines.push("");
|
||||
lines.push("<details>");
|
||||
lines.push("<summary>📊 审查数据 (JSON)</summary>\n");
|
||||
lines.push(REVIEW_DATA_START);
|
||||
lines.push(Buffer.from(JSON.stringify(result)).toString("base64"));
|
||||
lines.push(REVIEW_DATA_END);
|
||||
lines.push("\n</details>");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
formatStats(stats: ReviewStats, prNumber?: number): string {
|
||||
const title = prNumber ? `PR #${prNumber} Review 状态统计` : "Review 状态统计";
|
||||
const lines = [`## 📊 ${title}\n`, `| 指标 | 数量 |`, `|------|------|`];
|
||||
lines.push(`| 总问题数 | ${stats.total} |`);
|
||||
lines.push(`| ✅ 已修复 | ${stats.fixed} |`);
|
||||
lines.push(`| ❌ 无效 | ${stats.invalid} |`);
|
||||
lines.push(`| ⚠️ 待处理 | ${stats.pending} |`);
|
||||
lines.push(`| 修复率 | ${stats.fixRate}% |`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { extname } from "path";
|
||||
import {
|
||||
FileSummary,
|
||||
ReviewIssue,
|
||||
ReviewResult,
|
||||
ReviewStats,
|
||||
Severity,
|
||||
} from "../../review-spec/types";
|
||||
import { ReportOptions, ReviewReportFormatter } from "../types";
|
||||
|
||||
const SEVERITY_COLORS: Record<Severity, string> = {
|
||||
off: "\x1b[90m",
|
||||
warn: "\x1b[33m",
|
||||
error: "\x1b[31m",
|
||||
};
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
const BOLD = "\x1b[1m";
|
||||
const DIM = "\x1b[2m";
|
||||
const CYAN = "\x1b[36m";
|
||||
const GREEN = "\x1b[32m";
|
||||
const YELLOW = "\x1b[33m";
|
||||
const RED = "\x1b[31m";
|
||||
|
||||
export class TerminalFormatter implements ReviewReportFormatter {
|
||||
private formatFileSummaries(summaries: FileSummary[], issues: ReviewIssue[]): string {
|
||||
if (summaries.length === 0) {
|
||||
return "没有需要审查的文件";
|
||||
}
|
||||
|
||||
const issuesByFile = new Map<string, { resolved: number; unresolved: number }>();
|
||||
for (const issue of issues) {
|
||||
const stats = issuesByFile.get(issue.file) || { resolved: 0, unresolved: 0 };
|
||||
if (issue.fixed) {
|
||||
stats.resolved++;
|
||||
} else {
|
||||
stats.unresolved++;
|
||||
}
|
||||
issuesByFile.set(issue.file, stats);
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const fileSummary of summaries) {
|
||||
const stats = issuesByFile.get(fileSummary.file) || { resolved: 0, unresolved: 0 };
|
||||
const resolvedText = stats.resolved > 0 ? `${GREEN}✅ ${stats.resolved} 已解决${RESET}` : "";
|
||||
const unresolvedText =
|
||||
stats.unresolved > 0 ? `${YELLOW}❌ ${stats.unresolved} 未解决${RESET}` : "";
|
||||
const statsText = [resolvedText, unresolvedText].filter(Boolean).join(" / ");
|
||||
|
||||
if (statsText) {
|
||||
lines.push(`${BOLD}${fileSummary.file}${RESET} (${statsText}): ${fileSummary.summary}`);
|
||||
} else {
|
||||
lines.push(`${BOLD}${fileSummary.file}${RESET}: ${fileSummary.summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
format(result: ReviewResult, _options: ReportOptions = {}): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("");
|
||||
lines.push(`${BOLD}${CYAN}═══════════════════════════════════════════════════════════${RESET}`);
|
||||
lines.push(
|
||||
`${BOLD}${CYAN} 🤖 AI 代码审查报告 ${RESET}`,
|
||||
);
|
||||
lines.push(`${BOLD}${CYAN}═══════════════════════════════════════════════════════════${RESET}`);
|
||||
lines.push("");
|
||||
|
||||
const issues = result.issues;
|
||||
|
||||
if (issues.length === 0) {
|
||||
lines.push(`${GREEN}✅ 未发现问题${RESET}`);
|
||||
lines.push("");
|
||||
lines.push(this.formatFileSummaries(result.summary, []));
|
||||
} else {
|
||||
lines.push(`${YELLOW}⚠️ 发现 ${issues.length} 个问题${RESET}`);
|
||||
lines.push("");
|
||||
|
||||
for (let i = 0; i < issues.length; i++) {
|
||||
const issue = issues[i];
|
||||
const color = SEVERITY_COLORS[issue.severity] || SEVERITY_COLORS.error;
|
||||
const severityLabel = issue.severity.toUpperCase();
|
||||
|
||||
lines.push(`${DIM}───────────────────────────────────────────────────────────${RESET}`);
|
||||
lines.push(`${BOLD}[${i + 1}/${issues.length}]${RESET} ${color}${severityLabel}${RESET}`);
|
||||
lines.push(`${BOLD}📍 位置:${RESET} ${issue.file}:${issue.line}`);
|
||||
lines.push(`${BOLD}📋 规则:${RESET} ${issue.ruleId} ${DIM}(${issue.specFile})${RESET}`);
|
||||
lines.push(`${BOLD}❓ 问题:${RESET} ${issue.reason}`);
|
||||
|
||||
if (issue.commit) {
|
||||
lines.push(`${BOLD}📝 Commit:${RESET} ${issue.commit}`);
|
||||
}
|
||||
|
||||
if (issue.suggestion) {
|
||||
const ext = extname(issue.file).slice(1) || "";
|
||||
lines.push(`${BOLD}💡 建议:${RESET}`);
|
||||
lines.push(`${DIM}--- ${ext} ---${RESET}`);
|
||||
lines.push(issue.suggestion);
|
||||
lines.push(`${DIM}------------${RESET}`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push(`${DIM}───────────────────────────────────────────────────────────${RESET}`);
|
||||
lines.push("");
|
||||
lines.push(`${BOLD}📝 总结${RESET}`);
|
||||
lines.push(this.formatFileSummaries(result.summary, issues));
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`${BOLD}${CYAN}═══════════════════════════════════════════════════════════${RESET}`);
|
||||
lines.push("");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
formatStats(stats: ReviewStats, prNumber?: number): string {
|
||||
const title = prNumber ? `PR #${prNumber} Review 状态统计` : "Review 状态统计";
|
||||
const lines = [`\n${BOLD}${CYAN}📊 ${title}:${RESET}`];
|
||||
lines.push(` 总问题数: ${stats.total}`);
|
||||
lines.push(` ${GREEN}✅ 已修复: ${stats.fixed}${RESET}`);
|
||||
lines.push(` ${RED}❌ 无效: ${stats.invalid}${RESET}`);
|
||||
lines.push(` ${YELLOW}⚠️ 待处理: ${stats.pending}${RESET}`);
|
||||
lines.push(` 修复率: ${stats.fixRate}%`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
4
commands/review/src/review-report/index.ts
Normal file
4
commands/review/src/review-report/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./types";
|
||||
export * from "./formatters";
|
||||
export * from "./review-report.service";
|
||||
export * from "./review-report.module";
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ReviewReportService } from "./review-report.service";
|
||||
|
||||
@Module({
|
||||
providers: [ReviewReportService],
|
||||
exports: [ReviewReportService],
|
||||
})
|
||||
export class ReviewReportModule {}
|
||||
58
commands/review/src/review-report/review-report.service.ts
Normal file
58
commands/review/src/review-report/review-report.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ReviewResult, ReviewStats } from "../review-spec/types";
|
||||
import { JsonFormatter, MarkdownFormatter, TerminalFormatter } from "./formatters";
|
||||
import { ParsedReport, ReportFormat, ReportOptions, ReviewReportFormatter } from "./types";
|
||||
|
||||
@Injectable()
|
||||
export class ReviewReportService {
|
||||
private readonly markdownFormatter = new MarkdownFormatter();
|
||||
private readonly terminalFormatter = new TerminalFormatter();
|
||||
|
||||
private formatters: Map<ReportFormat, ReviewReportFormatter> = new Map([
|
||||
["markdown", this.markdownFormatter],
|
||||
["json", new JsonFormatter()],
|
||||
["terminal", this.terminalFormatter],
|
||||
]);
|
||||
|
||||
format(result: ReviewResult, format: ReportFormat = "markdown", options?: ReportOptions): string {
|
||||
const formatter = this.formatters.get(format);
|
||||
if (!formatter) {
|
||||
throw new Error(`Unsupported format: ${format}`);
|
||||
}
|
||||
return formatter.format(result, options);
|
||||
}
|
||||
|
||||
formatMarkdown(result: ReviewResult, options?: ReportOptions): string {
|
||||
return this.format(result, "markdown", options);
|
||||
}
|
||||
|
||||
formatJson(result: ReviewResult, options?: ReportOptions): string {
|
||||
return this.format(result, "json", options);
|
||||
}
|
||||
|
||||
formatTerminal(result: ReviewResult, options?: ReportOptions): string {
|
||||
return this.format(result, "terminal", options);
|
||||
}
|
||||
|
||||
parseMarkdown(content: string): ParsedReport | null {
|
||||
return this.markdownFormatter.parse(content);
|
||||
}
|
||||
|
||||
registerFormatter(format: ReportFormat, formatter: ReviewReportFormatter): void {
|
||||
this.formatters.set(format, formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化统计信息为终端输出
|
||||
*/
|
||||
formatStatsTerminal(stats: ReviewStats, prNumber?: number): string {
|
||||
return this.terminalFormatter.formatStats(stats, prNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化统计信息为 Markdown
|
||||
*/
|
||||
formatStatsMarkdown(stats: ReviewStats, prNumber?: number): string {
|
||||
return this.markdownFormatter.formatStats(stats, prNumber);
|
||||
}
|
||||
}
|
||||
26
commands/review/src/review-report/types.ts
Normal file
26
commands/review/src/review-report/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ReviewIssue, ReviewResult, ReviewStats } from "../review-spec/types";
|
||||
|
||||
export type ReportFormat = "markdown" | "json" | "terminal";
|
||||
|
||||
export interface ReportOptions {
|
||||
prNumber?: number;
|
||||
includeReanalysisCheckbox?: boolean;
|
||||
includeJsonData?: boolean;
|
||||
reviewCommentMarker?: string;
|
||||
}
|
||||
|
||||
export interface ParsedReport {
|
||||
/** 完整的 ReviewResult 数据 */
|
||||
result: ReviewResult;
|
||||
/** 是否请求重新分析 */
|
||||
hasReanalysisRequest?: boolean;
|
||||
}
|
||||
|
||||
export interface ReviewReportFormatter {
|
||||
format(result: ReviewResult, options?: ReportOptions): string;
|
||||
formatStats?(stats: ReviewStats, prNumber?: number): string;
|
||||
}
|
||||
|
||||
export interface ReviewReportParser {
|
||||
parse(content: string): ParsedReport | null;
|
||||
}
|
||||
3
commands/review/src/review-spec/index.ts
Normal file
3
commands/review/src/review-spec/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./types";
|
||||
export * from "./review-spec.service";
|
||||
export * from "./review-spec.module";
|
||||
10
commands/review/src/review-spec/review-spec.module.ts
Normal file
10
commands/review/src/review-spec/review-spec.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { GitProviderModule } from "@spaceflow/core";
|
||||
import { ReviewSpecService } from "./review-spec.service";
|
||||
|
||||
@Module({
|
||||
imports: [GitProviderModule.forFeature()],
|
||||
providers: [ReviewSpecService],
|
||||
exports: [ReviewSpecService],
|
||||
})
|
||||
export class ReviewSpecModule {}
|
||||
1543
commands/review/src/review-spec/review-spec.service.spec.ts
Normal file
1543
commands/review/src/review-spec/review-spec.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
902
commands/review/src/review-spec/review-spec.service.ts
Normal file
902
commands/review/src/review-spec/review-spec.service.ts
Normal file
@@ -0,0 +1,902 @@
|
||||
import {
|
||||
Injectable,
|
||||
type ChangedFile,
|
||||
type VerboseLevel,
|
||||
shouldLog,
|
||||
GitProviderService,
|
||||
parseRepoUrl,
|
||||
type RemoteRepoRef,
|
||||
type RepositoryContent,
|
||||
} from "@spaceflow/core";
|
||||
import { Optional } from "@nestjs/common";
|
||||
import { readdir, readFile, mkdir, access, writeFile } from "fs/promises";
|
||||
import { join, basename, extname } from "path";
|
||||
import { homedir } from "os";
|
||||
import { execSync } from "child_process";
|
||||
import micromatch from "micromatch";
|
||||
import { ReviewSpec, ReviewRule, RuleExample, Severity } from "./types";
|
||||
|
||||
/** 远程规则缓存 TTL(毫秒),默认 5 分钟 */
|
||||
const REMOTE_SPEC_CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
@Injectable()
|
||||
export class ReviewSpecService {
|
||||
constructor(@Optional() protected readonly gitProvider?: GitProviderService) {}
|
||||
/**
|
||||
* 检查规则 ID 是否匹配(精确匹配或前缀匹配)
|
||||
* 例如: "JsTs.FileName" 匹配 "JsTs.FileName" 和 "JsTs.FileName.UpperCamel"
|
||||
*/
|
||||
protected matchRuleId(ruleId: string, pattern: string): boolean {
|
||||
if (!ruleId || !pattern) {
|
||||
console.warn(
|
||||
`matchRuleId: 参数为空 (ruleId=${JSON.stringify(ruleId)}, pattern=${JSON.stringify(pattern)})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return ruleId === pattern || ruleId.startsWith(pattern + ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中查找匹配的规则值(精确匹配优先,然后前缀匹配)
|
||||
*/
|
||||
protected findByRuleId<T>(ruleId: string, map: Map<string, T>): T | undefined {
|
||||
if (!ruleId) {
|
||||
console.warn(`findByRuleId: ruleId 为空 (ruleId=${JSON.stringify(ruleId)})`);
|
||||
return undefined;
|
||||
}
|
||||
// 精确匹配
|
||||
if (map.has(ruleId)) {
|
||||
return map.get(ruleId);
|
||||
}
|
||||
// 前缀匹配
|
||||
for (const [key, value] of map) {
|
||||
if (ruleId.startsWith(key + ".")) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* 根据变更文件的扩展名过滤适用的规则文件
|
||||
* 只按扩展名过滤,includes 和 override 在 LLM 审查后处理
|
||||
*/
|
||||
filterApplicableSpecs(specs: ReviewSpec[], changedFiles: { filename?: string }[]): ReviewSpec[] {
|
||||
const changedExtensions = new Set<string>();
|
||||
|
||||
for (const file of changedFiles) {
|
||||
if (file.filename) {
|
||||
const ext = extname(file.filename).slice(1).toLowerCase();
|
||||
if (ext) {
|
||||
changedExtensions.add(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return specs.filter((spec) => spec.extensions.some((ext) => changedExtensions.has(ext)));
|
||||
}
|
||||
|
||||
async loadReviewSpecs(specDir: string): Promise<ReviewSpec[]> {
|
||||
const specs: ReviewSpec[] = [];
|
||||
|
||||
try {
|
||||
const files = await readdir(specDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".md")) continue;
|
||||
|
||||
const content = await readFile(join(specDir, file), "utf-8");
|
||||
const spec = this.parseSpecFile(file, content);
|
||||
if (spec) {
|
||||
specs.push(spec);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 目录不存在时静默跳过(这些是可选的配置目录)
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
console.warn(`警告: 无法读取规则目录 ${specDir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return specs;
|
||||
}
|
||||
|
||||
async resolveSpecSources(sources: string[]): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
for (const source of sources) {
|
||||
// 优先尝试解析为远程仓库 URL(浏览器复制的链接)
|
||||
const repoRef = parseRepoUrl(source);
|
||||
if (repoRef && this.gitProvider) {
|
||||
const dir = await this.fetchRemoteSpecs(repoRef);
|
||||
if (dir) {
|
||||
dirs.push(dir);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (this.isRepoUrl(source)) {
|
||||
const dir = await this.cloneSpecRepo(source);
|
||||
if (dir) {
|
||||
dirs.push(dir);
|
||||
}
|
||||
} else {
|
||||
// 检查是否是 deps 目录,如果是则扫描子目录的 references
|
||||
const resolvedDirs = await this.resolveDepsDir(source);
|
||||
dirs.push(...resolvedDirs);
|
||||
}
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Git API 从远程仓库拉取规则文件
|
||||
* 缓存到 ~/.spaceflow/review-spec-cache/ 目录,带 TTL
|
||||
*/
|
||||
protected async fetchRemoteSpecs(ref: RemoteRepoRef): Promise<string | null> {
|
||||
const cacheKey = `${ref.owner}__${ref.repo}${ref.path ? `__${ref.path.replace(/\//g, "_")}` : ""}${ref.ref ? `@${ref.ref}` : ""}`;
|
||||
const cacheDir = join(homedir(), ".spaceflow", "review-spec-cache", cacheKey);
|
||||
// 检查缓存是否有效(非 CI 环境下使用 TTL)
|
||||
const isCI = !!process.env.CI;
|
||||
if (!isCI) {
|
||||
try {
|
||||
const timestampFile = join(cacheDir, ".timestamp");
|
||||
const timestamp = await readFile(timestampFile, "utf-8");
|
||||
const age = Date.now() - Number(timestamp);
|
||||
if (age < REMOTE_SPEC_CACHE_TTL) {
|
||||
const entries = await readdir(cacheDir);
|
||||
if (entries.some((f) => f.endsWith(".md"))) {
|
||||
return cacheDir;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 缓存不存在或无效,继续拉取
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log(
|
||||
` 📡 从远程仓库拉取规则: ${ref.owner}/${ref.repo}${ref.path ? `/${ref.path}` : ""}${ref.ref ? `@${ref.ref}` : ""}`,
|
||||
);
|
||||
const contents = await this.gitProvider!.listRepositoryContents(
|
||||
ref.owner,
|
||||
ref.repo,
|
||||
ref.path || undefined,
|
||||
ref.ref,
|
||||
);
|
||||
const mdFiles = contents.filter(
|
||||
(f: RepositoryContent) => f.type === "file" && f.name.endsWith(".md"),
|
||||
);
|
||||
if (mdFiles.length === 0) {
|
||||
console.warn(` ⚠️ 远程目录中未找到 .md 规则文件`);
|
||||
return null;
|
||||
}
|
||||
await mkdir(cacheDir, { recursive: true });
|
||||
for (const file of mdFiles) {
|
||||
const content = await this.gitProvider!.getFileContent(
|
||||
ref.owner,
|
||||
ref.repo,
|
||||
file.path,
|
||||
ref.ref,
|
||||
);
|
||||
await writeFile(join(cacheDir, file.name), content, "utf-8");
|
||||
}
|
||||
// 写入时间戳
|
||||
await writeFile(join(cacheDir, ".timestamp"), String(Date.now()), "utf-8");
|
||||
console.log(` ✅ 已拉取 ${mdFiles.length} 个规则文件到缓存`);
|
||||
return cacheDir;
|
||||
} catch (error) {
|
||||
console.warn(` ⚠️ 远程规则拉取失败:`, error instanceof Error ? error.message : error);
|
||||
// 尝试使用过期缓存
|
||||
try {
|
||||
const entries = await readdir(cacheDir);
|
||||
if (entries.some((f) => f.endsWith(".md"))) {
|
||||
console.log(` 📦 使用过期缓存`);
|
||||
return cacheDir;
|
||||
}
|
||||
} catch {
|
||||
// 无缓存可用
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 deps 目录,扫描子目录中的 references 文件夹
|
||||
* 如果目录本身包含 .md 文件则直接返回,否则扫描子目录
|
||||
*/
|
||||
protected async resolveDepsDir(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
// 检查目录本身是否包含 .md 文件
|
||||
const hasMdFiles = entries.some((e) => e.isFile() && e.name.endsWith(".md"));
|
||||
if (hasMdFiles) {
|
||||
dirs.push(dir);
|
||||
return dirs;
|
||||
}
|
||||
|
||||
// 扫描子目录
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDir = join(dir, entry.name);
|
||||
// 优先检查 references 子目录
|
||||
const referencesDir = join(subDir, "references");
|
||||
try {
|
||||
await access(referencesDir);
|
||||
dirs.push(referencesDir);
|
||||
} catch {
|
||||
// 没有 references 子目录,检查子目录本身是否有 .md 文件
|
||||
try {
|
||||
const subEntries = await readdir(subDir);
|
||||
if (subEntries.some((f) => f.endsWith(".md"))) {
|
||||
dirs.push(subDir);
|
||||
}
|
||||
} catch {
|
||||
// 忽略无法读取的子目录
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 目录不存在时静默跳过
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
protected isRepoUrl(source: string): boolean {
|
||||
return (
|
||||
source.startsWith("http://") ||
|
||||
source.startsWith("https://") ||
|
||||
source.startsWith("git@") ||
|
||||
source.includes("://")
|
||||
);
|
||||
}
|
||||
|
||||
protected async cloneSpecRepo(repoUrl: string): Promise<string | null> {
|
||||
const repoName = this.extractRepoName(repoUrl);
|
||||
if (!repoName) {
|
||||
console.warn(`警告: 无法解析仓库名称: ${repoUrl}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheDir = join(homedir(), ".spaceflow", "review-spec", repoName);
|
||||
|
||||
try {
|
||||
await access(cacheDir);
|
||||
// console.log(` 使用缓存的规则仓库: ${cacheDir}`);
|
||||
try {
|
||||
execSync("git pull --ff-only", { cwd: cacheDir, stdio: "pipe" });
|
||||
// console.log(` 已更新规则仓库`);
|
||||
} catch {
|
||||
console.warn(` 警告: 无法更新规则仓库,使用现有版本`);
|
||||
}
|
||||
return cacheDir;
|
||||
} catch {
|
||||
// console.log(` 克隆规则仓库: ${repoUrl}`);
|
||||
try {
|
||||
await mkdir(join(homedir(), ".spaceflow", "review-spec"), { recursive: true });
|
||||
execSync(`git clone --depth 1 "${repoUrl}" "${cacheDir}"`, { stdio: "pipe" });
|
||||
// console.log(` 克隆完成: ${cacheDir}`);
|
||||
return cacheDir;
|
||||
} catch (error) {
|
||||
console.warn(`警告: 无法克隆仓库 ${repoUrl}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected extractRepoName(repoUrl: string): string | null {
|
||||
let path = repoUrl;
|
||||
path = path.replace(/\.git$/, "");
|
||||
path = path.replace(/^git@[^:]+:/, "");
|
||||
path = path.replace(/^https?:\/\/[^/]+\//, "");
|
||||
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[parts.length - 2]}__${parts[parts.length - 1]}`;
|
||||
} else if (parts.length === 1) {
|
||||
return parts[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
parseSpecFile(filename: string, content: string): ReviewSpec | null {
|
||||
const nameWithoutExt = basename(filename, ".md");
|
||||
const parts = nameWithoutExt.split(".");
|
||||
|
||||
if (parts.length < 2) {
|
||||
console.warn(`警告: 规则文件名格式不正确: ${filename}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const extensionsPart = parts[0];
|
||||
const type = parts.slice(1).join(".");
|
||||
const extensions = extensionsPart.split("&").map((ext) => ext.toLowerCase());
|
||||
|
||||
const rules = this.extractRules(content);
|
||||
|
||||
// 文件级别的 override 来自第一个规则(标题规则)的 overrides
|
||||
const fileOverrides = rules.length > 0 ? rules[0].overrides : [];
|
||||
// 文件级别的 severity 来自第一个规则(标题规则)的 severity,默认为 error
|
||||
const fileSeverity = (rules.length > 0 ? rules[0].severity : undefined) || "error";
|
||||
// 文件级别的 includes 从内容中提取
|
||||
const fileIncludes = this.extractIncludes(content);
|
||||
|
||||
return {
|
||||
filename,
|
||||
extensions,
|
||||
type,
|
||||
content,
|
||||
rules,
|
||||
overrides: fileOverrides,
|
||||
severity: fileSeverity,
|
||||
includes: fileIncludes,
|
||||
};
|
||||
}
|
||||
|
||||
protected extractRules(content: string): ReviewRule[] {
|
||||
const rules: ReviewRule[] = [];
|
||||
const ruleRegex = /^(#{1,3})\s+(.+?)\s+`\[([^\]]+)\]`/gm;
|
||||
|
||||
const matches: { index: number; length: number; title: string; id: string }[] = [];
|
||||
let match;
|
||||
while ((match = ruleRegex.exec(content)) !== null) {
|
||||
matches.push({
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
title: match[2].trim(),
|
||||
id: match[3],
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const current = matches[i];
|
||||
const startIndex = current.index + current.length;
|
||||
const endIndex = i + 1 < matches.length ? matches[i + 1].index : content.length;
|
||||
|
||||
const ruleContent = content.slice(startIndex, endIndex).trim();
|
||||
const examples = this.extractExamples(ruleContent);
|
||||
const overrides = this.extractOverrides(ruleContent);
|
||||
|
||||
// 提取描述:在第一个例子之前的文本
|
||||
let description = ruleContent;
|
||||
const firstExampleIndex = ruleContent.search(/(?:^|\n)###\s+(?:good|bad)/i);
|
||||
if (firstExampleIndex !== -1) {
|
||||
description = ruleContent.slice(0, firstExampleIndex).trim();
|
||||
} else {
|
||||
// 如果没有例子,则整个 ruleContent 都是描述
|
||||
description = ruleContent;
|
||||
}
|
||||
|
||||
const severity = this.extractSeverity(ruleContent);
|
||||
const includes = this.extractConfigValues(ruleContent, "includes");
|
||||
|
||||
rules.push({
|
||||
id: current.id,
|
||||
title: current.title,
|
||||
description,
|
||||
examples,
|
||||
overrides,
|
||||
severity,
|
||||
includes: includes.length > 0 ? includes : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用配置解析方法
|
||||
* 格式: > - <configName> `value1` `value2` ...
|
||||
* 同名配置项后面的覆盖前面的
|
||||
*/
|
||||
protected extractConfigValues(content: string, configName: string): string[] {
|
||||
const configRegex = new RegExp(`^>\\s*-\\s*${configName}\\s+(.+)$`, "gm");
|
||||
let values: string[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = configRegex.exec(content)) !== null) {
|
||||
const valuesStr = match[1];
|
||||
const valueRegex = /`([^`]+)`/g;
|
||||
let valueMatch;
|
||||
const lineValues: string[] = [];
|
||||
while ((valueMatch = valueRegex.exec(valuesStr)) !== null) {
|
||||
lineValues.push(valueMatch[1]);
|
||||
}
|
||||
// 同名配置项覆盖
|
||||
values = lineValues;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
protected extractOverrides(content: string): string[] {
|
||||
// override 的值格式是 `[RuleId]`,需要去掉方括号
|
||||
return this.extractConfigValues(content, "override").map((v) =>
|
||||
v.startsWith("[") && v.endsWith("]") ? v.slice(1, -1) : v,
|
||||
);
|
||||
}
|
||||
|
||||
protected extractSeverity(content: string): Severity | undefined {
|
||||
const values = this.extractConfigValues(content, "severity");
|
||||
if (values.length > 0) {
|
||||
const value = values[values.length - 1];
|
||||
if (value === "off" || value === "warn" || value === "error") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected extractIncludes(content: string): string[] {
|
||||
// 只提取文件开头(第一个 ## 规则标题之前)的 includes 配置
|
||||
// 避免规则级的 includes 覆盖文件级的 includes
|
||||
const firstRuleIndex = content.indexOf("\n## ");
|
||||
const headerContent = firstRuleIndex > 0 ? content.slice(0, firstRuleIndex) : content;
|
||||
return this.extractConfigValues(headerContent, "includes");
|
||||
}
|
||||
|
||||
protected extractExamples(content: string): RuleExample[] {
|
||||
const examples: RuleExample[] = [];
|
||||
const sections = content.split(/(?:^|\n)###\s+/);
|
||||
|
||||
for (const section of sections) {
|
||||
const trimmedSection = section.trim();
|
||||
if (!trimmedSection) continue;
|
||||
|
||||
let type: "good" | "bad" | null = null;
|
||||
if (/^good\b/i.test(trimmedSection)) {
|
||||
type = "good";
|
||||
} else if (/^bad\b/i.test(trimmedSection)) {
|
||||
type = "bad";
|
||||
}
|
||||
|
||||
if (!type) continue;
|
||||
|
||||
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
||||
let codeMatch;
|
||||
while ((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null) {
|
||||
const lang = codeMatch[1] || "text";
|
||||
const code = codeMatch[2].trim();
|
||||
examples.push({ lang, code, type });
|
||||
}
|
||||
}
|
||||
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有 override 声明并排除被覆盖的规则
|
||||
* override 使用前缀匹配:如果规则 ID 以 override 值开头,则被排除
|
||||
*/
|
||||
applyOverrides(specs: ReviewSpec[], verbose?: VerboseLevel): ReviewSpec[] {
|
||||
// 收集所有 override 声明(文件级别 + 规则级别)
|
||||
const allOverrides: string[] = [];
|
||||
for (const spec of specs) {
|
||||
allOverrides.push(...spec.overrides);
|
||||
for (const rule of spec.rules) {
|
||||
allOverrides.push(...rule.overrides);
|
||||
}
|
||||
}
|
||||
|
||||
if (allOverrides.length === 0) {
|
||||
return specs;
|
||||
}
|
||||
|
||||
// 过滤规则:排除 ID 以任意 override 值开头的规则
|
||||
return specs
|
||||
.map((spec) => ({
|
||||
...spec,
|
||||
rules: spec.rules.filter((rule) => {
|
||||
const isOverridden = allOverrides.some((override) => this.matchRuleId(rule.id, override));
|
||||
if (isOverridden && shouldLog(verbose, 2)) {
|
||||
console.error(` 规则 [${rule.id}] 被 override 排除`);
|
||||
}
|
||||
return !isOverridden;
|
||||
}),
|
||||
}))
|
||||
.filter((spec) => spec.rules.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 spec 的 includes 配置过滤 issues
|
||||
* 只保留文件名匹配对应 spec includes 模式的 issues
|
||||
* 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
|
||||
*/
|
||||
filterIssuesByIncludes<T extends { file: string; ruleId: string }>(
|
||||
issues: T[],
|
||||
specs: ReviewSpec[],
|
||||
): T[] {
|
||||
// 构建 spec filename -> includes 的映射
|
||||
const specIncludesMap = new Map<string, string[]>();
|
||||
for (const spec of specs) {
|
||||
// 从规则 ID 前缀推断 spec filename
|
||||
for (const rule of spec.rules) {
|
||||
specIncludesMap.set(rule.id, spec.includes);
|
||||
}
|
||||
}
|
||||
|
||||
return issues.filter((issue) => {
|
||||
// 找到该 issue 对应的 spec includes
|
||||
const includes = this.findByRuleId(issue.ruleId, specIncludesMap) ?? [];
|
||||
|
||||
// 如果没有 includes 配置,保留该 issue
|
||||
if (includes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查文件是否匹配 includes 模式
|
||||
const matches = micromatch.isMatch(issue.file, includes, { matchBase: true });
|
||||
if (!matches) {
|
||||
// console.log(` Issue [${issue.ruleId}] 在文件 ${issue.file} 不匹配 includes 模式,跳过`);
|
||||
}
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 override 配置过滤 issues,排除被覆盖规则产生的 issues
|
||||
*
|
||||
* ## Override 机制说明
|
||||
* Override 允许高优先级规则"覆盖"低优先级规则。当规则 A 声明 `overrides: ["B"]` 时,
|
||||
* 规则 B 产生的 issues 会被过滤掉,避免重复报告或低优先级噪音。
|
||||
*
|
||||
* ## 作用域规则
|
||||
* Override 是**作用域感知**的:只有当 issue 的文件匹配 override 所在 spec 的 includes 时,
|
||||
* 该 override 才会生效。这允许不同目录/文件类型使用不同的规则优先级。
|
||||
*
|
||||
* 示例:
|
||||
* ```yaml
|
||||
* # controller-spec.yaml (includes: ["*.controller.ts"])
|
||||
* overrides: ["JsTs.Base.Rule1"] # 只在 controller 文件中覆盖 Rule1
|
||||
* ```
|
||||
* 上述 override 不会影响 `*.service.ts` 文件中的 `Rule1` issues。
|
||||
*
|
||||
* ## 处理流程
|
||||
* 1. **收集阶段**:遍历所有 specs,收集 overrides 并保留其作用域(includes)信息
|
||||
* - 文件级 overrides (`spec.overrides`) - 继承 spec 的 includes 作用域
|
||||
* - 规则级 overrides (`rule.overrides`) - 同样继承 spec 的 includes 作用域
|
||||
* 2. **过滤阶段**:对每个 issue,检查是否存在匹配的 override
|
||||
* - 需同时满足:ruleId 匹配 AND issue 文件在 override 的 includes 作用域内
|
||||
* - 如果 includes 为空,表示全局作用域(匹配所有文件)
|
||||
*
|
||||
* @param issues - 待过滤的 issues 列表,每个 issue 必须包含 ruleId 字段,可选 file 字段
|
||||
* @param specs - 已加载的 ReviewSpec 列表
|
||||
* @param verbose - 日志详细级别:1=基础统计,3=详细收集过程
|
||||
* @returns 过滤后的 issues 列表(排除了被 override 的规则产生的 issues)
|
||||
*/
|
||||
filterIssuesByOverrides<T extends { ruleId: string; file?: string }>(
|
||||
issues: T[],
|
||||
specs: ReviewSpec[],
|
||||
verbose?: VerboseLevel,
|
||||
): T[] {
|
||||
// ========== 阶段1: 收集 spec -> overrides 的映射(保留作用域信息) ==========
|
||||
// 每个 override 需要记录其来源 spec 的 includes,用于作用域判断
|
||||
const scopedOverrides: Array<{
|
||||
override: string;
|
||||
includes: string[];
|
||||
source: string; // 用于日志:spec filename 或 rule id
|
||||
}> = [];
|
||||
|
||||
for (const spec of specs) {
|
||||
// 文件级 overrides:作用域为该 spec 的 includes
|
||||
if (shouldLog(verbose, 3) && spec.overrides.length > 0) {
|
||||
console.error(` 📋 ${spec.filename} 文件级 overrides: ${spec.overrides.join(", ")}`);
|
||||
}
|
||||
for (const override of spec.overrides) {
|
||||
scopedOverrides.push({
|
||||
override,
|
||||
includes: spec.includes,
|
||||
source: spec.filename,
|
||||
});
|
||||
}
|
||||
|
||||
// 规则级 overrides:继承该 spec 的 includes 作用域
|
||||
for (const rule of spec.rules) {
|
||||
if (shouldLog(verbose, 3) && rule.overrides.length > 0) {
|
||||
console.error(
|
||||
` 📋 ${spec.filename} 规则 [${rule.id}] overrides: ${rule.overrides.join(", ")}`,
|
||||
);
|
||||
}
|
||||
for (const override of rule.overrides) {
|
||||
scopedOverrides.push({
|
||||
override,
|
||||
includes: spec.includes,
|
||||
source: `${spec.filename}#${rule.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 输出收集结果汇总(verbose=3 时)
|
||||
if (shouldLog(verbose, 3)) {
|
||||
const overrideList = scopedOverrides.map((o) => o.override);
|
||||
console.error(
|
||||
` 🔍 收集到的 overrides 总计: ${overrideList.length > 0 ? overrideList.join(", ") : "(无)"}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 快速路径:无 override 声明时直接返回原列表
|
||||
if (scopedOverrides.length === 0) {
|
||||
return issues;
|
||||
}
|
||||
|
||||
// ========== 阶段2: 过滤 issues(作用域感知) ==========
|
||||
// 对每个 issue,只检查其文件匹配的 spec 中声明的 overrides
|
||||
const beforeCount = issues.length;
|
||||
const skipped: Array<{ issue: T; override: string; source: string }> = [];
|
||||
const filtered = issues.filter((issue) => {
|
||||
const issueFile = "file" in issue ? (issue as { file: string }).file : "";
|
||||
|
||||
// 查找第一个匹配的 override(需同时满足:ruleId 匹配 AND 文件在 includes 作用域内)
|
||||
const matched = scopedOverrides.find((scoped) => {
|
||||
// 检查 ruleId 是否匹配 override 模式
|
||||
if (!this.matchRuleId(issue.ruleId, scoped.override)) {
|
||||
return false;
|
||||
}
|
||||
// 检查 issue 文件是否在该 override 的作用域内
|
||||
// 如果 includes 为空,表示全局作用域(匹配所有文件)
|
||||
if (scoped.includes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// 使用 micromatch 检查文件是否匹配 includes 模式
|
||||
return issueFile && micromatch.isMatch(issueFile, scoped.includes, { matchBase: true });
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
skipped.push({ issue, override: matched.override, source: matched.source });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// ========== 阶段3: 输出过滤结果日志 ==========
|
||||
if (skipped.length > 0 && shouldLog(verbose, 1)) {
|
||||
console.error(` Override 过滤: ${beforeCount} -> ${filtered.length} 个问题`);
|
||||
for (const { issue, override, source } of skipped) {
|
||||
const file = "file" in issue ? (issue as { file: string }).file : "";
|
||||
const line = "line" in issue ? (issue as { line: string }).line : "";
|
||||
console.error(
|
||||
` ❌ [${issue.ruleId}] ${file}:${line} (override: ${override} from ${source})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变更文件的 patch 信息过滤 issues
|
||||
* 只保留 issue 的行号在实际变更行范围内的问题
|
||||
*/
|
||||
filterIssuesByCommits<T extends { file: string; line: string }>(
|
||||
issues: T[],
|
||||
changedFiles: { filename?: string; patch?: string }[],
|
||||
): T[] {
|
||||
// 构建文件 -> 变更行集合的映射
|
||||
const fileChangedLines = new Map<string, Set<number>>();
|
||||
|
||||
for (const file of changedFiles) {
|
||||
if (!file.filename || !file.patch) continue;
|
||||
const lines = this.parseChangedLinesFromPatch(file.patch);
|
||||
fileChangedLines.set(file.filename, lines);
|
||||
}
|
||||
|
||||
return issues.filter((issue) => {
|
||||
const changedLines = fileChangedLines.get(issue.file);
|
||||
|
||||
// 如果没有该文件的 patch 信息,保留 issue
|
||||
if (!changedLines || changedLines.size === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解析 issue 的行号(支持单行或范围如 "123" 或 "123-125")
|
||||
const issueLines = this.parseLineRange(issue.line);
|
||||
|
||||
// 检查 issue 的任意行是否在变更行范围内
|
||||
const matches = issueLines.some((line) => changedLines.has(line));
|
||||
if (!matches) {
|
||||
// console.log(` Issue ${issue.file}:${issue.line} 不在变更行范围内,跳过`);
|
||||
}
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 unified diff patch 中解析变更的行号(新文件中的行号)
|
||||
*/
|
||||
protected parseChangedLinesFromPatch(patch: string): Set<number> {
|
||||
const changedLines = new Set<number>();
|
||||
const lines = patch.split("\n");
|
||||
|
||||
let currentLine = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
||||
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
currentLine = parseInt(hunkMatch[1], 10);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||
// 新增行
|
||||
changedLines.add(currentLine);
|
||||
currentLine++;
|
||||
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
||||
// 删除行不增加行号
|
||||
} else {
|
||||
// 上下文行
|
||||
currentLine++;
|
||||
}
|
||||
}
|
||||
|
||||
return changedLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析行号字符串,支持单行 "123" 或范围 "123-125"
|
||||
* 返回行号数组,如果解析失败返回空数组
|
||||
*/
|
||||
parseLineRange(lineStr: string): number[] {
|
||||
const lines: number[] = [];
|
||||
const rangeMatch = lineStr.match(/^(\d+)-(\d+)$/);
|
||||
|
||||
if (rangeMatch) {
|
||||
const start = parseInt(rangeMatch[1], 10);
|
||||
const end = parseInt(rangeMatch[2], 10);
|
||||
for (let i = start; i <= end; i++) {
|
||||
lines.push(i);
|
||||
}
|
||||
} else {
|
||||
const line = parseInt(lineStr, 10);
|
||||
if (!isNaN(line)) {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 specs 的 prompt 部分
|
||||
*/
|
||||
buildSpecsSection(specs: ReviewSpec[]): string {
|
||||
return specs
|
||||
.map((spec) => {
|
||||
const firstRule = spec.rules[0];
|
||||
const rulesText = spec.rules
|
||||
.slice(1)
|
||||
.map((rule) => {
|
||||
let text = `#### [${rule.id}] ${rule.title}\n`;
|
||||
if (rule.description) {
|
||||
text += `${rule.description}\n`;
|
||||
}
|
||||
if (rule.examples.length > 0) {
|
||||
for (const example of rule.examples) {
|
||||
text += `##### ${example.type === "good" ? "推荐做法 (Good)" : "不推荐做法 (Bad)"}\n`;
|
||||
text += `\`\`\`${example.lang}\n${example.code}\n\`\`\`\n`;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `### ${firstRule.title}\n- 规范文件: ${spec.filename}\n- 适用扩展名: ${spec.extensions.join(", ")}\n\n${rulesText}`;
|
||||
})
|
||||
.join("\n\n-------------------\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ruleId 查找规则定义
|
||||
* 支持精确匹配和前缀匹配
|
||||
*/
|
||||
findRuleById(ruleId: string, specs: ReviewSpec[]): { rule: ReviewRule; spec: ReviewSpec } | null {
|
||||
for (const spec of specs) {
|
||||
for (const rule of spec.rules) {
|
||||
if (this.matchRuleId(ruleId, rule.id)) {
|
||||
return { rule, spec };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤 issues,只保留 ruleId 存在于 specs 中的问题
|
||||
*/
|
||||
filterIssuesByRuleExistence<T extends { ruleId: string }>(issues: T[], specs: ReviewSpec[]): T[] {
|
||||
return issues.filter((issue) => {
|
||||
const ruleInfo = this.findRuleById(issue.ruleId, specs);
|
||||
if (!ruleInfo) {
|
||||
// console.log(` Issue [${issue.ruleId}] 规则不存在,跳过`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重规范文件中的重复 ruleId
|
||||
* 后加载的规则覆盖先加载的(符合配置优先级:命令行 > 配置文件 > 默认路径)
|
||||
* @returns 去重后的 specs 数组
|
||||
*/
|
||||
deduplicateSpecs(specs: ReviewSpec[]): ReviewSpec[] {
|
||||
// 记录 ruleId -> { specIndex, ruleIndex } 的映射,用于检测重复
|
||||
const ruleIdMap = new Map<string, { specIndex: number; ruleIndex: number }>();
|
||||
// 记录需要从每个 spec 中移除的 rule 索引
|
||||
const rulesToRemove = new Map<number, Set<number>>();
|
||||
|
||||
for (let specIndex = 0; specIndex < specs.length; specIndex++) {
|
||||
const spec = specs[specIndex];
|
||||
for (let ruleIndex = 0; ruleIndex < spec.rules.length; ruleIndex++) {
|
||||
const rule = spec.rules[ruleIndex];
|
||||
const existing = ruleIdMap.get(rule.id);
|
||||
|
||||
if (existing) {
|
||||
// 标记先前的规则为待移除(后加载的覆盖先加载的)
|
||||
if (!rulesToRemove.has(existing.specIndex)) {
|
||||
rulesToRemove.set(existing.specIndex, new Set());
|
||||
}
|
||||
rulesToRemove.get(existing.specIndex)!.add(existing.ruleIndex);
|
||||
}
|
||||
|
||||
// 更新映射为当前规则
|
||||
ruleIdMap.set(rule.id, { specIndex, ruleIndex });
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有重复,直接返回原数组
|
||||
if (rulesToRemove.size === 0) {
|
||||
return specs;
|
||||
}
|
||||
|
||||
// 构建去重后的 specs
|
||||
const result: ReviewSpec[] = [];
|
||||
for (let specIndex = 0; specIndex < specs.length; specIndex++) {
|
||||
const spec = specs[specIndex];
|
||||
const removeSet = rulesToRemove.get(specIndex);
|
||||
|
||||
if (!removeSet || removeSet.size === 0) {
|
||||
result.push(spec);
|
||||
} else {
|
||||
const filteredRules = spec.rules.filter((_, ruleIndex) => !removeSet.has(ruleIndex));
|
||||
if (filteredRules.length > 0) {
|
||||
result.push({ ...spec, rules: filteredRules });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 issues,用规则定义的 severity 覆盖 AI 返回的值
|
||||
*/
|
||||
formatIssues<T extends { ruleId: string; severity?: Severity }>(
|
||||
issues: T[],
|
||||
{ specs, changedFiles }: { specs: ReviewSpec[]; changedFiles: ChangedFile[] },
|
||||
): T[] {
|
||||
// 构建 ruleId -> severity 的映射
|
||||
const ruleSeverityMap = new Map<string, Severity>();
|
||||
|
||||
for (const spec of specs) {
|
||||
for (const rule of spec.rules) {
|
||||
// 规则级别的 severity 优先,否则使用文件级别的 severity
|
||||
const severity = rule.severity ?? spec.severity;
|
||||
ruleSeverityMap.set(rule.id, severity);
|
||||
}
|
||||
}
|
||||
|
||||
return issues.map((issue) => {
|
||||
const ruleSeverity = this.findByRuleId(issue.ruleId, ruleSeverityMap);
|
||||
|
||||
if (ruleSeverity && ruleSeverity !== issue.severity) {
|
||||
return { ...issue, severity: ruleSeverity };
|
||||
}
|
||||
|
||||
return issue;
|
||||
});
|
||||
}
|
||||
}
|
||||
143
commands/review/src/review-spec/types.ts
Normal file
143
commands/review/src/review-spec/types.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
export type Severity = "off" | "warn" | "error";
|
||||
|
||||
/** 文件内容行:[commitHash, lineCode],commitHash 为 7 位短 hash 或 "-------" 表示非变更行 */
|
||||
export type FileContentLine = [string, string];
|
||||
|
||||
/** 文件内容映射:filename -> 每行的 [commitHash, lineCode] 数组 */
|
||||
export type FileContentsMap = Map<string, FileContentLine[]>;
|
||||
|
||||
export const SEVERITY_EMOJI: Record<Severity, string> = {
|
||||
off: "⚪",
|
||||
warn: "🟡",
|
||||
error: "🔴",
|
||||
};
|
||||
|
||||
export interface ReviewSpec {
|
||||
filename: string;
|
||||
extensions: string[];
|
||||
type: string;
|
||||
content: string;
|
||||
rules: ReviewRule[];
|
||||
overrides: string[]; // 文件级别的 override
|
||||
severity: Severity; // 文件级别的默认 severity
|
||||
includes: string[]; // 文件级别的 includes,只有匹配的文件才应用此规范
|
||||
}
|
||||
|
||||
export interface RuleExample {
|
||||
lang: string;
|
||||
code: string;
|
||||
type: "good" | "bad";
|
||||
}
|
||||
|
||||
export interface ReviewRule {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
examples: RuleExample[];
|
||||
overrides: string[]; // 规则级别的 override
|
||||
severity?: Severity; // 规则级别的 severity,可覆盖文件级别
|
||||
includes?: string[]; // 规则级别的 includes,可覆盖文件级别
|
||||
}
|
||||
|
||||
/** 问题的 Reaction 记录 */
|
||||
export interface IssueReaction {
|
||||
/** reaction 内容,如 +1, -1, laugh, hooray, confused, heart, rocket, eyes */
|
||||
content: string;
|
||||
/** 点击该 reaction 的用户列表 */
|
||||
users: string[];
|
||||
}
|
||||
|
||||
/** 用户信息 */
|
||||
export interface UserInfo {
|
||||
/** 用户 ID */
|
||||
id?: string;
|
||||
/** 用户登录名 */
|
||||
login: string;
|
||||
}
|
||||
|
||||
/** 问题评论的回复记录 */
|
||||
export interface IssueReply {
|
||||
/** 回复用户 */
|
||||
user: UserInfo;
|
||||
/** 回复内容 */
|
||||
body: string;
|
||||
/** 回复时间 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ReviewIssue {
|
||||
file: string;
|
||||
line: string; // 格式 12 或者 12-14
|
||||
code: string; // 当前行代码, 去除首尾空白
|
||||
ruleId: string;
|
||||
specFile: string;
|
||||
reason: string;
|
||||
date?: string; // 发现问题的时间
|
||||
fixed?: string; // 修复时间
|
||||
valid?: string; // 问题是否有效
|
||||
suggestion?: string;
|
||||
commit?: string;
|
||||
severity: Severity;
|
||||
round: number; // 发现问题的轮次
|
||||
/** 问题的作者 */
|
||||
author?: UserInfo;
|
||||
/** 问题评论的 reactions 记录 */
|
||||
reactions?: IssueReaction[];
|
||||
/** 问题评论的回复/聊天记录 */
|
||||
replies?: IssueReply[];
|
||||
/** 原始行号(行号更新前的值) */
|
||||
originalLine?: string;
|
||||
}
|
||||
|
||||
export interface FileSummary {
|
||||
file: string;
|
||||
resolved: number;
|
||||
unresolved: number;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface DeletionImpact {
|
||||
file: string;
|
||||
deletedCode: string;
|
||||
riskLevel: "high" | "medium" | "low" | "none";
|
||||
affectedFiles: string[];
|
||||
reason: string;
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
export interface DeletionImpactResult {
|
||||
impacts: DeletionImpact[];
|
||||
summary: string;
|
||||
}
|
||||
|
||||
/** Review 统计信息 */
|
||||
export interface ReviewStats {
|
||||
/** 总问题数 */
|
||||
total: number;
|
||||
/** 已修复数 */
|
||||
fixed: number;
|
||||
/** 无效问题数 */
|
||||
invalid: number;
|
||||
/** 待处理数 */
|
||||
pending: number;
|
||||
/** 修复率 (0-100) */
|
||||
fixRate: number;
|
||||
}
|
||||
|
||||
export interface ReviewResult {
|
||||
success: boolean;
|
||||
/**
|
||||
* AI 生成的 PR 标题
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 通过 commit 和 文件总结一下这个 PR 开发的什么功能
|
||||
*/
|
||||
description: string;
|
||||
issues: ReviewIssue[];
|
||||
summary: FileSummary[];
|
||||
deletionImpact?: DeletionImpactResult;
|
||||
round: number; // 当前 review 的轮次
|
||||
/** 问题统计信息 */
|
||||
stats?: ReviewStats;
|
||||
}
|
||||
244
commands/review/src/review.command.ts
Normal file
244
commands/review/src/review.command.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { Command, CommandRunner, Option, t } from "@spaceflow/core";
|
||||
import type { LLMMode, VerboseLevel } from "@spaceflow/core";
|
||||
import type { AnalyzeDeletionsMode } from "./review.config";
|
||||
import type { ReportFormat } from "./review-report";
|
||||
import { ReviewService } from "./review.service";
|
||||
|
||||
export interface ReviewOptions {
|
||||
dryRun: boolean;
|
||||
ci: boolean;
|
||||
prNumber?: number;
|
||||
base?: string;
|
||||
head?: string;
|
||||
references?: string[];
|
||||
verbose?: VerboseLevel;
|
||||
includes?: string[];
|
||||
llmMode?: LLMMode;
|
||||
files?: string[];
|
||||
commits?: string[];
|
||||
verifyFixes?: boolean;
|
||||
verifyConcurrency?: number;
|
||||
analyzeDeletions?: AnalyzeDeletionsMode;
|
||||
/** 仅执行删除代码分析,跳过常规代码审查 */
|
||||
deletionOnly?: boolean;
|
||||
/** 删除代码分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式 */
|
||||
deletionAnalysisMode?: LLMMode;
|
||||
/** 输出格式:markdown, terminal, json。不指定则智能选择 */
|
||||
outputFormat?: ReportFormat;
|
||||
/** 是否使用 AI 生成 PR 功能描述 */
|
||||
generateDescription?: boolean;
|
||||
/** 显示所有问题,不过滤非变更行的问题 */
|
||||
showAll?: boolean;
|
||||
/** PR 事件类型(opened, synchronize, closed 等) */
|
||||
eventAction?: string;
|
||||
concurrency?: number;
|
||||
timeout?: number;
|
||||
retries?: number;
|
||||
retryDelay?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Review 命令
|
||||
*
|
||||
* 在 GitHub Actions 中执行,用于自动代码审查
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo 格式)
|
||||
* - GITHUB_REF_NAME: 当前分支名称
|
||||
* - GITHUB_EVENT_PATH: 事件文件路径(包含 PR 信息)
|
||||
*/
|
||||
@Command({
|
||||
name: "review",
|
||||
description: t("review:description"),
|
||||
})
|
||||
export class ReviewCommand extends CommandRunner {
|
||||
constructor(protected readonly reviewService: ReviewService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(_passedParams: string[], options: ReviewOptions): Promise<void> {
|
||||
try {
|
||||
const context = await this.reviewService.getContextFromEnv(options);
|
||||
await this.reviewService.execute(context);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(t("common.executionFailed", { error: error.message }));
|
||||
if (error.stack) {
|
||||
console.error(t("common.stackTrace", { stack: error.stack }));
|
||||
}
|
||||
} else {
|
||||
console.error(t("common.executionFailed", { error }));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-d, --dry-run",
|
||||
description: t("review:options.dryRun"),
|
||||
})
|
||||
parseDryRun(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-c, --ci",
|
||||
description: t("common.options.ci"),
|
||||
})
|
||||
parseCi(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-p, --pr-number <number>",
|
||||
description: t("review:options.prNumber"),
|
||||
})
|
||||
parsePrNumber(val: string): number {
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-b, --base <ref>",
|
||||
description: t("review:options.base"),
|
||||
})
|
||||
parseBase(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--head <ref>",
|
||||
description: t("review:options.head"),
|
||||
})
|
||||
parseHead(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-v, --verbose",
|
||||
description: t("common.options.verboseDebug"),
|
||||
})
|
||||
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
|
||||
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
|
||||
return Math.min(current + 1, 3) as VerboseLevel;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-i, --includes <patterns...>",
|
||||
description: t("review:options.includes"),
|
||||
})
|
||||
parseIncludes(val: string, previous: string[] = []): string[] {
|
||||
return [...previous, val];
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-l, --llm-mode <mode>",
|
||||
description: t("review:options.llmMode"),
|
||||
choices: ["claude-code", "openai", "gemini"],
|
||||
})
|
||||
parseLlmMode(val: string): LLMMode {
|
||||
return val as LLMMode;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-f, --files <files...>",
|
||||
description: t("review:options.files"),
|
||||
})
|
||||
parseFiles(val: string, previous: string[] = []): string[] {
|
||||
return [...previous, val];
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--commits <commits...>",
|
||||
description: t("review:options.commits"),
|
||||
})
|
||||
parseCommits(val: string, previous: string[] = []): string[] {
|
||||
return [...previous, val];
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--verify-fixes",
|
||||
description: t("review:options.verifyFixes"),
|
||||
})
|
||||
parseVerifyFixes(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--no-verify-fixes",
|
||||
description: t("review:options.noVerifyFixes"),
|
||||
})
|
||||
parseNoVerifyFixes(val: boolean): boolean {
|
||||
return !val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--verify-concurrency <number>",
|
||||
description: t("review:options.verifyConcurrency"),
|
||||
})
|
||||
parseVerifyConcurrency(val: string): number {
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--analyze-deletions [mode]",
|
||||
description: t("review:options.analyzeDeletions"),
|
||||
})
|
||||
parseAnalyzeDeletions(val: string | boolean): AnalyzeDeletionsMode {
|
||||
if (val === true || val === "true") return true;
|
||||
if (val === false || val === "false") return false;
|
||||
if (val === "ci" || val === "pr" || val === "terminal") return val;
|
||||
// 默认为 true(当只传 --analyze-deletions 不带值时)
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--deletion-analysis-mode <mode>",
|
||||
description: t("review:options.deletionAnalysisMode"),
|
||||
choices: ["openai", "claude-code"],
|
||||
})
|
||||
parseDeletionAnalysisMode(val: string): LLMMode {
|
||||
return val as LLMMode;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--deletion-only",
|
||||
description: t("review:options.deletionOnly"),
|
||||
})
|
||||
parseDeletionOnly(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "-o, --output-format <format>",
|
||||
description: t("review:options.outputFormat"),
|
||||
choices: ["markdown", "terminal", "json"],
|
||||
})
|
||||
parseOutputFormat(val: string): ReportFormat {
|
||||
return val as ReportFormat;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--generate-description",
|
||||
description: t("review:options.generateDescription"),
|
||||
})
|
||||
parseGenerateDescription(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--show-all",
|
||||
description: t("review:options.showAll"),
|
||||
})
|
||||
parseShowAll(val: boolean): boolean {
|
||||
return val;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: "--event-action <action>",
|
||||
description: t("review:options.eventAction"),
|
||||
})
|
||||
parseEventAction(val: string): string {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
58
commands/review/src/review.config.ts
Normal file
58
commands/review/src/review.config.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { z } from "@spaceflow/core";
|
||||
|
||||
/** LLM 模式 schema(与 core 中的 LLMMode 保持一致) */
|
||||
const llmModeSchema = z.enum(["claude-code", "openai", "gemini", "open-code"]);
|
||||
|
||||
/** 删除代码分析模式 schema */
|
||||
const analyzeDeletionsModeSchema = z.union([z.boolean(), z.enum(["ci", "pr", "terminal"])]);
|
||||
|
||||
/** 审查规则严重级别 schema */
|
||||
const severitySchema = z.enum(["off", "warn", "error"]);
|
||||
|
||||
/** 变更文件处理策略 schema */
|
||||
const invalidateChangedFilesSchema = z.enum(["invalidate", "keep", "off"]);
|
||||
|
||||
/** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
|
||||
export const reviewSchema = () =>
|
||||
z.object({
|
||||
references: z.array(z.string()).optional(),
|
||||
llmMode: llmModeSchema.default("openai").optional(),
|
||||
includes: z.array(z.string()).optional(),
|
||||
rules: z.record(z.string(), severitySchema).optional(),
|
||||
verifyFixes: z.boolean().default(false),
|
||||
verifyFixesConcurrency: z.number().default(10).optional(),
|
||||
analyzeDeletions: analyzeDeletionsModeSchema.default(false),
|
||||
deletionAnalysisMode: llmModeSchema.default("openai").optional(),
|
||||
lineComments: z.boolean().default(false),
|
||||
generateDescription: z.boolean().default(false).optional(),
|
||||
autoUpdatePrTitle: z.boolean().default(false).optional(),
|
||||
concurrency: z.number().default(5).optional(),
|
||||
timeout: z.number().optional(),
|
||||
retries: z.number().default(0).optional(),
|
||||
retryDelay: z.number().default(1000).optional(),
|
||||
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* 变更文件处理策略
|
||||
* - 'invalidate': 将变更文件的历史问题标记为无效(默认)
|
||||
* - 'keep': 保留历史问题,不做处理
|
||||
* - 'off': 关闭此功能
|
||||
*/
|
||||
export type InvalidateChangedFilesMode = z.infer<typeof invalidateChangedFilesSchema>;
|
||||
|
||||
/** review 配置类型(从 schema 推导) */
|
||||
export type ReviewConfig = z.infer<ReturnType<typeof reviewSchema>>;
|
||||
|
||||
/**
|
||||
* 删除代码分析模式
|
||||
* - true: 始终启用
|
||||
* - false: 始终禁用
|
||||
* - 'ci': 仅在 CI 环境中启用
|
||||
* - 'pr': 仅在 PR 环境中启用
|
||||
* - 'terminal': 仅在终端环境中启用
|
||||
*/
|
||||
export type AnalyzeDeletionsMode = z.infer<typeof analyzeDeletionsModeSchema>;
|
||||
|
||||
/** 审查规则严重级别 */
|
||||
export type Severity = z.infer<typeof severitySchema>;
|
||||
184
commands/review/src/review.mcp.ts
Normal file
184
commands/review/src/review.mcp.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Review MCP 服务
|
||||
* 提供代码审查规则查询的 MCP 工具
|
||||
*/
|
||||
|
||||
import { McpServer, McpTool, ConfigReaderService, t } from "@spaceflow/core";
|
||||
import { ReviewSpecService } from "./review-spec/review-spec.service";
|
||||
import { join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { ListRulesInput, GetRulesForFileInput, GetRuleDetailInput } from "./dto/mcp.dto";
|
||||
import type { ReviewConfig } from "./review.config";
|
||||
|
||||
@McpServer({ name: "review-mcp", version: "1.0.0", description: t("review:mcp.serverDescription") })
|
||||
export class ReviewMcp {
|
||||
constructor(
|
||||
private readonly specService: ReviewSpecService,
|
||||
private readonly configReader: ConfigReaderService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取项目的规则目录
|
||||
*/
|
||||
private async getSpecDirs(cwd: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
// 1. 通过 ConfigReaderService 读取 review 配置
|
||||
try {
|
||||
const reviewConfig = this.configReader.getPluginConfig<ReviewConfig>("review");
|
||||
if (reviewConfig?.references?.length) {
|
||||
const resolved = await this.specService.resolveSpecSources(reviewConfig.references);
|
||||
dirs.push(...resolved);
|
||||
}
|
||||
} catch {
|
||||
// 忽略配置读取错误
|
||||
}
|
||||
|
||||
// 2. 检查默认目录
|
||||
const defaultDirs = [
|
||||
join(cwd, ".claude", "skills"),
|
||||
join(cwd, ".cursor", "skills"),
|
||||
join(cwd, "review-specs"),
|
||||
];
|
||||
|
||||
for (const dir of defaultDirs) {
|
||||
if (existsSync(dir)) {
|
||||
dirs.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(dirs)]; // 去重
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载所有规则
|
||||
*/
|
||||
private async loadAllSpecs(cwd: string) {
|
||||
const specDirs = await this.getSpecDirs(cwd);
|
||||
const allSpecs = [];
|
||||
|
||||
for (const dir of specDirs) {
|
||||
const specs = await this.specService.loadReviewSpecs(dir);
|
||||
allSpecs.push(...specs);
|
||||
}
|
||||
|
||||
// 只去重,不应用 override(MCP 工具应返回所有规则,override 在实际审查时应用)
|
||||
return this.specService.deduplicateSpecs(allSpecs);
|
||||
}
|
||||
|
||||
@McpTool({
|
||||
name: "list_rules",
|
||||
description: t("review:mcp.listRules"),
|
||||
dto: ListRulesInput,
|
||||
})
|
||||
async listRules(input: ListRulesInput) {
|
||||
const workDir = input.cwd || process.cwd();
|
||||
const specs = await this.loadAllSpecs(workDir);
|
||||
|
||||
const rules = specs.flatMap((spec) =>
|
||||
spec.rules.map((rule) => ({
|
||||
id: rule.id,
|
||||
title: rule.title,
|
||||
description: rule.description.slice(0, 200) + (rule.description.length > 200 ? "..." : ""),
|
||||
severity: rule.severity || spec.severity,
|
||||
extensions: spec.extensions,
|
||||
specFile: spec.filename,
|
||||
includes: spec.includes,
|
||||
hasExamples: rule.examples.length > 0,
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
total: rules.length,
|
||||
rules,
|
||||
};
|
||||
}
|
||||
|
||||
@McpTool({
|
||||
name: "get_rules_for_file",
|
||||
description: t("review:mcp.getRulesForFile"),
|
||||
dto: GetRulesForFileInput,
|
||||
})
|
||||
async getRulesForFile(input: GetRulesForFileInput) {
|
||||
const workDir = input.cwd || process.cwd();
|
||||
const allSpecs = await this.loadAllSpecs(workDir);
|
||||
|
||||
// 根据文件过滤适用的规则
|
||||
const applicableSpecs = this.specService.filterApplicableSpecs(allSpecs, [
|
||||
{ filename: input.filePath },
|
||||
]);
|
||||
|
||||
// 进一步根据 includes 过滤(支持规则级 includes 覆盖文件级)
|
||||
const micromatchModule = await import("micromatch");
|
||||
const micromatch = micromatchModule.default || micromatchModule;
|
||||
|
||||
const rules = applicableSpecs.flatMap((spec) =>
|
||||
spec.rules
|
||||
.filter((rule) => {
|
||||
// 规则级 includes 优先于文件级
|
||||
const includes = rule.includes || spec.includes;
|
||||
if (includes.length === 0) return true;
|
||||
return micromatch.isMatch(input.filePath, includes, { matchBase: true });
|
||||
})
|
||||
.map((rule) => ({
|
||||
id: rule.id,
|
||||
title: rule.title,
|
||||
description: rule.description,
|
||||
severity: rule.severity || spec.severity,
|
||||
specFile: spec.filename,
|
||||
...(input.includeExamples && rule.examples.length > 0
|
||||
? {
|
||||
examples: rule.examples.map((ex) => ({
|
||||
type: ex.type,
|
||||
lang: ex.lang,
|
||||
code: ex.code,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
file: input.filePath,
|
||||
total: rules.length,
|
||||
rules,
|
||||
};
|
||||
}
|
||||
|
||||
@McpTool({
|
||||
name: "get_rule_detail",
|
||||
description: t("review:mcp.getRuleDetail"),
|
||||
dto: GetRuleDetailInput,
|
||||
})
|
||||
async getRuleDetail(input: GetRuleDetailInput) {
|
||||
const workDir = input.cwd || process.cwd();
|
||||
const specs = await this.loadAllSpecs(workDir);
|
||||
|
||||
const result = this.specService.findRuleById(input.ruleId, specs);
|
||||
|
||||
if (!result) {
|
||||
return { error: t("review:mcp.ruleNotFound", { ruleId: input.ruleId }) };
|
||||
}
|
||||
|
||||
const { rule, spec } = result;
|
||||
|
||||
return {
|
||||
id: rule.id,
|
||||
title: rule.title,
|
||||
description: rule.description,
|
||||
severity: rule.severity || spec.severity,
|
||||
specFile: spec.filename,
|
||||
extensions: spec.extensions,
|
||||
includes: spec.includes,
|
||||
overrides: rule.overrides,
|
||||
examples: rule.examples.map((ex) => ({
|
||||
type: ex.type,
|
||||
lang: ex.lang,
|
||||
code: ex.code,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ReviewMcpService 类已通过 @McpServer 装饰器标记
|
||||
// CLI 的 `spaceflow mcp` 命令会自动扫描并发现该类
|
||||
52
commands/review/src/review.module.ts
Normal file
52
commands/review/src/review.module.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Module,
|
||||
ConfigModule,
|
||||
ConfigService,
|
||||
ConfigReaderModule,
|
||||
ConfigReaderService,
|
||||
GitProviderModule,
|
||||
ciConfig,
|
||||
llmConfig,
|
||||
ClaudeSetupModule,
|
||||
LlmProxyModule,
|
||||
GitSdkModule,
|
||||
type LlmConfig,
|
||||
} from "@spaceflow/core";
|
||||
import { ReviewSpecModule } from "./review-spec";
|
||||
import { ReviewReportModule } from "./review-report";
|
||||
import { ReviewCommand } from "./review.command";
|
||||
import { ReviewService } from "./review.service";
|
||||
import { IssueVerifyService } from "./issue-verify.service";
|
||||
import { DeletionImpactService } from "./deletion-impact.service";
|
||||
import { ReviewMcp } from "./review.mcp";
|
||||
import type { ReviewConfig } from "./review.config";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forFeature(ciConfig),
|
||||
ConfigModule.forFeature(llmConfig),
|
||||
ConfigReaderModule,
|
||||
GitProviderModule.forFeature(),
|
||||
ClaudeSetupModule,
|
||||
ReviewSpecModule,
|
||||
ReviewReportModule,
|
||||
GitSdkModule,
|
||||
LlmProxyModule.forRootAsync({
|
||||
imports: [ConfigReaderModule, ConfigModule],
|
||||
useFactory: (configReader: ConfigReaderService, configService: ConfigService) => {
|
||||
const reviewConf = configReader.getPluginConfig<ReviewConfig>("review");
|
||||
const llm = configService.get<LlmConfig>("llm")!;
|
||||
return {
|
||||
defaultAdapter: reviewConf?.llmMode || "openai",
|
||||
claudeCode: llm.claudeCode,
|
||||
openai: llm.openai,
|
||||
openCode: llm.openCode,
|
||||
};
|
||||
},
|
||||
inject: [ConfigReaderService, ConfigService],
|
||||
}),
|
||||
],
|
||||
providers: [ReviewCommand, ReviewService, IssueVerifyService, DeletionImpactService, ReviewMcp],
|
||||
exports: [ReviewMcp],
|
||||
})
|
||||
export class ReviewModule {}
|
||||
3007
commands/review/src/review.service.spec.ts
Normal file
3007
commands/review/src/review.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
2603
commands/review/src/review.service.ts
Normal file
2603
commands/review/src/review.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
8
commands/review/tsconfig.json
Normal file
8
commands/review/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../core/tsconfig.skill.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
34
commands/review/vitest.config.ts
Normal file
34
commands/review/vitest.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import swc from "unplugin-swc";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [swc.vite()],
|
||||
test: {
|
||||
root: "src",
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["**/*.spec.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
include: ["**/*.ts"],
|
||||
exclude: [
|
||||
"**/*.spec.ts",
|
||||
"**/*.module.ts",
|
||||
"**/index.ts",
|
||||
"**/__mocks__/**",
|
||||
"**/*.command.ts",
|
||||
"**/locales/**",
|
||||
"**/dto/**",
|
||||
"**/review-report/**",
|
||||
"**/review.mcp.ts",
|
||||
"**/review.config.ts",
|
||||
],
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user