From f88f74a7586a4a50eef59b1fd682d85cd8aec0fe Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Wed, 16 Sep 2026 17:25:12 +0800 Subject: [PATCH] feat: report library search locations --- packages/contracts/src/library.ts | 1 + packages/plugins/library/src/services/library-manager.ts | 3 ++- packages/plugins/library/src/tools/library-tools.ts | 4 ++-- packages/plugins/library/tests/library.test.mjs | 8 ++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/library.ts b/packages/contracts/src/library.ts index d7913ce460..3b53fd3c16 100644 --- a/packages/contracts/src/library.ts +++ b/packages/contracts/src/library.ts @@ -72,6 +72,7 @@ export interface LibrarySearchHit { readonly nodeId: string; readonly name: string; readonly kind: LibraryAssetKind; + readonly location?: string; readonly excerpt: string; readonly score: number; } diff --git a/packages/plugins/library/src/services/library-manager.ts b/packages/plugins/library/src/services/library-manager.ts index f0137f17b0..f1d34eb46e 100644 --- a/packages/plugins/library/src/services/library-manager.ts +++ b/packages/plugins/library/src/services/library-manager.ts @@ -133,7 +133,8 @@ export class LibraryManager extends Service implements LibraryService { const text = await readFile(this.safePath(revision.contentRelativePath), 'utf8'); const lower = text.toLocaleLowerCase(); const titleMatch = node.name.toLocaleLowerCase().includes(needle); const offset = lower.indexOf(needle); if (!titleMatch && offset < 0) continue; const start = Math.max(0, offset < 0 ? 0 : offset - 80); const excerpt = text.slice(start, start + 240).replace(/\s+/g, ' ').trim(); - hits.push({ assetId: asset.id, revisionId: revision.id, nodeId: node.id, name: node.name, kind: asset.kind, excerpt, score: titleMatch ? 2 : 1 }); + const prefix = text.slice(0, offset < 0 ? text.length : offset); const heading = [...prefix.matchAll(/^#{1,6}\s+(.+)$/gm)].at(-1)?.[1]?.trim(); + hits.push({ assetId: asset.id, revisionId: revision.id, nodeId: node.id, name: node.name, kind: asset.kind, ...(heading ? { location: heading } : {}), excerpt, score: titleMatch ? 2 : 1 }); } return hits.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name, 'zh-CN')).slice(0, 50); }); diff --git a/packages/plugins/library/src/tools/library-tools.ts b/packages/plugins/library/src/tools/library-tools.ts index 29414c0d92..de2f8916fe 100644 --- a/packages/plugins/library/src/tools/library-tools.ts +++ b/packages/plugins/library/src/tools/library-tools.ts @@ -17,7 +17,7 @@ export function registerLibraryTools(ctx: Context): void { type: 'object', additionalProperties: false, properties: { hits: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, - properties: { asset_id: { type: 'string', required: true }, revision_id: { type: 'string', required: true }, name: { type: 'string', required: true }, kind: { type: 'string', required: true }, excerpt: { type: 'string', required: true } }, + properties: { asset_id: { type: 'string', required: true }, revision_id: { type: 'string', required: true }, name: { type: 'string', required: true }, kind: { type: 'string', required: true }, location: { type: 'string' }, excerpt: { type: 'string', required: true } }, } } }, }, render: (_args, value) => [{ type: 'text', text: value.hits.length ? value.hits.map(hit => `${hit.name}: ${hit.excerpt}`).join('\n') : '没有找到匹配资料。' }], @@ -26,7 +26,7 @@ export function registerLibraryTools(ctx: Context): void { const current = await actor(ctx, exec); const sessionId = current.sessionId ?? (exec.agent ? String(exec.agent.id) : undefined); if (!sessionId) throw new Error('library/session-required'); const selected = new Map((await ctx.workdshLibrary.taskSelection(current, sessionId, exec.signal)).map(row => [row.assetId, row.revisionId])); const hits = (await ctx.workdshLibrary.search(current, args.query, exec.signal)).filter(hit => selected.get(hit.assetId) === hit.revisionId); - return { hits: hits.map(hit => ({ asset_id: hit.assetId, revision_id: hit.revisionId, name: hit.name, kind: hit.kind, excerpt: hit.excerpt })) }; + return { hits: hits.map(hit => ({ asset_id: hit.assetId, revision_id: hit.revisionId, name: hit.name, kind: hit.kind, ...(hit.location ? { location: hit.location } : {}), excerpt: hit.excerpt })) }; }, })); ctx.tools.register(defineTool({ diff --git a/packages/plugins/library/tests/library.test.mjs b/packages/plugins/library/tests/library.test.mjs index fcef6b9c6d..a53c3cddc2 100644 --- a/packages/plugins/library/tests/library.test.mjs +++ b/packages/plugins/library/tests/library.test.mjs @@ -64,8 +64,12 @@ test('independent library plugin persists a tree, originals and searchable deriv assert.equal(repeated.asset.id, imported[0].asset.id); assert.equal((await ctx.workdshLibrary.list(actor, folder.id)).length, 5); assert.equal((await ctx.workdshLibrary.search(actor, 'DocxCharlie'))[0].name, '报告.docx'); - assert.equal((await ctx.workdshLibrary.search(actor, 'PptxDelta'))[0].name, '简报.pptx'); - assert.equal((await ctx.workdshLibrary.search(actor, 'UniquePdfEcho'))[0].name, '附件.pdf'); + const pptxHit = (await ctx.workdshLibrary.search(actor, 'PptxDelta'))[0]; + assert.equal(pptxHit.name, '简报.pptx'); + assert.match(pptxHit.location, /第 1 页/); + const pdfHit = (await ctx.workdshLibrary.search(actor, 'UniquePdfEcho'))[0]; + assert.equal(pdfHit.name, '附件.pdf'); + assert.match(pdfHit.location, /第 1 页/); assert.deepEqual(await ctx.workdshLibrary.readOriginal(actor, imported[1].asset.id), samples[1][1]); const selected = await ctx.workdshLibrary.setTaskSelection(actor, 'session-a', [folder.id]); assert.equal(selected.length, 5);