diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index aed24d0..1ed11af 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -8,6 +8,8 @@ - 2026-03-25:继续微调 `/workspace` Workbench,新增默认“快捷指令”标签、调整三栏宽度到更接近 xterminal 参考图,并修复终端区域鼠标悬停时指针异常消失的问题。 ### 修复 +- **[frontend]**: 重排文件区右键菜单结构,补齐终端子菜单、复制文件名和复制绝对路径等动作 — by yinjianm + - 方案: [202603260228_file-context-menu-terminal-actions](archive/2026-03/202603260228_file-context-menu-terminal-actions/) - **[frontend]**: 将工作台文件区继续收敛为固定 `/` 根节点的单栏资源管理器树,并在树内同时显示目录与文件 — by yinjianm - 方案: [202603260212_workbench-file-root-tree](archive/2026-03/202603260212_workbench-file-root-tree/) - **[frontend]**: 修正快捷命令右键菜单的透明背景与粘贴项语义,改为实底菜单并将回填动作统一为“粘贴到命令输入框(不发送)” — by yinjianm @@ -24,6 +26,12 @@ - 方案: [202603250614_terminal-ansi-color-effects](archive/2026-03/202603250614_terminal-ansi-color-effects/) ### 快速修改 +- **[backend]**: 将后端包版本元数据同步提升到 `1.0.0`,与根工作区和其余主包保持一致 — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: packages/backend/package.json, packages/backend/package-lock.json, package-lock.json +- **[frontend]**: 将设置页本地版本显示调整为 `1.0`,并同步前端包版本元数据到 `1.0.0` — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: packages/frontend/package.json, package-lock.json, packages/frontend/src/composables/settings/useVersionCheck.ts, packages/frontend/src/composables/settings/useAboutSection.ts - **[workspace-root]**: 同步更新中英文 README,补充 monorepo 结构、最新功能清单与 `.helloagents/` 开发说明 — by yinjianm - 类型: 快速修改(无方案包) - 文件: README.md, doc/README_EN.md @@ -32,6 +40,8 @@ - 文件: packages/frontend/src/components/AddEditQuickCommandForm.vue:9,184-185,242-245 ### 新增 +- **[frontend]**: 为文件管理器补齐“上传文件夹”入口,选择目录后会先在浏览器端打包为 zip,再上传并自动触发远端解压 — by yinjianm + - 方案: [202603260234_folder-upload-auto-zip](archive/2026-03/202603260234_folder-upload-auto-zip/) - **[frontend]**: 为工作台文件面板补齐左侧多根目录资源管理器,支持收藏路径与当前路径同屏作为多个根目录展开浏览 — by yinjianm - 方案: [202603260041_workbench-file-multi-root-explorer](archive/2026-03/202603260041_workbench-file-multi-root-explorer/) - **[frontend]**: 为快捷指令编辑弹窗补充动态变量清单与点击插入,并统一列表执行/弹窗执行的动态变量解析链路 — by yinjianm diff --git a/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/.status.json b/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/.status.json new file mode 100644 index 0000000..13682c9 --- /dev/null +++ b/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":4,"failed":0,"pending":0,"total":4,"done":4,"percent":100,"current":"文件右键菜单已增强并补齐终端子菜单动作","updated_at":"2026-03-26 02:39:00"} diff --git a/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/proposal.md b/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/proposal.md new file mode 100644 index 0000000..6f68082 --- /dev/null +++ b/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/proposal.md @@ -0,0 +1,60 @@ +# 变更提案: file-context-menu-terminal-actions + +## 元信息 +```yaml +类型: 功能增强 +方案类型: implementation +优先级: P1 +状态: 已完成 +状态说明: 已按参考图重排文件右键菜单并补终端子菜单动作,前端构建通过 +创建: 2026-03-26 +``` + +--- + +## 1. 需求 + +### 背景 +当前文件区已有右键菜单基础能力,但结构、顺序和终端相关动作与参考图差距较大,尤其缺少“执行 cd 命令到终端 / 新建终端到当前目录”这类直接联动终端的入口。 + +### 目标 +- 将文件右键菜单按参考图重排为更接近资源管理器的结构。 +- 补齐终端子菜单,支持发送 `cd` 命令和新建终端到当前目录。 +- 保留现有下载、权限、复制路径、删除、上传等文件动作。 + +### 约束条件 +```yaml +范围约束: 优先限制在 FileManager.vue、FileManagerContextMenu.vue 和 useFileManagerContextMenu.ts +交互约束: 菜单结构接近参考图,但仍复用现有文件动作和上下文选择逻辑 +兼容约束: 空白处右键、多选右键和单文件右键保持可用 +后端约束: 不新增后端接口 +``` + +### 验收标准 +- [x] 单文件/目录右键菜单顺序与分组接近参考图 +- [x] 终端子菜单支持“执行 cd 命令到终端 / 新建终端到当前目录” +- [x] 文件名和绝对路径可分别复制 +- [x] 前端构建通过 + +--- + +## 2. 方案 + +### 技术方案 +在 `useFileManagerContextMenu.ts` 中扩展菜单项模型,支持图标、危险态和更明确的子菜单结构;重排单项右键菜单为“刷新 / 新建 / 重命名 / 下载 / 权限 / 终端 / 复制 / 删除 / 上传”顺序,并将终端子菜单动作通过 `FileManager.vue` 暴露的现有终端发送逻辑与新建会话逻辑实现。`FileManagerContextMenu.vue` 同步渲染图标、危险态和二级菜单样式。 + +### 影响范围 +```yaml +涉及模块: + - frontend: FileManager.vue + - frontend: FileManagerContextMenu.vue + - frontend: useFileManagerContextMenu.ts +预计变更文件: 3-6 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 新建终端后过早发送 `cd` 命令可能丢失 | 中 | 在新会话收到 `ssh:connected` 后再发送一次 `cd` | +| 单项菜单重排后与多选/空白处菜单的分组不一致 | 低 | 仅对单项菜单贴近参考图,多选和空白处菜单保留现有能力并做最小整理 | +| locale 新增文案可能与其他并行改动冲突 | 低 | 只做精确增量补丁,避免覆盖已有键 | diff --git a/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/tasks.md b/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/tasks.md new file mode 100644 index 0000000..e4b3d64 --- /dev/null +++ b/.helloagents/archive/2026-03/202603260228_file-context-menu-terminal-actions/tasks.md @@ -0,0 +1,42 @@ +# 任务清单: file-context-menu-terminal-actions + +```yaml +@feature: file-context-menu-terminal-actions +@created: 2026-03-26 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 4 | 0 | 0 | 4 | + +--- + +## 任务列表 + +### 1. 方案与范围确认 + +- [√] 1.1 创建文件右键菜单增强方案包并锁定到文件菜单链路 | depends_on: [] + +### 2. 菜单交互实现 + +- [√] 2.1 扩展上下文菜单项模型与菜单渲染,支持图标和终端子菜单 | depends_on: [1.1] +- [√] 2.2 在 FileManager 中补终端联动、复制文件名等菜单动作 | depends_on: [2.1] + +### 3. 验证与同步 + +- [√] 3.1 运行前端构建验证并同步 `.helloagents` 文档与归档记录 | depends_on: [2.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-26 02:28 | 1.1 | 完成 | 创建 implementation 方案包,范围锁定为文件右键菜单结构与终端子菜单增强 | +| 2026-03-26 02:33 | 2.1 | 完成 | 扩展菜单项模型与菜单渲染,支持图标、危险态和终端/上传二级菜单 | +| 2026-03-26 02:36 | 2.2 | 完成 | 在 FileManager 中补齐复制文件名、按目录发送 cd、以及新建终端后自动 cd 的动作 | +| 2026-03-26 02:39 | 3.1 | 完成 | `npm run build --workspace @nexus-terminal/frontend` 通过,准备同步知识库并归档 | diff --git a/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/.status.json b/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/.status.json new file mode 100644 index 0000000..e6a56ee --- /dev/null +++ b/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":6,"failed":0,"pending":0,"total":6,"done":6,"percent":100,"current":"已完成归档前同步:文件夹上传自动压缩并远端解压","updated_at":"2026-03-26 02:52:00"} diff --git a/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/proposal.md b/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/proposal.md new file mode 100644 index 0000000..61f1bd7 --- /dev/null +++ b/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/proposal.md @@ -0,0 +1,139 @@ +# 变更提案: folder-upload-auto-zip + +## 元信息 +```yaml +类型: 功能调整 +方案类型: implementation +优先级: P1 +状态: 已完成 +状态说明: 已补齐文件夹选择、浏览器端 zip 上传与远端自动解压,并通过前端构建验证 +创建: 2026-03-26 +``` + +--- + +## 1. 需求 + +### 背景 +当前文件管理器只支持普通文件上传。虽然拖拽目录时已经能递归遍历并逐个上传文件,但面对大量小文件目录时,浏览器侧扫描、前端逐文件分块、后端逐文件创建与远端 SFTP 写入都会显著拉长整体耗时,用户体感接近“卡住”。 + +### 目标 +- 在现有文件管理器中新增明确的“上传文件夹”入口。 +- 选择文件夹后,在浏览器端先将目录树压缩为单个 zip,再复用现有上传链路。 +- 上传成功后自动调用远端解压,尽量让用户获得“像直接上传文件夹”的结果。 +- 普通文件上传保持现有行为,不影响已有入口和拖拽目录上传兼容性。 + +### 约束条件 +```yaml +范围约束: 优先复用现有 sftp:upload 与 sftp:decompress,不新增 REST 上传接口 +前端约束: 需要在浏览器端完成目录树收集和 zip 构建 +后端约束: 远端自动解压依赖现有服务器命令检测与解压实现 +兼容约束: 普通文件上传、现有拖拽上传和文件树刷新逻辑不能回归 +``` + +### 验收标准 +- [ ] 文件管理器出现独立的“上传文件夹”入口 +- [ ] 选择文件夹后会先压缩为 zip,再作为单次上传任务发送 +- [ ] zip 上传成功后会自动触发远端解压 +- [ ] 普通文件上传行为保持不变 +- [ ] 前后端构建通过 + +--- + +## 2. 方案 + +### 技术方案 +在 `FileManager.vue` 中增加第二个隐藏目录选择 input,并将“上传文件”与“上传文件夹”拆分为两个明确入口。前端新增目录压缩逻辑:利用 `webkitdirectory` 返回的 `FileList` 生成目录树,并通过前端 zip 库构建一个临时 `File`/`Blob`。上传层继续走 `useFileUploader` 的 `sftp:upload:start/chunk` 协议,但补充目录压缩任务元数据、成功回调和自动解压回调;上传成功后复用 `useSftpActions` 现有 `decompressItem()` 能力,并在成功后清理临时 zip。 + +### 影响范围 +```yaml +涉及模块: + - frontend: FileManager.vue、useFileUploader.ts、upload.types.ts、locales + - backend: 无协议重构,最多仅需适配现有上传/解压消息的边界处理 +预计变更文件: 6-10 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 浏览器端压缩大目录时主线程占用明显 | 中 | 在 UI 中明确显示“压缩中”,并只对显式文件夹入口启用 | +| 远端缺少 unzip 或 tar 命令时自动解压失败 | 中 | 继续复用现有 `sftp:command_not_found` 提示,上传成功但解压失败时给出明确报错 | +| 自动解压后再清理临时 zip 可能因当前目录变更导致删除目标错误 | 中 | 删除逻辑基于上传完成返回的绝对远端路径,而不是依赖当前 UI 路径 | +| 目录名与 zip 临时文件名冲突 | 低 | 为临时 zip 文件名附加固定后缀,避免覆盖现有同名目录/文件 | + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart TD + A[FileManager 文件夹选择] --> B[浏览器收集 FileList] + B --> C[前端生成 zip Blob/File] + C --> D[useFileUploader 发起 sftp:upload] + D --> E[后端 SftpService 写入远端 zip] + E --> F[上传成功回调] + F --> G[useSftpActions.decompressItem] + G --> H[远端解压并刷新文件树] + H --> I[清理临时 zip] +``` + +### API设计 +本轮不新增独立 HTTP API,沿用现有 WebSocket 消息: + +- `sftp:upload:start` +- `sftp:upload:chunk` +- `sftp:upload:success` +- `sftp:decompress` +- `sftp:decompress:success` + +前端本地新增的只是上传任务元数据,不改动后端消息协议主体。 + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `mode` | `'file' \| 'folder-archive'` | 上传任务模式,区分普通文件与目录压缩上传 | +| `archiveFileName` | `string` | 浏览器端生成的临时 zip 文件名 | +| `remoteArchivePath` | `string` | 远端临时 zip 的绝对路径 | +| `decompressAfterUpload` | `boolean` | 上传成功后是否自动触发解压 | +| `cleanupArchiveAfterExtract` | `boolean` | 解压成功后是否自动删除临时 zip | + +--- + +## 4. 核心场景 + +### 场景: 上传文件夹并自动解压 +**模块**: frontend +**条件**: 用户在文件管理器点击“上传文件夹”,并选择一个本地目录。 +**行为**: 前端将目录内容打包为 zip,上传到当前远端目录,成功后自动调用解压并清理临时 zip。 +**结果**: 远端目录出现解压后的文件夹内容,用户无需手工上传 zip 再解压。 + +### 场景: 远端缺少解压命令 +**模块**: frontend / backend +**条件**: zip 上传成功,但服务器上没有可用解压命令。 +**行为**: 复用现有 `sftp:command_not_found` / `sftp:decompress:error` 错误反馈。 +**结果**: 用户能看到“上传成功但自动解压失败”的明确信号,并保留上传的 zip 文件用于手工处理。 + +--- + +## 5. 技术决策 + +### folder-upload-auto-zip#D001: 文件夹上传采用“前端压缩 + 现有上传协议 + 远端自动解压” +**日期**: 2026-03-26 +**状态**: ✅采纳 +**背景**: 现有目录上传是逐文件递归上传,小文件多时开销大;而后端已经具备远端解压能力。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 保持逐文件目录上传 | 不引入新依赖,链路简单 | 小文件多时扫描与上传时间长,用户体感差 | +| B: 前端压缩成 zip 后上传,再自动解压 | 最大化复用现有协议,明显减少小文件上传请求数 | 前端需要额外压缩逻辑,浏览器端会有打包耗时 | +| C: 后端接收目录流并服务端压缩/展开 | 可把压缩开销从浏览器移走 | 需要重写上传协议与后端缓存链路,改动面过大 | +**决策**: 选择方案 B +**理由**: 在当前仓库里,这是性能收益最大且改动最小的路径,能复用现有 `sftp:upload` 和 `sftp:decompress`,避免引入新的后端上传接口。 +**影响**: 主要影响 `packages/frontend` 的文件管理器和上传状态管理,后端维持现有 WebSocket/SFTP 能力。 + +--- + +## 6. 成果设计 + +N/A。本轮是现有文件管理器能力增强,不引入新的视觉主题方向,仅延续当前工具栏和上传浮层风格。 diff --git a/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/tasks.md b/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/tasks.md new file mode 100644 index 0000000..b44bcbe --- /dev/null +++ b/.helloagents/archive/2026-03/202603260234_folder-upload-auto-zip/tasks.md @@ -0,0 +1,56 @@ +# 任务清单: folder-upload-auto-zip + +```yaml +@feature: folder-upload-auto-zip +@created: 2026-03-26 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 6 | 0 | 0 | 6 | + +--- + +## 任务列表 + +### 1. 方案与范围确认 + +- [√] 1.1 创建文件夹压缩上传方案包并锁定为前端主导实现 | depends_on: [] + +### 2. 上传入口与压缩任务 + +- [√] 2.1 在 `packages/frontend/src/components/FileManager.vue` 中新增“上传文件夹”入口与目录选择 input | depends_on: [1.1] +- [√] 2.2 在前端实现目录选择后的 zip 打包流程,并生成可上传的临时归档文件 | depends_on: [2.1] + +### 3. 上传状态与自动解压 + +- [√] 3.1 在 `packages/frontend/src/composables/useFileUploader.ts` 和相关类型中扩展压缩中/解压中任务状态与成功回调 | depends_on: [2.2] +- [√] 3.2 上传成功后自动调用远端解压,并在成功时清理临时 zip 与刷新文件树 | depends_on: [3.1] + +### 4. 文案与验证 + +- [√] 4.1 更新 locale 文案并验证前后端构建 | depends_on: [3.2] +- [√] 4.2 同步 `.helloagents` 文档、模块说明与 CHANGELOG | depends_on: [4.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-26 02:34 | 1.1 | 完成 | 创建 implementation 方案包,并锁定为前端主导的文件夹压缩上传方案 | +| 2026-03-26 02:41 | 2.1 | 完成 | 文件管理器新增隐藏目录选择 input、上传文件夹按钮与上下文菜单入口 | +| 2026-03-26 02:45 | 2.2 | 完成 | 新增浏览器端目录压缩逻辑,选择文件夹后生成临时 zip 归档 | +| 2026-03-26 02:49 | 3.1 | 完成 | 扩展上传任务状态,支持压缩中、上传中和解压中串联展示 | +| 2026-03-26 02:50 | 3.2 | 完成 | 上传成功后自动触发远端解压,并尝试清理临时 zip | +| 2026-03-26 02:52 | 4.1 | 完成 | `npm run build --workspace @nexus-terminal/frontend` 通过 | + +--- + +## 执行备注 + +> 当前方案已按“前端压缩 + 现有上传协议 + 远端自动解压”落地;空目录仍受浏览器 `webkitdirectory` 选择结果限制,不会被单独打包上传。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index bfa0d3b..f87da6e 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -7,6 +7,8 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| +| 202603260234 | folder-upload-auto-zip | implementation | frontend | folder-upload-auto-zip#D001 | ✅完成 | +| 202603260228 | file-context-menu-terminal-actions | implementation | frontend | - | ✅完成 | | 202603260212 | workbench-file-root-tree | implementation | frontend | - | ✅完成 | | 202603260156 | quickcommands-context-menu-polish | implementation | frontend | quickcommands-context-menu-polish#D001 | ✅完成 | | 202603260150 | workbench-file-folder-overview | implementation | frontend | - | ✅完成 | @@ -35,6 +37,8 @@ ## 按月归档 ### 2026-03 +- [202603260234_folder-upload-auto-zip](./2026-03/202603260234_folder-upload-auto-zip/) - 为文件管理器补齐上传文件夹入口,选择目录后先打包为 zip,再上传并自动触发远端解压 +- [202603260228_file-context-menu-terminal-actions](./2026-03/202603260228_file-context-menu-terminal-actions/) - 重排文件区右键菜单结构,并补齐终端子菜单、复制文件名和复制绝对路径等动作 - [202603260212_workbench-file-root-tree](./2026-03/202603260212_workbench-file-root-tree/) - 将工作台文件区收敛为固定 / 根节点的单栏资源管理器树,并在树内同时显示目录与文件 - [202603260156_quickcommands-context-menu-polish](./2026-03/202603260156_quickcommands-context-menu-polish/) - 修正快捷命令右键菜单透明背景,并将粘贴动作统一为“粘贴到命令输入框(不发送)” - [202603260150_workbench-file-folder-overview](./2026-03/202603260150_workbench-file-folder-overview/) - 将工作台文件区调整为多根目录常驻的文件夹总览,不再点击目录后切成单独文件表格 diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index 2c93e4a..2af592b 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -36,7 +36,7 @@ ### 工作区交互 **条件**: 用户进入 `/workspace` 或相关管理页面。 -**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合快捷指令、命令历史、文件管理和编辑器,默认激活快捷指令。`CommandInputBar.vue` 当前已将底部命令框升级为支持会话级草稿保留的多行 `textarea`:普通 `Enter` 插入换行,`Ctrl+Shift+Enter` 发送当前命令,输入框会按内容自动增高至约 6 行,超出后在输入框内部滚动,并继续兼容快捷指令/命令历史同步与选中发送逻辑。快捷指令相关能力目前由 `AddEditQuickCommandForm.vue`、`QuickCommandsView.vue` 与新增的 `utils/quickCommandTemplate.ts` 协同实现:编辑弹窗左侧既可维护自定义 `${变量名}`,也提供 `${{date}}`、`${{time}}`、`${{timestamp}}`、`${{week}}`、`${{uuid}}`、`${{random:8}}`、`${{clipboard}}`、`${{password}}` 等动态变量的一键插入;实际执行时会统一走共享解析器,覆盖编辑弹窗执行、列表直接执行、粘贴到命令输入框和发送到全部服务器等链路,并对未定义变量、无法读取的剪贴板或不可用密码给出非阻断告警。`QuickCommandsView.vue` 内的新增按钮、空状态按钮和列表操作按钮统一复用 `bg-button`、`text-button-text`、`hover:bg-button-hover`、`hover:bg-border` 等主题变量类,避免写死黑白 hover 色值;该视图当前还支持命令项右键菜单,并已修正为实底卡片式上下文菜单,提供立即执行、粘贴到命令输入框(不自动发送)、复制命令、发送到全部服务器、编辑和删除等动作。`Terminal.vue` 会跟踪 xterm 的视口行号与贴底状态,在终端标签切换、重新激活和 `fit()` 后按原滚动意图恢复,并在渲染层为带 `xterm-fg-*` class 或内联 `style.color` 的显式前景色字符打标记,让终端文字描边/阴影仅作用于默认前景文本,不覆盖 ANSI 彩色输出;`session.store` 当前会为同一 SSH 连接下的新终端分配递增的 `terminalIndex`,`TerminalTabBar.vue` 则进一步把连续同连接会话渲染成“服务器组头 + 终端子标签 + 组尾新增按钮”,全局 `+` 只负责选择其他服务器,从而让“单连接默认 1 个终端、可继续追加多个终端”的关系在顶部标签栏里更接近参考图;`ConnectionsView.vue` 已升级为“左侧范围树 + 顶部搜索工具条 + 右侧结果列表”的双栏管理台,当前左侧进一步支持基于标签名路径分隔符推导的多级标签树、树节点展开状态持久化、分组 scope 恢复,以及树工具栏中的展开全部、收起全部和重置范围控制;近期又补上了独立的左侧树搜索、命中节点及祖先路径过滤、命中链路自动展开、节点计数高亮,以及更接近资源管理器的树头部布局;本轮继续为树节点加入 hover 工具按钮、资源管理器式分隔标题行与拖拽重排占位反馈;右侧结果列表则同时支持顶部排序控件、列头点击排序,并将行内操作整理为“连接”主按钮加“更多”菜单(编辑/测试/克隆/删除);`FileManager.vue` 当前已进一步收敛为固定 `/` 根节点的单栏资源管理器树,组件加载时会优先拉取 `/` 目录,树中按“目录在前、文件在后”同时显示目录和文件节点,点击目录只展开与聚焦,点击文件则沿用现有工作区文件打开链路;样式编辑器中的终端文字描边/阴影默认开关也已与新的黑绿终端风格保持默认开启。 +**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合快捷指令、命令历史、文件管理和编辑器,默认激活快捷指令。`CommandInputBar.vue` 当前已将底部命令框升级为支持会话级草稿保留的多行 `textarea`:普通 `Enter` 插入换行,`Ctrl+Shift+Enter` 发送当前命令,输入框会按内容自动增高至约 6 行,超出后在输入框内部滚动,并继续兼容快捷指令/命令历史同步与选中发送逻辑。快捷指令相关能力目前由 `AddEditQuickCommandForm.vue`、`QuickCommandsView.vue` 与新增的 `utils/quickCommandTemplate.ts` 协同实现:编辑弹窗左侧既可维护自定义 `${变量名}`,也提供 `${{date}}`、`${{time}}`、`${{timestamp}}`、`${{week}}`、`${{uuid}}`、`${{random:8}}`、`${{clipboard}}`、`${{password}}` 等动态变量的一键插入;实际执行时会统一走共享解析器,覆盖编辑弹窗执行、列表直接执行、粘贴到命令输入框和发送到全部服务器等链路,并对未定义变量、无法读取的剪贴板或不可用密码给出非阻断告警。`QuickCommandsView.vue` 内的新增按钮、空状态按钮和列表操作按钮统一复用 `bg-button`、`text-button-text`、`hover:bg-button-hover`、`hover:bg-border` 等主题变量类,避免写死黑白 hover 色值;该视图当前还支持命令项右键菜单,并已修正为实底卡片式上下文菜单,提供立即执行、粘贴到命令输入框(不自动发送)、复制命令、发送到全部服务器、编辑和删除等动作。`Terminal.vue` 会跟踪 xterm 的视口行号与贴底状态,在终端标签切换、重新激活和 `fit()` 后按原滚动意图恢复,并在渲染层为带 `xterm-fg-*` class 或内联 `style.color` 的显式前景色字符打标记,让终端文字描边/阴影仅作用于默认前景文本,不覆盖 ANSI 彩色输出;`session.store` 当前会为同一 SSH 连接下的新终端分配递增的 `terminalIndex`,`TerminalTabBar.vue` 则进一步把连续同连接会话渲染成“服务器组头 + 终端子标签 + 组尾新增按钮”,全局 `+` 只负责选择其他服务器,从而让“单连接默认 1 个终端、可继续追加多个终端”的关系在顶部标签栏里更接近参考图;`ConnectionsView.vue` 已升级为“左侧范围树 + 顶部搜索工具条 + 右侧结果列表”的双栏管理台,当前左侧进一步支持基于标签名路径分隔符推导的多级标签树、树节点展开状态持久化、分组 scope 恢复,以及树工具栏中的展开全部、收起全部和重置范围控制;近期又补上了独立的左侧树搜索、命中节点及祖先路径过滤、命中链路自动展开、节点计数高亮,以及更接近资源管理器的树头部布局;本轮继续为树节点加入 hover 工具按钮、资源管理器式分隔标题行与拖拽重排占位反馈;右侧结果列表则同时支持顶部排序控件、列头点击排序,并将行内操作整理为“连接”主按钮加“更多”菜单(编辑/测试/克隆/删除);`FileManager.vue` 当前已进一步收敛为固定 `/` 根节点的单栏资源管理器树,组件加载时会优先拉取 `/` 目录,树中按“目录在前、文件在后”同时显示目录和文件节点,点击目录只展开与聚焦,点击文件则沿用现有工作区文件打开链路;文件右键菜单链路则已补齐图标化菜单结构、危险态删除项、终端子菜单(执行 `cd` 命令到终端 / 新建终端到当前目录)、复制文件名与复制绝对路径等动作,并继续复用现有下载、权限、新建、上传和删除逻辑,同时又新增了独立“上传文件夹”入口:前端会先将本地目录打包为 zip,再复用现有 `sftp:upload` 链路上传,并在上传成功后自动调用远端解压、尝试清理临时压缩包,从而显著降低小文件很多时的扫描与上传耗时;样式编辑器中的终端文字描边/阴影默认开关也已与新的黑绿终端风格保持默认开启。 **结果**: 页面逻辑分散在 `views/`、`components/`、`stores/` 与 `composables/`,其中工作区终端行为和标签交互优先落在 `session.store.ts`、`session/actions/sessionActions.ts`、`session/getters.ts`、`TerminalTabBar.vue`、`WorkspaceView.vue`、`Terminal.vue` 与相关 locale 文件。 ### 仪表盘总览 diff --git a/package-lock.json b/package-lock.json index 0c2be03..663230b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5622,6 +5622,12 @@ "dev": true, "license": "ISC" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5939,6 +5945,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/klaw-sync": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", @@ -6018,6 +6066,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -7160,6 +7217,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8100,6 +8163,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -9793,7 +9862,7 @@ }, "packages/backend": { "name": "@nexus-terminal/backend", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@simplewebauthn/server": "^13.1.1", "@types/archiver": "^6.0.3", @@ -9922,7 +9991,7 @@ }, "packages/frontend": { "name": "@nexus-terminal/frontend", - "version": "0.8.1", + "version": "1.0.0", "dependencies": { "@codemirror/commands": "^6.8.1", "@codemirror/lang-cpp": "^6.0.2", @@ -9954,6 +10023,7 @@ "date-fns": "^4.1.0", "guacamole-common-js": "^1.5.0", "iconv-lite": "^0.6.3", + "jszip": "^3.10.1", "mitt": "^3.0.1", "monaco-editor": "^0.52.2", "pinia": "^3.0.2", diff --git a/packages/backend/package-lock.json b/packages/backend/package-lock.json index a43c3c0..7a2d100 100644 --- a/packages/backend/package-lock.json +++ b/packages/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nexus-terminal/backend", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nexus-terminal/backend", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@simplewebauthn/server": "^13.1.1", "@types/archiver": "^6.0.3", diff --git a/packages/backend/package.json b/packages/backend/package.json index fcc93e6..6e91c7a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@nexus-terminal/backend", - "version": "0.1.0", + "version": "1.0.0", "private": true, "main": "dist/index.js", "scripts": { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 29da0a3..6325ec6 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@nexus-terminal/frontend", - "version": "0.8.1", + "version": "1.0.0", "private": true, "type": "module", "scripts": { @@ -39,6 +39,7 @@ "date-fns": "^4.1.0", "guacamole-common-js": "^1.5.0", "iconv-lite": "^0.6.3", + "jszip": "^3.10.1", "mitt": "^3.0.1", "monaco-editor": "^0.52.2", "pinia": "^3.0.2", diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index 2c67c2a..6fb856e 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -13,6 +13,7 @@ import { useFileManagerContextMenu, type ClipboardState, type CompressFormat } f import { useFileManagerSelection } from '../composables/file-manager/useFileManagerSelection'; import { useFileManagerDragAndDrop } from '../composables/file-manager/useFileManagerDragAndDrop'; import { useFileManagerKeyboardNavigation } from '../composables/file-manager/useFileManagerKeyboardNavigation'; +import { createFolderArchive } from '../composables/file-manager/useFolderArchiveUpload'; import FileUploadPopup from './FileUploadPopup.vue'; import FileManagerContextMenu from './FileManagerContextMenu.vue'; import FileManagerActionModal from './FileManagerActionModal.vue'; @@ -102,6 +103,9 @@ const { uploads, startFileUpload, cancelUpload, + createUploadTask, + updateUploadTask, + cleanupUploadTask, } = useFileUploader( computed(() => props.sessionId), // 传递 manager 的 currentPath 和 fileList ref @@ -131,6 +135,7 @@ const { // --- UI 状态 Refs (Remain mostly the same) --- const fileInputRef = ref(null); +const folderInputRef = ref(null); const sortKey = ref('filename'); const sortDirection = ref<'asc' | 'desc'>('asc'); const isEditingPath = ref(false); @@ -142,6 +147,7 @@ const pathInputRef = ref(null); const editablePath = ref(''); const fileListContainerRef = ref(null); // 文件列表容器引用 const dropOverlayRef = ref(null); // +++ 拖拽蒙版引用 +++ +const isFolderUploadBusy = ref(false); // +++ Favorite Paths Modal State +++ const showFavoritePathsModal = ref(false); @@ -869,6 +875,90 @@ const handlePaste = () => { // --- 文件上传触发器 (定义在此处,供 Composable 使用) --- const triggerFileUpload = () => { fileInputRef.value?.click(); }; +const triggerFolderUpload = () => { + if (isFolderUploadBusy.value) { + return; + } + folderInputRef.value?.click(); +}; + +const getFolderUploadName = (files: File[]) => { + const firstRelativePath = files[0]?.webkitRelativePath || files[0]?.name || 'folder'; + return firstRelativePath.split('/').filter(Boolean)[0] || 'folder'; +}; + +const handleFolderSelected = async (event: Event) => { + const input = event.target as HTMLInputElement; + const files = input.files ? Array.from(input.files) : []; + input.value = ''; + + if (files.length === 0) { + return; + } + + if (!currentSftpManager.value || !props.wsDeps.isConnected.value) { + uiNotificationsStore.showError(t('fileManager.errors.sftpNotReady')); + return; + } + + const folderName = getFolderUploadName(files); + const uploadId = createUploadTask(folderName, { + status: 'compressing', + mode: 'folder-archive', + detail: t('fileManager.notifications.folderArchiveQueued', { count: files.length }), + }); + + isFolderUploadBusy.value = true; + + try { + const { archiveFile, entryCount } = await createFolderArchive(files, (progress) => { + updateUploadTask(uploadId, { + status: 'compressing', + progress, + detail: t('fileManager.notifications.folderArchivePreparing', { count: files.length }), + }); + }); + + updateUploadTask(uploadId, { + progress: 100, + detail: t('fileManager.notifications.folderArchiveReady', { count: entryCount }), + }); + + startFileUpload(archiveFile, undefined, { + uploadId, + displayName: folderName, + mode: 'folder-archive', + detail: t('fileManager.notifications.folderArchiveUploading', { count: entryCount }), + afterUpload: async ({ remotePath }) => { + if (!currentSftpManager.value) { + throw new Error(t('fileManager.errors.sftpManagerNotFound')); + } + + await currentSftpManager.value.decompressPath(remotePath, folderName); + + try { + await currentSftpManager.value.unlinkPath(remotePath); + } catch (cleanupError: any) { + console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to cleanup uploaded archive ${remotePath}:`, cleanupError); + uiNotificationsStore.showWarning( + t('fileManager.errors.archiveCleanupFailed', { + name: remotePath.split('/').pop() || remotePath, + }), + ); + } + }, + }); + } catch (error: any) { + console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to archive folder before upload:`, error); + updateUploadTask(uploadId, { + status: 'error', + error: error?.message || t('fileManager.errors.folderCompressionFailed'), + }); + cleanupUploadTask(uploadId, 5000); + } finally { + isFolderUploadBusy.value = false; + } +}; // --- 下载触发器 (定义在此处,供 Composable 使用) --- const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileListItem 数组 @@ -1038,6 +1128,90 @@ const handleCopyPath = async (item: FileListItem) => { } }; +const handleCopyFilename = async (item: FileListItem) => { + try { + await navigator.clipboard.writeText(item.filename); + uiNotificationsStore.showSuccess(t('fileManager.notifications.filenameCopied', 'Filename copied to clipboard')); + } catch (err) { + console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to copy filename: `, err); + uiNotificationsStore.showError(t('fileManager.errors.copyFilenameFailed', 'Failed to copy filename')); + } +}; + +const getTargetPathForItem = (item?: FileListItem | null): string | null => { + if (!currentSftpManager.value) { + return null; + } + + const currentPath = currentSftpManager.value.currentPath.value; + if (!item || item.filename === '..') { + return currentPath; + } + + if (item.attrs.isDirectory) { + return currentSftpManager.value.joinPath(currentPath, item.filename); + } + + return currentPath; +}; + +const sendCdCommandToPath = (targetPath: string, sessionId?: string) => { + if (!props.wsDeps.isConnected.value) { + console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot send CD command: not connected.`); + return; + } + + const escapedPath = `"${targetPath}"`; + const command = `cd ${escapedPath}\n`; + + try { + const targetSession = sessionId ? sessionStore.sessions.get(sessionId) : sessionStore.activeSession; + if (!targetSession?.terminalManager) { + console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to send command: Terminal manager not found.`); + return; + } + + targetSession.terminalManager.sendData(command); + } catch (error) { + console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to send command to terminal:`, error); + } +}; + +const handleSendItemPathToTerminal = (item: FileListItem) => { + const targetPath = getTargetPathForItem(item); + if (!targetPath) { + return; + } + + sendCdCommandToPath(targetPath); +}; + +const handleOpenTerminalAtItemPath = (item: FileListItem) => { + const targetPath = getTargetPathForItem(item); + const connectionId = Number(props.dbConnectionId); + if (!targetPath || Number.isNaN(connectionId)) { + return; + } + + const previousActiveSessionId = sessionStore.activeSessionId; + sessionStore.handleOpenNewSession(connectionId); + const newSessionId = sessionStore.activeSessionId; + + if (!newSessionId || newSessionId === previousActiveSessionId) { + return; + } + + const newSession = sessionStore.sessions.get(newSessionId); + if (!newSession?.wsManager) { + return; + } + + const unregister = newSession.wsManager.onMessage('ssh:connected', () => { + sendCdCommandToPath(targetPath, newSessionId); + unregister?.(); + }); +}; + // --- 上下文菜单逻辑 (使用 Composable, 需要 Selection 和 Action Handlers) --- const { contextMenuVisible, @@ -1065,6 +1239,7 @@ const { } }, onUpload: triggerFileUpload, + onUploadFolder: triggerFolderUpload, onDownload: triggerDownload, onDelete: handleDeleteSelectedClick, onRename: handleRenameContextMenuClick, @@ -1079,6 +1254,9 @@ const { onCompressRequest: handleCompress, onDecompressRequest: handleDecompress, onCopyPath: handleCopyPath, // +++ 传递复制路径回调 +++ + onCopyFilename: handleCopyFilename, + onSendCdToTerminal: handleSendItemPathToTerminal, + onOpenTerminalAtPath: handleOpenTerminalAtItemPath, }); // --- 目录加载与导航 --- @@ -1970,10 +2148,11 @@ watch( class="left-0 right-0 top-full mt-1" /> - +
+ +