@jungtz/wiki-router 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +208 -225
  2. package/package.json +2 -1
package/README.md CHANGED
@@ -1,28 +1,29 @@
1
1
  # @jungtz/wiki-router
2
2
 
3
- LLM Wiki 知識庫路由引擎 — 將結構化知識 (JSON/Markdown) 轉為 LLM 可查詢的 Markdown 維基
3
+ 把結構化知識(JSON / Markdown)丟給 LLM 拆成 wiki,再依使用者問題自動挑相關 .md 當上下文回傳。
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/@jungtz%2Fwiki-router.svg)](https://badge.fury.io/js/@jungtz%2Fwiki-router)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ ```
9
+ 知識來源 → build() → LLM 拆成多個 .md → getContext(prompt) → LLM 路由 → 相關 .md
10
+ ```
11
+
8
12
  ## 安裝
9
13
 
10
14
  ```bash
11
- npm install @jungtz/wiki-router
15
+ npm install @jungtz/wiki-router @jungtz/ai-router
16
+ # 多租戶 + DB 儲存(範本 B)需要:
17
+ npm install better-sqlite3
12
18
  ```
13
19
 
14
- ## 運作流程
15
-
16
- ```
17
- 知識來源 -> build() -> LLM 生成 Wiki -> getContext(prompt) -> LLM 路由 -> 相關上下文
18
- ```
20
+ ---
19
21
 
20
- 1. **build()**: 將知識來源 (JSON/Markdown) 送交 LLM,拆分成結構化 `.md` 維基頁面
21
- 2. **getContext(prompt)**: 根據使用者問題,由 LLM 從 Index.md 中選擇相關檔案,回傳合併後的上下文
22
+ ## 整合範本(複製即用)
22
23
 
23
- 知識的「來源」與 wiki 的「儲存位置」皆透過 **adapter** 介面抽象,可使用內建檔案系統 adapter,亦可自訂 (例如 API + 資料庫),方便支援多租戶 / 多資料集場景。
24
+ ### A. 單一 wiki(檔案系統,最小整合)
24
25
 
25
- ## 快速開始(檔案系統)
26
+ `server.js`:
26
27
 
27
28
  ```js
28
29
  const { createWikiRouter } = require('@jungtz/wiki-router')
@@ -36,317 +37,299 @@ const router = createRouter({
36
37
 
37
38
  const wiki = createWikiRouter({
38
39
  router,
39
- knowledgeDir: './knowledge', // fs 來源(簡寫)
40
- outputDir: './wiki-output', // fs 儲存(簡寫)
40
+ knowledgeDir: './knowledge', // .json / .md 知識來源
41
+ outputDir: './wiki-output', // 生成的 .md 維基
41
42
  modelId: 'ollama-local/gemma4:31b',
42
43
  })
43
44
 
44
- await wiki.build()
45
- const ctx = await wiki.getContext('住宿有什麼規定?')
45
+ ;(async () => {
46
+ await wiki.build() // 啟動時建構(指紋未變動會自動跳過)
47
+ const ctx = await wiki.getContext('住宿規定?')
48
+ console.log(ctx)
49
+ })()
46
50
  ```
47
51
 
48
- ## 進階:自訂 adapter(多租戶 / API + DB)
49
-
50
- 當知識來自 API、wiki 要存到資料庫時,傳入 `source` / `store` adapter:
51
-
52
- ```js
53
- const { createWikiRouter } = require('@jungtz/wiki-router')
54
-
55
- function wikiOf(hotelId) {
56
- return createWikiRouter({
57
- router,
58
- modelId: 'ollama-local/gemma4:31b',
59
- source: {
60
- async list() { return ['base.json'] },
61
- async read() {
62
- const res = await fetch(`/api/hotels/${hotelId}/knowledge`)
63
- return { type: 'json', content: await res.text() }
64
- },
65
- },
66
- store: {
67
- async list() { return await db.wiki.list(hotelId) },
68
- async read(filename) { return await db.wiki.read(hotelId, filename) },
69
- async write(name, body) { await db.wiki.upsert(hotelId, name, body) },
70
- },
71
- })
72
- }
73
-
74
- const wikiA = wikiOf(4) // 旅館 A
75
- const wikiB = wikiOf(7) // 旅館 B
52
+ 目錄結構:
53
+ ```
54
+ your-project/
55
+ ├── server.js
56
+ ├── knowledge/
57
+ │ └── base.json ← 知識來源
58
+ └── wiki-output/ ← 自動產生
59
+ ├── Index.md
60
+ ├── About.md
61
+ └── .manifest.json ← 來源指紋(自動寫入)
76
62
  ```
77
63
 
78
- ## API
79
-
80
- ### `createWikiRouter(config)`
81
-
82
- | 參數 | 類型 | 必要 | 說明 |
83
- |------|------|------|------|
84
- | `router` | `AIProviderRouter` | ✅ | AI Router 實例 |
85
- | `source` | `Source` | ✱ | Knowledge 來源 adapter(與 `knowledgeDir` 二選一) |
86
- | `store` | `Store` | ✱ | Wiki 儲存 adapter(與 `outputDir` 二選一) |
87
- | `knowledgeDir` | `string` | ✱ | 簡寫:等同 `source: fsSource(dir)` |
88
- | `outputDir` | `string` | ✱ | 簡寫:等同 `store: fsStore(dir)` |
89
- | `modelId` | `string` | | Wiki 生成模型,格式 `provider/model` |
90
- | `routerModelId` | `string` | | 路由選擇模型,預設同 `modelId` |
91
- | `timeout` | `number` | | LLM 對話逾時毫秒數,預設 `300000` (5 分鐘) |
92
- | `splitPrompt` | `string` | | 自訂 Split prompt |
93
- | `mergePrompt` | `string` | | 自訂 Merge prompt |
94
- | `routerPrompt` | `string` | | 自訂 Router prompt |
64
+ ### B. 多租戶 + SQLite(推薦正式環境)
95
65
 
96
- `source` 與 `knowledgeDir` 至少擇一;`store` 與 `outputDir` 至少擇一。
66
+ `server.js`:
97
67
 
98
- ### `wiki.build(options?)`
68
+ ```js
69
+ const fs = require('fs')
70
+ const Database = require('better-sqlite3')
71
+ const {
72
+ createTenantManager,
73
+ fsSource,
74
+ sqliteStore,
75
+ ensureWikiTables,
76
+ } = require('@jungtz/wiki-router')
77
+ const { createRouter } = require('@jungtz/ai-router')
99
78
 
100
- 建構或增量更新 Wiki 知識庫。回傳 `Promise<boolean>`。
79
+ const router = createRouter({
80
+ providers: {
81
+ 'ollama-local': { type: 'local', baseURL: 'http://localhost:11434' },
82
+ },
83
+ })
101
84
 
102
- | 選項 | 類型 | 預設 | 說明 |
103
- |------|------|------|------|
104
- | `force` | `boolean` | `false` | 為 `true` 時跳過 fingerprint 比對,無條件重新呼叫 LLM 生成 |
85
+ // DB(每個 tenant 透過 tenantId 隔離,全部存同一個 .db 檔)
86
+ const db = new Database('./data/wiki.db')
87
+ db.pragma('journal_mode = WAL')
88
+ ensureWikiTables(db)
105
89
 
106
- - 首次(`store.list()` 為空):對 JSON 來源執行 split prompt,拆分成多個 `.md`
107
- - 後續:使用 merge prompt,將新來源合併到既有檔案
108
- - **快取**:若 source 提供 `getFingerprint()` 且 store 提供 `readManifest()` / `writeManifest()`,build 會比對來源指紋,未變動時直接跳過 LLM(詳見「來源變更偵測」)
90
+ const KNOWLEDGE_DIR = './knowledge'
109
91
 
110
- ### 來源變更偵測 (Fingerprint)
92
+ const wiki = createTenantManager({
93
+ router,
94
+ modelId: 'ollama-local/gemma4:31b',
111
95
 
112
- source store 都實作對應的選用方法時,`build()` 會自動進行內容指紋比對:
96
+ // 每個 tenantId 各自建立 source / store
97
+ source: tid => fsSource(`${KNOWLEDGE_DIR}/${tid}`),
98
+ store: tid => sqliteStore({ db, tenantId: tid }),
113
99
 
114
- 1. 進入 build → 呼叫 `source.getFingerprint()` 取得當前來源指紋
115
- 2. 呼叫 `store.readManifest()` 取得上次 build 留下的指紋
116
- 3. 兩者相等且 store 已有檔案 → 跳過 LLM,回傳 `true`
117
- 4. 不等 → 走原本流程,build 成功後呼叫 `store.writeManifest()` 寫入新指紋
100
+ // buildAll() 才需要:列出所有 tenant
101
+ listTenants: () => fs.readdirSync(KNOWLEDGE_DIR),
102
+ })
118
103
 
119
- `force: true` 跳過比對直接重建。內建 `fsSource` / `fsStore` 已實作這組方法(manifest 存在 store 目錄下的 `.manifest.json`)。
104
+ ;(async () => {
105
+ await wiki.buildAll() // 並行 build 所有 tenant
106
+ const ctx = await wiki.getContext('問題', 'hotel-001') // 依 tenantId 取上下文
107
+ })()
108
+ ```
120
109
 
121
- 自訂 adapter 沒實作這些方法時,build 仍會正常執行(每次都重跑 LLM),向下相容。
110
+ 目錄結構:
111
+ ```
112
+ your-project/
113
+ ├── server.js
114
+ ├── data/
115
+ │ └── wiki.db ← 自動建立(含 wiki_files / wiki_manifests 兩張表)
116
+ └── knowledge/
117
+ ├── hotel-001/
118
+ │ └── base.json
119
+ └── hotel-002/
120
+ └── base.json
121
+ ```
122
122
 
123
- ### `wiki.getContext(prompt)`
123
+ ### C. 串進 Express server
124
124
 
125
- 根據使用者問題取得相關 Wiki 上下文。回傳 `Promise<string>`。
125
+ 接續範本 B `wiki`:
126
126
 
127
- - 若 store 為空,自動調用 `build()`
128
- - LLM 根據 `Index.md` 選擇最相關的檔案
129
- - 回傳合併後的 Markdown 內容;無相關檔案時回傳空字串
127
+ ```js
128
+ const express = require('express')
129
+ const app = express()
130
+ app.use(express.json())
130
131
 
131
- ## Adapter 介面
132
+ // 啟動時背景 build(不阻塞 listen)
133
+ app.listen(3000, () => {
134
+ wiki.buildAll().catch(err => console.error('[Wiki] init failed', err))
135
+ })
132
136
 
133
- ### `Source`
137
+ // 強制重建某 tenant
138
+ app.post('/api/wiki/rebuild', async (req, res) => {
139
+ const ok = await wiki.build(req.body.tenantId, { force: true })
140
+ res.json({ ok })
141
+ })
134
142
 
135
- ```ts
136
- interface Source {
137
- list(): Promise<string[]> // 來源 key 清單
138
- read(key: string): Promise<{ type: 'json' | 'markdown', content: string }>
139
- getFingerprint?(): Promise<Record<string, string>> // 選用:來源指紋 { key: hash }
140
- }
143
+ // 對話:用 wiki context 增強 prompt
144
+ app.post('/api/chat', async (req, res) => {
145
+ const { prompt, tenantId } = req.body
146
+ const ctx = await wiki.getContext(prompt, tenantId)
147
+ const enhanced = ctx ? `${ctx}\n\n問題:${prompt}` : prompt
148
+ // ... 把 enhanced 餵給你的 LLM
149
+ })
141
150
  ```
142
151
 
143
- ### `Store`
144
-
145
- ```ts
146
- interface Store {
147
- list(): Promise<string[]> // 已生成的 wiki 檔名(含 .md)
148
- read(filename: string): Promise<string | null> // 不存在回 null
149
- write(filename: string, content: string): Promise<void>
150
- readManifest?(): Promise<Record<string, string> | null> // 選用:讀上次 build 指紋
151
- writeManifest?(m: Record<string, string>): Promise<void> // 選用:寫本次 build 指紋
152
- }
153
- ```
152
+ ---
154
153
 
155
- `getFingerprint` / `readManifest` / `writeManifest` 是選用介面,用於來源變更偵測快取。**三者必須同時實作才會啟用快取**;缺其一則 build 仍會正常執行(每次都重跑 LLM)。
154
+ ## 換成自訂 source / store
156
155
 
157
- #### 自訂 adapter 範例(API + DB)
156
+ 當知識不在檔案系統(例如打 API)或 wiki 不存 SQLite(例如改 PostgreSQL)時,自訂 adapter:
158
157
 
159
158
  ```js
160
- source: {
161
- async list() { return ['base.json'] },
162
- async read() { /* fetch from API */ },
159
+ const customSource = (tenantId) => ({
160
+ async list() { return ['base.json'] },
161
+ async read() {
162
+ const res = await fetch(`https://api.example.com/hotels/${tenantId}/knowledge`)
163
+ return { type: 'json', content: await res.text() }
164
+ },
165
+ // 選用:用 ETag / updated_at 當指紋,比 sha256 整檔輕量
163
166
  async getFingerprint() {
164
- const res = await fetch(`/api/hotels/${hotelId}/etag`)
165
- return { 'base.json': await res.text() } // 用 ETag 當指紋
167
+ const res = await fetch(`https://api.example.com/hotels/${tenantId}/etag`)
168
+ return { 'base.json': await res.text() }
166
169
  },
167
- },
168
- store: {
169
- async list() { /* DB query */ },
170
- async read(filename) { /* ... */ },
171
- async write(filename, body) { /* ... */ },
172
- async readManifest() { return await db.wikiManifest.get(hotelId) },
173
- async writeManifest(manifest) { await db.wikiManifest.upsert(hotelId, manifest) },
174
- },
175
- ```
176
-
177
- ### 內建 adapter
178
-
179
- ```js
180
- import { fsSource, fsStore, sqliteStore, ensureWikiTables } from '@jungtz/wiki-router'
181
-
182
- // 檔案系統
183
- createWikiRouter({
184
- router,
185
- source: fsSource('./knowledge'),
186
- store: fsStore('./wiki-output'),
187
170
  })
188
171
 
189
- // SQLite (better-sqlite3 相容 db 物件)
190
- ensureWikiTables(db) // 一次性建表(idempotent)
191
- createWikiRouter({
172
+ createTenantManager({
192
173
  router,
193
- source: fsSource('./knowledge'),
194
- store: sqliteStore({ db, tenantId: 'hotel-001' }), // 多租戶用同一個 db、不同 tenantId
174
+ source: customSource,
175
+ store: tid => sqliteStore({ db, tenantId: tid }), // 仍用 SQLite 儲 wiki
176
+ listTenants: async () => {
177
+ const res = await fetch('https://api.example.com/hotels')
178
+ return (await res.json()).map(h => String(h.id))
179
+ },
195
180
  })
196
181
  ```
197
182
 
198
- `knowledgeDir` / `outputDir` 簡寫即在內部分別包成 `fsSource` / `fsStore`。
183
+ ---
184
+
185
+ ## API 參考
199
186
 
200
- #### `sqliteStore({ db, tenantId, ... })`
187
+ ### `createWikiRouter(config)` 單一 wiki 工廠
201
188
 
202
189
  | 參數 | 類型 | 必要 | 說明 |
203
190
  |------|------|------|------|
204
- | `db` | better-sqlite3 instance | ✅ | 須有 `prepare(sql)` `exec(sql)` 方法(不直接相依 better-sqlite3 套件) |
205
- | `tenantId` | `string` | | 多租戶識別字串,不同 tenant 完全隔離 |
206
- | `filesTable` | `string` | | 預設 `wiki_files` |
207
- | `manifestsTable` | `string` | | 預設 `wiki_manifests` |
208
-
209
- `sqliteStore` 內部會自動呼叫 `ensureWikiTables(db)`,但若想預先建表也可以單獨匯入使用。
210
-
211
- ## 多租戶管理 `createTenantManager`
212
-
213
- 把「快取 wiki 實例 / 並行 build dedup / buildAll / 已 build 狀態追蹤 / Index.md fallback」統一封裝。其他專案串多 wiki 時只要設定 config 即可。
214
-
215
- ```js
216
- import { createTenantManager, fsSource, sqliteStore } from '@jungtz/wiki-router'
217
- import Database from 'better-sqlite3'
218
-
219
- const db = new Database('./wiki.db')
191
+ | `router` | `AIProviderRouter` | ✅ | AI Router 實例 |
192
+ | `source` | `Source` | | Knowledge 來源 adapter(與 `knowledgeDir` 二選一) |
193
+ | `store` | `Store` | | Wiki 儲存 adapter(與 `outputDir` 二選一) |
194
+ | `knowledgeDir` | `string` | | 簡寫:等同 `source: fsSource(dir)` |
195
+ | `outputDir` | `string` | ✱ | 簡寫:等同 `store: fsStore(dir)` |
196
+ | `modelId` | `string` | | Wiki 生成模型,格式 `provider/model` |
197
+ | `routerModelId` | `string` | | 路由選擇模型,預設同 `modelId` |
198
+ | `timeout` | `number` | | LLM 對話逾時毫秒數,預設 `300000` (5 分鐘) |
199
+ | `splitPrompt` / `mergePrompt` / `routerPrompt` | `string` | | 自訂三種內建 prompt |
220
200
 
221
- const manager = createTenantManager({
222
- router,
223
- modelId: 'ollama-local/gemma4:31b',
201
+ `source`/`knowledgeDir` 至少擇一;`store`/`outputDir` 至少擇一。
224
202
 
225
- // adapter factory:每個 tenantId 各自建立 source / store
226
- source: tid => fsSource(`./knowledge/${tid}`),
227
- store: tid => sqliteStore({ db, tenantId: tid }),
203
+ 回傳:
228
204
 
229
- // 選用:buildAll() 才需要
230
- listTenants: () => fs.readdirSync('./knowledge'),
231
- })
205
+ | 方法 | 說明 |
206
+ |------|------|
207
+ | `build({ force })` | 建構或增量更新;`force: true` 跳過指紋比對 |
208
+ | `getContext(prompt)` | 依問題回傳相關 .md 合併內容;無相關回空字串 |
232
209
 
233
- await manager.buildAll() // 啟動時並行 build 所有租戶
234
- await manager.build('hotel-001', { force: true }) // 單一租戶強制重建
235
- const ctx = await manager.getContext('問題', 'hotel-001')
236
- ```
210
+ ### `createTenantManager(config)` 多租戶協調器
237
211
 
238
- ### Config
212
+ 封裝多 wiki 的快取、並行 build dedup、`buildAll`、Index.md fallback 等樣板。
239
213
 
240
214
  | 參數 | 類型 | 必要 | 說明 |
241
215
  |------|------|------|------|
242
- | `router` | `AIProviderRouter` | ✅ | AI Router 實例 |
216
+ | `router` | `AIProviderRouter` | ✅ | 同上 |
243
217
  | `source` | `(tenantId) => Source` | ✅ | Source factory |
244
- | `store` | `(tenantId) => Store` | ✅ | Store factory |
218
+ | `store` | `(tenantId) => Store` | ✅ | Store factory |
245
219
  | `listTenants` | `() => string[] \| Promise<string[]>` | | `buildAll()` 才需要 |
246
- | `autoIndex` | `boolean` | | 預設 `true`;build store 沒 Index.md 時,從現有 .md 合成一份目錄 |
220
+ | `autoIndex` | `boolean` | | 預設 `true`;build 後若 store 沒 Index.md,從現有 .md 合成 |
247
221
  | `autoIndexHeader` | `string` | | 預設 `# 知識庫目錄` |
248
222
  | `logger` | `{ log, warn, error }` | | 預設 `console` |
249
- | `modelId` / `routerModelId` / `timeout` / `*Prompt` | | | 同 `createWikiRouter` config,傳給每個 tenant 的 wiki 實例 |
223
+ | `modelId` / `routerModelId` / `timeout` / `*Prompt` | | | 同 `createWikiRouter`,傳給每個 wiki |
250
224
 
251
- ### 回傳 API
225
+ 回傳:
252
226
 
253
227
  | 方法 | 說明 |
254
228
  |------|------|
255
- | `build(tenantId, { force })` | 為單一租戶 build;同租戶並行呼叫共用同一個 promise |
256
- | `buildAll()` | 並行 build 所有租戶,使用 `Promise.allSettled` 避免單租戶失敗影響其他 |
257
- | `getContext(prompt, tenantId)` | 取得該租戶的 wiki 上下文;尚未 build 完成時回空字串(不阻塞) |
258
- | `isBuilt(tenantId)` | 該租戶是否已成功 build |
259
- | `listBuilt()` | 已 build 過的租戶清單 |
260
-
261
- ## 子模組
229
+ | `build(tenantId, { force })` | 為單一 tenant build;同 tenant 並行呼叫共用 promise |
230
+ | `buildAll()` | 並行 build 所有 tenant,使用 `allSettled` |
231
+ | `getContext(prompt, tenantId)` | 取得指定 tenant 的 wiki 上下文;尚未 build 完成時回空字串 |
232
+ | `isBuilt(tenantId)` / `listBuilt()` | build 狀態查詢 |
262
233
 
263
- ### 解析器
234
+ ### 內建 adapter
264
235
 
265
236
  ```js
266
- import { parseWikiOutput } from '@jungtz/wiki-router/parser'
237
+ const { fsSource, fsStore, sqliteStore, ensureWikiTables } = require('@jungtz/wiki-router')
267
238
 
268
- const files = parseWikiOutput(llmResponse)
269
- // [{ filename: 'Index.md', content: '...' }, ...]
239
+ fsSource('./knowledge') // .json/.md
240
+ fsStore('./wiki-output') // .md + .manifest.json
241
+ sqliteStore({ db, tenantId: 'x' }) // SQLite Store(多租戶用同一 db)
242
+ ensureWikiTables(db, { filesTable, manifestsTable }) // 一次性建表(idempotent)
270
243
  ```
271
244
 
272
- ### 提示詞模板
245
+ `sqliteStore` 內部會自動 `ensureWikiTables`,但建議啟動時先呼叫一次以避免每個 tenant 都跑 CREATE TABLE。
273
246
 
274
- 三個內建 prompt 定義於 `src/prompts/*.md`,build 時自動 inline 進 dist:
247
+ ### Adapter 介面
275
248
 
276
- | 檔案 | 匯出名稱 | 用途 |
277
- |------|----------|------|
278
- | `src/prompts/split.md` | `SPLIT_PROMPT` | 將 JSON 資料拆分為多個 `.md` 檔案 |
279
- | `src/prompts/merge.md` | `MERGE_PROMPT` | 將新內容合併到既有 Wiki 檔案 |
280
- | `src/prompts/router.md` | `ROUTER_PROMPT` | 根據使用者問題選擇相關檔案 |
249
+ ```ts
250
+ interface Source {
251
+ list(): Promise<string[]>
252
+ read(key: string): Promise<{ type: 'json' | 'markdown', content: string }>
253
+ getFingerprint?(): Promise<Record<string, string>> // 選用:啟用快取
254
+ }
281
255
 
282
- ```js
283
- import { SPLIT_PROMPT, MERGE_PROMPT, ROUTER_PROMPT } from '@jungtz/wiki-router/prompts'
256
+ interface Store {
257
+ list(): Promise<string[]>
258
+ read(filename: string): Promise<string | null>
259
+ write(filename: string, content: string): Promise<void>
260
+ readManifest?(): Promise<Record<string, string> | null> // 選用:啟用快取
261
+ writeManifest?(m: Record<string, string>): Promise<void>// 選用:啟用快取
262
+ }
284
263
  ```
285
264
 
286
- #### 自訂 Prompt
265
+ `getFingerprint` + `readManifest` + `writeManifest` **三者同時實作才會啟用快取**;缺其一則每次 build 都重跑 LLM(向下相容)。
287
266
 
288
- 於 `createWikiRouter` 設定中傳入 `splitPrompt` / `mergePrompt` / `routerPrompt` 字串可在 runtime 動態覆蓋;不傳則使用內建預設。
267
+ ---
289
268
 
290
- ```js
291
- const wiki = createWikiRouter({
292
- router,
293
- knowledgeDir: './knowledge',
294
- outputDir: './wiki-output',
295
- mergePrompt: readFileSync('./my-custom-merge.md', 'utf-8'),
296
- })
297
- ```
269
+ ## 來源變更偵測(Fingerprint Cache)
270
+
271
+ `build()` 流程:
272
+ 1. 從 `source.getFingerprint()` 算當前指紋
273
+ 2. 從 `store.readManifest()` 讀上次指紋
274
+ 3. 兩者相等且 store 已有檔案 → 跳過 LLM,回傳 `true`
275
+ 4. 不等 → 走原本流程;成功後 `store.writeManifest()` 更新指紋
276
+
277
+ `build({ force: true })` 跳過比對直接重建。
278
+
279
+ `fsStore` 把 manifest 存於 store 目錄下的 `.manifest.json`;`sqliteStore` 存於 `wiki_manifests` 表。
280
+
281
+ ---
298
282
 
299
283
  ## 範例
300
284
 
301
- - `examples/basic.js` — 檔案系統用法
302
- - `examples/multi-tenant.js` — 多租戶 (API + DB) 用法
285
+ - [`examples/basic.js`](./examples/basic.js)範本 A 完整可跑檔
286
+ - [`examples/multi-tenant.js`](./examples/multi-tenant.js)範本 B 完整可跑檔
303
287
 
304
288
  ## 互動預覽
305
289
 
306
- 內建互動式 TUI,可在終端直接問答測試 Wiki 路由效果:
290
+ 單一 wiki:
307
291
 
308
292
  ```bash
309
293
  npm run preview
310
-
311
- # 自訂路徑
312
294
  npm run preview -- --knowledge ./my-knowledge --output ./my-wiki
313
295
  ```
314
296
 
315
- 支援指令:`:list` 查看維基頁面、`:build` 重建維基、`:files` 顯示檔案、`:quit` 離開。
297
+ 多租戶(createTenantManager + sqliteStore):
316
298
 
317
- ## 發布
299
+ ```bash
300
+ npm run preview:multi
301
+ npm run preview:multi -- --knowledge ./knowledge --db ./data/wiki-preview.db
302
+ ```
318
303
 
319
- ### 自動發布(推薦)
304
+ `preview:multi` 額外支援 `:tenants`、`:tenant <id>` 切換、`:buildall` 全租戶重建。其餘指令:`:list / :build / :files / :help / :quit`。
320
305
 
321
- push 到 `master` 分支時,CI 會自動掃描自上個 tag 以來的 commit message,依 gitmoji 決定 bump 類型並 publish 到 npm:
306
+ ## 子模組匯出
322
307
 
323
- | Commit message 包含 | Bump 類型 | 例 |
324
- |--------------------|----------|----|
325
- | `:boom:` 💥 / `BREAKING CHANGE` | major | 1.0.5 → 2.0.0 |
326
- | `:sparkles:` ✨ | minor | 1.0.5 → 1.1.0 |
327
- | 其他 (`:bug:` / `:hammer:` / ...) | patch | 1.0.5 → 1.0.6 |
308
+ ```js
309
+ const { parseWikiOutput } = require('@jungtz/wiki-router/parser')
310
+ const { SPLIT_PROMPT, MERGE_PROMPT, ROUTER_PROMPT } = require('@jungtz/wiki-router/prompts')
311
+ ```
328
312
 
329
- 只要照常用 gitmoji 寫 commit,版號自己對齊語意。
313
+ ## 發布(自動)
330
314
 
331
- ### 本地手動發布腳本(備用)
315
+ push 到 master 後 CI 掃 commit message 決定 bump:
332
316
 
333
- ```bash
334
- npm run bump:patch # 版號 patch +1 並自動 git commit + tag
335
- npm run bump:minor # 版號 minor +1
336
- npm run bump:major # 版號 major +1
337
- npm run push:version # 推送 commit tag remote
338
- npm run release # bump:patch + push:version
339
- ```
317
+ | Commit 訊息 | Bump |
318
+ |---|---|
319
+ | `:boom:` 💥 / `BREAKING CHANGE` | major |
320
+ | `:sparkles:` | minor |
321
+ | 其他 (`:bug:` / `:hammer:` / ...) | patch |
340
322
 
341
323
  ## 錯誤處理
342
324
 
343
- Wiki Router 採用靜默失敗策略:若模型設定錯誤或 provider 無法連線,只會輸出警告並回傳空內容,不會中斷主流程。
325
+ 採靜默失敗:模型連不上時只 warn throw,`getContext` 回空字串、`build` 回 `false`,主流程不中斷。
344
326
 
345
- ## 相依關係
327
+ ## 相依
346
328
 
347
329
  | 套件 | 關係 |
348
330
  |------|------|
349
331
  | `@jungtz/ai-router` | Peer (optional) — LLM 調度 |
332
+ | `better-sqlite3` 等 SQLite 驅動 | 由使用者提供(傳入 `sqliteStore`),套件不直接相依 |
350
333
 
351
334
  ## 授權
352
335
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungtz/wiki-router",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "LLM Wiki 知識庫路由引擎 - 將結構化知識轉為 Markdown 維基,智慧路由查詢",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -34,6 +34,7 @@
34
34
  "dev": "rollup -c -w",
35
35
  "test": "node --test",
36
36
  "preview": "node scripts/preview.js",
37
+ "preview:multi": "node scripts/preview-multi.js",
37
38
  "lint": "eslint src/",
38
39
  "lint:fix": "eslint src/ --fix",
39
40
  "format": "prettier --write src/",