gpteam 0.1.6 → 0.1.8

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/lib/cli.js CHANGED
@@ -5,6 +5,7 @@ import { CLIENTS, writeClientConfig } from './config.js';
5
5
  import { getHelpText, PACKAGE_NAME, PACKAGE_VERSION } from './help.js';
6
6
  import { modelByID, validateApiKey } from './models.js';
7
7
  import { describeSplit, INGRESS_NODES } from './nodes.js';
8
+ import { createTheme, stripAnsi } from './terminal.js';
8
9
 
9
10
  export async function runCli(argv = []) {
10
11
  const normalizedArgv = normalizeSubcommand(argv);
@@ -18,25 +19,27 @@ export async function runCli(argv = []) {
18
19
  return;
19
20
  }
20
21
  const rl = readline.createInterface({ input, output });
22
+ const theme = createTheme({ stream: output });
21
23
  try {
22
- console.log('GPTeam API 配置助手');
23
- console.log('接下来会用你的 key 跑几次真实请求测速,然后把选中的入口写进客户端配置。原来的配置会先备份。\n');
24
+ printBanner(theme);
24
25
 
25
26
  const apiKey = args.key || args.apiKey || await askRequired(rl, '请输入 API key:');
26
- console.log('\n正在校验 API key,会请求 /v1/models。校验通过后才会继续。');
27
+ printStep(theme, 1, 5, '校验 API key', '请求 /v1/models,校验通过后才继续。');
27
28
  const validation = await validateApiKey(INGRESS_NODES, apiKey);
28
- console.log(`API key 校验通过:${validation.node.label}`);
29
- const client = await choose(rl, '请选择客户端类型', CLIENTS, args.client);
29
+ printStatus(theme, '通过', `API key 可用,校验入口:${validation.node.label}`);
30
+
31
+ printStep(theme, 2, 5, '选择客户端和模型');
32
+ const client = await choose(rl, '请选择客户端类型', CLIENTS, args.client, theme);
30
33
  const models = validation.models;
31
- const model = await chooseModel(rl, models, args.model);
34
+ const model = await chooseModel(rl, models, args.model, theme);
32
35
  const contextLength = await askContextLength(rl, model, args.context);
33
- const effort = await choose(rl, '请选择思考深度', model.efforts.map((id) => ({ id, label: id })), args.effort);
36
+ const effort = await choose(rl, '请选择思考深度', model.efforts.map((id) => ({ id, label: id })), args.effort, theme);
34
37
  const maxOutputTokens = Number(args.maxOutputTokens || 648);
35
38
 
36
- console.log('\n开始真实测速:GET /api/health + POST /v1/responses stream=true');
37
- console.log('测速会按入口并行执行,每个入口内部仍按轮次顺序执行,避免同时打出过多真实请求。');
38
- console.log('推荐规则:成功率优先,其次按首包、完成、尾延迟和健康检查计算体验分,分数越低越好。');
39
- console.log(`模型:${model.id},测速输出上限:${maxOutputTokens}`);
39
+ printStep(theme, 3, 5, '真实请求测速', 'GET /api/health + POST /v1/responses stream=true');
40
+ printHint(theme, '入口之间并行测速,单入口多轮顺序执行,避免同时打出过多真实请求。');
41
+ printHint(theme, '推荐规则:成功率优先,其次按首包、完成、尾延迟和健康检查计算体验分,分数越低越好。');
42
+ printHint(theme, `模型:${model.id},测速输出上限:${maxOutputTokens}`);
40
43
  const results = await benchmarkNodes(INGRESS_NODES, {
41
44
  apiKey,
42
45
  model: model.id,
@@ -44,11 +47,14 @@ export async function runCli(argv = []) {
44
47
  maxOutputTokens,
45
48
  rounds: Number(args.rounds || 3)
46
49
  });
47
- printResults(results);
50
+ printResults(results, theme);
48
51
 
49
52
  const recommended = results.find((item) => item.successRate > 0) || results[0];
50
- const selectedNode = await chooseNode(rl, results, args.node, recommended && recommended.node.id);
53
+ printStep(theme, 4, 5, '选择入口');
54
+ const selectedNode = await chooseNode(rl, results, args.node, recommended && recommended.node.id, theme);
51
55
  assertNodeConfig(selectedNode);
56
+
57
+ printStep(theme, 5, 5, '写入配置');
52
58
  const written = writeClientConfig(client.id, {
53
59
  apiKey,
54
60
  model: model.id,
@@ -58,7 +64,8 @@ export async function runCli(argv = []) {
58
64
  node: selectedNode
59
65
  });
60
66
 
61
- console.log('\n已写入配置:');
67
+ console.log('');
68
+ printStatus(theme, '完成', '配置已写入,旧配置已按时间戳备份。');
62
69
  for (const filePath of written) console.log(`- ${filePath}`);
63
70
  console.log(`入口:${formatNodeLabel(selectedNode)}`);
64
71
  console.log(`地址:${selectedNode.baseUrl}`);
@@ -88,7 +95,7 @@ export function parseArgs(argv) {
88
95
  return result;
89
96
  }
90
97
 
91
- export function printResults(results) {
98
+ export function printResults(results, theme = createTheme()) {
92
99
  const recommended = results.find((item) => item.successRate > 0);
93
100
  const rows = results.map((item) => [
94
101
  `${item.node.label}(${describeSplit(item.node)})`,
@@ -101,22 +108,25 @@ export function printResults(results) {
101
108
  `${Math.round(item.successRate * 100)}%`,
102
109
  formatScore(item.experienceScore),
103
110
  formatMs(item.healthMs),
104
- item === recommended ? '推荐' : '-',
111
+ item === recommended ? theme.ok('推荐') : '-',
105
112
  item.error || '-'
106
113
  ]);
107
114
  const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '尾延迟', '成功率', '体验分', '健康检查', '推荐', '错误'];
108
115
  const widths = header.map((name, i) => Math.max(displayWidth(name), ...rows.map((row) => displayWidth(row[i]))) + 2);
109
116
  console.log('');
110
- console.log(formatRow(header, widths));
111
- for (const row of rows) console.log(formatRow(row, widths));
117
+ console.log(theme.title(formatRow(header, widths)));
118
+ for (const row of rows) {
119
+ const line = formatRow(row, widths);
120
+ console.log(row[10] === '-' ? line : theme.ok(line));
121
+ }
112
122
  }
113
123
 
114
- async function chooseModel(rl, models, preferred) {
124
+ async function chooseModel(rl, models, preferred, theme) {
115
125
  const selected = preferred ? modelByID(models, preferred) : null;
116
126
  if (selected) return selected;
117
- console.log('\n可用模型:');
127
+ printSubTitle(theme, '可用模型');
118
128
  models.forEach((model, index) => {
119
- console.log(`${index + 1}. ${formatModelLabel(model)}`);
129
+ console.log(` ${theme.info(String(index + 1).padStart(2, ' '))} ${formatModelLabel(model)}`);
120
130
  });
121
131
  const answer = await askRequired(rl, '请选择模型序号:');
122
132
  const index = Math.max(1, Math.min(models.length, Number(answer) || 1)) - 1;
@@ -130,25 +140,25 @@ async function askContextLength(rl, model, preferred) {
130
140
  return clamp(Number(answer || max), 1, max);
131
141
  }
132
142
 
133
- export async function chooseNode(rl, results, preferred, recommendedID) {
143
+ export async function chooseNode(rl, results, preferred, recommendedID, theme = createTheme()) {
134
144
  const nodes = results.map((item) => item.node);
135
145
  const preferredNode = nodes.find((node) => node.id === preferred);
136
146
  if (preferredNode) return preferredNode;
137
147
  if (recommendedID) {
138
- console.log(`\n推荐入口:${recommendedID}`);
148
+ printStatus(theme, '推荐', recommendedID);
139
149
  }
140
150
  return (await choose(rl, '请选择最终写入的入口', nodes.map((node) => ({
141
151
  id: node.id,
142
152
  label: formatNodeLabel(node),
143
153
  raw: node
144
- })))).raw;
154
+ })), undefined, theme)).raw;
145
155
  }
146
156
 
147
- async function choose(rl, title, items, preferred) {
157
+ async function choose(rl, title, items, preferred, theme = createTheme()) {
148
158
  const found = items.find((item) => item.id === preferred);
149
159
  if (found) return { ...found, raw: found.raw || found };
150
- console.log(`\n${title}:`);
151
- items.forEach((item, index) => console.log(`${index + 1}. ${item.label}`));
160
+ printSubTitle(theme, title);
161
+ items.forEach((item, index) => console.log(` ${theme.info(String(index + 1).padStart(2, ' '))} ${item.label}`));
152
162
  const answer = await askRequired(rl, '请输入序号:');
153
163
  const index = Math.max(1, Math.min(items.length, Number(answer) || 1)) - 1;
154
164
  const item = items[index];
@@ -172,7 +182,7 @@ function padCell(value, width) {
172
182
  }
173
183
 
174
184
  function displayWidth(value) {
175
- return Array.from(String(value)).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 2 : 1), 0);
185
+ return Array.from(stripAnsi(String(value))).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 2 : 1), 0);
176
186
  }
177
187
 
178
188
  function formatScore(value) {
@@ -200,6 +210,30 @@ export function assertNodeConfig(node) {
200
210
  }
201
211
  }
202
212
 
213
+ function printBanner(theme) {
214
+ console.log(theme.brand('GPTeam API 配置助手'));
215
+ console.log(theme.muted('真实请求测速,自动写入客户端配置,旧配置会先备份。'));
216
+ }
217
+
218
+ function printStep(theme, current, total, title, detail = '') {
219
+ console.log('');
220
+ console.log(`${theme.info(`[${current}/${total}]`)} ${theme.title(title)}`);
221
+ if (detail) printHint(theme, detail);
222
+ }
223
+
224
+ function printSubTitle(theme, title) {
225
+ console.log('');
226
+ console.log(theme.title(title));
227
+ }
228
+
229
+ function printHint(theme, text) {
230
+ console.log(theme.muted(` ${text}`));
231
+ }
232
+
233
+ function printStatus(theme, label, detail) {
234
+ console.log(`${theme.ok(`[${label}]`)} ${detail}`);
235
+ }
236
+
203
237
  function toCamel(value) {
204
238
  return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
205
239
  }
package/lib/help.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.6';
2
+ export const PACKAGE_VERSION = '0.1.8';
3
3
 
4
4
  export function getHelpText() {
5
5
  return [
package/lib/nodes.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const INGRESS_NODES = [
2
2
  {
3
3
  id: 'jp-direct',
4
- label: '日本主机直连',
4
+ label: '直连',
5
5
  region: 'JP',
6
6
  split: false,
7
7
  baseUrl: 'https://api.gpteamservices.com/v1',
@@ -0,0 +1,42 @@
1
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
2
+
3
+ const CODES = {
4
+ reset: '\u001b[0m',
5
+ bold: '\u001b[1m',
6
+ dim: '\u001b[2m',
7
+ cyan: '\u001b[36m',
8
+ green: '\u001b[32m',
9
+ yellow: '\u001b[33m',
10
+ red: '\u001b[31m',
11
+ gray: '\u001b[90m'
12
+ };
13
+
14
+ export function createTheme(options = {}) {
15
+ const enabled = shouldUseColor(options);
16
+ const wrap = (code, value) => enabled ? `${code}${value}${CODES.reset}` : String(value);
17
+ return {
18
+ enabled,
19
+ brand: (value) => wrap(CODES.bold + CODES.cyan, value),
20
+ title: (value) => wrap(CODES.bold, value),
21
+ muted: (value) => wrap(CODES.gray, value),
22
+ info: (value) => wrap(CODES.cyan, value),
23
+ ok: (value) => wrap(CODES.green, value),
24
+ warn: (value) => wrap(CODES.yellow, value),
25
+ error: (value) => wrap(CODES.red, value),
26
+ dim: (value) => wrap(CODES.dim, value)
27
+ };
28
+ }
29
+
30
+ export function shouldUseColor(options = {}) {
31
+ const env = options.env || process.env;
32
+ if (env.NO_COLOR) return false;
33
+ if (env.FORCE_COLOR && env.FORCE_COLOR !== '0') return true;
34
+ const stream = options.stream || process.stdout;
35
+ if (!stream || !stream.isTTY) return false;
36
+ if (env.TERM === 'dumb') return false;
37
+ return true;
38
+ }
39
+
40
+ export function stripAnsi(value) {
41
+ return String(value).replace(ANSI_PATTERN, '');
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
5
5
  "type": "module",
6
6
  "bin": {