feat(browser): unify site credentials with browser profiles (BR-2)

- Inject the site account Cookie (manual + auto-login session, session wins)
  into the camofox profile before the first tab opens; password accounts
  without a session run refresh-login first; API Key sites are skipped.
- Cookie expired after injection -> credential.status=expired; cookie
  accounts go to waiting/login_required ("log in once, applies site-wide").
- Write back site-domain cookies from the browser on handover, session close,
  every 10 min of activity, and from refresh-login when the stored cookie is
  invalid (verified before saving). user_info.cookieOrigin records
  source=browser; source shown as manual/login/browser in the origin view.
- New camofox plugin umt-cookies (GET/POST /umt/sessions/:userId/cookies
  under the access key); profile dir 0700/files 0600 in the desktop image.
- Single entry: sources-identity/service/credentials.ts.
- Real check: HHClub cookie injected into a fresh profile -> home page
  logged in (logout link present), 5 cookies read back.

Requires rebuilding umt-browser-desktop (docker compose build camofox &&
docker compose up -d camofox) to take effect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 15:25:07 +08:00
1 parent 901b01d1a1
commit 28974d1e1a
58 files changed
+2843 -1112

No files matched your search

+17 -8
View File
@@ -1,35 +1,44 @@
import type { AgentRunStatus } from "@umt/contracts";
import { ArrowLeft } from "lucide-react";
import { useState } from "react";
import { Link, useParams } from "react-router";
import { agentMockEnabled, useAgentRun } from "~/api/agent.ts";
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
import { splitContextGoal } from "~/components/agent/agent-store.ts";
import { PageHeader } from "~/components/console/page-header.tsx";
import { Button } from "~/components/ui/button.tsx";
// 会话页:左栏会话列表(xl 以上)· 中间会话列(统一最大宽度,输入区固定在底部)· 右侧栏状态/步骤/预算(lg 以上;
// 更窄时换成会话列顶部的可折叠概况条)。
export default function AgentRunPage() {
const { runId = "" } = useParams();
const run = useAgentRun(runId);
const mock = agentMockEnabled();
const [status, setStatus] = useState<"" | AgentRunStatus>("");
const suffix = mock ? "?mock=1" : "";
return (
<>
<PageHeader
title="Agent 会话"
description={run.data ? <span className="line-clamp-2 break-words">{run.data.run.goal}</span> : "加载中…"}
description={
run.data ? (
<span className="line-clamp-2 break-words" title={run.data.run.goal}>
{splitContextGoal(run.data.run.goal).text}
</span>
) : (
"加载中…"
)
}
actions={
<Button asChild variant="ghost" size="sm">
<Link to={`/console/agent${mock ? "?mock=1" : ""}`}>
<Link to={`/console/agent${suffix}`}>
<ArrowLeft />
全部会话
</Link>
</Button>
}
/>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,17rem)_minmax(0,1fr)]">
<aside className="pg-slab hidden rounded-xl p-3 lg:block lg:h-[calc(100dvh-13rem)] lg:overflow-hidden">
<AgentSessionList activeId={runId} status={status} onStatusChange={setStatus} className="h-full" />
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,16rem)_minmax(0,1fr)]">
<aside className="pg-slab hidden rounded-xl p-3 xl:block xl:h-[calc(100dvh-13rem)] xl:overflow-hidden">
<AgentSessionList activeId={runId} hrefFor={(id) => `/console/agent/${id}${suffix}`} className="h-full" />
</aside>
<div className="pg-slab flex h-[calc(100dvh-11rem)] min-h-[32rem] flex-col rounded-xl p-3 md:p-4 lg:h-[calc(100dvh-13rem)]">
<AgentRunPanel key={runId} runId={runId} className="flex-1" />
+8 -16
View File
@@ -1,6 +1,4 @@
import type { AgentRunStatus } from "@umt/contracts";
import { FlaskConical, ShieldCheck } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts";
@@ -11,7 +9,6 @@ import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
export default function AgentPage() {
const [status, setStatus] = useState<"" | AgentRunStatus>("");
const create = useCreateAgentRun();
const navigate = useNavigate();
const mock = agentMockEnabled();
@@ -31,11 +28,11 @@ export default function AgentPage() {
</Button>
</div>
) : null}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]">
<div className="pg-slab order-2 rounded-xl p-3 lg:order-1 lg:max-h-[calc(100dvh-12rem)] lg:overflow-hidden">
<AgentSessionList status={status} onStatusChange={setStatus} className="h-full" />
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:items-start">
<div className="pg-slab order-2 rounded-xl p-3 lg:order-1 lg:h-[calc(100dvh-12rem)] lg:overflow-hidden">
<AgentSessionList hrefFor={(id) => `/console/agent/${id}${mock ? "?mock=1" : ""}`} className="h-full" />
</div>
<div className="order-1 space-y-4 lg:order-2">
<div className="order-1 mx-auto w-full max-w-[88ch] space-y-3 lg:order-2">
<div className="pg-slab rounded-xl p-4">
<h2 className="mb-3 text-body font-medium text-foreground">新会话</h2>
<GoalComposer
@@ -51,15 +48,10 @@ export default function AgentPage() {
}
/>
</div>
<div className="flex gap-3 rounded-xl border border-border/70 p-4 text-caption text-muted-foreground">
<ShieldCheck aria-hidden className="mt-0.5 size-4 shrink-0" />
<div className="space-y-1">
<p>
每次工具调用都经服务端授权校验。控制台发起的会话里,没有持续授权的下载、整理、策略发布会在对应步骤出审批卡。
</p>
<p>汇报分两栏:模型自述仅供参考,"系统核实的结果"取自下载计划、下载任务和整理记录。</p>
</div>
</div>
<p className="flex items-center gap-2 px-1 text-meta text-muted-foreground">
<ShieldCheck aria-hidden className="size-3.5 shrink-0" />
每次工具调用都经服务端授权校验,汇报以系统核实的结果为准。
</p>
</div>
</div>
</>
+3 -13
View File
@@ -17,7 +17,7 @@ import { Link, useNavigate, useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
import { useInventory } from "~/api/inventory.ts";
import { type ReleaseFilters, useSiteReleases } from "~/api/releases.ts";
import { accessSummary, snapshotStale, useSiteRequestStats, useSites, useSnapshotSite } from "~/api/sites.ts";
import { snapshotStale, useSites, useSnapshotSite } from "~/api/sites.ts";
import { openAgentDrawer } from "~/components/agent/agent-store.ts";
import { SiteOriginPanel } from "~/components/browser/desktop-view.tsx";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
@@ -157,7 +157,6 @@ export default function SiteWorkbenchPage() {
const staleBefore = refreshFailed ? now + 1 : s ? now - (s.snapshotIntervalSec ?? 3600) * 2000 : null;
const selected = all.find((r) => r.id === openId) ?? null;
const freeCount = all.filter(isFree).length;
const reqStats = useSiteRequestStats(tab === "resources" ? siteId : undefined);
const reset = () => {
setQInput("");
@@ -293,14 +292,10 @@ export default function SiteWorkbenchPage() {
variant="outline"
size="sm"
disabled={!s?.enabled || snap.isPending}
title={
reqStats.data
? `抓取站点列表前 ${reqStats.data.access.snapshotMaxPages} 页(受站点限流)`
: "抓取站点列表(受站点限流)"
}
title="手动快照(受站点限流)"
onClick={() =>
snap.mutate(
{ id: siteId },
{ id: siteId, maxPages: 2 },
{
onSuccess: () => toast.success("快照已排队"),
onError: (e) => toast.error(errorMessage(e)),
@@ -394,11 +389,6 @@ export default function SiteWorkbenchPage() {
已加载 {all.length} 条{localActive ? `,本地筛选后 ${rows.length} 条` : ""}
</span>
{freeCount ? <span>其中免费 {freeCount}</span> : null}
{reqStats.data ? (
<span className="ml-auto" title={accessSummary(reqStats.data)}>
近 24 小时请求站点 {reqStats.data.last24h} 次
</span>
) : null}
{releases.isFetching && !releases.isFetchingNextPage ? <span>刷新中…</span> : null}
</div>
<ul className="divide-y divide-border/60">