feat:添加文件路径标签显示和rules需求文档
This commit is contained in:
161
docs/personal-rules-mvp-requirements.md
Normal file
161
docs/personal-rules-mvp-requirements.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# 个人规则功能需求文档(方案 A:本地 `.md` 注入)
|
||||||
|
|
||||||
|
## 1. 文档目标
|
||||||
|
|
||||||
|
在不改动现有核心对话模式的前提下,实现“个人规则(Personal Rules)”能力:
|
||||||
|
用户可在插件内维护个人规则文本,插件保存到本地 `.md` 文件;每次发起对话时自动读取并随请求传递给后端,由后端注入模型上下文,影响回答风格与行为。
|
||||||
|
|
||||||
|
## 2. 范围定义
|
||||||
|
|
||||||
|
### 2.1 本期范围(MVP)
|
||||||
|
|
||||||
|
1. 支持用户编辑、保存、启用/停用个人规则。
|
||||||
|
2. 本地落盘为 `.md` 文件。
|
||||||
|
3. 发消息时自动加载规则并传给后端。
|
||||||
|
4. 后端接收结构化字段并注入提示词。
|
||||||
|
5. 基础异常处理和可观测提示。
|
||||||
|
|
||||||
|
### 2.2 非本期范围
|
||||||
|
|
||||||
|
1. 云端同步、多设备同步。
|
||||||
|
2. 规则版本历史/回滚。
|
||||||
|
3. 多规则集合管理(仅单份个人规则文本)。
|
||||||
|
4. 团队共享规则。
|
||||||
|
|
||||||
|
## 3. 术语与核心概念
|
||||||
|
|
||||||
|
1. `Personal Rules`:用户个人偏好与约束文本。
|
||||||
|
2. `Rules File`:本地规则文件,Markdown 格式。
|
||||||
|
3. `Rules Enabled`:规则开关;关闭时不注入。
|
||||||
|
4. `Rules Injection`:请求时将规则传后端并参与模型上下文构建。
|
||||||
|
|
||||||
|
## 4. 用户故事
|
||||||
|
|
||||||
|
1. 作为用户,我希望在插件里写下“回答风格、代码习惯、语言偏好”等个人规则。
|
||||||
|
2. 作为用户,我希望规则保存在本地可见文件中。
|
||||||
|
3. 作为用户,我希望发消息时自动生效,无需每次重复输入。
|
||||||
|
4. 作为用户,我希望可以一键关闭规则,临时不生效。
|
||||||
|
|
||||||
|
## 5. 功能需求(前端/Webview + 扩展端)
|
||||||
|
|
||||||
|
### 5.1 规则管理界面
|
||||||
|
|
||||||
|
1. 提供“个人规则”入口。
|
||||||
|
2. 提供多行编辑框(显示当前规则内容)。
|
||||||
|
3. 提供“保存”按钮。
|
||||||
|
4. 提供“启用/停用”开关。
|
||||||
|
5. 显示当前状态:
|
||||||
|
6. 规则是否启用。
|
||||||
|
7. 规则字数/长度。
|
||||||
|
8. 最近保存时间(可选)。
|
||||||
|
|
||||||
|
### 5.2 本地文件存储
|
||||||
|
|
||||||
|
1. 规则内容保存到本地 `.md`。
|
||||||
|
2. 推荐文件名:`personal-rules.md`。
|
||||||
|
3. 推荐路径(优先):插件全局存储目录下固定子路径。
|
||||||
|
4. 文件不存在时可自动创建。
|
||||||
|
5. 用户可通过“打开规则文件”查看(可选)。
|
||||||
|
|
||||||
|
### 5.3 对话发送前处理
|
||||||
|
|
||||||
|
1. 用户点击发送消息。
|
||||||
|
2. 扩展端检查规则开关:
|
||||||
|
3. 关闭:不读取规则,不传后端。
|
||||||
|
4. 开启:读取 `.md` 内容。
|
||||||
|
5. 读取成功且非空时,将规则文本附加到对话请求结构化字段。
|
||||||
|
6. 读取失败时:提示告警,但不阻断正常对话。
|
||||||
|
|
||||||
|
### 5.4 限制与防护
|
||||||
|
|
||||||
|
1. 规则长度上限(例如 4000 字符,可配置)。
|
||||||
|
2. 超限时保存被拒绝,提示用户缩短。
|
||||||
|
3. 空白内容视为“无规则”。
|
||||||
|
4. 不允许二进制或非文本写入。
|
||||||
|
|
||||||
|
## 6. 功能需求(后端)
|
||||||
|
|
||||||
|
### 6.1 请求协议扩展
|
||||||
|
|
||||||
|
在现有对话请求结构中增加字段:
|
||||||
|
|
||||||
|
1. `personalRules`:字符串,可选。
|
||||||
|
2. `rulesEnabled`:布尔,可选(便于追踪)。
|
||||||
|
3. `rulesMeta`:可选元信息(长度、来源)。
|
||||||
|
|
||||||
|
### 6.2 注入策略
|
||||||
|
|
||||||
|
1. 后端收到 `personalRules` 后,将其注入系统提示层(而非用户消息层)。
|
||||||
|
2. 注入顺序建议:
|
||||||
|
3. 系统安全与平台策略。
|
||||||
|
4. 产品默认系统提示。
|
||||||
|
5. 用户个人规则。
|
||||||
|
6. 用户输入。
|
||||||
|
7. 若 `personalRules` 为空或开关关闭,则跳过注入。
|
||||||
|
|
||||||
|
### 6.3 风险控制
|
||||||
|
|
||||||
|
1. 规则文本不允许覆盖平台安全策略。
|
||||||
|
2. 记录本次是否注入规则(日志字段即可)。
|
||||||
|
3. 异常不应导致整次对话失败(可降级为无规则对话)。
|
||||||
|
|
||||||
|
## 7. 前后端对接设计
|
||||||
|
|
||||||
|
### 7.1 消息链路
|
||||||
|
|
||||||
|
1. Webview 触发 `sendMessage`。
|
||||||
|
2. 扩展端 `messageHandler` 统一处理发送。
|
||||||
|
3. `messageHandler` 在调用 `dialogService.sendMessage` 前读取个人规则。
|
||||||
|
4. `dialogService` 组装 `DialogRequest`,带上 `personalRules`。
|
||||||
|
5. `sseHandler` 发起流式请求。
|
||||||
|
6. 后端注入规则后进入模型推理。
|
||||||
|
7. 正常走现有 SSE 回传流程。
|
||||||
|
|
||||||
|
### 7.2 职责边界
|
||||||
|
|
||||||
|
1. Webview:展示与编辑,不直接拼接最终请求。
|
||||||
|
2. 扩展端:规则文件读写、开关状态管理、请求组装。
|
||||||
|
3. 后端:规则注入、优先级控制、审计日志。
|
||||||
|
|
||||||
|
## 8. 数据与状态设计
|
||||||
|
|
||||||
|
### 8.1 本地文件
|
||||||
|
|
||||||
|
1. 文件格式:Markdown 纯文本。
|
||||||
|
2. 内容约定:无强制模板,允许自由文本。
|
||||||
|
3. 编码:UTF-8。
|
||||||
|
|
||||||
|
### 8.2 本地配置状态
|
||||||
|
|
||||||
|
1. `personalRulesEnabled`:是否启用。
|
||||||
|
2. `personalRulesPath`:规则文件路径(可固定也可配置)。
|
||||||
|
3. `lastSavedAt`:最近保存时间(可选)。
|
||||||
|
|
||||||
|
## 9. 异常与降级
|
||||||
|
|
||||||
|
1. 文件不存在:自动创建空文件,视为无规则。
|
||||||
|
2. 文件读取失败:弹出提示,继续无规则发送。
|
||||||
|
3. 文件写入失败:保存失败提示,不更新状态。
|
||||||
|
4. 后端字段不识别:请求兼容,后端忽略新字段。
|
||||||
|
5. 后端注入失败:降级为普通对话,记录日志。
|
||||||
|
|
||||||
|
## 10. 安全与合规要求
|
||||||
|
|
||||||
|
1. 个人规则属于用户本地数据,不主动上传除非发起对话。
|
||||||
|
2. 日志中避免完整打印规则正文(最多打印长度和哈希)。
|
||||||
|
3. 后端注入时必须确保平台安全策略优先级更高。
|
||||||
|
|
||||||
|
## 11. 验收标准(UAT)
|
||||||
|
|
||||||
|
1. 用户保存规则后,本地存在 `personal-rules.md` 且内容一致。
|
||||||
|
2. 开启规则发送消息时,请求中可观测到 `personalRules` 字段。
|
||||||
|
3. 关闭规则发送消息时,请求中不含该字段或为空。
|
||||||
|
4. 规则文件损坏/读取失败时,不影响正常聊天。
|
||||||
|
5. 超过长度上限时,前端保存被拒绝且提示明确。
|
||||||
|
6. 后端日志可确认“本次是否注入个人规则”。
|
||||||
|
|
||||||
|
## 12. 迭代建议(下一阶段)
|
||||||
|
|
||||||
|
1. 规则模板(代码风格、语言风格、测试偏好)。
|
||||||
|
2. 项目规则与个人规则合并策略。
|
||||||
|
3. 云端同步(按 `userId`),多端一致。
|
||||||
@ -758,6 +758,23 @@ export async function showICHelperPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
// 打开文件
|
||||||
|
case "openFile":
|
||||||
|
{
|
||||||
|
let filePath = message.filePath;
|
||||||
|
if (filePath) {
|
||||||
|
// 如果是相对路径,转换为绝对路径
|
||||||
|
if (!require("path").isAbsolute(filePath)) {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (workspaceFolder) {
|
||||||
|
filePath = require("path").join(workspaceFolder.uri.fsPath, filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const uri = vscode.Uri.file(filePath);
|
||||||
|
vscode.window.showTextDocument(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
// 新增:检查工作区状态
|
// 新增:检查工作区状态
|
||||||
case "checkWorkspace":
|
case "checkWorkspace":
|
||||||
const hasWorkspace = !!(
|
const hasWorkspace = !!(
|
||||||
|
|||||||
62
src/views/filePathTag.ts
Normal file
62
src/views/filePathTag.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 文件路径标签组件
|
||||||
|
* 功能:显示可点击的文件路径标签
|
||||||
|
* 使用场景:在用户消息中显示上下文文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件路径标签的样式
|
||||||
|
*/
|
||||||
|
export function getFilePathTagStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 文件路径标签 */
|
||||||
|
.file-path-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background: rgba(0, 122, 204, 0.15);
|
||||||
|
border: 1px solid rgba(0, 122, 204, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path-tag:hover {
|
||||||
|
background: rgba(0, 122, 204, 0.25);
|
||||||
|
border-color: rgba(0, 122, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path-tag svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件路径标签的脚本
|
||||||
|
*/
|
||||||
|
export function getFilePathTagScript(): string {
|
||||||
|
return `
|
||||||
|
// 处理文件路径标签点击
|
||||||
|
function handleFilePathClick(filePath) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFile',
|
||||||
|
filePath: filePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件路径标签
|
||||||
|
window.createFilePathTag = function(filePath) {
|
||||||
|
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||||
|
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
||||||
|
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -24,6 +24,10 @@ import {
|
|||||||
getContextCompressStyles,
|
getContextCompressStyles,
|
||||||
getContextCompressScript,
|
getContextCompressScript,
|
||||||
} from "./contextCompress";
|
} from "./contextCompress";
|
||||||
|
import {
|
||||||
|
getFilePathTagStyles,
|
||||||
|
getFilePathTagScript,
|
||||||
|
} from "./filePathTag";
|
||||||
import {
|
import {
|
||||||
getOptimizeButtonContent,
|
getOptimizeButtonContent,
|
||||||
getOptimizeButtonStyles,
|
getOptimizeButtonStyles,
|
||||||
@ -98,6 +102,7 @@ export function getInputAreaStyles(): string {
|
|||||||
${getModelSelectorStyles()}
|
${getModelSelectorStyles()}
|
||||||
${getContextButtonStyles()}
|
${getContextButtonStyles()}
|
||||||
${getContextDisplayStyles()}
|
${getContextDisplayStyles()}
|
||||||
|
${getFilePathTagStyles()}
|
||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
${getExampleShowcaseStyles()}
|
${getExampleShowcaseStyles()}
|
||||||
@ -309,6 +314,7 @@ export function getInputAreaScript(): string {
|
|||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
${getChangePanelScript()}
|
${getChangePanelScript()}
|
||||||
|
${getFilePathTagScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
@ -426,7 +432,19 @@ export function getInputAreaScript(): string {
|
|||||||
// 获取上下文项
|
// 获取上下文项
|
||||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||||
|
|
||||||
addMessage(text, 'user');
|
// 构建显示消息:如果有上下文文件,添加文件路径前缀
|
||||||
|
let displayText = text;
|
||||||
|
if (contextItems.length > 0) {
|
||||||
|
const filePaths = contextItems
|
||||||
|
.filter(item => item.type === 'file')
|
||||||
|
.map(item => item.displayPath || item.path)
|
||||||
|
.join(' ');
|
||||||
|
if (filePaths) {
|
||||||
|
displayText = filePaths + ' ' + text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(displayText, 'user');
|
||||||
|
|
||||||
// 标记已有消息,切换布局到底部
|
// 标记已有消息,切换布局到底部
|
||||||
hasMessages = true;
|
hasMessages = true;
|
||||||
|
|||||||
@ -849,8 +849,27 @@ export function getMessageAreaScript(): string {
|
|||||||
actionsDiv.appendChild(dislikeBtn);
|
actionsDiv.appendChild(dislikeBtn);
|
||||||
|
|
||||||
div.appendChild(actionsDiv);
|
div.appendChild(actionsDiv);
|
||||||
|
} else {
|
||||||
|
// 用户消息:解析文件路径并转换为标签
|
||||||
|
const parts = text.split(' ');
|
||||||
|
const filePaths = [];
|
||||||
|
const textParts = [];
|
||||||
|
|
||||||
|
parts.forEach(part => {
|
||||||
|
// 判断是否为文件路径:包含路径分隔符或文件扩展名
|
||||||
|
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part)) {
|
||||||
|
filePaths.push(part);
|
||||||
|
} else {
|
||||||
|
textParts.push(part);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths.length > 0) {
|
||||||
|
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
|
||||||
} else {
|
} else {
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
// 当添加用户消息时,隐藏 header
|
// 当添加用户消息时,隐藏 header
|
||||||
hideHeaderIfNeeded();
|
hideHeaderIfNeeded();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user