cc-haha/adapters/common/permission.ts
程序员阿江(Relakkes) 7ca25687fd feat: make IM permission approval mobile-friendly
Text-only IM channels forced users to copy long permission request IDs from mobile chat, which made approval slow and error-prone. This keeps the requestId-based authorization protocol intact while adding short replies for the single-pending-request case and preserving full command fallbacks for ambiguous cases.

Constraint: IM authorization must still resolve through the existing requestId permission_response path
Rejected: Replace request IDs with global numeric IDs | concurrent permission prompts would make numeric IDs ambiguous across chats
Confidence: high
Scope-risk: moderate
Directive: Do not allow short numeric replies when more than one permission request is pending in the chat
Tested: cd adapters && bun test common/ telegram/ feishu/ dingtalk/ wechat/
Tested: cd adapters && bunx tsc --noEmit
Tested: bun run check:adapters
Not-tested: Real WeChat or Telegram account end-to-end message delivery
2026-05-05 21:06:58 +08:00

71 lines
2.4 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.

export type PermissionDecision = {
requestId: string
allowed: boolean
rule?: 'always'
}
function getSinglePendingRequestId(requestIds?: Iterable<string> | null): string | null {
if (!requestIds) return null
const ids = Array.from(requestIds)
return ids.length === 1 ? ids[0]! : null
}
export function parsePermissionCommand(
text: string,
pendingRequestIds?: Iterable<string> | null,
): PermissionDecision | null {
const trimmed = text.trim()
const match = text.trim().match(/^\/(allow|always|allow-always|deny)\s+(\S+)/i)
if (match) {
const action = match[1]!.toLowerCase()
const requestId = match[2]!
if (action === 'deny') return { requestId, allowed: false }
if (action === 'always' || action === 'allow-always') return { requestId, allowed: true, rule: 'always' }
return { requestId, allowed: true }
}
const requestId = getSinglePendingRequestId(pendingRequestIds)
if (!requestId) return null
const shortcut = trimmed.toLowerCase()
if (['1', '/1', 'allow', '/allow', 'y', 'yes', '允许', '允许一次', '同意', '批准'].includes(shortcut)) {
return { requestId, allowed: true }
}
if (['2', '/2', 'always', '/always', 'allow-always', '/allow-always', '永久允许', '一直允许'].includes(shortcut)) {
return { requestId, allowed: true, rule: 'always' }
}
if (['3', '/3', 'deny', '/deny', 'n', 'no', '拒绝', '不允许', '否'].includes(shortcut)) {
return { requestId, allowed: false }
}
return null
}
export function parsePermitCallbackData(data: string): PermissionDecision | null {
const parts = data.split(':')
if (parts.length !== 3 || parts[0] !== 'permit' || !parts[1]) return null
switch (parts[2]) {
case 'yes':
return { requestId: parts[1], allowed: true }
case 'always':
return { requestId: parts[1], allowed: true, rule: 'always' }
case 'no':
return { requestId: parts[1], allowed: false }
default:
return null
}
}
export function formatPermissionInstructions(requestId: string): string {
return [
'回复 1 允许一次2 永久允许3 拒绝。',
`也可回复 /allow ${requestId}、/always ${requestId}、/deny ${requestId}`,
].join('\n')
}
export function formatPermissionDecisionStatus(decision: Pick<PermissionDecision, 'allowed' | 'rule'>): string {
if (!decision.allowed) return '❌ 已拒绝'
return decision.rule === 'always' ? '♾️ 已永久允许' : '✅ 已允许'
}