feat: add reasoning effort commands

This commit is contained in:
xmanrui 2026-08-25 00:41:44 +08:00
parent 36cbbe48e7
commit 9e39333ff6
24 changed files with 1346 additions and 240 deletions

View file

@ -57,8 +57,10 @@ const HELP_TEXT_LINES = [
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
'/models 按序号列出所有可用模型',
'/model [序号或完整模型ID] 查看或切换当前会话模型',
'示例:先发 /models,再发 /model 2',
'/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
'/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型',
'示例:先发 /models,再发 /model 2 [推理等级ID]',
'/presetlist 按序号列出可用 Agent Preset',
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
'纯数字 ID:/preset id:<ID>',

View file

@ -509,7 +509,9 @@ export function menuHelpText() {
'纯数字 ID:/preset id:<ID>',
'/preset --default 跟随 Host 默认',
'/models 列出模型',
'/model 2 按序号切换模型',
'/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
'/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型',
'',
'🎮 任务控制',
'/stop 停止当前任务',
@ -559,7 +561,9 @@ const HELP_TEXT_COMMANDS = [
'`/preset [序号/ID]` — 切换预设',
'`/preset --default` — 跟随默认',
'`/models` — 列出模型',
'`/model 2` — 切换模型',
'`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级',
'`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级',
'`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型',
'`/repair` — 修复卡片按钮',
].join('\n');

View file

@ -70,8 +70,10 @@ function helpText() {
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),

View file

@ -104,7 +104,26 @@ function validModelSelection(value) {
&& Boolean(value.provider)
&& typeof value.model === 'string'
&& Boolean(value.model)
&& (value.reasoningEffort === undefined || typeof value.reasoningEffort === 'string');
&& (value.reasoningEffort === undefined
|| (typeof value.reasoningEffort === 'string' && Boolean(value.reasoningEffort)));
}
function validModelReasoning(value) {
return value !== null
&& typeof value === 'object'
&& Array.isArray(value.efforts)
&& value.efforts.length > 0
&& value.efforts.every((effort) => (
effort !== null
&& typeof effort === 'object'
&& typeof effort.id === 'string'
&& Boolean(effort.id)
&& typeof effort.name === 'string'
&& Boolean(effort.name)
&& (effort.description === undefined || typeof effort.description === 'string')
))
&& (value.defaultEffort === undefined
|| (typeof value.defaultEffort === 'string' && Boolean(value.defaultEffort)));
}
function validateModelCatalog(value, method, { session = false } = {}) {
@ -123,7 +142,9 @@ function validateModelCatalog(value, method, { session = false } = {}) {
for (const model of group.models) {
if (!model || typeof model !== 'object'
|| typeof model.id !== 'string' || !model.id
|| typeof model.name !== 'string' || !model.name) {
|| typeof model.name !== 'string' || !model.name
|| (model.description !== undefined && typeof model.description !== 'string')
|| (model.reasoning !== undefined && !validModelReasoning(model.reasoning))) {
throw new Error(`Harness returned an invalid response for ${method}`);
}
}
@ -728,6 +749,9 @@ export class HarnessClient {
sessionId,
provider: selection.provider,
model: selection.model,
...(selection.reasoningEffort === undefined
? {}
: { reasoningEffort: selection.reasoningEffort }),
}, 30_000, signal ? { ...options, signal } : options);
};
const value = this.#sessionMaintenanceExecutor

View file

@ -236,13 +236,12 @@ export default {
'/unwatch ID 取消关注': '/unwatch ID Stop watching a session',
'🤖 预设 / 模型': '🤖 Presets / models',
'/models 列出模型': '/models List models',
'/model 2 按序号切换模型': '/model 2 Switch model by index',
'🎮 任务控制': '🎮 Task controls',
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/model 2` — 切换模型\n`/repair` — 修复卡片按钮':
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/model 2` — switch model\n`/repair` — repair card buttons',
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/repair` — 修复卡片按钮':
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/repair` — repair card buttons',
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**修复 · **6**帮助':
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Repair · **6** Help',
'从下方下拉选择补充指令;最后一项可自定义输入。':

View file

@ -50,9 +50,17 @@ export default {
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
'/session <Session ID or workspace index> Bind this chat to the specified session',
'/models 按序号列出所有可用模型': '/models List all available models by index',
'/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级':
'/reasoninglist or /reasonings List reasoning efforts for the current model by index',
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级':
'/reasoning [index, effort ID, or --default] Show or switch the current reasoning effort',
'/model [序号或完整模型ID] 查看或切换当前会话模型':
'/model [index or full model ID] Show or switch the model of the current session',
'/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型':
'/model [index or full model ID] [reasoning effort ID] Show or switch the current Session model',
'示例:先发 /models,再发 /model 2': 'Example: send /models first, then /model 2',
'示例:先发 /models,再发 /model 2 [推理等级ID]':
'Example: send /models first, then /model 2 [reasoning effort ID]',
'/presetlist 按序号列出可用 Agent Preset': '/presetlist List available Agent Presets by index',
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset':
'/preset [index or full ID] Show or set the Agent Preset of this bot',

View file

@ -134,17 +134,39 @@ export default {
// model-command.mjs
'用法:/model <序号> 或 /model <provider>/<model>': 'Usage: /model <index> or /model <provider>/<model>',
'用法:/model <序号或 provider/model> [推理等级ID]':
'Usage: /model <index or provider/model> [reasoning effort ID]',
'用法:/models(不带参数)': 'Usage: /models (no arguments)',
'用法:/reasoning [序号、等级ID或 --default]':
'Usage: /reasoning [index, effort ID, or --default]',
'用法:/reasoninglist 或 /reasonings(不带参数)':
'Usage: /reasoninglist or /reasonings (no arguments)',
'模型序号无效:{input}': 'Invalid model index: {input}',
'请发送 /models 查看并输入有效的正整数序号。':
'Please send /models to view the list and enter a valid positive integer index.',
'推理等级序号无效:{input}': 'Invalid reasoning effort index: {input}',
'请发送 /reasoninglist 查看并输入有效的正整数序号。':
'Please send /reasoninglist and enter a valid positive integer index.',
'可用模型:': 'Available models:',
'可用推理等级:': 'Available reasoning efforts:',
'可用推理等级:{efforts}': 'Available reasoning efforts: {efforts}',
'当前没有可用模型。': 'No models are currently available.',
'以下模型提供方暂时不可用:': 'The following model providers are temporarily unavailable:',
'切换模型:/model <序号>': 'To switch models: /model <index>',
'切换模型:/model <序号> [推理等级ID]':
'Switch model: /model <index> [reasoning effort ID]',
'当前模型:': 'Current model:',
'当前推理等级:{effort}': 'Current reasoning effort: {effort}',
'Default(由模型或 Provider 决定)': 'Default (determined by the model or Provider)',
'(当前、默认)': ' (current, default)',
'(默认)': ' (default)',
'查看全部模型:/models': 'See all models: /models',
'查看可用推理等级:/reasoninglist': 'See reasoning efforts: /reasoninglist',
'切换推理等级:/reasoning <序号或等级ID>':
'Switch reasoning effort: /reasoning <index or effort ID>',
'恢复默认等级:/reasoning --default': 'Restore the default effort: /reasoning --default',
'当前聊天还没有会话。': 'This chat has no Session yet.',
'请先发送一条普通消息创建会话。': 'Send a regular message first to create a Session.',
'查看模型:/models': 'View models: /models',
'选择模型:/model <序号>': 'Select a model: /model <index>',
'当前任务正在运行,请等待完成或先发送 /stop。':
@ -153,22 +175,39 @@ export default {
'The Session bound to this chat no longer exists; please try again.',
'无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。':
'Cannot switch to that model. It is currently unavailable, or does not support the images in this Session.',
'无法切换推理等级。当前模型或推理等级不可用。':
'Cannot switch the reasoning effort. The current model or effort is unavailable.',
'当前聊天绑定的会话已发生变化,请重试。':
'The Session bound to this chat has changed; please try again.',
'获取模型列表已取消。': 'Fetching the model list was cancelled.',
'获取推理等级列表已取消。': 'Fetching the reasoning effort list was cancelled.',
'模型切换已取消。': 'The model switch was cancelled.',
'推理等级切换已取消。': 'The reasoning effort switch was cancelled.',
'暂时无法获取模型列表,请稍后重试。':
'Unable to get the model list right now; please try again later.',
'暂时无法获取推理等级,请稍后重试。':
'Unable to get reasoning efforts right now; please try again later.',
'模型切换失败,请稍后重试。': 'Failed to switch the model; please try again later.',
'推理等级切换失败,请稍后重试。':
'Failed to switch the reasoning effort; please try again later.',
'模型命令仅支持纯文字,请移除图片后重试。':
'Model commands support text only; please remove images and try again.',
'模型和推理等级命令仅支持纯文字,请移除图片后重试。':
'Model and reasoning effort commands support text only; please remove images and try again.',
'当前任务正在等待你的回答或审批。': 'The current task is waiting for your answer or approval.',
'请先处理当前请求,或者发送 /stop 停止任务。':
'Please handle the current request first, or send /stop to stop the task.',
'没有找到模型:{model}': 'Model not found: {model}',
'请发送 /models 查看可用模型。': 'Please send /models to view available models.',
'模型不支持推理等级:{effort}': 'The model does not support reasoning effort: {effort}',
'该模型不提供可切换的推理等级。':
'This model does not provide switchable reasoning efforts.',
'模型已切换为:\n{model}\n\n后续消息将使用该模型。':
'Model switched to:\n{model}\n\nSubsequent messages will use this model.',
'模型已切换为:\n{model}\n推理等级:{effort}\n\n后续消息将使用该模型和推理等级。':
'Model switched to:\n{model}\nReasoning effort: {effort}\n\nSubsequent messages will use this model and reasoning effort.',
'推理等级已切换为:\n{effort}\n\n当前模型:{model}\n后续消息将使用该推理等级。':
'Reasoning effort switched to:\n{effort}\n\nCurrent model: {model}\nSubsequent messages will use this reasoning effort.',
// compact-command.mjs
'用法:/compact(不带参数)': 'Usage: /compact (no arguments)',

View file

@ -5,8 +5,13 @@ import { withSessionBindingLock } from './session-binding-lock.mjs';
const MODEL_COMMAND = /^\/model(?=$|\s)/i;
const MODELS_COMMAND = /^\/models(?=$|\s)/i;
const MODEL_USAGE = '用法:/model <序号> 或 /model <provider>/<model>';
const REASONING_COMMAND = /^\/reasoning(?=$|\s)/i;
const REASONINGS_COMMAND = /^\/reasonings(?=$|\s)/i;
const REASONING_LIST_COMMAND = /^\/reasoninglist(?=$|\s)/i;
const MODEL_USAGE = '用法:/model <序号或 provider/model> [推理等级ID]';
const MODELS_USAGE = '用法:/models(不带参数)';
const REASONING_USAGE = '用法:/reasoning [序号、等级ID或 --default]';
const REASONING_LIST_USAGE = '用法:/reasoninglist 或 /reasonings(不带参数)';
const SESSION_BINDING_CHANGED = 'session-binding-changed';
const MODEL_SELECTION_MISMATCH = 'model-selection-mismatch';
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
@ -28,6 +33,35 @@ function rpcOptions(signal) {
return signal ? { signal } : {};
}
function normalizeReasoning(value) {
if (value === undefined) return undefined;
if (!value || typeof value !== 'object'
|| !Array.isArray(value.efforts) || value.efforts.length === 0) {
throw new TypeError('Harness returned invalid model reasoning metadata');
}
const efforts = value.efforts.map((effort) => {
if (!effort || typeof effort !== 'object'
|| typeof effort.id !== 'string' || !effort.id
|| typeof effort.name !== 'string' || !effort.name
|| (effort.description !== undefined && typeof effort.description !== 'string')) {
throw new TypeError('Harness returned an invalid reasoning effort');
}
return {
id: effort.id,
name: effort.name,
...(effort.description === undefined ? {} : { description: effort.description }),
};
});
if (value.defaultEffort !== undefined
&& (typeof value.defaultEffort !== 'string' || !value.defaultEffort)) {
throw new TypeError('Harness returned an invalid default reasoning effort');
}
return {
efforts,
...(value.defaultEffort === undefined ? {} : { defaultEffort: value.defaultEffort }),
};
}
function normalizeCatalog(value, { requireCurrent = false } = {}) {
if (!value || typeof value !== 'object'
|| !Array.isArray(value.groups) || !Array.isArray(value.failures)) {
@ -46,10 +80,18 @@ function normalizeCatalog(value, { requireCurrent = false } = {}) {
models: group.models.map((model) => {
if (!model || typeof model !== 'object'
|| typeof model.id !== 'string' || !model.id
|| typeof model.name !== 'string' || !model.name) {
|| typeof model.name !== 'string' || !model.name
|| (model.description !== undefined && typeof model.description !== 'string')) {
throw new TypeError('Harness returned an invalid model');
}
return { id: model.id, name: model.name };
return {
id: model.id,
name: model.name,
...(model.description === undefined ? {} : { description: model.description }),
...(model.reasoning === undefined
? {}
: { reasoning: normalizeReasoning(model.reasoning) }),
};
}),
};
});
@ -65,10 +107,19 @@ function normalizeCatalog(value, { requireCurrent = false } = {}) {
if (value.current !== undefined) {
if (!value.current || typeof value.current !== 'object'
|| typeof value.current.provider !== 'string' || !value.current.provider
|| typeof value.current.model !== 'string' || !value.current.model) {
|| typeof value.current.model !== 'string' || !value.current.model
|| (value.current.reasoningEffort !== undefined
&& (typeof value.current.reasoningEffort !== 'string'
|| !value.current.reasoningEffort))) {
throw new TypeError('Harness returned an invalid current model');
}
current = { provider: value.current.provider, model: value.current.model };
current = {
provider: value.current.provider,
model: value.current.model,
...(value.current.reasoningEffort === undefined
? {}
: { reasoningEffort: value.current.reasoningEffort }),
};
} else if (requireCurrent) {
throw new TypeError('Harness returned no current model');
}
@ -83,6 +134,24 @@ function sameModel(left, right) {
return left?.provider === right?.provider && left?.model === right?.model;
}
function sameSelection(left, right) {
return sameModel(left, right) && left?.reasoningEffort === right?.reasoningEffort;
}
function confirmsSelection(actual, requested) {
return sameModel(actual, requested)
&& (requested.reasoningEffort === undefined
|| actual?.reasoningEffort === requested.reasoningEffort);
}
function selectionText(selection) {
if (!selection?.provider || !selection?.model) return '';
const id = modelId(selection.provider, selection.model);
return selection.reasoningEffort === undefined
? id
: `${id} · reasoningEffort=${safeDisplayText(selection.reasoningEffort)}`;
}
function selectionMismatch(expected, actual, source) {
const error = new Error(`Harness ${source} did not confirm the selected model`);
error.code = MODEL_SELECTION_MISMATCH;
@ -129,12 +198,54 @@ function modelAt(catalog, requestedIndex) {
return null;
}
function modelNumberRequest(requested) {
function positiveNumberRequest(requested) {
if (!/^\d+$/u.test(requested)) return null;
const index = Number(requested);
return { index: Number.isSafeInteger(index) && index > 0 ? index : null };
}
function modelForSelection(catalog, selection) {
if (!selection) return null;
const group = catalog.groups.find(({ id }) => id === selection.provider);
return group?.models.find(({ id }) => id === selection.model) ?? null;
}
function reasoningEffortAt(model, requestedIndex) {
return model?.reasoning?.efforts?.[requestedIndex - 1] ?? null;
}
function reasoningEffortById(model, requestedId) {
return model?.reasoning?.efforts?.find(({ id }) => id === requestedId) ?? null;
}
function effectiveReasoningEffort(current, model) {
return current?.reasoningEffort ?? model?.reasoning?.defaultEffort;
}
function reasoningEffortText(model, effortId) {
if (effortId === undefined) return t('Default(由模型或 Provider 决定)');
const effort = reasoningEffortById(model, effortId);
if (!effort) return safeDisplayText(effortId);
const name = safeDisplayText(effort.name);
const id = safeDisplayText(effort.id);
return name === id ? id : `${name} (${id})`;
}
function currentReasoningEffortText(catalog) {
const model = modelForSelection(catalog, catalog.current);
return reasoningEffortText(
model,
effectiveReasoningEffort(catalog.current, model),
);
}
function reasoningMarker(effortId, currentId, defaultId) {
if (effortId === currentId && effortId === defaultId) return t('(当前、默认)');
if (effortId === currentId) return t('(当前)');
if (effortId === defaultId) return t('(默认)');
return '';
}
function invalidModelNumberMessage(requested) {
return [
t('模型序号无效:{input}', { input: safeDisplayText(requested) }),
@ -143,6 +254,29 @@ function invalidModelNumberMessage(requested) {
].join('\n');
}
function invalidReasoningNumberMessage(requested) {
return [
t('推理等级序号无效:{input}', { input: safeDisplayText(requested) }),
'',
t('请发送 /reasoninglist 查看并输入有效的正整数序号。'),
].join('\n');
}
function unsupportedReasoningMessage(selection, requested, model) {
const lines = [
t('模型不支持推理等级:{effort}', { effort: safeDisplayText(requested) }),
'',
safeDisplayText(selectionText(selection)),
];
const ids = model?.reasoning?.efforts?.map(({ id }) => safeDisplayText(id)) ?? [];
if (ids.length > 0) {
lines.push(t('可用推理等级:{efforts}', { efforts: ids.join(', ') }));
} else {
lines.push(t('该模型不提供可切换的推理等级。'));
}
return lines.join('\n');
}
function formatCatalog(catalog) {
const currentId = catalog.current
? modelId(catalog.current.provider, catalog.current.model)
@ -164,20 +298,72 @@ function formatCatalog(catalog) {
lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
}
}
if (index > 0) lines.push('', t('切换模型:/model <序号>'));
if (index > 0) lines.push('', t('切换模型:/model <序号> [推理等级ID]'));
return lines.join('\n');
}
function currentModelMessage(current) {
function currentModelMessage(catalog) {
return [
t('当前模型:'),
modelId(current.provider, current.model),
modelId(catalog.current.provider, catalog.current.model),
t('当前推理等级:{effort}', { effort: currentReasoningEffortText(catalog) }),
'',
t('查看全部模型:/models'),
t('切换模型:/model <序号>'),
t('查看可用推理等级:/reasoninglist'),
t('切换模型:/model <序号> [推理等级ID]'),
].join('\n');
}
function currentReasoningMessage(catalog) {
return [
t('当前模型:'),
modelId(catalog.current.provider, catalog.current.model),
t('当前推理等级:{effort}', { effort: currentReasoningEffortText(catalog) }),
'',
t('查看可用推理等级:/reasoninglist'),
t('切换推理等级:/reasoning <序号或等级ID>'),
t('恢复默认等级:/reasoning --default'),
].join('\n');
}
function formatReasoningCatalog(catalog) {
const current = catalog.current;
const model = modelForSelection(catalog, current);
const currentEffort = effectiveReasoningEffort(current, model);
const lines = [
t('当前模型:'),
modelId(current.provider, current.model),
t('当前推理等级:{effort}', { effort: reasoningEffortText(model, currentEffort) }),
'',
t('可用推理等级:'),
];
if (!model?.reasoning) {
lines.push(
t('该模型不提供可切换的推理等级。'),
'',
t('恢复默认等级:/reasoning --default'),
);
return lines.join('\n');
}
for (const [index, effort] of model.reasoning.efforts.entries()) {
const label = reasoningEffortText(model, effort.id);
const marker = reasoningMarker(
effort.id,
currentEffort,
model.reasoning.defaultEffort,
);
lines.push(`${index + 1}. ${label}${marker}`);
const description = safeDisplayText(effort.description);
if (description) lines.push(` ${description}`);
}
lines.push(
'',
t('切换推理等级:/reasoning <序号或等级ID>'),
t('恢复默认等级:/reasoning --default'),
);
return lines.join('\n');
}
function noSessionMessage() {
return [
t('当前聊天还没有会话。'),
@ -187,6 +373,14 @@ function noSessionMessage() {
].join('\n');
}
function noReasoningSessionMessage() {
return [
t('当前聊天还没有会话。'),
'',
t('请先发送一条普通消息创建会话。'),
].join('\n');
}
function errorCode(error) {
return error?.code ?? error?.failure?.code;
}
@ -200,6 +394,9 @@ function modelErrorMessage(error, action) {
return t('当前聊天绑定的会话已不存在,请重试。');
}
if (code === 'model-unavailable') {
if (action === 'reasoning-select') {
return t('无法切换推理等级。当前模型或推理等级不可用。');
}
return t('无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。');
}
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
@ -211,26 +408,32 @@ function modelErrorMessage(error, action) {
if (code === MODEL_SELECTION_MISMATCH) {
const expected = error?.expected;
const actual = error?.actual;
const lines = [t('模型切换失败,请稍后重试。')];
const lines = [action === 'reasoning-select'
? t('推理等级切换失败,请稍后重试。')
: t('模型切换失败,请稍后重试。')];
if (expected?.provider && expected?.model) {
lines.push('', `requested: ${safeDisplayText(modelId(expected.provider, expected.model))}`);
lines.push('', `requested: ${safeDisplayText(selectionText(expected))}`);
}
if (actual?.provider && actual?.model) {
const label = error?.source === 'models.current'
? t('当前模型:')
: 'selectModel.selected:';
lines.push(`${label} ${safeDisplayText(modelId(actual.provider, actual.model))}`);
lines.push(`${label} ${safeDisplayText(selectionText(actual))}`);
} else {
lines.push(`${error?.source ?? 'Harness'}: unconfirmed`);
}
return lines.join('\n');
}
if (code === 'cancelled' || error?.name === 'AbortError') {
return action === 'list' ? t('获取模型列表已取消。') : t('模型切换已取消。');
if (action === 'list') return t('获取模型列表已取消。');
if (action === 'reasoning-list') return t('获取推理等级列表已取消。');
if (action === 'reasoning-select') return t('推理等级切换已取消。');
return t('模型切换已取消。');
}
return action === 'list'
? t('暂时无法获取模型列表,请稍后重试。')
: t('模型切换失败,请稍后重试。');
if (action === 'list') return t('暂时无法获取模型列表,请稍后重试。');
if (action === 'reasoning-list') return t('暂时无法获取推理等级,请稍后重试。');
if (action === 'reasoning-select') return t('推理等级切换失败,请稍后重试。');
return t('模型切换失败,请稍后重试。');
}
async function boundSession(harness, state, key, options) {
@ -279,12 +482,12 @@ async function selectAndVerifyModel(session, selection, options) {
throw new TypeError('Harness session does not support model selection');
}
const selected = (await session.selectModel(selection, options))?.selected;
if (!sameModel(selected, selection)) {
if (!confirmsSelection(selected, selection)) {
throw selectionMismatch(selection, selected, 'selectModel.selected');
}
const current = (await sessionCatalog(session, options)).current;
if (!sameModel(current, selection)) {
throw selectionMismatch(selection, current, 'models.current');
if (!sameSelection(current, selected)) {
throw selectionMismatch(selected, current, 'models.current');
}
return current;
}
@ -293,17 +496,29 @@ function isModelsCommand(command) {
return MODELS_COMMAND.test(command);
}
function isReasoningListCommand(command) {
return REASONING_LIST_COMMAND.test(command) || REASONINGS_COMMAND.test(command);
}
function isReasoningCommand(command) {
return REASONING_COMMAND.test(command);
}
export function isModelCommand(text) {
if (typeof text !== 'string') return false;
const command = text.trim();
return MODELS_COMMAND.test(command) || MODEL_COMMAND.test(command);
return MODELS_COMMAND.test(command)
|| MODEL_COMMAND.test(command)
|| REASONING_LIST_COMMAND.test(command)
|| REASONINGS_COMMAND.test(command)
|| REASONING_COMMAND.test(command);
}
export async function runModelCommand(text, harness, state, key, options = {}) {
if (!isModelCommand(text)) return null;
const command = text.trim();
if (options.hasImages) {
return commandResult(t('模型命令仅支持纯文字,请移除图片后重试。'));
return commandResult(t('模型和推理等级命令仅支持纯文字,请移除图片后重试。'));
}
const requestOptions = rpcOptions(options.signal);
@ -320,20 +535,115 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
}
}
const match = /^\/model(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
if (isReasoningListCommand(command)) {
if (!/^\/(?:reasoninglist|reasonings)[ \t]*$/iu.test(command)) {
return commandResult(t(REASONING_LIST_USAGE));
}
try {
const bound = await boundSession(harness, state, key, requestOptions);
if (!bound) return commandResult(noReasoningSessionMessage());
return commandResult(formatReasoningCatalog(
await sessionCatalog(bound.session, requestOptions),
));
} catch (error) {
return commandResult(modelErrorMessage(error, 'reasoning-list'));
}
}
if (isReasoningCommand(command)) {
const match = /^\/reasoning(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
if (!match) return commandResult(t(REASONING_USAGE));
const requested = match[1];
if (!requested) {
try {
const bound = await boundSession(harness, state, key, requestOptions);
if (!bound) return commandResult(noReasoningSessionMessage());
return commandResult(currentReasoningMessage(
await sessionCatalog(bound.session, requestOptions),
));
} catch (error) {
return commandResult(modelErrorMessage(error, 'reasoning-list'));
}
}
if (options.pendingInteraction) {
return commandResult([
t('当前任务正在等待你的回答或审批。'),
'',
t('请先处理当前请求,或者发送 /stop 停止任务。'),
].join('\n'));
}
try {
return await withSessionBindingLock(state, key, async () => {
const bound = await boundSession(harness, state, key, requestOptions);
if (!bound) return commandResult(noReasoningSessionMessage());
if (await sessionIsBusy(bound.session, options.control, requestOptions)) {
return commandResult(t('当前任务正在运行,请等待完成或先发送 /stop。'));
}
const catalog = await sessionCatalog(bound.session, requestOptions);
const current = catalog.current;
const model = modelForSelection(catalog, current);
let effort;
if (requested.toLowerCase() === '--default') {
effort = undefined;
} else {
if (!model?.reasoning) {
return commandResult(unsupportedReasoningMessage(current, requested, model));
}
effort = reasoningEffortById(model, requested);
if (!effort) {
const numberRequest = positiveNumberRequest(requested);
if (numberRequest?.index === null) {
return commandResult(invalidReasoningNumberMessage(requested));
}
if (!numberRequest) {
return commandResult(unsupportedReasoningMessage(current, requested, model));
}
effort = reasoningEffortAt(model, numberRequest.index);
if (!effort) return commandResult(invalidReasoningNumberMessage(requested));
}
effort = effort.id;
}
const selection = {
provider: current.provider,
model: current.model,
...(effort === undefined ? {} : { reasoningEffort: effort }),
};
const applied = await selectAndVerifyModel(bound.session, selection, requestOptions);
assertSessionBinding(state, key, bound.sessionId);
return commandResult(t(`推理等级已切换为:
{effort}
当前模型:{model}
后续消息将使用该推理等级。`, {
model: modelId(applied.provider, applied.model),
effort: reasoningEffortText(
model,
effectiveReasoningEffort(applied, model),
),
}));
});
} catch (error) {
return commandResult(modelErrorMessage(error, 'reasoning-select'));
}
}
const match = /^\/model(?:[ \t]+([^\s]+)(?:[ \t]+([^\s]+))?)?[ \t]*$/iu.exec(command);
if (!match) return commandResult(t(MODEL_USAGE));
const requested = match[1];
const requestedEffort = match[2];
if (!requested) {
try {
const bound = await boundSession(harness, state, key, requestOptions);
if (!bound) return commandResult(noSessionMessage());
const catalog = await sessionCatalog(bound.session, requestOptions);
return commandResult(currentModelMessage(catalog.current));
return commandResult(currentModelMessage(catalog));
} catch (error) {
return commandResult(modelErrorMessage(error, 'select'));
}
}
const numberRequest = modelNumberRequest(requested);
const numberRequest = positiveNumberRequest(requested);
if (numberRequest?.index === null) {
return commandResult(invalidModelNumberMessage(requested));
}
@ -370,6 +680,18 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
t('请发送 /models 查看可用模型。'),
].join('\n'));
}
const targetModel = modelForSelection(catalog, selection);
if (requestedEffort !== undefined) {
const effort = reasoningEffortById(targetModel, requestedEffort);
if (!effort) {
return commandResult(unsupportedReasoningMessage(
selection,
requestedEffort,
targetModel,
));
}
selection.reasoningEffort = effort.id;
}
let applied;
if (bound) {
@ -400,8 +722,15 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
}
return commandResult(t(`模型已切换为:
{model}
推理等级:{effort}
后续消息将使用该模型。`, { model: modelId(applied.provider, applied.model) }));
后续消息将使用该模型和推理等级。`, {
model: modelId(applied.provider, applied.model),
effort: reasoningEffortText(
targetModel,
effectiveReasoningEffort(applied, targetModel),
),
}));
});
} catch (error) {
return commandResult(modelErrorMessage(error, 'select'));

View file

@ -401,8 +401,10 @@ export class TextHarnessBridge {
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),

View file

@ -53,8 +53,10 @@ function helpText() {
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),

View file

@ -60,8 +60,10 @@ const HELP_TEXT = () => [
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),