Files
程序员阿江(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

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' ? '♾️ 已永久允许' : '✅ 已允许'
}