修改
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { loginByEmail } from "@/lib/claw-auth";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const payload = (await req.json()) as {
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
const account = await loginByEmail(
|
||||
payload.email ?? "",
|
||||
payload.password ?? "",
|
||||
);
|
||||
return Response.json({ account });
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{ error: err instanceof Error ? err.message : "登录失败" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { registerByEmail } from "@/lib/claw-auth";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const payload = (await req.json()) as {
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
const account = await registerByEmail(
|
||||
payload.email ?? "",
|
||||
payload.password ?? "",
|
||||
);
|
||||
return Response.json({ account });
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{ error: err instanceof Error ? err.message : "注册失败" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { getCurrentAccount } from "@/lib/claw-auth";
|
||||
|
||||
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
|
||||
|
||||
type BindBody = {
|
||||
session_id?: string;
|
||||
};
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ workspaceId: string }> },
|
||||
) {
|
||||
const account = await getCurrentAccount();
|
||||
if (!account)
|
||||
return Response.json({ error: "请先登录账号" }, { status: 401 });
|
||||
|
||||
const { workspaceId } = await params;
|
||||
const payload = (await req.json()) as BindBody;
|
||||
try {
|
||||
const target = `${CLAW_API_URL}/api/jupyter/workspaces/${encodeURIComponent(workspaceId)}/bind`;
|
||||
const response = await fetch(target, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
session_id: payload.session_id,
|
||||
account_id: account.id,
|
||||
}),
|
||||
cache: "no-store",
|
||||
});
|
||||
const body = await response.text();
|
||||
return new Response(body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"content-type":
|
||||
response.headers.get("content-type") ?? "application/json",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{
|
||||
error:
|
||||
err instanceof Error
|
||||
? `Unable to reach Claw backend: ${err.message}`
|
||||
: "Unable to reach Claw backend",
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getCurrentAccount } from "@/lib/claw-auth";
|
||||
|
||||
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ workspaceId: string }> },
|
||||
) {
|
||||
const account = await getCurrentAccount();
|
||||
if (!account)
|
||||
return Response.json({ error: "请先登录账号" }, { status: 401 });
|
||||
|
||||
const { workspaceId } = await params;
|
||||
try {
|
||||
const target = new URL(
|
||||
`${CLAW_API_URL}/api/jupyter/workspaces/${encodeURIComponent(workspaceId)}`,
|
||||
);
|
||||
target.searchParams.set("account_id", account.id);
|
||||
const response = await fetch(target, {
|
||||
method: "DELETE",
|
||||
cache: "no-store",
|
||||
});
|
||||
const body = await response.text();
|
||||
return new Response(body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"content-type":
|
||||
response.headers.get("content-type") ?? "application/json",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{
|
||||
error:
|
||||
err instanceof Error
|
||||
? `Unable to reach Claw backend: ${err.message}`
|
||||
: "Unable to reach Claw backend",
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getCurrentAccount } from "@/lib/claw-auth";
|
||||
|
||||
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
|
||||
|
||||
export async function GET() {
|
||||
const account = await getCurrentAccount();
|
||||
if (!account)
|
||||
return Response.json({ error: "请先登录账号" }, { status: 401 });
|
||||
|
||||
try {
|
||||
const target = new URL(`${CLAW_API_URL}/api/jupyter/workspaces`);
|
||||
target.searchParams.set("account_id", account.id);
|
||||
const response = await fetch(target, { cache: "no-store" });
|
||||
const body = await response.text();
|
||||
return new Response(body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"content-type":
|
||||
response.headers.get("content-type") ?? "application/json",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{
|
||||
error:
|
||||
err instanceof Error
|
||||
? `Unable to reach Claw backend: ${err.message}`
|
||||
: "Unable to reach Claw backend",
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,8 @@ const APPLY_INTENT_PATTERNS: RegExp[] = [
|
||||
/(?:进行|开始|启动|开启|执行|发起|做)\s*(?:模型)?\s*训练/i,
|
||||
// 训练 + 目标集合
|
||||
/训练[\s\S]{0,60}?(?:目标集合|需求集合|specific[\s_-]?test|集合)/i,
|
||||
// 「(模型)训练,目标是 X」「训练,基模 X」「训练 ... csv」之类自然表达
|
||||
/(?:^|[\s,,。.!?])(?:模型)?训练[\s,,][\s\S]{0,80}?(?:目标|基模|specific[\s_-]?test|\.csv)/i,
|
||||
// 跟着 program.md 的旧表达保留
|
||||
/(?:应用|加载|载入|启用|装载|使用|跑|按照?|根据|用)\s*\S*program\.?md/i,
|
||||
/program\.?md[\s\S]{0,120}?(?:训练|应用|启用|跑起来|跑一下)/i,
|
||||
@@ -204,6 +206,42 @@ function AssistantWorkspace() {
|
||||
const showPipeline = skill.enabled && applied;
|
||||
const messages = useAuiState((s) => s.thread.messages);
|
||||
const lastProcessedUserMsgId = useRef<string | null>(null);
|
||||
const pipelineContainerRef = useRef<HTMLDivElement>(null);
|
||||
const lastPipelineYRef = useRef<number | null>(null);
|
||||
const [exitButtonVisible, setExitButtonVisible] = useState(false);
|
||||
|
||||
const handlePipelineMouseMove = useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const node = pipelineContainerRef.current;
|
||||
if (!node) return;
|
||||
const rect = node.getBoundingClientRect();
|
||||
const relativeY = event.clientY - rect.top;
|
||||
const inTopThird = relativeY >= 0 && relativeY <= rect.height / 3;
|
||||
const lastY = lastPipelineYRef.current;
|
||||
const movingUp = lastY !== null && event.clientY < lastY;
|
||||
const movingDown = lastY !== null && event.clientY > lastY;
|
||||
lastPipelineYRef.current = event.clientY;
|
||||
setExitButtonVisible((prev) => {
|
||||
if (!inTopThird) return false;
|
||||
if (movingUp) return true;
|
||||
if (movingDown) return false;
|
||||
return prev;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handlePipelineMouseLeave = useCallback(() => {
|
||||
lastPipelineYRef.current = null;
|
||||
setExitButtonVisible(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPipeline) {
|
||||
lastPipelineYRef.current = null;
|
||||
setExitButtonVisible(false);
|
||||
}
|
||||
}, [showPipeline]);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => setActiveSessionId(readActiveSessionId());
|
||||
@@ -235,31 +273,52 @@ function AssistantWorkspace() {
|
||||
// 自动应用:skill 启用 + 当前 session 后端的 pipeline 已经有进展(任何
|
||||
// 卡 running 或 complete)→ applied=true。这样关 tab 重开 / 刷新 / 切回
|
||||
// 已在跑的会话都能自动恢复 monitor 面板,不依赖触发词。
|
||||
// 30 秒轮询一次,覆盖「页面打开时 pipeline 还没起、之后才被外部脚本启动」
|
||||
// 这类情形——一次性 fetch 容易错过。
|
||||
useEffect(() => {
|
||||
if (!skill.enabled) return;
|
||||
if (applied) return;
|
||||
if (!activeSessionId) return;
|
||||
let cancelled = false;
|
||||
const url = `/api/claw/training/pipeline?session_id=${encodeURIComponent(activeSessionId)}`;
|
||||
fetch(url, { cache: "no-store" })
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((payload) => {
|
||||
const tick = async () => {
|
||||
try {
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
const payload = await res.json();
|
||||
if (cancelled || !payload) return;
|
||||
const phases = Array.isArray(payload.phases) ? payload.phases : [];
|
||||
const hasProgress = phases.some(
|
||||
(ph: { cards?: Array<{ status?: string }> }) =>
|
||||
Array.isArray(ph.cards) &&
|
||||
ph.cards.some(
|
||||
(c) => c.status === "running" || c.status === "complete",
|
||||
),
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
const hasItemProgress = items.some(
|
||||
(it: {
|
||||
type?: string;
|
||||
status?: string;
|
||||
cards?: Array<{ status?: string }>;
|
||||
}) => {
|
||||
if (it.type === "gate") {
|
||||
return it.status === "running" || it.status === "complete";
|
||||
}
|
||||
return (
|
||||
Array.isArray(it.cards) &&
|
||||
it.cards.some(
|
||||
(c) =>
|
||||
c.status === "running" || c.status === "complete",
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
if (hasProgress) setApplied(true);
|
||||
})
|
||||
.catch(() => {
|
||||
// items[] 走 phase-complete-only 过滤;R0 只有一张卡在跑时
|
||||
// items 是空的,进展信号在 in_flight 字段里——也算进展。
|
||||
const hasInFlight = Boolean(payload.in_flight);
|
||||
if (hasItemProgress || hasInFlight) setApplied(true);
|
||||
} catch {
|
||||
/* ignore */
|
||||
});
|
||||
}
|
||||
};
|
||||
tick();
|
||||
const id = window.setInterval(tick, 30000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(id);
|
||||
};
|
||||
}, [skill.enabled, applied, activeSessionId, setApplied]);
|
||||
|
||||
@@ -293,15 +352,24 @@ function AssistantWorkspace() {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{showPipeline ? (
|
||||
<div className="relative flex min-w-0 flex-1 flex-col">
|
||||
<div
|
||||
ref={pipelineContainerRef}
|
||||
onMouseMove={handlePipelineMouseMove}
|
||||
onMouseLeave={handlePipelineMouseLeave}
|
||||
className="relative flex min-w-0 flex-1 flex-col"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApplied(false)}
|
||||
title="退出训练模式"
|
||||
aria-label="退出训练模式"
|
||||
className="absolute top-3 left-3 z-20 inline-flex size-7 items-center justify-center rounded-full border bg-background/90 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:bg-muted"
|
||||
className={cn(
|
||||
"-translate-x-1/2 absolute top-0 left-1/2 z-20 inline-flex items-center gap-1 rounded-b-md border-x border-b border-border bg-background px-3 py-1 text-muted-foreground text-xs shadow-md ring-offset-background transition-transform duration-200 hover:bg-muted hover:text-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
exitButtonVisible ? "translate-y-0" : "-translate-y-full",
|
||||
)}
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
<span>退出训练模式</span>
|
||||
</button>
|
||||
<TrainingPipelinePanel sessionId={activeSessionId} />
|
||||
</div>
|
||||
@@ -309,9 +377,7 @@ function AssistantWorkspace() {
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col overflow-hidden",
|
||||
showPipeline
|
||||
? "w-[28rem] shrink-0 border-l"
|
||||
: "min-w-0 flex-1",
|
||||
showPipeline ? "w-[28rem] shrink-0 border-l" : "min-w-0 flex-1",
|
||||
)}
|
||||
>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
|
||||
@@ -36,36 +36,59 @@ export function useClawAccount() {
|
||||
return { account, isLoading, refresh, setAccount };
|
||||
}
|
||||
|
||||
const XIAOMI_EMAIL_RE = /^[\w.-]+@xiaomi\.com$/i;
|
||||
|
||||
export function ClawAccountGate({
|
||||
onAccount,
|
||||
}: {
|
||||
onAccount: (account: ClawAccount) => void;
|
||||
}) {
|
||||
const [mode, setMode] = useState<"login" | "register">("login");
|
||||
const [username, setUsername] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
async function submit() {
|
||||
setError("");
|
||||
const trimmed = email.trim();
|
||||
if (!XIAOMI_EMAIL_RE.test(trimmed)) {
|
||||
setError("请使用小米邮箱(@xiaomi.com)登录");
|
||||
return;
|
||||
}
|
||||
if (password.length < 6) {
|
||||
setError("密码至少 6 位");
|
||||
return;
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const response = await fetch(`/api/claw/auth/${mode}`, {
|
||||
const endpoint =
|
||||
mode === "register"
|
||||
? "/api/claw/auth/email-register"
|
||||
: "/api/claw/auth/email-login";
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
body: JSON.stringify({ email: trimmed, password }),
|
||||
});
|
||||
const payload = (await response.json()) as {
|
||||
account?: ClawAccount;
|
||||
error?: string;
|
||||
};
|
||||
if (!response.ok || !payload.account) {
|
||||
throw new Error(payload.error ?? "账号操作失败");
|
||||
throw new Error(
|
||||
payload.error ?? (mode === "register" ? "注册失败" : "登录失败"),
|
||||
);
|
||||
}
|
||||
onAccount(payload.account);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "账号操作失败");
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: mode === "register"
|
||||
? "注册失败"
|
||||
: "登录失败",
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -77,22 +100,25 @@ export function ClawAccountGate({
|
||||
<div className="mb-6">
|
||||
<h1 className="font-semibold text-xl">ZK Data Agent</h1>
|
||||
<p className="mt-1 text-muted-foreground text-sm">
|
||||
中控数据开发平台会按账号隔离会话、上传文件和生成产物。
|
||||
{mode === "register"
|
||||
? "创建账号:使用小米邮箱注册,前缀作为 autoresearch 工作根目录。"
|
||||
: "使用小米邮箱登录。会话/上传/产物按邮箱前缀和 chat 二级隔离。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="账号"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
type="email"
|
||||
placeholder="your_name@xiaomi.com"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") submit();
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
placeholder={mode === "register" ? "设置密码(≥6 位)" : "密码"}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
@@ -101,7 +127,7 @@ export function ClawAccountGate({
|
||||
/>
|
||||
{error ? <p className="text-destructive text-sm">{error}</p> : null}
|
||||
<Button onClick={submit} disabled={isSubmitting}>
|
||||
{isSubmitting ? "处理中..." : mode === "login" ? "登录" : "注册"}
|
||||
{isSubmitting ? "处理中..." : mode === "register" ? "注册" : "登录"}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -100,15 +100,15 @@
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 0 1px rgb(52 211 153 / 0.3),
|
||||
0 0 0 0 rgb(52 211 153 / 0);
|
||||
border-color: rgb(52 211 153 / 0.45);
|
||||
0 0 0 1px rgb(56 189 248 / 0.3),
|
||||
0 0 0 0 rgb(56 189 248 / 0);
|
||||
border-color: rgb(56 189 248 / 0.45);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 0 1px rgb(52 211 153 / 0.7),
|
||||
0 0 10px 2px rgb(52 211 153 / 0.35);
|
||||
border-color: rgb(52 211 153 / 0.85);
|
||||
0 0 0 1px rgb(56 189 248 / 0.7),
|
||||
0 0 10px 2px rgb(56 189 248 / 0.35);
|
||||
border-color: rgb(56 189 248 / 0.85);
|
||||
}
|
||||
}
|
||||
--animate-pipeline-dot-ping: pipeline-dot-ping 1400ms ease-out infinite;
|
||||
|
||||
Reference in New Issue
Block a user