cc-haha/adapters/common/permission.ts
程序员阿江(Relakkes) f1c5f86b80 feat: keep IM permission approvals consistent
DingTalk can receive interactive card callbacks, but the card UI still depends on a published template, so the adapter now supports a template-backed card path with text commands as the reliable fallback. Telegram and WeChat use the same allow-once, allow-always, and deny semantics so manual authorization behaves the same across platforms.

Constraint: DingTalk button rendering requires an operator-provided interactive card template id
Rejected: Treat the existing AI streaming card template as a permission card | it cannot guarantee visible action buttons
Confidence: high
Scope-risk: moderate
Directive: Do not remove the text approval fallback unless DingTalk card template provisioning is guaranteed
Tested: bun test adapters/common/__tests__/permission.test.ts adapters/dingtalk/__tests__/permission-card.test.ts adapters/telegram/__tests__/telegram.test.ts adapters/common/__tests__/config.test.ts src/server/__tests__/adapters.test.ts
Tested: bunx tsc -p adapters/tsconfig.json --noEmit
Tested: bun run check:adapters
Tested: cd desktop && bun run build
Tested: bun run check:server
Tested: bun run check:docs
Tested: git diff --check
Not-tested: bun run quality:pr is blocked by existing CLI core approval policy; see artifacts/quality-runs/2026-05-03T13-19-56-232Z/report.md
2026-05-04 19:03:36 +08:00

46 lines
1.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.

export type PermissionDecision = {
requestId: string
allowed: boolean
rule?: 'always'
}
export function parsePermissionCommand(text: string): PermissionDecision | null {
const match = text.trim().match(/^\/(allow|always|allow-always|deny)\s+(\S+)/i)
if (!match) return null
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 }
}
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 [
`回复 /allow ${requestId} 允许一次`,
`/always ${requestId} 永久允许`,
`/deny ${requestId} 拒绝。`,
].join('')
}
export function formatPermissionDecisionStatus(decision: Pick<PermissionDecision, 'allowed' | 'rule'>): string {
if (!decision.allowed) return '❌ 已拒绝'
return decision.rule === 'always' ? '♾️ 已永久允许' : '✅ 已允许'
}