diff --git a/api/core/types/task.go b/api/core/types/task.go
index 599482af..afdcf504 100644
--- a/api/core/types/task.go
+++ b/api/core/types/task.go
@@ -70,17 +70,18 @@ type SdTaskParams struct {
// DallTask DALL-E task
type DallTask struct {
- ModelId uint `json:"model_id"`
- ModelName string `json:"model_name"`
- Id uint `json:"id"`
- UserId uint `json:"user_id"`
- Prompt string `json:"prompt"`
- N int `json:"n"`
- Quality string `json:"quality"`
- Size string `json:"size"`
- Style string `json:"style"`
- Power int `json:"power"`
- TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
+ ModelId uint `json:"model_id"`
+ ModelName string `json:"model_name"`
+ Image []string `json:"image,omitempty"`
+ Id uint `json:"id"`
+ UserId uint `json:"user_id"`
+ Prompt string `json:"prompt"`
+ N int `json:"n"`
+ Quality string `json:"quality"`
+ Size string `json:"size"`
+ Style string `json:"style"`
+ Power int `json:"power"`
+ TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type SunoTask struct {
diff --git a/api/handler/admin/moderation_handler.go b/api/handler/admin/moderation_handler.go
index c1d4b29c..86317332 100644
--- a/api/handler/admin/moderation_handler.go
+++ b/api/handler/admin/moderation_handler.go
@@ -244,7 +244,16 @@ func (h *ModerationHandler) UpdateModeration(c *gin.Context) {
return
}
- err := h.DB.Where("name", types.ConfigKeyModeration).FirstOrCreate(&model.Config{Name: types.ConfigKeyModeration, Value: utils.JsonEncode(data)}).Error
+ var config model.Config
+ err := h.DB.Where("name", types.ConfigKeyModeration).First(&config).Error
+ if err != nil {
+ config.Name = types.ConfigKeyModeration
+ config.Value = utils.JsonEncode(data)
+ err = h.DB.Create(&config).Error
+ } else {
+ config.Value = utils.JsonEncode(data)
+ err = h.DB.Updates(&config).Error
+ }
if err != nil {
resp.ERROR(c, err.Error())
return
diff --git a/api/handler/chat_handler.go b/api/handler/chat_handler.go
index 9785a185..c44bb172 100644
--- a/api/handler/chat_handler.go
+++ b/api/handler/chat_handler.go
@@ -194,7 +194,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
}
if userVo.Power < input.ChatModel.Power {
- return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, input.ChatModel.Power)
+ return fmt.Errorf("您的算力不足,请购买算力。")
}
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
@@ -338,16 +338,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
},
})
} else {
- // 如果不是逆向模型,则提取文件内容
- modelValue := input.ChatModel.Value
- if !(strings.Contains(modelValue, "-all") || strings.HasPrefix(modelValue, "gpt-4-gizmo") || strings.HasPrefix(modelValue, "claude")) {
- content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
- if err != nil {
- logger.Error("error with read file: ", err)
- continue
- } else {
- fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
- }
+ // 处理文件,提取文件内容
+ content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
+ if err != nil {
+ logger.Error("error with read file: ", err)
+ continue
+ } else {
+ fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
+ logger.Debugf("fileContents: %s", fileContents)
}
}
}
diff --git a/api/handler/dalle_handler.go b/api/handler/dalle_handler.go
index 959b3298..5c34d613 100644
--- a/api/handler/dalle_handler.go
+++ b/api/handler/dalle_handler.go
@@ -118,6 +118,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
UserId: uint(userId),
ModelId: chatModel.Id,
ModelName: chatModel.Value,
+ Image: data.Image,
Prompt: data.Prompt,
Quality: data.Quality,
Size: data.Size,
diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go
index 79fbb684..c70ecb90 100644
--- a/api/handler/user_handler.go
+++ b/api/handler/user_handler.go
@@ -112,8 +112,8 @@ func (h *UserHandler) Register(c *gin.Context) {
return
}
- // 如果注册方式不是账号密码,则需要验证码
- if h.captchaService.GetConfig().Enabled && data.RegWay != "username" {
+ // 人机验证
+ if h.captchaService.GetConfig().Enabled {
var check bool
if data.X != 0 {
check = h.captchaService.SlideCheck(data)
diff --git a/api/service/dalle/service.go b/api/service/dalle/service.go
index 065e27e5..8dabfa70 100644
--- a/api/service/dalle/service.go
+++ b/api/service/dalle/service.go
@@ -16,6 +16,7 @@ import (
"geekai/store"
"geekai/store/model"
"geekai/utils"
+ "strings"
"time"
"github.com/go-redis/redis/v8"
@@ -94,12 +95,14 @@ func (s *Service) Run() {
}
type imgReq struct {
- Model string `json:"model"`
- Prompt string `json:"prompt"`
- N int `json:"n,omitempty"`
- Size string `json:"size,omitempty"`
- Quality string `json:"quality,omitempty"`
- Style string `json:"style,omitempty"`
+ Model string `json:"model"`
+ Image []string `json:"image,omitempty"`
+ Prompt string `json:"prompt"`
+ N int `json:"n,omitempty"`
+ Size string `json:"size,omitempty"`
+ Quality string `json:"quality,omitempty"`
+ Style string `json:"style,omitempty"`
+ ResponseFormat string `json:"response_format,omitempty"`
}
type imgRes struct {
@@ -157,6 +160,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
Style: task.Style,
Quality: task.Quality,
}
+ // 图片编辑
+ if len(task.Image) > 0 {
+ reqBody.Prompt = fmt.Sprintf("%s, %s", strings.Join(task.Image, " "), task.Prompt)
+ }
+
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
r, err := s.httpClient.R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
diff --git a/api/service/sms/service.go b/api/service/sms/service.go
index 14d12ca9..82d0a914 100644
--- a/api/service/sms/service.go
+++ b/api/service/sms/service.go
@@ -7,8 +7,8 @@ package sms
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-const Ali = "ALI"
-const Bao = "BAO"
+const Ali = "aliyun"
+const Bao = "bao"
type Service interface {
SendVerifyCode(mobile string, code int) error
diff --git a/web/src/assets/css/mobile/chat-session.scss b/web/src/assets/css/mobile/chat-session.scss
index bea42876..41773c0d 100644
--- a/web/src/assets/css/mobile/chat-session.scss
+++ b/web/src/assets/css/mobile/chat-session.scss
@@ -49,6 +49,77 @@
}
}
}
+
+ // .file-preview-list {
+ // display: flex;
+ // flex-wrap: wrap;
+ // padding: 6px 10px 0 10px;
+ // gap: 8px;
+
+ // .file-preview-item {
+ // position: relative;
+ // display: inline-flex;
+ // align-items: center;
+ // border: 1px solid #e8e8e8;
+ // background: var(--van-cell-background);
+ // border-radius: 8px;
+ // padding: 6px 26px 6px 6px;
+ // overflow: hidden;
+
+ // .thumb {
+ // width: 56px;
+ // height: 56px;
+ // border-radius: 6px;
+ // overflow: hidden;
+
+ // .img {
+ // width: 56px;
+ // height: 56px;
+ // }
+
+ // .size {
+ // position: absolute;
+ // left: 6px;
+ // bottom: 6px;
+ // background: rgba(0, 0, 0, 0.5);
+ // color: #fff;
+ // font-size: 10px;
+ // padding: 1px 4px;
+ // border-radius: 3px;
+ // }
+ // }
+
+ // .doc {
+ // display: inline-flex;
+ // align-items: center;
+ // gap: 6px;
+ // max-width: 220px;
+ // .icon {
+ // width: 24px;
+ // height: 24px;
+ // }
+ // .name {
+ // white-space: nowrap;
+ // text-overflow: ellipsis;
+ // overflow: hidden;
+ // max-width: 180px;
+ // }
+ // .size {
+ // color: #8c8c8c;
+ // font-size: 12px;
+ // margin-left: 6px;
+ // }
+ // }
+
+ // .remove {
+ // position: absolute;
+ // right: 6px;
+ // top: 6px;
+ // font-size: 14px;
+ // color: #999;
+ // }
+ // }
+ // }
}
}
diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue
index 6fe20889..4b403646 100644
--- a/web/src/components/ChatPrompt.vue
+++ b/web/src/components/ChatPrompt.vue
@@ -232,6 +232,7 @@ const copyContent = (text) => {
flex-flow: row;
margin-right: 10px;
position: relative;
+ justify-content: start;
.el-image {
border: 1px solid #e3e3e3;
@@ -365,6 +366,7 @@ const copyContent = (text) => {
flex-flow: row;
margin-right: 10px;
position: relative;
+ justify-content: end;
.el-image {
border: 1px solid #e3e3e3;
diff --git a/web/src/components/FileList.vue b/web/src/components/FileList.vue
index b0b739c0..0ecb5cee 100644
--- a/web/src/components/FileList.vue
+++ b/web/src/components/FileList.vue
@@ -81,6 +81,7 @@ const removeFile = (file) => {
border: 1px solid #e3e3e3;
padding: 6px;
margin-right: 10px;
+ max-height: 54px;
.icon {
.el-image {
@@ -91,10 +92,10 @@ const removeFile = (file) => {
.body {
margin-left: 5px;
- font-size: 14px;
+ font-size: 12px;
.title {
- line-height: 24px;
+ // line-height: 20px;
color: #0d0d0d;
}
diff --git a/web/src/components/ImageUpload.vue b/web/src/components/ImageUpload.vue
index e1a3845c..26163cd8 100644
--- a/web/src/components/ImageUpload.vue
+++ b/web/src/components/ImageUpload.vue
@@ -85,7 +85,7 @@
- 登录您的账户以继续使用服务 + {{ subtitle }}
- 创建您的账户以开始使用服务 -
-${preCode}${copyBtn} ${langHtml}`
- }
- // 处理代码高亮
- const preCode = md.utils.escapeHtml(str)
- // 将代码包裹在 pre 中
- return `${preCode}${copyBtn}`
- },
-})
-md.use(mathjaxPlugin)
-md.use(emoji)
onMounted(() => {
winHeight.value =
window.innerHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight - 70
-
- // 移除 Clipboard.js 相关内容
-})
-
-onUnmounted(() => {
- // Remove WebSocket handler cleanup
})
const newChat = (item) => {
@@ -272,10 +239,7 @@ const newChat = (item) => {
}
const onLoad = () => {
- // checkSession().then(() => {
- // connect()
- // }).catch(() => {
- // })
+ // 加载更多消息的逻辑可以在这里实现
}
const loadChatHistory = () => {
@@ -294,6 +258,7 @@ const loadChatHistory = () => {
text: role.hello_msg,
},
orgContent: role.hello_msg,
+ showAction: false,
})
return
}
@@ -303,19 +268,12 @@ const loadChatHistory = () => {
chatData.value.push(data[i])
continue
}
-
- data[i].orgContent = data[i].content
- data[i].content.text = md.render(processContent(data[i].content.text))
+ data[i].showAction = true
+ data[i].orgContent = data[i].content.text
chatData.value.push(data[i])
}
nextTick(() => {
- hl.configure({ ignoreUnescapedHTML: true })
- const blocks = document.querySelector('#message-list-box').querySelectorAll('pre code')
- blocks.forEach((block) => {
- hl.highlightElement(block)
- })
-
scrollListBox()
})
})
@@ -326,13 +284,12 @@ const loadChatHistory = () => {
// 创建 socket 连接
const prompt = ref('')
-const showStopGenerate = ref(false) // 停止生成
-const showReGenerate = ref(false) // 重新生成
const previousText = ref('') // 上一次提问
const lineBuffer = ref('') // 输出缓冲行
-const canSend = ref(true)
+const isGenerating = ref(false)
const isNewMsg = ref(true)
const stream = ref(store.chatStream)
+const abortController = new AbortController()
watch(
() => store.chatStream,
(newValue) => {
@@ -340,18 +297,6 @@ watch(
}
)
-const disableInput = (force) => {
- canSend.value = false
- showReGenerate.value = false
- showStopGenerate.value = !force
-}
-
-const enableInput = () => {
- canSend.value = true
- showReGenerate.value = previousText.value !== ''
- showStopGenerate.value = false
-}
-
// 将聊天框的滚动条滑动到最底部
const scrollListBox = () => {
document
@@ -359,9 +304,27 @@ const scrollListBox = () => {
.scrollTo(0, document.getElementById('message-list-box').scrollHeight + 46)
}
+// 滚动到输入区域,确保预览文件可见
+const scrollToBottomBar = () => {
+ try {
+ // 优先让底部输入区域进入视野
+ bottomBarRef.value &&
+ bottomBarRef.value.$el &&
+ bottomBarRef.value.$el.scrollIntoView({
+ behavior: 'smooth',
+ block: 'end',
+ })
+ } catch (e) {}
+ // 再兜底滚动到页面底部
+ try {
+ window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' })
+ } catch (e) {}
+}
+
// 发送 SSE 请求
const sendSSERequest = async (message) => {
try {
+ isGenerating.value = true
await fetchEventSource('/api/chat/message', {
method: 'POST',
headers: {
@@ -369,6 +332,13 @@ const sendSSERequest = async (message) => {
},
body: JSON.stringify(message),
openWhenHidden: true,
+ // 重试机制,避免连接断开后一直重试
+ retry: 3000,
+ // 设置重试延迟为0,确保不重试
+ retryDelay: 3000,
+ // 设置最大重试次数为0
+ maxRetries: 3,
+ signal: abortController.signal,
onopen(response) {
if (response.ok && response.status === 200) {
console.log('SSE connection opened')
@@ -380,13 +350,13 @@ const sendSSERequest = async (message) => {
try {
const data = JSON.parse(msg.data)
if (data.type === 'error') {
- showMessageError(data.body)
- enableInput()
+ chatData.value[chatData.value.length - 1].error = data.body
+ isGenerating.value = false
return
}
if (data.type === 'end') {
- enableInput()
+ isGenerating.value = false
lineBuffer.value = '' // 清空缓冲
isNewMsg.value = true
return
@@ -404,15 +374,9 @@ const sendSSERequest = async (message) => {
lineBuffer.value += data.body
const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value
- reply['content']['text'] = md.render(processContent(lineBuffer.value))
+ reply['content']['text'] = lineBuffer.value
nextTick(() => {
- hl.configure({ ignoreUnescapedHTML: true })
- const lines = document.querySelectorAll('.message-line')
- const blocks = lines[lines.length - 1].querySelectorAll('pre code')
- blocks.forEach((block) => {
- hl.highlightElement(block)
- })
scrollListBox()
const items = document.querySelectorAll('.message-line')
@@ -429,32 +393,49 @@ const sendSSERequest = async (message) => {
})
}
}
+
+ // 回答完毕,更新完整的消息内容
+ if (data.type === 'complete') {
+ data.body.showAction = true
+ data.body.orgContent = data.body.content.text
+ chatData.value[chatData.value.length - 1] = data.body
+ }
} catch (error) {
console.error('Error processing message:', error)
- enableInput()
+ isGenerating.value = false
showMessageError('消息处理出错,请重试')
}
},
onerror(err) {
console.error('SSE Error:', err)
- enableInput()
+ try {
+ abortController && abortController.abort()
+ } catch (e) {
+ console.error('AbortController abort error:', e)
+ }
+ isGenerating.value = false
showMessageError('连接已断开,请重试')
},
onclose() {
console.log('SSE connection closed')
- enableInput()
+ isGenerating.value = false
},
})
} catch (error) {
+ try {
+ abortController && abortController.abort()
+ } catch (e) {
+ console.error('AbortController abort error:', e)
+ }
console.error('Failed to send message:', error)
- enableInput()
+ isGenerating.value = false
showMessageError('发送消息失败,请重试')
}
}
// 发送消息
const sendMessage = () => {
- if (canSend.value === false) {
+ if (isGenerating.value) {
showToast('AI 正在作答中,请稍后...')
return
}
@@ -469,9 +450,78 @@ const sendMessage = () => {
type: 'prompt',
id: randString(32),
icon: loginUser.value.avatar,
- content: { text: renderInputText(prompt.value) },
+ content: { text: renderInputText(prompt.value), files: files.value },
created_at: new Date().getTime(),
})
+ // 添加空回复消息
+ const _role = getRoleById(roleId.value)
+ chatData.value.push({
+ chat_id: chatId,
+ role_id: roleId.value,
+ type: 'reply',
+ id: randString(32),
+ icon: _role['icon'],
+ content: {
+ text: '',
+ files: [],
+ },
+ })
+
+ nextTick(() => {
+ scrollListBox()
+ })
+
+ // 发送 SSE 请求
+ sendSSERequest({
+ user_id: loginUser.value.id,
+ role_id: roleId.value,
+ model_id: modelId.value,
+ chat_id: chatId.value,
+ prompt: prompt.value,
+ stream: stream.value,
+ files: files.value,
+ })
+
+ previousText.value = prompt.value
+ prompt.value = ''
+ files.value = []
+ return true
+}
+
+// 停止生成
+const stopGenerate = function () {
+ if (abortController) {
+ abortController.abort()
+ isGenerating.value = false
+ httpGet('/api/chat/stop?session_id=' + getClientId())
+ .then(() => {
+ showToast('会话已中断')
+ })
+ .catch((e) => {
+ showMessageError('中断对话失败:' + e.message)
+ })
+ }
+}
+
+// 处理从ChatReply组件触发的重新生成
+const handleRegenerate = (messageId) => {
+ if (isGenerating.value) {
+ showToast('AI 正在作答中,请稍后...')
+ return
+ }
+
+ console.log('messageId', messageId)
+ console.log('chatData.value', chatData.value)
+
+ // 判断 messageId 是整数
+ if (messageId !== '' && isNaN(messageId)) {
+ showToast('消息 ID 不合法,无法重新生成')
+ return
+ }
+
+ chatData.value = chatData.value.filter((item) => item.id < messageId && !item.isHello)
+ const userPrompt = chatData.value.pop()
+
// 添加空回复消息
const _role = getRoleById(roleId.value)
chatData.value.push({
@@ -485,52 +535,19 @@ const sendMessage = () => {
},
})
- nextTick(() => {
- scrollListBox()
- })
-
- disableInput(false)
-
// 发送 SSE 请求
sendSSERequest({
user_id: loginUser.value.id,
role_id: roleId.value,
model_id: modelId.value,
chat_id: chatId.value,
- prompt: prompt.value,
- stream: stream.value,
- })
-
- previousText.value = prompt.value
- prompt.value = ''
- return true
-}
-
-// 重新生成
-const reGenerate = () => {
- disableInput(false)
- const text = '重新生成上述问题的答案:' + previousText.value
- // 追加消息
- chatData.value.push({
- type: 'prompt',
- id: randString(32),
- icon: loginUser.value.avatar,
- content: renderInputText(text),
- })
-
- // 发送 SSE 请求
- sendSSERequest({
- user_id: loginUser.value.id,
- role_id: roleId.value,
- model_id: modelId.value,
- chat_id: chatId.value,
- prompt: previousText.value,
+ last_msg_id: messageId,
+ prompt: userPrompt.content.text,
stream: stream.value,
+ files: [],
})
}
-// 移除 showShare、shareOptions、shareChat 相关内容
-
const getRoleById = function (rid) {
for (let i = 0; i < roles.value.length; i++) {
if (roles.value[i]['id'] === rid) {
@@ -571,6 +588,38 @@ const copyShareUrl = async () => {
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
}
}
+
+// 文件上传与管理
+const files = ref([])
+const isHttpUrl = (url) => url.startsWith('http://') || url.startsWith('https://')
+const toAbsUrl = (url) => (isHttpUrl(url) ? url : location.protocol + '//' + location.host + url)
+const afterRead = async (fileItem) => {
+ showLoading('文件上传中...')
+ try {
+ const file = Array.isArray(fileItem) ? fileItem[0].file : fileItem.file || fileItem
+ const formData = new FormData()
+ formData.append('file', file, file.name)
+ const res = await httpPost('/api/upload', formData)
+ const f = res.data || {}
+ f.url = toAbsUrl(f.url || '')
+ files.value = [f, ...files.value]
+ // 确保上传后文件预览立即可见
+ nextTick(() => {
+ scrollToBottomBar()
+ })
+ } catch (e) {
+ showNotify({ type: 'danger', message: '文件上传失败:' + (e.message || '网络错误') })
+ } finally {
+ closeLoading()
+ }
+}
+const removeFile = (f, idx) => {
+ files.value.splice(idx, 1)
+}
+
+const onRemovePreview = ({ file, index }) => {
+ files.value.splice(index, 1)
+}