1. 支持自定义MJ API,接口格式必须满足 Midjourney-proxy-plus 接口,https://apiai.apifox.cn/folder-31977042
2. 支持自定义生图包。生图包的格式必须满足 Midjourney-proxy-plus 接口,https://apiai.apifox.cn/folder-31977042
3. 修改软件机器码生成方式
4. 新增推理模式(聚合推文,配合人物提取可以做到人物统一)
5. 修改软件内置人物提取提示词
This commit is contained in:
2025-04-10 20:16:51 +08:00
parent 863ac4d7f4
commit bf4b488a02
28 changed files with 1924 additions and 386 deletions
+75
View File
@@ -0,0 +1,75 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog']
const useId: typeof import('vue')['useId']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel']
const useNotification: typeof import('naive-ui')['useNotification']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+58
View File
@@ -0,0 +1,58 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCode: typeof import('naive-ui')['NCode']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NImageGroup: typeof import('naive-ui')['NImageGroup']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NLog: typeof import('naive-ui')['NLog']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopover: typeof import('naive-ui')['NPopover']
NProgress: typeof import('naive-ui')['NProgress']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NSplit: typeof import('naive-ui')['NSplit']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
+19
View File
@@ -0,0 +1,19 @@
interface ValidationErrorItem {
message?: string;
[key: string]: any;
}
interface ValidationErrors {
[key: string]: ValidationErrorItem[] | any;
message?: string;
}
export function ValidateErrorString(errors: ValidationErrors): string {
const errorMessages = Object.values(errors)
.map((err) => {
return err[0]?.message || '验证错误'
})
.join(' ')
let res = '请修正以下错误: ' + (errorMessages || errors.message);
return res
}
@@ -137,7 +137,7 @@ import { isEmpty } from 'lodash'
import VideoCanvas from '../../VideoSubtitle/VideoCanvas.vue'
import { SubtitleSavePositionType } from '../../../../../define/enum/waterMarkAndSubtitle.ts'
import { DEFINE_STRING } from '../../../../../define/define_string/index.ts'
import Setting from './Setting.vue'
import Setting from './SettingTabs.vue'
import {
BookBackTaskType,
BookImageCategory,
@@ -1,32 +1,44 @@
<!-- filepath: src\renderer\src\components\Common\NotesCollapse.vue -->
<template>
<n-collapse class="custom-collapse">
<n-collapse class="custom-collapse" :default-expanded-names="defaultExpanded ? [name] : []">
<n-collapse-item :name="name" :title="title">
<template #header>
<div class="collapse-header">
<n-icon size="20" class="warning-icon">
<svg v-if="iconType === 'warning'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<svg
v-if="iconType === 'warning'"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
</svg>
<svg v-else-if="iconType === 'info'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
<svg
v-else-if="iconType === 'info'"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"
/>
</svg>
<slot v-else name="icon"></slot>
</n-icon>
<span style="font-size: 16px">{{ title }}</span>
</div>
</template>
<div class="notes-content">
<!-- 如果传入内容数组自动生成段落 -->
<template v-if="items && items.length">
<p v-for="(item, index) in items" :key="index" v-html="item"></p>
</template>
<!-- 默认插槽用于完全自定义内容 -->
<slot></slot>
</div>
</n-collapse-item>
</n-collapse>
</template>
@@ -53,6 +65,11 @@ defineProps({
items: {
type: Array,
default: () => []
},
// 是否默认展开
defaultExpanded: {
type: Boolean,
default: false
}
})
</script>
@@ -146,6 +163,7 @@ defineProps({
text-decoration: underline;
cursor: pointer;
transition: color 0.2s ease;
font-weight: bold;
}
.clickable-link:hover {
@@ -18,20 +18,45 @@
</template>
<span>VXxiangbie88</span>
</n-popover>
<n-button
text
:style="{
width: '24px',
fontSize: '24px',
color: '#2080f0',
display: 'flex',
marginLeft: '10px'
}"
@click="copyMachineId"
title="复制机器码"
>
<n-icon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em">
<path
fill="currentColor"
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
/>
</svg>
</n-icon>
</n-button>
</div>
<div style="margin: 10px">
<div :style="{ margin: '10px' }">
<strong>怎么激活 --> </strong>
<strong
style="color: #e18a3b; margin-left: 10px"
:style="{ color: '#e18a3b', marginLeft: '10px' }"
@click="OpenTeach('activate')"
class="url_class"
>软件激活教程 -->
</strong>
<strong style="margin-left: 10px; color: #ea5514" @click="OpenTeach('lms')" class="url_class">
<strong
:style="{ marginLeft: '10px', color: '#ea5514' }"
@click="OpenTeach('lms')"
class="url_class"
>
后台入口
</strong>
</div>
<div style="margin: 10px; color: red">
<div :style="{ margin: '10px', color: 'red' }">
有使用问题或者购买问题请联系客服微信
<n-popover trigger="hover" raw :show-arrow="false">
<template #trigger>
@@ -44,13 +69,13 @@
/>
</n-popover>
</div>
<div style="margin: 10px; font-size: large; color: red">
<div :style="{ margin: '10px', fontSize: 'large', color: 'red' }">
教程视频<span class="url_class" @click="OpenTeach('video')">向北-LAITool</span>
</div>
<div style="margin: 10px">
<div :style="{ margin: '10px' }">
详细的教程文档<span class="url_class" @click="OpenTeach('doc')">教程文档</span>
</div>
<div style="margin: 10px">
<div :style="{ margin: '10px' }">
问题/需求收集表<span class="url_class" @click="OpenQueDoc">问题/需求收集表</span>
</div>
</div>
@@ -96,6 +121,39 @@ function OpenQueDoc() {
'https://pvwu1oahp5m.feishu.cn/sheets/G5s3sX0KahH1XQtWDazcF70anUb?from=from_copylink'
)
}
function copyMachineId() {
if (systemStore.machineId) {
try {
// Try to use browser's Clipboard API (renderer process method)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(systemStore.machineId)
.then(() => {
message.success('复制成功')
})
.catch(() => {
message.success('复制失败。请手动复制')
})
} else {
// Fallback for browsers without Clipboard API
const textarea = document.createElement('textarea')
textarea.value = systemStore.machineId
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
message.success('复制成功')
}
} catch (error) {
message.success('复制失败。请手动复制')
}
} else {
message.error('获取机器码失败')
}
}
</script>
<style>
+1 -5
View File
@@ -29,7 +29,7 @@
</n-layout-content>
</n-layout>
</n-space>
<template #description> 你不知道你有多幸运 </template>
<template #description> 正在加载软件授权信息 </template>
</n-spin>
</template>
@@ -624,10 +624,6 @@ const menuOptions = computed(() => {
}
})
function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
}
function OpneBackTask() {
let dialogWidth = window.innerWidth * 0.9
if (dialogWidth < 800) dialogWidth = 800
@@ -20,7 +20,7 @@
placeholder="请选择"
:options="gpt_auto_inference_options"
v-model:value="gpt_auto_inference"
style="width: 180px"
style="width: 230px"
>
</n-select>
</div>
@@ -0,0 +1,273 @@
<template>
<div class="custom-mj-api-setting">
<n-space vertical size="large">
<n-select
v-model:value="selectedConfig"
placeholder="选择要编辑的配置"
:options="configOptions"
@update:value="handleConfigChange"
/>
<n-form
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="120px"
require-mark-placement="right-hanging"
>
<n-form-item label="设置名称" path="name">
<n-input v-model:value="formData.name" placeholder="请输入设置名称" />
</n-form-item>
<n-form-item label="基础URL" path="baseUrl">
<n-input v-model:value="formData.baseUrl" placeholder="请输入API的基础URL" />
</n-form-item>
<NotesCollapse title="注意事项(必看!!!)" :default-expanded="true">
<div class="notes-content">
<p>
<strong>1. 接口兼容性要求</strong>自定义API必须兼容
<span
@click="openExternalLink('https://apiai.apifox.cn/folder-31977042')"
class="clickable-link"
>midjourney-proxy-plus</span
>
的接口规范否则将无法正常工作
</p>
<p>
<strong>2. 基础URL说明</strong>只需填写服务器的基础URL (例如: https://api.example.com),不需要包含路径。
</p>
<p>
<strong>3. 技术支持声明</strong>请注意自定义的API配置属于个性化设置我们不提供关于自定义的API的技术支持
</p>
<p>
<strong>4. 详细教程</strong>请参考我们的
<span
@click="
openExternalLink('https://rvgyir5wk1c.feishu.cn/wiki/OEj7wIdD6ivvCAkez4OcUPLcnIf')
"
class="clickable-link"
>详细文档教程</span
>
获取完整的配置指南
</p>
</div>
</NotesCollapse>
<n-form-item>
<n-button
type="error"
@click="handleDelete"
style="min-width: 120px; margin-top: 20px; margin-right: 10px"
>
删除选中
</n-button>
<n-button type="primary" @click="handleSave" style="min-width: 120px; margin-top: 20px">
保存配置
</n-button>
</n-form-item>
</n-form>
</n-space>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useMessage } from 'naive-ui'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { isEmpty } from 'lodash'
import { ValidateJson, ValidateJsonAndParse } from '@/define/Tools/validate'
import { ValidateErrorString } from '@/renderer/src/common/validateError'
const message = useMessage()
const formRef = ref(null)
// 配置列表
const configList = ref([])
const selectedConfig = ref(null)
// 简化后的表单数据结构
const formData = reactive({
id: null,
name: '',
baseUrl: ''
})
// 简化后的表单验证规则
const rules = {
name: {
required: true,
message: '请输入设置名称',
trigger: 'blur'
},
baseUrl: {
required: true,
message: '请输入基础URL',
trigger: 'blur'
}
}
// 将配置列表转换为下拉选项格式
const configOptions = computed(() => {
const options = configList.value.map((item) => ({
label: item.name,
value: item.id
}))
// 添加一个新建选项
options.unshift({
label: '-- 新建配置 --',
value: 'new'
})
return options
})
// 获取配置列表
const fetchConfigList = async () => {
try {
// 这里应该是从API或存储中获取配置列表
let res = await window.options.GetOptionByKey(OptionKeyName.MJ_CustomAPISetting)
if (res.code != 1) {
throw new Error(res.message)
}
if (
res.data == null ||
res.data.value == null ||
isEmpty(res.data.value) ||
!ValidateJson(res.data.value)
) {
configList.value = []
return
}
configList.value = ValidateJsonAndParse(res.data.value)
} catch (error) {
message.error('获取配置列表失败')
console.error(error)
}
}
// 处理配置选择变化
const handleConfigChange = (value) => {
if (value === 'new') {
// 清空表单,准备创建新配置 - 使用新的数据结构
Object.assign(formData, {
id: null,
name: '',
baseUrl: ''
})
} else {
// 加载选中的配置
const config = configList.value.find((item) => item.id === value)
if (config) {
Object.assign(formData, { ...config })
}
}
}
// 删除自定义配置
async function handleDelete() {
if (formData == null || formData.id == null) {
message.error('请先选择要删除的配置')
return
}
// 开始删除
let findIndex = configList.value.findIndex((item) => item.id === formData.id)
if (findIndex === -1) {
message.error('删除的配置不存在,请重新选择')
return
}
configList.value.splice(findIndex, 1)
let res = await window.options.ModifyOptionByKey(
OptionKeyName.MJ_CustomAPISetting,
JSON.stringify(configList.value),
OptionType.JOSN
)
if (res.code != 1) {
message.error('删除配置失败,' + res.message)
return
}
message.success('删除配置成功')
// 重置 - 使用新的数据结构
Object.assign(formData, {
id: null,
name: '',
baseUrl: ''
})
selectedConfig.value = null
}
// 保存配置
const handleSave = (e) => {
e.preventDefault()
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
// 这里应该是保存配置到API或存储
if (!formData.id) {
// 新增
formData.id = crypto.randomUUID()
configList.value.push({ ...formData })
} else {
// 修改原有的
const index = configList.value.findIndex((item) => item.id === formData.id)
if (index !== -1) {
configList.value[index] = { ...formData }
} else {
throw new Error('配置不存在,请新增配置')
}
}
// 开始保存
let res = await window.options.ModifyOptionByKey(
OptionKeyName.MJ_CustomAPISetting,
JSON.stringify(configList.value),
OptionType.JOSN
)
if (res.code != 1) {
throw new Error(res.message)
}
// 刷新配置列表
await fetchConfigList()
// 选中新创建/更新的配置
selectedConfig.value = formData.id
message.success('保存配置成功')
} catch (error) {
message.error('保存配置失败,' + error.message)
}
} else {
let res = ValidateErrorString(errors)
message.error(res)
}
})
}
/**
* 打开外部链接
* @param {string} url - 要打开的URL
*/
function openExternalLink(url) {
window.api.OpenUrl(url)
}
onMounted(() => {
fetchConfigList()
})
</script>
<style scoped>
.custom-mj-api-setting {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
:deep(.n-input) {
width: 100%;
}
</style>
@@ -0,0 +1,286 @@
<template>
<div class="custom-mj-api-setting">
<n-space vertical size="large">
<n-select
v-model:value="selectedConfig"
placeholder="选择要编辑的配置"
:options="configOptions"
@update:value="handleConfigChange"
/>
<n-form
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="120px"
require-mark-placement="right-hanging"
>
<n-form-item label="设置名称" path="name">
<n-input v-model:value="formData.name" placeholder="请输入设置名称" />
</n-form-item>
<n-form-item label="基础URL" path="baseUrl">
<n-input v-model:value="formData.baseUrl" placeholder="请输入API的基础URL" />
</n-form-item>
<n-form-item label="代理地址" path="proxyUrl">
<n-input v-model:value="formData.proxyUrl" placeholder="可选,输入图片代理服务器地址" />
</n-form-item>
<NotesCollapse title="注意事项(必看!!!)" :default-expanded="true">
<div class="notes-content">
<p>
<strong>1. 接口兼容性要求</strong>自定义API必须兼容
<span
@click="openExternalLink('https://apiai.apifox.cn/folder-31977042')"
class="clickable-link"
>midjourney-proxy-plus</span
>
的接口规范否则将无法正常工作
</p>
<p>
<strong>2. 基础URL说明</strong>只需填写服务器的基础URL (例如:
https://api.example.com),不需要包含路径。
</p>
<p>
<strong>3. 代理地址说明</strong
>如果您的API需要通过代理访问可以填写代理服务器地址比如https://cdn.laitool.cn,不需要则留空。
</p>
<p>
<strong>4. 技术支持声明</strong
>请注意自定义的生图包配置属于个性化设置我们不提供关于任何生图包的技术支持
</p>
<p>
<strong>5. 详细教程</strong>请参考我们的
<span
@click="
openExternalLink('https://rvgyir5wk1c.feishu.cn/wiki/OEj7wIdD6ivvCAkez4OcUPLcnIf')
"
class="clickable-link"
>详细文档教程</span
>
获取完整的配置指南
</p>
</div>
</NotesCollapse>
<n-form-item>
<n-button
type="error"
@click="handleDelete"
style="min-width: 120px; margin-top: 20px; margin-right: 10px"
>
删除选中
</n-button>
<n-button type="primary" @click="handleSave" style="min-width: 120px; margin-top: 20px">
保存配置
</n-button>
</n-form-item>
</n-form>
</n-space>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useMessage } from 'naive-ui'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { isEmpty } from 'lodash'
import { ValidateJson, ValidateJsonAndParse } from '@/define/Tools/validate'
import { ValidateErrorString } from '@/renderer/src/common/validateError'
const message = useMessage()
const formRef = ref(null)
// 配置列表
const configList = ref([])
const selectedConfig = ref(null)
// 简化后的表单数据结构,添加了代理地址字段
const formData = reactive({
id: null,
name: '',
baseUrl: '',
proxyUrl: '' // 新增代理地址字段,可以为空
})
// 简化后的表单验证规则
const rules = {
name: {
required: true,
message: '请输入设置名称',
trigger: 'blur'
},
baseUrl: {
required: true,
message: '请输入基础URL',
trigger: 'blur'
}
}
// 将配置列表转换为下拉选项格式
const configOptions = computed(() => {
const options = configList.value.map((item) => ({
label: item.name,
value: item.id
}))
// 添加一个新建选项
options.unshift({
label: '-- 新建配置 --',
value: 'new'
})
return options
})
// 获取配置列表
const fetchConfigList = async () => {
try {
// 这里应该是从API或存储中获取配置列表
let res = await window.options.GetOptionByKey(OptionKeyName.MJ_CustomPackageSetting)
if (res.code != 1) {
throw new Error(res.message)
}
if (
res.data == null ||
res.data.value == null ||
isEmpty(res.data.value) ||
!ValidateJson(res.data.value)
) {
configList.value = []
return
}
configList.value = ValidateJsonAndParse(res.data.value)
} catch (error) {
message.error('获取配置列表失败')
console.error(error)
}
}
// 处理配置选择变化
const handleConfigChange = (value) => {
if (value === 'new') {
// 清空表单,准备创建新配置 - 使用新的数据结构
Object.assign(formData, {
id: null,
name: '',
baseUrl: '',
proxyUrl: '' // 重置代理地址
})
} else {
// 加载选中的配置
const config = configList.value.find((item) => item.id === value)
if (config) {
Object.assign(formData, { ...config })
}
}
}
// 删除自定义配置
async function handleDelete() {
if (formData == null || formData.id == null) {
message.error('请先选择要删除的配置')
return
}
// 开始删除
let findIndex = configList.value.findIndex((item) => item.id === formData.id)
if (findIndex === -1) {
message.error('删除的配置不存在,请重新选择')
return
}
configList.value.splice(findIndex, 1)
let res = await window.options.ModifyOptionByKey(
OptionKeyName.MJ_CustomPackageSetting,
JSON.stringify(configList.value),
OptionType.JOSN
)
if (res.code != 1) {
message.error('删除配置失败,' + res.message)
return
}
message.success('删除配置成功')
// 重置 - 使用新的数据结构
Object.assign(formData, {
id: null,
name: '',
baseUrl: '',
proxyUrl: '' // 重置代理地址
})
selectedConfig.value = null
}
// 保存配置
const handleSave = (e) => {
e.preventDefault()
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
// 这里应该是保存配置到API或存储
if (!formData.id) {
// 新增
formData.id = crypto.randomUUID()
configList.value.push({ ...formData })
} else {
// 修改原有的
const index = configList.value.findIndex((item) => item.id === formData.id)
if (index !== -1) {
configList.value[index] = { ...formData }
} else {
throw new Error('配置不存在,请新增配置')
}
}
// 开始保存
let res = await window.options.ModifyOptionByKey(
OptionKeyName.MJ_CustomPackageSetting,
JSON.stringify(configList.value),
OptionType.JOSN
)
if (res.code != 1) {
throw new Error(res.message)
}
// 刷新配置列表
await fetchConfigList()
// 选中新创建/更新的配置
selectedConfig.value = formData.id
message.success('保存配置成功')
} catch (error) {
message.error('保存配置失败,' + error.message)
}
} else {
let res = ValidateErrorString(errors)
message.error(res)
}
})
}
/**
* 打开外部链接
* @param {string} url - 要打开的URL
*/
function openExternalLink(url) {
window.api.OpenUrl(url)
}
onMounted(() => {
fetchConfigList()
})
</script>
<style scoped>
.custom-mj-api-setting {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
:deep(.n-input) {
width: 100%;
}
</style>
@@ -50,6 +50,9 @@
style="width: 100px"
/>
</n-form-item>
<n-form-item style="margin-left: 10px" path="mj_api_url">
<n-button @click="CustomMJAPI">自定义MJ API</n-button>
</n-form-item>
</div>
</n-form>
<NotesCollapse title="注意事项">
@@ -79,22 +82,16 @@
<script setup>
import { onMounted, ref } from 'vue'
import {
NCard,
NForm,
NFormItem,
NInput,
NSelect,
NButton,
useMessage,
NIcon,
NCollapse,
NCollapseItem
} from 'naive-ui'
import { NCard, NForm, NFormItem, NInput, NSelect, NButton, useMessage, useDialog } from 'naive-ui'
import { useOptionStore } from '@/stores/option'
import MJDefine from '@/main/Service/MJ/mjDefine'
import { isEmpty } from 'lodash'
import NotesCollapse from '../../Common/NotesCollapse.vue'
import CustomMJAPISetting from './CustomMJAPISetting.vue'
import { GetMJUrlOptions } from '@/define/api/apiUrlDefine'
import { OptionKeyName } from '@/define/enum/option'
import { ValidateJson, ValidateJsonAndParse } from '@/define/Tools/validate'
let dialog = useDialog()
let optionStore = useOptionStore()
let message = useMessage()
@@ -103,10 +100,55 @@ let mjAPIURLOptions = ref([])
let mjImageSpeedOptions = ref([])
onMounted(() => {
mjAPIURLOptions.value = MJDefine.GetMJAPIUrlOptions()
GetAllAPIUrlOptions()
mjImageSpeedOptions.value = MJDefine.GetMJSpeedOptions()
})
async function GetAllAPIUrlOptions() {
let defaultApiUrl = GetMJUrlOptions('api')
let customApiUrl = []
// 加载自定义的
let customApiSettingString = await window.options.GetOptionByKey(
OptionKeyName.MJ_CustomAPISetting
)
if (customApiSettingString.code != 1) {
throw new Error('获取自定义的API设置失败,请检查配置')
}
if (
customApiSettingString.data == null ||
isEmpty(customApiSettingString.data.value) ||
!ValidateJson(customApiSettingString.data.value)
) {
customApiUrl = []
} else {
let customApiUrlList = ValidateJsonAndParse(customApiSettingString.data.value)
if (customApiUrlList.length == 0) {
customApiUrl = []
} else {
customApiUrlList.forEach((item) => {
let baseUrl = item.baseUrl.replace(/\/$/, '')
customApiUrl.push({
label: item.name,
value: item.id,
isPackage: true,
mj_url: {
imagine: baseUrl + '/mj/submit/imagine',
describe: baseUrl + '/mj/submit/describe',
update_file: baseUrl + '/mj/submit/upload-discord-images',
once_get_task: baseUrl + '/mj/task/${id}/fetch',
query_url: null
},
buy_url: null
})
})
}
}
// 合并默认的和自定义的API地址
mjAPIURLOptions.value = defaultApiUrl.concat(customApiUrl)
console.log('mjAPIURLOptions', mjAPIURLOptions.value)
}
/**
* 打开购买GPT的地址
*/
@@ -126,6 +168,22 @@ async function openGptBuyUrl() {
}
}
async function CustomMJAPI() {
dialog.create({
title: '自定义MJ API',
maskClosable: false,
content: () => h(CustomMJAPISetting, {}, {}),
closable: true,
showIcon: false,
style: { width: '680px' },
footer: null,
onClose: async () => {
// 关闭时刷新数据
await GetAllAPIUrlOptions()
}
})
}
/**
* 打开外部链接
* @param {string} url - 要打开的URL
@@ -2,10 +2,11 @@
<n-card title="生图包设置" hoverable style="margin-top: 10px; min-width: 850px" size="medium">
<n-form inline>
<n-space align="center" :size="12">
<n-form-item label="套餐选择">
<n-form-item label="生图包选择">
<n-select
v-model:value="optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectPackage"
:options="GetMJUrlOptions('package')"
:options="imagePackageOptions"
@update-value="handlePackageChange"
placeholder="选择套餐"
style="min-width: 200px"
/>
@@ -28,15 +29,19 @@
<n-form-item label="出图代理">
<n-select
v-model:value="optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectedProxy"
:options="ImagePackageProxyOptions"
:options="imageProxyUrlOptions"
placeholder="选择出图代理"
style="min-width: 200px"
/>
</n-form-item>
<!-- <n-form-item>
<n-form-item>
<n-button @click="handleQuery"> 查询 </n-button>
</n-form-item> -->
</n-form-item>
<n-form-item>
<n-button @click="CustomImagePackage" > 自定义生图包 </n-button>
</n-form-item>
</n-space>
</n-form>
<NotesCollapse title="注意事项">
@@ -44,10 +49,7 @@
1. 使用生图包前请确保您已完成套餐购买<strong>未购买套餐将无法使用相关功能</strong>
</p>
<p>2. 标准生图包最大并发数为3具体并发数量以您所购买的套餐规格为准</p>
<p>3. 出图代理支持香港和美国节点选择也兼容系统默认代理设置与代理模式保持一致</p>
<p>
4. 您可随时点击 <strong>"查询"</strong> 按钮实时查看当前套餐已使用的出图数量及剩余额度
</p>
<p>3. 可以选择出图包对应的图片代理也兼容系统默认代理设置与代理模式保持一致</p>
</NotesCollapse>
</n-card>
</template>
@@ -56,12 +58,19 @@
import { useMessage, NCard, NForm, NFormItem, NButton, NSelect, NInput, NSpace } from 'naive-ui'
import { GetMJUrlOptions } from '@/define/api/apiUrlDefine'
import { useOptionStore } from '@/stores/option'
import { ImagePackageProxyOptions } from '@/define/data/settingData'
import { isEmpty } from 'lodash'
import NotesCollapse from '../../Common/NotesCollapse.vue'
import CustomMJPackageSetting from './CustomMJPackageSetting.vue'
import { OptionKeyName } from '@/define/enum/option'
import { ValidateJson, ValidateJsonAndParse } from '@/define/Tools/validate'
import { GetImageProxyUrlOptions } from '@/define/data/settingData'
const optionStore = useOptionStore()
const message = useMessage()
const dialog = useDialog()
let imagePackageOptions = ref([])
let imageProxyUrlOptions = ref([])
/**
* 打开购买网址
@@ -99,6 +108,133 @@ const handleQuery = () => {
window.api.OpenUrl(queryUrl)
}
}
// 修改处理选择变化的方法,添加明确的参数
function handlePackageChange(value) {
// 先更新store中的值
optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectPackage = value
// 然后使用最新选择的值来获取代理选项
fetchImageProxyOptions(value)
}
// 修改方法定义,使其接收当前选择的值作为参数
function fetchImageProxyOptions(selectedPackage) {
// 移除debugger,避免中断执行
// 使用传入的参数作为当前选择的生图包
let packageValue =
selectedPackage || optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectPackage
let defaultImageProxyUrl = GetImageProxyUrlOptions(packageValue)
// 获取当前选中的生图包
let findIndex = imagePackageOptions.value.findIndex((item) => item.value == packageValue)
if (findIndex == -1) {
// 即使找不到对应包,也设置可用的代理选项
imageProxyUrlOptions.value = defaultImageProxyUrl
// 如果有可用的代理选项,默认选择第一个
// 修改条件判断,处理null、undefined、空字符串等所有假值情况
if (
defaultImageProxyUrl.length > 0 &&
!optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectedProxy
) {
optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectedProxy =
defaultImageProxyUrl[0].value
}
return
}
let selectPackage = imagePackageOptions.value[findIndex]
// 确保 proxyUrl 存在才添加
if (selectPackage.proxyUrl) {
defaultImageProxyUrl.unshift({
label: selectPackage.label + ' 图片代理',
value: selectPackage.proxyUrl
})
}
imageProxyUrlOptions.value = defaultImageProxyUrl
debugger
// 判断当前选中的代理是不是在里面,不是的话重置选中代理
let findProxyIndex = defaultImageProxyUrl.findIndex(
(item) => item.value == optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectedProxy
)
if (findProxyIndex == -1) {
// 确保有默认值可以设置
if (selectPackage.proxyUrl) {
optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectedProxy = selectPackage.proxyUrl
} else if (defaultImageProxyUrl.length > 0) {
// 如果没有包专用代理,则使用第一个可用代理
setTimeout(() => {
optionStore.MJ_GlobalSetting.mj_imagePackageSetting.selectedProxy =
defaultImageProxyUrl[0].value
}, 0)
}
}
}
// 获取生图包 默认的和自定义的
async function fetchImagePackageOptions() {
let defaultApiUrl = GetMJUrlOptions('package')
let customApiUrl = []
// 加载自定义的
let res = await window.options.GetOptionByKey(OptionKeyName.MJ_CustomPackageSetting)
if (res.code != 1) {
message.error('获取自定义的生图包设置失败,请检查配置')
return
}
if (res.data == null || isEmpty(res.data.value) || !ValidateJson(res.data.value)) {
imagePackageOptions.value = defaultApiUrl
return
}
let customApiUrlList = ValidateJsonAndParse(res.data.value)
if (customApiUrlList.length <= 0) {
imagePackageOptions.value = defaultApiUrl
return
}
customApiUrlList.forEach((element) => {
let baseUrl = element.baseUrl.replace(/\/$/, '')
customApiUrl.push({
label: element.name,
value: element.id,
isPackage: true,
mj_url: {
imagine: baseUrl + '/mj/submit/imagine',
describe: baseUrl + '/mj/submit/describe',
update_file: baseUrl + '/mj/submit/upload-discord-images',
once_get_task: baseUrl + '/mj/task/${id}/fetch',
query_url: null
},
buy_url: null,
proxyUrl: element.proxyUrl // 添加代理地址
})
})
imagePackageOptions.value = defaultApiUrl.concat(customApiUrl)
fetchImageProxyOptions()
}
function CustomImagePackage() {
dialog.create({
title: '自定义生图包',
content: () => h(CustomMJPackageSetting),
style: {
width: '680px'
},
closable: true,
maskClosable: false,
showIcon: false,
onClose: async () => {
await fetchImagePackageOptions()
}
})
}
onMounted(() => {
// 确保先完全加载包选项,再设置代理选项
fetchImagePackageOptions()
})
</script>
<style scoped>