diff --git a/apps/web/src/components/add-provider/index.vue b/apps/web/src/components/add-provider/index.vue index f1985d29..e8ddf90a 100644 --- a/apps/web/src/components/add-provider/index.vue +++ b/apps/web/src/components/add-provider/index.vue @@ -155,9 +155,10 @@ import { } from '@memoh/ui' import { toTypedSchema } from '@vee-validate/zod' import z from 'zod' -import { useForm,Form,Field } from 'vee-validate' +import { useForm } from 'vee-validate' import { useMutation, useQueryCache } from '@pinia/colada' import { postProviders, postProvidersByIdImportModels } from '@memoh/sdk' +import type { ProvidersCreateRequest } from '@memoh/sdk' import { useI18n } from 'vue-i18n' import FormDialogShell from '@/components/form-dialog-shell/index.vue' import { useDialogMutation } from '@/composables/useDialogMutation' @@ -176,7 +177,7 @@ const { mutateAsync: createProviderMutation, isLoading } = useMutation({ ...data, metadata: { additionalProp1: {} }, } - const { data: result } = await postProviders({ body: payload as any, throwOnError: true }) + const { data: result } = await postProviders({ body: payload as ProvidersCreateRequest, throwOnError: true }) if (data.auto_import && result?.id) { try { const { data: importResult } = await postProvidersByIdImportModels({ diff --git a/apps/web/src/components/create-model/index.vue b/apps/web/src/components/create-model/index.vue index 27d7a383..10e01631 100644 --- a/apps/web/src/components/create-model/index.vue +++ b/apps/web/src/components/create-model/index.vue @@ -202,7 +202,7 @@ import { toTypedSchema } from '@vee-validate/zod' import z from 'zod' import { useMutation, useQueryCache } from '@pinia/colada' import { postModels, putModelsById, putModelsModelByModelId } from '@memoh/sdk' -import type { ModelsGetResponse } from '@memoh/sdk' +import type { ModelsGetResponse, ModelsAddRequest, ModelsUpdateRequest } from '@memoh/sdk' import { useI18n } from 'vue-i18n' import { CLIENT_TYPE_LIST, CLIENT_TYPE_META } from '@/constants/client-types' import FormDialogShell from '@/components/form-dialog-shell/index.vue' @@ -288,7 +288,7 @@ const { id } = defineProps<{ id: string }>() const queryCache = useQueryCache() const { mutateAsync: createModel, isLoading: createLoading } = useMutation({ mutation: async (data: Record) => { - const { data: result } = await postModels({ body: data as any, throwOnError: true }) + const { data: result } = await postModels({ body: data as ModelsAddRequest, throwOnError: true }) return result }, onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }), @@ -297,7 +297,7 @@ const { mutateAsync: updateModel, isLoading: updateLoading } = useMutation({ mutation: async ({ id, data }: { id: string; data: Record }) => { const { data: result } = await putModelsById({ path: { id }, - body: data as any, + body: data as ModelsUpdateRequest, throwOnError: true, }) return result @@ -308,7 +308,7 @@ const { mutateAsync: updateModelByLegacyModelID, isLoading: updateLegacyLoading mutation: async ({ modelId, data }: { modelId: string; data: Record }) => { const { data: result } = await putModelsModelByModelId({ path: { modelId }, - body: data as any, + body: data as ModelsUpdateRequest, throwOnError: true, }) return result @@ -361,9 +361,9 @@ async function addModel() { if (isEdit) { const modelUUID = fallback?.id if (modelUUID) { - return updateModel({ id: modelUUID, data: payload as any }) + return updateModel({ id: modelUUID, data: payload as ModelsUpdateRequest }) } - return updateModelByLegacyModelID({ modelId: fallback!.model_id, data: payload as any }) + return updateModelByLegacyModelID({ modelId: fallback!.model_id, data: payload as ModelsUpdateRequest }) } return createModel(payload) }, diff --git a/apps/web/src/components/form-dialog-shell/index.vue b/apps/web/src/components/form-dialog-shell/index.vue index b684644e..3e213dd6 100644 --- a/apps/web/src/components/form-dialog-shell/index.vue +++ b/apps/web/src/components/form-dialog-shell/index.vue @@ -55,7 +55,7 @@ import { DialogTrigger, Spinner } from '@memoh/ui' -import { Form, Field } from 'vee-validate' + withDefaults(defineProps<{ title: string diff --git a/apps/web/src/composables/api/useChat.chat-api.ts b/apps/web/src/composables/api/useChat.chat-api.ts index 5fb3886a..54569817 100644 --- a/apps/web/src/composables/api/useChat.chat-api.ts +++ b/apps/web/src/composables/api/useChat.chat-api.ts @@ -1,27 +1,56 @@ -import { getBots, deleteBotsByBotIdMessages } from '@memoh/sdk' -import type { Bot, ChatSummary } from './useChat.types' +import { + getBots, + deleteBotsByBotIdMessages, + getBotsByBotIdSessions, + postBotsByBotIdSessions, + deleteBotsByBotIdSessionsBySessionId, + patchBotsByBotIdSessionsBySessionId, +} from '@memoh/sdk' +import type { Bot, SessionSummary } from './useChat.types' export async function fetchBots(): Promise { const { data } = await getBots({ throwOnError: true }) return data?.items ?? [] } -export async function fetchChats(botId: string): Promise { +export async function fetchSessions(botId: string): Promise { const id = botId.trim() if (!id) return [] - return [{ id, bot_id: id, kind: 'bot' }] + const { data } = await getBotsByBotIdSessions({ + path: { bot_id: id }, + throwOnError: true, + }) + return (data as Record)?.items as SessionSummary[] ?? [] } -export async function createChat(botId: string): Promise { +export async function createSession(botId: string, title?: string): Promise { const id = botId.trim() if (!id) throw new Error('bot id is required') - return { id, bot_id: id, kind: 'bot' } + const { data } = await postBotsByBotIdSessions({ + path: { bot_id: id }, + body: { title: title ?? '', channel_type: 'web' }, + throwOnError: true, + }) + return data as SessionSummary } -export async function deleteChat(botId: string, chatId: string): Promise { - if (botId.trim() !== chatId.trim()) { - throw new Error('chat id must match bot id') - } +export async function updateSessionTitle(botId: string, sessionId: string, title: string): Promise { + const { data } = await patchBotsByBotIdSessionsBySessionId({ + path: { bot_id: botId.trim(), session_id: sessionId.trim() }, + body: { title }, + throwOnError: true, + }) + return data as SessionSummary +} + +export async function deleteSession(botId: string, sessionId: string): Promise { + await deleteBotsByBotIdSessionsBySessionId({ + path: { bot_id: botId.trim(), session_id: sessionId.trim() }, + throwOnError: true, + }) +} + +export async function deleteAllMessages(botId: string): Promise { await deleteBotsByBotIdMessages({ path: { bot_id: botId }, throwOnError: true, diff --git a/apps/web/src/composables/api/useChat.message-api.ts b/apps/web/src/composables/api/useChat.message-api.ts index 2887ada3..16f51c23 100644 --- a/apps/web/src/composables/api/useChat.message-api.ts +++ b/apps/web/src/composables/api/useChat.message-api.ts @@ -12,18 +12,15 @@ import { parseStreamPayload, readSSEStream } from './useChat.sse' export async function fetchMessages( botId: string, - chatId: string, + sessionId?: string, options?: FetchMessagesOptions, ): Promise { - if (botId.trim() !== chatId.trim()) { - throw new Error('chat id must match bot id') - } - const { data } = await getBotsByBotIdMessages({ path: { bot_id: botId }, query: { limit: options?.limit ?? 30, ...(options?.before?.trim() ? { before: options.before.trim() } : {}), + ...(sessionId?.trim() ? { session_id: sessionId.trim() } : {}), }, throwOnError: true, }) @@ -106,9 +103,13 @@ export async function streamMessageEvents( if (!body) throw new Error('No response body') await readSSEStream(body, (payload) => { - const parsed = parseStreamPayload(payload) - if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) return - if (typeof parsed.type !== 'string' || !parsed.type.trim()) return - onEvent(parsed as MessageStreamEvent) + try { + const parsed = JSON.parse(payload) + if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) return + if (typeof parsed.type !== 'string' || !parsed.type.trim()) return + onEvent(parsed as MessageStreamEvent) + } catch { + // Ignore unparsable payloads + } }) } diff --git a/apps/web/src/composables/api/useChat.types.ts b/apps/web/src/composables/api/useChat.types.ts index 162a046a..f7c32e75 100644 --- a/apps/web/src/composables/api/useChat.types.ts +++ b/apps/web/src/composables/api/useChat.types.ts @@ -2,16 +2,18 @@ import type { BotsBot } from '@memoh/sdk' export type Bot = BotsBot -export interface ChatSummary { +export interface SessionSummary { id: string bot_id: string - kind: string - title?: string + route_id?: string + channel_type?: string + type?: string + title: string + metadata?: Record created_at?: string updated_at?: string - access_mode?: 'participant' | 'channel_identity_observed' - participant_role?: string - last_observed_at?: string + route_metadata?: Record + route_conversation_type?: string } export interface MessageAsset { @@ -28,7 +30,7 @@ export interface MessageAsset { export interface Message { id: string bot_id: string - route_id?: string + session_id?: string sender_channel_identity_id?: string sender_user_id?: string sender_display_name?: string @@ -69,11 +71,14 @@ export interface MessageStreamEvent { type: string bot_id?: string message?: Message + session_id?: string + title?: string } export interface FetchMessagesOptions { limit?: number before?: string + session_id?: string } export interface ChatAttachment { diff --git a/apps/web/src/composables/api/useChat.ws.ts b/apps/web/src/composables/api/useChat.ws.ts index 0cfdc725..5a458737 100644 --- a/apps/web/src/composables/api/useChat.ws.ts +++ b/apps/web/src/composables/api/useChat.ws.ts @@ -4,6 +4,7 @@ import type { StreamEvent, MessageStreamEvent, ChatAttachment, StreamEventHandle export interface WSClientMessage { type: 'message' | 'abort' text?: string + session_id?: string attachments?: ChatAttachment[] } diff --git a/packages/sdk/src/container-stream.ts b/apps/web/src/composables/api/useContainerStream.ts similarity index 83% rename from packages/sdk/src/container-stream.ts rename to apps/web/src/composables/api/useContainerStream.ts index 70aae99a..95045915 100644 --- a/packages/sdk/src/container-stream.ts +++ b/apps/web/src/composables/api/useContainerStream.ts @@ -1,14 +1,9 @@ -import { mergeHeaders } from './client' -import { client } from './client.gen' -import type { Options } from './sdk.gen' +import { client } from '@memoh/sdk/client' +import type { Options } from '@memoh/sdk' import type { HandlersCreateContainerResponse, PostBotsByBotIdContainerData, -} from './types.gen' - -// Handwritten SDK supplement for container-create SSE. -// Re-export this module via @memoh/sdk/extra instead of the generated root entry, -// because packages/sdk/src/index.ts is regenerated from OpenAPI. +} from '@memoh/sdk' export type ContainerCreateLayerStatus = { ref: string @@ -65,18 +60,20 @@ function toError(error: unknown): Error { return new Error('Container create stream failed') } -export async function postBotsByBotIdContainerStream( - options: Options, +export async function postBotsByBotIdContainerStream( + options: Options, ): Promise { let streamError: unknown + const { throwOnError: _throwOnError, ...rest } = options const result = await client.sse.post({ url: '/bots/{bot_id}/container', - ...options, - headers: mergeHeaders(options.headers, { + ...rest, + headers: { + ...options.headers as Record, Accept: 'text/event-stream', 'Content-Type': 'application/json', - }), + }, onSseError: (error) => { streamError = error }, diff --git a/apps/web/src/composables/useBotStatusMeta.ts b/apps/web/src/composables/useBotStatusMeta.ts index 7d0f31fb..bde16f89 100644 --- a/apps/web/src/composables/useBotStatusMeta.ts +++ b/apps/web/src/composables/useBotStatusMeta.ts @@ -9,7 +9,7 @@ interface BotStatusSource { export function useBotStatusMeta( bot: Ref, - t: (...args: any[]) => string, + t: (key: string, named?: Record) => string, ) { const isCreating = computed(() => bot.value?.status === 'creating') const isDeleting = computed(() => bot.value?.status === 'deleting') diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 5439e3af..b30a3de1 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -147,14 +147,23 @@ "toolExecError": "Error", "toolScheduleItems": "{count} items", "toolMemoryResults": "{count} memories", - "toolInboxResults": "{count} messages", "toolWebFetchPreview": "Preview", "toolContactsCount": "{count} contacts", "toolEmailCount": "{count} emails", "toolEmailAccounts": "{count} accounts", "toolSubagentCount": "{count} subagents", "unknownUser": "{platform} User", - "files": "Files" + "files": "Files", + "sessions": "Sessions", + "newSession": "New Session", + "deleteSession": "Delete Session", + "deleteSessionConfirm": "Are you sure you want to delete this session?", + "renameSession": "Rename Session", + "sessionTitle": "Session", + "untitledSession": "Untitled Session", + "noSessions": "No sessions yet", + "sessionTypeHeartbeat": "Heartbeat", + "sessionTypeSchedule": "Scheduled Task" }, "models": { "title": "Models", @@ -723,6 +732,9 @@ }, "settings": { "chatModel": "Chat Model", + "titleModel": "Title Model", + "titleModelDescription": "Select a small model to auto-generate session titles. Leave empty to disable.", + "titleModelPlaceholder": "Disabled (no auto title)", "searchProvider": "Search Provider", "searchProviderPlaceholder": "Select search provider", "memoryProvider": "Memory Provider", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index a4b95216..c49b92a3 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -143,14 +143,23 @@ "toolExecError": "错误", "toolScheduleItems": "{count} 条", "toolMemoryResults": "{count} 条记忆", - "toolInboxResults": "{count} 条消息", "toolWebFetchPreview": "预览", "toolContactsCount": "{count} 个联系人", "toolEmailCount": "{count} 封邮件", "toolEmailAccounts": "{count} 个账户", "toolSubagentCount": "{count} 个子代理", "unknownUser": "{platform}用户", - "files": "文件管理" + "files": "文件管理", + "sessions": "会话", + "newSession": "新建会话", + "deleteSession": "删除会话", + "deleteSessionConfirm": "确定要删除此会话吗?", + "renameSession": "重命名会话", + "sessionTitle": "会话", + "untitledSession": "未命名会话", + "noSessions": "暂无会话", + "sessionTypeHeartbeat": "心跳", + "sessionTypeSchedule": "定时任务" }, "models": { "title": "模型", @@ -719,6 +728,9 @@ }, "settings": { "chatModel": "对话模型", + "titleModel": "标题模型", + "titleModelDescription": "选择一个小模型来自动生成会话标题。留空则禁用自动标题生成。", + "titleModelPlaceholder": "未启用(不自动生成标题)", "searchProvider": "搜索提供方", "searchProviderPlaceholder": "选择搜索提供方", "memoryProvider": "记忆提供方", diff --git a/apps/web/src/pages/bots/components/bot-container.vue b/apps/web/src/pages/bots/components/bot-container.vue index e0bcee37..1b99ca17 100644 --- a/apps/web/src/pages/bots/components/bot-container.vue +++ b/apps/web/src/pages/bots/components/bot-container.vue @@ -24,7 +24,7 @@ import { postBotsByBotIdContainerStream, type ContainerCreateLayerStatus, type ContainerCreateStreamEvent, -} from '@memoh/sdk/extra' +} from '@/composables/api/useContainerStream' import { Button, Input, Label, Separator, Spinner, Switch } from '@memoh/ui' import ConfirmPopover from '@/components/confirm-popover/index.vue' import ContainerCreateProgress from './container-create-progress.vue' diff --git a/apps/web/src/pages/bots/components/bot-email.vue b/apps/web/src/pages/bots/components/bot-email.vue index 6c3bf3cd..7e693498 100644 --- a/apps/web/src/pages/bots/components/bot-email.vue +++ b/apps/web/src/pages/bots/components/bot-email.vue @@ -273,7 +273,7 @@ async function loadOutbox() { query: { limit: 50, offset: 0 }, throwOnError: true, }) - outboxItems.value = (data as any)?.items ?? [] + outboxItems.value = (data as Record)?.items as typeof outboxItems.value ?? [] } finally { outboxLoading.value = false } @@ -282,7 +282,7 @@ async function loadOutbox() { async function handleAddBinding(provider: EmailProviderResponse) { addingBinding.value = true addingProviderId.value = provider.id! - const emailAddr = (provider.config as any)?.username || provider.name || '' + const emailAddr = (provider.config as Record)?.username as string || provider.name || '' try { await postBotsByBotIdEmailBindings({ path: { bot_id: props.botId }, @@ -297,8 +297,8 @@ async function handleAddBinding(provider: EmailProviderResponse) { }) await loadBindings() toast.success(t('bots.email.bindSuccess')) - } catch (e: any) { - toast.error(e?.message || t('common.saveFailed')) + } catch (e: unknown) { + toast.error(e instanceof Error ? e.message : t('common.saveFailed')) } finally { addingBinding.value = false addingProviderId.value = '' @@ -313,8 +313,8 @@ async function handleTogglePerm(binding: EmailBindingResponse, field: string, va throwOnError: true, }) await loadBindings() - } catch (e: any) { - toast.error(e?.message || t('common.saveFailed')) + } catch (e: unknown) { + toast.error(e instanceof Error ? e.message : t('common.saveFailed')) } } @@ -327,8 +327,8 @@ async function handleDeleteBinding(id: string) { }) await loadBindings() toast.success(t('bots.email.unbindSuccess')) - } catch (e: any) { - toast.error(e?.message || t('common.saveFailed')) + } catch (e: unknown) { + toast.error(e instanceof Error ? e.message : t('common.saveFailed')) } finally { deletingId.value = '' } diff --git a/apps/web/src/pages/bots/components/bot-settings.vue b/apps/web/src/pages/bots/components/bot-settings.vue index 480ea25e..a9a1c66e 100644 --- a/apps/web/src/pages/bots/components/bot-settings.vue +++ b/apps/web/src/pages/bots/components/bot-settings.vue @@ -12,6 +12,21 @@ /> + +
+ +

+ {{ $t('bots.settings.titleModelDescription') }} +

+ +
+
@@ -450,6 +465,7 @@ const browserContexts = computed(() => browserContextData.value ?? []) // ---- Form ---- const form = reactive({ chat_model_id: '', + title_model_id: '', search_provider_id: '', memory_provider_id: '', tts_model_id: '', @@ -560,6 +576,7 @@ const encoderHealthLabel = computed(() => watch(settings, (val) => { if (val) { form.chat_model_id = val.chat_model_id ?? '' + form.title_model_id = val.title_model_id ?? '' form.search_provider_id = val.search_provider_id ?? '' form.memory_provider_id = val.memory_provider_id ?? '' form.tts_model_id = val.tts_model_id ?? '' @@ -577,6 +594,7 @@ const hasChanges = computed(() => { const s = settings.value let changed = form.chat_model_id !== (s.chat_model_id ?? '') + || form.title_model_id !== (s.title_model_id ?? '') || form.search_provider_id !== (s.search_provider_id ?? '') || form.memory_provider_id !== (s.memory_provider_id ?? '') || form.tts_model_id !== (s.tts_model_id ?? '') diff --git a/apps/web/src/pages/browser-contexts/components/add-browser-context.vue b/apps/web/src/pages/browser-contexts/components/add-browser-context.vue index 8247e785..4e37a3bc 100644 --- a/apps/web/src/pages/browser-contexts/components/add-browser-context.vue +++ b/apps/web/src/pages/browser-contexts/components/add-browser-context.vue @@ -60,6 +60,7 @@ import z from 'zod' import { useForm } from 'vee-validate' import { useMutation, useQueryCache } from '@pinia/colada' import { postBrowserContexts } from '@memoh/sdk' +import type { BrowsercontextsCreateRequest } from '@memoh/sdk' import { useI18n } from 'vue-i18n' import FormDialogShell from '@/components/form-dialog-shell/index.vue' import { useDialogMutation } from '@/composables/useDialogMutation' @@ -72,7 +73,7 @@ const queryCache = useQueryCache() const { mutateAsync: createMutation, isLoading } = useMutation({ mutation: async (data: { name: string }) => { const { data: result } = await postBrowserContexts({ - body: { name: data.name, config: {} } as any, + body: { name: data.name } as BrowsercontextsCreateRequest, throwOnError: true, }) return result diff --git a/apps/web/src/pages/browser-contexts/components/context-setting.vue b/apps/web/src/pages/browser-contexts/components/context-setting.vue index d628bd55..008109e1 100644 --- a/apps/web/src/pages/browser-contexts/components/context-setting.vue +++ b/apps/web/src/pages/browser-contexts/components/context-setting.vue @@ -240,7 +240,7 @@ import { useForm } from 'vee-validate' import { useMutation, useQuery, useQueryCache } from '@pinia/colada' import { putBrowserContextsById, deleteBrowserContextsById } from '@memoh/sdk' import { getBrowserContextsCoresQuery } from '@memoh/sdk/colada' -import type { BrowsercontextsBrowserContext } from '@memoh/sdk' +import type { BrowsercontextsBrowserContext, BrowsercontextsUpdateRequest } from '@memoh/sdk' import { inject, watch, computed, type Ref } from 'vue' import { useI18n } from 'vue-i18n' import { toast } from 'vue-sonner' @@ -314,10 +314,10 @@ watch(() => curContext?.value, (ctx) => { }, { immediate: true }) const { mutateAsync: updateMutation, isLoading: isSaving } = useMutation({ - mutation: async (data: { id: string; name: string; config: any }) => { + mutation: async (data: { id: string; name: string; config: Record }) => { const { data: result } = await putBrowserContextsById({ path: { id: data.id }, - body: { name: data.name, config: data.config } as any, + body: { name: data.name } as BrowsercontextsUpdateRequest, throwOnError: true, }) return result @@ -339,7 +339,7 @@ const handleSave = form.handleSubmit(async (values) => { const id = curContext?.value?.id if (!id) return - const config: Record = { + const config: Record = { core: values.core ?? 'chromium', } if (values.viewportWidth || values.viewportHeight) { diff --git a/apps/web/src/pages/chat/components/bot-sidebar.vue b/apps/web/src/pages/chat/components/bot-sidebar.vue index 68bb024c..b11044be 100644 --- a/apps/web/src/pages/chat/components/bot-sidebar.vue +++ b/apps/web/src/pages/chat/components/bot-sidebar.vue @@ -54,7 +54,6 @@ diff --git a/apps/web/src/pages/chat/components/tool-call-block.vue b/apps/web/src/pages/chat/components/tool-call-block.vue index 98f32d59..13ffca23 100644 --- a/apps/web/src/pages/chat/components/tool-call-block.vue +++ b/apps/web/src/pages/chat/components/tool-call-block.vue @@ -31,10 +31,6 @@ v-else-if="block.toolName === 'search_memory'" :block="block" /> - -
-
- - - - {{ query }} - - - search_inbox - - - {{ $t('chat.toolInboxResults', { count: results.length }) }} - - - {{ $t('chat.toolDone') }} - - - {{ $t('chat.toolRunning') }} - -
- - - - - {{ $t('chat.toolSearchResultsLabel') }} - - -
-
-
- - {{ item.header }} - - - {{ item.created_at }} - -
- - {{ item.content }} - -
-
-
-
-
- - - diff --git a/apps/web/src/pages/chat/index.vue b/apps/web/src/pages/chat/index.vue index 3b5fa989..9fae8da4 100644 --- a/apps/web/src/pages/chat/index.vue +++ b/apps/web/src/pages/chat/index.vue @@ -8,8 +8,27 @@

-