coding-tool-x 3.5.11 → 3.5.13

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +11 -1
  3. package/dist/web/assets/{Analytics-Bw-0r4Ys.js → Analytics-B9Shk-uq.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-C6QRIeDj.js → ConfigTemplates-Dlkn-_zW.js} +1 -1
  5. package/dist/web/assets/Home-AGf32LvJ.js +1 -0
  6. package/dist/web/assets/Home-BeGkC4bb.css +1 -0
  7. package/dist/web/assets/{PluginManager-Ce-mws6Q.js → PluginManager-BnAjHN4u.js} +1 -1
  8. package/dist/web/assets/{ProjectList-9Z7FFCIT.js → ProjectList-7YV_zk9L.js} +1 -1
  9. package/dist/web/assets/{SessionList-CyETQcs8.js → SessionList-D0un-UKS.js} +1 -1
  10. package/dist/web/assets/{SkillManager-DSUcP1se.js → SkillManager-DhYBmbtd.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-Cvbc11bJ.js → WorkspaceManager-JOZIPAGo.js} +1 -1
  12. package/dist/web/assets/icons-QFXzEgqM.js +1 -0
  13. package/dist/web/assets/index-DM3yF1Oo.js +2 -0
  14. package/dist/web/assets/{index-BS7aplYp.css → index-dzKgnRhx.css} +1 -1
  15. package/dist/web/index.html +4 -4
  16. package/package.json +2 -1
  17. package/src/commands/daemon.js +6 -2
  18. package/src/commands/ui.js +8 -3
  19. package/src/index.js +2 -0
  20. package/src/server/api/hooks.js +18 -1
  21. package/src/server/codex-proxy-server.js +22 -6
  22. package/src/server/gemini-proxy-server.js +3 -1
  23. package/src/server/index.js +37 -5
  24. package/src/server/opencode-proxy-server.js +39 -14
  25. package/src/server/proxy-server.js +3 -1
  26. package/src/server/services/base/proxy-utils.js +41 -0
  27. package/src/server/services/base/response-usage-parser.js +137 -59
  28. package/src/server/services/codex-statistics-service.js +1 -0
  29. package/src/server/services/gemini-statistics-service.js +1 -0
  30. package/src/server/services/https-cert.js +202 -0
  31. package/src/server/services/notification-hooks.js +138 -6
  32. package/src/server/services/opencode-gateway-converter.js +3 -0
  33. package/src/server/services/opencode-statistics-service.js +1 -0
  34. package/src/server/services/proxy-log-helper.js +44 -12
  35. package/src/server/services/statistics-service.js +73 -28
  36. package/src/server/services/web-build.js +172 -0
  37. package/src/server/websocket-server.js +18 -1
  38. package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
  39. package/dist/web/assets/Home-B_Z9ythw.js +0 -1
  40. package/dist/web/assets/icons-lKCTySl7.js +0 -1
  41. package/dist/web/assets/index-CTeg1xG-.js +0 -2
@@ -12,6 +12,30 @@ function normalizeToolSource(source = '') {
12
12
  return 'claude';
13
13
  }
14
14
 
15
+ function normalizeModelName(value = '') {
16
+ return typeof value === 'string' ? value.trim() : '';
17
+ }
18
+
19
+ function resolveActualModel(model = '', metadata = {}) {
20
+ const candidates = [
21
+ model,
22
+ metadata.redirectedModel,
23
+ metadata.modelFromUrl,
24
+ metadata.requestModel,
25
+ metadata.model,
26
+ metadata.originalModel
27
+ ];
28
+
29
+ for (const candidate of candidates) {
30
+ const normalized = normalizeModelName(candidate);
31
+ if (normalized) {
32
+ return normalized;
33
+ }
34
+ }
35
+
36
+ return '';
37
+ }
38
+
15
39
  function normalizeUsageTokens(source, tokens = {}) {
16
40
  const normalizedSource = normalizeToolSource(source);
17
41
  const input = toNumber(tokens.input);
@@ -68,7 +92,8 @@ function buildSuccessLogPayload({
68
92
  redirectedModel,
69
93
  tokens,
70
94
  cost = 0,
71
- timestamp = Date.now()
95
+ timestamp = Date.now(),
96
+ usageMissing = false
72
97
  }) {
73
98
  const normalized = normalizeUsageTokens(source, tokens);
74
99
  const payload = {
@@ -87,7 +112,8 @@ function buildSuccessLogPayload({
87
112
  totalTokens: normalized.total,
88
113
  cost,
89
114
  source: normalizeToolSource(source),
90
- timestamp
115
+ timestamp,
116
+ usageMissing: Boolean(usageMissing)
91
117
  };
92
118
  if (originalModel) {
93
119
  payload.originalModel = originalModel;
@@ -151,40 +177,43 @@ function publishUsageLog({
151
177
  recordSuccess,
152
178
  allowBroadcast = true
153
179
  }) {
154
- if (!hasMeaningfulUsage(source, tokens)) {
155
- return null;
156
- }
157
-
158
180
  const normalizedSource = normalizeToolSource(source);
181
+ const actualModel = resolveActualModel(model, metadata);
159
182
  const normalizedTokens = normalizeUsageTokens(normalizedSource, tokens);
183
+ const usageAvailable = hasMeaningfulUsage(normalizedSource, normalizedTokens);
160
184
  const requestId = metadata.id || `${normalizedSource}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
161
185
  const timestamp = Date.now();
162
186
  const cost = typeof calculateCost === 'function'
163
- ? calculateCost(model || '', normalizedTokens)
187
+ ? calculateCost(actualModel || '', normalizedTokens)
164
188
  : 0;
165
189
 
190
+ if (!usageAvailable && !actualModel) {
191
+ return null;
192
+ }
193
+
166
194
  if (allowBroadcast && typeof broadcastLog === 'function') {
167
195
  broadcastLog(buildSuccessLogPayload({
168
196
  source: normalizedSource,
169
197
  requestId,
170
198
  channel: metadata.channel,
171
- model,
199
+ model: actualModel,
172
200
  originalModel: metadata.originalModel,
173
201
  redirectedModel: metadata.redirectedModel,
174
202
  tokens: normalizedTokens,
175
203
  cost,
176
- timestamp
204
+ timestamp,
205
+ usageMissing: !usageAvailable
177
206
  }));
178
207
  }
179
208
 
180
- if (typeof recordRequest === 'function') {
209
+ if (usageAvailable && typeof recordRequest === 'function') {
181
210
  const entry = {
182
211
  id: requestId,
183
212
  timestamp: new Date(metadata.startTime || timestamp).toISOString(),
184
213
  toolType: normalizedSource === 'claude' ? 'claude-code' : normalizedSource,
185
214
  channel: metadata.channel,
186
215
  channelId: metadata.channelId,
187
- model: model || '',
216
+ model: actualModel || '',
188
217
  tokens: {
189
218
  input: normalizedTokens.input,
190
219
  output: normalizedTokens.output,
@@ -213,8 +242,10 @@ function publishUsageLog({
213
242
 
214
243
  return {
215
244
  cost,
245
+ model: actualModel,
216
246
  tokens: normalizedTokens,
217
- timestamp
247
+ timestamp,
248
+ usageMissing: !usageAvailable
218
249
  };
219
250
  }
220
251
 
@@ -250,6 +281,7 @@ function publishFailureLog({
250
281
  module.exports = {
251
282
  toNumber,
252
283
  normalizeToolSource,
284
+ resolveActualModel,
253
285
  normalizeUsageTokens,
254
286
  hasMeaningfulUsage,
255
287
  formatRealtimeTime,
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { PATHS } = require('../../config/paths');
4
+ const { normalizeUsageTokens, resolveActualModel } = require('./proxy-log-helper');
4
5
 
5
6
  // 北京时间辅助(UTC+8),统一所有时间计算
6
7
  const CST_OFFSET_MS = 8 * 60 * 60 * 1000;
@@ -189,6 +190,8 @@ function initStatsObject() {
189
190
  output: 0,
190
191
  cacheCreation: 0,
191
192
  cacheRead: 0,
193
+ cached: 0,
194
+ reasoning: 0,
192
195
  total: 0
193
196
  },
194
197
  cost: 0
@@ -202,7 +205,9 @@ function updateStats(stats, tokens, cost) {
202
205
  stats.tokens.output += tokens.output || 0;
203
206
  stats.tokens.cacheCreation += tokens.cacheCreation || 0;
204
207
  stats.tokens.cacheRead += tokens.cacheRead || 0;
205
- stats.tokens.total += tokens.total || 0;
208
+ stats.tokens.cached += tokens.cached || 0;
209
+ stats.tokens.reasoning += tokens.reasoning || 0;
210
+ stats.tokens.total += getTokenTotal(tokens);
206
211
  stats.cost += cost || 0;
207
212
  }
208
213
 
@@ -226,6 +231,9 @@ function recordRequest(requestData) {
226
231
  session,
227
232
  project
228
233
  } = requestData;
234
+ const resolvedModel = resolveActualModel(model, requestData);
235
+ const modelKey = resolvedModel || 'unknown';
236
+ const totalTokens = getTokenTotal(tokens);
229
237
 
230
238
  // 1. 写入详细日志
231
239
  const logEntry = {
@@ -234,7 +242,7 @@ function recordRequest(requestData) {
234
242
  toolType,
235
243
  channel,
236
244
  channelId,
237
- model,
245
+ model: resolvedModel,
238
246
  tokens,
239
247
  duration,
240
248
  success,
@@ -256,7 +264,7 @@ function recordRequest(requestData) {
256
264
 
257
265
  // 更新全局统计
258
266
  globalStats.global.totalRequests += 1;
259
- globalStats.global.totalTokens += tokens.total || 0;
267
+ globalStats.global.totalTokens += totalTokens;
260
268
  globalStats.global.totalCost += cost || 0;
261
269
 
262
270
  // 按工具类型统计
@@ -283,10 +291,10 @@ function recordRequest(requestData) {
283
291
  updateStats(globalStats.byToolType[toolType].channels[channelId], tokens, cost);
284
292
 
285
293
  // 按工具类型 -> 模型统计
286
- if (!globalStats.byToolType[toolType].models[model]) {
287
- globalStats.byToolType[toolType].models[model] = initStatsObject();
294
+ if (!globalStats.byToolType[toolType].models[modelKey]) {
295
+ globalStats.byToolType[toolType].models[modelKey] = initStatsObject();
288
296
  }
289
- updateStats(globalStats.byToolType[toolType].models[model], tokens, cost);
297
+ updateStats(globalStats.byToolType[toolType].models[modelKey], tokens, cost);
290
298
 
291
299
  // 按渠道统计(跨工具)
292
300
  if (!globalStats.byChannel[channelId]) {
@@ -303,13 +311,13 @@ function recordRequest(requestData) {
303
311
  updateStats(globalStats.byChannel[channelId], tokens, cost);
304
312
 
305
313
  // 按模型统计(跨工具)
306
- if (!globalStats.byModel[model]) {
307
- globalStats.byModel[model] = {
314
+ if (!globalStats.byModel[modelKey]) {
315
+ globalStats.byModel[modelKey] = {
308
316
  toolType,
309
317
  ...initStatsObject()
310
318
  };
311
319
  }
312
- updateStats(globalStats.byModel[model], tokens, cost);
320
+ updateStats(globalStats.byModel[modelKey], tokens, cost);
313
321
 
314
322
  saveStatistics(globalStats);
315
323
 
@@ -321,7 +329,7 @@ function recordRequest(requestData) {
321
329
 
322
330
  // 更新每日汇总
323
331
  dailyStats.summary.requests += 1;
324
- dailyStats.summary.tokens += tokens.total || 0;
332
+ dailyStats.summary.tokens += totalTokens;
325
333
  dailyStats.summary.cost += cost || 0;
326
334
 
327
335
  // 按小时统计
@@ -361,6 +369,15 @@ function recordRequest(requestData) {
361
369
  }
362
370
  updateStats(dailyStats.byToolType[toolType].channels[channelId], tokens, cost);
363
371
 
372
+ // 按工具类型 -> 模型
373
+ if (!dailyStats.byToolType[toolType].models) {
374
+ dailyStats.byToolType[toolType].models = {};
375
+ }
376
+ if (!dailyStats.byToolType[toolType].models[modelKey]) {
377
+ dailyStats.byToolType[toolType].models[modelKey] = initStatsObject();
378
+ }
379
+ updateStats(dailyStats.byToolType[toolType].models[modelKey], tokens, cost);
380
+
364
381
  // 按渠道统计
365
382
  if (!dailyStats.byChannel[channelId]) {
366
383
  dailyStats.byChannel[channelId] = {
@@ -372,13 +389,13 @@ function recordRequest(requestData) {
372
389
  updateStats(dailyStats.byChannel[channelId], tokens, cost);
373
390
 
374
391
  // 按模型统计
375
- if (!dailyStats.byModel[model]) {
376
- dailyStats.byModel[model] = {
392
+ if (!dailyStats.byModel[modelKey]) {
393
+ dailyStats.byModel[modelKey] = {
377
394
  toolType,
378
395
  ...initStatsObject()
379
396
  };
380
397
  }
381
- updateStats(dailyStats.byModel[model], tokens, cost);
398
+ updateStats(dailyStats.byModel[modelKey], tokens, cost);
382
399
 
383
400
  saveDailyStats(date, dailyStats);
384
401
 
@@ -447,21 +464,23 @@ function readJsonlForHour(year, month, day, hour, groupBy, filters) {
447
464
  const ts = new Date(entry.timestamp);
448
465
  if (getCSTHour(ts) !== hour) continue;
449
466
 
467
+ const actualModel = resolveActualModel(entry.model, entry);
468
+
450
469
  // Apply filters
451
470
  if (filters) {
452
471
  if (filters.toolType && entry.toolType !== filters.toolType) continue;
453
472
  if (filters.channel && entry.channel !== filters.channel) continue;
454
- if (filters.model && entry.model !== filters.model) continue;
473
+ if (filters.model && actualModel !== filters.model) continue;
455
474
  }
456
475
 
457
476
  let key;
458
- if (groupBy === 'model') key = entry.model || 'unknown';
477
+ if (groupBy === 'model') key = actualModel || 'unknown';
459
478
  else if (groupBy === 'channel') key = entry.channel || entry.channelId || 'unknown';
460
479
  else if (groupBy === 'toolType') key = entry.toolType || 'claude-code';
461
480
  else continue;
462
481
 
463
482
  if (!result[key]) result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
464
- result[key].tokens.total += entry.tokens?.total || 0;
483
+ result[key].tokens.total += getTokenTotal(entry.tokens);
465
484
  result[key].cost += entry.cost || 0;
466
485
  result[key].requests += 1;
467
486
  }
@@ -493,21 +512,23 @@ function readJsonlForDay(year, month, day, groupBy, filters) {
493
512
  let entry;
494
513
  try { entry = JSON.parse(line); } catch { continue; }
495
514
 
515
+ const actualModel = resolveActualModel(entry.model, entry);
516
+
496
517
  // Apply filters
497
518
  if (filters) {
498
519
  if (filters.toolType && entry.toolType !== filters.toolType) continue;
499
520
  if (filters.channel && entry.channel !== filters.channel) continue;
500
- if (filters.model && entry.model !== filters.model) continue;
521
+ if (filters.model && actualModel !== filters.model) continue;
501
522
  }
502
523
 
503
524
  let key;
504
- if (groupBy === 'model') key = entry.model || 'unknown';
525
+ if (groupBy === 'model') key = actualModel || 'unknown';
505
526
  else if (groupBy === 'channel') key = entry.channel || entry.channelId || 'unknown';
506
527
  else if (groupBy === 'toolType') key = entry.toolType || 'claude-code';
507
528
  else continue;
508
529
 
509
530
  if (!result[key]) result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
510
- result[key].tokens.total += entry.tokens?.total || 0;
531
+ result[key].tokens.total += getTokenTotal(entry.tokens);
511
532
  result[key].cost += entry.cost || 0;
512
533
  result[key].requests += 1;
513
534
  }
@@ -541,7 +562,7 @@ function filterProxyLogsForHour(logs, dateStr, hour, groupBy) {
541
562
  const result = {};
542
563
 
543
564
  for (const entry of logs) {
544
- if (!entry || entry.type === 'action') continue;
565
+ if (!entry || entry.type === 'action' || entry.status === 'error') continue;
545
566
 
546
567
  const ts = new Date(entry.timestamp || Date.now());
547
568
  if (Number.isNaN(ts.getTime())) continue;
@@ -549,11 +570,22 @@ function filterProxyLogsForHour(logs, dateStr, hour, groupBy) {
549
570
  const entryDate = getCSTDateStr(ts);
550
571
  if (entryDate !== dateStr || getCSTHour(ts) !== hour) continue;
551
572
 
573
+ const actualModel = resolveActualModel(entry.model, entry);
574
+ const normalizedTokens = normalizeUsageTokens(entry.source || mapSourceToToolType(entry.source), {
575
+ input: entry.inputTokens ?? entry.tokens?.input,
576
+ output: entry.outputTokens ?? entry.tokens?.output,
577
+ cacheCreation: entry.cacheCreation ?? entry.tokens?.cacheCreation,
578
+ cacheRead: entry.cacheRead ?? entry.tokens?.cacheRead,
579
+ cached: entry.cachedTokens ?? entry.tokens?.cached,
580
+ reasoning: entry.reasoningTokens ?? entry.tokens?.reasoning,
581
+ total: entry.totalTokens ?? entry.tokens?.total
582
+ });
583
+
552
584
  let key;
553
585
  if (groupBy === 'toolType') {
554
586
  key = mapSourceToToolType(entry.source);
555
587
  } else if (groupBy === 'model') {
556
- key = entry.model || 'unknown';
588
+ key = actualModel || 'unknown';
557
589
  } else if (groupBy === 'channel') {
558
590
  key = entry.channel || 'unknown';
559
591
  } else {
@@ -564,11 +596,7 @@ function filterProxyLogsForHour(logs, dateStr, hour, groupBy) {
564
596
  result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
565
597
  }
566
598
 
567
- const totalTokens = entry.totalTokens ||
568
- entry.tokens?.total ||
569
- (entry.inputTokens || 0) + (entry.outputTokens || 0) + (entry.reasoningTokens || 0);
570
-
571
- result[key].tokens.total += totalTokens || 0;
599
+ result[key].tokens.total += normalizedTokens.total || 0;
572
600
  result[key].cost += entry.cost || 0;
573
601
  result[key].requests += 1;
574
602
  }
@@ -741,7 +769,7 @@ function aggregateDailyStatistics(dateStr) {
741
769
  }
742
770
 
743
771
  for (const [modelName, modelStats] of Object.entries(toolStats.models || {})) {
744
- const key = getScopedKey(aggregated.byModel, modelName, toolType);
772
+ const key = modelName || 'unknown';
745
773
  if (!aggregated.byModel[key]) {
746
774
  aggregated.byModel[key] = createEmptyEntry(toolType);
747
775
  }
@@ -835,6 +863,7 @@ function getAvailableFilters(startDate, endDate) {
835
863
  const toolTypes = new Set();
836
864
  const channels = new Set();
837
865
  const models = new Set();
866
+ const includeProxyLogs = startDate <= getCSTDateStr(Date.now()) && getCSTDateStr(Date.now()) <= endDate;
838
867
 
839
868
  const start = new Date(startDate + 'T00:00:00');
840
869
  const end = new Date(endDate + 'T00:00:00');
@@ -852,15 +881,31 @@ function getAvailableFilters(startDate, endDate) {
852
881
  if (!line.trim()) continue;
853
882
  let entry;
854
883
  try { entry = JSON.parse(line); } catch { continue; }
884
+ const actualModel = resolveActualModel(entry.model, entry);
855
885
  if (entry.toolType) toolTypes.add(entry.toolType);
856
886
  if (entry.channel) channels.add(entry.channel);
857
- if (entry.model) models.add(entry.model);
887
+ if (actualModel) models.add(actualModel);
858
888
  }
859
889
  } catch (err) {
860
890
  console.error('Failed to scan JSONL for filters:', err);
861
891
  }
862
892
  }
863
893
 
894
+ if (includeProxyLogs) {
895
+ for (const entry of loadProxyLogs()) {
896
+ if (!entry || entry.type === 'action' || entry.status === 'error') continue;
897
+ const ts = new Date(entry.timestamp || Date.now());
898
+ if (Number.isNaN(ts.getTime())) continue;
899
+ const entryDate = getCSTDateStr(ts);
900
+ if (entryDate < startDate || entryDate > endDate) continue;
901
+
902
+ toolTypes.add(mapSourceToToolType(entry.source));
903
+ if (entry.channel) channels.add(entry.channel);
904
+ const actualModel = resolveActualModel(entry.model, entry);
905
+ if (actualModel) models.add(actualModel);
906
+ }
907
+ }
908
+
864
909
  return {
865
910
  toolTypes: Array.from(toolTypes).sort(),
866
911
  channels: Array.from(channels).sort(),
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const DEFAULT_PROJECT_ROOT = path.resolve(__dirname, '../../..');
8
+ const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', 'dist']);
9
+ const WATCHED_WEB_FILES = ['index.html', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'vite.config.js'];
10
+ const WATCHED_WEB_DIRS = ['src', 'public'];
11
+
12
+ function pathExists(targetPath) {
13
+ return fs.existsSync(targetPath);
14
+ }
15
+
16
+ function getLatestMtimeMs(targetPath) {
17
+ if (!pathExists(targetPath)) {
18
+ return 0;
19
+ }
20
+
21
+ const stat = fs.statSync(targetPath);
22
+ if (!stat.isDirectory()) {
23
+ return stat.mtimeMs;
24
+ }
25
+
26
+ let latest = stat.mtimeMs;
27
+ for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
28
+ if (entry.isDirectory() && IGNORED_DIR_NAMES.has(entry.name)) {
29
+ continue;
30
+ }
31
+ const childPath = path.join(targetPath, entry.name);
32
+ latest = Math.max(latest, getLatestMtimeMs(childPath));
33
+ }
34
+ return latest;
35
+ }
36
+
37
+ function getWatchedWebSourceMtimeMs(sourceRoot) {
38
+ let latest = 0;
39
+ for (const fileName of WATCHED_WEB_FILES) {
40
+ latest = Math.max(latest, getLatestMtimeMs(path.join(sourceRoot, fileName)));
41
+ }
42
+ for (const dirName of WATCHED_WEB_DIRS) {
43
+ latest = Math.max(latest, getLatestMtimeMs(path.join(sourceRoot, dirName)));
44
+ }
45
+ return latest;
46
+ }
47
+
48
+ function inspectWebBuildState(options = {}) {
49
+ const projectRoot = options.projectRoot || DEFAULT_PROJECT_ROOT;
50
+ const sourceRoot = options.sourceRoot || path.join(projectRoot, 'src/web');
51
+ const distRoot = options.distRoot || path.join(projectRoot, 'dist/web');
52
+ const distIndexPath = path.join(distRoot, 'index.html');
53
+ const sourceAvailable = pathExists(sourceRoot) && pathExists(path.join(sourceRoot, 'package.json'));
54
+ const distExists = pathExists(distIndexPath);
55
+
56
+ if (!sourceAvailable) {
57
+ return {
58
+ projectRoot,
59
+ sourceRoot,
60
+ distRoot,
61
+ distIndexPath,
62
+ sourceAvailable: false,
63
+ distExists,
64
+ sourceMtimeMs: 0,
65
+ distMtimeMs: getLatestMtimeMs(distRoot),
66
+ needsBuild: false,
67
+ reason: 'source-unavailable'
68
+ };
69
+ }
70
+
71
+ const sourceMtimeMs = getWatchedWebSourceMtimeMs(sourceRoot);
72
+ const distMtimeMs = getLatestMtimeMs(distRoot);
73
+ const hasDistAssets = distExists && distMtimeMs > 0;
74
+ const isStale = hasDistAssets && sourceMtimeMs > distMtimeMs + 1;
75
+
76
+ return {
77
+ projectRoot,
78
+ sourceRoot,
79
+ distRoot,
80
+ distIndexPath,
81
+ sourceAvailable: true,
82
+ distExists,
83
+ sourceMtimeMs,
84
+ distMtimeMs,
85
+ needsBuild: !hasDistAssets || isStale,
86
+ reason: !hasDistAssets ? 'dist-missing' : (isStale ? 'dist-stale' : 'up-to-date')
87
+ };
88
+ }
89
+
90
+ function getNpmCommand() {
91
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
92
+ }
93
+
94
+ function runWebBuild(options = {}) {
95
+ const sourceRoot = options.sourceRoot || path.join(DEFAULT_PROJECT_ROOT, 'src/web');
96
+ const npmCommand = options.npmCommand || getNpmCommand();
97
+ const env = options.env || process.env;
98
+ const stdio = options.stdio || 'inherit';
99
+
100
+ return new Promise((resolve, reject) => {
101
+ const child = spawn(npmCommand, ['run', 'build'], {
102
+ cwd: sourceRoot,
103
+ env,
104
+ stdio
105
+ });
106
+
107
+ child.once('error', (error) => {
108
+ reject(error);
109
+ });
110
+
111
+ child.once('close', (code) => {
112
+ if (code === 0) {
113
+ resolve();
114
+ return;
115
+ }
116
+ reject(new Error(`Web UI build exited with code ${code}`));
117
+ });
118
+ });
119
+ }
120
+
121
+ async function ensureWebDistReady(options = {}) {
122
+ const initialState = options.state || inspectWebBuildState(options);
123
+ if (!initialState.needsBuild) {
124
+ return {
125
+ ...initialState,
126
+ built: false,
127
+ skipped: true,
128
+ previousReason: initialState.reason
129
+ };
130
+ }
131
+
132
+ const runBuild = options.runBuild || runWebBuild;
133
+
134
+ try {
135
+ await runBuild({
136
+ sourceRoot: initialState.sourceRoot,
137
+ distRoot: initialState.distRoot,
138
+ npmCommand: options.npmCommand || getNpmCommand(),
139
+ env: options.env,
140
+ stdio: options.stdio
141
+ });
142
+ } catch (error) {
143
+ const dependencyHint = pathExists(path.join(initialState.sourceRoot, 'node_modules'))
144
+ ? ''
145
+ : '(未检测到 src/web/node_modules,前端依赖可能尚未安装)';
146
+ throw new Error(`Web UI 静态资源构建失败: ${error.message}${dependencyHint}`);
147
+ }
148
+
149
+ const finalState = inspectWebBuildState({
150
+ projectRoot: initialState.projectRoot,
151
+ sourceRoot: initialState.sourceRoot,
152
+ distRoot: initialState.distRoot
153
+ });
154
+
155
+ return {
156
+ ...finalState,
157
+ built: true,
158
+ skipped: false,
159
+ previousReason: initialState.reason
160
+ };
161
+ }
162
+
163
+ module.exports = {
164
+ inspectWebBuildState,
165
+ ensureWebDistReady,
166
+ _test: {
167
+ getLatestMtimeMs,
168
+ getWatchedWebSourceMtimeMs,
169
+ getNpmCommand,
170
+ runWebBuild
171
+ }
172
+ };
@@ -504,11 +504,28 @@ function broadcastSchedulerState(source, schedulerState) {
504
504
  }
505
505
  }
506
506
 
507
+ function broadcastBrowserNotification(notification = {}) {
508
+ if (wss && wsClients.size > 0) {
509
+ const message = JSON.stringify({
510
+ type: 'browser-notification',
511
+ ...notification,
512
+ timestamp: notification.timestamp || Date.now()
513
+ });
514
+
515
+ wsClients.forEach(client => {
516
+ if (client.readyState === WebSocket.OPEN) {
517
+ client.send(message);
518
+ }
519
+ });
520
+ }
521
+ }
522
+
507
523
  module.exports = {
508
524
  startWebSocketServer,
509
525
  stopWebSocketServer,
510
526
  broadcastLog,
511
527
  clearAllLogs,
512
528
  broadcastProxyState,
513
- broadcastSchedulerState
529
+ broadcastSchedulerState,
530
+ broadcastBrowserNotification
514
531
  };
@@ -1 +0,0 @@
1
- .channel-column[data-v-0bad694b]{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--gradient-card);border:1px solid var(--border-primary);border-radius:8px;overflow:hidden;box-shadow:var(--shadow-sm)}.channel-header[data-v-0bad694b]{display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--bg-primary);position:relative}.drag-handle[data-v-0bad694b]{cursor:grab;color:var(--text-tertiary);display:flex;align-items:center;padding:4px;margin:-4px 0 -4px -4px;border-radius:4px;transition:all .2s}.drag-handle[data-v-0bad694b]:hover{color:var(--text-secondary);background:var(--bg-tertiary)}.drag-handle[data-v-0bad694b]:active{cursor:grabbing}.channel-header[data-v-0bad694b]:after{content:"";position:absolute;bottom:0;left:14px;right:14px;height:2px;border-radius:1px}.channel-header.claude[data-v-0bad694b]{background:linear-gradient(135deg,rgba(24,160,88,.08) 0%,transparent 100%)}.channel-header.claude[data-v-0bad694b]:after{background:linear-gradient(90deg,#18a058,#18a0584d)}.channel-header.codex[data-v-0bad694b]{background:linear-gradient(135deg,rgba(59,130,246,.08) 0%,transparent 100%)}.channel-header.codex[data-v-0bad694b]:after{background:linear-gradient(90deg,#3b82f6,#3b82f64d)}.channel-header.gemini[data-v-0bad694b]{background:linear-gradient(135deg,rgba(168,85,247,.08) 0%,transparent 100%)}.channel-header.gemini[data-v-0bad694b]:after{background:linear-gradient(90deg,#a855f7,#a855f74d)}.channel-header.opencode[data-v-0bad694b]{background:linear-gradient(135deg,rgba(234,88,12,.08) 0%,transparent 100%)}.channel-header.opencode[data-v-0bad694b]:after{background:linear-gradient(90deg,#ea580c,#ea580c4d)}.header-icon[data-v-0bad694b]{width:32px;height:32px;border-radius:6px;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px #0000001a}.channel-header.claude .header-icon[data-v-0bad694b]{background:linear-gradient(135deg,#18a058,#15803d);color:#fff}.channel-header.codex .header-icon[data-v-0bad694b]{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff}.channel-header.gemini .header-icon[data-v-0bad694b]{background:linear-gradient(135deg,#a855f7,#9333ea);color:#fff}.channel-header.opencode .header-icon[data-v-0bad694b]{background:linear-gradient(135deg,#ea580c,#c2410c);color:#fff}.channel-title[data-v-0bad694b]{font-size:15px;font-weight:700;margin:0;color:var(--text-primary);letter-spacing:.3px}.channel-content[data-v-0bad694b]{flex:1;min-height:0;overflow:hidden;padding:10px;display:flex;flex-direction:column;gap:8px}.card[data-v-0bad694b]{background:var(--bg-primary);border:1px solid var(--border-primary);border-radius:8px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 12px #0000000d;position:relative}.card[data-v-0bad694b]:before{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.1),transparent);opacity:0;transition:opacity .3s ease}.card[data-v-0bad694b]:hover{box-shadow:0 6px 20px #0000001a;border-color:var(--border-secondary)}.card[data-v-0bad694b]:hover:before{opacity:1}.card.clickable[data-v-0bad694b]{cursor:pointer}.card.clickable[data-v-0bad694b]:hover{border-color:#18a058;transform:translateY(-2px);box-shadow:0 8px 24px #18a0582e}.card.clickable[data-v-0bad694b]:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(135deg,rgba(24,160,88,.02) 0%,transparent 100%);opacity:0;transition:opacity .3s ease;pointer-events:none}.card.clickable[data-v-0bad694b]:hover:after{opacity:1}.card-header[data-v-0bad694b]{display:flex;align-items:center;gap:8px;padding:10px 12px;background:linear-gradient(180deg,var(--bg-secondary) 0%,var(--bg-primary) 100%);border-bottom:1px solid var(--border-primary);position:relative;min-height:24px}.card-header.compact[data-v-0bad694b]{padding:8px 12px}.card-header .n-icon[data-v-0bad694b]{color:var(--text-tertiary);transition:color .2s ease}.card:hover .card-header .n-icon[data-v-0bad694b]{color:var(--text-secondary)}.card-title[data-v-0bad694b]{font-size:11px;font-weight:700;margin:0;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.8px}.runtime-badge[data-v-0bad694b]{display:inline-flex;align-items:center;padding:3px 8px;margin-left:8px;font-size:10px;font-weight:600;color:#10b981;background:linear-gradient(135deg,#10b9811a,#34d3991a);border:1px solid rgba(16,185,129,.2);border-radius:4px;white-space:nowrap;animation:pulse-runtime-0bad694b 2s ease-in-out infinite}@keyframes pulse-runtime-0bad694b{0%,to{opacity:1;transform:scale(1)}50%{opacity:.8;transform:scale(.98)}}.card-body[data-v-0bad694b]{padding:12px;background:var(--bg-primary)}.card-body.compact[data-v-0bad694b]{padding:10px 12px}.proxy-control[data-v-0bad694b]{display:flex;justify-content:space-between;align-items:center;gap:16px}.proxy-info[data-v-0bad694b]{display:flex;flex-direction:column;gap:4px}.proxy-info-row[data-v-0bad694b]{display:flex;align-items:center;justify-content:space-between}.proxy-status[data-v-0bad694b]{display:flex;align-items:center;gap:6px}.proxy-port[data-v-0bad694b]{font-size:11px;color:var(--text-secondary);margin-left:6px}.channel-selector[data-v-0bad694b]{display:flex;align-items:center;padding:4px 8px;background:linear-gradient(135deg,var(--bg-secondary) 0%,var(--bg-tertiary) 100%);border:1px solid var(--border-primary);border-radius:4px;color:var(--text-secondary);transition:all .2s ease}.channel-selector[data-v-0bad694b]:hover{background:var(--hover-bg);border-color:var(--border-secondary);color:var(--text-primary)}.channel-selector[data-v-0bad694b]:disabled{opacity:.5;cursor:not-allowed}.channel-name[data-v-0bad694b]{font-size:11px;font-weight:500;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.status-dot[data-v-0bad694b]{width:8px;height:8px;border-radius:50%;background:var(--border-secondary);transition:all .3s ease}.status-dot.active[data-v-0bad694b]{background:#18a058;box-shadow:0 0 8px #18a05880}.quick-stats[data-v-0bad694b]{display:flex;gap:16px;padding:4px 0}.stat-item[data-v-0bad694b]{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 0}.stat-divider[data-v-0bad694b]{width:1px;background:linear-gradient(180deg,transparent 0%,var(--border-primary) 50%,transparent 100%)}.quick-access-list[data-v-0bad694b]{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.access-card[data-v-0bad694b]{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;background:var(--bg-secondary);border:1px solid var(--border-color);transition:all .3s cubic-bezier(.4,0,.2,1);position:relative;min-height:52px;overflow:hidden}.access-card[data-v-0bad694b]:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s ease;pointer-events:none}.access-icon[data-v-0bad694b]{width:32px;height:32px;border-radius:5px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .3s ease}.access-content[data-v-0bad694b]{display:flex;flex-direction:column;gap:4px;flex:1}.access-card-projects .access-icon[data-v-0bad694b]{background:linear-gradient(135deg,#6366f126,#8b5cf626);color:#6366f1}.access-card-projects[data-v-0bad694b]:before{background:linear-gradient(135deg,#6366f114,#8b5cf614)}.access-card-projects:hover .access-icon[data-v-0bad694b]{background:linear-gradient(135deg,#6366f140,#8b5cf640);transform:scale(1.1) rotate(-5deg)}.access-card-sessions .access-icon[data-v-0bad694b]{background:linear-gradient(135deg,#10b98126,#05966926);color:#10b981}.access-card-sessions[data-v-0bad694b]:before{background:linear-gradient(135deg,#10b98114,#05966914)}.access-card-sessions:hover .access-icon[data-v-0bad694b]{background:linear-gradient(135deg,#10b98140,#05966940);transform:scale(1.1) rotate(5deg)}.access-card-goto .access-icon[data-v-0bad694b]{background:linear-gradient(135deg,#f59e0b26,#fb923c26);color:#f59e0b}.access-card-goto[data-v-0bad694b]:before{background:linear-gradient(135deg,#f59e0b14,#fb923c14)}.access-card-goto:hover .access-icon[data-v-0bad694b]{background:linear-gradient(135deg,#f59e0b40,#fb923c40);transform:scale(1.1) translate(3px)}.access-card.clickable[data-v-0bad694b]:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0000001a;border-color:var(--border-secondary)}.access-card.clickable[data-v-0bad694b]:hover:before{opacity:1}.access-card.clickable[data-v-0bad694b]:active{transform:translateY(0)}.access-label[data-v-0bad694b]{display:block;font-size:10px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.3px}.access-value[data-v-0bad694b]{display:block;font-size:18px;font-weight:800;color:var(--text-primary);line-height:1}.access-goto[data-v-0bad694b]{display:block;font-size:12px;font-weight:700;color:var(--text-primary);line-height:1.2;white-space:nowrap}.stats-inline[data-v-0bad694b]{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.stats-inline.stats-3col[data-v-0bad694b]{grid-template-columns:repeat(3,1fr)}.stat-inline-item[data-v-0bad694b]{display:flex;align-items:center;gap:8px;padding:10px;background:var(--bg-secondary);border-radius:6px;border:1px solid var(--border-primary);transition:all .2s ease}.stat-inline-item[data-v-0bad694b]:hover{border-color:var(--border-secondary);background:var(--hover-bg)}.stat-icon-dot[data-v-0bad694b]{width:8px;height:8px;border-radius:50%;flex-shrink:0}.stat-icon-dot.requests[data-v-0bad694b]{background:#3b82f6;box-shadow:0 0 6px #3b82f680}.stat-icon-dot.tokens[data-v-0bad694b]{background:#18a058;box-shadow:0 0 6px #18a05880}.stat-icon-dot.cost[data-v-0bad694b]{background:#f59e0b;box-shadow:0 0 6px #f59e0b80}.stat-info[data-v-0bad694b]{display:flex;flex-direction:column;gap:1px;min-width:0}.stat-label[data-v-0bad694b]{font-size:10px;color:var(--text-tertiary);font-weight:500;text-transform:uppercase;letter-spacing:.2px}.stat-value[data-v-0bad694b]{font-size:16px;font-weight:700;color:var(--text-primary);line-height:1.2;transition:all .3s ease}.stat-value.animating[data-v-0bad694b]{animation:numberChange-0bad694b .6s ease}@keyframes numberChange-0bad694b{0%{transform:translateY(-6px);opacity:.5}50%{transform:translateY(0);opacity:1;color:var(--primary-color)}to{transform:translateY(0);opacity:1}}.stats-card[data-v-0bad694b],.chart-card[data-v-0bad694b]{border-left:2px solid transparent}.stats-card-claude[data-v-0bad694b],.chart-card[data-v-0bad694b]:has(+.stats-card-claude),.card[data-v-0bad694b]:has(.panel-card):nth-child(4){border-left-color:#18a058}.chart-card.chart-card-claude[data-v-0bad694b]{border-left-color:#18a058}.stats-card-codex[data-v-0bad694b],.chart-card[data-v-0bad694b]:has(+.stats-card-codex),.card[data-v-0bad694b]:has(.panel-card):nth-child(4){border-left-color:#3b82f6}.chart-card.chart-card-codex[data-v-0bad694b]{border-left-color:#3b82f6}.stats-card-gemini[data-v-0bad694b],.chart-card[data-v-0bad694b]:has(+.stats-card-gemini),.card[data-v-0bad694b]:has(.panel-card):nth-child(4){border-left-color:#a855f7}.chart-card.chart-card-gemini[data-v-0bad694b]{border-left-color:#a855f7}.stats-card-opencode[data-v-0bad694b],.chart-card[data-v-0bad694b]:has(+.stats-card-opencode),.card[data-v-0bad694b]:has(.panel-card):nth-child(4){border-left-color:#ea580c}.chart-card.chart-card-opencode[data-v-0bad694b]{border-left-color:#ea580c}.chart-card[data-v-0bad694b]{padding:0;overflow:hidden}.chart-card[data-v-0bad694b] .panel-card{border:none;border-radius:0;box-shadow:none;background:transparent}.stat-requests .stat-value[data-v-0bad694b]{color:#3b82f6}.stat-input .stat-value[data-v-0bad694b]{color:#18a058}.stat-output .stat-value[data-v-0bad694b]{color:#f59e0b}.token-label[data-v-0bad694b]{display:flex;align-items:center;gap:8px}.token-dot[data-v-0bad694b]{width:8px;height:8px;border-radius:50%}.logs-card[data-v-0bad694b]{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border-primary);border-radius:6px}.logs-card-body[data-v-0bad694b]{flex:1;min-height:0;display:flex;flex-direction:column;padding:0}.logs-card .card-header[data-v-0bad694b]{background:linear-gradient(135deg,var(--bg-secondary) 0%,var(--bg-tertiary) 100%)}.logs-table-wrapper[data-v-0bad694b]{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-primary)}.logs-table-header[data-v-0bad694b]{display:flex;padding:10px 12px;background:linear-gradient(180deg,var(--bg-tertiary) 0%,var(--bg-secondary) 100%);border-bottom:2px solid var(--border-primary);font-size:11px;font-weight:700;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.6px;flex-shrink:0}.logs-header-claude .log-col[data-v-0bad694b]{color:#18a058b3;font-weight:600}.logs-header-codex .log-col[data-v-0bad694b]{color:#3b82f6b3;font-weight:600}.logs-header-gemini .log-col[data-v-0bad694b]{color:#a855f7b3;font-weight:600}.logs-header-opencode .log-col[data-v-0bad694b]{color:#ea580cb3;font-weight:600}[data-theme=dark] .logs-header-claude .log-col[data-v-0bad694b]{color:#34d399a6}[data-theme=dark] .logs-header-codex .log-col[data-v-0bad694b]{color:#60a5faa6}[data-theme=dark] .logs-header-gemini .log-col[data-v-0bad694b]{color:#c084fca6}[data-theme=dark] .logs-header-opencode .log-col[data-v-0bad694b]{color:#fb923ca6}.logs-container[data-v-0bad694b]{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden}.logs-container[data-v-0bad694b]::-webkit-scrollbar{width:4px}.logs-container[data-v-0bad694b]::-webkit-scrollbar-thumb{background:#18a0584d;border-radius:2px}.logs-container[data-v-0bad694b]::-webkit-scrollbar-thumb:hover{background:#18a05880}.empty-logs[data-v-0bad694b]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:var(--text-tertiary);min-height:200px;text-align:center}.log-row[data-v-0bad694b]{display:flex;align-items:center;padding:10px 12px;min-height:40px;border-bottom:1px solid var(--border-primary);font-size:12px;transition:all .2s ease;background:var(--bg-primary);position:relative}.log-row[data-v-0bad694b]:nth-child(2n){background:var(--bg-secondary)}.log-row[data-v-0bad694b]:hover{background:var(--hover-bg);transform:translate(2px)}.log-row.new-log[data-v-0bad694b]{animation:newLogPulse-0bad694b 4.5s cubic-bezier(.25,.46,.45,.94) forwards;border-left:3px solid #18a058}@keyframes newLogPulse-0bad694b{0%{background:linear-gradient(90deg,#18a05826,#18a0580f);box-shadow:0 0 8px #18a05833}5%{background:linear-gradient(90deg,#18a0582e,#18a05814);box-shadow:0 0 12px 1px #18a05840}15%{background:linear-gradient(90deg,#18a05829,#18a05812);box-shadow:0 0 10px 1px #18a05833}30%{background:linear-gradient(90deg,#18a0581f,#18a0580d);box-shadow:0 0 6px #18a05826}50%{background:linear-gradient(90deg,#18a05814,#18a05808);box-shadow:0 0 4px #18a0581a}70%{background:linear-gradient(90deg,#18a0580a,#18a05804);box-shadow:0 0 2px #18a0580f}85%{background:linear-gradient(90deg,#18a05805,#18a05802);box-shadow:0 0 1px #18a05808}to{background:transparent;box-shadow:0 0 #18a05800;border-left-color:transparent}}[data-theme=dark] .log-row.new-log[data-v-0bad694b]{animation:newLogPulseDark-0bad694b 4.5s cubic-bezier(.25,.46,.45,.94) forwards}@keyframes newLogPulseDark-0bad694b{0%{background:linear-gradient(90deg,#18a05833,#18a05814);box-shadow:0 0 10px #18a05840}5%{background:linear-gradient(90deg,#18a0583d,#18a0581a);box-shadow:0 0 14px 2px #18a0584d}15%{background:linear-gradient(90deg,#18a05836,#18a05817);box-shadow:0 0 12px 1px #18a05840}30%{background:linear-gradient(90deg,#18a05829,#18a05812);box-shadow:0 0 8px 1px #18a0582e}50%{background:linear-gradient(90deg,#18a0581c,#18a0580b);box-shadow:0 0 5px #18a0581f}70%{background:linear-gradient(90deg,#18a0580f,#18a05806);box-shadow:0 0 3px #18a05814}85%{background:linear-gradient(90deg,#18a05808,#18a05803);box-shadow:0 0 1px #18a0580a}to{background:transparent;box-shadow:0 0 #18a05800;border-left-color:transparent}}.log-row.new-log.action-row[data-v-0bad694b]{border-left:3px solid #18a058}.log-row.action-row[data-v-0bad694b]{background:linear-gradient(90deg,#18a0581f,#18a0580a);border-left:3px solid #18a058;padding-left:8px}.log-row.error-row[data-v-0bad694b]{background:linear-gradient(90deg,#d0305024,#d030500a);border-left:3px solid #d03050;padding-left:8px}.log-row.action-row[data-v-0bad694b]:hover{background:linear-gradient(90deg,#18a0582e,#18a05814)}.log-row.error-row[data-v-0bad694b]:hover{background:linear-gradient(90deg,#d0305033,#d0305014)}.action-content[data-v-0bad694b]{display:flex;align-items:center;gap:8px;width:100%}.action-msg[data-v-0bad694b]{flex:1;font-size:11px;color:#18a058;font-weight:600;letter-spacing:.2px}.action-msg.error-msg[data-v-0bad694b]{color:#d03050}.action-time[data-v-0bad694b]{font-size:10px;font-family:SF Mono,Monaco,monospace;color:var(--text-tertiary);background:#18a0581a;padding:2px 6px;border-radius:4px}.action-time.error-time[data-v-0bad694b]{background:#d030501f}.log-col[data-v-0bad694b]{display:flex;align-items:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.col-channel[data-v-0bad694b]{min-width:0}.col-channel .n-tag[data-v-0bad694b]{max-width:100%;font-size:11px;font-weight:600;border-radius:4px;padding:3px 10px}.col-token[data-v-0bad694b]{justify-content:center;font-family:SF Mono,Monaco,Consolas,monospace;font-size:12px;font-weight:600;color:var(--text-secondary);background:#00000008;padding:4px 8px;border-radius:4px;margin:0 3px}[data-theme=dark] .col-token[data-v-0bad694b]{background:#ffffff0f}.col-time[data-v-0bad694b]{justify-content:flex-end;font-family:SF Mono,Monaco,Consolas,monospace;font-size:11px;font-weight:500;color:var(--text-tertiary);padding-right:2px;opacity:.85}.log-row:hover .col-time[data-v-0bad694b]{opacity:1;color:var(--text-secondary)}.col-channel-claude[data-v-0bad694b]{flex:2 1 60px;min-width:50px}.col-token-claude[data-v-0bad694b]{flex:1 1 40px;min-width:35px}.col-time-claude[data-v-0bad694b]{flex:1.5 1 55px;min-width:50px}.col-channel-codex[data-v-0bad694b]{flex:2 1 60px;min-width:50px}.col-token-codex[data-v-0bad694b]{flex:1 1 40px;min-width:35px}.col-time-codex[data-v-0bad694b]{flex:1.5 1 55px;min-width:50px}.col-channel-gemini[data-v-0bad694b]{flex:2.5 1 70px;min-width:55px}.col-token-gemini[data-v-0bad694b]{flex:1.2 1 45px;min-width:40px}.col-time-gemini[data-v-0bad694b]{flex:1.8 1 60px;min-width:55px}.col-channel-opencode[data-v-0bad694b]{flex:2.5 1 70px;min-width:55px}.col-token-opencode[data-v-0bad694b]{flex:1.2 1 45px;min-width:40px}.col-time-opencode[data-v-0bad694b]{flex:1.8 1 60px;min-width:55px}.claude-extra-area[data-v-0bad694b]{display:flex;align-items:center;gap:6px;margin-left:auto}.mcp-count-tag[data-v-0bad694b],.skills-count-tag[data-v-0bad694b]{font-size:10px;font-weight:500;padding:0 8px;height:22px;line-height:22px;border-radius:4px;cursor:pointer;transition:all .2s ease}.mcp-count-tag.clickable[data-v-0bad694b]:hover,.skills-count-tag.clickable[data-v-0bad694b]:hover{transform:scale(1.03);filter:brightness(1.1)}.mcp-quick-panel[data-v-0bad694b],.skills-quick-panel[data-v-0bad694b]{padding:2px 0}.mcp-quick-panel .panel-title[data-v-0bad694b],.skills-quick-panel .panel-title[data-v-0bad694b]{display:flex;justify-content:space-between;align-items:center;padding:6px 10px 8px;border-bottom:1px solid var(--border-primary);margin-bottom:4px}.mcp-quick-panel .panel-title span[data-v-0bad694b]:first-child,.skills-quick-panel .panel-title span[data-v-0bad694b]:first-child{font-size:13px;font-weight:600;color:var(--text-primary)}.no-items[data-v-0bad694b]{padding:16px 10px;text-align:center}.mcp-quick-list[data-v-0bad694b],.skills-quick-list[data-v-0bad694b]{max-height:300px;overflow-y:auto;padding:0 4px 4px}.mcp-quick-item[data-v-0bad694b],.skill-quick-item[data-v-0bad694b]{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:5px;margin-bottom:4px;background:var(--bg-secondary);transition:all .2s ease}.mcp-quick-item[data-v-0bad694b]:hover,.skill-quick-item[data-v-0bad694b]:hover{background:var(--hover-bg)}.mcp-item-icon[data-v-0bad694b],.skill-item-icon[data-v-0bad694b]{width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.mcp-item-icon[data-v-0bad694b]{background:linear-gradient(135deg,#3b82f626,#3b82f60d);color:#3b82f6}.skill-item-icon[data-v-0bad694b]{background:linear-gradient(135deg,#18a05826,#18a0580d);color:#18a058}.mcp-item-info[data-v-0bad694b],.skill-item-info[data-v-0bad694b]{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}.mcp-item-name[data-v-0bad694b],.skill-item-name[data-v-0bad694b]{font-size:12px;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mcp-item-type[data-v-0bad694b]{font-size:10px;color:var(--text-tertiary);text-transform:uppercase}.skill-item-desc[data-v-0bad694b]{font-size:10px;color:var(--text-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mcp-item-status[data-v-0bad694b]{flex-shrink:0}.mcp-item-status .status-dot[data-v-0bad694b]{width:8px;height:8px;border-radius:50%;background:var(--border-secondary)}.mcp-item-status.online .status-dot[data-v-0bad694b]{background:#18a058;box-shadow:0 0 6px #18a05880}.mcp-item-status.error .status-dot[data-v-0bad694b]{background:#d03050;box-shadow:0 0 6px #d0305080}.mcp-quick-list[data-v-0bad694b]::-webkit-scrollbar,.skills-quick-list[data-v-0bad694b]::-webkit-scrollbar{width:4px}.mcp-quick-list[data-v-0bad694b]::-webkit-scrollbar-thumb,.skills-quick-list[data-v-0bad694b]::-webkit-scrollbar-thumb{background:#00000026;border-radius:2px}[data-theme=dark] .mcp-quick-list[data-v-0bad694b]::-webkit-scrollbar-thumb,[data-theme=dark] .skills-quick-list[data-v-0bad694b]::-webkit-scrollbar-thumb{background:#ffffff26}.skills-button[data-v-0bad694b]{padding:6px!important;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:5px;background:var(--bg-secondary);border:1px solid var(--border-primary);transition:all .25s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 4px #0000000d}.skills-button .n-icon[data-v-0bad694b]{color:var(--text-color-3);transition:all .25s ease}.skills-button[data-v-0bad694b]:hover{background:linear-gradient(135deg,#18a0581a,#18a0580d);border-color:#18a0584d;box-shadow:0 4px 12px #18a05826;transform:translateY(-1px)}.skills-button:hover .n-icon[data-v-0bad694b]{color:#18a058;transform:scale(1.1)}.skills-button[data-v-0bad694b]:active{transform:translateY(0);box-shadow:0 2px 4px #0000000d}.lock-button[data-v-0bad694b]{margin-left:6px;padding:6px!important;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:5px;background:var(--bg-secondary);border:1px solid var(--border-primary);transition:all .25s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 4px #0000000d}.lock-button .n-icon[data-v-0bad694b]{color:var(--text-color-3);transition:all .25s ease}.lock-button[data-v-0bad694b]:hover{background:linear-gradient(135deg,#6b72801a,#6b72800d);border-color:#6b72804d;box-shadow:0 4px 12px #0000001a;transform:translateY(-1px)}.lock-button:hover .n-icon[data-v-0bad694b]{color:var(--text-color-1);transform:scale(1.1)}.lock-button[data-v-0bad694b]:active{transform:translateY(0);box-shadow:0 2px 4px #0000000d}.locked-overlay[data-v-0bad694b]{flex:1;display:flex;align-items:center;justify-content:center;border-radius:6px;margin:10px;position:relative;overflow:hidden;background:var(--bg-secondary);border:1px solid var(--border-primary);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.locked-overlay[data-v-0bad694b]:before{content:"";position:absolute;width:400px;height:400px;border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%);opacity:.4;filter:blur(60px);pointer-events:none}.locked-claude[data-v-0bad694b]{background:linear-gradient(135deg,rgba(24,160,88,.03) 0%,var(--bg-secondary) 50%,rgba(24,160,88,.02) 100%)}.locked-claude[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(24,160,88,.15) 0%,transparent 70%)}.locked-codex[data-v-0bad694b]{background:linear-gradient(135deg,rgba(59,130,246,.03) 0%,var(--bg-secondary) 50%,rgba(59,130,246,.02) 100%)}.locked-codex[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(59,130,246,.15) 0%,transparent 70%)}.locked-gemini[data-v-0bad694b]{background:linear-gradient(135deg,rgba(168,85,247,.03) 0%,var(--bg-secondary) 50%,rgba(168,85,247,.02) 100%)}.locked-gemini[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(168,85,247,.15) 0%,transparent 70%)}.locked-opencode[data-v-0bad694b]{background:linear-gradient(135deg,rgba(234,88,12,.03) 0%,var(--bg-secondary) 50%,rgba(234,88,12,.02) 100%)}.locked-opencode[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(234,88,12,.15) 0%,transparent 70%)}.locked-content[data-v-0bad694b]{display:flex;flex-direction:column;align-items:center;gap:20px;padding:60px 40px;text-align:center;position:relative;z-index:1}.lock-icon[data-v-0bad694b]{width:96px;height:96px;display:flex;align-items:center;justify-content:center;border-radius:10px;background:var(--bg-primary);border:2px solid var(--border-primary);margin-bottom:8px;position:relative;box-shadow:0 8px 24px #0000001f;animation:lock-float-0bad694b 3s ease-in-out infinite}@keyframes lock-float-0bad694b{0%,to{transform:translateY(0)}50%{transform:translateY(-6px)}}.lock-icon[data-v-0bad694b]:before{content:"";position:absolute;top:-8px;right:-8px;bottom:-8px;left:-8px;border-radius:20px;opacity:.6;filter:blur(12px)}.locked-claude .lock-icon[data-v-0bad694b]{background:linear-gradient(135deg,rgba(24,160,88,.12) 0%,var(--bg-primary) 100%);border-color:#18a05840;box-shadow:0 8px 24px #18a05826,0 0 0 1px #18a0581a inset}.locked-claude .lock-icon[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(24,160,88,.3) 0%,transparent 60%)}.locked-claude .lock-icon .n-icon[data-v-0bad694b]{color:#18a058;filter:drop-shadow(0 2px 4px rgba(24,160,88,.3))}.locked-codex .lock-icon[data-v-0bad694b]{background:linear-gradient(135deg,rgba(59,130,246,.12) 0%,var(--bg-primary) 100%);border-color:#3b82f640;box-shadow:0 8px 24px #3b82f626,0 0 0 1px #3b82f61a inset}.locked-codex .lock-icon[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(59,130,246,.3) 0%,transparent 60%)}.locked-codex .lock-icon .n-icon[data-v-0bad694b]{color:#3b82f6;filter:drop-shadow(0 2px 4px rgba(59,130,246,.3))}.locked-gemini .lock-icon[data-v-0bad694b]{background:linear-gradient(135deg,rgba(168,85,247,.12) 0%,var(--bg-primary) 100%);border-color:#a855f740;box-shadow:0 8px 24px #a855f726,0 0 0 1px #a855f71a inset}.locked-gemini .lock-icon[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(168,85,247,.3) 0%,transparent 60%)}.locked-gemini .lock-icon .n-icon[data-v-0bad694b]{color:#a855f7;filter:drop-shadow(0 2px 4px rgba(168,85,247,.3))}.locked-opencode .lock-icon[data-v-0bad694b]{background:linear-gradient(135deg,rgba(234,88,12,.12) 0%,var(--bg-primary) 100%);border-color:#ea580c40;box-shadow:0 8px 24px #ea580c26,0 0 0 1px #ea580c1a inset}.locked-opencode .lock-icon[data-v-0bad694b]:before{background:radial-gradient(circle,rgba(234,88,12,.3) 0%,transparent 60%)}.locked-opencode .lock-icon .n-icon[data-v-0bad694b]{color:#ea580c;filter:drop-shadow(0 2px 4px rgba(234,88,12,.3))}.lock-icon .n-icon[data-v-0bad694b]{opacity:.85;position:relative;z-index:1}.locked-title[data-v-0bad694b]{font-size:17px;font-weight:600;color:var(--text-color-1);margin:0;letter-spacing:.3px}.locked-hint[data-v-0bad694b]{font-size:13px;color:var(--text-color-3);max-width:220px;line-height:1.7;opacity:.9}.channel-status[data-v-0bad694b]{cursor:pointer;padding:4px 8px!important;border-radius:4px;transition:all .2s ease}.channel-status[data-v-0bad694b]:hover{background:var(--hover-bg)}.channel-quick-panel[data-v-0bad694b]{padding:4px 0}.panel-title[data-v-0bad694b]{display:flex;justify-content:space-between;align-items:center;padding:8px 12px 12px;border-bottom:1px solid var(--border-primary);margin-bottom:8px}.panel-title span[data-v-0bad694b]:first-child{font-size:13px;font-weight:600;color:var(--text-primary)}.no-channels[data-v-0bad694b]{padding:24px 12px;text-align:center}.channel-quick-list[data-v-0bad694b]{max-height:360px;overflow-y:auto}.channel-quick-item[data-v-0bad694b]{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border-radius:6px;margin:0 4px 6px;background:var(--bg-secondary);transition:all .2s ease}.channel-quick-item[data-v-0bad694b]:hover{background:var(--hover-bg)}.channel-quick-item.disabled[data-v-0bad694b]{opacity:.6}.channel-quick-info[data-v-0bad694b]{display:flex;align-items:center;gap:8px;width:100%}.channel-quick-name[data-v-0bad694b]{font-size:13px;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.channel-metrics[data-v-0bad694b]{display:flex;gap:6px;width:100%}.channel-metrics .metric-item[data-v-0bad694b]{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 4px;background:var(--bg-primary);border-radius:4px;border:1px solid var(--border-primary)}.channel-metrics .metric-label[data-v-0bad694b]{font-size:10px;color:var(--text-tertiary);font-weight:500}.channel-metrics .metric-value[data-v-0bad694b]{font-size:12px;font-weight:700;color:var(--text-primary);font-family:SF Mono,Monaco,monospace}.channel-metrics .metric-value.active[data-v-0bad694b]{color:#f59e0b}.channel-switching-tip[data-v-0bad694b]{font-size:11px;color:var(--text-tertiary);padding:0 2px}.channel-quick-item.disabled .channel-metrics .metric-value[data-v-0bad694b]{color:var(--text-tertiary)}.channel-quick-list[data-v-0bad694b]::-webkit-scrollbar{width:4px}.channel-quick-list[data-v-0bad694b]::-webkit-scrollbar-thumb{background:#00000026;border-radius:2px}[data-theme=dark] .channel-quick-list[data-v-0bad694b]::-webkit-scrollbar-thumb{background:#ffffff26}@media (max-width: 1024px){.channel-column[data-v-0bad694b]{border-radius:6px}.card-header[data-v-0bad694b]{padding:8px 10px}.card-title[data-v-0bad694b]{font-size:13px}.stats-inline[data-v-0bad694b]{gap:6px}.stat-inline-item[data-v-0bad694b]{padding:8px}.stat-value[data-v-0bad694b]{font-size:14px}.stat-label[data-v-0bad694b]{font-size:9px}.logs-table-header[data-v-0bad694b]{padding:8px 10px;font-size:10px}.log-row[data-v-0bad694b]{padding:8px 10px;font-size:11px}}@media (max-width: 768px){.channel-column[data-v-0bad694b]{border-radius:6px;min-height:300px;height:auto;max-height:400px}.card-header[data-v-0bad694b]{padding:8px;gap:6px}.card-title[data-v-0bad694b]{font-size:12px}.stats-inline[data-v-0bad694b]{gap:4px}.stat-inline-item[data-v-0bad694b]{padding:6px;gap:6px}.stat-icon-dot[data-v-0bad694b]{width:6px;height:6px}.stat-value[data-v-0bad694b]{font-size:13px}.stat-label[data-v-0bad694b]{font-size:8px}.logs-card[data-v-0bad694b]{min-height:150px}.logs-table-header[data-v-0bad694b]{padding:6px 8px;font-size:9px}.log-row[data-v-0bad694b]{padding:6px 8px;font-size:10px;min-height:32px}.col-token[data-v-0bad694b]{font-size:10px;padding:2px 4px}.col-time[data-v-0bad694b]{font-size:9px}.mcp-count-tag[data-v-0bad694b],.skills-count-tag[data-v-0bad694b]{font-size:9px;padding:0 6px;height:18px;line-height:18px}.skills-button[data-v-0bad694b]{padding:4px 8px;font-size:10px;gap:4px}.lock-button[data-v-0bad694b]{width:26px;height:26px}}@media (max-width: 640px){.channel-column[data-v-0bad694b]{min-height:250px;max-height:350px}.card-header[data-v-0bad694b]{padding:6px;gap:4px;flex-wrap:wrap}.card-title[data-v-0bad694b]{font-size:11px}.stats-card .card-content[data-v-0bad694b]{padding:6px}.stats-inline[data-v-0bad694b]{gap:3px;flex-wrap:wrap}.stat-inline-item[data-v-0bad694b]{padding:5px;gap:4px;min-width:calc(33% - 3px);flex:1 1 auto}.stat-icon-dot[data-v-0bad694b]{width:5px;height:5px}.stat-value[data-v-0bad694b]{font-size:12px}.stat-label[data-v-0bad694b]{font-size:7px}.logs-card[data-v-0bad694b]{min-height:120px}.logs-table-header[data-v-0bad694b]{padding:5px 6px;font-size:8px;letter-spacing:.3px}.log-row[data-v-0bad694b]{padding:5px 6px;font-size:9px;min-height:28px}.col-channel .n-tag[data-v-0bad694b]{font-size:9px;padding:2px 6px}.col-token[data-v-0bad694b]{font-size:9px;padding:2px 3px;margin:0 1px}.col-time[data-v-0bad694b]{font-size:8px}.claude-extra-area[data-v-0bad694b]{gap:4px}.mcp-count-tag[data-v-0bad694b],.skills-count-tag[data-v-0bad694b]{font-size:8px;padding:0 4px;height:16px;line-height:16px}.skills-button[data-v-0bad694b]{padding:3px 6px;font-size:9px;gap:3px}.skills-button .n-icon[data-v-0bad694b]{font-size:12px!important}.lock-button[data-v-0bad694b]{width:24px;height:24px;margin-left:4px}.lock-button .n-icon[data-v-0bad694b]{font-size:12px!important}.locked-content[data-v-0bad694b]{padding:30px 20px;gap:12px}.lock-icon[data-v-0bad694b]{width:64px;height:64px}.lock-icon .n-icon[data-v-0bad694b]{font-size:28px!important}.lock-title[data-v-0bad694b]{font-size:14px}.lock-subtitle[data-v-0bad694b]{font-size:10px}.unlock-btn[data-v-0bad694b]{padding:8px 16px;font-size:11px}}@media (max-width: 480px){.channel-column[data-v-0bad694b]{min-height:220px;max-height:300px}.card-header[data-v-0bad694b]{padding:5px}.card-title[data-v-0bad694b]{font-size:10px}.stats-inline[data-v-0bad694b]{gap:2px}.stat-inline-item[data-v-0bad694b]{padding:4px;gap:3px}.stat-value[data-v-0bad694b]{font-size:11px}.stat-label[data-v-0bad694b]{font-size:6px}.logs-table-header[data-v-0bad694b]{padding:4px 5px;font-size:7px}.log-row[data-v-0bad694b]{padding:4px 5px;font-size:8px;min-height:24px}.col-token[data-v-0bad694b]{font-size:8px}.col-time[data-v-0bad694b]{font-size:7px}}.card[data-v-0bad694b]:not(.logs-card) .n-collapse{background:transparent}.card[data-v-0bad694b]:not(.logs-card) .n-collapse-item{background:transparent}.card[data-v-0bad694b] .n-collapse-item__header{padding:12px 16px;background:transparent;transition:all .3s ease}.card[data-v-0bad694b] .n-collapse-item__header:hover{background:#00000005}.card[data-v-0bad694b] .n-collapse-item__header-main{width:100%}.card[data-v-0bad694b]:not(.logs-card) .n-collapse-item__content-wrapper{padding:0 16px 12px}.card[data-v-0bad694b]:not(.logs-card) .n-collapse-item__content-inner{padding-top:0}.chart-card[data-v-0bad694b]:has(.n-collapse){padding:0}.dashboard-container[data-v-06100f0f]{height:100%;background:var(--bg-primary);overflow:hidden;padding:12px;box-sizing:border-box;display:flex;flex-direction:column}.dashboard-toolbar[data-v-06100f0f]{display:flex;justify-content:flex-end;margin-bottom:8px;flex-shrink:0}.analytics-link[data-v-06100f0f]{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--primary-color, #18a058);text-decoration:none;padding:4px 10px;border-radius:4px;background:#18a05814;transition:background .2s}.analytics-link[data-v-06100f0f]:hover{background:#18a05826}.analytics-link-icon[data-v-06100f0f]{font-size:14px}.dashboard-grid[data-v-06100f0f]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;flex:1;min-height:0;overflow:hidden;box-sizing:border-box}.dashboard-grid[data-v-06100f0f] .sortable-ghost{opacity:.4}.dashboard-grid[data-v-06100f0f] .sortable-chosen{box-shadow:0 8px 24px #00000026}@media (max-width: 1600px){.dashboard-grid[data-v-06100f0f]{grid-template-columns:repeat(3,1fr)}}@media (max-width: 1200px){.dashboard-grid[data-v-06100f0f]{grid-template-columns:repeat(2,1fr)}}@media (max-width: 1024px){.dashboard-container[data-v-06100f0f]{padding:10px}.dashboard-grid[data-v-06100f0f]{gap:10px}}@media (max-width: 900px){.dashboard-grid[data-v-06100f0f]{grid-template-columns:1fr}}@media (max-width: 768px){.dashboard-container[data-v-06100f0f]{padding:8px;overflow-y:auto}.dashboard-grid[data-v-06100f0f]{gap:8px;overflow:visible;min-height:auto}}@media (max-width: 640px){.dashboard-container[data-v-06100f0f]{padding:6px}.dashboard-grid[data-v-06100f0f]{gap:6px}}@media (max-width: 480px){.dashboard-container[data-v-06100f0f]{padding:4px}.dashboard-grid[data-v-06100f0f]{gap:4px}}