feat: add system disclosure policy gate
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s

This commit is contained in:
john
2026-07-23 22:53:15 +08:00
parent ba6cc10f08
commit 205b5fd8be
20 changed files with 1585 additions and 2 deletions
+3
View File
@@ -11,6 +11,8 @@ import { ReviewPage } from './pages/ReviewPage';
import { SummaryPage } from './pages/admin/SummaryPage';
import { UsersPage } from './pages/admin/UsersPage';
import { BillingPage } from './pages/admin/BillingPage';
import { WechatPage } from './pages/admin/WechatPage';
import { SystemPolicyPage } from './pages/admin/SystemPolicyPage';
export function App() {
return (
@@ -43,6 +45,7 @@ export function App() {
<Route path="users" element={<UsersPage />} />
<Route path="billing" element={<BillingPage />} />
<Route path="wechat" element={<WechatPage />} />
<Route path="policy" element={<SystemPolicyPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Route>
+34
View File
@@ -177,12 +177,46 @@ export type CreateWechatNotificationPayload = {
channels?: Array<'web' | 'wechat'>;
};
export type SystemDisclosureMode = 'off' | 'shadow' | 'enforce';
export type SystemDisclosurePolicyConfig = {
enabled: boolean;
mode: SystemDisclosureMode;
refusalText: string;
productNames: string[];
selfReferences: string[];
categories: Record<string, boolean>;
};
export type SystemDisclosurePolicyState = {
config: SystemDisclosurePolicyConfig;
policyVersion: number;
updatedBy: string | null;
updatedAt: number | null;
source: string;
refreshedAt?: number | null;
lastRefreshError?: string | null;
};
// ─── Summary ──────────────────────────────────────────────────────────────────
export async function fetchAdminSummary() {
return adminFetch<{ summary: AdminSummary }>('/admin-api/summary');
}
// ─── Trust & Policy ───────────────────────────────────────────────────────────
export async function fetchSystemDisclosurePolicy() {
return adminFetch<SystemDisclosurePolicyState>('/admin-api/system-disclosure-policy/config');
}
export async function updateSystemDisclosurePolicy(config: SystemDisclosurePolicyConfig) {
return adminFetch<SystemDisclosurePolicyState>('/admin-api/system-disclosure-policy/config', {
method: 'PUT',
body: JSON.stringify({ config }),
});
}
// ─── Users ────────────────────────────────────────────────────────────────────
export async function fetchAdminUsers(params: {
+1
View File
@@ -5,6 +5,7 @@ const links = [
{ to: '/admin/users', label: '用户管理' },
{ to: '/admin/billing', label: '账单记录' },
{ to: '/admin/wechat', label: '服务号管理' },
{ to: '/admin/policy', label: '策略中心' },
];
export function AdminLayout() {
+235
View File
@@ -0,0 +1,235 @@
import { useEffect, useMemo, useState } from 'react';
import {
fetchSystemDisclosurePolicy,
updateSystemDisclosurePolicy,
type SystemDisclosurePolicyConfig,
type SystemDisclosurePolicyState,
} from '../../api/admin';
const CATEGORY_LABELS: Record<string, { title: string; description: string }> = {
architecture: {
title: '架构与内部实现',
description: '底层架构、运行机制、服务设计与内部实现细节。',
},
model_and_routing: {
title: '模型与路由',
description: '模型供应商、模型选择、意图路由与 Agent 编排。',
},
prompts_and_memory: {
title: '提示词与记忆',
description: '系统提示词、上下文拼接、记忆注入与召回机制。',
},
tools_and_extensions: {
title: '工具与扩展',
description: '内部工具、插件、Skill、MCP 清单和调用方式。',
},
data_and_deployment: {
title: '数据与部署',
description: '数据库结构、存储位置、部署拓扑、主机与端口。',
},
security_boundaries: {
title: '安全边界',
description: '沙箱、鉴权、权限策略、安全实现及绕过方式。',
},
};
function listToText(values: string[]) {
return values.join('\n');
}
function textToList(value: string) {
return [...new Set(value.split(/\r?\n|,/).map((item) => item.trim()).filter(Boolean))];
}
function formatTime(value?: number | null) {
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未保存';
}
export function SystemPolicyPage() {
const [state, setState] = useState<SystemDisclosurePolicyState | null>(null);
const [draft, setDraft] = useState<SystemDisclosurePolicyConfig | null>(null);
const [productNames, setProductNames] = useState('');
const [selfReferences, setSelfReferences] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const load = async () => {
setLoading(true);
setError(null);
try {
const result = await fetchSystemDisclosurePolicy();
setState(result);
setDraft(result.config);
setProductNames(listToText(result.config.productNames));
setSelfReferences(listToText(result.config.selfReferences));
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const dirty = useMemo(() => {
if (!state || !draft) return false;
return JSON.stringify({
...draft,
productNames: textToList(productNames),
selfReferences: textToList(selfReferences),
}) !== JSON.stringify(state.config);
}, [draft, productNames, selfReferences, state]);
const save = async () => {
if (!draft) return;
if (
draft.mode === 'enforce'
&& !window.confirm('确认进入强制执行模式?明确命中的请求将直接固定拒答,不再进入意图识别和模型。')
) {
return;
}
setSaving(true);
setError(null);
setNotice(null);
try {
const result = await updateSystemDisclosurePolicy({
...draft,
productNames: textToList(productNames),
selfReferences: textToList(selfReferences),
});
setState(result);
setDraft(result.config);
setProductNames(listToText(result.config.productNames));
setSelfReferences(listToText(result.config.selfReferences));
setNotice(`策略版本 v${result.policyVersion} 已保存。Portal 将通过内存快照自动刷新。`);
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败');
} finally {
setSaving(false);
}
};
if (loading) return <p></p>;
if (!draft || !state) return <p className="alert">{error ?? '策略配置不可用'}</p>;
return (
<div className="grid">
<div className="card">
<h2 style={{ marginTop: 0 }}>Trust &amp; Policy Center</h2>
<p style={{ color: '#68716c', marginBottom: 0 }}>
System Disclosure Policy
</p>
</div>
{error ? <p className="alert">{error}</p> : null}
{notice ? <p style={{ color: '#2f6f57', fontSize: 13 }}>{notice}</p> : null}
<div className="card grid">
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(180px, 1fr) 2fr', gap: 16 }}>
<label>
<strong></strong>
<select
value={draft.mode}
onChange={(event) => setDraft({
...draft,
mode: event.target.value as SystemDisclosurePolicyConfig['mode'],
enabled: event.target.value !== 'off',
})}
style={{ marginTop: 8 }}
>
<option value="off"></option>
<option value="shadow">Shadow</option>
<option value="enforce">Enforce</option>
</select>
</label>
<div>
<strong>Zero-Impact Allow Invariant</strong>
<p style={{ color: '#68716c', margin: '8px 0 0' }}>
Allow Prompt
</p>
</div>
</div>
<div style={{ color: '#68716c', fontSize: 13 }}>
v{state.policyVersion} · {state.source} · {formatTime(state.updatedAt)}
</div>
</div>
<div className="card grid">
<h3 style={{ margin: 0 }}></h3>
{Object.entries(draft.categories).map(([key, enabled]) => {
const label = CATEGORY_LABELS[key] ?? { title: key, description: '' };
return (
<label
key={key}
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
alignItems: 'start',
gap: 10,
padding: '10px 0',
borderBottom: '1px solid #eee7da',
}}
>
<input
type="checkbox"
checked={enabled}
onChange={(event) => setDraft({
...draft,
categories: { ...draft.categories, [key]: event.target.checked },
})}
style={{ width: 'auto', marginTop: 3 }}
/>
<span>
<strong>{label.title}</strong>
<span style={{ display: 'block', color: '#68716c', fontSize: 13 }}>
{label.description}
</span>
</span>
</label>
);
})}
</div>
<div className="card grid">
<label>
<strong></strong>
<textarea
rows={4}
value={draft.refusalText}
onChange={(event) => setDraft({ ...draft, refusalText: event.target.value })}
style={{ marginTop: 8 }}
/>
</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<label>
<strong></strong>
<span style={{ display: 'block', color: '#68716c', fontSize: 12, margin: '4px 0 8px' }}>
/ +
</span>
<textarea rows={8} value={productNames} onChange={(event) => setProductNames(event.target.value)} />
</label>
<label>
<strong></strong>
<span style={{ display: 'block', color: '#68716c', fontSize: 12, margin: '4px 0 8px' }}>
</span>
<textarea rows={8} value={selfReferences} onChange={(event) => setSelfReferences(event.target.value)} />
</label>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button type="button" className="btn secondary" onClick={() => void load()} disabled={saving}>
</button>
<button type="button" className="btn" onClick={() => void save()} disabled={saving || !dirty}>
{saving ? '保存中…' : '保存新版本'}
</button>
</div>
</div>
);
}