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

@ -841,6 +841,7 @@ test('DingTalk lists models and presets without prompting and advertises fast co
for (const command of [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
]) {
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 [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/batch', '/send', '/cancel',
'/stop', '/steer',
'/stop', '/steer', '/version',
]) {
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, /\/send/);
assert.match(help, /\/cancel/);
assert.match(help, /\/version/);
const card = helpCard();
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 [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
]) {
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 { join } from 'node:path';
import test from 'node:test';
import manifest from '../../../package.json' with { type: 'json' };
import { DiscordHarnessBridge } from '../../../src/channels/discord/discord-bridge.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);
for (const command of [
'/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}`);
}
@ -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 () => {
const fixture = stateFixture({ 'direct:chat-a': 'session-running' });
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', () => {
assert.equal(TELEGRAM_COMMAND_MENU[0].description, '开启一个全新会话');
assert.equal(TELEGRAM_COMMAND_MENU.some(({ command }) => command === 'version'), true);
setImHostLanguage('en');
try {
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 [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
'/batch', '/send', '/cancel',
]) {
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 [
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
'/version',
'/batch', '/send', '/cancel',
]) {
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', () => {
for (const value of [
'/stop', ' /STOP ', '/stop now', '/steer', '/StEeR do this', '/steer line one\nline two',
'/version', ' /VERSION ', '/version now',
]) {
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));
}
});
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 () => {
const { calls, harness, state } = fixture();
assert.match((await runControlCommand('/stop later', harness, state, 'direct:one')).message, /用法/);