@follenfang/fupload 0.0.10 → 0.0.11
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 +1 -1
- package/fupload/references/modus.md +37 -1
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/modus.py +126 -47
- package/fupload/scripts/fupload_cli/state_machine.py +40 -12
- package/npm/skill-manifest.json +11 -11
- 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.11"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Fupload
|
|
@@ -27,6 +27,39 @@ The complete project model includes `project_id`, `name`, `alt_name`, `summary`,
|
|
|
27
27
|
`required_tier_id`, `required_dependencies`, `cf_url`, returned `logo`, `status`,
|
|
28
28
|
`game`, and `description`. Write inputs use `project_state`, `publish_platforms`,
|
|
29
29
|
`logo_base64`, the compatibility `screenshot_base64s`, and edit-only `image_ops`.
|
|
30
|
+
|
|
31
|
+
### Project field contract
|
|
32
|
+
|
|
33
|
+
| CLI/state field | Creator wire/readback | JSON type | Required/default | Source and state dependency | Successful readback |
|
|
34
|
+
| --- | --- | --- | --- | --- | --- |
|
|
35
|
+
| `schema` | none | string | required by every write document | versioned CLI envelope | not sent |
|
|
36
|
+
| `project_id` | `projectId` | positive integer | edit/delete required; create assigned by server | outer write document, not form state | `projectId` |
|
|
37
|
+
| `game` | none confirmed | object or non-empty string | required; no default | `choose_game`; object needs a stable ID/key | preserved in state snapshot |
|
|
38
|
+
| `name` | `name` | non-empty string | required; no default | `basic_info` | `name` |
|
|
39
|
+
| `alt_name` | `altName` | string/null | optional; omitted preserves value | `basic_info`; edit clear uses `<null>` | `altName` |
|
|
40
|
+
| `summary` | `summary` | non-empty string | required; no default | `basic_info` | `summary` |
|
|
41
|
+
| `categories` | comma-delimited `categories` | 1-5 positive integer IDs | required; no default | service `categories` enum; `basic_info`; ID 998 requires BigFoot only | comma-delimited `categories` |
|
|
42
|
+
| `publish_platforms` | derived `synchronizationType` | non-empty unique enum array | required; no default | `basic_info`; values `modus`, `bigfoot`, or both | integer `1`, `2`, or `3` |
|
|
43
|
+
| `synchronization_type` | `synchronizationType` | integer | derived; caller value is replaced | derived from platform toggles in `basic_info` | `synchronizationType` |
|
|
44
|
+
| `required_tier_id` | `requiredTierId` | null for this account | defaults to null | service `subscription-tiers` enum; current result is `[]`; BigFoot requires null | `requiredTierId: null` |
|
|
45
|
+
| `repo_url` | `repoUrl` | string/null | optional; omitted preserves value | `basic_info`; create empty is omitted; edit clear uses `<null>` | `repoUrl` |
|
|
46
|
+
| `logo_base64` | `screenshotBase64sReqs.screenshotBase64s` | base64 string | optional; default empty string | create-only `basic_info`; sent as `logo.webp` | server-managed `logo` path |
|
|
47
|
+
| `screenshot_base64s` | same create logo payload | string array | compatibility alias; default empty array | create-only `basic_info`; first value is used when `logo_base64` is absent | server-managed `logo` path |
|
|
48
|
+
| `license.type` | `license` JSON string `type` | non-empty string | required; no default | separate `license` step | parsed `license` |
|
|
49
|
+
| `license.holder/year/content` | fields inside `license` JSON string | non-empty strings | optional except custom content | separate `license` step; custom requires content | parsed `license` |
|
|
50
|
+
| `description` | `description` | string/null | edit-only; omitted preserves value | edit `basic_info`; clear uses `<null>` | `description` |
|
|
51
|
+
| `required_dependencies` | `requiredDependencies` | string/null | edit-only; omitted preserves value | edit `basic_info`; clear uses `<null>`; dependency query is dynamic | `requiredDependencies` |
|
|
52
|
+
| `images` | `images` | non-negative integer | edit-only; required with `image_ops` | edit `basic_info` | `images` count |
|
|
53
|
+
| `image_ops` | `imagesOps` | operation array | edit-only; no default | edit `basic_info`; upload requires base64, delete forbids it | `images` count |
|
|
54
|
+
| `cf_url` | `cfUrl` | string/null | read-only | known detail field may be preserved in a snapshot but is never sent | `cfUrl` |
|
|
55
|
+
| `logo` | `logo` | string/object/null | read-only | server-managed detail field; create uses `logo_base64` instead | `logo` |
|
|
56
|
+
| `status` | `status` | integer/null | read-only | server-managed detail field | `status` |
|
|
57
|
+
| `project_state` | none | object | required and must be `complete` | wraps `choose_game -> basic_info -> license`; unknown nested fields are rejected | local resumable snapshot |
|
|
58
|
+
|
|
59
|
+
Create-only, edit-only, and read-only fields may be preserved in a resumable
|
|
60
|
+
snapshot, but `_project_wire` sends only fields supported by the selected API
|
|
61
|
+
operation. The state machine rejects unknown `basic_info` and `license` fields
|
|
62
|
+
before changing state.
|
|
30
63
|
`publish_platforms` is a non-empty array containing `modus`,
|
|
31
64
|
`bigfoot`, or both; the two values are not mutually exclusive. Creator derives
|
|
32
65
|
wire `synchronizationType` as 1 for ModUs, 2 for BigFoot, and 3 for both.
|
|
@@ -64,7 +97,10 @@ Deletion requires `confirm: "DELETE"`.
|
|
|
64
97
|
`transaction_log` optionally selects the redacted transaction record path.
|
|
65
98
|
Successful records include the completed `file_id`, metadata, signature, and
|
|
66
99
|
binary upload stages that ran; failed records include `failed_stage`, a redacted
|
|
67
|
-
error, and whether the ZIP was retained. The
|
|
100
|
+
error, and whether the ZIP was retained. The transaction record is established
|
|
101
|
+
before file-ID allocation and ZIP preflight, so those early failures are also
|
|
102
|
+
durable. Binary-upload errors expose only a query-free endpoint identifier,
|
|
103
|
+
status when available, and a bounded redacted response summary. The provider parses every ZIP `.toc`
|
|
68
104
|
`Interface` value to derive
|
|
69
105
|
`toc_version` and `supported_game_versions`; explicit caller values must match.
|
|
70
106
|
The provider validates the ZIP, computes `md5`, `zip_size`, and `unzip_size`,
|
|
@@ -633,12 +633,21 @@ class ModUs:
|
|
|
633
633
|
if size > MAX_PACKAGE_BYTES:
|
|
634
634
|
raise ValidationError("release ZIP exceeds the 200 MB upload limit", path=file_path)
|
|
635
635
|
parsed = urllib.parse.urlsplit(signed_url)
|
|
636
|
+
if parsed.scheme not in ("http", "https") or not parsed.hostname:
|
|
637
|
+
raise ValidationError("release upload URL must be HTTP(S)", path="signed_url")
|
|
638
|
+
# The query carries the object-store signature. Keep a useful endpoint
|
|
639
|
+
# in errors without ever persisting that credential material.
|
|
640
|
+
endpoint_host = parsed.hostname
|
|
641
|
+
if parsed.port is not None:
|
|
642
|
+
endpoint_host = "%s:%d" % (endpoint_host, parsed.port)
|
|
643
|
+
endpoint = urllib.parse.urlunsplit((parsed.scheme, endpoint_host, parsed.path or "/", "", ""))
|
|
636
644
|
conn_cls = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
|
|
637
|
-
|
|
638
|
-
conn = conn_cls(parsed.hostname, parsed.port, timeout=max(self.timeout, 600), context=ssl.create_default_context())
|
|
639
|
-
else:
|
|
640
|
-
conn = conn_cls(parsed.hostname, parsed.port, timeout=max(self.timeout, 600))
|
|
645
|
+
conn = None
|
|
641
646
|
try:
|
|
647
|
+
if parsed.scheme == "https":
|
|
648
|
+
conn = conn_cls(parsed.hostname, parsed.port, timeout=max(self.timeout, 600), context=ssl.create_default_context())
|
|
649
|
+
else:
|
|
650
|
+
conn = conn_cls(parsed.hostname, parsed.port, timeout=max(self.timeout, 600))
|
|
642
651
|
conn.putrequest("PUT", urllib.parse.urlunsplit(("", "", parsed.path or "/", parsed.query, "")))
|
|
643
652
|
conn.putheader("Content-Type", "application/zip")
|
|
644
653
|
conn.putheader("Content-Length", str(size))
|
|
@@ -650,15 +659,33 @@ class ModUs:
|
|
|
650
659
|
break
|
|
651
660
|
conn.send(chunk)
|
|
652
661
|
response = conn.getresponse()
|
|
653
|
-
response.read()
|
|
662
|
+
raw = response.read(4096)
|
|
654
663
|
if response.status < 200 or response.status >= 300:
|
|
655
|
-
|
|
664
|
+
summary = redact(raw.decode("utf-8", errors="replace")) if raw else "<empty>"
|
|
665
|
+
raise FuploadError(
|
|
666
|
+
"ModUs binary upload returned HTTP %d" % response.status,
|
|
667
|
+
endpoint=endpoint,
|
|
668
|
+
http_status=response.status,
|
|
669
|
+
verification_required=True,
|
|
670
|
+
stage="binary_upload",
|
|
671
|
+
details={"response_summary": summary},
|
|
672
|
+
)
|
|
656
673
|
except FuploadError:
|
|
657
674
|
raise
|
|
658
|
-
except OSError as exc:
|
|
659
|
-
raise FuploadError(
|
|
675
|
+
except (OSError, http.client.HTTPException) as exc:
|
|
676
|
+
raise FuploadError(
|
|
677
|
+
"ModUs binary upload failed",
|
|
678
|
+
endpoint=endpoint,
|
|
679
|
+
verification_required=True,
|
|
680
|
+
stage="binary_upload",
|
|
681
|
+
details={"response_summary": redact(str(exc)) or exc.__class__.__name__},
|
|
682
|
+
) from exc
|
|
660
683
|
finally:
|
|
661
|
-
conn
|
|
684
|
+
if conn is not None:
|
|
685
|
+
try:
|
|
686
|
+
conn.close()
|
|
687
|
+
except OSError:
|
|
688
|
+
pass
|
|
662
689
|
return {"status": "uploaded", "archive": path.name, "bytes": size}
|
|
663
690
|
|
|
664
691
|
def release_delete(self, project_id: int, file_id: int) -> Any:
|
|
@@ -666,70 +693,108 @@ class ModUs:
|
|
|
666
693
|
|
|
667
694
|
def publish(self, doc: Mapping[str, Any], *, update: bool = False) -> Dict[str, Any]:
|
|
668
695
|
project_id = _positive_id(doc["project_id"], field="project_id")
|
|
669
|
-
allocated_file_id = not bool(doc.get("file_id"))
|
|
670
|
-
file_id = _positive_id(doc.get("file_id") or self.release_file_id(project_id), field="file_id")
|
|
671
|
-
metadata = {k: doc[k] for k in ("project_id", "version", "type", "supported_game_versions", "toc_version", "changelog", "path") if k in doc}
|
|
672
|
-
metadata["file_id"] = file_id
|
|
673
696
|
file_path = doc.get("file")
|
|
674
|
-
if file_path is None and not update:
|
|
675
|
-
raise ValidationError("release ZIP file is required", path="file")
|
|
676
697
|
archive = Path(str(file_path)) if file_path is not None else None
|
|
677
|
-
|
|
678
|
-
if not archive.is_file():
|
|
679
|
-
raise ValidationError("release ZIP file does not exist", path=str(file_path))
|
|
680
|
-
size = archive.stat().st_size
|
|
681
|
-
if size > MAX_PACKAGE_BYTES:
|
|
682
|
-
raise ValidationError("release ZIP exceeds the 200 MB upload limit", path=str(file_path))
|
|
683
|
-
md5 = hashlib.md5(archive.read_bytes()).hexdigest()
|
|
684
|
-
try:
|
|
685
|
-
unzip_size = sum(info.file_size for info in zipfile.ZipFile(str(archive)).infolist())
|
|
686
|
-
except (OSError, zipfile.BadZipFile) as exc:
|
|
687
|
-
raise ValidationError("release file is not a valid ZIP", path=str(file_path)) from exc
|
|
688
|
-
derived = parse_modus_zip(archive)
|
|
689
|
-
supplied_toc = doc.get("toc_version")
|
|
690
|
-
supplied_games = doc.get("supported_game_versions")
|
|
691
|
-
if supplied_toc is not None and str(supplied_toc) != derived["toc_version"]:
|
|
692
|
-
raise ValidationError("toc_version does not match the ZIP Interface field", path="$.toc_version")
|
|
693
|
-
if supplied_games is not None and _supported_game_versions(supplied_games) != derived["supported_game_versions"]:
|
|
694
|
-
raise ValidationError("supported_game_versions does not match the ZIP Interface field", path="$.supported_game_versions")
|
|
695
|
-
metadata.update({
|
|
696
|
-
"md5": md5,
|
|
697
|
-
"zip_size": size,
|
|
698
|
-
"unzip_size": unzip_size,
|
|
699
|
-
"toc_version": derived["toc_version"],
|
|
700
|
-
"supported_game_versions": derived["supported_game_versions"],
|
|
701
|
-
"path": doc.get("path") or archive.name,
|
|
702
|
-
})
|
|
698
|
+
supplied_file_id = doc.get("file_id")
|
|
703
699
|
transaction: Dict[str, Any] = {
|
|
704
700
|
"schema": "fupload.v1.modus.upload-transaction",
|
|
705
701
|
"created_at": int(time.time()),
|
|
706
702
|
"project_id": project_id,
|
|
707
|
-
"file_id":
|
|
703
|
+
"file_id": None,
|
|
708
704
|
"update": bool(update),
|
|
709
705
|
"archive": str(archive) if archive is not None else None,
|
|
710
|
-
"stages": [
|
|
706
|
+
"stages": [],
|
|
711
707
|
}
|
|
712
708
|
transaction_path = Path(str(doc.get("transaction_log") or ((str(archive) + ".modus-transaction.json") if archive else "modus-transaction.json")))
|
|
713
709
|
|
|
714
710
|
def save_transaction() -> None:
|
|
715
711
|
transaction_path.write_text(json.dumps(transaction, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
716
712
|
|
|
713
|
+
# Establish the audit record before reserving a file ID or validating
|
|
714
|
+
# the archive so every publish failure has a durable transaction.
|
|
717
715
|
try:
|
|
718
|
-
|
|
716
|
+
save_transaction()
|
|
717
|
+
except OSError as exc:
|
|
718
|
+
raise FuploadError(
|
|
719
|
+
"ModUs upload transaction could not be written",
|
|
720
|
+
stage="transaction_log",
|
|
721
|
+
details={"path": str(transaction_path)},
|
|
722
|
+
) from exc
|
|
723
|
+
|
|
724
|
+
current_stage = "prepare"
|
|
725
|
+
try:
|
|
726
|
+
allocated_file_id = not bool(supplied_file_id)
|
|
727
|
+
current_stage = "file_id"
|
|
728
|
+
transaction["active_stage"] = current_stage
|
|
729
|
+
save_transaction()
|
|
730
|
+
if allocated_file_id:
|
|
731
|
+
transaction["stages"].append("file_id")
|
|
732
|
+
file_id = _positive_id(self.release_file_id(project_id), field="file_id")
|
|
733
|
+
else:
|
|
734
|
+
file_id = _positive_id(supplied_file_id, field="file_id")
|
|
735
|
+
transaction["file_id"] = file_id
|
|
736
|
+
|
|
737
|
+
metadata = {k: doc[k] for k in ("project_id", "version", "type", "supported_game_versions", "toc_version", "changelog", "path") if k in doc}
|
|
738
|
+
metadata["file_id"] = file_id
|
|
739
|
+
current_stage = "zip_preflight"
|
|
740
|
+
transaction["active_stage"] = current_stage
|
|
741
|
+
save_transaction()
|
|
742
|
+
if archive is None and not update:
|
|
743
|
+
raise ValidationError("release ZIP file is required", path="file")
|
|
744
|
+
if archive is not None:
|
|
745
|
+
if not archive.is_file():
|
|
746
|
+
raise ValidationError("release ZIP file does not exist", path=str(file_path))
|
|
747
|
+
size = archive.stat().st_size
|
|
748
|
+
if size > MAX_PACKAGE_BYTES:
|
|
749
|
+
raise ValidationError("release ZIP exceeds the 200 MB upload limit", path=str(file_path))
|
|
750
|
+
md5 = hashlib.md5(archive.read_bytes()).hexdigest()
|
|
751
|
+
try:
|
|
752
|
+
unzip_size = sum(info.file_size for info in zipfile.ZipFile(str(archive)).infolist())
|
|
753
|
+
except (OSError, zipfile.BadZipFile) as exc:
|
|
754
|
+
raise ValidationError("release file is not a valid ZIP", path=str(file_path)) from exc
|
|
755
|
+
derived = parse_modus_zip(archive)
|
|
756
|
+
supplied_toc = doc.get("toc_version")
|
|
757
|
+
supplied_games = doc.get("supported_game_versions")
|
|
758
|
+
if supplied_toc is not None and str(supplied_toc) != derived["toc_version"]:
|
|
759
|
+
raise ValidationError("toc_version does not match the ZIP Interface field", path="$.toc_version")
|
|
760
|
+
if supplied_games is not None and _supported_game_versions(supplied_games) != derived["supported_game_versions"]:
|
|
761
|
+
raise ValidationError("supported_game_versions does not match the ZIP Interface field", path="$.supported_game_versions")
|
|
762
|
+
metadata.update({
|
|
763
|
+
"md5": md5,
|
|
764
|
+
"zip_size": size,
|
|
765
|
+
"unzip_size": unzip_size,
|
|
766
|
+
"toc_version": derived["toc_version"],
|
|
767
|
+
"supported_game_versions": derived["supported_game_versions"],
|
|
768
|
+
"path": doc.get("path") or archive.name,
|
|
769
|
+
})
|
|
770
|
+
current_stage = "release_metadata_update" if update else "release_metadata"
|
|
771
|
+
transaction["stages"].append(current_stage)
|
|
772
|
+
transaction["active_stage"] = current_stage
|
|
773
|
+
save_transaction()
|
|
719
774
|
result = self.release_metadata(metadata, update=update)
|
|
720
775
|
if archive is None:
|
|
776
|
+
transaction.pop("active_stage", None)
|
|
721
777
|
transaction.update({"completed": True, "upload": None})
|
|
722
778
|
save_transaction()
|
|
723
779
|
return {"project_id": project_id, "file_id": file_id, "metadata": result, "upload": None, "transaction": _safe(transaction)}
|
|
724
|
-
|
|
780
|
+
current_stage = "signature"
|
|
781
|
+
transaction["stages"].append(current_stage)
|
|
782
|
+
transaction["active_stage"] = current_stage
|
|
783
|
+
save_transaction()
|
|
725
784
|
signed = self.release_signature(project_id, file_id)
|
|
726
|
-
|
|
785
|
+
current_stage = "binary_upload"
|
|
786
|
+
transaction["stages"].append(current_stage)
|
|
787
|
+
transaction["active_stage"] = current_stage
|
|
788
|
+
save_transaction()
|
|
727
789
|
uploaded = self.upload_zip(signed, str(archive))
|
|
790
|
+
transaction.pop("active_stage", None)
|
|
728
791
|
transaction.update({"completed": True, "upload": uploaded})
|
|
729
792
|
save_transaction()
|
|
730
793
|
return {"project_id": project_id, "file_id": file_id, "metadata": result, "upload": uploaded, "transaction": _safe(transaction)}
|
|
731
794
|
except FuploadError as exc:
|
|
732
|
-
|
|
795
|
+
if exc.stage in (None, "dependency_get"):
|
|
796
|
+
exc.stage = current_stage
|
|
797
|
+
transaction["failed_stage"] = exc.stage or current_stage
|
|
733
798
|
transaction["error"] = exc.as_dict()
|
|
734
799
|
transaction["retained_archive"] = bool(archive and archive.is_file())
|
|
735
800
|
try:
|
|
@@ -737,6 +802,20 @@ class ModUs:
|
|
|
737
802
|
except OSError:
|
|
738
803
|
pass
|
|
739
804
|
raise
|
|
805
|
+
except OSError as exc:
|
|
806
|
+
wrapped = FuploadError(
|
|
807
|
+
"ModUs upload preparation failed",
|
|
808
|
+
stage=current_stage,
|
|
809
|
+
details={"response_summary": redact(str(exc)) or exc.__class__.__name__},
|
|
810
|
+
)
|
|
811
|
+
transaction["failed_stage"] = current_stage
|
|
812
|
+
transaction["error"] = wrapped.as_dict()
|
|
813
|
+
transaction["retained_archive"] = bool(archive and archive.is_file())
|
|
814
|
+
try:
|
|
815
|
+
save_transaction()
|
|
816
|
+
except OSError:
|
|
817
|
+
pass
|
|
818
|
+
raise wrapped from exc
|
|
740
819
|
|
|
741
820
|
def execute_read(self, resource: str, action: str, args: Any = None) -> Any:
|
|
742
821
|
if resource == "session" and action == "doctor": return self.doctor()
|
|
@@ -41,6 +41,17 @@ COMPLETE = ProjectStep.COMPLETE.value
|
|
|
41
41
|
_SCHEMA = "fupload.v1.modus.project-state"
|
|
42
42
|
_PLATFORMS = ("modus", "bigfoot")
|
|
43
43
|
_BIGFOOT_EXCLUSIVE_CATEGORY_ID = 998
|
|
44
|
+
_BASIC_INFO_KEYS = {
|
|
45
|
+
# Shared create/edit form fields.
|
|
46
|
+
"schema", "name", "project_name", "alt_name", "summary", "categories",
|
|
47
|
+
"synchronization_type", "publish_platforms", "publishPlatforms",
|
|
48
|
+
"required_tier_id", "requiredTierId", "repo_url",
|
|
49
|
+
# Create-only image fields and edit-only detail/image fields.
|
|
50
|
+
"logo_base64", "screenshot_base64s", "description",
|
|
51
|
+
"required_dependencies", "images", "image_ops",
|
|
52
|
+
# Known project-detail readback fields preserved in resumable snapshots.
|
|
53
|
+
"cf_url", "logo", "status",
|
|
54
|
+
}
|
|
44
55
|
_LICENSE_KEYS = {
|
|
45
56
|
"type", "template", "license_template", "licenseTemplate",
|
|
46
57
|
"holder", "copyright_holder", "copyrightHolder",
|
|
@@ -49,6 +60,15 @@ _LICENSE_KEYS = {
|
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
|
|
63
|
+
def _basic_info_fields(value: Mapping[str, Any]) -> None:
|
|
64
|
+
unknown = sorted(set(value) - _BASIC_INFO_KEYS)
|
|
65
|
+
if unknown:
|
|
66
|
+
raise ValidationError(
|
|
67
|
+
"unknown basic_info field(s): %s" % ", ".join(unknown),
|
|
68
|
+
path="$.basic_info.%s" % unknown[0],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
52
72
|
def _nonempty_text(value: Any, *, path: str) -> str:
|
|
53
73
|
if not isinstance(value, str) or not value.strip():
|
|
54
74
|
raise ValidationError("value must be a non-empty string", path=path)
|
|
@@ -194,6 +214,7 @@ class ProjectStateMachine:
|
|
|
194
214
|
self._require(ProjectStep.BASIC_INFO)
|
|
195
215
|
value: Dict[str, Any] = dict(info or {})
|
|
196
216
|
value.update(fields)
|
|
217
|
+
_basic_info_fields(value)
|
|
197
218
|
name = value.get("name", value.get("project_name"))
|
|
198
219
|
summary = value.get("summary")
|
|
199
220
|
_nonempty_text(name, path="$.basic_info.name")
|
|
@@ -288,26 +309,33 @@ class ProjectStateMachine:
|
|
|
288
309
|
game = snapshot.get("game")
|
|
289
310
|
basic = snapshot.get("basic_info") or {}
|
|
290
311
|
lic = snapshot.get("license") or {}
|
|
312
|
+
restored_game: Any = None
|
|
313
|
+
restored_basic: Dict[str, Any] = {}
|
|
314
|
+
restored_license: Dict[str, Any] = {}
|
|
291
315
|
if state != CHOOSE_GAME:
|
|
292
|
-
|
|
316
|
+
restored_game = _game(game)
|
|
293
317
|
if state in {LICENSE, COMPLETE}:
|
|
294
318
|
if not isinstance(basic, Mapping):
|
|
295
319
|
raise ValidationError("basic_info must be an object", path="$.basic_info")
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
_nonempty_text(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
restored_basic = dict(basic)
|
|
321
|
+
_basic_info_fields(restored_basic)
|
|
322
|
+
_nonempty_text(restored_basic.get("name"), path="$.basic_info.name")
|
|
323
|
+
_nonempty_text(restored_basic.get("summary"), path="$.basic_info.summary")
|
|
324
|
+
restored_basic["publish_platforms"] = _platforms(restored_basic.get("publish_platforms"))
|
|
325
|
+
restored_basic["categories"] = _categories(restored_basic.get("categories"))
|
|
326
|
+
restored_basic["required_tier_id"] = _required_tier(restored_basic.get("required_tier_id"))
|
|
327
|
+
if _BIGFOOT_EXCLUSIVE_CATEGORY_ID in restored_basic["categories"] and restored_basic["publish_platforms"] != ["bigfoot"]:
|
|
303
328
|
raise ValidationError("category 998 requires bigfoot as the only publish platform", path="$.basic_info.publish_platforms")
|
|
304
|
-
if "bigfoot" in
|
|
329
|
+
if "bigfoot" in restored_basic["publish_platforms"] and restored_basic["required_tier_id"] is not None:
|
|
305
330
|
raise ValidationError("required_tier_id must be null when bigfoot is selected", path="$.basic_info.required_tier_id")
|
|
306
|
-
derived_sync_type = (1 if "modus" in
|
|
307
|
-
if
|
|
331
|
+
derived_sync_type = (1 if "modus" in restored_basic["publish_platforms"] else 0) | (2 if "bigfoot" in restored_basic["publish_platforms"] else 0)
|
|
332
|
+
if restored_basic.get("synchronization_type") != derived_sync_type:
|
|
308
333
|
raise ValidationError("synchronization_type must match publish_platforms", path="$.basic_info.synchronization_type")
|
|
309
334
|
if state == COMPLETE:
|
|
310
|
-
|
|
335
|
+
restored_license = _license(lic)
|
|
336
|
+
self._game = restored_game
|
|
337
|
+
self._basic_info = restored_basic
|
|
338
|
+
self._license = restored_license
|
|
311
339
|
self._step = str(state)
|
|
312
340
|
|
|
313
341
|
def save(self, path: Union[str, os.PathLike[str]]) -> Path:
|
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.11",
|
|
5
|
+
"skill_version": "0.0.11",
|
|
6
|
+
"tree_sha256": "a0d589cb5d67c7bfbd34862ee4c3109e88068662ae3b7430c0635e71ace9a8d9",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "agents/openai.yaml",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"path": "references/modus.md",
|
|
95
|
-
"bytes":
|
|
96
|
-
"sha256": "
|
|
95
|
+
"bytes": 8705,
|
|
96
|
+
"sha256": "10c2db10ade101c6738d9007865db68da54892da59fc1cc0fc1a422296a4bf07"
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"path": "references/newbee-official-cli.md",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
{
|
|
114
114
|
"path": "scripts/fupload_cli/__init__.py",
|
|
115
115
|
"bytes": 50,
|
|
116
|
-
"sha256": "
|
|
116
|
+
"sha256": "c7d5308bddd21b0e8042689cdeaba91ba45170a998f42844d42f88b7248fc057"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"path": "scripts/fupload_cli/blackbox_web.py",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "scripts/fupload_cli/modus.py",
|
|
170
|
-
"bytes":
|
|
171
|
-
"sha256": "
|
|
170
|
+
"bytes": 42241,
|
|
171
|
+
"sha256": "ee298b064f4b3c4a25081f76e93d48b18f1b810f647437e78d78a4a20ab5bd5c"
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
"path": "scripts/fupload_cli/newbee_auth.py",
|
|
@@ -187,8 +187,8 @@
|
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "scripts/fupload_cli/state_machine.py",
|
|
190
|
-
"bytes":
|
|
191
|
-
"sha256": "
|
|
190
|
+
"bytes": 16450,
|
|
191
|
+
"sha256": "da26b63be4e085b9b39aea4d13a2c58206836d98fa46400b243113ce4b1ff4a9"
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"path": "scripts/fupload_cli/transport.py",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
{
|
|
209
209
|
"path": "SKILL.md",
|
|
210
210
|
"bytes": 25720,
|
|
211
|
-
"sha256": "
|
|
211
|
+
"sha256": "cfc3ce770c3594c5a787b25da03cb2e92e0f6ea6fbd162f26f4b6b3c44295a89"
|
|
212
212
|
}
|
|
213
213
|
]
|
|
214
214
|
}
|