feat: 逻辑优化

This commit is contained in:
digua
2025-12-06 02:20:11 +08:00
parent 5618bf7be4
commit cd2dc0b8db
12 changed files with 361 additions and 292 deletions
+11 -1
View File
@@ -147,7 +147,7 @@ function getLockedPromptSection(chatType: 'group' | 'private'): string {
` `
: `成员查询策略: : `成员查询策略:
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表 - 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表
- 群成员有三种名称:accountNameQQ原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名) - 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称 - 通过 get_group_members 的 search 参数可以模糊搜索这三种名称
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言 - 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言
` `
@@ -553,6 +553,16 @@ export class Agent {
let toolParams: Record<string, unknown> | undefined let toolParams: Record<string, unknown> | undefined
try { try {
toolParams = JSON.parse(tc.function.arguments || '{}') toolParams = JSON.parse(tc.function.arguments || '{}')
// 对于消息获取类工具,用用户配置的 limit 覆盖 LLM 传递的值(保持显示一致)
const toolsWithLimit = ['search_messages', 'get_recent_messages', 'get_conversation_between']
if (this.context.maxMessagesLimit && toolsWithLimit.includes(tc.function.name)) {
toolParams = {
...toolParams,
limit: this.context.maxMessagesLimit, // 用户配置优先
}
}
// 对于搜索类工具,添加时间范围信息 // 对于搜索类工具,添加时间范围信息
if ( if (
this.context.timeFilter && this.context.timeFilter &&
+13 -6
View File
@@ -54,8 +54,8 @@ async function searchMessagesExecutor(
context: ToolContext context: ToolContext
): Promise<unknown> { ): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context
// 优先使用 LLM 指定的 limit,其次使用用户配置,最后使用默认值 200,上限 5000 // 用户配置优先:如果用户设置了 maxMessagesLimit,使用它;否则使用 LLM 指定的值或默认值 200,上限 5000
const limit = Math.min(params.limit || maxMessagesLimit || 200, 5000) const limit = Math.min(maxMessagesLimit || params.limit || 200, 5000)
// 构建时间过滤器:优先使用 LLM 指定的年/月,否则使用 context 中的 // 构建时间过滤器:优先使用 LLM 指定的年/月,否则使用 context 中的
let effectiveTimeFilter = contextTimeFilter let effectiveTimeFilter = contextTimeFilter
@@ -144,8 +144,15 @@ async function getRecentMessagesExecutor(
context: ToolContext context: ToolContext
): Promise<unknown> { ): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context
// 优先使用 LLM 指定的 limit,其次使用用户配置,最后使用默认值 100 // 用户配置优先:如果用户设置了 maxMessagesLimit,使用它;否则使用 LLM 指定的值或默认值 100
const limit = params.limit || maxMessagesLimit || 100 const limit = maxMessagesLimit || params.limit || 100
console.log('[getRecentMessages] 参数调试:', {
'params.limit': params.limit,
'context.maxMessagesLimit': maxMessagesLimit,
'计算后的 limit': limit,
'context 完整内容': JSON.stringify(context),
})
// 构建时间过滤器:优先使用 LLM 指定的年/月 // 构建时间过滤器:优先使用 LLM 指定的年/月
let effectiveTimeFilter = contextTimeFilter let effectiveTimeFilter = contextTimeFilter
@@ -495,8 +502,8 @@ async function getConversationBetweenExecutor(
context: ToolContext context: ToolContext
): Promise<unknown> { ): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context
// 优先使用 LLM 指定的 limit,其次使用用户配置,最后使用默认值 100 // 用户配置优先:如果用户设置了 maxMessagesLimit,使用它;否则使用 LLM 指定的值或默认值 100
const limit = params.limit || maxMessagesLimit || 100 const limit = maxMessagesLimit || params.limit || 100
// 构建时间过滤器 // 构建时间过滤器
let effectiveTimeFilter = contextTimeFilter let effectiveTimeFilter = contextTimeFilter
+7 -5
View File
@@ -476,11 +476,13 @@ export function registerAIHandlers({ win }: IpcContext): void {
if (abortController.signal.aborted) { if (abortController.signal.aborted) {
return return
} }
aiLogger.debug('IPC', `Agent chunk: ${requestId}`, { // 减少日志噪音:只在工具调用时记录
type: chunk.type, if (chunk.type === 'tool_start' || chunk.type === 'tool_result') {
contentLength: chunk.content?.length, aiLogger.debug('IPC', `Agent chunk: ${requestId}`, {
toolName: chunk.toolName, type: chunk.type,
}) toolName: chunk.toolName,
})
}
win.webContents.send('agent:streamChunk', { requestId, chunk }) win.webContents.send('agent:streamChunk', { requestId, chunk })
}) })
+9 -1
View File
@@ -472,7 +472,15 @@ const aiApi = {
dataMessageCount?: number, dataMessageCount?: number,
contentBlocks?: ContentBlock[] contentBlocks?: ContentBlock[]
): Promise<AIMessage> => { ): Promise<AIMessage> => {
return ipcRenderer.invoke('ai:addMessage', conversationId, role, content, dataKeywords, dataMessageCount, contentBlocks) return ipcRenderer.invoke(
'ai:addMessage',
conversationId,
role,
content,
dataKeywords,
dataMessageCount,
contentBlocks
)
}, },
/** /**
+3 -3
View File
@@ -42,9 +42,9 @@ watch(
() => props.open, () => props.open,
(newVal) => { (newVal) => {
if (newVal) { if (newVal) {
activeTab.value = 'ai-config' activeTab.value = 'settings' // 默认打开基础设置 Tab
// 刷新 AI 配置 // 刷新缓存管理
aiConfigRef.value?.refresh() cacheManageRef.value?.refresh()
} }
} }
) )
-9
View File
@@ -4,10 +4,6 @@ import { storeToRefs } from 'pinia'
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import type { AnalysisSession } from '@/types/chat' import type { AnalysisSession } from '@/types/chat'
import pkg from '../../../package.json'
const { version } = pkg
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
@@ -331,11 +327,6 @@ function isPrivateChat(session: AnalysisSession): boolean {
<span v-if="!isCollapsed" class="truncate">设置</span> <span v-if="!isCollapsed" class="truncate">设置</span>
</UButton> </UButton>
</UTooltip> </UTooltip>
<!-- 版本号 & 社交链接 -->
<div v-if="!isCollapsed" class="flex items-center justify-center gap-2 py-1 text-xs text-gray-400">
<span>v{{ version }}</span>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -105,9 +105,9 @@ function isActivePreset(presetId: string, chatType: 'group' | 'private'): boolea
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white"> <h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-4 w-4 text-violet-500" /> <UIcon name="i-heroicons-chat-bubble-left-right" class="h-4 w-4 text-violet-500" />
群聊预设 群聊系统提示词
</h4> </h4>
<UButton size="xs" variant="ghost" color="gray" @click="openAddModal('group')"> <UButton variant="ghost" color="gray" @click="openAddModal('group')">
<UIcon name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" /> <UIcon name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" />
添加 添加
</UButton> </UButton>
@@ -141,17 +141,17 @@ function isActivePreset(presetId: string, chatType: 'group' | 'private'): boolea
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name }}</span> <span class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name }}</span>
<UBadge v-if="preset.isBuiltIn" color="gray" variant="soft" size="xs">内置</UBadge> <UBadge v-if="preset.isBuiltIn" color="gray" variant="soft">内置</UBadge>
</div> </div>
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100" @click.stop> <div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
<UButton size="xs" color="gray" variant="ghost" @click="openEditModal(preset)"> <UButton color="gray" variant="ghost" @click="openEditModal(preset)">
{{ preset.isBuiltIn ? '查看' : '编辑' }} {{ preset.isBuiltIn ? '查看' : '编辑' }}
</UButton> </UButton>
<UButton size="xs" color="gray" variant="ghost" @click="duplicatePreset(preset.id)">复制</UButton> <UButton color="gray" variant="ghost" @click="duplicatePreset(preset.id)">复制</UButton>
<UButton v-if="!preset.isBuiltIn" size="xs" color="error" variant="ghost" @click="deletePreset(preset.id)"> <UButton v-if="!preset.isBuiltIn" color="error" variant="ghost" @click="deletePreset(preset.id)">
删除 删除
</UButton> </UButton>
</div> </div>
@@ -162,14 +162,14 @@ function isActivePreset(presetId: string, chatType: 'group' | 'private'): boolea
<!-- 分隔线 --> <!-- 分隔线 -->
<div class="border-t border-gray-200 dark:border-gray-700"></div> <div class="border-t border-gray-200 dark:border-gray-700"></div>
<!-- 私聊预设组 --> <!-- 私聊系统提示词 -->
<div> <div>
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white"> <h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
<UIcon name="i-heroicons-user" class="h-4 w-4 text-blue-500" /> <UIcon name="i-heroicons-user" class="h-4 w-4 text-blue-500" />
私聊预设 私聊系统提示词
</h4> </h4>
<UButton size="xs" variant="ghost" color="gray" @click="openAddModal('private')"> <UButton variant="ghost" color="gray" @click="openAddModal('private')">
<UIcon name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" /> <UIcon name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" />
添加 添加
</UButton> </UButton>
@@ -203,33 +203,23 @@ function isActivePreset(presetId: string, chatType: 'group' | 'private'): boolea
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name }}</span> <span class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name }}</span>
<UBadge v-if="preset.isBuiltIn" color="gray" variant="soft" size="xs">内置</UBadge> <UBadge v-if="preset.isBuiltIn" color="gray" variant="soft">内置</UBadge>
</div> </div>
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100" @click.stop> <div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
<UButton size="xs" color="gray" variant="ghost" @click="openEditModal(preset)"> <UButton color="gray" variant="ghost" @click="openEditModal(preset)">
{{ preset.isBuiltIn ? '查看' : '编辑' }} {{ preset.isBuiltIn ? '查看' : '编辑' }}
</UButton> </UButton>
<UButton size="xs" color="gray" variant="ghost" @click="duplicatePreset(preset.id)">复制</UButton> <UButton color="gray" variant="ghost" @click="duplicatePreset(preset.id)">复制</UButton>
<UButton v-if="!preset.isBuiltIn" size="xs" color="error" variant="ghost" @click="deletePreset(preset.id)"> <UButton v-if="!preset.isBuiltIn" color="error" variant="ghost" @click="deletePreset(preset.id)">
删除 删除
</UButton> </UButton>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 提示信息 -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800/50">
<div class="flex items-start gap-2">
<UIcon name="i-heroicons-light-bulb" class="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
<p class="text-xs text-gray-600 dark:text-gray-400">
点击预设即可激活时间信息和工具说明由系统自动注入你只需编辑角色定义和回答要求
</p>
</div>
</div>
</div> </div>
<!-- 编辑/添加弹窗 --> <!-- 编辑/添加弹窗 -->
@@ -32,8 +32,8 @@ const formData = ref({
const isBuiltIn = computed(() => props.preset?.isBuiltIn ?? false) const isBuiltIn = computed(() => props.preset?.isBuiltIn ?? false)
const isEditMode = computed(() => props.mode === 'edit') const isEditMode = computed(() => props.mode === 'edit')
const modalTitle = computed(() => { const modalTitle = computed(() => {
if (isBuiltIn.value) return '查看预设' if (isBuiltIn.value) return '查看系统提示词'
return isEditMode.value ? '编辑预设' : '添加预设' return isEditMode.value ? '编辑系统提示词' : '添加系统提示词'
}) })
const canSave = computed(() => { const canSave = computed(() => {
@@ -149,7 +149,7 @@ function getLockedPromptSection(chatType: string): string {
- 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息` - 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息`
: `成员查询策略: : `成员查询策略:
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表 - 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表
- 群成员有三种名称:accountNameQQ原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名) - 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称 - 通过 get_group_members 的 search 参数可以模糊搜索这三种名称
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言` - 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言`
@@ -213,12 +213,15 @@ ${formData.value.responseRules}`
<!-- 预设名称 --> <!-- 预设名称 -->
<div> <div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">预设名称</label> <label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">预设名称</label>
<UInput v-model="formData.name" placeholder="为预设起个名字" :disabled="isBuiltIn" /> <UInput v-model="formData.name" placeholder="为预设起个名字" :disabled="isBuiltIn" class="w-60" />
</div> </div>
<!-- 适用类型只读显示 --> <!-- 适用类型只读显示 -->
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"> <div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<UIcon :name="formData.chatType === 'group' ? 'i-heroicons-chat-bubble-left-right' : 'i-heroicons-user'" class="h-4 w-4" /> <UIcon
:name="formData.chatType === 'group' ? 'i-heroicons-chat-bubble-left-right' : 'i-heroicons-user'"
class="h-4 w-4"
/>
<span>适用于{{ formData.chatType === 'group' ? '群聊' : '私聊' }}</span> <span>适用于{{ formData.chatType === 'group' ? '群聊' : '私聊' }}</span>
</div> </div>
@@ -227,10 +230,10 @@ ${formData.value.responseRules}`
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">角色定义</label> <label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">角色定义</label>
<UTextarea <UTextarea
v-model="formData.roleDefinition" v-model="formData.roleDefinition"
:rows="5" :rows="8"
placeholder="定义 AI 助手的角色和任务..." placeholder="定义 AI 助手的角色和任务..."
:disabled="isBuiltIn" :disabled="isBuiltIn"
class="font-mono text-sm" class="font-mono text-sm w-120"
/> />
</div> </div>
@@ -245,7 +248,7 @@ ${formData.value.responseRules}`
:rows="5" :rows="5"
placeholder="定义 AI 回答的格式和要求..." placeholder="定义 AI 回答的格式和要求..."
:disabled="isBuiltIn" :disabled="isBuiltIn"
class="font-mono text-sm" class="font-mono text-sm w-120"
/> />
</div> </div>
@@ -256,7 +259,7 @@ ${formData.value.responseRules}`
完整提示词预览 完整提示词预览
</label> </label>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50"> <div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
<pre class="max-h-48 overflow-y-auto whitespace-pre-wrap text-xs text-gray-700 dark:text-gray-300">{{ previewContent }}</pre> <pre class="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300">{{ previewContent }}</pre>
</div> </div>
</div> </div>
</div> </div>
@@ -272,4 +275,3 @@ ${formData.value.responseRules}`
</template> </template>
</UModal> </UModal>
</template> </template>
+10 -1
View File
@@ -243,6 +243,12 @@ export function useAIChat(
maxMessagesLimit: aiGlobalSettings.value.maxMessagesPerRequest, maxMessagesLimit: aiGlobalSettings.value.maxMessagesPerRequest,
} }
console.log('[AI] 构建 context:', {
sessionId,
maxMessagesLimit: context.maxMessagesLimit,
aiGlobalSettings: aiGlobalSettings.value,
})
// 收集历史消息(排除当前用户消息和 AI 占位消息) // 收集历史消息(排除当前用户消息和 AI 占位消息)
const historyMessages = messages.value const historyMessages = messages.value
.slice(0, -2) // 排除刚添加的用户消息和 AI 占位消息 .slice(0, -2) // 排除刚添加的用户消息和 AI 占位消息
@@ -270,7 +276,10 @@ export function useAIChat(
return return
} }
console.log('[AI] Agent chunk:', chunk.type, chunk.toolName || chunk.content?.slice(0, 50)) // 只在工具调用时记录,减少日志噪音
if (chunk.type === 'tool_start' || chunk.type === 'tool_result') {
console.log('[AI] Agent chunk:', chunk.type, chunk.toolName)
}
switch (chunk.type) { switch (chunk.type) {
case 'content': case 'content':
@@ -0,0 +1,280 @@
<script setup lang="ts">
import { ref } from 'vue'
// Props
defineProps<{
open: boolean
}>()
// Emits
const emit = defineEmits<{
'update:open': [value: boolean]
}>()
// 通用格式弹窗状态
const showFormatModal = ref(false)
// 复制格式示例
const formatExample = `{
"chatlab": {
"version": "1.0.0",
"exportedAt": 1732924800,
"generator": "Your Tool Name"
},
"meta": {
"name": "群聊名称",
"platform": "qq",
"type": "group"
},
"members": [
{
"platformId": "123456789",
"accountName": "用户昵称",
"groupNickname": "群昵称(可选)"
}
],
"messages": [
{
"sender": "123456789",
"accountName": "发送时昵称",
"timestamp": 1732924800,
"type": 0,
"content": "消息内容"
}
]
}`
function copyFormatExample() {
navigator.clipboard.writeText(formatExample)
}
function closeModal() {
emit('update:open', false)
}
function openExternalLink(url: string) {
window.open(url, '_blank')
}
</script>
<template>
<!-- 导入教程弹窗 -->
<UModal :open="open" @update:open="emit('update:open', $event)" :ui="{ content: 'md:w-full max-w-2xl' }">
<template #content>
<div class="p-6">
<!-- Header -->
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-linear-to-br from-pink-100 to-rose-100 dark:from-pink-900/30 dark:to-rose-900/30"
>
<UIcon name="i-heroicons-book-open" class="h-5 w-5 text-pink-600 dark:text-pink-400" />
</div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">聊天记录导入教程</h2>
</div>
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closeModal" />
</div>
<!-- 教程内容 -->
<div class="space-y-6">
<!-- QQ 教程 -->
<div class="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
<div class="mb-3 flex items-center gap-2">
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-5 w-5 text-pink-500" />
<h3 class="font-semibold text-gray-900 dark:text-white">QQ</h3>
</div>
<ol class="space-y-2">
<li class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300">
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-pink-100 text-xs font-medium text-pink-600 dark:bg-pink-900/30 dark:text-pink-400"
>
1
</span>
<span>
使用
<a
class="cursor-pointer text-pink-600 underline underline-offset-2 hover:text-pink-700 dark:text-pink-400 dark:hover:text-pink-300"
@click="openExternalLink('https://github.com/shuakami/qq-chat-exporter')"
>
qq-chat-exporter
<UIcon name="i-heroicons-arrow-top-right-on-square" class="inline h-3 w-3" />
</a>
导出聊天记录目前仅支持Windows/Linux
</span>
</li>
<li class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300">
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-pink-100 text-xs font-medium text-pink-600 dark:bg-pink-900/30 dark:text-pink-400"
>
2
</span>
<span>导出完成后会得到 .json 文件</span>
</li>
<li class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300">
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-pink-100 text-xs font-medium text-pink-600 dark:bg-pink-900/30 dark:text-pink-400"
>
3
</span>
<span> .json 文件拖拽到上方导入区域</span>
</li>
</ol>
</div>
<!-- 微信教程 -->
<div class="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
<div class="mb-3 flex items-center gap-2">
<UIcon name="i-heroicons-device-phone-mobile" class="h-5 w-5 text-blue-500" />
<h3 class="font-semibold text-gray-900 dark:text-white">微信</h3>
</div>
<ol class="mb-4 space-y-2">
<li class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300">
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
>
1
</span>
<span>网络上工具较多请自行找工具导出聊天记录</span>
</li>
<li class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300">
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
>
2
</span>
<span>使用脚本将导出文件转换为 ChatLab 通用格式</span>
</li>
<li class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300">
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
>
3
</span>
<span>将转换后的 .json 文件拖拽到上方导入区域</span>
</li>
</ol>
<UButton
variant="soft"
size="sm"
:trailing-icon="'i-heroicons-document-text'"
@click="showFormatModal = true"
>
查看通用格式说明
</UButton>
</div>
</div>
<!-- 底部提示 -->
<div class="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800/50">
<p class="text-sm text-gray-500 dark:text-gray-400">
💡 提示ChatLab 支持多种聊天记录格式包括 QQ微信Discord
等平台将导出的文件直接拖拽到导入区域即可开始分析
</p>
</div>
</div>
</template>
</UModal>
<!-- 通用格式说明弹窗层级高于教程弹窗 -->
<UModal v-model:open="showFormatModal" :ui="{ content: 'md:w-full max-w-3xl z-[60]', overlay: 'z-[60]' }">
<template #content>
<div class="p-6">
<!-- Header -->
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-linear-to-br from-blue-100 to-indigo-100 dark:from-blue-900/30 dark:to-indigo-900/30"
>
<UIcon name="i-heroicons-document-text" class="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">ChatLab 通用格式说明</h2>
</div>
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="showFormatModal = false" />
</div>
<!-- 格式说明 -->
<div class="space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-300">
ChatLab 定义了一套聊天记录分析用标准 JSON 格式只需在 JSON 文件中包含
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-pink-600 dark:bg-gray-800 dark:text-pink-400">
chatlab
</code>
对象即可被识别
</p>
<!-- JSON 示例 -->
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">示例格式</span>
<UButton variant="ghost" size="xs" icon="i-heroicons-clipboard-document" @click="copyFormatExample">
复制
</UButton>
</div>
<pre class="overflow-x-auto text-xs leading-relaxed text-gray-700 dark:text-gray-300"><code>{
"chatlab": {
"version": "1.0.0",
"exportedAt": 1732924800,
"generator": "Your Tool Name"
},
"meta": {
"name": "群聊名称",
"platform": "qq", // qq | wechat | telegram | discord 等
"type": "group" // group | private (群聊|私聊)
},
"members": [
{
"platformId": "123456789",
"accountName": "用户昵称",
"groupNickname": "群昵称(可选)"
}
],
"messages": [
{
"sender": "123456789",
"accountName": "发送时昵称",
"timestamp": 1732924800, // 秒级时间戳
"type": 0, // 0=文本 1=图片 2=语音 3=视频
"content": "消息内容"
}
]
}</code></pre>
</div>
<!-- 字段说明 -->
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
<h3 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">消息类型说明</h3>
<div class="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">0</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">文本</span>
</div>
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">1</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">图片</span>
</div>
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">2</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">语音</span>
</div>
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">3</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">视频</span>
</div>
</div>
</div>
</div>
<!-- 底部提示 -->
<div class="mt-6 rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<p class="text-sm text-blue-600 dark:text-blue-400">
💡 文件名只需以
<code class="rounded bg-blue-100 px-1 dark:bg-blue-800">.json</code>
结尾JSON 中包含
<code class="rounded bg-blue-100 px-1 dark:bg-blue-800">chatlab</code>
对象即可被识别
</p>
</div>
</div>
</template>
</UModal>
</template>
@@ -4,13 +4,13 @@ import { FileDropZone } from '@/components/UI'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import ImportTutorialModal from './components/ImportTutorialModal.vue'
const chatStore = useChatStore() const chatStore = useChatStore()
const { isImporting, importProgress } = storeToRefs(chatStore) const { isImporting, importProgress } = storeToRefs(chatStore)
const importError = ref<string | null>(null) const importError = ref<string | null>(null)
const showTutorialModal = ref(false) const showTutorialModal = ref(false)
const showFormatModal = ref(false)
const features = [ const features = [
{ {
@@ -85,71 +85,10 @@ async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) {
} }
} }
// Accordion
const tutorialItems = [
{
value: 'qq',
label: 'QQ',
icon: 'i-heroicons-chat-bubble-left-right',
steps: [
'使用 qq-chat-exporter 导出聊天记录(推荐最新版)',
'导出完成后会得到 .json 文件',
'将 .json 文件拖拽到上方导入区域',
],
link: 'https://github.com/shuakami/qq-chat-exporter',
hasExternalLink: true,
},
{
value: 'other',
label: '其他平台',
icon: 'i-heroicons-device-phone-mobile',
steps: ['使用任意工具导出聊天记录', '将导出文件转换为 ChatLab 通用格式', '将转换后的 .json 文件拖拽到上方导入区域'],
hasExternalLink: false,
showFormatButton: true,
},
]
//
const tutorialDefaultValue = tutorialItems.map((item) => item.value)
function openTutorial() { function openTutorial() {
showTutorialModal.value = true showTutorialModal.value = true
} }
//
const formatExample = `{
"chatlab": {
"version": "1.0.0",
"exportedAt": 1732924800,
"generator": "Your Tool Name"
},
"meta": {
"name": "群聊名称",
"platform": "qq",
"type": "group"
},
"members": [
{
"platformId": "123456789",
"accountName": "用户昵称",
"groupNickname": "群昵称(可选)"
}
],
"messages": [
{
"sender": "123456789",
"accountName": "发送时昵称",
"timestamp": 1732924800,
"type": 0,
"content": "消息内容"
}
]
}`
function copyFormatExample() {
window.electron.copyToClipboard(formatExample)
}
function getProgressText(): string { function getProgressText(): string {
if (!importProgress.value) return '' if (!importProgress.value) return ''
switch (importProgress.value.stage) { switch (importProgress.value.stage) {
@@ -324,176 +263,7 @@ function getProgressDetail(): string {
</div> </div>
</div> </div>
<!-- 通用格式说明弹窗层级高于教程弹窗 -->
<UModal v-model:open="showFormatModal" :ui="{ content: 'md:w-full max-w-3xl z-[60]', overlay: 'z-[60]' }">
<template #content>
<div class="p-6">
<!-- Header -->
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-linear-to-br from-blue-100 to-indigo-100 dark:from-blue-900/30 dark:to-indigo-900/30"
>
<UIcon name="i-heroicons-document-text" class="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">ChatLab 通用格式说明</h2>
</div>
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="showFormatModal = false" />
</div>
<!-- 格式说明 -->
<div class="space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-300">
ChatLab 支持通用的 JSON 格式只需在 JSON 文件中包含
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-pink-600 dark:bg-gray-800 dark:text-pink-400">
chatlab
</code>
对象即可被识别
</p>
<!-- JSON 示例 -->
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">示例格式</span>
<UButton variant="ghost" size="xs" icon="i-heroicons-clipboard-document" @click="copyFormatExample">
复制
</UButton>
</div>
<pre class="overflow-x-auto text-xs leading-relaxed text-gray-700 dark:text-gray-300"><code>{
"chatlab": {
"version": "1.0.0",
"exportedAt": 1732924800,
"generator": "Your Tool Name"
},
"meta": {
"name": "群聊名称",
"platform": "qq", // qq | wechat | telegram | discord
"type": "group" // group | private |
},
"members": [
{
"platformId": "123456789",
"accountName": "用户昵称",
"groupNickname": "群昵称(可选)"
}
],
"messages": [
{
"sender": "123456789",
"accountName": "发送时昵称",
"timestamp": 1732924800, //
"type": 0, // 0= 1= 2= 3=
"content": "消息内容"
}
]
}</code></pre>
</div>
<!-- 字段说明 -->
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
<h3 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">消息类型说明</h3>
<div class="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">0</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">文本</span>
</div>
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">1</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">图片</span>
</div>
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">2</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">语音</span>
</div>
<div class="rounded-lg bg-gray-50 p-2 dark:bg-gray-700">
<span class="font-mono text-pink-600 dark:text-pink-400">3</span>
<span class="ml-2 text-gray-600 dark:text-gray-300">视频</span>
</div>
</div>
</div>
</div>
<!-- 底部提示 -->
<div class="mt-6 rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<p class="text-sm text-blue-600 dark:text-blue-400">
💡 文件名只需以
<code class="rounded bg-blue-100 px-1 dark:bg-blue-800">.json</code>
结尾JSON 中包含
<code class="rounded bg-blue-100 px-1 dark:bg-blue-800">chatlab</code>
对象即可被识别
</p>
</div>
</div>
</template>
</UModal>
<!-- 导入教程弹窗 --> <!-- 导入教程弹窗 -->
<UModal v-model:open="showTutorialModal" :ui="{ content: 'md:w-full max-w-2xl' }"> <ImportTutorialModal v-model:open="showTutorialModal" />
<template #content>
<div class="p-6">
<!-- Header -->
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-linear-to-br from-pink-100 to-rose-100 dark:from-pink-900/30 dark:to-rose-900/30"
>
<UIcon name="i-heroicons-book-open" class="h-5 w-5 text-pink-600 dark:text-pink-400" />
</div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">聊天记录导入教程</h2>
</div>
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="showTutorialModal = false" />
</div>
<!-- 教程内容 - 使用 Accordion -->
<UAccordion type="multiple" :default-value="tutorialDefaultValue" :items="tutorialItems">
<template #body="{ item }">
<!-- 步骤列表 -->
<ol class="mb-4 space-y-2">
<li
v-for="(step, index) in item.steps"
:key="index"
class="flex items-start gap-3 text-sm text-gray-600 dark:text-gray-300"
>
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-pink-100 text-xs font-medium text-pink-600 dark:bg-pink-900/30 dark:text-pink-400"
>
{{ index + 1 }}
</span>
<span>{{ step }}</span>
</li>
</ol>
<!-- 工具链接 / 格式说明按钮 -->
<UButton
v-if="item.hasExternalLink"
variant="soft"
size="sm"
:trailing-icon="'i-heroicons-arrow-top-right-on-square'"
@click="window.electron.openExternal(item.link)"
>
查看导出工具
</UButton>
<UButton
v-if="item.showFormatButton"
variant="soft"
size="sm"
:trailing-icon="'i-heroicons-document-text'"
@click="showFormatModal = true"
>
查看通用格式说明
</UButton>
</template>
</UAccordion>
<!-- 底部提示 -->
<div class="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800/50">
<p class="text-sm text-gray-500 dark:text-gray-400">
💡 提示ChatLab 支持多种聊天记录格式包括 QQ微信Discord
等平台将导出的文件直接拖拽到导入区域即可开始分析
</p>
</div>
</div>
</template>
</UModal>
</div> </div>
</template> </template>
+1 -1
View File
@@ -5,7 +5,7 @@ export const router = createRouter({
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: () => import('@/pages/index.vue'), component: () => import('@/pages/home/index.vue'),
}, },
{ {
path: '/group-chat/:id', path: '/group-chat/:id',