diff --git a/docs/ZHIZI_CLOUD_ENGINE.md b/docs/ZHIZI_CLOUD_ENGINE.md index dc09bb4..71f7c10 100644 --- a/docs/ZHIZI_CLOUD_ENGINE.md +++ b/docs/ZHIZI_CLOUD_ENGINE.md @@ -19,20 +19,48 @@ GoAgent -> 智子云登录 -> Socket.IO 远程 KataGo -> GTP kata-analyze 在 GoAgent 设置页打开“分析引擎”: 1. 引擎模式选择 `智子云远程算力:直连`。 -2. 填写智子云账号和密码,点击“登录并连接智子云”。 +2. 选择算力档位: + - `标准`:默认 `gpu-type 1x`,适合日常复盘。 + - `快速`:默认 `gpu-type 3x`,适合胜率图和当前手。 + - `深度`:默认 `gpu-type 6x`,适合关键局面精读。 +3. 填写智子云账号和密码,点击“登录并连接智子云”。 - GoAgent 会调用智子云登录接口获取 token。 - 登录成功后,GoAgent 会把 token 保存到本地加密存储。 - 登录成功后,引擎模式会自动切到 `智子云远程算力:直连`。 - 后续只需要启动 GoAgent,不需要先启动其它应用。 -3. 如果密码登录提示密码不正确,可以点击“发送验证码”,收到短信后用“验证码登录”。 +4. 如果密码登录提示密码不正确,可以点击“发送验证码”,收到短信后用“验证码登录”。 - 这和智子 Web 端的 `send-code` / `fast-login` 流程一致。 - 验证码只用于本次登录;GoAgent 不会长期保存验证码。 -4. Token 通常不需要手动填写。只有你已经从其它方式拿到了 token,才使用“高级:Token”。 -5. `zz-ikatago 路径` 是兼容旧连接器的高级选项,普通用户可以不填。 -6. 附加参数一般留空;只有你明确知道智子云 Socket.IO 连接参数时再填写。 +5. 点击 `检测连接`。GoAgent 会获取短期 Socket.IO token,启动远程 KataGo worker,并跑一个小局面 smoke test。 +6. Token 通常不需要手动填写。只有你已经从其它方式拿到了 token,才使用“高级:Token”。 +7. `zz-ikatago 路径` 是兼容旧连接器的高级选项,普通用户可以不填。 +8. 附加参数一般留空;只有你明确知道智子云 Socket.IO 连接参数时再填写。 `自动` 模式现在是本机优先的默认模式,不会因为本机分析失败就自动上传局面到智子云。需要远程算力时,请手动启用 `智子云远程算力:直连`。 +## 充值 / 官方 App + +当前智子开放文档没有提供可嵌入 GoAgent 的订单、支付、充值或余额 API。因此 GoAgent 不做应用内支付,也不会保存任何支付信息。 + +如果检测连接时提示 `额度不足`、`没有可用算力` 或 `暂无 worker`: + +1. 点击设置页里的 `下载智子官方 App`。 +2. GoAgent 会打开 `https://zhizigo.com/download`,这是智子围棋官方 App 下载页。 +3. 在智子官方 App 内登录同一账号,并按 App 内指引充值/开通远程算力。 +4. 回到 GoAgent,点击 `刷新远程会话` 或 `检测连接`。 + +后续如果智子官方开放稳定的购买/余额 API,可以在不改变远程分析协议的前提下接入更直接的状态检测。 + +## Socket.IO 参数 + +GoAgent 默认按智子开放文档构造远程 worker 参数: + +```text +--platform all --engine-type go --gpu-type 1x --kata-name katago-TENSORRT --kata-weight 28bnbt +``` + +其中 `gpu-type` 会跟随设置里的算力档位变化,`kata-weight` 默认是 `28bnbt`。高级用户可以在设置里覆盖 platform、gpu type、kata name、kata weight 或附加参数。 + ## 退出和重新登录 如果刚购买套餐、充值或修改了智子云账号状态,但 GoAgent 仍提示额度不足或远程算力不可用,可以在设置页点击 `退出智子云登录`。GoAgent 会: @@ -67,7 +95,7 @@ GoAgent 不会把智子 token 写入普通设置文件,也不会在日志中 - `智子云未登录`:当前选择了 `智子云远程算力:直连`,但本地没有可用 token。请在设置页用账号密码或短信验证码登录;普通用户不需要填写 `zz-ikatago` 路径。 - `密码不正确`:智子官方密码接口拒绝了这组密码。可以改用“发送验证码”再“验证码登录”。 - `验证码不正确或已过期`:重新发送验证码后再登录。 -- `智子云余额不足或当前没有可用算力`:先确认智子云套餐或余额已经生效;如果刚充值,点击 `退出智子云登录` 后重新登录,让 GoAgent 刷新 token 和远程会话。 +- `智子云余额不足或当前没有可用算力`:先下载智子官方 App,在 App 内充值/开通远程算力;如果刚充值,点击 `刷新远程会话` 或 `检测连接`。 - `智子云 KataGo 启动超时`:检查 token 是否有效,或在 GoAgent 中重新登录。 - `智子云 GTP 命令失败`:可能是远端引擎尚未 ready、账号状态异常,或智子云当前连接不可用。 - 如果本机 KataGo 可用,且不是强制智子云模式,GoAgent 会在智子云失败后回退本机 KataGo。 diff --git a/src/main/index.ts b/src/main/index.ts index ea3dd8c..646e22a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,7 +2,7 @@ import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell, type Conte import { isAbsolute, relative, resolve, join } from 'node:path' import { appHome, findGame, getGames, getIkatagoPassword, getSettings, getTtsCustomApiKey, getTtsVolcengineAccessToken, getTtsVolcengineApiKey, getZhiziToken, hasIkatagoPassword, hasLlmApiKey, hasTtsCustomApiKey, hasTtsVolcengineAccessToken, hasTtsVolcengineApiKey, hasZhiziToken, replaceSettings, setSettings, upsertGames } from './lib/store' import { BRAND_NAME } from '@shared/brand' -import type { AnalyzeGameQuickRequest, AnalyzePositionRequest, AnalyzeTrialPositionRequest, AppSettings, DashboardData, FoxSyncRequest, KataGoAssetInstallRequest, KataGoBenchmarkRequest, KataGoCancelAnalysisRequest, LibraryDeleteRequest, LlmModelsListRequest, LlmSettingsTestRequest, ReviewRequest, TeacherBoardImageRenderImage, TeacherBoardImageRenderRequest, TeacherBoardImageRenderResponse, TeacherChatMessage, TeacherRunCancelRequest, TeacherRunRequest, ZhiziCloudConnectionTestResult, ZhiziCloudLoginCodeRequest, ZhiziCloudLoginRequest, ZhiziCloudLoginResult, ZhiziCloudSendCodeRequest, ZhiziCloudSendCodeResult } from './lib/types' +import type { AnalyzeGameQuickRequest, AnalyzePositionRequest, AnalyzeTrialPositionRequest, AppSettings, DashboardData, FoxSyncRequest, KataGoAssetInstallRequest, KataGoBenchmarkRequest, KataGoCancelAnalysisRequest, LibraryDeleteRequest, LlmModelsListRequest, LlmSettingsTestRequest, ReviewRequest, TeacherBoardImageRenderImage, TeacherBoardImageRenderRequest, TeacherBoardImageRenderResponse, TeacherChatMessage, TeacherRunCancelRequest, TeacherRunRequest, ZhiziCloudAccountStatusResult, ZhiziCloudConnectionTestResult, ZhiziCloudLoginCodeRequest, ZhiziCloudLoginRequest, ZhiziCloudLoginResult, ZhiziCloudSendCodeRequest, ZhiziCloudSendCodeResult, ZhiziCloudStatus } from './lib/types' import { importSgfFile, readGameRecord } from './services/sgf' import { ensureFoxGameDownloaded, syncFoxGames } from './services/fox' import { runReview } from './services/review' @@ -30,7 +30,7 @@ import { } from './services/studentProfile' import { archiveTeacherSession, createTeacherSession, deleteTeacherSession, getActiveTeacherSession, listTeacherSessions, updateTeacherSessionMessages } from './services/teacherSession' import { clearTtsCache, inspectTtsAssets, listTtsVoices, synthesizeTts, testTtsSettings } from './services/tts' -import { loginZhiziCloudByCode, loginZhiziCloudByPassword, sendZhiziCloudLoginCode } from './services/zhiziCloudAuth' +import { inspectZhiziCloudAccount, loginZhiziCloudByCode, loginZhiziCloudByPassword, sendZhiziCloudLoginCode, ZHIZI_OFFICIAL_APP_DOWNLOAD_URL } from './services/zhiziCloudAuth' import { queryZhiziGtpAnalysisBatch } from './services/zhiziGtpEngine' let mainWindow: BrowserWindow | null = null @@ -108,21 +108,40 @@ function requestTeacherBoardImages(event: IpcMainInvokeEvent, request: TeacherBo }) } -function humanizeZhiziConnectionError(error: unknown): string { +function classifyZhiziConnectionError(error: unknown): { status: ZhiziCloudStatus; message: string } { const text = error instanceof Error ? error.message : String(error) if (/not_enough_credit|余额不足|not enough credit|没有可用算力/i.test(text)) { - return '智子云 token 有效,但远程 worker 分配失败:当前账号没有可用算力或额度不足。请在智子云确认套餐/余额后重试。' + return { + status: 'no-credit', + message: '智子云 token 有效,但远程 worker 分配失败:当前账号没有可用算力或额度不足。请下载智子官方 App,在 App 内充值/开通后回到 GoAgent 重新检测。' + } } if (/invalid_status|NoSuchKey|ssh\.json|colab/i.test(text)) { - return '智子云远程平台没有找到当前账号的可用 worker 配置。请确认该账号已开通对应远程算力。' + return { + status: 'worker-unavailable', + message: '智子云远程平台没有找到当前账号的可用 worker 配置。请确认该账号已开通对应远程算力。' + } } if (/token 已失效|401|invalid.*token|auth/i.test(text)) { - return '智子云 token 已失效,请退出后重新登录智子云。' + return { + status: 'token-expired', + message: '智子云 token 已失效,请退出后重新登录智子云。' + } } if (/websocket error|Socket 连接失败|Socket 已断开|transport error|xhr poll error|timeout|超时/i.test(text)) { - return `智子云 Socket 连接失败:${text.slice(0, 260)}` + return { + status: 'network-error', + message: `智子云 Socket 连接失败:${text.slice(0, 260)}` + } + } + return { + status: 'worker-unavailable', + message: `智子云连接检测失败:${text.slice(0, 320)}` } - return `智子云连接检测失败:${text.slice(0, 320)}` +} + +function humanizeZhiziConnectionError(error: unknown): string { + return classifyZhiziConnectionError(error).message } function attachTextEditingContextMenu(window: BrowserWindow): void { @@ -518,12 +537,15 @@ app.whenReady().then(() => { setSettings({ zhiziUsername: payload.phone.trim(), zhiziToken: result.token, - katagoEngineMode: 'zhizi' + katagoEngineMode: 'zhizi', + zhiziLastStatus: 'logged-in', + zhiziLastCheckedAt: new Date().toISOString() }) return { ok: true, message: `${result.message} GoAgent 可直接连接智子云。`, hasToken: true, + status: 'logged-in', dashboard: await dashboard() } }) @@ -539,12 +561,15 @@ app.whenReady().then(() => { setSettings({ zhiziUsername: payload.phone.trim(), zhiziToken: result.token, - katagoEngineMode: 'zhizi' + katagoEngineMode: 'zhizi', + zhiziLastStatus: 'logged-in', + zhiziLastCheckedAt: new Date().toISOString() }) return { ok: true, message: `${result.message} GoAgent 可直接连接智子云。`, hasToken: true, + status: 'logged-in', dashboard: await dashboard() } }) @@ -553,20 +578,44 @@ app.whenReady().then(() => { setSettings({ zhiziToken: '', katagoEngineMode: 'auto', - zhiziUseWhenLocalSlow: false + zhiziUseWhenLocalSlow: false, + zhiziLastStatus: 'logged-out', + zhiziLastCheckedAt: new Date().toISOString() }) return { ok: true, message: '已退出智子云登录,已清除本地 token,并切回自动分析模式。重新登录后会自动连接智子云。', hasToken: false, + status: 'logged-out', dashboard: await dashboard() } }) - ipcMain.handle('zhizi:test-connection', async (): Promise => { + ipcMain.handle('zhizi:me', async (): Promise => { + const settings = getSettings() + const result = await inspectZhiziCloudAccount(settings.zhiziToken) + setSettings({ + zhiziLastStatus: result.status, + zhiziLastCheckedAt: new Date().toISOString() + }) + return { + ...result, + dashboard: await dashboard() + } + }) + ipcMain.handle('zhizi:open-official-app-download', async () => { + await shell.openExternal(ZHIZI_OFFICIAL_APP_DOWNLOAD_URL) + return { ok: true, url: ZHIZI_OFFICIAL_APP_DOWNLOAD_URL } + }) + async function runZhiziConnectionTest(): Promise { const settings = getSettings() if (!settings.zhiziToken.trim()) { + setSettings({ + zhiziLastStatus: 'logged-out', + zhiziLastCheckedAt: new Date().toISOString() + }) return { ok: false, + status: 'logged-out', message: '智子云未登录:请先用账号密码或短信验证码登录。' } } @@ -599,13 +648,23 @@ app.whenReady().then(() => { const result = results.get('zhizi-smoke') const best = result?.moveInfos?.[0] if (!best) { + setSettings({ + zhiziLastStatus: 'worker-unavailable', + zhiziLastCheckedAt: new Date().toISOString() + }) return { ok: false, + status: 'worker-unavailable', message: '智子云已连接,但没有返回候选点。请稍后重试。' } } + setSettings({ + zhiziLastStatus: 'ready', + zhiziLastCheckedAt: new Date().toISOString() + }) return { ok: true, + status: 'ready', message: '智子云连接成功,远程 KataGo 已返回候选点。', candidateCount: result?.moveInfos?.length ?? 0, topMove: typeof best.move === 'string' ? best.move : undefined, @@ -614,12 +673,20 @@ app.whenReady().then(() => { scoreMean: typeof best.scoreMean === 'number' ? best.scoreMean : typeof best.scoreLead === 'number' ? best.scoreLead : undefined } } catch (error) { + const classified = classifyZhiziConnectionError(error) + setSettings({ + zhiziLastStatus: classified.status, + zhiziLastCheckedAt: new Date().toISOString() + }) return { ok: false, - message: humanizeZhiziConnectionError(error) + status: classified.status, + message: classified.message } } - }) + } + ipcMain.handle('zhizi:refresh-session', async (): Promise => runZhiziConnectionTest()) + ipcMain.handle('zhizi:test-connection', async (): Promise => runZhiziConnectionTest()) ipcMain.handle('tts:inspect-assets', async () => inspectTtsAssets()) ipcMain.handle('tts:list-voices', async () => listTtsVoices()) ipcMain.handle('tts:synthesize', async (_event, payload) => synthesizeTts(payload)) diff --git a/src/main/lib/store.ts b/src/main/lib/store.ts index ba56a2e..d72e63d 100644 --- a/src/main/lib/store.ts +++ b/src/main/lib/store.ts @@ -46,8 +46,15 @@ const defaults: AppSettings = { zhiziClientBin: '', zhiziUsername: '', zhiziToken: '', + zhiziGpuTier: 'standard', + zhiziPlatform: 'all', + zhiziGpuType: '', + zhiziKataName: 'katago-TENSORRT', + zhiziKataWeight: '28bnbt', zhiziExtraArgs: '', zhiziUseWhenLocalSlow: false, + zhiziLastStatus: 'logged-out', + zhiziLastCheckedAt: '', pythonBin: defaultPythonBin(), llmBaseUrl: 'https://api.openai.com/v1', llmApiKey: '', diff --git a/src/main/lib/types.ts b/src/main/lib/types.ts index 182656f..44d20a7 100644 --- a/src/main/lib/types.ts +++ b/src/main/lib/types.ts @@ -107,8 +107,15 @@ export interface AppSettings { zhiziClientBin: string zhiziUsername: string zhiziToken: string + zhiziGpuTier: ZhiziCloudGpuTier + zhiziPlatform: string + zhiziGpuType: string + zhiziKataName: string + zhiziKataWeight: string zhiziExtraArgs: string zhiziUseWhenLocalSlow: boolean + zhiziLastStatus: ZhiziCloudStatus + zhiziLastCheckedAt: string pythonBin: string llmBaseUrl: string llmApiKey: string @@ -157,6 +164,15 @@ export interface AppSettings { export type KataGoEngineMode = 'auto' | 'persistent' | 'spawn' | 'ikatago' | 'zhizi' export type KataGoAnalysisSpeedMode = 'auto' | 'fast' | 'balanced' | 'deep' export type KataGoModelPresetId = string +export type ZhiziCloudGpuTier = 'standard' | 'fast' | 'deep' +export type ZhiziCloudStatus = + | 'logged-out' + | 'logged-in' + | 'ready' + | 'no-credit' + | 'token-expired' + | 'network-error' + | 'worker-unavailable' export interface ZhiziCloudLoginRequest { phone: string @@ -181,11 +197,21 @@ export interface ZhiziCloudLoginResult { ok: boolean message: string hasToken: boolean + status?: ZhiziCloudStatus + dashboard?: DashboardData +} + +export interface ZhiziCloudAccountStatusResult { + ok: boolean + status: ZhiziCloudStatus + message: string + account?: Record dashboard?: DashboardData } export interface ZhiziCloudConnectionTestResult { ok: boolean + status?: ZhiziCloudStatus message: string candidateCount?: number topMove?: string diff --git a/src/main/services/zhiziCloudAuth.ts b/src/main/services/zhiziCloudAuth.ts index 4740d4a..671ae47 100644 --- a/src/main/services/zhiziCloudAuth.ts +++ b/src/main/services/zhiziCloudAuth.ts @@ -1,8 +1,11 @@ -import type { ZhiziCloudLoginCodeRequest, ZhiziCloudLoginRequest, ZhiziCloudSendCodeRequest } from '@main/lib/types' +import type { ZhiziCloudAccountStatusResult, ZhiziCloudLoginCodeRequest, ZhiziCloudLoginRequest, ZhiziCloudSendCodeRequest } from '@main/lib/types' const ZHIZI_LOGIN_URL = 'https://www.zhizigo.com/api/cluster/account/login' const ZHIZI_SEND_CODE_URL = 'https://www.zhizigo.com/api/cluster/account/send-code' const ZHIZI_FAST_LOGIN_URL = 'https://www.zhizigo.com/api/cluster/account/fast-login' +const ZHIZI_ME_URL = 'https://www.zhizigo.com/api/cluster/account/me' + +export const ZHIZI_OFFICIAL_APP_DOWNLOAD_URL = 'https://zhizigo.com/download' function findToken(value: unknown): string { if (!value || typeof value !== 'object') return '' @@ -74,6 +77,36 @@ async function postZhiziJson(url: string, body: Record, timeout } } +async function getZhiziJson(url: string, token: string, timeoutMs = 15_000): Promise<{ status: number; ok: boolean; json: unknown; rawText: string }> { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + try { + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token.trim()}` + }, + signal: controller.signal + }) + const rawText = await response.text() + let json: unknown = undefined + try { + json = rawText ? JSON.parse(rawText) : undefined + } catch { + json = undefined + } + return { status: response.status, ok: response.ok, json, rawText } + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error('智子云账号状态检测超时,请检查网络后重试。') + } + throw error + } finally { + clearTimeout(timer) + } +} + export async function loginZhiziCloudByPassword(request: ZhiziCloudLoginRequest): Promise<{ token: string; message: string }> { const phone = request.phone.trim() const password = request.password.trim() @@ -120,3 +153,34 @@ export async function loginZhiziCloudByCode(request: ZhiziCloudLoginCodeRequest) } return { token, message: '智子云验证码登录成功,已保存 token。' } } + +export async function inspectZhiziCloudAccount(token: string): Promise { + if (!token.trim()) { + return { + ok: false, + status: 'logged-out', + message: '智子云未登录:请先用账号密码或短信验证码登录。' + } + } + const response = await getZhiziJson(ZHIZI_ME_URL, token) + if (response.ok) { + return { + ok: true, + status: 'logged-in', + message: '智子云账号登录有效。下一步可以检测远程算力。', + account: response.json && typeof response.json === 'object' ? response.json as Record : undefined + } + } + if (response.status === 401 || response.status === 403) { + return { + ok: false, + status: 'token-expired', + message: '智子云 token 已失效,请退出后重新登录。' + } + } + return { + ok: false, + status: response.status >= 500 ? 'worker-unavailable' : 'network-error', + message: `智子云账号状态检测失败:HTTP ${response.status}${response.rawText ? ` · ${response.rawText.slice(0, 160)}` : ''}` + } +} diff --git a/src/main/services/zhiziGtpEngine.ts b/src/main/services/zhiziGtpEngine.ts index 53aa5c3..d1c6c10 100644 --- a/src/main/services/zhiziGtpEngine.ts +++ b/src/main/services/zhiziGtpEngine.ts @@ -1,6 +1,6 @@ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' import { io, type Socket } from 'socket.io-client' -import type { AppSettings, GameMove, KataGoAnalysisGroup } from '@main/lib/types' +import type { AppSettings, GameMove, KataGoAnalysisGroup, ZhiziCloudGpuTier } from '@main/lib/types' export type ZhiziGtpAnalysisResponse = Record & { id?: string @@ -113,12 +113,59 @@ function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'` } -function directSocketArgs(settings: AppSettings): string { - const args = ['--platform', 'all', '--client-id', 'zhizi-web'] - args.push(...splitCommandLine(settings.zhiziExtraArgs).filter((arg) => arg !== '--token')) +function zhiziGpuTypeForTier(tier: ZhiziCloudGpuTier): string { + if (tier === 'deep') return '6x' + if (tier === 'fast') return '3x' + return '1x' +} + +function hasOption(args: string[], option: string): boolean { + return args.some((arg) => arg === option || arg.startsWith(`${option}=`)) +} + +function withoutUnsafeSocketArgs(args: string[]): string[] { + const filtered: string[] = [] + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] + if (arg === '--token' || arg.startsWith('--token=')) { + if (arg === '--token') index += 1 + continue + } + if (arg === '--zz-socketio-token' || arg.startsWith('--zz-socketio-token=')) { + if (arg === '--zz-socketio-token') index += 1 + continue + } + filtered.push(arg) + } + return filtered +} + +export function buildZhiziSocketArgs(settings: AppSettings): string { + const extraArgs = withoutUnsafeSocketArgs(splitCommandLine(settings.zhiziExtraArgs)) + const args: string[] = [] + if (!hasOption(extraArgs, '--platform')) { + args.push('--platform', settings.zhiziPlatform.trim() || 'all') + } + if (!hasOption(extraArgs, '--engine-type')) { + args.push('--engine-type', 'go') + } + if (!hasOption(extraArgs, '--gpu-type')) { + args.push('--gpu-type', settings.zhiziGpuType.trim() || zhiziGpuTypeForTier(settings.zhiziGpuTier)) + } + if (!hasOption(extraArgs, '--kata-name')) { + args.push('--kata-name', settings.zhiziKataName.trim() || 'katago-TENSORRT') + } + if (!hasOption(extraArgs, '--kata-weight')) { + args.push('--kata-weight', settings.zhiziKataWeight.trim() || '28bnbt') + } + args.push(...extraArgs) return args.map(shellQuote).join(' ') } +function directSocketArgs(settings: AppSettings): string { + return buildZhiziSocketArgs(settings) +} + function chunkToText(chunk: unknown): string { if (typeof chunk === 'string') return chunk if (Buffer.isBuffer(chunk)) return chunk.toString('utf8') diff --git a/src/preload/index.ts b/src/preload/index.ts index 5629d83..4f9a76c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -44,6 +44,7 @@ import type { TtsSynthesisRequest, TtsSynthesisResult, TtsVoice, + ZhiziCloudAccountStatusResult, ZhiziCloudConnectionTestResult, ZhiziCloudLoginCodeRequest, ZhiziCloudLoginRequest, @@ -146,7 +147,10 @@ const api = { sendZhiziCloudLoginCode: (payload: ZhiziCloudSendCodeRequest): Promise => ipcRenderer.invoke('zhizi:send-code', payload), loginZhiziCloudCode: (payload: ZhiziCloudLoginCodeRequest): Promise => ipcRenderer.invoke('zhizi:login-code', payload), logoutZhiziCloud: (): Promise => ipcRenderer.invoke('zhizi:logout'), + inspectZhiziCloudAccount: (): Promise => ipcRenderer.invoke('zhizi:me'), testZhiziCloudConnection: (): Promise => ipcRenderer.invoke('zhizi:test-connection'), + refreshZhiziCloudSession: (): Promise => ipcRenderer.invoke('zhizi:refresh-session'), + openZhiziOfficialAppDownload: (): Promise<{ ok: boolean; url: string }> => ipcRenderer.invoke('zhizi:open-official-app-download'), inspectTtsAssets: (): Promise => ipcRenderer.invoke('tts:inspect-assets'), listTtsVoices: (): Promise => ipcRenderer.invoke('tts:list-voices'), synthesizeTts: (payload: TtsSynthesisRequest): Promise => ipcRenderer.invoke('tts:synthesize', payload), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7065f82..b06d668 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -103,8 +103,15 @@ const emptyDashboard: DashboardData = { zhiziClientBin: '', zhiziUsername: '', zhiziToken: '', + zhiziGpuTier: 'standard', + zhiziPlatform: 'all', + zhiziGpuType: '', + zhiziKataName: 'katago-TENSORRT', + zhiziKataWeight: '28bnbt', zhiziExtraArgs: '', zhiziUseWhenLocalSlow: false, + zhiziLastStatus: 'logged-out', + zhiziLastCheckedAt: '', pythonBin: 'python', llmBaseUrl: 'https://api.openai.com/v1', llmApiKey: '', @@ -1480,6 +1487,11 @@ export function App(): ReactElement { zhiziClientBin: String(formData.get('zhiziClientBin') ?? dashboard.settings.zhiziClientBin), zhiziUsername: String(formData.get('zhiziUsername') ?? dashboard.settings.zhiziUsername), zhiziToken: String(formData.get('zhiziToken') ?? ''), + zhiziGpuTier: String(formData.get('zhiziGpuTier') ?? dashboard.settings.zhiziGpuTier) as AppSettings['zhiziGpuTier'], + zhiziPlatform: String(formData.get('zhiziPlatform') ?? dashboard.settings.zhiziPlatform), + zhiziGpuType: String(formData.get('zhiziGpuType') ?? dashboard.settings.zhiziGpuType), + zhiziKataName: String(formData.get('zhiziKataName') ?? dashboard.settings.zhiziKataName), + zhiziKataWeight: String(formData.get('zhiziKataWeight') ?? dashboard.settings.zhiziKataWeight), zhiziExtraArgs: String(formData.get('zhiziExtraArgs') ?? dashboard.settings.zhiziExtraArgs), zhiziUseWhenLocalSlow: formData.get('zhiziUseWhenLocalSlow') === 'on', reviewLanguage: normalizeUiLocale(String(formData.get('reviewLanguage') ?? dashboard.settings.reviewLanguage)), @@ -4518,6 +4530,49 @@ function SettingsDrawer({ } } + async function inspectZhiziCloudAccount(): Promise { + setZhiziTestMessage('') + setZhiziTestBusy(true) + try { + const result = await window.goagent.inspectZhiziCloudAccount() + if (result.dashboard) { + onDashboardUpdated(result.dashboard) + } + setZhiziTestMessage(result.message) + } catch (cause) { + setZhiziTestMessage(`智子云账号检测失败:${String(cause).replace(/^Error:\s*/, '')}`) + } finally { + setZhiziTestBusy(false) + } + } + + async function refreshZhiziCloudSession(): Promise { + setZhiziTestMessage('') + setZhiziTestBusy(true) + try { + const result = await window.goagent.refreshZhiziCloudSession() + if (result.dashboard) { + onDashboardUpdated(result.dashboard) + } + const detail = result.ok && result.topMove + ? `首选 ${result.topMove}${typeof result.visits === 'number' ? ` · ${result.visits} visits` : ''}` + : '' + setZhiziTestMessage(`${result.message}${detail ? `(${detail})` : ''}`) + } catch (cause) { + setZhiziTestMessage(`智子云远程会话刷新失败:${String(cause).replace(/^Error:\s*/, '')}`) + } finally { + setZhiziTestBusy(false) + } + } + + async function openZhiziOfficialAppDownload(): Promise { + try { + await window.goagent.openZhiziOfficialAppDownload() + } catch (cause) { + setZhiziTestMessage(`无法打开智子官方 App 下载页:${String(cause).replace(/^Error:\s*/, '')}`) + } + } + async function saveTtsSettings(next: Partial): Promise { const updated = await window.goagent.updateSettings(next) onDashboardUpdated(updated) @@ -4530,8 +4585,22 @@ function SettingsDrawer({ const zhiziReady = /Zhizi Cloud Direct Ready|智子云直连/i.test(zhiziRawStatus) || (zhiziEnabled && dashboard.systemProfile.katagoReady && /智子|Zhizi/i.test(zhiziRawStatus)) const zhiziNeedsLogin = /Zhizi Cloud Login Required|智子云需登录|智子云未登录/i.test(zhiziRawStatus) - const zhiziStatusLabel = zhiziReady ? '已连接' : zhiziEnabled && zhiziNeedsLogin ? '需登录' : zhiziEnabled ? '连接中' : '未启用' - const zhiziStatusClassName = zhiziReady ? 'settings-status-chip is-ready' : zhiziNeedsLogin ? 'settings-status-chip is-warning' : 'settings-status-chip' + const zhiziLastStatus = dashboard.settings.zhiziLastStatus + const zhiziStatusLabel = + zhiziReady || zhiziLastStatus === 'ready' ? '算力可用' + : zhiziLastStatus === 'no-credit' ? '需开通' + : zhiziLastStatus === 'token-expired' || (zhiziEnabled && zhiziNeedsLogin) ? '需登录' + : zhiziLastStatus === 'network-error' ? '网络异常' + : zhiziLastStatus === 'worker-unavailable' ? '暂无 worker' + : zhiziLastStatus === 'logged-in' ? '已登录' + : zhiziEnabled ? '待检测' : '未启用' + const zhiziStatusClassName = + zhiziReady || zhiziLastStatus === 'ready' ? 'settings-status-chip is-ready' + : zhiziLastStatus === 'no-credit' || zhiziLastStatus === 'token-expired' || zhiziNeedsLogin ? 'settings-status-chip is-warning' + : 'settings-status-chip' + const zhiziLastCheckedText = dashboard.settings.zhiziLastCheckedAt + ? new Date(dashboard.settings.zhiziLastCheckedAt).toLocaleString() + : '尚未检测' return (
@@ -4721,12 +4790,12 @@ function SettingsDrawer({
默认本机,远程手动启用 -

本机分析不上传棋谱。需要智子云时,先登录账号,再点击启用直连;不用时点“回到本地分析”。

+

本机分析不上传棋谱。需要智子云时,先登录账号、检测连接,再点击启用直连;不用时点“回到本地分析”。

  1. 1登录账号
  2. -
  3. 2启用直连
  4. -
  5. 3开始分析
  6. +
  7. 2检测算力
  8. +
  9. 3启用分析
@@ -4757,6 +4826,36 @@ function SettingsDrawer({ + +
- {zhiziEnabled ? '当前已选择智子云。若分析提示需登录,请先完成下面的账号登录。' : '当前使用本机 KataGo,不会连接智子云。'} + + + + {zhiziEnabled ? `当前已选择智子云。上次检测:${zhiziLastCheckedText}` : '当前使用本机 KataGo,不会连接智子云。'}
+ {dashboard.settings.zhiziLastStatus === 'no-credit' ? ( +
+ 智子云账号没有可用算力或额度不足。请先下载智子官方 App,并在 App 内充值/开通远程算力;完成后回到这里点击“刷新远程会话”。 +
+ ) : null} {zhiziTestMessage ?
{zhiziTestMessage}
: null}

登录智子云

@@ -4879,6 +5006,44 @@ function SettingsDrawer({ 高级兼容选项

普通用户不需要填写这里。只有在账号登录不可用、或需要兼容旧智子连接器时才使用。

+
+ + + +