Phase G5: Local Agent / Privacy Mode — 4 tables, 19 API endpoints, platform_bridge MCP, PrivacySettings.tsx (740 lines). 25/25 tests pass.

This commit is contained in:
qyyfw committed 2026-07-06 17:18:05 +08:00
1 parent 0486210133
commit 891d1ce527
7 files changed
+1681 -2

No files matched your search

@@ -0,0 +1,742 @@
// @ts-nocheck
import React, { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api';
interface Stats {
total_configs: number;
active_licenses: number;
synced_knowledge_bases: number;
mode_breakdown: Record<string, number>;
}
interface PrivacyConfig {
privacy_mode: string;
local_llm_provider: string;
local_llm_model: string;
local_llm_endpoint: string;
data_retention_days: number;
anonymize_usage: boolean;
sync_knowledge_bases: number[];
sync_frequency: string;
status: string;
last_sync_at?: string;
}
interface License {
id: number;
license_key: string;
agent_id: number | null;
issued_at: string;
expires_at: string;
max_offline_days: number;
status: string;
device_fingerprint?: string;
}
interface KbSyncState {
id: number;
knowledge_base_id: number;
kb_name: string;
sync_status: string;
total_docs: number;
synced_docs: number;
sync_direction: string;
last_sync_at: string | null;
local_version: number;
remote_version: number;
}
interface LocalAgent {
id: number;
name: string;
privacy_mode: string;
can_run_local: boolean;
llm_provider: string;
last_sync: string | null;
}
interface UsageStats {
total_reports: number;
unique_users: number;
today_reports: number;
by_type: Array<{ type: string; count: number }>;
}
const MODE_OPTIONS = [
{ value: 'cloud', label: '云端模式', icon: '☁️', desc: '所有数据和计算在云端处理' },
{ value: 'local', label: '本地模式', icon: '🖥️', desc: '数据完全本地处理,隐私优先' },
{ value: 'hybrid', label: '混合模式', icon: '🔀', desc: '敏感数据本地,通用能力云端' },
];
const FREQ_OPTIONS = [
{ value: 'realtime', label: '实时' },
{ value: 'hourly', label: '每小时' },
{ value: 'daily', label: '每天' },
{ value: 'manual', label: '手动' },
];
export default function PrivacySettings() {
const [activeTab, setActiveTab] = useState('settings');
const [stats, setStats] = useState<Stats | null>(null);
const [config, setConfig] = useState<PrivacyConfig | null>(null);
const [licenses, setLicenses] = useState<License[]>([]);
const [kbStates, setKbStates] = useState<KbSyncState[]>([]);
const [localAgents, setLocalAgents] = useState<LocalAgent[]>([]);
const [usageStats, setUsageStats] = useState<UsageStats | null>(null);
const [bridgeConfig, setBridgeConfig] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
// Modal states
const [showLicenseModal, setShowLicenseModal] = useState(false);
const [showSyncModal, setShowSyncModal] = useState(false);
const [licenseForm, setLicenseForm] = useState({ agent_id: '', max_offline_days: 7 });
const [syncForm, setSyncForm] = useState({ kb_id: '', direction: 'pull' });
const [newLicense, setNewLicense] = useState<any>(null);
const [validatingLicense, setValidatingLicense] = useState('');
const showMsg = (msg: string) => { setMessage(msg); setTimeout(() => setMessage(''), 3000); };
const loadStats = useCallback(async () => {
try {
const res = await adminAPI.getPrivacyStats();
if (res?.code === 0) setStats(res.data);
} catch (e) { console.error('loadStats:', e); }
}, []);
const loadConfig = useCallback(async () => {
try {
const res = await adminAPI.getPrivacyConfig();
if (res?.code === 0) setConfig(res.data);
} catch (e) { console.error('loadConfig:', e); }
}, []);
const loadLicenses = useCallback(async () => {
try {
const res = await adminAPI.getPrivacyLicenses();
if (res?.code === 0) setLicenses(res.data?.items || []);
} catch (e) { console.error('loadLicenses:', e); }
}, []);
const loadKbStates = useCallback(async () => {
try {
const res = await adminAPI.getKbSyncStates();
if (res?.code === 0) setKbStates(res.data || []);
} catch (e) { console.error('loadKbStates:', e); }
}, []);
const loadLocalAgents = useCallback(async () => {
try {
const res = await adminAPI.getLocalAgents();
if (res?.code === 0) setLocalAgents(res.data || []);
} catch (e) { console.error('loadLocalAgents:', e); }
}, []);
const loadUsage = useCallback(async () => {
try {
const res = await adminAPI.getPrivacyUsageStats();
if (res?.code === 0) setUsageStats(res.data);
} catch (e) { console.error('loadUsage:', e); }
}, []);
const loadBridgeConfig = useCallback(async () => {
try {
const res = await adminAPI.getBridgeConfig();
if (res?.code === 0) setBridgeConfig(res.data);
} catch (e) { console.error('loadBridge:', e); }
}, []);
useEffect(() => {
loadStats();
loadConfig();
loadLicenses();
loadKbStates();
loadLocalAgents();
loadUsage();
loadBridgeConfig();
}, [loadStats, loadConfig, loadLicenses, loadKbStates, loadLocalAgents, loadUsage, loadBridgeConfig]);
const switchMode = async (mode: string) => {
setLoading(true);
try {
const res = await adminAPI.switchPrivacyMode(mode);
if (res?.code === 0) { showMsg(`已切换到${MODE_OPTIONS.find(m => m.value === mode)?.label}`); loadConfig(); loadStats(); }
} catch (e) { showMsg('切换失败'); }
setLoading(false);
};
const updateConfig = async (field: string, value: any) => {
setLoading(true);
try {
const params: any = {};
if (field === 'privacy_mode') params.privacy_mode = value;
else if (field === 'local_llm_provider') params.local_llm_provider = value;
else if (field === 'local_llm_model') params.local_llm_model = value;
else if (field === 'local_llm_endpoint') params.local_llm_endpoint = value;
else if (field === 'data_retention_days') params.data_retention_days = value;
else if (field === 'sync_frequency') params.sync_frequency = value;
const res = await adminAPI.updatePrivacyConfig(params);
if (res?.code === 0) { showMsg('配置已更新'); loadConfig(); }
} catch (e) { showMsg('更新失败'); }
setLoading(false);
};
const generateLicense = async () => {
setLoading(true);
try {
const res = await adminAPI.generateOfflineLicense({
agent_id: licenseForm.agent_id || undefined,
max_offline_days: licenseForm.max_offline_days,
});
if (res?.code === 0) {
setNewLicense(res.data);
loadLicenses();
loadStats();
showMsg('许可证已生成');
}
} catch (e) { showMsg('生成失败'); }
setLoading(false);
};
const revokeLicense = async (id: number) => {
if (!confirm('确定吊销此许可证?')) return;
try {
await adminAPI.revokePrivacyLicense(id);
showMsg('已吊销'); loadLicenses(); loadStats();
} catch (e) { showMsg('吊销失败'); }
};
const validateLicenseKey = async () => {
if (!validatingLicense) return;
setLoading(true);
try {
const res = await adminAPI.validatePrivacyLicense(validatingLicense);
if (res?.data?.valid) showMsg('✅ 许可证有效');
else showMsg('❌ ' + (res?.message || '许可证无效'));
} catch (e) { showMsg('验证失败'); }
setLoading(false);
};
const triggerSync = async () => {
if (!syncForm.kb_id) return;
setLoading(true);
try {
const res = await adminAPI.triggerKbSync(parseInt(syncForm.kb_id), syncForm.direction);
if (res?.code === 0) { showMsg('同步已触发'); loadKbStates(); loadStats(); setShowSyncModal(false); }
} catch (e) { showMsg('同步失败'); }
setLoading(false);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => showMsg('已复制到剪贴板'));
};
const formatDate = (d: string | null) => d ? new Date(d).toLocaleString('zh-CN') : '-';
const TABS = [
{ key: 'settings', label: '隐私设置', icon: '🔒' },
{ key: 'licenses', label: '许可证管理', icon: '🔑' },
{ key: 'kb_sync', label: '知识库同步', icon: '🔄' },
{ key: 'agents', label: '本地智能体', icon: '🤖' },
{ key: 'bridge', label: '平台桥接', icon: '🌉' },
{ key: 'usage', label: '用量统计', icon: '📊' },
];
return (
<div style={{ padding: 24, color: '#e0e0e0', background: '#0f0f1a', minHeight: '100vh', fontFamily: 'system-ui' }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<div>
<h1 style={{ margin: 0, fontSize: 24, color: '#fff' }}>🔐 隐私与本地智能体</h1>
<p style={{ margin: '4px 0 0', color: '#888', fontSize: 13 }}>管理隐私模式、本地代理、离线许可证和知识库同步</p>
</div>
</div>
{message && (
<div style={{ background: '#1a472a', color: '#4ade80', padding: '8px 16px', borderRadius: 6, marginBottom: 16, fontSize: 13 }}>
{message}
</div>
)}
{/* Stats Cards */}
{stats && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12, marginBottom: 24 }}>
{[
{ label: '隐私配置', value: stats.total_configs, color: '#7c3aed' },
{ label: '活跃许可证', value: stats.active_licenses, color: '#059669' },
{ label: '已同步知识库', value: stats.synced_knowledge_bases, color: '#2563eb' },
{ label: '本地模式', value: stats.mode_breakdown?.local || 0, color: '#ea580c' },
{ label: '混合模式', value: stats.mode_breakdown?.hybrid || 0, color: '#ca8a04' },
{ label: '云端模式', value: stats.mode_breakdown?.cloud || 0, color: '#6366f1' },
].map((s, i) => (
<div key={i} style={{ background: '#1a1a2e', borderRadius: 8, padding: 16, borderLeft: `3px solid ${s.color}` }}>
<div style={{ fontSize: 12, color: '#888' }}>{s.label}</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#fff' }}>{s.value}</div>
</div>
))}
</div>
)}
{/* Tabs */}
<div style={{ display: 'flex', gap: 4, marginBottom: 24, borderBottom: '1px solid #2a2a3e', paddingBottom: 0 }}>
{TABS.map(t => (
<button
key={t.key}
onClick={() => setActiveTab(t.key)}
style={{
padding: '10px 20px',
background: activeTab === t.key ? '#7c3aed' : 'transparent',
color: activeTab === t.key ? '#fff' : '#888',
border: 'none',
borderRadius: '8px 8px 0 0',
cursor: 'pointer',
fontSize: 13,
fontWeight: activeTab === t.key ? 600 : 400,
transition: 'all 0.2s',
}}
>
{t.icon} {t.label}
</button>
))}
</div>
{/* Tab Content */}
<div style={{ background: '#1a1a2e', borderRadius: 12, padding: 24 }}>
{/* ============ TAB: Privacy Settings ============ */}
{activeTab === 'settings' && (
<div>
<h3 style={{ margin: '0 0 20px', color: '#fff', fontSize: 18 }}>隐私模式选择</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 12, marginBottom: 32 }}>
{MODE_OPTIONS.map(m => (
<div
key={m.value}
onClick={() => !loading && switchMode(m.value)}
style={{
background: config?.privacy_mode === m.value ? '#2d1f5e' : '#252540',
border: config?.privacy_mode === m.value ? '2px solid #7c3aed' : '2px solid transparent',
borderRadius: 10,
padding: 20,
cursor: 'pointer',
transition: 'all 0.2s',
opacity: loading ? 0.6 : 1,
}}
>
<div style={{ fontSize: 32, marginBottom: 8 }}>{m.icon}</div>
<div style={{ fontSize: 16, fontWeight: 600, color: '#fff' }}>{m.label}</div>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>{m.desc}</div>
{config?.privacy_mode === m.value && (
<div style={{ marginTop: 8, fontSize: 11, color: '#7c3aed', fontWeight: 600 }}>✓ 当前模式</div>
)}
</div>
))}
</div>
{/* Local LLM Config */}
{(config?.privacy_mode === 'local' || config?.privacy_mode === 'hybrid') && (
<div style={{ marginBottom: 32 }}>
<h3 style={{ margin: '0 0 16px', color: '#fff', fontSize: 16 }}>本地 LLM 配置</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<div>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>Provider</label>
<select
value={config?.local_llm_provider || 'ollama'}
onChange={e => updateConfig('local_llm_provider', e.target.value)}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6, fontSize: 13 }}
>
<option value="ollama">Ollama</option>
<option value="vllm">vLLM</option>
<option value="localai">LocalAI</option>
<option value="llamacpp">llama.cpp</option>
</select>
</div>
<div>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>Model</label>
<input
value={config?.local_llm_model || 'qwen2.5:7b'}
onChange={e => updateConfig('local_llm_model', e.target.value)}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6, fontSize: 13 }}
/>
</div>
<div>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>Endpoint</label>
<input
value={config?.local_llm_endpoint || 'http://localhost:11434'}
onChange={e => updateConfig('local_llm_endpoint', e.target.value)}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6, fontSize: 13 }}
/>
</div>
</div>
</div>
)}
{/* Sync Config */}
<div style={{ marginBottom: 32 }}>
<h3 style={{ margin: '0 0 16px', color: '#fff', fontSize: 16 }}>同步配置</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>同步频率</label>
<select
value={config?.sync_frequency || 'daily'}
onChange={e => updateConfig('sync_frequency', e.target.value)}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6, fontSize: 13 }}
>
{FREQ_OPTIONS.map(f => <option key={f.value} value={f.value}>{f.label}</option>)}
</select>
</div>
<div>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>数据保留天数</label>
<input
type="number"
value={config?.data_retention_days || 30}
onChange={e => updateConfig('data_retention_days', parseInt(e.target.value))}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6, fontSize: 13 }}
/>
</div>
</div>
</div>
{/* Status */}
<div style={{ background: '#252540', borderRadius: 8, padding: 16 }}>
<div style={{ fontSize: 14, color: '#fff', fontWeight: 600, marginBottom: 8 }}>当前状态</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 13, color: '#aaa' }}>
<div>模式: <span style={{ color: '#7c3aed', fontWeight: 600 }}>{config?.privacy_mode || 'cloud'}</span></div>
<div>状态: <span style={{ color: config?.status === 'active' ? '#4ade80' : '#f87171' }}>{config?.status || 'active'}</span></div>
<div>匿名上报: <span style={{ color: config?.anonymize_usage ? '#4ade80' : '#f87171' }}>{config?.anonymize_usage ? '已启用' : '已禁用'}</span></div>
<div>最后同步: <span>{config?.last_sync_at ? formatDate(config.last_sync_at) : '从未'}</span></div>
</div>
</div>
</div>
)}
{/* ============ TAB: Licenses ============ */}
{activeTab === 'licenses' && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
<h3 style={{ margin: 0, color: '#fff', fontSize: 18 }}>离线许可证管理</h3>
<div style={{ display: 'flex', gap: 8 }}>
<input
placeholder="输入许可证密钥验证..."
value={validatingLicense}
onChange={e => setValidatingLicense(e.target.value)}
style={{ padding: '8px 12px', background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6, fontSize: 13, width: 280 }}
/>
<button onClick={validateLicenseKey} disabled={loading} style={{
padding: '8px 16px', background: '#2563eb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 13
}}>验证</button>
<button onClick={() => { setShowLicenseModal(true); setNewLicense(null); }} style={{
padding: '8px 16px', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 13
}}>+ 生成许可证</button>
</div>
</div>
{/* License Table */}
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #2a2a3e' }}>
{['ID', '许可证密钥', 'Agent ID', '最大离线天数', '签发时间', '过期时间', '状态', '操作'].map(h => (
<th key={h} style={{ padding: '10px 12px', textAlign: 'left', fontSize: 12, color: '#888', fontWeight: 500 }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{licenses.length === 0 ? (
<tr><td colSpan={8} style={{ padding: 40, textAlign: 'center', color: '#666' }}>暂无许可证</td></tr>
) : licenses.map(l => (
<tr key={l.id} style={{ borderBottom: '1px solid #2a2a3e' }}>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#ccc' }}>{l.id}</td>
<td style={{ padding: '10px 12px', fontSize: 12, color: '#aaa', fontFamily: 'monospace', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis' }}>
{l.license_key?.substring(0, 40)}...
</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#ccc' }}>{l.agent_id || '-'}</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#ccc' }}>{l.max_offline_days} 天</td>
<td style={{ padding: '10px 12px', fontSize: 12, color: '#888' }}>{formatDate(l.issued_at)}</td>
<td style={{ padding: '10px 12px', fontSize: 12, color: l.status === 'active' ? '#4ade80' : '#f87171' }}>{formatDate(l.expires_at)}</td>
<td style={{ padding: '10px 12px' }}>
<span style={{
padding: '2px 10px', borderRadius: 10, fontSize: 11, fontWeight: 600,
background: l.status === 'active' ? '#064e3b' : l.status === 'expired' ? '#7f1d1d' : '#4a1d1d',
color: l.status === 'active' ? '#4ade80' : l.status === 'expired' ? '#fca5a5' : '#f87171'
}}>{l.status}</span>
</td>
<td style={{ padding: '10px 12px' }}>
{l.status === 'active' && (
<button onClick={() => revokeLicense(l.id)} style={{
padding: '4px 12px', background: '#7f1d1d', color: '#fca5a5', border: 'none', borderRadius: 4, cursor: 'pointer', fontSize: 12
}}>吊销</button>
)}
</td>
</tr>
))}
</tbody>
</table>
{/* License Modal */}
{showLicenseModal && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: '#1a1a2e', borderRadius: 12, padding: 24, width: 480, maxHeight: '80vh', overflow: 'auto' }}>
<h3 style={{ margin: '0 0 20px', color: '#fff' }}>{newLicense ? '✅ 许可证已生成' : '生成离线许可证'}</h3>
{newLicense ? (
<div>
<div style={{ background: '#252540', borderRadius: 8, padding: 16, marginBottom: 12 }}>
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>许可证密钥 (点击复制)</div>
<div onClick={() => copyToClipboard(newLicense.license_key)} style={{
fontFamily: 'monospace', fontSize: 11, color: '#4ade80', wordBreak: 'break-all',
cursor: 'pointer', background: '#0f0f1a', padding: 10, borderRadius: 6, maxHeight: 100, overflow: 'auto'
}}>
{newLicense.license_key}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 13, color: '#aaa' }}>
<div>签发时间: {formatDate(newLicense.issued_at ? new Date(newLicense.issued_at * 1000).toISOString() : null)}</div>
<div>过期时间: {formatDate(newLicense.expires_at ? new Date(newLicense.expires_at * 1000).toISOString() : null)}</div>
<div>离线天数: {newLicense.max_offline_days} 天</div>
<div>状态: <span style={{ color: '#4ade80' }}>活跃</span></div>
</div>
<button onClick={() => setShowLicenseModal(false)} style={{
marginTop: 16, width: '100%', padding: 10, background: '#7c3aed', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer'
}}>关闭</button>
</div>
) : (
<div>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>Agent (可选)</label>
<select value={licenseForm.agent_id} onChange={e => setLicenseForm({ ...licenseForm, agent_id: e.target.value })}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6 }}>
<option value="">默认</option>
{localAgents.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
</select>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>最大离线天数: {licenseForm.max_offline_days} 天</label>
<input type="range" min={1} max={90} value={licenseForm.max_offline_days}
onChange={e => setLicenseForm({ ...licenseForm, max_offline_days: parseInt(e.target.value) })}
style={{ width: '100%' }} />
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => setShowLicenseModal(false)} style={{
flex: 1, padding: 10, background: '#333', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer'
}}>取消</button>
<button onClick={generateLicense} disabled={loading} style={{
flex: 1, padding: 10, background: '#7c3aed', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer'
}}>{loading ? '生成中...' : '生成'}</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
)}
{/* ============ TAB: KB Sync ============ */}
{activeTab === 'kb_sync' && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
<h3 style={{ margin: 0, color: '#fff', fontSize: 18 }}>知识库同步状态</h3>
<button onClick={() => setShowSyncModal(true)} style={{
padding: '8px 16px', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 13
}}>+ 触发同步</button>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #2a2a3e' }}>
{['知识库', '状态', '方向', '文档数', '已同步', '本地版本', '远程版本', '最后同步'].map(h => (
<th key={h} style={{ padding: '10px 12px', textAlign: 'left', fontSize: 12, color: '#888', fontWeight: 500 }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{kbStates.length === 0 ? (
<tr><td colSpan={8} style={{ padding: 40, textAlign: 'center', color: '#666' }}>暂无同步记录</td></tr>
) : kbStates.map(kb => (
<tr key={kb.id} style={{ borderBottom: '1px solid #2a2a3e' }}>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#ccc', fontWeight: 500 }}>{kb.kb_name}</td>
<td style={{ padding: '10px 12px' }}>
<span style={{
padding: '2px 10px', borderRadius: 10, fontSize: 11, fontWeight: 600,
background: kb.sync_status === 'synced' ? '#064e3b' : kb.sync_status === 'syncing' ? '#1e3a5f' : '#4a1d1d',
color: kb.sync_status === 'synced' ? '#4ade80' : kb.sync_status === 'syncing' ? '#60a5fa' : '#f87171'
}}>{kb.sync_status}</span>
</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#aaa' }}>{kb.sync_direction}</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#ccc' }}>{kb.total_docs}</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#4ade80' }}>{kb.synced_docs}</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#aaa' }}>v{kb.local_version}</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#aaa' }}>v{kb.remote_version}</td>
<td style={{ padding: '10px 12px', fontSize: 12, color: '#888' }}>{formatDate(kb.last_sync_at)}</td>
</tr>
))}
</tbody>
</table>
{/* Sync Modal */}
{showSyncModal && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: '#1a1a2e', borderRadius: 12, padding: 24, width: 400 }}>
<h3 style={{ margin: '0 0 20px', color: '#fff' }}>触发知识库同步</h3>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>知识库 ID</label>
<input type="number" value={syncForm.kb_id} onChange={e => setSyncForm({ ...syncForm, kb_id: e.target.value })}
placeholder="输入知识库 ID" style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6 }} />
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ fontSize: 12, color: '#888', display: 'block', marginBottom: 4 }}>同步方向</label>
<select value={syncForm.direction} onChange={e => setSyncForm({ ...syncForm, direction: e.target.value })}
style={{ width: '100%', padding: 10, background: '#252540', color: '#fff', border: '1px solid #3a3a5a', borderRadius: 6 }}>
<option value="pull">拉取 (平台→本地)</option>
<option value="push">推送 (本地→平台)</option>
<option value="bidirectional">双向同步</option>
</select>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => setShowSyncModal(false)} style={{ flex: 1, padding: 10, background: '#333', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>取消</button>
<button onClick={triggerSync} disabled={loading} style={{ flex: 1, padding: 10, background: '#7c3aed', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
{loading ? '同步中...' : '触发同步'}
</button>
</div>
</div>
</div>
)}
</div>
)}
{/* ============ TAB: Local Agents ============ */}
{activeTab === 'agents' && (
<div>
<h3 style={{ margin: '0 0 20px', color: '#fff', fontSize: 18 }}>本地智能体状态</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 12 }}>
{localAgents.length === 0 ? (
<div style={{ color: '#666', padding: 40, textAlign: 'center' }}>暂无已发布的 Agent</div>
) : localAgents.map(agent => (
<div key={agent.id} style={{ background: '#252540', borderRadius: 10, padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: 12 }}>
<div>
<div style={{ fontSize: 15, fontWeight: 600, color: '#fff' }}>{agent.name}</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 2 }}>ID: {agent.id}</div>
</div>
<span style={{
padding: '3px 12px', borderRadius: 10, fontSize: 11, fontWeight: 600,
background: agent.privacy_mode === 'local' ? '#2d1f5e' : agent.privacy_mode === 'hybrid' ? '#4a3a0a' : '#1e3a5f',
color: agent.privacy_mode === 'local' ? '#a78bfa' : agent.privacy_mode === 'hybrid' ? '#fbbf24' : '#60a5fa'
}}>{agent.privacy_mode}</span>
</div>
<div style={{ fontSize: 13, color: '#aaa', marginBottom: 12 }}>
{agent.can_run_local ? (
<span style={{ color: '#4ade80' }}>✓ 支持本地运行 ({agent.llm_provider || '未配置LLM'})</span>
) : (
<span style={{ color: '#f87171' }}>✗ 仅云端运行</span>
)}
</div>
<div style={{ fontSize: 12, color: '#666' }}>
最后同步: {formatDate(agent.last_sync)}
</div>
</div>
))}
</div>
</div>
)}
{/* ============ TAB: Platform Bridge ============ */}
{activeTab === 'bridge' && (
<div>
<h3 style={{ margin: '0 0 20px', color: '#fff', fontSize: 18 }}>Platform Bridge 配置</h3>
<p style={{ color: '#888', fontSize: 13, marginBottom: 20 }}>
将此配置部署到本地 Goose,即可通过 Platform Bridge MCP 扩展连接云端平台。
</p>
{bridgeConfig && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
{/* MCP Server Config */}
<div>
<h4 style={{ margin: '0 0 12px', color: '#ccc', fontSize: 14 }}>MCP 服务器配置</h4>
<pre style={{
background: '#0f0f1a', color: '#4ade80', padding: 16, borderRadius: 8, fontSize: 12, fontFamily: 'monospace',
overflow: 'auto', maxHeight: 300, whiteSpace: 'pre-wrap'
}}>
{JSON.stringify(bridgeConfig.mcp_server, null, 2)}
</pre>
<button onClick={() => copyToClipboard(JSON.stringify(bridgeConfig.mcp_server, null, 2))} style={{
marginTop: 8, padding: '6px 14px', background: '#2563eb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 12
}}>复制配置</button>
</div>
{/* Sync & LLM Config */}
<div>
<h4 style={{ margin: '0 0 12px', color: '#ccc', fontSize: 14 }}>同步 & LLM 配置</h4>
<pre style={{
background: '#0f0f1a', color: '#fbbf24', padding: 16, borderRadius: 8, fontSize: 12, fontFamily: 'monospace',
overflow: 'auto', maxHeight: 300, whiteSpace: 'pre-wrap'
}}>
{JSON.stringify({ sync_config: bridgeConfig.sync_config, local_llm: bridgeConfig.local_llm, privacy_mode: bridgeConfig.privacy_mode }, null, 2)}
</pre>
<button onClick={() => copyToClipboard(JSON.stringify({
sync_config: bridgeConfig.sync_config,
local_llm: bridgeConfig.local_llm,
privacy_mode: bridgeConfig.privacy_mode
}, null, 2))} style={{
marginTop: 8, padding: '6px 14px', background: '#ca8a04', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 12
}}>复制配置</button>
{/* Deployment Instructions */}
<div style={{ marginTop: 20, background: '#252540', borderRadius: 8, padding: 16 }}>
<h4 style={{ margin: '0 0 8px', color: '#ccc', fontSize: 14 }}>🚀 部署步骤</h4>
<ol style={{ margin: 0, paddingLeft: 20, fontSize: 12, color: '#aaa', lineHeight: 2 }}>
<li>复制 Platform Bridge 代码到本地 <code style={{ background: '#0f0f1a', padding: '2px 6px', borderRadius: 4, color: '#4ade80' }}>~/.goose/extensions/platform-bridge/</code></li>
<li>安装依赖: <code style={{ background: '#0f0f1a', padding: '2px 6px', borderRadius: 4, color: '#4ade80' }}>pip install -r requirements.txt</code></li>
<li>配置环境变量: 将上方 MCP 服务器配置添加到 Goose</li>
<li>启动 Goose: 配置将自动连接到平台</li>
</ol>
</div>
</div>
</div>
)}
</div>
)}
{/* ============ TAB: Usage ============ */}
{activeTab === 'usage' && (
<div>
<h3 style={{ margin: '0 0 20px', color: '#fff', fontSize: 18 }}>匿名化用量统计</h3>
{usageStats && (
<div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, marginBottom: 24 }}>
<div style={{ background: '#252540', borderRadius: 8, padding: 16 }}>
<div style={{ fontSize: 12, color: '#888' }}>总上报数</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#fff' }}>{usageStats.total_reports}</div>
</div>
<div style={{ background: '#252540', borderRadius: 8, padding: 16 }}>
<div style={{ fontSize: 12, color: '#888' }}>匿名用户数</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#2563eb' }}>{usageStats.unique_users}</div>
</div>
<div style={{ background: '#252540', borderRadius: 8, padding: 16 }}>
<div style={{ fontSize: 12, color: '#888' }}>今日上报</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#4ade80' }}>{usageStats.today_reports}</div>
</div>
</div>
<h4 style={{ margin: '0 0 12px', color: '#ccc', fontSize: 14 }}>按类型分布</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{usageStats.by_type?.map((t, i) => (
<div key={i} style={{ background: '#252540', borderRadius: 8, padding: '10px 16px' }}>
<div style={{ fontSize: 11, color: '#888' }}>{t.type}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: '#fff' }}>{t.count}</div>
</div>
))}
</div>
<div style={{ marginTop: 24, background: '#252540', borderRadius: 8, padding: 16 }}>
<h4 style={{ margin: '0 0 8px', color: '#ccc', fontSize: 14 }}>📋 隐私说明</h4>
<p style={{ margin: 0, fontSize: 12, color: '#888', lineHeight: 1.8 }}>
所有用量数据在上报前均已通过 SHA-256 哈希匿名化处理。我们仅收集聚合统计数据,不包含任何可识别个人身份的信息。
数据包括:执行次数、模型调用次数、知识库查询次数、Agent 运行时统计等。
</p>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}