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.
Files changed (55) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +13 -5
  3. package/README.zh-CN.md +13 -5
  4. package/admin-ui/dist/assets/InfoRow-0ULI9iI3.js +1 -0
  5. package/admin-ui/dist/assets/accounts-DymL4WIa.js +2 -0
  6. package/admin-ui/dist/assets/activity-D21-Xrc4.js +1 -0
  7. package/admin-ui/dist/assets/app-mark-nsRs4vo7.svg +8 -0
  8. package/admin-ui/dist/assets/circle-check-ZYtn9GqY.js +1 -0
  9. package/admin-ui/dist/assets/clock-3-BzDANsVk.js +1 -0
  10. package/admin-ui/dist/assets/docs-BRNWAMPw.css +1 -0
  11. package/admin-ui/dist/assets/docs-DtO-AOWU.js +1735 -0
  12. package/admin-ui/dist/assets/earth-DFdZaQIi.js +1 -0
  13. package/admin-ui/dist/assets/image-bed-yIVQ4dKs.js +1 -0
  14. package/admin-ui/dist/assets/index-By4r-wy3.css +1 -0
  15. package/admin-ui/dist/assets/index-DRe-tByu.js +10 -0
  16. package/admin-ui/dist/assets/jsx-runtime-DqpGtLhh.js +1 -0
  17. package/admin-ui/dist/assets/launch-CQXYrl-h.js +1 -0
  18. package/admin-ui/dist/assets/logs-awABDg1C.js +1 -0
  19. package/admin-ui/dist/assets/network-detect-sSrnwZqf.js +1 -0
  20. package/admin-ui/dist/assets/overview-BbSON0Jl.js +1 -0
  21. package/admin-ui/dist/assets/profiles-DMOjJORP.js +1 -0
  22. package/admin-ui/dist/assets/refresh-cw-CAAH2rqe.js +1 -0
  23. package/admin-ui/dist/assets/search-B2hz41D3.js +1 -0
  24. package/admin-ui/dist/assets/server-BrjJPb9D.js +1 -0
  25. package/admin-ui/dist/assets/settings-DvRiHS7i.js +1 -0
  26. package/admin-ui/dist/assets/tester-CftPgRE9.js +3 -0
  27. package/admin-ui/dist/assets/upload-CwXb7Q1b.js +1 -0
  28. package/admin-ui/dist/assets/zap-B4_oDbCp.js +1 -0
  29. package/admin-ui/dist/index.html +5 -3
  30. package/build/icon.icns +0 -0
  31. package/build/icon.ico +0 -0
  32. package/build/icon.png +0 -0
  33. package/build/icon.svg +8 -0
  34. package/build/mac-install-guide.txt +25 -0
  35. package/dist/core/context.js +3 -0
  36. package/dist/core/providers/http-client.js +11 -4
  37. package/dist/core/services/auth-service.js +88 -12
  38. package/dist/core/services/config-service.js +87 -23
  39. package/dist/core/services/github-image-bed-service.js +264 -0
  40. package/dist/core/services/network-detect-service.js +189 -74
  41. package/dist/core/services/version-service.js +1 -1
  42. package/dist/core/store/github-image-bed-history-store.js +68 -0
  43. package/dist/core/store/github-image-bed-store.js +52 -0
  44. package/dist/core/store/profile-store.js +73 -32
  45. package/dist/core/store/settings-store.js +14 -0
  46. package/dist/desktop/main.js +158 -6
  47. package/dist/server/app.js +168 -26
  48. package/dist/server/index.js +41 -15
  49. package/docs/DESKTOP_RELEASE.md +35 -3
  50. package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +2 -2
  51. package/package.json +34 -2
  52. package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +0 -9
  53. package/admin-ui/dist/assets/index-DNzR8XR7.css +0 -1
  54. package/admin-ui/dist/assets/index-DZMegNPs.js +0 -1745
  55. 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
- return /^(10|127|169\.254|172\.(1[6-9]|2\d|3[0-1])|192\.168)\./.test(ip) || /^0\./.test(ip) || /^100\.(6[4-9]|[7-9]\d|1\d\d|2[0-3]\d|24[0-7])\./.test(ip) || /^198\.(18|19)\./.test(ip) || /^203\.0\.113\./.test(ip) || /^192\.0\.2\./.test(ip) || /^198\.51\.100\./.test(ip) || /^fc00:/i.test(ip) || /^fd00:/i.test(ip) || /^fe80:/i.test(ip) || /^::1$/.test(ip);
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
- let source = "\u7CFB\u7EDF DNS";
54
- try {
55
- const scutil = await runCommand("scutil", ["--dns"], 2500);
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
- try {
61
- const wifiDns = await runCommand("networksetup", ["-getdnsservers", "Wi-Fi"], 2500);
62
- for (const line of wifiDns.split(/\r?\n/)) {
63
- const value = line.trim();
64
- if (value && /^[0-9a-fA-F:.]+$/.test(value)) {
65
- servers.add(value);
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
- if (servers.size > 0) {
69
- source = `${source} + networksetup`;
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 response = await requestText({
81
- method: "GET",
82
- url: "https://ifconfig.me/ip",
83
- timeoutMs: 8e3,
84
- proxyOverride: proxy
85
- });
86
- if (response.status < 200 || response.status >= 500) {
87
- throw new Error(`\u516C\u7F51 IPv4 \u63A2\u6D4B\u5931\u8D25: HTTP ${response.status}`);
88
- }
89
- const ip = response.body.trim();
90
- let trace = {};
91
- try {
92
- const traceResponse = await requestText({
93
- method: "GET",
94
- url: "https://www.cloudflare.com/cdn-cgi/trace",
95
- timeoutMs: 8e3,
96
- proxyOverride: proxy
97
- });
98
- trace = parseKeyValueText(traceResponse.body);
99
- } catch {
100
- trace = {};
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 countryCode = trace.loc;
231
+ const attemptedSources = results.map((item) => `${item.label}: ${item.error || "\u672A\u8FD4\u56DE\u7ED3\u679C"}`);
103
232
  return {
104
- ip,
105
- countryCode,
106
- countryName: toCountryName(countryCode),
107
- colo: trace.colo,
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 ipv6Candidates = [
115
- "https://ifconfig.me/ipv6",
116
- "https://ipv6.test-ipv6.com/"
117
- ];
118
- for (const url of ipv6Candidates) {
119
- try {
120
- const response = await requestText({
121
- method: "GET",
122
- url,
123
- timeoutMs: 6e3,
124
- proxyOverride: proxy
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: "ifconfig.me / test-ipv6.com",
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(" / ")}` : "\u672A\u8BFB\u53D6\u5230\u7CFB\u7EDF DNS\u3002";
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
+ };