feat: add plugin version command

This commit is contained in:
xmanrui 2026-08-25 12:25:01 +08:00
parent 10ca7eec2c
commit c5becbe379
24 changed files with 240 additions and 172 deletions

View file

@ -138,6 +138,7 @@ Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only
| `/help` | Show the commands and usage supported by the bot. | | `/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. | | `/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. | | `/status` | Check the connection between the current bot and DeepSeek Harness. |
| `/version` | Show the version of the running dsh-im plugin. |
| `/models` | List every currently configured model with a number. | | `/models` | List every currently configured model with a number. |
| `/model` | Show the model and reasoning effort used by the Session bound to this chat. | | `/model` | Show the model and reasoning effort used by the Session bound to this chat. |
| `/model <number or provider/model-id> [reasoning effort ID]` | Switch the Session model and optionally select an effort supported by the target model. | | `/model <number or provider/model-id> [reasoning effort ID]` | Switch the Session model and optionally select an effort supported by the target model. |
@ -163,7 +164,7 @@ Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only
| Interactive question | Reply with an option number, option label, or custom text; separate multiple choices with commas. | | Interactive question | Reply with an option number, option label, or custom text; separate multiple choices with commas. |
| Remote approval | Reply with `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`. | | Remote approval | Reply with `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`. |
Example: send `/models`, then `/model 2` to switch to the second model in the list; send `/reasoninglist`, then `/reasoning 2` to switch to the current model's second reasoning effort; send `/presetlist`, then `/preset 2` to select the second Agent Preset for this bot. Other examples: `/help`, `/new`, `/status`, `/model deepseek-official/deepseek-v4-pro max`, `/reasoning --default`, `/preset marketing-jeep`, `/preset --default`, `/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; send `/reasoninglist`, then `/reasoning 2` to switch to the current model's second reasoning effort; send `/presetlist`, then `/preset 2` to select the second Agent Preset for this bot. Other examples: `/help`, `/new`, `/status`, `/version`, `/model deepseek-official/deepseek-v4-pro max`, `/reasoning --default`, `/preset marketing-jeep`, `/preset --default`, `/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`
If the Slack desktop app has no native Slash Command registered with the same name, it intercepts messages that begin directly with `/`. Send the command with one leading space instead, for example ` /presetlist` or ` /preset 2`; the plugin command layer trims surrounding whitespace, so it executes exactly like the unspaced form. If the Slack desktop app has no native Slash Command registered with the same name, it intercepts messages that begin directly with `/`. Send the command with one leading space instead, for example ` /presetlist` or ` /preset 2`; the plugin command layer trims surrounding whitespace, so it executes exactly like the unspaced form.
@ -171,6 +172,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
- `/help` takes no arguments and never creates a Session. It returns the complete command list supported by the current bot. - `/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. - `/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.
- `/version` takes no arguments and never contacts Harness, creates a Session, or prompts the model. It returns the version of the running dsh-im plugin.
- `/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`. - `/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 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. - `/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` shows the current Session model and reasoning effort. With arguments, it accepts a number from `/models` or an exact full model ID, followed optionally by an exact reasoning effort ID published in that target model's metadata, for example `/model 2 max`. When the effort is omitted, Harness resolves the target model's current default. If the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model. - Bare `/model` shows the current Session model and reasoning effort. With arguments, it accepts a number from `/models` or an exact full model ID, followed optionally by an exact reasoning effort ID published in that target model's metadata, for example `/model 2 max`. When the effort is omitted, Harness resolves the target model's current default. If the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model.

View file

@ -141,6 +141,7 @@ dsh web
| `/help` | 显示机器人支持的命令和用法。 | | `/help` | 显示机器人支持的命令和用法。 |
| `/new` | 解除当前聊天的会话绑定,让下一条普通消息开启全新 Harness 会话。 | | `/new` | 解除当前聊天的会话绑定,让下一条普通消息开启全新 Harness 会话。 |
| `/status` | 检查当前机器人与 DeepSeek Harness 的连接状态。 | | `/status` | 检查当前机器人与 DeepSeek Harness 的连接状态。 |
| `/version` | 查看当前运行的 dsh-im 插件版本。 |
| `/models` | 按序号列出当前配置的全部可用模型。 | | `/models` | 按序号列出当前配置的全部可用模型。 |
| `/model` | 查看当前聊天绑定会话正在使用的模型和推理等级。 | | `/model` | 查看当前聊天绑定会话正在使用的模型和推理等级。 |
| `/model <序号或 Provider/模型ID> [推理等级ID]` | 切换当前会话模型,并可同时指定目标模型支持的推理等级。 | | `/model <序号或 Provider/模型ID> [推理等级ID]` | 切换当前会话模型,并可同时指定目标模型支持的推理等级。 |
@ -166,7 +167,7 @@ dsh web
| 交互式提问 | 回复选项序号、选项文字或自定义文字;多选时用逗号分隔。 | | 交互式提问 | 回复选项序号、选项文字或自定义文字;多选时用逗号分隔。 |
| 远程审批 | 回复 `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`。 | | 远程审批 | 回复 `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`。 |
示例:先发送 `/models`,再发送 `/model 2` 切换到列表中的第 2 个模型;先发送 `/reasoninglist`,再发送 `/reasoning 2` 切换到当前模型的第 2 个推理等级;先发送 `/presetlist`,再发送 `/preset 2` 为当前机器人选择第 2 个 Agent Preset。其他命令示例:`/help`、`/new`、`/status`、`/model deepseek-official/deepseek-v4-pro max`、`/reasoning --default`、`/preset marketing-jeep`、`/preset --default`、`/steer 只检查配置文件`、`/stop`、`/compact`、`/workspace /Users/alice/projects/my-app`、`/sessionlist 2`、`/sessionlist /Users/alice/projects/my-app` 或 `/session session-id` 示例:先发送 `/models`,再发送 `/model 2` 切换到列表中的第 2 个模型;先发送 `/reasoninglist`,再发送 `/reasoning 2` 切换到当前模型的第 2 个推理等级;先发送 `/presetlist`,再发送 `/preset 2` 为当前机器人选择第 2 个 Agent Preset。其他命令示例:`/help`、`/new`、`/status`、`/version`、`/model deepseek-official/deepseek-v4-pro max`、`/reasoning --default`、`/preset marketing-jeep`、`/preset --default`、`/steer 只检查配置文件`、`/stop`、`/compact`、`/workspace /Users/alice/projects/my-app`、`/sessionlist 2`、`/sessionlist /Users/alice/projects/my-app` 或 `/session session-id`
Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/` 开头的消息。此时请加一个前导空格发送,例如 ` /presetlist` 或 ` /preset 2`;插件命令层会去除首尾空白,执行效果与无空格命令相同。 Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/` 开头的消息。此时请加一个前导空格发送,例如 ` /presetlist` 或 ` /preset 2`;插件命令层会去除首尾空白,执行效果与无空格命令相同。
@ -174,6 +175,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
- `/help` 不需要参数,也不会创建会话;它会返回当前机器人支持的完整命令列表。 - `/help` 不需要参数,也不会创建会话;它会返回当前机器人支持的完整命令列表。
- `/status` 不需要参数,也不会向模型发送消息或改变会话绑定;它用于确认当前机器人能够连接 DeepSeek Harness。 - `/status` 不需要参数,也不会向模型发送消息或改变会话绑定;它用于确认当前机器人能够连接 DeepSeek Harness。
- `/version` 不需要参数,也不会访问 Harness、创建会话或调用模型;它返回当前运行的 dsh-im 插件版本。
- `/new` 只解除当前聊天在 dsh-im 中保存的会话绑定,不会删除、清空或归档旧 Session。下一条普通消息会在当前工作区创建并绑定一个新 Session。任务正在运行或等待问题、审批时,应先完成交互或使用 `/stop`,再使用 `/new`。 - `/new` 只解除当前聊天在 dsh-im 中保存的会话绑定,不会删除、清空或归档旧 Session。下一条普通消息会在当前工作区创建并绑定一个新 Session。任务正在运行或等待问题、审批时,应先完成交互或使用 `/stop`,再使用 `/new`。
- `/models` 不需要参数,也不会创建会话。它为 Harness 当前配置的全部可用模型分配序号,同时显示可稳定复制的 `Provider/模型ID`;某个 Provider 查询失败时,其他 Provider 的结果仍会显示。 - `/models` 不需要参数,也不会创建会话。它为 Harness 当前配置的全部可用模型分配序号,同时显示可稳定复制的 `Provider/模型ID`;某个 Provider 查询失败时,其他 Provider 的结果仍会显示。
- `/model` 不带参数时查看当前会话的模型和推理等级;带参数时接受 `/models` 列出的序号或精确完整模型 ID,并可追加目标模型元数据公布的精确推理等级 ID,例如 `/model 2 max`。省略推理等级时,由 Harness 解析目标模型的当前默认值。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。 - `/model` 不带参数时查看当前会话的模型和推理等级;带参数时接受 `/models` 列出的序号或精确完整模型 ID,并可追加目标模型元数据公布的精确推理等级 ID,例如 `/model 2 max`。省略推理等级时,由 Harness 解析目标模型的当前默认值。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。

File diff suppressed because one or more lines are too long

View file

@ -77,6 +77,7 @@ const HELP_TEXT_LINES = [
'/send 提交当前批次', '/send 提交当前批次',
'/cancel 取消当前批次', '/cancel 取消当前批次',
'/status 检查连接状态', '/status 检查连接状态',
'/version 查看插件版本',
'/help 显示本帮助', '/help 显示本帮助',
]; ];

View file

@ -495,6 +495,7 @@ export function menuHelpText() {
'', '',
'📊 状态 / 压缩', '📊 状态 / 压缩',
'/status 连接状态', '/status 连接状态',
'/version 查看插件版本',
'/compact 压缩当前会话上下文', '/compact 压缩当前会话上下文',
'/archived on/off 会话列表显示/隐藏归档', '/archived on/off 会话列表显示/隐藏归档',
'', '',
@ -589,7 +590,10 @@ export function helpCard(extraTextLines = []) {
const elements = [ const elements = [
{ tag: 'div', text: markdown(t(HELP_CARD_FEATURES)) }, { tag: 'div', text: markdown(t(HELP_CARD_FEATURES)) },
{ tag: 'hr' }, { tag: 'hr' },
{ tag: 'div', text: markdown(t(HELP_TEXT_COMMANDS) + extraText) }, { tag: 'div', text: markdown([
t(HELP_TEXT_COMMANDS),
t('`/version` — 查看插件版本'),
].join('\n') + extraText) },
{ tag: 'hr' }, { tag: 'hr' },
{ tag: 'div', text: markdown(t(HELP_NUMBER_FALLBACK)) }, { tag: 'div', text: markdown(t(HELP_NUMBER_FALLBACK)) },
{ tag: 'hr' }, { tag: 'hr' },

View file

@ -90,6 +90,7 @@ function helpText() {
t('/send 提交当前批次'), t('/send 提交当前批次'),
t('/cancel 取消当前批次'), t('/cancel 取消当前批次'),
t('/status 检查连接状态'), t('/status 检查连接状态'),
t('/version 查看插件版本'),
t('/help 显示本帮助'), t('/help 显示本帮助'),
].join('\n'); ].join('\n');
} }

View file

@ -1,8 +1,11 @@
import { t } from './i18n.mjs'; import { t } from './i18n.mjs';
import manifest from '../../../package.json' with { type: 'json' };
const CONTROL_COMMAND = /^\/(?:stop|steer)(?=$|\s)/iu; const CONTROL_COMMAND = /^\/(?:stop|steer|version)(?=$|\s)/iu;
const STOP_COMMAND = /^\/stop(?=$|\s)/iu; const STOP_COMMAND = /^\/stop(?=$|\s)/iu;
const VERSION_COMMAND = /^\/version(?=$|\s)/iu;
const STOP_USAGE = '用法:/stop(不带参数)'; const STOP_USAGE = '用法:/stop(不带参数)';
const VERSION_USAGE = '用法:/version(不带参数)';
const STEER_USAGE = '用法:/steer <补充指令>'; const STEER_USAGE = '用法:/steer <补充指令>';
const TEXT_ONLY = '控制命令仅支持纯文字,请移除图片后重试。'; const TEXT_ONLY = '控制命令仅支持纯文字,请移除图片后重试。';
@ -41,9 +44,16 @@ export async function runControlCommand(text, harness, state, key, {
if (!isControlCommand(text)) return null; if (!isControlCommand(text)) return null;
const command = text.trim(); const command = text.trim();
const stop = STOP_COMMAND.test(command); const stop = STOP_COMMAND.test(command);
const version = VERSION_COMMAND.test(command);
if (hasImages) return commandResult(t(TEXT_ONLY)); if (hasImages) return commandResult(t(TEXT_ONLY));
if (version) {
return /^\/version$/iu.test(command)
? commandResult(`dsh-im v${manifest.version}`)
: commandResult(t(VERSION_USAGE));
}
if (stop) { if (stop) {
if (!/^\/stop$/iu.test(command)) return commandResult(t(STOP_USAGE)); if (!/^\/stop$/iu.test(command)) return commandResult(t(STOP_USAGE));
const session = boundSession(harness, state, key); const session = boundSession(harness, state, key);

View file

@ -229,6 +229,7 @@ export default {
'/new 开启全新会话': '/new Start a new session', '/new 开启全新会话': '/new Start a new session',
'📊 状态 / 压缩': '📊 Status / compact', '📊 状态 / 压缩': '📊 Status / compact',
'/status 连接状态': '/status Connection status', '/status 连接状态': '/status Connection status',
'`/version` — 查看插件版本': '`/version` — show the plugin version',
'/compact 压缩当前会话上下文': '/compact Compact the current session context', '/compact 压缩当前会话上下文': '/compact Compact the current session context',
'/archived on/off 会话列表显示/隐藏归档': '/archived on/off Show/hide archived sessions', '/archived on/off 会话列表显示/隐藏归档': '/archived on/off Show/hide archived sessions',
'👁 关注': '👁 Watches', '👁 关注': '👁 Watches',

View file

@ -69,6 +69,7 @@ export default {
'/stop 停止当前任务': '/stop Stop the current task', '/stop 停止当前任务': '/stop Stop the current task',
'/steer 补充指令 纠偏当前任务': '/steer <additional instruction> Steer the current task', '/steer 补充指令 纠偏当前任务': '/steer <additional instruction> Steer the current task',
'/status 检查连接状态': '/status Check the connection status', '/status 检查连接状态': '/status Check the connection status',
'/version 查看插件版本': '/version Show the plugin version',
'/help 显示本帮助': '/help Show this help', '/help 显示本帮助': '/help Show this help',
'{label}机器人与 DeepSeek Harness 连接正常。': '{label}机器人与 DeepSeek Harness 连接正常。':
'The {label} bot is connected to DeepSeek Harness and working normally.', 'The {label} bot is connected to DeepSeek Harness and working normally.',

View file

@ -242,6 +242,7 @@ export default {
// control-command.mjs // control-command.mjs
'用法:/stop(不带参数)': 'Usage: /stop (no arguments)', '用法:/stop(不带参数)': 'Usage: /stop (no arguments)',
'用法:/version(不带参数)': 'Usage: /version (no arguments)',
'用法:/steer <补充指令>': 'Usage: /steer <additional instruction>', '用法:/steer <补充指令>': 'Usage: /steer <additional instruction>',
'控制命令仅支持纯文字,请移除图片后重试。': '控制命令仅支持纯文字,请移除图片后重试。':
'Control commands support text only; please remove images and try again.', 'Control commands support text only; please remove images and try again.',

View file

@ -13,6 +13,7 @@ export default {
'停止当前任务': 'Stop the current task', '停止当前任务': 'Stop the current task',
'纠偏当前任务': 'Steer the current task', '纠偏当前任务': 'Steer the current task',
'检查连接状态': 'Check connection status', '检查连接状态': 'Check connection status',
'查看插件版本': 'Show plugin version',
'显示帮助': 'Show help', '显示帮助': 'Show help',
'该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。': '该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。':
'This Telegram bot already has a Webhook configured. Remove the Webhook from the original service and try again.', 'This Telegram bot already has a Webhook configured. Remove the Webhook from the original service and try again.',

View file

@ -494,6 +494,7 @@ export class TextHarnessBridge {
t('/send 提交当前批次'), t('/send 提交当前批次'),
t('/cancel 取消当前批次'), t('/cancel 取消当前批次'),
t('/status 检查连接状态'), t('/status 检查连接状态'),
t('/version 查看插件版本'),
t('/help 显示本帮助'), t('/help 显示本帮助'),
].join('\n')); ].join('\n'));
return; return;

View file

@ -32,6 +32,7 @@ export const TELEGRAM_COMMAND_MENU = Object.freeze([
{ command: 'send', description: '提交当前批次' }, { command: 'send', description: '提交当前批次' },
{ command: 'cancel', description: '取消当前批次' }, { command: 'cancel', description: '取消当前批次' },
{ command: 'status', description: '检查连接状态' }, { command: 'status', description: '检查连接状态' },
{ command: 'version', description: '查看插件版本' },
{ command: 'help', description: '显示帮助' }, { command: 'help', description: '显示帮助' },
]); ]);

View file

@ -73,6 +73,7 @@ function helpText() {
t('/send 提交当前批次'), t('/send 提交当前批次'),
t('/cancel 取消当前批次'), t('/cancel 取消当前批次'),
t('/status 检查连接状态'), t('/status 检查连接状态'),
t('/version 查看插件版本'),
t('/help 显示本帮助'), t('/help 显示本帮助'),
].join('\n'); ].join('\n');
} }

View file

@ -79,6 +79,7 @@ const HELP_TEXT = () => [
t('/send 提交当前批次'), t('/send 提交当前批次'),
t('/cancel 取消当前批次'), t('/cancel 取消当前批次'),
t('/status 检查连接状态'), t('/status 检查连接状态'),
t('/version 查看插件版本'),
t('/help 显示本帮助'), t('/help 显示本帮助'),
].join('\n'); ].join('\n');

View file

@ -841,6 +841,7 @@ test('DingTalk lists models and presets without prompting and advertises fast co
for (const command of [ for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning', '/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer', '/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
]) { ]) {
assert.equal(help.includes(command), true, command); assert.equal(help.includes(command), true, command);
} }

View file

@ -390,7 +390,7 @@ test('Feishu lists models and presets without prompting and advertises fast comm
for (const command of [ for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning', '/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/batch', '/send', '/cancel', '/presetlist', '/preset', '/preset --default', '/batch', '/send', '/cancel',
'/stop', '/steer', '/stop', '/steer', '/version',
]) { ]) {
assert.equal(help.includes(command), true, command); assert.equal(help.includes(command), true, command);
} }

View file

@ -76,6 +76,7 @@ test('menu and card help advertise Agent Preset, reasoning, and batch commands',
assert.match(help, /\/batch/); assert.match(help, /\/batch/);
assert.match(help, /\/send/); assert.match(help, /\/send/);
assert.match(help, /\/cancel/); assert.match(help, /\/cancel/);
assert.match(help, /\/version/);
const card = helpCard(); const card = helpCard();
assert.match(card, /\/reasoninglist/); assert.match(card, /\/reasoninglist/);

View file

@ -673,6 +673,7 @@ test('QQ lists models and presets without prompting and advertises fast commands
for (const command of [ for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning', '/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer', '/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
]) { ]) {
assert.equal(help.includes(command), true, command); assert.equal(help.includes(command), true, command);
} }

View file

@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import test from 'node:test'; import test from 'node:test';
import manifest from '../../../package.json' with { type: 'json' };
import { DiscordHarnessBridge } from '../../../src/channels/discord/discord-bridge.mjs'; import { DiscordHarnessBridge } from '../../../src/channels/discord/discord-bridge.mjs';
import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs'; import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
@ -794,7 +795,7 @@ test('all four shared text channels list models and presets locally and advertis
const help = sent.at(-1); const help = sent.at(-1);
for (const command of [ for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning', '/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer', '/presetlist', '/preset', '/preset --default', '/stop', '/steer', '/version',
]) { ]) {
assert.match(help, new RegExp(`\\${command}`), `${name} ${command}`); assert.match(help, new RegExp(`\\${command}`), `${name} ${command}`);
} }
@ -805,6 +806,21 @@ test('all four shared text channels list models and presets locally and advertis
} }
}); });
test('/version uses the shared command fast lane without accessing Harness', async () => {
const fixture = stateFixture();
const sent = [];
const bridge = createBridge({
state: fixture.state,
bot: { sendText: async (_target, text) => sent.push(text) },
harness: {},
});
await bridge.accept(message('plugin-version', '/version'));
assert.deepEqual(sent, [`dsh-im v${manifest.version}`]);
assert.equal(fixture.sessions.size, 0);
});
test('/stop uses the shared command fast lane without waiting for the running prompt', async () => { test('/stop uses the shared command fast lane without waiting for the running prompt', async () => {
const fixture = stateFixture({ 'direct:chat-a': 'session-running' }); const fixture = stateFixture({ 'direct:chat-a': 'session-running' });
const askStarted = deferred(); const askStarted = deferred();

View file

@ -251,6 +251,7 @@ test('Telegram API registers the command menu and commands-type menu button', as
test('Telegram command menu follows the host language at runtime', () => { test('Telegram command menu follows the host language at runtime', () => {
assert.equal(TELEGRAM_COMMAND_MENU[0].description, '开启一个全新会话'); assert.equal(TELEGRAM_COMMAND_MENU[0].description, '开启一个全新会话');
assert.equal(TELEGRAM_COMMAND_MENU.some(({ command }) => command === 'version'), true);
setImHostLanguage('en'); setImHostLanguage('en');
try { try {
assert.equal(telegramCommandMenu()[0].description, 'Start a brand-new Session'); assert.equal(telegramCommandMenu()[0].description, 'Start a brand-new Session');

View file

@ -462,6 +462,7 @@ test('Enterprise WeChat lists models and presets without prompting and advertise
for (const command of [ for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning', '/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer', '/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
'/batch', '/send', '/cancel', '/batch', '/send', '/cancel',
]) { ]) {
assert.equal(help.includes(command), true, command); assert.equal(help.includes(command), true, command);

View file

@ -778,6 +778,7 @@ test('Weixin lists models and presets without prompting and advertises fast comm
for (const command of [ for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning', '/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer', '/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
'/batch', '/send', '/cancel', '/batch', '/send', '/cancel',
]) { ]) {
assert.equal(help.includes(command), true, command); assert.equal(help.includes(command), true, command);

View file

@ -36,14 +36,31 @@ function fixture({ sessionId = 'session-one', stopped = true, steered = true } =
test('isControlCommand reserves valid and malformed control command forms', () => { test('isControlCommand reserves valid and malformed control command forms', () => {
for (const value of [ for (const value of [
'/stop', ' /STOP ', '/stop now', '/steer', '/StEeR do this', '/steer line one\nline two', '/stop', ' /STOP ', '/stop now', '/steer', '/StEeR do this', '/steer line one\nline two',
'/version', ' /VERSION ', '/version now',
]) { ]) {
assert.equal(isControlCommand(value), true, value); assert.equal(isControlCommand(value), true, value);
} }
for (const value of [null, '', 'stop', '/stopping', '/steering', 'hello /stop']) { for (const value of [null, '', 'stop', '/stopping', '/steering', '/versions', 'hello /stop']) {
assert.equal(isControlCommand(value), false, String(value)); assert.equal(isControlCommand(value), false, String(value));
} }
}); });
test('/version returns the package version without touching Harness or Session state', async () => {
const { calls, harness, state } = fixture();
const { version } = await import('../package.json', { with: { type: 'json' } })
.then((module) => module.default);
assert.deepEqual(
await runControlCommand('/VERSION', harness, state, 'direct:one'),
{ message: `dsh-im v${version}` },
);
assert.match(
(await runControlCommand('/version details', harness, state, 'direct:one')).message,
/用法/,
);
assert.equal(calls.length, 0);
});
test('/stop is exact, text-only, and never creates a Session', async () => { test('/stop is exact, text-only, and never creates a Session', async () => {
const { calls, harness, state } = fixture(); const { calls, harness, state } = fixture();
assert.match((await runControlCommand('/stop later', harness, state, 'direct:one')).message, /用法/); assert.match((await runControlCommand('/stop later', harness, state, 'direct:one')).message, /用法/);