diff --git a/docs/system-notification-implementation.md b/docs/system-notification-implementation.md new file mode 100644 index 0000000..529444c --- /dev/null +++ b/docs/system-notification-implementation.md @@ -0,0 +1,911 @@ +# IC Coder 系统通知功能实现方案 + +## 目录 + +- [1. 需求背景](#1-需求背景) +- [2. 技术方案对比](#2-技术方案对比) +- [3. 推荐方案详解](#3-推荐方案详解) +- [4. 实现步骤](#4-实现步骤) +- [5. API 设计](#5-api-设计) +- [6. 配置选项](#6-配置选项) +- [7. 测试方案](#7-测试方案) +- [8. 注意事项](#8-注意事项) +- [9. 常见问题](#9-常见问题) + +--- + +## 1. 需求背景 + +### 1.1 问题描述 + +当前 IC Coder 插件使用 VS Code 内置的通知 API (`vscode.window.showInformationMessage`) 来提示用户任务完成。这种方式存在以下问题: + +- **可见性问题**: 用户切换到其他应用时,无法看到 VS Code 内部的通知 +- **错过通知**: 长时间运行的任务(如 iverilog 仿真)完成时,用户可能已经离开 VS Code +- **用户体验**: 需要用户主动回到 VS Code 才能知道任务状态 + +### 1.2 目标 + +实现系统级通知功能,使得: + +1. 用户在任何应用中都能收到任务完成通知 +2. 通知显示在操作系统的通知中心(Windows Action Center / macOS Notification Center / Linux notify-send) +3. 支持自定义通知内容、图标、声音 +4. 用户可以配置是否启用系统通知 + +--- + +## 2. 技术方案对比 + +### 2.1 方案一:node-notifier(推荐) + +**描述**: 使用 `node-notifier` 库,封装了各平台的原生通知 API + +**优点**: +- ✅ 跨平台支持(Windows/macOS/Linux) +- ✅ API 简单易用 +- ✅ 支持自定义图标、声音、操作按钮 +- ✅ 活跃维护,社区支持良好 +- ✅ 支持通知点击回调 + +**缺点**: +- ❌ 需要添加额外依赖(~500KB) +- ❌ 首次使用需要用户授权 + +**适用场景**: 需要跨平台支持的生产环境 + +--- + +### 2.2 方案二:Windows PowerShell Toast 通知 + +**描述**: 使用 PowerShell 脚本调用 Windows 10/11 的 Toast 通知 API + +**优点**: +- ✅ 无需额外依赖 +- ✅ 支持丰富的 Toast 样式(按钮、输入框等) +- ✅ 与 Windows 系统深度集成 + +**缺点**: +- ❌ 仅支持 Windows 10/11 +- ❌ 需要执行 PowerShell 脚本,可能有安全限制 +- ❌ 实现复杂度较高 + +**适用场景**: 仅针对 Windows 平台的专用功能 + +--- + +### 2.3 方案三:Electron Notification API + +**描述**: 使用 Electron 的 `Notification` API(VS Code 基于 Electron) + +**优点**: +- ✅ 无需额外依赖 +- ✅ 跨平台支持 +- ✅ API 简洁 + +**缺点**: +- ❌ VS Code 扩展 API 未直接暴露 Electron API +- ❌ 需要通过 `@vscode/webview-ui-toolkit` 或其他方式间接调用 +- ❌ 可能存在兼容性问题 + +**适用场景**: 理论可行,但实际受限于 VS Code 扩展沙箱 + +--- + +### 2.4 方案四:结合 VS Code 通知 + 系统通知 + +**描述**: 同时使用 VS Code 内置通知和系统通知 + +**优点**: +- ✅ 双重保障,覆盖所有场景 +- ✅ 用户在 VS Code 内外都能看到 + +**缺点**: +- ❌ 可能显得冗余 +- ❌ 需要处理两种通知的协调逻辑 + +**适用场景**: 对通知可靠性要求极高的场景 + +--- + +### 2.5 方案对比表 + +| 方案 | 跨平台 | 依赖大小 | 实现难度 | 用户体验 | 推荐度 | +|------|--------|----------|----------|----------|--------| +| node-notifier | ✅ | ~500KB | ⭐ 低 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| PowerShell Toast | ❌ Windows Only | 0 | ⭐⭐⭐ 高 | ⭐⭐⭐⭐ | ⭐⭐ | +| Electron API | ✅ | 0 | ⭐⭐⭐⭐ 很高 | ⭐⭐⭐ | ⭐ | +| 双重通知 | ✅ | ~500KB | ⭐⭐ 中 | ⭐⭐⭐⭐ | ⭐⭐⭐ | + +--- + +## 3. 推荐方案详解 + +### 3.1 选择 node-notifier 的理由 + +1. **成熟稳定**: 被广泛使用(npm 周下载量 > 200 万) +2. **跨平台**: 自动适配不同操作系统的通知机制 +3. **功能丰富**: 支持图标、声音、操作按钮、回调 +4. **易于集成**: 与 VS Code 扩展开发无缝集成 + +### 3.2 node-notifier 工作原理 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ IC Coder Extension │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ notificationService.ts │ │ +│ │ ┌──────────────────────────────────────────────────┐ │ │ +│ │ │ sendSystemNotification(title, message, options) │ │ │ +│ │ └──────────────────┬───────────────────────────────┘ │ │ +│ └────────────────────┼──────────────────────────────────┘ │ +└────────────────────────┼─────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ node-notifier │ + │ (跨平台适配层) │ + └──────────┬───────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌──────────┐ ┌──────────┐ + │ Windows │ │ macOS │ │ Linux │ + │ Toast │ │ NSUser │ │ notify- │ + │ Notif. │ │ Notif. │ │ send │ + └─────────┘ └──────────┘ └──────────┘ +``` + +### 3.3 各平台通知效果 + +#### Windows 10/11 +- 显示在右下角 Action Center +- 支持应用图标、标题、消息、操作按钮 +- 可以播放系统声音 +- 通知历史保存在通知中心 + +#### macOS +- 显示在右上角 Notification Center +- 支持应用图标、标题、副标题、消息 +- 可以播放系统声音 +- 支持回复和操作按钮 + +#### Linux +- 使用 `notify-send` 或 `libnotify` +- 显示位置取决于桌面环境(GNOME/KDE/XFCE) +- 支持图标、标题、消息、紧急程度 + +--- + +## 4. 实现步骤 + +### 4.1 安装依赖 + +```bash +# 安装 node-notifier +pnpm add node-notifier + +# 安装类型定义 +pnpm add -D @types/node-notifier +``` + +### 4.2 创建通知服务模块 + +创建 `src/services/notificationService.ts` + +```typescript +import * as notifier from 'node-notifier'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +/** + * 通知类型枚举 + */ +export enum NotificationType { + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error' +} + +/** + * 通知选项接口 + */ +export interface NotificationOptions { + /** 通知标题 */ + title: string; + /** 通知消息 */ + message: string; + /** 通知类型 */ + type?: NotificationType; + /** 是否播放声音 */ + sound?: boolean; + /** 超时时间(秒),0 表示不自动消失 */ + timeout?: number; + /** 自定义图标路径 */ + icon?: string; + /** 点击通知时的回调 */ + onClick?: () => void; +} + +/** + * 系统通知服务类 + */ +export class NotificationService { + private static instance: NotificationService; + private readonly extensionPath: string; + private readonly iconPath: string; + + private constructor(context: vscode.ExtensionContext) { + this.extensionPath = context.extensionPath; + this.iconPath = path.join(this.extensionPath, 'resources', 'icon.png'); + } + + /** + * 获取单例实例 + */ + public static getInstance(context?: vscode.ExtensionContext): NotificationService { + if (!NotificationService.instance && context) { + NotificationService.instance = new NotificationService(context); + } + return NotificationService.instance; + } + + /** + * 检查是否启用系统通知 + */ + private isSystemNotificationEnabled(): boolean { + const config = vscode.workspace.getConfiguration('ic-coder'); + return config.get('enableSystemNotification', true); + } + + /** + * 发送系统通知 + */ + public sendNotification(options: NotificationOptions): void { + // 检查用户配置 + if (!this.isSystemNotificationEnabled()) { + console.log('[NotificationService] 系统通知已禁用'); + return; + } + + const { + title, + message, + type = NotificationType.INFO, + sound = true, + timeout = 10, + icon, + onClick + } = options; + + // 准备通知参数 + const notificationConfig: notifier.Notification = { + title: title, + message: message, + icon: icon || this.iconPath, + sound: sound, + wait: false, + timeout: timeout, + appID: 'IC Coder' // Windows 10/11 需要 + }; + + // 发送通知 + notifier.notify(notificationConfig, (err, response, metadata) => { + if (err) { + console.error('[NotificationService] 通知发送失败:', err); + // 降级到 VS Code 内置通知 + this.fallbackToVSCodeNotification(title, message, type); + return; + } + + console.log('[NotificationService] 通知已发送:', response, metadata); + }); + + // 监听通知点击事件 + if (onClick) { + notifier.on('click', (notifierObject, options, event) => { + onClick(); + }); + } + } + + /** + * 降级到 VS Code 内置通知 + */ + private fallbackToVSCodeNotification( + title: string, + message: string, + type: NotificationType + ): void { + const fullMessage = `${title}: ${message}`; + + switch (type) { + case NotificationType.ERROR: + vscode.window.showErrorMessage(fullMessage); + break; + case NotificationType.WARNING: + vscode.window.showWarningMessage(fullMessage); + break; + case NotificationType.SUCCESS: + case NotificationType.INFO: + default: + vscode.window.showInformationMessage(fullMessage); + break; + } + } + + /** + * 发送成功通知 + */ + public success(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.SUCCESS, + sound: true, + timeout: 10, + onClick + }); + } + + /** + * 发送错误通知 + */ + public error(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.ERROR, + sound: true, + timeout: 15, + onClick + }); + } + + /** + * 发送警告通知 + */ + public warning(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.WARNING, + sound: true, + timeout: 10, + onClick + }); + } + + /** + * 发送信息通知 + */ + public info(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.INFO, + sound: false, + timeout: 8, + onClick + }); + } +} +``` + +### 4.3 在扩展入口初始化服务 + +修改 `src/extension.ts` + +```typescript +import { NotificationService } from './services/notificationService'; + +export function activate(context: vscode.ExtensionContext) { + // 初始化通知服务 + const notificationService = NotificationService.getInstance(context); + + // ... 其他初始化代码 +} +``` + +### 4.4 在消息处理器中使用 + +修改 `src/utils/messageHandler.ts` + +```typescript +import { NotificationService } from '../services/notificationService'; + +// 在适当的位置添加通知 +export async function handleMessage(message: any, panel: vscode.WebviewPanel) { + const notificationService = NotificationService.getInstance(); + + // 示例:iverilog 仿真完成 + if (message.type === 'simulationComplete') { + notificationService.success( + 'IC Coder - 仿真完成', + 'iverilog 仿真已成功完成,VCD 文件已生成', + () => { + // 点击通知时聚焦到 VS Code + vscode.window.showTextDocument(vscode.window.activeTextEditor!.document); + } + ); + } + + // 示例:仿真失败 + if (message.type === 'simulationError') { + notificationService.error( + 'IC Coder - 仿真失败', + `仿真过程中发生错误: ${message.error}`, + () => { + // 点击通知时打开输出面板 + panel.reveal(); + } + ); + } +} +``` + +### 4.5 添加配置项 + +修改 `package.json` + +```json +{ + "contributes": { + "configuration": { + "title": "IC Coder", + "properties": { + "ic-coder.enableSystemNotification": { + "type": "boolean", + "default": true, + "description": "启用系统级通知(任务完成时显示操作系统通知)" + }, + "ic-coder.notificationSound": { + "type": "boolean", + "default": true, + "description": "通知时播放系统声音" + }, + "ic-coder.notificationTimeout": { + "type": "number", + "default": 10, + "minimum": 0, + "maximum": 60, + "description": "通知自动消失时间(秒),0 表示不自动消失" + } + } + } + } +} +``` + +--- + +## 5. API 设计 + +### 5.1 核心 API + +#### `NotificationService.getInstance(context?)` + +获取通知服务单例实例 + +**参数**: +- `context` (可选): `vscode.ExtensionContext` - 扩展上下文,首次调用时必须提供 + +**返回**: `NotificationService` 实例 + +**示例**: +```typescript +const notificationService = NotificationService.getInstance(context); +``` + +--- + +#### `sendNotification(options)` + +发送自定义通知 + +**参数**: +- `options`: `NotificationOptions` - 通知选项对象 + +**返回**: `void` + +**示例**: +```typescript +notificationService.sendNotification({ + title: 'IC Coder', + message: '任务已完成', + type: NotificationType.SUCCESS, + sound: true, + timeout: 10, + onClick: () => { + console.log('用户点击了通知'); + } +}); +``` + +--- + +#### `success(title, message, onClick?)` + +发送成功通知(快捷方法) + +**参数**: +- `title`: `string` - 通知标题 +- `message`: `string` - 通知消息 +- `onClick` (可选): `() => void` - 点击回调 + +**示例**: +```typescript +notificationService.success( + 'IC Coder', + 'VCD 文件生成成功', + () => panel.reveal() +); +``` + +--- + +#### `error(title, message, onClick?)` + +发送错误通知(快捷方法) + +**参数**: +- `title`: `string` - 通知标题 +- `message`: `string` - 通知消息 +- `onClick` (可选): `() => void` - 点击回调 + +**示例**: +```typescript +notificationService.error( + 'IC Coder', + '编译失败: 语法错误', + () => vscode.commands.executeCommand('workbench.action.showErrorsWarnings') +); +``` + +--- + +#### `warning(title, message, onClick?)` + +发送警告通知(快捷方法) + +--- + +#### `info(title, message, onClick?)` + +发送信息通知(快捷方法) + +--- + +### 5.2 类型定义 + +```typescript +enum NotificationType { + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error' +} + +interface NotificationOptions { + title: string; + message: string; + type?: NotificationType; + sound?: boolean; + timeout?: number; + icon?: string; + onClick?: () => void; +} +``` + +--- + +## 6. 配置选项 + +### 6.1 用户配置项 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `ic-coder.enableSystemNotification` | `boolean` | `true` | 是否启用系统通知 | +| `ic-coder.notificationSound` | `boolean` | `true` | 是否播放通知声音 | +| `ic-coder.notificationTimeout` | `number` | `10` | 通知自动消失时间(秒) | + +### 6.2 配置方式 + +#### 方式 1: VS Code 设置界面 + +1. 打开 VS Code 设置 (`Ctrl+,` / `Cmd+,`) +2. 搜索 "IC Coder" +3. 找到 "Enable System Notification" 选项 +4. 勾选或取消勾选 + +#### 方式 2: settings.json + +```json +{ + "ic-coder.enableSystemNotification": true, + "ic-coder.notificationSound": true, + "ic-coder.notificationTimeout": 10 +} +``` + +--- + +## 7. 测试方案 + +### 7.1 单元测试 + +创建 `src/test/suite/notificationService.test.ts` + +```typescript +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { NotificationService, NotificationType } from '../../services/notificationService'; + +suite('NotificationService Test Suite', () => { + let notificationService: NotificationService; + + suiteSetup(() => { + const context = { + extensionPath: __dirname + } as vscode.ExtensionContext; + notificationService = NotificationService.getInstance(context); + }); + + test('应该成功创建单例实例', () => { + const instance1 = NotificationService.getInstance(); + const instance2 = NotificationService.getInstance(); + assert.strictEqual(instance1, instance2); + }); + + test('应该发送成功通知', (done) => { + notificationService.success('测试标题', '测试消息'); + setTimeout(() => done(), 1000); + }); +}); +``` + +### 7.2 手动测试清单 + +#### Windows 测试 +- [ ] 通知显示在 Action Center +- [ ] 点击通知能够聚焦到 VS Code +- [ ] 通知声音正常播放 +- [ ] 通知图标正确显示 +- [ ] 通知在设定时间后自动消失 +- [ ] 禁用系统通知后不再显示 + +--- + +## 8. 注意事项 + +### 8.1 权限问题 + +**Windows**: +- 首次使用时,Windows 可能会弹出权限请求 +- 用户需要在"设置 > 系统 > 通知和操作"中允许应用通知 + +**macOS**: +- 需要在"系统偏好设置 > 通知"中允许 VS Code 发送通知 + +**Linux**: +- 需要安装 `libnotify-bin` 包 +- 不同桌面环境的通知样式可能不同 + +### 8.2 通知频率控制 + +为避免通知轰炸,建议实现防抖机制: + +```typescript +export class NotificationService { + private lastNotificationTime: Map = new Map(); + private readonly DEBOUNCE_INTERVAL = 3000; // 3 秒 + + private shouldSendNotification(key: string): boolean { + const now = Date.now(); + const lastTime = this.lastNotificationTime.get(key) || 0; + + if (now - lastTime < this.DEBOUNCE_INTERVAL) { + return false; + } + + this.lastNotificationTime.set(key, now); + return true; + } +} +``` + +### 8.3 错误处理 + +通知发送失败时,自动降级到 VS Code 内置通知。 + +### 8.4 安全考虑 + +- **不要在通知中显示敏感信息**(如 token、密码) +- **验证通知内容**,防止 XSS 攻击 +- **限制通知频率**,防止滥用 + +--- + +## 9. 常见问题 + +### 9.1 通知不显示 + +**问题**: 调用通知 API 后,系统没有显示通知 + +**可能原因**: +1. 用户禁用了系统通知权限 +2. 操作系统的"勿扰模式"已启用 +3. `node-notifier` 安装失败或版本不兼容 + +**解决方案**: +```typescript +// 添加调试日志 +notifier.notify(notificationConfig, (err, response, metadata) => { + if (err) { + console.error('[NotificationService] 错误:', err); + } else { + console.log('[NotificationService] 响应:', response); + } +}); +``` + +### 9.2 通知点击回调不触发 + +**问题**: 点击通知后,`onClick` 回调没有执行 + +**解决方案**: +```typescript +// 设置 wait: true +const notificationConfig: notifier.Notification = { + title: title, + message: message, + wait: true, // 等待用户交互 +}; +``` + +### 9.3 通知图标不显示 + +**问题**: 通知显示时没有自定义图标 + +**解决方案**: +```typescript +import * as fs from 'fs'; + +// 检查图标是否存在 +if (!fs.existsSync(this.iconPath)) { + console.warn(`图标文件不存在: ${this.iconPath}`); + this.iconPath = ''; // 使用系统默认图标 +} +``` + +### 9.4 Linux 上通知不工作 + +**问题**: 在 Linux 系统上通知无法显示 + +**解决方案**: +```bash +# Ubuntu/Debian +sudo apt-get install libnotify-bin + +# Fedora/RHEL +sudo dnf install libnotify +``` + +--- + +## 10. 最佳实践 + +### 10.1 通知时机 + +**推荐发送通知的场景**: +- ✅ 长时间运行的任务完成(> 10 秒) +- ✅ 后台任务完成(用户可能已切换到其他应用) +- ✅ 发生错误需要用户关注 +- ✅ 重要状态变更 + +**不推荐发送通知的场景**: +- ❌ 即时完成的操作(< 3 秒) +- ❌ 用户主动触发且立即完成的操作 +- ❌ 频繁发生的事件(如自动保存) +- ❌ 调试信息或日志 + +### 10.2 通知内容 + +**标题**: +- 简洁明了,不超过 20 个字符 +- 包含应用名称(如 "IC Coder - 仿真完成") +- 使用动作完成时态("已完成" 而不是 "完成中") + +**消息**: +- 提供具体信息,不超过 100 个字符 +- 包含关键细节(如文件名、错误类型) +- 避免技术术语,使用用户友好的语言 + +**示例**: +```typescript +// ✅ 好的通知 +notificationService.success( + 'IC Coder - 仿真完成', + 'testbench.v 仿真成功,VCD 文件已生成' +); + +// ❌ 不好的通知 +notificationService.success('完成', '操作已完成'); +``` + +### 10.3 通知优先级 + +根据重要性设置不同的通知类型和超时时间: + +```typescript +// 高优先级:错误(15 秒) +notificationService.error( + 'IC Coder - 编译失败', + '发现 3 个语法错误,请检查代码' +); + +// 中优先级:警告(10 秒) +notificationService.warning( + 'IC Coder - 警告', + '仿真时间过长,可能存在死循环' +); + +// 低优先级:信息(8 秒,无声音) +notificationService.info( + 'IC Coder - 提示', + '已自动保存工作区' +); +``` + +--- + +## 11. 性能指标 + +### 11.1 预期性能 + +| 指标 | 目标值 | 说明 | +|------|--------|------| +| 通知发送延迟 | < 100ms | 从调用到系统显示 | +| 内存占用 | < 5MB | 通知服务常驻内存 | +| CPU 占用 | < 1% | 空闲时 CPU 使用率 | +| 包体积增加 | ~500KB | node-notifier 依赖 | + +--- + +## 12. 参考资料 + +### 12.1 官方文档 + +- [node-notifier GitHub](https://github.com/mikaelbr/node-notifier) +- [VS Code Extension API](https://code.visualstudio.com/api) +- [Windows Toast Notifications](https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-notifications-overview) + +### 12.2 相关文章 + +- [Best Practices for Desktop Notifications](https://web.dev/notifications/) +- [Designing Better Notifications](https://uxdesign.cc/designing-better-notifications-36ba9c0b3e0e) + +--- + +## 13. 总结 + +本文档详细介绍了在 IC Coder 插件中实现系统级通知功能的完整方案,包括: + +✅ **技术选型**: 选择 `node-notifier` 作为跨平台通知解决方案 +✅ **架构设计**: 单例模式的通知服务类,支持多种通知类型 +✅ **实现细节**: 完整的代码示例和配置说明 +✅ **测试方案**: 单元测试、集成测试和手动测试清单 +✅ **最佳实践**: 通知时机、内容设计和用户体验优化 +✅ **故障排查**: 常见问题和解决方案 + +通过实现系统级通知,IC Coder 插件能够在用户切换到其他应用时仍然及时通知任务状态,显著提升用户体验。 + +--- + +**文档版本**: v1.0 +**最后更新**: 2026-01-26 +**作者**: IC Coder Team +**许可**: MIT License + diff --git a/package.json b/package.json index dde9723..b52f763 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,11 @@ "command": "ic-coder.openVCDViewer", "title": "打开 VCD 波形查看器", "category": "IC Coder" + }, + { + "command": "ic-coder.testNotification", + "title": "测试系统通知", + "category": "IC Coder" } ], "viewsContainers": { @@ -86,7 +91,29 @@ ], "priority": "default" } - ] + ], + "configuration": { + "title": "IC Coder", + "properties": { + "ic-coder.enableSystemNotification": { + "type": "boolean", + "default": true, + "description": "启用系统级通知(任务完成时显示操作系统通知)" + }, + "ic-coder.notificationSound": { + "type": "boolean", + "default": true, + "description": "通知时播放系统声音" + }, + "ic-coder.notificationTimeout": { + "type": "number", + "default": 10, + "minimum": 0, + "maximum": 60, + "description": "通知自动消失时间(秒),0 表示不自动消失" + } + } + } }, "scripts": { "vscode:prepublish": "pnpm run package", @@ -103,6 +130,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", + "@types/node-notifier": "^8.0.5", "@types/vscode": "^1.80.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", @@ -126,6 +154,7 @@ "@wavedrom/doppler": "^1.14.0", "eventsource-parser": "^3.0.6", "iconv-lite": "^0.7.1", + "node-notifier": "^10.0.1", "onml": "^2.1.0", "style-mod": "^4.1.3", "vcd-stream": "^1.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 456d0ca..40e1bf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: iconv-lite: specifier: ^0.7.1 version: 0.7.1 + node-notifier: + specifier: ^10.0.1 + version: 10.0.1 onml: specifier: ^2.1.0 version: 2.1.0 @@ -39,6 +42,9 @@ importers: '@types/node': specifier: 22.x version: 22.19.2 + '@types/node-notifier': + specifier: ^8.0.5 + version: 8.0.5 '@types/vscode': specifier: ^1.80.0 version: 1.107.0 @@ -349,6 +355,9 @@ packages: '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + '@types/node-notifier@8.0.5': + resolution: {integrity: sha512-LX7+8MtTsv6szumAp6WOy87nqMEdGhhry/Qfprjm1Ma6REjVzeF7SCyvPtp5RaF6IkXCS9V4ra8g5fwvf2ZAYg==} + '@types/node@22.19.2': resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==} @@ -1185,6 +1194,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1284,6 +1296,11 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1342,6 +1359,10 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -1622,6 +1643,9 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -1919,6 +1943,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -2702,6 +2729,10 @@ snapshots: '@types/mocha@10.0.10': {} + '@types/node-notifier@8.0.5': + dependencies: + '@types/node': 22.19.2 + '@types/node@22.19.2': dependencies: undici-types: 6.21.0 @@ -3646,6 +3677,8 @@ snapshots: graceful-fs@4.2.11: {} + growly@1.3.0: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -3737,6 +3770,8 @@ snapshots: dependencies: hasown: 2.0.2 + is-docker@2.2.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -3771,6 +3806,10 @@ snapshots: is-unicode-supported@2.1.0: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -4074,6 +4113,15 @@ snapshots: node-addon-api@4.3.0: optional: true + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.7.3 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + node-releases@2.0.27: {} node-sarif-builder@3.3.1: @@ -4395,6 +4443,8 @@ snapshots: shebang-regex@3.0.0: {} + shellwords@0.1.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 diff --git a/src/extension.ts b/src/extension.ts index 64cf6c4..05491bc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,10 +8,15 @@ import { VCDFileServer } from "./services/vcdFileServer"; import { initUserService } from "./services/userService"; import { initCreditsService } from "./services/creditsService"; import { isTokenExpired } from "./utils/jwtUtils"; +import { NotificationService } from "./services/notificationService"; export async function activate(context: vscode.ExtensionContext) { console.log("🎉 IC Coder 插件已激活!"); + // 初始化通知服务 + const notificationService = NotificationService.getInstance(context); + console.log('[Extension] 通知服务已初始化'); + // 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session const storedSessions = context.globalState.get('icCoderSessions', []); console.log('[Extension] 检查 sessions 数量:', storedSessions.length); @@ -191,6 +196,28 @@ export async function activate(context: vscode.ExtensionContext) { } ); + // 注册命令:测试系统通知 + const testNotificationCommand = vscode.commands.registerCommand( + "ic-coder.testNotification", + () => { + console.log('[Extension] ========== 测试通知命令被调用 =========='); + + // 先显示 VS Code 通知确认命令执行 + vscode.window.showInformationMessage('正在测试系统通知...'); + + // 发送系统通知 + notificationService.success( + 'IC Coder - 测试通知', + '系统通知功能正常工作!', + () => { + vscode.window.showInformationMessage('您点击了系统通知!'); + } + ); + + console.log('[Extension] 测试通知命令执行完成'); + } + ); + // 注册命令:查看会话历史 // TODO: 这些命令需要根据新的任务架构重新实现 // 暂时注释掉,等待重新实现 @@ -256,6 +283,7 @@ export async function activate(context: vscode.ExtensionContext) { openVCDViewerInBrowserCommand, loginCommand, logoutCommand, + testNotificationCommand, // TODO: 等待重新实现这些命令 // viewHistoryCommand, // newSessionCommand, diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts new file mode 100644 index 0000000..db18ee0 --- /dev/null +++ b/src/services/notificationService.ts @@ -0,0 +1,257 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +// 使用 require 导入 node-notifier +const notifier = require('node-notifier'); + +/** + * 通知类型枚举 + */ +export enum NotificationType { + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error' +} + +/** + * 通知选项接口 + */ +export interface NotificationOptions { + /** 通知标题 */ + title: string; + /** 通知消息 */ + message: string; + /** 通知类型 */ + type?: NotificationType; + /** 是否播放声音 */ + sound?: boolean; + /** 超时时间(秒),0 表示不自动消失 */ + timeout?: number; + /** 自定义图标路径 */ + icon?: string; + /** 点击通知时的回调 */ + onClick?: () => void; +} + +/** + * 系统通知服务类 + */ +export class NotificationService { + private static instance: NotificationService; + private readonly extensionPath: string; + private readonly iconPath: string; + private lastNotificationTime: Map = new Map(); + private readonly DEBOUNCE_INTERVAL = 3000; // 3 秒防抖 + + private constructor(context: vscode.ExtensionContext) { + this.extensionPath = context.extensionPath; + this.iconPath = path.join(this.extensionPath, 'media', 'icon.png'); + console.log('[NotificationService] 初始化通知服务'); + console.log('[NotificationService] 扩展路径:', this.extensionPath); + console.log('[NotificationService] 图标路径:', this.iconPath); + } + + /** + * 获取单例实例 + */ + public static getInstance(context?: vscode.ExtensionContext): NotificationService { + if (!NotificationService.instance && context) { + NotificationService.instance = new NotificationService(context); + } + return NotificationService.instance; + } + + /** + * 检查是否启用系统通知 + */ + private isSystemNotificationEnabled(): boolean { + const config = vscode.workspace.getConfiguration('ic-coder'); + return config.get('enableSystemNotification', true); + } + + /** + * 检查是否应该发送通知(防抖) + */ + private shouldSendNotification(key: string): boolean { + const now = Date.now(); + const lastTime = this.lastNotificationTime.get(key) || 0; + + if (now - lastTime < this.DEBOUNCE_INTERVAL) { + return false; + } + + this.lastNotificationTime.set(key, now); + return true; + } + + /** + * 发送系统通知 + */ + public sendNotification(options: NotificationOptions): void { + console.log('[NotificationService] ========== 开始发送通知 =========='); + console.log('[NotificationService] 通知选项:', options); + + // 检查用户配置 + if (!this.isSystemNotificationEnabled()) { + console.log('[NotificationService] 系统通知已禁用'); + return; + } + console.log('[NotificationService] 系统通知已启用'); + + const { + title, + message, + type = NotificationType.INFO, + onClick + } = options; + + // 防抖检查 + const notificationKey = `${title}-${message}`; + if (!this.shouldSendNotification(notificationKey)) { + console.log('[NotificationService] 通知被防抖机制拦截'); + return; + } + console.log('[NotificationService] 通过防抖检查'); + + // 使用 node-notifier 发送系统通知 + console.log('[NotificationService] 使用 node-notifier 发送系统通知'); + + try { + const notificationConfig: any = { + title: title, + message: message, + sound: true, + wait: false, + timeout: 10, + appID: 'IC Coder' + }; + + // Windows 特定配置 + if (process.platform === 'win32') { + notificationConfig.icon = this.iconPath; + console.log('[NotificationService] Windows 平台,图标路径:', this.iconPath); + } + + console.log('[NotificationService] 通知配置:', notificationConfig); + + notifier.notify(notificationConfig, (err: any, response: any, metadata: any) => { + if (err) { + console.error('[NotificationService] ❌ node-notifier 失败:', err); + } else { + console.log('[NotificationService] ✅ node-notifier 成功'); + console.log('[NotificationService] 响应:', response); + console.log('[NotificationService] 元数据:', metadata); + } + }); + + if (onClick) { + notifier.on('click', () => { + console.log('[NotificationService] 用户点击了系统通知'); + onClick(); + }); + } + } catch (error) { + console.error('[NotificationService] ❌ node-notifier 异常:', error); + // 如果系统通知失败,显示 VS Code 内置通知作为备用 + console.log('[NotificationService] 系统通知失败,显示 VS Code 内置通知'); + this.showVSCodeNotification(title, message, type, onClick); + } + } + + /** + * 显示 VS Code 内置通知 + */ + private showVSCodeNotification( + title: string, + message: string, + type: NotificationType, + onClick?: () => void + ): void { + const fullMessage = `${title}: ${message}`; + console.log('[NotificationService] 显示 VS Code 通知:', fullMessage); + + let notificationPromise: Thenable; + + switch (type) { + case NotificationType.ERROR: + notificationPromise = vscode.window.showErrorMessage(fullMessage, '查看详情'); + break; + case NotificationType.WARNING: + notificationPromise = vscode.window.showWarningMessage(fullMessage, '查看详情'); + break; + case NotificationType.SUCCESS: + case NotificationType.INFO: + default: + notificationPromise = vscode.window.showInformationMessage(fullMessage, '查看详情'); + break; + } + + // 处理点击事件 + if (onClick) { + notificationPromise.then((selection) => { + if (selection === '查看详情') { + console.log('[NotificationService] 用户点击了通知'); + onClick(); + } + }); + } + } + + /** + * 发送成功通知 + */ + public success(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.SUCCESS, + sound: true, + timeout: 10, + onClick + }); + } + + /** + * 发送错误通知 + */ + public error(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.ERROR, + sound: true, + timeout: 15, + onClick + }); + } + + /** + * 发送警告通知 + */ + public warning(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.WARNING, + sound: true, + timeout: 10, + onClick + }); + } + + /** + * 发送信息通知 + */ + public info(title: string, message: string, onClick?: () => void): void { + this.sendNotification({ + title, + message, + type: NotificationType.INFO, + sound: false, + timeout: 8, + onClick + }); + } + +} diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 5fb0743..f7647f1 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -24,6 +24,7 @@ import { fetchBalance, } from "../services/creditsService"; import { optimizePrompt } from "../services/promptOptimizeService"; +import { NotificationService } from "../services/notificationService"; import type { RunMode, ServiceTier } from "../types/api"; @@ -333,6 +334,17 @@ async function handleUserMessageWithBackend( isComplete: true, }); console.log("[MessageHandler] postMessage 返回值:", result); + + // 发送系统通知 - AI 响应完成 + const notificationService = NotificationService.getInstance(); + notificationService.success( + 'IC Coder - AI 响应完成', + '您的问题已得到回复,点击查看详情', + () => { + // 点击通知时聚焦到面板 + panel.reveal(); + } + ); } catch (error) { console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error); } @@ -845,6 +857,16 @@ export async function handleCreateFile( message: " 文件创建成功", }); vscode.window.showInformationMessage(`文件创建成功: ${filePath}`); + + // 发送系统通知 + const notificationService = NotificationService.getInstance(); + notificationService.success( + 'IC Coder - 文件创建', + `文件已创建: ${path.basename(filePath)}`, + () => { + vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath)); + } + ); } catch (error) { panel.webview.postMessage({ command: "fileCreateError", @@ -872,6 +894,13 @@ export async function handleUpdateFile( message: " 文件更新成功", }); vscode.window.showInformationMessage(`文件更新成功: ${filePath}`); + + // 发送系统通知 + const notificationService = NotificationService.getInstance(); + notificationService.info( + 'IC Coder - 文件更新', + `文件已更新: ${path.basename(filePath)}` + ); } catch (error) { panel.webview.postMessage({ command: "fileUpdateError", @@ -1079,6 +1108,17 @@ async function handleVCDGeneration( }); vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`); + + // 发送系统通知 + const notificationService = NotificationService.getInstance(); + notificationService.success( + 'IC Coder - 仿真完成', + `VCD 文件已生成: ${fileName}`, + () => { + // 点击通知时打开 VCD 查看器 + vscode.commands.executeCommand('ic-coder.openVCDViewer', result.vcdFilePath); + } + ); } else { panel.webview.postMessage({ command: "receiveMessage", @@ -1102,6 +1142,17 @@ async function handleVCDGeneration( }); vscode.window.showErrorMessage("VCD 文件生成失败"); + + // 发送系统通知 + const notificationService = NotificationService.getInstance(); + notificationService.error( + 'IC Coder - 仿真失败', + 'VCD 文件生成失败,请查看错误信息', + () => { + // 点击通知时聚焦到面板 + panel.reveal(); + } + ); } } catch (error) { const errorMsg = `❌ 生成 VCD 文件时出错: ${ @@ -1114,6 +1165,16 @@ async function handleVCDGeneration( }); vscode.window.showErrorMessage(errorMsg); + + // 发送系统通知 + const notificationService = NotificationService.getInstance(); + notificationService.error( + 'IC Coder - 仿真错误', + error instanceof Error ? error.message : '生成 VCD 文件时出错', + () => { + panel.reveal(); + } + ); } } diff --git a/webpack.config.js b/webpack.config.js index 37d7024..f8060a9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,7 +20,8 @@ const extensionConfig = { libraryTarget: 'commonjs2' }, externals: { - vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + 'node-notifier': 'commonjs node-notifier' // node-notifier 依赖原生模块,必须排除 // modules added here also need to be added in the .vscodeignore file }, resolve: {