diff --git "a/.github/AI\344\273\273\345\212\241\345\210\206\345\267\245.md" "b/.github/AI\344\273\273\345\212\241\345\210\206\345\267\245.md" new file mode 100644 index 0000000..a1edb6d --- /dev/null +++ "b/.github/AI\344\273\273\345\212\241\345\210\206\345\267\245.md" @@ -0,0 +1,128 @@ +# AI 任务分工 + +DotNetCampus Terminal 是一个基于 .NET 9.0 和 Consolonia 的远程设备连接管理工具。 + +## 协作规则 + +1. 🔥 **开始任务前必读**: 先阅读对应角色的经验总结文档 (见下方各角色链接) +2. 完成任务时更新进度状态 ✅ +3. 重要技术经验记录到 `.github/knowledge/` 知识库 +4. 大任务拆分成小任务,便于其他 AI 协助 +5. 详细技术文档查看 `.github/knowledge/` 目录 + +## AI 角色分工 + +### 配置管理专家 (Configuration AI) +**📖 必读文档**: `.github/knowledge/AI协作经验/角色经验总结/配置管理专家-核心经验总结.md` +- ✅ JSON配置系统实现(替代TOML) +- ✅ 配置解析和数据模型 +- ✅ 个人设备配置存储(JSON源保存功能) +- ✅ UI保存按钮绑定和命令实现 +- ✅ 设备唯一标识符优化(避免改名导致重复保存) +- ✅ AOT兼容性配置系统(JSON+源生成器) +- ✅ JSON配置系统用户体验优化(注释支持、减少Attribute、重命名改进、移除无用属性) +- ✅ TOML配置系统清理完成 +- [ ] 配置迁移工具实现 +- [ ] 团队配置同步 +- [ ] 配置加密存储 + +### 文件同步工程师 (File Sync AI) +**📖 必读文档**: `.github/knowledge/AI协作经验/角色经验总结/文件同步工程师-核心经验总结.md` +- ✅ 基于SFTP的文件同步 +- ✅ 同步状态监控和UI集成 +- ✅ 远程到本地同步 +- ✅ 增量同步优化 +- ✅ 错误处理优化和诊断信息改进 +- ✅ 错误消息格式优化(解决双冒号问题) +- ✅ "没有启用的目录同步"UI错误提示优化 + +### UI界面设计师 (UI Designer AI) +**📖 必读文档**: `.github/knowledge/AI协作经验/角色经验总结/UI界面设计师-核心经验总结.md` +- ✅ 主界面布局和SSH设备编辑界面 +- ✅ 数据绑定和MVVM模式 +- ✅ 同步状态显示和全局进度条 +- ✅ 最近同步时间显示和错误消息绑定 +- ✅ 新Shell标签页打开功能(解决TUI应用中的Shell冲突问题) +- ✅ SshRemoteDeviceInfoViewModel重构(拆分为同步和命令子ViewModels) +- ✅ 建立ViewModels和Views的文件夹重构规范 +- ✅ 删除设备功能(使用交互式命令模式实现) +- ✅ SshRemoteDeviceInfoView.axaml重构(从208行拆分为31行主View + 3个子TabViews) +- ✅ 一键部署SSH密钥对功能的界面和ViewModel实现 +- [ ] 路径省略功能 +- [ ] 交互优化 + +### SSH连接专家 (SSH Connection Expert AI) +**📖 必读文档**: `.github/knowledge/AI协作经验/角色经验总结/SSH连接专家-核心经验总结.md` +- ✅ SSH密钥认证配置指南 +- ✅ 多设备连接安全性分析 +- ✅ SSH.NET封装和连接管理 +- ✅ 一键部署SSH密钥对功能(与UI设计师协作完成) +- [ ] SSH连接池优化 +- [ ] 断线重连机制 +- [ ] SSH隧道和端口转发支持 + +### 文档维护员 (Documentation AI) +**📖 必读文档**: `.github/knowledge/AI协作经验/角色经验总结/文档维护员-核心经验总结.md` +- ✅ 简化协作文档,提升AI协作效率 +- ✅ 知识库分类重组(`.github/knowledge/` 目录结构优化) +- ✅ 项目文档维护和技术标准制定 +- ✅ AI角色权限和职责优化 +- ✅ Copilot指令文档完善 +- ✅ SyncGroup术语重命名(同步组→目录同步,SyncGroupConfiguration→DirectorySyncingModel) +- 🔄 **TOML文档清理项目** (2025年7月11日) + - ⚠️ **重要说明**: 迁移历史只保留一篇 (`TOML到JSON配置系统迁移记录.md`),其他文档**完全清理**所有TOML字眼 + - ✅ **任务1-技术设计文档清理** (分配给: 文档维护员同事A) ✅ + - `技术设计文档/配置管理/02-配置保存功能实现.md` - **完全清理**TOML引用 ✅ + - `技术设计文档/配置管理/03-配置数据源迁移方案.md` - **完全清理**TOML引用 ✅ + - `技术设计文档/配置管理/04-设备唯一标识符设计.md` - **完全清理**TOML示例 ✅ + - `技术设计文档/配置管理/01-JSON配置系统架构设计.md` - **完全清理**TOML历史部分 ✅ + - ✅ **任务2-AI协作经验清理** (分配给: 文档维护员同事B) ✅ + - `AI协作经验/角色经验总结/配置管理专家-核心经验总结.md` - **完全清理**TOML代码示例 ✅ + - `AI协作经验/角色经验总结/DevOps自动化专家-核心经验总结.md` - **完全清理**TOML文件路径引用 ✅ + - `AI协作经验/实现经验总结/设备唯一ID实现技术总结.md` - **完全清理**TOML示例 ✅ + - ✅ **任务3-依赖库和问题文档清理** (分配给: 文档维护员同事C) ✅ + - `依赖库文档/System.Text.Json/01-JSON配置系统使用指南.md` - **完全清理**TOML迁移代码示例 ✅ + - `问题排查/开发问题快速解答手册.md` - **完全清理**TOML历史问题说明 ✅ + - ⚠️ **发现遗漏任务** (2025年7月15日验收发现) + - `技术设计文档/SSH连接管理/01-SSH密钥认证配置方案.md` - **完全清理**TOML配置示例 ✅ + - `技术设计文档/SSH连接管理/02-多设备连接安全分析.md` - **完全清理**所有TOML代码块 ✅ +- ✅ **终端模拟器技术文档** (2025年7月17日) + - `技术设计文档/终端模拟器/01-终端模拟器核心实现要点.md` - 详细分析终端模拟器实现的所有技术要点 ✅ + - `技术设计文档/终端模拟器/02-DotNetCampus Terminal终端集成方案.md` - 针对项目具体需求的实现方案 ✅ + - `技术设计文档/终端模拟器/03-集成现有终端功能的实现方案.md` - 分析集成现有终端功能的四种方案,大幅降低开发成本 ✅ +- [ ] 自动化文档生成工具 +- [ ] 多语言文档支持 +- [ ] API文档自动更新 + +### DevOps 自动化专家 (DevOps Automation Expert AI) +**📖 必读文档**: `.github/knowledge/AI协作经验/角色经验总结/DevOps自动化专家-核心经验总结.md` +- ✅ GitHub Actions CI/CD 流水线设计和实现 +- ✅ 跨平台应用程序自动化构建(Windows/Linux/macOS) +- ✅ 自动化发布和版本管理 +- ✅ 构建产物自动化打包和分发 +- ✅ GitHub Releases 自动化发布 +- ✅ 代码质量检查和自动化测试集成 +- ✅ 依赖项安全扫描和更新 +- [ ] 性能基准测试自动化 +- [ ] 部署环境配置和基础设施即代码 + +### 其他模块 +- 进程管理专家:Shell启动和进程管理 +- Windows连接专家:Windows远程连接 +- 测试工程师:测试框架和自动化 +- 知识学习者:技术文档和最佳实践 + +## 协作流程 + +1. **开始任务前必读** 📖 对应角色的经验总结文档(见各角色的必读文档链接) +2. **技术文档查阅** `.github/knowledge/` 相关技术文档 +3. **接口设计** 优先,确保模块间依赖清晰 +4. **及时测试** 每个模块完成后立即编译验证 +5. **知识共享** 将技术问题和解决方案更新到知识库和经验总结 +6. **进度同步** 接口变更时通知相关AI + +## 重要提醒 + +- 详细的编码规范、Consolonia使用指南等技术文档已整理到 `.github/knowledge/` 目录 +- 遇到技术问题先查阅知识库,避免重复踩坑 +- 复杂问题及时寻求人类帮助,避免AI陷入错误循环 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d47fc59 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,150 @@ +# GitHub Copilot 指导文档 + +DotNetCampus Terminal 项目的 AI 协作开发指南。 + +## 项目概述 + +基于 .NET 9.0 和 Consolonia 的远程设备连接管理工具,采用多AI协同开发模式。 + +**🤖 AI角色自动认领机制**:如果用户在提示词中指定了AI的角色(如"你是文档维护员"、"你是UI界面设计师"等),AI应该: +1. 立即查看 `.github/AI任务分工.md` 文件认领对应职位 +2. 阅读该角色的必读文档(角色经验总结文档) +3. 按照职位要求和经验总结执行后续行动 +4. 遵循该角色的技术规范和工作流程 + +**技术栈**:.NET 9.0, Consolonia, SSH.NET, System.Text.Json, DotNetCampus.Logger + +## 核心编码规范 + +### 命名约定 +- 类名/方法名/属性名:PascalCase +- 私有字段:camelCase + 下划线前缀 `_fieldName` +- 接口:以I开头 + +### 代码风格 +- 启用 nullable 引用类型 +- 异步方法以 Async 结尾 +- 使用依赖注入 +- 添加 XML 文档注释 + +### 常见错误提醒 ⚠️ +**以下是AI同事经常犯的错误,必须避免**: +1. **日志命名空间**:使用 `DotNetCampus.Logging` 而不是 `DotNetCampus.Logger`(前者是命名空间,后者是库名称) +2. **日志方法名**:使用 `Log.Warn` 而不是 `Log.Warning` +3. **编译文件占用**:`dotnet build` 提示文件被占用时,这是正常现象(VS Code的bug),不要报错 +4. **SSH.NET引用**:确保添加 `using Renci.SshNet;` + +### 兼容性原则 +**重要:本项目不需要考虑兼容性问题** +- 不使用 `[Obsolete]` 标记 +- 不保留旧方法或接口 +- 不添加"兼容旧版本"等注释说明 +- 直接重构和替换,无需向后兼容 +- 项目处于开发阶段,API可以自由变更 + +### 代码重构原则 +- **400行规则**:代码超过400行时,需要酌情考虑重构 +- **600行硬限制**:代码超过600行时,必须考虑重构 +- **例外情况**:超过600行但非常单一易懂的代码(如大型枚举)可以保留 +- 重构方式:拆分类、提取方法、分离职责 + +## Consolonia 关键要点 + +### 核心概念 +- **像素 = 字符**:每个像素对应一个控制台字符 +- **文件扩展名**:使用 `.axaml` 而不是 `.xaml` +- **命名空间**:`xmlns:console="https://github.com/jinek/consolonia"` +- **主题**:推荐使用 `TurboVisionDarkTheme` + +### 重要差异 +- 使用 `AvaloniaList` 替代 `ObservableCollection` +- 使用 `console:LineBrush` 配置边框样式 +- 按钮禁用阴影:`console:ButtonExtensions.Shadow="False"` +- 异步UI更新:`Dispatcher.UIThread.InvokeAsync` + +### 性能优化 +- 使用 `VirtualizingStackPanel` 处理大数据集 +- 选择合适的绑定模式(OneTime/OneWay/TwoWay) +- 使用 `x:DataType` 实现强类型绑定 + +## 日志规范 + +使用 **DotNetCampus.Logger**: +- 使用静态 `Log` 类,无需依赖注入 +- 日志格式:`Log.Info("[标签] 消息内容")` +- 标签约定:`[FileSync]` `[SSH]` `[UI]` `[Config]` `[Network]` `[System]` +- **常用方法**:`Log.Info()`, `Log.Warn()`, `Log.Error()` + +## 协作要点 + +### 开发流程 +1. 🔥 **首要步骤**:查看 `.github/AI任务分工.md` 确定自己的角色 +2. 📖 **必读文档**:阅读对应角色的经验总结文档(详见下方技术文档索引) +3. 📚 **技术查阅**:查看 `.github/knowledge/` 相关技术文档 +4. 接口设计优先,确保模块依赖清晰 +5. 及时测试,避免积累错误 +6. 知识更新到知识库和经验总结,便于复用 + +### 求助时机 +以下情况建议寻求人类帮助: +- 多个命名空间冲突 +- API版本兼容性问题 +- 复杂的泛型推断失败 +- 平台特定显示问题 +- **反复犯错**:如果发现自己在重复犯同样的错误 + +**注意**:`dotnet build` 文件占用问题是VS Code的已知bug,属于正常现象,无需求助。 + +## 技术文档索引 + +详细的技术资料已整理到 `.github/knowledge/` 目录: + +### 角色经验总结(首要阅读) +- `UI界面设计师-核心经验总结.md` - UI设计师核心经验 +- `文件同步工程师-核心经验总结.md` - 文件同步工程师核心经验 +- `配置管理专家-核心经验总结.md` - 配置管理专家核心经验 +- `SSH连接专家-核心经验总结.md` - SSH连接专家核心经验 +- `文档维护员-核心经验总结.md` - 文档维护员核心经验 + +### 技术参考文档 +详细的技术资料已按照以下结构组织,AI可根据具体技术需求选择相应文档: + +#### 依赖库使用指南 +- `依赖库文档/Consolonia/01-快速参考指南.md` - Consolonia快速参考 +- `依赖库文档/Consolonia/02-架构核心要点.md` - 架构设计要点 +- `依赖库文档/Consolonia/03-UI框架使用.md` - UI框架详细使用 +- `依赖库文档/Consolonia/04-UI设计模式最佳实践.md` - UI设计模式 +- `依赖库文档/DotNetCampus.Logger/01-日志框架使用指南.md` - 日志使用指南 +- `依赖库文档/SSH.NET/01-基础使用指南.md` - SSH.NET基础使用 +- `依赖库文档/SSH.NET/02-文件同步实现.md` - 文件同步指南 +- `依赖库文档/System.Text.Json/01-JSON配置系统使用指南.md` - JSON配置系统使用 +- `依赖库文档/DotNet9/01-新特性在项目中的应用.md` - .NET 9新特性 + +#### 技术设计文档 +- `技术设计文档/界面设计/01-Terminal界面开发指南.md` - Terminal界面开发 +- `技术设计文档/界面设计/02-SSH设备信息视图设计.md` - SSH设备视图设计 +- `技术设计文档/界面设计/03-进度显示和数据绑定.md` - 进度显示设计 +- `技术设计文档/界面设计/04-ViewModel重构最佳实践.md` - ViewModel重构 +- `技术设计文档/界面设计/05-TUI与Shell集成解决方案.md` - Shell集成方案 +- `技术设计文档/界面设计/06-交互式命令模式设计.md` - 交互式命令设计 +- `技术设计文档/配置管理/01-JSON配置系统架构设计.md` - 配置架构设计 +- `技术设计文档/配置管理/02-配置保存功能实现.md` - 配置保存实现 +- `技术设计文档/配置管理/03-配置数据源迁移方案.md` - 配置迁移方案 +- `技术设计文档/配置管理/04-设备唯一标识符设计.md` - 设备ID设计 +- `技术设计文档/文件同步/01-远程到本地同步架构.md` - 同步架构设计 +- `技术设计文档/文件同步/02-增量同步性能优化.md` - 同步性能优化 +- `技术设计文档/文件同步/03-同步错误处理机制.md` - 错误处理机制 +- `技术设计文档/SSH连接管理/01-SSH密钥认证配置方案.md` - SSH密钥配置 +- `技术设计文档/SSH连接管理/02-多设备连接安全分析.md` - 连接安全分析 + +#### 问题排查指南 +- `问题排查/开发问题快速解答手册.md` - 问题解决方案 + +#### AI协作经验 +- `AI协作经验/实现经验总结/设备唯一ID实现技术总结.md` - 设备ID实现经验 +- `AI协作经验/AI多角色协作开发经验.md` - 多角色协作经验 + +**重要提醒**: +1. 🔥 开始任务前,必须先阅读对应角色的经验总结文档 +2. 📚 遇到技术问题先查阅知识库,避免重复踩坑 +3. 💡 将新的踩坑经验及时更新到经验总结文档中 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/AI\345\244\232\350\247\222\350\211\262\345\215\217\344\275\234\345\274\200\345\217\221\347\273\217\351\252\214.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/AI\345\244\232\350\247\222\350\211\262\345\215\217\344\275\234\345\274\200\345\217\221\347\273\217\351\252\214.md" new file mode 100644 index 0000000..48eeeb7 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/AI\345\244\232\350\247\222\350\211\262\345\215\217\344\275\234\345\274\200\345\217\221\347\273\217\351\252\214.md" @@ -0,0 +1,121 @@ +# AI 协作开发经验教训 + +## 概述 + +本文档记录了在 DotNetCampus Terminal 项目中多 AI 协作开发过程中总结的经验教训,帮助提高协作效率和代码质量。 + +## 开发过程中的经验教训 + +### 2025年7月8日 - 技术栈迁移阶段 + +#### 经验总结 + +1. **技术栈变更管理** + - **问题**: 项目从 Terminal.Gui 迁移到 Consolonia,旧的知识库需要清理 + - **教训**: 技术栈变更需要系统性地清理相关文档和配置 + - **改进**: 建立技术栈变更checklist,确保所有相关文档都得到更新 + +2. **文档维护的重要性** + - **问题**: 旧的技术文档可能误导后续开发 + - **教训**: 文档维护需要及时跟进技术变更 + - **改进**: 指定专门的文档维护员角色,负责文档的一致性 + +### 2025年7月4日 - Terminal.Gui 知识学习阶段(已迁移) + +#### 经验总结 + +1. **知识文档管理** + - **问题**: 创建了重复的文档(`terminal-gui-guide.md` 和 `Terminal.Gui-使用指南.md`) + - **教训**: 在创建新文档前,应该先检查是否已有类似文档 + - **改进**: 建立统一的文件命名规范,使用中文命名更清晰 + +2. **编译错误处理策略** + - **问题**: AI 在遇到复杂编译错误时容易陷入重复尝试的循环 + - **教训**: 应该更早地识别需要人类帮助的情况 + - **改进**: 建立明确的求助触发条件,避免无效迭代 + +3. **知识积累方式** + - **问题**: 刚开始没有系统性地记录遇到的问题和解决方案 + - **教训**: 应该在开发过程中实时记录经验,而不是事后总结 + - **改进**: 建立"边开发边记录"的习惯 + +4. **文档结构优化** + - **问题**: `copilot-instructions.md` 文件过长,不利于快速查阅 + - **教训**: 主要文档应该保持简洁,详细内容应该分散到专门的知识库中 + - **改进**: 采用"总纲+详细文档"的结构,通过链接引导到具体内容 + +#### 技术层面的发现(已过期) + +*注意:以下内容关于 Terminal.Gui 的技术细节已过期,项目现已迁移到 Consolonia* + +1. **Terminal.Gui v2 的变化** + - 命名空间结构发生了重大变化,不能使用 v1 的经验 + - `MenuBarv2` 替代了旧版 `MenuBar` + - 事件处理模型有所调整(`KeyDown` 替代 `Accept`) + +2. **编译错误的模式** + - 命名空间冲突是最常见的问题 + - 泛型推断失败通常是类型不明确导致的 + - 事件绑定错误往往是 API 版本差异导致的 + +3. **最有效的调试方法** + - 使用 `MessageBox.Query` 进行快速调试 + - 分层验证:先基础控件,再复杂布局,最后事件处理 + - 渐进式开发比一次性完成更可靠 + +#### 协作流程的改进 + +1. **人机协作的边界** + - **AI 适合**: 模式化的代码实现、文档整理、知识积累 + - **人类适合**: 复杂问题诊断、架构决策、跨模块协调 + - **关键**: 及时识别问题的复杂度,避免AI在困难问题上浪费时间 + +2. **知识传承机制** + - 建立系统性的知识库,按技术栈分类 + - 重要经验要同时记录在主文档和详细文档中 + - 使用统一的文档格式和命名规范 + +3. **任务分工的优化** + - 知识学习者应该先建立技术基础,再进行实际开发 + - 实际开发过程中发现的问题要及时反馈到知识库 + - 不同AI之间应该有明确的依赖关系和沟通机制 + +## 最佳实践总结 + +### 1. 开发前准备 +- 查阅相关技术的知识库文档 +- 了解依赖模块的接口状态 +- 制定渐进式的开发计划 + +### 2. 开发过程中 +- 遇到编译错误先查知识库 +- 复杂问题及时求助人类开发者 +- 实时记录新发现的问题和解决方案 + +### 3. 开发完成后 +- 更新相关的知识库文档 +- 总结开发过程中的经验教训 +- 通知依赖此模块的其他AI + +### 4. 知识管理 +- 使用统一的文档命名规范 +- 保持主文档简洁,详细内容分散到专门文档 +- 定期检查和清理重复或过时的文档 + +## 未来改进方向 + +1. **自动化工具** + - 考虑建立自动的编译错误诊断工具 + - 实现知识库的自动索引和搜索功能 + +2. **协作机制** + - 建立更正式的AI间通信协议 + - 实现进度跟踪和依赖管理系统 + +3. **质量保证** + - 建立代码审查流程 + - 实现自动化测试和持续集成 + +--- + +**注意**: 这个文档应该持续更新,每次重要的协作经验都应该记录在这里,形成团队的集体智慧。 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/TOML\345\210\260JSON\351\205\215\347\275\256\347\263\273\347\273\237\350\277\201\347\247\273\350\256\260\345\275\225.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/TOML\345\210\260JSON\351\205\215\347\275\256\347\263\273\347\273\237\350\277\201\347\247\273\350\256\260\345\275\225.md" new file mode 100644 index 0000000..ee228bc --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/TOML\345\210\260JSON\351\205\215\347\275\256\347\263\273\347\273\237\350\277\201\347\247\273\350\256\260\345\275\225.md" @@ -0,0 +1,131 @@ +# TOML到JSON配置系统迁移记录 + +## 迁移概述 + +DotNetCampus Terminal 项目已于 2025年7月 完成从 TOML 配置系统到 JSON 配置系统的完整迁移。 + +## 技术决策演进 + +### 配置系统发展历程 +1. **Debug阶段**:硬编码配置,用于开发调试 +2. **TOML阶段**:使用 `Samboy063.Tomlet` 库,人类可读的配置格式 +3. **JSON阶段**:使用 `System.Text.Json` + 源生成器,AOT兼容 + +## 迁移原因 + +### TOML 系统的问题 +- 🔥 **AOT兼容性问题**:`Tomlet` 库依赖反射,无法在 AOT 环境下工作 +- **性能考虑**:JSON 解析性能更优 +- **生态系统**:.NET 内置支持,无需额外依赖 + +### JSON 系统的优势 +- ✅ **AOT 兼容**:完全支持源生成器 +- ✅ **高性能**:System.Text.Json 优化的序列化性能 +- ✅ **零依赖**:.NET 内置库,无需外部包 +- ✅ **类型安全**:编译时验证和强类型支持 + +## 迁移范围 + +### 已清理的 TOML 组件 +- ✅ **包引用**:移除 `Samboy063.Tomlet` +- ✅ **配置文件**:删除 `devices.toml` +- ✅ **源代码**:删除 `TomlSource` 目录 +- ✅ **模型重命名**:`TomlDeviceConfiguration.cs` → `SyncModels.cs` + +### 已实现的 JSON 组件 +- ✅ **配置源**:`JsonRemoteDeviceConfigurationSource` +- ✅ **数据模型**:`DeviceConfigurationModel` + 源生成器 +- ✅ **配置文件**:`devices.json` (AOT兼容格式) + +## 技术架构对比 + +### TOML 架构 (已废弃) +``` +TomlRemoteDeviceConfigurationSource +├── Samboy063.Tomlet (第三方库) +├── TomlDeviceConfiguration (数据模型) +└── devices.toml (配置文件) +``` + +### JSON 架构 (当前) +``` +JsonRemoteDeviceConfigurationSource +├── System.Text.Json (内置库) +├── DeviceConfigurationModel (数据模型) +├── JsonSourceGenerationContext (源生成器) +└── devices.json (配置文件) +``` + +## 配置格式对比 + +### TOML 格式示例 (历史格式) +```toml +[devices.server-01] +name = "开发服务器" +host = "192.168.1.100" +port = 22 +username = "root" + +[[devices.server-01.sync_groups]] +name = "项目代码" +remote_path = "/var/www" +local_path = "C:\\Projects" +enabled = true +``` + +### JSON 格式 (当前格式) +```json +{ + "Devices": [ + { + "LocalId": "server-01", + "Name": "开发服务器", + "Host": "192.168.1.100", + "Port": 22, + "Username": "root", + "DirectorySyncings": [ + { + "RemotePath": "/var/www", + "LocalPath": "C:\\Projects", + "IsEnabled": true + } + ] + } + ] +} +``` + +## 迁移经验总结 + +### 成功经验 +- **渐进式迁移**:先实现 JSON 系统,确保功能完整后再清理 TOML +- **数据保持**:确保用户配置在迁移过程中不丢失 +- **向前兼容**:JSON 系统设计时考虑了未来扩展性 + +### 技术难点 +- **AOT 约束**:需要使用源生成器替代反射 +- **数据转换**:TOML 到 JSON 的字段映射 +- **错误处理**:配置解析异常的统一处理 + +## 文档清理记录 + +### 2025年7月11日 - 文档维护员清理行动 +**清理原则**: +- 保留一份迁移历史记录(本文档) +- 清理其他文档中的 TOML 引用 +- 更新技术栈描述为 JSON 系统 + +**清理任务分工**: +- **任务1**:技术设计文档清理 (4个文件) +- **任务2**:AI协作经验清理 (3个文件) +- **任务3**:依赖库和问题文档清理 (2个文件) + +## 参考资料 + +- [JSON配置系统架构设计](../技术设计文档/配置管理/01-JSON配置系统架构设计.md) +- [System.Text.Json使用指南](../依赖库文档/System.Text.Json/01-JSON配置系统使用指南.md) +- [配置管理专家核心经验](../角色经验总结/配置管理专家-核心经验总结.md) + +--- + +> 📝 **文档说明**:本文档记录了 TOML 到 JSON 配置系统的完整迁移过程,作为项目技术决策的历史记录保存。TOML 相关代码已全部清理完成,当前项目完全基于 JSON 配置系统。 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\344\273\266\346\227\266\351\227\264\346\210\263\345\220\214\346\255\245\345\256\236\347\216\260\350\256\260\345\275\225.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\344\273\266\346\227\266\351\227\264\346\210\263\345\220\214\346\255\245\345\256\236\347\216\260\350\256\260\345\275\225.md" new file mode 100644 index 0000000..0abfc76 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\344\273\266\346\227\266\351\227\264\346\210\263\345\220\214\346\255\245\345\256\236\347\216\260\350\256\260\345\275\225.md" @@ -0,0 +1,260 @@ +# 文件时间戳同步实现技术记录 + +## 问题描述 + +在实际使用文件同步功能时发现的关键问题: + +### 现象 +1. **时间戳不匹配**:远程设备上的文件时间显示为同步时间,而非原始文件的修改时间 +2. **增量同步失效**:`IncrementalSyncComparator` 中的 `localFile.LastWriteTime > remoteFile.LastWriteTime` 比较始终为真 +3. **人工确认困难**:远程设备上很难通过文件时间判断是否是新同步的文件 + +### 根本原因 +SFTP 文件传输过程中,SSH.NET 的 `UploadFile` 和 `DownloadFile` 方法会将目标文件的时间戳设置为传输时间,而非保持原始文件的时间戳。 + +## 解决方案设计 + +### 核心思路 +在文件传输完成后,立即同步文件的时间戳信息,确保两端文件的时间戳保持一致。 + +### 技术实现 + +#### 1. 新增时间戳同步方法 + +在 `SftpOperationHelper` 类中新增两个方法: + +```csharp +/// +/// 同步远程文件的时间戳到本地文件的时间戳 +/// 用于本地到远程同步完成后 +/// +public void SyncRemoteFileTimestamps(SftpClient client, string remoteFilePath, string localFilePath) + +/// +/// 同步本地文件的时间戳到远程文件的时间戳 +/// 用于远程到本地同步完成后 +/// +public void SyncLocalFileTimestamps(SftpClient client, string remoteFilePath, string localFilePath) +``` + +#### 2. 多重降级策略 + +由于不同SSH服务器对时间戳修改的支持程度不同,采用多重降级策略: + +**方法1:SetAttributes(推荐)** +```csharp +var remoteAttributes = client.GetAttributes(remoteFilePath); +remoteAttributes.LastWriteTime = localFileInfo.LastWriteTime; +remoteAttributes.LastAccessTime = localFileInfo.LastWriteTime; +client.SetAttributes(remoteFilePath, remoteAttributes); +``` + +**方法2:SetLastWriteTime(备用)** +```csharp +client.SetLastWriteTime(remoteFilePath, localFileInfo.LastWriteTime); +``` + +#### 3. 时间戳比较优化 + +在 `IncrementalSyncComparator` 中引入时间戳容差机制: + +```csharp +private const int TimestampToleranceSeconds = 2; + +private static bool IsSignificantlyDifferent(DateTimeOffset time1, DateTimeOffset time2) +{ + var timeDifference = Math.Abs((time1 - time2).TotalSeconds); + return timeDifference > TimestampToleranceSeconds; +} +``` + +修改文件比较逻辑: +```csharp +if (localFile.Size != remoteFile.Size || + (localFile.LastWriteTime > remoteFile.LastWriteTime && + IsSignificantlyDifferent(localFile.LastWriteTime, remoteFile.LastWriteTime))) +``` + +## 实施过程 + +### 修改的文件 + +1. **SftpOperationHelper.cs** + - 新增 `SyncRemoteFileTimestamps` 方法 + - 新增 `SyncLocalFileTimestamps` 方法 + - 新增 `VerifyRemoteFileTimestamp` 私有验证方法 + +2. **LocalToRemoteSyncOperation.cs** + - 在文件上传完成后调用 `SyncRemoteFileTimestamps` + +3. **RemoteToLocalSyncOperation.cs** + - 在文件下载完成后调用 `SyncLocalFileTimestamps` + - 添加 `SftpOperationHelper` 依赖 + +4. **IncrementalSyncComparator.cs** + - 新增时间戳容差常量 + - 新增 `IsSignificantlyDifferent` 方法 + - 优化文件比较逻辑 + +### 关键代码位置 + +**本地到远程同步**(LocalToRemoteSyncOperation.cs): +```csharp +// 上传文件 +client.UploadFile(fileStream, remoteFilePath, true, progress => { ... }); + +// 同步文件时间戳,确保远程文件的修改时间与本地文件一致 +_sftpHelper.SyncRemoteFileTimestamps(client, remoteFilePath, localFile); +``` + +**远程到本地同步**(RemoteToLocalSyncOperation.cs): +```csharp +// 下载文件 +client.DownloadFile(remoteFile, fileStream, progress => { ... }); + +// 同步文件时间戳,确保本地文件的修改时间与远程文件一致 +_sftpHelper.SyncLocalFileTimestamps(client, remoteFile, localFilePath); +``` + +## 调试和验证 + +### 调试信息输出 +增加详细的调试日志来跟踪时间戳同步过程: + +```csharp +Log.Debug($"[FileSync] 同步前远程文件时间戳: {remoteFilePath} (修改时间: {beforeSync.LastWriteTime})"); +Log.Debug($"[FileSync] 本地文件时间戳: {localFilePath} (修改时间: {localFileInfo.LastWriteTime})"); +// 执行同步操作 +Log.Debug($"[FileSync] 已同步文件时间戳到远程(SetAttributes): {remoteFilePath} (修改时间: {localFileInfo.LastWriteTime})"); +``` + +### 验证机制 +实现时间戳验证方法,确保同步成功: + +```csharp +private void VerifyRemoteFileTimestamp(SftpClient client, string remoteFilePath, DateTime expectedTime) +{ + var remoteAttributes = client.GetAttributes(remoteFilePath); + var timeDifference = Math.Abs((remoteAttributes.LastWriteTime - expectedTime).TotalSeconds); + + if (timeDifference <= 2) // 允许2秒的误差 + { + Log.Debug($"[FileSync] 远程文件时间戳验证成功"); + } + else + { + Log.Warn($"[FileSync] 远程文件时间戳验证失败"); + } +} +``` + +## 潜在问题和注意事项 + +### 1. 服务器兼容性 +- 不是所有SSH服务器都支持时间戳修改 +- 某些受限环境可能禁用文件属性修改 +- 实现了多重降级策略来处理这种情况 + +### 2. 时区问题 +- SSH.NET处理时间戳时可能存在时区转换 +- 允许2秒容差来处理这种差异 + +### 3. 文件系统差异 +- 不同文件系统对时间戳精度的支持不同 +- NTFS vs ext4 vs HFS+ 等文件系统的时间精度差异 + +### 4. 性能影响 +- 每个文件传输后都要进行时间戳同步操作 +- 增加了额外的网络请求 +- 对于大量文件同步可能会有性能影响 + +## 测试建议 + +### 测试场景 +1. **基本功能测试**: + - 上传文件后检查远程文件时间戳 + - 下载文件后检查本地文件时间戳 + +2. **增量同步测试**: + - 修改本地文件并同步 + - 验证增量同步只处理真正修改的文件 + +3. **兼容性测试**: + - 测试不同SSH服务器的兼容性 + - 验证降级策略是否生效 + +4. **边界条件测试**: + - 时间戳接近的文件同步 + - 跨时区的文件同步 + +## 实测结果与经验总结 + +### ✅ **成功实现的功能** +1. **修改时间同步**:✅ 已成功实现,远程文件的 `LastWriteTime` 现在与本地文件保持一致 +2. **增量同步修复**:✅ 时间戳比较逻辑现在能正确工作 +3. **访问时间同步**:✅ 可以通过 `SetAttributes` 设置 `LastAccessTime` + +### ❌ **无法实现的功能** +1. **创建时间同步**:❌ **无法实现**,原因如下: + - SSH.NET 的 `SftpFileAttributes` 类不包含 `CreationTime` 属性 + - SFTP 协议不标准化支持创建时间(只支持 `mtime` 和 `atime`) + - 不同 SSH 服务器对创建时间的支持差异很大 + - 跨平台兼容性差(Linux 的 `ctime` ≠ Windows 的 `CreationTime`) + +### 🔍 **实测发现** +经过实际测试,以下现象得到确认: +1. **修改时间**:成功同步 ✅ +2. **创建时间**:仍为同步时间 ❌(技术限制,无法解决) +3. **访问时间**:仍为同步时间 ⚠️(可以同步但通常不必要) + +### 📋 **技术限制分析** + +#### SSH.NET 库限制 +```csharp +// SftpFileAttributes 只包含以下时间属性: +public class SftpFileAttributes +{ + public DateTime LastWriteTime { get; set; } // ✅ 支持 + public DateTime LastAccessTime { get; set; } // ✅ 支持 + // 没有 CreationTime 属性 // ❌ 不支持 +} +``` + +#### SFTP 协议限制 +- SFTP v3/v4 标准只定义了 `mtime`(修改时间)和 `atime`(访问时间) +- 创建时间不是 SFTP 标准的一部分 +- 服务器实现差异大,无法保证跨平台兼容性 + +#### 可能的替代方案及其限制 +1. **SSH 命令执行**: + ```csharp + // 理论可行但实际限制很多 + var command = sshClient.CreateCommand($"touch -t {timestamp} {filePath}"); + ``` + **限制**: + - 需要目标系统命令支持 + - 跨平台命令差异(Linux vs Windows vs macOS) + - 可能需要特殊权限 + - 可靠性差 + +2. **文件系统直接操作**: + **限制**: + - 需要系统级权限 + - 远程操作复杂 + - 安全风险高 + +### 🎯 **最终结论** + +**当前实现已达到最佳效果**: +- ✅ 修改时间同步:核心需求已满足 +- ✅ 增量同步准确性:问题已解决 +- ❌ 创建时间同步:技术不可行,属于可接受的限制 + +**建议策略**: +1. 接受创建时间无法同步的技术限制 +2. 重点关注修改时间同步(这是最重要的) +3. 如有特殊需求,考虑在应用层记录原始创建时间 + +--- +*实测更新:2025年7月16日* +*状态:功能验证完成,技术限制已确认* diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/\350\256\276\345\244\207\345\224\257\344\270\200ID\345\256\236\347\216\260\346\212\200\346\234\257\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/\350\256\276\345\244\207\345\224\257\344\270\200ID\345\256\236\347\216\260\346\212\200\346\234\257\346\200\273\347\273\223.md" new file mode 100644 index 0000000..ee7f4a9 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\345\256\236\347\216\260\347\273\217\351\252\214\346\200\273\347\273\223/\350\256\276\345\244\207\345\224\257\344\270\200ID\345\256\236\347\216\260\346\212\200\346\234\257\346\200\273\347\273\223.md" @@ -0,0 +1,113 @@ +# 设备唯一标识符实现总结 + +## 实现完成情况 + +✅ **任务已完成** - 设备唯一标识符优化功能已成功实现 + +## 功能概述 + +解决了"修改设备连接名称后点击保存会导致保存为新设备"的问题,通过双重ID机制确保设备的唯一性和可追踪性。 + +## 实现内容 + +### 1. 数据模型扩展 ✅ +- **SshRemoteDeviceInfo**: 添加了 `LocalId` (required) 和 `RemoteId` (可空) 字段 +- **SshDeviceConfiguration**: 添加了对应的 LocalId 和 RemoteId 字段 +- **数据转换**: 更新了 JsonRemoteDeviceConfigurationSource 中的转换逻辑 + +### 2. 保存逻辑重构 ✅ +- **多重匹配策略**: 实现了 LocalId → RemoteId → ConnectionName 的降级匹配逻辑 +- **FindExistingDeviceIndex**: 新的设备查找方法,优先使用LocalId精确匹配 +- **设备保存**: 更新现有设备而非创建新设备(基于LocalId匹配) + +### 3. ViewModel集成 ✅ +- **SshRemoteDeviceInfoViewModel**: 添加了 LocalId 和 RemoteId 属性 +- **自动生成**: 新设备创建时自动生成 LocalId +- **数据保持**: 编辑现有设备时 LocalId 保持不变 +- **GetCurrentDeviceInfo**: 包含新字段的设备信息生成 + +### 4. 配置文件更新 ✅ +- **JSON格式**: 手动为现有设备配置添加了 LocalId 字段 +- **兼容性**: 移除了自动生成 LocalId 的逻辑,要求配置文件中必须包含 LocalId + +## 技术实现细节 + +### LocalId 生成规则 +```csharp +private static string GenerateLocalId() +{ + return "device_" + Guid.NewGuid().ToString("N")[..16]; +} +``` + +### 设备匹配优先级 +1. **LocalId** - 精确匹配 (主要标识符) +2. **RemoteId** - 如果LocalId未找到且RemoteId非空 +3. **ConnectionName** - 兼容性降级方案 + +### JSON配置示例 +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f67890", + "remoteId": null, + "connectionName": "开发服务器", + "host": "192.168.1.100", + "port": 22, + "userName": "developer", + "password": "secret123" + } + ] +} +``` + +## 验证结果 + +### ✅ 功能验证 +- 新创建的设备自动生成LocalId +- 修改设备连接名称后保存不会创建重复设备 +- 现有JSON文件可以正常加载(已手动添加LocalId) +- 保存配置后JSON文件包含LocalId字段 + +### ✅ 兼容性验证 +- 现有配置文件已手动迁移(添加LocalId) +- 现有功能不受影响 +- 项目成功编译运行 + +### ✅ 代码质量 +- 简化了向后兼容性处理(移除自动生成LocalId逻辑) +- 代码清晰明确,要求配置文件必须包含LocalId +- 遵循了设计文档的实现方案 + +## 文件修改清单 + +### 核心文件 +- `src/DotNetCampus.Terminal/Modules/Configurations/Models/SshRemoteDeviceInfo.cs` - 数据模型 +- `src/DotNetCampus.Terminal/Modules/Configurations/Models/SyncModels.cs` - 同步模型 +- `src/DotNetCampus.Terminal/Modules/Configurations/JsonSource/JsonRemoteDeviceConfigurationSource.cs` - 配置源 +- `src/DotNetCampus.Terminal/ViewModels/SshRemoteDeviceInfoViewModel.cs` - ViewModel + +### 配置文件 +- `src/DotNetCampus.Terminal/Configs/devices.json` - 手动添加LocalId + +### 文档更新 +- `.github/knowledge/Device-Unique-ID-Design.md` - 设计文档 +- `.github/AI任务分工.md` - 任务状态更新 + +## 未来扩展预留 + +### RemoteId自动生成 +- RemoteId字段已预留,当前保持为null +- 等待部署模块在服务自发现时自动生成 +- 用于支持IP地址变更后的设备自动识别 + +### 兼容性说明 +- 当前版本不再支持缺少LocalId的配置文件 +- 如需迁移旧配置,需手动为每个设备添加唯一的LocalId + +--- + +**实现完成时间**: 2025年7月9日 +**实现者**: Configuration AI +**验证状态**: ✅ 编译通过,功能正常 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/DevOps\350\207\252\345\212\250\345\214\226\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/DevOps\350\207\252\345\212\250\345\214\226\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 0000000..b0d79ed --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/DevOps\350\207\252\345\212\250\345\214\226\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,304 @@ +# DevOps 自动化专家 - 核心经验总结 + +**角色职责**: 负责 GitHub Actions CI/CD 流水线设计、跨平台构建、自动化发布和代码质量检查 + +## ✅ 已完成任务总结 + +### 1. GitHub Actions CI/CD 流水线设计和实现 +- ✅ 创建了完整的 CI 流水线 (`.github/workflows/ci.yml`) +- ✅ 支持跨平台构建验证 (Windows/Linux/macOS) +- ✅ 集成了 .NET 9.0 支持和 AOT 发布验证 +- ✅ 基于 push 和 pull request 触发 + +### 2. 跨平台应用程序自动化构建 +- ✅ 实现了基于 Tag 的自动化发布流水线 (`.github/workflows/release.yml`) +- ✅ 支持三大平台:Windows (win-x64)、Linux (linux-x64)、macOS (osx-x64) +- ✅ 使用 PublishAot=true 进行 AOT 编译 +- ✅ 自动重命名可执行文件为平台特定名称 +- ✅ **修复发布产物上传问题**: 解决了 .exe.zip 后缀和 404 链接问题 +- ✅ **修复跨平台文件夹结构问题**: 解决了 macOS/Linux 版本中 devices.json 文件位置错误的问题 + +### 3. 自动化发布和版本管理 +- ✅ 集成了 dotnetCampus.TagToVersion 工具 +- ✅ 版本号自动从 Git Tag 提取并写入 Version.props +- ✅ 支持预发布版本识别 (alpha/beta/rc) +- ✅ 自动创建 GitHub Releases 并上传构建产物 +- ✅ **优化发布页面**: 统一英文描述,提升国际化标准 +- ✅ **修复发布产物命名问题**: + - 产物文件夹内保持原始 exe 文件名 (DotNetCampus.Terminal.exe) + - 仅 zip 包使用规范化命名 (DotNetCampus.Terminal.win-x64.1.0.0.zip) + - 过滤 .pdb/.dbg/.dsym 调试文件以减小包体积 + - zip 包命名规范:使用 `.` 分割,版本号不带 `v` 前缀 + +### 4. 代码质量检查和自动化测试集成 +- ✅ 创建了代码质量检查流水线 (`.github/workflows/code-quality.yml`) +- ✅ 集成了 CodeQL 安全扫描 +- ✅ 添加了依赖项审查 (Dependency Review) +- ❌ 代码格式检查已移除(团队尚未统一 .editorconfig 标准) + +### 5. 依赖项安全扫描和更新 +- ✅ 创建了自动化依赖更新流水线 (`.github/workflows/dependency-update.yml`) +- ✅ 每周一自动检查过期的 NuGet 包 +- ✅ 自动创建 PR 进行依赖更新 + +### 6. 基础设施即代码 +- ❌ EditorConfig 文件已删除(团队尚未统一代码格式标准) +- ✅ 设置了 CHANGELOG.md 模板 +- ✅ 在 README.md 中添加了 CI 状态徽章 + +## 🔧 核心技术要点 + +### GitHub Actions 最佳实践 +```yaml +# 1. 使用最新版本的 Actions +- uses: actions/checkout@v4 +- uses: actions/setup-dotnet@v4 + +# 2. 跨平台路径处理 +- name: Publish (Windows) + if: matrix.os == 'windows-latest' + run: dotnet publish .\src\DotNetCampus.Terminal\ -p:PublishAot=true -r ${{ matrix.runtime }} + +- name: Publish (Unix) + if: matrix.os != 'windows-latest' + run: dotnet publish ./src/DotNetCampus.Terminal/ -p:PublishAot=true -r ${{ matrix.runtime }} + +# 3. 条件执行和矩阵策略 (修正版) +strategy: + matrix: + include: + - os: windows-latest + runtime: win-x64 + artifact-name: DotNetCampus.Terminal-win-x64 # 用于 zip 文件名 + executable-name: DotNetCampus.Terminal-win-x64.exe # 用于可执行文件名 +``` + +### 版本管理策略 +- 使用 `dotnetCampus.TagToVersion` 工具从 Git Tag 自动提取版本号 +- 版本号格式:`v{major}.{minor}.{patch}[-{prerelease}]` +- 预发布版本自动识别:`alpha`、`beta`、`rc` + +### 构建产物策略 +- 使用 AOT 编译 (`-p:PublishAot=true`) 提高性能 +- 生成 self-contained 单文件可执行程序 +- 平台特定命名: + - Windows: `DotNetCampus.Terminal-win-x64.exe` + - Linux: `DotNetCampus.Terminal-linux-x64` + - macOS: `DotNetCampus.Terminal-osx-x64` + +## ⚠️ 踩坑经验 + +### 1. 跨平台路径问题 +**问题**: Windows 使用反斜杠 `\`,Unix 系统使用正斜杠 `/` +**解决方案**: +- 使用条件执行分别处理不同平台 +- 在 matrix 中定义 `path-separator` 变量 + +### 2. 文件重命名问题 +**问题**: 不同平台的文件重命名命令不同 +**解决方案**: +```yaml +# Windows (PowerShell) +- name: Rename executable (Windows) + if: matrix.os == 'windows-latest' + run: | + if (Test-Path ".\publish\${{ matrix.runtime }}\DotNetCampus.Terminal.exe") { + Rename-Item ".\publish\${{ matrix.runtime }}\DotNetCampus.Terminal.exe" "${{ matrix.executable-name }}" + } + shell: pwsh + +# Unix (Bash) +- name: Rename executable (Unix) + if: matrix.os != 'windows-latest' + run: | + if [ -f "./publish/${{ matrix.runtime }}/DotNetCampus.Terminal" ]; then + mv "./publish/${{ matrix.runtime }}/DotNetCampus.Terminal" "./publish/${{ matrix.runtime }}/${{ matrix.executable-name }}" + fi +``` + +### 3. Artifacts 上传路径问题 +**问题**: 需要同时支持 Windows 和 Unix 路径格式 +**解决方案**: 在 `upload-artifact` 中同时指定两种路径格式 + +### 4. 跨平台文件夹结构保留问题 ⚠️⚠️⚠️ +**问题**: macOS 和 Linux 打包时,`devices.json` 等配置文件没有保留原有的文件夹结构 +**现象**: +- Windows 版本的 zip 包中 `devices.json` 在 `Config/devices.json` 路径(正确) +- macOS/Linux 版本的 zip 包中 `devices.json` 在根目录(错误) + +**根本原因**: +- Windows PowerShell 的 `Copy-Item -Recurse` 会保留文件夹结构 +- Unix 的 `find ... -exec cp {} ...` 只复制文件到目标根目录,丢失文件夹结构 + +**解决方案**: 使用 `rsync` 保留文件夹结构 +```yaml +# 错误的方式 (会丢失文件夹结构) +find ./publish/${{ matrix.runtime }} -type f ! -name "*.pdb" ! -name "*.dbg" ! -path "*.dsym*" -exec cp {} ./temp-release/ \; + +# 正确的方式 (保留文件夹结构) +rsync -av --exclude='*.pdb' --exclude='*.dbg' --exclude='*.dsym' ./publish/${{ matrix.runtime }}/ ./temp-release/ +``` + +**重要**: 配置文件如 `devices.json` 需要保持其在 `Config/` 文件夹中的相对路径,应用程序才能正确找到它们。 + +### 6. 代码格式检查的团队协作问题 ⭐ +**问题**: 团队对 .editorconfig 标准尚未统一,强制格式检查可能导致CI失败 +**解决方案**: +- 暂时移除 .editorconfig 文件和相关的格式检查步骤 +- 等待团队统一标准后再重新引入 +- 保留 CodeQL 和依赖项检查等核心安全功能 + +**经验教训**: 在多人协作项目中,代码格式标准化需要全团队讨论决定,不能单方面强制实施 + +### 7. GitHub Release 产物上传问题 ⭐⭐⭐ +**问题**: Release 页面只有 Windows 版本,其他平台缺失且文件链接 404 +**根本原因**: +1. 只上传了单个可执行文件,没有上传完整的发布包 +2. 文件夹结构包含多余的外层目录 +3. artifacts 下载路径不正确 +4. **命名规范错误**: `DotNetCampus.Terminal-win-x64.exe` 作为 artifact-name 导致生成 `.exe.zip` 后缀 + +**解决方案**: +```yaml +# 1. 分离 artifact-name 和 executable-name +matrix: + include: + - os: windows-latest + runtime: win-x64 + artifact-name: DotNetCampus.Terminal-win-x64 # 用于 zip 文件名 + executable-name: DotNetCampus.Terminal-win-x64.exe # 用于可执行文件名 + +# 2. 创建完整的 zip 包,包含所有发布文件 +- name: Create release package (Windows) + if: matrix.os == 'windows-latest' + run: | + # 创建临时文件夹,复制所有发布文件 + New-Item -ItemType Directory -Path ".\temp-release" -Force + Copy-Item -Path ".\publish\${{ matrix.runtime }}\*" -Destination ".\temp-release\" -Recurse + # 创建 zip 包,不包含外层文件夹 + Compress-Archive -Path ".\temp-release\*" -DestinationPath ".\${{ matrix.artifact-name }}.zip" + +# 3. 上传 zip 文件而不是单个可执行文件 +- name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact-name }} + path: ${{ matrix.artifact-name }}.zip + +# 4. 正确的文件路径引用 +files: | + ./artifacts/DotNetCampus.Terminal-win-x64/DotNetCampus.Terminal-win-x64.zip + ./artifacts/DotNetCampus.Terminal-linux-x64/DotNetCampus.Terminal-linux-x64.zip + ./artifacts/DotNetCampus.Terminal-osx-x64/DotNetCampus.Terminal-osx-x64.zip +``` + +**经验教训**: +- AOT 发布的应用程序除了主程序外,还可能包含其他必要文件 +- 应该打包完整的发布目录而不是只上传可执行文件 +- GitHub Actions 的 artifacts 下载会创建以 artifact name 命名的文件夹 +- **命名规范很重要**: artifact-name 不应包含文件扩展名,否则会产生混乱的后缀 + +### 8. 跨平台 zip 创建兼容性问题 +**问题**: Windows 和 Unix 系统的压缩命令不同 +**解决方案**: +```yaml +# Windows: 使用 PowerShell 的 Compress-Archive +Compress-Archive -Path ".\temp-release\*" -DestinationPath ".\${{ matrix.artifact-name }}.zip" + +# Unix: 使用 zip 命令,注意工作目录 +cd temp-release && zip -r ../${{ matrix.artifact-name }}.zip * && cd .. +``` + +**注意事项**: +- zip 命令需要进入目录内部执行,避免包含外层文件夹 +- PowerShell 的 Compress-Archive 直接指定源路径和目标路径 + +### 9. Release 页面国际化标准 ⭐ +**问题**: 发布页面中英文混杂,不符合国际开源项目标准 +**解决方案**: +- 统一使用英文描述,提升项目专业性 +- 保持表格格式清晰,便于用户快速定位下载链接 +- 提供详细的安装指南,覆盖各个平台 + +**经验教训**: 开源项目的发布页面应该保持国际化标准,使用英文可以让更多开发者理解和使用 + +### 10. 发布产物命名规范优化 ⭐⭐ +**问题**: 初期的命名规范存在以下问题: +1. **exe 文件重命名**: 擅自修改产物文件夹内的 exe 文件名,造成混乱 +2. **调试文件冗余**: .pdb/.dbg/.dsym 文件大幅增加包体积 +3. **命名格式不统一**: 使用连字符分割,版本号带 `v` 前缀 + +**最终解决方案**: +```yaml +# 1. 保持产物文件夹内原始文件名 +# 删除所有重命名 exe 文件的步骤 + +# 2. 过滤调试文件 +# Windows: 排除 .pdb, .dbg 文件 +Get-ChildItem -Path ".\publish\${{ matrix.runtime }}" -Recurse | + Where-Object { $_.Extension -notin @('.pdb', '.dbg') } + +# Unix: 排除 .pdb, .dbg, .dsym 文件 +find ./publish/${{ matrix.runtime }} -type f ! -name "*.pdb" ! -name "*.dbg" ! -path "*.dsym*" + +# 3. 统一命名规范:使用 . 分割,版本号不带 v +$zipName = "DotNetCampus.Terminal.${{ matrix.runtime }}.${{ steps.get_version.outputs.version }}.zip" +``` + +**最终命名格式**: +- `DotNetCampus.Terminal.win-x64.1.0.0.zip` +- `DotNetCampus.Terminal.linux-x64.1.0.0.zip` +- `DotNetCampus.Terminal.osx-x64.1.0.0.zip` + +**经验教训**: +- **产物内容不变原则**: 发布产物文件夹内的文件应保持原始名称,只有打包文件名需要规范化 +- **体积优化**: 生产环境不需要调试符号文件,过滤可显著减小包体积 +- **命名一致性**: 统一的命名规范有助于用户识别和自动化脚本处理 + +## 📋 待实现功能 + +### 高级特性 (未来考虑) +- [ ] 增量构建优化 +- [ ] 性能基准测试自动化 +- [ ] 自动化变更日志生成 +- [ ] 代码签名 (Windows 平台) +- [ ] 集成测试和 UI 自动化测试 (由其他 AI 负责) + +## 🤝 与其他角色的协作 + +- **测试工程师**: 将来需要集成自动化测试到 CI 流水线 +- **文档维护员**: 协作维护 CHANGELOG.md 和发布文档 +- **所有开发AI**: 确保代码质量检查通过 + +## 📞 需要人类协助的事项 + +以下事项需要项目管理员在 GitHub 上配置: + +1. **GitHub Secrets 配置**: + - 确保 `GITHUB_TOKEN` 具有 Release 创建权限 + - 如需代码签名,需要配置相关证书 secrets + +2. **Branch Protection Rules**: + - 设置 main 分支保护规则 + - 要求 CI 检查通过才能合并 + +3. **GitHub Actions 权限**: + - 确保 Actions 有权限创建 Releases + - 确保 Actions 有权限执行 CodeQL 扫描 + +4. **项目设置**: + - 启用 Vulnerability alerts + - 启用 Dependency graph + - 配置 Security advisories + +## 💡 持续改进建议 + +1. **监控和告警**: 考虑集成构建失败通知 +2. **缓存优化**: 添加 NuGet 包缓存以加速构建 +3. **并行化**: 进一步优化构建并行度 +4. **环境隔离**: 考虑使用容器化构建环境 + +--- + +**最后更新**: 2025年7月10日 +**负责AI**: DevOps 自动化专家 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/SSH\350\277\236\346\216\245\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/SSH\350\277\236\346\216\245\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 0000000..0035902 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/SSH\350\277\236\346\216\245\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,143 @@ +# SSH连接专家核心经验总结 + +## 角色定位 +SSH连接专家负责 DotNetCampus Terminal 项目中所有与SSH连接相关的功能开发和维护。 + +## 核心职责 +1. SSH.NET库的封装和连接管理 +2. SSH密钥认证配置和部署 +3. 多设备连接的安全性分析 +4. SSH连接池优化和断线重连机制 +5. SSH隧道和端口转发支持 + +## 技术重点 + +### SSH.NET 使用要点 +- 使用 `SshClient` 进行基本SSH连接 +- 使用 `SftpClient` 进行文件传输 +- 使用 `PasswordConnectionInfo` 或 `PrivateKeyConnectionInfo` 进行身份认证 +- 连接超时设置:默认30秒,可根据网络环境调整 +- 异常处理:捕获 `SshException`、`SocketException` 等 + +### 密钥管理最佳实践 +- 支持RSA、ECDSA、Ed25519密钥格式 +- 密钥文件路径:`~/.ssh/id_rsa`、`~/.ssh/id_ecdsa`、`~/.ssh/id_ed25519` +- 公钥部署:自动追加到远程 `~/.ssh/authorized_keys` +- 权限设置:私钥600,公钥644,.ssh目录700 + +### 连接状态管理 +- 实现连接池避免频繁建立连接 +- 心跳检测机制保持长连接 +- 优雅的断线重连策略 +- 连接状态通知UI层 + +## 核心代码模式 + +### SSH连接封装 +```csharp +public class SshConnectionManager : ISshConnectionManager +{ + private readonly ConcurrentDictionary _connections = new(); + + public async Task GetConnectionAsync(SshDeviceInfo deviceInfo) + { + var key = deviceInfo.GetConnectionKey(); + return _connections.GetOrAdd(key, _ => CreateConnection(deviceInfo)); + } + + private SshClient CreateConnection(SshDeviceInfo deviceInfo) + { + var connectionInfo = CreateConnectionInfo(deviceInfo); + var client = new SshClient(connectionInfo); + client.Connect(); + return client; + } +} +``` + +### 密钥部署流程 +```csharp +public async Task DeployPublicKeyAsync(SshDeviceInfo deviceInfo, string publicKeyPath) +{ + try + { + using var sftpClient = new SftpClient(deviceInfo.ToConnectionInfo()); + sftpClient.Connect(); + + var publicKeyContent = await File.ReadAllTextAsync(publicKeyPath); + var authorizedKeysPath = ".ssh/authorized_keys"; + + // 检查是否已存在 + if (!await IsKeyAlreadyDeployedAsync(sftpClient, authorizedKeysPath, publicKeyContent)) + { + await AppendKeyToAuthorizedKeysAsync(sftpClient, authorizedKeysPath, publicKeyContent); + } + + return true; + } + catch (Exception ex) + { + Log.Error($"[SSH] 密钥部署失败: {ex.Message}"); + return false; + } +} +``` + +## 踩坑经验 + +### 1. SSH连接超时问题 +- **问题**:网络不稳定时连接经常超时 +- **解决**:设置合适的超时时间,实现重试机制 +- **代码**:`connectionInfo.Timeout = TimeSpan.FromSeconds(30)` + +### 2. 密钥格式兼容性 +- **问题**:OpenSSH新格式私钥无法直接使用 +- **解决**:使用SSH.NET支持的格式,或转换密钥格式 +- **经验**:优先推荐RSA 2048位密钥的广泛兼容性 + +### 3. 并发连接管理 +- **问题**:多个操作同时建立连接导致资源浪费 +- **解决**:实现连接池,复用已建立的连接 +- **注意**:及时释放不用的连接避免资源泄漏 + +### 4. SFTP路径处理 +- **问题**:Windows和Linux路径分隔符不一致 +- **解决**:统一使用Unix路径格式,使用Path.Combine谨慎处理 +- **代码**:`remotePath = remotePath.Replace('\\', '/')` + +## 与其他模块协作 + +### 与UI界面设计师协作 +- 提供连接状态变更事件供UI绑定 +- 配合实现密钥部署进度显示 +- 提供错误信息格式化供UI展示 + +### 与文件同步工程师协作 +- 提供稳定的SFTP连接用于文件传输 +- 配合优化文件同步性能 +- 共享连接池减少资源占用 + +### 与配置管理专家协作 +- 定义SSH设备配置数据结构 +- 支持密钥路径配置和验证 +- 实现配置变更时的连接更新 + +## 日志规范 +- 标签:`[SSH]` +- 连接事件:`Log.Info("[SSH] 连接到设备: {deviceName}")` +- 错误处理:`Log.Error("[SSH] 连接失败: {error}")` +- 密钥操作:`Log.Info("[SSH] 密钥部署成功: {deviceName}")` + +## 性能优化要点 +1. 连接复用:避免频繁建立新连接 +2. 异步操作:所有网络操作使用异步模式 +3. 超时控制:合理设置各种操作的超时时间 +4. 资源释放:及时关闭不用的连接和流 +5. 批量操作:尽可能批量处理多个SSH命令 + +## 安全考虑 +1. 密钥权限:确保私钥文件权限正确 +2. 连接加密:使用强加密算法 +3. 会话管理:避免长时间保持不活跃连接 +4. 错误信息:避免在日志中暴露敏感信息 +5. 密钥轮换:支持定期更换SSH密钥对 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/UI\347\225\214\351\235\242\350\256\276\350\256\241\345\270\210-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/UI\347\225\214\351\235\242\350\256\276\350\256\241\345\270\210-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 0000000..99c6473 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/UI\347\225\214\351\235\242\350\256\276\350\256\241\345\270\210-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,360 @@ +# UI界面设计师经验总结 + +## 必读提醒 +🔥 **在开始任何UI相关任务前,必须先阅读本文档!** + +## ❗ 致命错误避坑 + +### 文件位置错误导致编译失败 +**这是UI设计师容易犯的文件组织错误!** + +#### ❌ 错误:文件创建在错误位置 +``` +项目根目录/ +├── Views/ ← 错误位置! +│ ├── StatusTipView.axaml +│ ├── StatusTipView.axaml.cs +│ └── StatusBarView.axaml.cs +└── src/ + └── DotNetCampus.Terminal/ + └── Views/ ← 正确位置 + ├── MainView.axaml + └── MainView.axaml.cs +``` + +**典型编译错误**: +``` +CSC : error AXN0001: Avalonia x:Name generator was unable to generate names for type 'DotNetCampus.Terminal.Views.StatusTipView'. +The type 'DotNetCampus.Terminal.Views.StatusTipView' does not exist in the assembly. +``` + +#### ✅ 正确:文件必须在项目目录内 +``` +src/ +└── DotNetCampus.Terminal/ + ├── Views/ ← 正确位置 + │ ├── StatusTipView.axaml + │ ├── StatusTipView.axaml.cs + │ ├── StatusBarView.axaml + │ └── StatusBarView.axaml.cs + └── ViewModels/ + ├── StatusTipViewModel.cs + └── StatusBarViewModel.cs +``` + +#### 错误原因分析 +1. **AXAML文件声明了`x:Class`**:`x:Class="DotNetCampus.Terminal.Views.StatusTipView"` +2. **但C#文件不在项目中**:创建在了项目外部的错误位置 +3. **Avalonia无法生成代码**:x:Name生成器找不到对应的类型 + +#### 修复方法 +```powershell +# 删除错误位置的文件 +Remove-Item "Views\StatusTipView.axaml.cs" -Force +Remove-Item "Views\StatusBarView.axaml" -Force +Remove-Item "Views\StatusBarView.axaml.cs" -Force +Remove-Item "Views" -Recurse -Force + +# 确保文件在正确位置:src/DotNetCampus.Terminal/Views/ +``` + +**记忆要点**: +- **AXAML和其C#代码隐藏文件必须在同一个项目中** +- **使用绝对路径创建文件时要特别小心路径是否正确** +- **遇到AXN0001错误时,首先检查文件位置是否正确** + +### 字符级测量单位错误 +**这是UI设计师最容易犯的致命错误!** + +#### ❌ 错误的像素思维 +```xml + + + + + + + + + +短输入框 +中等输入框 +长输入框 + + +紧密布局 +标准布局 + + +标准内边距 +宽松内边距 +``` + +**记住:在Consolonia中,每个数字都是字符数量,不是像素!** + +## 核心技术栈速查 + +### ViewModel基类选择 +- `BindableRecord` - 基础属性绑定,使用 `SetField(ref field, value)` +- `TrackableBindableRecord` - 带变更跟踪,使用 `SetFieldTrackingChanges(ref field, value)` + +### 命令类型速查 +- `ActionCommand` - 同步无参数命令,适用于简单操作 +- `AsyncCommand` - 异步命令,适用于I/O操作 +- `InteractiveCommand` - 交互式命令,用于需要用户确认的操作 + +### Consolonia特殊语法 +```xml + +*.axaml (不是 .xaml) + + +xmlns:console="https://github.com/jinek/consolonia" + + + + + + + +x:DataType="vm:SomeViewModel" +``` + +### 集合绑定 +- 使用 `AvaloniaList` 替代 `ObservableCollection` +- 大数据集使用 `VirtualizingStackPanel` + +### 异步UI更新 +```csharp +// 后台线程更新UI +Dispatcher.UIThread.InvokeAsync(() => { /* UI更新代码 */ }); +``` + +## 项目架构规范 + +### 文件夹结构 +``` +ViewModels/ +├── RemoteDevices/ +│ └── Ssh/ +│ ├── SshDeviceSyncViewModel.cs // 同步相关 +│ ├── SshDeviceCommandsViewModel.cs // 命令相关 +│ └── SshDeviceDeployViewModel.cs // 部署相关 + +Views/ +├── RemoteDevices/ +│ └── Ssh/ +│ ├── SshDeviceSyncView.axaml // 对应同步ViewModel +│ ├── SshDeviceCommandsView.axaml // 对应命令ViewModel +│ └── SshDeviceDeployView.axaml // 对应部署ViewModel +``` + +### ViewModel组合模式 +```csharp +public record SshRemoteDeviceInfoViewModel : RemoteDeviceInfoNode +{ + // 子ViewModels - 使用简洁属性名 + public SshDeviceSyncViewModel Sync { get; } + public SshDeviceCommandsViewModel Commands { get; } + public SshDeviceDeployViewModel Deploy { get; } +} +``` + +## 常见错误避坑 + +### ❌ 错误做法 +- 使用 `class` 继承 `record` 类型 +- 在Button上直接设置 `console:ButtonExtensions.Shadow` +- 使用 `SetProperty` 方法(不存在) +- 使用 `RelayCommand`(项目中不存在) +- 忘记设置 `x:DataType` + +### ✅ 正确做法 +- 使用 `record` 继承 `record` 类型 +- 在Styles中统一设置Button样式 +- 根据基类使用 `SetField` 或 `SetFieldTrackingChanges` +- 使用项目中的 `ActionCommand`、`AsyncCommand` 等 +- 始终设置强类型绑定 `x:DataType` + +## 性能优化要点 +- 选择合适的绑定模式(OneTime/OneWay/TwoWay) +- 大列表使用 `VirtualizingStackPanel` +- 使用 `x:DataType` 实现编译时绑定检查 + +## 相关知识库文档 +- `Consolonia-Quick-Reference.md` - Consolonia核心语法 +- `UI-Progress-And-Binding-Best-Practices.md` - 进度显示最佳实践 +- `ViewModel-重构最佳实践.md` - ViewModel架构指南 +- `Interactive-Command-Pattern-Guide.md` - 交互式命令模式 + +## 命令框架使用经验 + +### 静态CanExecute模式 +项目框架的 `ActionCommand` 和 `AsyncCommand` 不支持动态刷新 CanExecute,需要手动管理: + +```csharp +public SshDeviceDeployViewModel() +{ + // 初始化命令时不传递CanExecute委托 + DeployKeyCommand = new AsyncCommand(DeployKeyAsync); + + // 手动设置初始状态 + UpdateCommandStates(); +} + +// 在属性变更时手动更新命令状态 +public bool IsDeploying +{ + get => _isDeploying; + private set + { + if (SetFieldTrackingChanges(ref _isDeploying, value)) + { + UpdateCommandStates(); // 关键:属性变更时更新命令状态 + } + } +} + +private void UpdateCommandStates() +{ + DeployKeyCommand.CanExecute = !IsDeploying && ConfirmOperation; + RetryDeployCommand.CanExecute = CanRetry; + // ... +} +``` + +### UI安全设计模式 +SSH密钥部署等安全敏感操作的UI设计要点: + +1. **多步确认机制**:用户必须勾选"理解安全影响" +2. **清晰的操作说明**:详细列出将执行的步骤 +3. **安全警告**:使用醒目颜色(Orange/Red)标识风险操作 +4. **进度反馈**:实时显示当前执行步骤和进度百分比 +5. **错误恢复**:提供重试和回滚功能 +6. **状态可视化**:不同阶段显示不同的UI区域(进度/错误/成功) + +### Consolonia布局最佳实践 +```xml + + + + + + + + + + + + + + +``` + +## 📋 最新重构经验 (2025-07-09) + +### 全局功能键状态栏重构成功案例 + +#### 重构前问题 +- 功能键代码直接嵌入在MainView.axaml中,难以维护 +- 没有统一的命令绑定,功能键点击无实际响应 +- 功能键设计不够完整,缺少常用的TUI功能 + +#### 重构后解决方案 +1. **创建专用状态栏控件**:`StatusBarView.axaml` + `StatusBarViewModel.cs` +2. **完整的功能键设计**:F1-F10 覆盖所有常用TUI操作 +3. **统一的命令绑定**:每个功能键都有对应的命令实现 +4. **模块化设计**:状态栏可以独立维护和测试 + +#### TUI程序标准功能键设计 +```xml +F1 - 帮助 (ShowHelpCommand) +F2 - 连接 (ConnectCommand) +F3 - 同步 (StartSyncCommand) +F4 - 新建 (NewDeviceCommand) +F5 - 刷新 (RefreshCommand) +F6 - 保存 (SaveConfigCommand) +F7 - 终端 (OpenShellCommand) +F8 - 搜索 (ToggleSearchCommand) +F9 - 设置 (SettingsCommand) +F10 - 退出 (ExitCommand) +``` + +#### 关键技术要点 +- **依赖注入集成**:StatusBarViewModel 需要注册到 Startup.cs +- **命令类型选择**:同步用 ActionCommand,异步用 AsyncCommand +- **日志标签规范**:使用 `[StatusBar]` 标签便于调试 +- **循环引用避免**:StatusBarViewModel 不能直接引用 ViewModels 命名空间 + +### 全局状态提示栏集成 +#### 实现鼠标悬停状态提示 +- **删除ToolTip**:用鼠标悬停事件替代传统ToolTip +- **Tag参数传递**:使用Button.Tag传递功能键信息格式为"F1,描述,是否启用" +- **事件处理**:PointerEntered/PointerExited事件处理鼠标悬停 +- **状态栏集成**:通过StatusTipViewModel显示实时提示信息 + +#### 美学优化技巧 +- **功能键淡化设计**:使用 `#606060` 前景色和 `#404040` 背景色让按钮更低调 +- **悬停突出显示**:鼠标悬停时恢复为White前景色,营造交互感 +- **TurboVisionDarkTheme适配**:颜色选择符合深色主题美学 + +#### F1帮助功能实现 +- **GitHub仓库跳转**:使用 `Process.Start` 在默认浏览器打开仓库 +- **错误处理**:捕获异常并在状态栏显示错误信息 +- **状态反馈**:操作过程中提供实时状态更新 + +#### 编译错误解决 +```csharp +// ❌ 错误:循环引用 +using DotNetCampus.Terminal.ViewModels; + +// ✅ 正确:避免循环引用,使用 EnsureGet 扩展方法 +using DotNetCampus.Terminal.Framework.DependencyInjection; +_mainViewModel = serviceProvider.EnsureGet(); +``` + +#### 功能键响应状态管理 +**重要发现**:功能键的可用状态应该根据当前选中的设备/界面动态变化 +- F2(连接)、F3(同步)、F6(保存)、F7(终端) 需要选中设备时才可用 +- F1(帮助)、F4(新建)、F5(刷新)、F8(搜索)、F9(设置)、F10(退出) 始终可用 + +**未来改进方向**: +1. 实现功能键的动态可用状态管理 +2. 添加快捷键支持 (KeyBinding) +3. 功能键标签的多语言支持 +4. 根据选中项类型显示不同的功能键组合 + +--- +*最后更新:2025年7月9日* +*下次更新时,请基于实际踩坑经验补充内容* diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\344\273\266\345\220\214\346\255\245\345\267\245\347\250\213\345\270\210-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\344\273\266\345\220\214\346\255\245\345\267\245\347\250\213\345\270\210-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 0000000..73df3c5 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\344\273\266\345\220\214\346\255\245\345\267\245\347\250\213\345\270\210-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,199 @@ +# 文件同步工程师经验总结 + +## 必读提醒 +🔥 **在开始任何文件同步相关任务前,必须先阅读本文档!** + +## 核心技术栈速查 + +### SSH.NET关键类型 +- `SftpClient` - SFTP文件传输客户端 +- `ConnectionInfo` - SSH连接信息配置 +- `PasswordAuthenticationMethod` - 密码认证 +- `PrivateKeyAuthenticationMethod` - 密钥认证 +- `PrivateKeyFile` - 私钥文件 + +### 文件同步服务接口 +```csharp +public interface IFileSyncService +{ + Task SyncDirectoryAsync(SyncRequest request, + IProgress? progress = null, + CancellationToken cancellationToken = default); +} +``` + +### 同步方向类型 +- `RemoteToLocal` - 远程到本地(主要使用) +- `LocalToRemote` - 本地到远程 +- `Bidirectional` - 双向同步 + +### 进度报告模式 +```csharp +public record SyncProgress( + int FileIndex, // 当前文件索引 + int TotalFiles, // 总文件数 + string CurrentFile, // 当前文件名 + double OverallProgress // 总体进度 (0-100) +); +``` + +## 错误处理最佳实践 + +### 错误消息格式规范 +- ✅ `"[SSH] 连接失败: 主机不可达"` +- ✅ `"[FileSync] 同步失败: 权限不足"` +- ❌ `"[SSH]: 连接失败: 主机不可达"` (双冒号) + +### 常见错误类型处理 +```csharp +// 网络连接错误 +catch (SocketException ex) +{ + return SyncResult.Failure($"[SSH] 网络连接失败: {ex.Message}"); +} + +// SSH认证错误 +catch (SshAuthenticationException ex) +{ + return SyncResult.Failure($"[SSH] 认证失败: {ex.Message}"); +} + +// SFTP操作错误 +catch (SftpPathNotFoundException ex) +{ + return SyncResult.Failure($"[FileSync] 路径不存在: {ex.Message}"); +} +``` + +### 诊断信息格式 +```csharp +var diagnostics = new StringBuilder(); +diagnostics.AppendLine($"连接信息: {info.UserName}@{info.Host}:{info.Port}"); +diagnostics.AppendLine($"同步方向: {request.Direction}"); +diagnostics.AppendLine($"远程路径: {request.RemotePath}"); +diagnostics.AppendLine($"本地路径: {request.LocalPath}"); +diagnostics.AppendLine($"错误详情: {ex}"); +``` + +## 性能优化要点 + +### 增量同步 +- 基于文件修改时间和大小判断 +- 避免重复传输未更改文件 +- 使用 `SftpFile.LastWriteTime` 和 `SftpFile.Length` + +### 并发控制 +- SFTP连接复用,避免频繁建立连接 +- 使用 `SemaphoreSlim` 控制并发数 +- 单个SftpClient不支持并发,需要连接池 + +### 大文件处理 +- 分块传输大文件 +- 实时进度报告 +- 支持取消操作 + +## UI集成要点 + +### ViewModel绑定属性 +```csharp +// 全局同步状态 +public bool IsGlobalSyncing { get; set; } +public double GlobalSyncProgress { get; set; } + +// 时间显示 +public DateTimeOffset? LastSyncTime { get; set; } + +// 错误处理 +public string LastSyncErrorMessage { get; set; } +public string DetailedDiagnostics { get; set; } +``` + +### 空状态处理 +- 当没有启用的目录同步时,显示友好提示 +- 避免显示"同步失败"的误导信息 + +## 常见错误避坑 + +### ❌ 错误做法 +- 错误消息使用双冒号格式 +- 忘记处理取消令牌 +- 没有提供详细诊断信息 +- 目录同步为空时显示错误状态 + +### ✅ 正确做法 +- 使用标准错误消息格式 `[标签] 描述` +- 正确处理 `CancellationToken` +- 提供完整的诊断信息便于调试 +- 区分"无目录同步"和"同步失败"状态 + +## 日志记录规范 +```csharp +using DotNetCampus.Logging; + +Log.Info($"[FileSync] 开始同步: {request.RemotePath} -> {request.LocalPath}"); +Log.Warning($"[FileSync] 跳过文件: {file.Name}, 原因: 权限不足"); +Log.Error($"[FileSync] 同步失败: {ex.Message}"); +``` + +## 相关知识库文档 +- `SSH.NET-File-Sync-Guide.md` - SSH.NET详细使用指南 +- `File-Sync-Error-Handling-Optimization.md` - 错误处理优化 +- `Incremental-Sync-Optimization.md` - 增量同步实现 +- `Remote-To-Local-Sync-Implementation.md` - 同步实现细节 + +## 时间戳同步技术要点 🆕 + +### 问题背景 +在文件同步过程中,远程文件的时间戳会被设置为同步时间,导致: +1. 增量同步比较算法失效(本地时间 vs 远程同步时间永远不相等) +2. 远程设备上难以通过文件时间判断文件来源 +3. 双向同步时出现误判 + +### 解决方案:双向时间戳同步 + +#### 核心实现方法 +```csharp +// 方法1:使用 SetAttributes(推荐) +var remoteAttributes = client.GetAttributes(remoteFilePath); +remoteAttributes.LastWriteTime = localFileInfo.LastWriteTime; +remoteAttributes.LastAccessTime = localFileInfo.LastWriteTime; +client.SetAttributes(remoteFilePath, remoteAttributes); + +// 方法2:使用 SetLastWriteTime(备用) +client.SetLastWriteTime(remoteFilePath, localFileInfo.LastWriteTime); +``` + +#### 重要注意事项 +1. **服务器兼容性**:不是所有SSH服务器都支持时间戳修改,需要实现多种方法的降级处理 +2. **时间精度**:允许2秒的时间戳差异容忍度,处理网络和文件系统的时间差异 +3. **调试信息**:增加详细的时间戳验证日志,便于排查问题 +4. **只设置必要时间**:创建时间和修改时间,不需要设置访问时间 + +#### ⚠️ **技术限制(已实测确认)** +- ✅ **修改时间同步**:可以实现,功能正常 +- ❌ **创建时间同步**:无法实现(SSH.NET和SFTP协议限制) +- ⚠️ **访问时间同步**:可以实现但通常不必要 + +**创建时间无法同步的根本原因**: +- SSH.NET 的 `SftpFileAttributes` 类不包含 `CreationTime` 属性 +- SFTP 协议不标准化支持创建时间 +- 跨平台兼容性差,属于可接受的技术限制 + +#### 实现位置 +- `SftpOperationHelper.SyncRemoteFileTimestamps()` - 本地到远程时间戳同步 +- `SftpOperationHelper.SyncLocalFileTimestamps()` - 远程到本地时间戳同步 +- 在文件传输完成后立即调用时间戳同步方法 + +#### 调试技巧 +```csharp +// 记录同步前后的时间戳对比 +Log.Debug($"[FileSync] 同步前远程文件时间戳: {beforeSync.LastWriteTime}"); +Log.Debug($"[FileSync] 本地文件时间戳: {localFileInfo.LastWriteTime}"); +// 执行同步 +VerifyRemoteFileTimestamp(client, remoteFilePath, localFileInfo.LastWriteTime); +``` + +--- +*最后更新:2025年7月16日* +*新增:时间戳同步解决方案* +*下次更新时,请基于实际踩坑经验补充内容* diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\346\241\243\347\273\264\346\212\244\345\221\230-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\346\241\243\347\273\264\346\212\244\345\221\230-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 0000000..b6f2332 --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\346\226\207\346\241\243\347\273\264\346\212\244\345\221\230-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,304 @@ +# 文档维护员核心经验总结 + +## 角色定位 +文档维护员负责 DotNetCampus Terminal 项目的所有文档管理、AI协作流程优化和技术标准制定。 + +## 核心职责 +1. 项目文档的创建、维护和更新 +2. AI协作流程的设计和优化 +3. 技术知识库的分类和组织 +4. 编码规范和最佳实践的制定 +5. AI角色权限和职责的分配管理 + +## 文档架构设计 + +### `.github/` 目录结构 +``` +.github/ +├── AI任务分工.md # AI角色分工和协作规则 +├── copilot-instructions.md # GitHub Copilot指令文档 +└── knowledge/ # 技术知识库 + ├── AI协作经验/ # AI协作经验和最佳实践 + │ ├── 角色经验总结/ # 各AI角色的核心经验 + │ └── 协作流程优化/ # 协作流程改进记录 + ├── 技术文档/ # 技术参考文档 + │ ├── Consolonia-快速参考.md + │ ├── SSH.NET-文件同步指南.md + │ └── 配置设计文档.md + └── 问题解决/ # 常见问题和解决方案 + ├── 常见问题解答.md + └── 错误处理指南.md +``` + +### 文档命名规范 +- 中文文档:使用中文名称,用连字符分隔 +- 英文文档:使用PascalCase或kebab-case +- 角色经验:`{角色名}-核心经验总结.md` +- 技术指南:`{技术栈}-{用途}指南.md` +- 问题文档:`{问题类型}解答.md` + +## AI协作流程优化 + +### 角色认领机制 +```markdown +**🤖 AI角色自动认领机制**:如果用户在提示词中指定了AI的角色,AI应该: +1. 立即查看 `.github/AI任务分工.md` 文件认领对应职位 +2. 阅读该角色的必读文档(角色经验总结文档) +3. 按照职位要求和经验总结执行后续行动 +4. 遵循该角色的技术规范和工作流程 +``` + +### 协作规则制定 +1. **开始任务前必读**:角色经验总结文档 +2. **技术问题查阅**:知识库优先,避免重复踩坑 +3. **接口设计优先**:确保模块间依赖清晰 +4. **及时测试验证**:每个模块完成后立即编译测试 +5. **知识共享更新**:及时更新知识库和经验总结 + +### 进度跟踪机制 +- 使用 ✅ 标记已完成任务 +- 使用 [ ] 标记待完成任务 +- 重要任务添加优先级标识 +- 阶段性里程碑单独标注 + +## 技术标准制定 + +### 编码规范要点 +```markdown +### 兼容性原则 +**重要:本项目不需要考虑兼容性问题** +- 不使用 `[Obsolete]` 标记 +- 不保留旧方法或接口 +- 不添加"兼容旧版本"等注释说明 +- 直接重构和替换,无需向后兼容 +- 项目处于开发阶段,API可以自由变更 + +### 代码重构原则 +- **400行规则**:代码超过400行时,需要酌情考虑重构 +- **600行硬限制**:代码超过600行时,必须考虑重构 +- **例外情况**:超过600行但非常单一易懂的代码(如大型枚举)可以保留 +``` + +### 文档质量标准 +1. **结构清晰**:使用Markdown标准格式 +2. **内容完整**:包含背景、方法、示例、注意事项 +3. **及时更新**:技术变更时同步更新文档 +4. **交叉引用**:相关文档间建立链接关系 +5. **版本控制**:重要变更记录修改日志 + +## 知识库管理 + +### 分类原则 +- **按角色分类**:每个AI角色对应一个经验总结文档 +- **按技术栈分类**:Consolonia、SSH.NET、配置管理等 +- **按问题类型分类**:编译错误、运行时错误、性能问题等 +- **按协作流程分类**:角色分工、任务管理、质量控制等 + +### 内容维护 +- **定期审查**:每月检查文档的准确性和完整性 +- **版本更新**:技术栈升级时及时更新相关文档 +- **经验积累**:将新的踩坑经验及时补充到知识库 +- **交叉验证**:不同角色间的技术观点进行验证 + +### 搜索优化 +- **标签系统**:为文档添加分类标签 +- **关键词索引**:在文档中包含相关技术关键词 +- **目录结构**:保持清晰的文件夹层次结构 +- **文档链接**:建立文档间的关联关系 + +## copilot-instructions.md 维护 + +### 核心内容结构 +1. **项目概述**:技术栈和开发模式说明 +2. **AI角色认领机制**:自动化角色分配流程 +3. **编码规范**:命名、风格、兼容性原则 +4. **技术要点**:Consolonia、日志、性能优化 +5. **协作流程**:开发步骤和文档索引 +6. **经验总结**:各角色经验文档链接 + +### 更新维护策略 +- **技术栈变更**:及时更新技术栈说明 +- **规范完善**:根据实践经验完善编码规范 +- **流程优化**:基于协作效果调整流程描述 +- **文档索引**:保持与知识库结构的同步 + +## 质量控制 + +### 文档审查清单 +- [ ] 标题和结构是否清晰 +- [ ] 内容是否准确完整 +- [ ] 代码示例是否可运行 +- [ ] 链接是否有效 +- [ ] 格式是否符合Markdown规范 +- [ ] 是否与项目当前状态一致 + +### 持续改进机制 +1. **反馈收集**:收集其他AI角色的文档使用反馈 +2. **效果评估**:定期评估文档对协作效率的影响 +3. **流程优化**:根据实际使用情况调整协作流程 +4. **工具改进**:探索自动化文档生成和维护工具 + +## 与其他角色协作 + +### 与技术角色协作 +- **UI界面设计师**:维护UI设计规范和最佳实践 +- **文件同步工程师**:整理文件同步技术文档 +- **配置管理专家**:完善配置格式和使用指南 +- **SSH连接专家**:汇总SSH连接相关技术要点 + +### 协作模式 +1. **主动询问**:定期询问各角色的文档需求 +2. **及时响应**:快速响应文档更新请求 +3. **标准统一**:确保各模块文档格式的一致性 +4. **知识整合**:将分散的技术经验整合成系统性文档 + +## 性能指标 + +### 文档质量指标 +- 文档完整性:核心功能是否都有对应文档 +- 更新及时性:文档与代码变更的同步程度 +- 使用频率:文档被其他AI角色查阅的频次 +- 问题解决率:文档能解决的技术问题比例 + +### 协作效率指标 +- 角色认领速度:AI自动认领角色的成功率 +- 重复问题率:相同技术问题的重复出现频率 +- 知识复用率:已有知识库内容的复用程度 +- 流程执行率:协作流程的实际执行情况 + +## 持续改进方向 + +1. **自动化文档生成**:探索从代码注释自动生成API文档 +2. **多语言支持**:为国际化项目提供英文文档版本 +3. **交互式文档**:集成代码示例的在线运行环境 +4. **AI辅助写作**:利用AI工具提升文档写作效率 +5. **版本控制优化**:建立更精细的文档版本管理机制 + +## 最新实践经验 + +### JSON库文档整理 (2025-07-10) +**问题背景**:其他AI撰写的 SystemTextJson 文档不符合项目文档规范 +**处理措施**: +1. **文档重命名**:从 `01-AOT配置系统使用指南.md` 规范为 `01-JSON配置系统使用指南.md` +2. **标题优化**:统一为 `# System.Text.Json 使用指南` 格式,与其他依赖库文档保持一致 +3. **结构重组**:增加"项目中的应用场景"、"基础使用"等标准章节 +4. **代码示例规范**:添加文件路径注释,明确代码所属模块 +5. **索引更新**:在 `copilot-instructions.md` 中正确添加文档索引 +6. **集成指导**:增加与项目现有架构的集成示例 + +**经验总结**: +- 定期检查新增文档是否符合规范 +- 建立文档审查清单,确保格式一致性 +- 及时更新技术文档索引,保持可发现性 +- **库名称规范**:必须使用正确的库名称,如 `System.Text.Json` 而不是 `SystemTextJson` + +### TOML配置系统文档清理 (2025年7月10日) +**任务背景**:AI同事完成了从TOML到JSON的配置系统迁移,需要清理所有TOML相关文档 + +**清理内容**: +1. **删除TOML文档**: + - `技术设计文档/配置管理/01-TOML配置文件架构设计.md` + - `技术设计文档/配置管理/05-TOML到JSON迁移技术方案.md` + - `AI协作经验/实现经验总结/TOML配置功能实现踩坑记录.md` + - `依赖库文档/Tomlet/` 整个目录 + +2. **创建新文档**: + - `技术设计文档/配置管理/01-JSON配置系统架构设计.md` - 新的JSON配置架构文档 + +3. **更新现有文档**: + - `copilot-instructions.md` - 移除TOML引用,更新技术栈 + - `AI任务分工.md` - 更新配置管理专家的任务列表 + - `配置管理专家-核心经验总结.md` - 添加TOML迁移历史记录 + - `02-配置保存功能实现.md` - 将TOML替换为JSON描述 + - `03-配置数据源迁移方案.md` - 添加完整的迁移历史 + - `knowledge/README.md` - 更新技术栈索引 + +**历史记录原则**: +- 在配置相关文档中保留TOML历史,记录迁移原因(AOT兼容性问题) +- 说明技术决策的演进过程:Debug → TOML → JSON +- 避免完全删除历史信息,保持决策的可追溯性 + +### TOML文档清理项目 - 任务分解 (2025年7月11日) +**项目背景**:搜索发现约10+个文档仍包含TOML引用,需要系统性清理 + +**清理策略** (重要澄清): +- 🔥 **完全清理原则**:除了迁移记录文档外,所有其他文档**完全清理**TOML字眼 +- 📝 **唯一历史文档**:只保留 `TOML到JSON配置系统迁移记录.md` 一篇迁移历史 +- ❌ **不保留历史**:其他文档不应该出现任何 TOML 相关内容或历史说明 + +**分解策略**: +- 按文档类型分组,每组分配给一个文档维护员同事 +- 所有同事都必须**完全清理**TOML相关内容,不保留任何字眼 +- 如需要解释历史,统一指向唯一的迁移记录文档 + +**任务分组**: +1. **技术设计文档组** (4个文件) → 文档维护员同事A +2. **AI协作经验组** (3个文件) → 文档维护员同事B +3. **依赖库+问题文档组** (2个文件) → 文档维护员同事C + +**创建的集中文档**: +- `AI协作经验/实现经验总结/TOML到JSON配置系统迁移记录.md` - **唯一的**完整迁移历史记录 + +**协作经验**: +- 🔥 **任务分解原则**:单个AI处理超过3个文档时,应该分解给其他同事 +- 📝 **集中管理**:创建一个**唯一的**权威迁移记录,其他文档完全清理 +- ⚠️ **误解澄清**:AI同事可能误以为要"保留迁移历史",实际上是要**完全清理** + +**文档维护经验**: +- 技术迁移时应该保留历史决策记录,说明迁移原因 +- 清理过时文档的同时要更新所有相关引用 +- 建立完整的文档依赖关系图,确保清理时不遗漏 +- 及时更新技术栈索引和AI角色任务列表 + +### SyncGroup术语重命名批量处理 (2025年7月11日) +**任务背景**:人类完成了代码层面的SyncGroup重命名,需要同步更新所有文档和注释中的相关术语 + +**重命名规则**: +1. **类型名称**: + - `SyncGroupConfiguration` → `DirectorySyncingModel` + - `SyncGroupStatus` → `DirectorySyncingStatus` + - `SyncGroupViewModel` → `DirectorySyncingViewModel` + +2. **属性名称**: + - `SyncGroups` → `SyncDirectories` + - `syncGroups` → `syncDirectories` + +3. **中文术语**: + - `同步组` → `目录同步` + - `同步组配置` → `目录同步配置` + - `同步组列表` → `目录同步列表` + - (以及所有相关组合术语) + +**处理方式**: +1. **批量脚本**:创建PowerShell脚本进行批量替换,涵盖: + - `.github/knowledge/` 目录下所有文档 + - `src/DotNetCampus.Terminal/ViewModels/` 中的注释 + - `src/DotNetCampus.Terminal/Views/` 中的注释 + - `src/DotNetCampus.Terminal/FileSync/` 中的注释 + +2. **替换顺序**:按照优先级顺序替换,避免部分匹配问题: + - 先替换复合术语(如"同步组配置") + - 再替换简单术语(如"同步组") + - 最后处理变量名和属性名 + +3. **手动修正**:脚本完成后手动修正特殊情况: + - 变量名(如 `syncGroup` → `directorySyncing`) + - 方法名(如 `InitializeSyncGroups` → `InitializeDirectorySyncing`) + +**结果统计**: +- 总共更新了 **23个文件** +- 涵盖技术文档、代码注释、界面文档等 +- 额外手动修正了 **6处** 变量名和方法名 + +**经验总结**: +- **脚本优势**:批量处理大量文件,效率高,一致性好 +- **替换顺序很重要**:复合词优先,避免部分匹配导致的错误替换 +- **正则表达式**:使用词边界(`\b`)避免误替换相似词汇 +- **验证必不可少**:脚本完成后必须手动检查特殊情况 +- **保持术语一致性**:确保代码、文档、界面中的术语完全统一 + +**最佳实践**: +1. 大规模术语重命名应该先在代码中完成,再同步到文档 +2. 使用脚本处理批量替换,但要预留手动修正的时间 +3. 建立术语对照表,确保团队理解一致 +4. 及时更新 `.github/AI任务分工.md` 中的进度状态 diff --git "a/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\351\205\215\347\275\256\347\256\241\347\220\206\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\351\205\215\347\275\256\347\256\241\347\220\206\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 0000000..bfc565f --- /dev/null +++ "b/.github/knowledge/AI\345\215\217\344\275\234\347\273\217\351\252\214/\350\247\222\350\211\262\347\273\217\351\252\214\346\200\273\347\273\223/\351\205\215\347\275\256\347\256\241\347\220\206\344\270\223\345\256\266-\346\240\270\345\277\203\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,368 @@ +# 配置管理专家经验总结 + +## 必读提醒 +🔥 **在开始任何配置相关任务前,必须先阅读本文档!** + +## 重要更新 - JSON配置系统 + +> ✅ **实现完成**: JSON配置系统支持AOT编译。 + +### 实现架构 +- **JsonRemoteDeviceConfigurationSource** - JSON配置源实现 +- **ConfigurationJsonContext** - AOT源生成器上下文 +- **DeviceConfiguration** - JSON配置根对象 +- **ConfigurationManager** - 简化接口调用,无需类型转换 + +### 设计改进要点 +1. **接口优先**: ConfigurationManager直接使用IRemoteDeviceConfigurationSource,无需类型转换 +2. **简化设计**: 移除不必要的迁移工具,直接替换配置系统 +3. **AOT兼容**: 使用源生成器确保AOT编译支持 +4. **一致性**: 保持与现有模型SshRemoteDeviceInfo的兼容性 + +### 新技术栈 (JSON+源生成器) +- `System.Text.Json` - 官方JSON序列化库 +- `JsonSourceGenerationOptions` - AOT源生成器配置 +- `ConfigurationJsonContext` - 序列化上下文 + +### 配置模型类型 (保持不变) +```csharp +// 主配置模型 +public record DeviceConfiguration +{ + public List SshDevices { get; set; } = []; +} + +// SSH设备配置 +public record SshRemoteDeviceInfo : RemoteDeviceInfo +{ + public string LocalId { get; set; } // 本地唯一标识 + public string? RemoteId { get; set; } // 远程设备标识 + public string ConnectionName { get; set; } + public string Host { get; set; } + public int Port { get; set; } = 22; + public string UserName { get; set; } + public string? Password { get; set; } + public List SyncDirectories { get; set; } = []; +} +``` + +### 配置服务接口 (保持不变) +```csharp +public interface IDeviceConfigurationService +{ + Task LoadConfigurationAsync(); + Task SaveConfigurationAsync(DeviceConfiguration configuration); + string GetConfigurationSourcePath(); +} +``` + +## 设备唯一标识设计 + +### LocalId生成规则 +```csharp +// 生成16位随机标识符,避免重复 +private static string GenerateLocalId() +{ + return "device_" + Guid.NewGuid().ToString("N")[..16]; +} +``` + +### 重复设备检测 +- 基于 `LocalId` 进行设备去重 +- 避免因改名导致重复保存 +- 更新现有设备而不是创建新设备 + +## JSON配置文件格式 (新系统) + +### 标准结构 +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f6g7h8", + "remoteId": "ubuntu-dev-001", + "connectionName": "开发服务器", + "host": "192.168.1.100", + "port": 22, + "userName": "developer", + "password": "optional_password", + "syncDirectories": [ + { + "name": "项目代码", + "remotePath": "/home/developer/projects", + "localPath": "D:\\Projects", + "isEnabled": true + } + ] + } + ] +} +``` + +### 路径处理 +- Windows路径使用双反斜杠 `\\` 或正斜杠 `/` +- Linux路径始终使用正斜杠 `/` +- 相对路径自动转换为绝对路径 + +## 配置持久化策略 + +### 保存时机 +- 用户手动点击"保存"按钮 +- 设备信息变更时(通过变更跟踪) +- 程序退出时(可选自动保存) + +### JSON源生成器配置 +```csharp +[JsonSerializable(typeof(DeviceConfiguration))] +[JsonSourceGenerationOptions( + WriteIndented = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull +)] +public partial class ConfigurationJsonContext : JsonSerializerContext +{ +} +``` + +### 配置文件路径 +- 个人配置:`./Configs/devices.json` +- 团队配置:项目根目录 `terminal.json` +- 配置优先级:个人配置 > 团队配置 > 默认配置 + +## 配置文件位置最佳实践 + +### 当前采用位置 +- **主配置文件**: `./Configs/devices.json` +- **优势**: + - ✅ 语义明确,专门存储设备配置 + - ✅ 与程序资源分离,便于管理 + - ✅ 符合现代应用配置管理惯例 + - ✅ 便于扩展其他配置文件 + - ✅ AOT兼容,性能优异 + +## 过时技术栈 + +> ⚠️ **已移除**: 以下技术栈已完全移除,请使用JSON配置系统 + +### 已移除的配置格式 +- 配置解析库已移除 +- 配置文件格式已弃用 +- 构造函数初始化已重构为JSON源 + +## UI集成要点 + +### 保存按钮绑定 +```csharp +// ViewModel中的保存命令 +public AsyncCommand SaveCommand { get; } + +// 变更跟踪状态 +public bool HasChanges { get; set; } + +// 保存状态显示 +public bool IsSaving { get; set; } +public string SaveStatus { get; set; } = ""; +``` + +### 表单验证 +- 必填字段验证(Host, UserName, ConnectionName) +- 端口范围验证(1-65535) +- 路径有效性验证 +- 连接名重复检查 + +## 错误处理最佳实践 + +### 配置加载错误 +```csharp +try +{ + var config = await LoadConfigurationAsync(); + return config; +} +catch (JsonException ex) +{ + Log.Error($"[Config] JSON解析失败: {ex.Message}"); + return new DeviceConfiguration(); // 返回默认配置 +} +catch (FileNotFoundException) +{ + Log.Info($"[Config] 配置文件不存在,使用默认配置"); + return new DeviceConfiguration(); +} +``` + +### 配置保存错误 +```csharp +try +{ + await SaveConfigurationAsync(config); + Log.Info($"[Config] 配置保存成功: {configPath}"); +} +catch (UnauthorizedAccessException ex) +{ + Log.Error($"[Config] 权限不足,无法保存配置: {ex.Message}"); + throw new ConfigurationException("配置保存失败:权限不足"); +} +``` + +## 常见错误避坑 + +### ❌ 错误做法 +- 忘记设置 `LocalId` 导致重复设备 +- 直接使用 `ConnectionName` 作为唯一标识 +- 没有处理JSON解析异常 +- 忘记转义路径中的反斜杠 + +### ✅ 正确做法 +- 始终为新设备生成 `LocalId` +- 使用 `LocalId` 进行设备去重和更新 +- 完善异常处理和降级策略 +- 正确处理跨平台路径差异 + +## 性能优化要点 +- 配置文件大小控制(避免存储敏感信息) +- 延迟加载大型配置项 +- 配置变更时批量保存 +- 使用文件监视器检测外部修改 + +## 日志记录规范 +```csharp +Log.Info($"[Config] 加载配置文件: {configPath}"); +Log.Info($"[Config] 发现 {config.SshDevices.Count} 个SSH设备"); +Log.Warning($"[Config] 配置文件格式过旧,执行自动迁移"); +Log.Error($"[Config] 配置保存失败: {ex.Message}"); +``` + +## 相关知识库文档 +- `JSON-Configuration-System-Architecture.md` - JSON配置系统架构 +- `JSON-Configuration-Implementation-Experience.md` - 实现经验 +- `Device-Unique-ID-Design.md` - 设备唯一标识设计 +- `Configuration-Save-Feature-Guide.md` - 保存功能指南 + +## 发布版本配置优化 + +### v1.0 预览版优化 (2025年7月10日) +- ✅ **脱敏处理**:移除真实IP地址和敏感信息 +- ✅ **示例简化**:保留2个典型示例设备 +- ✅ **注释优化**:添加行级注释,简洁明了 +- ✅ **用户指导**:每个字段都有说明,快速上手 +- ✅ **密码安全**:示例中密码留空,推荐安全做法 + +### 脱敏后的示例特点 +```json +{ + // 使用通用的示例地址和用户名 + "host": "192.168.1.100", // 内网示例地址 + "host": "test.example.com", // 域名示例地址 + "userName": "developer", // 通用用户名 + "password": "" // 留空,推荐安全输入 +} +``` + +## 重要更新 - JSON配置系统优化完成 (2025年7月10日) + +> ✅ **优化完成**: JSON配置系统已完成用户体验优化,支持注释解析、减少属性标记、移除无用属性、重命名改进。 + +### 本次优化内容 +1. **✅ JSON注释支持**: 配置了`ReadCommentHandling = JsonCommentHandling.Skip`和`AllowTrailingCommas = true`,支持注释和尾随逗号 +2. **✅ 减少Attribute使用**: 移除了`DirectorySyncingModel`类中不必要的`JsonPropertyName`属性,依靠命名策略处理 +3. **✅ 移除无用属性**: 删除了`ExcludePatterns`属性,该功能暂时用不到 +4. **✅ 命名改进**: 将`SyncDirectories`重命名为`SyncDirectories`,语义更清晰(同步目录而非目录同步) +5. **✅ 保持源生成器**: 坚持使用AOT源生成器,确保性能和兼容性 + +### 核心改进实现 + +#### 1. JSON注释和尾随逗号支持 +```csharp +// JsonRemoteDeviceConfigurationSource.cs +var jsonOptions = new JsonSerializerOptions(ConfigurationJsonContext.Default.Options) +{ + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true +}; + +var deviceConfiguration = JsonSerializer.Deserialize( + jsonContent, + jsonOptions +); +``` + +#### 2. 简化的模型定义(移除不必要的JsonPropertyName) +```csharp +public class DirectorySyncingModel +{ + /// + /// 同步目录名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 远程路径 + /// + public string RemotePath { get; set; } = string.Empty; + + /// + /// 本地路径 + /// + public string LocalPath { get; set; } = string.Empty; + + /// + /// 是否启用 + /// + public bool Enabled { get; set; } = true; + + /// + /// 同步方向 + /// + public string Direction { get; set; } = "LocalToRemote"; + + // 移除了不需要的 ExcludePatterns 属性 +} +``` + +#### 3. 改进的配置文件格式 +```json +{ + // SSH设备配置文件,支持注释和尾随逗号 + "sshDevices": [ + { + // 本地唯一标识符,自动生成 + "localId": "device_example_001", + // 显示名称,可随时修改 + "connectionName": "开发服务器", + "host": "192.168.1.100", + "port": 22, + "userName": "developer", + // 同步目录配置(重命名为syncDirectories) + "syncDirectories": [ + { + "name": "项目产物", + "remotePath": "/home/developer/projects", + "localPath": "C:\\Projects\\Artifacts", + "enabled": true, + "direction": "RemoteToLocal" + } + ] + } + ] +} +``` + +### 重要经验教训 +1. **🔥 必须保持源生成器**: 绝对不能放弃源生成器以支持AOT编译,这是硬性要求 +2. **✅ 运行时配置优于编译时配置**: 通过`JsonSerializerOptions`在运行时配置注释支持,而不是在源生成器选项中配置 +3. **✅ 语义命名很重要**: `SyncDirectories`比`SyncDirectories`更准确描述功能 +4. **✅ 渐进式重构**: 先确保基本功能正常,再逐步移除不必要的代码 + +### 更新的文件清单 +- ✅ `JsonRemoteDeviceConfigurationSource.cs` - 添加注释支持 +- ✅ `ConfigurationJsonContext.cs` - 优化源生成器配置 +- ✅ `SyncModels.cs` - 简化属性标记、移除无用属性 +- ✅ `SshRemoteDeviceInfo.cs` - 重命名SyncGroups为SyncDirectories +- ✅ `SshRemoteDeviceInfoViewModel.cs` - 更新属性引用 +- ✅ `SshDeviceCommandsViewModel.cs` - 更新属性引用 +- ✅ `devices.json` - 更新示例配置文件 + +--- +*最后更新:2025年7月9日* +*下次更新时,请基于实际踩坑经验补充内容* diff --git a/.github/knowledge/README.md b/.github/knowledge/README.md new file mode 100644 index 0000000..ffea0a6 --- /dev/null +++ b/.github/knowledge/README.md @@ -0,0 +1,69 @@ +# 技术知识库 + +DotNetCampus Terminal 项目的技术文档和经验总结。 + +## � 快速开始 + +### AI工作者必读流程 +1. **确定角色** - 查看 [`.github/AI任务分工.md`](../AI任务分工.md) 确认你的职位 +2. **阅读经验总结** - 查看下方对应角色的核心经验文档 +3. **查阅技术文档** - 根据任务需要浏览相关技术资料 +4. **更新知识库** - 完成任务后及时补充新的经验 + +### 核心角色经验总结 +🎯 **开始任何工作前,必须先阅读对应角色的经验总结**: + +- **[UI界面设计师](AI协作经验/角色经验总结/UI界面设计师-核心经验总结.md)** - Consolonia UI设计和MVVM模式 +- **[文件同步工程师](AI协作经验/角色经验总结/文件同步工程师-核心经验总结.md)** - SFTP同步和性能优化 +- **[配置管理专家](AI协作经验/角色经验总结/配置管理专家-核心经验总结.md)** - JSON配置系统和AOT兼容 +- **[SSH连接专家](AI协作经验/角色经验总结/SSH连接专家-核心经验总结.md)** - SSH.NET连接和密钥管理 +- **[文档维护员](AI协作经验/角色经验总结/文档维护员-核心经验总结.md)** - 文档架构和协作流程 + +## 📚 技术文档结构 + +``` +📁 .github/knowledge/ +├── 🤖 AI协作经验/ # AI协作流程和角色经验 +│ ├── 角色经验总结/ # 各AI角色核心经验(必读) +│ ├── 实现经验总结/ # 具体功能实现踩坑记录 +│ └── AI多角色协作开发经验.md +├── 📖 依赖库文档/ # 第三方库使用指南 +│ ├── Consolonia/ # 控制台UI框架 +│ ├── SSH.NET/ # SSH连接库 +│ ├── DotNetCampus.Logger/ # 日志框架 +│ ├── System.Text.Json/ # JSON序列化 +│ └── DotNet9/ # .NET 9新特性 +├── 🏗️ 技术设计文档/ # 项目架构和设计方案 +│ ├── 界面设计/ # UI架构和交互设计 +│ ├── 配置管理/ # 配置系统设计 +│ ├── 文件同步/ # 同步机制设计 +│ └── SSH连接管理/ # SSH连接架构 +└── 🔧 问题排查/ # 常见问题解决方案 + └── 开发问题快速解答手册.md +``` + +## 🎯 快速查找指南 + +- **按角色查找** → `AI协作经验/角色经验总结/` → 对应角色文档 +- **按技术栈查找** → `依赖库文档/` → 对应库名 → 具体功能 +- **按功能模块查找** → `技术设计文档/` → 功能分类 → 具体设计 +- **遇到问题时** → `问题排查/开发问题快速解答手册.md` + +## � 维护说明 + +### 文档更新原则 +- **经验优先** - 重点记录实际开发中的有用经验 +- **避坑指南** - 详细记录踩坑过程和解决方案 +- **代码示例** - 提供可直接使用的代码片段 +- **及时更新** - 发现新问题或有新经验时立即更新 + +### 文件命名规范 +- 数字前缀排序:`01-`、`02-`、`03-`... +- 中文描述内容,提高可读性 +- 保持库名原始格式:`Consolonia`、`SSH.NET` + +--- + +💡 **提示**:完整的文档索引和编码规范请查看 [`.github/copilot-instructions.md`](../copilot-instructions.md) + +*维护者:文档维护员AI | 最后更新:2025年7月10日* diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/01-\345\277\253\351\200\237\345\217\202\350\200\203\346\214\207\345\215\227.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/01-\345\277\253\351\200\237\345\217\202\350\200\203\346\214\207\345\215\227.md" new file mode 100644 index 0000000..473030b --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/01-\345\277\253\351\200\237\345\217\202\350\200\203\346\214\207\345\215\227.md" @@ -0,0 +1,445 @@ +# Consolonia 快速参考指南 + +## 测量单位 + +**重要**: TUI 程序中的所有单位都是字符级的: +- `Width="10"` = 10个字符宽度 +- `Padding="1 0"` = 左右各1个字符 +- `Margin="0 1"` = 上下各1个字符 + +**核心原则**: 像素 = 字符,每个像素对应一个控制台字符 + +## 基本设置 + +### 1. 项目配置 +```xml + + +``` + +### 2. 程序入口 +```csharp +// Program.cs +AppBuilder.Configure() + .UseConsolonia() + .UseAutoDetectedConsole() + .UseAutoDetectConsoleColorMode() + .UseContainerServices() + .StartWithConsoleLifetime(args); +``` + +### 3. 数据节点接口 +```csharp +public interface IRemoteDeviceNode +{ + IReadOnlyList Children { get; } +} +``` + +**注意**: 这些 Node 类本质上是专门用于 TreeView 的 ViewModel,不是传统意义上的数据模型。 + +### 4. 应用程序配置 +```xml + + + + + + +``` + +## 常用控件 + +### 1. 基础布局 +```xml + + + + + + +``` + +### 2. 按钮样式 +```xml + +``` + +### 3. 边框样式 +```xml + + + + + +``` + +### 4. 树形控件 +```xml + + + + + + + + + + + + +``` + +## 数据绑定 + +### 1. 绑定模式 +```xml + + + + + + + + +``` + +### 2. 转换器 +```xml + + + + + + + +``` + +### 3. 数据模板 +```xml + + + + + + + + + + +``` + +## 样式系统 + +### 1. 选择器语法 +```xml + + + + + + + + + + + +``` + +### 2. 常用颜色 +```xml + +Background="Black" +Background="DimGray" +Background="Transparent" + + +Foreground="White" +Foreground="DarkGray" +Foreground="Green" +Foreground="Red" +Foreground="Orange" +``` + +## MVVM 模式 + +### 1. ViewModel 基类 +```csharp +public record DeviceViewModel : BindableRecord +{ + private string _name = string.Empty; + private ConnectionState _state; + + public string Name + { + get => _name; + set => SetField(ref _name, value); + } + + public ConnectionState State + { + get => _state; + set => SetField(ref _state, value); + } +} +``` + +### 2. 集合属性 +```csharp +// 使用 AvaloniaList 而不是 ObservableCollection +public AvaloniaList Devices { get; } = []; +``` + +### 3. 异步命令 +```csharp +public AsyncCommand RefreshCommand { get; } + +public MainViewModel() +{ + RefreshCommand = new AsyncCommand(OnRefreshAsync); +} + +private async Task OnRefreshAsync() +{ + // 异步操作 +} +``` + +## 性能优化 + +### 1. 虚拟化 +```xml + + + + + +``` + +### 2. 异步UI更新 +```csharp +// 确保UI更新在UI线程 +await Dispatcher.UIThread.InvokeAsync(() => +{ + Status = "已连接"; +}); +``` + +### 3. 绑定优化 +```xml + + + + + +``` + +## 常见问题 + +### 1. 控件不显示 +- 检查 DataTemplate 的 x:DataType 是否匹配 +- 确认 DataContext 是否正确设置 +- 验证 ItemsSource 绑定是否正确 + +### 2. 样式不生效 +- 检查选择器语法是否正确 +- 确认 console: 命名空间是否正确引用 +- 验证样式定义的位置和作用域 + +### 3. 数据绑定不工作 +- 确保 ViewModel 继承自 BindableRecord +- 检查属性名称是否正确 +- 验证绑定模式是否合适 + +### 4. 布局问题 +- 检查 Grid 的行列定义 +- 确认控件的 Grid.Row 和 Grid.Column 属性 +- 验证 Margin 和 Padding 设置 + +## 开发工具 + +### 1. 文件扩展名 +- XAML 文件使用 `.axaml` 扩展名 +- 代码隐藏使用 `.axaml.cs` 扩展名 + +### 2. 设计时支持 +```xml + + + +``` + +### 3. 调试技巧 +```csharp +// 在代码隐藏中添加调试输出 +System.Diagnostics.Debug.WriteLine($"DataContext: {DataContext?.GetType().Name}"); +``` + +## 项目特定约定 + +### 1. 命名空间 +```xml +xmlns:vm="using:DotNetCampus.Terminal.ViewModels" +xmlns:views="using:DotNetCampus.Terminal.Views" +xmlns:converters="using:DotNetCampus.Terminal.Views.Converters" +``` + +### 2. 数据节点接口 +```csharp +public interface IRemoteDeviceNode +{ + IReadOnlyList Children { get; } +} +``` + +### 3. 状态枚举 +```csharp +public enum ConnectionState +{ + Default, + Testing, + Online, + Offline +} +``` + +### 4. 主题配置 +```xml + + + + + + + +``` + +## 绘制系统 + +### 1. LineBrush 边框样式 +```xml + + + + + + + + + + + + + + + + + + + + +``` + +### 2. 线条和矩形 +```xml + + + + + +``` + +### 3. 字体样式 +```xml + + +``` + +## 进度条和数据绑定 + +### 1. 进度条基本用法 +```xml + +``` + +### 2. 动态显示隐藏 +```xml + + + + + +``` + +### 3. 只读属性绑定问题 +```csharp +// 当只读属性依赖其他属性时,需要手动触发更新通知 +public DirectorySyncingStatus Status +{ + get => _status; + set + { + if (SetField(ref _status, value)) + { + // 手动触发只读属性的更新通知 + OnPropertyChanged(nameof(StatusSymbol)); + OnPropertyChanged(nameof(StatusColor)); + } + } +} + +public string StatusSymbol => Status switch +{ + DirectorySyncingStatus.Normal => "✓", + DirectorySyncingStatus.Error => "⚠", + _ => "○" +}; +``` + +### 4. 进度报告模式 +```csharp +var progress = new Progress(p => +{ + GlobalProgress = p.TotalProgress; + CurrentItemProgress = p.CurrentProgress; +}); + +IsOperationRunning = true; +try +{ + await longRunningOperation(progress); +} +finally +{ + IsOperationRunning = false; +} +``` + +这个快速参考指南涵盖了 Consolonia 开发的核心要点,便于快速查阅和使用。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/02-\346\236\266\346\236\204\346\240\270\345\277\203\350\246\201\347\202\271.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/02-\346\236\266\346\236\204\346\240\270\345\277\203\350\246\201\347\202\271.md" new file mode 100644 index 0000000..5faf62b --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/02-\346\236\266\346\236\204\346\240\270\345\277\203\350\246\201\347\202\271.md" @@ -0,0 +1,144 @@ +# Consolonia 架构核心要点 + +> 为 UI 界面设计师准备的精炼架构指南 + +## 基本概念 + +### 1. 像素 = 字符 +- **核心原则**: 每个像素对应一个字符 +- **布局单位**: 所有尺寸都以字符为单位 +- **示例**: `Width="10"` = 10个字符宽度 + +### 2. 颜色系统 +每个字符具有三个特征: +- **前景色** (Foreground) +- **背景色** (Background) +- **显示字符** (包括字体样式) + +### 3. 字体限制 +- **固定字体**: 控制台使用等宽字体 +- **可用属性**: FontWeight、FontStyle、TextDecorations +- **不可用属性**: FontFamily、FontSize + +## 绘制系统 + +### 1. 线条绘制 +```xml + + +``` + +### 2. 边框绘制 +```xml + + + + + + + + + + + + +``` + +### 3. LineBrush 样式 +| 样式 | 描述 | +|------|------| +| `SingleLine` | 单线边框(默认) | +| `DoubleLine` | 双线边框 | +| `Edge` | 1/8 宽度边框 | +| `EdgeWide` | 1/4 宽度边框 | +| `Bold` | 全尺寸边框 | + +### 4. 矩形绘制 +```xml + +``` + +## 主题系统 + +### 1. 可用主题 +- **TurboVisionTheme**: 经典 Borland TurboVision 风格 + - `TurboVisionDarkTheme` - 深色主题 + - `TurboVisionLightTheme` - 浅色主题 + - `TurboVisionBlackTheme` - 黑白主题 +- **MaterialTheme**: Material Design 风格 +- **FluentTheme**: Fluent Design 风格 + +### 2. 主题配置 +```xml + + + + +``` + +## 布局约束 + +### 1. 单位换算 +- 1 像素 = 1 字符 +- 布局以字符为基本单位 +- Padding、Margin 都是字符级间距 + +### 2. 控件尺寸 +- 最小可视单位是 1 字符 +- 所有控件都按字符网格对齐 +- 无法实现亚字符级的精确定位 + +## 性能考虑 + +### 1. 渲染优化 +- 每个字符都是独立的渲染单元 +- 颜色变化会影响渲染性能 +- 大量控件时使用虚拟化 + +### 2. 布局优化 +- 简化布局层次 +- 避免过度嵌套 +- 合理使用 Grid 和 StackPanel + +## 图标与装饰 + +### 1. Unicode 字符 +- 支持完整的 Unicode 字符集 +- 可以使用 Emoji 作为图标 +- 利用 Unicode 框线字符绘制边框 + +### 2. 示例字符 +``` +┌─┬─┐ ╔═╦═╗ ▲ ▼ ◄ ► ■ □ ● ○ ★ ☆ ♠ ♥ ♦ ♣ +├─┼─┤ ╠═╬═╣ +└─┴─┘ ╚═╩═╝ +``` + +## 限制与注意事项 + +### 1. 不支持的功能 +- 任意角度的线条 +- 复杂的图形变换 +- 自定义字体 +- 亚字符级定位 + +### 2. 控制台特性 +- 终端窗口大小可能改变 +- 不同终端的字符显示可能不同 +- 颜色支持取决于终端能力 + +## 最佳实践 + +### 1. 设计原则 +- 保持简洁的布局 +- 使用对比鲜明的颜色 +- 充分利用 Unicode 字符 +- 考虑不同终端的兼容性 + +### 2. 用户体验 +- 提供清晰的视觉层次 +- 使用一致的颜色方案 +- 确保文本可读性 +- 合理使用空白空间 + +这个架构指南涵盖了 UI 界面设计师需要了解的核心概念,为高效开发 Consolonia 应用提供了必要的基础知识。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/03-UI\346\241\206\346\236\266\344\275\277\347\224\250.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/03-UI\346\241\206\346\236\266\344\275\277\347\224\250.md" new file mode 100644 index 0000000..352f818 --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/Consolonia/03-UI\346\241\206\346\236\266\344\275\277\347\224\250.md" @@ -0,0 +1,479 @@ +# Consolonia UI 框架学习笔记 + +## 概述 + +Consolonia 是一个基于 Avalonia UI 的控制台应用程序框架,允许开发者使用 XAML 和 MVVM 模式构建控制台应用程序。它提供了类似 WPF 的开发体验,但运行在控制台环境中。 + +## 技术栈 + +- **基础框架**: Avalonia UI +- **UI 标记**: XAML (.axaml 文件) +- **数据绑定**: MVVM 模式 +- **样式系统**: CSS-like 选择器 +- **布局系统**: Grid, StackPanel, DockPanel 等 +- **控件库**: 专门针对控制台优化的控件 + +## 项目结构分析 + +### 1. 应用程序入口点 + +```csharp +// Program.cs +public static AppBuilder BuildAvaloniaApp() +{ + return AppBuilder.Configure() + .UseConsolonia() // 启用 Consolonia 框架 + .UseAutoDetectedConsole() // 自动检测控制台 + .UseAutoDetectConsoleColorMode() // 自动检测颜色模式 + .UseContainerServices() // 使用依赖注入 + .LogToException(); +} +``` + +### 2. 应用程序配置 + +```xml + + + + + + +``` + +### 3. 主窗口结构 + +```xml + + + + + + + + + +``` + +**注意**: 在TUI模式下,主要使用 `MainView`,`MainWindow` 仅在需要GUI兼容性时使用。 + +## 核心概念 + +### 1. 命名空间 + +```xml +xmlns:console="https://github.com/jinek/consolonia" +``` + +所有 Consolonia 特有的功能都在这个命名空间下。 + +### 2. 测量单位 + +**重要**: 在 TUI(文本用户界面)程序中,所有的测量单位都是基于字符的: +- `Width="10"` 表示 10 个字符宽度 +- `Height="5"` 表示 5 个字符高度 +- `Padding="1 0"` 表示左右各留 1 个字符的空间 +- `Margin="0 1"` 表示上下各留 1 个字符的间距 + +### 3. 主题系统 + +- `TurboVisionDarkTheme`: 深色主题,类似传统终端界面 +- `RequestedThemeVariant="Dark"`: 请求深色变体 + +### 3. 布局控件 + +#### Grid 布局 +```xml + + + +``` + +#### StackPanel 布局 +```xml + + + +``` + +### 4. 控件特性 + +#### 按钮样式 +```xml + +``` + +#### 边框样式 +```xml + + + + + +``` + +## 样式系统 + +### 1. 选择器语法 + +```xml + + + + + + + +``` + +### 2. 常用样式属性 + +```xml + + + + + + + + + + +``` + +## 数据绑定 + +### 1. 基本绑定 + +```xml + + + + + + + + +``` + +### 2. 转换器 + +```xml + + + + + +``` + +### 3. 数据模板 + +```xml + + + + + + + +``` + +## 高级控件 + +### 1. TreeView + +```xml + + + + + + + +``` + +### 2. TabControl + +```xml + + + + + + + + +``` + +### 3. ContentControl + +```xml + + + + + + + +``` + +## MVVM 模式 + +### 1. ViewModel 基类 + +```csharp +public record BindableRecord : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } +} +``` + +### 2. 集合绑定 + +```csharp +public AvaloniaList RemoteDevices { get; } = []; +``` + +使用 `AvaloniaList` 而不是 `ObservableCollection`。 + +### 3. 命令绑定 + +```csharp +public AsyncCommand ReloadDevicesCommand { get; } +``` + +## 依赖注入集成 + +### 1. 服务配置 + +```csharp +public static AppBuilder UseContainerServices(this AppBuilder appBuilder) +{ + return appBuilder.UseContainer(s => s + .AddSingleton()); +} +``` + +### 2. 服务使用 + +```csharp +public class MainViewModel +{ + private readonly ConfigurationManager _configurationManager; + + public MainViewModel(IServiceProvider serviceProvider) + { + _configurationManager = serviceProvider.EnsureGet(); + } +} +``` + +## 线程和异步操作 + +### 1. UI 线程调度 + +```csharp +await Dispatcher.UIThread.InvokeAsync(() => +{ + ConnectionState = ConnectionState.Testing; +}); +``` + +### 2. 异步命令 + +```csharp +ReloadDevicesCommand = new AsyncCommand(OnReloadDevices); + +private async Task OnReloadDevices() +{ + // 异步操作 +} +``` + +## 特殊控件和效果 + +### 1. LineBrush + +```xml + + + +``` + +专门用于绘制控制台风格的线条。 + +### 2. ButtonExtensions + +```xml + + + + + +``` + +## 可维护性模式 + +### 1. 视图分离 + +#### 每个功能独立视图 +``` +Views/ +├── MainView.axaml # 主视图 +├── CreateNewRemoteDeviceView.axaml # 创建设备视图 +├── SshRemoteDeviceInfoView.axaml # SSH设备信息视图 +└── RemoteDeviceGroupView.axaml # 设备组视图 +``` + +### 2. 样式复用 + +#### 全局样式定义 +```xml + + + + + +``` + +### 3. 数据模型分层 + +#### 接口抽象 +```csharp +public interface IRemoteDeviceNode +{ + IReadOnlyList Children { get; } +} +``` + +#### 具体实现 +```csharp +public record CreateNewRemoteDeviceNode : IRemoteDeviceNode +public record FavoriteDeviceGroupNode : IRemoteDeviceNode +public record RemoteDeviceGroupNode : IRemoteDeviceNode +public record SshRemoteDeviceInfoNode : IRemoteDeviceNode +``` + +## 开发建议 + +### 1. 新功能开发流程 + +1. **定义数据模型**: 创建 ViewModel 和 Model +2. **设计视图结构**: 创建 XAML 布局 +3. **实现数据绑定**: 连接 ViewModel 和 View +4. **添加样式**: 定义外观和交互效果 +5. **优化性能**: 使用虚拟化和异步操作 +6. **测试交互**: 验证用户体验 + +### 2. 调试技巧 + +#### 设计时数据 +```xml + + + +``` + +#### 属性检查 +```xml + +``` + +### 3. 常见陷阱 + +- **忘记设置 DataContext**: 检查数据绑定是否正确 +- **样式选择器错误**: 验证选择器语法 +- **线程问题**: 确保UI更新在UI线程 +- **性能问题**: 避免过度绑定和频繁更新 + +### 4. 扩展性考虑 + +- **新设备类型**: 通过 IRemoteDeviceNode 接口扩展 +- **新视图**: 通过 DataTemplate 添加 +- **新样式**: 通过样式选择器定制 +- **新功能**: 通过 ViewModel 扩展 + +这个设计模式文档为后续的 UI 界面设计师提供了完整的参考框架,涵盖了从基础概念到高级模式的所有内容。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/DotNet9/01-\346\226\260\347\211\271\346\200\247\345\234\250\351\241\271\347\233\256\344\270\255\347\232\204\345\272\224\347\224\250.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/DotNet9/01-\346\226\260\347\211\271\346\200\247\345\234\250\351\241\271\347\233\256\344\270\255\347\232\204\345\272\224\347\224\250.md" new file mode 100644 index 0000000..9d771fa --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/DotNet9/01-\346\226\260\347\211\271\346\200\247\345\234\250\351\241\271\347\233\256\344\270\255\347\232\204\345\272\224\347\224\250.md" @@ -0,0 +1,57 @@ +# .NET 9.0 新特性应用 + +记录在项目中如何应用 .NET 9.0 的新特性来提升开发效率和性能。 + +## 新特性概览 + +### 1. 性能改进 +[待补充在项目中的应用] + +### 2. C# 12 语言特性 +[待补充在项目中的应用] + +### 3. 库改进 +[待补充在项目中的应用] + +## 项目中的应用 + +### 1. Primary Constructors +```csharp +// 示例:在配置类中使用主构造函数 +public class DeviceManager(IConfigurationManager configManager, ILogger logger) : IDeviceManager +{ + public async Task> GetDevicesAsync() + { + logger.LogInformation("开始获取设备列表"); + // 实现逻辑 + return await configManager.LoadDevicesAsync(); + } +} +``` + +### 2. Collection Expressions +[待补充具体应用示例] + +### 3. Required Members +```csharp +// 示例:在数据模型中使用required关键字 +public record Device +{ + public required string Id { get; init; } + public required string Name { get; init; } + public required DeviceType Type { get; init; } + public SshConfiguration? SshConfig { get; init; } +} +``` + +## 最佳实践 + +[由各AI在使用过程中补充] + +## 兼容性注意事项 + +[待补充兼容性相关的注意事项] + +--- + +**注意**: 这是一个框架文档,请各位AI在使用.NET 9.0新特性的过程中,将实际经验和最佳实践补充到这个文档中。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/DotNetCampus.Logger/01-\346\227\245\345\277\227\346\241\206\346\236\266\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/DotNetCampus.Logger/01-\346\227\245\345\277\227\346\241\206\346\236\266\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 0000000..6f308cd --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/DotNetCampus.Logger/01-\346\227\245\345\277\227\346\241\206\346\236\266\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,220 @@ +# DotNetCampus.Logger 使用指南 + +DotNetCampus.Logger 是一个高性能的 .NET 日志库,支持源生成器,提供零依赖的日志记录能力。 + +## 1. 核心特性 + +### 源生成器支持 +- 使用源生成器生成日志代码,无运行时依赖 +- 支持编译时条件编译(TRACE、DEBUG) +- 自动生成高性能的日志记录代码 + +### 静态方法调用 +- 提供静态 `Log` 类,可在任何地方直接使用 +- 无需依赖注入,简化使用流程 +- 与传统依赖注入方式性能等价 + +### 多级别日志支持 +- 支持 Trace、Debug、Info、Warn、Error、Fatal 六个级别 +- 支持条件编译的调试日志 +- 灵活的日志级别控制 + +## 2. 基础使用 + +### 安装 +```xml + +``` + +### 初始化 +```csharp +new LoggerBuilder() + .WithLevel(LogLevel.Debug) + .AddWriter(new ConsoleLogger()) + .Build() + .IntoGlobalStaticLog(); +``` + +### 基本日志记录 +```csharp +// 基本日志级别(始终编译) +Log.Trace("这是追踪信息"); +Log.Debug("这是调试信息"); +Log.Info("这是一般信息"); +Log.Warn("这是警告信息"); +Log.Error("这是错误信息"); +Log.Fatal("这是致命错误"); + +// 条件编译日志(仅在 DEBUG 模式下编译) +Log.DebugLogger.Info("调试模式下的信息"); +Log.DebugLogger.Error("调试模式下的错误"); + +// 条件编译日志(仅在 TRACE 模式下编译) +Log.TraceLogger.Debug("追踪模式下的调试信息"); +``` + +### 带标签的日志记录 +```csharp +// 使用标签便于过滤和分类 +Log.Info("[FileSync] 开始同步文件"); +Log.Error("[Network] 连接超时"); +Log.Debug("[UI] 用户点击了按钮"); +``` + +## 3. 日志级别说明 + +### 级别定义 +- **Trace**: 最详细的信息,通常仅在追踪问题时使用 +- **Debug**: 调试信息,用于开发阶段问题诊断 +- **Info**: 一般信息,记录程序正常运行的关键节点 +- **Warn**: 警告信息,程序可以继续运行但存在潜在问题 +- **Error**: 错误信息,程序遇到错误但可以恢复 +- **Fatal**: 致命错误,程序无法继续运行 + +### 使用建议 +```csharp +// Info - 记录重要的业务流程 +Log.Info("[FileSync] 开始同步目录 ProjectA: /local/path -> /remote/path"); + +// Debug - 记录详细的执行步骤 +Log.Debug("[FileSync] 正在上传文件: file.txt -> /remote/file.txt"); + +// Warn - 记录可恢复的问题 +Log.Warn("[FileSync] 同步操作被用户取消"); + +// Error - 记录错误但程序可继续 +Log.Error("[FileSync] 上传文件失败: file.txt. 错误: 权限不足"); + +// Fatal - 记录致命错误 +Log.Fatal("[System] 系统内存不足,程序即将退出"); +``` + +## 4. 高级特性 + +### 日志过滤 +支持通过命令行参数过滤日志: +```bash +# 只显示包含 FileSync 标签的日志 +--log-console-tags FileSync + +# 显示包含 FileSync 或 Network 标签的日志 +--log-console-tags FileSync,Network + +# 必须同时包含 FileSync 和 Debug 标签 +--log-console-tags FileSync,+Debug + +# 排除包含 UI 标签的日志 +--log-console-tags FileSync,-UI +``` + +### 多Writer支持 +```csharp +new LoggerBuilder() + .WithLevel(LogLevel.Debug) + .AddWriter(new ConsoleLogger()) + .AddWriter(new FileLogger("app.log")) + .Build() + .IntoGlobalStaticLog(); +``` + +### 内存缓存 +```csharp +new LoggerBuilder() + .WithMemoryCache() // 在日志系统初始化前也可以使用日志 + .WithLevel(LogLevel.Debug) + .AddWriter(new ConsoleLogger()) + .Build() + .IntoGlobalStaticLog(); +``` + +## 5. 在 DotNetCampus.Terminal 中的应用 + +### 项目配置 +项目已在 `Startup.cs` 中完成了日志系统的初始化: + +```csharp +.AddSingleton(_ => new LoggerBuilder() + .WithLevel(LogLevel.Information) + .AddWriter(new EmptyLogger()) + .Build() + .IntoGlobalStaticLog()) +``` + +### 使用模式 +在项目中,我们采用以下日志记录模式: + +```csharp +// 文件同步服务中的日志记录 +Log.Info("[FileSync] 开始同步目录 {directorySyncing.Name}"); +Log.Debug("[FileSync] 正在上传文件: {localFile} -> {remoteFile}"); +Log.Error("[FileSync] 同步失败: {error.Message}"); + +// UI 层的日志记录 +Log.Info("[UI] 用户启动同步操作"); +Log.Warn("[UI] 没有启用的目录同步,跳过同步"); +Log.Error("[UI] 文件同步服务未初始化"); +``` + +### 标签约定 +- `[FileSync]`: 文件同步相关操作 +- `[UI]`: 用户界面相关操作 +- `[SSH]`: SSH 连接相关操作 +- `[Config]`: 配置管理相关操作 + +## 6. 性能优化 + +### 源生成器优势 +- 编译时生成代码,运行时零反射 +- 条件编译支持,调试日志在 Release 版本中完全移除 +- 内联优化,减少方法调用开销 + +### 最佳实践 +```csharp +// 推荐:使用插值字符串,简洁易读 +Log.Info($"[FileSync] 处理文件 {fileName},大小 {fileSize} 字节"); + +// 推荐:对于复杂格式化,使用条件判断避免不必要的字符串构造 +if (Log.Current.IsEnabled(LogLevel.Debug)) +{ + Log.Debug($"[FileSync] 详细进度信息: {GetDetailedProgress()}"); +} + +// 避免:在循环中频繁记录详细日志 +// 建议使用批量日志或者定期日志 +``` + +## 7. 故障排除 + +### 常见问题 + +**问题**: 日志没有输出 +- 检查日志级别设置是否正确 +- 确认 Writer 是否正确添加 +- 验证是否调用了 `IntoGlobalStaticLog()` + +**问题**: 条件编译日志不生效 +- 检查项目的编译条件(DEBUG、TRACE) +- 确认使用的是正确的 Logger(`Log.DebugLogger` 等) + +**问题**: 性能问题 +- 检查是否在循环中记录过多日志 +- 考虑提高日志级别,减少不必要的日志输出 +- 使用异步 Writer 处理大量日志 + +## 8. 与其他日志库的对比 + +### 优势 +- **零依赖**: 使用源生成器,无运行时依赖 +- **高性能**: 编译时优化,运行时开销极小 +- **简单易用**: 静态方法调用,无需依赖注入 +- **条件编译**: 调试日志在发布版本中完全移除 + +### 适用场景 +- 库项目,不希望引入日志依赖 +- 性能敏感的应用程序 +- 需要简单易用的日志系统 +- 多模块项目的日志统一管理 + +## 总结 + +DotNetCampus.Logger 通过源生成器技术,提供了一个高性能、零依赖的日志解决方案。在 DotNetCampus.Terminal 项目中,我们充分利用其静态方法调用的便利性和标签过滤功能,实现了结构化的日志记录,便于问题诊断和系统监控。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/SSH.NET/01-\345\237\272\347\241\200\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/SSH.NET/01-\345\237\272\347\241\200\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 0000000..d63ed93 --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/SSH.NET/01-\345\237\272\347\241\200\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,126 @@ +# SSH.NET 使用指南 + +SSH.NET 是一个用于SSH连接的 .NET 库。项目中使用版本:`2024.1.0` + +## 基本概念 + +SSH.NET提供了完整的SSH客户端功能,包括: +- SSH连接和认证 +- 命令执行 +- SFTP文件传输 +- 端口转发 + +## 项目中的应用场景 + +### 1. 基本SSH连接 +[待补充具体示例] + +### 2. SFTP文件同步 +[待补充具体示例] + +### 3. 连接状态监控 +[待补充具体示例] + +### 4. 批量命令执行 +[待补充具体示例] + +## 认证方式 + +### 1. 密码认证 +```csharp +var connectionInfo = new ConnectionInfo("hostname", "username", + new PasswordAuthenticationMethod("username", "password")); + +using var client = new SshClient(connectionInfo); +client.Connect(); +``` + +### 2. 私钥认证 +```csharp +// 无密码短语的私钥 +var keyFile = new PrivateKeyFile(@"C:\Users\YourUser\.ssh\id_rsa"); +var connectionInfo = new ConnectionInfo("hostname", "username", + new PrivateKeyAuthenticationMethod("username", keyFile)); + +// 有密码短语的私钥 +var keyFileWithPassphrase = new PrivateKeyFile(@"C:\Users\YourUser\.ssh\id_rsa", "passphrase"); +var connectionInfoSecure = new ConnectionInfo("hostname", "username", + new PrivateKeyAuthenticationMethod("username", keyFileWithPassphrase)); +``` + +### 3. 组合认证(推荐) +```csharp +// 优先尝试密钥认证,失败时回退到密码认证 +var connectionInfo = new ConnectionInfo("hostname", "username", + new PrivateKeyAuthenticationMethod("username", keyFile), + new PasswordAuthenticationMethod("username", "password")); +``` + +### 4. 全局SSH密钥认证(推荐) +```csharp +// 自动检测和使用全局SSH密钥 +public static ConnectionInfo CreateGlobalKeyConnection(string hostname, int port, string username) +{ + var globalKeyPath = FindGlobalPrivateKey(); + if (globalKeyPath != null) + { + var keyFile = new PrivateKeyFile(globalKeyPath); + return new ConnectionInfo(hostname, port, username, + new PrivateKeyAuthenticationMethod(username, keyFile)); + } + + // 如果没有全局密钥,回退到密码认证 + throw new InvalidOperationException("未找到全局SSH私钥,请先配置SSH密钥或使用密码认证"); +} + +private static string? FindGlobalPrivateKey() +{ + var sshDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh"); + var keyNames = new[] { "id_ed25519", "id_rsa", "id_ecdsa" }; + + foreach (var keyName in keyNames) + { + var keyPath = Path.Combine(sshDir, keyName); + if (File.Exists(keyPath)) + { + return keyPath; + } + } + + return null; +} +``` + +## 错误处理 + +### 1. 连接失败处理 +[待补充示例] + +### 2. 认证失败处理 +[待补充示例] + +### 3. 网络中断处理 +[待补充示例] + +## 性能优化 + +### 1. 连接池管理 +[待补充示例] + +### 2. 并发连接控制 +[待补充示例] + +### 3. 超时配置 +[待补充示例] + +## 最佳实践 + +[由各AI在实际使用过程中补充] + +## 常见问题 + +[待补充常见问题和解决方案] + +--- + +**注意**: 这是一个框架文档,请各位AI在使用SSH.NET的过程中,将实际经验、示例代码、遇到的问题和解决方案补充到这个文档中。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/SSH.NET/02-\346\226\207\344\273\266\345\220\214\346\255\245\345\256\236\347\216\260.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/SSH.NET/02-\346\226\207\344\273\266\345\220\214\346\255\245\345\256\236\347\216\260.md" new file mode 100644 index 0000000..e1268ee --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/SSH.NET/02-\346\226\207\344\273\266\345\220\214\346\255\245\345\256\236\347\216\260.md" @@ -0,0 +1,384 @@ +# SSH.NET 文件同步使用指南 + +本文档提供了使用 SSH.NET 进行 SFTP 文件同步的核心知识和最佳实践。 + +## 1. 基础概念 + +### SSH.NET 库概述 + +SSH.NET 是一个基于 .NET 的 SSH 客户端库,提供了丰富的 SSH 功能,包括: + +- SSH 命令执行(`SshClient`) +- SFTP 文件传输(`SftpClient`) +- SCP 文件传输 +- 端口转发 +- 交互式 Shell + +### 主要组件 + +SSH.NET 中的主要类型包括: + +- `SshClient` - 用于执行 SSH 命令 +- `SftpClient` - 用于 SFTP 文件传输 +- `ScpClient` - 用于 SCP 文件传输 +- `ForwardedPort` - 用于端口转发 +- `ShellStream` - 用于交互式 Shell + +## 2. SFTP 文件传输 + +### 建立连接 + +```csharp +// 基于用户名密码的连接 +using var client = new SftpClient("host", port, "username", "password"); +client.Connect(); + +// 基于私钥的连接 +using var client = new SftpClient("host", port, "username", new PrivateKeyFile("path/to/key")); +client.Connect(); +``` + +### 上传文件 + +```csharp +// 上传文件(覆盖) +using (var fileStream = File.OpenRead("localPath")) +{ + client.UploadFile(fileStream, "remotePath", true); +} + +// 带进度的上传 +using (var fileStream = File.OpenRead("localPath")) +{ + client.UploadFile(fileStream, "remotePath", true, progress => + { + double percent = (double)progress / fileStream.Length * 100; + Console.WriteLine($"上传进度: {percent:F2}%"); + }); +} +``` + +### 下载文件 + +```csharp +// 下载文件 +using (var fileStream = File.Create("localPath")) +{ + client.DownloadFile("remotePath", fileStream); +} + +// 带进度的下载 +using (var fileStream = File.Create("localPath")) +{ + client.DownloadFile("remotePath", fileStream, progress => + { + var fileInfo = client.GetAttributes("remotePath"); + double percent = (double)progress / fileInfo.Size * 100; + Console.WriteLine($"下载进度: {percent:F2}%"); + }); +} +``` + +### 列出目录 + +```csharp +// 列出目录内容 +var files = client.ListDirectory("/path/to/dir"); +foreach (var file in files) +{ + Console.WriteLine($"{file.FullName} - {file.Length} bytes"); +} +``` + +### 文件和目录操作 + +```csharp +// 创建目录 +client.CreateDirectory("/path/to/newDir"); + +// 检查文件或目录是否存在 +bool exists = client.Exists("/path/to/file"); + +// 删除文件 +client.DeleteFile("/path/to/file"); + +// 删除目录 +client.DeleteDirectory("/path/to/dir"); + +// 重命名文件或目录 +client.RenameFile("/path/to/oldName", "/path/to/newName"); +``` + +## 3. 异步操作 + +虽然 SSH.NET 的 API 主要是同步的,但可以通过 Task.Run 在后台线程上执行: + +```csharp +await Task.Run(() => +{ + using (var client = new SftpClient(host, port, username, password)) + { + client.Connect(); + client.UploadFile(stream, remotePath); + client.Disconnect(); + } +}); +``` + +## 4. 错误处理最佳实践 + +```csharp +try +{ + client.Connect(); + + if (!client.IsConnected) + { + throw new Exception("无法连接到服务器"); + } + + // 执行操作... +} +catch (Renci.SshNet.Common.SshConnectionException ex) +{ + Console.WriteLine($"SSH连接错误: {ex.Message}"); +} +catch (Renci.SshNet.Common.SftpPermissionDeniedException ex) +{ + Console.WriteLine($"SFTP权限错误: {ex.Message}"); +} +catch (Exception ex) +{ + Console.WriteLine($"发生错误: {ex.Message}"); +} +finally +{ + if (client.IsConnected) + { + client.Disconnect(); + } +} +``` + +## 5. 性能优化技巧 + +### 文件传输缓冲区 + +默认缓冲区大小可能不是最优的。对于大文件传输,可以考虑调整缓冲区大小: + +```csharp +// 设置缓冲区大小为8MB +client.BufferSize = 8 * 1024 * 1024; +``` + +### 批量操作 + +对于多文件操作,保持连接打开状态比反复连接和断开效率更高: + +```csharp +client.Connect(); +try +{ + foreach (var file in files) + { + // 上传或下载文件 + } +} +finally +{ + client.Disconnect(); +} +``` + +### 异步和并行 + +对于多文件传输,可以考虑并行处理,但要注意控制并发度,避免过度消耗资源: + +```csharp +var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; +await Parallel.ForEachAsync(files, options, async (file, token) => +{ + using var client = new SftpClient(host, port, username, password); + await Task.Run(() => + { + client.Connect(); + // 上传或下载文件 + client.Disconnect(); + }, token); +}); +``` + +## 6. 安全性考虑 + +### 密码处理 + +避免在代码中硬编码密码,考虑使用: + +- 配置文件(加密存储) +- 环境变量 +- 密钥库(如Azure Key Vault) +- 私钥认证(而非密码) + +### 密钥认证 + +使用密钥认证通常比密码认证更安全: + +```csharp +var pkFile = new PrivateKeyFile("path/to/key", "passphrase"); +using var client = new SftpClient(host, port, username, pkFile); +``` + +### 连接加密 + +SSH.NET 支持多种加密方法: + +- aes128-ctr, aes192-ctr, aes256-ctr +- aes128-gcm@openssh.com, aes256-gcm@openssh.com +- chacha20-poly1305@openssh.com +- aes128-cbc, aes192-cbc, aes256-cbc +- 3des-cbc + +在需要时可以配置连接参数来使用特定的加密方法。 + +## 7. 常见问题解决 + +### 连接被拒绝 + +- 检查主机地址和端口是否正确 +- 确保防火墙未阻止连接 +- 验证服务器上的SSH服务是否运行 + +### 认证失败 + +- 检查用户名和密码是否正确 +- 验证密钥文件路径和格式 +- 确认服务器上的授权配置 + +### 权限问题 + +- 检查远程文件/目录的权限 +- 确保用户有适当的读写权限 +- 尝试使用具有更高权限的用户 + +### 连接超时 + +- 增加连接超时时间:`client.ConnectionInfo.Timeout = TimeSpan.FromMinutes(5);` +- 实现重试机制 +- 检查网络连接质量 + +## 8. 实际使用案例 + +### 增量同步 + +```csharp +// 获取远程文件列表 +var remoteFiles = client.ListDirectory(remotePath) + .Where(f => !f.IsDirectory) + .ToDictionary(f => f.FullName, f => f.LastWriteTime); + +// 获取本地文件列表 +var localFiles = Directory.GetFiles(localPath, "*", SearchOption.AllDirectories) + .ToDictionary(f => Path.Combine(remotePath, f.Substring(localPath.Length).TrimStart('\\')).Replace('\\', '/'), + f => File.GetLastWriteTime(f)); + +// 找出需要更新的文件 +var filesToUpdate = localFiles + .Where(lf => !remoteFiles.ContainsKey(lf.Key) || remoteFiles[lf.Key] < lf.Value) + .Select(lf => lf.Key) + .ToList(); + +// 上传需要更新的文件 +foreach (var file in filesToUpdate) +{ + var localFile = Path.Combine(localPath, file.Substring(remotePath.Length).TrimStart('/').Replace('/', '\\')); + using (var fs = File.OpenRead(localFile)) + { + client.UploadFile(fs, file); + } +} +``` + +### 递归创建远程目录 + +```csharp +public void EnsureRemoteDirectoryExists(SftpClient client, string remoteDirectory) +{ + string[] directories = remoteDirectory.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + string currentPath = "/"; + + foreach (string directory in directories) + { + currentPath = Path.Combine(currentPath, directory).Replace('\\', '/'); + + if (!client.Exists(currentPath)) + { + client.CreateDirectory(currentPath); + } + } +} +``` + +## 9. 版本兼容性 + +SSH.NET 支持以下目标框架: + +- .NET Framework 4.6.2 及更高版本 +- .NET Standard 2.0 +- .NET 8.0 及更高版本 + +在项目中安装 SSH.NET: + +```powershell +dotnet add package SSH.NET +``` + +## 10. 扩展与集成 + +### 与进度报告集成 + +```csharp +public async Task UploadWithProgressAsync(string localPath, string remotePath, IProgress progress, CancellationToken cancellationToken) +{ + await Task.Run(() => + { + using var fileStream = File.OpenRead(localPath); + long fileSize = fileStream.Length; + + client.UploadFile(fileStream, remotePath, true, uploadedBytes => + { + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(); + } + + double percentage = (double)uploadedBytes / fileSize * 100; + progress?.Report(percentage); + }); + }, cancellationToken); +} +``` + +### 与日志系统集成 + +```csharp +public async Task SyncDirectoryAsync(string localPath, string remotePath, ILogger logger) +{ + logger.LogInformation("开始同步 {LocalPath} 到 {RemotePath}", localPath, remotePath); + + try + { + // 执行同步... + + logger.LogInformation("同步完成"); + } + catch (Exception ex) + { + logger.LogError(ex, "同步过程中发生错误"); + throw; + } +} +``` + +## 总结 + +SSH.NET 提供了强大而灵活的SSH和SFTP功能,适用于各种远程文件操作场景。通过本文档的最佳实践,可以高效、安全地实现文件同步功能,并解决常见的问题。对于DotNetCampus.Terminal项目,我们利用SSH.NET实现了高效的远程文件同步功能,支持进度报告、取消操作和错误处理。 diff --git "a/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/System.Text.Json/01-JSON\351\205\215\347\275\256\347\263\273\347\273\237\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/System.Text.Json/01-JSON\351\205\215\347\275\256\347\263\273\347\273\237\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 0000000..3205a9b --- /dev/null +++ "b/.github/knowledge/\344\276\235\350\265\226\345\272\223\346\226\207\346\241\243/System.Text.Json/01-JSON\351\205\215\347\275\256\347\263\273\347\273\237\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,487 @@ +# System.Text.Json 使用指南 + +System.Text.Json 是 .NET 内置的高性能 JSON 序列化库,支持 AOT 编译和源生成器,为项目提供高性能的配置系统。 + +## 核心特性 + +### AOT 兼容性 +- 使用源生成器避免运行时反射 +- 零依赖的 JSON 序列化 +- 支持静态编译优化 + +### 高性能序列化 +- 基于 Span<T> 的内存高效处理 +- 预编译序列化逻辑 +- 支持异步流处理 + +## 项目中的应用场景 + +### 1. 设备配置管理 +提供 AOT 兼容的设备配置存储,支持高性能的JSON配置文件读写。 + +### 2. 高性能序列化 +利用源生成器实现零反射的高性能配置读写。 + +## 基础使用 + +### 安装 +```xml + + +``` + +## 核心组件 + +### 1. JSON源生成器上下文 + +```csharp +// Configurations/DataSources/ConfigurationJsonContext.cs +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(DeviceConfiguration))] +[JsonSerializable(typeof(SshRemoteDeviceInfo))] +[JsonSerializable(typeof(DirectorySyncingModel))] +[JsonSourceGenerationOptions( + WriteIndented = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + GenerationMode = JsonSourceGenerationMode.Default, + UseStringEnumConverter = true +)] +public partial class ConfigurationJsonContext : JsonSerializerContext +{ +} +``` + +**关键特性说明**: +- `WriteIndented = true`: 生成格式化的JSON,便于手工编辑 +- `PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase`: 使用驼峰命名 +- `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`: 忽略null值 +- `GenerationMode = JsonSourceGenerationMode.Default`: 完整功能模式 +- `UseStringEnumConverter = true`: 枚举序列化为字符串 + +### 2. JSON配置源实现 + +```csharp +// Configurations/DataSources/JsonRemoteDeviceConfigurationSource.cs +public class JsonRemoteDeviceConfigurationSource : IRemoteDeviceConfigurationSource +{ + private readonly string _configurationPath; + + public JsonRemoteDeviceConfigurationSource() + { + var basePath = Path.GetDirectoryName(Environment.ProcessPath)!; + _configurationPath = Path.Combine(basePath, "Configs", "devices.json"); + } + + public string GroupName => "JSON 配置文件"; + + public async Task> FetchRemoteDevicesAsync() + { + try + { + if (!File.Exists(_configurationPath)) + return []; + + var jsonContent = await File.ReadAllTextAsync(_configurationPath); + if (string.IsNullOrWhiteSpace(jsonContent)) + return []; + + var deviceConfiguration = JsonSerializer.Deserialize( + jsonContent, + ConfigurationJsonContext.Default.DeviceConfiguration + ); + + return deviceConfiguration?.SshDevices?.Cast().ToList() ?? []; + } + catch (JsonException ex) + { + Log.Error($"[Config] JSON配置文件格式错误: {ex.Message}"); + return []; + } + catch (Exception ex) + { + Log.Error($"[Config] 加载配置文件失败: {ex.Message}"); + return []; + } + } + + public async Task SaveRemoteDeviceAsync(IRemoteDeviceInfo deviceInfo) + { + if (deviceInfo is not SshRemoteDeviceInfo sshDeviceInfo) + { + Log.Error($"[Config] 不支持的设备类型: {deviceInfo.GetType().Name}"); + return; + } + + try + { + // 加载现有配置 + var existingDevices = await FetchRemoteDevicesAsync(); + var deviceList = existingDevices.OfType().ToList(); + + // 更新或添加设备 + var existingIndex = deviceList.FindIndex(d => d.LocalId == sshDeviceInfo.LocalId); + if (existingIndex >= 0) + { + deviceList[existingIndex] = sshDeviceInfo; + Log.Info($"[Config] 更新现有设备配置: {sshDeviceInfo.ConnectionName}"); + } + else + { + deviceList.Add(sshDeviceInfo); + Log.Info($"[Config] 添加新设备配置: {sshDeviceInfo.ConnectionName}"); + } + + // 保存配置 + var configuration = new DeviceConfiguration { SshDevices = deviceList }; + var jsonContent = JsonSerializer.Serialize( + configuration, + ConfigurationJsonContext.Default.DeviceConfiguration + ); + + Directory.CreateDirectory(Path.GetDirectoryName(_configurationPath)!); + await File.WriteAllTextAsync(_configurationPath, jsonContent); + + Log.Info($"[Config] 配置文件保存成功: {_configurationPath}"); + } + catch (Exception ex) + { + Log.Error($"[Config] 保存配置文件失败: {ex.Message}"); + throw; + } + } +} +``` + +## 配置文件格式 + +### JSON配置文件示例 (devices.json) + +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f6g7h8", + "remoteId": "ubuntu-dev-001", + "connectionName": "开发服务器", + "host": "192.168.1.100", + "port": 22, + "userName": "developer", + "password": "optional_password", + "syncDirectories": [ + { + "name": "项目代码", + "remotePath": "/home/developer/projects", + "localPath": "D:\\Projects", + "isEnabled": true, + "syncDirection": "RemoteToLocal", + "excludePatterns": [ + "*.tmp", + "node_modules/", + ".git/" + ] + }, + { + "name": "配置文件", + "remotePath": "/etc/myapp", + "localPath": "D:\\Config\\MyApp", + "isEnabled": false, + "syncDirection": "RemoteToLocal" + } + ] + }, + { + "localId": "device_b9c8d7e6f5g4h3i2", + "connectionName": "测试服务器", + "host": "test.example.com", + "port": 2222, + "userName": "testuser", + "syncDirectories": [] + } + ] +} +``` + +### 配置文件结构说明 + +- **sshDevices**: SSH设备配置数组 + - **localId**: 本地唯一标识符(必需) + - **remoteId**: 远程设备标识符(可选) + - **connectionName**: 连接显示名称(必需) + - **host**: 主机地址(必需) + - **port**: SSH端口(默认22) + - **userName**: 用户名(必需) + - **password**: 密码(可选,建议使用密钥认证) + - **SyncDirectories**: 目录同步配置数组 + - **name**: 目录同步名称 + - **remotePath**: 远程路径 + - **localPath**: 本地路径 + - **isEnabled**: 是否启用同步 + - **syncDirection**: 同步方向 + - **excludePatterns**: 排除模式(可选) + +## AOT兼容性要点 + +### 1. 源生成器配置 + +```csharp +// 必须标记所有需要序列化的类型 +[JsonSerializable(typeof(DeviceConfiguration))] +[JsonSerializable(typeof(SshRemoteDeviceInfo))] +[JsonSerializable(typeof(DirectorySyncingModel))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +``` + +### 2. 序列化调用 + +```csharp +// 正确的AOT兼容调用方式 +var config = JsonSerializer.Deserialize( + jsonContent, + ConfigurationJsonContext.Default.DeviceConfiguration +); + +var jsonContent = JsonSerializer.Serialize( + configuration, + ConfigurationJsonContext.Default.DeviceConfiguration +); +``` + +### 3. 避免的反射调用 + +```csharp +// ❌ 错误:在AOT中不可用 +var config = JsonSerializer.Deserialize(jsonContent); + +// ❌ 错误:使用了反射 +var json = JsonSerializer.Serialize(configuration, new JsonSerializerOptions +{ + WriteIndented = true +}); +``` + +## 错误处理模式 + +### 1. 配置文件损坏处理 + +```csharp +public async Task> FetchRemoteDevicesAsync() +{ + try + { + // 正常加载逻辑 + return deviceConfiguration?.SshDevices?.Cast().ToList() ?? []; + } + catch (JsonException ex) + { + Log.Error($"[Config] JSON配置文件格式错误: {ex.Message}"); + + // 可选:尝试创建备份并重置为默认配置 + await BackupCorruptedConfigAsync(); + return []; + } + catch (Exception ex) + { + Log.Error($"[Config] 加载配置文件失败: {ex.Message}"); + return []; + } +} + +private async Task BackupCorruptedConfigAsync() +{ + try + { + var backupPath = _configurationPath + $".corrupted.{DateTime.Now:yyyyMMddHHmmss}"; + File.Copy(_configurationPath, backupPath); + Log.Info($"[Config] 已备份损坏的配置文件到: {backupPath}"); + } + catch (Exception ex) + { + Log.Warn($"[Config] 备份损坏配置文件失败: {ex.Message}"); + } +} +``` + +### 2. 保存操作原子性 + +```csharp +public async Task SaveRemoteDeviceAsync(IRemoteDeviceInfo deviceInfo) +{ + var tempPath = _configurationPath + ".tmp"; + + try + { + // 先写入临时文件 + var jsonContent = JsonSerializer.Serialize( + configuration, + ConfigurationJsonContext.Default.DeviceConfiguration + ); + + await File.WriteAllTextAsync(tempPath, jsonContent); + + // 原子性替换 + File.Move(tempPath, _configurationPath, overwrite: true); + + Log.Info($"[Config] 配置文件保存成功"); + } + catch (Exception ex) + { + // 清理临时文件 + if (File.Exists(tempPath)) + File.Delete(tempPath); + + Log.Error($"[Config] 保存配置文件失败: {ex.Message}"); + throw; + } +} +``` + +## 性能优化 + +### 1. 缓存序列化选项 + +```csharp +public class JsonRemoteDeviceConfigurationSource +{ + private static readonly JsonTypeInfo ConfigTypeInfo = + ConfigurationJsonContext.Default.DeviceConfiguration; + + public async Task LoadConfigurationAsync() + { + var jsonContent = await File.ReadAllTextAsync(_configurationPath); + return JsonSerializer.Deserialize(jsonContent, ConfigTypeInfo); + } +} +``` + +### 2. 异步流处理大文件 + +```csharp +public async Task LoadConfigurationStreamAsync() +{ + using var fileStream = new FileStream(_configurationPath, FileMode.Open, FileAccess.Read); + return await JsonSerializer.DeserializeAsync(fileStream, ConfigTypeInfo); +} +``` + +## 配置验证 + +### 1. 数据模型验证 + +```csharp +public static class ConfigurationValidator +{ + public static ValidationResult ValidateConfiguration(DeviceConfiguration config) + { + var result = new ValidationResult(); + + if (config?.SshDevices == null) + { + result.AddError("配置文件不能为空"); + return result; + } + + foreach (var device in config.SshDevices) + { + ValidateDevice(device, result); + } + + return result; + } + + private static void ValidateDevice(SshRemoteDeviceInfo device, ValidationResult result) + { + if (string.IsNullOrWhiteSpace(device.LocalId)) + result.AddError($"设备 '{device.ConnectionName}' 缺少本地ID"); + + if (string.IsNullOrWhiteSpace(device.ConnectionName)) + result.AddError("设备缺少连接名称"); + + if (string.IsNullOrWhiteSpace(device.Host)) + result.AddError($"设备 '{device.ConnectionName}' 缺少主机地址"); + + if (device.Port <= 0 || device.Port > 65535) + result.AddError($"设备 '{device.ConnectionName}' 端口号无效: {device.Port}"); + } +} +``` + +## 迁移工具集成 + +### 配置管理器 + +```csharp +public class ConfigurationManager +{ + private readonly List _remoteDeviceSources = + [ + new JsonRemoteDeviceConfigurationSource(), // JSON配置源 + ]; +} +``` + +## 常见问题排查 + +### 1. 序列化错误 +**问题**: `JsonException: The JSON value could not be converted` +**解决**: 检查JSON格式和数据模型属性类型是否匹配 + +### 2. AOT编译错误 +**问题**: 运行时找不到序列化方法 +**解决**: 确保所有类型都在`JsonSerializable`中声明 + +### 3. 配置文件权限错误 +**问题**: `UnauthorizedAccessException` +**解决**: 检查配置目录权限,确保应用有读写权限 + +### 4. 配置文件损坏 +**问题**: JSON格式错误 +**解决**: 使用配置验证工具检查和修复 + +## 最佳实践 + +1. **始终使用源生成器上下文进行序列化** +2. **实现原子性保存操作** +3. **添加配置验证逻辑** +4. **妥善处理错误和异常** +5. **定期备份配置文件** +6. **使用结构化日志记录操作** + +## 与项目集成 + +### 依赖注入配置 +```csharp +// Modules/Configurations/ConfigurationModule.cs +public static class ConfigurationModule +{ + public static IServiceCollection AddConfiguration(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} +``` + +### 日志集成 +```csharp +// 使用 DotNetCampus.Logging 命名空间 +using DotNetCampus.Logging; + +public class JsonRemoteDeviceConfigurationSource +{ + public async Task SaveAsync() + { + try + { + // 保存逻辑 + Log.Info("[Config] JSON配置保存成功"); + } + catch (Exception ex) + { + Log.Error($"[Config] JSON配置保存失败: {ex.Message}"); + } + } +} +``` diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/SSH\350\277\236\346\216\245\347\256\241\347\220\206/01-SSH\345\257\206\351\222\245\350\256\244\350\257\201\351\205\215\347\275\256\346\226\271\346\241\210.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/SSH\350\277\236\346\216\245\347\256\241\347\220\206/01-SSH\345\257\206\351\222\245\350\256\244\350\257\201\351\205\215\347\275\256\346\226\271\346\241\210.md" new file mode 100644 index 0000000..d10f9b9 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/SSH\350\277\236\346\216\245\347\256\241\347\220\206/01-SSH\345\257\206\351\222\245\350\256\244\350\257\201\351\205\215\347\275\256\346\226\271\346\241\210.md" @@ -0,0 +1,217 @@ +# SSH 密钥认证配置指南 + +作为SSH连接专家,本文档详细说明了如何从密码认证迁移到更安全的私钥认证方式。 + +## 概述 + +SSH密钥认证是一种比密码认证更安全、更便捷的认证方式。一旦配置完成,客户端可以自动登录到远程服务器,无需每次输入密码。 + +## 完整配置流程 + +### 1. 生成SSH密钥对 + +在客户端(Windows/Linux)上生成密钥对: + +```bash +# 使用 RSA 算法生成密钥对(推荐 4096 位) +ssh-keygen -t rsa -b 4096 -C "your_email@example.com" + +# 或使用更现代的 Ed25519 算法(推荐) +ssh-keygen -t ed25519 -C "your_email@example.com" +``` + +**密钥生成过程中的选择**: +- 密钥保存路径:默认 `~/.ssh/id_rsa`(或 `id_ed25519`) +- 密码短语(passphrase):可选,增加额外安全性 +- 生成两个文件: + - 私钥:`id_rsa`(保留在客户端,绝不外传) + - 公钥:`id_rsa.pub`(需要复制到服务器) + +### 2. 将公钥复制到远程服务器 + +#### 方法一:使用 ssh-copy-id(推荐) +```bash +ssh-copy-id username@remote_host +``` + +#### 方法二:手动复制(适用于Windows或ssh-copy-id不可用的情况) + +**步骤 2.1:读取公钥内容** +```bash +# Linux/macOS +cat ~/.ssh/id_rsa.pub + +# Windows PowerShell +Get-Content $env:USERPROFILE\.ssh\id_rsa.pub +``` + +**步骤 2.2:通过SSH连接到远程服务器** +```bash +ssh username@remote_host +``` + +**步骤 2.3:在远程服务器上配置公钥** +```bash +# 创建 .ssh 目录(如果不存在) +mkdir -p ~/.ssh + +# 设置正确的权限 +chmod 700 ~/.ssh + +# 将公钥内容追加到 authorized_keys 文件 +echo "your_public_key_content_here" >> ~/.ssh/authorized_keys + +# 设置 authorized_keys 文件权限 +chmod 600 ~/.ssh/authorized_keys +``` + +### 3. 验证密钥认证 + +退出SSH连接,重新连接验证: +```bash +ssh username@remote_host +``` + +如果配置正确,应该无需输入密码即可登录。 + +### 4. 禁用密码认证(可选,增强安全性) + +在远程服务器上编辑SSH配置: +```bash +sudo nano /etc/ssh/sshd_config +``` + +修改以下配置项: +``` +PasswordAuthentication no +ChallengeResponseAuthentication no +UsePAM no +``` + +重启SSH服务: +```bash +sudo systemctl restart sshd +``` + +## SSH.NET 中的密钥认证实现 + +### 基本代码示例 + +```csharp +using Renci.SshNet; + +// 使用私钥文件认证 +var keyFile = new PrivateKeyFile(@"C:\Users\YourUser\.ssh\id_rsa"); +var connectionInfo = new ConnectionInfo("hostname", "username", + new PrivateKeyAuthenticationMethod("username", keyFile)); + +using var client = new SshClient(connectionInfo); +client.Connect(); + +// 执行命令或SFTP操作 +``` + +### 带密码短语的私钥 + +```csharp +// 如果私钥有密码短语保护 +var keyFile = new PrivateKeyFile(@"C:\Users\YourUser\.ssh\id_rsa", "passphrase"); +var connectionInfo = new ConnectionInfo("hostname", "username", + new PrivateKeyAuthenticationMethod("username", keyFile)); +``` + +### 多种认证方式组合 + +```csharp +// 组合多种认证方式,SSH.NET会按顺序尝试 +var connectionInfo = new ConnectionInfo("hostname", "username", + new PrivateKeyAuthenticationMethod("username", keyFile), + new PasswordAuthenticationMethod("username", "password")); +``` + +## 安全最佳实践 + +### 1. 密钥管理 +- **私钥安全**:私钥文件绝不能泄露,建议设置密码短语 +- **定期轮换**:定期更换密钥对,特别是在人员变动时 +- **备份策略**:安全备份私钥,防止丢失 + +### 2. 服务器配置 +- **禁用root登录**:`PermitRootLogin no` +- **限制用户**:`AllowUsers username1 username2` +- **更改默认端口**:避免使用22端口 +- **启用防火墙**:只开放必要的SSH端口 + +### 3. 文件权限 +- 私钥文件:`600` 或 `400` +- 公钥文件:`644` +- `.ssh` 目录:`700` +- `authorized_keys`:`600` + +## 常见问题和解决方案 + +### 1. 权限错误 +**问题**:SSH仍然要求密码 +**解决方案**:检查文件权限 +```bash +# 修复权限 +chmod 700 ~/.ssh +chmod 600 ~/.ssh/authorized_keys +``` + +### 2. 密钥格式不兼容 +**问题**:新版本OpenSSH生成的密钥格式不被老版本支持 +**解决方案**:使用PEM格式 +```bash +ssh-keygen -t rsa -b 4096 -m PEM +``` + +### 3. SSH.NET 无法读取密钥 +**问题**:`PrivateKeyFile` 抛出异常 +**解决方案**: +- 确保密钥格式正确(支持OpenSSH、PEM格式) +- 检查密码短语是否正确 +- 验证文件路径和权限 + +### 4. 服务器日志排查 +查看SSH服务器日志: +```bash +sudo tail -f /var/log/auth.log # Ubuntu/Debian +sudo tail -f /var/log/secure # CentOS/RHEL +``` + +## 项目中的应用建议 + +### 1. 配置存储 +在JSON配置中支持私钥路径: +```json +{ + "sshDevices": [ + { + "auth": { + "type": "key", + "privateKeyPath": "~/.ssh/id_rsa", + "passphrase": "" + } + } + ] +} +``` + +### 2. 认证流程优化 +1. 优先尝试密钥认证 +2. 失败时回退到密码认证 +3. 提供UI引导用户配置密钥 + +### 3. 安全存储 +- 密码短语不应明文存储 +- 考虑使用Windows Credential Manager或类似安全存储 + +## 相关文档 + +- [SSH.NET 使用指南](SSH.NET-使用指南.md) +- [SSH.NET 文件同步指南](SSH.NET-File-Sync-Guide.md) + +--- + +**维护说明**:本文档由SSH连接专家维护,其他AI在实际开发中遇到相关问题时,请及时更新此文档。 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/SSH\350\277\236\346\216\245\347\256\241\347\220\206/02-\345\244\232\350\256\276\345\244\207\350\277\236\346\216\245\345\256\211\345\205\250\345\210\206\346\236\220.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/SSH\350\277\236\346\216\245\347\256\241\347\220\206/02-\345\244\232\350\256\276\345\244\207\350\277\236\346\216\245\345\256\211\345\205\250\345\210\206\346\236\220.md" new file mode 100644 index 0000000..5ae3c2d --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/SSH\350\277\236\346\216\245\347\256\241\347\220\206/02-\345\244\232\350\256\276\345\244\207\350\277\236\346\216\245\345\256\211\345\205\250\345\210\206\346\236\220.md" @@ -0,0 +1,1135 @@ +# SSH 多设备连接安全性分析与最佳实践 + +作为SSH连接专家,本文档详细分析DotNetCampus Terminal多设备连接的安全性考虑,包括密钥管理策略、配置文件调整和一键部署功能的安全性要求。 + +## 1. 多设备密钥管理策略分析 + +### 共用密钥 vs 独立密钥的安全性对比 + +#### 方案一:共用一个私钥(不推荐) +``` +本机 (私钥: id_rsa) +├── 设备A (公钥: authorized_keys) +├── 设备B (公钥: authorized_keys) +├── 设备C (公钥: authorized_keys) +└── 设备D (公钥: authorized_keys) +``` + +**安全风险**: +- **单点失效**:私钥泄露后,所有设备都面临风险 +- **权限管理困难**:无法针对单个设备撤销访问权限 +- **审计追踪不足**:无法区分来自不同设备的连接 +- **密钥轮换复杂**:更换密钥需要更新所有设备 + +#### 方案二:每设备独立密钥(理论最佳,但复杂) +``` +本机 +├── device_a_key (私钥) → 设备A (公钥) +├── device_b_key (私钥) → 设备B (公钥) +├── device_c_key (私钥) → 设备C (公钥) +└── device_d_key (私钥) → 设备D (公钥) +``` + +**安全优势**: +- **隔离性**:单个设备密钥泄露不影响其他设备 +- **精细权限控制**:可以为不同设备配置不同权限 +- **易于管理**:可以独立撤销、轮换特定设备的密钥 +- **审计友好**:通过密钥可以追踪特定设备的操作 + +#### 方案三:全局SSH密钥复用(第一期实际采用方案) +``` +本机全局SSH密钥 +├── ~/.ssh/id_ed25519 (私钥) +├── ~/.ssh/id_ed25519.pub (公钥) +└── 所有设备共用此公钥 + ├── 设备A (authorized_keys) + ├── 设备B (authorized_keys) + ├── 设备C (authorized_keys) + └── 设备D (authorized_keys) +``` + +**实用优势**: +- **通用性**:用户可以使用任何SSH工具无密码连接所有设备 +- **简单性**:只需要管理一个密钥对 +- **兼容性**:与用户现有SSH工作流完全兼容 +- **用户友好**:提升整体SSH使用体验,而不仅仅是本软件 + +### 推荐的密钥管理策略(第一期实现) + +#### 1. 全局SSH密钥检测和使用 +```bash +# 优先检查标准命名的密钥文件 +~/.ssh/id_ed25519 # Ed25519 (优先) +~/.ssh/id_rsa # RSA (备选) +~/.ssh/id_ecdsa # ECDSA (备选) +~/.ssh/id_dsa # DSA (已废弃) + +# 如果标准命名不存在,扫描其他可能的命名方式 +~/.ssh/ssh_host_rsa_key # 某些工具生成的命名 +~/.ssh/github_rsa # GitHub专用密钥 +~/.ssh/default # 默认命名 +~/.ssh/key # 简单命名 +~/.ssh/private_key # 描述性命名 + +# 智能检测策略 +1. 按优先级查找已知命名的密钥 +2. 扫描.ssh目录下所有无扩展名文件 +3. 验证文件头格式(-----BEGIN ... PRIVATE KEY-----) +4. 尝试用SSH.NET加载验证 +``` + +#### 2. 密钥生成策略(如果不存在) +```bash +# 生成全局Ed25519密钥对 +ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "$(whoami)@$(hostname)-$(date +%Y%m%d)" +``` + +#### 3. 一键部署流程设计 +``` +1. 检测本机是否存在全局SSH私钥 + ├── 存在 → 使用现有密钥对 + └── 不存在 → 生成新的全局密钥对 +2. 连接到目标设备(使用密码) +3. 将公钥部署到远程设备的 ~/.ssh/authorized_keys +4. 验证密钥认证是否正常工作 +5. 可选:提示用户是否禁用密码认证(提升安全性) +6. 更新本地配置为密钥认证模式 +``` + +## 2. 配置文件安全性调整方案 + +### 当前配置结构分析 +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f67890", + "connectionName": "麒麟 Kylin (x86_64)", + "host": "172.20.114.71", + "port": 22, + "userName": "seewo", + "password": "123" // 明文密码 - 安全风险 + } + ] +} +``` + +### 支持全局SSH密钥的配置升级方案 + +#### 方案一:简化配置(推荐用于第一期) +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f67890", + "connectionName": "麒麟 Kylin (x86_64)", + "host": "172.20.114.71", + "port": 22, + "userName": "seewo", + "authType": "Key", // "Password" | "Key" | "Auto" + // privateKeyPath 留空表示使用系统默认全局密钥 + "privateKeyPath": "", // 空值 = 自动检测各种可能的密钥文件 + "passphraseRequired": false // 标记全局密钥是否需要密码短语 + } + ] +} +``` + +#### 方案二:显式指定全局密钥路径 +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f67890", + "connectionName": "麒麟 Kylin (x86_64)", + "host": "172.20.114.71", + "port": 22, + "userName": "seewo", + "authType": "Key", + "privateKeyPath": "~/.ssh/id_ed25519" // 显式指定全局密钥 + } + ] +} +``` + +#### 方案三:混合认证(兼容性最好) +```json +{ + "sshDevices": [ + { + "localId": "device_a1b2c3d4e5f67890", + "connectionName": "麒麟 Kylin (x86_64)", + "host": "172.20.114.71", + "port": 22, + "userName": "seewo", + "authType": "Auto", // 自动尝试:密钥优先,失败时使用密码 + "privateKeyPath": "" // 空值 = 使用全局默认密钥 + // password字段在运行时输入,不存储 + } + ] +} +``` + +### 配置文件安全性改进 + +#### 1. 敏感信息处理 +```json +{ + "sshDevices": [ + { + // 移除明文密码 + // "password": "123", // 删除此行 + + // 添加安全存储引用 + "passwordStorageKey": "DotNetCampus.Terminal.Device.{LocalId}.Password", + "passphraseStorageKey": "DotNetCampus.Terminal.Device.{LocalId}.Passphrase" + } + ] +} +``` + +#### 2. 配置文件权限 +- Windows: 限制文件访问权限为当前用户 +- Linux/macOS: `chmod 600 devices.json` + +#### 3. 配置加密(可选) +```json +{ + // 配置文件头部添加加密标识 + "encryption": { + "enabled": true, + "algorithm": "AES-256-GCM" + // 密钥通过用户密码或Windows DPAPI派生 + } +} +``` + +## 3. 一键部署功能安全性考虑(全局密钥方案) + +### 一键部署流程设计 +``` +1. 检测本机全局SSH密钥(~/.ssh/id_ed25519, ~/.ssh/id_rsa 等) + ├── 存在 → 使用现有全局密钥对 + └── 不存在 → 生成新的全局密钥对(id_ed25519) +2. 使用SSH密码连接到目标设备 +3. 将全局公钥部署到远程设备的 ~/.ssh/authorized_keys +4. 验证密钥认证是否正常工作 +5. 可选:提示用户是否禁用设备密码认证 +6. 更新本地配置为密钥认证模式(AuthType = "Key", PrivateKeyPath = "") +``` + +### 全局密钥方案的安全性考虑 + +#### 1. 密钥检测和生成安全性 +```csharp +// 检测现有密钥时的安全验证 +public static bool ValidatePrivateKey(string keyPath) +{ + try + { + // 验证密钥文件格式和权限 + var keyFile = new PrivateKeyFile(keyPath); + + // 检查文件权限(Windows/Linux) + var fileInfo = new FileInfo(keyPath); + // TODO: 添加权限检查逻辑 + + return true; + } + catch (Exception ex) + { + Log.Warning($"[SSH] 密钥验证失败: {keyPath}, 错误: {ex.Message}"); + return false; + } +} +``` + +**风险控制**: +- 验证现有密钥文件的完整性和格式 +- 检查密钥文件权限设置 +- 生成新密钥时使用强密码学算法(Ed25519) + +#### 2. 公钥去重和完整性保护 +```bash +# 部署时的去重逻辑(防止重复添加) +grep -F "${public_key_content}" ~/.ssh/authorized_keys || echo "${public_key_content}" >> ~/.ssh/authorized_keys + +# 或者使用更安全的方式 +sort ~/.ssh/authorized_keys | uniq > ~/.ssh/authorized_keys.tmp && mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys +``` + +**风险控制**: +- 防止重复部署相同公钥 +- 保护existing authorized_keys内容 +- 验证部署后文件完整性 + +#### 2. 传输过程安全性 +```csharp +// 公钥传输使用已建立的SSH连接 +using var sftpClient = new SftpClient(connectionInfo); +sftpClient.Connect(); + +// 验证远程路径安全性 +var remoteSshDir = "~/.ssh"; +if (!sftpClient.Exists(remoteSshDir)) +{ + sftpClient.CreateDirectory(remoteSshDir); + // 设置正确权限 + sftpClient.ChangePermissions(remoteSshDir, 700); +} +``` + +**风险控制**: +- 验证远程目录权限 +- 防止路径遍历攻击 +- 确保authorized_keys文件完整性 + +#### 3. 全局密钥的权限验证安全性 +```csharp +// 验证全局密钥认证 +public async Task ValidateGlobalKeyAuthAsync(SshDeviceConfig device) +{ + var privateKeyPath = GlobalSshKeyManager.FindExistingPrivateKey(); + if (privateKeyPath == null) + { + throw new InvalidOperationException("未找到全局SSH私钥"); + } + + var keyFile = new PrivateKeyFile(privateKeyPath); + var connectionInfo = new ConnectionInfo(device.Host, device.Port, device.UserName, + new PrivateKeyAuthenticationMethod(device.UserName, keyFile)); + + using var testClient = new SshClient(connectionInfo); + try + { + testClient.Connect(); + + // 执行基本命令验证权限 + var result = testClient.RunCommand("whoami"); + var isValid = result.Result.Trim() == device.UserName && result.ExitStatus == 0; + + Log.Info($"[SSH] 全局密钥认证验证: {(isValid ? "成功" : "失败")}"); + return isValid; + } + catch (Exception ex) + { + Log.Error($"[SSH] 全局密钥认证验证失败: {ex.Message}"); + return false; + } + finally + { + testClient.Disconnect(); + } +} +``` + +**风险控制**: +- 部署后立即验证全局密钥认证 +- 验证失败时回滚到密码认证 +- 记录详细的验证日志 + +#### 4. 原密码处理安全性 +```csharp +// 使用SecureString处理密码 +public class SecurePasswordHandler +{ + private readonly SecureString _password; + + public SecurePasswordHandler(string password) + { + _password = new SecureString(); + foreach (char c in password) + { + _password.AppendChar(c); + } + _password.MakeReadOnly(); + } + + public void Clear() + { + _password?.Dispose(); + } +} +``` + +**风险控制**: +- 密码在内存中的存在时间最小化 +- 使用SecureString或类似安全容器 +- 操作完成后立即清理敏感信息 + +#### 5. 配置更新安全性 +```csharp +// 原子性配置更新 +public async Task UpdateConfigurationAsync(DeviceConfig newConfig) +{ + var tempFile = Path.GetTempFileName(); + try + { + // 写入临时文件 + await WriteConfigToFileAsync(tempFile, newConfig); + + // 验证配置完整性 + ValidateConfiguration(tempFile); + + // 原子性替换 + File.Replace(tempFile, _configPath, _configPath + ".backup"); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } +} +``` + +**风险控制**: +- 配置文件原子性更新 +- 更新失败时自动回滚 +- 保留配置备份文件 + +### 一键部署UI安全性设计 + +#### 1. 用户确认机制 +```xml + + + 此操作将: + 1. 生成新的SSH密钥对 + 2. 将公钥部署到远程设备 + 3. 验证密钥认证 + 4. 可选:禁用设备密码认证 + + + 禁用密码认证(提高安全性,但需确保密钥正常工作) + + + 我理解此操作的安全影响并同意继续 + + +``` + +#### 2. 进度反馈和错误处理 +```csharp +public class KeyDeploymentProgress +{ + public string CurrentStep { get; set; } + public int PercentComplete { get; set; } + public bool HasError { get; set; } + public string ErrorMessage { get; set; } + public bool CanRetry { get; set; } + public bool CanRollback { get; set; } +} +``` + +## 4. 全局密钥方案安全性检查清单 + +### 部署前检查 +- [ ] 检测现有全局SSH密钥(~/.ssh/id_ed25519, ~/.ssh/id_rsa等) +- [ ] 验证现有密钥文件完整性和权限 +- [ ] 确认目标设备SSH服务配置正常 +- [ ] 验证用户权限(是否可以修改~/.ssh/authorized_keys) +- [ ] 检查网络连接安全性 + +### 部署过程检查 +- [ ] 全局密钥生成使用强算法(Ed25519优先) +- [ ] 公钥传输过程完整性验证 +- [ ] 远程authorized_keys文件去重处理 +- [ ] 远程文件权限设置正确(700 for .ssh, 600 for authorized_keys) +- [ ] 全局密钥认证验证成功 + +### 部署后检查 +- [ ] 全局密钥可以正常认证所有已配置设备 +- [ ] 原密码信息已安全清理(可选) +- [ ] 配置文件已更新为密钥认证模式 +- [ ] 审计日志已记录部署操作 +- [ ] 用户已收到成功通知和使用指导 + +### 用户体验检查 +- [ ] 用户可以使用任何SSH工具无密码连接 +- [ ] 现有SSH工作流程未受影响 +- [ ] 密钥轮换和管理流程清晰 +- [ ] 提供密钥管理的用户指导文档 + +## 6. 全局密钥方案的优缺点总结 + +### 优点 +- **用户友好**:提升整体SSH使用体验,不仅限于本软件 +- **简单易用**:只需管理一个全局密钥对 +- **通用兼容**:与所有SSH工具和现有工作流兼容 +- **降低门槛**:用户无需理解复杂的密钥管理概念 + +### 缺点和风险 +- **单点风险**:全局密钥泄露影响所有配置的设备 +- **权限粗糙**:无法为不同设备设置不同权限 +- **审计困难**:难以区分来自不同设备管理器的连接 + +### 风险缓解措施 +- **定期轮换**:建议每6-12个月轮换全局密钥 +- **访问监控**:在关键设备上启用SSH访问日志 +- **权限最小化**:确保SSH用户只有必要的权限 +- **备份策略**:安全备份全局私钥,防止丢失 + +## 相关文档 + +- [SSH 密钥认证配置指南](SSH-Key-Based-Authentication-Guide.md) +- [SSH.NET 使用指南](SSH.NET-使用指南.md) + +--- + +**维护说明**:本文档由SSH连接专家维护,涵盖多设备连接的安全性考虑和最佳实践。其他AI在开发相关功能时应严格遵循这些安全准则。 + +## 5. 全局SSH密钥管理实现指南 + +### SSH密钥检测逻辑 +```csharp +public class GlobalSshKeyManager +{ + // 扩展密钥检测列表,覆盖更多常见的命名方式 + private static readonly string[] KeyPriority = { + // 标准命名 + "id_ed25519", // Ed25519 (现代、安全) + "id_rsa", // RSA (广泛兼容) + "id_ecdsa", // ECDSA (椭圆曲线) + "id_dsa", // DSA (已废弃,但可能存在) + + // 其他常见命名方式 + "ssh_host_rsa_key", // 某些工具生成的命名 + "ssh_host_ed25519_key", // 某些工具生成的命名 + "github_rsa", // GitHub特定密钥 + "gitlab_rsa", // GitLab特定密钥 + "default", // 一些SSH客户端的默认命名 + "key", // 简单命名 + "private_key", // 描述性命名 + "ssh_private_key", // 描述性命名 + }; + + public static string? FindExistingPrivateKey() + { + var sshDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh"); + + if (!Directory.Exists(sshDir)) + { + Log.Info("[SSH] .ssh 目录不存在"); + return null; + } + + // 首先按优先级查找已知命名的密钥 + foreach (var keyName in KeyPriority) + { + var keyPath = Path.Combine(sshDir, keyName); + if (File.Exists(keyPath) && IsValidPrivateKey(keyPath)) + { + Log.Info($"[SSH] 发现现有私钥: {keyPath}"); + return keyPath; + } + } + + // 如果没找到,扫描所有文件,查找可能的私钥文件 + var allFiles = Directory.GetFiles(sshDir, "*", SearchOption.TopDirectoryOnly) + .Where(f => !Path.GetFileName(f).EndsWith(".pub")) // 排除公钥文件 + .Where(f => !Path.GetFileName(f).StartsWith("known_hosts")) // 排除known_hosts + .Where(f => !Path.GetFileName(f).StartsWith("config")) // 排除config文件 + .Where(f => !Path.GetFileName(f).Contains(".")) // 排除有扩展名的文件 + .ToArray(); + + foreach (var filePath in allFiles) + { + if (IsValidPrivateKey(filePath)) + { + Log.Info($"[SSH] 发现潜在私钥文件: {filePath}"); + return filePath; + } + } + + Log.Info("[SSH] 未发现任何SSH私钥"); + return null; + } + + private static bool IsValidPrivateKey(string keyPath) + { + try + { + // 检查文件是否存在且可读 + if (!File.Exists(keyPath)) + return false; + + // 检查文件大小(私钥通常不会太小) + var fileInfo = new FileInfo(keyPath); + if (fileInfo.Length < 100) // 私钥至少应该有100字节 + return false; + + // 读取文件开头,检查是否是SSH私钥格式 + var firstLine = File.ReadLines(keyPath).FirstOrDefault()?.Trim(); + if (string.IsNullOrEmpty(firstLine)) + return false; + + // 检查常见的私钥文件头 + var validHeaders = new[] + { + "-----BEGIN OPENSSH PRIVATE KEY-----", // OpenSSH 新格式 + "-----BEGIN RSA PRIVATE KEY-----", // RSA 私钥 + "-----BEGIN EC PRIVATE KEY-----", // ECDSA 私钥 + "-----BEGIN DSA PRIVATE KEY-----", // DSA 私钥 + "-----BEGIN PRIVATE KEY-----", // PKCS#8 格式 + "-----BEGIN ENCRYPTED PRIVATE KEY-----" // 加密的私钥 + }; + + if (validHeaders.Any(header => firstLine.StartsWith(header))) + { + // 进一步验证:尝试用SSH.NET加载 + try + { + var keyFile = new PrivateKeyFile(keyPath); + return true; + } + catch + { + // 如果SSH.NET无法加载,可能需要密码短语,但仍然是有效的私钥文件 + Log.Warning($"[SSH] 私钥文件可能需要密码短语: {keyPath}"); + return true; // 仍然认为是有效的,但需要用户输入密码短语 + } + } + + return false; + } + catch (Exception ex) + { + Log.Warning($"[SSH] 验证私钥文件时出错: {keyPath}, 错误: {ex.Message}"); + return false; + } + } + + public static string GetPublicKeyContent(string privateKeyPath) + { + // 尝试多种公钥文件命名方式 + var possiblePublicKeyPaths = new[] + { + privateKeyPath + ".pub", // 标准命名 + privateKeyPath.Replace("_key", "_key.pub"), // 某些工具的命名习惯 + Path.ChangeExtension(privateKeyPath, ".pub") // 直接替换扩展名 + }; + + foreach (var publicKeyPath in possiblePublicKeyPaths) + { + if (File.Exists(publicKeyPath)) + { + var content = File.ReadAllText(publicKeyPath).Trim(); + if (!string.IsNullOrEmpty(content) && (content.StartsWith("ssh-") || content.StartsWith("ecdsa-") || content.StartsWith("ssh-ed25519"))) + { + Log.Info($"[SSH] 找到对应公钥文件: {publicKeyPath}"); + return content; + } + } + } + + // 如果找不到公钥文件,尝试从私钥生成 + try + { + Log.Info($"[SSH] 未找到公钥文件,尝试从私钥生成: {privateKeyPath}"); + var keyFile = new PrivateKeyFile(privateKeyPath); + // 注意:SSH.NET 可能不直接支持导出公钥内容,这里需要其他方式 + // 可以通过ssh-keygen命令生成 + return GeneratePublicKeyFromPrivateKey(privateKeyPath); + } + catch (Exception ex) + { + throw new FileNotFoundException($"无法找到或生成公钥文件,私钥: {privateKeyPath}, 错误: {ex.Message}"); + } + } + + private static string GeneratePublicKeyFromPrivateKey(string privateKeyPath) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "ssh-keygen", + Arguments = $"-y -f \"{privateKeyPath}\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using var process = Process.Start(startInfo); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + var publicKey = process.StandardOutput.ReadToEnd().Trim(); + if (!string.IsNullOrEmpty(publicKey)) + { + Log.Info($"[SSH] 成功从私钥生成公钥内容"); + return publicKey; + } + } + + var error = process.StandardError.ReadToEnd(); + throw new InvalidOperationException($"ssh-keygen 生成公钥失败: {error}"); + } + catch (Exception ex) + { + throw new InvalidOperationException($"从私钥生成公钥失败: {ex.Message}"); + } + } +} +``` + +### 密钥对生成逻辑 +```csharp +public class SshKeyGenerator +{ + public static async Task GenerateGlobalKeyPairAsync() + { + var sshDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh"); + Directory.CreateDirectory(sshDir); + + var keyPath = Path.Combine(sshDir, "id_ed25519"); + + if (File.Exists(keyPath)) + { + Log.Info($"[SSH] 全局密钥已存在: {keyPath}"); + return keyPath; + } + + var comment = $"{Environment.UserName}@{Environment.MachineName}-{DateTime.Now:yyyyMMdd}"; + + // 使用 ssh-keygen 生成密钥 + var startInfo = new ProcessStartInfo + { + FileName = "ssh-keygen", + Arguments = $"-t ed25519 -f \"{keyPath}\" -C \"{comment}\" -N \"\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using var process = Process.Start(startInfo); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"密钥生成失败: {error}"); + } + + Log.Info($"[SSH] 成功生成全局密钥对: {keyPath}"); + return keyPath; + } +} +``` + +### 一键部署完整实现 +```csharp +public class SshKeyDeploymentService +{ + public async Task DeployKeyToDeviceAsync(SshDeviceConfig device, string password, string? passphrase = null) + { + try + { + // 1. 检测或生成全局密钥 + var privateKeyPath = GlobalSshKeyManager.FindExistingPrivateKey() + ?? await SshKeyGenerator.GenerateGlobalKeyPairAsync(); + + var publicKeyContent = GlobalSshKeyManager.GetPublicKeyContent(privateKeyPath); + + // 2. 使用密码连接到设备 + var passwordAuth = new PasswordAuthenticationMethod(device.UserName, password); + var connectionInfo = new ConnectionInfo(device.Host, device.Port, device.UserName, passwordAuth); + + using var client = new SshClient(connectionInfo); + client.Connect(); + + // 3. 部署公钥到远程设备 + await DeployPublicKeyAsync(client, publicKeyContent); + + // 4. 验证密钥认证 + PrivateKeyFile keyFile; + if (!string.IsNullOrEmpty(passphrase)) + { + keyFile = new PrivateKeyFile(privateKeyPath, passphrase); + } + else + { + try + { + keyFile = new PrivateKeyFile(privateKeyPath); + } + catch (Exception ex) when (ex.Message.Contains("passphrase") || ex.Message.Contains("encrypted")) + { + throw new InvalidOperationException("私钥需要密码短语,请提供passphrase参数"); + } + } + + var keyAuth = new PrivateKeyAuthenticationMethod(device.UserName, keyFile); + var testConnection = new ConnectionInfo(device.Host, device.Port, device.UserName, keyAuth); + + using var testClient = new SshClient(testConnection); + testClient.Connect(); + + var whoami = testClient.RunCommand("whoami"); + if (whoami.Result.Trim() != device.UserName) + { + throw new SecurityException("密钥认证验证失败"); + } + + // 5. 更新配置文件 + device.AuthType = "Key"; + device.PrivateKeyPath = ""; // 空值表示使用全局默认密钥 + device.PassphraseRequired = !string.IsNullOrEmpty(passphrase); // 记录是否需要密码短语 + + Log.Info($"[SSH] 成功为设备 {device.ConnectionName} 部署SSH密钥"); + return true; + } + catch (Exception ex) + { + Log.Error($"[SSH] 密钥部署失败: {ex.Message}"); + return false; + } + } + + private async Task DeployPublicKeyAsync(SshClient client, string publicKeyContent) + { + var commands = new [] + { + "mkdir -p ~/.ssh", + "chmod 700 ~/.ssh", + $"echo '{publicKeyContent}' >> ~/.ssh/authorized_keys", + "chmod 600 ~/.ssh/authorized_keys", + // 去重:移除重复的公钥条目 + "sort ~/.ssh/authorized_keys | uniq > ~/.ssh/authorized_keys.tmp && mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys" + }; + + foreach (var command in commands) + { + var result = client.RunCommand(command); + if (result.ExitStatus != 0) + { + throw new InvalidOperationException($"命令执行失败: {command}, 错误: {result.Error}"); + } + } + } +} +``` + +### 配置文件更新逻辑 +```csharp +public class ConfigurationUpdater +{ + public void UpdateDeviceToKeyAuth(SshDeviceConfig device) + { + // 更新为密钥认证 + device.AuthType = "Key"; + device.PrivateKeyPath = ""; // 空值 = 使用全局默认密钥 + + // 清除密码(出于安全考虑) + device.Password = null; + + Log.Info($"[Config] 设备 {device.ConnectionName} 已更新为密钥认证模式"); + } + + public void UpdateDeviceToAutoAuth(SshDeviceConfig device) + { + // 设置为自动认证(密钥优先,密码备用) + device.AuthType = "Auto"; + device.PrivateKeyPath = ""; // 使用全局默认密钥 + + // 保留密码配置作为备用(可选) + // device.Password 保持不变 + + Log.Info($"[Config] 设备 {device.ConnectionName} 已更新为自动认证模式"); + } +} +``` + +## 7. 实际部署中的常见情况处理 + +### 7.1 密钥文件命名多样性处理 + +实际用户环境中,SSH密钥文件可能有各种命名方式: + +#### Windows 环境常见命名 +``` +%USERPROFILE%\.ssh\ +├── id_rsa # OpenSSH 标准 +├── ssh_host_rsa_key # Windows OpenSSH Server +├── github_rsa # GitHub Desktop 生成 +├── key # 用户自定义 +├── private_key # 描述性命名 +└── default # 某些SSH工具默认命名 +``` + +#### Linux/macOS 环境常见命名 +``` +~/.ssh/ +├── id_ed25519 # 现代标准 +├── id_rsa # 传统标准 +├── id_ecdsa # ECDSA 算法 +├── gitlab_rsa # GitLab 专用 +├── work_key # 工作用途 +└── personal_key # 个人用途 +``` + +### 7.2 密码短语保护密钥处理 + +```csharp +public class PassphraseHandling +{ + public static ConnectionInfo CreateConnectionWithKey(SshDeviceConfig device, string? passphrase = null) + { + var privateKeyPath = GlobalSshKeyManager.FindExistingPrivateKey(); + if (privateKeyPath == null) + { + throw new InvalidOperationException("未找到全局SSH私钥"); + } + + PrivateKeyFile keyFile; + + // 首先尝试无密码短语加载 + try + { + keyFile = new PrivateKeyFile(privateKeyPath); + Log.Info("[SSH] 成功加载无密码短语保护的私钥"); + } + catch (Exception ex) when (IsPassphraseRequired(ex)) + { + if (string.IsNullOrEmpty(passphrase)) + { + throw new InvalidOperationException("私钥需要密码短语,请提供密码短语"); + } + + try + { + keyFile = new PrivateKeyFile(privateKeyPath, passphrase); + Log.Info("[SSH] 成功加载密码短语保护的私钥"); + } + catch (Exception passphraseEx) + { + throw new UnauthorizedAccessException($"密码短语错误或私钥文件损坏: {passphraseEx.Message}"); + } + } + + var authMethod = new PrivateKeyAuthenticationMethod(device.UserName, keyFile); + return new ConnectionInfo(device.Host, device.Port, device.UserName, authMethod); + } + + private static bool IsPassphraseRequired(Exception ex) + { + var message = ex.Message.ToLower(); + return message.Contains("passphrase") || + message.Contains("encrypted") || + message.Contains("password") || + message.Contains("decrypt"); + } +} +``` + +### 7.3 多种密钥格式支持 + +现代SSH环境可能包含不同格式的密钥: + +```csharp +public static class KeyFormatDetection +{ + public static string DetectKeyFormat(string keyPath) + { + try + { + var firstLine = File.ReadLines(keyPath).FirstOrDefault()?.Trim(); + + return firstLine switch + { + var line when line.StartsWith("-----BEGIN OPENSSH PRIVATE KEY-----") => "OpenSSH", + var line when line.StartsWith("-----BEGIN RSA PRIVATE KEY-----") => "RSA (PEM)", + var line when line.StartsWith("-----BEGIN EC PRIVATE KEY-----") => "ECDSA (PEM)", + var line when line.StartsWith("-----BEGIN DSA PRIVATE KEY-----") => "DSA (PEM)", + var line when line.StartsWith("-----BEGIN PRIVATE KEY-----") => "PKCS#8", + var line when line.StartsWith("-----BEGIN ENCRYPTED PRIVATE KEY-----") => "Encrypted PKCS#8", + _ => "Unknown" + }; + } + catch + { + return "Error"; + } + } + + public static bool IsSupportedBySSHNet(string keyPath) + { + try + { + var keyFile = new PrivateKeyFile(keyPath); + return true; + } + catch + { + return false; + } + } +} +``` + +### 7.4 用户友好的错误处理 + +```csharp +public class UserFriendlyKeyDeployment +{ + public async Task DeployWithUserGuidance(SshDeviceConfig device, string password) + { + var result = new KeyDeploymentResult(); + + try + { + // 1. 检测现有密钥 + var privateKeyPath = GlobalSshKeyManager.FindExistingPrivateKey(); + + if (privateKeyPath == null) + { + result.AddStep("未找到现有SSH密钥,将生成新的密钥对..."); + privateKeyPath = await SshKeyGenerator.GenerateGlobalKeyPairAsync(); + result.AddStep($"成功生成新密钥: {Path.GetFileName(privateKeyPath)}"); + } + else + { + var keyFormat = KeyFormatDetection.DetectKeyFormat(privateKeyPath); + result.AddStep($"找到现有密钥: {Path.GetFileName(privateKeyPath)} ({keyFormat})"); + } + + // 2. 检查密钥是否需要密码短语 + bool needsPassphrase = false; + try + { + var testKey = new PrivateKeyFile(privateKeyPath); + } + catch (Exception ex) when (IsPassphraseRequired(ex)) + { + needsPassphrase = true; + result.AddStep("检测到密钥需要密码短语保护"); + } + + // 3. 部署公钥 + result.AddStep("正在连接远程设备..."); + var publicKeyContent = GlobalSshKeyManager.GetPublicKeyContent(privateKeyPath); + + var passwordAuth = new PasswordAuthenticationMethod(device.UserName, password); + var connectionInfo = new ConnectionInfo(device.Host, device.Port, device.UserName, passwordAuth); + + using var client = new SshClient(connectionInfo); + client.Connect(); + result.AddStep("已连接到远程设备"); + + await DeployPublicKeyAsync(client, publicKeyContent); + result.AddStep("公钥已部署到远程设备"); + + // 4. 验证密钥认证 + result.AddStep("正在验证密钥认证..."); + + PrivateKeyFile keyFile; + if (needsPassphrase) + { + // 这里可能需要UI交互获取密码短语 + result.RequiresPassphrase = true; + result.PrivateKeyPath = privateKeyPath; + return result; // 返回给UI处理密码短语输入 + } + else + { + keyFile = new PrivateKeyFile(privateKeyPath); + } + + var keyAuth = new PrivateKeyAuthenticationMethod(device.UserName, keyFile); + var testConnection = new ConnectionInfo(device.Host, device.Port, device.UserName, keyAuth); + + using var testClient = new SshClient(testConnection); + testClient.Connect(); + + var whoami = testClient.RunCommand("whoami"); + if (whoami.Result.Trim() == device.UserName) + { + result.AddStep("密钥认证验证成功!"); + result.Success = true; + + // 更新配置 + device.AuthType = "Key"; + device.PrivateKeyPath = ""; + device.PassphraseRequired = needsPassphrase; + + result.AddStep("配置已更新为密钥认证模式"); + } + else + { + throw new SecurityException("密钥认证验证失败"); + } + + } + catch (Exception ex) + { + result.Success = false; + result.ErrorMessage = ex.Message; + result.AddStep($"部署失败: {ex.Message}"); + } + + return result; + } + + private static bool IsPassphraseRequired(Exception ex) + { + var message = ex.Message.ToLower(); + return message.Contains("passphrase") || + message.Contains("encrypted") || + message.Contains("password"); + } +} + +public class KeyDeploymentResult +{ + public bool Success { get; set; } + public string? ErrorMessage { get; set; } + public List Steps { get; set; } = new(); + public bool RequiresPassphrase { get; set; } + public string? PrivateKeyPath { get; set; } + + public void AddStep(string step) + { + Steps.Add($"[{DateTime.Now:HH:mm:ss}] {step}"); + } +} +``` + +### 7.5 配置文件向后兼容性 + +```json +// 支持旧版本配置格式 +{ + "sshDevices": [ + { + "localId": "device_old_format", + "connectionName": "旧格式设备", + "host": "192.168.1.100", + "port": 22, + "userName": "user", + "password": "123" // 旧格式:明文密码 + } + ] +} + +// 自动升级为新格式后 +{ + "sshDevices": [ + { + "localId": "device_old_format", + "connectionName": "旧格式设备", + "host": "192.168.1.100", + "port": 22, + "userName": "user", + "authType": "Auto", // 新格式:自动认证 + "privateKeyPath": "", // 使用全局密钥 + "passphraseRequired": false + // "password": "123" // 移除明文密码,改为安全存储 + } + ] +} +``` diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/01-\350\277\234\347\250\213\345\210\260\346\234\254\345\234\260\345\220\214\346\255\245\346\236\266\346\236\204.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/01-\350\277\234\347\250\213\345\210\260\346\234\254\345\234\260\345\220\214\346\255\245\346\236\266\346\236\204.md" new file mode 100644 index 0000000..2fd87a5 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/01-\350\277\234\347\250\213\345\210\260\346\234\254\345\234\260\345\220\214\346\255\245\346\236\266\346\236\204.md" @@ -0,0 +1,121 @@ +# 远程到本地文件同步实现 + +## 概述 + +基于 SSH.NET 的双向文件同步功能,支持本地到远程和远程到本地两种同步方向。 + +## 技术实现 + +### 同步方向枚举 +```csharp +public enum SyncDirection +{ + LocalToRemote, // 本地到远程 + RemoteToLocal // 远程到本地 +} +``` + +### 核心方法结构 + +`FileSyncService` 现在支持根据 `SyncDirection` 选择不同的同步策略: + +1. **SyncDirectoryInternalAsync**: 根据同步方向分发到具体实现 +2. **SyncLocalToRemoteAsync**: 本地到远程同步(原有功能) +3. **SyncRemoteToLocalAsync**: 远程到本地同步(新增功能) + +### 远程到本地同步特点 + +#### 1. 目录处理 +- 自动创建本地目录结构 +- 递归下载远程文件夹中的所有文件 +- 保持远程目录结构 + +#### 2. 文件下载流程 +```csharp +// 1. 获取远程文件列表 +var remoteFiles = GetRemoteFiles(client, directorySyncing.RemotePath); + +// 2. 递归遍历远程目录 +GetRemoteFilesRecursive(client, currentPath, files); + +// 3. 下载文件并报告进度 +client.DownloadFile(remoteFile, fileStream, progress => { ... }); +``` + +#### 3. 进度报告 +- 文件级别进度:基于单个文件下载的字节数 +- 总体进度:基于已处理文件数/总文件数 +- 实时更新 UI 进度条 + +## 关键技术点 + +### 1. 远程文件遍历 +```csharp +private void GetRemoteFilesRecursive(SftpClient client, string currentPath, List files) +{ + var entries = client.ListDirectory(currentPath); + foreach (var entry in entries) + { + if (entry.IsDirectory) + GetRemoteFilesRecursive(client, fullPath, files); + else if (entry.IsRegularFile) + files.Add(fullPath); + } +} +``` + +### 2. 路径处理 +- 远程路径:使用 Unix 风格路径分隔符 `/` +- 本地路径:使用 Windows 风格路径分隔符 `\` +- 路径转换:`relativePath.Replace('/', '\\')` + +### 3. 错误处理 +- 远程目录不存在检查 +- 网络连接异常处理 +- 文件权限错误处理 +- 磁盘空间不足处理 + +## 配置示例 + +```csharp +var directorySyncing = new DirectorySyncingModel +{ + Name = "配置下载", + RemotePath = "/home/user/configs", + LocalPath = @"D:\Downloaded\Configs", + Direction = SyncDirection.RemoteToLocal, + Enabled = true +}; +``` + +## 日志输出 + +``` +[FileSync] 开始同步目录 配置下载: /home/user/configs -> D:\Downloaded\Configs +[FileSync] 正在连接到 192.168.1.100:22 +[FileSync] 找到 15 个文件需要下载 +[FileSync] 正在下载文件: /home/user/configs/app.conf -> D:\Downloaded\Configs\app.conf +[FileSync] 远程到本地同步完成: 配置下载 +``` + +## 注意事项 + +1. **文件覆盖**:下载时会覆盖现有本地文件 +2. **目录权限**:确保本地目录有写入权限 +3. **网络稳定性**:大文件下载时注意网络连接稳定性 +4. **字符编码**:文件名包含特殊字符时的处理 + +## 测试建议 + +1. 测试空目录同步 +2. 测试深层嵌套目录结构 +3. 测试大文件下载 +4. 测试网络中断恢复 +5. 测试中文文件名处理 + +## 未来优化方向 + +1. **增量同步**:只下载修改过的文件 +2. **断点续传**:支持大文件的断点续传 +3. **并发下载**:多文件并发下载提升速度 +4. **文件校验**:下载后的文件完整性校验 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/02-\345\242\236\351\207\217\345\220\214\346\255\245\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/02-\345\242\236\351\207\217\345\220\214\346\255\245\346\200\247\350\203\275\344\274\230\345\214\226.md" new file mode 100644 index 0000000..b830fa4 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/02-\345\242\236\351\207\217\345\220\214\346\255\245\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -0,0 +1,146 @@ +# 增量同步优化实现指南 + +本文档记录了DotNetCampus Terminal项目中文件同步服务的增量同步优化实现。 + +## 优化概述 + +### 背景问题 +原始的文件同步实现存在以下性能问题: +1. **全量同步**:每次同步都会传输所有文件,无论文件是否发生变化 +2. **效率低下**:对于大文件夹和大文件,同步时间过长 +3. **资源浪费**:重复传输未变更的文件,浪费网络带宽和时间 + +### 优化目标 +实现增量同步,只传输有变化的文件: +- 比较文件修改时间和大小 +- 跳过未变更的文件 +- 显著提升同步性能 + +## 架构设计 + +### 代码重构 +将原来的单一文件 `FileSyncService.cs` 重构为多个专职类: + +``` +FileSync/ +├── Models/ +│ └── FileChangeInfo.cs # 文件变更信息模型 +├── Core/ +│ ├── LocalFileInfoProvider.cs # 本地文件信息获取 +│ ├── RemoteFileInfoProvider.cs # 远程文件信息获取 +│ ├── IncrementalSyncComparator.cs # 增量同步比较器 +│ └── SftpOperationHelper.cs # SFTP操作工具 +├── Operations/ +│ ├── LocalToRemoteSyncOperation.cs # 本地到远程同步 +│ └── RemoteToLocalSyncOperation.cs # 远程到本地同步 +└── FileSyncService.cs # 主要服务类(精简版) +``` + +### 核心组件 + +#### 1. FileChangeInfo 模型 +```csharp +public record FileChangeInfo +{ + public required string FilePath { get; init; } + public long Size { get; init; } + public DateTimeOffset LastWriteTime { get; init; } +} +``` + +#### 2. 增量比较逻辑 +- 比较文件大小(Size) +- 比较最后修改时间(LastWriteTime) +- 如果任一属性不同,则标记为需要同步 + +## 增量同步流程 + +### 本地到远程同步 +1. 获取本地文件信息列表 +2. 获取远程文件信息列表 +3. 逐文件比较,识别需要同步的文件: + - 远程不存在的文件(新文件) + - 本地文件比远程文件更新的文件(修改文件) +4. 只传输需要同步的文件 + +### 远程到本地同步 +1. 获取远程文件信息列表 +2. 获取本地文件信息列表 +3. 逐文件比较,识别需要同步的文件: + - 本地不存在的文件(新文件) + - 远程文件比本地文件更新的文件(修改文件) +4. 只下载需要同步的文件 + +## 性能提升效果 + +### 预期收益 +- **首次同步**:与全量同步相同 +- **后续同步**:仅同步变更文件,性能提升显著 +- **大型项目**:性能提升更明显,可减少90%以上的传输时间 + +### 日志输出 +增量同步会在日志中明确显示: +``` +[FileSync] 开始增量同步目录 项目A: /local/path -> /remote/path +[FileSync] 找到 3 个文件需要上传(增量同步) +[FileSync] 新文件需要上传: src/newfile.cs +[FileSync] 文件已修改需要上传: src/modified.cs (本地: 2025-07-09 10:30:00, 远程: 2025-07-09 09:15:00) +``` + +## 技术特点 + +### 1. 职责分离 +- **LocalFileInfoProvider**:专门处理本地文件信息获取 +- **RemoteFileInfoProvider**:专门处理远程文件信息获取 +- **IncrementalSyncComparator**:专门处理增量比较逻辑 +- **SyncOperations**:专门处理同步传输操作 + +### 2. 兼容性保持 +- 保持原有接口不变 +- 向后兼容现有调用代码 +- 平滑升级体验 + +### 3. 错误处理 +- 文件信息获取失败时的降级处理 +- 网络传输中断的恢复机制 +- 详细的错误日志记录 + +## 使用方法 + +增量同步对用户完全透明,无需修改任何配置或调用代码: + +```csharp +// 调用方式保持不变 +var result = await fileSyncService.SyncDirectoryAsync( + sshInfo, directorySyncing, progressCallback, cancellationToken); +``` + +系统会自动: +1. 分析文件变更情况 +2. 只传输需要同步的文件 +3. 在日志中显示增量同步信息 + +## 未来扩展 + +### 可能的优化方向 +1. **文件哈希校验**:基于内容哈希进一步提升准确性 +2. **并行传输**:支持多文件并行传输 +3. **断点续传**:支持大文件的断点续传 +4. **压缩传输**:传输前压缩文件减少网络开销 + +### 配置选项 +可考虑添加配置选项: +- 是否启用增量同步 +- 文件大小阈值设置 +- 时间精度设置 + +## 技术总结 + +本次增量同步优化实现了: +- ✅ 显著提升同步性能 +- ✅ 保持接口兼容性 +- ✅ 代码结构清晰化 +- ✅ 详细的日志记录 +- ✅ 完善的错误处理 + +为DotNetCampus Terminal的文件同步功能提供了更好的用户体验和性能表现。 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/03-\345\220\214\346\255\245\351\224\231\350\257\257\345\244\204\347\220\206\346\234\272\345\210\266.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/03-\345\220\214\346\255\245\351\224\231\350\257\257\345\244\204\347\220\206\346\234\272\345\210\266.md" new file mode 100644 index 0000000..44c7bbc --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\346\226\207\344\273\266\345\220\214\346\255\245/03-\345\220\214\346\255\245\351\224\231\350\257\257\345\244\204\347\220\206\346\234\272\345\210\266.md" @@ -0,0 +1,198 @@ +# 文件同步错误处理优化方案 + +## 问题分析 + +### 原有问题 +1. **诊断信息不足**:`SshRemoteDeviceInfoViewModel` 的 `LastSyncErrorMessage` 只显示简单的错误信息,难以诊断具体问题 +2. **异常捕获过早**:在 `FileSyncService` 和各个 Operation 类中,异常被捕获后只是简单地返回 `FileSyncResult.Failed`,丢失了具体的错误信息 +3. **错误传递链断裂**:详细的错误信息都在日志中,但 UI 层无法获取 + +### 根本原因 +- 使用简单的枚举类型 `FileSyncResult` 无法携带详细错误信息 +- 缺乏标准化的错误分类和诊断信息传递机制 + +## 解决方案:Result 模式 + +### 1. 核心设计 + +#### SyncError 错误信息类 +```csharp +public record SyncError( + string Message, + SyncErrorType ErrorType = SyncErrorType.Unknown, + string? Context = null) +{ + public string? InnerException { get; init; } + public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.Now; + + // 提供用户友好的错误描述 + public string GetUserFriendlyMessage() { ... } + + // 提供详细的诊断信息 + public string GetDiagnosticInfo() { ... } +} +``` + +#### SyncErrorType 错误类型分类 +- `NetworkError`: 网络连接问题 +- `AuthenticationError`: 身份验证失败 +- `FileSystemError`: 文件系统错误(权限、磁盘空间) +- `RemotePathNotFound`: 远程路径不存在 +- `LocalPathError`: 本地路径错误 +- `ConfigurationError`: 配置错误 +- `TransferError`: 文件传输错误 +- `Cancelled`: 操作被取消 + +#### SyncResult 结果包装器 +```csharp +public class SyncResult +{ + public bool IsSuccess { get; } + public T? Value { get; } // 成功时的数据 + public SyncError? Error { get; } // 失败时的错误信息 + + public static SyncResult Success(T value); + public static SyncResult Failure(SyncError error); + public static SyncResult Failure(Exception exception, string operation, string? context); +} +``` + +#### MultiSyncResult 多组同步结果 +```csharp +public class MultiSyncResult +{ + public List GroupResults { get; init; } + public FileSyncResult OverallResult { get; init; } + + public string GetErrorSummary(); // 用户友好的错误摘要 + public string GetDetailedDiagnostics(); // 详细诊断信息 +} +``` + +### 2. 接口升级 + +#### 新增带详细错误信息的方法 +```csharp +public interface IFileSyncService +{ + // 新方法 - 返回详细错误信息 + Task> SyncDirectoryWithDetailsAsync(...); + Task SyncMultipleDirectoriesWithDetailsAsync(...); + + // 旧方法 - 标记为过时,保持兼容性 + [Obsolete("使用 SyncDirectoryWithDetailsAsync 替代")] + Task SyncDirectoryAsync(...); +} +``` + +### 3. 实现层改进 + +#### 同步操作类更新 +- `RemoteToLocalSyncOperation.ExecuteWithDetailsAsync()` +- `LocalToRemoteSyncOperation.ExecuteWithDetailsAsync()` + +#### 错误处理策略 +1. **精确分类**:根据异常类型自动分类错误 +2. **上下文信息**:记录操作名称、文件路径等 +3. **异常链保留**:保留内部异常信息 +4. **时间戳记录**:记录错误发生时间 + +### 4. UI层改进 + +#### ViewModel 新增属性 +```csharp +public class SshRemoteDeviceInfoViewModel +{ + // 用户友好的简短错误描述 + public string LastSyncErrorMessage { get; } + + // 详细的诊断信息(用于故障排除) + public string DetailedDiagnostics { get; } + + // 显示诊断信息的命令 + public ActionCommand ShowDiagnosticsCommand { get; } +} +``` + +#### 错误信息展示分层 +1. **第一层**:简短的用户友好描述 (`LastSyncErrorMessage`) +2. **第二层**:详细的技术诊断信息 (`DetailedDiagnostics`) +3. **第三层**:完整的日志信息(开发者调试用) + +## 使用示例 + +### 1. 服务层调用 +```csharp +var result = await _fileSyncService.SyncMultipleDirectoriesWithDetailsAsync( + sshInfo, syncConfigs, progress, cancellationToken); + +if (result.IsSuccess) +{ + // 处理成功情况 +} +else +{ + // 显示用户友好的错误摘要 + LastSyncErrorMessage = result.GetErrorSummary(); + + // 记录详细诊断信息供技术人员使用 + DetailedDiagnostics = result.GetDetailedDiagnostics(); +} +``` + +### 2. 操作层错误处理 +```csharp +try +{ + // 执行文件同步操作 + return SyncResult.Success(processedFiles); +} +catch (UnauthorizedAccessException ex) +{ + return SyncResult.Failure(ex, "下载文件", remoteFile); +} +catch (DirectoryNotFoundException ex) +{ + return SyncResult.Failure(ex, "创建本地目录", directorySyncing.LocalPath); +} +``` + +## 优势 + +### 1. 诊断能力提升 +- **精确定位**:明确的错误类型和上下文信息 +- **分层展示**:用户友好 + 技术详细信息 +- **历史追踪**:错误时间戳和操作历史 + +### 2. 开发体验改善 +- **类型安全**:编译时检查错误处理 +- **一致性**:标准化的错误处理模式 +- **可测试性**:易于编写单元测试 + +### 3. 用户体验优化 +- **清晰反馈**:明确的错误原因和解决建议 +- **渐进展示**:根据用户需求展示不同级别的信息 +- **快速诊断**:技术人员可快速定位问题 + +## 兼容性 + +- 保留了原有的 `FileSyncResult` 枚举和旧方法 +- 新方法标记旧方法为 `[Obsolete]` 但不会破坏现有代码 +- 支持渐进式迁移,可以逐步替换调用点 + +## 扩展性 + +- `SyncErrorType` 枚举可以轻松添加新的错误类型 +- `SyncError` 可以扩展更多诊断信息字段 +- `MultiSyncResult` 可以添加更多聚合分析功能 + +## 总结 + +这个改进方案通过引入 `Result` 模式,从根本上解决了文件同步错误处理的问题: + +1. **完整的错误信息传递链**:从底层操作到UI层的完整错误信息传递 +2. **标准化的错误分类**:便于用户理解和开发者诊断 +3. **分层的信息展示**:满足不同用户的信息需求 +4. **良好的扩展性**:易于添加新的错误类型和诊断信息 + +这个方案能够显著提升同步失败时的诊断能力,帮助用户快速找到问题根源并解决。 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/01-Terminal\347\225\214\351\235\242\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/01-Terminal\347\225\214\351\235\242\345\274\200\345\217\221\346\214\207\345\215\227.md" new file mode 100644 index 0000000..56bf8a6 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/01-Terminal\347\225\214\351\235\242\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -0,0 +1,427 @@ +# DotNetCampus.Terminal UI 开发指南 + +## 项目UI现状分析 + +### 已实现的UI组件 + +#### 1. 应用程序框架 +- ✅ **App.axaml**: 应用程序配置,使用 TurboVisionDarkTheme +- ✅ **MainWindow.axaml**: 主窗口,包含缩放控制(保留用于可能的GUI迁移) +- ✅ **MainView.axaml**: 主视图,包含完整的布局结构 + +#### 2. 核心视图组件 +- ✅ **设备管理界面**: 左侧设备树,右侧详细信息 +- ✅ **TabControl**: 设备和外壳两个标签页 +- ✅ **TreeView**: 分层显示设备组和设备信息 +- ✅ **StatusBar**: 底部功能键栏(F1-F10) + +#### 3. 专用视图 +- ✅ **CreateNewRemoteDeviceView**: 新设备创建界面 +- ✅ **SshRemoteDeviceInfoView**: SSH设备详细信息 +- ✅ **RemoteDeviceGroupView**: 设备组视图 + +#### 4. 辅助组件 +- ✅ **数据转换器**: 连接状态到颜色/字符串转换 +- ✅ **样式系统**: 完整的控件样式定义 +- ✅ **数据绑定**: MVVM模式实现 + +### 当前UI架构 + +``` +MainWindow (缩放控制) +└── MainView (主要内容) + ├── TabControl + │ ├── 设备 Tab + │ │ ├── 搜索框 (已定义但未实现) + │ │ ├── 设备树 (TreeView) + │ │ │ ├── 创建新设备节点 + │ │ │ ├── 收藏设备组 + │ │ │ └── 设备组 + 设备列表 + │ │ └── 详细信息面板 (ContentControl) + │ └── 外壳 Tab (空) + └── 状态栏 (功能键) +``` + +## 待完善的UI功能 + +### 1. 搜索功能 +```xml + + + + +``` + +### 2. 外壳Tab内容 +```xml + + + + + +``` + +### 3. 功能键响应 +```xml + + + + +``` + +### 4. 收藏功能 +```csharp +// 当前状态:代码被注释 +private void FavoriteButton_IsCheckedChanged(object? sender, RoutedEventArgs e) +{ + // 注释的代码... +} + +// 需要实现:完整的收藏逻辑 +``` + +## UI开发规范和约定 + +### 1. 文件命名约定 +- **视图文件**: `{功能名}View.axaml` +- **代码隐藏**: `{功能名}View.axaml.cs` +- **视图模型**: `{功能名}ViewModel.cs` +- **树节点模型**: `{功能名}Node.cs`(用于TreeView的ViewModel) + +### 2. 命名空间约定 +```xml +xmlns="https://github.com/avaloniaui" +xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" +xmlns:console="https://github.com/jinek/consolonia" +xmlns:vm="using:DotNetCampus.Terminal.ViewModels" +xmlns:views="using:DotNetCampus.Terminal.Views" +``` + +### 3. 数据绑定约定 +```xml + + + + + + + + +``` + +### 4. 样式定义约定 +```xml + + + + + +``` + +## 关键UI设计原则 + +### 1. 控制台美学 +- **颜色方案**: 深色背景,浅色文字 +- **边框样式**: 使用 `console:LineBrush` 创建控制台风格线条 +- **按钮样式**: 禁用阴影 (`console:ButtonExtensions.Shadow="False"`) +- **间距控制**: 使用 `Padding="1 0"` 等字符级间距(TUI程序中1表示1个字符宽度) + +### 2. 信息密度 +- **状态指示**: 使用单字符和颜色表示状态 +- **分层显示**: 通过缩进和边距体现层次 +- **紧凑布局**: 最大化信息显示效率 + +### 3. 交互反馈 +- **状态变化**: 连接状态的实时更新 +- **选择反馈**: 选中项的视觉变化 +- **悬停效果**: 鼠标悬停时的样式变化 + +### 4. 可访问性 +- **键盘导航**: 支持Tab键导航 +- **功能键**: 提供快捷键操作 +- **状态提示**: 清晰的状态指示 + +## 开发工作流 + +### 1. 新功能开发步骤 + +#### 步骤1: 设计数据模型 +```csharp +// 1. 定义接口 +public interface INewFeatureNode : IRemoteDeviceNode +{ + // 新功能特有属性 +} + +// 2. 实现具体类(注意:这是TreeView的ViewModel,不是数据模型) +public record NewFeatureNode : BindableRecord, INewFeatureNode +{ + // 实现细节 +} +``` + +#### 步骤2: 创建视图模型 +```csharp +public class NewFeatureViewModel : BindableRecord +{ + private readonly IServiceProvider _serviceProvider; + + public NewFeatureViewModel(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + // 属性和命令 +} +``` + +#### 步骤3: 设计XAML视图 +```xml + + + + + + + + + + +``` + +#### 步骤4: 实现代码隐藏 +```csharp +public partial class NewFeatureView : UserControl +{ + public NewFeatureView() + { + InitializeComponent(); + } + + private NewFeatureViewModel ViewModel => (NewFeatureViewModel)DataContext!; +} +``` + +#### 步骤5: 集成到主界面 +```xml + + + + +``` + +### 2. 样式开发步骤 + +#### 步骤1: 定义基础样式 +```xml + +``` + +#### 步骤2: 添加状态样式 +```xml + +``` + +#### 步骤3: 定义交互样式 +```xml + +``` + +### 3. 调试和测试 + +#### 使用设计时数据 +```xml + + + +``` + +#### 运行时调试 +```csharp +// 在代码隐藏中添加调试代码 +private void OnLoaded(object? sender, RoutedEventArgs e) +{ + System.Diagnostics.Debug.WriteLine($"View loaded: {DataContext?.GetType().Name}"); +} +``` + +## 现有组件使用指南 + +### 1. 使用 TreeView 显示分层数据 +```xml + + + + + + + + + + +``` + +### 2. 使用 ContentControl 实现视图切换 +```xml + + + + + + + + + + +``` + +### 3. 使用转换器进行数据转换 +```xml + + + + + +``` + +## 性能优化建议 + +### 1. 虚拟化大数据集 +```xml + + + + + +``` + +### 2. 优化绑定性能 +```xml + + + + + +``` + +### 3. 异步操作 +```csharp +// 避免阻塞UI线程 +private async Task UpdateDataAsync() +{ + var data = await GetDataAsync(); + await Dispatcher.UIThread.InvokeAsync(() => + { + // 更新UI + }); +} +``` + +## 常见问题解决 + +### 1. 数据绑定不工作 +- 检查 DataContext 是否正确设置 +- 验证属性名称是否正确 +- 确认实现了 INotifyPropertyChanged + +### 2. 样式不生效 +- 检查选择器语法是否正确 +- 确认样式定义的位置和优先级 +- 验证目标控件是否匹配选择器 + +### 3. 布局问题 +- 检查 Grid 的行列定义 +- 确认控件的 Grid.Row 和 Grid.Column 设置 +- 验证 Margin 和 Padding 设置 + +### 4. 性能问题 +- 使用虚拟化面板 +- 减少不必要的数据绑定 +- 避免频繁的UI更新 + +## 待开发功能建议 + +### 1. 搜索过滤功能 +```csharp +public class SearchableTreeViewModel : BindableRecord +{ + private string _searchText = string.Empty; + + public string SearchText + { + get => _searchText; + set + { + if (SetField(ref _searchText, value)) + { + FilterItems(); + } + } + } + + private void FilterItems() + { + // 实现过滤逻辑 + } +} +``` + +### 2. 外壳Tab功能 +```xml + + + + + + +``` + +### 3. 功能键响应 +```csharp +private void MainView_KeyDown(object? sender, KeyEventArgs e) +{ + switch (e.Key) + { + case Key.F1: + ShowHelp(); + break; + case Key.F2: + ConnectToDevice(); + break; + // 其他功能键 + } +} +``` + +### 4. 上下文菜单 +```xml + + + + + + + +``` + +这个开发指南为UI界面设计师提供了完整的项目背景和开发指导,包括现有代码的理解、开发规范、工作流程和常见问题的解决方案。 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/02-SSH\350\256\276\345\244\207\344\277\241\346\201\257\350\247\206\345\233\276\350\256\276\350\256\241.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/02-SSH\350\256\276\345\244\207\344\277\241\346\201\257\350\247\206\345\233\276\350\256\276\350\256\241.md" new file mode 100644 index 0000000..e6ad755 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/02-SSH\350\256\276\345\244\207\344\277\241\346\201\257\350\247\206\345\233\276\350\256\276\350\256\241.md" @@ -0,0 +1,175 @@ +# SshRemoteDeviceInfoView UI 设计文档 + +## 设计概述 +SSH 远程设备详细信息编辑界面,包含设备连接信息编辑和目录同步配置。 + +## 整体布局 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 设备基本信息编辑区 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 连接名称 │ │ 主机地址 │ │ 端口 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 用户名 │ │ 密码 │ │ 连接状态 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 【连接】 │ │ 【保存】 │ │ 【重置】 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ 目录同步配置区 │ +│ ┌─── 目录同步列表 ──────────────────────────────────────────┐│ +│ │ [✓] MyVeryLongProjectName [编辑] [✗] ││ +│ │ 📁 /home/user/projects/myproject/... ││ +│ │ ↑ ││ +│ │ 📁 D:\Projects\MyVeryLongProjectName\... ││ +│ │ ││ +│ │ [⚠] VeryLongDepartment (同步出错) [编辑] [✗] ││ +│ │ 📁 /home/user/documents/work/... ││ +│ │ ↑ ││ +│ │ 📁 D:\Documents\Work\VeryLongDepartment\... ││ +│ └─────────────────────────────────────────────────────────┘│ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 【添加同步】│ │ 【全部启用】│ │ 【全部禁用】│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 核心UI设计要点 + +### 1. 设备信息编辑区 +- **3×3网格布局**:连接名称、主机地址、端口 / 用户名、密码、连接状态 / 连接、保存、重置 +- **状态指示**:○未连接 ◐连接中 ●已连接 ✗失败 +- **实时验证**:输入字段支持实时验证和错误提示 + +### 2. 目录同步配置区 +- **垂直列表**:每个目录同步占用4行空间 +- **同步方向**:使用 ↑ 箭头表示"上传"(本地→远程) +- **状态指示**:[✓]正常 [⚠]出错 [✗]禁用 [◐]同步中 + +### 3. 目录同步命名 +- **自动命名**:新建时使用本地路径最深目录名 +- **可编辑**:双击名称进入编辑模式 +- **冲突处理**:自动添加数字后缀避免重名 + +### 4. 长路径处理 +- **智能省略**:保留开头和结尾,中间用...省略 +- **悬停显示**:ToolTip显示完整路径 +- **自适应**:根据控制台宽度调整省略策略 + +### 5. 关键交互 +- **F2**:重命名目录同步 +- **Ctrl+S**:保存配置 +- **Tab**:字段间导航 +- **上下键**:目录同步列表导航 + +## 技术实现要点 + +### 1. 核心控件 +- **Grid**:主要布局容器 +- **TextBox**:输入字段(支持TwoWay绑定) +- **ListBox**:目录同步列表(自定义ItemTemplate) +- **Border**:分区边框(使用console:LineBrush) + +### 2. 样式配置 +- **主题**:TurboVisionDarkTheme +- **按钮**:禁用阴影效果 +- **边框**:使用console:LineBrush绘制 + +### 3. 数据绑定 +- **实时编辑**:TextBox使用TwoWay绑定 +- **状态转换**:ConnectionState到颜色/字符串的转换器 +- **路径省略**:PathTruncateConverter自动处理长路径 + +## 实现进展记录 + +### 第一阶段:设备基本信息编辑区 ✅ +**时间**: 2025-07-08 + +#### 完成内容 +1. **连接名称**: 标题样式显示,加粗居中,单独占一行 +2. **连接状态**: 美化显示,使用状态符号(○●✗)和颜色指示 +3. **信息分组**: 主机地址/端口、用户名/密码分组显示,用边框包裹 +4. **操作按钮**: 连接、保存、重置按钮布局 + +#### 技术实现 +- 使用 `UniformGrid` 实现响应式布局 +- 使用 `console:LineBrush` 绘制边框 +- 配置连接状态转换器(符号、颜色、文本) +- 实现数据绑定和双向绑定 + +### 第二阶段:目录同步配置区 ✅ +**时间**: 2025-07-08 + +#### 完成内容 +1. **分隔线**: 使用 `console:LineBrush` 清晰分隔功能区域 +2. **目录同步列表**: 垂直列表显示,支持滚动 +3. **目录同步项目**: 包含状态符号、名称、远程路径、本地路径、操作按钮 +4. **底部操作**: 添加同步、全部启用、全部禁用按钮 + +#### 技术实现 +- 创建 `DirectorySyncingViewModel` 数据模型 +- 实现 `DirectorySyncingStatus` 枚举和状态管理 +- 使用 `ListBox` 和 `DataTemplate` 实现列表显示 +- 配置 `x:DataType` 实现强类型绑定 + +### 第三阶段:数据模型和状态系统 ✅ +**时间**: 2025-07-08 + +#### 完成内容 +1. **DirectorySyncingViewModel**: 目录同步视图模型,包含状态管理 +2. **DirectorySyncingStatus**: 目录同步状态枚举(Normal/Error/Disabled/Syncing) +3. **状态指示**: 符号和颜色的自动计算属性 +4. **设计时数据**: 添加示例数据用于界面预览 + +#### 技术实现 +- 使用 `BindableRecord` 作为 ViewModel 基类 +- 实现属性变更通知机制 +- 使用 `AvaloniaList` 替代 `ObservableCollection` +- 配置状态转换器和计算属性 + +### 当前界面布局结构 +``` +┌─────────────────────────────────────────┐ +│ [连接名称标题] │ +├─────────────────────────────────────────┤ +│ ● 在线 [连接] │ +├─────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 主机地址 │ │ 用户名 │ │ +│ │ [________] │ │ [________] │ │ +│ │ 端口 │ │ 密码 │ │ +│ │ [____] │ │ [________] │ │ +│ └─────────────────┘ └─────────────────┘ │ +├─────────────────────────────────────────┤ +│ [保存] [重置] │ +├─────────────────────────────────────────┤ +│ 目录同步配置 │ +│ ┌─────────────────────────────────────┐ │ +│ │ ✓ MyVeryLongProjectName [编辑][✗] │ │ +│ │ 📁 /home/user/projects/... │ │ +│ │ ↑ │ │ +│ │ 📁 D:\Projects\... │ │ +│ ├─────────────────────────────────────┤ │ +│ │ ⚠ VeryLongDepartment (同步出错) │ │ +│ │ 📁 /home/user/documents/... │ │ +│ │ ↑ │ │ +│ │ 📁 D:\Documents\... │ │ +│ └─────────────────────────────────────┘ │ +│ [添加同步] [全部启用] [全部禁用] │ +└────────────────────~~~~─────────────────────┘ +``` + +### 下一步计划 +1. **路径省略功能** - 实现长路径的智能省略显示 +2. **交互功能** - 添加按钮点击事件和命令绑定 +3. **编辑对话框** - 实现目录同步的编辑界面 +4. **验证系统** - 添加输入验证和错误提示 +5. **持久化** - 连接配置和同步设置的保存加载 + +### 当前状态 +- ✅ 程序可以正常编译运行 +- ✅ 界面布局符合设计文档要求 +- ✅ 数据绑定工作正常 +- ✅ 状态指示系统完整 +- ⏳ 等待用户验收当前进展 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/03-\350\277\233\345\272\246\346\230\276\347\244\272\345\222\214\346\225\260\346\215\256\347\273\221\345\256\232.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/03-\350\277\233\345\272\246\346\230\276\347\244\272\345\222\214\346\225\260\346\215\256\347\273\221\345\256\232.md" new file mode 100644 index 0000000..9a94da5 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/03-\350\277\233\345\272\246\346\230\276\347\244\272\345\222\214\346\225\260\346\215\256\347\273\221\345\256\232.md" @@ -0,0 +1,198 @@ +# Consolonia UI 进度条和数据绑定最佳实践 + +本文档总结了在 Consolonia 应用程序中实现进度条和解决数据绑定问题的最佳实践。 + +## 数据绑定更新通知问题 + +### 问题描述 +当 ViewModel 中的只读属性依赖于其他属性时,如果不主动触发属性更改通知,UI 可能不会自动更新。 + +### 解决方案 +在依赖属性发生变化时,主动调用 `OnPropertyChanged` 触发只读属性的更新通知: + +```csharp +public DirectorySyncingStatus Status +{ + get => _status; + set + { + if (SetField(ref _status, value)) + { + UpdateStatusDisplay(); + // 手动触发只读属性的更新通知 + OnPropertyChanged(nameof(StatusSymbol)); + OnPropertyChanged(nameof(StatusColor)); + } + } +} + +// 只读属性,根据 Status 计算 +public string StatusSymbol => Status switch +{ + DirectorySyncingStatus.Normal => "✓", + DirectorySyncingStatus.Error => "⚠", + DirectorySyncingStatus.Disabled => "✗", + DirectorySyncingStatus.Syncing => "◐", + _ => "○" +}; +``` + +## Consolonia 进度条实现 + +### 基本语法 +```xml + +``` + +### 要点说明 +1. **高度**: 在 TUI 中,进度条高度通常设置为 `Height="1"`(1个字符高度) +2. **宽度**: 根据界面空间调整,通常使用字符数量单位(如 `Width="30"`) +3. **颜色**: 使用 `Background` 和 `Foreground` 设置进度条的背景和前景色 +4. **绑定**: 使用 `{Binding PropertyName}` 绑定到 ViewModel 的进度属性 + +### 动态显示/隐藏 +```xml + + + + + +``` + +## 进度报告最佳实践 + +### ViewModel 设计 +```csharp +public class SyncViewModel : BindableRecord +{ + private double _globalSyncProgress; + private bool _isGlobalSyncing; + + public double GlobalSyncProgress + { + get => _globalSyncProgress; + private set => SetField(ref _globalSyncProgress, value); + } + + public bool IsGlobalSyncing + { + get => _isGlobalSyncing; + private set => SetField(ref _isGlobalSyncing, value); + } +} +``` + +### 进度更新模式 +```csharp +// 创建进度报告器 +var progress = new Progress(p => +{ + // 更新全局进度 + GlobalSyncProgress = p.TotalProgress; + + // 更新个别项目进度 + var currentItem = FindCurrentItem(p.CurrentFile); + if (currentItem != null) + { + currentItem.SyncProgress = p.CurrentFileProgress; + } +}); + +// 在操作开始时设置状态 +IsGlobalSyncing = true; +GlobalSyncProgress = 0; + +try +{ + // 执行异步操作 + await longRunningOperation(progress, cancellationToken); +} +finally +{ + // 清理状态 + IsGlobalSyncing = false; + GlobalSyncProgress = 0; +} +``` + +## 布局调整 + +### 动态行定义 +当向现有 Grid 添加新行时,需要更新 `RowDefinitions`: + +```xml + + + + + + + + + + +``` + +## 字符串格式化 + +### 百分比显示 +```xml + +``` + +### 要点 +- 使用 `StringFormat={}{0:F0}%` 格式化百分比 +- `{}` 用于转义 XAML 中的特殊字符 +- `F0` 表示保留0位小数 +- 可以根据需要调整小数位数(如 `F1` 保留1位小数) + +## 调试技巧 + +### 编译验证 +```powershell +cd "项目根目录" +dotnet build +``` + +### 运行测试 +```powershell +cd "项目根目录/src/ProjectName" +dotnet run +``` + +### 日志记录 +```csharp +Log.Debug($"[UI] 同步进度: {progress:F2}%, 当前文件: {currentFile}"); +``` + +## 常见问题 + +### 1. 进度条不显示 +检查 `IsVisible` 绑定和 `IsGlobalSyncing` 属性是否正确设置。 + +### 2. 进度不更新 +确保 `Progress` 回调中正确更新了绑定的属性,并且属性有 `OnPropertyChanged` 通知。 + +### 3. 布局错乱 +检查 Grid 的 `RowDefinitions` 和各元素的 `Grid.Row` 属性是否匹配。 + +### 4. 只读属性不更新 +在依赖属性变化时,手动调用 `OnPropertyChanged(nameof(ReadOnlyProperty))`。 + +## 总结 + +本次实现的关键点: +1. 修复了 `StatusSymbol` 属性的绑定更新问题 +2. 添加了全局同步进度条功能 +3. 实现了进度条的动态显示/隐藏 +4. 确保了 UI 布局的正确性 + +这些实践可以应用于其他需要进度指示和动态数据绑定的场景。 diff --git "a/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/04-ViewModel\351\207\215\346\236\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/04-ViewModel\351\207\215\346\236\204\346\234\200\344\275\263\345\256\236\350\267\265.md" new file mode 100644 index 0000000..c4bd0a2 --- /dev/null +++ "b/.github/knowledge/\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243/\347\225\214\351\235\242\350\256\276\350\256\241/04-ViewModel\351\207\215\346\236\204\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -0,0 +1,215 @@ +# ViewModel重构最佳实践 + +本文档总结了SshRemoteDeviceInfoViewModel重构的经验和最佳实践。 + +## 重构背景 + +`SshRemoteDeviceInfoViewModel`原始文件有585行,超过了400行的重构建议,需要进行拆分优化。 + +## 重构策略 + +### 文件夹结构设计 + +``` +ViewModels/ +├── SshRemoteDeviceInfoViewModel.cs # 主ViewModel(精简版) +└── RemoteDevices/ # 远程设备相关 + └── Ssh/ # SSH设备专用 + ├── SshDeviceSyncViewModel.cs # 同步功能 + └── SshDeviceCommandsViewModel.cs # 命令处理 + +Views/ +├── SshRemoteDeviceInfoView.axaml # 主View +└── RemoteDevices/ # 远程设备Views(预留) + ├── README.md # 目录说明 + └── Ssh/ # SSH设备Views(预留) + └── README.md # SSH说明 +``` + +### 职责分离原则 + +#### 主ViewModel职责 +- 基础设备属性(连接名、主机、端口、用户名、密码) +- 子ViewModels的组合和初始化 +- 连接测试功能 + +#### 同步ViewModel职责 +- 目录同步管理 +- 同步进度追踪 +- 同步状态管理 +- 错误信息处理 + +#### 命令ViewModel职责 +- 所有按钮命令的处理 +- 与外部服务的交互 +- 配置保存逻辑 + +## 重构要点 + +### 1. 避免委托属性 +**错误做法**: +```csharp +public double GlobalSyncProgress => SyncViewModel.GlobalSyncProgress; +``` + +**正确做法**: +```csharp +public SshDeviceSyncViewModel SyncViewModel { get; } +``` + +**理由**:委托属性不会触发PropertyChanged事件,导致UI绑定失效。 + +### 2. ViewModel类型选择 +**规则**:在本项目中使用`record`而非`class` + +```csharp +public partial record SshDeviceSyncViewModel : BindableRecord +``` + +### 3. 初始化顺序 +**关键**:确保依赖关系正确 + +```csharp +public SshRemoteDeviceInfoViewModel(SshRemoteDeviceInfo info) : base(info) +{ + // 1. 先初始化基础属性 + _connectionName = info.ConnectionName; + + // 2. 再初始化子ViewModels + var fileSyncService = Container.Current.EnsureGet(); + SyncViewModel = new SshDeviceSyncViewModel(fileSyncService); + CommandsViewModel = new SshDeviceCommandsViewModel(SyncViewModel, GetCurrentDeviceInfo); + + // 3. 最后加载数据 + SyncViewModel.InitializeDirectorySyncing(info.SyncDirectories); +} +``` + +### 4. UI绑定更新 +**重构前**: +```xml + +