0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
>4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
-- M8 基础设施:站内消息(Inbox)
|
||||
-- 通知 worker 将 outbox 事件物化为站内消息;管理员可广播;门户/管理员收件箱按行读回执。
|
||||
|
||||
-- inbox_messages:一条消息一个收件人(sender 广播时枚举收件人逐行落库)。
|
||||
-- recipient_kind + recipient_user_id 区分 admin/portal 两个身份表;不设 FK(跨表)。
|
||||
CREATE TABLE IF NOT EXISTS gateway.inbox_messages (
|
||||
id uuid PRIMARY KEY,
|
||||
source_event_id uuid, -- 来源 outbox 事件;广播无来源事件为 NULL
|
||||
recipient_kind text NOT NULL CHECK (recipient_kind IN ('admin', 'portal')),
|
||||
recipient_user_id uuid NOT NULL, -- 具体收件人(admin 或 portal 账号 id)
|
||||
sender_type text NOT NULL CHECK (sender_type IN ('system', 'admin', 'portal')),
|
||||
category text NOT NULL CHECK (category IN ('system', 'approval', 'task_result', 'resource')),
|
||||
title text NOT NULL CHECK (length(title) BETWEEN 1 AND 256),
|
||||
body text NOT NULL DEFAULT '' CHECK (length(body) <= 4000),
|
||||
link text NOT NULL DEFAULT '' CHECK (length(link) <= 512),
|
||||
payload jsonb,
|
||||
read_at timestamptz, -- 已读回执;NULL=未读
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
-- 幂等:同一来源事件对同一收件人只落一条(重放不重复);NULLS NOT DISTINCT 兜底 NULL 收件人。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS inbox_messages_source_event_idx
|
||||
ON gateway.inbox_messages (source_event_id, recipient_kind, recipient_user_id)
|
||||
NULLS NOT DISTINCT
|
||||
WHERE source_event_id IS NOT NULL;
|
||||
|
||||
-- 收件箱按收件人倒序 + 未读数
|
||||
CREATE INDEX IF NOT EXISTS inbox_messages_recipient_idx
|
||||
ON gateway.inbox_messages (recipient_kind, recipient_user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS inbox_messages_unread_idx
|
||||
ON gateway.inbox_messages (recipient_kind, recipient_user_id)
|
||||
WHERE read_at IS NULL;
|
||||
@@ -0,0 +1,56 @@
|
||||
-- M8 P3: PostgreSQL-backed scheduled task definitions and durable execution queue.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.scheduled_tasks (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{1,63}$'),
|
||||
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 128),
|
||||
description text NOT NULL DEFAULT '' CHECK (length(description) <= 4000),
|
||||
cron_expression text NOT NULL CHECK (length(cron_expression) <= 128),
|
||||
timezone text NOT NULL DEFAULT 'UTC' CHECK (length(timezone) <= 128),
|
||||
target_type text NOT NULL CHECK (target_type IN ('application', 'digital_employee')),
|
||||
target_code text NOT NULL CHECK (length(target_code) BETWEEN 1 AND 64),
|
||||
prompt text NOT NULL CHECK (length(prompt) BETWEEN 1 AND 100000),
|
||||
variables jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(variables) = 'object'),
|
||||
skill_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
mcp_server_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
conversation_id text NOT NULL DEFAULT '' CHECK (length(conversation_id) <= 128),
|
||||
notification_channel_id uuid REFERENCES gateway.notification_channels(id) ON DELETE SET NULL,
|
||||
encrypted_api_key bytea NOT NULL,
|
||||
api_key_kek_version integer NOT NULL CHECK (api_key_kek_version > 0),
|
||||
enabled boolean NOT NULL DEFAULT false,
|
||||
next_run_at timestamptz,
|
||||
last_run_at timestamptz,
|
||||
last_status text NOT NULL DEFAULT '' CHECK (last_status IN ('', 'success', 'failed')),
|
||||
last_error text NOT NULL DEFAULT '' CHECK (length(last_error) <= 4000),
|
||||
created_by uuid NOT NULL REFERENCES gateway.admin_accounts(id),
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CHECK ((enabled AND next_run_at IS NOT NULL) OR (NOT enabled))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS scheduled_tasks_due_idx
|
||||
ON gateway.scheduled_tasks (next_run_at, id) WHERE enabled;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.scheduled_task_runs (
|
||||
id uuid PRIMARY KEY,
|
||||
task_id uuid NOT NULL REFERENCES gateway.scheduled_tasks(id) ON DELETE CASCADE,
|
||||
trigger_type text NOT NULL CHECK (trigger_type IN ('scheduled', 'manual')),
|
||||
scheduled_for timestamptz NOT NULL,
|
||||
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'success', 'failed')),
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
worker_id text NOT NULL DEFAULT '' CHECK (length(worker_id) <= 128),
|
||||
started_at timestamptz,
|
||||
finished_at timestamptz,
|
||||
response jsonb,
|
||||
error text NOT NULL DEFAULT '' CHECK (length(error) <= 4000),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (task_id, trigger_type, scheduled_for)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS scheduled_task_runs_pending_idx
|
||||
ON gateway.scheduled_task_runs (created_at, id) WHERE status = 'pending';
|
||||
CREATE INDEX IF NOT EXISTS scheduled_task_runs_history_idx
|
||||
ON gateway.scheduled_task_runs (task_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS scheduled_task_runs_running_idx
|
||||
ON gateway.scheduled_task_runs (started_at) WHERE status = 'running';
|
||||
@@ -0,0 +1,65 @@
|
||||
-- M9: bounded observability for application and digital-employee runs.
|
||||
-- Trace rows intentionally store metadata only; prompts, tool arguments and
|
||||
-- model responses remain outside this table.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.agent_traces (
|
||||
id uuid PRIMARY KEY,
|
||||
request_id text NOT NULL,
|
||||
api_key_id uuid REFERENCES gateway.api_keys(id) ON DELETE SET NULL,
|
||||
tenant_id uuid,
|
||||
trace_type text NOT NULL CHECK (trace_type IN ('application', 'digital_employee')),
|
||||
target_id uuid,
|
||||
target_code text NOT NULL CHECK (length(target_code) BETWEEN 1 AND 128),
|
||||
conversation_id text NOT NULL DEFAULT '' CHECK (length(conversation_id) <= 128),
|
||||
status text NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'success', 'error')),
|
||||
started_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
finished_at timestamptz,
|
||||
latency_ms integer,
|
||||
retrieval_count integer NOT NULL DEFAULT 0 CHECK (retrieval_count >= 0),
|
||||
model_call_count integer NOT NULL DEFAULT 0 CHECK (model_call_count >= 0),
|
||||
tool_call_count integer NOT NULL DEFAULT 0 CHECK (tool_call_count >= 0),
|
||||
error text NOT NULL DEFAULT '' CHECK (length(error) <= 4000),
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object')
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS agent_traces_started_idx
|
||||
ON gateway.agent_traces (started_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS agent_traces_request_idx
|
||||
ON gateway.agent_traces (request_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS agent_traces_target_idx
|
||||
ON gateway.agent_traces (trace_type, target_code, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS agent_traces_status_idx
|
||||
ON gateway.agent_traces (status, started_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.agent_trace_spans (
|
||||
id uuid PRIMARY KEY,
|
||||
trace_id uuid NOT NULL REFERENCES gateway.agent_traces(id) ON DELETE CASCADE,
|
||||
parent_id uuid REFERENCES gateway.agent_trace_spans(id) ON DELETE SET NULL,
|
||||
span_type text NOT NULL CHECK (span_type IN ('model', 'tool', 'retrieval')),
|
||||
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 256),
|
||||
status text NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'success', 'error')),
|
||||
started_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
finished_at timestamptz,
|
||||
latency_ms integer,
|
||||
provider_code text,
|
||||
model text,
|
||||
input_tokens bigint NOT NULL DEFAULT 0 CHECK (input_tokens >= 0),
|
||||
output_tokens bigint NOT NULL DEFAULT 0 CHECK (output_tokens >= 0),
|
||||
round integer NOT NULL DEFAULT 0 CHECK (round >= 0),
|
||||
error text NOT NULL DEFAULT '' CHECK (length(error) <= 4000),
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object')
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS agent_trace_spans_trace_idx
|
||||
ON gateway.agent_trace_spans (trace_id, started_at, id);
|
||||
CREATE INDEX IF NOT EXISTS agent_trace_spans_type_idx
|
||||
ON gateway.agent_trace_spans (span_type, started_at DESC);
|
||||
|
||||
ALTER TABLE gateway.application_runs
|
||||
ADD COLUMN IF NOT EXISTS trace_id uuid REFERENCES gateway.agent_traces(id) ON DELETE SET NULL;
|
||||
ALTER TABLE gateway.digital_employee_runs
|
||||
ADD COLUMN IF NOT EXISTS trace_id uuid REFERENCES gateway.agent_traces(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS application_runs_trace_idx
|
||||
ON gateway.application_runs (trace_id) WHERE trace_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS digital_employee_runs_trace_idx
|
||||
ON gateway.digital_employee_runs (trace_id) WHERE trace_id IS NOT NULL;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- M9 P3: agent node registry and heartbeat metadata.
|
||||
-- Node tokens are stored as SHA-256 digests and are only returned at create /
|
||||
-- rotation time. Heartbeat payloads are bounded metadata, not task content.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gateway.agent_nodes (
|
||||
id uuid PRIMARY KEY,
|
||||
code text NOT NULL UNIQUE CHECK (length(code) BETWEEN 1 AND 128),
|
||||
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 128),
|
||||
description text NOT NULL DEFAULT '' CHECK (length(description) <= 4000),
|
||||
endpoint text NOT NULL DEFAULT '' CHECK (length(endpoint) <= 512),
|
||||
node_type text NOT NULL DEFAULT 'worker' CHECK (node_type IN ('worker', 'gateway', 'executor')),
|
||||
pool_type text NOT NULL DEFAULT 'private' CHECK (pool_type IN ('public', 'private')),
|
||||
pool_code text NOT NULL DEFAULT 'default' CHECK (length(pool_code) BETWEEN 1 AND 64),
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
token_prefix text NOT NULL CHECK (length(token_prefix) BETWEEN 4 AND 32),
|
||||
token_hash bytea NOT NULL CHECK (octet_length(token_hash) = 32),
|
||||
version text NOT NULL DEFAULT '' CHECK (length(version) <= 128),
|
||||
capabilities jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(capabilities) = 'object'),
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
|
||||
last_heartbeat_at timestamptz,
|
||||
last_heartbeat_ip inet,
|
||||
last_error text NOT NULL DEFAULT '' CHECK (length(last_error) <= 4000),
|
||||
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 INDEX IF NOT EXISTS agent_nodes_pool_idx
|
||||
ON gateway.agent_nodes (pool_type, pool_code, enabled, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS agent_nodes_heartbeat_idx
|
||||
ON gateway.agent_nodes (last_heartbeat_at DESC NULLS LAST, enabled);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 性能修复:API Key 认证热路径按 key_hash 单列查询。
|
||||
-- 原有 UNIQUE (key_prefix, key_hash) 复合索引以 key_prefix 开头,
|
||||
-- WHERE key_hash=$1 无法使用,每次代理请求都对 gateway.api_keys 全表扫描。
|
||||
-- 该索引同时为 UNIQUE 去重语义提供单列约束。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS api_keys_key_hash_idx
|
||||
ON gateway.api_keys (key_hash);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 成本核算修复:usage_daily 增加币种维度。
|
||||
-- 原表 cost_microunits 无币种列,配置多种货币价格后会把不同币种的成本
|
||||
-- 直接相加成单一数字,污染成本报表。
|
||||
-- 幂等:列不存在时添加;PK 重建后包含 currency。已有数据统一归入 USD
|
||||
-- (迁移前所有成本按旧逻辑混算,无法追溯拆分)。
|
||||
ALTER TABLE gateway.usage_daily
|
||||
ADD COLUMN IF NOT EXISTS currency char(3) NOT NULL DEFAULT 'USD';
|
||||
|
||||
ALTER TABLE gateway.usage_daily
|
||||
DROP CONSTRAINT IF EXISTS usage_daily_pkey;
|
||||
|
||||
ALTER TABLE gateway.usage_daily
|
||||
ADD CONSTRAINT usage_daily_pkey PRIMARY KEY (usage_date, api_key_id, provider_code, model, currency);
|
||||
Reference in New Issue
Block a user