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;
|
||||
// 打开文件
|
||||
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":
|
||||
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,
|
||||
getContextCompressScript,
|
||||
} from "./contextCompress";
|
||||
import {
|
||||
getFilePathTagStyles,
|
||||
getFilePathTagScript,
|
||||
} from "./filePathTag";
|
||||
import {
|
||||
getOptimizeButtonContent,
|
||||
getOptimizeButtonStyles,
|
||||
@ -98,6 +102,7 @@ export function getInputAreaStyles(): string {
|
||||
${getModelSelectorStyles()}
|
||||
${getContextButtonStyles()}
|
||||
${getContextDisplayStyles()}
|
||||
${getFilePathTagStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
${getExampleShowcaseStyles()}
|
||||
@ -309,6 +314,7 @@ export function getInputAreaScript(): string {
|
||||
${getContextCompressScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
${getChangePanelScript()}
|
||||
${getFilePathTagScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
@ -426,7 +432,19 @@ export function getInputAreaScript(): string {
|
||||
// 获取上下文项
|
||||
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;
|
||||
|
||||
@ -850,7 +850,26 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
div.textContent = text;
|
||||
// 用户消息:解析文件路径并转换为标签
|
||||
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 {
|
||||
div.textContent = text;
|
||||
}
|
||||
|
||||
// 当添加用户消息时,隐藏 header
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user