Add installer instructions.

This commit is contained in:
fanlusky committed 2026-08-30 15:40:11 +08:00
commit d2a5320ec2
10 files changed
+1654

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
# Build and package outputs (collected locally, not part of this repo)
Output/
server_exe/
tsf_dll/
app_data/
MetasequoiaIME.ico
# Local test certificates (generated on each machine)
*.cer
*.crt
*.pfx
*.p12
*.pvk
*.spc
+93
View File
@@ -0,0 +1,93 @@
[CmdletBinding()]
param(
[string]$IsccPath,
[string]$IssPath = (Join-Path $PSScriptRoot 'msime_setup.iss')
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Resolve-IsccPath {
param([string]$ExplicitPath)
if ($ExplicitPath) {
if (-not (Test-Path -LiteralPath $ExplicitPath -PathType Leaf)) {
throw "指定的 Inno Setup 编译器不存在:$ExplicitPath"
}
return (Resolve-Path -LiteralPath $ExplicitPath).Path
}
$pathCommand = Get-Command 'ISCC.exe' -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($pathCommand) {
return $pathCommand.Source
}
$uninstallPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$candidates = @(
Get-ItemProperty -Path $uninstallPaths -ErrorAction SilentlyContinue |
Where-Object {
$displayName = $_.PSObject.Properties['DisplayName']
$installLocation = $_.PSObject.Properties['InstallLocation']
$displayName -and $displayName.Value -like 'Inno Setup*' -and
$installLocation -and $installLocation.Value
} |
Sort-Object {
$displayVersion = $_.PSObject.Properties['DisplayVersion']
try { [version]$displayVersion.Value } catch { [version]'0.0' }
} -Descending |
ForEach-Object {
Join-Path $_.PSObject.Properties['InstallLocation'].Value 'ISCC.exe'
}
)
$programRoots = @(
${env:ProgramFiles(x86)},
$env:ProgramFiles,
(Join-Path $env:LOCALAPPDATA 'Programs')
) | Where-Object { $_ }
foreach ($version in @(7, 6)) {
foreach ($root in $programRoots) {
$candidates += Join-Path $root "Inno Setup $version\ISCC.exe"
}
}
$candidate = $candidates |
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
Select-Object -First 1
if ($candidate) {
return $candidate
}
throw @'
找不到 Inno Setup 命令行编译器 ISCC.exe。请安装 Inno Setup 6.6 或更高版本,或通过
-IsccPath 参数传入 ISCC.exe 的完整路径。
'@
}
if (-not (Test-Path -LiteralPath $IssPath -PathType Leaf)) {
throw "Inno Setup 脚本不存在:$IssPath"
}
$resolvedIssPath = (Resolve-Path -LiteralPath $IssPath).Path
$resolvedIsccPath = Resolve-IsccPath -ExplicitPath $IsccPath
Write-Host "Inno Setup 编译器:$resolvedIsccPath"
Write-Host "安装脚本:$resolvedIssPath"
Push-Location $PSScriptRoot
try {
& $resolvedIsccPath $resolvedIssPath
if ($LASTEXITCODE -ne 0) {
throw "Inno Setup 编译失败,退出码:$LASTEXITCODE"
}
}
finally {
Pop-Location
}
Write-Host "安装包编译完成,输出目录:$(Join-Path $PSScriptRoot 'Output')"
+21
View File
@@ -0,0 +1,21 @@
# 启动本目录 Output\ 里刚编译好的安装包。
# 安装包文件名从 msime_setup.iss 的 MyAppVersion 读出。
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$issPath = Join-Path $PSScriptRoot 'msime_setup.iss'
if (-not (Test-Path -LiteralPath $issPath -PathType Leaf)) {
throw "找不到安装脚本:$issPath"
}
$issContent = Get-Content -LiteralPath $issPath -Raw
if ($issContent -notmatch '(?m)^#define\s+MyAppVersion\s+"(?<version>[^"]+)"') {
throw '未能在 msime_setup.iss 中找到 MyAppVersion。'
}
$installerPath = Join-Path $PSScriptRoot "Output\MetasequoiaIME_Setup_v$($Matches.version).exe"
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
throw "安装文件不存在,请先运行 Compile-Installer.ps1:$installerPath"
}
& $installerPath
+177
View File
@@ -0,0 +1,177 @@
[CmdletBinding()]
param(
[string]$TargetVersion = '0.0.1',
[string]$RepoRoot = (Split-Path -Parent $PSScriptRoot)
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Assert-PathExists {
param([Parameter(Mandatory)][string]$LiteralPath, [Parameter(Mandatory)][string]$Description)
if (-not (Test-Path -LiteralPath $LiteralPath)) {
throw "$Description 不存在:$LiteralPath"
}
}
function Copy-DirectoryContents {
param([Parameter(Mandatory)][string]$Source, [Parameter(Mandatory)][string]$Destination)
Assert-PathExists -LiteralPath $Source -Description '源目录'
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
Get-ChildItem -LiteralPath $Source -Force | Copy-Item -Destination $Destination -Recurse -Force
}
function Reset-Directory {
param([Parameter(Mandatory)][string]$LiteralPath)
if (Test-Path -LiteralPath $LiteralPath) {
Remove-Item -LiteralPath $LiteralPath -Recurse -Force
}
New-Item -ItemType Directory -Path $LiteralPath -Force | Out-Null
}
$serverRelease = Join-Path $RepoRoot 'MetasequoiaImeServer\build-release\bin\Release'
$dictionaryReplayRelease = Join-Path $serverRelease 'MetasequoiaImeDictionaryReplay.exe'
$tsf32Release = Join-Path $RepoRoot 'MetasequoiaImeTsf\build32-release\Release\MetasequoiaImeTsf.dll'
$tsf64Release = Join-Path $RepoRoot 'MetasequoiaImeTsf\build64-release\Release\MetasequoiaImeTsf.dll'
$webviewRoot = Join-Path $RepoRoot 'MetasequoiaImeUiHtml\webview2'
$serverConfig = Join-Path $RepoRoot 'MetasequoiaImeServer\assets\config\config.toml'
$factoryConfig = Join-Path $PSScriptRoot 'default_config\config.default.toml'
$pinyinTable = Join-Path $RepoRoot 'MetasequoiaImeServer\assets\tables\pinyin.txt'
$helpcodeSource = Join-Path $RepoRoot 'MetasequoiaImeHelpCode\helpcodes'
$appIcon = Join-Path $RepoRoot 'MetasequoiaImeServer\src\resource\MetasequoiaIME.ico'
$dictionaryDb = Join-Path $RepoRoot 'MetasequoiaImeDict\out\msime.db'
$japaneseModel = Join-Path $RepoRoot 'MetasequoiaImeDict\out\dict_japanese.dat'
$japaneseModelLicense = Join-Path $RepoRoot 'MetasequoiaImeDict\source\mozc_dictionary_oss\README.txt'
$englishDb = Join-Path $RepoRoot 'MetasequoiaImeDict\out\english.db'
$customTranslations = Join-Path $RepoRoot 'MetasequoiaImeDict\MetasequoiaImeCustomDict\translations.txt'
$othersDb = Join-Path $RepoRoot 'MetasequoiaImeDict\out\others.db'
Assert-PathExists -LiteralPath $RepoRoot -Description '源码仓库根目录'
Assert-PathExists -LiteralPath $serverRelease -Description 'Server Release 输出目录'
Assert-PathExists -LiteralPath $dictionaryReplayRelease -Description '用户词库回放程序 Release EXE'
Assert-PathExists -LiteralPath $tsf32Release -Description '32 位 TSF Release DLL'
Assert-PathExists -LiteralPath $tsf64Release -Description '64 位 TSF Release DLL'
Assert-PathExists -LiteralPath $serverConfig -Description 'Server config.toml'
Assert-PathExists -LiteralPath $factoryConfig -Description '出厂配置 default_config\config.default.toml'
Assert-PathExists -LiteralPath $pinyinTable -Description '完整拼音音节表 pinyin.txt'
Assert-PathExists -LiteralPath $helpcodeSource -Description '辅助码目录'
Assert-PathExists -LiteralPath $appIcon -Description '应用图标'
Assert-PathExists -LiteralPath $dictionaryDb -Description '词库数据库 msime.db'
Assert-PathExists -LiteralPath $japaneseModel -Description '日语整句模型 dict_japanese.dat'
Assert-PathExists -LiteralPath $japaneseModelLicense -Description 'Mozc 日语词典授权声明'
Assert-PathExists -LiteralPath $englishDb -Description '英文词库数据库 english.db'
Assert-PathExists -LiteralPath $customTranslations -Description '自定义翻译词库 translations.txt'
python -c @"
import sqlite3, sys
cols = list(sqlite3.connect(sys.argv[1]).execute('PRAGMA table_info(english_words)'))
names = {row[1] for row in cols}
pk = [row[1] for row in cols if row[5] > 0]
if 'weight' not in names or pk != ['word', 'display']:
raise SystemExit('english.db schema is stale; rebuild with weight and PRIMARY KEY(word, display)')
"@ $englishDb
if ($LASTEXITCODE -ne 0) {
throw "英文词库数据库 schema 检查失败:$englishDb"
}
Assert-PathExists -LiteralPath $othersDb -Description '杂项数据库 others.db'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'candwnd') -Description '候选窗 HTML 目录'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'ftb') -Description '悬浮工具栏 HTML 目录'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'menu') -Description '菜单 HTML 目录'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'settings\ime-settings\dist') -Description '设置页面 dist 目录'
$targetAppData = Join-Path $PSScriptRoot 'app_data'
$targetServer = Join-Path $PSScriptRoot 'server_exe'
$targetTsf = Join-Path $PSScriptRoot 'tsf_dll'
Reset-Directory -LiteralPath $targetAppData
if (-not (Get-Content -LiteralPath $pinyinTable | Where-Object { $_.Trim() -eq 'xing' })) {
throw "完整拼音音节表缺少 xing:$pinyinTable"
}
Copy-Item -LiteralPath $pinyinTable -Destination (Join-Path $targetAppData 'pinyin.txt') -Force
Copy-Item -LiteralPath $dictionaryDb -Destination (Join-Path $targetAppData 'msime.db') -Force
Copy-Item -LiteralPath $japaneseModel -Destination (Join-Path $targetAppData 'dict_japanese.dat') -Force
Copy-Item -LiteralPath $japaneseModelLicense -Destination (Join-Path $targetAppData 'MOZC_DICTIONARY_LICENSE.txt') -Force
Copy-Item -LiteralPath $englishDb -Destination (Join-Path $targetAppData 'english.db') -Force
Copy-Item -LiteralPath $customTranslations -Destination (Join-Path $targetAppData 'custom_translations.txt') -Force
Copy-Item -LiteralPath $othersDb -Destination (Join-Path $targetAppData 'others.db') -Force
$defaultConfigPath = Join-Path $targetAppData 'config.default.toml'
# 出厂配置来自本仓库的 default_config,不依赖本机是否已安装输入法。
# 安装脚本用 onlyifdoesntexist 生成用户 config.toml,升级不会覆盖已有方案/主题。
Copy-Item -LiteralPath $factoryConfig -Destination $defaultConfigPath -Force
$defaultConfig = Get-Content -LiteralPath $defaultConfigPath -Raw
if ($defaultConfig -notmatch '(?m)^schema\s*=\s*"quanpin"\s*$') {
throw '出厂配置的 input.schema 必须是 quanpin。'
}
if ($defaultConfig -notmatch '(?m)^theme_mode\s*=\s*"system"\s*$') {
throw '出厂配置的 appearance.theme_mode 必须是 system。'
}
if ($defaultConfig -match '(?m)^diagnostic_log\s*=\s*true\s*$') {
throw '出厂配置不应默认打开 diagnostic_log。'
}
$defaultConfig = $defaultConfig.TrimEnd("`r", "`n") + "`r`n"
Set-Content -LiteralPath $defaultConfigPath -Value $defaultConfig -Encoding utf8NoBOM -NoNewline
foreach ($stagedUserConfig in @('config.toml', 'config.base.toml')) {
$stagedPath = Join-Path $targetAppData $stagedUserConfig
if (Test-Path -LiteralPath $stagedPath) {
Remove-Item -LiteralPath $stagedPath -Force
}
}
$targetHelpcodes = Join-Path $targetAppData 'helpcodes'
Reset-Directory -LiteralPath $targetHelpcodes
Copy-DirectoryContents -Source $helpcodeSource -Destination $targetHelpcodes
$targetHtml = Join-Path $targetAppData 'html'
$targetWebview = Join-Path $targetHtml 'webview2'
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'candwnd') -Destination (Join-Path $targetWebview 'candwnd')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'ftb') -Destination (Join-Path $targetWebview 'ftb')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'menu') -Destination (Join-Path $targetWebview 'menu')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'settings\ime-settings\dist') `
-Destination (Join-Path $targetWebview 'settings\ime-settings\dist')
# Server Release 输出整体复制,但测试程序绝不能进入安装包。
Reset-Directory -LiteralPath $targetServer
Copy-DirectoryContents -Source $serverRelease -Destination $targetServer
Get-ChildItem -LiteralPath $targetServer -Recurse -File -Filter '*Tests.exe' |
Remove-Item -Force
Reset-Directory -LiteralPath $targetTsf
$targetTsf32 = Join-Path $targetTsf '32'
$targetTsf64 = Join-Path $targetTsf '64'
New-Item -ItemType Directory -Path $targetTsf32, $targetTsf64 -Force | Out-Null
Copy-Item -LiteralPath $tsf32Release -Destination $targetTsf32 -Force
Copy-Item -LiteralPath $tsf64Release -Destination $targetTsf64 -Force
Copy-Item -LiteralPath $appIcon -Destination (Join-Path $PSScriptRoot 'MetasequoiaIME.ico') -Force
$targetIss = Join-Path $PSScriptRoot 'msime_setup.iss'
Assert-PathExists -LiteralPath $targetIss -Description '安装脚本'
$issContent = Get-Content -LiteralPath $targetIss -Raw
if ($issContent -notmatch '(?m)^#define\s+MyAppVersion\s+"[^"]+"\s*$') {
throw '未能在安装脚本中找到 MyAppVersion。'
}
$updatedIss = [regex]::Replace(
$issContent,
'(?m)^#define\s+MyAppVersion\s+"[^"]+"\s*$',
"#define MyAppVersion `"$TargetVersion`""
)
$updatedIss = $updatedIss.TrimEnd("`r", "`n") + "`r`n"
Set-Content -LiteralPath $targetIss -Value $updatedIss -Encoding utf8NoBOM -NoNewline
# 设置页是已构建的静态资源,同步其“当前版本”展示。
$settingsDist = Join-Path $targetWebview 'settings\ime-settings\dist'
Get-ChildItem -LiteralPath $settingsDist -Recurse -File -Include '*.js', '*.html' | ForEach-Object {
$content = Get-Content -LiteralPath $_.FullName -Raw
$updated = [regex]::Replace(
$content,
'(<div class="about-version">)v\d+(?:\.\d+)+(</div>)',
"`$1v$TargetVersion`$2"
)
if ($updated -ne $content) {
Set-Content -LiteralPath $_.FullName -Value $updated -Encoding utf8NoBOM
}
}
$serverBinaryCount = @(Get-ChildItem -LiteralPath $targetServer -Recurse -File -Include '*.exe', '*.dll').Count
$tsfBinaryCount = @(Get-ChildItem -LiteralPath $targetTsf -Recurse -File -Include '*.exe', '*.dll').Count
Write-Host "安装文件准备完成:$PSScriptRoot"
Write-Host "版本:$TargetVersion;Server EXE/DLL:$serverBinaryCount 个;TSF EXE/DLL:$tsfBinaryCount 个。"
+73
View File
@@ -0,0 +1,73 @@
# Metasequoia IME Installer(本地测试流程)
公开这份仓库的目的,是把 Metasequoia IME 的 **本地测试安装流程** 整理清楚:从相邻源码仓库收集产物、用本机自签名证书签名、用 Inno Setup 打安装包、再跑安装程序。
测试版本号固定为 **0.0.1**。
本仓库 **不包含** 正式代码签名证书、私钥、指纹,也不包含编译好的 EXE/DLL、词库或安装包。签名脚本会在你自己的机器上生成并复用一张自签名测试证书。
## 依赖
- Windows 10/11,PowerShell 7(`pwsh`)
- [Inno Setup](https://jrsoftware.org/isinfo.php) 6.6 或更高版本
- Windows SDK(提供 `signtool.exe`)
- 下列源码仓库需要你自己准备,并先完成 Release 编译 / 词库构建:
- `MetasequoiaImeTsf`
- `MetasequoiaImeServer`
- `MetasequoiaImeUiHtml`
- `MetasequoiaImeHelpCode`
- `MetasequoiaImeDict`
## 请先改路径
脚本里写死的默认布局是:本仓库和上面那些项目都在**同一个父目录**下(例如都在 `IMECodes\` 里)。这只是作者本机的相对路径,**换到你自己的机器上通常对不上**。
请按自己的目录改这些地方,尤其是收集安装文件时的复制源:
- `Prepare-PackageFiles.ps1`:`$serverRelease`、`$tsf32Release`、`$tsf64Release`、`$webviewRoot`、`$helpcodeSource`、词库 `msime.db` / `english.db` 等,都是从其他项目里 **Copy** 过来的。路径不对就会直接报“不存在”。
- `test.ps1`:TSF / Server 的编译脚本、设置页的 `pnpm run build` 目录同样要改。
如果只是父目录不同、各子项目文件夹名没变,可以不改脚本,运行时传入:
```powershell
pwsh -File .\Prepare-PackageFiles.ps1 -RepoRoot "D:\your\src"
```
子项目不在同一父目录、或编译输出目录和默认不一致时,请直接改脚本里的那些路径。
## 一键测试
先确认上一节的路径已经改成你本机的。然后在管理员 PowerShell 中(把本机测试证书写入受信任存储需要提升权限):
```powershell
cd path\to\msime-installer
pwsh -File .\test.ps1
```
`test.ps1` 会按顺序做这些事:
1. 若找到相邻仓库,则编译 TSF、Server,并构建设置页
2. `Prepare-PackageFiles.ps1` — 收集 `server_exe\`、`tsf_dll\`、`app_data\`,并把 `msime_setup.iss` 的版本写成 `0.0.1`
3. `Sign-PackageBinaries-Local.ps1` — 本机自签名包内 EXE/DLL
4. `Compile-Installer.ps1` — 编译出 `Output\MetasequoiaIME_Setup_v0.0.1.exe`
5. `Sign-Installer-Local.ps1` — 本机自签名安装包
6. `Install.ps1` — 启动安装程序
也可以按上面的顺序逐步运行。
## 本机测试证书
第一次运行签名脚本时,会在当前用户证书存储里创建:
`CN=Metasequoia IME Local Test Code Signing`
之后复用同一张证书。产物只适合本机或已导入该证书公钥的测试机,不要当作正式分发包。
想让另一台测试机信任这张证书,把公钥导出成 `.cer` 后在那台机器上执行:
```powershell
certutil -addstore Root MetasequoiaImeLocalTest.cer
certutil -addstore TrustedPublisher MetasequoiaImeLocalTest.cer
```
不要把 `.cer` / `.pfx` 提交进本仓库。
+180
View File
@@ -0,0 +1,180 @@
# 用本机自签名测试证书给 Inno Setup 编译出来的安装包签名。
# 与 Sign-PackageBinaries-Local.ps1 共用同一张本机测试证书。
# 每台机器各自生成证书,本仓库不包含任何预置证书或指纹。
#
# 直接运行,不需要参数:
# pwsh -File .\Sign-Installer-Local.ps1
#
# 用的是和 Sign-PackageBinaries-Local.ps1 同一张测试证书:第一次运行时自动创建,
# 并装进本机受信任存储(这一步需要管理员 PowerShell;没有管理员权限只会警告,
# 签名照做,只是 signtool verify 的链校验过不了)。之后每次运行都会复用它。
#
# 安装包文件名从 msime_setup.iss 的 MyAppVersion 读出,免得两处版本号打架。
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$CertificateSubject = 'CN=Metasequoia IME Local Test Code Signing'
$CertificateValidityYears = 5
$CodeSigningEku = '1.3.6.1.5.5.7.3.3'
$TrustStores = @('Cert:\LocalMachine\Root', 'Cert:\LocalMachine\TrustedPublisher')
function Test-Administrator {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [System.Security.Principal.WindowsPrincipal]::new($identity)
return $principal.IsInRole(
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Find-SignTool {
$kitsBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
$candidate = Get-ChildItem -LiteralPath $kitsBin -Directory -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Name } catch { [version]'0.0' } } -Descending |
ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } |
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
Select-Object -First 1
if (-not $candidate) {
throw '找不到 signtool.exe;请安装 Windows SDK。'
}
return $candidate
}
function Get-LocalTestCertificate {
$now = Get-Date
$candidates = @(
Get-ChildItem -Path 'Cert:\CurrentUser\My' |
Where-Object {
$_.Subject -eq $CertificateSubject -and
$_.HasPrivateKey -and
$_.NotBefore -le $now -and
$_.NotAfter -gt $now -and
($_.EnhancedKeyUsageList.ObjectId -contains $CodeSigningEku)
} |
Sort-Object NotAfter -Descending
)
if ($candidates.Count -eq 0) {
return $null
}
return $candidates[0]
}
function Add-CertificateTrust {
param([Parameter(Mandatory)]$Certificate)
$missing = @(
$TrustStores | Where-Object {
-not (Test-Path -LiteralPath (Join-Path $_ $Certificate.Thumbprint))
}
)
if ($missing.Count -eq 0) {
Write-Host '证书已在本机受信任存储中,跳过信任步骤。'
return
}
if (-not (Test-Administrator)) {
Write-Warning @"
证书还没进本机受信任存储,而写入它需要管理员权限:$($missing -join '、')
请用管理员 PowerShell 重新运行一次本脚本。在那之前签名依然有效,只是链校验会失败。
"@
return
}
$temporaryFile = Join-Path ([System.IO.Path]::GetTempPath()) `
('msime-local-test-{0}.cer' -f $Certificate.Thumbprint)
try {
Export-Certificate -Cert $Certificate -FilePath $temporaryFile -Type CERT -Force | Out-Null
foreach ($store in $missing) {
Import-Certificate -FilePath $temporaryFile -CertStoreLocation $store | Out-Null
Write-Host "已信任:$store"
}
}
finally {
if (Test-Path -LiteralPath $temporaryFile) {
Remove-Item -LiteralPath $temporaryFile -Force
}
}
}
function Initialize-LocalTestCertificate {
$certificate = Get-LocalTestCertificate
if ($certificate) {
Write-Host '已有本机测试证书,跳过创建。'
}
else {
# -Type CodeSigningCert 负责代码签名 EKU;Exportable 是为了以后能导出公钥给测试机。
$certificate = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject $CertificateSubject `
-FriendlyName 'Metasequoia IME Local Test Code Signing' `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-HashAlgorithm SHA256 `
-KeyExportPolicy Exportable `
-KeyUsage DigitalSignature `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-NotAfter (Get-Date).AddYears($CertificateValidityYears)
Write-Host '已创建本机测试证书。'
}
Write-Host "指纹:$($certificate.Thumbprint)"
Write-Host "有效期至:$($certificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'))"
Add-CertificateTrust -Certificate $certificate
return $certificate
}
function Test-SignedByLocalTestCertificate {
param([Parameter(Mandatory)][string]$LiteralPath)
$signature = Get-AuthenticodeSignature -LiteralPath $LiteralPath
if (-not $signature.SignerCertificate) {
return $false
}
# 不看 Status:测试证书没装进受信任根时链校验必然失败,但文件确实已由它签过名。
return $signature.SignerCertificate.Thumbprint.Replace(' ', '') -ieq $script:thumbprint
}
function Invoke-Sign {
param([Parameter(Mandatory)][string]$LiteralPath)
# 自签名证书的时间戳没有意义,测试机还可能离线,所以不加 /tr。
& $script:signTool sign /sha1 $script:thumbprint /s My /fd sha256 /v $LiteralPath
if ($LASTEXITCODE -ne 0) {
throw "signtool.exe 签名失败,退出码:$LASTEXITCODE"
}
& $script:signTool verify /pa /v $LiteralPath
if ($LASTEXITCODE -ne 0) {
Write-Warning "已签名但链校验失败(测试证书尚未被本机信任):$LiteralPath"
}
}
$issPath = Join-Path $PSScriptRoot 'msime_setup.iss'
if (-not (Test-Path -LiteralPath $issPath -PathType Leaf)) {
throw "找不到安装脚本:$issPath"
}
$issContent = Get-Content -LiteralPath $issPath -Raw
if ($issContent -notmatch '(?m)^#define\s+MyAppVersion\s+"(?<version>[^"]+)"') {
throw '未能在 msime_setup.iss 中找到 MyAppVersion。'
}
$version = $Matches.version
$installerPath = Join-Path $PSScriptRoot "Output\MetasequoiaIME_Setup_v$version.exe"
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
throw "安装文件不存在,请先用 Inno Setup 编译 msime_setup.iss:$installerPath"
}
$resolvedInstaller = (Resolve-Path -LiteralPath $installerPath).Path
Write-Warning '使用本机测试证书签名,安装包只能内部测试,不要对外分发。'
$certificate = Initialize-LocalTestCertificate
$script:thumbprint = $certificate.Thumbprint
$script:signTool = Find-SignTool
if (Test-SignedByLocalTestCertificate -LiteralPath $resolvedInstaller) {
Write-Host "安装文件已由本机测试证书签名,跳过:$resolvedInstaller"
}
else {
Write-Host "正在签名安装文件:$resolvedInstaller"
Invoke-Sign -LiteralPath $resolvedInstaller
}
Write-Host '安装文件本地签名检查完成。'
# 链校验警告会让最后一个 signtool 留下非零退出码;对本脚本来说整体是成功的。
$global:LASTEXITCODE = 0
+194
View File
@@ -0,0 +1,194 @@
# 用本机自签名测试证书给安装文件(server_exe\ 与 tsf_dll\)里的 EXE/DLL 签名。
# 每台机器各自生成证书,本仓库不包含任何预置证书或指纹。
#
# 直接运行,不需要参数:
# pwsh -File .\Sign-PackageBinaries-Local.ps1
#
# 证书在第一次运行时自动创建,并装进本机受信任存储(这一步需要管理员 PowerShell;
# 没有管理员权限只会警告,签名照做,只是 signtool verify 的链校验过不了)。
# 之后每次运行都会复用同一张证书,已经被它签过的文件也会自动跳过。
#
# 每台机器各自生成自己的测试证书,不需要在机器之间搬私钥。想让纯测试机信任本机签名,
# 把证书的公钥导出成 .cer 拷过去,在那台机器上用 certutil 装进受信任根即可:
# certutil -addstore Root MetasequoiaImeLocalTest.cer
# certutil -addstore TrustedPublisher MetasequoiaImeLocalTest.cer
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$CertificateSubject = 'CN=Metasequoia IME Local Test Code Signing'
$CertificateValidityYears = 5
$CodeSigningEku = '1.3.6.1.5.5.7.3.3'
$TrustStores = @('Cert:\LocalMachine\Root', 'Cert:\LocalMachine\TrustedPublisher')
function Test-Administrator {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [System.Security.Principal.WindowsPrincipal]::new($identity)
return $principal.IsInRole(
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Find-SignTool {
$kitsBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
$candidate = Get-ChildItem -LiteralPath $kitsBin -Directory -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Name } catch { [version]'0.0' } } -Descending |
ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } |
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
Select-Object -First 1
if (-not $candidate) {
throw '找不到 signtool.exe;请安装 Windows SDK。'
}
return $candidate
}
function Get-LocalTestCertificate {
$now = Get-Date
$candidates = @(
Get-ChildItem -Path 'Cert:\CurrentUser\My' |
Where-Object {
$_.Subject -eq $CertificateSubject -and
$_.HasPrivateKey -and
$_.NotBefore -le $now -and
$_.NotAfter -gt $now -and
($_.EnhancedKeyUsageList.ObjectId -contains $CodeSigningEku)
} |
Sort-Object NotAfter -Descending
)
if ($candidates.Count -eq 0) {
return $null
}
return $candidates[0]
}
function Add-CertificateTrust {
param([Parameter(Mandatory)]$Certificate)
$missing = @(
$TrustStores | Where-Object {
-not (Test-Path -LiteralPath (Join-Path $_ $Certificate.Thumbprint))
}
)
if ($missing.Count -eq 0) {
Write-Host '证书已在本机受信任存储中,跳过信任步骤。'
return
}
if (-not (Test-Administrator)) {
Write-Warning @"
证书还没进本机受信任存储,而写入它需要管理员权限:$($missing -join '、')
请用管理员 PowerShell 重新运行一次本脚本。在那之前签名依然有效,只是链校验会失败。
"@
return
}
$temporaryFile = Join-Path ([System.IO.Path]::GetTempPath()) `
('msime-local-test-{0}.cer' -f $Certificate.Thumbprint)
try {
Export-Certificate -Cert $Certificate -FilePath $temporaryFile -Type CERT -Force | Out-Null
foreach ($store in $missing) {
Import-Certificate -FilePath $temporaryFile -CertStoreLocation $store | Out-Null
Write-Host "已信任:$store"
}
}
finally {
if (Test-Path -LiteralPath $temporaryFile) {
Remove-Item -LiteralPath $temporaryFile -Force
}
}
}
function Initialize-LocalTestCertificate {
$certificate = Get-LocalTestCertificate
if ($certificate) {
Write-Host '已有本机测试证书,跳过创建。'
}
else {
# -Type CodeSigningCert 负责代码签名 EKU;Exportable 是为了以后能导出公钥给测试机。
$certificate = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject $CertificateSubject `
-FriendlyName 'Metasequoia IME Local Test Code Signing' `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-HashAlgorithm SHA256 `
-KeyExportPolicy Exportable `
-KeyUsage DigitalSignature `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-NotAfter (Get-Date).AddYears($CertificateValidityYears)
Write-Host '已创建本机测试证书。'
}
Write-Host "指纹:$($certificate.Thumbprint)"
Write-Host "有效期至:$($certificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'))"
Add-CertificateTrust -Certificate $certificate
return $certificate
}
function Test-SignedByLocalTestCertificate {
param([Parameter(Mandatory)][string]$LiteralPath)
$signature = Get-AuthenticodeSignature -LiteralPath $LiteralPath
if (-not $signature.SignerCertificate) {
return $false
}
# 不看 Status:测试证书没装进受信任根时链校验必然失败,但文件确实已由它签过名。
return $signature.SignerCertificate.Thumbprint.Replace(' ', '') -ieq $script:thumbprint
}
function Invoke-Sign {
param([Parameter(Mandatory)][string]$LiteralPath)
# 自签名证书的时间戳没有意义,测试机还可能离线,所以不加 /tr。
& $script:signTool sign /sha1 $script:thumbprint /s My /fd sha256 /v $LiteralPath
if ($LASTEXITCODE -ne 0) {
throw "signtool.exe 签名失败,退出码:$LASTEXITCODE"
}
& $script:signTool verify /pa /v $LiteralPath
if ($LASTEXITCODE -ne 0) {
Write-Warning "已签名但链校验失败(测试证书尚未被本机信任):$LiteralPath"
}
}
$packageDirectories = @(
(Join-Path $PSScriptRoot 'server_exe'),
(Join-Path $PSScriptRoot 'tsf_dll')
)
foreach ($directory in $packageDirectories) {
if (-not (Test-Path -LiteralPath $directory -PathType Container)) {
throw "待签名目录不存在,请先运行 Prepare-PackageFiles.ps1:$directory"
}
}
# 最外层的 @() 不能省:只有一个待签文件时,管道结果会是标量,.Count 在严格模式下会报错。
$targets = @(
@(
foreach ($directory in $packageDirectories) {
Get-ChildItem -LiteralPath $directory -Recurse -File |
Where-Object { $_.Extension -in @('.exe', '.dll') }
}
) | Sort-Object FullName -Unique
)
if ($targets.Count -eq 0) {
throw '没有找到待签名的 EXE 或 DLL。'
}
Write-Warning '使用本机测试证书签名,产物只能内部测试,不要对外分发。'
$certificate = Initialize-LocalTestCertificate
$script:thumbprint = $certificate.Thumbprint
$script:signTool = Find-SignTool
$signedCount = 0
$skippedCount = 0
foreach ($target in $targets) {
if (Test-SignedByLocalTestCertificate -LiteralPath $target.FullName) {
Write-Host "已由本机测试证书签名,跳过:$($target.FullName)"
$skippedCount++
continue
}
Write-Host "正在签名:$($target.FullName)"
Invoke-Sign -LiteralPath $target.FullName
$signedCount++
}
Write-Host "包内 EXE/DLL 本地签名完成:新签名 $signedCount 个,已签名跳过 $skippedCount 个,总计 $($targets.Count) 个文件。"
# 链校验警告会让最后一个 signtool 留下非零退出码;对本脚本来说整体是成功的。
$global:LASTEXITCODE = 0
@@ -0,0 +1,341 @@
#
# Settings page
# 设置页面
#
[settings_page]
theme = "dark"
#
# General
# 通用
#
[general]
# 界面诊断日志总开关(默认关闭;不记录输入内容或候选文本)
diagnostic_log = false
# 候选框诊断日志(旧键,与 diagnostic_log 同步;默认关闭)
candidate_window_diagnostic_log = false
# 悬浮工具栏
floating_toolbar = true
# 悬浮工具栏组件(中英文切换始终显示)
floating_toolbar_fullwidth = true
floating_toolbar_punctuation = true
floating_toolbar_character_set = true
floating_toolbar_emoji = true
floating_toolbar_screen_keyboard = false
floating_toolbar_settings = true
# 悬浮工具栏用户缩放(相对系统 DPI),可选:0.75 / 1.0 / 1.25 / 1.5
floating_toolbar_scale = 1.0
# 悬浮工具栏图标尺寸(px,再乘 scale),可选:16–28
floating_toolbar_font_size = 24
# Emoji
enable_emoji = true
# 翻页方式
paging_minus_equal = true
paging_comma_period = true
paging_tab = true
paging_page_up_down = true
candidate_arrow_navigation = true
# 中文输入状态下显示英文前缀候选
cn_en_mixed_input = true
# 中英混输触发所需的最少字母数,范围 1–8
cn_en_mixed_input_min_chars = 2
# 在竖排候选窗口中显示中英文互译(每项最多两个简短释义)
candidate_translations = true
# 中文输入状态下显示 emoji 前缀候选(第三个候选项,随云候选/AI 联想顺移)
emoji_mixed_input = true
# 中文输入状态下显示颜文字前缀候选(排在 emoji 之后)
kaomoji_mixed_input = false
# 云候选
cloud_candidates = true
# TODO: 极简模式。极简模式下,候选窗口使用 d2d 绘制,界面只作展示候选项,无其他功能。
clean_mode = false
#
# 腾讯云机器翻译(竖排候选项本地词库未命中时的补全)
#
[tencent_tmt]
enabled = true
# 请自行填写腾讯云密钥,不要把真实密钥写进仓库。
secret_id = "<YOUR_TENCENT_SECRET_ID>"
secret_key = "<YOUR_TENCENT_SECRET_KEY>"
region = "ap-guangzhou"
[frequency_adjustment]
# disabled / pin / halve / linear / promote
mode = "promote"
# 每选中几次执行一次调频,范围 1~10
trigger_count = 1
# 线性调频每次前移数量,范围 1~10
linear_step = 1
#
# Appearance
# 外观
#
[appearance]
# 主题模式,可选:"dark", "light", "system"
theme_mode = "system"
# 分表面主题,可选:"follow", "dark", "light"(follow = 跟随 theme_mode)
theme_settings = "follow"
theme_cand = "follow"
theme_ftb = "follow"
theme_menu = "follow"
theme_emoji = "follow"
theme_screen_keyboard = "follow"
theme_handwriting = "follow"
theme_voice = "follow"
# 字体(中文)
font = "Noto Sans SC"
# 英文字体(拉丁字母优先使用;汉字仍回落到 font)
english_font = "Segoe UI"
# 缺省字体,如果系统中没有找到指定字体,则使用缺省字体
default_font = "Microsoft YaHei"
# 字体大小(px),可选:12–32
font_size = 18
# 候选窗口顶部预编辑字号(px),可选:12–32
candidate_window_preedit_font_size = 15
# 候选文字颜色,"auto" 跟随主题,或 #RRGGBB
cand_text_color = "auto"
# 每页候选词数量,可选:3, 4, 5, 6, 7, 8, 9
page_size = 6
# 候选窗口跟随光标
candidate_window_follow_cursor = true
# 候选窗口排列方式,可选:"vertical", "horizontal"
candidate_window_layout = "vertical"
# 候选窗口与悬浮工具栏皮肤,可选:"fluent", "wechat"
candidate_skin = "willow_green"
# 候选窗口顶部预编辑,可选:"pinyin", "empty"
# pinyin -> 显示分词预编辑(与候选框当前行为一致)
# empty -> 隐藏顶部预编辑行
candidate_window_preedit_style = "pinyin"
# TSF 行内预编辑样式,可选:"raw", "pinyin", "empty"
# raw -> 原始按键序列
# pinyin -> 分词后的拼音序列(Server 回包 Preedit)
# empty -> 行内不显示预编辑
tsf_preedit_style = "raw"
# tsf_preedit_style = "pinyin"
# tsf_preedit_style = "empty"
#
# Input
# 输入
#
[input]
character_set = "simplified" # 候选输出字形,可选:"simplified", "traditional"
# 启动/从其他输入法切换到本输入法时的默认中英文,可选:"chinese", "english"
default_ime_mode = "english"
# 中英文状态范围,可选:"app"(按应用分别记忆), "global"(所有应用保持统一)
ime_mode_scope = "app"
# 以词定字:开启后,[ 上屏高亮候选的首个汉字,] 上屏末个汉字
word_to_character = true
# 智能标点:中文标点模式下,字母/数字后的 , . : 输出英文标点(类似小狼毫 digit_separators)
smart_punctuation = true
# 重复标点转中文:智能标点输出英文标点后,2 秒内再次输入同一标点时替换为中文标点
smart_punctuation_repeat_to_chinese = true
# 成对标点补全:输入左侧符号时自动补全右侧符号,并将光标置于中间
paired_punctuation = true
# 组合说明:
# legacy + shuangpin -> 使用旧版双拼会话实现(当前最稳定)
# legacy + quanpin -> 自动切换为新版引擎全拼会话
# engine + shuangpin -> 使用新版引擎双拼会话
# engine + quanpin -> 使用新版引擎全拼会话
# 任意 backend + wubi -> 使用新版引擎五笔会话
# 会话后端,可选:"legacy", "engine"
# session_backend = "legacy"
session_backend = "engine"
# 输入模式,可选:"chinese", "japanese"。切换模式时保留各语言上次使用的方案。
mode = "chinese"
# 输入方案,可选:"quanpin", "shuangpin", "wubi"
schema = "quanpin"
# schema = "quanpin"
# schema = "wubi"
# engine + shuangpin 时的 preedit 显示方式,可选:"quanpin", "shuangpin"
# quanpin -> 预编辑显示转换后的全拼分词
# shuangpin -> 预编辑显示原始双拼输入串
shuangpin_preedit_mode = "shuangpin"
# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft"
# shuangpin_schema = "xiaohe"
shuangpin_schema = "xiaohe"
# 五笔方案,可选:"wubi86", "wubi98"
wubi_schema = "wubi86"
# 日语方案,当前支持:"romaji"
japanese_schema = "romaji"
#
# Helpcode
# 辅助码
#
[helpcode]
# 双拼辅助码
shuangpin_helpcode = true
# 双拼辅助码方案,可选:"lantian", "ziranma", "shouyou2_0", "shouyouplus", "xiaohe"
shuangpin_helpcode_schema = "lantian"
# 是否在候选窗口中显示双拼辅助码
show_sp_helpcode_in_candidate_window = true
# 全拼辅助码
quanpin_helpcode = true
# 全拼辅助码方案,可选:"lantian", "ziranma", "shouyou2_0", "shouyouplus", "xiaohe"
quanpin_helpcode_schema = "ziranma"
# 是否在候选窗口中显示全拼辅助码
show_qp_helpcode_in_candidate_window = false
#
# Keybindings
# 快捷键
#
[keybindings]
# 中英文切换快捷键(Ctrl+Space 由 Windows 系统管理,此处不控制)
switch_language_shift = true
switch_language_ctrl = false
switch_language_ctrl_alt_space = true
#
# Dictionary
# 词库
#
[dictionary]
# 词库路径
dictionary_path = "./assets/dictionaries/msime.db"
#
# Skin
# 皮肤
#
[skin]
# 皮肤名称
skin_name = "default"
#
# Voice Input
# 语音输入
#
[voice_input]
# 语音输入
voice_input = true
# 全局语音快捷键(可分别启用或关闭)
hotkey_ralt = true
hotkey_ctrl_f9 = true
hotkey_ctrl_win = false
hotkey_rctrl_ralt = false
hotkey_hold_space_lock = true
# ASR 服务: doubao | openai | siliconflow | groq
# openai / siliconflow / groq 均走 OpenAI 兼容的 /v1/audio/transcriptions。
asr_provider = "doubao"
# 豆包旧版控制台鉴权:App ID + Access Token(Secret Key 不使用)
# 新版控制台使用单 API Key 时,将 asr_app_key 留空并把 API Key 填入 asr_token。
# 请自行填写各提供商的密钥,不要把真实密钥写进仓库。
asr_app_key = "<YOUR_ASR_APP_KEY>"
asr_token = "FAKESECRET_k4l5m6n7o8p9q0r1s2t3"
# 每个 ASR 提供商单独保存 Token,切换提供商只改当前使用哪一份,不会互相覆盖。
asr_token_doubao = "<YOUR_ASR_TOKEN_DOUBAO>"
asr_token_openai = "<YOUR_ASR_TOKEN_OPENAI>"
asr_token_siliconflow = "<YOUR_ASR_TOKEN_SILICONFLOW>"
asr_token_groq = "<YOUR_ASR_TOKEN_GROQ>"
asr_endpoint = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
# 默认使用豆包流式语音识别模型 2.0 小时版
asr_resource_id = "volc.seedasr.sauc.duration"
# 豆包识别选项:数字格式化(ITN)、标点预测默认开启,语义顺滑(DDC)默认关闭。
doubao_enable_itn = true
doubao_enable_punc = true
doubao_enable_ddc = true
# 在豆包语音控制台“自学习平台 -> 热词管理”创建词表后,将热词 ID 填在这里;留空则不使用。
doubao_boosting_table_id = ""
# OpenAI 兼容识别模型。豆包流式不使用此项;留空则按提供商填默认值。
# SiliconFlow 当前默认使用 FunAudioLLM/SenseVoiceSmall,也可在这里填写其他模型名。
asr_model = ""
# 文本润色: siliconflow | openai | deepseek | groq
# 均走 OpenAI 兼容的 /v1/chat/completions。
polish_provider = "deepseek"
# 请自行填写各提供商的 API Token,不要把真实密钥写进仓库。
polish_token = "FAKESECRET_q2r3s4t5u6v7w8x9y0z1"
polish_token_siliconflow = "<YOUR_POLISH_TOKEN_SILICONFLOW>"
polish_token_openai = "<YOUR_POLISH_TOKEN_OPENAI>"
polish_token_deepseek = "<YOUR_POLISH_TOKEN_DEEPSEEK>"
polish_token_groq = "<YOUR_POLISH_TOKEN_GROQ>"
polish_endpoint = "https://api.deepseek.com/chat/completions"
# 留空则按提供商填默认值,例如 Qwen/Qwen3-8B、gpt-4o-mini。
polish_model = "deepseek-v4-flash"
# 润色提示词: cleanup | faithful | zh2en | casual | custom_1 | custom_2 | custom_3
polish_prompt_id = "cleanup"
# 旧版单槽位提示词,升级时作为“自定义一”的回退值。
polish_prompt = ""
polish_prompt_custom_1 = ""
polish_prompt_custom_2 = ""
polish_prompt_custom_3 = ""
language = "zh-cn"
# 开始和结束录音提示音可分别关闭
start_sound = true
end_sound = true
# 录音时暂时静音其他应用的播放声音,结束后自动恢复
mute_system_audio = true
polish_text = true
# 豆包流式识别时,将已识别文本显示在输入框 inline preedit 中,松开录音键后上屏。
# 仅在豆包流式识别且 commit_mode = "tsf" 时生效。
stream_inline_preedit = true
# 上屏方式: tsf | sendinput | ctrl_v
commit_mode = "tsf"
#
# Soft Keyboard
# 屏幕键盘
#
[soft_keyboard]
theme_mode = "dark"
background_img = "" # 默认是空,没有背景图片
#
# Utility
# 实用功能
#
[utility]
clipboard_history = true # 表情面板剪贴板历史,仅记录文本;关闭后立即清空
study_english_word = "false" # 背单词功能,开启后可以在候选项旁边显示英文
quick_phrase = true # Shift+K 进入快捷短语模式
unicode_mode = true # Shift+U 进入 Unicode 码位输入模式
date_time_mode = true # Shift+T 进入日期与时间快捷输入模式
emoji_mode = true # Shift+E 进入 Emoji 快捷输入模式(支持全拼/简拼/双拼/英文)
kaomoji_mode = true # Shift+M 进入颜文字快捷输入模式(支持全拼/简拼/双拼/英文)
jianpin_mode = true # Shift+J 进入超级简拼模式(每个字母作为简拼;双拼按方案转换 zh/ch/sh)
y_mode = true # Shift+Y 进入临时英文模式(中文状态下直接打英文,上屏后回到中文)
r_mode = true # Shift+R 进入临时日语模式(中文状态下使用罗马字输入日语,上屏后回到中文)
#
# AI Assistant
# AI 辅助
#
[ai_assistant]
enabled = true
provider = "deepseek"
# 请自行填写各提供商的 API Token,不要把真实密钥写进仓库。
token = "FAKESECRET_e1f2g3h4i5j6k7l8m9n0"
token_deepseek = "<YOUR_AI_TOKEN_DEEPSEEK>"
token_openai = "<YOUR_AI_TOKEN_OPENAI>"
token_siliconflow = "<YOUR_AI_TOKEN_SILICONFLOW>"
token_groq = "<YOUR_AI_TOKEN_GROQ>"
endpoint = "https://api.deepseek.com/chat/completions"
model = "deepseek-v4-flash"
candidate_limit = 3
prompt_id = "custom_1"
prompt = """你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。
优先生成与拼音严格对应的中文候选:若有 N 段拼音,首选必须尽量为 N 个汉字,每段拼音对应一个汉字,不得随意增删或改变读音。结合上下文、常用程度、语义完整性和固定搭配排序。
若去掉分词后能明显组成更合理的英文单词、缩写、产品名或技术术语,如 `deep + seek → DeepSeek`、`git + hub → GitHub`,可优先返回英文;不要生造英文或做牵强匹配。
只输出合法 JSON,不要解释或输出 Markdown:
{
"candidates": [
{
"text": "候选内容",
"type": "chinese或english",
"confidence": 0.98
}
]
}
候选按推荐程度降序排列,数量不超过指定上限;没有合理结果时返回空数组。"""
# prompt 是旧版“自定义一”;首次修改后会写入下面的独立槽位。
prompt_custom_1 = ""
prompt_custom_2 = ""
prompt_custom_3 = ""
+518
View File
@@ -0,0 +1,518 @@
; Metasequoia IME — Inno Setup script
; 源文件根目录:本脚本所在目录
;
; 编译方法:
; 1. 安装 Inno Setup 6.6 或更高版本:https://jrsoftware.org/isinfo.php
; 2. 运行 .\Compile-Installer.ps1(或用 Inno Setup Compiler 打开本文件并按 Ctrl+F9)
; 输出安装包默认在:Output\
;
; 本地测试打包顺序:
; 1. Prepare-PackageFiles.ps1 收集本版本的安装文件
; 2. Sign-PackageBinaries-Local.ps1 用本机自签名测试证书给包内 EXE/DLL 签名
; 3. 本文件用 Inno Setup 编译
; 4. Sign-Installer-Local.ps1 用同一张本机测试证书给安装包签名
;
; 也可以直接运行 .\test.ps1 走完整测试流程。
; 本仓库不包含任何预置代码签名证书。
#define MyAppName "Metasequoia IME"
#define MyAppVersion "0.0.1"
#define MyAppPublisher "Metasequoia"
#define MyAppExeName "MetasequoiaImeServer.exe"
#define MySettingsExeName "MetasequoiaImeSettings.exe"
#define MyWatchdogName "MetasequoiaImeWatchdog.exe"
#define MyWatchdogTaskName "Metasequoia IME Watchdog"
#define MyReplayName "MetasequoiaImeDictionaryReplay.exe"
#define MyVersionDirBase "msime_v" + MyAppVersion
#define MySourceRoot "."
[Setup]
AppId={{A7C3E91F-4B2D-4E8A-9F1C-6D5E8B0A2C4D}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\metasequoiaime
DefaultGroupName={#MyAppName}
DisableDirPage=yes
DisableProgramGroupPage=yes
OutputDir=Output
OutputBaseFilename=MetasequoiaIME_Setup_v{#MyAppVersion}
SetupIconFile={#MySourceRoot}\MetasequoiaIME.ico
Compression=lzma2
SolidCompression=yes
; 安装和卸载界面自动跟随 Windows 的浅色/深色模式。
WizardStyle=modern dynamic
PrivilegesRequired=admin
UsedUserAreasWarning=no
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={commonpf64}\metasequoiaime\MetasequoiaIME.ico
VersionInfoVersion={#MyAppVersion}
[Languages]
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
[Dirs]
Name: "{commonpf32}\metasequoiaime\{code:GetVersionDir}"
Name: "{commonpf64}\metasequoiaime\{code:GetVersionDir}"
Name: "{commonpf64}\metasequoiaime\server"
Name: "{localappdata}\metasequoiaime"
; WebView2 子进程是中完整性,写不进内置 Administrator 的高完整性 LocalAppData。
Name: "{commonappdata}\metasequoiaime"
Name: "{commonappdata}\metasequoiaime\webview2"; Permissions: users-modify
Name: "{commonappdata}\metasequoiaime\webview2-settings"; Permissions: users-modify
[Files]
; 独立安装应用图标,供 Windows“已安装的应用”列表稳定显示。
Source: "{#MySourceRoot}\MetasequoiaIME.ico"; \
DestDir: "{commonpf64}\metasequoiaime"; Flags: ignoreversion
; TSF DLL 使用版本独立目录,避免升级时覆盖仍被进程加载的 DLL。
Source: "{#MySourceRoot}\tsf_dll\32\*.dll"; \
DestDir: "{commonpf32}\metasequoiaime\{code:GetVersionDir}"; \
Flags: ignoreversion regserver 32bit
Source: "{#MySourceRoot}\tsf_dll\64\*.dll"; \
DestDir: "{commonpf64}\metasequoiaime\{code:GetVersionDir}"; \
Flags: ignoreversion regserver
Source: "{#MySourceRoot}\server_exe\*"; \
DestDir: "{commonpf64}\metasequoiaime\server"; \
Flags: ignoreversion recursesubdirs createallsubdirs
; 包内故意不带 config.toml。通配复制再排除一次,防止以后又把用户配置打进包内。
Source: "{#MySourceRoot}\app_data\*"; DestDir: "{localappdata}\metasequoiaime"; \
Excludes: "\config.toml,\config.base.toml,\config.default.toml"; \
Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall
; 用户配置只在首次安装时从出厂模板生成。升级时绝不覆盖已有 config.toml;
; Server 启动时再以 config.default.toml 合并:保留用户改过的值,带入新版新增项。
Source: "{#MySourceRoot}\app_data\config.default.toml"; \
DestDir: "{localappdata}\metasequoiaime"; DestName: "config.toml"; \
Flags: onlyifdoesntexist uninsneveruninstall
Source: "{#MySourceRoot}\app_data\config.default.toml"; \
DestDir: "{localappdata}\metasequoiaime"; DestName: "config.default.toml"; \
Flags: ignoreversion uninsneveruninstall
[Icons]
Name: "{group}\{#MyAppName}"; \
Filename: "{commonpf64}\metasequoiaime\server\{#MySettingsExeName}"; \
WorkingDir: "{commonpf64}\metasequoiaime\server"
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
[Registry]
Root: HKLM; Subkey: "Software\Metasequoia\MetasequoiaIME"; \
ValueType: string; ValueName: "VersionDir"; ValueData: "{code:GetVersionDir}"; \
Flags: uninsdeletevalue
Root: HKLM; Subkey: "Software\Metasequoia\MetasequoiaIME"; \
ValueType: string; ValueName: "ServerPath"; \
ValueData: "{commonpf64}\metasequoiaime\server\{#MyAppExeName}"; \
Flags: uninsdeletevalue
[Run]
; uiAccess=true 的程序不能用 CreateProcess 拉起(会报 740),必须用 shellexec(等同双击)。
; postinstall 默认 runasoriginaluser:不要用安装器的提升令牌启动 WebView2。
Filename: "{commonpf64}\metasequoiaime\server\{#MyAppExeName}"; \
Description: "启动 Metasequoia IME Server"; \
Flags: nowait postinstall skipifsilent shellexec runasoriginaluser
Filename: "{commonpf64}\metasequoiaime\server\{#MyWatchdogName}"; \
Description: "启动 Metasequoia IME Watchdog"; \
Flags: nowait postinstall skipifsilent shellexec runasoriginaluser
[Code]
var
VersionDirName: String;
function GetVersionDir(Param: String): String;
var
Candidate: String;
Suffix: Integer;
begin
if VersionDirName = '' then
begin
Candidate := '{#MyVersionDirBase}';
Suffix := 0;
while
DirExists(ExpandConstant(
'{commonpf32}\metasequoiaime\' + Candidate)) or
DirExists(ExpandConstant(
'{commonpf64}\metasequoiaime\' + Candidate))
do
begin
Suffix := Suffix + 1;
Candidate := '{#MyVersionDirBase}.' + IntToStr(Suffix);
end;
VersionDirName := Candidate;
end;
Result := VersionDirName;
end;
function IsUserDatabaseFile(const FileName: String): Boolean;
begin
{ WAL 中可能还有尚未 checkpoint 的用户操作,必须与主库一起保留。}
Result :=
(CompareText(FileName, 'msime_user.db') = 0) or
(CompareText(FileName, 'msime_user.db-wal') = 0) or
(CompareText(FileName, 'msime_user.db-shm') = 0) or
(CompareText(FileName, 'msime_user.db-journal') = 0);
end;
function IsUserConfigFile(const FileName: String): Boolean;
begin
{ config.toml 是用户配置,config.base.toml 是上次合并用的模板基线:
没有它,Server 就无法判断某一项到底是用户改的还是旧版默认值。}
Result :=
(CompareText(FileName, 'config.toml') = 0) or
(CompareText(FileName, 'config.base.toml') = 0);
end;
function IsUserSkinDirectory(const FileName: String): Boolean;
begin
{ 外部皮肤在 %LOCALAPPDATA%\metasequoiaime\skins,升级安装不得清掉。}
Result := CompareText(FileName, 'skins') = 0;
end;
function IsPreservedAppDataItem(const FileName: String): Boolean;
begin
Result :=
IsUserDatabaseFile(FileName) or
IsUserConfigFile(FileName) or
IsUserSkinDirectory(FileName);
end;
function InitializeUninstall(): Boolean;
begin
RegQueryStringValue(
HKLM,
'Software\Metasequoia\MetasequoiaIME',
'VersionDir',
VersionDirName
);
Result := True;
end;
procedure StopProcess(const ImageName: String);
var
ResultCode: Integer;
begin
{ Watchdog 必须先停,否则它可能在卸载期间重新启动 Server。}
Exec(
ExpandConstant('{sys}\taskkill.exe'),
'/F /T /IM "' + ImageName + '"',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
);
end;
procedure DeleteWatchdogLogonTask;
var
ResultCode: Integer;
begin
{ /F makes this idempotent when upgrading from a build without the task. }
Exec(
ExpandConstant('{sys}\schtasks.exe'),
'/Delete /F /TN "{#MyWatchdogTaskName}"',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
);
end;
procedure EnsureSharedWebView2DataDir;
var
RootPath: String;
ResultCode: Integer;
begin
{ Edge 子进程需要 Users 可写、中完整性的目录。安装器本身是高完整性,
只 CreateDir 会带上高完整性标签,所以还要降完整性。 }
RootPath := ExpandConstant('{commonappdata}\metasequoiaime');
ForceDirectories(RootPath + '\webview2');
ForceDirectories(RootPath + '\webview2-settings');
Exec(
ExpandConstant('{sys}\icacls.exe'),
'"' + RootPath + '" /grant *S-1-5-32-545:(OI)(CI)M /T /C /Q',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
);
Exec(
ExpandConstant('{sys}\icacls.exe'),
'"' + RootPath + '" /setintegritylevel (OI)(CI)M /T /C /Q',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
);
end;
procedure CreateWatchdogLogonTask;
var
WatchdogPath: String;
Params: String;
ResultCode: Integer;
begin
ResultCode := -1;
WatchdogPath := ExpandConstant(
'{commonpf64}\metasequoiaime\server\{#MyWatchdogName}');
{ /F replaces the same fixed-name task during an upgrade. /IT keeps the
task in the interactive user's session; LIMITED avoids an elevated token. }
Params :=
'/Create /F /TN "{#MyWatchdogTaskName}" /SC ONLOGON ' +
'/RL LIMITED /IT /TR "\"' + WatchdogPath + '\""';
if
(not Exec(
ExpandConstant('{sys}\schtasks.exe'),
Params,
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
)) or
(ResultCode <> 0)
then
RaiseException(
'无法创建输入法登录启动任务(退出码:' +
IntToStr(ResultCode) + ')。');
end;
procedure TryDeleteTree(const Path: String);
begin
{ 忽略返回值:被占用文件保留,其他能删除的文件仍继续清理。}
DelTree(Path, True, True, True);
end;
procedure CleanAppDataExceptUserFiles;
var
AppDataPath: String;
FindRec: TFindRec;
ItemPath: String;
begin
AppDataPath := ExpandConstant('{localappdata}\metasequoiaime');
if not DirExists(AppDataPath) then
exit;
if FindFirst(AddBackslash(AppDataPath) + '*', FindRec) then
begin
try
repeat
if
(FindRec.Name <> '.') and
(FindRec.Name <> '..') and
(not IsPreservedAppDataItem(FindRec.Name))
then
begin
ItemPath := AddBackslash(AppDataPath) + FindRec.Name;
if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
TryDeleteTree(ItemPath)
else
DeleteFile(ItemPath);
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
function RemoveFileWithRetry(const Path: String): Boolean;
var
Attempt: Integer;
begin
for Attempt := 1 to 5 do
begin
if not FileExists(Path) then
begin
Result := True;
exit;
end;
DeleteFile(Path);
if not FileExists(Path) then
begin
Result := True;
exit;
end;
Sleep(200);
end;
Result := not FileExists(Path);
end;
function RemoveOldTargetDatabaseFiles(var FailedPath: String): Boolean;
var
AppDataPath: String;
FileNames: array[0..11] of String;
Index: Integer;
Path: String;
begin
AppDataPath := ExpandConstant('{localappdata}\metasequoiaime');
{ 先删 sidecar;若仍被占用,可在动主库和其他应用数据前安全中止。}
FileNames[0] := 'msime.db-wal';
FileNames[1] := 'msime.db-shm';
FileNames[2] := 'msime.db-journal';
FileNames[3] := 'english.db-wal';
FileNames[4] := 'english.db-shm';
FileNames[5] := 'english.db-journal';
FileNames[6] := 'others.db-wal';
FileNames[7] := 'others.db-shm';
FileNames[8] := 'others.db-journal';
FileNames[9] := 'msime.db';
FileNames[10] := 'english.db';
FileNames[11] := 'others.db';
for Index := 0 to 11 do
begin
Path := AddBackslash(AppDataPath) + FileNames[Index];
if not RemoveFileWithRetry(Path) then
begin
FailedPath := Path;
Result := False;
exit;
end;
end;
Result := True;
end;
procedure ReplayUserDictionary;
var
ReplayPath: String;
DataPath: String;
ResultCode: Integer;
begin
DataPath := ExpandConstant('{localappdata}\metasequoiaime');
if not FileExists(AddBackslash(DataPath) + 'msime_user.db') then
begin
Log('User dictionary replay skipped: msime_user.db does not exist.');
exit;
end;
ReplayPath := ExpandConstant(
'{commonpf64}\metasequoiaime\server\{#MyReplayName}');
Log('Starting user dictionary replay.');
if not Exec(
ReplayPath,
'--data-dir "' + DataPath + '"',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
) then
RaiseException(
'无法启动用户词库回放程序。请确认安装文件完整后重试。');
if ResultCode <> 0 then
RaiseException(
'用户词库回放失败(退出码:' + IntToStr(ResultCode) +
')。安装已停止,以避免启动未恢复用户词库的新版本。');
Log('User dictionary replay completed successfully.');
end;
procedure TryDeleteOldVersionDirs(const RootPath: String);
var
FindRec: TFindRec;
begin
if FindFirst(
AddBackslash(RootPath) + 'msime_v*',
FindRec
) then
begin
try
repeat
if
((FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0) and
(FindRec.Name <> '.') and
(FindRec.Name <> '..') and
(CompareText(FindRec.Name, VersionDirName) <> 0)
then
TryDeleteTree(AddBackslash(RootPath) + FindRec.Name);
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
FailedPath: String;
begin
{ 先锁定本次目录名,再清理能够释放的旧版本 DLL。}
VersionDirName := GetVersionDir('');
StopProcess('{#MyWatchdogName}');
StopProcess('{#MyAppExeName}');
{ 不能让旧 WAL/SHM 与即将复制的新主数据库混用。}
if not RemoveOldTargetDatabaseFiles(FailedPath) then
begin
Result :=
'无法删除旧词库文件:' + FailedPath + #13#10 +
'它可能仍被输入法相关进程占用。请关闭相关程序后重试安装。';
exit;
end;
{ 目标词库已安全移除,再清理旧前端资源与 WebView2 用户数据。
用户配置和外部皮肤目录在这里保留;webview2 目录故意重建,
由 Server 冷启动路径保证 FTB/候选窗仍能稳定揭罩。}
CleanAppDataExceptUserFiles;
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime\webview2'));
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime\webview2-settings'));
TryDeleteTree(ExpandConstant(
'{commonpf64}\metasequoiaime\server'));
TryDeleteOldVersionDirs(ExpandConstant(
'{commonpf32}\metasequoiaime'));
TryDeleteOldVersionDirs(ExpandConstant(
'{commonpf64}\metasequoiaime'));
{ 随后的 [Files] 与 ssPostInstall 会写入新 Server 和登录任务。}
Result := '';
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
ReplayUserDictionary;
CreateWatchdogLogonTask;
EnsureSharedWebView2DataDir;
{ Keep the old autostart intact until its scheduled-task replacement has
been created successfully, then remove the Explorer-delayed Run entry. }
RegDeleteValue(
HKLM,
'Software\Microsoft\Windows\CurrentVersion\Run',
'MetasequoiaImeWatchdog'
);
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usUninstall then
begin
DeleteWatchdogLogonTask;
RegDeleteValue(
HKLM,
'Software\Microsoft\Windows\CurrentVersion\Run',
'MetasequoiaImeWatchdog'
);
StopProcess('{#MyWatchdogName}');
StopProcess('{#MyAppExeName}');
end
else if CurUninstallStep = usPostUninstall then
begin
TryDeleteTree(ExpandConstant(
'{commonpf64}\metasequoiaime\server'));
if VersionDirName <> '' then
begin
TryDeleteTree(ExpandConstant(
'{commonpf32}\metasequoiaime\' + VersionDirName));
TryDeleteTree(ExpandConstant(
'{commonpf64}\metasequoiaime\' + VersionDirName));
end;
TryDeleteTree(ExpandConstant('{commonpf32}\metasequoiaime'));
TryDeleteTree(ExpandConstant('{commonpf64}\metasequoiaime'));
TryDeleteTree(ExpandConstant('{localappdata}\metasequoiaime'));
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime'));
end;
end;
+43
View File
@@ -0,0 +1,43 @@
# 本地测试安装流程:编译相邻源码仓库(若存在)→ 收集安装文件 →
# 本机自签名 → Inno Setup 打包 → 签安装包 → 启动安装程序。
#
# 不使用任何预置证书。签名脚本会在本机生成并复用自签名测试证书。
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
Set-Location $PSScriptRoot
$repoRoot = Split-Path -Parent $PSScriptRoot
$tsfCompile = Join-Path $repoRoot 'MetasequoiaImeTsf\scripts\lcompile-release-both.ps1'
$serverCompile = Join-Path $repoRoot 'MetasequoiaImeServer\scripts\lcompile-release.ps1'
$settingsDir = Join-Path $repoRoot 'MetasequoiaImeUiHtml\webview2\settings\ime-settings'
if (Test-Path -LiteralPath $tsfCompile -PathType Leaf) {
Push-Location (Join-Path $repoRoot 'MetasequoiaImeTsf')
try { & $tsfCompile } finally { Pop-Location }
}
else {
Write-Host '跳过 TSF 编译:未找到相邻的 MetasequoiaImeTsf 仓库。'
}
if (Test-Path -LiteralPath $serverCompile -PathType Leaf) {
Push-Location (Join-Path $repoRoot 'MetasequoiaImeServer')
try { & $serverCompile } finally { Pop-Location }
}
else {
Write-Host '跳过 Server 编译:未找到相邻的 MetasequoiaImeServer 仓库。'
}
if (Test-Path -LiteralPath (Join-Path $settingsDir 'package.json') -PathType Leaf) {
Push-Location $settingsDir
try { pnpm run build } finally { Pop-Location }
}
else {
Write-Host '跳过设置页构建:未找到相邻的 MetasequoiaImeUiHtml 仓库。'
}
& (Join-Path $PSScriptRoot 'Prepare-PackageFiles.ps1')
& (Join-Path $PSScriptRoot 'Sign-PackageBinaries-Local.ps1')
& (Join-Path $PSScriptRoot 'Compile-Installer.ps1')
& (Join-Path $PSScriptRoot 'Sign-Installer-Local.ps1')
& (Join-Path $PSScriptRoot 'Install.ps1')