@follenfang/fupload 0.0.16 → 0.0.18

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 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, Heybox Workshop, and ModUs.Creator, including local Creator login reuse and plugin ZIP publishing. 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, ModUs, plugins, configurations, or WA.
4
4
  metadata:
5
- version: "0.0.16"
5
+ version: "0.0.18"
6
6
  ---
7
7
 
8
8
  # Fupload
@@ -1,3 +1,3 @@
1
1
  """Fupload Python CLI."""
2
2
 
3
- __version__ = "0.0.16"
3
+ __version__ = "0.0.18"
@@ -20,6 +20,8 @@ from .trust import trusted_local_dir, trusted_roaming_dir, verify_dd_executable
20
20
 
21
21
 
22
22
  EXPECTED_DD_VERSION = os.environ.get("FUPLOAD_DD_EXPECTED_VERSION", "any")
23
+ SIDECAR_DEFAULT_RESPONSE_TIMEOUT_SECONDS = 180
24
+ SIDECAR_UPLOAD_RESPONSE_TIMEOUT_SECONDS = 660
23
25
  LIFE_TYPES = [
24
26
  {"name": "7 days", "value": "seven_day"},
25
27
  {"name": "14 days", "value": "fourteen_day"},
@@ -240,6 +242,20 @@ def state_dir() -> Path:
240
242
  return result
241
243
 
242
244
 
245
+ def _sidecar_startup_error(error: Mapping[str, Any]) -> FuploadError:
246
+ message = str(error.get("message") or "DD sidecar failed to start")
247
+ return FuploadError(
248
+ message,
249
+ kind=str(error.get("kind") or "authentication_error"),
250
+ stage=str(error.get("stage") or "session"),
251
+ endpoint=error.get("endpoint"),
252
+ http_status=error.get("http_status"),
253
+ business_code=error.get("business_code"),
254
+ verification_required=bool(error.get("verification_required")),
255
+ details=error.get("details") if isinstance(error.get("details"), dict) else None,
256
+ )
257
+
258
+
243
259
  class Sidecar:
244
260
  def __init__(self) -> None:
245
261
  self.dd_dir, self.signature = discover_dd_info()
@@ -272,11 +288,8 @@ class Sidecar:
272
288
  raise
273
289
  if not ready.get("ready"):
274
290
  self.close()
275
- raise FuploadError(
276
- str((ready.get("error") or {}).get("message") or "DD sidecar failed to start"),
277
- kind="authentication_error",
278
- stage="session",
279
- )
291
+ error = ready.get("error") if isinstance(ready.get("error"), dict) else {}
292
+ raise _sidecar_startup_error(error)
280
293
  credential_kind = ready.get("credential_kind")
281
294
  if credential_kind not in ("email", "mobile"):
282
295
  self.close()
@@ -315,7 +328,7 @@ class Sidecar:
315
328
  self.responses.put(FuploadError("DD sidecar exited without a result", kind="sidecar_error"))
316
329
 
317
330
  def _next_result(
318
- self, *, timeout: float = 180, endpoint: Optional[str] = None,
331
+ self, *, timeout: float = SIDECAR_DEFAULT_RESPONSE_TIMEOUT_SECONDS, endpoint: Optional[str] = None,
319
332
  stage: Optional[str] = None,
320
333
  verification_required: bool = False,
321
334
  ) -> Dict[str, Any]:
@@ -366,6 +379,11 @@ class Sidecar:
366
379
  else:
367
380
  expected_stage, uncertain = "dependency_get", False
368
381
  response = self._next_result(
382
+ timeout=(
383
+ SIDECAR_UPLOAD_RESPONSE_TIMEOUT_SECONDS
384
+ if action == "upload"
385
+ else SIDECAR_DEFAULT_RESPONSE_TIMEOUT_SECONDS
386
+ ),
369
387
  endpoint=endpoint,
370
388
  stage=expected_stage,
371
389
  verification_required=uncertain,
@@ -1939,7 +1957,10 @@ class DD:
1939
1957
  if available and any(str(value) not in available for value in values):
1940
1958
  raise ValidationError("selection contains an unavailable live option", path=path)
1941
1959
 
1942
- def _validate_options(self, session: Sidecar, resource: str, form: Mapping[str, Any]) -> None:
1960
+ def _validate_options(
1961
+ self, session: Sidecar, resource: str, form: Mapping[str, Any], *,
1962
+ validate_optional_dependencies: bool = True,
1963
+ ) -> None:
1943
1964
  game_type = form.get("game_type")
1944
1965
  if resource == "plugin":
1945
1966
  versions = session.get("/game_versions/list", {"game_type": game_type})
@@ -1972,11 +1993,11 @@ class DD:
1972
1993
  if form.get("share_code_life_type"):
1973
1994
  self._validate_choices(LIFE_TYPES, [form.get("share_code_life_type")], ("value",), "$.share_code_life_type")
1974
1995
 
1975
- if form.get("need_anchor_vip"):
1996
+ if validate_optional_dependencies and form.get("need_anchor_vip"):
1976
1997
  vip_levels = session.get("/anchor_vip/level/list", {"enrich_acts": "false"})
1977
1998
  self._validate_choices(vip_levels, form.get("vip_levels") or [], ("id", "level", "value"), "$.vip_levels")
1978
1999
 
1979
- if form.get("jump_room"):
2000
+ if validate_optional_dependencies and form.get("jump_room"):
1980
2001
  channels = safe_channels(session.cc_get("https://api.cc.163.com/v1/mixteammsgproxy/channelList?source=pluginPublish"))
1981
2002
  if not channels["items"]:
1982
2003
  raise FuploadError("live channel response contained no selectable values", kind="platform_data_error")
@@ -1985,7 +2006,7 @@ class DD:
1985
2006
  if wanted not in available:
1986
2007
  raise ValidationError("room/channel selection is unavailable", path="$.channel_id")
1987
2008
 
1988
- if form.get("with_associate"):
2009
+ if validate_optional_dependencies and form.get("with_associate"):
1989
2010
  references = self._associated_refs(session, game_type)
1990
2011
  if not references:
1991
2012
  raise FuploadError("live association response contained no selectable values", kind="platform_data_error")
@@ -2094,7 +2115,14 @@ class DD:
2094
2115
  validate_locked_usage_mode(current, form, doc)
2095
2116
  normalize_commercial(form, "plugin", create=action == "create")
2096
2117
  validate_plugin_submission(form, doc)
2097
- self._validate_options(session, "plugin", form)
2118
+ # A version-only update cannot change these optional metadata fields.
2119
+ # Validate their live dependencies on create/edit, but do not block a
2120
+ # release merely because an already-saved room, VIP, or association is
2121
+ # temporarily absent from an auxiliary option endpoint.
2122
+ self._validate_options(
2123
+ session, "plugin", form,
2124
+ validate_optional_dependencies=action != "update",
2125
+ )
2098
2126
  validate_no_display_objects(form, "plugin")
2099
2127
  if doc.get("logo_file"):
2100
2128
  form["logo"] = session.upload(doc["logo_file"], "addon", file_name="", media=True, max_bytes=10 * 1024 * 1024)
@@ -13,7 +13,7 @@ import time
13
13
  import uuid
14
14
  from pathlib import Path
15
15
  from types import SimpleNamespace
16
- from typing import Any, Dict, List, Optional
16
+ from typing import Any, Dict, List, Mapping, Optional
17
17
 
18
18
  from .errors import FuploadError, redact
19
19
  from .trust import verify_dd_executable
@@ -23,6 +23,7 @@ STATE_NAME = "broker.json"
23
23
  STARTUP_NAME = "broker.starting.json"
24
24
  MAX_REQUEST_BYTES = 64 * 1024 * 1024
25
25
  IDLE_SECONDS = 10 * 60
26
+ BROKER_WRITE_TIMEOUT_SECONDS = 15 * 60
26
27
 
27
28
 
28
29
  def _dd_module():
@@ -406,6 +407,17 @@ def _send(value: Dict[str, Any], timeout: float = 300) -> Dict[str, Any]:
406
407
  return response.get("data")
407
408
 
408
409
 
410
+ def _startup_error(pending: Mapping[str, Any]) -> FuploadError:
411
+ error = pending.get("error")
412
+ if isinstance(error, dict):
413
+ return FuploadError.from_dict(error)
414
+ return FuploadError(
415
+ str(error or "DD task session failed to start"),
416
+ kind="session_start_failed",
417
+ stage="session",
418
+ )
419
+
420
+
409
421
  def start(confirm_close_gui: bool) -> Dict[str, Any]:
410
422
  existing = _load_live_state()
411
423
  if existing:
@@ -447,7 +459,7 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
447
459
  creationflags=flags,
448
460
  )
449
461
  deadline = time.time() + 90
450
- last_error = ""
462
+ last_error: Any = ""
451
463
  while time.time() < deadline:
452
464
  state = _load_live_state()
453
465
  if state and state.get("startup_id") == startup_id:
@@ -465,7 +477,7 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
465
477
  if startup.exists():
466
478
  try:
467
479
  pending = _read_json(startup)
468
- last_error = str(pending.get("error") or "")
480
+ last_error = pending.get("error") or ""
469
481
  except FuploadError:
470
482
  pass
471
483
  if process.poll() is not None:
@@ -487,10 +499,11 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
487
499
  try:
488
500
  pending = _read_json(startup)
489
501
  if pending.get("startup_id") == startup_id:
502
+ last_error = pending.get("error") or last_error
490
503
  startup.unlink()
491
504
  except (FuploadError, OSError):
492
505
  pass
493
- raise FuploadError(last_error or "DD task session failed to start", kind="session_start_failed", stage="session")
506
+ raise _startup_error({"error": last_error})
494
507
 
495
508
 
496
509
  def status(session_id: Optional[str] = None) -> Dict[str, Any]:
@@ -533,7 +546,7 @@ def execute(session_id: str, kind: str, resource: str, action: str, payload: Dic
533
546
  "resource": resource,
534
547
  "action": action,
535
548
  "payload": payload,
536
- })
549
+ }, timeout=BROKER_WRITE_TIMEOUT_SECONDS if kind == "write" else 300)
537
550
 
538
551
 
539
552
  def _read_request(connection: socket.socket) -> Dict[str, Any]:
@@ -655,9 +668,17 @@ def _serve(startup_id: str) -> int:
655
668
  connection.sendall((json.dumps(response, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8"))
656
669
  return 0
657
670
  except Exception as exc:
671
+ if isinstance(exc, FuploadError):
672
+ error = exc.as_dict()
673
+ else:
674
+ error = FuploadError(
675
+ redact(str(exc))[:400] or "DD task session failed to start",
676
+ kind="session_start_failed",
677
+ stage="session",
678
+ ).as_dict()
658
679
  _atomic_json(startup_path, {
659
680
  "startup_id": startup_id,
660
- "error": redact(str(exc))[:400],
681
+ "error": error,
661
682
  })
662
683
  return 1
663
684
  finally:
@@ -42,6 +42,7 @@ _SENSITIVE_KEYS = {
42
42
  "presigneduri", "presigned_uri", "signature", "client_secret", "api_key", "auth_key", "password", "secret",
43
43
  }
44
44
  _MAX_LOG_BODY = 1024 * 1024
45
+ OBJECT_PUT_TIMEOUT_SECONDS = 600
45
46
  _CONFIG_CONTENT_KEYS = {
46
47
  "known_addon", "unknown_addon", "wtf", "material", "font",
47
48
  "known_wa", "unknown_wa", "retail_ui_config",
@@ -362,12 +363,13 @@ def _validation_details(probe):
362
363
  return result
363
364
 
364
365
 
365
- def install_response_probe(client):
366
+ def install_response_probe(client, capture_success=False):
366
367
  """Capture response objects and HTTPError bodies without changing DD behavior."""
367
368
  session = getattr(client, "_session", None)
368
369
  restore = getattr(client, "_fupload_restore_response_probe", None)
369
370
  if callable(restore):
370
371
  restore()
372
+ client._fupload_capture_success = bool(capture_success)
371
373
  original_opener_open = urllib.request.OpenerDirector.open
372
374
 
373
375
  def opener_open(opener, *args, **kwargs):
@@ -400,6 +402,7 @@ def install_response_probe(client):
400
402
  if getattr(urllib.request.OpenerDirector, "open", None) is opener_open:
401
403
  urllib.request.OpenerDirector.open = original_opener_open
402
404
  client._fupload_restore_response_probe = None
405
+ client._fupload_capture_success = False
403
406
 
404
407
  client._fupload_restore_response_probe = restore_probe
405
408
  for method_name in ("get", "post"):
@@ -415,7 +418,7 @@ def install_response_probe(client):
415
418
  status = int(getattr(response, "status_code", 0) or 0)
416
419
  except (TypeError, ValueError):
417
420
  status = 0
418
- if status >= 400:
421
+ if status >= 400 or getattr(client, "_fupload_capture_success", False):
419
422
  try:
420
423
  body = str(getattr(response, "text", "") or "")
421
424
  except Exception:
@@ -507,6 +510,59 @@ def failure_from_exception(exc, stage, response=None):
507
510
  )
508
511
 
509
512
 
513
+ def author_login_failure(client):
514
+ probe = getattr(client, "_fupload_last_response_error", None)
515
+ return failure_from_exception(
516
+ RuntimeError("DD author API login failed"), "session", probe,
517
+ )
518
+
519
+
520
+ def _last_response_payload(client):
521
+ probe = getattr(client, "_fupload_last_response_error", None)
522
+ if not isinstance(probe, dict) or not probe.get("body"):
523
+ return None
524
+ try:
525
+ payload = json.loads(probe["body"])
526
+ except (TypeError, ValueError):
527
+ return None
528
+ return payload if isinstance(payload, dict) else None
529
+
530
+
531
+ def login_author_client(client):
532
+ try:
533
+ if client.login():
534
+ return
535
+ if _retryable_get_signature_rejection(_last_response_payload(client)):
536
+ client._fupload_last_response_error = None
537
+ if client.login():
538
+ return
539
+ raise author_login_failure(client)
540
+ finally:
541
+ client._fupload_capture_success = False
542
+
543
+
544
+ def author_session_or_cleanup(qt, container, flow, jwt_helper, client):
545
+ session = (qt, container, flow, jwt_helper, client)
546
+ try:
547
+ login_author_client(client)
548
+ except Exception:
549
+ try:
550
+ close_session(session)
551
+ except Exception:
552
+ pass
553
+ raise
554
+ return session
555
+
556
+
557
+ def startup_error_payload(exc):
558
+ if isinstance(exc, SidecarFailure):
559
+ return exc.as_dict()
560
+ return {
561
+ "type": type(exc).__name__,
562
+ "message": safe_exception_message(exc)[:400],
563
+ }
564
+
565
+
510
566
  def bootstrap():
511
567
  resource = os.path.join(DD_DIR, "ccvoicehub.res")
512
568
  pyqt_dir = os.path.join(DD_DIR, "ccsub64", "PyQt5")
@@ -643,10 +699,8 @@ def open_session(timeout=45):
643
699
  raise RuntimeError("DD NEP module is not initialized")
644
700
  client = UiApiClient(nep, login_cookie=getattr(controller, "_cookie", None))
645
701
  client._session.headers["User-Agent"] = USER_AGENT
646
- if not client.login():
647
- raise RuntimeError("DD author API login failed")
648
- install_response_probe(client)
649
- return qt, container, flow, jwt_helper, client
702
+ install_response_probe(client, capture_success=True)
703
+ return author_session_or_cleanup(qt, container, flow, jwt_helper, client)
650
704
 
651
705
 
652
706
  def api_result(payload):
@@ -778,7 +832,7 @@ def run_command(session, command):
778
832
  request = urllib.request.Request(
779
833
  info["url"], data=data, method="PUT", headers=headers,
780
834
  )
781
- with urllib.request.urlopen(request, timeout=180) as response:
835
+ with urllib.request.urlopen(request, timeout=OBJECT_PUT_TIMEOUT_SECONDS) as response:
782
836
  if response.status != 200:
783
837
  try:
784
838
  response_body = response.read()
@@ -905,10 +959,7 @@ def main():
905
959
  "error": error})
906
960
  return 0
907
961
  except Exception as exc:
908
- output({"ready": False, "error": {
909
- "type": type(exc).__name__,
910
- "message": safe_exception_message(exc)[:400],
911
- }})
962
+ output({"ready": False, "error": startup_error_payload(exc)})
912
963
  return 1
913
964
  finally:
914
965
  machine_data.clientNo = original
@@ -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.16",
5
- "skill_version": "0.0.16",
6
- "tree_sha256": "aba774b14679854ac01a09711aa10b2f7266af2c43e8220cc67c64b9559bfb51",
4
+ "package_version": "0.0.18",
5
+ "skill_version": "0.0.18",
6
+ "tree_sha256": "36d839247defb278814b13da703acef8766aeb4f01efb8bda4c3d104367ac13b",
7
7
  "files": [
8
8
  {
9
9
  "path": "agents/openai.yaml",
@@ -113,7 +113,7 @@
113
113
  {
114
114
  "path": "scripts/fupload_cli/__init__.py",
115
115
  "bytes": 50,
116
- "sha256": "f0d2cbd76206a61d5fd78eef58ad808108f9538963da2c0ea67df79c8a41e104"
116
+ "sha256": "b899101a4de87efa255361fb768398e6f03a68740eca391ba57991984bd77c55"
117
117
  },
118
118
  {
119
119
  "path": "scripts/fupload_cli/blackbox_web.py",
@@ -137,18 +137,18 @@
137
137
  },
138
138
  {
139
139
  "path": "scripts/fupload_cli/dd_broker.py",
140
- "bytes": 27637,
141
- "sha256": "1ba00dcaea80905769bb2e00a6e965f2a1e9bb46a33534d1e77a6564158605a8"
140
+ "bytes": 28353,
141
+ "sha256": "1b05604c27253cd77b40bb2b549a5cd1c99434245ccb60959f989ec690e3c70f"
142
142
  },
143
143
  {
144
144
  "path": "scripts/fupload_cli/dd_sidecar.py",
145
- "bytes": 37603,
146
- "sha256": "6ad1ab9e2145757f44b410d87d554485566812d9584820180ff3bb7b5bc7763a"
145
+ "bytes": 39206,
146
+ "sha256": "722038884c5b050c2c6030f5d4e7b1088c1fb9b614e99c3b241b539631cbc3e3"
147
147
  },
148
148
  {
149
149
  "path": "scripts/fupload_cli/dd.py",
150
- "bytes": 113914,
151
- "sha256": "31e349cfee243c1ae8afefd74acfe3f5d79771a1c23ed1892aaf9d8e9b416922"
150
+ "bytes": 115318,
151
+ "sha256": "588102eb959e9f4737c39311cd689e400e2b0a8197abc231a145f342c03dbd68"
152
152
  },
153
153
  {
154
154
  "path": "scripts/fupload_cli/errors.py",
@@ -208,7 +208,7 @@
208
208
  {
209
209
  "path": "SKILL.md",
210
210
  "bytes": 28383,
211
- "sha256": "2477e603ca826b9bd31533995f2217bfd9239c070adbaa2aa4b829fb6a89fb7f"
211
+ "sha256": "512d6d5b7d34987009c12675e4cee4c540950e32ac808fbf4081d8db6c3157f0"
212
212
  }
213
213
  ]
214
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@follenfang/fupload",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Install and run the Fuploader Agent Skill and Python CLI.",
5
5
  "type": "module",
6
6
  "bin": {