mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat: service discovery with scene merge and internalized pagination
This commit is contained in:
@@ -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<ServiceList> {
|
||||
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<ServiceListResponse>(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 }
|
||||
}
|
||||
@@ -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<string, ServiceListResponse>) {
|
||||
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<string, unknown>
|
||||
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<string, unknown>).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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user