diff --git a/admin/src/app/admin/projects/[id]/page.tsx b/admin/src/app/admin/projects/[id]/page.tsx index c4f8bfc..ee588f4 100644 --- a/admin/src/app/admin/projects/[id]/page.tsx +++ b/admin/src/app/admin/projects/[id]/page.tsx @@ -61,6 +61,7 @@ export default function ProjectDetailPage() { // Member management const [showAddMember, setShowAddMember] = useState(false); const [addUserId, setAddUserId] = useState(""); + const [addUserDisplay, setAddUserDisplay] = useState(""); // shown in the input field const [addScope, setAddScope] = useState("member"); const [addMemberLoading, setAddMemberLoading] = useState(false); const [addMemberError, setAddMemberError] = useState(""); @@ -111,15 +112,24 @@ export default function ProjectDetailPage() { } async function handleUserSearch(q: string) { - setAddUserId(q); + setAddUserId(q); // raw typed text for form submission if (q.length < 2) { setSearchUsers([]); return; } setSearchLoading(true); try { const res = await fetch(`/api/platform/users?search=${encodeURIComponent(q)}&pageSize=10`); const data = await res.json(); + if (!res.ok) { + setSearchUsers([]); + setAddMemberError(data.error || "搜索失败,请重试"); + return; + } const existingIds = new Set(members.map(m => m.uid)); setSearchUsers((data.users || []).filter((u: { uid: string }) => !existingIds.has(u.uid))); - } catch { setSearchUsers([]); } + setAddMemberError(""); + } catch { + setSearchUsers([]); + setAddMemberError("搜索失败,请检查网络连接"); + } finally { setSearchLoading(false); } } @@ -137,6 +147,7 @@ export default function ProjectDetailPage() { if (!res.ok) { setAddMemberError(data.error || "添加失败"); return; } setShowAddMember(false); setAddUserId(""); + setAddUserDisplay(""); setAddScope("member"); setSearchUsers([]); reload(); @@ -347,8 +358,8 @@ export default function ProjectDetailPage() { handleUserSearch(e.target.value)} + value={addUserDisplay || addUserId} + onChange={e => { setAddUserDisplay(e.target.value); handleUserSearch(e.target.value); }} placeholder="输入用户名搜索..." autoFocus /> @@ -358,7 +369,7 @@ export default function ProjectDetailPage() {
{ setAddUserId(u.uid); setSearchUsers([]); }} + onClick={() => { setAddUserId(u.uid); setAddUserDisplay(u.username); setSearchUsers([]); }} >
{u.username}
{u.display_name || ""}
@@ -376,7 +387,7 @@ export default function ProjectDetailPage() {
- + diff --git a/admin/src/app/admin/workspaces/[id]/page.tsx b/admin/src/app/admin/workspaces/[id]/page.tsx index ded5e80..a805b62 100644 --- a/admin/src/app/admin/workspaces/[id]/page.tsx +++ b/admin/src/app/admin/workspaces/[id]/page.tsx @@ -73,6 +73,7 @@ export default function WorkspaceDetailPage() { // Member management const [showAddMember, setShowAddMember] = useState(false); const [addUserId, setAddUserId] = useState(""); + const [addUserDisplay, setAddUserDisplay] = useState(""); // shown in the input field const [addRole, setAddRole] = useState("member"); const [addMemberLoading, setAddMemberLoading] = useState(false); const [addMemberError, setAddMemberError] = useState(""); @@ -163,15 +164,25 @@ export default function WorkspaceDetailPage() { // Search users for adding member async function handleUserSearch(q: string) { - setAddUserId(q); + setAddUserId(q); // keep the raw typed text for form submission if (q.length < 2) { setSearchUsers([]); return; } setSearchLoading(true); try { const res = await fetch(`/api/platform/users?search=${encodeURIComponent(q)}&pageSize=10`); const data = await res.json(); - const existingIds = new Set(members.map(m => m.uid)); + if (!res.ok) { + // Show error from API + setSearchUsers([]); + setAddMemberError(data.error || "搜索失败,请重试"); + return; + } + const existingIds = new Set(members.map(m => m.userId)); setSearchUsers((data.users || []).filter((u: { uid: string }) => !existingIds.has(u.uid))); - } catch { setSearchUsers([]); } + setAddMemberError(""); // clear previous errors + } catch { + setSearchUsers([]); + setAddMemberError("搜索失败,请检查网络连接"); + } finally { setSearchLoading(false); } } @@ -189,6 +200,7 @@ export default function WorkspaceDetailPage() { if (!res.ok) { setAddMemberError(data.error || "添加失败"); return; } setShowAddMember(false); setAddUserId(""); + setAddUserDisplay(""); setAddRole("member"); setSearchUsers([]); reload(); @@ -496,8 +508,8 @@ export default function WorkspaceDetailPage() { handleUserSearch(e.target.value)} + value={addUserDisplay || addUserId} + onChange={(e) => { setAddUserDisplay(e.target.value); handleUserSearch(e.target.value); }} placeholder="输入用户名搜索..." autoFocus /> @@ -509,6 +521,7 @@ export default function WorkspaceDetailPage() { style={{ padding: "8px 12px", cursor: "pointer", borderBottom: "1px solid #f0f0f0" }} onClick={() => { setAddUserId(u.uid); + setAddUserDisplay(u.username); setSearchUsers([]); }} > @@ -528,7 +541,7 @@ export default function WorkspaceDetailPage() {
- + diff --git a/admin/src/app/platform/sessions/page.tsx b/admin/src/app/platform/sessions/page.tsx index 9da3b2f..2041965 100644 --- a/admin/src/app/platform/sessions/page.tsx +++ b/admin/src/app/platform/sessions/page.tsx @@ -2,8 +2,14 @@ import { useEffect, useState } from "react"; import { format } from "date-fns"; +import { + listUserSessions, + kickUser, + getUserStatus, + type UserSession, +} from "@/lib/admin-rpc"; -interface SessionInfo { +interface PlatformSessionInfo { sessionId: string; userId: string; username: string | null; @@ -14,14 +20,17 @@ interface SessionInfo { } export default function PlatformSessionsPage() { - const [sessions, setSessions] = useState([]); + const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); const [kicking, setKicking] = useState(null); + const [adminRpcAvailable, setAdminRpcAvailable] = useState(false); useEffect(() => { loadSessions(); }, []); + async function loadSessions() { setLoading(true); try { + // Load platform sessions from the app's REST API (Redis-based) const res = await fetch("/api/platform/sessions"); if (!res.ok) { setSessions([]); @@ -37,6 +46,31 @@ export default function PlatformSessionsPage() { } } + async function handleKickAllSessions(userId: string) { + if (!confirm(`强制下线用户 ${userId} 的所有会话?`)) return; + setKicking(userId); + try { + if (adminRpcAvailable) { + await kickUser(userId); + } else { + // Fallback: kick sessions via Redis (app REST API) + const userSessions = sessions.filter((s) => s.userId === userId); + await Promise.all( + userSessions.map((s) => + fetch("/api/platform/sessions", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: s.sessionId }), + }) + ) + ); + } + loadSessions(); + } finally { + setKicking(null); + } + } + async function handleKickSession(sessionId: string) { if (!confirm("确定强制下线该会话吗?")) return; setKicking(sessionId); @@ -47,11 +81,13 @@ export default function PlatformSessionsPage() { body: JSON.stringify({ sessionId }), }); loadSessions(); - } finally { setKicking(null); } + } finally { + setKicking(null); + } } // Group by user - const byUser = sessions.reduce>((acc, s) => { + const byUser = sessions.reduce>((acc, s) => { const key = s.userId || "unknown"; if (!acc[key]) acc[key] = []; acc[key].push(s); @@ -65,9 +101,30 @@ export default function PlatformSessionsPage() {

平台用户在线会话

共 {sessions.length} 个活跃会话,涉及 {Object.keys(byUser).length} 个用户 + {adminRpcAvailable && ( + + ● adminrpc 已连接 + + )}

- +
+ + +
{loading ? ( @@ -91,23 +148,19 @@ export default function PlatformSessionsPage() { {userSessions.length} 个会话 + {adminRpcAvailable && ( + + )} - + {adminRpcAvailable && ( + + )}
@@ -115,6 +168,7 @@ export default function PlatformSessionsPage() { Session ID + 工作空间 登录时间 IP User Agent @@ -129,10 +183,15 @@ export default function PlatformSessionsPage() { {s.sessionId.slice(0, 16)}... + + {s.workspaceId ? ( + {s.workspaceId.slice(0, 8)}... + ) : "—"} + {s.createdAt ? format(new Date(s.createdAt), "yyyy-MM-dd HH:mm") : "—"} {s.ipAddress || "—"} {s.userAgent || "—"} @@ -143,7 +202,7 @@ export default function PlatformSessionsPage() { disabled={kicking === s.sessionId} onClick={() => handleKickSession(s.sessionId)} > - {kicking === s.sessionId ? "处理中..." : "下线"} + {kicking === s.sessionId ? "..." : "下线"} @@ -157,3 +216,23 @@ export default function PlatformSessionsPage() {
); } + +// ─── AdminRpc Status Badge ──────────────────────────────────────────────────── + +function AdminRpcUserStatus({ userId }: { userId: string }) { + const [status, setStatus] = useState(null); + + useEffect(() => { + getUserStatus(userId) + .then((r) => setStatus(r.status)) + .catch(() => setStatus(null)); + }, [userId]); + + if (!status) return null; + const color = status === "Online" ? "#22c55e" : "#f59e0b"; + return ( + + [{status}] + + ); +} diff --git a/admin/src/lib/env.ts b/admin/src/lib/env.ts index 18f7577..01c1bb8 100644 --- a/admin/src/lib/env.ts +++ b/admin/src/lib/env.ts @@ -48,3 +48,8 @@ export const RUST_BACKEND_URL = process.env.RUST_BACKEND_URL || "http://localhost:3000"; export const ADMIN_API_SHARED_KEY = process.env.ADMIN_API_SHARED_KEY || ""; + +// adminrpc HTTP 服务地址(k8s 内部默认地址) +// 在 Kubernetes 环境中默认使用 Service DNS,在本地开发时覆盖为 localhost:9091 +export const ADMIN_RPC_URL = + process.env.ADMIN_RPC_URL || "http://adminrpc.admin.svc.cluster.local:9091"; diff --git a/src/app/page.tsx b/src/app/page.tsx index c0e3b79..8ecb89f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ import {useEffect} from 'react'; import {useNavigate} from 'react-router-dom'; import {LandingNav} from '@/components/landing/landing-nav'; -import {LandingHero, LandingFeatures, LandingHighlight} from '@/components/landing/landing-sections'; +import {LandingFeatures, LandingHero, LandingHighlight} from '@/components/landing/landing-sections'; import {LandingFooter} from '@/components/landing/landing-footer'; import {useUser} from '@/contexts/user-context'; @@ -9,25 +9,19 @@ export default function GitDataLandingPage() { const navigate = useNavigate(); const {isAuthenticated} = useUser(); const handleRegister = () => navigate('/auth/register'); - - // Redirect authenticated users to workspace useEffect(() => { if (isAuthenticated) { navigate('/w/me', {replace: true}); } }, [isAuthenticated, navigate]); - // Don't render landing page for authenticated users if (isAuthenticated) { return null; } - return (
- {/* Subtle background grid */}
-
diff --git a/src/client/types.gen.ts b/src/client/types.gen.ts index 5e9a71d..b9be364 100644 --- a/src/client/types.gen.ts +++ b/src/client/types.gen.ts @@ -4360,6 +4360,7 @@ export type RoomAiResponse = { think: boolean; stream: boolean; min_score?: number | null; + agent_type?: string | null; created_at: string; updated_at: string; }; @@ -4375,6 +4376,7 @@ export type RoomAiUpsertRequest = { think?: boolean | null; stream?: boolean | null; min_score?: number | null; + agent_type?: string | null; }; export type RoomCategoryCreateRequest = { diff --git a/src/components/room/RoomSettingsPanel.tsx b/src/components/room/RoomSettingsPanel.tsx index 5112075..358cf06 100644 --- a/src/components/room/RoomSettingsPanel.tsx +++ b/src/components/room/RoomSettingsPanel.tsx @@ -55,6 +55,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({ const [useExact, setUseExact] = useState(false); const [think, setThink] = useState(false); const [stream, setStream] = useState(true); + const [agentType, setAgentType] = useState('chat'); const [isAddingAi, setIsAddingAi] = useState(false); const handleSave = async () => { @@ -110,6 +111,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({ setUseExact(false); setThink(false); setStream(true); + setAgentType('chat'); setShowAdvanced(false); setShowAiAddDialog(true); loadModels(); @@ -131,6 +133,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({ use_exact: useExact, think, stream, + agent_type: agentType || undefined, }; await aiUpsert({ path: { room_id: room.id }, @@ -275,6 +278,14 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({ think )} + {config.agent_type === 'react' && ( + + react + + )}
+ {/* Agent type */} +
+ + +
+ {/* Advanced settings toggle */}