fix: preserve channel message delivery

This commit is contained in:
lofyer
2026-08-13 02:09:09 +08:00
parent 67cb69f07d
commit 980f3a0c8f
15 changed files with 586 additions and 130 deletions
+64 -9
View File
@@ -98,7 +98,7 @@ describe('AssistantDatabase', () => {
database.close()
})
it('migrates existing databases to schema version 18', async () => {
it('migrates existing databases to schema version 19', async () => {
const directory = await mkdtemp(
join(tmpdir(), 'goodbuddy-assistant-migration-')
)
@@ -127,7 +127,7 @@ describe('AssistantDatabase', () => {
user_version: number
}
).user_version
).toBe(18)
).toBe(19)
expect(
current
.prepare(
@@ -231,7 +231,7 @@ describe('AssistantDatabase', () => {
user_version: number
}
).user_version
).toBe(18)
).toBe(19)
expect(
current
.prepare(
@@ -630,9 +630,18 @@ describe('AssistantDatabase', () => {
const databasePath = join(directory, 'assistant.sqlite')
const database = new AssistantDatabase(databasePath)
database.initialize('C:\\Workspace')
expect(database.claimChannelEvent('weixin', 'event-1')).toBe(true)
expect(database.claimChannelEvent('weixin', 'event-1')).toBe(false)
expect(database.claimChannelEvent('dingtalk', 'event-1')).toBe(true)
expect(
database.claimChannelEvent('weixin', 'account-1', 'event-1')
).toBe(true)
expect(
database.claimChannelEvent('weixin', 'account-1', 'event-1')
).toBe(false)
expect(
database.claimChannelEvent('weixin', 'account-2', 'event-1')
).toBe(true)
expect(
database.claimChannelEvent('dingtalk', 'account-1', 'event-1')
).toBe(true)
const entry = database.enqueueChannelResult({
channel: 'weixin',
@@ -656,12 +665,58 @@ describe('AssistantDatabase', () => {
const reopened = new AssistantDatabase(databasePath)
reopened.initialize('C:\\Workspace')
expect(reopened.claimChannelEvent('weixin', 'event-1')).toBe(
false
)
expect(
reopened.claimChannelEvent('weixin', 'account-1', 'event-1')
).toBe(false)
reopened.close()
})
it('preserves legacy channel event claims while adding account identity', async () => {
const directory = await mkdtemp(
join(tmpdir(), 'goodbuddy-channel-event-migration-')
)
temporaryDirectories.push(directory)
const databasePath = join(directory, 'assistant.sqlite')
const initial = new AssistantDatabase(databasePath)
initial.initialize('C:\\Workspace')
initial.close()
const legacy = new DatabaseSync(databasePath)
legacy.exec(`
DROP TABLE channel_events;
CREATE TABLE channel_events (
channel TEXT NOT NULL,
event_id TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
PRIMARY KEY(channel, event_id)
);
CREATE INDEX channel_events_claimed_at
ON channel_events(claimed_at);
INSERT INTO channel_events(channel, event_id, claimed_at)
VALUES ('weixin', 'legacy-event', 1);
PRAGMA user_version = 18;
`)
legacy.close()
const migrated = new AssistantDatabase(databasePath)
migrated.initialize('C:\\Workspace')
expect(
migrated.claimChannelEvent(
'weixin',
'default',
'legacy-event'
)
).toBe(false)
expect(
migrated.claimChannelEvent(
'weixin',
'new-account',
'legacy-event'
)
).toBe(true)
migrated.close()
})
it('safely deletes a confirmed project and its scoped data', async () => {
const database = await createDatabase()
const project = database.createProject({
+56 -10
View File
@@ -1636,15 +1636,19 @@ export class AssistantDatabase {
}
}
claimChannelEvent(channel: string, eventId: string): boolean {
claimChannelEvent(
channel: string,
accountId: string,
eventId: string
): boolean {
const database = this.requireDatabase()
const result = database
.prepare(
`INSERT OR IGNORE INTO channel_events
(channel, event_id, claimed_at)
VALUES (?, ?, ?)`
(channel, account_id, event_id, claimed_at)
VALUES (?, ?, ?, ?)`
)
.run(channel, eventId, Date.now())
.run(channel, accountId, eventId, Date.now())
if (result.changes === 1) {
this.channelEventWrites += 1
if (this.channelEventWrites % 128 === 0) {
@@ -1664,12 +1668,17 @@ export class AssistantDatabase {
return result.changes === 1
}
releaseChannelEvent(channel: string, eventId: string): void {
releaseChannelEvent(
channel: string,
accountId: string,
eventId: string
): void {
this.requireDatabase()
.prepare(
'DELETE FROM channel_events WHERE channel = ? AND event_id = ?'
`DELETE FROM channel_events
WHERE channel = ? AND account_id = ? AND event_id = ?`
)
.run(channel, eventId)
.run(channel, accountId, eventId)
}
enqueueChannelResult(message: ChannelResultMessage): {
@@ -4475,12 +4484,12 @@ export class AssistantDatabase {
const version = database
.prepare('PRAGMA user_version')
.get() as { user_version: number }
if (version.user_version > 18) {
if (version.user_version > 19) {
throw new Error(
` GoodBuddy ${version.user_version}`
)
}
if (version.user_version === 18) {
if (version.user_version === 19) {
return
}
if (version.user_version < 1) {
@@ -5162,9 +5171,10 @@ export class AssistantDatabase {
database.exec(`
CREATE TABLE IF NOT EXISTS channel_events (
channel TEXT NOT NULL,
account_id TEXT NOT NULL DEFAULT 'default',
event_id TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
PRIMARY KEY(channel, event_id)
PRIMARY KEY(channel, account_id, event_id)
);
CREATE INDEX IF NOT EXISTS channel_events_claimed_at
ON channel_events(claimed_at);
@@ -5353,6 +5363,42 @@ export class AssistantDatabase {
throw error
}
}
if (version.user_version < 19) {
database.exec('BEGIN IMMEDIATE')
try {
const eventColumns = new Set(
(
database
.prepare('PRAGMA table_info(channel_events)')
.all() as Array<{ name: string }>
).map((column) => column.name)
)
if (!eventColumns.has('account_id')) {
database.exec(`
ALTER TABLE channel_events RENAME TO channel_events_legacy;
DROP INDEX IF EXISTS channel_events_claimed_at;
CREATE TABLE channel_events (
channel TEXT NOT NULL,
account_id TEXT NOT NULL DEFAULT 'default',
event_id TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
PRIMARY KEY(channel, account_id, event_id)
);
INSERT INTO channel_events
(channel, account_id, event_id, claimed_at)
SELECT channel, 'default', event_id, claimed_at
FROM channel_events_legacy;
DROP TABLE channel_events_legacy;
CREATE INDEX channel_events_claimed_at
ON channel_events(claimed_at);
`)
}
database.exec('PRAGMA user_version = 19; COMMIT;')
} catch (error) {
database.exec('ROLLBACK')
throw error
}
}
}
private requireDatabase(): DatabaseSync {
@@ -100,7 +100,7 @@ describe('AssistantDatabase heartbeat persistence', () => {
).count
check.close()
migrated.close()
expect(version).toBe(18)
expect(version).toBe(19)
expect(heartbeatTableCount).toBe(3)
})