@follenfang/fupload 0.0.12 → 0.0.16
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/references/modus.md +228 -64
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/cli.py +15 -3
- package/fupload/scripts/fupload_cli/dd.py +17 -0
- package/fupload/scripts/fupload_cli/dd_broker.py +58 -14
- package/fupload/scripts/fupload_cli/dd_sidecar.py +67 -8
- package/fupload/scripts/fupload_cli/io.py +20 -2
- package/fupload/scripts/fupload_cli/modus.py +279 -14
- package/fupload/scripts/fupload_cli/schema.py +53 -7
- package/fupload/scripts/fupload_cli/transport.py +13 -1
- package/npm/skill-manifest.json +26 -26
- package/package.json +1 -1
|
@@ -282,6 +282,31 @@ def _load_live_state() -> Optional[Dict[str, Any]]:
|
|
|
282
282
|
return value
|
|
283
283
|
|
|
284
284
|
|
|
285
|
+
def _remove_session_state(path: Path, session_id: str, timeout: float = 5) -> bool:
|
|
286
|
+
deadline = time.monotonic() + timeout
|
|
287
|
+
while True:
|
|
288
|
+
try:
|
|
289
|
+
current = _read_json(path)
|
|
290
|
+
except FuploadError as exc:
|
|
291
|
+
if isinstance(exc.__cause__, FileNotFoundError):
|
|
292
|
+
return True
|
|
293
|
+
if not isinstance(exc.__cause__, OSError):
|
|
294
|
+
return False
|
|
295
|
+
else:
|
|
296
|
+
if current.get("session_id") != session_id:
|
|
297
|
+
return False
|
|
298
|
+
try:
|
|
299
|
+
path.unlink()
|
|
300
|
+
return True
|
|
301
|
+
except FileNotFoundError:
|
|
302
|
+
return True
|
|
303
|
+
except OSError:
|
|
304
|
+
pass
|
|
305
|
+
if time.monotonic() >= deadline:
|
|
306
|
+
return False
|
|
307
|
+
time.sleep(0.05)
|
|
308
|
+
|
|
309
|
+
|
|
285
310
|
def doctor() -> Dict[str, Any]:
|
|
286
311
|
dd_dir, signature = _dd_module().discover_dd_info()
|
|
287
312
|
processes = running_dd_processes()
|
|
@@ -390,6 +415,10 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
|
390
415
|
"running": True,
|
|
391
416
|
"reused": True,
|
|
392
417
|
"login_count": active.get("login_count", 1),
|
|
418
|
+
"broker_count": active.get("broker_count", 1),
|
|
419
|
+
"sidecar_count": active.get("sidecar_count", 1),
|
|
420
|
+
"native_login_count": active.get("native_login_count", 1),
|
|
421
|
+
"credential_kind": active.get("credential_kind"),
|
|
393
422
|
}
|
|
394
423
|
processes = running_dd_processes()
|
|
395
424
|
if processes and not confirm_close_gui:
|
|
@@ -427,6 +456,10 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
|
|
|
427
456
|
"running": True,
|
|
428
457
|
"reused": False,
|
|
429
458
|
"login_count": 1,
|
|
459
|
+
"broker_count": state.get("broker_count", 1),
|
|
460
|
+
"sidecar_count": state.get("sidecar_count", 1),
|
|
461
|
+
"native_login_count": state.get("native_login_count", 1),
|
|
462
|
+
"credential_kind": state.get("credential_kind"),
|
|
430
463
|
"closed_gui_processes": len(processes),
|
|
431
464
|
}
|
|
432
465
|
if startup.exists():
|
|
@@ -471,15 +504,23 @@ def status(session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
|
471
504
|
|
|
472
505
|
def stop(session_id: str) -> Dict[str, Any]:
|
|
473
506
|
data = _send({"session_id": session_id, "command": "stop"}, timeout=30)
|
|
474
|
-
deadline = time.
|
|
475
|
-
while
|
|
507
|
+
deadline = time.monotonic() + 30
|
|
508
|
+
while True:
|
|
509
|
+
try:
|
|
510
|
+
state = _load_live_state()
|
|
511
|
+
except FuploadError as exc:
|
|
512
|
+
if exc.kind != "session_error" or not isinstance(exc.__cause__, OSError):
|
|
513
|
+
raise
|
|
514
|
+
else:
|
|
515
|
+
if not state:
|
|
516
|
+
break
|
|
517
|
+
if time.monotonic() >= deadline:
|
|
518
|
+
raise FuploadError(
|
|
519
|
+
"DD task session acknowledged stop but did not finish cleanup",
|
|
520
|
+
kind="session_stop_failed",
|
|
521
|
+
stage="session",
|
|
522
|
+
)
|
|
476
523
|
time.sleep(0.05)
|
|
477
|
-
if _load_live_state():
|
|
478
|
-
raise FuploadError(
|
|
479
|
-
"DD task session acknowledged stop but did not finish cleanup",
|
|
480
|
-
kind="session_stop_failed",
|
|
481
|
-
stage="session",
|
|
482
|
-
)
|
|
483
524
|
result = dict(data or {})
|
|
484
525
|
result["cleanup_complete"] = True
|
|
485
526
|
return result
|
|
@@ -542,6 +583,10 @@ def _serve(startup_id: str) -> int:
|
|
|
542
583
|
sidecar = _dd_module().Sidecar().__enter__()
|
|
543
584
|
state["dd_dir"] = str(sidecar.dd_dir)
|
|
544
585
|
state["signature"] = sidecar.signature
|
|
586
|
+
state["credential_kind"] = sidecar.credential_kind
|
|
587
|
+
state["broker_count"] = 1
|
|
588
|
+
state["sidecar_count"] = 1
|
|
589
|
+
state["native_login_count"] = 1
|
|
545
590
|
_atomic_json(state_path, state)
|
|
546
591
|
try:
|
|
547
592
|
startup_path.unlink()
|
|
@@ -570,6 +615,10 @@ def _serve(startup_id: str) -> int:
|
|
|
570
615
|
"started_at": started_at,
|
|
571
616
|
"last_activity": last_activity,
|
|
572
617
|
"login_count": 1,
|
|
618
|
+
"broker_count": state.get("broker_count", 1),
|
|
619
|
+
"sidecar_count": state.get("sidecar_count", 1),
|
|
620
|
+
"native_login_count": state.get("native_login_count", 1),
|
|
621
|
+
"credential_kind": state.get("credential_kind"),
|
|
573
622
|
"dd_dir": state.get("dd_dir"),
|
|
574
623
|
"signature": state.get("signature"),
|
|
575
624
|
}
|
|
@@ -615,12 +664,7 @@ def _serve(startup_id: str) -> int:
|
|
|
615
664
|
listener.close()
|
|
616
665
|
if sidecar is not None:
|
|
617
666
|
sidecar.__exit__(None, None, None)
|
|
618
|
-
|
|
619
|
-
current = _read_json(state_path)
|
|
620
|
-
if current.get("session_id") == session_id:
|
|
621
|
-
state_path.unlink()
|
|
622
|
-
except (FuploadError, OSError):
|
|
623
|
-
pass
|
|
667
|
+
_remove_session_state(state_path, session_id)
|
|
624
668
|
|
|
625
669
|
|
|
626
670
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
@@ -551,10 +551,52 @@ def wait_until(qt, predicate, timeout):
|
|
|
551
551
|
return bool(predicate())
|
|
552
552
|
|
|
553
553
|
|
|
554
|
+
def _enum_name(value):
|
|
555
|
+
name = getattr(value, "name", None)
|
|
556
|
+
return name if isinstance(name, str) else None
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _create_relogin_flow(
|
|
560
|
+
account, credential, controller, sdk, cgi, netconfig,
|
|
561
|
+
urs_flow_class, mobile_flow_class,
|
|
562
|
+
):
|
|
563
|
+
method = _enum_name(getattr(account, "method", None))
|
|
564
|
+
credential_type = _enum_name(getattr(credential, "type", None))
|
|
565
|
+
modifier = _enum_name(getattr(credential, "modifier", None))
|
|
566
|
+
|
|
567
|
+
if (method, credential_type, modifier) == ("urs", "urs_token", "normal"):
|
|
568
|
+
return urs_flow_class(controller, sdk, credential.value, account.name)
|
|
569
|
+
if (
|
|
570
|
+
method == "mobile"
|
|
571
|
+
and credential_type == "urs_mobile_token"
|
|
572
|
+
and modifier in ("mobile_password", "mobile_uplink")
|
|
573
|
+
):
|
|
574
|
+
return mobile_flow_class(
|
|
575
|
+
controller, sdk, cgi, netconfig, credential.value,
|
|
576
|
+
modifier == "mobile_password", account.name,
|
|
577
|
+
)
|
|
578
|
+
raise RuntimeError("DD persisted login state uses an unsupported credential combination")
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _credential_kind(account, credential):
|
|
582
|
+
method = _enum_name(getattr(account, "method", None))
|
|
583
|
+
credential_type = _enum_name(getattr(credential, "type", None))
|
|
584
|
+
modifier = _enum_name(getattr(credential, "modifier", None))
|
|
585
|
+
if (method, credential_type, modifier) == ("urs", "urs_token", "normal"):
|
|
586
|
+
return "email"
|
|
587
|
+
if (
|
|
588
|
+
method == "mobile"
|
|
589
|
+
and credential_type == "urs_mobile_token"
|
|
590
|
+
and modifier in ("mobile_password", "mobile_uplink")
|
|
591
|
+
):
|
|
592
|
+
return "mobile"
|
|
593
|
+
raise RuntimeError("DD persisted login state uses an unsupported credential combination")
|
|
594
|
+
|
|
595
|
+
|
|
554
596
|
def open_session(timeout=45):
|
|
555
597
|
from cli_anything.ccvoicehub.core.container import ContainerManager
|
|
556
598
|
from cli_anything.ccvoicehub.core.qt_runtime import QtRuntime
|
|
557
|
-
from components.login.flow import MobileReLoginFlow
|
|
599
|
+
from components.login.flow import MobileReLoginFlow, UrsReLoginFlow
|
|
558
600
|
import components.login.login_controller as login_controller_module
|
|
559
601
|
import logger.cclogger as cclogger
|
|
560
602
|
|
|
@@ -562,7 +604,7 @@ def open_session(timeout=45):
|
|
|
562
604
|
container = ContainerManager.get_container()
|
|
563
605
|
storage = container.get_instance("AccountCredStorage")
|
|
564
606
|
account = storage.getAutoAccount()
|
|
565
|
-
credential = storage.getCred(account)
|
|
607
|
+
credential = storage.getCred(account) if account else None
|
|
566
608
|
if not account or not credential:
|
|
567
609
|
raise RuntimeError("DD has no persisted credential; sign in with the desktop client first")
|
|
568
610
|
cclogger.renameLogAfterLogin = lambda *_args, **_kwargs: None
|
|
@@ -581,11 +623,12 @@ def open_session(timeout=45):
|
|
|
581
623
|
state["jwt"] = True
|
|
582
624
|
|
|
583
625
|
jwt_helper.sigJwtUpdated.connect(jwt_result)
|
|
584
|
-
flow =
|
|
585
|
-
|
|
586
|
-
container.get_instance("
|
|
587
|
-
|
|
626
|
+
flow = _create_relogin_flow(
|
|
627
|
+
account, credential, controller,
|
|
628
|
+
container.get_instance("UrsSDK"), container.get_instance("CgiHelper"),
|
|
629
|
+
container.get_instance("NetConfig"), UrsReLoginFlow, MobileReLoginFlow,
|
|
588
630
|
)
|
|
631
|
+
flow._fupload_credential_kind = _credential_kind(account, credential)
|
|
589
632
|
flow.sigResult.connect(login_result)
|
|
590
633
|
flow.start()
|
|
591
634
|
if not wait_until(qt, lambda: state["done"], timeout) or not state["ok"]:
|
|
@@ -610,6 +653,12 @@ def api_result(payload):
|
|
|
610
653
|
return payload.get("result") if isinstance(payload, dict) else None
|
|
611
654
|
|
|
612
655
|
|
|
656
|
+
def _retryable_get_signature_rejection(payload):
|
|
657
|
+
if not isinstance(payload, dict) or payload.get("code") != 409:
|
|
658
|
+
return False
|
|
659
|
+
return str(payload.get("msg") or payload.get("message") or "").strip() == "签名无效"
|
|
660
|
+
|
|
661
|
+
|
|
613
662
|
def _native_json(value):
|
|
614
663
|
to_json = getattr(value, "toJson", None)
|
|
615
664
|
if callable(to_json):
|
|
@@ -673,7 +722,13 @@ def run_command(session, command):
|
|
|
673
722
|
method = command.get("method", "GET").upper()
|
|
674
723
|
path = command["path"]
|
|
675
724
|
params = command.get("payload") or {}
|
|
676
|
-
|
|
725
|
+
if method == "GET":
|
|
726
|
+
response_payload = client.get(path, params)
|
|
727
|
+
if _retryable_get_signature_rejection(response_payload):
|
|
728
|
+
client._fupload_last_response_error = None
|
|
729
|
+
response_payload = client.get(path, params)
|
|
730
|
+
else:
|
|
731
|
+
response_payload = client.post(path, params)
|
|
677
732
|
if isinstance(response_payload, dict) and response_payload.get("code") not in (None, 0):
|
|
678
733
|
rejected_payload = response_payload
|
|
679
734
|
response_probe = getattr(client, "_fupload_last_response_error", None)
|
|
@@ -831,7 +886,11 @@ def main():
|
|
|
831
886
|
session = None
|
|
832
887
|
try:
|
|
833
888
|
session = open_session()
|
|
834
|
-
output({
|
|
889
|
+
output({
|
|
890
|
+
"ready": True,
|
|
891
|
+
"device_state_created": created,
|
|
892
|
+
"credential_kind": getattr(session[2], "_fupload_credential_kind", None),
|
|
893
|
+
})
|
|
835
894
|
for line in sys.stdin:
|
|
836
895
|
if not line.strip():
|
|
837
896
|
continue
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
5
7
|
import hashlib
|
|
6
8
|
import json
|
|
7
9
|
import sys
|
|
@@ -22,8 +24,10 @@ _SENSITIVE_KEYS = {
|
|
|
22
24
|
}
|
|
23
25
|
_RAW_CONTENT_KEYS = {
|
|
24
26
|
"content", "wa_str", "t_wa_str", "raw_wtf", "wtf_zip", "download_url",
|
|
25
|
-
"import_string",
|
|
27
|
+
"import_string", "content_text", "contenttext", "code_text", "codetext",
|
|
28
|
+
"description", "changelog", "license_content", "licensecontent",
|
|
26
29
|
}
|
|
30
|
+
_BASE64_CONTENT_KEYS = {"base64", "logo_base64", "screenshot_base64s"}
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class _DuplicateKey(ValueError):
|
|
@@ -89,7 +93,9 @@ def sanitize_output(value: Any) -> Any:
|
|
|
89
93
|
result: Dict[str, Any] = {}
|
|
90
94
|
for key, item in value.items():
|
|
91
95
|
normalized = str(key).replace("-", "_").lower()
|
|
92
|
-
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):
|
|
93
99
|
result[key] = item
|
|
94
100
|
elif normalized in _SENSITIVE_KEYS or any(
|
|
95
101
|
marker in normalized
|
|
@@ -102,6 +108,18 @@ def sanitize_output(value: Any) -> Any:
|
|
|
102
108
|
"bytes": len(text),
|
|
103
109
|
"sha256": hashlib.sha256(text).hexdigest(),
|
|
104
110
|
}
|
|
111
|
+
elif normalized in _BASE64_CONTENT_KEYS and isinstance(item, str):
|
|
112
|
+
try:
|
|
113
|
+
content = base64.b64decode(item, validate=True)
|
|
114
|
+
encoding = "base64"
|
|
115
|
+
except (binascii.Error, ValueError):
|
|
116
|
+
content = item.encode("utf-8")
|
|
117
|
+
encoding = "utf-8"
|
|
118
|
+
result[str(key) + "_summary"] = {
|
|
119
|
+
"bytes": len(content),
|
|
120
|
+
"sha256": hashlib.sha256(content).hexdigest(),
|
|
121
|
+
"source_encoding": encoding,
|
|
122
|
+
}
|
|
105
123
|
else:
|
|
106
124
|
result[key] = sanitize_output(item)
|
|
107
125
|
return result
|