feat: implement local deep search engine

This commit is contained in:
john
2026-07-23 22:27:07 +08:00
parent 4e36b98c9e
commit be1e4c18c0
12 changed files with 1717 additions and 16 deletions
+48 -3
View File
@@ -9,6 +9,8 @@ import {
} from './mindsearch-config.mjs';
import { buildAgentExtensionPolicy, DEFAULT_USER_CAPABILITIES } from './capabilities.mjs';
import {
cancelResearchTask,
getResearchTask,
searchGithubCode,
searchResearchService,
searchSearxng,
@@ -22,7 +24,8 @@ test('MindSearch defaults to disabled and normalizes unsafe modes', () => {
assert.equal(config.mode, 'off');
assert.deepEqual(config.providers, { searxng: false, github: true, reader: false });
assert.equal(config.services.find((service) => service.id === 'github').enabled, true);
assert.deepEqual(config.routes, { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: '' });
assert.deepEqual(config.routes, { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: 'deep-search' });
assert.equal(config.services.find((service) => service.id === 'deep-search').enabled, false);
});
test('MindSearch service registry accepts research orchestrators and rejects unsafe endpoints', () => {
@@ -89,6 +92,20 @@ test('MindSearch never changes legacy web extension and is gated by capability/c
assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'tkmind-search'), false);
policy = buildAgentExtensionPolicy({ ...base, search_external: true }, { mindSearchConfig: { enabled: true, mode: 'assist', providers: {} } });
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'tkmind-search'));
policy = buildAgentExtensionPolicy({ ...base, search_external: true }, {
userId: 'user-123',
mindSearchConfig: {
enabled: true,
mode: 'assist',
providers: {},
services: [{ id: 'deep-search', enabled: true, adapter: 'research-http' }],
routes: { research: 'deep-search' },
},
});
const extension = policy.extensionOverrides.find((ext) => ext.name === 'tkmind-search');
assert.equal(extension.envs.TKMIND_SEARCH_USER_ID, 'user-123');
assert.ok(extension.available_tools.includes('tkmind_research_status'));
assert.ok(extension.available_tools.includes('tkmind_research_cancel'));
});
test('SearXNG adapter normalizes provider results without requiring a live network', async () => {
@@ -111,12 +128,40 @@ test('Research service adapter uses the stable HTTP contract', async () => {
if (String(url).endsWith('/v1/research')) {
return { ok: true, json: async () => ({ task_id: 'task-1', status: 'running' }) };
}
if (String(url).endsWith('/v1/research/task-1')) {
return {
ok: true,
status: 200,
json: async () => ({
id: 'task-1',
status: options?.method === 'DELETE' ? 'cancelling' : 'researching',
progress: 55,
}),
};
}
return { ok: true, json: async () => ({ results: [{ title: 'Report', url: 'https://example.com/report', snippet: 'evidence' }] }) };
};
const results = await searchResearchService('AI Agent', { endpoint: 'http://research.local', limit: 5, fetchImpl });
const task = await startResearchTask('Analyze AI Agent', { endpoint: 'http://research.local', depth: 'deep', fetchImpl });
const task = await startResearchTask('Analyze AI Agent', {
endpoint: 'http://research.local',
depth: 'deep',
userId: 'user-1',
secret: 'secret-1',
fetchImpl,
});
const status = await getResearchTask('task-1', { endpoint: 'http://research.local', fetchImpl });
const cancelled = await cancelResearchTask('task-1', { endpoint: 'http://research.local', fetchImpl });
assert.equal(results[0].source, 'research-service');
assert.deepEqual(task, { taskId: 'task-1', status: 'running', service: 'research-service' });
assert.equal(status.status, 'researching');
assert.equal(cancelled.status, 'cancelling');
assert.equal(JSON.parse(requests[0].options.body).query, 'AI Agent');
assert.deepEqual(JSON.parse(requests[1].options.body), { question: 'Analyze AI Agent', depth: 'deep' });
assert.deepEqual(JSON.parse(requests[1].options.body), {
question: 'Analyze AI Agent',
depth: 'deep',
userId: 'user-1',
});
assert.equal(requests[1].options.headers['x-user-id'], 'user-1');
assert.equal(requests[1].options.headers['x-secret-key'], 'secret-1');
assert.equal(requests[3].options.method, 'DELETE');
});