diff --git a/.gitignore b/.gitignore index 105cfdc..af3d308 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,8 @@ tests/* !tests/translation_service_contracts.mjs !tests/studio_contracts.mjs !tests/material_workspace_flow.mjs +!tests/export_availability.mjs +!tests/test_export_availability.py !tests/model_sources_hub.mjs # Local diagnostic/test artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index 36fe598..da557b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ ## Unreleased +### Export availability +- Removed Prompt Studio's JSON export action; copying prompts and saving combinations to the Material Library remain available. Existing prompt JSON files can still be imported. +- Temporarily disabled Recipe package export in both the UI and API pending further validation. Package imports, workflow share codes, and ordinary image downloads remain available. + ### Material Library - Fixed grouped material filters, Recipe scope/tag filters, and prompt studio imports using actual detail payloads. Fixed side-studio draft reset/return state and guarded recipe drops against canvas changes. Recipe model status no longer treats saved hash verification as current availability. - Separated the lower-left Material Library from the Recipes/Prompt Notes workspace. Added canvas drag-and-drop: parameter materials replace matching node values, while the simplified prompt panel inserts text at the beginning or end, with undo. - Material Library now opens from the lower-left control; Workflow Recipes stays at the upper right. Import/export tools are available from the library's More menu. -- Added Prompt Combinations: ordered general/specific fragments, enable switches, separate positive/negative content, saved snapshots, and JSON import/export. +- Added Prompt Combinations: ordered general/specific fragments, enable switches, separate positive/negative content, saved snapshots, and JSON import. - Library cards can apply matching parameters to one selected node through the same transaction as Node Assistant, with seed preservation, native-option validation and guarded undo. Prompt combinations support explicit text replacement or append. - Material details initially expand prompt cards only; other node parameters render on first expansion. Details omit the complete source workflow, which loads separately when opening a full snapshot. Selected-node materials never return their hidden source graph. - Material capture supports PNG workflows, Recipe parameters and Prompt Notes. Applying reference images to LoadImage remains future work. diff --git a/README.md b/README.md index 7817067..60d87b7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Open it with **Ctrl + Shift + M**, the floating **📦** button, or **Extensions #### 4. Workflow Recipes & Parameter Notebooks 🧰 * **Location**: Open **Creative Workspace (📑)** and switch to **Workflow Recipes**. -* **Features**: Save graphs with covers, notes, and model hashes. Append recipes to current canvas, inspect parameter differences against outputs, compare versions, and import/export portable packages. +* **Features**: Save graphs with covers, notes, and model hashes. Append recipes to current canvas, inspect parameter differences against outputs, compare versions, and import portable packages. Package export is temporarily unavailable. #### 5. Settings Panel ⚙️ * **Location**: The Gear icon (**⚙️**) at the bottom-left. diff --git a/api/recipe_packages.py b/api/recipe_packages.py index 558b59d..305242e 100644 --- a/api/recipe_packages.py +++ b/api/recipe_packages.py @@ -19,6 +19,9 @@ from .recipes import get_recipes_dir from .utils import require_filename, resolve_within +# Release gate: export stays closed until package validation is complete. +# Keep the format implementation for existing package/import compatibility. +RECIPE_PACKAGE_EXPORT_ENABLED = False PACKAGE_VERSION = 1 MAX_UPLOAD_BYTES = 32 * 1024 * 1024 MAX_ENTRY_COUNT = 256 @@ -457,6 +460,12 @@ def _commit_import(record, payload): async def api_export_recipe_package(request): + if not RECIPE_PACKAGE_EXPORT_ENABLED: + return web.json_response({ + "status": "error", + "code": "recipe_export_disabled", + "message": "Recipe package export is temporarily unavailable", + }, status=503) try: payload = await request.json() filename = require_filename(payload.get("filename", "")) diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index fb5d219..9b19f89 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -149,6 +149,10 @@ browsing, editing, or already stored data. ## Prompt Studio ownership +The studio supports copying prompt text and saving combinations to the local +Material Library. Standalone prompt-plan JSON export has been removed; importing +existing plan files remains supported by the Material Library transfer center. + The studio has one standalone drawer; the former embedded side/full composer and material import drawer are removed. Existing browser integration continues to call `openPromptStudio(owner)`, and external prompt dispatch uses @@ -157,7 +161,7 @@ to call `openPromptStudio(owner)`, and external prompt dispatch uses | Module | Owned state and responsibilities | | --- | --- | | `ui_prompt_composer.js` | Active drawer, docking width/side, trigger visibility and plan-loading request | -| `ui_prompt_workbench.js` | Owner-backed draft, active role, block editing, ordering, write/copy/save/export | +| `ui_prompt_workbench.js` | Owner-backed draft, active role, block editing, ordering, write/copy/save | | `ui_prompt_source_deck.js` | Source cards, filters, new-card form, preview popover and current sync request | | `ui_prompt_inspector.js` | Full-text inspection window, role tab and its translation lifetime | | `prompt_studio_data.js` | Starter cards, display categories, draft/block initialization and synthesis | diff --git a/docs/architecture/material-library.md b/docs/architecture/material-library.md index 813b5e8..bbcdae8 100644 --- a/docs/architecture/material-library.md +++ b/docs/architecture/material-library.md @@ -278,8 +278,8 @@ the fields; unknown workflow roles are not silently assigned a role. Saving creates an independent new snapshot. There is no automatic source sync or model binding. A page-session draft survives library navigation and is replaced by an existing saved plan only after confirmation; it must be saved before page -reload. Stale detail requests cannot replace the current draft. Export/import use -JSON format `anomalous-prompt-plan-v1`; imports are bounded to 2 MiB and validated +reload. Stale detail requests cannot replace the current draft. Standalone JSON +export has been removed. Existing prompt JSON imports are bounded to 2 MiB and validated by the same save endpoint. The panel also exposes role and target-widget selection, beginning/end buttons for a selected node, copy controls and guarded undo. diff --git a/docs/architecture/recipes.md b/docs/architecture/recipes.md index ef94ff5..0f36bc1 100644 --- a/docs/architecture/recipes.md +++ b/docs/architecture/recipes.md @@ -4,6 +4,21 @@ Read this document for recipe schemas, cards/detail behavior, package handling, result galleries, Parameter Notebooks, prompt roles, and the recipe-powered Node Assistant. +## Export availability + +Recipe package export is temporarily closed pending validation. Card/detail +buttons are disabled with a localized explanation. The registered +`POST /anomalous/export_recipe_package` route returns HTTP 503 with code +`recipe_export_disabled` before reading request JSON or recipe files. +`RECIPE_PACKAGE_EXPORT_ENABLED` in `api/recipe_packages.py` is a release gate, +not a user setting. Package format helpers remain available for import and +compatibility tests; existing imports and local saves are unaffected. + +Reopening requires an explicit release decision, package round-trip and failure +validation, then restoring the frontend export action and enabling the backend +gate together. Do not reopen exports as a side effect of UX work. Workflow +share codes are a separate feature and remain available. + ## Product and data model Workspace contains Prompt Notes, Workflow Recipes, and the Material Library. diff --git a/tests/export_availability.mjs b/tests/export_availability.mjs new file mode 100644 index 0000000..4f848ed --- /dev/null +++ b/tests/export_availability.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { fixture, Element, all } from './ui_fixture.mjs'; + +const f = fixture(); +const studio = await f.module('ui_prompt_composer.js'); +const owner = {}; +await studio.openPromptStudio(owner); +await f.flush(); +const workbench = f.document.querySelector('.anomalous-prompt-workbench'); +assert.equal(all(workbench).some(el => el.tagName === 'button' && /export|导出/i.test(el.textContent)), false); +owner.sidePromptComposerControl.addBlock({ content: 'cinematic light', role: 'positive' }); +await f.button(workbench, '📋').click(); +assert.equal(f.clipboard.at(-1), 'cinematic light'); +const name = workbench.querySelector('.anomalous-prompt-name-input'); +name.value = 'Local combination'; name.oninput(); +f.fetch = async () => ({ ok: true, json: async () => ({ status: 'success', material: { filename: 'local.json' } }) }); +await f.button(workbench, '💾').click(); +const save = f.requests.findLast(([url]) => url.includes('/save_prompt_plan')); +assert.equal(JSON.parse(save[1].body).plan.positive, 'cinematic light'); +studio.closePromptStudio(owner); + +const recipes = await f.module('ui_recipes.js'); +const recipeOwner = { recipeListContainer: new Element('div') }; +recipes.renderRecipeList.call(recipeOwner, [{ filename: 'example.json', data: { name: 'Example', params: {} } }]); +const exportButton = all(recipeOwner.recipeListContainer).find(el => el.attrs?.['aria-label'] === 'Recipe package export is temporarily unavailable'); +assert.ok(exportButton); +assert.equal(exportButton.disabled, true); +await exportButton.click(); +assert.equal(f.requests.some(([url]) => url.includes('/export_recipe_package')), false); +assert.deepEqual(f.errors, []); +console.log('Export availability: prompt export absent; copy/local save work; recipe export cannot issue a request.'); diff --git a/tests/test_export_availability.py b/tests/test_export_availability.py new file mode 100644 index 0000000..aaaac10 --- /dev/null +++ b/tests/test_export_availability.py @@ -0,0 +1,26 @@ +import json +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +PLUGIN_DIR = Path(__file__).resolve().parents[1] +sys.path[:0] = [str(PLUGIN_DIR), str(PLUGIN_DIR.parents[1])] +from api import recipe_packages + + +class ExportAvailabilityTests(unittest.IsolatedAsyncioTestCase): + async def test_export_rejected_before_request_or_files_are_read(self): + class UnreadRequest: + async def json(self): + raise AssertionError('Disabled export must not read request data') + + with patch.object(recipe_packages, 'get_recipes_dir', side_effect=AssertionError('No filesystem access')): + response = await recipe_packages.api_export_recipe_package(UnreadRequest()) + self.assertEqual(response.status, 503) + self.assertEqual(json.loads(response.text)['code'], 'recipe_export_disabled') + self.assertNotIn('Content-Disposition', response.headers) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ui_fixture.mjs b/tests/ui_fixture.mjs index 2dae420..ce0e9b7 100644 --- a/tests/ui_fixture.mjs +++ b/tests/ui_fixture.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import vm from 'node:vm'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; export const all = node => [node, ...node.children.flatMap(all)]; export class Events { @@ -95,11 +95,11 @@ export function fixture() { if (modules.has(filename)) return modules.get(filename); let source; if (filename.endsWith('/scripts/app.js')) source = 'export const app = globalThis.app;'; - else if (filename.endsWith('/ui_dialog.js')) source = 'export async function anomalousAlert(message) { errors.push(message); } export async function anomalousConfirm() { return true; }'; + else if (filename.endsWith('/ui_dialog.js')) source = 'export async function anomalousAlert(message) { errors.push(message); } export async function anomalousConfirm() { return true; } export async function anomalousPrompt() { return null; }'; else if (filename.endsWith('/material_feedback.js')) source = 'export function showMaterialSaved() {}'; else if (filename.endsWith('/ui_gallery_detail.js')) source = 'export function showImageWorkbench() {}'; else source = fs.readFileSync(filename, 'utf8'); - const mod = new vm.SourceTextModule(source, { context, identifier: filename }); + const mod = new vm.SourceTextModule(source, { context, identifier: filename, initializeImportMeta: meta => { meta.url = pathToFileURL(filename).href; } }); modules.set(filename, mod); return mod; }; state.module = async name => { diff --git a/web/modules/locales.js b/web/modules/locales.js index ad51d55..938d6be 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -15,7 +15,6 @@ export const i18n = { promptInsert_after: '放在后面', promptDragText: '拖到提示词节点', promptChooseTarget: '选择要写入的文本框', - promptExportError: '无法导出:文件需小于 2 MiB,标签最多 20 个、每个不超过 60 字。', materialApplyFailed: '应用失败,节点参数已恢复。', materialUndo: "撤销这次应用", materialUndone: "已撤销", @@ -70,7 +69,6 @@ export const i18n = { promptAppend: "追加到节点", promptReplace: "替换节点文本", promptSavePlan: "保存组合", - promptExportPlan: "导出组合", promptImportPlan: "导入提示词方案", promptAlreadyImported: "素材库中已有相同的提示词方案。", promptImportError: "导入失败,请选择有效的提示词方案 JSON 文件(不超过 2 MiB)。", @@ -227,7 +225,7 @@ export const i18n = {
1. 🔄 前置准备 (扫描向导)
具体位置:界面左侧底部的 扫描向导 (🔄)。
这是最重要的前置操作!请先打开扫描向导执行扫描。切记保持网络畅通。部分 C站限制级模型信息需要在设置中填入 API-KEY。扫描完成后,插件的各项功能基本激活。
2. 🩺 拯救爆红 (模型医生)
具体位置:左侧底部第二个 听诊器 (🩺) 按钮。
当导入别人使用该插件输出的工作流或者图片时,若模型路径爆红,点击模型医生就能智能识别当前所有节点实现正确路径的替换。
3. 🤖 选中交互与参数预设 (节点助手)
具体位置:左侧第三个 机器人 (🤖) 按钮。
在画布上选中节点后,“动作”页可以可视化替换模型,或在兼容的 MODEL + CLIP 链前后插入 LoRA。“参数方案”页会按配方分组读取工作流配方保存的同类型节点参数;点击一条记录即可把参数应用到当前选中节点。例如选中 KSampler 后,可一键载入配方中的采样器、调度器、步数、CFG 与降噪等值;种子等每次运行变化的参数会被忽略。
4. 工作流配方、参数方案与素材库
具体位置:打开 创作工作台。
配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入/导出配方包。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。
4. 工作流配方、参数方案与素材库
具体位置:打开 创作工作台。
配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入配方包,配方包导出暂未开放。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。
5. 个性化配置 (设置面板)
具体位置:左侧底部的 齿轮 () 按钮。
可以调节语言、主页面字体大小;进入“模型设置”可选择视频封面始终播放或悬停播放,以及卡片使用流畅缩略图或原始封面。详情页始终保留原图画质。
6. 📑 顶部导航与提示词笔记
具体位置:右侧顶部的按钮分别为 模型 ()、图库 ()、创作工作台 (📑)、停靠侧边栏 (◧)。
进入创作工作台的“提示词笔记”,点击新建 。选择基础模型、主模型和 LoRA 后,可通过 📝纯文本/粘贴 填入提示词并进行双语对照、分块、查找替换和标签编辑,最后点击 发送到画布 一键组装节点。
7. 文件夹管理 (Folder Manager)
具体位置:设置 () 面板内的 文件夹管理。
可以用来隐藏不需要的内置文件夹。隐藏后的文件夹不仅从侧边栏消失,后端的静默扫描和本地读取也会完全跳过,实现真正的“零性能损耗”。并且可以在这里拖拽调整文件夹在侧边栏显示的顺序。
1. 🔄 Prerequisites (Scan Wizard)
Location: The Scan (🔄) icon at the bottom left.
This is the most crucial step! Open the Scan Wizard and execute a scan. Keep your network connected. (API-KEY may be needed in Settings for NSFW Civitai models). Scanning activates all core features.
2. 🩺 Fix Broken Nodes (Model Doctor)
Location: The 2nd icon (Stethoscope 🩺) from the bottom left.
When importing a workflow/image exported by this plugin and nodes turn red, open the Model Doctor to intelligently identify and replace them with correct local paths.
3. 🤖 Node actions and parameter presets (Node Assistant)
Location: The 3rd icon (Robot 🤖) from the bottom left.
After selecting a canvas node, Actions can visually replace its model or insert a LoRA before/after a compatible MODEL + CLIP chain. Parameter Presets reads same-type node values saved by Workflow Recipes and groups them by recipe. Click one record to apply it to the selected node. For example, selecting a KSampler can load its sampler, scheduler, steps, CFG, and denoise values in one click; volatile values such as seeds are ignored.
4. Workflow Recipes and Parameter Sets
Location: Open the Creative Workspace and switch to Workflow Recipes.
A recipe saves the workflow, cover, tags, notes, model references, and common parameters. Open partial recipes append to the current canvas, while independently runnable complete recipes open in a new canvas; both can be shared as import/export packages. Details include Overview, Parameter Set, Gallery, and Versions for local model matching, full parameter viewing/copying, output comparison, and version comparison/restoration. Parameter notes can capture fresh values from a matching live canvas or edit safe controls; Apply to Current Workflow writes only after the recipe skeleton matches. Third-party prompt nodes that cannot be classified reliably can be labelled manually.
4. Workflow Recipes and Parameter Sets
Location: Open the Creative Workspace and switch to Workflow Recipes.
A recipe saves the workflow, cover, tags, notes, model references, and common parameters. Open partial recipes append to the current canvas, while independently runnable complete recipes open in a new canvas; both support importing recipe packages; package export is temporarily unavailable. Details include Overview, Parameter Set, Gallery, and Versions for local model matching, full parameter viewing/copying, output comparison, and version comparison/restoration. Parameter notes can capture fresh values from a matching live canvas or edit safe controls; Apply to Current Workflow writes only after the recipe skeleton matches. Third-party prompt nodes that cannot be classified reliably can be labelled manually.
5. Settings Panel
Location: The Gear () icon at the bottom left.
Adjust language and UI scale. Open “Model Settings” to choose always-play or hover-play video covers and optimized thumbnails or original covers. Detail pages always retain original image quality.
6. 📑 Top Navigation & Prompt Notes
Location: Top-right tabs Models (), Gallery (), Creative Workspace (📑), and Dock (◧).
Open Prompt Notes in the Creative Workspace and click . Select a Base Model, Main Model, and LoRAs; use 📝 Edit Raw/Paste for bilingual prompts, chunking, find/replace, and tag editing; then click Send to Canvas to assemble the nodes.
7. Folder Manager
Location: Inside the Settings () panel, click Manage Folders.
Use this to hide unneeded built-in folders. Hidden folders will be completely skipped by background scans and I/O reads, achieving true "zero performance cost". You can also drag and drop to reorder how folders appear in the sidebar.