2.5版本 增加dall-e 优化mj 对接mj-plus

This commit is contained in:
小易 2024-02-04 18:51:37 +08:00
parent 822ff0a51d
commit 2ca3c164a0
57 changed files with 12239 additions and 7693 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"]
}

78
.vscode/settings.json vendored
View File

@ -1,20 +1,66 @@
{
"eslint.validate": ["html", "vue", "javascript", "jsx"],
"emmet.syntaxProfiles": {
"vue-html": "html",
"vue": "html"
},
"editor.tabSize": 2,
"eslint.alwaysShowStatus": true,
"eslint.quiet": true,
"prettier.enable": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll": true,
"source.fixAll.stylelint": true
"source.fixAll.eslint": "explicit"
},
"stylelint.customSyntax": "postcss-less",
"stylelint.validate": [
"css",
"less"
]
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"json",
"jsonc",
"json5",
"yaml",
"yml",
"markdown"
],
"cSpell.words": [
"antfu",
"axios",
"bumpp",
"chatgpt",
"chenzhaoyu",
"commitlint",
"davinci",
"dockerhub",
"esno",
"GPTAPI",
"highlightjs",
"hljs",
"iconify",
"katex",
"katexmath",
"linkify",
"logprobs",
"mdhljs",
"mila",
"MODELSMAPLIST",
"modelvalue",
"newconfig",
"nodata",
"OPENAI",
"pinia",
"Popconfirm",
"rushstack",
"Sider",
"tailwindcss",
"traptitech",
"tsup",
"Typecheck",
"unplugin",
"VITE",
"vueuse",
"Zhao"
],
"vue.codeActions.enabled": false,
"volar.experimental.tsconfigPaths": {
"./chat": ["./chat/tsconfig.json"],
"./admin": ["./admin/tsconfig.json"],
"./service": ["./service/tsconfig.json"]
// tsconfig.json
}
}

125
README.md
View File

@ -1,3 +1,128 @@
# Yi - Ai 更新日志
## V2.5.020240203
### 功能更新
1. **mj-proxy-plus 支持更新**
- 新增容错和重试机制,提高稳定性。
2. **模型新增排序功能**
- 优化模型排序逻辑,提升用户体验。
3. **精简 mj 模型配置**
- 后台配置现仅需地址和 key简化操作流程。
4. **dall-e 绘画整合进 chat**
- dall-e-3 模型可在后台单独配置。
- 保留 dall-e-3 页面,绘图功能将纳入 chat 组件。
- 连续绘图功能开发中。
5. **文件类型支持扩展**
- all 模型除 pdf 外,增加多种文件类型支持。
### Bug 修复
1. **国产模型兼容性修复**
- 修复了国产模型添加后无法使用的 bug。
2. **界面显示优化**
- 修复后台及绘画广场的显示问题。
### 注意事项
- 由于本次 mj-proxy-plus 升级不向下兼容,建议删除数据库中旧的 mj 数据库。
- 新的 key 可以通过中转平台购买。
- 如果您之前订阅过,但不想自建 mj-proxy-plus可以考虑共享账号给我们以合组账号池。
## V2.4.5
1. 部分页面UI精简。
2. 管理端地址改为 `/admin`,默认密码均设为 `123456`
3. 支持使用 GPT-4-All第三方逆向解析上传的文件、图片。
4. 增加模型关联 Token 计费(可选)。
5. MJ 版本默认调整为 v6.0。
# 页面效果展示
<img src="https://nineai-1313051656.cos.ap-guangzhou.myqcloud.com/haibao/chat.png" width="100%">
<img src="https://nineai-1313051656.cos.ap-guangzhou.myqcloud.com/haibao/dark.png" width="100%">
# 项目部署教程
## 环境准备
1. **安装Node.js环境**
- 请根据您的操作系统下载并安装Node.js。
- 可以从[Node.js官网](https://nodejs.org/)下载。
2. **安装PM2**
- 使用npm安装PM2`npm install pm2 -g`
- PM2是一个带有负载均衡功能的Node应用的进程管理器。
3. **安装PNPM**
- 使用npm安装PNPM`npm install -g pnpm`
- PNPM是一个快速、节省磁盘空间的包管理工具。
## 配置项目
1. **配置环境变量**
- 复制`.env.example`文件为`.env`。
- 根据需要修改`.env`文件中的配置项。
2. **安装项目依赖**
- 运行命令:`pnpm install`(若安装失败可尝试使用国内源)
- 这将根据`package.json`文件安装所有必需的依赖。
## 启动项目
1. **启动服务**
- 使用命令:`pnpm start`
- 这将启动项目并默认在9520端口监听。
2. **访问项目**
- 在浏览器中访问`http://localhost:9520`或者如果配置了nginx反向代理则通过配置的域名访问。
## 管理平台
- **管理端地址**`/`
- **普通管理员账号**`admin`
- **超级管理员账号**`super`
- **密码**`123456`
普通管理员,可以预览后台非敏感信息。登入后台后请及时修改管理员密码,或按需要禁用普通管理员。
请确保遵循上述步骤进行配置和启动,以保证系统的正确运行。
## 项目升级
1. **拉取更新**
- 拉取新的整合包:`git pull`
2. **删除旧进程**
- 删除旧的 PM2 进程。
3. **安装依赖**
- 运行命令:`pnpm install` 以安装 `package.json` 中定义的必需依赖。
4. **启动服务**
- 使用命令:`pnpm start` 来启动项目,它将默认在 9520 端口监听。
## 作者wx
<img src="https://photo-1313051656.cos.ap-guangzhou.myqcloud.com/WechatIMG65.jpeg" width="300">
# NineAI 更新整合版
## 更新日志

View File

@ -1,5 +1,5 @@
{
"version": "2.4.0",
"version": "2.5.0",
"scripts": {
"dev": "vite",
"build:test": "vue-tsc --noEmit && vite build --mode test",

View File

@ -174,6 +174,7 @@ export const MODEL_LIST = [
"gpt-4-vision-preview",
"gpt-4-all",
"gpt-4-0125-preview",
'dall-e-3',
// claude
"claude-2.0",
"claude-2.1",
@ -293,6 +294,7 @@ export const MODELSMAPLIST = {
"gpt-4-vision-preview",
"gpt-4-all",
"gpt-4-0125-preview",
'dall-e-3',
// claude
"claude-2.0",
"claude-2.1",

View File

@ -23,21 +23,21 @@ const routes: RouteRecordRaw = {
icon: 'menu-history',
},
},
{
path: 'config',
name: 'mjManage',
component: () => import('@/views/mjDraw/index.vue'),
meta: {
title: '参数配置',
icon: 'menu-params',
},
},
// {
// path: 'config',
// name: 'mjManage',
// component: () => import('@/views/mjDraw/index.vue'),
// meta: {
// title: '参数配置',
// icon: 'menu-params',
// },
// },
{
path: 'proxy',
name: 'mjProxyManage',
component: () => import('@/views/mjDraw/proxy.vue'),
meta: {
title: '更多设置',
title: '参数配置',
icon: 'menu-proxy',
},
},

View File

@ -1,20 +1,23 @@
<route lang="yaml">
meta:
title: MJ绘画管理
</route>
</route>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import ApiChat from '@/api/modules/chat'
import ApiUsre from '@/api/modules/user'
import { DRAW_MJ_STATUS_LIST, RECOMMEND_STATUS_OPTIONS } from '@/constants/index'
import { onMounted, reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessage } from 'element-plus';
import ApiChat from '@/api/modules/chat';
import ApiUsre from '@/api/modules/user';
import {
DRAW_MJ_STATUS_LIST,
RECOMMEND_STATUS_OPTIONS,
} from '@/constants/index';
const loading = ref(false)
const formRef = ref<FormInstance>()
const total = ref(0)
const userList = ref([])
const loading = ref(false);
const formRef = ref<FormInstance>();
const total = ref(0);
const userList = ref([]);
const formInline = reactive({
userId: '',
@ -22,60 +25,60 @@ const formInline = reactive({
status: 3,
page: 1,
size: 10,
})
});
interface Logitem {
id: number
userId: number
answer: string
thumbImg: string
rec: number
model: number
createdAt: string
updatedAt: string
id: number;
userId: number;
answer: string;
thumbImg: string;
rec: number;
model: number;
createdAt: string;
updatedAt: string;
drawUrl: string;
fileInfo: {
width: number
height: number
thumbImg: string
cosUrl: string
}
width: number;
height: number;
thumbImg: string;
cosUrl: string;
};
}
const tableData = ref<Logitem[]>([])
const tableData = ref<Logitem[]>([]);
async function queryAllDrawLog() {
loading.value = true
loading.value = true;
try {
const res = await ApiChat.queryMjDrawAll(formInline)
const { rows, count } = res.data
loading.value = false
const res = await ApiChat.queryMjDrawAll(formInline);
const { rows, count } = res.data;
loading.value = false;
total.value = count
tableData.value = rows
}
catch (error) {
loading.value = false
total.value = count;
tableData.value = rows;
} catch (error) {
loading.value = false;
}
}
async function recommendDrawImg(id: number) {
const res = await ApiChat.recMjDrawImg({ id })
ElMessage.success(res.data)
queryAllDrawLog()
const res = await ApiChat.recMjDrawImg({ id });
ElMessage.success(res.data);
queryAllDrawLog();
}
async function handlerSearchUser(val: string) {
const res = await ApiUsre.queryAllUser({ size: 30, username: val })
userList.value = res.data.rows
const res = await ApiUsre.queryAllUser({ size: 30, username: val });
userList.value = res.data.rows;
}
function handlerReset(formEl: FormInstance | undefined) {
formEl?.resetFields()
queryAllDrawLog()
formEl?.resetFields();
queryAllDrawLog();
}
onMounted(() => {
queryAllDrawLog()
})
queryAllDrawLog();
});
</script>
<template>
@ -102,37 +105,68 @@ onMounted(() => {
</el-select>
</el-form-item>
<el-form-item label="推荐状态" prop="rec">
<el-select v-model="formInline.rec" placeholder="请选择推荐状态" clearable>
<el-option v-for="item in RECOMMEND_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
<el-select
v-model="formInline.rec"
placeholder="请选择推荐状态"
clearable
>
<el-option
v-for="item in RECOMMEND_STATUS_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="绘制状态" prop="status">
<el-select v-model="formInline.status" placeholder="请选择图片绘制状态" clearable>
<el-option v-for="item in DRAW_MJ_STATUS_LIST" :key="item.value" :label="item.label" :value="item.value" />
<el-select
v-model="formInline.status"
placeholder="请选择图片绘制状态"
clearable
>
<el-option
v-for="item in DRAW_MJ_STATUS_LIST"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="queryAllDrawLog">
查询
</el-button>
<el-button @click="handlerReset(formRef)">
重置
</el-button>
<el-button type="primary" @click="queryAllDrawLog"> 查询 </el-button>
<el-button @click="handlerReset(formRef)"> 重置 </el-button>
</el-form-item>
</el-form>
</page-main>
<page-main v-loading="loading" style="width: 100%;">
<page-main v-loading="loading" style="width: 100%">
<div class="flex draw_container">
<div v-for="item in tableData" :key="item.id" style="height: 280px;" class="draw_img_container flex border">
<div
v-for="item in tableData"
:key="item.id"
style="height: 280px"
class="draw_img_container flex border"
>
<div class="draw_head">
<el-image fit="contain" :preview-src-list="[item.fileInfo.cosUrl]" :src="item.fileInfo.thumbImg" lazy class="draw_img" hide-on-click-modal />
<el-image
fit="contain"
:preview-src-list="[item.drawUrl]"
:src="item.drawUrl"
lazy
class="draw_img"
hide-on-click-modal
/>
</div>
<div class="draw_footer flex mt-3 justify-between items-center">
<el-tag class="ml-2" :type="item.rec ? 'success' : 'info'">
{{ item.rec ? '已推荐' : '未推荐' }}
</el-tag>
<el-button type="warning" plain size="small" @click="recommendDrawImg(item.id)">
<el-button
type="warning"
plain
size="small"
@click="recommendDrawImg(item.id)"
>
{{ item.rec ? '取消推荐' : '加入推荐' }}
<el-icon v-if="!item.rec">
<Plus />
@ -161,32 +195,32 @@ onMounted(() => {
</div>
</template>
<style lang="less">
.draw_container {
flex-wrap: wrap;
min-height: 400px;
<style lang="less">
.draw_container {
flex-wrap: wrap;
min-height: 400px;
}
.draw_img_container {
max-width: 18%;
flex-direction: column;
margin: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 5px;
.draw_head {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.draw_img {
width: 100%;
}
.draw_img_container {
max-width: 18%;
flex-direction: column;
margin: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 5px;
.draw_head{
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.draw_img {
width: 100%;
}
.draw_footer {
height: 25px;
}
.draw_footer {
height: 25px;
}
</style>
}
</style>

View File

@ -1,82 +1,85 @@
<route lang="yaml">
meta:
title: key列表
</route>
</route>
<script lang="ts" setup>
import { onMounted, reactive } from 'vue'
import { ElMessage, type FormInstance } from 'element-plus'
import ApiMj from '@/api/modules/mj'
import ApiChat from '@/api/modules/chat'
import { utcToShanghaiTime } from '@/utils/utcformatTime'
import { onMounted, reactive } from 'vue';
import { ElMessage, type FormInstance } from 'element-plus';
import ApiMj from '@/api/modules/mj';
import ApiChat from '@/api/modules/chat';
import { utcToShanghaiTime } from '@/utils/utcformatTime';
import { DRAW_STATUS_MAP, RECOMMEND_STATUS, WITHDRAW_STATUS_OPTIONS } from '@/constants/index'
import {
DRAW_STATUS_MAP,
RECOMMEND_STATUS,
WITHDRAW_STATUS_OPTIONS,
} from '@/constants/index';
const formRef = ref<FormInstance>()
const total = ref(0)
const loading = ref(false)
const formRef = ref<FormInstance>();
const total = ref(0);
const loading = ref(false);
const formInline = reactive({
rec: null,
status: null,
page: 1,
size: 10,
})
});
interface OrderInfo {
id: number
status: number
withdrawalAmount: number
userId: number
remark: string
contactInformation: string
auditStatus: number
auditUserId: number
createdAt: Date
updatedAt: Date
id: number;
status: number;
withdrawalAmount: number;
userId: number;
remark: string;
contactInformation: string;
auditStatus: number;
auditUserId: number;
createdAt: Date;
updatedAt: Date;
userInfo: {
avatar: string
username: string
email: string
}
avatar: string;
username: string;
email: string;
};
}
const tableData = ref<OrderInfo[]>([])
const tableData = ref<OrderInfo[]>([]);
async function queryDrawList() {
try {
loading.value = true
const res = await ApiMj.queryAdminDrawList(formInline)
loading.value = false
const { rows, count } = res.data
total.value = count
tableData.value = rows
}
catch (error) {
loading.value = false
loading.value = true;
const res = await ApiMj.queryAdminDrawList(formInline);
loading.value = false;
const { rows, count } = res.data;
total.value = count;
tableData.value = rows;
} catch (error) {
loading.value = false;
}
}
function handlerReset(formEl: FormInstance | undefined) {
formEl?.resetFields()
queryDrawList()
formEl?.resetFields();
queryDrawList();
}
async function recommendDrawImg(id: number) {
const res = await ApiChat.recMjDrawImg({ id })
ElMessage.success(res.data)
queryDrawList()
const res = await ApiChat.recMjDrawImg({ id });
ElMessage.success(res.data);
queryDrawList();
}
async function handleDelChatLog(id) {
const res = await ApiChat.delChatLog({ id })
ElMessage.success(res.data)
queryDrawList()
const res = await ApiChat.delChatLog({ id });
ElMessage.success(res.data);
queryDrawList();
}
onMounted(() => {
queryDrawList()
})
queryDrawList();
});
</script>
<template>
@ -84,24 +87,38 @@ onMounted(() => {
<page-main>
<el-form ref="formRef" :inline="true" :model="formInline">
<el-form-item label="推荐状态" prop="rec">
<el-select v-model="formInline.rec" placeholder="请选择推荐状态" clearable>
<el-option v-for="item in RECOMMEND_STATUS" :key="item.value" :label="item.label" :value="item.value" />
<el-select
v-model="formInline.rec"
placeholder="请选择推荐状态"
clearable
>
<el-option
v-for="item in RECOMMEND_STATUS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="绘制状态" prop="status">
<el-select v-model="formInline.status" placeholder="请选择绘制状态" clearable>
<el-option v-for="item in WITHDRAW_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
<el-select
v-model="formInline.status"
placeholder="请选择绘制状态"
clearable
>
<el-option
v-for="item in WITHDRAW_STATUS_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="queryDrawList">
查询
</el-button>
<el-button @click="handlerReset(formRef)">
重置
</el-button>
<el-button type="primary" @click="queryDrawList"> 查询 </el-button>
<el-button @click="handlerReset(formRef)"> 重置 </el-button>
</el-form-item>
<span style="float: right;">
<span style="float: right">
<el-button type="success" @click="queryDrawList">
刷新列表
</el-button>
@ -109,33 +126,77 @@ onMounted(() => {
</el-form>
</page-main>
<page-main>
<el-alert show-icon title="MJ绘图历史说明" description="点击推荐的图片将会出现在画廊当中!" type="success" />
<el-alert
show-icon
title="MJ绘图历史说明"
description="点击推荐的图片将会出现在画廊当中!"
type="success"
/>
</page-main>
<page-main style="width: 100%;">
<el-table v-loading="loading" border :data="tableData" style="width: 100%;" size="large">
<page-main style="width: 100%">
<el-table
v-loading="loading"
border
:data="tableData"
style="width: 100%"
size="large"
>
<el-table-column prop="id" align="center" label="ID" width="70" />
<el-table-column prop="fileInfo.thumbImg" align="center" label="绘图结果">
<el-table-column prop="drawUrl" align="center" label="绘图结果">
<template #default="scope">
<el-image style="height: 120px;" preview-teleported fit="contain" :preview-src-list="[scope.row?.fileInfo?.cosUrl]" :src="scope.row?.fileInfo?.thumbImg" lazy hide-on-click-modal />
<el-image
style="height: 120px"
preview-teleported
fit="contain"
:preview-src-list="[scope.row?.drawUrl]"
:src="scope.row?.drawUrl"
lazy
hide-on-click-modal
/>
</template>
</el-table-column>
<el-table-column prop="userInfo.username" align="center" label="用户名" width="120" />
<el-table-column prop="fileInfo.thumbImg" align="center" label="推荐状态" width="90">
<el-table-column
prop="userInfo.username"
align="center"
label="用户名"
width="120"
/>
<el-table-column
prop="fileInfo.thumbImg"
align="center"
label="推荐状态"
width="90"
>
<template #default="scope">
<el-tag :type="scope.row.rec === 1 ? 'success' : ''">
{{ scope.row.rec === 1 ? '已推荐' : '未推荐' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="userInfo.email" label="邮箱" width="180" align="center" />
<el-table-column prop="status" align="center" label="绘图状态" width="105">
<el-table-column
prop="userInfo.email"
label="邮箱"
width="180"
align="center"
/>
<el-table-column
prop="status"
align="center"
label="绘图状态"
width="105"
>
<template #default="scope">
<el-tag :type="scope.row.status === 100 ? 'success' : ''">
{{ DRAW_STATUS_MAP[scope.row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fullPrompt" label="绘图指令" align="center" width="200">
<el-table-column
prop="fullPrompt"
label="绘图指令"
align="center"
width="200"
>
<template #default="scope">
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
@ -149,19 +210,38 @@ onMounted(() => {
</el-popover>
</template>
</el-table-column>
<el-table-column prop="progress" align="center" label="绘图进度" width="90" />
<el-table-column
prop="progress"
align="center"
label="绘图进度"
width="90"
/>
<el-table-column prop="fileInfo.thumbImg" align="center" label="绘图尺寸" width="120">
<el-table-column
prop="fileInfo.thumbImg"
align="center"
label="绘图尺寸"
width="120"
>
<template #default="scope">
{{ scope.row?.fileInfo ? `${scope.row?.fileInfo?.width}*${scope.row?.fileInfo?.height}` : '---' }}
{{
scope.row?.fileInfo
? `${scope.row?.fileInfo?.width}*${scope.row?.fileInfo?.height}`
: '---'
}}
</template>
</el-table-column>
<el-table-column prop="userInfo.avatar" label="用户头像" width="90">
<template #default="scope">
<img :src="scope.row?.userInfo?.avatar" style="height: 50px;">
<img :src="scope.row?.userInfo?.avatar" style="height: 50px" />
</template>
</el-table-column>
<el-table-column prop="createdAt" label="提问时间" align="center" width="200">
<el-table-column
prop="createdAt"
label="提问时间"
align="center"
width="200"
>
<template #default="scope">
{{ utcToShanghaiTime(scope.row.createdAt, 'YYYY-MM-DD hh:mm:ss') }}
</template>
@ -169,18 +249,32 @@ onMounted(() => {
<el-table-column fixed="right" label="操作" width="200" align="center">
<template #default="scope">
<el-popconfirm :title="`确认${scope.row.rec === 1 ? '取消推荐' : '推荐'}图片吗!`" width="260" icon-color="red" @confirm="recommendDrawImg(scope.row.id)">
<el-popconfirm
:title="`确认${
scope.row.rec === 1 ? '取消推荐' : '推荐'
}图片吗`"
width="260"
icon-color="red"
@confirm="recommendDrawImg(scope.row.id)"
>
<template #reference>
<el-button link :type="scope.row.rec === 1 ? 'success' : '' " size="small">
<el-button
link
:type="scope.row.rec === 1 ? 'success' : ''"
size="small"
>
推荐图片
</el-button>
</template>
</el-popconfirm>
<el-popconfirm title="`确认删除此条记录么!" width="260" icon-color="red" @confirm="handleDelChatLog(scope.row.id)">
<el-popconfirm
title="`确认删除此条记录么!"
width="260"
icon-color="red"
@confirm="handleDelChatLog(scope.row.id)"
>
<template #reference>
<el-button type="warning" size="small">
删除记录
</el-button>
<el-button type="warning" size="small"> 删除记录 </el-button>
</template>
</el-popconfirm>
</template>

View File

@ -1,49 +1,59 @@
<route lang="yaml">
meta:
title: MJ设置
</route>
</route>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import apiConfig from '@/api/modules/config'
import { onMounted, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import apiConfig from '@/api/modules/config';
const formInline = reactive({
mjTimeoutMs: '', //
mjProxy: '0',
mjTimeoutMs: '500000',
mjProxyUrl: '',
mjLimitCount: null,
mjProxyImgUrl: '',
mjKey: '',
mjLimitCount: '2',
// mjProxyImgUrl: '',
mjNotSaveImg: '0',
mjUseBaiduFy: '0',
// mjUseBaiduFy: '0',
mjHideNotBlock: '0',
mjHideWorkIn: '0'
})
mjHideWorkIn: '0',
});
const rules = ref<FormRules>({})
const rules = ref<FormRules>({});
const formRef = ref<FormInstance>()
const formRef = ref<FormInstance>();
async function queryAllconfig() {
const res = await apiConfig.queryConfig({ keys: ['mjTimeoutMs', 'mjProxy', 'mjProxyUrl','mjLimitCount','mjNotSaveImg','mjProxyImgUrl','mjUseBaiduFy','mjHideNotBlock','mjHideWorkIn'] })
Object.assign(formInline, res.data)
const res = await apiConfig.queryConfig({
keys: [
'mjTimeoutMs',
'mjKey',
'mjProxyUrl',
'mjLimitCount',
'mjNotSaveImg',
// 'mjProxyImgUrl',
// 'mjUseBaiduFy',
'mjHideNotBlock',
'mjHideWorkIn',
],
});
Object.assign(formInline, res.data);
}
function handlerUpdateConfig() {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
await apiConfig.setConfig({ settings: fotmatSetting(formInline) })
ElMessage.success('变更配置信息成功')
}
catch (error) {}
queryAllconfig()
await apiConfig.setConfig({ settings: fotmatSetting(formInline) });
ElMessage.success('变更配置信息成功');
} catch (error) {}
queryAllconfig();
} else {
ElMessage.error('请填写完整信息');
}
else {
ElMessage.error('请填写完整信息')
}
})
});
}
function fotmatSetting(settings: any) {
@ -51,21 +61,27 @@ function fotmatSetting(settings: any) {
return {
configKey: key,
configVal: settings[key],
}
})
};
});
}
onMounted(() => {
queryAllconfig()
})
queryAllconfig();
});
</script>
<template>
<div>
<page-main>
<el-alert :closable="false" show-icon title="MJ参数说明" description="如果您是海外服务器则不强制开启代理、反之则需要开启代理、代理为系统配套项目、非常规代理、如果您想自己搭建代理请查看教程、如果您想使用系统提供的默认代理、那么选择开启代理并且不填写代理地址即可使用默认地址、如果想获取默认地址请在售后群获取地址!" type="success" />
<el-alert
:closable="false"
show-icon
title="MJ参数说明"
description="如果您是海外服务器则不强制开启代理、反之则需要开启代理、代理为系统配套项目、非常规代理、如果您想自己搭建代理请查看教程、如果您想使用系统提供的默认代理、那么选择开启代理并且不填写代理地址即可使用默认地址、如果想获取默认地址请在售后群获取地址!"
type="success"
/>
</page-main>
<el-card style="margin: 20px;">
<el-card style="margin: 20px">
<template #header>
<div class="flex justify-between">
<b>MJ参数设置</b>
@ -74,9 +90,14 @@ onMounted(() => {
</el-button>
</div>
</template>
<el-form ref="formRef" :rules="rules" :model="formInline" label-width="150px">
<el-form
ref="formRef"
:rules="rules"
:model="formInline"
label-width="150px"
>
<h4>绘图代理设置</h4>
<el-row>
<!-- <el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="是否开启代理" prop="mjProxy">
<el-switch
@ -86,107 +107,143 @@ onMounted(() => {
/>
</el-form-item>
</el-col>
</el-row>
</el-row> -->
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="请填写代理地址" prop="mjProxyUrl" label-width="150">
<el-input v-model="formInline.mjProxyUrl" placeholder="请填写代理地址、详细使用请访问教程!" clearable />
<el-form-item
label="请填写代理地址"
prop="mjProxyUrl"
label-width="150"
>
<el-input
v-model="formInline.mjProxyUrl"
placeholder="请填写代理地址、详细使用请访问教程!"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="绘画超时时间设置ms" prop="mjTimeoutMs" label-width="150">
<el-input v-model="formInline.mjTimeoutMs" placeholder="请设置绘画超时时间、单位为ms、根据慢速快速定义后续优化逻辑" clearable />
<el-form-item label="MJ 绘图 Key" prop="mjKey" label-width="150">
<el-input
v-model="formInline.mjKey"
placeholder="请填 MJ 绘图使用的 Key"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-divider />
<h4>绘图并发设置</h4>
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="单人绘图并发限制" prop="mjLimitCount" label-width="150">
<el-input v-model="formInline.mjLimitCount" placeholder="单人同时绘制限制数量、同一时间最多可以绘制几张!" clearable />
<el-form-item
label="绘画超时时间设置ms"
prop="mjTimeoutMs"
label-width="150"
>
<el-input
v-model="formInline.mjTimeoutMs"
placeholder="请设置绘画超时时间、单位为ms、根据慢速快速定义后续优化逻辑"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item
label="单人绘图并发限制"
prop="mjLimitCount"
label-width="150"
>
<el-input
v-model="formInline.mjLimitCount"
placeholder="单人同时绘制限制数量、同一时间最多可以绘制几张!"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-divider />
<h4>绘图可选设置</h4>
<el-row>
<!-- <el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="描述词使用百度翻译" prop="mjUseBaiduFy" label-width="150" >
<el-switch
<el-form-item
label="描述词使用百度翻译"
prop="mjUseBaiduFy"
label-width="150"
>
<el-switch
v-model="formInline.mjUseBaiduFy"
active-value="1"
inactive-value="0"
/>
<el-tooltip
class="box-item"
effect="dark"
placement="right"
>
<el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px;">
<div style="width: 250px">
mj描述词的翻译默认设置为AI翻译如果您想使用百度翻译请打开此选项并且在下面的百度翻译中配置上所需参数
</div>
</template>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
<el-icon class="ml-3 cursor-pointer"
><QuestionFilled
/></el-icon>
</el-tooltip>
</el-form-item>
</el-col>
</el-row> -->
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item
label="隐藏不需要元素模块"
prop="mjHideNotBlock"
label-width="150"
>
<el-switch
v-model="formInline.mjHideNotBlock"
active-value="1"
inactive-value="0"
/>
<el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px">
隐藏客户端绘图页面的不需要的元素模块隐藏后用户不可选择无法选中模块
</div>
</template>
<el-icon class="ml-3 cursor-pointer"
><QuestionFilled
/></el-icon>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="隐藏不需要元素模块" prop="mjHideNotBlock" label-width="150" >
<el-switch
v-model="formInline.mjHideNotBlock"
active-value="1"
inactive-value="0"
/>
<el-tooltip
class="box-item"
effect="dark"
placement="right"
>
<template #content>
<div style="width: 250px;">
隐藏客户端绘图页面的不需要的元素模块隐藏后用户不可选择无法选中模块
</div>
</template>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="隐藏工作中内容模块" prop="mjHideWorkIn" label-width="150" >
<el-switch
<el-form-item
label="隐藏工作中内容模块"
prop="mjHideWorkIn"
label-width="150"
>
<el-switch
v-model="formInline.mjHideWorkIn"
active-value="1"
inactive-value="0"
/>
<el-tooltip
class="box-item"
effect="dark"
placement="right"
>
<el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px;">
<div style="width: 250px">
客户端绘图页面隐藏掉工作中模块将不再展示给用户此模块
</div>
</template>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
<el-icon class="ml-3 cursor-pointer"
><QuestionFilled
/></el-icon>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
<el-divider />
<h4>图片存储设置</h4>
<el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="不存储图片" prop="mjNotSaveImg">
@ -195,28 +252,35 @@ onMounted(() => {
active-value="1"
inactive-value="0"
/>
<el-tooltip
class="box-item"
effect="dark"
placement="right"
>
<el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px;">
<div style="width: 250px">
默认会存储图片到配置的存储中如果开启此选择则表示不保存原图到我们配置的存储上那么则必须配置一个图片反代地址直接反代访问原始图片这样可以进一步节省空间需要您部署mj-proxy项目并填写基础地址即可
</div>
</template>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
<el-icon class="ml-3 cursor-pointer"
><QuestionFilled
/></el-icon>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
<el-row>
<!-- <el-row>
<el-col :xs="24" :md="20" :lg="15" :xl="12">
<el-form-item label="图片反代地址" prop="mjProxyImgUrl" label-width="150">
<el-input v-model="formInline.mjProxyImgUrl" placeholder="图片反代地址、用于代理访问图片、此项目请自行部署mj-proxy项目配置其中的地址即可" clearable style="width: 100%;" />
<el-form-item
label="图片反代地址"
prop="mjProxyImgUrl"
label-width="150"
>
<el-input
v-model="formInline.mjProxyImgUrl"
placeholder="图片反代地址、用于代理访问图片、此项目请自行部署mj-proxy项目配置其中的地址即可"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-row> -->
</el-form>
</el-card>
</div>

View File

@ -10,13 +10,13 @@ import ApiModels from '@/api/modules/models'
import { utcToShanghaiTime } from '@/utils/utcformatTime'
import {
DEDUCTTYPELIST,
MODELSMAP,
MODELSMAPLIST,
MODELTYPELIST,
MODEL_LIST,
ModelTypeLabelMap,
QUESTION_STATUS_OPTIONS,
MODELTYPELIST,
MODELSMAP,
DEDUCTTYPELIST,
MODELSMAPLIST,
} from '@/constants/index'
const formBlukRef = ref<FormInstance>()
@ -45,16 +45,17 @@ const formPackage = reactive({
status: true,
model: '',
isDraw: false,
isTokenBased: false,
tokenFeeRatio: 1000,
keyWeight: 1,
maxModelTokens: 4096,
modelOrder: 1,
maxModelTokens: 4000,
maxResponseTokens: 2000,
proxyUrl: '',
timeout: 300,
deduct: 1,
deductType: 1,
maxRounds: 12,
isTokenBased: false,
tokenFeeRatio: 1000,
})
const rules = reactive<FormRules>({
@ -164,8 +165,7 @@ async function queryModelsList() {
const { rows, count } = res.data
total.value = count
tableData.value = rows
}
catch (error) {
} catch (error) {
loading.value = false
}
}
@ -187,6 +187,7 @@ function handleEditKey(row: any) {
status,
model,
keyWeight,
modelOrder,
maxModelTokens,
maxResponseTokens,
proxyUrl,
@ -196,7 +197,7 @@ function handleEditKey(row: any) {
maxRounds,
isDraw,
isTokenBased,
tokenFeeRatio
tokenFeeRatio,
} = row
nextTick(() => {
Object.assign(formPackage, {
@ -216,7 +217,8 @@ function handleEditKey(row: any) {
maxRounds,
isDraw,
isTokenBased,
tokenFeeRatio
tokenFeeRatio,
modelOrder,
})
})
visible.value = true
@ -254,7 +256,7 @@ onMounted(() => {
<template>
<div>
<page-main>
<page-main style="padding-bottom:0">
<el-form ref="formRef" :inline="true" :model="formInline">
<el-form-item label="模型类型" prop="model">
<el-select
@ -303,21 +305,9 @@ onMounted(() => {
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="queryModelsList">
查询
</el-button>
<el-button @click="handlerReset(formRef)">
重置
</el-button>
<el-button type="primary" @click="queryModelsList"> 查询 </el-button>
<el-button @click="handlerReset(formRef)"> 重置 </el-button>
</el-form-item>
<span style="float: right">
<el-button type="success" @click="visible = true">
添加模型Key
<el-icon class="ml-3">
<Plus />
</el-icon>
</el-button>
</span>
</el-form>
</page-main>
<page-main>
@ -329,6 +319,12 @@ onMounted(() => {
/>
</page-main>
<page-main style="width: 100%">
<el-button type="success" @click="visible = true" style="margin-bottom:20px">
添加模型Key
<el-icon class="ml-3">
<Plus />
</el-icon>
</el-button>
<el-table
v-loading="loading"
border
@ -336,13 +332,19 @@ onMounted(() => {
style="width: 100%"
size="large"
>
<el-table-column prop="keyType" label="模型类型" width="120">
<!-- <el-table-column prop="keyType" label="模型类型" width="120">
<template #default="scope">
<el-tag type="success">
{{ MODELSMAP[scope.row.keyType] }}
</el-tag>
</template>
</el-table-column>
</el-table-column> -->
<el-table-column
prop="modelOrder"
label="模型排序"
width="90"
align="center"
/>
<el-table-column prop="modelName" label="模型名称" width="180" />
<el-table-column
prop="status"
@ -386,11 +388,10 @@ onMounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="isDraw"
align="center"
label="是否绘画KEY"
label="绘画KEY"
width="120"
>
<template #default="scope">
@ -402,7 +403,7 @@ onMounted(() => {
<el-table-column
prop="isTokenBased"
align="center"
label="设为Token计费"
label="Token计费"
width="120"
>
<template #default="scope">
@ -419,7 +420,7 @@ onMounted(() => {
>
<template #default="scope">
<el-tag :type="scope.row.deductType === 1 ? 'success' : 'warning'">
{{ scope.row.deductType === 1 ? '普通余额' : '高级余额' }}
{{ scope.row.deductType === 1 ? '普通积分' : '高级积分' }}
</el-tag>
</template>
</el-table-column>
@ -431,7 +432,7 @@ onMounted(() => {
>
<template #default="scope">
<el-tag :type="scope.row.deductType === 1 ? 'success' : 'warning'">
{{ `${scope.row.deduct} 余额` }}
{{ `${scope.row.deduct} 积分` }}
</el-tag>
</template>
</el-table-column>
@ -459,8 +460,8 @@ onMounted(() => {
scope.row.keyStatus === 1
? '正常工作'
: scope.row.keyStatus === -1
? '已被封禁'
: '余额耗尽 '
? '已被封禁'
: '余额耗尽 '
}}
</el-tag>
</template>
@ -575,7 +576,7 @@ onMounted(() => {
:model="formPackage"
:rules="rules"
>
<el-form-item label="模型类型选择" prop="keyType">
<!-- <el-form-item label="模型类型选择" prop="keyType">
<el-select
v-model="formPackage.keyType"
placeholder="请选择模型类型"
@ -588,6 +589,27 @@ onMounted(() => {
:value="item.value"
/>
</el-select>
</el-form-item> -->
<el-form-item label="模型中文名称" prop="modelName">
<el-input
v-model="formPackage.modelName"
placeholder="请填写模型中文名称(用户选择的)"
/>
</el-form-item>
<el-form-item label="模型排序" prop="modelOrder">
<el-input
v-model.number="formPackage.modelOrder"
placeholder="模型排序,越大越靠前。"
/>
<!-- <el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px">
填写此配置可以限制用户在选择模型时候的高级配置中的最大上下文轮次可以通过限制此数量减少token的损耗减低上下文的损耗量
如果设置了模型的最大token和返回量那么两个限制会同时生效
</div>
</template>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip> -->
</el-form-item>
<el-form-item label="模型启用状态" prop="status">
<el-switch v-model="formPackage.status" />
@ -597,23 +619,16 @@ onMounted(() => {
账号启用状态一旦锁定当前key将停止工作
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="模型中文名称" prop="modelName">
<el-input
v-model="formPackage.modelName"
placeholder="请填写模型中文名称(用户选择的)"
/>
</el-form-item>
<el-form-item :label="labelKeyName" prop="key">
<el-input
v-model="formPackage.key"
:type="Number(formPackage.keyType) === 1 ? 'textarea' : 'text'"
:rows="5"
placeholder="请填写模型Key|clientId|AppId"
placeholder="请填写模型Key"
style="width: 95%"
/>
<el-tooltip class="box-item" effect="dark" placement="right">
@ -622,15 +637,13 @@ onMounted(() => {
不同模型的设置不同例如openai仅设置key即可如果是百度大模型则填写clientId以及同时需要填写secret对于OPENAI模型我们支持批量导入如果您需要批量导入key则一行一个key即可多个key使用换行隔离其余配置将共享多个key可以重复选用默认模型
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item
v-if="[2].includes(Number(formPackage.keyType))"
label="SecretKey"
prop="secret"
v-if="[2].includes(Number(formPackage.keyType))"
>
<el-input
v-model="formPackage.secret"
@ -643,9 +656,7 @@ onMounted(() => {
不同账号填写的内容不同但是都代表的是Secret秘钥
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="账号关联模型" prop="model">
@ -670,9 +681,7 @@ onMounted(() => {
给定了部分可选的模型列表你可以可以手动填写您需要调用的模型请确保填写的模型是当前key支持的类型否则可能会在调用中出现不可预知错误
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="模型扣费类型" prop="deductType">
@ -694,12 +703,10 @@ onMounted(() => {
<el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px">
设置当前key的扣费类型扣除普通余额或是高级余额
设置当前key的扣费类型扣除普通积分或是高级积分
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="单次扣除金额" prop="deduct">
@ -711,12 +718,10 @@ onMounted(() => {
<el-tooltip class="box-item" effect="dark" placement="right">
<template #content>
<div style="width: 250px">
设置当前key的单次调用扣除余额建议同模型或名称key设置相同的金额避免扣费发生异常
设置当前key的单次调用扣除积分建议同模型或名称key设置相同的金额避免扣费发生异常
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="上下文限制" prop="maxRounds">
@ -732,9 +737,7 @@ onMounted(() => {
如果设置了模型的最大token和返回量那么两个限制会同时生效
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="调用轮询权重" prop="keyWeight">
@ -750,9 +753,7 @@ onMounted(() => {
保证每个key的调用顺序以及限制每次调用的准确次数
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="模型最大Token" prop="maxModelTokens">
@ -768,9 +769,9 @@ onMounted(() => {
/>
</el-form-item>
<el-form-item
v-if="[1].includes(Number(formPackage.keyType))"
label="调用超时时间"
prop="timeout"
v-if="[1].includes(Number(formPackage.keyType))"
>
<el-input
v-model.number="formPackage.timeout"
@ -778,9 +779,9 @@ onMounted(() => {
/>
</el-form-item>
<el-form-item
v-if="[1].includes(Number(formPackage.keyType))"
label="设为特殊key"
prop="isDraw"
v-if="[1].includes(Number(formPackage.keyType))"
>
<el-switch v-model="formPackage.isDraw" />
<el-tooltip class="box-item" effect="dark" placement="right">
@ -789,15 +790,13 @@ onMounted(() => {
基础绘画来自于OPENAI的DALL-E模型所以需要为官方的apiKey请确定至少设置一张key为基础绘画key即可使用绘画功能同时当前版本的mind思维导图和mj联想绘图等功能都会走当前设置的key会后后续版本解除此限制
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item
v-if="[1].includes(Number(formPackage.keyType))"
label="设为token计费"
prop="isTokenBased"
v-if="[1].includes(Number(formPackage.keyType))"
>
<el-switch v-model="formPackage.isTokenBased" />
<el-tooltip class="box-item" effect="dark" placement="right">
@ -806,12 +805,10 @@ onMounted(() => {
基于 token 计费计费方式为基础消费 * token消耗
</div>
</template>
<el-icon class="ml-3 cursor-pointer">
<QuestionFilled />
</el-icon>
<el-icon class="ml-3 cursor-pointer"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="token计费比例" prop="tokenFeeRatio">
<el-form-item label="token计费比例" prop="tokenFeeRatio">
<el-input
v-model.number="formPackage.tokenFeeRatio"
placeholder="请填写token计费比例"
@ -827,9 +824,9 @@ onMounted(() => {
</el-tooltip>
</el-form-item>
<el-form-item
v-if="[1].includes(Number(formPackage.keyType))"
label="指定代理地址"
prop="proxyUrl"
v-if="[1].includes(Number(formPackage.keyType))"
>
<el-input
v-model.number="formPackage.proxyUrl"
@ -848,9 +845,3 @@ onMounted(() => {
</el-dialog>
</div>
</template>
<style scoped>
.el-form--inline .el-form-item {
margin-right: 15px;
}
</style>

33
build.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
set -e
cd admin/
pnpm build
cd ..
cd chat/
pnpm build
cd ..
cd service/
pnpm build
cd ..
rm -rf AIWebQuickDeploy/dist/* AIWebQuickDeploy/public/* AIWebQuickDeploy/templates/*
mkdir -p AIWebQuickDeploy/dist AIWebQuickDeploy/public/admin AIWebQuickDeploy/templates
cp service/pm2.conf.json AIWebQuickDeploy/pm2.conf.json
cp service/package.json AIWebQuickDeploy/package.json
cp service/README.md AIWebQuickDeploy/README.md
cp service/.env.example AIWebQuickDeploy/.env.example
cp service/Dockerfile AIWebQuickDeploy/Dockerfile
cp service/docker-compose.yml AIWebQuickDeploy/docker-compose.yml
cp -r service/templates/* AIWebQuickDeploy/templates
cp -r service/dist/* AIWebQuickDeploy/dist
cp -r admin/dist/* AIWebQuickDeploy/public/admin
cp -r chat/dist/* AIWebQuickDeploy/public
echo "打包完成"

View File

@ -5,11 +5,11 @@ const path = require('path')
function configureAppMenu(mainWindow) {
let tray = new Tray(path.join(__dirname, '../icons/16x16.png'));
// tray.setToolTip('Nine Ai');
// tray.setToolTip('YiAi Ai');
const template = [
{
label: 'NineAi',
label: 'YiAi',
submenu: [
{
label: '退出应用',

View File

@ -26,7 +26,7 @@ function createMainWindow() {
if (app.isPackaged) {
// mainWindow.loadFile(filePath)
mainWindow.loadURL('https://ai.jiangly.com')
// mainWindow.loadURL('https://ai.jiangly.com')
}
else {
mainWindow.loadURL('http://127.0.0.1:1002')

View File

@ -190,6 +190,11 @@
}
}
}
console.log(
"%c本项目作者----小易联系QQ805239273",
"background-color:rgb(30,30,30);border-radius:4px;font-size:12px;padding:4px;color:rgb(220,208,129);"
)
</script>
</body>

View File

@ -1,106 +1,106 @@
{
"name": "chatgpt-cooper",
"version": "2.3.0",
"private": true,
"description": "ChatGPT Cooper",
"author": "Snine <J_longyan@163.com>",
"keywords": [
"chatgpt-cooper",
"chatgpt",
"chatbot",
"vue",
"nestjs"
],
"main": "electron/main.js",
"scripts": {
"start:h": "pnpm run -C service dev",
"start:f": "vite",
"all": "npm-run-all --parallel start:h start:f",
"dev": "vite",
"build-check": "run-p type-check build-only",
"preview": "vite preview",
"build": "vite build --mode=production",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bootstrap": "pnpm install && pnpm run common:prepare",
"start": "pnpm dev && electron .",
"ele": "electron .",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
"pack:mac": "NPM_CONFIG_ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ electron-builder build --mac",
"pack:win": "NPM_CONFIG_ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ electron-builder build --win --ia32"
},
"dependencies": {
"@electron/remote": "^2.1.0",
"@icon-park/vue-next": "^1.4.2",
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/dom-to-image": "^2.6.4",
"@types/file-saver": "^2.0.5",
"@vicons/ionicons5": "^0.12.0",
"@vueuse/core": "^9.13.0",
"@vueuse/electron": "^10.2.1",
"@vueuse/integrations": "^10.2.0",
"@vueuse/motion": "^2.0.0",
"add": "^2.0.6",
"clientjs": "^0.2.1",
"date-fns": "^2.30.0",
"dom-to-image": "^2.6.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.7.0",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"marked": "^4.3.0",
"markmap-common": "0.14.2",
"markmap-lib": "0.14.4",
"markmap-view": "0.14.4",
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"qrcode": "^1.5.3",
"v-viewer": "3.0.11",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"crypto-js": "^4.1.1",
"electron": "^25.3.1",
"electron-builder": "^24.4.0",
"eslint": "^8.35.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.2.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
},
"build": {
"productName": "NineAi",
"name": "chatgpt-cooper",
"version": "2.5.0",
"private": true,
"description": "ChatGPT Cooper",
"author": "Yi <a8052@qq.com>",
"keywords": [
"chatgpt-cooper",
"chatgpt",
"chatbot",
"vue",
"nestjs"
],
"main": "electron/main.js",
"scripts": {
"start:h": "pnpm run -C service dev",
"start:f": "vite",
"all": "npm-run-all --parallel start:h start:f",
"dev": "vite",
"build-check": "run-p type-check build-only",
"preview": "vite preview",
"build": "vite build --mode=production",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bootstrap": "pnpm install && pnpm run common:prepare",
"start": "pnpm dev && electron .",
"ele": "electron .",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
"pack:mac": "NPM_CONFIG_ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ electron-builder build --mac",
"pack:win": "NPM_CONFIG_ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ electron-builder build --win --ia32"
},
"dependencies": {
"@electron/remote": "^2.1.0",
"@icon-park/vue-next": "^1.4.2",
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/dom-to-image": "^2.6.4",
"@types/file-saver": "^2.0.5",
"@vicons/ionicons5": "^0.12.0",
"@vueuse/core": "^9.13.0",
"@vueuse/electron": "^10.2.1",
"@vueuse/integrations": "^10.2.0",
"@vueuse/motion": "^2.0.0",
"add": "^2.0.6",
"clientjs": "^0.2.1",
"date-fns": "^2.30.0",
"dom-to-image": "^2.6.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.7.0",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"marked": "^4.3.0",
"markmap-common": "0.14.2",
"markmap-lib": "0.14.4",
"markmap-view": "0.14.4",
"naive-ui": "^2.37.3",
"pinia": "^2.0.33",
"qrcode": "^1.5.3",
"v-viewer": "3.0.11",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"crypto-js": "^4.1.1",
"electron": "^25.3.1",
"electron-builder": "^24.4.0",
"eslint": "^8.35.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.2.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
},
"build": {
"productName": "Yiai",
"appId": "ai.jiangly.com",
"icon": "icons/icon.icns",
"directories": {

View File

@ -1,165 +1,223 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { get, post } from '@/utils/request'
import { useSettingStore } from '@/store'
import type { AxiosProgressEvent, GenericAbortSignal } from "axios";
import { get, post } from "@/utils/request";
import { useSettingStore } from "@/store";
/* 流失对话聊天 */
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
appId?: number
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
imageUrl?:string
model?:string
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chatgpt/chat-process',
data: { prompt: params.prompt, appId: params?.appId, options: params.options,imageUrl: params.imageUrl,model: params.model},
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
export function fetchChatAPIProcess<T = any>(params: {
prompt: string;
appId?: number;
options?: {
conversationId?: string;
parentMessageId?: string;
temperature: number;
};
imageUrl?: string;
model?: string;
signal?: GenericAbortSignal;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
}) {
return post<T>({
url: "/chatgpt/chat-process",
data: {
prompt: params.prompt,
appId: params?.appId,
options: params.options,
imageUrl: params.imageUrl,
model: params.model,
},
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
});
}
/* 获取个人信息 */
export function fetchGetInfo<T>() {
return get<T>({ url: '/auth/getInfo' })
return get<T>({ url: "/auth/getInfo" });
}
/* 注册 */
export function fetchRegisterAPI<T>(data: { username: string;password: string;email: string }): Promise<T> {
return post<T>({ url: '/auth/register', data }) as Promise<T>
export function fetchRegisterAPI<T>(data: {
username: string;
password: string;
email: string;
}): Promise<T> {
return post<T>({ url: "/auth/register", data }) as Promise<T>;
}
/* 注册 */
export function fetchRegisterByPhoneAPI<T>(data: { username: string;password: string; phone: string; phoneCode: string }): Promise<T> {
return post<T>({ url: '/auth/registerByPhone', data }) as Promise<T>
export function fetchRegisterByPhoneAPI<T>(data: {
username: string;
password: string;
phone: string;
phoneCode: string;
}): Promise<T> {
return post<T>({ url: "/auth/registerByPhone", data }) as Promise<T>;
}
/* 登录 */
export function fetchLoginAPI<T>(data: { username: string; password: string }): Promise<T> {
return post<T>({ url: '/auth/login', data }) as Promise<T>
export function fetchLoginAPI<T>(data: {
username: string;
password: string;
}): Promise<T> {
return post<T>({ url: "/auth/login", data }) as Promise<T>;
}
/* 手机号登录 */
export function fetchLoginByPhoneAPI<T>(data: { phone: string; password: string }): Promise<T> {
return post<T>({ url: '/auth/loginByPhone', data }) as Promise<T>
export function fetchLoginByPhoneAPI<T>(data: {
phone: string;
password: string;
}): Promise<T> {
return post<T>({ url: "/auth/loginByPhone", data }) as Promise<T>;
}
/* 修改个人信息 */
export function fetchUpdateInfoAPI<T>(data: { username?: string; avatar?: string }): Promise<T> {
return post<T>({ url: '/user/update', data }) as Promise<T>
export function fetchUpdateInfoAPI<T>(data: {
username?: string;
avatar?: string;
}): Promise<T> {
return post<T>({ url: "/user/update", data }) as Promise<T>;
}
/* 获取个人绘画记录 */
export function fetchGetChatLogDraw<T>(data: { model: string }): Promise<T> {
return get<T>({ url: '/chatLog/draw', data }) as Promise<T>
return get<T>({ url: "/chatLog/draw", data }) as Promise<T>;
}
/* 获取所有绘画记录 */
export function fetchGetAllChatLogDraw<T>(data: { size: number; rec: number; model: string }): Promise<T> {
return get<T>({ url: '/chatLog/drawAll', data }) as Promise<T>
export function fetchGetAllChatLogDraw<T>(data: {
size: number;
rec: number;
model: string;
}): Promise<T> {
return get<T>({ url: "/chatLog/drawAll", data }) as Promise<T>;
}
/* chatgpt的dall-e2绘画 */
export function fetchChatDraw<T>(data: { prompt: string;n: number;size: string }): Promise<T> {
return post<T>({ url: '/chatgpt/chat-draw', data }) as Promise<T>
export function fetchChatDraw<T>(data: {
prompt: string;
n: number;
size: string;
}): Promise<T> {
return post<T>({ url: "/chatgpt/chat-draw", data }) as Promise<T>;
}
/* 修改密码 */
export function fetchUpdatePasswordAPI<T>(data: { oldPassword?: string;password?: string }): Promise<T> {
return post<T>({ url: '/auth/updatePassword', data }) as Promise<T>
export function fetchUpdatePasswordAPI<T>(data: {
oldPassword?: string;
password?: string;
}): Promise<T> {
return post<T>({ url: "/auth/updatePassword", data }) as Promise<T>;
}
/* 同步对话 */
export function fetchGetchatSyncApi<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chatgpt/chat-sync',
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
export function fetchGetchatSyncApi<T = any>(params: {
prompt: string;
options?: {
conversationId?: string;
parentMessageId?: string;
temperature: number;
};
signal?: GenericAbortSignal;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
}) {
return post<T>({
url: "/chatgpt/chat-sync",
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
});
}
/* 获取mind绘画联想词 */
export function fetchGetchatMindApi<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chatgpt/chat-mind',
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
export function fetchGetchatMindApi<T = any>(params: {
prompt: string;
options?: {
conversationId?: string;
parentMessageId?: string;
temperature: number;
};
signal?: GenericAbortSignal;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
}) {
return post<T>({
url: "/chatgpt/chat-mind",
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
});
}
/* 获取MJ绘画联想词 */
export function fetchGetMjPromptAssociateApi<T>(data: { prompt: string }): Promise<T> {
return post<T>({ url: '/chatgpt/mj-associate', data }) as Promise<T>
export function fetchGetMjPromptAssociateApi<T>(data: {
prompt: string;
}): Promise<T> {
return post<T>({ url: "/chatgpt/mj-associate", data }) as Promise<T>;
}
/* 获取MJ绘画联想词 */
export function fetchGetMjPromptFanyiApi<T>(data: { prompt: string }): Promise<T> {
return post<T>({ url: '/chatgpt/mj-fy', data }) as Promise<T>
export function fetchGetMjPromptFanyiApi<T>(data: {
prompt: string;
}): Promise<T> {
return post<T>({ url: "/chatgpt/mj-fy", data }) as Promise<T>;
}
/* 获取我得绘制列表 */
export function fetchMidjourneyDrawList<T>(data: { page?: number; size?: number }): Promise<T> {
return get<T>({ url: '/midjourney/drawList', data }) as Promise<T>
export function fetchMidjourneyDrawList<T>(data: {
page?: number;
size?: number;
}): Promise<T> {
return get<T>({ url: "/midjourney/drawList", data }) as Promise<T>;
}
/* 获取Mj提示词 */
export function fetchMidjourneyPromptList<T>(): Promise<T> {
return get<T>({ url: '/midjourney/queryPrompts' }) as Promise<T>
return get<T>({ url: "/midjourney/queryPrompts" }) as Promise<T>;
}
/* 获取Mj完整提示词 */
export function fetchMidjourneyFullPrompt<T>(data: any): Promise<T> {
return get<T>({ url: '/midjourney/getFullPrompt', data }) as Promise<T>
return get<T>({ url: "/midjourney/getFullPrompt", data }) as Promise<T>;
}
/* 删除MJ绘画记录 */
export function fetchDownloadImg<T>(data: { id: number }): Promise<T> {
return post<T>({ url: '/midjourney/delete', data }) as Promise<T>
return post<T>({ url: "/midjourney/delete", data }) as Promise<T>;
}
/* 获取我得绘制列表 */
export function fetchMidjourneyGetList<T>(data: { page?: number; size?: number; rec: number }): Promise<T> {
return get<T>({ url: '/midjourney/getList', data }) as Promise<T>
export function fetchMidjourneyGetList<T>(data: {
page?: number;
size?: number;
rec: number;
}): Promise<T> {
return get<T>({ url: "/midjourney/getList", data }) as Promise<T>;
}
/* 推荐图片 */
export function fetchRecDraw<T>(data: { id: number }): Promise<T> {
return post<T>({ url: '/midjourney/rec', data }) as Promise<T>
return post<T>({ url: "/midjourney/rec", data }) as Promise<T>;
}
/* 获取图片验证码 */
export function fetchCaptchaImg<T>(data: { color: string }): Promise<T> {
return post<T>({ url: '/auth/captcha', data }) as Promise<T>
return post<T>({ url: "/auth/captcha", data }) as Promise<T>;
}
/* 发送手机验证码 */
export function fetchSendSms<T>(data: { phone: string; captchaId: string; captchaCode: string }): Promise<T> {
return post<T>({ url: '/auth/sendPhoneCode', data }) as Promise<T>
export function fetchSendSms<T>(data: {
phone: string;
captchaId: string;
captchaCode: string;
}): Promise<T> {
return post<T>({ url: "/auth/sendPhoneCode", data }) as Promise<T>;
}
/* 获取九宫格设置 */
export function fetchGetChatBoxList<T>() {
return get<T>({ url: '/chatgpt/queryChatBoxFrontend' })
return get<T>({ url: "/chatgpt/queryChatBoxFrontend" });
}
/* 获取快问设置 */
export function fetchGetChatPreList<T>() {
return get<T>({ url: '/chatgpt/queryChatPreList' })
return get<T>({ url: "/chatgpt/queryChatPreList" });
}

View File

@ -33,7 +33,7 @@ export function fetchTranslateAPI<T>(data: { text: string }): Promise<T> {
}
/* 提交一个绘画任务 */
export function fetchDrawTaskAPI<T>(data: { prompt?: string; imgUrl?: string; extraParam?: string; drawId?: number; action?: number; orderId?: number }): Promise<T> {
export function fetchDrawTaskAPI<T>(data: { prompt?: string; imgUrl?: string; extraParam?: string; drawId?: number; action?: string; orderId?: number }): Promise<T> {
return post<T>({
url: '/queue/addMjDrawQueue',
data,
@ -45,8 +45,8 @@ export function fetchProxyImgAPI<T>(data: { url: string }): Promise<T> {
return get<T>({
url: '/midjourney/proxy',
data,
headers: {
responseType: 'arraybuffer'
}
headers: {
responseType: 'arraybuffer'
}
})
}

BIN
chat/src/assets/voice.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -6,20 +6,23 @@ const props = withDefaults(defineProps<Props>(), {
gap: 10,
progress: 0,
tips: '',
words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
// words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
words: ['AI', '绘', '画', '中'],
})
const appStore = useAppStore()
const theme = computed(() => appStore.theme)
const loadingTextColor = computed(() => theme.value === 'dark' ? '#fff' : '#000')
const loadingTextColor = computed(() =>
theme.value === 'dark' ? '#fff' : '#000'
)
interface Props {
gap?: number
progress?: number
tips?: string
bgColor?: string
words?: any
words?: any
}
// const words = ref<string[]>(['L', 'O', 'A', 'D', 'I', 'N', 'G'])
</script>
@ -27,7 +30,13 @@ interface Props {
<template>
<div class="loading" :style="{ background: props.bgColor }">
<div class="loading-text">
<span v-for="item in props.words" :key="item" :style="{ margin: `0 ${props.gap}px`, color: loadingTextColor }" class="loading-text-words">{{ item }}</span>
<span
v-for="item in props.words"
:key="item"
:style="{ margin: `0 ${props.gap}px`, color: loadingTextColor }"
class="loading-text-words"
>{{ item }}</span
>
</div>
<div v-if="!tips && props.progress" class="progress">
绘制进度 {{ props.progress }}%
@ -46,14 +55,14 @@ interface Props {
width: 100%;
height: 100%;
z-index: 100;
user-select: none;
user-select: none;
}
.progress{
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
.progress {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.loading-text {
@ -72,42 +81,42 @@ interface Props {
display: inline-block;
margin: 0 5px;
color: #fff;
font-family: "Quattrocento Sans", sans-serif;
font-family: 'Quattrocento Sans', sans-serif;
}
.loading-text span:nth-child(1) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0s infinite linear alternate;
animation: blur-text 1.5s 0s infinite linear alternate;
animation: blur-text 1.5s 0s infinite linear alternate;
}
.loading-text span:nth-child(2) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.2s infinite linear alternate;
animation: blur-text 1.5s 0.2s infinite linear alternate;
animation: blur-text 1.5s 0.2s infinite linear alternate;
}
.loading-text span:nth-child(3) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.4s infinite linear alternate;
animation: blur-text 1.5s 0.4s infinite linear alternate;
animation: blur-text 1.5s 0.4s infinite linear alternate;
}
.loading-text span:nth-child(4) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.6s infinite linear alternate;
animation: blur-text 1.5s 0.6s infinite linear alternate;
animation: blur-text 1.5s 0.6s infinite linear alternate;
}
.loading-text span:nth-child(5) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.8s infinite linear alternate;
animation: blur-text 1.5s 0.8s infinite linear alternate;
animation: blur-text 1.5s 0.8s infinite linear alternate;
}
.loading-text span:nth-child(6) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 1s infinite linear alternate;
animation: blur-text 1.5s 1s infinite linear alternate;
animation: blur-text 1.5s 1s infinite linear alternate;
}
.loading-text span:nth-child(7) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 1.2s infinite linear alternate;
animation: blur-text 1.5s 1.2s infinite linear alternate;
animation: blur-text 1.5s 1.2s infinite linear alternate;
}
@-webkit-keyframes blur-text {

View File

@ -0,0 +1,377 @@
<script lang="ts" setup>
import {
onMounted,
ref,
computed,
onUnmounted,
getCurrentInstance,
watch,
nextTick,
} from 'vue';
import { throttle } from '@/utils/functions/throttle';
import { SvgIcon } from '@/components/common';
import { copyText } from '@/utils/format';
import { useMessage, NPopover } from 'naive-ui';
import { useAuthStore } from '@/store';
import { useRouter } from 'vue-router';
const authStore = useAuthStore();
interface Props {
dataList: FileItem[];
scaleWidth?: number;
isDrawLike?: boolean;
usePropmpt?: boolean;
copyPropmpt?: boolean;
gap?: number;
}
interface FileItem {
id: number;
drawUrl: string;
fullPrompt?: string;
drawRatio: string;
}
interface Emit {
(ev: 'loadMore'): void;
(ev: 'usePropmptDraw', prompt: string): void;
}
const props = withDefaults(defineProps<Props>(), {
gap: 5,
});
const emit = defineEmits<Emit>();
const $viewerApi =
getCurrentInstance()?.appContext.config.globalProperties.$viewerApi;
const ms = useMessage();
const boxRefs = ref<any>({});
const otherInfoContainerHeight = ref(0);
const realWidth = ref(160);
const realColumn = ref(0);
const loadComplete = ref<number[]>([]);
const wapperRef = ref<HTMLDivElement | null>(null);
const wapperHeigth = ref(0);
const isLogin = computed(() => authStore.isLogin);
const width = computed(() => {
return props.scaleWidth
? Number(props.scaleWidth) * 2 + props.gap + 150
: 150;
});
const router = useRouter();
/* 拿到图片高度 对定位top和right 新的一轮去插入最小值的那一列 贪心算法即可 */
function compilerContainer() {
calcHeight();
compilerColumn();
const columns = realColumn.value;
const itemWidth = realWidth.value;
const cacheHeight = <any>[];
props.dataList.forEach((item, index) => {
const drawRatio = item.drawRatio; // drawRatio "1632x2912"
const dimensions = drawRatio.split('x'); // 使 'x'
const width = parseInt(dimensions[0], 10); //
const height = parseInt(dimensions[1], 10); //
const bi = itemWidth / width;
const boxheight = height * bi + props.gap + otherInfoContainerHeight.value;
const currentBox = boxRefs.value[item.id];
if (cacheHeight.length < columns) {
currentBox.style.top = '0px';
currentBox.style.left = `${(itemWidth + props.gap) * index}px`;
cacheHeight.push(boxheight);
} else {
const minHeight = Math.min.apply(null, cacheHeight);
const minIndex = cacheHeight.findIndex((t: number) => t === minHeight);
currentBox.style.top = `${minHeight + 0}px`;
currentBox.style.left = `${minIndex * (realWidth.value + props.gap)}px`;
cacheHeight[minIndex] += boxheight;
}
});
wapperHeigth.value = Math.max(...cacheHeight) + 100;
}
function setItemRefs(el: HTMLDivElement, item: FileItem) {
if (el && item) {
boxRefs.value[item.id] = el;
}
}
/* 通过额外展示的信息计算有没有除了图片意外额外的高度 eg 图片100px 额外显示其他信息30px cacheHeight的高度在图片的基础上需要+30 */
function calcHeight() {
const { showName = 0, showOther = 0 } = {};
otherInfoContainerHeight.value =
[showName, showOther].filter((t) => t).length * 15;
}
watch(
() => props.scaleWidth,
(val) => {
handleResizeThrottled();
}
);
watch(
() => props.dataList,
(val) => {
if (!val) return;
nextTick(() => {
handleResizeThrottled();
});
},
{ immediate: true }
);
/* 计算放多少列比较合理,并计算最终单个图片的宽 */
function compilerColumn() {
if (!wapperRef.value) return;
const containerWidth = wapperRef.value.clientWidth;
/* 计算按目前宽度最多可以是几列 */
realColumn.value = Math.floor(containerWidth / width.value);
const surplus = containerWidth - realColumn.value * width.value; //
/* 计算如果给了左右间距那么作业间距需要占多少宽度 */
const positionWith = (realColumn.value - 1) * props.gap; // right padding
/* 总宽度减去right的宽度如果是负数考虑要不要cloumn-1 那么图片真实宽度就会比传入的宽度大 */
if (surplus - positionWith < 0) {
realColumn.value -= 1;
}
/* 图片宽度*列 + right的间距 不管大于小于总宽 多的或者少的那部分都平分给列容器 保证总宽是100% */
realWidth.value = Math.floor(
(containerWidth - positionWith) / realColumn.value
);
}
function imgLoadSuccess(e: any, item: FileItem) {
loadComplete.value.push(item.id);
}
function imgLoadError(e: any, item: FileItem) {
console.error('Image failed to load:', item);
loadComplete.value.push(item.id);
}
function handleCopy(item: any) {
if (!isLogin.value) {
return authStore.setLoginDialog(true);
}
const { prompt } = item;
copyText({ text: prompt });
ms.success('复制prompt成功');
}
function drawLike(item: any) {
router.push(`/midjourney?mjId=${item.id}`);
}
function usePropmptDraw(item: FileItem) {
const { fullPrompt } = item;
emit('usePropmptDraw', fullPrompt);
}
function handlePreview(item: any) {
const { drawUrl } = item;
$viewerApi({ options: {}, images: [drawUrl] });
}
const realHeight = computed(() => (item) => {
const ratio = item.drawRatio.split('x');
const originalWidth = Number(ratio[0]);
const originalHeight = Number(ratio[1]);
return originalHeight / (originalWidth / realWidth.value);
});
const handleResizeThrottled = throttle(function (this: any) {
compilerContainer();
}, 200);
onMounted(async () => {
window.addEventListener('resize', handleResizeThrottled);
const container: any = document.getElementById('footer');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
emit('loadMore');
}
});
});
observer.observe(container);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResizeThrottled);
});
</script>
<template>
<div class="min-h-full overflow-hidden flex flex-col">
<div class="flex-1 min-h-full p-4 relative">
<div
id="wapper"
ref="wapperRef"
class="wapper"
:style="{ height: `${wapperHeigth}px` }"
>
<div
v-for="(item, index) in dataList"
:id="item.id.toString()"
:key="index"
:ref="(el) => setItemRefs(el, item)"
class="wapper-item"
:style="{ width: `${realWidth}px` }"
>
<transition name="img" :css="true">
<img
:id="item.id.toString()"
class="item-file rounded-sm"
:style="{
width: `${realWidth}px`,
height: `${realHeight(item)}px`,
}"
:src="item.drawUrl"
loading="lazy"
@load="imgLoadSuccess($event, item)"
@error="imgLoadError($event, item)"
@click="handlePreview(item)"
/>
</transition>
<div class="menu p-2 text-[#cbd5e1]">
<div class="prompt">
{{ item.fullPrompt }}
</div>
<div class="flex justify-end items-end space-x-2">
<n-popover trigger="hover" v-if="isDrawLike">
<template #trigger>
<button
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click.stop="drawLike(item)"
>
<span class="text-sm dark:text-slate-400">
<SvgIcon
icon="fluent:draw-image-24-regular"
class="text-sm"
/>
</span>
</button>
</template>
<span>画同款</span>
</n-popover>
<n-popover trigger="hover" v-if="usePropmpt">
<template #trigger>
<button
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click.stop="usePropmptDraw(item)"
>
<span class="text-sm dark:text-slate-400">
<SvgIcon
icon="fluent:draw-image-24-regular"
class="text-sm"
/>
</span>
</button>
</template>
<span>使用当前画同款</span>
</n-popover>
<n-popover trigger="hover" v-if="copyPropmpt">
<template #trigger>
<button
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click.stop="handleCopy(item)"
>
<span class="text-sm dark:text-slate-400">
<SvgIcon icon="tabler:copy" class="text-sm" />
</span>
</button>
</template>
<span>复制提示词</span>
</n-popover>
</div>
</div>
<div
class="item-loading"
v-if="!loadComplete.includes(item.id)"
:style="{
width: `${realWidth}px`,
height: `${realHeight(item)}px`,
}"
></div>
</div>
<div id="footer" class="w-full absolute bottom-[350px]" />
</div>
</div>
</div>
</template>
<style lang="less">
.wapper {
width: 100%;
position: relative;
height: 100%;
padding-bottom: 20px;
&-item {
z-index: 10;
overflow: hidden;
position: absolute;
transition: all 0.5s;
cursor: pointer;
&:hover {
.menu {
transition: transform 0.3s ease-in-out;
transform: translateY(-10px);
}
img {
transform: scale(1.1);
}
}
.menu {
position: absolute;
bottom: 0;
width: 94%;
left: 3%;
max-height: 70%;
height: 100px;
transform: translateY(100%);
background-color: #090b15;
opacity: 0.8;
transition: all 0.1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
border-radius: 10px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.prompt {
height: 50px;
overflow: hidden;
}
}
img {
user-select: none;
cursor: pointer;
transition: all 0.6s cubic-bezier(0.19, 1, 0.22, 1);
border-radius: 6px;
}
.item-loading {
background: url(@/assets/img-bg.png) no-repeat center center;
filter: blur(20px);
position: absolute;
top: 0;
}
}
}
.img-enter-active,
.img-leave-active {
transition: transform 0.3s;
}
.img-enter,
.img-leave-to {
transform: scale(0.6);
opacity: 0;
}
</style>

View File

@ -317,7 +317,7 @@ onUnmounted(() => {
}
.item-loading {
background: url(../../assets/img-bg.png) no-repeat center center;
background: url(@/assets/img-bg.png) no-repeat center center;
filter: blur(20px);
position: absolute;
top: 0;

View File

@ -10,7 +10,8 @@ export function defaultState(): Chat.ChatState {
groupList: [],
chatList: [],
groupKeyWord: '',
baseConfig: null
baseConfig: null,
chatPreList: [],
}
}

View File

@ -15,7 +15,7 @@ export function defaultSetting(): UserState {
return {
userInfo: {
avatar: 'https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1681310872890image.png',
name: 'Nine Ai',
name: 'Yi Ai',
},
}
}

View File

@ -4,13 +4,6 @@ body,
height: 100%;
}
// @font-face {
// font-family: "nineai";
// src: url("//at.alicdn.com/wf/webfont/KDHmc7Mx03dG/NkbQEk5ZpA2z.woff2") format("woff2"),
// url("//at.alicdn.com/wf/webfont/KDHmc7Mx03dG/MG5Fkb82nDmI.woff") format("woff");
// font-display: swap;
// }
*{
font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", sans-serif;
}

View File

@ -13,6 +13,9 @@ import {
useDialog,
useMessage,
NAlert,
NSpace,
NPopselect,
NText,
} from 'naive-ui'
import type { MessageRenderMessage } from 'naive-ui'
@ -40,7 +43,7 @@ import {
useGlobalStoreWithOut,
} from '@/store'
import { fetchQueryOneCatAPI } from '@/api/appStore'
import { fetchChatAPIProcess } from '@/api'
import { fetchChatAPIProcess, fetchVideoAPIProcess } from '@/api'
import { t } from '@/locales'
import { router } from '@/router'
const uploadUrl = ref(`${import.meta.env.VITE_GLOB_API_URL}/upload/file`)
@ -84,6 +87,38 @@ const themeOptions: {
icon: 'noto-v1:last-quarter-moon-face',
},
]
const videoOptions: {
label: string
value: string
}[] = [
{
value: 'alloy',
label: 'alloy',
},
{
value: 'echo',
label: 'echo',
},
{
value: 'fable',
label: 'fable',
},
{
value: 'nova',
label: 'nova',
},
{
value: 'onyx',
label: 'onyx',
},
{
value: 'shimmer',
label: 'shimmer',
},
]
let currentVideo = ref('alloy')
const theme = computed(() => appStore.theme)
const globaelConfig = computed(() => authStore.globalConfig)
@ -198,6 +233,48 @@ function handleSignIn() {
useGlobalStore.updateSignInDialog(true)
}
const audioRef = ref(null)
const audioState = ref('Play')
function hendleVideo(item) {
var data = JSON.stringify({
model: 'tts-1',
input: item.text,
voice: 'alloy',
})
axios({
method: 'post',
url: 'https://api.oneapi.dwyu.cn/v1/audio/speech',
headers: {
Authorization:
'Bearer sk-z726fTNvD1jzSBZ42e8dF919840b48A5820e4e5d9d4e70A4',
'Content-Type': 'application/json',
},
data: data,
})
.then(function (response) {
console.log('--response.data', response.data)
const audio = audioRef.value
const blob = new Blob([response.data], { type: 'audio/mpeg' })
if (!audio) return
audio.src = URL.createObjectURL(blob)
console.log(audio)
audio.load()
audio.play()
// if (audio.paused) {
// audio.play()
// audioState.value = 'Stop'
// } else {
// audio.pause()
// audio.currentTime = 0
// audioState.value = 'Play'
// }
})
.catch(function (error) {
console.error('There was an error fetching the audio data', error)
})
}
// gpt-4-all
let curFile: File | null
@ -279,7 +356,6 @@ async function uploadFile() {
} finally {
dataBase64.value = null
curFile = null
showProgressModal.value = false
}
}
@ -426,6 +502,7 @@ async function onConversation(msg?: string) {
usage: data?.detail?.usage,
error: false,
loading: true,
imageUrl: data?.imageUrl,
conversationOptions: {
conversationId: data?.conversationId,
parentMessageId: data?.id,
@ -594,6 +671,7 @@ async function onConversation(msg?: string) {
return
}
updateGroupChat(dataSources.value.length - 1, {
imageUrl: data.imageUrl,
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
@ -607,6 +685,8 @@ async function onConversation(msg?: string) {
loading.value = false
isStreamIn.value = false
imageUrl = null
typingStatusEnd.value = true
scrollToBottom()
}
}
@ -818,6 +898,7 @@ onUnmounted(() => {
:imageUrl="item.imageUrl"
@regenerate="handleSubmit(index)"
@delete="handleDelete(item)"
@video="hendleVideo(item)"
/>
<div class="sticky bottom-1 left-0 flex justify-center">
<NButton v-if="loading" @click="handleStop">
@ -844,6 +925,7 @@ onUnmounted(() => {
@click="toggleUsingContext"
>
<span
class=""
:class="{
'text-[#3076fd]': usingContext,
'text-[#ffffff]': !usingContext,
@ -971,7 +1053,6 @@ onUnmounted(() => {
</NTooltip>
</div>
</div>
<div
class="m-auto max-w-screen-4xl"
:class="[isMobile ? 'px-2 py-1' : 'px-4 py-2']"
@ -1097,35 +1178,6 @@ onUnmounted(() => {
</div>
</div>
<div class="flex justify-between items-center">
<!-- <div
class="flex items-center text-neutral-400 cursor-pointer hover:text-[#3076fd]"
>
<span class="ml-2 mr-2 text-xs" @click="toggleUsingNetwork"
>{{ usingNetwork ? '关闭' : '开启' }}联网访问</span
>
<NTooltip trigger="hover" :disabled="isMobile">
<template #trigger>
<SvgIcon
icon="zondicons:network"
class="cursor-pointer mb-0.5"
:class="[
{
'text-[#3076fd]': usingNetwork,
'': !usingNetwork,
},
]"
@click="toggleUsingNetwork"
/>
</template>
{{ usingNetwork ? '关闭联网模式' : '开启联网模式' }}
</NTooltip>
<div
class="mx-4 h-full text-neutral-300 dark:text-neutral-600"
>
|
</div>
</div> -->
<NButton
type="primary"
size="small"
@ -1181,6 +1233,10 @@ onUnmounted(() => {
/>
</NCard>
</NModal>
<!-- <audio ref="audioRef" controls>
<source type="audio/mpeg" />
</audio> -->
</div>
</template>

View File

@ -61,11 +61,9 @@ const themeOptions: {
]
const modelName = computed(() => {
if (!chatStore.activeConfig)
return
if (!chatStore.activeConfig) return
const { modelTypeInfo, modelInfo } = chatStore.activeConfig
if (!modelTypeInfo || !modelInfo)
return
if (!modelTypeInfo || !modelInfo) return
return `${modelTypeInfo?.label} / ${modelInfo.modelName}`
})
@ -85,8 +83,7 @@ function handleUpdateCollapsed() {
function onScrollToTop() {
const scrollRef = document.querySelector('#scrollRef')
if (scrollRef)
nextTick(() => (scrollRef.scrollTop = 0))
if (scrollRef) nextTick(() => (scrollRef.scrollTop = 0))
}
function handleExport() {
@ -180,7 +177,7 @@ function handleSignIn() {
</div>
</div>
</NPopover> -->
<NTooltip v-if="isMobile" trigger="hover" :disabled="isMobile">
<template #trigger>
<button
@ -199,7 +196,7 @@ function handleSignIn() {
<button
class="flex h-8 w-8 items-center justify-center rounded border transition hover:bg-[#eef0f3] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click="handleExport"
v-show="!isMobile"
v-show="!isMobile"
>
<span class="text-base text-slate-500 dark:text-slate-400">
<SvgIcon
@ -216,7 +213,9 @@ function handleSignIn() {
class="flex h-8 w-8 items-center justify-center rounded border transition hover:bg-[#eef0f3] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click="handleClear"
>
<span class="text-base text-slate-500 dark:text-slate-400"><SvgIcon icon="material-symbols:delete-outline"/></span>
<span class="text-base text-slate-500 dark:text-slate-400"
><SvgIcon icon="material-symbols:delete-outline"
/></span>
</button>
</template>
删除本页内容
@ -227,7 +226,9 @@ function handleSignIn() {
class="flex h-8 w-8 items-center justify-center rounded border transition hover:bg-[#eef0f3] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click="handleScrollBtm"
>
<span class="text-base text-slate-500 dark:text-slate-400"><SvgIcon icon="material-symbols:keyboard-arrow-down" /></span>
<span class="text-base text-slate-500 dark:text-slate-400"
><SvgIcon icon="material-symbols:keyboard-arrow-down"
/></span>
</button>
</template>
滚动到底部

View File

@ -4,11 +4,13 @@ import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import { NButton, NIcon } from 'naive-ui'
import { NButton, NIcon, NImage } from 'naive-ui'
import { Copy, Delete } from '@icon-park/vue-next'
import { useAppStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
import { SvgIcon } from '@/components/common'
interface Props {
inversion?: boolean
error?: boolean
@ -22,6 +24,7 @@ interface Emit {
(ev: 'regenerate'): void
(ev: 'delete'): void
(ev: 'copy'): void
(ev: 'video'): void
}
const props = defineProps<Props>()
@ -106,6 +109,10 @@ function handleRegenerate() {
emit('regenerate')
}
function hendleVideo() {
emit('video')
}
function handleCopy() {
emit('copy')
}
@ -118,106 +125,137 @@ defineExpose({ textRef })
</script>
<template>
<div :class="wrapClass" class="w-full" style="width: auto">
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion" class="flex flex-col items-start">
<div class="w-full">
<div
v-if="!asRawText"
class="w-full markdown-body"
:class="[{ 'markdown-body-generate': loading }]"
v-html="text"
/>
<div v-else class="w-full whitespace-pre-wrap" v-text="text" />
<span
<div class="flex flex-col group max-w-full" style="width: auto">
<div :class="wrapClass" class="w-full" style="width: auto">
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion" class="flex flex-col items-start">
<div class="w-full">
<div
v-if="!asRawText"
class="w-full markdown-body"
:class="[{ 'markdown-body-generate': loading }]"
v-html="text"
/>
<div v-else class="w-full whitespace-pre-wrap" v-text="text" />
<!-- <span
v-if="loading"
class="dark:text-white w-[4px] h-[20px] block animate-blink"
style="display: none"
class="dark:text-white w-[4px] h-[10px] block animate-blink"
/> -->
<NImage
v-if="imageUrl && isImageUrl"
:src="imageUrl"
:preview-src="imageUrl"
alt="图片"
class="h-md rounded-md m-1"
:style="{ 'max-width': isMobile ? '100%' : '20vw' }"
style="margin-top: 0.5rem"
/>
</div>
<!-- 小易改动注册掉底部的内容 -->
</div>
<div v-else>
<div class="whitespace-pre-wrap" v-text="text" />
<NImage
:src="imageUrl"
:preview-src="imageUrl"
alt="图片"
class="h-md rounded-md m-1"
:style="{ 'max-width': isMobile ? '100%' : '20vw' }"
style="margin-top: 0.5rem"
v-if="imageUrl && isImageUrl"
/>
</div>
<!-- 小易改动注册掉底部的内容 -->
<!-- <div style="margin-top: 0.5rem"> -->
<!-- <NButton class="ml-2" text type="primary" @click="handleCopy">
<template #icon>
<NIcon :size="10" :component="Copy" />
</template>
<span class="text-xs">复制</span>
</NButton>
</div>
</div>
<div
class="flex opacity-0 transition-opacity duration-300 group-hover:opacity-100 text-gray-700"
>
<div v-if="!inversion">
<div class="mt-1 flex">
<button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="handleCopy"
>
<SvgIcon class="flex h-3 w-3 mx-1" icon="tabler:copy" />
<span class="flex text-xs">复制</span>
</button>
<span style="margin-left: 0.5rem" />
<NButton
class="ml-2"
<button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="handleRegenerate"
>
<SvgIcon class="flex h-3 w-3 mx-1" icon="clarity:refresh-line" />
<span class="flex text-xs">重新生成</span>
</button>
<button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="handleDelete"
>
<SvgIcon
class="flex h-3 w-3 mx-1"
icon="fluent:delete-48-regular"
/>
<span class="flex text-xs">删除</span>
</button>
<button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="asRawText = !asRawText"
>
<template #icon>
<SvgIcon
class="text-xs"
:icon="asRawText ? 'ic:outline-code-off' : 'ic:outline-code'"
/>
</template>
<span class="text-xs">{{
<SvgIcon
class="flex h-3 w-3 mx-1"
:icon="asRawText ? 'ic:outline-code-off' : 'ic:outline-code'"
/>
<span class="flex text-xs">{{
asRawText ? t('chat.preview') : t('chat.showRawText')
}}</span>
</NButton> -->
<!-- 删除BUG -->
<!-- <span style="margin-left: 0.5rem" />
<NButton text type="primary" @click="handleDelete">
<template #icon>
<NIcon :size="10" :component="Delete" />
</template>
<span class="text-xs">删除</span>
</NButton> -->
</button>
<!-- <span style="margin-left: 0.5rem" />
<NButton text type="primary" @click="handleRegenerate">
<template #icon>
<NIcon :size="10" :component="Refresh" />
</template>
<span class="text-xs">重新回答</span>
</NButton> -->
<!-- </div> -->
<!-- <button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="hendleVideo"
>
<img src="@/assets/voice.gif" class="flex h-3 w-3 mx-1" />
<SvgIcon class="flex h-3 w-3 mx-1" icon="ep:video-play" />
<span class="flex text-xs">播放</span>
</button> -->
</div>
</div>
<div v-else>
<div class="whitespace-pre-wrap" v-text="text" />
<a v-if="imageUrl && isImageUrl" :href="imageUrl" target="_blank">
<img
:src="imageUrl"
alt="图片"
class="h-auto rounded-md mb-1"
:class="{ 'max-w-full': isMobile, 'max-w-sm': !isMobile }"
style="margin-top: 0.5rem"
/>
</a>
<a
:href="imageUrl"
target="_blank"
:class="{ 'file-2': isMobile, 'file-1': !isMobile }"
>
<img
src="@/assets/file.jpeg"
alt="文件"
class="h-auto rounded-md mb-1"
:class="{ 'file-2': isMobile, 'file-1': !isMobile }"
v-if="imageUrl && !isImageUrl"
/>
</a>
<div v-if="false" style="margin-left: 0.5rem">
<NButton class="ml-2" text color="#FFF" @click="handleCopy">
<template #icon>
<NIcon :size="10" :component="Copy" />
</template>
<span class="text-xs">复制</span>
</NButton>
<span class="ml-3" />
<NButton text color="#FFF" @click="handleDelete">
<template #icon>
<NIcon :size="10" :component="Delete" />
</template>
<span class="text-xs">删除</span>
</NButton>
<div v-else>
<div class="mt-1 flex">
<button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="handleCopy"
>
<SvgIcon class="flex h-3 w-3 mx-1" icon="tabler:copy" />
<span class="flex text-xs">复制</span>
</button>
<button
class="flex ml-0 items-center text-gray-400 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-400 mx-1"
text
type="primary"
@click="handleDelete"
>
<SvgIcon
class="flex h-3 w-3 mx-1"
icon="fluent:delete-48-regular"
/>
<span class="flex text-xs">删除</span>
</button>
</div>
</div>
</div>

View File

@ -21,6 +21,7 @@ interface Props {
interface Emit {
(ev: 'regenerate'): void
(ev: 'delete'): void
(ev: 'video'): void
}
const props = defineProps<Props>()
@ -81,6 +82,10 @@ function handleDetele() {
emit('delete')
}
function hendleVideo() {
emit('video')
}
function handleCopy() {
copyText({ text: props.text ?? '' })
props.text && ms.success('复制成功!')
@ -128,9 +133,10 @@ function handleRegenerate() {
@regenerate="handleRegenerate"
@copy="handleCopy"
@delete="handleDetele"
@video="hendleVideo"
:imageUrl="imageUrl"
/>
<div class="flex flex-col">
<!-- <div class="flex flex-col">
<button
v-if="!inversion"
class="flex mb-2 transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-300"
@ -150,7 +156,7 @@ function handleRegenerate() {
<SvgIcon icon="ri:more-2-fill" />
</button>
</NDropdown>
</div>
</div> -->
</div>
</div>
</div>

View File

@ -6,7 +6,7 @@ import { fetchChatDraw, fetchGetAllChatLogDraw, fetchGetChatLogDraw } from '@/ap
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { TitleBar } from '@/components/base'
import { useAppStore, useAuthStore } from '@/store'
import GridManager from '@/components/common/GridManager/index.vue'
import OldGridManager from '@/components/common/OldGridManager/index.vue'
import Loading from '@/components/base/Loading.vue'
const theme = computed(() => appStore.theme)
@ -73,14 +73,14 @@ function updateEx() {
}
async function queryMyDrawList() {
const res: ResData = await fetchGetChatLogDraw({ model: 'DALL-E2' })
const res: ResData = await fetchGetChatLogDraw({ model: 'dall-e-3' })
if (!res.success)
return
mineDrawList.value = formatFileInfo(res.data)
}
async function queryAllDrawList() {
const res: ResData = await fetchGetAllChatLogDraw({ size: 999, rec: 1, model: 'DALL-E2' })
const res: ResData = await fetchGetAllChatLogDraw({ size: 999, rec: 1, model: 'dall-e-3' })
if (!res.success)
return ms.error(res.message)
allDrawList.value = formatFileInfo(res.data.rows)
@ -184,13 +184,13 @@ onMounted(() => {
参数设置
</h4>
<div class="flex items-center mt-5">
<span class="mr-2 inline-block w-16 flex-shrink-0">图片尺寸:</span>
<span class="mr-2 inline-block w-20 flex-shrink-0">图片尺寸:</span>
<div>
<span v-for="item in imageSizeList" :key="item.value" class="rounded ml-2 select-none cursor-pointer inline-block mb-2" :class="[item.value === form.size ? ['text-primary', 'bg-[#0d6efd1c]'] : ['bg-[#bfc4d033]'], isMobile ? 'px-1.5 py-0.5' : 'px-3 py-1']" @click="form.size = item.value">{{ item.label }}</span>
</div>
</div>
<div class="flex items-center mt-5">
<span class="mr-2 inline-block w-16 flex-shrink-0">图片质量:</span>
<span class="mr-2 inline-block w-20 flex-shrink-0">图片质量:</span>
<div>
<span v-for="item in qualityList" :key="item.value" class=" py-0.5 px-2.5 rounded ml-2 select-none cursor-pointer inline-block mb-2" :class="item.value === form.quality ? ['text-primary', 'bg-[#0d6efd1c]'] : ['bg-[#bfc4d033]']" @click="form.quality = item.value">{{ item.label }}</span>
</div>
@ -232,13 +232,13 @@ onMounted(() => {
<NTabs type="line" animated class="mt-5" @update:value="updateTabs">
<NTabPane name="all" tab="公共生成">
<div v-if="allDrawList.length" class="min-h-screen">
<GridManager @loadMore="loadMore" usePropmpt :gap="8" preOrigin @usePropmptDraw="usePropmptDraw" :dataList="allDrawList" :scaleWidth="50" />
<OldGridManager @loadMore="loadMore" usePropmpt :gap="8" preOrigin @usePropmptDraw="usePropmptDraw" :dataList="allDrawList" :scaleWidth="50" />
</div>
<NEmpty v-else size="huge" class="mt-20" description="暂无数据哟~" />
</NTabPane>
<NTabPane name="mine" tab="我的生成">
<div v-if="mineDrawList.length" class="min-h-screen">
<GridManager @loadMore="loadMore" usePropmpt :gap="8" preOrigin @usePropmptDraw="usePropmptDraw" :dataList="mineDrawList" :scaleWidth="50" />
<OldGridManager @loadMore="loadMore" usePropmpt :gap="8" preOrigin @usePropmptDraw="usePropmptDraw" :dataList="mineDrawList" :scaleWidth="50" />
</div>
<NEmpty v-else size="huge" class="mt-20" description="暂无数据哟~" />
</NTabPane>

View File

@ -7,8 +7,8 @@ import { NButton } from 'naive-ui'
const proxyImgBase = ref('')
const mode1Url = 'https://chevereto.jiangly.com/images/2023/11/21/61888bd4ede4.png'
const mode2Url = 'https://chevereto.jiangly.com/images/2023/11/19/shoes.jpg'
const mode1Url = ''
const mode2Url = ''
const canvasRef = ref<any>(null)

View File

@ -1,153 +1,163 @@
<script lang="ts" setup>
import { onMounted, ref, computed } from 'vue'
import { fetchMidjourneyGetList } from '@/api'
import type { ResData } from '@/api/types'
import { NSlider, NInput, NIcon } from 'naive-ui'
import GridManager from '@/components/common/GridManager/index.vue'
import { FlashOutline } from '@vicons/ionicons5'
import { onMounted, ref, computed } from 'vue';
import { fetchMidjourneyGetList } from '@/api';
import type { ResData } from '@/api/types';
import { NSlider, NInput, NIcon } from 'naive-ui';
import GridManager2 from '@/components/common/GridManager2/index.vue';
import { FlashOutline } from '@vicons/ionicons5';
const imageList = ref<any>([])
const wapperRef = ref<HTMLDivElement | null>(null)
const scaleWidth = ref(50)
const keyword = ref('')
const page = ref(1)
const size = ref(20)
const loading = ref(false)
const isMore = ref(true)
const imageList = ref<any>([]);
const wapperRef = ref<HTMLDivElement | null>(null);
const scaleWidth = ref(50);
const keyword = ref('');
const page = ref(1);
const size = ref(20);
const loading = ref(false);
const isMore = ref(true);
const dataList = computed(() => {
if (!keyword.value) {
return imageList.value;
} else {
return imageList.value.filter((item: any) => {
const { prompt } = item;
return prompt.includes(keyword.value);
});
}
});
const dataList = computed(() => {
if(!keyword.value){
return imageList.value
}else{
return imageList.value.filter((item: any) => {
const { prompt } = item
return prompt.includes(keyword.value)
})
}
})
async function queryDrawImg() {
loading.value = true;
const res: ResData = await fetchMidjourneyGetList({
page: page.value,
size: size.value,
rec: 1,
});
loading.value = false;
isMore.value = size.value === res.data.rows.length;
imageList.value = [...imageList.value, ...res.data.rows];
}
async function queryDrawImg() {
loading.value = true
const res: ResData = await fetchMidjourneyGetList({ page: page.value ,size: size.value, rec: 1 })
loading.value = false
isMore.value = size.value === res.data.rows.length
imageList.value = [...imageList.value, ...res.data.rows]
}
onMounted(async () => {
await queryDrawImg();
});
onMounted(async () => {
await queryDrawImg()
})
function loadMore() {
page.value = page.value + 1;
queryDrawImg();
}
</script>
function loadMore(){
page.value = page.value + 1
queryDrawImg()
}
</script>
<template>
<div
class="bg-[#fff] h-[100vh] overflow-hidden p-4 pr-0 dark:bg-[#18181c] flex flex-col"
>
<div class="p-4 flex pr-6 justify-between items-center">
<div class="font-bold text-xl">AI绘画广场</div>
<div class="w-[200px] sm:w-[300px] flex justify-between">
<span class="hidden sm:block">尺寸调整</span>
<div class="flex-1 ml-5">
<n-slider v-model:value="scaleWidth" :step="10" />
</div>
</div>
</div>
<div class="px-4 mb-1 pr-5">
<n-input v-model:value="keyword" placeholder="prompt关键词搜索">
<template #prefix>
<n-icon :component="FlashOutline" />
</template>
</n-input>
</div>
<div
class="market overflow-y-scroll flex-1 min-h-screen p-4 dark:bg-[#18181c] relative"
>
<div id="wapper" ref="wapperRef" class="wapper">
<GridManager2
@loadMore="loadMore"
copyPropmpt
isDrawLike
:dataList="dataList"
:scaleWidth="scaleWidth"
/>
</div>
</div>
</div>
</template>
<template>
<div class="bg-[#fff] h-[100vh] overflow-hidden p-4 pr-0 dark:bg-[#18181c] flex flex-col">
<div class="p-4 flex pr-6 justify-between items-center">
<div class="font-bold text-xl">AI绘画广场</div>
<div class="w-[200px] sm:w-[300px] flex justify-between">
<span class="hidden sm:block" >尺寸调整</span>
<div class="flex-1 ml-5">
<n-slider v-model:value="scaleWidth" :step="10" />
</div>
</div>
</div>
<div class="px-4 mb-1 pr-5">
<n-input v-model:value="keyword" placeholder="prompt关键词搜索">
<template #prefix>
<n-icon :component="FlashOutline" />
</template>
</n-input>
</div>
<div class="market overflow-y-scroll flex-1 min-h-screen p-4 dark:bg-[#18181c] relative ">
<div id="wapper" ref="wapperRef" class="wapper">
<GridManager @loadMore="loadMore" copyPropmpt isDrawLike :dataList="dataList" :scaleWidth="scaleWidth" />
</div>
</div>
</div>
<style lang="less">
.market {
padding: 15px;
}
</template>
.wapper {
width: 100%;
position: relative;
height: 100%;
padding-bottom: 20px;
<style lang="less">
.market{
padding: 15px;
}
&-item {
z-index: 10;
overflow: hidden;
position: absolute;
transition: all 0.5s;
cursor: pointer;
.wapper{
width: 100%;
position: relative;
height: 100%;
padding-bottom: 20px;
&:hover {
.menu {
transition: transform 0.3s ease-in-out;
transform: translateY(-10px);
}
img {
transform: scale(1.1);
}
}
.menu {
position: absolute;
bottom: 0;
width: 94%;
left: 3%;
max-height: 70%;
height: 100px;
transform: translateY(100%);
background-color: #090b15;
opacity: 0.8;
transition: all 0.1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
border-radius: 10px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
&-item{
z-index: 10;
overflow: hidden;
position: absolute;
transition: all 0.5s;
cursor: pointer;
.prompt {
height: 50px;
overflow: hidden;
}
}
&:hover{
.menu{
transition: transform 0.3s ease-in-out;
transform: translateY(-10px);
}
img{
transform: scale(1.1);
}
}
img {
user-select: none;
cursor: pointer;
transition: all 0.6s cubic-bezier(0.19, 1, 0.22, 1);
border-radius: 6px;
}
.menu{
position: absolute;
bottom: 0;
width: 94%;
left: 3%;
max-height: 70%;
height: 100px;
transform: translateY(100%);
background-color: #090b15;
opacity: 0.8;
transition: all .1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
border-radius: 10px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.item-loading {
background: url(../../assets/img-bg.png) no-repeat center center;
filter: blur(20px);
position: absolute;
top: 0;
}
}
}
.prompt{
height: 50px;
overflow: hidden;
}
}
img{
user-select: none;
cursor: pointer;
transition: all .6s cubic-bezier(0.19, 1, 0.22, 1);
border-radius: 6px;
}
.item-loading{
background: url(../../assets/img-bg.png) no-repeat center center;
filter: blur(20px);
position: absolute;
top: 0;
}
}
}
.img-enter-active, .img-leave-active {
transition: transform .3s;
}
.img-enter, .img-leave-to{
transform: scale(.6);
opacity: 0;
}
</style>
.img-enter-active,
.img-leave-active {
transition: transform 0.3s;
}
.img-enter,
.img-leave-to {
transform: scale(0.6);
opacity: 0;
}
</style>

View File

@ -1,67 +1,71 @@
<script lang='ts' setup>
import { NButton, NImage, NInput, NInputNumber, NSelect, NSpace, NSwitch, NTag, NTooltip, useDialog, useMessage, NScrollbar } from 'naive-ui'
import { onMounted, reactive, ref, toRefs, watch, computed } from 'vue'
import axios from 'axios'
import { fetchDownloadImg } from '@/api'
import type { ResData } from '@/api/types'
import failImg from '@/assets/fail.png'
import drawSvg from '@/assets/icons/draw.svg'
import zoomSvg from '@/assets/icons/zoom.svg'
import { useAppStore, useAuthStore } from '@/store'
import { fetchDrawTaskAPI, fetchTranslateAPI } from '@/api/mjDraw'
import { SvgIcon } from '@/components/common'
import Loading from '@/components/base/Loading.vue'
<script lang="ts" setup>
import {
NButton,
NImage,
NInput,
NInputNumber,
NSelect,
NSpace,
NSwitch,
NTag,
NTooltip,
useDialog,
useMessage,
NScrollbar,
} from 'naive-ui';
import { onMounted, reactive, ref, toRefs, watch, computed } from 'vue';
import axios from 'axios';
import { fetchDownloadImg } from '@/api';
import type { ResData } from '@/api/types';
import failImg from '@/assets/fail.png';
import drawSvg from '@/assets/icons/draw.svg';
import zoomSvg from '@/assets/icons/zoom.svg';
import { useAppStore, useAuthStore } from '@/store';
import { fetchDrawTaskAPI, fetchTranslateAPI } from '@/api/mjDraw';
import { SvgIcon } from '@/components/common';
import Loading from '@/components/base/Loading.vue';
interface Emits {
(e: 'usePrompt', val: any): void
(e: 'queryData'): void
(e: 'usePrompt', val: any): void;
(e: 'queryData'): void;
}
interface Props {
drawItemInfo: any
drawItemInfo: any;
}
const emit = defineEmits<Emits>()
const appStore = useAppStore()
const authStore = useAuthStore()
const theme = computed(() => appStore.theme)
const loadingTextColor = computed(() => theme.value === 'dark' ? '#fff' : '#000')
const props = defineProps<Props>()
const dialog = useDialog()
const ms = useMessage()
const downloadUrl = `${import.meta.env.VITE_GLOB_API_URL}/midjourney/download`
const refreshLoading = ref(false)
const emit = defineEmits<Emits>();
const appStore = useAppStore();
const authStore = useAuthStore();
const theme = computed(() => appStore.theme);
const loadingTextColor = computed(() =>
theme.value === 'dark' ? '#fff' : '#000'
);
const props = defineProps<Props>();
const dialog = useDialog();
const ms = useMessage();
const downloadUrl = `${import.meta.env.VITE_GLOB_API_URL}/midjourney/download`;
const refreshLoading = ref(false);
const statusType: any = computed(() => {
const { status } = props.drawItemInfo
if (status === 1)
return ''
if (status === 2)
return 'info'
if (status === 3)
return 'primary'
if (status === 4)
return 'error'
if (status === 5)
return 'error'
})
const { status } = props.drawItemInfo;
if (status === 1) return '';
if (status === 2) return 'info';
if (status === 3) return 'primary';
if (status === 4) return 'error';
if (status === 5) return 'error';
});
const statusMsg = computed(() => {
const { status } = props.drawItemInfo
if (status === 1)
return '等待中'
if (status === 2)
return '绘制中'
if (status === 3)
return '成功'
if (status === 4)
return '失败'
if (status === 5)
return '超时'
})
const { status } = props.drawItemInfo;
if (status === 1) return '等待中';
if (status === 2) return '绘制中';
if (status === 3) return '成功';
if (status === 4) return '失败';
if (status === 5) return '超时';
});
function usePrompt(){
emit('usePrompt')
function usePrompt() {
emit('usePrompt');
}
/* 下载图片 */
@ -72,21 +76,27 @@ async function handleDownloadImg(item: any) {
positiveText: '下载',
negativeText: '取消',
onPositiveClick: async () => {
d.loading = true
return new Promise(async (resolve) => {
const { fileInfo } = item
const { filename, cosUrl } = fileInfo
const response = await axios.post(downloadUrl, { url: cosUrl }, { responseType: 'blob' })
const blob = new Blob([response.data], { type: response.headers['content-type'] })
const urlObject = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = urlObject
link.download = filename
link.click()
resolve(true)
})
}
})
d.loading = true;
return new Promise(async (resolve) => {
const { fileInfo } = item;
const { filename, cosUrl } = fileInfo;
const response = await axios.post(
downloadUrl,
{ url: cosUrl },
{ responseType: 'blob' }
);
const blob = new Blob([response.data], {
type: response.headers['content-type'],
});
const urlObject = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = urlObject;
link.download = filename;
link.click();
resolve(true);
});
},
});
}
/* 删除图片 */
@ -97,375 +107,425 @@ async function handleDeleteDraw(item: any) {
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
const { id } = item
const res: ResData = await fetchDownloadImg({ id })
if (!res.success)
return ms.error(res.message)
ms.success('删除绘制记录成功!')
emit('queryData')
const { id } = item;
const res: ResData = await fetchDownloadImg({ id });
if (!res.success) return ms.error(res.message);
ms.success('删除绘制记录成功!');
emit('queryData');
},
})
});
}
/* 提交放大绘制任务 */
async function handleUpscale(item: any, orderId: number) {
const { id } = item
await fetchDrawTaskAPI({ drawId: id, action: 2, orderId })
ms.success('提交放大绘制任务成功、请等待绘制结束!')
if(authStore.token){
await refreshUserInfo()
}
emit('queryData')
const { drawId } = item;
await fetchDrawTaskAPI({ drawId: drawId, action: 'UPSCALE', orderId });
ms.success('提交放大绘制任务成功、请等待绘制结束!');
if (authStore.token) {
await refreshUserInfo();
}
emit('queryData');
}
/* 提交重新生成任务 */
async function handleReGenerate(item: any, orderId: number) {
const { id } = item
await fetchDrawTaskAPI({ drawId: id, action: 5, orderId })
ms.success('提交重新生成绘制任务成功、请等待绘制结束!')
if(authStore.token){
await refreshUserInfo()
}
emit('queryData')
const { drawId } = item;
await fetchDrawTaskAPI({ drawId: drawId, action: 'REGENERATE', orderId });
ms.success('提交重新生成绘制任务成功、请等待绘制结束!');
if (authStore.token) {
await refreshUserInfo();
}
emit('queryData');
}
/* 提交变体任务 */
async function handleVariation(item: any, orderId: number) {
const { id } = item
await fetchDrawTaskAPI({ drawId: id, action: 3, orderId })
ms.success('提交图片变换绘制任务成功、请等待绘制结束!')
if(authStore.token){
await refreshUserInfo()
}
emit('queryData')
const { drawId } = item;
await fetchDrawTaskAPI({ drawId: drawId, action: 'VARIATION', orderId });
ms.success('提交图片变换绘制任务成功、请等待绘制结束!');
if (authStore.token) {
await refreshUserInfo();
}
emit('queryData');
}
async function refreshUserInfo() {
refreshLoading.value = true
refreshLoading.value = true;
try {
await authStore.getUserInfo()
refreshLoading.value = false
}
catch (error) {
refreshLoading.value = false
await authStore.getUserInfo();
refreshLoading.value = false;
} catch (error) {
refreshLoading.value = false;
}
}
const calcTips = computed(() => {
const { progress, status } = props.drawItemInfo
if (status === 1)
return '正在排队中...'
if (status === 2 && !progress)
return '正在绘制中...'
if (status === 2 && progress === 100)
return '正在存储图片中...'
})
const { progress, status } = props.drawItemInfo;
if (status === 1) return '正在排队中...';
if (status === 2 && !progress) return '正在绘制中...';
if (status === 2 && progress === 100) return '正在存储图片中...';
});
/* 提交对单张图片调整任务 */
async function handleVary(item: any, orderId: number) {
const { id } = item
await fetchDrawTaskAPI({ drawId: id, action: 7, orderId })
ms.success('提交图片调整绘制任务成功、请等待绘制结束!')
if(authStore.token){
await refreshUserInfo()
}
emit('queryData')
const { drawId } = item;
await fetchDrawTaskAPI({ drawId: drawId, action: 'VARIATION', orderId });
ms.success('提交图片调整绘制任务成功、请等待绘制结束!');
if (authStore.token) {
await refreshUserInfo();
}
emit('queryData');
}
/* 提交对单张图片缩放任务 */
async function handleZoom(item: any, orderId: number) {
const { id } = item
await fetchDrawTaskAPI({ drawId: id, action: 6, orderId })
ms.success('提交图片调整绘制任务成功、请等待绘制结束!')
if(authStore.token){
await refreshUserInfo()
}
emit('queryData')
const { drawId } = item;
await fetchDrawTaskAPI({ drawId: drawId, action: 'UPSCALE', orderId });
ms.success('提交图片调整绘制任务成功、请等待绘制结束!');
if (authStore.token) {
await refreshUserInfo();
}
emit('queryData');
}
function handleRegion(file){}
function handleRegion(file) {}
</script>
<template>
<div class="relative overflow-hidden rounded-md border p-4 transition-all hover:shadow dark:border-neutral-700">
<div class="flex items-center justify-between">
<span>
<NTag size="small" :type="statusType">
{{ statusMsg }}
</NTag>
</span>
<div
class="relative overflow-hidden rounded-md border p-4 transition-all hover:shadow dark:border-neutral-700"
>
<div class="flex items-center justify-between">
<span>
<NTag size="small" :type="statusType">
{{ statusMsg }}
</NTag>
</span>
<NSpace>
<NTooltip v-if="drawItemInfo.isGroup" placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" ghost @click="usePrompt">
<template #icon>
<SvgIcon icon="ri:brush-line" class="text-base" />
</template>
使用
</NButton>
</template>
<div style="width: 240px">
<p>{{ drawItemInfo.fullPrompt }}</p>
</div>
</NTooltip>
<NSpace>
<NTooltip
v-if="drawItemInfo.action === 'IMAGINE'"
placement="top"
trigger="hover"
>
<template #trigger>
<NButton size="tiny" ghost @click="usePrompt">
<template #icon>
<SvgIcon icon="ri:brush-line" class="text-base" />
</template>
使用
</NButton>
</template>
<div style="width: 240px">
<p>{{ drawItemInfo.fullPrompt }}</p>
</div>
</NTooltip>
<NButton size="tiny" ghost @click="handleDownloadImg(drawItemInfo)">
<template #icon>
<SvgIcon icon="mingcute:file-download-line" class="text-base" />
</template>
下载
</NButton>
<NButton size="tiny" ghost @click="handleDeleteDraw(drawItemInfo)">
<template #icon>
<SvgIcon icon="ri:delete-bin-line" class="text-base" />
</template>
删除
</NButton>
</NSpace>
</div>
<!-- content -->
<div class="my-4 h-[280px]">
<div v-if="drawItemInfo.status === 3" class="flex h-full w-full items-center justify-center overflow-hidden rounded-md">
<NImage
style="object-fit: contain;"
:src="drawItemInfo.fileInfo.thumbImg"
:preview-src="drawItemInfo.fileInfo.cosUrl"
object-fit="contain"
/>
</div>
<div v-if="[4, 5, 6].includes(drawItemInfo.status)" class="flex flex-col h-full w-full items-center justify-center overflow-hidden rounded-md">
<img class="w-[75px]" :src="failImg">
<span class="mt-3 text-base">绘制失败</span>
<span class="mt-1">已退还余额至您的账户</span>
</div>
<div v-if="[1, 2].includes(drawItemInfo.status)" class="my-4 h-[280px] relative">
<Loading :text-color="loadingTextColor" :progress="drawItemInfo.progress" :tips="calcTips" />
</div>
</div>
<!-- footer -->
<div class="-mx-4 -mb-4 bg-[#fafafc] px-4 py-2 dark:bg-[#262629]">
<div v-if="drawItemInfo.isGroup" class="w-full">
<div class="mb-2 flex items-center justify-between">
<span>放大</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 240px">
<p>参数释义放大某张图片如 U1 放大第一张图片以此类推</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center justify-around">
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 1)">
U1
</NButton>
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 2)">
U2
</NButton>
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 3)">
U3
</NButton>
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 4)">
U4
</NButton>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleReGenerate(drawItemInfo, 5)">
<SvgIcon icon="solar:refresh-outline" class="text-base" />
</NButton>
</template>
<p>重新生成一次</p>
</NTooltip>
</div>
</div>
</div>
</div>
<NButton size="tiny" ghost @click="handleDownloadImg(drawItemInfo)">
<template #icon>
<SvgIcon icon="mingcute:file-download-line" class="text-base" />
</template>
下载
</NButton>
<NButton size="tiny" ghost @click="handleDeleteDraw(drawItemInfo)">
<template #icon>
<SvgIcon icon="ri:delete-bin-line" class="text-base" />
</template>
删除
</NButton>
</NSpace>
</div>
<!-- content -->
<div class="my-4 h-[280px]">
<div
v-if="drawItemInfo.status === 3"
class="flex h-full w-full items-center justify-center overflow-hidden rounded-md"
>
<NImage
style="object-fit: contain"
:src="drawItemInfo.drawUrl"
:preview-src="drawItemInfo.drawUrl"
object-fit="contain"
/>
</div>
<div
v-if="[4, 5, 6].includes(drawItemInfo.status)"
class="flex flex-col h-full w-full items-center justify-center overflow-hidden rounded-md"
>
<img class="w-[75px]" :src="failImg" />
<span class="mt-3 text-base">绘制失败</span>
<span class="mt-1">已退还余额至您的账户</span>
</div>
<div
v-if="[1, 2].includes(drawItemInfo.status)"
class="my-4 h-[280px] relative"
>
<Loading
:text-color="loadingTextColor"
:progress="drawItemInfo.progress"
:tips="calcTips"
/>
</div>
</div>
<!-- footer -->
<div class="-mx-4 -mb-4 bg-[#fafafc] px-4 py-2 dark:bg-[#262629]">
<div
v-if="
(drawItemInfo.action === 'IMAGINE' ||
drawItemInfo.action === 'VARIATION' ||
drawItemInfo.action === 'ZOOM' ||
drawItemInfo.action === 'OUTPAINT' ||
drawItemInfo.action === 'REROLL') &&
drawItemInfo.status === 3
"
class="w-full"
>
<div class="mb-2 flex items-center justify-between">
<span>放大</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 240px">
<p>参数释义放大某张图片如 U1 放大第一张图片以此类推</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center justify-around">
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 1)">
U1
</NButton>
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 2)">
U2
</NButton>
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 3)">
U3
</NButton>
<NButton size="tiny" @click="handleUpscale(drawItemInfo, 4)">
U4
</NButton>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton
size="tiny"
@click="handleReGenerate(drawItemInfo, 5)"
>
<SvgIcon icon="solar:refresh-outline" class="text-base" />
</NButton>
</template>
<p>重新生成一次</p>
</NTooltip>
</div>
</div>
</div>
</div>
<!-- 套图 新生成 变体图 重新生成 三种类型 -->
<div v-if="drawItemInfo.isGroup" class="w-full">
<div class="mb-2 flex items-center justify-between">
<span>变换</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 240px">
<p>参数释义以某张图片为基准重新生成
V1 则变换第一张图片以此类推</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center justify-around">
<NButton size="tiny" @click="handleVariation(drawItemInfo, 1)">
V1
</NButton>
<NButton size="tiny" @click="handleVariation(drawItemInfo, 2)">
V2
</NButton>
<NButton size="tiny" @click="handleVariation(drawItemInfo, 3)">
V3
</NButton>
<NButton size="tiny" @click="handleVariation(drawItemInfo, 4)">
V4
</NButton>
<NButton size="tiny" style="opacity:0">
V5
</NButton>
</div>
</div>
</div>
</div>
<!-- 套图 新生成 变体图 重新生成 三种类型 -->
<div
v-if="
(drawItemInfo.action === 'IMAGINE' ||
drawItemInfo.action === 'VARIATION' ||
drawItemInfo.action === 'ZOOM' ||
drawItemInfo.action === 'OUTPAINT' ||
drawItemInfo.action === 'REROLL') &&
drawItemInfo.status === 3
"
class="w-full"
>
<div class="mb-2 flex items-center justify-between">
<span>变换</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 240px">
<p>
参数释义以某张图片为基准重新生成 V1
则变换第一张图片以此类推
</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center justify-around">
<NButton size="tiny" @click="handleVariation(drawItemInfo, 1)">
V1
</NButton>
<NButton size="tiny" @click="handleVariation(drawItemInfo, 2)">
V2
</NButton>
<NButton size="tiny" @click="handleVariation(drawItemInfo, 3)">
V3
</NButton>
<NButton size="tiny" @click="handleVariation(drawItemInfo, 4)">
V4
</NButton>
<NButton size="tiny" style="opacity: 0"> V5 </NButton>
</div>
</div>
</div>
</div>
<!-- 对老图片增强 单张图或生成中的图 -->
<div v-if="!drawItemInfo.isGroup && drawItemInfo.orderId" class="w-full mb-2 flex items-center justify-between">
<!-- 图片放大或变体 并且图片还未生成成功的时候没有message_id -->
<div v-if="drawItemInfo.orderId !== 5 && !drawItemInfo.extend">
<span v-if="drawItemInfo.action === 2">
操作{{ `选中套图第${drawItemInfo.orderId || 'x'}张图片进行放大` }}
</span>
<span v-if="drawItemInfo.action === 3">
操作{{ `选中套图第${drawItemInfo.orderId || 'x'}张图片进行变换` }}
</span>
</div>
<!-- 已经生成成功的单张图 可以zoom和vary -->
<div v-if="drawItemInfo.orderId !== 5 && drawItemInfo.extend" class="flex w-full">
<div class="mb-2 flex flex-1 items-center justify-between">
<span>调整</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 275px">
<p>参数释义Vary 以当前图片为基础调整图片</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center pl-2">
<NSpace>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleVary(drawItemInfo, 1)">
<template #icon>
<img :src="drawSvg" class="w-4" alt="">
</template>
V(Strong)
</NButton>
</template>
<p>以当前图片为基础大幅增强</p>
</NTooltip>
<!-- 对老图片增强 单张图或生成中的图 -->
<div
v-if="drawItemInfo.progress !== 100 && drawItemInfo.status !== 3"
class="w-full mb-2 flex items-center justify-between"
>
<!-- 图片放大或变体 并且图片还未生成成功的时候没有message_id -->
<div v-if="drawItemInfo.orderId !== 5">
<span v-if="drawItemInfo.action === 'UPSCALE'">
操作{{ `选中套图第${drawItemInfo.orderId || 'x'}张图片进行放大` }}
</span>
<span v-if="drawItemInfo.action === 'VARIATION'">
操作{{ `选中套图第${drawItemInfo.orderId || 'x'}张图片进行变换` }}
</span>
</div>
<!-- 已经生成成功的单张图 可以zoom和vary -->
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleVary(drawItemInfo, 2)">
<template #icon>
<img :src="drawSvg" class="w-4" alt="">
</template>
V(Subtle)
</NButton>
</template>
<p>以当前图片为基础细微调整</p>
</NTooltip>
</NSpace>
</div>
</div>
</div>
</div>
<!-- 重新绘制套图只在生成中显示 生成完毕即会进入group套图 -->
<span v-if="drawItemInfo.orderId === 5">
操作正在对图片重新生成一次
</span>
</div>
<!-- 重新绘制套图只在生成中显示 生成完毕即会进入group套图 -->
<span v-if="drawItemInfo.orderId === 5">
操作正在对图片重新生成一次
</span>
</div>
<!-- 新图绘制中 -->
<div
v-if="
drawItemInfo.action === 'IMAGINE' &&
!drawItemInfo.orderId &&
drawItemInfo.status === 'UPSCALE'
"
class="w-full mb-2 flex items-center justify-between"
>
操作正在火速绘制中...
</div>
<!-- 新图绘制中 -->
<div v-if="!drawItemInfo.isGroup && !drawItemInfo.orderId && drawItemInfo.status === 2" class="w-full mb-2 flex items-center justify-between">
操作正在火速绘制中...
</div>
<!-- 绘制失败了 -->
<div
v-if="!drawItemInfo.orderId && [4, 5, 6].includes(drawItemInfo.status)"
class="w-full mb-2 flex items-center justify-between"
>
执行 换个提示词重新试试吧
</div>
<!-- 加载失败 -->
<div
v-if="!drawItemInfo.action && !drawItemInfo.extend"
class="w-full mb-2 flex items-center justify-between"
>
上级 {{ drawItemInfo.message_id || '正在加载中...' }}
</div>
<!-- 绘制失败了 -->
<div v-if="!drawItemInfo.isGroup && !drawItemInfo.orderId && [4, 5, 6].includes(drawItemInfo.status) " class="w-full mb-2 flex items-center justify-between">
执行 换个提示词重新试试吧
</div>
<!-- 加载失败 -->
<div v-if="!drawItemInfo.isGroup && !drawItemInfo.extend" class="w-full mb-2 flex items-center justify-between">
上级 {{ drawItemInfo.message_id || '正在加载中...' }}
</div>
<!-- 2 -->
<div
v-if="
(drawItemInfo.action === 'UPSCALE' ||
drawItemInfo.action === 'ACTION') &&
drawItemInfo.status === 3
"
>
<div class="mb-2 flex flex-1 items-center justify-between">
<span>缩放</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 270px">
<p>参数释义Zoom 对当前图片进行无限缩放</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center pl-2">
<NSpace>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleZoom(drawItemInfo, 1)">
<template #icon>
<img :src="zoomSvg" class="w-4" alt="" />
</template>
U(Subtle)
</NButton>
</template>
<p>放大</p>
</NTooltip>
<!-- -->
<div v-if="!drawItemInfo.isGroup && drawItemInfo.orderId !== 5 && drawItemInfo.extend">
<div class="mb-2 flex flex-1 items-center justify-between">
<span>缩放</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 270px">
<p>参数释义Zoom 对当前图片进行无限缩放</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center pl-2">
<NSpace>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleZoom(drawItemInfo, 1)">
<template #icon>
<img :src="zoomSvg" class="w-4" alt="">
</template>
Zoom 2
</NButton>
</template>
<p>缩放2倍</p>
</NTooltip>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleZoom(drawItemInfo, 2)">
<template #icon>
<img :src="zoomSvg" class="w-4" alt="" />
</template>
U(Creative)
</NButton>
</template>
<p>放大</p>
</NTooltip>
</NSpace>
</div>
</div>
</div>
</div>
<div
v-if="
(drawItemInfo.action === 'UPSCALE' ||
drawItemInfo.action === 'ACTION') &&
drawItemInfo.status === 3
"
class="flex w-full"
>
<div class="mb-2 flex flex-1 items-center justify-between">
<span>调整</span>
<span class="text-base text-neutral-400">
<NTooltip placement="top" trigger="hover">
<template #trigger>
<SvgIcon icon="ri:error-warning-line" class="text-base" />
</template>
<div style="width: 275px">
<p>参数释义Vary 以当前图片为基础调整图片</p>
</div>
</NTooltip>
</span>
<div class="flex-1">
<div class="flex items-center pl-2">
<NSpace>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleVary(drawItemInfo, 1)">
<template #icon>
<img :src="drawSvg" class="w-4" alt="" />
</template>
V(Strong)
</NButton>
</template>
<p>以当前图片为基础大幅增强</p>
</NTooltip>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleZoom(drawItemInfo, 2)">
<template #icon>
<img :src="zoomSvg" class="w-4" alt="">
</template>
Zoom 1.5
</NButton>
</template>
<p>缩放1.5</p>
</NTooltip>
<NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleVary(drawItemInfo, 2)">
<template #icon>
<img :src="drawSvg" class="w-4" alt="" />
</template>
V(Subtle)
</NButton>
</template>
<p>以当前图片为基础细微调整</p>
</NTooltip>
</NSpace>
</div>
</div>
</div>
</div>
<!-- <NTooltip placement="top" trigger="hover">
<template #trigger>
<NButton size="tiny" @click="handleRegion(drawItemInfo)">
<template #icon>
<img :src="zoomSvg" class="w-4" alt="">
</template>
Region
</NButton>
</template>
<p>缩放2倍</p>
</NTooltip> -->
</NSpace>
</div>
</div>
</div>
</div>
<div class="w-full flex">
<span class="text-[#64748b]">时间{{ drawItemInfo.createdAt }}</span>
</div>
</div>
</div>
<!-- <div class="w-full flex">
<span class="text-[#64748b]">时间{{ drawItemInfo.createdAt }}</span>
</div> -->
</div>
</div>
</template>
<style lang='scss' scoped>
</style>
<style lang="scss" scoped></style>

File diff suppressed because one or more lines are too long

View File

@ -59,7 +59,7 @@ const demoData = `
`
const prompt = ref('')
const initValue = `# NineAi
const initValue = `# YiAi
## 基础功能
- 支持AI聊天
- 支持GPT4

View File

@ -10,8 +10,8 @@ function setupPlugins(env: ImportMetaEnv): PluginOption[] {
env.VITE_GLOB_APP_PWA === 'true' && VitePWA({
injectRegister: 'auto',
manifest: {
name: 'Nine AI',
short_name: 'Nine AI',
name: 'YI AI',
short_name: 'YI AI',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },

View File

@ -1,6 +1,6 @@
{
"name": "nine-ai",
"version": "2.4.5",
"name": "yi-ai",
"version": "2.5.0",
"description": "使用 Nestjs 和 Vue3 搭建的 AIGC 生态社区 持续集成AI能力到社区之中",
"main": "index.js",
"author": "longyanjiang",
@ -39,11 +39,8 @@
"devDependencies": {},
"repository": {
"type": "git",
"url": "git+https://github.com/longyanjiang/Nine-Ai.git"
"url": ""
},
"license": "MID",
"bugs": {
"url": "https://github.com/longyanjiang/Nine-Ai/issues"
},
"homepage": "https://github.com/longyanjiang/Nine-Ai#readme"
"homepage": ""
}

File diff suppressed because it is too large Load Diff

BIN
service/.DS_Store vendored

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "service",
"version": "2.4.5",
"version": "2.5.0",
"description": "",
"author": "",
"private": true,
@ -49,7 +49,7 @@
"body-parser": "^1.20.2",
"bull": "^4.10.4",
"cache-manager-redis-store": "^3.0.1",
"chatgpt-nine-ai": "^1.0.2",
"chatgpt-ai-web": "^1.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"compression": "^1.7.4",
@ -64,6 +64,7 @@
"form-data": "^4.0.0",
"guid-typescript": "^1.0.9",
"hbs": "^4.2.0",
"image-size": "^1.1.1",
"ioredis": "^5.3.2",
"isomorphic-fetch": "^3.0.0",
"javascript-obfuscator": "^4.0.2",

View File

@ -1,6 +1,6 @@
{
"apps": {
"name": "nineai-v2.4.5",
"name": "yiai-v2.5.0",
"script": "./dist/main.js",
"watch": true,
"ignore_watch": [

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Nine Ai</title>
<title>Yi Ai</title>
<style>
.loading-container {
position: fixed;
@ -45,6 +45,13 @@
<div class="loading"></div>
</div>
<h1>Welcome Use Nine Ai</h1>
<h1>Welcome Use Yi Ai</h1>
</body>
<script>
console.log(
"%c本项目作者----小易联系QQ805239273",
"background-color:rgb(30,30,30);border-radius:4px;font-size:12px;padding:4px;color:rgb(220,208,129);"
)
</script>
</html>

BIN
service/src/.DS_Store vendored

Binary file not shown.

View File

@ -2,8 +2,8 @@ import { UploadService } from './../upload/upload.service';
import { UserService } from './../user/user.service';
import { ConfigService } from 'nestjs-config';
import { HttpException, HttpStatus, Injectable, OnModuleInit, Logger } from '@nestjs/common';
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt-nine-ai';
import { Request, Response } from 'express';
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt-ai-web';
import e, { Request, Response } from 'express';
import { OpenAiErrorCodeMessage } from '@/common/constants/errorMessage.constant';
import {
compileNetwork,
@ -97,7 +97,7 @@ export class ChatgptService implements OnModuleInit {
};
async onModuleInit() {
let chatgpt = await importDynamic('chatgpt-nine-ai');
let chatgpt = await importDynamic('chatgpt-ai-web');
let KeyvRedis = await importDynamic('@keyv/redis');
let Keyv = await importDynamic('keyv');
chatgpt = chatgpt?.default ? chatgpt.default : chatgpt;
@ -165,7 +165,7 @@ export class ChatgptService implements OnModuleInit {
/* 不同场景会变更其信息 */
let setSystemMessage = systemMessage;
const { parentMessageId } = options;
const { prompt ,imageUrl,model:activeModel} = body;
const { prompt, imageUrl, model: activeModel } = body;
const { groupId, usingNetwork } = options;
// const { model = 3 } = options;
/* 获取当前对话组的详细配置信息 */
@ -184,7 +184,7 @@ export class ChatgptService implements OnModuleInit {
throw new HttpException('当前流程所需要的模型已被管理员下架、请联系管理员上架专属模型!', HttpStatus.BAD_REQUEST);
}
const { deduct, isTokenBased, deductType, key: modelKey, secret, modelName, id: keyId, accessToken } = currentRequestModelKey;
const { deduct, isTokenBased, tokenFeeRatio, deductType, key: modelKey, secret, modelName, id: keyId, accessToken } = currentRequestModelKey;
/* 用户状态检测 */
await this.userService.checkUserStatus(req.user);
/* 用户余额检测 */
@ -260,7 +260,7 @@ export class ChatgptService implements OnModuleInit {
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt,
imageUrl,
imageUrl:response?.imageUrl,
activeModel,
answer: '',
promptTokens: prompt_tokens,
@ -307,7 +307,7 @@ export class ChatgptService implements OnModuleInit {
/* 当用户回答一般停止时 也需要扣费 */
let charge = deduct;
if (isTokenBased === true) {
charge = deduct * total_tokens;
charge = Math.ceil((deduct * total_tokens) / tokenFeeRatio);
}
await this.userBalanceService.deductFromBalance(req.user.id, `model${deductType === 1 ? 3 : 4}`, charge, total_tokens);
});
@ -320,11 +320,11 @@ export class ChatgptService implements OnModuleInit {
const { context: messagesHistory } = await this.nineStore.buildMessageFromParentMessageId(usingNetwork ? netWorkPrompt : prompt, {
parentMessageId,
systemMessage,
imageUrl,
activeModel,
maxModelToken: maxToken,
maxResponseTokens: maxTokenRes,
maxRounds: addOneIfOdd(rounds),
imageUrl,
activeModel,
});
let firstChunk = true;
response = await sendMessageFromOpenAi(messagesHistory, {
@ -332,8 +332,9 @@ export class ChatgptService implements OnModuleInit {
maxTokenRes,
apiKey: modelKey,
model,
imageUrl,
prompt,
activeModel,
imageUrl,
temperature,
proxyUrl: proxyResUrl,
onProgress: (chat) => {
@ -341,7 +342,7 @@ export class ChatgptService implements OnModuleInit {
lastChat = chat;
firstChunk = false;
},
});
},this.uploadService);
isSuccess = true;
}
@ -385,7 +386,6 @@ export class ChatgptService implements OnModuleInit {
isSuccess = true;
}
/* 分别将本次用户输入的 和 机器人返回的分两次存入到 store */
const userMessageData: MessageInfo = {
id: this.nineStore.getUuid(),
text: prompt,
@ -407,7 +407,8 @@ export class ChatgptService implements OnModuleInit {
text: response.text,
role: 'assistant',
name: undefined,
usage: response.usage,
usage: response?.usage,
imageUrl: response?.imageUrl,
parentMessageId: userMessageData.id,
conversationId: response?.conversationId,
};
@ -415,7 +416,6 @@ export class ChatgptService implements OnModuleInit {
await this.nineStore.setData(assistantMessageData);
othersInfo = { model, parentMessageId: userMessageData.id };
/* 回答完毕 */
} else {
const { key, maxToken, maxTokenRes, proxyResUrl } = await this.formatModelToken(currentRequestModelKey);
const { parentMessageId, completionParams, systemMessage } = mergedOptions;
@ -431,17 +431,22 @@ export class ChatgptService implements OnModuleInit {
temperature,
proxyUrl: proxyResUrl,
onProgress: null,
prompt,
});
}
/* 统一最终输出格式 */
const formatResponse = await unifiedFormattingResponse(keyType, response, othersInfo);
const { prompt_tokens = 0, completion_tokens = 0, total_tokens = 0 } = formatResponse.usage;
let usage = null;
let formatResponse = null;
if (model.includes('dall')) {
usage = response.detail?.usage || { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 };
} else {
formatResponse = await unifiedFormattingResponse(keyType, response, othersInfo);
}
const { prompt_tokens, completion_tokens, total_tokens } = model.includes('dall') ? usage : formatResponse.usage;
/* 区分扣除普通还是高级余额 model3: 普通余额 model4 高级余额 */
let charge = deduct;
if (isTokenBased === true) {
charge = deduct * total_tokens;
charge = Math.ceil((deduct * total_tokens) / tokenFeeRatio);
}
await this.userBalanceService.deductFromBalance(req.user.id, `model${deductType === 1 ? 3 : 4}`, charge, total_tokens);
@ -457,13 +462,13 @@ export class ChatgptService implements OnModuleInit {
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt,
imageUrl,
imageUrl: response?.imageUrl,
activeModel,
answer: '',
promptTokens: prompt_tokens,
completionTokens: 0,
totalTokens: total_tokens,
model: formatResponse.model,
model: model,
role: 'user',
groupId,
requestOptions: JSON.stringify({
@ -479,7 +484,8 @@ export class ChatgptService implements OnModuleInit {
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt: prompt,
answer: formatResponse?.text,
imageUrl: response?.imageUrl,
answer: response.text,
promptTokens: prompt_tokens,
completionTokens: completion_tokens,
totalTokens: total_tokens,
@ -501,7 +507,7 @@ export class ChatgptService implements OnModuleInit {
}),
});
Logger.debug(
`本次调用: ${req.user.id} model: ${model} key -> ${key}, 模型名称: ${modelName}, 最大回复token: ${maxResponseTokens}`,
`户ID: ${req.user.id} 模型名称: ${modelName}-${activeModel}, 消耗token: ${total_tokens}, 消耗积分: ${charge}`,
'ChatgptService',
);
const userBalance = await this.userBalanceService.queryUserBalance(req.user.id);
@ -599,7 +605,7 @@ export class ChatgptService implements OnModuleInit {
await this.userBalanceService.validateBalance(req, 'mjDraw', money);
let images = [];
/* 从3的卡池随机拿一个key */
const detailKeyInfo = await this.modelsService.getRandomDrawKey();
const detailKeyInfo = await this.modelsService.getCurrentModelKeyInfo('dall-e-3');
const keyId = detailKeyInfo?.id;
const { key, proxyResUrl } = await this.formatModelToken(detailKeyInfo);
Logger.log(`draw paompt info <==**==> ${body.prompt}, key ===> ${key}`, 'DrawService');

View File

@ -1,6 +1,9 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { get_encoding } from '@dqbd/tiktoken'
import { removeSpecialCharacters } from '@/common/utils';
import { ConsoleLogger, HttpException, HttpStatus, Logger } from '@nestjs/common';
import * as uuid from 'uuid';
const tokenizer = get_encoding('cl100k_base')
@ -11,96 +14,166 @@ interface SendMessageResult {
detail?: any;
}
function getFullUrl(proxyUrl){
function getFullUrl(proxyUrl) {
const processedUrl = proxyUrl.endsWith('/') ? proxyUrl.slice(0, -1) : proxyUrl;
const baseUrl = processedUrl || 'https://api.openai.com'
return `${baseUrl}/v1/chat/completions`
}
export function sendMessageFromOpenAi(messagesHistory, inputs ){
const { onProgress, maxToken, apiKey, model, temperature = 0.95, proxyUrl } = inputs
console.log('current request options: ',apiKey, model, maxToken, proxyUrl );
const max_tokens = compilerToken(model, maxToken)
const options: AxiosRequestConfig = {
method: 'POST',
url: getFullUrl(proxyUrl),
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${removeSpecialCharacters(apiKey)}`,
},
data: {
max_tokens,
stream: true,
temperature,
model,
messages: messagesHistory
},
};
const prompt = messagesHistory[messagesHistory.length-1]?.content
return new Promise(async (resolve, reject) =>{
export async function sendMessageFromOpenAi(messagesHistory, inputs, uploadService?) {
const { onProgress, maxToken, apiKey, model, temperature = 0.8, proxyUrl, prompt } = inputs
if (model.includes('dall')) {
let result: any = { text: '', imageUrl: '' };
try {
const options: AxiosRequestConfig = {
method: 'POST',
url: `${proxyUrl}/v1/images/generations`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
data: {
prompt: prompt,
model: model,
response_format: 'b64_json'
},
}
const response: any = await axios(options);
const stream = response.data;
let result: any = { text: '' };
stream.on('data', (chunk) => {
const splitArr = chunk.toString().split('\n\n').filter((line) => line.trim() !== '');
for (const line of splitArr) {
const data = line.replace('data:', '');
let ISEND = false;
try {
ISEND = JSON.parse(data).choices[0].finish_reason === 'stop';
} catch (error) {
ISEND = false;
}
/* 如果结束 返回所有 */
if (data === '[DONE]' || ISEND) {
result.text = result.text.trim();
return result;
}
try {
const parsedData = JSON.parse(data);
if (parsedData.id) {
result.id = parsedData.id;
}
if (parsedData.choices?.length) {
const delta = parsedData.choices[0].delta;
result.delta = delta.content;
if (delta?.content) result.text += delta.content;
if (delta.role) {
result.role = delta.role;
}
result.detail = parsedData;
}
onProgress && onProgress({text:result.text})
} catch (error) {
console.log('parse Error', data )
}
}
});
stream.on('end', () => {
// 手动计算token
if(result.detail && result.text){
const promptTokens = getTokenCount(prompt)
const completionTokens = getTokenCount(result.text)
result.detail.usage = {
prompt_tokens: promptTokens,
completion_tokens: completionTokens ,
total_tokens: promptTokens + completionTokens,
estimated: true
}
}
return resolve(result);
});
const { b64_json, revised_prompt } = response.data.data[0]
const buffer = Buffer.from(b64_json, 'base64');
let imgUrl = '';
try {
const filename = uuid.v4().slice(0, 10) + '.png';
Logger.debug(`------> 开始上传图片!!!`, 'MidjourneyService');
const buffer = Buffer.from(b64_json, 'base64');
// imgUrl = await uploadService.uploadFileFromUrl({ filename, url })
imgUrl = await uploadService.uploadFile({ filename, buffer });
Logger.debug(`图片上传成功URL: ${imgUrl}`, 'MidjourneyService');
} catch (error) {
Logger.error(`上传图片过程中出现错误: ${error}`, 'MidjourneyService');
}
result.imageUrl = imgUrl
result.text = revised_prompt;
onProgress && onProgress({ text: result.text })
return result;
} catch (error) {
reject(error)
const status = error?.response?.status || 500;
console.log('openai-draw error: ', JSON.stringify(error), status);
const message = error?.response?.data?.error?.message;
if (status === 429) {
result.text = '当前请求已过载、请稍等会儿再试试吧!';
return result;
}
if (status === 400 && message.includes('This request has been blocked by our content filters')) {
result.text = '您的请求已被系统拒绝。您的提示可能存在一些非法的文本。';
return result;
}
if (status === 400 && message.includes('Billing hard limit has been reached')) {
result.text = '当前模型key已被封禁、已冻结当前调用Key、尝试重新对话试试吧';
return result;
}
if (status === 500) {
result.text = '绘制图片失败,请检查你的提示词是否有非法描述!';
return result;
}
if (status === 401) {
result.text = '绘制图片失败,此次绘画被拒绝了!';
return result;
}
result.text = '绘制图片失败,请稍后试试吧!';
return result;
}
})
} else {
let result: any = { text: '' };
const options: AxiosRequestConfig = {
method: 'POST',
url: getFullUrl(proxyUrl),
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
data: {
stream: true,
temperature,
model,
messages: messagesHistory,
},
};
if (model === 'gpt-4-vision-preview') {
options.data.max_tokens = 2048;
}
return new Promise(async (resolve, reject) => {
try {
const response: any = await axios(options);
const stream = response.data;
stream.on('data', (chunk) => {
const splitArr = chunk.toString().split('\n\n').filter((line) => line.trim() !== '');
for (const line of splitArr) {
const data = line.replace('data:', '');
let ISEND = false;
try {
ISEND = JSON.parse(data).choices[0].finish_reason === 'stop';
} catch (error) {
ISEND = false;
}
/* 如果结束 返回所有 */
if (ISEND) {
result.text = result.text.trim();
return result;
}
try {
if (data !== " [DONE]" && data !== "[DONE]" && data != "[DONE] ") {
const parsedData = JSON.parse(data);
if (parsedData.id) {
result.id = parsedData.id;
}
if (parsedData.choices?.length) {
const delta = parsedData.choices[0].delta;
result.delta = delta.content;
if (delta?.content) result.text += delta.content;
if (delta.role) {
result.role = delta.role;
}
result.detail = parsedData;
}
onProgress && onProgress({ text: result.text })
}
} catch (error) {
console.log('parse Error', data)
}
}
});
let totalText = '';
messagesHistory.forEach(message => {
totalText += message.content + ' ';
});
stream.on('end', () => {
// 手动计算token
if (result.detail && result.text) {
const promptTokens = getTokenCount(totalText)
const completionTokens = getTokenCount(result.text)
result.detail.usage = {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
estimated: true
}
}
return resolve(result);
});
} catch (error) {
reject(error)
}
})
}
}
export function getTokenCount(text: string) {
if (!text) return 0;
// 确保text是字符串类型
@ -111,28 +184,4 @@ export function getTokenCount(text: string) {
return tokenizer.encode(text).length
}
function compilerToken(model, maxToken){
let max = 0
/* 3.5 */
if(model.includes(3.5)){
max = maxToken > 4096 ? 4096 : maxToken
}
/* 4.0 */
if(model.includes('gpt-4')){
max = maxToken > 8192 ? 8192 : maxToken
}
/* 4.0 preview */
if(model.includes('preview')){
max = maxToken > 4096 ? 4096 : maxToken
}
/* 4.0 32k */
if(model.includes('32k')){
max = maxToken > 32768 ? 32768 : maxToken
}
return max
}

View File

@ -2,6 +2,7 @@ import Keyv from 'keyv';
import { v4 as uuidv4 } from 'uuid';
import { get_encoding } from '@dqbd/tiktoken';
import { Logger } from '@nestjs/common';
import { includes } from 'lodash';
const tokenizer = get_encoding('cl100k_base');
@ -96,7 +97,7 @@ export class NineStore implements NineStoreInterface {
let nextNumTokensEstimate = 0;
// messages.push({ role: 'system', content: systemMessage, name })
if (systemMessage) {
const specialModels = ['gemini-pro', 'ERNIE', 'qwen', 'SparkDesk', 'hunyuan'];
const specialModels = ['gemini-pro', 'ERNIE','hunyuan'];
const isSpecialModel = activeModel && specialModels.some((specialModel) => activeModel.includes(specialModel));
if (isSpecialModel) {
messages.push({ role: 'user', content: systemMessage, name });
@ -146,7 +147,7 @@ export class NineStore implements NineStoreInterface {
let content = text; // 默认情况下使用text作为content
// 特别处理包含 imageUrl 的消息
if (role === 'user' && imageUrl) {
if (imageUrl) {
if (activeModel === 'gpt-4-vision-preview') {
content = [
{ type: 'text', text: text },

View File

@ -13,7 +13,7 @@ interface UserInfo {
@Injectable()
export class DatabaseService implements OnModuleInit {
constructor(private connection: Connection) {}
constructor(private connection: Connection) { }
async onModuleInit() {
await this.checkSuperAdmin();
await this.checkSiteBaseConfig();
@ -23,7 +23,7 @@ export class DatabaseService implements OnModuleInit {
async checkSuperAdmin() {
const user = await this.connection.query(`SELECT * FROM users WHERE role = 'super'`);
if (!user || user.length === 0) {
const superPassword = bcrypt.hashSync('123456', 10);
const superPassword = bcrypt.hashSync('123456', 10); //初始密码
const adminPassword = bcrypt.hashSync('123456', 10);
const superEmail = 'default@cooper.com';
const adminEmail = 'defaultAdmin@cooper.com';
@ -44,7 +44,7 @@ export class DatabaseService implements OnModuleInit {
const userId = user.insertId;
const balance = await this.connection.query(`INSERT INTO balance (userId, balance, usesLeft, paintCount) VALUES ('${userId}', 0, 1000, 100)`);
Logger.log(
`初始化创建${role}用户成功、用户名为[${username}]、初始密码为[${username === 'super' ? '123456' : '123456'}] ==============> 请注意查阅`,
`初始化创建${role}用户成功、用户名为[${username}]、初始密码为[${username === 'super' ? 'nine-super' : '123456'}] ==============> 请注意查阅`,
'DatabaseService',
);
} catch (error) {
@ -68,17 +68,7 @@ export class DatabaseService implements OnModuleInit {
/* 创建基础的网站数据 */
async createBaseSiteConfig() {
try {
const code = `
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?cb8c9a3bcadbc200e950b05f9c61a385";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
`;
const code = ``;
const noticeInfo = `
#### YiAi
@ -88,21 +78,21 @@ export class DatabaseService implements OnModuleInit {
`;
const defaultConfig = [
{ configKey: 'siteName', configVal: 'Nine Ai', public: 1, encry: 0 },
{ configKey: 'qqNumber', configVal: '840814166', public: 1, encry: 0 },
{ configKey: 'vxNumber', configVal: 'wangpanzhu321', public: 1, encry: 0 },
{ configKey: 'siteName', configVal: 'Yi Ai', public: 1, encry: 0 },
{ configKey: 'qqNumber', configVal: '805239273', public: 1, encry: 0 },
{ configKey: 'vxNumber', configVal: 'HelloWordYi819', public: 1, encry: 0 },
{ configKey: 'robotAvatar', configVal: '', public: 1, encry: 0 },
{
configKey: 'userDefautlAvatar',
configVal: 'https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1682571295452image.png',
configVal: '',
public: 0,
encry: 0,
},
{ configKey: 'baiduCode', configVal: code, public: 1, encry: 0 },
{ configKey: 'baiduSiteId', configVal: '19024441', public: 0, encry: 0 },
{ configKey: 'baiduSiteId', configVal: '', public: 0, encry: 0 },
{
configKey: 'baiduToken',
configVal: '121.a1600b9b60910feea2ef627ea9776a6f.YGP_CWCOA2lNcIGJ27BwXGxa6nZhBQyLUS4XVaD.TWt9TA',
configVal: '',
public: 0,
encry: 0,
},
@ -110,25 +100,25 @@ export class DatabaseService implements OnModuleInit {
{ configKey: 'openaiBaseUrl', configVal: 'https://api.openai.com', public: 0, encry: 0 },
{ configKey: 'noticeInfo', configVal: noticeInfo, public: 1, encry: 0 },
{ configKey: 'registerVerifyEmailTitle', configVal: 'NineTeam团队账号验证', public: 0, encry: 0 },
{ configKey: 'registerVerifyEmailTitle', configVal: 'Yi Ai团队账号验证', public: 0, encry: 0 },
{
configKey: 'registerVerifyEmailDesc',
configVal: '欢迎使用Nine Team团队的产品服务,请在五分钟内完成你的账号激活,点击以下按钮激活您的账号,',
configVal: '欢迎使用Yi Ai团队的产品服务,请在五分钟内完成你的账号激活,点击以下按钮激活您的账号,',
public: 0,
encry: 0,
},
{ configKey: 'registerVerifyEmailFrom', configVal: 'NineTeam团队', public: 0, encry: 0 },
{ configKey: 'registerVerifyEmailFrom', configVal: 'Yi Ai团队', public: 0, encry: 0 },
{ configKey: 'registerVerifyExpir', configVal: '1800', public: 0, encry: 0 },
{ configKey: 'registerSuccessEmailTitle', configVal: 'NineTeam团队账号激活成功', public: 0, encry: 0 },
{ configKey: 'registerSuccessEmailTeamName', configVal: 'NineTeam团队', public: 0, encry: 0 },
{ configKey: 'registerSuccessEmailTitle', configVal: 'Yi Ai账号激活成功', public: 0, encry: 0 },
{ configKey: 'registerSuccessEmailTeamName', configVal: 'Yi Ai', public: 0, encry: 0 },
{
configKey: 'registerSuccessEmaileAppend',
configVal: ',请妥善保管您的账号,我们将为您赠送50次对话额度和5次绘画额度、祝您使用愉快',
configVal: ',请妥善保管您的账号,祝您使用愉快',
public: 0,
encry: 0,
},
{ configKey: 'registerFailEmailTitle', configVal: 'NineTeam账号激活失败', public: 0, encry: 0 },
{ configKey: 'registerFailEmailTeamName', configVal: 'NineTeam团队', public: 0, encry: 0 },
{ configKey: 'registerFailEmailTitle', configVal: 'Yi Ai账号激活失败', public: 0, encry: 0 },
{ configKey: 'registerFailEmailTeamName', configVal: 'Yi Ai团队', public: 0, encry: 0 },
/* 注册默认设置 */
{ configKey: 'registerSendStatus', configVal: '1', public: 1, encry: 0 },
{ configKey: 'registerSendModel3Count', configVal: '30', public: 1, encry: 0 },
@ -136,16 +126,16 @@ export class DatabaseService implements OnModuleInit {
{ configKey: 'registerSendDrawMjCount', configVal: '3', public: 1, encry: 0 },
{ configKey: 'firstRegisterSendStatus', configVal: '1', public: 1, encry: 0 },
{ configKey: 'firstRegisterSendRank', configVal: '500', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendModel3Count', configVal: '20', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendModel4Count', configVal: '2', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendDrawMjCount', configVal: '3', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendModel3Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendModel4Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendDrawMjCount', configVal: '10', public: 1, encry: 0 },
{ configKey: 'inviteSendStatus', configVal: '1', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendModel3Count', configVal: '30', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendModel4Count', configVal: '3', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendDrawMjCount', configVal: '1', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendModel3Count', configVal: '0', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendModel4Count', configVal: '0', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendDrawMjCount', configVal: '0', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendModel3Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendModel4Count', configVal: '1', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendDrawMjCount', configVal: '1', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendModel4Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendDrawMjCount', configVal: '10', public: 1, encry: 0 },
{ configKey: 'isVerifyEmail', configVal: '1', public: 1, encry: 0 },
];

View File

@ -22,9 +22,6 @@ export class MidjourneyEntity extends BaseEntity {
@Column({ comment: '垫图图片 + 绘画描述词 + 额外参数 = 完整的prompt', type: 'text' })
fullPrompt: string;
@Column({ comment: '随机产生的绘画ID用于拿取比对结果' })
randomDrawId: string;
@Column({ comment: '当前绘制任务的进度', nullable: true })
progress: number;
@ -35,7 +32,7 @@ export class MidjourneyEntity extends BaseEntity {
status: number;
@Column({ comment: 'mj绘画的动作、绘图、放大、变换、图生图' })
action: number;
action: string;
@Column({ comment: '一组图片的第几张、放大或者变换的时候需要使用', nullable: true })
orderId: number;
@ -43,14 +40,17 @@ export class MidjourneyEntity extends BaseEntity {
@Column({ comment: '是否推荐0: 默认不推荐 1: 推荐', nullable: true, default: 0 })
rec: number;
@Column({ comment: '对图片操作的', nullable: true })
customId: string;
@Column({ comment: '绘画的ID每条不一样', nullable: true })
message_id: string;
drawId: string;
@Column({ comment: '对图片放大或者变体的ID', nullable: true })
custom_id: string;
@Column({ comment: '图片链接', nullable: true, type: 'text' })
drawUrl: string;
@Column({ comment: '图片信息尺寸', nullable: true, type: 'text' })
fileInfo: string;
@Column({ comment: '图片比例', nullable: true, type: 'text' })
drawRatio: string;
@Column({ comment: '扩展参数', nullable: true, type: 'text' })
extend: string;
@ -60,4 +60,5 @@ export class MidjourneyEntity extends BaseEntity {
@Column({ comment: '是否存入了图片到配置的储存项 配置了则存储 不配置地址则是源地址', default: true })
isSaveImg: boolean;
messageId: any;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { AddBadWordDto } from '../../badwords/dto/addBadWords.dto';
import { AddBadWordDto } from './../../badwords/dto/addBadWords.dto';
import { IsNotEmpty, MinLength, MaxLength, IsEmail, IsOptional, IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
@ -27,6 +27,9 @@ export class SetModelDto {
@ApiProperty({ example: 1, description: 'key的权重' })
keyWeight: number;
@ApiProperty({ example: 1, description: '模型排序' })
modelOrder: number;
@ApiProperty({ example: 4096, description: '模型支持的最大TOken数量', required: true })
maxModelTokens: number;
@ -53,9 +56,10 @@ export class SetModelDto {
@ApiProperty({ example: true, description: '是否设置为绘画Key', required: false })
isDraw: boolean;
//设置token计费
@ApiProperty({ example: true, description: '是否使用token计费', required: false })
isTokenBased: boolean;
@ApiProperty({ example: true, description: 'token计费比例', required: false })
tokenFeeRatio: number;
}

View File

@ -42,4 +42,7 @@ export class ModelsTypeEntity extends BaseEntity {
@Column({ comment: '是否为特殊模型、可以提供联想翻译、思维导图等特殊操作', default: 0 })
isUseTool: boolean;
@Column({ comment: '模型排序', default: 1 })
modelOrder: number;
}

View File

@ -69,4 +69,8 @@ export class ModelsEntity extends BaseEntity {
@Column({ comment: 'token计费比例', default: 0 })
tokenFeeRatio: number;
@Column({ comment: 'key权重', default: 1 })
modelOrder: number;
}

View File

@ -5,7 +5,7 @@ import { ModelsEntity } from './models.entity';
import { SetModelDto } from './dto/setModel.dto';
import { QueryModelDto } from './dto/queryModel.dto';
import { ModelsMapCn } from '@/common/constants/status.constant';
import { getAccessToken } from '../chatgpt/baidu';
// import { getAccessToken } from '../chatgpt/baidu';
import { getRandomItemFromArray, hideString } from '@/common/utils';
import { ModelsTypeEntity } from './modelType.entity';
import { SetModelTypeDto } from './dto/setModelType.dto';
@ -19,8 +19,8 @@ export class ModelsService {
private readonly modelsEntity: Repository<ModelsEntity>,
@InjectRepository(ModelsTypeEntity)
private readonly modelsTypeEntity: Repository<ModelsTypeEntity>,
){}
) { }
private modelTypes = []
private modelMaps = {}
private keyList = {}
@ -28,145 +28,145 @@ export class ModelsService {
private keyPoolMap = {} // 记录每个模型的所有key 并且记录顺序
private keyPoolIndexMap = {} // 记录每个模型的当前调用的下标
async onModuleInit(){
async onModuleInit() {
await this.initCalcKey()
this.refreshBaiduAccesstoken()
}
/* 初始化整理所有key 进行分类并且默认一个初始模型配置 默认是配置的第一个分类的第一个key为准 */
async initCalcKey(){
async initCalcKey() {
this.keyPoolMap = {}
this.keyPoolIndexMap = {}
this.keyList = {}
this.modelMaps = {}
this.modelTypes = []
const allKeys = await this.modelsEntity.find({where: { status: true }})
const keyTypes = allKeys.reduce( (pre: any, cur ) => {
if(!pre[cur.keyType]){
const allKeys = await this.modelsEntity.find({ where: { status: true } })
const keyTypes = allKeys.reduce((pre: any, cur) => {
if (!pre[cur.keyType]) {
pre[cur.keyType] = [cur]
}else{
} else {
pre[cur.keyType].push(cur)
}
return pre
}, {})
this.modelTypes = Object.keys(keyTypes).map( keyType => {
return { label: ModelsMapCn[keyType] , val: keyType}
this.modelTypes = Object.keys(keyTypes).map(keyType => {
return { label: ModelsMapCn[keyType], val: keyType }
})
this.modelMaps = keyTypes
this.keyList = {}
allKeys.forEach( keyDetail => {
allKeys.forEach(keyDetail => {
const { keyType, model, keyWeight } = keyDetail
if(!this.keyPoolMap[model]) this.keyPoolMap[model] = []
if (!this.keyPoolMap[model]) this.keyPoolMap[model] = []
for (let index = 0; index < keyWeight; index++) {
this.keyPoolMap[model].push(keyDetail)
}
if(!this.keyPoolIndexMap[model]) this.keyPoolIndexMap[model] = 0
if(!this.keyList[keyType]) this.keyList[keyType] = {}
if(!this.keyList[keyType][model]) this.keyList[keyType][model] = []
if (!this.keyPoolIndexMap[model]) this.keyPoolIndexMap[model] = 0
if (!this.keyList[keyType]) this.keyList[keyType] = {}
if (!this.keyList[keyType][model]) this.keyList[keyType][model] = []
this.keyList[keyType][model].push(keyDetail)
})
}
/* lock key 自动锁定key */
async lockKey(keyId, remark, keyStatus = -1){
async lockKey(keyId, remark, keyStatus = -1) {
const res = await this.modelsEntity.update({ id: keyId }, { status: false, keyStatus, remark });
Logger.error(`key: ${keyId} 欠费或被官方封禁导致不可用,已被系统自动锁定`);
this.initCalcKey()
}
/* 获取本次调用key的详细信息 */
async getCurrentModelKeyInfo(model){
if(!this.keyPoolMap[model]){
async getCurrentModelKeyInfo(model) {
if (!this.keyPoolMap[model]) {
throw new HttpException('当前调用模型已经被移除、请重新选择模型!', HttpStatus.BAD_REQUEST)
}
/* 调用下标+1 */
this.keyPoolIndexMap[model]++
/* 判断下标超出边界没有 */
const index = this.keyPoolIndexMap[model]
if(index >= this.keyPoolMap[model].length) this.keyPoolIndexMap[model] = 0
const key = this.keyPoolMap[model][this.keyPoolIndexMap[model]]
if (index >= this.keyPoolMap[model].length) this.keyPoolIndexMap[model] = 0
const key = this.keyPoolMap[model][this.keyPoolIndexMap[model]]
return key
}
/* 通过现有配置的key和分类给到默认的配置信息 默认给到第一个分类的第一个key的配置 */
async getBaseConfig(appId?: number): Promise<any>{
if(!this.modelTypes.length || !Object.keys(this.modelMaps).length) return;
async getBaseConfig(appId?: number): Promise<any> {
if (!this.modelTypes.length || !Object.keys(this.modelMaps).length) return;
/* 有appid只可以使用openai 的 模型 */
const modelTypeInfo = appId ? this.modelTypes.find( item => Number(item.val) === 1) : this.modelTypes[0]
const modelTypeInfo = appId ? this.modelTypes.find(item => Number(item.val) === 1) : this.modelTypes[0]
// TODO 第0个会有问题 先添加的4默认就是模型4了 后面优化下
if(!modelTypeInfo) return;
if (!modelTypeInfo) return;
const { keyType, modelName, model, maxModelTokens, maxResponseTokens, deductType, deduct, maxRounds } = this.modelMaps[modelTypeInfo.val][0] // 取到第一个默认的配置项信息
return {
modelTypeInfo,
modelInfo: { keyType, modelName, model, maxModelTokens, maxResponseTokens, topN: 0.8, systemMessage: '', deductType, deduct, maxRounds, rounds: 8 }
modelInfo: { keyType, modelName, model, maxModelTokens, maxResponseTokens, topN: 0.8, systemMessage: '', deductType, deduct, maxRounds, rounds: 8 }
}
}
async setModel(params: SetModelDto){
try {
const { id } = params
params.status && (params.keyStatus = 1)
if(id){
const res = await this.modelsEntity.update({id}, params)
await this.initCalcKey()
return res.affected > 0
}else{
const { keyType, key } = params
if(Number(keyType !== 1)){
const res = await this.modelsEntity.save(params)
async setModel(params: SetModelDto) {
try {
const { id } = params
params.status && (params.keyStatus = 1)
if (id) {
const res = await this.modelsEntity.update({ id }, params)
await this.initCalcKey()
if(keyType === 2){ //百度的需要刷新token
this.refreshBaiduAccesstoken()
return res.affected > 0
} else {
const { keyType, key } = params
if (Number(keyType !== 1)) {
const res = await this.modelsEntity.save(params)
await this.initCalcKey()
return res
} else {
const data = key.map(k => {
try {
const data = JSON.parse(JSON.stringify(params))
data.key = k
return data
} catch (error) {
console.log('parse error: ', error);
}
})
const res = await this.modelsEntity.save(data)
await this.initCalcKey()
return res
}
return res
}else{
const data = key.map( k => {
try {
const data = JSON.parse(JSON.stringify(params))
data.key = k
return data
} catch (error) {
console.log('parse error: ', error);
}
})
const res = await this.modelsEntity.save(data)
await this.initCalcKey()
return res
}
} catch (error) {
console.log('error: ', error);
}
} catch (error) {
console.log('error: ', error);
}
}
async delModel({id}){
if(!id) {
async delModel({ id }) {
if (!id) {
throw new HttpException('缺失必要参数!', HttpStatus.BAD_REQUEST)
}
const m = await this.modelsEntity.findOne({where: {id}})
if(!m){
const m = await this.modelsEntity.findOne({ where: { id } })
if (!m) {
throw new HttpException('当前账号不存在!', HttpStatus.BAD_REQUEST)
}
const res = await this.modelsEntity.delete({id})
const res = await this.modelsEntity.delete({ id })
await this.initCalcKey()
return res;
}
async queryModels(req, params: QueryModelDto){
async queryModels(req, params: QueryModelDto) {
const { role } = req.user
const { keyType, key, status, model, page = 1, size = 10 } = params
let where: any = {}
keyType && (where.keyType = keyType)
model && (where.model = model)
status && (where.status = Number(status) === 1 ? true : false)
key && ( where.key = Like(`%${key}%`))
key && (where.key = Like(`%${key}%`))
const [rows, count] = await this.modelsEntity.findAndCount({
where: where,
skip: (page - 1) * size,
take: size,
where: where,
order: {
modelOrder: 'ASC'
},
skip: (page - 1) * size,
take: size,
})
if(role !== 'super'){
rows.forEach( item => {
if (role !== 'super') {
rows.forEach(item => {
item.key && (item.key = hideString(item.key))
item.secret && (item.secret = hideString(item.secret))
})
@ -176,24 +176,29 @@ export class ModelsService {
}
/* 客户端查询到的所有的配置的模型类别 以及类别下自定义的多少中文模型名称 */
async modelsList(){
const cloneModelMaps = JSON.parse(JSON.stringify(this.modelMaps))
Object.keys(cloneModelMaps).forEach( key => {
async modelsList() {
const cloneModelMaps = JSON.parse(JSON.stringify(this.modelMaps));
Object.keys(cloneModelMaps).forEach(key => {
// 对每个模型进行排序
cloneModelMaps[key] = cloneModelMaps[key].sort((a, b) => a.modelOrder - b.modelOrder);
cloneModelMaps[key] = Array.from(
cloneModelMaps[key].map( t => {
const { modelName, model, deduct, deductType, maxRounds } = t
return { modelName, model, deduct, deductType, maxRounds }
}).reduce((map, obj) => map.set(obj.modelName, obj), new Map()).values()
cloneModelMaps[key]
.map(t => {
const { modelName, model, deduct, deductType, maxRounds } = t;
return { modelName, model, deduct, deductType, maxRounds };
})
.reduce((map, obj) => map.set(obj.modelName, obj), new Map()).values()
);
})
});
return {
modelTypeList: this.modelTypes,
modelMaps: cloneModelMaps
}
};
}
/* 记录使用次数和使用的token数量 */
async saveUseLog(id, useToken){
async saveUseLog(id, useToken) {
await this.modelsEntity
.createQueryBuilder()
.update(ModelsEntity)
@ -202,54 +207,32 @@ export class ModelsService {
.execute();
}
async refreshBaiduAccesstoken(){
const allKeys = await this.modelsEntity.find({ where: { keyType: 2 } })
const keysMap: any = {}
allKeys.forEach( keyInfo => {
const { key, secret } = keyInfo
if(!keysMap.key){
keysMap[key] = [{ keyInfo }]
}else{
keysMap[key].push(keyInfo)
}
})
Object.keys(keysMap).forEach( async key => {
const {secret, id } = keysMap[key][0]['keyInfo']
const accessToken: any = await getAccessToken(key, secret)
await this.modelsEntity.update({ key }, { accessToken })
})
setTimeout(() => {
this.initCalcKey()
}, 1000)
}
/* 获取一张绘画key */
async getRandomDrawKey(){
const drawkeys = await this.modelsEntity.find({where: { isDraw: true, status: true }})
if(!drawkeys.length){
async getRandomDrawKey() {
const drawkeys = await this.modelsEntity.find({ where: { isDraw: true, status: true } })
if (!drawkeys.length) {
throw new HttpException('当前未指定特殊模型KEY、前往后台模型池设置吧', HttpStatus.BAD_REQUEST)
}
return getRandomItemFromArray(drawkeys)
}
/* 获取所有key */
async getAllKey(){
async getAllKey() {
return await this.modelsEntity.find()
}
/* 查询模型类型 */
async queryModelType(params: QueryModelTypeDto){
async queryModelType(params: QueryModelTypeDto) {
return 1
}
/* 创建修改模型类型 */
async setModelType(params: SetModelTypeDto){
async setModelType(params: SetModelTypeDto) {
return 1
}
/* 删除模型类型 */
async delModelType(params){
async delModelType(params) {
return 1
}

View File

@ -20,9 +20,9 @@ export class MjDrawDto {
@IsOptional()
imgUrl?: string;
@ApiProperty({ example: 1, description: '绘画动作 绘图、放大、变换、图生图' })
@ApiProperty({ example: 'IMAGINE', description: '任务类型,可用值:IMAGINE,UPSCALE,VARIATION,ZOOM,PAN,DESCRIBE,BLEND,SHORTEN,SWAP_FACE' })
@IsOptional()
action: number;
action: string;
@ApiProperty({ example: 1, description: '变体或者放大的序号' })
@IsOptional()
@ -31,4 +31,8 @@ export class MjDrawDto {
@ApiProperty({ example: 1, description: '绘画的DBID' })
@IsOptional()
drawId: number;
@ApiProperty({ example: 1, description: '任务ID' })
@IsOptional()
taskId: number;
}

View File

@ -15,7 +15,7 @@ export class QueueService implements OnApplicationBootstrap {
private readonly midjourneyService: MidjourneyService,
private readonly userBalanceService: UserBalanceService,
private readonly globalConfigService: GlobalConfigService,
) {}
) { }
private readonly jobIds: any[] = [];
async onApplicationBootstrap() {
@ -27,14 +27,13 @@ export class QueueService implements OnApplicationBootstrap {
/* 提交绘画任务 */
async addMjDrawQueue(body: MjDrawDto, req: Request) {
const { prompt, imgUrl, extraParam, orderId, action = 1, drawId } = body;
const { imgUrl, orderId, action, drawId } = body;
/* 限制普通用户队列最多可以有两个任务在排队或者等待中 */
await this.midjourneyService.checkLimit(req);
/* 检测余额 */
await this.userBalanceService.validateBalance(req, 'mjDraw', action === 2 ? 1 : 4);
await this.userBalanceService.validateBalance(req, 'mjDraw', action === 'UPSCALE' ? 1 : 4);
/* 绘图或者图生图 */
if (action === MidjourneyActionEnum.DRAW || action === MidjourneyActionEnum.GENERATE) {
if (action === 'IMAGINE') {
/* 绘图或者图生图是相同的 区分一个action即可 */
const randomDrawId = `${createRandomUid()}`;
const params = { ...body, userId: req.user.id, randomDrawId };
@ -44,84 +43,28 @@ export class QueueService implements OnApplicationBootstrap {
/* 添加任务到队列 通过imgUrl判断是不是图生图 */
const job = await this.mjDrawQueue.add(
'mjDraw',
{ id: res.id, action: imgUrl ? 4 : 1, userId: req.user.id },
{ id: res.id, action: action, userId: req.user.id },
{ delay: 1000, timeout: +timeout },
);
/* 绘图和图生图扣除余额4 */
this.jobIds.push(job.id);
/* 扣费 */
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return true;
} else {
const { orderId, action, drawId } = body;
const actionDetail = await this.midjourneyService.getDrawActionDetail(action, drawId, orderId);
const params = { ...body, userId: req.user.id, ...actionDetail };
const res = await this.midjourneyService.addDrawQueue(params);
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000;
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id);
return;
}
if (!drawId || !orderId) {
throw new HttpException('缺少必要参数!', HttpStatus.BAD_REQUEST);
}
/* 图片操作 */
/* 图片放大 */
if (action === MidjourneyActionEnum.UPSCALE) {
const actionDetail: any = await this.midjourneyService.getDrawActionDetail(action, drawId, orderId);
const { custom_id } = actionDetail;
/* 检测当前图片是不是已经放大过了 */
await this.midjourneyService.checkIsUpscale(custom_id);
const params = { ...body, userId: req.user.id, ...actionDetail };
const res = await this.midjourneyService.addDrawQueue(params);
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000;
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
/* 扣费 */
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 1, 1);
this.jobIds.push(job.id);
return;
}
/* 图片变体 */
if (action === MidjourneyActionEnum.VARIATION) {
const actionDetail: any = await this.midjourneyService.getDrawActionDetail(action, drawId, orderId);
const params = { ...body, userId: req.user.id, ...actionDetail };
const res = await this.midjourneyService.addDrawQueue(params);
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000;
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id);
/* 扣费 */
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return;
}
/* 重新生成 */
if (action === MidjourneyActionEnum.REGENERATE) {
const actionDetail: any = await this.midjourneyService.getDrawActionDetail(action, drawId, orderId);
const params = { ...body, userId: req.user.id, ...actionDetail };
const res = await this.midjourneyService.addDrawQueue(params);
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000;
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id);
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return;
}
/* 对图片增强 Vary */
if (action === MidjourneyActionEnum.VARY) {
const actionDetail: any = await this.midjourneyService.getDrawActionDetail(action, drawId, orderId);
const params = { ...body, userId: req.user.id, ...actionDetail };
const res = await this.midjourneyService.addDrawQueue(params);
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000;
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id);
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return;
}
/* 对图片缩放 Zoom */
if (action === MidjourneyActionEnum.ZOOM) {
const actionDetail: any = await this.midjourneyService.getDrawActionDetail(action, drawId, orderId);
const params = { ...body, userId: req.user.id, ...actionDetail };
const res = await this.midjourneyService.addDrawQueue(params);
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000;
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id);
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return;
}
}
/* 查询队列 */

View File

@ -1,4 +1,4 @@
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
import { GlobalConfigService } from './../globalConfig/globalConfig.service';
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
@ -13,7 +13,7 @@ export class TaskService {
private readonly userBalanceEntity: Repository<UserBalanceEntity>,
private readonly globalConfigService: GlobalConfigService,
private readonly modelsService: ModelsService,
) {}
) { }
/* 每小时刷新一次微信的token */
@Cron(CronExpression.EVERY_HOUR)
@ -40,8 +40,8 @@ export class TaskService {
}
/* 每小时检测一次授权 */
@Cron('0 0 */5 * *')
refreshBaiduAccesstoken() {
this.modelsService.refreshBaiduAccesstoken();
}
// @Cron('0 0 */5 * *')
// refreshBaiduAccesstoken() {
// this.modelsService.refreshBaiduAccesstoken();
// }
}

View File

@ -10,33 +10,46 @@ import * as FormData from 'form-data';
@Injectable()
export class UploadService implements OnModuleInit {
constructor(private readonly globalConfigService: GlobalConfigService) {}
constructor(private readonly globalConfigService: GlobalConfigService) { }
private tencentCos: any;
onModuleInit() {}
onModuleInit() { }
async uploadFile(file) {
const { filename: name, originalname, buffer, dir = 'ai', mimetype } = file;
const fileTyle = mimetype ? mimetype.split('/')[1] : '';
const filename = originalname || name
Logger.debug(`准备上传文件: ${filename}, 类型: ${fileTyle}`, 'UploadService');
const {
tencentCosStatus = 0,
aliOssStatus = 0,
cheveretoStatus = 0,
} = await this.globalConfigService.getConfigs(['tencentCosStatus', 'aliOssStatus', 'cheveretoStatus']);
Logger.debug(`上传配置状态 - 腾讯云: ${tencentCosStatus}, 阿里云: ${aliOssStatus}, Chevereto: ${cheveretoStatus}`, 'UploadService');
if (!Number(tencentCosStatus) && !Number(aliOssStatus) && !Number(cheveretoStatus)) {
throw new HttpException('请先前往后台配置上传图片的方式', HttpStatus.BAD_REQUEST);
}
if (Number(tencentCosStatus)) {
return this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle });
}
if (Number(aliOssStatus)) {
return await this.uploadFileByAliOss({ filename, buffer, dir, fileTyle });
}
if (Number(cheveretoStatus)) {
const { filename, buffer: fromBuffer, dir } = file;
return await this.uploadFileByChevereto({ filename, buffer: fromBuffer.toString('base64'), dir, fileTyle });
try {
if (Number(tencentCosStatus)) {
Logger.debug(`使用腾讯云COS上传`, 'UploadService');
return await this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle });
}
if (Number(aliOssStatus)) {
Logger.debug(`使用阿里云OSS上传`, 'UploadService');
return await this.uploadFileByAliOss({ filename, buffer, dir, fileTyle });
}
if (Number(cheveretoStatus)) {
Logger.debug(`使用Chevereto上传`, 'UploadService');
const { filename, buffer: fromBuffer, dir } = file;
return await this.uploadFileByChevereto({ filename, buffer: fromBuffer.toString('base64'), dir, fileTyle });
}
} catch (error) {
Logger.error(`上传失败: ${error.message}`, 'UploadService');
throw error; // 重新抛出异常,以便调用方可以处理
}
}
@ -122,23 +135,10 @@ export class UploadService implements OnModuleInit {
this.tencentCos = new TENCENTCOS({ SecretId, SecretKey, FileParallelLimit: 10 });
try {
const proxyMj = (await this.globalConfigService.getConfigs(['mjProxy'])) || 0;
/* 开启代理 */
if (Number(proxyMj) === 1) {
const data = { cosType: 'tencent', url, cosParams: { Bucket, Region, SecretId, SecretKey } };
const mjProxyUrl = (await this.globalConfigService.getConfigs(['mjProxyUrl'])) || 'http://172.247.48.137:8000';
const res = await axios.post(`${mjProxyUrl}/mj/replaceUpload`, data);
if (!res.data) throw new HttpException('上传图片失败[ten][url]', HttpStatus.BAD_REQUEST);
let locationUrl = res.data.replace(/^(http:\/\/|https:\/\/|\/\/|)(.*)/, 'https://$2');
const { acceleratedDomain } = await this.getUploadConfig('tencent');
if (acceleratedDomain) {
locationUrl = locationUrl.replace(/^(https:\/\/[^/]+)(\/.*)$/, `https://${acceleratedDomain}$2`);
console.log('当前已开启全球加速----------------->');
}
return locationUrl;
} else {
const buffer = await this.getBufferFromUrl(url);
return await this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle: '' });
}
const buffer = await this.getBufferFromUrl(url);
return await this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle: '' });
} catch (error) {
console.log('TODO->error: ', error);
throw new HttpException('上传图片失败[ten][url]', HttpStatus.BAD_REQUEST);