From 5800222ea4b7cdea24e82f50fa332dbd08b6dd07 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:37:34 +0800 Subject: [PATCH] feat: service discovery with scene merge and internalized pagination --- packages/tool-bailian-kb/src/services.ts | 59 +++++++++++++++++++ .../tool-bailian-kb/tests/services.test.ts | 49 +++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 packages/tool-bailian-kb/src/services.ts create mode 100644 packages/tool-bailian-kb/tests/services.test.ts diff --git a/packages/tool-bailian-kb/src/services.ts b/packages/tool-bailian-kb/src/services.ts new file mode 100644 index 0000000..9d37dd4 --- /dev/null +++ b/packages/tool-bailian-kb/src/services.ts @@ -0,0 +1,59 @@ +/** Retrieval-service discovery: per-scene queries merged into one model-facing list; pagination stays internal. */ + +import type { ServiceListResponse } from './api-types.js' +import type { KbClient } from './client.js' +import { KB_PATHS } from './endpoints.js' + +/** Server page-size maximum; one page per scene covers ordinary workspaces. */ +const MAX_PAGE_SIZE = 100 + +export interface ServiceEntry { + agent_id: string + name: string + scene: string + status: string + knowledge_bases: string[] +} + +export interface ServiceList { + services: ServiceEntry[] + total: number + /** True when some scene reported more rows than one max page returned. */ + truncated: boolean +} + +export interface ListServicesQuery { + scene?: 'chat' | 'search' + nameFilter?: string +} + +/** + * List retrieval/Q&A services. An omitted scene fans out to both scenes and merges. + * @param client - the shared knowledge API client. + * @param query - optional scene and fuzzy name filter. + * @returns merged entries, the server-reported total, and the truncation flag. + */ +export async function listServices(client: KbClient, query: ListServicesQuery): Promise { + const scenes: ('chat' | 'search')[] = query.scene ? [query.scene] : ['chat', 'search'] + const services: ServiceEntry[] = [] + let total = 0 + for (const scene of scenes) { + const res = await client.postJson(KB_PATHS.serviceList, { + agent_scene: scene, + ...(query.nameFilter ? { agent_name: query.nameFilter } : {}), + page_number: 1, + page_size: MAX_PAGE_SIZE, + }) + total += res.data?.total_count ?? 0 + for (const row of res.data?.rows ?? []) { + services.push({ + agent_id: row.agent_id ?? '', + name: row.agent_name ?? '', + scene: row.agent_scene ?? scene, + status: row.agent_status ?? '', + knowledge_bases: (row.pipeline_list ?? []).map(p => p.pipeline_name ?? p.pipeline_id ?? '').filter(Boolean), + }) + } + } + return { services, total, truncated: total > services.length } +} diff --git a/packages/tool-bailian-kb/tests/services.test.ts b/packages/tool-bailian-kb/tests/services.test.ts new file mode 100644 index 0000000..f5853cc --- /dev/null +++ b/packages/tool-bailian-kb/tests/services.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest' +import type { ServiceListResponse } from '../src/api-types.js' +import type { KbClient } from '../src/client.js' +import { listServices } from '../src/services.js' + +function fakeClient(byScene: Record) { + const postJson = vi.fn(async (_path: string, body: { agent_scene: string }) => byScene[body.agent_scene]) + return { client: { postJson } as unknown as KbClient, postJson } +} + +const row = (id: string, scene: string) => ({ + agent_id: id, agent_name: `svc-${id}`, agent_scene: scene, agent_status: 'deployed', + pipeline_list: [{ pipeline_id: 'p1', pipeline_name: 'kb-one' }], +}) + +describe('listServices', () => { + it('queries both scenes when scene is omitted and merges rows with scene tags', async () => { + const { client, postJson } = fakeClient({ + chat: { data: { total_count: 1, rows: [row('a', 'chat')] } }, + search: { data: { total_count: 1, rows: [row('b', 'search')] } }, + }) + const out = await listServices(client, {}) + expect(postJson).toHaveBeenCalledTimes(2) + expect(out.services.map(s => [s.agent_id, s.scene])).toEqual([['a', 'chat'], ['b', 'search']]) + expect(out.services[0]!.knowledge_bases).toEqual(['kb-one']) + expect(out.total).toBe(2) + expect(out.truncated).toBe(false) + const body = postJson.mock.calls[0]![1] as unknown as Record + expect(body.page_number).toBe(1) + expect(body.page_size).toBe(100) + }) + + it('queries one scene and forwards the name filter', async () => { + const { client, postJson } = fakeClient({ search: { data: { total_count: 0, rows: [] } } }) + await listServices(client, { scene: 'search', nameFilter: '客服' }) + expect(postJson).toHaveBeenCalledTimes(1) + expect((postJson.mock.calls[0]![1] as unknown as Record).agent_name).toBe('客服') + }) + + it('flags truncation when a scene exceeds one max page', async () => { + const { client } = fakeClient({ + chat: { data: { total_count: 250, rows: [row('a', 'chat')] } }, + search: { data: { total_count: 0, rows: [] } }, + }) + const out = await listServices(client, {}) + expect(out.truncated).toBe(true) + expect(out.total).toBe(250) + }) +})