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:
tangweijie 2026-01-05 18:08:18 +08:00
parent c2432e9883
commit 7b40485f60
46 changed files with 3234 additions and 2 deletions

16
.editorconfig Normal file
View 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
View File

@ -0,0 +1,10 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid"
}

150
README.md
View File

@ -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
View 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"
]
}
}

View 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"
}
}

View 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;
}
}
}

View 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;
}
}

View 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);
}
}
}
}

View File

@ -0,0 +1,4 @@
export { CodeCompletionCollector } from './code-completion';
export { ChatSessionCollector } from './chat-session';
export { UserBehaviorCollector } from './user-behavior';

View 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();
}
}

View 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;
}
}

View File

@ -0,0 +1,2 @@
export { ConfigManager } from './config-manager';

View 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';

View 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];
}
}

View File

@ -0,0 +1,3 @@
export { PrivacyHandler } from './privacy-handler';
export { CodeSanitizer } from './code-sanitizer';

View 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 };
}
}

View 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;
}
}

View 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}`;
}
}

View File

@ -0,0 +1,3 @@
export { HttpReporter } from './http-reporter';
export { BatchQueue } from './batch-queue';

View 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));
}
}

View File

@ -0,0 +1,3 @@
export { EventQueue } from './event-queue';
export { LocalStorage } from './local-storage';

View File

@ -0,0 +1,72 @@
/**
*
* Node.jsVSCode
*/
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);
}
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View 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"))
}
}

View File

@ -0,0 +1,2 @@
rootProject.name = "ide-data-collector-jetbrains"

View File

@ -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...")
}
}

View File

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

View File

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

View File

@ -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)
}
}

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

View 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"
}
}

View 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
];

View File

@ -0,0 +1,5 @@
// 共享工具函数和类型定义
export * from './types';
export * from './utils';
export * from './constants';

View 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;
}

View 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;
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View 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"
}
}

View 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 = [];
}
}

View 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(),
};
}
}

View 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);
}
}
}

View 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);
})
);
}

View 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();
}
}

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

@ -0,0 +1,3 @@
packages:
- 'packages/*'

24
tsconfig.base.json Normal file
View 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
View 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
}
}
}