mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(settings): manage session owner identities
This commit is contained in:
1 parent
abf4a015cc
commit
f198b41bdb
12 files changed
+206
-23
No files matched your search
@@ -24,6 +24,8 @@ export interface AnalysisSessionDTO extends CoreSessionInfo {
|
||||
dbPath: string
|
||||
memberAvatar: string | null
|
||||
aiConversationCount: number
|
||||
ownerName: string | null
|
||||
ownerStatus: 'resolved' | 'missing' | 'unresolved'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +66,26 @@ function buildSession(
|
||||
memberAvatar = getPrivateChatMemberAvatar(db, meta.name, meta.ownerId)
|
||||
}
|
||||
|
||||
let dto: AnalysisSessionDTO = { ...info, id, dbPath, memberAvatar, aiConversationCount: 0 }
|
||||
const ownerMember = meta.ownerId
|
||||
? (db
|
||||
.prepare(
|
||||
`SELECT COALESCE(NULLIF(group_nickname, ''), NULLIF(account_name, ''), platform_id) as displayName
|
||||
FROM member
|
||||
WHERE platform_id = ?
|
||||
LIMIT 1`
|
||||
)
|
||||
.get(meta.ownerId) as { displayName: string } | undefined)
|
||||
: undefined
|
||||
|
||||
let dto: AnalysisSessionDTO = {
|
||||
...info,
|
||||
id,
|
||||
dbPath,
|
||||
memberAvatar,
|
||||
aiConversationCount: 0,
|
||||
ownerName: ownerMember?.displayName ?? null,
|
||||
ownerStatus: !meta.ownerId ? 'missing' : ownerMember ? 'resolved' : 'unresolved',
|
||||
}
|
||||
if (options?.enrichSession) {
|
||||
dto = options.enrichSession(dto)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,9 @@ const isDismissing = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedPlatformId = ref<string | null>(null)
|
||||
|
||||
const currentOwnerId = computed(() => sessionStore.currentSession?.ownerId ?? null)
|
||||
const currentOwnerId = computed(
|
||||
() => sessionStore.sessions.find((session) => session.id === props.sessionId)?.ownerId ?? null
|
||||
)
|
||||
|
||||
function getDisplayName(member: MemberWithStats): string {
|
||||
return member.groupNickname || member.accountName || member.platformId
|
||||
@@ -154,18 +156,26 @@ function later() {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
// 打开时加载成员并默认选中当前 owner
|
||||
watch(isOpen, (open) => {
|
||||
if (open) {
|
||||
searchQuery.value = ''
|
||||
selectedPlatformId.value = currentOwnerId.value
|
||||
loadMembers()
|
||||
}
|
||||
})
|
||||
function prepareOwnerSelection() {
|
||||
searchQuery.value = ''
|
||||
selectedPlatformId.value = currentOwnerId.value
|
||||
members.value = []
|
||||
void loadMembers()
|
||||
}
|
||||
|
||||
// 打开时加载成员并默认选中当前 owner。immediate 覆盖以打开状态首次挂载的场景。
|
||||
watch(
|
||||
isOpen,
|
||||
(open) => {
|
||||
if (open) prepareOwnerSelection()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.sessionId,
|
||||
() => {
|
||||
if (isOpen.value) prepareOwnerSelection()
|
||||
checkAndAutoOpen()
|
||||
}
|
||||
)
|
||||
@@ -176,7 +186,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'max-w-md' }">
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'max-w-md z-[111]', overlay: 'z-[110]' }">
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<!-- 头部 -->
|
||||
@@ -270,8 +280,8 @@ onMounted(() => {
|
||||
<div class="mt-5 flex items-center justify-between gap-2">
|
||||
<UButton
|
||||
v-if="!currentOwnerId"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
color="primary"
|
||||
size="sm"
|
||||
:loading="isDismissing"
|
||||
@click="dismissForever"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual'
|
||||
@@ -7,6 +7,7 @@ import { useToast } from '@/composables/useToast'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import type { AnalysisSession } from '@/types/base'
|
||||
import LazyAvatar from '@/components/common/avatar/LazyAvatar.vue'
|
||||
import OwnerPromptModal from '@/components/analysis/member/OwnerPromptModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
@@ -15,12 +16,24 @@ dayjs.extend(relativeTime)
|
||||
const toast = useToast()
|
||||
const SESSION_ROW_HEIGHT = 48
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
focusOwnerIssues?: boolean
|
||||
}>(),
|
||||
{
|
||||
focusOwnerIssues: false,
|
||||
}
|
||||
)
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const sessionStore = useSessionStore()
|
||||
const { sessions } = storeToRefs(sessionStore)
|
||||
|
||||
// 搜索关键词
|
||||
const searchQuery = ref('')
|
||||
const ownerIssueCount = computed(
|
||||
() => sessions.value.filter((session) => session.ownerStatus !== 'resolved').length
|
||||
)
|
||||
|
||||
// 过滤后的会话列表
|
||||
const filteredSessions = computed(() => {
|
||||
@@ -28,10 +41,12 @@ const filteredSessions = computed(() => {
|
||||
return sessions.value
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
return sessions.value.filter((s) => s.name.toLowerCase().includes(query) || s.platform.toLowerCase().includes(query))
|
||||
return sessions.value.filter(
|
||||
(session) => session.name.toLowerCase().includes(query) || session.platform.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
type SortField = 'name' | 'platform' | 'messageCount' | 'importedAt'
|
||||
type SortField = 'name' | 'platform' | 'owner' | 'messageCount' | 'importedAt'
|
||||
type SortDirection = 'asc' | 'desc'
|
||||
type HeaderAlign = 'left' | 'center' | 'right'
|
||||
type HeaderColumn =
|
||||
@@ -75,6 +90,14 @@ const headerColumns: HeaderColumn[] = [
|
||||
class: 'w-20',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
key: 'owner',
|
||||
type: 'sortable',
|
||||
field: 'owner',
|
||||
labelKey: 'tools.batchManage.columns.owner',
|
||||
class: 'w-28',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
type: 'sortable',
|
||||
@@ -108,8 +131,8 @@ const headerColumns: HeaderColumn[] = [
|
||||
]
|
||||
|
||||
const sortState = ref<{ field: SortField | null; direction: SortDirection | null }>({
|
||||
field: null,
|
||||
direction: null,
|
||||
field: 'importedAt',
|
||||
direction: 'desc',
|
||||
})
|
||||
|
||||
function getDefaultDirection(_field: SortField): SortDirection {
|
||||
@@ -173,6 +196,13 @@ const sortedSessions = computed(() => {
|
||||
if (field === 'platform') {
|
||||
return getPlatformLabel(a.platform).localeCompare(getPlatformLabel(b.platform), locale.value) * multiplier
|
||||
}
|
||||
if (field === 'owner') {
|
||||
const ownerValue = (session: AnalysisSession) => {
|
||||
if (session.ownerStatus === 'resolved') return `0:${session.ownerName || session.ownerId || ''}`
|
||||
return session.ownerStatus === 'unresolved' ? '1:' : '2:'
|
||||
}
|
||||
return ownerValue(a).localeCompare(ownerValue(b), locale.value) * multiplier
|
||||
}
|
||||
if (field === 'messageCount') {
|
||||
return (a.messageCount - b.messageCount) * multiplier
|
||||
}
|
||||
@@ -183,6 +213,20 @@ const sortedSessions = computed(() => {
|
||||
})
|
||||
|
||||
const listScrollRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function prioritizeOwnerIssues() {
|
||||
sortState.value = { field: 'owner', direction: 'desc' }
|
||||
nextTick(() => listScrollRef.value?.scrollTo({ top: 0 }))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.focusOwnerIssues,
|
||||
(focus) => {
|
||||
if (focus) prioritizeOwnerIssues()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const sessionVirtualizer = useVirtualizer(
|
||||
computed(() => ({
|
||||
count: sortedSessions.value.length,
|
||||
@@ -215,6 +259,32 @@ const editingId = ref<string | null>(null)
|
||||
// 编辑中的名称
|
||||
const editingName = ref('')
|
||||
|
||||
const ownerSessionId = ref<string | null>(null)
|
||||
const showOwnerModal = ref(false)
|
||||
const continueOwnerSetup = ref(false)
|
||||
const ownerSession = computed(() => sessions.value.find((session) => session.id === ownerSessionId.value) ?? null)
|
||||
|
||||
function openOwnerModal(session: AnalysisSession, event: Event) {
|
||||
event.stopPropagation()
|
||||
ownerSessionId.value = session.id
|
||||
continueOwnerSetup.value = session.ownerStatus !== 'resolved'
|
||||
showOwnerModal.value = true
|
||||
}
|
||||
|
||||
async function refreshAfterOwnerChange() {
|
||||
await sessionStore.loadSessions()
|
||||
if (!continueOwnerSetup.value) return
|
||||
|
||||
await nextTick()
|
||||
const nextSession = sortedSessions.value.find((session) => session.ownerStatus !== 'resolved')
|
||||
if (nextSession) {
|
||||
ownerSessionId.value = nextSession.id
|
||||
showOwnerModal.value = true
|
||||
} else {
|
||||
continueOwnerSetup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectedMergeSessions = computed(() => sessions.value.filter((s) => selectedIds.value.has(s.id)))
|
||||
const selectedMergeTypes = computed(() => new Set(selectedMergeSessions.value.map((s) => s.type)))
|
||||
|
||||
@@ -538,6 +608,17 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<button
|
||||
v-if="ownerIssueCount > 0"
|
||||
type="button"
|
||||
class="mb-3 inline-flex w-fit max-w-full items-center gap-2 rounded-lg bg-amber-50 px-3 py-2 text-left text-xs text-amber-800 transition-colors hover:bg-amber-100 dark:bg-amber-950/20 dark:text-amber-300 dark:hover:bg-amber-950/30"
|
||||
@click="prioritizeOwnerIssues"
|
||||
>
|
||||
<UIcon name="i-heroicons-user-circle" class="h-4 w-4 shrink-0" />
|
||||
<span>{{ t('tools.batchManage.ownerIssues', { count: ownerIssueCount }) }}</span>
|
||||
<UIcon name="i-heroicons-arrow-right" class="h-3.5 w-3.5 shrink-0 opacity-70" />
|
||||
</button>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="mb-4">
|
||||
<UInput
|
||||
@@ -713,6 +794,28 @@ onMounted(() => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 我是谁 -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-28 min-w-0 items-center gap-1.5 text-left text-xs transition-colors hover:text-primary-600 dark:hover:text-primary-400"
|
||||
:class="session.ownerStatus === 'resolved' ? 'text-gray-600 dark:text-gray-300' : 'text-amber-600 dark:text-amber-400'"
|
||||
:title="session.ownerId || t('tools.batchManage.ownerStatus.missing')"
|
||||
@click="openOwnerModal(session, $event)"
|
||||
>
|
||||
<UIcon
|
||||
:name="session.ownerStatus === 'resolved' ? 'i-heroicons-user-circle' : 'i-heroicons-exclamation-circle'"
|
||||
class="h-3.5 w-3.5 shrink-0"
|
||||
/>
|
||||
<span class="truncate">
|
||||
{{
|
||||
session.ownerStatus === 'resolved'
|
||||
? session.ownerName || session.ownerId
|
||||
: t(`tools.batchManage.ownerStatus.${session.ownerStatus}`)
|
||||
}}
|
||||
</span>
|
||||
<UIcon name="i-heroicons-chevron-right" class="h-3 w-3 shrink-0 opacity-60" />
|
||||
</button>
|
||||
|
||||
<!-- 消息数 -->
|
||||
<div class="w-24 text-right text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ session.messageCount.toLocaleString() }}
|
||||
@@ -737,6 +840,15 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OwnerPromptModal
|
||||
v-if="ownerSession"
|
||||
v-model="showOwnerModal"
|
||||
:session-id="ownerSession.id"
|
||||
:chat-type="ownerSession.type"
|
||||
@saved="refreshAfterOwnerChange"
|
||||
@cleared="refreshAfterOwnerChange"
|
||||
/>
|
||||
|
||||
<!-- 合并确认弹窗 -->
|
||||
<UModal v-model:open="showMergeModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
|
||||
@@ -116,7 +116,11 @@ watch(showSettings, async (visible) => {
|
||||
:ref="(el: unknown) => setTabRef('ai', el)"
|
||||
@config-changed="handleAIConfigChanged"
|
||||
/>
|
||||
<BatchManageTab v-else-if="activeTab === 'data'" key="data" />
|
||||
<BatchManageTab
|
||||
v-else-if="activeTab === 'data'"
|
||||
key="data"
|
||||
:focus-owner-issues="settingsSubTab === 'missing-owner'"
|
||||
/>
|
||||
<ApiSettingsTab v-else-if="activeTab === 'api'" key="api" />
|
||||
<StorageTab
|
||||
v-else-if="activeTab === 'storage'"
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"batchManage": {
|
||||
"searchPlaceholder": "Search conversation name",
|
||||
"searchResult": "Found {count} / {total}",
|
||||
"ownerIssues": "{count} conversations have not set who you are",
|
||||
"ownerStatus": {
|
||||
"missing": "Not set",
|
||||
"unresolved": "Needs review"
|
||||
},
|
||||
"noSearchResult": "No matching chat records found",
|
||||
"selectAll": "Select All",
|
||||
"shiftClickHint": "Hold Shift to select a range",
|
||||
@@ -14,6 +19,7 @@
|
||||
"columns": {
|
||||
"name": "Name",
|
||||
"platform": "Platform",
|
||||
"owner": "Me",
|
||||
"messages": "Messages",
|
||||
"summaries": "AI Summaries",
|
||||
"aiChats": "AI Chats",
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"batchManage": {
|
||||
"searchPlaceholder": "チャット名を検索",
|
||||
"searchResult": "{count} / {total} 件を検出",
|
||||
"ownerIssues": "{count} 件の会話で自分が未設定です",
|
||||
"ownerStatus": {
|
||||
"missing": "未設定",
|
||||
"unresolved": "要確認"
|
||||
},
|
||||
"noSearchResult": "一致するチャット履歴が見つかりません",
|
||||
"selectAll": "すべて選択",
|
||||
"shiftClickHint": "Shiftキーを押しながらクリックで範囲選択できます",
|
||||
@@ -14,6 +19,7 @@
|
||||
"columns": {
|
||||
"name": "名前",
|
||||
"platform": "プラットフォーム",
|
||||
"owner": "自分",
|
||||
"messages": "メッセージ数",
|
||||
"summaries": "AI要約",
|
||||
"aiChats": "AI チャット",
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"batchManage": {
|
||||
"searchPlaceholder": "搜索对话名称",
|
||||
"searchResult": "找到 {count} / {total} 个",
|
||||
"ownerIssues": "{count} 个会话尚未设置“我是谁”",
|
||||
"ownerStatus": {
|
||||
"missing": "未设置",
|
||||
"unresolved": "需确认"
|
||||
},
|
||||
"noSearchResult": "未找到匹配的聊天记录",
|
||||
"selectAll": "全选",
|
||||
"shiftClickHint": "按住 Shift 点击可范围多选",
|
||||
@@ -14,6 +19,7 @@
|
||||
"columns": {
|
||||
"name": "名称",
|
||||
"platform": "平台",
|
||||
"owner": "我是谁",
|
||||
"messages": "消息数",
|
||||
"summaries": "AI摘要",
|
||||
"aiChats": "AI 对话",
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"batchManage": {
|
||||
"searchPlaceholder": "搜尋聊天名稱",
|
||||
"searchResult": "找到 {count} / {total} 筆",
|
||||
"ownerIssues": "{count} 個會話尚未設定「我是誰」",
|
||||
"ownerStatus": {
|
||||
"missing": "未設定",
|
||||
"unresolved": "需確認"
|
||||
},
|
||||
"noSearchResult": "未找到匹配的聊天紀錄",
|
||||
"selectAll": "全選",
|
||||
"shiftClickHint": "按住 Shift 點擊可範圍多選",
|
||||
@@ -14,6 +19,7 @@
|
||||
"columns": {
|
||||
"name": "名稱",
|
||||
"platform": "平台",
|
||||
"owner": "我是誰",
|
||||
"messages": "訊息數",
|
||||
"summaries": "AI摘要",
|
||||
"aiChats": "AI 對話",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import type { AnnualSummaryFetchOptions } from '@/services/data/types'
|
||||
import type { AnnualSummaryResponse } from '@openchatlab/shared-types'
|
||||
import LoadingState from '@/components/UI/LoadingState.vue'
|
||||
@@ -11,7 +11,7 @@ import { useAnnualSummaryTimeRange } from '../annual-summary-time-range'
|
||||
import AnnualInsightBoard from './components/AnnualInsightBoard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
const currentYear = new Date().getFullYear()
|
||||
const timeRange = useAnnualSummaryTimeRange()
|
||||
const response = ref<AnnualSummaryResponse | null>(null)
|
||||
@@ -103,7 +103,7 @@ function switchToLatestYear(): void {
|
||||
}
|
||||
|
||||
function openSessions(): void {
|
||||
void router.push('/')
|
||||
layoutStore.openSettings('data', 'missing-owner')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -119,7 +119,11 @@ onMounted(async () => {
|
||||
:ref="(el) => setTabRef('ai', el)"
|
||||
@config-changed="handleAIConfigChanged"
|
||||
/>
|
||||
<BatchManageTab v-else-if="activeTab === 'data'" key="data" />
|
||||
<BatchManageTab
|
||||
v-else-if="activeTab === 'data'"
|
||||
key="data"
|
||||
:focus-owner-issues="route.query.subTab === 'missing-owner'"
|
||||
/>
|
||||
<ApiSettingsTab v-else-if="activeTab === 'api'" key="api" />
|
||||
<StorageTab v-else-if="activeTab === 'storage'" key="storage" :ref="(el) => setTabRef('storage', el)" />
|
||||
<AboutTab v-else-if="activeTab === 'about'" key="about" />
|
||||
|
||||
@@ -679,6 +679,8 @@ export const useSessionStore = defineStore(
|
||||
const session = sessions.value.find((s) => s.id === id)
|
||||
if (session) {
|
||||
session.ownerId = ownerId
|
||||
session.ownerName = null
|
||||
session.ownerStatus = ownerId ? 'unresolved' : 'missing'
|
||||
}
|
||||
}
|
||||
return success
|
||||
@@ -698,6 +700,8 @@ export const useSessionStore = defineStore(
|
||||
const session = sessions.value.find((s) => s.id === sessionId)
|
||||
if (session) {
|
||||
session.ownerId = result.updatedSessionOwnerIds[sessionId] ?? result.ownerId
|
||||
session.ownerName = session.ownerId
|
||||
session.ownerStatus = 'resolved'
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -712,6 +716,8 @@ export const useSessionStore = defineStore(
|
||||
const session = sessions.value.find((s) => s.id === id)
|
||||
if (session) {
|
||||
session.ownerId = result.ownerId
|
||||
session.ownerName = result.ownerId
|
||||
session.ownerStatus = 'resolved'
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -128,6 +128,8 @@ export interface AnalysisSession {
|
||||
groupId: string | null // 群ID(群聊类型有值,私聊为空)
|
||||
groupAvatar: string | null // 群头像(base64 Data URL)
|
||||
ownerId: string | null // 所有者/导出者的 platformId
|
||||
ownerName: string | null // 所有者在当前会话中的显示名称
|
||||
ownerStatus: 'resolved' | 'missing' | 'unresolved' // 所有者是否能匹配当前会话成员
|
||||
memberAvatar: string | null // 私聊对方头像(base64 Data URL)
|
||||
lastMessageTs: number | null // 最后一条消息时间戳(秒)
|
||||
summaryCount: number // 已生成摘要的会话片段数
|
||||
|
||||
Reference in new issue
Block a user