fix(exports): remove prompt JSON export and pause recipe packages

This commit is contained in:
DemonGatanjieu
2026-09-15 20:50:58 +08:00
parent 45958356f1
commit 8bd3130fe7
15 changed files with 110 additions and 107 deletions
+2
View File
@@ -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
+5 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+9
View File
@@ -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", ""))
+5 -1
View File
@@ -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 |
+2 -2
View File
@@ -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.
+15
View File
@@ -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.
+31
View File
@@ -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.');
+26
View File
@@ -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()
+3 -3
View File
@@ -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 => {
+4 -16
View File
@@ -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 = {
<p><strong>1. 🔄 前置准备 (扫描向导)</strong><br>具体位置:界面左侧底部的 <strong>扫描向导 (🔄)</strong>。<br>这是最重要的前置操作!请先打开扫描向导执行扫描。切记保持网络畅通。部分 C站限制级模型信息需要在设置中填入 API-KEY。扫描完成后,插件的各项功能基本激活。</p>
<p><strong>2. 🩺 拯救爆红 (模型医生)</strong><br>具体位置:左侧底部第二个 <strong>听诊器 (🩺)</strong> 按钮。<br>当导入别人使用该插件输出的工作流或者图片时,若模型路径爆红,点击模型医生就能智能识别当前所有节点实现正确路径的替换。</p>
<p><strong>3. 🤖 选中交互与参数预设 (节点助手)</strong><br>具体位置:左侧第三个 <strong>机器人 (🤖)</strong> 按钮。<br>在画布上选中节点后,“动作”页可以可视化替换模型,或在兼容的 MODEL + CLIP 链前后插入 LoRA。“参数方案”页会按配方分组读取工作流配方保存的同类型节点参数;点击一条记录即可把参数应用到当前选中节点。例如选中 KSampler 后,可一键载入配方中的采样器、调度器、步数、CFG 与降噪等值;种子等每次运行变化的参数会被忽略。</p>
<p><strong>4. 工作流配方、参数方案与素材库</strong><br>具体位置:打开 <strong>创作工作台</strong>。<br>配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入/导出配方包。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。</p>
<p><strong>4. 工作流配方、参数方案与素材库</strong><br>具体位置:打开 <strong>创作工作台</strong>。<br>配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入配方包,配方包导出暂未开放。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。</p>
<p><strong>5. 个性化配置 (设置面板)</strong><br>具体位置:左侧底部的 <strong>齿轮 ()</strong> 按钮。<br>可以调节语言、主页面字体大小;进入“模型设置”可选择视频封面始终播放或悬停播放,以及卡片使用流畅缩略图或原始封面。详情页始终保留原图画质。</p>
<p><strong>6. 📑 顶部导航与提示词笔记</strong><br>具体位置:右侧顶部的按钮分别为 <strong>模型 ()</strong>、<strong>图库 ()</strong>、<strong>创作工作台 (📑)</strong>、<strong>停靠侧边栏 (◧)</strong>。<br>进入创作工作台的“提示词笔记”,点击新建 <strong></strong>。选择基础模型、主模型和 LoRA 后,可通过 <strong>📝纯文本/粘贴</strong> 填入提示词并进行双语对照、分块、查找替换和标签编辑,最后点击 <strong>发送到画布</strong> 一键组装节点。</p>
<p><strong>7. 文件夹管理 (Folder Manager)</strong><br>具体位置:设置 () 面板内的 <strong>文件夹管理</strong>。<br>可以用来隐藏不需要的内置文件夹。隐藏后的文件夹不仅从侧边栏消失,后端的静默扫描和本地读取也会完全跳过,实现真正的“零性能损耗”。并且可以在这里拖拽调整文件夹在侧边栏显示的顺序。</p>
@@ -864,12 +862,8 @@ export const i18n = {
recipeFilterByTag: '按此标签筛选',
recipeImport: '导入配方包',
recipeExport: '导出配方包',
recipeExportError: '导出工作流配方包失败。',
recipeExportUnavailable: '配方包导出暂未开放',
recipeImportError: '导入工作流配方包失败。',
recipeExportSnapshotsConfirm: '导出时包含模型预览快照吗?\n\n快照是展示数据,不包含模型文件;视频封面只保留轻量静态快照,不会打包原视频。',
recipeExportHistoryConfirm: '导出时包含历史版本吗?\n\n历史可能包含旧提示词和备注。',
recipeExportModelNotesConfirm: '导出时包含每个模型的个人备注吗?\n\n这些备注可能包含你的私有使用习惯;选择“否”将仅从导出包中移除,原配方不受影响。',
recipeExportRedactIdentityConfirm: '是否移除模型 SHA-256、大小和来源信息?\n\n点击“确定”将移除这些身份信息。',
recipeImportSummary: '配方包检查完成。确认导入以下内容吗?',
recipeImportAssets: '预览资产',
recipeImportHistory: '历史版本',
@@ -1150,7 +1144,6 @@ export const i18n = {
promptInsert_after: 'Add to end',
promptDragText: 'Drag to a prompt node',
promptChooseTarget: 'Choose the text field to update',
promptExportError: 'Cannot export: the file must be at most 2 MiB, with up to 20 tags of 60 characters each.',
materialApplyFailed: 'Application failed; node parameters were restored.',
materialUndo: "Undo this application",
materialUndone: "Undone",
@@ -1205,7 +1198,6 @@ export const i18n = {
promptAppend: "Append to node",
promptReplace: "Replace node text",
promptSavePlan: "Save Plan",
promptExportPlan: "Export Plan",
promptImportPlan: "Import prompt plan",
promptAlreadyImported: "An identical prompt plan is already in the library.",
promptImportError: "Import failed. Choose a valid prompt plan JSON file (up to 2 MiB).",
@@ -1362,7 +1354,7 @@ export const i18n = {
<p><strong>1. 🔄 Prerequisites (Scan Wizard)</strong><br>Location: The <strong>Scan (🔄)</strong> icon at the bottom left.<br>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.</p>
<p><strong>2. 🩺 Fix Broken Nodes (Model Doctor)</strong><br>Location: The 2nd icon <strong>(Stethoscope 🩺)</strong> from the bottom left.<br>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.</p>
<p><strong>3. 🤖 Node actions and parameter presets (Node Assistant)</strong><br>Location: The 3rd icon <strong>(Robot 🤖)</strong> from the bottom left.<br>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.</p>
<p><strong>4. Workflow Recipes and Parameter Sets</strong><br>Location: Open the <strong>Creative Workspace</strong> and switch to <strong>Workflow Recipes</strong>.<br>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.</p>
<p><strong>4. Workflow Recipes and Parameter Sets</strong><br>Location: Open the <strong>Creative Workspace</strong> and switch to <strong>Workflow Recipes</strong>.<br>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.</p>
<p><strong>5. Settings Panel</strong><br>Location: The <strong>Gear ()</strong> icon at the bottom left.<br>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.</p>
<p><strong>6. 📑 Top Navigation & Prompt Notes</strong><br>Location: Top-right tabs <strong>Models ()</strong>, <strong>Gallery ()</strong>, <strong>Creative Workspace (📑)</strong>, and <strong>Dock (◧)</strong>.<br>Open Prompt Notes in the Creative Workspace and click <strong></strong>. Select a Base Model, Main Model, and LoRAs; use <strong>📝 Edit Raw/Paste</strong> for bilingual prompts, chunking, find/replace, and tag editing; then click <strong>Send to Canvas</strong> to assemble the nodes.</p>
<p><strong>7. Folder Manager</strong><br>Location: Inside the Settings () panel, click <strong>Manage Folders</strong>.<br>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.</p>
@@ -1998,12 +1990,8 @@ export const i18n = {
recipeFilterByTag: 'Filter by this tag',
recipeImport: 'Import package',
recipeExport: 'Export package',
recipeExportError: 'Could not export the Workflow Recipe package.',
recipeExportUnavailable: 'Recipe package export is temporarily unavailable',
recipeImportError: 'Could not import the Workflow Recipe package.',
recipeExportSnapshotsConfirm: 'Include model preview snapshots in the export?\n\nSnapshots are presentation data; video covers are reduced to lightweight stills and original videos are never packed.',
recipeExportHistoryConfirm: 'Include historical versions in the export?\n\nHistory may contain older prompts and notes.',
recipeExportModelNotesConfirm: 'Include personal notes for each model in the export?\n\nThese notes may contain private usage habits. Choosing No removes them only from the exported package.',
recipeExportRedactIdentityConfirm: 'Remove model SHA-256, size, and provenance information?\n\nChoose OK to remove those identity fields.',
recipeDialogNo: 'No',
recipeImportSummary: 'The recipe package passed inspection. Import these contents?',
recipeImportAssets: 'Preview assets',
-1
View File
@@ -1222,7 +1222,6 @@ function showTransferCenter(owner) {
} catch (error) { status.textContent = t('promptImportError'); }
finally { file.value = ''; importPlan.disabled = false; }
};
text(dialog, 'p', t('promptExportLocation'));
const close = text(dialog, 'button', t('close'), 'anomalous-btn-ghost'); close.onclick = () => dialog.close();
dialog.onclose = () => dialog.remove(); document.body.appendChild(dialog); dialog.showModal();
}
+1 -34
View File
@@ -87,7 +87,7 @@ export function createPromptWorkbench(owner, container, scope, options) {
};
}
// "More" Dropdown Menu (holds Save, New Draft, Tags, Export, Material Sync)
// "More" Dropdown Menu (holds Save, New Draft, Tags, Material Sync)
const moreWrap = text(topActions, 'div', '', 'anomalous-prompt-more-wrap');
const moreBtn = text(moreWrap, 'button', window.anomalous_browser_lang === 'zh' ? '··· 更多' : '··· More', 'anomalous-btn-ghost anomalous-btn-sm anomalous-prompt-more-btn');
const moreMenu = text(moreWrap, 'div', '', 'anomalous-prompt-more-menu');
@@ -123,8 +123,6 @@ export function createPromptWorkbench(owner, container, scope, options) {
}
};
const exportBtn = text(moreMenu, 'button', `📤 ${t('promptExportPlan')}`, 'anomalous-prompt-more-item');
const syncLibBtn = text(moreMenu, 'button', `📥 ${window.anomalous_browser_lang === 'zh' ? '同步素材库' : 'Sync Library'}`, 'anomalous-prompt-more-item');
syncLibBtn.onclick = () => {
closeMoreMenu();
@@ -855,37 +853,6 @@ export function createPromptWorkbench(owner, container, scope, options) {
}
};
// Export Plan JSON
exportBtn.onclick = () => {
closeMoreMenu();
if (!draft.name?.trim()) {
nameInput.focus();
nameInput.classList.add('is-invalid');
setTimeout(() => nameInput.classList.remove('is-invalid'), 1200);
showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '请先在顶栏输入方案名称!' : 'Please enter preset name first!');
return;
}
syncDraftSynthesizedText(draft);
const saved = workbenchDraftToSavedPlan(draft);
const exportData = {
format: 'anomalous-prompt-plan-v1',
version: 2,
name: draft.name,
tags: draft.tags,
plan: saved.plan,
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
if (blob.size > 2 * 1024 * 1024 || draft.tags.length > 20 || draft.tags.some(tag => tag.length > 60)) {
return anomalousAlert(t('promptExportError'));
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${draft.name || 'prompt-mixer-preset'}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
// New Draft
newBtn.onclick = async () => {
closeMoreMenu();
+3 -9
View File
@@ -1111,15 +1111,9 @@ function renderOverview(content, owner, recipe, references, finish) {
const heroExport = button(more, '', 'anomalous-btn-ghost');
heroExport.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>${t('recipeExport')}`;
heroExport.title = t('recipeExport');
heroExport.onclick = async () => {
try {
await exportRecipePackage(owner.recipeDetailFilename, recipe);
} catch (error) {
console.error('Could not export recipe package:', error);
await anomalousAlert(t('recipeExportError'));
}
};
heroExport.title = t('recipeExportUnavailable');
heroExport.setAttribute('aria-label', t('recipeExportUnavailable'));
heroExport.disabled = true;
copy.appendChild(overviewActions);
+3 -39
View File
@@ -183,40 +183,6 @@ function appendRecipeCover(parent, url, alt, seed = '') {
parent.appendChild(image);
}
async function exportRecipePackage(filename) {
const choice = { noLabel: t('recipeDialogNo') };
const includeSnapshots = await anomalousConfirm(t('recipeExportSnapshotsConfirm'), 'Anomalous', choice);
if (includeSnapshots === null) return;
const includeHistory = await anomalousConfirm(t('recipeExportHistoryConfirm'), 'Anomalous', choice);
if (includeHistory === null) return;
const includeModelNotes = await anomalousConfirm(t('recipeExportModelNotesConfirm'), 'Anomalous', choice);
if (includeModelNotes === null) return;
const redactIdentity = await anomalousConfirm(t('recipeExportRedactIdentityConfirm'), 'Anomalous', choice);
if (redactIdentity === null) return;
const includeIdentity = !redactIdentity;
const response = await fetch('/anomalous/export_recipe_package', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename,
include_snapshots: includeSnapshots,
include_history: includeHistory,
include_identity: includeIdentity,
include_model_notes: includeModelNotes,
}),
});
if (!response.ok) throw new Error('recipe export failed');
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename.replace(/\.json$/i, '')}.anomalous-recipe.zip`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
async function importRecipePackage(owner, file) {
const inspectResponse = await fetch('/anomalous/import_recipe_package_inspect', {
method: 'POST',
@@ -1156,12 +1122,10 @@ function createRecipeCard(owner, recipe) {
exportBtn.className = 'anomalous-recipe-card-mini-btn anomalous-tooltip-target';
exportBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
exportBtn.removeAttribute('title');
exportBtn.setAttribute('data-tooltip', t('recipeExport'));
exportBtn.setAttribute('data-tooltip', t('recipeExportUnavailable'));
exportBtn.setAttribute('aria-label', t('recipeExportUnavailable'));
exportBtn.disabled = true;
exportBtn.setAttribute('data-tooltip-pos', 'top');
exportBtn.onclick = (e) => {
e.stopPropagation();
runRecipeCardAction(exportBtn, () => exportRecipePackage(recipe.filename), 'recipeExportError');
};
const removeBtn = document.createElement('button');
removeBtn.type = 'button';