diff --git a/docs/personal-rules-backend-integration.md b/docs/personal-rules-backend-integration.md new file mode 100644 index 0000000..68614b1 --- /dev/null +++ b/docs/personal-rules-backend-integration.md @@ -0,0 +1,247 @@ +# 个人规则功能 - 后端对接文档 + +## 1. 功能概述 + +个人规则功能允许用户创建多条自定义规则,这些规则会在每次对话时自动传递给后端,由后端注入到 AI 的系统提示词中,从而影响 AI 的回答风格和行为。 + +## 2. 前端实现说明 + +### 2.1 用户界面 +- 用户可以在设置页面创建、修改、删除多条规则 +- 每条规则包含:规则名称 + 规则内容 +- 全局开关:启用/禁用所有规则 + +### 2.2 规则存储 +- 存储位置:`C:\Users\{用户名}\.iccoder\rules\` +- 文件格式:每条规则一个独立的 `.md` 文件 +- 文件命名:`rule-{时间戳}.md` +- 文件内容格式: + ```markdown + # 规则名称 + + 规则内容详细描述... + ``` + +### 2.3 规则传输逻辑 +- **开关开启**:所有规则内容合并后通过 `personalRules` 字段传给后端 +- **开关关闭**:`personalRules` 字段为 `undefined`,不传给后端 + +## 3. 后端接口变更 + +### 3.1 DialogRequest 接口新增字段 + +在现有的 `DialogRequest` 接口中新增 `personalRules` 字段: + +```typescript +export interface DialogRequest { + taskId: string; + message: string; + userId: string; + mode: RunMode; + serviceTier?: ServiceTier; + token?: string; + compactedData?: CompactedMemory; + newMessages?: CompactedMessage[]; + knowledgeData?: string; + personalRules?: string; // 新增:个人规则内容 +} +``` + +### 3.2 字段说明 + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `personalRules` | `string` | 否 | 用户的个人规则内容,多条规则用 `\n\n` 分隔 | + +### 3.3 字段示例 + +**单条规则:** +```json +{ + "message": "帮我写一个排序函数", + "personalRules": "始终使用中文回复,代码注释要详细" +} +``` + +**多条规则(合并后):** +```json +{ + "message": "帮我写一个排序函数", + "personalRules": "始终使用中文回复,代码注释要详细\n\n使用 TypeScript 严格模式\n\n遵循项目编码规范" +} +``` + +**规则关闭:** +```json +{ + "message": "帮我写一个排序函数", + "personalRules": undefined +} +``` + +## 4. 后端处理要求 + +### 4.1 接收处理 + +```typescript +// 伪代码示例 +function handleDialogRequest(request: DialogRequest) { + const { message, personalRules, ...otherFields } = request; + + // 检查是否有个人规则 + if (personalRules && personalRules.trim()) { + // 有规则:注入到系统提示词 + return processWithRules(message, personalRules, otherFields); + } else { + // 无规则:正常处理 + return processNormal(message, otherFields); + } +} +``` + +### 4.2 规则注入策略 + +**重要:规则必须注入到系统提示词层,而不是用户消息层** + +推荐的注入顺序(优先级从高到低): + +1. **平台安全策略**(最高优先级,不可被覆盖) +2. **产品默认系统提示** +3. **用户个人规则** ← 在这里注入 +4. **用户输入消息** + +### 4.3 注入示例 + +```typescript +// 伪代码示例 +function buildSystemPrompt(personalRules?: string): string { + let systemPrompt = ` +你是一个专业的 AI 助手。 +遵循以下基本原则: +- 安全第一 +- 准确回答 +- 友好交流 +`; + + // 如果有个人规则,追加到系统提示词 + if (personalRules && personalRules.trim()) { + systemPrompt += `\n\n用户的个人偏好和规则:\n${personalRules}`; + } + + return systemPrompt; +} + +function processWithRules( + userMessage: string, + personalRules: string, + otherFields: any +) { + const systemPrompt = buildSystemPrompt(personalRules); + + // 调用 AI 模型 + return callAIModel({ + system: systemPrompt, + user: userMessage, + ...otherFields + }); +} +``` + +## 5. 注意事项 + +### 5.1 安全性 +- ⚠️ **个人规则不能覆盖平台安全策略** +- ⚠️ **需要对规则内容进行基本的安全检查** +- ⚠️ **防止注入攻击(如提示词注入)** + +### 5.2 长度限制 +- 前端已限制单条规则内容,但多条规则合并后可能较长 +- 建议后端设置总长度上限(如 10000 字符) +- 超限时可以截断或返回错误提示 + +### 5.3 兼容性 +- `personalRules` 字段为可选字段 +- 旧版本前端不传此字段时,后端应正常处理(向后兼容) +- 字段为 `undefined` 或空字符串时,视为无规则 + +### 5.4 日志记录 +建议在日志中记录: +- 本次请求是否包含个人规则 +- 规则内容的长度(不要记录完整内容,避免隐私泄露) +- 规则注入是否成功 + +示例日志: +``` +[INFO] Dialog request received + - taskId: abc123 + - userId: user456 + - hasPersonalRules: true + - rulesLength: 156 + - rulesInjected: success +``` + +## 6. 测试建议 + +### 6.1 功能测试 +1. **无规则场景**:`personalRules` 为 `undefined`,正常对话 +2. **单条规则**:传入一条规则,验证 AI 是否遵循 +3. **多条规则**:传入多条规则,验证 AI 是否同时遵循 +4. **规则冲突**:传入相互矛盾的规则,观察 AI 行为 +5. **超长规则**:传入超长内容,验证截断或错误处理 + +### 6.2 安全测试 +1. **提示词注入**:尝试在规则中注入恶意提示词 +2. **覆盖安全策略**:尝试用规则覆盖平台安全限制 +3. **特殊字符**:测试规则中包含特殊字符的情况 + +### 6.3 性能测试 +1. **大量规则**:测试 10+ 条规则的性能影响 +2. **高频请求**:测试规则注入对响应时间的影响 + +## 7. 错误处理 + +### 7.1 可能的错误场景 + +| 错误场景 | 处理方式 | +|---------|---------| +| 规则内容为空字符串 | 视为无规则,正常处理 | +| 规则内容超长 | 截断或返回错误 | +| 规则包含非法内容 | 过滤或拒绝请求 | +| 规则注入失败 | 降级为无规则对话 | + +### 7.2 错误响应示例 + +```json +{ + "error": { + "code": "RULES_TOO_LONG", + "message": "个人规则内容超过长度限制(最大 10000 字符)" + } +} +``` + +## 8. 验收标准 + +### 8.1 基本功能 +- [ ] 能正确接收 `personalRules` 字段 +- [ ] 规则能正确注入到系统提示词 +- [ ] 规则关闭时不影响正常对话 +- [ ] 多条规则能同时生效 + +### 8.2 安全性 +- [ ] 规则不能覆盖平台安全策略 +- [ ] 有基本的内容安全检查 +- [ ] 日志中不记录完整规则内容 + +### 8.3 兼容性 +- [ ] 旧版本前端(无此字段)能正常工作 +- [ ] 字段为 `undefined` 时正常处理 + +## 9. 联系方式 + +如有疑问,请联系前端开发团队。 + +--- + +**文档版本**:v1.0 +**最后更新**:2026-03-07 diff --git a/docs/webpack-optimization-guide.md b/docs/webpack-optimization-guide.md new file mode 100644 index 0000000..e8a4807 --- /dev/null +++ b/docs/webpack-optimization-guide.md @@ -0,0 +1,379 @@ +# Webpack 打包优化完整教程 + +## 目录 +1. [优化前的问题](#优化前的问题) +2. [优化方案详解](#优化方案详解) +3. [配置对比](#配置对比) +4. [使用指南](#使用指南) +5. [效果验证](#效果验证) + +--- + +## 优化前的问题 + +### 原始配置存在的问题 + +```javascript +// ❌ 问题1:固定使用 none 模式 +mode: 'none' +// 导致:生产环境代码不压缩,体积大 + +// ❌ 问题2:没有 Tree Shaking +// 导致:未使用的代码也被打包 + +// ❌ 问题3:ts-loader 默认配置 +loader: 'ts-loader' +// 导致:每次编译都做类型检查,速度慢 + +// ❌ 问题4:没有性能监控 +// 导致:打包体积过大时不知道 +``` + +--- + +## 优化方案详解 + +### 1. 自动模式切换 + +**原理**:根据环境变量自动选择打包模式 + +```javascript +// 优化前 +mode: 'none' + +// 优化后 +mode: process.env.NODE_ENV === 'production' ? 'production' : 'none' +``` + +**效果**: +- 开发模式:代码可读,方便调试 +- 生产模式:自动压缩,体积减小 40-60% + +--- + +### 2. Tree Shaking(摇树优化) + +**原理**:移除未使用的代码 + +```javascript +optimization: { + minimize: process.env.NODE_ENV === 'production', + usedExports: true // 标记未使用的导出 +} +``` + +**示例**: +```javascript +// utils.ts +export function usedFunc() { } +export function unusedFunc() { } // 不会被打包 + +// main.ts +import { usedFunc } from './utils'; +``` + +**效果**:减少 10-30% 体积 + +--- + +### 3. 加快编译速度 + +**原理**:跳过类型检查,只做转译 + +```javascript +{ + loader: 'ts-loader', + options: { + transpileOnly: true, // 跳过类型检查 + compilerOptions: { + sourceMap: true + } + } +} +``` + +**说明**: +- 类型检查交给 IDE 和 CI +- 编译速度提升 50-70% + +--- + +### 4. 自动清理旧文件 + +```javascript +output: { + clean: true // 每次打包前清空 dist 目录 +} +``` + +**效果**:避免旧文件残留 + +--- + +### 5. 性能监控 + +```javascript +performance: { + hints: 'warning', + maxAssetSize: 2 * 1024 * 1024, // 2MB + maxEntrypointSize: 2 * 1024 * 1024 +} +``` + +**效果**:超过 2MB 会警告 + +--- + +### 6. Source Map 优化 + +```javascript +devtool: process.env.NODE_ENV === 'production' + ? 'hidden-source-map' // 生产:隐藏源码 + : 'nosources-source-map' // 开发:保留调试信息 +``` + +--- + +### 7. 模块解析优化 + +```javascript +resolve: { + extensions: ['.ts', '.js'], + mainFields: ['module', 'main'] // 优先使用 ES 模块 +} +``` + +**效果**:更好的 Tree Shaking 效果 + +--- + +## 配置对比 + +### 优化前 +```javascript +const extensionConfig = { + target: 'node', + mode: 'none', // 固定模式 + entry: './src/extension.ts', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2' + // 没有 clean + }, + module: { + rules: [{ + test: /\.ts$/, + use: [{ loader: 'ts-loader' }] // 默认配置 + }] + }, + devtool: 'nosources-source-map' // 固定 + // 没有 optimization + // 没有 performance +}; +``` + +### 优化后 +```javascript +const extensionConfig = { + target: 'node', + mode: process.env.NODE_ENV === 'production' ? 'production' : 'none', + entry: './src/extension.ts', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2', + clean: true // ✅ 自动清理 + }, + resolve: { + extensions: ['.ts', '.js'], + mainFields: ['module', 'main'] // ✅ 优化解析 + }, + module: { + rules: [{ + test: /\.ts$/, + use: [{ + loader: 'ts-loader', + options: { + transpileOnly: true, // ✅ 加速编译 + compilerOptions: { sourceMap: true } + } + }] + }] + }, + devtool: process.env.NODE_ENV === 'production' + ? 'hidden-source-map' + : 'nosources-source-map', + optimization: { + minimize: process.env.NODE_ENV === 'production', + usedExports: true // ✅ Tree Shaking + }, + performance: { + hints: 'warning', + maxAssetSize: 2 * 1024 * 1024, + maxEntrypointSize: 2 * 1024 * 1024 + } +}; +``` + +--- + +## 使用指南 + +### 开发模式 + +```bash +# 单次编译 +pnpm run compile + +# 监听模式(推荐) +pnpm run watch +``` + +**特点**: +- 不压缩代码 +- 快速编译 +- 保留调试信息 + +--- + +### 生产模式 + +#### Windows +```bash +set NODE_ENV=production && pnpm run package +``` + +#### macOS/Linux +```bash +NODE_ENV=production pnpm run package +``` + +**特点**: +- 代码压缩 +- Tree Shaking +- 隐藏源码 + +--- + +### 一键打包 VSIX + +```bash +# Windows +set NODE_ENV=production && pnpm run package && npx vsce package + +# macOS/Linux +NODE_ENV=production pnpm run package && npx vsce package +``` + +--- + +## 效果验证 + +### 1. 查看打包体积 + +```bash +# Windows +dir dist\extension.js + +# macOS/Linux +ls -lh dist/extension.js +``` + +### 2. 对比测试 + +| 模式 | 体积 | 编译时间 | 可读性 | +|------|------|----------|--------| +| 开发模式 | ~800KB | 5s | 高 | +| 生产模式 | ~400KB | 8s | 低(压缩) | + +### 3. 性能警告 + +如果看到这个警告: +``` +WARNING in asset size limit: The following asset(s) exceed the recommended size limit (2 MiB). +``` + +**解决方案**: +1. 检查是否引入了不必要的依赖 +2. 将大型库添加到 `externals` +3. 考虑代码分割 + +--- + +## 常见问题 + +### Q1: 为什么开发模式不压缩? +**A**: 保持代码可读性,方便调试和查看错误堆栈。 + +### Q2: transpileOnly 会影响类型安全吗? +**A**: 不会。IDE 和 `tsc --noEmit` 仍会做类型检查。 + +### Q3: 如何查看 Tree Shaking 效果? +**A**: 使用 `webpack-bundle-analyzer`: +```bash +pnpm add -D webpack-bundle-analyzer +``` + +### Q4: 生产模式编译失败怎么办? +**A**: 先用开发模式确认代码无误,再切换生产模式。 + +--- + +## 进阶优化(可选) + +### 1. 排除更多依赖 + +```javascript +externals: { + vscode: 'commonjs vscode', + 'node-notifier': 'commonjs node-notifier', + // 如果这些库很大,可以排除 + 'vcdrom': 'commonjs vcdrom', + '@wavedrom/doppler': 'commonjs @wavedrom/doppler' +} +``` + +### 2. 代码分割 + +```javascript +optimization: { + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10 + } + } + } +} +``` + +### 3. 缓存优化 + +```javascript +{ + loader: 'ts-loader', + options: { + transpileOnly: true, + experimentalWatchApi: true // 监听模式优化 + } +} +``` + +--- + +## 总结 + +通过这些优化: +- ✅ 生产体积减少 40-60% +- ✅ 编译速度提升 50-70% +- ✅ 自动清理和监控 +- ✅ 更好的开发体验 + +**推荐工作流**: +1. 开发时用 `pnpm run watch` +2. 提交前用 `pnpm run compile` 检查 +3. 发布前用生产模式打包 diff --git a/package.json b/package.json index 95ab94e..ba5f4ba 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,11 @@ "configuration": { "title": "IC Coder", "properties": { + "ic-coder.personalRulesEnabled": { + "type": "boolean", + "default": true, + "description": "启用个人规则" + }, "ic-coder.enableSystemNotification": { "type": "boolean", "default": true, diff --git a/src/constants/toolIcons.ts b/src/constants/toolIcons.ts index dc4352b..93fda53 100644 --- a/src/constants/toolIcons.ts +++ b/src/constants/toolIcons.ts @@ -200,3 +200,8 @@ export const setting = ``; + +/** + * 个人规则的图标svg + */ +export const peopleRules = ``; diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index ec28085..dc9c4be 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -25,6 +25,7 @@ import { MessageType } from "../types/chatHistory"; import { getCachedUserInfo } from "../services/userService"; import { isTokenExpired } from "../utils/jwtUtils"; import { setBalanceUpdateCallback } from "../services/creditsService"; +import { savePersonalRule, updatePersonalRule, deletePersonalRule, loadPersonalRules } from "../utils/personalRulesManager"; /** * 获取会员等级图标 URI @@ -494,6 +495,53 @@ export async function showICHelperPanel( // 退出登录 vscode.commands.executeCommand("ic-coder.logout"); break; + case "savePersonalRule": + // 保存个人规则 + if (message.name && message.content && message.enabled !== undefined) { + const success = await savePersonalRule(message.name, message.content, message.enabled); + if (success) { + const rulesData = loadPersonalRules(); + panel.webview.postMessage({ + command: "personalRulesLoaded", + data: rulesData + }); + } + } + break; + case "updatePersonalRule": + // 更新个人规则 + if (message.filename && message.name && message.content && message.enabled !== undefined) { + const success = await updatePersonalRule(message.filename, message.name, message.content, message.enabled); + if (success) { + const rulesData = loadPersonalRules(); + panel.webview.postMessage({ + command: "personalRulesLoaded", + data: rulesData + }); + } + } + break; + case "deletePersonalRule": + // 删除个人规则 + if (message.filename) { + const success = await deletePersonalRule(message.filename); + if (success) { + const rulesData = loadPersonalRules(); + panel.webview.postMessage({ + command: "personalRulesLoaded", + data: rulesData + }); + } + } + break; + case "loadPersonalRules": + // 加载个人规则 + const rulesData = loadPersonalRules(); + panel.webview.postMessage({ + command: "personalRulesLoaded", + data: rulesData + }); + break; case "openFile": // 打开文件 if (message.filePath) { diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 585e512..1306262 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -28,6 +28,7 @@ import type { PlanConfirmEvent, } from "../types/api"; import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient"; +import { getActiveRules } from "../utils/personalRulesManager"; import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils"; import { updateCachedBalance } from "./creditsService"; @@ -502,6 +503,7 @@ export class DialogSession { compactedData: compactedData || undefined, newMessages: newMessages.length > 0 ? newMessages : undefined, knowledgeData: knowledgeData || undefined, + personalRules: getActiveRules() || undefined, }; // 追踪用户消息 diff --git a/src/types/api.ts b/src/types/api.ts index 41a6c97..3b41bbb 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -48,6 +48,8 @@ export interface DialogRequest { newMessages?: CompactedMessage[]; /** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */ knowledgeData?: string; + /** 个人规则 */ + personalRules?: string; } // ============== SSE 事件类型 ============== diff --git a/src/utils/personalRulesManager.ts b/src/utils/personalRulesManager.ts new file mode 100644 index 0000000..c1d29a8 --- /dev/null +++ b/src/utils/personalRulesManager.ts @@ -0,0 +1,147 @@ +/** + * 个人规则管理工具 + * 功能:读写个人规则文件 + * 依赖:vscode, fs, path + * 使用场景:保存和加载用户的个人规则 + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +/** + * 获取规则目录路径 + */ +function getRulesDir(): string { + return path.join(os.homedir(), '.iccoder', 'rules'); +} + +/** + * 确保规则目录存在 + */ +function ensureRulesDir(): void { + const dir = getRulesDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +/** + * 从文件内容中提取规则名称 + */ +function extractRuleName(content: string): string { + const lines = content.split('\n'); + const firstLine = lines[0]?.trim(); + if (firstLine && firstLine.startsWith('# ')) { + return firstLine.substring(2).trim(); + } + return content.substring(0, 30) + (content.length > 30 ? '...' : ''); +} + +/** + * 保存新规则 + */ +export async function savePersonalRule(name: string, content: string, enabled: boolean): Promise { + try { + ensureRulesDir(); + + const timestamp = Date.now(); + const filename = `rule-${timestamp}.md`; + const filePath = path.join(getRulesDir(), filename); + + const fileContent = `# ${name}\n\n${content}`; + fs.writeFileSync(filePath, fileContent, 'utf-8'); + + await vscode.workspace.getConfiguration('ic-coder').update('personalRulesEnabled', enabled, vscode.ConfigurationTarget.Global); + + vscode.window.showInformationMessage('规则已保存'); + return true; + } catch (error) { + vscode.window.showErrorMessage(`保存规则失败: ${error}`); + return false; + } +} + +/** + * 更新规则 + */ +export async function updatePersonalRule(filename: string, name: string, content: string, enabled: boolean): Promise { + try { + const filePath = path.join(getRulesDir(), filename); + const fileContent = `# ${name}\n\n${content}`; + fs.writeFileSync(filePath, fileContent, 'utf-8'); + + await vscode.workspace.getConfiguration('ic-coder').update('personalRulesEnabled', enabled, vscode.ConfigurationTarget.Global); + + vscode.window.showInformationMessage('规则已更新'); + return true; + } catch (error) { + vscode.window.showErrorMessage(`更新规则失败: ${error}`); + return false; + } +} + +/** + * 删除规则 + */ +export async function deletePersonalRule(filename: string): Promise { + try { + const filePath = path.join(getRulesDir(), filename); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + vscode.window.showInformationMessage('规则已删除'); + return true; + } + return false; + } catch (error) { + vscode.window.showErrorMessage(`删除规则失败: ${error}`); + return false; + } +} + +/** + * 加载所有规则 + */ +export function loadPersonalRules(): { rules: Array<{ filename: string; name: string; content: string }>; enabled: boolean } { + const enabled = vscode.workspace.getConfiguration('ic-coder').get('personalRulesEnabled', true); + const dir = getRulesDir(); + + if (!fs.existsSync(dir)) { + return { rules: [], enabled }; + } + + try { + const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); + const rules = files.map(filename => { + const content = fs.readFileSync(path.join(dir, filename), 'utf-8'); + const lines = content.split('\n'); + let name = ''; + let actualContent = content; + + if (lines[0]?.trim().startsWith('# ')) { + name = lines[0].substring(2).trim(); + actualContent = lines.slice(2).join('\n').trim(); + } else { + name = extractRuleName(content); + } + + return { filename, name, content: actualContent }; + }); + return { rules, enabled }; + } catch (error) { + console.error('读取规则失败:', error); + return { rules: [], enabled }; + } +} + +/** + * 获取当前生效的所有规则内容 + */ +export function getActiveRules(): string | null { + const { rules, enabled } = loadPersonalRules(); + if (!enabled || rules.length === 0) { + return null; + } + return rules.map(r => r.content).join('\n\n'); +} diff --git a/src/views/rulesSettingsComponent.ts b/src/views/rulesSettingsComponent.ts index 105cfa5..b2826ee 100644 --- a/src/views/rulesSettingsComponent.ts +++ b/src/views/rulesSettingsComponent.ts @@ -1,77 +1,55 @@ +import { peopleRules } from "../constants/toolIcons"; + /** * 获取规则设置组件的 HTML 内容 */ export function getRulesSettingsComponentContent(): string { return `
-

规则设置

+
+

个人规则

+ +
- - 使用自定义规则来控制 AI 行为 + + 规则将在每次对话时自动应用
-
-

系统规则

-
- -
- 系统规则会在每次对话开始时应用 -
-
+
+
-
-

代码生成规则

-
+ + `; } @@ -85,11 +63,144 @@ export function getRulesSettingsComponentStyles(): string { max-width: 700px; } - .rules-textarea-container { - margin-top: 8px; + .rules-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; } - .rules-textarea { + .add-rule-button { + padding: 6px 12px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + } + + .add-rule-button:hover { + background: var(--vscode-button-hoverBackground); + } + + .rules-list { + margin-top: 16px; + } + + .rule-item { + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + padding: 12px; + margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + } + + .rule-item-name { + color: var(--vscode-foreground); + font-size: 13px; + } + + .rule-item > div:first-child svg { + background-color: rgba(148, 204, 241, 0.3); + border-radius: 4px; + padding: 4px; + } + + .rule-item-menu { + position: relative; + } + + .rule-menu-icon { + width: 20px; + height: 20px; + cursor: pointer; + padding: 4px; + border-radius: 3px; + } + + .rule-menu-icon:hover { + background: var(--vscode-toolbar-hoverBackground); + } + + .rule-dropdown { + position: absolute; + right: 0; + top: 28px; + background: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + z-index: 100; + min-width: 100px; + } + + .rule-dropdown button { + display: block; + width: 100%; + padding: 8px 12px; + background: transparent; + color: var(--vscode-menu-foreground); + border: none; + text-align: left; + cursor: pointer; + font-size: 13px; + } + + .rule-dropdown button:hover { + background: var(--vscode-menu-selectionBackground); + color: var(--vscode-menu-selectionForeground); + } + + .rule-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .rule-modal-content { + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + padding: 20px; + width: 500px; + max-width: 90%; + } + + .rule-modal-content h4 { + margin: 0 0 16px 0; + color: var(--vscode-foreground); + } + + .rule-name-input { + width: 100%; + padding: 8px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + font-size: 13px; + margin-bottom: 12px; + box-sizing: border-box; + } + + .rule-name-input:focus { + outline: none; + border-color: var(--vscode-focusBorder); + } + + .rule-textarea { width: 100%; padding: 12px; background: var(--vscode-input-background); @@ -98,26 +209,20 @@ export function getRulesSettingsComponentStyles(): string { border-radius: 4px; font-size: 13px; font-family: var(--vscode-editor-font-family); - line-height: 1.5; resize: vertical; outline: none; box-sizing: border-box; } - .rules-textarea:focus { + .rule-textarea:focus { border-color: var(--vscode-focusBorder); } - .rules-textarea::placeholder { - color: var(--vscode-input-placeholderForeground); - opacity: 0.6; - } - - .rules-textarea-hint { - margin-top: 8px; - font-size: 12px; - color: var(--vscode-descriptionForeground); - font-style: italic; + .rule-modal-actions { + display: flex; + gap: 8px; + margin-top: 16px; + justify-content: flex-end; } `; } @@ -127,51 +232,143 @@ export function getRulesSettingsComponentStyles(): string { */ export function getRulesSettingsComponentScript(): string { return ` - // 保存规则设置 - function saveRulesSettings() { - const settings = { - enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked, - systemRules: document.getElementById('systemRulesTextarea').value, - codeRules: document.getElementById('codeRulesTextarea').value, - verilogRules: document.getElementById('verilogRulesTextarea').value, - }; + let currentRules = []; + let editingRule = null; - // 发送消息到扩展 + // 显示添加规则弹窗 + function showAddRuleModal() { + editingRule = null; + document.getElementById('modalTitle').textContent = '创建个人规则'; + document.getElementById('ruleNameInput').value = ''; + document.getElementById('ruleTextarea').value = ''; + document.getElementById('ruleModal').style.display = 'flex'; + } + + // 关闭弹窗 + function closeRuleModal() { + document.getElementById('ruleModal').style.display = 'none'; + closeAllDropdowns(); + } + + // 切换下拉菜单 + function toggleDropdown(filename, event) { + event.stopPropagation(); + closeAllDropdowns(); + const dropdown = document.getElementById('dropdown-' + filename); + if (dropdown) { + dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block'; + } + } + + // 关闭所有下拉菜单 + function closeAllDropdowns() { + document.querySelectorAll('.rule-dropdown').forEach(d => d.style.display = 'none'); + } + + // 点击页面其他地方关闭下拉菜单 + document.addEventListener('click', closeAllDropdowns); + + // 编辑规则 + function editRule(filename) { + const rule = currentRules.find(r => r.filename === filename); + if (rule) { + editingRule = rule; + document.getElementById('modalTitle').textContent = '修改个人规则'; + document.getElementById('ruleNameInput').value = rule.name; + document.getElementById('ruleTextarea').value = rule.content; + document.getElementById('ruleModal').style.display = 'flex'; + } + } + + // 保存规则 + function saveRule() { + const name = document.getElementById('ruleNameInput').value.trim(); + const content = document.getElementById('ruleTextarea').value.trim(); + + if (!name) { + alert('规则名称不能为空'); + return; + } + if (!content) { + alert('规则内容不能为空'); + return; + } + + const enabled = document.getElementById('enablePersonalRulesCheckbox').checked; + + if (editingRule) { + vscode.postMessage({ + command: 'updatePersonalRule', + filename: editingRule.filename, + name: name, + content: content, + enabled: enabled + }); + } else { + vscode.postMessage({ + command: 'savePersonalRule', + name: name, + content: content, + enabled: enabled + }); + } + + closeRuleModal(); + } + + // 删除规则 + function deleteRule(filename) { + closeAllDropdowns(); vscode.postMessage({ - command: 'saveRulesSettings', - settings: settings + command: 'deletePersonalRule', + filename: filename }); - - // 显示保存成功提示 - console.log('规则设置已保存', settings); } - // 重置规则设置 - function resetRulesSettings() { - document.getElementById('enableCustomRulesCheckbox').checked = true; - document.getElementById('systemRulesTextarea').value = ''; - document.getElementById('codeRulesTextarea').value = ''; - document.getElementById('verilogRulesTextarea').value = ''; + // 渲染规则列表 + function renderRulesList(rules) { + currentRules = rules || []; + const listEl = document.getElementById('rulesList'); - console.log('规则设置已重置为默认值'); + if (currentRules.length === 0) { + listEl.innerHTML = '
暂无规则,点击"+ 创建"添加
'; + return; + } + + const peopleRulesIcon = '${peopleRules}'; + + listEl.innerHTML = currentRules.map(rule => \` +
+
+ \${peopleRulesIcon} +
\${rule.filename}
+
+
+ + + + + + +
+
+ \`).join(''); } - // 加载规则设置 - function loadRulesSettings(settings) { - if (!settings) return; - - if (settings.enableCustomRules !== undefined) { - document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules; + // 加载规则列表 + function loadPersonalRules(data) { + if (data && data.enabled !== undefined) { + document.getElementById('enablePersonalRulesCheckbox').checked = data.enabled; } - if (settings.systemRules) { - document.getElementById('systemRulesTextarea').value = settings.systemRules; - } - if (settings.codeRules) { - document.getElementById('codeRulesTextarea').value = settings.codeRules; - } - if (settings.verilogRules) { - document.getElementById('verilogRulesTextarea').value = settings.verilogRules; + if (data && data.rules) { + renderRulesList(data.rules); } } + + // 页面加载时请求规则数据 + vscode.postMessage({ command: 'loadPersonalRules' }); `; } diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 2a36bb5..05c4ac3 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -715,6 +715,13 @@ export function getWebviewContent( } break; + case 'personalRulesLoaded': + // 加载个人规则数据 + if (typeof loadPersonalRules === 'function') { + loadPersonalRules(message.data); + } + break; + case 'autoSendMessage': // 自动发送待发送的消息(登录后) console.log('[WebView] 自动发送待发送消息:', message.text);