@follenfang/fupload 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fupload/SKILL.md +4 -4
- package/fupload/references/blackbox.md +3 -1
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/blackbox.py +129 -101
- package/fupload/scripts/fupload_cli/blackbox_web.py +522 -0
- package/fupload/scripts/fupload_cli/cli.py +19 -9
- package/fupload/scripts/fupload_cli/schema.py +14 -3
- package/npm/lib/python-requirements.txt +1 -0
- package/npm/lib/python.mjs +53 -17
- package/npm/skill-manifest.json +17 -17
- package/package.json +1 -1
- package/fupload/scripts/fupload_cli/blackbox_auth.py +0 -159
package/npm/lib/python.mjs
CHANGED
|
@@ -82,45 +82,68 @@ function readMarker(root) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function
|
|
85
|
+
function runtimeEnvironment(root, env) {
|
|
86
|
+
return {
|
|
87
|
+
...env,
|
|
88
|
+
PLAYWRIGHT_BROWSERS_PATH: path.join(root, "browsers"),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function probeRuntime(executable, root, env, run = spawnSync) {
|
|
86
93
|
if (!fs.existsSync(executable)) {
|
|
87
94
|
return null;
|
|
88
95
|
}
|
|
89
96
|
const result = run(executable, [
|
|
90
97
|
"-c",
|
|
91
|
-
"import importlib.metadata,sys; import qcloud_cos; print('.'.join(map(str,sys.version_info[:3]))
|
|
92
|
-
], {
|
|
98
|
+
"import importlib.metadata,json,pathlib,sys; import qcloud_cos; from playwright.sync_api import sync_playwright; p=sync_playwright().start(); executable=p.chromium.executable_path; p.stop(); print(json.dumps({'python_version': '.'.join(map(str,sys.version_info[:3])), 'cos_version': importlib.metadata.version('cos-python-sdk-v5'), 'playwright_version': importlib.metadata.version('playwright'), 'chromium_executable': executable})); sys.exit(0 if pathlib.Path(executable).is_file() else 1)",
|
|
99
|
+
], {
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
env: runtimeEnvironment(root, env),
|
|
102
|
+
shell: false,
|
|
103
|
+
windowsHide: true,
|
|
104
|
+
});
|
|
93
105
|
if (result.status !== 0) {
|
|
94
106
|
return null;
|
|
95
107
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
let details;
|
|
109
|
+
try {
|
|
110
|
+
details = JSON.parse(result.stdout.trim());
|
|
111
|
+
} catch {
|
|
99
112
|
return null;
|
|
100
113
|
}
|
|
101
|
-
|
|
114
|
+
const match = details.python_version?.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
115
|
+
if (!match || !details.cos_version || !details.playwright_version || !details.chromium_executable) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
version: match.slice(1).map(Number),
|
|
120
|
+
dependencyVersion: details.cos_version,
|
|
121
|
+
playwrightVersion: details.playwright_version,
|
|
122
|
+
chromiumExecutable: details.chromium_executable,
|
|
123
|
+
};
|
|
102
124
|
}
|
|
103
125
|
|
|
104
|
-
function inspectRuntime({ root, platform, requirementsHash, run }) {
|
|
126
|
+
function inspectRuntime({ root, platform, requirementsHash, env, run }) {
|
|
105
127
|
const marker = readMarker(root);
|
|
106
128
|
if (marker?.schema !== PYTHON_RUNTIME_SCHEMA || marker.requirements_sha256 !== requirementsHash) {
|
|
107
129
|
return null;
|
|
108
130
|
}
|
|
109
131
|
const command = pythonRuntimeExecutable(root, platform);
|
|
110
|
-
const probe = probeRuntime(command, run);
|
|
132
|
+
const probe = probeRuntime(command, root, env, run);
|
|
111
133
|
if (!probe || probe.version[0] !== 3 || probe.version[1] < 9) {
|
|
112
134
|
return null;
|
|
113
135
|
}
|
|
114
|
-
return { command, args: [],
|
|
136
|
+
return { command, args: [], ...probe };
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
function bounded(value) {
|
|
118
140
|
return String(value || "").trim().slice(0, 4000);
|
|
119
141
|
}
|
|
120
142
|
|
|
121
|
-
function runChecked(run, command, args, message) {
|
|
143
|
+
function runChecked(run, command, args, message, options = {}) {
|
|
122
144
|
const result = run(command, args, {
|
|
123
145
|
encoding: "utf8",
|
|
146
|
+
...options,
|
|
124
147
|
shell: false,
|
|
125
148
|
windowsHide: true,
|
|
126
149
|
});
|
|
@@ -191,7 +214,7 @@ export function ensurePythonRuntime({
|
|
|
191
214
|
const parent = path.dirname(root);
|
|
192
215
|
const lock = acquireRuntimeLock(root);
|
|
193
216
|
try {
|
|
194
|
-
const current = inspectRuntime({ root, platform, requirementsHash, run });
|
|
217
|
+
const current = inspectRuntime({ root, platform, requirementsHash, env, run });
|
|
195
218
|
if (current) {
|
|
196
219
|
return { status: "current", root, requirements, python: current };
|
|
197
220
|
}
|
|
@@ -205,13 +228,19 @@ export function ensurePythonRuntime({
|
|
|
205
228
|
const backup = path.join(parent, `.python-backup-${nonce}`);
|
|
206
229
|
fs.mkdirSync(parent, { recursive: true });
|
|
207
230
|
let movedOld = false;
|
|
231
|
+
let created;
|
|
208
232
|
try {
|
|
209
233
|
runChecked(run, base.command, [...base.args, "-m", "venv", staging], "Could not create the Fuploader Python runtime");
|
|
210
234
|
const stagingPython = pythonRuntimeExecutable(staging, platform);
|
|
211
235
|
runChecked(run, stagingPython, [
|
|
212
236
|
"-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--requirement", requirements,
|
|
213
237
|
], "Could not install Fuploader Python dependencies");
|
|
214
|
-
|
|
238
|
+
runChecked(run, stagingPython, [
|
|
239
|
+
"-m", "playwright", "install", "chromium",
|
|
240
|
+
], "Could not install Fuploader Chromium", {
|
|
241
|
+
env: runtimeEnvironment(staging, env),
|
|
242
|
+
});
|
|
243
|
+
const installed = probeRuntime(stagingPython, staging, env, run);
|
|
215
244
|
if (!installed) {
|
|
216
245
|
throw new Error("The Fuploader Python runtime did not pass its dependency probe.");
|
|
217
246
|
}
|
|
@@ -220,6 +249,8 @@ export function ensurePythonRuntime({
|
|
|
220
249
|
requirements_sha256: requirementsHash,
|
|
221
250
|
python_version: installed.version.join("."),
|
|
222
251
|
dependency_version: installed.dependencyVersion,
|
|
252
|
+
playwright_version: installed.playwrightVersion,
|
|
253
|
+
chromium_executable: path.relative(staging, installed.chromiumExecutable),
|
|
223
254
|
});
|
|
224
255
|
if (fs.existsSync(root)) {
|
|
225
256
|
fs.renameSync(root, backup);
|
|
@@ -234,16 +265,21 @@ export function ensurePythonRuntime({
|
|
|
234
265
|
}
|
|
235
266
|
throw error;
|
|
236
267
|
}
|
|
268
|
+
created = inspectRuntime({ root, platform, requirementsHash, env, run });
|
|
269
|
+
if (!created) {
|
|
270
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
271
|
+
if (movedOld) {
|
|
272
|
+
fs.renameSync(backup, root);
|
|
273
|
+
movedOld = false;
|
|
274
|
+
}
|
|
275
|
+
throw new Error("The installed Fuploader Python runtime failed final validation.");
|
|
276
|
+
}
|
|
237
277
|
if (movedOld) {
|
|
238
278
|
fs.rmSync(backup, { recursive: true, force: true });
|
|
239
279
|
}
|
|
240
280
|
} finally {
|
|
241
281
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
242
282
|
}
|
|
243
|
-
const created = inspectRuntime({ root, platform, requirementsHash, run });
|
|
244
|
-
if (!created) {
|
|
245
|
-
throw new Error("The installed Fuploader Python runtime failed final validation.");
|
|
246
|
-
}
|
|
247
283
|
return { status: "installed", root, requirements, python: created };
|
|
248
284
|
} finally {
|
|
249
285
|
lock.release();
|
package/npm/skill-manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "fupload.npm-skill-manifest.v1",
|
|
3
3
|
"package_name": "@follenfang/fupload",
|
|
4
|
-
"package_version": "0.0.
|
|
5
|
-
"skill_version": "0.0.
|
|
6
|
-
"tree_sha256": "
|
|
4
|
+
"package_version": "0.0.7",
|
|
5
|
+
"skill_version": "0.0.7",
|
|
6
|
+
"tree_sha256": "f2693bc26ecd3f9bab3ed691a2f372c5702b6ac5f72f52880fc6cee40ca90314",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "agents/openai.yaml",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
"path": "references/blackbox.md",
|
|
80
|
-
"bytes":
|
|
81
|
-
"sha256": "
|
|
80
|
+
"bytes": 2966,
|
|
81
|
+
"sha256": "ea95e8826af8e54c82343c404f9c97a611ab8ec3fb0282b561907c50169e4307"
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"path": "references/curseforge.md",
|
|
@@ -108,22 +108,22 @@
|
|
|
108
108
|
{
|
|
109
109
|
"path": "scripts/fupload_cli/__init__.py",
|
|
110
110
|
"bytes": 49,
|
|
111
|
-
"sha256": "
|
|
111
|
+
"sha256": "b44c52f9e6b2989921c93580a400ddcd3dd3e5c1cc787e9984baf0dabdfd93e7"
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
|
-
"path": "scripts/fupload_cli/
|
|
115
|
-
"bytes":
|
|
116
|
-
"sha256": "
|
|
114
|
+
"path": "scripts/fupload_cli/blackbox_web.py",
|
|
115
|
+
"bytes": 19392,
|
|
116
|
+
"sha256": "4a849fb297f045b81d261f4456542c165a3c0d9a77f963dbfcbc1c9dad8fec65"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"path": "scripts/fupload_cli/blackbox.py",
|
|
120
|
-
"bytes":
|
|
121
|
-
"sha256": "
|
|
120
|
+
"bytes": 17089,
|
|
121
|
+
"sha256": "4487141898ba6a080d2120c1df9ad83c290528e9b462acb22d6c0f89dbbd117c"
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
"path": "scripts/fupload_cli/cli.py",
|
|
125
|
-
"bytes":
|
|
126
|
-
"sha256": "
|
|
125
|
+
"bytes": 25982,
|
|
126
|
+
"sha256": "79c5de939e3a7e7d0ed49d79fcce6c22e78c1a5abcf8eae5422dbd78f7c7bf2d"
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"path": "scripts/fupload_cli/curseforge.py",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "scripts/fupload_cli/schema.py",
|
|
170
|
-
"bytes":
|
|
171
|
-
"sha256": "
|
|
170
|
+
"bytes": 37399,
|
|
171
|
+
"sha256": "5538eaf099bf9e7cae5f8142faecf827d41da698ae574b5597d17756da23e331"
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
"path": "scripts/fupload_cli/transport.py",
|
|
@@ -187,8 +187,8 @@
|
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "SKILL.md",
|
|
190
|
-
"bytes":
|
|
191
|
-
"sha256": "
|
|
190
|
+
"bytes": 23242,
|
|
191
|
+
"sha256": "941f2c5f615dded9fcfe086516a9bf903f54db52351815c38d26c2691f71793b"
|
|
192
192
|
}
|
|
193
193
|
]
|
|
194
194
|
}
|
package/package.json
CHANGED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""Authentication helpers for the Heybox desktop Workshop client."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import hashlib, json, platform, secrets, sqlite3, time
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from urllib.parse import parse_qs, urlparse
|
|
6
|
-
from .errors import FuploadError
|
|
7
|
-
|
|
8
|
-
API_BASE = "https://workshopapi.xiaoheihe.cn"
|
|
9
|
-
CLIENT_VERSION = "1.14.1"
|
|
10
|
-
_CONFIG_AES_KEY = bytes.fromhex("5f1d7f11e6e90dbb5c2f0c1e614a6a8c4b9e16b50fa724e4c54d6f25b1208b93")
|
|
11
|
-
_ALPHABET = "AB45STUVWZEFGJ6CH01D237IXYPQRKLMN89"
|
|
12
|
-
|
|
13
|
-
def _n8(value: str, limit: int) -> str:
|
|
14
|
-
table = _ALPHABET[:limit]
|
|
15
|
-
return "".join(table[ord(ch) % len(table)] for ch in value)
|
|
16
|
-
def _i8(value: str) -> str:
|
|
17
|
-
return "".join(_ALPHABET[ord(ch) % len(_ALPHABET)] for ch in value)
|
|
18
|
-
def _interleave(parts):
|
|
19
|
-
return "".join(part[i] for i in range(max(map(len, parts))) for part in parts if i < len(part))
|
|
20
|
-
def _mix(values):
|
|
21
|
-
def p(x): return ((x << 1) ^ 27) & 255 if x & 128 else (x << 1) & 255
|
|
22
|
-
def hm(x): return p(x) ^ x
|
|
23
|
-
def qg(x): return hm(p(x))
|
|
24
|
-
def dx(x): return qg(hm(p(x)))
|
|
25
|
-
def mw(x): return dx(x) ^ qg(x) ^ hm(x)
|
|
26
|
-
a,b,c,d,*rest=values
|
|
27
|
-
return [mw(a)^dx(b)^qg(c)^hm(d), hm(a)^mw(b)^dx(c)^qg(d), qg(a)^hm(b)^mw(c)^dx(d), dx(a)^qg(b)^hm(c)^mw(d), *rest]
|
|
28
|
-
def hkey(path: str, timestamp: int, nonce: str) -> str:
|
|
29
|
-
normalized = "/" + "/".join(x for x in path.split("/") if x) + "/"
|
|
30
|
-
digest = hashlib.md5(_interleave([_n8(str(timestamp), -2), _i8(normalized), _i8(nonce)]).encode()).hexdigest()
|
|
31
|
-
return _n8(digest[:5], -4) + "%02d" % (sum(_mix([ord(x) for x in digest[-6:]])) % 100)
|
|
32
|
-
|
|
33
|
-
def _decrypt_user_pkey(value: str) -> str:
|
|
34
|
-
"""Decrypt the desktop config's ``iv:ciphertext`` AES-256-CBC value.
|
|
35
|
-
|
|
36
|
-
Older Chromium cookie databases contain the already decrypted pkey, so this
|
|
37
|
-
helper deliberately returns ordinary values unchanged and only attempts
|
|
38
|
-
crypto for the new config format.
|
|
39
|
-
"""
|
|
40
|
-
if not isinstance(value, str) or ":" not in value:
|
|
41
|
-
return value
|
|
42
|
-
iv_hex, ciphertext_hex = value.split(":", 1)
|
|
43
|
-
try:
|
|
44
|
-
iv = bytes.fromhex(iv_hex)
|
|
45
|
-
ciphertext = bytes.fromhex(ciphertext_hex)
|
|
46
|
-
if len(iv) != 16 or not ciphertext or len(ciphertext) % 16:
|
|
47
|
-
return value
|
|
48
|
-
except ValueError:
|
|
49
|
-
return value
|
|
50
|
-
try:
|
|
51
|
-
from Crypto.Cipher import AES
|
|
52
|
-
plaintext = AES.new(_CONFIG_AES_KEY, AES.MODE_CBC, iv).decrypt(ciphertext)
|
|
53
|
-
except ImportError:
|
|
54
|
-
try:
|
|
55
|
-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
56
|
-
decryptor = Cipher(algorithms.AES(_CONFIG_AES_KEY), modes.CBC(iv)).decryptor()
|
|
57
|
-
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
|
58
|
-
except ImportError as exc:
|
|
59
|
-
raise FuploadError(
|
|
60
|
-
"AES support is required to read the Heybox desktop session",
|
|
61
|
-
kind="environment_error",
|
|
62
|
-
details={"install": "Crypto or cryptography"},
|
|
63
|
-
) from exc
|
|
64
|
-
if not plaintext:
|
|
65
|
-
return value
|
|
66
|
-
pad = plaintext[-1]
|
|
67
|
-
if not 1 <= pad <= 16 or plaintext[-pad:] != bytes([pad]) * pad:
|
|
68
|
-
raise FuploadError("Heybox desktop pkey could not be decrypted", kind="authentication_error")
|
|
69
|
-
try:
|
|
70
|
-
return plaintext[:-pad].decode("utf-8")
|
|
71
|
-
except UnicodeDecodeError as exc:
|
|
72
|
-
raise FuploadError("Heybox desktop pkey is invalid", kind="authentication_error") from exc
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _read_json(path: Path) -> dict:
|
|
76
|
-
try:
|
|
77
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
78
|
-
except (OSError, UnicodeError, ValueError):
|
|
79
|
-
return {}
|
|
80
|
-
return data if isinstance(data, dict) else {}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _load_cookie_db(profile: Path) -> dict[str, str]:
|
|
84
|
-
db = profile / "Network/Cookies"
|
|
85
|
-
try:
|
|
86
|
-
with sqlite3.connect("file:%s?mode=ro" % db, uri=True) as con:
|
|
87
|
-
rows = con.execute("select name,value from cookies where host_key like '%xiaoheihe.cn'").fetchall()
|
|
88
|
-
except (OSError, sqlite3.Error):
|
|
89
|
-
return {}
|
|
90
|
-
return {str(k): str(v) for k, v in rows if k in {"user_heybox_id", "user_pkey", "x_xhh_tokenid"} and v}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _read_sentry_identity(profile: Path) -> dict[str, str]:
|
|
94
|
-
identity: dict[str, str] = {}
|
|
95
|
-
scope = profile / "sentry/scope_v3.json"
|
|
96
|
-
try:
|
|
97
|
-
crumbs = _read_json(scope).get("scope", {}).get("breadcrumbs", [])
|
|
98
|
-
for crumb in reversed(crumbs):
|
|
99
|
-
raw = crumb.get("data",{}).get("url","")
|
|
100
|
-
if "x_app=heybox_pc" in raw:
|
|
101
|
-
query = parse_qs(urlparse(raw).query)
|
|
102
|
-
allowed = {
|
|
103
|
-
"app", "client_type", "device_id", "exe_version", "heybox_id",
|
|
104
|
-
"os_type", "os_version", "version", "web_version", "x_app",
|
|
105
|
-
"x_client_type", "x_client_version", "x_os_type",
|
|
106
|
-
}
|
|
107
|
-
identity = {k: v[-1] for k, v in query.items() if k in allowed and v}
|
|
108
|
-
break
|
|
109
|
-
except (OSError, ValueError, TypeError):
|
|
110
|
-
pass
|
|
111
|
-
return identity
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def load_session(profile: Path | None = None):
|
|
115
|
-
"""Load the current desktop session without exposing credential material.
|
|
116
|
-
|
|
117
|
-
Heybox 1.14 stores its authoritative session in ``config.json``. The
|
|
118
|
-
Chromium database is retained as a compatibility fallback because older
|
|
119
|
-
installations and upgrade paths may not have written the new config yet.
|
|
120
|
-
"""
|
|
121
|
-
profile = profile or Path.home() / "AppData/Roaming/heybox-pc-launcher"
|
|
122
|
-
config = _read_json(profile / "config.json")
|
|
123
|
-
db_cookies = _load_cookie_db(profile)
|
|
124
|
-
cookies: dict[str, str] = dict(db_cookies)
|
|
125
|
-
config_cookies = config.get("cookies")
|
|
126
|
-
if isinstance(config_cookies, list):
|
|
127
|
-
for item in config_cookies:
|
|
128
|
-
if not isinstance(item, dict):
|
|
129
|
-
continue
|
|
130
|
-
name, value = item.get("name"), item.get("value")
|
|
131
|
-
if isinstance(name, str) and isinstance(value, str) and value:
|
|
132
|
-
if name in {"user_heybox_id", "user_pkey", "x_xhh_tokenid"}:
|
|
133
|
-
cookies[name] = value
|
|
134
|
-
acc_config = config.get("acc_config")
|
|
135
|
-
if isinstance(acc_config, dict) and acc_config.get("xhh_token_id"):
|
|
136
|
-
cookies["x_xhh_tokenid"] = str(acc_config["xhh_token_id"])
|
|
137
|
-
if cookies.get("user_pkey"):
|
|
138
|
-
cookies["user_pkey"] = _decrypt_user_pkey(cookies["user_pkey"])
|
|
139
|
-
account = config.get("account") if isinstance(config.get("account"), dict) else {}
|
|
140
|
-
if not cookies.get("user_heybox_id") and account.get("heybox_id"):
|
|
141
|
-
cookies["user_heybox_id"] = str(account["heybox_id"])
|
|
142
|
-
if not {"user_heybox_id", "user_pkey", "x_xhh_tokenid"} <= set(cookies):
|
|
143
|
-
raise FuploadError("Heybox desktop login state is incomplete", kind="authentication_error")
|
|
144
|
-
|
|
145
|
-
identity = _read_sentry_identity(profile)
|
|
146
|
-
version = identity.get("version") or CLIENT_VERSION
|
|
147
|
-
identity = {
|
|
148
|
-
"x_client_type": "pc",
|
|
149
|
-
"x_os_type": "Windows",
|
|
150
|
-
"x_app": "heybox_pc",
|
|
151
|
-
"version": version,
|
|
152
|
-
"exe_version": identity.get("exe_version") or version,
|
|
153
|
-
"os_version": identity.get("os_version") or platform.platform(aliased=True),
|
|
154
|
-
**identity,
|
|
155
|
-
}
|
|
156
|
-
identity["version"] = version
|
|
157
|
-
identity["exe_version"] = identity.get("exe_version") or version
|
|
158
|
-
identity["heybox_id"] = cookies["user_heybox_id"]
|
|
159
|
-
return cookies, identity
|