Add installer instructions.
This commit is contained in:
commit
d2a5320ec2
10 files changed
+1654
No files matched your search
@@ -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
|
||||
@@ -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')"
|
||||
@@ -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
|
||||
@@ -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 个。"
|
||||
@@ -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` 提交进本仓库。
|
||||
@@ -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
|
||||
@@ -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 = ""
|
||||
@@ -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;
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user