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:
owo
2026-08-14 22:41:43 +08:00
co-authored by Cursor
parent fb5a334edb
commit 819010de1c
10 changed files with 190 additions and 29 deletions
+2
View File
@@ -19,6 +19,8 @@ export declare class TelegramBridge {
private readonly maxMessageLength;
private readonly bindings;
private readonly pickers;
/** chatId → model awaiting reasoning-effort pick (kept outside picker so list refreshes won't drop it). */
private readonly pendingModels;
private polling;
private offset;
private pollPromise;
+32 -9
View File
@@ -12,7 +12,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;
function lastContextKeyboard() {
@@ -37,6 +38,8 @@ export class TelegramBridge {
maxMessageLength;
bindings = new Map();
pickers = new Map();
/** chatId → model awaiting reasoning-effort pick (kept outside picker so list refreshes won't drop it). */
pendingModels = new Map();
polling = false;
offset;
pollPromise;
@@ -89,6 +92,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;
@@ -196,21 +200,33 @@ export class TelegramBridge {
await this.client.sendMessage(chatId, MSG.PICKER_STALE);
return;
}
if (option.efforts && option.efforts.length > 0) {
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 = option;
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;
@@ -421,19 +437,26 @@ 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));
}
}
liveAgents() {
+1 -1
View File
@@ -20,7 +20,7 @@ export declare const MSG: {
readonly MODEL_UNROUTABLE: (current: string) => string;
readonly MODEL_EMPTY: (current: string) => string;
readonly MODEL_SET: (selected: string) => string;
readonly MODEL_FAILED: "切换模型失败。请稍后重试或在 Web 中切换。";
readonly MODEL_FAILED: (detail?: string) => string;
readonly unknown: (command: string) => string;
};
export type ParsedCommand = {
+6 -1
View File
@@ -53,7 +53,12 @@ export const MSG = {
MODEL_SET(selected) {
return `已切换模型:${selected}\n下一回合生效。`;
},
MODEL_FAILED: '切换模型失败。请稍后重试或在 Web 中切换。',
MODEL_FAILED(detail) {
const tip = '切换模型失败。请稍后重试或在 Web 中切换。';
if (!detail)
return tip;
return `${tip}\n详情:${detail}`;
},
unknown(command) {
return `未知命令 ${command}。发送 /help 查看可用命令。`;
},
+7 -3
View File
@@ -53,12 +53,16 @@ export async function selectSessionModel(ctx, sessionId, selection) {
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,
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(raw);
if (!value?.selected) {
throw new Error(unwrapError(raw) ?? 'failed to select model');