From 1f52ff6e0a701c668865c3286f3d6cbba5f12b43 Mon Sep 17 00:00:00 2001 From: yinjianm Date: Wed, 25 Mar 2026 23:57:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(workspace):=20=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E8=A1=8C=E5=91=BD=E4=BB=A4=E8=BE=93=E5=85=A5=E5=B9=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BB=AA=E8=A1=A8=E7=9B=98=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将底部命令输入框改为支持自动增高的多行 textarea, 并把发送快捷键调整为 Ctrl+Shift+Enter,同时更新多语言提示文案 新增 dashboard summary 后端接口与聚合类型定义, 为首页管理驾驶舱改造提供统一数据入口,并同步知识库方案记录 --- .helloagents/CHANGELOG.md | 2 + .helloagents/INDEX.md | 2 +- .../.status.json | 1 + .../proposal.md | 134 ++++++++++ .../tasks.md | 45 ++++ .helloagents/archive/_index.md | 2 + .helloagents/modules/frontend.md | 2 +- .../proposal.md | 184 +++++++++++++ .../tasks.md | 50 ++++ .../.status.json | 1 + .../proposal.md | 206 +++++++++++++++ .../tasks.md | 58 ++++ .../src/dashboard/dashboard.controller.ts | 19 ++ .../src/dashboard/dashboard.repository.ts | 247 ++++++++++++++++++ .../backend/src/dashboard/dashboard.routes.ts | 12 + .../src/dashboard/dashboard.service.ts | 8 + .../backend/src/dashboard/dashboard.types.ts | 45 ++++ packages/backend/src/index.ts | 2 + .../src/components/CommandInputBar.vue | 58 +++- packages/frontend/src/locales/en-US.json | 2 +- packages/frontend/src/locales/ja-JP.json | 2 +- packages/frontend/src/locales/zh-CN.json | 2 +- 22 files changed, 1068 insertions(+), 16 deletions(-) create mode 100644 .helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/.status.json create mode 100644 .helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/proposal.md create mode 100644 .helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/tasks.md create mode 100644 .helloagents/plan/202603252343_dashboard-management-cockpit/proposal.md create mode 100644 .helloagents/plan/202603252343_dashboard-management-cockpit/tasks.md create mode 100644 .helloagents/plan/202603252354_login-credential-management/.status.json create mode 100644 .helloagents/plan/202603252354_login-credential-management/proposal.md create mode 100644 .helloagents/plan/202603252354_login-credential-management/tasks.md create mode 100644 packages/backend/src/dashboard/dashboard.controller.ts create mode 100644 packages/backend/src/dashboard/dashboard.repository.ts create mode 100644 packages/backend/src/dashboard/dashboard.routes.ts create mode 100644 packages/backend/src/dashboard/dashboard.service.ts create mode 100644 packages/backend/src/dashboard/dashboard.types.ts diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index f65c3ec..e6f253a 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -18,6 +18,8 @@ - 方案: [202603250614_terminal-ansi-color-effects](archive/2026-03/202603250614_terminal-ansi-color-effects/) ### 新增 +- **[frontend]**: 将底部命令输入框升级为支持多行草稿与自动增高,并把发送快捷键改为 `Ctrl+Shift+Enter` — by yinjianm + - 方案: [202603252340_command-input-multiline-shortcut](archive/2026-03/202603252340_command-input-multiline-shortcut/) - **[frontend]**: 将服务器状态中的内存与磁盘区域升级为卡片化监控视图,补齐环形内存占比、磁盘设备信息、读写速率与挂载表格展示 — by yinjianm - 方案: [202603252200_server-status-memory-disk-cards](archive/2026-03/202603252200_server-status-memory-disk-cards/) - **[backend]**: 扩展 `StatusMonitorService` 的内存/磁盘采集字段,新增缓存、空闲、挂载点、文件系统类型、磁盘设备与磁盘 I/O 速率 — by yinjianm diff --git a/.helloagents/INDEX.md b/.helloagents/INDEX.md index 19a5036..2cddde5 100644 --- a/.helloagents/INDEX.md +++ b/.helloagents/INDEX.md @@ -31,7 +31,7 @@ ```yaml kb_version: 2.3.7 -最后更新: 2026-03-25 23:03 +最后更新: 2026-03-25 23:45 模块数量: 4 待执行方案: 0 ``` diff --git a/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/.status.json b/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/.status.json new file mode 100644 index 0000000..d3145b3 --- /dev/null +++ b/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":4,"failed":0,"pending":0,"total":4,"done":4,"percent":100,"current":"已完成:命令输入多行与发送快捷键改造","updated_at":"2026-03-25 23:45:00"} diff --git a/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/proposal.md b/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/proposal.md new file mode 100644 index 0000000..9baeab4 --- /dev/null +++ b/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/proposal.md @@ -0,0 +1,134 @@ +# 变更提案: command-input-multiline-shortcut + +## 元信息 +```yaml +类型: 优化 +方案类型: implementation +优先级: P1 +状态: 已完成 +创建: 2026-03-25 +完成: 2026-03-25 +``` + +--- + +## 1. 需求 + +### 背景 +当前工作区底部命令输入条仍是单行 `input`,默认按 `Enter` 立即发送到终端。这会阻碍输入多行命令或临时脚本,也容易在编辑中误发送。用户希望把发送动作切换为 `Ctrl+Shift+Enter`,并让输入框按内容自动扩展为多行。 + +### 目标 +- 将终端命令发送快捷键从单独 `Enter` 改为 `Ctrl+Shift+Enter`。 +- 将命令输入框升级为支持多行输入和按内容动态增高。 +- 保留现有会话级命令草稿状态、搜索切换、命令历史/快捷指令联动和空命令发送能力。 + +### 约束条件 +```yaml +时间约束: 本轮内完成前端改造与基础验证 +性能约束: 自动增高应只作用于当前输入框,不引入新的重型依赖 +兼容性约束: 保持现有 session store 的 commandInputContent 数据结构不变 +业务约束: 不破坏快捷指令/命令历史选中后回车发送逻辑;多行输入最多扩展约 6 行,超出后内部滚动 +``` + +### 验收标准 +- [ ] `CommandInputBar.vue` 支持多行命令输入,输入框随内容自动增高,最大高度约 6 行 +- [ ] 普通 `Enter` 仅插入换行,不再直接发送;`Ctrl+Shift+Enter` 可发送当前命令内容 +- [ ] 快捷指令/命令历史存在选中项时,发送逻辑仍可正常工作 +- [ ] 中英文/日文占位提示同步更新为新的快捷键说明 +- [ ] `packages/frontend` 的类型检查与构建通过 + +--- + +## 2. 方案 + +### 技术方案 +将 `CommandInputBar.vue` 中的单行 `input` 改为 `textarea`,继续通过 `currentSessionCommandInput` 读写当前会话的命令草稿。新增输入框高度同步逻辑,在内容变化、会话切换和组件挂载后按 `scrollHeight` 重新计算高度,并限制最大高度。键盘处理改为区分三类路径:`Ctrl+Shift+Enter` 发送当前输入、存在同步面板选中项时优先发送选中命令、普通 `Enter` 保持换行。 + +### 影响范围 +```yaml +涉及模块: + - frontend: `CommandInputBar.vue` 的输入组件、快捷键与高度同步逻辑 + - frontend: locale 文案中的命令输入占位提示 +预计变更文件: 3-4 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| `textarea` 替换后影响现有焦点切换、搜索切换或样式布局 | 中 | 复用现有 ref 与 focus action,只做输入节点最小替换 | +| 自动增高在切换会话或清空输入后高度残留 | 中 | 在内容变更、activeSession 变更和发送后统一触发高度重算 | +| 快捷指令/命令历史列表的选中发送逻辑被新快捷键覆盖 | 中 | 保留现有选中命令优先路径,并改为新快捷键触发 | + +--- + +## 3. 技术设计(可选) + +### 架构设计 +```mermaid +flowchart LR + A[session.commandInputContent] --> B[CommandInputBar textarea] + B --> C[auto resize] + B --> D[keydown handler] + D --> E[terminal:sendCommand] + D --> F[quickCommands/history selected item] +``` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `commandInputRef` | `HTMLTextAreaElement \| null` | 命令输入框 DOM 引用 | +| `maxCommandInputHeight` | `number` | 动态高度上限,约 6 行 | +| `currentSessionCommandInput` | `string` | 当前活动会话的命令草稿内容 | + +--- + +## 4. 核心场景 + +### 场景: 多行命令编辑并发送 +**模块**: frontend +**条件**: 用户在工作区命令输入条中输入多行命令或脚本片段。 +**行为**: 输入框按内容自动扩展高度,普通 `Enter` 插入换行,`Ctrl+Shift+Enter` 发送当前内容到终端。 +**结果**: 用户可在发送前完整编辑多行命令,不会因单独回车误触发发送。 + +### 场景: 选中快捷指令或历史命令后发送 +**模块**: frontend +**条件**: 命令输入同步到快捷指令或命令历史,且列表中存在当前选中项。 +**行为**: 用户按发送快捷键时优先发送选中的命令项,并清空输入框与选中状态。 +**结果**: 原有联动发送体验保持不变,只是发送触发键改为新组合键。 + +--- + +## 5. 技术决策 + +### command-input-multiline-shortcut#D001: 使用 `textarea` + 自动高度同步,而不是保留单行 `input` +**日期**: 2026-03-25 +**状态**: ✅采纳 +**背景**: 需求同时要求“多行输入”和“动态支持多行”,单行 `input` 无法自然承载换行编辑与高度增长。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 改为 `textarea` 并动态调整高度 | 原生支持换行,改动集中,易于和现有 v-model/focus 逻辑兼容 | 需要额外处理高度重置和最大高度 | +| B: 保留 `input`,另做弹层或隐藏编辑器 | 可保留单行样式 | 交互割裂,改动范围更大,和用户诉求不匹配 | +**决策**: 选择方案A +**理由**: `textarea` 是最小且直接满足需求的实现路径,可以在不改 store 结构的前提下提供多行编辑与动态高度能力。 +**影响**: frontend + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有终端工作台的轻量工具条风格,不引入额外视觉噪声 +- **记忆点**: 单行输入自然生长为多行命令编辑区,但整体仍保持底部命令条的一体化布局 +- **参考**: 当前 `CommandInputBar` 样式语言 + 常见终端/IDE 命令面板的多行输入体验 + +### 视觉要素 +- **配色**: 保持现有 `bg-input`、`border-border/50`、`focus:ring-primary/50` 体系不变 +- **字体**: 沿用当前项目输入控件字体体系,保持终端工作台一致性 +- **布局**: 命令框横向占满剩余空间,纵向在 1 行到约 6 行之间平滑扩展 +- **动效**: 保留现有 `transition-all duration-300 ease-in-out`,让高度变化与 focus 态自然过渡 +- **氛围**: 不额外增加装饰,重点保持工具型界面的克制与稳定 + +### 技术约束 +- **可访问性**: 保留 placeholder 与键盘操作,避免纯图标表达发送规则 +- **响应式**: 在移动端仍需保持输入框可用,不挤压既有工具按钮 diff --git a/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/tasks.md b/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/tasks.md new file mode 100644 index 0000000..fcc4061 --- /dev/null +++ b/.helloagents/archive/2026-03/202603252340_command-input-multiline-shortcut/tasks.md @@ -0,0 +1,45 @@ +# 任务清单: command-input-multiline-shortcut + +```yaml +@feature: command-input-multiline-shortcut +@created: 2026-03-25 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 4 | 0 | 0 | 4 | + +--- + +## 任务列表 + +### 1. 命令输入交互改造 + +- [√] 1.1 在 `packages/frontend/src/components/CommandInputBar.vue` 中将命令输入节点从单行输入改为支持多行的 `textarea`,补齐自动高度同步逻辑 | depends_on: [] +- [√] 1.2 在 `packages/frontend/src/components/CommandInputBar.vue` 中调整键盘处理逻辑,将发送动作改为 `Ctrl+Shift+Enter`,并保持快捷指令/命令历史选中发送路径可用 | depends_on: [1.1] + +### 2. 文案与验证 + +- [√] 2.1 更新 `packages/frontend/src/locales/zh-CN.json`、`packages/frontend/src/locales/en-US.json`、`packages/frontend/src/locales/ja-JP.json` 中的命令输入提示文案,反映新的发送快捷键 | depends_on: [1.2] +- [√] 2.2 执行 `packages/frontend` 的构建验证,确认类型检查与打包通过 | depends_on: [2.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-25 23:40 | DESIGN | completed | 已确认采用 textarea 自动增高方案,最多约 6 行并改用 Ctrl+Shift+Enter 发送 | +| 2026-03-25 23:44 | 1.1 / 1.2 | 完成 | `CommandInputBar.vue` 已切换为多行 textarea,并将发送动作改为 Ctrl+Shift+Enter | +| 2026-03-25 23:44 | 2.1 | 完成 | 已同步中文、英文、日文命令输入占位提示文案 | +| 2026-03-25 23:45 | 2.2 | 完成 | `packages/frontend` 执行 `npm run build` 通过,仅保留既有 Vite chunk 警告 | + +--- + +## 执行备注 + +> 当前环境缺少可直接调用的 `python/py`,方案包通过模板降级方式手工创建。本轮未做浏览器级工作区实机操作,运行态确认以代码审查和前端构建通过为主。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index 714bf99..2730021 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -19,6 +19,7 @@ | 202603252220 | connections-tree-toolbar-menu-polish | implementation | frontend | - | ✅完成 | | 202603252310 | connections-tree-search-explorer-polish | implementation | frontend | - | ✅完成 | | 202603252336 | connections-tree-hover-drag-polish | implementation | frontend | - | ✅完成 | +| 202603252340 | command-input-multiline-shortcut | implementation | frontend | command-input-multiline-shortcut#D001 | ✅完成 | | 202603252229 | terminal-tab-group-visual | implementation | frontend | terminal-tab-group-visual#D001 | ✅完成 | | 202603252256 | workspace-monitor-terminal-polish | implementation | workspace-root | workspace-monitor-terminal-polish#D001 | ✅完成 | | 202603251200 | workspace-workbench-monitor | implementation | frontend, backend | workspace-workbench-monitor#D001 | ✅完成 | @@ -38,6 +39,7 @@ - [202603252220_connections-tree-toolbar-menu-polish](./2026-03/202603252220_connections-tree-toolbar-menu-polish/) - 为连接管理页补树工具栏与展开/收起控制,并整理行内更多菜单 - [202603252310_connections-tree-search-explorer-polish](./2026-03/202603252310_connections-tree-search-explorer-polish/) - 为连接管理页补左侧树搜索、命中链路过滤、节点计数高亮和资源管理器式头部布局 - [202603252336_connections-tree-hover-drag-polish](./2026-03/202603252336_connections-tree-hover-drag-polish/) - 为连接管理页补树节点 hover 工具、分隔标题行和拖拽重排占位反馈 +- [202603252340_command-input-multiline-shortcut](./2026-03/202603252340_command-input-multiline-shortcut/) - 将命令输入框改为多行自动增高,并改用 Ctrl+Shift+Enter 发送 - [202603252229_terminal-tab-group-visual](./2026-03/202603252229_terminal-tab-group-visual/) - 将顶部终端标签栏改成更明显的服务器组头与终端子标签 - [202603252256_workspace-monitor-terminal-polish](./2026-03/202603252256_workspace-monitor-terminal-polish/) - 重新核对状态监控与终端标签剩余改动,并修正知识库归档索引与活跃方案状态 - [202603251200_workspace-workbench-monitor](./2026-03/202603251200_workspace-workbench-monitor/) - `/workspace` 改为三栏 Workbench 布局,并新增开机累计流量监控 diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index ca454e6..8165fe6 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -36,7 +36,7 @@ ### 工作区交互 **条件**: 用户进入 `/workspace` 或相关管理页面。 -**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合快捷指令、命令历史、文件管理和编辑器,默认激活快捷指令。`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 工具按钮、资源管理器式分隔标题行与拖拽重排占位反馈;右侧结果列表则同时支持顶部排序控件、列头点击排序,并将行内操作整理为“连接”主按钮加“更多”菜单(编辑/测试/克隆/删除);样式编辑器中的终端文字描边/阴影默认开关也已与新的黑绿终端风格保持默认开启。 +**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合快捷指令、命令历史、文件管理和编辑器,默认激活快捷指令。`CommandInputBar.vue` 当前已将底部命令框升级为支持会话级草稿保留的多行 `textarea`:普通 `Enter` 插入换行,`Ctrl+Shift+Enter` 发送当前命令,输入框会按内容自动增高至约 6 行,超出后在输入框内部滚动,并继续兼容快捷指令/命令历史同步与选中发送逻辑。`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 工具按钮、资源管理器式分隔标题行与拖拽重排占位反馈;右侧结果列表则同时支持顶部排序控件、列头点击排序,并将行内操作整理为“连接”主按钮加“更多”菜单(编辑/测试/克隆/删除);样式编辑器中的终端文字描边/阴影默认开关也已与新的黑绿终端风格保持默认开启。 **结果**: 页面逻辑分散在 `views/`、`components/`、`stores/` 与 `composables/`,其中工作区终端行为和标签交互优先落在 `session.store.ts`、`session/actions/sessionActions.ts`、`session/getters.ts`、`TerminalTabBar.vue`、`WorkspaceView.vue`、`Terminal.vue` 与相关 locale 文件。 ## 依赖关系 diff --git a/.helloagents/plan/202603252343_dashboard-management-cockpit/proposal.md b/.helloagents/plan/202603252343_dashboard-management-cockpit/proposal.md new file mode 100644 index 0000000..4041f7b --- /dev/null +++ b/.helloagents/plan/202603252343_dashboard-management-cockpit/proposal.md @@ -0,0 +1,184 @@ +# 变更提案: dashboard-management-cockpit + +## 元信息 +```yaml +类型: 新功能 +方案类型: implementation +优先级: P1 +状态: 草稿 +创建: 2026-03-25 +``` + +--- + +## 1. 需求 + +### 背景 +当前首页仪表盘主要由连接列表和最近活动组成,信息密度偏低,缺少“总览面板”应有的关键统计、趋势和分布视角。用户明确反馈该区域“太简陋”,希望加入各类数据统计,使首页能更快反映连接资产规模、近期活跃度和审计行为概况。 + +### 目标 +- 将首页升级为管理驾驶舱式仪表盘,而不再只是列表首页。 +- 新增可快速扫读的统计卡片、近 7 天趋势图、连接类型分布和高频连接排行。 +- 通过后端聚合接口统一提供 dashboard summary,避免前端依赖多接口拼装复杂统计。 +- 保留现有连接列表、最近活动和跳转链路,让首页既能看总览,也能继续执行常用操作。 + +### 约束条件 +```yaml +时间约束: 本轮内完成前后端联动与基础构建验证 +性能约束: 仪表盘首页统计应通过单个 summary 接口返回,避免首屏并发拉取过多数据 +兼容性约束: 保持现有 `/connections` 与 `/audit-logs` 页面和接口行为不变 +业务约束: 统计口径优先使用当前数据库中已稳定存在的连接表和审计日志表,不依赖临时内存状态 +``` + +### 验收标准 +- [ ] 首页新增核心统计卡片,至少覆盖连接总数、近 7 天活跃连接数、审计日志总数、近 24 小时 SSH 成功/失败次数 +- [ ] 首页新增趋势和分布图表,至少覆盖近 7 天审计事件趋势和连接类型分布 +- [ ] 首页新增“近期最活跃连接”排行,基于审计日志中的连接事件聚合 +- [ ] 后端提供单独的 dashboard summary API,并由前端页面消费 +- [ ] `packages/frontend` 与 `packages/backend` 的构建校验通过 + +--- + +## 2. 方案 + +### 技术方案 +在后端新增 `dashboard` 聚合模块,提供 `GET /api/v1/dashboard/summary` 接口,直接基于 SQLite 中的 `connections`、`connection_tags`、`audit_logs` 数据生成首页所需的统计摘要。前端新增 dashboard store 与 summary 类型定义,重构 `DashboardView.vue`,将当前首页拆分为“统计卡片 + 图表区 + 排行/列表区 + 最近活动”,并继续复用现有连接列表与最近活动交互。图表继续使用仓库已存在的 `chart.js` / `vue-chartjs` 技术栈。 + +### 影响范围 +```yaml +涉及模块: + - backend: 新增 dashboard 聚合接口与数据查询逻辑 + - frontend: 新增 summary 类型和 store,重构 DashboardView 展示结构 + - frontend: 补充 dashboard 多语言文案 +预计变更文件: 10-14 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 审计日志 `details` 为 JSON 字符串,连接排行和事件摘要解析不稳 | 中 | 后端统一做安全解析,解析失败时忽略单条异常数据,不阻断整体 summary | +| 新增 dashboard 接口后,首页首屏依赖单次聚合查询,若 SQL 设计粗糙会拖慢加载 | 中 | 采用有限窗口统计和聚合 SQL,避免全量明细搬运到前端 | +| DashboardView 重构后破坏现有连接列表或最近活动交互 | 中 | 保留现有 store 和核心交互函数,仅将展示层重组并补充 summary 数据源 | + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart LR + A[DashboardView] --> B[dashboard.store] + B --> C[/api/v1/dashboard/summary] + C --> D[dashboard.controller] + D --> E[dashboard.service] + E --> F[(connections)] + E --> G[(connection_tags)] + E --> H[(audit_logs)] +``` + +### API设计 +#### GET /api/v1/dashboard/summary +- **请求**: 无 +- **响应**: +```json +{ + "totals": { + "connections": 0, + "activeConnections7d": 0, + "taggedConnections": 0, + "auditLogs": 0 + }, + "sshOutcomes24h": { + "success": 0, + "failure": 0 + }, + "connectionTypes": [ + { "type": "SSH", "count": 0 } + ], + "activityTrend7d": [ + { "date": "2026-03-25", "count": 0 } + ], + "topConnections": [ + { "connectionId": 1, "connectionName": "prod-1", "host": "1.2.3.4", "count": 8 } + ] +} +``` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `totals.connections` | `number` | 连接总数 | +| `totals.activeConnections7d` | `number` | 最近 7 天内 `last_connected_at` 有值的连接数 | +| `totals.taggedConnections` | `number` | 至少绑定一个标签的连接数 | +| `totals.auditLogs` | `number` | 审计日志总数 | +| `sshOutcomes24h.success` | `number` | 24 小时内 SSH 成功次数 | +| `sshOutcomes24h.failure` | `number` | 24 小时内 SSH 失败次数 | +| `connectionTypes[]` | `{ type, count }[]` | 按连接类型统计 | +| `activityTrend7d[]` | `{ date, count }[]` | 近 7 天事件趋势 | +| `topConnections[]` | `{ connectionId, connectionName, host, count }[]` | 近期最活跃连接排行 | + +--- + +## 4. 核心场景 + +### 场景: 首页快速判断系统活跃度 +**模块**: frontend / backend +**条件**: 用户登录后进入首页仪表盘。 +**行为**: 页面调用 summary 接口并展示总量卡片、近 7 天趋势和 24 小时 SSH 结果摘要。 +**结果**: 用户无需进入连接页和审计页,就能快速判断近期使用情况与异常趋势。 + +### 场景: 从总览切换到具体操作 +**模块**: frontend +**条件**: 用户在仪表盘查看高频连接、连接列表和最近活动。 +**行为**: 用户可直接从连接列表发起连接,或从最近活动跳转到完整审计日志页。 +**结果**: 仪表盘成为“总览 + 操作入口”的组合页,而非只读报表。 + +--- + +## 5. 技术决策 + +### dashboard-management-cockpit#D001: 采用后端聚合 summary 接口,而不是前端多接口拼装 +**日期**: 2026-03-25 +**状态**: ✅采纳 +**背景**: 管理驾驶舱需要同时展示总量、趋势、分布和排行。若继续依赖前端分别拉取连接列表和审计日志再本地聚合,首页逻辑会迅速变重,且趋势统计需要更多分页数据支撑。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 新增后端 summary 接口 | 首页只需一次请求,统计口径集中统一,前端组件更清晰 | 需要新增后端模块和类型定义 | +| B: 前端继续基于 `/connections` 与 `/audit-logs` 本地聚合 | 不需要新增后端路由 | 首页需要管理更多请求和聚合逻辑,趋势/排行容易依赖不完整数据 | +**决策**: 选择方案A +**理由**: 这次需求明确是“管理驾驶舱版”而非轻量美化,后端聚合更适合承载统计口径和后续扩展。 +**影响**: backend, frontend + +### dashboard-management-cockpit#D002: 首页统计只使用数据库稳定数据,不引入在线会话内存态 +**日期**: 2026-03-25 +**状态**: ✅采纳 +**背景**: 在线会话数等实时指标需要额外打通 WebSocket 或运行时状态管理,而当前仓库首页并无现成只读接口。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 先基于连接表和审计日志做稳定指标 | 实现确定性高,口径清晰,适合本轮交付 | 暂不覆盖实时在线会话数 | +| B: 额外引入内存会话态指标 | 指标更丰富 | 改动范围扩大,需处理跨进程/重启后一致性问题 | +**决策**: 选择方案A +**理由**: 用户当前核心诉求是“首页更像仪表盘”,稳定的趋势和资产统计优先级高于易漂移的瞬时在线数。 +**影响**: backend, frontend + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有后台控制台风格,但提升为更有层次的运营驾驶舱布局,强调信息密度和扫读效率 +- **记忆点**: 首页首屏以一排高对比统计卡片和双图表区形成明显“总览区”,不再像普通列表页 +- **参考**: 当前项目的卡片/边框/主题变量体系 + 运维控制台式信息编排 + +### 视觉要素 +- **配色**: 继续使用现有主题 token;通过不同强调色区分卡片状态,例如主色用于总量、绿色/红色用于 SSH 成功失败、蓝青色用于趋势图 +- **字体**: 沿用项目现有字体体系,避免引入与整体后台风格冲突的新字体 +- **布局**: 顶部四卡片总览,中部双栏图表,下部左侧活跃连接排行、右侧最近活动,连接列表保留为可操作区域 +- **动效**: 卡片和图表使用轻量渐进出现与 hover 反馈,避免夸张动画影响管理界面稳定感 +- **氛围**: 保留现有边框卡片语言,通过更清晰的区块分层、数据注释和图例提升专业感 + +### 技术约束 +- **可访问性**: 图表需要同时提供标题和数字文本,避免纯图形表达 +- **响应式**: 桌面端优先双栏布局,窄屏时需自然折叠为单栏 diff --git a/.helloagents/plan/202603252343_dashboard-management-cockpit/tasks.md b/.helloagents/plan/202603252343_dashboard-management-cockpit/tasks.md new file mode 100644 index 0000000..5923053 --- /dev/null +++ b/.helloagents/plan/202603252343_dashboard-management-cockpit/tasks.md @@ -0,0 +1,50 @@ +# 任务清单: dashboard-management-cockpit + +```yaml +@feature: dashboard-management-cockpit +@created: 2026-03-25 +@status: pending +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 0 | 0 | 0 | 6 | + +--- + +## 任务列表 + +### 1. 后端 Dashboard 聚合接口 + +- [ ] 1.1 在 `packages/backend/src/dashboard/` 下新增 summary 查询与 service/controller/routes,实现基于连接表、标签关联表和审计日志表的聚合统计接口 | depends_on: [] +- [ ] 1.2 在 `packages/backend/src/index.ts` 中注册 dashboard 路由,并补齐必要的后端类型定义 | depends_on: [1.1] + +### 2. 前端数据接入 + +- [ ] 2.1 在 `packages/frontend/src/types/server.types.ts` 中新增 dashboard summary 响应类型,并在 `packages/frontend/src/stores/` 中新增 dashboard store 负责获取首页聚合数据 | depends_on: [1.2] + +### 3. 仪表盘页面重构 + +- [ ] 3.1 重构 `packages/frontend/src/views/DashboardView.vue`,新增统计卡片、近 7 天趋势图、连接类型分布图和活跃连接排行,同时保留现有连接列表与最近活动区域 | depends_on: [2.1] +- [ ] 3.2 更新 `packages/frontend/src/locales/zh-CN.json`、`packages/frontend/src/locales/en-US.json`、`packages/frontend/src/locales/ja-JP.json` 的 dashboard 文案,补齐新增统计与图表标签 | depends_on: [3.1] + +### 4. 验证 + +- [ ] 4.1 执行 `packages/backend` 与 `packages/frontend` 的构建校验,确认新增 dashboard 接口和页面改造通过类型检查与打包 | depends_on: [3.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-25 23:43 | DESIGN | completed | 已确认采用“后端 summary 接口 + 前端驾驶舱重构”的实现路径,统计口径优先使用稳定数据库数据 | + +--- + +## 执行备注 + +> `create_package.py` 在当前环境未返回有效执行报告,方案包已按模板规则手工创建。开发实施阶段需同步前后端、locale 和知识库变更记录。 diff --git a/.helloagents/plan/202603252354_login-credential-management/.status.json b/.helloagents/plan/202603252354_login-credential-management/.status.json new file mode 100644 index 0000000..6f4e886 --- /dev/null +++ b/.helloagents/plan/202603252354_login-credential-management/.status.json @@ -0,0 +1 @@ +{"status":"in_progress","completed":0,"failed":0,"pending":9,"total":9,"done":0,"percent":0,"current":"DESIGN completed - package created for login credential management","updated_at":"2026-03-25 23:54:00"} diff --git a/.helloagents/plan/202603252354_login-credential-management/proposal.md b/.helloagents/plan/202603252354_login-credential-management/proposal.md new file mode 100644 index 0000000..4625b69 --- /dev/null +++ b/.helloagents/plan/202603252354_login-credential-management/proposal.md @@ -0,0 +1,206 @@ +# 变更提案: login-credential-management + +## 元信息 +```yaml +类型: 新功能 +方案类型: implementation +优先级: P1 +状态: 草稿 +创建: 2026-03-25 +``` + +--- + +## 1. 需求 + +### 背景 +当前连接的用户名、密码、认证方式和 SSH 密钥选择直接存放在连接本体中。这样虽然能满足单连接直填,但无法形成独立的登录凭证资产,也无法在多台服务器之间稳定复用同一组登录配置。用户明确要求新增独立“登录凭证管理”,同时保留原有直填方式,并支持在新增连接、编辑连接和批量编辑中一键应用已保存凭证。 + +### 目标 +- 新增统一“登录凭证管理”,支持 SSH / RDP / VNC 三类凭证的列表、新增、编辑、删除。 +- 新增连接和编辑连接时,保留“账号密码 / 密钥直填”能力,同时增加“使用已保存凭证”模式。 +- 批量编辑连接时支持一键把选中的服务器切换到某个已保存凭证。 +- 连接运行时需要优先解析“引用凭证”,未引用时继续使用连接自身保存的直填凭证。 +- 兼容已有连接数据,不强制迁移旧连接为凭证引用。 + +### 约束条件 +```yaml +时间约束: 本轮内完成数据库、后端 API、前端交互和基础验证闭环 +兼容性约束: 旧连接必须继续可编辑、可测试、可连接 +数据约束: 已保存凭证需要单独建模,不能继续依附于 connections 表临时拼装 +交互约束: 不做本地仓库/云端仓库区分,管理入口采用贴近现有连接工作流的管理面板 +安全约束: 凭证敏感字段继续复用现有 encrypt/decrypt 机制,不在连接列表接口中明文返回 +``` + +### 验收标准 +- [ ] 后端新增独立登录凭证表和 CRUD API,支持 SSH / RDP / VNC 三类凭证 +- [ ] `connections` 表新增可选凭证引用字段,连接创建、更新、测试和实际使用时都能解析已保存凭证 +- [ ] 新增连接/编辑连接时支持在“直填凭证”和“已保存凭证”之间切换,且不移除原有直填能力 +- [ ] 批量编辑连接支持一键应用某个已保存凭证 +- [ ] 旧连接在不绑定登录凭证时仍按原逻辑工作 +- [ ] 前后端至少完成可运行的构建或类型校验验证 + +--- + +## 2. 方案 + +### 技术方案 +采用“独立登录凭证实体 + 连接可选引用”的实现路径。后端新增 `login_credentials` 表和对应模块,统一存储凭证名称、协议类型、用户名、认证方式、加密后的密码/密钥等信息。`connections` 表新增 `login_credential_id` 外键,连接在保存时可以选择两种模式: + +1. 继续在连接中直填并保存用户名/密码/密钥 +2. 改为引用某个已保存凭证 + +连接运行时、测试连接和批量编辑都统一走“先看引用凭证,再回退连接自身凭证”的解析逻辑。前端在现有连接表单的认证区加入“认证来源”切换,并新增登录凭证管理面板;批量编辑弹窗补“应用已保存凭证”入口。 + +### 影响范围 +```yaml +涉及模块: + - backend: database schema/migrations, login-credentials 模块, connections 模块 + - frontend: AddConnectionForm, BatchEditConnectionForm, 连接相关 store/types, 新增登录凭证管理组件与入口 + - frontend: locales 多语言文案 + - knowledge-base: 方案包、实施日志与知识同步 +预计变更文件: 14-22 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 连接、测试连接、实际握手三条链路凭证解析不一致 | 高 | 后端提炼统一的“解析有效凭证”逻辑,禁止三处各写一套 | +| 旧连接编辑回显时,直填模式与引用模式切换导致字段覆盖 | 中 | 前端明确区分 `credential_source` 状态,提交时只发送当前模式需要的字段 | +| 批量编辑对不同类型连接应用凭证时可能出现协议不匹配 | 中 | 限制只能应用同类型凭证,后端再次校验 | +| 新增通用凭证实体后与现有 `ssh_keys` 关系重复 | 中 | 保留 `ssh_keys` 作为 SSH 私钥仓库,登录凭证通过 `ssh_key_id` 引用已保存 SSH 密钥 | + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart LR + A[AddConnectionForm / BatchEditConnectionForm] --> B[loginCredentials.store] + A --> C[connections.store] + B --> D[/api/v1/login-credentials] + C --> E[/api/v1/connections] + D --> F[login-credentials.controller] + E --> G[connections.controller] + F --> H[login-credentials.service] + G --> I[connection.service] + H --> J[(login_credentials)] + I --> J + I --> K[(connections)] + I --> L[(ssh_keys)] +``` + +### API 设计 + +#### GET /api/v1/login-credentials +- 返回所有登录凭证的安全摘要,不包含明文密码/私钥 + +#### POST /api/v1/login-credentials +- 新增登录凭证 +- 请求需包含 `type`、`name`、`username` 和对应认证字段 + +#### PUT /api/v1/login-credentials/:id +- 更新登录凭证 +- 允许局部更新;敏感字段为空时表示“不改” + +#### DELETE /api/v1/login-credentials/:id +- 删除登录凭证 +- 若有连接引用,默认将连接的 `login_credential_id` 置空,不删连接 + +#### POST /api/v1/connections/test-unsaved +- 继续保留原接口 +- 新增支持 `login_credential_id`,测试时优先使用登录凭证解析结果 + +### 数据模型 + +#### 新表 `login_credentials` +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | `INTEGER` | 主键 | +| `name` | `TEXT` | 凭证名称,前端展示用 | +| `type` | `TEXT` | `SSH` / `RDP` / `VNC` | +| `username` | `TEXT` | 登录用户名 | +| `auth_method` | `TEXT` | SSH 为 `password` / `key`,RDP/VNC 固定 `password` | +| `encrypted_password` | `TEXT NULL` | 加密后的密码 | +| `ssh_key_id` | `INTEGER NULL` | SSH 引用的密钥 ID | +| `encrypted_private_key` | `TEXT NULL` | 直接保存的 SSH 私钥 | +| `encrypted_passphrase` | `TEXT NULL` | SSH 私钥口令 | +| `notes` | `TEXT NULL` | 凭证备注 | +| `created_at` | `INTEGER` | 创建时间 | +| `updated_at` | `INTEGER` | 更新时间 | + +#### `connections` 表增量字段 +| 字段 | 类型 | 说明 | +|------|------|------| +| `login_credential_id` | `INTEGER NULL` | 引用的登录凭证 ID | + +### 核心决策 + +### login-credential-management#D001: 采用“连接可选引用凭证 + 保留直填”的双轨方案 +**日期**: 2026-03-25 +**状态**: ✅采纳 +**背景**: 用户明确要求“不要移除直填”,同时还要能选择使用已保存配置。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 连接只允许引用凭证 | 数据归一最强 | 会破坏现有直填体验,不符合用户要求 | +| B: 连接保留直填,并可选引用凭证 | 兼容旧逻辑,迁移风险低 | 数据模型和前端状态更复杂 | +**决策**: 选择方案 B +**理由**: 这是唯一满足用户要求且兼容现有连接资产的路径。 +**影响**: backend, frontend + +### login-credential-management#D002: SSH 登录凭证继续复用 `ssh_keys` 作为底层密钥仓库 +**日期**: 2026-03-25 +**状态**: ✅采纳 +**背景**: 仓库已有完整 SSH 密钥管理链路,不应为了通用登录凭证重复造一套密钥管理。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 登录凭证内部直接复用 `ssh_key_id` | 复用现有密钥管理和解密逻辑 | 模型上需要解释“凭证”和“密钥”两层关系 | +| B: 在登录凭证里完全复制私钥管理 | 数据模型更扁平 | 重复实现、风险更高 | +**决策**: 选择方案 A +**理由**: 能最大程度复用现有 SSH 密钥能力,减少改动面和安全风险。 +**影响**: backend, frontend + +--- + +## 4. 核心场景 + +### 场景: 新增连接时选择已保存凭证 +**模块**: frontend / backend +**条件**: 用户打开新增连接弹窗并选择“使用已保存凭证”。 +**行为**: 表单展示可筛选的凭证下拉或管理入口,用户选择后仅保存 `login_credential_id`。 +**结果**: 新连接不再重复保存一套账号密码,而是复用独立登录凭证。 + +### 场景: 批量编辑时一键应用凭证 +**模块**: frontend / backend +**条件**: 用户在连接列表中多选多台服务器并打开批量编辑。 +**行为**: 批量编辑弹窗允许选择某个已保存凭证,提交后把选中连接统一切换到该凭证。 +**结果**: 多台同账号主机可快速改绑同一登录凭证。 + +### 场景: 旧连接继续使用直填凭证 +**模块**: backend +**条件**: 连接未绑定 `login_credential_id`。 +**行为**: 测试连接、建立连接、编辑回显时继续走旧字段。 +**结果**: 老数据零强制迁移,升级后仍可工作。 + +--- + +## 5. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有深色控制台视觉,新增一个更聚焦“资产管理”的右侧管理面板 +- **记忆点**: 在连接表单中将“认证信息”升级为“认证来源 + 凭证管理”的组合区域,形成明显的操作跃迁 +- **交互立场**: 新能力应嵌在现有连接工作流内,而不是把用户赶去陌生页面 + +### 视觉要素 +- **配色**: 继续使用现有 `background / border / primary / text-secondary` 变量,凭证列表用绿色锁图标和次级文字区分状态 +- **布局**: 连接弹窗中认证区采用上下分段,先选“直填 / 已保存凭证”,再展示对应表单;登录凭证管理使用右侧抽屉或侧面板布局 +- **动效**: 切换认证来源时做轻量内容切换,不引入重动画;凭证管理面板滑入滑出,保持与现有 modal 体系兼容 +- **氛围**: 保持专业、克制,不做多余装饰;重点在于“操作密度”和“字段分组清晰” + +### 技术约束 +- **响应式**: 桌面优先,窄屏时管理面板应退化为全屏弹层 +- **可访问性**: 切换认证来源后应保留明确字段标题和禁用态反馈 + diff --git a/.helloagents/plan/202603252354_login-credential-management/tasks.md b/.helloagents/plan/202603252354_login-credential-management/tasks.md new file mode 100644 index 0000000..369aa67 --- /dev/null +++ b/.helloagents/plan/202603252354_login-credential-management/tasks.md @@ -0,0 +1,58 @@ +# 任务清单: login-credential-management + +```yaml +@feature: login-credential-management +@created: 2026-03-25 +@status: pending +@mode: R3 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 0 | 0 | 0 | 9 | + +--- + +## 任务列表 + +### 1. 后端登录凭证模型与迁移 + +- [ ] 1.1 在 `packages/backend/src/database/schema.ts`、`schema.registry.ts`、`migrations.ts` 中新增 `login_credentials` 表定义和 `connections.login_credential_id` 迁移 | depends_on: [] +- [ ] 1.2 新增 `packages/backend/src/login-credentials/` 模块,实现 repository/service/controller/routes 和类型定义,提供凭证 CRUD 接口 | depends_on: [1.1] + +### 2. 连接模块凭证引用支持 + +- [ ] 2.1 在 `packages/backend/src/types/connection.types.ts`、`packages/backend/src/connections/connection.service.ts`、`packages/backend/src/connections/connections.controller.ts` 中新增 `login_credential_id` 与统一凭证解析逻辑,覆盖创建、更新、测试和读取回显 | depends_on: [1.2] +- [ ] 2.2 在 `packages/backend/src/index.ts` 注册登录凭证路由,并确保连接测试与实际连接链路复用新的凭证解析逻辑 | depends_on: [2.1] + +### 3. 前端登录凭证管理与表单接入 + +- [ ] 3.1 新增前端登录凭证类型、store 和管理组件,提供列表、新增、编辑、删除交互入口 | depends_on: [2.2] +- [ ] 3.2 改造 `packages/frontend/src/components/AddConnectionFormAuth.vue`、`AddConnectionForm.vue`、`packages/frontend/src/composables/useAddConnectionForm.ts`,支持“直填凭证 / 已保存凭证”双模式 | depends_on: [3.1] + +### 4. 批量编辑与文案同步 + +- [ ] 4.1 改造 `packages/frontend/src/components/BatchEditConnectionForm.vue`,支持批量应用已保存登录凭证并做类型校验 | depends_on: [3.1] +- [ ] 4.2 更新连接相关 store、页面入口与 `packages/frontend/src/locales/zh-CN.json`、`packages/frontend/src/locales/en-US.json`、`packages/frontend/src/locales/ja-JP.json` 文案 | depends_on: [3.2, 4.1] + +### 5. 验证与知识同步 + +- [ ] 5.1 执行前后端构建或类型校验,验证登录凭证管理、连接表单和批量编辑改造可通过基础检查 | depends_on: [4.2] +- [ ] 5.2 同步 `.helloagents` 知识库与 CHANGELOG,记录本次“登录凭证管理”实现方案和落地结果 | depends_on: [5.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-25 23:54 | DESIGN | completed | 已确认采用“独立登录凭证实体 + 连接可选引用 + 保留直填”的实现路径 | + +--- + +## 执行备注 + +> 当前环境缺少可用 Python 运行时,`create_package.py` 未能执行;本方案包已按模板规范手工创建。开发阶段需优先保证统一凭证解析逻辑,避免连接创建、测试和运行时三套行为分叉。 + diff --git a/packages/backend/src/dashboard/dashboard.controller.ts b/packages/backend/src/dashboard/dashboard.controller.ts new file mode 100644 index 0000000..8536984 --- /dev/null +++ b/packages/backend/src/dashboard/dashboard.controller.ts @@ -0,0 +1,19 @@ +import { Request, Response } from 'express'; +import { DashboardService } from './dashboard.service'; + +const dashboardService = new DashboardService(); + +export class DashboardController { + async getSummary(req: Request, res: Response): Promise { + try { + const summary = await dashboardService.getSummary(); + res.status(200).json(summary); + } catch (error: any) { + console.error('[DashboardController] 获取仪表盘统计失败:', error); + res.status(500).json({ + message: '获取仪表盘统计失败', + error: error.message, + }); + } + } +} diff --git a/packages/backend/src/dashboard/dashboard.repository.ts b/packages/backend/src/dashboard/dashboard.repository.ts new file mode 100644 index 0000000..2c655bf --- /dev/null +++ b/packages/backend/src/dashboard/dashboard.repository.ts @@ -0,0 +1,247 @@ +import { allDb, getDb, getDbInstance } from '../database/connection'; +import type { + DashboardActionBreakdownItem, + DashboardActivityTrendPoint, + DashboardCountByType, + DashboardSummary, + DashboardTopConnection, +} from './dashboard.types'; +import type { AuditLogActionType } from '../types/audit.types'; + +const DAY_IN_SECONDS = 24 * 60 * 60; +const DASHBOARD_WINDOW_DAYS = 7; +const SSH_SUCCESS_ACTION = 'SSH_CONNECT_SUCCESS'; +const SSH_FAILURE_ACTIONS: AuditLogActionType[] = ['SSH_CONNECT_FAILURE', 'SSH_SHELL_FAILURE']; +const TOP_CONNECTION_ACTIONS: AuditLogActionType[] = [ + SSH_SUCCESS_ACTION, + ...SSH_FAILURE_ACTIONS, +]; +const ACTION_BREAKDOWN_LIMIT = 6; +const TOP_CONNECTION_LIMIT = 5; + +interface CountRow { + total: number; +} + +interface CountByLabelRow { + label: string; + count: number; +} + +interface TrendRow { + date: string; + count: number; +} + +interface ConnectionLookupRow { + id: number; + name: string | null; + host: string; +} + +interface AuditDetailRow { + timestamp: number; + details: string | null; +} + +interface ParsedAuditDetails { + connectionId?: number; + connectionName?: string; +} + +const buildDateWindow = (days: number): string[] => { + const result: string[] = []; + const now = new Date(); + + for (let index = days - 1; index >= 0; index -= 1) { + const date = new Date(now); + date.setUTCDate(date.getUTCDate() - index); + result.push(date.toISOString().slice(0, 10)); + } + + return result; +}; + +const safeParseAuditDetails = (raw: string | null): ParsedAuditDetails | null => { + if (!raw) { + return null; + } + + try { + const parsed = JSON.parse(raw) as ParsedAuditDetails; + return parsed && typeof parsed === 'object' ? parsed : null; + } catch (error) { + return null; + } +}; + +export const getDashboardSummary = async (): Promise => { + const db = await getDbInstance(); + const now = Math.floor(Date.now() / 1000); + const since7d = now - (DASHBOARD_WINDOW_DAYS - 1) * DAY_IN_SECONDS; + const since24h = now - DAY_IN_SECONDS; + + const [ + totalConnectionsRow, + activeConnectionsRow, + taggedConnectionsRow, + auditLogsRow, + sshOutcomeRows, + connectionTypeRows, + actionBreakdownRows, + trendRows, + topConnectionLogRows, + connectionLookupRows, + ] = await Promise.all([ + getDb(db, 'SELECT COUNT(*) as total FROM connections'), + getDb(db, 'SELECT COUNT(*) as total FROM connections WHERE last_connected_at >= ?', [since7d]), + getDb(db, 'SELECT COUNT(DISTINCT connection_id) as total FROM connection_tags'), + getDb(db, 'SELECT COUNT(*) as total FROM audit_logs'), + allDb( + db, + ` + SELECT action_type as label, COUNT(*) as count + FROM audit_logs + WHERE timestamp >= ? + AND action_type IN (?, ?, ?) + GROUP BY action_type + `, + [since24h, SSH_SUCCESS_ACTION, ...SSH_FAILURE_ACTIONS], + ), + allDb( + db, + ` + SELECT type as label, COUNT(*) as count + FROM connections + GROUP BY type + ORDER BY count DESC, type ASC + `, + ), + allDb( + db, + ` + SELECT action_type as label, COUNT(*) as count + FROM audit_logs + WHERE timestamp >= ? + GROUP BY action_type + ORDER BY count DESC, action_type ASC + LIMIT ? + `, + [since7d, ACTION_BREAKDOWN_LIMIT], + ), + allDb( + db, + ` + SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch') as date, COUNT(*) as count + FROM audit_logs + WHERE timestamp >= ? + GROUP BY strftime('%Y-%m-%d', timestamp, 'unixepoch') + ORDER BY date ASC + `, + [since7d], + ), + allDb( + db, + ` + SELECT timestamp, details + FROM audit_logs + WHERE timestamp >= ? + AND action_type IN (?, ?, ?) + AND details IS NOT NULL + ORDER BY timestamp DESC + `, + [since7d, ...TOP_CONNECTION_ACTIONS], + ), + allDb( + db, + 'SELECT id, name, host FROM connections', + ), + ]); + + const sshOutcomesMap = new Map( + sshOutcomeRows.map((row) => [row.label, row.count]), + ); + + const connectionTypeMap = new Map( + connectionTypeRows.map((row) => [row.label, row.count]), + ); + const connectionTypes: DashboardCountByType[] = ['SSH', 'RDP', 'VNC'].map((type) => ({ + label: type, + count: connectionTypeMap.get(type) ?? 0, + })); + + const actionBreakdown7d: DashboardActionBreakdownItem[] = actionBreakdownRows.map((row) => ({ + actionType: (row.label as AuditLogActionType) ?? 'OTHER', + count: row.count, + })); + + const trendMap = new Map( + trendRows.map((row) => [row.date, row.count]), + ); + const activityTrend7d: DashboardActivityTrendPoint[] = buildDateWindow(DASHBOARD_WINDOW_DAYS).map((date) => ({ + date, + count: trendMap.get(date) ?? 0, + })); + + const connectionLookup = new Map( + connectionLookupRows.map((row) => [row.id, row]), + ); + const topConnectionCounts = new Map(); + + for (const row of topConnectionLogRows) { + const details = safeParseAuditDetails(row.details); + const connectionId = details?.connectionId; + if (typeof connectionId !== 'number' || Number.isNaN(connectionId)) { + continue; + } + + const lookup = connectionLookup.get(connectionId); + const connectionName = details?.connectionName || lookup?.name || `#${connectionId}`; + const host = lookup?.host || '-'; + const existing = topConnectionCounts.get(connectionId); + + if (existing) { + existing.count += 1; + existing.lastSeenAt = Math.max(existing.lastSeenAt, row.timestamp); + continue; + } + + topConnectionCounts.set(connectionId, { + connectionId, + connectionName, + host, + count: 1, + lastSeenAt: row.timestamp, + }); + } + + const topConnections = Array.from(topConnectionCounts.values()) + .sort((left, right) => { + if (right.count !== left.count) { + return right.count - left.count; + } + + return right.lastSeenAt - left.lastSeenAt; + }) + .slice(0, TOP_CONNECTION_LIMIT); + + return { + totals: { + connections: totalConnectionsRow?.total ?? 0, + activeConnections7d: activeConnectionsRow?.total ?? 0, + taggedConnections: taggedConnectionsRow?.total ?? 0, + auditLogs: auditLogsRow?.total ?? 0, + }, + sshOutcomes24h: { + success: sshOutcomesMap.get(SSH_SUCCESS_ACTION) ?? 0, + failure: SSH_FAILURE_ACTIONS.reduce( + (total, actionType) => total + (sshOutcomesMap.get(actionType) ?? 0), + 0, + ), + }, + connectionTypes, + actionBreakdown7d, + activityTrend7d, + topConnections, + }; +}; diff --git a/packages/backend/src/dashboard/dashboard.routes.ts b/packages/backend/src/dashboard/dashboard.routes.ts new file mode 100644 index 0000000..42cd18d --- /dev/null +++ b/packages/backend/src/dashboard/dashboard.routes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { DashboardController } from './dashboard.controller'; +import { isAuthenticated } from '../auth/auth.middleware'; + +const router = Router(); +const dashboardController = new DashboardController(); + +router.use(isAuthenticated); + +router.get('/summary', dashboardController.getSummary); + +export default router; diff --git a/packages/backend/src/dashboard/dashboard.service.ts b/packages/backend/src/dashboard/dashboard.service.ts new file mode 100644 index 0000000..8d95c8d --- /dev/null +++ b/packages/backend/src/dashboard/dashboard.service.ts @@ -0,0 +1,8 @@ +import { getDashboardSummary } from './dashboard.repository'; +import type { DashboardSummary } from './dashboard.types'; + +export class DashboardService { + async getSummary(): Promise { + return getDashboardSummary(); + } +} diff --git a/packages/backend/src/dashboard/dashboard.types.ts b/packages/backend/src/dashboard/dashboard.types.ts new file mode 100644 index 0000000..8f99859 --- /dev/null +++ b/packages/backend/src/dashboard/dashboard.types.ts @@ -0,0 +1,45 @@ +import type { AuditLogActionType } from '../types/audit.types'; + +export interface DashboardTotals { + connections: number; + activeConnections7d: number; + taggedConnections: number; + auditLogs: number; +} + +export interface DashboardSshOutcomes24h { + success: number; + failure: number; +} + +export interface DashboardCountByType { + label: string; + count: number; +} + +export interface DashboardActivityTrendPoint { + date: string; + count: number; +} + +export interface DashboardTopConnection { + connectionId: number; + connectionName: string; + host: string; + count: number; + lastSeenAt: number; +} + +export interface DashboardActionBreakdownItem { + actionType: AuditLogActionType | 'OTHER'; + count: number; +} + +export interface DashboardSummary { + totals: DashboardTotals; + sshOutcomes24h: DashboardSshOutcomes24h; + connectionTypes: DashboardCountByType[]; + actionBreakdown7d: DashboardActionBreakdownItem[]; + activityTrend7d: DashboardActivityTrendPoint[]; + topConnections: DashboardTopConnection[]; +} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index cb86ef9..d9970ad 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -56,6 +56,7 @@ import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes'; import { transfersRoutes } from './transfers/transfers.routes'; import pathHistoryRoutes from './path-history/path-history.routes'; import favoritePathsRouter from './favorite-paths/favorite-paths.routes'; +import dashboardRoutes from './dashboard/dashboard.routes'; import { initializeWebSocket } from './websocket'; import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; @@ -263,6 +264,7 @@ const startServer = () => { app.use('/api/v1/transfers', transfersRoutes()); app.use('/api/v1/path-history', pathHistoryRoutes); app.use('/api/v1/favorite-paths', favoritePathsRouter); + app.use('/api/v1/dashboard', dashboardRoutes); // 状态检查接口 app.get('/api/v1/status', (req: Request, res: Response) => { diff --git a/packages/frontend/src/components/CommandInputBar.vue b/packages/frontend/src/components/CommandInputBar.vue index ffd68e6..13a92f8 100644 --- a/packages/frontend/src/components/CommandInputBar.vue +++ b/packages/frontend/src/components/CommandInputBar.vue @@ -68,6 +68,33 @@ const currentSessionCommandInput = computed({ } }); +const maxCommandInputRows = 6; + +const syncCommandInputHeight = () => { + const textarea = commandInputRef.value; + if (!textarea) { + return; + } + + textarea.style.height = 'auto'; + + const computedStyle = window.getComputedStyle(textarea); + const lineHeight = Number.parseFloat(computedStyle.lineHeight) || 20; + const verticalPadding = Number.parseFloat(computedStyle.paddingTop) + Number.parseFloat(computedStyle.paddingBottom); + const borderWidth = Number.parseFloat(computedStyle.borderTopWidth) + Number.parseFloat(computedStyle.borderBottomWidth); + const maxHeight = lineHeight * maxCommandInputRows + verticalPadding + borderWidth; + const nextHeight = Math.min(textarea.scrollHeight, maxHeight); + + textarea.style.height = `${nextHeight}px`; + textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'; +}; + +const scheduleCommandInputHeightSync = () => { + void nextTick(() => { + syncCommandInputHeight(); + }); +}; + const sendCommand = () => { const command = currentSessionCommandInput.value; // 使用计算属性获取值 console.log(`[CommandInputBar] Sending command: ${command || ''} `); @@ -125,17 +152,24 @@ watch(currentSessionCommandInput, (newValue) => { // 监听计算属性 commandHistoryStore.setSearchTerm(newValue); } // If target is 'none', do nothing + scheduleCommandInputHeightSync(); }); // 可以在这里添加一个 ref 用于聚焦搜索框 const searchInputRef = ref(null); -const commandInputRef = ref(null); // Ref for command input +const commandInputRef = ref(null); // Ref for command input + +watch(activeSessionId, () => { + scheduleCommandInputHeightSync(); +}); // Removed debug computed property const handleCommandInputKeydown = (event: KeyboardEvent) => { - // --- 移动到外部:优先处理 Enter 键执行选中项 --- - if (!event.altKey && event.key === 'Enter') { + const isSendShortcut = event.key === 'Enter' && event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey; + + // --- 移动到外部:优先处理发送快捷键执行选中项 --- + if (isSendShortcut) { const target = commandInputSyncTarget.value; let selectedCommand: string | undefined; let resetSelection: (() => void) | undefined; @@ -156,7 +190,7 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => { if (selectedCommand !== undefined) { event.preventDefault(); - console.log(`[CommandInputBar] Enter detected with selection. Sending selected command: ${selectedCommand}`); + console.log(`[CommandInputBar] Send shortcut detected with selection. Sending selected command: ${selectedCommand}`); emitWorkspaceEvent('terminal:sendCommand', { command: selectedCommand }); // 发送选中命令 if (activeSessionId.value) { updateSessionCommandInput(activeSessionId.value, ''); // 清空输入框 @@ -164,9 +198,9 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => { resetSelection?.(); // 重置列表选中状态 return; // 阻止后续的 Enter 处理 } - // 如果没有选中项,则继续执行下面的默认 Enter 逻辑 + // 如果没有选中项,则继续执行下面的默认发送逻辑 } - // --- 结束:优先处理 Enter 键执行选中项 --- + // --- 结束:优先处理发送快捷键执行选中项 --- if (event.ctrlKey && event.key === 'f') { event.preventDefault(); // 阻止浏览器默认的查找行为 @@ -197,8 +231,8 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => { event.preventDefault(); console.log('[CommandInputBar] Ctrl+C detected with empty input. Sending SIGINT.'); emitWorkspaceEvent('terminal:sendCommand', { command: '\x03' }); // Send ETX character (Ctrl+C) - } else if (!event.altKey && event.key === 'Enter') { - // Handle regular Enter key press - send current input (empty or not) + } else if (isSendShortcut) { + // Handle Ctrl+Shift+Enter - send current input (empty or not) event.preventDefault(); // Prevent default if needed, e.g., form submission sendCommand(); // Call the existing sendCommand function } else { @@ -271,6 +305,7 @@ let unregisterTerminalSearchFocus: (() => void) | null = null; onMounted(() => { unregisterCommandInputFocus = focusSwitcherStore.registerFocusAction('commandInput', focusCommandInput); unregisterTerminalSearchFocus = focusSwitcherStore.registerFocusAction('terminalSearch', focusSearchInput); + scheduleCommandInputHeightSync(); }); onBeforeUnmount(() => { @@ -360,17 +395,18 @@ const handleQuickCommandExecute = (command: string) => { -