|
1 | 1 | #!/usr/bin/env python3 |
2 | | -"""Inject platform-config globals into index.html before </head>.""" |
| 2 | +"""Keep the console's configuration bootstrap as a same-origin script. |
| 3 | +
|
| 4 | +The Worker serializes the non-secret configuration from ``config.js`` at |
| 5 | +``/bootstrap-config.js``. Keeping this loader external lets the page retain |
| 6 | +its strict ``script-src 'self'`` CSP without a hash or ``unsafe-inline``. |
| 7 | +""" |
3 | 8 |
|
4 | | -import json |
5 | 9 | from pathlib import Path |
6 | 10 |
|
| 11 | + |
7 | 12 | ROOT = Path(__file__).resolve().parents[2] |
8 | 13 | SOURCE = ROOT / "web" / "strategy-switch-console" / "index.html" |
9 | | -CONFIG = ROOT / "platform-config.json" |
| 14 | +MARKER = "<!-- Generated by inject_platform_config.py -->" |
| 15 | +BOOTSTRAP = '<script src="/bootstrap-config.js"></script>' |
10 | 16 |
|
11 | 17 |
|
12 | 18 | def main() -> int: |
13 | | - config = json.loads(CONFIG.read_text(encoding="utf-8")) |
14 | | - platforms = config["platforms"] |
15 | | - strategies = config["strategies"] |
16 | | - meta = config.get("meta", {}) |
17 | | - runtime_authority = meta.get("runtime_authority", {}) if isinstance(meta, dict) else {} |
18 | | - |
19 | | - pc, dao, dca, inc_layer, opt_overlay = {}, {}, {}, {}, {} |
20 | | - strategy_profiles = [] |
21 | | - domain_labels = { |
22 | | - did: {"zh": ddata.get("label_zh", did), "en": ddata.get("label_en", did)} |
23 | | - for did, ddata in config.get("domains", {}).items() |
24 | | - } |
25 | | - for pid, pdata in platforms.items(): |
26 | | - caps, depl = pdata["capabilities"], pdata["deployment"] |
27 | | - pc[pid] = dict( |
28 | | - dry_run_only=depl.get("dry_run_only", False), |
29 | | - margin_policy=caps.get("margin_policy", False), |
30 | | - reserved_cash=caps.get("reserved_cash", False), |
31 | | - income_layer=caps.get("income_layer", False), |
32 | | - option_overlay=caps.get("option_overlay", False), |
33 | | - dca=caps.get("dca", False), |
34 | | - execution_mode=depl.get("default_execution_mode", "live"), |
35 | | - service_name=depl.get("service_name", ""), |
36 | | - default_execution_mode=depl.get("default_execution_mode", "live"), |
37 | | - ) |
38 | | - acct = pdata.get("default_account", {}) |
39 | | - entry = dict( |
40 | | - key=acct.get("key", pid), |
41 | | - label=acct.get("label", pdata.get("label", pid)), |
42 | | - target_name=acct.get("target_name", acct.get("key", pid)), |
43 | | - supported_domains=acct.get("supported_domains", pdata.get("supported_domains", [])), |
44 | | - cash_currency=acct.get("cash_currency", "USD"), |
45 | | - ) |
46 | | - for fld in ( |
47 | | - "service_name", |
48 | | - "account_scope", |
49 | | - "deployment_selector", |
50 | | - "account_selector", |
51 | | - "default_execution_mode", |
52 | | - "min_reserved_cash_usd", |
53 | | - "reserved_cash_ratio", |
54 | | - "cash_only_execution_mode", |
55 | | - "dca_mode", |
56 | | - "dca_base_investment_usd", |
57 | | - ): |
58 | | - if acct.get(fld): |
59 | | - entry[fld] = acct[fld] |
60 | | - if "service_name" not in entry: |
61 | | - entry["service_name"] = depl.get("service_name", "") |
62 | | - if "default_execution_mode" not in entry: |
63 | | - entry["default_execution_mode"] = depl.get("default_execution_mode", "live") |
64 | | - dao[pid] = [entry] |
65 | | - for sid, sdata in strategies.items(): |
66 | | - feat = sdata.get("features", {}) |
67 | | - strategy_profiles.append(_strategy_profile_entry(sid, sdata)) |
68 | | - dd = sdata.get("dca_defaults") |
69 | | - if dd: |
70 | | - dca[sid] = dict( |
71 | | - defaultMode=dd.get("default_mode", "fixed"), |
72 | | - defaultBaseInvestmentUsd=str(dd.get("default_base_investment_usd", "1000")), |
73 | | - ) |
74 | | - if feat.get("income_layer"): |
75 | | - idl = sdata.get("income_layer_defaults", {}) |
76 | | - inc_layer[sid] = dict( |
77 | | - startUsd=int(idl.get("start_usd", 0)), |
78 | | - maxRatio=str(idl.get("max_ratio", "")), |
79 | | - allocations=idl.get("allocations", {}), |
80 | | - ) |
81 | | - if feat.get("option_overlay"): |
82 | | - odl = sdata.get("option_overlay_defaults", {}) |
83 | | - families = [] |
84 | | - if odl.get("growth_enabled"): |
85 | | - families.append( |
86 | | - dict( |
87 | | - family="growth", |
88 | | - recipe=odl["growth_recipe"], |
89 | | - startUsd=odl["growth_start_usd"], |
90 | | - ratio=str(odl.get("nav_budget_ratio", "")), |
91 | | - ratioKind="budget", |
92 | | - ) |
93 | | - ) |
94 | | - if odl.get("income_enabled"): |
95 | | - families.append( |
96 | | - dict( |
97 | | - family="income", |
98 | | - recipe=odl["income_recipe"], |
99 | | - startUsd=odl["income_start_usd"], |
100 | | - ratio=str(odl.get("nav_risk_ratio", "")), |
101 | | - ratioKind="risk", |
102 | | - ) |
103 | | - ) |
104 | | - opt_overlay[sid] = dict( |
105 | | - liveGate=odl.get("live_gate", ""), liveStatus=odl.get("live_status", ""), families=families |
106 | | - ) |
107 | | - |
108 | | - block = "\n".join( |
109 | | - [ |
110 | | - "<!-- Generated by inject_platform_config.py -->", |
111 | | - '<script id="platform-config">', |
112 | | - "window.__PLATFORM_CONFIG__ = " + json.dumps(pc, ensure_ascii=False) + ";", |
113 | | - "window.__QSL_RUNTIME_AUTHORITY_STATUS__ = " + json.dumps(runtime_authority, ensure_ascii=False) + ";", |
114 | | - "window.__DEFAULT_ACCOUNT_OPTIONS__ = " + json.dumps(dao, ensure_ascii=False) + ";", |
115 | | - "window.__DOMAIN_LABELS__ = " + json.dumps(domain_labels, ensure_ascii=False) + ";", |
116 | | - "window.__DEFAULT_STRATEGY_PROFILES__ = " + json.dumps(strategy_profiles, ensure_ascii=False) + ";", |
117 | | - "window.__DCA_PROFILE_DEFAULTS__ = " + json.dumps(dca, ensure_ascii=False) + ";", |
118 | | - "window.__INCOME_LAYER_DEFAULTS__ = " + json.dumps(inc_layer, ensure_ascii=False) + ";", |
119 | | - "window.__OPTION_OVERLAY_DEFAULTS__ = " + json.dumps(opt_overlay, ensure_ascii=False) + ";", |
120 | | - "</script>", |
121 | | - ] |
122 | | - ) |
123 | | - |
124 | 19 | html = SOURCE.read_text(encoding="utf-8") |
125 | | - marker = "<!-- Generated by inject_platform_config.py -->" |
126 | | - existing = html.find('<script id="platform-config">') |
127 | | - if existing >= 0: |
128 | | - start = existing |
129 | | - while True: |
130 | | - prefix = html[:start].rstrip() |
131 | | - marker_start = prefix.rfind(marker) |
132 | | - if marker_start < 0: |
133 | | - break |
134 | | - if prefix[marker_start:].strip() != marker: |
135 | | - break |
136 | | - start = marker_start |
137 | | - end = html.find("</script>", existing) + 9 |
138 | | - html = html[:start].rstrip() + "\n\n" + block + html[end:] |
139 | | - else: |
| 20 | + replacement = f"{MARKER}\n{BOOTSTRAP}" |
| 21 | + legacy_script = html.find('<script id="platform-config">') |
| 22 | + |
| 23 | + if legacy_script >= 0: |
| 24 | + start = legacy_script |
| 25 | + prefix = html[:start].rstrip() |
| 26 | + if prefix.endswith(MARKER): |
| 27 | + start = prefix.rfind(MARKER) |
| 28 | + end = html.find("</script>", legacy_script) |
| 29 | + if end < 0: |
| 30 | + raise RuntimeError("platform-config script is missing its closing tag") |
| 31 | + html = html[:start].rstrip() + "\n\n" + replacement + html[end + len("</script>"):] |
| 32 | + elif BOOTSTRAP not in html: |
140 | 33 | head_end = html.find("</head>") |
141 | | - html = html[:head_end] + "\n" + block + "\n" + html[head_end:] |
| 34 | + if head_end < 0: |
| 35 | + raise RuntimeError("index.html is missing </head>") |
| 36 | + html = html[:head_end] + "\n" + replacement + "\n" + html[head_end:] |
142 | 37 |
|
143 | 38 | SOURCE.write_text(html, encoding="utf-8") |
144 | | - print("Injected platform-config into index.html") |
| 39 | + print("Configured same-origin bootstrap-config.js loader") |
145 | 40 | return 0 |
146 | 41 |
|
147 | 42 |
|
148 | | -def _strategy_profile_entry(sid: str, sdata: dict) -> dict: |
149 | | - feat = sdata.get("features", {}) |
150 | | - runtime_enabled = sdata.get("runtime_enabled", False) |
151 | | - lifecycle_stage = str( |
152 | | - sdata.get("lifecycle_stage") or ("runtime_enabled" if runtime_enabled else "research_active") |
153 | | - ).strip() |
154 | | - can_switch_live = sdata.get( |
155 | | - "can_switch_live", |
156 | | - runtime_enabled and lifecycle_stage in {"live_enabled", "runtime_enabled"}, |
157 | | - ) |
158 | | - blocked_live_reason = sdata.get("blocked_live_reason") |
159 | | - if blocked_live_reason is None and not can_switch_live: |
160 | | - blocked_live_reason = lifecycle_stage or "not_runtime_enabled" |
161 | | - continuity = sdata.get("live_continuity") if isinstance(sdata.get("live_continuity"), dict) else {} |
162 | | - entry = { |
163 | | - "profile": sid, |
164 | | - "label": sdata.get("label", sid), |
165 | | - "label_en": sdata.get("label_en", sid), |
166 | | - "label_zh": sdata.get("label", sid), |
167 | | - "domain": sdata.get("domain", ""), |
168 | | - "runtime_enabled": runtime_enabled, |
169 | | - "lifecycle_stage": lifecycle_stage, |
170 | | - "can_switch_live": can_switch_live, |
171 | | - "allowed_execution_modes": _normalize_allowed_execution_modes(sdata.get("allowed_execution_modes")), |
172 | | - "blocked_live_reason": "" if blocked_live_reason is None else str(blocked_live_reason).strip(), |
173 | | - "live_continuity": { |
174 | | - "eligible": continuity.get("eligible") is True, |
175 | | - "allowed_platforms": list(continuity.get("allowed_platforms") or []), |
176 | | - }, |
177 | | - "income_layer_enabled": feat.get("income_layer", False), |
178 | | - "option_overlay_enabled": feat.get("option_overlay", False), |
179 | | - "combo_enabled": feat.get("combo", False), |
180 | | - } |
181 | | - if feat.get("combo"): |
182 | | - entry["combo_mode"] = feat.get("combo_mode", "dynamic") |
183 | | - return entry |
184 | | - |
185 | | - |
186 | | -def _normalize_allowed_execution_modes(raw_modes: object) -> list[str]: |
187 | | - if raw_modes is None: |
188 | | - return ["paper", "dry_run"] |
189 | | - if isinstance(raw_modes, str): |
190 | | - modes = [raw_modes.strip()] |
191 | | - elif isinstance(raw_modes, list): |
192 | | - modes = [str(mode).strip() for mode in raw_modes] |
193 | | - elif isinstance(raw_modes, tuple): |
194 | | - modes = [str(mode).strip() for mode in raw_modes] |
195 | | - elif isinstance(raw_modes, set): |
196 | | - modes = [str(mode).strip() for mode in sorted(raw_modes)] |
197 | | - else: |
198 | | - modes = ["paper", "dry_run"] |
199 | | - modes = [mode for mode in modes if mode] |
200 | | - return modes if modes else ["paper", "dry_run"] |
201 | | - |
202 | | - |
203 | 43 | if __name__ == "__main__": |
204 | 44 | raise SystemExit(main()) |
0 commit comments