👷 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] saved_at "timestamp with time zone" [not null]
indexes { indexes {
(document_id, saved_at) [name: 'document_histories_document_id_saved_at_idx'] document_id [name: 'document_histories_document_id_idx']
(user_id, saved_at) [name: 'document_histories_user_id_saved_at_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'] status text [not null, default: 'backlog']
priority integer [default: 0] priority integer [default: 0]
sort_order integer [default: 0] sort_order integer [default: 0]
heartbeat_interval integer [default: 300] automation_mode text
heartbeat_interval integer
heartbeat_timeout integer heartbeat_timeout integer
last_heartbeat_at "timestamp with time zone" last_heartbeat_at "timestamp with time zone"
schedule_pattern text schedule_pattern text
@ -1550,6 +1552,7 @@ table tasks {
parent_task_id [name: 'tasks_parent_task_id_idx'] parent_task_id [name: 'tasks_parent_task_id_idx']
status [name: 'tasks_status_idx'] status [name: 'tasks_status_idx']
priority [name: 'tasks_priority_idx'] priority [name: 'tasks_priority_idx']
automation_mode [name: 'tasks_automation_mode_idx']
(status, last_heartbeat_at) [name: 'tasks_heartbeat_idx'] (status, last_heartbeat_at) [name: 'tasks_heartbeat_idx']
} }
} }
@ -1631,6 +1634,8 @@ table topics {
metadata jsonb metadata jsonb
trigger text trigger text
mode text mode text
status text
completed_at "timestamp with time zone"
accessed_at "timestamp with time zone" [not null, default: `now()`] accessed_at "timestamp with time zone" [not null, default: `now()`]
created_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()`] updated_at "timestamp with time zone" [not null, default: `now()`]
@ -1643,6 +1648,8 @@ table topics {
group_id [name: 'topics_group_id_idx'] group_id [name: 'topics_group_id_idx']
agent_id [name: 'topics_agent_id_idx'] agent_id [name: 'topics_agent_id_idx']
trigger [name: 'topics_trigger_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'] () [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, "when": 1776234919716,
"tag": "0098_add_document_history", "tag": "0098_add_document_history",
"breakpoints": true "breakpoints": true
},
{
"idx": 99,
"version": "7",
"when": 1776674965365,
"tag": "0099_topic_status_tasks_automation_mode",
"breakpoints": true
} }
], ],
"version": "6" "version": "6"

View file

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

View file

@ -58,8 +58,11 @@ export const tasks = pgTable(
priority: integer('priority').default(0), // 'no' | 'urgent' | 'high' | 'normal' | 'low' priority: integer('priority').default(0), // 'no' | 'urgent' | 'high' | 'normal' | 'low'
sortOrder: integer('sort_order').default(0), // manual sort within parent, lower = higher 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 // 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) heartbeatTimeout: integer('heartbeat_timeout'), // seconds, null = disabled (default off)
lastHeartbeatAt: timestamptz('last_heartbeat_at'), lastHeartbeatAt: timestamptz('last_heartbeat_at'),
@ -97,6 +100,7 @@ export const tasks = pgTable(
index('tasks_parent_task_id_idx').on(t.parentTaskId), index('tasks_parent_task_id_idx').on(t.parentTaskId),
index('tasks_status_idx').on(t.status), index('tasks_status_idx').on(t.status),
index('tasks_priority_idx').on(t.priority), index('tasks_priority_idx').on(t.priority),
index('tasks_automation_mode_idx').on(t.automationMode),
index('tasks_heartbeat_idx').on(t.status, t.lastHeartbeatAt), 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>(), metadata: jsonb('metadata').$type<ChatTopicMetadata | undefined>(),
trigger: text('trigger'), // 'cron' | 'chat' | 'api' | 'eval' - topic creation trigger source trigger: text('trigger'), // 'cron' | 'chat' | 'api' | 'eval' - topic creation trigger source
mode: text('mode'), // 'temp' | 'test' | 'default' - topic usage scenario mode: text('mode'), // 'temp' | 'test' | 'default' - topic usage scenario
status: text('status', { enum: ['active', 'completed', 'archived'] }),
completedAt: timestamptz('completed_at'),
...timestamps, ...timestamps,
}, },
(t) => [ (t) => [
@ -52,6 +54,8 @@ export const topics = pgTable(
index('topics_group_id_idx').on(t.groupId), index('topics_group_id_idx').on(t.groupId),
index('topics_agent_id_idx').on(t.agentId), index('topics_agent_id_idx').on(t.agentId),
index('topics_trigger_idx').on(t.trigger), 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( index('topics_extract_status_gin_idx').using(
'gin', 'gin',
sql`(metadata->'userMemoryExtractStatus') jsonb_path_ops`, sql`(metadata->'userMemoryExtractStatus') jsonb_path_ops`,

View file

@ -44,22 +44,35 @@ afterEach(() => {
vi.doUnmock('@/routes/onboarding/config'); 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', () => { describe('ModeSwitch', () => {
it('renders both onboarding variants when agent onboarding is enabled', async () => { it(
await renderModeSwitch({ enabled: true, showLabel: true }); '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.getByText('Choose your onboarding mode')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Conversational' })).toBeChecked(); expect(screen.getByRole('radio', { name: 'Conversational' })).toBeChecked();
expect(screen.getByRole('radio', { name: 'Classic' })).not.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(
await renderModeSwitch({ enabled: false }); '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: 'Conversational' })).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'Classic' })).not.toBeInTheDocument(); expect(screen.queryByRole('radio', { name: 'Classic' })).not.toBeInTheDocument();
expect(screen.queryByText('Choose your onboarding mode')).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 () => { it('keeps action buttons visible when agent onboarding is disabled', async () => {
await renderModeSwitch({ await renderModeSwitch({