ai-zero-token 2.0.1 → 2.0.3
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/CHANGELOG.md +20 -0
- package/README.md +13 -5
- package/README.zh-CN.md +13 -5
- package/admin-ui/dist/assets/InfoRow-0ULI9iI3.js +1 -0
- package/admin-ui/dist/assets/accounts-DymL4WIa.js +2 -0
- package/admin-ui/dist/assets/activity-D21-Xrc4.js +1 -0
- package/admin-ui/dist/assets/app-mark-nsRs4vo7.svg +8 -0
- package/admin-ui/dist/assets/circle-check-ZYtn9GqY.js +1 -0
- package/admin-ui/dist/assets/clock-3-BzDANsVk.js +1 -0
- package/admin-ui/dist/assets/docs-BRNWAMPw.css +1 -0
- package/admin-ui/dist/assets/docs-DtO-AOWU.js +1735 -0
- package/admin-ui/dist/assets/earth-DFdZaQIi.js +1 -0
- package/admin-ui/dist/assets/image-bed-yIVQ4dKs.js +1 -0
- package/admin-ui/dist/assets/index-By4r-wy3.css +1 -0
- package/admin-ui/dist/assets/index-DRe-tByu.js +10 -0
- package/admin-ui/dist/assets/jsx-runtime-DqpGtLhh.js +1 -0
- package/admin-ui/dist/assets/launch-CQXYrl-h.js +1 -0
- package/admin-ui/dist/assets/logs-awABDg1C.js +1 -0
- package/admin-ui/dist/assets/network-detect-sSrnwZqf.js +1 -0
- package/admin-ui/dist/assets/overview-BbSON0Jl.js +1 -0
- package/admin-ui/dist/assets/profiles-DMOjJORP.js +1 -0
- package/admin-ui/dist/assets/refresh-cw-CAAH2rqe.js +1 -0
- package/admin-ui/dist/assets/search-B2hz41D3.js +1 -0
- package/admin-ui/dist/assets/server-BrjJPb9D.js +1 -0
- package/admin-ui/dist/assets/settings-DvRiHS7i.js +1 -0
- package/admin-ui/dist/assets/tester-CftPgRE9.js +3 -0
- package/admin-ui/dist/assets/upload-CwXb7Q1b.js +1 -0
- package/admin-ui/dist/assets/zap-B4_oDbCp.js +1 -0
- package/admin-ui/dist/index.html +5 -3
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon.svg +8 -0
- package/build/mac-install-guide.txt +25 -0
- package/dist/core/context.js +3 -0
- package/dist/core/providers/http-client.js +11 -4
- package/dist/core/services/auth-service.js +88 -12
- package/dist/core/services/config-service.js +87 -23
- package/dist/core/services/github-image-bed-service.js +264 -0
- package/dist/core/services/network-detect-service.js +189 -74
- package/dist/core/services/version-service.js +1 -1
- package/dist/core/store/github-image-bed-history-store.js +68 -0
- package/dist/core/store/github-image-bed-store.js +52 -0
- package/dist/core/store/profile-store.js +73 -32
- package/dist/core/store/settings-store.js +14 -0
- package/dist/desktop/main.js +158 -6
- package/dist/server/app.js +168 -26
- package/dist/server/index.js +41 -15
- package/docs/DESKTOP_RELEASE.md +35 -3
- package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +2 -2
- package/package.json +34 -2
- package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +0 -9
- package/admin-ui/dist/assets/index-DNzR8XR7.css +0 -1
- package/admin-ui/dist/assets/index-DZMegNPs.js +0 -1745
- package/dist/server/admin-page.js +0 -4586
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { loadGithubImageBedStore, updateGithubImageBedToken, clearGithubImageBedStore } from "../store/github-image-bed-store.js";
|
|
4
|
+
import {
|
|
5
|
+
appendGithubImageBedHistory,
|
|
6
|
+
clearGithubImageBedHistory,
|
|
7
|
+
findGithubImageBedHistoryItem,
|
|
8
|
+
listGithubImageBedHistory,
|
|
9
|
+
removeGithubImageBedHistoryItem
|
|
10
|
+
} from "../store/github-image-bed-history-store.js";
|
|
11
|
+
const DEFAULT_REPOSITORY = "azt-img-bed";
|
|
12
|
+
const DEFAULT_PREFIX = "images";
|
|
13
|
+
const GITHUB_API_BASE = "https://api.github.com";
|
|
14
|
+
const GITHUB_API_VERSION = "2022-11-28";
|
|
15
|
+
const USER_AGENT = "AI Zero Token";
|
|
16
|
+
function serviceError(message, statusCode = 400) {
|
|
17
|
+
const error = new Error(message);
|
|
18
|
+
error.statusCode = statusCode;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
function encodePath(pathValue) {
|
|
22
|
+
return pathValue.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
23
|
+
}
|
|
24
|
+
function sanitizeFileName(fileName) {
|
|
25
|
+
const trimmed = fileName.trim() || "image.png";
|
|
26
|
+
const normalized = trimmed.normalize("NFKD").replace(/[^\w.\-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
27
|
+
return normalized || "image.png";
|
|
28
|
+
}
|
|
29
|
+
function makeImagePath(fileName) {
|
|
30
|
+
const now = /* @__PURE__ */ new Date();
|
|
31
|
+
const yyyy = String(now.getFullYear());
|
|
32
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
33
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
34
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
35
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
36
|
+
const safeName = sanitizeFileName(fileName);
|
|
37
|
+
return `${DEFAULT_PREFIX}/${yyyy}/${mm}/${dd}/${stamp}-${suffix}-${safeName}`;
|
|
38
|
+
}
|
|
39
|
+
function parseDataUrl(dataUrl) {
|
|
40
|
+
const trimmed = dataUrl.trim();
|
|
41
|
+
const match = /^data:([^;]+);base64,(.+)$/i.exec(trimmed);
|
|
42
|
+
if (!match) {
|
|
43
|
+
throw serviceError("\u56FE\u7247\u5185\u5BB9\u5FC5\u987B\u662F data URL\u3002");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
mimeType: match[1] ?? "application/octet-stream",
|
|
47
|
+
bytes: Buffer.from(match[2] ?? "", "base64"),
|
|
48
|
+
base64: match[2] ?? ""
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function buildRawUrl(owner, repository, branch, objectPath) {
|
|
52
|
+
return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/${encodeURIComponent(branch)}/${encodePath(objectPath)}`;
|
|
53
|
+
}
|
|
54
|
+
async function requestGithubJson(token, path, options) {
|
|
55
|
+
const response = await fetch(`${GITHUB_API_BASE}${path}`, {
|
|
56
|
+
method: options?.method ?? "GET",
|
|
57
|
+
headers: {
|
|
58
|
+
"Authorization": `Bearer ${token}`,
|
|
59
|
+
"Accept": "application/vnd.github+json",
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"User-Agent": USER_AGENT,
|
|
62
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
63
|
+
},
|
|
64
|
+
body: typeof options?.body === "undefined" ? void 0 : JSON.stringify(options.body)
|
|
65
|
+
});
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
let parsed = null;
|
|
68
|
+
if (text) {
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(text);
|
|
71
|
+
} catch {
|
|
72
|
+
parsed = text;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const message = typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? String(parsed.message) : typeof parsed === "string" ? parsed : `${response.status} ${response.statusText}`;
|
|
77
|
+
throw serviceError(`GitHub API \u8C03\u7528\u5931\u8D25: ${message}`, response.status);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
status: response.status,
|
|
81
|
+
data: parsed
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function readConfigSummary(hasToken) {
|
|
85
|
+
return {
|
|
86
|
+
hasToken,
|
|
87
|
+
repository: DEFAULT_REPOSITORY,
|
|
88
|
+
pathPrefix: DEFAULT_PREFIX,
|
|
89
|
+
defaultBranch: "auto"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
class GithubImageBedService {
|
|
93
|
+
async getConfig() {
|
|
94
|
+
const store = await loadGithubImageBedStore();
|
|
95
|
+
return readConfigSummary(Boolean(store.github.token));
|
|
96
|
+
}
|
|
97
|
+
async saveToken(token) {
|
|
98
|
+
const trimmed = token.trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
throw serviceError("\u8BF7\u5148\u586B\u5199 GitHub token\u3002");
|
|
101
|
+
}
|
|
102
|
+
await updateGithubImageBedToken(trimmed);
|
|
103
|
+
return this.getConfig();
|
|
104
|
+
}
|
|
105
|
+
async clearToken() {
|
|
106
|
+
await clearGithubImageBedStore();
|
|
107
|
+
return this.getConfig();
|
|
108
|
+
}
|
|
109
|
+
async testConnection() {
|
|
110
|
+
const { token } = await this.requireToken();
|
|
111
|
+
const owner = await this.getCurrentLogin(token);
|
|
112
|
+
const repo = await this.ensureRepository(token, owner);
|
|
113
|
+
const branch = repo.default_branch?.trim() || "main";
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
owner,
|
|
117
|
+
repository: DEFAULT_REPOSITORY,
|
|
118
|
+
repositoryUrl: repo.html_url || `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(DEFAULT_REPOSITORY)}`,
|
|
119
|
+
branch,
|
|
120
|
+
publicUrl: repo.html_url || `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(DEFAULT_REPOSITORY)}`,
|
|
121
|
+
createdRepository: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async uploadImage(params) {
|
|
125
|
+
const { token } = await this.requireToken();
|
|
126
|
+
const owner = await this.getCurrentLogin(token);
|
|
127
|
+
const repo = await this.ensureRepository(token, owner);
|
|
128
|
+
const branch = repo.default_branch?.trim() || "main";
|
|
129
|
+
const parsed = parseDataUrl(params.dataUrl);
|
|
130
|
+
const objectPath = makeImagePath(params.filename);
|
|
131
|
+
const response = await requestGithubJson(token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(DEFAULT_REPOSITORY)}/contents/${encodePath(objectPath)}`, {
|
|
132
|
+
method: "PUT",
|
|
133
|
+
body: {
|
|
134
|
+
message: `upload ${params.filename}`,
|
|
135
|
+
content: parsed.base64,
|
|
136
|
+
branch
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const content = response.data.content;
|
|
140
|
+
const downloadUrl = content?.download_url || buildRawUrl(owner, DEFAULT_REPOSITORY, branch, objectPath);
|
|
141
|
+
const htmlUrl = content?.html_url || `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(DEFAULT_REPOSITORY)}/blob/${encodeURIComponent(branch)}/${encodePath(objectPath)}`;
|
|
142
|
+
return {
|
|
143
|
+
filename: params.filename,
|
|
144
|
+
path: objectPath,
|
|
145
|
+
url: downloadUrl,
|
|
146
|
+
htmlUrl,
|
|
147
|
+
downloadUrl,
|
|
148
|
+
owner,
|
|
149
|
+
repository: DEFAULT_REPOSITORY,
|
|
150
|
+
branch,
|
|
151
|
+
size: parsed.bytes.byteLength,
|
|
152
|
+
mimeType: parsed.mimeType,
|
|
153
|
+
sha: content?.sha || response.data.commit?.sha
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async listHistory(limit = 50) {
|
|
157
|
+
return {
|
|
158
|
+
items: await listGithubImageBedHistory(limit)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async clearHistory() {
|
|
162
|
+
await clearGithubImageBedHistory();
|
|
163
|
+
return { items: [] };
|
|
164
|
+
}
|
|
165
|
+
async deleteHistoryItem(id) {
|
|
166
|
+
const item = await findGithubImageBedHistoryItem(id);
|
|
167
|
+
if (!item) {
|
|
168
|
+
throw serviceError("\u672A\u627E\u5230\u8FD9\u6761\u4E0A\u4F20\u5386\u53F2\u3002", 404);
|
|
169
|
+
}
|
|
170
|
+
const { token } = await this.requireToken();
|
|
171
|
+
await this.deleteRemoteFile(token, item);
|
|
172
|
+
await removeGithubImageBedHistoryItem(id);
|
|
173
|
+
return this.listHistory(100);
|
|
174
|
+
}
|
|
175
|
+
async rememberUpload(result) {
|
|
176
|
+
await appendGithubImageBedHistory({
|
|
177
|
+
id: crypto.randomUUID(),
|
|
178
|
+
createdAt: Date.now(),
|
|
179
|
+
filename: result.filename,
|
|
180
|
+
path: result.path,
|
|
181
|
+
url: result.url,
|
|
182
|
+
htmlUrl: result.htmlUrl,
|
|
183
|
+
downloadUrl: result.downloadUrl,
|
|
184
|
+
owner: result.owner,
|
|
185
|
+
repository: result.repository,
|
|
186
|
+
branch: result.branch,
|
|
187
|
+
size: result.size,
|
|
188
|
+
mimeType: result.mimeType,
|
|
189
|
+
previewUrl: result.url,
|
|
190
|
+
sha: result.sha
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async requireToken() {
|
|
194
|
+
const store = await loadGithubImageBedStore();
|
|
195
|
+
const token = store.github.token.trim();
|
|
196
|
+
if (!token) {
|
|
197
|
+
throw serviceError("\u8BF7\u5148\u4FDD\u5B58 GitHub token\u3002", 400);
|
|
198
|
+
}
|
|
199
|
+
return { token };
|
|
200
|
+
}
|
|
201
|
+
async getCurrentLogin(token) {
|
|
202
|
+
const response = await requestGithubJson(token, "/user");
|
|
203
|
+
const login = response.data.login?.trim();
|
|
204
|
+
if (!login) {
|
|
205
|
+
throw serviceError("\u65E0\u6CD5\u8BFB\u53D6 GitHub \u767B\u5F55\u540D\u3002", 502);
|
|
206
|
+
}
|
|
207
|
+
return login;
|
|
208
|
+
}
|
|
209
|
+
async ensureRepository(token, owner) {
|
|
210
|
+
const repositoryPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(DEFAULT_REPOSITORY)}`;
|
|
211
|
+
try {
|
|
212
|
+
const response = await requestGithubJson(token, repositoryPath);
|
|
213
|
+
if (response.data.private) {
|
|
214
|
+
throw serviceError(`\u4ED3\u5E93 ${DEFAULT_REPOSITORY} \u5FC5\u987B\u662F\u516C\u5F00\u4ED3\u5E93\uFF0C\u624D\u80FD\u8FD4\u56DE\u516C\u7F51\u56FE\u7247\u94FE\u63A5\u3002`, 400);
|
|
215
|
+
}
|
|
216
|
+
return response.data;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const normalized = error;
|
|
219
|
+
if (normalized.statusCode === 404) {
|
|
220
|
+
throw serviceError(
|
|
221
|
+
`\u672A\u627E\u5230\u516C\u5F00\u4ED3\u5E93 ${DEFAULT_REPOSITORY}\u3002\u8BF7\u5148\u5728 GitHub \u521B\u5EFA\u4E00\u4E2A\u516C\u5F00\u4ED3\u5E93\uFF0C\u7136\u540E\u628A\u8FD9\u4E2A\u4ED3\u5E93\u52A0\u5165 token \u7684 Repository access\uFF0C\u518D\u56DE\u6765\u91CD\u8BD5\u3002`,
|
|
222
|
+
404
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async deleteRemoteFile(token, item) {
|
|
229
|
+
const owner = item.owner?.trim();
|
|
230
|
+
const repository = item.repository?.trim() || DEFAULT_REPOSITORY;
|
|
231
|
+
const branch = item.branch?.trim() || "main";
|
|
232
|
+
const objectPath = item.path?.trim();
|
|
233
|
+
if (!owner || !objectPath) {
|
|
234
|
+
throw serviceError("\u5386\u53F2\u8BB0\u5F55\u7F3A\u5C11 GitHub \u6587\u4EF6\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6\u3002", 400);
|
|
235
|
+
}
|
|
236
|
+
const sha = item.sha?.trim() || await this.readRemoteFileSha(token, owner, repository, objectPath, branch);
|
|
237
|
+
if (!sha) {
|
|
238
|
+
throw serviceError("\u65E0\u6CD5\u8BFB\u53D6\u8FDC\u7AEF\u6587\u4EF6 sha\uFF0C\u5220\u9664\u5931\u8D25\u3002", 502);
|
|
239
|
+
}
|
|
240
|
+
await requestGithubJson(token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${encodePath(objectPath)}`, {
|
|
241
|
+
method: "DELETE",
|
|
242
|
+
body: {
|
|
243
|
+
message: `delete ${item.filename || objectPath}`,
|
|
244
|
+
sha,
|
|
245
|
+
branch
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async readRemoteFileSha(token, owner, repository, objectPath, branch) {
|
|
250
|
+
const response = await requestGithubJson(
|
|
251
|
+
token,
|
|
252
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${encodePath(objectPath)}?ref=${encodeURIComponent(branch)}`
|
|
253
|
+
);
|
|
254
|
+
const content = Array.isArray(response.data) ? null : response.data;
|
|
255
|
+
const sha = content?.type === "file" || content?.type === "symlink" ? content.sha?.trim() : content?.sha?.trim();
|
|
256
|
+
if (!sha) {
|
|
257
|
+
throw serviceError("\u65E0\u6CD5\u8BFB\u53D6\u8FDC\u7AEF\u6587\u4EF6 sha\u3002", 502);
|
|
258
|
+
}
|
|
259
|
+
return sha;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
GithubImageBedService
|
|
264
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { getServers as getDnsServers } from "node:dns";
|
|
2
4
|
import { execFile } from "node:child_process";
|
|
5
|
+
import { isIP } from "node:net";
|
|
3
6
|
import { promisify } from "node:util";
|
|
4
7
|
import { requestText } from "../providers/http-client.js";
|
|
5
8
|
const execFileAsync = promisify(execFile);
|
|
@@ -10,6 +13,18 @@ const PLATFORM_PROBE_HEADERS = {
|
|
|
10
13
|
"pragma": "no-cache",
|
|
11
14
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
|
|
12
15
|
};
|
|
16
|
+
const PUBLIC_IPV4_CANDIDATES = [
|
|
17
|
+
"https://ifconfig.me/ip",
|
|
18
|
+
"https://api.ipify.org/",
|
|
19
|
+
"https://icanhazip.com/",
|
|
20
|
+
"https://checkip.amazonaws.com/"
|
|
21
|
+
];
|
|
22
|
+
const PUBLIC_IPV6_CANDIDATES = [
|
|
23
|
+
"https://ifconfig.me/ipv6",
|
|
24
|
+
"https://api6.ipify.org/",
|
|
25
|
+
"https://ipv6.icanhazip.com/",
|
|
26
|
+
"https://ipv6.test-ipv6.com/"
|
|
27
|
+
];
|
|
13
28
|
function parseKeyValueText(input) {
|
|
14
29
|
return Object.fromEntries(
|
|
15
30
|
input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
@@ -28,8 +43,54 @@ function parseDnsServers(text) {
|
|
|
28
43
|
}
|
|
29
44
|
return [...servers];
|
|
30
45
|
}
|
|
46
|
+
function parseResolvConfServers(text) {
|
|
47
|
+
const servers = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const line of text.split(/\r?\n/)) {
|
|
49
|
+
const match = line.trim().match(/^nameserver\s+([0-9a-fA-F:.%]+)/i);
|
|
50
|
+
if (match) {
|
|
51
|
+
servers.add(match[1]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...servers];
|
|
55
|
+
}
|
|
56
|
+
function parseWindowsDnsServers(text) {
|
|
57
|
+
const servers = /* @__PURE__ */ new Set();
|
|
58
|
+
let collecting = false;
|
|
59
|
+
for (const line of text.split(/\r?\n/)) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
collecting = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const match = trimmed.match(/^(DNS Servers?|DNS 服务器)[^:]*:\s*(.*)$/i);
|
|
66
|
+
if (match) {
|
|
67
|
+
const first = match[2].trim();
|
|
68
|
+
if (first) {
|
|
69
|
+
servers.add(first);
|
|
70
|
+
}
|
|
71
|
+
collecting = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (collecting && /^[0-9a-fA-F:.%]+$/.test(trimmed)) {
|
|
75
|
+
servers.add(trimmed);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
collecting = false;
|
|
79
|
+
}
|
|
80
|
+
return [...servers];
|
|
81
|
+
}
|
|
82
|
+
function normalizeIpAddress(ip) {
|
|
83
|
+
return ip.trim().replace(/%[0-9A-Za-z._-]+$/, "");
|
|
84
|
+
}
|
|
85
|
+
function collectDnsServer(servers, value) {
|
|
86
|
+
const normalized = normalizeIpAddress(value);
|
|
87
|
+
if (isIP(normalized) > 0) {
|
|
88
|
+
servers.add(normalized);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
31
91
|
function isPrivateOrReservedIp(ip) {
|
|
32
|
-
|
|
92
|
+
const normalizedIp = normalizeIpAddress(ip);
|
|
93
|
+
return /^(10|127|169\.254|172\.(1[6-9]|2\d|3[0-1])|192\.168)\./.test(normalizedIp) || /^0\./.test(normalizedIp) || /^100\.(6[4-9]|[7-9]\d|1\d\d|2[0-3]\d|24[0-7])\./.test(normalizedIp) || /^198\.(18|19)\./.test(normalizedIp) || /^203\.0\.113\./.test(normalizedIp) || /^192\.0\.2\./.test(normalizedIp) || /^198\.51\.100\./.test(normalizedIp) || /^fc00:/i.test(normalizedIp) || /^fd00:/i.test(normalizedIp) || /^fe80:/i.test(normalizedIp) || /^::1$/.test(normalizedIp);
|
|
33
94
|
}
|
|
34
95
|
function toCountryName(code) {
|
|
35
96
|
if (!code) {
|
|
@@ -41,6 +102,37 @@ function toCountryName(code) {
|
|
|
41
102
|
return code;
|
|
42
103
|
}
|
|
43
104
|
}
|
|
105
|
+
function normalizeProbeError(error) {
|
|
106
|
+
return error instanceof Error ? error.message : String(error);
|
|
107
|
+
}
|
|
108
|
+
function describeSourceUrl(url) {
|
|
109
|
+
try {
|
|
110
|
+
return new URL(url).hostname.replace(/^www\./i, "");
|
|
111
|
+
} catch {
|
|
112
|
+
return url;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function probePublicIpCandidate(url, family, proxy, timeoutMs = 4e3) {
|
|
116
|
+
const label = describeSourceUrl(url);
|
|
117
|
+
try {
|
|
118
|
+
const response = await requestText({
|
|
119
|
+
method: "GET",
|
|
120
|
+
url,
|
|
121
|
+
timeoutMs,
|
|
122
|
+
proxyOverride: proxy
|
|
123
|
+
});
|
|
124
|
+
if (response.status < 200 || response.status >= 300) {
|
|
125
|
+
return { url, label, error: `HTTP ${response.status}` };
|
|
126
|
+
}
|
|
127
|
+
const ip = normalizeIpAddress(response.body);
|
|
128
|
+
if (isIP(ip) !== family) {
|
|
129
|
+
return { url, label, error: `\u672A\u8FD4\u56DE IPv${family} \u5730\u5740` };
|
|
130
|
+
}
|
|
131
|
+
return { url, label, ip };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { url, label, error: normalizeProbeError(error) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
44
136
|
async function runCommand(command, args, timeoutMs = 2500) {
|
|
45
137
|
const result = await execFileAsync(command, args, {
|
|
46
138
|
timeout: timeoutMs,
|
|
@@ -50,99 +142,122 @@ async function runCommand(command, args, timeoutMs = 2500) {
|
|
|
50
142
|
}
|
|
51
143
|
async function readDnsServers() {
|
|
52
144
|
const servers = /* @__PURE__ */ new Set();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
parseDnsServers(scutil).forEach((item) => servers.add(item));
|
|
57
|
-
source = "scutil --dns";
|
|
58
|
-
} catch {
|
|
145
|
+
const sources = /* @__PURE__ */ new Set();
|
|
146
|
+
for (const server of getDnsServers()) {
|
|
147
|
+
collectDnsServer(servers, server);
|
|
59
148
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
149
|
+
if (servers.size > 0) {
|
|
150
|
+
sources.add("node:dns");
|
|
151
|
+
}
|
|
152
|
+
if (process.platform === "darwin") {
|
|
153
|
+
try {
|
|
154
|
+
const scutil = await runCommand("scutil", ["--dns"], 2500);
|
|
155
|
+
parseDnsServers(scutil).forEach((item) => collectDnsServer(servers, item));
|
|
156
|
+
if (servers.size > 0) {
|
|
157
|
+
sources.add("scutil --dns");
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const wifiDns = await runCommand("networksetup", ["-getdnsservers", "Wi-Fi"], 2500);
|
|
163
|
+
for (const line of wifiDns.split(/\r?\n/)) {
|
|
164
|
+
const value = line.trim();
|
|
165
|
+
if (value && /^[0-9a-fA-F:.%]+$/.test(value)) {
|
|
166
|
+
collectDnsServer(servers, value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (servers.size > 0) {
|
|
170
|
+
sources.add("networksetup");
|
|
66
171
|
}
|
|
172
|
+
} catch {
|
|
67
173
|
}
|
|
68
|
-
|
|
69
|
-
|
|
174
|
+
} else if (process.platform === "linux") {
|
|
175
|
+
try {
|
|
176
|
+
const resolvConf = await fs.readFile("/etc/resolv.conf", "utf8");
|
|
177
|
+
parseResolvConfServers(resolvConf).forEach((item) => collectDnsServer(servers, item));
|
|
178
|
+
if (servers.size > 0) {
|
|
179
|
+
sources.add("/etc/resolv.conf");
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
} else if (process.platform === "win32") {
|
|
184
|
+
try {
|
|
185
|
+
const ipconfig = await runCommand("ipconfig", ["/all"], 3e3);
|
|
186
|
+
parseWindowsDnsServers(ipconfig).forEach((item) => collectDnsServer(servers, item));
|
|
187
|
+
if (servers.size > 0) {
|
|
188
|
+
sources.add("ipconfig /all");
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
70
191
|
}
|
|
71
|
-
} catch {
|
|
72
192
|
}
|
|
73
193
|
return {
|
|
74
194
|
servers: [...servers],
|
|
75
|
-
source
|
|
195
|
+
source: sources.size > 0 ? [...sources].join(" + ") : "\u7CFB\u7EDF DNS"
|
|
76
196
|
};
|
|
77
197
|
}
|
|
78
198
|
async function probePublicIpv4(proxy) {
|
|
79
199
|
const startedAt = performance.now();
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
url
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
200
|
+
const sourceLabels = PUBLIC_IPV4_CANDIDATES.map(describeSourceUrl);
|
|
201
|
+
const results = await Promise.all(
|
|
202
|
+
PUBLIC_IPV4_CANDIDATES.map((url) => probePublicIpCandidate(url, 4, proxy, 3500))
|
|
203
|
+
);
|
|
204
|
+
const matched = results.find((item) => item.ip);
|
|
205
|
+
if (matched?.ip) {
|
|
206
|
+
let trace = {};
|
|
207
|
+
try {
|
|
208
|
+
const traceResponse = await requestText({
|
|
209
|
+
method: "GET",
|
|
210
|
+
url: "https://www.cloudflare.com/cdn-cgi/trace",
|
|
211
|
+
timeoutMs: 5e3,
|
|
212
|
+
proxyOverride: proxy
|
|
213
|
+
});
|
|
214
|
+
trace = parseKeyValueText(traceResponse.body);
|
|
215
|
+
} catch {
|
|
216
|
+
trace = {};
|
|
217
|
+
}
|
|
218
|
+
const countryCode = trace.loc;
|
|
219
|
+
const countryName = toCountryName(countryCode);
|
|
220
|
+
return {
|
|
221
|
+
available: true,
|
|
222
|
+
ip: matched.ip,
|
|
223
|
+
countryCode,
|
|
224
|
+
countryName,
|
|
225
|
+
colo: trace.colo,
|
|
226
|
+
source: `${matched.label}${trace.loc || trace.colo ? " + cloudflare trace" : ""}`,
|
|
227
|
+
detail: countryName ? `\u51FA\u53E3\u4F4D\u4E8E ${countryName}${trace.colo ? ` \xB7 ${trace.colo}` : ""}` : "\u68C0\u6D4B\u5230\u516C\u7F51 IPv4 \u51FA\u53E3\u3002",
|
|
228
|
+
elapsedMs: Math.round(performance.now() - startedAt)
|
|
229
|
+
};
|
|
101
230
|
}
|
|
102
|
-
const
|
|
231
|
+
const attemptedSources = results.map((item) => `${item.label}: ${item.error || "\u672A\u8FD4\u56DE\u7ED3\u679C"}`);
|
|
103
232
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
source: "ifconfig.me + cloudflare trace",
|
|
233
|
+
available: false,
|
|
234
|
+
ip: "",
|
|
235
|
+
source: sourceLabels.join(" / "),
|
|
236
|
+
detail: attemptedSources.length > 0 ? `\u672A\u83B7\u5F97\u516C\u7F51 IPv4\u3002${attemptedSources.slice(0, 2).join("\uFF1B")}` : "\u672A\u83B7\u5F97\u516C\u7F51 IPv4\u3002",
|
|
109
237
|
elapsedMs: Math.round(performance.now() - startedAt)
|
|
110
238
|
};
|
|
111
239
|
}
|
|
112
240
|
async function probePublicIpv6(proxy) {
|
|
113
241
|
const startedAt = performance.now();
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const body = response.body.trim();
|
|
127
|
-
if (!body) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (/^[0-9a-fA-F:]+$/.test(body) && body.includes(":")) {
|
|
131
|
-
return {
|
|
132
|
-
available: true,
|
|
133
|
-
ip: body,
|
|
134
|
-
source: url,
|
|
135
|
-
detail: "\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\u3002",
|
|
136
|
-
elapsedMs: Math.round(performance.now() - startedAt)
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
}
|
|
242
|
+
const results = await Promise.all(
|
|
243
|
+
PUBLIC_IPV6_CANDIDATES.map((url) => probePublicIpCandidate(url, 6, proxy, 3e3))
|
|
244
|
+
);
|
|
245
|
+
const matched = results.find((item) => item.ip);
|
|
246
|
+
if (matched?.ip) {
|
|
247
|
+
return {
|
|
248
|
+
available: true,
|
|
249
|
+
ip: matched.ip,
|
|
250
|
+
source: matched.label,
|
|
251
|
+
detail: "\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\u3002",
|
|
252
|
+
elapsedMs: Math.round(performance.now() - startedAt)
|
|
253
|
+
};
|
|
141
254
|
}
|
|
255
|
+
const attemptedSources = results.map((item) => `${item.label}: ${item.error || "\u672A\u8FD4\u56DE\u7ED3\u679C"}`);
|
|
142
256
|
return {
|
|
143
257
|
available: false,
|
|
144
|
-
source: "
|
|
145
|
-
detail: "\u672A\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\uFF0C\u5F53\u524D\u8FDE\u63A5\u53EF\u80FD\u53EA\u8D70 IPv4\u3002"
|
|
258
|
+
source: PUBLIC_IPV6_CANDIDATES.map(describeSourceUrl).join(" / "),
|
|
259
|
+
detail: attemptedSources.length > 0 ? `\u672A\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\u3002${attemptedSources.slice(0, 2).join("\uFF1B")}` : "\u672A\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\uFF0C\u5F53\u524D\u8FDE\u63A5\u53EF\u80FD\u53EA\u8D70 IPv4\u3002",
|
|
260
|
+
elapsedMs: Math.round(performance.now() - startedAt)
|
|
146
261
|
};
|
|
147
262
|
}
|
|
148
263
|
async function probePlatform(url, label, proxy) {
|
|
@@ -216,7 +331,7 @@ class NetworkDetectService {
|
|
|
216
331
|
probePlatform("https://x.com/", "X", proxy)
|
|
217
332
|
])
|
|
218
333
|
]);
|
|
219
|
-
const dnsDetail = dns.servers.length > 0 ? `\u7CFB\u7EDF DNS: ${dns.servers.join(" / ")}` :
|
|
334
|
+
const dnsDetail = dns.servers.length > 0 ? `\u7CFB\u7EDF DNS: ${dns.servers.join(" / ")}` : `${dns.source} \u672A\u8BFB\u53D6\u5230\u7CFB\u7EDF DNS\u3002`;
|
|
220
335
|
return {
|
|
221
336
|
checkedAt: Date.now(),
|
|
222
337
|
publicIpv4,
|
|
@@ -56,7 +56,7 @@ class VersionService {
|
|
|
56
56
|
const manifest = await readPackageManifest();
|
|
57
57
|
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(manifest.name)}/latest`;
|
|
58
58
|
try {
|
|
59
|
-
const latestVersion = await this.fetchNpmLatestVersion(registryUrl);
|
|
59
|
+
const latestVersion = process.env.AZT_FORCE_LATEST_VERSION || await this.fetchNpmLatestVersion(registryUrl);
|
|
60
60
|
const needsUpdate = compareSemver(manifest.version, latestVersion) < 0;
|
|
61
61
|
return {
|
|
62
62
|
packageName: manifest.name,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureStateMigrated, getStateDir } from "./state-paths.js";
|
|
5
|
+
const MAX_HISTORY_ITEMS = 100;
|
|
6
|
+
function createEmptyStore() {
|
|
7
|
+
return {
|
|
8
|
+
version: 1,
|
|
9
|
+
items: []
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function getGithubImageBedHistoryPath() {
|
|
13
|
+
return path.join(getStateDir(), "github-image-bed-history.json");
|
|
14
|
+
}
|
|
15
|
+
async function readStore() {
|
|
16
|
+
try {
|
|
17
|
+
await ensureStateMigrated();
|
|
18
|
+
const raw = await fs.readFile(getGithubImageBedHistoryPath(), "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
return {
|
|
21
|
+
version: 1,
|
|
22
|
+
items: Array.isArray(parsed.items) ? parsed.items.slice(0, MAX_HISTORY_ITEMS) : []
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return createEmptyStore();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function writeStore(store) {
|
|
29
|
+
await ensureStateMigrated();
|
|
30
|
+
await fs.mkdir(getStateDir(), { recursive: true });
|
|
31
|
+
await fs.writeFile(getGithubImageBedHistoryPath(), `${JSON.stringify(store, null, 2)}
|
|
32
|
+
`, "utf8");
|
|
33
|
+
}
|
|
34
|
+
async function listGithubImageBedHistory(limit = 50) {
|
|
35
|
+
const store = await readStore();
|
|
36
|
+
return store.items.slice(0, Math.max(1, Math.min(limit, MAX_HISTORY_ITEMS)));
|
|
37
|
+
}
|
|
38
|
+
async function appendGithubImageBedHistory(item) {
|
|
39
|
+
const store = await readStore();
|
|
40
|
+
store.items = [item, ...store.items.filter((entry) => entry.id !== item.id)].slice(0, MAX_HISTORY_ITEMS);
|
|
41
|
+
await writeStore(store);
|
|
42
|
+
return item;
|
|
43
|
+
}
|
|
44
|
+
async function findGithubImageBedHistoryItem(id) {
|
|
45
|
+
const store = await readStore();
|
|
46
|
+
return store.items.find((entry) => entry.id === id) || null;
|
|
47
|
+
}
|
|
48
|
+
async function removeGithubImageBedHistoryItem(id) {
|
|
49
|
+
const store = await readStore();
|
|
50
|
+
const target = store.items.find((entry) => entry.id === id) || null;
|
|
51
|
+
if (!target) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
store.items = store.items.filter((entry) => entry.id !== id);
|
|
55
|
+
await writeStore(store);
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
async function clearGithubImageBedHistory() {
|
|
59
|
+
await writeStore(createEmptyStore());
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
appendGithubImageBedHistory,
|
|
63
|
+
clearGithubImageBedHistory,
|
|
64
|
+
findGithubImageBedHistoryItem,
|
|
65
|
+
getGithubImageBedHistoryPath,
|
|
66
|
+
listGithubImageBedHistory,
|
|
67
|
+
removeGithubImageBedHistoryItem
|
|
68
|
+
};
|