chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { ChatInteraction, InteractionResponse, PermissionOption, QuestionItem } from '@shared/conversation'
|
||||
import type { ChatEvent } from '@shared/types'
|
||||
|
||||
interface PendingInteraction { request: ChatInteraction; resolve(value: InteractionResponse): void; reject(error: Error): void; detach(): void }
|
||||
const pending = new Map<string, PendingInteraction>()
|
||||
|
||||
function abortError(): Error { return Object.assign(new Error('交互已取消'), { name: 'AbortError' }) }
|
||||
|
||||
export function createInteractionBroker(runId: string, signal: AbortSignal, emit: (event: ChatEvent) => void) {
|
||||
async function request(input: ChatInteraction): Promise<InteractionResponse> {
|
||||
if (signal.aborted) throw abortError()
|
||||
return new Promise((resolve, reject) => {
|
||||
const onAbort = (): void => {
|
||||
const entry = pending.get(input.id)
|
||||
if (!entry) return
|
||||
pending.delete(input.id)
|
||||
entry.detach()
|
||||
emit({ runId, type: 'interaction', interaction: { ...input, status: 'cancelled' } })
|
||||
reject(abortError())
|
||||
}
|
||||
pending.set(input.id, { request: input, resolve, reject, detach: () => signal.removeEventListener('abort', onAbort) })
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
emit({ runId, type: 'interaction', interaction: input })
|
||||
})
|
||||
}
|
||||
return {
|
||||
async question(toolCallId: string, title: string, questions: QuestionItem[]) {
|
||||
const response = await request({ id: randomUUID(), runId, toolCallId, kind: 'question', title, questions, status: 'pending' })
|
||||
if (response.kind !== 'question') throw new Error('问答响应类型不匹配')
|
||||
return response
|
||||
},
|
||||
async approval(toolCallId: string, title: string, options: PermissionOption[], scope?: string) {
|
||||
const id = randomUUID()
|
||||
const response = await request({ id, requestId: id, runId, toolCallId, kind: 'approval', title, options, scope, status: 'pending' })
|
||||
if (response.kind !== 'approval') throw new Error('审批响应类型不匹配')
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
export type InteractionBroker = ReturnType<typeof createInteractionBroker>
|
||||
|
||||
/** Exact run/id binding makes stale or duplicated button submissions harmless. */
|
||||
export function respondToInteraction(runId: string, id: string, response: InteractionResponse, emit: (event: ChatEvent) => void): boolean {
|
||||
const entry = pending.get(id)
|
||||
if (!entry || entry.request.runId !== runId) return false
|
||||
const request = entry.request
|
||||
if (request.kind !== response.kind) throw new Error('交互响应类型不匹配')
|
||||
let resolved: ChatInteraction
|
||||
let finalResponse = response
|
||||
if (request.kind === 'approval' && response.kind === 'approval') {
|
||||
const option = request.options.find((item) => item.optionId === response.optionId)
|
||||
if (!option) throw new Error('审批选项无效')
|
||||
resolved = { ...request, selectedOptionId: option.optionId, status: option.kind.startsWith('reject') ? 'rejected' : 'answered' }
|
||||
} else if (request.kind === 'question' && response.kind === 'question') {
|
||||
if (!response.answer || !Array.isArray(response.answer.questions)) throw new Error('问答内容无效')
|
||||
const ids = response.answer.questions.map((item) => item.id)
|
||||
if (new Set(ids).size !== ids.length || ids.some((id) => !request.questions.some((question) => question.id === id))) throw new Error('问题标识无效')
|
||||
if (!response.skipped) {
|
||||
for (const question of request.questions) {
|
||||
const answer = response.answer.questions.find((item) => item.id === question.id)
|
||||
if (!answer || !Array.isArray(answer.answers) || answer.answers.some((value) => typeof value !== 'string' || !value.trim())) throw new Error('回答内容无效')
|
||||
if (!question.multiSelect && answer.answers.length > 1) throw new Error('该问题仅支持单选')
|
||||
}
|
||||
if (!response.answer.questions.some((item) => item.answers.length)) throw new Error('请至少回答一个问题,或跳过全部')
|
||||
}
|
||||
const answer = { title: request.title, questions: request.questions.map((question) => ({ id: question.id, question: question.question,
|
||||
answers: response.skipped ? [] : response.answer.questions.find((item) => item.id === question.id)?.answers ?? [] })) }
|
||||
finalResponse = { ...response, answer }
|
||||
resolved = { ...request, answer, skipped: response.skipped, status: 'answered' }
|
||||
} else throw new Error('交互响应类型不匹配')
|
||||
pending.delete(id)
|
||||
entry.detach()
|
||||
emit({ runId, type: 'interaction', interaction: resolved })
|
||||
entry.resolve(finalResponse)
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user