AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
CREATE SCHEMA IF NOT EXISTS gateway;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.outbox_events (
|
||||
event_id uuid PRIMARY KEY,
|
||||
event_type text NOT NULL,
|
||||
event_version integer NOT NULL CHECK (event_version > 0),
|
||||
tenant_id uuid,
|
||||
aggregate_type text NOT NULL,
|
||||
aggregate_id text NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
trace_context jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
occurred_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
available_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
locked_at timestamptz,
|
||||
locked_by text,
|
||||
processed_at timestamptz,
|
||||
last_error text
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS outbox_events_pending_idx
|
||||
ON gateway.outbox_events (available_at, occurred_at)
|
||||
WHERE processed_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.event_consumptions (
|
||||
subscriber text NOT NULL,
|
||||
event_id uuid NOT NULL,
|
||||
consumed_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (subscriber, event_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.api_keys (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id uuid,
|
||||
name text NOT NULL,
|
||||
key_prefix varchar(16) NOT NULL,
|
||||
key_hash bytea NOT NULL,
|
||||
scopes text[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
expires_at timestamptz,
|
||||
last_used_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (key_prefix, key_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_tenant_idx ON gateway.api_keys (tenant_id, enabled);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.providers (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id uuid,
|
||||
code text NOT NULL,
|
||||
adapter text NOT NULL,
|
||||
base_url text NOT NULL,
|
||||
encrypted_credentials bytea NOT NULL,
|
||||
credential_kek_version integer NOT NULL,
|
||||
capabilities text[] NOT NULL DEFAULT '{}',
|
||||
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE NULLS NOT DISTINCT (tenant_id, code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.audit_events (
|
||||
id uuid NOT NULL,
|
||||
tenant_id uuid,
|
||||
request_id text NOT NULL,
|
||||
actor_id uuid,
|
||||
api_key_id uuid,
|
||||
provider_code text,
|
||||
model text,
|
||||
protocol text NOT NULL,
|
||||
status_code integer,
|
||||
prompt_tokens bigint CHECK (prompt_tokens IS NULL OR prompt_tokens >= 0),
|
||||
completion_tokens bigint CHECK (completion_tokens IS NULL OR completion_tokens >= 0),
|
||||
cost_microunits bigint CHECK (cost_microunits IS NULL OR cost_microunits >= 0),
|
||||
latency_ms integer CHECK (latency_ms IS NULL OR latency_ms >= 0),
|
||||
request_preview text CHECK (request_preview IS NULL OR octet_length(request_preview) <= 65536),
|
||||
response_preview text CHECK (response_preview IS NULL OR octet_length(response_preview) <= 65536),
|
||||
labels jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (id, recorded_at)
|
||||
) PARTITION BY RANGE (recorded_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.audit_events_default
|
||||
PARTITION OF gateway.audit_events DEFAULT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS audit_events_default_tenant_time_idx
|
||||
ON gateway.audit_events_default (tenant_id, recorded_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS audit_events_default_request_idx
|
||||
ON gateway.audit_events_default (request_id);
|
||||
|
||||
COMMENT ON TABLE gateway.audit_events IS
|
||||
'PostgreSQL baseline audit storage. Full unbounded bodies are deliberately not retained.';
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.departments (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id uuid,
|
||||
code text NOT NULL,
|
||||
name text NOT NULL,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE NULLS NOT DISTINCT (tenant_id, code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.admin_accounts (
|
||||
id uuid PRIMARY KEY,
|
||||
username varchar(64) NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
display_name varchar(64) NOT NULL DEFAULT '',
|
||||
role varchar(24) NOT NULL DEFAULT 'operator'
|
||||
CHECK (role IN ('superadmin', 'operator', 'auditor')),
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
failed_logins integer NOT NULL DEFAULT 0 CHECK (failed_logins >= 0),
|
||||
locked_until timestamptz,
|
||||
last_login timestamptz,
|
||||
encrypted_totp_secret bytea,
|
||||
totp_kek_version integer,
|
||||
totp_enabled boolean NOT NULL DEFAULT false,
|
||||
totp_last_step bigint,
|
||||
totp_backup_codes jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
totp_confirmed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS admin_accounts_username_lower_idx
|
||||
ON gateway.admin_accounts (lower(username));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.portal_users (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id uuid,
|
||||
account varchar(128) NOT NULL,
|
||||
name varchar(64) NOT NULL DEFAULT '',
|
||||
department_id uuid REFERENCES gateway.departments(id),
|
||||
password_hash text,
|
||||
auth_source varchar(24) NOT NULL DEFAULT 'local'
|
||||
CHECK (auth_source IN ('local', 'feishu', 'oidc', 'saml')),
|
||||
external_subject text,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
provisioning_status varchar(24) NOT NULL DEFAULT 'active'
|
||||
CHECK (provisioning_status IN ('active', 'pending', 'rejected')),
|
||||
failed_logins integer NOT NULL DEFAULT 0 CHECK (failed_logins >= 0),
|
||||
locked_until timestamptz,
|
||||
last_login timestamptz,
|
||||
encrypted_totp_secret bytea,
|
||||
totp_kek_version integer,
|
||||
totp_enabled boolean NOT NULL DEFAULT false,
|
||||
totp_last_step bigint,
|
||||
totp_backup_codes jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
totp_confirmed_at timestamptz,
|
||||
deleted_name_snapshot varchar(128) NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS portal_users_account_lower_idx
|
||||
ON gateway.portal_users (lower(account));
|
||||
CREATE INDEX IF NOT EXISTS portal_users_department_idx
|
||||
ON gateway.portal_users (department_id, active);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS portal_users_external_subject_idx
|
||||
ON gateway.portal_users (auth_source, external_subject)
|
||||
WHERE external_subject IS NOT NULL;
|
||||
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES gateway.portal_users(id),
|
||||
ADD COLUMN IF NOT EXISTS department_id uuid REFERENCES gateway.departments(id);
|
||||
|
||||
ALTER TABLE gateway.providers
|
||||
ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES gateway.admin_accounts(id);
|
||||
|
||||
COMMENT ON COLUMN gateway.admin_accounts.password_hash IS
|
||||
'PBKDF2-SHA256 self-describing hash; compatible with the Python gateway format.';
|
||||
COMMENT ON TABLE gateway.portal_users IS
|
||||
'Portal identities. Runtime API keys remain separate credentials.';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES gateway.admin_accounts(id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_created_by_idx
|
||||
ON gateway.api_keys (created_by, created_at DESC);
|
||||
|
||||
COMMENT ON COLUMN gateway.api_keys.created_by IS
|
||||
'Administrator that created the key. Key material is returned once and never stored in plaintext.';
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS providers_single_global_default_idx
|
||||
ON gateway.providers ((1))
|
||||
WHERE tenant_id IS NULL AND enabled AND config @> '{"default": true}'::jsonb;
|
||||
|
||||
COMMENT ON INDEX gateway.providers_single_global_default_idx IS
|
||||
'At most one enabled global provider may be the explicit runtime default.';
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.provider_models (
|
||||
id uuid PRIMARY KEY,
|
||||
provider_id uuid NOT NULL REFERENCES gateway.providers(id) ON DELETE CASCADE,
|
||||
provider_model_id text NOT NULL CHECK (
|
||||
length(provider_model_id) BETWEEN 1 AND 512
|
||||
),
|
||||
owned_by text NOT NULL DEFAULT '',
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
discovered_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
last_seen_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (provider_id, provider_model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS provider_models_provider_active_idx
|
||||
ON gateway.provider_models (provider_id, enabled, provider_model_id);
|
||||
|
||||
COMMENT ON TABLE gateway.provider_models IS
|
||||
'Provider model catalog snapshots. Missing upstream models are retained and disabled.';
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE gateway.admin_accounts
|
||||
ADD COLUMN IF NOT EXISTS permissions text[] NOT NULL DEFAULT '{}';
|
||||
|
||||
ALTER TABLE gateway.portal_users
|
||||
ADD COLUMN IF NOT EXISTS role varchar(24) NOT NULL DEFAULT 'member',
|
||||
ADD COLUMN IF NOT EXISTS permissions text[] NOT NULL DEFAULT '{}';
|
||||
|
||||
COMMENT ON COLUMN gateway.admin_accounts.permissions IS
|
||||
'Direct RBAC permission grants. Built-in role permissions are resolved in the application.';
|
||||
COMMENT ON COLUMN gateway.portal_users.permissions IS
|
||||
'Direct portal permission grants using the same resource:action convention.';
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE gateway.departments
|
||||
ADD COLUMN IF NOT EXISTS parent_id uuid REFERENCES gateway.departments(id),
|
||||
ADD COLUMN IF NOT EXISTS description text NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS departments_parent_active_idx
|
||||
ON gateway.departments (parent_id, active, code);
|
||||
|
||||
ALTER TABLE gateway.departments
|
||||
ADD CONSTRAINT departments_not_self_parent
|
||||
CHECK (parent_id IS NULL OR parent_id <> id);
|
||||
|
||||
COMMENT ON COLUMN gateway.departments.parent_id IS
|
||||
'Adjacency-list parent. Application writes reject cycles transactionally.';
|
||||
@@ -0,0 +1,31 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.identity_providers (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE,
|
||||
kind varchar(16) NOT NULL CHECK (kind IN ('oidc')),
|
||||
display_name varchar(128) NOT NULL,
|
||||
issuer_url text NOT NULL,
|
||||
client_id text NOT NULL,
|
||||
encrypted_credentials bytea NOT NULL,
|
||||
credential_kek_version integer NOT NULL,
|
||||
redirect_uri text NOT NULL,
|
||||
portal_return_url text NOT NULL,
|
||||
scopes text[] NOT NULL DEFAULT ARRAY['openid', 'profile', 'email'],
|
||||
auto_provision boolean NOT NULL DEFAULT false,
|
||||
default_department_id uuid REFERENCES gateway.departments(id),
|
||||
enabled boolean NOT NULL DEFAULT false,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
ALTER TABLE gateway.portal_users
|
||||
ADD COLUMN IF NOT EXISTS identity_provider_id uuid
|
||||
REFERENCES gateway.identity_providers(id);
|
||||
|
||||
DROP INDEX IF EXISTS gateway.portal_users_external_subject_idx;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS portal_users_provider_subject_idx
|
||||
ON gateway.portal_users (identity_provider_id, external_subject)
|
||||
WHERE identity_provider_id IS NOT NULL AND external_subject IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE gateway.identity_providers IS
|
||||
'OIDC provider registry. Client secrets are encrypted with a dedicated AEAD purpose.';
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.legacy_id_mappings (
|
||||
source_system varchar(64) NOT NULL,
|
||||
entity_type varchar(128) NOT NULL,
|
||||
legacy_id text NOT NULL,
|
||||
new_id uuid NOT NULL,
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
imported_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (source_system, entity_type, legacy_id),
|
||||
UNIQUE (source_system, entity_type, new_id),
|
||||
CHECK (length(source_system) BETWEEN 1 AND 64),
|
||||
CHECK (length(entity_type) BETWEEN 1 AND 128),
|
||||
CHECK (length(legacy_id) BETWEEN 1 AND 256)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS legacy_id_mappings_new_id_idx
|
||||
ON gateway.legacy_id_mappings (new_id);
|
||||
|
||||
COMMENT ON TABLE gateway.legacy_id_mappings IS
|
||||
'Idempotent compatibility map from legacy entity IDs to deterministic UUIDv5 IDs.';
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE gateway.identity_providers
|
||||
DROP CONSTRAINT IF EXISTS identity_providers_kind_check;
|
||||
|
||||
ALTER TABLE gateway.identity_providers
|
||||
ADD CONSTRAINT identity_providers_kind_check
|
||||
CHECK (kind IN ('oidc', 'saml')),
|
||||
ALTER COLUMN issuer_url DROP NOT NULL,
|
||||
ALTER COLUMN client_id DROP NOT NULL,
|
||||
ALTER COLUMN encrypted_credentials DROP NOT NULL,
|
||||
ALTER COLUMN credential_kek_version DROP NOT NULL,
|
||||
ALTER COLUMN redirect_uri DROP NOT NULL,
|
||||
ADD COLUMN IF NOT EXISTS config jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN gateway.identity_providers.config IS
|
||||
'Kind-specific non-secret settings. SAML metadata URL, SP entity ID, ACS URL and attribute names live here.';
|
||||
@@ -0,0 +1,18 @@
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD COLUMN IF NOT EXISTS requests_per_minute integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS monthly_request_quota bigint NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE gateway.api_keys
|
||||
DROP CONSTRAINT IF EXISTS api_keys_requests_per_minute_check,
|
||||
DROP CONSTRAINT IF EXISTS api_keys_monthly_request_quota_check;
|
||||
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD CONSTRAINT api_keys_requests_per_minute_check
|
||||
CHECK (requests_per_minute BETWEEN 0 AND 1000000),
|
||||
ADD CONSTRAINT api_keys_monthly_request_quota_check
|
||||
CHECK (monthly_request_quota BETWEEN 0 AND 1000000000000);
|
||||
|
||||
COMMENT ON COLUMN gateway.api_keys.requests_per_minute IS
|
||||
'Atomic Redis-enforced request limit per UTC minute. Zero means unlimited.';
|
||||
COMMENT ON COLUMN gateway.api_keys.monthly_request_quota IS
|
||||
'Atomic Redis-enforced accepted request quota per UTC calendar month. Zero means unlimited.';
|
||||
@@ -0,0 +1,12 @@
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD COLUMN IF NOT EXISTS monthly_token_quota bigint NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE gateway.api_keys
|
||||
DROP CONSTRAINT IF EXISTS api_keys_monthly_token_quota_check;
|
||||
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD CONSTRAINT api_keys_monthly_token_quota_check
|
||||
CHECK (monthly_token_quota BETWEEN 0 AND 1000000000000000);
|
||||
|
||||
COMMENT ON COLUMN gateway.api_keys.monthly_token_quota IS
|
||||
'Maximum input plus output tokens per UTC calendar month; zero means unlimited.';
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.model_routes (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 128),
|
||||
source_model text NOT NULL CHECK (length(source_model) BETWEEN 1 AND 512),
|
||||
target_model text NOT NULL CHECK (length(target_model) BETWEEN 1 AND 512),
|
||||
provider_id uuid NOT NULL REFERENCES gateway.providers(id) ON DELETE CASCADE,
|
||||
weight integer NOT NULL DEFAULT 100 CHECK (weight BETWEEN 1 AND 10000),
|
||||
priority integer NOT NULL DEFAULT 100 CHECK (priority BETWEEN -100000 AND 100000),
|
||||
conditions jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (source_model, provider_id, target_model, priority, conditions)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS model_routes_lookup_idx
|
||||
ON gateway.model_routes (source_model, enabled, priority DESC);
|
||||
|
||||
COMMENT ON COLUMN gateway.model_routes.conditions IS
|
||||
'Extensible route predicates; initial runtime supports endpoint, API key and tenant matching.';
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE INDEX IF NOT EXISTS audit_events_recorded_at_idx
|
||||
ON gateway.audit_events (recorded_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS audit_events_api_key_time_idx
|
||||
ON gateway.audit_events (api_key_id, recorded_at DESC)
|
||||
WHERE api_key_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS audit_events_provider_model_time_idx
|
||||
ON gateway.audit_events (provider_code, model, recorded_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.usage_daily (
|
||||
usage_date date NOT NULL,
|
||||
api_key_id uuid NOT NULL REFERENCES gateway.api_keys(id),
|
||||
provider_code text NOT NULL DEFAULT '',
|
||||
model text NOT NULL DEFAULT '',
|
||||
requests bigint NOT NULL DEFAULT 0 CHECK (requests >= 0),
|
||||
failed_requests bigint NOT NULL DEFAULT 0 CHECK (failed_requests >= 0),
|
||||
prompt_tokens bigint NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0),
|
||||
completion_tokens bigint NOT NULL DEFAULT 0 CHECK (completion_tokens >= 0),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (usage_date, api_key_id, provider_code, model)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS usage_daily_date_idx
|
||||
ON gateway.usage_daily (usage_date DESC, provider_code, model);
|
||||
|
||||
COMMENT ON TABLE gateway.usage_daily IS
|
||||
'PostgreSQL daily usage aggregates produced asynchronously from gateway audit events.';
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE gateway.outbox_events
|
||||
ADD COLUMN IF NOT EXISTS dead_lettered_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS published_stream_id text;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS outbox_events_claimable_idx
|
||||
ON gateway.outbox_events (available_at, occurred_at)
|
||||
WHERE processed_at IS NULL AND dead_lettered_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS outbox_events_dead_letter_idx
|
||||
ON gateway.outbox_events (dead_lettered_at DESC)
|
||||
WHERE dead_lettered_at IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN gateway.outbox_events.published_stream_id IS
|
||||
'Redis Stream entry ID, or duplicate when the event marker proved it was already published.';
|
||||
@@ -0,0 +1,60 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.content_policies (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
description text NOT NULL DEFAULT '',
|
||||
action text NOT NULL CHECK (action IN ('audit', 'block', 'redact')),
|
||||
priority integer NOT NULL DEFAULT 0,
|
||||
paths text[] NOT NULL DEFAULT '{}',
|
||||
models text[] NOT NULL DEFAULT '{}',
|
||||
api_key_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
rules jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CHECK (jsonb_typeof(rules) = 'array')
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS content_policies_runtime_idx
|
||||
ON gateway.content_policies (enabled, priority DESC, name);
|
||||
|
||||
COMMENT ON TABLE gateway.content_policies IS
|
||||
'Ordered request content policies compiled with Go RE2; only textual prompt fields are inspected or redacted.';
|
||||
|
||||
INSERT INTO gateway.content_policies
|
||||
(id, name, description, action, priority, rules, enabled)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000016', '默认敏感信息脱敏',
|
||||
'对提示词文本中的常见 API 密钥、访问令牌、密码和 secret 赋值执行脱敏。',
|
||||
'redact', 1000,
|
||||
'[{"name":"OpenAI 风格密钥","pattern":"sk-[A-Za-z0-9_-]{16,}","replacement":"[REDACTED_API_KEY]"},{"name":"凭据赋值","pattern":"(?i)(api[_-]?key|access[_-]?token|password|secret)[[:space:]]*[:=][[:space:]]*[\"'']?([A-Za-z0-9_./+=-]{6,})","replacement":"$1=[REDACTED]"}]'::jsonb,
|
||||
true)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.model_prices (
|
||||
id uuid PRIMARY KEY,
|
||||
provider_code text NOT NULL,
|
||||
model_pattern text NOT NULL,
|
||||
input_microunits_per_million bigint NOT NULL CHECK (input_microunits_per_million >= 0),
|
||||
output_microunits_per_million bigint NOT NULL CHECK (output_microunits_per_million >= 0),
|
||||
currency char(3) NOT NULL DEFAULT 'USD' CHECK (currency = upper(currency)),
|
||||
effective_from timestamptz NOT NULL,
|
||||
effective_to timestamptz,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CHECK (effective_to IS NULL OR effective_to > effective_from),
|
||||
UNIQUE (provider_code, model_pattern, effective_from)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS model_prices_runtime_idx
|
||||
ON gateway.model_prices (provider_code, enabled, effective_from DESC);
|
||||
|
||||
ALTER TABLE gateway.usage_daily
|
||||
ADD COLUMN IF NOT EXISTS cost_microunits bigint NOT NULL DEFAULT 0 CHECK (cost_microunits >= 0);
|
||||
|
||||
COMMENT ON COLUMN gateway.audit_events.cost_microunits IS
|
||||
'Estimated request cost in one-millionth currency units using the price version effective at request time.';
|
||||
@@ -0,0 +1,212 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.prompt_categories (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
description text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.prompt_templates (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
description text NOT NULL DEFAULT '',
|
||||
category_id uuid REFERENCES gateway.prompt_categories(id) ON DELETE SET NULL,
|
||||
tags text[] NOT NULL DEFAULT '{}',
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
current_version integer,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.prompt_versions (
|
||||
id uuid PRIMARY KEY,
|
||||
template_id uuid NOT NULL REFERENCES gateway.prompt_templates(id) ON DELETE CASCADE,
|
||||
version integer NOT NULL CHECK (version > 0),
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 100000),
|
||||
variables jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(variables) = 'array'),
|
||||
change_note text NOT NULL DEFAULT '',
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (template_id, version)
|
||||
);
|
||||
|
||||
ALTER TABLE gateway.prompt_templates
|
||||
ADD CONSTRAINT prompt_templates_current_version_fk
|
||||
FOREIGN KEY (id, current_version)
|
||||
REFERENCES gateway.prompt_versions(template_id, version)
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS prompt_templates_visible_idx
|
||||
ON gateway.prompt_templates (enabled, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.knowledge_bases (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
description text NOT NULL DEFAULT '',
|
||||
retrieval_mode text NOT NULL DEFAULT 'postgres_fts'
|
||||
CHECK (retrieval_mode IN ('postgres_fts')),
|
||||
chunk_size integer NOT NULL DEFAULT 800 CHECK (chunk_size BETWEEN 200 AND 8000),
|
||||
chunk_overlap integer NOT NULL DEFAULT 100 CHECK (chunk_overlap >= 0 AND chunk_overlap <= chunk_size / 2),
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.knowledge_documents (
|
||||
id uuid PRIMARY KEY,
|
||||
knowledge_base_id uuid NOT NULL REFERENCES gateway.knowledge_bases(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
source_type text NOT NULL DEFAULT 'text' CHECK (source_type IN ('text', 'url', 'import')),
|
||||
source_uri text NOT NULL DEFAULT '',
|
||||
content text NOT NULL CHECK (octet_length(content) <= 2097152),
|
||||
content_sha256 char(64) NOT NULL,
|
||||
status text NOT NULL DEFAULT 'ready' CHECK (status IN ('ready', 'failed')),
|
||||
status_message text NOT NULL DEFAULT '',
|
||||
chunk_count integer NOT NULL DEFAULT 0 CHECK (chunk_count >= 0),
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (knowledge_base_id, content_sha256)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.knowledge_chunks (
|
||||
id uuid PRIMARY KEY,
|
||||
knowledge_base_id uuid NOT NULL REFERENCES gateway.knowledge_bases(id) ON DELETE CASCADE,
|
||||
document_id uuid NOT NULL REFERENCES gateway.knowledge_documents(id) ON DELETE CASCADE,
|
||||
chunk_index integer NOT NULL CHECK (chunk_index >= 0),
|
||||
content text NOT NULL,
|
||||
search_vector tsvector GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (document_id, chunk_index)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS knowledge_chunks_search_idx
|
||||
ON gateway.knowledge_chunks USING gin (search_vector);
|
||||
CREATE INDEX IF NOT EXISTS knowledge_chunks_base_idx
|
||||
ON gateway.knowledge_chunks (knowledge_base_id, document_id, chunk_index);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.tool_definitions (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{1,63}$'),
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
endpoint_url text NOT NULL,
|
||||
http_method text NOT NULL DEFAULT 'POST' CHECK (http_method IN ('GET','POST','PUT','PATCH','DELETE')),
|
||||
encrypted_headers bytea NOT NULL DEFAULT ''::bytea,
|
||||
headers_kek_version integer NOT NULL DEFAULT 1,
|
||||
input_schema jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(input_schema) = 'object'),
|
||||
timeout_seconds integer NOT NULL DEFAULT 15 CHECK (timeout_seconds BETWEEN 1 AND 120),
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.applications (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{2,63}$'),
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','active','suspended','retired')),
|
||||
draft_config jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(draft_config) = 'object'),
|
||||
published_version integer CHECK (published_version > 0),
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.tool_runs (
|
||||
id uuid PRIMARY KEY,
|
||||
tool_id uuid NOT NULL REFERENCES gateway.tool_definitions(id) ON DELETE CASCADE,
|
||||
api_key_id uuid REFERENCES gateway.api_keys(id) ON DELETE SET NULL,
|
||||
request_id text NOT NULL,
|
||||
status text NOT NULL CHECK (status IN ('success','error')),
|
||||
response_status integer,
|
||||
latency_ms bigint NOT NULL DEFAULT 0,
|
||||
error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS tool_runs_tool_time_idx
|
||||
ON gateway.tool_runs (tool_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.application_versions (
|
||||
id uuid PRIMARY KEY,
|
||||
application_id uuid NOT NULL REFERENCES gateway.applications(id) ON DELETE CASCADE,
|
||||
version integer NOT NULL CHECK (version > 0),
|
||||
config jsonb NOT NULL CHECK (jsonb_typeof(config) = 'object'),
|
||||
change_note text NOT NULL DEFAULT '',
|
||||
published_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (application_id, version)
|
||||
);
|
||||
|
||||
ALTER TABLE gateway.applications
|
||||
ADD CONSTRAINT applications_published_version_fk
|
||||
FOREIGN KEY (id, published_version)
|
||||
REFERENCES gateway.application_versions(application_id, version)
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.application_runs (
|
||||
id uuid PRIMARY KEY,
|
||||
application_id uuid NOT NULL REFERENCES gateway.applications(id) ON DELETE CASCADE,
|
||||
version integer NOT NULL,
|
||||
api_key_id uuid REFERENCES gateway.api_keys(id) ON DELETE SET NULL,
|
||||
request_id text NOT NULL,
|
||||
status text NOT NULL CHECK (status IN ('success','error')),
|
||||
latency_ms bigint NOT NULL DEFAULT 0,
|
||||
retrieval_count integer NOT NULL DEFAULT 0,
|
||||
tool_count integer NOT NULL DEFAULT 0,
|
||||
error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS application_runs_app_time_idx
|
||||
ON gateway.application_runs (application_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.notification_channels (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
webhook_url text NOT NULL,
|
||||
encrypted_signing_secret bytea NOT NULL DEFAULT ''::bytea,
|
||||
signing_secret_kek_version integer NOT NULL DEFAULT 1,
|
||||
event_patterns text[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.notification_deliveries (
|
||||
id uuid PRIMARY KEY,
|
||||
channel_id uuid NOT NULL REFERENCES gateway.notification_channels(id) ON DELETE CASCADE,
|
||||
event_id uuid NOT NULL,
|
||||
event_type text NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
status text NOT NULL CHECK (status IN ('pending','delivered','failed')),
|
||||
attempts integer NOT NULL DEFAULT 0,
|
||||
response_status integer,
|
||||
last_error text NOT NULL DEFAULT '',
|
||||
delivered_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (channel_id, event_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS notification_deliveries_status_idx
|
||||
ON gateway.notification_deliveries (status, updated_at DESC);
|
||||
|
||||
COMMENT ON TABLE gateway.knowledge_documents IS
|
||||
'Baseline stores bounded extracted text in PostgreSQL; blob/object storage is deliberately not required.';
|
||||
COMMENT ON TABLE gateway.application_versions IS
|
||||
'Immutable published application compositions referencing prompt, knowledge and tool assets.';
|
||||
@@ -0,0 +1,45 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.legacy_import_batches (
|
||||
id uuid PRIMARY KEY,
|
||||
source_system text NOT NULL CHECK (length(source_system) BETWEEN 1 AND 64),
|
||||
source_checksum char(64) NOT NULL,
|
||||
status text NOT NULL CHECK (status IN ('staged','validated','applied','failed')),
|
||||
record_count integer NOT NULL CHECK (record_count >= 0),
|
||||
entity_counts jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
completed_at timestamptz,
|
||||
UNIQUE (source_system, source_checksum)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.legacy_import_records (
|
||||
first_seen_batch_id uuid NOT NULL REFERENCES gateway.legacy_import_batches(id),
|
||||
source_system text NOT NULL,
|
||||
entity_type text NOT NULL,
|
||||
legacy_id text NOT NULL,
|
||||
new_id uuid NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
payload_checksum char(64) NOT NULL,
|
||||
target_table text NOT NULL DEFAULT '',
|
||||
applied_at timestamptz,
|
||||
error text NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (source_system, entity_type, legacy_id),
|
||||
UNIQUE (source_system, entity_type, new_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.legacy_import_batch_records (
|
||||
batch_id uuid NOT NULL REFERENCES gateway.legacy_import_batches(id) ON DELETE CASCADE,
|
||||
source_system text NOT NULL,
|
||||
entity_type text NOT NULL,
|
||||
legacy_id text NOT NULL,
|
||||
payload_checksum char(64) NOT NULL,
|
||||
PRIMARY KEY (batch_id, entity_type, legacy_id),
|
||||
FOREIGN KEY (source_system, entity_type, legacy_id)
|
||||
REFERENCES gateway.legacy_import_records(source_system, entity_type, legacy_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS legacy_import_records_pending_idx
|
||||
ON gateway.legacy_import_records (entity_type, legacy_id)
|
||||
WHERE applied_at IS NULL AND error = '';
|
||||
|
||||
COMMENT ON TABLE gateway.legacy_import_records IS
|
||||
'Validated immutable staging records. Sensitive legacy ciphertext is never treated as a usable new credential; domain transforms must decrypt and re-encrypt with the new purpose label.';
|
||||
@@ -0,0 +1,38 @@
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD COLUMN IF NOT EXISTS portal_user_id uuid REFERENCES gateway.portal_users(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_portal_user_idx
|
||||
ON gateway.api_keys (portal_user_id, enabled, created_at DESC)
|
||||
WHERE portal_user_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.prompt_favorites (
|
||||
portal_user_id uuid NOT NULL REFERENCES gateway.portal_users(id) ON DELETE CASCADE,
|
||||
prompt_id uuid NOT NULL REFERENCES gateway.prompt_templates(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (portal_user_id, prompt_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.model_access_requests (
|
||||
id uuid PRIMARY KEY,
|
||||
portal_user_id uuid NOT NULL REFERENCES gateway.portal_users(id) ON DELETE CASCADE,
|
||||
provider_code text NOT NULL DEFAULT '',
|
||||
model text NOT NULL CHECK (length(model) BETWEEN 1 AND 512),
|
||||
reason text NOT NULL DEFAULT '' CHECK (length(reason) <= 4000),
|
||||
requested_rpm integer NOT NULL DEFAULT 60 CHECK (requested_rpm BETWEEN 1 AND 100000),
|
||||
requested_monthly_tokens bigint NOT NULL DEFAULT 0 CHECK (requested_monthly_tokens >= 0),
|
||||
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected','cancelled')),
|
||||
decision_note text NOT NULL DEFAULT '' CHECK (length(decision_note) <= 4000),
|
||||
decided_by uuid REFERENCES gateway.admin_accounts(id) ON DELETE SET NULL,
|
||||
decided_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS model_access_requests_one_pending_idx
|
||||
ON gateway.model_access_requests (portal_user_id, provider_code, model)
|
||||
WHERE status = 'pending';
|
||||
CREATE INDEX IF NOT EXISTS model_access_requests_status_time_idx
|
||||
ON gateway.model_access_requests (status, created_at DESC);
|
||||
|
||||
COMMENT ON COLUMN gateway.api_keys.portal_user_id IS
|
||||
'Optional portal owner. It enables least-privilege self-service usage and audit views without exposing key material.';
|
||||
@@ -0,0 +1,49 @@
|
||||
ALTER TABLE gateway.api_keys
|
||||
ADD COLUMN IF NOT EXISTS application_id uuid REFERENCES gateway.applications(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_application_idx
|
||||
ON gateway.api_keys (application_id, enabled)
|
||||
WHERE application_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.application_runtime_credentials (
|
||||
application_id uuid NOT NULL REFERENCES gateway.applications(id) ON DELETE CASCADE,
|
||||
department_id uuid REFERENCES gateway.departments(id) ON DELETE CASCADE,
|
||||
api_key_id uuid NOT NULL UNIQUE REFERENCES gateway.api_keys(id) ON DELETE CASCADE,
|
||||
encrypted_key bytea NOT NULL,
|
||||
key_kek_version integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE NULLS NOT DISTINCT (application_id, department_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.portal_conversations (
|
||||
id uuid PRIMARY KEY,
|
||||
portal_user_id uuid NOT NULL REFERENCES gateway.portal_users(id) ON DELETE CASCADE,
|
||||
application_id uuid NOT NULL REFERENCES gateway.applications(id) ON DELETE CASCADE,
|
||||
title text NOT NULL DEFAULT '新会话' CHECK (length(title) BETWEEN 1 AND 160),
|
||||
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','archived')),
|
||||
busy boolean NOT NULL DEFAULT false,
|
||||
busy_token uuid,
|
||||
busy_since timestamptz,
|
||||
next_sequence integer NOT NULL DEFAULT 1 CHECK (next_sequence BETWEEN 1 AND 202),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS portal_conversations_user_time_idx
|
||||
ON gateway.portal_conversations (portal_user_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.portal_conversation_messages (
|
||||
id uuid PRIMARY KEY,
|
||||
conversation_id uuid NOT NULL REFERENCES gateway.portal_conversations(id) ON DELETE CASCADE,
|
||||
sequence integer NOT NULL CHECK (sequence BETWEEN 1 AND 200),
|
||||
role text NOT NULL CHECK (role IN ('user','assistant')),
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 100000),
|
||||
previous_hash char(64) NOT NULL,
|
||||
message_hash char(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (conversation_id, sequence)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE gateway.portal_conversation_messages IS
|
||||
'Server-managed immutable conversation history. A SHA-256 hash chain detects database-side sequence/content corruption before replay.';
|
||||
@@ -0,0 +1,51 @@
|
||||
CREATE TABLE IF NOT EXISTS gateway.fact_check_settings (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
provider_id uuid REFERENCES gateway.providers(id) ON DELETE SET NULL,
|
||||
model text NOT NULL DEFAULT '',
|
||||
timeout_seconds integer NOT NULL DEFAULT 15 CHECK (timeout_seconds BETWEEN 3 AND 60),
|
||||
updated_by uuid REFERENCES gateway.admin_accounts(id) ON DELETE SET NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
INSERT INTO gateway.fact_check_settings(singleton) VALUES(true) ON CONFLICT DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.fact_check_policies (
|
||||
id uuid PRIMARY KEY,
|
||||
scope text NOT NULL UNIQUE CHECK (scope='global' OR scope ~ '^(department|application|api_key):[0-9a-f-]{36}$'),
|
||||
enabled boolean NOT NULL DEFAULT false,
|
||||
mode text NOT NULL DEFAULT 'async' CHECK (mode IN ('async','sync')),
|
||||
action text NOT NULL DEFAULT 'observe' CHECK (action IN ('observe','annotate','block')),
|
||||
knowledge_base_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
support_threshold integer NOT NULL DEFAULT 70 CHECK (support_threshold BETWEEN 0 AND 100),
|
||||
evidence_threshold double precision NOT NULL DEFAULT 0.35 CHECK (evidence_threshold BETWEEN 0 AND 1),
|
||||
top_k integer NOT NULL DEFAULT 4 CHECK (top_k BETWEEN 1 AND 10),
|
||||
max_claims integer NOT NULL DEFAULT 8 CHECK (max_claims BETWEEN 1 AND 20),
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CHECK (mode='sync' OR action='observe'),
|
||||
CHECK (NOT enabled OR cardinality(knowledge_base_ids)>0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.fact_check_events (
|
||||
id uuid PRIMARY KEY,
|
||||
policy_id uuid REFERENCES gateway.fact_check_policies(id) ON DELETE SET NULL,
|
||||
request_id text NOT NULL,
|
||||
model text NOT NULL DEFAULT '',
|
||||
mode text NOT NULL CHECK (mode IN ('async','sync')),
|
||||
action text NOT NULL CHECK (action IN ('observe','annotate','block')),
|
||||
verdict text NOT NULL CHECK (verdict IN ('supported','unsupported','uncertain','error')),
|
||||
support_score integer CHECK (support_score BETWEEN 0 AND 100),
|
||||
latency_ms integer NOT NULL DEFAULT 0 CHECK (latency_ms >= 0),
|
||||
question text NOT NULL DEFAULT '' CHECK (octet_length(question)<=65536),
|
||||
answer text NOT NULL DEFAULT '' CHECK (octet_length(answer)<=65536),
|
||||
claims jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(claims)='array'),
|
||||
evidence jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(evidence)='array'),
|
||||
error text NOT NULL DEFAULT '' CHECK (length(error)<=4000),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS fact_check_events_time_idx ON gateway.fact_check_events(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS fact_check_events_verdict_time_idx ON gateway.fact_check_events(verdict,created_at DESC);
|
||||
|
||||
COMMENT ON COLUMN gateway.fact_check_settings.provider_id IS
|
||||
'Reuses the encrypted Provider credential subsystem; fact-check settings never duplicate plaintext API keys or upstream URLs.';
|
||||
@@ -0,0 +1,130 @@
|
||||
-- 旗舰版资源市场:MCP 服务器 / Skills / 数字员工
|
||||
-- 三类可发布资产 + 共享分类 + 市场安装(工作区绑定 + 权限)。
|
||||
|
||||
-- 共享市场分类(resource_type = '' 表示全局分类,适用所有资源类型)
|
||||
CREATE TABLE IF NOT EXISTS gateway.marketplace_categories (
|
||||
id uuid PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
description text NOT NULL DEFAULT '',
|
||||
resource_type text NOT NULL DEFAULT ''
|
||||
CHECK (resource_type IN ('', 'mcp_server', 'skill', 'digital_employee')),
|
||||
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
-- MCP 服务器:连接外部 MCP 服务器的注册信息(transport/endpoint/凭证)
|
||||
CREATE TABLE IF NOT EXISTS gateway.mcp_servers (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{1,63}$'),
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
transport text NOT NULL DEFAULT 'streamable-http'
|
||||
CHECK (transport IN ('streamable-http', 'sse')),
|
||||
endpoint_url text NOT NULL,
|
||||
encrypted_headers bytea NOT NULL DEFAULT ''::bytea,
|
||||
headers_kek_version integer NOT NULL DEFAULT 1,
|
||||
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
|
||||
category_id uuid REFERENCES gateway.marketplace_categories(id) ON DELETE SET NULL,
|
||||
tags text[] NOT NULL DEFAULT '{}',
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
-- Skills:带绑定的提示词包(content + variables + 工具/MCP/知识库绑定)
|
||||
CREATE TABLE IF NOT EXISTS gateway.skills (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{1,63}$'),
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 100000),
|
||||
variables jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(variables) = 'array'),
|
||||
tool_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
mcp_server_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
knowledge_base_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
|
||||
category_id uuid REFERENCES gateway.marketplace_categories(id) ON DELETE SET NULL,
|
||||
tags text[] NOT NULL DEFAULT '{}',
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
-- 数字员工:复合编排(persona + 模型 + 技能/工具/MCP/知识库绑定 + 生成参数)
|
||||
CREATE TABLE IF NOT EXISTS gateway.digital_employees (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{2,63}$'),
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
persona text NOT NULL DEFAULT '',
|
||||
model text NOT NULL DEFAULT '',
|
||||
skill_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
tool_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
mcp_server_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
knowledge_base_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
temperature numeric NOT NULL DEFAULT 0.7 CHECK (temperature >= 0 AND temperature <= 2),
|
||||
retrieval_top_k integer NOT NULL DEFAULT 5 CHECK (retrieval_top_k BETWEEN 1 AND 50),
|
||||
max_tool_rounds integer NOT NULL DEFAULT 5 CHECK (max_tool_rounds BETWEEN 1 AND 20),
|
||||
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
|
||||
category_id uuid REFERENCES gateway.marketplace_categories(id) ON DELETE SET NULL,
|
||||
tags text[] NOT NULL DEFAULT '{}',
|
||||
department_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_by uuid REFERENCES gateway.admin_accounts(id),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.digital_employee_runs (
|
||||
id uuid PRIMARY KEY,
|
||||
digital_employee_id uuid NOT NULL REFERENCES gateway.digital_employees(id) ON DELETE CASCADE,
|
||||
api_key_id uuid REFERENCES gateway.api_keys(id) ON DELETE SET NULL,
|
||||
request_id text NOT NULL,
|
||||
status text NOT NULL CHECK (status IN ('success', 'error')),
|
||||
latency_ms bigint NOT NULL DEFAULT 0,
|
||||
retrieval_count integer NOT NULL DEFAULT 0,
|
||||
tool_count integer NOT NULL DEFAULT 0,
|
||||
error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS digital_employee_runs_de_time_idx
|
||||
ON gateway.digital_employee_runs (digital_employee_id, created_at DESC);
|
||||
|
||||
-- 市场安装:portal 用户工作区绑定(资源权限:跨部门使用需先安装)
|
||||
CREATE TABLE IF NOT EXISTS gateway.marketplace_installations (
|
||||
id uuid PRIMARY KEY,
|
||||
resource_type text NOT NULL
|
||||
CHECK (resource_type IN ('mcp_server', 'skill', 'digital_employee')),
|
||||
resource_id uuid NOT NULL,
|
||||
portal_user_id uuid NOT NULL REFERENCES gateway.portal_users(id) ON DELETE CASCADE,
|
||||
config_override jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(config_override) = 'object'),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (resource_type, resource_id, portal_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS marketplace_installations_user_idx
|
||||
ON gateway.marketplace_installations (portal_user_id);
|
||||
|
||||
-- 市场可见性:published + enabled 的资源按更新时间倒序
|
||||
CREATE INDEX IF NOT EXISTS mcp_servers_visible_idx
|
||||
ON gateway.mcp_servers (status, enabled, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS skills_visible_idx
|
||||
ON gateway.skills (status, enabled, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS digital_employees_visible_idx
|
||||
ON gateway.digital_employees (status, enabled, updated_at DESC);
|
||||
|
||||
COMMENT ON TABLE gateway.mcp_servers IS
|
||||
'Marketplace MCP server registrations (streamable-http/sse). Tools are discovered live via the MCP client, not stored.';
|
||||
COMMENT ON TABLE gateway.digital_employees IS
|
||||
'Composite digital employees: persona + model + bound skills/tools/MCP servers/knowledge bases.';
|
||||
COMMENT ON TABLE gateway.marketplace_installations IS
|
||||
'Portal workspace bindings granting cross-department access to published marketplace resources.';
|
||||
Reference in New Issue
Block a user