新增管理员可视化工具, 替代 CLI gen-authcode.js: - WinForms GUI: 用户名/Sub-Key/有效期 + 一键生成 + 一键复制 - 品牌视觉: 白底 + #5865F2 蓝紫 + 深色标题栏 + 书虫学者图标 - PS2EXE -NoConsole -NoOutput (92 KB) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
338 lines
14 KiB
PowerShell
338 lines
14 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 }
|
|
|
|
$GenScript = Join-Path $ScriptDir "gen-authcode.js"
|
|
$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)
|
|
|
|
# ─── 输入区 ───────────────────────────────────────────
|
|
$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)
|