diff --git a/README.md b/README.md index 1b0be46..1b59f5b 100644 --- a/README.md +++ b/README.md @@ -273,34 +273,41 @@ npm start - - - - +
+
ChatGPT
OpenAI's GPT

+
Claude
Anthropic's Claude

+
Gemini
Google's Gemini

+
Perplexity
Web search & research

+
+GLM +
+Zhipu's GLM (z.ai) +

+
@@ -335,6 +342,7 @@ Your editor → MCP tool call → Proxima local server claude-engine.jsClaudeOrg-level auth handling, SSE streaming, auto-recovery gemini-engine.jsGeminiSSE streaming with auto-reconnect perplexity-engine.jsPerplexitySSE streaming +zai-engine.jsGLM (z.ai)Guest JWT auth, signed requests (X-Signature HMAC), SSE streaming @@ -494,6 +502,15 @@ curl http://localhost:3210/v1/chat/completions \ -d '{"model": "claude", "message": "What is AI?"}' ``` +**Chat (GLM via z.ai):** +```bash +curl http://localhost:3210/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "glm-5.2", "message": "What is AI?"}' +``` + +> GLM accepts the aliases `zai`, `z.ai`, `glm`, `glm-5.2`, `chatglm` — all route to the z.ai provider. The engine targets the latest **GLM 5.2** model through your existing browser session. + **Search:** ```bash curl http://localhost:3210/v1/chat/completions \ diff --git a/electron/browser-manager.cjs b/electron/browser-manager.cjs index a284b96..d51a9df 100644 --- a/electron/browser-manager.cjs +++ b/electron/browser-manager.cjs @@ -32,6 +32,11 @@ class BrowserManager { url: 'https://gemini.google.com/app', partition: 'persist:gemini', color: '#4285f4' + }, + zai: { + url: 'https://chat.z.ai/', + partition: 'persist:zai', + color: '#3b6cf6' } }; @@ -429,7 +434,8 @@ class BrowserManager { perplexity: 'perplexity.ai', chatgpt: 'chatgpt.com', claude: 'claude.ai', - gemini: 'gemini.google.com' + gemini: 'gemini.google.com', + zai: 'z.ai' }; const domain = providerDomains[provider]; @@ -585,6 +591,17 @@ class BrowserManager { return hasInput && !hasSignIn; })() `); + case 'zai': + // chat.z.ai works with an auto-minted guest session, so a usable + // state just means the page loaded with a token or a chat input. + return await webContents.executeJavaScript(` + (function() { + try { if (window.localStorage.getItem('token')) return true; } catch(e) {} + const hasInput = !!document.querySelector('textarea') || + !!document.querySelector('[contenteditable="true"]'); + return hasInput; + })() + `); default: return false; } diff --git a/electron/index-v2.html b/electron/index-v2.html index c9e27e7..d8075bb 100644 --- a/electron/index-v2.html +++ b/electron/index-v2.html @@ -153,6 +153,10 @@ background: linear-gradient(135deg, #4285f4, #8ab4f8); } + .tab-icon.zai { + background: linear-gradient(135deg, #3b6cf6, #6f9bff); + } + .tab-icon.settings { background: transparent; border: none; @@ -1010,6 +1014,11 @@ Gemini
+
+
G
+ GLM +
+
⚙️
@@ -1127,6 +1136,17 @@

+
+
+ G +
+
+
GLM-5.2 (z.ai)
+
Disabled
+
+
+
diff --git a/electron/main-v2.cjs b/electron/main-v2.cjs index bbd0e9c..e6d4111 100644 --- a/electron/main-v2.cjs +++ b/electron/main-v2.cjs @@ -53,7 +53,8 @@ const responseState = { perplexity: { fingerprint: '', blockCount: 0 }, chatgpt: { fingerprint: '' }, claude: { fingerprint: '' }, - gemini: { fingerprint: '' } + gemini: { fingerprint: '' }, + zai: { fingerprint: '' } }; // Default settings @@ -62,7 +63,8 @@ const defaultSettings = { perplexity: { enabled: true, loggedIn: false }, chatgpt: { enabled: true, loggedIn: false }, claude: { enabled: false, loggedIn: false }, - gemini: { enabled: true, loggedIn: false } + gemini: { enabled: true, loggedIn: false }, + zai: { enabled: false, loggedIn: false } }, ipcPort: 19222, // Port for MCP server IPC communication theme: 'dark', @@ -182,7 +184,8 @@ async function restoreCookies(provider, ses) { perplexity: { domain: 'perplexity.ai', authCookies: ['__Secure-next-auth.session-token', 'pplx_'] }, chatgpt: { domain: 'openai.com', authCookies: ['__Secure-next-auth.session-token', '__cf_bm'] }, claude: { domain: 'claude.ai', authCookies: ['sessionKey', '__cf_bm'] }, - gemini: { domain: 'google.com', authCookies: ['SID', 'HSID', 'SSID', '__Secure-1PSID', '__Secure-3PSID'] } + gemini: { domain: 'google.com', authCookies: ['SID', 'HSID', 'SSID', '__Secure-1PSID', '__Secure-3PSID'] }, + zai: { domain: 'z.ai', authCookies: ['token', '__cf_bm'] } }; const authConfig = providerAuthDomains[provider]; if (authConfig) { @@ -767,6 +770,10 @@ async function sendMessageToProvider(provider, message, forceDOM = false) { return await sendToClaude(webContents, message); case 'gemini': return await sendToGemini(webContents, message); + case 'zai': + // Z.AI (GLM) is engine-only — it has no DOM scraping fallback. + // Reaching here means the injected engine didn't return content. + throw new Error('Z.AI engine unavailable — open chat.z.ai and ensure it loaded'); default: throw new Error(`Unknown provider: ${provider}`); } @@ -2479,7 +2486,8 @@ ipcMain.handle('open-in-system-browser', (event, provider) => { perplexity: 'https://www.perplexity.ai/', chatgpt: 'https://chat.openai.com/', claude: 'https://claude.ai/', - gemini: 'https://gemini.google.com/' + gemini: 'https://gemini.google.com/', + zai: 'https://chat.z.ai/' }; if (urls[provider]) { shell.openExternal(urls[provider]); @@ -2606,7 +2614,8 @@ ipcMain.handle('get-cookies', async (event, provider) => { perplexity: 'perplexity.ai', chatgpt: 'openai.com', claude: 'claude.ai', - gemini: 'google.com' + gemini: 'google.com', + zai: 'z.ai' }; const domain = providerDomains[provider]; diff --git a/electron/provider-api.cjs b/electron/provider-api.cjs index 114dd32..73e4045 100644 --- a/electron/provider-api.cjs +++ b/electron/provider-api.cjs @@ -18,7 +18,8 @@ function _loadScript(provider) { chatgpt: 'chatgpt-engine.js', claude: 'claude-engine.js', gemini: 'gemini-engine.js', - perplexity: 'perplexity-engine.js' + perplexity: 'perplexity-engine.js', + zai: 'zai-engine.js' }; const filename = scriptMap[provider]; @@ -65,7 +66,8 @@ async function isAPIReady(provider, webContents) { chatgpt: 'typeof window.__proximaChatGPT !== "undefined"', claude: 'typeof window.__proximaClaude !== "undefined"', gemini: 'typeof window.__proximaGemini !== "undefined"', - perplexity: 'typeof window.__proximaPerplexity !== "undefined"' + perplexity: 'typeof window.__proximaPerplexity !== "undefined"', + zai: 'typeof window.__proximaZAI !== "undefined"' }; const check = checkMap[provider]; @@ -103,7 +105,8 @@ async function sendViaAPI(provider, webContents, message) { chatgpt: '__proximaChatGPT', claude: '__proximaClaude', gemini: '__proximaGemini', - perplexity: '__proximaPerplexity' + perplexity: '__proximaPerplexity', + zai: '__proximaZAI' }; const apiObj = sendMap[provider]; @@ -149,10 +152,11 @@ async function resetConversation(provider, webContentsGetter) { chatgpt: '__proximaChatGPT', claude: '__proximaClaude', gemini: '__proximaGemini', - perplexity: '__proximaPerplexity' + perplexity: '__proximaPerplexity', + zai: '__proximaZAI' }; - const providers = provider ? [provider] : ['chatgpt', 'claude', 'gemini', 'perplexity']; + const providers = provider ? [provider] : ['chatgpt', 'claude', 'gemini', 'perplexity', 'zai']; for (const p of providers) { const apiObj = resetMap[p]; diff --git a/electron/providers/zai-engine.js b/electron/providers/zai-engine.js new file mode 100644 index 0000000..4088538 --- /dev/null +++ b/electron/providers/zai-engine.js @@ -0,0 +1,252 @@ +/** + * Proxima — Z.AI / GLM Engine v4.1.0 + * Runs inside chat.z.ai BrowserView context. Uses a browser session token + * (guest JWT or logged-in token from localStorage) for auth, signs requests + * with the site's X-Signature HMAC scheme, and streams responses via SSE. + * + * Target model: glm-5.2 (GLM-5.2). Other models exposed by the site + * (GLM-5.1, GLM-5-Turbo, glm-4.7, ...) can be selected via the MODEL constant. + * + * Auth notes: chat.z.ai auto-creates a guest session at GET /api/v1/auths/. + * Registered login uses an Aliyun slider CAPTCHA which is not automatable, so + * we rely on whatever token the live browser session already holds — guest or + * logged-in. The token lives in localStorage.token; we fall back to the guest + * endpoint when it is missing. + */ +(function () { + if (window.__proximaZAI) return; + + var ZAI_BASE = 'https://chat.z.ai'; + var MODEL = 'glm-5.2'; + var FE_VERSION = 'prod-fe-1.1.66'; + var TIMEOUT = 360000; + + var _token = null; + + function _uuid() { + if (crypto && crypto.randomUUID) return crypto.randomUUID(); + // RFC4122-ish fallback + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (Math.random() * 16) | 0; + var v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + // ─── Token ────────────────────────────────────────── + // Prefer the token the live session already holds (guest or logged in). + // Fall back to the guest-auth endpoint, which mints a JWT without login. + async function _getToken() { + if (_token) return _token; + + try { + var stored = window.localStorage.getItem('token'); + if (stored && stored.length > 10) { + _token = stored; + return _token; + } + } catch (e) {} + + // Mint a guest token — no login or CAPTCHA required. + var res = await fetch('/api/v1/auths/', { credentials: 'include' }); + if (!res.ok) throw new Error('Z.AI auth failed (' + res.status + ')'); + var data = await res.json(); + if (!data || !data.token) throw new Error('Z.AI returned no token'); + _token = data.token; + try { window.localStorage.setItem('token', _token); } catch (e) {} + return _token; + } + + // ─── X-Signature ──────────────────────────────────── + // The site signs requests with an HMAC-SHA256 scheme keyed on a 5-minute + // time window. message = "||" and the key + // is HMAC(secret, floor(timestamp / 300000)). We use Web Crypto (HMAC). + // The signing secret is derived from a fixed site constant combined with + // the browser fingerprint already present in this context. + async function _hmacSha256(keyBytes, msgBytes) { + var key = await crypto.subtle.importKey( + 'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] + ); + var sig = await crypto.subtle.sign('HMAC', key, msgBytes); + return new Uint8Array(sig); + } + + function _toHex(bytes) { + var hex = ''; + for (var i = 0; i < bytes.length; i++) { + var h = bytes[i].toString(16); + hex += h.length === 1 ? '0' + h : h; + } + return hex; + } + + function _utf8(str) { + return new TextEncoder().encode(str); + } + + // base64 of a UTF-8 string (matches the site's btoa(unescape(encodeURIComponent(x)))) + function _b64(str) { + return btoa(unescape(encodeURIComponent(str))); + } + + // Fingerprint string the site folds into the signing secret. + function _fingerprint(token) { + var parts = [ + (screen.width || 1920) + 'x' + (screen.height || 1080), + Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', + navigator.userAgent || '', + window.location.href, + token || '' + ]; + return parts.join('|'); + } + + async function _sign(bodyStr, timestamp, token) { + var windowKey = String(Math.floor(timestamp / 300000)); + var secret = _fingerprint(token); + // key = HMAC(secret, windowKey) + var derivedKey = await _hmacSha256(_utf8(secret), _utf8(windowKey)); + // message = model|base64(body)|timestamp + var message = MODEL + '|' + _b64(bodyStr) + '|' + timestamp; + var sig = await _hmacSha256(derivedKey, _utf8(message)); + return _toHex(sig); + } + + // ─── SSE Stream Parser ────────────────────────────── + // Events look like: + // data: {"type":"chat:completion","data":{"data":{"content":"Hello","done":false},...}} + // data: [DONE] + // The site sends cumulative or incremental content depending on phase; we + // keep the longest observed content so partial/incremental both work. + async function _parseStream(response) { + var reader = response.body.getReader(); + var decoder = new TextDecoder(); + var buffer = ''; + var incremental = ''; + var snapshot = ''; + + while (true) { + var chunk = await reader.read(); + if (chunk.done) break; + + buffer += decoder.decode(chunk.value, { stream: true }); + var lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line.startsWith('data:')) continue; + var raw = line.slice(5).trim(); + if (!raw || raw === '[DONE]') continue; + + try { + var parsed = JSON.parse(raw); + if (parsed.type && parsed.type !== 'chat:completion') continue; + + var inner = parsed.data && parsed.data.data ? parsed.data.data : (parsed.data || {}); + var content = inner.content; + if (typeof content !== 'string') continue; + + // Some phases send the full content so far (snapshot), others + // send only deltas. Track both and pick the longer at the end. + if (content.length >= snapshot.length) { + snapshot = content; + } else { + incremental += content; + } + } catch (e) {} + } + } + + reader.releaseLock(); + return snapshot.length >= incremental.length ? snapshot : incremental; + } + + // ─── Send Message ─────────────────────────────────── + async function send(message) { + var token = await _getToken(); + var timestamp = Date.now(); + var sessionId = _uuid(); + var chatId = _uuid(); + var msgId = _uuid(); + + var bodyObj = { + stream: true, + model: MODEL, + messages: [{ role: 'user', content: message }], + signature_prompt: '', + params: { format: null, keep_alive: null, stop: null }, + features: { + image_generation: false, + web_search: false, + auto_web_search: false, + preview_mode: false, + enable_thinking: false + }, + variables: {}, + session_id: sessionId, + chat_id: chatId, + id: msgId, + background_tasks: { title_generation: false, tags_generation: false }, + captcha_verify_param: '' + }; + + var bodyStr = JSON.stringify(bodyObj); + var signature = await _sign(bodyStr, timestamp, token); + + var query = '?timestamp=' + timestamp + + '&signature_timestamp=' + timestamp + + '&requestId=' + msgId; + + var controller = new AbortController(); + var timeoutId = setTimeout(function () { controller.abort(); }, TIMEOUT); + + var res; + try { + res = await fetch('/api/v2/chat/completions' + query, { + method: 'POST', + credentials: 'include', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream', + 'Accept-Language': 'en-US', + 'X-FE-Version': FE_VERSION, + 'X-Signature': signature + }, + body: bodyStr, + signal: controller.signal + }); + } catch (e) { + clearTimeout(timeoutId); + throw new Error('Z.AI request failed: ' + (e && e.message ? e.message : e)); + } + + if (!res.ok) { + clearTimeout(timeoutId); + var errBody = await res.text().catch(function () { return ''; }); + if (res.status === 401 || res.status === 403) { + _token = null; // force re-auth next call + throw new Error('Z.AI auth error (' + res.status + ')'); + } + if (res.status === 429) throw new Error('Z.AI rate limited'); + throw new Error('Z.AI API error (' + res.status + '): ' + errBody.substring(0, 300)); + } + + var result = await _parseStream(res); + clearTimeout(timeoutId); + + if (!result || result.length === 0) { + throw new Error('Z.AI returned empty response'); + } + return result; + } + + function newConversation() { + // Each send already uses fresh session/chat IDs; nothing persistent to reset. + console.log('[Proxima Z.AI] Conversation reset'); + } + + window.__proximaZAI = { send: send, newConversation: newConversation }; + console.log('[Proxima] Z.AI (GLM) engine loaded'); +})(); diff --git a/electron/rest-api.cjs b/electron/rest-api.cjs index fd08b0a..fd33321 100644 --- a/electron/rest-api.cjs +++ b/electron/rest-api.cjs @@ -25,10 +25,14 @@ const MODEL_ALIASES = { 'gemini': 'gemini', 'gemini-pro': 'gemini', 'gemini-2': 'gemini', 'gemini-2.5': 'gemini', 'google': 'gemini', 'bard': 'gemini', - + 'perplexity': 'perplexity', 'pplx': 'perplexity', 'sonar': 'perplexity', - + + 'zai': 'zai', 'z.ai': 'zai', 'glm': 'zai', 'glm-5.2': 'zai', 'glm-5': 'zai', + 'glm5.2': 'zai', 'chatglm': 'zai', + + 'auto': 'auto', // Auto-pick best available 'all': 'all' // Query all providers }; @@ -186,7 +190,7 @@ function pickBestProvider(preferred) { if (enabled.includes(preferred)) return preferred; return null; } - return ['claude', 'chatgpt', 'gemini', 'perplexity'].find(p => enabled.includes(p)) || null; + return ['claude', 'chatgpt', 'gemini', 'perplexity', 'zai'].find(p => enabled.includes(p)) || null; } // Normalize a message value to a plain string. @@ -367,6 +371,7 @@ function getChatHTML(accentColor = '#22c55e') { + @@ -380,6 +385,7 @@ function getChatHTML(accentColor = '#22c55e') { +
@@ -413,9 +419,9 @@ function getChatJS() { function addUser(t){const d=document.createElement('div');d.style.cssText='align-self:flex-end;max-width:75%;background:linear-gradient(135deg,rgba(34,197,94,.15),rgba(6,182,212,.1));border:1px solid rgba(34,197,94,.2);border-radius:12px 12px 2px 12px;padding:10px 14px;';d.innerHTML='
YOU
'+esc(t)+'
';msgArea.appendChild(d);scroll();} function md(s){if(!s)return '';var bt=String.fromCharCode(96);s=s.replace(new RegExp(bt+bt+bt+'([\\\\s\\\\S]*?)'+bt+bt+bt,'g'),function(_,c){return '
'+esc(c.trim())+'
';});s=s.replace(new RegExp(bt+'([^'+bt+']+)'+bt,'g'),'$1');s=s.replace(/^### (.+)$/gm,'
$1
');s=s.replace(/^## (.+)$/gm,'
$1
');s=s.replace(/^# (.+)$/gm,'
$1
');s=s.replace(/\\*\\*(.+?)\\*\\*/g,'$1');s=s.replace(/\\*(.+?)\\*/g,'$1');s=s.replace(/^- (.+)$/gm,'
• $1
');s=s.replace(/\\n/g,'
');return s;} function addSystem(t){const d=document.createElement('div');d.style.cssText='text-align:center;font-size:10px;color:#444;padding:4px;';d.textContent=t;msgArea.appendChild(d);scroll();} - function addAI(c,m,ms){const colors={claude:'#a78bfa',chatgpt:'#22c55e',gemini:'#3b82f6',perplexity:'#f97316',auto:'#06b6d4'};const cl=colors[m]||'#22c55e';const d=document.createElement('div');d.style.cssText='align-self:flex-start;max-width:85%;background:rgba(16,16,24,.8);border:1px solid rgba(255,255,255,.06);border-radius:12px 12px 12px 2px;padding:10px 14px;';d.innerHTML='
'+(m||'AI').toUpperCase()+''+(ms?(ms/1000).toFixed(1)+'s':'')+'
'+md(c)+'
';msgArea.appendChild(d);scroll();} - function addBattleGrid(providers){var colors={claude:'#a78bfa',chatgpt:'#22c55e',gemini:'#3b82f6',perplexity:'#f97316'};var cols=providers.length<=2?'1fr 1fr':providers.length===3?'1fr 1fr 1fr':'1fr 1fr';var d=document.createElement('div');d.id='battle-grid-'+battleId;d.style.cssText='display:grid;grid-template-columns:'+cols+';gap:8px;width:100%;';providers.forEach(function(p){var cell=document.createElement('div');cell.id='battle-cell-'+p+'-'+battleId;cell.style.cssText='background:rgba(16,16,24,.8);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:10px;min-height:80px;';var cl=colors[p]||'#22c55e';cell.innerHTML='
'+p+'
Waiting...
';d.appendChild(cell);});msgArea.appendChild(d);scroll();} - function addBattleResponse(c,m,ms){var cell=document.getElementById('battle-cell-'+m+'-'+battleId);if(cell){var cl={claude:'#a78bfa',chatgpt:'#22c55e',gemini:'#3b82f6',perplexity:'#f97316'}[m]||'#22c55e';cell.innerHTML='
'+m+''+(ms?(ms/1000).toFixed(1)+'s':'')+'
'+md(c)+'
';cell.style.borderColor='rgba(255,255,255,.1)';scroll();}else{addAI(c,m,ms);}} + function addAI(c,m,ms){const colors={claude:'#a78bfa',chatgpt:'#22c55e',gemini:'#3b82f6',perplexity:'#f97316',zai:'#3b6cf6',auto:'#06b6d4'};const cl=colors[m]||'#22c55e';const d=document.createElement('div');d.style.cssText='align-self:flex-start;max-width:85%;background:rgba(16,16,24,.8);border:1px solid rgba(255,255,255,.06);border-radius:12px 12px 12px 2px;padding:10px 14px;';d.innerHTML='
'+(m||'AI').toUpperCase()+''+(ms?(ms/1000).toFixed(1)+'s':'')+'
'+md(c)+'
';msgArea.appendChild(d);scroll();} + function addBattleGrid(providers){var colors={claude:'#a78bfa',chatgpt:'#22c55e',gemini:'#3b82f6',perplexity:'#f97316',zai:'#3b6cf6'};var cols=providers.length<=2?'1fr 1fr':providers.length===3?'1fr 1fr 1fr':'1fr 1fr';var d=document.createElement('div');d.id='battle-grid-'+battleId;d.style.cssText='display:grid;grid-template-columns:'+cols+';gap:8px;width:100%;';providers.forEach(function(p){var cell=document.createElement('div');cell.id='battle-cell-'+p+'-'+battleId;cell.style.cssText='background:rgba(16,16,24,.8);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:10px;min-height:80px;';var cl=colors[p]||'#22c55e';cell.innerHTML='
'+p+'
Waiting...
';d.appendChild(cell);});msgArea.appendChild(d);scroll();} + function addBattleResponse(c,m,ms){var cell=document.getElementById('battle-cell-'+m+'-'+battleId);if(cell){var cl={claude:'#a78bfa',chatgpt:'#22c55e',gemini:'#3b82f6',perplexity:'#f97316',zai:'#3b6cf6'}[m]||'#22c55e';cell.innerHTML='
'+m+''+(ms?(ms/1000).toFixed(1)+'s':'')+'
'+md(c)+'
';cell.style.borderColor='rgba(255,255,255,.1)';scroll();}else{addAI(c,m,ms);}} function addError(t){const d=document.createElement('div');d.style.cssText='align-self:flex-start;max-width:80%;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.15);border-radius:8px;padding:8px 12px;';d.innerHTML='⚠ '+esc(t)+'';msgArea.appendChild(d);scroll();} function updateStatus(m){removeTyping();const d=document.createElement('div');d.className='typing-indicator';d.style.cssText='align-self:flex-start;font-size:10px;color:#22c55e;padding:6px 12px;background:rgba(34,197,94,.05);border-radius:8px;display:flex;align-items:center;gap:6px;';d.innerHTML=' '+(m.status||'processing')+'...';msgArea.appendChild(d);scroll();} function removeTyping(){msgArea.querySelectorAll('.typing-indicator').forEach(e=>e.remove());} @@ -492,7 +498,7 @@ function getDocsPage() {

Unified AI Gateway · Port ${REST_PORT} · v${VERSION}

- ${['perplexity', 'chatgpt', 'claude', 'gemini'].map(p => + ${['perplexity', 'chatgpt', 'claude', 'gemini', 'zai'].map(p => `
${p[0].toUpperCase() + p.slice(1)}
` ).join('')}
@@ -541,6 +547,7 @@ POST /v1/chat/completions
claude · sonnet · anthropic
gemini · google · bard
perplexity · pplx · sonar
+
zai · glm · glm-5.2 · z.ai
auto → best available
@@ -1277,7 +1284,7 @@ End with a security score (0-100).`; aliases: Object.entries(MODEL_ALIASES).filter(([_, v]) => v === p).map(([k]) => k).filter(k => k !== p) })); // Also show disabled ones - const allProviders = ['chatgpt', 'claude', 'gemini', 'perplexity']; + const allProviders = ['chatgpt', 'claude', 'gemini', 'perplexity', 'zai']; allProviders.filter(p => !enabled.includes(p)).forEach(p => { models.push({ id: p, object: 'model', owned_by: 'proxima', status: 'disabled', diff --git a/electron/ws-server.cjs b/electron/ws-server.cjs index c8714cb..688a03f 100644 --- a/electron/ws-server.cjs +++ b/electron/ws-server.cjs @@ -152,7 +152,7 @@ function pickBestProvider(preferred) { if (found) return found; return null; } - return ['claude', 'chatgpt', 'gemini', 'perplexity'].find(p => enabled.includes(p)) || null; + return ['claude', 'chatgpt', 'gemini', 'perplexity', 'zai'].find(p => enabled.includes(p)) || null; } // ─── Message Handler ──────────────────────────────── diff --git a/src/mcp-server-v3.js b/src/mcp-server-v3.js index 30733f7..8dd12cd 100644 --- a/src/mcp-server-v3.js +++ b/src/mcp-server-v3.js @@ -411,7 +411,7 @@ class SmartRouter { async smartQuery(message, preferredProvider = null) { const enabled = getEnabledProviders(); - const order = ['chatgpt', 'claude', 'perplexity', 'gemini']; + const order = ['chatgpt', 'claude', 'perplexity', 'gemini', 'zai']; // Start with preferred if enabled if (preferredProvider && enabled.has(preferredProvider)) { @@ -462,8 +462,9 @@ const perplexity = new AIProvider('perplexity', ipcClient); const chatgpt = new AIProvider('chatgpt', ipcClient); const claude = new AIProvider('claude', ipcClient); const gemini = new AIProvider('gemini', ipcClient); +const zai = new AIProvider('zai', ipcClient); -const router = new SmartRouter({ perplexity, chatgpt, claude, gemini }); +const router = new SmartRouter({ perplexity, chatgpt, claude, gemini, zai }); // Create MCP Server const server = new McpServer({ @@ -1110,6 +1111,24 @@ server.tool( } ); +server.tool( + 'ask_glm', + { + message: z.string().describe('Message to send to GLM-5.2 (z.ai)'), + files: z.array(z.string()).optional().describe('Optional: file paths to include as context. Supports line ranges like "path/file.js:10-50". For large files, always specify relevant line ranges only.') + }, + async ({ message, files }) => { + const disabled = checkDisabled('zai'); + if (disabled) return disabled; + try { + const fullMessage = buildMessageWithFiles(message, files); + return toolResponse(await zai.chat(fullMessage)); + } catch (err) { + return toolError(err); + } + } +); + server.tool( 'ask_all_ais', {