@follenfang/fupload 0.0.15 → 0.0.17
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 +11 -1
- package/fupload/references/dd.md +18 -3
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/dd.py +30 -2
- package/fupload/scripts/fupload_cli/dd_broker.py +41 -5
- package/fupload/scripts/fupload_cli/dd_sidecar.py +127 -18
- package/fupload/scripts/fupload_cli/io.py +3 -1
- package/npm/skill-manifest.json +16 -16
- package/package.json +1 -1
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.
|
|
5
|
+
version: "0.0.17"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Fupload
|
|
@@ -54,6 +54,8 @@ For NewBeeBox, prefer an existing official `ncc login`, then a caller-provided `
|
|
|
54
54
|
|
|
55
55
|
For CurseForge, use `~/.fupload/curseforge.env`, created idempotently by npm install/update, with `CURSEFORGE_AUTHOR_ID`, `CURSEFORGE_API_KEY`, and `CURSEFORGE_UPLOAD_TOKEN`. The author ID is non-secret: if absent, proactively ask for its numeric value or offer `--author-id`. The API key and upload token are secrets: never ask the user to paste either into the conversation; direct them to fill the local env file in their own editor or terminal, then run `curseforge session doctor`. Never print file contents or secret values. Process environment values may override the file without being shown.
|
|
56
56
|
|
|
57
|
+
For DD, reuse only the official client's local auto account and credential through `dd session start`. Never request or accept an email address, mobile number, password, token, credential value, or account-type override. Account and credential values must not appear in CLI output, plans, logs, tests, or analysis evidence; only the safe credential kind may be retained for diagnosis and verification.
|
|
58
|
+
|
|
57
59
|
For ModUs, authentication comes only from `%LOCALAPPDATA%\ModUs.Creator\auth\token.dat` under the current Windows user. `modus session doctor` reports only token presence, DPAPI decryption, nonempty plaintext, and authenticated API readiness. Never print, copy, hash, or persist the token, ciphertext, Bearer header, or signed upload URL.
|
|
58
60
|
|
|
59
61
|
If a token is pasted into the conversation, do not repeat or use it. Tell the user to revoke or regenerate it at its provider and configure the replacement locally. Check only whether the expected field is present, never its length, prefix, hash, or value.
|
|
@@ -107,8 +109,12 @@ For ModUs, require all four `modus session doctor` booleans to be true before an
|
|
|
107
109
|
|
|
108
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.
|
|
109
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.
|
|
113
|
+
|
|
110
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.
|
|
111
115
|
|
|
116
|
+
Email and mobile differ only during native relogin. After a matching flow succeeds, both reuse the same JWT refresh, author API client, GET, upload, mutation, readback, logout, and cleanup path through that one task session.
|
|
117
|
+
|
|
112
118
|
For every DD configuration, first list backups, select `backup_sn`, then run `dd config backup-get --sn <backup>`. Select a WTF account/server/role before presenting that account's known/unknown WA choices. For a retail configuration, present the safe edit-mode and cooldown selector metadata. Send only stable IDs and returned selectors; never request, display, store, or reconstruct raw backup objects or retail import strings.
|
|
113
119
|
|
|
114
120
|
For a configuration, require a cloud backup already uploaded by the matching desktop client. If none exists, ask the user to upload one in the client and stop before writing.
|
|
@@ -147,6 +153,10 @@ For a Python delete, first run the resource `get` command, show the exact name a
|
|
|
147
153
|
|
|
148
154
|
Run writes serially. For official `ncc`, always request `-o json`, parse stdout as JSON, and treat stderr only as progress diagnostics. For Python, parse its stable JSON output. Never scrape human text. After each successful step, immediately run the corresponding info/get/list/versions/history command and compare the intended fields in the same DD session when applicable. DD performs a bounded GET-only readback poll and never resends a mutation during verification. For DD plugin update, use a nonempty `/addon/addon_versions` result only as a pre-upload duplicate guard; the matching author-list item's `latest_version` is the primary success confirmation and `detail_v2` is supplementary. Plugin edit also uses the same-SN author projection when detail remains stale. An empty history is diagnostic-only. Treat “submitted for review” and “under review” as distinct from “approved” or “publicly visible.”
|
|
149
155
|
|
|
156
|
+
When validating a DD authentication change, automated dispatch and constructor tests must cover every supported email and mobile enum combination, select exactly one matching flow with the correct arguments, and reject missing, unknown, or contradictory combinations without fallback. Exercise the shared JWT refresh, author API, resource, broker, sidecar, and logout path once through unified tests; do not require that common path to be repeated per credential kind.
|
|
157
|
+
|
|
158
|
+
Run the full isolated live matrix only with the official client's currently persisted account and record its safe credential kind; do not switch the persisted account merely to repeat the matrix. For every current non-exploration seasonal build, read the dependency graph and complete plugin and WA create, update, edit, readback, binary upload, delete, and final cleanup. Complete the same configuration matrix, including image upload, only for builds where `/backup/list` and `/backup/detail` provide a usable cloud backup. Record an explicit safe `N/A` reason for every build without one; never silently skip it or fabricate a backup selector. Evidence must record the executed commands, exit status, readback, `N/A` items, implementation commit, DD version, resource hashes, and cleanup result while redacting account names, credential values, tokens, Cookies, JWTs, signed URLs, `clientNo`, raw WA strings, and raw backup content. Cleanup may touch only objects created by that test run, and the run must finish with no sidecar, task broker, or live broker state.
|
|
159
|
+
|
|
150
160
|
For CurseForge, treat the Upload API's returned file ID as upload acceptance only. Record the ID and command result, then report that moderation, processing, manual release, and public visibility are separate states. On an interrupted or ambiguous upload, do not resend automatically because that can create a duplicate file; inspect the Authors project page or public file list before deciding on a new attempt. Follow the HTTP/error handling table in the CurseForge reference.
|
|
151
161
|
|
|
152
162
|
In the third-party Python NewBeeBox channel, public plugin publication is three atomic writes: create privately, upload and verify the first version, then edit to public with explicit review intent. Never send a public `share_state` during Python create. In the official channel, follow the installed `ncc docs` sequence for create, init, push, and visibility instead of applying Python wire rules.
|
package/fupload/references/dd.md
CHANGED
|
@@ -2,11 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Task session
|
|
4
4
|
|
|
5
|
-
Run `dd session doctor` first. Doctor only discovers the installation, verifies the Authenticode publisher, reports official DD processes and reads local broker state; it does not login. When `gui_running=true`, explain that the official DD GUI must close and obtain explicit user consent. Only then run `dd session start --confirm-close-gui`. When `gui_running=false`, run `dd session start` without the confirmation flag.
|
|
5
|
+
Run `dd session doctor` first. Doctor only discovers the installation, verifies the Authenticode publisher, reports official DD processes and reads local broker state; it does not login or run either native relogin flow. When `gui_running=true`, explain that the official DD GUI must close and obtain explicit user consent. Only then run `dd session start --confirm-close-gui`. When `gui_running=false`, run `dd session start` without the confirmation flag.
|
|
6
6
|
|
|
7
|
-
`start` closes only identity- and signature-verified official GUI processes, starts one task broker and
|
|
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`.
|
|
11
|
+
|
|
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
|
+
|
|
14
|
+
After the selected flow succeeds, both credential kinds use the same JWT refresh, author API client, GET, parser, upload, mutation, readback, logout, and cleanup path. `start` returns an opaque `session_id`; pass it through `--session` to every DD read and write. All operations are serialized through this one task session and one native login. Run `dd session status --session <id>` for a local status check and always run `dd session stop --session <id>` in `finally`; successful stop reports `cleanup_complete=true`. The ten-minute idle timeout is only a crash fallback.
|
|
15
|
+
|
|
16
|
+
Do not request or expose account names, credential values, tokens, Cookies, JWTs, the credential database, signed URLs, `clientNo`, raw WA content, or raw backup objects. The safe credential kind may be reported without its account identity or value. The device state remains under Windows Known Folder Roaming AppData at `CCVoiceHub/Fupload/sidecar-device.json`.
|
|
17
|
+
|
|
18
|
+
## Authentication compatibility verification
|
|
19
|
+
|
|
20
|
+
Any change to DD authentication or session dispatch requires automated tests for every supported email and mobile enum combination. Each case must select exactly one matching flow, verify its constructor arguments, and reject missing, unknown, or contradictory combinations before login without fallback. Unified tests cover the common JWT refresh, author API client, resource operations, single broker/sidecar/login lifecycle, and explicit logout path after dispatch; this common path does not need to be repeated per credential kind.
|
|
21
|
+
|
|
22
|
+
Run the full isolated live matrix only under the account currently persisted by the official DD client, and record its safe credential kind. Do not switch that account merely to repeat the matrix for another credential kind. In one task session, read the live dependency graph for every current non-exploration seasonal build, then cover plugin and WA create, update, edit, readback, binary upload, delete, and final cleanup. For configuration, run the same CRUD, image-upload, readback, and cleanup matrix on every build where `/backup/list` returns a cloud backup whose `/backup/detail` supports valid selections. When a build has no usable cloud backup, record an explicit safe `N/A` reason; never silently skip it or fabricate a backup selector.
|
|
23
|
+
|
|
24
|
+
Verification evidence may contain only the safe credential kind, executed command, exit status, readback result, `N/A` items, implementation commit, DD version, resource hashes, and cleanup result. Redact account names, credential values, passwords, tokens, Cookies, JWTs, signed URLs, `clientNo`, signatures, device proofs, raw WA strings, and raw configuration backup content. Cleanup may remove only the isolated objects recorded as created by that run; do not modify pre-existing remote resources. A completed, failed, or timed-out run must verify that no DD sidecar, task broker, or live broker state remains.
|
|
10
25
|
|
|
11
26
|
## GET before one final JSON
|
|
12
27
|
|
|
@@ -240,11 +240,26 @@ def state_dir() -> Path:
|
|
|
240
240
|
return result
|
|
241
241
|
|
|
242
242
|
|
|
243
|
+
def _sidecar_startup_error(error: Mapping[str, Any]) -> FuploadError:
|
|
244
|
+
message = str(error.get("message") or "DD sidecar failed to start")
|
|
245
|
+
return FuploadError(
|
|
246
|
+
message,
|
|
247
|
+
kind=str(error.get("kind") or "authentication_error"),
|
|
248
|
+
stage=str(error.get("stage") or "session"),
|
|
249
|
+
endpoint=error.get("endpoint"),
|
|
250
|
+
http_status=error.get("http_status"),
|
|
251
|
+
business_code=error.get("business_code"),
|
|
252
|
+
verification_required=bool(error.get("verification_required")),
|
|
253
|
+
details=error.get("details") if isinstance(error.get("details"), dict) else None,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
243
257
|
class Sidecar:
|
|
244
258
|
def __init__(self) -> None:
|
|
245
259
|
self.dd_dir, self.signature = discover_dd_info()
|
|
246
260
|
self.process: Optional[subprocess.Popen[str]] = None
|
|
247
261
|
self.counter = 0
|
|
262
|
+
self.credential_kind: Optional[str] = None
|
|
248
263
|
self.lock_handle = None
|
|
249
264
|
self.responses: queue.Queue[Any] = queue.Queue()
|
|
250
265
|
self.reader_thread: Optional[threading.Thread] = None
|
|
@@ -270,12 +285,18 @@ class Sidecar:
|
|
|
270
285
|
self.close()
|
|
271
286
|
raise
|
|
272
287
|
if not ready.get("ready"):
|
|
288
|
+
self.close()
|
|
289
|
+
error = ready.get("error") if isinstance(ready.get("error"), dict) else {}
|
|
290
|
+
raise _sidecar_startup_error(error)
|
|
291
|
+
credential_kind = ready.get("credential_kind")
|
|
292
|
+
if credential_kind not in ("email", "mobile"):
|
|
273
293
|
self.close()
|
|
274
294
|
raise FuploadError(
|
|
275
|
-
|
|
276
|
-
kind="
|
|
295
|
+
"DD sidecar returned an invalid credential kind",
|
|
296
|
+
kind="sidecar_error",
|
|
277
297
|
stage="session",
|
|
278
298
|
)
|
|
299
|
+
self.credential_kind = credential_kind
|
|
279
300
|
return self
|
|
280
301
|
|
|
281
302
|
def _lock(self) -> None:
|
|
@@ -1781,6 +1802,13 @@ def config_readback_projection(value: Any) -> Mapping[str, Any]:
|
|
|
1781
1802
|
projected = dict(value)
|
|
1782
1803
|
if "need_buy" in projected:
|
|
1783
1804
|
projected["need_buy"] = 1 if projected["need_buy"] else 0
|
|
1805
|
+
for name in ("known_addon", "unknown_addon", "material", "font", "known_wa", "unknown_wa"):
|
|
1806
|
+
if projected.get(name) is None:
|
|
1807
|
+
projected[name] = {"items": [], "inner_version": {}}
|
|
1808
|
+
if projected.get("wtf") is None:
|
|
1809
|
+
projected["wtf"] = {"accounts": []}
|
|
1810
|
+
if projected.get("vip_levels") is None:
|
|
1811
|
+
projected["vip_levels"] = []
|
|
1784
1812
|
return projected
|
|
1785
1813
|
|
|
1786
1814
|
|
|
@@ -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
|
|
@@ -406,6 +406,17 @@ def _send(value: Dict[str, Any], timeout: float = 300) -> Dict[str, Any]:
|
|
|
406
406
|
return response.get("data")
|
|
407
407
|
|
|
408
408
|
|
|
409
|
+
def _startup_error(pending: Mapping[str, Any]) -> FuploadError:
|
|
410
|
+
error = pending.get("error")
|
|
411
|
+
if isinstance(error, dict):
|
|
412
|
+
return FuploadError.from_dict(error)
|
|
413
|
+
return FuploadError(
|
|
414
|
+
str(error or "DD task session failed to start"),
|
|
415
|
+
kind="session_start_failed",
|
|
416
|
+
stage="session",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
409
420
|
def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
410
421
|
existing = _load_live_state()
|
|
411
422
|
if existing:
|
|
@@ -415,6 +426,10 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
|
415
426
|
"running": True,
|
|
416
427
|
"reused": True,
|
|
417
428
|
"login_count": active.get("login_count", 1),
|
|
429
|
+
"broker_count": active.get("broker_count", 1),
|
|
430
|
+
"sidecar_count": active.get("sidecar_count", 1),
|
|
431
|
+
"native_login_count": active.get("native_login_count", 1),
|
|
432
|
+
"credential_kind": active.get("credential_kind"),
|
|
418
433
|
}
|
|
419
434
|
processes = running_dd_processes()
|
|
420
435
|
if processes and not confirm_close_gui:
|
|
@@ -443,7 +458,7 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
|
443
458
|
creationflags=flags,
|
|
444
459
|
)
|
|
445
460
|
deadline = time.time() + 90
|
|
446
|
-
last_error = ""
|
|
461
|
+
last_error: Any = ""
|
|
447
462
|
while time.time() < deadline:
|
|
448
463
|
state = _load_live_state()
|
|
449
464
|
if state and state.get("startup_id") == startup_id:
|
|
@@ -452,12 +467,16 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
|
452
467
|
"running": True,
|
|
453
468
|
"reused": False,
|
|
454
469
|
"login_count": 1,
|
|
470
|
+
"broker_count": state.get("broker_count", 1),
|
|
471
|
+
"sidecar_count": state.get("sidecar_count", 1),
|
|
472
|
+
"native_login_count": state.get("native_login_count", 1),
|
|
473
|
+
"credential_kind": state.get("credential_kind"),
|
|
455
474
|
"closed_gui_processes": len(processes),
|
|
456
475
|
}
|
|
457
476
|
if startup.exists():
|
|
458
477
|
try:
|
|
459
478
|
pending = _read_json(startup)
|
|
460
|
-
last_error =
|
|
479
|
+
last_error = pending.get("error") or ""
|
|
461
480
|
except FuploadError:
|
|
462
481
|
pass
|
|
463
482
|
if process.poll() is not None:
|
|
@@ -479,10 +498,11 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
|
479
498
|
try:
|
|
480
499
|
pending = _read_json(startup)
|
|
481
500
|
if pending.get("startup_id") == startup_id:
|
|
501
|
+
last_error = pending.get("error") or last_error
|
|
482
502
|
startup.unlink()
|
|
483
503
|
except (FuploadError, OSError):
|
|
484
504
|
pass
|
|
485
|
-
raise
|
|
505
|
+
raise _startup_error({"error": last_error})
|
|
486
506
|
|
|
487
507
|
|
|
488
508
|
def status(session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -575,6 +595,10 @@ def _serve(startup_id: str) -> int:
|
|
|
575
595
|
sidecar = _dd_module().Sidecar().__enter__()
|
|
576
596
|
state["dd_dir"] = str(sidecar.dd_dir)
|
|
577
597
|
state["signature"] = sidecar.signature
|
|
598
|
+
state["credential_kind"] = sidecar.credential_kind
|
|
599
|
+
state["broker_count"] = 1
|
|
600
|
+
state["sidecar_count"] = 1
|
|
601
|
+
state["native_login_count"] = 1
|
|
578
602
|
_atomic_json(state_path, state)
|
|
579
603
|
try:
|
|
580
604
|
startup_path.unlink()
|
|
@@ -603,6 +627,10 @@ def _serve(startup_id: str) -> int:
|
|
|
603
627
|
"started_at": started_at,
|
|
604
628
|
"last_activity": last_activity,
|
|
605
629
|
"login_count": 1,
|
|
630
|
+
"broker_count": state.get("broker_count", 1),
|
|
631
|
+
"sidecar_count": state.get("sidecar_count", 1),
|
|
632
|
+
"native_login_count": state.get("native_login_count", 1),
|
|
633
|
+
"credential_kind": state.get("credential_kind"),
|
|
606
634
|
"dd_dir": state.get("dd_dir"),
|
|
607
635
|
"signature": state.get("signature"),
|
|
608
636
|
}
|
|
@@ -639,9 +667,17 @@ def _serve(startup_id: str) -> int:
|
|
|
639
667
|
connection.sendall((json.dumps(response, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8"))
|
|
640
668
|
return 0
|
|
641
669
|
except Exception as exc:
|
|
670
|
+
if isinstance(exc, FuploadError):
|
|
671
|
+
error = exc.as_dict()
|
|
672
|
+
else:
|
|
673
|
+
error = FuploadError(
|
|
674
|
+
redact(str(exc))[:400] or "DD task session failed to start",
|
|
675
|
+
kind="session_start_failed",
|
|
676
|
+
stage="session",
|
|
677
|
+
).as_dict()
|
|
642
678
|
_atomic_json(startup_path, {
|
|
643
679
|
"startup_id": startup_id,
|
|
644
|
-
"error":
|
|
680
|
+
"error": error,
|
|
645
681
|
})
|
|
646
682
|
return 1
|
|
647
683
|
finally:
|
|
@@ -362,12 +362,13 @@ def _validation_details(probe):
|
|
|
362
362
|
return result
|
|
363
363
|
|
|
364
364
|
|
|
365
|
-
def install_response_probe(client):
|
|
365
|
+
def install_response_probe(client, capture_success=False):
|
|
366
366
|
"""Capture response objects and HTTPError bodies without changing DD behavior."""
|
|
367
367
|
session = getattr(client, "_session", None)
|
|
368
368
|
restore = getattr(client, "_fupload_restore_response_probe", None)
|
|
369
369
|
if callable(restore):
|
|
370
370
|
restore()
|
|
371
|
+
client._fupload_capture_success = bool(capture_success)
|
|
371
372
|
original_opener_open = urllib.request.OpenerDirector.open
|
|
372
373
|
|
|
373
374
|
def opener_open(opener, *args, **kwargs):
|
|
@@ -400,6 +401,7 @@ def install_response_probe(client):
|
|
|
400
401
|
if getattr(urllib.request.OpenerDirector, "open", None) is opener_open:
|
|
401
402
|
urllib.request.OpenerDirector.open = original_opener_open
|
|
402
403
|
client._fupload_restore_response_probe = None
|
|
404
|
+
client._fupload_capture_success = False
|
|
403
405
|
|
|
404
406
|
client._fupload_restore_response_probe = restore_probe
|
|
405
407
|
for method_name in ("get", "post"):
|
|
@@ -415,7 +417,7 @@ def install_response_probe(client):
|
|
|
415
417
|
status = int(getattr(response, "status_code", 0) or 0)
|
|
416
418
|
except (TypeError, ValueError):
|
|
417
419
|
status = 0
|
|
418
|
-
if status >= 400:
|
|
420
|
+
if status >= 400 or getattr(client, "_fupload_capture_success", False):
|
|
419
421
|
try:
|
|
420
422
|
body = str(getattr(response, "text", "") or "")
|
|
421
423
|
except Exception:
|
|
@@ -507,6 +509,59 @@ def failure_from_exception(exc, stage, response=None):
|
|
|
507
509
|
)
|
|
508
510
|
|
|
509
511
|
|
|
512
|
+
def author_login_failure(client):
|
|
513
|
+
probe = getattr(client, "_fupload_last_response_error", None)
|
|
514
|
+
return failure_from_exception(
|
|
515
|
+
RuntimeError("DD author API login failed"), "session", probe,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _last_response_payload(client):
|
|
520
|
+
probe = getattr(client, "_fupload_last_response_error", None)
|
|
521
|
+
if not isinstance(probe, dict) or not probe.get("body"):
|
|
522
|
+
return None
|
|
523
|
+
try:
|
|
524
|
+
payload = json.loads(probe["body"])
|
|
525
|
+
except (TypeError, ValueError):
|
|
526
|
+
return None
|
|
527
|
+
return payload if isinstance(payload, dict) else None
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def login_author_client(client):
|
|
531
|
+
try:
|
|
532
|
+
if client.login():
|
|
533
|
+
return
|
|
534
|
+
if _retryable_get_signature_rejection(_last_response_payload(client)):
|
|
535
|
+
client._fupload_last_response_error = None
|
|
536
|
+
if client.login():
|
|
537
|
+
return
|
|
538
|
+
raise author_login_failure(client)
|
|
539
|
+
finally:
|
|
540
|
+
client._fupload_capture_success = False
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def author_session_or_cleanup(qt, container, flow, jwt_helper, client):
|
|
544
|
+
session = (qt, container, flow, jwt_helper, client)
|
|
545
|
+
try:
|
|
546
|
+
login_author_client(client)
|
|
547
|
+
except Exception:
|
|
548
|
+
try:
|
|
549
|
+
close_session(session)
|
|
550
|
+
except Exception:
|
|
551
|
+
pass
|
|
552
|
+
raise
|
|
553
|
+
return session
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def startup_error_payload(exc):
|
|
557
|
+
if isinstance(exc, SidecarFailure):
|
|
558
|
+
return exc.as_dict()
|
|
559
|
+
return {
|
|
560
|
+
"type": type(exc).__name__,
|
|
561
|
+
"message": safe_exception_message(exc)[:400],
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
510
565
|
def bootstrap():
|
|
511
566
|
resource = os.path.join(DD_DIR, "ccvoicehub.res")
|
|
512
567
|
pyqt_dir = os.path.join(DD_DIR, "ccsub64", "PyQt5")
|
|
@@ -551,10 +606,52 @@ def wait_until(qt, predicate, timeout):
|
|
|
551
606
|
return bool(predicate())
|
|
552
607
|
|
|
553
608
|
|
|
609
|
+
def _enum_name(value):
|
|
610
|
+
name = getattr(value, "name", None)
|
|
611
|
+
return name if isinstance(name, str) else None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _create_relogin_flow(
|
|
615
|
+
account, credential, controller, sdk, cgi, netconfig,
|
|
616
|
+
urs_flow_class, mobile_flow_class,
|
|
617
|
+
):
|
|
618
|
+
method = _enum_name(getattr(account, "method", None))
|
|
619
|
+
credential_type = _enum_name(getattr(credential, "type", None))
|
|
620
|
+
modifier = _enum_name(getattr(credential, "modifier", None))
|
|
621
|
+
|
|
622
|
+
if (method, credential_type, modifier) == ("urs", "urs_token", "normal"):
|
|
623
|
+
return urs_flow_class(controller, sdk, credential.value, account.name)
|
|
624
|
+
if (
|
|
625
|
+
method == "mobile"
|
|
626
|
+
and credential_type == "urs_mobile_token"
|
|
627
|
+
and modifier in ("mobile_password", "mobile_uplink")
|
|
628
|
+
):
|
|
629
|
+
return mobile_flow_class(
|
|
630
|
+
controller, sdk, cgi, netconfig, credential.value,
|
|
631
|
+
modifier == "mobile_password", account.name,
|
|
632
|
+
)
|
|
633
|
+
raise RuntimeError("DD persisted login state uses an unsupported credential combination")
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _credential_kind(account, credential):
|
|
637
|
+
method = _enum_name(getattr(account, "method", None))
|
|
638
|
+
credential_type = _enum_name(getattr(credential, "type", None))
|
|
639
|
+
modifier = _enum_name(getattr(credential, "modifier", None))
|
|
640
|
+
if (method, credential_type, modifier) == ("urs", "urs_token", "normal"):
|
|
641
|
+
return "email"
|
|
642
|
+
if (
|
|
643
|
+
method == "mobile"
|
|
644
|
+
and credential_type == "urs_mobile_token"
|
|
645
|
+
and modifier in ("mobile_password", "mobile_uplink")
|
|
646
|
+
):
|
|
647
|
+
return "mobile"
|
|
648
|
+
raise RuntimeError("DD persisted login state uses an unsupported credential combination")
|
|
649
|
+
|
|
650
|
+
|
|
554
651
|
def open_session(timeout=45):
|
|
555
652
|
from cli_anything.ccvoicehub.core.container import ContainerManager
|
|
556
653
|
from cli_anything.ccvoicehub.core.qt_runtime import QtRuntime
|
|
557
|
-
from components.login.flow import MobileReLoginFlow
|
|
654
|
+
from components.login.flow import MobileReLoginFlow, UrsReLoginFlow
|
|
558
655
|
import components.login.login_controller as login_controller_module
|
|
559
656
|
import logger.cclogger as cclogger
|
|
560
657
|
|
|
@@ -562,7 +659,7 @@ def open_session(timeout=45):
|
|
|
562
659
|
container = ContainerManager.get_container()
|
|
563
660
|
storage = container.get_instance("AccountCredStorage")
|
|
564
661
|
account = storage.getAutoAccount()
|
|
565
|
-
credential = storage.getCred(account)
|
|
662
|
+
credential = storage.getCred(account) if account else None
|
|
566
663
|
if not account or not credential:
|
|
567
664
|
raise RuntimeError("DD has no persisted credential; sign in with the desktop client first")
|
|
568
665
|
cclogger.renameLogAfterLogin = lambda *_args, **_kwargs: None
|
|
@@ -581,11 +678,12 @@ def open_session(timeout=45):
|
|
|
581
678
|
state["jwt"] = True
|
|
582
679
|
|
|
583
680
|
jwt_helper.sigJwtUpdated.connect(jwt_result)
|
|
584
|
-
flow =
|
|
585
|
-
|
|
586
|
-
container.get_instance("
|
|
587
|
-
|
|
681
|
+
flow = _create_relogin_flow(
|
|
682
|
+
account, credential, controller,
|
|
683
|
+
container.get_instance("UrsSDK"), container.get_instance("CgiHelper"),
|
|
684
|
+
container.get_instance("NetConfig"), UrsReLoginFlow, MobileReLoginFlow,
|
|
588
685
|
)
|
|
686
|
+
flow._fupload_credential_kind = _credential_kind(account, credential)
|
|
589
687
|
flow.sigResult.connect(login_result)
|
|
590
688
|
flow.start()
|
|
591
689
|
if not wait_until(qt, lambda: state["done"], timeout) or not state["ok"]:
|
|
@@ -600,16 +698,20 @@ def open_session(timeout=45):
|
|
|
600
698
|
raise RuntimeError("DD NEP module is not initialized")
|
|
601
699
|
client = UiApiClient(nep, login_cookie=getattr(controller, "_cookie", None))
|
|
602
700
|
client._session.headers["User-Agent"] = USER_AGENT
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
install_response_probe(client)
|
|
606
|
-
return qt, container, flow, jwt_helper, client
|
|
701
|
+
install_response_probe(client, capture_success=True)
|
|
702
|
+
return author_session_or_cleanup(qt, container, flow, jwt_helper, client)
|
|
607
703
|
|
|
608
704
|
|
|
609
705
|
def api_result(payload):
|
|
610
706
|
return payload.get("result") if isinstance(payload, dict) else None
|
|
611
707
|
|
|
612
708
|
|
|
709
|
+
def _retryable_get_signature_rejection(payload):
|
|
710
|
+
if not isinstance(payload, dict) or payload.get("code") != 409:
|
|
711
|
+
return False
|
|
712
|
+
return str(payload.get("msg") or payload.get("message") or "").strip() == "签名无效"
|
|
713
|
+
|
|
714
|
+
|
|
613
715
|
def _native_json(value):
|
|
614
716
|
to_json = getattr(value, "toJson", None)
|
|
615
717
|
if callable(to_json):
|
|
@@ -673,7 +775,13 @@ def run_command(session, command):
|
|
|
673
775
|
method = command.get("method", "GET").upper()
|
|
674
776
|
path = command["path"]
|
|
675
777
|
params = command.get("payload") or {}
|
|
676
|
-
|
|
778
|
+
if method == "GET":
|
|
779
|
+
response_payload = client.get(path, params)
|
|
780
|
+
if _retryable_get_signature_rejection(response_payload):
|
|
781
|
+
client._fupload_last_response_error = None
|
|
782
|
+
response_payload = client.get(path, params)
|
|
783
|
+
else:
|
|
784
|
+
response_payload = client.post(path, params)
|
|
677
785
|
if isinstance(response_payload, dict) and response_payload.get("code") not in (None, 0):
|
|
678
786
|
rejected_payload = response_payload
|
|
679
787
|
response_probe = getattr(client, "_fupload_last_response_error", None)
|
|
@@ -831,7 +939,11 @@ def main():
|
|
|
831
939
|
session = None
|
|
832
940
|
try:
|
|
833
941
|
session = open_session()
|
|
834
|
-
output({
|
|
942
|
+
output({
|
|
943
|
+
"ready": True,
|
|
944
|
+
"device_state_created": created,
|
|
945
|
+
"credential_kind": getattr(session[2], "_fupload_credential_kind", None),
|
|
946
|
+
})
|
|
835
947
|
for line in sys.stdin:
|
|
836
948
|
if not line.strip():
|
|
837
949
|
continue
|
|
@@ -846,10 +958,7 @@ def main():
|
|
|
846
958
|
"error": error})
|
|
847
959
|
return 0
|
|
848
960
|
except Exception as exc:
|
|
849
|
-
output({"ready": False, "error":
|
|
850
|
-
"type": type(exc).__name__,
|
|
851
|
-
"message": safe_exception_message(exc)[:400],
|
|
852
|
-
}})
|
|
961
|
+
output({"ready": False, "error": startup_error_payload(exc)})
|
|
853
962
|
return 1
|
|
854
963
|
finally:
|
|
855
964
|
machine_data.clientNo = original
|
|
@@ -93,7 +93,9 @@ def sanitize_output(value: Any) -> Any:
|
|
|
93
93
|
result: Dict[str, Any] = {}
|
|
94
94
|
for key, item in value.items():
|
|
95
95
|
normalized = str(key).replace("-", "_").lower()
|
|
96
|
-
if normalized
|
|
96
|
+
if normalized == "credential_kind" and item in {"email", "mobile"}:
|
|
97
|
+
result[key] = item
|
|
98
|
+
elif normalized in {"token_present", "token_decrypted", "token_nonempty", "api_ready"} and isinstance(item, bool):
|
|
97
99
|
result[key] = item
|
|
98
100
|
elif normalized in _SENSITIVE_KEYS or any(
|
|
99
101
|
marker in normalized
|
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.17",
|
|
5
|
+
"skill_version": "0.0.17",
|
|
6
|
+
"tree_sha256": "064aeb1a1291d240a3bea7e71458846395abe29c6e91f682d269ec0e684c4294",
|
|
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":
|
|
91
|
-
"sha256": "
|
|
90
|
+
"bytes": 19727,
|
|
91
|
+
"sha256": "2804e4b77c000d0bd12ca3f69bbcd5720f759fd4e84b4127ea3461093bf5da7c"
|
|
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": "
|
|
116
|
+
"sha256": "8e72acb94a18f173cf9cbbf0436b20f64e1991f54a3537b7e930b4196bab8439"
|
|
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":
|
|
141
|
-
"sha256": "
|
|
140
|
+
"bytes": 28248,
|
|
141
|
+
"sha256": "6bbae4ed7aef4feca552816d7dbefa6069dc3d1d9a39a4af35582a06db4ce2d5"
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
"path": "scripts/fupload_cli/dd_sidecar.py",
|
|
145
|
-
"bytes":
|
|
146
|
-
"sha256": "
|
|
145
|
+
"bytes": 39150,
|
|
146
|
+
"sha256": "42791c82e5518838aedbe209823b94deb7de09b2f3eccb4fe563364f58becc6c"
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
"path": "scripts/fupload_cli/dd.py",
|
|
150
|
-
"bytes":
|
|
151
|
-
"sha256": "
|
|
150
|
+
"bytes": 114434,
|
|
151
|
+
"sha256": "cf808efdd4a93d80bddb533af4ac14696e889e0fd359acdacd58a7a49ce050b0"
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"path": "scripts/fupload_cli/errors.py",
|
|
@@ -157,8 +157,8 @@
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"path": "scripts/fupload_cli/io.py",
|
|
160
|
-
"bytes":
|
|
161
|
-
"sha256": "
|
|
160
|
+
"bytes": 5436,
|
|
161
|
+
"sha256": "4e5929f80223493057244613caf23575275c5838bf6505da0f3b01c5bae791da"
|
|
162
162
|
},
|
|
163
163
|
{
|
|
164
164
|
"path": "scripts/fupload_cli/modus_zip.py",
|
|
@@ -207,8 +207,8 @@
|
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
"path": "SKILL.md",
|
|
210
|
-
"bytes":
|
|
211
|
-
"sha256": "
|
|
210
|
+
"bytes": 28383,
|
|
211
|
+
"sha256": "9aebce883e6d83f0e3c8b36f497e59eb6f4df5e1e4af231df60ca3299188fb86"
|
|
212
212
|
}
|
|
213
213
|
]
|
|
214
214
|
}
|