feat(server): add workbench API, DSH planner worker and RAG pipeline
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
"""数据库引擎与初始化"""
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
from sqlmodel import SQLModel, Session, create_engine, select
|
||||
from sqlalchemy.pool import NullPool
|
||||
from sqlalchemy import event
|
||||
from .config import DB_PATH
|
||||
import app.models # noqa: F401 确保所有表注册
|
||||
from .models import Agent, Artifact, Campaign, KnowledgeUsage, QCReport, Segment, RoleDefinition
|
||||
from .auth import ROLE_LABELS, ROLE_PERMISSIONS
|
||||
|
||||
engine = create_engine(f"sqlite:///{DB_PATH}",
|
||||
connect_args={"check_same_thread": False, "timeout": 30},
|
||||
poolclass=NullPool)
|
||||
|
||||
|
||||
@event.listens_for(engine, "connect")
|
||||
def _configure_sqlite_connection(dbapi_connection, _connection_record):
|
||||
"""Apply connection-level SQLite tuning for concurrent API workers.
|
||||
|
||||
The application uses ``NullPool`` so every request may open a fresh
|
||||
connection. Applying these pragmas at connect time keeps the settings
|
||||
consistent without requiring callers to remember to configure them.
|
||||
WAL and a bounded busy timeout let readers continue while a writer is
|
||||
committing; ``synchronous=NORMAL`` provides a useful throughput gain while
|
||||
retaining SQLite's normal crash-safety guarantees.
|
||||
"""
|
||||
cursor = dbapi_connection.cursor()
|
||||
try:
|
||||
cursor.execute("PRAGMA busy_timeout=30000")
|
||||
# ``:memory:`` databases (used by tests) do not support WAL. Ignore
|
||||
# that specific SQLite error while still applying the other pragmas.
|
||||
try:
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
except sqlite3.DatabaseError:
|
||||
pass
|
||||
cursor.execute("PRAGMA synchronous=NORMAL")
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def _apply_column_migrations(conn):
|
||||
"""Add missing columns without rebuilding tables or touching existing rows."""
|
||||
migrations_by_table = {
|
||||
"resourcerevision": {
|
||||
"fork_resource_id": "ALTER TABLE resourcerevision ADD COLUMN fork_resource_id INTEGER DEFAULT 0",
|
||||
"base_fingerprint": "ALTER TABLE resourcerevision ADD COLUMN base_fingerprint VARCHAR DEFAULT ''",
|
||||
},
|
||||
"trainingmap": {
|
||||
"kb_category_ids": "ALTER TABLE trainingmap ADD COLUMN kb_category_ids JSON DEFAULT '{}'",
|
||||
"coverage": "ALTER TABLE trainingmap ADD COLUMN coverage JSON DEFAULT '{}'",
|
||||
"assessment_cancel_requested": "ALTER TABLE trainingmap ADD COLUMN assessment_cancel_requested BOOLEAN DEFAULT 0",
|
||||
"agent_id": "ALTER TABLE trainingmap ADD COLUMN agent_id INTEGER",
|
||||
},
|
||||
"systemlog": {
|
||||
"level": "ALTER TABLE systemlog ADD COLUMN level VARCHAR DEFAULT 'INFO'",
|
||||
"source": "ALTER TABLE systemlog ADD COLUMN source VARCHAR DEFAULT 'app'",
|
||||
"message": "ALTER TABLE systemlog ADD COLUMN message VARCHAR DEFAULT ''",
|
||||
"trace_id": "ALTER TABLE systemlog ADD COLUMN trace_id VARCHAR DEFAULT ''",
|
||||
"status": "ALTER TABLE systemlog ADD COLUMN status VARCHAR DEFAULT 'ok'",
|
||||
"created_at": "ALTER TABLE systemlog ADD COLUMN created_at VARCHAR DEFAULT ''",
|
||||
},
|
||||
"campaignrun": {
|
||||
"pause_requested": "ALTER TABLE campaignrun ADD COLUMN pause_requested BOOLEAN DEFAULT 0",
|
||||
},
|
||||
"contenttag": {
|
||||
"organization_id": "ALTER TABLE contenttag ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE contenttag ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE contenttag ADD COLUMN visibility VARCHAR DEFAULT 'global'",
|
||||
},
|
||||
"agentskill": {
|
||||
"organization_id": "ALTER TABLE agentskill ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE agentskill ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE agentskill ADD COLUMN visibility VARCHAR DEFAULT 'global'",
|
||||
},
|
||||
"agentprompttemplate": {
|
||||
"organization_id": "ALTER TABLE agentprompttemplate ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE agentprompttemplate ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE agentprompttemplate ADD COLUMN visibility VARCHAR DEFAULT 'global'",
|
||||
},
|
||||
"knowledgefile": {
|
||||
"ai_category_id": "ALTER TABLE knowledgefile ADD COLUMN ai_category_id INTEGER",
|
||||
"classification_status": "ALTER TABLE knowledgefile ADD COLUMN classification_status VARCHAR DEFAULT 'unclassified'",
|
||||
"ai_confidence": "ALTER TABLE knowledgefile ADD COLUMN ai_confidence FLOAT DEFAULT 0",
|
||||
"ai_reason": "ALTER TABLE knowledgefile ADD COLUMN ai_reason VARCHAR DEFAULT ''",
|
||||
"content_hash": "ALTER TABLE knowledgefile ADD COLUMN content_hash VARCHAR DEFAULT ''",
|
||||
"revision": "ALTER TABLE knowledgefile ADD COLUMN revision INTEGER DEFAULT 1",
|
||||
},
|
||||
"knowledgebase": {
|
||||
"organization_id": "ALTER TABLE knowledgebase ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE knowledgebase ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE knowledgebase ADD COLUMN visibility VARCHAR DEFAULT 'global'",
|
||||
"chunk_target": "ALTER TABLE knowledgebase ADD COLUMN chunk_target INTEGER DEFAULT 0",
|
||||
"chunk_mode": "ALTER TABLE knowledgebase ADD COLUMN chunk_mode VARCHAR DEFAULT 'auto'",
|
||||
},
|
||||
"knowledgeusage": {
|
||||
"banned": "ALTER TABLE knowledgeusage ADD COLUMN banned BOOLEAN DEFAULT 0",
|
||||
},
|
||||
"agent": {
|
||||
"kb_category_ids": "ALTER TABLE agent ADD COLUMN kb_category_ids JSON DEFAULT '[]'",
|
||||
"fusion_agent_ids": "ALTER TABLE agent ADD COLUMN fusion_agent_ids JSON DEFAULT '[]'",
|
||||
"prompt_template_id": "ALTER TABLE agent ADD COLUMN prompt_template_id INTEGER",
|
||||
"model_catalog_id": "ALTER TABLE agent ADD COLUMN model_catalog_id INTEGER",
|
||||
"allowed_tools": "ALTER TABLE agent ADD COLUMN allowed_tools JSON DEFAULT '[]'",
|
||||
"organization_id": "ALTER TABLE agent ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE agent ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE agent ADD COLUMN visibility VARCHAR DEFAULT 'global'",
|
||||
"runtime_snapshot": "ALTER TABLE agent ADD COLUMN runtime_snapshot JSON",
|
||||
"persona_json": "ALTER TABLE agent ADD COLUMN persona_json JSON",
|
||||
"digital_human_id": "ALTER TABLE agent ADD COLUMN digital_human_id INTEGER",
|
||||
},
|
||||
"digitalhuman": {
|
||||
"visibility": "ALTER TABLE digitalhuman ADD COLUMN visibility VARCHAR DEFAULT 'private'",
|
||||
"persona_meta": "ALTER TABLE digitalhuman ADD COLUMN persona_meta JSON DEFAULT '{}'",
|
||||
},
|
||||
"modelprovider": {
|
||||
"organization_id": "ALTER TABLE modelprovider ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
},
|
||||
"digitalhumangrant": {
|
||||
"enabled": "ALTER TABLE digitalhumangrant ADD COLUMN enabled BOOLEAN DEFAULT 1",
|
||||
},
|
||||
"canvas": {
|
||||
"organization_id": "ALTER TABLE canvas ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE canvas ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE canvas ADD COLUMN visibility VARCHAR DEFAULT 'global'",
|
||||
"supervisor_agent_id": "ALTER TABLE canvas ADD COLUMN supervisor_agent_id INTEGER",
|
||||
"supervisor_mode": "ALTER TABLE canvas ADD COLUMN supervisor_mode VARCHAR DEFAULT 'on_drift'",
|
||||
},
|
||||
"campaign": {
|
||||
"approved_plan_version_id": "ALTER TABLE campaign ADD COLUMN approved_plan_version_id INTEGER",
|
||||
"organization_id": "ALTER TABLE campaign ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE campaign ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE campaign ADD COLUMN visibility VARCHAR DEFAULT 'private'",
|
||||
"objective": "ALTER TABLE campaign ADD COLUMN objective VARCHAR DEFAULT ''",
|
||||
"success_criteria": "ALTER TABLE campaign ADD COLUMN success_criteria VARCHAR DEFAULT ''",
|
||||
"duration_minutes": "ALTER TABLE campaign ADD COLUMN duration_minutes INTEGER DEFAULT 0",
|
||||
"qc_mode": "ALTER TABLE campaign ADD COLUMN qc_mode VARCHAR DEFAULT 'full'",
|
||||
},
|
||||
"pendingmemory": {
|
||||
"organization_id": "ALTER TABLE pendingmemory ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE pendingmemory ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE pendingmemory ADD COLUMN visibility VARCHAR DEFAULT 'private'",
|
||||
},
|
||||
"memory": {
|
||||
"organization_id": "ALTER TABLE memory ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE memory ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE memory ADD COLUMN visibility VARCHAR DEFAULT 'private'",
|
||||
"title": "ALTER TABLE memory ADD COLUMN title VARCHAR DEFAULT ''",
|
||||
"description": "ALTER TABLE memory ADD COLUMN description VARCHAR DEFAULT ''",
|
||||
"privacy": "ALTER TABLE memory ADD COLUMN privacy VARCHAR DEFAULT 'internal'",
|
||||
"tags": "ALTER TABLE memory ADD COLUMN tags JSON DEFAULT '[]'",
|
||||
"timestamp": "ALTER TABLE memory ADD COLUMN timestamp VARCHAR DEFAULT ''",
|
||||
"index_status": "ALTER TABLE memory ADD COLUMN index_status VARCHAR DEFAULT 'indexed'",
|
||||
"usage_count": "ALTER TABLE memory ADD COLUMN usage_count INTEGER DEFAULT 0",
|
||||
"last_used_at": "ALTER TABLE memory ADD COLUMN last_used_at VARCHAR DEFAULT ''",
|
||||
"supersedes_id": "ALTER TABLE memory ADD COLUMN supersedes_id INTEGER DEFAULT 0",
|
||||
},
|
||||
"appuser": {
|
||||
"organization_id": "ALTER TABLE appuser ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
},
|
||||
"roledefinition": {
|
||||
"organization_id": "ALTER TABLE roledefinition ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
},
|
||||
"segment": {
|
||||
"agent_assignment_mode": "ALTER TABLE segment ADD COLUMN agent_assignment_mode VARCHAR DEFAULT 'auto'",
|
||||
"generation_history": "ALTER TABLE segment ADD COLUMN generation_history JSON DEFAULT '[]'",
|
||||
"fusion_agent_ids": "ALTER TABLE segment ADD COLUMN fusion_agent_ids JSON DEFAULT '[]'",
|
||||
"runtime_snapshot": "ALTER TABLE segment ADD COLUMN runtime_snapshot JSON DEFAULT '{}'",
|
||||
"kb_category_ids": "ALTER TABLE segment ADD COLUMN kb_category_ids JSON DEFAULT '[]'",
|
||||
"kb_category_recommendation": "ALTER TABLE segment ADD COLUMN kb_category_recommendation JSON DEFAULT '{}'",
|
||||
},
|
||||
"llmcall": {
|
||||
"run_id": "ALTER TABLE llmcall ADD COLUMN run_id INTEGER DEFAULT 0",
|
||||
"trace_id": "ALTER TABLE llmcall ADD COLUMN trace_id VARCHAR DEFAULT ''",
|
||||
"span_id": "ALTER TABLE llmcall ADD COLUMN span_id VARCHAR DEFAULT ''",
|
||||
"attempt": "ALTER TABLE llmcall ADD COLUMN attempt INTEGER DEFAULT 1",
|
||||
"prompt_hash": "ALTER TABLE llmcall ADD COLUMN prompt_hash VARCHAR DEFAULT ''",
|
||||
"output_hash": "ALTER TABLE llmcall ADD COLUMN output_hash VARCHAR DEFAULT ''",
|
||||
"user_id": "ALTER TABLE llmcall ADD COLUMN user_id INTEGER DEFAULT 0",
|
||||
"organization_id": "ALTER TABLE llmcall ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"usage_event_id": "ALTER TABLE llmcall ADD COLUMN usage_event_id INTEGER",
|
||||
"billing_status": "ALTER TABLE llmcall ADD COLUMN billing_status VARCHAR DEFAULT 'unbilled'",
|
||||
},
|
||||
"campaignrun": {
|
||||
"user_id": "ALTER TABLE campaignrun ADD COLUMN user_id INTEGER DEFAULT 0",
|
||||
"organization_id": "ALTER TABLE campaignrun ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"trace_id": "ALTER TABLE campaignrun ADD COLUMN trace_id VARCHAR DEFAULT ''",
|
||||
"runtime_version": "ALTER TABLE campaignrun ADD COLUMN runtime_version VARCHAR DEFAULT 'v1'",
|
||||
"pause_requested": "ALTER TABLE campaignrun ADD COLUMN pause_requested BOOLEAN DEFAULT 0",
|
||||
},
|
||||
"runevent": {
|
||||
"trace_id": "ALTER TABLE runevent ADD COLUMN trace_id VARCHAR DEFAULT ''",
|
||||
"span_id": "ALTER TABLE runevent ADD COLUMN span_id VARCHAR DEFAULT ''",
|
||||
"parent_span_id": "ALTER TABLE runevent ADD COLUMN parent_span_id VARCHAR DEFAULT ''",
|
||||
"actor_agent_id": "ALTER TABLE runevent ADD COLUMN actor_agent_id INTEGER DEFAULT 0",
|
||||
"actor_name": "ALTER TABLE runevent ADD COLUMN actor_name VARCHAR DEFAULT ''",
|
||||
"tool_name": "ALTER TABLE runevent ADD COLUMN tool_name VARCHAR DEFAULT ''",
|
||||
},
|
||||
"qcreport": {
|
||||
"run_id": "ALTER TABLE qcreport ADD COLUMN run_id INTEGER DEFAULT 0",
|
||||
"trace_id": "ALTER TABLE qcreport ADD COLUMN trace_id VARCHAR DEFAULT ''",
|
||||
"runtime_snapshot": "ALTER TABLE qcreport ADD COLUMN runtime_snapshot JSON DEFAULT '{}'",
|
||||
},
|
||||
"artifact": {
|
||||
"organization_id": "ALTER TABLE artifact ADD COLUMN organization_id INTEGER DEFAULT 1",
|
||||
"owner_user_id": "ALTER TABLE artifact ADD COLUMN owner_user_id INTEGER DEFAULT 0",
|
||||
"visibility": "ALTER TABLE artifact ADD COLUMN visibility VARCHAR DEFAULT 'private'",
|
||||
"run_id": "ALTER TABLE artifact ADD COLUMN run_id INTEGER DEFAULT 0",
|
||||
"trace_id": "ALTER TABLE artifact ADD COLUMN trace_id VARCHAR DEFAULT ''",
|
||||
"runtime_snapshot": "ALTER TABLE artifact ADD COLUMN runtime_snapshot JSON DEFAULT '{}'",
|
||||
},
|
||||
}
|
||||
for table, migrations in migrations_by_table.items():
|
||||
columns = {row[1] for row in conn.exec_driver_sql(f"PRAGMA table_info({table})").fetchall()}
|
||||
for name, statement in migrations.items():
|
||||
if name not in columns:
|
||||
conn.exec_driver_sql(statement)
|
||||
|
||||
def init_db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with engine.begin() as conn:
|
||||
_apply_column_migrations(conn)
|
||||
# 用户对全局资源的修改/删除提案。该表在旧生产库中可能不存在;
|
||||
# SQLModel.metadata.create_all 只会覆盖新表,因此这里显式补建并建立
|
||||
# 常用筛选索引,保证版本审核接口升级后可直接使用。
|
||||
conn.exec_driver_sql(
|
||||
"CREATE TABLE IF NOT EXISTS resourcerevision ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"resource_type VARCHAR NOT NULL,"
|
||||
"resource_id INTEGER NOT NULL,"
|
||||
"fork_resource_id INTEGER NOT NULL DEFAULT 0,"
|
||||
"owner_user_id INTEGER NOT NULL,"
|
||||
"organization_id INTEGER NOT NULL DEFAULT 1,"
|
||||
"operation VARCHAR NOT NULL DEFAULT 'update',"
|
||||
"base_version INTEGER NOT NULL DEFAULT 1,"
|
||||
"base_fingerprint VARCHAR NOT NULL DEFAULT '',"
|
||||
"version INTEGER NOT NULL DEFAULT 1,"
|
||||
"payload JSON NOT NULL DEFAULT '{}',"
|
||||
"status VARCHAR NOT NULL DEFAULT 'pending',"
|
||||
"reviewer_user_id INTEGER NOT NULL DEFAULT 0,"
|
||||
"review_note VARCHAR NOT NULL DEFAULT '',"
|
||||
"created_at VARCHAR NOT NULL DEFAULT '',"
|
||||
"reviewed_at VARCHAR NOT NULL DEFAULT ''"
|
||||
")"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resourcerevision_resource "
|
||||
"ON resourcerevision (resource_type, resource_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resourcerevision_status "
|
||||
"ON resourcerevision (status, organization_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE TABLE IF NOT EXISTS resourcefork ("
|
||||
"id INTEGER PRIMARY KEY, resource_type VARCHAR NOT NULL, source_id INTEGER NOT NULL,"
|
||||
"copy_id INTEGER NOT NULL, owner_user_id INTEGER NOT NULL, organization_id INTEGER NOT NULL DEFAULT 1,"
|
||||
"base_version INTEGER NOT NULL DEFAULT 1, base_fingerprint VARCHAR NOT NULL DEFAULT '', created_at VARCHAR NOT NULL DEFAULT '')"
|
||||
)
|
||||
# 历史记忆没有沉淀日期时,沿用创建时间
|
||||
conn.exec_driver_sql("UPDATE memory SET timestamp = created_at WHERE COALESCE(timestamp, '') = ''")
|
||||
# Memory 使用本地文本召回,没有外部向量索引。旧版本编辑元数据留下的
|
||||
# needs_reindex 没有后台消费者,恢复这些记忆,避免被永久静默排除。
|
||||
conn.exec_driver_sql("UPDATE memory SET index_status = 'indexed' WHERE index_status = 'needs_reindex'")
|
||||
# 所有历史列补齐后再读取/写入角色种子,避免升级期间按新模型查询旧表结构。
|
||||
with Session(engine) as session:
|
||||
for role, permissions in ROLE_PERMISSIONS.items():
|
||||
if not session.exec(select(RoleDefinition).where(RoleDefinition.role == role)).first():
|
||||
session.add(RoleDefinition(role=role, label=ROLE_LABELS.get(role, role),
|
||||
description=f"{ROLE_LABELS.get(role, role)}默认权限",
|
||||
permissions=sorted(permissions), organization_id=1))
|
||||
session.commit()
|
||||
# 工作台拥有并幂等播种内置工作流模板;DSH Planner 只能在模板边界内提案。
|
||||
with Session(engine) as session:
|
||||
from .planner import ensure_default_workflow_template
|
||||
ensure_default_workflow_template(session)
|
||||
# 同一 Agent 对同一知识库片段只能保留一条状态记录,作为并发认领的数据库约束。
|
||||
with engine.begin() as conn:
|
||||
# 兼容旧版本删除 Provider 后遗留的模型目录,避免用户侧读取到无来源的模型。
|
||||
conn.exec_driver_sql(
|
||||
"DELETE FROM modelcatalog WHERE provider_id NOT IN (SELECT id FROM modelprovider)"
|
||||
)
|
||||
# 早期版本没有唯一约束,先保留最早一条记录再创建索引,避免升级时因历史重复数据失败。
|
||||
duplicate_groups = conn.exec_driver_sql(
|
||||
"SELECT agent_id, kb_id, chunk_id, MIN(id) AS keep_id "
|
||||
"FROM knowledgeusage GROUP BY agent_id, kb_id, chunk_id HAVING COUNT(*) > 1"
|
||||
).fetchall()
|
||||
for agent_id, kb_id, chunk_id, keep_id in duplicate_groups:
|
||||
conn.exec_driver_sql(
|
||||
"DELETE FROM knowledgeusage WHERE agent_id = ? AND kb_id = ? AND chunk_id = ? AND id <> ?",
|
||||
(agent_id, kb_id, chunk_id, keep_id),
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_knowledge_usage_agent_chunk "
|
||||
"ON knowledgeusage (agent_id, kb_id, chunk_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_campaign_active_run "
|
||||
"ON campaignrun (campaign_id) WHERE status IN ('queued', 'running')"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_agentsession_run_segment "
|
||||
"ON agentsession (run_id, segment_id, created_at)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_qcreport_run_trace "
|
||||
"ON qcreport (run_id, trace_id, created_at)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_artifact_run_trace "
|
||||
"ON artifact (run_id, trace_id, created_at)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_agent_prompt_template "
|
||||
"ON agent (prompt_template_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_grant_user "
|
||||
"ON knowledgebasegrant (kb_id, user_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_memory_grant_user "
|
||||
"ON memorygrant (memory_id, user_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_canvas_grant_user "
|
||||
"ON canvasgrant (canvas_id, user_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resource_scope_agent "
|
||||
"ON agent (organization_id, owner_user_id, visibility)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resource_scope_kb "
|
||||
"ON knowledgebase (organization_id, owner_user_id, visibility)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resource_scope_tag "
|
||||
"ON contenttag (organization_id, owner_user_id, visibility)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resource_scope_skill "
|
||||
"ON agentskill (organization_id, owner_user_id, visibility)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_resource_scope_prompt "
|
||||
"ON agentprompttemplate (organization_id, owner_user_id, visibility)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_model_catalog_provider_name "
|
||||
"ON modelcatalog (provider_id, model_name)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_model_catalog_enabled "
|
||||
"ON modelcatalog (enabled, provider_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_usage_event_idempotency "
|
||||
"ON usageevent (idempotency_key)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE INDEX IF NOT EXISTS ix_usage_event_owner_created "
|
||||
"ON usageevent (user_id, organization_id, created_at)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_usage_quota_owner_period "
|
||||
"ON usagequota (user_id, organization_id, period)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_point_account_owner "
|
||||
"ON pointaccount (user_id, organization_id)"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_point_transaction_idempotency "
|
||||
"ON pointtransaction (idempotency_key)"
|
||||
)
|
||||
# 高频列表与租户筛选的复合索引。单列索引虽可用,但无法覆盖常见的
|
||||
# ``WHERE ... ORDER BY ...`` 组合,数据量增大后会退化为全表扫描。
|
||||
for statement in (
|
||||
"CREATE INDEX IF NOT EXISTS ix_systemlog_created_level "
|
||||
"ON systemlog (created_at DESC, level)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_auditlog_created_actor "
|
||||
"ON auditlog (created_at DESC, actor_user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_kbfile_kb_category "
|
||||
"ON knowledgefile (kb_id, category_id)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_kbcategory_tree "
|
||||
"ON knowledgecategory (kb_id, parent_id, sort_order)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_kbusage_kb_status_time "
|
||||
"ON knowledgeusage (kb_id, status, used_at DESC)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_kbusage_agent_status_time "
|
||||
"ON knowledgeusage (agent_id, status, used_at DESC)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_agentskillbinding_agent_skill "
|
||||
"ON agentskillbinding (agent_id, skill_id)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_agentgrant_agent_user "
|
||||
"ON agentgrant (agent_id, user_id, enabled)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_kbgrant_kb_user "
|
||||
"ON knowledgebasegrant (kb_id, user_id, enabled)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_memorygrant_memory_user "
|
||||
"ON memorygrant (memory_id, user_id, enabled)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_canvasgrant_canvas_user "
|
||||
"ON canvasgrant (canvas_id, user_id, enabled)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_runevent_run_time "
|
||||
"ON runevent (run_id, created_at DESC)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_taskqueue_status_lease "
|
||||
"ON taskqueueitem (status, lease_until, created_at)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_llmcall_run_time "
|
||||
"ON llmcall (run_id, created_at DESC)",
|
||||
):
|
||||
conn.exec_driver_sql(statement)
|
||||
# 兼容旧数据:没有知识库的生产 Agent 不可参与生成,直到管理员完成挂接。
|
||||
with Session(engine) as session:
|
||||
invalid_agents = session.exec(select(Agent).where(
|
||||
Agent.role == "production", Agent.status == "active"
|
||||
)).all()
|
||||
changed = False
|
||||
for agent in invalid_agents:
|
||||
if not agent.kb_ids:
|
||||
agent.status = "disabled"
|
||||
session.add(agent)
|
||||
changed = True
|
||||
if changed:
|
||||
session.commit()
|
||||
# 旧版本曾把 checks=[] 的模型返回当作 PASS。保留原始成稿,但一律改为需复检,
|
||||
# 防止它们继续作为已验证产物被交付或复用。
|
||||
empty_reports = session.exec(select(QCReport).where(QCReport.verdict == "PASS")).all()
|
||||
affected_campaign_ids: set[int] = set()
|
||||
for report in empty_reports:
|
||||
if report.checks:
|
||||
continue
|
||||
report.verdict = "FAIL"
|
||||
report.checks = [{
|
||||
"id": "qc-empty-output", "name": "质检返回完整性", "status": "FAIL",
|
||||
"issues": [{"desc": "历史质检返回空检查列表,系统已自动标记为需要复检", "suggestion": "重新运行本段质检"}],
|
||||
}]
|
||||
segment = session.get(Segment, report.segment_id)
|
||||
if segment:
|
||||
affected_campaign_ids.add(segment.campaign_id)
|
||||
if segment.status == "approved":
|
||||
segment.status = "human_review"
|
||||
session.add(segment)
|
||||
session.add(report)
|
||||
if affected_campaign_ids:
|
||||
for campaign_id in affected_campaign_ids:
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
if campaign and campaign.status == "done":
|
||||
campaign.status = "human_review"
|
||||
campaign.finished_at = ""
|
||||
session.add(campaign)
|
||||
for artifact in session.exec(select(Artifact).where(Artifact.campaign_id == campaign_id)).all():
|
||||
artifact.final_status = "needs_recheck"
|
||||
report = dict(artifact.compose_report or {})
|
||||
report["safety_recheck"] = "历史段质检为空,组合产物需在重新审查后再交付"
|
||||
artifact.compose_report = report
|
||||
session.add(artifact)
|
||||
# 进程在模型请求中断时,reserved 片段从未产出正文,启动后应自动归还。
|
||||
for usage in session.exec(select(KnowledgeUsage).where(KnowledgeUsage.status == "reserved")).all():
|
||||
usage.status = "available"
|
||||
usage.released_at = usage.released_at or "startup-recovery"
|
||||
session.add(usage)
|
||||
session.commit()
|
||||
# P0-4 租户边界数据库级强制:业务表 organization_id 必须 >= 0(0=全局资源),
|
||||
# user 归属 organization_id 必须 >= 1。CHECK 约束让越权数据无法落库。
|
||||
with engine.begin() as conn:
|
||||
tenant_checks = {
|
||||
"campaign": "campaign_tenant_check",
|
||||
"knowledgebase": "knowledgebase_tenant_check",
|
||||
"memory": "memory_tenant_check",
|
||||
"agent": "agent_tenant_check",
|
||||
"artifact": "artifact_tenant_check",
|
||||
"canvas": "canvas_tenant_check",
|
||||
"contenttag": "contenttag_tenant_check",
|
||||
"agentskill": "agentskill_tenant_check",
|
||||
"agentprompttemplate": "agentprompttemplate_tenant_check",
|
||||
}
|
||||
for table, constraint in tenant_checks.items():
|
||||
exists = conn.exec_driver_sql(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='check_constraint' AND name=?",
|
||||
(constraint,),
|
||||
).fetchone()
|
||||
if not exists:
|
||||
# SQLite 不支持 ADD CONSTRAINT,重建表代价高;改用触发器实现同等级强制。
|
||||
trigger = f"trg_{table}_tenant_ins"
|
||||
trg_exists = conn.exec_driver_sql(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='trigger' AND name=?",
|
||||
(trigger,),
|
||||
).fetchone()
|
||||
if not trg_exists:
|
||||
conn.exec_driver_sql(
|
||||
f"CREATE TRIGGER {trigger} BEFORE INSERT ON {table} "
|
||||
f"WHEN NEW.organization_id IS NULL OR NEW.organization_id < 0 "
|
||||
f"BEGIN SELECT RAISE(ABORT, '{table}.organization_id 必须为 0(全局)或正整数(租户)'); END"
|
||||
)
|
||||
|
||||
def get_session():
|
||||
with Session(_current_engine()) as s:
|
||||
yield s
|
||||
|
||||
|
||||
def _current_engine():
|
||||
"""动态引擎:测试可以替换模块级 engine 变量,所有 Session 跟随。"""
|
||||
return engine
|
||||
Reference in New Issue
Block a user