mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix: preserve DingTalk permission reply order
DingTalk keeps AI card placement tied to the original card delivery time, so a card created before a permission request can later be updated with the final answer above the permission card. Finish and drop the active stream when permission interrupts a turn so authorized output creates a new card below the prompt. Constraint: DingTalk does not reorder an existing delivered AI card when its streaming content is updated Rejected: Reuse the pre-permission AI card after approval | final answers can remain visually above the permission request Confidence: high Scope-risk: narrow Directive: Keep permission_request as a lifecycle boundary for DingTalk AI card streams Tested: cd adapters && bun test dingtalk/__tests__/stream-state.test.ts dingtalk/__tests__/ai-card.test.ts Tested: cd adapters && bunx tsc --noEmit Tested: bun run check:adapters Tested: git diff --check Not-tested: Live DingTalk account end-to-end permission approval
This commit is contained in:
parent
ca62d1b24f
commit
54c975bc09
53
adapters/dingtalk/__tests__/stream-state.test.ts
Normal file
53
adapters/dingtalk/__tests__/stream-state.test.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { MessageBuffer } from '../../common/message-buffer.js'
|
||||
import { finishAndResetDingTalkStreamingState, resetDingTalkStreamingState } from '../stream-state.js'
|
||||
|
||||
describe('DingTalk streaming state', () => {
|
||||
it('drops the active AI card stream when permission interrupts output ordering', async () => {
|
||||
let resetCalled = false
|
||||
const buffer = new MessageBuffer(async () => {}, 100, 1000)
|
||||
const originalReset = buffer.reset.bind(buffer)
|
||||
buffer.reset = () => {
|
||||
resetCalled = true
|
||||
originalReset()
|
||||
}
|
||||
|
||||
const state = {
|
||||
aiCardBuffers: new Map([['chat-1', buffer]]),
|
||||
streamingCards: new Map([['chat-1', Promise.resolve(null)]]),
|
||||
streamingCardText: new Map([['chat-1', 'pre-permission text']]),
|
||||
}
|
||||
|
||||
resetDingTalkStreamingState(state, 'chat-1')
|
||||
|
||||
expect(resetCalled).toBe(true)
|
||||
expect(state.aiCardBuffers.has('chat-1')).toBe(false)
|
||||
expect(state.streamingCards.has('chat-1')).toBe(false)
|
||||
expect(state.streamingCardText.has('chat-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('completes the existing stream before dropping it for a permission request', async () => {
|
||||
const flushed: Array<{ text: string; complete: boolean }> = []
|
||||
const buffer = new MessageBuffer(
|
||||
async (text, complete) => {
|
||||
flushed.push({ text, complete })
|
||||
},
|
||||
100,
|
||||
1000,
|
||||
)
|
||||
buffer.append('pre-permission text')
|
||||
|
||||
const state = {
|
||||
aiCardBuffers: new Map([['chat-1', buffer]]),
|
||||
streamingCards: new Map([['chat-1', Promise.resolve(null)]]),
|
||||
streamingCardText: new Map([['chat-1', 'already streamed']]),
|
||||
}
|
||||
|
||||
await finishAndResetDingTalkStreamingState(state, 'chat-1')
|
||||
|
||||
expect(flushed).toEqual([{ text: 'pre-permission text', complete: true }])
|
||||
expect(state.aiCardBuffers.has('chat-1')).toBe(false)
|
||||
expect(state.streamingCards.has('chat-1')).toBe(false)
|
||||
expect(state.streamingCardText.has('chat-1')).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -44,6 +44,7 @@ import {
|
||||
DINGTALK_PERMISSION_CARD_CALLBACK_ROUTE,
|
||||
parseDingTalkPermissionCardAction,
|
||||
} from './permission-card.js'
|
||||
import { finishAndResetDingTalkStreamingState, resetDingTalkStreamingState } from './stream-state.js'
|
||||
|
||||
const DINGTALK_API = 'https://api.dingtalk.com'
|
||||
|
||||
@ -204,10 +205,7 @@ async function flushToAiCard(chatId: string, newText: string, isComplete: boolea
|
||||
}
|
||||
|
||||
function clearTransientChatState(chatId: string): void {
|
||||
aiCardBuffers.get(chatId)?.reset()
|
||||
aiCardBuffers.delete(chatId)
|
||||
streamingCards.delete(chatId)
|
||||
streamingCardText.delete(chatId)
|
||||
resetDingTalkStreamingState({ aiCardBuffers, streamingCards, streamingCardText }, chatId)
|
||||
clearPendingPermissions(chatId)
|
||||
const runtime = getRuntimeState(chatId)
|
||||
runtime.state = 'idle'
|
||||
@ -388,7 +386,6 @@ async function handleServerMessage(chatId: string, msg: ServerMessage): Promise<
|
||||
case 'content_start':
|
||||
if (msg.blockType === 'text') {
|
||||
runtime.state = 'streaming'
|
||||
void getOrCreateAiCard(chatId)
|
||||
}
|
||||
if (msg.blockType === 'tool_use') runtime.state = 'tool_executing'
|
||||
break
|
||||
@ -428,6 +425,7 @@ async function sendPermissionRequest(chatId: string, msg: ServerMessage): Promis
|
||||
const runtime = getRuntimeState(chatId)
|
||||
runtime.pendingPermissionCount += 1
|
||||
runtime.state = 'permission_pending'
|
||||
await finishAndResetDingTalkStreamingState({ aiCardBuffers, streamingCards, streamingCardText }, chatId)
|
||||
|
||||
const set = pendingPermissions.get(chatId) ?? new Set<string>()
|
||||
set.add(msg.requestId)
|
||||
|
||||
26
adapters/dingtalk/stream-state.ts
Normal file
26
adapters/dingtalk/stream-state.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { MessageBuffer } from '../common/message-buffer.js'
|
||||
import type { DingTalkAiCardInstance } from './ai-card.js'
|
||||
|
||||
export type DingTalkStreamingState = {
|
||||
aiCardBuffers: Map<string, MessageBuffer>
|
||||
streamingCards: Map<string, Promise<DingTalkAiCardInstance | null>>
|
||||
streamingCardText: Map<string, string>
|
||||
}
|
||||
|
||||
export function resetDingTalkStreamingState(
|
||||
state: DingTalkStreamingState,
|
||||
chatId: string,
|
||||
): void {
|
||||
state.aiCardBuffers.get(chatId)?.reset()
|
||||
state.aiCardBuffers.delete(chatId)
|
||||
state.streamingCards.delete(chatId)
|
||||
state.streamingCardText.delete(chatId)
|
||||
}
|
||||
|
||||
export async function finishAndResetDingTalkStreamingState(
|
||||
state: DingTalkStreamingState,
|
||||
chatId: string,
|
||||
): Promise<void> {
|
||||
await state.aiCardBuffers.get(chatId)?.complete()
|
||||
resetDingTalkStreamingState(state, chatId)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user