Просмотр исходного кода

fix: send a User-Agent when fetching a feed; follow the loaded SD.cpp model

RSS 403

The reader sent no User-Agent at all. Reddit answers 403 to that, which is what
the reddit2image workflow was hitting - nothing to do with the URL or the feed
itself. Confirmed directly: no User-Agent gives 403, a named one gives 200.

There is now a User-Agent setting, defaulting to a string that identifies this
application rather than pretending to be a browser. Feeds that police this
generally want to know who is calling, and Reddit rate-limits a browser string
harder than a named client.

A refusal also says what it means now: 403 points at the User-Agent setting,
429 explains that the feed is rate limiting the address and counts failed
requests too, 404 repeats the address. "HTTP 403" on its own gave nobody
anywhere to start.

Verified by re-running the workflow: 25 items from the Selfie feed.

FOLLOW THE LOADED MODEL

SD.cpp Model can now take the model the server currently has loaded instead of
naming one. It keeps a workflow portable between servers, and it stops a
workflow spending minutes swapping a model just to match what a node was set
to. Reading it goes through /health, which needs no credential, so this works
before one is set up.

With Include Components it also supplies the VAE, CLIP, T5, LLM and TAESD the
server reports - useful for a diffusion model, which loads its parts separately.
A component the server does not have is left alone rather than supplied empty,
which would have blanked the target's own value.

Set to follow the loaded model and no model is loaded, it says so and names the
server rather than passing an empty name down to a node that would fail more
confusingly.

Verified against the live server: the picker reports AnythingXL_v50 and SD 1.x
without being told either, feeds the name to Load Model, and Load Model reports
alreadyLoaded rather than reloading. Full suite 58/58.
fszontagh 1 месяц назад
Родитель
Сommit
46662640bd
3 измененных файлов с 115 добавлено и 8 удалено
  1. 25 2
      nodes/rss/rss-reader.js
  2. 70 6
      nodes/sdcpp/sdcpp-model.js
  3. 20 0
      tests/nodes/sdcpp-model-follow-loaded.json

+ 25 - 2
nodes/rss/rss-reader.js

@@ -31,6 +31,12 @@ const configSchema = {
             description: 'Maximum number of items to return (0 = unlimited)',
             default: 0
         },
+        userAgent: {
+            type: 'string',
+            title: 'User-Agent',
+            description: 'Sent with the request. Several feeds refuse an unnamed client outright - Reddit answers 403 - and some rate-limit a browser string harder than a named one. Identify yourself here',
+            default: 'smartbotic-rss/1.0 (+https://smartbotics.ai)'
+        },
         timeout: {
             type: 'number',
             title: 'Timeout (ms)',
@@ -699,6 +705,11 @@ async function execute(config, input) {
         throw new Error('State collection is required when "Detect New Items" is enabled. Please select a read-write collection in the workflow storage settings.');
     }
 
+    // No User-Agent at all is what several feeds reject, Reddit among them, so
+    // there is always one.
+    var userAgent = String(config.userAgent || '').trim() ||
+                    'smartbotic-rss/1.0 (+https://smartbotics.ai)';
+
     smartbotic.log.info('Fetching RSS/Atom feed from ' + url);
 
     var response = smartbotic.http.request({
@@ -706,12 +717,24 @@ async function execute(config, input) {
         url: url,
         timeout: timeout,
         headers: {
-            'Accept': 'application/rss+xml, application/atom+xml, application/xml, text/xml, */*'
+            'Accept': 'application/rss+xml, application/atom+xml, application/xml, text/xml, */*',
+            'User-Agent': userAgent
         }
     });
 
     if (response.status < 200 || response.status >= 300) {
-        throw new Error('Failed to fetch feed: HTTP ' + response.status);
+        var hint = '';
+        if (response.status === 403) {
+            hint = '. The feed refused this client - most feeds that do this want a descriptive ' +
+                   'User-Agent, which is the User-Agent setting on this node';
+        } else if (response.status === 429) {
+            hint = '. The feed is rate limiting this address - poll it less often, or from ' +
+                   'somewhere else. Reddit throttles hard and counts every request, including ' +
+                   'failed ones';
+        } else if (response.status === 404) {
+            hint = '. Check the address: ' + url;
+        }
+        throw new Error('Failed to fetch feed: HTTP ' + response.status + hint);
     }
 
     var xml = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);

+ 70 - 6
nodes/sdcpp/sdcpp-model.js

@@ -36,15 +36,30 @@ const configSchema = {
             description: 'A basic credential holding the sdcpp-restapi username and password. Listing models needs one',
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
         },
+        source: {
+            type: 'string', title: 'Model',
+            enum: ['choose', 'loaded'],
+            enumLabels: ['Pick one from the server', 'Whatever is loaded right now'],
+            default: 'choose',
+            description: 'Pick one, or take the model the server currently has loaded. Taking the loaded one keeps a workflow portable between servers, and means it never spends minutes swapping a model just to match what a node was set to'
+        },
+        includeComponents: {
+            type: 'boolean', title: 'Include Components',
+            default: false,
+            showWhen: { field: 'source', value: 'loaded' },
+            description: 'Also supply the VAE, CLIP, T5, LLM and TAESD the server has loaded, not only the model name. Only useful for a diffusion model, which loads its parts separately'
+        },
         modelType: {
             type: 'string', title: 'Kind',
+            showWhen: { field: 'source', value: 'choose' },
             enum: ['checkpoint', 'diffusion', 'vae', 'lora', 'clip', 't5', 'embedding',
                    'controlnet', 'llm', 'esrgan', 'taesd', 'motion_module', 'adetailer'],
             default: 'checkpoint',
             description: 'Which kind of model to choose from. esrgan is the upscaler kind'
         },
         modelName: {
-            type: 'string', title: 'Model',
+            type: 'string', title: 'Choose',
+            showWhen: { field: 'source', value: 'choose' },
             description: 'Chosen from what the server has. Change the Kind above to list a different sort',
             dynamicOptions: {
                 source: 'node',
@@ -73,6 +88,8 @@ const outputSchema = {
         _config: { type: 'object', description: 'The setting this supplies, as the engine consumes it' },
         modelName: { type: 'string', description: 'The chosen model' },
         modelType: { type: 'string' },
+        architecture: { type: 'string', description: 'What the server detected, when following the loaded model' },
+        loadedComponents: { type: 'object', description: 'The components the server has loaded, when following it' },
         setting: { type: 'string', description: 'Which setting it was supplied as' },
         models: { type: 'array', description: 'Everything of this kind the server has. Only filled when listing' }
     }
@@ -98,6 +115,19 @@ const SETTING_FOR_KIND = {
     adetailer: 'adetailer'
 };
 
+// What the server calls each loaded component, and the setting it fills in on a
+// node that loads one. Reported under several spellings across versions, so each
+// setting lists the ones seen.
+const COMPONENT_SETTINGS = [
+    { setting: 'vae', keys: ['vae', 'vae_model'] },
+    { setting: 'clipL', keys: ['clip_l', 'clipL'] },
+    { setting: 'clipG', keys: ['clip_g', 'clipG'] },
+    { setting: 't5xxl', keys: ['t5xxl', 't5'] },
+    { setting: 'llm', keys: ['llm'] },
+    { setting: 'taesd', keys: ['taesd'] },
+    { setting: 'controlnet', keys: ['controlnet', 'controlnet_model'] }
+];
+
 // The server groups models by kind under its own key names.
 const GROUP_FOR_KIND = {
     checkpoint: 'checkpoints',
@@ -256,16 +286,48 @@ async function execute(config, input, context) {
         };
     }
 
-    const modelName = String(config.modelName || '').trim();
-    if (!modelName) {
-        throw new Error('SD.cpp Model: choose a model. Pick one from the list, which is read ' +
-            'from the server named above');
+    let modelName;
+    let architecture = '';
+    let loadedComponents = {};
+    const values = {};
+
+    if ((config.source || 'choose') === 'loaded') {
+        // /health is unauthenticated, so this works before any credential is
+        // set up - useful when the point is to follow whatever the server has.
+        const health = readHealth(server, Math.min(timeout, 15000));
+        if (health.model_loaded !== true || !health.model_name) {
+            throw new Error('SD.cpp Model: this node is set to use the model the server has ' +
+                'loaded, but ' + server + ' has none. Load one first, or choose a model here');
+        }
+        modelName = health.model_name;
+        architecture = health.model_architecture || '';
+        loadedComponents = health.loaded_components || {};
+
+        if (config.includeComponents === true) {
+            for (let i = 0; i < COMPONENT_SETTINGS.length; i++) {
+                const entry = COMPONENT_SETTINGS[i];
+                for (let k = 0; k < entry.keys.length; k++) {
+                    const found = loadedComponents[entry.keys[k]];
+                    // A component the server does not have comes back empty or
+                    // absent; supplying that would blank the target's own value.
+                    if (found) {
+                        values[entry.setting] = found;
+                        break;
+                    }
+                }
+            }
+        }
+    } else {
+        modelName = String(config.modelName || '').trim();
+        if (!modelName) {
+            throw new Error('SD.cpp Model: choose a model. Pick one from the list, which is read ' +
+                'from the server named above');
+        }
     }
 
     const setting = String(config.setting || '').trim() ||
                     SETTING_FOR_KIND[modelType] || 'modelName';
 
-    const values = {};
     values[setting] = modelName;
 
     smartbotic.log.info('SD.cpp Model: supplying ' + setting + ' = ' + modelName);
@@ -276,6 +338,8 @@ async function execute(config, input, context) {
         _config: values,
         modelName: modelName,
         modelType: modelType,
+        architecture: architecture,
+        loadedComponents: loadedComponents,
         setting: setting,
         models: []
     };

+ 20 - 0
tests/nodes/sdcpp-model-follow-loaded.json

@@ -0,0 +1,20 @@
+{
+  "name": "verify-sdcpp-model-follow-loaded",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "pick", "name": "Model", "type": "sdcpp-model", "position": {"x": -200, "y": 100},
+     "config": {"serverUrl": "http://mulan:8077", "source": "loaded"}},
+    {"id": "load", "name": "Load", "type": "sdcpp-model-load", "position": {"x": 0, "y": 250},
+     "config": {"serverUrl": "http://mulan:8077",
+                "credentialId": "cred_62748b40-e9d1-4659-b175-6dbc691cc0b1"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "data"},
+    {"sourceNodeId": "pick", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "config"}
+  ],
+  "expectStatus": "completed",
+  "expect": {
+    "pick": {"status": "completed", "output": {"modelName": "SD1x/AnythingXL_v50.safetensors", "architecture": "SD 1.x"}},
+    "load": {"status": "completed", "output": {"alreadyLoaded": true, "loaded": false}}
+  }
+}