refactor(tool-bailian-kb): 移除 kb_service_list 工具,统一服务发现方式

- 移除 kb_service_list 相关代码,包括接口定义、服务查询函数和工具定义
- 服务发现改为通过 kscli 命令行工具 `kscli service list` 查询
- 更新文档和注释,删除关于 kb_service_list 的描述和示例
- 删除对服务列表接口路径的引用,保留 kb_search 和 kb_chat 两个工具
- 调整错误处理逻辑,不再附加可用服务信息,错误直接透传
- 修改默认服务 id 提示文本,均改为引用 `kscli service list` 作为服务发现手段
- 移除相关测试内容,包括对 kb_service_list 注册和服务查询测试
- 保持其他功能和接口不变,确保兼容和功能完整性
This commit is contained in:
zeyu.fz
2026-08-17 15:07:38 +08:00
parent 8a09881626
commit cda1e326f3
12 changed files with 36 additions and 234 deletions
+4 -3
View File
@@ -1,6 +1,6 @@
# dsh-tool-bailian-kb
百炼知识库的 dsh 插件本体:在 `ctx.tools` 注册三个模型工具,并在 skills 服务可用时注册管理面 skill。
百炼知识库的 dsh 插件本体:在 `ctx.tools` 注册两个检索模型工具(kb_search、kb_chat),并在 skills 服务可用时注册管理面 skill。服务发现通过 kscli CLI 完成。
## Config
@@ -21,13 +21,14 @@ Config 同时注册为 `bailian-kb` settings namespace(`installSettingsSection
| 工具 | 参数 | 返回 |
|---|---|---|
| `kb_service_list` | `scene?`(chat\|search,省略查双场景合并)、`name_filter?` | 服务清单(agent_id、名称、scene、status、绑定知识库)+ total + truncated;分页内部消化(单 scene 100 条上限) |
| `kb_search` | `query`、`agent_id`(见 defaultRetrieveAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total |
| `kb_chat` | `message`、`agent_id`(见 defaultChatAgentId) | 完整答案(内部消费 SSE 流缓冲返回)+ request_id |
服务发现(`kb_service_list` 已移除):通过 `kscli service list` CLI 命令查询可用检索/对话服务及其 agent_id。
## 错误语义
- HTTP 错误(除 401/403 鉴权类):错误信息**附当前服务清单**,模型可一步纠正无效 `agent_id`(5xx 也附,但通常代表服务端异常);
- HTTP 错误:原始错误透传,模型可通过 `kscli service list` 发现可用服务以纠正无效 `agent_id`;
- 凭证缺失:指向 `~/.dsh/.env` / `.credentials.yaml` 配置方式与控制台取 key 页面;
- chat 超时:说明服务端多轮检索特性,建议重试或改用 `kb_search`;
- 服务端错误体截断至 500 字符进入错误信息(优先 `code: message`)。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-tool-bailian-kb",
"version": "0.1.0",
"description": "Bailian knowledge-base tools for DeepSeek Harness: kb_service_list, kb_search, kb_chat over the DashScope RAG API, plus the kscli management skill.",
"description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the kscli management skill.",
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -22,7 +22,7 @@ kscli doc status --kb-id <kb-id> --doc-id <doc-id> # 3. 轮
kscli service create ... && kscli service deploy ... # 4. 建/部署检索服务 → 得到 agent_id
```
部署完成后用 `kb_service_list` 确认服务可见,再用 `kb_search` 带该 `agent_id` 验证检索。
部署完成后用 `kscli service list` 确认服务可见,再用 `kb_search` 带该 `agent_id` 验证检索。
## 命令组速查
+1 -22
View File
@@ -1,25 +1,4 @@
/** Request/response fields of the three DashScope knowledge endpoints, mirrored from the verified kscli types. */
export interface ServiceListRequest {
agent_scene: 'chat' | 'search'
agent_name?: string
page_number: number
page_size: number
}
export interface ServiceListRow {
agent_id?: string
agent_name?: string
agent_scene?: string
agent_status?: string
pipeline_list?: { pipeline_id?: string; pipeline_name?: string }[]
}
export interface ServiceListResponse {
code?: string
message?: string
data?: { total_count?: number; rows?: ServiceListRow[] }
}
/** Request/response fields of the DashScope search and chat endpoints, mirrored from the verified kscli types. */
export interface SearchRequest {
query: string
+1 -2
View File
@@ -1,8 +1,7 @@
/** Protocol path constants and the workspace-subdomain URL builder (external API spec; not configurable). */
/** DashScope knowledge API paths, mirrored from the verified kscli endpoint table. */
/** DashScope knowledge API paths for the two native tools (search, chat); service listing is handled by kscli. */
export const KB_PATHS = {
serviceList: '/api/v1/indices/rag/app/list',
search: '/api/v1/indices/knowledge/search',
chat: '/api/v2/apps/knowledge/chat',
} as const
+2 -2
View File
@@ -1,6 +1,6 @@
/**
* Bailian knowledge-base consumer plugin: registers kb_service_list, kb_search,
* and kb_chat over the DashScope RAG API, plus the kscli management skill.
* Bailian knowledge-base consumer plugin: registers kb_search and kb_chat over the DashScope RAG API,
* plus the kscli management skill.
* @module dsh-tool-bailian-kb
*/
-59
View File
@@ -1,59 +0,0 @@
/** 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 }
}
+10 -78
View File
@@ -1,5 +1,5 @@
/**
* The three model-facing knowledge tools. agent_id stays optional in the schema
* The two model-facing knowledge tools (kb_search, kb_chat). agent_id stays optional in the schema
* regardless of deployment: the default services (patch config or credential)
* can change at runtime through the credentials domain, so the fallback runs
* per call and a missing default surfaces as an executable error instead of a
@@ -11,7 +11,6 @@ import type { SearchRequest, SearchResponse } from './api-types.js'
import { KbApiError, type KbClient } from './client.js'
import { consumeChatStream } from './chat.js'
import { KB_PATHS } from './endpoints.js'
import { listServices } from './services.js'
/** Client-side chunk cap applied when the model omits top_k. */
const DEFAULT_TOP_K = 5
@@ -26,32 +25,17 @@ export interface KbToolDeps {
chatTimeoutMs: number
}
/** Format one service list into the error-hint / result text form. */
function formatServices(services: { agent_id: string; name: string; scene: string; status: string }[]): string {
return services.map(s => `${s.agent_id} (${s.name}, scene=${s.scene}, ${s.status})`).join('; ')
}
/**
* Append the current service list to a client error so the model can correct
* an invalid agent_id in one step. Auth failures (401/403) keep their own message.
* @param client - the shared knowledge API client used for best-effort discovery.
* @param err - the failure being enriched; always rethrown.
* @returns never; the original or enriched error is thrown.
* Forward the original tool error unchanged; service discovery now lives
* in the kscli management skill (`kscli service list`), so the tool no
* longer makes a best-effort API round-trip to enrich the message.
*/
async function withServiceHint(client: KbClient, err: unknown): Promise<never> {
if (err instanceof KbApiError && err.status !== undefined && err.status >= 400 && err.status !== 401 && err.status !== 403) {
let hint: string | undefined
try {
const { services } = await listServices(client, {})
if (services.length > 0) hint = formatServices(services)
} catch { /* discovery is best-effort; the original error already carries the failure */ }
if (hint !== undefined) throw new KbApiError(`${err.message}. Available services: ${hint}`, err.status)
}
async function withServiceHint(_client: KbClient, err: unknown): Promise<never> {
throw err
}
/**
* Build the three tool definitions over one shared client.
* Build the two tool definitions over one shared client.
* @param deps - client plus the deployment's explicit pinning and timeout choices.
* @returns definitions ready for `ctx.tools.register()`.
*/
@@ -61,13 +45,13 @@ export function createKbTools(deps: KbToolDeps) {
const { client, resolveDefaultRetrieveAgentId, resolveDefaultChatAgentId } = deps
const agentIdParam = {
type: 'string' as const,
description: 'Retrieval/Q&A service id; omit to use the default service when this deployment configures one (find ids via kb_service_list).',
description: 'Retrieval/Q&A service id; omit to use the default service when this deployment configures one (find ids via `kscli service list`).',
}
const resolveRetrieveAgentId = async (supplied: string | undefined): Promise<string> => {
if (supplied !== undefined) return supplied
const defaultId = resolveDefaultRetrieveAgentId === undefined ? undefined : await resolveDefaultRetrieveAgentId()
if (defaultId === undefined) {
throw new Error('agent_id is required: no default retrieval service is configured; discover services with kb_service_list')
throw new Error('agent_id is required: no default retrieval service is configured; discover services with `kscli service list`')
}
return defaultId
}
@@ -75,63 +59,11 @@ export function createKbTools(deps: KbToolDeps) {
if (supplied !== undefined) return supplied
const defaultId = resolveDefaultChatAgentId === undefined ? undefined : await resolveDefaultChatAgentId()
if (defaultId === undefined) {
throw new Error('agent_id is required: no default chat service is configured; discover services with kb_service_list')
throw new Error('agent_id is required: no default chat service is configured; discover services with `kscli service list`')
}
return defaultId
}
const serviceList = defineTool({
name: 'kb_service_list',
description:
'List the Bailian knowledge retrieval/Q&A services available in this workspace. '
+ 'Each entry names the service id (agent_id) to pass to kb_search (scene=search) or kb_chat (scene=chat), '
+ 'its bound knowledge bases, and its status (prefer deployed). '
+ 'Omit scene to see both kinds; narrow large workspaces with name_filter.',
parameters: {
scene: { type: 'string', enum: ['chat', 'search'], description: 'Only list services for this scene; omitted lists both.' },
name_filter: { type: 'string', description: 'Fuzzy match on the service name.' },
},
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: {
services: {
type: 'array',
required: true,
items: {
type: 'object',
additionalProperties: false,
properties: {
agent_id: { type: 'string', required: true },
name: { type: 'string', required: true },
scene: { type: 'string', required: true },
status: { type: 'string', required: true },
knowledge_bases: { type: 'array', required: true, items: { type: 'string' } },
},
},
},
total: { type: 'integer', required: true },
truncated: { type: 'boolean', required: true },
},
},
render: (_args, value) => [{
type: 'text',
text: value.services.length === 0
? 'No knowledge services found.'
: `${value.services.length} service(s): ${formatServices(value.services)}`
+ (value.truncated ? ` — listed first ${value.services.length} of ${value.total}; narrow with name_filter.` : ''),
}],
},
async execute(args) {
return await listServices(client, {
...(args.scene === 'chat' || args.scene === 'search' ? { scene: args.scene } : {}),
...(args.name_filter ? { nameFilter: args.name_filter } : {}),
})
},
presentCall: args => ({ card: 'generic', title: 'List knowledge services', kind: 'other', rawInput: args }),
})
const search = defineTool({
name: 'kb_search',
description:
@@ -251,5 +183,5 @@ export function createKbTools(deps: KbToolDeps) {
presentCall: args => ({ card: 'generic', title: 'Ask knowledge base (may take a few minutes)', kind: 'fetch', rawInput: args }),
})
return [serviceList, search, chat]
return [search, chat]
}
+8 -8
View File
@@ -34,13 +34,13 @@ export const en: Record<BailianKbLocaleKey, string> = {
workspaceIdSet: 'A workspace is configured.',
workspaceIdUnset: 'No workspace is configured; knowledge tools fail until one is.',
retrieveAgentId: 'Default retrieval service id',
retrieveAgentIdHint: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (kb_service_list discovers ids). Stored in the settings document.',
retrieveAgentIdHintFallback: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (kb_service_list discovers ids). Leave blank to keep the current one.',
retrieveAgentIdHint: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (`kscli service list` discovers ids). Stored in the settings document.',
retrieveAgentIdHintFallback: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (`kscli service list` discovers ids). Leave blank to keep the current one.',
retrieveAgentIdSet: 'A default retrieval service is configured.',
retrieveAgentIdUnset: 'No default retrieval service; every kb_search call must name one.',
chatAgentId: 'Default chat service id',
chatAgentIdHint: 'agent_id of the default Q&A service (kb_chat); when unset, every call must name one (kb_service_list discovers ids). Stored in the settings document.',
chatAgentIdHintFallback: 'agent_id of the default Q&A service (kb_chat); when unset, every call must name one (kb_service_list discovers ids). Leave blank to keep the current one.',
chatAgentIdHint: 'agent_id of the default Q&A service (kb_chat); when unset, every call must name one (`kscli service list` discovers ids). Stored in the settings document.',
chatAgentIdHintFallback: 'agent_id of the default Q&A service (kb_chat); when unset, every call must name one (`kscli service list` discovers ids). Leave blank to keep the current one.',
chatAgentIdSet: 'A default chat service is configured.',
chatAgentIdUnset: 'No default chat service; every kb_chat call must name one.',
fromEnv: 'Set by the environment (read-only here)',
@@ -70,13 +70,13 @@ export const zh: Record<BailianKbLocaleKey, string> = {
workspaceIdSet: '已配置工作空间。',
workspaceIdUnset: '未配置工作空间;配置前知识库工具不可用。',
retrieveAgentId: '默认检索服务 ID',
retrieveAgentIdHint: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。存入设置文档。',
retrieveAgentIdHintFallback: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。',
retrieveAgentIdHint: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 `kscli service list` 发现 id)。存入设置文档。',
retrieveAgentIdHintFallback: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 `kscli service list` 发现 id)。留空表示保持当前值。',
retrieveAgentIdSet: '已配置默认检索服务。',
retrieveAgentIdUnset: '未配置默认检索服务;每次 kb_search 调用需显式指定。',
chatAgentId: '默认对话服务 ID',
chatAgentIdHint: '默认对话服务(kb_chat)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。存入设置文档。',
chatAgentIdHintFallback: '默认对话服务(kb_chat)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。',
chatAgentIdHint: '默认对话服务(kb_chat)的 agent_id;未设置时每次调用都需显式指定(可用 `kscli service list` 发现 id)。存入设置文档。',
chatAgentIdHintFallback: '默认对话服务(kb_chat)的 agent_id;未设置时每次调用都需显式指定(可用 `kscli service list` 发现 id)。留空表示保持当前值。',
chatAgentIdSet: '已配置默认对话服务。',
chatAgentIdUnset: '未配置默认对话服务;每次 kb_chat 调用需显式指定。',
fromEnv: '来自环境变量(此处只读)',
@@ -9,6 +9,6 @@ describe('kbEndpoint', () => {
it('keeps protocol paths as constants', () => {
expect(KB_PATHS.chat).toBe('/api/v2/apps/knowledge/chat')
expect(KB_PATHS.serviceList).toBe('/api/v1/indices/rag/app/list')
expect(KB_PATHS.search).toBe('/api/v1/indices/knowledge/search')
})
})
@@ -1,49 +0,0 @@
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)
})
})
+7 -8
View File
@@ -21,9 +21,9 @@ const searchResponse = {
}
describe('createKbTools', () => {
it('registers exactly kb_service_list, kb_search, kb_chat', () => {
it('registers kb_search and kb_chat', () => {
const { list } = toolsWith(vi.fn())
expect(list.map(t => t.name).sort()).toEqual(['kb_chat', 'kb_search', 'kb_service_list'])
expect(list.map(t => t.name).sort()).toEqual(['kb_chat', 'kb_search'])
})
it('kb_search truncates nodes client-side to top_k and never sends top_k to the server', async () => {
@@ -59,7 +59,7 @@ describe('createKbTools', () => {
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
const { byName } = toolsWith(postJson)
const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e)
expect((err as Error).message).toContain('kb_service_list')
expect((err as Error).message).toContain('kscli service list')
})
it('kb_search re-resolves the default per call (credential hot-swap contract)', async () => {
@@ -76,14 +76,13 @@ describe('createKbTools', () => {
expect(postJson).toHaveBeenCalledTimes(1)
})
it('a 4xx failure appends the current service list to the error', async () => {
const postJson = vi.fn(async (path: string) => {
if (path === '/api/v1/indices/knowledge/search') throw new KbApiError('agent not found', 400)
return { data: { total_count: 1, rows: [{ agent_id: 'aid-9', agent_name: 'faq', agent_scene: 'search', agent_status: 'deployed' }] } }
it('a 4xx failure passes the original error through unchanged', async () => {
const postJson = vi.fn(async (_path: string) => {
throw new KbApiError('agent not found', 400)
})
const { byName } = toolsWith(postJson)
const err = await byName.kb_search!.execute({ query: 'q', agent_id: 'bad' }, EXEC).catch((e: unknown) => e)
expect((err as Error).message).toContain('aid-9')
expect((err as Error).message).toBe('agent not found')
})
it('kb_chat buffers the SSE stream into one answer', async () => {