diff --git a/.agents/skills/ulthon-update-workflow/SKILL.md b/.agents/skills/ulthon-update-workflow/SKILL.md index 140dcab..552cc1d 100644 --- a/.agents/skills/ulthon-update-workflow/SKILL.md +++ b/.agents/skills/ulthon-update-workflow/SKILL.md @@ -98,6 +98,29 @@ php think admin:update --dry-run 向开发者报告以上信息,由开发者决定是否继续。 +### 4.5 跳过文件处理 + +当 dry-run 输出中存在 `[skipped]` 标记的文件时,说明这些冲突文件被跳过了。此时建议重新执行 dry-run 并加 `--keep-repo`,保留上游克隆目录用于对比: + +```bash +php think admin:update --dry-run --optional-conflict=skip --force-conflict=overwrite --keep-repo +``` + +命令结束后,以下目录会被保留: + +| 目录 | 内容 | +|------|------| +| `runtime/update/current/` | 当前安装版本的原始代码 | +| `runtime/update/repo/` | 上游最新版本的代码 | +| 项目根目录 | 开发者实际修改的代码 | + +**对比方式**:逐个读取三个版本的跳过文件,辅助开发者判断是否需要合入上游改动。关注点: +- 上游改了什么(对比 `current/` vs `repo/`) +- 开发者改了什么(对比 `current/` vs 项目根目录) +- 两者是否冲突 + +**清理方式**:下次运行 `admin:update` 时会自动清理 `runtime/update/` 目录,或手动删除。 + ## 5. 参数参考 | 参数 | 可选值 | 说明 | @@ -106,6 +129,7 @@ php think admin:update --dry-run | `--optional-conflict` | `skip` / `overwrite` / `ask` | 可选文件(app/config/route/extend/think/根目录文件)的冲突处理策略 | | `--force-conflict` | `overwrite` / `skip` / `ask` | 强制文件(extend/base/public/database/think)的冲突处理策略 | | `--show` | `all` / `conflict` | 变更输出范围:all 显示全部文件,conflict 只显示冲突文件 | +| `--keep-repo` | (无值) | 预览模式下保留上游克隆目录(runtime/update/current/ 和 runtime/update/repo/),便于手动对比跳过的冲突文件 | | `--reinstall` | (无值) | 即使当前版本已是最新,也强制重新安装代码 | | `--update-master` | (无值) | 更新到 master 分支而非最新 tag | @@ -143,6 +167,7 @@ php think admin:update --optional-conflict=skip --force-conflict=skip --show=all - **永远不要使用 `ask` 模式**。agent 无法处理交互式确认提示(`$output->confirm()`),会导致命令挂起 - **不要省略冲突策略参数**。不传参数时,如果存在冲突文件,命令会回退到交互模式,同样会挂起 +- **`--keep-repo` 仅在预览模式下有效**:正式执行时自动忽略,不会保留目录 ## 7. 冲突处理指引 diff --git a/extend/base/admin/service/AdminUpdateServiceBase.php b/extend/base/admin/service/AdminUpdateServiceBase.php index 12b796e..c1f8fd1 100644 --- a/extend/base/admin/service/AdminUpdateServiceBase.php +++ b/extend/base/admin/service/AdminUpdateServiceBase.php @@ -30,6 +30,7 @@ class AdminUpdateServiceBase public $optionalConflict = null; // null=未指定(走交互), skip|overwrite|ask public $forceConflict = null; // null=未指定(走交互), overwrite|skip|ask public $showScope = null; // null=未指定(默认all), all|conflict + public $keepRepo = false; // dry-run模式下保留上游克隆目录 public $skippedConflictFiles = []; // [file_path => ['type' => 'add|delete|update', 'category' => 'optional|force']] /** @@ -351,7 +352,14 @@ class AdminUpdateServiceBase if (empty($need_process_files)) { $output->writeln('没有需要更新的文件'); - $this->cleanWorkpaceDir(); + + // 即使没有需要处理的文件,如果有跳过的冲突文件且keepRepo,保留目录并输出摘要 + if ($this->dryRun && $this->keepRepo && !empty($this->skippedConflictFiles)) { + $this->outputSkippedFilesSummary($this->skippedConflictFiles, $output); + $this->outputKeepRepoPaths($current_version_dir, $last_version_dir, $now_dir, $output); + } else { + $this->cleanWorkpaceDir(); + } return; } @@ -470,6 +478,11 @@ class AdminUpdateServiceBase $output->writeln("风险评估: 强制冲突 {$force_conflict_count}个(高风险) | 可选冲突 {$optional_conflict_count}个(中风险) | 无冲突变更 {$no_conflict_count}个(低风险)"); } + // 跳过文件汇总输出(独立于 --show 参数和 need_process_files) + if ($this->dryRun && !empty($this->skippedConflictFiles)) { + $this->outputSkippedFilesSummary($this->skippedConflictFiles, $output); + } + // 非 dry-run 模式:仅在显式传了 --show 时输出变更摘要 if (!$this->dryRun && $this->showScope !== null && !empty($need_process_files)) { $showScope = $this->showScope ?: 'all'; @@ -530,7 +543,11 @@ class AdminUpdateServiceBase } } - $this->cleanWorkpaceDir(); + if ($this->dryRun && $this->keepRepo) { + $this->outputKeepRepoPaths($current_version_dir, $last_version_dir, $now_dir, $output); + } else { + $this->cleanWorkpaceDir(); + } } protected function showPostUpdateGuidance(array $need_process_files, Output $output): void @@ -641,6 +658,28 @@ class AdminUpdateServiceBase return false; } + protected function outputSkippedFilesSummary(array $skippedFiles, Output $output): void + { + $output->writeln(''); + $output->writeln('跳过的冲突文件(' . count($skippedFiles) . '个):'); + foreach ($skippedFiles as $file_path => $info) { + $category = $info['category']; + $output->writeln(' ' . $file_path . ' [' . $category . ']'); + $output->writeln(' 对比: diff runtime/update/current/' . $file_path . ' runtime/update/repo/' . $file_path); + $output->writeln(' 开发者版本: ' . $file_path); + } + } + + protected function outputKeepRepoPaths(string $currentDir, string $repoDir, string $nowDir, Output $output): void + { + $output->writeln(''); + $output->writeln('已保留更新目录(预览模式 --keep-repo):'); + $output->writeln(' 当前版本原始代码: ' . $currentDir); + $output->writeln(' 上游最新代码: ' . $repoDir); + $output->writeln(' 开发者代码: ' . $nowDir); + $output->writeln(' 目录将在下次更新时自动清理,或手动删除 runtime/update/'); + } + protected function cleanWorkpaceDir() { $dir = App::getRuntimePath() . '/update/'; diff --git a/extend/base/common/command/admin/UpdateBase.php b/extend/base/common/command/admin/UpdateBase.php index 217ef16..27d411b 100644 --- a/extend/base/common/command/admin/UpdateBase.php +++ b/extend/base/common/command/admin/UpdateBase.php @@ -28,6 +28,7 @@ class UpdateBase extends Command ->addOption('optional-conflict', null, Option::VALUE_OPTIONAL, '可选文件冲突处理策略: skip|overwrite|ask') ->addOption('force-conflict', null, Option::VALUE_OPTIONAL, '强制文件冲突处理策略: overwrite|skip|ask') ->addOption('show', null, Option::VALUE_OPTIONAL, '变更输出范围: all|conflict') + ->addOption('keep-repo', null, Option::VALUE_NONE, '预览模式下保留上游克隆目录,便于手动对比跳过的文件') ->setDescription('更新系统代码'); } @@ -49,6 +50,7 @@ class UpdateBase extends Command $update_service->optionalConflict = $input->getOption('optional-conflict') ?: null; $update_service->forceConflict = $input->getOption('force-conflict') ?: null; $update_service->showScope = $input->getOption('show') ?: null; + $update_service->keepRepo = (bool)$input->getOption('keep-repo'); $update_service->update(); }