feat(compliance): add content compliance detection across tenant, ops, and clients
Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web content-safety pages, admin-web editor/publish gate integration, desktop publish block surfacing, and supporting migrations / shared types / config plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
DROP TABLE IF EXISTS ops.compliance_policies;
|
||||
DROP TABLE IF EXISTS ops.compliance_dictionary_versions;
|
||||
DROP TABLE IF EXISTS ops.compliance_dictionary_terms;
|
||||
DROP TABLE IF EXISTS ops.compliance_dictionaries;
|
||||
@@ -0,0 +1,84 @@
|
||||
CREATE TABLE IF NOT EXISTS ops.compliance_dictionaries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
platform_scope VARCHAR(16) NOT NULL DEFAULT 'all',
|
||||
applicable_platforms TEXT[],
|
||||
default_level VARCHAR(16) NOT NULL DEFAULT 'block',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
description TEXT,
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uk_compliance_dictionaries_code UNIQUE (code),
|
||||
CONSTRAINT ck_compliance_dictionary_platform_scope CHECK (platform_scope IN ('all', 'specific')),
|
||||
CONSTRAINT ck_compliance_dictionary_platforms CHECK (
|
||||
(platform_scope = 'all' AND (applicable_platforms IS NULL OR cardinality(applicable_platforms) = 0))
|
||||
OR
|
||||
(platform_scope = 'specific' AND cardinality(applicable_platforms) >= 1)
|
||||
),
|
||||
CONSTRAINT ck_compliance_dictionary_default_level CHECK (default_level IN ('block', 'high', 'medium', 'info'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_compliance_dictionaries_enabled_version
|
||||
ON ops.compliance_dictionaries (enabled, version, id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ops.compliance_dictionary_terms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dictionary_id BIGINT NOT NULL REFERENCES ops.compliance_dictionaries(id) ON DELETE CASCADE,
|
||||
match_type VARCHAR(24) NOT NULL DEFAULT 'exact',
|
||||
pattern TEXT NOT NULL,
|
||||
level_override VARCHAR(16),
|
||||
hint TEXT,
|
||||
reference_law TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT ck_compliance_term_match_type CHECK (match_type IN ('exact', 'regex')),
|
||||
CONSTRAINT ck_compliance_term_level CHECK (level_override IS NULL OR level_override IN ('block', 'high', 'medium', 'info')),
|
||||
CONSTRAINT ck_compliance_term_pattern CHECK (char_length(trim(pattern)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_compliance_terms_dictionary_enabled
|
||||
ON ops.compliance_dictionary_terms (dictionary_id, enabled, id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ops.compliance_dictionary_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dictionary_id BIGINT NOT NULL REFERENCES ops.compliance_dictionaries(id) ON DELETE CASCADE,
|
||||
version INT NOT NULL,
|
||||
snapshot JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
published_by BIGINT,
|
||||
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uk_compliance_dictionary_versions UNIQUE (dictionary_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_compliance_dictionary_versions_latest
|
||||
ON ops.compliance_dictionary_versions (dictionary_id, version DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ops.compliance_policies (
|
||||
id BIGINT PRIMARY KEY DEFAULT 1,
|
||||
master_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
enforcement_mode VARCHAR(24) NOT NULL DEFAULT 'mandatory',
|
||||
enabled_dictionaries JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
llm_judge_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
llm_categories JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
revision BIGINT NOT NULL DEFAULT 1,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT ck_compliance_policy_singleton CHECK (id = 1),
|
||||
CONSTRAINT ck_compliance_policy_mode CHECK (enforcement_mode IN ('mandatory', 'advisory', 'disabled'))
|
||||
);
|
||||
|
||||
INSERT INTO ops.compliance_policies (
|
||||
id,
|
||||
master_enabled,
|
||||
enforcement_mode,
|
||||
enabled_dictionaries,
|
||||
llm_judge_enabled,
|
||||
llm_categories
|
||||
)
|
||||
VALUES (1, TRUE, 'mandatory', '[]'::jsonb, FALSE, '[]'::jsonb)
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,22 @@
|
||||
DELETE FROM ops.compliance_dictionary_versions
|
||||
WHERE dictionary_id IN (
|
||||
SELECT id
|
||||
FROM ops.compliance_dictionaries
|
||||
WHERE code IN ('ad_law_extreme', 'political_sensitive', 'violence_pornography', 'generic_prohibited')
|
||||
);
|
||||
|
||||
DELETE FROM ops.compliance_dictionary_terms
|
||||
WHERE dictionary_id IN (
|
||||
SELECT id
|
||||
FROM ops.compliance_dictionaries
|
||||
WHERE code IN ('ad_law_extreme', 'political_sensitive', 'violence_pornography', 'generic_prohibited')
|
||||
);
|
||||
|
||||
DELETE FROM ops.compliance_dictionaries
|
||||
WHERE code IN ('ad_law_extreme', 'political_sensitive', 'violence_pornography', 'generic_prohibited');
|
||||
|
||||
UPDATE ops.compliance_policies
|
||||
SET enabled_dictionaries = '[]'::jsonb,
|
||||
revision = revision + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = 1;
|
||||
@@ -0,0 +1,119 @@
|
||||
WITH seeded_dictionaries AS (
|
||||
INSERT INTO ops.compliance_dictionaries (
|
||||
code,
|
||||
name,
|
||||
platform_scope,
|
||||
applicable_platforms,
|
||||
default_level,
|
||||
enabled,
|
||||
version,
|
||||
description
|
||||
)
|
||||
VALUES
|
||||
('ad_law_extreme', '广告法极限词', 'all', NULL, 'block', TRUE, 1, '广告法、宣传语场景常见极限表述'),
|
||||
('political_sensitive', '政治敏感词', 'all', NULL, 'block', TRUE, 1, '公开运营口径下的政治敏感表达'),
|
||||
('violence_pornography', '涉黄涉暴通用词', 'all', NULL, 'block', TRUE, 1, '涉黄、涉暴、血腥等通用违规词'),
|
||||
('generic_prohibited', '通用违禁词', 'all', NULL, 'high', TRUE, 1, '赌博、毒品、黑产等通用违禁表达')
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
RETURNING id, code
|
||||
),
|
||||
all_dictionaries AS (
|
||||
SELECT id, code
|
||||
FROM seeded_dictionaries
|
||||
UNION
|
||||
SELECT id, code
|
||||
FROM ops.compliance_dictionaries
|
||||
WHERE code IN ('ad_law_extreme', 'political_sensitive', 'violence_pornography', 'generic_prohibited')
|
||||
),
|
||||
seed_terms(dictionary_code, pattern, level_override, hint, reference_law) AS (
|
||||
VALUES
|
||||
('ad_law_extreme', '国家级', 'block', '广告宣传中避免使用绝对化、最高级表述。', '广告法第九条'),
|
||||
('ad_law_extreme', '最高级', 'block', '广告宣传中避免使用绝对化、最高级表述。', '广告法第九条'),
|
||||
('ad_law_extreme', '最佳', 'block', '广告宣传中避免使用绝对化、最高级表述。', '广告法第九条'),
|
||||
('ad_law_extreme', '最强', 'block', '广告宣传中避免使用绝对化、最高级表述。', '广告法第九条'),
|
||||
('ad_law_extreme', '第一品牌', 'block', '广告宣传中避免使用无法证明的排名表述。', '广告法第九条'),
|
||||
('ad_law_extreme', '顶级', 'block', '广告宣传中避免使用绝对化、最高级表述。', '广告法第九条'),
|
||||
('ad_law_extreme', '全网最低', 'block', '价格比较类结论需要可证明依据。', '广告法第八条'),
|
||||
('ad_law_extreme', '永久有效', 'high', '承诺类表述需确保真实、完整、可履行。', '广告法第四条'),
|
||||
('political_sensitive', '反动', 'block', '涉及政治安全风险,发布前需修改或提交审阅。', NULL),
|
||||
('political_sensitive', '颠覆国家', 'block', '涉及政治安全风险,发布前需修改或提交审阅。', NULL),
|
||||
('political_sensitive', '分裂国家', 'block', '涉及政治安全风险,发布前需修改或提交审阅。', NULL),
|
||||
('political_sensitive', '恐怖主义', 'block', '涉及公共安全风险,发布前需修改或提交审阅。', NULL),
|
||||
('violence_pornography', '色情', 'block', '平台通常禁止涉黄内容或导流表达。', NULL),
|
||||
('violence_pornography', '约炮', 'block', '平台通常禁止涉黄内容或导流表达。', NULL),
|
||||
('violence_pornography', '血腥', 'block', '平台通常限制血腥暴力内容。', NULL),
|
||||
('violence_pornography', '虐杀', 'block', '平台通常限制血腥暴力内容。', NULL),
|
||||
('generic_prohibited', '赌博', 'high', '平台通常限制赌博相关内容。', NULL),
|
||||
('generic_prohibited', '博彩', 'high', '平台通常限制博彩相关内容。', NULL),
|
||||
('generic_prohibited', '毒品', 'high', '平台通常禁止毒品相关内容。', NULL),
|
||||
('generic_prohibited', '代开发票', 'high', '平台通常限制黑产交易相关内容。', NULL),
|
||||
('generic_prohibited', '刷单', 'high', '平台通常限制黑产交易相关内容。', NULL)
|
||||
)
|
||||
INSERT INTO ops.compliance_dictionary_terms (
|
||||
dictionary_id,
|
||||
match_type,
|
||||
pattern,
|
||||
level_override,
|
||||
hint,
|
||||
reference_law,
|
||||
enabled
|
||||
)
|
||||
SELECT d.id, 'exact', t.pattern, t.level_override, t.hint, t.reference_law, TRUE
|
||||
FROM seed_terms t
|
||||
JOIN all_dictionaries d ON d.code = t.dictionary_code
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ops.compliance_dictionary_terms existing
|
||||
WHERE existing.dictionary_id = d.id
|
||||
AND existing.pattern = t.pattern
|
||||
);
|
||||
|
||||
INSERT INTO ops.compliance_dictionary_versions (
|
||||
dictionary_id,
|
||||
version,
|
||||
snapshot,
|
||||
published_by
|
||||
)
|
||||
SELECT
|
||||
cd.id,
|
||||
cd.version,
|
||||
jsonb_build_object(
|
||||
'code', cd.code,
|
||||
'name', cd.name,
|
||||
'platform_scope', cd.platform_scope,
|
||||
'applicable_platforms', COALESCE(to_jsonb(cd.applicable_platforms), '[]'::jsonb),
|
||||
'default_level', cd.default_level,
|
||||
'terms', COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'pattern', t.pattern,
|
||||
'match_type', t.match_type,
|
||||
'level_override', t.level_override,
|
||||
'hint', t.hint,
|
||||
'reference_law', t.reference_law,
|
||||
'enabled', t.enabled
|
||||
)
|
||||
ORDER BY t.id
|
||||
)
|
||||
FROM ops.compliance_dictionary_terms t
|
||||
WHERE t.dictionary_id = cd.id
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
),
|
||||
NULL
|
||||
FROM ops.compliance_dictionaries cd
|
||||
WHERE cd.code IN ('ad_law_extreme', 'political_sensitive', 'violence_pornography', 'generic_prohibited')
|
||||
ON CONFLICT (dictionary_id, version) DO NOTHING;
|
||||
|
||||
UPDATE ops.compliance_policies
|
||||
SET enabled_dictionaries = (
|
||||
SELECT COALESCE(jsonb_agg(id ORDER BY id), '[]'::jsonb)
|
||||
FROM ops.compliance_dictionaries
|
||||
WHERE code IN ('ad_law_extreme', 'political_sensitive', 'violence_pornography', 'generic_prohibited')
|
||||
),
|
||||
revision = revision + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = 1
|
||||
AND enabled_dictionaries = '[]'::jsonb;
|
||||
Reference in New Issue
Block a user