Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,14 @@ import test from 'node:test';
|
||||
import {
|
||||
createLlmProviderService,
|
||||
CUSTOM_PROVIDER_ID,
|
||||
buildExecutorLaunchPlan,
|
||||
decryptSecret,
|
||||
encryptSecret,
|
||||
LLM_PROVIDER_CATALOG,
|
||||
LOCAL_LLM_FALLBACK,
|
||||
maskApiKey,
|
||||
normalizeApiUrl,
|
||||
resolveApiBaseUrl,
|
||||
parseModelList,
|
||||
resolveChatCompletionsUrl,
|
||||
testLocalLlmConnection,
|
||||
@@ -248,6 +250,27 @@ test('catalog includes custom provider template', () => {
|
||||
assert.equal(custom.kind, 'custom');
|
||||
});
|
||||
|
||||
test('listExecutorBindings returns all executor placeholders', async () => {
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
if (sql.includes('ORDER BY is_selected DESC')) return [[]];
|
||||
if (sql.includes('FROM h5_llm_executor_bindings')) return [[]];
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
});
|
||||
const bindings = await service.listExecutorBindings();
|
||||
assert.deepEqual(
|
||||
bindings.map((binding) => binding.executor),
|
||||
['goose', 'aider', 'openhands'],
|
||||
);
|
||||
assert.equal(bindings[0].enabled, false);
|
||||
});
|
||||
|
||||
test('resolveChatCompletionsUrl keeps full completions path', () => {
|
||||
assert.equal(
|
||||
resolveChatCompletionsUrl('http://127.0.0.1:18300/relay/buyer/v1/chat/completions'),
|
||||
@@ -255,6 +278,261 @@ test('resolveChatCompletionsUrl keeps full completions path', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveApiBaseUrl strips chat completions suffix', () => {
|
||||
assert.equal(
|
||||
resolveApiBaseUrl('http://127.0.0.1:18300/relay/buyer/v1/chat/completions'),
|
||||
'http://127.0.0.1:18300/relay/buyer/v1',
|
||||
);
|
||||
});
|
||||
|
||||
test('testKey checks builtin DeepSeek through OpenAI compatible endpoint', async () => {
|
||||
const encrypted = encryptSecret('sk-deepseek-test', 'unit-test-secret');
|
||||
const row = {
|
||||
id: 'key-deepseek',
|
||||
provider_id: 'custom_deepseek',
|
||||
provider_kind: 'builtin',
|
||||
name: 'DeepSeek',
|
||||
api_key_ciphertext: encrypted.ciphertext,
|
||||
api_key_iv: encrypted.iv,
|
||||
api_key_tag: encrypted.tag,
|
||||
default_model: 'deepseek-chat',
|
||||
status: 'active',
|
||||
is_selected: 1,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
const pool = {
|
||||
async query(sql, params = []) {
|
||||
if (sql.includes('SELECT * FROM h5_llm_provider_keys WHERE id = ?')) {
|
||||
assert.equal(params[0], 'key-deepseek');
|
||||
return [[row]];
|
||||
}
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
};
|
||||
const calls = [];
|
||||
const mockFetch = async (url, init) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(init.body) });
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ choices: [{ message: { content: 'ok' } }] }),
|
||||
};
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
apiFetchImpl: mockFetch,
|
||||
});
|
||||
const result = await service.testKey('key-deepseek', 'deepseek-chat');
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(calls[0].url, 'https://api.deepseek.com/v1/chat/completions');
|
||||
assert.equal(calls[0].init.headers.Authorization, 'Bearer sk-deepseek-test');
|
||||
assert.equal(calls[0].body.model, 'deepseek-chat');
|
||||
});
|
||||
|
||||
test('getExecutorRuntimeConfig resolves aider env from binding', async () => {
|
||||
const encrypted = encryptSecret('sk-deepseek-test', 'unit-test-secret');
|
||||
const keyRow = {
|
||||
id: 'key-deepseek',
|
||||
provider_id: 'custom_deepseek',
|
||||
provider_kind: 'builtin',
|
||||
name: 'DeepSeek 主账号',
|
||||
api_key_ciphertext: encrypted.ciphertext,
|
||||
api_key_iv: encrypted.iv,
|
||||
api_key_tag: encrypted.tag,
|
||||
default_model: 'deepseek-chat',
|
||||
status: 'active',
|
||||
is_selected: 1,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
const bindingRow = {
|
||||
id: 'binding-aider',
|
||||
executor: 'aider',
|
||||
purpose: 'default',
|
||||
provider_key_id: 'key-deepseek',
|
||||
model: 'deepseek-reasoner',
|
||||
enabled: 1,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
const pool = {
|
||||
async query(sql, params = []) {
|
||||
if (sql.includes('SELECT * FROM h5_llm_executor_bindings')) return [[bindingRow]];
|
||||
if (sql.includes('SELECT * FROM h5_llm_provider_keys WHERE id = ?')) {
|
||||
assert.equal(params[0], 'key-deepseek');
|
||||
return [[keyRow]];
|
||||
}
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
});
|
||||
const runtime = await service.getExecutorRuntimeConfig('aider');
|
||||
assert.equal(runtime.ok, true);
|
||||
assert.equal(runtime.model, 'deepseek-reasoner');
|
||||
assert.equal(runtime.env.AIDER_MODEL, 'deepseek-reasoner');
|
||||
assert.equal(runtime.env.DEEPSEEK_API_KEY, '[hidden]');
|
||||
});
|
||||
|
||||
test('buildExecutorLaunchPlan creates openhands headless command', () => {
|
||||
const plan = buildExecutorLaunchPlan({
|
||||
ok: true,
|
||||
executor: 'openhands',
|
||||
executorLabel: 'OpenHands',
|
||||
purpose: 'default',
|
||||
providerName: 'DeepSeek 主账号',
|
||||
model: 'deepseek-reasoner',
|
||||
env: { LLM_MODEL: 'deepseek-reasoner', LLM_API_KEY: '[hidden]' },
|
||||
}, {
|
||||
mode: 'headless',
|
||||
cwd: '/repo/memind',
|
||||
instruction: 'add search',
|
||||
});
|
||||
assert.equal(plan.ok, true);
|
||||
assert.equal(plan.command, 'openhands');
|
||||
assert.deepEqual(plan.args.slice(0, 3), ['--headless', '--json', '--override-with-envs']);
|
||||
assert.equal(plan.cwd, '/repo/memind');
|
||||
assert.ok(plan.args.includes('--task'));
|
||||
});
|
||||
|
||||
test('buildExecutorLaunchPlan creates openhands serve command', () => {
|
||||
const plan = buildExecutorLaunchPlan({
|
||||
ok: true,
|
||||
executor: 'openhands',
|
||||
executorLabel: 'OpenHands',
|
||||
purpose: 'default',
|
||||
providerName: 'DeepSeek 主账号',
|
||||
model: 'deepseek-reasoner',
|
||||
env: { LLM_MODEL: 'deepseek-reasoner', LLM_API_KEY: '[hidden]' },
|
||||
}, {
|
||||
mode: 'serve',
|
||||
cwd: '/repo/memind',
|
||||
});
|
||||
assert.equal(plan.ok, true);
|
||||
assert.equal(plan.command, 'openhands');
|
||||
assert.deepEqual(plan.args, ['serve', '--mount-cwd']);
|
||||
});
|
||||
|
||||
test('buildExecutorLaunchPlan adds aider one-shot message when provided', () => {
|
||||
const plan = buildExecutorLaunchPlan({
|
||||
ok: true,
|
||||
executor: 'aider',
|
||||
executorLabel: 'Aider',
|
||||
purpose: 'default',
|
||||
providerName: 'DeepSeek 主账号',
|
||||
model: 'deepseek-chat',
|
||||
env: { AIDER_MODEL: 'deepseek-chat', OPENAI_API_KEY: '[hidden]' },
|
||||
}, {
|
||||
cwd: '/repo/memind',
|
||||
instruction: 'fix login button',
|
||||
});
|
||||
assert.equal(plan.ok, true);
|
||||
assert.equal(plan.command, 'aider');
|
||||
assert.ok(plan.args.includes('--message'));
|
||||
assert.ok(plan.args.includes('fix login button'));
|
||||
});
|
||||
|
||||
test('buildExecutorLaunchPlan rejects openhands headless without instruction', () => {
|
||||
const plan = buildExecutorLaunchPlan({
|
||||
ok: true,
|
||||
executor: 'openhands',
|
||||
executorLabel: 'OpenHands',
|
||||
purpose: 'default',
|
||||
providerName: 'DeepSeek 主账号',
|
||||
model: 'deepseek-reasoner',
|
||||
env: { LLM_MODEL: 'deepseek-reasoner', LLM_API_KEY: '[hidden]' },
|
||||
}, {
|
||||
mode: 'headless',
|
||||
cwd: '/repo/memind',
|
||||
});
|
||||
assert.equal(plan.ok, false);
|
||||
assert.match(plan.message ?? '', /instruction/);
|
||||
});
|
||||
|
||||
test('launchExecutor rejects goose launch path', async () => {
|
||||
const pool = {
|
||||
async query() {
|
||||
throw new Error('should not query for goose launch');
|
||||
},
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
});
|
||||
const result = await service.launchExecutor('goose');
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.message ?? '', /Goose/);
|
||||
});
|
||||
|
||||
test('launchExecutor rejects openhands headless without instruction', async () => {
|
||||
const pool = {
|
||||
async query() {
|
||||
throw new Error('should not query for validation failure');
|
||||
},
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
});
|
||||
const result = await service.launchExecutor('openhands', { mode: 'headless' });
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.message ?? '', /instruction/);
|
||||
});
|
||||
|
||||
test('launchExecutor returns friendly error when command is missing', async () => {
|
||||
const keyRow = {
|
||||
id: 'key-1',
|
||||
provider_id: 'custom_deepseek',
|
||||
provider_kind: 'builtin',
|
||||
name: 'DeepSeek',
|
||||
api_key_ciphertext: encryptSecret('sk-x', 'unit-test-secret').ciphertext,
|
||||
api_key_iv: encryptSecret('sk-x', 'unit-test-secret').iv,
|
||||
api_key_tag: encryptSecret('sk-x', 'unit-test-secret').tag,
|
||||
default_model: 'deepseek-chat',
|
||||
status: 'active',
|
||||
is_selected: 1,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
const bindingRow = {
|
||||
id: 'binding-1',
|
||||
executor: 'aider',
|
||||
purpose: 'default',
|
||||
provider_key_id: 'key-1',
|
||||
model: 'deepseek-chat',
|
||||
enabled: 1,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
if (sql.includes('SELECT * FROM h5_llm_executor_bindings')) return [[]];
|
||||
if (sql.includes('SELECT * FROM h5_llm_provider_keys WHERE id = ?')) {
|
||||
return [[keyRow]];
|
||||
}
|
||||
if (sql.includes('SELECT * FROM h5_llm_executor_bindings WHERE executor = ? AND purpose = ? LIMIT 1')) {
|
||||
return [[bindingRow]];
|
||||
}
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
});
|
||||
const result = await service.launchExecutor('aider', { instruction: 'hello' });
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.message ?? '', /aider .*PATH|aider 未安装/);
|
||||
});
|
||||
|
||||
test('testRelayConnection surfaces 401 as token error', async () => {
|
||||
const mockFetch = async () => ({
|
||||
ok: false,
|
||||
@@ -380,6 +658,7 @@ test('syncSelectedToGoosed does not auto-fallback to local when selected relay i
|
||||
};
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
if (sql.includes('SELECT * FROM h5_llm_executor_bindings')) return [[]];
|
||||
if (sql.includes('is_selected = 1')) return [[row]];
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
@@ -398,3 +677,47 @@ test('syncSelectedToGoosed does not auto-fallback to local when selected relay i
|
||||
assert.equal(result.synced, false);
|
||||
assert.ok(result.message);
|
||||
});
|
||||
|
||||
test('ensureBootstrapRelay keeps existing selected provider', async () => {
|
||||
const rows = [
|
||||
{
|
||||
id: 'key-deepseek',
|
||||
provider_id: 'custom_deepseek',
|
||||
provider_kind: 'builtin',
|
||||
name: 'DeepSeek',
|
||||
status: 'active',
|
||||
is_selected: 1,
|
||||
updated_at: 1,
|
||||
},
|
||||
{
|
||||
id: 'key-relay',
|
||||
provider_id: 'custom_relay_buyer_ollama_27',
|
||||
provider_kind: 'custom',
|
||||
name: 'Relay Buyer Ollama',
|
||||
status: 'active',
|
||||
is_selected: 0,
|
||||
updated_at: 1,
|
||||
},
|
||||
];
|
||||
const pool = {
|
||||
async query(sql, params = []) {
|
||||
if (sql.includes('SELECT id FROM h5_llm_provider_keys WHERE name = ?')) {
|
||||
return [[rows.find((row) => row.name === params[0])].filter(Boolean)];
|
||||
}
|
||||
if (sql.includes('UPDATE h5_llm_provider_keys SET is_selected')) {
|
||||
throw new Error('bootstrap relay should not change the selected provider');
|
||||
}
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
};
|
||||
const service = createLlmProviderService(pool, {
|
||||
apiTarget: 'https://127.0.0.1:18006',
|
||||
apiSecret: 'secret',
|
||||
encryptionKey: 'unit-test-secret',
|
||||
});
|
||||
const result = await service.ensureBootstrapRelay();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.created, false);
|
||||
assert.equal(rows.find((row) => row.id === 'key-deepseek').is_selected, 1);
|
||||
assert.equal(rows.find((row) => row.id === 'key-relay').is_selected, 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user