mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(import): preserve Discord DM channel identity
Retain the channel ID for stable matching of short and non-overlapping private exports. Add a real SQLite regression test covering repeated import deduplication and incremental append.
This commit is contained in:
1 parent
2322f7ea48
commit
d1e3cbde47
3 files changed
+67
-2
No files matched your search
@@ -4,6 +4,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { CHAT_DB_SCHEMA } from '@openchatlab/core'
|
||||
import { parseFileSync } from '@openchatlab/parser'
|
||||
import { MessageType, type ParsedMessage } from '@openchatlab/shared-types'
|
||||
import { openBetterSqliteDatabase } from '../better-sqlite3-adapter'
|
||||
import { resolveAutoImportTarget, resolveAutoImportTargetPlan, type AutoImportMatcherDeps } from './auto-import-matcher'
|
||||
@@ -186,6 +187,69 @@ test('matches a unique group session by stable group id', async (t) => {
|
||||
)
|
||||
})
|
||||
|
||||
test('reimports short Discord DMs and appends non-overlapping exports to the same session', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
const sourcePath = path.join(tempDir, 'discord.json')
|
||||
const writeExport = (id: number) =>
|
||||
fs.writeFileSync(
|
||||
sourcePath,
|
||||
JSON.stringify({
|
||||
guild: { id: '0', name: 'Direct Messages' },
|
||||
channel: { id: 'dm-channel-1', type: 'DirectTextChat', name: 'Alice' },
|
||||
messages: [
|
||||
{
|
||||
id: String(id),
|
||||
type: 'Default',
|
||||
timestamp: `2026-01-01T00:00:0${id}Z`,
|
||||
content: `Message ${id}`,
|
||||
author: { id: 'alice', name: 'Alice', discriminator: '0000', isBot: false, roles: [] },
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
stickers: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
writeExport(1)
|
||||
const parsed = await parseFileSync(sourcePath)
|
||||
const dbPath = path.join(tempDir, 'existing.db')
|
||||
seedSession(dbPath, parsed.meta, parsed.members, parsed.messages)
|
||||
|
||||
for (const id of [1, 2]) {
|
||||
writeExport(id)
|
||||
const result = await autoImportFile(sourcePath, {
|
||||
...createDeps(tempDir, ['existing']),
|
||||
sessionExists: (sessionId) => sessionId === 'existing',
|
||||
createSession: async () => {
|
||||
throw new Error('must not create a duplicate Discord DM')
|
||||
},
|
||||
appendSession: (sessionId, filePath) =>
|
||||
incrementalImport(sessionId, filePath, {
|
||||
openDatabase: (_sessionId, readonly = false) => openBetterSqliteDatabase(dbPath, { readonly, nativeBinding }),
|
||||
onProgress: () => {},
|
||||
}),
|
||||
})
|
||||
assert.equal(result.success, true)
|
||||
assert.equal(result.sessionId, 'existing')
|
||||
assert.equal(result.importMode, 'incremental')
|
||||
assert.equal(result.matchedBy, 'stable-id')
|
||||
assert.equal(result.newMessageCount, id === 1 ? 0 : 1)
|
||||
assert.equal(result.duplicateCount, id === 1 ? 1 : 0)
|
||||
}
|
||||
const db = openBetterSqliteDatabase(dbPath, { readonly: true, nativeBinding })
|
||||
try {
|
||||
assert.deepEqual(db.prepare('SELECT platform_message_id FROM message ORDER BY ts').all(), [
|
||||
{ platform_message_id: '1' },
|
||||
{ platform_message_id: '2' },
|
||||
])
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('matches a unique private session by stable owner and participant ids', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
|
||||
@@ -48,7 +48,7 @@ for (const { channelType, guildName, expectedType } of cases) {
|
||||
assert.equal(result.meta.platform, 'discord')
|
||||
const isPrivate = expectedType === ChatType.PRIVATE
|
||||
assert.equal(result.meta.name, isPrivate ? 'Alice' : `${guildName} - Alice`)
|
||||
assert.equal(result.meta.groupId, isPrivate ? undefined : 'channel-1')
|
||||
assert.equal(result.meta.groupId, 'channel-1')
|
||||
assert.equal(result.meta.groupAvatar, isPrivate ? undefined : 'https://example.com/icon.png')
|
||||
assert.deepEqual(
|
||||
result.members.map((member) => member.platformId),
|
||||
|
||||
@@ -307,7 +307,8 @@ async function* parseDiscordExporter(options: ParseOptions): AsyncGenerator<Pars
|
||||
name: chatName,
|
||||
platform: KNOWN_PLATFORMS.DISCORD,
|
||||
type: isPrivate ? ChatType.PRIVATE : ChatType.GROUP,
|
||||
groupId: isPrivate ? undefined : channel?.id,
|
||||
// Keep the channel ID for stable auto-import matching, including direct messages.
|
||||
groupId: channel?.id,
|
||||
groupAvatar: isPrivate ? undefined : guild?.iconUrl,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user