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"], "prettier.enable": true,
"emmet.syntaxProfiles": { "editor.formatOnSave": true,
"vue-html": "html",
"vue": "html"
},
"editor.tabSize": 2,
"eslint.alwaysShowStatus": true,
"eslint.quiet": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true, "source.fixAll.eslint": "explicit"
"source.fixAll": true,
"source.fixAll.stylelint": true
}, },
"stylelint.customSyntax": "postcss-less", "eslint.validate": [
"stylelint.validate": [ "javascript",
"css", "javascriptreact",
"less" "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 更新整合版 # NineAI 更新整合版
## 更新日志 ## 更新日志

View File

@@ -1,5 +1,5 @@
{ {
"version": "2.4.0", "version": "2.5.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build:test": "vue-tsc --noEmit && vite build --mode test", "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-vision-preview",
"gpt-4-all", "gpt-4-all",
"gpt-4-0125-preview", "gpt-4-0125-preview",
'dall-e-3',
// claude // claude
"claude-2.0", "claude-2.0",
"claude-2.1", "claude-2.1",
@@ -293,6 +294,7 @@ export const MODELSMAPLIST = {
"gpt-4-vision-preview", "gpt-4-vision-preview",
"gpt-4-all", "gpt-4-all",
"gpt-4-0125-preview", "gpt-4-0125-preview",
'dall-e-3',
// claude // claude
"claude-2.0", "claude-2.0",
"claude-2.1", "claude-2.1",

View File

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

View File

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

View File

@@ -1,82 +1,85 @@
<route lang="yaml"> <route lang="yaml">
meta: meta:
title: key列表 title: key列表
</route> </route>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive } from 'vue' import { onMounted, reactive } from 'vue';
import { ElMessage, type FormInstance } from 'element-plus' import { ElMessage, type FormInstance } from 'element-plus';
import ApiMj from '@/api/modules/mj' import ApiMj from '@/api/modules/mj';
import ApiChat from '@/api/modules/chat' import ApiChat from '@/api/modules/chat';
import { utcToShanghaiTime } from '@/utils/utcformatTime' 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 formRef = ref<FormInstance>();
const total = ref(0) const total = ref(0);
const loading = ref(false) const loading = ref(false);
const formInline = reactive({ const formInline = reactive({
rec: null, rec: null,
status: null, status: null,
page: 1, page: 1,
size: 10, size: 10,
}) });
interface OrderInfo { interface OrderInfo {
id: number id: number;
status: number status: number;
withdrawalAmount: number withdrawalAmount: number;
userId: number userId: number;
remark: string remark: string;
contactInformation: string contactInformation: string;
auditStatus: number auditStatus: number;
auditUserId: number auditUserId: number;
createdAt: Date createdAt: Date;
updatedAt: Date updatedAt: Date;
userInfo: { userInfo: {
avatar: string avatar: string;
username: string username: string;
email: string email: string;
} };
} }
const tableData = ref<OrderInfo[]>([]) const tableData = ref<OrderInfo[]>([]);
async function queryDrawList() { async function queryDrawList() {
try { try {
loading.value = true loading.value = true;
const res = await ApiMj.queryAdminDrawList(formInline) const res = await ApiMj.queryAdminDrawList(formInline);
loading.value = false loading.value = false;
const { rows, count } = res.data const { rows, count } = res.data;
total.value = count total.value = count;
tableData.value = rows tableData.value = rows;
} } catch (error) {
catch (error) { loading.value = false;
loading.value = false
} }
} }
function handlerReset(formEl: FormInstance | undefined) { function handlerReset(formEl: FormInstance | undefined) {
formEl?.resetFields() formEl?.resetFields();
queryDrawList() queryDrawList();
} }
async function recommendDrawImg(id: number) { async function recommendDrawImg(id: number) {
const res = await ApiChat.recMjDrawImg({ id }) const res = await ApiChat.recMjDrawImg({ id });
ElMessage.success(res.data) ElMessage.success(res.data);
queryDrawList() queryDrawList();
} }
async function handleDelChatLog(id) { async function handleDelChatLog(id) {
const res = await ApiChat.delChatLog({ id }) const res = await ApiChat.delChatLog({ id });
ElMessage.success(res.data) ElMessage.success(res.data);
queryDrawList() queryDrawList();
} }
onMounted(() => { onMounted(() => {
queryDrawList() queryDrawList();
}) });
</script> </script>
<template> <template>
@@ -84,24 +87,38 @@ onMounted(() => {
<page-main> <page-main>
<el-form ref="formRef" :inline="true" :model="formInline"> <el-form ref="formRef" :inline="true" :model="formInline">
<el-form-item label="推荐状态" prop="rec"> <el-form-item label="推荐状态" prop="rec">
<el-select v-model="formInline.rec" placeholder="请选择推荐状态" clearable> <el-select
<el-option v-for="item in RECOMMEND_STATUS" :key="item.value" :label="item.label" :value="item.value" /> 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-select>
</el-form-item> </el-form-item>
<el-form-item label="绘制状态" prop="status"> <el-form-item label="绘制状态" prop="status">
<el-select v-model="formInline.status" placeholder="请选择绘制状态" clearable> <el-select
<el-option v-for="item in WITHDRAW_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" /> 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-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="queryDrawList"> <el-button type="primary" @click="queryDrawList"> 查询 </el-button>
查询 <el-button @click="handlerReset(formRef)"> 重置 </el-button>
</el-button>
<el-button @click="handlerReset(formRef)">
重置
</el-button>
</el-form-item> </el-form-item>
<span style="float: right;"> <span style="float: right">
<el-button type="success" @click="queryDrawList"> <el-button type="success" @click="queryDrawList">
刷新列表 刷新列表
</el-button> </el-button>
@@ -109,33 +126,77 @@ onMounted(() => {
</el-form> </el-form>
</page-main> </page-main>
<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>
<page-main style="width: 100%;"> <page-main style="width: 100%">
<el-table v-loading="loading" border :data="tableData" style="width: 100%;" size="large"> <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="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"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column prop="userInfo.username" align="center" label="用户名" width="120" /> <el-table-column
<el-table-column prop="fileInfo.thumbImg" align="center" label="推荐状态" width="90"> prop="userInfo.username"
align="center"
label="用户名"
width="120"
/>
<el-table-column
prop="fileInfo.thumbImg"
align="center"
label="推荐状态"
width="90"
>
<template #default="scope"> <template #default="scope">
<el-tag :type="scope.row.rec === 1 ? 'success' : ''"> <el-tag :type="scope.row.rec === 1 ? 'success' : ''">
{{ scope.row.rec === 1 ? '已推荐' : '未推荐' }} {{ scope.row.rec === 1 ? '已推荐' : '未推荐' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="userInfo.email" label="邮箱" width="180" align="center" /> <el-table-column
<el-table-column prop="status" align="center" label="绘图状态" width="105"> prop="userInfo.email"
label="邮箱"
width="180"
align="center"
/>
<el-table-column
prop="status"
align="center"
label="绘图状态"
width="105"
>
<template #default="scope"> <template #default="scope">
<el-tag :type="scope.row.status === 100 ? 'success' : ''"> <el-tag :type="scope.row.status === 100 ? 'success' : ''">
{{ DRAW_STATUS_MAP[scope.row.status] }} {{ DRAW_STATUS_MAP[scope.row.status] }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </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"> <template #default="scope">
<el-popover placement="top" :width="400" trigger="click"> <el-popover placement="top" :width="400" trigger="click">
<template #reference> <template #reference>
@@ -149,19 +210,38 @@ onMounted(() => {
</el-popover> </el-popover>
</template> </template>
</el-table-column> </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"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column prop="userInfo.avatar" label="用户头像" width="90"> <el-table-column prop="userInfo.avatar" label="用户头像" width="90">
<template #default="scope"> <template #default="scope">
<img :src="scope.row?.userInfo?.avatar" style="height: 50px;"> <img :src="scope.row?.userInfo?.avatar" style="height: 50px" />
</template> </template>
</el-table-column> </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"> <template #default="scope">
{{ utcToShanghaiTime(scope.row.createdAt, 'YYYY-MM-DD hh:mm:ss') }} {{ utcToShanghaiTime(scope.row.createdAt, 'YYYY-MM-DD hh:mm:ss') }}
</template> </template>
@@ -169,18 +249,32 @@ onMounted(() => {
<el-table-column fixed="right" label="操作" width="200" align="center"> <el-table-column fixed="right" label="操作" width="200" align="center">
<template #default="scope"> <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> <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> </el-button>
</template> </template>
</el-popconfirm> </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> <template #reference>
<el-button type="warning" size="small"> <el-button type="warning" size="small"> 删除记录 </el-button>
删除记录
</el-button>
</template> </template>
</el-popconfirm> </el-popconfirm>
</template> </template>

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ function createMainWindow() {
if (app.isPackaged) { if (app.isPackaged) {
// mainWindow.loadFile(filePath) // mainWindow.loadFile(filePath)
mainWindow.loadURL('https://ai.jiangly.com') // mainWindow.loadURL('https://ai.jiangly.com')
} }
else { else {
mainWindow.loadURL('http://127.0.0.1:1002') 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> </script>
</body> </body>

View File

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

View File

@@ -1,165 +1,223 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' import type { AxiosProgressEvent, GenericAbortSignal } from "axios";
import { get, post } from '@/utils/request' import { get, post } from "@/utils/request";
import { useSettingStore } from '@/store' import { useSettingStore } from "@/store";
/* 流失对话聊天 */ /* 流失对话聊天 */
export function fetchChatAPIProcess<T = any>( export function fetchChatAPIProcess<T = any>(params: {
params: { prompt: string;
prompt: string appId?: number;
appId?: number options?: {
options?: { conversationId?: string; parentMessageId?: string; temperature: number } conversationId?: string;
imageUrl?:string parentMessageId?: string;
model?:string temperature: number;
signal?: GenericAbortSignal };
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, imageUrl?: string;
) { model?: string;
return post<T>({ signal?: GenericAbortSignal;
url: '/chatgpt/chat-process', onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
data: { prompt: params.prompt, appId: params?.appId, options: params.options,imageUrl: params.imageUrl,model: params.model}, }) {
signal: params.signal, return post<T>({
onDownloadProgress: params.onDownloadProgress, 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>() { 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> { export function fetchRegisterAPI<T>(data: {
return post<T>({ url: '/auth/register', data }) as Promise<T> 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> { export function fetchRegisterByPhoneAPI<T>(data: {
return post<T>({ url: '/auth/registerByPhone', data }) as Promise<T> 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> { export function fetchLoginAPI<T>(data: {
return post<T>({ url: '/auth/login', data }) as Promise<T> 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> { export function fetchLoginByPhoneAPI<T>(data: {
return post<T>({ url: '/auth/loginByPhone', data }) as Promise<T> 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> { export function fetchUpdateInfoAPI<T>(data: {
return post<T>({ url: '/user/update', data }) as Promise<T> 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> { 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> { export function fetchGetAllChatLogDraw<T>(data: {
return get<T>({ url: '/chatLog/drawAll', data }) as Promise<T> size: number;
rec: number;
model: string;
}): Promise<T> {
return get<T>({ url: "/chatLog/drawAll", data }) as Promise<T>;
} }
/* chatgpt的dall-e2绘画 */ /* chatgpt的dall-e2绘画 */
export function fetchChatDraw<T>(data: { prompt: string;n: number;size: string }): Promise<T> { export function fetchChatDraw<T>(data: {
return post<T>({ url: '/chatgpt/chat-draw', data }) as Promise<T> 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> { export function fetchUpdatePasswordAPI<T>(data: {
return post<T>({ url: '/auth/updatePassword', data }) as Promise<T> oldPassword?: string;
password?: string;
}): Promise<T> {
return post<T>({ url: "/auth/updatePassword", data }) as Promise<T>;
} }
/* 同步对话 */ /* 同步对话 */
export function fetchGetchatSyncApi<T = any>( export function fetchGetchatSyncApi<T = any>(params: {
params: { prompt: string;
prompt: string options?: {
options?: { conversationId?: string; parentMessageId?: string; temperature: number } conversationId?: string;
signal?: GenericAbortSignal parentMessageId?: string;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, temperature: number;
) { };
return post<T>({ signal?: GenericAbortSignal;
url: '/chatgpt/chat-sync', onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
data: { prompt: params.prompt, options: params.options }, }) {
signal: params.signal, return post<T>({
onDownloadProgress: params.onDownloadProgress, url: "/chatgpt/chat-sync",
}) data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
});
} }
/* 获取mind绘画联想词 */ /* 获取mind绘画联想词 */
export function fetchGetchatMindApi<T = any>( export function fetchGetchatMindApi<T = any>(params: {
params: { prompt: string;
prompt: string options?: {
options?: { conversationId?: string; parentMessageId?: string; temperature: number } conversationId?: string;
signal?: GenericAbortSignal parentMessageId?: string;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, temperature: number;
) { };
return post<T>({ signal?: GenericAbortSignal;
url: '/chatgpt/chat-mind', onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
data: { prompt: params.prompt, options: params.options }, }) {
signal: params.signal, return post<T>({
onDownloadProgress: params.onDownloadProgress, url: "/chatgpt/chat-mind",
}) data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
});
} }
/* 获取MJ绘画联想词 */ /* 获取MJ绘画联想词 */
export function fetchGetMjPromptAssociateApi<T>(data: { prompt: string }): Promise<T> { export function fetchGetMjPromptAssociateApi<T>(data: {
return post<T>({ url: '/chatgpt/mj-associate', data }) as Promise<T> prompt: string;
}): Promise<T> {
return post<T>({ url: "/chatgpt/mj-associate", data }) as Promise<T>;
} }
/* 获取MJ绘画联想词 */ /* 获取MJ绘画联想词 */
export function fetchGetMjPromptFanyiApi<T>(data: { prompt: string }): Promise<T> { export function fetchGetMjPromptFanyiApi<T>(data: {
return post<T>({ url: '/chatgpt/mj-fy', data }) as Promise<T> 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> { export function fetchMidjourneyDrawList<T>(data: {
return get<T>({ url: '/midjourney/drawList', data }) as Promise<T> page?: number;
size?: number;
}): Promise<T> {
return get<T>({ url: "/midjourney/drawList", data }) as Promise<T>;
} }
/* 获取Mj提示词 */ /* 获取Mj提示词 */
export function fetchMidjourneyPromptList<T>(): Promise<T> { 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完整提示词 */ /* 获取Mj完整提示词 */
export function fetchMidjourneyFullPrompt<T>(data: any): Promise<T> { 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绘画记录 */ /* 删除MJ绘画记录 */
export function fetchDownloadImg<T>(data: { id: number }): Promise<T> { 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> { export function fetchMidjourneyGetList<T>(data: {
return get<T>({ url: '/midjourney/getList', data }) as Promise<T> 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> { 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> { 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> { export function fetchSendSms<T>(data: {
return post<T>({ url: '/auth/sendPhoneCode', data }) as Promise<T> phone: string;
captchaId: string;
captchaCode: string;
}): Promise<T> {
return post<T>({ url: "/auth/sendPhoneCode", data }) as Promise<T>;
} }
/* 获取九宫格设置 */ /* 获取九宫格设置 */
export function fetchGetChatBoxList<T>() { export function fetchGetChatBoxList<T>() {
return get<T>({ url: '/chatgpt/queryChatBoxFrontend' }) return get<T>({ url: "/chatgpt/queryChatBoxFrontend" });
} }
/* 获取快问设置 */ /* 获取快问设置 */
export function fetchGetChatPreList<T>() { 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>({ return post<T>({
url: '/queue/addMjDrawQueue', url: '/queue/addMjDrawQueue',
data, data,
@@ -45,8 +45,8 @@ export function fetchProxyImgAPI<T>(data: { url: string }): Promise<T> {
return get<T>({ return get<T>({
url: '/midjourney/proxy', url: '/midjourney/proxy',
data, data,
headers: { headers: {
responseType: 'arraybuffer' 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, gap: 10,
progress: 0, progress: 0,
tips: '', tips: '',
words: ['L', 'O', 'A', 'D', 'I', 'N', 'G'] // words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
words: ['AI', '绘', '画', '中'],
}) })
const appStore = useAppStore() const appStore = useAppStore()
const theme = computed(() => appStore.theme) const theme = computed(() => appStore.theme)
const loadingTextColor = computed(() => theme.value === 'dark' ? '#fff' : '#000') const loadingTextColor = computed(() =>
theme.value === 'dark' ? '#fff' : '#000'
)
interface Props { interface Props {
gap?: number gap?: number
progress?: number progress?: number
tips?: string tips?: string
bgColor?: string bgColor?: string
words?: any words?: any
} }
// const words = ref<string[]>(['L', 'O', 'A', 'D', 'I', 'N', 'G']) // const words = ref<string[]>(['L', 'O', 'A', 'D', 'I', 'N', 'G'])
</script> </script>
@@ -27,7 +30,13 @@ interface Props {
<template> <template>
<div class="loading" :style="{ background: props.bgColor }"> <div class="loading" :style="{ background: props.bgColor }">
<div class="loading-text"> <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>
<div v-if="!tips && props.progress" class="progress"> <div v-if="!tips && props.progress" class="progress">
绘制进度 {{ props.progress }}% 绘制进度 {{ props.progress }}%
@@ -46,14 +55,14 @@ interface Props {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 100; z-index: 100;
user-select: none; user-select: none;
} }
.progress{ .progress {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
.loading-text { .loading-text {
@@ -72,42 +81,42 @@ interface Props {
display: inline-block; display: inline-block;
margin: 0 5px; margin: 0 5px;
color: #fff; color: #fff;
font-family: "Quattrocento Sans", sans-serif; font-family: 'Quattrocento Sans', sans-serif;
} }
.loading-text span:nth-child(1) { .loading-text span:nth-child(1) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 0s infinite linear alternate; -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) { .loading-text span:nth-child(2) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.2s infinite linear alternate; -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) { .loading-text span:nth-child(3) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.4s infinite linear alternate; -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) { .loading-text span:nth-child(4) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.6s infinite linear alternate; -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) { .loading-text span:nth-child(5) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.8s infinite linear alternate; -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) { .loading-text span:nth-child(6) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 1s infinite linear alternate; -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) { .loading-text span:nth-child(7) {
filter: blur(0px); filter: blur(0px);
-webkit-animation: blur-text 1.5s 1.2s infinite linear alternate; -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 { @-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 { .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); filter: blur(20px);
position: absolute; position: absolute;
top: 0; top: 0;

View File

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

View File

@@ -15,7 +15,7 @@ export function defaultSetting(): UserState {
return { return {
userInfo: { userInfo: {
avatar: 'https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1681310872890image.png', 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%; 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; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", sans-serif;
} }

View File

@@ -13,6 +13,9 @@ import {
useDialog, useDialog,
useMessage, useMessage,
NAlert, NAlert,
NSpace,
NPopselect,
NText,
} from 'naive-ui' } from 'naive-ui'
import type { MessageRenderMessage } from 'naive-ui' import type { MessageRenderMessage } from 'naive-ui'
@@ -40,7 +43,7 @@ import {
useGlobalStoreWithOut, useGlobalStoreWithOut,
} from '@/store' } from '@/store'
import { fetchQueryOneCatAPI } from '@/api/appStore' import { fetchQueryOneCatAPI } from '@/api/appStore'
import { fetchChatAPIProcess } from '@/api' import { fetchChatAPIProcess, fetchVideoAPIProcess } from '@/api'
import { t } from '@/locales' import { t } from '@/locales'
import { router } from '@/router' import { router } from '@/router'
const uploadUrl = ref(`${import.meta.env.VITE_GLOB_API_URL}/upload/file`) 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', 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 theme = computed(() => appStore.theme)
const globaelConfig = computed(() => authStore.globalConfig) const globaelConfig = computed(() => authStore.globalConfig)
@@ -198,6 +233,48 @@ function handleSignIn() {
useGlobalStore.updateSignInDialog(true) 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逆向 // 解析文件 gpt-4-all逆向
let curFile: File | null let curFile: File | null
@@ -279,7 +356,6 @@ async function uploadFile() {
} finally { } finally {
dataBase64.value = null dataBase64.value = null
curFile = null curFile = null
showProgressModal.value = false
} }
} }
@@ -426,6 +502,7 @@ async function onConversation(msg?: string) {
usage: data?.detail?.usage, usage: data?.detail?.usage,
error: false, error: false,
loading: true, loading: true,
imageUrl: data?.imageUrl,
conversationOptions: { conversationOptions: {
conversationId: data?.conversationId, conversationId: data?.conversationId,
parentMessageId: data?.id, parentMessageId: data?.id,
@@ -594,6 +671,7 @@ async function onConversation(msg?: string) {
return return
} }
updateGroupChat(dataSources.value.length - 1, { updateGroupChat(dataSources.value.length - 1, {
imageUrl: data.imageUrl,
dateTime: new Date().toLocaleString(), dateTime: new Date().toLocaleString(),
text: errorMessage, text: errorMessage,
inversion: false, inversion: false,
@@ -607,6 +685,8 @@ async function onConversation(msg?: string) {
loading.value = false loading.value = false
isStreamIn.value = false isStreamIn.value = false
imageUrl = null imageUrl = null
typingStatusEnd.value = true
scrollToBottom()
} }
} }
@@ -818,6 +898,7 @@ onUnmounted(() => {
:imageUrl="item.imageUrl" :imageUrl="item.imageUrl"
@regenerate="handleSubmit(index)" @regenerate="handleSubmit(index)"
@delete="handleDelete(item)" @delete="handleDelete(item)"
@video="hendleVideo(item)"
/> />
<div class="sticky bottom-1 left-0 flex justify-center"> <div class="sticky bottom-1 left-0 flex justify-center">
<NButton v-if="loading" @click="handleStop"> <NButton v-if="loading" @click="handleStop">
@@ -844,6 +925,7 @@ onUnmounted(() => {
@click="toggleUsingContext" @click="toggleUsingContext"
> >
<span <span
class=""
:class="{ :class="{
'text-[#3076fd]': usingContext, 'text-[#3076fd]': usingContext,
'text-[#ffffff]': !usingContext, 'text-[#ffffff]': !usingContext,
@@ -971,7 +1053,6 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
</div> </div>
</div> </div>
<div <div
class="m-auto max-w-screen-4xl" class="m-auto max-w-screen-4xl"
:class="[isMobile ? 'px-2 py-1' : 'px-4 py-2']" :class="[isMobile ? 'px-2 py-1' : 'px-4 py-2']"
@@ -1097,35 +1178,6 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div class="flex justify-between items-center"> <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 <NButton
type="primary" type="primary"
size="small" size="small"
@@ -1181,6 +1233,10 @@ onUnmounted(() => {
/> />
</NCard> </NCard>
</NModal> </NModal>
<!-- <audio ref="audioRef" controls>
<source type="audio/mpeg" />
</audio> -->
</div> </div>
</template> </template>

View File

@@ -61,11 +61,9 @@ const themeOptions: {
] ]
const modelName = computed(() => { const modelName = computed(() => {
if (!chatStore.activeConfig) if (!chatStore.activeConfig) return
return
const { modelTypeInfo, modelInfo } = chatStore.activeConfig const { modelTypeInfo, modelInfo } = chatStore.activeConfig
if (!modelTypeInfo || !modelInfo) if (!modelTypeInfo || !modelInfo) return
return
return `${modelTypeInfo?.label} / ${modelInfo.modelName}` return `${modelTypeInfo?.label} / ${modelInfo.modelName}`
}) })
@@ -85,8 +83,7 @@ function handleUpdateCollapsed() {
function onScrollToTop() { function onScrollToTop() {
const scrollRef = document.querySelector('#scrollRef') const scrollRef = document.querySelector('#scrollRef')
if (scrollRef) if (scrollRef) nextTick(() => (scrollRef.scrollTop = 0))
nextTick(() => (scrollRef.scrollTop = 0))
} }
function handleExport() { function handleExport() {
@@ -180,7 +177,7 @@ function handleSignIn() {
</div> </div>
</div> </div>
</NPopover> --> </NPopover> -->
<NTooltip v-if="isMobile" trigger="hover" :disabled="isMobile"> <NTooltip v-if="isMobile" trigger="hover" :disabled="isMobile">
<template #trigger> <template #trigger>
<button <button
@@ -199,7 +196,7 @@ function handleSignIn() {
<button <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]" 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" @click="handleExport"
v-show="!isMobile" v-show="!isMobile"
> >
<span class="text-base text-slate-500 dark:text-slate-400"> <span class="text-base text-slate-500 dark:text-slate-400">
<SvgIcon <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]" 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" @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> </button>
</template> </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]" 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" @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> </button>
</template> </template>
滚动到底部 滚动到底部

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { fetchChatDraw, fetchGetAllChatLogDraw, fetchGetChatLogDraw } from '@/ap
import { useBasicLayout } from '@/hooks/useBasicLayout' import { useBasicLayout } from '@/hooks/useBasicLayout'
import { TitleBar } from '@/components/base' import { TitleBar } from '@/components/base'
import { useAppStore, useAuthStore } from '@/store' 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' import Loading from '@/components/base/Loading.vue'
const theme = computed(() => appStore.theme) const theme = computed(() => appStore.theme)
@@ -73,14 +73,14 @@ function updateEx() {
} }
async function queryMyDrawList() { async function queryMyDrawList() {
const res: ResData = await fetchGetChatLogDraw({ model: 'DALL-E2' }) const res: ResData = await fetchGetChatLogDraw({ model: 'dall-e-3' })
if (!res.success) if (!res.success)
return return
mineDrawList.value = formatFileInfo(res.data) mineDrawList.value = formatFileInfo(res.data)
} }
async function queryAllDrawList() { 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) if (!res.success)
return ms.error(res.message) return ms.error(res.message)
allDrawList.value = formatFileInfo(res.data.rows) allDrawList.value = formatFileInfo(res.data.rows)
@@ -184,13 +184,13 @@ onMounted(() => {
参数设置 参数设置
</h4> </h4>
<div class="flex items-center mt-5"> <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> <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> <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> </div>
<div class="flex items-center mt-5"> <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> <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> <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> </div>
@@ -232,13 +232,13 @@ onMounted(() => {
<NTabs type="line" animated class="mt-5" @update:value="updateTabs"> <NTabs type="line" animated class="mt-5" @update:value="updateTabs">
<NTabPane name="all" tab="公共生成"> <NTabPane name="all" tab="公共生成">
<div v-if="allDrawList.length" class="min-h-screen"> <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> </div>
<NEmpty v-else size="huge" class="mt-20" description="暂无数据哟~" /> <NEmpty v-else size="huge" class="mt-20" description="暂无数据哟~" />
</NTabPane> </NTabPane>
<NTabPane name="mine" tab="我的生成"> <NTabPane name="mine" tab="我的生成">
<div v-if="mineDrawList.length" class="min-h-screen"> <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> </div>
<NEmpty v-else size="huge" class="mt-20" description="暂无数据哟~" /> <NEmpty v-else size="huge" class="mt-20" description="暂无数据哟~" />
</NTabPane> </NTabPane>

View File

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

View File

@@ -1,153 +1,163 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, computed } from 'vue' import { onMounted, ref, computed } from 'vue';
import { fetchMidjourneyGetList } from '@/api' import { fetchMidjourneyGetList } from '@/api';
import type { ResData } from '@/api/types' import type { ResData } from '@/api/types';
import { NSlider, NInput, NIcon } from 'naive-ui' import { NSlider, NInput, NIcon } from 'naive-ui';
import GridManager from '@/components/common/GridManager/index.vue' import GridManager2 from '@/components/common/GridManager2/index.vue';
import { FlashOutline } from '@vicons/ionicons5' import { FlashOutline } from '@vicons/ionicons5';
const imageList = ref<any>([]) const imageList = ref<any>([]);
const wapperRef = ref<HTMLDivElement | null>(null) const wapperRef = ref<HTMLDivElement | null>(null);
const scaleWidth = ref(50) const scaleWidth = ref(50);
const keyword = ref('') const keyword = ref('');
const page = ref(1) const page = ref(1);
const size = ref(20) const size = ref(20);
const loading = ref(false) const loading = ref(false);
const isMore = ref(true) 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(() => { async function queryDrawImg() {
if(!keyword.value){ loading.value = true;
return imageList.value const res: ResData = await fetchMidjourneyGetList({
}else{ page: page.value,
return imageList.value.filter((item: any) => { size: size.value,
const { prompt } = item rec: 1,
return prompt.includes(keyword.value) });
}) loading.value = false;
} isMore.value = size.value === res.data.rows.length;
}) imageList.value = [...imageList.value, ...res.data.rows];
}
async function queryDrawImg() { onMounted(async () => {
loading.value = true await queryDrawImg();
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 () => { function loadMore() {
await queryDrawImg() page.value = page.value + 1;
}) queryDrawImg();
}
</script>
function loadMore(){ <template>
page.value = page.value + 1 <div
queryDrawImg() class="bg-[#fff] h-[100vh] overflow-hidden p-4 pr-0 dark:bg-[#18181c] flex flex-col"
} >
</script> <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> <style lang="less">
<div class="bg-[#fff] h-[100vh] overflow-hidden p-4 pr-0 dark:bg-[#18181c] flex flex-col"> .market {
<div class="p-4 flex pr-6 justify-between items-center"> padding: 15px;
<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>
</template> .wapper {
width: 100%;
position: relative;
height: 100%;
padding-bottom: 20px;
<style lang="less"> &-item {
.market{ z-index: 10;
padding: 15px; overflow: hidden;
} position: absolute;
transition: all 0.5s;
cursor: pointer;
.wapper{ &:hover {
width: 100%; .menu {
position: relative; transition: transform 0.3s ease-in-out;
height: 100%; transform: translateY(-10px);
padding-bottom: 20px; }
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{ .prompt {
z-index: 10; height: 50px;
overflow: hidden; overflow: hidden;
position: absolute; }
transition: all 0.5s; }
cursor: pointer;
&:hover{ img {
.menu{ user-select: none;
transition: transform 0.3s ease-in-out; cursor: pointer;
transform: translateY(-10px); transition: all 0.6s cubic-bezier(0.19, 1, 0.22, 1);
} border-radius: 6px;
img{ }
transform: scale(1.1);
}
}
.menu{ .item-loading {
position: absolute; background: url(../../assets/img-bg.png) no-repeat center center;
bottom: 0; filter: blur(20px);
width: 94%; position: absolute;
left: 3%; top: 0;
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;
.prompt{ .img-enter-active,
height: 50px; .img-leave-active {
overflow: hidden; transition: transform 0.3s;
} }
.img-enter,
} .img-leave-to {
transform: scale(0.6);
img{ opacity: 0;
user-select: none; }
cursor: pointer; </style>
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>

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

13696
pnpm-lock.yaml generated

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

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { get_encoding } from '@dqbd/tiktoken' import { get_encoding } from '@dqbd/tiktoken'
import { removeSpecialCharacters } from '@/common/utils'; import { removeSpecialCharacters } from '@/common/utils';
import { ConsoleLogger, HttpException, HttpStatus, Logger } from '@nestjs/common';
import * as uuid from 'uuid';
const tokenizer = get_encoding('cl100k_base') const tokenizer = get_encoding('cl100k_base')
@@ -11,96 +14,166 @@ interface SendMessageResult {
detail?: any; detail?: any;
} }
function getFullUrl(proxyUrl){ function getFullUrl(proxyUrl) {
const processedUrl = proxyUrl.endsWith('/') ? proxyUrl.slice(0, -1) : proxyUrl; const processedUrl = proxyUrl.endsWith('/') ? proxyUrl.slice(0, -1) : proxyUrl;
const baseUrl = processedUrl || 'https://api.openai.com' const baseUrl = processedUrl || 'https://api.openai.com'
return `${baseUrl}/v1/chat/completions` return `${baseUrl}/v1/chat/completions`
} }
export function sendMessageFromOpenAi(messagesHistory, inputs ){ export async function sendMessageFromOpenAi(messagesHistory, inputs, uploadService?) {
const { onProgress, maxToken, apiKey, model, temperature = 0.95, proxyUrl } = inputs const { onProgress, maxToken, apiKey, model, temperature = 0.8, proxyUrl, prompt } = inputs
console.log('current request options: ',apiKey, model, maxToken, proxyUrl ); if (model.includes('dall')) {
const max_tokens = compilerToken(model, maxToken) let result: any = { text: '', imageUrl: '' };
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) =>{
try { 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 response: any = await axios(options);
const stream = response.data; const { b64_json, revised_prompt } = response.data.data[0]
let result: any = { text: '' }; const buffer = Buffer.from(b64_json, 'base64');
stream.on('data', (chunk) => { let imgUrl = '';
const splitArr = chunk.toString().split('\n\n').filter((line) => line.trim() !== ''); try {
for (const line of splitArr) { const filename = uuid.v4().slice(0, 10) + '.png';
const data = line.replace('data:', ''); Logger.debug(`------> 开始上传图片!!!`, 'MidjourneyService');
let ISEND = false; const buffer = Buffer.from(b64_json, 'base64');
try { // imgUrl = await uploadService.uploadFileFromUrl({ filename, url })
ISEND = JSON.parse(data).choices[0].finish_reason === 'stop'; imgUrl = await uploadService.uploadFile({ filename, buffer });
} catch (error) { Logger.debug(`图片上传成功URL: ${imgUrl}`, 'MidjourneyService');
ISEND = false; } catch (error) {
} Logger.error(`上传图片过程中出现错误: ${error}`, 'MidjourneyService');
/* 如果结束 返回所有 */ }
if (data === '[DONE]' || ISEND) { result.imageUrl = imgUrl
result.text = result.text.trim(); result.text = revised_prompt;
return result; onProgress && onProgress({ text: result.text })
} 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);
});
} catch (error) { } 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) { export function getTokenCount(text: string) {
if (!text) return 0; if (!text) return 0;
// 确保text是字符串类型 // 确保text是字符串类型
@@ -111,28 +184,4 @@ export function getTokenCount(text: string) {
return tokenizer.encode(text).length 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 { v4 as uuidv4 } from 'uuid';
import { get_encoding } from '@dqbd/tiktoken'; import { get_encoding } from '@dqbd/tiktoken';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { includes } from 'lodash';
const tokenizer = get_encoding('cl100k_base'); const tokenizer = get_encoding('cl100k_base');
@@ -96,7 +97,7 @@ export class NineStore implements NineStoreInterface {
let nextNumTokensEstimate = 0; let nextNumTokensEstimate = 0;
// messages.push({ role: 'system', content: systemMessage, name }) // messages.push({ role: 'system', content: systemMessage, name })
if (systemMessage) { 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)); const isSpecialModel = activeModel && specialModels.some((specialModel) => activeModel.includes(specialModel));
if (isSpecialModel) { if (isSpecialModel) {
messages.push({ role: 'user', content: systemMessage, name }); messages.push({ role: 'user', content: systemMessage, name });
@@ -146,7 +147,7 @@ export class NineStore implements NineStoreInterface {
let content = text; // 默认情况下使用text作为content let content = text; // 默认情况下使用text作为content
// 特别处理包含 imageUrl 的消息 // 特别处理包含 imageUrl 的消息
if (role === 'user' && imageUrl) { if (imageUrl) {
if (activeModel === 'gpt-4-vision-preview') { if (activeModel === 'gpt-4-vision-preview') {
content = [ content = [
{ type: 'text', text: text }, { type: 'text', text: text },

View File

@@ -13,7 +13,7 @@ interface UserInfo {
@Injectable() @Injectable()
export class DatabaseService implements OnModuleInit { export class DatabaseService implements OnModuleInit {
constructor(private connection: Connection) {} constructor(private connection: Connection) { }
async onModuleInit() { async onModuleInit() {
await this.checkSuperAdmin(); await this.checkSuperAdmin();
await this.checkSiteBaseConfig(); await this.checkSiteBaseConfig();
@@ -23,7 +23,7 @@ export class DatabaseService implements OnModuleInit {
async checkSuperAdmin() { async checkSuperAdmin() {
const user = await this.connection.query(`SELECT * FROM users WHERE role = 'super'`); const user = await this.connection.query(`SELECT * FROM users WHERE role = 'super'`);
if (!user || user.length === 0) { if (!user || user.length === 0) {
const superPassword = bcrypt.hashSync('123456', 10); const superPassword = bcrypt.hashSync('123456', 10); //初始密码
const adminPassword = bcrypt.hashSync('123456', 10); const adminPassword = bcrypt.hashSync('123456', 10);
const superEmail = 'default@cooper.com'; const superEmail = 'default@cooper.com';
const adminEmail = 'defaultAdmin@cooper.com'; const adminEmail = 'defaultAdmin@cooper.com';
@@ -44,7 +44,7 @@ export class DatabaseService implements OnModuleInit {
const userId = user.insertId; const userId = user.insertId;
const balance = await this.connection.query(`INSERT INTO balance (userId, balance, usesLeft, paintCount) VALUES ('${userId}', 0, 1000, 100)`); const balance = await this.connection.query(`INSERT INTO balance (userId, balance, usesLeft, paintCount) VALUES ('${userId}', 0, 1000, 100)`);
Logger.log( Logger.log(
`初始化创建${role}用户成功、用户名为[${username}]、初始密码为[${username === 'super' ? '123456' : '123456'}] ==============> 请注意查阅`, `初始化创建${role}用户成功、用户名为[${username}]、初始密码为[${username === 'super' ? 'nine-super' : '123456'}] ==============> 请注意查阅`,
'DatabaseService', 'DatabaseService',
); );
} catch (error) { } catch (error) {
@@ -68,17 +68,7 @@ export class DatabaseService implements OnModuleInit {
/* 创建基础的网站数据 */ /* 创建基础的网站数据 */
async createBaseSiteConfig() { async createBaseSiteConfig() {
try { try {
const code = ` 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 noticeInfo = ` const noticeInfo = `
#### YiAi 欢迎您 #### YiAi 欢迎您
@@ -88,21 +78,21 @@ export class DatabaseService implements OnModuleInit {
`; `;
const defaultConfig = [ const defaultConfig = [
{ configKey: 'siteName', configVal: 'Nine Ai', public: 1, encry: 0 }, { configKey: 'siteName', configVal: 'Yi Ai', public: 1, encry: 0 },
{ configKey: 'qqNumber', configVal: '840814166', public: 1, encry: 0 }, { configKey: 'qqNumber', configVal: '805239273', public: 1, encry: 0 },
{ configKey: 'vxNumber', configVal: 'wangpanzhu321', public: 1, encry: 0 }, { configKey: 'vxNumber', configVal: 'HelloWordYi819', public: 1, encry: 0 },
{ configKey: 'robotAvatar', configVal: '', public: 1, encry: 0 }, { configKey: 'robotAvatar', configVal: '', public: 1, encry: 0 },
{ {
configKey: 'userDefautlAvatar', configKey: 'userDefautlAvatar',
configVal: 'https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1682571295452image.png', configVal: '',
public: 0, public: 0,
encry: 0, encry: 0,
}, },
{ configKey: 'baiduCode', configVal: code, public: 1, 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', configKey: 'baiduToken',
configVal: '121.a1600b9b60910feea2ef627ea9776a6f.YGP_CWCOA2lNcIGJ27BwXGxa6nZhBQyLUS4XVaD.TWt9TA', configVal: '',
public: 0, public: 0,
encry: 0, encry: 0,
}, },
@@ -110,25 +100,25 @@ export class DatabaseService implements OnModuleInit {
{ configKey: 'openaiBaseUrl', configVal: 'https://api.openai.com', public: 0, encry: 0 }, { configKey: 'openaiBaseUrl', configVal: 'https://api.openai.com', public: 0, encry: 0 },
{ configKey: 'noticeInfo', configVal: noticeInfo, public: 1, 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', configKey: 'registerVerifyEmailDesc',
configVal: '欢迎使用Nine Team团队的产品服务,请在五分钟内完成你的账号激活,点击以下按钮激活您的账号,', configVal: '欢迎使用Yi Ai团队的产品服务,请在五分钟内完成你的账号激活,点击以下按钮激活您的账号,',
public: 0, public: 0,
encry: 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: 'registerVerifyExpir', configVal: '1800', public: 0, encry: 0 },
{ configKey: 'registerSuccessEmailTitle', configVal: 'NineTeam团队账号激活成功', public: 0, encry: 0 }, { configKey: 'registerSuccessEmailTitle', configVal: 'Yi Ai账号激活成功', public: 0, encry: 0 },
{ configKey: 'registerSuccessEmailTeamName', configVal: 'NineTeam团队', public: 0, encry: 0 }, { configKey: 'registerSuccessEmailTeamName', configVal: 'Yi Ai', public: 0, encry: 0 },
{ {
configKey: 'registerSuccessEmaileAppend', configKey: 'registerSuccessEmaileAppend',
configVal: ',请妥善保管您的账号,我们将为您赠送50次对话额度和5次绘画额度、祝您使用愉快', configVal: ',请妥善保管您的账号,祝您使用愉快',
public: 0, public: 0,
encry: 0, encry: 0,
}, },
{ configKey: 'registerFailEmailTitle', configVal: 'NineTeam账号激活失败', public: 0, encry: 0 }, { configKey: 'registerFailEmailTitle', configVal: 'Yi Ai账号激活失败', public: 0, encry: 0 },
{ configKey: 'registerFailEmailTeamName', configVal: 'NineTeam团队', public: 0, encry: 0 }, { configKey: 'registerFailEmailTeamName', configVal: 'Yi Ai团队', public: 0, encry: 0 },
/* 注册默认设置 */ /* 注册默认设置 */
{ configKey: 'registerSendStatus', configVal: '1', public: 1, encry: 0 }, { configKey: 'registerSendStatus', configVal: '1', public: 1, encry: 0 },
{ configKey: 'registerSendModel3Count', configVal: '30', 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: 'registerSendDrawMjCount', configVal: '3', public: 1, encry: 0 },
{ configKey: 'firstRegisterSendStatus', configVal: '1', public: 1, encry: 0 }, { configKey: 'firstRegisterSendStatus', configVal: '1', public: 1, encry: 0 },
{ configKey: 'firstRegisterSendRank', configVal: '500', public: 1, encry: 0 }, { configKey: 'firstRegisterSendRank', configVal: '500', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendModel3Count', configVal: '20', public: 1, encry: 0 }, { configKey: 'firstRregisterSendModel3Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendModel4Count', configVal: '2', public: 1, encry: 0 }, { configKey: 'firstRregisterSendModel4Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'firstRregisterSendDrawMjCount', configVal: '3', public: 1, encry: 0 }, { configKey: 'firstRregisterSendDrawMjCount', configVal: '10', public: 1, encry: 0 },
{ configKey: 'inviteSendStatus', configVal: '1', public: 1, encry: 0 }, { configKey: 'inviteSendStatus', configVal: '1', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendModel3Count', configVal: '30', public: 1, encry: 0 }, { configKey: 'inviteGiveSendModel3Count', configVal: '0', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendModel4Count', configVal: '3', public: 1, encry: 0 }, { configKey: 'inviteGiveSendModel4Count', configVal: '0', public: 1, encry: 0 },
{ configKey: 'inviteGiveSendDrawMjCount', configVal: '1', public: 1, encry: 0 }, { configKey: 'inviteGiveSendDrawMjCount', configVal: '0', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendModel3Count', configVal: '10', public: 1, encry: 0 }, { configKey: 'invitedGuestSendModel3Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendModel4Count', configVal: '1', public: 1, encry: 0 }, { configKey: 'invitedGuestSendModel4Count', configVal: '10', public: 1, encry: 0 },
{ configKey: 'invitedGuestSendDrawMjCount', configVal: '1', public: 1, encry: 0 }, { configKey: 'invitedGuestSendDrawMjCount', configVal: '10', public: 1, encry: 0 },
{ configKey: 'isVerifyEmail', configVal: '1', 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' }) @Column({ comment: '垫图图片 + 绘画描述词 + 额外参数 = 完整的prompt', type: 'text' })
fullPrompt: string; fullPrompt: string;
@Column({ comment: '随机产生的绘画ID用于拿取比对结果' })
randomDrawId: string;
@Column({ comment: '当前绘制任务的进度', nullable: true }) @Column({ comment: '当前绘制任务的进度', nullable: true })
progress: number; progress: number;
@@ -35,7 +32,7 @@ export class MidjourneyEntity extends BaseEntity {
status: number; status: number;
@Column({ comment: 'mj绘画的动作、绘图、放大、变换、图生图' }) @Column({ comment: 'mj绘画的动作、绘图、放大、变换、图生图' })
action: number; action: string;
@Column({ comment: '一组图片的第几张、放大或者变换的时候需要使用', nullable: true }) @Column({ comment: '一组图片的第几张、放大或者变换的时候需要使用', nullable: true })
orderId: number; orderId: number;
@@ -43,14 +40,17 @@ export class MidjourneyEntity extends BaseEntity {
@Column({ comment: '是否推荐0: 默认不推荐 1: 推荐', nullable: true, default: 0 }) @Column({ comment: '是否推荐0: 默认不推荐 1: 推荐', nullable: true, default: 0 })
rec: number; rec: number;
@Column({ comment: '对图片操作的', nullable: true })
customId: string;
@Column({ comment: '绘画的ID每条不一样', nullable: true }) @Column({ comment: '绘画的ID每条不一样', nullable: true })
message_id: string; drawId: string;
@Column({ comment: '图片放大或者变体的ID', nullable: true }) @Column({ comment: '图片链接', nullable: true, type: 'text' })
custom_id: string; drawUrl: string;
@Column({ comment: '图片信息尺寸', nullable: true, type: 'text' }) @Column({ comment: '图片比例', nullable: true, type: 'text' })
fileInfo: string; drawRatio: string;
@Column({ comment: '扩展参数', nullable: true, type: 'text' }) @Column({ comment: '扩展参数', nullable: true, type: 'text' })
extend: string; extend: string;
@@ -60,4 +60,5 @@ export class MidjourneyEntity extends BaseEntity {
@Column({ comment: '是否存入了图片到配置的储存项 配置了则存储 不配置地址则是源地址', default: true }) @Column({ comment: '是否存入了图片到配置的储存项 配置了则存储 不配置地址则是源地址', default: true })
isSaveImg: boolean; 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 { IsNotEmpty, MinLength, MaxLength, IsEmail, IsOptional, IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
@@ -27,6 +27,9 @@ export class SetModelDto {
@ApiProperty({ example: 1, description: 'key的权重' }) @ApiProperty({ example: 1, description: 'key的权重' })
keyWeight: number; keyWeight: number;
@ApiProperty({ example: 1, description: '模型排序' })
modelOrder: number;
@ApiProperty({ example: 4096, description: '模型支持的最大TOken数量', required: true }) @ApiProperty({ example: 4096, description: '模型支持的最大TOken数量', required: true })
maxModelTokens: number; maxModelTokens: number;
@@ -53,9 +56,10 @@ export class SetModelDto {
@ApiProperty({ example: true, description: '是否设置为绘画Key', required: false }) @ApiProperty({ example: true, description: '是否设置为绘画Key', required: false })
isDraw: boolean; isDraw: boolean;
//设置token计费
@ApiProperty({ example: true, description: '是否使用token计费', required: false }) @ApiProperty({ example: true, description: '是否使用token计费', required: false })
isTokenBased: boolean; isTokenBased: boolean;
@ApiProperty({ example: true, description: 'token计费比例', required: false }) @ApiProperty({ example: true, description: 'token计费比例', required: false })
tokenFeeRatio: number; tokenFeeRatio: number;
} }

View File

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

View File

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

View File

@@ -20,9 +20,9 @@ export class MjDrawDto {
@IsOptional() @IsOptional()
imgUrl?: string; imgUrl?: string;
@ApiProperty({ example: 1, description: '绘画动作 绘图、放大、变换、图生图' }) @ApiProperty({ example: 'IMAGINE', description: '任务类型,可用值:IMAGINE,UPSCALE,VARIATION,ZOOM,PAN,DESCRIBE,BLEND,SHORTEN,SWAP_FACE' })
@IsOptional() @IsOptional()
action: number; action: string;
@ApiProperty({ example: 1, description: '变体或者放大的序号' }) @ApiProperty({ example: 1, description: '变体或者放大的序号' })
@IsOptional() @IsOptional()
@@ -31,4 +31,8 @@ export class MjDrawDto {
@ApiProperty({ example: 1, description: '绘画的DBID' }) @ApiProperty({ example: 1, description: '绘画的DBID' })
@IsOptional() @IsOptional()
drawId: number; 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 midjourneyService: MidjourneyService,
private readonly userBalanceService: UserBalanceService, private readonly userBalanceService: UserBalanceService,
private readonly globalConfigService: GlobalConfigService, private readonly globalConfigService: GlobalConfigService,
) {} ) { }
private readonly jobIds: any[] = []; private readonly jobIds: any[] = [];
async onApplicationBootstrap() { async onApplicationBootstrap() {
@@ -27,14 +27,13 @@ export class QueueService implements OnApplicationBootstrap {
/* 提交绘画任务 */ /* 提交绘画任务 */
async addMjDrawQueue(body: MjDrawDto, req: Request) { 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.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即可 */ /* 绘图或者图生图是相同的 区分一个action即可 */
const randomDrawId = `${createRandomUid()}`; const randomDrawId = `${createRandomUid()}`;
const params = { ...body, userId: req.user.id, randomDrawId }; const params = { ...body, userId: req.user.id, randomDrawId };
@@ -44,84 +43,28 @@ export class QueueService implements OnApplicationBootstrap {
/* 添加任务到队列 通过imgUrl判断是不是图生图 */ /* 添加任务到队列 通过imgUrl判断是不是图生图 */
const job = await this.mjDrawQueue.add( const job = await this.mjDrawQueue.add(
'mjDraw', '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 }, { delay: 1000, timeout: +timeout },
); );
/* 绘图和图生图扣除余额4 */ /* 绘图和图生图扣除余额4 */
this.jobIds.push(job.id); this.jobIds.push(job.id);
/* 扣费 */
// await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return true; 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) { if (!drawId || !orderId) {
throw new HttpException('缺少必要参数!', HttpStatus.BAD_REQUEST); 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 { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule'; import { Cron, CronExpression } from '@nestjs/schedule';
import { UserBalanceEntity } from '../userBalance/userBalance.entity'; import { UserBalanceEntity } from '../userBalance/userBalance.entity';
@@ -13,7 +13,7 @@ export class TaskService {
private readonly userBalanceEntity: Repository<UserBalanceEntity>, private readonly userBalanceEntity: Repository<UserBalanceEntity>,
private readonly globalConfigService: GlobalConfigService, private readonly globalConfigService: GlobalConfigService,
private readonly modelsService: ModelsService, private readonly modelsService: ModelsService,
) {} ) { }
/* 每小时刷新一次微信的token */ /* 每小时刷新一次微信的token */
@Cron(CronExpression.EVERY_HOUR) @Cron(CronExpression.EVERY_HOUR)
@@ -40,8 +40,8 @@ export class TaskService {
} }
/* 每小时检测一次授权 */ /* 每小时检测一次授权 */
@Cron('0 0 */5 * *') // @Cron('0 0 */5 * *')
refreshBaiduAccesstoken() { // refreshBaiduAccesstoken() {
this.modelsService.refreshBaiduAccesstoken(); // this.modelsService.refreshBaiduAccesstoken();
} // }
} }

View File

@@ -10,33 +10,46 @@ import * as FormData from 'form-data';
@Injectable() @Injectable()
export class UploadService implements OnModuleInit { export class UploadService implements OnModuleInit {
constructor(private readonly globalConfigService: GlobalConfigService) {} constructor(private readonly globalConfigService: GlobalConfigService) { }
private tencentCos: any; private tencentCos: any;
onModuleInit() {} onModuleInit() { }
async uploadFile(file) { async uploadFile(file) {
const { filename: name, originalname, buffer, dir = 'ai', mimetype } = file; const { filename: name, originalname, buffer, dir = 'ai', mimetype } = file;
const fileTyle = mimetype ? mimetype.split('/')[1] : ''; const fileTyle = mimetype ? mimetype.split('/')[1] : '';
const filename = originalname || name const filename = originalname || name
Logger.debug(`准备上传文件: ${filename}, 类型: ${fileTyle}`, 'UploadService');
const { const {
tencentCosStatus = 0, tencentCosStatus = 0,
aliOssStatus = 0, aliOssStatus = 0,
cheveretoStatus = 0, cheveretoStatus = 0,
} = await this.globalConfigService.getConfigs(['tencentCosStatus', 'aliOssStatus', 'cheveretoStatus']); } = await this.globalConfigService.getConfigs(['tencentCosStatus', 'aliOssStatus', 'cheveretoStatus']);
Logger.debug(`上传配置状态 - 腾讯云: ${tencentCosStatus}, 阿里云: ${aliOssStatus}, Chevereto: ${cheveretoStatus}`, 'UploadService');
if (!Number(tencentCosStatus) && !Number(aliOssStatus) && !Number(cheveretoStatus)) { if (!Number(tencentCosStatus) && !Number(aliOssStatus) && !Number(cheveretoStatus)) {
throw new HttpException('请先前往后台配置上传图片的方式', HttpStatus.BAD_REQUEST); throw new HttpException('请先前往后台配置上传图片的方式', HttpStatus.BAD_REQUEST);
} }
if (Number(tencentCosStatus)) { try {
return this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle }); if (Number(tencentCosStatus)) {
} Logger.debug(`使用腾讯云COS上传`, 'UploadService');
if (Number(aliOssStatus)) { return await this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle });
return await this.uploadFileByAliOss({ filename, buffer, dir, fileTyle }); }
} if (Number(aliOssStatus)) {
if (Number(cheveretoStatus)) { Logger.debug(`使用阿里云OSS上传`, 'UploadService');
const { filename, buffer: fromBuffer, dir } = file; return await this.uploadFileByAliOss({ filename, buffer, dir, fileTyle });
return await this.uploadFileByChevereto({ filename, buffer: fromBuffer.toString('base64'), 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 }); this.tencentCos = new TENCENTCOS({ SecretId, SecretKey, FileParallelLimit: 10 });
try { try {
const proxyMj = (await this.globalConfigService.getConfigs(['mjProxy'])) || 0; const proxyMj = (await this.globalConfigService.getConfigs(['mjProxy'])) || 0;
/* 开启代理 */
if (Number(proxyMj) === 1) { const buffer = await this.getBufferFromUrl(url);
const data = { cosType: 'tencent', url, cosParams: { Bucket, Region, SecretId, SecretKey } }; return await this.uploadFileByTencentCos({ filename, buffer, dir, fileTyle: '' });
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: '' });
}
} catch (error) { } catch (error) {
console.log('TODO->error: ', error); console.log('TODO->error: ', error);
throw new HttpException('上传图片失败[ten][url]', HttpStatus.BAD_REQUEST); throw new HttpException('上传图片失败[ten][url]', HttpStatus.BAD_REQUEST);