replicate-token-verification 1.0.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/replicate-token-verification.js +671 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Duan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# replicate-token-verification
|
|
2
|
+
|
|
3
|
+
一个基于主流 CLI 库的 Node.js 中文命令行工具,用来快速判断给定的 Replicate API Token 当前能不能直接使用。
|
|
4
|
+
|
|
5
|
+
支持两种使用方式:
|
|
6
|
+
|
|
7
|
+
- 交互式启动后手动输入 Token
|
|
8
|
+
- 一键执行,直接通过参数或环境变量传入 Token
|
|
9
|
+
|
|
10
|
+
## 环境要求
|
|
11
|
+
|
|
12
|
+
- Node.js 20.12+
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
在当前目录下直接运行:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node ./bin/replicate-token-verification.js
|
|
20
|
+
node ./bin/replicate-token-verification.js --token r8_xxx
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
或者安装为全局命令:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g .
|
|
27
|
+
rtv
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
发布到 npm 后,也可以直接全局安装:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g replicate-token-verification
|
|
34
|
+
rtv
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 基本用法
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
rtv
|
|
41
|
+
rtv --token <token>
|
|
42
|
+
rtv <token>
|
|
43
|
+
REPLICATE_API_TOKEN=<token> rtv
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 可用选项
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
--json 输出 JSON,适合脚本调用
|
|
50
|
+
--quiet 仅输出最终结果
|
|
51
|
+
--no-loop 交互模式下只校验一次后退出
|
|
52
|
+
--no-model-test 跳过调用测试,只检查认证状态和账号状态
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 交互模式
|
|
56
|
+
|
|
57
|
+
直接运行:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
rtv
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
CLI 会提示你输入 Token,输入后按回车即可开始校验。
|
|
64
|
+
输入过程会明文显示 Token,每次校验完成后会询问是否继续校验下一个 Token。
|
|
65
|
+
|
|
66
|
+
如果你只想交互校验一次:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
rtv --no-loop
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 一键执行
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
rtv --token r8_xxx
|
|
76
|
+
rtv r8_xxx
|
|
77
|
+
REPLICATE_API_TOKEN=r8_xxx rtv
|
|
78
|
+
rtv --token r8_xxx --quiet
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 脚本调用
|
|
82
|
+
|
|
83
|
+
如果你希望给脚本或 CI 使用:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
rtv --token r8_xxx --json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`--json` 模式下会输出这些核心字段:
|
|
90
|
+
|
|
91
|
+
- `usabilityStatus`:当前 Token 是否可直接使用
|
|
92
|
+
- `authStatus`:认证是否通过
|
|
93
|
+
- `accountStatus`:账号状态
|
|
94
|
+
- `modelStatus`:调用测试结果
|
|
95
|
+
|
|
96
|
+
如果你更偏向直接执行,也可以这样:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx replicate-token-verification --token r8_xxx
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
如果你只想检查认证状态和账号状态,不做调用测试:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
rtv --token r8_xxx --no-model-test
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 返回码说明
|
|
109
|
+
|
|
110
|
+
- 当前 Token 可直接使用:退出码 `0`
|
|
111
|
+
- 当前 Token 不可直接使用:退出码 `1`
|
|
112
|
+
- 请求失败、结果未知或缺少 Token:退出码 `1`
|
|
113
|
+
|
|
114
|
+
## 校验原理
|
|
115
|
+
|
|
116
|
+
CLI 默认会执行两段校验:
|
|
117
|
+
|
|
118
|
+
1. 请求 `GET /v1/account` 判断认证是否通过,以及账号当前是否可用
|
|
119
|
+
2. 使用 `replicate/hello-world` 做一次最小调用测试,判断这个 Token 是否具备实际调用模型的能力
|
|
120
|
+
|
|
121
|
+
返回结果会区分:
|
|
122
|
+
|
|
123
|
+
- `可直接使用`:可直接使用 / 不可直接使用 / 大概率可直接使用 / 未知
|
|
124
|
+
- `认证状态`:已通过 / 未通过 / 未知
|
|
125
|
+
- `账号状态`:可用 / 账单受限 / 未知
|
|
126
|
+
- `调用测试`:可调用 / 已接受调用 / 因账号账单问题不可调用 / 无权限调用 / 未执行 / 未知
|
|
127
|
+
|
|
128
|
+
说明:
|
|
129
|
+
|
|
130
|
+
- 默认会进行真实调用测试,这可能触发一次实际的 Replicate 模型请求
|
|
131
|
+
- 如果你不希望执行调用测试,可以加 `--no-model-test`
|
|
132
|
+
- 普通终端输出会使用颜色高亮状态:通过/可用为绿色,未通过/不可用为红色,未知为黄色
|
|
133
|
+
- 返回详情默认按原始返回内容展示;如果接口返回的是 JSON,会以格式化 JSON 形式展示
|
|
134
|
+
|
|
135
|
+
## 发布到 npm
|
|
136
|
+
|
|
137
|
+
这个项目已经具备标准 npm CLI 包结构:
|
|
138
|
+
|
|
139
|
+
- `bin` 字段已配置全局命令 `rtv`
|
|
140
|
+
- `files` 仅发布必要文件
|
|
141
|
+
- `publishConfig.access` 已设为 `public`
|
|
142
|
+
|
|
143
|
+
发布时直接执行:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm publish
|
|
147
|
+
```
|
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
const PROBE_MODEL = "replicate/hello-world";
|
|
9
|
+
const PROBE_INPUT = { text: "codex" };
|
|
10
|
+
const COLOR = {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
dim: "\x1b[2m",
|
|
13
|
+
red: "\x1b[31m",
|
|
14
|
+
green: "\x1b[32m",
|
|
15
|
+
yellow: "\x1b[33m",
|
|
16
|
+
cyan: "\x1b[36m",
|
|
17
|
+
bold: "\x1b[1m"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name("rtv")
|
|
22
|
+
.description("检查 Replicate API Token 当前是否可直接使用")
|
|
23
|
+
.argument("[token]", "要检查的 Replicate API Token")
|
|
24
|
+
.option("-t, --token <token>", "要检查的 Replicate API Token")
|
|
25
|
+
.option("--json", "输出 JSON 结果,便于脚本调用")
|
|
26
|
+
.option("-q, --quiet", "仅输出最终结果")
|
|
27
|
+
.option("--no-loop", "交互模式下只校验一次后退出")
|
|
28
|
+
.option("--no-model-test", "跳过调用测试,只检查认证状态和账号状态")
|
|
29
|
+
.helpOption("-h, --help", "显示帮助信息")
|
|
30
|
+
.showHelpAfterError("(提示:不传参数直接运行可进入交互模式。)")
|
|
31
|
+
.version("1.0.0", "-V, --version", "显示版本号");
|
|
32
|
+
|
|
33
|
+
function supportsColor() {
|
|
34
|
+
return Boolean(process.stdout.isTTY && process.env.NO_COLOR === undefined);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function paint(text, color) {
|
|
38
|
+
if (!supportsColor()) {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `${color}${text}${COLOR.reset}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatStatusLabel(status) {
|
|
46
|
+
if (status === "valid") {
|
|
47
|
+
return paint("有效", COLOR.green);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (status === "invalid") {
|
|
51
|
+
return paint("无效", COLOR.red);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return paint("失败", COLOR.yellow);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function colorizeValue(value, tone) {
|
|
58
|
+
if (tone === "good") {
|
|
59
|
+
return paint(value, COLOR.green);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (tone === "bad") {
|
|
63
|
+
return paint(value, COLOR.red);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (tone === "warn") {
|
|
67
|
+
return paint(value, COLOR.yellow);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatUsabilitySummary(classification) {
|
|
74
|
+
if (classification.usabilityStatus === "usable" || classification.usabilityStatus === "probably_usable") {
|
|
75
|
+
return colorizeValue(classification.usabilitySummary, "good");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (classification.usabilityStatus === "unusable") {
|
|
79
|
+
return colorizeValue(classification.usabilitySummary, "bad");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return colorizeValue(classification.usabilitySummary, "warn");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatAuthSummary(classification) {
|
|
86
|
+
if (classification.authStatus === "passed") {
|
|
87
|
+
return colorizeValue(classification.authSummary, "good");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (classification.authStatus === "failed") {
|
|
91
|
+
return colorizeValue(classification.authSummary, "bad");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return colorizeValue(classification.authSummary, "warn");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatAccountSummary(classification) {
|
|
98
|
+
if (classification.accountStatus === "active") {
|
|
99
|
+
return colorizeValue(classification.accountSummary, "good");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (classification.accountStatus === "billing_blocked") {
|
|
103
|
+
return colorizeValue(classification.accountSummary, "bad");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return colorizeValue(classification.accountSummary, "warn");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatModelSummary(classification) {
|
|
110
|
+
if (classification.modelStatus === "callable" || classification.modelStatus === "accepted") {
|
|
111
|
+
return colorizeValue(classification.modelSummary, "good");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (classification.modelStatus === "unauthorized" || classification.modelStatus === "billing_blocked" || classification.modelStatus === "failed") {
|
|
115
|
+
return colorizeValue(classification.modelSummary, "bad");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return colorizeValue(classification.modelSummary, "warn");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function verifyToken(token) {
|
|
122
|
+
const response = await fetch("https://api.replicate.com/v1/account", {
|
|
123
|
+
method: "GET",
|
|
124
|
+
headers: {
|
|
125
|
+
Authorization: `Bearer ${token}`,
|
|
126
|
+
Accept: "application/json"
|
|
127
|
+
},
|
|
128
|
+
signal: AbortSignal.timeout(10000)
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const contentType = response.headers.get("content-type") || "";
|
|
132
|
+
let payload = null;
|
|
133
|
+
|
|
134
|
+
if (contentType.includes("application/json")) {
|
|
135
|
+
payload = await response.json().catch(() => null);
|
|
136
|
+
} else {
|
|
137
|
+
payload = await response.text().catch(() => "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: response.ok,
|
|
142
|
+
status: response.status,
|
|
143
|
+
payload
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function verifyModelInvocation(token) {
|
|
148
|
+
const response = await fetch(`https://api.replicate.com/v1/models/${PROBE_MODEL}/predictions`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
Authorization: `Bearer ${token}`,
|
|
152
|
+
Accept: "application/json",
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
Prefer: "wait"
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({ input: PROBE_INPUT }),
|
|
157
|
+
signal: AbortSignal.timeout(20000)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const contentType = response.headers.get("content-type") || "";
|
|
161
|
+
let payload = null;
|
|
162
|
+
|
|
163
|
+
if (contentType.includes("application/json")) {
|
|
164
|
+
payload = await response.json().catch(() => null);
|
|
165
|
+
} else {
|
|
166
|
+
payload = await response.text().catch(() => "");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
ok: response.ok,
|
|
171
|
+
status: response.status,
|
|
172
|
+
payload
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractErrorMessage(payload) {
|
|
177
|
+
const parsedPayload = parsePayload(payload);
|
|
178
|
+
|
|
179
|
+
if (!parsedPayload) return null;
|
|
180
|
+
if (typeof parsedPayload === "string" && parsedPayload.trim()) return parsedPayload.trim();
|
|
181
|
+
if (typeof parsedPayload === "object") {
|
|
182
|
+
if (typeof parsedPayload.detail === "string" && parsedPayload.detail.trim()) return parsedPayload.detail.trim();
|
|
183
|
+
if (typeof parsedPayload.error === "string" && parsedPayload.error.trim()) return parsedPayload.error.trim();
|
|
184
|
+
if (typeof parsedPayload.title === "string" && parsedPayload.title.trim()) return parsedPayload.title.trim();
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parsePayload(payload) {
|
|
190
|
+
if (typeof payload !== "string") {
|
|
191
|
+
return payload;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const trimmed = payload.trim();
|
|
195
|
+
if (!trimmed) {
|
|
196
|
+
return payload;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(trimmed);
|
|
202
|
+
} catch {
|
|
203
|
+
return payload;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return payload;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizePayload(payload) {
|
|
211
|
+
const parsedPayload = parsePayload(payload);
|
|
212
|
+
|
|
213
|
+
if (!parsedPayload || typeof parsedPayload !== "object") {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
title: typeof parsedPayload.title === "string" ? parsedPayload.title.trim() : null,
|
|
219
|
+
detail: typeof parsedPayload.detail === "string" ? parsedPayload.detail.trim() : null,
|
|
220
|
+
status: typeof parsedPayload.status === "number" ? parsedPayload.status : null
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getPredictionStatus(payload) {
|
|
225
|
+
const parsedPayload = parsePayload(payload);
|
|
226
|
+
|
|
227
|
+
if (!parsedPayload || typeof parsedPayload !== "object") {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return typeof parsedPayload.status === "string" ? parsedPayload.status.trim() : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getPredictionError(payload) {
|
|
235
|
+
const parsedPayload = parsePayload(payload);
|
|
236
|
+
|
|
237
|
+
if (!parsedPayload || typeof parsedPayload !== "object") {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return typeof parsedPayload.error === "string" && parsedPayload.error.trim() ? parsedPayload.error.trim() : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function formatPayloadForDisplay(payload) {
|
|
245
|
+
const parsedPayload = parsePayload(payload);
|
|
246
|
+
|
|
247
|
+
if (parsedPayload == null) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (typeof parsedPayload === "string") {
|
|
252
|
+
return parsedPayload.trim() || null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
return JSON.stringify(parsedPayload, null, 2);
|
|
257
|
+
} catch {
|
|
258
|
+
return String(parsedPayload);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function classifyAccountResult(result) {
|
|
263
|
+
if (!result) {
|
|
264
|
+
return {
|
|
265
|
+
authStatus: "unknown",
|
|
266
|
+
authSummary: "未知",
|
|
267
|
+
accountStatus: "unknown",
|
|
268
|
+
accountSummary: "未知"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (result.ok) {
|
|
273
|
+
return {
|
|
274
|
+
authStatus: "passed",
|
|
275
|
+
authSummary: "已通过",
|
|
276
|
+
accountStatus: "active",
|
|
277
|
+
accountSummary: "可用"
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (result.status === 401 || result.status === 403) {
|
|
282
|
+
return {
|
|
283
|
+
authStatus: "failed",
|
|
284
|
+
authSummary: "未通过",
|
|
285
|
+
accountStatus: "unknown",
|
|
286
|
+
accountSummary: "未知"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (result.status === 402) {
|
|
291
|
+
return {
|
|
292
|
+
authStatus: "passed",
|
|
293
|
+
authSummary: "已通过",
|
|
294
|
+
accountStatus: "billing_blocked",
|
|
295
|
+
accountSummary: "账单受限"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
authStatus: "unknown",
|
|
301
|
+
authSummary: "未知",
|
|
302
|
+
accountStatus: "unknown",
|
|
303
|
+
accountSummary: "未知"
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function classifyModelResult(result) {
|
|
308
|
+
if (!result) {
|
|
309
|
+
return {
|
|
310
|
+
modelStatus: "unknown",
|
|
311
|
+
modelSummary: "未知"
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const predictionStatus = getPredictionStatus(result.payload);
|
|
316
|
+
const predictionError = getPredictionError(result.payload);
|
|
317
|
+
|
|
318
|
+
if (result.ok && (predictionStatus === "succeeded" || predictionStatus === "successful")) {
|
|
319
|
+
return {
|
|
320
|
+
modelStatus: "callable",
|
|
321
|
+
modelSummary: "可调用"
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (result.ok && (predictionStatus === "starting" || predictionStatus === "processing")) {
|
|
326
|
+
return {
|
|
327
|
+
modelStatus: "accepted",
|
|
328
|
+
modelSummary: "已接受调用"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (result.status === 402) {
|
|
333
|
+
return {
|
|
334
|
+
modelStatus: "billing_blocked",
|
|
335
|
+
modelSummary: "因账号账单问题不可调用"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (result.status === 401 || result.status === 403) {
|
|
340
|
+
return {
|
|
341
|
+
modelStatus: "unauthorized",
|
|
342
|
+
modelSummary: "无权限调用"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (predictionError) {
|
|
347
|
+
return {
|
|
348
|
+
modelStatus: "failed",
|
|
349
|
+
modelSummary: "调用失败"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
modelStatus: "unknown",
|
|
355
|
+
modelSummary: "未知"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function classifyUsability(report) {
|
|
360
|
+
const accountClassification = classifyAccountResult(report.account);
|
|
361
|
+
const modelClassification = report.model ? classifyModelResult(report.model) : null;
|
|
362
|
+
|
|
363
|
+
if (report.accountError || report.modelError) {
|
|
364
|
+
return {
|
|
365
|
+
usabilityStatus: "unknown",
|
|
366
|
+
usabilitySummary: "未知"
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (accountClassification.authStatus === "failed") {
|
|
371
|
+
return {
|
|
372
|
+
usabilityStatus: "unusable",
|
|
373
|
+
usabilitySummary: "不可直接使用"
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (accountClassification.accountStatus === "billing_blocked") {
|
|
378
|
+
return {
|
|
379
|
+
usabilityStatus: "unusable",
|
|
380
|
+
usabilitySummary: "不可直接使用"
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (report.modelSkipped) {
|
|
385
|
+
if (accountClassification.authStatus === "passed" && accountClassification.accountStatus === "active") {
|
|
386
|
+
return {
|
|
387
|
+
usabilityStatus: "probably_usable",
|
|
388
|
+
usabilitySummary: "大概率可直接使用"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
usabilityStatus: "unknown",
|
|
394
|
+
usabilitySummary: "未知"
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (modelClassification && (modelClassification.modelStatus === "callable" || modelClassification.modelStatus === "accepted")) {
|
|
399
|
+
return {
|
|
400
|
+
usabilityStatus: "usable",
|
|
401
|
+
usabilitySummary: "可直接使用"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (modelClassification && (modelClassification.modelStatus === "unauthorized" || modelClassification.modelStatus === "billing_blocked" || modelClassification.modelStatus === "failed")) {
|
|
406
|
+
return {
|
|
407
|
+
usabilityStatus: "unusable",
|
|
408
|
+
usabilitySummary: "不可直接使用"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
usabilityStatus: "unknown",
|
|
414
|
+
usabilitySummary: "未知"
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function printStructuredPayload(payload, status) {
|
|
419
|
+
const formattedPayload = formatPayloadForDisplay(payload);
|
|
420
|
+
|
|
421
|
+
if (formattedPayload) {
|
|
422
|
+
console.log(" 详情:");
|
|
423
|
+
console.log(paint(formattedPayload, COLOR.dim));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function printRequestError(label, error) {
|
|
428
|
+
console.log(`${label}:未知`);
|
|
429
|
+
console.log(` 请求异常:${paint(error instanceof Error ? error.message : String(error), COLOR.dim)}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function printAccountDetails(result, error) {
|
|
433
|
+
console.log("认证与账号");
|
|
434
|
+
if (error) {
|
|
435
|
+
printRequestError("认证状态", error);
|
|
436
|
+
console.log("账号状态:未知");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const classification = classifyAccountResult(result);
|
|
441
|
+
|
|
442
|
+
console.log(`认证状态:${formatAuthSummary(classification)}`);
|
|
443
|
+
console.log(`账号状态:${formatAccountSummary(classification)}`);
|
|
444
|
+
|
|
445
|
+
if (!result.ok) {
|
|
446
|
+
printStructuredPayload(result.payload, result.status);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function printModelDetails(result, error) {
|
|
451
|
+
console.log("调用测试");
|
|
452
|
+
if (error) {
|
|
453
|
+
printRequestError("调用测试", error);
|
|
454
|
+
console.log(`探测模型:${PROBE_MODEL}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const classification = classifyModelResult(result);
|
|
459
|
+
const message = extractErrorMessage(result.payload);
|
|
460
|
+
const predictionStatus = getPredictionStatus(result.payload);
|
|
461
|
+
const predictionError = getPredictionError(result.payload);
|
|
462
|
+
|
|
463
|
+
console.log(`调用测试:${formatModelSummary(classification)}`);
|
|
464
|
+
console.log(`探测模型:${PROBE_MODEL}`);
|
|
465
|
+
|
|
466
|
+
if (predictionStatus) {
|
|
467
|
+
console.log(`任务状态:${predictionStatus}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (predictionError) {
|
|
471
|
+
console.log(` 模型错误:${paint(predictionError, COLOR.dim)}`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!result.ok || classification.modelStatus !== "callable") {
|
|
476
|
+
printStructuredPayload(result.payload, result.status);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (message && predictionStatus !== "succeeded" && predictionStatus !== "successful") {
|
|
481
|
+
console.log(` 返回:${paint(message, COLOR.dim)}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function printSummary(report, outputJson) {
|
|
486
|
+
const accountClassification = classifyAccountResult(report.account);
|
|
487
|
+
const modelClassification = report.model ? classifyModelResult(report.model) : null;
|
|
488
|
+
const usabilityClassification = classifyUsability(report);
|
|
489
|
+
const accountMessage = report.account ? extractErrorMessage(report.account.payload) : null;
|
|
490
|
+
const normalized = report.account ? normalizePayload(report.account.payload) : null;
|
|
491
|
+
|
|
492
|
+
if (outputJson) {
|
|
493
|
+
console.log(JSON.stringify({
|
|
494
|
+
usabilityStatus: usabilityClassification.usabilityStatus,
|
|
495
|
+
authStatus: accountClassification.authStatus,
|
|
496
|
+
accountStatus: accountClassification.accountStatus,
|
|
497
|
+
modelStatus: report.modelSkipped ? "skipped" : report.modelError ? "unknown" : modelClassification?.modelStatus || "not_run",
|
|
498
|
+
probeModel: report.modelSkipped ? null : PROBE_MODEL,
|
|
499
|
+
accountHttpStatus: report.account?.status || null,
|
|
500
|
+
modelHttpStatus: report.model?.status || null,
|
|
501
|
+
accountRequestError: report.accountError ? (report.accountError instanceof Error ? report.accountError.message : String(report.accountError)) : null,
|
|
502
|
+
modelRequestError: report.modelError ? (report.modelError instanceof Error ? report.modelError.message : String(report.modelError)) : null,
|
|
503
|
+
accountMessage,
|
|
504
|
+
accountTitle: normalized?.title || null,
|
|
505
|
+
accountDetail: normalized?.detail || null,
|
|
506
|
+
modelPredictionStatus: report.model ? getPredictionStatus(report.model.payload) : null,
|
|
507
|
+
modelError: report.model ? getPredictionError(report.model.payload) : null
|
|
508
|
+
}, null, 2));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log(paint("校验摘要", COLOR.bold));
|
|
513
|
+
console.log(`${formatStatusLabel(usabilityClassification.usabilityStatus === "usable" || usabilityClassification.usabilityStatus === "probably_usable" ? "valid" : usabilityClassification.usabilityStatus === "unusable" ? "invalid" : "unknown")}:当前 Token ${formatUsabilitySummary(usabilityClassification)}`);
|
|
514
|
+
console.log("");
|
|
515
|
+
printAccountDetails(report.account, report.accountError);
|
|
516
|
+
|
|
517
|
+
if (report.modelSkipped) {
|
|
518
|
+
console.log("");
|
|
519
|
+
console.log("调用测试:未执行");
|
|
520
|
+
} else if (report.model) {
|
|
521
|
+
console.log("");
|
|
522
|
+
printModelDetails(report.model, report.modelError);
|
|
523
|
+
} else if (report.modelError) {
|
|
524
|
+
console.log("");
|
|
525
|
+
printModelDetails(null, report.modelError);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function resolveToken(positionalToken, optionsToken) {
|
|
530
|
+
return optionsToken || positionalToken || process.env.REPLICATE_API_TOKEN || null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function exitCodeForReport(report) {
|
|
534
|
+
const accountClassification = classifyAccountResult(report.account);
|
|
535
|
+
const modelClassification = report.model ? classifyModelResult(report.model) : null;
|
|
536
|
+
|
|
537
|
+
if (report.accountError || report.modelError) {
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (accountClassification.authStatus !== "passed") {
|
|
542
|
+
return 1;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (accountClassification.accountStatus !== "active") {
|
|
546
|
+
return 1;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!report.modelSkipped && modelClassification && modelClassification.modelStatus !== "callable" && modelClassification.modelStatus !== "accepted") {
|
|
550
|
+
return 1;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (report.modelSkipped || !report.model) {
|
|
554
|
+
return 0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function runVerification(token, options) {
|
|
561
|
+
try {
|
|
562
|
+
const modelSkipped = Boolean(options.modelTest === false);
|
|
563
|
+
const accountPromise = verifyToken(token);
|
|
564
|
+
const modelPromise = modelSkipped ? Promise.resolve(null) : verifyModelInvocation(token);
|
|
565
|
+
const [accountResult, modelResult] = await Promise.allSettled([accountPromise, modelPromise]);
|
|
566
|
+
const report = {
|
|
567
|
+
account: accountResult.status === "fulfilled" ? accountResult.value : null,
|
|
568
|
+
accountError: accountResult.status === "rejected" ? accountResult.reason : null,
|
|
569
|
+
model: modelResult.status === "fulfilled" ? modelResult.value : null,
|
|
570
|
+
modelError: modelResult.status === "rejected" ? modelResult.reason : null,
|
|
571
|
+
modelSkipped
|
|
572
|
+
};
|
|
573
|
+
printSummary(report, options.json);
|
|
574
|
+
return exitCodeForReport(report);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (options.json) {
|
|
577
|
+
console.log(JSON.stringify({
|
|
578
|
+
status: "failed",
|
|
579
|
+
message: error instanceof Error ? error.message : String(error)
|
|
580
|
+
}, null, 2));
|
|
581
|
+
} else {
|
|
582
|
+
console.error(`${formatStatusLabel("unknown")}:无法完成 Token 校验。`);
|
|
583
|
+
console.error(paint(error instanceof Error ? error.message : String(error), COLOR.dim));
|
|
584
|
+
}
|
|
585
|
+
return 1;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function promptForToken() {
|
|
590
|
+
return input({
|
|
591
|
+
message: "请输入你的 Replicate API Token",
|
|
592
|
+
validate(value) {
|
|
593
|
+
return value.trim() ? true : "Token 不能为空。";
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function runInteractive(options) {
|
|
599
|
+
if (!options.quiet) {
|
|
600
|
+
console.log(paint("Replicate Token 校验工具", COLOR.bold));
|
|
601
|
+
console.log(paint("已进入交互模式。", COLOR.cyan));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
while (true) {
|
|
605
|
+
const token = (await promptForToken()).trim();
|
|
606
|
+
if (!options.json && !options.quiet) {
|
|
607
|
+
console.log(paint("正在校验 Token...", COLOR.dim));
|
|
608
|
+
if (options.modelTest !== false) {
|
|
609
|
+
console.log(paint(`将附带执行 ${PROBE_MODEL} 调用测试。`, COLOR.dim));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const exitCode = await runVerification(token, options);
|
|
614
|
+
|
|
615
|
+
if (exitCode === 0) {
|
|
616
|
+
process.exitCode = 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (options.loop === false) {
|
|
620
|
+
return exitCode;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const shouldContinue = await confirm({
|
|
624
|
+
message: "是否继续校验下一个 Token?",
|
|
625
|
+
default: false
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (!shouldContinue) {
|
|
629
|
+
return exitCode;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function main() {
|
|
635
|
+
program.parse(process.argv);
|
|
636
|
+
|
|
637
|
+
const options = program.opts();
|
|
638
|
+
const positionalToken = program.args[0];
|
|
639
|
+
const token = resolveToken(positionalToken, options.token);
|
|
640
|
+
|
|
641
|
+
if (token) {
|
|
642
|
+
if (!options.json && !options.quiet) {
|
|
643
|
+
console.log(paint("正在校验 Token...", COLOR.dim));
|
|
644
|
+
if (options.modelTest !== false) {
|
|
645
|
+
console.log(paint(`将附带执行 ${PROBE_MODEL} 调用测试。`, COLOR.dim));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
process.exitCode = await runVerification(token, options);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
653
|
+
console.error("缺少 Replicate API Token。请通过参数、选项或 REPLICATE_API_TOKEN 环境变量传入。");
|
|
654
|
+
process.exitCode = 1;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
process.exitCode = await runInteractive(options);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (error && typeof error === "object" && "name" in error && error.name === "ExitPromptError") {
|
|
662
|
+
console.error("\n已取消。");
|
|
663
|
+
process.exitCode = 130;
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
await main();
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "replicate-token-verification",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to check whether a Replicate API token can be directly used",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Duan <hello@duanjun.net>",
|
|
7
|
+
"homepage": "https://github.com/lenuxo/replicate-token-verification#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/lenuxo/replicate-token-verification/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/lenuxo/replicate-token-verification.git"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"replicate-token-verification": "./bin/replicate-token-verification.js",
|
|
17
|
+
"rtv": "./bin/replicate-token-verification.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node ./bin/replicate-token-verification.js",
|
|
26
|
+
"check": "node ./bin/replicate-token-verification.js --help",
|
|
27
|
+
"prepublishOnly": "npm run check"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"replicate",
|
|
31
|
+
"cli",
|
|
32
|
+
"token",
|
|
33
|
+
"verification",
|
|
34
|
+
"api",
|
|
35
|
+
"npm",
|
|
36
|
+
"terminal"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.12"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@inquirer/prompts": "^8.3.2",
|
|
47
|
+
"commander": "^14.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|