fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
- 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。未执行实机烧录。
@@ -18,3 +18,6 @@ dist/
|
||||
|
||||
# 测试运行时被 XDG_CONFIG_HOME 相对路径写出来的配置,不是仓库内容。
|
||||
src-tauri/.config/
|
||||
|
||||
# Installer hotspot guidance is local configuration.
|
||||
/src-tauri/installer/network.local.nsh
|
||||
|
||||
@@ -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()
|
||||
@@ -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)."
|
||||
@@ -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 }
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 917 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
@@ -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} "同时删除应用数据"
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
@@ -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 |
@@ -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
|
||||
@@ -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 "现场热点密码"
|
||||
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ export function SettingsPanel({
|
||||
|
||||
{cameraOptions.length === 0 && (
|
||||
<p className="sect__warn">
|
||||
尚未发现图传板。板卡每 2 秒广播一次;请检查供电,确认与本机处于
|
||||
尚未发现图传板。裁判系统每 2 秒广播一次;请检查供电,确认与本机处于
|
||||
同一网络后稍候。
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -63,7 +63,7 @@ export function TopBar({
|
||||
<button
|
||||
type="button"
|
||||
className="tag tag--warn tag--button"
|
||||
title="画面与血量由本机合成,不代表现场板卡状态。"
|
||||
title="画面与血量由本机合成,不代表现场裁判系统状态。"
|
||||
onClick={() =>
|
||||
pushNote(
|
||||
"演示模式:画面与血量由本机合成。截图和验证时请注明为演示数据。",
|
||||
|
||||
@@ -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"` —— 界面上放得下的那一行。 */
|
||||
|
||||
@@ -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>
|
||||
|
||||