mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat: add filter log by user
This commit is contained in:
@@ -22,8 +22,14 @@ function colorLine(line) {
|
||||
export default function ConsoleLogClient() {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [selectedUserId, setSelectedUserId] = useState("");
|
||||
const logRef = useRef(null);
|
||||
|
||||
const filteredLogs = selectedUserId
|
||||
? logs.filter((line) => line.includes(`[USER:${selectedUserId}]`))
|
||||
: logs;
|
||||
|
||||
const handleClear = async () => {
|
||||
try {
|
||||
await fetch("/api/translator/console-logs", { method: "DELETE" });
|
||||
@@ -62,16 +68,49 @@ export default function ConsoleLogClient() {
|
||||
return () => es.close();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch("/api/users", { signal: controller.signal });
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
setUsers((data.users || []).filter((user) => user.isActive));
|
||||
} catch (error) {
|
||||
if (error.name !== "AbortError") console.error("Failed to load users:", error);
|
||||
}
|
||||
}
|
||||
|
||||
loadUsers();
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to bottom on new logs
|
||||
useEffect(() => {
|
||||
if (!logRef.current) return;
|
||||
logRef.current.scrollTop = logRef.current.scrollHeight;
|
||||
}, [logs]);
|
||||
}, [filteredLogs]);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Card>
|
||||
<div className="flex items-center justify-end px-4 pt-3 pb-2">
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 px-4 pt-3 pb-2">
|
||||
<label className="flex items-center gap-2 text-xs text-text-muted">
|
||||
<span>User</span>
|
||||
<select
|
||||
value={selectedUserId}
|
||||
onChange={(event) => setSelectedUserId(event.target.value)}
|
||||
className="rounded-md border border-border bg-bg-subtle px-2 py-1.5 text-xs text-text focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="">All users</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.username}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<Button size="sm" variant="outline" icon="delete" onClick={handleClear}>
|
||||
Clear
|
||||
</Button>
|
||||
@@ -82,9 +121,11 @@ export default function ConsoleLogClient() {
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<span className="text-text-muted">No console logs yet.</span>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<span className="text-text-muted">No console logs for the selected user.</span>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{logs.map((line, i) => (
|
||||
{filteredLogs.map((line, i) => (
|
||||
<div key={i}>{colorLine(line)}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { CONSOLE_LOG_CONFIG } from "@/shared/constants/config.js";
|
||||
import { getRequestUser } from "@/lib/requestContext";
|
||||
|
||||
const consoleLevels = ["log", "info", "warn", "error", "debug"];
|
||||
|
||||
@@ -63,12 +64,17 @@ function formatArg(arg) {
|
||||
}
|
||||
|
||||
function appendLine(line) {
|
||||
state.logs.push(line);
|
||||
const requestUser = getRequestUser();
|
||||
const attributedLine = requestUser
|
||||
? `[USER:${requestUser.id}] [${requestUser.username}] ${line}`
|
||||
: line;
|
||||
|
||||
state.logs.push(attributedLine);
|
||||
const maxLines = CONSOLE_LOG_CONFIG.maxLines;
|
||||
if (state.logs.length > maxLines) {
|
||||
state.logs = state.logs.slice(-maxLines);
|
||||
}
|
||||
state.pendingLines.push(line);
|
||||
state.pendingLines.push(attributedLine);
|
||||
if (state.pendingLines.length >= MAX_BATCH_LINES) {
|
||||
if (state.flushTimer) {
|
||||
clearTimeout(state.flushTimer);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
if (!global._requestUserContext) {
|
||||
global._requestUserContext = new AsyncLocalStorage();
|
||||
}
|
||||
|
||||
const requestContext = global._requestUserContext;
|
||||
|
||||
function normalizeUser(user) {
|
||||
if (!user?.id || !user?.username) return null;
|
||||
return { id: String(user.id), username: String(user.username) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an async request handler with its dashboard user available to
|
||||
* console-log capture. AsyncLocalStorage keeps concurrent request contexts
|
||||
* isolated while their asynchronous work is in flight.
|
||||
*/
|
||||
export function runWithRequestUser(user, handler) {
|
||||
return requestContext.run({ user: normalizeUser(user) }, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user associated with the active request, for example after the
|
||||
* selected provider connection resolves its owner.
|
||||
*/
|
||||
export function setRequestUser(user) {
|
||||
const store = requestContext.getStore();
|
||||
if (store) {
|
||||
store.user = normalizeUser(user);
|
||||
return;
|
||||
}
|
||||
requestContext.enterWith({ user: normalizeUser(user) });
|
||||
}
|
||||
|
||||
export function getRequestUser() {
|
||||
return requestContext.getStore()?.user || null;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "../services/auth.js";
|
||||
import { cacheClaudeHeaders } from "open-sse/utils/claudeHeaderCache.js";
|
||||
import { getSettings } from "@/lib/localDb";
|
||||
import { getUserById } from "@/lib/db";
|
||||
import { runWithRequestUser, setRequestUser } from "@/lib/requestContext";
|
||||
import { getModelInfo, getCombo } from "../services/model.js";
|
||||
import { handleChatCore } from "open-sse/handlers/chatCore.js";
|
||||
import { DEFAULT_HEADROOM_URL } from "@/lib/headroom/detect";
|
||||
@@ -58,6 +60,9 @@ export async function handleChat(request, clientRawRequest = null) {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
const apiKey = extractApiKey(request);
|
||||
const ownerId = await getApiKeyOwnerId(apiKey);
|
||||
const requestUser = ownerId ? await getUserById(ownerId) : null;
|
||||
|
||||
return runWithRequestUser(requestUser, async () => {
|
||||
if (authHeader && apiKey) {
|
||||
const masked = log.maskKey(apiKey);
|
||||
log.debug("AUTH", `API Key: ${masked}`);
|
||||
@@ -135,6 +140,7 @@ export async function handleChat(request, clientRawRequest = null) {
|
||||
|
||||
// Single model request
|
||||
return handleSingleModelChat(body, modelStr, clientRawRequest, request, apiKey, ownerId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,6 +232,13 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
|
||||
return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || "All accounts unavailable");
|
||||
}
|
||||
|
||||
// Requests without an API key inherit the selected provider connection's
|
||||
// owner, matching usage-history attribution.
|
||||
if (!ownerId && credentials._connection?.ownerId) {
|
||||
const connectionOwner = await getUserById(credentials._connection.ownerId);
|
||||
setRequestUser(connectionOwner);
|
||||
}
|
||||
|
||||
// Account selection shown in the unified "▶" line (acc:...)
|
||||
const refreshedCredentials = await checkAndRefreshToken(provider, credentials);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user