mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +08:00
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
This commit is contained in:
parent
82c454e4dc
commit
7ca25687fd
@ -14,6 +14,19 @@ describe('permission helpers', () => {
|
|||||||
expect(parsePermissionCommand('/deny req-4')).toEqual({ requestId: 'req-4', allowed: false })
|
expect(parsePermissionCommand('/deny req-4')).toEqual({ requestId: 'req-4', allowed: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('parses short replies when one permission is pending', () => {
|
||||||
|
const pending = new Set(['req-1'])
|
||||||
|
expect(parsePermissionCommand('1', pending)).toEqual({ requestId: 'req-1', allowed: true })
|
||||||
|
expect(parsePermissionCommand('2', pending)).toEqual({ requestId: 'req-1', allowed: true, rule: 'always' })
|
||||||
|
expect(parsePermissionCommand('3', pending)).toEqual({ requestId: 'req-1', allowed: false })
|
||||||
|
expect(parsePermissionCommand('/always', pending)).toEqual({ requestId: 'req-1', allowed: true, rule: 'always' })
|
||||||
|
expect(parsePermissionCommand('永久允许', pending)).toEqual({ requestId: 'req-1', allowed: true, rule: 'always' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not parse short replies when multiple permissions are pending', () => {
|
||||||
|
expect(parsePermissionCommand('1', new Set(['req-1', 'req-2']))).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('parses callback permission actions', () => {
|
it('parses callback permission actions', () => {
|
||||||
expect(parsePermitCallbackData('permit:req-1:yes')).toEqual({ requestId: 'req-1', allowed: true })
|
expect(parsePermitCallbackData('permit:req-1:yes')).toEqual({ requestId: 'req-1', allowed: true })
|
||||||
expect(parsePermitCallbackData('permit:req-2:always')).toEqual({ requestId: 'req-2', allowed: true, rule: 'always' })
|
expect(parsePermitCallbackData('permit:req-2:always')).toEqual({ requestId: 'req-2', allowed: true, rule: 'always' })
|
||||||
@ -22,6 +35,7 @@ describe('permission helpers', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('formats text fallback and status labels', () => {
|
it('formats text fallback and status labels', () => {
|
||||||
|
expect(formatPermissionInstructions('req-1')).toContain('回复 1')
|
||||||
expect(formatPermissionInstructions('req-1')).toContain('/always req-1')
|
expect(formatPermissionInstructions('req-1')).toContain('/always req-1')
|
||||||
expect(formatPermissionDecisionStatus({ allowed: true, rule: 'always' })).toContain('永久允许')
|
expect(formatPermissionDecisionStatus({ allowed: true, rule: 'always' })).toContain('永久允许')
|
||||||
expect(formatPermissionDecisionStatus({ allowed: false })).toContain('拒绝')
|
expect(formatPermissionDecisionStatus({ allowed: false })).toContain('拒绝')
|
||||||
|
|||||||
@ -4,10 +4,19 @@ export type PermissionDecision = {
|
|||||||
rule?: 'always'
|
rule?: 'always'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePermissionCommand(text: string): PermissionDecision | null {
|
function getSinglePendingRequestId(requestIds?: Iterable<string> | null): string | null {
|
||||||
const match = text.trim().match(/^\/(allow|always|allow-always|deny)\s+(\S+)/i)
|
if (!requestIds) return null
|
||||||
if (!match) 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 action = match[1]!.toLowerCase()
|
||||||
const requestId = match[2]!
|
const requestId = match[2]!
|
||||||
if (action === 'deny') return { requestId, allowed: false }
|
if (action === 'deny') return { requestId, allowed: false }
|
||||||
@ -15,6 +24,23 @@ export function parsePermissionCommand(text: string): PermissionDecision | null
|
|||||||
return { requestId, allowed: true }
|
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 {
|
export function parsePermitCallbackData(data: string): PermissionDecision | null {
|
||||||
const parts = data.split(':')
|
const parts = data.split(':')
|
||||||
if (parts.length !== 3 || parts[0] !== 'permit' || !parts[1]) return null
|
if (parts.length !== 3 || parts[0] !== 'permit' || !parts[1]) return null
|
||||||
@ -33,10 +59,9 @@ export function parsePermitCallbackData(data: string): PermissionDecision | null
|
|||||||
|
|
||||||
export function formatPermissionInstructions(requestId: string): string {
|
export function formatPermissionInstructions(requestId: string): string {
|
||||||
return [
|
return [
|
||||||
`回复 /allow ${requestId} 允许一次`,
|
'回复 1 允许一次,2 永久允许,3 拒绝。',
|
||||||
`/always ${requestId} 永久允许`,
|
`也可回复 /allow ${requestId}、/always ${requestId}、/deny ${requestId}。`,
|
||||||
`/deny ${requestId} 拒绝。`,
|
].join('\n')
|
||||||
].join(',')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPermissionDecisionStatus(decision: Pick<PermissionDecision, 'allowed' | 'rule'>): string {
|
export function formatPermissionDecisionStatus(decision: Pick<PermissionDecision, 'allowed' | 'rule'>): string {
|
||||||
|
|||||||
@ -456,7 +456,7 @@ async function sendPermissionRequest(chatId: string, msg: ServerMessage): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePermissionCommand(chatId: string, text: string): boolean {
|
function handlePermissionCommand(chatId: string, text: string): boolean {
|
||||||
const decision = parsePermissionCommand(text)
|
const decision = parsePermissionCommand(text, pendingPermissions.get(chatId))
|
||||||
if (!decision) return false
|
if (!decision) return false
|
||||||
|
|
||||||
const sent = applyPermissionDecision(chatId, decision)
|
const sent = applyPermissionDecision(chatId, decision)
|
||||||
|
|||||||
@ -18,8 +18,15 @@ import { getConfiguredWorkDir, loadConfig } from '../common/config.js'
|
|||||||
import {
|
import {
|
||||||
formatImHelp,
|
formatImHelp,
|
||||||
formatImStatus,
|
formatImStatus,
|
||||||
|
formatPermissionRequest,
|
||||||
splitMessage,
|
splitMessage,
|
||||||
} from '../common/format.js'
|
} from '../common/format.js'
|
||||||
|
import {
|
||||||
|
formatPermissionDecisionStatus,
|
||||||
|
formatPermissionInstructions,
|
||||||
|
parsePermissionCommand,
|
||||||
|
type PermissionDecision,
|
||||||
|
} from '../common/permission.js'
|
||||||
import { SessionStore } from '../common/session-store.js'
|
import { SessionStore } from '../common/session-store.js'
|
||||||
import { AdapterHttpClient, type RecentProject } from '../common/http-client.js'
|
import { AdapterHttpClient, type RecentProject } from '../common/http-client.js'
|
||||||
import { isAllowedUser, tryPair } from '../common/pairing.js'
|
import { isAllowedUser, tryPair } from '../common/pairing.js'
|
||||||
@ -63,6 +70,7 @@ attachmentStore.gc().catch((err) => {
|
|||||||
const streamingCards = new Map<string, StreamingCard>()
|
const streamingCards = new Map<string, StreamingCard>()
|
||||||
const pendingProjectSelection = new Map<string, boolean>()
|
const pendingProjectSelection = new Map<string, boolean>()
|
||||||
const runtimeStates = new Map<string, ChatRuntimeState>()
|
const runtimeStates = new Map<string, ChatRuntimeState>()
|
||||||
|
const pendingPermissions = new Map<string, Set<string>>()
|
||||||
|
|
||||||
// Per-chat outbound watchers for Agent-produced markdown image references.
|
// Per-chat outbound watchers for Agent-produced markdown image references.
|
||||||
// `imageWatchers` extracts `` from streaming text;
|
// `imageWatchers` extracts `` from streaming text;
|
||||||
@ -205,6 +213,7 @@ function clearTransientChatState(chatId: string): void {
|
|||||||
runtime.state = 'idle'
|
runtime.state = 'idle'
|
||||||
runtime.verb = undefined
|
runtime.verb = undefined
|
||||||
runtime.pendingPermissionCount = 0
|
runtime.pendingPermissionCount = 0
|
||||||
|
pendingPermissions.delete(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureExistingSession(chatId: string): Promise<{ sessionId: string; workDir: string } | null> {
|
async function ensureExistingSession(chatId: string): Promise<{ sessionId: string; workDir: string } | null> {
|
||||||
@ -728,6 +737,7 @@ async function startNewSession(chatId: string, query?: string): Promise<void> {
|
|||||||
imageWatchers.delete(chatId)
|
imageWatchers.delete(chatId)
|
||||||
uploadedImageKeys.delete(chatId)
|
uploadedImageKeys.delete(chatId)
|
||||||
pendingProjectSelection.delete(chatId)
|
pendingProjectSelection.delete(chatId)
|
||||||
|
pendingPermissions.delete(chatId)
|
||||||
runtimeStates.delete(chatId)
|
runtimeStates.delete(chatId)
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
@ -855,6 +865,9 @@ async function handleServerMessage(chatId: string, msg: ServerMessage): Promise<
|
|||||||
case 'permission_request': {
|
case 'permission_request': {
|
||||||
runtime.pendingPermissionCount += 1
|
runtime.pendingPermissionCount += 1
|
||||||
runtime.state = 'permission_pending'
|
runtime.state = 'permission_pending'
|
||||||
|
const pending = pendingPermissions.get(chatId) ?? new Set<string>()
|
||||||
|
pending.add(msg.requestId)
|
||||||
|
pendingPermissions.set(chatId, pending)
|
||||||
const stored = sessionStore.get(chatId)
|
const stored = sessionStore.get(chatId)
|
||||||
const card = buildPermissionCard(
|
const card = buildPermissionCard(
|
||||||
msg.toolName,
|
msg.toolName,
|
||||||
@ -862,7 +875,13 @@ async function handleServerMessage(chatId: string, msg: ServerMessage): Promise<
|
|||||||
msg.requestId,
|
msg.requestId,
|
||||||
stored?.workDir,
|
stored?.workDir,
|
||||||
)
|
)
|
||||||
await sendCard(chatId, card)
|
const cardId = await sendCard(chatId, card)
|
||||||
|
if (!cardId) {
|
||||||
|
await sendText(
|
||||||
|
chatId,
|
||||||
|
`${formatPermissionRequest(msg.toolName, msg.input, msg.requestId)}\n\n${formatPermissionInstructions(msg.requestId)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -999,6 +1018,12 @@ async function handleMessage(data: any): Promise<void> {
|
|||||||
// ----- Commands (only when there are no attachments — `command + image`
|
// ----- Commands (only when there are no attachments — `command + image`
|
||||||
// isn't a meaningful combo, so attachments always take precedence) -----
|
// isn't a meaningful combo, so attachments always take precedence) -----
|
||||||
|
|
||||||
|
const permissionDecision = !hasAttachments ? parsePermissionCommand(msgText, pendingPermissions.get(chatId)) : null
|
||||||
|
if (permissionDecision) {
|
||||||
|
await handlePermissionDecision(chatId, permissionDecision)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasAttachments && (msgText === '/new' || msgText === '新会话' || msgText.startsWith('/new '))) {
|
if (!hasAttachments && (msgText === '/new' || msgText === '新会话' || msgText.startsWith('/new '))) {
|
||||||
const arg = msgText.startsWith('/new ') ? msgText.slice(5).trim() : ''
|
const arg = msgText.startsWith('/new ') ? msgText.slice(5).trim() : ''
|
||||||
await startNewSession(chatId, arg || undefined)
|
await startNewSession(chatId, arg || undefined)
|
||||||
@ -1142,6 +1167,31 @@ async function handleMessage(data: any): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyPermissionDecision(chatId: string, decision: PermissionDecision): boolean {
|
||||||
|
const { requestId, allowed, rule } = decision
|
||||||
|
const pending = pendingPermissions.get(chatId)
|
||||||
|
if (!pending?.has(requestId)) {
|
||||||
|
void sendText(chatId, `未找到待确认的权限请求:${requestId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const sent = bridge.sendPermissionResponse(chatId, requestId, allowed, rule)
|
||||||
|
if (!sent) {
|
||||||
|
void sendText(chatId, '权限响应发送失败,请检查会话状态。')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.delete(requestId)
|
||||||
|
const runtime = getRuntimeState(chatId)
|
||||||
|
runtime.pendingPermissionCount = Math.max(0, runtime.pendingPermissionCount - 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePermissionDecision(chatId: string, decision: PermissionDecision): Promise<void> {
|
||||||
|
const sent = applyPermissionDecision(chatId, decision)
|
||||||
|
if (sent) await sendText(chatId, `${formatPermissionDecisionStatus(decision)}。`)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCardAction(data: any): Promise<any> {
|
async function handleCardAction(data: any): Promise<any> {
|
||||||
const event = data as {
|
const event = data as {
|
||||||
operator?: { open_id?: string }
|
operator?: { open_id?: string }
|
||||||
@ -1168,9 +1218,12 @@ async function handleCardAction(data: any): Promise<any> {
|
|||||||
const rule = event.action?.value?.rule
|
const rule = event.action?.value?.rule
|
||||||
if (!requestId) return
|
if (!requestId) return
|
||||||
|
|
||||||
bridge.sendPermissionResponse(chatId, requestId, allowed, rule)
|
const sent = applyPermissionDecision(chatId, {
|
||||||
const runtime = getRuntimeState(chatId)
|
requestId,
|
||||||
runtime.pendingPermissionCount = Math.max(0, runtime.pendingPermissionCount - 1)
|
allowed,
|
||||||
|
rule: rule === 'always' ? 'always' : undefined,
|
||||||
|
})
|
||||||
|
if (!sent) return { toast: { type: 'warning', content: '权限响应发送失败' } }
|
||||||
|
|
||||||
const statusText = allowed
|
const statusText = allowed
|
||||||
? rule === 'always'
|
? rule === 'always'
|
||||||
|
|||||||
@ -20,7 +20,10 @@ import {
|
|||||||
} from '../common/format.js'
|
} from '../common/format.js'
|
||||||
import {
|
import {
|
||||||
formatPermissionDecisionStatus,
|
formatPermissionDecisionStatus,
|
||||||
|
formatPermissionInstructions,
|
||||||
|
parsePermissionCommand,
|
||||||
parsePermitCallbackData,
|
parsePermitCallbackData,
|
||||||
|
type PermissionDecision,
|
||||||
} from '../common/permission.js'
|
} from '../common/permission.js'
|
||||||
import { SessionStore } from '../common/session-store.js'
|
import { SessionStore } from '../common/session-store.js'
|
||||||
import { AdapterHttpClient } from '../common/http-client.js'
|
import { AdapterHttpClient } from '../common/http-client.js'
|
||||||
@ -64,6 +67,7 @@ const buffers = new Map<string, MessageBuffer>()
|
|||||||
// Track chats waiting for project selection
|
// Track chats waiting for project selection
|
||||||
const pendingProjectSelection = new Map<string, boolean>()
|
const pendingProjectSelection = new Map<string, boolean>()
|
||||||
const runtimeStates = new Map<string, ChatRuntimeState>()
|
const runtimeStates = new Map<string, ChatRuntimeState>()
|
||||||
|
const pendingPermissions = new Map<string, Set<string>>()
|
||||||
/** Per-chat outbound image watcher for Agent-produced markdown images. */
|
/** Per-chat outbound image watcher for Agent-produced markdown images. */
|
||||||
const tgImageWatchers = new Map<string, ImageBlockWatcher>()
|
const tgImageWatchers = new Map<string, ImageBlockWatcher>()
|
||||||
|
|
||||||
@ -113,9 +117,29 @@ function clearTransientChatState(chatId: string): void {
|
|||||||
runtime.state = 'idle'
|
runtime.state = 'idle'
|
||||||
runtime.verb = undefined
|
runtime.verb = undefined
|
||||||
runtime.pendingPermissionCount = 0
|
runtime.pendingPermissionCount = 0
|
||||||
|
pendingPermissions.delete(chatId)
|
||||||
tgImageWatchers.delete(chatId)
|
tgImageWatchers.delete(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handlePermissionDecision(chatId: string, decision: PermissionDecision): Promise<void> {
|
||||||
|
const pending = pendingPermissions.get(chatId)
|
||||||
|
if (!pending?.has(decision.requestId)) {
|
||||||
|
await bot.api.sendMessage(Number(chatId), `未找到待确认的权限请求:${decision.requestId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sent = bridge.sendPermissionResponse(chatId, decision.requestId, decision.allowed, decision.rule)
|
||||||
|
if (sent) {
|
||||||
|
pending.delete(decision.requestId)
|
||||||
|
const runtime = getRuntimeState(chatId)
|
||||||
|
runtime.pendingPermissionCount = Math.max(0, runtime.pendingPermissionCount - 1)
|
||||||
|
}
|
||||||
|
await bot.api.sendMessage(
|
||||||
|
Number(chatId),
|
||||||
|
sent ? `${formatPermissionDecisionStatus(decision)}。` : '权限响应发送失败,请检查会话状态。',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureExistingSession(chatId: string): Promise<{ sessionId: string; workDir: string } | null> {
|
async function ensureExistingSession(chatId: string): Promise<{ sessionId: string; workDir: string } | null> {
|
||||||
const stored = sessionStore.get(chatId)
|
const stored = sessionStore.get(chatId)
|
||||||
if (!stored) return null
|
if (!stored) return null
|
||||||
@ -412,7 +436,10 @@ async function handleServerMessage(chatId: string, msg: ServerMessage): Promise<
|
|||||||
case 'permission_request': {
|
case 'permission_request': {
|
||||||
runtime.pendingPermissionCount += 1
|
runtime.pendingPermissionCount += 1
|
||||||
runtime.state = 'permission_pending'
|
runtime.state = 'permission_pending'
|
||||||
const text = formatPermissionRequest(msg.toolName, msg.input, msg.requestId)
|
const pending = pendingPermissions.get(chatId) ?? new Set<string>()
|
||||||
|
pending.add(msg.requestId)
|
||||||
|
pendingPermissions.set(chatId, pending)
|
||||||
|
const text = `${formatPermissionRequest(msg.toolName, msg.input, msg.requestId)}\n\n${formatPermissionInstructions(msg.requestId)}`
|
||||||
const keyboard = new InlineKeyboard()
|
const keyboard = new InlineKeyboard()
|
||||||
.text('✅ 允许', `permit:${msg.requestId}:yes`)
|
.text('✅ 允许', `permit:${msg.requestId}:yes`)
|
||||||
.text('♾️ 永久允许', `permit:${msg.requestId}:always`)
|
.text('♾️ 永久允许', `permit:${msg.requestId}:always`)
|
||||||
@ -504,6 +531,7 @@ async function startNewSession(chatId: string, query?: string): Promise<void> {
|
|||||||
buffers.get(chatId)?.reset()
|
buffers.get(chatId)?.reset()
|
||||||
buffers.delete(chatId)
|
buffers.delete(chatId)
|
||||||
pendingProjectSelection.delete(chatId)
|
pendingProjectSelection.delete(chatId)
|
||||||
|
pendingPermissions.delete(chatId)
|
||||||
runtimeStates.delete(chatId)
|
runtimeStates.delete(chatId)
|
||||||
tgImageWatchers.delete(chatId)
|
tgImageWatchers.delete(chatId)
|
||||||
|
|
||||||
@ -587,6 +615,12 @@ bot.command('clear', (ctx) => {
|
|||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const command of ['allow', 'always', 'allow-always', 'deny'] as const) {
|
||||||
|
bot.command(command, async (ctx) => {
|
||||||
|
await routeUserMessage(ctx, `/${command}${ctx.match ? ` ${ctx.match}` : ''}`, [])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** Shared per-user-message pipeline: dedup, pairing check, project-pick
|
/** Shared per-user-message pipeline: dedup, pairing check, project-pick
|
||||||
* routing, enqueue, ensureSession, sendUserMessage with attachments.
|
* routing, enqueue, ensureSession, sendUserMessage with attachments.
|
||||||
* Caller has already extracted text and attachments from the context. */
|
* Caller has already extracted text and attachments from the context. */
|
||||||
@ -613,6 +647,14 @@ async function routeUserMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enqueue(chatId, async () => {
|
enqueue(chatId, async () => {
|
||||||
|
const permissionDecision = attachments.length === 0
|
||||||
|
? parsePermissionCommand(text, pendingPermissions.get(chatId))
|
||||||
|
: null
|
||||||
|
if (permissionDecision) {
|
||||||
|
await handlePermissionDecision(chatId, permissionDecision)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingProjectSelection.has(chatId)) {
|
if (pendingProjectSelection.has(chatId)) {
|
||||||
if (text.trim()) await startNewSession(chatId, text.trim())
|
if (text.trim()) await startNewSession(chatId, text.trim())
|
||||||
return
|
return
|
||||||
@ -727,6 +769,7 @@ bot.on('callback_query:data', async (ctx) => {
|
|||||||
bridge.sendPermissionResponse(chatId, decision.requestId, decision.allowed, decision.rule)
|
bridge.sendPermissionResponse(chatId, decision.requestId, decision.allowed, decision.rule)
|
||||||
const runtime = getRuntimeState(chatId)
|
const runtime = getRuntimeState(chatId)
|
||||||
runtime.pendingPermissionCount = Math.max(0, runtime.pendingPermissionCount - 1)
|
runtime.pendingPermissionCount = Math.max(0, runtime.pendingPermissionCount - 1)
|
||||||
|
pendingPermissions.get(chatId)?.delete(decision.requestId)
|
||||||
|
|
||||||
const statusText = formatPermissionDecisionStatus(decision)
|
const statusText = formatPermissionDecisionStatus(decision)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -502,7 +502,7 @@ async function routeUserMessage(message: WechatMessage): Promise<void> {
|
|||||||
await sendText(chatId, sent ? '已清空当前会话上下文。' : '无法发送 /clear,请先发送 /new 重新连接会话。')
|
await sendText(chatId, sent ? '已清空当前会话上下文。' : '无法发送 /clear,请先发送 /new 重新连接会话。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const permissionDecision = !hasAttachments ? parsePermissionCommand(text) : null
|
const permissionDecision = !hasAttachments ? parsePermissionCommand(text, pendingPermissions.get(chatId)) : null
|
||||||
if (permissionDecision) {
|
if (permissionDecision) {
|
||||||
const { requestId, allowed, rule } = permissionDecision
|
const { requestId, allowed, rule } = permissionDecision
|
||||||
const pending = pendingPermissions.get(chatId)
|
const pending = pendingPermissions.get(chatId)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user