Skip to content
Merged
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
24 changes: 17 additions & 7 deletions docs/ZHIZI_CLOUD_ENGINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ GoAgent -> 智子云登录 -> Socket.IO 远程 KataGo -> GTP kata-analyze

在 GoAgent 设置页打开“分析引擎”:

1. 引擎模式选择 `智子云远程算力:直连`。
2. 算力类型默认选择 `VIP 共享引擎`,GoAgent 会提交 `--gpu-type vip-share`。这是智子管理员确认的 VIP 包月引擎,不会额外按时间扣独享费用。
1. 算力类型默认选择 `VIP 共享引擎`,GoAgent 会提交 `--gpu-type vip-share`。这是智子管理员确认的 VIP 包月引擎,不会额外按时间扣独享费用。
- `独享 1x / 3x / 6x` 对应 `--gpu-type 1x / 3x / 6x`,属于按算力和时间计费的独享 worker。
- 如果你只是购买了 VIP,请不要选择独享档位。
3. 填写智子云主账号手机号或邮箱和密码,点击“登录并连接智子云”。
2. 填写智子云主账号手机号或邮箱和密码,点击“登录智子云”。
- GoAgent 会调用智子云登录接口获取 token。
- 登录成功后,GoAgent 会把 token 保存到本地加密存储。
- 登录成功后,引擎模式会自动切到 `智子云远程算力:直连`
- 登录本身不会中断当前本机分析,也不会立即上传局面
- 后续只需要启动 GoAgent,不需要先启动其它应用。
- `zz-` 开头的是智子连接账号,不是主账号登录 token。它需要先在智子官方账号体系里绑定,不能直接填在 GoAgent 的登录框里换取远程算力 token。
3. 点击“检测并启用”。GoAgent 会真实启动远程 KataGo,并等待候选点、胜率、目差和实时搜索速度返回。只有检测通过后才切换到智子云;检测失败时仍保持本机分析。
4. 如果密码登录提示密码不正确,可以点击“发送验证码”,收到短信后用“验证码登录”。
- 这和智子 Web 端的 `send-code` / `fast-login` 流程一致。
- 验证码只用于本次登录;GoAgent 不会长期保存验证码。
Expand All @@ -37,6 +37,15 @@ GoAgent -> 智子云登录 -> Socket.IO 远程 KataGo -> GTP kata-analyze

`自动` 模式现在是本机优先的默认模式,不会因为本机分析失败就自动上传局面到智子云。需要远程算力时,请手动启用 `智子云远程算力:直连`。

## 连接与分析机制

- 检测通过后,GoAgent 会复用同一个远程 Socket.IO / GTP 会话,避免每次切换手数都重新排队启动 worker。
- Socket 断线时会自动重连;局面同步或分析过程中断线,会重新建立会话并重新同步当前局面,旧结果不会覆盖新局面。
- 分析按真实 `visits` 达到目标后停止,不再用固定等待时间猜测分析是否完成。
- `stdout` / `stderr` 的字符串、Buffer、ArrayBuffer 和 Socket.IO Buffer JSON 都会按 UTF-8 解码。
- VIP 共享会话空闲 5 分钟后释放;独享 1x / 3x / 6x 空闲 90 秒后释放,减少无效占用。
- 切回本机分析或退出登录时,会立即停止任务并释放远程会话。

## 退出和重新登录

如果刚购买套餐、充值或修改了智子云账号状态,但 GoAgent 仍提示额度不足或远程算力不可用,可以在设置页点击 `退出智子云登录`。GoAgent 会:
Expand All @@ -45,7 +54,7 @@ GoAgent -> 智子云登录 -> Socket.IO 远程 KataGo -> GTP kata-analyze
2. 清除本地保存的智子云 token。
3. 自动切回 `自动` 分析模式,并关闭“本机慢时自动使用智子云”,避免旧 token 或旧连接器路径继续重试。

然后使用账号密码或短信验证码重新登录。重新登录成功后,GoAgent 会自动切回 `智子云远程算力:直连`
然后使用账号密码或短信验证码重新登录,再点击“检测并启用”

## 和 iKataGo 的区别

Expand All @@ -72,8 +81,9 @@ GoAgent 不会把智子 token 写入普通设置文件,也不会在日志中
- `这是智子云连接账号,不是可直接登录的主账号`:请改用智子云主账号手机号或邮箱登录。`zz-` 连接账号不能直接换取 GoAgent 远程算力 token。
- `密码不正确`:智子官方密码接口拒绝了这组密码。可以改用“发送验证码”再“验证码登录”。
- `验证码不正确或已过期`:重新发送验证码后再登录。
- `智子云远程 worker 返回额度不足或不可分配`:如果你是 VIP 用户,请先确认算力类型选择的是 `VIP 共享引擎(--gpu-type vip-share)`。如果选择 `1x / 3x / 6x`,智子云会按独享 worker 计费,并可能因为独享余额不足返回 `not_enough_credit`。如果刚充值或刚购买 VIP,点击 `退出智子云登录` 后重新登录,让 GoAgent 刷新 token 和远程会话。
- 如果检测连接显示 `主账号 token 有效`、`会员有效`、`连接账号已绑定`,但远端仍返回 `not_enough_credit`,说明 GoAgent 已完成官方公开流程:`fetch-socketio-token` 成功、Socket.IO 收到 `ready`,但智子 worker 在执行 GTP 初始化时拒绝分配算力。此时请把这段状态反馈给智子官方,让官方检查连接账号的远程 worker/GPU 权益是否已经实际生效。
- `当前没有空闲算力`:GoAgent 会按有限次数自动重试。这通常是 worker 暂时繁忙,不等于账号余额不足。
- VIP 共享返回 `not_enough_credit`:GoAgent 会明确提示“VIP 权益/连接账号未同步”,不会把它误报成独享余额不足。先退出并重新登录;持续失败时请让智子官方检查 VIP 与连接账号的 worker 权益。
- 独享 1x / 3x / 6x 返回 `not_enough_credit`:这是按量档位的余额或档位问题,请在智子官方 App 检查。
- 官方 Postman 中的 `connectAccount/login` 只用于验证连接账号凭据,不返回 GoAgent 直连所需的 token,也不会替代主账号登录流程。
- `智子云 KataGo 启动超时`:检查 token 是否有效,或在 GoAgent 中重新登录。
- `智子云 GTP 命令失败`:可能是远端引擎尚未 ready、账号状态异常,或智子云当前连接不可用。
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"check:artifacts": "node scripts/package_artifact_smoke.mjs --mode=dev",
"smoke:teacher-llm": "pnpm build && node scripts/teacher_llm_smoke.mjs",
"smoke:teacher-llm:real": "pnpm build && node scripts/teacher_llm_real_smoke.mjs",
"smoke:zhizi-remote": "node scripts/smoke_zhizi_remote.mjs",
"rc:check": "node scripts/p0_release_candidate_check.mjs --mode=dev",
"rc:artifacts": "node scripts/verify_release_artifacts.mjs --mode=dev",
"rc:evidence": "node scripts/collect_release_evidence.mjs --mode=dev"
Expand Down
158 changes: 158 additions & 0 deletions scripts/smoke_zhizi_remote.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env node
import { createDecipheriv, scryptSync } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import os from 'node:os'
import { join } from 'node:path'

import {
buildZhiziRemoteArgs,
parseKataAnalyzeInfo,
zhiziAnalysisReachedVisits
} from '../src/main/services/zhiziGtpProtocol.ts'
import { ZhiziPersistentSession } from '../src/main/services/zhiziSocketSession.ts'

const strict = process.env.GOAGENT_ZHIZI_REAL === '1'
const appHome = process.env.GOAGENT_APP_HOME || join(os.homedir(), '.goagent')
const settingsPath = join(appHome, 'settings.json')
const secretsPath = join(appHome, 'secrets.json')
const keyPath = join(appHome, 'secrets.key')

function skipped(reason) {
console.log(JSON.stringify({ status: 'skipped', reason }, null, 2))
if (strict) process.exitCode = 1
}

function decryptLocalSecret(secret) {
if (!secret || secret.mode !== 'local-v1') return ''
const seed = readFileSync(keyPath, 'utf8').trim()
const key = scryptSync(seed, 'goagent-local-secret-store-v1', 32)
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(secret.iv, 'base64'))
decipher.setAuthTag(Buffer.from(secret.tag, 'base64'))
return Buffer.concat([
decipher.update(Buffer.from(secret.value, 'base64')),
decipher.final()
]).toString('utf8')
}

function responseCount(text) {
return (text.match(/\r?\n\r?\n/g) ?? []).length
}

async function sendSetup(session, signal, commands) {
const start = session.output().stdout.length
const disconnectVersion = session.captureDisconnectVersion()
session.send(`${commands.join('\n')}\n`)
await session.waitUntil(
() => responseCount(session.output().stdout.slice(start)) >= commands.length,
15_000,
'Zhizi smoke setup',
signal,
disconnectVersion
)
const output = session.output().stdout.slice(start)
if (/(?:^|\n)\?/.test(output.trim())) {
throw new Error(`GTP setup rejected: ${output.trim().slice(0, 300)}`)
}
}

async function analyze(session, signal, id, moves) {
session.clearOutput()
await sendSetup(session, signal, [
'boardsize 19',
'kata-set-rules chinese',
'komi 7.5',
'clear_board',
...moves.map(([color, point]) => `play ${color} ${point}`)
])
const start = session.output().stdout.length
const disconnectVersion = session.captureDisconnectVersion()
const player = moves.at(-1)?.[0] === 'B' ? 'W' : 'B'
session.send(`kata-analyze ${player} 25 rootInfo true maxmoves 8\n`)
let response
const startedAt = Date.now()
await session.waitUntil(
() => {
const output = session.output().stdout.slice(start)
if (!/\binfo\s+/.test(output)) return false
const parsed = parseKataAnalyzeInfo(output, player, id)
if (!parsed.moveInfos?.length) return false
response = parsed
return zhiziAnalysisReachedVisits(parsed, 64)
},
30_000,
'Zhizi smoke analysis',
signal,
disconnectVersion
)
session.send('stop\n')
await session.waitUntil(
() => responseCount(session.output().stdout.slice(start)) >= 1,
10_000,
'Zhizi smoke stop',
signal,
disconnectVersion
)
return {
id,
elapsedMillis: Date.now() - startedAt,
topMove: response.moveInfos[0].move,
visits: response.moveInfos.reduce((sum, move) => sum + Number(move.visits ?? 0), 0),
winrate: response.rootInfo?.winrate,
scoreLead: response.rootInfo?.scoreLead
}
}

if (!strict) {
skipped('Set GOAGENT_ZHIZI_REAL=1 to run the real Zhizi cloud smoke test.')
} else if (![settingsPath, secretsPath, keyPath].every(existsSync)) {
skipped('GoAgent local settings or secret files are missing.')
} else {
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'))
const secrets = JSON.parse(readFileSync(secretsPath, 'utf8'))
const accountToken = decryptLocalSecret(secrets.zhiziToken)
if (!accountToken) {
skipped('No locally saved Zhizi token is available.')
} else {
const gpuType = String(settings.zhiziGpuType || 'vip-share')
const args = buildZhiziRemoteArgs({
zhiziGpuType: gpuType,
zhiziExtraArgs: String(settings.zhiziExtraArgs || '')
})
const session = new ZhiziPersistentSession({ accountToken, args, gpuType })
const controller = new AbortController()
try {
const results = await session.runExclusive(controller.signal, async (channel) => [
await analyze(channel, controller.signal, 'smoke-1', [
['B', 'D4'],
['W', 'Q16'],
['B', 'Q4']
]),
await analyze(channel, controller.signal, 'smoke-2', [
['B', 'D4'],
['W', 'Q16'],
['B', 'Q4'],
['W', 'D16']
])
])
const telemetry = session.telemetry()
console.log(JSON.stringify({
status: 'passed',
gpuType,
connectionCount: telemetry.connectionCount,
sessionReadyMillis: telemetry.lastReadyMillis,
results
}, null, 2))
if (telemetry.connectionCount !== 1) {
throw new Error(`Expected one persistent connection, received ${telemetry.connectionCount}.`)
}
} catch (error) {
console.error(JSON.stringify({
status: 'failed',
error: error instanceof Error ? error.message : String(error)
}, null, 2))
process.exitCode = 1
} finally {
session.close()
}
}
}
Loading
Loading