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:
ben
2026-08-12 11:45:54 +08:00
commit 5759c1862e
807 changed files with 114727 additions and 0 deletions
+97
View File
@@ -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.';
+82
View File
@@ -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.';
+8
View File
@@ -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.';
+6
View File
@@ -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.';
+20
View File
@@ -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.';
+13
View File
@@ -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.';
+31
View File
@@ -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.';
+19
View File
@@ -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.';
+15
View File
@@ -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.';
+18
View File
@@ -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.';
+12
View File
@@ -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.';
+21
View File
@@ -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.';
+26
View File
@@ -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.';
+14
View File
@@ -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.';
+38
View File
@@ -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.';
+51
View File
@@ -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.';
+130
View File
@@ -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.';