From 7b40485f60ccecfb2b46ffe757a6b0d6b87f2193 Mon Sep 17 00:00:00 2001 From: tangweijie <877588133@qq.com> Date: Mon, 5 Jan 2026 18:08:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20Monorepo?= =?UTF-8?q?=20=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 pnpm workspace 和 Turborepo 配置 - 创建 packages/shared 共享类型和工具 - 创建 packages/core-sdk 核心 SDK - 创建 packages/vscode-extension VSCode 插件 - 创建 packages/jetbrains-plugin JetBrains 插件基础结构 - 添加 README 文档 --- .editorconfig | 16 ++ .prettierrc | 10 + README.md | 152 +++++++++++- package.json | 44 ++++ packages/core-sdk/package.json | 31 +++ packages/core-sdk/src/collector.ts | 226 ++++++++++++++++++ .../core-sdk/src/collectors/chat-session.ts | 133 +++++++++++ .../src/collectors/code-completion.ts | 136 +++++++++++ packages/core-sdk/src/collectors/index.ts | 4 + .../core-sdk/src/collectors/user-behavior.ts | 86 +++++++ .../core-sdk/src/config/config-manager.ts | 102 ++++++++ packages/core-sdk/src/config/index.ts | 2 + packages/core-sdk/src/index.ts | 8 + .../core-sdk/src/privacy/code-sanitizer.ts | 55 +++++ packages/core-sdk/src/privacy/index.ts | 3 + .../core-sdk/src/privacy/privacy-handler.ts | 123 ++++++++++ .../core-sdk/src/reporters/batch-queue.ts | 104 ++++++++ .../core-sdk/src/reporters/http-reporter.ts | 97 ++++++++ packages/core-sdk/src/reporters/index.ts | 3 + packages/core-sdk/src/storage/event-queue.ts | 113 +++++++++ packages/core-sdk/src/storage/index.ts | 3 + .../core-sdk/src/storage/local-storage.ts | 72 ++++++ packages/core-sdk/tsconfig.json | 10 + packages/jetbrains-plugin/build.gradle.kts | 51 ++++ packages/jetbrains-plugin/settings.gradle.kts | 2 + .../com/devtools/collector/CollectorPlugin.kt | 23 ++ .../com/devtools/collector/models/Events.kt | 134 +++++++++++ .../collector/reporters/HttpReporter.kt | 65 +++++ .../collector/services/CollectorService.kt | 113 +++++++++ .../src/main/resources/META-INF/plugin.xml | 101 ++++++++ packages/shared/package.json | 28 +++ packages/shared/src/constants/index.ts | 104 ++++++++ packages/shared/src/index.ts | 5 + packages/shared/src/types/index.ts | 192 +++++++++++++++ packages/shared/src/utils/index.ts | 133 +++++++++++ packages/shared/tsconfig.json | 10 + packages/vscode-extension/package.json | 130 ++++++++++ .../src/adapters/completion-adapter.ts | 119 +++++++++ .../src/adapters/editor-adapter.ts | 101 ++++++++ .../src/config/settings-manager.ts | 81 +++++++ packages/vscode-extension/src/extension.ts | 177 ++++++++++++++ .../vscode-extension/src/ui/status-bar.ts | 70 ++++++ packages/vscode-extension/tsconfig.json | 12 + pnpm-workspace.yaml | 3 + tsconfig.base.json | 24 ++ turbo.json | 25 ++ 46 files changed, 3234 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierrc create mode 100644 package.json create mode 100644 packages/core-sdk/package.json create mode 100644 packages/core-sdk/src/collector.ts create mode 100644 packages/core-sdk/src/collectors/chat-session.ts create mode 100644 packages/core-sdk/src/collectors/code-completion.ts create mode 100644 packages/core-sdk/src/collectors/index.ts create mode 100644 packages/core-sdk/src/collectors/user-behavior.ts create mode 100644 packages/core-sdk/src/config/config-manager.ts create mode 100644 packages/core-sdk/src/config/index.ts create mode 100644 packages/core-sdk/src/index.ts create mode 100644 packages/core-sdk/src/privacy/code-sanitizer.ts create mode 100644 packages/core-sdk/src/privacy/index.ts create mode 100644 packages/core-sdk/src/privacy/privacy-handler.ts create mode 100644 packages/core-sdk/src/reporters/batch-queue.ts create mode 100644 packages/core-sdk/src/reporters/http-reporter.ts create mode 100644 packages/core-sdk/src/reporters/index.ts create mode 100644 packages/core-sdk/src/storage/event-queue.ts create mode 100644 packages/core-sdk/src/storage/index.ts create mode 100644 packages/core-sdk/src/storage/local-storage.ts create mode 100644 packages/core-sdk/tsconfig.json create mode 100644 packages/jetbrains-plugin/build.gradle.kts create mode 100644 packages/jetbrains-plugin/settings.gradle.kts create mode 100644 packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/CollectorPlugin.kt create mode 100644 packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/models/Events.kt create mode 100644 packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/reporters/HttpReporter.kt create mode 100644 packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/services/CollectorService.kt create mode 100644 packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/constants/index.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/types/index.ts create mode 100644 packages/shared/src/utils/index.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/vscode-extension/package.json create mode 100644 packages/vscode-extension/src/adapters/completion-adapter.ts create mode 100644 packages/vscode-extension/src/adapters/editor-adapter.ts create mode 100644 packages/vscode-extension/src/config/settings-manager.ts create mode 100644 packages/vscode-extension/src/extension.ts create mode 100644 packages/vscode-extension/src/ui/status-bar.ts create mode 100644 packages/vscode-extension/tsconfig.json create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json create mode 100644 turbo.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5c471dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.{kt,kts}] +indent_size = 4 + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..23a1e8f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "avoid" +} + diff --git a/README.md b/README.md index 0e640f3..a944d3d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,151 @@ -# ide-data-collector +# IDE Data Collector -多IDE数据采集插件 Monorepo - 核心SDK与各IDE插件统一管理 \ No newline at end of file +多IDE数据采集插件 Monorepo - 核心SDK与各IDE插件统一管理 + +## 项目介绍 + +IDE Data Collector 是一个用于采集AI编程工具使用数据的插件系统,支持多种主流IDE。通过采集代码补全、聊天会话等AI交互数据,帮助评估AI编程助手的效率和效果。 + +## 架构概览 + +``` +ide-data-collector/ +├── packages/ +│ ├── shared/ # 共享类型和工具函数 +│ ├── core-sdk/ # 核心SDK(数据采集、上报、隐私处理) +│ ├── vscode-extension/ # VSCode/Cursor 插件 +│ └── jetbrains-plugin/ # JetBrains IDE 插件 +├── docs/ # 文档 +└── scripts/ # 构建脚本 +``` + +## 功能特性 + +### 数据采集 +- 🔍 代码补全采纳/拒绝事件 +- 💬 AI聊天会话记录 +- 📊 开发者行为分析 +- ⏱️ 响应时间和效率指标 + +### 隐私保护 +- 🔒 用户ID匿名化 +- 🔐 代码内容脱敏 +- 🚫 敏感信息过滤 +- 📁 路径排除规则 + +### 多IDE支持 +- ✅ Visual Studio Code +- ✅ Cursor +- ✅ IntelliJ IDEA +- ✅ PyCharm +- ✅ WebStorm +- ✅ GoLand +- ✅ 其他 JetBrains IDE + +## 快速开始 + +### 环境要求 + +- Node.js >= 18.0.0 +- pnpm >= 8.0.0 +- JDK 17+ (JetBrains插件) + +### 安装依赖 + +```bash +# 安装所有依赖 +pnpm install + +# 构建所有包 +pnpm build +``` + +### 开发 + +```bash +# 启动开发模式 +pnpm dev + +# 运行测试 +pnpm test + +# 代码检查 +pnpm lint +``` + +### 构建VSCode插件 + +```bash +cd packages/vscode-extension +pnpm build +pnpm package +``` + +### 构建JetBrains插件 + +```bash +cd packages/jetbrains-plugin +./gradlew buildPlugin +``` + +## 配置说明 + +### VSCode 配置项 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `ideCollector.enabled` | boolean | true | 启用/禁用数据采集 | +| `ideCollector.apiEndpoint` | string | localhost:8000 | API端点地址 | +| `ideCollector.samplingRate` | number | 1.0 | 采样率 (0-1) | +| `ideCollector.batchSize` | number | 50 | 批量上报大小 | +| `ideCollector.flushInterval` | number | 60 | 刷新间隔(秒) | +| `ideCollector.anonymizeUser` | boolean | true | 匿名化用户ID | +| `ideCollector.obfuscateCode` | boolean | true | 混淆代码内容 | + +## 事件类型 + +| 事件类型 | 说明 | +|----------|------| +| `code_completion_shown` | 代码补全建议展示 | +| `code_completion_accepted` | 代码补全被接受 | +| `code_completion_rejected` | 代码补全被拒绝 | +| `chat_session_start` | 聊天会话开始 | +| `chat_message_sent` | 用户发送消息 | +| `chat_response_received` | 收到AI响应 | +| `chat_session_end` | 聊天会话结束 | + +## 数据格式 + +```json +{ + "eventId": "uuid", + "eventType": "code_completion_accepted", + "timestamp": "2024-01-05T10:30:00Z", + "userInfo": { + "userId": "anonymous-hash", + "ideType": "vscode", + "ideVersion": "1.85.0" + }, + "context": { + "filePath": ".../src/controller.js", + "language": "javascript" + }, + "aiInteraction": { + "provider": "github-copilot", + "latencyMs": 1200 + }, + "codeData": { + "suggestedCode": "...", + "accepted": true + } +} +``` + +## 相关仓库 + +- [collector-backend](../collector-backend) - 数据采集后端服务 +- [collector-dashboard](../collector-dashboard) - 数据分析看板 + +## 许可证 + +MIT License diff --git a/package.json b/package.json new file mode 100644 index 0000000..cab4e22 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "ide-data-collector", + "version": "0.1.0", + "private": true, + "description": "多IDE数据采集插件 Monorepo - 核心SDK与各IDE插件统一管理", + "repository": { + "type": "git", + "url": "https://gitea.devops.1msoft.cn/tangweijie/ide-data-collector.git" + }, + "author": "tangweijie", + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.0.0" + }, + "packageManager": "pnpm@8.15.0", + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev", + "lint": "turbo run lint", + "test": "turbo run test", + "clean": "turbo run clean && rm -rf node_modules", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", + "prepare": "husky install" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "husky": "^8.0.3", + "lint-staged": "^15.2.0", + "prettier": "^3.1.0", + "turbo": "^1.11.0", + "typescript": "^5.3.0" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx}": [ + "prettier --write", + "eslint --fix" + ], + "*.{json,md}": [ + "prettier --write" + ] + } +} + diff --git a/packages/core-sdk/package.json b/packages/core-sdk/package.json new file mode 100644 index 0000000..98f4bed --- /dev/null +++ b/packages/core-sdk/package.json @@ -0,0 +1,31 @@ +{ + "name": "@ide-collector/core-sdk", + "version": "0.1.0", + "description": "IDE数据采集核心SDK - 事件采集、上报、隐私处理", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts", + "dev": "tsup src/index.ts --format cjs,esm --dts --watch", + "clean": "rm -rf dist", + "lint": "eslint src --ext .ts", + "test": "vitest run" + }, + "dependencies": { + "@ide-collector/shared": "workspace:*" + }, + "devDependencies": { + "tsup": "^8.0.0", + "vitest": "^1.0.0" + } +} + diff --git a/packages/core-sdk/src/collector.ts b/packages/core-sdk/src/collector.ts new file mode 100644 index 0000000..4805d6a --- /dev/null +++ b/packages/core-sdk/src/collector.ts @@ -0,0 +1,226 @@ +import type { BaseEvent, CollectorConfig, UserInfo, CodeContext } from '@ide-collector/shared'; +import { DEFAULT_COLLECTOR_CONFIG, generateAnonymousUserId } from '@ide-collector/shared'; +import { EventQueue } from './storage/event-queue'; +import { HttpReporter } from './reporters/http-reporter'; +import { PrivacyHandler } from './privacy/privacy-handler'; +import { ConfigManager } from './config/config-manager'; + +/** + * IDE 数据采集器主类 + */ +export class Collector { + private static instance: Collector | null = null; + + private config: CollectorConfig; + private userInfo: UserInfo | null = null; + private eventQueue: EventQueue; + private reporter: HttpReporter; + private privacyHandler: PrivacyHandler; + private configManager: ConfigManager; + private flushTimer: ReturnType | null = null; + private isInitialized = false; + + private constructor(config: Partial = {}) { + this.config = { ...DEFAULT_COLLECTOR_CONFIG, ...config }; + this.eventQueue = new EventQueue(this.config.batchSize); + this.reporter = new HttpReporter(this.config.apiEndpoint); + this.privacyHandler = new PrivacyHandler(this.config.privacy); + this.configManager = new ConfigManager(); + } + + /** + * 获取单例实例 + */ + public static getInstance(config?: Partial): Collector { + if (!Collector.instance) { + Collector.instance = new Collector(config); + } + return Collector.instance; + } + + /** + * 初始化采集器 + */ + public async initialize(userInfo: Omit): Promise { + if (this.isInitialized) { + return; + } + + // 加载保存的配置 + const savedConfig = await this.configManager.loadConfig(); + if (savedConfig) { + this.config = { ...this.config, ...savedConfig }; + } + + // 生成或恢复用户ID + const savedUserId = await this.configManager.getUserId(); + this.userInfo = { + ...userInfo, + userId: savedUserId || generateAnonymousUserId(), + }; + + // 保存用户ID + if (!savedUserId) { + await this.configManager.saveUserId(this.userInfo.userId); + } + + // 恢复未发送的事件 + const pendingEvents = await this.eventQueue.loadFromStorage(); + if (pendingEvents.length > 0) { + console.log(`[Collector] Restored ${pendingEvents.length} pending events`); + } + + // 启动定时刷新 + this.startFlushTimer(); + + this.isInitialized = true; + console.log('[Collector] Initialized successfully'); + } + + /** + * 记录事件 + */ + public async trackEvent(event: Omit): Promise { + if (!this.isInitialized || !this.config.enabled) { + return; + } + + // 检查采样率 + if (Math.random() > this.config.samplingRate) { + return; + } + + // 检查事件类型是否需要采集 + if (!this.config.eventsToCapture.includes(event.eventType)) { + return; + } + + // 应用隐私处理 + const sanitizedEvent = this.privacyHandler.sanitizeEvent({ + ...event, + userInfo: this.userInfo!, + }); + + // 添加到队列 + await this.eventQueue.enqueue(sanitizedEvent); + + // 如果开启实时上报,立即发送 + if (this.config.realtimeEnabled) { + await this.flush(); + } + } + + /** + * 强制刷新队列 + */ + public async flush(): Promise { + if (!this.isInitialized) { + return; + } + + const events = await this.eventQueue.dequeueAll(); + if (events.length === 0) { + return; + } + + try { + await this.reporter.sendBatch(events); + console.log(`[Collector] Successfully sent ${events.length} events`); + } catch (error) { + console.error('[Collector] Failed to send events:', error); + // 重新入队失败的事件 + for (const event of events) { + await this.eventQueue.enqueue(event); + } + } + } + + /** + * 更新配置 + */ + public async updateConfig(config: Partial): Promise { + this.config = { ...this.config, ...config }; + this.privacyHandler = new PrivacyHandler(this.config.privacy); + this.reporter = new HttpReporter(this.config.apiEndpoint); + this.eventQueue.setMaxSize(this.config.batchSize); + + // 重启定时器 + this.stopFlushTimer(); + this.startFlushTimer(); + + // 保存配置 + await this.configManager.saveConfig(this.config); + } + + /** + * 获取当前配置 + */ + public getConfig(): CollectorConfig { + return { ...this.config }; + } + + /** + * 获取队列状态 + */ + public getQueueStatus(): { size: number; maxSize: number } { + return { + size: this.eventQueue.size(), + maxSize: this.config.batchSize, + }; + } + + /** + * 暂停采集 + */ + public pause(): void { + this.config.enabled = false; + this.stopFlushTimer(); + } + + /** + * 恢复采集 + */ + public resume(): void { + this.config.enabled = true; + this.startFlushTimer(); + } + + /** + * 销毁采集器 + */ + public async destroy(): Promise { + this.stopFlushTimer(); + await this.flush(); + this.isInitialized = false; + Collector.instance = null; + } + + /** + * 创建代码上下文 + */ + public createCodeContext(context: Partial): CodeContext { + return this.privacyHandler.sanitizeContext({ + filePath: context.filePath || '', + language: context.language || 'unknown', + ...context, + }); + } + + private startFlushTimer(): void { + if (this.flushTimer) { + return; + } + this.flushTimer = setInterval( + () => this.flush(), + this.config.flushIntervalSeconds * 1000 + ); + } + + private stopFlushTimer(): void { + if (this.flushTimer) { + clearInterval(this.flushTimer); + this.flushTimer = null; + } + } +} + diff --git a/packages/core-sdk/src/collectors/chat-session.ts b/packages/core-sdk/src/collectors/chat-session.ts new file mode 100644 index 0000000..3378606 --- /dev/null +++ b/packages/core-sdk/src/collectors/chat-session.ts @@ -0,0 +1,133 @@ +import type { ChatSessionEvent, AIInteraction } from '@ide-collector/shared'; +import { EventType, generateEventId, getCurrentTimestamp } from '@ide-collector/shared'; +import { Collector } from '../collector'; + +/** + * 聊天会话事件采集器 + */ +export class ChatSessionCollector { + private collector: Collector; + private activeSessions: Map< + string, + { + startTime: number; + turnCount: number; + aiInteraction: AIInteraction; + } + > = new Map(); + + constructor(collector: Collector) { + this.collector = collector; + } + + /** + * 开始新会话 + */ + public async startSession( + sessionId: string, + aiInteraction: AIInteraction + ): Promise { + this.activeSessions.set(sessionId, { + startTime: Date.now(), + turnCount: 0, + aiInteraction, + }); + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CHAT_SESSION_START, + timestamp: getCurrentTimestamp(), + aiInteraction, + sessionId, + } as Omit); + } + + /** + * 记录用户消息 + */ + public async onUserMessage( + sessionId: string, + message: string + ): Promise { + const session = this.activeSessions.get(sessionId); + if (!session) { + return; + } + + session.turnCount++; + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CHAT_MESSAGE_SENT, + timestamp: getCurrentTimestamp(), + aiInteraction: session.aiInteraction, + sessionId, + turnNumber: session.turnCount, + userMessage: message, + } as Omit); + } + + /** + * 记录 AI 响应 + */ + public async onAIResponse( + sessionId: string, + response: string, + latencyMs?: number + ): Promise { + const session = this.activeSessions.get(sessionId); + if (!session) { + return; + } + + const aiInteraction = { + ...session.aiInteraction, + latencyMs, + }; + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CHAT_RESPONSE_RECEIVED, + timestamp: getCurrentTimestamp(), + aiInteraction, + sessionId, + turnNumber: session.turnCount, + aiResponse: response, + } as Omit); + } + + /** + * 结束会话 + */ + public async endSession(sessionId: string): Promise { + const session = this.activeSessions.get(sessionId); + if (!session) { + return; + } + + const sessionDurationMs = Date.now() - session.startTime; + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CHAT_SESSION_END, + timestamp: getCurrentTimestamp(), + aiInteraction: session.aiInteraction, + sessionId, + turnNumber: session.turnCount, + metadata: { + sessionDurationMs, + totalTurns: session.turnCount, + }, + } as Omit); + + this.activeSessions.delete(sessionId); + } + + /** + * 获取活跃会话数 + */ + public getActiveSessionCount(): number { + return this.activeSessions.size; + } +} + diff --git a/packages/core-sdk/src/collectors/code-completion.ts b/packages/core-sdk/src/collectors/code-completion.ts new file mode 100644 index 0000000..69824c4 --- /dev/null +++ b/packages/core-sdk/src/collectors/code-completion.ts @@ -0,0 +1,136 @@ +import type { + CodeCompletionEvent, + CodeContext, + AIInteraction, + CodeData, +} from '@ide-collector/shared'; +import { EventType, generateEventId, getCurrentTimestamp } from '@ide-collector/shared'; +import { Collector } from '../collector'; + +/** + * 代码补全事件采集器 + */ +export class CodeCompletionCollector { + private collector: Collector; + private pendingCompletions: Map< + string, + { + showTime: number; + context: CodeContext; + aiInteraction: AIInteraction; + suggestedCode: string; + } + > = new Map(); + + constructor(collector: Collector) { + this.collector = collector; + } + + /** + * 记录补全建议展示 + */ + public async onCompletionShown( + completionId: string, + context: CodeContext, + aiInteraction: AIInteraction, + suggestedCode: string + ): Promise { + // 保存待处理的补全信息 + this.pendingCompletions.set(completionId, { + showTime: Date.now(), + context, + aiInteraction, + suggestedCode, + }); + + // 记录展示事件 + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CODE_COMPLETION_SHOWN, + timestamp: getCurrentTimestamp(), + context, + aiInteraction, + codeData: { + suggestedCode, + }, + } as Omit); + } + + /** + * 记录补全接受 + */ + public async onCompletionAccepted( + completionId: string, + actualCode?: string + ): Promise { + const pending = this.pendingCompletions.get(completionId); + if (!pending) { + return; + } + + const decisionTimeMs = Date.now() - pending.showTime; + const modifications: string[] = []; + + // 检测修改 + if (actualCode && actualCode !== pending.suggestedCode) { + modifications.push('modified_after_accept'); + } + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CODE_COMPLETION_ACCEPTED, + timestamp: getCurrentTimestamp(), + context: pending.context, + aiInteraction: pending.aiInteraction, + codeData: { + suggestedCode: pending.suggestedCode, + afterModification: actualCode, + accepted: true, + modifications, + }, + decisionTimeMs, + } as Omit); + + this.pendingCompletions.delete(completionId); + } + + /** + * 记录补全拒绝 + */ + public async onCompletionRejected(completionId: string): Promise { + const pending = this.pendingCompletions.get(completionId); + if (!pending) { + return; + } + + const decisionTimeMs = Date.now() - pending.showTime; + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CODE_COMPLETION_REJECTED, + timestamp: getCurrentTimestamp(), + context: pending.context, + aiInteraction: pending.aiInteraction, + codeData: { + suggestedCode: pending.suggestedCode, + accepted: false, + }, + decisionTimeMs, + } as Omit); + + this.pendingCompletions.delete(completionId); + } + + /** + * 清理过期的待处理补全 + */ + public cleanupStale(maxAgeMs: number = 300000): void { + const now = Date.now(); + for (const [id, pending] of this.pendingCompletions) { + if (now - pending.showTime > maxAgeMs) { + this.pendingCompletions.delete(id); + } + } + } +} + diff --git a/packages/core-sdk/src/collectors/index.ts b/packages/core-sdk/src/collectors/index.ts new file mode 100644 index 0000000..fa69a51 --- /dev/null +++ b/packages/core-sdk/src/collectors/index.ts @@ -0,0 +1,4 @@ +export { CodeCompletionCollector } from './code-completion'; +export { ChatSessionCollector } from './chat-session'; +export { UserBehaviorCollector } from './user-behavior'; + diff --git a/packages/core-sdk/src/collectors/user-behavior.ts b/packages/core-sdk/src/collectors/user-behavior.ts new file mode 100644 index 0000000..ee6f27e --- /dev/null +++ b/packages/core-sdk/src/collectors/user-behavior.ts @@ -0,0 +1,86 @@ +import type { BaseEvent } from '@ide-collector/shared'; +import { EventType, generateEventId, getCurrentTimestamp } from '@ide-collector/shared'; +import { Collector } from '../collector'; + +/** + * 用户行为采集器 + */ +export class UserBehaviorCollector { + private collector: Collector; + private activityStartTime: number = 0; + private isActive: boolean = false; + private aiUsageCount: Map = new Map(); + + constructor(collector: Collector) { + this.collector = collector; + } + + /** + * 记录活动开始 + */ + public startActivity(): void { + if (!this.isActive) { + this.activityStartTime = Date.now(); + this.isActive = true; + } + } + + /** + * 记录活动结束并返回活动时长 + */ + public endActivity(): number { + if (!this.isActive) { + return 0; + } + + const duration = Date.now() - this.activityStartTime; + this.isActive = false; + return duration; + } + + /** + * 记录 AI 功能使用 + */ + public trackAIUsage(featureType: string): void { + const count = this.aiUsageCount.get(featureType) || 0; + this.aiUsageCount.set(featureType, count + 1); + } + + /** + * 获取 AI 使用统计 + */ + public getAIUsageStats(): Record { + return Object.fromEntries(this.aiUsageCount); + } + + /** + * 重置统计 + */ + public resetStats(): void { + this.aiUsageCount.clear(); + } + + /** + * 发送使用统计汇总 + */ + public async flushStats(): Promise { + const stats = this.getAIUsageStats(); + if (Object.keys(stats).length === 0) { + return; + } + + await this.collector.trackEvent({ + eventId: generateEventId(), + eventType: EventType.CODE_COMPLETION_SHOWN, // 临时使用,后续可扩展 + timestamp: getCurrentTimestamp(), + metadata: { + type: 'usage_summary', + aiUsageStats: stats, + activityDurationMs: this.endActivity(), + }, + } as Omit); + + this.resetStats(); + } +} + diff --git a/packages/core-sdk/src/config/config-manager.ts b/packages/core-sdk/src/config/config-manager.ts new file mode 100644 index 0000000..0e6e339 --- /dev/null +++ b/packages/core-sdk/src/config/config-manager.ts @@ -0,0 +1,102 @@ +import type { CollectorConfig } from '@ide-collector/shared'; +import { STORAGE_KEYS, safeJsonParse, DEFAULT_COLLECTOR_CONFIG } from '@ide-collector/shared'; +import { LocalStorage } from '../storage/local-storage'; + +/** + * 配置管理器 + */ +export class ConfigManager { + private storage: LocalStorage; + private config: CollectorConfig; + + constructor(storage?: LocalStorage) { + this.storage = storage || new LocalStorage(); + this.config = { ...DEFAULT_COLLECTOR_CONFIG }; + } + + /** + * 加载配置 + */ + public async loadConfig(): Promise { + const data = await this.storage.get(STORAGE_KEYS.CONFIG); + if (data) { + const loaded = safeJsonParse(data, this.config); + this.config = { ...DEFAULT_COLLECTOR_CONFIG, ...loaded }; + return this.config; + } + return null; + } + + /** + * 保存配置 + */ + public async saveConfig(config: CollectorConfig): Promise { + this.config = config; + await this.storage.set(STORAGE_KEYS.CONFIG, JSON.stringify(config)); + } + + /** + * 获取当前配置 + */ + public getConfig(): CollectorConfig { + return { ...this.config }; + } + + /** + * 更新部分配置 + */ + public async updateConfig(partial: Partial): Promise { + this.config = { ...this.config, ...partial }; + await this.saveConfig(this.config); + return this.config; + } + + /** + * 重置为默认配置 + */ + public async resetConfig(): Promise { + this.config = { ...DEFAULT_COLLECTOR_CONFIG }; + await this.saveConfig(this.config); + return this.config; + } + + /** + * 获取用户ID + */ + public async getUserId(): Promise { + return this.storage.get(STORAGE_KEYS.USER_ID); + } + + /** + * 保存用户ID + */ + public async saveUserId(userId: string): Promise { + await this.storage.set(STORAGE_KEYS.USER_ID, userId); + } + + /** + * 获取最后同步时间 + */ + public async getLastSyncTime(): Promise { + const data = await this.storage.get(STORAGE_KEYS.LAST_SYNC); + if (data) { + return new Date(data); + } + return null; + } + + /** + * 更新最后同步时间 + */ + public async updateLastSyncTime(): Promise { + await this.storage.set(STORAGE_KEYS.LAST_SYNC, new Date().toISOString()); + } + + /** + * 设置存储后端 + */ + public setStorage(storage: LocalStorage): void { + this.storage = storage; + } +} + diff --git a/packages/core-sdk/src/config/index.ts b/packages/core-sdk/src/config/index.ts new file mode 100644 index 0000000..d92ffe0 --- /dev/null +++ b/packages/core-sdk/src/config/index.ts @@ -0,0 +1,2 @@ +export { ConfigManager } from './config-manager'; + diff --git a/packages/core-sdk/src/index.ts b/packages/core-sdk/src/index.ts new file mode 100644 index 0000000..09650b7 --- /dev/null +++ b/packages/core-sdk/src/index.ts @@ -0,0 +1,8 @@ +// 核心 SDK 导出 +export * from './collectors'; +export * from './reporters'; +export * from './privacy'; +export * from './storage'; +export * from './config'; +export { Collector } from './collector'; + diff --git a/packages/core-sdk/src/privacy/code-sanitizer.ts b/packages/core-sdk/src/privacy/code-sanitizer.ts new file mode 100644 index 0000000..0cea494 --- /dev/null +++ b/packages/core-sdk/src/privacy/code-sanitizer.ts @@ -0,0 +1,55 @@ +import { SENSITIVE_PATTERNS } from '@ide-collector/shared'; + +/** + * 代码脱敏处理器 + */ +export class CodeSanitizer { + private patterns: RegExp[]; + + constructor(additionalPatterns: RegExp[] = []) { + this.patterns = [...SENSITIVE_PATTERNS, ...additionalPatterns]; + } + + /** + * 脱敏代码 + */ + public sanitize(code: string): string { + let sanitized = code; + + for (const pattern of this.patterns) { + sanitized = sanitized.replace(pattern, match => { + // 保留结构,替换内容 + if (match.includes('=') || match.includes(':')) { + const separator = match.includes('=') ? '=' : ':'; + const parts = match.split(separator); + return `${parts[0]}${separator}[REDACTED]`; + } + return '[REDACTED]'; + }); + } + + return sanitized; + } + + /** + * 检查代码是否包含敏感信息 + */ + public containsSensitiveData(code: string): boolean { + return this.patterns.some(pattern => pattern.test(code)); + } + + /** + * 添加自定义模式 + */ + public addPattern(pattern: RegExp): void { + this.patterns.push(pattern); + } + + /** + * 重置为默认模式 + */ + public resetPatterns(): void { + this.patterns = [...SENSITIVE_PATTERNS]; + } +} + diff --git a/packages/core-sdk/src/privacy/index.ts b/packages/core-sdk/src/privacy/index.ts new file mode 100644 index 0000000..8c3dcc9 --- /dev/null +++ b/packages/core-sdk/src/privacy/index.ts @@ -0,0 +1,3 @@ +export { PrivacyHandler } from './privacy-handler'; +export { CodeSanitizer } from './code-sanitizer'; + diff --git a/packages/core-sdk/src/privacy/privacy-handler.ts b/packages/core-sdk/src/privacy/privacy-handler.ts new file mode 100644 index 0000000..404dee5 --- /dev/null +++ b/packages/core-sdk/src/privacy/privacy-handler.ts @@ -0,0 +1,123 @@ +import type { BaseEvent, PrivacyConfig, CodeContext } from '@ide-collector/shared'; +import { hashString, matchesExcludePattern, truncateString } from '@ide-collector/shared'; +import { CodeSanitizer } from './code-sanitizer'; + +/** + * 隐私处理器 + */ +export class PrivacyHandler { + private config: PrivacyConfig; + private codeSanitizer: CodeSanitizer; + + constructor(config: PrivacyConfig) { + this.config = config; + this.codeSanitizer = new CodeSanitizer(); + } + + /** + * 处理完整事件 + */ + public sanitizeEvent(event: BaseEvent): BaseEvent { + const sanitized = { ...event }; + + // 处理用户信息 + if (this.config.anonymizeUser && sanitized.userInfo) { + sanitized.userInfo = { + ...sanitized.userInfo, + userId: hashString(sanitized.userInfo.userId), + }; + } + + // 处理代码上下文 + if (sanitized.context) { + sanitized.context = this.sanitizeContext(sanitized.context); + } + + // 处理代码数据 + if (sanitized.codeData) { + sanitized.codeData = this.sanitizeCodeData(sanitized.codeData); + } + + return sanitized; + } + + /** + * 处理代码上下文 + */ + public sanitizeContext(context: CodeContext): CodeContext { + // 检查是否需要排除 + if (matchesExcludePattern(context.filePath, this.config.excludePaths)) { + return { + ...context, + filePath: '[excluded]', + }; + } + + return { + ...context, + // 只保留文件名,去除完整路径 + filePath: this.sanitizeFilePath(context.filePath), + // 哈希工作区名称 + workspace: context.workspace ? hashString(context.workspace) : undefined, + }; + } + + /** + * 处理代码数据 + */ + private sanitizeCodeData( + codeData: NonNullable + ): NonNullable { + const sanitized = { ...codeData }; + + if (this.config.obfuscateCode) { + if (sanitized.beforeCursor) { + sanitized.beforeCursor = this.sanitizeCode(sanitized.beforeCursor); + } + if (sanitized.suggestedCode) { + sanitized.suggestedCode = this.sanitizeCode(sanitized.suggestedCode); + } + if (sanitized.afterModification) { + sanitized.afterModification = this.sanitizeCode(sanitized.afterModification); + } + } + + return sanitized; + } + + /** + * 处理代码内容 + */ + private sanitizeCode(code: string): string { + // 移除敏感信息 + let sanitized = this.codeSanitizer.sanitize(code); + + // 截断到最大长度 + sanitized = truncateString(sanitized, this.config.maxCodeLength); + + return sanitized; + } + + /** + * 处理文件路径 + */ + private sanitizeFilePath(filePath: string): string { + // 只保留文件扩展名和部分路径结构 + const parts = filePath.split(/[/\\]/); + const fileName = parts[parts.length - 1]; + const parentDir = parts.length > 1 ? parts[parts.length - 2] : ''; + + if (parentDir) { + return `.../${parentDir}/${fileName}`; + } + return fileName; + } + + /** + * 更新配置 + */ + public updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + } +} + diff --git a/packages/core-sdk/src/reporters/batch-queue.ts b/packages/core-sdk/src/reporters/batch-queue.ts new file mode 100644 index 0000000..96e6617 --- /dev/null +++ b/packages/core-sdk/src/reporters/batch-queue.ts @@ -0,0 +1,104 @@ +import type { BaseEvent } from '@ide-collector/shared'; +import { MAX_QUEUE_SIZE } from '@ide-collector/shared'; + +/** + * 批量队列管理器 + */ +export class BatchQueue { + private queue: T[] = []; + private maxSize: number; + private onFlush?: (items: T[]) => Promise; + + constructor(maxSize: number = MAX_QUEUE_SIZE, onFlush?: (items: T[]) => Promise) { + this.maxSize = maxSize; + this.onFlush = onFlush; + } + + /** + * 添加项目到队列 + */ + public async add(item: T): Promise { + this.queue.push(item); + + // 如果达到最大大小,自动刷新 + if (this.queue.length >= this.maxSize) { + await this.flush(); + } + } + + /** + * 批量添加项目 + */ + public async addBatch(items: T[]): Promise { + for (const item of items) { + await this.add(item); + } + } + + /** + * 刷新队列 + */ + public async flush(): Promise { + if (this.queue.length === 0) { + return []; + } + + const items = [...this.queue]; + this.queue = []; + + if (this.onFlush) { + try { + await this.onFlush(items); + } catch (error) { + // 失败时将项目重新入队 + this.queue = [...items, ...this.queue]; + throw error; + } + } + + return items; + } + + /** + * 获取队列大小 + */ + public size(): number { + return this.queue.length; + } + + /** + * 检查队列是否为空 + */ + public isEmpty(): boolean { + return this.queue.length === 0; + } + + /** + * 清空队列 + */ + public clear(): void { + this.queue = []; + } + + /** + * 查看队列内容(不移除) + */ + public peek(): T[] { + return [...this.queue]; + } + + /** + * 设置最大大小 + */ + public setMaxSize(size: number): void { + this.maxSize = size; + } + + /** + * 设置刷新回调 + */ + public setOnFlush(callback: (items: T[]) => Promise): void { + this.onFlush = callback; + } +} + diff --git a/packages/core-sdk/src/reporters/http-reporter.ts b/packages/core-sdk/src/reporters/http-reporter.ts new file mode 100644 index 0000000..6fb07c2 --- /dev/null +++ b/packages/core-sdk/src/reporters/http-reporter.ts @@ -0,0 +1,97 @@ +import type { BaseEvent } from '@ide-collector/shared'; +import { HTTP_TIMEOUT_MS, withRetry, API_VERSION } from '@ide-collector/shared'; + +/** + * HTTP 数据上报器 + */ +export class HttpReporter { + private endpoint: string; + private headers: Record; + + constructor(endpoint: string, apiKey?: string) { + this.endpoint = endpoint; + this.headers = { + 'Content-Type': 'application/json', + 'X-API-Version': API_VERSION, + }; + if (apiKey) { + this.headers['Authorization'] = `Bearer ${apiKey}`; + } + } + + /** + * 发送单个事件 + */ + public async send(event: BaseEvent): Promise { + await this.sendBatch([event]); + } + + /** + * 批量发送事件 + */ + public async sendBatch(events: BaseEvent[]): Promise { + if (events.length === 0) { + return; + } + + await withRetry(async () => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS); + + try { + const response = await fetch(`${this.endpoint}/batch`, { + method: 'POST', + headers: this.headers, + body: JSON.stringify({ events }), + signal: controller.signal, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + } finally { + clearTimeout(timeoutId); + } + }, 3, 1000); + } + + /** + * 健康检查 + */ + public async healthCheck(): Promise { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + try { + const response = await fetch(`${this.endpoint}/health`, { + method: 'GET', + headers: this.headers, + signal: controller.signal, + }); + + return response.ok; + } finally { + clearTimeout(timeoutId); + } + } catch { + return false; + } + } + + /** + * 更新端点 + */ + public setEndpoint(endpoint: string): void { + this.endpoint = endpoint; + } + + /** + * 更新 API Key + */ + public setApiKey(apiKey: string): void { + this.headers['Authorization'] = `Bearer ${apiKey}`; + } +} + diff --git a/packages/core-sdk/src/reporters/index.ts b/packages/core-sdk/src/reporters/index.ts new file mode 100644 index 0000000..c064689 --- /dev/null +++ b/packages/core-sdk/src/reporters/index.ts @@ -0,0 +1,3 @@ +export { HttpReporter } from './http-reporter'; +export { BatchQueue } from './batch-queue'; + diff --git a/packages/core-sdk/src/storage/event-queue.ts b/packages/core-sdk/src/storage/event-queue.ts new file mode 100644 index 0000000..aedd755 --- /dev/null +++ b/packages/core-sdk/src/storage/event-queue.ts @@ -0,0 +1,113 @@ +import type { BaseEvent } from '@ide-collector/shared'; +import { MAX_QUEUE_SIZE, STORAGE_KEYS, safeJsonParse } from '@ide-collector/shared'; +import { LocalStorage } from './local-storage'; + +/** + * 事件队列管理器 + */ +export class EventQueue { + private queue: BaseEvent[] = []; + private maxSize: number; + private storage: LocalStorage; + + constructor(maxSize: number = MAX_QUEUE_SIZE) { + this.maxSize = maxSize; + this.storage = new LocalStorage(); + } + + /** + * 入队事件 + */ + public async enqueue(event: BaseEvent): Promise { + this.queue.push(event); + + // 如果超过最大大小,移除最早的事件 + while (this.queue.length > this.maxSize) { + this.queue.shift(); + } + + // 持久化到存储 + await this.saveToStorage(); + } + + /** + * 出队所有事件 + */ + public async dequeueAll(): Promise { + const events = [...this.queue]; + this.queue = []; + await this.saveToStorage(); + return events; + } + + /** + * 出队指定数量的事件 + */ + public async dequeue(count: number): Promise { + const events = this.queue.splice(0, count); + await this.saveToStorage(); + return events; + } + + /** + * 查看队列(不移除) + */ + public peek(count?: number): BaseEvent[] { + if (count) { + return this.queue.slice(0, count); + } + return [...this.queue]; + } + + /** + * 获取队列大小 + */ + public size(): number { + return this.queue.length; + } + + /** + * 检查是否为空 + */ + public isEmpty(): boolean { + return this.queue.length === 0; + } + + /** + * 清空队列 + */ + public async clear(): Promise { + this.queue = []; + await this.saveToStorage(); + } + + /** + * 设置最大大小 + */ + public setMaxSize(size: number): void { + this.maxSize = size; + // 调整队列大小 + while (this.queue.length > this.maxSize) { + this.queue.shift(); + } + } + + /** + * 从存储加载事件 + */ + public async loadFromStorage(): Promise { + const data = await this.storage.get(STORAGE_KEYS.EVENT_QUEUE); + if (data) { + this.queue = safeJsonParse(data, []); + } + return this.queue; + } + + /** + * 保存到存储 + */ + private async saveToStorage(): Promise { + await this.storage.set(STORAGE_KEYS.EVENT_QUEUE, JSON.stringify(this.queue)); + } +} + diff --git a/packages/core-sdk/src/storage/index.ts b/packages/core-sdk/src/storage/index.ts new file mode 100644 index 0000000..b42436f --- /dev/null +++ b/packages/core-sdk/src/storage/index.ts @@ -0,0 +1,3 @@ +export { EventQueue } from './event-queue'; +export { LocalStorage } from './local-storage'; + diff --git a/packages/core-sdk/src/storage/local-storage.ts b/packages/core-sdk/src/storage/local-storage.ts new file mode 100644 index 0000000..c5472aa --- /dev/null +++ b/packages/core-sdk/src/storage/local-storage.ts @@ -0,0 +1,72 @@ +/** + * 本地存储抽象层 + * 用于在不同环境(浏览器、Node.js、VSCode 等)中统一存储接口 + */ +export class LocalStorage { + private storage: Map = new Map(); + private persistCallback?: (key: string, value: string | null) => Promise; + + constructor(persistCallback?: (key: string, value: string | null) => Promise) { + this.persistCallback = persistCallback; + } + + /** + * 获取值 + */ + public async get(key: string): Promise { + return this.storage.get(key) ?? null; + } + + /** + * 设置值 + */ + public async set(key: string, value: string): Promise { + this.storage.set(key, value); + if (this.persistCallback) { + await this.persistCallback(key, value); + } + } + + /** + * 删除值 + */ + public async remove(key: string): Promise { + this.storage.delete(key); + if (this.persistCallback) { + await this.persistCallback(key, null); + } + } + + /** + * 清空所有 + */ + public async clear(): Promise { + for (const key of this.storage.keys()) { + await this.remove(key); + } + } + + /** + * 获取所有键 + */ + public keys(): string[] { + return Array.from(this.storage.keys()); + } + + /** + * 设置持久化回调 + */ + public setPersistCallback(callback: (key: string, value: string | null) => Promise): void { + this.persistCallback = callback; + } + + /** + * 从外部数据恢复 + */ + public restore(data: Record): void { + for (const [key, value] of Object.entries(data)) { + this.storage.set(key, value); + } + } +} + diff --git a/packages/core-sdk/tsconfig.json b/packages/core-sdk/tsconfig.json new file mode 100644 index 0000000..8fae313 --- /dev/null +++ b/packages/core-sdk/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/packages/jetbrains-plugin/build.gradle.kts b/packages/jetbrains-plugin/build.gradle.kts new file mode 100644 index 0000000..5108f1c --- /dev/null +++ b/packages/jetbrains-plugin/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("java") + id("org.jetbrains.kotlin.jvm") version "1.9.21" + id("org.jetbrains.intellij") version "1.16.1" +} + +group = "com.devtools.collector" +version = "0.1.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.code.gson:gson:2.10.1") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") +} + +intellij { + version.set("2023.3") + type.set("IC") // IntelliJ IDEA Community Edition + plugins.set(listOf()) +} + +tasks { + withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + + withType { + kotlinOptions.jvmTarget = "17" + } + + patchPluginXml { + sinceBuild.set("233") + untilBuild.set("243.*") + } + + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } +} + diff --git a/packages/jetbrains-plugin/settings.gradle.kts b/packages/jetbrains-plugin/settings.gradle.kts new file mode 100644 index 0000000..94f0904 --- /dev/null +++ b/packages/jetbrains-plugin/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "ide-data-collector-jetbrains" + diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/CollectorPlugin.kt b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/CollectorPlugin.kt new file mode 100644 index 0000000..836376f --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/CollectorPlugin.kt @@ -0,0 +1,23 @@ +package com.devtools.collector + +import com.intellij.openapi.diagnostic.Logger + +/** + * IDE Data Collector 插件主入口 + */ +object CollectorPlugin { + private val LOG = Logger.getInstance(CollectorPlugin::class.java) + + const val PLUGIN_ID = "com.devtools.collector" + const val PLUGIN_NAME = "IDE Data Collector" + const val VERSION = "0.1.0" + + fun initialize() { + LOG.info("$PLUGIN_NAME v$VERSION initializing...") + } + + fun shutdown() { + LOG.info("$PLUGIN_NAME shutting down...") + } +} + diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/models/Events.kt b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/models/Events.kt new file mode 100644 index 0000000..db3f3cf --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/models/Events.kt @@ -0,0 +1,134 @@ +package com.devtools.collector.models + +import java.time.Instant +import java.util.UUID + +/** + * 事件类型枚举 + */ +enum class EventType { + CODE_COMPLETION_SHOWN, + CODE_COMPLETION_ACCEPTED, + CODE_COMPLETION_REJECTED, + CHAT_SESSION_START, + CHAT_MESSAGE_SENT, + CHAT_RESPONSE_RECEIVED, + CHAT_SESSION_END, + CODE_GENERATION_REQUEST, + CODE_GENERATION_COMPLETE, + CODE_EDIT_AFTER_ACCEPT +} + +/** + * IDE 类型 + */ +enum class IDEType { + INTELLIJ, + PYCHARM, + WEBSTORM, + GOLAND, + OTHER +} + +/** + * AI 提供商 + */ +enum class AIProvider { + GITHUB_COPILOT, + JETBRAINS_AI, + CODEIUM, + TABNINE, + CUSTOM +} + +/** + * 用户信息 + */ +data class UserInfo( + val userId: String, + val ideType: IDEType, + val ideVersion: String, + val os: String? = null, + val timezone: String? = null +) + +/** + * 代码上下文 + */ +data class CodeContext( + val filePath: String, + val language: String, + val projectType: String? = null, + val workspace: String? = null, + val cursorLine: Int? = null, + val cursorColumn: Int? = null +) + +/** + * AI 交互信息 + */ +data class AIInteraction( + val provider: AIProvider, + val model: String? = null, + val promptTokens: Int? = null, + val completionTokens: Int? = null, + val latencyMs: Long? = null +) + +/** + * 代码数据 + */ +data class CodeData( + val beforeCursor: String? = null, + val suggestedCode: String? = null, + val afterModification: String? = null, + val accepted: Boolean? = null, + val modifications: List? = null +) + +/** + * 基础事件 + */ +open class BaseEvent( + val eventId: String = UUID.randomUUID().toString(), + val eventType: EventType, + val timestamp: String = Instant.now().toString(), + val userInfo: UserInfo, + val context: CodeContext? = null, + val aiInteraction: AIInteraction? = null, + val codeData: CodeData? = null, + val metadata: Map? = null +) + +/** + * 代码补全事件 + */ +class CodeCompletionEvent( + eventType: EventType, + userInfo: UserInfo, + context: CodeContext? = null, + aiInteraction: AIInteraction? = null, + codeData: CodeData? = null, + val decisionTimeMs: Long? = null +) : BaseEvent( + eventType = eventType, + userInfo = userInfo, + context = context, + aiInteraction = aiInteraction, + codeData = codeData +) + +/** + * 采集器配置 + */ +data class CollectorConfig( + val enabled: Boolean = true, + val samplingRate: Double = 1.0, + val realtimeEnabled: Boolean = false, + val batchSize: Int = 50, + val flushIntervalSeconds: Long = 60, + val apiEndpoint: String = "http://localhost:8000/api/v1/events", + val anonymizeUser: Boolean = true, + val obfuscateCode: Boolean = true +) + diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/reporters/HttpReporter.kt b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/reporters/HttpReporter.kt new file mode 100644 index 0000000..8521fa4 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/reporters/HttpReporter.kt @@ -0,0 +1,65 @@ +package com.devtools.collector.reporters + +import com.devtools.collector.models.BaseEvent +import com.google.gson.Gson +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.concurrent.TimeUnit + +/** + * HTTP 数据上报器 + */ +class HttpReporter { + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + private val gson = Gson() + private val jsonMediaType = "application/json; charset=utf-8".toMediaType() + + /** + * 批量发送事件 + */ + fun sendBatch(events: List, endpoint: String) { + if (events.isEmpty()) return + + val payload = mapOf("events" to events) + val json = gson.toJson(payload) + + val request = Request.Builder() + .url("$endpoint/batch") + .post(json.toRequestBody(jsonMediaType)) + .header("Content-Type", "application/json") + .header("X-API-Version", "v1") + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw RuntimeException("HTTP ${response.code}: ${response.body?.string()}") + } + } + } + + /** + * 健康检查 + */ + fun healthCheck(endpoint: String): Boolean { + return try { + val request = Request.Builder() + .url("$endpoint/health") + .get() + .build() + + client.newCall(request).execute().use { response -> + response.isSuccessful + } + } catch (e: Exception) { + false + } + } +} + diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/services/CollectorService.kt b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/services/CollectorService.kt new file mode 100644 index 0000000..8b7f45f --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/devtools/collector/services/CollectorService.kt @@ -0,0 +1,113 @@ +package com.devtools.collector.services + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.Logger +import com.devtools.collector.CollectorPlugin +import com.devtools.collector.models.* +import com.devtools.collector.reporters.HttpReporter +import java.util.UUID +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * 应用级采集服务 + */ +@Service(Service.Level.APP) +class CollectorService { + private val LOG = Logger.getInstance(CollectorService::class.java) + + private val eventQueue = ConcurrentLinkedQueue() + private val reporter = HttpReporter() + private val scheduler = Executors.newSingleThreadScheduledExecutor() + + private var config = CollectorConfig() + private var userId: String = UUID.randomUUID().toString() + private var isRunning = false + + companion object { + fun getInstance(): CollectorService { + return ApplicationManager.getApplication().getService(CollectorService::class.java) + } + } + + fun initialize() { + if (isRunning) return + + CollectorPlugin.initialize() + + // 启动定期刷新任务 + scheduler.scheduleAtFixedRate( + { flush() }, + config.flushIntervalSeconds, + config.flushIntervalSeconds, + TimeUnit.SECONDS + ) + + isRunning = true + LOG.info("CollectorService initialized") + } + + fun shutdown() { + if (!isRunning) return + + flush() + scheduler.shutdown() + + CollectorPlugin.shutdown() + isRunning = false + LOG.info("CollectorService shutdown") + } + + fun trackEvent(event: BaseEvent) { + if (!config.enabled || !isRunning) return + + // 采样检查 + if (Math.random() > config.samplingRate) return + + eventQueue.offer(event) + + // 检查队列大小 + if (eventQueue.size >= config.batchSize) { + flush() + } + } + + fun flush() { + if (eventQueue.isEmpty()) return + + val events = mutableListOf() + while (eventQueue.isNotEmpty() && events.size < config.batchSize) { + eventQueue.poll()?.let { events.add(it) } + } + + if (events.isNotEmpty()) { + try { + reporter.sendBatch(events, config.apiEndpoint) + LOG.info("Flushed ${events.size} events") + } catch (e: Exception) { + LOG.error("Failed to flush events", e) + // 重新入队 + events.forEach { eventQueue.offer(it) } + } + } + } + + fun updateConfig(newConfig: CollectorConfig) { + config = newConfig + } + + fun getConfig(): CollectorConfig = config + + fun getUserId(): String = userId + + fun getQueueSize(): Int = eventQueue.size + + fun isEnabled(): Boolean = config.enabled + + fun toggle() { + config = config.copy(enabled = !config.enabled) + } +} + diff --git a/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml b/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..3c6bb7b --- /dev/null +++ b/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,101 @@ + + com.devtools.collector + IDE Data Collector + DevTools AI + + 功能特性: +
    +
  • 代码补全采纳/拒绝事件采集
  • +
  • AI聊天会话记录
  • +
  • 开发者行为分析
  • +
  • 隐私保护和数据脱敏
  • +
+ +

支持的IDE:

+
    +
  • IntelliJ IDEA
  • +
  • PyCharm
  • +
  • WebStorm
  • +
  • GoLand
  • +
  • 其他 JetBrains IDE
  • +
+ ]]>
+ + 0.1.0 +
    +
  • 初始版本
  • +
  • 基础事件采集功能
  • +
  • 配置管理界面
  • +
+ ]]>
+ + com.intellij.modules.platform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..9bd3662 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,28 @@ +{ + "name": "@ide-collector/shared", + "version": "0.1.0", + "description": "共享工具函数和类型定义", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts", + "dev": "tsup src/index.ts --format cjs,esm --dts --watch", + "clean": "rm -rf dist", + "lint": "eslint src --ext .ts", + "test": "vitest run" + }, + "devDependencies": { + "tsup": "^8.0.0", + "vitest": "^1.0.0" + } +} + diff --git a/packages/shared/src/constants/index.ts b/packages/shared/src/constants/index.ts new file mode 100644 index 0000000..e2a0fdf --- /dev/null +++ b/packages/shared/src/constants/index.ts @@ -0,0 +1,104 @@ +import type { CollectorConfig, PrivacyConfig } from '../types'; +import { EventType } from '../types'; + +/** + * 默认隐私配置 + */ +export const DEFAULT_PRIVACY_CONFIG: PrivacyConfig = { + anonymizeUser: true, + obfuscateCode: true, + excludePaths: ['*.secret.*', 'node_modules/', '.git/', '*.env*', '*.key', '*.pem'], + maxCodeLength: 500, +}; + +/** + * 默认采集配置 + */ +export const DEFAULT_COLLECTOR_CONFIG: CollectorConfig = { + enabled: true, + samplingRate: 1.0, + realtimeEnabled: false, + batchSize: 50, + flushIntervalSeconds: 60, + apiEndpoint: 'http://localhost:8000/api/v1/events', + privacy: DEFAULT_PRIVACY_CONFIG, + eventsToCapture: [ + EventType.CODE_COMPLETION_SHOWN, + EventType.CODE_COMPLETION_ACCEPTED, + EventType.CODE_COMPLETION_REJECTED, + EventType.CHAT_SESSION_START, + EventType.CHAT_MESSAGE_SENT, + EventType.CHAT_RESPONSE_RECEIVED, + EventType.CHAT_SESSION_END, + ], +}; + +/** + * 支持的编程语言列表 + */ +export const SUPPORTED_LANGUAGES = [ + 'javascript', + 'typescript', + 'python', + 'java', + 'go', + 'rust', + 'cpp', + 'c', + 'csharp', + 'php', + 'ruby', + 'swift', + 'kotlin', + 'scala', + 'vue', + 'react', + 'html', + 'css', + 'sql', + 'shell', + 'markdown', +] as const; + +/** + * API 版本 + */ +export const API_VERSION = 'v1'; + +/** + * 插件版本 + */ +export const PLUGIN_VERSION = '0.1.0'; + +/** + * 本地存储键名 + */ +export const STORAGE_KEYS = { + CONFIG: 'ide-collector-config', + USER_ID: 'ide-collector-user-id', + EVENT_QUEUE: 'ide-collector-event-queue', + LAST_SYNC: 'ide-collector-last-sync', +} as const; + +/** + * HTTP 请求超时 (ms) + */ +export const HTTP_TIMEOUT_MS = 30000; + +/** + * 最大队列长度 + */ +export const MAX_QUEUE_SIZE = 1000; + +/** + * 敏感词模式 + */ +export const SENSITIVE_PATTERNS = [ + /password\s*[:=]\s*['"][^'"]+['"]/gi, + /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/gi, + /secret\s*[:=]\s*['"][^'"]+['"]/gi, + /token\s*[:=]\s*['"][^'"]+['"]/gi, + /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, // IP 地址 + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, // Email +]; + diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..a9bdd4e --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,5 @@ +// 共享工具函数和类型定义 +export * from './types'; +export * from './utils'; +export * from './constants'; + diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts new file mode 100644 index 0000000..252f221 --- /dev/null +++ b/packages/shared/src/types/index.ts @@ -0,0 +1,192 @@ +/** + * IDE 类型枚举 + */ +export enum IDEType { + VSCODE = 'vscode', + CURSOR = 'cursor', + JETBRAINS = 'jetbrains', + UNKNOWN = 'unknown', +} + +/** + * AI 提供商类型 + */ +export enum AIProvider { + GITHUB_COPILOT = 'github-copilot', + CURSOR_AI = 'cursor-ai', + CODEIUM = 'codeium', + TABNINE = 'tabnine', + CUSTOM = 'custom', +} + +/** + * 事件类型枚举 + */ +export enum EventType { + CODE_COMPLETION_SHOWN = 'code_completion_shown', + CODE_COMPLETION_ACCEPTED = 'code_completion_accepted', + CODE_COMPLETION_REJECTED = 'code_completion_rejected', + CHAT_SESSION_START = 'chat_session_start', + CHAT_MESSAGE_SENT = 'chat_message_sent', + CHAT_RESPONSE_RECEIVED = 'chat_response_received', + CHAT_SESSION_END = 'chat_session_end', + CODE_GENERATION_REQUEST = 'code_generation_request', + CODE_GENERATION_COMPLETE = 'code_generation_complete', + CODE_EDIT_AFTER_ACCEPT = 'code_edit_after_accept', +} + +/** + * 用户信息接口 + */ +export interface UserInfo { + /** 匿名用户ID */ + userId: string; + /** IDE类型 */ + ideType: IDEType; + /** IDE版本 */ + ideVersion: string; + /** 操作系统 */ + os?: string; + /** 时区 */ + timezone?: string; +} + +/** + * 代码上下文信息 + */ +export interface CodeContext { + /** 文件路径 (脱敏后) */ + filePath: string; + /** 编程语言 */ + language: string; + /** 项目类型 */ + projectType?: string; + /** 工作区标识 (哈希) */ + workspace?: string; + /** 光标行号 */ + cursorLine?: number; + /** 光标列号 */ + cursorColumn?: number; +} + +/** + * AI 交互信息 + */ +export interface AIInteraction { + /** AI 服务提供商 */ + provider: AIProvider; + /** 模型名称 */ + model?: string; + /** 提示词 token 数 */ + promptTokens?: number; + /** 补全 token 数 */ + completionTokens?: number; + /** 响应延迟 (ms) */ + latencyMs?: number; +} + +/** + * 代码数据 + */ +export interface CodeData { + /** 光标前的代码 */ + beforeCursor?: string; + /** AI 建议的代码 */ + suggestedCode?: string; + /** 采纳后的实际代码 */ + afterModification?: string; + /** 是否接受 */ + accepted?: boolean; + /** 修改说明 */ + modifications?: string[]; +} + +/** + * 基础事件接口 + */ +export interface BaseEvent { + /** 唯一事件ID */ + eventId: string; + /** 事件类型 */ + eventType: EventType; + /** 时间戳 (ISO 8601) */ + timestamp: string; + /** 用户信息 */ + userInfo: UserInfo; + /** 代码上下文 */ + context?: CodeContext; + /** AI 交互信息 */ + aiInteraction?: AIInteraction; + /** 代码数据 */ + codeData?: CodeData; + /** 额外元数据 */ + metadata?: Record; +} + +/** + * 代码补全事件 + */ +export interface CodeCompletionEvent extends BaseEvent { + eventType: + | EventType.CODE_COMPLETION_SHOWN + | EventType.CODE_COMPLETION_ACCEPTED + | EventType.CODE_COMPLETION_REJECTED; + /** 从展示到决策的时间 (ms) */ + decisionTimeMs?: number; +} + +/** + * 聊天会话事件 + */ +export interface ChatSessionEvent extends BaseEvent { + eventType: + | EventType.CHAT_SESSION_START + | EventType.CHAT_MESSAGE_SENT + | EventType.CHAT_RESPONSE_RECEIVED + | EventType.CHAT_SESSION_END; + /** 会话ID */ + sessionId: string; + /** 对话轮次 */ + turnNumber?: number; + /** 用户消息 */ + userMessage?: string; + /** AI响应 */ + aiResponse?: string; +} + +/** + * 插件配置 + */ +export interface CollectorConfig { + /** 是否启用采集 */ + enabled: boolean; + /** 采样率 (0-1) */ + samplingRate: number; + /** 是否启用实时上报 */ + realtimeEnabled: boolean; + /** 批量大小 */ + batchSize: number; + /** 刷新间隔 (秒) */ + flushIntervalSeconds: number; + /** API 端点 */ + apiEndpoint: string; + /** 隐私设置 */ + privacy: PrivacyConfig; + /** 要采集的事件类型 */ + eventsToCapture: EventType[]; +} + +/** + * 隐私配置 + */ +export interface PrivacyConfig { + /** 匿名化用户 */ + anonymizeUser: boolean; + /** 混淆代码 */ + obfuscateCode: boolean; + /** 排除的路径模式 */ + excludePaths: string[]; + /** 最大代码长度 */ + maxCodeLength: number; +} + diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts new file mode 100644 index 0000000..4aee3fa --- /dev/null +++ b/packages/shared/src/utils/index.ts @@ -0,0 +1,133 @@ +import { v4 as uuidv4 } from 'uuid'; +import * as crypto from 'crypto'; + +/** + * 生成唯一事件ID + */ +export function generateEventId(): string { + return uuidv4(); +} + +/** + * 生成匿名用户ID + */ +export function generateAnonymousUserId(seed?: string): string { + if (seed) { + return crypto.createHash('sha256').update(seed).digest('hex').substring(0, 32); + } + return uuidv4(); +} + +/** + * 获取当前 ISO 时间戳 + */ +export function getCurrentTimestamp(): string { + return new Date().toISOString(); +} + +/** + * 哈希敏感字符串 + */ +export function hashString(str: string): string { + return crypto.createHash('sha256').update(str).digest('hex').substring(0, 16); +} + +/** + * 检查路径是否匹配排除模式 + */ +export function matchesExcludePattern(path: string, patterns: string[]): boolean { + return patterns.some(pattern => { + const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.')); + return regex.test(path); + }); +} + +/** + * 截断字符串到指定长度 + */ +export function truncateString(str: string, maxLength: number): string { + if (str.length <= maxLength) { + return str; + } + return str.substring(0, maxLength - 3) + '...'; +} + +/** + * 安全解析 JSON + */ +export function safeJsonParse(json: string, defaultValue: T): T { + try { + return JSON.parse(json) as T; + } catch { + return defaultValue; + } +} + +/** + * 防抖函数 + */ +export function debounce void>( + func: T, + waitMs: number +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + func(...args); + }, waitMs); + }; +} + +/** + * 节流函数 + */ +export function throttle void>( + func: T, + limitMs: number +): (...args: Parameters) => void { + let lastRun = 0; + + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastRun >= limitMs) { + lastRun = now; + func(...args); + } + }; +} + +/** + * 延迟执行 + */ +export function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * 重试包装器 + */ +export async function withRetry( + fn: () => Promise, + maxRetries: number = 3, + delayMs: number = 1000 +): Promise { + let lastError: Error | undefined; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + if (attempt < maxRetries - 1) { + await delay(delayMs * Math.pow(2, attempt)); + } + } + } + + throw lastError; +} + diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..8fae313 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json new file mode 100644 index 0000000..55f1a61 --- /dev/null +++ b/packages/vscode-extension/package.json @@ -0,0 +1,130 @@ +{ + "name": "ide-data-collector-vscode", + "displayName": "IDE Data Collector", + "description": "AI编程工具数据采集插件 - 评估AI代码助手效率", + "version": "0.1.0", + "publisher": "devtools-ai", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other" + ], + "keywords": [ + "ai", + "copilot", + "analytics", + "productivity" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "ide-collector.toggle", + "title": "Toggle Data Collection", + "category": "IDE Collector" + }, + { + "command": "ide-collector.showDashboard", + "title": "Show Dashboard", + "category": "IDE Collector" + }, + { + "command": "ide-collector.openSettings", + "title": "Open Settings", + "category": "IDE Collector" + }, + { + "command": "ide-collector.syncNow", + "title": "Sync Now", + "category": "IDE Collector" + } + ], + "configuration": { + "title": "IDE Data Collector", + "properties": { + "ideCollector.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable data collection" + }, + "ideCollector.apiEndpoint": { + "type": "string", + "default": "http://localhost:8000/api/v1/events", + "description": "API endpoint for data reporting" + }, + "ideCollector.samplingRate": { + "type": "number", + "default": 1.0, + "minimum": 0, + "maximum": 1, + "description": "Sampling rate (0-1)" + }, + "ideCollector.batchSize": { + "type": "number", + "default": 50, + "description": "Batch size for event upload" + }, + "ideCollector.flushInterval": { + "type": "number", + "default": 60, + "description": "Flush interval in seconds" + }, + "ideCollector.anonymizeUser": { + "type": "boolean", + "default": true, + "description": "Anonymize user ID" + }, + "ideCollector.obfuscateCode": { + "type": "boolean", + "default": true, + "description": "Obfuscate code content" + } + } + }, + "viewsContainers": { + "activitybar": [ + { + "id": "ide-collector", + "title": "IDE Collector", + "icon": "resources/icon.svg" + } + ] + }, + "views": { + "ide-collector": [ + { + "id": "ide-collector.status", + "name": "Status" + }, + { + "id": "ide-collector.stats", + "name": "Statistics" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "pnpm run build", + "build": "tsup src/extension.ts --format cjs --external vscode", + "dev": "tsup src/extension.ts --format cjs --external vscode --watch", + "lint": "eslint src --ext .ts", + "test": "vitest run", + "package": "vsce package --no-dependencies", + "publish": "vsce publish --no-dependencies" + }, + "dependencies": { + "@ide-collector/core-sdk": "workspace:*", + "@ide-collector/shared": "workspace:*" + }, + "devDependencies": { + "@types/vscode": "^1.85.0", + "@vscode/vsce": "^2.22.0", + "tsup": "^8.0.0", + "vitest": "^1.0.0" + } +} + diff --git a/packages/vscode-extension/src/adapters/completion-adapter.ts b/packages/vscode-extension/src/adapters/completion-adapter.ts new file mode 100644 index 0000000..bc67b92 --- /dev/null +++ b/packages/vscode-extension/src/adapters/completion-adapter.ts @@ -0,0 +1,119 @@ +import * as vscode from 'vscode'; +import { Collector, CodeCompletionCollector } from '@ide-collector/core-sdk'; +import { AIProvider, generateEventId } from '@ide-collector/shared'; + +/** + * 代码补全适配器 + * 用于捕获和处理 VSCode 中的代码补全事件 + */ +export class CompletionAdapter { + private collector: Collector; + private completionCollector: CodeCompletionCollector; + private disposables: vscode.Disposable[] = []; + + constructor(collector: Collector) { + this.collector = collector; + this.completionCollector = new CodeCompletionCollector(collector); + + this.setupListeners(); + } + + /** + * 设置事件监听器 + */ + private setupListeners(): void { + // 注意:VSCode 原生 API 不直接暴露补全接受/拒绝事件 + // 这里提供一个框架,实际实现需要配合具体的 AI 插件 API + + // 定期清理过期的待处理补全 + const cleanupInterval = setInterval(() => { + this.completionCollector.cleanupStale(); + }, 60000); + + this.disposables.push({ + dispose: () => clearInterval(cleanupInterval), + }); + } + + /** + * 记录补全建议展示(由外部调用) + */ + public async onCompletionShown( + document: vscode.TextDocument, + position: vscode.Position, + suggestedCode: string, + provider: AIProvider = AIProvider.GITHUB_COPILOT + ): Promise { + const completionId = generateEventId(); + + const context = this.collector.createCodeContext({ + filePath: document.uri.fsPath, + language: document.languageId, + cursorLine: position.line, + cursorColumn: position.character, + }); + + await this.completionCollector.onCompletionShown( + completionId, + context, + { + provider, + model: 'unknown', + }, + suggestedCode + ); + + return completionId; + } + + /** + * 记录补全接受(由外部调用) + */ + public async onCompletionAccepted( + completionId: string, + actualCode?: string + ): Promise { + await this.completionCollector.onCompletionAccepted(completionId, actualCode); + } + + /** + * 记录补全拒绝(由外部调用) + */ + public async onCompletionRejected(completionId: string): Promise { + await this.completionCollector.onCompletionRejected(completionId); + } + + /** + * 获取文档位置的代码上下文 + */ + public getCodeContext( + document: vscode.TextDocument, + position: vscode.Position, + linesBeforeCursor: number = 5 + ): { beforeCursor: string; afterCursor: string } { + const startLine = Math.max(0, position.line - linesBeforeCursor); + const beforeRange = new vscode.Range(startLine, 0, position.line, position.character); + const afterRange = new vscode.Range( + position.line, + position.character, + Math.min(document.lineCount - 1, position.line + 5), + 0 + ); + + return { + beforeCursor: document.getText(beforeRange), + afterCursor: document.getText(afterRange), + }; + } + + /** + * 销毁适配器 + */ + public dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose(); + } + this.disposables = []; + } +} + diff --git a/packages/vscode-extension/src/adapters/editor-adapter.ts b/packages/vscode-extension/src/adapters/editor-adapter.ts new file mode 100644 index 0000000..ba427ca --- /dev/null +++ b/packages/vscode-extension/src/adapters/editor-adapter.ts @@ -0,0 +1,101 @@ +import * as vscode from 'vscode'; +import { Collector, UserBehaviorCollector } from '@ide-collector/core-sdk'; +import { debounce } from '@ide-collector/shared'; + +/** + * 编辑器适配器 + * 用于捕获编辑器活动和用户行为 + */ +export class EditorAdapter { + private collector: Collector; + private behaviorCollector: UserBehaviorCollector; + private lastActivityTime: number = 0; + private inactivityTimeout: ReturnType | null = null; + private readonly INACTIVITY_THRESHOLD = 300000; // 5分钟 + + constructor(collector: Collector) { + this.collector = collector; + this.behaviorCollector = new UserBehaviorCollector(collector); + } + + /** + * 编辑器切换时调用 + */ + public onEditorChange(editor: vscode.TextEditor): void { + this.recordActivity(); + + // 记录当前编辑的语言 + const language = editor.document.languageId; + this.behaviorCollector.trackAIUsage(`editor_language_${language}`); + } + + /** + * 文档变化时调用 + */ + public onDocumentChange = debounce( + (event: vscode.TextDocumentChangeEvent): void => { + this.recordActivity(); + + // 检测是否可能是 AI 生成的代码(大量文本一次性插入) + for (const change of event.contentChanges) { + if (change.text.length > 50 && change.rangeLength === 0) { + this.behaviorCollector.trackAIUsage('possible_ai_insertion'); + } + } + }, + 500 + ); + + /** + * 选择变化时调用 + */ + public onSelectionChange(event: vscode.TextEditorSelectionChangeEvent): void { + this.recordActivity(); + } + + /** + * 记录活动 + */ + private recordActivity(): void { + const now = Date.now(); + + // 如果之前处于不活跃状态,开始新的活动周期 + if (now - this.lastActivityTime > this.INACTIVITY_THRESHOLD) { + this.behaviorCollector.startActivity(); + } + + this.lastActivityTime = now; + + // 重置不活跃计时器 + if (this.inactivityTimeout) { + clearTimeout(this.inactivityTimeout); + } + + this.inactivityTimeout = setTimeout(() => { + this.onInactive(); + }, this.INACTIVITY_THRESHOLD); + } + + /** + * 不活跃时调用 + */ + private async onInactive(): Promise { + await this.behaviorCollector.flushStats(); + } + + /** + * 获取当前活动状态 + */ + public getActivityStatus(): { + isActive: boolean; + lastActivityTime: number; + aiUsageStats: Record; + } { + return { + isActive: Date.now() - this.lastActivityTime < this.INACTIVITY_THRESHOLD, + lastActivityTime: this.lastActivityTime, + aiUsageStats: this.behaviorCollector.getAIUsageStats(), + }; + } +} + diff --git a/packages/vscode-extension/src/config/settings-manager.ts b/packages/vscode-extension/src/config/settings-manager.ts new file mode 100644 index 0000000..aa1690e --- /dev/null +++ b/packages/vscode-extension/src/config/settings-manager.ts @@ -0,0 +1,81 @@ +import * as vscode from 'vscode'; +import type { CollectorConfig, PrivacyConfig, EventType } from '@ide-collector/shared'; +import { DEFAULT_COLLECTOR_CONFIG, DEFAULT_PRIVACY_CONFIG } from '@ide-collector/shared'; + +/** + * VSCode 设置管理器 + */ +export class SettingsManager { + private readonly SECTION = 'ideCollector'; + + /** + * 获取配置项 + */ + public get(key: string, defaultValue: T): T { + const config = vscode.workspace.getConfiguration(this.SECTION); + return config.get(key, defaultValue); + } + + /** + * 设置配置项 + */ + public async set(key: string, value: T, global: boolean = true): Promise { + const config = vscode.workspace.getConfiguration(this.SECTION); + await config.update( + key, + value, + global ? vscode.ConfigurationTarget.Global : vscode.ConfigurationTarget.Workspace + ); + } + + /** + * 获取完整的采集器配置 + */ + public getCollectorConfig(): CollectorConfig { + return { + enabled: this.get('enabled', DEFAULT_COLLECTOR_CONFIG.enabled), + apiEndpoint: this.get('apiEndpoint', DEFAULT_COLLECTOR_CONFIG.apiEndpoint), + samplingRate: this.get('samplingRate', DEFAULT_COLLECTOR_CONFIG.samplingRate), + batchSize: this.get('batchSize', DEFAULT_COLLECTOR_CONFIG.batchSize), + flushIntervalSeconds: this.get('flushInterval', DEFAULT_COLLECTOR_CONFIG.flushIntervalSeconds), + realtimeEnabled: this.get('realtimeEnabled', DEFAULT_COLLECTOR_CONFIG.realtimeEnabled), + privacy: this.getPrivacyConfig(), + eventsToCapture: DEFAULT_COLLECTOR_CONFIG.eventsToCapture, + }; + } + + /** + * 获取隐私配置 + */ + public getPrivacyConfig(): PrivacyConfig { + return { + anonymizeUser: this.get('anonymizeUser', DEFAULT_PRIVACY_CONFIG.anonymizeUser), + obfuscateCode: this.get('obfuscateCode', DEFAULT_PRIVACY_CONFIG.obfuscateCode), + excludePaths: this.get('excludePaths', DEFAULT_PRIVACY_CONFIG.excludePaths), + maxCodeLength: this.get('maxCodeLength', DEFAULT_PRIVACY_CONFIG.maxCodeLength), + }; + } + + /** + * 重置所有设置 + */ + public async resetAll(): Promise { + const config = vscode.workspace.getConfiguration(this.SECTION); + + // 获取所有配置键 + const keys = [ + 'enabled', + 'apiEndpoint', + 'samplingRate', + 'batchSize', + 'flushInterval', + 'anonymizeUser', + 'obfuscateCode', + ]; + + for (const key of keys) { + await config.update(key, undefined, vscode.ConfigurationTarget.Global); + } + } +} + diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts new file mode 100644 index 0000000..e3b2ed0 --- /dev/null +++ b/packages/vscode-extension/src/extension.ts @@ -0,0 +1,177 @@ +import * as vscode from 'vscode'; +import { IDEType } from '@ide-collector/shared'; +import { Collector } from '@ide-collector/core-sdk'; +import { CompletionAdapter } from './adapters/completion-adapter'; +import { EditorAdapter } from './adapters/editor-adapter'; +import { StatusBarManager } from './ui/status-bar'; +import { SettingsManager } from './config/settings-manager'; + +let collector: Collector; +let completionAdapter: CompletionAdapter; +let editorAdapter: EditorAdapter; +let statusBarManager: StatusBarManager; +let settingsManager: SettingsManager; + +/** + * 插件激活时调用 + */ +export async function activate(context: vscode.ExtensionContext): Promise { + console.log('[IDE Collector] Activating extension...'); + + // 初始化设置管理器 + settingsManager = new SettingsManager(); + const config = settingsManager.getCollectorConfig(); + + // 初始化采集器 + collector = Collector.getInstance(config); + + // 检测 IDE 类型 + const ideType = detectIDEType(); + + await collector.initialize({ + ideType, + ideVersion: vscode.version, + os: process.platform, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + + // 初始化适配器 + completionAdapter = new CompletionAdapter(collector); + editorAdapter = new EditorAdapter(collector); + + // 初始化 UI + statusBarManager = new StatusBarManager(collector); + statusBarManager.show(); + + // 注册命令 + registerCommands(context); + + // 注册事件监听器 + registerEventListeners(context); + + // 监听配置变更 + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration('ideCollector')) { + const newConfig = settingsManager.getCollectorConfig(); + collector.updateConfig(newConfig); + statusBarManager.update(); + } + }) + ); + + console.log('[IDE Collector] Extension activated successfully'); +} + +/** + * 插件停用时调用 + */ +export async function deactivate(): Promise { + console.log('[IDE Collector] Deactivating extension...'); + + if (statusBarManager) { + statusBarManager.dispose(); + } + + if (collector) { + await collector.destroy(); + } + + console.log('[IDE Collector] Extension deactivated'); +} + +/** + * 检测 IDE 类型 + */ +function detectIDEType(): IDEType { + const appName = vscode.env.appName.toLowerCase(); + + if (appName.includes('cursor')) { + return IDEType.CURSOR; + } + + return IDEType.VSCODE; +} + +/** + * 注册命令 + */ +function registerCommands(context: vscode.ExtensionContext): void { + // 切换采集开关 + context.subscriptions.push( + vscode.commands.registerCommand('ide-collector.toggle', () => { + const config = collector.getConfig(); + if (config.enabled) { + collector.pause(); + vscode.window.showInformationMessage('IDE Collector: Data collection paused'); + } else { + collector.resume(); + vscode.window.showInformationMessage('IDE Collector: Data collection resumed'); + } + statusBarManager.update(); + }) + ); + + // 显示仪表板 + context.subscriptions.push( + vscode.commands.registerCommand('ide-collector.showDashboard', () => { + const status = collector.getQueueStatus(); + const config = collector.getConfig(); + + vscode.window.showInformationMessage( + `IDE Collector Status:\n` + + `- Enabled: ${config.enabled}\n` + + `- Queue: ${status.size}/${status.maxSize}\n` + + `- Sampling Rate: ${config.samplingRate * 100}%` + ); + }) + ); + + // 打开设置 + context.subscriptions.push( + vscode.commands.registerCommand('ide-collector.openSettings', () => { + vscode.commands.executeCommand( + 'workbench.action.openSettings', + '@ext:devtools-ai.ide-data-collector-vscode' + ); + }) + ); + + // 立即同步 + context.subscriptions.push( + vscode.commands.registerCommand('ide-collector.syncNow', async () => { + await collector.flush(); + vscode.window.showInformationMessage('IDE Collector: Data synced successfully'); + statusBarManager.update(); + }) + ); +} + +/** + * 注册事件监听器 + */ +function registerEventListeners(context: vscode.ExtensionContext): void { + // 监听编辑器变化 + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(editor => { + if (editor) { + editorAdapter.onEditorChange(editor); + } + }) + ); + + // 监听文档变化 + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument(event => { + editorAdapter.onDocumentChange(event); + }) + ); + + // 监听选择变化 + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection(event => { + editorAdapter.onSelectionChange(event); + }) + ); +} + diff --git a/packages/vscode-extension/src/ui/status-bar.ts b/packages/vscode-extension/src/ui/status-bar.ts new file mode 100644 index 0000000..8eb7a64 --- /dev/null +++ b/packages/vscode-extension/src/ui/status-bar.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode'; +import { Collector } from '@ide-collector/core-sdk'; + +/** + * 状态栏管理器 + */ +export class StatusBarManager { + private collector: Collector; + private statusBarItem: vscode.StatusBarItem; + private updateInterval: ReturnType | null = null; + + constructor(collector: Collector) { + this.collector = collector; + this.statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100 + ); + this.statusBarItem.command = 'ide-collector.toggle'; + } + + /** + * 显示状态栏 + */ + public show(): void { + this.update(); + this.statusBarItem.show(); + + // 定期更新状态 + this.updateInterval = setInterval(() => this.update(), 5000); + } + + /** + * 更新状态显示 + */ + public update(): void { + const config = this.collector.getConfig(); + const status = this.collector.getQueueStatus(); + + if (config.enabled) { + this.statusBarItem.text = `$(pulse) IDE Collector (${status.size})`; + this.statusBarItem.tooltip = `Data Collection Active\nQueue: ${status.size}/${status.maxSize}\nClick to pause`; + this.statusBarItem.backgroundColor = undefined; + } else { + this.statusBarItem.text = `$(circle-slash) IDE Collector`; + this.statusBarItem.tooltip = 'Data Collection Paused\nClick to resume'; + this.statusBarItem.backgroundColor = new vscode.ThemeColor( + 'statusBarItem.warningBackground' + ); + } + } + + /** + * 隐藏状态栏 + */ + public hide(): void { + this.statusBarItem.hide(); + } + + /** + * 销毁状态栏 + */ + public dispose(): void { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + this.statusBarItem.dispose(); + } +} + diff --git a/packages/vscode-extension/tsconfig.json b/packages/vscode-extension/tsconfig.json new file mode 100644 index 0000000..48300a1 --- /dev/null +++ b/packages/vscode-extension/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "CommonJS", + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..66330e2 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'packages/*' + diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..3ad412a --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitOverride": true + }, + "exclude": ["node_modules", "dist"] +} + diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..55b874a --- /dev/null +++ b/turbo.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["**/.env.*local"], + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": { + "outputs": [] + }, + "test": { + "dependsOn": ["build"], + "outputs": [] + }, + "clean": { + "cache": false + } + } +} +