fix: stabilize /model reasoning-effort selection (v0.3.5)
Keep pending model outside the picker map, omit undefined reasoningEffort in selectModel payload, auto-apply single-effort models, and surface RPC errors. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+31
-9
@@ -64,7 +64,8 @@ const WS_CB = 'ws:'
|
||||
const SID_CB = 'sid:'
|
||||
const BACK_WS_CB = 'wb'
|
||||
const MODEL_CB = 'mdl:'
|
||||
const EFFORT_CB = 'me:'
|
||||
/** Use eff: (not me:) — short prefix, no collision with other callbacks. */
|
||||
const EFFORT_CB = 'eff:'
|
||||
const BACK_MODEL_CB = 'mb'
|
||||
const MAX_BUTTONS = 40
|
||||
|
||||
@@ -94,6 +95,8 @@ export class TelegramBridge {
|
||||
|
||||
private readonly bindings = new Map<string, Binding>()
|
||||
private readonly pickers = new Map<string, PickerState>()
|
||||
/** chatId → model awaiting reasoning-effort pick (kept outside picker so list refreshes won't drop it). */
|
||||
private readonly pendingModels = new Map<string, ModelOption>()
|
||||
private polling = false
|
||||
private offset: number | undefined
|
||||
private pollPromise: Promise<void> | undefined
|
||||
@@ -149,6 +152,7 @@ export class TelegramBridge {
|
||||
// Never dispose host agents — only clear remote bindings.
|
||||
this.bindings.clear()
|
||||
this.pickers.clear()
|
||||
this.pendingModels.clear()
|
||||
if (this.pollPromise) {
|
||||
await this.pollPromise.catch(() => {})
|
||||
this.pollPromise = undefined
|
||||
@@ -261,20 +265,32 @@ export class TelegramBridge {
|
||||
await this.client.sendMessage(chatId, MSG.PICKER_STALE)
|
||||
return
|
||||
}
|
||||
if (option.efforts && option.efforts.length > 0) {
|
||||
if (picker) picker.pendingModel = option
|
||||
const efforts = (option.efforts ?? []).filter((e) => e.id)
|
||||
if (efforts.length === 1) {
|
||||
// Single effort (often only "off") — apply immediately, no second tap.
|
||||
await this.applyModel(chatId, cq.id, { ...option, efforts }, efforts[0]!.id)
|
||||
return
|
||||
}
|
||||
if (efforts.length > 1) {
|
||||
const pending = { ...option, efforts }
|
||||
this.pendingModels.set(String(chatId), pending)
|
||||
if (picker) picker.pendingModel = pending
|
||||
await this.client.answerCallbackQuery(cq.id)
|
||||
await this.sendEffortPicker(chatId, option)
|
||||
await this.sendEffortPicker(chatId, pending)
|
||||
return
|
||||
}
|
||||
await this.applyModel(chatId, cq.id, option)
|
||||
return
|
||||
}
|
||||
if (data.startsWith(EFFORT_CB)) {
|
||||
const index = Number(data.slice(EFFORT_CB.length))
|
||||
const pending = picker?.pendingModel
|
||||
if (data.startsWith(EFFORT_CB) || data.startsWith('me:')) {
|
||||
// Accept legacy "me:" callbacks from older bot messages.
|
||||
const rawIndex = data.startsWith(EFFORT_CB)
|
||||
? data.slice(EFFORT_CB.length)
|
||||
: data.slice('me:'.length)
|
||||
const index = Number(rawIndex)
|
||||
const pending = this.pendingModels.get(String(chatId)) ?? picker?.pendingModel
|
||||
const effort = pending?.efforts?.[index]
|
||||
if (!pending || !effort) {
|
||||
if (!pending || !effort?.id) {
|
||||
await this.client.answerCallbackQuery(cq.id, '列表已过期')
|
||||
await this.client.sendMessage(chatId, MSG.PICKER_STALE)
|
||||
return
|
||||
@@ -503,18 +519,24 @@ export class TelegramBridge {
|
||||
await this.client.sendMessage(chatId, MSG.NEED_BIND)
|
||||
return
|
||||
}
|
||||
// Ensure session is live before selectModel (same as followup path).
|
||||
await this.ensureLiveAgent(binding.sessionId)
|
||||
try {
|
||||
const selected = await selectSessionModel(this.ctx, binding.sessionId, {
|
||||
provider: option.provider,
|
||||
model: option.model,
|
||||
reasoningEffort,
|
||||
})
|
||||
this.pendingModels.delete(String(chatId))
|
||||
const picker = this.pickers.get(String(chatId))
|
||||
if (picker) picker.pendingModel = undefined
|
||||
await this.client.answerCallbackQuery(callbackId, '已切换')
|
||||
await this.client.sendMessage(chatId, MSG.MODEL_SET(formatModel(selected)))
|
||||
} catch (err) {
|
||||
this.ctx.logger.warn(`dsh-telegram-channel: selectModel failed: ${this.redact(err)}`)
|
||||
const detail = err instanceof Error ? err.message : String(err)
|
||||
await this.client.answerCallbackQuery(callbackId, '切换失败')
|
||||
await this.client.sendMessage(chatId, MSG.MODEL_FAILED)
|
||||
await this.client.sendMessage(chatId, MSG.MODEL_FAILED(detail))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -52,7 +52,11 @@ export const MSG = {
|
||||
MODEL_SET(selected: string): string {
|
||||
return `已切换模型:${selected}\n下一回合生效。`
|
||||
},
|
||||
MODEL_FAILED: '切换模型失败。请稍后重试或在 Web 中切换。',
|
||||
MODEL_FAILED(detail?: string): string {
|
||||
const tip = '切换模型失败。请稍后重试或在 Web 中切换。'
|
||||
if (!detail) return tip
|
||||
return `${tip}\n详情:${detail}`
|
||||
},
|
||||
unknown(command: string): string {
|
||||
return `未知命令 ${command}。发送 /help 查看可用命令。`
|
||||
},
|
||||
|
||||
+12
-3
@@ -89,12 +89,21 @@ export async function selectSessionModel(
|
||||
if (!api?.sessions?.selectModel) {
|
||||
throw new Error('apiProxy unavailable (use ctx.get / inject apiProxy)')
|
||||
}
|
||||
const raw = await call(api.sessions.selectModel, {
|
||||
// Omit undefined reasoningEffort — some validators treat explicit undefined as invalid.
|
||||
const payload: {
|
||||
sessionId: string
|
||||
provider: string
|
||||
model: string
|
||||
reasoningEffort?: string
|
||||
} = {
|
||||
sessionId,
|
||||
provider: selection.provider,
|
||||
model: selection.model,
|
||||
reasoningEffort: selection.reasoningEffort,
|
||||
})
|
||||
}
|
||||
if (selection.reasoningEffort !== undefined && selection.reasoningEffort !== '') {
|
||||
payload.reasoningEffort = selection.reasoningEffort
|
||||
}
|
||||
const raw = await call(api.sessions.selectModel, payload)
|
||||
const value = unwrap<{ selected: { provider: string; model: string; reasoningEffort?: string } }>(raw)
|
||||
if (!value?.selected) {
|
||||
throw new Error(unwrapError(raw) ?? 'failed to select model')
|
||||
|
||||
Reference in New Issue
Block a user