Merge pull request #711 from Tokisaki-Galaxy/feat/llm-model-fetch

feat: AI 辅助新增获取模型、测试连接与 Custom 提供商
This commit is contained in:
時崎 ギャラクシー authored and GitHub committed 2026-10-08 23:13:01 +08:00
1 parent 83a28afe2a
commit aa5c632ea9
21 files changed
+699 -38

No files matched your search

+56
View File
@@ -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": {
+3
View File
@@ -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<string> }
| { "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<number>; "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 };
+1 -1
View File
@@ -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"],"datLine truncated
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},"leftELine truncated
}
+1 -1
View File
@@ -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","propertLine truncated
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",Line truncated
root.MsimeMessageSchema = schema;
if (typeof module !== 'undefined' && module.exports) module.exports = schema;
})(globalThis);
@@ -499,6 +499,7 @@ token_deepseek = "<YOUR_AI_TOKEN_DEEPSEEK>"
token_openai = "<YOUR_AI_TOKEN_OPENAI>"
token_siliconflow = "<YOUR_AI_TOKEN_SILICONFLOW>"
token_groq = "<YOUR_AI_TOKEN_GROQ>"
token_custom = "<YOUR_AI_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 = """你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。
+3
View File
@@ -496,6 +496,7 @@ token_deepseek = "<YOUR_OWN_DEEPSEEK_TOKEN>"
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 = """你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。
+13 -4
View File
@@ -286,9 +286,15 @@ namespace
{
const std::vector<std::string_view> &AiAssistantProviders()
{
static const std::vector<std::string_view> providers{"deepseek", "openai", "siliconflow", "groq"};
static const std::vector<std::string_view> 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]);
+7 -3
View File
@@ -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<std::string, std::string> tokens;
@@ -71,11 +71,15 @@ struct AiAssistantConfig
std::map<std::string, std::string> 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<std::string, std::string> 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";
+105 -10
View File
@@ -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<std::string *>(user);
if (bytes > kMaxResponseBytes - (std::min)(response->size(), kMaxResponseBytes))
auto *response = static_cast<HttpResponse *>(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<long>(payload.size()));
if (payload)
{
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload->c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(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<std::string> 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<std::string>());
}
}
}
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<std::string>());
}
else if (item.is_string())
{
model_ids.push_back(item.get<std::string>());
}
}
}
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
@@ -2,6 +2,7 @@
#include <string>
#include <unordered_map>
#include <vector>
namespace ApiCredentialTest
{
@@ -17,5 +18,13 @@ struct Result
std::string message;
};
struct ModelListResult
{
bool ok = false;
std::string message;
std::vector<std::string> models;
};
Result Run(const Request &request);
ModelListResult FetchModels(const Request &request);
} // namespace ApiCredentialTest
+39
View File
@@ -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<std::string>(data.at("requestId"));
ApiCredentialTest::Request request;
request.service = json::value_to<std::string>(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<std::string>(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();
@@ -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;
@@ -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');
});
@@ -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<string, ProviderDefaults> = {
},
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<string, string> = {};
let endpoints: Record<string, string> = {};
let models: Record<string, string> = {};
@@ -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<string, string> {
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) => {
@@ -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<string, string> = {};
children: FakeElement[] = [];
className = '';
title = '';
classList = {
values: new Set<string>(),
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<typeof vi.fn>;
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('');
});
@@ -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<string, string>;
}
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;
}
@@ -211,6 +211,8 @@ function delay(ms: number): Promise<void> {
async function openDropdownMenu(menu: HTMLElement, menuId: string): Promise<void> {
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));
@@ -8,7 +8,7 @@
</div>
<div class="section ai-section">
<div class="section-title ai-heading">API 配置</div>
<div class="ai-description ai-api-description">DeepSeek、OpenAI、SiliconFlow、Groq 均使用 OpenAI 兼容的 Chat Completions 接口。每个提供商的 Token 分开保存。</div>
<div class="ai-description ai-api-description">DeepSeek、OpenAI、SiliconFlow、Groq 均使用 OpenAI 兼容的 Chat Completions 接口。Custom 用于接入任意兼容服务,填写自己的接口地址与模型。每个提供商的接口地址、Token 和模型分开保存。</div>
<div class="ai-grid">
<div class="ai-field">
<span>服务提供商</span>
@@ -22,10 +22,29 @@
<div class="dropdown-item" data-value="openai">OpenAI</div>
<div class="dropdown-item" data-value="siliconflow">SiliconFlow</div>
<div class="dropdown-item" data-value="groq">Groq</div>
<div class="dropdown-item" data-value="custom">Custom</div>
</div>
</div>
</div>
<label><span>模型</span><input id="aiModel" type="text" autocomplete="off" spellcheck="false"></label>
<div class="ai-field">
<span>模型</span>
<div class="ai-model-row">
<div class="dropdown ai-model-dropdown">
<div class="dropdown-toggle" id="aiModelBtn">
<input id="aiModel" type="text" autocomplete="off" spellcheck="false"
aria-label="模型名称" aria-controls="aiModelMenu" aria-autocomplete="list">
<div class="arrow"><img src="./assets/arrow.svg" alt=""></div>
</div>
<div class="dropdown-menu font-dropdown-menu compact-scroll-menu" id="aiModelMenu"></div>
</div>
<button id="aiModelFetchButton" class="ai-model-fetch-button" type="button" title="按当前服务商和凭据获取可用模型">获取模型</button>
</div>
<div class="ai-model-actions">
<button id="aiCredentialTestButton" class="credential-test-button" type="button">测试连接</button>
<span id="aiCredentialTestStatus" class="credential-test-status" role="status" aria-live="polite"></span>
</div>
<span id="aiModelFetchStatus" class="ai-model-fetch-status" role="status" aria-live="polite"></span>
</div>
<div class="ai-field">
<label for="aiToken">API Token</label>
<span class="ai-secret-input">
@@ -46,10 +65,6 @@
<label><span>候选数量</span><input id="aiCandidateLimit" type="number" min="1" max="10" autocomplete="off"></label>
<label class="ai-wide"><span>接口地址</span><input id="aiEndpoint" type="url" autocomplete="off" spellcheck="false"></label>
</div>
<div class="credential-test-row">
<button id="aiCredentialTestButton" class="credential-test-button" type="button">测试配置</button>
<span id="aiCredentialTestStatus" class="credential-test-status" role="status" aria-live="polite"></span>
</div>
</div>
<div class="section ai-section">
<div class="ai-prompt-header">
@@ -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;
+3
View File
@@ -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<string> }
| { "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<number>; "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 };
+1 -1
View File
@@ -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","propertLine truncated
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",Line truncated
root.MsimeMessageSchema = schema;
if (typeof module !== 'undefined' && module.exports) module.exports = schema;
})(globalThis);