diff --git a/ai-service/scripts/migrations/005_create_mid_tables.sql b/ai-service/scripts/migrations/005_create_mid_tables.sql new file mode 100644 index 0000000..657deef --- /dev/null +++ b/ai-service/scripts/migrations/005_create_mid_tables.sql @@ -0,0 +1,67 @@ +-- [AC-IDMP-05/07/09/20] 中台改造数据库迁移 +-- 创建高风险策略表、会话模式记录表、审计日志表 + +-- 高风险策略配置表 +CREATE TABLE IF NOT EXISTS high_risk_policies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id VARCHAR(255) NOT NULL, + scenario VARCHAR(64) NOT NULL, + handler_mode VARCHAR(32) NOT NULL DEFAULT 'micro_flow', + flow_id UUID REFERENCES script_flows(id), + transfer_message TEXT, + keywords JSONB, + patterns JSONB, + priority INTEGER DEFAULT 0, + is_enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS ix_high_risk_policies_tenant ON high_risk_policies(tenant_id); +CREATE INDEX IF NOT EXISTS ix_high_risk_policies_tenant_enabled ON high_risk_policies(tenant_id, is_enabled); + +-- 会话模式记录表 +CREATE TABLE IF NOT EXISTS session_mode_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id VARCHAR(255) NOT NULL, + session_id VARCHAR(255) NOT NULL, + mode VARCHAR(32) NOT NULL DEFAULT 'BOT_ACTIVE', + reason TEXT, + switched_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + CONSTRAINT ix_session_mode_records_tenant_session UNIQUE (tenant_id, session_id) +); + +-- 中台审计日志表 +CREATE TABLE IF NOT EXISTS mid_audit_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id VARCHAR(255) NOT NULL, + session_id VARCHAR(255) NOT NULL, + request_id VARCHAR(255) NOT NULL, + generation_id VARCHAR(255) NOT NULL, + mode VARCHAR(32) NOT NULL, + intent VARCHAR(255), + tool_calls JSONB, + guardrail_triggered BOOLEAN DEFAULT FALSE, + fallback_reason_code VARCHAR(64), + react_iterations INTEGER, + high_risk_scenario VARCHAR(64), + latency_ms INTEGER, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS ix_mid_audit_logs_tenant_session ON mid_audit_logs(tenant_id, session_id); +CREATE INDEX IF NOT EXISTS ix_mid_audit_logs_tenant_request ON mid_audit_logs(tenant_id, request_id); +CREATE INDEX IF NOT EXISTS ix_mid_audit_logs_tenant_generation ON mid_audit_logs(tenant_id, generation_id); +CREATE INDEX IF NOT EXISTS ix_mid_audit_logs_created ON mid_audit_logs(created_at); + +-- 插入默认高风险策略(空集保护) +-- 注意:这里只插入示例,实际由租户配置 +INSERT INTO high_risk_policies (tenant_id, scenario, handler_mode, keywords, priority, is_enabled) +VALUES + ('default', 'refund', 'micro_flow', '["退款", "退货", "退钱", "退费"]'::jsonb, 100, TRUE), + ('default', 'complaint_escalation', 'micro_flow', '["投诉", "举报", "差评", "曝光"]'::jsonb, 100, TRUE), + ('default', 'privacy_sensitive_promise', 'micro_flow', '["承诺", "保证", "隐私", "个人信息"]'::jsonb, 100, TRUE), + ('default', 'transfer', 'transfer', '["人工", "转人工", "人工客服", "真人"]'::jsonb, 100, TRUE) +ON CONFLICT DO NOTHING; diff --git a/ai-service/scripts/migrations/006_create_shared_sessions.sql b/ai-service/scripts/migrations/006_create_shared_sessions.sql new file mode 100644 index 0000000..8396fd6 --- /dev/null +++ b/ai-service/scripts/migrations/006_create_shared_sessions.sql @@ -0,0 +1,24 @@ +-- [AC-IDMP-SHARE] 对话分享功能数据库迁移 +-- 创建 shared_sessions 表,支持通过链接分享对话 + +-- 分享会话表 +CREATE TABLE IF NOT EXISTS shared_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + share_token VARCHAR(255) NOT NULL UNIQUE, + session_id VARCHAR(255) NOT NULL, + tenant_id VARCHAR(255) NOT NULL, + title VARCHAR(255), + description TEXT, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + max_concurrent_users INTEGER DEFAULT 10, + current_users INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS ix_shared_sessions_share_token ON shared_sessions(share_token); +CREATE INDEX IF NOT EXISTS ix_shared_sessions_session_id ON shared_sessions(session_id); +CREATE INDEX IF NOT EXISTS ix_shared_sessions_tenant ON shared_sessions(tenant_id); +CREATE INDEX IF NOT EXISTS ix_shared_sessions_tenant_session ON shared_sessions(tenant_id, session_id); +CREATE INDEX IF NOT EXISTS ix_shared_sessions_expires_at ON shared_sessions(expires_at); diff --git a/ai-service/scripts/migrations/create_shared_sessions_table.py b/ai-service/scripts/migrations/create_shared_sessions_table.py new file mode 100644 index 0000000..89d2b3d --- /dev/null +++ b/ai-service/scripts/migrations/create_shared_sessions_table.py @@ -0,0 +1,71 @@ +""" +Migration script to create shared_sessions table. +Run: python scripts/migrations/create_shared_sessions_table.py +""" + +import asyncio +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.core.config import get_settings + + +async def run_migration(): + """Run the migration to create shared_sessions table.""" + settings = get_settings() + engine = create_async_engine(settings.database_url, echo=True) + async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + migration_sql = """ + CREATE TABLE IF NOT EXISTS shared_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + share_token VARCHAR(255) NOT NULL UNIQUE, + session_id VARCHAR(255) NOT NULL, + tenant_id VARCHAR(255) NOT NULL, + title VARCHAR(255), + description TEXT, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + max_concurrent_users INTEGER DEFAULT 10, + current_users INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + CREATE INDEX IF NOT EXISTS ix_shared_sessions_share_token ON shared_sessions(share_token); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_session_id ON shared_sessions(session_id); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_tenant ON shared_sessions(tenant_id); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_tenant_session ON shared_sessions(tenant_id, session_id); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_expires_at ON shared_sessions(expires_at); + """ + + async with async_session_maker() as session: + for statement in migration_sql.strip().split(';'): + statement = statement.strip() + if statement and not statement.startswith('--'): + try: + await session.execute(text(statement)) + print(f"Executed: {statement[:60]}...") + except Exception as e: + error_str = str(e).lower() + if "already exists" in error_str or "duplicate" in error_str: + print(f"Skipped (already exists): {statement[:60]}...") + else: + print(f"Error: {e}") + raise + + await session.commit() + print("\nMigration completed successfully!") + + await engine.dispose() + + +if __name__ == "__main__": + asyncio.run(run_migration()) diff --git a/ai-service/scripts/migrations/run_shared_sessions_migration.py b/ai-service/scripts/migrations/run_shared_sessions_migration.py new file mode 100644 index 0000000..463921e --- /dev/null +++ b/ai-service/scripts/migrations/run_shared_sessions_migration.py @@ -0,0 +1,71 @@ +""" +Migration script to create shared_sessions table. +Run: python scripts/migrations/run_shared_sessions_migration.py +""" + +import asyncio +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.core.config import get_settings + + +async def run_migration(): + """Run the migration to create shared_sessions table.""" + settings = get_settings() + engine = create_async_engine(settings.database_url, echo=True) + async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + migration_sql = """ + CREATE TABLE IF NOT EXISTS shared_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + share_token VARCHAR(255) NOT NULL UNIQUE, + session_id VARCHAR(255) NOT NULL, + tenant_id VARCHAR(255) NOT NULL, + title VARCHAR(255), + description TEXT, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + max_concurrent_users INTEGER DEFAULT 10, + current_users INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + CREATE INDEX IF NOT EXISTS ix_shared_sessions_share_token ON shared_sessions(share_token); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_session_id ON shared_sessions(session_id); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_tenant ON shared_sessions(tenant_id); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_tenant_session ON shared_sessions(tenant_id, session_id); + CREATE INDEX IF NOT EXISTS ix_shared_sessions_expires_at ON shared_sessions(expires_at); + """ + + async with async_session_maker() as session: + for statement in migration_sql.strip().split(';'): + statement = statement.strip() + if statement and not statement.startswith('--'): + try: + await session.execute(text(statement)) + print(f"Executed: {statement[:60]}...") + except Exception as e: + error_str = str(e).lower() + if "already exists" in error_str or "duplicate" in error_str: + print(f"Skipped (already exists): {statement[:60]}...") + else: + print(f"Error: {e}") + raise + + await session.commit() + print("\nMigration completed successfully!") + + await engine.dispose() + + +if __name__ == "__main__": + asyncio.run(run_migration())