Initial commit: grok-free-register-oss

Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
This commit is contained in:
chaos
2026-07-16 21:04:05 +08:00
commit d10009d639
72 changed files with 18752 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# Grok Session → CPA / sub2api
纯前端单页工具:把 grok-free-register 产出的凭证,转成 CPA、sub2api 导入包、batch 建号、或 sso-to-oauth 请求体。
对照思路GPT session → CPA / sub2api 纯前端转换器(同类工具,无外部依赖)。
## 本地使用
直接打开:
```text
tools/GrokSession2CPAAndSub2API/index.html
```
所有解析和转换都在浏览器本地完成,不上传 token。
## 支持输入
| 来源 | 说明 |
|------|------|
| `authenticated/xai-*.json` | 注册成功 OAuthbase_url 常是 api.x.ai |
| `cpa_ready/xai-*.json` | watchdog 终态(带 headers/email |
| `sub2api-data` / accounts 数组 | 已转换过的包,可再导成其它格式 |
| `auth-sessions.jsonl` | `email` + `cookies.sso` |
| `accounts.txt` | `email:password:sso` |
| `grok.txt` | 每行一个 SSO JWT |
可粘贴 JSON / 数组 / jsonl或拖入多个文件。
## 输出格式
### CPA
对齐注册机 cpa_ready
- `type: "xai"`
- `access_token` / `refresh_token` / `id_token`
- `base_url` **强制** `https://cli-chat-proxy.grok.com/v1`(忽略 authenticated 里的 `api.x.ai`
- `headers`:保留源 headers否则写 grok-cli 默认头
- `email` / `sub` / `expired` / `expires_in`
单条输出对象,多条输出数组。
### sub2api 导入
`type: "sub2api-data"` 包,可走 sub2api 数据导入:
```json
{
"type": "sub2api-data",
"version": 1,
"exported_at": "...",
"proxies": [],
"accounts": [ ... ]
}
```
账号字段:
- `platform: "grok"`, `type: "oauth"`
- `credentials.access_token` / `refresh_token` / `id_token` / `expires_at` / `email` / `sub` / `base_url`
- **有 `refresh_token`**:不写账号顶层 `expires_at` / `auto_pause_on_expired`(避免 access 6h 到期被 pause
- **无 `refresh_token`**:写 access JWT exp + `auto_pause_on_expired: true`
### batch 建号
`POST /api/v1/admin/accounts/batch` 体:
```json
{ "accounts": [ /* 同上 sub2api 账号对象 */ ] }
```
### sso-to-oauth
`POST /api/v1/admin/grok/sso-to-oauth` 体:
```json
{
"sso_tokens": ["eyJ...", "..."],
"_meta": [ /* 仅本地对照API 可忽略 */ ]
}
```
纯 SSO 输入grok.txt / accounts.txt / auth-sessions**只能**走这个格式CPA / sub2api / batch 需要 OAuth。
## 关键规则(别踩坑)
1. free 号 API 走 `cli-chat-proxy.grok.com`,不是 `api.x.ai`
2. free OAuth access ≈ 6h`expires_in=21600`)。有 refresh 时让 sub2api 自己刷;别把账号级 `expires_at` 钉成 access exp。
3. 当前线上常见refresh 被整批 revokeaccess 在 exp 前仍可用。这种号导入后到期无法续命,只能重铸。
4. 注册机 `grok.txt` / `accounts.txt` / `auth-sessions.jsonl` 只有 SSO没有 OAuth要进 sub2api 要么走 sso-to-oauth要么先走 xai_enroller 出 `authenticated/` 再转。
## 与注册机衔接
```
register → grok.txt / accounts.txt / auth-sessions.jsonl (SSO)
→ xai_enroller → authenticated/xai-*.json (OAuth)
→ acpa_watchdog → cpa_ready/xai-*.json + acc.md
→ 本工具 → sub2api-data / batch / CPA / sso-to-oauth
```

View File

@@ -0,0 +1,902 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grok Session → CPA / sub2api</title>
<meta name="theme-color" content="#111827" />
<style>
:root {
color-scheme: light dark;
--bg: #0b1220;
--surface: #121a2b;
--surface-soft: #1a2438;
--line: #2a3650;
--text: #e8eefc;
--muted: #93a0bc;
--accent: #38bdf8;
--accent-strong: #0ea5e9;
--success: #34d399;
--danger: #f87171;
--warning: #fbbf24;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
--sans: "Inter", "Avenir Next", "Helvetica Neue", "PingFang SC", "Noto Sans SC", sans-serif;
--radius: 10px;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
* { box-sizing: border-box; }
html, body { min-height: 100%; margin: 0; }
body {
font-family: var(--sans);
color: var(--text);
background:
radial-gradient(circle at top left, rgba(56, 189, 248, 0.12), transparent 28%),
linear-gradient(180deg, #0b1220, #0a101b 40%, #070b14);
}
button, textarea, input { font: inherit; }
button { border: 0; cursor: pointer; }
.app { width: min(1280px, calc(100vw - 28px)); margin: 0 auto; padding: 22px 0 36px; }
.topbar { display: flex; justify-content: space-between; gap: 16px; align-items: flex-end; margin-bottom: 16px; }
.eyebrow { margin: 0 0 8px; color: var(--accent); font-size: 0.78rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
h1 { margin: 0; font-size: clamp(1.5rem, 3vw, 2.3rem); line-height: 1.1; }
.subtitle { margin: 10px 0 0; color: var(--muted); line-height: 1.5; max-width: 70ch; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
.chip {
display: inline-flex; align-items: center; min-height: 32px; padding: 6px 10px;
border: 1px solid var(--line); border-radius: 999px; background: rgba(18, 26, 43, 0.9); color: var(--muted); font-size: 0.84rem;
}
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.segmented {
display: inline-grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px;
width: min(760px, 100%); padding: 4px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
}
.segmented button {
min-height: 40px; border-radius: 8px; background: transparent; color: var(--muted); padding: 0 10px;
}
.segmented button[aria-pressed="true"] { background: var(--accent-strong); color: #041018; font-weight: 800; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; }
.button {
display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 14px;
border-radius: var(--radius); transition: 0.15s ease;
}
.button-primary { background: var(--accent-strong); color: #041018; font-weight: 800; }
.button-primary:hover { filter: brightness(1.08); }
.button-secondary { border: 1px solid var(--line); background: var(--surface); color: var(--text); }
.button-secondary:hover { background: var(--surface-soft); }
.button:disabled { opacity: 0.5; cursor: not-allowed; }
.workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
.panel {
min-width: 0; border: 1px solid var(--line); border-radius: var(--radius);
background: rgba(18, 26, 43, 0.92); box-shadow: var(--shadow);
}
.panel-head { display: flex; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--line); }
.panel-head h2 { margin: 0; font-size: 1rem; }
.panel-head p { margin: 4px 0 0; color: var(--muted); font-size: 0.88rem; line-height: 1.45; }
.panel-body { padding: 14px 16px 16px; }
.guide {
margin: 0 0 12px; padding: 12px 14px; border: 1px solid rgba(56, 189, 248, 0.28);
border-left: 4px solid var(--accent); border-radius: var(--radius); background: rgba(14, 165, 233, 0.08);
color: var(--muted); line-height: 1.5; font-size: 0.9rem;
}
.guide strong { color: var(--text); }
.guide code { font-family: var(--mono); color: var(--accent); }
textarea {
width: 100%; min-height: 460px; resize: vertical; border: 1px solid var(--line); border-radius: var(--radius);
padding: 12px; background: #0a1220; color: var(--text); font-family: var(--mono); font-size: 0.82rem; line-height: 1.5;
}
textarea:focus { outline: 2px solid rgba(56, 189, 248, 0.35); border-color: var(--accent); }
.status-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.status {
min-height: 30px; display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px;
border: 1px solid var(--line); color: var(--muted); font-size: 0.84rem; background: rgba(0,0,0,0.15);
}
.status.ok { color: var(--success); border-color: rgba(52, 211, 153, 0.35); }
.status.err { color: var(--danger); border-color: rgba(248, 113, 113, 0.35); }
.status.warn { color: var(--warning); border-color: rgba(251, 191, 36, 0.35); }
.accounts, .issues {
margin-top: 12px; display: grid; gap: 8px; max-height: 180px; overflow: auto;
}
.card {
border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: rgba(0,0,0,0.14);
font-size: 0.86rem; line-height: 1.4;
}
.card .title { font-weight: 700; }
.card .meta { color: var(--muted); margin-top: 4px; font-family: var(--mono); font-size: 0.78rem; word-break: break-all; }
.drop-active textarea { border-color: var(--accent); box-shadow: inset 0 0 0 1px rgba(56,189,248,0.35); }
.footer-note { margin-top: 14px; color: var(--muted); font-size: 0.84rem; line-height: 1.5; }
@media (max-width: 960px) {
.workspace { grid-template-columns: 1fr; }
.segmented { grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; }
.topbar { flex-direction: column; align-items: flex-start; }
.chips { justify-content: flex-start; }
}
</style>
</head>
<body>
<div class="app">
<div class="topbar">
<div>
<p class="eyebrow">Grok Free Register toolkit</p>
<h1>Grok Session → CPA / sub2api</h1>
<p class="subtitle">
纯前端转换:把注册机 <code>xai-*.json</code> / SSO / accounts / sessions
转成 CPA、sub2api 导入包、batch 建号、或 sso-to-oauth 请求体。本地解析,不上传。
</p>
</div>
<div class="chips">
<span class="chip">platform: grok</span>
<span class="chip">base: cli-chat-proxy</span>
<span class="chip">有 refresh 不写账号 6h 死期</span>
</div>
</div>
<div class="toolbar">
<div class="segmented" role="tablist" aria-label="输出格式">
<button type="button" data-format="cpa" aria-pressed="true">CPA</button>
<button type="button" data-format="sub2api" aria-pressed="false">sub2api 导入</button>
<button type="button" data-format="batch" aria-pressed="false">batch 建号</button>
<button type="button" data-format="sso" aria-pressed="false">sso-to-oauth</button>
</div>
<div class="actions">
<button class="button button-secondary" id="btn-sample" type="button">填示例</button>
<button class="button button-secondary" id="btn-clear" type="button">清空</button>
<button class="button button-secondary" id="btn-copy" type="button" disabled>复制输出</button>
<button class="button button-primary" id="btn-download" type="button" disabled>下载 JSON</button>
</div>
</div>
<div class="workspace">
<section class="panel" id="input-panel">
<div class="panel-head">
<div>
<h2>输入</h2>
<p>粘贴 / 拖入 JSON、JSON 数组、jsonl、accounts 行、纯 SSO 行</p>
</div>
</div>
<div class="panel-body">
<div class="guide">
<strong>支持输入</strong><br />
1. <code>authenticated/xai-*.json</code><code>cpa_ready/xai-*.json</code><br />
2. sub2api account / accounts 数组 / sub2api-data 包<br />
3. <code>auth-sessions.jsonl</code>email + cookies.sso<br />
4. <code>accounts.txt</code><code>email:password:sso</code><br />
5. <code>grok.txt</code>:每行一个 SSO JWT
</div>
<textarea id="session-input" spellcheck="false" placeholder="在此粘贴 xai-*.json / SSO / accounts 行…"></textarea>
<div class="status-row">
<span class="status" id="input-status">等待输入</span>
<span class="status" id="convert-status">未转换</span>
</div>
<div class="accounts" id="accounts"></div>
<div class="issues" id="issues"></div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h2>输出</h2>
<p id="output-help">CPAtype=xai对齐注册机 cpa_ready / new-api 风格</p>
</div>
</div>
<div class="panel-body">
<textarea id="output" readonly spellcheck="false" placeholder="转换结果会显示在这里"></textarea>
<div class="status-row">
<span class="status" id="output-status"></span>
</div>
<p class="footer-note">
规则:<br />
• free 号 <code>base_url</code> 固定为 <code>https://cli-chat-proxy.grok.com/v1</code>(忽略 authenticated 里的 api.x.ai<br />
• 有 <code>refresh_token</code>sub2api 账号<strong>不写</strong>顶层 <code>expires_at/auto_pause</code>,避免 6h 被 pause<br />
• 无 refresh 时:写 access 的 exp + auto_pause到期停用<br />
• 纯 SSO 输入只能出 <code>sso-to-oauth</code> 请求体CPA/sub2api 需要 OAuth token
</p>
</div>
</section>
</div>
</div>
<script>
(() => {
const CLI_BASE = "https://cli-chat-proxy.grok.com/v1";
const TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
const HELP = {
cpa: "CPAtype=xai对齐注册机 cpa_ready / cli-proxy 风格",
sub2api: "sub2api 数据导入type=sub2api-data / accounts[] platform=grok",
batch: "batch 建号POST /api/v1/admin/accounts/batch 的 {accounts:[]}",
sso: "sso-to-oauthPOST /api/v1/admin/grok/sso-to-oauth 的 {sso_tokens:[]}",
};
const els = {
input: document.querySelector("#session-input"),
output: document.querySelector("#output"),
inputStatus: document.querySelector("#input-status"),
convertStatus: document.querySelector("#convert-status"),
outputStatus: document.querySelector("#output-status"),
accounts: document.querySelector("#accounts"),
issues: document.querySelector("#issues"),
outputHelp: document.querySelector("#output-help"),
inputPanel: document.querySelector("#input-panel"),
btnCopy: document.querySelector("#btn-copy"),
btnDownload: document.querySelector("#btn-download"),
btnClear: document.querySelector("#btn-clear"),
btnSample: document.querySelector("#btn-sample"),
formatButtons: [...document.querySelectorAll("[data-format]")],
};
const state = {
format: "cpa",
converted: [],
ssoTokens: [],
skipped: [],
timer: null,
outputText: "",
};
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function firstNonEmpty(...values) {
for (const value of values) {
if (value === undefined || value === null) continue;
if (typeof value === "string" && value.trim() === "") continue;
return typeof value === "string" ? value.trim() : value;
}
return undefined;
}
function setStatus(el, text, tone = "") {
el.textContent = text;
el.className = "status" + (tone ? ` ${tone}` : "");
}
function decodeBase64Url(value) {
const normalized = String(value).replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
try {
return decodeURIComponent(
Array.from(atob(padded), (c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`).join("")
);
} catch {
return null;
}
}
function parseJwtPayload(token) {
if (!token || typeof token !== "string" || token.split(".").length < 2) return null;
try {
const raw = decodeBase64Url(token.split(".")[1]);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function looksLikeJwt(value) {
return typeof value === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value.trim());
}
function normalizeTimestamp(value) {
if (value === undefined || value === null || value === "") return undefined;
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString().replace(/\.\d{3}Z$/, "Z");
if (typeof value === "number" && Number.isFinite(value)) {
const ms = value > 1e12 ? value : value * 1000;
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
}
if (typeof value === "string") {
const n = Number(value);
if (Number.isFinite(n) && /^\d+(\.\d+)?$/.test(value.trim())) {
const ms = n > 1e12 ? n : n * 1000;
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
}
const d = new Date(value);
if (!Number.isNaN(d.getTime())) return d.toISOString().replace(/\.\d{3}Z$/, "Z");
}
return undefined;
}
function unixSecondsFromValue(value) {
if (value === undefined || value === null || value === "") return undefined;
if (typeof value === "number" && Number.isFinite(value)) return Math.floor(value > 1e12 ? value / 1000 : value);
if (typeof value === "string") {
const n = Number(value);
if (Number.isFinite(n) && /^\d+(\.\d+)?$/.test(value.trim())) return Math.floor(n > 1e12 ? n / 1000 : n);
const d = new Date(value);
if (!Number.isNaN(d.getTime())) return Math.floor(d.getTime() / 1000);
}
return undefined;
}
function stripUndefined(obj) {
if (Array.isArray(obj)) return obj.map(stripUndefined).filter((v) => v !== undefined);
if (!isPlainObject(obj)) return obj;
const out = {};
for (const [k, v] of Object.entries(obj)) {
if (v === undefined || v === null || v === "") continue;
const next = stripUndefined(v);
if (next === undefined) continue;
if (isPlainObject(next) && !Object.keys(next).length) continue;
out[k] = next;
}
return out;
}
function extractSsoFromCookies(cookies) {
if (!Array.isArray(cookies)) return undefined;
let sso = "";
let fallback = "";
for (const cookie of cookies) {
if (!isPlainObject(cookie)) continue;
const name = String(cookie.name || "");
const value = String(cookie.value || "").trim();
if (!value) continue;
if (name === "sso" && !sso) sso = value;
if (name === "sso-rw" && !fallback) fallback = value;
}
return sso || fallback || undefined;
}
function collectOAuthLikeObjects(value, sourceName = "pasted-json") {
const found = [];
const visited = new WeakSet();
function visit(item, path) {
if (!isPlainObject(item) && !Array.isArray(item)) return;
if (Array.isArray(item)) {
item.forEach((child, i) => visit(child, `${path}[${i}]`));
return;
}
if (visited.has(item)) return;
visited.add(item);
const access = firstNonEmpty(
item.access_token, item.accessToken,
item.tokens?.access_token, item.tokens?.accessToken,
item.credentials?.access_token, item.credentials?.accessToken,
);
const refresh = firstNonEmpty(
item.refresh_token, item.refreshToken,
item.tokens?.refresh_token, item.tokens?.refreshToken,
item.credentials?.refresh_token, item.credentials?.refreshToken,
);
const idToken = firstNonEmpty(
item.id_token, item.idToken,
item.tokens?.id_token, item.tokens?.idToken,
item.credentials?.id_token, item.credentials?.idToken,
);
const email = firstNonEmpty(item.email, item.user?.email, item.credentials?.email, item.name);
const sub = firstNonEmpty(item.sub, item.credentials?.sub, item.subject);
const sso = firstNonEmpty(item.sso, item.sso_token, item.ssoToken, extractSsoFromCookies(item.cookies));
const isXai = item.type === "xai" || item.auth_kind === "oauth" || item.platform === "grok";
const isSub2 = item.platform === "grok" && item.credentials;
if (access || (refresh && (email || sub || isXai || isSub2)) || (sso && (email || item.cookies))) {
found.push({ value: item, sourceName, path, kind: access || refresh ? "oauth" : "sso" });
return;
}
// sub2api-data envelope
if (Array.isArray(item.accounts)) {
visit(item.accounts, `${path}.accounts`);
return;
}
for (const [key, child] of Object.entries(item)) {
if (["access_token", "accessToken", "refresh_token", "id_token", "cookies"].includes(key)) continue;
visit(child, `${path}.${key}`);
}
}
visit(value, "$");
return found;
}
function parseTextInput(text) {
const raw = String(text || "").trim();
if (!raw) return { oauthSources: [], ssoTokens: [], skipped: [] };
// Try full JSON first
try {
const parsed = JSON.parse(raw);
const oauthSources = collectOAuthLikeObjects(parsed);
const ssoTokens = [];
// also collect pure sso fields if any
for (const src of oauthSources) {
if (src.kind === "sso") {
const sso = firstNonEmpty(src.value.sso, src.value.sso_token, extractSsoFromCookies(src.value.cookies));
if (sso) ssoTokens.push({ token: sso, email: firstNonEmpty(src.value.email, src.value.user?.email), path: src.path });
}
}
if (oauthSources.length || ssoTokens.length) {
return { oauthSources: oauthSources.filter((s) => s.kind === "oauth"), ssoTokens, skipped: [] };
}
} catch {
// line mode below
}
const oauthSources = [];
const ssoTokens = [];
const skipped = [];
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
lines.forEach((line, index) => {
// jsonl
if (line.startsWith("{") || line.startsWith("[")) {
try {
const parsed = JSON.parse(line);
const found = collectOAuthLikeObjects(parsed, `line-${index + 1}`);
for (const src of found) {
if (src.kind === "oauth") oauthSources.push(src);
else {
const sso = firstNonEmpty(src.value.sso, src.value.sso_token, extractSsoFromCookies(src.value.cookies));
if (sso) ssoTokens.push({ token: sso, email: firstNonEmpty(src.value.email), path: src.path });
}
}
if (!found.length) skipped.push({ path: `line-${index + 1}`, reason: "JSON 行未识别为 Grok 凭证" });
return;
} catch (error) {
skipped.push({ path: `line-${index + 1}`, reason: `JSON 行解析失败:${error.message}` });
return;
}
}
// accounts.txt: email:password:sso
if (line.includes(":") && !looksLikeJwt(line)) {
const parts = line.split(":");
if (parts.length >= 3) {
const email = parts[0];
const sso = parts.slice(2).join(":");
if (looksLikeJwt(sso)) {
ssoTokens.push({ token: sso.trim(), email: email.trim(), path: `accounts:${index + 1}` });
return;
}
}
}
// pure SSO jwt line
if (looksLikeJwt(line)) {
ssoTokens.push({ token: line.trim(), path: `sso:${index + 1}` });
return;
}
skipped.push({ path: `line-${index + 1}`, reason: "无法识别该行" });
});
return { oauthSources, ssoTokens, skipped };
}
function convertOAuthRecord(record, options = {}) {
if (!isPlainObject(record)) throw new Error("记录不是对象");
// unwrap sub2api account
const credsIn = isPlainObject(record.credentials) ? record.credentials : {};
const accessToken = firstNonEmpty(
record.access_token, record.accessToken,
credsIn.access_token, credsIn.accessToken,
record.tokens?.access_token,
);
if (!accessToken) throw new Error("缺少 access_token");
const refreshToken = firstNonEmpty(
record.refresh_token, record.refreshToken,
credsIn.refresh_token, credsIn.refreshToken,
record.tokens?.refresh_token,
);
const idToken = firstNonEmpty(
record.id_token, record.idToken,
credsIn.id_token, credsIn.idToken,
record.tokens?.id_token,
);
const tokenType = firstNonEmpty(record.token_type, credsIn.token_type, "Bearer");
const accessClaims = parseJwtPayload(accessToken) || {};
const idClaims = parseJwtPayload(idToken) || {};
const email = firstNonEmpty(
record.email, credsIn.email, record.user?.email, record.name,
idClaims.email, accessClaims.email, accessClaims.preferred_username,
);
const sub = firstNonEmpty(
record.sub, credsIn.sub, record.subject,
idClaims.sub, accessClaims.sub, accessClaims.principal_id,
);
const expiresIn = firstNonEmpty(
record.expires_in, credsIn.expires_in,
accessClaims.exp && accessClaims.iat ? Number(accessClaims.exp) - Number(accessClaims.iat) : undefined,
21600,
);
const expiredIso = firstNonEmpty(
normalizeTimestamp(record.expired),
normalizeTimestamp(record.expires_at),
normalizeTimestamp(credsIn.expires_at),
normalizeTimestamp(record.expiresAt),
accessClaims.exp ? normalizeTimestamp(accessClaims.exp) : undefined,
);
const expiresUnix = firstNonEmpty(
unixSecondsFromValue(expiredIso),
unixSecondsFromValue(accessClaims.exp),
);
const lastRefresh = firstNonEmpty(
normalizeTimestamp(record.last_refresh),
normalizeTimestamp(credsIn.last_refresh),
normalizeTimestamp(options.now || new Date()),
);
const baseUrl = CLI_BASE; // free CLI 强制
const hasRefresh = Boolean(refreshToken);
const name = firstNonEmpty(email, sub && `grok-${String(sub).slice(0, 8)}`, options.sourceName, "Grok Account");
const headers = isPlainObject(record.headers) ? record.headers : {
"x-grok-client-version": "0.2.93",
"x-xai-token-auth": "xai-grok-cli",
"X-XAI-Token-Auth": "xai-grok-cli",
"x-authenticateresponse": "authenticate-response",
"x-grok-client-identifier": "grok-shell",
"x-compaction-at": "400000",
"User-Agent": "grok-shell/0.2.93 (linux; x86_64)",
};
const cpa = stripUndefined({
type: "xai",
access_token: accessToken,
refresh_token: refreshToken,
id_token: idToken,
token_type: tokenType,
expires_in: Number(expiresIn) || undefined,
expired: expiredIso,
last_refresh: lastRefresh,
sub,
base_url: baseUrl,
token_endpoint: firstNonEmpty(record.token_endpoint, TOKEN_ENDPOINT),
auth_kind: "oauth",
headers,
email,
});
// sub2api credentials always keep token-level expires_at
const credentials = stripUndefined({
access_token: accessToken,
refresh_token: refreshToken,
id_token: idToken,
token_type: tokenType,
expires_at: expiredIso,
email,
sub,
base_url: baseUrl,
});
// 有 refresh不写账号级 expires_at避免 6h auto-pause
// 无 refresh写 access exp + auto_pause
const sub2apiAccount = stripUndefined({
name,
platform: "grok",
type: "oauth",
credentials,
concurrency: Number.isFinite(Number(record.concurrency)) ? Number(record.concurrency) : 1,
priority: Number.isFinite(Number(record.priority)) ? Number(record.priority) : 0,
expires_at: hasRefresh ? undefined : expiresUnix,
auto_pause_on_expired: hasRefresh ? undefined : (expiresUnix ? true : undefined),
notes: firstNonEmpty(record.notes, record.note, record.remark),
});
return {
sourceName: options.sourceName,
sourcePath: options.sourcePath,
email,
name,
sub,
hasRefresh,
expiresAt: expiredIso,
expiresUnix,
cpa,
sub2apiAccount,
};
}
function convertFromText(text) {
const { oauthSources, ssoTokens, skipped } = parseTextInput(text);
const converted = [];
const allSkipped = [...skipped];
const now = new Date();
oauthSources.forEach((item, index) => {
try {
converted.push(convertOAuthRecord(item.value, {
now,
sourceName: item.sourceName,
sourcePath: item.path || `$[${index}]`,
}));
} catch (error) {
allSkipped.push({
path: item.path || `$[${index}]`,
reason: error instanceof Error ? error.message : "无法转换",
});
}
});
// de-dupe sso
const seenSso = new Set();
const uniqueSso = [];
for (const item of ssoTokens) {
const token = String(item.token || "").trim();
if (!token || seenSso.has(token)) continue;
seenSso.add(token);
uniqueSso.push(item);
}
if (!converted.length && !uniqueSso.length) {
allSkipped.push({ path: "$", reason: "未找到可识别的 Grok OAuth / SSO 记录" });
}
return { converted, ssoTokens: uniqueSso, skipped: allSkipped };
}
function buildSub2apiDocument(converted, now = new Date()) {
return {
type: "sub2api-data",
version: 1,
exported_at: normalizeTimestamp(now),
proxies: [],
accounts: converted.map((item) => item.sub2apiAccount),
};
}
function buildBatchDocument(converted) {
return { accounts: converted.map((item) => item.sub2apiAccount) };
}
function buildSsoDocument(ssoTokens) {
return {
sso_tokens: ssoTokens.map((item) => item.token),
// 便于对照API 实际只吃 sso_tokens
_meta: ssoTokens.map((item, i) => stripUndefined({
index: i + 1,
email: item.email,
path: item.path,
token_head: String(item.token).slice(0, 24) + "...",
})),
};
}
function buildCpaDocument(converted) {
return converted.length === 1
? converted[0].cpa
: converted.map((item) => item.cpa);
}
function buildOutputDocument() {
const now = new Date();
if (state.format === "cpa") {
if (!state.converted.length) throw new Error("CPA 需要 OAuth 凭证access_token。纯 SSO 请切换到 sso-to-oauth");
return buildCpaDocument(state.converted);
}
if (state.format === "sub2api") {
if (!state.converted.length) throw new Error("sub2api 导入需要 OAuth 凭证。纯 SSO 请切换到 sso-to-oauth");
return buildSub2apiDocument(state.converted, now);
}
if (state.format === "batch") {
if (!state.converted.length) throw new Error("batch 建号需要 OAuth 凭证。纯 SSO 请切换到 sso-to-oauth");
return buildBatchDocument(state.converted);
}
if (state.format === "sso") {
if (!state.ssoTokens.length) {
// also allow extracting nothing useful message
throw new Error("未找到 SSO token。可粘贴 grok.txt / accounts.txt / auth-sessions.jsonl");
}
return buildSsoDocument(state.ssoTokens);
}
return buildSub2apiDocument(state.converted, now);
}
function renderAccounts() {
if (!state.converted.length && !state.ssoTokens.length) {
els.accounts.innerHTML = "";
return;
}
const oauthCards = state.converted.map((item) => `
<div class="card">
<div class="title">${escapeHtml(item.name || item.email || "Grok Account")}</div>
<div class="meta">
email=${escapeHtml(item.email || "-")}
· sub=${escapeHtml(item.sub || "-")}
· refresh=${item.hasRefresh ? "yes" : "no"}
· exp=${escapeHtml(item.expiresAt || "-")}
</div>
</div>
`).join("");
const ssoCards = state.ssoTokens.slice(0, 20).map((item, i) => `
<div class="card">
<div class="title">SSO #${i + 1}${item.email ? " · " + escapeHtml(item.email) : ""}</div>
<div class="meta">${escapeHtml(String(item.token).slice(0, 48))}...</div>
</div>
`).join("");
els.accounts.innerHTML = oauthCards + ssoCards;
}
function renderIssues() {
if (!state.skipped.length) {
els.issues.innerHTML = "";
return;
}
els.issues.innerHTML = state.skipped.map((item) => `
<div class="card">
<div class="title">跳过 ${escapeHtml(item.path || "?")}</div>
<div class="meta">${escapeHtml(item.reason || "")}</div>
</div>
`).join("");
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function updateOutput() {
els.outputHelp.textContent = HELP[state.format] || "";
try {
if (!els.input.value.trim()) {
state.outputText = "";
els.output.value = "";
els.btnCopy.disabled = true;
els.btnDownload.disabled = true;
setStatus(els.outputStatus, "空");
setStatus(els.convertStatus, "未转换");
return;
}
const doc = buildOutputDocument();
state.outputText = JSON.stringify(doc, null, 2);
els.output.value = state.outputText;
els.btnCopy.disabled = false;
els.btnDownload.disabled = false;
const count = state.format === "sso" ? state.ssoTokens.length : state.converted.length;
setStatus(els.outputStatus, `已生成 · ${count}`, "ok");
setStatus(els.convertStatus, `OAuth ${state.converted.length} / SSO ${state.ssoTokens.length}`, "ok");
} catch (error) {
state.outputText = "";
els.output.value = "";
els.btnCopy.disabled = true;
els.btnDownload.disabled = true;
setStatus(els.outputStatus, error instanceof Error ? error.message : "生成失败", "err");
setStatus(els.convertStatus, `OAuth ${state.converted.length} / SSO ${state.ssoTokens.length}`, state.converted.length || state.ssoTokens.length ? "warn" : "err");
}
}
function scheduleConvert() {
clearTimeout(state.timer);
state.timer = setTimeout(() => {
try {
const result = convertFromText(els.input.value);
state.converted = result.converted;
state.ssoTokens = result.ssoTokens;
state.skipped = result.skipped;
const total = state.converted.length + state.ssoTokens.length;
setStatus(
els.inputStatus,
total ? `识别 OAuth ${state.converted.length} · SSO ${state.ssoTokens.length}` : "未识别到凭证",
total ? "ok" : "warn",
);
renderAccounts();
renderIssues();
updateOutput();
} catch (error) {
state.converted = [];
state.ssoTokens = [];
state.skipped = [{ path: "$", reason: error instanceof Error ? error.message : "转换失败" }];
setStatus(els.inputStatus, error instanceof Error ? error.message : "转换失败", "err");
renderAccounts();
renderIssues();
updateOutput();
}
}, 120);
}
function downloadOutput() {
if (!state.outputText) return;
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const name = `grok-${state.format}-${stamp}.json`;
const blob = new Blob([state.outputText], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
}
async function copyOutput() {
if (!state.outputText) return;
try {
await navigator.clipboard.writeText(state.outputText);
setStatus(els.outputStatus, "已复制", "ok");
} catch {
els.output.select();
document.execCommand("copy");
setStatus(els.outputStatus, "已复制", "ok");
}
}
function setFormat(format) {
state.format = format;
els.formatButtons.forEach((btn) => {
btn.setAttribute("aria-pressed", btn.dataset.format === format ? "true" : "false");
});
updateOutput();
}
function fillSample() {
const now = Math.floor(Date.now() / 1000);
const exp = now + 21600;
// fake jwt-like payload (not signed; only for local demo decode)
const header = btoa(JSON.stringify({ alg: "none", typ: "JWT" })).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
const payload = btoa(JSON.stringify({
sub: "55c526cb-9f70-4486-a232-3710e6f3df38",
email: "demo@example.com",
iat: now,
exp,
})).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
const access = `${header}.${payload}.demo`;
const sample = {
type: "xai",
access_token: access,
refresh_token: "demo-refresh-token-replace-me",
id_token: access,
token_type: "Bearer",
expires_in: 21600,
expired: new Date(exp * 1000).toISOString().replace(/\.\d{3}Z$/, "Z"),
last_refresh: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
sub: "55c526cb-9f70-4486-a232-3710e6f3df38",
base_url: "https://api.x.ai/v1",
token_endpoint: TOKEN_ENDPOINT,
auth_kind: "oauth",
email: "demo@example.com",
};
els.input.value = JSON.stringify(sample, null, 2);
scheduleConvert();
}
// events
els.input.addEventListener("input", scheduleConvert);
els.btnClear.addEventListener("click", () => {
els.input.value = "";
scheduleConvert();
});
els.btnSample.addEventListener("click", fillSample);
els.btnCopy.addEventListener("click", copyOutput);
els.btnDownload.addEventListener("click", downloadOutput);
els.formatButtons.forEach((btn) => {
btn.addEventListener("click", () => setFormat(btn.dataset.format));
});
// drag-drop
["dragenter", "dragover"].forEach((type) => {
els.inputPanel.addEventListener(type, (event) => {
event.preventDefault();
els.inputPanel.classList.add("drop-active");
});
});
["dragleave", "drop"].forEach((type) => {
els.inputPanel.addEventListener(type, (event) => {
event.preventDefault();
if (type === "drop") {
const files = [...(event.dataTransfer?.files || [])];
if (!files.length) return;
Promise.all(files.map((file) => file.text())).then((parts) => {
const existing = els.input.value.trim();
const incoming = parts.join("\n");
els.input.value = existing ? `${existing}\n${incoming}` : incoming;
scheduleConvert();
});
}
els.inputPanel.classList.remove("drop-active");
});
});
setFormat("cpa");
})();
</script>
</body>
</html>

1
tools/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Developer-only analysis and stress tools."""

View File

@@ -0,0 +1,419 @@
"""
Parse real runtime monitor logs from both CSP and legacy state-machine runs.
The analyzer is intentionally read-only. It does not tune parameters or modify
runtime state; it only turns production logs into comparable stage rates.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import re
from typing import Iterable
_CSP_RE = re.compile(
r"^\[\*\] T:(?P<t>\d+) Q:(?P<q>\d+) phys:(?P<phys>\d+) "
r"(?:p_send:(?P<p_send>\d+) )?t_slot:(?P<t_slot>\d+) q_slot:(?P<q_slot>\d+) q_pend:(?P<q_pend>\d+)"
r"(?P<rest>.*)rate:(?P<rate>[0-9.]+)/min #(?P<ok>\d+)"
)
_STATE_RE = re.compile(
r"^\[\*\] slots:(?P<slots>\d+)/(?P<max_slots>\d+) act:(?P<active>\d+) "
r"cpu:(?P<cpu>[0-9.]+)% avg:(?P<cpu_avg>[0-9.]+)%/(?P<cpu_target>[0-9.]+) "
r"mem:(?P<mem>\d+)M T:(?P<t>\d+) Q:(?P<q>\d+) "
r"sent:(?P<q_sent>\d+) got:(?P<q_ret>\d+)\((?P<q_hit>[0-9.]+)%\) "
r"rate:(?P<rate>[0-9.]+)/min #(?P<ok>\d+)"
)
_SOLVER_TIMELINE_PREFIX = "[solver_timeline] "
@dataclass(frozen=True)
class MonitorRow:
kind: str
t: int
q: int
ok: int
rate: float
phys: int | None = None
p_send: int | None = None
t_slot: int | None = None
q_slot: int | None = None
q_pend: int | None = None
p_batch: float | None = None
t_prog: int | None = None
q_inflight: int | None = None
t_prod: int | None = None
q_sent: int | None = None
q_ret: int | None = None
q_adm: int | None = None
pair: int | None = None
fail: int | None = None
s_phys_wait: float | None = None
s_phys_hold: float | None = None
p_phys_wait: float | None = None
p_phys_hold: float | None = None
c_phys_wait: float | None = None
c_phys_hold: float | None = None
p_email_create: float | None = None
p_page_prepare: float | None = None
p_send_stage: float | None = None
c_page_acquire: float | None = None
c_verify: float | None = None
c_register: float | None = None
c_hot_hits: int | None = None
c_hot_misses: int | None = None
solver_goto: float | None = None
solver_inject: float | None = None
solver_initial: float | None = None
solver_click: float | None = None
solver_wait: float | None = None
solver_reuse: float | None = None
solver_visible: float | None = None
slots: int | None = None
max_slots: int | None = None
active: int | None = None
@property
def elapsed_min(self) -> float | None:
if self.ok <= 0 or self.rate <= 0:
return None
return self.ok / self.rate
@dataclass(frozen=True)
class SolverTimeline:
events: list[dict]
def _int_field(rest: str, name: str) -> int | None:
match = re.search(rf"\b{name}:(\d+)", rest)
return int(match.group(1)) if match else None
def _float_field(rest: str, name: str) -> float | None:
match = re.search(rf"\b{name}:([0-9.]+)", rest)
return float(match.group(1)) if match else None
def _float_pair_field(rest: str, name: str) -> tuple[float | None, float | None]:
match = re.search(rf"\b{name}:([0-9.]+)/([0-9.]+)", rest)
if not match:
return None, None
return float(match.group(1)), float(match.group(2))
def _float_triple_field(rest: str, name: str) -> tuple[float | None, float | None, float | None]:
match = re.search(rf"\b{name}:([0-9.]+)/([0-9.]+)/([0-9.]+)", rest)
if not match:
return None, None, None
return float(match.group(1)), float(match.group(2)), float(match.group(3))
def _int_pair_field(rest: str, name: str) -> tuple[int | None, int | None]:
match = re.search(rf"\b{name}:(\d+)/(\d+)", rest)
if not match:
return None, None
return int(match.group(1)), int(match.group(2))
def parse_monitor_lines(text_or_lines: str | Iterable[str]) -> list[MonitorRow]:
if isinstance(text_or_lines, str):
lines = text_or_lines.splitlines()
else:
lines = list(text_or_lines)
rows: list[MonitorRow] = []
for line in lines:
csp = _CSP_RE.match(line)
if csp:
rest = csp.group("rest")
s_phys_wait, s_phys_hold = _float_pair_field(rest, "s_phys")
p_phys_wait, p_phys_hold = _float_pair_field(rest, "p_phys")
c_phys_wait, c_phys_hold = _float_pair_field(rest, "c_phys")
p_email_create, p_page_prepare, p_send_stage = _float_triple_field(rest, "p_stage")
c_page_acquire, c_verify, c_register = _float_triple_field(rest, "c_stage")
c_hot_hits, c_hot_misses = _int_pair_field(rest, "c_hot")
rows.append(
MonitorRow(
kind="csp",
t=int(csp.group("t")),
q=int(csp.group("q")),
ok=int(csp.group("ok")),
rate=float(csp.group("rate")),
phys=int(csp.group("phys")),
p_send=int(csp.group("p_send")) if csp.group("p_send") is not None else None,
t_slot=int(csp.group("t_slot")),
q_slot=int(csp.group("q_slot")),
q_pend=int(csp.group("q_pend")),
p_batch=_float_field(rest, "p_batch"),
t_prog=_int_field(rest, "t_prog"),
q_inflight=_int_field(rest, "q_inflight"),
t_prod=_int_field(rest, "t_prod"),
q_sent=_int_field(rest, "q_sent"),
q_ret=_int_field(rest, "q_ret"),
q_adm=_int_field(rest, "q_adm"),
pair=_int_field(rest, "pair"),
fail=_int_field(rest, "fail"),
s_phys_wait=s_phys_wait,
s_phys_hold=s_phys_hold,
p_phys_wait=p_phys_wait,
p_phys_hold=p_phys_hold,
c_phys_wait=c_phys_wait,
c_phys_hold=c_phys_hold,
p_email_create=p_email_create,
p_page_prepare=p_page_prepare,
p_send_stage=p_send_stage,
c_page_acquire=c_page_acquire,
c_verify=c_verify,
c_register=c_register,
c_hot_hits=c_hot_hits,
c_hot_misses=c_hot_misses,
solver_goto=_float_field(rest, "solver_goto"),
solver_inject=_float_field(rest, "solver_inject"),
solver_initial=_float_field(rest, "solver_initial"),
solver_click=_float_field(rest, "solver_click"),
solver_wait=_float_field(rest, "solver_wait"),
solver_reuse=_float_field(rest, "solver_reuse"),
solver_visible=_float_field(rest, "solver_visible"),
)
)
continue
state = _STATE_RE.match(line)
if state:
rows.append(
MonitorRow(
kind="state_machine",
t=int(state.group("t")),
q=int(state.group("q")),
ok=int(state.group("ok")),
rate=float(state.group("rate")),
q_sent=int(state.group("q_sent")),
q_ret=int(state.group("q_ret")),
slots=int(state.group("slots")),
max_slots=int(state.group("max_slots")),
active=int(state.group("active")),
)
)
return rows
def _rate_delta(rows: list[MonitorRow], attr: str) -> float | None:
candidates = [r for r in rows if r.elapsed_min is not None and getattr(r, attr) is not None]
if len(candidates) < 2:
return None
first = candidates[0]
last = candidates[-1]
dt = (last.elapsed_min or 0) - (first.elapsed_min or 0)
if dt <= 0:
return None
return (getattr(last, attr) - getattr(first, attr)) / dt
def _leader(values: dict[str, float | None]) -> str | None:
if any(value is None for value in values.values()):
return None
return max(values, key=lambda key: values[key] or 0)
def _avg(values: list[float]) -> float | None:
if not values:
return None
return round(sum(values) / len(values), 2)
def _ratio(numerator: int, denominator: int) -> float | None:
if denominator <= 0:
return None
return round(numerator / denominator, 3)
def summarize_monitor_rows(rows: list[MonitorRow], recent_count: int = 6) -> dict[str, float | int | str | None]:
if not rows:
return {"rows": 0}
last = rows[-1]
recent = rows[-recent_count:]
summary: dict[str, float | int | str | None] = {
"rows": len(rows),
"kind": last.kind,
"last_ok": last.ok,
"last_cumulative_rate": last.rate,
"last_q_minus_t": last.q - last.t,
"last_phys": last.phys,
"last_p_send_sem": last.p_send,
"last_t_slot": last.t_slot,
"last_q_slot": last.q_slot,
"last_q_pend": last.q_pend,
"last_p_batch": last.p_batch,
"last_t_prog": last.t_prog,
"last_q_inflight": last.q_inflight,
"last_q_return_minus_t_prod": (
last.q_ret - last.t_prod
if last.q_ret is not None and last.t_prod is not None
else None
),
"last_slots": last.slots,
"last_t_prod": last.t_prod,
"last_q_ret": last.q_ret,
"last_q_adm": last.q_adm,
"last_pair": last.pair,
"last_fail": last.fail,
"last_s_phys_wait": last.s_phys_wait,
"last_s_phys_hold": last.s_phys_hold,
"last_p_phys_wait": last.p_phys_wait,
"last_p_phys_hold": last.p_phys_hold,
"last_c_phys_wait": last.c_phys_wait,
"last_c_phys_hold": last.c_phys_hold,
"last_p_email_create": last.p_email_create,
"last_p_page_prepare": last.p_page_prepare,
"last_p_send": last.p_send_stage,
"last_c_page_acquire": last.c_page_acquire,
"last_c_verify": last.c_verify,
"last_c_register": last.c_register,
"last_c_hot_hits": last.c_hot_hits,
"last_c_hot_misses": last.c_hot_misses,
"last_physical_hold_leader": _leader({
"s": last.s_phys_hold,
"p": last.p_phys_hold,
"c": last.c_phys_hold,
}),
"last_physical_wait_leader": _leader({
"s": last.s_phys_wait,
"p": last.p_phys_wait,
"c": last.c_phys_wait,
}),
"last_solver_goto": last.solver_goto,
"last_solver_inject": last.solver_inject,
"last_solver_initial": last.solver_initial,
"last_solver_click": last.solver_click,
"last_solver_wait": last.solver_wait,
"last_solver_reuse": last.solver_reuse,
"last_solver_visible": last.solver_visible,
"recent_ok_per_min": _rate_delta(recent, "ok"),
"recent_t_prod_per_min": _rate_delta(recent, "t_prod"),
"recent_q_ret_per_min": _rate_delta(recent, "q_ret"),
"recent_pair_per_min": _rate_delta(recent, "pair"),
}
return summary
def parse_solver_timelines(text_or_lines: str | Iterable[str]) -> list[SolverTimeline]:
if isinstance(text_or_lines, str):
lines = text_or_lines.splitlines()
else:
lines = list(text_or_lines)
timelines: list[SolverTimeline] = []
for line in lines:
if not line.startswith(_SOLVER_TIMELINE_PREFIX):
continue
payload = line[len(_SOLVER_TIMELINE_PREFIX):]
try:
events = json.loads(payload)
except json.JSONDecodeError:
continue
if isinstance(events, list):
timelines.append(SolverTimeline(events=[e for e in events if isinstance(e, dict)]))
return timelines
def _last_page_trace(events: list[dict]) -> dict | None:
for event in reversed(events):
page_trace = event.get("page_trace")
if isinstance(page_trace, dict):
return page_trace
return None
def summarize_solver_timelines(timelines: list[SolverTimeline]) -> dict[str, float | int | None]:
if not timelines:
return {"solver_timeline_count": 0}
ok_count = 0
click_calls: list[float] = []
click_move_ms: list[float] = []
click_down_up_ms: list[float] = []
render_to_token_ms: list[float] = []
token_write_to_poll_done_ms: list[float] = []
poll_attempts: list[float] = []
poll_read_avg_ms: list[float] = []
click_before_count = 0
center_iframe_hits = 0
turnstile_iframe_seen = 0
widget_seen = 0
for timeline in timelines:
events = timeline.events
page_trace = _last_page_trace(events)
if page_trace:
render_called = page_trace.get("render_called_at")
token_written = page_trace.get("token_written_at")
if isinstance(render_called, (int, float)) and isinstance(token_written, (int, float)):
render_to_token_ms.append(float(token_written) - float(render_called))
for event in events:
if event.get("event") == "click_before":
click_before_count += 1
dom = event.get("dom") if isinstance(event.get("dom"), dict) else {}
widget = dom.get("widget") if isinstance(dom.get("widget"), dict) else {}
center = dom.get("element_at_center") if isinstance(dom.get("element_at_center"), dict) else {}
if widget.get("present") and widget.get("visible"):
widget_seen += 1
if center.get("is_iframe"):
center_iframe_hits += 1
if (dom.get("turnstile_iframe_count") or 0) > 0:
turnstile_iframe_seen += 1
if event.get("event") == "click_after":
call_ms = event.get("click_call_ms")
if isinstance(call_ms, (int, float)):
click_calls.append(float(call_ms))
trace = event.get("click_trace") if isinstance(event.get("click_trace"), dict) else {}
move1 = trace.get("mouse_move1_ms")
move2 = trace.get("mouse_move2_ms")
down = trace.get("mouse_down_ms")
up = trace.get("mouse_up_ms")
move_total = sum(float(v) for v in (move1, move2) if isinstance(v, (int, float)))
down_up_total = sum(float(v) for v in (down, up) if isinstance(v, (int, float)))
if move_total:
click_move_ms.append(move_total)
if down_up_total:
click_down_up_ms.append(down_up_total)
if event.get("event") == "poll_done":
if event.get("ok"):
ok_count += 1
attempts = event.get("poll_attempts")
if isinstance(attempts, (int, float)):
poll_attempts.append(float(attempts))
read_avg = event.get("poll_read_ms_avg")
if isinstance(read_avg, (int, float)):
poll_read_avg_ms.append(float(read_avg))
page_trace = event.get("page_trace") if isinstance(event.get("page_trace"), dict) else {}
token_written = page_trace.get("token_written_at")
event_t = event.get("t")
created_at = page_trace.get("created_at")
if all(isinstance(v, (int, float)) for v in (token_written, event_t, created_at)):
token_write_to_poll_done_ms.append((float(event_t) * 1000.0) - (float(token_written) - float(created_at)))
return {
"solver_timeline_count": len(timelines),
"ok_count": ok_count,
"avg_click_call_ms": _avg(click_calls),
"avg_click_mouse_move_ms": _avg(click_move_ms),
"avg_click_down_up_ms": _avg(click_down_up_ms),
"avg_render_to_token_ms": _avg(render_to_token_ms),
"avg_token_write_to_poll_done_ms": _avg(token_write_to_poll_done_ms),
"avg_poll_attempts": _avg(poll_attempts),
"avg_poll_read_ms": _avg(poll_read_avg_ms),
"widget_seen_ratio": _ratio(widget_seen, click_before_count),
"center_iframe_hit_ratio": _ratio(center_iframe_hits, click_before_count),
"turnstile_iframe_seen_ratio": _ratio(turnstile_iframe_seen, click_before_count),
}
def analyze_text(text: str) -> dict[str, float | int | str | None]:
return summarize_monitor_rows(parse_monitor_lines(text))