feat: add system disclosure policy gate
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user