feat: 实现图像模式与多模态功能架构
- 新增图像模式支持文生图(T2I)和图生图(I2I)功能 - 实现图像服务层,支持Gemini/Seedream/OpenAI等多适配器 - 升级功能模式架构:basic/pro/image三重模式系统 - 新增图像模型管理器与专用UI组件 - 优化存储键管理,统一图像模式相关配置 - 完善国际化支持,新增图像模式相关翻译 - 增强模型管理器,支持文本/图像双模型管理
This commit is contained in:
1 parent
6e4917852a
commit
2ffb7556ee
71 files changed
+6868
-1147
No files matched your search
+5
-1
@@ -189,4 +189,8 @@ nul
|
||||
.trae
|
||||
.cursor/rules/bmad
|
||||
CLAUDE.md
|
||||
.vscode
|
||||
.vscode
|
||||
.mcp.json
|
||||
.kilocode
|
||||
.serena
|
||||
.kiro
|
||||
@@ -6,6 +6,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: linshen/prompt-optimizer:dev
|
||||
container_name: prompt-optimizer-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# 图像模式(Image Mode)
|
||||
|
||||
图像模式提供文生图(T2I)与图生图(I2I,单张本地图片)能力,输出统一为 base64(默认 image/png),生成多张时串行执行。
|
||||
|
||||
## 功能范围
|
||||
- 文生图:仅文本提示词
|
||||
- 图生图:单张本地图片 + 文本提示词(仅 png/jpeg,≤10MB)
|
||||
- 输出:base64(默认 image/png)
|
||||
- 生成张数:1~4(串行,不并发)
|
||||
- 暂不支持:多图融合、组图、mask/局部编辑、upscale、历史记录、图像模板
|
||||
|
||||
## 内置图像模型与环境变量
|
||||
- Gemini(image-gemini)
|
||||
- provider: `gemini`
|
||||
- defaultModel: `gemini-2.5-flash-image-preview`
|
||||
- apiKey: 复用 `VITE_GEMINI_API_KEY`
|
||||
- Seedream(image-seedream)
|
||||
- provider: `seedream`
|
||||
- defaultModel: `doubao-seedream-4-0-250828`
|
||||
- apiKey: 读取 `VITE_SEEDREAM_API_KEY` | `VITE_ARK_API_KEY`(或 `process.env.ARK_API_KEY`)
|
||||
|
||||
> 提示:配置好以上环境变量后,内置图像模型将自动注入并按需启用。
|
||||
|
||||
## 使用方法(Web)
|
||||
1. 顶部导航“高级模式”改为下拉:选择“图像模式”。
|
||||
2. 左侧输入提示词;可选择本地图片(图生图);设置生成张数(1~4)。
|
||||
3. 选择图像模型(来自图像模型管理器)。
|
||||
4. 点击“生成”,右侧显示单图 base64 预览,支持下载与复制。
|
||||
|
||||
## 模型管理
|
||||
- 模型管理器新增标签页:“文本模型|图像模型”。
|
||||
- 图像模型页支持:新增、编辑、启用/禁用、删除。
|
||||
- 连通性测试:当前版本在图像页暂不提供(后续可考虑快速小图验证)。
|
||||
|
||||
## 校验与限制
|
||||
- 本地图片:仅 `image/png` 或 `image/jpeg`;大小 ≤ 10MB(前端与后端均有校验)。
|
||||
- count:1~4,串行执行。
|
||||
- Seedream 请求固定关闭组图(`sequential_image_generation='disabled'`),返回 `b64_json`。
|
||||
|
||||
## 开发说明
|
||||
- 核心层:`ImageService` + 适配器(Gemini/Seedream/OpenAI),适配器注册表按 provider 路由。
|
||||
- UI:`ImageWorkspace.vue` 为图像模式工作区;通过 `useImageGeneration` 调用 `ImageService`。
|
||||
- 代理与网络:沿用现有 `useVercelProxy/useDockerProxy + getProxyUrl` 策略(Seedream REST)。
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# 图像模式模板与迭代能力改造规范(Spec)
|
||||
|
||||
本文档定义在 UI 与 Core 中新增“图像模式”的三类模板类型与相关能力,使图像模式与基础模式在体验上对齐(不启用高级模式),并为图像场景提供专属的“迭代”模板与流程。
|
||||
|
||||
- 目标
|
||||
- 在核心模板体系中引入独立的 `imageIterate` 模板类型,避免通过 tags/id 的二次过滤。
|
||||
- 按文生图/图生图/图像迭代三类划分默认模板目录与加载逻辑。
|
||||
- 在模板管理界面添加图像三类的管理入口(与基础/上下文并列)。
|
||||
- 在图像模式工作区完善“迭代”模板的初始化、持久化与调用(无需高级模式)。
|
||||
|
||||
- 范围
|
||||
- Core:模板类型、默认模板、加载器与管理器 API 扩展。
|
||||
- UI:模板管理器新增标签、TemplateSelect 支持 `imageIterate`、图像工作区迭代逻辑完善。
|
||||
- 存储:图像模式专属的迭代模板选择键。
|
||||
- i18n:图像模板分类的界面文案。
|
||||
|
||||
## 一、Core 改造
|
||||
|
||||
### 1. 新增模板类型 `imageIterate`
|
||||
- 文件:`packages/core/src/services/template/types.ts`
|
||||
- 在 `TemplateMetadata.templateType` union 与 zod schema 中新增 `'imageIterate'`。
|
||||
- 在 `ITemplateManager.listTemplatesByType` 的参数 union 中新增 `'imageIterate'`。
|
||||
- 文件:`packages/core/src/services/template/manager.ts`
|
||||
- `listTemplatesByType` 参数 union 同步扩展(实现已按模板元数据类型过滤,兼容新增类型)。
|
||||
- 文件:`packages/core/src/services/template/electron-proxy.ts`
|
||||
- 接口定义中的 union 同步增加 `'imageIterate'`。
|
||||
- 文件:`packages/core/src/services/template/static-loader.ts`
|
||||
- `TemplateType` union 新增 `'imageIterate'`。
|
||||
- `byType` 初始化新增 `imageIterate` 节点。
|
||||
- 将 `metadata.templateType === 'imageIterate'` 规范化映射到 `normalizedType = 'imageIterate'`。
|
||||
- 日志统计中增加 `imageIterate` 的计数。
|
||||
- 文件:`packages/core/src/services/prompt/service.ts`
|
||||
- `getDefaultTemplateId(...)` 对 `'imageIterate'` 增加处理与回退(若无可用 `imageIterate`,回退至 `iterate`)。
|
||||
|
||||
### 2. 默认模板目录重构与新增
|
||||
- 目录:`packages/core/src/services/template/default-templates/image-optimize/`
|
||||
- `text2image/`
|
||||
- 存放文生图优化模板:`*-optimize(.ts|_en.ts)` (如 `dalle-optimize` / `general-image-optimize` / `chinese-model-optimize` / `stable-diffusion-optimize`)。
|
||||
- `image2image/`
|
||||
- 存放图生图优化模板:`image2image-optimize(.ts|_en.ts)`。
|
||||
- `iterate/`(新增)
|
||||
- 新增图像迭代模板(中/英):
|
||||
- `image-iterate-general.ts`
|
||||
- `image-iterate-general_en.ts`
|
||||
- 模板元数据:`templateType: 'imageIterate'`,`language: 'zh'|'en'`,`version`,`lastModified`,`author`,`description`。
|
||||
- 内容要求(建议为高级模板:消息数组):
|
||||
- System:描述“基于优化后图像提示词做定向改进”的规则,强调视觉意图/风格延续、构图/光照/风格参数、强度/seed 等可控性。
|
||||
- User:包含 `originalPrompt`(或 last optimized prompt)与 `iterateInput`,指示“直接输出新的优化图像提示词”。
|
||||
- 文件:`packages/core/src/services/template/default-templates/index.ts`
|
||||
- 更新 import 路径(目录重构后)。
|
||||
- 引入并聚合 `image-iterate-general(.ts|_en.ts)` 到 `ALL_TEMPLATES`。
|
||||
|
||||
## 二、UI 改造
|
||||
|
||||
### 1. 模板管理器(TemplateManager.vue)
|
||||
- 文件:`packages/ui/src/components/TemplateManager.vue`
|
||||
- 新增三枚类别按钮(与现有两行三列布局一致):
|
||||
- 图像 · 文生图 → `currentCategory='image-text2image-optimize'` → 类型 `'text2imageOptimize'`。
|
||||
- 图像 · 图生图 → `currentCategory='image-image2image-optimize'` → 类型 `'image2imageOptimize'`。
|
||||
- 图像 · 迭代 → `currentCategory='image-iterate'` → 类型 `'imageIterate'`。
|
||||
- 类型映射:
|
||||
- props 支持初始 `templateType: 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate'`。
|
||||
- `getCategoryFromProps()` 增加以上映射。
|
||||
- `getCurrentTemplateType()` 返回当前分类对应的元数据类型(严格等值)。
|
||||
- 列表过滤:
|
||||
- `filteredTemplates` 新增三类过滤条件:`text2imageOptimize` / `image2imageOptimize` / `imageIterate`。
|
||||
- i18n:
|
||||
- 在 `zh-CN.ts` / `en-US.ts` 新增:
|
||||
- `templateManager.imageText2ImageTemplates`
|
||||
- `templateManager.imageImage2ImageTemplates`
|
||||
- `templateManager.imageIterateTemplates`
|
||||
|
||||
### 2. 模板选择器(TemplateSelect.vue)
|
||||
- 文件:`packages/ui/src/components/TemplateSelect.vue`
|
||||
- props.validator 支持 `'imageIterate'`。
|
||||
- 其余无需变更,`listTemplatesByType(props.type)` 会加载 `imageIterate`。
|
||||
|
||||
## 三、图像模式工作区(useImageWorkspace)
|
||||
|
||||
### 1. 存储键(Storage Keys)
|
||||
- 文件:`packages/core/src/constants/storage-keys.ts`
|
||||
- 在 `IMAGE_MODE_KEYS` 中新增:
|
||||
- `SELECTED_ITERATE_TEMPLATE: 'app:image-mode:selected-iterate-template'`
|
||||
|
||||
### 2. 迭代模板的初始化、恢复与保存
|
||||
- 文件:`packages/ui/src/composables/useImageWorkspace.ts`
|
||||
- 在 `initialize()/restoreSelections()` 阶段:
|
||||
- 读取 `IMAGE_MODE_KEYS.SELECTED_ITERATE_TEMPLATE`;若为空,则 `listTemplatesByType('imageIterate')` 并选择第一个可用模板。
|
||||
- 在 `saveSelections()` 中:
|
||||
- 若存在 `selectedIterateTemplate`,保存其 `id` 至上述存储键。
|
||||
|
||||
### 3. 触发迭代时使用所选模板
|
||||
- 与 PromptPanel 的迭代对接:
|
||||
- 推荐绑定到现有 `handleIteratePrompt`(其签名包含 `templateId`),或在 `handleIterate` 内部调用 `iteratePromptStream` 时明确传入 `state.selectedIterateTemplate.id`。
|
||||
- 若未选择,toast 明确提示“请选择图像迭代模板”。
|
||||
|
||||
### 4. 打开模板管理器类型(修复与支持)
|
||||
- 文件:`packages/ui/src/composables/useImageWorkspace.ts`
|
||||
- 修正 `handleOpenTemplateManager` 的优先级与类型选择:
|
||||
- 传入 `'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate'`。
|
||||
- 未传入参数时,根据 `imageMode` 推断 `'text2imageOptimize' | 'image2imageOptimize'`。
|
||||
|
||||
## 四、验收标准(Acceptance Criteria)
|
||||
- Core
|
||||
- TS 编译通过,`imageIterate` 类型在各接口与实现中受支持。
|
||||
- 静态加载器日志显示 `imageIterate` 模板数量 > 0(默认模板存在)。
|
||||
- `TemplateManager.listTemplatesByType('imageIterate')` 返回新增默认模板。
|
||||
- UI
|
||||
- 模板管理器出现 9 个分类(基础 3 + 上下文 3 + 图像 3)。
|
||||
- 每个“图像”分类仅显示相应类型的模板。
|
||||
- TemplateSelect 可传入 `'imageIterate'` 并正确加载列表。
|
||||
- 图像工作区
|
||||
- 首次进入时,若无保存记录,自动选择一个 `imageIterate` 默认模板。
|
||||
- 点击“继续优化”(图像模式)能直接弹出迭代输入弹窗,不再提示“请选择迭代模板”。
|
||||
- 从图像工作区打开“管理模板”,能够跳转到正确的图像分类。
|
||||
|
||||
## 五、实施计划(Rollout Plan)
|
||||
1) Phase 1 — Core
|
||||
- 新增 `imageIterate` 类型(types/manager/proxy/static-loader/service)。
|
||||
- 重构默认模板目录,并新增 `image-iterate`(中/英)。
|
||||
- 更新 `default-templates/index.ts` 聚合导入。
|
||||
|
||||
2) Phase 2 — UI
|
||||
- TemplateManager 新增图像三类按钮与过滤。
|
||||
- TemplateSelect 支持 `'imageIterate'`。
|
||||
- i18n 新增相关文案。
|
||||
|
||||
3) Phase 3 — Image Workspace
|
||||
- 增加/恢复 `selectedIterateTemplate` 逻辑与存储。
|
||||
- 迭代调用传递所选模板 `id`;修复打开模板管理器类型。
|
||||
|
||||
4) Phase 4 — 验证
|
||||
- 编译 core/ui 包,确保类型与构建通过。
|
||||
- 验证模板列表、选择与迭代弹窗的端到端流程。
|
||||
- 回归基础/上下文模板管理无回归。
|
||||
|
||||
## 六、风险与对策
|
||||
- 类型遗漏:通过全局检索 `listTemplatesByType(...)` 与 `templateType` union 的使用点,保证全部覆盖;CI 构建验证。
|
||||
- 兼容性:旧数据若仅保存 `iterate`,恢复时提示用户切换至 `imageIterate`;必要时提供回退逻辑(仅临时)。
|
||||
- UI 对接:图像工作区与全局 TemplateManager 的控制方式需统一(建议通过 provide/inject 或复用 `useTemplateManager`)。
|
||||
|
||||
---
|
||||
如需补充更多内置模板(如“风格保持迭代”“细节增强迭代”等),可在 `image-optimize/iterate/` 下继续添加,并在 `index.ts` 中聚合导出。
|
||||
+8
-1
@@ -11,6 +11,13 @@
|
||||
# Google Gemini API 配置
|
||||
# VITE_GEMINI_API_KEY=your-gemini-api-key-here
|
||||
|
||||
# ===== 图像生成 API 密钥配置 =====
|
||||
# Gemini 图像生成复用 VITE_GEMINI_API_KEY(无需单独配置)
|
||||
# Seedream (Doubao) / 火山方舟 ARK API Key(任选其一变量,优先级从上到下)
|
||||
# VITE_SEEDREAM_API_KEY=your-seedream-api-key-here
|
||||
# VITE_ARK_API_KEY=your-ark-api-key-here
|
||||
# (也兼容进程环境变量 ARK_API_KEY,但推荐统一使用 VITE_ 前缀)
|
||||
|
||||
# DeepSeek API 配置
|
||||
# VITE_DEEPSEEK_API_KEY=sk-your-deepseek-api-key-here
|
||||
|
||||
@@ -129,4 +136,4 @@
|
||||
# 1. 只支持公开仓库,不支持私有仓库
|
||||
# 2. 如果不设置仓库配置,将使用默认的公开仓库 (linshenkx/prompt-optimizer)
|
||||
# 3. 开发环境现在默认支持更新测试,无需额外配置文件
|
||||
# 4. 更新日志将保存在用户数据目录的 logs/auto-updater.log 文件中
|
||||
# 4. 更新日志将保存在用户数据目录的 logs/auto-updater.log 文件中
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@google/genai": "^1.0.0",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"dexie": "^4.0.11",
|
||||
"diff": "^8.0.2",
|
||||
@@ -39,4 +40,4 @@
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
// 核心服务存储键
|
||||
export const CORE_SERVICE_KEYS = {
|
||||
MODELS: 'models', // 模型配置存储键
|
||||
IMAGE_MODELS: 'image-models', // 图像模型配置存储键
|
||||
USER_TEMPLATES: 'user-templates', // 用户模板存储键
|
||||
PROMPT_HISTORY: 'prompt_history', // 提示词历史记录存储键
|
||||
} as const
|
||||
@@ -36,12 +37,22 @@ export const TEMPLATE_SELECTION_KEYS = {
|
||||
CONTEXT_ITERATE_TEMPLATE: 'app:selected-context-iterate-template',
|
||||
} as const
|
||||
|
||||
// 图像模式选择相关
|
||||
export const IMAGE_MODE_KEYS = {
|
||||
SELECTED_TEXT_MODEL: 'app:image-mode:selected-text-model',
|
||||
SELECTED_IMAGE_MODEL: 'app:image-mode:selected-image-model',
|
||||
SELECTED_TEMPLATE: 'app:image-mode:selected-template',
|
||||
SELECTED_ITERATE_TEMPLATE: 'app:image-mode:selected-iterate-template',
|
||||
COMPARE_MODE_ENABLED: 'app:image-mode:compare-mode-enabled',
|
||||
} as const
|
||||
|
||||
// 所有存储键的联合类型
|
||||
export const ALL_STORAGE_KEYS = {
|
||||
...CORE_SERVICE_KEYS,
|
||||
...UI_SETTINGS_KEYS,
|
||||
...MODEL_SELECTION_KEYS,
|
||||
...TEMPLATE_SELECTION_KEYS,
|
||||
...IMAGE_MODE_KEYS,
|
||||
} as const
|
||||
|
||||
// 导出所有键的数组(用于DataManager等需要遍历的场景)
|
||||
@@ -52,4 +63,5 @@ export type CoreServiceKey = typeof CORE_SERVICE_KEYS[keyof typeof CORE_SERVICE_
|
||||
export type UISettingsKey = typeof UI_SETTINGS_KEYS[keyof typeof UI_SETTINGS_KEYS]
|
||||
export type ModelSelectionKey = typeof MODEL_SELECTION_KEYS[keyof typeof MODEL_SELECTION_KEYS]
|
||||
export type TemplateSelectionKey = typeof TEMPLATE_SELECTION_KEYS[keyof typeof TEMPLATE_SELECTION_KEYS]
|
||||
export type ImageModeKey = typeof IMAGE_MODE_KEYS[keyof typeof IMAGE_MODE_KEYS]
|
||||
export type StorageKey = typeof ALL_STORAGE_KEYS[keyof typeof ALL_STORAGE_KEYS]
|
||||
@@ -23,6 +23,17 @@ export { LLMService, createLLMService } from './services/llm/service'
|
||||
export { ElectronLLMProxy } from './services/llm/electron-proxy'
|
||||
export * from './services/llm/errors'
|
||||
|
||||
// 导出图像服务相关类型
|
||||
export type {
|
||||
ImageRequest,
|
||||
ImageResult,
|
||||
ImageProgressHandlers,
|
||||
ImageModelConfig,
|
||||
IImageModelManager,
|
||||
IImageProviderAdapter,
|
||||
IImageAdapterRegistry
|
||||
} from './services/image/types'
|
||||
|
||||
// 导出模型管理相关
|
||||
export { ModelManager, createModelManager } from './services/model/manager'
|
||||
export * from './services/model/types'
|
||||
@@ -31,6 +42,13 @@ export * from './services/model/advancedParameterDefinitions'
|
||||
export { ElectronModelManagerProxy } from './services/model/electron-proxy'
|
||||
export { ElectronConfigManager, isElectronRenderer } from './services/model/electron-config'
|
||||
|
||||
// 导出图像模型管理与服务
|
||||
export { ImageModelManager, createImageModelManager } from './services/image-model/manager'
|
||||
export { ImageService, createImageService } from './services/image/service'
|
||||
export { ImageAdapterRegistry as _ImageAdapterRegistry, createImageAdapterRegistry } from './services/image/adapters/registry'
|
||||
export { ElectronImageServiceProxy, ElectronImageModelManagerProxy } from './services/image/electron-proxy'
|
||||
export * from './services/image/types'
|
||||
|
||||
// 导出存储相关
|
||||
export * from './services/storage/types'
|
||||
export { StorageFactory } from './services/storage/factory'
|
||||
@@ -95,6 +113,7 @@ export {
|
||||
UI_SETTINGS_KEYS,
|
||||
MODEL_SELECTION_KEYS,
|
||||
TEMPLATE_SELECTION_KEYS,
|
||||
IMAGE_MODE_KEYS,
|
||||
ALL_STORAGE_KEYS,
|
||||
ALL_STORAGE_KEYS_ARRAY
|
||||
} from './constants/storage-keys'
|
||||
@@ -103,6 +122,7 @@ export type {
|
||||
UISettingsKey,
|
||||
ModelSelectionKey,
|
||||
TemplateSelectionKey,
|
||||
ImageModeKey,
|
||||
StorageKey
|
||||
} from './constants/storage-keys'
|
||||
|
||||
@@ -113,4 +133,4 @@ export { ElectronContextRepoProxy } from './services/context/electron-proxy'
|
||||
export * from './services/context/constants'
|
||||
|
||||
// 导出高级模块相关类型
|
||||
export * from './types/advanced'
|
||||
export * from './types/advanced'
|
||||
@@ -3,7 +3,7 @@ import type { OptimizationMode } from '../prompt/types';
|
||||
/**
|
||||
* 提示词记录类型
|
||||
*/
|
||||
export type PromptRecordType = 'optimize' | 'userOptimize' | 'iterate' | 'test' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate';
|
||||
export type PromptRecordType = 'optimize' | 'userOptimize' | 'iterate' | 'test' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate' | 'imageOptimize' | 'contextImageOptimize' | 'imageIterate';
|
||||
|
||||
/**
|
||||
* 提示词记录接口
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ImageModelConfig } from '../image/types'
|
||||
import { getEnvVar } from '../../utils/environment'
|
||||
|
||||
/**
|
||||
* 图像模型静态默认配置
|
||||
* - Gemini 图像:使用 imagen-3.0 模型系列
|
||||
* - Seedream:火山方舟 Doubao 图像生成模型
|
||||
* - SiliconFlow:使用 Kwai-Kolors/Kolors 模型
|
||||
* - OpenAI:DALL-E 系列模型
|
||||
*/
|
||||
export function getDefaultImageModels(): Record<string, ImageModelConfig> {
|
||||
const GEMINI_API_KEY = getEnvVar('VITE_GEMINI_API_KEY').trim()
|
||||
const SEEDREAM_API_KEY = (
|
||||
getEnvVar('VITE_SEEDREAM_API_KEY') ||
|
||||
getEnvVar('VITE_ARK_API_KEY') ||
|
||||
(typeof process !== 'undefined' ? (process.env.ARK_API_KEY || '') : '')
|
||||
).trim()
|
||||
const OPENAI_API_KEY = getEnvVar('VITE_OPENAI_API_KEY').trim()
|
||||
const OPENROUTER_API_KEY = getEnvVar('VITE_OPENROUTER_API_KEY').trim()
|
||||
const SILICONFLOW_API_KEY = getEnvVar('VITE_SILICONFLOW_API_KEY').trim()
|
||||
|
||||
const models: Record<string, ImageModelConfig> = {
|
||||
'image-gemini-fast': {
|
||||
name: 'Gemini 2.5 Flash Image',
|
||||
baseURL: undefined, // Gemini使用SDK,不需要baseURL
|
||||
defaultModel: 'gemini-2.5-flash-image-preview',
|
||||
apiKey: GEMINI_API_KEY,
|
||||
enabled: !!GEMINI_API_KEY,
|
||||
provider: 'gemini',
|
||||
imgParams: {
|
||||
outputMimeType: 'image/png'
|
||||
},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
},
|
||||
'image-seedream': {
|
||||
name: 'Doubao Seedream 4.0',
|
||||
baseURL: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
defaultModel: 'doubao-seedream-4-0-250828',
|
||||
apiKey: SEEDREAM_API_KEY,
|
||||
enabled: !!SEEDREAM_API_KEY,
|
||||
provider: 'seedream',
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
watermark: true,
|
||||
outputMimeType: 'image/png'
|
||||
},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
},
|
||||
'image-siliconflow': {
|
||||
name: 'SiliconFlow Kolors',
|
||||
baseURL: 'https://api.siliconflow.cn/v1',
|
||||
defaultModel: 'Kwai-Kolors/Kolors',
|
||||
apiKey: SILICONFLOW_API_KEY,
|
||||
enabled: !!SILICONFLOW_API_KEY,
|
||||
provider: 'siliconflow',
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
steps: 20,
|
||||
guidance: 7.5,
|
||||
outputMimeType: 'image/png'
|
||||
},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
},
|
||||
'image-openrouter-gemini': {
|
||||
name: 'OpenRouter Gemini 2.5 Flash Image',
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
defaultModel: 'google/gemini-2.5-flash-image-preview',
|
||||
apiKey: OPENROUTER_API_KEY,
|
||||
enabled: !!OPENROUTER_API_KEY,
|
||||
provider: 'openai',
|
||||
imgParams: {
|
||||
outputMimeType: 'image/png'
|
||||
},
|
||||
capabilities: { edit: true, multiImage: true, asyncJob: false, streamingPreview: false }
|
||||
},
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import { IImageModelManager, ImageModelConfig } from '../image/types'
|
||||
import { IStorageProvider } from '../storage/types'
|
||||
import { StorageAdapter } from '../storage/adapter'
|
||||
import { CORE_SERVICE_KEYS } from '../../constants/storage-keys'
|
||||
import { ImportExportError } from '../../interfaces/import-export'
|
||||
import { APIError } from '../llm/errors'
|
||||
import { getDefaultImageModels } from './defaults'
|
||||
|
||||
/**
|
||||
* 图像模型管理器:与文本模型管理器解耦,使用独立存储键
|
||||
*/
|
||||
export class ImageModelManager implements IImageModelManager {
|
||||
private readonly storageKey = CORE_SERVICE_KEYS.IMAGE_MODELS
|
||||
private readonly storage: IStorageProvider
|
||||
private initPromise: Promise<void>
|
||||
|
||||
constructor(storageProvider: IStorageProvider) {
|
||||
this.storage = new StorageAdapter(storageProvider)
|
||||
this.initPromise = this.init().catch(err => {
|
||||
console.error('[ImageModelManager] initialization failed:', err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
public async ensureInitialized(): Promise<void> {
|
||||
await this.initPromise
|
||||
}
|
||||
|
||||
public async isInitialized(): Promise<boolean> {
|
||||
const stored = await this.storage.getItem(this.storageKey)
|
||||
return !!stored
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
const storedRaw = await this.storage.getItem(this.storageKey)
|
||||
const defaults = getDefaultImageModels()
|
||||
|
||||
if (!storedRaw) {
|
||||
// 初次写入默认模型
|
||||
await this.storage.setItem(this.storageKey, JSON.stringify(defaults))
|
||||
return
|
||||
}
|
||||
|
||||
// 合并已有配置与默认配置(保留用户关键自定义字段)
|
||||
let needSave = false
|
||||
let existing: Record<string, ImageModelConfig>
|
||||
try {
|
||||
const parsed = JSON.parse(storedRaw)
|
||||
existing = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
existing = {}
|
||||
}
|
||||
|
||||
const merged: Record<string, ImageModelConfig> = { ...existing }
|
||||
for (const [key, defCfg] of Object.entries(defaults)) {
|
||||
if (!merged[key]) {
|
||||
merged[key] = defCfg
|
||||
needSave = true
|
||||
} else {
|
||||
const cur = merged[key]
|
||||
const updated: ImageModelConfig = {
|
||||
...defCfg,
|
||||
name: cur.name !== undefined ? cur.name : defCfg.name,
|
||||
baseURL: cur.baseURL || defCfg.baseURL,
|
||||
defaultModel: cur.defaultModel !== undefined ? cur.defaultModel : defCfg.defaultModel,
|
||||
apiKey: cur.apiKey || defCfg.apiKey,
|
||||
enabled: cur.enabled !== undefined ? cur.enabled : defCfg.enabled,
|
||||
imgParams: cur.imgParams || defCfg.imgParams,
|
||||
capabilities: cur.capabilities || defCfg.capabilities,
|
||||
provider: cur.provider || defCfg.provider
|
||||
}
|
||||
if (JSON.stringify(cur) !== JSON.stringify(updated)) {
|
||||
merged[key] = updated
|
||||
needSave = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needSave) {
|
||||
await this.storage.setItem(this.storageKey, JSON.stringify(merged))
|
||||
}
|
||||
}
|
||||
|
||||
// === 基本CRUD ===
|
||||
|
||||
async getAllModels(): Promise<Array<ImageModelConfig & { key: string }>> {
|
||||
await this.ensureInitialized()
|
||||
const raw = await this.storage.getItem(this.storageKey)
|
||||
const data: Record<string, ImageModelConfig> = raw ? JSON.parse(raw) : {}
|
||||
return Object.entries(data).map(([key, cfg]) => ({ ...cfg, key }))
|
||||
}
|
||||
|
||||
async getModel(key: string): Promise<ImageModelConfig | undefined> {
|
||||
await this.ensureInitialized()
|
||||
const raw = await this.storage.getItem(this.storageKey)
|
||||
const data: Record<string, ImageModelConfig> = raw ? JSON.parse(raw) : {}
|
||||
return data[key]
|
||||
}
|
||||
|
||||
async addModel(key: string, config: ImageModelConfig): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
this.validateConfig(config)
|
||||
await this.storage.updateData<Record<string, ImageModelConfig>>(
|
||||
this.storageKey,
|
||||
(current) => {
|
||||
const data = current || {}
|
||||
if (data[key]) {
|
||||
throw new APIError(`Image model ${key} already exists`)
|
||||
}
|
||||
return { ...data, [key]: { ...config, ...(config.imgParams && { imgParams: { ...config.imgParams } }) } }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateModel(key: string, config: Partial<ImageModelConfig>): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
await this.storage.updateData<Record<string, ImageModelConfig>>(
|
||||
this.storageKey,
|
||||
(current) => {
|
||||
const data = current || {}
|
||||
if (!data[key]) {
|
||||
throw new APIError(`Image model ${key} does not exist`)
|
||||
}
|
||||
const updated: ImageModelConfig = {
|
||||
...data[key],
|
||||
...config,
|
||||
enabled: config.enabled !== undefined ? config.enabled : data[key].enabled,
|
||||
...(config.imgParams && { imgParams: { ...config.imgParams } })
|
||||
}
|
||||
this.validateConfig(updated)
|
||||
return { ...data, [key]: updated }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async deleteModel(key: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
await this.storage.updateData<Record<string, ImageModelConfig>>(
|
||||
this.storageKey,
|
||||
(current) => {
|
||||
const data = current || {}
|
||||
if (!data[key]) {
|
||||
throw new APIError(`Image model ${key} does not exist`)
|
||||
}
|
||||
const { [key]: removed, ...rest } = data
|
||||
return rest
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async enableModel(key: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
await this.storage.updateData<Record<string, ImageModelConfig>>(
|
||||
this.storageKey,
|
||||
(current) => {
|
||||
const data = current || {}
|
||||
if (!data[key]) throw new APIError(`Unknown image model: ${key}`)
|
||||
const cfg = { ...data[key], enabled: true }
|
||||
this.validateEnableConfig(cfg)
|
||||
return { ...data, [key]: cfg }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async disableModel(key: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
await this.storage.updateData<Record<string, ImageModelConfig>>(
|
||||
this.storageKey,
|
||||
(current) => {
|
||||
const data = current || {}
|
||||
if (!data[key]) throw new APIError(`Unknown image model: ${key}`)
|
||||
return { ...data, [key]: { ...data[key], enabled: false } }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async getEnabledModels(): Promise<Array<ImageModelConfig & { key: string }>> {
|
||||
const all = await this.getAllModels()
|
||||
return all.filter(m => m.enabled)
|
||||
}
|
||||
|
||||
// === 导入导出 ===
|
||||
|
||||
async exportData(): Promise<ImageModelConfig[]> {
|
||||
try {
|
||||
return await this.getAllModels()
|
||||
} catch (error) {
|
||||
throw new ImportExportError('Failed to export image model data', await this.getDataType(), error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
async importData(data: any): Promise<void> {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid image model data format: data must be an array')
|
||||
}
|
||||
const models = data as (ImageModelConfig & { key: string })[]
|
||||
const failed: { model: ImageModelConfig & { key: string }, error: Error }[] = []
|
||||
for (const m of models) {
|
||||
try {
|
||||
if (!this.validateSingleModel(m)) {
|
||||
failed.push({ model: m, error: new Error('Invalid image model configuration') })
|
||||
continue
|
||||
}
|
||||
const existing = await this.getModel(m.key)
|
||||
if (existing) {
|
||||
const merged: ImageModelConfig = {
|
||||
name: m.name,
|
||||
baseURL: m.baseURL ?? existing.baseURL,
|
||||
defaultModel: m.defaultModel ?? existing.defaultModel,
|
||||
provider: m.provider ?? existing.provider,
|
||||
enabled: m.enabled ?? existing.enabled,
|
||||
...(m.apiKey !== undefined && { apiKey: m.apiKey }),
|
||||
...(m.useVercelProxy !== undefined && { useVercelProxy: m.useVercelProxy }),
|
||||
...(m.useDockerProxy !== undefined && { useDockerProxy: m.useDockerProxy }),
|
||||
...(m.imgParams !== undefined && { imgParams: m.imgParams })
|
||||
}
|
||||
await this.updateModel(m.key, merged)
|
||||
} else {
|
||||
const newCfg: ImageModelConfig = {
|
||||
name: m.name,
|
||||
baseURL: m.baseURL,
|
||||
defaultModel: m.defaultModel,
|
||||
provider: m.provider || 'custom',
|
||||
enabled: m.enabled ?? false,
|
||||
...(m.apiKey !== undefined && { apiKey: m.apiKey }),
|
||||
...(m.useVercelProxy !== undefined && { useVercelProxy: m.useVercelProxy }),
|
||||
...(m.useDockerProxy !== undefined && { useDockerProxy: m.useDockerProxy }),
|
||||
...(m.imgParams !== undefined && { imgParams: m.imgParams })
|
||||
}
|
||||
await this.addModel(m.key, newCfg)
|
||||
}
|
||||
} catch (e) {
|
||||
failed.push({ model: m, error: e as Error })
|
||||
}
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
console.warn(`[ImageModelManager] Failed to import ${failed.length} image models`)
|
||||
}
|
||||
}
|
||||
|
||||
async getDataType(): Promise<string> { return 'image-models' }
|
||||
|
||||
async validateData(data: any): Promise<boolean> {
|
||||
return Array.isArray(data) && data.every(item => this.validateSingleModel(item))
|
||||
}
|
||||
|
||||
// === 校验 ===
|
||||
|
||||
private validateConfig(cfg: ImageModelConfig): void {
|
||||
const errors: string[] = []
|
||||
if (!cfg.name) errors.push('Missing model name (name)')
|
||||
if (!cfg.defaultModel) errors.push('Missing default model (defaultModel)')
|
||||
if (!cfg.provider) errors.push('Missing provider (provider)')
|
||||
// imgParams 允许为任意对象
|
||||
if (cfg.imgParams !== undefined && (typeof cfg.imgParams !== 'object' || cfg.imgParams === null || Array.isArray(cfg.imgParams))) {
|
||||
errors.push('imgParams must be an object')
|
||||
}
|
||||
if (errors.length) {
|
||||
throw new APIError('Invalid image model configuration: ' + errors.join(', '))
|
||||
}
|
||||
}
|
||||
|
||||
private validateEnableConfig(cfg: ImageModelConfig): void {
|
||||
this.validateConfig(cfg)
|
||||
}
|
||||
|
||||
private validateSingleModel(item: any): boolean {
|
||||
return typeof item === 'object' && item !== null &&
|
||||
typeof item.key === 'string' &&
|
||||
typeof item.name === 'string' &&
|
||||
typeof item.defaultModel === 'string' &&
|
||||
typeof item.enabled === 'boolean' &&
|
||||
typeof item.provider === 'string'
|
||||
}
|
||||
}
|
||||
|
||||
export function createImageModelManager(storageProvider: IStorageProvider): ImageModelManager {
|
||||
return new ImageModelManager(storageProvider)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 与官方文档对齐:使用 @google/genai 的 GoogleGenAI 并通过 ai.models.generateContent 调用
|
||||
import { GoogleGenAI } from '@google/genai'
|
||||
import { IImageProviderAdapter, ImageModelConfig, ImageProgressHandlers, ImageRequest, ImageResult } from '../../image/types'
|
||||
import { APIError, RequestConfigError } from '../../llm/errors'
|
||||
|
||||
export class GeminiImageAdapter implements IImageProviderAdapter {
|
||||
public readonly capabilities = {
|
||||
edit: true,
|
||||
multiImage: false,
|
||||
asyncJob: false,
|
||||
streamingPreview: false
|
||||
}
|
||||
|
||||
async generate(request: ImageRequest, config: ImageModelConfig, handlers?: ImageProgressHandlers): Promise<ImageResult> {
|
||||
if (!config.apiKey) throw new RequestConfigError('Gemini: 缺少 API Key')
|
||||
if (!config.defaultModel) throw new RequestConfigError('Gemini: 缺少默认模型')
|
||||
// 默认按 gemini-2.5-flash-image-preview 的逻辑调用;
|
||||
// 非该模型也放行(可能由 Google 后续新增兼容),但给出提示便于排查兼容性。
|
||||
if (config.defaultModel !== 'gemini-2.5-flash-image-preview') {
|
||||
console.warn('[GeminiImageAdapter] 非推荐模型,仍按 2.5 flash image 预览逻辑调用:', config.defaultModel)
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey: config.apiKey })
|
||||
|
||||
const outputMime: 'image/png' | 'image/jpeg' = (request.imgParams?.outputMimeType as any) || 'image/png'
|
||||
|
||||
// 构造 contents(与官方文档一致):
|
||||
// - 文生图:contents: string
|
||||
// - 图生图:contents: [{ text }, { inlineData: { mimeType, data } }]
|
||||
const contents: any = request.inputImage?.b64
|
||||
? [
|
||||
{ text: request.prompt },
|
||||
{ inlineData: { mimeType: request.inputImage.mimeType || outputMime, data: request.inputImage.b64 } }
|
||||
]
|
||||
: request.prompt
|
||||
|
||||
try {
|
||||
handlers?.onProgress?.('generating')
|
||||
|
||||
const res = await ai.models.generateContent({
|
||||
model: config.defaultModel,
|
||||
contents
|
||||
})
|
||||
|
||||
// 检查是否被安全过滤阻止
|
||||
const response = (res as any)
|
||||
if (response?.candidates?.[0]?.finishReason === 'SAFETY') {
|
||||
throw new APIError('Gemini: 内容被安全过滤器阻止,请调整提示词')
|
||||
}
|
||||
|
||||
const parts: any[] = response?.candidates?.[0]?.content?.parts || []
|
||||
|
||||
const images: { b64: string; mimeType?: string }[] = []
|
||||
const notes: string[] = []
|
||||
|
||||
for (const p of parts) {
|
||||
if (p?.inlineData?.data) {
|
||||
images.push({
|
||||
b64: p.inlineData.data,
|
||||
mimeType: p.inlineData.mimeType || outputMime
|
||||
})
|
||||
} else if (typeof p?.text === 'string' && p.text.trim()) {
|
||||
notes.push(p.text.trim())
|
||||
}
|
||||
}
|
||||
|
||||
if (images.length === 0) {
|
||||
// 提供更详细的错误信息
|
||||
const finishReason = response?.candidates?.[0]?.finishReason
|
||||
const errorMsg = finishReason
|
||||
? `未返回图片数据,结束原因: ${finishReason}`
|
||||
: '未返回图片数据'
|
||||
throw new APIError(`Gemini: ${errorMsg}`)
|
||||
}
|
||||
|
||||
const result: ImageResult = {
|
||||
images,
|
||||
metadata: {
|
||||
model: config.defaultModel,
|
||||
notes: notes.length ? notes : undefined,
|
||||
finishReason: response?.candidates?.[0]?.finishReason
|
||||
}
|
||||
}
|
||||
handlers?.onComplete?.(result)
|
||||
return result
|
||||
} catch (err: any) {
|
||||
const message = err?.message || String(err)
|
||||
handlers?.onError?.(err instanceof Error ? err : new Error(message))
|
||||
|
||||
// 提供更友好的错误信息
|
||||
if (message.includes('API_KEY')) {
|
||||
throw new APIError('Gemini: API Key 无效或已过期')
|
||||
} else if (message.includes('SAFETY')) {
|
||||
throw new APIError('Gemini: 内容被安全过滤器阻止')
|
||||
} else if (message.includes('QUOTA')) {
|
||||
throw new APIError('Gemini: API 配额已用尽')
|
||||
}
|
||||
|
||||
throw new APIError(`Gemini 调用失败: ${message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import OpenAI from 'openai'
|
||||
import { IImageProviderAdapter, ImageModelConfig, ImageProgressHandlers, ImageRequest, ImageResult } from '../../image/types'
|
||||
import { APIError, RequestConfigError } from '../../llm/errors'
|
||||
|
||||
export class OpenAIImageAdapter implements IImageProviderAdapter {
|
||||
public readonly capabilities = {
|
||||
edit: true, // 支持图生图编辑(文本+图片输入)
|
||||
multiImage: true, // 支持多图像输入
|
||||
asyncJob: false,
|
||||
streamingPreview: false
|
||||
}
|
||||
|
||||
async generate(request: ImageRequest, config: ImageModelConfig, handlers?: ImageProgressHandlers): Promise<ImageResult> {
|
||||
if (!config.apiKey) throw new RequestConfigError('OpenAI: 缺少 API Key')
|
||||
if (!config.defaultModel) throw new RequestConfigError('OpenAI: 缺少默认模型')
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseURL,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
|
||||
try {
|
||||
handlers?.onProgress?.('generating')
|
||||
|
||||
// 构建消息内容
|
||||
const messageContent: any[] = []
|
||||
|
||||
// 添加文本提示
|
||||
if (request.prompt) {
|
||||
messageContent.push({
|
||||
type: "text",
|
||||
text: request.prompt
|
||||
})
|
||||
}
|
||||
|
||||
// 添加输入图像(如果有)
|
||||
if (request.inputImage?.url) {
|
||||
messageContent.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: request.inputImage.url
|
||||
}
|
||||
})
|
||||
} else if (request.inputImage?.b64) {
|
||||
// 如果是 base64 图像,需要转换为 data URL
|
||||
const mimeType = request.inputImage.mimeType || 'image/png'
|
||||
const dataUrl = `data:${mimeType};base64,${request.inputImage.b64}`
|
||||
messageContent.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: dataUrl
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`[OpenAIImageAdapter] 使用 Chat Completions API,模型: ${config.defaultModel}`)
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model: config.defaultModel,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: messageContent
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 从响应中提取图像
|
||||
const choice = response.choices?.[0]
|
||||
const images: { b64: string; mimeType?: string }[] = []
|
||||
|
||||
// 检查是否有图像返回(使用类型断言处理扩展的响应格式)
|
||||
const message = choice?.message as any
|
||||
if (message?.images) {
|
||||
for (const img of message.images) {
|
||||
if (img.type === 'image_url' && img.image_url?.url) {
|
||||
const url = img.image_url.url
|
||||
// 处理 data URL 格式
|
||||
if (url.startsWith('data:')) {
|
||||
const match = url.match(/^data:([^;]+);base64,(.+)$/)
|
||||
if (match) {
|
||||
images.push({
|
||||
b64: match[2],
|
||||
mimeType: match[1]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: ImageResult = {
|
||||
images,
|
||||
metadata: {
|
||||
model: config.defaultModel,
|
||||
originalPrompt: request.prompt,
|
||||
usage: response.usage ? {
|
||||
prompt_tokens: response.usage.prompt_tokens,
|
||||
completion_tokens: response.usage.completion_tokens,
|
||||
total_tokens: response.usage.total_tokens
|
||||
} : undefined
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[OpenAIImageAdapter] 成功生成 ${result.images.length} 张图片`)
|
||||
handlers?.onComplete?.(result)
|
||||
return result
|
||||
|
||||
} catch (err: any) {
|
||||
handlers?.onError?.(err instanceof Error ? err : new Error(err?.message || String(err)))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IImageAdapterRegistry, IImageProviderAdapter } from '../../image/types'
|
||||
import { RequestConfigError } from '../../llm/errors'
|
||||
import { GeminiImageAdapter } from './gemini'
|
||||
import { SeedreamImageAdapter } from './seedream'
|
||||
import { OpenAIImageAdapter } from './openai'
|
||||
import { SiliconFlowImageAdapter } from './siliconflow-adapter'
|
||||
|
||||
/**
|
||||
* 简单注册表:按 provider 返回具体适配器实例
|
||||
*/
|
||||
export class ImageAdapterRegistry implements IImageAdapterRegistry {
|
||||
getAdapter(provider: string): IImageProviderAdapter {
|
||||
switch ((provider || '').toLowerCase()) {
|
||||
case 'gemini':
|
||||
return new GeminiImageAdapter()
|
||||
case 'seedream':
|
||||
case 'doubao':
|
||||
case 'volcengine':
|
||||
return new SeedreamImageAdapter()
|
||||
case 'siliconflow':
|
||||
case 'silicon-flow':
|
||||
case 'kolors':
|
||||
return new SiliconFlowImageAdapter()
|
||||
case 'openai':
|
||||
return new OpenAIImageAdapter()
|
||||
case 'custom':
|
||||
throw new RequestConfigError('自定义图像提供商暂未实现适配器')
|
||||
default:
|
||||
throw new RequestConfigError(`未知图像提供商: ${provider}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createImageAdapterRegistry = () => new ImageAdapterRegistry()
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { IImageProviderAdapter, ImageModelConfig, ImageProgressHandlers, ImageRequest, ImageResult } from '../../image/types'
|
||||
import { APIError, RequestConfigError } from '../../llm/errors'
|
||||
import { getProxyUrl, isDocker, isVercel } from '../../../utils/environment'
|
||||
|
||||
export class SeedreamImageAdapter implements IImageProviderAdapter {
|
||||
public readonly capabilities = {
|
||||
edit: true,
|
||||
multiImage: false,
|
||||
asyncJob: false,
|
||||
streamingPreview: false
|
||||
}
|
||||
|
||||
async generate(request: ImageRequest, config: ImageModelConfig, handlers?: ImageProgressHandlers): Promise<ImageResult> {
|
||||
if (!config.apiKey) throw new RequestConfigError('Seedream: 缺少 API Key')
|
||||
if (!config.defaultModel) throw new RequestConfigError('Seedream: 缺少默认模型')
|
||||
|
||||
// 火山方舟图像生成 API 端点
|
||||
const base = config.baseURL || 'https://ark.cn-beijing.volces.com/api/v3'
|
||||
const url = `${base.replace(/\/$/, '')}/images/generations`
|
||||
|
||||
// 构建请求体
|
||||
const payload: any = {
|
||||
model: config.defaultModel, // 例如: doubao-seedream-4-0-250828
|
||||
prompt: request.prompt,
|
||||
response_format: 'b64_json', // 固定使用 base64 格式
|
||||
stream: false,
|
||||
watermark: false
|
||||
}
|
||||
|
||||
// 图生图支持:仅支持单图输入
|
||||
if (request.inputImage?.b64) {
|
||||
const mime = (request.inputImage.mimeType || request.imgParams?.outputMimeType || 'image/png').toLowerCase()
|
||||
// 火山方舟要求使用 data URI 格式
|
||||
payload.image = `data:${mime};base64,${request.inputImage.b64}`
|
||||
// 图生图时禁用序列生成
|
||||
payload.sequential_image_generation = 'disabled'
|
||||
} else if (request.inputImage?.url) {
|
||||
// 虽然规范要求"仅本地文件",此处兼容 URL(如后续 UI 开启 URL 输入)
|
||||
payload.image = request.inputImage.url
|
||||
payload.sequential_image_generation = 'disabled'
|
||||
}
|
||||
|
||||
// 尺寸参数 - 火山方舟支持的尺寸格式
|
||||
if (request.imgParams?.size) {
|
||||
// 支持如 "1024x1024", "1K", "2K" 等格式
|
||||
payload.size = request.imgParams.size
|
||||
} else if (request.imgParams?.width && request.imgParams?.height) {
|
||||
// 转换为火山方舟支持的尺寸格式
|
||||
payload.size = `${request.imgParams.width}x${request.imgParams.height}`
|
||||
}
|
||||
|
||||
// 生成数量 - 确保符合API限制
|
||||
if (request.count && request.count > 1) {
|
||||
payload.n = Math.min(request.count, 4) // 限制最多4张
|
||||
}
|
||||
|
||||
// 高级参数 - 根据模型能力添加
|
||||
// 4.0 不支持 seed/guidance_scale,仅 3.0/seededit 支持
|
||||
const modelId = (config.defaultModel || '').toLowerCase()
|
||||
const supportsSeed = modelId.includes('seedream-3.0') || modelId.includes('seededit-3.0')
|
||||
if (supportsSeed) {
|
||||
if (typeof request.imgParams?.seed === 'number') {
|
||||
payload.seed = request.imgParams.seed
|
||||
}
|
||||
if (typeof request.imgParams?.guidance_scale === 'number') {
|
||||
payload.guidance_scale = Math.max(1.0, Math.min(20.0, request.imgParams.guidance_scale))
|
||||
}
|
||||
}
|
||||
|
||||
// 质量设置
|
||||
if (request.imgParams?.quality) {
|
||||
payload.quality = request.imgParams.quality
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`
|
||||
}
|
||||
|
||||
// 添加用户代理
|
||||
if (typeof window === 'undefined') {
|
||||
headers['User-Agent'] = 'Prompt-Optimizer/1.0'
|
||||
}
|
||||
|
||||
try {
|
||||
handlers?.onProgress?.('generating')
|
||||
|
||||
// 代理处理(与现有策略一致)
|
||||
let requestUrl = url
|
||||
if (config.useVercelProxy === true && isVercel()) {
|
||||
requestUrl = getProxyUrl(url, false)
|
||||
} else if (config.useDockerProxy === true && isDocker()) {
|
||||
requestUrl = getProxyUrl(url, false)
|
||||
}
|
||||
|
||||
console.log(`[SeedreamImageAdapter] 请求 ${requestUrl} 使用模型 ${config.defaultModel}`)
|
||||
|
||||
const resp = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
// 设置合理的超时时间 (图像生成通常较慢)
|
||||
signal: AbortSignal.timeout(120000) // 2分钟超时
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
let errorMessage: string
|
||||
try {
|
||||
const errorData = await resp.json()
|
||||
errorMessage = errorData?.error?.message || errorData?.message || resp.statusText
|
||||
} catch {
|
||||
errorMessage = await resp.text().catch(() => resp.statusText)
|
||||
}
|
||||
|
||||
// 处理常见错误
|
||||
if (resp.status === 401) {
|
||||
throw new APIError('Seedream: API Key 无效或已过期')
|
||||
} else if (resp.status === 429) {
|
||||
throw new APIError('Seedream: 请求频率过高,请稍后重试')
|
||||
} else if (resp.status === 400) {
|
||||
throw new APIError(`Seedream: 请求参数无效 - ${errorMessage}`)
|
||||
} else if (resp.status >= 500) {
|
||||
throw new APIError(`Seedream: 服务器错误 (${resp.status}) - ${errorMessage}`)
|
||||
}
|
||||
|
||||
throw new APIError(`Seedream HTTP ${resp.status}: ${errorMessage}`)
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
|
||||
// 解析响应数据
|
||||
const list: any[] = Array.isArray(data?.data) ? data.data : []
|
||||
if (list.length === 0 && data?.data) {
|
||||
// 如果不是数组,尝试作为单个对象处理
|
||||
list.push(data.data)
|
||||
}
|
||||
|
||||
const images: { b64: string; mimeType?: string }[] = []
|
||||
const notes: string[] = []
|
||||
|
||||
for (const item of list) {
|
||||
if (item?.b64_json) {
|
||||
images.push({
|
||||
b64: item.b64_json,
|
||||
mimeType: 'image/png' // 火山方舟通常返回PNG格式
|
||||
})
|
||||
} else if (item?.url) {
|
||||
// 如果返回URL,需要额外处理转换为base64
|
||||
notes.push('警告: 返回了URL格式,需要额外处理转换为base64')
|
||||
} else if (item?.error) {
|
||||
const errorMsg = item.error?.message || item.error?.code || 'Unknown item error'
|
||||
notes.push(`生成失败: ${errorMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有成功的图片生成
|
||||
if (images.length === 0) {
|
||||
const errorMsg = notes.length > 0
|
||||
? `生成失败: ${notes.join('; ')}`
|
||||
: '未返回图片数据'
|
||||
throw new APIError(`Seedream: ${errorMsg}`)
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
const result: ImageResult = {
|
||||
images,
|
||||
metadata: {
|
||||
model: config.defaultModel,
|
||||
notes: notes.length ? notes : undefined,
|
||||
usage: data?.usage, // 包含用量信息
|
||||
created: data?.created
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SeedreamImageAdapter] 成功生成 ${images.length} 张图片`)
|
||||
handlers?.onComplete?.(result)
|
||||
return result
|
||||
|
||||
} catch (err: any) {
|
||||
const message = err?.message || String(err)
|
||||
handlers?.onError?.(err instanceof Error ? err : new Error(message))
|
||||
|
||||
// 提供更友好的错误信息
|
||||
if (message.includes('timeout') || message.includes('TIMEOUT')) {
|
||||
throw new APIError('Seedream: 生成超时,请稍后重试')
|
||||
} else if (message.includes('network') || message.includes('fetch')) {
|
||||
throw new APIError('Seedream: 网络连接失败,请检查网络设置')
|
||||
}
|
||||
|
||||
throw new APIError(`Seedream 调用失败: ${message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { IImageProviderAdapter, ImageModelConfig, ImageProgressHandlers, ImageRequest, ImageResult } from '../../image/types'
|
||||
import { APIError, RequestConfigError } from '../../llm/errors'
|
||||
import { getProxyUrl, isDocker, isVercel } from '../../../utils/environment'
|
||||
|
||||
/**
|
||||
* SiliconFlow 图像生成适配器
|
||||
* 支持 Kwai-Kolors/Kolors 模型
|
||||
* API 文档: https://docs.siliconflow.cn/
|
||||
*/
|
||||
export class SiliconFlowImageAdapter implements IImageProviderAdapter {
|
||||
public readonly capabilities = {
|
||||
edit: true,
|
||||
multiImage: false,
|
||||
asyncJob: false,
|
||||
streamingPreview: false
|
||||
}
|
||||
|
||||
async generate(request: ImageRequest, config: ImageModelConfig, handlers?: ImageProgressHandlers): Promise<ImageResult> {
|
||||
if (!config.apiKey) throw new RequestConfigError('SiliconFlow: 缺少 API Key')
|
||||
if (!config.defaultModel) throw new RequestConfigError('SiliconFlow: 缺少默认模型')
|
||||
|
||||
// SiliconFlow 图像生成 API 端点
|
||||
const base = config.baseURL || 'https://api.siliconflow.cn/v1'
|
||||
const url = `${base.replace(/\/$/, '')}/images/generations`
|
||||
|
||||
// 构建请求体
|
||||
const body: any = {
|
||||
model: config.defaultModel || 'Kwai-Kolors/Kolors',
|
||||
prompt: request.prompt,
|
||||
image_size: this.mapImageSize(request.imgParams?.size),
|
||||
batch_size: Math.min(request.count || 1, 4), // 限制最大为4
|
||||
num_inference_steps: request.imgParams?.steps || 20,
|
||||
guidance_scale: request.imgParams?.guidance || 7.5
|
||||
}
|
||||
|
||||
// 添加可选参数
|
||||
if (request.imgParams?.negativePrompt) {
|
||||
body.negative_prompt = request.imgParams.negativePrompt
|
||||
}
|
||||
|
||||
if (request.imgParams?.seed) {
|
||||
body.seed = Math.min(request.imgParams.seed, 9999999999)
|
||||
}
|
||||
|
||||
// 如果有输入图像(用于图像编辑)
|
||||
if (request.imgParams?.image) {
|
||||
body.image = request.imgParams.image
|
||||
}
|
||||
|
||||
// 构建代理 URL(如果需要)
|
||||
const targetUrl = isVercel() || isDocker()
|
||||
? getProxyUrl(url, 'SILICONFLOW')
|
||||
: url
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
|
||||
try {
|
||||
handlers?.onProgress?.({ phase: 'request', progress: 10 })
|
||||
|
||||
const response = await fetch(targetUrl, requestOptions)
|
||||
|
||||
handlers?.onProgress?.({ phase: 'response', progress: 50 })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text()
|
||||
throw new APIError(
|
||||
`SiliconFlow API 错误 (${response.status}): ${errorData}`,
|
||||
response.status,
|
||||
'siliconflow'
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
handlers?.onProgress?.({ phase: 'processing', progress: 90 })
|
||||
|
||||
// 转换响应格式
|
||||
const images = data.images?.map((img: any, index: number) => ({
|
||||
url: img.url,
|
||||
b64_json: undefined, // SiliconFlow 返回 URL,不是 base64
|
||||
revised_prompt: undefined // SiliconFlow 不返回修订后的提示词
|
||||
})) || []
|
||||
|
||||
const result: ImageResult = {
|
||||
images: images.map((img: any) => ({
|
||||
url: img.url,
|
||||
mimeType: 'image/png'
|
||||
})),
|
||||
metadata: {
|
||||
model: config.defaultModel,
|
||||
created: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加额外信息
|
||||
if (data.timings?.inference) {
|
||||
result.metadata!.usage = {
|
||||
inference_time: data.timings.inference
|
||||
}
|
||||
}
|
||||
|
||||
if (data.seed) {
|
||||
result.metadata!.seed = data.seed
|
||||
}
|
||||
|
||||
handlers?.onProgress?.({ phase: 'complete', progress: 100 })
|
||||
handlers?.onComplete?.(result)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error: any) {
|
||||
handlers?.onError?.(error)
|
||||
|
||||
if (error instanceof APIError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
throw new RequestConfigError(`SiliconFlow: 网络连接失败,请检查网络连接或 API 地址: ${error.message}`)
|
||||
}
|
||||
|
||||
throw new APIError(`SiliconFlow: 请求失败: ${error.message}`, 500, 'siliconflow')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将标准尺寸映射到 SiliconFlow 支持的格式
|
||||
*/
|
||||
private mapImageSize(size?: string): string {
|
||||
if (!size) return '1024x1024'
|
||||
|
||||
// 处理不同的尺寸格式
|
||||
const sizeMap: Record<string, string> = {
|
||||
// 标准格式
|
||||
'1024x1024': '1024x1024',
|
||||
'960x1280': '960x1280',
|
||||
'768x1024': '768x1024',
|
||||
'720x1440': '720x1440',
|
||||
'720x1280': '720x1280',
|
||||
|
||||
// 兼容性别名
|
||||
'square': '1024x1024',
|
||||
'portrait': '768x1024',
|
||||
'landscape': '1280x720',
|
||||
'1:1': '1024x1024',
|
||||
'3:4': '768x1024',
|
||||
'4:3': '1024x768',
|
||||
'16:9': '1280x720',
|
||||
'9:16': '720x1280',
|
||||
'1:2': '720x1440'
|
||||
}
|
||||
|
||||
return sizeMap[size] || '1024x1024'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的图像尺寸
|
||||
*/
|
||||
getSupportedSizes(): string[] {
|
||||
return [
|
||||
'1024x1024',
|
||||
'960x1280',
|
||||
'768x1024',
|
||||
'720x1440',
|
||||
'720x1280'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否有效
|
||||
*/
|
||||
async validateConfig(config: ImageModelConfig): Promise<boolean> {
|
||||
try {
|
||||
// 构建测试请求但不实际发送
|
||||
const base = config.baseURL || 'https://api.siliconflow.cn/v1'
|
||||
const url = `${base.replace(/\/$/, '')}/images/generations`
|
||||
|
||||
const testBody = {
|
||||
model: config.defaultModel || 'Kwai-Kolors/Kolors',
|
||||
prompt: 'test',
|
||||
image_size: '1024x1024',
|
||||
batch_size: 1
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(testBody)
|
||||
})
|
||||
|
||||
return response.ok || response.status === 400 // 400 可能是参数错误,但认证通过了
|
||||
|
||||
} catch (error) {
|
||||
console.warn('SiliconFlow config validation failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { IImageModelManager, ImageRequest, ImageResult } from './types'
|
||||
|
||||
export class ElectronImageServiceProxy {
|
||||
private electronAPI: any
|
||||
|
||||
constructor() {
|
||||
if (typeof window === 'undefined' || !window.electronAPI) {
|
||||
throw new Error('ElectronImageServiceProxy can only be used in Electron renderer process')
|
||||
}
|
||||
this.electronAPI = window.electronAPI
|
||||
}
|
||||
|
||||
async generate(request: ImageRequest, modelKey: string): Promise<ImageResult> {
|
||||
const safeReq = JSON.parse(JSON.stringify(request))
|
||||
return await (this.electronAPI as any).image.generate(safeReq, modelKey)
|
||||
}
|
||||
}
|
||||
|
||||
export class ElectronImageModelManagerProxy implements IImageModelManager {
|
||||
private electronAPI: any
|
||||
|
||||
constructor() {
|
||||
if (typeof window === 'undefined' || !window.electronAPI) {
|
||||
throw new Error('ElectronImageModelManagerProxy can only be used in Electron renderer process')
|
||||
}
|
||||
this.electronAPI = window.electronAPI
|
||||
}
|
||||
|
||||
async ensureInitialized(): Promise<void> { await (this.electronAPI as any).imageModel.ensureInitialized() }
|
||||
async isInitialized(): Promise<boolean> { return await (this.electronAPI as any).imageModel.isInitialized() }
|
||||
async getAllModels(): Promise<any[]> { return await (this.electronAPI as any).imageModel.getAllModels() }
|
||||
async getModel(key: string): Promise<any> { return await (this.electronAPI as any).imageModel.getModel(key) }
|
||||
async addModel(key: string, config: any): Promise<void> { await (this.electronAPI as any).imageModel.addModel({ key, ...config }) }
|
||||
async updateModel(key: string, config: any): Promise<void> { await (this.electronAPI as any).imageModel.updateModel(key, config) }
|
||||
async deleteModel(key: string): Promise<void> { await (this.electronAPI as any).imageModel.deleteModel(key) }
|
||||
async enableModel(key: string): Promise<void> { await (this.electronAPI as any).imageModel.enableModel(key) }
|
||||
async disableModel(key: string): Promise<void> { await (this.electronAPI as any).imageModel.disableModel(key) }
|
||||
async getEnabledModels(): Promise<any[]> { return await (this.electronAPI as any).imageModel.getEnabledModels() }
|
||||
async exportData(): Promise<any> { return await (this.electronAPI as any).imageModel.exportData() }
|
||||
async importData(data: any): Promise<void> { await (this.electronAPI as any).imageModel.importData(data) }
|
||||
async getDataType(): Promise<string> { return await (this.electronAPI as any).imageModel.getDataType() }
|
||||
async validateData(data: any): Promise<boolean> { return await (this.electronAPI as any).imageModel.validateData(data) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { IImageModelManager, ImageRequest, ImageResult, ImageProgressHandlers, IImageService, IImageAdapterRegistry } from './types'
|
||||
import { createImageAdapterRegistry } from './adapters/registry'
|
||||
import { RequestConfigError } from '../llm/errors'
|
||||
|
||||
export class ImageService implements IImageService {
|
||||
public readonly registry: IImageAdapterRegistry = createImageAdapterRegistry()
|
||||
|
||||
constructor(public readonly imageModelManager: IImageModelManager) {}
|
||||
|
||||
public validateRequest(req: ImageRequest) {
|
||||
if (!req?.prompt || !req.prompt.trim()) {
|
||||
throw new RequestConfigError('图像生成: 提示词不能为空')
|
||||
}
|
||||
if (req.inputImage && req.inputImage.b64 && typeof req.inputImage.b64 !== 'string') {
|
||||
throw new RequestConfigError('图像生成: 输入图片格式无效')
|
||||
}
|
||||
const count = req.count ?? 1
|
||||
if (count < 1 || count > 4) {
|
||||
throw new RequestConfigError('图像生成: 仅支持生成 1~4 张(串行)')
|
||||
}
|
||||
|
||||
// 校验单图输入(仅允许 0 或 1)
|
||||
// 已由类型约束,这里补充 MIME 与大小限制
|
||||
if (req.inputImage?.b64) {
|
||||
const mime = (req.inputImage.mimeType || '').toLowerCase()
|
||||
if (mime && mime !== 'image/png' && mime !== 'image/jpeg') {
|
||||
throw new RequestConfigError('图像生成: 仅支持 PNG 或 JPEG')
|
||||
}
|
||||
// 估算 base64 大小:每4字符≈3字节,去除末尾填充
|
||||
const len = req.inputImage.b64.length
|
||||
const padding = (req.inputImage.b64.endsWith('==') ? 2 : req.inputImage.b64.endsWith('=') ? 1 : 0)
|
||||
const bytes = Math.floor(len * 3 / 4) - padding
|
||||
const max = 10 * 1024 * 1024 // 10MB
|
||||
if (bytes > max) {
|
||||
throw new RequestConfigError('图像生成: 本地图片大小不能超过 10MB')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async generate(request: ImageRequest, modelKey: string, handlers?: ImageProgressHandlers): Promise<ImageResult> {
|
||||
this.validateRequest(request)
|
||||
|
||||
if (!modelKey?.trim()) throw new RequestConfigError('图像生成: 模型 key 不能为空')
|
||||
|
||||
const cfg = await this.imageModelManager.getModel(modelKey)
|
||||
if (!cfg) throw new RequestConfigError(`图像生成: 模型不存在: ${modelKey}`)
|
||||
if (!cfg.enabled) throw new RequestConfigError(`图像生成: 模型未启用: ${modelKey}`)
|
||||
|
||||
const adapter = this.registry.getAdapter(cfg.provider)
|
||||
|
||||
handlers?.onProgress?.('queued')
|
||||
|
||||
// 锁定单次生成,避免与适配器内置的批量逻辑叠加造成重复调用
|
||||
const single = await adapter.generate({ ...request, count: 1 }, cfg, handlers)
|
||||
const agg: ImageResult = { images: [], metadata: { model: cfg.defaultModel, notes: [] } }
|
||||
if (single.images?.length) agg.images.push(...single.images)
|
||||
if (single.metadata?.notes?.length) agg.metadata!.notes!.push(...single.metadata.notes)
|
||||
|
||||
// 清理 notes 空数组
|
||||
if (agg.metadata && agg.metadata.notes && agg.metadata.notes.length === 0) {
|
||||
delete agg.metadata.notes
|
||||
}
|
||||
|
||||
handlers?.onProgress?.('done')
|
||||
handlers?.onComplete?.(agg)
|
||||
return agg
|
||||
}
|
||||
}
|
||||
|
||||
export const createImageService = (imageModelManager: IImageModelManager) => new ImageService(imageModelManager)
|
||||
@@ -0,0 +1,113 @@
|
||||
import { IImportExportable } from '../../interfaces/import-export'
|
||||
import type { ModelInfo } from '../llm/types'
|
||||
|
||||
// === 基础类型(请求/结果/进度) ===
|
||||
|
||||
export interface ImageInputRef {
|
||||
b64?: string
|
||||
url?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export interface ImageRequest {
|
||||
prompt: string
|
||||
// 本地文件上传转成 base64;仅支持 0 或 1 张
|
||||
inputImage?: ImageInputRef
|
||||
// 生成张数(串行),默认 1,最大建议 4
|
||||
count?: number
|
||||
// 可选参数(按厂商能力选择性透传)
|
||||
imgParams?: {
|
||||
outputMimeType?: 'image/png' | 'image/jpeg'
|
||||
width?: number
|
||||
height?: number
|
||||
size?: string // 例如 Seedream 的 1K/2K/4K 或 'WxH',OpenAI 的 '1024x1024'
|
||||
seed?: number
|
||||
guidance_scale?: number
|
||||
watermark?: boolean
|
||||
// OpenAI DALL-E 3 特有参数
|
||||
quality?: 'standard' | 'hd'
|
||||
style?: 'vivid' | 'natural'
|
||||
}
|
||||
}
|
||||
|
||||
export interface ImageResultItem {
|
||||
b64?: string
|
||||
url?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export interface ImageResult {
|
||||
images: ImageResultItem[]
|
||||
metadata?: {
|
||||
model?: string
|
||||
notes?: string[]
|
||||
// 扩展metadata支持更多字段
|
||||
finishReason?: string
|
||||
originalPrompt?: string
|
||||
usage?: any
|
||||
created?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ImageProgressHandlers {
|
||||
onProgress?: (stage: 'queued' | 'generating' | 'done' | string | number) => void
|
||||
onPreview?: (img: { b64: string }) => void
|
||||
onComplete?: (result: ImageResult) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
// === 模型配置与管理器接口 ===
|
||||
|
||||
export interface ImageModelConfig {
|
||||
name: string
|
||||
baseURL?: string
|
||||
apiKey?: string
|
||||
defaultModel: string
|
||||
enabled: boolean
|
||||
provider: 'gemini' | 'seedream' | 'openai' | 'custom' | string
|
||||
useVercelProxy?: boolean
|
||||
useDockerProxy?: boolean
|
||||
imgParams?: Record<string, any>
|
||||
capabilities?: {
|
||||
edit?: boolean
|
||||
multiImage?: boolean
|
||||
asyncJob?: boolean
|
||||
streamingPreview?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface IImageModelManager extends IImportExportable {
|
||||
ensureInitialized(): Promise<void>
|
||||
isInitialized(): Promise<boolean>
|
||||
getAllModels(): Promise<Array<ImageModelConfig & { key: string }>>
|
||||
getModel(key: string): Promise<ImageModelConfig | undefined>
|
||||
addModel(key: string, config: ImageModelConfig): Promise<void>
|
||||
updateModel(key: string, config: Partial<ImageModelConfig>): Promise<void>
|
||||
deleteModel(key: string): Promise<void>
|
||||
enableModel(key: string): Promise<void>
|
||||
disableModel(key: string): Promise<void>
|
||||
getEnabledModels(): Promise<Array<ImageModelConfig & { key: string }>>
|
||||
}
|
||||
|
||||
// === 适配器接口与注册表 ===
|
||||
|
||||
export interface IImageProviderAdapter {
|
||||
// 统一的生成方法(内部可阻塞直到完成)
|
||||
generate(request: ImageRequest, config: ImageModelConfig, handlers?: ImageProgressHandlers): Promise<ImageResult>
|
||||
// 可选的模型拉取方法
|
||||
fetchModels?: (config: ImageModelConfig) => Promise<ModelInfo[]>
|
||||
// 能力说明
|
||||
capabilities: NonNullable<ImageModelConfig['capabilities']>
|
||||
}
|
||||
|
||||
export interface IImageAdapterRegistry {
|
||||
getAdapter(provider: string): IImageProviderAdapter
|
||||
}
|
||||
|
||||
export interface IImageService {
|
||||
imageModelManager: IImageModelManager
|
||||
registry: IImageAdapterRegistry
|
||||
validateRequest(req: ImageRequest): void
|
||||
generate(request: ImageRequest, modelKey: string, handlers?: ImageProgressHandlers): Promise<ImageResult>
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ export class PromptService implements IPromptService {
|
||||
/**
|
||||
* 获取默认模板ID
|
||||
*/
|
||||
private async getDefaultTemplateId(templateType: 'optimize' | 'userOptimize' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'): Promise<string> {
|
||||
private async getDefaultTemplateId(templateType: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'): Promise<string> {
|
||||
try {
|
||||
// 尝试获取指定类型的模板列表
|
||||
const templates = await this.templateManager.listTemplatesByType(templateType as any);
|
||||
@@ -466,7 +466,7 @@ export class PromptService implements IPromptService {
|
||||
|
||||
// 如果指定类型没有模板,尝试获取相关类型的模板作为回退
|
||||
try {
|
||||
let fallbackTypes: ('optimize' | 'userOptimize' | 'iterate')[] = [];
|
||||
let fallbackTypes: ('optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'iterate')[] = [];
|
||||
|
||||
if (templateType === 'optimize' || templateType === 'contextSystemOptimize') {
|
||||
fallbackTypes = ['userOptimize']; // optimize类型回退到userOptimize
|
||||
@@ -474,6 +474,12 @@ export class PromptService implements IPromptService {
|
||||
fallbackTypes = ['optimize']; // userOptimize类型回退到optimize
|
||||
} else if (templateType === 'iterate' || templateType === 'contextIterate') {
|
||||
fallbackTypes = ['optimize', 'userOptimize']; // iterate类型回退到任意优化类型
|
||||
} else if (templateType === 'text2imageOptimize') {
|
||||
fallbackTypes = ['userOptimize', 'optimize']; // 文生图回退到基础优化
|
||||
} else if (templateType === 'image2imageOptimize') {
|
||||
fallbackTypes = ['text2imageOptimize', 'userOptimize', 'optimize']; // 图生图优先回退到文生图
|
||||
} else if (templateType === 'imageIterate') {
|
||||
fallbackTypes = ['iterate', 'text2imageOptimize', 'userOptimize']; // 图像迭代回退到通用迭代/文生图
|
||||
}
|
||||
|
||||
for (const fallbackType of fallbackTypes) {
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image2image-general-optimize',
|
||||
name: '图生图优化',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: 图生图提示词优化专家
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: 中文
|
||||
- Description: 专门针对图生图(Image-to-Image)场景的提示词优化专家,能够基于现有图像进行精确的修改和优化指导
|
||||
|
||||
## Background
|
||||
- 图生图与文生图不同,需要在保持原图特征基础上进行修改
|
||||
- 需要明确指出保留什么、修改什么、增强什么
|
||||
- 要考虑原图的构图、风格、主体等要素
|
||||
- 修改指令需要精确、具体,避免过度改变原图意图
|
||||
- 需要平衡保持原图特色与实现用户修改需求
|
||||
|
||||
## 任务理解
|
||||
你的任务是将用户的简单修改需求优化为精确的图生图提示词,确保在保持原图核心特征的基础上实现用户想要的修改效果。
|
||||
|
||||
## Skills
|
||||
1. 图像分析理解能力
|
||||
- 识别需要保留的核心元素
|
||||
- 理解用户的修改意图和程度
|
||||
- 判断修改的可行性和合理性
|
||||
- 预测修改对整体效果的影响
|
||||
|
||||
2. 精确指令构建
|
||||
- 明确指出保持不变的元素
|
||||
- 精确描述需要修改的部分
|
||||
- 提供具体的修改方向和程度
|
||||
- 包含必要的技术参数和风格指导
|
||||
|
||||
## Goals
|
||||
- 保持原图的核心构图和主要特征
|
||||
- 精确实现用户的修改需求
|
||||
- 避免不必要的过度修改
|
||||
- 确保修改后的效果自然协调
|
||||
|
||||
## Constrains
|
||||
- 必须尊重原图的基本构图和主体
|
||||
- 修改幅度要适中,避免面目全非
|
||||
- 保持原图的整体风格协调性
|
||||
- 确保指令清晰、具体、可执行
|
||||
|
||||
## Workflow
|
||||
1. **需求分析**: 理解用户想要修改的具体内容
|
||||
2. **保留识别**: 确定需要保持不变的核心元素
|
||||
3. **修改定义**: 明确指出需要修改的部分和程度
|
||||
4. **效果预期**: 描述期望的最终视觉效果
|
||||
5. **技术参数**: 添加必要的技术指导参数
|
||||
|
||||
## Output Requirements
|
||||
- 直接输出优化后的图生图提示词
|
||||
- 明确区分保留元素和修改元素
|
||||
- 包含具体的修改指导和技术参数
|
||||
- 确保指令精确、可执行且效果可控
|
||||
- 适合主流图生图模型使用`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `请将以下简单的图像修改需求优化为精确的图生图提示词。
|
||||
|
||||
重要说明:
|
||||
- 这是基于现有图像的修改,需要保持原图的核心特征
|
||||
- 请明确指出需要保留的元素和需要修改的部分
|
||||
- 修改指令要具体、精确,避免模糊表达
|
||||
- 包含适当的技术参数和强度控制
|
||||
- 确保修改后效果自然协调
|
||||
|
||||
需要优化的修改需求:
|
||||
{{originalPrompt}}
|
||||
|
||||
请输出精确的图生图优化提示词:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC (固定值,内置模板不可修改)
|
||||
author: 'System',
|
||||
description: '图生图专用提示词优化模板,专注于基于现有图像的精确修改指导',
|
||||
templateType: 'image2imageOptimize',
|
||||
language: 'zh'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image2image-general-optimize_en',
|
||||
name: 'Image-to-Image Optimization',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: Image-to-Image Prompt Optimization Expert
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: English
|
||||
- Description: Specialized expert in optimizing prompts for Image-to-Image scenarios, capable of providing precise modification and enhancement guidance based on existing images
|
||||
|
||||
## Background
|
||||
- Image-to-Image differs from Text-to-Image, requiring modifications while preserving original image characteristics
|
||||
- Need to clearly specify what to preserve, what to modify, and what to enhance
|
||||
- Must consider original image composition, style, subjects, and other elements
|
||||
- Modification instructions need to be precise and specific, avoiding excessive changes to original intent
|
||||
- Need to balance maintaining original image features with achieving user's modification requirements
|
||||
|
||||
## Task Understanding
|
||||
Your task is to optimize simple modification requests into precise Image-to-Image prompts, ensuring user's desired modifications are achieved while maintaining core characteristics of the original image.
|
||||
|
||||
## Skills
|
||||
1. Image Analysis and Understanding
|
||||
- Identify core elements that need preservation
|
||||
- Understand user's modification intent and degree
|
||||
- Judge feasibility and reasonableness of modifications
|
||||
- Predict impact of modifications on overall effect
|
||||
|
||||
2. Precise Instruction Construction
|
||||
- Clearly specify elements to keep unchanged
|
||||
- Precisely describe parts needing modification
|
||||
- Provide specific modification direction and degree
|
||||
- Include necessary technical parameters and style guidance
|
||||
|
||||
## Goals
|
||||
- Maintain original image's core composition and main features
|
||||
- Precisely achieve user's modification requirements
|
||||
- Avoid unnecessary excessive modifications
|
||||
- Ensure modified results are natural and harmonious
|
||||
|
||||
## Constrains
|
||||
- Must respect original image's basic composition and subjects
|
||||
- Modification amplitude should be moderate, avoid complete transformation
|
||||
- Maintain original image's overall style coherence
|
||||
- Ensure instructions are clear, specific, and executable
|
||||
|
||||
## Workflow
|
||||
1. **Requirement Analysis**: Understand specific content user wants to modify
|
||||
2. **Preservation Identification**: Determine core elements to keep unchanged
|
||||
3. **Modification Definition**: Clearly specify parts and degree of modification needed
|
||||
4. **Effect Expectation**: Describe expected final visual effect
|
||||
5. **Technical Parameters**: Add necessary technical guidance parameters
|
||||
|
||||
## Output Requirements
|
||||
- Directly output optimized Image-to-Image prompt
|
||||
- Clearly distinguish preserved elements from modified elements
|
||||
- Include specific modification guidance and technical parameters
|
||||
- Ensure instructions are precise, executable, and effect-controllable
|
||||
- Suitable for mainstream Image-to-Image models`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please optimize the following simple image modification request into a precise Image-to-Image prompt.
|
||||
|
||||
Important Notes:
|
||||
- This is modification based on existing image, need to maintain core characteristics of original image
|
||||
- Please clearly specify elements to preserve and parts to modify
|
||||
- Modification instructions should be specific and precise, avoid vague expressions
|
||||
- Include appropriate technical parameters and intensity control
|
||||
- Ensure modified results are natural and harmonious
|
||||
|
||||
Modification request to optimize:
|
||||
{{originalPrompt}}
|
||||
|
||||
Please output precise Image-to-Image optimization prompt:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC (fixed)
|
||||
author: 'System',
|
||||
description: 'Image-to-Image specialized prompt optimization template, focused on precise modification guidance based on existing images',
|
||||
templateType: 'image2imageOptimize',
|
||||
language: 'en'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-iterate-general',
|
||||
name: '图像迭代(通用)',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role:图像提示词迭代优化专家
|
||||
|
||||
## 背景
|
||||
- 用户已有一个“优化后的图像提示词”,希望在此基础上进行定向改进
|
||||
- 需要保持原有图像的核心视觉意图与风格连续性
|
||||
- 迭代修改需可控、可回退,避免过度修改
|
||||
|
||||
## 任务理解
|
||||
你的工作是基于上一次的“优化后图像提示词”,按照用户给出的迭代方向进行精准改进,输出新的优化版本。
|
||||
|
||||
## 核心原则
|
||||
- 保持视觉意图:主体、构图与叙事不跑偏
|
||||
- 风格连续:风格、光照、质感等保持连贯,不突变
|
||||
- 改动可控:明确说明增强/弱化/替换的元素与程度
|
||||
- 参数友好:必要时给出可控参数(强度、采样、seed/随机性)
|
||||
|
||||
## 工作要点
|
||||
1. 明确“保留”与“改动”范围
|
||||
2. 对关键词进行增删与权重调整(如需要)
|
||||
3. 关键视觉元素(主体/场景/风格/光照/镜头)给出清晰指令
|
||||
4. 适度给出质量增强与负面提示建议(如需要)
|
||||
|
||||
## 输出要求
|
||||
- 直接输出新的“优化后图像提示词”
|
||||
- 保持可读性与可执行性
|
||||
- 仅输出结果,不要解释过程`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `上一次优化后的图像提示词:
|
||||
{{lastOptimizedPrompt}}
|
||||
|
||||
本次迭代方向:
|
||||
{{iterateInput}}
|
||||
|
||||
请据此输出新的优化后图像提示词:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000,
|
||||
author: 'System',
|
||||
description: '基于上一次优化结果进行小步可控的图像提示词迭代,保持风格连续与视觉意图',
|
||||
templateType: 'imageIterate',
|
||||
language: 'zh'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-iterate-general-en',
|
||||
name: 'Image Iterate (General)',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: Image Prompt Iteration Expert
|
||||
|
||||
## Background
|
||||
- The user already has an "optimized image prompt" and wants targeted improvements
|
||||
- Maintain the original visual intent and style continuity
|
||||
- Iterative changes should be controllable and reversible; avoid over-modification
|
||||
|
||||
## Task Understanding
|
||||
Your job is to produce a new optimized image prompt based on the previous optimized prompt and the user's iteration direction.
|
||||
|
||||
## Core Principles
|
||||
- Preserve visual intent: subject, composition, narrative stay aligned
|
||||
- Style continuity: style/lighting/texture remain coherent
|
||||
- Controlled changes: clearly state which elements are enhanced/weakened/replaced and to what extent
|
||||
- Parameter friendliness: include controllable parameters when helpful (strength, sampling, seed/randomness)
|
||||
|
||||
## Key Points
|
||||
1. Clearly separate what to "preserve" vs "change"
|
||||
2. Add/remove keywords and adjust weights where appropriate
|
||||
3. Provide clear directives for key visual elements (subject/scene/style/lighting/lens)
|
||||
4. Optionally include quality enhancements and negative prompt suggestions
|
||||
|
||||
## Output Requirements
|
||||
- Directly output the new optimized image prompt
|
||||
- Keep it readable and executable
|
||||
- Output result only, no explanations`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Previous optimized image prompt:
|
||||
{{lastOptimizedPrompt}}
|
||||
|
||||
Iteration direction:
|
||||
{{iterateInput}}
|
||||
|
||||
Please output the new optimized image prompt accordingly:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000,
|
||||
author: 'System',
|
||||
description: 'Small, controllable iterative improvement of image prompts based on the last optimized version, keeping style continuity and visual intent',
|
||||
templateType: 'imageIterate',
|
||||
language: 'en'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-chinese-optimize',
|
||||
name: '中文模型优化',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: 中文图像生成模型提示词优化专家
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: 中文
|
||||
- Description: 专门针对中文图像生成模型(如 Doubao Seedream)的提示词优化,擅长中文语境和文化元素融合
|
||||
|
||||
## Background
|
||||
- 中文图像生成模型对中文语境有更好的理解能力
|
||||
- 能够更好地理解中国文化元素和传统美学
|
||||
- 适合融入中式色彩描述和意境表达
|
||||
- 支持国风、水墨、工笔等中式艺术风格
|
||||
- 对中文的情感色彩和文化内涵有深度理解
|
||||
|
||||
## 任务理解
|
||||
你的任务是将用户提供的简单图像描述优化为适合中文图像生成模型的提示词,重点在于中文语境优化、文化元素融合和中式美学表达。
|
||||
|
||||
## Skills
|
||||
1. 中文语境优化能力
|
||||
- 语言自然化: 使用地道的中文表达和语言习惯
|
||||
- 文化融合: 融入适当的中国文化元素和传统符号
|
||||
- 意境营造: 运用中文特有的意境和情感表达
|
||||
- 色彩描述: 使用中式色彩词汇和美学概念
|
||||
|
||||
2. 中式美学理解
|
||||
- 传统艺术: 了解国风、水墨、工笔等传统艺术形式
|
||||
- 构图原则: 掌握中式构图的留白、对称、层次等原则
|
||||
- 文化符号: 熟悉传统文化符号和寓意
|
||||
- 季节意境: 理解春夏秋冬的中式意境表达
|
||||
- 诗词意境: 融入古典诗词的意境和美感
|
||||
|
||||
## Goals
|
||||
- 将简单描述转换为富有中文特色的详细提示词
|
||||
- 融入适当的中国文化元素和传统美学
|
||||
- 使用地道的中文表达和情感色彩
|
||||
- 营造符合中式审美的意境和氛围
|
||||
|
||||
## Constrains
|
||||
- 保持用户的原始创意意图不变
|
||||
- 使用自然、地道的中文表达
|
||||
- 适度融入文化元素,避免过度堆砌
|
||||
- 确保描述具体生动,富有画面感
|
||||
|
||||
## Workflow
|
||||
1. **意图理解**: 准确理解用户想要表达的核心内容
|
||||
2. **文化融合**: 识别可以融入的中国文化元素
|
||||
3. **语境优化**: 使用地道的中文表达和语言习惯
|
||||
4. **意境营造**: 添加符合中式美学的意境描述
|
||||
5. **细节完善**: 补充色彩、光线、构图等视觉细节
|
||||
|
||||
## Output Requirements
|
||||
- 直接输出优化后的图像生成提示词
|
||||
- 使用自然、地道的中文表达
|
||||
- 融入适当的中国文化元素
|
||||
- 营造富有中式美感的意境和氛围
|
||||
- 确保描述生动具体,富有画面感`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `请将以下简单的图像描述优化为适合中文图像生成模型的提示词。
|
||||
|
||||
重要说明:
|
||||
- 中文模型对中文语境和文化元素有更好的理解
|
||||
- 请使用地道的中文表达和语言习惯
|
||||
- 可以融入适当的中国文化元素和传统美学
|
||||
- 考虑使用国风、水墨、工笔等中式艺术风格
|
||||
- 营造富有中式意境的氛围和情感
|
||||
|
||||
需要优化的图像描述:
|
||||
{{originalPrompt}}
|
||||
|
||||
请输出适合中文图像模型的优化提示词:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC (固定值,内置模板不可修改)
|
||||
author: 'System',
|
||||
description: '专门针对中文图像生成模型优化的提示词模板,擅长中文语境和文化元素融合',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'zh'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-chinese-optimize-en',
|
||||
name: 'Chinese Model Optimization',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: Chinese Image Generation Model Prompt Optimization Expert
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: English
|
||||
- Description: Specialized in optimizing prompts for Chinese image generation models (such as Doubao Seedream), excelling in Chinese cultural context and cultural element integration
|
||||
|
||||
## Background
|
||||
- Chinese image generation models have better understanding of Chinese cultural context
|
||||
- Can better comprehend Chinese cultural elements and traditional aesthetics
|
||||
- Suitable for integrating Chinese color descriptions and artistic conception expressions
|
||||
- Supports traditional Chinese artistic styles like Guofeng, ink painting, Gongbi
|
||||
- Has deep understanding of Chinese emotional nuances and cultural connotations
|
||||
|
||||
## Task Understanding
|
||||
Your task is to optimize simple image descriptions provided by users into prompts suitable for Chinese image generation models, focusing on Chinese cultural context optimization, cultural element integration, and traditional Chinese aesthetic expression.
|
||||
|
||||
## Skills
|
||||
1. Chinese Cultural Context Optimization
|
||||
- Language Naturalization: Use authentic Chinese expressions and language habits
|
||||
- Cultural Integration: Incorporate appropriate Chinese cultural elements and traditional symbols
|
||||
- Artistic Conception Creation: Utilize unique Chinese artistic conception and emotional expression
|
||||
- Color Description: Use traditional Chinese color vocabulary and aesthetic concepts
|
||||
|
||||
2. Traditional Chinese Aesthetics Understanding
|
||||
- Traditional Arts: Understanding of Guofeng, ink painting, Gongbi and other traditional art forms
|
||||
- Composition Principles: Master Chinese composition principles like negative space, symmetry, layering
|
||||
- Cultural Symbols: Familiar with traditional cultural symbols and their meanings
|
||||
- Seasonal Moods: Understanding of traditional Chinese seasonal mood expressions
|
||||
- Poetic Conception: Incorporating the artistic conception and beauty of classical poetry
|
||||
|
||||
## Goals
|
||||
- Transform simple descriptions into detailed prompts with Chinese characteristics
|
||||
- Integrate appropriate Chinese cultural elements and traditional aesthetics
|
||||
- Use authentic Chinese expressions and emotional colors
|
||||
- Create artistic conception and atmosphere that aligns with traditional Chinese aesthetics
|
||||
|
||||
## Constraints
|
||||
- Maintain the user's original creative intent unchanged
|
||||
- Use natural, authentic expressions
|
||||
- Moderately integrate cultural elements, avoid excessive accumulation
|
||||
- Ensure descriptions are specific, vivid, and visually compelling
|
||||
|
||||
## Workflow
|
||||
1. **Intent Understanding**: Accurately understand the core content the user wants to express
|
||||
2. **Cultural Integration**: Identify Chinese cultural elements that can be incorporated
|
||||
3. **Context Optimization**: Use authentic expressions and language habits
|
||||
4. **Artistic Conception Creation**: Add descriptions that align with traditional Chinese aesthetics
|
||||
5. **Detail Enhancement**: Supplement visual details like color, lighting, composition
|
||||
|
||||
## Output Requirements
|
||||
- Directly output the optimized image generation prompt
|
||||
- Use natural, authentic expressions with Chinese cultural characteristics
|
||||
- Integrate appropriate Chinese cultural elements
|
||||
- Create artistic conception and atmosphere with traditional Chinese beauty
|
||||
- Ensure descriptions are vivid, specific, and visually compelling`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please optimize the following simple image description into a prompt suitable for Chinese image generation models.
|
||||
|
||||
Important Notes:
|
||||
- Chinese models have better understanding of Chinese cultural context and elements
|
||||
- Please consider incorporating appropriate Chinese cultural elements and traditional aesthetics
|
||||
- You can use traditional Chinese artistic styles like Guofeng, ink painting, Gongbi
|
||||
- Create atmosphere and emotions rich in traditional Chinese artistic conception
|
||||
- Focus on cultural authenticity and aesthetic harmony
|
||||
|
||||
Image description to optimize:
|
||||
{{originalPrompt}}
|
||||
|
||||
Please output the optimized prompt suitable for Chinese image models:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC (固定值,内置模板不可修改)
|
||||
author: 'System',
|
||||
description: 'Image prompt template specifically optimized for Chinese image generation models, excelling in Chinese cultural context and cultural element integration',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'en'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-dalle-optimize',
|
||||
name: 'DALL-E 风格优化',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: DALL-E 图像提示词优化专家
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: 中文
|
||||
- Description: 专门针对 DALL-E 模型特点的图像提示词优化,擅长自然语言描述和复杂场景构建
|
||||
|
||||
## Background
|
||||
- DALL-E 模型擅长理解自然语言描述和复杂的场景构建
|
||||
- 相比标签式提示词,DALL-E 更适合流畅的自然语言表达
|
||||
- 能够很好地理解情感色彩、氛围描述和复杂的构图要求
|
||||
- 支持详细的场景描述和人物情感表达
|
||||
|
||||
## 任务理解
|
||||
你的任务是将用户提供的简单图像描述优化为适合 DALL-E 模型的详细、自然的提示词,重点在于场景构建、情感表达和自然语言流畅性。
|
||||
|
||||
## Skills
|
||||
1. 自然语言优化能力
|
||||
- 场景构建: 将简单描述扩展为完整的场景叙述
|
||||
- 情感表达: 添加适当的情感色彩和氛围描述
|
||||
- 细节补充: 补充视觉细节,如光线、色彩、质感
|
||||
- 构图指导: 使用摄影术语描述构图和视角
|
||||
|
||||
2. DALL-E 特性理解
|
||||
- 自然语言偏好: 使用流畅的句子而非关键词堆砌
|
||||
- 场景理解: 构建有逻辑的完整场景
|
||||
- 风格描述: 使用艺术风格和摄影风格的自然描述
|
||||
- 质量提升: 通过描述性语言提升图像质量
|
||||
|
||||
## Goals
|
||||
- 将简单的图像描述转换为丰富的场景叙述
|
||||
- 添加适当的情感色彩和氛围描述
|
||||
- 补充必要的视觉细节和技术要求
|
||||
- 确保语言自然流畅,符合 DALL-E 的理解特点
|
||||
|
||||
## Constrains
|
||||
- 保持用户的原始创意意图不变
|
||||
- 使用自然、流畅的中文表达
|
||||
- 避免过度技术化的术语和参数
|
||||
- 确保描述具体而不抽象
|
||||
|
||||
## Workflow
|
||||
1. **意图理解**: 准确理解用户想要表达的核心内容
|
||||
2. **场景构建**: 将简单描述扩展为完整的场景
|
||||
3. **细节补充**: 添加光线、色彩、质感等视觉细节
|
||||
4. **氛围营造**: 加入情感色彩和氛围描述
|
||||
5. **语言优化**: 确保表达自然流畅,符合 DALL-E 特点
|
||||
|
||||
## Output Requirements
|
||||
- 直接输出优化后的图像生成提示词
|
||||
- 使用自然、流畅的中文表达
|
||||
- 包含丰富的场景描述和视觉细节
|
||||
- 确保描述具体、生动,易于 DALL-E 理解`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `请将以下简单的图像描述优化为适合 DALL-E 模型的详细提示词。
|
||||
|
||||
重要说明:
|
||||
- DALL-E 擅长理解自然语言描述和复杂场景
|
||||
- 请使用流畅的句子而非关键词堆砌
|
||||
- 重点在于场景构建、情感表达和氛围营造
|
||||
- 补充必要的视觉细节,如光线、色彩、构图等
|
||||
|
||||
需要优化的图像描述:
|
||||
{{originalPrompt}}
|
||||
|
||||
请输出适合 DALL-E 的优化提示词:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC
|
||||
author: 'System',
|
||||
description: '专门针对 DALL-E 模型优化的图像提示词模板,擅长自然语言描述和场景构建',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'zh'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-dalle-optimize-en',
|
||||
name: 'DALL-E Style Optimization',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: DALL-E Image Prompt Optimization Expert
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: English
|
||||
- Description: Specialized in optimizing image prompts for DALL-E model characteristics, excelling in natural language descriptions and complex scene construction
|
||||
|
||||
## Background
|
||||
- DALL-E model excels at understanding natural language descriptions and complex scene construction
|
||||
- Compared to tag-style prompts, DALL-E is better suited for fluent natural language expressions
|
||||
- Can effectively understand emotional nuances, atmospheric descriptions, and complex compositional requirements
|
||||
- Supports detailed scene descriptions and character emotional expressions
|
||||
|
||||
## Task Understanding
|
||||
Your task is to optimize simple image descriptions provided by users into detailed, natural prompts suitable for the DALL-E model, focusing on scene construction, emotional expression, and natural language fluency.
|
||||
|
||||
## Skills
|
||||
1. Natural Language Optimization
|
||||
- Scene Construction: Expand simple descriptions into complete scene narratives
|
||||
- Emotional Expression: Add appropriate emotional colors and atmospheric descriptions
|
||||
- Detail Enhancement: Supplement visual details such as lighting, color, texture
|
||||
- Composition Guidance: Use photography terminology to describe composition and perspective
|
||||
|
||||
2. DALL-E Characteristics Understanding
|
||||
- Natural Language Preference: Use fluent sentences rather than keyword stacking
|
||||
- Scene Understanding: Build logical and complete scenes
|
||||
- Style Description: Use natural descriptions of artistic and photographic styles
|
||||
- Quality Enhancement: Improve image quality through descriptive language
|
||||
|
||||
## Goals
|
||||
- Transform simple image descriptions into rich scene narratives
|
||||
- Add appropriate emotional colors and atmospheric descriptions
|
||||
- Supplement necessary visual details and technical requirements
|
||||
- Ensure natural and fluent language that aligns with DALL-E's understanding characteristics
|
||||
|
||||
## Constraints
|
||||
- Maintain the user's original creative intent unchanged
|
||||
- Use natural, fluent English expressions
|
||||
- Avoid overly technical terminology and parameters
|
||||
- Ensure descriptions are specific rather than abstract
|
||||
|
||||
## Workflow
|
||||
1. **Intent Understanding**: Accurately understand the core content the user wants to express
|
||||
2. **Scene Construction**: Expand simple descriptions into complete scenes
|
||||
3. **Detail Enhancement**: Add visual details such as lighting, color, texture
|
||||
4. **Atmosphere Creation**: Include emotional colors and atmospheric descriptions
|
||||
5. **Language Optimization**: Ensure natural and fluent expression suitable for DALL-E
|
||||
|
||||
## Output Requirements
|
||||
- Directly output the optimized image generation prompt
|
||||
- Use natural, fluent English expressions
|
||||
- Include rich scene descriptions and visual details
|
||||
- Ensure descriptions are specific, vivid, and easily understood by DALL-E`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please optimize the following simple image description into a detailed prompt suitable for the DALL-E model.
|
||||
|
||||
Important Notes:
|
||||
- DALL-E excels at understanding natural language descriptions and complex scenes
|
||||
- Please use fluent sentences rather than keyword stacking
|
||||
- Focus on scene construction, emotional expression, and atmosphere creation
|
||||
- Supplement necessary visual details such as lighting, color, composition, etc.
|
||||
|
||||
Image description to optimize:
|
||||
{{originalPrompt}}
|
||||
|
||||
Please output the optimized prompt suitable for DALL-E:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC
|
||||
author: 'System',
|
||||
description: 'Image prompt template specifically optimized for DALL-E model, excelling in natural language descriptions and scene construction',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'en'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-general-optimize',
|
||||
name: '通用图像优化',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: 通用图像生成提示词优化专家
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: 中文
|
||||
- Description: 通用图像生成提示词优化专家,能够创建适合多种图像模型的平衡优化提示词
|
||||
|
||||
## Background
|
||||
- 不同的图像生成模型有不同的特点和偏好
|
||||
- 通用优化需要平衡各种模型的特性
|
||||
- 既要保持自然语言的流畅性,又要包含关键的标签信息
|
||||
- 需要提供适度的技术参数和质量提升要素
|
||||
- 确保优化后的提示词具有广泛的兼容性
|
||||
|
||||
## 任务理解
|
||||
你的任务是将用户提供的简单图像描述优化为通用的、适合多种图像生成模型的提示词,平衡自然语言描述和关键词标签的优势。
|
||||
|
||||
## Skills
|
||||
1. 平衡优化能力
|
||||
- 语言平衡: 结合自然语言描述和关键词标签
|
||||
- 细节补充: 添加必要的视觉细节和技术要求
|
||||
- 质量提升: 包含通用的质量提升关键词
|
||||
- 风格指导: 提供适度的艺术风格建议
|
||||
- 兼容性考虑: 确保各种模型都能理解
|
||||
|
||||
2. 通用优化理解
|
||||
- 模型兼容: 了解不同模型的共同特点
|
||||
- 描述平衡: 平衡详细程度和简洁性
|
||||
- 关键要素: 识别对所有模型都重要的元素
|
||||
- 质量标准: 使用通用的质量提升方法
|
||||
- 技术参数: 包含基础的技术要求
|
||||
|
||||
## Goals
|
||||
- 将简单描述转换为详细而平衡的提示词
|
||||
- 补充必要的视觉细节和质量要求
|
||||
- 确保提示词具有广泛的模型兼容性
|
||||
- 提供清晰、具体的图像生成指导
|
||||
|
||||
## Constrains
|
||||
- 保持用户的原始创意意图不变
|
||||
- 平衡自然语言和关键词的使用
|
||||
- 避免过于偏向某种特定模型
|
||||
- 确保描述清晰、具体、易理解
|
||||
|
||||
## Workflow
|
||||
1. **需求分析**: 理解用户想要表达的核心内容
|
||||
2. **细节补充**: 添加必要的视觉细节和描述
|
||||
3. **质量优化**: 包含通用的质量提升要素
|
||||
4. **风格平衡**: 提供适度的艺术风格指导
|
||||
5. **兼容性检查**: 确保各种模型都能理解
|
||||
|
||||
## Output Requirements
|
||||
- 直接输出优化后的图像生成提示词
|
||||
- 平衡使用自然语言描述和关键词
|
||||
- 包含必要的视觉细节和质量要求
|
||||
- 确保描述清晰、具体、兼容性强
|
||||
- 适合多种图像生成模型使用`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `请将以下简单的图像描述优化为通用的、适合多种图像生成模型的提示词。
|
||||
|
||||
重要说明:
|
||||
- 请平衡自然语言描述和关键词标签的使用
|
||||
- 补充必要的视觉细节:颜色、光线、构图、质感等
|
||||
- 添加适当的质量提升关键词
|
||||
- 包含基础的艺术风格指导
|
||||
- 确保提示词具有广泛的模型兼容性
|
||||
|
||||
需要优化的图像描述:
|
||||
{{originalPrompt}}
|
||||
|
||||
请输出通用的优化提示词:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000, // 2024-01-01 00:00:00 UTC
|
||||
author: 'System',
|
||||
description: '通用图像生成提示词优化模板,平衡各种模型特点,具有广泛兼容性',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'zh'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-general-optimize-en',
|
||||
name: 'General Image Optimization',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: Universal Image Generation Prompt Optimization Expert
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: English
|
||||
- Description: Universal image generation prompt optimization expert, capable of creating balanced optimization prompts suitable for multiple image models
|
||||
|
||||
## Background
|
||||
- Different image generation models have different characteristics and preferences
|
||||
- Universal optimization needs to balance the characteristics of various models
|
||||
- Must maintain natural language fluency while including key tag information
|
||||
- Need to provide moderate technical parameters and quality enhancement elements
|
||||
- Ensure optimized prompts have broad compatibility
|
||||
|
||||
## Task Understanding
|
||||
Your task is to optimize simple image descriptions provided by users into universal prompts suitable for multiple image generation models, balancing the advantages of natural language descriptions and keyword tags.
|
||||
|
||||
## Skills
|
||||
1. Balanced Optimization Ability
|
||||
- Language Balance: Combine natural language descriptions with keyword tags
|
||||
- Detail Enhancement: Add necessary visual details and technical requirements
|
||||
- Quality Improvement: Include universal quality enhancement keywords
|
||||
- Style Guidance: Provide moderate artistic style suggestions
|
||||
- Compatibility Consideration: Ensure understanding across various models
|
||||
|
||||
2. Universal Optimization Understanding
|
||||
- Model Compatibility: Understand common characteristics of different models
|
||||
- Description Balance: Balance detail level and conciseness
|
||||
- Key Elements: Identify elements important to all models
|
||||
- Quality Standards: Use universal quality enhancement methods
|
||||
- Technical Parameters: Include basic technical requirements
|
||||
|
||||
## Goals
|
||||
- Transform simple descriptions into detailed and balanced prompts
|
||||
- Supplement necessary visual details and quality requirements
|
||||
- Ensure prompts have broad model compatibility
|
||||
- Provide clear, specific image generation guidance
|
||||
|
||||
## Constraints
|
||||
- Maintain the user's original creative intent unchanged
|
||||
- Balance the use of natural language and keywords
|
||||
- Avoid over-favoring any specific model
|
||||
- Ensure descriptions are clear, specific, and easily understood
|
||||
|
||||
## Workflow
|
||||
1. **Requirement Analysis**: Understand the core content the user wants to express
|
||||
2. **Detail Enhancement**: Add necessary visual details and descriptions
|
||||
3. **Quality Optimization**: Include universal quality enhancement elements
|
||||
4. **Style Balance**: Provide moderate artistic style guidance
|
||||
5. **Compatibility Check**: Ensure understanding across various models
|
||||
|
||||
## Output Requirements
|
||||
- Directly output the optimized image generation prompt
|
||||
- Balance the use of natural language descriptions and keywords
|
||||
- Include necessary visual details and quality requirements
|
||||
- Ensure descriptions are clear, specific, and highly compatible
|
||||
- Suitable for use with multiple image generation models`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please optimize the following simple image description into a universal prompt suitable for multiple image generation models.
|
||||
|
||||
Important Notes:
|
||||
- Please balance the use of natural language descriptions and keyword tags
|
||||
- Supplement necessary visual details: color, lighting, composition, texture, etc.
|
||||
- Add appropriate quality enhancement keywords
|
||||
- Include basic artistic style guidance
|
||||
- Ensure the prompt has broad model compatibility
|
||||
|
||||
Image description to optimize:
|
||||
{{originalPrompt}}
|
||||
|
||||
Please output the universal optimized prompt:`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000,
|
||||
author: 'System',
|
||||
description: 'Universal image generation prompt optimization template, balancing various model characteristics with broad compatibility',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'en'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-sd-optimize',
|
||||
name: 'Stable Diffusion 风格优化',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: Stable Diffusion 图像提示词优化专家
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: 中文
|
||||
- Description: 专门针对 Stable Diffusion 模型特点的图像提示词优化,擅长标签式关键词和权重控制
|
||||
|
||||
## Background
|
||||
- Stable Diffusion 模型适合标签式关键词和权重控制
|
||||
- 使用逗号分隔的关键词形式效果最佳
|
||||
- 支持权重语法 (keyword:1.2) 来强调重要元素
|
||||
- 质量标签和技术参数对生成效果有显著影响
|
||||
- 负面提示词能有效避免不想要的元素
|
||||
|
||||
## 任务理解
|
||||
你的任务是将用户提供的简单图像描述优化为适合 Stable Diffusion 模型的标签式提示词,重点在于关键词提取、权重分配和质量提升。
|
||||
|
||||
## Skills
|
||||
1. 标签式优化能力
|
||||
- 关键词提取: 将描述转换为精确的关键词标签
|
||||
- 权重控制: 为重要元素添加适当的权重语法
|
||||
- 标签组合: 合理组织关键词的顺序和搭配
|
||||
- 质量提升: 添加质量相关的标签和参数
|
||||
|
||||
2. Stable Diffusion 特性理解
|
||||
- 标签偏好: 使用关键词而非完整句子
|
||||
- 权重语法: 掌握 (keyword:1.2) 等权重控制方法
|
||||
- 质量标签: 了解 masterpiece, best quality 等质量提升标签
|
||||
- 技术参数: 熟悉分辨率、光线、风格等技术标签
|
||||
- 负面提示: 提供避免低质量结果的负面关键词
|
||||
|
||||
## Goals
|
||||
- 将自然语言描述转换为标签式关键词
|
||||
- 为重要元素添加适当的权重控制
|
||||
- 补充质量提升和技术参数标签
|
||||
- 提供有效的负面提示词建议
|
||||
|
||||
## Constrains
|
||||
- 保持用户的原始创意意图不变
|
||||
- 使用逗号分隔的标签格式
|
||||
- 合理使用权重语法,避免过度使用
|
||||
- 确保标签准确、具体
|
||||
|
||||
## Workflow
|
||||
1. **内容分析**: 识别描述中的核心元素和重要特征
|
||||
2. **关键词提取**: 将描述转换为精确的标签关键词
|
||||
3. **权重分配**: 为重要元素添加权重语法
|
||||
4. **质量增强**: 添加质量提升和技术参数标签
|
||||
5. **负面建议**: 提供相关的负面提示词
|
||||
|
||||
## Output Requirements
|
||||
- 使用逗号分隔的标签格式
|
||||
- 重要元素使用权重语法 (keyword:1.2)
|
||||
- 包含质量提升标签
|
||||
- 提供负面提示词建议
|
||||
- 确保标签准确、具体`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `请将以下简单的图像描述优化为适合 Stable Diffusion 模型的标签式提示词。
|
||||
|
||||
重要说明:
|
||||
- Stable Diffusion 适合标签式关键词,用逗号分隔
|
||||
- 重要元素可以使用权重语法 (keyword:1.2)
|
||||
- 请添加质量提升标签:masterpiece, best quality, highly detailed
|
||||
- 包含适当的艺术风格和技术参数标签
|
||||
- 可以提供负面提示词建议
|
||||
|
||||
需要优化的图像描述:
|
||||
{{originalPrompt}}
|
||||
|
||||
请按以下格式输出:
|
||||
|
||||
**正面提示词:**
|
||||
[优化后的标签式提示词]
|
||||
|
||||
**负面提示词建议:**
|
||||
[避免低质量结果的负面关键词]`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000,
|
||||
author: 'System',
|
||||
description: '专门针对 Stable Diffusion 模型优化的图像提示词模板,擅长标签式关键词和权重控制',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'zh'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { Template, MessageTemplate } from '../../../types';
|
||||
|
||||
export const template: Template = {
|
||||
id: 'image-sd-optimize-en',
|
||||
name: 'Stable Diffusion Style Optimization',
|
||||
content: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `# Role: Stable Diffusion Image Prompt Optimization Expert
|
||||
|
||||
## Profile
|
||||
- Author: prompt-optimizer
|
||||
- Version: 1.0.0
|
||||
- Language: English
|
||||
- Description: Specialized in optimizing image prompts for Stable Diffusion model characteristics, excelling in tag-style keywords and weight control
|
||||
|
||||
## Background
|
||||
- Stable Diffusion model is suitable for tag-style keywords and weight control
|
||||
- Comma-separated keyword format works best
|
||||
- Supports weight syntax (keyword:1.2) to emphasize important elements
|
||||
- Quality tags and technical parameters have significant impact on generation results
|
||||
- Negative prompts can effectively avoid unwanted elements
|
||||
|
||||
## Task Understanding
|
||||
Your task is to optimize simple image descriptions provided by users into tag-style prompts suitable for the Stable Diffusion model, focusing on keyword extraction, weight allocation, and quality enhancement.
|
||||
|
||||
## Skills
|
||||
1. Tag-style Optimization
|
||||
- Keyword Extraction: Convert descriptions into precise keyword tags
|
||||
- Weight Control: Add appropriate weight syntax for important elements
|
||||
- Tag Combination: Reasonably organize keyword order and combinations
|
||||
- Quality Enhancement: Add quality-related tags and parameters
|
||||
|
||||
2. Stable Diffusion Characteristics Understanding
|
||||
- Tag Preference: Use keywords rather than complete sentences
|
||||
- Weight Syntax: Master weight control methods like (keyword:1.2)
|
||||
- Quality Tags: Understand quality enhancement tags like masterpiece, best quality
|
||||
- Technical Parameters: Familiar with resolution, lighting, style technical tags
|
||||
- Negative Prompts: Provide negative keywords to avoid low-quality results
|
||||
|
||||
## Goals
|
||||
- Convert natural language descriptions into tag-style keywords
|
||||
- Add appropriate weight control for important elements
|
||||
- Supplement quality enhancement and technical parameter tags
|
||||
- Provide effective negative prompt suggestions
|
||||
|
||||
## Constraints
|
||||
- Maintain the user's original creative intent unchanged
|
||||
- Use comma-separated tag format
|
||||
- Use weight syntax reasonably, avoid overuse
|
||||
- Ensure tags are accurate and specific
|
||||
|
||||
## Workflow
|
||||
1. **Content Analysis**: Identify core elements and important features in the description
|
||||
2. **Keyword Extraction**: Convert descriptions into precise tag keywords
|
||||
3. **Weight Allocation**: Add weight syntax for important elements
|
||||
4. **Quality Enhancement**: Add quality enhancement and technical parameter tags
|
||||
5. **Negative Suggestions**: Provide relevant negative prompt words
|
||||
|
||||
## Output Requirements
|
||||
- Use comma-separated tag format
|
||||
- Use weight syntax (keyword:1.2) for important elements
|
||||
- Include quality enhancement tags
|
||||
- Provide negative prompt suggestions
|
||||
- Ensure tags are accurate and specific`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please optimize the following simple image description into tag-style prompts suitable for the Stable Diffusion model.
|
||||
|
||||
Important Notes:
|
||||
- Stable Diffusion works well with tag-style keywords, separated by commas
|
||||
- Important elements can use weight syntax (keyword:1.2)
|
||||
- Please add quality enhancement tags: masterpiece, best quality, highly detailed
|
||||
- Include appropriate artistic style and technical parameter tags
|
||||
- You can provide negative prompt suggestions
|
||||
|
||||
Image description to optimize:
|
||||
{{originalPrompt}}
|
||||
|
||||
Please output in the following format:
|
||||
|
||||
**Positive Prompt:**
|
||||
[Optimized tag-style prompt]
|
||||
|
||||
**Negative Prompt Suggestions:**
|
||||
[Negative keywords to avoid low-quality results]`
|
||||
}
|
||||
] as MessageTemplate[],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: 1704067200000,
|
||||
author: 'System',
|
||||
description: 'Image prompt template specifically optimized for Stable Diffusion model, excelling in tag-style keywords and weight control',
|
||||
templateType: 'text2imageOptimize',
|
||||
language: 'en'
|
||||
},
|
||||
isBuiltin: true
|
||||
};
|
||||
@@ -39,6 +39,23 @@ import { user_prompt_basic_en } from './user-optimize/user-prompt-basic_en';
|
||||
import { user_prompt_planning } from './user-optimize/user-prompt-planning';
|
||||
import { user_prompt_planning_en } from './user-optimize/user-prompt-planning_en';
|
||||
|
||||
// 图像优化模板(重构后的目录结构)
|
||||
// 文生图
|
||||
import { template as image_dalle_optimize } from './image-optimize/text2image/dalle-optimize';
|
||||
import { template as image_dalle_optimize_en } from './image-optimize/text2image/dalle-optimize_en';
|
||||
import { template as image_sd_optimize } from './image-optimize/text2image/stable-diffusion-optimize';
|
||||
import { template as image_sd_optimize_en } from './image-optimize/text2image/stable-diffusion-optimize_en';
|
||||
import { template as image_chinese_optimize } from './image-optimize/text2image/chinese-model-optimize';
|
||||
import { template as image_chinese_optimize_en } from './image-optimize/text2image/chinese-model-optimize_en';
|
||||
import { template as image_general_optimize } from './image-optimize/text2image/general-image-optimize';
|
||||
import { template as image_general_optimize_en } from './image-optimize/text2image/general-image-optimize_en';
|
||||
// 图生图
|
||||
import { template as image2image_optimize } from './image-optimize/image2image/image2image-optimize';
|
||||
import { template as image2image_optimize_en } from './image-optimize/image2image/image2image-optimize_en';
|
||||
// 图像迭代
|
||||
import { template as image_iterate_general } from './image-optimize/iterate/image-iterate-general';
|
||||
import { template as image_iterate_general_en } from './image-optimize/iterate/image-iterate-general_en';
|
||||
|
||||
// 简单的模板集合 - 模板自身已包含完整信息(id、name、language、type等)
|
||||
export const ALL_TEMPLATES = {
|
||||
general_optimize,
|
||||
@@ -69,4 +86,19 @@ export const ALL_TEMPLATES = {
|
||||
user_prompt_basic_en,
|
||||
user_prompt_planning,
|
||||
user_prompt_planning_en,
|
||||
// 图像优化模板
|
||||
image_dalle_optimize,
|
||||
image_dalle_optimize_en,
|
||||
image_sd_optimize,
|
||||
image_sd_optimize_en,
|
||||
image_chinese_optimize,
|
||||
image_chinese_optimize_en,
|
||||
image_general_optimize,
|
||||
image_general_optimize_en,
|
||||
// 图生图模板
|
||||
image2image_optimize,
|
||||
image2image_optimize_en,
|
||||
// 图像迭代模板
|
||||
image_iterate_general,
|
||||
image_iterate_general_en,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ interface ElectronAPI {
|
||||
getTemplates: () => Promise<Template[]>;
|
||||
exportTemplate: (id: string) => Promise<string>;
|
||||
importTemplate: (jsonString: string) => Promise<void>;
|
||||
listTemplatesByType: (type: 'optimize' | 'userOptimize' | 'iterate') => Promise<Template[]>;
|
||||
listTemplatesByType: (type: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate') => Promise<Template[]>;
|
||||
changeBuiltinTemplateLanguage: (language: BuiltinTemplateLanguage) => Promise<void>;
|
||||
getCurrentBuiltinTemplateLanguage: () => Promise<BuiltinTemplateLanguage>;
|
||||
getSupportedBuiltinTemplateLanguages: () => Promise<BuiltinTemplateLanguage[]>;
|
||||
@@ -71,7 +71,7 @@ export class ElectronTemplateManagerProxy implements ITemplateManager {
|
||||
return this.electronAPI.importTemplate(jsonString);
|
||||
}
|
||||
|
||||
async listTemplatesByType(type: 'optimize' | 'userOptimize' | 'iterate'): Promise<Template[]> {
|
||||
async listTemplatesByType(type: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'): Promise<Template[]> {
|
||||
return this.electronAPI.listTemplatesByType(type);
|
||||
}
|
||||
|
||||
@@ -120,4 +120,4 @@ export class ElectronTemplateManagerProxy implements ITemplateManager {
|
||||
const safeData = safeSerializeForIPC(data);
|
||||
return this.electronAPI.validateData(safeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ export class TemplateManager implements ITemplateManager {
|
||||
/**
|
||||
* List templates by type
|
||||
*/
|
||||
async listTemplatesByType(type: 'optimize' | 'userOptimize' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'): Promise<Template[]> {
|
||||
async listTemplatesByType(type: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'): Promise<Template[]> {
|
||||
try {
|
||||
const templates = await this.listTemplates();
|
||||
return templates.filter(
|
||||
|
||||
@@ -8,10 +8,13 @@ import { ALL_TEMPLATES } from './default-templates';
|
||||
* 🔄 直接使用:无需复杂的元数据推导和映射
|
||||
*/
|
||||
|
||||
// 类型定义(支持 6 类:基础 + 上下文)
|
||||
// 类型定义(支持 8 类:基础 + 上下文 + 图像)
|
||||
export type TemplateType =
|
||||
| 'optimize'
|
||||
| 'user-optimize'
|
||||
| 'text2imageOptimize'
|
||||
| 'image2imageOptimize'
|
||||
| 'imageIterate'
|
||||
| 'iterate'
|
||||
| 'context-system-optimize'
|
||||
| 'context-user-optimize'
|
||||
@@ -67,6 +70,9 @@ export class StaticLoader {
|
||||
const byType: Record<TemplateType, Record<Language, Record<string, Template>>> = {
|
||||
'optimize': { zh: {}, en: {} },
|
||||
'user-optimize': { zh: {}, en: {} },
|
||||
'text2imageOptimize': { zh: {}, en: {} },
|
||||
'image2imageOptimize': { zh: {}, en: {} },
|
||||
'imageIterate': { zh: {}, en: {} },
|
||||
'iterate': { zh: {}, en: {} },
|
||||
'context-system-optimize': { zh: {}, en: {} },
|
||||
'context-user-optimize': { zh: {}, en: {} },
|
||||
@@ -84,12 +90,21 @@ export class StaticLoader {
|
||||
throw new Error(`Built-in template '${id}' is missing required 'language' field in metadata`);
|
||||
}
|
||||
|
||||
// 规范化模板类型(将 metadata.templateType 映射为静态分类键)
|
||||
// 规范化模板类型(直接使用 metadata.templateType)
|
||||
let normalizedType: TemplateType;
|
||||
switch (templateType) {
|
||||
case 'userOptimize':
|
||||
normalizedType = 'user-optimize';
|
||||
break;
|
||||
case 'text2imageOptimize':
|
||||
normalizedType = 'text2imageOptimize';
|
||||
break;
|
||||
case 'image2imageOptimize':
|
||||
normalizedType = 'image2imageOptimize';
|
||||
break;
|
||||
case 'imageIterate':
|
||||
normalizedType = 'imageIterate';
|
||||
break;
|
||||
case 'contextSystemOptimize':
|
||||
normalizedType = 'context-system-optimize';
|
||||
break;
|
||||
@@ -124,6 +139,9 @@ export class StaticLoader {
|
||||
'英文': Object.keys(byLanguage.en).length,
|
||||
optimize: Object.keys(byType.optimize.zh).length + Object.keys(byType.optimize.en).length,
|
||||
'user-optimize': Object.keys(byType['user-optimize'].zh).length + Object.keys(byType['user-optimize'].en).length,
|
||||
text2imageOptimize: Object.keys(byType.text2imageOptimize.zh).length + Object.keys(byType.text2imageOptimize.en).length,
|
||||
image2imageOptimize: Object.keys(byType.image2imageOptimize.zh).length + Object.keys(byType.image2imageOptimize.en).length,
|
||||
imageIterate: Object.keys(byType.imageIterate.zh).length + Object.keys(byType.imageIterate.en).length,
|
||||
iterate: Object.keys(byType.iterate.zh).length + Object.keys(byType.iterate.en).length,
|
||||
'context-system-optimize': Object.keys(byType['context-system-optimize'].zh).length + Object.keys(byType['context-system-optimize'].en).length,
|
||||
'context-user-optimize': Object.keys(byType['context-user-optimize'].zh).length + Object.keys(byType['context-user-optimize'].en).length,
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface TemplateMetadata {
|
||||
lastModified: number; // 最后修改时间
|
||||
author?: string; // 作者(可选)
|
||||
description?: string; // 描述(可选)
|
||||
templateType: 'optimize' | 'userOptimize' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'; // 模板类型标识
|
||||
templateType: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'; // 模板类型标识
|
||||
language?: 'zh' | 'en'; // 模板语言(可选,主要用于内置模板语言切换)
|
||||
[key: string]: any; // 允许任意额外字段
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export interface ITemplateManager extends IImportExportable {
|
||||
/**
|
||||
* List templates by type
|
||||
*/
|
||||
listTemplatesByType(type: 'optimize' | 'userOptimize' | 'iterate'): Promise<Template[]>;
|
||||
listTemplatesByType(type: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate'): Promise<Template[]>;
|
||||
|
||||
/**
|
||||
* Change built-in template language
|
||||
@@ -119,7 +119,7 @@ export const templateSchema = z.object({
|
||||
lastModified: z.number(),
|
||||
author: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
templateType: z.enum(['optimize', 'userOptimize', 'iterate', 'contextSystemOptimize', 'contextUserOptimize', 'contextIterate']),
|
||||
templateType: z.enum(['optimize', 'userOptimize', 'text2imageOptimize', 'image2imageOptimize', 'imageIterate', 'iterate', 'contextSystemOptimize', 'contextUserOptimize', 'contextIterate']),
|
||||
language: z.enum(['zh', 'en']).optional()
|
||||
}).passthrough(), // 允许额外字段通过验证
|
||||
isBuiltin: z.boolean().optional()
|
||||
|
||||
Vendored
+19
@@ -83,6 +83,25 @@ interface Window {
|
||||
config: {
|
||||
getEnvironmentVariables: () => Promise<Record<string, string>>;
|
||||
};
|
||||
image: {
|
||||
generate: (request: any, modelKey: string) => Promise<any>;
|
||||
};
|
||||
imageModel: {
|
||||
ensureInitialized: () => Promise<void>;
|
||||
isInitialized: () => Promise<boolean>;
|
||||
getAllModels: () => Promise<any[]>;
|
||||
getModel: (key: string) => Promise<any>;
|
||||
addModel: (model: any) => Promise<void>;
|
||||
updateModel: (key: string, updates: any) => Promise<void>;
|
||||
deleteModel: (key: string) => Promise<void>;
|
||||
enableModel: (key: string) => Promise<void>;
|
||||
disableModel: (key: string) => Promise<void>;
|
||||
getEnabledModels: () => Promise<any[]>;
|
||||
exportData: () => Promise<any>;
|
||||
importData: (data: any) => Promise<void>;
|
||||
getDataType: () => Promise<string>;
|
||||
validateData: (data: any) => Promise<boolean>;
|
||||
};
|
||||
storage: {
|
||||
// Define the methods for the Storage API proxy
|
||||
getItem: (key: string) => Promise<string | null>;
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest'
|
||||
import { ImageService } from '../../src/services/image/service'
|
||||
import { ImageModelManager } from '../../src/services/image-model/manager'
|
||||
import { LocalStorageProvider } from '../../src/services/storage/localStorageProvider'
|
||||
import type { ImageRequest, ImageModelConfig } from '../../src/services/image/types'
|
||||
|
||||
/**
|
||||
* 图像适配器真实API集成测试
|
||||
* 只有在相应的环境变量存在时才执行
|
||||
*/
|
||||
describe('Image Adapters Real API Integration Tests', () => {
|
||||
const hasGeminiKey = !!process.env.VITE_GEMINI_API_KEY
|
||||
const hasOpenAIKey = !!process.env.VITE_OPENAI_API_KEY
|
||||
const hasSeedreamKey = !!(process.env.VITE_SEEDREAM_API_KEY || process.env.VITE_ARK_API_KEY || process.env.ARK_API_KEY)
|
||||
|
||||
let storage: LocalStorageProvider
|
||||
let imageModelManager: ImageModelManager
|
||||
let imageService: ImageService
|
||||
|
||||
beforeAll(() => {
|
||||
console.log('图像生成API密钥检查:', {
|
||||
GEMINI_API_KEY: hasGeminiKey,
|
||||
OPENAI_API_KEY: hasOpenAIKey,
|
||||
SEEDREAM_API_KEY: hasSeedreamKey
|
||||
})
|
||||
|
||||
if (!hasGeminiKey && !hasOpenAIKey && !hasSeedreamKey) {
|
||||
console.log('跳过图像适配器真实API测试:未设置任何API密钥环境变量')
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
storage = new LocalStorageProvider()
|
||||
imageModelManager = new ImageModelManager(storage)
|
||||
imageService = new ImageService(imageModelManager)
|
||||
|
||||
await storage.clearAll()
|
||||
})
|
||||
|
||||
describe('Gemini 图像适配器测试', () => {
|
||||
const runGeminiTests = hasGeminiKey
|
||||
|
||||
it.runIf(runGeminiTests)('应该能使用Gemini 2.5 Flash Image生成图像', async () => {
|
||||
// 添加Gemini图像模型
|
||||
const geminiModel: ImageModelConfig = {
|
||||
name: 'Gemini 2.5 Flash Image',
|
||||
baseURL: undefined,
|
||||
apiKey: process.env.VITE_GEMINI_API_KEY!,
|
||||
defaultModel: 'gemini-2.5-flash-image-preview',
|
||||
enabled: true,
|
||||
provider: 'gemini',
|
||||
imgParams: {
|
||||
outputMimeType: 'image/png'
|
||||
}
|
||||
}
|
||||
await imageModelManager.addModel('test-gemini-fast', geminiModel)
|
||||
|
||||
// 生成图像
|
||||
const request: ImageRequest = {
|
||||
prompt: 'A beautiful sunset over the ocean with calm waves',
|
||||
count: 1,
|
||||
imgParams: {
|
||||
outputMimeType: 'image/png'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await imageService.generate(request, 'test-gemini-fast')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.images).toBeDefined()
|
||||
expect(result.images.length).toBe(1)
|
||||
expect(result.images[0].b64).toBeDefined()
|
||||
expect(result.images[0].b64.length).toBeGreaterThan(100)
|
||||
expect(result.images[0].mimeType).toBe('image/png')
|
||||
expect(result.metadata?.model).toBe('gemini-2.5-flash-image-preview')
|
||||
}, 60000)
|
||||
|
||||
// 仅支持 gemini-2.5-flash-image-preview,不再测试 Imagen 3.x
|
||||
|
||||
it.skipIf(!runGeminiTests)('跳过Gemini测试 - 未设置API密钥', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('OpenAI DALL-E 适配器测试', () => {
|
||||
const runOpenAITests = hasOpenAIKey
|
||||
|
||||
it.runIf(runOpenAITests)('应该能使用DALL-E 3生成图像', async () => {
|
||||
// 添加DALL-E 3模型
|
||||
const dalle3Model: ImageModelConfig = {
|
||||
name: 'DALL-E 3',
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.VITE_OPENAI_API_KEY!,
|
||||
defaultModel: 'dall-e-3',
|
||||
enabled: true,
|
||||
provider: 'openai',
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
quality: 'standard',
|
||||
style: 'vivid'
|
||||
}
|
||||
}
|
||||
await imageModelManager.addModel('test-dalle3', dalle3Model)
|
||||
|
||||
// 生成图像
|
||||
const request: ImageRequest = {
|
||||
prompt: 'A robot playing chess in a garden',
|
||||
count: 1,
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
quality: 'standard',
|
||||
style: 'vivid'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await imageService.generate(request, 'test-dalle3')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.images).toBeDefined()
|
||||
expect(result.images.length).toBe(1)
|
||||
expect(result.images[0].b64).toBeDefined()
|
||||
expect(result.images[0].b64.length).toBeGreaterThan(100)
|
||||
expect(result.metadata?.model).toBe('dall-e-3')
|
||||
}, 60000)
|
||||
|
||||
it.runIf(runOpenAITests)('应该能使用DALL-E 2生成图像', async () => {
|
||||
// 添加DALL-E 2模型
|
||||
const dalle2Model: ImageModelConfig = {
|
||||
name: 'DALL-E 2',
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.VITE_OPENAI_API_KEY!,
|
||||
defaultModel: 'dall-e-2',
|
||||
enabled: true,
|
||||
provider: 'openai',
|
||||
imgParams: {
|
||||
size: '1024x1024'
|
||||
}
|
||||
}
|
||||
await imageModelManager.addModel('test-dalle2', dalle2Model)
|
||||
|
||||
// 生成图像
|
||||
const request: ImageRequest = {
|
||||
prompt: 'A magical forest with glowing mushrooms',
|
||||
count: 1,
|
||||
imgParams: {
|
||||
size: '1024x1024'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await imageService.generate(request, 'test-dalle2')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.images).toBeDefined()
|
||||
expect(result.images.length).toBe(1)
|
||||
expect(result.images[0].b64).toBeDefined()
|
||||
expect(result.images[0].b64.length).toBeGreaterThan(100)
|
||||
expect(result.metadata?.model).toBe('dall-e-2')
|
||||
}, 60000)
|
||||
|
||||
it.skipIf(!runOpenAITests)('跳过OpenAI DALL-E测试 - 未设置API密钥', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Seedream (火山方舟) 适配器测试', () => {
|
||||
const runSeedreamTests = hasSeedreamKey
|
||||
|
||||
it.runIf(runSeedreamTests)('应该能使用Doubao Seedream 4.0生成图像', async () => {
|
||||
// 添加Seedream模型 - 使用与 curl 示例匹配的参数
|
||||
const seedreamApiKey = process.env.VITE_SEEDREAM_API_KEY ||
|
||||
process.env.VITE_ARK_API_KEY ||
|
||||
process.env.ARK_API_KEY
|
||||
const seedreamModel: ImageModelConfig = {
|
||||
name: 'Doubao Seedream 4.0',
|
||||
baseURL: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
apiKey: seedreamApiKey!,
|
||||
defaultModel: 'doubao-seedream-4-0-250828',
|
||||
enabled: true,
|
||||
provider: 'seedream',
|
||||
imgParams: {
|
||||
size: '2K', // 匹配 curl 示例
|
||||
watermark: false, // 匹配 curl 示例
|
||||
outputMimeType: 'image/png'
|
||||
}
|
||||
}
|
||||
await imageModelManager.addModel('test-seedream', seedreamModel)
|
||||
|
||||
// 生成图像 - 使用你提供的复杂科幻提示词
|
||||
const request: ImageRequest = {
|
||||
prompt: '星际穿越,黑洞,黑洞里冲出一辆快支离破碎的复古列车,抢视觉冲击力,电影大片,末日既视感,动感,对比色,oc渲染,光线追踪,动态模糊,景深,超现实主义,深蓝,画面通过细腻的丰富的色彩层次塑造主体与场景,质感真实,暗黑风背景的光影效果营造出氛围,整体兼具艺术幻想感,夸张的广角透视效果,耀光,反射,极致的光影,强引力,吞噬',
|
||||
count: 1,
|
||||
imgParams: {
|
||||
size: '2K',
|
||||
watermark: false,
|
||||
outputMimeType: 'image/png'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await imageService.generate(request, 'test-seedream')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.images).toBeDefined()
|
||||
expect(result.images.length).toBe(1)
|
||||
expect(result.images[0].b64).toBeDefined()
|
||||
expect(result.images[0].b64.length).toBeGreaterThan(100)
|
||||
expect(result.images[0].mimeType).toBe('image/png') // 验证MIME类型
|
||||
expect(result.metadata?.model).toBe('doubao-seedream-4-0-250828')
|
||||
|
||||
// 打印一些调试信息
|
||||
console.log(`[测试] 成功生成图像,base64长度: ${result.images[0].b64.length}`)
|
||||
console.log(`[测试] 模型: ${result.metadata?.model}`)
|
||||
if (result.metadata?.notes) {
|
||||
console.log(`[测试] 注释: ${result.metadata.notes.join('; ')}`)
|
||||
}
|
||||
}, 120000) // 增加超时到120秒
|
||||
|
||||
it.skipIf(!runSeedreamTests)('跳过Seedream测试 - 未设置API密钥', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('图像服务错误处理测试', () => {
|
||||
const runErrorTests = hasGeminiKey || hasOpenAIKey || hasSeedreamKey
|
||||
|
||||
it.runIf(runErrorTests)('应该能正确处理无效的API密钥', async () => {
|
||||
// 添加一个有无效API密钥的模型
|
||||
const invalidModel: ImageModelConfig = {
|
||||
name: 'Invalid Model',
|
||||
baseURL: hasGeminiKey ? undefined : 'https://api.openai.com/v1',
|
||||
apiKey: 'invalid-key',
|
||||
defaultModel: hasGeminiKey ? 'gemini-2.5-flash-image-preview' : 'dall-e-3',
|
||||
enabled: true,
|
||||
provider: hasGeminiKey ? 'gemini' : 'openai'
|
||||
}
|
||||
await imageModelManager.addModel('invalid-model', invalidModel)
|
||||
|
||||
// 尝试生成图像应该失败
|
||||
const request: ImageRequest = {
|
||||
prompt: 'Test image generation',
|
||||
count: 1
|
||||
}
|
||||
|
||||
await expect(imageService.generate(request, 'invalid-model')).rejects.toThrow()
|
||||
}, 30000)
|
||||
|
||||
it.skipIf(!runErrorTests)('跳过错误处理测试 - 未设置API密钥', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('多图像生成测试', () => {
|
||||
const runMultiImageTests = hasOpenAIKey // 使用OpenAI进行多图像测试,因为DALL-E支持较好
|
||||
|
||||
it.runIf(runMultiImageTests)('应该能生成多张图像', async () => {
|
||||
// 添加DALL-E 2模型(更稳定)
|
||||
const dalle2Model: ImageModelConfig = {
|
||||
name: 'DALL-E 2 Multi',
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.VITE_OPENAI_API_KEY!,
|
||||
defaultModel: 'dall-e-2',
|
||||
enabled: true,
|
||||
provider: 'openai'
|
||||
}
|
||||
await imageModelManager.addModel('test-dalle2-multi', dalle2Model)
|
||||
|
||||
// 生成2张图像
|
||||
const request: ImageRequest = {
|
||||
prompt: 'A simple geometric pattern',
|
||||
count: 2,
|
||||
imgParams: {
|
||||
size: '256x256' // 使用较小尺寸以节省时间和费用
|
||||
}
|
||||
}
|
||||
|
||||
const result = await imageService.generate(request, 'test-dalle2-multi')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.images).toBeDefined()
|
||||
expect(result.images.length).toBe(2)
|
||||
result.images.forEach(image => {
|
||||
expect(image.b64).toBeDefined()
|
||||
expect(image.b64.length).toBeGreaterThan(100)
|
||||
})
|
||||
}, 120000) // 多图像生成需要更长时间
|
||||
|
||||
it.skipIf(!runMultiImageTests)('跳过多图像测试 - 未设置OpenAI API密钥', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import { SiliconFlowImageAdapter } from '../../src/services/image/adapters/siliconflow-adapter'
|
||||
import { ImageRequest, ImageModelConfig } from '../../src/services/image/types'
|
||||
|
||||
describe('SiliconFlowImageAdapter Integration Test', () => {
|
||||
test('should generate image with SiliconFlow API', async () => {
|
||||
// 获取 API Key
|
||||
const apiKey = process.env.VITE_SILICONFLOW_API_KEY
|
||||
if (!apiKey) {
|
||||
console.log('⏸️ 跳过 SiliconFlow 真实 API 测试:未设置 VITE_SILICONFLOW_API_KEY 环境变量')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🚀 开始 SiliconFlow 图像生成测试...')
|
||||
console.log('API Key:', apiKey.substring(0, 20) + '...')
|
||||
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
const config: ImageModelConfig = {
|
||||
name: 'SiliconFlow Kolors Test',
|
||||
baseURL: 'https://api.siliconflow.cn/v1',
|
||||
defaultModel: 'Kwai-Kolors/Kolors',
|
||||
apiKey: apiKey,
|
||||
enabled: true,
|
||||
provider: 'siliconflow',
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
steps: 20,
|
||||
guidance: 7.5
|
||||
},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
}
|
||||
|
||||
const request: ImageRequest = {
|
||||
prompt: '星际穿越,黑洞,黑洞里冲出一辆快支离破碎的复古列车,蒸汽朋克风格,科幻电影场景,高质量,细节丰富,8K分辨率,壮观震撼',
|
||||
count: 1,
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
steps: 20,
|
||||
guidance: 7.5
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📝 请求参数:', JSON.stringify({
|
||||
prompt: request.prompt.substring(0, 50) + '...',
|
||||
count: request.count,
|
||||
imgParams: request.imgParams
|
||||
}, null, 2))
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const result = await adapter.generate(request, config)
|
||||
const endTime = Date.now()
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(1)
|
||||
|
||||
console.log(`✅ SiliconFlow 图像生成成功!耗时: ${duration}秒`)
|
||||
console.log('📊 生成结果概览:', {
|
||||
dataLength: result.data.length,
|
||||
created: new Date(result.created * 1000).toLocaleString(),
|
||||
seed: result.seed,
|
||||
usage: result.usage
|
||||
})
|
||||
|
||||
// 详细结果
|
||||
console.log('🎨 生成的图像:', result.data.map((img, index) => ({
|
||||
index: index + 1,
|
||||
hasUrl: !!img.url,
|
||||
urlLength: img.url?.length,
|
||||
hasB64: !!img.b64_json,
|
||||
revisedPrompt: img.revised_prompt
|
||||
})))
|
||||
|
||||
// 断言验证
|
||||
expect(result).toBeDefined()
|
||||
expect(result.data).toBeInstanceOf(Array)
|
||||
expect(result.data.length).toBe(1)
|
||||
expect(result.data[0]).toHaveProperty('url')
|
||||
expect(typeof result.data[0].url).toBe('string')
|
||||
expect(result.created).toBeGreaterThan(0)
|
||||
|
||||
// 验证 URL 是否可访问
|
||||
if (result.data[0].url) {
|
||||
console.log('🔗 验证图像URL可访问性...')
|
||||
const urlCheckStart = Date.now()
|
||||
const response = await fetch(result.data[0].url, { method: 'HEAD' })
|
||||
const urlCheckTime = Date.now() - urlCheckStart
|
||||
|
||||
console.log(`🌐 URL访问检查: ${response.status} (${urlCheckTime}ms)`)
|
||||
expect(response.ok).toBe(true)
|
||||
|
||||
if (response.headers.get('content-length')) {
|
||||
const sizeKB = Math.round(parseInt(response.headers.get('content-length')!) / 1024)
|
||||
console.log(`📎 图像文件大小: ${sizeKB}KB`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 SiliconFlow API 测试完全成功!')
|
||||
|
||||
} catch (error: any) {
|
||||
const endTime = Date.now()
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(1)
|
||||
|
||||
console.error('❌ SiliconFlow API 测试失败 (耗时 ' + duration + '秒):')
|
||||
console.error('错误类型:', error.constructor.name)
|
||||
console.error('错误消息:', error.message)
|
||||
|
||||
if (error.stack) {
|
||||
console.error('错误堆栈:', error.stack.split('\n').slice(0, 5).join('\n'))
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}, 60000) // 60秒超时
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { getDefaultImageModels } from '../../../src/services/image-model/defaults'
|
||||
|
||||
describe('default image models', () => {
|
||||
const env = process.env
|
||||
beforeEach(() => { process.env = { ...env } })
|
||||
|
||||
it('uses VITE_GEMINI_API_KEY for image-gemini models', () => {
|
||||
process.env.VITE_GEMINI_API_KEY = 'gemi'
|
||||
const models = getDefaultImageModels()
|
||||
expect(models['image-gemini-fast'].apiKey).toBe('gemi')
|
||||
expect(models['image-gemini-fast'].enabled).toBe(true)
|
||||
expect(models['image-gemini-quality'].apiKey).toBe('gemi')
|
||||
expect(models['image-gemini-quality'].enabled).toBe(true)
|
||||
})
|
||||
|
||||
it('prefers VITE_SEEDREAM_API_KEY for seedream', () => {
|
||||
process.env.VITE_SEEDREAM_API_KEY = 'seed'
|
||||
const models = getDefaultImageModels()
|
||||
expect(models['image-seedream'].apiKey).toBe('seed')
|
||||
expect(models['image-seedream'].enabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ImageService } from '../../../src/services/image/service'
|
||||
import type { IImageModelManager, ImageModelConfig } from '../../../src/services/image/types'
|
||||
|
||||
class MockImageModelManager implements IImageModelManager {
|
||||
async ensureInitialized() {}
|
||||
async isInitialized() { return true }
|
||||
async getAllModels() { return [] as any }
|
||||
async getModel(key: string) {
|
||||
const cfg: ImageModelConfig = {
|
||||
name: 'Gemini Image', baseURL: '', apiKey: 'k', defaultModel: 'gemini-2.5-flash-image-preview', enabled: true, provider: 'gemini'
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
async addModel() {}
|
||||
async updateModel() {}
|
||||
async deleteModel() {}
|
||||
async enableModel() {}
|
||||
async disableModel() {}
|
||||
async getEnabledModels() { return [] as any }
|
||||
async exportData() { return [] as any }
|
||||
async importData() {}
|
||||
async getDataType() { return 'image-models' }
|
||||
async validateData() { return true }
|
||||
}
|
||||
|
||||
describe('ImageService validation', () => {
|
||||
const svc = new ImageService(new MockImageModelManager())
|
||||
|
||||
it('rejects empty prompt', async () => {
|
||||
await expect(svc.generate({ prompt: '' }, 'image-gemini')).rejects.toThrow(/提示词不能为空/)
|
||||
})
|
||||
|
||||
it('rejects count > 4', async () => {
|
||||
await expect(svc.generate({ prompt: 'a', count: 5 }, 'image-gemini')).rejects.toThrow(/1~4/)
|
||||
})
|
||||
|
||||
it('rejects non png/jpeg mime', async () => {
|
||||
await expect(svc.generate({ prompt: 'a', inputImage: { b64: 'AAA', mimeType: 'image/webp' } }, 'image-gemini')).rejects.toThrow(/仅支持 PNG 或 JPEG/)
|
||||
})
|
||||
|
||||
it('rejects >10MB base64', async () => {
|
||||
// Construct a base64 string ~ >10MB
|
||||
const bytes = 10 * 1024 * 1024 + 1024
|
||||
const b64 = Buffer.alloc(bytes, 0).toString('base64')
|
||||
await expect(svc.generate({ prompt: 'a', inputImage: { b64, mimeType: 'image/png' } }, 'image-gemini')).rejects.toThrow(/不能超过 10MB/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import { SiliconFlowImageAdapter } from '../../../src/services/image/adapters/siliconflow-adapter'
|
||||
import { ImageRequest, ImageModelConfig } from '../../../src/services/image/types'
|
||||
|
||||
describe('SiliconFlowImageAdapter', () => {
|
||||
test('should create adapter with correct capabilities', () => {
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
|
||||
expect(adapter.capabilities).toEqual({
|
||||
edit: true,
|
||||
multiImage: false,
|
||||
asyncJob: false,
|
||||
streamingPreview: false
|
||||
})
|
||||
})
|
||||
|
||||
test.skip('should map image sizes correctly', () => {
|
||||
// 注意:mapImageSize 是私有方法,这里通过请求参数间接测试
|
||||
// 实际尺寸映射将在集成测试或真实 API 调用中验证
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
|
||||
// 测试支持的尺寸列表
|
||||
const sizes = adapter.getSupportedSizes()
|
||||
expect(sizes).toContain('1024x1024')
|
||||
expect(sizes).toContain('960x1280')
|
||||
expect(sizes).toContain('768x1024')
|
||||
expect(sizes).toContain('720x1440')
|
||||
expect(sizes).toContain('720x1280')
|
||||
})
|
||||
|
||||
test('should get supported sizes', () => {
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
const sizes = adapter.getSupportedSizes()
|
||||
|
||||
expect(sizes).toEqual([
|
||||
'1024x1024',
|
||||
'960x1280',
|
||||
'768x1024',
|
||||
'720x1440',
|
||||
'720x1280'
|
||||
])
|
||||
})
|
||||
|
||||
test('should generate image with real API call', async () => {
|
||||
// 跳过测试,除非设置了 API Key
|
||||
const apiKey = process.env.VITE_SILICONFLOW_API_KEY
|
||||
if (!apiKey) {
|
||||
console.log('跳过 SiliconFlow 真实 API 测试:未设置 VITE_SILICONFLOW_API_KEY')
|
||||
return
|
||||
}
|
||||
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
const config: ImageModelConfig = {
|
||||
name: 'SiliconFlow Kolors Test',
|
||||
baseURL: 'https://api.siliconflow.cn/v1',
|
||||
defaultModel: 'Kwai-Kolors/Kolors',
|
||||
apiKey: apiKey,
|
||||
enabled: true,
|
||||
provider: 'siliconflow',
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
steps: 20,
|
||||
guidance: 7.5
|
||||
},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
}
|
||||
|
||||
const request: ImageRequest = {
|
||||
prompt: '星际穿越,黑洞,黑洞里冲出一辆快支离破碎的复古列车,蒸汽朋克风格,科幻电影场景,高质量,细节丰富,8K分辨率,壮观震撼',
|
||||
count: 1,
|
||||
imgParams: {
|
||||
size: '1024x1024',
|
||||
steps: 20,
|
||||
guidance: 7.5
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
const result = await adapter.generate(request, config)
|
||||
const endTime = Date.now()
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(1)
|
||||
|
||||
console.log(`SiliconFlow 图像生成耗时: ${duration}秒`)
|
||||
console.log('生成结果:', JSON.stringify(result, null, 2))
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.data).toBeInstanceOf(Array)
|
||||
expect(result.data.length).toBe(1)
|
||||
expect(result.data[0]).toHaveProperty('url')
|
||||
expect(typeof result.data[0].url).toBe('string')
|
||||
expect(result.created).toBeGreaterThan(0)
|
||||
|
||||
// 验证 URL 是否可访问
|
||||
if (result.data[0].url) {
|
||||
const response = await fetch(result.data[0].url, { method: 'HEAD' })
|
||||
expect(response.ok).toBe(true)
|
||||
console.log('图像 URL 可访问,状态码:', response.status)
|
||||
}
|
||||
})
|
||||
|
||||
test('should throw error for missing API key', async () => {
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
const config: ImageModelConfig = {
|
||||
name: 'Test Config',
|
||||
baseURL: 'https://api.siliconflow.cn/v1',
|
||||
defaultModel: 'Kwai-Kolors/Kolors',
|
||||
apiKey: '', // 空的 API Key
|
||||
enabled: true,
|
||||
provider: 'siliconflow',
|
||||
imgParams: {},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
}
|
||||
|
||||
const request: ImageRequest = {
|
||||
prompt: 'test',
|
||||
count: 1
|
||||
}
|
||||
|
||||
await expect(adapter.generate(request, config)).rejects.toThrow('SiliconFlow: 缺少 API Key')
|
||||
})
|
||||
|
||||
test('should throw error for missing default model', async () => {
|
||||
const adapter = new SiliconFlowImageAdapter()
|
||||
const config: ImageModelConfig = {
|
||||
name: 'Test Config',
|
||||
baseURL: 'https://api.siliconflow.cn/v1',
|
||||
defaultModel: '', // 空的默认模型
|
||||
apiKey: 'test-key',
|
||||
enabled: true,
|
||||
provider: 'siliconflow',
|
||||
imgParams: {},
|
||||
capabilities: { edit: true, multiImage: false, asyncJob: false, streamingPreview: false }
|
||||
}
|
||||
|
||||
const request: ImageRequest = {
|
||||
prompt: 'test',
|
||||
count: 1
|
||||
}
|
||||
|
||||
await expect(adapter.generate(request, config)).rejects.toThrow('SiliconFlow: 缺少默认模型')
|
||||
})
|
||||
})
|
||||
@@ -41,6 +41,8 @@ const {
|
||||
createHistoryManager,
|
||||
createLLMService,
|
||||
createPromptService,
|
||||
createImageModelManager,
|
||||
createImageService,
|
||||
createTemplateLanguageService,
|
||||
createDataManager,
|
||||
createContextRepo,
|
||||
@@ -79,6 +81,7 @@ function safeSerialize(obj) {
|
||||
|
||||
let mainWindow;
|
||||
let modelManager, templateManager, historyManager, llmService, promptService, templateLanguageService, preferenceService, dataManager, contextRepo;
|
||||
let imageModelManager, imageService;
|
||||
let storageProvider; // 全局存储提供器引用,用于退出时保存数据
|
||||
let isQuitting = false; // 防止重复保存数据的标志
|
||||
let isUpdaterQuitting = false; // 标识是否为更新安装退出,跳过数据保存
|
||||
@@ -456,6 +459,10 @@ async function initializeServices() {
|
||||
|
||||
console.log('[DESKTOP] Initializing model manager...');
|
||||
await modelManager.ensureInitialized();
|
||||
// 图像模型管理器
|
||||
console.log('[DESKTOP] Creating image model manager...');
|
||||
imageModelManager = createImageModelManager(storageProvider);
|
||||
await imageModelManager.ensureInitialized();
|
||||
|
||||
// 在创建任何网络相关服务前,先根据系统代理设置 undici 全局分发器
|
||||
await setupGlobalProxyDispatcherFromSystem();
|
||||
@@ -465,6 +472,8 @@ async function initializeServices() {
|
||||
|
||||
console.log('[DESKTOP] Creating Prompt service...');
|
||||
promptService = createPromptService(modelManager, llmService, templateManager, historyManager);
|
||||
console.log('[DESKTOP] Creating Image service...');
|
||||
imageService = createImageService(imageModelManager);
|
||||
|
||||
console.log('[DESKTOP] Creating Context repository...');
|
||||
contextRepo = createContextRepo(storageProvider);
|
||||
@@ -845,6 +854,75 @@ function setupIPC() {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== Image Model handlers =====
|
||||
ipcMain.handle('image-model-ensureInitialized', async () => {
|
||||
try { await imageModelManager.ensureInitialized(); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-isInitialized', async () => {
|
||||
try { const r = await imageModelManager.isInitialized(); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-getAllModels', async () => {
|
||||
try { const r = await imageModelManager.getAllModels(); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-getModel', async (e, key) => {
|
||||
try { const r = await imageModelManager.getModel(key); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-addModel', async (e, model) => {
|
||||
try { const safeModel = safeSerialize(model); const { key, ...cfg } = safeModel; await imageModelManager.addModel(key, cfg); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-updateModel', async (e, key, updates) => {
|
||||
try { const safe = safeSerialize(updates); await imageModelManager.updateModel(key, safe); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-deleteModel', async (e, key) => {
|
||||
try { await imageModelManager.deleteModel(key); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-enableModel', async (e, key) => {
|
||||
try { await imageModelManager.enableModel(key); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-disableModel', async (e, key) => {
|
||||
try { await imageModelManager.disableModel(key); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-getEnabledModels', async () => {
|
||||
try { const r = await imageModelManager.getEnabledModels(); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-exportData', async () => {
|
||||
try { const r = await imageModelManager.exportData(); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-importData', async (e, data) => {
|
||||
try { const safe = safeSerialize(data); await imageModelManager.importData(safe); return createSuccessResponse(null) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-getDataType', async () => {
|
||||
try { const r = await imageModelManager.getDataType(); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
ipcMain.handle('image-model-validateData', async (e, data) => {
|
||||
try { const safe = safeSerialize(data); const r = await imageModelManager.validateData(safe); return createSuccessResponse(r) }
|
||||
catch (error) { return createErrorResponse(error) }
|
||||
})
|
||||
|
||||
// ===== Image Service handlers =====
|
||||
ipcMain.handle('image-generate', async (e, request, modelKey) => {
|
||||
try {
|
||||
const safeReq = safeSerialize(request)
|
||||
const res = await imageService.generate(safeReq, modelKey)
|
||||
return createSuccessResponse(res)
|
||||
} catch (error) {
|
||||
return createErrorResponse(error)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('model-importData', async (event, data) => {
|
||||
try {
|
||||
// 清理Vue响应式对象,防止IPC序列化错误
|
||||
@@ -1356,6 +1434,9 @@ function setupIPC() {
|
||||
VITE_DEEPSEEK_API_KEY: process.env.VITE_DEEPSEEK_API_KEY || '',
|
||||
VITE_SILICONFLOW_API_KEY: process.env.VITE_SILICONFLOW_API_KEY || '',
|
||||
VITE_ZHIPU_API_KEY: process.env.VITE_ZHIPU_API_KEY || '',
|
||||
// 图像模型相关(Seedream / ARK)
|
||||
VITE_SEEDREAM_API_KEY: process.env.VITE_SEEDREAM_API_KEY || '',
|
||||
VITE_ARK_API_KEY: process.env.VITE_ARK_API_KEY || '',
|
||||
VITE_CUSTOM_API_KEY: process.env.VITE_CUSTOM_API_KEY || '',
|
||||
VITE_CUSTOM_API_BASE_URL: process.env.VITE_CUSTOM_API_BASE_URL || '',
|
||||
VITE_CUSTOM_API_MODEL: process.env.VITE_CUSTOM_API_MODEL || ''
|
||||
|
||||
@@ -236,6 +236,82 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
},
|
||||
},
|
||||
|
||||
// Image Model Manager interface
|
||||
imageModel: {
|
||||
ensureInitialized: async () => {
|
||||
const result = await ipcRenderer.invoke('image-model-ensureInitialized');
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
isInitialized: async () => {
|
||||
const result = await ipcRenderer.invoke('image-model-isInitialized');
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
getAllModels: async () => {
|
||||
const result = await ipcRenderer.invoke('image-model-getAllModels');
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
getModel: async (key) => {
|
||||
const result = await ipcRenderer.invoke('image-model-getModel', key);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
addModel: async (model) => {
|
||||
const result = await ipcRenderer.invoke('image-model-addModel', model);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
updateModel: async (key, updates) => {
|
||||
const result = await ipcRenderer.invoke('image-model-updateModel', key, updates);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
deleteModel: async (key) => {
|
||||
const result = await ipcRenderer.invoke('image-model-deleteModel', key);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
enableModel: async (key) => {
|
||||
const result = await ipcRenderer.invoke('image-model-enableModel', key);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
disableModel: async (key) => {
|
||||
const result = await ipcRenderer.invoke('image-model-disableModel', key);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
getEnabledModels: async () => {
|
||||
const result = await ipcRenderer.invoke('image-model-getEnabledModels');
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
exportData: async () => {
|
||||
const result = await ipcRenderer.invoke('image-model-exportData');
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
importData: async (data) => {
|
||||
const result = await ipcRenderer.invoke('image-model-importData', data);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
},
|
||||
getDataType: async () => {
|
||||
const result = await ipcRenderer.invoke('image-model-getDataType');
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
validateData: async (data) => {
|
||||
const result = await ipcRenderer.invoke('image-model-validateData', data);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
},
|
||||
},
|
||||
|
||||
// Image Service interface
|
||||
image: {
|
||||
generate: async (request, modelKey) => {
|
||||
const result = await ipcRenderer.invoke('image-generate', request, modelKey);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return result.data;
|
||||
}
|
||||
},
|
||||
|
||||
// Template Manager interface
|
||||
template: {
|
||||
// Get all templates
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"default_popup": "index.html",
|
||||
"default_title": "__MSG_extName__"
|
||||
},
|
||||
"background": {
|
||||
@@ -26,7 +25,7 @@
|
||||
"homepage_url": "https://github.com/linshenkx/prompt-optimizer",
|
||||
"author": "linshenkx",
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self';"
|
||||
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline';"
|
||||
},
|
||||
"offline_enabled": true,
|
||||
"incognito": "split",
|
||||
|
||||
+1099
-1042
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,68 @@
|
||||
<!-- 功能模式选择器组件 - 使用 Naive UI RadioGroup -->
|
||||
<template>
|
||||
<NRadioGroup
|
||||
:value="modelValue"
|
||||
@update:value="updateFunctionMode"
|
||||
size="small"
|
||||
class="function-mode-selector"
|
||||
>
|
||||
<NRadioButton
|
||||
value="basic"
|
||||
:title="t('nav.basicMode')"
|
||||
>
|
||||
{{ t('nav.basicMode') }}
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
value="pro"
|
||||
:title="t('nav.contextMode')"
|
||||
>
|
||||
{{ t('nav.contextMode') }}
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
value="image"
|
||||
:title="t('nav.imageMode')"
|
||||
>
|
||||
{{ t('nav.imageMode') }}
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NRadioGroup, NRadioButton } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
modelValue: 'basic' | 'pro' | 'image'
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: 'basic' | 'pro' | 'image'): void
|
||||
(e: 'change', value: 'basic' | 'pro' | 'image'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 更新功能模式
|
||||
*/
|
||||
const updateFunctionMode = (mode: 'basic' | 'pro' | 'image') => {
|
||||
emit('update:modelValue', mode)
|
||||
emit('change', mode)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 响应式设计 - 移动端全宽显示 */
|
||||
@media (max-width: 640px) {
|
||||
.function-mode-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.function-mode-selector :deep(.n-radio-button) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,15 +33,23 @@
|
||||
<NText depth="3" style="font-size: 14px;">
|
||||
{{ t('common.createdAt') }} {{ formatDate(chain.rootRecord.timestamp) }}
|
||||
</NText>
|
||||
<!-- 功能模式标签 -->
|
||||
<NTag
|
||||
v-if="chain.rootRecord.type === 'optimize'"
|
||||
:type="getFunctionModeTagType(chain.rootRecord.type)"
|
||||
size="small"
|
||||
>
|
||||
{{ getFunctionModeLabel(chain.rootRecord.type) }}
|
||||
</NTag>
|
||||
<!-- 优化模式标签 -->
|
||||
<NTag
|
||||
v-if="chain.rootRecord.type === 'optimize' || chain.rootRecord.type === 'contextSystemOptimize'"
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
{{ t('common.system') }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="chain.rootRecord.type === 'userOptimize'"
|
||||
v-if="chain.rootRecord.type === 'userOptimize' || chain.rootRecord.type === 'contextUserOptimize'"
|
||||
type="success"
|
||||
size="small"
|
||||
>
|
||||
@@ -178,10 +186,11 @@ const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
(e: 'reuse', context: {
|
||||
record: PromptRecord,
|
||||
(e: 'reuse', context: {
|
||||
record: PromptRecord,
|
||||
chainId: string,
|
||||
rootPrompt: string
|
||||
rootPrompt: string,
|
||||
chain: PromptRecordChain
|
||||
}): void
|
||||
(e: 'clear'): void
|
||||
(e: 'deleteChain', chainId: string): void
|
||||
@@ -247,6 +256,28 @@ const truncateText = (text: string, maxLength: number) => {
|
||||
return text.slice(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
// 获取功能模式标签类型
|
||||
const getFunctionModeTagType = (recordType: string) => {
|
||||
if (recordType.includes('image')) {
|
||||
return 'warning'
|
||||
} else if (recordType.includes('context')) {
|
||||
return 'primary'
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取功能模式标签文本
|
||||
const getFunctionModeLabel = (recordType: string) => {
|
||||
if (recordType.includes('image')) {
|
||||
return t('nav.imageMode')
|
||||
} else if (recordType.includes('context')) {
|
||||
return t('nav.contextMode')
|
||||
} else {
|
||||
return t('nav.basicMode')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加删除单条记录的方法
|
||||
const deleteChain = (chainId: string) => {
|
||||
if (confirm(t('history.confirmDeleteChain'))) {
|
||||
|
||||
@@ -11,22 +11,30 @@
|
||||
<!-- 顶部导航栏 -->
|
||||
<NLayoutHeader class="theme-header nav-header-enhanced">
|
||||
<NFlex justify="space-between" align="center" class="w-full nav-content" :wrap="false" :size="[16, 12]">
|
||||
<!-- 左侧:Logo + 标题 -->
|
||||
<NFlex align="center" :size="8" :wrap="false">
|
||||
<NImage
|
||||
:src="logoSrc"
|
||||
alt="Logo"
|
||||
:width="logoSize"
|
||||
:height="logoSize"
|
||||
object-fit="cover"
|
||||
class="logo-image"
|
||||
:show-toolbar="false"
|
||||
:preview-disabled="true"
|
||||
:fallback-src="fallbackLogoSrc"
|
||||
/>
|
||||
<NText class="text-lg sm:text-xl font-bold theme-title" tag="h2">
|
||||
<slot name="title">{{ t('common.appName') }}</slot>
|
||||
</NText>
|
||||
<!-- 左侧:Logo + 标题 + 核心导航 -->
|
||||
<NFlex align="center" :size="16" :wrap="false">
|
||||
<!-- Logo + 标题 -->
|
||||
<NFlex align="center" :size="8" :wrap="false">
|
||||
<NImage
|
||||
:src="logoSrc"
|
||||
alt="Logo"
|
||||
:width="logoSize"
|
||||
:height="logoSize"
|
||||
object-fit="cover"
|
||||
class="logo-image"
|
||||
:show-toolbar="false"
|
||||
:preview-disabled="true"
|
||||
:fallback-src="fallbackLogoSrc"
|
||||
/>
|
||||
<NText class="text-lg sm:text-xl font-bold theme-title" tag="h2">
|
||||
<slot name="title">{{ t('common.appName') }}</slot>
|
||||
</NText>
|
||||
</NFlex>
|
||||
|
||||
<!-- 核心导航元素 -->
|
||||
<div class="core-navigation">
|
||||
<slot name="core-nav"></slot>
|
||||
</div>
|
||||
</NFlex>
|
||||
|
||||
<!-- 右侧:操作按钮 -->
|
||||
@@ -141,11 +149,65 @@ const logoSize = computed(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 核心导航样式 */
|
||||
.core-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--border-color, rgba(239, 239, 245, 0.6));
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.core-navigation :deep(.function-mode-selector) {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.core-navigation :deep(.n-radio-group) {
|
||||
background: var(--modal-color, #fff);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--border-color, rgba(239, 239, 245, 0.6));
|
||||
}
|
||||
|
||||
.core-navigation :deep(.n-radio-button) {
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.core-navigation :deep(.n-radio-button--checked) {
|
||||
background: var(--primary-color) !important;
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.core-navigation :deep(.n-radio-button:not(.n-radio-button--checked):hover) {
|
||||
background: var(--hover-color, rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 639px) {
|
||||
.logo-image {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.core-navigation {
|
||||
margin-left: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.core-navigation :deep(.function-mode-selector) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.core-navigation :deep(.n-radio-button) {
|
||||
min-width: 48px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<template #header-extra>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="showAddForm = true"
|
||||
@click="openAddForActiveTab"
|
||||
ghost
|
||||
>
|
||||
<template #icon>
|
||||
@@ -23,6 +23,12 @@
|
||||
</template>
|
||||
|
||||
<NScrollbar style="max-height: 75vh;">
|
||||
<NTabs v-model:value="activeTab" type="line" size="small" style="margin-bottom: 12px;">
|
||||
<NTabPane name="text" tab="文本模型" />
|
||||
<NTabPane name="image" tab="图像模型" />
|
||||
</NTabs>
|
||||
|
||||
<template v-if="activeTab === 'text'">
|
||||
<NSpace vertical :size="12">
|
||||
<NCard
|
||||
v-for="model in models"
|
||||
@@ -111,9 +117,119 @@
|
||||
</template>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<NSpace vertical :size="12">
|
||||
<NCard
|
||||
v-for="im in imageModels"
|
||||
:key="im.key"
|
||||
hoverable
|
||||
:style="{ opacity: im.enabled ? 1 : 0.6 }"
|
||||
>
|
||||
<template #header>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<NSpace vertical :size="4">
|
||||
<NSpace align="center">
|
||||
<NText strong>{{ im.name }}</NText>
|
||||
<NTag size="small">{{ im.provider }}</NTag>
|
||||
<NTag v-if="!im.enabled" type="warning" size="small">{{ t('modelManager.disabled') }}</NTag>
|
||||
</NSpace>
|
||||
<NText depth="3" style="font-size: 14px;">{{ im.defaultModel }}</NText>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NSpace @click.stop>
|
||||
<NButton size="small" quaternary disabled>
|
||||
<span class="hidden md:inline">测试连接(暂不可用)</span>
|
||||
</NButton>
|
||||
<NButton @click="editImageModel(im.key)" size="small" quaternary>
|
||||
{{ t('modelManager.editModel') }}
|
||||
</NButton>
|
||||
<NButton @click="im.enabled ? disableImageModel(im.key) : enableImageModel(im.key)"
|
||||
size="small"
|
||||
:type="im.enabled ? 'warning' : 'success'"
|
||||
quaternary>
|
||||
{{ im.enabled ? t('common.disable') : t('common.enable') }}
|
||||
</NButton>
|
||||
<NButton v-if="!isDefaultImageModel(im.key)" @click="deleteImageModel(im.key)" size="small" type="error" quaternary>
|
||||
{{ t('common.delete') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NScrollbar>
|
||||
</NModal>
|
||||
|
||||
<!-- 图像模型:编辑弹窗 -->
|
||||
<NModal
|
||||
:show="isEditingImage"
|
||||
preset="card"
|
||||
:style="{ width: '720px' }"
|
||||
title="编辑图像模型"
|
||||
:bordered="false"
|
||||
:segmented="true"
|
||||
@update:show="(v) => !v && (isEditingImage = false)"
|
||||
>
|
||||
<NScrollbar style="max-height: 60vh;" v-if="editingImageModel">
|
||||
<NSpace vertical :size="12">
|
||||
<NInput v-model:value="editingImageModel.name" placeholder="显示名称" />
|
||||
<NSelect v-model:value="editingImageModel.provider" :options="[
|
||||
{ label: 'Gemini', value: 'gemini' },
|
||||
{ label: 'Seedream', value: 'seedream' },
|
||||
{ label: 'OpenAI', value: 'openai' },
|
||||
{ label: 'Custom', value: 'custom' }
|
||||
]" />
|
||||
<NInput v-model:value="editingImageModel.baseURL" placeholder="API Base URL(可选)" />
|
||||
<NInput v-model:value="editingImageModel.apiKey" placeholder="API Key(可选)" type="password" />
|
||||
<NInput v-model:value="editingImageModel.defaultModel" placeholder="默认模型ID" />
|
||||
<NCheckbox v-model:checked="editingImageModel.enabled">{{ t('common.enable') }}</NCheckbox>
|
||||
</NSpace>
|
||||
</NScrollbar>
|
||||
<template #action>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="isEditingImage = false">{{ t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="saveImageEdit">{{ t('common.save') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
<!-- 图像模型:新增弹窗 -->
|
||||
<NModal
|
||||
:show="showAddImageForm"
|
||||
preset="card"
|
||||
:style="{ width: '720px' }"
|
||||
title="新增图像模型"
|
||||
:bordered="false"
|
||||
:segmented="true"
|
||||
@update:show="(v) => !v && (showAddImageForm = false)"
|
||||
>
|
||||
<NScrollbar style="max-height: 60vh;">
|
||||
<NSpace vertical :size="12">
|
||||
<NInput v-model:value="newImageModel.key" placeholder="模型Key(唯一标识)" />
|
||||
<NInput v-model:value="newImageModel.name" placeholder="显示名称" />
|
||||
<NSelect v-model:value="newImageModel.provider" :options="[
|
||||
{ label: 'Gemini', value: 'gemini' },
|
||||
{ label: 'Seedream', value: 'seedream' },
|
||||
{ label: 'OpenAI', value: 'openai' },
|
||||
{ label: 'Custom', value: 'custom' }
|
||||
]" />
|
||||
<NInput v-model:value="newImageModel.baseURL" placeholder="API Base URL(可选)" />
|
||||
<NInput v-model:value="newImageModel.apiKey" placeholder="API Key(可选)" type="password" />
|
||||
<NInput v-model:value="newImageModel.defaultModel" placeholder="默认模型ID" />
|
||||
</NSpace>
|
||||
</NScrollbar>
|
||||
<template #action>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="showAddImageForm = false">{{ t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="addImageModel">{{ t('common.create') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
<!-- 编辑模型弹窗 - 独立的顶级模态框 -->
|
||||
<NModal
|
||||
:show="isEditing"
|
||||
@@ -646,7 +762,7 @@ import { ref, onMounted, watch, computed, inject } from 'vue'; // Added computed
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
NModal, NScrollbar, NSpace, NCard, NText, NH3, NH4, NTag, NButton,
|
||||
NInput, NInputNumber, NCheckbox, NDivider, NSelect
|
||||
NInput, NInputNumber, NCheckbox, NDivider, NSelect, NTabs, NTabPane
|
||||
} from 'naive-ui';
|
||||
import {
|
||||
createLLMService,
|
||||
@@ -678,6 +794,22 @@ const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// 活动标签(文本/图像)
|
||||
const activeTab = ref('text')
|
||||
|
||||
// 图像模型区状态
|
||||
const imageModels = ref([])
|
||||
const isEditingImage = ref(false)
|
||||
const editingImageModel = ref(null)
|
||||
const showAddImageForm = ref(false)
|
||||
const newImageModel = ref({ key: '', name: '', provider: 'gemini', baseURL: '', apiKey: '', defaultModel: '', enabled: true })
|
||||
|
||||
// 打开新增弹窗(根据活动标签)
|
||||
const openAddForActiveTab = () => {
|
||||
if (activeTab.value === 'image') showAddImageForm.value = true
|
||||
else showAddForm.value = true
|
||||
}
|
||||
|
||||
// 通过依赖注入获取服务
|
||||
const services = inject('services');
|
||||
if (!services) {
|
||||
@@ -685,6 +817,7 @@ if (!services) {
|
||||
}
|
||||
const modelManager = services.value.modelManager;
|
||||
const llmService = services.value.llmService;
|
||||
const imageModelManager = services.value.imageModelManager;
|
||||
|
||||
// =============== 状态变量 ===============
|
||||
// UI状态
|
||||
@@ -860,6 +993,52 @@ const disableModel = async (key) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 图像模型:加载/增删改/启用禁用 =====
|
||||
const loadImageModels = async () => {
|
||||
try {
|
||||
const list = await imageModelManager.getAllModels()
|
||||
imageModels.value = JSON.parse(JSON.stringify(list))
|
||||
} catch (e) {
|
||||
console.error('加载图像模型失败:', e)
|
||||
}
|
||||
}
|
||||
const isDefaultImageModel = (key) => ['image-gemini', 'image-seedream'].includes(key)
|
||||
const enableImageModel = async (key) => {
|
||||
try { await imageModelManager.enableModel(key); await loadImageModels(); toast.success(t('modelManager.enableSuccess')) }
|
||||
catch (e) { toast.error(t('modelManager.enableFailed', { error: e.message })) }
|
||||
}
|
||||
const disableImageModel = async (key) => {
|
||||
try { await imageModelManager.disableModel(key); await loadImageModels(); toast.success(t('modelManager.disableSuccess')) }
|
||||
catch (e) { toast.error(t('modelManager.disableFailed', { error: e.message })) }
|
||||
}
|
||||
const deleteImageModel = async (key) => {
|
||||
if (!confirm(t('modelManager.deleteConfirm'))) return
|
||||
try { await imageModelManager.deleteModel(key); await loadImageModels(); toast.success(t('modelManager.deleteSuccess')) }
|
||||
catch (e) { toast.error(t('modelManager.deleteFailed', { error: e.message })) }
|
||||
}
|
||||
const editImageModel = async (key) => {
|
||||
const m = await imageModelManager.getModel(key)
|
||||
if (!m) return
|
||||
editingImageModel.value = { originalKey: key, name: m.name, provider: m.provider || 'gemini', baseURL: m.baseURL || '', apiKey: m.apiKey || '', defaultModel: m.defaultModel || '', enabled: m.enabled }
|
||||
isEditingImage.value = true
|
||||
}
|
||||
const saveImageEdit = async () => {
|
||||
const e = editingImageModel.value
|
||||
if (!e?.originalKey) return
|
||||
try {
|
||||
await imageModelManager.updateModel(e.originalKey, { name: e.name, provider: e.provider, baseURL: e.baseURL, apiKey: e.apiKey, defaultModel: e.defaultModel, enabled: e.enabled })
|
||||
isEditingImage.value = false; editingImageModel.value = null; await loadImageModels(); toast.success((t('common.save') || '保存') + '成功')
|
||||
} catch (err) { const msg = err && err.message ? err.message : String(err); toast.error((t('common.save') || '保存') + '失败: ' + msg) }
|
||||
}
|
||||
const addImageModel = async () => {
|
||||
const n = newImageModel.value
|
||||
if (!n.key) { toast.error(t('modelManager.modelKeyPlaceholder')); return }
|
||||
try {
|
||||
await imageModelManager.addModel(n.key, { name: n.name, provider: n.provider, baseURL: n.baseURL, apiKey: n.apiKey, defaultModel: n.defaultModel, enabled: true, imgParams: {} })
|
||||
showAddImageForm.value = false; newImageModel.value = { key: '', name: '', provider: 'gemini', baseURL: '', apiKey: '', defaultModel: '', enabled: true }; await loadImageModels(); toast.success(t('modelManager.addSuccess') || '创建成功')
|
||||
} catch (err) { const msg = err && err.message ? err.message : String(err); toast.error(t('modelManager.addFailed', { error: msg }) || ('创建失败: ' + msg)) }
|
||||
}
|
||||
|
||||
// =============== 编辑相关函数 ===============
|
||||
// 编辑模型
|
||||
const editModel = async (key) => {
|
||||
@@ -1380,6 +1559,7 @@ onMounted(() => {
|
||||
loadModels();
|
||||
checkVercelProxy();
|
||||
checkDockerProxy();
|
||||
if (imageModelManager) loadImageModels();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1403,4 +1583,4 @@ onMounted(() => {
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -193,13 +193,18 @@ const props = defineProps({
|
||||
advancedModeEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 🆕 允许外部指定迭代模板类型(基础/上下文/图像),默认保持原行为
|
||||
iterateTemplateType: {
|
||||
type: String as () => 'iterate' | 'contextIterate' | 'imageIterate',
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:optimizedPrompt': [value: string];
|
||||
'iterate': [payload: IteratePayload];
|
||||
'openTemplateManager': [type: 'optimize' | 'userOptimize' | 'iterate'];
|
||||
'openTemplateManager': [type: 'optimize' | 'userOptimize' | 'iterate' | 'imageIterate' | 'contextIterate'];
|
||||
'update:selectedIterateTemplate': [template: Template | null];
|
||||
'switchVersion': [version: PromptRecord];
|
||||
'templateSelect': [template: Template];
|
||||
@@ -207,8 +212,8 @@ const emit = defineEmits<{
|
||||
|
||||
const showIterateInput = ref(false)
|
||||
const iterateInput = ref('')
|
||||
const templateType = computed<'iterate' | 'contextIterate'>(() => {
|
||||
return props.advancedModeEnabled ? 'contextIterate' : 'iterate'
|
||||
const templateType = computed<'iterate' | 'contextIterate' | 'imageIterate'>(() => {
|
||||
return (props.iterateTemplateType as any) || (props.advancedModeEnabled ? 'contextIterate' : 'iterate')
|
||||
})
|
||||
|
||||
const outputDisplayRef = ref<InstanceType<typeof OutputDisplay> | null>(null);
|
||||
|
||||
@@ -60,6 +60,25 @@
|
||||
{{ `🔄 ${t('templateManager.iterateTemplatesContext')}` }}
|
||||
</NButton>
|
||||
</NGridItem>
|
||||
|
||||
<!-- 图像 · 文生图 -->
|
||||
<NGridItem>
|
||||
<NButton block :type="currentCategory==='image-text2image-optimize' ? 'primary' : 'default'" @click="currentCategory='image-text2image-optimize'">
|
||||
{{ `🖼️ ${t('templateManager.imageText2ImageTemplates')}` }}
|
||||
</NButton>
|
||||
</NGridItem>
|
||||
<!-- 图像 · 图生图 -->
|
||||
<NGridItem>
|
||||
<NButton block :type="currentCategory==='image-image2image-optimize' ? 'primary' : 'default'" @click="currentCategory='image-image2image-optimize'">
|
||||
{{ `📷 ${t('templateManager.imageImage2ImageTemplates')}` }}
|
||||
</NButton>
|
||||
</NGridItem>
|
||||
<!-- 图像 · 迭代 -->
|
||||
<NGridItem>
|
||||
<NButton block :type="currentCategory==='image-iterate' ? 'primary' : 'default'" @click="currentCategory='image-iterate'">
|
||||
{{ `🌀 ${t('templateManager.imageIterateTemplates')}` }}
|
||||
</NButton>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
|
||||
<!-- 模板列表 -->
|
||||
@@ -574,7 +593,7 @@ const props = defineProps<{
|
||||
selectedSystemOptimizeTemplate?: Template,
|
||||
selectedUserOptimizeTemplate?: Template,
|
||||
selectedIterateTemplate?: Template,
|
||||
templateType: 'optimize' | 'userOptimize' | 'iterate',
|
||||
templateType: 'optimize' | 'userOptimize' | 'iterate' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate',
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
@@ -638,13 +657,19 @@ function getCategoryFromProps() {
|
||||
return 'user-optimize'
|
||||
case 'iterate':
|
||||
return 'iterate'
|
||||
case 'text2imageOptimize':
|
||||
return 'image-text2image-optimize'
|
||||
case 'image2imageOptimize':
|
||||
return 'image-image2image-optimize'
|
||||
case 'imageIterate':
|
||||
return 'image-iterate'
|
||||
default:
|
||||
return 'system-optimize'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前模板类型 - 根据当前分类而不是props
|
||||
function getCurrentTemplateType(): 'optimize' | 'userOptimize' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate' {
|
||||
function getCurrentTemplateType(): 'optimize' | 'userOptimize' | 'iterate' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate' {
|
||||
switch (currentCategory.value) {
|
||||
case 'system-optimize':
|
||||
return 'optimize'
|
||||
@@ -652,6 +677,12 @@ function getCurrentTemplateType(): 'optimize' | 'userOptimize' | 'iterate' | 'co
|
||||
return 'userOptimize'
|
||||
case 'iterate':
|
||||
return 'iterate'
|
||||
case 'image-text2image-optimize':
|
||||
return 'text2imageOptimize'
|
||||
case 'image-image2image-optimize':
|
||||
return 'image2imageOptimize'
|
||||
case 'image-iterate':
|
||||
return 'imageIterate'
|
||||
case 'context-system-optimize':
|
||||
return 'contextSystemOptimize'
|
||||
case 'context-user-optimize':
|
||||
@@ -677,6 +708,12 @@ function getCurrentCategoryLabel() {
|
||||
return t('templateManager.userOptimizeTemplateList')
|
||||
case 'iterate':
|
||||
return t('templateManager.iterateTemplateList')
|
||||
case 'image-text2image-optimize':
|
||||
return t('templateManager.imageText2ImageTemplates')
|
||||
case 'image-image2image-optimize':
|
||||
return t('templateManager.imageImage2ImageTemplates')
|
||||
case 'image-iterate':
|
||||
return t('templateManager.imageIterateTemplates')
|
||||
case 'context-system-optimize':
|
||||
return t('templateManager.optimizeTemplateList') + ' (Pro)'
|
||||
case 'context-user-optimize':
|
||||
@@ -1104,6 +1141,14 @@ const filteredTemplates = computed(() => {
|
||||
// 迭代优化模板:iterate类型
|
||||
return templateType === 'iterate'
|
||||
|
||||
// 图像类模板
|
||||
case 'image-text2image-optimize':
|
||||
return templateType === 'text2imageOptimize'
|
||||
case 'image-image2image-optimize':
|
||||
return templateType === 'image2imageOptimize'
|
||||
case 'image-iterate':
|
||||
return templateType === 'imageIterate'
|
||||
|
||||
case 'context-system-optimize':
|
||||
// 上下文-系统优化模板
|
||||
return templateType === 'contextSystemOptimize'
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
import { ref, computed, watch, nextTick, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NSelect, NButton, NSpace, NText } from 'naive-ui'
|
||||
import type { OptimizationMode, ITemplateManager, Template } from '@prompt-optimizer/core'
|
||||
import type { OptimizationMode, ITemplateManager, Template, TemplateMetadata } from '@prompt-optimizer/core'
|
||||
import type { AppServices } from '../types/services'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
type TemplateType = 'optimize' | 'userOptimize' | 'iterate' | 'contextSystemOptimize' | 'contextUserOptimize' | 'contextIterate';
|
||||
type TemplateType = TemplateMetadata['templateType'];
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -50,7 +50,7 @@ const props = defineProps({
|
||||
type: String as () => TemplateType,
|
||||
required: true,
|
||||
validator: (value: string): boolean => (
|
||||
['optimize', 'userOptimize', 'iterate', 'contextSystemOptimize', 'contextUserOptimize', 'contextIterate'] as string[]
|
||||
['optimize', 'userOptimize', 'text2imageOptimize', 'image2imageOptimize', 'imageIterate', 'iterate', 'contextSystemOptimize', 'contextUserOptimize', 'contextIterate'] as string[]
|
||||
).includes(value)
|
||||
},
|
||||
optimizationMode: {
|
||||
@@ -161,7 +161,7 @@ const loadTemplatesByType = async () => {
|
||||
}
|
||||
|
||||
// 统一使用异步方法,立即抛错不静默处理
|
||||
const typeTemplates = await templateManager.value.listTemplatesByType(props.type)
|
||||
const typeTemplates = await templateManager.value.listTemplatesByType(props.type as any)
|
||||
templates.value.splice(0, templates.value.length, ...typeTemplates)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<NButtonGroup>
|
||||
<NButton
|
||||
:type="modelValue === 'text2image' ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleModeChange('text2image')"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ t('imageMode.text2image') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
:type="modelValue === 'image2image' ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleModeChange('image2image')"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ t('imageMode.image2image') }}
|
||||
</NButton>
|
||||
</NButtonGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NButtonGroup, NButton } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type ImageMode = 'text2image' | 'image2image'
|
||||
|
||||
interface Props {
|
||||
modelValue: ImageMode
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: ImageMode): void
|
||||
(e: 'change', value: ImageMode): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const handleModeChange = (mode: ImageMode) => {
|
||||
if (props.disabled || props.modelValue === mode) return
|
||||
|
||||
emit('update:modelValue', mode)
|
||||
emit('change', mode)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<!-- 使用NFlex实现水平左右布局,参考App.vue的实现 -->
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
gap: '16px',
|
||||
overflow: 'hidden'
|
||||
}"
|
||||
>
|
||||
<!-- 左侧:提示词优化区域(文本模型) -->
|
||||
<NFlex vertical :style="{ flex: 1, overflow: 'auto', height: '100%' }" size="medium">
|
||||
<!-- 输入控制区域 - 对齐InputPanel布局 -->
|
||||
<NCard :style="{ flexShrink: 0 }">
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 标题区域 -->
|
||||
<NFlex justify="space-between" align="center" :wrap="false">
|
||||
<NText :depth="1" style="font-size: 18px; font-weight: 500;">{{ t('imageWorkspace.input.originalPrompt') }}</NText>
|
||||
<NFlex align="center" :size="12">
|
||||
<!-- 图像模式选择器 -->
|
||||
<ImageModeSelector
|
||||
v-model="imageMode"
|
||||
:disabled="isOptimizing"
|
||||
@change="handleImageModeChange"
|
||||
/>
|
||||
<NButton
|
||||
type="tertiary"
|
||||
size="small"
|
||||
@click="openFullscreen"
|
||||
:title="t('common.expand')"
|
||||
ghost
|
||||
round
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
||||
</svg>
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<NInput
|
||||
v-model:value="originalPrompt"
|
||||
type="textarea"
|
||||
:placeholder="t('imageWorkspace.input.originalPromptPlaceholder')"
|
||||
:rows="4"
|
||||
:autosize="{ minRows: 4, maxRows: 12 }"
|
||||
:maxlength="2000"
|
||||
clearable
|
||||
show-count
|
||||
:disabled="isOptimizing"
|
||||
/>
|
||||
|
||||
<!-- 图片上传区域 - 仅在图生图模式显示 -->
|
||||
<NSpace v-if="imageMode === 'image2image'" vertical :size="8">
|
||||
<NText :depth="2" style="font-size: 14px; font-weight: 500;">{{ t('imageWorkspace.input.image') }}</NText>
|
||||
<NFlex align="center" size="small" :style="{ flex: 1, gap: '8px' }">
|
||||
<NButton
|
||||
:disabled="isOptimizing"
|
||||
@click="openUploadModal"
|
||||
size="medium"
|
||||
>
|
||||
{{ t('imageWorkspace.input.selectImage') }}
|
||||
</NButton>
|
||||
|
||||
<!-- 缩略图显示区域 -->
|
||||
<div v-if="previewImageUrl" class="thumbnail-container">
|
||||
<NImage
|
||||
:src="previewImageUrl"
|
||||
:style="{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
objectFit: 'cover',
|
||||
border: '1px solid #e0e0e6'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<NButton
|
||||
v-if="previewImageUrl"
|
||||
@click="clearUploadedImage"
|
||||
:disabled="isOptimizing"
|
||||
size="medium"
|
||||
type="error"
|
||||
secondary
|
||||
>
|
||||
❌
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NSpace>
|
||||
|
||||
<!-- 控制面板 - 使用网格布局 -->
|
||||
<NGrid :cols="24" :x-gap="12" responsive="screen">
|
||||
<!-- 优化模板选择 -->
|
||||
<NGridItem :span="12" :xs="24" :sm="12">
|
||||
<NSpace vertical :size="8">
|
||||
<NText :depth="2" style="font-size: 14px; font-weight: 500;">{{ t('imageWorkspace.input.optimizeTemplate') }}</NText>
|
||||
<TemplateSelectUI
|
||||
v-if="services && services.templateManager"
|
||||
v-model="selectedTemplate"
|
||||
:type="templateType"
|
||||
optimization-mode="user"
|
||||
:placeholder="t('imageWorkspace.input.templatePlaceholder')"
|
||||
size="medium"
|
||||
:disabled="isOptimizing"
|
||||
@update:model-value="saveSelections"
|
||||
/>
|
||||
<NText v-else depth="3" style="padding: 0; font-size: 14px;">
|
||||
{{ t('common.loading') }}
|
||||
</NText>
|
||||
</NSpace>
|
||||
</NGridItem>
|
||||
|
||||
<!-- 文本模型选择 -->
|
||||
<NGridItem :span="8" :xs="24" :sm="8">
|
||||
<NSpace vertical :size="8">
|
||||
<NText :depth="2" style="font-size: 14px; font-weight: 500;">{{ t('imageWorkspace.input.textModel') }}</NText>
|
||||
<NSelect
|
||||
:options="textModelOptions"
|
||||
v-model:value="selectedTextModelKey"
|
||||
:placeholder="t('imageWorkspace.input.modelPlaceholder')"
|
||||
size="medium"
|
||||
:disabled="isOptimizing"
|
||||
@update:value="saveSelections"
|
||||
/>
|
||||
</NSpace>
|
||||
</NGridItem>
|
||||
|
||||
<!-- 优化按钮 -->
|
||||
<NGridItem :span="4" :xs="24" :sm="4">
|
||||
<NSpace vertical :size="8" align="end">
|
||||
<NButton
|
||||
type="primary"
|
||||
size="medium"
|
||||
:loading="isOptimizing"
|
||||
@click="handleOptimizePrompt"
|
||||
:disabled="!originalPrompt.trim() || !selectedTextModelKey || !selectedTemplate"
|
||||
block
|
||||
round
|
||||
>
|
||||
{{ isOptimizing ? t('imageWorkspace.input.optimizing') : t('imageWorkspace.input.optimizePrompt') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 优化结果区域 - 使用与基础模式一致的卡片容器 -->
|
||||
<NCard :style="{ flex: 1, minHeight: '200px', overflow: 'hidden' }"
|
||||
content-style="height: 100%; max-height: 100%; overflow: hidden;">
|
||||
<PromptPanelUI
|
||||
v-if="services && services.templateManager"
|
||||
ref="promptPanelRef"
|
||||
v-model:optimized-prompt="optimizedPrompt"
|
||||
:reasoning="optimizedReasoning"
|
||||
:original-prompt="originalPrompt"
|
||||
:is-optimizing="isOptimizing"
|
||||
:is-iterating="isIterating"
|
||||
v-model:selected-iterate-template="selectedIterateTemplate"
|
||||
:versions="currentVersions"
|
||||
:current-version-id="currentVersionId"
|
||||
:optimization-mode="optimizationMode"
|
||||
:services="services"
|
||||
:advanced-mode-enabled="advancedModeEnabled"
|
||||
iterate-template-type="imageIterate"
|
||||
@iterate="handleIteratePrompt"
|
||||
@openTemplateManager="onOpenTemplateManager"
|
||||
@switchVersion="handleSwitchVersion"
|
||||
/>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
|
||||
<!-- 右侧:图像生成测试区域(图像模型) -->
|
||||
<NCard
|
||||
:style="{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
height: '100%'
|
||||
}"
|
||||
content-style="height: 100%; max-height: 100%; overflow: hidden;"
|
||||
>
|
||||
<NFlex vertical :style="{ height: '100%' }">
|
||||
<!-- 测试控制栏 -->
|
||||
<div :style="{ flexShrink: 0 }">
|
||||
<n-form label-placement="left" size="medium">
|
||||
<n-form-item :label="t('imageWorkspace.generation.imageModel')">
|
||||
<n-select
|
||||
:options="imageModelOptions"
|
||||
v-model:value="selectedImageModelKey"
|
||||
:placeholder="t('imageWorkspace.generation.imageModelPlaceholder')"
|
||||
style="width: 260px;"
|
||||
:disabled="isGenerating"
|
||||
@update:value="saveSelections"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space align="center" wrap>
|
||||
<n-switch
|
||||
v-model:value="isCompareMode"
|
||||
:disabled="isGenerating"
|
||||
@update:value="saveSelections"
|
||||
/>
|
||||
<n-text depth="3">{{ t('imageWorkspace.generation.compareMode') }}</n-text>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="isGenerating"
|
||||
@click="handleGenerateImage"
|
||||
:disabled="!currentPrompt.trim() || !selectedImageModelKey"
|
||||
>
|
||||
{{ isGenerating ? t('imageWorkspace.generation.generating') : t('imageWorkspace.generation.generateImage') }}
|
||||
</n-button>
|
||||
<!-- 进度显示:对象格式显示进度条,字符串格式显示文字 -->
|
||||
<template v-if="generationProgress && generationProgress !== 'idle'">
|
||||
<!-- 对象格式:显示进度条 -->
|
||||
<template v-if="typeof generationProgress === 'object' && (generationProgress as any).progress !== undefined">
|
||||
<n-space align="center" size="small">
|
||||
<n-text depth="3" style="font-size: 12px;">{{ (generationProgress as any).phase || t('imageWorkspace.generation.processing') }}</n-text>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="(generationProgress as any).progress"
|
||||
:show-indicator="true"
|
||||
status="info"
|
||||
style="width: 120px;"
|
||||
/>
|
||||
</n-space>
|
||||
</template>
|
||||
<!-- 字符串格式:显示文字 -->
|
||||
<n-text v-else depth="3">
|
||||
{{ generationProgress }}
|
||||
</n-text>
|
||||
</template>
|
||||
<n-text type="error" v-if="generationError">
|
||||
{{ generationError }}
|
||||
</n-text>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 图像结果展示区域(使用统一的 TestResultSection 布局) -->
|
||||
<TestResultSection
|
||||
:is-compare-mode="isCompareMode"
|
||||
:style="{ flex: 1, minHeight: 0 }"
|
||||
:original-title="t('imageWorkspace.results.originalPromptResult')"
|
||||
:optimized-title="t('imageWorkspace.results.optimizedPromptResult')"
|
||||
:single-result-title="t('imageWorkspace.results.optimizedPromptResult')"
|
||||
>
|
||||
<template #original-result>
|
||||
<template v-if="originalImageResult && originalImageResult.images.length > 0">
|
||||
<n-image
|
||||
:src="getImageSrc(originalImageResult.images[0])"
|
||||
object-fit="contain"
|
||||
:img-props="{ style: { width: '100%', height: 'auto', display: 'block' } }"
|
||||
/>
|
||||
<n-space style="margin-top: 8px;" justify="center">
|
||||
<n-button size="small" @click="downloadImageFromResult(originalImageResult.images[0], 'original')">
|
||||
{{ t('imageWorkspace.results.download') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-empty :description="t('imageWorkspace.results.noOriginalResult')" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #optimized-result>
|
||||
<template v-if="optimizedImageResult && optimizedImageResult.images.length > 0">
|
||||
<n-image
|
||||
:src="getImageSrc(optimizedImageResult.images[0])"
|
||||
object-fit="contain"
|
||||
:img-props="{ style: { width: '100%', height: 'auto', display: 'block' } }"
|
||||
/>
|
||||
<n-space style="margin-top: 8px;" justify="center">
|
||||
<n-button size="small" @click="downloadImageFromResult(optimizedImageResult.images[0], 'optimized')">
|
||||
{{ t('imageWorkspace.results.download') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-empty :description="t('imageWorkspace.results.noOptimizedResult')" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #single-result>
|
||||
<template v-if="optimizedImageResult && optimizedImageResult.images.length > 0">
|
||||
<n-image
|
||||
:src="getImageSrc(optimizedImageResult.images[0])"
|
||||
object-fit="contain"
|
||||
:img-props="{ style: { width: '100%', height: 'auto', display: 'block' } }"
|
||||
/>
|
||||
<n-space style="margin-top: 8px;" justify="center">
|
||||
<n-button @click="downloadImageFromResult(optimizedImageResult.images[0], 'optimized')">
|
||||
{{ t('imageWorkspace.results.download') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="originalImageResult && originalImageResult.images.length > 0">
|
||||
<n-image
|
||||
:src="getImageSrc(originalImageResult.images[0])"
|
||||
object-fit="contain"
|
||||
:img-props="{ style: { width: '100%', height: 'auto', display: 'block' } }"
|
||||
/>
|
||||
<n-space style="margin-top: 8px;" justify="center">
|
||||
<n-button @click="downloadImageFromResult(originalImageResult.images[0], 'original')">
|
||||
{{ t('imageWorkspace.results.download') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<n-empty v-else :description="t('imageWorkspace.results.noGenerationResult')" />
|
||||
</template>
|
||||
</template>
|
||||
</TestResultSection>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
|
||||
<!-- 原始提示词 - 全屏编辑器 -->
|
||||
<FullscreenDialog v-model="isFullscreen" :title="t('imageWorkspace.input.originalPrompt')">
|
||||
<NInput
|
||||
v-model:value="fullscreenValue"
|
||||
type="textarea"
|
||||
:placeholder="t('imageWorkspace.input.originalPromptPlaceholder')"
|
||||
:autosize="{ minRows: 20 }"
|
||||
clearable
|
||||
show-count
|
||||
:disabled="isOptimizing"
|
||||
/>
|
||||
</FullscreenDialog>
|
||||
|
||||
<!-- 图片上传弹窗 -->
|
||||
<n-modal v-model:show="showUploadModal" preset="card" :title="t('imageWorkspace.upload.title')" style="width: 500px;">
|
||||
<div style="padding: 16px;">
|
||||
<n-upload
|
||||
:max="1"
|
||||
accept="image/png,image/jpeg"
|
||||
:show-file-list="true"
|
||||
@change="handleModalUploadChange"
|
||||
:disabled="isOptimizing"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div style="padding: 24px; text-align: center;">
|
||||
<div style="font-size: 32px; margin-bottom: 12px;">📁</div>
|
||||
<n-text style="font-size: 14px;">{{ t('imageWorkspace.upload.dragText') }}</n-text>
|
||||
<n-p depth="3" style="margin-top: 8px; font-size: 12px;">
|
||||
{{ t('imageWorkspace.upload.fileRequirements') }}
|
||||
</n-p>
|
||||
</div>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<!-- 上传状态指示 -->
|
||||
<div v-if="uploadStatus !== 'idle'" style="margin-top: 16px;">
|
||||
<n-progress
|
||||
v-if="uploadStatus === 'uploading'"
|
||||
:percentage="uploadProgress"
|
||||
:show-indicator="true"
|
||||
status="info"
|
||||
/>
|
||||
<n-alert
|
||||
v-else-if="uploadStatus === 'error'"
|
||||
:title="t('imageWorkspace.upload.uploadFailed')"
|
||||
type="error"
|
||||
size="small"
|
||||
/>
|
||||
<n-alert
|
||||
v-else-if="uploadStatus === 'success'"
|
||||
:title="t('imageWorkspace.upload.uploadSuccess')"
|
||||
type="success"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 模板管理器由 App 统一管理,这里不再渲染 -->
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, inject, ref, computed, type Ref } from 'vue'
|
||||
import {
|
||||
NCard, NButton, NInput, NSelect, NEmpty, NFormItem, NForm, NSpace,
|
||||
NUpload, NUploadDragger, NImage, NText, NSwitch, NFlex, NGrid, NGridItem,
|
||||
NP, NProgress, NAlert, NModal, NIcon
|
||||
} from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
// 使用 Naive UI 内置图标或简单文本替代
|
||||
import TemplateSelectUI from '../TemplateSelect.vue'
|
||||
import PromptPanelUI from '../PromptPanel.vue'
|
||||
import TestResultSection from '../TestResultSection.vue'
|
||||
import ImageModeSelector, { type ImageMode } from './ImageModeSelector.vue'
|
||||
import { useImageWorkspace } from '../../composables/useImageWorkspace'
|
||||
import type { AppServices } from '../../types/services'
|
||||
import { useFullscreen } from '../../composables/useFullscreen'
|
||||
import FullscreenDialog from '../FullscreenDialog.vue'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 服务注入
|
||||
const services = inject<Ref<AppServices | null>>('services', ref(null))
|
||||
|
||||
// 使用图像工作区 composable
|
||||
const {
|
||||
// 状态
|
||||
originalPrompt,
|
||||
optimizedPrompt,
|
||||
optimizedReasoning,
|
||||
isOptimizing,
|
||||
isIterating,
|
||||
imageMode,
|
||||
selectedTextModelKey,
|
||||
selectedImageModelKey,
|
||||
selectedTemplate,
|
||||
selectedIterateTemplate,
|
||||
inputImageB64,
|
||||
isCompareMode,
|
||||
originalImageResult,
|
||||
optimizedImageResult,
|
||||
currentImageResult,
|
||||
currentVersions,
|
||||
currentVersionId,
|
||||
uploadStatus,
|
||||
uploadProgress,
|
||||
|
||||
// 计算属性
|
||||
currentPrompt,
|
||||
previewImageUrl,
|
||||
templateType,
|
||||
textModelOptions,
|
||||
imageModelOptions,
|
||||
optimizationMode,
|
||||
advancedModeEnabled,
|
||||
|
||||
// 图像生成状态
|
||||
isGenerating,
|
||||
generationProgress,
|
||||
generationError,
|
||||
|
||||
// 方法
|
||||
initialize,
|
||||
handleUploadChange,
|
||||
handleOptimizePrompt,
|
||||
handleIteratePrompt,
|
||||
handleGenerateImage,
|
||||
handleImageModeChange,
|
||||
handleSwitchVersion,
|
||||
getImageSrc,
|
||||
downloadImageFromResult,
|
||||
saveSelections,
|
||||
cleanup,
|
||||
refreshImageModels
|
||||
} = useImageWorkspace(services)
|
||||
|
||||
// PromptPanel 引用,用于在语言切换后刷新迭代模板选择
|
||||
const promptPanelRef = ref<InstanceType<typeof PromptPanelUI> | null>(null)
|
||||
|
||||
// 注入 App 层统一的 openTemplateManager 接口
|
||||
const appOpenTemplateManager = inject<(type?: 'optimize' | 'userOptimize' | 'iterate' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate') => void>('openTemplateManager', null as any)
|
||||
|
||||
// 将迭代类型映射为图像迭代,并调用 App 入口
|
||||
const onOpenTemplateManager = (type: 'optimize' | 'userOptimize' | 'iterate' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate') => {
|
||||
const target = type === 'iterate' ? 'imageIterate' : type
|
||||
if (appOpenTemplateManager) appOpenTemplateManager(target as any)
|
||||
}
|
||||
|
||||
// 全屏编辑:复用 useFullscreen 模式,编辑 originalPrompt
|
||||
const { isFullscreen, fullscreenValue, openFullscreen } = useFullscreen(
|
||||
computed(() => originalPrompt.value),
|
||||
(value) => { originalPrompt.value = value }
|
||||
)
|
||||
|
||||
// 弹窗状态
|
||||
const showUploadModal = ref(false)
|
||||
|
||||
// 弹窗相关方法
|
||||
const openUploadModal = () => {
|
||||
showUploadModal.value = true
|
||||
}
|
||||
|
||||
// 弹窗中的上传处理
|
||||
const handleModalUploadChange = (data: any) => {
|
||||
// 复用原有的上传逻辑
|
||||
handleUploadChange(data)
|
||||
// 上传成功后关闭弹窗
|
||||
if (data?.file && data.file.status === 'finished') {
|
||||
setTimeout(() => {
|
||||
showUploadModal.value = false
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除上传的图片 - 通过重新触发上传变更来清除
|
||||
const clearUploadedImage = () => {
|
||||
// 调用上传变更处理器,传入空数据来清除图片
|
||||
handleUploadChange({ file: null, fileList: [] })
|
||||
}
|
||||
|
||||
// 初始化
|
||||
// 语言切换事件处理器(用于刷新迭代模板选择)
|
||||
const refreshIterateHandler = () => {
|
||||
promptPanelRef.value?.refreshIterateTemplateSelect?.()
|
||||
}
|
||||
|
||||
// 图像模型刷新事件处理器(模型管理器关闭后同步刷新)
|
||||
const refreshImageModelsHandler = async () => {
|
||||
try {
|
||||
await refreshImageModels()
|
||||
} catch (e) {
|
||||
console.warn('[ImageWorkspace] Failed to refresh image models after manager close:', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('[ImageWorkspace] Starting initialization...')
|
||||
console.log('[ImageWorkspace] Services available:', !!services?.value)
|
||||
try {
|
||||
await initialize()
|
||||
console.log('[ImageWorkspace] Initialization completed successfully')
|
||||
} catch (error) {
|
||||
console.error('[ImageWorkspace] Initialization failed:', error)
|
||||
}
|
||||
|
||||
// 监听模板语言切换事件,刷新迭代模板选择
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('image-workspace-refresh-iterate-select', refreshIterateHandler)
|
||||
window.addEventListener('image-workspace-refresh-image-models', refreshImageModelsHandler)
|
||||
}
|
||||
})
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
console.log('[ImageWorkspace] Cleaning up...')
|
||||
cleanup()
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('image-workspace-refresh-iterate-select', refreshIterateHandler)
|
||||
window.removeEventListener('image-workspace-refresh-image-models', refreshImageModelsHandler)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 缩略图容器样式 */
|
||||
.thumbnail-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thumbnail-container :deep(.n-image) {
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.thumbnail-container :deep(.n-image:hover) {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 移除了所有自定义上传样式,使用 Naive UI 原生样式 */
|
||||
</style>
|
||||
@@ -16,6 +16,7 @@ export * from './useNaiveTheme'
|
||||
export * from './useResponsiveTestLayout'
|
||||
export * from './useTestModeConfig'
|
||||
export * from './useFunctionMode'
|
||||
export * from './useImageWorkspace'
|
||||
export * from './useResponsive'
|
||||
export * from './usePerformanceMonitor'
|
||||
export * from './useDebounceThrottle'
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
waitForElectronApi,
|
||||
ElectronPreferenceServiceProxy,
|
||||
createPreferenceService,
|
||||
} from '../'; // 从UI包的index导入所有核心模块
|
||||
} from '../'; // 从UI包的index导出所有核心模块
|
||||
import type { AppServices } from '../types/services';
|
||||
import { createImageModelManager, createImageService } from '@prompt-optimizer/core';
|
||||
import type { IImageModelManager, IImageService } from '@prompt-optimizer/core'
|
||||
import type { IModelManager, ITemplateManager, IHistoryManager, ILLMService, IPromptService, IDataManager, IPreferenceService } from '@prompt-optimizer/core';
|
||||
|
||||
/**
|
||||
@@ -52,6 +54,8 @@ export function useAppInitializer(): {
|
||||
let llmService: ILLMService;
|
||||
let promptService: IPromptService;
|
||||
let preferenceService: IPreferenceService;
|
||||
let imageModelManager: IImageModelManager | undefined;
|
||||
let imageService: IImageService | undefined;
|
||||
|
||||
if (isRunningInElectron()) {
|
||||
console.log('[AppInitializer] 检测到Electron环境,等待API就绪...');
|
||||
@@ -74,6 +78,10 @@ export function useAppInitializer(): {
|
||||
llmService = new ElectronLLMProxy();
|
||||
promptService = new ElectronPromptServiceProxy();
|
||||
preferenceService = new ElectronPreferenceServiceProxy();
|
||||
// 图像相关(Electron 渲染进程代理)
|
||||
const { ElectronImageModelManagerProxy, ElectronImageServiceProxy } = await import('@prompt-optimizer/core')
|
||||
imageModelManager = new ElectronImageModelManagerProxy();
|
||||
imageService = new ElectronImageServiceProxy();
|
||||
|
||||
// DataManager在Electron环境下使用代理模式
|
||||
dataManager = new ElectronDataManagerProxy();
|
||||
@@ -98,6 +106,8 @@ export function useAppInitializer(): {
|
||||
preferenceService, // 使用从core包导入的ElectronPreferenceServiceProxy
|
||||
compareService, // 直接使用,无需代理
|
||||
contextRepo, // 使用Electron代理
|
||||
imageModelManager,
|
||||
imageService,
|
||||
};
|
||||
console.log('[AppInitializer] Electron代理服务初始化完成');
|
||||
|
||||
@@ -113,6 +123,8 @@ export function useAppInitializer(): {
|
||||
|
||||
// Services with no dependencies or only storage
|
||||
const modelManagerInstance = createModelManager(storageProvider);
|
||||
// 图像模型管理器(独立存储空间)
|
||||
const imageModelManagerInstance = createImageModelManager(storageProvider);
|
||||
|
||||
// Initialize language service first, as template manager depends on it
|
||||
console.log('[AppInitializer] 初始化语言服务...');
|
||||
@@ -190,10 +202,14 @@ export function useAppInitializer(): {
|
||||
validateData: (data) => historyManagerInstance.validateData(data),
|
||||
};
|
||||
|
||||
// 预热跨域代理检测(以便适配器使用 /api/proxy 避免CORS)
|
||||
try { const { checkVercelApiAvailability, checkDockerApiAvailability } = await import('@prompt-optimizer/core'); await checkVercelApiAvailability(); await checkDockerApiAvailability(); } catch {}
|
||||
|
||||
// Services that depend on initialized managers
|
||||
console.log('[AppInitializer] 创建依赖其他管理器的服务...');
|
||||
llmService = createLLMService(modelManagerInstance);
|
||||
promptService = createPromptService(modelManager, llmService, templateManager, historyManager);
|
||||
imageService = createImageService(imageModelManagerInstance);
|
||||
|
||||
// 创建 CompareService(直接使用)
|
||||
const compareService = createCompareService();
|
||||
@@ -216,6 +232,8 @@ export function useAppInitializer(): {
|
||||
preferenceService, // 使用从core包导入的PreferenceService
|
||||
compareService, // 直接使用
|
||||
contextRepo, // 上下文仓库
|
||||
imageModelManager: imageModelManagerInstance,
|
||||
imageService,
|
||||
};
|
||||
|
||||
console.log('[AppInitializer] 所有服务初始化完成');
|
||||
@@ -233,4 +251,4 @@ export function useAppInitializer(): {
|
||||
});
|
||||
|
||||
return { services, isInitializing, error };
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import type { AppServices } from '../types/services'
|
||||
import { usePreferences } from './usePreferenceManager'
|
||||
import { UI_SETTINGS_KEYS } from '@prompt-optimizer/core'
|
||||
|
||||
export type FunctionMode = 'basic' | 'pro'
|
||||
export type FunctionMode = 'basic' | 'pro' | 'image'
|
||||
|
||||
interface UseFunctionModeApi {
|
||||
functionMode: Ref<FunctionMode>
|
||||
setFunctionMode: (mode: FunctionMode) => Promise<void>
|
||||
switchToBasic: () => Promise<void>
|
||||
switchToPro: () => Promise<void>
|
||||
switchToImage: () => Promise<void>
|
||||
ensureInitialized: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -41,9 +42,9 @@ export function useFunctionMode(services: Ref<AppServices | null>): UseFunctionM
|
||||
try {
|
||||
// 读取 function-mode;若不存在,返回默认 'basic'
|
||||
const saved = await getPreference<FunctionMode>(UI_SETTINGS_KEYS.FUNCTION_MODE, 'basic')
|
||||
singleton!.mode.value = (saved === 'pro' ? 'pro' : 'basic')
|
||||
singleton!.mode.value = (saved === 'pro' || saved === 'image') ? saved : 'basic'
|
||||
// 将默认值持久化(若未设置过)
|
||||
if (saved !== 'pro' && saved !== 'basic') {
|
||||
if (saved !== 'pro' && saved !== 'basic' && saved !== 'image') {
|
||||
await setPreference(UI_SETTINGS_KEYS.FUNCTION_MODE, 'basic')
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -65,13 +66,14 @@ export function useFunctionMode(services: Ref<AppServices | null>): UseFunctionM
|
||||
|
||||
const switchToBasic = () => setFunctionMode('basic')
|
||||
const switchToPro = () => setFunctionMode('pro')
|
||||
const switchToImage = () => setFunctionMode('image')
|
||||
|
||||
return {
|
||||
functionMode: readonly(singleton.mode) as Ref<FunctionMode>,
|
||||
setFunctionMode,
|
||||
switchToBasic,
|
||||
switchToPro,
|
||||
switchToImage,
|
||||
ensureInitialized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { computed, ref, type Ref, inject } from 'vue'
|
||||
import type { AppServices } from '../types/services'
|
||||
import type { ImageRequest, ImageResult, ImageModelConfig } from '@prompt-optimizer/core'
|
||||
|
||||
export function useImageGeneration() {
|
||||
const services = inject<Ref<AppServices | null>>('services')
|
||||
const generating = ref(false)
|
||||
const progress = ref<string | number | { phase: string; progress: number }>('idle')
|
||||
const error = ref<string>('')
|
||||
const result = ref<ImageResult | null>(null)
|
||||
|
||||
const imageModels = ref<(ImageModelConfig & { key: string })[]>([])
|
||||
|
||||
const loadImageModels = async () => {
|
||||
if (!services?.value?.imageModelManager) {
|
||||
imageModels.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const models = await services.value.imageModelManager.getEnabledModels()
|
||||
imageModels.value = models
|
||||
} catch (error) {
|
||||
console.error('Failed to load image models:', error)
|
||||
imageModels.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const generate = async (modelKey: string, req: ImageRequest) => {
|
||||
if (!services?.value?.imageService) throw new Error('Image service not available')
|
||||
error.value = ''
|
||||
result.value = null
|
||||
generating.value = true
|
||||
progress.value = 'queued'
|
||||
try {
|
||||
const res = await services.value.imageService.generate(req, modelKey, {
|
||||
onProgress: (s: string | number) => { progress.value = s },
|
||||
})
|
||||
result.value = res
|
||||
progress.value = 'done'
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || String(e)
|
||||
progress.value = 'error'
|
||||
throw e
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
services,
|
||||
imageModels,
|
||||
generating,
|
||||
progress,
|
||||
error,
|
||||
result,
|
||||
generate,
|
||||
loadImageModels
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,778 @@
|
||||
import { ref, computed, reactive, nextTick, toRef, type Ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePreferences } from './usePreferenceManager'
|
||||
import { useImageGeneration } from './useImageGeneration'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type {
|
||||
Template,
|
||||
PromptRecordChain,
|
||||
PromptRecordType,
|
||||
OptimizationRequest,
|
||||
OptimizationMode,
|
||||
ImageRequest,
|
||||
ImageResult
|
||||
} from '@prompt-optimizer/core'
|
||||
import { IMAGE_MODE_KEYS } from '@prompt-optimizer/core'
|
||||
import type { AppServices } from '../types/services'
|
||||
|
||||
/**
|
||||
* 图像模式工作区 Hook
|
||||
* 复用现有的历史记录系统,添加图像优化类型支持
|
||||
*/
|
||||
export function useImageWorkspace(services: Ref<AppServices | null>) {
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const { getPreference, setPreference } = usePreferences(services)
|
||||
const { imageModels, generating: isGenerating, progress: generationProgress, error: generationError, result: imageResult, generate: generateImage, loadImageModels } = useImageGeneration()
|
||||
|
||||
// 服务引用
|
||||
const modelManager = computed(() => services.value?.modelManager)
|
||||
const templateManager = computed(() => services.value?.templateManager)
|
||||
const historyManager = computed(() => services.value?.historyManager)
|
||||
const promptService = computed(() => services.value?.promptService)
|
||||
|
||||
// 响应式状态
|
||||
const state = reactive({
|
||||
// 基础状态
|
||||
originalPrompt: '',
|
||||
optimizedPrompt: '',
|
||||
optimizedReasoning: '',
|
||||
isOptimizing: false,
|
||||
isIterating: false,
|
||||
|
||||
// 图像模式状态
|
||||
imageMode: 'text2image' as 'text2image' | 'image2image',
|
||||
|
||||
// 模型和模板选择
|
||||
selectedTextModelKey: '',
|
||||
selectedImageModelKey: '',
|
||||
selectedTemplate: null as Template | null,
|
||||
selectedIterateTemplate: null as Template | null,
|
||||
|
||||
// 图像相关状态
|
||||
inputImageB64: null as string | null,
|
||||
inputImageMime: 'image/png',
|
||||
isCompareMode: true,
|
||||
|
||||
// 图像结果
|
||||
originalImageResult: null as ImageResult | null,
|
||||
optimizedImageResult: null as ImageResult | null,
|
||||
currentImageResult: null as ImageResult | null,
|
||||
|
||||
// 版本管理
|
||||
currentChainId: '',
|
||||
currentVersions: [] as PromptRecordChain['versions'],
|
||||
currentVersionId: '',
|
||||
|
||||
// 上传状态
|
||||
uploadStatus: 'idle' as 'idle' | 'uploading' | 'success' | 'error',
|
||||
uploadProgress: 0
|
||||
})
|
||||
|
||||
// 模板管理器状态
|
||||
const templateManagerState = reactive({
|
||||
showTemplates: false,
|
||||
currentType: 'text2imageOptimize' as 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate'
|
||||
})
|
||||
|
||||
// 模型选项
|
||||
const textModelOptions = ref<{ label: string; value: string }[]>([])
|
||||
const imageModelOptions = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
// 当前使用的提示词
|
||||
const currentPrompt = computed(() => state.optimizedPrompt || state.originalPrompt)
|
||||
|
||||
// 根据图像模式确定模板类型
|
||||
const templateType = computed(() => {
|
||||
return state.imageMode === 'text2image' ? 'text2imageOptimize' : 'image2imageOptimize'
|
||||
})
|
||||
|
||||
// 计算优化模式(图像模式统一使用 user 模式)
|
||||
const optimizationMode = 'user' as OptimizationMode
|
||||
|
||||
// 计算高级模式状态(图像模式暂不支持高级模式)
|
||||
const advancedModeEnabled = false
|
||||
|
||||
// 预览图像URL - 加强防护,确保不会因为undefined值而出错
|
||||
const previewImageUrl = computed(() => {
|
||||
if (!state.inputImageB64) return null
|
||||
const mimeType = state.inputImageMime || 'image/png' // 确保mime类型不为空
|
||||
return `data:${mimeType};base64,${state.inputImageB64}`
|
||||
})
|
||||
|
||||
// 初始化
|
||||
const initialize = async () => {
|
||||
try {
|
||||
// 加载文本模型
|
||||
if (modelManager.value) {
|
||||
const textModels = await modelManager.value.getEnabledModels()
|
||||
textModelOptions.value = textModels.map(m => ({
|
||||
label: `${m.name} (${m.provider})`,
|
||||
value: m.key
|
||||
}))
|
||||
}
|
||||
|
||||
// 加载图像模型
|
||||
await loadImageModels()
|
||||
imageModelOptions.value = imageModels.value.map((m) => ({
|
||||
label: `${m.name} (${m.provider})`,
|
||||
value: m.key
|
||||
}))
|
||||
|
||||
// 恢复保存的选择(包括模板选择)
|
||||
await restoreSelections()
|
||||
|
||||
// 🆕 监听历史记录恢复事件
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('image-workspace-restore', handleHistoryRestore as EventListener)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize image workspace:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新图像模型及下拉选项,并校验当前选择
|
||||
const refreshImageModels = async () => {
|
||||
try {
|
||||
await loadImageModels()
|
||||
imageModelOptions.value = imageModels.value.map((m) => ({
|
||||
label: `${m.name} (${m.provider})`,
|
||||
value: m.key
|
||||
}))
|
||||
// 若当前选择已不在可用列表,回退到第一个可用项
|
||||
const current = state.selectedImageModelKey
|
||||
const exists = imageModels.value.some(m => m.key === current)
|
||||
if (!exists) {
|
||||
state.selectedImageModelKey = imageModels.value[0]?.key || ''
|
||||
await saveSelections()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[useImageWorkspace] Failed to refresh image models:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复保存的选择
|
||||
const restoreSelections = async () => {
|
||||
try {
|
||||
state.selectedTextModelKey = await getPreference(IMAGE_MODE_KEYS.SELECTED_TEXT_MODEL, textModelOptions.value[0]?.value || '')
|
||||
state.selectedImageModelKey = await getPreference(IMAGE_MODE_KEYS.SELECTED_IMAGE_MODEL, imageModelOptions.value[0]?.value || '')
|
||||
state.isCompareMode = await getPreference(IMAGE_MODE_KEYS.COMPARE_MODE_ENABLED, true)
|
||||
|
||||
// 恢复保存的模板选择
|
||||
await restoreTemplateSelection()
|
||||
|
||||
// 恢复图像迭代模板选择(仅使用 imageIterate 类型,不做其他类型回退)
|
||||
await restoreImageIterateTemplateSelection()
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore selections:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复模板选择
|
||||
const restoreTemplateSelection = async () => {
|
||||
if (!templateManager.value) return
|
||||
|
||||
try {
|
||||
// 根据当前图像模式获取对应的模板
|
||||
const currentTemplateType = templateType.value
|
||||
const templates = await templateManager.value.listTemplatesByType(currentTemplateType as any)
|
||||
if (templates.length === 0) return
|
||||
|
||||
// 尝试恢复保存的模板
|
||||
const savedTemplateId = await getPreference(IMAGE_MODE_KEYS.SELECTED_TEMPLATE, '')
|
||||
let selectedTemplate: Template | null = null
|
||||
|
||||
if (savedTemplateId) {
|
||||
selectedTemplate = templates.find(t => t.id === savedTemplateId) || null
|
||||
}
|
||||
|
||||
// 如果没有保存的模板或找不到,使用默认模板
|
||||
if (!selectedTemplate) {
|
||||
// 根据模板类型选择合适的默认模板
|
||||
if (currentTemplateType === 'text2imageOptimize') {
|
||||
selectedTemplate = templates.find(t => t.id === 'image-chinese-optimize') ||
|
||||
templates.find(t => t.id === 'image-dalle-optimize') ||
|
||||
templates.find(t => t.name.includes('通用')) ||
|
||||
templates[0]
|
||||
} else {
|
||||
// image2imageOptimize 模板
|
||||
selectedTemplate = templates.find(t => t.id === 'image2image-general-optimize') ||
|
||||
templates[0]
|
||||
}
|
||||
|
||||
// 保存默认选择
|
||||
if (selectedTemplate) {
|
||||
await setPreference(IMAGE_MODE_KEYS.SELECTED_TEMPLATE, selectedTemplate.id)
|
||||
}
|
||||
}
|
||||
|
||||
state.selectedTemplate = selectedTemplate
|
||||
console.log('[useImageWorkspace] Template restored:', selectedTemplate?.name, 'ID:', selectedTemplate?.id, 'Type:', currentTemplateType)
|
||||
} catch (error) {
|
||||
console.error('Failed to restore template selection:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认模板(仅在没有保存选择时使用)
|
||||
const loadDefaultTemplate = async () => {
|
||||
// 这个函数现在由 restoreTemplateSelection 处理
|
||||
// 保留为空函数以保持兼容性
|
||||
}
|
||||
|
||||
// 保存选择
|
||||
const saveSelections = async () => {
|
||||
try {
|
||||
await setPreference(IMAGE_MODE_KEYS.SELECTED_TEXT_MODEL, state.selectedTextModelKey)
|
||||
await setPreference(IMAGE_MODE_KEYS.SELECTED_IMAGE_MODEL, state.selectedImageModelKey)
|
||||
await setPreference(IMAGE_MODE_KEYS.COMPARE_MODE_ENABLED, state.isCompareMode)
|
||||
if (state.selectedTemplate) {
|
||||
await setPreference(IMAGE_MODE_KEYS.SELECTED_TEMPLATE, state.selectedTemplate.id)
|
||||
}
|
||||
if (state.selectedIterateTemplate) {
|
||||
await setPreference(IMAGE_MODE_KEYS.SELECTED_ITERATE_TEMPLATE, state.selectedIterateTemplate.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to save selections:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复图像迭代模板选择
|
||||
const restoreImageIterateTemplateSelection = async () => {
|
||||
if (!templateManager.value) return
|
||||
try {
|
||||
const iterateTemplates = await templateManager.value.listTemplatesByType('imageIterate' as any)
|
||||
if (iterateTemplates.length === 0) {
|
||||
console.warn('[useImageWorkspace] No imageIterate templates available')
|
||||
state.selectedIterateTemplate = null
|
||||
return
|
||||
}
|
||||
const savedIterateId = await getPreference(IMAGE_MODE_KEYS.SELECTED_ITERATE_TEMPLATE, '')
|
||||
if (savedIterateId) {
|
||||
const found = iterateTemplates.find(t => t.id === savedIterateId)
|
||||
state.selectedIterateTemplate = found || iterateTemplates[0]
|
||||
} else {
|
||||
state.selectedIterateTemplate = iterateTemplates[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useImageWorkspace] Failed to restore image iterate template:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传处理
|
||||
const handleUploadChange = async (data: { file: any, fileList: any[] }) => {
|
||||
const f: File | undefined = data?.file?.file || data?.file
|
||||
|
||||
if (!f) {
|
||||
state.inputImageB64 = null
|
||||
state.inputImageMime = 'image/png' // 重置为默认值,防止undefined错误
|
||||
state.uploadStatus = 'idle'
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if (!/image\/(png|jpeg)/.test(f.type)) {
|
||||
toast.error('仅支持 PNG/JPEG 格式')
|
||||
state.uploadStatus = 'error'
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (f.size > 10 * 1024 * 1024) {
|
||||
toast.error('文件大小不能超过 10MB')
|
||||
state.uploadStatus = 'error'
|
||||
return
|
||||
}
|
||||
|
||||
state.uploadStatus = 'uploading'
|
||||
state.uploadProgress = 0
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = () => {
|
||||
const dataUrl = reader.result as string
|
||||
const base64 = dataUrl.split(',')[1]
|
||||
state.inputImageB64 = base64
|
||||
state.inputImageMime = f.type
|
||||
state.uploadStatus = 'success'
|
||||
state.uploadProgress = 100
|
||||
toast.success('图片上传成功')
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
toast.error('文件读取失败,请重试')
|
||||
state.uploadStatus = 'error'
|
||||
}
|
||||
|
||||
reader.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
state.uploadProgress = Math.round((e.loaded / e.total) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsDataURL(f)
|
||||
}
|
||||
|
||||
// 优化提示词
|
||||
const handleOptimizePrompt = async () => {
|
||||
if (!state.originalPrompt.trim() || state.isOptimizing) return
|
||||
|
||||
if (!state.selectedTemplate) {
|
||||
toast.error('请选择优化模板')
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.selectedTextModelKey) {
|
||||
toast.error('请选择文本模型')
|
||||
return
|
||||
}
|
||||
|
||||
// 保存当前选择
|
||||
await saveSelections()
|
||||
|
||||
state.isOptimizing = true
|
||||
state.optimizedPrompt = ''
|
||||
state.optimizedReasoning = ''
|
||||
|
||||
await nextTick()
|
||||
|
||||
try {
|
||||
const request: OptimizationRequest = {
|
||||
optimizationMode: 'user' as const, // 图像优化使用用户模式
|
||||
targetPrompt: state.originalPrompt,
|
||||
templateId: state.selectedTemplate.id,
|
||||
modelKey: state.selectedTextModelKey
|
||||
}
|
||||
|
||||
await promptService.value!.optimizePromptStream(
|
||||
request,
|
||||
{
|
||||
onToken: (token: string) => {
|
||||
state.optimizedPrompt += token
|
||||
},
|
||||
onReasoningToken: (reasoningToken: string) => {
|
||||
state.optimizedReasoning += reasoningToken
|
||||
},
|
||||
onComplete: async () => {
|
||||
await createHistoryRecord()
|
||||
toast.success('提示词优化完成')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (error: any) {
|
||||
toast.error('优化失败:' + (error?.message || String(error)))
|
||||
} finally {
|
||||
state.isOptimizing = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建历史记录
|
||||
const createHistoryRecord = async () => {
|
||||
if (!state.selectedTemplate || !historyManager.value) return
|
||||
|
||||
try {
|
||||
const recordData = {
|
||||
id: uuidv4(),
|
||||
originalPrompt: state.originalPrompt,
|
||||
optimizedPrompt: state.optimizedPrompt,
|
||||
type: 'imageOptimize' as PromptRecordType,
|
||||
modelKey: state.selectedTextModelKey,
|
||||
templateId: state.selectedTemplate.id,
|
||||
timestamp: Date.now(),
|
||||
metadata: {
|
||||
optimizationMode: 'user' as OptimizationMode,
|
||||
functionMode: 'image',
|
||||
imageModelKey: state.selectedImageModelKey,
|
||||
hasInputImage: !!state.inputImageB64,
|
||||
compareMode: state.isCompareMode
|
||||
}
|
||||
}
|
||||
|
||||
const newRecord = await historyManager.value.createNewChain(recordData)
|
||||
|
||||
state.currentChainId = newRecord.chainId
|
||||
state.currentVersions = newRecord.versions
|
||||
state.currentVersionId = newRecord.currentRecord.id
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建历史记录失败:', error)
|
||||
toast.warning('历史记录保存失败,但优化结果已生成')
|
||||
}
|
||||
}
|
||||
|
||||
// 生成图像
|
||||
const handleGenerateImage = async () => {
|
||||
if (!state.selectedImageModelKey || !currentPrompt.value.trim()) {
|
||||
toast.error('请选择图像模型并确保有有效的提示词')
|
||||
return
|
||||
}
|
||||
|
||||
// 保存当前选择
|
||||
await saveSelections()
|
||||
|
||||
const imageRequest: ImageRequest = {
|
||||
prompt: currentPrompt.value,
|
||||
count: 1,
|
||||
inputImage: state.inputImageB64 ? {
|
||||
b64: state.inputImageB64,
|
||||
mimeType: state.inputImageMime
|
||||
} : undefined,
|
||||
imgParams: { outputMimeType: 'image/png' }
|
||||
}
|
||||
|
||||
try {
|
||||
if (state.isCompareMode) {
|
||||
// 对比模式:生成原始和优化版本
|
||||
if (state.originalPrompt.trim()) {
|
||||
const originalRequest: ImageRequest = {
|
||||
...imageRequest,
|
||||
prompt: state.originalPrompt
|
||||
}
|
||||
await generateImage(state.selectedImageModelKey, originalRequest)
|
||||
state.originalImageResult = imageResult.value
|
||||
}
|
||||
|
||||
if (state.optimizedPrompt.trim()) {
|
||||
const optimizedRequest: ImageRequest = {
|
||||
...imageRequest,
|
||||
prompt: state.optimizedPrompt
|
||||
}
|
||||
await generateImage(state.selectedImageModelKey, optimizedRequest)
|
||||
state.optimizedImageResult = imageResult.value
|
||||
}
|
||||
} else {
|
||||
// 单一模式:生成当前提示词图像
|
||||
await generateImage(state.selectedImageModelKey, imageRequest)
|
||||
// 单一模式下按基础模式语义展示“优化后的测试结果”
|
||||
if (state.optimizedPrompt.trim()) {
|
||||
state.optimizedImageResult = imageResult.value
|
||||
} else if (state.originalPrompt.trim()) {
|
||||
// 若尚未有优化内容,则临时作为原始结果
|
||||
state.originalImageResult = imageResult.value
|
||||
}
|
||||
// 兼容旧UI:仍保留 currentImageResult 赋值
|
||||
state.currentImageResult = imageResult.value
|
||||
}
|
||||
|
||||
toast.success('图像生成完成')
|
||||
} catch (error: any) {
|
||||
toast.error('生成失败:' + (error?.message || String(error)))
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化后提示词
|
||||
const useOptimizedPrompt = () => {
|
||||
toast.success('已应用优化后的提示词')
|
||||
}
|
||||
|
||||
// 复制优化后提示词
|
||||
const copyOptimizedPrompt = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(state.optimizedPrompt)
|
||||
toast.success('已复制到剪贴板')
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换版本
|
||||
const handleSwitchVersion = async (version: PromptRecordChain['versions'][number]) => {
|
||||
state.optimizedPrompt = version.optimizedPrompt
|
||||
state.currentVersionId = version.id
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
// 获取图像显示源地址
|
||||
const getImageSrc = (imageItem: any) => {
|
||||
if (imageItem?.url) {
|
||||
return imageItem.url
|
||||
} else if (imageItem?.b64) {
|
||||
return 'data:image/png;base64,' + imageItem.b64
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 下载图像
|
||||
const downloadImageFromResult = async (imageItem: any, prefix: string) => {
|
||||
if (imageItem?.url) {
|
||||
try {
|
||||
const response = await fetch(imageItem.url)
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${prefix}-image.png`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
toast.error('下载失败')
|
||||
}
|
||||
} else if (imageItem?.b64) {
|
||||
const a = document.createElement('a')
|
||||
a.href = 'data:image/png;base64,' + imageItem.b64
|
||||
a.download = `${prefix}-image.png`
|
||||
a.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 迭代优化处理
|
||||
const handleIteratePrompt = async (payload: {
|
||||
originalPrompt: string
|
||||
optimizedPrompt: string
|
||||
iterateInput: string
|
||||
}) => {
|
||||
if (!state.selectedIterateTemplate || !templateManager.value || !promptService.value) {
|
||||
console.error('Missing dependencies for iteration')
|
||||
return
|
||||
}
|
||||
|
||||
state.isIterating = true
|
||||
|
||||
// 清空当前内容,准备接收新的流式内容
|
||||
const previousOptimizedPrompt = state.optimizedPrompt
|
||||
state.optimizedPrompt = ''
|
||||
state.optimizedReasoning = ''
|
||||
|
||||
try {
|
||||
await promptService.value.iteratePromptStream(
|
||||
payload.originalPrompt,
|
||||
payload.optimizedPrompt,
|
||||
payload.iterateInput,
|
||||
state.selectedTextModelKey,
|
||||
{
|
||||
onToken: (token: string) => {
|
||||
state.optimizedPrompt += token
|
||||
},
|
||||
onReasoningToken: (reasoningToken: string) => {
|
||||
state.optimizedReasoning += reasoningToken
|
||||
},
|
||||
onComplete: async () => {
|
||||
try {
|
||||
if (historyManager.value && state.currentChainId) {
|
||||
const updatedChain = await historyManager.value.addIteration({
|
||||
chainId: state.currentChainId,
|
||||
originalPrompt: payload.originalPrompt,
|
||||
optimizedPrompt: state.optimizedPrompt,
|
||||
iterationNote: payload.iterateInput,
|
||||
modelKey: state.selectedTextModelKey,
|
||||
templateId: state.selectedIterateTemplate!.id
|
||||
})
|
||||
state.currentVersions = updatedChain.versions
|
||||
state.currentVersionId = updatedChain.currentRecord.id
|
||||
} else {
|
||||
await createHistoryRecord()
|
||||
}
|
||||
toast.success('提示词迭代优化完成')
|
||||
} catch (e) {
|
||||
console.error('[useImageWorkspace] Failed to persist iteration:', e)
|
||||
toast.warning('迭代结果已生成,但历史记录保存失败')
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
state.selectedIterateTemplate.id
|
||||
)
|
||||
} catch (error: any) {
|
||||
toast.error('迭代优化失败:' + (error?.message || String(error)))
|
||||
// 恢复之前的内容
|
||||
state.optimizedPrompt = previousOptimizedPrompt
|
||||
} finally {
|
||||
state.isIterating = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制图像数据
|
||||
const copyImageFromResult = async (imageItem: any) => {
|
||||
if (imageItem?.b64) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(imageItem.b64)
|
||||
toast.success('Base64已复制')
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
} else if (imageItem?.url) {
|
||||
try {
|
||||
const response = await fetch(imageItem.url)
|
||||
const blob = await response.blob()
|
||||
const reader = new FileReader()
|
||||
reader.onload = async () => {
|
||||
const dataUrl = reader.result as string
|
||||
const base64 = dataUrl.split(',')[1]
|
||||
try {
|
||||
await navigator.clipboard.writeText(base64)
|
||||
toast.success('Base64已复制')
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
} catch {
|
||||
toast.error('获取图像数据失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 处理历史记录恢复
|
||||
const handleHistoryRestore = async (event: Event) => {
|
||||
const customEvent = event as CustomEvent
|
||||
try {
|
||||
const historyData = customEvent.detail
|
||||
console.log('[useImageWorkspace] Restoring history data:', historyData)
|
||||
|
||||
// 恢复基础数据
|
||||
state.originalPrompt = historyData.originalPrompt || ''
|
||||
state.optimizedPrompt = historyData.optimizedPrompt || ''
|
||||
|
||||
// 恢复版本信息
|
||||
state.currentChainId = historyData.chainId || ''
|
||||
state.currentVersions = historyData.versions || []
|
||||
state.currentVersionId = historyData.currentVersionId || ''
|
||||
|
||||
// 恢复模型选择(如果历史记录中有保存)
|
||||
if (historyData.metadata) {
|
||||
if (historyData.metadata.imageModelKey) {
|
||||
state.selectedImageModelKey = historyData.metadata.imageModelKey
|
||||
}
|
||||
if (historyData.metadata.compareMode !== undefined) {
|
||||
state.isCompareMode = historyData.metadata.compareMode
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复模板选择(根据历史记录中的templateId)
|
||||
if (historyData.templateId && templateManager.value) {
|
||||
try {
|
||||
// 直接通过ID获取模板,避免类型不匹配导致恢复失败
|
||||
const template = await templateManager.value.getTemplate(historyData.templateId)
|
||||
if (template) {
|
||||
state.selectedTemplate = template
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[useImageWorkspace] Failed to restore template by id, fallback to type search:', error)
|
||||
try {
|
||||
// 兼容旧记录:根据当前模式的类型尝试列表查找
|
||||
const type = templateType.value
|
||||
const list = await templateManager.value.listTemplatesByType(type as any)
|
||||
const t = list.find(t => t.id === historyData.templateId)
|
||||
if (t) state.selectedTemplate = t
|
||||
} catch (err) {
|
||||
console.warn('[useImageWorkspace] Fallback template restore failed:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存恢复的选择
|
||||
await saveSelections()
|
||||
|
||||
console.log('[useImageWorkspace] History data restored successfully')
|
||||
} catch (error) {
|
||||
console.error('[useImageWorkspace] Failed to restore history data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 图像模式切换处理
|
||||
const handleImageModeChange = async (mode: 'text2image' | 'image2image') => {
|
||||
if (state.imageMode === mode) return
|
||||
|
||||
state.imageMode = mode
|
||||
|
||||
// 如果切换到文生图模式,清除上传的图片
|
||||
if (mode === 'text2image' && state.inputImageB64) {
|
||||
state.inputImageB64 = null
|
||||
// previewImageUrl 是计算属性,会自动根据 inputImageB64 的值更新
|
||||
}
|
||||
|
||||
// 重置模板选择,因为模板类型已变化
|
||||
state.selectedTemplate = null
|
||||
|
||||
// 重新加载适合新模式的模板
|
||||
await restoreTemplateSelection()
|
||||
|
||||
// 保存选择
|
||||
await saveSelections()
|
||||
|
||||
console.log('[useImageWorkspace] Image mode changed to:', mode)
|
||||
}
|
||||
|
||||
|
||||
// 打开模板管理器
|
||||
const handleOpenTemplateManager = (templateType?: 'text2imageOptimize' | 'image2imageOptimize' | 'iterate' | 'imageIterate') => {
|
||||
let target: any = templateType
|
||||
// 在图像模式中,迭代应打开 imageIterate 类别
|
||||
if (templateType === 'iterate') target = 'imageIterate'
|
||||
if (!target) {
|
||||
target = state.imageMode === 'text2image' ? 'text2imageOptimize' : 'image2imageOptimize'
|
||||
}
|
||||
templateManagerState.currentType = target
|
||||
templateManagerState.showTemplates = true
|
||||
}
|
||||
|
||||
// 🆕 清理事件监听器
|
||||
const cleanup = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('image-workspace-restore', handleHistoryRestore as EventListener)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态 - 使用 toRefs 保持响应式
|
||||
originalPrompt: toRef(state, 'originalPrompt'),
|
||||
optimizedPrompt: toRef(state, 'optimizedPrompt'),
|
||||
optimizedReasoning: toRef(state, 'optimizedReasoning'),
|
||||
isOptimizing: toRef(state, 'isOptimizing'),
|
||||
isIterating: toRef(state, 'isIterating'),
|
||||
imageMode: toRef(state, 'imageMode'),
|
||||
selectedTextModelKey: toRef(state, 'selectedTextModelKey'),
|
||||
selectedImageModelKey: toRef(state, 'selectedImageModelKey'),
|
||||
selectedTemplate: toRef(state, 'selectedTemplate'),
|
||||
selectedIterateTemplate: toRef(state, 'selectedIterateTemplate'),
|
||||
inputImageB64: toRef(state, 'inputImageB64'),
|
||||
isCompareMode: toRef(state, 'isCompareMode'),
|
||||
originalImageResult: toRef(state, 'originalImageResult'),
|
||||
optimizedImageResult: toRef(state, 'optimizedImageResult'),
|
||||
currentImageResult: toRef(state, 'currentImageResult'),
|
||||
currentVersions: toRef(state, 'currentVersions'),
|
||||
currentVersionId: toRef(state, 'currentVersionId'),
|
||||
uploadStatus: toRef(state, 'uploadStatus'),
|
||||
uploadProgress: toRef(state, 'uploadProgress'),
|
||||
|
||||
// 计算属性
|
||||
currentPrompt,
|
||||
previewImageUrl,
|
||||
templateType,
|
||||
textModelOptions,
|
||||
imageModelOptions,
|
||||
optimizationMode,
|
||||
advancedModeEnabled,
|
||||
|
||||
// 图像生成状态
|
||||
isGenerating,
|
||||
generationProgress,
|
||||
generationError,
|
||||
|
||||
// 方法
|
||||
initialize,
|
||||
handleUploadChange,
|
||||
handleOptimizePrompt,
|
||||
handleIteratePrompt,
|
||||
handleGenerateImage,
|
||||
handleImageModeChange,
|
||||
handleOpenTemplateManager,
|
||||
useOptimizedPrompt,
|
||||
copyOptimizedPrompt,
|
||||
handleSwitchVersion,
|
||||
getImageSrc,
|
||||
downloadImageFromResult,
|
||||
copyImageFromResult,
|
||||
saveSelections,
|
||||
cleanup,
|
||||
refreshImageModels,
|
||||
templateManagerState
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,9 @@ export default {
|
||||
dataManager: 'Data Manager',
|
||||
advancedMode: 'Advanced Mode',
|
||||
variableManager: 'Variable Manager',
|
||||
basicMode: 'Basic',
|
||||
contextMode: 'Context',
|
||||
imageMode: 'Image',
|
||||
},
|
||||
promptOptimizer: {
|
||||
title: 'Prompt Optimizer',
|
||||
@@ -430,6 +433,8 @@ export default {
|
||||
updateFailed: 'Update failed: {error}',
|
||||
addSuccess: 'Model added successfully',
|
||||
addFailed: 'Failed to add model: {error}',
|
||||
createSuccess: 'Model created successfully',
|
||||
createFailed: 'Failed to create model: {error}',
|
||||
enableSuccess: 'Model enabled',
|
||||
enableFailed: 'Failed to enable model: {error}',
|
||||
disableSuccess: 'Model disabled',
|
||||
@@ -486,6 +491,10 @@ export default {
|
||||
userOptimizeTemplates: 'User Prompt Optimization Templates',
|
||||
userOptimizeTemplatesContext: 'User Prompt Optimization Templates (Context)',
|
||||
userOptimizeTemplateList: 'User Prompt Optimization Template List',
|
||||
// Image categories
|
||||
imageText2ImageTemplates: 'Image · Text-to-Image Templates',
|
||||
imageImage2ImageTemplates: 'Image · Image-to-Image Templates',
|
||||
imageIterateTemplates: 'Image · Iterate Templates',
|
||||
addTemplate: 'Add',
|
||||
editTemplate: 'Edit',
|
||||
deleteTemplate: 'Delete',
|
||||
@@ -651,6 +660,7 @@ export default {
|
||||
configure: 'Configure Template',
|
||||
selected: 'Selected',
|
||||
select: 'Select',
|
||||
noAvailableTemplates: 'No available templates',
|
||||
builtinLanguage: 'Built-in Template Language',
|
||||
switchBuiltinLanguage: 'Switch built-in template language',
|
||||
languageChanged: 'Built-in template language switched to {language}',
|
||||
@@ -1127,5 +1137,61 @@ export default {
|
||||
success: 'Success',
|
||||
error: 'Failed'
|
||||
}
|
||||
},
|
||||
|
||||
// Image mode configuration
|
||||
imageMode: {
|
||||
text2image: 'Text-to-Image',
|
||||
image2image: 'Image-to-Image',
|
||||
text2imageDescription: 'Generate images from text descriptions',
|
||||
image2imageDescription: 'Modify based on existing images',
|
||||
uploadRequired: 'Image-to-Image mode requires uploading a reference image first'
|
||||
},
|
||||
|
||||
imageWorkspace: {
|
||||
// Input area
|
||||
input: {
|
||||
originalPrompt: 'Original Prompt',
|
||||
originalPromptPlaceholder: 'Enter the image generation prompt to optimize',
|
||||
image: 'Image',
|
||||
selectImage: '📁 Select',
|
||||
optimizeTemplate: 'Optimization Template',
|
||||
templatePlaceholder: 'Please select template',
|
||||
textModel: 'Text Model',
|
||||
modelPlaceholder: 'Select model',
|
||||
optimizing: 'Optimizing...',
|
||||
optimizePrompt: 'Optimize Prompt'
|
||||
},
|
||||
|
||||
// Image generation area
|
||||
generation: {
|
||||
imageModel: 'Image Model',
|
||||
imageModelPlaceholder: 'Please select image model',
|
||||
compareMode: 'Compare Mode',
|
||||
generating: 'Generating...',
|
||||
generateImage: 'Generate Image',
|
||||
processing: 'Processing'
|
||||
},
|
||||
|
||||
// Results display
|
||||
results: {
|
||||
originalPromptResult: 'Original Prompt',
|
||||
optimizedPromptResult: 'Optimized Prompt',
|
||||
testResult: 'Test Result',
|
||||
download: 'Download',
|
||||
copyBase64: 'Copy Base64',
|
||||
noOriginalResult: 'No original result',
|
||||
noOptimizedResult: 'No optimized result',
|
||||
noGenerationResult: 'No generation result'
|
||||
},
|
||||
|
||||
// Upload modal
|
||||
upload: {
|
||||
title: 'Upload Reference Image',
|
||||
dragText: 'Click or drag to upload image',
|
||||
fileRequirements: 'Supports PNG/JPEG formats, file size up to 10MB',
|
||||
uploadFailed: 'Upload failed',
|
||||
uploadSuccess: 'Upload successful'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -94,6 +94,9 @@
|
||||
dataManager: '数据管理',
|
||||
advancedMode: '高级模式',
|
||||
variableManager: '变量管理',
|
||||
basicMode: '基础',
|
||||
contextMode: '上下文',
|
||||
imageMode: '图像',
|
||||
},
|
||||
promptOptimizer: {
|
||||
title: '提示词优化器',
|
||||
@@ -430,6 +433,8 @@
|
||||
updateFailed: '更新失败:{error}',
|
||||
addSuccess: '添加成功',
|
||||
addFailed: '添加失败:{error}',
|
||||
createSuccess: '创建成功',
|
||||
createFailed: '创建失败:{error}',
|
||||
enableSuccess: '启用成功',
|
||||
enableFailed: '启用失败:{error}',
|
||||
disableSuccess: '禁用成功',
|
||||
@@ -486,6 +491,10 @@
|
||||
userOptimizeTemplates: '用户提示词优化模板',
|
||||
userOptimizeTemplatesContext: '用户提示词优化模板(上下文)',
|
||||
userOptimizeTemplateList: '用户提示词优化模板列表',
|
||||
// 图像类标签
|
||||
imageText2ImageTemplates: '图像 · 文生图模板',
|
||||
imageImage2ImageTemplates: '图像 · 图生图模板',
|
||||
imageIterateTemplates: '图像 · 迭代模板',
|
||||
addTemplate: '添加',
|
||||
editTemplate: '编辑',
|
||||
deleteTemplate: '删除',
|
||||
@@ -652,6 +661,7 @@
|
||||
configure: '配置提示词',
|
||||
selected: '已选择',
|
||||
select: '选择',
|
||||
noAvailableTemplates: '暂无可用模板',
|
||||
builtinLanguage: '内置模板语言',
|
||||
switchBuiltinLanguage: '切换内置模板语言',
|
||||
languageChanged: '内置模板语言已切换为 {language}',
|
||||
@@ -1127,5 +1137,61 @@
|
||||
success: '成功',
|
||||
error: '失败'
|
||||
}
|
||||
},
|
||||
|
||||
// 图像模式配置
|
||||
imageMode: {
|
||||
text2image: '文生图',
|
||||
image2image: '图生图',
|
||||
text2imageDescription: '从文本描述生成图像',
|
||||
image2imageDescription: '基于现有图像进行修改',
|
||||
uploadRequired: '图生图模式需要先上传参考图片'
|
||||
},
|
||||
|
||||
imageWorkspace: {
|
||||
// 输入区域
|
||||
input: {
|
||||
originalPrompt: '原始提示词',
|
||||
originalPromptPlaceholder: '请输入需要优化的图像生成提示词',
|
||||
image: '图片',
|
||||
selectImage: '📁 选择',
|
||||
optimizeTemplate: '优化模板',
|
||||
templatePlaceholder: '请选择模板',
|
||||
textModel: '文本模型',
|
||||
modelPlaceholder: '选择模型',
|
||||
optimizing: '优化中...',
|
||||
optimizePrompt: '优化提示词'
|
||||
},
|
||||
|
||||
// 图像生成区域
|
||||
generation: {
|
||||
imageModel: '图像模型',
|
||||
imageModelPlaceholder: '请选择图像模型',
|
||||
compareMode: '对比模式',
|
||||
generating: '生成中...',
|
||||
generateImage: '生成图像',
|
||||
processing: '处理中'
|
||||
},
|
||||
|
||||
// 结果显示
|
||||
results: {
|
||||
originalPromptResult: '原始提示词',
|
||||
optimizedPromptResult: '优化提示词',
|
||||
testResult: '测试结果',
|
||||
download: '下载',
|
||||
copyBase64: '复制Base64',
|
||||
noOriginalResult: '暂无原始结果',
|
||||
noOptimizedResult: '暂无优化结果',
|
||||
noGenerationResult: '暂无生成结果'
|
||||
},
|
||||
|
||||
// 上传弹窗
|
||||
upload: {
|
||||
title: '上传参考图片',
|
||||
dragText: '点击或拖拽上传图片',
|
||||
fileRequirements: '支持 PNG/JPEG 格式,文件大小不超过 10MB',
|
||||
uploadFailed: '上传失败',
|
||||
uploadSuccess: '上传成功'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -58,6 +58,7 @@ export { default as LanguageSwitchDropdown } from './components/LanguageSwitchDr
|
||||
export { default as BuiltinTemplateLanguageSwitchUi } from './components/BuiltinTemplateLanguageSwitch.vue'
|
||||
export { default as DataManagerUI } from './components/DataManager.vue'
|
||||
export { default as OptimizationModeSelectorUI } from './components/OptimizationModeSelector.vue'
|
||||
export { default as FunctionModeSelector } from './components/FunctionModeSelector.vue'
|
||||
export { default as TextDiffUI } from './components/TextDiff.vue'
|
||||
export { default as OutputDisplayFullscreen } from './components/OutputDisplayFullscreen.vue'
|
||||
export { default as OutputDisplayCore } from './components/OutputDisplayCore.vue'
|
||||
@@ -163,3 +164,7 @@ export * from './services'
|
||||
|
||||
// 导出快速模板管理器
|
||||
export { quickTemplateManager } from './data/quickTemplates'
|
||||
|
||||
// 导出图像模式组件与核心图像服务(转发 core 能力)
|
||||
export { default as ImageWorkspace } from './components/image-mode/ImageWorkspace.vue'
|
||||
export { ImageModelManager, createImageModelManager, ImageService, createImageService } from '@prompt-optimizer/core'
|
||||
Vendored
+27
@@ -75,11 +75,36 @@ interface EventAPI {
|
||||
once(channel: string, listener: (...args: any[]) => void): void
|
||||
}
|
||||
|
||||
// 图像生成API
|
||||
interface ImageAPI {
|
||||
generate(request: any, modelKey: string): Promise<any>
|
||||
}
|
||||
|
||||
// 图像模型管理API
|
||||
interface ImageModelAPI {
|
||||
ensureInitialized(): Promise<void>
|
||||
isInitialized(): Promise<boolean>
|
||||
getAllModels(): Promise<any[]>
|
||||
getModel(key: string): Promise<any>
|
||||
addModel(model: any): Promise<void>
|
||||
updateModel(key: string, updates: any): Promise<void>
|
||||
deleteModel(key: string): Promise<void>
|
||||
enableModel(key: string): Promise<void>
|
||||
disableModel(key: string): Promise<void>
|
||||
getEnabledModels(): Promise<any[]>
|
||||
exportData(): Promise<any>
|
||||
importData(data: any): Promise<void>
|
||||
getDataType(): Promise<string>
|
||||
validateData(data: any): Promise<boolean>
|
||||
}
|
||||
|
||||
// 完整的ElectronAPI接口
|
||||
interface ElectronAPI {
|
||||
app: AppAPI
|
||||
updater: UpdaterAPI
|
||||
shell: ShellAPI
|
||||
image: ImageAPI
|
||||
imageModel: ImageModelAPI
|
||||
on: EventAPI['on']
|
||||
off: EventAPI['off']
|
||||
once: EventAPI['once']
|
||||
@@ -138,6 +163,8 @@ export type {
|
||||
UpdaterAPI,
|
||||
ShellAPI,
|
||||
EventAPI,
|
||||
ImageAPI,
|
||||
ImageModelAPI,
|
||||
ElectronAPI,
|
||||
DownloadProgress,
|
||||
UpdateInfo,
|
||||
|
||||
@@ -8,7 +8,9 @@ import type {
|
||||
ITemplateLanguageService,
|
||||
ICompareService,
|
||||
IPreferenceService,
|
||||
ContextRepo
|
||||
ContextRepo,
|
||||
IImageModelManager,
|
||||
IImageService
|
||||
} from '@prompt-optimizer/core'
|
||||
|
||||
/**
|
||||
@@ -25,4 +27,7 @@ export interface AppServices {
|
||||
preferenceService: IPreferenceService;
|
||||
compareService: ICompareService;
|
||||
contextRepo: ContextRepo;
|
||||
}
|
||||
// 图像相关(Web 优先,可选)
|
||||
imageModelManager?: IImageModelManager;
|
||||
imageService?: IImageService;
|
||||
}
|
||||
Vendored
+10
-1
@@ -119,9 +119,18 @@ declare module 'vue-i18n' {
|
||||
templateManager: {
|
||||
title: string;
|
||||
optimizeTemplates: string;
|
||||
userOptimizeTemplates: string;
|
||||
iterateTemplates: string;
|
||||
optimizeTemplatesContext: string;
|
||||
userOptimizeTemplatesContext: string;
|
||||
iterateTemplatesContext: string;
|
||||
optimizeTemplateList: string;
|
||||
userOptimizeTemplateList: string;
|
||||
iterateTemplateList: string;
|
||||
// Image template categories
|
||||
imageText2ImageTemplates: string;
|
||||
imageImage2ImageTemplates: string;
|
||||
imageIterateTemplates: string;
|
||||
addTemplate: string;
|
||||
editTemplate: string;
|
||||
deleteTemplate: string;
|
||||
@@ -190,4 +199,4 @@ declare module 'vue-i18n' {
|
||||
export function useI18n(): {
|
||||
t: (key: string, ...args: any[]) => string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import { useFunctionMode } from '../../../src/composables/useFunctionMode'
|
||||
|
||||
const makeServices = () => {
|
||||
const store = new Map<string, any>()
|
||||
const preferenceService = {
|
||||
async get<T>(key: string, def: T): Promise<T> { return (store.has(key) ? store.get(key) : def) as T },
|
||||
async set<T>(key: string, value: T) { store.set(key, value) },
|
||||
async delete() {}, async keys() { return Array.from(store.keys()) }, async clear() { store.clear() }, async getAll() { return {} as any }
|
||||
}
|
||||
return ref({ preferenceService } as any)
|
||||
}
|
||||
|
||||
describe('function mode image', () => {
|
||||
let services: any
|
||||
beforeEach(() => { services = makeServices() })
|
||||
it('can set image mode and persist', async () => {
|
||||
const { functionMode, setFunctionMode, ensureInitialized } = useFunctionMode(services)
|
||||
await ensureInitialized()
|
||||
await setFunctionMode('image' as any)
|
||||
expect(functionMode.value).toBe('image')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,5 +33,11 @@ describe('useFunctionMode', () => {
|
||||
await setFunctionMode('pro')
|
||||
expect(functionMode.value).toBe('pro')
|
||||
})
|
||||
})
|
||||
|
||||
it('supports image mode', async () => {
|
||||
const { functionMode, setFunctionMode, ensureInitialized } = useFunctionMode(services)
|
||||
await ensureInitialized()
|
||||
await setFunctionMode('image' as any)
|
||||
expect(functionMode.value).toBe('image')
|
||||
})
|
||||
})
|
||||
+113
-44
@@ -13,7 +13,15 @@
|
||||
<template #title>
|
||||
{{ $t('promptOptimizer.title') }}
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Core Navigation Slot -->
|
||||
<template #core-nav>
|
||||
<FunctionModeSelector
|
||||
v-model="functionMode"
|
||||
@change="handleModeSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actions Slot -->
|
||||
<template #actions>
|
||||
<!-- 核心功能区 -->
|
||||
@@ -28,17 +36,6 @@
|
||||
:ghost="false"
|
||||
:round="true"
|
||||
/>
|
||||
<!-- 高级模式导航按钮 - 始终显示,作为布局锚点 -->
|
||||
<ActionButtonUI
|
||||
icon="🚀"
|
||||
:text="$t('nav.advancedMode')"
|
||||
@click="toggleAdvancedMode"
|
||||
:class="{ 'active-button': advancedModeEnabled }"
|
||||
type="default"
|
||||
size="medium"
|
||||
:ghost="false"
|
||||
:round="true"
|
||||
/>
|
||||
<ActionButtonUI
|
||||
icon="📝"
|
||||
:text="$t('nav.templates')"
|
||||
@@ -99,6 +96,8 @@
|
||||
<template #main>
|
||||
|
||||
|
||||
<!-- 非图像模式:沿用现有布局 -->
|
||||
<template v-if="functionMode !== 'image'">
|
||||
<!-- Main Content - 使用 Naive UI NGrid 实现响应式水平左右布局 class="h-full min-h-0 overflow-hidden max-height=100%" -->
|
||||
<NFlex justify="space-between" :style="{display: 'flex', flexDirection: 'row', width: '100%' , 'max-height': '100%', gap: '16px' }" >
|
||||
<!-- 左侧:优化区域 -->
|
||||
@@ -263,10 +262,19 @@
|
||||
</NCard>
|
||||
</NFlex>
|
||||
</template>
|
||||
<!-- 图像模式:渲染新的工作区组件,不破坏现有结构 -->
|
||||
<template v-else>
|
||||
<ImageWorkspace />
|
||||
</template>
|
||||
</template>
|
||||
</MainLayoutUI>
|
||||
|
||||
<!-- Modals and Drawers that are conditionally rendered -->
|
||||
<ModelManagerUI v-if="isReady" v-model:show="modelManager.showConfig" />
|
||||
<ModelManagerUI
|
||||
v-if="isReady"
|
||||
v-model:show="modelManager.showConfig"
|
||||
@update:show="(v: boolean) => { if (!v) handleModelManagerClosed() }"
|
||||
/>
|
||||
<TemplateManagerUI
|
||||
v-if="isReady"
|
||||
v-model:show="templateManagerState.showTemplates"
|
||||
@@ -328,6 +336,7 @@ hljs.registerLanguage('json', jsonLang)
|
||||
MainLayoutUI, ThemeToggleUI, ActionButtonUI, ModelManagerUI, TemplateManagerUI, HistoryDrawerUI,
|
||||
LanguageSwitchDropdown, DataManagerUI, InputPanelUI, PromptPanelUI, OptimizationModeSelectorUI,
|
||||
ModelSelectUI, TemplateSelectUI, TestAreaPanel, UpdaterIcon, VariableManagerModal,
|
||||
ImageWorkspace, FunctionModeSelector,
|
||||
ConversationManager, OutputDisplay, ContextEditor,
|
||||
|
||||
// Composables
|
||||
@@ -401,7 +410,12 @@ hljs.registerLanguage('json', jsonLang)
|
||||
get: () => functionMode.value === 'pro',
|
||||
set: (val: boolean) => { setFunctionMode(val ? 'pro' : 'basic') }
|
||||
})
|
||||
|
||||
|
||||
// 处理功能模式变化
|
||||
const handleModeSelect = async (mode: 'basic' | 'pro' | 'image') => {
|
||||
await setFunctionMode(mode)
|
||||
}
|
||||
|
||||
// 测试内容状态 - 新增
|
||||
const testContent = ref('')
|
||||
const isCompareMode = ref(true)
|
||||
@@ -873,9 +887,9 @@ hljs.registerLanguage('json', jsonLang)
|
||||
}
|
||||
|
||||
// 打开模板管理器
|
||||
const openTemplateManager = (templateType?: 'optimize' | 'userOptimize' | 'iterate') => {
|
||||
const openTemplateManager = (templateType?: 'optimize' | 'userOptimize' | 'iterate' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate') => {
|
||||
// 如果传入了模板类型,直接使用;否则根据当前优化模式判断(向后兼容)
|
||||
templateManagerState.currentType = templateType || (selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize')
|
||||
templateManagerState.currentType = (templateType as any) || (selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize')
|
||||
templateManagerState.showTemplates = true
|
||||
}
|
||||
|
||||
@@ -887,47 +901,102 @@ hljs.registerLanguage('json', jsonLang)
|
||||
// 处理模板语言变化
|
||||
const handleTemplateLanguageChanged = (newLanguage: string) => {
|
||||
console.log('[App] 模板语言已切换:', newLanguage)
|
||||
|
||||
|
||||
// 刷新主界面的模板选择组件
|
||||
if (templateSelectRef.value?.refresh) {
|
||||
templateSelectRef.value.refresh()
|
||||
}
|
||||
|
||||
|
||||
// 刷新迭代页面的模板选择组件
|
||||
if (promptPanelRef.value?.refreshIterateTemplateSelect) {
|
||||
promptPanelRef.value.refreshIterateTemplateSelect()
|
||||
}
|
||||
|
||||
// 通知图像模式工作区刷新迭代模板选择
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new Event('image-workspace-refresh-iterate-select'))
|
||||
}
|
||||
}
|
||||
|
||||
// 向子组件提供统一的 openTemplateManager 接口(图像模式复用)
|
||||
provide('openTemplateManager', openTemplateManager)
|
||||
|
||||
// 模型管理器关闭回调:同步刷新基础模式下拉,并通知图像模式刷新图像模型
|
||||
const handleModelManagerClosed = async () => {
|
||||
try {
|
||||
// 基础模式:复用现有逻辑刷新文本模型与下拉
|
||||
modelManager.handleModelManagerClose()
|
||||
} catch (e) {
|
||||
console.warn('[App] Failed to refresh text models after manager close:', e)
|
||||
}
|
||||
// 图像模式:广播刷新图像模型事件(ImageWorkspace 监听并执行刷新)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new Event('image-workspace-refresh-image-models'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理历史记录使用 - 智能模式切换
|
||||
const handleHistoryReuse = async (context: { record: any, chainId: string, rootPrompt: string, chain: any }) => {
|
||||
const { chain } = context
|
||||
|
||||
// 根据链条的根记录类型确定应该切换到的优化模式
|
||||
let targetMode: OptimizationMode
|
||||
if (chain.rootRecord.type === 'optimize' || chain.rootRecord.type === 'contextSystemOptimize') {
|
||||
targetMode = 'system'
|
||||
} else if (chain.rootRecord.type === 'userOptimize' || chain.rootRecord.type === 'contextUserOptimize') {
|
||||
targetMode = 'user'
|
||||
} else {
|
||||
// 兜底:从根记录的 metadata 中获取优化模式
|
||||
targetMode = chain.rootRecord.metadata?.optimizationMode || 'system'
|
||||
}
|
||||
|
||||
// 如果目标模式与当前模式不同,自动切换
|
||||
if (targetMode !== selectedOptimizationMode.value) {
|
||||
selectedOptimizationMode.value = targetMode
|
||||
useToast().info(t('toast.info.optimizationModeAutoSwitched', {
|
||||
mode: targetMode === 'system' ? t('common.system') : t('common.user')
|
||||
}))
|
||||
}
|
||||
// 根据根记录类型自动切换功能模式
|
||||
const { record, chain } = context
|
||||
const rt = chain.rootRecord.type
|
||||
const isContext = rt === 'contextSystemOptimize' || rt === 'contextUserOptimize' || rt === 'contextIterate'
|
||||
await setFunctionMode(isContext ? 'pro' : 'basic')
|
||||
|
||||
// 调用原有的历史记录处理逻辑
|
||||
await promptHistory.handleSelectHistory(context)
|
||||
|
||||
// 🆕 扩展模式切换逻辑 - 支持图像模式
|
||||
if (rt === 'imageOptimize' || rt === 'contextImageOptimize' || rt === 'imageIterate') {
|
||||
// 切换到图像模式
|
||||
await setFunctionMode('image')
|
||||
useToast().info('已自动切换到图像模式')
|
||||
|
||||
// 🆕 图像模式专用数据回填逻辑
|
||||
// 等待模式切换完成后再回填数据
|
||||
await nextTick()
|
||||
|
||||
// 通过全局事件或直接访问ImageWorkspace的数据来回填
|
||||
// 由于ImageWorkspace是独立组件,我们需要通过provide/inject或事件系统来传递数据
|
||||
const imageHistoryData = {
|
||||
originalPrompt: record.originalPrompt || chain.rootRecord.originalPrompt,
|
||||
optimizedPrompt: record.optimizedPrompt,
|
||||
metadata: record.metadata || chain.rootRecord.metadata,
|
||||
chainId: chain.chainId,
|
||||
versions: chain.versions,
|
||||
currentVersionId: record.id
|
||||
}
|
||||
|
||||
// 触发图像工作区数据恢复事件
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('image-workspace-restore', {
|
||||
detail: imageHistoryData
|
||||
}))
|
||||
}
|
||||
|
||||
useToast().success('图像历史记录已恢复')
|
||||
return // 图像模式不需要调用原有的历史记录处理逻辑
|
||||
} else {
|
||||
// 根据链条的根记录类型确定应该切换到的优化模式
|
||||
let targetMode: OptimizationMode
|
||||
if (rt === 'optimize' || rt === 'contextSystemOptimize') {
|
||||
targetMode = 'system'
|
||||
} else if (rt === 'userOptimize' || rt === 'contextUserOptimize') {
|
||||
targetMode = 'user'
|
||||
} else {
|
||||
// 兜底:从根记录的 metadata 中获取优化模式
|
||||
targetMode = chain.rootRecord.metadata?.optimizationMode || 'system'
|
||||
}
|
||||
|
||||
// 如果目标模式与当前模式不同,自动切换
|
||||
if (targetMode !== selectedOptimizationMode.value) {
|
||||
selectedOptimizationMode.value = targetMode
|
||||
useToast().info(t('toast.info.optimizationModeAutoSwitched', {
|
||||
mode: targetMode === 'system' ? t('common.system') : t('common.user')
|
||||
}))
|
||||
}
|
||||
|
||||
// 根据根记录类型自动切换功能模式
|
||||
const isContext = rt === 'contextSystemOptimize' || rt === 'contextUserOptimize' || rt === 'contextIterate'
|
||||
await setFunctionMode(isContext ? 'pro' : 'basic')
|
||||
|
||||
// 调用原有的历史记录处理逻辑
|
||||
await promptHistory.handleSelectHistory(context)
|
||||
}
|
||||
}
|
||||
|
||||
// 提示词输入标签
|
||||
|
||||
Generated
+144
@@ -105,6 +105,9 @@ importers:
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
'@google/genai':
|
||||
specifier: ^1.0.0
|
||||
version: 1.19.0(@modelcontextprotocol/sdk@1.16.0)
|
||||
'@google/generative-ai':
|
||||
specifier: ^0.21.0
|
||||
version: 0.21.0
|
||||
@@ -736,6 +739,15 @@ packages:
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@google/genai@1.19.0':
|
||||
resolution: {integrity: sha512-mIMV3M/KfzzFA//0fziK472wKBJ1TdJLhozIUJKTPLyTDN1NotU+hyoHW/N0cfrcEWUK20YA0GxCeHC4z0SbMA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
'@modelcontextprotocol/sdk': ^1.11.4
|
||||
peerDependenciesMeta:
|
||||
'@modelcontextprotocol/sdk':
|
||||
optional: true
|
||||
|
||||
'@google/generative-ai@0.21.0':
|
||||
resolution: {integrity: sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -1661,6 +1673,9 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1710,6 +1725,9 @@ packages:
|
||||
buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-equal@1.0.1:
|
||||
resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==}
|
||||
engines: {node: '>=0.4'}
|
||||
@@ -2116,6 +2134,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
editorconfig@1.0.4:
|
||||
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -2368,6 +2389,9 @@ packages:
|
||||
exsolve@1.0.7:
|
||||
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||
engines: {node: '>= 10.17.0'}
|
||||
@@ -2520,6 +2544,14 @@ packages:
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
gaxios@6.7.1:
|
||||
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
gcp-metadata@6.1.1:
|
||||
resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
@@ -2580,6 +2612,14 @@ packages:
|
||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
google-auth-library@9.15.1:
|
||||
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
google-logging-utils@0.0.2:
|
||||
resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2598,6 +2638,10 @@ packages:
|
||||
resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
gtoken@7.1.0:
|
||||
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2849,6 +2893,10 @@ packages:
|
||||
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-string@1.1.1:
|
||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2938,6 +2986,9 @@ packages:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@@ -2971,6 +3022,12 @@ packages:
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -4321,6 +4378,10 @@ packages:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||
|
||||
@@ -4920,6 +4981,18 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@google/genai@1.19.0(@modelcontextprotocol/sdk@1.16.0)':
|
||||
dependencies:
|
||||
google-auth-library: 9.15.1
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
'@modelcontextprotocol/sdk': 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- encoding
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@google/generative-ai@0.21.0': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
@@ -6110,6 +6183,8 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
birpc@2.4.0: {}
|
||||
@@ -6184,6 +6259,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-equal@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
@@ -6611,6 +6688,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
editorconfig@1.0.4:
|
||||
dependencies:
|
||||
'@one-ini/wasm': 0.1.1
|
||||
@@ -7092,6 +7173,8 @@ snapshots:
|
||||
|
||||
exsolve@1.0.7: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
@@ -7275,6 +7358,26 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
gaxios@6.7.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-stream: 2.0.1
|
||||
node-fetch: 2.7.0
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@6.1.1:
|
||||
dependencies:
|
||||
gaxios: 6.7.1
|
||||
google-logging-utils: 0.0.2
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
@@ -7368,6 +7471,20 @@ snapshots:
|
||||
merge2: 1.4.1
|
||||
slash: 3.0.0
|
||||
|
||||
google-auth-library@9.15.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 6.7.1
|
||||
gcp-metadata: 6.1.1
|
||||
gtoken: 7.1.0
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
google-logging-utils@0.0.2: {}
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
got@11.8.6:
|
||||
@@ -7391,6 +7508,14 @@ snapshots:
|
||||
graphql@16.11.0:
|
||||
optional: true
|
||||
|
||||
gtoken@7.1.0:
|
||||
dependencies:
|
||||
gaxios: 6.7.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@3.0.0: {}
|
||||
@@ -7638,6 +7763,8 @@ snapshots:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
is-string@1.1.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -7738,6 +7865,10 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
json-bigint@1.0.0:
|
||||
dependencies:
|
||||
bignumber.js: 9.3.1
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-parse-better-errors@1.0.2: {}
|
||||
@@ -7770,6 +7901,17 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.0:
|
||||
dependencies:
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -9198,6 +9340,8 @@ snapshots:
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
dependencies:
|
||||
spdx-correct: 3.2.0
|
||||
|
||||
Reference in new issue
Block a user