@follenfang/fupload 0.0.4 → 0.0.6
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/README.md +2 -2
- package/fupload/SKILL.md +2 -2
- package/fupload/references/blackbox.md +1 -1
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/blackbox.py +78 -10
- package/fupload/scripts/fupload_cli/blackbox_auth.py +117 -12
- package/npm/bin/fupload.mjs +8 -6
- package/npm/lib/managed-install.mjs +7 -3
- package/npm/lib/python-requirements.txt +1 -0
- package/npm/lib/python.mjs +227 -0
- package/npm/lib/uninstall.mjs +12 -0
- package/npm/lib/update.mjs +13 -0
- package/npm/postinstall.mjs +4 -1
- package/npm/skill-manifest.json +12 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ fupload/
|
|
|
42
42
|
- 网易 DD:Windows、Python 3、已安装并登录官方客户端
|
|
43
43
|
- Heybox Workshop:Windows、Python 3、已安装并登录 Heybox 桌面客户端
|
|
44
44
|
|
|
45
|
-
Heybox ZIP
|
|
45
|
+
npm 安装和 `fupload update` 会在用户状态目录中创建 Fuploader 专用 Python venv,并自动安装 Heybox ZIP 上传所需的腾讯官方 COS SDK;不会修改系统 Python。使用 `npm --ignore-scripts` 安装时,首次运行实际 CLI 命令会校验并补齐该 runtime。
|
|
46
46
|
|
|
47
47
|
官方 `ncc` 的安装命令为 `npm i -g @newbeebox/newbeebox-creator-center-cli@latest`。Fuploader 不读取其凭据文件;用户在自己的终端完成 `ncc login`,Agent 仅用 `ncc whoami -o json` 验证。自动化可在启动 Agent 前注入 `NCC_TOKEN`,令牌不得写入命令参数、项目文件或 Git。
|
|
48
48
|
|
|
@@ -169,7 +169,7 @@ fupload uninstall
|
|
|
169
169
|
|
|
170
170
|
`fupload update` 固定安装 `@follenfang/fupload@latest`,随后把默认位置和所有登记有效的自定义 Skill 同步到同一版本。未知目录不会被覆盖。
|
|
171
171
|
|
|
172
|
-
`fupload uninstall` 先删除所有仍有有效 npm 管理标记的 Fuploader Skill,再调用 npm 删除 `@follenfang/fupload` 和 `fupload` 命令。Skill 清理失败时 npm 包保持可用,处理占用或权限问题后可重试。项目中的 `publish/`、新手盒子/DD 登录数据、DD 日志和 `~/.fupload/curseforge.env` 始终保留。
|
|
172
|
+
`fupload uninstall` 先删除所有仍有有效 npm 管理标记的 Fuploader Skill 和专用 Python runtime,再调用 npm 删除 `@follenfang/fupload` 和 `fupload` 命令。Skill 或 runtime 清理失败时 npm 包保持可用,处理占用或权限问题后可重试。项目中的 `publish/`、新手盒子/DD 登录数据、DD 日志和 `~/.fupload/curseforge.env` 始终保留。
|
|
173
173
|
|
|
174
174
|
npm 7 及以上不执行卸载 lifecycle,因此直接运行 `npm uninstall -g @follenfang/fupload` 只删除 npm 包和 CLI,可能留下 Skill;它不属于完整卸载流程。
|
|
175
175
|
|
package/fupload/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: fupload
|
|
3
3
|
description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox, NetEase DD, CurseForge, and Heybox Workshop, including CurseForge author project lookup, plugin ZIP upload, and Heybox client-session version management. Use only when the user explicitly invokes `$fupload`, explicitly asks to use the Fupload Skill, or loads this Skill by path. Do not trigger from ordinary mentions of publishing, NewBeeBox, DD, CurseForge, Heybox, plugins, configurations, or WA.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.0.
|
|
5
|
+
version: "0.0.6"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Fupload
|
|
@@ -60,7 +60,7 @@ Run `<fupload-cli> --help` from the user's project. The npm launcher resolves an
|
|
|
60
60
|
|
|
61
61
|
When the npm command is absent and the source/direct-Skill fallback is required, resolve `<skill-root>` from this `SKILL.md` and use `python <skill-root>/scripts/fupload.py`. On Windows, prefer `py -3` only if `python` is unavailable. Do not use a binary, Go source, repository-relative fallback, browser automation, Computer Use, or direct handcrafted HTTP calls. NewBeeBox's official `ncc` is not this bundled CLI and must not be wrapped as a Python `--input` command.
|
|
62
62
|
|
|
63
|
-
For npm package maintenance, use `fupload update` to update the CLI and every registered managed Skill to `@follenfang/fupload@latest`. For a complete removal, use `fupload uninstall`; it removes every registered Skill that still has a valid Fuploader npm ownership marker, then
|
|
63
|
+
For npm package maintenance, use `fupload update` to update the CLI, managed Python runtime, and every registered managed Skill to `@follenfang/fupload@latest`. The npm installer and launcher create a Fuploader-only venv and install the pinned Python dependencies there; never ask for a system-wide `pip install`. For a complete removal, use `fupload uninstall`; it removes every registered Skill that still has a valid Fuploader npm ownership marker, the managed Python runtime, then the npm package and command. Do not use direct `npm install -g` or `npm uninstall -g` as the normal Agent-managed update/removal workflow.
|
|
64
64
|
|
|
65
65
|
## Load only the needed contract
|
|
66
66
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The blackbox provider reuses the signed-in Heybox desktop client's local login state. It never accepts credentials, cookies, tokens, signatures, or COS temporary credentials as input.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The npm installer manages Tencent's official COS SDK in a Fuploader-only Python venv. `fupload update` synchronizes it, the launcher repairs a missing or stale runtime before executing the Python CLI, and `fupload uninstall` removes it. No system-wide `pip install` is required.
|
|
6
6
|
|
|
7
7
|
## `blackbox plugin edit`
|
|
8
8
|
|
|
@@ -6,19 +6,35 @@ from urllib.parse import urlencode
|
|
|
6
6
|
from urllib.request import Request, urlopen
|
|
7
7
|
from typing import Any, Mapping
|
|
8
8
|
from .errors import FuploadError, ValidationError
|
|
9
|
-
from .blackbox_auth import API_BASE, hkey, load_session
|
|
9
|
+
from .blackbox_auth import API_BASE, CLIENT_VERSION, hkey, load_session
|
|
10
|
+
|
|
11
|
+
API_MISC_BASE = "https://api.xiaoheihe.cn"
|
|
10
12
|
|
|
11
13
|
class Blackbox:
|
|
12
14
|
def __init__(self, config: Mapping[str, Any] | None = None, transport=None):
|
|
13
15
|
self.config = dict(config or {})
|
|
14
16
|
self.profile = Path(self.config.get("client_profile") or Path.home() / "AppData/Roaming/heybox-pc-launcher")
|
|
15
17
|
self._transport = transport
|
|
16
|
-
def _request(self, method, path, body=None, query=None):
|
|
18
|
+
def _request(self, method, path, body=None, query=None, *, base=API_BASE):
|
|
17
19
|
if self._transport: return self._transport(method, path, body or {}, query or {})
|
|
18
20
|
cookies, identity = load_session(self.profile); ts=int(time.time()); nonce=hashlib.md5((str(ts)+secrets.token_hex(16)).encode()).hexdigest().upper()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
version = identity.get("version") or CLIENT_VERSION
|
|
22
|
+
params={
|
|
23
|
+
"app": "heybox",
|
|
24
|
+
"os_type": "Windows",
|
|
25
|
+
"web_version": "",
|
|
26
|
+
**identity,
|
|
27
|
+
"version": version,
|
|
28
|
+
"hkey": hkey(path,ts + 1,nonce),
|
|
29
|
+
"_time": ts,
|
|
30
|
+
"_chat_time": int(time.time()*1000),
|
|
31
|
+
"nonce": nonce,
|
|
32
|
+
**(query or {}),
|
|
33
|
+
}
|
|
34
|
+
json_body = base == API_MISC_BASE or path.startswith("/bbs/app/api/qcloud/")
|
|
35
|
+
headers={"Referer":"https://chat.xiaoheihe.cn","User-Agent":f"HeyboxApp/{identity.get('exe_version') or version}","x_xhh_tokenid":cookies["x_xhh_tokenid"],"Cookie":" ".join(f"{k}={v};" for k,v in cookies.items()),"Content-Type":("application/json;charset=utf-8" if json_body else "application/x-www-form-urlencoded;charset=utf-8")}
|
|
36
|
+
url=base.rstrip("/")+path+"?"+urlencode(params)
|
|
37
|
+
data=(json.dumps(body or {}, separators=(",", ":")).encode() if json_body else urlencode(body or {},doseq=True).encode()) if method=="POST" else None
|
|
22
38
|
try:
|
|
23
39
|
with urlopen(Request(url,data=data,headers=headers,method=method),timeout=60) as r: result=json.loads(r.read().decode())
|
|
24
40
|
except Exception as exc: raise FuploadError("Workshop API request failed", endpoint=path, verification_required=method=="POST") from exc
|
|
@@ -72,8 +88,10 @@ class Blackbox:
|
|
|
72
88
|
def version_upsert(self,doc):
|
|
73
89
|
aliases={"module_id":"moduleId","version_id":"versionId","game_versions":"gameVersions","file_url":"fileUrl"}
|
|
74
90
|
normalized={aliases.get(k,k):v for k,v in doc.items() if k not in {"schema","dry_run"}}
|
|
91
|
+
upload_result = None
|
|
75
92
|
if "file" in normalized and "fileUrl" not in normalized:
|
|
76
|
-
|
|
93
|
+
upload_result=self.upload_zip(int(normalized["moduleId"]),str(normalized["file"]))
|
|
94
|
+
normalized["fileUrl"]=upload_result["url"]
|
|
77
95
|
if "fileUrl" not in normalized and "versionId" in normalized:
|
|
78
96
|
existing = self._find_version(int(normalized["moduleId"]), int(normalized["versionId"]))
|
|
79
97
|
if existing and existing.get("fileUrlHeybox"):
|
|
@@ -91,7 +109,10 @@ class Blackbox:
|
|
|
91
109
|
if not found.get("fileUrlHeybox"):
|
|
92
110
|
mismatches.append("fileUrl")
|
|
93
111
|
if mismatches: raise FuploadError("version upsert readback mismatch",kind="verification_required",verification_required=True,details={"fields":mismatches})
|
|
94
|
-
|
|
112
|
+
result={"accepted":True,"verified":True,"module_id":body["moduleId"],"version_id":found.get("id"),"readback":self._redact(found),"response":self._redact(response)}
|
|
113
|
+
if upload_result:
|
|
114
|
+
result["upload"]={key:upload_result[key] for key in ("protocol","bytes","sha256") if key in upload_result}
|
|
115
|
+
return result
|
|
95
116
|
def version_delete(self,doc):
|
|
96
117
|
version_id=doc.get("versionId",doc.get("version_id")); module_id=doc.get("moduleId",doc.get("module_id"))
|
|
97
118
|
if version_id is None or module_id is None: raise ValidationError("versionId and moduleId are required")
|
|
@@ -133,6 +154,53 @@ class Blackbox:
|
|
|
133
154
|
path=Path(file_path)
|
|
134
155
|
if not path.is_file(): raise ValidationError("file does not exist",path="$.file")
|
|
135
156
|
if dry_run: return {"dry_run":True,"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
157
|
+
try:
|
|
158
|
+
return self._upload_zip_v2(path)
|
|
159
|
+
except FuploadError:
|
|
160
|
+
# The web creator still exposes the legacy Workshop token route.
|
|
161
|
+
# Fall back to it when the desktop generic uploader is unavailable.
|
|
162
|
+
return self._upload_zip_legacy(path)
|
|
163
|
+
|
|
164
|
+
def _upload_zip_v2(self, path: Path):
|
|
165
|
+
file_info = {"name": path.name, "mimetype": "application/zip", "fsize": path.stat().st_size}
|
|
166
|
+
info = self._result(self._request(
|
|
167
|
+
"POST", "/bbs/app/api/qcloud/cos/upload/info/v2", base=API_MISC_BASE,
|
|
168
|
+
body={"file_infos": json.dumps([file_info], separators=(",", ":")), "scope": "any", "need_cache": 0},
|
|
169
|
+
))
|
|
170
|
+
keys = info.get("keys") or []
|
|
171
|
+
if not keys or not info.get("bucket"):
|
|
172
|
+
raise FuploadError("Current COS upload info response is incomplete", endpoint="/bbs/app/api/qcloud/cos/upload/info/v2", verification_required=True)
|
|
173
|
+
key = str(keys[0])
|
|
174
|
+
token = self._result(self._request(
|
|
175
|
+
"POST", "/bbs/app/api/qcloud/cos/upload/token/v2", base=API_MISC_BASE,
|
|
176
|
+
body={"bucket": info["bucket"], "keys": json.dumps([key], separators=(",", ":")), "mimetypes": json.dumps([file_info["mimetype"]]), "is_multipart_upload": 0},
|
|
177
|
+
))
|
|
178
|
+
credentials = token.get("credentials") or {}
|
|
179
|
+
secret_id = credentials.get("tmpSecretId") or credentials.get("TmpSecretID")
|
|
180
|
+
secret_key = credentials.get("tmpSecretKey") or credentials.get("TmpSecretKey")
|
|
181
|
+
session_token = credentials.get("sessionToken") or credentials.get("Token")
|
|
182
|
+
if not all((secret_id, secret_key, session_token)):
|
|
183
|
+
raise FuploadError("Current COS upload credentials are incomplete", endpoint="/bbs/app/api/qcloud/cos/upload/token/v2", verification_required=True)
|
|
184
|
+
try:
|
|
185
|
+
from qcloud_cos import CosConfig, CosS3Client
|
|
186
|
+
except ModuleNotFoundError as exc:
|
|
187
|
+
raise FuploadError("The managed Fuploader Python runtime is missing the Heybox COS SDK", kind="environment_error", details={"repair_command":"fupload update"}) from exc
|
|
188
|
+
try:
|
|
189
|
+
cos=CosS3Client(CosConfig(Region=info.get("region") or "ap-shanghai",SecretId=secret_id,SecretKey=secret_key,Token=session_token,Scheme="https"))
|
|
190
|
+
with path.open("rb") as stream: cos.put_object(Bucket=info["bucket"],Key=key,Body=stream)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
raise FuploadError("COS upload failed", endpoint="/bbs/app/api/qcloud/cos/upload/token/v2", verification_required=True) from exc
|
|
193
|
+
callback = self._result(self._request(
|
|
194
|
+
"POST", "/bbs/app/api/qcloud/cos/upload/callback/v2", base=API_MISC_BASE,
|
|
195
|
+
query={"is_finished": "true"}, body={"keys": json.dumps([key], separators=(",", ":"))},
|
|
196
|
+
))
|
|
197
|
+
urls = callback.get("preview_urls") or callback.get("previewUrls") or []
|
|
198
|
+
url = urls[0] if urls else callback.get("url")
|
|
199
|
+
if not url:
|
|
200
|
+
raise FuploadError("Current COS upload callback did not return a URL", endpoint="/bbs/app/api/qcloud/cos/upload/callback/v2", verification_required=True)
|
|
201
|
+
return {"url":url,"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest(),"protocol":"v2"}
|
|
202
|
+
|
|
203
|
+
def _upload_zip_legacy(self, path: Path):
|
|
136
204
|
size_mb=path.stat().st_size/1024/1024
|
|
137
205
|
token=self._result(self._request("POST","/wow/cos/upload/token/",body={"jsonData":json.dumps({"upload_infos":[{"filename":path.name,"file_size":size_mb,"type":"module"}]},separators=(",",":"))}))
|
|
138
206
|
info=token["info"]; f=token["files"][0]; creds=info["Credentials"].get("Credentials",info["Credentials"])
|
|
@@ -140,12 +208,12 @@ class Blackbox:
|
|
|
140
208
|
from qcloud_cos import CosConfig, CosS3Client
|
|
141
209
|
except ModuleNotFoundError as exc:
|
|
142
210
|
raise FuploadError(
|
|
143
|
-
"
|
|
211
|
+
"The managed Fuploader Python runtime is missing the Heybox COS SDK",
|
|
144
212
|
kind="environment_error",
|
|
145
|
-
details={"
|
|
213
|
+
details={"repair_command":"fupload update"},
|
|
146
214
|
) from exc
|
|
147
215
|
try:
|
|
148
216
|
cos=CosS3Client(CosConfig(Region=info.get("region") or "ap-shanghai",SecretId=creds["TmpSecretID"],SecretKey=creds["TmpSecretKey"],Token=creds["Token"],Scheme="https"))
|
|
149
217
|
with path.open("rb") as stream: cos.put_object(Bucket=info["bucket"],Key=f["key"],Body=stream)
|
|
150
218
|
except Exception as exc: raise FuploadError("COS upload failed",verification_required=True) from exc
|
|
151
|
-
return {"url":"https://%s/%s"%(info["host"],f["key"]),"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
219
|
+
return {"url":"https://%s/%s"%(info["host"],f["key"]),"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest(),"protocol":"legacy"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Authentication helpers for the Heybox desktop Workshop client."""
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
import hashlib, json, secrets, sqlite3, time
|
|
3
|
+
import hashlib, json, platform, secrets, sqlite3, time
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from urllib.parse import parse_qs, urlparse
|
|
6
6
|
from .errors import FuploadError
|
|
7
7
|
|
|
8
8
|
API_BASE = "https://workshopapi.xiaoheihe.cn"
|
|
9
|
+
CLIENT_VERSION = "1.14.1"
|
|
10
|
+
_CONFIG_AES_KEY = bytes.fromhex("5f1d7f11e6e90dbb5c2f0c1e614a6a8c4b9e16b50fa724e4c54d6f25b1208b93")
|
|
9
11
|
_ALPHABET = "AB45STUVWZEFGJ6CH01D237IXYPQRKLMN89"
|
|
10
12
|
|
|
11
13
|
def _n8(value: str, limit: int) -> str:
|
|
@@ -28,27 +30,130 @@ def hkey(path: str, timestamp: int, nonce: str) -> str:
|
|
|
28
30
|
digest = hashlib.md5(_interleave([_n8(str(timestamp), -2), _i8(normalized), _i8(nonce)]).encode()).hexdigest()
|
|
29
31
|
return _n8(digest[:5], -4) + "%02d" % (sum(_mix([ord(x) for x in digest[-6:]])) % 100)
|
|
30
32
|
|
|
31
|
-
def
|
|
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]:
|
|
33
84
|
db = profile / "Network/Cookies"
|
|
34
85
|
try:
|
|
35
86
|
with sqlite3.connect("file:%s?mode=ro" % db, uri=True) as con:
|
|
36
87
|
rows = con.execute("select name,value from cookies where host_key like '%xiaoheihe.cn'").fetchall()
|
|
37
|
-
except (OSError, sqlite3.Error)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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] = {}
|
|
43
95
|
scope = profile / "sentry/scope_v3.json"
|
|
44
96
|
try:
|
|
45
|
-
crumbs =
|
|
97
|
+
crumbs = _read_json(scope).get("scope", {}).get("breadcrumbs", [])
|
|
46
98
|
for crumb in reversed(crumbs):
|
|
47
99
|
raw = crumb.get("data",{}).get("url","")
|
|
48
100
|
if "x_app=heybox_pc" in raw:
|
|
49
101
|
query = parse_qs(urlparse(raw).query)
|
|
50
|
-
allowed = {
|
|
51
|
-
|
|
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
|
|
52
109
|
except (OSError, ValueError, TypeError):
|
|
53
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"]
|
|
54
159
|
return cookies, identity
|
package/npm/bin/fupload.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { ensureCurseForgeEnv } from "../lib/curseforge-config.mjs";
|
|
7
7
|
import { recordManagedSkill } from "../lib/managed-install.mjs";
|
|
8
8
|
import { parseLauncherOptions, resolveSkillDirectory } from "../lib/options.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { ensurePythonRuntime, runPython } from "../lib/python.mjs";
|
|
10
10
|
import { ensureSkill } from "../lib/skill-installer.mjs";
|
|
11
11
|
import { uninstallSelf } from "../lib/uninstall.mjs";
|
|
12
12
|
import { updateSelf } from "../lib/update.mjs";
|
|
@@ -65,16 +65,18 @@ async function main() {
|
|
|
65
65
|
return 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
let runtime;
|
|
69
|
+
try {
|
|
70
|
+
runtime = ensurePythonRuntime({ packageRoot });
|
|
71
|
+
} catch (error) {
|
|
70
72
|
emitError(
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
+
"PYTHON_RUNTIME_SETUP_FAILED",
|
|
74
|
+
error.message,
|
|
73
75
|
);
|
|
74
76
|
return 1;
|
|
75
77
|
}
|
|
76
78
|
const script = path.join(target, "scripts", "fupload.py");
|
|
77
|
-
const result = runPython(python, script, options.forwarded);
|
|
79
|
+
const result = runPython(runtime.python, script, options.forwarded);
|
|
78
80
|
if (result.error) {
|
|
79
81
|
emitError("PYTHON_LAUNCH_FAILED", result.error.message);
|
|
80
82
|
return 1;
|
|
@@ -6,11 +6,15 @@ export const MANAGED_INSTALL_SCHEMA = "fupload.npm-managed-install.v1";
|
|
|
6
6
|
export const MANAGED_INSTALL_FILE = "managed-install.json";
|
|
7
7
|
const PACKAGE_NAME = "@follenfang/fupload";
|
|
8
8
|
|
|
9
|
-
function
|
|
9
|
+
export function productStateRoot({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
10
10
|
if (platform === "win32") {
|
|
11
|
-
return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Fupload"
|
|
11
|
+
return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Fupload");
|
|
12
12
|
}
|
|
13
|
-
return path.join(env.XDG_STATE_HOME || path.join(home, ".local", "state"), "fupload"
|
|
13
|
+
return path.join(env.XDG_STATE_HOME || path.join(home, ".local", "state"), "fupload");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stateRoot(options = {}) {
|
|
17
|
+
return path.join(productStateRoot(options), "npm");
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
function atomicWriteJson(filename, value) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cos-python-sdk-v5==1.9.44
|
package/npm/lib/python.mjs
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { productStateRoot } from "./managed-install.mjs";
|
|
8
|
+
|
|
9
|
+
export const PYTHON_RUNTIME_SCHEMA = "fupload.python-runtime.v1";
|
|
10
|
+
const RUNTIME_DIRECTORY = "python";
|
|
11
|
+
const RUNTIME_MARKER = "runtime.json";
|
|
12
|
+
const RUNTIME_LOCK_WAIT_MS = 5 * 60 * 1000;
|
|
13
|
+
const RUNTIME_LOCK_STALE_MS = 15 * 60 * 1000;
|
|
2
14
|
|
|
3
15
|
function probe(command, args, minimum) {
|
|
4
16
|
const result = spawnSync(command, [...args, "-c", "import sys; print('.'.join(map(str, sys.version_info[:3])))"], {
|
|
@@ -43,3 +55,218 @@ export function runPython(python, script, args, options = {}) {
|
|
|
43
55
|
windowsHide: true,
|
|
44
56
|
});
|
|
45
57
|
}
|
|
58
|
+
|
|
59
|
+
export function pythonRequirementsFile(packageRoot) {
|
|
60
|
+
return path.join(packageRoot, "npm", "lib", "python-requirements.txt");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function pythonRuntimeRoot(options = {}) {
|
|
64
|
+
return path.join(productStateRoot(options), RUNTIME_DIRECTORY);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pythonRuntimeExecutable(root, platform = process.platform) {
|
|
68
|
+
return platform === "win32"
|
|
69
|
+
? path.join(root, "Scripts", "python.exe")
|
|
70
|
+
: path.join(root, "bin", "python");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sha256(buffer) {
|
|
74
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readMarker(root) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(fs.readFileSync(path.join(root, RUNTIME_MARKER), "utf8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function probeRuntime(executable, run = spawnSync) {
|
|
86
|
+
if (!fs.existsSync(executable)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const result = run(executable, [
|
|
90
|
+
"-c",
|
|
91
|
+
"import importlib.metadata,sys; import qcloud_cos; print('.'.join(map(str,sys.version_info[:3]))); print(importlib.metadata.version('cos-python-sdk-v5'))",
|
|
92
|
+
], { encoding: "utf8", shell: false, windowsHide: true });
|
|
93
|
+
if (result.status !== 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const [pythonVersion, dependencyVersion] = result.stdout.trim().split(/\r?\n/);
|
|
97
|
+
const match = pythonVersion?.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
98
|
+
if (!match || !dependencyVersion) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return { version: match.slice(1).map(Number), dependencyVersion };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function inspectRuntime({ root, platform, requirementsHash, run }) {
|
|
105
|
+
const marker = readMarker(root);
|
|
106
|
+
if (marker?.schema !== PYTHON_RUNTIME_SCHEMA || marker.requirements_sha256 !== requirementsHash) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const command = pythonRuntimeExecutable(root, platform);
|
|
110
|
+
const probe = probeRuntime(command, run);
|
|
111
|
+
if (!probe || probe.version[0] !== 3 || probe.version[1] < 9) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return { command, args: [], version: probe.version, dependencyVersion: probe.dependencyVersion };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function bounded(value) {
|
|
118
|
+
return String(value || "").trim().slice(0, 4000);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function runChecked(run, command, args, message) {
|
|
122
|
+
const result = run(command, args, {
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
shell: false,
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
});
|
|
127
|
+
if (result.error || result.status !== 0) {
|
|
128
|
+
const detail = bounded(result.stderr || result.stdout || result.error?.message);
|
|
129
|
+
throw new Error(`${message}${detail ? `: ${detail}` : ""}`);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeMarker(root, value) {
|
|
135
|
+
fs.writeFileSync(path.join(root, RUNTIME_MARKER), `${JSON.stringify(value, null, 2)}\n`, {
|
|
136
|
+
encoding: "utf8",
|
|
137
|
+
mode: 0o600,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sleepSync(milliseconds) {
|
|
142
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function acquireRuntimeLock(root) {
|
|
146
|
+
const lock = `${root}.lock`;
|
|
147
|
+
fs.mkdirSync(path.dirname(root), { recursive: true });
|
|
148
|
+
const deadline = Date.now() + RUNTIME_LOCK_WAIT_MS;
|
|
149
|
+
while (Date.now() < deadline) {
|
|
150
|
+
try {
|
|
151
|
+
const descriptor = fs.openSync(lock, "wx", 0o600);
|
|
152
|
+
fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid })}\n`);
|
|
153
|
+
return {
|
|
154
|
+
release() {
|
|
155
|
+
fs.closeSync(descriptor);
|
|
156
|
+
fs.rmSync(lock, { force: true });
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code !== "EEXIST") {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > RUNTIME_LOCK_STALE_MS) {
|
|
165
|
+
fs.rmSync(lock, { force: true });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
} catch (statError) {
|
|
169
|
+
if (statError.code !== "ENOENT") {
|
|
170
|
+
throw statError;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
sleepSync(50);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`Timed out waiting for the Fuploader Python runtime lock: ${lock}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function ensurePythonRuntime({
|
|
180
|
+
packageRoot,
|
|
181
|
+
platform = process.platform,
|
|
182
|
+
env = process.env,
|
|
183
|
+
home = os.homedir(),
|
|
184
|
+
run = spawnSync,
|
|
185
|
+
discover = discoverPython,
|
|
186
|
+
} = {}) {
|
|
187
|
+
const requirements = pythonRequirementsFile(packageRoot);
|
|
188
|
+
const content = fs.readFileSync(requirements);
|
|
189
|
+
const requirementsHash = sha256(content);
|
|
190
|
+
const root = pythonRuntimeRoot({ platform, env, home });
|
|
191
|
+
const parent = path.dirname(root);
|
|
192
|
+
const lock = acquireRuntimeLock(root);
|
|
193
|
+
try {
|
|
194
|
+
const current = inspectRuntime({ root, platform, requirementsHash, run });
|
|
195
|
+
if (current) {
|
|
196
|
+
return { status: "current", root, requirements, python: current };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const base = discover({ platform });
|
|
200
|
+
if (!base) {
|
|
201
|
+
throw new Error("Fuploader requires Python >=3.9,<4.0 to create its managed runtime.");
|
|
202
|
+
}
|
|
203
|
+
const nonce = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
|
|
204
|
+
const staging = path.join(parent, `.python-stage-${nonce}`);
|
|
205
|
+
const backup = path.join(parent, `.python-backup-${nonce}`);
|
|
206
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
207
|
+
let movedOld = false;
|
|
208
|
+
try {
|
|
209
|
+
runChecked(run, base.command, [...base.args, "-m", "venv", staging], "Could not create the Fuploader Python runtime");
|
|
210
|
+
const stagingPython = pythonRuntimeExecutable(staging, platform);
|
|
211
|
+
runChecked(run, stagingPython, [
|
|
212
|
+
"-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--requirement", requirements,
|
|
213
|
+
], "Could not install Fuploader Python dependencies");
|
|
214
|
+
const installed = probeRuntime(stagingPython, run);
|
|
215
|
+
if (!installed) {
|
|
216
|
+
throw new Error("The Fuploader Python runtime did not pass its dependency probe.");
|
|
217
|
+
}
|
|
218
|
+
writeMarker(staging, {
|
|
219
|
+
schema: PYTHON_RUNTIME_SCHEMA,
|
|
220
|
+
requirements_sha256: requirementsHash,
|
|
221
|
+
python_version: installed.version.join("."),
|
|
222
|
+
dependency_version: installed.dependencyVersion,
|
|
223
|
+
});
|
|
224
|
+
if (fs.existsSync(root)) {
|
|
225
|
+
fs.renameSync(root, backup);
|
|
226
|
+
movedOld = true;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
fs.renameSync(staging, root);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (movedOld && !fs.existsSync(root)) {
|
|
232
|
+
fs.renameSync(backup, root);
|
|
233
|
+
movedOld = false;
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
if (movedOld) {
|
|
238
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
242
|
+
}
|
|
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
|
+
return { status: "installed", root, requirements, python: created };
|
|
248
|
+
} finally {
|
|
249
|
+
lock.release();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function removePythonRuntime({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
254
|
+
const productRoot = productStateRoot({ platform, env, home });
|
|
255
|
+
const root = pythonRuntimeRoot({ platform, env, home });
|
|
256
|
+
const lock = acquireRuntimeLock(root);
|
|
257
|
+
try {
|
|
258
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
259
|
+
} finally {
|
|
260
|
+
lock.release();
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
if (fs.readdirSync(productRoot).length === 0) {
|
|
264
|
+
fs.rmdirSync(productRoot);
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY") {
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { root, status: "removed" };
|
|
272
|
+
}
|
package/npm/lib/uninstall.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import { clearManagedInstall, readManagedInstall } from "./managed-install.mjs";
|
|
7
7
|
import { resolveSkillDirectory } from "./options.mjs";
|
|
8
|
+
import { removePythonRuntime } from "./python.mjs";
|
|
8
9
|
import { INSTALL_STAMP, INSTALL_STAMP_SCHEMA, readInstallStamp } from "./skill-installer.mjs";
|
|
9
10
|
|
|
10
11
|
export const PACKAGE_NAME = "@follenfang/fupload";
|
|
@@ -173,6 +174,7 @@ export async function uninstallSelf({
|
|
|
173
174
|
env = process.env,
|
|
174
175
|
home = os.homedir(),
|
|
175
176
|
runNpm = runNpmCli,
|
|
177
|
+
removeRuntime = removePythonRuntime,
|
|
176
178
|
} = {}) {
|
|
177
179
|
const installation = resolveGlobalInstall(packageRoot, platform);
|
|
178
180
|
const cleanup = await cleanupManagedSkills({
|
|
@@ -181,6 +183,15 @@ export async function uninstallSelf({
|
|
|
181
183
|
home,
|
|
182
184
|
extraTargets: target ? [target] : [],
|
|
183
185
|
});
|
|
186
|
+
let runtime;
|
|
187
|
+
try {
|
|
188
|
+
runtime = removeRuntime({ platform, env, home });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new ManagedUninstallError("The managed Fuploader Python runtime could not be removed.", {
|
|
191
|
+
cleanup,
|
|
192
|
+
reason: error.message,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
184
195
|
const args = ["uninstall", "-g", "--ignore-scripts", "--prefix", installation.prefix, PACKAGE_NAME];
|
|
185
196
|
const npmResult = runNpm(args, { platform, env });
|
|
186
197
|
if (npmResult.error || npmResult.status !== 0) {
|
|
@@ -206,6 +217,7 @@ export async function uninstallSelf({
|
|
|
206
217
|
package: PACKAGE_NAME,
|
|
207
218
|
prefix: installation.prefix,
|
|
208
219
|
cleanup,
|
|
220
|
+
python_runtime: runtime,
|
|
209
221
|
npm_exit_status: npmResult.status,
|
|
210
222
|
};
|
|
211
223
|
}
|
package/npm/lib/update.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { ensureCurseForgeEnv } from "./curseforge-config.mjs";
|
|
5
5
|
import { readManagedInstall, recordManagedSkill } from "./managed-install.mjs";
|
|
6
6
|
import { resolveSkillDirectory } from "./options.mjs";
|
|
7
|
+
import { ensurePythonRuntime } from "./python.mjs";
|
|
7
8
|
import { ensureSkill, loadDistribution } from "./skill-installer.mjs";
|
|
8
9
|
import { PACKAGE_NAME, inspectManagedSkill, resolveGlobalInstall, runNpmCli } from "./uninstall.mjs";
|
|
9
10
|
|
|
@@ -27,6 +28,7 @@ export async function updateSelf({
|
|
|
27
28
|
env = process.env,
|
|
28
29
|
home = os.homedir(),
|
|
29
30
|
runNpm = runNpmCli,
|
|
31
|
+
ensureRuntime = ensurePythonRuntime,
|
|
30
32
|
} = {}) {
|
|
31
33
|
const curseforgeConfig = ensureCurseForgeEnv({ home, platform });
|
|
32
34
|
const installation = resolveGlobalInstall(packageRoot, platform);
|
|
@@ -57,6 +59,16 @@ export async function updateSelf({
|
|
|
57
59
|
reason: error.message,
|
|
58
60
|
});
|
|
59
61
|
}
|
|
62
|
+
let runtime;
|
|
63
|
+
try {
|
|
64
|
+
runtime = ensureRuntime({ packageRoot, platform, env, home });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new ManagedUpdateError("The npm CLI updated but its Python runtime could not be synchronized.", {
|
|
67
|
+
from_version: current.packageRecord.version,
|
|
68
|
+
to_version: updated.packageRecord.version,
|
|
69
|
+
reason: error.message,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
60
72
|
const registry = readManagedInstall({ platform, env, home });
|
|
61
73
|
const targets = [
|
|
62
74
|
...new Set([
|
|
@@ -97,6 +109,7 @@ export async function updateSelf({
|
|
|
97
109
|
to_version: updated.packageRecord.version,
|
|
98
110
|
npm_exit_status: npmResult.status,
|
|
99
111
|
curseforge_config: curseforgeConfig,
|
|
112
|
+
python_runtime: { status: runtime.status, root: runtime.root },
|
|
100
113
|
skills,
|
|
101
114
|
};
|
|
102
115
|
}
|
package/npm/postinstall.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { ensureCurseForgeEnv } from "./lib/curseforge-config.mjs";
|
|
5
5
|
import { recordManagedSkill } from "./lib/managed-install.mjs";
|
|
6
6
|
import { resolveSkillDirectory } from "./lib/options.mjs";
|
|
7
|
+
import { ensurePythonRuntime } from "./lib/python.mjs";
|
|
7
8
|
import { ensureSkill } from "./lib/skill-installer.mjs";
|
|
8
9
|
|
|
9
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -11,11 +12,13 @@ const target = resolveSkillDirectory();
|
|
|
11
12
|
|
|
12
13
|
try {
|
|
13
14
|
const config = ensureCurseForgeEnv();
|
|
15
|
+
const runtime = ensurePythonRuntime({ packageRoot });
|
|
14
16
|
const result = await ensureSkill({ packageRoot, target });
|
|
15
17
|
recordManagedSkill(target);
|
|
16
18
|
process.stdout.write(`Fuploader Skill ${result.status}: ${target}\n`);
|
|
17
19
|
process.stdout.write(`CurseForge configuration ${config.status}: ${config.path}\n`);
|
|
20
|
+
process.stdout.write(`Python runtime ${runtime.status}: ${runtime.root}\n`);
|
|
18
21
|
} catch (error) {
|
|
19
|
-
process.stderr.write(`Fuploader
|
|
22
|
+
process.stderr.write(`Fuploader installation failed: ${error.message}\n`);
|
|
20
23
|
process.exitCode = 1;
|
|
21
24
|
}
|
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.6",
|
|
5
|
+
"skill_version": "0.0.6",
|
|
6
|
+
"tree_sha256": "89170e9d93a44d9aa72a1c041f93d941609eb9d762375358090c5a96129405d9",
|
|
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": 2357,
|
|
81
|
+
"sha256": "022563a7611a5877761df71074ed347ef51fd27f735da7926cbb96d3b781dcc6"
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"path": "references/curseforge.md",
|
|
@@ -108,17 +108,17 @@
|
|
|
108
108
|
{
|
|
109
109
|
"path": "scripts/fupload_cli/__init__.py",
|
|
110
110
|
"bytes": 49,
|
|
111
|
-
"sha256": "
|
|
111
|
+
"sha256": "acf9b050750b58742981c5b9ae836149b89bb86daad01d18dd1948e71817d2c2"
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
"path": "scripts/fupload_cli/blackbox_auth.py",
|
|
115
|
-
"bytes":
|
|
116
|
-
"sha256": "
|
|
115
|
+
"bytes": 7211,
|
|
116
|
+
"sha256": "797ca228c57d2b845a7105f132891b71d3e494adcdb8f44027ce835125206177"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"path": "scripts/fupload_cli/blackbox.py",
|
|
120
|
-
"bytes":
|
|
121
|
-
"sha256": "
|
|
120
|
+
"bytes": 16666,
|
|
121
|
+
"sha256": "c6a793cacc7d9ceb8995cac62a076c1944345eaa113bed21068c75a05b1b0ee6"
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
"path": "scripts/fupload_cli/cli.py",
|
|
@@ -187,8 +187,8 @@
|
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "SKILL.md",
|
|
190
|
-
"bytes":
|
|
191
|
-
"sha256": "
|
|
190
|
+
"bytes": 22890,
|
|
191
|
+
"sha256": "af700f4e6eb924d955c8e5ae1205934c0bc08b425c85ce5f279eb0c630a0f77a"
|
|
192
192
|
}
|
|
193
193
|
]
|
|
194
194
|
}
|