Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) {

if (
resolved.constraint.manageMode === 'admin' &&
resolved.constraint.routeMode !== 'personal' &&
!(await checkAdmin(session))
) {
context.message = session.text(
Expand Down
23 changes: 21 additions & 2 deletions packages/core/src/middlewares/system/wipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createLogger } from 'koishi-plugin-chatluna/utils/logger'
import fs from 'fs/promises'
import {
createLegacyTableRetention,
createPassedValidationResult,
dropTableIfExists,
getLegacySchemaSentinel,
getLegacySchemaSentinelDir,
Expand All @@ -15,6 +16,7 @@ import {
readMetaValue,
writeMetaValue
} from '../../migration/validators'
import { BUILTIN_SCHEMA_VERSION } from '../../migration/room_to_conversation'

let logger: Logger

Expand Down Expand Up @@ -76,12 +78,13 @@ export function apply(ctx: Context, _config: Config, chain: ChatChain) {
'chatluna_binding',
'chatluna_constraint',
'chatluna_archive',
'chatluna_acl',
'chatluna_meta'
'chatluna_acl'
]) {
await dropTableIfExists(ctx, table)
}

await ctx.database.remove('chatluna_meta', {})

for (const table of LEGACY_MIGRATION_TABLES) {
await dropTableIfExists(ctx, table)
}
Expand Down Expand Up @@ -115,6 +118,22 @@ export function apply(ctx: Context, _config: Config, chain: ChatChain) {
logger.warn(`wipe: ${e}`)
}

const validated = createPassedValidationResult()
await writeMetaValue(ctx, 'schema_version', BUILTIN_SCHEMA_VERSION)
await writeMetaValue(ctx, 'validation_result', validated)
await writeMetaValue(ctx, 'room_migration_done', true)
await writeMetaValue(ctx, 'message_migration_done', true)
await writeMetaValue(
ctx,
LEGACY_RETENTION_META_KEY,
createLegacyTableRetention('purged')
)
await writeMetaValue(
ctx,
'migration_finished_at',
new Date().toISOString()
)

const sentinelDir = getLegacySchemaSentinelDir(ctx.baseDir)
const sentinelPath = getLegacySchemaSentinel(ctx.baseDir)
await fs.mkdir(sentinelDir, { recursive: true })
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/migration/legacy_tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export function defineLegacyMigrationTables(ctx: Context) {
function isMissingTableError(error: unknown) {
const message = String(error).toLowerCase()
return (
message.includes('cannot resolve table') ||
message.includes('no such table') ||
message.includes('unknown table') ||
message.includes('not found') ||
Expand Down
78 changes: 76 additions & 2 deletions packages/core/src/migration/room_to_conversation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { existsSync } from 'fs'
import { createHash, randomUUID } from 'crypto'
import type { Context } from 'koishi'
import type { Config } from '../config'
Expand All @@ -21,7 +22,9 @@ import {
aclKey,
createLegacyBindingKey,
createLegacyTableRetention,
createPassedValidationResult,
filterValidRooms,
getLegacySchemaSentinel,
inferLegacyGroupRouteModes,
isComplexRoom,
LEGACY_RETENTION_META_KEY,
Expand All @@ -43,6 +46,10 @@ export async function runRoomToConversationMigration(
ctx: Context,
config: Config
) {
if (existsSync(getLegacySchemaSentinel(ctx.baseDir))) {
return ensureMigrationValidated(ctx, config)
Comment on lines +49 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid skipping migration on sentinel alone

This early return makes the sentinel file authoritative even when legacy chathub_* tables still contain data, so a stale sentinel (for example after restoring an old DB while keeping data/chatluna/temp/legacy-schema-disabled.json) will bypass migration entirely. In that case ensureMigrationValidated() marks validation as passed and writes migration-done metadata without reading legacy rows, leaving legacy conversations/messages unmigrated and effectively lost to ChatLuna.

Useful? React with 👍 / 👎.

}

defineLegacyMigrationTables(ctx)

const result = await readMetaValue<
Expand Down Expand Up @@ -88,6 +95,9 @@ export async function runRoomToConversationMigration(
throw new Error('ChatLuna migration validation failed.')
}

// Mark completion before purge so a restart after writing the sentinel
// can resume from the finished state instead of re-reading legacy tables.
await writeMigrationDone(ctx)
await purgeLegacyTables(ctx)
await writeMigrationFinished(ctx)

Expand All @@ -106,8 +116,68 @@ export async function ensureMigrationValidated(ctx: Context, config: Config) {
const retention = await readMetaValue<{
state?: string
}>(ctx, LEGACY_RETENTION_META_KEY)
const hasSentinel = existsSync(getLegacySchemaSentinel(ctx.baseDir))

if (hasSentinel) {
if (result?.passed === true) {
await writeMetaValue(ctx, 'schema_version', BUILTIN_SCHEMA_VERSION)
await writeMigrationDone(ctx)
await writeMigrationFinished(ctx)
return result
}

const conversations = (await ctx.database.get(
'chatluna_conversation',
{},
{
limit: 1
}
)) as ConversationRecord[]
const messages = (await ctx.database.get(
'chatluna_message',
{},
{
limit: 1
}
)) as MessageRecord[]
const bindings = (await ctx.database.get(
'chatluna_binding',
{},
{
limit: 1
}
)) as BindingRecord[]
const acl = (await ctx.database.get(
'chatluna_acl',
{},
{
limit: 1
}
)) as ACLRecord[]
const hasData =
conversations.length > 0 ||
messages.length > 0 ||
bindings.length > 0 ||
acl.length > 0

ctx.logger.warn(
hasData
? 'Legacy sentinel exists and ChatLuna data is present; adopting current ChatLuna state and marking migration finished.'
: 'Legacy sentinel exists and ChatLuna data is empty; treating startup as fresh install.'
)

const validated = createPassedValidationResult()
await writeMetaValue(ctx, 'schema_version', BUILTIN_SCHEMA_VERSION)
await writeMetaValue(ctx, 'validation_result', validated)
await writeMigrationDone(ctx)
await writeMigrationFinished(ctx)
return validated
}

if (result?.passed === true) {
await writeMetaValue(ctx, 'schema_version', BUILTIN_SCHEMA_VERSION)
await writeMigrationDone(ctx)

if (retention?.state !== 'purged') {
await purgeLegacyTables(ctx)
}
Expand Down Expand Up @@ -145,21 +215,25 @@ export async function ensureMigrationValidated(ctx: Context, config: Config) {
throw new Error('ChatLuna migration validation failed.')
}

await writeMigrationDone(ctx)
await purgeLegacyTables(ctx)
await writeMigrationFinished(ctx)

return validated
}

async function writeMigrationFinished(ctx: Context) {
await writeMetaValue(ctx, 'migration_finished_at', new Date().toISOString())
async function writeMigrationDone(ctx: Context) {
await writeMetaValue(ctx, 'room_migration_done', true)
await writeMetaValue(ctx, 'message_migration_done', true)
}

async function writeMigrationFinished(ctx: Context) {
await writeMetaValue(
ctx,
LEGACY_RETENTION_META_KEY,
createLegacyTableRetention('purged')
)
await writeMetaValue(ctx, 'migration_finished_at', new Date().toISOString())
}

// (#6) removed redundant inner `done` check — the caller already guards on room_migration_done
Expand Down
38 changes: 37 additions & 1 deletion packages/core/src/migration/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import type {
import type { MigrationValidationResult } from './types'
export type { MigrationValidationResult } from './types'
export {
getLegacySchemaSentinel,
dropTableIfExists,
getLegacySchemaSentinelDir,
getLegacySchemaSentinel,
LEGACY_MIGRATION_TABLES,
LEGACY_RETENTION_META_KEY,
LEGACY_RUNTIME_TABLES,
Expand All @@ -30,6 +30,42 @@ export {

const VALIDATION_BATCH_SIZE = 500

export function createPassedValidationResult(): MigrationValidationResult {
return {
passed: true,
checkedAt: new Date().toISOString(),
conversation: {
legacy: 0,
migrated: 0,
matched: true
},
message: {
legacy: 0,
migrated: 0,
matched: true
},
latestMessageId: {
missingConversationIds: [],
matched: true
},
bindingKey: {
inconsistentConversationIds: [],
matched: true
},
binding: {
missingBindingKeys: [],
missingConversationIds: [],
matched: true
},
acl: {
expected: 0,
migrated: 0,
missing: [],
matched: true
}
}
}

export async function validateRoomMigration(ctx: Context, _config: Config) {
ctx.logger.info('Validating built-in ChatLuna migration.')

Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/services/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,6 @@ export class ConversationService {
}
}

await assertManageAllowed(session, resolved.constraint)

if (!resolved.constraint.allowNew) {
throw new Error('Conversation creation is disabled by constraint.')
}
Expand Down
Loading
Loading