1.1 补全 docker-compose.yml (PostgreSQL 16 + SaaS 后端容器)
1.2 Migration 系统化:
- provider_keys.max_rpm/max_tpm 改为 BIGINT 匹配 Rust Option<i64>
- 移除 seed_demo_data 中的 ALTER TABLE 运行时修补
- seed 数据绑定类型 i32→i64 对齐列定义
1.3 saas-config.toml 修复:
- 添加 cors_origins (开发环境 localhost)
- 添加 [scheduler] section (注释示例)
- 数据库密码改为开发默认值 + ZCLAW_DATABASE_URL 环境变量覆盖
- 添加配置文档注释 (JWT/TOTP/管理员环境变量)
340 lines
12 KiB
SQL
340 lines
12 KiB
SQL
-- Migration: Initial schema with TIMESTAMPTZ
|
|
-- Extracted from inline SCHEMA_SQL in db.rs, with TEXT timestamps converted to TIMESTAMPTZ.
|
|
|
|
CREATE TABLE IF NOT EXISTS saas_schema_version (
|
|
version INTEGER PRIMARY KEY
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL UNIQUE,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
display_name TEXT NOT NULL DEFAULT '',
|
|
avatar_url TEXT,
|
|
role TEXT NOT NULL DEFAULT 'user',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
totp_secret TEXT,
|
|
totp_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
last_login_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email);
|
|
CREATE INDEX IF NOT EXISTS idx_accounts_role ON accounts(role);
|
|
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
token_hash TEXT NOT NULL,
|
|
token_prefix TEXT NOT NULL,
|
|
permissions TEXT NOT NULL DEFAULT '[]',
|
|
last_used_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
revoked_at TIMESTAMPTZ,
|
|
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_api_tokens_account ON api_tokens(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash);
|
|
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
permissions TEXT NOT NULL DEFAULT '[]',
|
|
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS permission_templates (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
permissions TEXT NOT NULL DEFAULT '[]',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS operation_logs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
account_id TEXT,
|
|
action TEXT NOT NULL,
|
|
target_type TEXT,
|
|
target_id TEXT,
|
|
details TEXT,
|
|
ip_address TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_op_logs_account ON operation_logs(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_op_logs_action ON operation_logs(action);
|
|
CREATE INDEX IF NOT EXISTS idx_op_logs_time ON operation_logs(created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS providers (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
display_name TEXT NOT NULL,
|
|
api_key TEXT,
|
|
base_url TEXT NOT NULL,
|
|
api_protocol TEXT NOT NULL DEFAULT 'openai',
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
rate_limit_rpm BIGINT,
|
|
rate_limit_tpm BIGINT,
|
|
config_json TEXT DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS models (
|
|
id TEXT PRIMARY KEY,
|
|
provider_id TEXT NOT NULL,
|
|
model_id TEXT NOT NULL,
|
|
alias TEXT NOT NULL,
|
|
context_window BIGINT NOT NULL DEFAULT 8192,
|
|
max_output_tokens BIGINT NOT NULL DEFAULT 4096,
|
|
supports_streaming BOOLEAN NOT NULL DEFAULT TRUE,
|
|
supports_vision BOOLEAN NOT NULL DEFAULT FALSE,
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
pricing_input DOUBLE PRECISION DEFAULT 0,
|
|
pricing_output DOUBLE PRECISION DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(provider_id, model_id),
|
|
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_models_provider ON models(provider_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS account_api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
provider_id TEXT NOT NULL,
|
|
key_value TEXT NOT NULL,
|
|
key_label TEXT,
|
|
permissions TEXT NOT NULL DEFAULT '[]',
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
last_used_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
revoked_at TIMESTAMPTZ,
|
|
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_account_api_keys_account ON account_api_keys(account_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS usage_records (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
provider_id TEXT NOT NULL,
|
|
model_id TEXT NOT NULL,
|
|
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
latency_ms INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'success',
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_usage_account ON usage_records(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_usage_time ON usage_records(created_at);
|
|
-- idx_usage_day: Skipping because ::date on TIMESTAMPTZ is not IMMUTABLE
|
|
-- CREATE INDEX IF NOT EXISTS idx_usage_day ON usage_records((created_at::date));
|
|
|
|
CREATE TABLE IF NOT EXISTS relay_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
provider_id TEXT NOT NULL,
|
|
model_id TEXT NOT NULL,
|
|
request_hash TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'queued',
|
|
priority INTEGER NOT NULL DEFAULT 0,
|
|
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
request_body TEXT NOT NULL,
|
|
response_body TEXT,
|
|
input_tokens INTEGER DEFAULT 0,
|
|
output_tokens INTEGER DEFAULT 0,
|
|
error_message TEXT,
|
|
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_relay_status ON relay_tasks(status);
|
|
CREATE INDEX IF NOT EXISTS idx_relay_account ON relay_tasks(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_relay_provider ON relay_tasks(provider_id);
|
|
CREATE INDEX IF NOT EXISTS idx_relay_time ON relay_tasks(created_at);
|
|
-- idx_relay_day: Skipping because ::date on TIMESTAMPTZ is not IMMUTABLE
|
|
-- CREATE INDEX IF NOT EXISTS idx_relay_day ON relay_tasks((created_at::date));
|
|
|
|
CREATE TABLE IF NOT EXISTS config_items (
|
|
id TEXT PRIMARY KEY,
|
|
category TEXT NOT NULL,
|
|
key_path TEXT NOT NULL,
|
|
value_type TEXT NOT NULL,
|
|
current_value TEXT,
|
|
default_value TEXT,
|
|
source TEXT NOT NULL DEFAULT 'local',
|
|
description TEXT,
|
|
requires_restart BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(category, key_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_config_category ON config_items(category);
|
|
|
|
CREATE TABLE IF NOT EXISTS config_sync_log (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
client_fingerprint TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
config_keys TEXT NOT NULL,
|
|
client_values TEXT,
|
|
saas_values TEXT,
|
|
resolution TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sync_account ON config_sync_log(account_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS devices (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
device_id TEXT NOT NULL,
|
|
device_name TEXT,
|
|
platform TEXT,
|
|
app_version TEXT,
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_devices_account ON devices(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_devices_device_id ON devices(device_id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_devices_unique ON devices(account_id, device_id);
|
|
|
|
-- Prompt template master table
|
|
CREATE TABLE IF NOT EXISTS prompt_templates (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
category TEXT NOT NULL,
|
|
description TEXT,
|
|
source TEXT NOT NULL DEFAULT 'builtin',
|
|
current_version INTEGER NOT NULL DEFAULT 1,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_prompt_status ON prompt_templates(status);
|
|
|
|
-- Prompt versions table (immutable)
|
|
CREATE TABLE IF NOT EXISTS prompt_versions (
|
|
id TEXT PRIMARY KEY,
|
|
template_id TEXT NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
system_prompt TEXT,
|
|
user_prompt_template TEXT,
|
|
variables TEXT NOT NULL DEFAULT '[]',
|
|
changelog TEXT,
|
|
min_app_version TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(template_id, version)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_prompt_ver_template ON prompt_versions(template_id);
|
|
|
|
-- Client prompt sync status
|
|
CREATE TABLE IF NOT EXISTS prompt_sync_status (
|
|
device_id TEXT NOT NULL,
|
|
template_id TEXT NOT NULL,
|
|
synced_version INTEGER NOT NULL,
|
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY(device_id, template_id)
|
|
);
|
|
|
|
-- Provider Key Pool table
|
|
CREATE TABLE IF NOT EXISTS provider_keys (
|
|
id TEXT PRIMARY KEY,
|
|
provider_id TEXT NOT NULL,
|
|
key_label TEXT NOT NULL,
|
|
key_value TEXT NOT NULL,
|
|
priority INTEGER NOT NULL DEFAULT 0,
|
|
max_rpm BIGINT,
|
|
max_tpm BIGINT,
|
|
quota_reset_interval TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
last_429_at TIMESTAMPTZ,
|
|
cooldown_until TIMESTAMPTZ,
|
|
total_requests BIGINT NOT NULL DEFAULT 0,
|
|
total_tokens BIGINT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_pkeys_provider ON provider_keys(provider_id);
|
|
CREATE INDEX IF NOT EXISTS idx_pkeys_active ON provider_keys(provider_id, is_active);
|
|
|
|
-- Key usage sliding window
|
|
CREATE TABLE IF NOT EXISTS key_usage_window (
|
|
key_id TEXT NOT NULL,
|
|
window_minute TEXT NOT NULL,
|
|
request_count INTEGER NOT NULL DEFAULT 0,
|
|
token_count BIGINT NOT NULL DEFAULT 0,
|
|
PRIMARY KEY(key_id, window_minute)
|
|
);
|
|
|
|
-- Agent config template table
|
|
CREATE TABLE IF NOT EXISTS agent_templates (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
category TEXT NOT NULL DEFAULT 'general',
|
|
source TEXT NOT NULL DEFAULT 'builtin',
|
|
model TEXT,
|
|
system_prompt TEXT,
|
|
tools TEXT NOT NULL DEFAULT '[]'::text,
|
|
capabilities TEXT NOT NULL DEFAULT '[]'::text,
|
|
temperature DOUBLE PRECISION,
|
|
max_tokens INTEGER,
|
|
visibility TEXT NOT NULL DEFAULT 'public',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
current_version INTEGER NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_agent_tmpl_status ON agent_templates(status);
|
|
CREATE INDEX IF NOT EXISTS idx_agent_tmpl_visibility ON agent_templates(visibility);
|
|
|
|
-- Desktop telemetry report table (token usage statistics, no content)
|
|
CREATE TABLE IF NOT EXISTS telemetry_reports (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
device_id TEXT NOT NULL,
|
|
app_version TEXT,
|
|
model_id TEXT NOT NULL,
|
|
input_tokens BIGINT NOT NULL DEFAULT 0,
|
|
output_tokens BIGINT NOT NULL DEFAULT 0,
|
|
latency_ms INTEGER,
|
|
success BOOLEAN NOT NULL DEFAULT TRUE,
|
|
error_type TEXT,
|
|
connection_mode TEXT NOT NULL DEFAULT 'tauri',
|
|
reported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_account ON telemetry_reports(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_time ON telemetry_reports(reported_at);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_model ON telemetry_reports(model_id);
|
|
-- idx_telemetry_day: Skipping because ::date on TIMESTAMPTZ is not IMMUTABLE
|
|
-- CREATE INDEX IF NOT EXISTS idx_telemetry_day ON telemetry_reports((reported_at::date));
|
|
|
|
-- Refresh Token storage (single-use, JWT jti tracking)
|
|
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
jti TEXT NOT NULL UNIQUE,
|
|
token_hash TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_account ON refresh_tokens(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_jti ON refresh_tokens(jti);
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_expires ON refresh_tokens(expires_at);
|