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 */}