diff --git a/hzhub-admin/apps/web-antd/src/api/chat/test/index.ts b/hzhub-admin/apps/web-antd/src/api/chat/test/index.ts new file mode 100644 index 0000000..bd52188 --- /dev/null +++ b/hzhub-admin/apps/web-antd/src/api/chat/test/index.ts @@ -0,0 +1,206 @@ +import { useAccessStore } from '@vben/stores'; + +import { useAppConfig } from '@vben/hooks'; + +// 获取 clientId 配置 +const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD); + +/** + * 对话请求参数 + */ +export interface ChatTestRequest { + /** 模型 */ + model: string; + /** 对话消息 */ + content: string; + /** 会话ID */ + sessionId?: number; + /** 是否启用工作流 */ + enableWorkFlow?: boolean; + /** 是否启用深度思考 */ + enableThinking?: boolean; + /** 是否联网 */ + enableInternet?: boolean; +} + +/** + * 对话响应 + */ +export interface ChatTestResponse { + uuid: string; + sessionId: number; + model: string; + content: string; + role: string; + createTime?: string; +} + +/** + * 日志类型 + */ +export type LogType = 'info' | 'error' | 'success' | 'warning'; + +/** + * 日志条目 + */ +export interface LogEntry { + time: string; + type: LogType; + message: string; + data?: any; +} + +/** + * 发送聊天消息 (SSE 流式响应) + * + * 使用原生 fetch API 处理 SSE 流 + * + * @param data 对话请求 + * @param onMessage 消息回调 + * @param onComplete 完成回调 + * @param onError 错误回调 + * @param onLog 日志回调 + * @returns abort 函数 + */ +export function sendChatMessageStream( + data: ChatTestRequest, + onMessage: (data: ChatTestResponse) => void, + onComplete?: () => void, + onError?: (error: Error) => void, + onLog?: (log: LogEntry) => void, +): () => void { + // 记录日志 + const log = (type: LogType, message: string, data?: any) => { + onLog?.({ + time: new Date().toLocaleTimeString(), + type, + message, + data, + }); + }; + + // 从 pinia store 获取 token + const accessStore = useAccessStore(); + const token = accessStore.accessToken; + + if (!token) { + const error = new Error('未登录或会话已过期,请重新登录'); + log('error', error.message); + onError?.(error); + return () => {}; + } + + log('info', '已获取认证 token'); + + const abortController = new AbortController(); + + // 发送聊天请求 + log('info', `发送聊天请求: POST /api/chat/send`, data); + + fetch('/api/chat/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + 'ClientID': clientId, + }, + body: JSON.stringify(data), + signal: abortController.signal, + }) + .then(async (response) => { + log('info', `聊天请求响应状态: ${response.status}`); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP ${response.status}: ${text.substring(0, 200)}`); + } + + // 处理 SSE 流 + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + throw new Error('无法获取响应流'); + } + + log('success', '开始接收 SSE 流'); + + let buffer = ''; // 缓冲区用于处理不完整的行 + + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + log('success', '对话完成'); + onComplete?.(); + break; + } + + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + const lines = buffer.split('\n'); + + // 保留最后一行(可能不完整) + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + + log('info', `收到原始数据: ${line}`); + + if (line.startsWith('data:')) { + const data = line.substring(5).trim(); + + // 检查是否是完成信号(支持两种格式) + if (data === '[DONE]' || (data.includes('"event":"done"') && data.includes('"done":true'))) { + log('success', '对话完成'); + onComplete?.(); + break; + } + + try { + const parsed = JSON.parse(data); + + // 再次检查解析后的对象是否标记完成 + if (parsed.event === 'done' && parsed.done === true) { + log('success', '对话完成'); + onComplete?.(); + break; + } + + log('info', '解析后的数据', parsed); + + onMessage({ + uuid: parsed.uuid || '', + sessionId: parsed.sessionId || 0, + model: parsed.model || '', + content: parsed.content || '', + role: 'assistant', + }); + } catch (e: any) { + log('error', `JSON 解析失败: ${e.message}, 原始数据: ${data}`); + } + } + } + } + } catch (error: any) { + if (error.name !== 'AbortError') { + log('error', `读取流错误: ${error.message}`); + onError?.(error); + } + } + }) + .catch((error) => { + if (error.name !== 'AbortError') { + log('error', `聊天请求错误: ${error.message}`); + onError?.(error); + } + }); + + // 返回 abort 函数 + return () => { + log('info', '用户中止请求'); + abortController.abort(); + }; +} \ No newline at end of file diff --git a/hzhub-admin/apps/web-antd/src/views/chat/test/index.vue b/hzhub-admin/apps/web-antd/src/views/chat/test/index.vue new file mode 100644 index 0000000..204f7de --- /dev/null +++ b/hzhub-admin/apps/web-antd/src/views/chat/test/index.vue @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + 启用深度思考(功能完善中,暂不可用) + + + 启用联网搜索 + + + + + + + + + + 按 Enter 发送,Shift+Enter 换行 + + + + + + + 发送消息 + + + 停止生成 + + + 清空对话 + + + + + + + + + + + + 👤 你 + 🤖 AI + ℹ️ 系统 + + + + 正在思考 + + + {{ msg.content || '...' }} + + + + + + + + + + 清空 + + + + [{{ log.time }}] + + [{{ log.type.toUpperCase() }}] + + {{ log.message }} + {{ JSON.stringify(log.data, null, 2) }} + + + + + + + + \ No newline at end of file diff --git a/hzhub-portal-company/.eslintrc-auto-import.json b/hzhub-portal-company/.eslintrc-auto-import.json new file mode 100644 index 0000000..313e671 --- /dev/null +++ b/hzhub-portal-company/.eslintrc-auto-import.json @@ -0,0 +1,78 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "DirectiveBinding": true, + "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "MaybeRef": true, + "MaybeRefOrGetter": true, + "PropType": true, + "Ref": true, + "Slot": true, + "Slots": true, + "VNode": true, + "WritableComputedRef": true, + "computed": true, + "createApp": true, + "customRef": true, + "defineAsyncComponent": true, + "defineComponent": true, + "effectScope": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "inject": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onUnmounted": true, + "onUpdated": true, + "onWatcherCleanup": true, + "provide": true, + "reactive": true, + "readonly": true, + "ref": true, + "resolveComponent": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "toRaw": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "unref": true, + "useAttrs": true, + "useCssModule": true, + "useCssVars": true, + "useId": true, + "useModel": true, + "useSlots": true, + "useTemplateRef": true, + "watch": true, + "watchEffect": true, + "watchPostEffect": true, + "watchSyncEffect": true + } +} diff --git a/hzhub-portal-dealer/.eslintrc-auto-import.json b/hzhub-portal-dealer/.eslintrc-auto-import.json new file mode 100644 index 0000000..af1083b --- /dev/null +++ b/hzhub-portal-dealer/.eslintrc-auto-import.json @@ -0,0 +1,76 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "DirectiveBinding": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "MaybeRef": true, + "MaybeRefOrGetter": true, + "PropType": true, + "Ref": true, + "Slot": true, + "Slots": true, + "VNode": true, + "WritableComputedRef": true, + "computed": true, + "createApp": true, + "customRef": true, + "defineAsyncComponent": true, + "defineComponent": true, + "effectScope": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "inject": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onUnmounted": true, + "onUpdated": true, + "onWatcherCleanup": true, + "provide": true, + "reactive": true, + "readonly": true, + "ref": true, + "resolveComponent": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "toRaw": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "unref": true, + "useAttrs": true, + "useCssModule": true, + "useCssVars": true, + "useId": true, + "useModel": true, + "useSlots": true, + "useTemplateRef": true, + "watch": true, + "watchEffect": true, + "watchPostEffect": true, + "watchSyncEffect": true + } +}
{{ msg.content || '...' }}
{{ JSON.stringify(log.data, null, 2) }}