diff --git a/docs/personal-rules-mvp-requirements.md b/docs/personal-rules-mvp-requirements.md
new file mode 100644
index 0000000..2fefd5d
--- /dev/null
+++ b/docs/personal-rules-mvp-requirements.md
@@ -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`),多端一致。
diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts
index ff5c5ff..76899c0 100644
--- a/src/panels/ICHelperPanel.ts
+++ b/src/panels/ICHelperPanel.ts
@@ -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 = !!(
diff --git a/src/views/filePathTag.ts b/src/views/filePathTag.ts
new file mode 100644
index 0000000..72b14e5
--- /dev/null
+++ b/src/views/filePathTag.ts
@@ -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 = '';
+ const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
+ return '' + fileIcon + filePath + '';
+ };
+ `;
+}
diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts
index 9da5dc6..9958e1d 100644
--- a/src/views/inputArea.ts
+++ b/src/views/inputArea.ts
@@ -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;
diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts
index 61d1950..e891af7 100644
--- a/src/views/messageArea.ts
+++ b/src/views/messageArea.ts
@@ -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();
}