👷 build(database): add topic status and tasks automation mode (#13994)

This commit is contained in:
Arvin Xu 2026-04-20 17:34:13 +08:00 committed by GitHub
parent 93603ae83b
commit 3bcd581e7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 14884 additions and 16 deletions

View file

@ -516,8 +516,9 @@ table document_histories {
saved_at "timestamp with time zone" [not null]
indexes {
(document_id, saved_at) [name: 'document_histories_document_id_saved_at_idx']
(user_id, saved_at) [name: 'document_histories_user_id_saved_at_idx']
document_id [name: 'document_histories_document_id_idx']
user_id [name: 'document_histories_user_id_idx']
saved_at [name: 'document_histories_saved_at_idx']
}
}
@ -1523,7 +1524,8 @@ table tasks {
status text [not null, default: 'backlog']
priority integer [default: 0]
sort_order integer [default: 0]
heartbeat_interval integer [default: 300]
automation_mode text
heartbeat_interval integer
heartbeat_timeout integer
last_heartbeat_at "timestamp with time zone"
schedule_pattern text
@ -1550,6 +1552,7 @@ table tasks {
parent_task_id [name: 'tasks_parent_task_id_idx']
status [name: 'tasks_status_idx']
priority [name: 'tasks_priority_idx']
automation_mode [name: 'tasks_automation_mode_idx']
(status, last_heartbeat_at) [name: 'tasks_heartbeat_idx']
}
}
@ -1631,6 +1634,8 @@ table topics {
metadata jsonb
trigger text
mode text
status text
completed_at "timestamp with time zone"
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
@ -1643,6 +1648,8 @@ table topics {
group_id [name: 'topics_group_id_idx']
agent_id [name: 'topics_agent_id_idx']
trigger [name: 'topics_trigger_idx']
status [name: 'topics_status_idx']
(user_id, completed_at) [name: 'topics_user_id_completed_at_idx']
() [name: 'topics_extract_status_gin_idx']
}
}

View file

@ -0,0 +1,7 @@
ALTER TABLE "tasks" ALTER COLUMN "heartbeat_interval" DROP DEFAULT;--> statement-breakpoint
ALTER TABLE "tasks" ADD COLUMN IF NOT EXISTS "automation_mode" text;--> statement-breakpoint
ALTER TABLE "topics" ADD COLUMN IF NOT EXISTS "status" text;--> statement-breakpoint
ALTER TABLE "topics" ADD COLUMN IF NOT EXISTS "completed_at" timestamp with time zone;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_automation_mode_idx" ON "tasks" USING btree ("automation_mode");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "topics_status_idx" ON "topics" USING btree ("status");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "topics_user_id_completed_at_idx" ON "topics" USING btree ("user_id","completed_at");

File diff suppressed because it is too large Load diff

View file

@ -693,6 +693,13 @@
"when": 1776234919716,
"tag": "0098_add_document_history",
"breakpoints": true
},
{
"idx": 99,
"version": "7",
"when": 1776674965365,
"tag": "0099_topic_status_tasks_automation_mode",
"breakpoints": true
}
],
"version": "6"

View file

@ -61,6 +61,8 @@ describe('TopicModel - Create', () => {
editorData: null,
trigger: null,
mode: null,
status: null,
completedAt: null,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
accessedAt: expect.any(Date),
@ -109,6 +111,8 @@ describe('TopicModel - Create', () => {
metadata: null,
trigger: null,
mode: null,
status: null,
completedAt: null,
sessionId,
userId,
createdAt: expect.any(Date),

View file

@ -58,8 +58,11 @@ export const tasks = pgTable(
priority: integer('priority').default(0), // 'no' | 'urgent' | 'high' | 'normal' | 'low'
sortOrder: integer('sort_order').default(0), // manual sort within parent, lower = higher
// Automation mode (mutually exclusive with each other; null = no automation)
automationMode: text('automation_mode').$type<'heartbeat' | 'schedule'>(),
// Heartbeat
heartbeatInterval: integer('heartbeat_interval').default(300), // seconds
heartbeatInterval: integer('heartbeat_interval'), // seconds, null = no heartbeat configured
heartbeatTimeout: integer('heartbeat_timeout'), // seconds, null = disabled (default off)
lastHeartbeatAt: timestamptz('last_heartbeat_at'),
@ -97,6 +100,7 @@ export const tasks = pgTable(
index('tasks_parent_task_id_idx').on(t.parentTaskId),
index('tasks_status_idx').on(t.status),
index('tasks_priority_idx').on(t.priority),
index('tasks_automation_mode_idx').on(t.automationMode),
index('tasks_heartbeat_idx').on(t.status, t.lastHeartbeatAt),
],
);

View file

@ -42,6 +42,8 @@ export const topics = pgTable(
metadata: jsonb('metadata').$type<ChatTopicMetadata | undefined>(),
trigger: text('trigger'), // 'cron' | 'chat' | 'api' | 'eval' - topic creation trigger source
mode: text('mode'), // 'temp' | 'test' | 'default' - topic usage scenario
status: text('status', { enum: ['active', 'completed', 'archived'] }),
completedAt: timestamptz('completed_at'),
...timestamps,
},
(t) => [
@ -52,6 +54,8 @@ export const topics = pgTable(
index('topics_group_id_idx').on(t.groupId),
index('topics_agent_id_idx').on(t.agentId),
index('topics_trigger_idx').on(t.trigger),
index('topics_status_idx').on(t.status),
index('topics_user_id_completed_at_idx').on(t.userId, t.completedAt),
index('topics_extract_status_gin_idx').using(
'gin',
sql`(metadata->'userMemoryExtractStatus') jsonb_path_ops`,

View file

@ -44,22 +44,35 @@ afterEach(() => {
vi.doUnmock('@/routes/onboarding/config');
});
// Each test does vi.resetModules() + dynamic import of the component, which
// re-parses antd + @lobehub/ui fresh. On cold CI runs this can blow past the
// default 5s timeout even though the test is doing nothing slow itself.
const TEST_TIMEOUT_MS = 15_000;
describe('ModeSwitch', () => {
it('renders both onboarding variants when agent onboarding is enabled', async () => {
it(
'renders both onboarding variants when agent onboarding is enabled',
async () => {
await renderModeSwitch({ enabled: true, showLabel: true });
expect(screen.getByText('Choose your onboarding mode')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Conversational' })).toBeChecked();
expect(screen.getByRole('radio', { name: 'Classic' })).not.toBeChecked();
});
},
TEST_TIMEOUT_MS,
);
it('hides the onboarding switch entirely when agent onboarding is disabled', async () => {
it(
'hides the onboarding switch entirely when agent onboarding is disabled',
async () => {
await renderModeSwitch({ enabled: false });
expect(screen.queryByRole('radio', { name: 'Conversational' })).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'Classic' })).not.toBeInTheDocument();
expect(screen.queryByText('Choose your onboarding mode')).not.toBeInTheDocument();
});
},
TEST_TIMEOUT_MS,
);
it('keeps action buttons visible when agent onboarding is disabled', async () => {
await renderModeSwitch({