feat: report library search locations

This commit is contained in:
techflag
2026-09-16 17:25:12 +08:00
parent 0513c94046
commit f88f74a758
4 changed files with 11 additions and 5 deletions
+1
View File
@@ -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;
}
@@ -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);
});
@@ -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({
@@ -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);