Files
ComfyUI-Lora-Manager/templates/components/context_menu.html
Will Miao cf898da193 feat(agent): add LLM-powered metadata enrichment system with AgentCLI and PostProcessor
Introduce an agent skill framework for LLM-driven metadata enrichment:

- AgentCLI (py/agent_cli/): in-process wrappers around internal services
  using standard relative imports, eliminating the need for sys.path hacks
- LLMService: centralized BYOK (bring-your-own-key) LLM client supporting
  OpenAI, Ollama, and custom OpenAI-compatible endpoints
- PostProcessor: deterministic engine that applies LLM output via AgentCLI
  (replaces old handler.py + _BASE_MODEL_ALIASES approach)
- SkillRegistry: filesystem-based skill discovery (skill.yaml + prompt.md)
- AgentService: orchestrates skill execution with WebSocket progress
- Frontend AgentManager: WebSocket listeners, skill execution, config UI
- Context menu entries (single + bulk) for "Enrich Metadata (Agent)"
- Settings UI for AI Provider configuration (BYOK)
- Full i18n support across 9 locales

Bug fixes found during review:
- aiohttp.web.json_response: status_code= -> status=
- settings_modal cancelEditApiKey: wrong argument position
- AgentManager.isLlmConfigured: allow Ollama without API key
- PostProcessor._merge_tags: lowercase all tags to match TagUpdateService
2026-07-02 21:27:01 +08:00

205 lines
11 KiB
HTML

<div id="loraContextMenu" class="context-menu">
<!-- <div class="context-menu-item" data-action="detail">
<i class="fas fa-info-circle"></i> Show Details
</div> -->
<!-- <div class="context-menu-item" data-action="civitai">
<i class="fas fa-external-link-alt"></i> View on Civitai
</div> -->
<!-- Metadata -->
<div class="context-menu-item" data-action="refresh-metadata">
<i class="fas fa-sync"></i> <span>{{ t('loras.contextMenu.refreshMetadata') }}</span>
</div>
<div class="context-menu-item" data-action="check-updates">
<i class="fas fa-bell"></i> <span>{{ t('loras.contextMenu.checkUpdates') }}</span>
</div>
<div class="context-menu-item" data-action="enrich-hf-agent">
<i class="fas fa-wand-magic-sparkles"></i> <span>{{ t('loras.contextMenu.enrichHfAgent') }}</span>
</div>
<div class="context-menu-item" data-action="relink-civitai">
<i class="fas fa-link"></i> <span>{{ t('loras.contextMenu.relinkCivitai') }}</span>
</div>
<div class="context-menu-separator menu-section-break"></div>
<!-- Workflow -->
<div class="context-menu-item" data-action="copyname">
<i class="fas fa-copy"></i> <span>{{ t('loras.contextMenu.copySyntax') }}</span>
</div>
<div class="context-menu-item" data-action="sendappend">
<i class="fas fa-paper-plane"></i> <span>{{ t('loras.contextMenu.sendToWorkflowAppend') }}</span>
</div>
<div class="context-menu-item" data-action="sendreplace">
<i class="fas fa-exchange-alt"></i> <span>{{ t('loras.contextMenu.sendToWorkflowReplace') }}</span>
</div>
<div class="context-menu-separator menu-section-break"></div>
<!-- Media / Preview -->
<div class="context-menu-item" data-action="preview">
<i class="fas fa-folder-open"></i> <span>{{ t('loras.contextMenu.openExamples') }}</span>
</div>
<div class="context-menu-item" data-action="download-examples">
<i class="fas fa-download"></i> <span>{{ t('loras.contextMenu.downloadExamples') }}</span>
</div>
<div class="context-menu-item" data-action="replace-preview">
<i class="fas fa-image"></i> <span>{{ t('loras.contextMenu.replacePreview') }}</span>
</div>
<div class="context-menu-separator menu-section-break"></div>
<!-- Attributes -->
<div class="context-menu-item" data-action="set-nsfw">
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.contextMenu.setContentRating') }}</span>
</div>
<div class="context-menu-separator menu-section-break"></div>
<!-- Organization -->
<div class="context-menu-item" data-action="move">
<i class="fas fa-folder-open"></i> <span>{{ t('loras.contextMenu.moveToFolder') }}</span>
</div>
<div class="context-menu-separator"></div>
<!-- Destructive -->
<div class="context-menu-item" data-action="exclude">
<i class="fas fa-eye-slash"></i> <span>{{ t('loras.contextMenu.excludeModel') }}</span>
</div>
<div class="context-menu-item delete-item" data-action="delete">
<i class="fas fa-trash"></i> <span>{{ t('loras.contextMenu.deleteModel') }}</span>
</div>
</div>
<div id="bulkContextMenu" class="context-menu">
<div class="bulk-context-header">
<i class="fas fa-th-large"></i>
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
</div>
<div class="context-menu-separator"></div>
<div class="context-menu-section" data-section="metadata">
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.metadata') }}</div>
<div class="context-menu-item" data-action="refresh-all">
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
</div>
<div class="context-menu-item" data-action="check-updates">
<i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span>
</div>
<div class="context-menu-item" data-action="repair-metadata">
<i class="fas fa-tools"></i> <span>{{ t('loras.bulkOperations.repairMetadata') }}</span>
</div>
<div class="context-menu-item" data-action="reimport-metadata">
<i class="fas fa-undo-alt"></i> <span>{{ t('loras.bulkOperations.reimportMetadata') }}</span>
</div>
<div class="context-menu-item" data-action="skip-metadata-refresh">
<i class="fas fa-ban"></i> <span>{{ t('loras.bulkOperations.skipMetadataRefresh') }}</span>
</div>
<div class="context-menu-item" data-action="resume-metadata-refresh">
<i class="fas fa-redo"></i> <span>{{ t('loras.bulkOperations.resumeMetadataRefresh') }}</span>
</div>
<div class="context-menu-item" data-action="enrich-hf-agent-bulk">
<i class="fas fa-wand-magic-sparkles"></i> <span>{{ t('loras.bulkOperations.enrichHfAgent') }}</span>
</div>
</div>
<div class="context-menu-section" data-section="workflow">
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.workflow') }}</div>
<div class="context-menu-item has-submenu" data-has-submenu="send-to-workflow">
<i class="fas fa-paper-plane"></i>
<span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
<i class="fas fa-chevron-right submenu-arrow"></i>
<div class="context-submenu">
<div class="context-menu-item" data-action="send-to-workflow-append">
<i class="fas fa-paper-plane"></i> <span>{{ t('loras.contextMenu.sendToWorkflowAppend') }}</span>
</div>
<div class="context-menu-item" data-action="send-to-workflow-replace">
<i class="fas fa-exchange-alt"></i> <span>{{ t('loras.contextMenu.sendToWorkflowReplace') }}</span>
</div>
<div class="context-menu-item" data-action="copy-all">
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
</div>
</div>
</div>
</div>
<div class="context-menu-section" data-section="attributes">
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.attributes') }}</div>
<div class="context-menu-item" data-action="add-tags">
<i class="fas fa-tags"></i> <span>{{ t('loras.bulkOperations.addTags') }}</span>
</div>
<div class="context-menu-item" data-action="set-base-model">
<i class="fas fa-layer-group"></i> <span>{{ t('loras.bulkOperations.setBaseModel') }}</span>
</div>
<div class="context-menu-item" data-action="set-favorite">
<i class="fas fa-star"></i> <span>{{ t('loras.bulkOperations.setFavorite') }}</span>
</div>
<div class="context-menu-item" data-action="set-content-rating">
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span>
</div>
</div>
<div class="context-menu-section" data-section="download">
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.download') }}</div>
<div class="context-menu-item" data-action="download-example-images">
<i class="fas fa-download"></i> <span>{{ t('loras.bulkOperations.downloadExamples') }}</span>
</div>
<div class="context-menu-item" data-action="download-missing-loras">
<i class="fas fa-download"></i> <span>{{ t('loras.bulkOperations.downloadMissingLoras') }}</span>
</div>
</div>
<div class="context-menu-section" data-section="organize">
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.organize') }}</div>
<div class="context-menu-item" data-action="auto-organize">
<i class="fas fa-magic"></i> <span>{{ t('loras.bulkOperations.autoOrganize') }}</span>
</div>
<div class="context-menu-item" data-action="move-all">
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
</div>
</div>
<div class="context-menu-separator"></div>
<div class="context-menu-item delete-item" data-action="delete-all">
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
</div>
</div>
<div id="globalContextMenu" class="context-menu">
<div class="context-menu-item" data-action="download-example-images">
<i class="fas fa-download"></i> <span>{{ t('globalContextMenu.downloadExampleImages.label') }}</span>
</div>
<div class="context-menu-item" data-action="check-model-updates">
<i class="fas fa-sync-alt"></i> <span>{{ t('globalContextMenu.checkModelUpdates.label') }}</span>
</div>
<div class="context-menu-item" data-action="fetch-missing-licenses">
<i class="fas fa-shield-alt"></i> <span>{{ t('globalContextMenu.fetchMissingLicenses.label') }}</span>
</div>
<div class="context-menu-item" data-action="cleanup-example-images-folders">
<i class="fas fa-trash-restore"></i> <span>{{ t('globalContextMenu.cleanupExampleImages.label') }}</span>
</div>
<div class="context-menu-item" data-action="manage-excluded-models">
<i class="fas fa-eye-slash"></i> <span>{{ t('globalContextMenu.manageExcludedModels.label', default='Manage Excluded Models') }}</span>
</div>
<div class="context-menu-separator"></div>
<div class="context-menu-item" data-action="toggle-group-by-model">
<i class="fas fa-layer-group"></i> <span>{{ t('globalContextMenu.groupByModel.label') }}</span>
<i class="fas fa-check check-indicator" style="margin-left:auto;display:none"></i>
</div>
<div class="context-menu-item" data-action="repair-recipes">
<i class="fas fa-tools"></i> <span>{{ t('globalContextMenu.repairRecipes.label') }}</span>
</div>
</div>
<!-- Sidebar Folder Context Menu -->
<div id="sidebarFolderContextMenu" class="context-menu">
<div class="context-menu-item" data-action="check-folder-updates">
<i class="fas fa-bell"></i> <span>{{ t('sidebar.folderUpdateCheck.label') }}</span>
</div>
</div>
<div id="nsfwLevelSelector" class="nsfw-level-selector">
<div class="nsfw-level-header">
<h3>{{ t('modals.contentRating.title') }}</h3>
<button class="close-nsfw-selector"><i class="fas fa-times"></i></button>
</div>
<div class="nsfw-level-content">
<div class="current-level"><span>{{ t('modals.contentRating.current') }}:</span> <span id="currentNSFWLevel">{{
t('common.status.unknown') }}</span></div>
<div class="nsfw-level-options">
<button class="nsfw-level-btn" data-level="1">{{ t('modals.contentRating.levels.pg') }}</button>
<button class="nsfw-level-btn" data-level="2">{{ t('modals.contentRating.levels.pg13') }}</button>
<button class="nsfw-level-btn" data-level="4">{{ t('modals.contentRating.levels.r') }}</button>
<button class="nsfw-level-btn" data-level="8">{{ t('modals.contentRating.levels.x') }}</button>
<button class="nsfw-level-btn" data-level="16">{{ t('modals.contentRating.levels.xxx') }}</button>
</div>
</div>
</div>
<div id="nodeSelector" class="node-selector">
<!-- Dynamic node list will be populated here -->
</div>