mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
- 将 WechatExplorer 产品身份升级为 TraceMemo - 更新 appId、Runtime Identity、Reader Skill 和 API 环境变量 - 增加旧用户数据、Knowledge、Token 与 AI Provider 安全迁移 - 保留 Windows WeFlow 和旧版配置兼容 - 完善首次启动迁移测试及 v2.2.0 发布文档 - 移除 macOS Intel x64 构建与发布支持
149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
import crypto from 'crypto'
|
|
import { app, safeStorage } from 'electron'
|
|
import fs from 'fs-extra'
|
|
import path from 'path'
|
|
import type {
|
|
ApiTokenActionResult,
|
|
ApiTokenRevealResult,
|
|
ApiTokenStatus
|
|
} from '../shared/local-api-auth'
|
|
|
|
const MASKED_TOKEN = '••••••••••••••••'
|
|
const TOKEN_BYTES = 32
|
|
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
|
|
|
|
interface TokenReadResult {
|
|
success: boolean
|
|
token?: string
|
|
error?: string
|
|
}
|
|
|
|
export class ApiTokenStore {
|
|
private cachedToken: string | null = null
|
|
private automaticGenerationBlockedReason: string | null = null
|
|
|
|
constructor(private readonly filePathOverride?: string) {}
|
|
|
|
private get filePath(): string {
|
|
return this.filePathOverride || path.join(app.getPath('userData'), 'local-api-token.bin')
|
|
}
|
|
|
|
getStatus(): ApiTokenStatus {
|
|
const available = safeStorage.isEncryptionAvailable()
|
|
if (!available) {
|
|
return {
|
|
available: false,
|
|
hasToken: false,
|
|
maskedToken: MASKED_TOKEN,
|
|
error: '系统安全存储不可用,本地 API 已安全停用。请检查系统钥匙串或凭据服务后重试。'
|
|
}
|
|
}
|
|
const result = this.read()
|
|
return {
|
|
available: true,
|
|
hasToken: result.success && Boolean(result.token),
|
|
maskedToken: MASKED_TOKEN,
|
|
...(result.success
|
|
? result.token || !this.automaticGenerationBlockedReason
|
|
? {}
|
|
: { error: this.automaticGenerationBlockedReason }
|
|
: { error: result.error })
|
|
}
|
|
}
|
|
|
|
setAutomaticGenerationBlocked(reason?: string): void {
|
|
this.automaticGenerationBlockedReason = reason?.trim() || null
|
|
}
|
|
|
|
ensureToken(): ApiTokenActionResult {
|
|
if (!safeStorage.isEncryptionAvailable()) {
|
|
return {
|
|
success: false,
|
|
available: false,
|
|
hasToken: false,
|
|
maskedToken: MASKED_TOKEN,
|
|
error: '系统安全存储不可用,本地 API 已安全停用。请检查系统钥匙串或凭据服务后重试。'
|
|
}
|
|
}
|
|
const current = this.read()
|
|
if (!current.success) return this.actionError(current.error)
|
|
if (current.token) return this.actionSuccess()
|
|
if (this.automaticGenerationBlockedReason) {
|
|
return this.actionError(this.automaticGenerationBlockedReason)
|
|
}
|
|
return this.persist(this.generateToken())
|
|
}
|
|
|
|
revealToken(): ApiTokenRevealResult {
|
|
const ensured = this.ensureToken()
|
|
if (!ensured.success) return ensured
|
|
return { ...ensured, token: this.cachedToken || undefined }
|
|
}
|
|
|
|
rotateToken(): ApiTokenActionResult {
|
|
if (!safeStorage.isEncryptionAvailable()) return this.ensureToken()
|
|
const result = this.persist(this.generateToken())
|
|
if (result.success) this.automaticGenerationBlockedReason = null
|
|
return result
|
|
}
|
|
|
|
getTokenForAuthentication(): string | null {
|
|
if (this.cachedToken) return this.cachedToken
|
|
const result = this.read()
|
|
return result.success ? result.token || null : null
|
|
}
|
|
|
|
private generateToken(): string {
|
|
return crypto.randomBytes(TOKEN_BYTES).toString('base64url')
|
|
}
|
|
|
|
private read(): TokenReadResult {
|
|
if (this.cachedToken) return { success: true, token: this.cachedToken }
|
|
if (!safeStorage.isEncryptionAvailable()) {
|
|
return { success: false, error: '系统安全存储不可用' }
|
|
}
|
|
if (!fs.existsSync(this.filePath)) return { success: true }
|
|
try {
|
|
const token = safeStorage.decryptString(fs.readFileSync(this.filePath))
|
|
if (!TOKEN_PATTERN.test(token)) throw new Error('invalid token data')
|
|
this.cachedToken = token
|
|
return { success: true, token }
|
|
} catch {
|
|
return { success: false, error: '已保存的 API Token 无法从系统安全存储读取' }
|
|
}
|
|
}
|
|
|
|
private persist(token: string): ApiTokenActionResult {
|
|
try {
|
|
fs.ensureDirSync(path.dirname(this.filePath))
|
|
fs.writeFileSync(this.filePath, safeStorage.encryptString(token), { mode: 0o600 })
|
|
fs.chmodSync(this.filePath, 0o600)
|
|
this.cachedToken = token
|
|
return this.actionSuccess()
|
|
} catch {
|
|
return this.actionError('API Token 无法保存到系统安全存储')
|
|
}
|
|
}
|
|
|
|
private actionSuccess(): ApiTokenActionResult {
|
|
return {
|
|
success: true,
|
|
available: true,
|
|
hasToken: true,
|
|
maskedToken: MASKED_TOKEN
|
|
}
|
|
}
|
|
|
|
private actionError(error?: string): ApiTokenActionResult {
|
|
return {
|
|
success: false,
|
|
available: safeStorage.isEncryptionAvailable(),
|
|
hasToken: false,
|
|
maskedToken: MASKED_TOKEN,
|
|
error: error || 'API Token 安全存储不可用'
|
|
}
|
|
}
|
|
}
|
|
|
|
export const apiTokenStore = new ApiTokenStore()
|