diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ae036d..ff0f4eff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## v4.2.4 - 功能新增:管理后台支持设置默认昵称 - 功能优化:支持 Suno v4.5 模型支持 +- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。 +- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文 ## v4.2.3 diff --git a/database/update-v4.2.3.1.sql b/database/update-v4.2.3.1.sql new file mode 100644 index 00000000..5029b160 --- /dev/null +++ b/database/update-v4.2.3.1.sql @@ -0,0 +1,3 @@ +INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES +(4, 'privacy', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"default_nickname\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"# 隐私政策\\n\\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。\",\"updated\":true}'), +(5, 'agreement', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"default_nickname\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"# 用户协议\\n\\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。\",\"updated\":true}'); diff --git a/database/update-v4.2.3.sql b/database/update-v4.2.3.sql index 0cdf9f14..98ac1d2f 100644 --- a/database/update-v4.2.3.sql +++ b/database/update-v4.2.3.sql @@ -2,4 +2,4 @@ ALTER TABLE `chatgpt_chat_models` ADD `category` VARCHAR(1024) NOT NULL DEFAULT ALTER TABLE `chatgpt_chat_models` ADD `description` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '模型类型描述' AFTER `id`; ALTER TABLE `chatgpt_orders` DROP `deleted_at`; ALTER TABLE `chatgpt_chat_history` DROP `deleted_at`; -ALTER TABLE `chatgpt_chat_items` DROP `deleted_at`; +ALTER TABLE `chatgpt_chat_items` DROP `deleted_at`; \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 82dd37b7..8e7ac086 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -28,6 +28,7 @@ "markdown-it": "^13.0.1", "markdown-it-emoji": "^2.0.0", "markdown-it-mathjax3": "^4.3.2", + "marked": "^15.0.11", "markmap-common": "^0.16.0", "markmap-lib": "^0.16.1", "markmap-toolbar": "^0.17.0", @@ -8721,6 +8722,17 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/marked": { + "version": "15.0.11", + "resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.11.tgz", + "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/markmap-common": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/markmap-common/-/markmap-common-0.16.0.tgz", diff --git a/web/package.json b/web/package.json index a7ea40e6..b6bdf84b 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,11 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@better-scroll/core": "^2.5.1", + "@better-scroll/mouse-wheel": "^2.5.1", + "@better-scroll/observe-dom": "^2.5.1", + "@better-scroll/pull-up": "^2.5.1", + "@better-scroll/scroll-bar": "^2.5.1", "@element-plus/icons-vue": "^2.3.1", "animate.css": "^4.1.1", "axios": "^0.27.2", @@ -23,6 +28,7 @@ "markdown-it": "^13.0.1", "markdown-it-emoji": "^2.0.0", "markdown-it-mathjax3": "^4.3.2", + "marked": "^15.0.11", "markmap-common": "^0.16.0", "markmap-lib": "^0.16.1", "markmap-toolbar": "^0.17.0", @@ -32,11 +38,6 @@ "pinia": "^2.1.4", "qrcode": "^1.5.3", "qs": "^6.11.1", - "@better-scroll/core": "^2.5.1", - "@better-scroll/mouse-wheel": "^2.5.1", - "@better-scroll/observe-dom": "^2.5.1", - "@better-scroll/pull-up": "^2.5.1", - "@better-scroll/scroll-bar": "^2.5.1", "sortablejs": "^1.15.0", "three": "^0.128.0", "vant": "^4.5.0", diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue index a974dd84..a7aec543 100644 --- a/web/src/components/ChatPrompt.vue +++ b/web/src/components/ChatPrompt.vue @@ -29,7 +29,9 @@ -
+
+
+
{{ dateFormat(data.created_at) }}
-
+
+
+
{ processFiles() }) @@ -475,4 +482,39 @@ const isExternalImg = (link, files) => { } } + +.operations + display none + position absolute + right 5px + top 5px + +.text-box + &:hover + .operations + display flex + gap 5px + +.op-edit + cursor pointer + color #409eff + font-size 16px + + &:hover + color darken(#409eff, 10%) + +.position-relative { + position: relative; +} + +.action-buttons { + position: absolute; + top: 10px; + right: 10px; + display: none; +} + +.content:hover .action-buttons { + display: block; +} diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue index efe2ee5f..4ed50237 100644 --- a/web/src/components/ChatReply.vue +++ b/web/src/components/ChatReply.vue @@ -26,7 +26,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -233,9 +233,8 @@ const stopSynthesis = () => { } // 重新生成 -const reGenerate = (prompt) => { - console.log(prompt) - emits('regen', prompt) +const reGenerate = () => { + emits('regen') } diff --git a/web/src/views/ChatPlus.vue b/web/src/views/ChatPlus.vue index aaa907e6..39910071 100644 --- a/web/src/views/ChatPlus.vue +++ b/web/src/views/ChatPlus.vue @@ -264,7 +264,7 @@
- + = 2) { + // 从后往前找,如果最后一条是AI回复,再往前一条是用户消息 + if (chatData.value[chatData.value.length - 1].type === 'reply') { + // 删除AI回复 + chatData.value.pop(); + + // 如果此时最后一条是用户消息,也删除它 + if (chatData.value.length > 0 && chatData.value[chatData.value.length - 1].type === 'prompt') { + // 保存用户消息内容,填入输入框 + const userPrompt = chatData.value[chatData.value.length - 1].content; + // 删除用户消息 + chatData.value.pop(); + // 填入输入框 + prompt.value = userPrompt; + } + } + } + + // 将光标定位到输入框并聚焦 + nextTick(() => { + if (inputRef.value) { + inputRef.value.focus(); + // 触发输入事件以更新文本高度 + onInput({ keyCode: null }); + } + }); +} + +// 编辑用户消息 +const editUserPrompt = function (messageId) { + // 找到要编辑的消息及其索引 + let messageIndex = -1; + let messageContent = ''; + + for (let i = 0; i < chatData.value.length; i++) { + if (chatData.value[i].id === messageId) { + messageIndex = i; + messageContent = chatData.value[i].content; + break; + } + } + + if (messageIndex === -1) return; + + // 弹出编辑对话框 + ElMessageBox.prompt('', '编辑消息', { + confirmButtonText: '确定', + cancelButtonText: '取消', + inputValue: messageContent, + inputType: 'textarea', + customClass: 'edit-prompt-dialog', + roundButton: true + }).then(({ value }) => { + if (value.trim() === '') { + ElMessage.warning('消息内容不能为空'); + return; + } + + // 更新用户消息 + chatData.value[messageIndex].content = value; + + // 移除该消息之后的所有消息 + chatData.value = chatData.value.slice(0, messageIndex + 1); + + // 添加空回复消息 + const _role = getRoleById(roleId.value); + chatData.value.push({ + chat_id: chatId, + role_id: roleId.value, + type: 'reply', + id: randString(32), + icon: _role['icon'], + content: '', + }); + + disableInput(false); + + // 发送编辑后的消息 + store.socket.conn.send( + JSON.stringify({ + channel: 'chat', + type: 'text', + body: { + role_id: roleId.value, + model_id: modelID.value, + chat_id: chatId.value, + content: value, + tools: toolSelected.value, + stream: stream.value, + edit_message: true + }, + }) + ); + }).catch(() => { + // 取消编辑 + }); } const chatName = ref('') diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 07d2887e..7111f477 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -40,6 +40,16 @@ @keyup="handleKeyup" /> + +
+ + 我已阅读并同意 + 《用户协议》 + 和 + 《隐私政策》 + +
+
{ // 检查URL中是否存在token参数 @@ -110,6 +132,34 @@ onMounted(() => { title.value = 'Geek-AI' }) + // 获取用户协议 + httpGet('/api/config/get?key=agreement') + .then((res) => { + if (res.data && res.data.content) { + agreementContent.value = res.data.content + } else { + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。' + } + }) + .catch((e) => { + console.warn(e) + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。' + }) + + // 获取隐私政策 + httpGet('/api/config/get?key=privacy') + .then((res) => { + if (res.data && res.data.content) { + privacyContent.value = res.data.content + } else { + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。' + } + }) + .catch((e) => { + console.warn(e) + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。' + }) + getLicenseInfo() .then((res) => { licenseConfig.value = res.data @@ -141,6 +191,16 @@ const handleKeyup = (e) => { } const login = async function () { + if (!ruleForm.agreement) { + agreementError.value = true + isShaking.value = true + setTimeout(() => { + isShaking.value = false + }, 500) + showMessageError('请先阅读并同意用户协议') + return + } + await ruleFormRef.value.validate(async (valid) => { if (valid) { if (enableVerify.value) { @@ -170,8 +230,126 @@ const doLogin = (verifyData) => { showMessageError('登录失败,' + e.message) }) } + +const agreementError = ref(false) +const isShaking = ref(false) + +const handleAgreementChange = () => { + agreementError.value = !ruleForm.agreement + if (agreementError.value) { + isShaking.value = true + setTimeout(() => { + isShaking.value = false + }, 500) + } +} + +const openAgreement = () => { + // 使用弹窗显示用户协议内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(agreementContent.value)}
`, + '用户协议', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ) +} + +const openPrivacy = () => { + // 使用弹窗显示隐私政策内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(privacyContent.value)}
`, + '隐私政策', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ) +} + + diff --git a/web/src/views/Register.vue b/web/src/views/Register.vue index 864a8fdb..b6663206 100644 --- a/web/src/views/Register.vue +++ b/web/src/views/Register.vue @@ -62,6 +62,17 @@
+ +
+ + 我已阅读并同意 + 《用户协议》 + 和 + 《隐私政策》 + +
+
+ @@ -97,8 +108,9 @@ import AccountTop from "@/components/AccountTop.vue"; import AccountBg from "@/components/AccountBg.vue"; import { httpGet, httpPost } from "@/utils/http"; -import { ElMessage } from "element-plus"; +import { ElMessage, ElMessageBox } from "element-plus"; import { useRouter } from "vue-router"; +import MarkdownIt from "markdown-it"; import SendMsg from "@/components/SendMsg.vue"; import { arrayContains, isMobile } from "@/utils/libs"; @@ -119,6 +131,7 @@ const data = ref({ code: "", repass: "", invite_code: router.currentRoute.value.query["invite_code"], + agreement: false, }); const enableMobile = ref(false); @@ -130,6 +143,18 @@ const wxImg = ref("/images/wx.png"); const licenseConfig = ref({}); const enableVerify = ref(false); const captchaRef = ref(null); +const agreementError = ref(false); +const isShaking = ref(false); + +// 初始化markdown解析器 +const md = new MarkdownIt({ + html: true, + linkify: true, + typographer: true +}); + +const agreementContent = ref(''); +const privacyContent = ref(''); // 记录邀请码点击次数 if (data.value.invite_code) { @@ -168,6 +193,34 @@ getSystemInfo() ElMessage.error("获取系统配置失败:" + e.message); }); +// 获取用户协议 +httpGet('/api/config/get?key=agreement') + .then((res) => { + if (res.data && res.data.content) { + agreementContent.value = res.data.content; + } else { + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'; + } + }) + .catch((e) => { + console.warn(e); + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'; + }); + +// 获取隐私政策 +httpGet('/api/config/get?key=privacy') + .then((res) => { + if (res.data && res.data.content) { + privacyContent.value = res.data.content; + } else { + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'; + } + }) + .catch((e) => { + console.warn(e); + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'; + }); + getLicenseInfo() .then((res) => { licenseConfig.value = res.data; @@ -201,6 +254,16 @@ const submitRegister = () => { return showMessageError("请输入验证码"); } + if (!data.value.agreement) { + agreementError.value = true; + isShaking.value = true; + setTimeout(() => { + isShaking.value = false; + }, 500); + showMessageError("请先阅读并同意用户协议和隐私政策"); + return; + } + // 如果是用户名和密码登录,那么需要加载验证码 if (enableVerify.value && activeName.value === "username") { captchaRef.value.loadCaptcha(); @@ -228,6 +291,36 @@ const doSubmitRegister = (verifyData) => { showMessageError("注册失败," + e.message); }); }; + +const handleAgreementChange = () => { + agreementError.value = !data.value.agreement; +}; + +const openAgreement = () => { + // 使用弹窗显示用户协议内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(agreementContent.value)}
`, + '用户协议', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ); +}; + +const openPrivacy = () => { + // 使用弹窗显示隐私政策内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(privacyContent.value)}
`, + '隐私政策', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ); +}; + + diff --git a/web/src/views/admin/SysConfig.vue b/web/src/views/admin/SysConfig.vue index f3820f46..fddd2b99 100644 --- a/web/src/views/admin/SysConfig.vue +++ b/web/src/views/admin/SysConfig.vue @@ -311,6 +311,25 @@
+ + + + +
+ 保存 +
+
+
+ + + + +
+ 保存 +
+
+
+ @@ -418,6 +437,8 @@ const loading = ref(true); const systemFormRef = ref(null); const models = ref([]); const notice = ref(""); +const agreement = ref(""); +const privacy = ref(""); const license = ref({ is_active: false }); const menus = ref([]); const mjModels = ref([ @@ -460,6 +481,24 @@ onMounted(() => { ElMessage.error("公告信息失败: " + e.message); }); + // 加载用户协议 + httpGet("/api/admin/config/get?key=agreement") + .then((res) => { + agreement.value = res.data["content"] || ''; + }) + .catch((e) => { + ElMessage.error("加载用户协议失败: " + e.message); + }); + + // 加载隐私政策 + httpGet("/api/admin/config/get?key=privacy") + .then((res) => { + privacy.value = res.data["content"] || ''; + }) + .catch((e) => { + ElMessage.error("加载隐私政策失败: " + e.message); + }); + httpGet("/api/admin/model/list") .then((res) => { models.value = res.data; @@ -517,6 +556,22 @@ const save = function (key) { .catch((e) => { ElMessage.error("操作失败:" + e.message); }); + } else if (key === "agreement") { + httpPost("/api/admin/config/update", { key: key, config: { content: agreement.value, updated: true } }) + .then(() => { + ElMessage.success("操作成功!"); + }) + .catch((e) => { + ElMessage.error("操作失败:" + e.message); + }); + } else if (key === "privacy") { + httpPost("/api/admin/config/update", { key: key, config: { content: privacy.value, updated: true } }) + .then(() => { + ElMessage.success("操作成功!"); + }) + .catch((e) => { + ElMessage.error("操作失败:" + e.message); + }); } };