@sfmc-bds/bds-tools 0.2.0-beta.9 → 0.2.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 +663 -0
- package/README.md +1 -1
- package/dist/bds-manager.d.ts +16 -6
- package/dist/bds-manager.d.ts.map +1 -1
- package/dist/bds-manager.js +19 -1
- package/dist/bds-manager.js.map +2 -2
- package/dist/changelog.d.ts +11 -7
- package/dist/changelog.d.ts.map +1 -1
- package/dist/changelog.js +60 -34
- package/dist/changelog.js.map +2 -2
- package/dist/check-update.d.ts.map +1 -1
- package/dist/check-update.js +55 -17
- package/dist/check-update.js.map +2 -2
- package/dist/cli-pack-manager.js +11 -1
- package/dist/cli-pack-manager.js.map +2 -2
- package/dist/dependency-negotiator.d.ts +73 -0
- package/dist/dependency-negotiator.d.ts.map +1 -0
- package/dist/dependency-negotiator.js +151 -0
- package/dist/dependency-negotiator.js.map +7 -0
- package/dist/fsx.d.ts +15 -7
- package/dist/fsx.d.ts.map +1 -1
- package/dist/fsx.js +67 -8
- package/dist/fsx.js.map +2 -2
- package/dist/http.d.ts +49 -11
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js.map +2 -2
- package/dist/level-dat.d.ts +105 -0
- package/dist/level-dat.d.ts.map +1 -0
- package/dist/level-dat.js +490 -0
- package/dist/level-dat.js.map +7 -0
- package/dist/pack-manager.d.ts +41 -34
- package/dist/pack-manager.d.ts.map +1 -1
- package/dist/pack-manager.js +73 -24
- package/dist/pack-manager.js.map +2 -2
- package/dist/qq-events-notify.d.ts +1 -1
- package/dist/qq-events-notify.d.ts.map +1 -1
- package/dist/qq-events-notify.js.map +1 -1
- package/dist/qqutil.d.ts +10 -6
- package/dist/qqutil.d.ts.map +1 -1
- package/dist/qqutil.js +14 -12
- package/dist/qqutil.js.map +2 -2
- package/dist/rollback.d.ts.map +1 -1
- package/dist/rollback.js +2 -2
- package/dist/rollback.js.map +2 -2
- package/dist/server-properties-i18n.d.ts +17 -0
- package/dist/server-properties-i18n.d.ts.map +1 -0
- package/dist/server-properties-i18n.js +201 -0
- package/dist/server-properties-i18n.js.map +7 -0
- package/dist/server-properties.d.ts +25 -0
- package/dist/server-properties.d.ts.map +1 -1
- package/dist/server-properties.js +141 -1
- package/dist/server-properties.js.map +2 -2
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/upstream.d.ts +14 -6
- package/dist/upstream.d.ts.map +1 -1
- package/dist/upstream.js.map +2 -2
- package/dist/world-packs.d.ts +106 -3
- package/dist/world-packs.d.ts.map +1 -1
- package/dist/world-packs.js +245 -10
- package/dist/world-packs.js.map +3 -3
- package/package.json +19 -14
- package/dist/qq-events-notify.test.d.ts +0 -5
- package/dist/qq-events-notify.test.d.ts.map +0 -1
- package/dist/qq-events-notify.test.js +0 -41
- package/dist/qq-events-notify.test.js.map +0 -7
package/dist/fsx.js
CHANGED
|
@@ -22,42 +22,99 @@ function hashFileSync(filePath, algo = "sha256") {
|
|
|
22
22
|
return "";
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
const DEFAULT_COPY_IGNORE = /* @__PURE__ */ new Set([
|
|
26
|
+
".git",
|
|
27
|
+
".svn",
|
|
28
|
+
".hg",
|
|
29
|
+
"node_modules",
|
|
30
|
+
".DS_Store",
|
|
31
|
+
"Thumbs.db",
|
|
32
|
+
"desktop.ini"
|
|
33
|
+
]);
|
|
34
|
+
function copyFileSyncSafe(src, dest) {
|
|
35
|
+
try {
|
|
36
|
+
fs.copyFileSync(src, dest);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const code = err?.code;
|
|
39
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(dest)) {
|
|
42
|
+
fs.chmodSync(dest, 438);
|
|
43
|
+
fs.unlinkSync(dest);
|
|
44
|
+
}
|
|
45
|
+
fs.copyFileSync(src, dest);
|
|
46
|
+
} catch {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(dest, 438);
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
25
58
|
async function copyFileAsync(src, dest) {
|
|
26
|
-
|
|
59
|
+
try {
|
|
60
|
+
await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const code = err?.code;
|
|
63
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
64
|
+
try {
|
|
65
|
+
if (fs.existsSync(dest)) {
|
|
66
|
+
fs.chmodSync(dest, 438);
|
|
67
|
+
fs.unlinkSync(dest);
|
|
68
|
+
}
|
|
69
|
+
await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));
|
|
70
|
+
} catch {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
fs.chmodSync(dest, 438);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
27
81
|
}
|
|
28
|
-
function copyDirSync(src, dest) {
|
|
82
|
+
function copyDirSync(src, dest, ignore = DEFAULT_COPY_IGNORE) {
|
|
29
83
|
fs.mkdirSync(dest, { recursive: true });
|
|
30
84
|
for (const entry of fs.readdirSync(src)) {
|
|
85
|
+
if (ignore.has(entry)) continue;
|
|
31
86
|
const srcPath = path.join(src, entry);
|
|
32
87
|
const destPath = path.join(dest, entry);
|
|
33
88
|
if (fs.statSync(srcPath).isDirectory()) {
|
|
34
|
-
copyDirSync(srcPath, destPath);
|
|
89
|
+
copyDirSync(srcPath, destPath, ignore);
|
|
35
90
|
} else {
|
|
36
|
-
|
|
91
|
+
copyFileSyncSafe(srcPath, destPath);
|
|
37
92
|
}
|
|
38
93
|
}
|
|
39
94
|
}
|
|
40
|
-
async function copyDirAsync(src, dest) {
|
|
95
|
+
async function copyDirAsync(src, dest, ignore = DEFAULT_COPY_IGNORE) {
|
|
41
96
|
fs.mkdirSync(dest, { recursive: true });
|
|
42
97
|
await Promise.all(
|
|
43
98
|
fs.readdirSync(src).map(async (entry) => {
|
|
99
|
+
if (ignore.has(entry)) return;
|
|
44
100
|
const srcPath = path.join(src, entry);
|
|
45
101
|
const destPath = path.join(dest, entry);
|
|
46
102
|
const stat = fs.statSync(srcPath);
|
|
47
103
|
if (stat.isDirectory()) {
|
|
48
|
-
await copyDirAsync(srcPath, destPath);
|
|
104
|
+
await copyDirAsync(srcPath, destPath, ignore);
|
|
49
105
|
} else {
|
|
50
106
|
await copyFileAsync(srcPath, destPath);
|
|
51
107
|
}
|
|
52
108
|
})
|
|
53
109
|
);
|
|
54
110
|
}
|
|
55
|
-
function getDirSize(dir) {
|
|
111
|
+
function getDirSize(dir, ignore = DEFAULT_COPY_IGNORE) {
|
|
56
112
|
let total = 0;
|
|
57
113
|
try {
|
|
58
114
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
115
|
+
if (ignore.has(entry.name)) continue;
|
|
59
116
|
const full = path.join(dir, entry.name);
|
|
60
|
-
if (entry.isDirectory()) total += getDirSize(full);
|
|
117
|
+
if (entry.isDirectory()) total += getDirSize(full, ignore);
|
|
61
118
|
else if (entry.isFile()) total += fs.statSync(full).size;
|
|
62
119
|
}
|
|
63
120
|
} catch {
|
|
@@ -97,9 +154,11 @@ function readJsonFile(filePath) {
|
|
|
97
154
|
return JSON.parse(text);
|
|
98
155
|
}
|
|
99
156
|
export {
|
|
157
|
+
DEFAULT_COPY_IGNORE,
|
|
100
158
|
copyDirAsync,
|
|
101
159
|
copyDirSync,
|
|
102
160
|
copyFileAsync,
|
|
161
|
+
copyFileSyncSafe,
|
|
103
162
|
emptyDirSync,
|
|
104
163
|
getDirSize,
|
|
105
164
|
hashFileAsync,
|
package/dist/fsx.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/fsx.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * fsx.ts \u2014 \u5F02\u6B65/\u6D41\u5F0F\u6587\u4EF6\u64CD\u4F5C (\u907F\u514D\u5927\u6587\u4EF6\u52A0\u8F7D\u8FDB\u5185\u5B58)\n */\n\nimport crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\n\n/**\n * \u6D41\u5F0F\u8BA1\u7B97\u6587\u4EF6\u54C8\u5E0C (sha1 / sha256)\uFF0C\n * \u7528\u4E8E\u5927\u6587\u4EF6 (\u4F8B\u5982 BDS \u53EF\u6267\u884C\u6587\u4EF6 ~80MB)\uFF0C\u4E0D\u4F1A OOM\u3002\n */\nexport async function hashFileAsync(filePath: string, algo: \"sha1\" | \"sha256\" = \"sha256\"): Promise<string> {\n return new Promise((resolve, reject) => {\n const h = crypto.createHash(algo);\n const stream = fs.createReadStream(filePath);\n stream.on(\"data\", (chunk: Buffer | string) => {\n h.update(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n stream.on(\"end\", () => resolve(h.digest(\"hex\").toLowerCase()));\n stream.on(\"error\", reject);\n });\n}\n\n/** \u540C\u6B65\u7248\u672C\uFF08\u7528\u4E8E\u5C0F\u6587\u4EF6\uFF09 */\nexport function hashFileSync(filePath: string, algo: \"sha1\" | \"sha256\" = \"sha256\"): string {\n try {\n const h = crypto.createHash(algo);\n h.update(fs.readFileSync(filePath));\n return h.digest(\"hex\").toLowerCase();\n } catch {\n return \"\";\n }\n}\n\n/** \u540C\u6B65\u6D41\u5F0F\u62F7\u8D1D\u5355\u6587\u4EF6
|
|
5
|
-
"mappings": "AAIA,OAAO,YAAY;AACnB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,gBAAgB;AAMzB,eAAsB,cAAc,UAAkB,OAA0B,UAA2B;AACzG,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,IAAI,OAAO,WAAW,IAAI;AAChC,UAAM,SAAS,GAAG,iBAAiB,QAAQ;AAC3C,WAAO,GAAG,QAAQ,CAAC,UAA2B;AAC5C,QAAE,OAAO,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,KAAK;AAAA,IACjE,CAAC;AACD,WAAO,GAAG,OAAO,MAAM,QAAQ,EAAE,OAAO,KAAK,EAAE,YAAY,CAAC,CAAC;AAC7D,WAAO,GAAG,SAAS,MAAM;AAAA,EAC3B,CAAC;AACH;AAGO,SAAS,aAAa,UAAkB,OAA0B,UAAkB;AACzF,MAAI;AACF,UAAM,IAAI,OAAO,WAAW,IAAI;AAChC,MAAE,OAAO,GAAG,aAAa,QAAQ,CAAC;AAClC,WAAO,EAAE,OAAO,KAAK,EAAE,YAAY;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,cAAc,KAAa,MAA6B;AAC5E,
|
|
4
|
+
"sourcesContent": ["/**\n * fsx.ts \u2014 \u5F02\u6B65/\u6D41\u5F0F\u6587\u4EF6\u64CD\u4F5C (\u907F\u514D\u5927\u6587\u4EF6\u52A0\u8F7D\u8FDB\u5185\u5B58)\n */\n\nimport crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\n\n/**\n * \u6D41\u5F0F\u8BA1\u7B97\u6587\u4EF6\u54C8\u5E0C (sha1 / sha256)\uFF0C\n * \u7528\u4E8E\u5927\u6587\u4EF6 (\u4F8B\u5982 BDS \u53EF\u6267\u884C\u6587\u4EF6 ~80MB)\uFF0C\u4E0D\u4F1A OOM\u3002\n */\nexport async function hashFileAsync(filePath: string, algo: \"sha1\" | \"sha256\" = \"sha256\"): Promise<string> {\n return new Promise((resolve, reject) => {\n const h = crypto.createHash(algo);\n const stream = fs.createReadStream(filePath);\n stream.on(\"data\", (chunk: Buffer | string) => {\n h.update(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n stream.on(\"end\", () => resolve(h.digest(\"hex\").toLowerCase()));\n stream.on(\"error\", reject);\n });\n}\n\n/** \u540C\u6B65\u7248\u672C\uFF08\u7528\u4E8E\u5C0F\u6587\u4EF6\uFF09 */\nexport function hashFileSync(filePath: string, algo: \"sha1\" | \"sha256\" = \"sha256\"): string {\n try {\n const h = crypto.createHash(algo);\n h.update(fs.readFileSync(filePath));\n return h.digest(\"hex\").toLowerCase();\n } catch {\n return \"\";\n }\n}\n\n/** \u6392\u9664\u65E0\u9700\u5907\u4EFD/\u590D\u5236\u7684\u4ED3\u5E93\u4E0E\u7CFB\u7EDF\u5783\u573E\u5143\u6570\u636E */\nexport const DEFAULT_COPY_IGNORE = new Set([\n \".git\",\n \".svn\",\n \".hg\",\n \"node_modules\",\n \".DS_Store\",\n \"Thumbs.db\",\n \"desktop.ini\",\n]);\n\n/**\n * Windows \u517C\u5BB9\u7684\u5B89\u5168\u5355\u6587\u4EF6\u62F7\u8D1D\u3002\n * \u82E5\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728\u4E14\u5904\u4E8E\u53EA\u8BFB\u72B6\u6001\uFF08\u4F8B\u5982 git objects \u5C5E\u6027\u4E3A\u53EA\u8BFB\uFF09\uFF0C\u5148\u8D4B\u5199\u6743\u9650\u5E76\u8986\u5199\uFF0C\n * \u5E76\u786E\u4FDD\u62F7\u8D1D\u540E\u7684\u76EE\u6807\u6587\u4EF6\u5177\u6709\u53EF\u5199\u6743\u9650\uFF0C\u907F\u514D\u540E\u7EED\u91CD\u590D\u5907\u4EFD\u62A5\u9519\u3002\n */\nexport function copyFileSyncSafe(src: string, dest: string): void {\n try {\n fs.copyFileSync(src, dest);\n } catch (err: unknown) {\n const code = (err as NodeJS.ErrnoException)?.code;\n if (code === \"EPERM\" || code === \"EACCES\") {\n try {\n if (fs.existsSync(dest)) {\n fs.chmodSync(dest, 0o666);\n fs.unlinkSync(dest);\n }\n fs.copyFileSync(src, dest);\n } catch {\n throw err;\n }\n } else {\n throw err;\n }\n }\n try {\n fs.chmodSync(dest, 0o666);\n } catch {\n /* ignore */\n }\n}\n\n/** \u540C\u6B65\u6D41\u5F0F\u62F7\u8D1D\u5355\u6587\u4EF6\uFF0C\u76EE\u6807\u53EA\u8BFB\u65F6\u81EA\u52A8\u89E3\u9664\u5E76\u8986\u5199 */\nexport async function copyFileAsync(src: string, dest: string): Promise<void> {\n try {\n await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));\n } catch (err: unknown) {\n const code = (err as NodeJS.ErrnoException)?.code;\n if (code === \"EPERM\" || code === \"EACCES\") {\n try {\n if (fs.existsSync(dest)) {\n fs.chmodSync(dest, 0o666);\n fs.unlinkSync(dest);\n }\n await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));\n } catch {\n throw err;\n }\n } else {\n throw err;\n }\n }\n try {\n fs.chmodSync(dest, 0o666);\n } catch {\n /* ignore */\n }\n}\n\n/** \u540C\u6B65\u76EE\u5F55\u590D\u5236 (\u9012\u5F52)\uFF0C\u9ED8\u8BA4\u6392\u9664 .git / node_modules \u7B49\u5143\u6570\u636E */\nexport function copyDirSync(\n src: string,\n dest: string,\n ignore: Set<string> = DEFAULT_COPY_IGNORE\n): void {\n fs.mkdirSync(dest, { recursive: true });\n for (const entry of fs.readdirSync(src)) {\n if (ignore.has(entry)) continue;\n const srcPath = path.join(src, entry);\n const destPath = path.join(dest, entry);\n if (fs.statSync(srcPath).isDirectory()) {\n copyDirSync(srcPath, destPath, ignore);\n } else {\n copyFileSyncSafe(srcPath, destPath);\n }\n }\n}\n\n/** \u5F02\u6B65\u76EE\u5F55\u590D\u5236 (\u57FA\u4E8E\u6D41)\uFF0C\u9ED8\u8BA4\u6392\u9664 .git / node_modules \u7B49\u5143\u6570\u636E */\nexport async function copyDirAsync(\n src: string,\n dest: string,\n ignore: Set<string> = DEFAULT_COPY_IGNORE\n): Promise<void> {\n fs.mkdirSync(dest, { recursive: true });\n await Promise.all(\n fs.readdirSync(src).map(async (entry) => {\n if (ignore.has(entry)) return;\n const srcPath = path.join(src, entry);\n const destPath = path.join(dest, entry);\n const stat = fs.statSync(srcPath);\n if (stat.isDirectory()) {\n await copyDirAsync(srcPath, destPath, ignore);\n } else {\n await copyFileAsync(srcPath, destPath);\n }\n })\n );\n}\n\n/** \u8BA1\u7B97\u76EE\u5F55\u5927\u5C0F (\u5B57\u8282)\uFF0C\u9ED8\u8BA4\u6392\u9664 .git / node_modules \u7B49\u5143\u6570\u636E */\nexport function getDirSize(dir: string, ignore: Set<string> = DEFAULT_COPY_IGNORE): number {\n let total = 0;\n try {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (ignore.has(entry.name)) continue;\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) total += getDirSize(full, ignore);\n else if (entry.isFile()) total += fs.statSync(full).size;\n }\n } catch {}\n return total;\n}\n\n/** \u5F3A\u5236\u6E05\u7A7A\u76EE\u5F55\u5185\u5BB9 (\u4F46\u4FDD\u7559\u76EE\u5F55\u672C\u8EAB) */\nexport function emptyDirSync(dir: string): void {\n if (!fs.existsSync(dir)) return;\n for (const entry of fs.readdirSync(dir)) {\n const full = path.join(dir, entry);\n try {\n const stat = fs.statSync(full);\n if (stat.isDirectory()) fs.rmSync(full, { recursive: true, force: true });\n else fs.unlinkSync(full);\n } catch (e) {\n // \u5FFD\u7565: \u9876\u5C42\u4ECD\u4FDD\u7559\u76EE\u5F55\uFF0C\u5373\u4F7F\u90E8\u5206\u5B50\u9879\u65E0\u6CD5\u5220\u9664\n }\n }\n}\n\n/** \u5B89\u5168\u5220\u9664\u6574\u4E2A\u76EE\u5F55 */\nexport function rmSafe(path: string): void {\n try {\n fs.rmSync(path, { recursive: true, force: true });\n } catch {\n /* ignore */\n }\n}\n\n/** \u5B89\u5168\u5199\u6587\u4EF6 (\u539F\u5B50: \u5199\u4E34\u65F6\u6587\u4EF6 \u2192 rename) */\nexport function writeFileSafe(filePath: string, data: string | Buffer): void {\n const dir = path.dirname(filePath);\n fs.mkdirSync(dir, { recursive: true });\n const tmp = `${filePath}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, data);\n fs.renameSync(tmp, filePath);\n}\n\n/**\n * \u8BFB JSON \u6587\u4EF6\uFF08utf8\uFF09\u3002\n * \u82E5\u6587\u4EF6\u4EE5 UTF-8 BOM\uFF08U+FEFF\uFF09\u5F00\u5934\u5219\u5265\u79BB\u540E\u518D\u89E3\u6790\uFF08\u8BB0\u4E8B\u672C\u300CUTF-8 with BOM\u300D\uFF09\u3002\n */\nexport function readJsonFile<T = unknown>(filePath: string): T {\n let text = fs.readFileSync(filePath, \"utf8\");\n if (text.length > 0 && text.charCodeAt(0) === 0xfeff) {\n text = text.slice(1);\n }\n return JSON.parse(text) as T;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["path"]
|
|
7
7
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* http.ts —
|
|
2
|
+
* http.ts — 针对大文件与外部上游设计的 HTTP 网络请求工具
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 核心机制:
|
|
5
|
+
* - 分阶段超时控制:区分连接建立超时(connectTimeout)与全量传输超时(totalTimeout)
|
|
6
|
+
* - 自动重定向处理:安全跟踪 3xx 跳转(最多 5 次)
|
|
7
|
+
* - 流式文件下载:通过 pipeline 管道直接落盘,避免大文件一次性加载至内存
|
|
8
|
+
* - 异常容错清理:下载失败时自动清理残留的临时文件碎片
|
|
8
9
|
*/
|
|
9
10
|
interface HttpOptions {
|
|
10
11
|
method?: "GET" | "HEAD" | "POST";
|
|
@@ -14,24 +15,61 @@ interface HttpOptions {
|
|
|
14
15
|
totalTimeoutMs?: number;
|
|
15
16
|
redirects?: number;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* 发起底层 HTTP/HTTPS 请求。
|
|
20
|
+
*
|
|
21
|
+
* @param url 目标请求地址。
|
|
22
|
+
* @param opts 请求参数(包含请求方法、请求头、主体、阶段超时时间及重定向次数等)。
|
|
23
|
+
* @returns 包含状态码、响应体 Buffer 及响应头字典的结果对象。
|
|
24
|
+
*/
|
|
17
25
|
export declare function httpRequest(url: string, opts?: HttpOptions): Promise<{
|
|
18
26
|
statusCode: number;
|
|
19
27
|
body: Buffer;
|
|
20
28
|
headers: Record<string, string | string[] | undefined>;
|
|
21
29
|
}>;
|
|
30
|
+
/**
|
|
31
|
+
* 发起 GET 请求并自动将响应体解析为 JSON 数据对象。
|
|
32
|
+
*
|
|
33
|
+
* @template T 预期的 JSON 数据类型。
|
|
34
|
+
* @param url 请求目标 URL。
|
|
35
|
+
* @param opts 请求配置选项。
|
|
36
|
+
* @returns 解析后的 JSON 对象。
|
|
37
|
+
*/
|
|
22
38
|
export declare function httpGetJson<T = unknown>(url: string, opts?: HttpOptions): Promise<T>;
|
|
39
|
+
/**
|
|
40
|
+
* 发起 GET 请求并获取 UTF-8 纯文本响应内容。
|
|
41
|
+
*
|
|
42
|
+
* @param url 请求目标 URL。
|
|
43
|
+
* @param opts 请求配置选项。
|
|
44
|
+
* @returns 响应文本字符串。
|
|
45
|
+
*/
|
|
23
46
|
export declare function httpGetText(url: string, opts?: HttpOptions): Promise<string>;
|
|
24
|
-
/** 多个源并发请求,谁先成功返回(每个源都用 JSON 解析,解析失败也失败) */
|
|
25
|
-
export declare function fetchJsonWithFallback<T = unknown>(sources: string[], timeoutMs?: number): Promise<T>;
|
|
26
47
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
48
|
+
* 针对多个备选源并发发起请求,由最先成功返回且合法解析 JSON 的源提供数据(Happy Eyeballs 机制)。
|
|
49
|
+
*
|
|
50
|
+
* @template T 返回的 JSON 数据类型。
|
|
51
|
+
* @param sources 候选 URL 列表。
|
|
52
|
+
* @param timeoutMs 超时时间(毫秒,默认为 15000)。
|
|
53
|
+
* @returns 最先成功响应的数据。
|
|
54
|
+
* @throws 当所有备选源均失败时抛出聚合错误。
|
|
31
55
|
*/
|
|
56
|
+
export declare function fetchJsonWithFallback<T = unknown>(sources: string[], timeoutMs?: number): Promise<T>;
|
|
57
|
+
/** 下载配置选项。 */
|
|
32
58
|
export interface DownloadOptions extends HttpOptions {
|
|
59
|
+
/** 下载进度变化回调函数。 */
|
|
33
60
|
onProgress?: (downloaded: number, total: number) => void;
|
|
34
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 流式下载网络资源至本地指定路径。
|
|
64
|
+
* - 传输失败时自动删除残留的临时文件碎片
|
|
65
|
+
* - 支持节流的进度通知回调
|
|
66
|
+
* - 支持连接建立与全量传输双阶段独立超时
|
|
67
|
+
*
|
|
68
|
+
* @param url 下载目标地址。
|
|
69
|
+
* @param destPath 本地落盘目标绝对路径。
|
|
70
|
+
* @param opts 下载参数选项。
|
|
71
|
+
* @returns 成功下载的总字节数。
|
|
72
|
+
*/
|
|
35
73
|
export declare function httpDownload(url: string, destPath: string, opts?: DownloadOptions): Promise<number>;
|
|
36
74
|
export {};
|
|
37
75
|
//# sourceMappingURL=http.d.ts.map
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAE/B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;CAAE,CAAC,CA2DvG;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,GAAG,OAAO,EAC3C,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGtF;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,GAAG,OAAO,EACrD,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,SAAS,GACjB,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,cAAc;AACd,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,kBAAkB;IAClB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AAED;;;;;;;;;;GAUG;AAGH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,MAAM,CAAC,CA4GjB"}
|
package/dist/http.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/http.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * http.ts \u2014 \u7A33\u5B9A HTTP \u5DE5\u5177 (\u5206\u9636\u6BB5\u8D85\u65F6 + \u81EA\u52A8\u91CD\u5B9A\u5411 + \u6D41\u5F0F\u4E0B\u8F7D)\n *\n * \u6539\u8FDB:\n * - connect timeout vs total timeout \u5206\u5F00\n * - \u4E0B\u8F7D\u901A\u8FC7\u6D41\u7BA1\u9053\uFF0C\u907F\u514D\u5927\u6587\u4EF6\u52A0\u8F7D\u5230\u5185\u5B58\n * - \u5931\u8D25\u540E\u5DF2\u5199\u5165\u7684\u4E34\u65F6\u6587\u4EF6\u53EF\u88AB\u6E05\u7406\n */\n\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport type { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createWriteStream, statSync } from \"node:fs\";\nimport { log } from \"./log.js\";\n\ninterface HttpOptions {\n method?: \"GET\" | \"HEAD\" | \"POST\";\n headers?: Record<string, string>;\n body?: string;\n connectTimeoutMs?: number;\n totalTimeoutMs?: number;\n redirects?: number;\n}\n\nconst MAX_REDIRECTS = 5;\nconst DEFAULT_CONNECT_TIMEOUT = 15_000;\nconst DEFAULT_TOTAL_TIMEOUT = 600_000;\n\nexport async function httpRequest(\n url: string,\n opts: HttpOptions = {}\n): Promise<{ statusCode: number; body: Buffer; headers: Record<string, string | string[] | undefined> }> {\n const redirects = opts.redirects ?? 0;\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: opts.method ?? \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => {\n const body = Buffer.concat(chunks);\n const status = res.statusCode ?? 0;\n // 3xx \u2192 \u8DDF\u968F Location\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u4F46\u7F3A\u5C11 Location`));\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n httpRequest(next, { ...opts, redirects: redirects + 1 })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) {\n return reject(new Error(`HTTP ${status} for ${url}`));\n }\n resolve({ statusCode: status, body, headers: res.headers });\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (err) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n reject(err);\n });\n\n if (opts.body) req.write(opts.body);\n req.end();\n });\n}\n\nexport async function httpGetJson<T = unknown>(\n url: string,\n opts: HttpOptions = {}\n): Promise<T> {\n const res = await httpRequest(url, opts);\n try {\n return JSON.parse(res.body.toString(\"utf-8\")) as T;\n } catch (e) {\n throw new Error(`JSON \u89E3\u6790\u5931\u8D25 ${url}: ${(e as Error).message}`);\n }\n}\n\nexport async function httpGetText(url: string, opts: HttpOptions = {}): Promise<string> {\n const res = await httpRequest(url, opts);\n return res.body.toString(\"utf-8\");\n}\n\n/** \u591A\u4E2A\u6E90\u5E76\u53D1\u8BF7\u6C42\uFF0C\u8C01\u5148\u6210\u529F\u8FD4\u56DE\uFF08\u6BCF\u4E2A\u6E90\u90FD\u7528 JSON \u89E3\u6790\uFF0C\u89E3\u6790\u5931\u8D25\u4E5F\u5931\u8D25\uFF09 */\nexport async function fetchJsonWithFallback<T = unknown>(\n sources: string[],\n timeoutMs = 15_000\n): Promise<T> {\n const tasks = sources.map(async (url) => {\n const res = await httpGetJson<T>(url, { totalTimeoutMs: timeoutMs });\n return { url, value: res };\n });\n const settled = await Promise.allSettled(tasks);\n for (const r of settled) {\n if (r.status === \"fulfilled\") return r.value.value;\n }\n const errors = settled\n .filter((r): r is PromiseRejectedResult => r.status === \"rejected\")\n .map((r) => r.reason?.message ?? \"unknown\")\n .join(\"; \");\n throw new Error(`\u6240\u6709\u6E90\u5747\u4E0D\u53EF\u7528: ${errors}`);\n}\n\n/**\n * \u6D41\u5F0F\u4E0B\u8F7D\u5230\u6307\u5B9A\u8DEF\u5F84\uFF0C\u8FD4\u56DE\u5B57\u8282\u6570\u3002\n * - \u5931\u8D25\u65F6\u81EA\u52A8\u6E05\u7406 partial \u6587\u4EF6\n * - \u652F\u6301 progress \u56DE\u8C03\n * - \u652F\u6301 connect/total \u9636\u6BB5\u8D85\u65F6\n */\nexport interface DownloadOptions extends HttpOptions {\n onProgress?: (downloaded: number, total: number) => void;\n}\n\nexport async function httpDownload(\n url: string,\n destPath: string,\n opts: DownloadOptions = {}\n): Promise<number> {\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n let redirects = opts.redirects ?? 0;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const status = res.statusCode ?? 0;\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u7F3A\u5C11 Location`));\n redirects++;\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n res.resume();\n httpDownload(next, destPath, { ...opts, redirects })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) return reject(new Error(`HTTP ${status} for ${url}`));\n\n const total = parseInt(String(res.headers[\"content-length\"] ?? 0), 10) || 0;\n const file = createWriteStream(destPath);\n let downloaded = 0;\n let failed = false;\n // \u8282\u6D41\u8FDB\u5EA6\u56DE\u8C03:\u6BCF\u4E2A tick \u6700\u591A 10 \u6B21/\u79D2(100ms \u95F4\u9694),\n // \u907F\u514D cli-progress \u9891\u7E41\u91CD\u7ED8\u62D6\u6162\u4E0B\u8F7D\n let lastProgressAt = 0;\n const PROGRESS_INTERVAL_MS = 100;\n\n const handleError = (e: Error): void => {\n if (failed) return;\n failed = true;\n file.close();\n // \u6E05\u7406 partial \u6587\u4EF6\n import(\"node:fs\").then((fs) => {\n try { fs.unlinkSync(destPath); } catch {}\n });\n reject(e);\n };\n\n const stream: Readable = res;\n stream.on(\"data\", (chunk: Buffer) => {\n downloaded += chunk.length;\n if (opts.onProgress) {\n const now = Date.now();\n if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {\n lastProgressAt = now;\n /* total=0\uFF08\u65E0 Content-Length\uFF09\u65F6\u4ECD\u56DE\u8C03\uFF0C\u8C03\u7528\u65B9\u53EF\u7528\u5DF2\u4E0B\u8F7D\u5B57\u8282\u66F4\u65B0 UI */\n opts.onProgress(downloaded, total);\n }\n }\n });\n stream.on(\"error\", (e) => handleError(e as Error));\n\n file.on(\"error\", (e) => handleError(e as Error));\n\n pipeline(stream, file).catch((e) => {\n if (!failed) handleError(e as Error);\n });\n\n // \u4E0D\u80FD\u76F4\u63A5\u76D1\u542C file 'finish' \u56E0\u4E3A\u6211\u4EEC\u5728\u9519\u8BEF\u65F6\u624B\u52A8 handleError\n file.on(\"finish\", () => {\n if (failed) return;\n try {\n const finalBytes = statSync(destPath).size;\n // \u6536\u5C3E:\u786E\u4FDD\u6700\u540E\u4E00\u6B21\u56DE\u8C03\u8BA9\u8FDB\u5EA6\u6761\u8D70\u5230 100%(\u5373\u4FBF\u6700\u8FD1\n // \u4E00\u4E2A tick \u7684\u8282\u6D41\u6CA1\u89E6\u53D1,\u4E5F\u8865\u4E00\u6B21)\u3002\n // \u65E0 Content-Length\uFF08total=0\uFF09\u65F6\u7528 finalBytes \u4F5C\u603B\u91CF\uFF0C\u4E0E\u4E2D\u9014\u300C\u4ECD\u56DE\u8C03\u300D\u5951\u7EA6\u4E00\u81F4\uFF08LSP\uFF09\u3002\n if (opts.onProgress) {\n opts.onProgress(finalBytes, total > 0 ? total : finalBytes);\n }\n resolve(finalBytes);\n } catch (e) {\n reject(e as Error);\n }\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (e) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n log.warn(`HTTP \u9519\u8BEF ${url}: ${(e as Error).message}`);\n reject(e);\n });\n req.end();\n });\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * http.ts \u2014 \u9488\u5BF9\u5927\u6587\u4EF6\u4E0E\u5916\u90E8\u4E0A\u6E38\u8BBE\u8BA1\u7684 HTTP \u7F51\u7EDC\u8BF7\u6C42\u5DE5\u5177\n *\n * \u6838\u5FC3\u673A\u5236\uFF1A\n * - \u5206\u9636\u6BB5\u8D85\u65F6\u63A7\u5236\uFF1A\u533A\u5206\u8FDE\u63A5\u5EFA\u7ACB\u8D85\u65F6\uFF08connectTimeout\uFF09\u4E0E\u5168\u91CF\u4F20\u8F93\u8D85\u65F6\uFF08totalTimeout\uFF09\n * - \u81EA\u52A8\u91CD\u5B9A\u5411\u5904\u7406\uFF1A\u5B89\u5168\u8DDF\u8E2A 3xx \u8DF3\u8F6C\uFF08\u6700\u591A 5 \u6B21\uFF09\n * - \u6D41\u5F0F\u6587\u4EF6\u4E0B\u8F7D\uFF1A\u901A\u8FC7 pipeline \u7BA1\u9053\u76F4\u63A5\u843D\u76D8\uFF0C\u907F\u514D\u5927\u6587\u4EF6\u4E00\u6B21\u6027\u52A0\u8F7D\u81F3\u5185\u5B58\n * - \u5F02\u5E38\u5BB9\u9519\u6E05\u7406\uFF1A\u4E0B\u8F7D\u5931\u8D25\u65F6\u81EA\u52A8\u6E05\u7406\u6B8B\u7559\u7684\u4E34\u65F6\u6587\u4EF6\u788E\u7247\n */\n\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport type { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createWriteStream, statSync } from \"node:fs\";\nimport { log } from \"./log.js\";\n\ninterface HttpOptions {\n method?: \"GET\" | \"HEAD\" | \"POST\";\n headers?: Record<string, string>;\n body?: string;\n connectTimeoutMs?: number;\n totalTimeoutMs?: number;\n redirects?: number;\n}\n\nconst MAX_REDIRECTS = 5;\nconst DEFAULT_CONNECT_TIMEOUT = 15_000;\nconst DEFAULT_TOTAL_TIMEOUT = 600_000;\n\n/**\n * \u53D1\u8D77\u5E95\u5C42 HTTP/HTTPS \u8BF7\u6C42\u3002\n *\n * @param url \u76EE\u6807\u8BF7\u6C42\u5730\u5740\u3002\n * @param opts \u8BF7\u6C42\u53C2\u6570\uFF08\u5305\u542B\u8BF7\u6C42\u65B9\u6CD5\u3001\u8BF7\u6C42\u5934\u3001\u4E3B\u4F53\u3001\u9636\u6BB5\u8D85\u65F6\u65F6\u95F4\u53CA\u91CD\u5B9A\u5411\u6B21\u6570\u7B49\uFF09\u3002\n * @returns \u5305\u542B\u72B6\u6001\u7801\u3001\u54CD\u5E94\u4F53 Buffer \u53CA\u54CD\u5E94\u5934\u5B57\u5178\u7684\u7ED3\u679C\u5BF9\u8C61\u3002\n */\nexport async function httpRequest(\n\n url: string,\n opts: HttpOptions = {}\n): Promise<{ statusCode: number; body: Buffer; headers: Record<string, string | string[] | undefined> }> {\n const redirects = opts.redirects ?? 0;\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: opts.method ?? \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => {\n const body = Buffer.concat(chunks);\n const status = res.statusCode ?? 0;\n // 3xx \u2192 \u8DDF\u968F Location\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u4F46\u7F3A\u5C11 Location`));\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n httpRequest(next, { ...opts, redirects: redirects + 1 })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) {\n return reject(new Error(`HTTP ${status} for ${url}`));\n }\n resolve({ statusCode: status, body, headers: res.headers });\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (err) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n reject(err);\n });\n\n if (opts.body) req.write(opts.body);\n req.end();\n });\n}\n\n/**\n * \u53D1\u8D77 GET \u8BF7\u6C42\u5E76\u81EA\u52A8\u5C06\u54CD\u5E94\u4F53\u89E3\u6790\u4E3A JSON \u6570\u636E\u5BF9\u8C61\u3002\n *\n * @template T \u9884\u671F\u7684 JSON \u6570\u636E\u7C7B\u578B\u3002\n * @param url \u8BF7\u6C42\u76EE\u6807 URL\u3002\n * @param opts \u8BF7\u6C42\u914D\u7F6E\u9009\u9879\u3002\n * @returns \u89E3\u6790\u540E\u7684 JSON \u5BF9\u8C61\u3002\n */\nexport async function httpGetJson<T = unknown>(\n url: string,\n opts: HttpOptions = {}\n): Promise<T> {\n const res = await httpRequest(url, opts);\n try {\n return JSON.parse(res.body.toString(\"utf-8\")) as T;\n } catch (e) {\n throw new Error(`JSON \u89E3\u6790\u5931\u8D25 ${url}: ${(e as Error).message}`);\n }\n}\n\n/**\n * \u53D1\u8D77 GET \u8BF7\u6C42\u5E76\u83B7\u53D6 UTF-8 \u7EAF\u6587\u672C\u54CD\u5E94\u5185\u5BB9\u3002\n *\n * @param url \u8BF7\u6C42\u76EE\u6807 URL\u3002\n * @param opts \u8BF7\u6C42\u914D\u7F6E\u9009\u9879\u3002\n * @returns \u54CD\u5E94\u6587\u672C\u5B57\u7B26\u4E32\u3002\n */\nexport async function httpGetText(url: string, opts: HttpOptions = {}): Promise<string> {\n const res = await httpRequest(url, opts);\n return res.body.toString(\"utf-8\");\n}\n\n/**\n * \u9488\u5BF9\u591A\u4E2A\u5907\u9009\u6E90\u5E76\u53D1\u53D1\u8D77\u8BF7\u6C42\uFF0C\u7531\u6700\u5148\u6210\u529F\u8FD4\u56DE\u4E14\u5408\u6CD5\u89E3\u6790 JSON \u7684\u6E90\u63D0\u4F9B\u6570\u636E\uFF08Happy Eyeballs \u673A\u5236\uFF09\u3002\n *\n * @template T \u8FD4\u56DE\u7684 JSON \u6570\u636E\u7C7B\u578B\u3002\n * @param sources \u5019\u9009 URL \u5217\u8868\u3002\n * @param timeoutMs \u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF0C\u9ED8\u8BA4\u4E3A 15000\uFF09\u3002\n * @returns \u6700\u5148\u6210\u529F\u54CD\u5E94\u7684\u6570\u636E\u3002\n * @throws \u5F53\u6240\u6709\u5907\u9009\u6E90\u5747\u5931\u8D25\u65F6\u629B\u51FA\u805A\u5408\u9519\u8BEF\u3002\n */\nexport async function fetchJsonWithFallback<T = unknown>(\n sources: string[],\n timeoutMs = 15_000\n): Promise<T> {\n const tasks = sources.map(async (url) => {\n const res = await httpGetJson<T>(url, { totalTimeoutMs: timeoutMs });\n return { url, value: res };\n });\n const settled = await Promise.allSettled(tasks);\n for (const r of settled) {\n if (r.status === \"fulfilled\") return r.value.value;\n }\n const errors = settled\n .filter((r): r is PromiseRejectedResult => r.status === \"rejected\")\n .map((r) => r.reason?.message ?? \"unknown\")\n .join(\"; \");\n throw new Error(`\u6240\u6709\u6E90\u5747\u4E0D\u53EF\u7528: ${errors}`);\n}\n\n/** \u4E0B\u8F7D\u914D\u7F6E\u9009\u9879\u3002 */\nexport interface DownloadOptions extends HttpOptions {\n /** \u4E0B\u8F7D\u8FDB\u5EA6\u53D8\u5316\u56DE\u8C03\u51FD\u6570\u3002 */\n onProgress?: (downloaded: number, total: number) => void;\n}\n\n/**\n * \u6D41\u5F0F\u4E0B\u8F7D\u7F51\u7EDC\u8D44\u6E90\u81F3\u672C\u5730\u6307\u5B9A\u8DEF\u5F84\u3002\n * - \u4F20\u8F93\u5931\u8D25\u65F6\u81EA\u52A8\u5220\u9664\u6B8B\u7559\u7684\u4E34\u65F6\u6587\u4EF6\u788E\u7247\n * - \u652F\u6301\u8282\u6D41\u7684\u8FDB\u5EA6\u901A\u77E5\u56DE\u8C03\n * - \u652F\u6301\u8FDE\u63A5\u5EFA\u7ACB\u4E0E\u5168\u91CF\u4F20\u8F93\u53CC\u9636\u6BB5\u72EC\u7ACB\u8D85\u65F6\n *\n * @param url \u4E0B\u8F7D\u76EE\u6807\u5730\u5740\u3002\n * @param destPath \u672C\u5730\u843D\u76D8\u76EE\u6807\u7EDD\u5BF9\u8DEF\u5F84\u3002\n * @param opts \u4E0B\u8F7D\u53C2\u6570\u9009\u9879\u3002\n * @returns \u6210\u529F\u4E0B\u8F7D\u7684\u603B\u5B57\u8282\u6570\u3002\n */\n\n\nexport async function httpDownload(\n url: string,\n destPath: string,\n opts: DownloadOptions = {}\n): Promise<number> {\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n let redirects = opts.redirects ?? 0;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const status = res.statusCode ?? 0;\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u7F3A\u5C11 Location`));\n redirects++;\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n res.resume();\n httpDownload(next, destPath, { ...opts, redirects })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) return reject(new Error(`HTTP ${status} for ${url}`));\n\n const total = parseInt(String(res.headers[\"content-length\"] ?? 0), 10) || 0;\n const file = createWriteStream(destPath);\n let downloaded = 0;\n let failed = false;\n // \u8282\u6D41\u8FDB\u5EA6\u56DE\u8C03:\u6BCF\u4E2A tick \u6700\u591A 10 \u6B21/\u79D2(100ms \u95F4\u9694),\n // \u907F\u514D cli-progress \u9891\u7E41\u91CD\u7ED8\u62D6\u6162\u4E0B\u8F7D\n let lastProgressAt = 0;\n const PROGRESS_INTERVAL_MS = 100;\n\n const handleError = (e: Error): void => {\n if (failed) return;\n failed = true;\n file.close();\n // \u6E05\u7406 partial \u6587\u4EF6\n import(\"node:fs\").then((fs) => {\n try { fs.unlinkSync(destPath); } catch {}\n });\n reject(e);\n };\n\n const stream: Readable = res;\n stream.on(\"data\", (chunk: Buffer) => {\n downloaded += chunk.length;\n if (opts.onProgress) {\n const now = Date.now();\n if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {\n lastProgressAt = now;\n /* total=0\uFF08\u65E0 Content-Length\uFF09\u65F6\u4ECD\u56DE\u8C03\uFF0C\u8C03\u7528\u65B9\u53EF\u7528\u5DF2\u4E0B\u8F7D\u5B57\u8282\u66F4\u65B0 UI */\n opts.onProgress(downloaded, total);\n }\n }\n });\n stream.on(\"error\", (e) => handleError(e as Error));\n\n file.on(\"error\", (e) => handleError(e as Error));\n\n pipeline(stream, file).catch((e) => {\n if (!failed) handleError(e as Error);\n });\n\n // \u4E0D\u80FD\u76F4\u63A5\u76D1\u542C file 'finish' \u56E0\u4E3A\u6211\u4EEC\u5728\u9519\u8BEF\u65F6\u624B\u52A8 handleError\n file.on(\"finish\", () => {\n if (failed) return;\n try {\n const finalBytes = statSync(destPath).size;\n // \u6536\u5C3E:\u786E\u4FDD\u6700\u540E\u4E00\u6B21\u56DE\u8C03\u8BA9\u8FDB\u5EA6\u6761\u8D70\u5230 100%(\u5373\u4FBF\u6700\u8FD1\n // \u4E00\u4E2A tick \u7684\u8282\u6D41\u6CA1\u89E6\u53D1,\u4E5F\u8865\u4E00\u6B21)\u3002\n // \u65E0 Content-Length\uFF08total=0\uFF09\u65F6\u7528 finalBytes \u4F5C\u603B\u91CF\uFF0C\u4E0E\u4E2D\u9014\u300C\u4ECD\u56DE\u8C03\u300D\u5951\u7EA6\u4E00\u81F4\uFF08LSP\uFF09\u3002\n if (opts.onProgress) {\n opts.onProgress(finalBytes, total > 0 ? total : finalBytes);\n }\n resolve(finalBytes);\n } catch (e) {\n reject(e as Error);\n }\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (e) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n log.warn(`HTTP \u9519\u8BEF ${url}: ${(e as Error).message}`);\n reject(e);\n });\n req.end();\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* level-dat.ts — Minecraft 基岩版世界存档 level.dat 小端序 NBT 解析与实验性玩法安全修改器
|
|
3
|
+
*
|
|
4
|
+
* 基岩版 level.dat 规范:
|
|
5
|
+
* - Header(8 字节):
|
|
6
|
+
* - [0..3] uint32 LE:存储格式版本(当前版本通常为 10)
|
|
7
|
+
* - [4..7] uint32 LE:有效 NBT 数据字节长度(data_length)
|
|
8
|
+
* - Payload:未压缩的小端序(Little-Endian)标准 NBT 复合标签(TAG_Compound)
|
|
9
|
+
*
|
|
10
|
+
* 核心能力:
|
|
11
|
+
* - 原生无外部黑盒依赖,Buffer 级解析与精确字节位移
|
|
12
|
+
* - 读取世界实验性玩法的开启状态(特别是 Beta APIs / gametest)
|
|
13
|
+
* - 安全注入或切换实验性开关并自动备份原文件(.bak)与校准 Header data_length
|
|
14
|
+
*/
|
|
15
|
+
export declare const TAG_END = 0;
|
|
16
|
+
export declare const TAG_BYTE = 1;
|
|
17
|
+
export declare const TAG_SHORT = 2;
|
|
18
|
+
export declare const TAG_INT = 3;
|
|
19
|
+
export declare const TAG_LONG = 4;
|
|
20
|
+
export declare const TAG_FLOAT = 5;
|
|
21
|
+
export declare const TAG_DOUBLE = 6;
|
|
22
|
+
export declare const TAG_BYTE_ARRAY = 7;
|
|
23
|
+
export declare const TAG_STRING = 8;
|
|
24
|
+
export declare const TAG_LIST = 9;
|
|
25
|
+
export declare const TAG_COMPOUND = 10;
|
|
26
|
+
export declare const TAG_INT_ARRAY = 11;
|
|
27
|
+
export declare const TAG_LONG_ARRAY = 12;
|
|
28
|
+
export interface NbtTagMeta {
|
|
29
|
+
type: number;
|
|
30
|
+
name: string;
|
|
31
|
+
startOffset: number;
|
|
32
|
+
nameOffset: number;
|
|
33
|
+
valueOffset: number;
|
|
34
|
+
endOffset: number;
|
|
35
|
+
value?: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface KnownExperimentDefinition {
|
|
38
|
+
id: string;
|
|
39
|
+
nbtKey: string;
|
|
40
|
+
nameZh: string;
|
|
41
|
+
nameEn: string;
|
|
42
|
+
aliases?: string[];
|
|
43
|
+
isRootTag?: boolean;
|
|
44
|
+
extraExpTags?: string[];
|
|
45
|
+
}
|
|
46
|
+
export declare const KNOWN_EXPERIMENTS: readonly KnownExperimentDefinition[];
|
|
47
|
+
export declare function resolveExperimentDefinition(query: string): KnownExperimentDefinition | null;
|
|
48
|
+
export interface LevelDatExperimentsInfo {
|
|
49
|
+
experimentsEverUsed: boolean;
|
|
50
|
+
savedWithToggledExperiments: boolean;
|
|
51
|
+
experiments: Record<string, number | boolean>;
|
|
52
|
+
hasBetaApis: boolean;
|
|
53
|
+
knownExperiments: Record<string, boolean>;
|
|
54
|
+
}
|
|
55
|
+
export interface LevelDatMutateResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
changed: boolean;
|
|
58
|
+
backupPath?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
previousBetaApisState?: boolean;
|
|
61
|
+
enabledExperiments?: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 遍历并跳过指定类型的 NBT 载荷,返回下一个 tag 的 offset。
|
|
65
|
+
*/
|
|
66
|
+
export declare function skipNbtPayload(buf: Buffer, offset: number, type: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* 解析复合标签内部所有直接子标签的元数据映射。
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseCompoundChildren(buf: Buffer, compoundValueOffset: number): {
|
|
71
|
+
children: Map<string, NbtTagMeta>;
|
|
72
|
+
compoundEndOffset: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 解析基岩版 level.dat 的完整文件结构并定位根 Compound。
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseBedrockLevelDat(buf: Buffer): {
|
|
78
|
+
version: number;
|
|
79
|
+
dataLength: number;
|
|
80
|
+
rootCompoundName: string;
|
|
81
|
+
rootChildren: Map<string, NbtTagMeta>;
|
|
82
|
+
rootEndOffset: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* 读取世界存档 level.dat 的实验性配置。若解析失败或文件不存在返回 null。
|
|
86
|
+
*/
|
|
87
|
+
export declare function readLevelDatExperiments(levelDatPath: string): LevelDatExperimentsInfo | null;
|
|
88
|
+
/**
|
|
89
|
+
* 为指定 level.dat 开启指定的实验性开关(若未指定或传 "all" 则开启全部已知实验性开关)。
|
|
90
|
+
*
|
|
91
|
+
* 保证:
|
|
92
|
+
* - 自动备份原文件至 `${levelDatPath}.bak`;
|
|
93
|
+
* - 自动更新 root 与 experiments 复合标签中的目标 byte tags;
|
|
94
|
+
* - 自动更新 experiments_ever_used = 1b 与 saved_with_toggled_experiments = 1b;
|
|
95
|
+
* - 精确校准 8 字节文件头部的 dataLength;
|
|
96
|
+
* - 原子写操作(.tmp -> rename);
|
|
97
|
+
* - 若所请求开关已全部处于开启状态,直接返回 changed: false,不破坏文件。
|
|
98
|
+
*/
|
|
99
|
+
export declare function enableExperimentsInLevelDat(levelDatPath: string, targetExperimentIds?: string[] | "all"): Promise<LevelDatMutateResult>;
|
|
100
|
+
/**
|
|
101
|
+
* 为指定 level.dat 开启 Beta APIs(gametest)。
|
|
102
|
+
* 保持向后兼容,委托给 enableExperimentsInLevelDat。
|
|
103
|
+
*/
|
|
104
|
+
export declare function enableBetaApisInLevelDat(levelDatPath: string): Promise<LevelDatMutateResult>;
|
|
105
|
+
//# sourceMappingURL=level-dat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"level-dat.d.ts","sourceRoot":"","sources":["../src/level-dat.ts"],"names":[],"mappings":"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"}
|