新增gpt-4V上传 修复部分bug

This commit is contained in:
小易 2024-01-28 18:41:04 +08:00
parent 6dc767f009
commit 8db214371a
26 changed files with 1043 additions and 685 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,6 +1,6 @@
# 本地链接生产# xxx填写你的后端服务地址后面/api勿删除 # 页面标题
VITE_GLOB_API_URL=https://xxx/api VITE_APP_TITLE = YiAi-Admin
# 接口请求地址,会设置到 axios 的 baseURL 参数上 生产地址测试
VITE_APP_API_BASEURL = https://xxxx/api
VITE_GLOB_OPEN_LONG_REPLY=false VITE_BASE_PATH=/
VITE_GLOB_APP_PWA=false

View File

@ -1,5 +1,5 @@
{ {
"version": "2.4.5", "version": "2.4.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

@ -1,305 +1,350 @@
export const USER_STATUS_OPTIONS = [ export const USER_STATUS_OPTIONS = [
{ value: 0, label: '待激活' }, { value: 0, label: "待激活" },
{ value: 1, label: '正常' }, { value: 1, label: "正常" },
{ value: 2, label: '已封禁' }, { value: 2, label: "已封禁" },
{ value: 3, label: '黑名单' }, { value: 3, label: "黑名单" },
] ];
export const USER_STATUS_MAP = { export const USER_STATUS_MAP = {
0: '待激活', 0: "待激活",
1: '正常', 1: "正常",
2: '已封禁', 2: "已封禁",
3: '黑名单', 3: "黑名单",
} };
export const USER_STATUS_TYPE_MAP = { export const USER_STATUS_TYPE_MAP = {
0: 'info', 0: "info",
1: 'success', 1: "success",
2: 'danger', 2: "danger",
3: 'danger', 3: "danger",
} };
// 充值类型map 1: 注册赠送 2: 受邀请赠送 3: 邀请人赠送 4: 购买套餐赠送 5: 管理员赠送 6扫码支付 7: 绘画失败退款 8: 签到奖励 // 充值类型map 1: 注册赠送 2: 受邀请赠送 3: 邀请人赠送 4: 购买套餐赠送 5: 管理员赠送 6扫码支付 7: 绘画失败退款 8: 签到奖励
export const RECHARGE_TYPE_MAP = { export const RECHARGE_TYPE_MAP = {
1: '注册赠送', 1: "注册赠送",
2: '受邀请赠送', 2: "受邀请赠送",
3: '邀请人赠送', 3: "邀请人赠送",
4: '购买套餐赠送', 4: "购买套餐赠送",
5: '管理员赠送', 5: "管理员赠送",
6: '扫码支付', 6: "扫码支付",
7: '绘画失败退款', 7: "绘画失败退款",
8: '签到奖励', 8: "签到奖励",
} };
// 充值数组 // 充值数组
export const RECHARGE_TYPE_OPTIONS = [ export const RECHARGE_TYPE_OPTIONS = [
{ value: 1, label: '注册赠送' }, { value: 1, label: "注册赠送" },
{ value: 2, label: '受邀请赠送' }, { value: 2, label: "受邀请赠送" },
{ value: 3, label: '邀请人赠送' }, { value: 3, label: "邀请人赠送" },
{ value: 4, label: '购买套餐赠送' }, { value: 4, label: "购买套餐赠送" },
{ value: 5, label: '管理员赠送' }, { value: 5, label: "管理员赠送" },
{ value: 6, label: '扫码支付' }, { value: 6, label: "扫码支付" },
{ value: 7, label: '绘画失败退款' }, { value: 7, label: "绘画失败退款" },
{ value: 8, label: '签到奖励' }, { value: 8, label: "签到奖励" },
] ];
export type UserStatus = keyof typeof USER_STATUS_TYPE_MAP export type UserStatus = keyof typeof USER_STATUS_TYPE_MAP;
// 是否开启额外赠送 // 是否开启额外赠送
export const IS_OPTIONS = { export const IS_OPTIONS = {
0: '关闭', 0: "关闭",
1: '开启', 1: "开启",
} };
// 是否开启额外赠送类型 // 是否开启额外赠送类型
export const IS_TYPE_MAP = { export const IS_TYPE_MAP = {
0: 'danger', 0: "danger",
1: 'success', 1: "success",
} };
export const PACKAGE_TYPE_OPTIONS = [ export const PACKAGE_TYPE_OPTIONS = [
{ value: 0, label: '禁用' }, { value: 0, label: "禁用" },
{ value: 1, label: '启动' }, { value: 1, label: "启动" },
] ];
// 扣费形式 1 按次数扣费 2按Token扣费 // 扣费形式 1 按次数扣费 2按Token扣费
export const DEDUCTION_TYPE_OPTIONS = [ export const DEDUCTION_TYPE_OPTIONS = [
{ value: 1, label: '按次数扣费' }, { value: 1, label: "按次数扣费" },
{ value: 2, label: '按Token扣费' }, { value: 2, label: "按Token扣费" },
] ];
// 扣费形式 map // 扣费形式 map
export const DEDUCTION_TYPE_MAP = { export const DEDUCTION_TYPE_MAP = {
1: '按次数扣费', 1: "按次数扣费",
2: '按Token扣费', 2: "按Token扣费",
} };
export const CRAMI_STATUS_OPTIONS = [ export const CRAMI_STATUS_OPTIONS = [
{ value: 0, label: '未使用' }, { value: 0, label: "未使用" },
{ value: 1, label: '已使用' }, { value: 1, label: "已使用" },
] ];
// 图片推荐状态0未推荐1已推荐 // 图片推荐状态0未推荐1已推荐
export const RECOMMEND_STATUS_OPTIONS = [ export const RECOMMEND_STATUS_OPTIONS = [
{ value: 0, label: '未推荐' }, { value: 0, label: "未推荐" },
{ value: 1, label: '已推荐' }, { value: 1, label: "已推荐" },
] ];
// 0 禁用 1 启用 // 0 禁用 1 启用
export const ENABLE_STATUS_OPTIONS = [ export const ENABLE_STATUS_OPTIONS = [
{ value: 0, label: '禁用' }, { value: 0, label: "禁用" },
{ value: 1, label: '启用' }, { value: 1, label: "启用" },
{ value: 3, label: '待审核' }, { value: 3, label: "待审核" },
{ value: 4, label: '拒绝共享' }, { value: 4, label: "拒绝共享" },
{ value: 5, label: '通过共享' }, { value: 5, label: "通过共享" },
] ];
// 问题状态 0 未解决 1 已解决 // 问题状态 0 未解决 1 已解决
export const QUESTION_STATUS_OPTIONS = [ export const QUESTION_STATUS_OPTIONS = [
{ value: '0', label: '未启用' }, { value: "0", label: "未启用" },
{ value: '1', label: '已启用' }, { value: "1", label: "已启用" },
] ];
// 问题状态 0 未解决 1 已解决 // 问题状态 0 未解决 1 已解决
export const ORDER_STATUS_OPTIONS = [ export const ORDER_STATUS_OPTIONS = [
{ value: 0, label: '待审核' }, { value: 0, label: "待审核" },
{ value: 1, label: '已通过' }, { value: 1, label: "已通过" },
{ value: -1, label: '已拒绝' }, { value: -1, label: "已拒绝" },
] ];
// 0未推荐 1已推荐 数组 // 0未推荐 1已推荐 数组
export const RECOMMEND_STATUS = [ export const RECOMMEND_STATUS = [
{ value: 0, label: '未推荐' }, { value: 0, label: "未推荐" },
{ value: 1, label: '已推荐' }, { value: 1, label: "已推荐" },
] ];
// 提现渠道 支付宝 微信 // 提现渠道 支付宝 微信
export const WITHDRAW_CHANNEL_OPTIONS = [ export const WITHDRAW_CHANNEL_OPTIONS = [
{ value: 1, label: '支付宝' }, { value: 1, label: "支付宝" },
{ value: 2, label: '微信' }, { value: 2, label: "微信" },
] ];
// 1 排队中 2 处理中 3 已完成 4 失败 5 超时 // 1 排队中 2 处理中 3 已完成 4 失败 5 超时
export const WITHDRAW_STATUS_OPTIONS = [ export const WITHDRAW_STATUS_OPTIONS = [
{ value: 1, label: '正在排队' }, { value: 1, label: "正在排队" },
{ value: 2, label: '正在绘制' }, { value: 2, label: "正在绘制" },
{ value: 3, label: '绘制完成' }, { value: 3, label: "绘制完成" },
{ value: 4, label: '绘制失败' }, { value: 4, label: "绘制失败" },
{ value: 5, label: '绘制超时' }, { value: 5, label: "绘制超时" },
] ];
// 0 禁用 warning 1启用 状态 success // 0 禁用 warning 1启用 状态 success
export const ENABLE_STATUS_TYPE_MAP: QuestionStatusMap = { export const ENABLE_STATUS_TYPE_MAP: QuestionStatusMap = {
0: 'danger', 0: "danger",
1: 'success', 1: "success",
} };
interface QuestionStatusMap { interface QuestionStatusMap {
[key: number]: string [key: number]: string;
} }
// 问题状态 0 未解决 1 已解决 映射 // 问题状态 0 未解决 1 已解决 映射
export const QUESTION_STATUS_MAP: QuestionStatusMap = { export const QUESTION_STATUS_MAP: QuestionStatusMap = {
'-1': '欠费锁定', "-1": "欠费锁定",
'0': '未启用', "0": "未启用",
'1': '已启用', "1": "已启用",
'3': '待审核', "3": "待审核",
'4': '拒绝共享', "4": "拒绝共享",
'5': '通过共享', "5": "通过共享",
} };
// 问题状态 0 被封号 1 正常 映射 // 问题状态 0 被封号 1 正常 映射
export const KEY_STATUS_MAP: QuestionStatusMap = { export const KEY_STATUS_MAP: QuestionStatusMap = {
0: '被封禁', 0: "被封禁",
1: '工作中', 1: "工作中",
} };
// 账号类型 5$ 18$ 120$ // 账号类型 5$ 18$ 120$
export const ACCOUNT_TYPE_MAP: QuestionStatus = [ export const ACCOUNT_TYPE_MAP: QuestionStatus = [
{ value: '5$', label: '5$' }, { value: "5$", label: "5$" },
{ value: '18$', label: '18$' }, { value: "18$", label: "18$" },
{ value: '120$', label: '120$' }, { value: "120$", label: "120$" },
{ value: '其他', label: '其他' }, { value: "其他", label: "其他" },
] ];
// 模型列表 // 模型列表
export const MODEL_LIST = [ export const MODEL_LIST = [
'gpt-3.5-turbo', "gpt-3.5-turbo",
'gpt-3.5-turbo-1106', "gpt-3.5-turbo-1106",
'gpt-3.5-turbo-16k', "gpt-3.5-turbo-16k",
'gpt-4', "gpt-4",
'gpt-4-0613', "gpt-4-0613",
'gpt-4-32k', "gpt-4-32k",
'gpt-4-32k-0613', "gpt-4-32k-0613",
'gpt-4-1106-preview', "gpt-4-1106-preview",
'gpt-4-vision-preview', "gpt-4-vision-preview",
'gpt-4-all', "gpt-4-all",
'gpt-4-0125-preview', "gpt-4-0125-preview",
] // claude
"claude-2.0",
"claude-2.1",
// gemini
"gemini-pro",
// 百度文心
"ERNIE-Bot",
"ERNIE-Bot-4",
"ERNIE-Bot-turbo",
// 阿里通义
"qwen-turbo",
"qwen-plus",
"qwen-max",
"qwen-max-lingcontext",
// 腾讯混元
"hunyuan",
// 清华智谱
"chatglm_turbo",
"chatglm_pro",
"chatglm_std",
"chatglm_lite",
// 360 智脑
"360GPT_S2_V9",
// 讯飞星火
"SparkDesk",
];
// 模型列表 0 mj 1 Dall-e // 模型列表 0 mj 1 Dall-e
export const DRAW_MODEL_LIST = [ export const DRAW_MODEL_LIST = [
{ value: 'mj', label: 'MidjourneyAi' }, { value: "mj", label: "MidjourneyAi" },
{ value: 'DALL-E2', label: 'DALL-E' }, { value: "DALL-E2", label: "DALL-E" },
] ];
// 支付状态列表 status 0未支付、1已支付、2、支付失败、3支付超时 // 支付状态列表 status 0未支付、1已支付、2、支付失败、3支付超时
export const PAY_STATUS_OPTIONS = [ export const PAY_STATUS_OPTIONS = [
{ value: 0, label: '未支付' }, { value: 0, label: "未支付" },
{ value: 1, label: '已支付' }, { value: 1, label: "已支付" },
{ value: 2, label: '支付失败' }, { value: 2, label: "支付失败" },
{ value: 3, label: '支付超时' }, { value: 3, label: "支付超时" },
] ];
// 支付状态 status 0未支付、1已支付、2、支付失败、3支付超时 // 支付状态 status 0未支付、1已支付、2、支付失败、3支付超时
export const PAY_STATUS_MAP: QuestionStatusMap = { export const PAY_STATUS_MAP: QuestionStatusMap = {
0: '未支付', 0: "未支付",
1: '已支付', 1: "已支付",
2: '支付失败', 2: "支付失败",
3: '支付超时', 3: "支付超时",
} };
// 平台列表 epay: 易支付 hupi虎皮椒 // 平台列表 epay: 易支付 hupi虎皮椒
export const PAY_PLATFORM_LIST = [ export const PAY_PLATFORM_LIST = [
{ value: 'epay', label: '易支付' }, { value: "epay", label: "易支付" },
{ value: 'hupi', label: '虎皮椒' }, { value: "hupi", label: "虎皮椒" },
{ value: 'wechat', label: '微信支付' }, { value: "wechat", label: "微信支付" },
{ value: 'mpay', label: '码支付' }, { value: "mpay", label: "码支付" },
] ];
// 支付对应 // 支付对应
export const PAY_PLATFORM_MAP = { export const PAY_PLATFORM_MAP = {
epay: '易支付', epay: "易支付",
hupi: '虎皮椒', hupi: "虎皮椒",
wechat: '微信支付', wechat: "微信支付",
mpay: '码支付', mpay: "码支付",
} };
// 绘画状态 1: 等待中 2: 绘制中 3: 绘制完成 4: 绘制失败 5: 绘制超时 // 绘画状态 1: 等待中 2: 绘制中 3: 绘制完成 4: 绘制失败 5: 绘制超时
export const DRAW_MJ_STATUS_LIST = [ export const DRAW_MJ_STATUS_LIST = [
{ value: 1, label: '等待中' }, { value: 1, label: "等待中" },
{ value: 2, label: '绘制中' }, { value: 2, label: "绘制中" },
{ value: 3, label: '绘制完成' }, { value: 3, label: "绘制完成" },
{ value: 4, label: '绘制失败' }, { value: 4, label: "绘制失败" },
{ value: 5, label: '绘制超时' }, { value: 5, label: "绘制超时" },
] ];
// App角色 系统 system 用户 user // App角色 系统 system 用户 user
export const APP_ROLE_LIST = [ export const APP_ROLE_LIST = [
{ value: 'system', label: '系统' }, { value: "system", label: "系统" },
{ value: 'user', label: '用户' }, { value: "user", label: "用户" },
] ];
// 绘画状态 1排队中 2绘制中 3绘制完成 4绘制失败 5绘制超时 // 绘画状态 1排队中 2绘制中 3绘制完成 4绘制失败 5绘制超时
export const DRAW_STATUS_MAP = { export const DRAW_STATUS_MAP = {
1: '排队中', 1: "排队中",
2: '绘制中', 2: "绘制中",
3: '绘制完成', 3: "绘制完成",
4: '绘制失败', 4: "绘制失败",
5: '绘制超时', 5: "绘制超时",
} };
export const TYPEORIGINLIST = [ export const TYPEORIGINLIST = [
{ value: '百度云检测', label: '百度云检测' }, { value: "百度云检测", label: "百度云检测" },
{ value: '自定义检测', label: '自定义检测' }, { value: "自定义检测", label: "自定义检测" },
{ value: 'NineAI检测', label: 'NineAI检测' }, { value: "NineAI检测", label: "NineAI检测" },
] ];
export const MODELTYPELIST = [ export const MODELTYPELIST = [
{ value: 1, label: 'OpenAi - [chatGpt]' }, { value: 1, label: "OpenAi - [chatGpt]" },
{ value: 2, label: '百度 - [千帆大模型]' }, { value: 2, label: "百度 - [千帆大模型]" },
{ value: 3, label: '清华 - [智谱大模型]' }, { value: 3, label: "清华 - [智谱大模型]" },
] ];
export const MODELSMAP = { export const MODELSMAP = {
1: 'OPENAI', 1: "OPENAI",
2: '百度文心', 2: "百度文心",
3: '清华智谱', 3: "清华智谱",
} };
export const MODELSMAPLIST = { export const MODELSMAPLIST = {
1: [ 1: [
'gpt-3.5-turbo', "gpt-3.5-turbo",
'gpt-3.5-turbo-1106', "gpt-3.5-turbo-1106",
'gpt-3.5-turbo-16k', "gpt-3.5-turbo-16k",
'gpt-4', "gpt-4",
'gpt-4-0613', "gpt-4-0613",
'gpt-4-32k', "gpt-4-32k",
'gpt-4-32k-0613', "gpt-4-32k-0613",
'gpt-4-1106-preview', "gpt-4-1106-preview",
'gpt-4-vision-preview', "gpt-4-vision-preview",
'gpt-4-all', "gpt-4-all",
'gpt-4-0125-preview', "gpt-4-0125-preview",
// claude
"claude-2.0",
"claude-2.1",
// gemini
"gemini-pro",
// 百度文心
"ERNIE-Bot",
"ERNIE-Bot-4",
"ERNIE-Bot-turbo",
// 阿里通义
"qwen-turbo",
"qwen-plus",
"qwen-max",
"qwen-max-lingcontext",
// 腾讯混元
"hunyuan",
// 清华智谱
"chatglm_turbo",
"chatglm_pro",
"chatglm_std",
"chatglm_lite",
// 360 智脑
"360GPT_S2_V9",
// 讯飞星火
"SparkDesk",
], ],
2: [ 2: [
'ERNIE-Bot', "ERNIE-Bot",
'ERNIE-Bot', "ERNIE-Bot",
'ERNIE-Bot-4', "ERNIE-Bot-4",
'ERNIE-Bot-turbo', "ERNIE-Bot-turbo",
'BLOOMZ-7B', "BLOOMZ-7B",
'Llama-2-7b-chat', "Llama-2-7b-chat",
'Llama-2-13b-chat', "Llama-2-13b-chat",
// 'Llama-2-70b-chat', // 'Llama-2-70b-chat',
// 'ChatGLM2-6B-32K', // 'ChatGLM2-6B-32K',
'Qianfan-BLOOMZ-7B-compressed', "Qianfan-BLOOMZ-7B-compressed",
'Qianfan-Chinese-Llama-2-7B', "Qianfan-Chinese-Llama-2-7B",
'AquilaChat-7B', "AquilaChat-7B",
], ],
3: [ 3: ["chatglm_pro", "chatglm_std", "chatglm_lite", "chatglm_lite_32k"],
'chatglm_pro', };
'chatglm_std',
'chatglm_lite',
'chatglm_lite_32k',
],
}
/* 扣费类型 普通余额还是高级余额 */ /* 扣费类型 普通余额还是高级余额 */
export const DEDUCTTYPELIST = [ export const DEDUCTTYPELIST = [
{ value: 1, label: '普通余额' }, { value: 1, label: "普通余额" },
{ value: 2, label: '高级余额' }, { value: 2, label: "高级余额" },
] ];
/* 不同模型在填入key字段的时候 key代表的含义不同 */ /* 不同模型在填入key字段的时候 key代表的含义不同 */
export const ModelTypeLabelMap = { export const ModelTypeLabelMap = {
1: 'APIKey', 1: "APIKey",
2: 'client_id', 2: "client_id",
3: 'AppKey', 3: "AppKey",
} };

View File

@ -54,6 +54,7 @@ const formPackage = reactive({
deductType: 1, deductType: 1,
maxRounds: 12, maxRounds: 12,
isTokenBased: false, isTokenBased: false,
tokenFeeRatio: 1000,
}) })
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
@ -84,6 +85,9 @@ const rules = reactive<FormRules>({
trigger: 'change', trigger: 'change',
}, },
], ],
tokenFeeRatio: [
{ required: true, message: 'token计费比例', trigger: 'change' },
],
model: [ model: [
{ {
required: true, required: true,
@ -192,6 +196,7 @@ function handleEditKey(row: any) {
maxRounds, maxRounds,
isDraw, isDraw,
isTokenBased, isTokenBased,
tokenFeeRatio
} = row } = row
nextTick(() => { nextTick(() => {
Object.assign(formPackage, { Object.assign(formPackage, {
@ -211,6 +216,7 @@ function handleEditKey(row: any) {
maxRounds, maxRounds,
isDraw, isDraw,
isTokenBased, isTokenBased,
tokenFeeRatio
}) })
}) })
visible.value = true visible.value = true
@ -393,6 +399,18 @@ onMounted(() => {
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column
prop="isTokenBased"
align="center"
label="设为Token计费"
width="120"
>
<template #default="scope">
<el-tag :type="scope.row.isTokenBased ? 'success' : 'danger'">
{{ scope.row.isTokenBased ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column <el-table-column
prop="deductType" prop="deductType"
align="center" align="center"
@ -792,6 +810,21 @@ onMounted(() => {
<QuestionFilled /> <QuestionFilled />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</el-form-item>
<el-form-item label="token计费比例" prop="tokenFeeRatio">
<el-input
v-model.number="formPackage.tokenFeeRatio"
placeholder="请填写token计费比例"
style="width: 80%"
/>
<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 <el-form-item
v-if="[1].includes(Number(formPackage.keyType))" v-if="[1].includes(Number(formPackage.keyType))"

View File

@ -8,12 +8,14 @@ export function fetchChatAPIProcess<T = any>(
prompt: string prompt: string
appId?: number appId?: number
options?: { conversationId?: string; parentMessageId?: string; temperature: number } options?: { conversationId?: string; parentMessageId?: string; temperature: number }
imageUrl?:string
model?:string
signal?: GenericAbortSignal signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) { ) {
return post<T>({ return post<T>({
url: '/chatgpt/chat-process', url: '/chatgpt/chat-process',
data: { prompt: params.prompt, appId: params?.appId, options: params.options }, data: { prompt: params.prompt, appId: params?.appId, options: params.options,imageUrl: params.imageUrl,model: params.model},
signal: params.signal, signal: params.signal,
onDownloadProgress: params.onDownloadProgress, onDownloadProgress: params.onDownloadProgress,
}) })

BIN
chat/src/assets/file.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -230,6 +230,10 @@ onMounted(() => {
<NInput v-model:value="loginForm.password" placeholder="请输入您的账户密码" type="password" :maxlength="30" show-password-on="click" tabindex="0" @keyup.enter="handlerSubmit" /> <NInput v-model:value="loginForm.password" placeholder="请输入您的账户密码" type="password" :maxlength="30" show-password-on="click" tabindex="0" @keyup.enter="handlerSubmit" />
</NFormItem> </NFormItem>
</Motion> </Motion>
<div style="color:red">
老用户密码统一重置为112233<br>
登录后请自行修改密码
</div>
<NFormItem> <NFormItem>
<NButton <NButton
block block

View File

@ -1,215 +1,246 @@
import { defineStore } from 'pinia' import { defineStore } from "pinia";
import { formatChatPre, getLocalState, setLocalState } from './helper' import { formatChatPre, getLocalState, setLocalState } from "./helper";
import { fetchCreateGroupAPI, fetchDelAllGroupAPI, fetchDelGroupAPI, fetchQueryGroupAPI, fetchUpdateGroupAPI } from '@/api/group' import {
import { fetchDelChatLogAPI, fetchDelChatLogByGroupIdAPI, fetchQueryChatLogListAPI } from '@/api/chatLog' fetchCreateGroupAPI,
import { fetchModelBaseConfigAPI } from '@/api/models' fetchDelAllGroupAPI,
import { fetchGetChatPreList } from '@/api/index' fetchDelGroupAPI,
fetchQueryGroupAPI,
fetchUpdateGroupAPI,
} from "@/api/group";
import {
fetchDelChatLogAPI,
fetchDelChatLogByGroupIdAPI,
fetchQueryChatLogListAPI,
} from "@/api/chatLog";
import { fetchModelBaseConfigAPI } from "@/api/models";
import { fetchGetChatPreList } from "@/api/index";
export const useChatStore = defineStore('chat-store', { export const useChatStore = defineStore("chat-store", {
state: (): Chat.ChatState => getLocalState(), state: (): Chat.ChatState => getLocalState(),
getters: { getters: {
/* 当前选用模型的配置 */ /* 当前选用模型的配置 */
activeConfig: (state) => { activeConfig: (state) => {
const uuid = state.active const uuid = state.active;
if (!uuid) if (!uuid) return {};
return {} const config = state.groupList.find((item) => item.uuid === uuid)?.config;
const config = state.groupList.find(item => item.uuid === uuid)?.config return config ? JSON.parse(config) : state.baseConfig;
return config ? JSON.parse(config) : state.baseConfig },
},
activeGroupAppId: (state) => { activeGroupAppId: (state) => {
const uuid = state.active const uuid = state.active;
if (!uuid) if (!uuid) return null;
return null return state.groupList.find((item) => item.uuid === uuid)?.appId;
return state.groupList.find(item => item.uuid === uuid)?.appId },
},
/* 当前选用模型的扣费类型 */ /* 当前选用模型的名称 */
activeModelKeyDeductType(state) { activeModelName(state) {
return this.activeConfig?.modelInfo?.deductType return this.activeConfig?.modelInfo?.model;
}, },
/* 当前选用模型的扣费类型 */
activeModelKeyDeductType(state) {
return this.activeConfig?.modelInfo?.deductType;
},
/* 当前选用模型的模型类型 */ /* 当前选用模型的模型类型 */
activeModelKeyType(state) { activeModelKeyType(state) {
return this.activeConfig?.modelInfo?.keyType return this.activeConfig?.modelInfo?.keyType;
}, },
/* 当前选用模型的调用价格 */ /* 当前选用模型的调用价格 */
activeModelKeyPrice(state) { activeModelKeyPrice(state) {
return this.activeConfig?.modelInfo?.deduct return this.activeConfig?.modelInfo?.deduct;
}, },
},
}, actions: {
/* 对话组过滤 */
setGroupKeyWord(keyWord: string) {
this.groupKeyWord = keyWord;
},
actions: { /* 计算拿到当前选择的对话组信息 */
/* 对话组过滤 */ getChatByGroupInfo() {
setGroupKeyWord(keyWord: string) { if (this.active)
this.groupKeyWord = keyWord return this.groupList.find((item) => item.uuid === this.active) || {};
}, },
/* 计算拿到当前选择的对话组信息 */ /* */
getChatByGroupInfo() { getConfigFromUuid(uuid: any) {
if (this.active) return this.groupList.find((item) => item.uuid === uuid)?.config;
return this.groupList.find(item => item.uuid === this.active) || {} },
},
/* */ /* 新增新的对话组 */
getConfigFromUuid(uuid: any) { async addNewChatGroup(appId = 0) {
return this.groupList.find(item => item.uuid === uuid)?.config const res: any = await fetchCreateGroupAPI({ appId });
}, const { id: uuid } = res.data;
await this.setActiveGroup(uuid);
this.recordState();
},
/* 新增新的对话组 */ /* 查询基础模型配置 兼容老的chatgroup */
async addNewChatGroup(appId = 0) { async getBaseModelConfig() {
const res: any = await fetchCreateGroupAPI({ appId }) const res = await fetchModelBaseConfigAPI();
const { id: uuid } = res.data this.baseConfig = res?.data;
await this.setActiveGroup(uuid) },
this.recordState()
},
/* 查询基础模型配置 兼容老的chatgroup */ /* 查询我的对话组 */
async getBaseModelConfig() { async queryMyGroup() {
const res = await fetchModelBaseConfigAPI() const res: any = await fetchQueryGroupAPI();
this.baseConfig = res?.data this.groupList = [
}, ...res.data.map((item: any) => {
const {
id: uuid,
title,
isSticky,
createdAt,
updatedAt,
appId,
config,
appLogo,
} = item;
return {
uuid,
title,
isEdit: false,
appId,
config,
isSticky,
appLogo,
createdAt,
updatedAt: new Date(updatedAt).getTime(),
};
}),
];
const isHasActive = this.groupList.some(
(item) => Number(item.uuid) === Number(this.active)
);
if (!this.active || !isHasActive)
this.groupList.length && this.setActiveGroup(this.groupList[0].uuid);
},
/* 查询我的对话组 */ /* 修改对话组信息 */
async queryMyGroup() { async updateGroupInfo(params: {
const res: any = await fetchQueryGroupAPI() groupId: number;
this.groupList = [...res.data.map((item: any) => { title?: string;
const { id: uuid, title, isSticky, createdAt, updatedAt, appId, config, appLogo } = item isSticky?: boolean;
return { uuid, title, isEdit: false, appId, config, isSticky, appLogo, createdAt, updatedAt: new Date(updatedAt).getTime() } }) {
})] await fetchUpdateGroupAPI(params);
const isHasActive = this.groupList.some(item => Number(item.uuid) === Number(this.active)) },
if (!this.active || !isHasActive)
this.groupList.length && this.setActiveGroup(this.groupList[0].uuid)
},
/* 修改对话组信息 */ /* 变更对话组 */
async updateGroupInfo(params: { groupId: number; title?: string; isSticky?: boolean }) { async setActiveGroup(uuid: number) {
await fetchUpdateGroupAPI(params) this.active = uuid;
}, if (this.active) await this.queryActiveChatLogList();
else this.chatList = [];
/* 变更对话组 */ this.groupList.forEach((item) => (item.isEdit = false));
async setActiveGroup(uuid: number) { this.recordState();
this.active = uuid },
if (this.active)
await this.queryActiveChatLogList()
else /* 删除对话组 */
this.chatList = [] async deleteGroup(params: Chat.History) {
const curIndex = this.groupList.findIndex(
(item) => item.uuid === params.uuid
);
const { uuid: groupId } = params;
await fetchDelGroupAPI({ groupId });
await this.queryMyGroup();
if (this.groupList.length === 0) await this.setActiveGroup(0);
this.groupList.forEach(item => (item.isEdit = false)) if (curIndex > 0 && curIndex < this.groupList.length)
this.recordState() await this.setActiveGroup(this.groupList[curIndex].uuid);
},
/* 删除对话组 */ if (curIndex === 0 && this.groupList.length > 0)
async deleteGroup(params: Chat.History) { await this.setActiveGroup(this.groupList[0].uuid);
const curIndex = this.groupList.findIndex(item => item.uuid === params.uuid)
const { uuid: groupId } = params
await fetchDelGroupAPI({ groupId })
await this.queryMyGroup()
if (this.groupList.length === 0)
await this.setActiveGroup(0)
if (curIndex > 0 && curIndex < this.groupList.length) if (
await this.setActiveGroup(this.groupList[curIndex].uuid) curIndex > this.groupList.length ||
(curIndex === 0 && this.groupList.length === 0)
)
await this.setActiveGroup(0);
if (curIndex === 0 && this.groupList.length > 0) if (curIndex > 0 && curIndex === this.groupList.length)
await this.setActiveGroup(this.groupList[0].uuid) await this.setActiveGroup(this.groupList[curIndex - 1].uuid);
if (curIndex > this.groupList.length || (curIndex === 0 && this.groupList.length === 0)) this.recordState();
await this.setActiveGroup(0) },
if (curIndex > 0 && curIndex === this.groupList.length) /* 删除全部非置顶对话组 */
await this.setActiveGroup(this.groupList[curIndex - 1].uuid) async delAllGroup() {
if (!this.active || !this.groupList.length) return;
await fetchDelAllGroupAPI();
await this.queryMyGroup();
if (this.groupList.length === 0) await this.setActiveGroup(0);
else await this.setActiveGroup(this.groupList[0].uuid);
},
this.recordState() /* 查询当前对话组的聊天记录 */
}, async queryActiveChatLogList() {
if (!this.active || Number(this.active) === 0) return;
const res: any = await fetchQueryChatLogListAPI({ groupId: this.active });
this.chatList = res.data;
},
/* 删除全部非置顶对话组 */ /* 添加一条虚拟的对话记录 */
async delAllGroup() { addGroupChat(data) {
if (!this.active || !this.groupList.length) this.chatList = [...this.chatList, data];
return },
await fetchDelAllGroupAPI()
await this.queryMyGroup()
if (this.groupList.length === 0)
await this.setActiveGroup(0)
else /* 动态修改对话记录 */
await this.setActiveGroup(this.groupList[0].uuid) updateGroupChat(index: number, data: Chat.Chat) {
}, this.chatList[index] = { ...this.chatList[index], ...data };
},
/* 查询当前对话组的聊天记录 */ /* 修改其中部分内容 */
async queryActiveChatLogList() { updateGroupChatSome(index: number, data: Partial<Chat.Chat>) {
if (!this.active || Number(this.active) === 0) this.chatList[index] = { ...this.chatList[index], ...data };
return },
const res: any = await fetchQueryChatLogListAPI({ groupId: this.active })
this.chatList = res.data
},
/* 添加一条虚拟的对话记录 */ /* 删除一条对话记录 */
addGroupChat(data) { async deleteChatById(chatId: number | undefined) {
this.chatList = [...this.chatList, data] console.log(chatId);
}, if (!chatId) return;
await fetchDelChatLogAPI({ id: chatId });
await this.queryActiveChatLogList();
},
/* 动态修改对话记录 */ /* 查询快问预设 */
updateGroupChat(index: number, data: Chat.Chat) { async queryChatPre() {
this.chatList[index] = { ...this.chatList[index], ...data } const res: any = await fetchGetChatPreList();
}, if (!res.data) return;
this.chatPreList = formatChatPre(res.data);
},
/* 修改其中部分内容 */ /* 设置使用上下文 */
updateGroupChatSome(index: number, data: Partial<Chat.Chat>) { setUsingContext(context: boolean) {
this.chatList[index] = { ...this.chatList[index], ...data } this.usingContext = context;
}, this.recordState();
},
/* 删除一条对话记录 */ /* 设置使用联网 */
async deleteChatById(chatId: number | undefined) { setUsingNetwork(context: boolean) {
console.log(chatId) this.usingNetwork = context;
if (!chatId) this.recordState();
return },
await fetchDelChatLogAPI({ id: chatId })
await this.queryActiveChatLogList()
},
/* 查询快问预设 */ /* 删除当前对话组的全部内容 */
async queryChatPre() { async clearChatByGroupId() {
const res: any = await fetchGetChatPreList() if (!this.active) return;
if (!res.data)
return
this.chatPreList = formatChatPre(res.data)
},
/* 设置使用上下文 */ await fetchDelChatLogByGroupIdAPI({ groupId: this.active });
setUsingContext(context: boolean) { await this.queryActiveChatLogList();
this.usingContext = context },
this.recordState()
},
/* 设置使用联网 */ recordState() {
setUsingNetwork(context: boolean) { setLocalState(this.$state);
this.usingNetwork = context },
this.recordState()
},
/* 删除当前对话组的全部内容 */ clearChat() {
async clearChatByGroupId() { this.chatList = [];
if (!this.active) this.groupList = [];
return this.active = 0;
this.recordState();
await fetchDelChatLogByGroupIdAPI({ groupId: this.active }) },
await this.queryActiveChatLogList() },
}, });
recordState() {
setLocalState(this.$state)
},
clearChat() {
this.chatList = []
this.groupList = []
this.active = 0
this.recordState()
},
},
})

View File

@ -12,7 +12,9 @@ import {
NTooltip, NTooltip,
useDialog, useDialog,
useMessage, useMessage,
NAlert
} from 'naive-ui' } from 'naive-ui'
import type { MessageRenderMessage } from 'naive-ui'
import html2canvas from 'html2canvas' import html2canvas from 'html2canvas'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@ -86,7 +88,7 @@ const theme = computed(() => appStore.theme)
const globaelConfig = computed(() => authStore.globalConfig) const globaelConfig = computed(() => authStore.globalConfig)
const isSetBeian = computed( const isSetBeian = computed(
() => globaelConfig.value?.companyName && globaelConfig.value?.filingNumber, () => globaelConfig.value?.companyName && globaelConfig.value?.filingNumber
) )
const { addGroupChat, updateGroupChat, updateGroupChatSome } = useChat() const { addGroupChat, updateGroupChat, updateGroupChatSome } = useChat()
const tradeStatus = computed(() => route.query.trade_status as string) const tradeStatus = computed(() => route.query.trade_status as string)
@ -101,13 +103,13 @@ const dataSources = computed(() => chatStore.chatList)
/* 当前所有的ai回复信息列表 方便拿到上下文 */ /* 当前所有的ai回复信息列表 方便拿到上下文 */
const conversationList = computed(() => const conversationList = computed(() =>
dataSources.value.filter(item => !item.inversion && !item.error), dataSources.value.filter((item) => !item.inversion && !item.error)
) )
/* 当前上下文有id的最后一条 防止停止回答的时候 上一条的id是空 接不上上下文 */ /* 当前上下文有id的最后一条 防止停止回答的时候 上一条的id是空 接不上上下文 */
const lastContext = computed(() => { const lastContext = computed(() => {
const hasIdCoversationList = conversationList.value.filter( const hasIdCoversationList = conversationList.value.filter(
item => item.conversationOptions?.parentMessageId, (item) => item.conversationOptions?.parentMessageId
) )
return hasIdCoversationList[hasIdCoversationList.length - 1] return hasIdCoversationList[hasIdCoversationList.length - 1]
?.conversationOptions ?.conversationOptions
@ -120,17 +122,22 @@ const firstScroll = ref<boolean>(true)
const tipsRef = ref<any>(null) const tipsRef = ref<any>(null)
const tipText = ref('') const tipText = ref('')
const tipsHeight = ref<any>(null) const tipsHeight = ref<any>(null)
const dataBase64 = ref(null)
const fileName = ref('')
const isImageFile = ref(false)
const showDeleteIcon = ref(false)
/* 当前选中的对话组 */ /* 当前选中的对话组 */
const activeGroupId = computed(() => chatStore.active) const activeGroupId = computed(() => chatStore.active)
/* 当前对话组的详细信息 */ /* 当前对话组的详细信息 */
const activeGroupInfo = computed(() => const activeGroupInfo = computed(() =>
chatStore.groupList.find((item: any) => item.uuid === chatStore.active), chatStore.groupList.find((item: any) => item.uuid === chatStore.active)
) )
/* 当前选用的模型的类型 1 openai 2: 百度 */ /* 当前选用的模型的类型 1 openai 2: 百度 */
const activeModelKeyType = computed(() => Number(chatStore?.activeModelKeyType)) const activeModelKeyType = computed(() => Number(chatStore?.activeModelKeyType))
/* 当前对话组是否是应用 */ /* 当前对话组是否是应用 */
const activeAppId = computed(() => const activeAppId = computed(() =>
activeGroupInfo?.value ? activeGroupInfo.value.appId : 0, activeGroupInfo?.value ? activeGroupInfo.value.appId : 0
) )
/* 粘贴板的文字 */ /* 粘贴板的文字 */
const clipboardText = computed(() => useGlobalStore.clipboardText) const clipboardText = computed(() => useGlobalStore.clipboardText)
@ -144,43 +151,37 @@ watch(clipboardText, (val) => {
watch( watch(
activeAppId, activeAppId,
(val) => { (val) => {
if (val) if (val) queryAppDetail(val)
queryAppDetail(val)
else appDetail.value = null else appDetail.value = null
}, },
{ immediate: true }, { immediate: true }
) )
watch( watch(
activeGroupId, activeGroupId,
(val) => { (val) => {
if (val) if (val) firstScroll.value = true
firstScroll.value = true if (inputRef.value && !isMobile.value) inputRef.value?.focus()
if (inputRef.value && !isMobile.value)
inputRef.value?.focus()
}, },
{ immediate: true }, { immediate: true }
) )
watch( watch(
dataSources, dataSources,
(val) => { (val) => {
if (val.length === 0) if (val.length === 0) return
return
if (firstScroll.value) { if (firstScroll.value) {
firstScroll.value = false firstScroll.value = false
scrollToBottom() scrollToBottom()
} }
}, },
{ immediate: true }, { immediate: true }
) )
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 `${modelInfo.modelName}` return `${modelInfo.modelName}`
}) })
function handleOpenModelDialog() { function handleOpenModelDialog() {
@ -202,14 +203,52 @@ let curFile: File | null
async function handleFileSelect(event: any) { async function handleFileSelect(event: any) {
const file = event?.target?.files[0] const file = event?.target?.files[0]
if (file.size <= 5 * 1024 * 1024) if (!file) return
if (file.size <= 10 * 1024 * 1024) {
await handleSetFile(file) await handleSetFile(file)
else ms.error('上传文件失败上传大小不能超过5M') } else {
return ms.error('上传文件失败上传大小不能超过10M')
}
let trimmedFileName = file.name
const maxLength = 8 //
const extension = trimmedFileName.split('.').pop() //
if (trimmedFileName.length > maxLength) {
//
trimmedFileName =
trimmedFileName.substring(0, maxLength - extension.length - 1) +
'….' +
extension
}
fileName.value = trimmedFileName //
console.log(file.type)
//
if (file.type.startsWith('image/')) {
//
isImageFile.value = true
handleSetFile(file)
} else if (
file.type.startsWith('application/') ||
file.type.startsWith('text/')
) {
//
isImageFile.value = false
handleSetFile(file)
} else {
//
ms.error('上传文件失败,不支持此类型文件')
console.log('不支持的文件类型')
}
} }
async function handleSetFile(file: File) { async function handleSetFile(file: File) {
curFile = file curFile = file
uploadFile() const reader = new FileReader()
reader.onload = (event: any) => {
dataBase64.value = event.target?.result as string
}
reader.readAsDataURL(file)
} }
function uploadBtn() { function uploadBtn() {
@ -233,11 +272,13 @@ async function uploadFile() {
} }
}, },
}) })
prompt.value = `请分析一下上传的这个文件:${res?.data?.data}` return res?.data?.data
ms.success('上传成功,输入内容后发送', { duration: 5000, closable: true }) } catch (error) {
} ms.error('网络异常,发送失败')
catch (error) { return null
ms.error('网络异常,上传失败') } finally {
dataBase64.value = null
curFile = null
} }
} }
@ -271,9 +312,9 @@ function handleScrollBtm() {
/* 发送消息 */ /* 发送消息 */
async function handleSubmit(index?: number) { async function handleSubmit(index?: number) {
if ( if (
chatStore.groupList.length === 0 chatStore.groupList.length === 0 ||
|| loading.value loading.value ||
|| !typingStatusEnd.value !typingStatusEnd.value
) )
return return
let message = '' let message = ''
@ -287,12 +328,10 @@ async function handleSubmit(index?: number) {
function parseTextToJSON(input: string) { function parseTextToJSON(input: string) {
const startIndex = input.indexOf(',"text":"') + 10 const startIndex = input.indexOf(',"text":"') + 10
if (startIndex === -1) if (startIndex === -1) return { text: '' }
return { text: '' }
let endIndex = input.indexOf('","delta"', startIndex) let endIndex = input.indexOf('","delta"', startIndex)
if (endIndex === -1) if (endIndex === -1) endIndex = input.length - 1
endIndex = input.length - 1
else endIndex = endIndex - 10 else endIndex = endIndex - 10
const text = input.substring(startIndex, endIndex) const text = input.substring(startIndex, endIndex)
@ -301,15 +340,17 @@ function parseTextToJSON(input: string) {
/* 按钮发送消息 */ /* 按钮发送消息 */
async function onConversation(msg?: string) { async function onConversation(msg?: string) {
let imageUrl = null
if (dataBase64.value || curFile) {
imageUrl = await uploadFile()
}
let message = msg || prompt.value let message = msg || prompt.value
if (tipText.value && !message.includes(tipText.value)) if (tipText.value && !message.includes(tipText.value))
message = `${tipText.value}\n${message}` message = `${tipText.value}\n${message}`
if (loading.value) if (loading.value) return
return
if (!message || message.trim() === '') if (!message || message.trim() === '') return
return
controller = new AbortController() controller = new AbortController()
@ -319,6 +360,7 @@ async function onConversation(msg?: string) {
text: message, text: message,
inversion: true, inversion: true,
error: false, error: false,
imageUrl,
conversationOptions: null, conversationOptions: null,
requestOptions: { prompt: message, options: null }, requestOptions: { prompt: message, options: null },
}) })
@ -369,12 +411,10 @@ async function onConversation(msg?: string) {
if (cacheResText.length - i > 150) { if (cacheResText.length - i > 150) {
currentText += cacheResText.substring(i, i + 10) currentText += cacheResText.substring(i, i + 10)
i += 10 i += 10
} } else if (cacheResText.length - i > 200) {
else if (cacheResText.length - i > 200) {
currentText += cacheResText.substring(i) currentText += cacheResText.substring(i)
i += cacheResText.length - i i += cacheResText.length - i
} } else {
else {
currentText += cacheResText[i] currentText += cacheResText[i]
i++ i++
} }
@ -396,8 +436,8 @@ async function onConversation(msg?: string) {
const curLen = currentText ? currentText.length : 0 const curLen = currentText ? currentText.length : 0
const cacheResLen = cacheResText ? cacheResText.length : 0 const cacheResLen = cacheResText ? cacheResText.length : 0
if ( if (
!isStreamIn.value !isStreamIn.value &&
&& (curLen === cacheResLen || curLen > cacheResLen) (curLen === cacheResLen || curLen > cacheResLen)
) { ) {
typingStatusEnd.value = true typingStatusEnd.value = true
updateGroupChatSome(dataSources.value.length - 1, { updateGroupChatSome(dataSources.value.length - 1, {
@ -413,12 +453,12 @@ async function onConversation(msg?: string) {
authStore.updateUserBanance(userBanance) authStore.updateUserBanance(userBanance)
if ( if (
dataSources.value.length === 2 dataSources.value.length === 2 &&
&& !activeGroupInfo?.value?.appId !activeGroupInfo?.value?.appId
) { ) {
const lengthStr = isMobile.value ? 10 : 20 const lengthStr = isMobile.value ? 10 : 20
const title const title =
= dataSources.value[1].text.length > lengthStr dataSources.value[1].text.length > lengthStr
? dataSources.value[1].text.slice(0, lengthStr) ? dataSources.value[1].text.slice(0, lengthStr)
: dataSources.value[1].text : dataSources.value[1].text
chatStore chatStore
@ -433,8 +473,7 @@ async function onConversation(msg?: string) {
/* 有多余的再请求下一帧 */ /* 有多余的再请求下一帧 */
if (cacheResText.length && cacheResText.length > currentText.length) { if (cacheResText.length && cacheResText.length > currentText.length) {
requestAnimationFrame(update) requestAnimationFrame(update)
} } else {
else {
setTimeout(() => { setTimeout(() => {
requestAnimationFrame(update) requestAnimationFrame(update)
}, 1000) }, 1000)
@ -447,6 +486,8 @@ async function onConversation(msg?: string) {
prompt: message, prompt: message,
appId: activeGroupInfo.value ? activeGroupInfo.value.appId : 0, appId: activeGroupInfo.value ? activeGroupInfo.value.appId : 0,
options, options,
imageUrl,
model: chatStore?.activeModelName,
signal: controller.signal, signal: controller.signal,
onDownloadProgress: ({ event }) => { onDownloadProgress: ({ event }) => {
const xhr = event.target const xhr = event.target
@ -456,16 +497,14 @@ async function onConversation(msg?: string) {
if ([1].includes(activeModelKeyType.value)) { if ([1].includes(activeModelKeyType.value)) {
const lastIndex = responseText.lastIndexOf( const lastIndex = responseText.lastIndexOf(
'\n', '\n',
responseText.length - 2, responseText.length - 2
) )
let chunk = responseText let chunk = responseText
if (lastIndex !== -1) if (lastIndex !== -1) chunk = responseText.substring(lastIndex)
chunk = responseText.substring(lastIndex)
try { try {
data = JSON.parse(chunk) data = JSON.parse(chunk)
} } catch (error) {
catch (error) {
/* 二次解析 */ /* 二次解析 */
// const parseData = parseTextToJSON(responseText) // const parseData = parseTextToJSON(responseText)
// TODO // TODO
@ -489,8 +528,7 @@ async function onConversation(msg?: string) {
const parseData = JSON.parse(line) const parseData = JSON.parse(line)
cacheResult += parseData.result cacheResult += parseData.result
tem = parseData tem = parseData
} } catch (error) {
catch (error) {
console.log('Json parse 2 3 type error: ') console.log('Json parse 2 3 type error: ')
} }
} }
@ -502,8 +540,7 @@ async function onConversation(msg?: string) {
/* 如果出现输出内容不一致就需要处理了 */ /* 如果出现输出内容不一致就需要处理了 */
if (activeModelKeyType.value === 1) { if (activeModelKeyType.value === 1) {
cacheResText = data.text cacheResText = data.text
if (data?.userBanance) if (data?.userBanance) userBanance = data?.userBanance
userBanance = data?.userBanance
} }
if ([2, 3].includes(activeModelKeyType.value)) { if ([2, 3].includes(activeModelKeyType.value)) {
@ -512,24 +549,21 @@ async function onConversation(msg?: string) {
isStreamIn.value = !is_end isStreamIn.value = !is_end
data?.userBanance && (userBanance = data?.userBanance) data?.userBanance && (userBanance = data?.userBanance)
} }
} } catch (error) {}
catch (error) {}
}, },
}) })
} }
await fetchChatAPIOnce() await fetchChatAPIOnce()
} } catch (error: any) {
catch (error: any) {
useGlobalStore.updateIsChatIn(false) useGlobalStore.updateIsChatIn(false)
clearInterval(timer) clearInterval(timer)
isStreamIn.value = false isStreamIn.value = false
if ( if (
error.code === 402 error.code === 402 ||
|| error?.message.includes('余额不足') error?.message.includes('余额不足') ||
|| error?.message.includes('免费额度已经使用完毕') error?.message.includes('免费额度已经使用完毕')
) { ) {
if (isLogin.value) if (isLogin.value) useGlobalStore.updateGoodsDialog(true)
useGlobalStore.updateGoodsDialog(true)
else authStore.setLoginDialog(true) else authStore.setLoginDialog(true)
} }
@ -568,10 +602,10 @@ async function onConversation(msg?: string) {
requestOptions: { prompt: message, options: { ...options } }, requestOptions: { prompt: message, options: { ...options } },
}) })
scrollToBottomIfAtBottom() scrollToBottomIfAtBottom()
} } finally {
finally {
loading.value = false loading.value = false
isStreamIn.value = false isStreamIn.value = false
imageUrl = null
} }
} }
@ -589,16 +623,14 @@ function handleRefresh() {
ms.success('感谢你的购买、祝您使用愉快~', { duration: 5000 }) ms.success('感谢你的购买、祝您使用愉快~', { duration: 5000 })
authStore.getUserInfo() authStore.getUserInfo()
router.replace({ name: 'Chat', query: {} }) router.replace({ name: 'Chat', query: {} })
} } else {
else {
ms.error('您还没有购买成功哦~') ms.error('您还没有购买成功哦~')
} }
} }
/* 导出 */ /* 导出 */
function handleExport() { function handleExport() {
if (loading.value) if (loading.value) return
return
const d = dialog.warning({ const d = dialog.warning({
title: t('chat.exportImage'), title: t('chat.exportImage'),
@ -627,11 +659,9 @@ function handleExport() {
d.loading = false d.loading = false
ms.success(t('chat.exportSuccess')) ms.success(t('chat.exportSuccess'))
Promise.resolve() Promise.resolve()
} } catch (error: any) {
catch (error: any) {
ms.error(t('chat.exportFailed')) ms.error(t('chat.exportFailed'))
} } finally {
finally {
d.loading = false d.loading = false
} }
}, },
@ -640,8 +670,7 @@ function handleExport() {
/* 删除 */ /* 删除 */
function handleDelete({ chatId }: Chat.Chat) { function handleDelete({ chatId }: Chat.Chat) {
if (loading.value) if (loading.value) return
return
dialog.warning({ dialog.warning({
title: t('chat.deleteMessage'), title: t('chat.deleteMessage'),
@ -655,8 +684,7 @@ function handleDelete({ chatId }: Chat.Chat) {
} }
function handleClear() { function handleClear() {
if (loading.value) if (loading.value) return
return
dialog.warning({ dialog.warning({
title: t('chat.clearChat'), title: t('chat.clearChat'),
@ -676,8 +704,7 @@ function handleEnter(event: KeyboardEvent) {
event.preventDefault() event.preventDefault()
handleSubmit() handleSubmit()
} }
} } else {
else {
if (event.key === 'Enter' && event.ctrlKey) { if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault() event.preventDefault()
handleSubmit() handleSubmit()
@ -695,17 +722,16 @@ function handleStop() {
} }
const placeholder = computed(() => { const placeholder = computed(() => {
if (isMobile.value) if (isMobile.value) return t('chat.placeholderMobile')
return t('chat.placeholderMobile')
return t('chat.placeholder') return t('chat.placeholder')
}) })
const buttonDisabled = computed(() => { const buttonDisabled = computed(() => {
return ( return (
loading.value loading.value ||
|| !prompt.value !prompt.value ||
|| prompt.value.trim() === '' prompt.value.trim() === '' ||
|| !typingStatusEnd.value !typingStatusEnd.value
) )
}) })
// loading // loading
@ -719,8 +745,7 @@ function getTipsRefHeight() {
function onInputeTip() { function onInputeTip() {
tipsHeight.value = 'auto' tipsHeight.value = 'auto'
if (!tipText.value) if (!tipText.value) tipsHeight.value = 0
tipsHeight.value = 0
nextTick(() => getTipsRefHeight()) nextTick(() => getTipsRefHeight())
} }
@ -728,24 +753,20 @@ function onInputeTip() {
onMounted(async () => { onMounted(async () => {
chatStore.queryChatPre() chatStore.queryChatPre()
if (token.value) if (token.value) otherLoginByToken(token.value)
otherLoginByToken(token.value)
if (tradeStatus.value) if (tradeStatus.value) handleRefresh()
handleRefresh()
nextTick(async () => { nextTick(async () => {
await chatStore.queryActiveChatLogList() await chatStore.queryActiveChatLogList()
scrollToBottom() scrollToBottom()
if (inputRef.value && !isMobile.value) if (inputRef.value && !isMobile.value) inputRef.value?.focus()
inputRef.value?.focus()
}) })
}) })
const darkMode = computed(() => appStore.theme === 'dark') const darkMode = computed(() => appStore.theme === 'dark')
onUnmounted(() => { onUnmounted(() => {
if (loading.value) if (loading.value) controller.abort()
controller.abort()
}) })
</script> </script>
@ -793,6 +814,7 @@ onUnmounted(() => {
:inversion="item.inversion" :inversion="item.inversion"
:error="item.error" :error="item.error"
:loading="item.loading" :loading="item.loading"
:imageUrl="item.imageUrl"
@regenerate="handleSubmit(index)" @regenerate="handleSubmit(index)"
@delete="handleDelete(item)" @delete="handleDelete(item)"
/> />
@ -825,10 +847,10 @@ onUnmounted(() => {
'text-[#3076fd]': usingContext, 'text-[#3076fd]': usingContext,
'text-[#a8071a]': !usingContext, 'text-[#a8071a]': !usingContext,
}" }"
><SvgIcon ><SvgIcon
class="text-lg" class="text-lg"
style="width: 1em; height: 1em" style="width: 1em; height: 1em"
icon="ri:chat-history-line" icon="ri:chat-history-line"
/></span> /></span>
</button> </button>
</template> </template>
@ -852,10 +874,11 @@ onUnmounted(() => {
class="shrink0 flex h-8 w-8 items-center justify-center rounded border transition hover:bg-[#eef0f3] dark:border-neutral-700 dark:hover:bg-[#33373c]" class="shrink0 flex h-8 w-8 items-center justify-center rounded border transition hover:bg-[#eef0f3] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click="openChatPre" @click="openChatPre"
> >
<span><SvgIcon <span
class="text-lg" ><SvgIcon
style="width: 1em; height: 1em" class="text-lg"
icon="noto:open-book" style="width: 1em; height: 1em"
icon="noto:open-book"
/></span> /></span>
</button> </button>
</template> </template>
@ -950,7 +973,7 @@ onUnmounted(() => {
<template #icon> <template #icon>
<span class="text-base text-slate-500 dark:text-slate-400"> <span class="text-base text-slate-500 dark:text-slate-400">
<!-- <SvgIcon icon="streamline-emojis:wrapped-gift-1" /> --> <!-- <SvgIcon icon="streamline-emojis:wrapped-gift-1" /> -->
<img :src="modelSvg" class="h-8" alt=""> <img :src="modelSvg" class="h-8" alt="" />
</span> </span>
</template> </template>
<span style="color: #3076fd">{{ modelName }}</span> <span style="color: #3076fd">{{ modelName }}</span>
@ -1007,7 +1030,10 @@ onUnmounted(() => {
<div class="flex space-x-2"> <div class="flex space-x-2">
<NTooltip <NTooltip
v-if=" v-if="
chatStore.activeConfig.modelInfo.model === 'gpt-4-all' !dataBase64 &&
(chatStore.activeConfig.modelInfo.model === 'gpt-4-all' ||
chatStore.activeConfig.modelInfo.model ===
'gpt-4-vision-preview')
" "
trigger="hover" trigger="hover"
placement="bottom-end" placement="bottom-end"
@ -1025,24 +1051,70 @@ onUnmounted(() => {
:multiple="false" :multiple="false"
type="file" type="file"
style="display: none" style="display: none"
accept="text/plain,image/*, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/pdf" :accept="
@change="handleFileSelect($event)" chatStore.activeConfig.modelInfo.model ===
> 'gpt-4-vision-preview'
<SvgIcon ? 'image/*'
class="text-lg" : 'text/plain,image/*, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/pdf'
style="width: 1em; height: 1em" "
icon="mingcute:upload-line" @change="handleFileSelect($event)" />
/></span> <SvgIcon icon="mingcute:upload-line"
/></span>
</button> </button>
</template> </template>
上传 上传
</NTooltip> </NTooltip>
<!-- 预览容器 -->
<div
v-if="dataBase64"
class="relative flex items-start justify-start"
>
<div
class="group"
@mouseover="showDeleteIcon = true"
@mouseleave="showDeleteIcon = false"
>
<!-- 根据 isImageFile 的值显示不同内容 -->
<template v-if="isImageFile">
<!-- 图片预览 -->
<img
:src="dataBase64"
class="max-w-full max-h-10 border border-gray-300 rounded-lg"
alt="预览图片"
/>
<!-- 清除图标 -->
<SvgIcon
class="close-icon"
icon="gg:close-o"
@click="dataBase64 = ''"
/>
</template>
<template v-else>
<!-- 非图片文件预览例如文件图标 -->
<div
style="white-space: nowrap; padding: 0.25rem"
class="flex items-center justify-center border border-gray-300 rounded-lg h-8 hover:bg-gray-100 text-gray-700 dark:hover:bg-gray-700 dark:text-gray-400"
>
<span>{{ fileName }}</span>
<!-- 清除图标 -->
<SvgIcon
class="close-icon"
icon="gg:close-o"
@click="dataBase64 = ''"
/>
<!-- 替换为适当的文件图标 -->
</div>
</template>
</div>
</div>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div <div
class="flex items-center text-neutral-400 cursor-pointer hover:text-[#3076fd]" class="flex items-center text-neutral-400 cursor-pointer hover:text-[#3076fd]"
> >
<span class="ml-2 mr-2 text-xs" @click="toggleUsingNetwork">{{ usingNetwork ? '关闭' : '开启' }}联网访问</span> <span class="ml-2 mr-2 text-xs" @click="toggleUsingNetwork"
>{{ usingNetwork ? '关闭' : '开启' }}联网访问</span
>
<NTooltip trigger="hover" :disabled="isMobile"> <NTooltip trigger="hover" :disabled="isMobile">
<template #trigger> <template #trigger>
<SvgIcon <SvgIcon
@ -1101,9 +1173,10 @@ onUnmounted(() => {
class="ml-2 transition-all text-[#aeaeae] hover:text-[#60606d]" class="ml-2 transition-all text-[#aeaeae] hover:text-[#60606d]"
href="https://beian.miit.gov.cn" href="https://beian.miit.gov.cn"
target="_blank" target="_blank"
>{{ globaelConfig?.filingNumber }}</a> >{{ globaelConfig?.filingNumber }}</a
>
</div> </div>
<NModal v-model:show="showProgressModal" :mask-closable="false"> <!-- <NModal v-model:show="showProgressModal" :mask-closable="false">
<NCard <NCard
style="width: 80%" style="width: 80%"
title="上传文件中" title="上传文件中"
@ -1119,7 +1192,7 @@ onUnmounted(() => {
processing processing
/> />
</NCard> </NCard>
</NModal> </NModal> -->
</div> </div>
</template> </template>
@ -1137,4 +1210,31 @@ onUnmounted(() => {
.shrink0 { .shrink0 {
flex-shrink: 0 !important; flex-shrink: 0 !important;
} }
.close-icon {
position: absolute;
top: -0;
right: 0;
color: #ff6347;
font-size: 1rem;
width: 1rem;
height: 1rem;
animation: scaleAnim 2s infinite ease-in-out;
cursor: pointer;
&:hover {
font-weight: 800;
color: red;
}
}
@keyframes scaleAnim {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
</style> </style>

View File

@ -15,6 +15,7 @@ interface Props {
text?: string text?: string
loading?: boolean loading?: boolean
asRawText?: boolean asRawText?: boolean
imageUrl?: string
} }
interface Emit { interface Emit {
@ -83,6 +84,12 @@ const text = computed(() => {
if (!props.asRawText) return mdi.render(value) if (!props.asRawText) return mdi.render(value)
return value return value
}) })
const imageUrl = computed(() => props.imageUrl)
const isImageUrl = computed(() => {
if (!imageUrl.value) return false
return /\.(jpg|jpeg|png|gif)$/i.test(imageUrl.value)
})
function highlightBlock(str: string, lang?: string) { function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper ${ return `<pre class="code-block-wrapper ${
@ -122,7 +129,11 @@ defineExpose({ textRef })
v-html="text" v-html="text"
/> />
<div v-else class="w-full whitespace-pre-wrap" v-text="text" /> <div v-else class="w-full whitespace-pre-wrap" v-text="text" />
<span v-if="loading" class="dark:text-white w-[4px] h-[20px] block animate-blink" style="display:none"/> <span
v-if="loading"
class="dark:text-white w-[4px] h-[20px] block animate-blink"
style="display: none"
/>
</div> </div>
<!-- 小易改动注册掉底部的内容 --> <!-- 小易改动注册掉底部的内容 -->
<!-- <div style="margin-top: 0.5rem"> --> <!-- <div style="margin-top: 0.5rem"> -->
@ -170,6 +181,28 @@ defineExpose({ textRef })
</div> </div>
<div v-else> <div v-else>
<div class="whitespace-pre-wrap" v-text="text" /> <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"> <div v-if="false" style="margin-left: 0.5rem">
<NButton class="ml-2" text color="#FFF" @click="handleCopy"> <NButton class="ml-2" text color="#FFF" @click="handleCopy">
<template #icon> <template #icon>
@ -251,4 +284,17 @@ defineExpose({ textRef })
html.dark pre code.hljs { html.dark pre code.hljs {
padding: 0 !important; padding: 0 !important;
} }
.file-1 {
display: inline;
margin-top: 0.5rem;
width: 120px;
height: 150px;
}
.file-2 {
display: inline;
margin-top: 0.5rem;
width: 90px;
height: 120px;
}
</style> </style>

View File

@ -15,6 +15,7 @@ interface Props {
inversion?: boolean inversion?: boolean
error?: boolean error?: boolean
loading?: boolean loading?: boolean
imageUrl?: string
} }
interface Emit { interface Emit {
@ -53,7 +54,9 @@ const options = computed(() => {
common.unshift({ common.unshift({
label: asRawText.value ? t('chat.preview') : t('chat.showRawText'), label: asRawText.value ? t('chat.preview') : t('chat.showRawText'),
key: 'toggleRenderType', key: 'toggleRenderType',
icon: iconRender({ icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code' }), icon: iconRender({
icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code',
}),
}) })
} }
@ -101,8 +104,14 @@ function handleRegenerate() {
> >
<AvatarComponent :image="inversion" /> <AvatarComponent :image="inversion" />
</div> </div>
<div class="overflow-hidden text-sm " :class="[inversion ? 'items-end' : 'items-start']"> <div
<p class="text-xs text-[#b4bbc4]" :class="[inversion ? 'text-right' : 'text-left']"> class="overflow-hidden text-sm"
:class="[inversion ? 'items-end' : 'items-start']"
>
<p
class="text-xs text-[#b4bbc4]"
:class="[inversion ? 'text-right' : 'text-left']"
>
{{ dateTime }} {{ dateTime }}
</p> </p>
<div <div
@ -119,11 +128,12 @@ function handleRegenerate() {
@regenerate="handleRegenerate" @regenerate="handleRegenerate"
@copy="handleCopy" @copy="handleCopy"
@delete="handleDetele" @delete="handleDetele"
: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"
@click="handleRegenerate" @click="handleRegenerate"
> >
<SvgIcon icon="ri:restart-line" /> <SvgIcon icon="ri:restart-line" />
@ -134,7 +144,9 @@ function handleRegenerate() {
:options="options" :options="options"
@select="handleSelect" @select="handleSelect"
> >
<button class="transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200"> <button
class="transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200"
>
<SvgIcon icon="ri:more-2-fill" /> <SvgIcon icon="ri:more-2-fill" />
</button> </button>
</NDropdown> </NDropdown>

BIN
service/.DS_Store vendored

Binary file not shown.

BIN
service/src/.DS_Store vendored

Binary file not shown.

View File

@ -32,7 +32,7 @@ async function bootstrap() {
createSwagger(app); createSwagger(app);
const server = await app.listen(PORT, () => { const server = await app.listen(PORT, () => {
Logger.log(`服务启动成功: http://localhost:${PORT}/nineai/swagger/docs 作者:小易 QQ805239273`, 'Main'); Logger.log(`服务启动成功: http://localhost:${PORT}/nineai/swagger/docs`, 'Main');
}); });
server.timeout = 5 * 60 * 1000; server.timeout = 5 * 60 * 1000;
} }

View File

@ -58,6 +58,9 @@ export class ChatLogEntity extends BaseEntity {
@Column({ comment: '图片信息的string', nullable: true, type: 'text' }) @Column({ comment: '图片信息的string', nullable: true, type: 'text' })
fileInfo: string; fileInfo: string;
@Column({ comment: '上传图片的信息', nullable: true, type: 'text' })
imageUrl: string;
@Column({ comment: 'role system user assistant', nullable: true }) @Column({ comment: 'role system user assistant', nullable: true })
role: string; role: string;

View File

@ -211,7 +211,7 @@ export class ChatLogService {
} }
const list = await this.chatLogEntity.find({ where }); const list = await this.chatLogEntity.find({ where });
return list.map((item) => { return list.map((item) => {
const { prompt, role, answer, createdAt, model, conversationOptions, requestOptions, id } = item; const { prompt, role, answer, createdAt, model, conversationOptions, requestOptions, id,imageUrl} = item;
let parseConversationOptions: any = null let parseConversationOptions: any = null
let parseRequestOptions: any = null let parseRequestOptions: any = null
try { try {
@ -228,6 +228,8 @@ export class ChatLogService {
error: false, error: false,
conversationOptions: parseConversationOptions, conversationOptions: parseConversationOptions,
requestOptions: parseRequestOptions, requestOptions: parseRequestOptions,
imageUrl,
model
}; };
}); });
} }

View File

@ -165,7 +165,7 @@ export class ChatgptService implements OnModuleInit {
/* 不同场景会变更其信息 */ /* 不同场景会变更其信息 */
let setSystemMessage = systemMessage; let setSystemMessage = systemMessage;
const { parentMessageId } = options; const { parentMessageId } = options;
const { prompt } = body; const { prompt ,imageUrl,model:activeModel} = body;
const { groupId, usingNetwork } = options; const { groupId, usingNetwork } = options;
// const { model = 3 } = options; // const { model = 3 } = options;
/* 获取当前对话组的详细配置信息 */ /* 获取当前对话组的详细配置信息 */
@ -260,6 +260,8 @@ export class ChatgptService implements OnModuleInit {
userId: req.user.id, userId: req.user.id,
type: DeductionKey.CHAT_TYPE, type: DeductionKey.CHAT_TYPE,
prompt, prompt,
imageUrl,
activeModel,
answer: '', answer: '',
promptTokens: prompt_tokens, promptTokens: prompt_tokens,
completionTokens: 0, completionTokens: 0,
@ -318,6 +320,8 @@ 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),
@ -328,6 +332,8 @@ export class ChatgptService implements OnModuleInit {
maxTokenRes, maxTokenRes,
apiKey: modelKey, apiKey: modelKey,
model, model,
imageUrl,
activeModel,
temperature, temperature,
proxyUrl: proxyResUrl, proxyUrl: proxyResUrl,
onProgress: (chat) => { onProgress: (chat) => {
@ -386,6 +392,8 @@ export class ChatgptService implements OnModuleInit {
role: 'user', role: 'user',
name: undefined, name: undefined,
usage: null, usage: null,
imageUrl,
activeModel,
parentMessageId: parentMessageId, parentMessageId: parentMessageId,
conversationId: response?.conversationId, conversationId: response?.conversationId,
}; };
@ -449,6 +457,8 @@ export class ChatgptService implements OnModuleInit {
userId: req.user.id, userId: req.user.id,
type: DeductionKey.CHAT_TYPE, type: DeductionKey.CHAT_TYPE,
prompt, prompt,
imageUrl,
activeModel,
answer: '', answer: '',
promptTokens: prompt_tokens, promptTokens: prompt_tokens,
completionTokens: 0, completionTokens: 0,

View File

@ -102,7 +102,11 @@ export function sendMessageFromOpenAi(messagesHistory, inputs ){
export function getTokenCount(text: string) { export function getTokenCount(text: string) {
if(!text) return 0; if (!text) return 0;
// 确保text是字符串类型
if (typeof text !== 'string') {
text = String(text);
}
text = text.replace(/<\|endoftext\|>/g, '') text = text.replace(/<\|endoftext\|>/g, '')
return tokenizer.encode(text).length return tokenizer.encode(text).length
} }

View File

@ -1,41 +1,44 @@
import Keyv from 'keyv' 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';
const tokenizer = get_encoding('cl100k_base') const tokenizer = get_encoding('cl100k_base');
export type Role = 'user' | 'assistant' | 'function'
export type Role = 'user' | 'assistant' | 'function';
interface Options { interface Options {
store: Keyv store: Keyv;
namespace: string namespace: string;
expires?: number expires?: number;
} }
export interface MessageInfo { export interface MessageInfo {
id: string id: string;
text: string text: string;
role: Role role: Role;
name?: string name?: string;
imageUrl?: string;
activeModel?: string;
usage: { usage: {
prompt_tokens?: number prompt_tokens?: number;
completion_tokens?: number completion_tokens?: number;
total_tokens?: number total_tokens?: number;
} };
parentMessageId?: string parentMessageId?: string;
conversationId?: string conversationId?: string;
} }
export interface BuildMessageOptions { export interface BuildMessageOptions {
systemMessage?: string systemMessage?: string;
parentMessageId: string parentMessageId: string;
maxRounds?: number maxRounds?: number;
maxModelToken?: number maxModelToken?: number;
maxResponseTokens?: number maxResponseTokens?: number;
name?: string name?: string;
imageUrl?: string;
activeModel?: string;
model?: string;
} }
// export interface BuildMessageRes { // export interface BuildMessageRes {
@ -43,12 +46,12 @@ export interface BuildMessageOptions {
// numTokens: number // numTokens: number
// maxTokens: number // maxTokens: number
// } // }
export type BuildMessageRes = any[] export type BuildMessageRes = any[];
export interface NineStoreInterface { export interface NineStoreInterface {
getData(id: string): Promise<string>; getData(id: string): Promise<string>;
setData(message: MessageInfo, expires?: number): Promise<void>; setData(message: MessageInfo, expires?: number): Promise<void>;
getUuid(): string getUuid(): string;
buildMessageFromParentMessageId(string, opt?: BuildMessageOptions): Promise<any>; buildMessageFromParentMessageId(string, opt?: BuildMessageOptions): Promise<any>;
} }
@ -58,115 +61,165 @@ export class NineStore implements NineStoreInterface {
private expires: number; private expires: number;
constructor(options: Options) { constructor(options: Options) {
const { store, namespace, expires } = this.formatOptions(options) const { store, namespace, expires } = this.formatOptions(options);
this.store = store this.store = store;
this.namespace = namespace this.namespace = namespace;
this.expires = expires this.expires = expires;
} }
public formatOptions(options: Options){ public formatOptions(options: Options) {
const { store, expires = 1000 * 60 * 60 * 24 * 3, namespace = 'chat'} = options const { store, expires = 1000 * 60 * 60 * 24 * 3, namespace = 'chat' } = options;
return { store, namespace, expires } return { store, namespace, expires };
} }
public generateKey(key){ public generateKey(key: any) {
return this.namespace ? `${this.namespace}-${key}` : key return this.namespace ? `${this.namespace}-${key}` : key;
} }
public async getData(id: string ): Promise<any> { public async getData(id: string): Promise<any> {
const res = await this.store.get(id) const res = await this.store.get(id);
return res return res;
} }
public async setData(message, expires = this.expires): Promise<void> { public async setData(message: MessageInfo, expires = this.expires): Promise<void> {
await this.store.set(message.id, message, expires) await this.store.set(message.id, message, expires);
} }
/** /**
* @desc prompt和parentMessageId * @desc prompt和parentMessageId
* maxRounds限制最大轮次的对话 maxModelToken, maxResponseTokens token计算出最大容量 * maxRounds限制最大轮次的对话 maxModelToken, maxResponseTokens token计算出最大容量
*/ */
public async buildMessageFromParentMessageId(text: string, options: BuildMessageOptions){ public async buildMessageFromParentMessageId(text: string, options: BuildMessageOptions) {
let { maxRounds, maxModelToken, maxResponseTokens, systemMessage = '', name } = options let { maxRounds, maxModelToken, maxResponseTokens, systemMessage = '', name, imageUrl, model, activeModel } = options;
let { parentMessageId } = options let { parentMessageId } = options;
let messages = [] let messages = [];
let nextNumTokensEstimate = 0 let nextNumTokensEstimate = 0;
// messages.push({ role: 'system', content: systemMessage, name })
if (systemMessage) { if (systemMessage) {
messages.push({ role: 'system', content: systemMessage }) const specialModels = ['gemini-pro', 'ERNIE', 'qwen', 'SparkDesk', 'hunyuan'];
const isSpecialModel = activeModel && specialModels.some((specialModel) => activeModel.includes(specialModel));
if (isSpecialModel) {
messages.push({ role: 'user', content: systemMessage, name });
messages.push({ role: 'assistant', content: '好的', name });
} else {
messages.push({ role: 'system', content: systemMessage, name });
}
} }
const systemMessageOffset = messages.length const systemMessageOffset = messages.length;
let round = 0 let round = 0;
let nextMessages = text ? messages.concat([{ role: 'user', content: text, name }]) : messages // 特殊处理 gpt-4-vision-preview 模型
do { if (activeModel === 'gpt-4-vision-preview' && imageUrl) {
// let parentId = '1bf30262-8f25-4a03-88ad-9d42d55e6f0b' const content = [
/* 没有parentMessageId就没有历史 直接返回 */ {
if(!parentMessageId){ type: 'text',
break; text: text,
},
{
type: 'image_url',
image_url: {
url: imageUrl,
},
},
];
messages.push({ role: 'user', content: content, name });
} else {
// 处理 gpt-4-all 模型
if (model === 'gpt-4-all' && imageUrl) {
text = imageUrl + '\n' + text;
} }
const parentMessage = await this.getData(parentMessageId) messages.push({ role: 'user', content: text, name });
}
// Logger.debug(`发送的参数:${messages}`)
if(!parentMessage){ let nextMessages = messages;
do {
// let parentId = '1bf30262-8f25-4a03-88ad-9d42d55e6f0b'
/* 没有parentMessageId就没有历史 直接返回 */
if (!parentMessageId) {
break; break;
} }
const { text, name, role } = parentMessage const parentMessage = await this.getData(parentMessageId);
if (!parentMessage) {
break;
}
const { text, name, role, imageUrl } = parentMessage;
let content = text; // 默认情况下使用text作为content
// 特别处理包含 imageUrl 的消息
if (role === 'user' && imageUrl) {
if (activeModel === 'gpt-4-vision-preview') {
content = [
{ type: 'text', text: text },
{ type: 'image_url', image_url: { url: imageUrl } },
];
}
}
/* 将本轮消息插入到列表中 */ /* 将本轮消息插入到列表中 */
nextMessages = nextMessages.slice(0, systemMessageOffset).concat([ nextMessages = nextMessages.slice(0, systemMessageOffset).concat([
{ role, content: text, name }, { role, content, name }, // 使用调整后的content
...nextMessages.slice(systemMessageOffset) ...nextMessages.slice(systemMessageOffset),
]) ]);
round++
// Logger.debug(`nextMessages${JSON.stringify(nextMessages, null, 2)}`);
round++;
/* 如果超出了限制的最大轮次 就退出 不包含本次发送的本身 */ /* 如果超出了限制的最大轮次 就退出 不包含本次发送的本身 */
if(maxRounds && round >= maxRounds){ if (maxRounds && round >= maxRounds) {
break; break;
} }
/* 如果传入maxModelToken maxResponseTokens 则判断是否超过边界 */ /* 如果传入maxModelToken maxResponseTokens 则判断是否超过边界 */
if(maxModelToken && maxResponseTokens){ if (maxModelToken && maxResponseTokens) {
const maxNumTokens = maxModelToken - maxResponseTokens // 模型最大token限制减去限制回复剩余空间 const maxNumTokens = maxModelToken - maxResponseTokens; // 模型最大token限制减去限制回复剩余空间
/* 当前的对话历史列表合并的总token容量 */ /* 当前的对话历史列表合并的总token容量 */
nextNumTokensEstimate = await this._getTokenCount(nextMessages) nextNumTokensEstimate = await this._getTokenCount(nextMessages);
/* 200是添加的一个安全区间 防止少量超过 待优化 */ /* 200是添加的一个安全区间 防止少量超过 待优化 */
const isValidPrompt = nextNumTokensEstimate + 200 <= maxNumTokens const isValidPrompt = nextNumTokensEstimate + 200 <= maxNumTokens;
/* 如果大于这个区间了说明本轮加入之后导致超过限制、则递归删除头部的对话轮次来保证不出边界 */ /* 如果大于这个区间了说明本轮加入之后导致超过限制、则递归删除头部的对话轮次来保证不出边界 */
if(!isValidPrompt){ if (!isValidPrompt) {
nextMessages = this._recursivePruning(nextMessages, maxNumTokens, systemMessage) nextMessages = this._recursivePruning(nextMessages, maxNumTokens, systemMessage);
} }
} }
parentMessageId = parentMessage.parentMessageId parentMessageId = parentMessage.parentMessageId;
} while (true); } while (true);
const maxTokens = Math.max( const maxTokens = Math.max(1, Math.min(maxModelToken - nextNumTokensEstimate, maxResponseTokens));
1,
Math.min(maxModelToken - nextNumTokensEstimate, maxResponseTokens)
)
// Logger.debug(`本轮调用:模型:${model}`) // Logger.debug(`本轮调用:模型:${model}`)
console.log('本次携带上下文的长度',nextMessages.length, nextNumTokensEstimate ) console.log('本次携带上下文的长度', nextMessages.length, nextNumTokensEstimate);
return { context: nextMessages, round: nextMessages.length, historyToken:nextNumTokensEstimate } return { context: nextMessages, round: nextMessages.length, historyToken: nextNumTokensEstimate };
} }
protected _getTokenCount(messages: any[]) { protected _getTokenCount(messages: any[]) {
let text = messages.reduce( (pre: string, cur: any) => { let text = messages.reduce((pre: string, cur: any) => {
return pre+=cur.content // 检查cur.content是否为数组
}, '') if (Array.isArray(cur.content)) {
text = text.replace(/<\|endoftext\|>/g, '') // 提取并连接数组中的文本元素
return tokenizer.encode(text).length const contentText = cur.content
} .filter((item: { type: string }) => item.type === 'text')
.map((item: { text: any }) => item.text)
.join(' ');
return pre + contentText;
} else {
// 如果不是数组,则直接添加
return pre + (cur.content || '');
}
}, '');
text = text.replace(/<\|endoftext\|>/g, '');
return tokenizer.encode(text).length;
}
/* 递归删除 当token超过模型限制容量 删除到在限制区域内 */ /* 递归删除 当token超过模型限制容量 删除到在限制区域内 */
protected _recursivePruning( protected _recursivePruning(messages: MessageInfo[], maxNumTokens: number, systemMessage: string) {
messages: MessageInfo[], const currentTokens = this._getTokenCount(messages);
maxNumTokens: number,
systemMessage: string
) {
const currentTokens = this._getTokenCount(messages)
if (currentTokens <= maxNumTokens) { if (currentTokens <= maxNumTokens) {
return messages return messages;
} }
/* 有系统预设则跳过第一条删除 没有则直接删除 */ /* 有系统预设则跳过第一条删除 没有则直接删除 */
messages.splice(systemMessage ? 1 : 0, 1) messages.splice(systemMessage ? 1 : 0, 1);
return this._recursivePruning(messages, maxNumTokens, systemMessage) return this._recursivePruning(messages, maxNumTokens, systemMessage);
} }
public getUuid(){ public getUuid() {
return uuidv4() return uuidv4();
} }
} }

View File

@ -32,7 +32,6 @@ export class MidjourneyService {
private redisCacheService: RedisCacheService, private redisCacheService: RedisCacheService,
) {} ) {}
private lockPrompt = []; private lockPrompt = [];
/* 睡眠 xs */ /* 睡眠 xs */
@ -111,6 +110,9 @@ export class MidjourneyService {
await this.updateDrawData(jobData, drawRes); await this.updateDrawData(jobData, drawRes);
/* 存完解锁当前文件 */ /* 存完解锁当前文件 */
this.lockPrompt = this.lockPrompt.filter((item) => item !== drawInfo.randomDrawId); this.lockPrompt = this.lockPrompt.filter((item) => item !== drawInfo.randomDrawId);
/* 只有在画成功后才扣分*/
this.drawSuccess(jobData);
} }
return true; return true;
@ -160,16 +162,16 @@ export class MidjourneyService {
const { id, content, channel_id, attachments = [], timestamp, durationSpent } = drawRes; const { id, content, channel_id, attachments = [], timestamp, durationSpent } = drawRes;
const { filename, url, proxy_url, width, height, size } = attachments[0]; const { filename, url, proxy_url, width, height, size } = attachments[0];
/* 将图片存入cos */ /* 将图片存入cos */
const mjNotSaveImg = await this.globalConfigService.getConfigs(['mjNotSaveImg']) const mjNotSaveImg = await this.globalConfigService.getConfigs(['mjNotSaveImg']);
let cosUrl = '' let cosUrl = '';
if(!Number(mjNotSaveImg) || Number(mjNotSaveImg) === 0){ if (!Number(mjNotSaveImg) || Number(mjNotSaveImg) === 0) {
Logger.debug(`------> 开始上传图片!!!`, 'MidjourneyService'); Logger.debug(`------> 开始上传图片!!!`, 'MidjourneyService');
const startDate = new Date(); const startDate = new Date();
cosUrl = await this.uploadService.uploadFileFromUrl({ filename, url }); cosUrl = await this.uploadService.uploadFileFromUrl({ filename, url });
const endDate = new Date(); const endDate = new Date();
Logger.debug(`本次图片上传耗时为${(endDate.getTime() - startDate.getTime()) / 1000}`, 'MidjourneyService'); Logger.debug(`本次图片上传耗时为${(endDate.getTime() - startDate.getTime()) / 1000}`, 'MidjourneyService');
}else{ } else {
console.log('本次不存图片了') console.log('本次不存图片了');
} }
/* 记录当前图片存储方式 方便后续对不同平台图片压缩 */ /* 记录当前图片存储方式 方便后续对不同平台图片压缩 */
const cosType = await this.uploadService.getUploadType(); const cosType = await this.uploadService.getUploadType();
@ -181,11 +183,11 @@ export class MidjourneyService {
fileInfo: JSON.stringify({ width, height, size, filename, cosUrl, cosType }), fileInfo: JSON.stringify({ width, height, size, filename, cosUrl, cosType }),
extend: this.removeEmoji(JSON.stringify(drawRes)), extend: this.removeEmoji(JSON.stringify(drawRes)),
durationSpent, durationSpent,
isSaveImg: !Number(mjNotSaveImg) || Number(mjNotSaveImg) === 0, isSaveImg: !Number(mjNotSaveImg) || Number(mjNotSaveImg) === 0,
}; };
await this.midjourneyEntity.update({ id: jobData.id }, drawInfo); await this.midjourneyEntity.update({ id: jobData.id }, drawInfo);
} catch (error) { } catch (error) {
console.log('TODO->存储图片失败, ', jobData,error); console.log('TODO->存储图片失败, ', jobData, error);
} }
} }
@ -734,17 +736,15 @@ export class MidjourneyService {
take: size, take: size,
skip: (page - 1) * size, skip: (page - 1) * size,
}); });
const mjProxyImgUrl = await this.globalConfigService.getConfigs(['mjProxyImgUrl']) const mjProxyImgUrl = await this.globalConfigService.getConfigs(['mjProxyImgUrl']);
rows.forEach((item: any) => { rows.forEach((item: any) => {
try { try {
const { extend, isSaveImg, fileInfo } = item; const { extend, isSaveImg, fileInfo } = item;
const originUrl = JSON.parse(extend)?.attachments[0]?.url const originUrl = JSON.parse(extend)?.attachments[0]?.url;
item.fileInfo = this.formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl); item.fileInfo = this.formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl);
item.isGroup = JSON.parse(extend)?.components[0]?.components[0].label === "U1"; item.isGroup = JSON.parse(extend)?.components[0]?.components[0].label === 'U1';
item.originUrl = originUrl item.originUrl = originUrl;
} catch (error) { } catch (error) {}
}
}); });
const countQueue = await this.midjourneyEntity.count({ where: { isDelete: 0, status: In([1, 2]) } }); const countQueue = await this.midjourneyEntity.count({ where: { isDelete: 0, status: In([1, 2]) } });
const data: any = { rows: formatCreateOrUpdateDate(rows), count, countQueue }; const data: any = { rows: formatCreateOrUpdateDate(rows), count, countQueue };
@ -755,15 +755,15 @@ export class MidjourneyService {
} }
/* 格式化fileinfo 对于不同平台的图片进行压缩 */ /* 格式化fileinfo 对于不同平台的图片进行压缩 */
formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl) { formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl) {
if (!fileInfo) return {}; if (!fileInfo) return {};
let parseFileInfo: any = null let parseFileInfo: any = null;
try { try {
parseFileInfo = JSON.parse(fileInfo); parseFileInfo = JSON.parse(fileInfo);
} catch (error) { } catch (error) {
parseFileInfo = null parseFileInfo = null;
} }
if(!parseFileInfo) return; if (!parseFileInfo) return;
const { url, filename, size, cosUrl, width, height } = parseFileInfo; const { url, filename, size, cosUrl, width, height } = parseFileInfo;
const targetSize = 310; // 目标宽度或高度 const targetSize = 310; // 目标宽度或高度
@ -786,10 +786,10 @@ export class MidjourneyService {
} }
parseFileInfo.thumbImg = thumbImg; parseFileInfo.thumbImg = thumbImg;
/* 如果配置了不存储图片 则 isSaceImg 为false的则需要使用反代地址拼接 */ /* 如果配置了不存储图片 则 isSaceImg 为false的则需要使用反代地址拼接 */
if(!isSaveImg){ if (!isSaveImg) {
const proxyImgUrl = `${mjProxyImgUrl}/mj/pipe?url=${originUrl}` const proxyImgUrl = `${mjProxyImgUrl}/mj/pipe?url=${originUrl}`;
parseFileInfo.thumbImg = proxyImgUrl parseFileInfo.thumbImg = proxyImgUrl;
parseFileInfo.cosUrl = proxyImgUrl parseFileInfo.cosUrl = proxyImgUrl;
} }
return parseFileInfo; return parseFileInfo;
} }
@ -859,8 +859,8 @@ export class MidjourneyService {
// return; // return;
// } // }
const count = await this.midjourneyEntity.count({ where: { userId: id, isDelete: 0, status: In([1, 2]) } }); const count = await this.midjourneyEntity.count({ where: { userId: id, isDelete: 0, status: In([1, 2]) } });
const mjLimitCount = await this.globalConfigService.getConfigs(['mjLimitCount']) const mjLimitCount = await this.globalConfigService.getConfigs(['mjLimitCount']);
const max = mjLimitCount ? Number(mjLimitCount) : 2 const max = mjLimitCount ? Number(mjLimitCount) : 2;
if (count >= max) { if (count >= max) {
throw new HttpException(`当前管理员限制单用户同时最多能执行${max}个任务`, HttpStatus.BAD_REQUEST); throw new HttpException(`当前管理员限制单用户同时最多能执行${max}个任务`, HttpStatus.BAD_REQUEST);
} }
@ -870,11 +870,21 @@ export class MidjourneyService {
async drawFailed(jobData) { async drawFailed(jobData) {
const { id, userId, action } = jobData; const { id, userId, action } = jobData;
/* 退还余额 放大图片类型2是1 其他都是4 */ /* 退还余额 放大图片类型2是1 其他都是4 */
const amount = action === 2 ? 1 : 4; // const amount = action === 2 ? 1 : 4;
await this.userBalanceService.refundMjBalance(userId, amount); // await this.userBalanceService.refundMjBalance(userId, amount);
await this.midjourneyEntity.update({ id }, { status: 4 }); await this.midjourneyEntity.update({ id }, { status: 4 });
} }
/* 绘图成功扣费 */
async drawSuccess(jobData) {
const { id, userId, action } = jobData;
/* 扣除余额 放大图片类型2是1 其他都是4 */
const amount = action === 2 ? 1 : 4;
Logger.debug(`绘画完成,执行扣费,扣除费用:${amount}积分。`);
await this.userBalanceService.refundMjBalance(userId, -amount);
await this.midjourneyEntity.update({ id }, { status: 3 });
}
/* 获取绘画列表 */ /* 获取绘画列表 */
async getList(params: GetListDto) { async getList(params: GetListDto) {
const { page = 1, size = 20, rec, userId, status } = params; const { page = 1, size = 20, rec, userId, status } = params;
@ -902,17 +912,15 @@ export class MidjourneyService {
skip: (page - 1) * size, skip: (page - 1) * size,
select: ['fileInfo', 'extend', 'prompt', 'createdAt', 'id', 'extend', 'fullPrompt', 'rec', 'isSaveImg'], select: ['fileInfo', 'extend', 'prompt', 'createdAt', 'id', 'extend', 'fullPrompt', 'rec', 'isSaveImg'],
}); });
const mjProxyImgUrl = await this.globalConfigService.getConfigs(['mjProxyImgUrl']) const mjProxyImgUrl = await this.globalConfigService.getConfigs(['mjProxyImgUrl']);
rows.forEach((item: any) => { rows.forEach((item: any) => {
try { try {
const { extend, isSaveImg, fileInfo } = item; const { extend, isSaveImg, fileInfo } = item;
const originUrl = JSON.parse(extend)?.attachments[0]?.url const originUrl = JSON.parse(extend)?.attachments[0]?.url;
item.fileInfo = this.formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl); item.fileInfo = this.formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl);
item.isGroup = JSON.parse(extend)?.components[0]?.components[0].label === "U1"; item.isGroup = JSON.parse(extend)?.components[0]?.components[0].label === 'U1';
item.originUrl = originUrl item.originUrl = originUrl;
} catch (error) { } catch (error) {}
}
}); });
if (Number(size) === 999) { if (Number(size) === 999) {
@ -931,10 +939,10 @@ export class MidjourneyService {
} }
/* */ /* */
async getFullPrompt(id: number){ async getFullPrompt(id: number) {
const m = await this.midjourneyEntity.findOne({where: {id}}) const m = await this.midjourneyEntity.findOne({ where: { id } });
if(!m) return '' if (!m) return '';
const { fullPrompt } = m const { fullPrompt } = m;
return fullPrompt; return fullPrompt;
} }
@ -953,27 +961,25 @@ export class MidjourneyService {
skip: (page - 1) * size, skip: (page - 1) * size,
}); });
const userIds = rows.map((item: any) => item.userId).filter( id => id < 100000); const userIds = rows.map((item: any) => item.userId).filter((id) => id < 100000);
const userInfos = await this.userEntity.find({ where: { id: In(userIds) }, select: ['id', 'username', 'avatar', 'email'] }); const userInfos = await this.userEntity.find({ where: { id: In(userIds) }, select: ['id', 'username', 'avatar', 'email'] });
rows.forEach((item: any) => { rows.forEach((item: any) => {
item.userInfo = userInfos.find((user) => user.id === item.userId); item.userInfo = userInfos.find((user) => user.id === item.userId);
}); });
const mjProxyImgUrl = await this.globalConfigService.getConfigs(['mjProxyImgUrl']) const mjProxyImgUrl = await this.globalConfigService.getConfigs(['mjProxyImgUrl']);
rows.forEach((item: any) => { rows.forEach((item: any) => {
try { try {
const { extend, isSaveImg, fileInfo } = item; const { extend, isSaveImg, fileInfo } = item;
const originUrl = JSON.parse(extend)?.attachments[0]?.url const originUrl = JSON.parse(extend)?.attachments[0]?.url;
item.fileInfo = this.formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl); item.fileInfo = this.formatFileInfo(fileInfo, isSaveImg, mjProxyImgUrl, originUrl);
// item.isGroup = JSON.parse(extend)?.components[0]?.components.length === 5; // item.isGroup = JSON.parse(extend)?.components[0]?.components.length === 5;
item.isGroup = JSON.parse(extend)?.components[0]?.components[0].label === "U1"; item.isGroup = JSON.parse(extend)?.components[0]?.components[0].label === 'U1';
item.originUrl = originUrl item.originUrl = originUrl;
} catch (error) { } catch (error) {}
}
}); });
if (req.user.role !== 'super') { if (req.user.role !== 'super') {
rows.forEach((item: any) => { rows.forEach((item: any) => {
if(item.userInfo && item.userInfo.email){ if (item.userInfo && item.userInfo.email) {
item.userInfo.email = item.userInfo.email.replace(/(.{2}).+(.{2}@.+)/, '$1****$2'); item.userInfo.email = item.userInfo.email.replace(/(.{2}).+(.{2}@.+)/, '$1****$2');
} }
}); });
@ -1021,38 +1027,38 @@ export class MidjourneyService {
} }
} }
async setPrompt(req: Request, body){ async setPrompt(req: Request, body) {
try { try {
const { prompt, status, isCarryParams, title, order, id, aspect } = body const { prompt, status, isCarryParams, title, order, id, aspect } = body;
if(id){ if (id) {
return await this.mjPromptsEntity.update({id}, {prompt, status, isCarryParams, order, aspect}) return await this.mjPromptsEntity.update({ id }, { prompt, status, isCarryParams, order, aspect });
}else{ } else {
return await this.mjPromptsEntity.save({prompt, status, isCarryParams, title, order, aspect}) return await this.mjPromptsEntity.save({ prompt, status, isCarryParams, title, order, aspect });
} }
} catch (error) { } catch (error) {
console.log('error: ', error); console.log('error: ', error);
} }
} }
async delPrompt(req: Request, body){ async delPrompt(req: Request, body) {
const {id} = body const { id } = body;
if(!id) { if (!id) {
throw new HttpException('非法操作!', HttpStatus.BAD_REQUEST); throw new HttpException('非法操作!', HttpStatus.BAD_REQUEST);
} }
return await this.mjPromptsEntity.delete({id}) return await this.mjPromptsEntity.delete({ id });
} }
async queryPrompt(){ async queryPrompt() {
return await this.mjPromptsEntity.find({ return await this.mjPromptsEntity.find({
order: { order: 'DESC' }, order: { order: 'DESC' },
}) });
} }
async proxyImg(params){ async proxyImg(params) {
const { url } = params const { url } = params;
if(!url) return if (!url) return;
const response = await axios.get(url, { responseType: 'arraybuffer' }); const response = await axios.get(url, { responseType: 'arraybuffer' });
const base64 = Buffer.from(response.data).toString('base64'); const base64 = Buffer.from(response.data).toString('base64');
return base64 return base64;
} }
} }

View File

@ -56,4 +56,6 @@ export class SetModelDto {
//设置token计费 //设置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 })
tokenFeeRatio: number;
} }

View File

@ -66,4 +66,7 @@ export class ModelsEntity extends BaseEntity {
@Column({ comment: '是否使用token计费: 0:不是 1是', default: 0 }) @Column({ comment: '是否使用token计费: 0:不是 1是', default: 0 })
isTokenBased: boolean; isTokenBased: boolean;
@Column({ comment: 'token计费比例', default: 0 })
tokenFeeRatio: number;
} }

View File

@ -50,7 +50,7 @@ export class QueueService implements OnApplicationBootstrap {
/* 绘图和图生图扣除余额4 */ /* 绘图和图生图扣除余额4 */
this.jobIds.push(job.id); this.jobIds.push(job.id);
/* 扣费 */ /* 扣费 */
await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4); // await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return true; return true;
} }
@ -69,7 +69,7 @@ export class QueueService implements OnApplicationBootstrap {
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000; 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 }); 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); // await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 1, 1);
this.jobIds.push(job.id); this.jobIds.push(job.id);
return; return;
} }
@ -83,7 +83,7 @@ export class QueueService implements OnApplicationBootstrap {
const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout }); const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id); this.jobIds.push(job.id);
/* 扣费 */ /* 扣费 */
await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4); // await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return; return;
} }
@ -95,7 +95,7 @@ export class QueueService implements OnApplicationBootstrap {
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000; 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 }); const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id); this.jobIds.push(job.id);
await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4); // await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return; return;
} }
@ -107,7 +107,7 @@ export class QueueService implements OnApplicationBootstrap {
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000; 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 }); const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id); this.jobIds.push(job.id);
await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4); // await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return; return;
} }
@ -119,7 +119,7 @@ export class QueueService implements OnApplicationBootstrap {
const timeout = (await this.globalConfigService.getConfigs(['mjTimeoutMs'])) || 200000; 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 }); const job = await this.mjDrawQueue.add('mjDraw', { id: res.id, action, userId: req.user.id }, { delay: 1000, timeout: +timeout });
this.jobIds.push(job.id); this.jobIds.push(job.id);
await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4); // await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', 4, 4);
return; return;
} }
} }

View File

@ -300,8 +300,8 @@ export class UserBalanceService {
/* 记录修改使用的token */ /* 记录修改使用的token */
const updateBalance = { [updateKey]: b[updateKey] - amount < 0 ? 0 : b[updateKey] - amount, [useKey]: b[useKey] + UseAmount }; const updateBalance = { [updateKey]: b[updateKey] - amount < 0 ? 0 : b[updateKey] - amount, [useKey]: b[useKey] + UseAmount };
/* 记录修改使用的次数 mj不需要 */ /* 记录修改使用的次数 mj不需要 */
useKey === 'useModel3Token' && (updateBalance['useModel3Count'] = b['useModel3Count'] + 1); useKey === 'useModel3Token' && (updateBalance['useModel3Count'] = b['useModel3Count'] + amount);
useKey === 'useModel4Token' && (updateBalance['useModel4Count'] = b['useModel4Count'] + 1); useKey === 'useModel4Token' && (updateBalance['useModel4Count'] = b['useModel4Count'] + amount);
const result = await this.userBalanceEntity.update({ userId }, updateBalance); const result = await this.userBalanceEntity.update({ userId }, updateBalance);
if (result.affected === 0) { if (result.affected === 0) {
throw new HttpException('消费余额失败!', HttpStatus.BAD_REQUEST); throw new HttpException('消费余额失败!', HttpStatus.BAD_REQUEST);
@ -552,7 +552,9 @@ export class UserBalanceService {
} }
/* MJ绘画失败退款 */ /* MJ绘画失败退款 */
async refundMjBalance(userId, amount) {} async refundMjBalance(userId, amount) {
return await this.deductFromBalance(userId, 'mjDraw', -amount);
}
/* V1.5升级将旧版本余额并入到新表 */ /* V1.5升级将旧版本余额并入到新表 */
async upgradeBalance() { async upgradeBalance() {

BIN
service/templates/.DS_Store vendored Normal file

Binary file not shown.