mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-12 02:33:19 +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
|
|
@ -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