@jungtz/wiki-router 1.3.0 → 1.5.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.
- package/README.md +258 -223
- package/dist/index.cjs +126 -6
- package/dist/index.d.ts +13 -1
- package/dist/index.mjs +126 -7
- package/dist/prompts.cjs +2 -0
- package/dist/prompts.mjs +2 -1
- package/package.json +2 -1
- package/src/types.d.ts +13 -1
package/README.md
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
# @jungtz/wiki-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把結構化知識(JSON / Markdown)丟給 LLM 拆成 wiki,再依使用者問題自動挑相關 .md 當上下文回傳。Build 完還會順便產一段「角色設定(persona)」給上層 chat 用。
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/js/@jungtz%2Fwiki-router)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
+
```
|
|
9
|
+
┌─→ getContext(prompt) → LLM 路由 → 相關 .md
|
|
10
|
+
知識來源 → build() ─┤
|
|
11
|
+
└─→ getPersona() → 自動產出的角色設定字串
|
|
12
|
+
```
|
|
13
|
+
|
|
8
14
|
## 安裝
|
|
9
15
|
|
|
10
16
|
```bash
|
|
11
|
-
npm install @jungtz/wiki-router
|
|
17
|
+
npm install @jungtz/wiki-router @jungtz/ai-router
|
|
18
|
+
# 多租戶 + DB 儲存(範本 B)需要:
|
|
19
|
+
npm install better-sqlite3
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
知識來源 -> build() -> LLM 生成 Wiki -> getContext(prompt) -> LLM 路由 -> 相關上下文
|
|
18
|
-
```
|
|
22
|
+
---
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
2. **getContext(prompt)**: 根據使用者問題,由 LLM 從 Index.md 中選擇相關檔案,回傳合併後的上下文
|
|
24
|
+
## 整合範本(複製即用)
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
### A. 單一 wiki(檔案系統,最小整合)
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
`server.js`:
|
|
26
29
|
|
|
27
30
|
```js
|
|
28
31
|
const { createWikiRouter } = require('@jungtz/wiki-router')
|
|
@@ -36,317 +39,349 @@ const router = createRouter({
|
|
|
36
39
|
|
|
37
40
|
const wiki = createWikiRouter({
|
|
38
41
|
router,
|
|
39
|
-
knowledgeDir: './knowledge',
|
|
40
|
-
outputDir: './wiki-output',
|
|
42
|
+
knowledgeDir: './knowledge', // 放 .json / .md 知識來源
|
|
43
|
+
outputDir: './wiki-output', // 生成的 .md 維基
|
|
41
44
|
modelId: 'ollama-local/gemma4:31b',
|
|
42
45
|
})
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
;(async () => {
|
|
48
|
+
await wiki.build() // 啟動時建構(指紋未變動會自動跳過)
|
|
49
|
+
const ctx = await wiki.getContext('住宿規定?')
|
|
50
|
+
const persona = await wiki.getPersona() // build 完自動產出的角色設定
|
|
51
|
+
console.log(persona)
|
|
52
|
+
})()
|
|
46
53
|
```
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
55
|
+
目錄結構:
|
|
56
|
+
```
|
|
57
|
+
your-project/
|
|
58
|
+
├── server.js
|
|
59
|
+
├── knowledge/
|
|
60
|
+
│ └── base.json ← 知識來源
|
|
61
|
+
└── wiki-output/ ← 自動產生
|
|
62
|
+
├── Index.md
|
|
63
|
+
├── About.md
|
|
64
|
+
├── _persona.md ← build 完自動產出(底線前綴排除於路由)
|
|
65
|
+
└── .manifest.json ← 來源指紋(自動寫入)
|
|
76
66
|
```
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
### B. 多租戶 + SQLite(推薦正式環境)
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
`server.js`:
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
| `splitPrompt` | `string` | | 自訂 Split prompt |
|
|
93
|
-
| `mergePrompt` | `string` | | 自訂 Merge prompt |
|
|
94
|
-
| `routerPrompt` | `string` | | 自訂 Router prompt |
|
|
95
|
-
|
|
96
|
-
✱ `source` 與 `knowledgeDir` 至少擇一;`store` 與 `outputDir` 至少擇一。
|
|
97
|
-
|
|
98
|
-
### `wiki.build(options?)`
|
|
72
|
+
```js
|
|
73
|
+
const fs = require('fs')
|
|
74
|
+
const Database = require('better-sqlite3')
|
|
75
|
+
const {
|
|
76
|
+
createTenantManager,
|
|
77
|
+
fsSource,
|
|
78
|
+
sqliteStore,
|
|
79
|
+
ensureWikiTables,
|
|
80
|
+
} = require('@jungtz/wiki-router')
|
|
81
|
+
const { createRouter } = require('@jungtz/ai-router')
|
|
99
82
|
|
|
100
|
-
|
|
83
|
+
const router = createRouter({
|
|
84
|
+
providers: {
|
|
85
|
+
'ollama-local': { type: 'local', baseURL: 'http://localhost:11434' },
|
|
86
|
+
},
|
|
87
|
+
})
|
|
101
88
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
89
|
+
// 開 DB(每個 tenant 透過 tenantId 隔離,全部存同一個 .db 檔)
|
|
90
|
+
const db = new Database('./data/wiki.db')
|
|
91
|
+
db.pragma('journal_mode = WAL')
|
|
92
|
+
ensureWikiTables(db)
|
|
105
93
|
|
|
106
|
-
|
|
107
|
-
- 後續:使用 merge prompt,將新來源合併到既有檔案
|
|
108
|
-
- **快取**:若 source 提供 `getFingerprint()` 且 store 提供 `readManifest()` / `writeManifest()`,build 會比對來源指紋,未變動時直接跳過 LLM(詳見「來源變更偵測」)
|
|
94
|
+
const KNOWLEDGE_DIR = './knowledge'
|
|
109
95
|
|
|
110
|
-
|
|
96
|
+
const wiki = createTenantManager({
|
|
97
|
+
router,
|
|
98
|
+
modelId: 'ollama-local/gemma4:31b',
|
|
111
99
|
|
|
112
|
-
|
|
100
|
+
// 每個 tenantId 各自建立 source / store
|
|
101
|
+
source: tid => fsSource(`${KNOWLEDGE_DIR}/${tid}`),
|
|
102
|
+
store: tid => sqliteStore({ db, tenantId: tid }),
|
|
113
103
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
4. 不等 → 走原本流程,build 成功後呼叫 `store.writeManifest()` 寫入新指紋
|
|
104
|
+
// buildAll() 才需要:列出所有 tenant
|
|
105
|
+
listTenants: () => fs.readdirSync(KNOWLEDGE_DIR),
|
|
106
|
+
})
|
|
118
107
|
|
|
119
|
-
|
|
108
|
+
;(async () => {
|
|
109
|
+
await wiki.buildAll() // 並行 build 所有 tenant
|
|
110
|
+
const ctx = await wiki.getContext('問題', 'hotel-001') // 依 tenantId 取上下文
|
|
111
|
+
const persona = await wiki.getPersona('hotel-001') // 自動產出的角色設定
|
|
112
|
+
})()
|
|
113
|
+
```
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
目錄結構:
|
|
116
|
+
```
|
|
117
|
+
your-project/
|
|
118
|
+
├── server.js
|
|
119
|
+
├── data/
|
|
120
|
+
│ └── wiki.db ← 自動建立(含 wiki_files / wiki_manifests / wiki_personas 三張表)
|
|
121
|
+
└── knowledge/
|
|
122
|
+
├── hotel-001/
|
|
123
|
+
│ └── base.json
|
|
124
|
+
└── hotel-002/
|
|
125
|
+
└── base.json
|
|
126
|
+
```
|
|
122
127
|
|
|
123
|
-
###
|
|
128
|
+
### C. 串進 Express server
|
|
124
129
|
|
|
125
|
-
|
|
130
|
+
接續範本 B 的 `wiki`:
|
|
126
131
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
```js
|
|
133
|
+
const express = require('express')
|
|
134
|
+
const app = express()
|
|
135
|
+
app.use(express.json())
|
|
130
136
|
|
|
131
|
-
|
|
137
|
+
// 啟動時背景 build(不阻塞 listen)
|
|
138
|
+
app.listen(3000, () => {
|
|
139
|
+
wiki.buildAll().catch(err => console.error('[Wiki] init failed', err))
|
|
140
|
+
})
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
// 強制重建某 tenant
|
|
143
|
+
app.post('/api/wiki/rebuild', async (req, res) => {
|
|
144
|
+
const ok = await wiki.build(req.body.tenantId, { force: true })
|
|
145
|
+
res.json({ ok })
|
|
146
|
+
})
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
// 對話:用 wiki context 增強 prompt,並用 persona 當系統提示
|
|
149
|
+
app.post('/api/chat', async (req, res) => {
|
|
150
|
+
const { prompt, tenantId } = req.body
|
|
151
|
+
const [ctx, persona] = await Promise.all([
|
|
152
|
+
wiki.getContext(prompt, tenantId),
|
|
153
|
+
wiki.getPersona(tenantId),
|
|
154
|
+
])
|
|
155
|
+
const system = persona || '你是一位專業的 AI 助理。'
|
|
156
|
+
const enhanced = ctx ? `${ctx}\n\n問題:${prompt}` : prompt
|
|
157
|
+
// ... 把 system + enhanced 餵給你的 LLM
|
|
158
|
+
})
|
|
141
159
|
```
|
|
142
160
|
|
|
143
|
-
|
|
161
|
+
---
|
|
144
162
|
|
|
145
|
-
|
|
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
|
-
```
|
|
163
|
+
## 換成自訂 source / store
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#### 自訂 adapter 範例(API + DB)
|
|
165
|
+
當知識不在檔案系統(例如打 API)或 wiki 不存 SQLite(例如改 PostgreSQL)時,自訂 adapter:
|
|
158
166
|
|
|
159
167
|
```js
|
|
160
|
-
|
|
161
|
-
async list()
|
|
162
|
-
async read()
|
|
168
|
+
const customSource = (tenantId) => ({
|
|
169
|
+
async list() { return ['base.json'] },
|
|
170
|
+
async read() {
|
|
171
|
+
const res = await fetch(`https://api.example.com/hotels/${tenantId}/knowledge`)
|
|
172
|
+
return { type: 'json', content: await res.text() }
|
|
173
|
+
},
|
|
174
|
+
// 選用:用 ETag / updated_at 當指紋,比 sha256 整檔輕量
|
|
163
175
|
async getFingerprint() {
|
|
164
|
-
const res = await fetch(
|
|
165
|
-
return { 'base.json': await res.text() }
|
|
176
|
+
const res = await fetch(`https://api.example.com/hotels/${tenantId}/etag`)
|
|
177
|
+
return { 'base.json': await res.text() }
|
|
166
178
|
},
|
|
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
179
|
})
|
|
188
180
|
|
|
189
|
-
|
|
190
|
-
ensureWikiTables(db) // 一次性建表(idempotent)
|
|
191
|
-
createWikiRouter({
|
|
181
|
+
createTenantManager({
|
|
192
182
|
router,
|
|
193
|
-
source:
|
|
194
|
-
store: sqliteStore({ db, tenantId:
|
|
183
|
+
source: customSource,
|
|
184
|
+
store: tid => sqliteStore({ db, tenantId: tid }), // 仍用 SQLite 儲 wiki
|
|
185
|
+
listTenants: async () => {
|
|
186
|
+
const res = await fetch('https://api.example.com/hotels')
|
|
187
|
+
return (await res.json()).map(h => String(h.id))
|
|
188
|
+
},
|
|
195
189
|
})
|
|
196
190
|
```
|
|
197
191
|
|
|
198
|
-
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## API 參考
|
|
199
195
|
|
|
200
|
-
|
|
196
|
+
### `createWikiRouter(config)` — 單一 wiki 工廠
|
|
201
197
|
|
|
202
198
|
| 參數 | 類型 | 必要 | 說明 |
|
|
203
199
|
|------|------|------|------|
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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')
|
|
200
|
+
| `router` | `AIProviderRouter` | ✅ | AI Router 實例 |
|
|
201
|
+
| `source` | `Source` | ✱ | Knowledge 來源 adapter(與 `knowledgeDir` 二選一) |
|
|
202
|
+
| `store` | `Store` | ✱ | Wiki 儲存 adapter(與 `outputDir` 二選一) |
|
|
203
|
+
| `knowledgeDir` | `string` | ✱ | 簡寫:等同 `source: fsSource(dir)` |
|
|
204
|
+
| `outputDir` | `string` | ✱ | 簡寫:等同 `store: fsStore(dir)` |
|
|
205
|
+
| `modelId` | `string` | | Wiki 生成模型,格式 `provider/model` |
|
|
206
|
+
| `routerModelId` | `string` | | 路由選擇模型,預設同 `modelId` |
|
|
207
|
+
| `timeout` | `number` | | LLM 對話逾時毫秒數,預設 `300000` (5 分鐘) |
|
|
208
|
+
| `splitPrompt` / `mergePrompt` / `routerPrompt` / `personaPrompt` | `string` | | 自訂四種內建 prompt |
|
|
209
|
+
| `personaSampleSize` | `number` | | 產 persona 時抽取的內容樣本字元上限,預設 `4000` |
|
|
220
210
|
|
|
221
|
-
|
|
222
|
-
router,
|
|
223
|
-
modelId: 'ollama-local/gemma4:31b',
|
|
211
|
+
✱ `source`/`knowledgeDir` 至少擇一;`store`/`outputDir` 至少擇一。
|
|
224
212
|
|
|
225
|
-
|
|
226
|
-
source: tid => fsSource(`./knowledge/${tid}`),
|
|
227
|
-
store: tid => sqliteStore({ db, tenantId: tid }),
|
|
213
|
+
回傳:
|
|
228
214
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
})
|
|
215
|
+
| 方法 | 說明 |
|
|
216
|
+
|------|------|
|
|
217
|
+
| `build({ force, skipPersona })` | 建構或增量更新;`force: true` 跳過指紋比對;`skipPersona: true` 跳過自動產 persona |
|
|
218
|
+
| `getContext(prompt)` | 依問題回傳相關 .md 合併內容;無相關回空字串 |
|
|
219
|
+
| `buildPersona({ force })` | 單獨重建 persona(不重建 wiki);store 不支援時 noop |
|
|
220
|
+
| `getPersona()` | 取出目前儲存的 persona;store 不支援或尚未產出回 `null` |
|
|
232
221
|
|
|
233
|
-
|
|
234
|
-
await manager.build('hotel-001', { force: true }) // 單一租戶強制重建
|
|
235
|
-
const ctx = await manager.getContext('問題', 'hotel-001')
|
|
236
|
-
```
|
|
222
|
+
### `createTenantManager(config)` — 多租戶協調器
|
|
237
223
|
|
|
238
|
-
|
|
224
|
+
封裝多 wiki 的快取、並行 build dedup、`buildAll`、Index.md fallback 等樣板。
|
|
239
225
|
|
|
240
226
|
| 參數 | 類型 | 必要 | 說明 |
|
|
241
227
|
|------|------|------|------|
|
|
242
|
-
| `router` | `AIProviderRouter` | ✅ |
|
|
228
|
+
| `router` | `AIProviderRouter` | ✅ | 同上 |
|
|
243
229
|
| `source` | `(tenantId) => Source` | ✅ | Source factory |
|
|
244
|
-
| `store`
|
|
230
|
+
| `store` | `(tenantId) => Store` | ✅ | Store factory |
|
|
245
231
|
| `listTenants` | `() => string[] \| Promise<string[]>` | | `buildAll()` 才需要 |
|
|
246
|
-
| `autoIndex` | `boolean` | | 預設 `true`;build
|
|
232
|
+
| `autoIndex` | `boolean` | | 預設 `true`;build 後若 store 沒 Index.md,從現有 .md 合成 |
|
|
247
233
|
| `autoIndexHeader` | `string` | | 預設 `# 知識庫目錄` |
|
|
248
234
|
| `logger` | `{ log, warn, error }` | | 預設 `console` |
|
|
249
|
-
| `modelId` / `routerModelId` / `timeout` / `*Prompt` | | | 同 `createWikiRouter
|
|
235
|
+
| `modelId` / `routerModelId` / `timeout` / `*Prompt` / `personaSampleSize` | | | 同 `createWikiRouter`,傳給每個 wiki |
|
|
250
236
|
|
|
251
|
-
|
|
237
|
+
回傳:
|
|
252
238
|
|
|
253
239
|
| 方法 | 說明 |
|
|
254
240
|
|------|------|
|
|
255
|
-
| `build(tenantId, { force })` |
|
|
256
|
-
| `buildAll()` | 並行 build
|
|
257
|
-
| `getContext(prompt, tenantId)` |
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
241
|
+
| `build(tenantId, { force, skipPersona })` | 為單一 tenant build;同 tenant 並行呼叫共用 promise |
|
|
242
|
+
| `buildAll()` | 並行 build 所有 tenant,使用 `allSettled` |
|
|
243
|
+
| `getContext(prompt, tenantId)` | 取得指定 tenant 的 wiki 上下文;尚未 build 完成時回空字串 |
|
|
244
|
+
| `getPersona(tenantId)` | 取出指定 tenant 的 persona;尚未產出回 `null` |
|
|
245
|
+
| `buildPersona(tenantId, { force })` | 為指定 tenant 單獨重建 persona |
|
|
246
|
+
| `isBuilt(tenantId)` / `listBuilt()` | build 狀態查詢 |
|
|
260
247
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
### 解析器
|
|
248
|
+
### 內建 adapter
|
|
264
249
|
|
|
265
250
|
```js
|
|
266
|
-
|
|
251
|
+
const { fsSource, fsStore, sqliteStore, ensureWikiTables } = require('@jungtz/wiki-router')
|
|
267
252
|
|
|
268
|
-
|
|
269
|
-
//
|
|
253
|
+
fsSource('./knowledge') // 讀 .json/.md
|
|
254
|
+
fsStore('./wiki-output') // 寫 .md + .manifest.json + _persona.md
|
|
255
|
+
sqliteStore({ db, tenantId: 'x' }) // SQLite Store(多租戶用同一 db)
|
|
256
|
+
ensureWikiTables(db, { filesTable, manifestsTable, personasTable }) // 一次性建表(idempotent)
|
|
270
257
|
```
|
|
271
258
|
|
|
272
|
-
|
|
259
|
+
`sqliteStore` 內部會自動 `ensureWikiTables`,但建議啟動時先呼叫一次以避免每個 tenant 都跑 CREATE TABLE。
|
|
273
260
|
|
|
274
|
-
|
|
261
|
+
`fsStore.list()` 會自動排除以底線開頭的檔案(如 `_persona.md`),避免污染路由與 Index 自動生成。
|
|
275
262
|
|
|
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` | 根據使用者問題選擇相關檔案 |
|
|
263
|
+
### Adapter 介面
|
|
281
264
|
|
|
282
|
-
```
|
|
283
|
-
|
|
265
|
+
```ts
|
|
266
|
+
interface Source {
|
|
267
|
+
list(): Promise<string[]>
|
|
268
|
+
read(key: string): Promise<{ type: 'json' | 'markdown', content: string }>
|
|
269
|
+
getFingerprint?(): Promise<Record<string, string>> // 選用:啟用快取
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface Store {
|
|
273
|
+
list(): Promise<string[]>
|
|
274
|
+
read(filename: string): Promise<string | null>
|
|
275
|
+
write(filename: string, content: string): Promise<void>
|
|
276
|
+
readManifest?(): Promise<Record<string, string> | null> // 選用:啟用快取
|
|
277
|
+
writeManifest?(m: Record<string, string>): Promise<void>// 選用:啟用快取
|
|
278
|
+
readPersona?(): Promise<string | null> // 選用:啟用 persona 自動生成
|
|
279
|
+
writePersona?(persona: string): Promise<void> // 選用:啟用 persona 自動生成
|
|
280
|
+
}
|
|
284
281
|
```
|
|
285
282
|
|
|
286
|
-
|
|
283
|
+
選用方法成對啟用各自的功能;缺其一則該功能 noop,主流程不受影響:
|
|
284
|
+
|
|
285
|
+
| 功能 | 必要的選用方法 |
|
|
286
|
+
|---|---|
|
|
287
|
+
| Fingerprint 快取 | `getFingerprint` + `readManifest` + `writeManifest` |
|
|
288
|
+
| Persona 自動產出 | `readPersona` + `writePersona` |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 來源變更偵測(Fingerprint Cache)
|
|
293
|
+
|
|
294
|
+
`build()` 流程:
|
|
295
|
+
1. 從 `source.getFingerprint()` 算當前指紋
|
|
296
|
+
2. 從 `store.readManifest()` 讀上次指紋
|
|
297
|
+
3. 兩者相等且 store 已有檔案 → 跳過 LLM,回傳 `true`
|
|
298
|
+
4. 不等 → 走原本流程;成功後 `store.writeManifest()` 更新指紋
|
|
299
|
+
|
|
300
|
+
`build({ force: true })` 跳過比對直接重建。
|
|
301
|
+
|
|
302
|
+
`fsStore` 把 manifest 存於 store 目錄下的 `.manifest.json`;`sqliteStore` 存於 `wiki_manifests` 表。
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Persona 自動生成
|
|
307
|
+
|
|
308
|
+
`build()` 成功後若 store 同時實作 `readPersona` / `writePersona`,會自動跑一輪 LLM 產出一段角色設定(給上層 chat 當系統提示用)。流程:
|
|
287
309
|
|
|
288
|
-
|
|
310
|
+
1. 從 store 讀取 `Index.md`(沒有就跳過)
|
|
311
|
+
2. 從非 Index、非底線前綴的 .md 抽樣(合計上限 `personaSampleSize`,預設 4000 字元)
|
|
312
|
+
3. 把目錄 + 樣本餵給 `PERSONA_PROMPT` → LLM 輸出純文字角色描述
|
|
313
|
+
4. 透過 `store.writePersona()` 落地
|
|
314
|
+
|
|
315
|
+
預設 prompt 要求模型:
|
|
316
|
+
- 120 字內、第二人稱「你是 …」開頭
|
|
317
|
+
- 必須使用知識庫實際出現的單位名稱、業種、語氣
|
|
318
|
+
- 嚴禁編造、嚴禁寫成介紹文/宣傳文
|
|
319
|
+
|
|
320
|
+
讀取與覆蓋:
|
|
289
321
|
|
|
290
322
|
```js
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
mergePrompt: readFileSync('./my-custom-merge.md', 'utf-8'),
|
|
296
|
-
})
|
|
323
|
+
const persona = await wiki.getPersona() // 讀目前的
|
|
324
|
+
await wiki.buildPersona({ force: true }) // 單獨重建(不重建 wiki)
|
|
325
|
+
await wiki.build({ skipPersona: true }) // build 時不自動產 persona
|
|
326
|
+
const wiki2 = createWikiRouter({ ..., personaPrompt: '...' }) // 換成自己的 prompt
|
|
297
327
|
```
|
|
298
328
|
|
|
329
|
+
儲存位置:`fsStore` 寫 `_persona.md`(底線前綴自動排除於 `list()` 與路由);`sqliteStore` 寫 `wiki_personas` 表(`tenant_id` PRIMARY KEY)。
|
|
330
|
+
|
|
331
|
+
不需要 persona 時:自訂 store 不實作 `readPersona/writePersona`,或每次 `build({ skipPersona: true })`。
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
299
335
|
## 範例
|
|
300
336
|
|
|
301
|
-
- `examples/basic.js` —
|
|
302
|
-
- `examples/multi-tenant.js` —
|
|
337
|
+
- [`examples/basic.js`](./examples/basic.js) — 範本 A 完整可跑檔
|
|
338
|
+
- [`examples/multi-tenant.js`](./examples/multi-tenant.js) — 範本 B 完整可跑檔
|
|
303
339
|
|
|
304
340
|
## 互動預覽
|
|
305
341
|
|
|
306
|
-
|
|
342
|
+
單一 wiki:
|
|
307
343
|
|
|
308
344
|
```bash
|
|
309
345
|
npm run preview
|
|
310
|
-
|
|
311
|
-
# 自訂路徑
|
|
312
346
|
npm run preview -- --knowledge ./my-knowledge --output ./my-wiki
|
|
313
347
|
```
|
|
314
348
|
|
|
315
|
-
|
|
349
|
+
多租戶(createTenantManager + sqliteStore):
|
|
316
350
|
|
|
317
|
-
|
|
351
|
+
```bash
|
|
352
|
+
npm run preview:multi
|
|
353
|
+
npm run preview:multi -- --knowledge ./knowledge --db ./data/wiki-preview.db
|
|
354
|
+
```
|
|
318
355
|
|
|
319
|
-
|
|
356
|
+
`preview:multi` 額外支援 `:tenants`、`:tenant <id>` 切換、`:buildall` 全租戶重建。其餘指令:`:list / :build / :files / :help / :quit`。
|
|
320
357
|
|
|
321
|
-
|
|
358
|
+
## 子模組匯出
|
|
322
359
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
| 其他 (`:bug:` / `:hammer:` / ...) | patch | 1.0.5 → 1.0.6 |
|
|
360
|
+
```js
|
|
361
|
+
const { parseWikiOutput } = require('@jungtz/wiki-router/parser')
|
|
362
|
+
const { SPLIT_PROMPT, MERGE_PROMPT, ROUTER_PROMPT, PERSONA_PROMPT } = require('@jungtz/wiki-router/prompts')
|
|
363
|
+
```
|
|
328
364
|
|
|
329
|
-
|
|
365
|
+
## 發布(自動)
|
|
330
366
|
|
|
331
|
-
|
|
367
|
+
push 到 master 後 CI 掃 commit message 決定 bump:
|
|
332
368
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
npm run release # bump:patch + push:version
|
|
339
|
-
```
|
|
369
|
+
| Commit 訊息 | Bump |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `:boom:` 💥 / `BREAKING CHANGE` | major |
|
|
372
|
+
| `:sparkles:` ✨ | minor |
|
|
373
|
+
| 其他 (`:bug:` / `:hammer:` / ...) | patch |
|
|
340
374
|
|
|
341
375
|
## 錯誤處理
|
|
342
376
|
|
|
343
|
-
|
|
377
|
+
採靜默失敗:模型連不上時只 warn 不 throw,`getContext` / `getPersona` 回 `null` 或空字串、`build` 回 `false`,主流程不中斷。Persona 生成失敗不影響 `build` 主結果(仍回 `true`)。
|
|
344
378
|
|
|
345
|
-
##
|
|
379
|
+
## 相依
|
|
346
380
|
|
|
347
381
|
| 套件 | 關係 |
|
|
348
382
|
|------|------|
|
|
349
383
|
| `@jungtz/ai-router` | Peer (optional) — LLM 調度 |
|
|
384
|
+
| `better-sqlite3` 等 SQLite 驅動 | 由使用者提供(傳入 `sqliteStore`),套件不直接相依 |
|
|
350
385
|
|
|
351
386
|
## 授權
|
|
352
387
|
|