mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
feat: support numbered model selection
This commit is contained in:
parent
12112b0881
commit
8121557460
17 changed files with 230 additions and 152 deletions
10
README.en.md
10
README.en.md
|
|
@ -88,9 +88,9 @@ After installation, follow the built-in instructions on each channel page to sca
|
|||
| `/help` | Show the commands and usage supported by the bot. |
|
||||
| `/new` | Unbind the current chat so its next ordinary message starts a new Harness Session. |
|
||||
| `/status` | Check the connection between the current bot and DeepSeek Harness. |
|
||||
| `/models` | List every currently configured model, grouped by provider. |
|
||||
| `/models` | List every currently configured model with a number. |
|
||||
| `/model` | Show the model used by the Session bound to this chat. |
|
||||
| `/model <provider/model-id>` | Switch the model for the Session bound to this chat. |
|
||||
| `/model <number or provider/model-id>` | Switch the model for the Session bound to this chat. |
|
||||
| `/stop` | Immediately stop this chat's running task while preserving work that has not started. |
|
||||
| `/steer <additional instruction>` | Inject an additional instruction into this chat's running task. |
|
||||
| `/compact` | Immediately compact older context in the Session bound to the current chat. |
|
||||
|
|
@ -101,15 +101,15 @@ After installation, follow the built-in instructions on each channel page to sca
|
|||
| Interactive question | Reply with an option number, option label, or custom text; separate multiple choices with commas. |
|
||||
| Remote approval | Reply with `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`. |
|
||||
|
||||
Examples: `/help`, `/new`, `/status`, `/models`, `/model deepseek-official/deepseek-v4-pro`, `/steer inspect only the configuration file`, `/stop`, `/compact`, `/workspace /Users/alice/projects/my-app`, `/sessionlist 2`, `/sessionlist /Users/alice/projects/my-app`, or `/session session-id`
|
||||
Example: send `/models`, then `/model 2` to switch to the second model in the list. Other examples: `/help`, `/new`, `/status`, `/model deepseek-official/deepseek-v4-pro`, `/steer inspect only the configuration file`, `/stop`, `/compact`, `/workspace /Users/alice/projects/my-app`, `/sessionlist 2`, `/sessionlist /Users/alice/projects/my-app`, or `/session session-id`
|
||||
|
||||
### Command details
|
||||
|
||||
- `/help` takes no arguments and never creates a Session. It returns the complete command list supported by the current bot.
|
||||
- `/status` takes no arguments, never prompts the model, and does not change the Session binding. It confirms that the current bot can reach DeepSeek Harness.
|
||||
- `/new` only removes the current chat's saved dsh-im Session binding; it never deletes, empties, or archives the old Session. The next ordinary message creates and binds a new Session in the current workspace. If a task is running or waiting for a question or approval, finish the interaction or use `/stop` before `/new`.
|
||||
- `/models` takes no arguments and never creates a Session. It lists every currently configured Harness model using stable, copyable `provider/model-id` values. If one provider fails, models from the remaining providers are still shown.
|
||||
- Bare `/model` only displays the current Session model. `/model <provider/model-id>` accepts an exact value returned by `/models`. When the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model. The switch affects only that Session; Harness also attempts to save it as the default for future Sessions, while other existing Sessions remain unchanged.
|
||||
- `/models` takes no arguments and never creates a Session. It assigns a number to every currently configured Harness model and also shows its stable, copyable `provider/model-id`. If one provider fails, models from the remaining providers are still shown.
|
||||
- Bare `/model` only displays the current Session model. A model can be selected by the number or exact full ID returned by `/models`, for example `/model 2`. When the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model. The switch affects only that Session; Harness also attempts to save it as the default for future Sessions, while other existing Sessions remain unchanged.
|
||||
- A model cannot be switched while a task is running or waiting for an approval or question answer. Wait for it to finish or use `/stop` first. A Session containing images cannot switch to a model that does not accept image input.
|
||||
- `/stop` and `/steer` control only a running task started by this chat. Even when multiple chats bind the same Session, they do not intentionally control another chat's task. `/stop` does not delete the Session or its history, preserves queued work that has not started, and is safe to repeat.
|
||||
- `/steer` accepts text only, including multiple lines. It neither creates another Session nor starts a second task. Send an ordinary message when no task is running; while an approval or question is pending, answer it first or use `/stop`.
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -91,9 +91,9 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
|
|||
| `/help` | 显示机器人支持的命令和用法。 |
|
||||
| `/new` | 解除当前聊天的会话绑定,让下一条普通消息开启全新 Harness 会话。 |
|
||||
| `/status` | 检查当前机器人与 DeepSeek Harness 的连接状态。 |
|
||||
| `/models` | 按 Provider 列出当前配置的全部可用模型。 |
|
||||
| `/models` | 按序号列出当前配置的全部可用模型。 |
|
||||
| `/model` | 查看当前聊天绑定会话正在使用的模型。 |
|
||||
| `/model <Provider/模型ID>` | 切换当前聊天绑定会话的模型。 |
|
||||
| `/model <序号或 Provider/模型ID>` | 切换当前聊天绑定会话的模型。 |
|
||||
| `/stop` | 立即停止当前聊天正在运行的任务,并保留尚未开始的排队消息。 |
|
||||
| `/steer <补充指令>` | 把补充指令立即加入当前聊天正在运行的任务。 |
|
||||
| `/compact` | 立即压缩当前聊天绑定会话的较早上下文。 |
|
||||
|
|
@ -104,15 +104,15 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
|
|||
| 交互式提问 | 回复选项序号、选项文字或自定义文字;多选时用逗号分隔。 |
|
||||
| 远程审批 | 回复 `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`。 |
|
||||
|
||||
示例:`/help`、`/new`、`/status`、`/models`、`/model deepseek-official/deepseek-v4-pro`、`/steer 只检查配置文件`、`/stop`、`/compact`、`/workspace /Users/alice/projects/my-app`、`/sessionlist 2`、`/sessionlist /Users/alice/projects/my-app` 或 `/session session-id`
|
||||
示例:先发送 `/models`,再发送 `/model 2` 切换到列表中的第 2 个模型。其他命令示例:`/help`、`/new`、`/status`、`/model deepseek-official/deepseek-v4-pro`、`/steer 只检查配置文件`、`/stop`、`/compact`、`/workspace /Users/alice/projects/my-app`、`/sessionlist 2`、`/sessionlist /Users/alice/projects/my-app` 或 `/session session-id`
|
||||
|
||||
### 命令说明
|
||||
|
||||
- `/help` 不需要参数,也不会创建会话;它会返回当前机器人支持的完整命令列表。
|
||||
- `/status` 不需要参数,也不会向模型发送消息或改变会话绑定;它用于确认当前机器人能够连接 DeepSeek Harness。
|
||||
- `/new` 只解除当前聊天在 dsh-im 中保存的会话绑定,不会删除、清空或归档旧 Session。下一条普通消息会在当前工作区创建并绑定一个新 Session。任务正在运行或等待问题、审批时,应先完成交互或使用 `/stop`,再使用 `/new`。
|
||||
- `/models` 不需要参数,也不会创建会话。它列出 Harness 当前配置的全部可用模型,使用可稳定复制的 `Provider/模型ID`;某个 Provider 查询失败时,其他 Provider 的结果仍会显示。
|
||||
- `/model` 不带参数时只查看当前会话模型;带完整模型 ID 时只接受 `/models` 列出的精确值。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。切换只影响当前会话;Harness 还会尝试把它保存为以后新会话的默认模型,已有其他会话不受影响。
|
||||
- `/models` 不需要参数,也不会创建会话。它为 Harness 当前配置的全部可用模型分配序号,同时显示可稳定复制的 `Provider/模型ID`;某个 Provider 查询失败时,其他 Provider 的结果仍会显示。
|
||||
- `/model` 不带参数时只查看当前会话模型;带参数时接受 `/models` 列出的序号或完整模型 ID,例如 `/model 2`。完整 ID 必须精确匹配。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。切换只影响当前会话;Harness 还会尝试把它保存为以后新会话的默认模型,已有其他会话不受影响。
|
||||
- 正在运行任务或等待审批、问题回答时不能切换模型;请等待完成,或先使用 `/stop`。含图片的会话无法切换到不支持图片输入的模型。
|
||||
- `/stop` 和 `/steer` 只控制当前聊天自己发起的运行任务,即使多个聊天绑定同一个 Session,也不会有意控制其他聊天的任务。`/stop` 不删除会话或历史,并保留尚未开始的排队消息;重复发送是安全的。
|
||||
- `/steer` 只接受文字,可包含多行;它不会创建新会话或第二个任务。没有运行任务时请直接发送普通消息;等待审批或问题回答时请先处理交互,或使用 `/stop`。
|
||||
|
|
|
|||
225
lib/index.js
225
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -41,8 +41,9 @@ const HELP_TEXT = [
|
|||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 列出所有可用模型',
|
||||
'/model [模型ID] 查看或切换当前会话模型',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ const HELP_TEXT = [
|
|||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 列出所有可用模型',
|
||||
'/model [模型ID] 查看或切换当前会话模型',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ const HELP_TEXT = [
|
|||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 列出所有可用模型',
|
||||
'/model [模型ID] 查看或切换当前会话模型',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|||
|
||||
const MODEL_COMMAND = /^\/model(?=$|\s)/i;
|
||||
const MODELS_COMMAND = /^\/models(?=$|\s)/i;
|
||||
const MODEL_USAGE = '用法:/model <provider>/<model>';
|
||||
const MODEL_USAGE = '用法:/model <序号> 或 /model <provider>/<model>';
|
||||
const MODELS_USAGE = '用法:/models(不带参数)';
|
||||
const SESSION_BINDING_CHANGED = 'session-binding-changed';
|
||||
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
||||
|
|
@ -88,17 +88,46 @@ function matchingModel(catalog, requested) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function modelAt(catalog, requestedIndex) {
|
||||
let index = 0;
|
||||
for (const group of catalog.groups) {
|
||||
for (const model of group.models) {
|
||||
index += 1;
|
||||
if (index === requestedIndex) {
|
||||
return { provider: group.id, model: model.id };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function modelNumberRequest(requested) {
|
||||
if (!/^\d+$/u.test(requested)) return null;
|
||||
const index = Number(requested);
|
||||
return { index: Number.isSafeInteger(index) && index > 0 ? index : null };
|
||||
}
|
||||
|
||||
function invalidModelNumberMessage(requested) {
|
||||
return [
|
||||
`模型序号无效:${safeDisplayText(requested)}`,
|
||||
'',
|
||||
'请发送 /models 查看并输入有效的正整数序号。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function formatCatalog(catalog) {
|
||||
const currentId = catalog.current
|
||||
? modelId(catalog.current.provider, catalog.current.model)
|
||||
: null;
|
||||
const lines = ['可用模型:'];
|
||||
let index = 0;
|
||||
if (catalog.groups.length === 0) lines.push('', '当前没有可用模型。');
|
||||
for (const group of catalog.groups) {
|
||||
lines.push('', safeDisplayText(group.name) || safeDisplayText(group.id));
|
||||
for (const model of group.models) {
|
||||
index += 1;
|
||||
const id = modelId(group.id, model.id);
|
||||
lines.push(`- ${safeDisplayText(id)}${id === currentId ? '(当前)' : ''}`);
|
||||
lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? '(当前)' : ''}`);
|
||||
}
|
||||
}
|
||||
if (catalog.failures.length > 0) {
|
||||
|
|
@ -107,6 +136,7 @@ function formatCatalog(catalog) {
|
|||
lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
|
||||
}
|
||||
}
|
||||
if (index > 0) lines.push('', '切换模型:/model <序号>');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +146,7 @@ function currentModelMessage(current) {
|
|||
modelId(current.provider, current.model),
|
||||
'',
|
||||
'查看全部模型:/models',
|
||||
'切换模型:/model <provider>/<model>',
|
||||
'切换模型:/model <序号>',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +155,7 @@ function noSessionMessage() {
|
|||
'当前聊天还没有会话。',
|
||||
'',
|
||||
'查看模型:/models',
|
||||
'选择模型:/model <provider>/<model>',
|
||||
'选择模型:/model <序号>',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +273,12 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
return commandResult(modelErrorMessage(error, 'select'));
|
||||
}
|
||||
}
|
||||
if (!requested.includes('/') || requested.startsWith('/') || requested.endsWith('/')) {
|
||||
const numberRequest = modelNumberRequest(requested);
|
||||
if (numberRequest?.index === null) {
|
||||
return commandResult(invalidModelNumberMessage(requested));
|
||||
}
|
||||
if (!numberRequest
|
||||
&& (!requested.includes('/') || requested.startsWith('/') || requested.endsWith('/'))) {
|
||||
return commandResult(MODEL_USAGE);
|
||||
}
|
||||
if (options.pendingInteraction) {
|
||||
|
|
@ -264,8 +299,11 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|||
const catalog = bound
|
||||
? await sessionCatalog(bound.session, requestOptions)
|
||||
: await listCatalog(harness, requestOptions);
|
||||
const selection = matchingModel(catalog, requested);
|
||||
const selection = numberRequest
|
||||
? modelAt(catalog, numberRequest.index)
|
||||
: matchingModel(catalog, requested);
|
||||
if (!selection) {
|
||||
if (numberRequest) return commandResult(invalidModelNumberMessage(requested));
|
||||
return commandResult([
|
||||
`没有找到模型:${safeDisplayText(requested)}`,
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -316,8 +316,9 @@ export class TextHarnessBridge {
|
|||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 列出所有可用模型',
|
||||
'/model [模型ID] 查看或切换当前会话模型',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ const HELP_TEXT = [
|
|||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 列出所有可用模型',
|
||||
'/model [模型ID] 查看或切换当前会话模型',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ const HELP_TEXT = [
|
|||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 列出所有可用模型',
|
||||
'/model [模型ID] 查看或切换当前会话模型',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
||||
'示例:先发 /models,再发 /model 2',
|
||||
'/stop 停止当前任务',
|
||||
'/steer 补充指令 纠偏当前任务',
|
||||
'/status 检查连接状态',
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ test('DingTalk lists models without prompting and help advertises all four comma
|
|||
});
|
||||
|
||||
await bridge.accept(message('models-dingtalk', '/models'));
|
||||
assert.match(sent.at(-1).text, /dingtalk-provider\/model-one/);
|
||||
assert.match(sent.at(-1).text, /1\. dingtalk-provider\/model-one/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
|
@ -356,6 +356,7 @@ test('DingTalk lists models without prompting and help advertises all four comma
|
|||
for (const command of ['/models', '/model', '/stop', '/steer']) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
});
|
||||
|
||||
test('bridge maps a DingTalk direct conversation to one persistent Harness session', async () => {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ test('Feishu lists models without prompting and help advertises all four command
|
|||
|
||||
await bridge.accept(event('models-feishu', '/models'));
|
||||
await bridge.waitForIdle();
|
||||
assert.match(sent.at(-1), /feishu-provider\/model-one/);
|
||||
assert.match(sent.at(-1), /1\. feishu-provider\/model-one/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
|
@ -199,6 +199,7 @@ test('Feishu lists models without prompting and help advertises all four command
|
|||
for (const command of ['/models', '/model', '/stop', '/steer']) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
});
|
||||
|
||||
test('bridge maps a Feishu conversation to a persistent Harness session and replies', async () => {
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ test('QQ lists models without prompting and help advertises all four commands',
|
|||
});
|
||||
|
||||
await bridge.accept(message({ messageId: 'models-qq', content: '/models' }));
|
||||
assert.match(sent.at(-1), /qq-provider\/model-one/);
|
||||
assert.match(sent.at(-1), /1\. qq-provider\/model-one/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
|
@ -293,6 +293,7 @@ test('QQ lists models without prompting and help advertises all four commands',
|
|||
for (const command of ['/models', '/model', '/stop', '/steer']) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
});
|
||||
|
||||
test('QQ remembers any authorized private inbound as a connection-test target', async () => {
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ test('all four shared text channels list models locally and advertise all four c
|
|||
});
|
||||
|
||||
await bridge.accept(message(`models-${name}`, '/models'));
|
||||
assert.match(sent.at(-1), new RegExp(`${name}-provider/model-one`), name);
|
||||
assert.match(sent.at(-1), new RegExp(`1\\. ${name}-provider/model-one`), name);
|
||||
assert.equal(asks, 0, `${name} ask`);
|
||||
assert.equal(creates, 0, `${name} create`);
|
||||
assert.equal(fixture.sessions.size, 0, `${name} session binding`);
|
||||
|
|
@ -203,6 +203,7 @@ test('all four shared text channels list models locally and advertise all four c
|
|||
for (const command of ['/models', '/model', '/stop', '/steer']) {
|
||||
assert.match(help, new RegExp(`\\${command}`), `${name} ${command}`);
|
||||
}
|
||||
assert.match(help, /\/model 2/, `${name} numbered model selection`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ test('Enterprise WeChat lists models without prompting and help advertises all f
|
|||
});
|
||||
|
||||
await bridge.accept(frame({ msgid: 'models-wecom', text: { content: '/models' } }));
|
||||
assert.match(transport.streamed.at(-1).content, /wecom-provider\/model-one/);
|
||||
assert.match(transport.streamed.at(-1).content, /1\. wecom-provider\/model-one/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
|
||||
|
|
@ -187,6 +187,7 @@ test('Enterprise WeChat lists models without prompting and help advertises all f
|
|||
for (const command of ['/models', '/model', '/stop', '/steer']) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
});
|
||||
|
||||
test('Enterprise WeChat messages stream Harness progress and finalize once', async () => {
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ test('Weixin lists models without prompting and help advertises all four command
|
|||
});
|
||||
|
||||
await bridge.accept(message('models-weixin', '/models'));
|
||||
assert.match(sent.at(-1).text, /weixin-provider\/model-one/);
|
||||
assert.match(sent.at(-1).text, /1\. weixin-provider\/model-one/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
|
@ -251,6 +251,7 @@ test('Weixin lists models without prompting and help advertises all four command
|
|||
for (const command of ['/models', '/model', '/stop', '/steer']) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
});
|
||||
|
||||
test('bridge maps the scanning Weixin user to one persistent Harness session and echoes context_token', async () => {
|
||||
|
|
|
|||
|
|
@ -124,8 +124,10 @@ test('/models lists the global catalog without creating a Session', async () =>
|
|||
const result = await runModelCommand('/MODELS', harness, state, 'direct:one', { signal });
|
||||
|
||||
assert.match(result.message, /DeepSeek/);
|
||||
assert.match(result.message, /deepseek-official\/deepseek-v4-flash/);
|
||||
assert.match(result.message, /openrouter\/anthropic\/claude-sonnet-4/);
|
||||
assert.match(result.message, /1\. deepseek-official\/deepseek-v4-flash/);
|
||||
assert.match(result.message, /2\. deepseek-official\/deepseek-v4-pro/);
|
||||
assert.match(result.message, /3\. openrouter\/anthropic\/claude-sonnet-4/);
|
||||
assert.match(result.message, /切换模型:\/model <序号>/);
|
||||
assert.deepEqual(calls, [
|
||||
['sessionFor', 'direct:one'],
|
||||
['listModels', { signal }],
|
||||
|
|
@ -146,7 +148,7 @@ test('/models marks the current Session model and contains provider-local failur
|
|||
const { harness, state } = fixture({ initialSessionId: 'session-one', sessionCatalog });
|
||||
const result = await runModelCommand('/models', harness, state, 'direct:one');
|
||||
|
||||
assert.match(result.message, /deepseek-official\/deepseek-v4-pro(当前)/);
|
||||
assert.match(result.message, /2\. deepseek-official\/deepseek-v4-pro(当前)/);
|
||||
assert.match(result.message, /Private Provider/);
|
||||
assert.doesNotMatch(result.message, /private\.example|secret=abc/);
|
||||
});
|
||||
|
|
@ -181,7 +183,7 @@ test('/models splits a long catalog into lossless 1,800-character messages', asy
|
|||
assert.ok(result.messages.length > 1);
|
||||
assert.ok(result.messages.every((message) => message.length <= 1_800));
|
||||
assert.equal(result.messages.join(''), result.message);
|
||||
assert.match(result.message, /provider\/model-079/);
|
||||
assert.match(result.message, /80\. provider\/model-079/);
|
||||
});
|
||||
|
||||
test('/model reports current state without creating or selecting', async () => {
|
||||
|
|
@ -221,6 +223,31 @@ test('/model uses an exact catalog ID and preserves slashes inside the model ID'
|
|||
]);
|
||||
});
|
||||
|
||||
test('/model accepts the current catalog\'s global 1-based model number', async () => {
|
||||
const { calls, harness, state } = fixture({ initialSessionId: 'session-one' });
|
||||
const result = await runModelCommand('/model 3', harness, state, 'direct:one');
|
||||
|
||||
assert.match(result.message, /openrouter\/anthropic\/claude-sonnet-4/);
|
||||
assert.deepEqual(calls.find(([name]) => name === 'selectModel'), [
|
||||
'selectModel',
|
||||
'session-one',
|
||||
{ provider: 'openrouter', model: 'anthropic/claude-sonnet-4' },
|
||||
{},
|
||||
]);
|
||||
});
|
||||
|
||||
test('/model rejects invalid model numbers without creating or selecting a Session', async () => {
|
||||
for (const requested of ['0', '4', '9007199254740992']) {
|
||||
const { calls, harness, state } = fixture();
|
||||
const result = await runModelCommand(`/model ${requested}`, harness, state, 'direct:one');
|
||||
|
||||
assert.match(result.message, /模型序号无效/, requested);
|
||||
assert.match(result.message, /\/models/, requested);
|
||||
assert.equal(calls.some(([name]) => name === 'createSession'), false, requested);
|
||||
assert.equal(calls.some(([name]) => name === 'selectModel'), false, requested);
|
||||
}
|
||||
});
|
||||
|
||||
test('/model rejects unknown IDs before creating a Session', async () => {
|
||||
const { calls, harness, state } = fixture();
|
||||
const result = await runModelCommand(
|
||||
|
|
@ -243,7 +270,7 @@ test('/model creates and selects a blank Session before exposing its binding', a
|
|||
const { calls, harness, state, boundId } = fixture();
|
||||
const signal = new AbortController().signal;
|
||||
const result = await runModelCommand(
|
||||
'/model deepseek-official/deepseek-v4-pro',
|
||||
'/model 2',
|
||||
harness,
|
||||
state,
|
||||
'direct:one',
|
||||
|
|
@ -288,7 +315,7 @@ test('two concurrent first model switches share one created Session', async () =
|
|||
|
||||
const results = await Promise.all([
|
||||
runModelCommand(
|
||||
'/model deepseek-official/deepseek-v4-pro',
|
||||
'/model 2',
|
||||
harness,
|
||||
state,
|
||||
'direct:one',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue