bookworm-boot/admin-authcode-gui.ps1
bookworm c2e3f0e65f fix(admin): AuthGen $y 类型修复 — [int]$y 防止 PS2EXE Object[] 降级
PS2EXE 编译后 $y 被隐式当作 Object[], 导致 $y - 2 算术运算失败:
"Method invocation failed because [System.Object[]] does not contain
a method named 'op_Subtraction'"

修复: 强类型声明 [int]$y = 72

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:15:10 +08:00

344 lines
15 KiB
PowerShell

<#
.SYNOPSIS
Bookworm 授权码生成器 (管理员 GUI 工具)
.DESCRIPTION
内部调用 node gen-authcode.js, 提供可视化界面生成多用户授权码。
打包命令: 见 build.ps1 -Admin
#>
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
# ─── 路径 ─────────────────────────────────────────────
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot }
elseif ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName -match '\.exe$') {
Split-Path -Parent ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)
} elseif ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else { $PWD.Path }
# gen-authcode.js / secrets.txt 查找: 当前目录 → 父目录 (dist/ 内运行时)
$GenScript = Join-Path $ScriptDir "gen-authcode.js"
if (-not (Test-Path $GenScript)) {
$parentDir = Split-Path $ScriptDir -Parent
$GenScript = Join-Path $parentDir "gen-authcode.js"
if (Test-Path $GenScript) { $ScriptDir = $parentDir } # 切到父目录
}
$SecretsTxt = Join-Path $ScriptDir "secrets.txt"
# ─── 品牌色 ───────────────────────────────────────────
$brandBlue = [System.Drawing.Color]::FromArgb(88, 101, 242)
$brandDark = [System.Drawing.Color]::FromArgb(30, 31, 46)
$brandLight = [System.Drawing.Color]::FromArgb(245, 245, 250)
$successGreen = [System.Drawing.Color]::FromArgb(46, 160, 67)
$warningOrange = [System.Drawing.Color]::FromArgb(227, 137, 29)
# ─── 主窗口 ───────────────────────────────────────────
$form = New-Object System.Windows.Forms.Form
$form.Text = "Bookworm 授权码生成器 — 管理员工具"
$form.Size = New-Object System.Drawing.Size(560, 580)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.BackColor = [System.Drawing.Color]::White
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
# ─── 标题栏 ───────────────────────────────────────────
$header = New-Object System.Windows.Forms.Panel
$header.Location = New-Object System.Drawing.Point(0, 0)
$header.Size = New-Object System.Drawing.Size(560, 56)
$header.BackColor = $brandDark
$form.Controls.Add($header)
$titleLabel = New-Object System.Windows.Forms.Label
$titleLabel.Location = New-Object System.Drawing.Point(20, 14)
$titleLabel.Size = New-Object System.Drawing.Size(520, 28)
$titleLabel.Text = "Bookworm 授权码生成器"
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold)
$titleLabel.ForeColor = [System.Drawing.Color]::White
$header.Controls.Add($titleLabel)
# ─── 输入区 ───────────────────────────────────────────
[int]$y = 72
# 用户名
$lblUser = New-Object System.Windows.Forms.Label
$lblUser.Location = New-Object System.Drawing.Point(24, $y)
$lblUser.Size = New-Object System.Drawing.Size(120, 22)
$lblUser.Text = "用户标识 (--user):"
$form.Controls.Add($lblUser)
$txtUser = New-Object System.Windows.Forms.TextBox
$txtUser.Location = New-Object System.Drawing.Point(150, $y - 2)
$txtUser.Size = New-Object System.Drawing.Size(370, 28)
$txtUser.Font = New-Object System.Drawing.Font("Consolas", 11)
$txtUser.PlaceholderText = "例如: alice, 张三, 茶师兄"
$form.Controls.Add($txtUser)
$y += 38
# Relay Sub-Key
$lblKey = New-Object System.Windows.Forms.Label
$lblKey.Location = New-Object System.Drawing.Point(24, $y)
$lblKey.Size = New-Object System.Drawing.Size(120, 22)
$lblKey.Text = "Relay Sub-Key:"
$form.Controls.Add($lblKey)
$txtKey = New-Object System.Windows.Forms.TextBox
$txtKey.Location = New-Object System.Drawing.Point(150, $y - 2)
$txtKey.Size = New-Object System.Drawing.Size(370, 28)
$txtKey.Font = New-Object System.Drawing.Font("Consolas", 10)
$txtKey.PasswordChar = '*'
$txtKey.PlaceholderText = "sk-... (中转站后台创建的子 Key)"
$form.Controls.Add($txtKey)
$y += 28
# 显示/隐藏 Key
$chkShowKey = New-Object System.Windows.Forms.CheckBox
$chkShowKey.Location = New-Object System.Drawing.Point(150, $y)
$chkShowKey.Size = New-Object System.Drawing.Size(120, 22)
$chkShowKey.Text = "显示 Key"
$chkShowKey.Add_CheckedChanged({ $txtKey.PasswordChar = if ($chkShowKey.Checked) { [char]0 } else { '*' } })
$form.Controls.Add($chkShowKey)
$y += 32
# 有效期
$lblDays = New-Object System.Windows.Forms.Label
$lblDays.Location = New-Object System.Drawing.Point(24, $y)
$lblDays.Size = New-Object System.Drawing.Size(120, 22)
$lblDays.Text = "有效期 (天):"
$form.Controls.Add($lblDays)
$cmbDays = New-Object System.Windows.Forms.ComboBox
$cmbDays.Location = New-Object System.Drawing.Point(150, $y - 2)
$cmbDays.Size = New-Object System.Drawing.Size(100, 28)
$cmbDays.DropDownStyle = "DropDownList"
@(7, 14, 30, 60, 90, 180, 365) | ForEach-Object { $cmbDays.Items.Add($_) | Out-Null }
$cmbDays.SelectedIndex = 2 # 默认 30 天
$form.Controls.Add($cmbDays)
$lblDaysHint = New-Object System.Windows.Forms.Label
$lblDaysHint.Location = New-Object System.Drawing.Point(260, $y)
$lblDaysHint.Size = New-Object System.Drawing.Size(260, 22)
$lblDaysHint.Text = ""
$lblDaysHint.ForeColor = [System.Drawing.Color]::Gray
$form.Controls.Add($lblDaysHint)
$cmbDays.Add_SelectedIndexChanged({
$d = [int]$cmbDays.SelectedItem
$exp = (Get-Date).AddDays($d).ToString("yyyy-MM-dd")
$lblDaysHint.Text = "到期: $exp"
})
# 触发初始值
$cmbDays.SelectedIndex = 2
$y += 38
# secrets.txt 状态
$lblSecrets = New-Object System.Windows.Forms.Label
$lblSecrets.Location = New-Object System.Drawing.Point(24, $y)
$lblSecrets.Size = New-Object System.Drawing.Size(500, 22)
if (Test-Path $SecretsTxt) {
$lineCount = (Get-Content $SecretsTxt -ErrorAction SilentlyContinue | Where-Object { $_ -match '=' }).Count
$lblSecrets.Text = "secrets.txt: $lineCount 个凭证已配置"
$lblSecrets.ForeColor = $successGreen
} else {
$lblSecrets.Text = "secrets.txt: 未找到 ($SecretsTxt)"
$lblSecrets.ForeColor = [System.Drawing.Color]::Red
}
$form.Controls.Add($lblSecrets)
$y += 28
# Node.js 状态
$lblNode = New-Object System.Windows.Forms.Label
$lblNode.Location = New-Object System.Drawing.Point(24, $y)
$lblNode.Size = New-Object System.Drawing.Size(500, 22)
$nodeOK = [bool](Get-Command node -ErrorAction SilentlyContinue)
if ($nodeOK) {
$nodeVer = try { (& node --version 2>$null) } catch { "" }
$lblNode.Text = "Node.js: $nodeVer"
$lblNode.ForeColor = $successGreen
} else {
$lblNode.Text = "Node.js: 未安装 (必需)"
$lblNode.ForeColor = [System.Drawing.Color]::Red
}
$form.Controls.Add($lblNode)
$y += 36
# ─── 生成按钮 ─────────────────────────────────────────
$btnGenerate = New-Object System.Windows.Forms.Button
$btnGenerate.Location = New-Object System.Drawing.Point(150, $y)
$btnGenerate.Size = New-Object System.Drawing.Size(160, 40)
$btnGenerate.Text = "生成授权码"
$btnGenerate.Font = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold)
$btnGenerate.FlatStyle = "Flat"
$btnGenerate.BackColor = $brandBlue
$btnGenerate.ForeColor = [System.Drawing.Color]::White
$btnGenerate.FlatAppearance.BorderSize = 0
$btnGenerate.Cursor = [System.Windows.Forms.Cursors]::Hand
$form.Controls.Add($btnGenerate)
$btnClear = New-Object System.Windows.Forms.Button
$btnClear.Location = New-Object System.Drawing.Point(320, $y)
$btnClear.Size = New-Object System.Drawing.Size(80, 40)
$btnClear.Text = "清空"
$btnClear.FlatStyle = "Flat"
$btnClear.FlatAppearance.BorderColor = [System.Drawing.Color]::LightGray
$form.Controls.Add($btnClear)
$y += 54
# ─── 分隔线 ───────────────────────────────────────────
$sep = New-Object System.Windows.Forms.Label
$sep.Location = New-Object System.Drawing.Point(20, $y)
$sep.Size = New-Object System.Drawing.Size(510, 1)
$sep.BorderStyle = "Fixed3D"
$form.Controls.Add($sep)
$y += 10
# ─── 结果区 ───────────────────────────────────────────
$lblResultTitle = New-Object System.Windows.Forms.Label
$lblResultTitle.Location = New-Object System.Drawing.Point(24, $y)
$lblResultTitle.Size = New-Object System.Drawing.Size(200, 22)
$lblResultTitle.Text = "生成结果"
$lblResultTitle.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
$lblResultTitle.ForeColor = $brandDark
$form.Controls.Add($lblResultTitle)
$y += 26
# 授权码 (大字 Consolas, 可选中复制)
$txtAuthCode = New-Object System.Windows.Forms.TextBox
$txtAuthCode.Location = New-Object System.Drawing.Point(24, $y)
$txtAuthCode.Size = New-Object System.Drawing.Size(400, 32)
$txtAuthCode.Font = New-Object System.Drawing.Font("Consolas", 13, [System.Drawing.FontStyle]::Bold)
$txtAuthCode.ReadOnly = $true
$txtAuthCode.BackColor = $brandLight
$txtAuthCode.ForeColor = $brandDark
$txtAuthCode.Text = ""
$form.Controls.Add($txtAuthCode)
$btnCopy = New-Object System.Windows.Forms.Button
$btnCopy.Location = New-Object System.Drawing.Point(430, $y)
$btnCopy.Size = New-Object System.Drawing.Size(90, 32)
$btnCopy.Text = "复制"
$btnCopy.FlatStyle = "Flat"
$btnCopy.BackColor = $brandLight
$btnCopy.FlatAppearance.BorderColor = [System.Drawing.Color]::LightGray
$btnCopy.Enabled = $false
$form.Controls.Add($btnCopy)
$y += 40
# 详细信息
$lblDetails = New-Object System.Windows.Forms.Label
$lblDetails.Location = New-Object System.Drawing.Point(24, $y)
$lblDetails.Size = New-Object System.Drawing.Size(500, 66)
$lblDetails.Text = ""
$lblDetails.ForeColor = [System.Drawing.Color]::FromArgb(100, 100, 120)
$form.Controls.Add($lblDetails)
$y += 70
# 状态栏
$statusBar = New-Object System.Windows.Forms.Label
$statusBar.Location = New-Object System.Drawing.Point(0, $y)
$statusBar.Size = New-Object System.Drawing.Size(560, 24)
$statusBar.BackColor = $brandLight
$statusBar.ForeColor = [System.Drawing.Color]::Gray
$statusBar.Text = " 就绪 | $ScriptDir"
$statusBar.Font = New-Object System.Drawing.Font("Segoe UI", 8)
$statusBar.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$form.Controls.Add($statusBar)
# ─── 事件处理 ─────────────────────────────────────────
$btnCopy.Add_Click({
if ($txtAuthCode.Text) {
[System.Windows.Forms.Clipboard]::SetText($txtAuthCode.Text)
$statusBar.Text = " 已复制到剪贴板"
$statusBar.ForeColor = $successGreen
}
})
$btnClear.Add_Click({
$txtUser.Text = ""
$txtKey.Text = ""
$txtAuthCode.Text = ""
$lblDetails.Text = ""
$btnCopy.Enabled = $false
$statusBar.Text = " 已清空"
$statusBar.ForeColor = [System.Drawing.Color]::Gray
})
$btnGenerate.Add_Click({
# 校验
$user = $txtUser.Text.Trim()
$key = $txtKey.Text.Trim()
$days = $cmbDays.SelectedItem.ToString()
if (-not $user) {
[System.Windows.Forms.MessageBox]::Show("请输入用户标识", "缺少用户名", "OK", "Warning")
$txtUser.Focus(); return
}
if (-not $key) {
[System.Windows.Forms.MessageBox]::Show("请输入 Relay Sub-Key`n(从中转站后台为用户创建)", "缺少 Sub-Key", "OK", "Warning")
$txtKey.Focus(); return
}
if (-not (Test-Path $SecretsTxt)) {
[System.Windows.Forms.MessageBox]::Show("secrets.txt 不存在:`n$SecretsTxt`n`n请先创建 secrets.txt (每行 KEY=VALUE)", "缺少凭证文件", "OK", "Error")
return
}
if (-not $nodeOK) {
[System.Windows.Forms.MessageBox]::Show("Node.js 未安装。`n请先安装: https://nodejs.org", "缺少 Node.js", "OK", "Error")
return
}
$statusBar.Text = " 生成中..."
$statusBar.ForeColor = $warningOrange
$btnGenerate.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
try {
$args = @($GenScript, $days, "-k", $key, "-u", $user)
$proc = Start-Process node -ArgumentList $args -NoNewWindow -PassThru `
-RedirectStandardOutput "$env:TEMP\bw-gen-out.tmp" `
-RedirectStandardError "$env:TEMP\bw-gen-err.tmp" `
-WorkingDirectory $ScriptDir
$sw = [System.Diagnostics.Stopwatch]::StartNew()
while (-not $proc.HasExited -and $sw.ElapsedMilliseconds -lt 30000) {
[System.Windows.Forms.Application]::DoEvents()
Start-Sleep -Milliseconds 100
}
if (-not $proc.HasExited) { $proc.Kill(); throw "超时 (30s)" }
$stdout = Get-Content "$env:TEMP\bw-gen-out.tmp" -Raw -ErrorAction SilentlyContinue
$stderr = Get-Content "$env:TEMP\bw-gen-err.tmp" -Raw -ErrorAction SilentlyContinue
Remove-Item "$env:TEMP\bw-gen-out.tmp", "$env:TEMP\bw-gen-err.tmp" -Force -ErrorAction SilentlyContinue
if ($proc.ExitCode -ne 0) {
throw "gen-authcode.js 退出码 $($proc.ExitCode)`n$stderr"
}
# 解析输出
$authCode = if ($stdout -match '授权码:\s*(BW-\d{8}-[A-F0-9]{24})') { $Matches[1] } else { "" }
$fileId = if ($stdout -match '文件 ID:\s*([a-f0-9]{8})\s*→\s*(secrets-[a-f0-9]{8}\.enc)') { $Matches[2] } else { "" }
$expiry = if ($stdout -match '有效期:\s*\d+\s*天\s*\(至\s*(\d{4}-\d{2}-\d{2})\)') { $Matches[1] } else { "" }
if ($authCode) {
$txtAuthCode.Text = $authCode
$lblDetails.Text = "用户: $user`n加密文件: $fileId`n有效期: $days 天 (至 $expiry)`n路径: $ScriptDir\$fileId"
$btnCopy.Enabled = $true
$statusBar.Text = " 生成成功 — $fileId | 请 git add + push 后将授权码发给 $user"
$statusBar.ForeColor = $successGreen
} else {
throw "无法解析授权码输出:`n$stdout"
}
} catch {
$txtAuthCode.Text = ""
$lblDetails.Text = "错误: $_"
$btnCopy.Enabled = $false
$statusBar.Text = " 生成失败"
$statusBar.ForeColor = [System.Drawing.Color]::Red
[System.Windows.Forms.MessageBox]::Show("生成失败:`n$_", "错误", "OK", "Error")
} finally {
$btnGenerate.Enabled = $true
}
})
# ─── 启动 ─────────────────────────────────────────────
$form.Add_Shown({ $txtUser.Focus() })
[System.Windows.Forms.Application]::Run($form)