feat(backend): update database schema and agent service

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-11 08:47:53 +08:00
parent 3e39b40a50
commit ac49c13965
3 changed files with 337 additions and 3 deletions

View File

@@ -35,6 +35,7 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await ensure_task_columns(conn)
await ensure_log_columns(conn)
await ensure_message_columns(conn)
await ensure_conversation_columns(conn)
@@ -47,6 +48,195 @@ async def init_db():
await ensure_learning_artifact_tables(conn)
async def ensure_task_columns(conn):
rows = await _get_table_info(conn, 'tasks')
if not rows:
return
columns = {row[1] for row in rows}
required_columns = {
'source': "ALTER TABLE tasks ADD COLUMN source VARCHAR(32) DEFAULT 'manual' NOT NULL",
'conversation_id': "ALTER TABLE tasks ADD COLUMN conversation_id VARCHAR(36)",
'quadrant': "ALTER TABLE tasks ADD COLUMN quadrant VARCHAR(64)",
'assignee_type': "ALTER TABLE tasks ADD COLUMN assignee_type VARCHAR(32)",
'assignee_id': "ALTER TABLE tasks ADD COLUMN assignee_id VARCHAR(255)",
'dispatch_status': "ALTER TABLE tasks ADD COLUMN dispatch_status VARCHAR(32) DEFAULT 'idle' NOT NULL",
'dispatch_run_id': "ALTER TABLE tasks ADD COLUMN dispatch_run_id VARCHAR(64)",
'result_summary': "ALTER TABLE tasks ADD COLUMN result_summary TEXT",
'started_at': "ALTER TABLE tasks ADD COLUMN started_at DATETIME",
'last_synced_at': "ALTER TABLE tasks ADD COLUMN last_synced_at DATETIME",
}
for column, ddl in required_columns.items():
if column not in columns:
await conn.execute(text(ddl))
indexes = {
'ix_tasks_due_date': "CREATE INDEX IF NOT EXISTS ix_tasks_due_date ON tasks (due_date)",
'ix_tasks_source': "CREATE INDEX IF NOT EXISTS ix_tasks_source ON tasks (source)",
'ix_tasks_conversation_id': "CREATE INDEX IF NOT EXISTS ix_tasks_conversation_id ON tasks (conversation_id)",
'ix_tasks_quadrant': "CREATE INDEX IF NOT EXISTS ix_tasks_quadrant ON tasks (quadrant)",
'ix_tasks_assignee_type': "CREATE INDEX IF NOT EXISTS ix_tasks_assignee_type ON tasks (assignee_type)",
'ix_tasks_assignee_id': "CREATE INDEX IF NOT EXISTS ix_tasks_assignee_id ON tasks (assignee_id)",
'ix_tasks_dispatch_status': "CREATE INDEX IF NOT EXISTS ix_tasks_dispatch_status ON tasks (dispatch_status)",
'ix_tasks_dispatch_run_id': "CREATE INDEX IF NOT EXISTS ix_tasks_dispatch_run_id ON tasks (dispatch_run_id)",
}
for ddl in indexes.values():
await conn.execute(text(ddl))
history_rows = await _get_table_info(conn, 'task_histories')
if history_rows:
history_columns = {row[1] for row in history_rows}
if 'subtask_id' not in history_columns:
await conn.execute(text("ALTER TABLE task_histories ADD COLUMN subtask_id VARCHAR(36)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_task_histories_subtask_id ON task_histories (subtask_id)"))
await conn.execute(
text(
"""
CREATE TABLE IF NOT EXISTS task_subtasks (
id VARCHAR(36) PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
task_id VARCHAR(36) NOT NULL,
title VARCHAR(500) NOT NULL,
description TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'todo',
order_index INTEGER NOT NULL DEFAULT 0,
assignee_type VARCHAR(32),
assignee_id VARCHAR(255),
dispatch_status VARCHAR(32) NOT NULL DEFAULT 'idle',
dispatch_run_id VARCHAR(64),
completed_at DATETIME,
FOREIGN KEY(task_id) REFERENCES tasks (id)
)
"""
)
)
subtask_rows = await _get_table_info(conn, 'task_subtasks')
subtask_columns = {row[1] for row in subtask_rows}
if 'result_summary' not in subtask_columns:
await conn.execute(text("ALTER TABLE task_subtasks ADD COLUMN result_summary TEXT"))
subtask_indexes = {
'ix_task_subtasks_task_id': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_task_id ON task_subtasks (task_id)",
'ix_task_subtasks_status': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_status ON task_subtasks (status)",
'ix_task_subtasks_order_index': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_order_index ON task_subtasks (order_index)",
'ix_task_subtasks_assignee_type': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_assignee_type ON task_subtasks (assignee_type)",
'ix_task_subtasks_assignee_id': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_assignee_id ON task_subtasks (assignee_id)",
'ix_task_subtasks_dispatch_status': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_dispatch_status ON task_subtasks (dispatch_status)",
'ix_task_subtasks_dispatch_run_id': "CREATE INDEX IF NOT EXISTS ix_task_subtasks_dispatch_run_id ON task_subtasks (dispatch_run_id)",
}
for ddl in subtask_indexes.values():
await conn.execute(text(ddl))
# Normalize legacy/invalid enum-like values to prevent ORM Enum decoding failures.
await conn.execute(
text(
"""
UPDATE tasks
SET source = 'manual'
WHERE source IS NULL
OR TRIM(source) = ''
OR source NOT IN ('manual','chat','schedule_center','today_status','commander')
"""
)
)
await conn.execute(
text(
"""
UPDATE tasks
SET status = 'todo'
WHERE status IS NULL
OR TRIM(status) = ''
OR status NOT IN ('todo','in_progress','done','cancelled')
"""
)
)
await conn.execute(
text(
"""
UPDATE tasks
SET priority = 'medium'
WHERE priority IS NULL
OR TRIM(priority) = ''
OR priority NOT IN ('low','medium','high','urgent')
"""
)
)
await conn.execute(
text(
"""
UPDATE tasks
SET quadrant = NULL
WHERE quadrant IS NOT NULL
AND (TRIM(quadrant) = '' OR quadrant NOT IN (
'urgent-important',
'not-urgent-important',
'urgent-not-important',
'not-urgent-not-important'
))
"""
)
)
await conn.execute(
text(
"""
UPDATE tasks
SET assignee_type = NULL
WHERE assignee_type IS NOT NULL
AND (TRIM(assignee_type) = '' OR assignee_type NOT IN (
'user','commander','agent','planner','executor','knowledge','analyst','coder','researcher'
))
"""
)
)
await conn.execute(
text(
"""
UPDATE tasks
SET dispatch_status = 'idle'
WHERE dispatch_status IS NULL
OR TRIM(dispatch_status) = ''
OR dispatch_status NOT IN ('idle','queued','running','completed','failed')
"""
)
)
await conn.execute(
text(
"""
UPDATE task_subtasks
SET status = 'todo'
WHERE status IS NULL
OR TRIM(status) = ''
OR status NOT IN ('todo','in_progress','done','cancelled')
"""
)
)
await conn.execute(
text(
"""
UPDATE task_subtasks
SET assignee_type = NULL
WHERE assignee_type IS NOT NULL
AND (TRIM(assignee_type) = '' OR assignee_type NOT IN (
'user','commander','agent','planner','executor','knowledge','analyst','coder','researcher'
))
"""
)
)
await conn.execute(
text(
"""
UPDATE task_subtasks
SET dispatch_status = 'idle'
WHERE dispatch_status IS NULL
OR TRIM(dispatch_status) = ''
OR dispatch_status NOT IN ('idle','queued','running','completed','failed')
"""
)
)
async def ensure_log_columns(conn):
result = await conn.execute(text("PRAGMA table_info(logs)"))
rows = result.fetchall()