fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s

- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- style(client): 应用图标增加圆角,统一“裁判系统客户端”命名
- fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- docs: 润色项目说明并统一裁判系统术语
- test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成

验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
This commit is contained in:
2026-10-21 09:25:17 +08:00
parent 6a0eb14b28
commit 054379dc64
52 changed files with 1070 additions and 280 deletions
+3
View File
@@ -18,3 +18,6 @@ dist/
# 测试运行时被 XDG_CONFIG_HOME 相对路径写出来的配置,不是仓库内容。
src-tauri/.config/
# Installer hotspot guidance is local configuration.
/src-tauri/installer/network.local.nsh
+144
View File
@@ -0,0 +1,144 @@
"""Render the existing SVG brand sources with Tauri, then export PNG/ICO/ICNS/BMP.
Requires the installed desktop Node dependencies and Python's Pillow package.
Text is rasterized with Pillow so exports do not depend on the SVG renderer's
font discovery. No dependency is added to the application or its build hooks.
"""
import argparse
import copy
import io
import os
from pathlib import Path
import shutil
import struct
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from PIL import Image, ImageDraw, ImageFont
DESKTOP = Path(__file__).resolve().parents[1]
ICONS = DESKTOP / "src-tauri/icons"
INSTALLER = DESKTOP / "src-tauri/installer"
SVG = "{http://www.w3.org/2000/svg}"
ET.register_namespace("", SVG[1:-1])
def system_font(monospace=False, bold=False):
fonts = Path(os.environ.get("WINDIR", "C:/Windows")) / "Fonts"
candidates = (
[fonts / ("consolab.ttf" if bold else "consola.ttf"),
Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf")]
if monospace else
[fonts / ("msyhbd.ttc" if bold else "msyh.ttc"),
Path("/System/Library/Fonts/PingFang.ttc"),
Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc")]
)
for path in candidates:
if path.exists():
return path
raise RuntimeError("Set --sans-font and --mono-font to installed fonts supporting the artwork's text.")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sans-font", type=Path)
parser.add_argument("--mono-font", type=Path)
args = parser.parse_args()
node = shutil.which("node")
cli = DESKTOP / "node_modules/@tauri-apps/cli/tauri.js"
if not node or not cli.exists():
raise RuntimeError("Install Node and run pnpm install --frozen-lockfile in desktop/ first.")
with tempfile.TemporaryDirectory(prefix="rmic-brand-") as temporary:
scratch = Path(temporary)
def render(root, size):
source = scratch / "source.svg"
ET.ElementTree(root).write(source, encoding="utf-8", xml_declaration=True)
subprocess.run([node, str(cli), "icon", str(source), "--output", str(scratch),
"--png", str(size)], cwd=DESKTOP, check=True, capture_output=True)
with Image.open(scratch / f"{size}x{size}.png") as image:
return image.convert("RGBA")
original = ET.parse(ICONS / "icon.svg").getroot()
images = {}
for size in (16, 20, 24, 32, 48, 64, 128, 256, 512, 1024):
root = copy.deepcopy(original)
scale = 0.97 if size <= 48 else 0.88 if size <= 128 else 0.82
group = root.find(f"{SVG}g")
group.set("transform", group.get("transform").replace("scale(0.82)", f"scale({scale})"))
images[size] = render(root, size)
for filename, size in (("32x32.png", 32), ("128x128.png", 128),
("128x128@2x.png", 256), ("icon.png", 512)):
images[size].save(ICONS / filename)
def png(size):
stream = io.BytesIO()
images[size].save(stream, format="PNG")
return stream.getvalue()
# Supply every ICO size separately, preserving optical scaling at 16–48px.
entries, payloads = [], []
sizes = (16, 20, 24, 32, 48, 64, 128, 256)
offset = 6 + 16 * len(sizes)
for size in sizes:
data = png(size)
entries.append(struct.pack("<BBBBHHII", size % 256, size % 256, 0, 0, 1, 32, len(data), offset))
payloads.append(data)
offset += len(data)
(ICONS / "icon.ico").write_bytes(struct.pack("<HHH", 0, 1, len(sizes)) + b"".join(entries + payloads))
chunks = []
for kind, size in ((b"icp4", 16), (b"icp5", 32), (b"icp6", 64), (b"ic07", 128),
(b"ic08", 256), (b"ic09", 512), (b"ic10", 1024),
(b"ic11", 32), (b"ic12", 64), (b"ic13", 256), (b"ic14", 512)):
data = png(size)
chunks.append(kind + struct.pack(">I", 8 + len(data)) + data)
body = b"".join(chunks)
(ICONS / "icon.icns").write_bytes(b"icns" + struct.pack(">I", 8 + len(body)) + body)
for name in ("header", "sidebar"):
root = ET.parse(INSTALLER / f"{name}.svg").getroot()
width, height = int(root.get("width")), int(root.get("height"))
text = []
for element in list(root):
if element.tag == f"{SVG}text":
text.append(element)
root.remove(element)
elif element.tag == f"{SVG}image":
# Installer art may reference the unframed original mark.
# Preserve its own viewBox instead of replacing every image
# with the square application icon.
source = (INSTALLER / element.get("href")).resolve()
logo = ET.parse(source).getroot()
for attr in ("x", "y", "width", "height"):
logo.set(attr, element.get(attr))
logo.set("preserveAspectRatio", "xMidYMid meet")
index = list(root).index(element)
root.remove(element)
root.insert(index, logo)
edge = max(width, height)
root.set("width", str(edge))
root.set("height", str(edge))
root.set("viewBox", f"0 0 {edge} {edge}")
factor = 4
image = render(root, edge * factor)
draw = ImageDraw.Draw(image)
for element in text:
mono = element.get("font-family") == "monospace"
bold = element.get("font-weight") == "700"
font_path = (args.mono_font if mono else args.sans_font) or system_font(mono, bold)
font = ImageFont.truetype(str(font_path), round(float(element.get("font-size")) * factor))
draw.text((float(element.get("x")) * factor, float(element.get("y")) * factor),
element.text, font=font, fill=element.get("fill"), anchor="ls")
image = image.crop((0, 0, width * factor, height * factor)).convert("RGB")
image.resize((width, height), Image.Resampling.LANCZOS).save(INSTALLER / f"{name}.png")
# MUI scales these to the dialog's font/DPI. Supply 2x bitmaps so
# Windows does not have to enlarge the raster text on modern screens.
image.resize((width * 2, height * 2), Image.Resampling.LANCZOS).save(INSTALLER / f"{name}.bmp")
print("Generated rounded PNG/ICO/ICNS icons and 24-bit NSIS header/sidebar artwork.")
if __name__ == "__main__":
main()
+137
View File
@@ -0,0 +1,137 @@
# Windows uses the executable's integrity label when creating its process. A Low
# label inherited from a sandboxed workspace prevents NSIS from writing to TEMP,
# before .onInit or any installer hook can run.
[CmdletBinding()]
param(
[string]$OutputDir,
[switch]$CheckProcessOnly,
[switch]$CheckOnly
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# whoami's group names are localized; the mandatory-label SID is stable.
$groups = (& whoami.exe /groups) -join "`n"
if ($LASTEXITCODE -ne 0 -or $groups -notmatch 'S-1-16-(\d+)') {
throw 'Cannot determine the Windows process integrity level.'
}
if ([int]$Matches[1] -lt 8192) {
throw 'Windows packaging requires Medium integrity or higher. Start the terminal / CI runner outside the Low integrity sandbox. Redirecting TEMP or changing directory write permissions does not fix executable integrity labels.'
}
if ($CheckProcessOnly) { return }
if (-not ('RmicBundleIntegrity' -as [type])) {
$compilerOptions = @{}
if ($PSVersionTable.PSEdition -eq 'Desktop') {
# Framework CodeDOM otherwise tries to label its private temp directory
# High when an admin token has been capped at Medium by an exe label.
$parameters = New-Object System.CodeDom.Compiler.CompilerParameters
$parameters.ReferencedAssemblies.Add('System.dll') | Out-Null
$parameters.TempFiles = New-Object System.CodeDom.Compiler.TempFileCollection([IO.Path]::GetTempPath(), $false)
$compilerOptions.CompilerParameters = $parameters
}
Add-Type @compilerOptions -TypeDefinition @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class RmicBundleIntegrity {
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
private static extern uint GetNamedSecurityInfoW(string name, int type, uint info,
out IntPtr owner, out IntPtr group, out IntPtr dacl, out IntPtr sacl, out IntPtr sd);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool ConvertSecurityDescriptorToStringSecurityDescriptorW(
IntPtr sd, uint revision, uint info, out IntPtr text, out uint length);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(IntPtr memory);
public static string ReadLabel(string path) {
// LABEL_SECURITY_INFORMATION reads only the label, without requiring the
// SeSecurityPrivilege needed to read an entire SACL (Get-Acl -Audit).
IntPtr owner, group, dacl, sacl, sd, text = IntPtr.Zero;
uint error = GetNamedSecurityInfoW(path, 1, 0x10, out owner, out group,
out dacl, out sacl, out sd);
if (error != 0) throw new Win32Exception((int)error, path);
try {
uint length;
if (!ConvertSecurityDescriptorToStringSecurityDescriptorW(sd, 1, 0x10,
out text, out length)) throw new Win32Exception(Marshal.GetLastWin32Error());
return Marshal.PtrToStringUni(text);
} finally {
if (text != IntPtr.Zero) LocalFree(text);
LocalFree(sd);
}
}
}
'@
}
function Assert-MediumIntegrity([string]$Path) {
$label = [RmicBundleIntegrity]::ReadLabel($Path)
# An absent label is Medium. Windows may render known SIDs as SDDL aliases.
if ($label -match ';(LW|UN|S-1-16-(\d+))\)') {
if ($Matches[1] -in @('LW', 'UN') -or [int]$Matches[2] -lt 8192) {
throw "Low / Untrusted integrity label on $Path. Run prepare-windows-bundle.ps1 from a normal Windows terminal before distributing this artifact."
}
}
}
$hostOutputDir = $null
if (-not $OutputDir) {
# Ask Cargo so CARGO_TARGET_DIR and Cargo's target-dir setting are respected.
$manifest = Join-Path $PSScriptRoot '../src-tauri/Cargo.toml'
$metadata = & cargo +stable metadata --no-deps --format-version 1 --manifest-path $manifest
if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve the Cargo output directory.' }
$target = $env:TAURI_ENV_TARGET_TRIPLE
if (-not $target) { $target = 'x86_64-pc-windows-msvc' }
$profile = if ($env:TAURI_ENV_DEBUG -eq 'true') { 'debug' } else { 'release' }
$targetDir = ($metadata | ConvertFrom-Json).target_directory
$OutputDir = Join-Path $targetDir "$target/$profile"
# Cross-target Cargo builds also run host build scripts from target/<profile>.
# Old Low executables there can fail before the bundling hook is reached.
$hostOutputDir = Join-Path $targetDir $profile
}
$OutputDir = [IO.Path]::GetFullPath($OutputDir)
# Limit changes to build outputs. Do not relabel the workspace, TEMP, user data,
# or DACLs, and do not request elevation for the installer.
$directories = @(
$OutputDir
(Join-Path $OutputDir 'bundle')
(Join-Path $OutputDir 'bundle/nsis')
(Join-Path $OutputDir 'nsis')
)
if ($hostOutputDir) { $directories = @($hostOutputDir) + $directories }
if (Test-Path -LiteralPath (Join-Path $OutputDir 'nsis')) {
$directories += @(Get-ChildItem -LiteralPath (Join-Path $OutputDir 'nsis') -Directory -Recurse |
Select-Object -ExpandProperty FullName)
}
foreach ($directory in $directories) {
if (-not $CheckOnly) {
New-Item -ItemType Directory -Force -Path $directory | Out-Null
& icacls.exe $directory /setintegritylevel '(OI)(CI)M' | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Cannot set Medium integrity on $directory" }
}
Assert-MediumIntegrity $directory
}
$executables = @(Get-ChildItem -LiteralPath $OutputDir -Filter '*.exe' -File)
foreach ($directory in @('nsis', 'bundle/nsis')) {
$executables += @(Get-ChildItem -LiteralPath (Join-Path $OutputDir $directory) -Filter '*.exe' -File -Recurse)
}
if ($hostOutputDir -and (Test-Path -LiteralPath (Join-Path $hostOutputDir 'build'))) {
$executables += @(Get-ChildItem -LiteralPath (Join-Path $hostOutputDir 'build') -Filter '*.exe' -File -Recurse)
}
foreach ($executable in $executables) {
if (-not $CheckOnly) {
# Also replace explicit Low labels left by an earlier Low integrity build.
& icacls.exe $executable.FullName /setintegritylevel M | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Cannot set Medium integrity on $($executable.FullName)" }
}
Assert-MediumIntegrity $executable.FullName
}
if ($CheckOnly -and -not (Get-ChildItem -LiteralPath (Join-Path $OutputDir 'bundle/nsis') -Filter '*.exe' -File)) {
throw "No NSIS installer found in $OutputDir/bundle/nsis."
}
Write-Host "Windows bundle integrity verified: $OutputDir ($($executables.Count) executables)."
+158
View File
@@ -0,0 +1,158 @@
# Integration regression: actual Windows labels, child process tokens, and an
# NSIS bootstrap that writes to TEMP and exits without installing anything.
[CmdletBinding()]
param(
[string]$NsisCompiler = (Join-Path $env:LOCALAPPDATA 'tauri/NSIS/makensis.exe')
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$prepare = Join-Path $PSScriptRoot 'prepare-windows-bundle.ps1'
& $prepare -CheckProcessOnly
if (-not (Test-Path -LiteralPath $NsisCompiler)) { throw "NSIS compiler not found: $NsisCompiler" }
function Assert([bool]$Condition, [string]$Message) {
if (-not $Condition) { throw $Message }
}
$testId = [Guid]::NewGuid().ToString('N')
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$fixture = Join-Path $tempRoot "rmic bundle test $testId"
$probeFile = Join-Path $tempRoot "rmic-nsis-temp-$testId.txt"
$installer = Join-Path $fixture 'bundle/nsis/probe-setup.exe'
$main = Join-Path $fixture 'cam-desktop.exe'
try {
New-Item -ItemType Directory -Force -Path (Join-Path $fixture 'bundle/nsis'), (Join-Path $fixture 'nsis/x64') | Out-Null
Copy-Item -LiteralPath (Join-Path $env:WINDIR 'System32/whoami.exe') -Destination $main
$accessBefore = (Get-Acl -LiteralPath $main).Sddl
# A small real NSIS executable exercises its early TEMP validation and
# plugin extraction without touching app files, registry, or shortcuts.
$source = Join-Path $fixture 'probe.nsi'
@"
Unicode true
Name "RMIC temporary directory probe"
OutFile "$($installer.Replace('$', '$$'))"
RequestExecutionLevel user
SilentInstall silent
Section
InitPluginsDir
FileOpen `$0 "`$TEMP\rmic-nsis-temp-$testId.txt" w
IfErrors failed
FileWrite `$0 "NSIS TEMP writable"
FileClose `$0
SetErrorLevel 0
Quit
failed:
SetErrorLevel 3
SectionEnd
"@ | Set-Content -LiteralPath $source -Encoding UTF8
& $NsisCompiler /V2 $source
Assert ($LASTEXITCODE -eq 0) 'Cannot compile the NSIS TEMP probe.'
& icacls.exe $fixture /setintegritylevel '(OI)(CI)L' | Out-Null
Assert ($LASTEXITCODE -eq 0) 'Cannot create a Low integrity fixture.'
& icacls.exe $main /setintegritylevel L | Out-Null
Assert ($LASTEXITCODE -eq 0) 'Cannot create an explicit Low executable label.'
$before = (& $main /groups) -join "`n"
Assert ($LASTEXITCODE -eq 0 -and $before -match 'S-1-16-4096') 'Low executable did not start with a Low token.'
$rejected = $false
try { & $prepare -OutputDir $fixture -CheckOnly } catch {
if ($_.Exception.Message -notmatch 'Low / Untrusted') { throw }
$rejected = $true
}
Assert $rejected 'Read-only artifact verification accepted a Low label.'
Assert ([RmicBundleIntegrity]::ReadLabel($installer) -match ';LW\)') 'Read-only verification changed the installer.'
# A Low process must fail before it tries to modify any output labels.
$lowShell = Join-Path $fixture 'low-shell.exe'
Copy-Item -LiteralPath (Join-Path $env:WINDIR 'System32/WindowsPowerShell/v1.0/powershell.exe') -Destination $lowShell
# Windows PowerShell 5 treats redirected native stderr as ErrorRecord objects.
# This command is expected to fail, so collect its diagnostic before asserting.
try {
$ErrorActionPreference = 'Continue'
$lowOutput = (& $lowShell -NoProfile -ExecutionPolicy Bypass -File $prepare -CheckProcessOnly 2>&1) -join "`n"
} finally {
$ErrorActionPreference = 'Stop'
}
Assert ($LASTEXITCODE -ne 0 -and $lowOutput -match 'requires Medium integrity') 'Low packaging process was not rejected.'
& $prepare -OutputDir $fixture
& $prepare -OutputDir $fixture -CheckOnly
& $prepare -OutputDir $fixture
$after = (& $main /groups) -join "`n"
Assert ($LASTEXITCODE -eq 0 -and $after -match 'S-1-16-(\d+)' -and [int]$Matches[1] -ge 8192) 'Repaired executable still starts below Medium integrity.'
Assert ((Get-Acl -LiteralPath $main).Sddl -eq $accessBefore) 'Repair changed the executable DACL.'
# The repaired shell now starts at Medium even from an elevated test runner.
# Verify that repairing owned Low outputs does not require elevation.
$mediumFixture = Join-Path $fixture 'medium repair'
New-Item -ItemType Directory -Path $mediumFixture | Out-Null
& icacls.exe $mediumFixture /setintegritylevel '(OI)(CI)L' | Out-Null
Assert ($LASTEXITCODE -eq 0) 'Cannot prepare the Medium-process repair fixture.'
$mediumOutput = (& $lowShell -NoProfile -ExecutionPolicy Bypass -File $prepare -OutputDir $mediumFixture 2>&1) -join "`n"
Assert ($LASTEXITCODE -eq 0) "Repair failed at Medium integrity: $mediumOutput"
Assert ([RmicBundleIntegrity]::ReadLabel($mediumFixture) -match ';ME\)') 'Medium process did not repair the output label.'
$probe = Start-Process -FilePath $installer -ArgumentList '/S' -PassThru -WindowStyle Hidden
try {
Assert ($probe.WaitForExit(15000)) 'NSIS TEMP probe timed out.'
Assert ($probe.ExitCode -eq 0) "NSIS TEMP probe failed: $($probe.ExitCode)"
Assert ((Get-Content -LiteralPath $probeFile -Raw) -eq 'NSIS TEMP writable') 'NSIS could not write to the normal TEMP directory.'
} finally {
if (-not $probe.HasExited) { $probe.Kill(); $probe.WaitForExit() }
$probe.Dispose()
}
$emptyOutput = Join-Path $fixture 'empty'
& $prepare -OutputDir $emptyOutput
$rejected = $false
try { & $prepare -OutputDir $emptyOutput -CheckOnly } catch {
if ($_.Exception.Message -notmatch 'No NSIS installer') { throw }
$rejected = $true
}
Assert $rejected 'Artifact verification accepted missing installers.'
# Exercise automatic Cargo output discovery, debug target selection and the
# separate host build-script directory, without invoking a real Rust build.
$savedEnvironment = @{}
foreach ($name in @('CARGO_TARGET_DIR', 'TAURI_ENV_TARGET_TRIPLE', 'TAURI_ENV_DEBUG')) {
$savedEnvironment[$name] = @{
Exists = Test-Path -LiteralPath "Env:$name"
Value = [Environment]::GetEnvironmentVariable($name, 'Process')
}
}
try {
$env:CARGO_TARGET_DIR = Join-Path $fixture 'cargo cache'
$env:TAURI_ENV_TARGET_TRIPLE = 'x86_64-pc-windows-msvc'
$env:TAURI_ENV_DEBUG = 'true'
$hostBuildDir = Join-Path $env:CARGO_TARGET_DIR 'debug/build/probe'
New-Item -ItemType Directory -Force -Path $hostBuildDir | Out-Null
$hostScript = Join-Path $hostBuildDir 'build-script-build.exe'
Copy-Item -LiteralPath $main -Destination $hostScript
& icacls.exe $env:CARGO_TARGET_DIR /setintegritylevel '(OI)(CI)L' | Out-Null
Assert ($LASTEXITCODE -eq 0) 'Cannot create Low host build outputs.'
& $prepare
Assert ([RmicBundleIntegrity]::ReadLabel($hostScript) -match ';ME\)') 'Host build script was not repaired.'
$targetOutput = Join-Path $env:CARGO_TARGET_DIR 'x86_64-pc-windows-msvc/debug/bundle/nsis'
Assert (Test-Path -LiteralPath $targetOutput) 'Cargo target directory or debug profile was ignored.'
} finally {
foreach ($name in $savedEnvironment.Keys) {
if ($savedEnvironment[$name].Exists) {
Set-Item -LiteralPath "Env:$name" -Value $savedEnvironment[$name].Value
} else {
Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue
}
}
}
Write-Host 'PASS: Low token reproduction, process guard, read-only rejection, label repair, unchanged DACL, repeat repair, NSIS TEMP bootstrap, missing artifacts, custom Cargo output and host build scripts.'
} finally {
# Only delete this test's uniquely named fixture, after checking its absolute path.
$resolvedFixture = [IO.Path]::GetFullPath($fixture)
if ($resolvedFixture.StartsWith($tempRoot.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase) -and
(Split-Path -Leaf $resolvedFixture) -eq "rmic bundle test $testId") {
if (Test-Path -LiteralPath $resolvedFixture) { Remove-Item -LiteralPath $resolvedFixture -Recurse -Force }
}
if (Test-Path -LiteralPath $probeFile) { Remove-Item -LiteralPath $probeFile -Force }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+3 -3
View File
@@ -1,9 +1,9 @@
<!-- RMIC::2026 应用图标(矢量源)。
由 https://simba-robotics.com/logo.svg 生成:把原图 1024 画布的 viewBox 收窄、标志居中,
底色用界面的 --deep。位图(PNG / ICO / ICNS)由这一份光栅化得到;
底色沿用界面的深色背景,圆角之外透明。位图(PNG / ICO / ICNS)由这一份光栅化得到;
小尺寸另有一版更大的缩放比(见 docs/desktop/client.md 的「队标」一节)。 -->
<svg width="1024" height="1024" viewBox="0 0 270.93333 270.93333" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="270.93333" height="270.93333" fill="#080b12"/>
<svg width="1024" height="1024" viewBox="0 0 270.93333 270.93333" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<rect x="0" y="0" width="270.93333" height="270.93333" rx="56.44" fill="#080b12"/>
<g transform="translate(135.4667 135.4667) scale(0.82) translate(-135.28 -136.22)">
<g
inkscape:label="图层 1"

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,32 @@
; Tauri-specific messages. Standard buttons, directory/install pages and
; uninstaller controls use NSIS's bundled SimpChinese language.
; This file is included after MUI_LANGUAGE, so it can override the NLF's font.
SetFont /LANG=${LANG_SIMPCHINESE} "Microsoft YaHei UI" 9
LangString addOrReinstall ${LANG_SIMPCHINESE} "重新安装或修复程序"
LangString alreadyInstalled ${LANG_SIMPCHINESE} "已安装此版本"
LangString alreadyInstalledLong ${LANG_SIMPCHINESE} "此电脑已安装客户端版本 ${VERSION}。请选择重新安装或卸载,然后点击“下一步”。"
LangString appRunning ${LANG_SIMPCHINESE} "RMIC::2026 正在运行。请先关闭软件,再重试。"
LangString appRunningOkKill ${LANG_SIMPCHINESE} "RMIC::2026 正在运行。$\n点击“确定”将关闭程序。"
LangString chooseMaintenanceOption ${LANG_SIMPCHINESE} "选择你所需的安装维护操作。"
LangString choowHowToInstall ${LANG_SIMPCHINESE} "请选择本软件安装范围。"
LangString createDesktop ${LANG_SIMPCHINESE} "创建桌面快捷方式"
LangString dontUninstall ${LANG_SIMPCHINESE} "保留现有版本并继续"
LangString dontUninstallDowngrade ${LANG_SIMPCHINESE} "保留现有版本"
LangString failedToKillApp ${LANG_SIMPCHINESE} "无法结束 RMIC::2026。请手动关闭软件后重试。"
LangString installingWebview2 ${LANG_SIMPCHINESE} "正在安装 WebView2 运行时……"
LangString newerVersionInstalled ${LANG_SIMPCHINESE} "此电脑已安装较新的 RMIC::2026。继续安装将降级,建议先卸载当前版本。请选择操作,然后点击“下一步”。"
LangString older ${LANG_SIMPCHINESE} "较旧"
LangString olderOrUnknownVersionInstalled ${LANG_SIMPCHINESE} "此电脑已安装 RMIC::2026,版本:$R4。建议先卸载现有程序,再安装新版本。请选择操作,然后点击“下一步”。"
LangString silentDowngrades ${LANG_SIMPCHINESE} "本安装包不允许静默降级。请打开安装界面选择维护操作。$\n"
LangString unableToUninstall ${LANG_SIMPCHINESE} "无法卸载现有程序。请关闭正在运行的 RMIC::2026 后重试。"
LangString uninstallApp ${LANG_SIMPCHINESE} "卸载 RMIC::2026"
LangString uninstallBeforeInstalling ${LANG_SIMPCHINESE} "先卸载现有程序,再继续安装"
LangString unknown ${LANG_SIMPCHINESE} "未知"
LangString webview2AbortError ${LANG_SIMPCHINESE} "WebView2 安装未完成,RMIC::2026 无法启动。请检查网络连接后重新运行安装程序。"
LangString webview2DownloadError ${LANG_SIMPCHINESE} "WebView2 下载失败。错误信息:$0"
LangString webview2DownloadSuccess ${LANG_SIMPCHINESE} "WebView2 安装引导程序已下载。"
LangString webview2Downloading ${LANG_SIMPCHINESE} "正在下载 WebView2 安装引导程序……"
LangString webview2InstallError ${LANG_SIMPCHINESE} "WebView2 安装失败,退出代码:$1"
LangString webview2InstallSuccess ${LANG_SIMPCHINESE} "WebView2 运行时已安装。"
LangString deleteAppData ${LANG_SIMPCHINESE} "同时删除应用数据"
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="132" height="57" viewBox="0 0 132 57">
<rect width="132" height="57" fill="#ffffff"/>
<image href="../icons/icon.svg" x="8" y="7" width="43" height="43"/>
<text x="61" y="26" font-family="monospace" font-size="15" font-weight="700" fill="#35475f">RMIC</text>
<text x="60" y="44" font-family="monospace" font-size="17" font-weight="700" fill="#e8232b">::2026</text>
</svg>

After

Width:  |  Height:  |  Size: 427 B

+47
View File
@@ -0,0 +1,47 @@
; Included by Tauri before its Modern UI pages. Keep the upstream installer
; template, upgrade handling and current-user installation behavior.
!ifndef RMIC_INSTALLER_THEME
!define RMIC_INSTALLER_THEME
; CJK dialog units have a different width/height ratio from the bitmap.
!define MUI_WELCOMEFINISHPAGE_BITMAP_STRETCH AspectFitHeight
; Header art is narrower than the standard 150px canvas so height-based
; scaling stays within the image area of the 9pt Chinese dialog.
!define MUI_HEADERIMAGE_BITMAP_STRETCH AspectFitHeight
!define MUI_WELCOMEPAGE_TITLE "欢迎安装$\r$\n裁判系统客户端"
!define MUI_WELCOMEPAGE_TEXT "本程序运行内存需求约为 400MB。$\r$\n$\r$\n点击“下一步”选择安装位置。"
!define MUI_DIRECTORYPAGE_TEXT_TOP "软件仅为当前 Windows 用户安装。请选择有写入权限的文件夹。"
!define MUI_FINISHPAGE_TITLE "安装完成"
; MUI reserves 60 dialog units above the two checkboxes: keep six short lines.
; Optional local guidance is excluded from version control.
!if /FileExists "${__FILEDIR__}/network.local.nsh"
!include "${__FILEDIR__}/network.local.nsh"
!endif
!ifdef RMIC_INSTALLER_WIFI_SSID & RMIC_INSTALLER_WIFI_PASS
!define MUI_FINISHPAGE_TEXT "裁判系统客户端已安装完成。$\r$\n首次启动请选择模式并绑定裁判系统。$\r$\n裁判系统会自动连接以下热点;请将$\r$\n本机也连接到同一网络。$\r$\n名称(SSID):${RMIC_INSTALLER_WIFI_SSID}$\r$\n密码:${RMIC_INSTALLER_WIFI_PASS}"
!else
!define MUI_FINISHPAGE_TEXT "裁判系统客户端已安装完成。$\r$\n首次启动请选择模式并绑定裁判系统。$\r$\n请让本机与裁判系统连接到同一网络。$\r$\n网络名称和密码请向现场工作人员确认。"
!endif
!define MUI_FINISHPAGE_TEXT_LARGE
!define MUI_FINISHPAGE_RUN_TEXT "启动裁判系统客户端"
!define MUI_INSTFILESPAGE_COLORS "E7ECF5 080B13"
!macro NSIS_HOOK_PREINSTALL
DetailPrint "正在安装 RMIC::2026 ${VERSION}。"
DetailPrint "安装位置:$INSTDIR"
!macroend
!macro NSIS_HOOK_POSTINSTALL
DetailPrint "程序文件与快捷方式已创建。请点击“下一步”完成安装。"
!macroend
!macro NSIS_HOOK_PREUNINSTALL
DetailPrint "正在移除 RMIC::2026 的程序文件与快捷方式。"
!macroend
!macro NSIS_HOOK_POSTUNINSTALL
DetailPrint "RMIC::2026 卸载步骤已完成。"
!macroend
!endif
+8
View File
@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 270.93333 270.93333">
<g id="layer1">
<path d="m 120.18387,65.065937 -8.00358,13.862771 h 27.77765 l 0.0541,0.09409 30.76276,53.283672 -35.41234,61.33599 -0.008,0.0143 -7.99546,13.84855 -0.008,0.0143 h 16.00718 44.01701 l -5.80572,10.05592 h -66.94744 l -0.007,0.0105 h -6.48395 l 5.81384,-10.07013 8.00358,-13.86278 28.83631,-49.94524 8.0036,-13.86276 H 142.78056 114.57375 107.01421 83.392663 67.385498 l -8.004262,13.86276 -42.562363,73.72043 -8.0035837,13.86278 H 24.822458 84.211033 l -0.0914,0.15771 h 16.007157 11.36098 l 0.007,-0.0105 h 62.07111 16.00784 l 0.0105,-0.0175 h 41.16526 15.05748 16.00718 l -8.00358,-13.86278 -5.81181,-10.06608 -8.00358,-13.86276 h -16.00718 -12.5848 -16.00784 l -0.0156,0.0278 h -1.6347 -0.80551 -12.33705 l 27.39928,-47.45699 8.00358,-13.86276 -5.11392,-8.85783 -11.76441,-20.37651 h 31.64205 l 5.06519,-8.773893 2.9384,-5.088885 h -15.13465 -48.55152 l 8.00358,13.862778 h 0.0297 l 16.87765,29.23434 -35.40286,61.31975 h -13.23258 l 27.41688,-47.48812 8.00358,-13.8621 -8.00358,-13.86276 -22.75918,-39.420912 h 60.26447 l -8.00332,-13.862894 h -32.42792 v -0.09409 z M 75.389113,143.70806 h 23.62222 7.558857 28.2075 l -28.83633,49.94523 -8.003587,13.86278 -5.11257,8.85512 -5.1119,-8.85512 -8.00427,-13.86278 H 63.701872 46.553462 l 8.574205,-14.85103 8.003585,-13.86277 z" style="fill:#ffffff;stroke-width:0.904925;paint-order:stroke fill markers" id="path24" />
<path d="m 134.72691,41.313757 -8.00358,13.862771 h 62.89352 16.00718 31.56419 l -8.00358,-13.862771 h -31.5642 -16.00717 z" style="fill:#ffffff;stroke-width:1.17159;paint-order:stroke fill markers" id="path26" />
<path d="M 243.26673,60.098895 A 11.644196,11.644196 0 0 0 231.62281,71.742811 11.644196,11.644196 0 0 0 243.26673,83.386727 11.644196,11.644196 0 0 0 254.91131,71.742811 11.644196,11.644196 0 0 0 243.26673,60.098895 Z" style="fill:#ff0000;stroke-width:0.513476;paint-order:stroke fill markers" id="path25" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,4 @@
; Copy to network.local.nsh and fill in the event's hotspot information.
; network.local.nsh is ignored by Git and must not be committed.
!define RMIC_INSTALLER_WIFI_SSID "现场热点名称"
!define RMIC_INSTALLER_WIFI_PASS "现场热点密码"
Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="338" viewBox="0 0 164 338">
<rect width="164" height="338" fill="#080b13"/>
<path d="M0 126H164M0 150H164M0 174H164M0 198H164M0 222H164M22 110V246M46 110V246M70 110V246M94 110V246M118 110V246M142 110V246" stroke="#141d2c" fill="none"/>
<text x="18" y="27" font-family="monospace" font-size="9" fill="#94a1b8">SIMBA ROBOTICS</text>
<text x="17" y="62" font-family="monospace" font-size="29" font-weight="700" fill="#e7ecf5">RMIC</text>
<text x="17" y="92" font-family="monospace" font-size="26" font-weight="700" fill="#e8232b">::</text>
<text x="48" y="92" font-family="monospace" font-size="26" font-weight="700" fill="#e7ecf5">2026</text>
<path d="M18 103H146" stroke="#26344a"/>
<path d="M18 103H48" stroke="#e8232b"/>
<image href="logo.svg" x="18" y="117" width="128" height="128" preserveAspectRatio="xMidYMid meet"/>
<text x="18" y="290" font-family="sans-serif" font-size="13" font-weight="700" fill="#e7ecf5">裁判系统客户端</text>
<text x="18" y="314" font-family="sans-serif" font-size="9" fill="#94a1b8">仅供第九届机甲大师校内赛使用</text>
<text x="18" y="330" font-family="sans-serif" font-size="7.5" fill="#616e86">黑龙江大学 Simba 战队</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+2 -2
View File
@@ -40,8 +40,8 @@
"active": true,
"targets": "all",
"category": "Utility",
"shortDescription": "RMIC::2026 —— 视野链路终端",
"longDescription": "RMIC::2026 视野链路终端。用户端:一路画面 + 装甲条与瞄准器。裁判端:两路画面 + 双方血量 + 装甲板发现列表 + 截图与录制。",
"shortDescription": "RMIC::2026 裁判系统客户端",
"longDescription": "RMIC::2026 裁判系统客户端。用户端:一路画面 + 装甲条与瞄准器。裁判端:两路画面 + 双方血量 + 装甲板发现列表 + 截图与录制。",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://schema.tauri.app/config/2",
"build": {
"beforeBuildCommand": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/prepare-windows-bundle.ps1 && pnpm build",
"beforeBundleCommand": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/prepare-windows-bundle.ps1"
},
"bundle": {
"windows": {
"nsis": {
"installMode": "currentUser",
"languages": ["SimpChinese"],
"displayLanguageSelector": false,
"customLanguageFiles": {
"SimpChinese": "installer/SimpChinese.nsh"
},
"installerHooks": "installer/hooks.nsh",
"installerIcon": "icons/icon.ico",
"uninstallerIcon": "icons/icon.ico",
"headerImage": "installer/header.bmp",
"sidebarImage": "installer/sidebar.bmp",
"uninstallerHeaderImage": "installer/header.bmp"
}
}
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ export function Boot({ milestones, progress, fatal, leaving = false }: Props) {
<span className="boot__name">
RMIC<span className="boot__slash">::</span>2026
</span>
<span className="boot__sub label">视野链路终端 · SIMBA ROBOTICS</span>
<span className="boot__sub label">裁判系统客户端 · SIMBA ROBOTICS</span>
</div>
</header>
+1 -1
View File
@@ -350,7 +350,7 @@ export function SettingsPanel({
{cameraOptions.length === 0 && (
<p className="sect__warn">
尚未发现图传板。板卡每 2 秒广播一次;请检查供电,确认与本机处于
尚未发现图传板。裁判系统每 2 秒广播一次;请检查供电,确认与本机处于
同一网络后稍候。
</p>
)}
+1 -1
View File
@@ -63,7 +63,7 @@ export function TopBar({
<button
type="button"
className="tag tag--warn tag--button"
title="画面与血量由本机合成,不代表现场板卡状态。"
title="画面与血量由本机合成,不代表现场裁判系统状态。"
onClick={() =>
pushNote(
"演示模式:画面与血量由本机合成。截图和验证时请注明为演示数据。",
+2 -2
View File
@@ -11,11 +11,11 @@ export const BUILD = {
minor: 2,
patch: 2,
/** git 提交数,单调递增。 */
build: 38,
build: 39,
/** 线路协议版本。与固件自报的那个数直接比对。 */
protocol: 1,
/** 生成时的提交日期(`YYYY-MM-DD`)。 */
date: "2026-10-19",
date: "2026-10-21",
} as const;
/** `"0.2.0+57"` —— 界面上放得下的那一行。 */
+2 -2
View File
@@ -119,7 +119,7 @@ export function TeamPage({ snapshot, hud, onOpenSettings, calibrating, onCalibra
camera={camera}
label={`CAM · ${cameraText}`}
emptyText={
stats?.hasFrames ? "" : stats?.online ? "已连接,等待画面…" : "离线:尚未收到板卡广播"
stats?.hasFrames ? "" : stats?.online ? "已连接,等待画面…" : "离线:尚未收到裁判系统广播"
}
mirrored={snapshot.mirrored}
crosshair={hud.crosshair}
@@ -179,7 +179,7 @@ export function TeamPage({ snapshot, hud, onOpenSettings, calibrating, onCalibra
<div className="fwfault" role="alert">
<span className="fwfault__tag label">固件不匹配</span>
<span className="fwfault__text">
板卡固件版本为 <b>{firmwareLabel(boundCamera)}</b>、线路协议{" "}
裁判系统固件版本为 <b>{firmwareLabel(boundCamera)}</b>、线路协议{" "}
<b>{boundCamera.protocol}</b>;本客户端使用协议 <b>{BUILD.protocol}</b>。
画面无法接收。请烧录协议匹配的固件,或使用对应版本的客户端。
</span>