From aa5c632ea9f5d393b887be4e301328e2e4cb06cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=82=E5=B4=8E=20=E3=82=AE=E3=83=A3=E3=83=A9=E3=82=AF?= =?UTF-8?q?=E3=82=B7=E3=83=BC?= Date: Thu, 8 Oct 2026 23:13:01 +0800 Subject: [PATCH] Merge pull request #711 from Tokisaki-Galaxy/feat/llm-model-fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: AI 辅助新增获取模型、测试连接与 Custom 提供商 --- engine/contracts/webview/messages.json | 56 +++++++ engine/contracts/webview/messages.ts | 3 + engine/contracts/webview/schema.h | 2 +- engine/contracts/webview/schema.js | 2 +- installer/default_config/config.default.toml | 3 + server/assets/config/config.toml | 3 + server/src/config/ime_config.cpp | 17 ++- server/src/config/ime_config.h | 10 +- server/src/settings/api_credential_test.cpp | 115 +++++++++++++-- server/src/settings/api_credential_test.h | 9 ++ server/src/settings/settings_app.cpp | 39 +++++ .../tests/src/test_config_non_ascii_path.cpp | 45 ++++++ .../src/modules/ai-settings.test.ts | 29 ++++ .../ime-settings/src/modules/ai-settings.ts | 44 ++++-- .../src/modules/model-fetch.test.ts | 139 ++++++++++++++++++ .../ime-settings/src/modules/model-fetch.ts | 97 ++++++++++++ .../ime-settings/src/modules/shared.ts | 4 + .../src/partials/ai-settings.html | 27 +++- .../src/styles/modules/ai-settings.css | 88 +++++++++++ ui-html/webview2/shared/messages.ts | 3 + ui-html/webview2/shared/schema.js | 2 +- 21 files changed, 699 insertions(+), 38 deletions(-) create mode 100644 ui-html/webview2/settings/ime-settings/src/modules/model-fetch.test.ts create mode 100644 ui-html/webview2/settings/ime-settings/src/modules/model-fetch.ts diff --git a/engine/contracts/webview/messages.json b/engine/contracts/webview/messages.json index c3ba2cc1..24c344da 100644 --- a/engine/contracts/webview/messages.json +++ b/engine/contracts/webview/messages.json @@ -274,6 +274,37 @@ "additionalProperties": false } }, + "apiModelList": { + "surfaces": [ + "settings" + ], + "data": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + }, + "service": { + "type": "string", + "enum": [ + "ai.assistant" + ] + }, + "config": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": true + } + }, + "required": [ + "requestId", + "service", + "config" + ], + "additionalProperties": false + } + }, "dictionaryRequest": { "surfaces": [ "settings" @@ -823,6 +854,31 @@ ], "additionalProperties": false }, + "apiModelListResult": { + "properties": { + "requestId": { + "type": "string" + }, + "ok": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "models": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "requestId", + "ok", + "message" + ], + "additionalProperties": false + }, "dictionaryResponse": { "properties": { "requestId": { diff --git a/engine/contracts/webview/messages.ts b/engine/contracts/webview/messages.ts index 38e1cd11..e645370e 100644 --- a/engine/contracts/webview/messages.ts +++ b/engine/contracts/webview/messages.ts @@ -23,6 +23,7 @@ export type ClientMessage = | { "type": "maximizeButtonRect"; "protocolVersion"?: 1; "data": { "x": number; "y": number; "width": number; "height": number; "dpr": number } } | { "type": "configUpdate"; "protocolVersion"?: 1; "data": { "path": string; "value": string | number | boolean } } | { "type": "apiCredentialTest"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "translation.tencent" | "translation.niutrans" | "translation.custom" | "voice.asr" | "voice.polish" | "ai.assistant"; "config": { [key: string]: unknown } } } + | { "type": "apiModelList"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "ai.assistant"; "config": { [key: string]: unknown } } } | { "type": "dictionaryRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "dictionary": "quanpin" | "wubi" | "english" | "quick"; "action": "query" | "create" | "update" | "delete" | "import" | "importHans" | "export"; "word"?: string; "code"?: string; "content"?: string; "weight"?: number; "oldWord"?: string; "oldCode"?: string; "display"?: string; "oldDisplay"?: string; "offset"?: number; "limit"?: number } } | { "type": "statsRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "action": "overview" | "clearAll" | "openDirectory" } } | { "type": "openKeyboardPanel"; "protocolVersion"?: 1 } @@ -62,6 +63,7 @@ export type ServerMessage = | { "type": "windowState"; "protocolVersion"?: 1; "data": { "isMaximized": boolean } } | { "type": "maxButtonEvent"; "protocolVersion"?: 1; "data": { "event": "enter" | "leave" | "down" | "up" | "click" } } | { "type": "apiCredentialTestResult"; "protocolVersion"?: 1; "requestId": string; "ok": boolean; "message": string } + | { "type": "apiModelListResult"; "protocolVersion"?: 1; "requestId": string; "ok": boolean; "message": string; "models"?: Array } | { "type": "dictionaryResponse"; "protocolVersion"?: 1; "requestId": string; "action"?: string; "dictionary"?: string; "ok": boolean; "message": string; "rows": Array<{ "word": string; "code"?: string; "display"?: string; "weight"?: number }>; "content"?: string; "filename"?: string; "offset"?: number; "hasMore"?: boolean; [key: string]: unknown } | { "type": "statsResponse"; "protocolVersion"?: 1; "requestId": string; "action": string; "ok": boolean; "message": string; "overview"?: { "hasData": boolean; "firstDayKey": number; "lastDayKey": number; "days": number; "totalChars": number; "totalActiveMs": number; "todayDayKey": number; "todayChars": number; "todayActiveMs": number; "averagePerDay": number; "currentStreak": number; "longestStreak": number; "bestDayKey": number; "bestDayChars": number; "todaySpeed": number; "averageSpeed": number; "fastestSpeed": number; "fastestDayKey": number; "categories": { "cjk": number; "latin": number; "digit": number; "punct": number; "other": number }; "todayHourly": Array; "daily": Array<{ "dayKey": number; "cjk": number; "latin": number; "digit": number; "punct": number; "other": number; "activeMs": number }> }; "removedDays"?: number; [key: string]: unknown }; export type SettingsMessage = @@ -86,6 +88,7 @@ export type SettingsMessage = | { "type": "maximizeButtonRect"; "protocolVersion"?: 1; "data": { "x": number; "y": number; "width": number; "height": number; "dpr": number } } | { "type": "configUpdate"; "protocolVersion"?: 1; "data": { "path": string; "value": string | number | boolean } } | { "type": "apiCredentialTest"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "translation.tencent" | "translation.niutrans" | "translation.custom" | "voice.asr" | "voice.polish" | "ai.assistant"; "config": { [key: string]: unknown } } } + | { "type": "apiModelList"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "ai.assistant"; "config": { [key: string]: unknown } } } | { "type": "dictionaryRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "dictionary": "quanpin" | "wubi" | "english" | "quick"; "action": "query" | "create" | "update" | "delete" | "import" | "importHans" | "export"; "word"?: string; "code"?: string; "content"?: string; "weight"?: number; "oldWord"?: string; "oldCode"?: string; "display"?: string; "oldDisplay"?: string; "offset"?: number; "limit"?: number } } | { "type": "statsRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "action": "overview" | "clearAll" | "openDirectory" } } | { "type": "openKeyboardPanel"; "protocolVersion"?: 1 }; diff --git a/engine/contracts/webview/schema.h b/engine/contracts/webview/schema.h index 99458266..2690cb4a 100644 --- a/engine/contracts/webview/schema.h +++ b/engine/contracts/webview/schema.h @@ -2,5 +2,5 @@ // Generated by generate.py; edit messages.json. namespace metasequoia::webview { inline constexpr int Version = 1; -inline constexpr char SchemaJson[] = R"msime({"version":1,"client":{"configRequest":{"surfaces":["settings"]},"skinCatalogRequest":{"surfaces":["settings"]},"openSkinDirectory":{"surfaces":["settings"]},"openHelpcodeDirectory":{"surfaces":["settings"]},"openShuangpinDirectory":{"surfaces":["settings"]},"collocationModelDownload":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelDelete":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelImport":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelStatusRequest":{"surfaces":["settings"]},"openHandwritingPanel":{"surfaces":["settings"]},"restartServer":{"surfaces":["settings"]},"copyText":{"surfaces":["settings"],"data":{"type":"string"}},"openExternalUrl":{"surfaces":["settings"],"data":{"type":"string"}},"dragStart":{"surfaces":["settings","toolbar"]},"focus":{"surfaces":["settings"]},"resizeStart":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"resizeHitTest":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"windowControl":{"surfaces":["settings"],"data":{"type":"string","enum":["minimize","maximize","restore","close"]}},"maximizeButtonRect":{"surfaces":["settings"],"data":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0},"dpr":{"type":"number","minimum":0.01}},"required":["x","y","width","height","dpr"],"additionalProperties":false}},"configUpdate":{"surfaces":["settings"],"data":{"type":"object","properties":{"path":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["path","value"],"additionalProperties":false}},"apiCredentialTest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["translation.tencent","translation.niutrans","translation.custom","voice.asr","voice.polish","ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"dictionaryRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"dictionary":{"type":"string","enum":["quanpin","wubi","english","quick"]},"action":{"type":"string","enum":["query","create","update","delete","import","importHans","export"]},"word":{"type":"string"},"code":{"type":"string"},"content":{"type":"string"},"weight":{"type":"integer"},"oldWord":{"type":"string"},"oldCode":{"type":"string"},"display":{"type":"string"},"oldDisplay":{"type":"string"},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["requestId","dictionary","action"],"additionalProperties":false}},"statsRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"action":{"type":"string","enum":["overview","clearAll","openDirectory"]}},"required":["requestId","action"],"additionalProperties":false}},"openKeyboardPanel":{"surfaces":["settings","toolbar"]},"candidate":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"delete":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"pin":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"clearPosition":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"candidateWheel":{"surfaces":["candidate"],"data":{"type":"integer","minimum":-32768,"maximum":32767}},"candidatePage":{"surfaces":["candidate"],"data":{"type":"string","enum":["previous","next"]}},"fixPosition":{"surfaces":["candidate"],"data":{"type":"object","properties":{"index":{"type":"integer","minimum":1,"maximum":10},"position":{"type":"integer","minimum":1,"maximum":5}},"required":["index","position"],"additionalProperties":false}},"contextMenuResize":{"surfaces":["candidate"],"data":{"type":"object","properties":{"width":{"type":"integer","minimum":0,"maximum":65535},"height":{"type":"integer","minimum":0,"maximum":65535},"topExpansion":{"type":"integer","minimum":0,"maximum":65535},"leftExpansion":{"type":"integer","minimum":0,"maximum":65535}},"required":["width","height"],"additionalProperties":false}},"contextMenuClosed":{"surfaces":["candidate"]},"candidatePointerArmed":{"surfaces":["candidate"]},"candidatePointerMotion":{"surfaces":["candidate"]},"candidateFrameProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"revision":{"type":"integer"},"stage":{"type":"integer"},"performanceMs":{"type":"number","minimum":0}},"required":["revision","stage","performanceMs"],"additionalProperties":false}},"candidatePointerProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"probe":{"type":"integer"},"screenX":{"type":"integer"},"screenY":{"type":"integer"},"clientX":{"type":"integer"},"clientY":{"type":"integer"},"movementX":{"type":"integer"},"movementY":{"type":"integer"},"armed":{"type":"boolean"}},"required":["probe","screenX","screenY","clientX","clientY","movementX","movementY","armed"],"additionalProperties":false}},"contentTruncated":{"surfaces":["candidate","toolbar","menu"],"data":{"type":"object","properties":{"surface":{"type":"string","enum":["candidate","toolbar","menu","ftb","candwnd","cand"]},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}},"required":["surface","width","height"],"additionalProperties":true}},"floatingToggle":{"surfaces":["menu"],"data":{"type":"boolean"}},"settings":{"surfaces":["menu"]},"about":{"surfaces":["menu"]},"emojiSymbols":{"surfaces":["menu"]},"keyboardPanel":{"surfaces":["menu"]},"handwritingPanel":{"surfaces":["menu"]},"voiceInput":{"surfaces":["menu"]},"changeInputScheme":{"surfaces":["menu"],"data":{"type":"string","enum":["quanpin","shuangpin","wubi"]}},"inputSchemeSubmenu":{"surfaces":["menu"],"data":{"type":"object","properties":{"open":{"type":"boolean"},"width":{"type":"number","minimum":0}},"required":["open","width"],"additionalProperties":false}},"ready":{"surfaces":["toolbar"]},"exitEnglishInputMode":{"surfaces":["toolbar"]},"changeCharacterSet":{"surfaces":["toolbar"]},"openSettings":{"surfaces":["toolbar"]},"openEmojiPanel":{"surfaces":["toolbar"]},"changeIMEMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["cn","en"]}},"changeCharMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["fullwidth","halfwidth"]}},"changePuncMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["puncEn","puncCn"]}}},"server":{"configSnapshot":{"data":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"windowState":{"data":{"type":"object","properties":{"isMaximized":{"type":"boolean"}},"required":["isMaximized"],"additionalProperties":false}},"maxButtonEvent":{"data":{"type":"object","properties":{"event":{"type":"string","enum":["enter","leave","down","up","click"]}},"required":["event"],"additionalProperties":false}},"apiCredentialTestResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}},"required":["requestId","ok","message"],"additionalProperties":false},"dictionaryResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"dictionary":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"code":{"type":"string"},"display":{"type":"string"},"weight":{"type":"integer"}},"required":["word"],"additionalProperties":false}},"content":{"type":"string"},"filename":{"type":"string"},"offset":{"type":"integer","minimum":0},"hasMore":{"type":"boolean"}},"required":["requestId","ok","message","rows"],"additionalProperties":true},"statsResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"overview":{"type":"object","properties":{"hasData":{"type":"boolean"},"firstDayKey":{"type":"integer"},"lastDayKey":{"type":"integer"},"days":{"type":"integer"},"totalChars":{"type":"integer"},"totalActiveMs":{"type":"integer"},"todayDayKey":{"type":"integer"},"todayChars":{"type":"integer"},"todayActiveMs":{"type":"integer"},"averagePerDay":{"type":"number"},"currentStreak":{"type":"integer"},"longestStreak":{"type":"integer"},"bestDayKey":{"type":"integer"},"bestDayChars":{"type":"integer"},"todaySpeed":{"type":"number"},"averageSpeed":{"type":"number"},"fastestSpeed":{"type":"number"},"fastestDayKey":{"type":"integer"},"categories":{"type":"object","properties":{"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"}},"required":["cjk","latin","digit","punct","other"],"additionalProperties":false},"todayHourly":{"type":"array","items":{"type":"integer"}},"daily":{"type":"array","items":{"type":"object","properties":{"dayKey":{"type":"integer"},"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"},"activeMs":{"type":"integer"}},"required":["dayKey","cjk","latin","digit","punct","other","activeMs"],"additionalProperties":false}}},"required":["hasData","firstDayKey","lastDayKey","days","totalChars","totalActiveMs","todayDayKey","todayChars","todayActiveMs","averagePerDay","currentStreak","longestStreak","bestDayKey","bestDayChars","todaySpeed","averageSpeed","fastestSpeed","fastestDayKey","categories","todayHourly","daily"],"additionalProperties":false},"removedDays":{"type":"integer"}},"required":["requestId","action","ok","message"],"additionalProperties":true}}})msime"; +inline constexpr char SchemaJson[] = R"msime({"version":1,"client":{"configRequest":{"surfaces":["settings"]},"skinCatalogRequest":{"surfaces":["settings"]},"openSkinDirectory":{"surfaces":["settings"]},"openHelpcodeDirectory":{"surfaces":["settings"]},"openShuangpinDirectory":{"surfaces":["settings"]},"collocationModelDownload":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelDelete":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelImport":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelStatusRequest":{"surfaces":["settings"]},"openHandwritingPanel":{"surfaces":["settings"]},"restartServer":{"surfaces":["settings"]},"copyText":{"surfaces":["settings"],"data":{"type":"string"}},"openExternalUrl":{"surfaces":["settings"],"data":{"type":"string"}},"dragStart":{"surfaces":["settings","toolbar"]},"focus":{"surfaces":["settings"]},"resizeStart":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"resizeHitTest":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"windowControl":{"surfaces":["settings"],"data":{"type":"string","enum":["minimize","maximize","restore","close"]}},"maximizeButtonRect":{"surfaces":["settings"],"data":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0},"dpr":{"type":"number","minimum":0.01}},"required":["x","y","width","height","dpr"],"additionalProperties":false}},"configUpdate":{"surfaces":["settings"],"data":{"type":"object","properties":{"path":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["path","value"],"additionalProperties":false}},"apiCredentialTest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["translation.tencent","translation.niutrans","translation.custom","voice.asr","voice.polish","ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"apiModelList":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"dictionaryRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"dictionary":{"type":"string","enum":["quanpin","wubi","english","quick"]},"action":{"type":"string","enum":["query","create","update","delete","import","importHans","export"]},"word":{"type":"string"},"code":{"type":"string"},"content":{"type":"string"},"weight":{"type":"integer"},"oldWord":{"type":"string"},"oldCode":{"type":"string"},"display":{"type":"string"},"oldDisplay":{"type":"string"},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["requestId","dictionary","action"],"additionalProperties":false}},"statsRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"action":{"type":"string","enum":["overview","clearAll","openDirectory"]}},"required":["requestId","action"],"additionalProperties":false}},"openKeyboardPanel":{"surfaces":["settings","toolbar"]},"candidate":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"delete":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"pin":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"clearPosition":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"candidateWheel":{"surfaces":["candidate"],"data":{"type":"integer","minimum":-32768,"maximum":32767}},"candidatePage":{"surfaces":["candidate"],"data":{"type":"string","enum":["previous","next"]}},"fixPosition":{"surfaces":["candidate"],"data":{"type":"object","properties":{"index":{"type":"integer","minimum":1,"maximum":10},"position":{"type":"integer","minimum":1,"maximum":5}},"required":["index","position"],"additionalProperties":false}},"contextMenuResize":{"surfaces":["candidate"],"data":{"type":"object","properties":{"width":{"type":"integer","minimum":0,"maximum":65535},"height":{"type":"integer","minimum":0,"maximum":65535},"topExpansion":{"type":"integer","minimum":0,"maximum":65535},"leftExpansion":{"type":"integer","minimum":0,"maximum":65535}},"required":["width","height"],"additionalProperties":false}},"contextMenuClosed":{"surfaces":["candidate"]},"candidatePointerArmed":{"surfaces":["candidate"]},"candidatePointerMotion":{"surfaces":["candidate"]},"candidateFrameProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"revision":{"type":"integer"},"stage":{"type":"integer"},"performanceMs":{"type":"number","minimum":0}},"required":["revision","stage","performanceMs"],"additionalProperties":false}},"candidatePointerProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"probe":{"type":"integer"},"screenX":{"type":"integer"},"screenY":{"type":"integer"},"clientX":{"type":"integer"},"clientY":{"type":"integer"},"movementX":{"type":"integer"},"movementY":{"type":"integer"},"armed":{"type":"boolean"}},"required":["probe","screenX","screenY","clientX","clientY","movementX","movementY","armed"],"additionalProperties":false}},"contentTruncated":{"surfaces":["candidate","toolbar","menu"],"data":{"type":"object","properties":{"surface":{"type":"string","enum":["candidate","toolbar","menu","ftb","candwnd","cand"]},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}},"required":["surface","width","height"],"additionalProperties":true}},"floatingToggle":{"surfaces":["menu"],"data":{"type":"boolean"}},"settings":{"surfaces":["menu"]},"about":{"surfaces":["menu"]},"emojiSymbols":{"surfaces":["menu"]},"keyboardPanel":{"surfaces":["menu"]},"handwritingPanel":{"surfaces":["menu"]},"voiceInput":{"surfaces":["menu"]},"changeInputScheme":{"surfaces":["menu"],"data":{"type":"string","enum":["quanpin","shuangpin","wubi"]}},"inputSchemeSubmenu":{"surfaces":["menu"],"data":{"type":"object","properties":{"open":{"type":"boolean"},"width":{"type":"number","minimum":0}},"required":["open","width"],"additionalProperties":false}},"ready":{"surfaces":["toolbar"]},"exitEnglishInputMode":{"surfaces":["toolbar"]},"changeCharacterSet":{"surfaces":["toolbar"]},"openSettings":{"surfaces":["toolbar"]},"openEmojiPanel":{"surfaces":["toolbar"]},"changeIMEMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["cn","en"]}},"changeCharMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["fullwidth","halfwidth"]}},"changePuncMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["puncEn","puncCn"]}}},"server":{"configSnapshot":{"data":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"windowState":{"data":{"type":"object","properties":{"isMaximized":{"type":"boolean"}},"required":["isMaximized"],"additionalProperties":false}},"maxButtonEvent":{"data":{"type":"object","properties":{"event":{"type":"string","enum":["enter","leave","down","up","click"]}},"required":["event"],"additionalProperties":false}},"apiCredentialTestResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}},"required":["requestId","ok","message"],"additionalProperties":false},"apiModelListResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"models":{"type":"array","items":{"type":"string"}}},"required":["requestId","ok","message"],"additionalProperties":false},"dictionaryResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"dictionary":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"code":{"type":"string"},"display":{"type":"string"},"weight":{"type":"integer"}},"required":["word"],"additionalProperties":false}},"content":{"type":"string"},"filename":{"type":"string"},"offset":{"type":"integer","minimum":0},"hasMore":{"type":"boolean"}},"required":["requestId","ok","message","rows"],"additionalProperties":true},"statsResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"overview":{"type":"object","properties":{"hasData":{"type":"boolean"},"firstDayKey":{"type":"integer"},"lastDayKey":{"type":"integer"},"days":{"type":"integer"},"totalChars":{"type":"integer"},"totalActiveMs":{"type":"integer"},"todayDayKey":{"type":"integer"},"todayChars":{"type":"integer"},"todayActiveMs":{"type":"integer"},"averagePerDay":{"type":"number"},"currentStreak":{"type":"integer"},"longestStreak":{"type":"integer"},"bestDayKey":{"type":"integer"},"bestDayChars":{"type":"integer"},"todaySpeed":{"type":"number"},"averageSpeed":{"type":"number"},"fastestSpeed":{"type":"number"},"fastestDayKey":{"type":"integer"},"categories":{"type":"object","properties":{"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"}},"required":["cjk","latin","digit","punct","other"],"additionalProperties":false},"todayHourly":{"type":"array","items":{"type":"integer"}},"daily":{"type":"array","items":{"type":"object","properties":{"dayKey":{"type":"integer"},"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"},"activeMs":{"type":"integer"}},"required":["dayKey","cjk","latin","digit","punct","other","activeMs"],"additionalProperties":false}}},"required":["hasData","firstDayKey","lastDayKey","days","totalChars","totalActiveMs","todayDayKey","todayChars","todayActiveMs","averagePerDay","currentStreak","longestStreak","bestDayKey","bestDayChars","todaySpeed","averageSpeed","fastestSpeed","fastestDayKey","categories","todayHourly","daily"],"additionalProperties":false},"removedDays":{"type":"integer"}},"required":["requestId","action","ok","message"],"additionalProperties":true}}})msime"; } diff --git a/engine/contracts/webview/schema.js b/engine/contracts/webview/schema.js index 0a0f2d87..a0dbad64 100644 --- a/engine/contracts/webview/schema.js +++ b/engine/contracts/webview/schema.js @@ -1,6 +1,6 @@ // Generated by generate.py; edit messages.json. (function(root) { - const schema = {"version":1,"client":{"configRequest":{"surfaces":["settings"]},"skinCatalogRequest":{"surfaces":["settings"]},"openSkinDirectory":{"surfaces":["settings"]},"openHelpcodeDirectory":{"surfaces":["settings"]},"openShuangpinDirectory":{"surfaces":["settings"]},"collocationModelDownload":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelDelete":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelImport":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelStatusRequest":{"surfaces":["settings"]},"openHandwritingPanel":{"surfaces":["settings"]},"restartServer":{"surfaces":["settings"]},"copyText":{"surfaces":["settings"],"data":{"type":"string"}},"openExternalUrl":{"surfaces":["settings"],"data":{"type":"string"}},"dragStart":{"surfaces":["settings","toolbar"]},"focus":{"surfaces":["settings"]},"resizeStart":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"resizeHitTest":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"windowControl":{"surfaces":["settings"],"data":{"type":"string","enum":["minimize","maximize","restore","close"]}},"maximizeButtonRect":{"surfaces":["settings"],"data":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0},"dpr":{"type":"number","minimum":0.01}},"required":["x","y","width","height","dpr"],"additionalProperties":false}},"configUpdate":{"surfaces":["settings"],"data":{"type":"object","properties":{"path":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["path","value"],"additionalProperties":false}},"apiCredentialTest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["translation.tencent","translation.niutrans","translation.custom","voice.asr","voice.polish","ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"dictionaryRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"dictionary":{"type":"string","enum":["quanpin","wubi","english","quick"]},"action":{"type":"string","enum":["query","create","update","delete","import","importHans","export"]},"word":{"type":"string"},"code":{"type":"string"},"content":{"type":"string"},"weight":{"type":"integer"},"oldWord":{"type":"string"},"oldCode":{"type":"string"},"display":{"type":"string"},"oldDisplay":{"type":"string"},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["requestId","dictionary","action"],"additionalProperties":false}},"statsRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"action":{"type":"string","enum":["overview","clearAll","openDirectory"]}},"required":["requestId","action"],"additionalProperties":false}},"openKeyboardPanel":{"surfaces":["settings","toolbar"]},"candidate":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"delete":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"pin":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"clearPosition":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"candidateWheel":{"surfaces":["candidate"],"data":{"type":"integer","minimum":-32768,"maximum":32767}},"candidatePage":{"surfaces":["candidate"],"data":{"type":"string","enum":["previous","next"]}},"fixPosition":{"surfaces":["candidate"],"data":{"type":"object","properties":{"index":{"type":"integer","minimum":1,"maximum":10},"position":{"type":"integer","minimum":1,"maximum":5}},"required":["index","position"],"additionalProperties":false}},"contextMenuResize":{"surfaces":["candidate"],"data":{"type":"object","properties":{"width":{"type":"integer","minimum":0,"maximum":65535},"height":{"type":"integer","minimum":0,"maximum":65535},"topExpansion":{"type":"integer","minimum":0,"maximum":65535},"leftExpansion":{"type":"integer","minimum":0,"maximum":65535}},"required":["width","height"],"additionalProperties":false}},"contextMenuClosed":{"surfaces":["candidate"]},"candidatePointerArmed":{"surfaces":["candidate"]},"candidatePointerMotion":{"surfaces":["candidate"]},"candidateFrameProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"revision":{"type":"integer"},"stage":{"type":"integer"},"performanceMs":{"type":"number","minimum":0}},"required":["revision","stage","performanceMs"],"additionalProperties":false}},"candidatePointerProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"probe":{"type":"integer"},"screenX":{"type":"integer"},"screenY":{"type":"integer"},"clientX":{"type":"integer"},"clientY":{"type":"integer"},"movementX":{"type":"integer"},"movementY":{"type":"integer"},"armed":{"type":"boolean"}},"required":["probe","screenX","screenY","clientX","clientY","movementX","movementY","armed"],"additionalProperties":false}},"contentTruncated":{"surfaces":["candidate","toolbar","menu"],"data":{"type":"object","properties":{"surface":{"type":"string","enum":["candidate","toolbar","menu","ftb","candwnd","cand"]},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}},"required":["surface","width","height"],"additionalProperties":true}},"floatingToggle":{"surfaces":["menu"],"data":{"type":"boolean"}},"settings":{"surfaces":["menu"]},"about":{"surfaces":["menu"]},"emojiSymbols":{"surfaces":["menu"]},"keyboardPanel":{"surfaces":["menu"]},"handwritingPanel":{"surfaces":["menu"]},"voiceInput":{"surfaces":["menu"]},"changeInputScheme":{"surfaces":["menu"],"data":{"type":"string","enum":["quanpin","shuangpin","wubi"]}},"inputSchemeSubmenu":{"surfaces":["menu"],"data":{"type":"object","properties":{"open":{"type":"boolean"},"width":{"type":"number","minimum":0}},"required":["open","width"],"additionalProperties":false}},"ready":{"surfaces":["toolbar"]},"exitEnglishInputMode":{"surfaces":["toolbar"]},"changeCharacterSet":{"surfaces":["toolbar"]},"openSettings":{"surfaces":["toolbar"]},"openEmojiPanel":{"surfaces":["toolbar"]},"changeIMEMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["cn","en"]}},"changeCharMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["fullwidth","halfwidth"]}},"changePuncMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["puncEn","puncCn"]}}},"server":{"configSnapshot":{"data":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"windowState":{"data":{"type":"object","properties":{"isMaximized":{"type":"boolean"}},"required":["isMaximized"],"additionalProperties":false}},"maxButtonEvent":{"data":{"type":"object","properties":{"event":{"type":"string","enum":["enter","leave","down","up","click"]}},"required":["event"],"additionalProperties":false}},"apiCredentialTestResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}},"required":["requestId","ok","message"],"additionalProperties":false},"dictionaryResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"dictionary":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"code":{"type":"string"},"display":{"type":"string"},"weight":{"type":"integer"}},"required":["word"],"additionalProperties":false}},"content":{"type":"string"},"filename":{"type":"string"},"offset":{"type":"integer","minimum":0},"hasMore":{"type":"boolean"}},"required":["requestId","ok","message","rows"],"additionalProperties":true},"statsResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"overview":{"type":"object","properties":{"hasData":{"type":"boolean"},"firstDayKey":{"type":"integer"},"lastDayKey":{"type":"integer"},"days":{"type":"integer"},"totalChars":{"type":"integer"},"totalActiveMs":{"type":"integer"},"todayDayKey":{"type":"integer"},"todayChars":{"type":"integer"},"todayActiveMs":{"type":"integer"},"averagePerDay":{"type":"number"},"currentStreak":{"type":"integer"},"longestStreak":{"type":"integer"},"bestDayKey":{"type":"integer"},"bestDayChars":{"type":"integer"},"todaySpeed":{"type":"number"},"averageSpeed":{"type":"number"},"fastestSpeed":{"type":"number"},"fastestDayKey":{"type":"integer"},"categories":{"type":"object","properties":{"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"}},"required":["cjk","latin","digit","punct","other"],"additionalProperties":false},"todayHourly":{"type":"array","items":{"type":"integer"}},"daily":{"type":"array","items":{"type":"object","properties":{"dayKey":{"type":"integer"},"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"},"activeMs":{"type":"integer"}},"required":["dayKey","cjk","latin","digit","punct","other","activeMs"],"additionalProperties":false}}},"required":["hasData","firstDayKey","lastDayKey","days","totalChars","totalActiveMs","todayDayKey","todayChars","todayActiveMs","averagePerDay","currentStreak","longestStreak","bestDayKey","bestDayChars","todaySpeed","averageSpeed","fastestSpeed","fastestDayKey","categories","todayHourly","daily"],"additionalProperties":false},"removedDays":{"type":"integer"}},"required":["requestId","action","ok","message"],"additionalProperties":true}}}; + const schema = {"version":1,"client":{"configRequest":{"surfaces":["settings"]},"skinCatalogRequest":{"surfaces":["settings"]},"openSkinDirectory":{"surfaces":["settings"]},"openHelpcodeDirectory":{"surfaces":["settings"]},"openShuangpinDirectory":{"surfaces":["settings"]},"collocationModelDownload":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelDelete":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelImport":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelStatusRequest":{"surfaces":["settings"]},"openHandwritingPanel":{"surfaces":["settings"]},"restartServer":{"surfaces":["settings"]},"copyText":{"surfaces":["settings"],"data":{"type":"string"}},"openExternalUrl":{"surfaces":["settings"],"data":{"type":"string"}},"dragStart":{"surfaces":["settings","toolbar"]},"focus":{"surfaces":["settings"]},"resizeStart":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"resizeHitTest":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"windowControl":{"surfaces":["settings"],"data":{"type":"string","enum":["minimize","maximize","restore","close"]}},"maximizeButtonRect":{"surfaces":["settings"],"data":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0},"dpr":{"type":"number","minimum":0.01}},"required":["x","y","width","height","dpr"],"additionalProperties":false}},"configUpdate":{"surfaces":["settings"],"data":{"type":"object","properties":{"path":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["path","value"],"additionalProperties":false}},"apiCredentialTest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["translation.tencent","translation.niutrans","translation.custom","voice.asr","voice.polish","ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"apiModelList":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"dictionaryRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"dictionary":{"type":"string","enum":["quanpin","wubi","english","quick"]},"action":{"type":"string","enum":["query","create","update","delete","import","importHans","export"]},"word":{"type":"string"},"code":{"type":"string"},"content":{"type":"string"},"weight":{"type":"integer"},"oldWord":{"type":"string"},"oldCode":{"type":"string"},"display":{"type":"string"},"oldDisplay":{"type":"string"},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["requestId","dictionary","action"],"additionalProperties":false}},"statsRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"action":{"type":"string","enum":["overview","clearAll","openDirectory"]}},"required":["requestId","action"],"additionalProperties":false}},"openKeyboardPanel":{"surfaces":["settings","toolbar"]},"candidate":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"delete":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"pin":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"clearPosition":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"candidateWheel":{"surfaces":["candidate"],"data":{"type":"integer","minimum":-32768,"maximum":32767}},"candidatePage":{"surfaces":["candidate"],"data":{"type":"string","enum":["previous","next"]}},"fixPosition":{"surfaces":["candidate"],"data":{"type":"object","properties":{"index":{"type":"integer","minimum":1,"maximum":10},"position":{"type":"integer","minimum":1,"maximum":5}},"required":["index","position"],"additionalProperties":false}},"contextMenuResize":{"surfaces":["candidate"],"data":{"type":"object","properties":{"width":{"type":"integer","minimum":0,"maximum":65535},"height":{"type":"integer","minimum":0,"maximum":65535},"topExpansion":{"type":"integer","minimum":0,"maximum":65535},"leftExpansion":{"type":"integer","minimum":0,"maximum":65535}},"required":["width","height"],"additionalProperties":false}},"contextMenuClosed":{"surfaces":["candidate"]},"candidatePointerArmed":{"surfaces":["candidate"]},"candidatePointerMotion":{"surfaces":["candidate"]},"candidateFrameProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"revision":{"type":"integer"},"stage":{"type":"integer"},"performanceMs":{"type":"number","minimum":0}},"required":["revision","stage","performanceMs"],"additionalProperties":false}},"candidatePointerProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"probe":{"type":"integer"},"screenX":{"type":"integer"},"screenY":{"type":"integer"},"clientX":{"type":"integer"},"clientY":{"type":"integer"},"movementX":{"type":"integer"},"movementY":{"type":"integer"},"armed":{"type":"boolean"}},"required":["probe","screenX","screenY","clientX","clientY","movementX","movementY","armed"],"additionalProperties":false}},"contentTruncated":{"surfaces":["candidate","toolbar","menu"],"data":{"type":"object","properties":{"surface":{"type":"string","enum":["candidate","toolbar","menu","ftb","candwnd","cand"]},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}},"required":["surface","width","height"],"additionalProperties":true}},"floatingToggle":{"surfaces":["menu"],"data":{"type":"boolean"}},"settings":{"surfaces":["menu"]},"about":{"surfaces":["menu"]},"emojiSymbols":{"surfaces":["menu"]},"keyboardPanel":{"surfaces":["menu"]},"handwritingPanel":{"surfaces":["menu"]},"voiceInput":{"surfaces":["menu"]},"changeInputScheme":{"surfaces":["menu"],"data":{"type":"string","enum":["quanpin","shuangpin","wubi"]}},"inputSchemeSubmenu":{"surfaces":["menu"],"data":{"type":"object","properties":{"open":{"type":"boolean"},"width":{"type":"number","minimum":0}},"required":["open","width"],"additionalProperties":false}},"ready":{"surfaces":["toolbar"]},"exitEnglishInputMode":{"surfaces":["toolbar"]},"changeCharacterSet":{"surfaces":["toolbar"]},"openSettings":{"surfaces":["toolbar"]},"openEmojiPanel":{"surfaces":["toolbar"]},"changeIMEMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["cn","en"]}},"changeCharMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["fullwidth","halfwidth"]}},"changePuncMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["puncEn","puncCn"]}}},"server":{"configSnapshot":{"data":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"windowState":{"data":{"type":"object","properties":{"isMaximized":{"type":"boolean"}},"required":["isMaximized"],"additionalProperties":false}},"maxButtonEvent":{"data":{"type":"object","properties":{"event":{"type":"string","enum":["enter","leave","down","up","click"]}},"required":["event"],"additionalProperties":false}},"apiCredentialTestResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}},"required":["requestId","ok","message"],"additionalProperties":false},"apiModelListResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"models":{"type":"array","items":{"type":"string"}}},"required":["requestId","ok","message"],"additionalProperties":false},"dictionaryResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"dictionary":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"code":{"type":"string"},"display":{"type":"string"},"weight":{"type":"integer"}},"required":["word"],"additionalProperties":false}},"content":{"type":"string"},"filename":{"type":"string"},"offset":{"type":"integer","minimum":0},"hasMore":{"type":"boolean"}},"required":["requestId","ok","message","rows"],"additionalProperties":true},"statsResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"overview":{"type":"object","properties":{"hasData":{"type":"boolean"},"firstDayKey":{"type":"integer"},"lastDayKey":{"type":"integer"},"days":{"type":"integer"},"totalChars":{"type":"integer"},"totalActiveMs":{"type":"integer"},"todayDayKey":{"type":"integer"},"todayChars":{"type":"integer"},"todayActiveMs":{"type":"integer"},"averagePerDay":{"type":"number"},"currentStreak":{"type":"integer"},"longestStreak":{"type":"integer"},"bestDayKey":{"type":"integer"},"bestDayChars":{"type":"integer"},"todaySpeed":{"type":"number"},"averageSpeed":{"type":"number"},"fastestSpeed":{"type":"number"},"fastestDayKey":{"type":"integer"},"categories":{"type":"object","properties":{"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"}},"required":["cjk","latin","digit","punct","other"],"additionalProperties":false},"todayHourly":{"type":"array","items":{"type":"integer"}},"daily":{"type":"array","items":{"type":"object","properties":{"dayKey":{"type":"integer"},"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"},"activeMs":{"type":"integer"}},"required":["dayKey","cjk","latin","digit","punct","other","activeMs"],"additionalProperties":false}}},"required":["hasData","firstDayKey","lastDayKey","days","totalChars","totalActiveMs","todayDayKey","todayChars","todayActiveMs","averagePerDay","currentStreak","longestStreak","bestDayKey","bestDayChars","todaySpeed","averageSpeed","fastestSpeed","fastestDayKey","categories","todayHourly","daily"],"additionalProperties":false},"removedDays":{"type":"integer"}},"required":["requestId","action","ok","message"],"additionalProperties":true}}}; root.MsimeMessageSchema = schema; if (typeof module !== 'undefined' && module.exports) module.exports = schema; })(globalThis); diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index 8e88589e..3262bb2b 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -499,6 +499,7 @@ token_deepseek = "" token_openai = "" token_siliconflow = "" token_groq = "" +token_custom = "" endpoint = "https://api.deepseek.com/chat/completions" model = "deepseek-v4-flash" # 空槽位使用提供商默认值,旧版 endpoint/model 只归属于当前提供商 @@ -506,10 +507,12 @@ endpoint_deepseek = "" endpoint_openai = "" endpoint_siliconflow = "" endpoint_groq = "" +endpoint_custom = "" model_deepseek = "" model_openai = "" model_siliconflow = "" model_groq = "" +model_custom = "" candidate_limit = 3 prompt_id = "custom_1" prompt = """你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。 diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 0d3cbdf1..2206e392 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -496,6 +496,7 @@ token_deepseek = "" token_openai = "" token_siliconflow = "" token_groq = "" +token_custom = "" endpoint = "https://api.deepseek.com/chat/completions" model = "deepseek-v4-flash" # 空槽位使用提供商默认值,旧版 endpoint/model 只归属于当前提供商 @@ -503,10 +504,12 @@ endpoint_deepseek = "" endpoint_openai = "" endpoint_siliconflow = "" endpoint_groq = "" +endpoint_custom = "" model_deepseek = "" model_openai = "" model_siliconflow = "" model_groq = "" +model_custom = "" candidate_limit = 3 prompt_id = "custom_1" prompt = """你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。 diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index ebc6bc2f..34619c14 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -286,9 +286,15 @@ namespace { const std::vector &AiAssistantProviders() { - static const std::vector providers{"deepseek", "openai", "siliconflow", "groq"}; + static const std::vector providers{"deepseek", "openai", "siliconflow", "groq", "custom"}; return providers; } + +// 服务商已下线的旧默认模型。用户配置里残留这些值时按空槽位处理,跟随当前默认值。 +bool IsRetiredAiAssistantDefaultModel(std::string_view provider, std::string_view model) +{ + return provider == "groq" && model == "llama-3.3-70b-versatile"; +} } // namespace namespace ime_config_detail @@ -819,11 +825,14 @@ bool LoadImeConfig() { const std::string id(provider); const auto load_slot = [&](const std::string &key, const std::string &legacy, std::string &target) { + // 空值和服务商已下线的旧默认模型都表示「用默认值」:早于 #610 的版本会把默认值原样写盘。 + const auto usable = [&](const std::string &value) { + return !value.empty() && !(key == "model" && IsRetiredAiAssistantDefaultModel(id, value)); + }; const std::string stored = tbl["ai_assistant"][key + "_" + id].value_or(std::string()); - // 空的旧版 endpoint/model 和空槽位一样表示「用默认值」。 - if (!stored.empty()) + if (usable(stored)) target = stored; - else if (id == g_ai_assistant.provider && !legacy.empty()) + else if (id == g_ai_assistant.provider && usable(legacy)) target = legacy; }; load_slot("endpoint", g_ai_assistant.endpoint, g_ai_assistant.endpoints[id]); diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index 932e36f1..e052b90e 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -62,7 +62,7 @@ struct VoiceInputConfig struct AiAssistantConfig { bool enabled = false; - // deepseek | openai | siliconflow | groq (all use Chat Completions) + // deepseek | openai | siliconflow | groq | custom (all use Chat Completions) std::string provider = "deepseek"; std::string token; std::map tokens; @@ -71,11 +71,15 @@ struct AiAssistantConfig std::map endpoints{{"deepseek", "https://api.deepseek.com/chat/completions"}, {"openai", "https://api.openai.com/v1/chat/completions"}, {"siliconflow", "https://api.siliconflow.cn/v1/chat/completions"}, - {"groq", "https://api.groq.com/openai/v1/chat/completions"}}; + {"groq", "https://api.groq.com/openai/v1/chat/completions"}, + {"custom", ""}}; + // Groq 已弃用 llama-3.3-70b-versatile,默认模型改用 qwen/qwen3.8-27b。 + // custom 槽位不预设接口地址和模型,全部由用户填写并单独保存。 std::map models{{"deepseek", "deepseek-v4-flash"}, {"openai", "gpt-4o-mini"}, {"siliconflow", "Qwen/Qwen3-8B"}, - {"groq", "llama-3.3-70b-versatile"}}; + {"groq", "qwen/qwen3.8-27b"}, + {"custom", ""}}; int candidate_limit = 3; // custom_1 | custom_2 | custom_3 std::string prompt_id = "custom_1"; diff --git a/server/src/settings/api_credential_test.cpp b/server/src/settings/api_credential_test.cpp index 52e03f5a..d52abcb6 100644 --- a/server/src/settings/api_credential_test.cpp +++ b/server/src/settings/api_credential_test.cpp @@ -20,6 +20,8 @@ namespace constexpr long kConnectTimeoutMs = 5000; constexpr long kRequestTimeoutMs = 15000; constexpr std::size_t kMaxResponseBytes = 256 * 1024; +// OpenRouter 等聚合服务的 /models 带描述和价格,能到数 MB。 +constexpr std::size_t kMaxModelListBytes = 16 * 1024 * 1024; struct HttpResponse { @@ -27,6 +29,7 @@ struct HttpResponse long status = 0; std::string body; std::string error; + std::size_t max_bytes = kMaxResponseBytes; }; std::string Value(const ApiCredentialTest::Request &request, const char *key) @@ -38,10 +41,10 @@ std::string Value(const ApiCredentialTest::Request &request, const char *key) size_t WriteResponse(char *data, size_t size, size_t count, void *user) { const size_t bytes = size * count; - auto *response = static_cast(user); - if (bytes > kMaxResponseBytes - (std::min)(response->size(), kMaxResponseBytes)) + auto *response = static_cast(user); + if (bytes > response->max_bytes - (std::min)(response->body.size(), response->max_bytes)) return 0; - response->append(data, bytes); + response->body.append(data, bytes); return bytes; } @@ -82,25 +85,35 @@ std::string ErrorDetail(const HttpResponse &response) return "HTTP " + std::to_string(response.status); } -HttpResponse PerformJsonPost(const std::string &endpoint, const std::string &token, const std::string &payload) +// payload 为空指针时发 GET,否则以 JSON body 发 POST。 +HttpResponse PerformJsonRequest(const std::string &endpoint, const std::string &token, const std::string *payload, + std::size_t max_bytes = kMaxResponseBytes) { InitCurl(); HttpResponse response; + response.max_bytes = max_bytes; CURL *curl = curl_easy_init(); if (!curl) return response; char error[CURL_ERROR_SIZE] = {}; curl_slist *headers = nullptr; - headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, payload ? "Content-Type: application/json" : "Accept: application/json"); const std::string authorization = "Authorization: Bearer " + token; headers = curl_slist_append(headers, authorization.c_str()); curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); NetworkProxy::ApplyToCurl(curl); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(payload.size())); + if (payload) + { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload->c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(payload->size())); + } + else + { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteResponse); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, kRequestTimeoutMs); @@ -113,6 +126,26 @@ HttpResponse PerformJsonPost(const std::string &endpoint, const std::string &tok return response; } +// 由 Chat Completions 地址或 base URL 推出 OpenAI 兼容的 /models 地址。 +// 查询串(如 Azure 的 api-version)原样保留,只改写路径部分。 +std::string ModelListEndpoint(const std::string &endpoint) +{ + const std::size_t query_at = endpoint.find_first_of("?#"); + std::string path = endpoint.substr(0, query_at); + const std::string query = query_at == std::string::npos ? std::string() : endpoint.substr(query_at); + while (!path.empty() && path.back() == '/') + path.pop_back(); + const auto ends_with = [&path](std::string_view suffix) { + return path.size() >= suffix.size() && path.compare(path.size() - suffix.size(), suffix.size(), suffix) == 0; + }; + constexpr std::string_view kChatSuffix = "/chat/completions"; + if (ends_with(kChatSuffix)) + path.resize(path.size() - kChatSuffix.size()); + if (!ends_with("/models")) + path += "/models"; + return path + query; +} + ApiCredentialTest::Result TestChat(const ApiCredentialTest::Request &request) { const std::string token = Value(request, "token"); @@ -131,7 +164,8 @@ ApiCredentialTest::Result TestChat(const ApiCredentialTest::Request &request) body["thinking"] = {{"type", "disabled"}}; else if (provider == "siliconflow") body["enable_thinking"] = false; - const HttpResponse response = PerformJsonPost(endpoint, token, body.dump()); + const std::string payload = body.dump(); + const HttpResponse response = PerformJsonRequest(endpoint, token, &payload); if (response.code == CURLE_OK && response.status >= 200 && response.status < 300) return {true, "连接成功,API Key 和模型配置有效。"}; return {false, "测试失败:" + ErrorDetail(response)}; @@ -206,7 +240,7 @@ ApiCredentialTest::Result TestBatchAsr(const ApiCredentialTest::Request &request curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteResponse); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, kRequestTimeoutMs); @@ -277,4 +311,65 @@ Result Run(const Request &request) } return {false, "不支持的配置测试类型。"}; } + +ModelListResult FetchModels(const Request &request) +{ + if (request.service != "ai.assistant") + return {false, "不支持的模型列表类型。", {}}; + const std::string token = Value(request, "token"); + const std::string endpoint = Value(request, "endpoint"); + if (!CloudTranslation::IsUsableSecret(token)) + return {false, "请先填写有效的 API Key。", {}}; + if (!IsHttpEndpoint(endpoint)) + return {false, "请先填写以 http:// 或 https:// 开头的接口地址。", {}}; + + const HttpResponse response = PerformJsonRequest(ModelListEndpoint(endpoint), token, nullptr, kMaxModelListBytes); + if (response.code != CURLE_OK || response.status < 200 || response.status >= 300) + { + return {false, "获取模型列表失败:" + ErrorDetail(response), {}}; + } + + try + { + const auto root = nlohmann::json::parse(response.body); + std::vector model_ids; + if (root.contains("data") && root.at("data").is_array()) + { + for (const auto &item : root.at("data")) + { + if (item.is_object() && item.contains("id") && item.at("id").is_string()) + { + model_ids.push_back(item.at("id").get()); + } + } + } + else if (root.is_array()) + { + for (const auto &item : root) + { + if (item.is_object() && item.contains("id") && item.at("id").is_string()) + { + model_ids.push_back(item.at("id").get()); + } + else if (item.is_string()) + { + model_ids.push_back(item.get()); + } + } + } + + if (model_ids.empty()) + { + return {false, "获取成功但未解析到可用模型。", {}}; + } + + std::sort(model_ids.begin(), model_ids.end()); + model_ids.erase(std::unique(model_ids.begin(), model_ids.end()), model_ids.end()); + return {true, "获取成功", std::move(model_ids)}; + } + catch (...) + { + return {false, "解析服务返回的模型列表失败。", {}}; + } +} } // namespace ApiCredentialTest diff --git a/server/src/settings/api_credential_test.h b/server/src/settings/api_credential_test.h index 33810c46..7515b11b 100644 --- a/server/src/settings/api_credential_test.h +++ b/server/src/settings/api_credential_test.h @@ -2,6 +2,7 @@ #include #include +#include namespace ApiCredentialTest { @@ -17,5 +18,13 @@ struct Result std::string message; }; +struct ModelListResult +{ + bool ok = false; + std::string message; + std::vector models; +}; + Result Run(const Request &request); +ModelListResult FetchModels(const Request &request); } // namespace ApiCredentialTest diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index 9bbf62bc..3db2f627 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -1472,6 +1472,45 @@ void HandleWebMessage(HWND hwnd, ICoreWebView2WebMessageReceivedEventArgs *args) }; }); } + else if (type == "apiModelList") + { + const auto &data = value.at("data").as_object(); + const std::string request_id = json::value_to(data.at("requestId")); + ApiCredentialTest::Request request; + request.service = json::value_to(data.at("service")); + for (const auto &[key, item] : data.at("config").as_object()) + { + if (item.is_string()) + request.config.emplace(std::string(key), json::value_to(item)); + } + g_worker->Submit([request_id, request = std::move(request)]() -> SerialTaskQueue::Completion { + ApiCredentialTest::ModelListResult result; + try + { + result = ApiCredentialTest::FetchModels(request); + } + catch (...) + { + result = {false, "获取模型列表失败:内部错误。", {}}; + } + json::array models_array; + for (const auto &m : result.models) + models_array.push_back(json::value(m)); + json::value response = {{"type", "apiModelListResult"}, + {"requestId", request_id}, + {"ok", result.ok}, + {"message", result.message}, + {"models", std::move(models_array)}, + {"protocolVersion", metasequoia::webview::Version}}; + if (!metasequoia::webview::Validate(response, "server")) + throw std::runtime_error("Invalid API model list response"); + auto message = string_to_wstring(json::serialize(response)); + return [message = std::move(message)] { + if (g_webview) + g_webview->PostWebMessageAsJson(message.c_str()); + }; + }); + } else if (type == "openKeyboardPanel") { OpenKeyboardPanelApplication(); diff --git a/server/tests/src/test_config_non_ascii_path.cpp b/server/tests/src/test_config_non_ascii_path.cpp index 85243bd1..e2f6226b 100644 --- a/server/tests/src/test_config_non_ascii_path.cpp +++ b/server/tests/src/test_config_non_ascii_path.cpp @@ -191,6 +191,29 @@ TEST_CASE(ai_provider_configuration_round_trips_without_mixing_credentials) REQUIRE_EQ(GetConfiguredAiAssistant().endpoint, std::string("https://openai.example.test/v1/chat/completions")); REQUIRE_EQ(GetConfiguredAiAssistant().model, std::string("custom-openai")); + // Custom 槽位的 base_url、API Key、模型独立落盘,切走再切回不串味。 + REQUIRE(SetConfiguredAiAssistantString("provider", "custom")); + REQUIRE(SetConfiguredAiAssistantString("token_custom", "test-custom")); + REQUIRE(SetConfiguredAiAssistantString("endpoint", "https://custom.example.test/v1/chat/completions")); + REQUIRE(SetConfiguredAiAssistantString("model", "custom-model")); + InitImeConfig(); + REQUIRE_EQ(GetConfiguredAiAssistant().provider, std::string("custom")); + REQUIRE_EQ(GetConfiguredAiAssistant().token, std::string("test-custom")); + REQUIRE_EQ(GetConfiguredAiAssistant().endpoint, std::string("https://custom.example.test/v1/chat/completions")); + REQUIRE_EQ(GetConfiguredAiAssistant().model, std::string("custom-model")); + REQUIRE_EQ(GetConfiguredAiAssistant().endpoints.at("custom"), + std::string("https://custom.example.test/v1/chat/completions")); + REQUIRE_EQ(GetConfiguredAiAssistant().models.at("custom"), std::string("custom-model")); + + REQUIRE(SetConfiguredAiAssistantString("provider", "openai")); + REQUIRE_EQ(GetConfiguredAiAssistant().token, std::string("test-openai")); + REQUIRE_EQ(GetConfiguredAiAssistant().endpoint, std::string("https://openai.example.test/v1/chat/completions")); + REQUIRE_EQ(GetConfiguredAiAssistant().model, std::string("custom-openai")); + REQUIRE(SetConfiguredAiAssistantString("provider", "custom")); + REQUIRE_EQ(GetConfiguredAiAssistant().token, std::string("test-custom")); + REQUIRE_EQ(GetConfiguredAiAssistant().endpoint, std::string("https://custom.example.test/v1/chat/completions")); + REQUIRE_EQ(GetConfiguredAiAssistant().model, std::string("custom-model")); + for (const std::string provider : {"siliconflow", "groq"}) { const AiAssistantConfig defaults; @@ -250,6 +273,28 @@ TEST_CASE(ai_provider_default_values_are_not_pinned_into_slots) fs::remove_all(unique_root, ec); } +TEST_CASE(ai_provider_retired_default_model_follows_new_default) +{ + namespace fs = std::filesystem; + const fs::path unique_root = MakeProfileRoot() / L"ai-provider-retired"; + const fs::path data_dir = unique_root / L"metasequoiaime"; + std::error_code ec; + fs::remove_all(unique_root, ec); + SeedTemplate(data_dir); + // 早期版本把 Groq 当时的默认模型原样写进了旧版 model 和 model_groq。 + WriteText(data_dir / L"config.toml", "[ai_assistant]\nprovider = \"groq\"\n" + "model = \"llama-3.3-70b-versatile\"\n" + "model_groq = \"llama-3.3-70b-versatile\"\n"); + { + ScopedConfigLocation location(unique_root); + const AiAssistantConfig defaults; + InitImeConfig(); + REQUIRE_EQ(GetConfiguredAiAssistant().model, defaults.models.at("groq")); + REQUIRE_EQ(GetConfiguredAiAssistant().models.at("groq"), defaults.models.at("groq")); + } + fs::remove_all(unique_root, ec); +} + TEST_CASE(config_recovers_unparseable_file_and_saves) { namespace fs = std::filesystem; diff --git a/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.test.ts b/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.test.ts index 0b6c39e0..25a04580 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.test.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.test.ts @@ -8,6 +8,7 @@ vi.mock('./shared', () => ({ })); vi.mock('./config-sync', () => ({ updateConfig: vi.fn() })); vi.mock('./credential-test', () => ({ setupCredentialTest: vi.fn() })); +vi.mock('./model-fetch', () => ({ setupModelFetch: vi.fn(() => vi.fn()) })); import { applyAiConfig, setupAiSettings } from './ai-settings'; import { updateConfig } from './config-sync'; @@ -99,3 +100,31 @@ it('reloads provider-specific values from a new config snapshot', () => { expect(elements.get('aiEndpoint')!.value).toBe('https://groq.example.test/v1/chat/completions'); expect(elements.get('aiModel')!.value).toBe('custom-groq'); }); + +it('keeps the Custom base url, api key and model independent from built-in providers', () => { + applyAiConfig({ + provider: 'deepseek', + token: 'deepseek-key', + endpoint: 'https://api.deepseek.com/chat/completions', + model: 'deepseek-v4-flash' + }); + + elements.get('aiProviderMenu')!.select('custom'); + // Custom 槽位没有内置默认值,切过去应为空等待填写。 + expect(elements.get('aiEndpoint')!.value).toBe(''); + expect(elements.get('aiModel')!.value).toBe(''); + expect(elements.get('aiToken')!.value).toBe(''); + + elements.get('aiEndpoint')!.value = 'https://my-llm.example.test/v1/chat/completions'; + elements.get('aiToken')!.value = 'custom-key'; + elements.get('aiModel')!.value = 'my-model'; + + elements.get('aiProviderMenu')!.select('openai'); + expect(elements.get('aiEndpoint')!.value).toBe('https://api.openai.com/v1/chat/completions'); + expect(elements.get('aiToken')!.value).toBe(''); + + elements.get('aiProviderMenu')!.select('custom'); + expect(elements.get('aiEndpoint')!.value).toBe('https://my-llm.example.test/v1/chat/completions'); + expect(elements.get('aiToken')!.value).toBe('custom-key'); + expect(elements.get('aiModel')!.value).toBe('my-model'); +}); diff --git a/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.ts b/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.ts index ed0cd807..5101d32a 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/ai-settings.ts @@ -1,6 +1,7 @@ import { applyDropdownValue, applyToggleState, setupDropdownMenu, setupToggleButton } from './shared'; import { updateConfig } from './config-sync'; import { setupCredentialTest } from './credential-test'; +import { setupModelFetch } from './model-fetch'; type ProviderDefaults = { endpoint: string; model: string }; @@ -23,11 +24,15 @@ const PROVIDER_DEFAULTS: Record = { }, groq: { endpoint: 'https://api.groq.com/openai/v1/chat/completions', - model: 'llama-3.3-70b-versatile' + model: 'qwen/qwen3.8-27b' + }, + custom: { + endpoint: '', + model: '' } }; -const PROVIDERS = ['deepseek', 'openai', 'siliconflow', 'groq'] as const; +const PROVIDERS = ['deepseek', 'openai', 'siliconflow', 'groq', 'custom'] as const; let tokens: Record = {}; let endpoints: Record = {}; let models: Record = {}; @@ -78,6 +83,7 @@ function switchProvider(provider: string): void { if (token) token.value = tokens[provider] ?? ''; updateConfig('ai_assistant.provider', provider); applyProviderFields(provider); + clearModelMenu(); } function switchPrompt(id: string): void { @@ -90,21 +96,35 @@ function switchPrompt(id: string): void { if (prompt) prompt.value = customPrompts[id] ?? ''; } +// 凭据测试和模型列表共用同一份请求配置,空值按服务商默认值补全。 +function currentAiConfig(): Record { + const defaults = PROVIDER_DEFAULTS[currentProvider]; + const value = (id: string) => + (document.getElementById(id) as HTMLInputElement | null)?.value.trim() ?? ''; + return { + provider: currentProvider, + token: value('aiToken'), + endpoint: value('aiEndpoint') || defaults?.endpoint || '', + model: value('aiModel') || defaults?.model || '' + }; +} + +// 切换服务商后旧的模型列表和在途请求都不再适用,清空等待重新获取。 +let clearModelMenu: () => void = () => {}; + export function setupAiSettings(): void { setupToggleButton('aiEnabled', value => updateConfig('ai_assistant.enabled', value)); setupTokenVisibilityToggle(); - setupCredentialTest('aiCredentialTestButton', 'aiCredentialTestStatus', () => 'ai.assistant', () => { - const defaults = PROVIDER_DEFAULTS[currentProvider]; - const value = (id: string) => - (document.getElementById(id) as HTMLInputElement | null)?.value.trim() ?? ''; - return { - provider: currentProvider, - token: value('aiToken'), - endpoint: value('aiEndpoint') || defaults?.endpoint || '', - model: value('aiModel') || defaults?.model || '' - }; + setupCredentialTest('aiCredentialTestButton', 'aiCredentialTestStatus', () => 'ai.assistant', currentAiConfig); + clearModelMenu = setupModelFetch({ + buttonId: 'aiModelFetchButton', + statusId: 'aiModelFetchStatus', + menuId: 'aiModelMenu', + service: () => 'ai.assistant', + readConfig: currentAiConfig }); setupDropdownMenu('aiProviderBtn', 'aiProviderMenu', 'changeAiProvider', true); + setupDropdownMenu('aiModelBtn', 'aiModelMenu', '', true, 'ai_assistant.model'); setupDropdownMenu('aiPromptSlotBtn', 'aiPromptSlotMenu', 'changeAiPromptSlot', true, 'ai_assistant.prompt_id'); document.getElementById('aiProviderMenu')?.addEventListener('click', (event) => { diff --git a/ui-html/webview2/settings/ime-settings/src/modules/model-fetch.test.ts b/ui-html/webview2/settings/ime-settings/src/modules/model-fetch.test.ts new file mode 100644 index 00000000..b01e9bf7 --- /dev/null +++ b/ui-html/webview2/settings/ime-settings/src/modules/model-fetch.test.ts @@ -0,0 +1,139 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import { setupModelFetch } from './model-fetch'; + +class FakeElement extends EventTarget { + textContent = ''; + disabled = false; + dataset: Record = {}; + children: FakeElement[] = []; + className = ''; + title = ''; + classList = { + values: new Set(), + add: (name: string) => { this.classList.values.add(name); }, + remove: (name: string) => { this.classList.values.delete(name); }, + contains: (name: string) => this.classList.values.has(name) + }; + replaceChildren(...nodes: FakeElement[]): void { + this.children = nodes; + } + get childElementCount(): number { + return this.children.length; + } +} + +let button: FakeElement; +let status: FakeElement; +let menu: FakeElement; +let postMessage: ReturnType; +let hostListener: ((event: Event & { data?: unknown }) => void) | undefined; + +beforeEach(() => { + button = new FakeElement(); + button.textContent = '获取模型'; + status = new FakeElement(); + menu = new FakeElement(); + postMessage = vi.fn(); + vi.stubGlobal('document', { + getElementById: (id: string) => { + if (id === 'modelButton') return button; + if (id === 'modelStatus') return status; + if (id === 'modelMenu') return menu; + return null; + }, + createElement: () => new FakeElement(), + querySelectorAll: () => [] + }); + vi.stubGlobal('window', { + chrome: { webview: { + postMessage, + addEventListener: (_type: string, listener: (event: Event & { data?: unknown }) => void) => { + hostListener = listener; + } + } } + }); + vi.stubGlobal('MsimeProtocol', { + version: 1, + validate: () => true, + serializeClientMessage: (message: unknown) => JSON.stringify(message) + }); +}); + +afterEach(() => vi.unstubAllGlobals()); + +function setup(): () => void { + return setupModelFetch({ + buttonId: 'modelButton', + statusId: 'modelStatus', + menuId: 'modelMenu', + service: () => 'ai.assistant', + readConfig: () => ({ provider: 'groq', token: 'token', endpoint: 'https://api.groq.com/openai/v1/chat/completions' }) + }); +} + +it('posts the current config and renders the returned models into the menu', () => { + setup(); + button.dispatchEvent(new Event('click')); + expect(button.disabled).toBe(true); + expect(button.textContent).toBe('获取中…'); + const request = JSON.parse(postMessage.mock.calls[0]![0]); + expect(request).toMatchObject({ + type: 'apiModelList', + data: { service: 'ai.assistant', config: { provider: 'groq' } } + }); + + hostListener?.({ data: { + type: 'apiModelListResult', requestId: request.data.requestId, + ok: true, message: '获取成功', models: ['llama-3.3-70b-versatile', 'qwen/qwen3.8-27b'] + } } as Event & { data?: unknown }); + + expect(button.disabled).toBe(false); + expect(button.textContent).toBe('获取模型'); + expect(menu.children.map(item => item.dataset.value)).toEqual(['llama-3.3-70b-versatile', 'qwen/qwen3.8-27b']); + expect(status.textContent).toBe('获取到 2 个模型'); + expect(status.dataset.kind).toBe('success'); + expect(menu.classList.contains('open')).toBe(true); +}); + +it('keeps the menu untouched and reports the error when the query fails', () => { + setup(); + button.dispatchEvent(new Event('click')); + const request = JSON.parse(postMessage.mock.calls[0]![0]); + hostListener?.({ data: { + type: 'apiModelListResult', requestId: request.data.requestId, + ok: false, message: '获取模型列表失败:HTTP 401', models: [] + } } as Event & { data?: unknown }); + + expect(menu.children).toEqual([]); + expect(status.textContent).toBe('获取模型列表失败:HTTP 401'); + expect(status.dataset.kind).toBe('error'); +}); + +it('ignores results that do not match the pending request', () => { + setup(); + button.dispatchEvent(new Event('click')); + hostListener?.({ data: { + type: 'apiModelListResult', requestId: 'stale', ok: true, message: '获取成功', models: ['x'] + } } as Event & { data?: unknown }); + + expect(button.disabled).toBe(true); + expect(menu.children).toEqual([]); +}); + +it('drops an in-flight result after reset, such as on a provider switch', () => { + const reset = setup(); + button.dispatchEvent(new Event('click')); + const request = JSON.parse(postMessage.mock.calls[0]![0]); + reset(); + expect(button.disabled).toBe(false); + expect(button.textContent).toBe('获取模型'); + + hostListener?.({ data: { + type: 'apiModelListResult', requestId: request.data.requestId, + ok: true, message: '获取成功', models: ['llama-3.3-70b-versatile'] + } } as Event & { data?: unknown }); + + expect(menu.children).toEqual([]); + expect(menu.classList.contains('open')).toBe(false); + expect(status.textContent).toBe(''); +}); diff --git a/ui-html/webview2/settings/ime-settings/src/modules/model-fetch.ts b/ui-html/webview2/settings/ime-settings/src/modules/model-fetch.ts new file mode 100644 index 00000000..ff60d2ef --- /dev/null +++ b/ui-html/webview2/settings/ime-settings/src/modules/model-fetch.ts @@ -0,0 +1,97 @@ +import { serializeHostMessage } from '../../../../shared/messages'; +import { onHostMessage } from '../utils/host-messages'; + +export type ModelListService = 'ai.assistant'; + +export interface ModelFetchOptions { + buttonId: string; + statusId: string; + menuId: string; + service: () => ModelListService; + readConfig: () => Record; +} + +let nextRequestId = 0; + +/** + * 「获取模型」按钮:点一下按当前服务商和凭据拉取模型列表,把结果灌进同字段的下拉菜单。 + * 模型本身仍是可自由输入的文本框,菜单只是候选来源,清空菜单不影响手动填写。 + * + * 返回的 reset 清空菜单和状态,并丢弃尚未返回的请求:切换服务商后旧请求的结果不再适用。 + */ +export function setupModelFetch(options: ModelFetchOptions): () => void { + const button = document.getElementById(options.buttonId) as HTMLButtonElement | null; + const status = document.getElementById(options.statusId); + const menu = document.getElementById(options.menuId); + if (!button || !menu) return () => {}; + + let pendingRequestId = ''; + const idleLabel = button.textContent?.trim() || '获取模型'; + const settle = (): void => { + pendingRequestId = ''; + button.disabled = false; + button.textContent = idleLabel; + }; + + onHostMessage('apiModelListResult', payload => { + if (!pendingRequestId || payload.requestId !== pendingRequestId) return; + settle(); + + if (!payload.ok) { + setStatus(status, payload.message, 'error'); + return; + } + + const models = (payload.models ?? []).filter(model => model.trim().length > 0); + renderModels(menu, models); + if (models.length === 0) { + setStatus(status, payload.message || '服务商没有返回可用模型', 'error'); + return; + } + + setStatus(status, `获取到 ${models.length} 个模型`, 'success'); + document.querySelectorAll('.dropdown-menu.open').forEach(openMenu => { + if (openMenu !== menu) openMenu.classList.remove('open'); + }); + menu.classList.add('open'); + }); + + button.addEventListener('click', () => { + if (!window.chrome?.webview || pendingRequestId) return; + pendingRequestId = `${Date.now()}-${++nextRequestId}`; + button.disabled = true; + button.textContent = '获取中…'; + setStatus(status, '正在向服务商查询模型列表…', 'pending'); + window.chrome.webview.postMessage(serializeHostMessage({ + type: 'apiModelList', + data: { requestId: pendingRequestId, service: options.service(), config: options.readConfig() } + })); + }); + + return () => { + settle(); + menu.classList.remove('open'); + menu.replaceChildren(); + if (status) { + status.textContent = ''; + delete status.dataset.kind; + } + }; +} + +function renderModels(menu: HTMLElement, models: string[]): void { + menu.replaceChildren(...models.map(model => { + const item = document.createElement('div'); + item.className = 'dropdown-item'; + item.dataset.value = model; + item.textContent = model; + item.title = model; + return item; + })); +} + +function setStatus(status: HTMLElement | null, message: string, kind: string): void { + if (!status) return; + status.textContent = message; + status.dataset.kind = kind; +} diff --git a/ui-html/webview2/settings/ime-settings/src/modules/shared.ts b/ui-html/webview2/settings/ime-settings/src/modules/shared.ts index deafa02e..09c93161 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/shared.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/shared.ts @@ -211,6 +211,8 @@ function delay(ms: number): Promise { async function openDropdownMenu(menu: HTMLElement, menuId: string): Promise { const preparer = dropdownPreparers.get(menuId); if (!preparer?.isPending()) { + // 选项按需灌入的菜单(如获取前的模型列表)为空时不弹出空框。 + if (!menu.querySelector('.dropdown-item')) return; menu.classList.add('open'); return; } @@ -285,6 +287,8 @@ export function setupDropdownMenu( btn.addEventListener('keydown', (event: KeyboardEvent) => { if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return; + // 空菜单没有可聚焦的项,方向键留给内嵌输入框移动光标。 + if (enabledItems().length === 0 && !dropdownPreparers.get(menuId)?.isPending()) return; event.preventDefault(); const direction = event.key === 'ArrowDown' ? 1 : -1; void openDropdownMenu(menu, menuId).then(() => focusMenuItem(direction)); diff --git a/ui-html/webview2/settings/ime-settings/src/partials/ai-settings.html b/ui-html/webview2/settings/ime-settings/src/partials/ai-settings.html index 2fc62114..99be15d5 100644 --- a/ui-html/webview2/settings/ime-settings/src/partials/ai-settings.html +++ b/ui-html/webview2/settings/ime-settings/src/partials/ai-settings.html @@ -8,7 +8,7 @@
API 配置
-
DeepSeek、OpenAI、SiliconFlow、Groq 均使用 OpenAI 兼容的 Chat Completions 接口。每个提供商的 Token 分开保存。
+
DeepSeek、OpenAI、SiliconFlow、Groq 均使用 OpenAI 兼容的 Chat Completions 接口。Custom 用于接入任意兼容服务,填写自己的接口地址与模型。每个提供商的接口地址、Token 和模型分开保存。
服务提供商 @@ -22,10 +22,29 @@ +
- +
+ 模型 +
+ + +
+
+ + +
+ +
@@ -46,10 +65,6 @@
-
- - -
diff --git a/ui-html/webview2/settings/ime-settings/src/styles/modules/ai-settings.css b/ui-html/webview2/settings/ime-settings/src/styles/modules/ai-settings.css index 2c6e0c45..ffbffb20 100644 --- a/ui-html/webview2/settings/ime-settings/src/styles/modules/ai-settings.css +++ b/ui-html/webview2/settings/ime-settings/src/styles/modules/ai-settings.css @@ -152,6 +152,94 @@ display: block; } +/* 模型字段:输入框兼作下拉触发器,右侧是「获取模型」按钮。 */ +.ai-model-row { + display: flex; + align-items: stretch; + gap: 8px; +} + +.ai-model-row .ai-model-dropdown { + position: relative; + flex: 1 1 auto; + min-width: 0; +} + +.ai-model-row .dropdown-toggle { + width: 100%; + min-width: 0; + padding: 9px 8px 9px 12px; +} + +/* 覆盖 .ai-grid input 的独立输入框外观,让输入框融进下拉按钮。 */ +.ai-grid .ai-model-row .dropdown-toggle input { + box-sizing: border-box; + width: 100%; + min-width: 0; + padding: 0; + color: inherit; + background: transparent; + border: 0; + border-radius: 0; + outline: none; + font: inherit; + cursor: text; +} + +.ai-grid .ai-model-row .dropdown-toggle input:focus { + border-color: transparent; + background: transparent; + box-shadow: none; +} + +.ai-model-fetch-button { + flex: 0 0 auto; + padding: 0 12px; + color: var(--button-secondary-text); + background: var(--button-secondary-bg); + border: 1px solid var(--control-border); + border-radius: 6px; + font-size: 12px; + line-height: 1; + white-space: nowrap; + cursor: pointer; +} + +.ai-model-fetch-button:hover:not(:disabled) { + background: var(--button-secondary-hover); +} + +.ai-model-fetch-button:disabled { + cursor: wait; + opacity: 0.65; +} + +.ai-model-actions { + display: flex; + align-items: center; + gap: 10px; + min-height: 20px; +} + +.ai-model-fetch-status { + min-height: 14px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.45; +} + +.ai-model-fetch-status:empty { + display: none; +} + +.ai-model-fetch-status[data-kind="success"] { + color: #4caf72; +} + +.ai-model-fetch-status[data-kind="error"] { + color: #d96767; +} + .ai-prompt { min-height: 280px; resize: none; diff --git a/ui-html/webview2/shared/messages.ts b/ui-html/webview2/shared/messages.ts index 38e1cd11..e645370e 100644 --- a/ui-html/webview2/shared/messages.ts +++ b/ui-html/webview2/shared/messages.ts @@ -23,6 +23,7 @@ export type ClientMessage = | { "type": "maximizeButtonRect"; "protocolVersion"?: 1; "data": { "x": number; "y": number; "width": number; "height": number; "dpr": number } } | { "type": "configUpdate"; "protocolVersion"?: 1; "data": { "path": string; "value": string | number | boolean } } | { "type": "apiCredentialTest"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "translation.tencent" | "translation.niutrans" | "translation.custom" | "voice.asr" | "voice.polish" | "ai.assistant"; "config": { [key: string]: unknown } } } + | { "type": "apiModelList"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "ai.assistant"; "config": { [key: string]: unknown } } } | { "type": "dictionaryRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "dictionary": "quanpin" | "wubi" | "english" | "quick"; "action": "query" | "create" | "update" | "delete" | "import" | "importHans" | "export"; "word"?: string; "code"?: string; "content"?: string; "weight"?: number; "oldWord"?: string; "oldCode"?: string; "display"?: string; "oldDisplay"?: string; "offset"?: number; "limit"?: number } } | { "type": "statsRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "action": "overview" | "clearAll" | "openDirectory" } } | { "type": "openKeyboardPanel"; "protocolVersion"?: 1 } @@ -62,6 +63,7 @@ export type ServerMessage = | { "type": "windowState"; "protocolVersion"?: 1; "data": { "isMaximized": boolean } } | { "type": "maxButtonEvent"; "protocolVersion"?: 1; "data": { "event": "enter" | "leave" | "down" | "up" | "click" } } | { "type": "apiCredentialTestResult"; "protocolVersion"?: 1; "requestId": string; "ok": boolean; "message": string } + | { "type": "apiModelListResult"; "protocolVersion"?: 1; "requestId": string; "ok": boolean; "message": string; "models"?: Array } | { "type": "dictionaryResponse"; "protocolVersion"?: 1; "requestId": string; "action"?: string; "dictionary"?: string; "ok": boolean; "message": string; "rows": Array<{ "word": string; "code"?: string; "display"?: string; "weight"?: number }>; "content"?: string; "filename"?: string; "offset"?: number; "hasMore"?: boolean; [key: string]: unknown } | { "type": "statsResponse"; "protocolVersion"?: 1; "requestId": string; "action": string; "ok": boolean; "message": string; "overview"?: { "hasData": boolean; "firstDayKey": number; "lastDayKey": number; "days": number; "totalChars": number; "totalActiveMs": number; "todayDayKey": number; "todayChars": number; "todayActiveMs": number; "averagePerDay": number; "currentStreak": number; "longestStreak": number; "bestDayKey": number; "bestDayChars": number; "todaySpeed": number; "averageSpeed": number; "fastestSpeed": number; "fastestDayKey": number; "categories": { "cjk": number; "latin": number; "digit": number; "punct": number; "other": number }; "todayHourly": Array; "daily": Array<{ "dayKey": number; "cjk": number; "latin": number; "digit": number; "punct": number; "other": number; "activeMs": number }> }; "removedDays"?: number; [key: string]: unknown }; export type SettingsMessage = @@ -86,6 +88,7 @@ export type SettingsMessage = | { "type": "maximizeButtonRect"; "protocolVersion"?: 1; "data": { "x": number; "y": number; "width": number; "height": number; "dpr": number } } | { "type": "configUpdate"; "protocolVersion"?: 1; "data": { "path": string; "value": string | number | boolean } } | { "type": "apiCredentialTest"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "translation.tencent" | "translation.niutrans" | "translation.custom" | "voice.asr" | "voice.polish" | "ai.assistant"; "config": { [key: string]: unknown } } } + | { "type": "apiModelList"; "protocolVersion"?: 1; "data": { "requestId": string; "service": "ai.assistant"; "config": { [key: string]: unknown } } } | { "type": "dictionaryRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "dictionary": "quanpin" | "wubi" | "english" | "quick"; "action": "query" | "create" | "update" | "delete" | "import" | "importHans" | "export"; "word"?: string; "code"?: string; "content"?: string; "weight"?: number; "oldWord"?: string; "oldCode"?: string; "display"?: string; "oldDisplay"?: string; "offset"?: number; "limit"?: number } } | { "type": "statsRequest"; "protocolVersion"?: 1; "data": { "requestId": string; "action": "overview" | "clearAll" | "openDirectory" } } | { "type": "openKeyboardPanel"; "protocolVersion"?: 1 }; diff --git a/ui-html/webview2/shared/schema.js b/ui-html/webview2/shared/schema.js index 0a0f2d87..a0dbad64 100644 --- a/ui-html/webview2/shared/schema.js +++ b/ui-html/webview2/shared/schema.js @@ -1,6 +1,6 @@ // Generated by generate.py; edit messages.json. (function(root) { - const schema = {"version":1,"client":{"configRequest":{"surfaces":["settings"]},"skinCatalogRequest":{"surfaces":["settings"]},"openSkinDirectory":{"surfaces":["settings"]},"openHelpcodeDirectory":{"surfaces":["settings"]},"openShuangpinDirectory":{"surfaces":["settings"]},"collocationModelDownload":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelDelete":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelImport":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelStatusRequest":{"surfaces":["settings"]},"openHandwritingPanel":{"surfaces":["settings"]},"restartServer":{"surfaces":["settings"]},"copyText":{"surfaces":["settings"],"data":{"type":"string"}},"openExternalUrl":{"surfaces":["settings"],"data":{"type":"string"}},"dragStart":{"surfaces":["settings","toolbar"]},"focus":{"surfaces":["settings"]},"resizeStart":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"resizeHitTest":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"windowControl":{"surfaces":["settings"],"data":{"type":"string","enum":["minimize","maximize","restore","close"]}},"maximizeButtonRect":{"surfaces":["settings"],"data":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0},"dpr":{"type":"number","minimum":0.01}},"required":["x","y","width","height","dpr"],"additionalProperties":false}},"configUpdate":{"surfaces":["settings"],"data":{"type":"object","properties":{"path":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["path","value"],"additionalProperties":false}},"apiCredentialTest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["translation.tencent","translation.niutrans","translation.custom","voice.asr","voice.polish","ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"dictionaryRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"dictionary":{"type":"string","enum":["quanpin","wubi","english","quick"]},"action":{"type":"string","enum":["query","create","update","delete","import","importHans","export"]},"word":{"type":"string"},"code":{"type":"string"},"content":{"type":"string"},"weight":{"type":"integer"},"oldWord":{"type":"string"},"oldCode":{"type":"string"},"display":{"type":"string"},"oldDisplay":{"type":"string"},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["requestId","dictionary","action"],"additionalProperties":false}},"statsRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"action":{"type":"string","enum":["overview","clearAll","openDirectory"]}},"required":["requestId","action"],"additionalProperties":false}},"openKeyboardPanel":{"surfaces":["settings","toolbar"]},"candidate":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"delete":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"pin":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"clearPosition":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"candidateWheel":{"surfaces":["candidate"],"data":{"type":"integer","minimum":-32768,"maximum":32767}},"candidatePage":{"surfaces":["candidate"],"data":{"type":"string","enum":["previous","next"]}},"fixPosition":{"surfaces":["candidate"],"data":{"type":"object","properties":{"index":{"type":"integer","minimum":1,"maximum":10},"position":{"type":"integer","minimum":1,"maximum":5}},"required":["index","position"],"additionalProperties":false}},"contextMenuResize":{"surfaces":["candidate"],"data":{"type":"object","properties":{"width":{"type":"integer","minimum":0,"maximum":65535},"height":{"type":"integer","minimum":0,"maximum":65535},"topExpansion":{"type":"integer","minimum":0,"maximum":65535},"leftExpansion":{"type":"integer","minimum":0,"maximum":65535}},"required":["width","height"],"additionalProperties":false}},"contextMenuClosed":{"surfaces":["candidate"]},"candidatePointerArmed":{"surfaces":["candidate"]},"candidatePointerMotion":{"surfaces":["candidate"]},"candidateFrameProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"revision":{"type":"integer"},"stage":{"type":"integer"},"performanceMs":{"type":"number","minimum":0}},"required":["revision","stage","performanceMs"],"additionalProperties":false}},"candidatePointerProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"probe":{"type":"integer"},"screenX":{"type":"integer"},"screenY":{"type":"integer"},"clientX":{"type":"integer"},"clientY":{"type":"integer"},"movementX":{"type":"integer"},"movementY":{"type":"integer"},"armed":{"type":"boolean"}},"required":["probe","screenX","screenY","clientX","clientY","movementX","movementY","armed"],"additionalProperties":false}},"contentTruncated":{"surfaces":["candidate","toolbar","menu"],"data":{"type":"object","properties":{"surface":{"type":"string","enum":["candidate","toolbar","menu","ftb","candwnd","cand"]},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}},"required":["surface","width","height"],"additionalProperties":true}},"floatingToggle":{"surfaces":["menu"],"data":{"type":"boolean"}},"settings":{"surfaces":["menu"]},"about":{"surfaces":["menu"]},"emojiSymbols":{"surfaces":["menu"]},"keyboardPanel":{"surfaces":["menu"]},"handwritingPanel":{"surfaces":["menu"]},"voiceInput":{"surfaces":["menu"]},"changeInputScheme":{"surfaces":["menu"],"data":{"type":"string","enum":["quanpin","shuangpin","wubi"]}},"inputSchemeSubmenu":{"surfaces":["menu"],"data":{"type":"object","properties":{"open":{"type":"boolean"},"width":{"type":"number","minimum":0}},"required":["open","width"],"additionalProperties":false}},"ready":{"surfaces":["toolbar"]},"exitEnglishInputMode":{"surfaces":["toolbar"]},"changeCharacterSet":{"surfaces":["toolbar"]},"openSettings":{"surfaces":["toolbar"]},"openEmojiPanel":{"surfaces":["toolbar"]},"changeIMEMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["cn","en"]}},"changeCharMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["fullwidth","halfwidth"]}},"changePuncMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["puncEn","puncCn"]}}},"server":{"configSnapshot":{"data":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"windowState":{"data":{"type":"object","properties":{"isMaximized":{"type":"boolean"}},"required":["isMaximized"],"additionalProperties":false}},"maxButtonEvent":{"data":{"type":"object","properties":{"event":{"type":"string","enum":["enter","leave","down","up","click"]}},"required":["event"],"additionalProperties":false}},"apiCredentialTestResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}},"required":["requestId","ok","message"],"additionalProperties":false},"dictionaryResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"dictionary":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"code":{"type":"string"},"display":{"type":"string"},"weight":{"type":"integer"}},"required":["word"],"additionalProperties":false}},"content":{"type":"string"},"filename":{"type":"string"},"offset":{"type":"integer","minimum":0},"hasMore":{"type":"boolean"}},"required":["requestId","ok","message","rows"],"additionalProperties":true},"statsResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"overview":{"type":"object","properties":{"hasData":{"type":"boolean"},"firstDayKey":{"type":"integer"},"lastDayKey":{"type":"integer"},"days":{"type":"integer"},"totalChars":{"type":"integer"},"totalActiveMs":{"type":"integer"},"todayDayKey":{"type":"integer"},"todayChars":{"type":"integer"},"todayActiveMs":{"type":"integer"},"averagePerDay":{"type":"number"},"currentStreak":{"type":"integer"},"longestStreak":{"type":"integer"},"bestDayKey":{"type":"integer"},"bestDayChars":{"type":"integer"},"todaySpeed":{"type":"number"},"averageSpeed":{"type":"number"},"fastestSpeed":{"type":"number"},"fastestDayKey":{"type":"integer"},"categories":{"type":"object","properties":{"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"}},"required":["cjk","latin","digit","punct","other"],"additionalProperties":false},"todayHourly":{"type":"array","items":{"type":"integer"}},"daily":{"type":"array","items":{"type":"object","properties":{"dayKey":{"type":"integer"},"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"},"activeMs":{"type":"integer"}},"required":["dayKey","cjk","latin","digit","punct","other","activeMs"],"additionalProperties":false}}},"required":["hasData","firstDayKey","lastDayKey","days","totalChars","totalActiveMs","todayDayKey","todayChars","todayActiveMs","averagePerDay","currentStreak","longestStreak","bestDayKey","bestDayChars","todaySpeed","averageSpeed","fastestSpeed","fastestDayKey","categories","todayHourly","daily"],"additionalProperties":false},"removedDays":{"type":"integer"}},"required":["requestId","action","ok","message"],"additionalProperties":true}}}; + const schema = {"version":1,"client":{"configRequest":{"surfaces":["settings"]},"skinCatalogRequest":{"surfaces":["settings"]},"openSkinDirectory":{"surfaces":["settings"]},"openHelpcodeDirectory":{"surfaces":["settings"]},"openShuangpinDirectory":{"surfaces":["settings"]},"collocationModelDownload":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelDelete":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelImport":{"surfaces":["settings"],"data":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"],"additionalProperties":false}},"collocationModelStatusRequest":{"surfaces":["settings"]},"openHandwritingPanel":{"surfaces":["settings"]},"restartServer":{"surfaces":["settings"]},"copyText":{"surfaces":["settings"],"data":{"type":"string"}},"openExternalUrl":{"surfaces":["settings"],"data":{"type":"string"}},"dragStart":{"surfaces":["settings","toolbar"]},"focus":{"surfaces":["settings"]},"resizeStart":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"resizeHitTest":{"surfaces":["settings"],"data":{"type":"string","enum":["left","right","top","bottom","left-top","right-top","left-bottom","right-bottom","client"]}},"windowControl":{"surfaces":["settings"],"data":{"type":"string","enum":["minimize","maximize","restore","close"]}},"maximizeButtonRect":{"surfaces":["settings"],"data":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0},"dpr":{"type":"number","minimum":0.01}},"required":["x","y","width","height","dpr"],"additionalProperties":false}},"configUpdate":{"surfaces":["settings"],"data":{"type":"object","properties":{"path":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["path","value"],"additionalProperties":false}},"apiCredentialTest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["translation.tencent","translation.niutrans","translation.custom","voice.asr","voice.polish","ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"apiModelList":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"service":{"type":"string","enum":["ai.assistant"]},"config":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"required":["requestId","service","config"],"additionalProperties":false}},"dictionaryRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"dictionary":{"type":"string","enum":["quanpin","wubi","english","quick"]},"action":{"type":"string","enum":["query","create","update","delete","import","importHans","export"]},"word":{"type":"string"},"code":{"type":"string"},"content":{"type":"string"},"weight":{"type":"integer"},"oldWord":{"type":"string"},"oldCode":{"type":"string"},"display":{"type":"string"},"oldDisplay":{"type":"string"},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["requestId","dictionary","action"],"additionalProperties":false}},"statsRequest":{"surfaces":["settings"],"data":{"type":"object","properties":{"requestId":{"type":"string"},"action":{"type":"string","enum":["overview","clearAll","openDirectory"]}},"required":["requestId","action"],"additionalProperties":false}},"openKeyboardPanel":{"surfaces":["settings","toolbar"]},"candidate":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"delete":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"pin":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"clearPosition":{"surfaces":["candidate"],"data":{"type":"integer","minimum":1,"maximum":10}},"candidateWheel":{"surfaces":["candidate"],"data":{"type":"integer","minimum":-32768,"maximum":32767}},"candidatePage":{"surfaces":["candidate"],"data":{"type":"string","enum":["previous","next"]}},"fixPosition":{"surfaces":["candidate"],"data":{"type":"object","properties":{"index":{"type":"integer","minimum":1,"maximum":10},"position":{"type":"integer","minimum":1,"maximum":5}},"required":["index","position"],"additionalProperties":false}},"contextMenuResize":{"surfaces":["candidate"],"data":{"type":"object","properties":{"width":{"type":"integer","minimum":0,"maximum":65535},"height":{"type":"integer","minimum":0,"maximum":65535},"topExpansion":{"type":"integer","minimum":0,"maximum":65535},"leftExpansion":{"type":"integer","minimum":0,"maximum":65535}},"required":["width","height"],"additionalProperties":false}},"contextMenuClosed":{"surfaces":["candidate"]},"candidatePointerArmed":{"surfaces":["candidate"]},"candidatePointerMotion":{"surfaces":["candidate"]},"candidateFrameProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"revision":{"type":"integer"},"stage":{"type":"integer"},"performanceMs":{"type":"number","minimum":0}},"required":["revision","stage","performanceMs"],"additionalProperties":false}},"candidatePointerProbe":{"surfaces":["candidate"],"data":{"type":"object","properties":{"probe":{"type":"integer"},"screenX":{"type":"integer"},"screenY":{"type":"integer"},"clientX":{"type":"integer"},"clientY":{"type":"integer"},"movementX":{"type":"integer"},"movementY":{"type":"integer"},"armed":{"type":"boolean"}},"required":["probe","screenX","screenY","clientX","clientY","movementX","movementY","armed"],"additionalProperties":false}},"contentTruncated":{"surfaces":["candidate","toolbar","menu"],"data":{"type":"object","properties":{"surface":{"type":"string","enum":["candidate","toolbar","menu","ftb","candwnd","cand"]},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}},"required":["surface","width","height"],"additionalProperties":true}},"floatingToggle":{"surfaces":["menu"],"data":{"type":"boolean"}},"settings":{"surfaces":["menu"]},"about":{"surfaces":["menu"]},"emojiSymbols":{"surfaces":["menu"]},"keyboardPanel":{"surfaces":["menu"]},"handwritingPanel":{"surfaces":["menu"]},"voiceInput":{"surfaces":["menu"]},"changeInputScheme":{"surfaces":["menu"],"data":{"type":"string","enum":["quanpin","shuangpin","wubi"]}},"inputSchemeSubmenu":{"surfaces":["menu"],"data":{"type":"object","properties":{"open":{"type":"boolean"},"width":{"type":"number","minimum":0}},"required":["open","width"],"additionalProperties":false}},"ready":{"surfaces":["toolbar"]},"exitEnglishInputMode":{"surfaces":["toolbar"]},"changeCharacterSet":{"surfaces":["toolbar"]},"openSettings":{"surfaces":["toolbar"]},"openEmojiPanel":{"surfaces":["toolbar"]},"changeIMEMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["cn","en"]}},"changeCharMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["fullwidth","halfwidth"]}},"changePuncMode":{"surfaces":["toolbar"],"data":{"type":"string","enum":["puncEn","puncCn"]}}},"server":{"configSnapshot":{"data":{"type":"object","properties":{},"required":[],"additionalProperties":true}},"windowState":{"data":{"type":"object","properties":{"isMaximized":{"type":"boolean"}},"required":["isMaximized"],"additionalProperties":false}},"maxButtonEvent":{"data":{"type":"object","properties":{"event":{"type":"string","enum":["enter","leave","down","up","click"]}},"required":["event"],"additionalProperties":false}},"apiCredentialTestResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}},"required":["requestId","ok","message"],"additionalProperties":false},"apiModelListResult":{"properties":{"requestId":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"models":{"type":"array","items":{"type":"string"}}},"required":["requestId","ok","message"],"additionalProperties":false},"dictionaryResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"dictionary":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"code":{"type":"string"},"display":{"type":"string"},"weight":{"type":"integer"}},"required":["word"],"additionalProperties":false}},"content":{"type":"string"},"filename":{"type":"string"},"offset":{"type":"integer","minimum":0},"hasMore":{"type":"boolean"}},"required":["requestId","ok","message","rows"],"additionalProperties":true},"statsResponse":{"properties":{"requestId":{"type":"string"},"action":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"},"overview":{"type":"object","properties":{"hasData":{"type":"boolean"},"firstDayKey":{"type":"integer"},"lastDayKey":{"type":"integer"},"days":{"type":"integer"},"totalChars":{"type":"integer"},"totalActiveMs":{"type":"integer"},"todayDayKey":{"type":"integer"},"todayChars":{"type":"integer"},"todayActiveMs":{"type":"integer"},"averagePerDay":{"type":"number"},"currentStreak":{"type":"integer"},"longestStreak":{"type":"integer"},"bestDayKey":{"type":"integer"},"bestDayChars":{"type":"integer"},"todaySpeed":{"type":"number"},"averageSpeed":{"type":"number"},"fastestSpeed":{"type":"number"},"fastestDayKey":{"type":"integer"},"categories":{"type":"object","properties":{"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"}},"required":["cjk","latin","digit","punct","other"],"additionalProperties":false},"todayHourly":{"type":"array","items":{"type":"integer"}},"daily":{"type":"array","items":{"type":"object","properties":{"dayKey":{"type":"integer"},"cjk":{"type":"integer"},"latin":{"type":"integer"},"digit":{"type":"integer"},"punct":{"type":"integer"},"other":{"type":"integer"},"activeMs":{"type":"integer"}},"required":["dayKey","cjk","latin","digit","punct","other","activeMs"],"additionalProperties":false}}},"required":["hasData","firstDayKey","lastDayKey","days","totalChars","totalActiveMs","todayDayKey","todayChars","todayActiveMs","averagePerDay","currentStreak","longestStreak","bestDayKey","bestDayChars","todaySpeed","averageSpeed","fastestSpeed","fastestDayKey","categories","todayHourly","daily"],"additionalProperties":false},"removedDays":{"type":"integer"}},"required":["requestId","action","ok","message"],"additionalProperties":true}}}; root.MsimeMessageSchema = schema; if (typeof module !== 'undefined' && module.exports) module.exports = schema; })(globalThis);