|
3 | 3 | The bundle (``knowledge.json`` + optional ``manifest.json``) is produced by |
4 | 4 | the curated knowledge bundle repo. It reaches this process by one of three routes, |
5 | 5 | tried in order: an explicit ``COMFY_KNOWLEDGE_FILE``, the per-user cache, or a |
6 | | -fetch from ``COMFY_KNOWLEDGE_URL``. A missing or broken bundle is a normal |
| 6 | +fetch from ``COMFY_KNOWLEDGE_URL``, which defaults to the knowledge channel |
| 7 | +under the cloud base URL. A missing or broken bundle is a normal |
7 | 8 | state: every entry point here returns ``None`` rather than raising, and nothing |
8 | 9 | is written to stdout or stderr. :func:`attach` is how discovery commands add a |
9 | 10 | capped ``knowledge`` block to their payload; it is fail-open for the same reason. |
|
39 | 40 | ENV_URL = "COMFY_KNOWLEDGE_URL" |
40 | 41 | ENV_TTL = "COMFY_KNOWLEDGE_TTL" |
41 | 42 | ENV_DISABLE = "COMFY_KNOWLEDGE_DISABLE" |
| 43 | +DEFAULT_URL_PATH = "/api/knowledge/knowledge.json" |
42 | 44 | DEFAULT_TTL_SECONDS = 24 * 60 * 60 |
43 | 45 | FETCH_TIMEOUT_SECONDS = 10.0 |
44 | 46 | MAX_BUNDLE_BYTES = 16 * 1024 * 1024 |
|
77 | 79 | MIN_SINGLE_TOKEN_CHARS = 4 |
78 | 80 |
|
79 | 81 | REASON_ENV_FILE = "COMFY_KNOWLEDGE_FILE is set but could not be loaded" |
80 | | -REASON_NO_URL = "no cache and COMFY_KNOWLEDGE_URL is not set" |
81 | | -REASON_FETCH_FAILED = "fetch failed and no cached bundle exists" |
| 82 | +REASON_SIGNED_OUT = ( |
| 83 | + "fetch from the cloud knowledge channel failed and no cached bundle exists; " |
| 84 | + "the usual cause is being signed out (run `comfy cloud login`)" |
| 85 | +) |
| 86 | +REASON_FETCH_FAILED = "fetch from COMFY_KNOWLEDGE_URL failed and no cached bundle exists" |
82 | 87 |
|
83 | 88 |
|
84 | 89 | @dataclass(frozen=True) |
@@ -137,11 +142,27 @@ def ttl_seconds() -> float: |
137 | 142 | return max(ttl, 0.0) if math.isfinite(ttl) else DEFAULT_TTL_SECONDS |
138 | 143 |
|
139 | 144 |
|
| 145 | +def default_url() -> str: |
| 146 | + """``knowledge.json`` under the cloud base URL, derived on every call. |
| 147 | +
|
| 148 | + Never stored: :func:`_http_get` attaches credentials only under |
| 149 | + ``get_base_url()``, which resolves per invocation, so a remembered |
| 150 | + production URL would silently fetch unauthenticated for anyone pointed at |
| 151 | + another environment. |
| 152 | + """ |
| 153 | + return get_base_url().rstrip("/") + DEFAULT_URL_PATH |
| 154 | + |
| 155 | + |
| 156 | +def bundle_url() -> str: |
| 157 | + """``COMFY_KNOWLEDGE_URL`` when set, else :func:`default_url`.""" |
| 158 | + return os.environ.get(ENV_URL, "").strip() or default_url() |
| 159 | + |
| 160 | + |
140 | 161 | def load_bundle(*, force_fetch: bool = False, cache_only: bool = False) -> Bundle | None: |
141 | 162 | """Return the indexed bundle, or ``None`` when no usable bundle exists. |
142 | 163 |
|
143 | 164 | Memoized per process; ``force_fetch=True`` re-runs the load and skips the |
144 | | - cache TTL gate so a fetch happens whenever ``COMFY_KNOWLEDGE_URL`` is set. |
| 165 | + cache TTL gate so a fetch happens unless ``COMFY_KNOWLEDGE_FILE`` is set. |
145 | 166 | ``cache_only=True`` never touches the network: env file, then any cache |
146 | 167 | (fresh or stale), else ``None``. The memo is shared, except that a |
147 | 168 | cache-only miss is not memoized so a later full load can still fetch. |
@@ -315,17 +336,17 @@ def _load(*, force_fetch: bool, cache_only: bool = False) -> tuple[Bundle | None |
315 | 336 | if bundle is not None: |
316 | 337 | return bundle, None |
317 | 338 |
|
318 | | - url = os.environ.get(ENV_URL, "").strip() |
319 | | - if url and not cache_only: |
320 | | - bundle = _fetch(url, knowledge_path, manifest_path) |
| 339 | + explicit_url = os.environ.get(ENV_URL, "").strip() |
| 340 | + if not cache_only: |
| 341 | + bundle = _fetch(explicit_url or default_url(), knowledge_path, manifest_path) |
321 | 342 | if bundle is not None: |
322 | 343 | return bundle, None |
323 | 344 |
|
324 | 345 | source = "cache" if fresh else "stale-cache" |
325 | 346 | bundle = _load_file(knowledge_path, manifest_path, source=source, stale=not fresh) |
326 | 347 | if bundle is not None: |
327 | 348 | return bundle, None |
328 | | - return None, (REASON_FETCH_FAILED if url else REASON_NO_URL) |
| 349 | + return None, (REASON_FETCH_FAILED if explicit_url else REASON_SIGNED_OUT) |
329 | 350 |
|
330 | 351 |
|
331 | 352 | def _cache_is_fresh(path: Path) -> bool: |
@@ -390,7 +411,9 @@ def _http_get(url: str) -> bytes: |
390 | 411 | if url.startswith(get_base_url().rstrip("/") + "/"): |
391 | 412 | from comfy_cli.target import resolve_target |
392 | 413 |
|
393 | | - opened = authed_urlopen(url, resolve_target(where="cloud"), timeout=FETCH_TIMEOUT_SECONDS) |
| 414 | + # Best-effort background fetch: a spurious refresh failure must not log the user out. |
| 415 | + target = resolve_target(where="cloud", allow_clear=False) |
| 416 | + opened = authed_urlopen(url, target, timeout=FETCH_TIMEOUT_SECONDS) |
394 | 417 | else: |
395 | 418 | req = urllib.request.Request(url, headers={"User-Agent": "comfy-cli"}) |
396 | 419 | opened = plain_urlopen(req, timeout=FETCH_TIMEOUT_SECONDS) |
@@ -922,9 +945,9 @@ def attach( |
922 | 945 | exactly as it was. |
923 | 946 |
|
924 | 947 | ``COMFY_KNOWLEDGE_DISABLE`` suppresses the block entirely. A cached bundle |
925 | | - keeps being read once it exists, stale or not, so clearing |
926 | | - ``COMFY_KNOWLEDGE_URL`` is not an off switch and this is. Following |
927 | | - ``DO_NOT_TRACK``, any value but empty or ``"0"`` disables. |
| 948 | + keeps being read once it exists, stale or not, and the default URL means a |
| 949 | + signed-in install fetches one without being asked, so this is the only off |
| 950 | + switch. Following ``DO_NOT_TRACK``, any value but empty or ``"0"`` disables. |
928 | 951 | """ |
929 | 952 | try: |
930 | 953 | disable = os.environ.get(ENV_DISABLE, "") |
|
0 commit comments