gpteam 0.1.5 → 0.1.7
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 +62 -28
- package/lib/help.js +1 -1
- package/lib/models.js +1 -1
- package/lib/terminal.js +42 -0
- package/package.json +1 -1
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
|
-
|
|
23
|
-
console.log('接下来会用你的 key 跑几次真实请求测速,然后把选中的入口写进客户端配置。原来的配置会先备份。\n');
|
|
24
|
+
printBanner(theme);
|
|
24
25
|
|
|
25
26
|
const apiKey = args.key || args.apiKey || await askRequired(rl, '请输入 API key:');
|
|
26
|
-
|
|
27
|
+
printStep(theme, 1, 5, '校验 API key', '请求 /v1/models,校验通过后才继续。');
|
|
27
28
|
const validation = await validateApiKey(INGRESS_NODES, apiKey);
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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('
|
|
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)
|
|
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
|
-
|
|
127
|
+
printSubTitle(theme, '可用模型');
|
|
118
128
|
models.forEach((model, index) => {
|
|
119
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
items.forEach((item, index) => console.log(
|
|
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
package/lib/models.js
CHANGED
package/lib/terminal.js
ADDED
|
@@ -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
|
+
}
|