|
|
@@ -2,19 +2,19 @@ import { computed, ref, shallowRef, watch } from 'vue'
|
|
|
import { defineStore } from 'pinia'
|
|
|
import { isSupportedGroupProtocolVersion, SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL } from '@shared/types'
|
|
|
import type {
|
|
|
- GroupAgent, GroupCapabilities, GroupMessage, GroupMessagePage, GroupRoomDetail, GroupRoomSummary, GroupSocketEnvelope, RealtimeConnectionState,
|
|
|
+ GroupAgent, GroupCapabilities, GroupMessage, GroupMessagePage, GroupRoomDetail, GroupRoomSummary, GroupRun, GroupSocketEnvelope, GroupTopicSummary, RealtimeConnectionState,
|
|
|
} from '@shared/types'
|
|
|
import {
|
|
|
addGroupAgent, approveGroupInteraction as approveApi, archiveGroupRoom as archiveApi,
|
|
|
clarifyGroupInteraction as clarifyApi, createGroupRoom as createApi, getGroupCapabilities,
|
|
|
- getGroupMessages, getGroupRoom, getGroupRooms, interruptGroupAgent as interruptApi,
|
|
|
+ getGroupMessages, getGroupRoom, getGroupRooms, getGroupTopics, interruptGroupAgent as interruptApi,
|
|
|
removeGroupAgent as removeAgentApi, sendGroupMessage as sendApi, updateGroupAgent as updateAgentApi,
|
|
|
updateGroupRoom as updateRoomApi, uploadGroupFiles, type AgentSeed,
|
|
|
} from '@/api/groups'
|
|
|
import { GroupEventSocket } from '@/api/realtime'
|
|
|
import { ScopedCache } from '@/utils/cache'
|
|
|
import {
|
|
|
- applyGroupEnvelope, snapshotGroupRoom, upsertGroupMessage, type GroupProtocolState,
|
|
|
+ applyGroupEnvelope, snapshotGroupRoom, upsertGroupMessage, upsertGroupTopic, type GroupProtocolState,
|
|
|
} from '@/utils/groupProtocol'
|
|
|
import { createUuid } from '@/utils/id'
|
|
|
import { ApiError } from '@/api/client'
|
|
|
@@ -23,25 +23,30 @@ import { useAuthStore } from './auth'
|
|
|
type GroupAvailability = 'checking' | 'available' | 'unsupported' | 'unavailable'
|
|
|
|
|
|
interface CachedGroups { state: GroupProtocolState; savedAt: number }
|
|
|
-const cache = new ScopedCache<CachedGroups>('groups-v2')
|
|
|
+const cache = new ScopedCache<CachedGroups>('groups-v3')
|
|
|
|
|
|
function initialProtocol(): GroupProtocolState {
|
|
|
- return { epoch: '', cursor: 0, rooms: [], roomDetails: {}, messagesByRoom: {} }
|
|
|
+ return { epoch: '', cursor: 0, rooms: [], roomDetails: {}, messagesByRoom: {}, topicsByRoom: {}, messagesByTopic: {} }
|
|
|
}
|
|
|
|
|
|
function errorMessage(cause: unknown): string { return cause instanceof Error ? cause.message : '群聊请求失败' }
|
|
|
|
|
|
+function topicTitle(content: string): string {
|
|
|
+ return content.split(/\s+/).filter(Boolean).join(' ').slice(0, 100) || '新话题'
|
|
|
+}
|
|
|
+
|
|
|
export const useGroupsStore = defineStore('groups', () => {
|
|
|
const auth = useAuthStore()
|
|
|
const availability = ref<GroupAvailability>('checking')
|
|
|
const capabilities = ref<GroupCapabilities>()
|
|
|
const protocol = shallowRef<GroupProtocolState>(initialProtocol())
|
|
|
const selectedRoomId = ref<string>()
|
|
|
+ const selectedTopicId = ref<string>()
|
|
|
const connectionState = ref<RealtimeConnectionState>('idle')
|
|
|
const isLoading = ref(false)
|
|
|
const isSending = ref(false)
|
|
|
const error = ref<string>()
|
|
|
- const hasOlderByRoom = ref<Record<string, boolean>>({})
|
|
|
+ const hasOlderByConversation = ref<Record<string, boolean>>({})
|
|
|
const socket = new GroupEventSocket()
|
|
|
let generation = 0
|
|
|
let rebuildPromise: Promise<void> | undefined
|
|
|
@@ -54,10 +59,17 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
|
|
|
const rooms = computed(() => protocol.value.rooms)
|
|
|
const selectedRoom = computed(() => selectedRoomId.value ? protocol.value.roomDetails[selectedRoomId.value] : undefined)
|
|
|
- const messages = computed(() => selectedRoomId.value ? protocol.value.messagesByRoom[selectedRoomId.value] ?? [] : [])
|
|
|
+ const topicProtocol = computed(() => (capabilities.value?.protocolVersion ?? 0) >= 4)
|
|
|
+ const topics = computed(() => selectedRoomId.value ? protocol.value.topicsByRoom[selectedRoomId.value] ?? [] : [])
|
|
|
+ const selectedTopic = computed(() => selectedTopicId.value ? topics.value.find(topic => topic.id === selectedTopicId.value) : undefined)
|
|
|
+ const messages = computed(() => topicProtocol.value
|
|
|
+ ? (selectedTopicId.value ? protocol.value.messagesByTopic[selectedTopicId.value] ?? [] : [])
|
|
|
+ : (selectedRoomId.value ? protocol.value.messagesByRoom[selectedRoomId.value] ?? [] : []))
|
|
|
const agents = computed(() => selectedRoom.value?.agents ?? [])
|
|
|
- const pendingInteractions = computed(() => selectedRoom.value?.pendingInteractions ?? [])
|
|
|
- const hasMoreBefore = computed(() => selectedRoomId.value ? hasOlderByRoom.value[selectedRoomId.value] ?? false : false)
|
|
|
+ const pendingInteractions = computed(() => (selectedRoom.value?.pendingInteractions ?? []).filter(interaction =>
|
|
|
+ !topicProtocol.value || !interaction.topicId || interaction.topicId === selectedTopicId.value))
|
|
|
+ const conversationKey = computed(() => topicProtocol.value ? selectedTopicId.value : selectedRoomId.value)
|
|
|
+ const hasMoreBefore = computed(() => conversationKey.value ? hasOlderByConversation.value[conversationKey.value] ?? false : false)
|
|
|
|
|
|
function scope(): string { return `${auth.user?.id ?? 'anonymous'}:${auth.activeProfile?.name ?? 'all'}` }
|
|
|
|
|
|
@@ -80,6 +92,36 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
return loaded.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
|
}
|
|
|
|
|
|
+ async function loadTopics(roomId: string): Promise<GroupTopicSummary[]> {
|
|
|
+ const loaded: GroupTopicSummary[] = []
|
|
|
+ const seen = new Set<string>()
|
|
|
+ let cursor: string | undefined
|
|
|
+ do {
|
|
|
+ const key = cursor ?? '<first-page>'
|
|
|
+ if (seen.has(key)) throw new Error('群聊话题分页返回了重复游标')
|
|
|
+ seen.add(key)
|
|
|
+ const page = await getGroupTopics(roomId, cursor, 50)
|
|
|
+ loaded.push(...page.items)
|
|
|
+ cursor = page.nextCursor ?? undefined
|
|
|
+ } while (cursor && loaded.length < 2_000)
|
|
|
+ return loaded.sort((a, b) => b.updatedAt - a.updatedAt || b.id.localeCompare(a.id))
|
|
|
+ }
|
|
|
+
|
|
|
+ function topicsForRoom(roomId: string): GroupTopicSummary[] {
|
|
|
+ return protocol.value.topicsByRoom[roomId] ?? []
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadRoomTopics(roomId: string): Promise<void> {
|
|
|
+ if (!topicProtocol.value || Object.prototype.hasOwnProperty.call(protocol.value.topicsByRoom, roomId)) return
|
|
|
+ const expectedGeneration = generation
|
|
|
+ const loaded = await loadTopics(roomId)
|
|
|
+ if (expectedGeneration !== generation) return
|
|
|
+ protocol.value = {
|
|
|
+ ...protocol.value,
|
|
|
+ topicsByRoom: { ...protocol.value.topicsByRoom, [roomId]: loaded },
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
async function connectEvents(anchor: GroupCapabilities, expectedGeneration: number): Promise<void> {
|
|
|
await socket.connect({
|
|
|
epoch: anchor.journalEpoch,
|
|
|
@@ -164,11 +206,16 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
rooms: loadedRooms,
|
|
|
roomDetails: {},
|
|
|
messagesByRoom: {},
|
|
|
+ topicsByRoom: {},
|
|
|
+ messagesByTopic: {},
|
|
|
}
|
|
|
const restoreId = selectedRoomId.value && loadedRooms.some(room => room.id === selectedRoomId.value)
|
|
|
? selectedRoomId.value
|
|
|
: loadedRooms[0]?.id
|
|
|
- if (restoreId) await loadRoomSnapshot(restoreId, expectedGeneration)
|
|
|
+ if (restoreId) {
|
|
|
+ const restoreTopicId = restoreId === selectedRoomId.value ? selectedTopicId.value : undefined
|
|
|
+ await loadRoomSnapshot(restoreId, expectedGeneration, selectionGeneration, restoreTopicId)
|
|
|
+ }
|
|
|
if (expectedGeneration !== generation) return
|
|
|
availability.value = 'available'
|
|
|
recoveryAttempt = 0
|
|
|
@@ -222,8 +269,9 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
stop()
|
|
|
protocol.value = initialProtocol()
|
|
|
selectedRoomId.value = undefined
|
|
|
+ selectedTopicId.value = undefined
|
|
|
capabilities.value = undefined
|
|
|
- hasOlderByRoom.value = {}
|
|
|
+ hasOlderByConversation.value = {}
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -231,13 +279,24 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
roomId: string,
|
|
|
expectedGeneration = generation,
|
|
|
expectedSelection = selectionGeneration,
|
|
|
+ requestedTopicId?: string,
|
|
|
): Promise<void> {
|
|
|
const startingCursor = protocol.value.cursor
|
|
|
loadingRoomEvents.set(roomId, [])
|
|
|
let detail: GroupRoomDetail
|
|
|
let page: GroupMessagePage
|
|
|
+ let loadedTopics: GroupTopicSummary[] | undefined
|
|
|
+ let topicId: string | undefined
|
|
|
try {
|
|
|
- ;[detail, page] = await Promise.all([getGroupRoom(roomId), getGroupMessages(roomId, { limit: 100 })])
|
|
|
+ if (topicProtocol.value) {
|
|
|
+ ;[detail, loadedTopics] = await Promise.all([getGroupRoom(roomId), loadTopics(roomId)])
|
|
|
+ topicId = requestedTopicId || loadedTopics[0]?.id || createUuid()
|
|
|
+ page = loadedTopics.some(topic => topic.id === topicId)
|
|
|
+ ? await getGroupMessages(roomId, { topicId, limit: 100 })
|
|
|
+ : { items: [] }
|
|
|
+ } else {
|
|
|
+ ;[detail, page] = await Promise.all([getGroupRoom(roomId), getGroupMessages(roomId, { limit: 100 })])
|
|
|
+ }
|
|
|
} catch (cause) {
|
|
|
loadingRoomEvents.delete(roomId)
|
|
|
throw cause
|
|
|
@@ -247,17 +306,21 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
return
|
|
|
}
|
|
|
selectedRoomId.value = roomId
|
|
|
+ selectedTopicId.value = topicId
|
|
|
let merged = page.items.sort((a, b) => a.seq - b.seq)
|
|
|
// Socket events can land while the REST baseline is in flight. Replay the
|
|
|
// already-published live rows last so a stale snapshot never erases or
|
|
|
// rolls back a streaming upsert.
|
|
|
- for (const live of protocol.value.messagesByRoom[roomId] ?? []) {
|
|
|
+ const liveMessages = topicId
|
|
|
+ ? protocol.value.messagesByTopic[topicId] ?? []
|
|
|
+ : protocol.value.messagesByRoom[roomId] ?? []
|
|
|
+ for (const live of liveMessages) {
|
|
|
merged = upsertGroupMessage(merged, live)
|
|
|
}
|
|
|
const liveDetail = protocol.value.cursor > startingCursor
|
|
|
? protocol.value.roomDetails[roomId]
|
|
|
: undefined
|
|
|
- protocol.value = snapshotGroupRoom(protocol.value, liveDetail ?? detail, merged)
|
|
|
+ protocol.value = snapshotGroupRoom(protocol.value, liveDetail ?? detail, merged, { topicId, topics: loadedTopics })
|
|
|
for (const envelope of loadingRoomEvents.get(roomId) ?? []) {
|
|
|
const durableCursor = protocol.value.cursor
|
|
|
const replayBase = envelope.cursor == null
|
|
|
@@ -267,41 +330,121 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
protocol.value = { ...replayed, cursor: Math.max(durableCursor, replayed.cursor) }
|
|
|
}
|
|
|
loadingRoomEvents.delete(roomId)
|
|
|
- hasOlderByRoom.value = { ...hasOlderByRoom.value, [roomId]: page.items.length >= 100 }
|
|
|
+ const key = topicId || roomId
|
|
|
+ hasOlderByConversation.value = { ...hasOlderByConversation.value, [key]: page.items.length >= 100 }
|
|
|
}
|
|
|
|
|
|
- async function selectRoom(roomId: string): Promise<void> {
|
|
|
+ async function loadTopicSnapshot(
|
|
|
+ roomId: string,
|
|
|
+ topicId: string,
|
|
|
+ expectedGeneration = generation,
|
|
|
+ expectedSelection = selectionGeneration,
|
|
|
+ ): Promise<void> {
|
|
|
+ selectedTopicId.value = topicId
|
|
|
+ const persisted = (protocol.value.topicsByRoom[roomId] ?? []).some(topic => topic.id === topicId)
|
|
|
+ if (!persisted) {
|
|
|
+ protocol.value = {
|
|
|
+ ...protocol.value,
|
|
|
+ messagesByTopic: { ...protocol.value.messagesByTopic, [topicId]: protocol.value.messagesByTopic[topicId] ?? [] },
|
|
|
+ }
|
|
|
+ hasOlderByConversation.value = { ...hasOlderByConversation.value, [topicId]: false }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const page = await getGroupMessages(roomId, { topicId, limit: 100 })
|
|
|
+ if (expectedGeneration !== generation || expectedSelection !== selectionGeneration) return
|
|
|
+ let merged = page.items.sort((a, b) => a.seq - b.seq)
|
|
|
+ for (const live of protocol.value.messagesByTopic[topicId] ?? []) merged = upsertGroupMessage(merged, live)
|
|
|
+ protocol.value = {
|
|
|
+ ...protocol.value,
|
|
|
+ messagesByTopic: { ...protocol.value.messagesByTopic, [topicId]: merged },
|
|
|
+ }
|
|
|
+ hasOlderByConversation.value = { ...hasOlderByConversation.value, [topicId]: page.items.length >= 100 }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function selectRoom(roomId: string, requestedTopicId?: string): Promise<void> {
|
|
|
const expectedSelection = ++selectionGeneration
|
|
|
+ const previousRoomId = selectedRoomId.value
|
|
|
+ const previousTopicId = selectedTopicId.value
|
|
|
+ const sameRoom = selectedRoomId.value === roomId
|
|
|
selectedRoomId.value = roomId
|
|
|
- if (!protocol.value.roomDetails[roomId]) {
|
|
|
+ const rollback = () => {
|
|
|
+ if (expectedSelection !== selectionGeneration) return
|
|
|
+ selectedRoomId.value = previousRoomId
|
|
|
+ selectedTopicId.value = previousTopicId
|
|
|
+ }
|
|
|
+ const topicsLoaded = Object.prototype.hasOwnProperty.call(protocol.value.topicsByRoom, roomId)
|
|
|
+ if (!protocol.value.roomDetails[roomId] || (topicProtocol.value && !topicsLoaded)) {
|
|
|
isLoading.value = true
|
|
|
- try { await loadRoomSnapshot(roomId, generation, expectedSelection) }
|
|
|
- catch (cause) { error.value = errorMessage(cause); throw cause }
|
|
|
+ const desiredTopicId = requestedTopicId || (sameRoom ? selectedTopicId.value : undefined)
|
|
|
+ try { await loadRoomSnapshot(roomId, generation, expectedSelection, desiredTopicId) }
|
|
|
+ catch (cause) { rollback(); error.value = errorMessage(cause); throw cause }
|
|
|
finally { isLoading.value = false }
|
|
|
+ return
|
|
|
}
|
|
|
+ if (!topicProtocol.value) {
|
|
|
+ selectedTopicId.value = undefined
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const topicId = requestedTopicId || (sameRoom ? selectedTopicId.value : undefined)
|
|
|
+ || protocol.value.topicsByRoom[roomId]?.[0]?.id || createUuid()
|
|
|
+ isLoading.value = true
|
|
|
+ try { await loadTopicSnapshot(roomId, topicId, generation, expectedSelection) }
|
|
|
+ catch (cause) { rollback(); error.value = errorMessage(cause); throw cause }
|
|
|
+ finally { isLoading.value = false }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function selectTopic(topicId: string): Promise<void> {
|
|
|
+ const roomId = selectedRoomId.value
|
|
|
+ if (!roomId || !topicProtocol.value || topicId === selectedTopicId.value) return
|
|
|
+ const expectedSelection = ++selectionGeneration
|
|
|
+ const previousTopicId = selectedTopicId.value
|
|
|
+ isLoading.value = true
|
|
|
+ try { await loadTopicSnapshot(roomId, topicId, generation, expectedSelection) }
|
|
|
+ catch (cause) {
|
|
|
+ if (expectedSelection === selectionGeneration) selectedTopicId.value = previousTopicId
|
|
|
+ error.value = errorMessage(cause)
|
|
|
+ throw cause
|
|
|
+ }
|
|
|
+ finally { isLoading.value = false }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function startNewTopic(): Promise<string | undefined> {
|
|
|
+ if (!selectedRoomId.value || !topicProtocol.value) return undefined
|
|
|
+ const topicId = createUuid()
|
|
|
+ await selectTopic(topicId)
|
|
|
+ return topicId
|
|
|
}
|
|
|
|
|
|
async function refresh(): Promise<void> { await rebuildSnapshot() }
|
|
|
|
|
|
async function createRoom(input: { name: string; cwd?: string; agents: AgentSeed[]; maxReplyRounds?: number }): Promise<GroupRoomDetail> {
|
|
|
const detail = await createApi(input)
|
|
|
- protocol.value = snapshotGroupRoom(protocol.value, detail, [])
|
|
|
+ protocol.value = snapshotGroupRoom(protocol.value, detail, [], topicProtocol.value ? { topics: [] } : {})
|
|
|
selectedRoomId.value = detail.id
|
|
|
+ selectedTopicId.value = undefined
|
|
|
+ if (topicProtocol.value) await startNewTopic()
|
|
|
await saveCache()
|
|
|
return detail
|
|
|
}
|
|
|
|
|
|
async function updateRoom(roomId: string, input: { name?: string; cwd?: string; maxReplyRounds?: number }): Promise<GroupRoomDetail> {
|
|
|
const detail = await updateRoomApi(roomId, input)
|
|
|
- protocol.value = snapshotGroupRoom(protocol.value, detail, protocol.value.messagesByRoom[roomId] ?? [])
|
|
|
+ const topicId = topicProtocol.value && selectedRoomId.value === roomId ? selectedTopicId.value : undefined
|
|
|
+ const roomMessages = topicId ? protocol.value.messagesByTopic[topicId] ?? [] : protocol.value.messagesByRoom[roomId] ?? []
|
|
|
+ protocol.value = snapshotGroupRoom(protocol.value, detail, roomMessages, { topicId })
|
|
|
await saveCache()
|
|
|
return detail
|
|
|
}
|
|
|
|
|
|
async function archiveRoom(roomId: string): Promise<void> {
|
|
|
const detail = await archiveApi(roomId)
|
|
|
- protocol.value = snapshotGroupRoom(protocol.value, detail, protocol.value.messagesByRoom[roomId] ?? [])
|
|
|
- if (selectedRoomId.value === roomId) selectedRoomId.value = protocol.value.rooms.find(room => !room.archived && room.id !== roomId)?.id
|
|
|
+ const topicId = topicProtocol.value && selectedRoomId.value === roomId ? selectedTopicId.value : undefined
|
|
|
+ const roomMessages = topicId ? protocol.value.messagesByTopic[topicId] ?? [] : protocol.value.messagesByRoom[roomId] ?? []
|
|
|
+ protocol.value = snapshotGroupRoom(protocol.value, detail, roomMessages, { topicId })
|
|
|
+ if (selectedRoomId.value === roomId) {
|
|
|
+ selectedRoomId.value = protocol.value.rooms.find(room => !room.archived && room.id !== roomId)?.id
|
|
|
+ selectedTopicId.value = undefined
|
|
|
+ }
|
|
|
await saveCache()
|
|
|
}
|
|
|
|
|
|
@@ -318,6 +461,21 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function publishRuns(roomId: string, incoming: GroupRun[]): void {
|
|
|
+ const detail = protocol.value.roomDetails[roomId]
|
|
|
+ if (!detail || !incoming.length) return
|
|
|
+ const runs = [...detail.runs]
|
|
|
+ for (const run of incoming) {
|
|
|
+ const index = runs.findIndex(item => item.id === run.id)
|
|
|
+ if (index < 0) runs.push(run)
|
|
|
+ else if (runs[index]!.updatedAt <= run.updatedAt) runs[index] = { ...runs[index]!, ...run }
|
|
|
+ }
|
|
|
+ protocol.value = {
|
|
|
+ ...protocol.value,
|
|
|
+ roomDetails: { ...protocol.value.roomDetails, [roomId]: { ...detail, runs } },
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
async function addAgent(roomId: string, input: AgentSeed): Promise<GroupAgent> {
|
|
|
const agent = await addGroupAgent(roomId, input)
|
|
|
publishAgent(roomId, agent)
|
|
|
@@ -342,6 +500,11 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
async function sendMessage(content: string, mentionAgentIds: string[] = [], files: File[] = []): Promise<void> {
|
|
|
const roomId = selectedRoomId.value
|
|
|
if (!roomId) return
|
|
|
+ let topicId = topicProtocol.value ? selectedTopicId.value : undefined
|
|
|
+ if (topicProtocol.value && !topicId) {
|
|
|
+ topicId = createUuid()
|
|
|
+ selectedTopicId.value = topicId
|
|
|
+ }
|
|
|
const text = content.trim()
|
|
|
let uploadIds: string[] = []
|
|
|
let optimisticText = text
|
|
|
@@ -355,10 +518,12 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
const clientMessageId = createUuid()
|
|
|
const stableRequestId = createUuid()
|
|
|
const now = Date.now() / 1000
|
|
|
+ const wasNewTopic = Boolean(topicId && !(protocol.value.topicsByRoom[roomId] ?? []).some(topic => topic.id === topicId))
|
|
|
const optimistic: GroupMessage = {
|
|
|
seq: Math.max(0, ...messages.value.map(message => message.seq)) + 1,
|
|
|
id: clientMessageId,
|
|
|
roomId,
|
|
|
+ topicId: topicId || null,
|
|
|
senderKind: 'human',
|
|
|
senderId: auth.user?.id ?? 'me',
|
|
|
senderName: auth.user?.displayName ?? auth.user?.username ?? '我',
|
|
|
@@ -372,12 +537,36 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
createdAt: now,
|
|
|
updatedAt: now,
|
|
|
}
|
|
|
- protocol.value = {
|
|
|
- ...protocol.value,
|
|
|
- messagesByRoom: {
|
|
|
- ...protocol.value.messagesByRoom,
|
|
|
- [roomId]: upsertGroupMessage(protocol.value.messagesByRoom[roomId] ?? [], optimistic),
|
|
|
- },
|
|
|
+ if (topicId) {
|
|
|
+ const existing = (protocol.value.topicsByRoom[roomId] ?? []).find(topic => topic.id === topicId)
|
|
|
+ const optimisticTopic: GroupTopicSummary = {
|
|
|
+ id: topicId,
|
|
|
+ roomId,
|
|
|
+ title: existing?.title || topicTitle(text || optimisticText),
|
|
|
+ preview: optimisticText,
|
|
|
+ messageCount: (existing?.messageCount ?? 0) + 1,
|
|
|
+ createdAt: existing?.createdAt ?? now,
|
|
|
+ updatedAt: now,
|
|
|
+ }
|
|
|
+ protocol.value = {
|
|
|
+ ...protocol.value,
|
|
|
+ topicsByRoom: {
|
|
|
+ ...protocol.value.topicsByRoom,
|
|
|
+ [roomId]: upsertGroupTopic(protocol.value.topicsByRoom[roomId] ?? [], optimisticTopic),
|
|
|
+ },
|
|
|
+ messagesByTopic: {
|
|
|
+ ...protocol.value.messagesByTopic,
|
|
|
+ [topicId]: upsertGroupMessage(protocol.value.messagesByTopic[topicId] ?? [], optimistic),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ protocol.value = {
|
|
|
+ ...protocol.value,
|
|
|
+ messagesByRoom: {
|
|
|
+ ...protocol.value.messagesByRoom,
|
|
|
+ [roomId]: upsertGroupMessage(protocol.value.messagesByRoom[roomId] ?? [], optimistic),
|
|
|
+ },
|
|
|
+ }
|
|
|
}
|
|
|
isSending.value = true
|
|
|
try {
|
|
|
@@ -385,8 +574,16 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
const resolvedMentions = mentionAgentIds.includes('all')
|
|
|
? [...enabled]
|
|
|
: [...new Set(mentionAgentIds.filter(id => enabled.has(id)))]
|
|
|
- const response = await sendApi(roomId, text, resolvedMentions, clientMessageId, uploadIds, stableRequestId)
|
|
|
- protocol.value = {
|
|
|
+ const response = await sendApi(roomId, text, resolvedMentions, clientMessageId, topicId, uploadIds, stableRequestId)
|
|
|
+ publishRuns(roomId, response.runs)
|
|
|
+ const responseTopicId = response.message.topicId || topicId
|
|
|
+ protocol.value = responseTopicId ? {
|
|
|
+ ...protocol.value,
|
|
|
+ messagesByTopic: {
|
|
|
+ ...protocol.value.messagesByTopic,
|
|
|
+ [responseTopicId]: upsertGroupMessage(protocol.value.messagesByTopic[responseTopicId] ?? [], response.message),
|
|
|
+ },
|
|
|
+ } : {
|
|
|
...protocol.value,
|
|
|
messagesByRoom: {
|
|
|
...protocol.value.messagesByRoom,
|
|
|
@@ -401,7 +598,14 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
error: explicitRejection ? errorMessage(cause) : '送达状态未知;刷新房间后再决定是否重试',
|
|
|
updatedAt: Date.now() / 1000,
|
|
|
}
|
|
|
- protocol.value = {
|
|
|
+ protocol.value = topicId ? {
|
|
|
+ ...protocol.value,
|
|
|
+ topicsByRoom: explicitRejection && wasNewTopic ? {
|
|
|
+ ...protocol.value.topicsByRoom,
|
|
|
+ [roomId]: (protocol.value.topicsByRoom[roomId] ?? []).filter(topic => topic.id !== topicId),
|
|
|
+ } : protocol.value.topicsByRoom,
|
|
|
+ messagesByTopic: { ...protocol.value.messagesByTopic, [topicId]: upsertGroupMessage(protocol.value.messagesByTopic[topicId] ?? [], failed) },
|
|
|
+ } : {
|
|
|
...protocol.value,
|
|
|
messagesByRoom: { ...protocol.value.messagesByRoom, [roomId]: upsertGroupMessage(protocol.value.messagesByRoom[roomId] ?? [], failed) },
|
|
|
}
|
|
|
@@ -413,11 +617,15 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
const roomId = selectedRoomId.value
|
|
|
const oldest = messages.value.find(message => message.seq > 0)?.seq
|
|
|
if (!roomId || !oldest) return
|
|
|
- const page = await getGroupMessages(roomId, { beforeSeq: oldest, limit: 100 })
|
|
|
- let merged = protocol.value.messagesByRoom[roomId] ?? []
|
|
|
+ const topicId = topicProtocol.value ? selectedTopicId.value : undefined
|
|
|
+ const page = await getGroupMessages(roomId, { topicId, beforeSeq: oldest, limit: 100 })
|
|
|
+ let merged = topicId ? protocol.value.messagesByTopic[topicId] ?? [] : protocol.value.messagesByRoom[roomId] ?? []
|
|
|
for (const message of page.items) merged = upsertGroupMessage(merged, message)
|
|
|
- protocol.value = { ...protocol.value, messagesByRoom: { ...protocol.value.messagesByRoom, [roomId]: merged } }
|
|
|
- hasOlderByRoom.value = { ...hasOlderByRoom.value, [roomId]: page.items.length >= 100 }
|
|
|
+ protocol.value = topicId
|
|
|
+ ? { ...protocol.value, messagesByTopic: { ...protocol.value.messagesByTopic, [topicId]: merged } }
|
|
|
+ : { ...protocol.value, messagesByRoom: { ...protocol.value.messagesByRoom, [roomId]: merged } }
|
|
|
+ const key = topicId || roomId
|
|
|
+ hasOlderByConversation.value = { ...hasOlderByConversation.value, [key]: page.items.length >= 100 }
|
|
|
}
|
|
|
|
|
|
async function interruptAgent(agentId: string, roomId = selectedRoomId.value): Promise<void> {
|
|
|
@@ -446,9 +654,10 @@ export const useGroupsStore = defineStore('groups', () => {
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
- availability, capabilities, rooms, selectedRoomId, selectedRoom, messages, agents, pendingInteractions,
|
|
|
+ availability, capabilities, topicProtocol, rooms, selectedRoomId, selectedRoom, topics, topicsForRoom, loadRoomTopics, selectedTopicId, selectedTopic,
|
|
|
+ messages, agents, pendingInteractions,
|
|
|
connectionState, isLoading, isSending, error, hasMoreBefore,
|
|
|
- start, stop, refresh, selectRoom, createRoom, updateRoom, archiveRoom, addAgent, updateAgent,
|
|
|
+ start, stop, refresh, selectRoom, selectTopic, startNewTopic, createRoom, updateRoom, archiveRoom, addAgent, updateAgent,
|
|
|
removeAgent, sendMessage, loadOlder, interruptAgent, approveInteraction, clarifyInteraction,
|
|
|
}
|
|
|
})
|