security: stop tracking env files; admin gate via VITE_SITE_ADMIN_GATE; Redis/cache/docs
Remove InfoGenie-frontend and Go .env from version control; add .env.example templates; ignore .claude local settings. Admin UI reads site gate from env only. Note: rotate secrets if repo history was ever public. Made-with: Cursor
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
# 复制为 .env.local 后可按需覆盖本地开发值
|
||||
VITE_API_URL=http://127.0.0.1:5002
|
||||
VITE_AUTH_URL=https://auth.shumengya.top
|
||||
VITE_AUTH_API_URL=https://auth.api.shumengya.top
|
||||
VITE_NAME=InfoGenie
|
||||
VITE_VERSION=1.0.0
|
||||
VITE_DEBUG=true
|
||||
7
InfoGenie-frontend/.env.example
Normal file
7
InfoGenie-frontend/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
# 复制为 .env.development / .env.production 后填写;勿提交真实密钥
|
||||
VITE_API_URL=http://127.0.0.1:5002
|
||||
VITE_AUTH_URL=https://auth.shumengya.top
|
||||
VITE_AUTH_API_URL=https://auth.api.shumengya.top
|
||||
VITE_DEBUG=true
|
||||
# 须与 Go 后端 INFOGENIE_SITE_ADMIN_TOKEN 一致
|
||||
VITE_SITE_ADMIN_GATE=
|
||||
@@ -1,7 +0,0 @@
|
||||
# 生产前端环境变量
|
||||
VITE_API_URL=https://infogenie.api.shumengya.top
|
||||
VITE_AUTH_URL=https://auth.shumengya.top
|
||||
VITE_AUTH_API_URL=https://auth.api.shumengya.top
|
||||
VITE_NAME=InfoGenie
|
||||
VITE_VERSION=1.0.0
|
||||
VITE_DEBUG=false
|
||||
@@ -4,11 +4,10 @@ import { FiUser, FiMenu, FiX, FiLogOut } from 'react-icons/fi';
|
||||
import { useUser } from '../contexts/UserContext';
|
||||
import toast from 'react-hot-toast';
|
||||
import clsx from 'clsx';
|
||||
import { ENV_CONFIG } from '../config/env';
|
||||
|
||||
const logoSrc = '/assets/logo.png';
|
||||
|
||||
const ADMIN_TOKEN = 'shumengya520';
|
||||
|
||||
const Header = () => {
|
||||
const { user, isLoggedIn, logout } = useUser();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -27,7 +26,7 @@ const Header = () => {
|
||||
|
||||
if (clickCountRef.current >= 5) {
|
||||
clickCountRef.current = 0;
|
||||
if (localStorage.getItem('admin_token') === ADMIN_TOKEN) {
|
||||
if (ENV_CONFIG.SITE_ADMIN_GATE && localStorage.getItem('admin_token') === ENV_CONFIG.SITE_ADMIN_GATE) {
|
||||
navigate('/admin');
|
||||
} else {
|
||||
setAdminInput('');
|
||||
@@ -44,8 +43,8 @@ const Header = () => {
|
||||
}, [navigate]);
|
||||
|
||||
const handleAdminSubmit = () => {
|
||||
if (adminInput === ADMIN_TOKEN) {
|
||||
localStorage.setItem('admin_token', ADMIN_TOKEN);
|
||||
if (ENV_CONFIG.SITE_ADMIN_GATE && adminInput === ENV_CONFIG.SITE_ADMIN_GATE) {
|
||||
localStorage.setItem('admin_token', ENV_CONFIG.SITE_ADMIN_GATE);
|
||||
setShowAdminModal(false);
|
||||
toast.success('管理员验证通过');
|
||||
navigate('/admin');
|
||||
|
||||
@@ -1,146 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
const accent = '#22c55e';
|
||||
const soft = 'rgba(74, 222, 128, 0.18)';
|
||||
const stroke = 1.8;
|
||||
/** 与 StaticPageConfig SMALL_GAMES 的 id 对齐 */
|
||||
const GAME_EMOJI = {
|
||||
'2048': { emoji: '🔢', label: '2048' },
|
||||
'white-tile': { emoji: '🎹', label: '别踩白方块' },
|
||||
tetris: { emoji: '🧱', label: '俄罗斯方块' },
|
||||
snake: { emoji: '🐍', label: '贪吃蛇' },
|
||||
minesweeper: { emoji: '💣', label: '扫雷' },
|
||||
'dodge-leaves': { emoji: '🍃', label: '躲树叶' },
|
||||
plane: { emoji: '✈️', label: '打飞机' },
|
||||
parkour: { emoji: '🏃', label: '跑酷' },
|
||||
floppybird: { emoji: '🐦', label: 'Floppy Bird' },
|
||||
h5cube: { emoji: '🧩', label: '网页魔方' },
|
||||
sokoban: { emoji: '📦', label: '推箱子' },
|
||||
};
|
||||
|
||||
function SvgBox({ children, ...rest }) {
|
||||
export function SmallGameIcon({ gameId }) {
|
||||
const meta = GAME_EMOJI[gameId] || { emoji: '🎮', label: '小游戏' };
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden
|
||||
{...rest}
|
||||
<span
|
||||
role="img"
|
||||
aria-label={meta.label}
|
||||
className="inline-flex items-center justify-center text-[1.65rem] sm:text-[1.35rem] leading-none select-none"
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
{meta.emoji}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 按 SMALL_GAMES 的 id 渲染矢量图标(纯 SVG,无 Emoji) */
|
||||
export function SmallGameIcon({ gameId }) {
|
||||
switch (gameId) {
|
||||
case '2048':
|
||||
return (
|
||||
<SvgBox>
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" fill={accent} opacity="0.9" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" fill={accent} opacity="0.6" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" fill={accent} opacity="0.75" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" fill={accent} opacity="0.45" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'white-tile':
|
||||
return (
|
||||
<SvgBox>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" fill={soft} stroke={accent} strokeWidth={stroke} />
|
||||
<rect x="7" y="7" width="10" height="10" rx="1" fill={accent} opacity="0.3" />
|
||||
<path d="M12 10v4M10 12h4" stroke={accent} strokeWidth="1.5" strokeLinecap="round" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'tetris':
|
||||
return (
|
||||
<SvgBox>
|
||||
<rect x="2" y="10" width="6" height="6" rx="1" fill={accent} />
|
||||
<rect x="9" y="10" width="6" height="6" rx="1" fill={accent} opacity="0.8" />
|
||||
<rect x="16" y="10" width="6" height="6" rx="1" fill={accent} />
|
||||
<rect x="9" y="3" width="6" height="6" rx="1" fill={accent} opacity="0.9" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'snake':
|
||||
return (
|
||||
<SvgBox>
|
||||
<path
|
||||
d="M4 16c0-4 3-7 7-7h4c3 0 5-1 7-4"
|
||||
stroke={accent}
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<circle cx="19" cy="5" r="2.5" fill={accent} />
|
||||
<circle cx="6" cy="16" r="2" fill={accent} opacity="0.6" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'minesweeper':
|
||||
return (
|
||||
<SvgBox>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" fill={soft} stroke={accent} strokeWidth={stroke} />
|
||||
<path d="M3 9h18M3 15h18M9 3v18M15 3v18" stroke={accent} strokeOpacity="0.3" strokeWidth="1" />
|
||||
<circle cx="15" cy="15" r="3" fill="#a855f7" />
|
||||
<path d="M15 12v6M12 15h6" stroke="#fff" strokeWidth="1.2" strokeLinecap="round" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'dodge-leaves':
|
||||
return (
|
||||
<SvgBox>
|
||||
<path
|
||||
d="M12 3c-5 3-8 8-8 14h16c0-6-3-11-8-14z"
|
||||
fill={soft}
|
||||
stroke={accent}
|
||||
strokeWidth={stroke}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M12 8v9M8 14l4-3 4 3" stroke={accent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'plane':
|
||||
return (
|
||||
<SvgBox>
|
||||
<path
|
||||
d="M4 14l8-6 8 6-8-2z"
|
||||
fill={soft}
|
||||
stroke={accent}
|
||||
strokeWidth={stroke}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M12 8v10M12 18l-3 3M12 18l3 3" stroke={accent} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="20" cy="10" r="2" fill="#ef4444" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'parkour':
|
||||
return (
|
||||
<SvgBox>
|
||||
<circle cx="8" cy="6" r="3" fill={soft} stroke={accent} strokeWidth={stroke} />
|
||||
<path d="M8 9v5l-4 4M8 14h6l4 4" stroke={accent} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<rect x="16" y="16" width="5" height="4" rx="1" fill={accent} opacity="0.4" />
|
||||
<circle cx="19" cy="8" r="3" fill="#fbbf24" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'floppybird':
|
||||
return (
|
||||
<SvgBox>
|
||||
<ellipse cx="12" cy="12" rx="8" ry="5" fill={soft} stroke={accent} strokeWidth={stroke} transform="rotate(-20 12 12)" />
|
||||
<circle cx="9" cy="11" r="2" fill={accent} />
|
||||
<path d="M16 10l4-2M16 12l4 2" stroke="#f97316" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'h5cube':
|
||||
return (
|
||||
<SvgBox>
|
||||
<path d="M4 8l8-4 8 4-8 4z" fill={soft} stroke={accent} strokeWidth={stroke} />
|
||||
<path d="M4 8v8l8 4v-8M12 20l8-4V8" stroke={accent} strokeWidth={stroke} fill="none" />
|
||||
<path d="M12 4v8" stroke={accent} strokeWidth="1" opacity="0.5" />
|
||||
</SvgBox>
|
||||
);
|
||||
case 'sokoban':
|
||||
return (
|
||||
<SvgBox>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" fill={soft} stroke={accent} strokeWidth={stroke} />
|
||||
<rect x="12" y="5" width="7" height="7" rx="1" fill={accent} opacity="0.5" />
|
||||
<rect x="5" y="12" width="7" height="7" rx="1" fill={accent} opacity="0.7" />
|
||||
<circle cx="8.5" cy="15.5" r="2.5" fill="#3b82f6" />
|
||||
</SvgBox>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<SvgBox>
|
||||
<rect x="4" y="4" width="16" height="16" rx="4" fill={soft} stroke={accent} strokeWidth={stroke} />
|
||||
<circle cx="12" cy="12" r="4" fill={accent} />
|
||||
</SvgBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SmallGameIcon;
|
||||
|
||||
@@ -45,6 +45,9 @@ const config = {
|
||||
LOG_LEVEL: 'debug',
|
||||
};
|
||||
|
||||
/** 与后端 INFOGENIE_SITE_ADMIN_TOKEN 一致;不设则无法进入 /admin */
|
||||
const siteAdminGate = String(import.meta.env.VITE_SITE_ADMIN_GATE || '').trim();
|
||||
|
||||
export const ENV_CONFIG = {
|
||||
API_URL: config.API_URL,
|
||||
AUTH_URL: config.AUTH_URL,
|
||||
@@ -55,6 +58,7 @@ export const ENV_CONFIG = {
|
||||
APP_VERSION: '2.2.3',
|
||||
CLIENT_ID: 'infogenie',
|
||||
CLIENT_NAME: '万象口袋',
|
||||
SITE_ADMIN_GATE: siteAdminGate,
|
||||
};
|
||||
|
||||
export const getEnvVar = (key, defaultValue = '') => {
|
||||
|
||||
@@ -11,8 +11,6 @@ import { ENV_CONFIG } from '../config/env';
|
||||
import { API_CATEGORIES, TOOLBOX_ITEMS, API_SOURCES } from '../config/Api60sConfig';
|
||||
import { AI_MODEL_APPS } from '../config/StaticPageConfig';
|
||||
|
||||
const ADMIN_TOKEN = 'shumengya520';
|
||||
|
||||
const InfoCard = ({ children }) => (
|
||||
<div className="bg-white/95 rounded-[14px] p-6 shadow-[0_4px_12px_rgba(0,0,0,0.08)] mb-6">{children}</div>
|
||||
);
|
||||
@@ -52,6 +50,7 @@ function normalizeHealthPayload(data) {
|
||||
: data.database === 'connected';
|
||||
const mysqlStatus = mysql?.status || data.database || (mysqlOk ? 'connected' : 'unknown');
|
||||
const sixty = data.sixty_api && typeof data.sixty_api === 'object' ? data.sixty_api : null;
|
||||
const redis = data.redis && typeof data.redis === 'object' ? data.redis : null;
|
||||
return {
|
||||
overall: data.status || 'unknown',
|
||||
ts: data.timestamp,
|
||||
@@ -59,11 +58,13 @@ function normalizeHealthPayload(data) {
|
||||
mysqlOk,
|
||||
mysqlStatus,
|
||||
sixty,
|
||||
redis,
|
||||
};
|
||||
}
|
||||
|
||||
const StatusPill = ({ ok, pending, unknown }) => {
|
||||
const StatusPill = ({ ok, pending, unknown, muted, mutedText = '未启用' }) => {
|
||||
if (pending) return <span className="text-xs font-semibold text-gray-400">检测中…</span>;
|
||||
if (muted) return <span className="text-xs font-semibold text-gray-700 bg-gray-100 px-2 py-0.5 rounded-md">{mutedText}</span>;
|
||||
if (unknown) return <span className="text-xs font-semibold text-amber-800 bg-amber-100 px-2 py-0.5 rounded-md">未上报</span>;
|
||||
return (
|
||||
<span
|
||||
@@ -77,8 +78,15 @@ const StatusPill = ({ ok, pending, unknown }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function formatPasswordConfigured(v) {
|
||||
if (v === true) return '已配置';
|
||||
if (v === false) return '未配置';
|
||||
return '—';
|
||||
}
|
||||
|
||||
const ADMIN_SECTIONS = [
|
||||
{ id: 'overview', label: '概览', short: '概览', Icon: FiHome },
|
||||
{ id: 'monitoring', label: '状态监控', short: '状态', Icon: FiActivity },
|
||||
{ id: 'ai-upstream', label: 'AI 上游配置', short: 'AI 上游', Icon: FiCpu },
|
||||
{ id: 'sixty-node', label: '60s 数据源', short: '60s 节点', Icon: FiRadio },
|
||||
{ id: 'sixty-display', label: '60s 前台展示', short: '60s 展示', Icon: FiGrid },
|
||||
@@ -114,6 +122,8 @@ const AdminPage = () => {
|
||||
const [healthRaw, setHealthRaw] = useState(null);
|
||||
const [healthLoading, setHealthLoading] = useState(false);
|
||||
const [healthFetchError, setHealthFetchError] = useState(null);
|
||||
const [diagnostics, setDiagnostics] = useState(null);
|
||||
const [diagError, setDiagError] = useState(null);
|
||||
|
||||
const disabled60sSet = useMemo(() => new Set(disabled60s), [disabled60s]);
|
||||
const disabledAIModelsSet = useMemo(() => new Set(disabledAIModels), [disabledAIModels]);
|
||||
@@ -145,18 +155,46 @@ const AdminPage = () => {
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const fetchAggregateHealth = useCallback(async () => {
|
||||
const fetchMonitoringData = useCallback(async () => {
|
||||
setHealthLoading(true);
|
||||
setHealthFetchError(null);
|
||||
try {
|
||||
const r = await fetch(`${ENV_CONFIG.API_URL}/api/health`);
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
const d = await r.json();
|
||||
setHealthRaw(d);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setDiagError(null);
|
||||
const token = localStorage.getItem('admin_token') || '';
|
||||
const healthP = fetch(`${ENV_CONFIG.API_URL}/api/health`).then(async (r) => {
|
||||
if (!r.ok) throw new Error(`健康检查 HTTP ${r.status}`);
|
||||
return r.json();
|
||||
});
|
||||
const diagP = fetch(`${ENV_CONFIG.API_URL}/api/admin/site/diagnostics`, {
|
||||
headers: { 'X-Site-Admin-Token': token },
|
||||
}).then(async (r) => {
|
||||
if (!r.ok) {
|
||||
let msg = `HTTP ${r.status}`;
|
||||
try {
|
||||
const j = await r.json();
|
||||
if (j.error === 'forbidden') {
|
||||
msg = '诊断接口拒绝访问(X-Site-Admin-Token 与后端 INFOGENIE_SITE_ADMIN_TOKEN 不一致)';
|
||||
} else if (j.message) {
|
||||
msg = j.message;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
return r.json();
|
||||
});
|
||||
const [hSettled, dSettled] = await Promise.allSettled([healthP, diagP]);
|
||||
if (hSettled.status === 'fulfilled') {
|
||||
setHealthRaw(hSettled.value);
|
||||
setHealthFetchError(null);
|
||||
} else {
|
||||
setHealthRaw(null);
|
||||
setHealthFetchError(e.message || '请求失败');
|
||||
setHealthFetchError(hSettled.reason?.message || '健康检查失败');
|
||||
}
|
||||
if (dSettled.status === 'fulfilled') {
|
||||
setDiagnostics(dSettled.value);
|
||||
setDiagError(null);
|
||||
} else {
|
||||
setDiagnostics(null);
|
||||
setDiagError(dSettled.reason?.message || '诊断加载失败');
|
||||
}
|
||||
setHealthLoading(false);
|
||||
}, []);
|
||||
@@ -164,7 +202,7 @@ const AdminPage = () => {
|
||||
const healthNorm = useMemo(() => normalizeHealthPayload(healthRaw), [healthRaw]);
|
||||
|
||||
const loadAiRuntime = useCallback(async () => {
|
||||
if (localStorage.getItem('admin_token') !== ADMIN_TOKEN) return;
|
||||
if (localStorage.getItem('admin_token') !== ENV_CONFIG.SITE_ADMIN_GATE || !ENV_CONFIG.SITE_ADMIN_GATE) return;
|
||||
setLoadingAi(true);
|
||||
try {
|
||||
const token = localStorage.getItem('admin_token') || '';
|
||||
@@ -186,7 +224,7 @@ const AdminPage = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchSysInfo(); }, [fetchSysInfo]);
|
||||
useEffect(() => { fetchAggregateHealth(); }, [fetchAggregateHealth]);
|
||||
useEffect(() => { fetchMonitoringData(); }, [fetchMonitoringData]);
|
||||
useEffect(() => { load60sConfig(); }, [load60sConfig]);
|
||||
useEffect(() => { loadAiRuntime(); }, [loadAiRuntime]);
|
||||
|
||||
@@ -319,7 +357,7 @@ const AdminPage = () => {
|
||||
};
|
||||
|
||||
const storedToken = localStorage.getItem('admin_token');
|
||||
if (storedToken !== ADMIN_TOKEN) return <Navigate to="/" replace />;
|
||||
if (!ENV_CONFIG.SITE_ADMIN_GATE || storedToken !== ENV_CONFIG.SITE_ADMIN_GATE) return <Navigate to="/" replace />;
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('admin_token');
|
||||
@@ -440,96 +478,6 @@ const AdminPage = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="text-base text-white mb-3 [text-shadow:0_1px_4px_rgba(0,0,0,0.12)] flex items-center gap-2">
|
||||
<FiActivity className="opacity-90" />
|
||||
状态监控
|
||||
</h3>
|
||||
<InfoCard>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 mb-4">
|
||||
<p className="text-sm text-gray-500 m-0 leading-snug max-w-xl">
|
||||
数据源:<code className="text-xs bg-gray-100 px-1 rounded">GET /api/health</code>
|
||||
。含 MySQL Ping 与当前配置的 60s 上游探测(请求其公开接口 <code className="text-xs bg-gray-100 px-1 rounded">/v2/ip</code>)。
|
||||
{healthNorm?.overall === 'degraded' && (
|
||||
<span className="text-amber-700 font-semibold"> 当前整体为 degraded。</span>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchAggregateHealth}
|
||||
disabled={healthLoading}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 disabled:opacity-50 shrink-0"
|
||||
>
|
||||
<FiRefreshCw size={16} className={healthLoading ? 'animate-spin' : ''} />
|
||||
{healthLoading ? '检测中…' : '重新检测'}
|
||||
</button>
|
||||
</div>
|
||||
{healthFetchError && (
|
||||
<p className="text-sm text-red-600 mb-3 m-0">
|
||||
无法连接健康检查接口:{healthFetchError}(请确认 Go 后端已启动且已更新到包含聚合健康字段的版本)
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 py-2 border-b border-gray-100">
|
||||
<div>
|
||||
<div className="text-gray-800 font-semibold text-sm">万象后端 API(InfoGenie Go)</div>
|
||||
<div className="text-gray-400 text-xs mt-0.5">{ENV_CONFIG.API_URL}</div>
|
||||
</div>
|
||||
<StatusPill
|
||||
ok={!healthFetchError && (healthNorm?.backendApiOk !== false)}
|
||||
pending={healthLoading && !healthRaw && !healthFetchError}
|
||||
unknown={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 py-2 border-b border-gray-100">
|
||||
<div>
|
||||
<div className="text-gray-800 font-semibold text-sm">MySQL 数据库</div>
|
||||
<div className="text-gray-500 text-xs mt-0.5">
|
||||
{healthNorm ? `状态字段:${healthNorm.mysqlStatus}` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill
|
||||
ok={!!healthNorm?.mysqlOk}
|
||||
pending={healthLoading && !healthRaw && !healthFetchError}
|
||||
unknown={!healthFetchError && !healthLoading && !healthRaw}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2 py-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-gray-800 font-semibold text-sm">60s API 上游(当前节点)</div>
|
||||
{healthNorm?.sixty ? (
|
||||
<div className="text-gray-500 text-xs mt-1 space-y-0.5 break-all">
|
||||
<div>
|
||||
{healthNorm.sixty.label || healthNorm.sixty.source_id}
|
||||
{healthNorm.sixty.base_url ? ` · ${healthNorm.sixty.base_url}` : ''}
|
||||
</div>
|
||||
{typeof healthNorm.sixty.latency_ms === 'number' && (
|
||||
<div>探测耗时 {healthNorm.sixty.latency_ms} ms</div>
|
||||
)}
|
||||
{healthNorm.sixty.probe_url && (
|
||||
<div className="text-gray-400">探测 URL:{healthNorm.sixty.probe_url}</div>
|
||||
)}
|
||||
{healthNorm.sixty.error ? (
|
||||
<div className="text-red-600 font-medium">错误:{String(healthNorm.sixty.error)}</div>
|
||||
) : null}
|
||||
{typeof healthNorm.sixty.http_status === 'number' && healthNorm.sixty.http_status > 0 ? (
|
||||
<div>HTTP {healthNorm.sixty.http_status}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
{!healthFetchError && healthRaw ? '响应中无 sixty_api 字段,请升级后端。' : '—'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<StatusPill
|
||||
ok={!!healthNorm?.sixty?.ok}
|
||||
pending={healthLoading && !healthRaw && !healthFetchError}
|
||||
unknown={!healthFetchError && !!healthRaw && !healthNorm?.sixty}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoCard>
|
||||
|
||||
<h3 className="text-base text-white mb-3 [text-shadow:0_1px_4px_rgba(0,0,0,0.12)]">系统信息</h3>
|
||||
<InfoCard>
|
||||
<InfoRow label="前端环境" value={ENV_CONFIG.ENV || 'development'} />
|
||||
@@ -557,6 +505,223 @@ const AdminPage = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{adminSection === 'monitoring' && (
|
||||
<InfoCard>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 mb-4">
|
||||
<p className="text-sm text-gray-500 m-0 leading-snug max-w-2xl">
|
||||
<strong>实时探测</strong>:
|
||||
<code className="text-xs bg-gray-100 px-1 rounded">GET /api/health</code>
|
||||
(MySQL Ping、60s 上游 <code className="text-xs bg-gray-100 px-1 rounded">/v2/ip</code>
|
||||
、可选 Redis Ping)。
|
||||
<strong className="font-semibold"> 配置快照</strong>:
|
||||
<code className="text-xs bg-gray-100 px-1 rounded">GET /api/admin/site/diagnostics</code>
|
||||
(监听地址、MySQL/Redis 连接目标、库名与用户、SMTP 与认证中心 URL 等;<span className="text-gray-600">不含密码明文</span>)。
|
||||
{healthNorm?.overall === 'degraded' && (
|
||||
<span className="text-amber-700 font-semibold"> 当前整体为 degraded。</span>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchMonitoringData}
|
||||
disabled={healthLoading}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 disabled:opacity-50 shrink-0"
|
||||
>
|
||||
<FiRefreshCw size={16} className={healthLoading ? 'animate-spin' : ''} />
|
||||
{healthLoading ? '刷新中…' : '刷新探测与配置'}
|
||||
</button>
|
||||
</div>
|
||||
{healthFetchError && (
|
||||
<p className="text-sm text-red-600 mb-3 m-0">
|
||||
健康检查失败:{healthFetchError}
|
||||
</p>
|
||||
)}
|
||||
{diagError && (
|
||||
<p className="text-sm text-amber-900 bg-amber-50 border border-amber-100 rounded-lg px-3 py-2 mb-3 m-0">
|
||||
配置快照失败:{diagError}
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div className="pb-3 border-b border-gray-100">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="text-gray-800 font-semibold text-sm">万象后端 API(Go 进程)</div>
|
||||
<StatusPill
|
||||
ok={!healthFetchError && (healthNorm?.backendApiOk !== false)}
|
||||
pending={healthLoading && !healthRaw && !healthFetchError}
|
||||
unknown={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-2 space-y-1 break-all">
|
||||
<div><span className="text-gray-400">前端当前 API 基址:</span>{ENV_CONFIG.API_URL}</div>
|
||||
{diagnostics?.app && (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-gray-400">进程监听:</span>
|
||||
{diagnostics.app.listen_addr || `0.0.0.0:${diagnostics.app.listen_port || '—'}`}
|
||||
{diagnostics.app.listen_port ? `(端口 ${diagnostics.app.listen_port})` : ''}
|
||||
</div>
|
||||
<div><span className="text-gray-400">APP_ENV:</span>{diagnostics.app.env || '—'}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-3 border-b border-gray-100">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="text-gray-800 font-semibold text-sm">MySQL 数据库</div>
|
||||
<StatusPill
|
||||
ok={!!healthNorm?.mysqlOk}
|
||||
pending={healthLoading && !healthRaw && !healthFetchError}
|
||||
unknown={!healthFetchError && !healthLoading && !healthRaw}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-2 space-y-1">
|
||||
{healthNorm && (
|
||||
<div><span className="text-gray-400">实时状态字段:</span>{healthNorm.mysqlStatus}</div>
|
||||
)}
|
||||
{diagnostics?.mysql && (
|
||||
<>
|
||||
<div className="break-all">
|
||||
<span className="text-gray-400">连接:</span>
|
||||
{diagnostics.mysql.host || '—'}:{diagnostics.mysql.port || '—'}
|
||||
</div>
|
||||
<div><span className="text-gray-400">数据库名:</span>{diagnostics.mysql.database || '—'}</div>
|
||||
<div><span className="text-gray-400">用户:</span>{diagnostics.mysql.user || '—'}</div>
|
||||
<div>
|
||||
<span className="text-gray-400">密码(环境变量):</span>
|
||||
{formatPasswordConfigured(diagnostics.mysql.password_configured)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-3 border-b border-gray-100">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="text-gray-800 font-semibold text-sm">60s API 上游</div>
|
||||
<StatusPill
|
||||
ok={!!healthNorm?.sixty?.ok}
|
||||
pending={healthLoading && !healthRaw && !healthFetchError}
|
||||
unknown={!healthFetchError && !!healthRaw && !healthNorm?.sixty}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-2 space-y-1 break-all">
|
||||
{diagnostics?.sixty_upstream && (
|
||||
<div>
|
||||
<span className="text-gray-400">配置的节点:</span>
|
||||
{diagnostics.sixty_upstream.label || diagnostics.sixty_upstream.source_id}
|
||||
{diagnostics.sixty_upstream.base_url ? ` · ${diagnostics.sixty_upstream.base_url}` : ''}
|
||||
</div>
|
||||
)}
|
||||
{healthNorm?.sixty ? (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-gray-400">探测目标:</span>
|
||||
{healthNorm.sixty.label || healthNorm.sixty.source_id}
|
||||
{healthNorm.sixty.base_url ? ` · ${healthNorm.sixty.base_url}` : ''}
|
||||
</div>
|
||||
{typeof healthNorm.sixty.latency_ms === 'number' && (
|
||||
<div>探测耗时 {healthNorm.sixty.latency_ms} ms</div>
|
||||
)}
|
||||
{healthNorm.sixty.probe_url && (
|
||||
<div className="text-gray-400">探测 URL:{healthNorm.sixty.probe_url}</div>
|
||||
)}
|
||||
{healthNorm.sixty.error ? (
|
||||
<div className="text-red-600 font-medium">错误:{String(healthNorm.sixty.error)}</div>
|
||||
) : null}
|
||||
{typeof healthNorm.sixty.http_status === 'number' && healthNorm.sixty.http_status > 0 ? (
|
||||
<div>HTTP {healthNorm.sixty.http_status}</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-1">
|
||||
{!healthFetchError && healthRaw ? '响应中无 sixty_api 字段,请升级后端。' : '—'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-3 border-b border-gray-100">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="text-gray-800 font-semibold text-sm">Redis 缓存</div>
|
||||
{healthLoading && !healthRaw && !healthFetchError ? (
|
||||
<StatusPill pending />
|
||||
) : healthNorm?.redis == null && healthRaw && !healthFetchError ? (
|
||||
<StatusPill unknown />
|
||||
) : healthNorm?.redis && healthNorm.redis.enabled === false ? (
|
||||
<StatusPill muted mutedText="未启用" />
|
||||
) : healthNorm?.redis && healthNorm.redis.enabled === true ? (
|
||||
<StatusPill ok={healthNorm.redis.ok === true} unknown={false} />
|
||||
) : (
|
||||
<StatusPill unknown />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-2 space-y-1 break-all">
|
||||
{!healthFetchError && healthRaw && healthNorm && healthNorm.redis == null && (
|
||||
<div>响应中无 redis 字段,请升级 Go 后端。</div>
|
||||
)}
|
||||
{healthNorm?.redis && healthNorm.redis.enabled === false && (
|
||||
<div>
|
||||
后端未启用(<code className="text-xs bg-gray-100 px-1 rounded">REDIS_ENABLED</code> 未打开);读站点配置时直连 MySQL。
|
||||
</div>
|
||||
)}
|
||||
{healthNorm?.redis && healthNorm.redis.enabled === true && healthNorm.redis.ok === false && healthNorm.redis.error && (
|
||||
<div className="text-red-600 font-medium">Ping 错误:{String(healthNorm.redis.error)}</div>
|
||||
)}
|
||||
{healthNorm?.redis && healthNorm.redis.enabled === true && healthNorm.redis.ok === true && (
|
||||
<div>健康检查 Redis Ping 成功(与业务共用同一套连接参数)。</div>
|
||||
)}
|
||||
{diagnostics?.redis && (
|
||||
<>
|
||||
<div><span className="text-gray-400">开关:</span>{diagnostics.redis.enabled ? '已启用' : '未启用'}</div>
|
||||
{diagnostics.redis.enabled ? (
|
||||
<>
|
||||
<div><span className="text-gray-400">地址(host:port):</span>{diagnostics.redis.addr || '—'}</div>
|
||||
<div><span className="text-gray-400">逻辑库编号:</span>{typeof diagnostics.redis.logical_db === 'number' ? diagnostics.redis.logical_db : '—'}</div>
|
||||
<div><span className="text-gray-400">Key 前缀:</span>{diagnostics.redis.key_prefix || '—'}</div>
|
||||
<div>
|
||||
<span className="text-gray-400">站点缓存 TTL:</span>
|
||||
{typeof diagnostics.redis.site_cache_ttl_sec === 'number'
|
||||
? `${diagnostics.redis.site_cache_ttl_sec} s`
|
||||
: '—'}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">密码(环境变量):</span>
|
||||
{formatPasswordConfigured(diagnostics.redis.password_configured)}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(diagnostics?.auth_center || diagnostics?.mail) && (
|
||||
<div className="space-y-3 pt-1">
|
||||
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wide">其他依赖(快照)</div>
|
||||
{diagnostics.auth_center && (
|
||||
<div className="rounded-lg bg-gray-50 border border-gray-100 px-3 py-2 text-xs text-gray-600 space-y-1">
|
||||
<div className="font-semibold text-gray-700">萌芽认证中心 API</div>
|
||||
<div className="break-all">{diagnostics.auth_center.api_url || '—'}</div>
|
||||
</div>
|
||||
)}
|
||||
{diagnostics.mail && (
|
||||
<div className="rounded-lg bg-gray-50 border border-gray-100 px-3 py-2 text-xs text-gray-600 space-y-1">
|
||||
<div className="font-semibold text-gray-700">邮件 SMTP</div>
|
||||
<div>
|
||||
{diagnostics.mail.host || '—'}:{diagnostics.mail.port ?? '—'}
|
||||
</div>
|
||||
<div className="break-all">发件账号:{diagnostics.mail.username || '—'}</div>
|
||||
<div>
|
||||
密码(环境变量):{formatPasswordConfigured(diagnostics.mail.password_configured)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InfoCard>
|
||||
)}
|
||||
|
||||
{adminSection === 'ai-upstream' && (
|
||||
<InfoCard>
|
||||
<p className="text-sm text-gray-500 leading-[1.55] mb-3">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `npm run dev` / `npm start` | 开发服务器(默认端口 3000) |
|
||||
| `npm run dev` / `npm start` | 开发服务器(Vite,默认端口 3000) |
|
||||
| `npm run build` | 生产构建,输出 `dist/` |
|
||||
| `npm run preview` | 本地预览 `dist/` 构建结果 |
|
||||
|
||||
@@ -23,7 +23,7 @@ infogenie-frontend/
|
||||
├── index.html ← Vite HTML 入口(根目录,非 public/)
|
||||
├── vite.config.js ← Vite 配置(React 插件、Tailwind 插件、路径别名)
|
||||
├── package.json
|
||||
├── .env.development ← 开发环境变量
|
||||
├── .env.development ← 开发环境变量(VITE_*)
|
||||
├── .env.production ← 生产环境变量
|
||||
├── public/ ← 静态资源(不参与构建,直接复制到 dist/)
|
||||
│ ├── assets/logo.png
|
||||
@@ -34,12 +34,12 @@ infogenie-frontend/
|
||||
└── src/
|
||||
├── index.js ← React 根挂载
|
||||
├── App.js ← 路由、布局、全局 Provider
|
||||
├── components/ ← 公共组件
|
||||
├── pages/ ← 页面组件
|
||||
├── components/ ← 公共组件(含 FullscreenEmbed、SmallGameIcons 等)
|
||||
├── pages/ ← 页面组件(含 AdminPage、SmallGamePage)
|
||||
├── contexts/ ← 全局 Context
|
||||
├── hooks/ ← 自定义 Hooks
|
||||
├── hooks/ ← 自定义 Hooks(如 useFeatureCardClickStats)
|
||||
├── utils/ ← Axios 封装、可见性过滤等
|
||||
├── config/ ← 环境变量解析、路由/内容配置
|
||||
├── config/ ← 环境变量解析、Api60sConfig、StaticPageConfig
|
||||
└── styles/
|
||||
├── index.css ← Tailwind 入口 + 全局 base 样式 + 自定义动画
|
||||
└── shared.js ← 设计系统组件(Tailwind 函数组件)
|
||||
@@ -52,7 +52,7 @@ infogenie-frontend/
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| `@vitejs/plugin-react` | React Fast Refresh + JSX 转换(含 `.js` 扩展名支持) |
|
||||
| `@tailwindcss/vite` | Tailwind CSS v4 Vite 插件,零配置文件 |
|
||||
| `@tailwindcss/vite` | Tailwind CSS v4 Vite 插件 |
|
||||
| `treat-js-as-jsx` | 内联插件,使 `.js` 文件中的 JSX 正常编译 |
|
||||
| `resolve.alias['@']` | `@/` 映射到 `src/` |
|
||||
| `base: '/'` | 部署根路径 |
|
||||
@@ -62,18 +62,19 @@ infogenie-frontend/
|
||||
|
||||
## 环境变量(`src/config/env.js`)
|
||||
|
||||
变量名使用 Vite 规范的 `VITE_` 前缀(通过 `import.meta.env` 访问):
|
||||
变量名使用 Vite 规范的 **`VITE_`** 前缀(通过 `import.meta.env` 访问):
|
||||
|
||||
| 变量 | 作用 | 开发默认 / 生产默认 |
|
||||
|------|------|---------------------|
|
||||
| `VITE_API_URL` | 万象口袋 **Go 后端** 根地址 | dev:`http://127.0.0.1:5002`;prod:`https://infogenie.api.shumengya.top` |
|
||||
| `VITE_API_URL` | 万象口袋 **Go 后端** 根地址 | dev:`http://127.0.0.1:5002`;prod 见 `.env.production` |
|
||||
| `VITE_AUTH_URL` | 认证中心 **页面** 域名 | `https://auth.shumengya.top` |
|
||||
| `VITE_AUTH_API_URL` | 认证中心 **API** 根地址 | `https://auth.api.shumengya.top` |
|
||||
| `VITE_DEBUG` | 调试开关 | `'true'` 开启 |
|
||||
| `VITE_SITE_ADMIN_GATE` | 管理员后台口令,须与后端 `INFOGENIE_SITE_ADMIN_TOKEN` 一致 | 本地写在 `.env.development`,勿提交 |
|
||||
|
||||
运行时可通过 `window.ENV_CONFIG` 查看解析结果。
|
||||
运行时可通过 `window.ENV_CONFIG` 查看解析结果(若已注入)。
|
||||
|
||||
> **迁移注意**:从 CRA 迁移时原 `REACT_APP_*` 变量已全部重命名为 `VITE_*`。
|
||||
> **迁移注意**:由 CRA 迁出后,原 `REACT_APP_*` 已统一为 `VITE_*`。
|
||||
|
||||
---
|
||||
|
||||
@@ -91,13 +92,27 @@ infogenie-frontend/
|
||||
| `/toolbox` | 工具箱 |
|
||||
| `/aimodel` | AI 应用(需登录) |
|
||||
| `/profile` | 个人中心 |
|
||||
| `/admin` | 管理 |
|
||||
| `/admin` | **管理员后台**(本地 `admin_token` 校验) |
|
||||
| `*` | 重定向首页 |
|
||||
|
||||
### 关键组件
|
||||
### 管理员后台(`src/pages/AdminPage.js`)
|
||||
|
||||
- **`RandomSiteBackground`**:全站随机背景(`https://randbg.api.smyhub.com/api/random?format=json`),毛玻璃模糊,会话级缓存。
|
||||
- **`Header` / `Footer`**:半透明绿 + `backdrop-filter`;**移动端 `Navigation`** 底栏为不透明渐变 + 圆角顶。
|
||||
- 浏览器 **`localStorage.admin_token`** 与内置口令一致方可进入(与后端 **`INFOGENIE_SITE_ADMIN_TOKEN`** 用于接口时是同一套站点管理员概念)。
|
||||
- **功能分区**(左侧):概览、**状态监控**、AI 上游、60s 数据源、60s 前台展示、AI 前台展示。
|
||||
- **状态监控**:
|
||||
- 并行请求 **`GET /api/health`**(MySQL / 60s 上游 / Redis Ping)与 **`GET /api/admin/site/diagnostics`**(进程监听、MySQL/Redis 连接目标、库名/用户、SMTP、认证中心 URL 等;**不含密码**)。
|
||||
- 请求诊断接口时携带头 **`X-Site-Admin-Token: <admin_token>`**。
|
||||
- 「刷新探测与配置」会同时刷新上述两个数据源。
|
||||
|
||||
### 休闲游戏页(`src/pages/SmallGamePage.js`)
|
||||
|
||||
- 游戏列表来自 **`src/config/StaticPageConfig.js`** 的 `SMALL_GAMES`。
|
||||
- 卡片图标由 **`src/components/SmallGameIcons.js`** 按 `game.id` 渲染 **Emoji**(如 2048→🔢、俄罗斯方块→🧱 等),嵌入在 **`FeatureCardIcon`** 内。
|
||||
|
||||
### 其他关键组件
|
||||
|
||||
- **`RandomSiteBackground`**:全站随机背景,毛玻璃模糊,会话级缓存。
|
||||
- **`Header` / `Footer` / `Navigation`**:导航与布局。
|
||||
- **`FullscreenEmbed`**:全屏 iframe(游戏、工具箱、AI 静态页),支持注入 token、加载超时提示。
|
||||
- **`ParticleEffect`**:全局点击粒子动画。
|
||||
|
||||
@@ -119,17 +134,12 @@ infogenie-frontend/
|
||||
|
||||
### 设计系统(`src/styles/shared.js`)
|
||||
|
||||
所有组件均为 **React 函数组件**,接受 `className` prop(可与额外 Tailwind 类合并):
|
||||
所有组件均为 **React 函数组件**,接受 `className` prop(可与 Tailwind 类合并):
|
||||
|
||||
- **`PageWrapper`**:页面容器,带 `animate-page-enter` 入场动画
|
||||
- **`Container`**:最大宽度容器,`$narrow`(800px)/ 默认(1200px)
|
||||
- **`FeatureGrid`**:响应式 5→4→3→2 列网格,带 `animate-fade-up`
|
||||
- **`CatalogCard`** / **`FeatureCard`**:统一卡片样式,顶条渐变色动态注入(`$c` prop)
|
||||
- **`PageWrapper`**、**`Container`**、**`FeatureGrid`**、**`CatalogCard`**、**`FeatureCard`**
|
||||
- **`FeatureCardUseCount`**:卡片右上角点击次数角标
|
||||
- **`ModuleCard`**:首页大模块横向卡片(包装 `react-router-dom` `Link`)
|
||||
- **`accentFromGradient`**:工具函数,从 gradient 字符串提取首色
|
||||
|
||||
> 动态样式(渐变色、动态 grid 列数等)通过 inline `style` prop 注入,Tailwind 负责静态部分。
|
||||
- **`ModuleCard`**:首页大模块卡片(`Link`)
|
||||
- **`accentFromGradient`**:从 gradient 字符串提取首色
|
||||
|
||||
---
|
||||
|
||||
@@ -146,9 +156,9 @@ infogenie-frontend/
|
||||
与 Vite 主站并列的大量**免构建**页面:
|
||||
|
||||
- **`public/aimodelapp/<应用名>/`**:AI 小应用(HTML + `script.js` + 可选 `env.js`)。
|
||||
- 统一聊天入口:**`public/aimodelapp/shared/ai-chat.js`** 的 `AiChat.complete()`。
|
||||
- 统一聊天入口:**`public/aimodelapp/shared/ai-chat.js`**。
|
||||
- **优先**请求 **`POST /api/aimodelapp/chat/stream`**(SSE),失败或无内容时回退 **`POST /api/aimodelapp/chat`**。
|
||||
- **`public/smallgame/`**、**`public/toolbox/`**:独立小游戏与工具页,由 `FullscreenEmbed` 打开。
|
||||
- **`public/smallgame/`**、**`public/toolbox/`**:独立页,由 **`FullscreenEmbed`** 打开。
|
||||
|
||||
各 `aimodelapp` 子目录的 **`env.js`** / **`API_CONFIG`** 需指向与主站一致的 Go 后端地址(通常与 `VITE_API_URL` 同源或同网段)。
|
||||
|
||||
@@ -156,14 +166,15 @@ infogenie-frontend/
|
||||
|
||||
## 与后端协作要点
|
||||
|
||||
1. **登录**:token 存 `localStorage`,AI 与受保护接口依赖 JWT。
|
||||
2. **AI**:静态页通过 **同源或配置的 API** 调 Go 的 `/api/aimodelapp/*`;流式响应类型为 **`text/event-stream`**。
|
||||
3. **站点开关**:60s / AI 应用显隐由 `GET /api/site/*-disabled` 等驱动(见后端文档)。
|
||||
4. **卡片统计**:四大板块列表页的 `CatalogCard` 已接 `feature-card-clicks` 接口。
|
||||
1. **登录**:token 存 `localStorage`,AI 与受保护接口依赖 JWT(萌芽认证中心签发)。
|
||||
2. **AI**:静态页通过配置的 API 调 Go 的 `/api/aimodelapp/*`;流式为 **`text/event-stream`**。
|
||||
3. **站点开关**:60s / AI 应用显隐由 `GET /api/site/*-disabled` 等驱动。
|
||||
4. **卡片统计**:列表页 `CatalogCard` 与 **`feature-card-clicks`** 接口对接;可选 Redis 缓存由后端控制。
|
||||
5. **管理员**:写配置类接口需后端配置 **`INFOGENIE_SITE_ADMIN_TOKEN`**,与前端管理员口令一致。
|
||||
|
||||
---
|
||||
|
||||
## 其他文档
|
||||
|
||||
- **Go 后端 API 与表结构**:[`infogenie-backend-go/后端文档.md`](../infogenie-backend-go/后端文档.md)
|
||||
- **仓库与工具说明**:[`../.claude/README.md`](../.claude/README.md)
|
||||
- **Go 后端 API、Redis、健康与诊断**:[`infogenie-backend-go/后端文档.md`](../infogenie-backend-go/后端文档.md)
|
||||
- **仓库总览与多模块说明**:[`../README.md`](../README.md)
|
||||
|
||||
Reference in New Issue
Block a user