Files
nocobase[bot] a1c353b825 chore: merge release 2026-10-12.3 into v3-develop [skip ci]
current app-template-default version: 1.0.0-beta.64
2026-10-12 04:56:14 +00:00
..

@nocobase/app-plugin-knowledge

A knowledge base for any NocoBase application: spaces holding a tree of folders, Markdown articles and files, every saved version kept whole under an optimistic lock, the text of files extracted for search, search fused from pluggable providers, proposals that someone who may edit decides, and snapshots that export spaces as files. It depends on no AI, agents or projects: the application names its spaces and decides who may do what in each.

Spaces and access

A space is named by a kind (scope) and a key (scopeId) the application chooses, such as an application's system space (system, key empty) and one space per project (project, its id). The application binds knowledgeAccessToken (server/tokens) with a KnowledgeAccessResolver:

Member What it answers
space(ref) the space in its normal form when the application has it, else null (400 INVALID_SCOPE)
inherits(ref) the spaces it inherits, nearest first: their documents join its view, never hidden by a slug
forReader(r) for one reader and request: access(space) → { read, propose, edit, manage }
title(ref) a space's title (a project's name), or null
names(refs) display names of people (user) and actors (by their kind), keyed kind:id

Nothing is read without a reader (KnowledgeReader: a person, or an actor such as an agent acting for one, with the run it works in). Whatever the resolver answers, an actor never edits or decides, never sees archived documents, and makes at most three proposals per run. Unbound, there are no spaces.

shared/access declares what the application's roles may hold: the page knowledge and the business actions kb.knowledge/read, propose, edit and manage, each reaching all spaces, none, or the spaces the application relates to a set of users (Scope, reaches). The plugin registers the actions with the authorization plugin as the kb resource type, one action per level (read.related, read.all), when the application has that plugin; the application grants them in its roles and builds its resolver's answers from them.

Below a space, each folder, article and file may narrow or widen its access: it inherits its parent's plus entries of its own, or in custom mode has only its own entries, at the levels read, propose, edit and manage; whoever manages the space manages every node in it. Entries name subjects of types the application declares with registerSubjectProvider (a user, a role, a group…), resolved at query time, and retrieval filters by each entry's access key inside every query. docs/permissions.md describes the model, the subject provider contract and how retrieval is filtered.

Entries and files

An entry of a tree is a folder (a name and a place, no versions; renamed in place), an article (Markdown) or a file. A file's every version names its stored file and holds, as its content, the file's text extracted as Markdown; an article is never replaced by a file, nor a file by text.

  • Storage. The bytes go through the file plugin's server repository (@nocobase/app-plugin-file, collection kbFiles) onto a Drive disk, and are only ever served by the plugin's own routes after its access check, never at the file plugin's public path. The application configures knowledge.disk (the Drive default, else local) and knowledge.maxFileSize (bytes, 100 MiB by default). Without the file plugin and Drive, uploads answer 400 FILES_UNAVAILABLE.
  • Cleanup. A sweep deletes upload tickets past their expiry and stored files that no version and no pending or accepted proposal names, once they are older than a grace period: what a crash, a failed deletion or an abandoned upload left (cleanup, server/services/cleanup.ts). The provider runs it on a timer from boot to shutdown, every knowledge.cleanup.intervalMinutes (60) for files older than knowledge.cleanup.graceHours (24); knowledge.cleanup: false turns it off. It is safe on several instances at once.
  • Parsing. After an upload commits, the file's text is extracted in a worker thread, one file at a time (server/parsing): PDF (pdf-parse's pdf.js, a section per page), DOCX (mammoth), DOC (word-extractor), XLSX, XLS, XLSM and CSV (SheetJS, a section per sheet, rows as tables), PPTX (officeparser, a section per slide), and TXT, MD and JSON as they are. No OCR. Anything else, images included, is stored only. A version's parseStatus is parsing, ready, failed (with the reason) or unsupported; files.reparse tries a failed one again, and the provider picks up at start what a stopped server left parsing.
  • Chunks. Articles and extracted text are cut into the same sections (kbChunks), at headings down to a depth and with sizes the application sets and a space may override (chunking), rewritten with new ids with each version and again in the background when the chunking changes. chunks.changed fires whenever an entry's sections change: a new version, a file's text extracted, or the entry archived (its sections leave search) or restored. An index of the application's own re-reads them then with chunksOf, which answers none for an archived entry, and can skip unchanged sections by their hash.

Server

knowledgeToken resolves Knowledge:

Member What it does
docs tree, get, resolve (slug or id in a view), versions, version, create (an article or a folder), update (expectedVersion, 409 KNOWLEDGE_VERSION_CONFLICT), move, archive, restore, verify, search
files upload (a new file entry), replace (its next version), content (a version's bytes, for whoever reads the entry), reparse, idle, resume, maxBytes
permissions get and replace a node's mode and entries (for whoever manages it), effective (the viewer's level and its source), subjects (to grant, from the providers)
registerSubjectProvider adds a KnowledgeSubjectProvider: a type of subject entries may grant to; answers what removes it
proposals propose (update, create, verify, with an application-defined source, and a fileId for a file), proposeFile, ticket and redeem (a file sent apart), file, list, get, accept (confirmStale), reject, withdraw
registerSearchProvider adds a KnowledgeSearchProvider; answers what removes it
registerReranker adds a KnowledgeSearchReranker that reorders the best fused sections; answers what removes it
chunking get and set a space's own chunking (manage; null follows the default), defaultChanged (cuts the spaces that follow the default again), resume, idle
bindSettings binds the application's KnowledgeSettingsSource: the default chunking(), recall() (limit, minScore, provider weights, rerankCandidates) and mayExplain(reader); answers what unbinds it
indexing doc (a document's sections with their index state), reindex (edit), unfinished (a space's documents not fully indexed)
registerIndexer binds a KnowledgeIndexer, the application's semantic index answering states, unfinished and reindex; answers what unbinds it
chunksOf(conn, docId) an entry's sections at its current version (id, ordinal, text, headingPath, hash) with its space and version; none for an archived or missing entry or a folder
eachChunk({ batchSize }) every live entry's current sections, in batches, for rebuilding an index
texts(conn, readable, { maxChars }) the whole current text of the readable spaces' live articles and files; with maxChars, sized first from the sections' stored lengths and not loaded when longer (complete: false)
cleanup({ graceMs }) deletes expired upload tickets and unused stored files older than the grace period (a day)
readable(reader, refs) of the spaces asked for, those the reader reads something in, with their gates; decided before a transaction that reads them
outline(conn, readable) the live documents of each readable space, through a transaction's connection
snapshots take(conn, readable, { key, seriesKey, layout }), files(conn, key, layout), docs(conn, key)
events.on(listener) after each commit: doc.versioned, doc.changed, chunks.changed, proposal.created, proposal.decided

A snapshot exports the readable spaces as files recorded under a key (a run's id): INDEX.md, one file per article with front matter naming it and its version, a file as its extracted text (<file name>.md, front matter naming the file; never the original, which files.content reads), folders as directories, and .manifest.json, hashed so a consumer caches them; a later snapshot of the same series (a session) says which documents changed. A SnapshotLayout words the files for the application: each space's directory and heading, the index's instructions, how to read a document not written, how to get a file's original, and the link in each file's front matter. readDocumentFile reads a file's front matter back.

A proposal may carry a file: a new file entry (create) or a file's replacement (update). A person uploads it with the proposal (proposeFile, POST /api/knowledge/proposals/upload); a client that sends files apart from its requests (an agent's command) asks for a one-time ticket and sends the bytes to POST /api/knowledge/tickets/:ticketId/redeem with Authorization: Bearer <token>, which proposes as the ticket's reader. Accepting extracts the file's text; rejecting or withdrawing deletes the file.

Search asks every registered KnowledgeSearchProvider — search(spaces, query, { limit }), the spaces being only those the reader reads something in, each with the reader's gates to filter by inside the provider's own query (gatesOf against the gate chunksOf and eachChunk give each section), answering ranked { chunkId, score } — and fuses their rankings with weighted Reciprocal Rank Fusion (k = 60, each provider weighing what the application's recall settings say, 1 by default), dropping a section whose fused score over the most the answering providers could give is under minScore; a section the reader's gates do not let through, of an archived entry or an older version is dropped before ranking, and a failing provider is reported and left out. A registered KnowledgeSearchReranker — rerank(query, candidates, { limit }), the candidates being the best fused sections (at most rerankCandidates, 50) with their text — may then reorder them: the sections it ranks come first in its order, the rest follow in their fused order; a failing reranker is reported and one that ranks nothing has no opinion, and either way the next is asked, the fused order staying without one. Each hit carries its fused score, every provider's own rank and score (providers), and the reranker's score (reranked, null otherwise). With explain, for someone who manages the space or whom the application's mayExplain allows, each hit also carries explain: its normalized score, each provider's rank, score and weight, and the rerank score. The built-in provider is a plain contains match over the sections of the current versions, title matches first, then the most recently updated (createContainsSearch). A full-text provider over the database's own index is reserved; the application registers others, a vector index say, through registerSearchProvider.

/api/knowledge serves the same for signed-in people (server/routes/api.ts lists the endpoints): every answer is { data }, a list { data, meta } (versions and proposals paged by page and pageSize), and every refusal the standard error body with domain knowledge and the service's code (KnowledgeError.code, such as KNOWLEDGE_VERSION_CONFLICT or DOC_NOT_FOUND) as its reason. The realtime topic knowledge announces each change by id. The schema is one migration, 202610030010_kb_create_tables (database/README.md).

Client

  • client/api: the headless hooks (useKnowledgeTree, useKnowledgeDoc, useKnowledgeProposal, …, knowledgeKeys, useKnowledgeRefresh) for pages the application composes.
  • client/pages: KnowledgePage and KnowledgeView, bound to the plugin's namespace with ProposalBody, ProposalActions, ProposalKindBadge and KnowledgeMarkdown. KnowledgeView fills the size it is given (className) with two resizable panes: on the left the view's title the full width of the pane, a ⌘K search with the New menu beside it, the proposals waiting, and each space's tree with its "…" menus (uploads by menu or drag and drop, file proposals for people who may propose but not edit, the search test showing each hit's relevance, every provider's rank and score, the reranker's and a link to its lines, and, for a manager, the space's chunking); on the right the space's home, an entry (its index state while semantic search is on and its sections from its "…" menu, highlighting a section's lines; an article in a reading column with its contents, its editor in place, where someone who may only propose submits a proposal, its history with inline or side-by-side diffs; a file with its parse status, preview and text), the proposals by status with their review, or search results. There is no header above the panes, in a page or a tab alike; with title={false} the left pane starts at the search. The space's home offers to write or upload only while the space is empty. Who may do what opens from what it applies to: the space's "…" menu opens its access (the viewer's level and where it comes from, and the roles); an entry's "…" menu in the tree opens its permissions for someone who manages it, and the "…" menu of the entry open opens them for anyone, starting with the viewer's own access and where it comes from (PermissionsDialog: inherit or only the people listed, what it inherits from where, its entries with a level each, and a search adding subjects grouped by type); a restricted entry shows a lock in the tree and "Restricted · N" in its header. It takes the space, optional labels that word the application's spaces, and accessDetails (KnowledgeAccessDetails): the application's roles × read, propose, edit and manage for the space, where the viewer's access comes from, what "related" means there and where roles are changed.
  • client/routes: createKnowledgeRoutes({ path, space }) registers the page behind the knowledge page grant; knowledge({ routes: false }) leaves routing to the application.

The file previews are the file plugin's preview components (its Registry item component-ui), installed under client/extensions/file-preview as this plugin's own source; DOCX, XLSX and PPTX render in the browser through @silurus/ooxml.

Verification

pnpm --filter @nocobase/app-plugin-knowledge lint
pnpm --filter @nocobase/app-plugin-knowledge typecheck
pnpm --filter @nocobase/app-plugin-knowledge test
pnpm --filter @nocobase/app-plugin-knowledge build