fix: restore upstream canvas capabilities
This commit is contained in:
396
web/canvas-app/src/components/ApiSettings.vue
Normal file
396
web/canvas-app/src/components/ApiSettings.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<!-- API Settings Modal | API 设置弹窗 -->
|
||||
<n-modal v-model:show="showModal" preset="card" title="API 设置" style="width: 560px;">
|
||||
<n-tabs type="line" animated>
|
||||
<!-- API 配置标签 -->
|
||||
<n-tab-pane name="api" tab="API 配置">
|
||||
<n-form ref="formRef" :model="formData" label-placement="left" label-width="80">
|
||||
<n-form-item label="渠道" path="provider">
|
||||
<n-select
|
||||
v-model:value="formData.provider"
|
||||
:options="providerOptions"
|
||||
placeholder="选择 API 渠道"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Base URL" path="baseUrl">
|
||||
<n-input
|
||||
v-model:value="formData.baseUrl"
|
||||
placeholder="/api"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="API Key" path="apiKey">
|
||||
<n-input
|
||||
v-model:value="formData.apiKey"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="内部接口无需填写"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-divider title-placement="left" class="!my-3">
|
||||
<span class="text-xs text-[var(--text-secondary)]">端点路径</span>
|
||||
</n-divider>
|
||||
|
||||
<div class="endpoint-list">
|
||||
<div class="endpoint-item">
|
||||
<span class="endpoint-label">问答</span>
|
||||
<n-tag size="small" type="info" class="endpoint-tag">{{ currentEndpoints.chat }}</n-tag>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<span class="endpoint-label">生图</span>
|
||||
<n-tag size="small" type="success" class="endpoint-tag">{{ currentEndpoints.image }}</n-tag>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<span class="endpoint-label">视频生成</span>
|
||||
<n-tag size="small" type="warning" class="endpoint-tag">{{ currentEndpoints.video }}</n-tag>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<span class="endpoint-label">视频查询</span>
|
||||
<n-tag size="small" type="warning" class="endpoint-tag">{{ currentEndpoints.videoQuery }}</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-alert v-if="!isConfigured" type="warning" title="未配置" class="mb-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>当前使用 SKG 内部登录会话调用生成接口。</p>
|
||||
</div>
|
||||
</n-alert>
|
||||
|
||||
<n-alert v-else type="success" title="已配置" class="mb-4">
|
||||
API 已就绪,可以使用 AI 功能
|
||||
</n-alert>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 模型配置标签 -->
|
||||
<n-tab-pane name="models" tab="模型配置">
|
||||
<div class="model-config-section">
|
||||
<!-- 问答模型 -->
|
||||
<div class="model-group">
|
||||
<div class="model-group-header">
|
||||
<span class="model-group-title">问答模型</span>
|
||||
<n-tag size="tiny" type="info">{{ allChatModels.length }} 个</n-tag>
|
||||
</div>
|
||||
<div class="model-input-row">
|
||||
<n-input
|
||||
v-model:value="newChatModel"
|
||||
placeholder="输入模型名称,如 gpt-4o"
|
||||
size="small"
|
||||
@keyup.enter="handleAddChatModel"
|
||||
/>
|
||||
<n-button size="small" type="primary" @click="handleAddChatModel" :disabled="!newChatModel">
|
||||
添加
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="model-tags">
|
||||
<n-tag
|
||||
v-for="model in allChatModels"
|
||||
:key="model.key"
|
||||
size="small"
|
||||
:closable="model.isCustom"
|
||||
:type="model.isCustom ? 'info' : 'default'"
|
||||
@close="handleRemoveChatModel(model.key)"
|
||||
>
|
||||
{{ model.label }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片模型 -->
|
||||
<div class="model-group">
|
||||
<div class="model-group-header">
|
||||
<span class="model-group-title">图片模型</span>
|
||||
<n-tag size="tiny" type="success">{{ allImageModels.length }} 个</n-tag>
|
||||
</div>
|
||||
<div class="model-input-row">
|
||||
<n-input
|
||||
v-model:value="newImageModel"
|
||||
placeholder="输入模型名称,如 dall-e-3"
|
||||
size="small"
|
||||
@keyup.enter="handleAddImageModel"
|
||||
/>
|
||||
<n-button size="small" type="primary" @click="handleAddImageModel" :disabled="!newImageModel">
|
||||
添加
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="model-tags">
|
||||
<n-tag
|
||||
v-for="model in allImageModels"
|
||||
:key="model.key"
|
||||
size="small"
|
||||
:closable="model.isCustom"
|
||||
:type="model.isCustom ? 'success' : 'default'"
|
||||
@close="handleRemoveImageModel(model.key)"
|
||||
>
|
||||
{{ model.label }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频模型 -->
|
||||
<div class="model-group">
|
||||
<div class="model-group-header">
|
||||
<span class="model-group-title">视频模型</span>
|
||||
<n-tag size="tiny" type="warning">{{ allVideoModels.length }} 个</n-tag>
|
||||
</div>
|
||||
<div class="model-input-row">
|
||||
<n-input
|
||||
v-model:value="newVideoModel"
|
||||
placeholder="输入模型名称,如 sora-2"
|
||||
size="small"
|
||||
@keyup.enter="handleAddVideoModel"
|
||||
/>
|
||||
<n-button size="small" type="primary" @click="handleAddVideoModel" :disabled="!newVideoModel">
|
||||
添加
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="model-tags">
|
||||
<n-tag
|
||||
v-for="model in allVideoModels"
|
||||
:key="model.key"
|
||||
size="small"
|
||||
:closable="model.isCustom"
|
||||
:type="model.isCustom ? 'warning' : 'default'"
|
||||
@close="handleRemoveVideoModel(model.key)"
|
||||
>
|
||||
{{ model.label }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-[var(--text-secondary)]">生成调用走当前登录会话,无需个人 API Key</span>
|
||||
<div class="flex gap-2">
|
||||
<n-button @click="handleClear" tertiary>清除配置</n-button>
|
||||
<n-button @click="showModal = false">取消</n-button>
|
||||
<n-button type="primary" @click="handleSave">保存</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* API Settings Component | API 设置组件
|
||||
* Modal for configuring API key, base URL, and custom models
|
||||
*/
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { NModal, NForm, NFormItem, NInput, NButton, NAlert, NDivider, NTag, NTabs, NTabPane, NSelect } from 'naive-ui'
|
||||
import { useModelStore } from '../stores/pinia'
|
||||
import { getProviderConfig } from '../config/providers'
|
||||
|
||||
// Props | 属性
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits | 事件
|
||||
const emit = defineEmits(['update:show', 'saved'])
|
||||
|
||||
// API Config 状态
|
||||
const isConfigured = computed(() => !!modelStore.currentApiKey)
|
||||
|
||||
// Model Store (Pinia) | 模型配置 Store
|
||||
const modelStore = useModelStore()
|
||||
|
||||
// Provider options for select | 渠道下拉选项
|
||||
const providerOptions = modelStore.providerList.map(p => ({
|
||||
label: p.label,
|
||||
value: p.key
|
||||
}))
|
||||
|
||||
// 当前渠道的端点路径
|
||||
const currentEndpoints = computed(() => {
|
||||
const config = getProviderConfig(formData.provider)
|
||||
return config.endpoints || {
|
||||
chat: '/chat/completions',
|
||||
image: '/v1/images/generations',
|
||||
video: '/v1/videos',
|
||||
videoQuery: '/v1/videos/{taskId}'
|
||||
}
|
||||
})
|
||||
|
||||
// 全局模型列表(不区分渠道)
|
||||
const allChatModels = computed(() => modelStore.allChatModels)
|
||||
const allImageModels = computed(() => modelStore.allImageModels)
|
||||
const allVideoModels = computed(() => modelStore.allVideoModels)
|
||||
|
||||
// Modal visibility | 弹窗可见性
|
||||
const showModal = ref(props.show)
|
||||
|
||||
// Form data | 表单数据
|
||||
const formData = reactive({
|
||||
provider: modelStore.currentProvider,
|
||||
apiKey: '',
|
||||
baseUrl: ''
|
||||
})
|
||||
|
||||
// New model inputs | 新模型输入
|
||||
const newChatModel = ref('')
|
||||
const newImageModel = ref('')
|
||||
const newVideoModel = ref('')
|
||||
|
||||
// 初始化或切换渠道时,更新 API 配置
|
||||
const updateFormApiConfig = () => {
|
||||
const provider = formData.provider
|
||||
const config = getProviderConfig(provider)
|
||||
formData.apiKey = modelStore.apiKeysByProvider[provider] || ''
|
||||
formData.baseUrl = modelStore.baseUrlsByProvider[provider] || config.defaultBaseUrl || ''
|
||||
}
|
||||
|
||||
// Watch prop changes | 监听属性变化
|
||||
watch(() => props.show, (val) => {
|
||||
showModal.value = val
|
||||
if (val) {
|
||||
formData.provider = modelStore.currentProvider
|
||||
updateFormApiConfig()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听渠道变化,更新表单中的 API 配置
|
||||
watch(() => formData.provider, () => {
|
||||
updateFormApiConfig()
|
||||
})
|
||||
|
||||
// Watch modal changes | 监听弹窗变化
|
||||
watch(showModal, (val) => {
|
||||
emit('update:show', val)
|
||||
})
|
||||
|
||||
// Handle add models | 处理添加模型
|
||||
const handleAddChatModel = () => {
|
||||
if (newChatModel.value.trim()) {
|
||||
modelStore.addCustomChatModel(newChatModel.value.trim())
|
||||
newChatModel.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddImageModel = () => {
|
||||
if (newImageModel.value.trim()) {
|
||||
modelStore.addCustomImageModel(newImageModel.value.trim())
|
||||
newImageModel.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddVideoModel = () => {
|
||||
if (newVideoModel.value.trim()) {
|
||||
modelStore.addCustomVideoModel(newVideoModel.value.trim())
|
||||
newVideoModel.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remove models | 处理删除模型
|
||||
const handleRemoveChatModel = (modelKey) => {
|
||||
modelStore.removeCustomChatModel(modelKey)
|
||||
}
|
||||
|
||||
const handleRemoveImageModel = (modelKey) => {
|
||||
modelStore.removeCustomImageModel(modelKey)
|
||||
}
|
||||
|
||||
const handleRemoveVideoModel = (modelKey) => {
|
||||
modelStore.removeCustomVideoModel(modelKey)
|
||||
}
|
||||
|
||||
// Handle save | 处理保存
|
||||
const handleSave = () => {
|
||||
if (formData.provider) {
|
||||
modelStore.setProvider(formData.provider)
|
||||
}
|
||||
if (formData.apiKey) {
|
||||
modelStore.setApiKeyByProvider(formData.provider, formData.apiKey)
|
||||
}
|
||||
if (formData.baseUrl) {
|
||||
modelStore.setBaseUrlByProvider(formData.provider, formData.baseUrl)
|
||||
}
|
||||
showModal.value = false
|
||||
emit('saved')
|
||||
}
|
||||
|
||||
// Handle clear | 处理清除
|
||||
const handleClear = () => {
|
||||
modelStore.clearApiConfigByProvider(formData.provider)
|
||||
modelStore.clearCustomModels()
|
||||
formData.apiKey = ''
|
||||
formData.baseUrl = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.endpoint-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.endpoint-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #666);
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.endpoint-tag {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-config-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.model-group {
|
||||
padding: 12px;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.model-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.model-group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.model-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.model-input-row .n-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.model-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -55,8 +55,8 @@ const props = defineProps({
|
||||
|
||||
// Image role options | 图片角色选项
|
||||
const imageRoleOptions = [
|
||||
{ label: '图片', key: 'first_frame_image' },
|
||||
{ label: '结束图', key: 'last_frame_image' },
|
||||
{ label: '首帧', key: 'first_frame_image' },
|
||||
{ label: '尾帧', key: 'last_frame_image' },
|
||||
{ label: '参考图', key: 'input_reference' }
|
||||
]
|
||||
|
||||
@@ -66,7 +66,7 @@ const currentRole = computed(() => props.data?.imageRole || 'first_frame_image')
|
||||
// Current role label | 当前角色标签
|
||||
const currentRoleLabel = computed(() => {
|
||||
const option = imageRoleOptions.find(o => o.key === currentRole.value)
|
||||
return option?.label || '图片'
|
||||
return option?.label || '首帧'
|
||||
})
|
||||
|
||||
// Calculate bezier path | 计算贝塞尔路径
|
||||
@@ -95,7 +95,7 @@ const edgeStyle = computed(() => ({
|
||||
|
||||
// Handle role selection | 处理角色选择
|
||||
const handleRoleSelect = (role) => {
|
||||
// Keep endpoint image roles unique when advanced users edit edge roles.
|
||||
// If selecting first_frame or last_frame, ensure uniqueness | 如果选择首帧或尾帧,确保唯一性
|
||||
if (role === 'first_frame_image' || role === 'last_frame_image') {
|
||||
// Find other edges connected to the same target with the same role | 查找连接到同一目标且具有相同角色的其他边
|
||||
const sameTargetEdges = edges.value.filter(edge =>
|
||||
|
||||
@@ -586,7 +586,7 @@ const handleGenerate = async (mode = 'auto') => {
|
||||
}
|
||||
|
||||
if (!isConfigured.value) {
|
||||
window.$message?.warning('登录状态异常,请重新进入工作台')
|
||||
window.$message?.warning('生成接口未就绪,请稍后重试')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -932,7 +932,7 @@ const handleVideoGen = () => {
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'imageRole',
|
||||
data: { imageRole: 'first_frame_image' } // Default reference image | 默认参考图
|
||||
data: { imageRole: 'first_frame_image' } // Default to first frame | 默认首帧
|
||||
})
|
||||
|
||||
// Connect text node to config node | 连接文本节点到配置节点
|
||||
|
||||
@@ -764,7 +764,7 @@ const getInputFromConnections = () => {
|
||||
// Handle generate | 处理生成
|
||||
const handleGenerate = async () => {
|
||||
if (!isApiConfigured.value) {
|
||||
window.$message?.warning('登录状态异常,请重新进入工作台')
|
||||
window.$message?.warning('生成接口未就绪,请稍后重试')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -623,7 +623,7 @@ const handlePolish = async () => {
|
||||
|
||||
// Check API configuration | 检查 API 配置
|
||||
if (!isApiConfigured.value) {
|
||||
window.$message?.warning('登录状态异常,请重新进入工作台')
|
||||
window.$message?.warning('生成接口未就绪,请稍后重试')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,16 @@
|
||||
提示词 {{ connectedPrompt ? '✓' : '○' }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full"
|
||||
:class="connectedImages.length > 0 ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
图片 {{ connectedImages.length > 0 ? `✓ ${connectedImages.length}` : '○' }}
|
||||
:class="imagesByRole.firstFrame ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
首帧 {{ imagesByRole.firstFrame ? '✓' : '○' }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full"
|
||||
:class="imagesByRole.lastFrame ? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
尾帧 {{ imagesByRole.lastFrame ? '✓' : '○' }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full"
|
||||
:class="imagesByRole.referenceImages.length > 0 ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
参考图 {{ imagesByRole.referenceImages.length > 0 ? `✓ ${imagesByRole.referenceImages.length}` : '○' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -187,7 +195,7 @@ const connectedImages = computed(() => {
|
||||
edgeId: edge.id,
|
||||
url: sourceNode.data.url,
|
||||
base64: sourceNode.data.base64,
|
||||
role: edge.data?.imageRole || 'first_frame_image' // Default reference image | 默认参考图
|
||||
role: edge.data?.imageRole || 'first_frame_image' // Default to first frame | 默认首帧
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -334,7 +342,7 @@ const handleGenerate = async () => {
|
||||
}
|
||||
|
||||
if (!isConfigured.value) {
|
||||
window.$message?.warning('登录状态异常,请重新进入工作台')
|
||||
window.$message?.warning('生成接口未就绪,请稍后重试')
|
||||
isGenerating.value = false
|
||||
return
|
||||
}
|
||||
@@ -377,12 +385,12 @@ const handleGenerate = async () => {
|
||||
params.prompt = prompt
|
||||
}
|
||||
|
||||
// Add primary reference image | 添加主参考图
|
||||
// Add first frame image | 添加首帧图片
|
||||
if (first_frame_image) {
|
||||
params.first_frame_image = first_frame_image
|
||||
}
|
||||
|
||||
// Add optional ending reference image | 添加可选结束参考图
|
||||
// Add last frame image | 添加尾帧图片
|
||||
if (last_frame_image) {
|
||||
params.last_frame_image = last_frame_image
|
||||
}
|
||||
|
||||
@@ -1,118 +1,269 @@
|
||||
/**
|
||||
* SKG model and size configuration.
|
||||
* These values mirror the backend /health capabilities and keep the canvas UI simple.
|
||||
* Models Configuration | 模型配置
|
||||
* Centralized model configuration | 集中模型配置
|
||||
*/
|
||||
|
||||
export const SKG_IMAGE_SIZE_OPTIONS = [
|
||||
{ label: '自动', key: 'auto' },
|
||||
{ label: '竖图 2:3', key: '1024x1536' },
|
||||
{ label: '方图 1:1', key: '1024x1024' },
|
||||
{ label: '横图 3:2', key: '1536x1024' }
|
||||
// Seedream image size options | 豆包图片尺寸选项
|
||||
export const SEEDREAM_SIZE_OPTIONS = [
|
||||
{ label: '21:9', key: '3024x1296' },
|
||||
{ label: '16:9', key: '2560x1440' },
|
||||
{ label: '4:3', key: '2304x1728' },
|
||||
{ label: '3:2', key: '2496x1664' },
|
||||
{ label: '1:1', key: '2048x2048' },
|
||||
{ label: '2:3', key: '1664x2496' },
|
||||
{ label: '3:4', key: '1728x2304' },
|
||||
{ label: '9:16', key: '1440x2560' },
|
||||
{ label: '9:21', key: '1296x3024' }
|
||||
]
|
||||
|
||||
export const SKG_IMAGE_QUALITY_OPTIONS = [
|
||||
{ label: '标准', key: 'standard' }
|
||||
// Seedream 4K image size options | 豆包4K图片尺寸选项
|
||||
export const SEEDREAM_4K_SIZE_OPTIONS = [
|
||||
{ label: '21:9', key: '6198x2656' },
|
||||
{ label: '16:9', key: '5404x3040' },
|
||||
{ label: '4:3', key: '4694x3520' },
|
||||
{ label: '3:2', key: '4992x3328' },
|
||||
{ label: '1:1', key: '4096x4096' },
|
||||
{ label: '2:3', key: '3328x4992' },
|
||||
{ label: '3:4', key: '3520x4694' },
|
||||
{ label: '9:16', key: '3040x5404' },
|
||||
{ label: '9:21', key: '2656x6198' }
|
||||
]
|
||||
|
||||
export const SKG_VIDEO_SIZE_OPTIONS = [
|
||||
{ label: '竖屏 9:16', key: '720x1280' },
|
||||
{ label: '横屏 16:9', key: '1280x720' },
|
||||
{ label: '方形 1:1', key: '1024x1024' },
|
||||
{ label: '竖屏 3:4', key: '960x1280' }
|
||||
// Seedream quality options | 豆包画质选项
|
||||
export const SEEDREAM_QUALITY_OPTIONS = [
|
||||
{ label: '标准画质', key: 'standard' },
|
||||
{ label: '4K 高清', key: '4k' }
|
||||
]
|
||||
|
||||
export const VIDEO_RATIO_LIST = SKG_VIDEO_SIZE_OPTIONS
|
||||
|
||||
export const SEEDANCE_RESOLUTION_OPTIONS = [
|
||||
{ label: '720p', key: '720p' },
|
||||
{ label: '1080p', key: '1080p' }
|
||||
export const BANANA_SIZE_OPTIONS = [
|
||||
{ label: '16:9', key: '16x9' },
|
||||
{ label: '4:3', key: '4x3' },
|
||||
{ label: '3:2', key: '3x2' },
|
||||
{ label: '1:1', key: '1x1' },
|
||||
{ label: '2:3', key: '2x3' },
|
||||
{ label: '3:4', key: '3x4' },
|
||||
{ label: '9:16', key: '9x16' },
|
||||
]
|
||||
|
||||
// Image generation models | 图片生成模型
|
||||
export const IMAGE_MODELS = [
|
||||
{
|
||||
label: '自动',
|
||||
key: 'auto',
|
||||
provider: ['skg'],
|
||||
sizes: SKG_IMAGE_SIZE_OPTIONS.map(s => s.key),
|
||||
qualities: SKG_IMAGE_QUALITY_OPTIONS,
|
||||
defaultParams: { size: '1024x1536', quality: 'standard', style: 'commercial' }
|
||||
},
|
||||
{
|
||||
label: 'GPT Image 2',
|
||||
key: 'gpt-image-2',
|
||||
provider: ['skg'],
|
||||
sizes: SKG_IMAGE_SIZE_OPTIONS.map(s => s.key),
|
||||
qualities: SKG_IMAGE_QUALITY_OPTIONS,
|
||||
defaultParams: { size: '1024x1536', quality: 'standard', style: 'commercial' }
|
||||
},
|
||||
{
|
||||
label: 'Gemini 图片备用',
|
||||
key: 'gemini-3-pro-image-preview',
|
||||
provider: ['skg'],
|
||||
sizes: SKG_IMAGE_SIZE_OPTIONS.map(s => s.key),
|
||||
qualities: SKG_IMAGE_QUALITY_OPTIONS,
|
||||
defaultParams: { size: '1024x1536', quality: 'standard', style: 'commercial' }
|
||||
}
|
||||
{
|
||||
label: 'Nano Banana 2',
|
||||
key: 'nano-banana-2',
|
||||
provider: ['chatfire'], // 火宝渠道
|
||||
sizes: BANANA_SIZE_OPTIONS.map(s => s.key),
|
||||
// qualities: SEEDREAM_QUALITY_OPTIONS,
|
||||
// getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
|
||||
defaultParams: {
|
||||
size: '1x1',
|
||||
quality: 'standard',
|
||||
style: 'vivid'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Nano Banana Pro',
|
||||
key: 'nano-banana-pro',
|
||||
provider: ['chatfire'], // 火宝渠道
|
||||
sizes: BANANA_SIZE_OPTIONS.map(s => s.key),
|
||||
// qualities: SEEDREAM_QUALITY_OPTIONS,
|
||||
// getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
|
||||
defaultParams: {
|
||||
size: '1x1',
|
||||
quality: 'standard',
|
||||
style: 'vivid'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '豆包 Seedream 4.5',
|
||||
key: 'doubao-seedream-4-5-251128',
|
||||
provider: ['chatfire'], // 火宝渠道
|
||||
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
|
||||
qualities: SEEDREAM_QUALITY_OPTIONS,
|
||||
getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
|
||||
defaultParams: {
|
||||
size: '2048x2048',
|
||||
quality: 'standard',
|
||||
style: 'vivid'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Nano Banana',
|
||||
key: 'nano-banana',
|
||||
provider: ['chatfire'], // 火宝渠道
|
||||
tips: '尺寸写在提示词中: 尺寸 9:16',
|
||||
sizes: [],
|
||||
defaultParams: {
|
||||
quality: 'standard',
|
||||
style: 'vivid'
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
// Video ratio options | 视频比例选项
|
||||
export const VIDEO_RATIO_LIST = [
|
||||
{ label: '16:9 (横版)', key: '16x9' },
|
||||
{ label: '4:3', key: '4x3' },
|
||||
{ label: '1:1 (方形)', key: '1x1' },
|
||||
{ label: '3:4', key: '3x4' },
|
||||
{ label: '9:16 (竖版)', key: '9x16' }
|
||||
]
|
||||
|
||||
// Video resolution options for Seedance | Seedance 分辨率选项
|
||||
export const SEEDANCE_RESOLUTION_OPTIONS = [
|
||||
{ label: '480p', key: '480p' },
|
||||
{ label: '720p', key: '720p' },
|
||||
{ label: '1080p', key: '1080p' }
|
||||
]
|
||||
|
||||
// Video generation models | 视频生成模型
|
||||
export const VIDEO_MODELS = [
|
||||
{
|
||||
label: 'Seedance',
|
||||
key: 'seedance',
|
||||
provider: ['skg'],
|
||||
type: 't2v+i2v',
|
||||
ratios: SKG_VIDEO_SIZE_OPTIONS.map(s => s.key),
|
||||
durs: [5, 8, 10, 12, 15].map(s => ({ label: `${s} 秒`, key: s })),
|
||||
resolutions: ['720p', '1080p'],
|
||||
defaultResolution: '1080p',
|
||||
defaultParams: { ratio: '720x1280', duration: 10, resolution: '1080p' }
|
||||
},
|
||||
{
|
||||
label: 'Kling',
|
||||
key: 'kling',
|
||||
provider: ['skg'],
|
||||
type: 't2v+i2v',
|
||||
ratios: SKG_VIDEO_SIZE_OPTIONS.map(s => s.key),
|
||||
durs: [4, 8, 12].map(s => ({ label: `${s} 秒`, key: s })),
|
||||
resolutions: ['720p'],
|
||||
defaultResolution: '720p',
|
||||
defaultParams: { ratio: '720x1280', duration: 8, resolution: '720p' }
|
||||
},
|
||||
{
|
||||
label: 'Veo 3',
|
||||
key: 'veo3',
|
||||
provider: ['skg'],
|
||||
type: 't2v+i2v',
|
||||
ratios: SKG_VIDEO_SIZE_OPTIONS.map(s => s.key),
|
||||
durs: [4, 8, 12].map(s => ({ label: `${s} 秒`, key: s })),
|
||||
resolutions: ['720p'],
|
||||
defaultResolution: '720p',
|
||||
defaultParams: { ratio: '720x1280', duration: 8, resolution: '720p' }
|
||||
}
|
||||
// Seedance 模型 - 1.5 Pro
|
||||
{
|
||||
label: 'Seedance 1.5 Pro (图文视频)',
|
||||
key: 'doubao-seedance-1-5-pro-251215',
|
||||
provider: ['chatfire'],
|
||||
type: 't2v+i2v',
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
defaultResolution: '1080p',
|
||||
defaultParams: { ratio: '16:9', duration: 10, resolution: '1080p' }
|
||||
},
|
||||
// Seedance 模型 - 文生视频
|
||||
{
|
||||
label: 'Seedance 1.0 Lite (文生视频)',
|
||||
key: 'doubao-seedance-1-0-lite-t2v-250428',
|
||||
provider: ['chatfire'],
|
||||
type: 't2v', // 文生视频
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
defaultResolution: '720p',
|
||||
defaultParams: { ratio: '16:9', duration: 5, resolution: '720p' }
|
||||
},
|
||||
// Seedance 模型 - 图生视频
|
||||
{
|
||||
label: 'Seedance 1.0 Lite (图生视频)',
|
||||
key: 'doubao-seedance-1-0-lite-i2v-250428',
|
||||
provider: ['chatfire'],
|
||||
type: 'i2v', // 图生视频
|
||||
ratios: ['16:9'],
|
||||
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
defaultResolution: '720p',
|
||||
defaultParams: { ratio: '16:9', duration: 5, resolution: '720p' }
|
||||
},
|
||||
// Seedance 模型 - 图文视频 Pro
|
||||
{
|
||||
label: 'Seedance 1.0 Pro (图文视频)',
|
||||
key: 'doubao-seedance-1-0-pro-250528',
|
||||
provider: ['chatfire'],
|
||||
type: 't2v+i2v', // 图文视频
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9', '16:9'],
|
||||
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
defaultResolution: '1080p',
|
||||
defaultParams: { ratio: '16:9', duration: 5, resolution: '1080p' }
|
||||
},
|
||||
|
||||
// Seedance 模型 - 1.0 Pro Fast
|
||||
{
|
||||
label: 'Seedance 1.0 Pro Fast (图文视频)',
|
||||
key: 'doubao-seedance-1-0-pro-fast-251015',
|
||||
provider: ['chatfire'],
|
||||
type: 't2v+i2v',
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
defaultResolution: '1080p',
|
||||
defaultParams: { ratio: '16:9', duration: 5, resolution: '1080p' }
|
||||
},
|
||||
// 可灵 Kling
|
||||
// {
|
||||
// label: '可灵 Kling v2.5-turbo',
|
||||
// key: 'kling-v2-1',
|
||||
// provider: ['chatfire'], // 仅火宝渠道
|
||||
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
|
||||
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
// defaultParams: { ratio: '9:16', duration: 10 }
|
||||
// },
|
||||
// {
|
||||
// label: 'runway/gen4-turbo',
|
||||
// key: 'runway/gen4-turbo',
|
||||
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
|
||||
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
// defaultParams: { ratio: '16:9', duration: 5 }
|
||||
// },
|
||||
// {
|
||||
// label: '可灵视频 O1',
|
||||
// key: 'kling-video-o1',
|
||||
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
|
||||
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
// defaultParams: { ratio: '16:9', duration: 5 }
|
||||
// },
|
||||
// {
|
||||
// label: 'viduq2-pro_720p', key: 'viduq2-pro_720p',
|
||||
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
|
||||
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
// defaultParams: { ratio: '16:9', duration: 5 }
|
||||
// },
|
||||
// {
|
||||
// label: 'Sora 2', key: 'sora-2',
|
||||
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
|
||||
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
|
||||
// defaultParams: { ratio: '16:9', duration: 5 }
|
||||
// }
|
||||
]
|
||||
|
||||
// Chat/LLM models | 对话模型
|
||||
export const CHAT_MODELS = [
|
||||
{ label: 'SKG 提示词助手', key: 'skg-copy', provider: ['skg'] }
|
||||
{ label: 'GPT-4o Mini', key: 'gpt-4o-mini', provider: ['openai'] },
|
||||
{ label: 'GPT-4o', key: 'gpt-4o', provider: ['openai'] },
|
||||
{ label: 'GPT-5.2', key: 'gpt-5.2', provider: ['openai'] },
|
||||
{ label: 'DeepSeek Chat', key: 'deepseek-chat', provider: ['openai', 'chatfire'] },
|
||||
{ label: '豆包 Seed Flash', key: 'doubao-seed-1-6-flash-250615', provider: ['chatfire'] },
|
||||
{ label: 'Gemini 3 Pro', key: 'gemini-3-pro', provider: ['openai'] }
|
||||
]
|
||||
|
||||
export const IMAGE_SIZE_OPTIONS = SKG_IMAGE_SIZE_OPTIONS
|
||||
export const IMAGE_QUALITY_OPTIONS = SKG_IMAGE_QUALITY_OPTIONS
|
||||
export const IMAGE_STYLE_OPTIONS = [{ label: '商业营销', key: 'commercial' }]
|
||||
export const VIDEO_RATIO_OPTIONS = SKG_VIDEO_SIZE_OPTIONS
|
||||
export const VIDEO_DURATION_OPTIONS = [5, 8, 10, 12, 15].map(s => ({ label: `${s} 秒`, key: s }))
|
||||
// Image size options | 图片尺寸选项
|
||||
export const IMAGE_SIZE_OPTIONS = [
|
||||
{ label: '2048x2048', key: '2048x2048' },
|
||||
{ label: '1792x1024 (横版)', key: '1792x1024' },
|
||||
{ label: '1024x1792 (竖版)', key: '1024x1792' }
|
||||
]
|
||||
|
||||
export const SEEDREAM_SIZE_OPTIONS = SKG_IMAGE_SIZE_OPTIONS
|
||||
export const SEEDREAM_4K_SIZE_OPTIONS = SKG_IMAGE_SIZE_OPTIONS
|
||||
export const SEEDREAM_QUALITY_OPTIONS = SKG_IMAGE_QUALITY_OPTIONS
|
||||
// Image quality options | 图片质量选项
|
||||
export const IMAGE_QUALITY_OPTIONS = [
|
||||
{ label: '标准', key: 'standard' },
|
||||
{ label: '高清', key: 'hd' }
|
||||
]
|
||||
|
||||
export const DEFAULT_IMAGE_MODEL = 'auto'
|
||||
export const DEFAULT_VIDEO_MODEL = 'seedance'
|
||||
export const DEFAULT_CHAT_MODEL = 'skg-copy'
|
||||
export const DEFAULT_IMAGE_SIZE = '1024x1536'
|
||||
export const DEFAULT_VIDEO_RATIO = '720x1280'
|
||||
export const DEFAULT_VIDEO_DURATION = 10
|
||||
// Image style options | 图片风格选项
|
||||
export const IMAGE_STYLE_OPTIONS = [
|
||||
{ label: '生动', key: 'vivid' },
|
||||
{ label: '自然', key: 'natural' }
|
||||
]
|
||||
|
||||
// Video ratio options | 视频比例选项
|
||||
export const VIDEO_RATIO_OPTIONS = VIDEO_RATIO_LIST
|
||||
|
||||
// Video duration options | 视频时长选项
|
||||
export const VIDEO_DURATION_OPTIONS = [
|
||||
{ label: '5 秒', key: 5 },
|
||||
{ label: '10 秒', key: 10 }
|
||||
]
|
||||
|
||||
// Default values | 默认值
|
||||
export const DEFAULT_IMAGE_MODEL = 'nano-banana-pro'
|
||||
export const DEFAULT_VIDEO_MODEL = 'doubao-seedance-1-5-pro-251215'
|
||||
export const DEFAULT_CHAT_MODEL = 'gpt-4o-mini'
|
||||
export const DEFAULT_IMAGE_SIZE = '2048x2048'
|
||||
export const DEFAULT_VIDEO_RATIO = '16:9'
|
||||
export const DEFAULT_VIDEO_DURATION = 5
|
||||
|
||||
// Get model by key | 根据 key 获取模型
|
||||
export const getModelByName = (key) => {
|
||||
const allModels = [...IMAGE_MODELS, ...VIDEO_MODELS, ...CHAT_MODELS]
|
||||
return allModels.find(m => m.key === key)
|
||||
const allModels = [...IMAGE_MODELS, ...VIDEO_MODELS, ...CHAT_MODELS]
|
||||
return allModels.find(m => m.key === key)
|
||||
}
|
||||
|
||||
@@ -1,40 +1,272 @@
|
||||
/**
|
||||
* SKG internal provider config.
|
||||
* The browser never receives upstream model keys; all generation goes through /api.
|
||||
* API Provider Adapters | API 渠道适配器
|
||||
* 适配不同 API 提供商的请求参数和响应格式
|
||||
*/
|
||||
|
||||
// 渠道适配配置
|
||||
export const PROVIDERS = {
|
||||
skg: {
|
||||
label: 'SKG 内部模型',
|
||||
chatfire: {
|
||||
label: 'SKG 内部',
|
||||
defaultBaseUrl: '/api',
|
||||
// 端点路径
|
||||
endpoints: {
|
||||
chat: '/creative/copy',
|
||||
image: '/jobs/{jobId}/frames/{idx}/generate',
|
||||
video: '/jobs/{jobId}/frames/{idx}/storyboard/video',
|
||||
videoQuery: '/jobs/{jobId}'
|
||||
chat: '/v1/chat/completions',
|
||||
image: '/v1/images/generations',
|
||||
video: '/v1/video/generations',
|
||||
videoQuery: '/v1/video/task/{taskId}'
|
||||
},
|
||||
// 火宝渠道请求适配
|
||||
requestAdapter: {
|
||||
chat: (params) => params,
|
||||
image: (params) => params,
|
||||
video: (params) => params
|
||||
chat: (params) => {
|
||||
const adapted = {
|
||||
model: params.model,
|
||||
messages: params.messages
|
||||
}
|
||||
if (params.temperature !== undefined) adapted.temperature = params.temperature
|
||||
if (params.max_tokens !== undefined) adapted.max_tokens = params.max_tokens
|
||||
if (params.stream !== undefined) adapted.stream = params.stream
|
||||
return adapted
|
||||
},
|
||||
image: (params) => {
|
||||
const adapted = {
|
||||
model: params.model,
|
||||
prompt: params.prompt
|
||||
}
|
||||
if (params.size) adapted.size = params.size
|
||||
if (params.n) adapted.n = params.n
|
||||
if (params.quality) adapted.quality = params.quality
|
||||
if (params.style) adapted.style = params.style
|
||||
if (params.image) adapted.image = params.image
|
||||
return adapted
|
||||
},
|
||||
video: (params) => {
|
||||
const model = params.model || ''
|
||||
|
||||
// Seedance 模型 - 使用 content 数组格式
|
||||
if (model.includes('seedance')) {
|
||||
const content = []
|
||||
|
||||
// 构建完整参数文本
|
||||
// 格式: prompt --resolution 720p --ratio 16:9 --dur 5 --fps 24 --wm true --seed 11 --cf false
|
||||
let textPrompt = params.prompt || ''
|
||||
|
||||
// 添加 resolution 参数
|
||||
if (params.resolution) {
|
||||
textPrompt += ` --resolution ${params.resolution}`
|
||||
}
|
||||
|
||||
// 添加 ratio 参数 (图生视频用 16:9)
|
||||
if (params.size) {
|
||||
textPrompt += ` --ratio ${params.size}`
|
||||
}
|
||||
|
||||
// 添加 duration 参数
|
||||
if (params.seconds) {
|
||||
textPrompt += ` --dur ${params.seconds}`
|
||||
}
|
||||
|
||||
// 添加 fps (固定 24)
|
||||
textPrompt += ` --fps 24`
|
||||
|
||||
// 添加水印参数 (默认 true)
|
||||
textPrompt += ` --wm ${params.wm !== false ? 'true' : 'false'}`
|
||||
|
||||
// 添加 seed 参数 (可选)
|
||||
if (params.seed !== undefined) {
|
||||
textPrompt += ` --seed ${params.seed}`
|
||||
}
|
||||
|
||||
// 添加 cf 参数 (默认 false)
|
||||
textPrompt += ` --cf ${params.cf === true ? 'true' : 'false'}`
|
||||
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: textPrompt
|
||||
})
|
||||
|
||||
// 添加参考图(如果有)
|
||||
if (params.first_frame_image) {
|
||||
content.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: params.first_frame_image
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const adapted = {
|
||||
model: model,
|
||||
content: content,
|
||||
generate_audio: params.generateAudio !== false
|
||||
}
|
||||
|
||||
return adapted
|
||||
}
|
||||
|
||||
// Kling 模型 - 使用 kling 特定格式
|
||||
if (model.includes('kling')) {
|
||||
// 将 ratio 转换为 aspect_ratio 格式
|
||||
const ratioMap = {
|
||||
'16:9': '16:9',
|
||||
'9:16': '9:16',
|
||||
'1:1': '1:1',
|
||||
'4:3': '4:3',
|
||||
'3:4': '3:4'
|
||||
}
|
||||
|
||||
const adapted = {
|
||||
model_name: model,
|
||||
mode: 'std',
|
||||
prompt: params.prompt || '',
|
||||
aspect_ratio: ratioMap[params.size] || '16:9',
|
||||
duration: params.seconds || 5,
|
||||
negative_prompt: '',
|
||||
cfg_scale: 0.5
|
||||
}
|
||||
|
||||
// 添加参考图(如果有)
|
||||
if (params.first_frame_image) {
|
||||
adapted.image = params.first_frame_image
|
||||
}
|
||||
|
||||
return adapted
|
||||
}
|
||||
|
||||
// 默认格式(veo 等)
|
||||
const adapted = {
|
||||
model: params.model,
|
||||
prompt: params.prompt || ''
|
||||
}
|
||||
if (params.first_frame_image) adapted.first_frame_image = params.first_frame_image
|
||||
if (params.last_frame_image) adapted.last_frame_image = params.last_frame_image
|
||||
if (params.size) adapted.size = params.size
|
||||
if (params.seconds) adapted.seconds = params.seconds
|
||||
|
||||
return adapted
|
||||
}
|
||||
},
|
||||
// 火宝渠道响应格式
|
||||
responseAdapter: {
|
||||
chat: (response) => response,
|
||||
image: (response) => response,
|
||||
video: (response) => response
|
||||
chat: (response) => {
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
return response.choices[0].message?.content || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
image: (response) => {
|
||||
const data = response.data || response
|
||||
return (Array.isArray(data) ? data : [data]).map(item => ({
|
||||
url: item.url || item.b64_json || '',
|
||||
revisedPrompt: item.revised_prompt || ''
|
||||
}))
|
||||
},
|
||||
video: (response) => {
|
||||
return {
|
||||
url: response.data?.url || response.url || response.data?.[0]?.url || '',
|
||||
...response
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
default: 'skg'
|
||||
openai: {
|
||||
label: 'OpenAI',
|
||||
defaultBaseUrl: 'https://api.openai.com',
|
||||
// 端点路径
|
||||
endpoints: {
|
||||
chat: '/v1/chat/completions',
|
||||
image: '/v1/images/generations',
|
||||
video: '/v1/videos',
|
||||
videoQuery: '/v1/videos/{taskId}'
|
||||
},
|
||||
// 请求参数适配
|
||||
requestAdapter: {
|
||||
chat: (params) => {
|
||||
const adapted = {
|
||||
model: params.model,
|
||||
messages: params.messages
|
||||
}
|
||||
// 添加可选参数
|
||||
if (params.temperature !== undefined) adapted.temperature = params.temperature
|
||||
if (params.max_tokens !== undefined) adapted.max_tokens = params.max_tokens
|
||||
if (params.stream !== undefined) adapted.stream = params.stream
|
||||
return adapted
|
||||
},
|
||||
image: (params) => {
|
||||
const adapted = {
|
||||
model: params.model,
|
||||
prompt: params.prompt
|
||||
}
|
||||
if (params.size) adapted.size = params.size
|
||||
if (params.n) adapted.n = params.n
|
||||
if (params.quality) adapted.quality = params.quality
|
||||
if (params.style) adapted.style = params.style
|
||||
if (params.image) adapted.image = params.image
|
||||
return adapted
|
||||
},
|
||||
video: (params) => {
|
||||
const adapted = {
|
||||
model: params.model,
|
||||
prompt: params.prompt || ''
|
||||
}
|
||||
if (params.first_frame_image) adapted.first_frame_image = params.first_frame_image
|
||||
if (params.last_frame_image) adapted.last_frame_image = params.last_frame_image
|
||||
if (params.size) adapted.size = params.size
|
||||
if (params.seconds) adapted.seconds = params.seconds
|
||||
return adapted
|
||||
}
|
||||
},
|
||||
// 响应数据适配
|
||||
responseAdapter: {
|
||||
chat: (response) => {
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
return response.choices[0].message?.content || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
image: (response) => {
|
||||
const data = response.data || response
|
||||
return (Array.isArray(data) ? data : [data]).map(item => ({
|
||||
url: item.url || item.b64_json || '',
|
||||
revisedPrompt: item.revised_prompt || ''
|
||||
}))
|
||||
},
|
||||
video: (response) => {
|
||||
return {
|
||||
url: response.data?.url || response.url || response.data?.[0]?.url || '',
|
||||
...response
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 默认使用 OpenAI 格式
|
||||
default: 'chatfire'
|
||||
}
|
||||
|
||||
export const getProviderList = () => (
|
||||
Object.entries(PROVIDERS)
|
||||
// 获取渠道列表
|
||||
export const getProviderList = () => {
|
||||
return Object.entries(PROVIDERS)
|
||||
.filter(([key]) => key !== 'default')
|
||||
.map(([key, value]) => ({ key, label: value.label }))
|
||||
)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
label: value.label
|
||||
}))
|
||||
}
|
||||
|
||||
export const getDefaultProvider = () => PROVIDERS.default || 'skg'
|
||||
// 获取默认渠道
|
||||
export const getDefaultProvider = () => {
|
||||
return PROVIDERS.default || 'chatfire'
|
||||
}
|
||||
|
||||
export const getProviderConfig = (provider) => PROVIDERS[provider] || PROVIDERS.skg
|
||||
// 获取渠道的默认 Base URL
|
||||
export const getDefaultBaseUrl = (providerKey) => {
|
||||
const config = getProviderConfig(providerKey)
|
||||
return config.defaultBaseUrl || ''
|
||||
}
|
||||
|
||||
export const getDefaultBaseUrl = (provider) => getProviderConfig(provider).defaultBaseUrl
|
||||
// 获取渠道配置
|
||||
export const getProviderConfig = (providerKey) => {
|
||||
return PROVIDERS[providerKey] || PROVIDERS[PROVIDERS.default]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@ const customChatModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_CHAT_MODELS, []))
|
||||
const customImageModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_MODELS, []))
|
||||
const customVideoModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_VIDEO_MODELS, []))
|
||||
|
||||
// 按渠道存储的自定义模型 | 结构: { 'skg': [{key, label}] }
|
||||
// 按渠道存储的自定义模型 | 结构: { 'openai': [{key, label}], 'chatfire': [{key, label}] }
|
||||
const customChatModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_CHAT_MODELS_BY_PROVIDER || 'custom-chat-models-by-provider', {}))
|
||||
const customImageModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_MODELS_BY_PROVIDER || 'custom-image-models-by-provider', {}))
|
||||
const customVideoModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_VIDEO_MODELS_BY_PROVIDER || 'custom-video-models-by-provider', {}))
|
||||
@@ -411,7 +411,7 @@ export const useModelConfig = () => {
|
||||
getImageModel,
|
||||
getVideoModel,
|
||||
|
||||
// Get models by provider
|
||||
// Get models by provider (for ApiSettings)
|
||||
getModelsByProvider,
|
||||
|
||||
// Custom models by provider
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,14 @@ const setStored = (key, value) => {
|
||||
}
|
||||
}
|
||||
|
||||
const removeStored = (key) => {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored JSON value from localStorage
|
||||
*/
|
||||
@@ -94,7 +102,8 @@ export const useModelStore = defineStore('model', () => {
|
||||
// ============ Provider 状态 | Provider State ============
|
||||
|
||||
// 当前选中的渠道
|
||||
const currentProvider = ref(getStored(STORAGE_KEYS.PROVIDER) || getDefaultProvider())
|
||||
const storedProvider = getStored(STORAGE_KEYS.PROVIDER)
|
||||
const currentProvider = ref(PROVIDERS[storedProvider] ? storedProvider : getDefaultProvider())
|
||||
|
||||
// 渠道列表
|
||||
const providerList = computed(() => getProviderList())
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Header | 顶部导航 -->
|
||||
<AppHeader class="bg-[var(--bg-secondary)]">
|
||||
<template #left>
|
||||
<button
|
||||
<button
|
||||
@click="goBack"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
>
|
||||
@@ -18,7 +18,7 @@
|
||||
</n-dropdown>
|
||||
</template>
|
||||
<template #right>
|
||||
<button
|
||||
<button
|
||||
@click="showDownloadModal = true"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
:class="{ 'text-[var(--accent-color)]': hasDownloadableAssets }"
|
||||
@@ -26,6 +26,14 @@
|
||||
>
|
||||
<n-icon :size="20"><DownloadOutline /></n-icon>
|
||||
</button>
|
||||
<button
|
||||
@click="showApiSettings = true"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
:class="{ 'text-[var(--accent-color)]': isApiConfigured }"
|
||||
title="API 设置"
|
||||
>
|
||||
<n-icon :size="20"><SettingsOutline /></n-icon>
|
||||
</button>
|
||||
</template>
|
||||
</AppHeader>
|
||||
|
||||
@@ -52,7 +60,7 @@
|
||||
class="canvas-flow"
|
||||
>
|
||||
<Background v-if="showGrid" :gap="20" :size="1" />
|
||||
<MiniMap
|
||||
<MiniMap
|
||||
v-if="!isMobile"
|
||||
position="bottom-right"
|
||||
:pannable="true"
|
||||
@@ -62,14 +70,14 @@
|
||||
|
||||
<!-- Left toolbar | 左侧工具栏 -->
|
||||
<aside class="absolute left-4 top-1/2 -translate-y-1/2 flex flex-col gap-1 p-2 bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] shadow-lg z-10">
|
||||
<button
|
||||
<button
|
||||
@click="showNodeMenu = !showNodeMenu"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-xl bg-[var(--accent-color)] text-white hover:bg-[var(--accent-hover)] transition-colors"
|
||||
title="添加节点"
|
||||
>
|
||||
<n-icon :size="20"><AddOutline /></n-icon>
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
@click="showWorkflowPanel = true"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-xl hover:bg-[var(--bg-tertiary)] transition-colors"
|
||||
title="工作流模板"
|
||||
@@ -77,8 +85,8 @@
|
||||
<n-icon :size="20"><AppsOutline /></n-icon>
|
||||
</button>
|
||||
<div class="w-full h-px bg-[var(--border-color)] my-1"></div>
|
||||
<button
|
||||
v-for="tool in tools"
|
||||
<button
|
||||
v-for="tool in tools"
|
||||
:key="tool.id"
|
||||
@click="tool.action"
|
||||
:disabled="tool.disabled && tool.disabled()"
|
||||
@@ -90,12 +98,12 @@
|
||||
</aside>
|
||||
|
||||
<!-- Node menu popup | 节点菜单弹窗 -->
|
||||
<div
|
||||
<div
|
||||
v-if="showNodeMenu"
|
||||
class="absolute left-20 top-1/2 -translate-y-1/2 bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] shadow-lg p-2 z-20"
|
||||
>
|
||||
<button
|
||||
v-for="nodeType in nodeTypeOptions"
|
||||
<button
|
||||
v-for="nodeType in nodeTypeOptions"
|
||||
:key="nodeType.type"
|
||||
@click="addNewNode(nodeType.type)"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors text-left"
|
||||
@@ -107,16 +115,16 @@
|
||||
|
||||
<!-- Bottom controls | 底部控制 -->
|
||||
<div class="absolute bottom-4 left-4 flex items-center gap-2 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)] p-1">
|
||||
<!-- <button
|
||||
@click="showGrid = !showGrid"
|
||||
<!-- <button
|
||||
@click="showGrid = !showGrid"
|
||||
:class="showGrid ? 'bg-[var(--accent-color)] text-white' : 'hover:bg-[var(--bg-tertiary)]'"
|
||||
class="p-2 rounded transition-colors"
|
||||
title="切换网格"
|
||||
>
|
||||
<n-icon :size="16"><GridOutline /></n-icon>
|
||||
</button> -->
|
||||
<button
|
||||
@click="fitView({ padding: 0.2 })"
|
||||
<button
|
||||
@click="fitView({ padding: 0.2 })"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded transition-colors"
|
||||
title="适应视图"
|
||||
>
|
||||
@@ -136,13 +144,13 @@
|
||||
<!-- Bottom input panel (floating) | 底部输入面板(悬浮) -->
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4 z-20">
|
||||
<!-- Processing indicator | 处理中指示器 -->
|
||||
<div
|
||||
v-if="isProcessing"
|
||||
<div
|
||||
v-if="isProcessing"
|
||||
class="mb-3 p-3 bg-[var(--bg-primary)] rounded-xl border border-[var(--accent-color)] animate-pulse"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-sm text-[var(--accent-color)] mb-2">
|
||||
<n-spin :size="14" />
|
||||
<span>正在创建生成任务...</span>
|
||||
<span>正在生成提示词...</span>
|
||||
</div>
|
||||
<div v-if="currentResponse" class="text-sm text-[var(--text-primary)] whitespace-pre-wrap">
|
||||
{{ currentResponse }}
|
||||
@@ -150,31 +158,6 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-[var(--bg-primary)] rounded-xl border border-[var(--border-color)] p-3">
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
v-for="modeItem in creationModes"
|
||||
:key="modeItem.id"
|
||||
@click="setCreationMode(modeItem.id)"
|
||||
class="px-3 py-1.5 text-xs rounded-lg border transition-colors"
|
||||
:class="creationMode === modeItem.id ? 'bg-[var(--accent-color)] text-white border-[var(--accent-color)]' : 'bg-[var(--bg-secondary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'"
|
||||
>
|
||||
{{ modeItem.label }}
|
||||
</button>
|
||||
<label
|
||||
v-if="needsFirstFrame"
|
||||
class="px-3 py-1.5 text-xs rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-secondary)] cursor-pointer hover:text-[var(--text-primary)]"
|
||||
>
|
||||
{{ firstFrameFile ? `图片 · ${firstFrameFile.name}` : '上传图片' }}
|
||||
<input type="file" accept="image/*" class="hidden" @change="event => handleFrameFile('first', event)" />
|
||||
</label>
|
||||
<button
|
||||
v-if="firstFrameFile"
|
||||
@click="clearFrameFiles"
|
||||
class="px-2 py-1.5 text-xs rounded-lg text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)]"
|
||||
>
|
||||
清空图片
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="chatInput"
|
||||
:placeholder="inputPlaceholder"
|
||||
@@ -186,14 +169,23 @@
|
||||
/>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="firstFramePreview" class="h-8 w-8 overflow-hidden rounded-md border border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
||||
<img :src="firstFramePreview" alt="参考图片" class="h-full w-full object-cover" />
|
||||
</span>
|
||||
<button
|
||||
@click="handlePolish"
|
||||
:disabled="isProcessing || !chatInput.trim()"
|
||||
class="px-3 py-1.5 text-xs rounded-lg bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="AI 润色提示词"
|
||||
>
|
||||
✨ AI 润色
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
<label class="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
||||
<n-switch v-model:value="autoExecute" size="small" />
|
||||
自动执行
|
||||
</label>
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="isProcessing || !canSubmit"
|
||||
:disabled="isProcessing"
|
||||
class="w-8 h-8 rounded-xl bg-[var(--accent-color)] hover:bg-[var(--accent-hover)] flex items-center justify-center transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<n-spin v-if="isProcessing" :size="16" />
|
||||
@@ -202,10 +194,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Quick suggestions | 快捷建议 -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 mt-2">
|
||||
<span class="text-xs text-[var(--text-secondary)]">推荐:</span>
|
||||
<button
|
||||
v-for="tag in suggestions"
|
||||
:key="tag"
|
||||
@click="chatInput = tag"
|
||||
class="px-2 py-0.5 text-xs rounded-full bg-[var(--bg-secondary)]/80 border border-[var(--border-color)] hover:border-[var(--accent-color)] transition-colors"
|
||||
>
|
||||
{{ tag }}
|
||||
</button>
|
||||
<button class="p-1 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
|
||||
<n-icon :size="14"><RefreshOutline /></n-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Settings Modal | API 设置弹窗 -->
|
||||
<ApiSettings v-model:show="showApiSettings" />
|
||||
|
||||
<!-- Rename Modal | 重命名弹窗 -->
|
||||
<n-modal v-model:show="showRenameModal" preset="dialog" title="重命名项目">
|
||||
<n-input v-model:value="renameValue" placeholder="请输入项目名称" />
|
||||
@@ -242,13 +252,15 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { VueFlow, useVueFlow } from '@vue-flow/core'
|
||||
import { Background } from '@vue-flow/background'
|
||||
import { MiniMap } from '@vue-flow/minimap'
|
||||
import { NIcon, NDropdown, NSpin, NModal, NInput, NButton } from 'naive-ui'
|
||||
import {
|
||||
import { NIcon, NSwitch, NDropdown, NMessageProvider, NSpin, NModal, NInput, NButton } from 'naive-ui'
|
||||
import {
|
||||
ChevronBackOutline,
|
||||
ChevronDownOutline,
|
||||
SettingsOutline,
|
||||
AddOutline,
|
||||
ImageOutline,
|
||||
SendOutline,
|
||||
RefreshOutline,
|
||||
TextOutline,
|
||||
VideocamOutline,
|
||||
ColorPaletteOutline,
|
||||
@@ -265,12 +277,19 @@ import {
|
||||
import { nodes, edges, addNode, addNodes, addEdge, addEdges, updateNode, initSampleData, loadProject, saveProject, clearCanvas, canvasViewport, updateViewport, undo, redo, canUndo, canRedo, manualSaveHistory, startBatchOperation, endBatchOperation } from '../stores/canvas'
|
||||
import { loadAllModels } from '../stores/models'
|
||||
import { useChat, useWorkflowOrchestrator } from '../hooks'
|
||||
import { useModelStore } from '../stores/pinia'
|
||||
import { projects, initProjectsStore, updateProject, renameProject, currentProject } from '../stores/projects'
|
||||
|
||||
// API Settings component | API 设置组件
|
||||
import ApiSettings from '../components/ApiSettings.vue'
|
||||
import DownloadModal from '../components/DownloadModal.vue'
|
||||
import WorkflowPanel from '../components/WorkflowPanel.vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
|
||||
// API Config state | API 配置状态
|
||||
const modelStore = useModelStore()
|
||||
const isApiConfigured = computed(() => !!modelStore.currentApiKey)
|
||||
|
||||
// Initialize models on page load | 页面加载时初始化模型
|
||||
onMounted(() => {
|
||||
loadAllModels()
|
||||
@@ -294,11 +313,11 @@ const CHAT_TEMPLATES = {
|
||||
const currentTemplate = ref('imagePrompt')
|
||||
|
||||
// Chat hook with image prompt template | 问答 hook
|
||||
const {
|
||||
loading: chatLoading,
|
||||
status: chatStatus,
|
||||
currentResponse,
|
||||
send: sendChat
|
||||
const {
|
||||
loading: chatLoading,
|
||||
status: chatStatus,
|
||||
currentResponse,
|
||||
send: sendChat
|
||||
} = useChat({
|
||||
systemPrompt: CHAT_TEMPLATES.imagePrompt.systemPrompt,
|
||||
model: CHAT_TEMPLATES.imagePrompt.model
|
||||
@@ -355,25 +374,12 @@ const edgeTypes = {
|
||||
// UI state | UI状态
|
||||
const showNodeMenu = ref(false)
|
||||
const chatInput = ref('')
|
||||
const autoExecute = ref(false)
|
||||
const isMobile = ref(false)
|
||||
const showGrid = ref(true)
|
||||
const showApiSettings = ref(false)
|
||||
const isProcessing = ref(false)
|
||||
|
||||
const creationModes = [
|
||||
{ id: 'text-image', label: '文生图' },
|
||||
{ id: 'text-video', label: '文生视频' },
|
||||
{ id: 'image-video', label: '图生视频' }
|
||||
]
|
||||
const creationMode = ref('text-image')
|
||||
const firstFrameFile = ref(null)
|
||||
const firstFramePreview = ref('')
|
||||
const needsFirstFrame = computed(() => creationMode.value === 'image-video')
|
||||
const canSubmit = computed(() => {
|
||||
if (!chatInput.value.trim()) return false
|
||||
if (needsFirstFrame.value && !firstFrameFile.value) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// Flow key for forcing re-render on project switch | 项目切换时强制重新渲染的 key
|
||||
const flowKey = ref(Date.now())
|
||||
|
||||
@@ -386,7 +392,7 @@ const renameValue = ref('')
|
||||
|
||||
// Check if has downloadable assets | 检查是否有可下载素材
|
||||
const hasDownloadableAssets = computed(() => {
|
||||
return nodes.value.some(n =>
|
||||
return nodes.value.some(n =>
|
||||
(n.type === 'image' || n.type === 'video') && n.data?.url
|
||||
)
|
||||
})
|
||||
@@ -426,61 +432,34 @@ const nodeTypeOptions = [
|
||||
]
|
||||
|
||||
// Input placeholder | 输入占位符
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (creationMode.value === 'text-image') return '写清楚画面、主体、构图、光线、比例和 SKG 产品露出方式'
|
||||
if (creationMode.value === 'image-video') return '上传图片后,写人物动作、镜头运动、产品细节保持和视频节奏'
|
||||
return '写清楚画面、动作、镜头、产品出现方式、视频比例和时长'
|
||||
})
|
||||
const inputPlaceholder = '你可以试着说"帮我生成一个二次元的卡通角色"'
|
||||
|
||||
const setCreationMode = (mode) => {
|
||||
creationMode.value = mode
|
||||
if (mode !== 'image-video') {
|
||||
clearFrameFiles()
|
||||
}
|
||||
}
|
||||
|
||||
const fileToDataUrl = (file) => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(String(reader.result || ''))
|
||||
reader.onerror = () => reject(reader.error || new Error('读取图片失败'))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
const handleFrameFile = (slot, event) => {
|
||||
const file = event?.target?.files?.[0]
|
||||
if (!file) return
|
||||
const url = URL.createObjectURL(file)
|
||||
if (slot === 'first') {
|
||||
if (firstFramePreview.value) URL.revokeObjectURL(firstFramePreview.value)
|
||||
firstFrameFile.value = file
|
||||
firstFramePreview.value = url
|
||||
}
|
||||
}
|
||||
|
||||
const clearFrameFiles = () => {
|
||||
if (firstFramePreview.value) URL.revokeObjectURL(firstFramePreview.value)
|
||||
firstFrameFile.value = null
|
||||
firstFramePreview.value = ''
|
||||
}
|
||||
// Quick suggestions | 快捷建议
|
||||
const suggestions = [
|
||||
'像个魔法森林',
|
||||
'三只不同的小猫',
|
||||
'生成多角度分镜',
|
||||
'夏日田野环绕漫步'
|
||||
]
|
||||
|
||||
// Add new node | 添加新节点
|
||||
const addNewNode = async (type) => {
|
||||
// Calculate viewport center position | 计算视口中心位置
|
||||
const viewportCenterX = -viewport.value.x / viewport.value.zoom + (window.innerWidth / 2) / viewport.value.zoom
|
||||
const viewportCenterY = -viewport.value.y / viewport.value.zoom + (window.innerHeight / 2) / viewport.value.zoom
|
||||
|
||||
|
||||
// Add node at viewport center | 在视口中心添加节点
|
||||
const nodeId = addNode(type, { x: viewportCenterX - 100, y: viewportCenterY - 100 })
|
||||
|
||||
|
||||
// Set highest z-index | 设置最高层级
|
||||
const maxZIndex = Math.max(0, ...nodes.value.map(n => n.zIndex || 0))
|
||||
updateNode(nodeId, { zIndex: maxZIndex + 1 })
|
||||
|
||||
|
||||
// Force Vue Flow to recalculate node dimensions | 强制 Vue Flow 重新计算节点尺寸
|
||||
setTimeout(() => {
|
||||
updateNodeInternals(nodeId)
|
||||
}, 50)
|
||||
|
||||
|
||||
showNodeMenu.value = false
|
||||
}
|
||||
|
||||
@@ -543,22 +522,22 @@ const onConnect = (params) => {
|
||||
// Check connection types | 检查连接类型
|
||||
const sourceNode = nodes.value.find(n => n.id === params.source)
|
||||
const targetNode = nodes.value.find(n => n.id === params.target)
|
||||
|
||||
|
||||
if (sourceNode?.type === 'image' && targetNode?.type === 'videoConfig') {
|
||||
// Use imageRole edge type | 使用图片角色边类型
|
||||
addEdge({
|
||||
...params,
|
||||
type: 'imageRole',
|
||||
data: { imageRole: 'first_frame_image' } // Default reference image | 默认参考图
|
||||
data: { imageRole: 'first_frame_image' } // Default to first frame | 默认首帧
|
||||
})
|
||||
} else if (sourceNode?.type === 'text' && targetNode?.type === 'imageConfig') {
|
||||
// Use promptOrder edge type | 使用提示词顺序边类型
|
||||
// Calculate next order number | 计算下一个顺序号
|
||||
const existingTextEdges = edges.value.filter(e =>
|
||||
const existingTextEdges = edges.value.filter(e =>
|
||||
e.target === params.target && e.type === 'promptOrder'
|
||||
)
|
||||
const nextOrder = existingTextEdges.length + 1
|
||||
|
||||
|
||||
addEdge({
|
||||
...params,
|
||||
type: 'promptOrder',
|
||||
@@ -625,7 +604,7 @@ const onNodeClick = (event) => {
|
||||
// nodes.value.forEach(node => {
|
||||
// updateNode(node.id, { selected: false })
|
||||
// })
|
||||
|
||||
|
||||
// // Select clicked node | 选中的节点
|
||||
// const clickedNode = nodes.value.find(n => n.id === event.node.id)
|
||||
// if (clickedNode) {
|
||||
@@ -642,7 +621,7 @@ const handleViewportChange = (newViewport) => {
|
||||
const onEdgesChange = (changes) => {
|
||||
// Check if any edge is being removed | 检查是否有边被删除
|
||||
const hasRemoval = changes.some(change => change.type === 'remove')
|
||||
|
||||
|
||||
if (hasRemoval) {
|
||||
// Trigger history save after edge removal | 边删除后触发历史保存
|
||||
nextTick(() => {
|
||||
@@ -703,10 +682,48 @@ const handleEnterKey = (e) => {
|
||||
sendMessage()
|
||||
}
|
||||
|
||||
// Handle AI polish | 处理 AI 润色
|
||||
const handlePolish = async () => {
|
||||
const input = chatInput.value.trim()
|
||||
if (!input) return
|
||||
|
||||
// Check API configuration | 检查 API 配置
|
||||
if (!isApiConfigured.value) {
|
||||
window.$message?.warning('生成接口未就绪,请稍后重试')
|
||||
showApiSettings.value = true
|
||||
return
|
||||
}
|
||||
|
||||
isProcessing.value = true
|
||||
const originalInput = chatInput.value
|
||||
|
||||
try {
|
||||
// Call chat API to polish the prompt | 调用 AI 润色提示词
|
||||
const result = await sendChat(input, true)
|
||||
|
||||
if (result) {
|
||||
chatInput.value = result
|
||||
window.$message?.success('提示词已润色')
|
||||
}
|
||||
} catch (err) {
|
||||
chatInput.value = originalInput
|
||||
window.$message?.error(err.message || '润色失败')
|
||||
} finally {
|
||||
isProcessing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Send message | 发送消息
|
||||
const sendMessage = async () => {
|
||||
const input = chatInput.value.trim()
|
||||
if (!input || !canSubmit.value) return
|
||||
if (!input) return
|
||||
|
||||
// Check API configuration | 检查 API 配置
|
||||
if (!isApiConfigured.value) {
|
||||
window.$message?.warning('生成接口未就绪,请稍后重试')
|
||||
showApiSettings.value = true
|
||||
return
|
||||
}
|
||||
|
||||
isProcessing.value = true
|
||||
const content = chatInput.value
|
||||
@@ -721,63 +738,54 @@ const sendMessage = async () => {
|
||||
const baseX = 100
|
||||
const baseY = maxY + 200
|
||||
|
||||
const textNodeId = addNode('text', { x: baseX, y: baseY }, {
|
||||
content,
|
||||
label: '提示词'
|
||||
})
|
||||
if (autoExecute.value) {
|
||||
// Auto-execute mode: analyze intent and execute workflow | 自动执行模式:分析意图并执行工作流
|
||||
window.$message?.info('正在分析工作流...')
|
||||
|
||||
if (creationMode.value === 'text-image') {
|
||||
const imageConfigNodeId = addNode('imageConfig', { x: baseX + 400, y: baseY }, {
|
||||
label: '文生图',
|
||||
autoExecute: true
|
||||
try {
|
||||
// Analyze user intent | 分析用户意图
|
||||
const result = await analyzeIntent(content)
|
||||
|
||||
// Ensure we have valid workflow params | 确保有效的工作流参数
|
||||
const workflowParams = {
|
||||
workflow_type: result?.workflow_type || WORKFLOW_TYPES.TEXT_TO_IMAGE,
|
||||
image_prompt: result?.image_prompt || content,
|
||||
video_prompt: result?.video_prompt || content,
|
||||
character: result?.character,
|
||||
shots: result?.shots,
|
||||
multi_angle: result?.multi_angle,
|
||||
picture_book: result?.picture_book
|
||||
}
|
||||
|
||||
window.$message?.info(`执行工作流: ${result?.description || '文生图'}`)
|
||||
|
||||
// Execute the workflow | 执行工作流
|
||||
await executeWorkflow(workflowParams, { x: baseX, y: baseY })
|
||||
|
||||
window.$message?.success('工作流已启动')
|
||||
} catch (err) {
|
||||
console.error('Workflow error:', err)
|
||||
// Fallback to simple text-to-image | 回退到文生图
|
||||
window.$message?.warning('使用默认文生图工作流')
|
||||
await createTextToImageWorkflow(content, { x: baseX, y: baseY })
|
||||
}
|
||||
} else {
|
||||
// Manual mode: just create nodes | 手动模式:仅创建节点
|
||||
const textNodeId = addNode('text', { x: baseX, y: baseY }, {
|
||||
content: content,
|
||||
label: '提示词'
|
||||
})
|
||||
|
||||
const imageConfigNodeId = addNode('imageConfig', { x: baseX + 400, y: baseY }, {
|
||||
label: '文生图'
|
||||
})
|
||||
|
||||
addEdge({
|
||||
source: textNodeId,
|
||||
target: imageConfigNodeId,
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left'
|
||||
})
|
||||
} else {
|
||||
let videoX = baseX + 400
|
||||
let promptY = baseY
|
||||
const imageNodeIds = []
|
||||
|
||||
if (needsFirstFrame.value && firstFrameFile.value) {
|
||||
const dataUrl = await fileToDataUrl(firstFrameFile.value)
|
||||
const firstId = addNode('image', { x: baseX, y: baseY + 160 }, {
|
||||
url: dataUrl,
|
||||
base64: dataUrl,
|
||||
label: '参考图'
|
||||
})
|
||||
imageNodeIds.push({ id: firstId, role: 'first_frame_image' })
|
||||
promptY = baseY - 140
|
||||
updateNode(textNodeId, { zIndex: 5 })
|
||||
}
|
||||
|
||||
const videoConfigNodeId = addNode('videoConfig', { x: videoX, y: promptY }, {
|
||||
label: creationMode.value === 'text-video' ? '文生视频' : '图生视频',
|
||||
autoExecute: true
|
||||
})
|
||||
|
||||
addEdge({
|
||||
source: textNodeId,
|
||||
target: videoConfigNodeId,
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'promptOrder',
|
||||
data: { promptOrder: 1 }
|
||||
})
|
||||
|
||||
for (const item of imageNodeIds) {
|
||||
addEdge({
|
||||
source: item.id,
|
||||
target: videoConfigNodeId,
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'imageRole',
|
||||
data: { imageRole: item.role }
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
window.$message?.error(err.message || '创建失败')
|
||||
@@ -800,7 +808,7 @@ const checkMobile = () => {
|
||||
const loadProjectById = (projectId) => {
|
||||
// Update flow key to force VueFlow re-render | 更新 key 强制 VueFlow 重新渲染
|
||||
flowKey.value = Date.now()
|
||||
|
||||
|
||||
if (projectId && projectId !== 'new') {
|
||||
loadProject(projectId)
|
||||
} else {
|
||||
@@ -828,13 +836,13 @@ watch(
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
|
||||
|
||||
// Initialize projects store | 初始化项目存储
|
||||
initProjectsStore()
|
||||
|
||||
|
||||
// Load project data | 加载项目数据
|
||||
loadProjectById(route.params.id)
|
||||
|
||||
|
||||
// Check for initial prompt from home page | 检查来自首页的初始提示词
|
||||
const initialPrompt = sessionStorage.getItem('ai-canvas-initial-prompt')
|
||||
if (initialPrompt) {
|
||||
@@ -850,7 +858,6 @@ onMounted(() => {
|
||||
// Cleanup on unmount | 卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
clearFrameFiles()
|
||||
// Save project before leaving | 离开前保存项目
|
||||
saveProject()
|
||||
})
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
<div class="min-h-screen h-screen overflow-y-auto bg-[var(--bg-primary)]">
|
||||
<!-- Header | 顶部导航 -->
|
||||
<AppHeader>
|
||||
<template #left>
|
||||
<div class="flex h-8 items-center rounded-full bg-white px-3 shadow-sm">
|
||||
<img src="/skg-logo-black.svg" alt="SKG" class="h-6 w-auto dark:invert" />
|
||||
</div>
|
||||
<template #right>
|
||||
<button
|
||||
@click="showApiSettings = true"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
:class="{ 'text-[var(--accent-color)]': isApiConfigured }"
|
||||
title="API 设置"
|
||||
>
|
||||
<n-icon :size="20"><SettingsOutline /></n-icon>
|
||||
</button>
|
||||
</template>
|
||||
</AppHeader>
|
||||
|
||||
@@ -14,17 +19,17 @@
|
||||
<main class="max-w-5xl mx-auto px-4 py-8 md:py-16">
|
||||
<!-- Welcome section | 欢迎区域 -->
|
||||
<section class="text-center mb-12">
|
||||
<div class="flex items-center justify-center mb-8">
|
||||
<img src="/skg-logo-black.svg" alt="SKG" class="h-12 w-auto dark:invert" />
|
||||
<div class="flex items-center justify-center gap-4 mb-8">
|
||||
<img src="/skg-logo-black.svg" alt="SKG" class="h-12 w-auto md:h-16 dark:invert" />
|
||||
<h1 class="sr-only">SKG</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Input area | 输入区域 -->
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-[var(--bg-secondary)] rounded-2xl border border-[var(--border-color)] p-4 shadow-sm">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="写提示词,生成图片或视频,结果会放进画布"
|
||||
placeholder="输入你的创意,开始新项目"
|
||||
class="w-full bg-transparent resize-none outline-none text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] min-h-[80px]"
|
||||
@keydown.enter.ctrl="handleCreateWithInput"
|
||||
/>
|
||||
@@ -38,7 +43,7 @@
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
<button
|
||||
@click="handleCreateWithInput"
|
||||
class="w-8 h-8 rounded-xl bg-[var(--accent-color)] hover:bg-[var(--accent-hover)] flex items-center justify-center transition-colors"
|
||||
>
|
||||
@@ -47,7 +52,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Quick suggestions | 快捷建议 -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 mt-4">
|
||||
<span class="text-sm text-[var(--text-secondary)]">推荐:</span>
|
||||
<button
|
||||
v-for="tag in suggestions"
|
||||
:key="tag"
|
||||
@click="inputText = tag"
|
||||
class="px-3 py-1.5 text-sm rounded-full bg-[var(--bg-secondary)] border border-[var(--border-color)] hover:border-[var(--accent-color)] transition-colors"
|
||||
>
|
||||
{{ tag }}
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
|
||||
<n-icon :size="16"><RefreshOutline /></n-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -55,7 +75,7 @@
|
||||
<section ref="projectsSection">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)]">我的项目</h2>
|
||||
<button
|
||||
<button
|
||||
@click="createNewProject"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg bg-[var(--accent-color)] hover:bg-[var(--accent-hover)] text-white transition-colors"
|
||||
>
|
||||
@@ -63,32 +83,32 @@
|
||||
新建项目
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Empty state | 空状态 -->
|
||||
<div v-if="projects.length === 0" class="text-center py-12 bg-[var(--bg-secondary)] rounded-xl border border-dashed border-[var(--border-color)]">
|
||||
<n-icon :size="48" class="text-[var(--text-secondary)] mb-4"><FolderOutline /></n-icon>
|
||||
<p class="text-[var(--text-secondary)] mb-4">还没有项目,创建一个开始吧</p>
|
||||
<button
|
||||
<button
|
||||
@click="createNewProject"
|
||||
class="px-4 py-2 text-sm rounded-lg bg-[var(--accent-color)] hover:bg-[var(--accent-hover)] text-white transition-colors"
|
||||
>
|
||||
创建第一个项目
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Projects grid | 项目网格 -->
|
||||
<div v-else class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="project in projects"
|
||||
<div
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
class="group relative"
|
||||
>
|
||||
<!-- Project card | 项目卡片 -->
|
||||
<div
|
||||
<div
|
||||
@click="openProject(project)"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
<div
|
||||
class="aspect-video rounded-xl overflow-hidden bg-[var(--bg-tertiary)] mb-2 border border-[var(--border-color)] relative"
|
||||
@mouseenter="handleThumbnailHover(project, true)"
|
||||
@mouseleave="handleThumbnailHover(project, false)"
|
||||
@@ -96,7 +116,7 @@
|
||||
<!-- Thumbnail or placeholder | 缩略图或占位 -->
|
||||
<template v-if="project.thumbnail">
|
||||
<!-- Video thumbnail | 视频缩略图 -->
|
||||
<video
|
||||
<video
|
||||
v-if="isVideoUrl(project.thumbnail)"
|
||||
:ref="el => setVideoRef(project.id, el)"
|
||||
:src="project.thumbnail"
|
||||
@@ -106,9 +126,9 @@
|
||||
playsinline
|
||||
/>
|
||||
<!-- Image thumbnail | 图片缩略图 -->
|
||||
<img
|
||||
<img
|
||||
v-else
|
||||
:src="project.thumbnail"
|
||||
:src="project.thumbnail"
|
||||
:alt="project.name"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
@@ -116,7 +136,7 @@
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<n-icon :size="32" class="text-[var(--text-secondary)]"><DocumentOutline /></n-icon>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Hover overlay | 悬浮遮罩 -->
|
||||
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<span class="text-white text-sm">打开项目</span>
|
||||
@@ -125,11 +145,11 @@
|
||||
<p class="text-sm text-[var(--text-primary)] truncate">{{ project.name }}</p>
|
||||
<p class="text-xs text-[var(--text-secondary)]">{{ formatDate(project.updatedAt) }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Project actions | 项目操作 -->
|
||||
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||
<n-dropdown :options="getProjectActions(project)" @select="(key) => handleProjectAction(key, project)" placement="bottom-end">
|
||||
<button
|
||||
<button
|
||||
@click.stop
|
||||
class="p-1.5 bg-white/90 dark:bg-gray-800/90 rounded-lg shadow hover:bg-white dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
@@ -144,14 +164,14 @@
|
||||
|
||||
<!-- Left sidebar | 左侧边栏 -->
|
||||
<aside class="fixed left-4 top-1/2 -translate-y-1/2 hidden md:flex flex-col gap-2 p-2 bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] shadow-sm">
|
||||
<button
|
||||
<button
|
||||
@click="createNewProject"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
title="新建项目"
|
||||
>
|
||||
<n-icon :size="20"><DocumentOutline /></n-icon>
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
@click="scrollToProjects"
|
||||
class="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
title="我的项目"
|
||||
@@ -160,6 +180,9 @@
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<!-- API Settings Modal | API 设置弹窗 -->
|
||||
<ApiSettings v-model:show="showApiSettings" @saved="refreshApiConfig" />
|
||||
|
||||
<!-- Rename modal | 重命名弹窗 -->
|
||||
<n-modal v-model:show="showRenameModal" preset="dialog" title="重命名项目">
|
||||
<n-input v-model:value="renameValue" placeholder="请输入项目名称" />
|
||||
@@ -176,31 +199,47 @@
|
||||
* Home view component | 首页视图组件
|
||||
* Entry point with project list and creation input
|
||||
*/
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NIcon, NDropdown, NModal, NInput, NButton, useDialog } from 'naive-ui'
|
||||
import {
|
||||
import {
|
||||
AddOutline,
|
||||
ImageOutline,
|
||||
SendOutline,
|
||||
RefreshOutline,
|
||||
DocumentOutline,
|
||||
FolderOutline,
|
||||
EllipsisHorizontalOutline,
|
||||
CreateOutline,
|
||||
CopyOutline,
|
||||
SettingsOutline,
|
||||
TrashOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import {
|
||||
projects,
|
||||
initProjectsStore,
|
||||
createProject,
|
||||
deleteProject,
|
||||
duplicateProject,
|
||||
renameProject
|
||||
import {
|
||||
projects,
|
||||
initProjectsStore,
|
||||
createProject,
|
||||
deleteProject,
|
||||
duplicateProject,
|
||||
renameProject
|
||||
} from '../stores/projects'
|
||||
import { useModelStore } from '../stores/pinia'
|
||||
import ApiSettings from '../components/ApiSettings.vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const dialog = useDialog()
|
||||
const modelStore = useModelStore()
|
||||
|
||||
// API Settings state | API 设置状态
|
||||
const showApiSettings = ref(false)
|
||||
const isApiConfigured = computed(() => !!modelStore.currentApiKey)
|
||||
|
||||
// Refresh API config state | 刷新 API 配置状态
|
||||
const refreshApiConfig = () => {
|
||||
// 通过 computed 自动更新,不需要手动刷新
|
||||
}
|
||||
|
||||
// Video refs for hover play | 视频引用用于悬停播放
|
||||
const videoRefs = new Map()
|
||||
|
||||
@@ -216,10 +255,10 @@ const setVideoRef = (projectId, el) => {
|
||||
// Handle thumbnail hover | 处理缩略图悬停
|
||||
const handleThumbnailHover = (project, isHovering) => {
|
||||
if (!isVideoUrl(project.thumbnail)) return
|
||||
|
||||
|
||||
const video = videoRefs.get(project.id)
|
||||
if (!video) return
|
||||
|
||||
|
||||
if (isHovering) {
|
||||
video.play().catch(() => {
|
||||
// Ignore play errors (e.g., autoplay policy)
|
||||
@@ -238,13 +277,21 @@ const showRenameModal = ref(false)
|
||||
const renameValue = ref('')
|
||||
const renameTargetId = ref(null)
|
||||
|
||||
// Suggestions tags | 建议标签
|
||||
const suggestions = [
|
||||
'雨中魔法森林',
|
||||
'日式街面美食摄影',
|
||||
'瀑布水流飞溅',
|
||||
'雨天富声旁边花语'
|
||||
]
|
||||
|
||||
// Format date | 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
const d = new Date(date)
|
||||
const now = new Date()
|
||||
const diff = now - d
|
||||
|
||||
|
||||
// Less than 1 minute | 小于1分钟
|
||||
if (diff < 60000) return '刚刚'
|
||||
// Less than 1 hour | 小于1小时
|
||||
@@ -305,24 +352,46 @@ const confirmRename = () => {
|
||||
renameValue.value = ''
|
||||
}
|
||||
|
||||
// Check internal API before navigation | 跳转前检查内部接口
|
||||
const checkApiKeyAndNavigate = (callback) => {
|
||||
|
||||
if (!isApiConfigured.value) {
|
||||
dialog.warning({
|
||||
title: '生成接口未就绪',
|
||||
content: '当前登录会话还不能使用生成接口,请稍后重试或联系管理员。',
|
||||
positiveText: '知道了'
|
||||
})
|
||||
return false
|
||||
}
|
||||
callback()
|
||||
return true
|
||||
}
|
||||
|
||||
// Create new project | 创建新项目
|
||||
const createNewProject = () => {
|
||||
const id = createProject('未命名项目')
|
||||
router.push(`/p/${id}`)
|
||||
checkApiKeyAndNavigate(() => {
|
||||
const id = createProject('未命名项目')
|
||||
router.push(`/p/${id}`)
|
||||
})
|
||||
}
|
||||
|
||||
// Create project with input text | 使用输入文本创建项目
|
||||
const handleCreateWithInput = () => {
|
||||
const name = inputText.value.trim() || '未命名项目'
|
||||
const id = createProject(name)
|
||||
sessionStorage.setItem('ai-canvas-initial-prompt', inputText.value.trim())
|
||||
inputText.value = ''
|
||||
router.push(`/p/${id}`)
|
||||
checkApiKeyAndNavigate(() => {
|
||||
const name = inputText.value.trim() || '未命名项目'
|
||||
const id = createProject(name)
|
||||
// Store the input text to be used as initial prompt
|
||||
sessionStorage.setItem('ai-canvas-initial-prompt', inputText.value.trim())
|
||||
inputText.value = ''
|
||||
router.push(`/p/${id}`)
|
||||
})
|
||||
}
|
||||
|
||||
// Open existing project | 打开已有项目
|
||||
const openProject = (project) => {
|
||||
router.push(`/p/${project.id}`)
|
||||
checkApiKeyAndNavigate(() => {
|
||||
router.push(`/p/${project.id}`)
|
||||
})
|
||||
}
|
||||
|
||||
// Check if URL is a video | 检查 URL 是否为视频
|
||||
|
||||
Reference in New Issue
Block a user