Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
44d7304f8e
14 files changed
+436
-49
No files matched your search
@@ -53,6 +53,7 @@ import { env } from "@/env";
|
||||
import { readTaskContext, taskThreadMetadata } from "@/core/auth/task-purpose";
|
||||
import { TaskPurposePanel } from "@/strategy-components/components/task-components/TaskPurposePanel";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { buildTaskOpeningText, readCopTaskDetail } from "@/core/auth/task-deeplink";
|
||||
|
||||
/** 内置「模板数据转换器」agent 的稳定 id。仅此 agent 的对话页显示「页面模板 + 渲染预览」调试入口。 */
|
||||
const TEMPLATE_BUILDER_AGENT_ID = "template-json-builder";
|
||||
@@ -100,10 +101,25 @@ export default function AgentChatPage({
|
||||
|
||||
// 任务深链上下文(taskId + 开场信息):闸门(rwfx)与纯绑定(3qfx/xdfx)都要读取。
|
||||
const taskCtxOn = !!(taskGate || taskBind);
|
||||
const taskContext = useMemo(
|
||||
() => (taskCtxOn ? readTaskContext() : null),
|
||||
[taskCtxOn],
|
||||
);
|
||||
const taskContext = useMemo(() => {
|
||||
if (!taskCtxOn) return null;
|
||||
const stored = readTaskContext();
|
||||
if (stored && stored.agentId === agent_id) return stored;
|
||||
// 3qfx:若任务上下文偶发丢失,但登录深链留下的任务详情仍在,则用它前端兜底重建
|
||||
// taskId + openingText,避免 landing 既不自动开始、也没有“开始回答”按钮。
|
||||
if (taskBind && !taskGate) {
|
||||
const detail = readCopTaskDetail();
|
||||
const fallbackTaskId = detail?.id == null ? "" : String(detail.id).trim();
|
||||
if (detail && fallbackTaskId && agent_id) {
|
||||
return {
|
||||
taskId: fallbackTaskId,
|
||||
agentId: agent_id,
|
||||
openingText: buildTaskOpeningText(detail, fallbackTaskId),
|
||||
};
|
||||
}
|
||||
}
|
||||
return stored;
|
||||
}, [agent_id, taskBind, taskCtxOn, taskGate]);
|
||||
const taskId =
|
||||
taskCtxOn && taskContext && taskContext.agentId === agent_id
|
||||
? taskContext.taskId
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { Info, Search, ShieldCheck, UserCheck } from "lucide-react";
|
||||
import { Copy, Info, KeyRound, Link2, Search, ShieldCheck, UserCheck, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { UserListItem } from "@/core/auth/api";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useAllUsers, useSetUserApproval } from "@/core/auth/hooks";
|
||||
import { useAllUsers, useSetUserApproval, useSetUserLoginPassword } from "@/core/auth/hooks";
|
||||
import {
|
||||
useLoginWhitelistEnabled,
|
||||
useSetLoginWhitelistEnabled,
|
||||
@@ -28,9 +29,13 @@ export default function LoginWhitelistPage() {
|
||||
|
||||
const { data: users = [], isLoading: loadingUsers } = useAllUsers(isAdmin);
|
||||
const setApproval = useSetUserApproval();
|
||||
const setLoginPassword = useSetUserLoginPassword();
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||
// 口令编辑:当前展开编辑的用户 id + 输入草稿(明文,便于管理员直接查看/复制)。
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [pwdDraft, setPwdDraft] = useState("");
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
const visibleUsers = useMemo(
|
||||
@@ -61,6 +66,56 @@ export default function LoginWhitelistPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 用户名直登链接:`<当前源>/#/login/<邮箱本地部分>?password=<口令>`。用户名取邮箱
|
||||
// `@` 前缀(用户名直登据此解析回同一账号);base 取去掉 hash 的当前地址,保留可能的
|
||||
// 部署子路径。
|
||||
const buildLoginLink = (user: UserListItem, password: string) => {
|
||||
const localPart = (user.email.split("@")[0] || "").trim();
|
||||
const base = window.location.href.split("#")[0];
|
||||
return `${base}#/login/${encodeURIComponent(localPart)}?password=${encodeURIComponent(password)}`;
|
||||
};
|
||||
|
||||
const copyText = async (text: string, okMsg: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success(okMsg);
|
||||
} catch {
|
||||
toast.error("复制失败,请手动复制");
|
||||
}
|
||||
};
|
||||
|
||||
const openEditor = (user: UserListItem) => {
|
||||
setEditingId(user.id);
|
||||
setPwdDraft(user.login_password ?? "");
|
||||
};
|
||||
|
||||
const closeEditor = () => {
|
||||
setEditingId(null);
|
||||
setPwdDraft("");
|
||||
};
|
||||
|
||||
const handleSavePassword = async (user: UserListItem) => {
|
||||
const pwd = pwdDraft.trim();
|
||||
if (!pwd) return;
|
||||
try {
|
||||
await setLoginPassword.mutateAsync({ userId: user.id, password: pwd });
|
||||
toast.success("已设置登录口令");
|
||||
closeEditor();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "保存失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearPassword = async (user: UserListItem) => {
|
||||
try {
|
||||
await setLoginPassword.mutateAsync({ userId: user.id, password: null });
|
||||
toast.success("已清除登录口令");
|
||||
closeEditor();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "清除失败");
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAdmin) return <Navigate to="/page/workspace/chats/new" replace />;
|
||||
|
||||
return (
|
||||
@@ -126,9 +181,10 @@ export default function LoginWhitelistPage() {
|
||||
|
||||
{/* 用户列表 */}
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200/60 bg-white shadow-sm">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] items-center gap-3 border-b border-slate-200/60 bg-slate-50/70 px-4 py-2.5 text-xs font-semibold text-slate-500">
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-3 border-b border-slate-200/60 bg-slate-50/70 px-4 py-2.5 text-xs font-semibold text-slate-500">
|
||||
<span>用户</span>
|
||||
<span className="px-1 text-center">问答</span>
|
||||
<span className="px-1 text-center">登录口令</span>
|
||||
<span className="px-1 text-center">放行</span>
|
||||
</div>
|
||||
|
||||
@@ -143,40 +199,137 @@ export default function LoginWhitelistPage() {
|
||||
{visibleUsers.map((user) => {
|
||||
const isUserAdmin = user.system_role === "admin";
|
||||
const allowed = user.approved || isUserAdmin;
|
||||
const hasPwd = Boolean(user.login_password);
|
||||
const isEditing = editingId === user.id;
|
||||
return (
|
||||
<li
|
||||
key={user.id}
|
||||
className="grid grid-cols-[1fr_auto_auto] items-center gap-3 px-4 py-3 transition-colors hover:bg-slate-50/60"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-medium text-slate-800">{user.email}</span>
|
||||
{isUserAdmin ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 px-1.5 py-0.5 text-[11px] font-semibold text-amber-600">
|
||||
<UserCheck className="size-3" />
|
||||
管理员
|
||||
</span>
|
||||
) : allowed ? (
|
||||
<span className="rounded-md border border-emerald-200 bg-emerald-50 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-600">
|
||||
已放行
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-md border border-rose-200 bg-rose-50 px-1.5 py-0.5 text-[11px] font-semibold text-rose-600">
|
||||
已拦截
|
||||
</span>
|
||||
)}
|
||||
<Fragment key={user.id}>
|
||||
<li className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-3 px-4 py-3 transition-colors hover:bg-slate-50/60">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-medium text-slate-800">{user.email}</span>
|
||||
{isUserAdmin ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 px-1.5 py-0.5 text-[11px] font-semibold text-amber-600">
|
||||
<UserCheck className="size-3" />
|
||||
管理员
|
||||
</span>
|
||||
) : allowed ? (
|
||||
<span className="rounded-md border border-emerald-200 bg-emerald-50 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-600">
|
||||
已放行
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-md border border-rose-200 bg-rose-50 px-1.5 py-0.5 text-[11px] font-semibold text-rose-600">
|
||||
已拦截
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate font-mono text-xs text-slate-400">{user.id}</div>
|
||||
</div>
|
||||
<div className="truncate font-mono text-xs text-slate-400">{user.id}</div>
|
||||
</div>
|
||||
<div className="px-1 text-center text-sm tabular-nums text-slate-500">{user.qa_count}</div>
|
||||
<div className="px-1">
|
||||
<Switch
|
||||
checked={allowed}
|
||||
disabled={isUserAdmin || (setApproval.isPending && pendingId === user.id)}
|
||||
onCheckedChange={(v) => handleToggleApproval(user.id, v)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<div className="px-1 text-center text-sm tabular-nums text-slate-500">{user.qa_count}</div>
|
||||
{/* 登录口令:设置 / 修改 + 复制登录链接 */}
|
||||
<div className="px-1">
|
||||
<div className="flex items-center justify-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? closeEditor() : openEditor(user))}
|
||||
title={hasPwd ? "已设登录口令,点击修改" : "设置登录口令"}
|
||||
className={`inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] font-semibold transition-colors ${
|
||||
hasPwd
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100"
|
||||
: "border-slate-200 bg-slate-50 text-slate-500 hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<KeyRound className="size-3" />
|
||||
{hasPwd ? "已设" : "设置"}
|
||||
</button>
|
||||
{hasPwd && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyText(buildLoginLink(user, user.login_password ?? ""), "已复制登录链接")}
|
||||
title="复制登录链接"
|
||||
className="inline-flex items-center rounded-md border border-slate-200 bg-white px-1.5 py-1 text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-700"
|
||||
>
|
||||
<Link2 className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<Switch
|
||||
checked={allowed}
|
||||
disabled={isUserAdmin || (setApproval.isPending && pendingId === user.id)}
|
||||
onCheckedChange={(v) => handleToggleApproval(user.id, v)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{isEditing && (
|
||||
<li className="bg-slate-50/70 px-4 py-4">
|
||||
<div className="rounded-xl border border-slate-200/70 bg-white p-4 shadow-sm">
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-600">
|
||||
<KeyRound className="size-3.5 text-slate-400" />
|
||||
为 <span className="font-mono text-slate-800">{user.email}</span> 设置登录口令
|
||||
</div>
|
||||
<p className="mb-3 text-[11px] leading-relaxed text-slate-400">
|
||||
设置后该账号需用此口令登录:
|
||||
<code className="rounded bg-slate-100 px-1 text-slate-500">/login/用户名?password=口令</code>
|
||||
。口令以明文随链接传输,仅用于临时开通,请妥善保管。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={pwdDraft}
|
||||
onChange={(e) => setPwdDraft(e.target.value)}
|
||||
placeholder="输入登录口令"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void handleSavePassword(user);
|
||||
}}
|
||||
className="w-56 rounded-lg border border-slate-200 bg-slate-50/50 px-3 py-1.5 text-sm transition-all hover:border-slate-300 focus:border-red-500 focus:outline-none focus:ring-4 focus:ring-red-500/5"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!pwdDraft.trim() || setLoginPassword.isPending}
|
||||
onClick={() => void handleSavePassword(user)}
|
||||
className="inline-flex items-center gap-1 rounded-lg bg-red-600 px-3 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
{hasPwd && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={setLoginPassword.isPending}
|
||||
onClick={() => void handleClearPassword(user)}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-rose-200 bg-rose-50 px-3 py-1.5 text-sm font-semibold text-rose-600 transition-colors hover:bg-rose-100 disabled:opacity-50"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeEditor}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-500 transition-colors hover:bg-slate-50"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
{pwdDraft.trim() && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-slate-200/70 bg-slate-50/70 p-2">
|
||||
<Link2 className="size-3.5 shrink-0 text-slate-400" />
|
||||
<span className="truncate font-mono text-[11px] text-slate-500">{buildLoginLink(user, pwdDraft.trim())}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyText(buildLoginLink(user, pwdDraft.trim()), "已复制登录链接")}
|
||||
className="ml-auto inline-flex shrink-0 items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-[11px] font-medium text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-700"
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
复制链接
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
Reference in new issue
Block a user