mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-29 16:03:34 +08:00
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>
This commit is contained in:
parent
88e7e14598
commit
4bcac9d25f
107
adapters/common/pairing.ts
Normal file
107
adapters/common/pairing.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 配对核心逻辑
|
||||||
|
*
|
||||||
|
* - 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user