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 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