From cb464c3ee47399053e254a47be91f1f197259c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 06:33:00 -0400 Subject: [PATCH] Q32: documents and knowledge appear in global search Co-Authored-By: Claude Opus 5.5 --- src/main/services/search-service.ts | 20 +++++++++++++++ src/renderer/src/i18n/zh.ts | 1 + src/renderer/src/shared/SearchDialog.tsx | 3 +++ src/shared/search-domain.ts | 2 ++ tests/unit/search-service.test.ts | 32 ++++++++++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/src/main/services/search-service.ts b/src/main/services/search-service.ts index 64f7c414..ea658bab 100644 --- a/src/main/services/search-service.ts +++ b/src/main/services/search-service.ts @@ -88,6 +88,8 @@ export class SearchService { ...this.providerDocs(orgId), ...this.socialDocs(orgId), ...this.scheduledPostDocs(orgId), + ...this.documentDocs(orgId), + ...this.knowledgeDocs(orgId), ]; } @@ -156,6 +158,24 @@ export class SearchService { return rows.map((row) => doc("scheduled_post", row.id, createTitle(row.body_md), row.body_md, { type: "Social", state: row.state, account: row.social_account_id })); } + private documentDocs(orgId: string): SearchDoc[] { + const rows = this.db.prepare("select id, title, doc_type, status, content from documents where org_id = ?").all(orgId) as Array<{ id: string; title: string; doc_type: string; status: string; content: string }>; + return rows.map((row) => doc("document", row.id, row.title, row.content, { type: "Library", kind: row.doc_type, status: row.status })); + } + + private knowledgeDocs(orgId: string): SearchDoc[] { + const rows = this.db.prepare("select id, topic, category, content, scope_type, superseded_by, valid_until from knowledge_entries where org_id = ?").all(orgId) as Array<{ + id: string; topic: string; category: string; content: string; scope_type: string; superseded_by: string | null; valid_until: string | null; + }>; + const now = new Date().toISOString(); + return rows.map((row) => doc("knowledge", row.id, row.topic, row.content, { + type: "Library", + category: row.category, + scope: row.scope_type, + state: row.superseded_by ? "superseded" : row.valid_until && row.valid_until <= now ? "expired" : "active", + })); + } + private canRead(orgId: string, employeeId: string, entityType: SearchEntityType, entityId: string): boolean { if (entityType === "chat_space") return this.canReadSpace(entityId, employeeId); if (entityType === "chat_message") return this.canReadMessage(orgId, entityId, employeeId); diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index 2f941d81..b5de9d27 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -27,6 +27,7 @@ export const zh: Record = { "Advance": "推进", "Advanced": "高级", "Integrations": "集成密钥", + "Document": "文档", "Applies to": "适用范围", "Category": "类别", "Content": "内容", diff --git a/src/renderer/src/shared/SearchDialog.tsx b/src/renderer/src/shared/SearchDialog.tsx index 721643dd..91d4d400 100644 --- a/src/renderer/src/shared/SearchDialog.tsx +++ b/src/renderer/src/shared/SearchDialog.tsx @@ -21,6 +21,8 @@ const LABELS: Record = { provider: t("Provider"), social_account: t("Social"), scheduled_post: t("Post"), + document: t("Document"), + knowledge: t("Knowledge"), }; export function SearchDialog({ @@ -142,6 +144,7 @@ function routeFor(entityType: SearchEntityType): RouteId { if (entityType === "file") return "files"; if (entityType === "provider") return "settings"; if (entityType === "social_account" || entityType === "scheduled_post") return "connectors"; + if (entityType === "document" || entityType === "knowledge") return "library"; return "work"; } diff --git a/src/shared/search-domain.ts b/src/shared/search-domain.ts index 4ab33e0d..74f8cbfe 100644 --- a/src/shared/search-domain.ts +++ b/src/shared/search-domain.ts @@ -12,6 +12,8 @@ export const SEARCH_ENTITY_TYPES = [ "provider", "social_account", "scheduled_post", + "document", + "knowledge", ] as const; export type SearchEntityType = (typeof SEARCH_ENTITY_TYPES)[number]; diff --git a/tests/unit/search-service.test.ts b/tests/unit/search-service.test.ts index f9142f74..6cbd3665 100644 --- a/tests/unit/search-service.test.ts +++ b/tests/unit/search-service.test.ts @@ -6,6 +6,8 @@ import { DatabaseConnection } from "../../src/main/database/connection"; import { ChatService } from "../../src/main/services/chat-service"; import { OrganizationService } from "../../src/main/services/organization-service"; import { SearchService } from "../../src/main/services/search-service"; +import { DocumentService } from "../../src/main/services/document-service"; +import { KnowledgeService } from "../../src/main/services/knowledge-service"; import { TASK_STATUS } from "../../src/shared/domain"; import { createId } from "../../src/shared/ids"; @@ -94,4 +96,34 @@ describe("search service", () => { expect(search.search({ orgId, employeeId: outsiderId, query: "secret", entityTypes: ["chat_message"] })).toHaveLength(0); connection.close(); }); + + test("finds documents and knowledge by their bodies, with state facets", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const search = new SearchService(connection.db); + let orgId = ""; + connection.transaction(() => { + orgId = organization.createOrganization({ organizationName: "Lib", employeeName: "Dana Lee", mission: "m", goal: "g" }); + }); + const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; + new DocumentService(connection.db).create({ + orgId, title: "Pricing memo", docType: "memo", content: "Enterprise tier costs twelve hundred dollars.", + authorAgentId: null, reviewerAgentId: null, projectId: null, goalId: null, taskId: null, + }); + const knowledge = new KnowledgeService(connection.db); + const oldId = knowledge.create({ + orgId, topic: "Refund window", category: "policy", content: "Refunds within fourteen days.", importance: "normal", + sourceType: "employee", sourceId: null, scopeType: "organization", scopeId: null, validUntil: null, supersedesId: null, + }); + knowledge.create({ + orgId, topic: "Refund window", category: "policy", content: "Refunds within thirty days.", importance: "normal", + sourceType: "employee", sourceId: null, scopeType: "organization", scopeId: null, validUntil: null, supersedesId: oldId, + }); + + const docs = search.search({ orgId, employeeId, query: "twelve hundred" }); + expect(docs.map((r) => [r.entityType, r.title, r.facets.kind])).toEqual([["document", "Pricing memo", "memo"]]); + const entries = search.search({ orgId, employeeId, query: "refunds", entityTypes: ["knowledge"] }); + expect(entries.map((r) => r.facets.state).sort()).toEqual(["active", "superseded"]); + connection.close(); + }); });