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
13 changes: 10 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ 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, KataGoBenchmarkCancelRequest, KataGoBenchmarkRequest, KataGoBenchmarkStartRequest, KataGoCancelAnalysisRequest, LibraryDeleteRequest, LlmModelsListRequest, LlmSettingsTestRequest, ReviewRequest, TeacherBoardImageRenderImage, TeacherBoardImageRenderRequest, TeacherBoardImageRenderResponse, TeacherChatMessage, TeacherRunCancelRequest, TeacherRunRequest, ZhiziCloudConnectionTestResult, ZhiziCloudLoginCodeRequest, ZhiziCloudLoginRequest, ZhiziCloudLoginResult, ZhiziCloudSendCodeRequest, ZhiziCloudSendCodeResult } from './lib/types'
import { importSgfFile, readGameRecord } from './services/sgf'
import { ensureFoxGameDownloaded, syncFoxGames } from './services/fox'
import { runReview } from './services/review'
import { applyDetectedDefaults, detectSystemProfile } from './services/systemProfile'
import { cancelTeacherRun, runTeacherTask } from './services/teacherAgent'
import { listLlmModels, testLlmSettings } from './services/llm'
import { analyzeTrialPositionWithProgress, cancelKataGoAnalysis } from './services/katago'
import { benchmarkKataGo } from './services/katagoBenchmark'
import { benchmarkKataGo, cancelKataGoBenchmark, startKataGoBenchmark } from './services/katagoBenchmark'
import { getKataGoEnginePoolStats } from './services/katagoEnginePool'
import { getAnalysisSchedulerStats, runScheduledAnalysis } from './services/analysis/scheduler'
import { analyzeGameQuickRuntime, analyzePositionRuntime, analyzePositionWithProgressRuntime } from './services/analysis/runtimeIntegration'
import { collectDiagnostics } from './services/diagnostics'
import { searchKnowledgeCards } from './services/knowledge/searchLocal'
import { inspectKataGoAssets, installOfficialKataGoModel } from './services/katago/katagoAssets'
import { cancelKataGoAssetInstall, inspectKataGoAssets, installOfficialKataGoModel } from './services/katago/katagoAssets'
import { bindFoxGamesToStudent, bindSgfGameToStudent, suggestStudentBindings } from './services/library/studentBinding'
import { deleteLibraryGame } from './services/library/deleteGame'
import { inspectReleaseReadiness } from './services/release/readiness'
Expand Down Expand Up @@ -381,6 +381,7 @@ app.whenReady().then(() => {
safeSendToRenderer(event, 'katago-assets:install-progress', progress)
})
)
ipcMain.handle('katago-assets:cancel-install', () => ({ cancelled: cancelKataGoAssetInstall() }))
ipcMain.handle('student:list', async () => listStudents())
ipcMain.handle('student:suggest-bindings', async (_event, payload) => suggestStudentBindings(payload))
ipcMain.handle('student:bind-sgf-game', async (_event, payload) => bindSgfGameToStudent(payload))
Expand Down Expand Up @@ -518,6 +519,12 @@ app.whenReady().then(() => {
ipcMain.handle('analysis-scheduler:stats', async () => getAnalysisSchedulerStats())
ipcMain.handle('katago:engine-pool-stats', async () => getKataGoEnginePoolStats())
ipcMain.handle('katago:benchmark', async (_event, payload: KataGoBenchmarkRequest | undefined) => benchmarkKataGo(payload ?? {}))
ipcMain.handle('katago:benchmark-start', (event, payload: KataGoBenchmarkStartRequest | undefined) =>
startKataGoBenchmark(payload ?? {}, (progress) => safeSendToRenderer(event, 'katago:benchmark-progress', progress))
)
ipcMain.handle('katago:benchmark-cancel', (_event, payload: KataGoBenchmarkCancelRequest | undefined) =>
cancelKataGoBenchmark(payload ?? {})
)
ipcMain.handle('teacher:run', async (event, payload: TeacherRunRequest) =>
runTeacherTask(payload, (progress) => {
safeSendToRenderer(event, 'teacher:run-progress', progress)
Expand Down
40 changes: 39 additions & 1 deletion src/main/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ function defaultPythonBin(): string {
return process.platform === 'win32' ? 'python' : 'python3'
}

function defaultReviewLanguage(): AppSettings['reviewLanguage'] {
let systemLocale = ''
try {
systemLocale = app.getLocale()
} catch {
// Electron may not expose app locale before ready on every platform.
}
const locale = (systemLocale || Intl.DateTimeFormat().resolvedOptions().locale).replaceAll('_', '-').toLowerCase()
const traditionalChinese =
(locale.startsWith('zh') || locale.startsWith('yue')) &&
(locale.startsWith('yue') || locale.includes('hant') || /-(tw|hk|mo)(?:-|\.|$)/.test(locale))
if (traditionalChinese) return 'zh-TW'
if (locale.startsWith('zh')) return 'zh-CN'
if (locale.startsWith('ja')) return 'ja-JP'
if (locale.startsWith('ko')) return 'ko-KR'
if (locale.startsWith('th')) return 'th-TH'
if (locale.startsWith('vi')) return 'vi-VN'
return 'en-US'
}

const defaults: AppSettings = {
katagoBin: '',
katagoConfig: '',
Expand All @@ -36,6 +56,9 @@ const defaults: AppSettings = {
katagoBenchmarkThreads: 0,
katagoBenchmarkVisitsPerSecond: 0,
katagoBenchmarkUpdatedAt: '',
katagoBenchmarkEngineFingerprint: '',
katagoBenchmarkLastCompletedAt: '',
katagoAutoBenchmarkEnabled: true,
katagoEngineMode: 'auto',
katagoAnalysisSpeedMode: 'auto',
localAnalysisDefaultApplied: false,
Expand All @@ -57,7 +80,10 @@ const defaults: AppSettings = {
llmBaseUrl: 'https://api.openai.com/v1',
llmApiKey: '',
llmModel: 'gpt-5-mini',
reviewLanguage: 'zh-CN',
onboardingVersion: 0,
llmSetupStatus: 'unconfigured',
llmLastVerifiedAt: '',
reviewLanguage: defaultReviewLanguage(),
defaultPlayerName: '',
ttsEnabled: true,
ttsAutoPlay: false,
Expand Down Expand Up @@ -347,6 +373,18 @@ export function setSettings(next: Partial<AppSettings>): AppSettings {
Object.prototype.hasOwnProperty.call(safeNext, 'ikatagoUseWhenLocalSlow') ||
Object.prototype.hasOwnProperty.call(safeNext, 'zhiziUseWhenLocalSlow')
settingsStore.set(shouldMarkLocalDefaultApplied ? { ...safeNext, localAnalysisDefaultApplied: true } : safeNext)
const llmConfigChanged =
Object.prototype.hasOwnProperty.call(next, 'llmBaseUrl') ||
Object.prototype.hasOwnProperty.call(next, 'llmApiKey') ||
Object.prototype.hasOwnProperty.call(next, 'llmModel')
if (llmConfigChanged && !Object.prototype.hasOwnProperty.call(next, 'llmSetupStatus')) {
const current = getSettings()
const configured = Boolean(current.llmBaseUrl.trim() && current.llmApiKey.trim() && current.llmModel.trim())
settingsStore.set({
llmSetupStatus: configured ? 'needs-attention' : 'unconfigured',
llmLastVerifiedAt: ''
})
}
return getSettings()
}

Expand Down
56 changes: 55 additions & 1 deletion src/main/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export interface VisionEvidenceReport {
createdAt: string
}

export type LlmSetupStatus = 'unconfigured' | 'verified' | 'skipped' | 'needs-attention'

export interface AppSettings {
katagoBin: string
katagoConfig: string
Expand All @@ -93,6 +95,9 @@ export interface AppSettings {
katagoBenchmarkThreads: number
katagoBenchmarkVisitsPerSecond: number
katagoBenchmarkUpdatedAt: string
katagoBenchmarkEngineFingerprint: string
katagoBenchmarkLastCompletedAt: string
katagoAutoBenchmarkEnabled: boolean
katagoEngineMode: KataGoEngineMode
katagoAnalysisSpeedMode: KataGoAnalysisSpeedMode
localAnalysisDefaultApplied: boolean
Expand All @@ -114,6 +119,9 @@ export interface AppSettings {
llmBaseUrl: string
llmApiKey: string
llmModel: string
onboardingVersion: number
llmSetupStatus: LlmSetupStatus
llmLastVerifiedAt: string
reviewLanguage: 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP' | 'ko-KR' | 'th-TH' | 'vi-VN'
defaultPlayerName: string
ttsEnabled: boolean
Expand Down Expand Up @@ -370,6 +378,36 @@ export interface KataGoBenchmarkRequest {
numPositions?: number
secondsPerMove?: number
threads?: number[]
timeoutMs?: number
}

export type KataGoBenchmarkTaskStatus = 'running' | 'completed' | 'cancelled' | 'timed-out' | 'failed' | 'skipped'

export interface KataGoBenchmarkStartRequest extends KataGoBenchmarkRequest {
automatic?: boolean
onlyIfNeeded?: boolean
}

export interface KataGoBenchmarkStartResult {
runId: string
status: 'running' | 'skipped'
message: string
}

export interface KataGoBenchmarkCancelRequest {
runId?: string
}

export interface KataGoBenchmarkCancelResult {
cancelled: boolean
runId?: string
}

export interface KataGoBenchmarkProgress {
runId: string
status: KataGoBenchmarkTaskStatus
message: string
result?: KataGoBenchmarkResult
}

export interface KataGoBenchmarkThreadResult {
Expand All @@ -388,13 +426,14 @@ export interface KataGoBenchmarkResult {
command: string
outputTail: string
updatedAt: string
engineFingerprint: string
}

export interface KataGoAssetInstallRequest {
presetId?: KataGoModelPresetId
}

export type KataGoAssetInstallStage = 'discovering' | 'downloading-binary' | 'downloading-model' | 'copying-binary' | 'writing-manifest' | 'done' | 'error'
export type KataGoAssetInstallStage = 'discovering' | 'downloading-binary' | 'downloading-model' | 'copying-binary' | 'writing-manifest' | 'paused' | 'done' | 'error'

export interface KataGoAssetInstallProgress {
stage: KataGoAssetInstallStage
Expand All @@ -414,6 +453,10 @@ export interface KataGoAssetInstallResult {
detail: string
}

export interface KataGoAssetInstallCancelResult {
cancelled: boolean
}

export type ReleaseReadinessStatus = 'pass' | 'warn' | 'fail' | 'unknown'

export interface ReleaseReadinessItem {
Expand Down Expand Up @@ -1227,6 +1270,17 @@ export interface LlmSettingsTestRequest {
export interface LlmSettingsTestResult {
ok: boolean
message: string
capabilities: {
text: LlmCapabilityCheck
vision: LlmCapabilityCheck
tools: LlmCapabilityCheck
}
}

export interface LlmCapabilityCheck {
ok: boolean
message: string
technicalDetail?: string
}

export interface LlmModelsListRequest {
Expand Down
2 changes: 2 additions & 0 deletions src/main/services/analysis/scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { randomUUID } from 'node:crypto'
import type { KataGoAnalysisGroup } from '@main/lib/types'
import { cancelKataGoAnalysis } from '../katago'
import { cancelKataGoBenchmark } from '../katagoBenchmark'

export type ScheduledAnalysisPriority = 'live' | 'teacher' | 'quick' | 'background'

Expand Down Expand Up @@ -133,6 +134,7 @@ function pump(): void {
}

export function runScheduledAnalysis<T>(input: ScheduledAnalysisInput, task: () => Promise<T>): Promise<T> {
cancelKataGoBenchmark()
const id = input.runId || randomUUID()
const priority = input.priority ?? priorityForGroup(input.group)
if (input.replaceGroup && input.group) {
Expand Down
22 changes: 8 additions & 14 deletions src/main/services/diagnostics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { appHome, getSettings, hasLlmApiKey } from '@main/lib/store'
import { resolveKataGoRuntime } from '../katagoRuntime'
import { ikatagoClientConfigured, shouldPreferIKataGoEngine } from '../ikatagoClientEngine'
import { shouldPreferZhiziGtpEngine, zhiziGtpConfigured } from '../zhiziGtpEngine'
import { probeOpenAICompatibleProvider } from '../llm/openaiCompatibleProvider'
import { inspectKataGoAssets } from '../katago/katagoAssets'
import { isLitePackagedRuntime } from '../release/packageRuntime'
import type { DiagnosticCheck, DiagnosticsReport, DiagnosticsOverall } from './types'
Expand Down Expand Up @@ -229,26 +228,21 @@ async function checkLlmProxy(): Promise<DiagnosticCheck> {
if (!configured) {
return {
id: 'llm-proxy',
title: 'Claude 兼容代理',
title: 'AI 老师',
status: 'warn',
required: false,
detail: '还没有配置 Claude 兼容代理。KataGo 基础分析可用,但老师讲解不可用。',
action: '在设置中填写 Base URL、API Key 和 Claude 模型名。'
detail: '还没有连接 AI 模型。KataGo 分析仍然可以正常使用。',
action: '在“设置 > AI 模型”中填写服务地址、访问密钥和模型。'
}
}
const result = await probeOpenAICompatibleProvider({
llmBaseUrl: settings.llmBaseUrl,
llmApiKey: settings.llmApiKey,
llmModel: settings.llmModel
})
const verified = settings.llmSetupStatus === 'verified'
return {
id: 'llm-proxy',
title: 'Claude 兼容代理',
status: result.ok ? 'pass' : 'warn',
title: 'AI 老师',
status: verified ? 'pass' : 'warn',
required: false,
detail: result.message,
action: result.ok ? undefined : '请检查代理是否启动、API Key 是否正确、模型是否支持图片输入。',
technicalDetail: result.technicalDetail
detail: verified ? '文字、棋盘图片和 Agent 工具调用已通过验证。' : '配置已保存,但还没有完成能力验证。',
action: verified ? undefined : '打开“设置 > AI 模型”,运行连接验证。'
}
}

Expand Down
Loading
Loading