@follenfang/fupload 0.0.17 → 0.0.19

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.17"
5
+ version: "0.0.19"
6
6
  ---
7
7
 
8
8
  # Fupload
@@ -22,7 +22,7 @@ Map natural language consistently:
22
22
 
23
23
  - `create`: create the main record. If first content is a separate platform action, plan it as a following atomic step.
24
24
  - `update`: publish a plugin version, change selected configuration-backup content, or publish a WA/string version.
25
- - `edit`: change fields allowed by the target platform's action allowlist. For DD plugins, this is existing-record commercial, association, room/channel, and creation-statement settings; first-publication metadata, categories, media, and version fields are create/update-only.
25
+ - `edit`: change fields allowed by the target platform's action allowlist. For DD plugins, this includes `description` and existing-record commercial, association, room/channel, and creation-statement settings; other first-publication metadata, categories, media, and version fields are create/update-only. Description edits read both detail and author-list projections; either may confirm the value, but if neither matches, report verification-required.
26
26
  - `delete`: delete one main record only. It never deletes an individual version, media object, or associated record.
27
27
 
28
28
  Never offer bulk delete, drafts, guides, messages, or GUI control automation. Heybox supports individual version deletion with asynchronous readback; whole-module deletion is not supported. The DD task-session command may close verified official GUI processes only after the consent flow below.
@@ -109,7 +109,7 @@ For ModUs, require all four `modus session doctor` booleans to be true before an
109
109
 
110
110
  Before any DD live GET or write, run `dd session doctor`. If it reports `gui_running=true`, tell the user that continuing will close the listed official DD GUI instances and ask for explicit consent. Without consent, do not run start, do not close a process, and do not issue a native login. After consent, run `dd session start --confirm-close-gui`; when no GUI is running, run `dd session start` without that flag.
111
111
 
112
- `dd session start` automatically selects the native relogin flow from the official `Account.method`, `Cred.type`, and `Cred.modifier` enums. `urs + urs_token + normal` uses `UrsReLoginFlow` for an email session; `mobile + urs_mobile_token + mobile_password|mobile_uplink` uses `MobileReLoginFlow` for a mobile session. Missing, unknown, or contradictory combinations fail before any flow runs. Never infer the account kind from its text, coerce an enum, or try the other flow after a failure.
112
+ `dd session start` automatically selects the native relogin flow from the official `Account.method`, `Cred.type`, and `Cred.modifier` enums. `urs + urs_token + normal` uses `UrsReLoginFlow` for an email session; `mobile + urs_mobile_token + normal|mobile_password|mobile_uplink` uses `MobileReLoginFlow` for a mobile session. Missing, unknown, or contradictory combinations fail before any flow runs. Never infer the account kind from its text, coerce an enum, or try the other flow after a failure.
113
113
 
114
114
  Keep the returned opaque `session_id` only in task memory and pass it as `--session <id>` to every DD GET, write, readback, status, and delete. Reuse this one session for the complete task, serialize all commands, stop on the first failure, and never start one session per item in a batch. In a `finally` path, always run `dd session stop --session <id>` and require `cleanup_complete=true`; the ten-minute idle timeout is only an abnormal-exit fallback.
115
115
 
@@ -7,7 +7,7 @@ Run `dd session doctor` first. Doctor only discovers the installation, verifies
7
7
  `start` closes only identity- and signature-verified official GUI processes, starts one task broker, and reads the official `AccountCredStorage` auto account and credential. It selects the native relogin flow only from this strict enum matrix:
8
8
 
9
9
  - `Account.method=urs`, `Cred.type=urs_token`, `Cred.modifier=normal`: run `UrsReLoginFlow(controller, sdk, token, username)` for the email login state.
10
- - `Account.method=mobile`, `Cred.type=urs_mobile_token`, `Cred.modifier=mobile_password|mobile_uplink`: run `MobileReLoginFlow(controller, sdk, cgi, netconfig, token, isPassword, username)` for the mobile login state. `isPassword` is true only for `mobile_password`.
10
+ - `Account.method=mobile`, `Cred.type=urs_mobile_token`, `Cred.modifier=normal|mobile_password|mobile_uplink`: run `MobileReLoginFlow(controller, sdk, cgi, netconfig, token, isPassword, username)` for the mobile login state. `normal` is emitted by the official DD 100130 client for SMS/non-password mobile login. `isPassword` is true only for `mobile_password`.
11
11
 
12
12
  Missing, unknown, or contradictory enum combinations fail before either flow is called. Do not infer account kind from the account string, coerce a value, or fall back to the other flow after a login failure. Do not request or accept manual account names, passwords, tokens, credential values, or account-type overrides.
13
13
 
@@ -65,7 +65,7 @@ Omission on edit preserves the remote value. Explicit false follows the field-sp
65
65
 
66
66
  Every path in `detail_img_files` must exist as a regular local file. Validation reports the exact indexed path, such as `$.detail_img_files[0]`, before any upload or mutation.
67
67
 
68
- `game_type` and the outer free/paid state are create-only. `plugin update` requires `sn`, `game_versions`, `version`, and `update_desc`; optional `file`, `detail_url`, and `release_type` publish a new version. `plugin edit` requires `sn` and only accepts the official existing-record commercial and association controls (`scope`, payment/lifetime/VIP fields, room/channel linkage, `creation_statement`, and associated content). First-publication metadata (`addon_type`, `name`, `description`, logo, detail images, categories, and `html_desc`) and version fields are not edit fields; in particular, sending `description` to `/addon/modify` can be accepted while leaving the remote value unchanged, so the CLI rejects it instead of reporting a false success.
68
+ `game_type` and the outer free/paid state are create-only. `plugin update` requires `sn`, `game_versions`, `version`, and `update_desc`; optional `file`, `detail_url`, and `release_type` publish a new version. `plugin edit` requires `sn` and accepts `description` plus the official existing-record commercial and association controls (`scope`, payment/lifetime/VIP fields, room/channel linkage, `creation_statement`, and associated content). Other first-publication metadata (`addon_type`, `name`, logo, detail images, categories, and `html_desc`) and version fields are not edit fields. The CLI verifies a description edit through both detail and author-list readback; if DD accepts the request but leaves the remote value unchanged, it reports a verification-required error rather than false success.
69
69
 
70
70
  When `assign_user_sn` is present, the official UI exposes only the public scope option. Python validates the final rebuilt form and rejects a private scope before upload or mutation.
71
71
 
@@ -1,3 +1,3 @@
1
1
  """Fupload Python CLI."""
2
2
 
3
- __version__ = "0.0.17"
3
+ __version__ = "0.0.19"
@@ -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"},
@@ -326,7 +328,7 @@ class Sidecar:
326
328
  self.responses.put(FuploadError("DD sidecar exited without a result", kind="sidecar_error"))
327
329
 
328
330
  def _next_result(
329
- self, *, timeout: float = 180, endpoint: Optional[str] = None,
331
+ self, *, timeout: float = SIDECAR_DEFAULT_RESPONSE_TIMEOUT_SECONDS, endpoint: Optional[str] = None,
330
332
  stage: Optional[str] = None,
331
333
  verification_required: bool = False,
332
334
  ) -> Dict[str, Any]:
@@ -377,6 +379,11 @@ class Sidecar:
377
379
  else:
378
380
  expected_stage, uncertain = "dependency_get", False
379
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
+ ),
380
387
  endpoint=endpoint,
381
388
  stage=expected_stage,
382
389
  verification_required=uncertain,
@@ -1456,10 +1463,12 @@ PLUGIN_CREATE_DEFAULTS = {
1456
1463
  "with_associate": False,
1457
1464
  }
1458
1465
 
1459
- # DD's modify form is not a second create form. The official edit page
1460
- # rebuilds the payload from the existing commercial/association controls;
1461
- # first-publication metadata and version fields belong to create/update.
1466
+ # DD's modify form is not a second create form. The official edit page
1467
+ # rebuilds the payload from existing commercial/association controls. The
1468
+ # brief description is the supported metadata exception and is verified by
1469
+ # detail and author-list readback after every edit.
1462
1470
  PLUGIN_EDIT_FIELDS = (
1471
+ "description",
1463
1472
  "scope", "share_code_life_type", "need_buy", "price_fen", "buy_life_type",
1464
1473
  "jump_room", "room_id", "channel_id", "channel_type", "sync_room",
1465
1474
  "creation_statement", "with_associate", "associated_acts", "need_anchor_vip",
@@ -1950,7 +1959,10 @@ class DD:
1950
1959
  if available and any(str(value) not in available for value in values):
1951
1960
  raise ValidationError("selection contains an unavailable live option", path=path)
1952
1961
 
1953
- def _validate_options(self, session: Sidecar, resource: str, form: Mapping[str, Any]) -> None:
1962
+ def _validate_options(
1963
+ self, session: Sidecar, resource: str, form: Mapping[str, Any], *,
1964
+ validate_optional_dependencies: bool = True,
1965
+ ) -> None:
1954
1966
  game_type = form.get("game_type")
1955
1967
  if resource == "plugin":
1956
1968
  versions = session.get("/game_versions/list", {"game_type": game_type})
@@ -1983,11 +1995,11 @@ class DD:
1983
1995
  if form.get("share_code_life_type"):
1984
1996
  self._validate_choices(LIFE_TYPES, [form.get("share_code_life_type")], ("value",), "$.share_code_life_type")
1985
1997
 
1986
- if form.get("need_anchor_vip"):
1998
+ if validate_optional_dependencies and form.get("need_anchor_vip"):
1987
1999
  vip_levels = session.get("/anchor_vip/level/list", {"enrich_acts": "false"})
1988
2000
  self._validate_choices(vip_levels, form.get("vip_levels") or [], ("id", "level", "value"), "$.vip_levels")
1989
2001
 
1990
- if form.get("jump_room"):
2002
+ if validate_optional_dependencies and form.get("jump_room"):
1991
2003
  channels = safe_channels(session.cc_get("https://api.cc.163.com/v1/mixteammsgproxy/channelList?source=pluginPublish"))
1992
2004
  if not channels["items"]:
1993
2005
  raise FuploadError("live channel response contained no selectable values", kind="platform_data_error")
@@ -1996,7 +2008,7 @@ class DD:
1996
2008
  if wanted not in available:
1997
2009
  raise ValidationError("room/channel selection is unavailable", path="$.channel_id")
1998
2010
 
1999
- if form.get("with_associate"):
2011
+ if validate_optional_dependencies and form.get("with_associate"):
2000
2012
  references = self._associated_refs(session, game_type)
2001
2013
  if not references:
2002
2014
  raise FuploadError("live association response contained no selectable values", kind="platform_data_error")
@@ -2105,7 +2117,14 @@ class DD:
2105
2117
  validate_locked_usage_mode(current, form, doc)
2106
2118
  normalize_commercial(form, "plugin", create=action == "create")
2107
2119
  validate_plugin_submission(form, doc)
2108
- self._validate_options(session, "plugin", form)
2120
+ # A version-only update cannot change these optional metadata fields.
2121
+ # Validate their live dependencies on create/edit, but do not block a
2122
+ # release merely because an already-saved room, VIP, or association is
2123
+ # temporarily absent from an auxiliary option endpoint.
2124
+ self._validate_options(
2125
+ session, "plugin", form,
2126
+ validate_optional_dependencies=action != "update",
2127
+ )
2109
2128
  validate_no_display_objects(form, "plugin")
2110
2129
  if doc.get("logo_file"):
2111
2130
  form["logo"] = session.upload(doc["logo_file"], "addon", file_name="", media=True, max_bytes=10 * 1024 * 1024)
@@ -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():
@@ -545,7 +546,7 @@ def execute(session_id: str, kind: str, resource: str, action: str, payload: Dic
545
546
  "resource": resource,
546
547
  "action": action,
547
548
  "payload": payload,
548
- })
549
+ }, timeout=BROKER_WRITE_TIMEOUT_SECONDS if kind == "write" else 300)
549
550
 
550
551
 
551
552
  def _read_request(connection: socket.socket) -> Dict[str, Any]:
@@ -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",
@@ -624,8 +625,9 @@ def _create_relogin_flow(
624
625
  if (
625
626
  method == "mobile"
626
627
  and credential_type == "urs_mobile_token"
627
- and modifier in ("mobile_password", "mobile_uplink")
628
+ and modifier in ("normal", "mobile_password", "mobile_uplink")
628
629
  ):
630
+ # Official DD 100130 emits normal for SMS/non-password mobile credentials.
629
631
  return mobile_flow_class(
630
632
  controller, sdk, cgi, netconfig, credential.value,
631
633
  modifier == "mobile_password", account.name,
@@ -642,7 +644,7 @@ def _credential_kind(account, credential):
642
644
  if (
643
645
  method == "mobile"
644
646
  and credential_type == "urs_mobile_token"
645
- and modifier in ("mobile_password", "mobile_uplink")
647
+ and modifier in ("normal", "mobile_password", "mobile_uplink")
646
648
  ):
647
649
  return "mobile"
648
650
  raise RuntimeError("DD persisted login state uses an unsupported credential combination")
@@ -831,7 +833,7 @@ def run_command(session, command):
831
833
  request = urllib.request.Request(
832
834
  info["url"], data=data, method="PUT", headers=headers,
833
835
  )
834
- with urllib.request.urlopen(request, timeout=180) as response:
836
+ with urllib.request.urlopen(request, timeout=OBJECT_PUT_TIMEOUT_SECONDS) as response:
835
837
  if response.status != 200:
836
838
  try:
837
839
  response_body = response.read()
@@ -580,6 +580,7 @@ DD_PLUGIN_EDIT_META = {
580
580
  name: DD_COMMERCIAL[name]
581
581
  for name in DD_COMMERCIAL
582
582
  }
583
+ DD_PLUGIN_EDIT_META["description"] = DD_PLUGIN_META["description"]
583
584
  DD_PLUGIN_VERSION = {
584
585
  "game_versions": f("array", nonempty=True), "detail_url": f("string"),
585
586
  "file": f("string", local_file=True), "release_type": f("integer", choices=(1, 2, 3)),
@@ -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.17",
5
- "skill_version": "0.0.17",
6
- "tree_sha256": "064aeb1a1291d240a3bea7e71458846395abe29c6e91f682d269ec0e684c4294",
4
+ "package_version": "0.0.19",
5
+ "skill_version": "0.0.19",
6
+ "tree_sha256": "dd39f508a55b1b4fe50e5bd076d2a2cd85cb03c6864cdedcb4a7de521ea92197",
7
7
  "files": [
8
8
  {
9
9
  "path": "agents/openai.yaml",
@@ -87,8 +87,8 @@
87
87
  },
88
88
  {
89
89
  "path": "references/dd.md",
90
- "bytes": 19727,
91
- "sha256": "2804e4b77c000d0bd12ca3f69bbcd5720f759fd4e84b4127ea3461093bf5da7c"
90
+ "bytes": 19868,
91
+ "sha256": "79498778d5d9f9c6205b2343b382fc71981d3c3e2cacd7470b63ad18e3afb491"
92
92
  },
93
93
  {
94
94
  "path": "references/modus.md",
@@ -113,7 +113,7 @@
113
113
  {
114
114
  "path": "scripts/fupload_cli/__init__.py",
115
115
  "bytes": 50,
116
- "sha256": "8e72acb94a18f173cf9cbbf0436b20f64e1991f54a3537b7e930b4196bab8439"
116
+ "sha256": "f292736d0644ad8cdfc2c5c693957ec320f889bab0ed5845b574d60e7998f225"
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": 28248,
141
- "sha256": "6bbae4ed7aef4feca552816d7dbefa6069dc3d1d9a39a4af35582a06db4ce2d5"
140
+ "bytes": 28353,
141
+ "sha256": "1b05604c27253cd77b40bb2b549a5cd1c99434245ccb60959f989ec690e3c70f"
142
142
  },
143
143
  {
144
144
  "path": "scripts/fupload_cli/dd_sidecar.py",
145
- "bytes": 39150,
146
- "sha256": "42791c82e5518838aedbe209823b94deb7de09b2f3eccb4fe563364f58becc6c"
145
+ "bytes": 39309,
146
+ "sha256": "fa0658f9cd4d5c85f4f1f84dd79bf7b33b5cafb74813107acc09fcf1f17d508c"
147
147
  },
148
148
  {
149
149
  "path": "scripts/fupload_cli/dd.py",
150
- "bytes": 114434,
151
- "sha256": "cf808efdd4a93d80bddb533af4ac14696e889e0fd359acdacd58a7a49ce050b0"
150
+ "bytes": 115390,
151
+ "sha256": "e160eeebb2914016d99f6dad1ad38b61fa201c884bb2314090839408fa70cd58"
152
152
  },
153
153
  {
154
154
  "path": "scripts/fupload_cli/errors.py",
@@ -182,8 +182,8 @@
182
182
  },
183
183
  {
184
184
  "path": "scripts/fupload_cli/schema.py",
185
- "bytes": 54561,
186
- "sha256": "17d57a9c9c53e556faf9e415020d47f84965283680c66f3f1c33beef6b83d104"
185
+ "bytes": 54628,
186
+ "sha256": "6eead57520d6f1d0db62d6da764c4e419cca24a267483dd3708f212a492e396a"
187
187
  },
188
188
  {
189
189
  "path": "scripts/fupload_cli/state_machine.py",
@@ -207,8 +207,8 @@
207
207
  },
208
208
  {
209
209
  "path": "SKILL.md",
210
- "bytes": 28383,
211
- "sha256": "9aebce883e6d83f0e3c8b36f497e59eb6f4df5e1e4af231df60ca3299188fb86"
210
+ "bytes": 28568,
211
+ "sha256": "e2f60649f6d239c555fac24e85fef6ab0d3354b2736e2022b2b9221eedbf0841"
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.17",
3
+ "version": "0.0.19",
4
4
  "description": "Install and run the Fuploader Agent Skill and Python CLI.",
5
5
  "type": "module",
6
6
  "bin": {