token-speed-tester 1.5.0 → 1.7.0

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/dist/index.js CHANGED
@@ -1,11 +1,167 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync } from "fs";
5
- import { dirname, join } from "path";
6
- import { fileURLToPath } from "url";
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { writeFile as fsWriteFile } from "fs/promises";
6
+ import { dirname as dirname2, join } from "path";
7
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
8
  import { Command } from "commander";
8
9
  import chalk from "chalk";
10
+ import open from "open";
11
+
12
+ // src/i18n.ts
13
+ var SUPPORTED_LANGS = ["zh", "en"];
14
+ var DEFAULT_LANG = "zh";
15
+ var zhMessages = {
16
+ defaultPrompt: "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587",
17
+ appTitle: "\u{1F680} Token \u901F\u5EA6\u6D4B\u8BD5\u5DE5\u5177",
18
+ runningTests: "\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...",
19
+ streamingOutput: "\u6A21\u578B\u8F93\u51FA (\u6D41\u5F0F):",
20
+ testComplete: "\u2705 \u6D4B\u8BD5\u5B8C\u6210!",
21
+ errorPrefix: "\u274C \u9519\u8BEF",
22
+ unknownError: "\u274C \u53D1\u751F\u672A\u77E5\u9519\u8BEF",
23
+ configLabels: {
24
+ provider: "Provider",
25
+ model: "Model",
26
+ maxTokens: "Max Tokens",
27
+ runs: "Runs",
28
+ prompt: "Prompt"
29
+ },
30
+ runLabel: (index) => `[\u8FD0\u884C ${index}]`,
31
+ runProgressLabel: (current, total) => `[\u8FD0\u884C ${current}/${total}]`,
32
+ reportTitle: "Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A",
33
+ speedChartTitle: "Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)",
34
+ tpsHistogramTitle: "TPS \u5206\u5E03",
35
+ noChartData: "\u6CA1\u6709\u53EF\u7528\u4E8E\u56FE\u8868\u7684\u6570\u636E",
36
+ noTpsData: "\u6CA1\u6709 TPS \u6570\u636E\u53EF\u7528",
37
+ statsSummaryTitle: (sampleSize) => `\u7EDF\u8BA1\u6C47\u603B (N=${sampleSize})`,
38
+ statsHeaders: {
39
+ metric: "\u6307\u6807",
40
+ mean: "\u5747\u503C",
41
+ min: "\u6700\u5C0F\u503C",
42
+ max: "\u6700\u5927\u503C",
43
+ stdDev: "\u6807\u51C6\u5DEE"
44
+ },
45
+ statsLabels: {
46
+ ttft: "TTFT (ms)",
47
+ totalTime: "\u603B\u8017\u65F6 (ms)",
48
+ totalTokens: "\u603B Token \u6570",
49
+ averageSpeed: "\u5E73\u5747\u901F\u5EA6",
50
+ peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
51
+ peakTps: "\u5CF0\u503C TPS"
52
+ },
53
+ resultLabels: {
54
+ ttft: "TTFT",
55
+ totalTime: "\u603B\u8017\u65F6",
56
+ totalTokens: "\u603B Token \u6570",
57
+ averageSpeed: "\u5E73\u5747\u901F\u5EA6",
58
+ peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
59
+ peakTps: "\u5CF0\u503C TPS"
60
+ },
61
+ htmlTitle: "Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A",
62
+ htmlReportTitle: "LLM API Token \u6D41\u5F0F\u6027\u80FD\u6D4B\u8BD5\u62A5\u544A",
63
+ htmlGenerated: (file) => `\u2713 HTML \u62A5\u544A\u5DF2\u751F\u6210: ${file}`,
64
+ htmlOpenError: (file) => `\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u62A5\u544A\uFF0C\u8BF7\u624B\u52A8\u6253\u5F00: ${file}`,
65
+ htmlConfigSection: "\u6D4B\u8BD5\u914D\u7F6E",
66
+ htmlSummarySection: "\u7EDF\u8BA1\u6C47\u603B",
67
+ htmlChartsSection: "\u56FE\u8868\u5206\u6790",
68
+ htmlDetailsSection: "\u8BE6\u7EC6\u6570\u636E",
69
+ htmlTestTime: "\u6D4B\u8BD5\u65F6\u95F4",
70
+ htmlMetric: "\u6307\u6807",
71
+ htmlValue: "\u6570\u503C",
72
+ htmlRun: "\u8FD0\u884C",
73
+ htmlTokens: "Token \u6570",
74
+ htmlDuration: "\u8017\u65F6",
75
+ htmlSpeed: "\u901F\u5EA6",
76
+ htmlTps: "TPS",
77
+ htmlAverageTps: "\u5E73\u5747 TPS",
78
+ htmlTpsDistribution: "TPS \u5206\u5E03",
79
+ htmlTimeUnit: "\u79D2",
80
+ htmlTpsChartHover: "\u6B21"
81
+ };
82
+ var enMessages = {
83
+ defaultPrompt: "Write a short essay about AI",
84
+ appTitle: "\u{1F680} Token Speed Test",
85
+ runningTests: "\u23F3 Running tests...",
86
+ streamingOutput: "Model output (streaming):",
87
+ testComplete: "\u2705 Tests complete!",
88
+ errorPrefix: "\u274C Error",
89
+ unknownError: "\u274C An unknown error occurred",
90
+ configLabels: {
91
+ provider: "Provider",
92
+ model: "Model",
93
+ maxTokens: "Max Tokens",
94
+ runs: "Runs",
95
+ prompt: "Prompt"
96
+ },
97
+ runLabel: (index) => `[Run ${index}]`,
98
+ runProgressLabel: (current, total) => `[Run ${current}/${total}]`,
99
+ reportTitle: "Token Speed Test Report",
100
+ speedChartTitle: "Token Speed Trend (TPS)",
101
+ tpsHistogramTitle: "TPS Distribution",
102
+ noChartData: "No data available for chart",
103
+ noTpsData: "No TPS data available",
104
+ statsSummaryTitle: (sampleSize) => `Summary (N=${sampleSize})`,
105
+ statsHeaders: {
106
+ metric: "Metric",
107
+ mean: "Mean",
108
+ min: "Min",
109
+ max: "Max",
110
+ stdDev: "Std Dev"
111
+ },
112
+ statsLabels: {
113
+ ttft: "TTFT (ms)",
114
+ totalTime: "Total Time (ms)",
115
+ totalTokens: "Total Tokens",
116
+ averageSpeed: "Avg Speed",
117
+ peakSpeed: "Peak Speed",
118
+ peakTps: "Peak TPS"
119
+ },
120
+ resultLabels: {
121
+ ttft: "TTFT",
122
+ totalTime: "Total Time",
123
+ totalTokens: "Total Tokens",
124
+ averageSpeed: "Avg Speed",
125
+ peakSpeed: "Peak Speed",
126
+ peakTps: "Peak TPS"
127
+ },
128
+ htmlTitle: "Token Speed Test Report",
129
+ htmlReportTitle: "LLM API Token Streaming Performance Report",
130
+ htmlGenerated: (file) => `\u2713 HTML report generated: ${file}`,
131
+ htmlOpenError: (file) => `Could not auto-open report, please open manually: ${file}`,
132
+ htmlConfigSection: "Test Configuration",
133
+ htmlSummarySection: "Summary Statistics",
134
+ htmlChartsSection: "Chart Analysis",
135
+ htmlDetailsSection: "Detailed Data",
136
+ htmlTestTime: "Test Time",
137
+ htmlMetric: "Metric",
138
+ htmlValue: "Value",
139
+ htmlRun: "Run",
140
+ htmlTokens: "Tokens",
141
+ htmlDuration: "Duration",
142
+ htmlSpeed: "Speed",
143
+ htmlTps: "TPS",
144
+ htmlAverageTps: "Average TPS",
145
+ htmlTpsDistribution: "TPS Distribution",
146
+ htmlTimeUnit: "s",
147
+ htmlTpsChartHover: "count"
148
+ };
149
+ function isSupportedLang(value) {
150
+ return SUPPORTED_LANGS.includes(value);
151
+ }
152
+ function resolveLang(value) {
153
+ if (!value) {
154
+ return DEFAULT_LANG;
155
+ }
156
+ const normalized = value.toLowerCase();
157
+ if (!isSupportedLang(normalized)) {
158
+ throw new Error(`Invalid lang: ${value}. Must be 'zh' or 'en'.`);
159
+ }
160
+ return normalized;
161
+ }
162
+ function getMessages(lang) {
163
+ return lang === "en" ? enMessages : zhMessages;
164
+ }
9
165
 
10
166
  // src/config.ts
11
167
  var DEFAULT_MODELS = {
@@ -14,7 +170,6 @@ var DEFAULT_MODELS = {
14
170
  };
15
171
  var DEFAULT_MAX_TOKENS = 1024;
16
172
  var DEFAULT_RUNS = 3;
17
- var DEFAULT_PROMPT = "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587";
18
173
  function parseConfig(args) {
19
174
  const {
20
175
  apiKey,
@@ -23,8 +178,12 @@ function parseConfig(args) {
23
178
  model,
24
179
  maxTokens = DEFAULT_MAX_TOKENS,
25
180
  runs = DEFAULT_RUNS,
26
- prompt = DEFAULT_PROMPT
181
+ prompt,
182
+ lang: langInput
27
183
  } = args;
184
+ const lang = resolveLang(langInput);
185
+ const messages = getMessages(lang);
186
+ const finalPrompt = prompt ?? messages.defaultPrompt;
28
187
  if (!apiKey || apiKey.trim() === "") {
29
188
  throw new Error("API Key is required. Use --api-key or -k to provide it.");
30
189
  }
@@ -45,7 +204,8 @@ function parseConfig(args) {
45
204
  model: finalModel,
46
205
  maxTokens,
47
206
  runCount: runs,
48
- prompt: prompt.trim()
207
+ prompt: finalPrompt.trim(),
208
+ lang
49
209
  };
50
210
  }
51
211
 
@@ -202,10 +362,11 @@ async function streamTest(config) {
202
362
  }
203
363
  async function runMultipleTests(config) {
204
364
  const results = [];
365
+ const messages = getMessages(config.lang);
205
366
  for (let i = 0; i < config.runCount; i++) {
206
367
  if (config.runCount > 1) {
207
368
  const label = `
208
- [\u8FD0\u884C ${i + 1}/${config.runCount}]`;
369
+ ${messages.runProgressLabel(i + 1, config.runCount)}`;
209
370
  console.log(label);
210
371
  console.log("-".repeat(label.length - 1));
211
372
  }
@@ -366,9 +527,10 @@ function padStartWidth(text, width) {
366
527
  }
367
528
  return " ".repeat(width - currentWidth) + text;
368
529
  }
369
- function renderSpeedChart(tps, maxSpeed) {
530
+ function renderSpeedChart(tps, maxSpeed, lang = DEFAULT_LANG) {
531
+ const messages = getMessages(lang);
370
532
  if (tps.length === 0) {
371
- return "No data available for chart";
533
+ return messages.noChartData;
372
534
  }
373
535
  const actualMax = maxSpeed ?? Math.max(...tps, 1);
374
536
  const maxVal = Math.max(actualMax, 1);
@@ -377,7 +539,7 @@ function renderSpeedChart(tps, maxSpeed) {
377
539
  const chartWidth = stringWidth(emptyRow) - 2;
378
540
  const axisPrefix = `\u2502 ${padStartWidth("", Y_LABEL_WIDTH)} \u253C`;
379
541
  const lines = [];
380
- lines.push("Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)");
542
+ lines.push(messages.speedChartTitle);
381
543
  lines.push("\u250C" + "\u2500".repeat(chartWidth) + "\u2510");
382
544
  for (let row = CHART_HEIGHT - 1; row >= 0; row--) {
383
545
  const value = row / (CHART_HEIGHT - 1) * maxVal;
@@ -423,12 +585,13 @@ function generateXLabels(dataPoints, maxLabels) {
423
585
  }
424
586
  return labels;
425
587
  }
426
- function renderTPSHistogram(tps) {
588
+ function renderTPSHistogram(tps, lang = DEFAULT_LANG) {
589
+ const messages = getMessages(lang);
427
590
  if (tps.length === 0) {
428
- return "No TPS data available";
591
+ return messages.noTpsData;
429
592
  }
430
593
  const lines = [];
431
- lines.push("TPS \u5206\u5E03");
594
+ lines.push(messages.tpsHistogramTitle);
432
595
  const maxTps = Math.max(...tps, 1);
433
596
  const buckets = 10;
434
597
  const bucketSize = maxTps / buckets;
@@ -454,18 +617,19 @@ function renderTPSHistogram(tps) {
454
617
  }
455
618
  return lines.join("\n");
456
619
  }
457
- function renderStatsTable(stats) {
620
+ function renderStatsTable(stats, lang = DEFAULT_LANG) {
621
+ const messages = getMessages(lang);
458
622
  const lines = [];
459
623
  lines.push("");
460
- lines.push("\u7EDF\u8BA1\u6C47\u603B (N=" + stats.sampleSize + ")");
461
- const headerRow = "\u2502 " + padEndWidth("\u6307\u6807", STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth("\u5747\u503C", STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth("\u6700\u5C0F\u503C", STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth("\u6700\u5927\u503C", STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth("\u6807\u51C6\u5DEE", STAT_VALUE_WIDTH) + " \u2502";
624
+ lines.push(messages.statsSummaryTitle(stats.sampleSize));
625
+ const headerRow = "\u2502 " + padEndWidth(messages.statsHeaders.metric, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.mean, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.min, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.max, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.stdDev, STAT_VALUE_WIDTH) + " \u2502";
462
626
  const tableWidth = stringWidth(headerRow) - 2;
463
627
  lines.push("\u250C" + "\u2500".repeat(tableWidth) + "\u2510");
464
628
  lines.push(headerRow);
465
629
  lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
466
630
  lines.push(
467
631
  formatStatRow(
468
- "TTFT (ms)",
632
+ messages.statsLabels.ttft,
469
633
  stats.mean.ttft,
470
634
  stats.min.ttft,
471
635
  stats.max.ttft,
@@ -476,7 +640,7 @@ function renderStatsTable(stats) {
476
640
  lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
477
641
  lines.push(
478
642
  formatStatRow(
479
- "\u603B\u8017\u65F6 (ms)",
643
+ messages.statsLabels.totalTime,
480
644
  stats.mean.totalTime,
481
645
  stats.min.totalTime,
482
646
  stats.max.totalTime,
@@ -487,7 +651,7 @@ function renderStatsTable(stats) {
487
651
  lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
488
652
  lines.push(
489
653
  formatStatRow(
490
- "\u603B Token \u6570",
654
+ messages.statsLabels.totalTokens,
491
655
  stats.mean.totalTokens,
492
656
  stats.min.totalTokens,
493
657
  stats.max.totalTokens,
@@ -498,7 +662,7 @@ function renderStatsTable(stats) {
498
662
  lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
499
663
  lines.push(
500
664
  formatStatRow(
501
- "\u5E73\u5747\u901F\u5EA6",
665
+ messages.statsLabels.averageSpeed,
502
666
  stats.mean.averageSpeed,
503
667
  stats.min.averageSpeed,
504
668
  stats.max.averageSpeed,
@@ -509,7 +673,7 @@ function renderStatsTable(stats) {
509
673
  lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
510
674
  lines.push(
511
675
  formatStatRow(
512
- "\u5CF0\u503C\u901F\u5EA6",
676
+ messages.statsLabels.peakSpeed,
513
677
  stats.mean.peakSpeed,
514
678
  stats.min.peakSpeed,
515
679
  stats.max.peakSpeed,
@@ -520,7 +684,7 @@ function renderStatsTable(stats) {
520
684
  lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
521
685
  lines.push(
522
686
  formatStatRow(
523
- "\u5CF0\u503C TPS",
687
+ messages.statsLabels.peakTps,
524
688
  stats.mean.peakTps,
525
689
  stats.min.peakTps,
526
690
  stats.max.peakTps,
@@ -541,39 +705,531 @@ function formatTimeWithDecimals(ms) {
541
705
  }
542
706
  return `${ms.toFixed(2)}ms`;
543
707
  }
544
- function renderSingleResult(metrics, runIndex) {
708
+ function renderSingleResult(metrics, runIndex, lang = DEFAULT_LANG) {
709
+ const messages = getMessages(lang);
545
710
  const lines = [];
546
711
  lines.push(`
547
- [\u8FD0\u884C ${runIndex + 1}]`);
548
- lines.push(` TTFT: ${formatTimeWithDecimals(metrics.ttft)}`);
549
- lines.push(` \u603B\u8017\u65F6: ${formatTimeWithDecimals(metrics.totalTime)}`);
550
- lines.push(` \u603B Token \u6570: ${metrics.totalTokens}`);
551
- lines.push(` \u5E73\u5747\u901F\u5EA6: ${metrics.averageSpeed.toFixed(2)} tokens/s`);
552
- lines.push(` \u5CF0\u503C\u901F\u5EA6: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
553
- lines.push(` \u5CF0\u503C TPS: ${metrics.peakTps.toFixed(2)} tokens/s`);
712
+ ${messages.runLabel(runIndex + 1)}`);
713
+ lines.push(` ${messages.resultLabels.ttft}: ${formatTimeWithDecimals(metrics.ttft)}`);
714
+ lines.push(` ${messages.resultLabels.totalTime}: ${formatTimeWithDecimals(metrics.totalTime)}`);
715
+ lines.push(` ${messages.resultLabels.totalTokens}: ${metrics.totalTokens}`);
716
+ lines.push(
717
+ ` ${messages.resultLabels.averageSpeed}: ${metrics.averageSpeed.toFixed(2)} tokens/s`
718
+ );
719
+ lines.push(` ${messages.resultLabels.peakSpeed}: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
720
+ lines.push(` ${messages.resultLabels.peakTps}: ${metrics.peakTps.toFixed(2)} tokens/s`);
554
721
  return lines.join("\n");
555
722
  }
556
- function renderReport(stats) {
723
+ function renderReport(stats, lang = DEFAULT_LANG) {
724
+ const messages = getMessages(lang);
557
725
  const lines = [];
558
726
  lines.push("\n" + "\u2550".repeat(72));
559
- lines.push("Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A");
727
+ lines.push(messages.reportTitle);
560
728
  lines.push("\u2550".repeat(72));
561
- lines.push(renderStatsTable(stats));
729
+ lines.push(renderStatsTable(stats, lang));
562
730
  if (stats.mean.tps.length > 0) {
563
- lines.push("\n" + renderSpeedChart(stats.mean.tps));
731
+ lines.push("\n" + renderSpeedChart(stats.mean.tps, void 0, lang));
564
732
  }
565
733
  if (stats.mean.tps.length > 0) {
566
- lines.push("\n" + renderTPSHistogram(stats.mean.tps));
734
+ lines.push("\n" + renderTPSHistogram(stats.mean.tps, lang));
567
735
  }
568
736
  return lines.join("\n");
569
737
  }
570
738
 
739
+ // src/html-report.ts
740
+ import { readFileSync } from "fs";
741
+ import { dirname, resolve } from "path";
742
+ import { fileURLToPath } from "url";
743
+ var CHART_COLORS = [
744
+ "#00f5ff",
745
+ // cyan
746
+ "#ff00aa",
747
+ // magenta
748
+ "#ffcc00",
749
+ // yellow
750
+ "#00ff88",
751
+ // green
752
+ "#ff6600",
753
+ // orange
754
+ "#aa00ff"
755
+ // purple
756
+ ];
757
+ var PALETTE = {
758
+ bg: "#0a0a0f",
759
+ bgSecondary: "#12121a",
760
+ bgCard: "#1a1a24",
761
+ border: "#2a2a3a",
762
+ text: "#e4e4eb",
763
+ textMuted: "#6a6a7a",
764
+ accent: "#00f5ff",
765
+ accentSecondary: "#ff00aa",
766
+ accentTertiary: "#ffcc00"
767
+ };
768
+ function formatNumber(num, decimals = 2) {
769
+ return num.toFixed(decimals);
770
+ }
771
+ function formatTime(ms) {
772
+ if (ms < 1e3) {
773
+ return `${ms.toFixed(0)}ms`;
774
+ }
775
+ return `${(ms / 1e3).toFixed(2)}s`;
776
+ }
777
+ function escapeHtml(text) {
778
+ const map = {
779
+ "&": "&amp;",
780
+ "<": "&lt;",
781
+ ">": "&gt;",
782
+ '"': "&quot;",
783
+ "'": "&#039;"
784
+ };
785
+ return text.replace(/[&<>"']/g, (m) => map[m]);
786
+ }
787
+ function loadTemplate() {
788
+ const __filename = fileURLToPath(import.meta.url);
789
+ const __dirname = dirname(__filename);
790
+ return readFileSync(resolve(__dirname, "template.html"), "utf-8");
791
+ }
792
+ function replaceTemplate(template, data) {
793
+ let result = template;
794
+ for (const [key, value] of Object.entries(data)) {
795
+ result = result.replaceAll(new RegExp(`{{${key}}}`, "g"), value);
796
+ }
797
+ return result;
798
+ }
799
+ function generateSpeedChart(results, messages) {
800
+ const allTps = results.flatMap((r) => r.tps);
801
+ if (allTps.length === 0) {
802
+ return `<div class="no-data">${messages.noChartData || "No data available"}</div>`;
803
+ }
804
+ const maxTps = Math.max(...allTps, 1);
805
+ const maxDuration = Math.max(...results.map((r) => r.tps.length));
806
+ const width = 800;
807
+ const height = 320;
808
+ const padding = { top: 30, right: 30, bottom: 45, left: 55 };
809
+ const chartWidth = width - padding.left - padding.right;
810
+ const chartHeight = height - padding.top - padding.bottom;
811
+ const avgTps = [];
812
+ for (let i = 0; i < maxDuration; i++) {
813
+ const values = results.map((r) => r.tps[i] ?? 0);
814
+ avgTps.push(values.reduce((a, b) => a + b, 0) / values.length);
815
+ }
816
+ const polylines = results.map((result, idx) => {
817
+ const color = CHART_COLORS[idx % CHART_COLORS.length];
818
+ let points = "";
819
+ let areaPoints = `${padding.left},${height - padding.bottom} `;
820
+ result.tps.forEach((tps, i) => {
821
+ const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
822
+ const y = padding.top + chartHeight - tps / maxTps * chartHeight;
823
+ points += `${x},${y} `;
824
+ areaPoints += `${x},${y} `;
825
+ });
826
+ areaPoints += `${padding.left + chartWidth},${height - padding.bottom}`;
827
+ return `
828
+ <defs>
829
+ <linearGradient id="grad-${idx}" x1="0%" y1="0%" x2="0%" y2="100%">
830
+ <stop offset="0%" style="stop-color:${color};stop-opacity:0.3"/>
831
+ <stop offset="100%" style="stop-color:${color};stop-opacity:0"/>
832
+ </linearGradient>
833
+ </defs>
834
+ <polygon
835
+ points="${areaPoints.trim()}"
836
+ fill="url(#grad-${idx})"
837
+ class="area-${idx}"
838
+ />
839
+ <polyline
840
+ fill="none"
841
+ stroke="${color}"
842
+ stroke-width="2.5"
843
+ points="${points.trim()}"
844
+ class="line line-${idx}"
845
+ data-run="${idx + 1}"
846
+ >
847
+ <animate
848
+ attributeName="stroke-dasharray"
849
+ from="0,2000"
850
+ to="2000,0"
851
+ dur="1.5s"
852
+ fill="freeze"
853
+ calcMode="spline"
854
+ keySplines="0.4 0 0.2 1"
855
+ />
856
+ </polyline>
857
+ ${result.tps.map((tps, i) => {
858
+ const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
859
+ const y = padding.top + chartHeight - tps / maxTps * chartHeight;
860
+ return `<circle cx="${x}" cy="${y}" r="4" fill="${PALETTE.bg}" stroke="${color}" stroke-width="2" class="dot-${idx}" opacity="0"><title>${messages.htmlAverageTps || "Avg TPS"} ${i}s: ${tps.toFixed(1)}</title>
861
+ <animate attributeName="opacity" from="0" to="1" begin="${0.5 + i * 0.05}s" dur="0.3s" fill="freeze"/>
862
+ </circle>`;
863
+ }).join("")}
864
+ `;
865
+ }).join("\n ");
866
+ let avgPoints = "";
867
+ avgTps.forEach((tps, i) => {
868
+ const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
869
+ const y = padding.top + chartHeight - tps / maxTps * chartHeight;
870
+ avgPoints += `${x},${y} `;
871
+ });
872
+ const yLabels = [];
873
+ for (let i = 0; i <= 5; i++) {
874
+ const value = Math.round(maxTps * i / 5);
875
+ const y = padding.top + chartHeight - i / 5 * chartHeight;
876
+ yLabels.push(
877
+ `<text x="${padding.left - 12}" y="${y + 4}" text-anchor="end" font-size="11" fill="${PALETTE.textMuted}">${value}</text>`
878
+ );
879
+ if (i > 0) {
880
+ const yLine = padding.top + chartHeight - i / 5 * chartHeight;
881
+ yLabels.push(
882
+ `<line x1="${padding.left}" y1="${yLine}" x2="${width - padding.right}" y2="${yLine}" stroke="${PALETTE.border}" stroke-width="1" opacity="0.5"/>`
883
+ );
884
+ }
885
+ }
886
+ const xLabels = [];
887
+ const xSteps = Math.min(maxDuration, 10);
888
+ for (let i = 0; i < xSteps; i++) {
889
+ const x = padding.left + i / Math.max(xSteps - 1, 1) * chartWidth;
890
+ const label = i.toString();
891
+ xLabels.push(
892
+ `<text x="${x}" y="${height - padding.bottom + 20}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">${label}${messages.htmlTimeUnit}</text>`
893
+ );
894
+ }
895
+ return `
896
+ <svg viewBox="0 0 ${width} ${height}" class="chart" id="speedChart">
897
+ <style>
898
+ #speedChart .line { stroke-dasharray: 2000; stroke-dashoffset: 0; }
899
+ #speedChart .line:hover { stroke-width: 4; filter: drop-shadow(0 0 8px currentColor); }
900
+ #speedChart circle { transition: all 0.2s ease; cursor: pointer; }
901
+ #speedChart circle:hover { r: 6; stroke-width: 3; }
902
+ </style>
903
+ <rect x="${padding.left}" y="${padding.top}" width="${chartWidth}" height="${chartHeight}" fill="${PALETTE.bgCard}" rx="4"/>
904
+ ${yLabels.join("\n ")}
905
+ ${xLabels.join("\n ")}
906
+ <line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
907
+ <line x1="${padding.left}" y1="${height - padding.bottom}" x2="${width - padding.right}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
908
+ ${polylines}
909
+ <polyline
910
+ fill="none"
911
+ stroke="${PALETTE.text}"
912
+ stroke-width="2"
913
+ stroke-dasharray="6,4"
914
+ opacity="0.7"
915
+ points="${avgPoints.trim()}"
916
+ />
917
+ <text x="${padding.left + chartWidth / 2}" y="${height - 8}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">TIME (${messages.htmlTimeUnit})</text>
918
+ <text x="12" y="${padding.top + chartHeight / 2}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}" transform="rotate(-90, 12, ${padding.top + chartHeight / 2})">TPS</text>
919
+ </svg>
920
+ <div class="chart-legend">
921
+ <div class="legend-item">
922
+ <span class="legend-line avg"></span>
923
+ <span>${messages.htmlSummarySection}</span>
924
+ </div>
925
+ ${results.map((_, idx) => {
926
+ const color = CHART_COLORS[idx % CHART_COLORS.length];
927
+ return `
928
+ <div class="legend-item">
929
+ <span class="legend-line" style="background: ${color};"></span>
930
+ <span>${messages.htmlRun} ${idx + 1}</span>
931
+ </div>`;
932
+ }).join("")}
933
+ </div>
934
+ `;
935
+ }
936
+ function generateTPSHistogram(stats, messages) {
937
+ const allTps = stats.mean.tps;
938
+ if (allTps.length === 0) {
939
+ return `<div class="no-data">${messages.noTpsData || "No TPS data available"}</div>`;
940
+ }
941
+ const maxTps = Math.max(...allTps);
942
+ const width = 400;
943
+ const height = 280;
944
+ const padding = { top: 25, right: 20, bottom: 40, left: 50 };
945
+ const chartWidth = width - padding.left - padding.right;
946
+ const chartHeight = height - padding.top - padding.bottom;
947
+ const bars = allTps.map((tps, i) => {
948
+ const barWidth = chartWidth / allTps.length - 2;
949
+ const x = padding.left + i / allTps.length * chartWidth;
950
+ const barHeight = tps / maxTps * chartHeight;
951
+ const y = padding.top + chartHeight - barHeight;
952
+ const hue = 180 + tps / maxTps * 60;
953
+ const color = `hsl(${hue}, 100%, 60%)`;
954
+ return `
955
+ <rect
956
+ x="${x}"
957
+ y="${y}"
958
+ width="${barWidth}"
959
+ height="${barHeight}"
960
+ fill="${color}"
961
+ class="bar"
962
+ data-second="${i}"
963
+ data-tps="${tps.toFixed(2)}"
964
+ rx="2"
965
+ >
966
+ <title>${messages.htmlAverageTps || "Average TPS"} ${i}s: ${tps.toFixed(1)}</title>
967
+ <animate
968
+ attributeName="height"
969
+ from="0"
970
+ to="${barHeight}"
971
+ dur="0.8s"
972
+ fill="freeze"
973
+ calcMode="spline"
974
+ keySplines="0.4 0 0.2 1"
975
+ begin="${i * 0.05}s"
976
+ />
977
+ <animate
978
+ attributeName="y"
979
+ from="${height - padding.bottom}"
980
+ to="${y}"
981
+ dur="0.8s"
982
+ fill="freeze"
983
+ calcMode="spline"
984
+ keySplines="0.4 0 0.2 1"
985
+ begin="${i * 0.05}s"
986
+ />
987
+ </rect>
988
+ `;
989
+ }).join("");
990
+ const yLabels = [];
991
+ for (let i = 0; i <= 5; i++) {
992
+ const value = Math.round(maxTps * i / 5);
993
+ const y = padding.top + chartHeight - i / 5 * chartHeight;
994
+ yLabels.push(
995
+ `<text x="${padding.left - 10}" y="${y + 4}" text-anchor="end" font-size="11" fill="${PALETTE.textMuted}">${value}</text>`
996
+ );
997
+ }
998
+ const xLabels = [];
999
+ const xSteps = Math.min(allTps.length, 8);
1000
+ for (let i = 0; i < xSteps; i++) {
1001
+ const x = padding.left + i / Math.max(xSteps - 1, 1) * chartWidth;
1002
+ const label = i.toString();
1003
+ xLabels.push(
1004
+ `<text x="${x}" y="${height - padding.bottom + 18}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">${label}${messages.htmlTimeUnit}</text>`
1005
+ );
1006
+ }
1007
+ return `
1008
+ <svg viewBox="0 0 ${width} ${height}" class="chart" id="tpsChart">
1009
+ <style>
1010
+ #tpsChart .bar { transition: all 0.2s ease; cursor: pointer; opacity: 0.9; }
1011
+ #tpsChart .bar:hover { opacity: 1; filter: brightness(1.2); }
1012
+ </style>
1013
+ <rect x="${padding.left}" y="${padding.top}" width="${chartWidth}" height="${chartHeight}" fill="${PALETTE.bgCard}" rx="4"/>
1014
+ ${yLabels.join("\n ")}
1015
+ ${xLabels.join("\n ")}
1016
+ <line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
1017
+ <line x1="${padding.left}" y1="${height - padding.bottom}" x2="${width - padding.right}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
1018
+ ${bars}
1019
+ </svg>
1020
+ `;
1021
+ }
1022
+ function generateHTMLReport(options2) {
1023
+ const { config, singleResults, stats, lang, messages } = options2;
1024
+ const isZh = lang === "zh";
1025
+ const testTime = (/* @__PURE__ */ new Date()).toLocaleString(isZh ? "zh-CN" : "en-US");
1026
+ const summaryCards = [
1027
+ {
1028
+ label: messages.statsLabels.ttft,
1029
+ value: formatTime(stats.mean.ttft),
1030
+ detail: `${messages.statsHeaders.min}: ${formatTime(stats.min.ttft)} \xB7 ${messages.statsHeaders.max}: ${formatTime(stats.max.ttft)}`,
1031
+ accent: PALETTE.accent
1032
+ },
1033
+ {
1034
+ label: messages.statsLabels.averageSpeed,
1035
+ value: formatNumber(stats.mean.averageSpeed),
1036
+ detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.averageSpeed)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.averageSpeed)}`,
1037
+ accent: PALETTE.accentSecondary,
1038
+ unit: messages.htmlSpeed
1039
+ },
1040
+ {
1041
+ label: messages.statsLabels.peakSpeed,
1042
+ value: formatNumber(stats.mean.peakSpeed),
1043
+ detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.peakSpeed)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.peakSpeed)}`,
1044
+ accent: PALETTE.accentTertiary,
1045
+ unit: messages.htmlSpeed
1046
+ },
1047
+ {
1048
+ label: messages.statsLabels.totalTokens,
1049
+ value: formatNumber(stats.mean.totalTokens, 0),
1050
+ detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.totalTokens, 0)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.totalTokens, 0)}`,
1051
+ accent: "#00ff88"
1052
+ }
1053
+ ];
1054
+ const detailRows = singleResults.map(
1055
+ (result, idx) => `
1056
+ <tr>
1057
+ <td><span class="run-badge">${idx + 1}</span></td>
1058
+ <td>${formatTime(result.ttft)}</td>
1059
+ <td>${formatTime(result.totalTime)}</td>
1060
+ <td>${result.totalTokens}</td>
1061
+ <td>${formatNumber(result.averageSpeed)}</td>
1062
+ <td>${formatNumber(result.peakSpeed)}</td>
1063
+ <td>${result.peakTps}</td>
1064
+ </tr>
1065
+ `
1066
+ ).join("");
1067
+ const statsRows = [
1068
+ {
1069
+ metric: messages.statsLabels.ttft,
1070
+ mean: formatTime(stats.mean.ttft),
1071
+ min: formatTime(stats.min.ttft),
1072
+ max: formatTime(stats.max.ttft),
1073
+ stdDev: formatTime(stats.stdDev.ttft)
1074
+ },
1075
+ {
1076
+ metric: messages.statsLabels.totalTime,
1077
+ mean: formatTime(stats.mean.totalTime),
1078
+ min: formatTime(stats.min.totalTime),
1079
+ max: formatTime(stats.max.totalTime),
1080
+ stdDev: formatTime(stats.stdDev.totalTime)
1081
+ },
1082
+ {
1083
+ metric: messages.statsLabels.totalTokens,
1084
+ mean: formatNumber(stats.mean.totalTokens, 1),
1085
+ min: formatNumber(stats.min.totalTokens, 0),
1086
+ max: formatNumber(stats.max.totalTokens, 0),
1087
+ stdDev: formatNumber(stats.stdDev.totalTokens, 1)
1088
+ },
1089
+ {
1090
+ metric: messages.statsLabels.averageSpeed,
1091
+ mean: formatNumber(stats.mean.averageSpeed),
1092
+ min: formatNumber(stats.min.averageSpeed),
1093
+ max: formatNumber(stats.max.averageSpeed),
1094
+ stdDev: formatNumber(stats.stdDev.averageSpeed)
1095
+ },
1096
+ {
1097
+ metric: messages.statsLabels.peakSpeed,
1098
+ mean: formatNumber(stats.mean.peakSpeed),
1099
+ min: formatNumber(stats.min.peakSpeed),
1100
+ max: formatNumber(stats.max.peakSpeed),
1101
+ stdDev: formatNumber(stats.stdDev.peakSpeed)
1102
+ },
1103
+ {
1104
+ metric: messages.statsLabels.peakTps,
1105
+ mean: formatNumber(stats.mean.peakTps),
1106
+ min: formatNumber(stats.min.peakTps),
1107
+ max: formatNumber(stats.max.peakTps),
1108
+ stdDev: formatNumber(stats.stdDev.peakTps)
1109
+ }
1110
+ ].map(
1111
+ (row) => `
1112
+ <tr>
1113
+ <td class="metric-name">${row.metric}</td>
1114
+ <td class="value-primary">${row.mean}</td>
1115
+ <td>${row.min}</td>
1116
+ <td>${row.max}</td>
1117
+ <td>${row.stdDev}</td>
1118
+ </tr>
1119
+ `
1120
+ ).join("");
1121
+ const speedChart = generateSpeedChart(singleResults, messages);
1122
+ const tpsChart = generateTPSHistogram(stats, messages);
1123
+ const configGridHtml = `
1124
+ <div class="config-grid">
1125
+ <div class="config-item">
1126
+ <span class="config-label">${messages.configLabels.provider}</span>
1127
+ <span class="config-value">${config.provider.toUpperCase()}</span>
1128
+ </div>
1129
+ <div class="config-item">
1130
+ <span class="config-label">${messages.configLabels.model}</span>
1131
+ <span class="config-value">${escapeHtml(config.model)}</span>
1132
+ </div>
1133
+ <div class="config-item">
1134
+ <span class="config-label">${messages.configLabels.maxTokens}</span>
1135
+ <span class="config-value">${config.maxTokens}</span>
1136
+ </div>
1137
+ <div class="config-item">
1138
+ <span class="config-label">${messages.configLabels.runs}</span>
1139
+ <span class="config-value">${config.runCount}</span>
1140
+ </div>
1141
+ <div class="config-item wide">
1142
+ <span class="config-label">${messages.configLabels.prompt}</span>
1143
+ <span class="config-value">"${escapeHtml(config.prompt)}"</span>
1144
+ </div>
1145
+ </div>
1146
+ `;
1147
+ const summaryCardsHtml = summaryCards.map(
1148
+ (card) => `
1149
+ <div class="card" style="--card-accent: ${card.accent}">
1150
+ <div class="card-label">${card.label}</div>
1151
+ <div class="card-value">${card.value}<span class="card-unit">${card.unit || ""}</span></div>
1152
+ <div class="card-detail">${card.detail}</div>
1153
+ </div>
1154
+ `
1155
+ ).join("");
1156
+ const chartsHtml = `
1157
+ <div class="charts-container">
1158
+ <div class="chart-wrapper">
1159
+ <div class="chart-title">${messages.speedChartTitle}</div>
1160
+ ${speedChart}
1161
+ </div>
1162
+ <div class="chart-wrapper">
1163
+ <div class="chart-title">${messages.htmlTpsDistribution}</div>
1164
+ ${tpsChart}
1165
+ </div>
1166
+ </div>
1167
+ `;
1168
+ const statsTableHtml = `
1169
+ <div class="table-wrapper">
1170
+ <table>
1171
+ <thead>
1172
+ <tr>
1173
+ <th>${messages.statsHeaders.metric}</th>
1174
+ <th>${messages.statsHeaders.mean}</th>
1175
+ <th>${messages.statsHeaders.min}</th>
1176
+ <th>${messages.statsHeaders.max}</th>
1177
+ <th>${messages.statsHeaders.stdDev}</th>
1178
+ </tr>
1179
+ </thead>
1180
+ <tbody>
1181
+ ${statsRows}
1182
+ </tbody>
1183
+ </table>
1184
+ </div>
1185
+ `;
1186
+ const detailsTableHtml = `
1187
+ <div class="table-wrapper">
1188
+ <table>
1189
+ <thead>
1190
+ <tr>
1191
+ <th>${messages.htmlRun}</th>
1192
+ <th>${messages.resultLabels.ttft}</th>
1193
+ <th>${messages.resultLabels.totalTime}</th>
1194
+ <th>${messages.resultLabels.totalTokens}</th>
1195
+ <th>${messages.resultLabels.averageSpeed}</th>
1196
+ <th>${messages.resultLabels.peakSpeed}</th>
1197
+ <th>${messages.resultLabels.peakTps}</th>
1198
+ </tr>
1199
+ </thead>
1200
+ <tbody>
1201
+ ${detailRows}
1202
+ </tbody>
1203
+ </table>
1204
+ </div>
1205
+ `;
1206
+ const data = {
1207
+ lang,
1208
+ title: messages.htmlTitle,
1209
+ reportTitle: messages.htmlReportTitle,
1210
+ testTimeLabel: messages.htmlTestTime,
1211
+ testTime,
1212
+ configSection: messages.htmlConfigSection,
1213
+ summarySection: messages.htmlSummarySection,
1214
+ chartsSection: messages.htmlChartsSection,
1215
+ statsTitle: messages.statsSummaryTitle(stats.sampleSize),
1216
+ detailsSection: messages.htmlDetailsSection,
1217
+ configGrid: configGridHtml,
1218
+ summaryCards: summaryCardsHtml,
1219
+ charts: chartsHtml,
1220
+ statsTable: statsTableHtml,
1221
+ detailsTable: detailsTableHtml
1222
+ };
1223
+ const template = loadTemplate();
1224
+ return replaceTemplate(template, data);
1225
+ }
1226
+
571
1227
  // src/index.ts
572
1228
  function getCliVersion() {
573
1229
  try {
574
- const currentDir = dirname(fileURLToPath(import.meta.url));
1230
+ const currentDir = dirname2(fileURLToPath2(import.meta.url));
575
1231
  const packagePath = join(currentDir, "..", "package.json");
576
- const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
1232
+ const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
577
1233
  return packageJson.version ?? "unknown";
578
1234
  } catch {
579
1235
  return "unknown";
@@ -581,9 +1237,10 @@ function getCliVersion() {
581
1237
  }
582
1238
  var program = new Command();
583
1239
  program.name("token-speed-test").description("A CLI tool to test LLM API token output speed").version(getCliVersion());
584
- program.option("-k, --api-key <key>", "API Key (required)", "").option("-p, --provider <provider>", "API provider: anthropic or openai", "anthropic").option("-u, --url <url>", "Custom API endpoint URL").option("-m, --model <model>", "Model name").option("--max-tokens <number>", "Maximum output tokens", "1024").option("-r, --runs <number>", "Number of test runs", "3").option("--prompt <text>", "Test prompt", "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587").parse(process.argv);
1240
+ program.option("-k, --api-key <key>", "API Key (required)", "").option("-p, --provider <provider>", "API provider: anthropic or openai", "anthropic").option("-u, --url <url>", "Custom API endpoint URL").option("-m, --model <model>", "Model name").option("--max-tokens <number>", "Maximum output tokens", "1024").option("-r, --runs <number>", "Number of test runs", "3").option("--prompt <text>", "Test prompt").option("--lang <lang>", "Output language: zh or en", "zh").option("--html", "Generate HTML report").option("-o, --output <path>", "HTML report output path", "report.html").parse(process.argv);
585
1241
  var options = program.opts();
586
1242
  async function main() {
1243
+ let messages = getMessages(DEFAULT_LANG);
587
1244
  try {
588
1245
  const config = parseConfig({
589
1246
  apiKey: options.apiKey,
@@ -592,37 +1249,72 @@ async function main() {
592
1249
  model: options.model,
593
1250
  maxTokens: parseInt(options.maxTokens, 10),
594
1251
  runs: parseInt(options.runs, 10),
595
- prompt: options.prompt
1252
+ prompt: options.prompt,
1253
+ lang: options.lang
596
1254
  });
597
- console.log(chalk.cyan("\n\u{1F680} Token \u901F\u5EA6\u6D4B\u8BD5\u5DE5\u5177"));
1255
+ messages = getMessages(config.lang);
1256
+ console.log(chalk.cyan(`
1257
+ ${messages.appTitle}`));
598
1258
  console.log(chalk.gray("\u2500".repeat(50)));
599
- console.log(chalk.gray(`Provider: ${chalk.white(config.provider)}`));
600
- console.log(chalk.gray(`Model: ${chalk.white(config.model)}`));
601
- console.log(chalk.gray(`Max Tokens: ${chalk.white(config.maxTokens)}`));
602
- console.log(chalk.gray(`Runs: ${chalk.white(config.runCount)}`));
1259
+ console.log(chalk.gray(`${messages.configLabels.provider}: ${chalk.white(config.provider)}`));
1260
+ console.log(chalk.gray(`${messages.configLabels.model}: ${chalk.white(config.model)}`));
1261
+ console.log(chalk.gray(`${messages.configLabels.maxTokens}: ${chalk.white(config.maxTokens)}`));
1262
+ console.log(chalk.gray(`${messages.configLabels.runs}: ${chalk.white(config.runCount)}`));
603
1263
  console.log(
604
1264
  chalk.gray(
605
- `Prompt: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? "..." : ""}`
1265
+ `${messages.configLabels.prompt}: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? "..." : ""}`
606
1266
  )
607
1267
  );
608
1268
  console.log(chalk.gray("\u2500".repeat(50)));
609
- console.log(chalk.yellow("\n\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...\n"));
610
- console.log(chalk.gray("\u6A21\u578B\u8F93\u51FA (\u6D41\u5F0F):\n"));
1269
+ console.log(chalk.yellow(`
1270
+ ${messages.runningTests}
1271
+ `));
1272
+ console.log(chalk.gray(`${messages.streamingOutput}
1273
+ `));
611
1274
  const results = await runMultipleTests(config);
612
1275
  const allMetrics = results.map((r) => calculateMetrics(r));
613
1276
  for (let i = 0; i < allMetrics.length; i++) {
614
- console.log(chalk.gray(renderSingleResult(allMetrics[i], i)));
1277
+ console.log(chalk.gray(renderSingleResult(allMetrics[i], i, config.lang)));
615
1278
  }
616
1279
  const stats = calculateStats(allMetrics);
617
- console.log(chalk.cyan("\n" + renderReport(stats)));
618
- console.log(chalk.green("\n\u2705 \u6D4B\u8BD5\u5B8C\u6210!\n"));
1280
+ console.log(chalk.cyan("\n" + renderReport(stats, config.lang)));
1281
+ console.log(chalk.green(`
1282
+ ${messages.testComplete}
1283
+ `));
1284
+ if (options.html) {
1285
+ const htmlPath = options.output;
1286
+ const htmlContent = generateHTMLReport({
1287
+ config,
1288
+ singleResults: allMetrics,
1289
+ stats,
1290
+ lang: config.lang,
1291
+ messages
1292
+ });
1293
+ try {
1294
+ await fsWriteFile(htmlPath, htmlContent, "utf-8");
1295
+ console.log(chalk.cyan(messages.htmlGenerated(htmlPath)));
1296
+ await open(htmlPath).catch(() => {
1297
+ console.warn(chalk.yellow(messages.htmlOpenError(htmlPath)));
1298
+ });
1299
+ } catch (err) {
1300
+ console.error(
1301
+ chalk.red(
1302
+ `
1303
+ ${messages.errorPrefix}: ${err instanceof Error ? err.message : String(err)}
1304
+ `
1305
+ )
1306
+ );
1307
+ }
1308
+ }
619
1309
  } catch (error) {
620
1310
  if (error instanceof Error) {
621
1311
  console.error(chalk.red(`
622
- \u274C \u9519\u8BEF: ${error.message}
1312
+ ${messages.errorPrefix}: ${error.message}
623
1313
  `));
624
1314
  } else {
625
- console.error(chalk.red("\n\u274C \u53D1\u751F\u672A\u77E5\u9519\u8BEF\n"));
1315
+ console.error(chalk.red(`
1316
+ ${messages.unknownError}
1317
+ `));
626
1318
  }
627
1319
  process.exit(1);
628
1320
  }