Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ API_KEYS=
PERSIST_ADMIN_ACCOUNTS=1
# Milliseconds to skip an account after provider error before retrying it
ACCOUNT_COOLDOWN_MS=60000
# ── Z.ai auto-relogin via CDP ──
# CDP HTTP endpoint of a running Chrome with a logged-in chat.z.ai session.
# When set, the bridge will auto-refresh expired JWTs by reading localStorage from the browser.
ZAI_CDP_URL=http://127.0.0.1:9222
# Proactive refresh: refresh token when TTL drops below this (ms, default 300000 = 5min)
ZAI_TOKEN_REFRESH_THRESHOLD_MS=300000
# Cooldown between refresh attempts (ms, default 30000 = 30s)
ZAI_TOKEN_REFRESH_COOLDOWN_MS=30000
# Proactive check interval (ms, default 60000 = 60s)
ZAI_TOKEN_REFRESH_INTERVAL_MS=60000
16 changes: 16 additions & 0 deletions src/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ export class AccountManager {
return a;
}

/**
* Update an account's token (and optionally cookie) in-place and persist.
* Called by the auto-relogin flow after a CDP token refresh.
*/
updateToken(id, { token, cookie } = {}, { persist = truthy(this.env.PERSIST_ADMIN_ACCOUNTS ?? '1') } = {}) {
const a = this.get(id);
if (!a) return null;
if (token) a.token = token;
if (cookie) a.cookie = cookie;
a.ok = true;
a.lastError = '';
a.cooldownUntil = 0;
if (persist) this.persist();
return this._safe(a);
}

markSuccess(id) {
const a = this.get(id);
if (!a) return;
Expand Down
7 changes: 7 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ export const AUTH_PATH = process.env.AUTH_PATH || path.join(process.cwd(), 'auth
export const API_KEYS = String(process.env.API_KEYS || '').split(',').map(s => s.trim()).filter(Boolean);
export const GLM_BACKEND = (process.env.GLM_BACKEND || 'zai').toLowerCase();

// ── Z.ai auto-relogin via CDP ──────────────────────────────────
export const ZAI_CDP_URL = process.env.ZAI_CDP_URL || ''; // e.g. 'http://127.0.0.1:9222'
export const ZAI_TOKEN_REFRESH_ENABLED = ['1','true','yes','on'].includes(String(process.env.ZAI_TOKEN_REFRESH_ENABLED || '').toLowerCase()) || !!ZAI_CDP_URL;
export const ZAI_TOKEN_REFRESH_THRESHOLD_MS = Number(process.env.ZAI_TOKEN_REFRESH_THRESHOLD_MS || 300_000); // 5 min
export const ZAI_TOKEN_REFRESH_COOLDOWN_MS = Number(process.env.ZAI_TOKEN_REFRESH_COOLDOWN_MS || 30_000);
export const ZAI_TOKEN_REFRESH_INTERVAL_MS = Number(process.env.ZAI_TOKEN_REFRESH_INTERVAL_MS || 60_000);

export const MODELS = {
'glm-5': { provider: 'glm', thinking: false, webSearch: false, deepResearch: false },
'glm-5-thinking': { provider: 'glm', thinking: true, webSearch: false, deepResearch: false },
Expand Down
120 changes: 94 additions & 26 deletions src/providers/zai.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from 'crypto';
import { preparePrompt } from '../message.js';
import { getZaiBrowserClient, isZaiCaptchaError, shouldUseZaiBrowserFallback } from './zaiBrowser.js';
import { isZaiAuthError, getTokenTtlMs } from './zaiTokenRefresh.js';

export const ZAI_BASE = 'https://chat.z.ai';
export const ZAI_FE_VERSION = process.env.ZAI_FE_VERSION || 'prod-fe-1.1.46';
Expand Down Expand Up @@ -135,7 +136,17 @@ export function buildZaiRequest({ token, model='glm-5', prompt='', chatId='', pa
}

export class ZaiProvider {
constructor(account){ this.account=account; this.token=account.token || account.accessToken || account.access_token || account.jwt || account.refresh_token || account.refreshToken; this.cookie = account.cookie || account.cookies || process.env.ZAI_COOKIE || null; this.captchaVerifyParam = account.captcha_verify_param || account.captchaVerifyParam || process.env.ZAI_CAPTCHA_VERIFY_PARAM || null; this.browserFallback = shouldUseZaiBrowserFallback(process.env, account); if(!this.token) throw new Error('Z.ai token missing'); }
constructor(account, { tokenRefresher = null, accountManager = null, logger = console } = {}) {
this.account = account;
this.token = account.token || account.accessToken || account.access_token || account.jwt || account.refresh_token || account.refreshToken;
this.cookie = account.cookie || account.cookies || process.env.ZAI_COOKIE || null;
this.captchaVerifyParam = account.captcha_verify_param || account.captchaVerifyParam || process.env.ZAI_CAPTCHA_VERIFY_PARAM || null;
this.browserFallback = shouldUseZaiBrowserFallback(process.env, account);
this.tokenRefresher = tokenRefresher;
this.accountManager = accountManager;
this.logger = logger;
if(!this.token) throw new Error('Z.ai token missing');
}
async createChat(model, prompt) {
const timestamp = Math.floor(Date.now()/1000);
const messageId = randomUuid();
Expand All @@ -149,35 +160,92 @@ export class ZaiProvider {
const prompt = preparePrompt(messages, tools, { simpleTools:false, isMultiTurn:!!session.providerSessionId });
let chatId = session.providerSessionId || '';
let parentId = session.parentMessageId || null;
if (!chatId) {
const created = await this.createChat(modelCfg.id, prompt);
chatId = created.chatId;
parentId = created.messageId;
}
const req = buildZaiRequest({ token:this.token, model:modelCfg.id, prompt, chatId, parentMessageId:parentId, thinking:modelCfg.thinking, webSearch:modelCfg.webSearch, captchaVerifyParam:this.captchaVerifyParam, cookie:this.cookie });
const resp = await fetch(req.url, { method:'POST', headers:req.headers, body:JSON.stringify(req.body) });
const raw = await resp.text();
if (!resp.ok) {
if (this.browserFallback) {

// ── Attempt with auto-relogin on auth error ──
for (let attempt = 0; attempt < 2; attempt++) {
// Create chat only once (or retry if it failed with auth error on first attempt)
if (!chatId) {
try {
const created = await this.createChat(modelCfg.id, prompt);
chatId = created.chatId;
parentId = created.messageId;
} catch (err) {
if (isZaiAuthError(0, err.message) && this.tokenRefresher && attempt === 0) {
this.logger.warn?.(`[zai] createChat auth error — triggering CDP relogin`);
const refreshed = await this.tokenRefresher.refresh();
if (refreshed?.ok && refreshed?.token) {
this.token = refreshed.token;
if (refreshed.cookie) this.cookie = refreshed.cookie;
if (this.accountManager && this.account?.id) {
this.accountManager.updateToken(this.account.id, { token: refreshed.token, cookie: refreshed.cookie });
}
this.logger.info?.('[zai] token refreshed via CDP — retrying createChat');
continue;
}
}
throw err;
}
}
const req = buildZaiRequest({ token:this.token, model:modelCfg.id, prompt, chatId, parentMessageId:parentId, thinking:modelCfg.thinking, webSearch:modelCfg.webSearch, captchaVerifyParam:this.captchaVerifyParam, cookie:this.cookie });
const resp = await fetch(req.url, { method:'POST', headers:req.headers, body:JSON.stringify(req.body) });
const raw = await resp.text();

// Auth error detection → auto-relogin via CDP
if (!resp.ok && isZaiAuthError(resp.status, raw) && this.tokenRefresher && attempt === 0) {
this.logger.warn?.(`[zai] auth error (HTTP ${resp.status}) — triggering CDP relogin`);
const refreshed = await this.tokenRefresher.refresh();
if (refreshed?.ok && refreshed?.token) {
this.token = refreshed.token;
if (refreshed.cookie) this.cookie = refreshed.cookie;
// Update the account in AccountManager + persist
if (this.accountManager && this.account?.id) {
this.accountManager.updateToken(this.account.id, { token: refreshed.token, cookie: refreshed.cookie });
}
this.logger.info?.('[zai] token refreshed via CDP — retrying request');
continue; // retry with new token
}
this.logger.error?.('[zai] CDP relogin failed — falling through to normal error handling');
}

// Browser fallback for non-auth errors (captcha, etc.)
if (!resp.ok) {
if (this.browserFallback && !isZaiAuthError(resp.status, raw)) {
const browser = getZaiBrowserClient();
const browserResult = await browser.completeAndParse(req, { token:this.token, chatId });
if (!browserResult.ok) throw new Error(`Z.ai browser HTTP ${browserResult.status}: ${browserResult.raw.slice(0,200)}`);
const browserParsed = browserResult.parsed;
if (browserParsed.error) throw new Error(`Z.ai browser error: ${browserParsed.error}`);
return { text: browserParsed.text || browserResult.raw, reasoning: browserParsed.reasoning, providerSessionId: browserParsed.providerSessionId || chatId, parentMessageId: browserParsed.parentMessageId || req.messageId, prompt };
}
throw new Error(`Z.ai HTTP ${resp.status}: ${raw.slice(0,200)}`);
}
let parsed = parseZaiSse(raw);
if (parsed.error && this.browserFallback && isZaiCaptchaError(parsed.error)) {
const browser = getZaiBrowserClient();
const browserResult = await browser.completeAndParse(req, { token:this.token, chatId });
if (!browserResult.ok) throw new Error(`Z.ai browser HTTP ${browserResult.status}: ${browserResult.raw.slice(0,200)}`);
const browserParsed = browserResult.parsed;
if (browserParsed.error) throw new Error(`Z.ai browser error: ${browserParsed.error}`);
return { text: browserParsed.text || browserResult.raw, reasoning: browserParsed.reasoning, providerSessionId: browserParsed.providerSessionId || chatId, parentMessageId: browserParsed.parentMessageId || req.messageId, prompt };
parsed = browserResult.parsed;
if (parsed.error) throw new Error(`Z.ai browser error: ${parsed.error}`);
} else if (parsed.error) {
// Auth error in SSE body → try relogin
if (isZaiAuthError(0, parsed.error) && this.tokenRefresher && attempt === 0) {
this.logger.warn?.(`[zai] SSE auth error — triggering CDP relogin`);
const refreshed = await this.tokenRefresher.refresh();
if (refreshed?.ok && refreshed?.token) {
this.token = refreshed.token;
if (refreshed.cookie) this.cookie = refreshed.cookie;
if (this.accountManager && this.account?.id) {
this.accountManager.updateToken(this.account.id, { token: refreshed.token, cookie: refreshed.cookie });
}
this.logger.info?.('[zai] token refreshed via CDP — retrying request');
continue;
}
}
throw new Error(`Z.ai error: ${parsed.error}`);
}
throw new Error(`Z.ai HTTP ${resp.status}: ${raw.slice(0,200)}`);
}
let parsed = parseZaiSse(raw);
if (parsed.error && this.browserFallback && isZaiCaptchaError(parsed.error)) {
const browser = getZaiBrowserClient();
const browserResult = await browser.completeAndParse(req, { token:this.token, chatId });
if (!browserResult.ok) throw new Error(`Z.ai browser HTTP ${browserResult.status}: ${browserResult.raw.slice(0,200)}`);
parsed = browserResult.parsed;
if (parsed.error) throw new Error(`Z.ai browser error: ${parsed.error}`);
} else if (parsed.error) {
throw new Error(`Z.ai error: ${parsed.error}`);
return { text: parsed.text || raw, reasoning: parsed.reasoning, providerSessionId: parsed.providerSessionId || chatId, parentMessageId: parsed.parentMessageId || req.messageId, prompt };
}
return { text: parsed.text || raw, reasoning: parsed.reasoning, providerSessionId: parsed.providerSessionId || chatId, parentMessageId: parsed.parentMessageId || req.messageId, prompt };
// Should not reach here — both attempts exhausted
throw new Error('Z.ai: both attempts exhausted after relogin');
}
}
Loading