# 快捷指令设计方案 ## 1. 概述 在桌面端对话输入框中实现快捷指令功能,通过 `/` 符号触发,展开一个可筛选的指令列表,包含 Skill 和 Command 两种类型。选中后以 tag 标签形式插入输入框,发送时后端根据标记格式自动解析并注入实际内容。 ### 1.1 术语定义 | 术语 | 说明 | |------|------| | **Skill** | 已有的技能知识文档,由 SKILL.md 文件定义,分为系统 Skill(Agent 关联)和用户 Skill(个人上传) | | **Command** | 新增概念,用户可自定义的提示词模板,存储在数据库中,选中后以 tag 形式插入输入框 | ## 2. 数据模型 ### 2.1 Command 表 新增数据库表 `user_command`,通过 MyBatis-Plus 管理: ```sql CREATE TABLE user_command ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '所属用户 ID', name VARCHAR(100) NOT NULL COMMENT '指令名称,同用户下唯一', content TEXT NOT NULL COMMENT '指令内容(提示词模板)', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_name (user_id, name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户快捷指令'; ``` **字段说明**: | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 自增主键 | | `user_id` | BIGINT | 所属用户 | | `name` | VARCHAR(100) | 指令名称,同用户下唯一 | | `content` | TEXT | 指令内容,即提示词模板正文 | ### 2.2 快捷指令列表项(统一视图) 前端展示时,Skill 和 Command 统一为 `QuickCommand` 类型: ```typescript interface QuickCommand { type: 'skill' | 'command' // 类型标识 name: string // 唯一名称 description: string // 一行描述(Skill 来自 SKILL.md frontmatter,Command 无此字段,留空) } ``` ### 2.3 输入框中的标记格式 两种指令在输入框中以不同颜色的 tag 展示,在文本传输时使用不同的标记格式: | 类型 | 传输格式 | 示例 | tag 颜色 | |------|----------|------|----------| | Skill | `${skill_name}$` | `${code-review}$` | 蓝色 | | Command | `#{command_name}#` | `#{commit}#` | 紫色 | 用户在输入框中看到的是带颜色的 tag 标签(显示 name),但底层 textarea 中存储的是纯文本标记格式。 ## 3. 后端 API ### 3.1 Command CRUD 新增 `UserCommandController`,遵循项目既有 controller → service → entity → mapper 分层: | 方法 | 路径 | 说明 | |------|------|------| | GET | `/v1/user-commands` | 列出当前用户的所有 Command | | GET | `/v1/user-commands/{name}` | 获取 Command 详情(含 content) | | POST | `/v1/user-commands` | 创建 Command | | PUT | `/v1/user-commands/{name}` | 更新 Command | | DELETE | `/v1/user-commands/{name}` | 删除 Command | **POST/PUT 请求体**: ```json { "name": "commit", "content": "请根据以下 diff 内容,按照 Conventional Commits 规范生成 commit message。" } ``` **GET `/v1/user-commands` 响应**: ```json { "code": 0, "data": [ { "name": "commit", "content": "请根据以下 diff 内容,按照 Conventional Commits 规范生成 commit message。" }, { "name": "code-review", "content": "请按照以下清单审查代码:..." } ] } ``` ### 3.2 快捷指令列表聚合 API 新增聚合接口,一次返回当前 Agent 上下文中可用的所有快捷指令: **GET `/v1/quick-commands?agentId={agentId}`** 响应: ```json { "code": 0, "data": { "skills": [ { "type": "skill", "name": "code-review", "description": "代码审查规范" }, { "type": "skill", "name": "data-export-spec", "description": "数据导出中心接入规范" } ], "commands": [ { "type": "command", "name": "commit", "description": "" }, { "type": "command", "name": "pr", "description": "" } ] } } ``` **数据来源**: - `skills`:当前 Agent 关联的系统 Skill + 当前用户的所有个人 Skill(合并去重,用户 Skill 优先) - `commands`:当前用户的所有 Command(从 `user_command` 表查询) ## 4. 消息发送与解析 ### 4.1 前端发送 用户输入框中的内容包含纯文本和标记格式的混合文本,例如: ``` 帮我审查这段代码 ${code-review}$,然后按 #{commit}# 的格式提交 ``` 前端直接将 textarea 中的文本作为消息内容通过 WebSocket 发送,不做额外处理。 ### 4.2 后端解析与替换 后端接收到消息后,由 `PromptEngine` 负责解析标记格式并在用户消息上原地替换: 1. **Skill 替换**:匹配 `${...}$` 格式,验证 Skill 存在后,将标记原地替换为 `/skill_name` 字符串 2. **Command 替换**:匹配 `#{...}#` 格式,从 `user_command` 表查询对应 Command 的 content,将标记原地替换为 content 正文 **解析规则**: - 正则:`\$\{([^}]+)\}\$` 匹配 Skill,`\#\{([^}]+)\}\#` 匹配 Command - 同一消息中可包含多个标记,逐一替换 - 若标记对应的 Skill/Command 不存在,保留原始标记文本并在日志中记录警告 - 替换直接作用于用户消息本身(`messages.set(i, ...)`),不生成额外的 system message ### 4.3 前端回显 历史消息回显时,前端对消息文本进行反向解析: 1. 匹配 `${...}$` 格式,渲染为蓝色 tag 标签(显示 Skill name) 2. 匹配 `#{...}#` 格式,渲染为紫色 tag 标签(显示 Command name) 回显仅做视觉渲染,标记文本本身保留在 DOM 的 data 属性中以支持复制粘贴。 ## 5. 前端设计 ### 5.1 数据获取 在 ChatInput 组件中,当输入框获得焦点且尚未加载过快捷指令数据时,调用 `GET /v1/quick-commands` 获取列表,结果缓存在组件内部的 `ref` 中(同一次会话内不重复请求)。 ### 5.2 触发与筛选逻辑 在 ChatInput 的 textarea 上监听 `input` 事件,检测输入内容: 1. **触发条件**:用户输入 `/` 且 `/` 前方为空白字符或位于行首 2. **筛选规则**:提取 `/` 之后到光标位置的文本作为关键词,对所有 QuickCommand 按 `name` 进行前缀匹配(大小写不敏感) 3. **关闭条件**: - 用户按下 `Escape` 键 - 用户删除了 `/` 符号 - 用户选择了某条指令 - 用户点击列表外部区域 - 输入内容中出现空格后跟非筛选内容(光标移出指令范围) ### 5.3 UI 组件 #### 5.3.1 指令面板(QuickCommandPanel) 新建组件 `src/components/chat/QuickCommandPanel.vue`,作为 ChatInput 的子组件。 **布局**: - 定位在 textarea 上方(空间不足时改为下方),左对齐 - 最大高度 320px,超出时内部滚动 - 宽度与 textarea 一致 **内容结构**: ``` ┌─────────────────────────────────────┐ │ Skills │ │ ┌─────────────────────────────────┐│ │ │ code-review 代码审查规范 ││ │ │ data-export 数据导出规范 ││ │ └─────────────────────────────────┘│ │ Commands │ │ ┌─────────────────────────────────┐│ │ │ commit ││ │ │ pr ││ │ └─────────────────────────────────┘│ └─────────────────────────────────────┘ ``` **交互**: - 键盘上下箭头移动选中项(跨分组连续移动) - `Enter` 键选中当前高亮项 - 鼠标悬停高亮,点击选中 - 选中项始终滚动到可视区域内 **空状态**:无匹配结果时显示"未找到匹配的指令"。 #### 5.3.2 定位方式 使用 CSS `position: absolute` 相对于 ChatInput 容器定位。面板固定在 textarea 顶部上方(`bottom: 100%`),不跟随光标位置移动。 ### 5.4 选中后的行为 用户选中某条快捷指令后: 1. **清空触发文本**:删除从 `/` 到光标位置的触发文本(含 `/` 符号本身) 2. **插入标记**:根据类型将对应格式的标记文本插入到光标位置 - Skill:`${skillName}$` - Command:`#{commandName}#` 3. **渲染 tag**:插入标记后,将其渲染为输入框内的可视 tag 标签 - Skill tag:蓝色背景,显示 name - Command tag:紫色背景,显示 name 4. **关闭面板** 5. **保持焦点**在 textarea 上 ### 5.5 输入框中 tag 的渲染方案 采用 textarea + overlay 叠加方案: - 底层 textarea 负责实际文本编辑和光标管理 - 上层放置一个与 textarea 完全重叠的 div(pointer-events: none),负责将标记文本渲染为可视 tag - textarea 自身设为透明文本(`color: transparent`),用户看到的是 overlay 层的渲染结果 - 用户编辑时,textarea 中操作的是纯文本标记格式(`${name}$` / `#{name}#`),overlay 同步更新渲染 ### 5.6 历史消息中的 tag 渲染 消息气泡组件中,对 `content` 文本做正向解析,将标记格式替换为对应的 tag 组件: ```vue {{ segment.content }} ``` `QuickCommandTag` 组件接收 `type`(skill/command)和 `name`,按类型渲染为不同颜色的内联标签。 ### 5.7 ChatInput 改动 修改文件:`src/components/chat/ChatInput.vue` **新增内部状态**: ```typescript const quickCommands = ref<{ skills: QuickCommand[], commands: QuickCommand[] }>({ skills: [], commands: [] }) const panelVisible = ref(false) const panelFilter = ref('') const selectedIndex = ref(0) const commandsLoaded = ref(false) ``` **事件处理**: 在 textarea 的 `input` 事件处理中增加 `/` 检测逻辑。在 `keydown` 事件处理中增加面板打开时的上下箭头、Enter、Escape 处理。 ### 5.8 指令管理抽屉(CommandDrawer) 参照 SkillDrawer 的模式,新增指令管理入口,用于用户创建、查看、编辑和删除个人指令。 #### 5.8.1 Composable 新建 `src/composables/useCommandDrawer.ts`,与 `useSkillDrawer` 结构一致: ```typescript const visible = ref(false) export function useCommandDrawer() { function open() { visible.value = true } function close() { visible.value = false } function toggle() { visible.value = !visible.value } return { visible, open, close, toggle } } ``` #### 5.8.2 TopNav 入口 在 TopNav 右侧工具栏中,于「技能」按钮旁新增「指令」按钮: ``` [终端] [技能] [指令] [主题切换] [用户头像] ``` 点击调用 `toggleCommandDrawer()`,图标使用闪电/命令相关 SVG。 #### 5.8.3 CommandDrawer 组件 新建 `src/components/command/CommandDrawer.vue`,使用 `` 实现。 **布局**: ``` ┌──────────────────────────────────────┐ │ 我的指令 │ │ │ │ 创建和管理个人快捷指令。 │ │ │ │ [+ 新建指令] │ │ │ │ ┌──────────────────────────────────┐│ │ │ commit [编辑][删]│ │ │ Conventional Commits ... ││ │ ├──────────────────────────────────┤│ │ │ pr [编辑][删]││ │ │ 生成 PR 描述 ... ││ │ └──────────────────────────────────┘│ └──────────────────────────────────────┘ ``` **功能说明**: | 操作 | 说明 | |------|------| | 列表展示 | 打开抽屉时调用 `GET /v1/user-commands` 加载列表,展示 name 和 content 前 100 字符预览 | | 新建指令 | 点击「新建指令」按钮,弹出表单对话框(name 输入框 + content 多行文本框),提交调用 `POST /v1/user-commands` | | 编辑指令 | 点击卡片上的编辑按钮,弹出表单对话框(name 只读 + content 可编辑),提交调用 `PUT /v1/user-commands/{name}` | | 删除指令 | 点击删除按钮,二次确认后调用 `DELETE /v1/user-commands/{name}` | **表单对话框**:使用 ``,内含 ``,字段: - `name`:``,新建时可编辑,编辑时只读 - `content`:``,必填 ## 6. 涉及文件 ### 6.1 新增文件 | 文件 | 说明 | |------|------| | `backend/.../command/entity/UserCommand.java` | Command 实体类 | | `backend/.../command/mapper/UserCommandMapper.java` | MyBatis-Plus Mapper | | `backend/.../command/service/UserCommandService.java` | Command 业务逻辑 | | `backend/.../command/controller/UserCommandController.java` | Command CRUD API | | `backend/.../command/controller/QuickCommandController.java` | 聚合查询 API | | `backend/.../resources/db/migration/V039__add_user_command.sql` | 建表迁移 | | `desktop/src/components/chat/QuickCommandPanel.vue` | 快捷指令面板组件 | | `desktop/src/components/chat/QuickCommandTag.vue` | 指令 tag 渲染组件 | | `desktop/src/components/command/CommandDrawer.vue` | 指令管理抽屉组件 | | `desktop/src/composables/useCommandDrawer.ts` | 指令抽屉可见性 composable | | `desktop/src/types/quick-command.ts` | QuickCommand 类型定义 | | `desktop/src/utils/quick-command-parser.ts` | 标记格式解析工具 | ### 6.2 修改文件 | 文件 | 改动 | |------|------| | `desktop/src/components/chat/ChatInput.vue` | 增加 `/` 检测、面板渲染、tag overlay、选中插入逻辑 | | `desktop/src/components/common/TopNav.vue` | 增加「指令」入口按钮,引入 useCommandDrawer | | `backend/.../harness/core/PromptEngine.java` | 增加消息中 `${...}$` 和 `#{...}#` 的解析与原地替换 | | `backend/.../skill/service/SkillLoader.java` | 供聚合 API 和 PromptEngine 查询系统 Skill | | `backend/.../skill/service/UserSkillService.java` | 供聚合 API 和 PromptEngine 查询用户 Skill | ## 7. Skill 与 Command 的差异对比 | 维度 | Skill | Command | |------|-------|---------| | 数据来源 | 文件系统(SKILL.md) | 数据库表(user_command) | | 包含字段 | name, description, body | name, content | | 传输标记格式 | `${skill_name}$` | `#{command_name}#` | | tag 颜色 | 蓝色 | 紫色 | | 后端替换行为 | 验证 Skill 存在后,替换标记为 `/skill_name` | 从 user_command 表获取 content,替换标记为 content 正文 | | 作用域 | 系统 Skill 按 Agent 关联过滤,用户 Skill 全局可用 | 仅当前用户可用 |