feat: 初始化 Monorepo 项目结构
- 添加 pnpm workspace 和 Turborepo 配置 - 创建 packages/shared 共享类型和工具 - 创建 packages/core-sdk 核心 SDK - 创建 packages/vscode-extension VSCode 插件 - 创建 packages/jetbrains-plugin JetBrains 插件基础结构 - 添加 README 文档
This commit is contained in:
parent
c2432e9883
commit
7b40485f60
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -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
|
||||||
|
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
|
|
||||||
150
README.md
150
README.md
@ -1,3 +1,151 @@
|
|||||||
# ide-data-collector
|
# IDE Data Collector
|
||||||
|
|
||||||
多IDE数据采集插件 Monorepo - 核心SDK与各IDE插件统一管理
|
多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
|
||||||
|
|||||||
44
package.json
Normal file
44
package.json
Normal file
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
31
packages/core-sdk/package.json
Normal file
31
packages/core-sdk/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
226
packages/core-sdk/src/collector.ts
Normal file
226
packages/core-sdk/src/collector.ts
Normal file
@ -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<typeof setInterval> | null = null;
|
||||||
|
private isInitialized = false;
|
||||||
|
|
||||||
|
private constructor(config: Partial<CollectorConfig> = {}) {
|
||||||
|
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<CollectorConfig>): Collector {
|
||||||
|
if (!Collector.instance) {
|
||||||
|
Collector.instance = new Collector(config);
|
||||||
|
}
|
||||||
|
return Collector.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化采集器
|
||||||
|
*/
|
||||||
|
public async initialize(userInfo: Omit<UserInfo, 'userId'>): Promise<void> {
|
||||||
|
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<BaseEvent, 'userInfo'>): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<CollectorConfig>): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
this.stopFlushTimer();
|
||||||
|
await this.flush();
|
||||||
|
this.isInitialized = false;
|
||||||
|
Collector.instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建代码上下文
|
||||||
|
*/
|
||||||
|
public createCodeContext(context: Partial<CodeContext>): 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
133
packages/core-sdk/src/collectors/chat-session.ts
Normal file
133
packages/core-sdk/src/collectors/chat-session.ts
Normal file
@ -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<void> {
|
||||||
|
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<ChatSessionEvent, 'userInfo'>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录用户消息
|
||||||
|
*/
|
||||||
|
public async onUserMessage(
|
||||||
|
sessionId: string,
|
||||||
|
message: string
|
||||||
|
): Promise<void> {
|
||||||
|
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<ChatSessionEvent, 'userInfo'>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 AI 响应
|
||||||
|
*/
|
||||||
|
public async onAIResponse(
|
||||||
|
sessionId: string,
|
||||||
|
response: string,
|
||||||
|
latencyMs?: number
|
||||||
|
): Promise<void> {
|
||||||
|
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<ChatSessionEvent, 'userInfo'>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束会话
|
||||||
|
*/
|
||||||
|
public async endSession(sessionId: string): Promise<void> {
|
||||||
|
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<ChatSessionEvent, 'userInfo'>);
|
||||||
|
|
||||||
|
this.activeSessions.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活跃会话数
|
||||||
|
*/
|
||||||
|
public getActiveSessionCount(): number {
|
||||||
|
return this.activeSessions.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
136
packages/core-sdk/src/collectors/code-completion.ts
Normal file
136
packages/core-sdk/src/collectors/code-completion.ts
Normal file
@ -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<void> {
|
||||||
|
// 保存待处理的补全信息
|
||||||
|
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<CodeCompletionEvent, 'userInfo'>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录补全接受
|
||||||
|
*/
|
||||||
|
public async onCompletionAccepted(
|
||||||
|
completionId: string,
|
||||||
|
actualCode?: string
|
||||||
|
): Promise<void> {
|
||||||
|
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<CodeCompletionEvent, 'userInfo'>);
|
||||||
|
|
||||||
|
this.pendingCompletions.delete(completionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录补全拒绝
|
||||||
|
*/
|
||||||
|
public async onCompletionRejected(completionId: string): Promise<void> {
|
||||||
|
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<CodeCompletionEvent, 'userInfo'>);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
4
packages/core-sdk/src/collectors/index.ts
Normal file
4
packages/core-sdk/src/collectors/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { CodeCompletionCollector } from './code-completion';
|
||||||
|
export { ChatSessionCollector } from './chat-session';
|
||||||
|
export { UserBehaviorCollector } from './user-behavior';
|
||||||
|
|
||||||
86
packages/core-sdk/src/collectors/user-behavior.ts
Normal file
86
packages/core-sdk/src/collectors/user-behavior.ts
Normal file
@ -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<string, number> = 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<string, number> {
|
||||||
|
return Object.fromEntries(this.aiUsageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置统计
|
||||||
|
*/
|
||||||
|
public resetStats(): void {
|
||||||
|
this.aiUsageCount.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送使用统计汇总
|
||||||
|
*/
|
||||||
|
public async flushStats(): Promise<void> {
|
||||||
|
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<BaseEvent, 'userInfo'>);
|
||||||
|
|
||||||
|
this.resetStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
102
packages/core-sdk/src/config/config-manager.ts
Normal file
102
packages/core-sdk/src/config/config-manager.ts
Normal file
@ -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<CollectorConfig | null> {
|
||||||
|
const data = await this.storage.get(STORAGE_KEYS.CONFIG);
|
||||||
|
if (data) {
|
||||||
|
const loaded = safeJsonParse<CollectorConfig>(data, this.config);
|
||||||
|
this.config = { ...DEFAULT_COLLECTOR_CONFIG, ...loaded };
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存配置
|
||||||
|
*/
|
||||||
|
public async saveConfig(config: CollectorConfig): Promise<void> {
|
||||||
|
this.config = config;
|
||||||
|
await this.storage.set(STORAGE_KEYS.CONFIG, JSON.stringify(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前配置
|
||||||
|
*/
|
||||||
|
public getConfig(): CollectorConfig {
|
||||||
|
return { ...this.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新部分配置
|
||||||
|
*/
|
||||||
|
public async updateConfig(partial: Partial<CollectorConfig>): Promise<CollectorConfig> {
|
||||||
|
this.config = { ...this.config, ...partial };
|
||||||
|
await this.saveConfig(this.config);
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置为默认配置
|
||||||
|
*/
|
||||||
|
public async resetConfig(): Promise<CollectorConfig> {
|
||||||
|
this.config = { ...DEFAULT_COLLECTOR_CONFIG };
|
||||||
|
await this.saveConfig(this.config);
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户ID
|
||||||
|
*/
|
||||||
|
public async getUserId(): Promise<string | null> {
|
||||||
|
return this.storage.get(STORAGE_KEYS.USER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存用户ID
|
||||||
|
*/
|
||||||
|
public async saveUserId(userId: string): Promise<void> {
|
||||||
|
await this.storage.set(STORAGE_KEYS.USER_ID, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后同步时间
|
||||||
|
*/
|
||||||
|
public async getLastSyncTime(): Promise<Date | null> {
|
||||||
|
const data = await this.storage.get(STORAGE_KEYS.LAST_SYNC);
|
||||||
|
if (data) {
|
||||||
|
return new Date(data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新最后同步时间
|
||||||
|
*/
|
||||||
|
public async updateLastSyncTime(): Promise<void> {
|
||||||
|
await this.storage.set(STORAGE_KEYS.LAST_SYNC, new Date().toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置存储后端
|
||||||
|
*/
|
||||||
|
public setStorage(storage: LocalStorage): void {
|
||||||
|
this.storage = storage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2
packages/core-sdk/src/config/index.ts
Normal file
2
packages/core-sdk/src/config/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { ConfigManager } from './config-manager';
|
||||||
|
|
||||||
8
packages/core-sdk/src/index.ts
Normal file
8
packages/core-sdk/src/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 核心 SDK 导出
|
||||||
|
export * from './collectors';
|
||||||
|
export * from './reporters';
|
||||||
|
export * from './privacy';
|
||||||
|
export * from './storage';
|
||||||
|
export * from './config';
|
||||||
|
export { Collector } from './collector';
|
||||||
|
|
||||||
55
packages/core-sdk/src/privacy/code-sanitizer.ts
Normal file
55
packages/core-sdk/src/privacy/code-sanitizer.ts
Normal file
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
packages/core-sdk/src/privacy/index.ts
Normal file
3
packages/core-sdk/src/privacy/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { PrivacyHandler } from './privacy-handler';
|
||||||
|
export { CodeSanitizer } from './code-sanitizer';
|
||||||
|
|
||||||
123
packages/core-sdk/src/privacy/privacy-handler.ts
Normal file
123
packages/core-sdk/src/privacy/privacy-handler.ts
Normal file
@ -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<BaseEvent['codeData']>
|
||||||
|
): NonNullable<BaseEvent['codeData']> {
|
||||||
|
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<PrivacyConfig>): void {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
104
packages/core-sdk/src/reporters/batch-queue.ts
Normal file
104
packages/core-sdk/src/reporters/batch-queue.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import type { BaseEvent } from '@ide-collector/shared';
|
||||||
|
import { MAX_QUEUE_SIZE } from '@ide-collector/shared';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量队列管理器
|
||||||
|
*/
|
||||||
|
export class BatchQueue<T = BaseEvent> {
|
||||||
|
private queue: T[] = [];
|
||||||
|
private maxSize: number;
|
||||||
|
private onFlush?: (items: T[]) => Promise<void>;
|
||||||
|
|
||||||
|
constructor(maxSize: number = MAX_QUEUE_SIZE, onFlush?: (items: T[]) => Promise<void>) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
this.onFlush = onFlush;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加项目到队列
|
||||||
|
*/
|
||||||
|
public async add(item: T): Promise<void> {
|
||||||
|
this.queue.push(item);
|
||||||
|
|
||||||
|
// 如果达到最大大小,自动刷新
|
||||||
|
if (this.queue.length >= this.maxSize) {
|
||||||
|
await this.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加项目
|
||||||
|
*/
|
||||||
|
public async addBatch(items: T[]): Promise<void> {
|
||||||
|
for (const item of items) {
|
||||||
|
await this.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新队列
|
||||||
|
*/
|
||||||
|
public async flush(): Promise<T[]> {
|
||||||
|
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>): void {
|
||||||
|
this.onFlush = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
97
packages/core-sdk/src/reporters/http-reporter.ts
Normal file
97
packages/core-sdk/src/reporters/http-reporter.ts
Normal file
@ -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<string, string>;
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
await this.sendBatch([event]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量发送事件
|
||||||
|
*/
|
||||||
|
public async sendBatch(events: BaseEvent[]): Promise<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
packages/core-sdk/src/reporters/index.ts
Normal file
3
packages/core-sdk/src/reporters/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { HttpReporter } from './http-reporter';
|
||||||
|
export { BatchQueue } from './batch-queue';
|
||||||
|
|
||||||
113
packages/core-sdk/src/storage/event-queue.ts
Normal file
113
packages/core-sdk/src/storage/event-queue.ts
Normal file
@ -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<void> {
|
||||||
|
this.queue.push(event);
|
||||||
|
|
||||||
|
// 如果超过最大大小,移除最早的事件
|
||||||
|
while (this.queue.length > this.maxSize) {
|
||||||
|
this.queue.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 持久化到存储
|
||||||
|
await this.saveToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出队所有事件
|
||||||
|
*/
|
||||||
|
public async dequeueAll(): Promise<BaseEvent[]> {
|
||||||
|
const events = [...this.queue];
|
||||||
|
this.queue = [];
|
||||||
|
await this.saveToStorage();
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出队指定数量的事件
|
||||||
|
*/
|
||||||
|
public async dequeue(count: number): Promise<BaseEvent[]> {
|
||||||
|
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<void> {
|
||||||
|
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<BaseEvent[]> {
|
||||||
|
const data = await this.storage.get(STORAGE_KEYS.EVENT_QUEUE);
|
||||||
|
if (data) {
|
||||||
|
this.queue = safeJsonParse(data, []);
|
||||||
|
}
|
||||||
|
return this.queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存到存储
|
||||||
|
*/
|
||||||
|
private async saveToStorage(): Promise<void> {
|
||||||
|
await this.storage.set(STORAGE_KEYS.EVENT_QUEUE, JSON.stringify(this.queue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
packages/core-sdk/src/storage/index.ts
Normal file
3
packages/core-sdk/src/storage/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { EventQueue } from './event-queue';
|
||||||
|
export { LocalStorage } from './local-storage';
|
||||||
|
|
||||||
72
packages/core-sdk/src/storage/local-storage.ts
Normal file
72
packages/core-sdk/src/storage/local-storage.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 本地存储抽象层
|
||||||
|
* 用于在不同环境(浏览器、Node.js、VSCode 等)中统一存储接口
|
||||||
|
*/
|
||||||
|
export class LocalStorage {
|
||||||
|
private storage: Map<string, string> = new Map();
|
||||||
|
private persistCallback?: (key: string, value: string | null) => Promise<void>;
|
||||||
|
|
||||||
|
constructor(persistCallback?: (key: string, value: string | null) => Promise<void>) {
|
||||||
|
this.persistCallback = persistCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取值
|
||||||
|
*/
|
||||||
|
public async get(key: string): Promise<string | null> {
|
||||||
|
return this.storage.get(key) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置值
|
||||||
|
*/
|
||||||
|
public async set(key: string, value: string): Promise<void> {
|
||||||
|
this.storage.set(key, value);
|
||||||
|
if (this.persistCallback) {
|
||||||
|
await this.persistCallback(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除值
|
||||||
|
*/
|
||||||
|
public async remove(key: string): Promise<void> {
|
||||||
|
this.storage.delete(key);
|
||||||
|
if (this.persistCallback) {
|
||||||
|
await this.persistCallback(key, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有
|
||||||
|
*/
|
||||||
|
public async clear(): Promise<void> {
|
||||||
|
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>): void {
|
||||||
|
this.persistCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从外部数据恢复
|
||||||
|
*/
|
||||||
|
public restore(data: Record<string, string>): void {
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
this.storage.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
10
packages/core-sdk/tsconfig.json
Normal file
10
packages/core-sdk/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
||||||
51
packages/jetbrains-plugin/build.gradle.kts
Normal file
51
packages/jetbrains-plugin/build.gradle.kts
Normal file
@ -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<JavaCompile> {
|
||||||
|
sourceCompatibility = "17"
|
||||||
|
targetCompatibility = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2
packages/jetbrains-plugin/settings.gradle.kts
Normal file
2
packages/jetbrains-plugin/settings.gradle.kts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = "ide-data-collector-jetbrains"
|
||||||
|
|
||||||
@ -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...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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<String>? = 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<String, Any>? = 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
|
||||||
|
)
|
||||||
|
|
||||||
@ -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<BaseEvent>, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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<BaseEvent>()
|
||||||
|
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<BaseEvent>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
101
packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml
Normal file
101
packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<idea-plugin>
|
||||||
|
<id>com.devtools.collector</id>
|
||||||
|
<name>IDE Data Collector</name>
|
||||||
|
<vendor email="support@devtools-ai.com" url="https://devtools-ai.com">DevTools AI</vendor>
|
||||||
|
|
||||||
|
<description><![CDATA[
|
||||||
|
AI编程工具数据采集插件 - 评估AI代码助手效率。
|
||||||
|
|
||||||
|
<h3>功能特性:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>代码补全采纳/拒绝事件采集</li>
|
||||||
|
<li>AI聊天会话记录</li>
|
||||||
|
<li>开发者行为分析</li>
|
||||||
|
<li>隐私保护和数据脱敏</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>支持的IDE:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>IntelliJ IDEA</li>
|
||||||
|
<li>PyCharm</li>
|
||||||
|
<li>WebStorm</li>
|
||||||
|
<li>GoLand</li>
|
||||||
|
<li>其他 JetBrains IDE</li>
|
||||||
|
</ul>
|
||||||
|
]]></description>
|
||||||
|
|
||||||
|
<change-notes><![CDATA[
|
||||||
|
<h3>0.1.0</h3>
|
||||||
|
<ul>
|
||||||
|
<li>初始版本</li>
|
||||||
|
<li>基础事件采集功能</li>
|
||||||
|
<li>配置管理界面</li>
|
||||||
|
</ul>
|
||||||
|
]]></change-notes>
|
||||||
|
|
||||||
|
<depends>com.intellij.modules.platform</depends>
|
||||||
|
|
||||||
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
|
<!-- 应用级服务 -->
|
||||||
|
<applicationService
|
||||||
|
serviceImplementation="com.devtools.collector.services.CollectorService"/>
|
||||||
|
|
||||||
|
<!-- 项目级服务 -->
|
||||||
|
<projectService
|
||||||
|
serviceImplementation="com.devtools.collector.services.ProjectCollectorService"/>
|
||||||
|
|
||||||
|
<!-- 设置页面 -->
|
||||||
|
<applicationConfigurable
|
||||||
|
parentId="tools"
|
||||||
|
instance="com.devtools.collector.settings.CollectorConfigurable"
|
||||||
|
id="com.devtools.collector.settings"
|
||||||
|
displayName="IDE Data Collector"/>
|
||||||
|
|
||||||
|
<!-- 工具窗口 -->
|
||||||
|
<toolWindow
|
||||||
|
id="IDE Collector"
|
||||||
|
secondary="true"
|
||||||
|
icon="AllIcons.Toolwindows.ToolWindowBuild"
|
||||||
|
anchor="bottom"
|
||||||
|
factoryClass="com.devtools.collector.ui.CollectorToolWindowFactory"/>
|
||||||
|
|
||||||
|
<!-- 状态栏组件 -->
|
||||||
|
<statusBarWidgetFactory
|
||||||
|
id="CollectorStatusWidget"
|
||||||
|
implementation="com.devtools.collector.ui.CollectorStatusWidgetFactory"/>
|
||||||
|
</extensions>
|
||||||
|
|
||||||
|
<applicationListeners>
|
||||||
|
<listener
|
||||||
|
class="com.devtools.collector.listeners.AppLifecycleListener"
|
||||||
|
topic="com.intellij.ide.AppLifecycleListener"/>
|
||||||
|
</applicationListeners>
|
||||||
|
|
||||||
|
<projectListeners>
|
||||||
|
<listener
|
||||||
|
class="com.devtools.collector.listeners.EditorListener"
|
||||||
|
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
|
||||||
|
</projectListeners>
|
||||||
|
|
||||||
|
<actions>
|
||||||
|
<group id="CollectorActions" text="IDE Collector" description="IDE Data Collector Actions">
|
||||||
|
<add-to-group group-id="ToolsMenu" anchor="last"/>
|
||||||
|
|
||||||
|
<action id="Collector.Toggle"
|
||||||
|
class="com.devtools.collector.actions.ToggleCollectionAction"
|
||||||
|
text="Toggle Data Collection"
|
||||||
|
description="Enable or disable data collection"/>
|
||||||
|
|
||||||
|
<action id="Collector.SyncNow"
|
||||||
|
class="com.devtools.collector.actions.SyncNowAction"
|
||||||
|
text="Sync Now"
|
||||||
|
description="Immediately sync collected data"/>
|
||||||
|
|
||||||
|
<action id="Collector.ShowDashboard"
|
||||||
|
class="com.devtools.collector.actions.ShowDashboardAction"
|
||||||
|
text="Show Dashboard"
|
||||||
|
description="Show collection statistics"/>
|
||||||
|
</group>
|
||||||
|
</actions>
|
||||||
|
</idea-plugin>
|
||||||
|
|
||||||
28
packages/shared/package.json
Normal file
28
packages/shared/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
104
packages/shared/src/constants/index.ts
Normal file
104
packages/shared/src/constants/index.ts
Normal file
@ -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
|
||||||
|
];
|
||||||
|
|
||||||
5
packages/shared/src/index.ts
Normal file
5
packages/shared/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// 共享工具函数和类型定义
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
|
export * from './constants';
|
||||||
|
|
||||||
192
packages/shared/src/types/index.ts
Normal file
192
packages/shared/src/types/index.ts
Normal file
@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码补全事件
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
133
packages/shared/src/utils/index.ts
Normal file
133
packages/shared/src/utils/index.ts
Normal file
@ -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<T>(json: string, defaultValue: T): T {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json) as T;
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: unknown[]) => void>(
|
||||||
|
func: T,
|
||||||
|
waitMs: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
func(...args);
|
||||||
|
}, waitMs);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数
|
||||||
|
*/
|
||||||
|
export function throttle<T extends (...args: unknown[]) => void>(
|
||||||
|
func: T,
|
||||||
|
limitMs: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let lastRun = 0;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastRun >= limitMs) {
|
||||||
|
lastRun = now;
|
||||||
|
func(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟执行
|
||||||
|
*/
|
||||||
|
export function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试包装器
|
||||||
|
*/
|
||||||
|
export async function withRetry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxRetries: number = 3,
|
||||||
|
delayMs: number = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
10
packages/shared/tsconfig.json
Normal file
10
packages/shared/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
||||||
130
packages/vscode-extension/package.json
Normal file
130
packages/vscode-extension/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
119
packages/vscode-extension/src/adapters/completion-adapter.ts
Normal file
119
packages/vscode-extension/src/adapters/completion-adapter.ts
Normal file
@ -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<string> {
|
||||||
|
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<void> {
|
||||||
|
await this.completionCollector.onCompletionAccepted(completionId, actualCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录补全拒绝(由外部调用)
|
||||||
|
*/
|
||||||
|
public async onCompletionRejected(completionId: string): Promise<void> {
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
101
packages/vscode-extension/src/adapters/editor-adapter.ts
Normal file
101
packages/vscode-extension/src/adapters/editor-adapter.ts
Normal file
@ -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<typeof setTimeout> | 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<void> {
|
||||||
|
await this.behaviorCollector.flushStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前活动状态
|
||||||
|
*/
|
||||||
|
public getActivityStatus(): {
|
||||||
|
isActive: boolean;
|
||||||
|
lastActivityTime: number;
|
||||||
|
aiUsageStats: Record<string, number>;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
isActive: Date.now() - this.lastActivityTime < this.INACTIVITY_THRESHOLD,
|
||||||
|
lastActivityTime: this.lastActivityTime,
|
||||||
|
aiUsageStats: this.behaviorCollector.getAIUsageStats(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
81
packages/vscode-extension/src/config/settings-manager.ts
Normal file
81
packages/vscode-extension/src/config/settings-manager.ts
Normal file
@ -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<T>(key: string, defaultValue: T): T {
|
||||||
|
const config = vscode.workspace.getConfiguration(this.SECTION);
|
||||||
|
return config.get<T>(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置配置项
|
||||||
|
*/
|
||||||
|
public async set<T>(key: string, value: T, global: boolean = true): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
177
packages/vscode-extension/src/extension.ts
Normal file
177
packages/vscode-extension/src/extension.ts
Normal file
@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
70
packages/vscode-extension/src/ui/status-bar.ts
Normal file
70
packages/vscode-extension/src/ui/status-bar.ts
Normal file
@ -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<typeof setInterval> | 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
12
packages/vscode-extension/tsconfig.json
Normal file
12
packages/vscode-extension/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
||||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- 'packages/*'
|
||||||
|
|
||||||
24
tsconfig.base.json
Normal file
24
tsconfig.base.json
Normal file
@ -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"]
|
||||||
|
}
|
||||||
|
|
||||||
25
turbo.json
Normal file
25
turbo.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user