cc-haha/adapters/common/pairing.ts
程序员阿江(Relakkes) 4bcac9d25f feat(pairing): implement core pairing logic module
Add adapters/common/pairing.ts with generatePairingCode(), isPaired(),
and tryPair() — one-time-use 6-char code flow with atomic config writes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 20:07:44 +08:00

108 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 配对核心逻辑
*
* - generatePairingCode(): 生成 6 位安全配对码
* - isPaired(): 检查用户是否已配对pairedUsers + allowedUsers 并集)
* - tryPair(): 验证配对码,成功则写入 pairedUsers 并清除 code
*/
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import * as crypto from 'node:crypto'
import type { PairedUser, PairingState } from './config.js'
const SAFE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' // 排除 0/O/1/I/L
const CODE_LENGTH = 6
const CODE_TTL_MS = 60 * 60 * 1000 // 60 minutes
function getConfigPath(): string {
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
return path.join(configDir, 'adapters.json')
}
function readConfigFile(): Record<string, any> {
try {
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'))
} catch {
return {}
}
}
function writeConfigFile(data: Record<string, any>): void {
const filePath = getConfigPath()
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
const tmp = `${filePath}.tmp.${Date.now()}`
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf-8')
fs.renameSync(tmp, filePath)
}
export function generatePairingCode(): string {
let code = ''
for (let i = 0; i < CODE_LENGTH; i++) {
code += SAFE_ALPHABET[crypto.randomInt(SAFE_ALPHABET.length)]
}
return code
}
/** 检查用户是否已配对pairedUsers + allowedUsers 并集) */
export function isPaired(
platform: 'telegram' | 'feishu',
userId: string | number,
config: Record<string, any>,
): boolean {
const platformConfig = config[platform] ?? {}
const allowedUsers: (string | number)[] = platformConfig.allowedUsers ?? []
const pairedUsers: PairedUser[] = platformConfig.pairedUsers ?? []
// allowedUsers 非空时检查
if (allowedUsers.length > 0 && allowedUsers.includes(userId)) return true
// allowedUsers 为空且 pairedUsers 也为空 => 旧行为(允许所有人)
if (pairedUsers.length === 0 && allowedUsers.length === 0) return true
return pairedUsers.some((p) => String(p.userId) === String(userId))
}
/**
* 尝试配对:验证消息文本是否匹配当前有效配对码。
* 成功则写入 pairedUsers 并清除 pairing.code返回 true。
*/
export function tryPair(
messageText: string,
senderInfo: { userId: string | number; displayName: string },
platform: 'telegram' | 'feishu',
): boolean {
const file = readConfigFile()
const pairing: PairingState = file.pairing ?? { code: null, expiresAt: null, createdAt: null }
// 检查配对码是否有效
if (!pairing.code || !pairing.expiresAt) return false
if (Date.now() > pairing.expiresAt) return false
// 比较(忽略大小写和空格)
const input = messageText.trim().toUpperCase()
if (input !== pairing.code.toUpperCase()) return false
// 配对成功:写入 pairedUsers
const platformConfig = file[platform] ?? {}
const pairedUsers: PairedUser[] = platformConfig.pairedUsers ?? []
// 避免重复
const exists = pairedUsers.some((p) => String(p.userId) === String(senderInfo.userId))
if (!exists) {
pairedUsers.push({
userId: senderInfo.userId,
displayName: senderInfo.displayName,
pairedAt: Date.now(),
})
}
// 更新 config
file[platform] = { ...platformConfig, pairedUsers }
file.pairing = { code: null, expiresAt: null, createdAt: null } // 一次性使用
writeConfigFile(file)
return true
}