npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jungtz/wiki-router

v5.1.0

Published

Deterministic Wiki 知識庫框架 — 把結構化來源轉成 markdown wiki, 多租戶 + plugin 化

Readme

@jungtz/wiki-router

把結構化來源(CMS API / JSON / 任何資料)轉成 markdown wiki,多租戶儲存,並提供 getContext 給下游使用(例如 chatbot 的 RAG context)。

想直接導入到自己的專案?INTEGRATION.md(給 AI agent 或工程師的整合指南,含完整 templates)。本檔是 API 參考。

由你寫 builder 函式決定怎麼把 source 轉成 markdown,套件負責 fingerprint 比對、transaction、manifest、多租戶協調。

source ─→ build() ─→ getContext(prompt) ─→ retriever ─→ 組合好的 wiki 內容

為什麼用這個套件

  • 多租戶 ready:N 家旅館 / N 個專案 / N 個使用者各有自己的 wiki,內建狀態追蹤
  • fingerprint 短路:source 沒變就不重 build
  • transaction safety:build 失敗自動 rollback,不留半套狀態
  • plugin 化:builder / retriever 兩個擴充點
  • Deterministic:沒有 LLM 依賴,相同輸入永遠相同輸出

安裝

npm install @jungtz/wiki-router
# 用 sqlite 儲存 (推薦):
npm install sqlite3 sqlite

最小範例

import { createWikiRouter } from '@jungtz/wiki-router'

const wiki = createWikiRouter({
  knowledgeDir: './knowledge',     // 從目錄讀 JSON / md
  outputDir:    './wiki-output',   // 輸出到目錄

  // 必填:自己決定怎麼把 source 轉成 store 的 .md
  builder: async ({ source, store }) => {
    const keys = await source.list()
    for (const k of keys) {
      const { content } = await source.read(k)
      await store.write(`${k}.md`, `# ${k}\n\n${content}`)
    }
    return { ok: true, filesWritten: keys.length }
  },
})

await wiki.build()
const ctx = await wiki.getContext('使用者問題')
// ctx = "\n[Wiki 相關參考資料]:\n\n--- 檔案: ... ---\n..."

多租戶範例

import { createTenantManager, sqliteStore, ensureWikiTables } from '@jungtz/wiki-router'
import sqlite3 from 'sqlite3'
import { open } from 'sqlite'

const db = await open({ filename: './wiki.db', driver: sqlite3.Database })
await ensureWikiTables(db)

const manager = createTenantManager({
  source: tenantId => myCmsSource(tenantId),
  store:  tenantId => sqliteStore({ db, tenantId }),
  listTenants: () => myDb.getAllHotelIds(),

  builder: async ({ source, store, tenantId }) => {
    const keys = await source.list()
    for (const k of keys) {
      const { content } = await source.read(k)
      await store.write(`${k}.md`, renderForHotel(content))
    }
    return { ok: true, filesWritten: keys.length }
  },

  retriever: 'concat-all',     // 預設值,把所有 .md 串接回傳
})

// 啟動時預熱
await manager.buildAll()

// 服務時
const ctx = await manager.getContext('使用者問題', 'hotel-uuid-1')

API

createWikiRouter(config)

建立單一 wiki 實例。

interface WikiRouterConfig {
  // 來源 / 儲存 (至少設一組)
  source?: Source
  store?: Store
  knowledgeDir?: string      // fsSource 簡寫
  outputDir?: string         // fsStore 簡寫

  // 兩大 plugin point
  builder: BuilderFn                                    // 必填
  retriever?: 'concat-all' | 'graph-rag' | RetrieverFn  // 預設 'concat-all'

  // Embedding (graph-rag 用)
  embedder?: (texts: string[]) => Promise<number[][]>   // 自接 OpenAI / Ollama / 自託管
  topK?: number                                         // graph-rag 種子數, 預設 5
  graphRagDepth?: number                                // wikilink 擴張跳數, 預設 1

  // 多租戶下由 createTenantManager 自動注入
  tenantId?: string

  logger?: Logger
  throwErrors?: boolean
}

回傳:

interface WikiRouterInstance {
  build(options?: { force?: boolean }): Promise<boolean>
  getContext(prompt: string, options?: { maxChars?: number; topK?: number; graphRagDepth?: number }): Promise<string>
  getRelated(filename: string, opts?: { direction?: 'out' | 'in' | 'both'; depth?: number }): Promise<string[]>
  getFrontmatter(filename: string): Promise<Record<string, any> | null>
}

createTenantManager(config)

多租戶版本,內含 wikiCache / buildPromises / builtTenants 追蹤。

interface TenantManagerConfig {
  source: (tenantId: string) => Source
  store: (tenantId: string) => Store
  listTenants?: () => string[] | Promise<string[]>      // buildAll 需要

  builder: BuilderFn
  retriever?: 'concat-all' | RetrieverFn

  logger?: Logger
}

回傳:

interface TenantManager {
  build(tenantId, options?): Promise<boolean>
  buildAll(): Promise<PromiseSettledResult<boolean>[]>
  getContext(prompt, tenantId, options?): Promise<string>
  getRelated(tenantId, filename, opts?): Promise<string[]>
  getFrontmatter(tenantId, filename): Promise<Record<string, any> | null>
  isBuilt(tenantId): boolean
  listBuilt(): string[]
}

getContext 在 tenant 還沒 build 過時回空字串(不阻塞)。建議啟動時跑 buildAll(),或在請求進來時觸發 build(tenantId) 預熱。

Plugin 簽名

Builder

type BuilderFn = (ctx: {
  source: Source
  store: Store
  tenantId?: string
  log: Logger
  options: { force?: boolean }
  /** 相對上次 build 新增或變動的 source key (排序); 首次 build 或 source 無 fingerprint 時等同全部 */
  changedKeys: string[]
  /** 上次 build 存在、本次 source 已移除的 key */
  removedKeys: string[]
  /** 是否首次 build (沒有 stored manifest) */
  isInitialBuild: boolean
}) => Promise<{ ok: boolean; filesWritten?: number; error?: string }>

Incremental build:builder 可只處理 changedKeys 跳過未變動檔;舊 builder 忽略此欄位即維持全量行為(向後相容)。

框架在呼叫 builder 前後會自動處理:

  1. source.getFingerprint() 比對 store.readManifest(),相同就跳過 build(除非 force: true
  2. 開啟 transaction(store.beginTransaction()
  3. 呼叫你的 builder
  4. builder 回傳 { ok: true } → 寫 manifest + commit;失敗 → rollback

所以 builder 本身只要:抓資料 → render → 用 store.write() 落地。不需要管 transaction、fingerprint、manifest。

Retriever

type RetrieverFn = (ctx: {
  prompt: string
  store: Store
  tenantId?: string
  log: Logger
}) => Promise<string>

內建 'concat-all':把 store 內所有 .md(排除 Index.md_ 開頭衍生檔)串接後回傳,格式:

\n[Wiki 相關參考資料]:\n
\n--- 檔案: A.md ---\n<內容>\n
\n--- 檔案: B.md ---\n<內容>\n

getContext 可傳 { maxChars }:concat-all 超過上限會跳過後續檔案並 log warn。自訂 retriever 從 ctx 拿到 maxChars 自行處理。

'graph-rag' (內建, 需要 embedder)

embedding top-k 取種子 → 沿 wikilink 圖擴張 → 依 maxChars 截斷組裝。

import { createWikiRouter } from '@jungtz/wiki-router'

async function openaiEmbedder(texts) {
  const res = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_KEY}` },
    body: JSON.stringify({ model: 'text-embedding-3-small', input: texts }),
  })
  const data = await res.json()
  return data.data.map(d => d.embedding)
}

const wiki = createWikiRouter({
  knowledgeDir: './knowledge',
  outputDir:    './wiki-output',
  builder,
  retriever: 'graph-rag',
  embedder: openaiEmbedder,
  topK: 5,            // 預設
  graphRagDepth: 1,   // 預設
})

await wiki.build()  // build 後框架自動 embed 變動的檔
const ctx = await wiki.getContext('營業時間?', { maxChars: 8000 })

增量 embed:框架在 build 結束時掃 store 內 .md 算 content hash,比對 _embed_manifest.json,只對新增/變動的檔呼叫 embedder;source 消失的檔會呼叫 store.removeEmbedding。Embedder 失敗只 log warn,不會拖垮 build。

Store 需求:retriever 需要 store.queryNearest(vec, k),增量機制需要 store.writeEmbedding / removeEmbedding。內建 fsStore / sqliteStore / mysqlStore 都已實作。其他 store(Postgres / 自訂)只要實作這三個 optional 方法即可。框架 export cosineNearest(queryVec, candidates, k) helper,只想存 raw vec 的 adapter 可以直接拿來算。

Gemini embedder (內建)

不想自己 wrap REST 的話, 套件已內建 Gemini embedder 工廠:

import { createGeminiEmbedder, createTenantManager } from '@jungtz/wiki-router'

const embedder = createGeminiEmbedder({
  apiKey: process.env.GEMINI_API_KEY,
  model: 'gemini-embedding-001',  // 必填; 強制顯式選擇避免誤上 token
  outputDimensionality: 768,      // 預設 768 (MRL 截斷, 省儲存)
  // 其他 optional: maxChars, batchSize, taskType, endpoint, fetch
})

createTenantManager({
  ...,
  embedder,
  retriever: 'graph-rag',
})

走 REST batchEmbedContents endpoint, 不相依 @google/generative-ai SDK。

MySQL adapter (5.1.0+)

import mysql from 'mysql2/promise'
import { mysqlStore, ensureMysqlWikiTables, createTenantManager } from '@jungtz/wiki-router'

const pool = mysql.createPool({ host, user, password, database })
await ensureMysqlWikiTables(pool)

createTenantManager({
  source: tid => mySource(tid),
  store:  tid => mysqlStore({ pool, tenantId: tid }),
  builder,
})

sqliteStore 完整對齊: 三張表 wiki_files / wiki_manifests / wiki_embeddings 自動建立, transaction 走獨立 connection。需要自訂表名可傳 tables: { files, manifests, embeddings }

Observability (5.1.0+)

監測每次 getContext 回傳的 context 體積, 判斷何時該升級 retriever:

createTenantManager({
  ...,
  onContextSize: ({ tenantId, chars, retriever }) => {
    // 寫 DB / metrics / log 都行
    db.run('UPDATE stats SET max=? WHERE id=? AND max<?', chars, tenantId, chars)
  },
})

event.retriever'concat-all' | 'graph-rag' | 'custom'。Callback 異常被框架接住, 不影響 getContext 回傳。

Frontmatter

Builder 可以用物件形式寫 YAML frontmatter 到檔頂:

await store.write('Rooms.md', {
  content: '# Rooms\n\n房型介紹...',
  frontmatter: { type: 'room', tags: ['suite', 'standard'], count: 12 },
})

落地內容:

---
type: room
tags: [suite, standard]
count: 12
---
# Rooms

房型介紹...

讀回:

const fm = await wiki.getFrontmatter('Rooms.md')
// → { type: 'room', tags: ['suite', 'standard'], count: 12 }

支援型別:string / number / boolean / null / array of primitives。不支援巢狀物件或多行字串——若需要請自行 escape 或寫純字串。

Retriever 可以用 parseFrontmatter(await store.read(f)) 拿到 { frontmatter, body },做 metadata 過濾或 type-aware 路由。

Wikilink 圖

Build 後框架會掃過所有 .md 抽出 [[target]] 連結,將鄰接表寫入 _links.json(底線開頭,retriever 預設排除)。

支援語法:

  • [[Rooms]] → 目標 Rooms
  • [[Rooms.md]] → 目標 Rooms(自動去 .md
  • [[Rooms|顯示文字]] → 目標 Rooms(取 | 前)

解析後若 <target>.md 不在 store 中,視為未解析連結並丟棄。

await wiki.build()

await wiki.getRelated('Rooms.md')
// → ['Hours.md', 'Address.md']  (Rooms.md 向外連的)

await wiki.getRelated('Index.md', { direction: 'in' })
// → 連向 Index.md 的所有檔

await wiki.getRelated('Rooms.md', { depth: 2 })
// → 一跳 + 兩跳鄰居 (BFS, 自動去重)

注意:解析不感知 markdown 結構,程式碼區塊內的 [[…]] 也會被掃到。Builder 端如有特殊需求請自行 escape。

視覺化

_links.json 是標準鄰接表,直接餵給現成工具即可,框架不附視覺化。

轉成 Mermaid(GitHub / markdown 原生渲染):

import fs from 'fs'
const graph = JSON.parse(fs.readFileSync('./wiki-output/_links.json', 'utf-8'))
const lines = ['graph LR']
for (const [src, targets] of Object.entries(graph)) {
  for (const t of targets) lines.push(`  ${src.replace('.md', '')} --> ${t.replace('.md', '')}`)
}
console.log('```mermaid\n' + lines.join('\n') + '\n```')

用 Obsidian 開:若 store 是 fsStore,輸出目錄本身就是個合法的 Obsidian vault——直接 "Open folder as vault",內建 Graph View 會讀 .md 內的 [[…]](不需要 _links.json)。

前端 D3 / Cytoscape:把 _links.json 轉成各自的 nodes/edges 格式餵入即可。

內建工具函式

給 builder 用的 helpers,從套件直接 import:

| 函式 | 用途 | |---|---| | parseQuillDelta(str) | Quill Delta JSON → 純文字 / markdown | | flattenLangFields(node, lang) | 攤平多語物件 {zh_tw, en, ja} 到指定語系 | | canonicalJson(v) | 對 JSON 產生 key-排序的字串(給 hash 用) | | hashCanonical(v) | 算 SHA-256 hex(適合 source.getFingerprint()) | | asText(v) | 任意值 → 字串(自動 parse Quill / strip HTML) | | kv(label, v) | 生成 - **label**:value,空值回空字串 | | joinLines(...parts) | 攤平 + 過濾空值 + \n 串接 | | formatElapsed(startMs) | 毫秒 → "123ms" / "1.23s" / "2m3.4s" | | fingerprintsEqual(a, b) | 比對兩個 fingerprint 物件 | | cosineSimilarity(a, b) | 兩個 vec 的 cosine 相似度 (0..1) | | cosineNearest(query, candidates, k) | 在 candidates [{filename, vec}] 中找出與 query 最相似的 k 筆 | | parseFrontmatter(content) / serializeFrontmatter(fm) | YAML frontmatter 處理 |

Adapters

  • fsSource(dir) / fsStore(dir) — 檔案系統
  • sqliteStore({ db, tenantId }) + ensureWikiTables(db) — sqlite(透過 sqlite wrapper)

需要 MySQL / Postgres / 其他 store?實作 Store 介面即可,拿 sqliteStore 當範本。

Store 介面

interface Store {
  list(): Promise<string[]>
  read(filename): Promise<string | null>
  write(filename, content: string | { content: string; frontmatter?: object }): Promise<void>
  readManifest?(): Promise<Record<string, string> | null>
  writeManifest?(manifest): Promise<void>
  beginTransaction?(): Promise<void>
  commit?(): Promise<void>
  rollback?(): Promise<void>
  // graph-rag 相關
  writeEmbedding?(filename, vec: number[]): Promise<void>
  removeEmbedding?(filename): Promise<void>
  queryNearest?(vec: number[], k: number): Promise<Array<{ filename: string; score: number }>>
}

所有 ? 標記皆 optional。沒實作 manifest 就無法 fingerprint 短路;沒實作 transaction 就無法 rollback;沒實作 embedding 三方法則 graph-rag retriever 無法運作。fs / sqlite adapter 都完整實作。

Source 介面

interface Source {
  list(): Promise<string[]>
  read(key): Promise<{ type: 'json' | 'markdown'; content: string }>
  getFingerprint?(): Promise<Record<string, string>>
}

getFingerprint 是 optional 但強烈建議實作——沒有 fingerprint 就無法短路重複 build,每次都會跑完整 builder。

設計哲學

  1. 框架不知道你的資料長什麼樣。CMS schema、欄位命名、render 規則都是專案專屬,放在 builder 函式裡,不在套件裡。
  2. 可預測、可重現。Deterministic 流程,相同輸入永遠相同輸出。
  3. 小核心 + 大彈性。框架只做 orchestration,所有業務邏輯由你決定。
  4. 失敗要明顯。Build 失敗 rollback、fingerprint 不寫,下次重試。不會留下「半套」狀態。

範例

Roadmap

後續規劃見 ROADMAP.md(incremental build / token budget / frontmatter / embedding / hybrid retrieval)。

互動預覽

單一 wiki:

npm run preview
npm run preview -- --knowledge ./my-knowledge --output ./my-wiki

多租戶(createTenantManager + sqliteStore):

npm run preview:multi
npm run preview:multi -- --knowledge ./knowledge --db ./data/wiki-preview.db

preview:multi 額外支援 :tenants:tenant <id> 切換、:buildall 全租戶重建。其餘指令::list / :build / :help / :quit

發布(自動)

push 到 master 後 CI 掃 commit message 決定 bump:

| Commit 訊息 | Bump | |---|---| | :boom: 💥 / BREAKING CHANGE | major | | :sparkles: ✨ | minor | | 其他 (:bug: / :hammer: / ...) | patch |

錯誤處理

預設靜默失敗:build 失敗回 falsegetContext''。設 throwErrors: true 可改為丟出 WikiRouterError(含 codecause)。

相依

| 套件 | 關係 | |------|------| | 無外部執行期相依 | 僅需 Node.js ≥ 18 | | SQLite 驅動(如 sqlite3 + sqlite) | 由使用者提供(傳入 sqliteStore),套件不直接相依 |

License

MIT