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

+43
View File
@@ -286,6 +286,47 @@ export const BrowserProvidersQuery = z.object({
export type BrowserProvidersQuery = z.infer<typeof BrowserProvidersQuery>;
/** 会话视图:会话 + 标签页 + 当前有效租约 + 界面可用能力 */
// ── BR-2 凭据统一:站点账号 Cookie ⇄ 浏览器 profile ──
/** 浏览器 Cookie(Playwright 形状);value 是凭据,只在服务端与浏览器服务之间传,不进日志、模型与 API 响应 */
export const BrowserCookie = z.object({
name: z.string().min(1),
value: z.string(),
/** 主机名(host-only)或 .根域 */
domain: z.string().min(1),
path: z.string().default("/"),
/** Unix 秒;-1 = 会话 Cookie */
expires: z.number().default(-1),
httpOnly: z.boolean().default(false),
secure: z.boolean().default(false),
sameSite: z.enum(["Strict", "Lax", "None"]).optional(),
});
export type BrowserCookie = z.infer<typeof BrowserCookie>;
/** 站点账号登录态(Cookie)来源:手填 / 账密自动登录 / 浏览器回写 */
export const SiteCookieSource = z.enum(["manual", "login", "browser"]);
export type SiteCookieSource = z.infer<typeof SiteCookieSource>;
/**
* 会话打开时的凭据注入结果(存 recovery_state.credential):
* injected = 已把账号 Cookie 注入浏览器 profile;none = 账号没有可注入的 Cookie;
* not_applicable = 匿名会话或 API Key 类站点(网页登录态与 API Key 无关,不注入);
* unsupported = 浏览器服务没有 Cookie 接口(镜像未带 umt-cookies 插件且上游导入口令不同);
* failed = 注入请求失败;expired = 注入后首页仍是登录页(Cookie 过期),会话转"需要人工登录"。
*/
export const BrowserCredentialState = z.object({
status: z.enum(["injected", "none", "not_applicable", "unsupported", "failed", "expired"]),
source: SiteCookieSource.nullable(),
/** 该来源最近一次写入凭据的时间 */
sourceAt: Timestamp.nullable(),
/** 注入的 Cookie 个数(不含值) */
count: NonNegInt,
message: z.string().nullable(),
/** 最近一次从浏览器读回并比对的时间 */
syncedAt: Timestamp.nullable(),
});
export type BrowserCredentialState = z.infer<typeof BrowserCredentialState>;
export const BrowserSessionView = BrowserSession.extend({
tabs: z.array(BrowserTab),
currentTabId: z.string().nullable(),
@@ -294,6 +335,8 @@ export const BrowserSessionView = BrowserSession.extend({
needsSnapshot: z.boolean(),
/** 等待处理/接管原因(login_required / captcha / human_takeover / lease_expired …) */
waitingReason: z.string().nullable(),
/** 打开会话时的凭据注入与回写状态(BR-2);旧会话没有 */
credential: BrowserCredentialState.nullable().optional(),
});
export type BrowserSessionView = z.infer<typeof BrowserSessionView>;