cc-team-viewer 1.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/build.js +15 -0
- package/cli/cli.js +446 -0
- package/cli/findcc.js +107 -0
- package/dist/assets/index-DOhmAaPC.css +10 -0
- package/dist/assets/index-DRdG9uNS.js +428 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +15 -0
- package/lib/updater.js +117 -0
- package/package.json +72 -0
- package/proxy/interceptor.js +926 -0
- package/proxy/proxy-env.js +21 -0
- package/proxy/proxy.js +172 -0
- package/server/registry.js +351 -0
- package/server/routes/index.js +321 -0
- package/server/routes/logs.js +347 -0
- package/server/routes/misc.js +316 -0
- package/server/routes/project.js +259 -0
- package/server/routes/teams.js +358 -0
- package/server/server.js +228 -0
- package/server/sse.js +503 -0
- package/server/stats-worker.js +377 -0
- package/src/vite.config.js +89 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Stats Worker — 后台线程,扫描 JSONL 日志生成项目级统计 JSON
|
|
2
|
+
import { parentPort } from 'node:worker_threads';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// 统计 schema 版本号,新增统计字段时递增,强制旧缓存失效重新解析
|
|
7
|
+
const STATS_VERSION = 3;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 解析单个 JSONL 文件,提取模型使用次数和 token 统计
|
|
11
|
+
* @param {string} filePath JSONL 文件绝对路径
|
|
12
|
+
* @returns {{ models: Object, summary: Object, members: Object }}
|
|
13
|
+
*/
|
|
14
|
+
function parseJsonlFile(filePath) {
|
|
15
|
+
const models = {};
|
|
16
|
+
let requestCount = 0;
|
|
17
|
+
let sessionCount = 0;
|
|
18
|
+
let totalInput = 0;
|
|
19
|
+
let totalOutput = 0;
|
|
20
|
+
let totalCacheRead = 0;
|
|
21
|
+
let totalCacheCreation = 0;
|
|
22
|
+
|
|
23
|
+
// 成员统计
|
|
24
|
+
const members = {};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
28
|
+
if (!content.trim()) return { models, summary: { requestCount: 0, sessionCount: 0, input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 }, members: {} };
|
|
29
|
+
|
|
30
|
+
const entries = content.split('\n---\n').filter(p => p.trim());
|
|
31
|
+
for (const raw of entries) {
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(raw);
|
|
34
|
+
requestCount++;
|
|
35
|
+
|
|
36
|
+
// 会话轮次:MainAgent 且 messages.length === 1 表示一次新会话开始
|
|
37
|
+
if (entry.mainAgent && Array.isArray(entry.body?.messages) && entry.body.messages.length === 1) {
|
|
38
|
+
sessionCount++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 提取模型名:优先 body.model,其次 response.body.model
|
|
42
|
+
const model = entry.body?.model || entry.response?.body?.model;
|
|
43
|
+
if (!model) continue;
|
|
44
|
+
|
|
45
|
+
if (!models[model]) {
|
|
46
|
+
models[model] = { count: 0, input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
|
|
47
|
+
}
|
|
48
|
+
models[model].count++;
|
|
49
|
+
|
|
50
|
+
// 提取 usage — 可能在 response.body.usage
|
|
51
|
+
const usage = entry.response?.body?.usage;
|
|
52
|
+
if (usage) {
|
|
53
|
+
const inp = usage.input_tokens || 0;
|
|
54
|
+
const out = usage.output_tokens || 0;
|
|
55
|
+
const cacheRead = usage.cache_read_input_tokens || usage.cache_creation_input_tokens ? (usage.cache_read_input_tokens || 0) : 0;
|
|
56
|
+
const cacheCreate = usage.cache_creation_input_tokens || 0;
|
|
57
|
+
|
|
58
|
+
models[model].input_tokens += inp;
|
|
59
|
+
models[model].output_tokens += out;
|
|
60
|
+
models[model].cache_read_input_tokens += cacheRead;
|
|
61
|
+
models[model].cache_creation_input_tokens += cacheCreate;
|
|
62
|
+
|
|
63
|
+
totalInput += inp;
|
|
64
|
+
totalOutput += out;
|
|
65
|
+
totalCacheRead += cacheRead;
|
|
66
|
+
totalCacheCreation += cacheCreate;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 成员统计:提取 teamMember
|
|
70
|
+
const teamMember = entry.teamMember;
|
|
71
|
+
if (teamMember) {
|
|
72
|
+
if (!members[teamMember]) {
|
|
73
|
+
members[teamMember] = {
|
|
74
|
+
requestCount: 0,
|
|
75
|
+
successCount: 0,
|
|
76
|
+
errorCount: 0,
|
|
77
|
+
inputTokens: 0,
|
|
78
|
+
outputTokens: 0,
|
|
79
|
+
toolCalls: {},
|
|
80
|
+
skillCalls: {},
|
|
81
|
+
lastActive: null,
|
|
82
|
+
systemPrompt: entry.body?.system || '',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
members[teamMember].requestCount++;
|
|
86
|
+
members[teamMember].inputTokens += usage?.input_tokens || 0;
|
|
87
|
+
members[teamMember].outputTokens += usage?.output_tokens || 0;
|
|
88
|
+
members[teamMember].lastActive = entry.timestamp;
|
|
89
|
+
|
|
90
|
+
// 统计成功/失败
|
|
91
|
+
const error = entry.response?.body?.error;
|
|
92
|
+
if (error) {
|
|
93
|
+
members[teamMember].errorCount++;
|
|
94
|
+
} else {
|
|
95
|
+
members[teamMember].successCount++;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 统计 Tool 调用
|
|
99
|
+
const content = entry.response?.body?.content;
|
|
100
|
+
if (Array.isArray(content)) {
|
|
101
|
+
for (const block of content) {
|
|
102
|
+
if (block.type === 'tool_use' && block.name) {
|
|
103
|
+
members[teamMember].toolCalls[block.name] = (members[teamMember].toolCalls[block.name] || 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 统计 Skill 调用(从请求体中检测 # skill-name)
|
|
109
|
+
const messages = entry.body?.messages;
|
|
110
|
+
if (Array.isArray(messages)) {
|
|
111
|
+
for (const msg of messages) {
|
|
112
|
+
const msgContent = msg?.content;
|
|
113
|
+
if (typeof msgContent === 'string') {
|
|
114
|
+
const skillMatch = msgContent.match(/#\s+(\S+)/g);
|
|
115
|
+
if (skillMatch) {
|
|
116
|
+
for (const skill of skillMatch) {
|
|
117
|
+
const skillName = skill.replace(/^#\s+/, '').trim();
|
|
118
|
+
if (skillName) {
|
|
119
|
+
members[teamMember].skillCalls[skillName] = (members[teamMember].skillCalls[skillName] || 0) + 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else if (Array.isArray(msgContent)) {
|
|
124
|
+
for (const block of msgContent) {
|
|
125
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
126
|
+
const skillMatch = block.text.match(/#\s+(\S+)/g);
|
|
127
|
+
if (skillMatch) {
|
|
128
|
+
for (const skill of skillMatch) {
|
|
129
|
+
const skillName = skill.replace(/^#\s+/, '').trim();
|
|
130
|
+
if (skillName) {
|
|
131
|
+
members[teamMember].skillCalls[skillName] = (members[teamMember].skillCalls[skillName] || 0) + 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// 跳过无法解析的条目
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// 文件读取失败
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
models,
|
|
151
|
+
summary: {
|
|
152
|
+
requestCount,
|
|
153
|
+
sessionCount,
|
|
154
|
+
input_tokens: totalInput,
|
|
155
|
+
output_tokens: totalOutput,
|
|
156
|
+
cache_read_input_tokens: totalCacheRead,
|
|
157
|
+
cache_creation_input_tokens: totalCacheCreation,
|
|
158
|
+
},
|
|
159
|
+
members,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 为单个项目生成或增量更新统计 JSON
|
|
165
|
+
* @param {string} projectDir 项目日志目录
|
|
166
|
+
* @param {string} projectName 项目名
|
|
167
|
+
* @param {string|null} onlyFile 仅更新此文件(增量),null 表示智能增量
|
|
168
|
+
*/
|
|
169
|
+
function generateProjectStats(projectDir, projectName, onlyFile) {
|
|
170
|
+
const statsFile = join(projectDir, `${projectName}.json`);
|
|
171
|
+
|
|
172
|
+
// 读取已有统计(用于增量更新)
|
|
173
|
+
let existing = null;
|
|
174
|
+
try {
|
|
175
|
+
if (existsSync(statsFile)) {
|
|
176
|
+
existing = JSON.parse(readFileSync(statsFile, 'utf-8'));
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
existing = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 列出所有 JSONL 文件(排除 _temp.jsonl)
|
|
183
|
+
let jsonlFiles;
|
|
184
|
+
try {
|
|
185
|
+
jsonlFiles = readdirSync(projectDir)
|
|
186
|
+
.filter(f => f.endsWith('.jsonl') && !f.endsWith('_temp.jsonl'))
|
|
187
|
+
.sort();
|
|
188
|
+
} catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (jsonlFiles.length === 0) return;
|
|
193
|
+
|
|
194
|
+
const filesStats = {};
|
|
195
|
+
const topModels = {};
|
|
196
|
+
|
|
197
|
+
for (const f of jsonlFiles) {
|
|
198
|
+
const filePath = join(projectDir, f);
|
|
199
|
+
let stat;
|
|
200
|
+
try {
|
|
201
|
+
stat = statSync(filePath);
|
|
202
|
+
} catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const size = stat.size;
|
|
207
|
+
const lastModified = stat.mtime.toISOString();
|
|
208
|
+
|
|
209
|
+
// 增量优化:如果有已有统计且文件未变化且 schema 版本一致,直接复用
|
|
210
|
+
if (existing?._v === STATS_VERSION
|
|
211
|
+
&& existing?.files?.[f] && existing.files[f].size === size && existing.files[f].lastModified === lastModified) {
|
|
212
|
+
// 如果指定了 onlyFile 且不是此文件,跳过重新解析
|
|
213
|
+
if (!onlyFile || onlyFile !== f) {
|
|
214
|
+
filesStats[f] = existing.files[f];
|
|
215
|
+
// 汇总模型
|
|
216
|
+
if (filesStats[f].models) {
|
|
217
|
+
for (const [model, data] of Object.entries(filesStats[f].models)) {
|
|
218
|
+
if (!topModels[model]) topModels[model] = 0;
|
|
219
|
+
topModels[model] += data.count;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 需要重新解析
|
|
227
|
+
const parsed = parseJsonlFile(filePath);
|
|
228
|
+
filesStats[f] = {
|
|
229
|
+
models: parsed.models,
|
|
230
|
+
summary: parsed.summary,
|
|
231
|
+
members: parsed.members,
|
|
232
|
+
size,
|
|
233
|
+
lastModified,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// 汇总模型使用次数
|
|
237
|
+
for (const [model, data] of Object.entries(parsed.models)) {
|
|
238
|
+
if (!topModels[model]) topModels[model] = 0;
|
|
239
|
+
topModels[model] += data.count;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 计算全局汇总
|
|
244
|
+
let totalRequests = 0;
|
|
245
|
+
let totalSessions = 0;
|
|
246
|
+
let totalInput = 0;
|
|
247
|
+
let totalOutput = 0;
|
|
248
|
+
let totalCacheRead = 0;
|
|
249
|
+
let totalCacheCreation = 0;
|
|
250
|
+
|
|
251
|
+
// 汇总成员统计
|
|
252
|
+
const allMembers = {};
|
|
253
|
+
|
|
254
|
+
for (const f of Object.values(filesStats)) {
|
|
255
|
+
totalRequests += f.summary.requestCount;
|
|
256
|
+
totalSessions += f.summary.sessionCount || 0;
|
|
257
|
+
totalInput += f.summary.input_tokens;
|
|
258
|
+
totalOutput += f.summary.output_tokens;
|
|
259
|
+
totalCacheRead += f.summary.cache_read_input_tokens;
|
|
260
|
+
totalCacheCreation += f.summary.cache_creation_input_tokens;
|
|
261
|
+
|
|
262
|
+
// 合并成员统计
|
|
263
|
+
if (f.members) {
|
|
264
|
+
for (const [memberName, memberData] of Object.entries(f.members)) {
|
|
265
|
+
if (!allMembers[memberName]) {
|
|
266
|
+
allMembers[memberName] = {
|
|
267
|
+
requestCount: 0,
|
|
268
|
+
successCount: 0,
|
|
269
|
+
errorCount: 0,
|
|
270
|
+
inputTokens: 0,
|
|
271
|
+
outputTokens: 0,
|
|
272
|
+
toolCalls: {},
|
|
273
|
+
skillCalls: {},
|
|
274
|
+
lastActive: null,
|
|
275
|
+
systemPrompt: memberData.systemPrompt || '',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
allMembers[memberName].requestCount += memberData.requestCount || 0;
|
|
279
|
+
allMembers[memberName].successCount += memberData.successCount || 0;
|
|
280
|
+
allMembers[memberName].errorCount += memberData.errorCount || 0;
|
|
281
|
+
allMembers[memberName].inputTokens += memberData.inputTokens || 0;
|
|
282
|
+
allMembers[memberName].outputTokens += memberData.outputTokens || 0;
|
|
283
|
+
|
|
284
|
+
// 合并 toolCalls
|
|
285
|
+
for (const [toolName, count] of Object.entries(memberData.toolCalls || {})) {
|
|
286
|
+
allMembers[memberName].toolCalls[toolName] = (allMembers[memberName].toolCalls[toolName] || 0) + count;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 合并 skillCalls
|
|
290
|
+
for (const [skillName, count] of Object.entries(memberData.skillCalls || {})) {
|
|
291
|
+
allMembers[memberName].skillCalls[skillName] = (allMembers[memberName].skillCalls[skillName] || 0) + count;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 更新最近活跃时间
|
|
295
|
+
if (memberData.lastActive && (!allMembers[memberName].lastActive || memberData.lastActive > allMembers[memberName].lastActive)) {
|
|
296
|
+
allMembers[memberName].lastActive = memberData.lastActive;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 保存 systemPrompt(取最新的)
|
|
300
|
+
if (memberData.systemPrompt) {
|
|
301
|
+
allMembers[memberName].systemPrompt = memberData.systemPrompt;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const stats = {
|
|
308
|
+
_v: STATS_VERSION,
|
|
309
|
+
project: projectName,
|
|
310
|
+
updatedAt: new Date().toISOString(),
|
|
311
|
+
models: topModels,
|
|
312
|
+
files: filesStats,
|
|
313
|
+
members: allMembers,
|
|
314
|
+
summary: {
|
|
315
|
+
requestCount: totalRequests,
|
|
316
|
+
sessionCount: totalSessions,
|
|
317
|
+
fileCount: jsonlFiles.length,
|
|
318
|
+
input_tokens: totalInput,
|
|
319
|
+
output_tokens: totalOutput,
|
|
320
|
+
cache_read_input_tokens: totalCacheRead,
|
|
321
|
+
cache_creation_input_tokens: totalCacheCreation,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
writeFileSync(statsFile, JSON.stringify(stats, null, 2));
|
|
327
|
+
} catch (err) {
|
|
328
|
+
parentPort?.postMessage({ type: 'error', message: `Failed to write stats: ${err.message}` });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 扫描 logDir 下所有项目目录,逐个生成统计
|
|
334
|
+
*/
|
|
335
|
+
function scanAllProjects(logDir) {
|
|
336
|
+
try {
|
|
337
|
+
const entries = readdirSync(logDir, { withFileTypes: true });
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
if (!entry.isDirectory()) continue;
|
|
340
|
+
const projectDir = join(logDir, entry.name);
|
|
341
|
+
generateProjectStats(projectDir, entry.name, null);
|
|
342
|
+
}
|
|
343
|
+
parentPort?.postMessage({ type: 'scan-all-done' });
|
|
344
|
+
} catch (err) {
|
|
345
|
+
parentPort?.postMessage({ type: 'error', message: `scan-all failed: ${err.message}` });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Worker 消息处理
|
|
350
|
+
parentPort?.on('message', (msg) => {
|
|
351
|
+
switch (msg.type) {
|
|
352
|
+
case 'init': {
|
|
353
|
+
const { logDir, projectName } = msg;
|
|
354
|
+
const projectDir = join(logDir, projectName);
|
|
355
|
+
if (existsSync(projectDir)) {
|
|
356
|
+
generateProjectStats(projectDir, projectName, null);
|
|
357
|
+
parentPort?.postMessage({ type: 'init-done', projectName });
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case 'update': {
|
|
362
|
+
const { logDir, projectName, logFile } = msg;
|
|
363
|
+
const projectDir = join(logDir, projectName);
|
|
364
|
+
const fileName = basename(logFile);
|
|
365
|
+
if (existsSync(projectDir)) {
|
|
366
|
+
generateProjectStats(projectDir, projectName, fileName);
|
|
367
|
+
parentPort?.postMessage({ type: 'update-done', projectName, logFile: fileName });
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case 'scan-all': {
|
|
372
|
+
const { logDir } = msg;
|
|
373
|
+
scanAllProjects(logDir);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
function findCurrentPort() {
|
|
11
|
+
const currentProject = process.cwd().split('/').pop();
|
|
12
|
+
console.error('[Vite] Current project:', currentProject);
|
|
13
|
+
|
|
14
|
+
// 1. 开发模式:使用环境变量 CC_VIEWER_PORT 指定开发环境服务器端口
|
|
15
|
+
if (process.env.CC_VIEWER_PORT) {
|
|
16
|
+
console.error('[Vite] Using port from CC_VIEWER_PORT (dev mode):', process.env.CC_VIEWER_PORT);
|
|
17
|
+
return parseInt(process.env.CC_VIEWER_PORT, 10);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. 自动查找:遍历端口,找到与当前项目匹配且有最新活动的端口
|
|
21
|
+
let matchedPort = 7008;
|
|
22
|
+
let latestTimestamp = '';
|
|
23
|
+
|
|
24
|
+
for (let port = 7008; port <= 7008; port++) {
|
|
25
|
+
try {
|
|
26
|
+
const result = execSync(`lsof -i :${port} -sTCP:LISTEN -t`, { encoding: 'utf-8' });
|
|
27
|
+
if (result.trim()) {
|
|
28
|
+
// 先验证这是 cc-team-viewer server(通过检查 /api/teams 端点)
|
|
29
|
+
let isCCViewerServer = false;
|
|
30
|
+
try {
|
|
31
|
+
const teamsResponse = execSync(`curl -s "http://127.0.0.1:${port}/api/teams" --max-time 1`, { encoding: 'utf-8' });
|
|
32
|
+
// cc-team-viewer server 返回 JSON,ccv proxy 返回 HTML
|
|
33
|
+
if (teamsResponse.trim().startsWith('{') || teamsResponse.trim().startsWith('[')) {
|
|
34
|
+
isCCViewerServer = true;
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// /api/teams 可能不存在,跳过这个端口
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isCCViewerServer) {
|
|
42
|
+
console.error(`[Vite] Port ${port}: not a cc-team-viewer server, skipping`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 获取这个端口的最新请求
|
|
47
|
+
try {
|
|
48
|
+
const response = execSync(`curl -s "http://127.0.0.1:${port}/api/requests?limit=1" --max-time 2`, { encoding: 'utf-8' });
|
|
49
|
+
const requests = JSON.parse(response);
|
|
50
|
+
if (requests && requests.length > 0) {
|
|
51
|
+
const lastRequest = requests[0];
|
|
52
|
+
const lastProject = lastRequest.project;
|
|
53
|
+
const lastTimestamp = lastRequest.timestamp;
|
|
54
|
+
|
|
55
|
+
console.error(`[Vite] Port ${port}: project="${lastProject}", timestamp="${lastTimestamp}"`);
|
|
56
|
+
|
|
57
|
+
// 如果项目名匹配,且时间戳更新,则选择这个端口
|
|
58
|
+
if (lastProject === currentProject && lastTimestamp > latestTimestamp) {
|
|
59
|
+
latestTimestamp = lastTimestamp;
|
|
60
|
+
matchedPort = port;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// Ignore curl errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error(`[Vite] Selected port: ${matchedPort} (latest timestamp: ${latestTimestamp})`);
|
|
71
|
+
return matchedPort;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default defineConfig(() => {
|
|
75
|
+
const port = findCurrentPort();
|
|
76
|
+
console.error('[Vite] CC-team-Viewer backend port:', port);
|
|
77
|
+
return {
|
|
78
|
+
plugins: [react()],
|
|
79
|
+
build: {
|
|
80
|
+
outDir: resolve(__dirname, '..', 'dist'),
|
|
81
|
+
},
|
|
82
|
+
server: {
|
|
83
|
+
proxy: {
|
|
84
|
+
'/events': `http://127.0.0.1:${port}`,
|
|
85
|
+
'/api': `http://127.0.0.1:${port}`,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|