@jungtz/wiki-router 1.0.5 → 1.1.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 +59 -2
- package/dist/index.cjs +99 -15
- package/dist/index.d.ts +9 -1
- package/dist/index.mjs +99 -15
- package/package.json +1 -1
- package/src/types.d.ts +9 -1
package/README.md
CHANGED
|
@@ -95,12 +95,30 @@ const wikiB = wikiOf(7) // 旅館 B
|
|
|
95
95
|
|
|
96
96
|
✱ `source` 與 `knowledgeDir` 至少擇一;`store` 與 `outputDir` 至少擇一。
|
|
97
97
|
|
|
98
|
-
### `wiki.build()`
|
|
98
|
+
### `wiki.build(options?)`
|
|
99
99
|
|
|
100
100
|
建構或增量更新 Wiki 知識庫。回傳 `Promise<boolean>`。
|
|
101
101
|
|
|
102
|
+
| 選項 | 類型 | 預設 | 說明 |
|
|
103
|
+
|------|------|------|------|
|
|
104
|
+
| `force` | `boolean` | `false` | 為 `true` 時跳過 fingerprint 比對,無條件重新呼叫 LLM 生成 |
|
|
105
|
+
|
|
102
106
|
- 首次(`store.list()` 為空):對 JSON 來源執行 split prompt,拆分成多個 `.md`
|
|
103
107
|
- 後續:使用 merge prompt,將新來源合併到既有檔案
|
|
108
|
+
- **快取**:若 source 提供 `getFingerprint()` 且 store 提供 `readManifest()` / `writeManifest()`,build 會比對來源指紋,未變動時直接跳過 LLM(詳見「來源變更偵測」)
|
|
109
|
+
|
|
110
|
+
### 來源變更偵測 (Fingerprint)
|
|
111
|
+
|
|
112
|
+
當 source 與 store 都實作對應的選用方法時,`build()` 會自動進行內容指紋比對:
|
|
113
|
+
|
|
114
|
+
1. 進入 build → 呼叫 `source.getFingerprint()` 取得當前來源指紋
|
|
115
|
+
2. 呼叫 `store.readManifest()` 取得上次 build 留下的指紋
|
|
116
|
+
3. 兩者相等且 store 已有檔案 → 跳過 LLM,回傳 `true`
|
|
117
|
+
4. 不等 → 走原本流程,build 成功後呼叫 `store.writeManifest()` 寫入新指紋
|
|
118
|
+
|
|
119
|
+
`force: true` 跳過比對直接重建。內建 `fsSource` / `fsStore` 已實作這組方法(manifest 存在 store 目錄下的 `.manifest.json`)。
|
|
120
|
+
|
|
121
|
+
自訂 adapter 沒實作這些方法時,build 仍會正常執行(每次都重跑 LLM),向下相容。
|
|
104
122
|
|
|
105
123
|
### `wiki.getContext(prompt)`
|
|
106
124
|
|
|
@@ -118,6 +136,7 @@ const wikiB = wikiOf(7) // 旅館 B
|
|
|
118
136
|
interface Source {
|
|
119
137
|
list(): Promise<string[]> // 來源 key 清單
|
|
120
138
|
read(key: string): Promise<{ type: 'json' | 'markdown', content: string }>
|
|
139
|
+
getFingerprint?(): Promise<Record<string, string>> // 選用:來源指紋 { key: hash }
|
|
121
140
|
}
|
|
122
141
|
```
|
|
123
142
|
|
|
@@ -128,9 +147,33 @@ interface Store {
|
|
|
128
147
|
list(): Promise<string[]> // 已生成的 wiki 檔名(含 .md)
|
|
129
148
|
read(filename: string): Promise<string | null> // 不存在回 null
|
|
130
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 指紋
|
|
131
152
|
}
|
|
132
153
|
```
|
|
133
154
|
|
|
155
|
+
`getFingerprint` / `readManifest` / `writeManifest` 是選用介面,用於來源變更偵測快取。**三者必須同時實作才會啟用快取**;缺其一則 build 仍會正常執行(每次都重跑 LLM)。
|
|
156
|
+
|
|
157
|
+
#### 自訂 adapter 範例(API + DB)
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
source: {
|
|
161
|
+
async list() { return ['base.json'] },
|
|
162
|
+
async read() { /* fetch from API */ },
|
|
163
|
+
async getFingerprint() {
|
|
164
|
+
const res = await fetch(`/api/hotels/${hotelId}/etag`)
|
|
165
|
+
return { 'base.json': await res.text() } // 用 ETag 當指紋
|
|
166
|
+
},
|
|
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
|
+
|
|
134
177
|
### 內建 adapter
|
|
135
178
|
|
|
136
179
|
```js
|
|
@@ -201,7 +244,21 @@ npm run preview -- --knowledge ./my-knowledge --output ./my-wiki
|
|
|
201
244
|
|
|
202
245
|
支援指令:`:list` 查看維基頁面、`:build` 重建維基、`:files` 顯示檔案、`:quit` 離開。
|
|
203
246
|
|
|
204
|
-
##
|
|
247
|
+
## 發布
|
|
248
|
+
|
|
249
|
+
### 自動發布(推薦)
|
|
250
|
+
|
|
251
|
+
push 到 `master` 分支時,CI 會自動掃描自上個 tag 以來的 commit message,依 gitmoji 決定 bump 類型並 publish 到 npm:
|
|
252
|
+
|
|
253
|
+
| Commit message 包含 | Bump 類型 | 例 |
|
|
254
|
+
|--------------------|----------|----|
|
|
255
|
+
| `:boom:` 💥 / `BREAKING CHANGE` | major | 1.0.5 → 2.0.0 |
|
|
256
|
+
| `:sparkles:` ✨ | minor | 1.0.5 → 1.1.0 |
|
|
257
|
+
| 其他 (`:bug:` / `:hammer:` / ...) | patch | 1.0.5 → 1.0.6 |
|
|
258
|
+
|
|
259
|
+
只要照常用 gitmoji 寫 commit,版號自己對齊語意。
|
|
260
|
+
|
|
261
|
+
### 本地手動發布腳本(備用)
|
|
205
262
|
|
|
206
263
|
```bash
|
|
207
264
|
npm run bump:patch # 版號 patch +1 並自動 git commit + tag
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* LLM Wiki 提示詞 — 從 src/prompts/*.md 檔案載入
|
|
@@ -97,6 +98,8 @@ function parseWikiOutput(output) {
|
|
|
97
98
|
*/
|
|
98
99
|
|
|
99
100
|
|
|
101
|
+
const MANIFEST_FILE = '.manifest.json';
|
|
102
|
+
|
|
100
103
|
/**
|
|
101
104
|
* 建立讀取本地檔案的 Source adapter
|
|
102
105
|
* @param {string} dir - knowledge 來源目錄
|
|
@@ -104,19 +107,30 @@ function parseWikiOutput(output) {
|
|
|
104
107
|
*/
|
|
105
108
|
function fsSource(dir) {
|
|
106
109
|
const resolved = path.resolve(dir);
|
|
110
|
+
async function list() {
|
|
111
|
+
if (!fs.existsSync(resolved)) return []
|
|
112
|
+
return fs
|
|
113
|
+
.readdirSync(resolved)
|
|
114
|
+
.filter(f => f.endsWith('.json') || f.endsWith('.md'))
|
|
115
|
+
.sort()
|
|
116
|
+
}
|
|
117
|
+
async function read(key) {
|
|
118
|
+
const filePath = path.join(resolved, key);
|
|
119
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
120
|
+
const type = key.endsWith('.json') ? 'json' : 'markdown';
|
|
121
|
+
return { type, content }
|
|
122
|
+
}
|
|
107
123
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const type = key.endsWith('.json') ? 'json' : 'markdown';
|
|
119
|
-
return { type, content }
|
|
124
|
+
list,
|
|
125
|
+
read,
|
|
126
|
+
async getFingerprint() {
|
|
127
|
+
const keys = await list();
|
|
128
|
+
const fp = {};
|
|
129
|
+
for (const key of keys) {
|
|
130
|
+
const { content } = await read(key);
|
|
131
|
+
fp[key] = crypto.createHash('sha256').update(content).digest('hex');
|
|
132
|
+
}
|
|
133
|
+
return fp
|
|
120
134
|
},
|
|
121
135
|
}
|
|
122
136
|
}
|
|
@@ -145,6 +159,23 @@ function fsStore(dir) {
|
|
|
145
159
|
ensure();
|
|
146
160
|
fs.writeFileSync(path.join(resolved, filename), content);
|
|
147
161
|
},
|
|
162
|
+
async readManifest() {
|
|
163
|
+
const filePath = path.join(resolved, MANIFEST_FILE);
|
|
164
|
+
if (!fs.existsSync(filePath)) return null
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
167
|
+
} catch {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async writeManifest(manifest) {
|
|
172
|
+
ensure();
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(resolved, MANIFEST_FILE),
|
|
175
|
+
JSON.stringify(manifest, null, 2),
|
|
176
|
+
'utf-8'
|
|
177
|
+
);
|
|
178
|
+
},
|
|
148
179
|
}
|
|
149
180
|
}
|
|
150
181
|
|
|
@@ -268,11 +299,28 @@ function createWikiRouter(config) {
|
|
|
268
299
|
}
|
|
269
300
|
}
|
|
270
301
|
|
|
302
|
+
/**
|
|
303
|
+
* 比對兩個 fingerprint 字典是否完全一致
|
|
304
|
+
* @param {Record<string,string>|null|undefined} a
|
|
305
|
+
* @param {Record<string,string>|null|undefined} b
|
|
306
|
+
* @returns {boolean}
|
|
307
|
+
*/
|
|
308
|
+
function fingerprintsEqual(a, b) {
|
|
309
|
+
if (!a || !b) return false
|
|
310
|
+
const ak = Object.keys(a);
|
|
311
|
+
const bk = Object.keys(b);
|
|
312
|
+
if (ak.length !== bk.length) return false
|
|
313
|
+
return ak.every(k => a[k] === b[k])
|
|
314
|
+
}
|
|
315
|
+
|
|
271
316
|
/**
|
|
272
317
|
* 建構或更新 LLM Wiki 知識庫
|
|
318
|
+
* @param {{ force?: boolean }} [options]
|
|
319
|
+
* force=true 時跳過 fingerprint 比對,無條件重建
|
|
273
320
|
* @returns {Promise<boolean>}
|
|
274
321
|
*/
|
|
275
|
-
async function build() {
|
|
322
|
+
async function build(options = {}) {
|
|
323
|
+
const { force = false } = options;
|
|
276
324
|
try {
|
|
277
325
|
const knowledgeKeys = await activeSource.list();
|
|
278
326
|
|
|
@@ -281,6 +329,25 @@ function createWikiRouter(config) {
|
|
|
281
329
|
return false
|
|
282
330
|
}
|
|
283
331
|
|
|
332
|
+
// Fingerprint cache: 若來源未變動且既有 wiki 存在,直接跳過 LLM
|
|
333
|
+
let currentFp = null;
|
|
334
|
+
const sourceSupportsFp = typeof activeSource.getFingerprint === 'function';
|
|
335
|
+
const storeSupportsManifest =
|
|
336
|
+
typeof activeStore.readManifest === 'function' &&
|
|
337
|
+
typeof activeStore.writeManifest === 'function';
|
|
338
|
+
|
|
339
|
+
if (sourceSupportsFp && storeSupportsManifest) {
|
|
340
|
+
currentFp = await activeSource.getFingerprint();
|
|
341
|
+
if (!force) {
|
|
342
|
+
const storedFp = await activeStore.readManifest();
|
|
343
|
+
const existingFiles = await activeStore.list();
|
|
344
|
+
if (existingFiles.length > 0 && fingerprintsEqual(currentFp, storedFp)) {
|
|
345
|
+
console.log('[Wiki] Source unchanged, skipping build (fingerprint match)');
|
|
346
|
+
return true
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
284
351
|
let totalFiles = 0;
|
|
285
352
|
|
|
286
353
|
for (const key of knowledgeKeys) {
|
|
@@ -322,6 +389,12 @@ function createWikiRouter(config) {
|
|
|
322
389
|
}
|
|
323
390
|
|
|
324
391
|
if (totalFiles === 0) return false
|
|
392
|
+
|
|
393
|
+
// build 成功 → 寫入 manifest 供下次比對
|
|
394
|
+
if (currentFp && storeSupportsManifest) {
|
|
395
|
+
await activeStore.writeManifest(currentFp);
|
|
396
|
+
}
|
|
397
|
+
|
|
325
398
|
return true
|
|
326
399
|
} catch (err) {
|
|
327
400
|
console.error('[Wiki Generation Error]', err);
|
|
@@ -354,11 +427,22 @@ function createWikiRouter(config) {
|
|
|
354
427
|
.replace('{{indexContent}}', indexContent);
|
|
355
428
|
|
|
356
429
|
const routeModel = routerModelId || process.env.WIKI_ROUTER_MODEL || modelId || process.env.WIKI_MODEL;
|
|
357
|
-
|
|
430
|
+
let output = await chat(
|
|
358
431
|
[{ role: 'user', content: selectionPrompt }],
|
|
359
432
|
routeModel
|
|
360
433
|
);
|
|
361
|
-
|
|
434
|
+
// Cloud LLM (e.g. Gemini) 偶爾會回空 stream,retry 一次
|
|
435
|
+
if (!output || !output.trim()) {
|
|
436
|
+
console.warn('[Wiki] Router LLM returned empty, retrying once...');
|
|
437
|
+
output = await chat(
|
|
438
|
+
[{ role: 'user', content: selectionPrompt }],
|
|
439
|
+
routeModel
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (!output) {
|
|
443
|
+
console.warn('[Wiki] Router LLM returned empty after retry.');
|
|
444
|
+
return ''
|
|
445
|
+
}
|
|
362
446
|
|
|
363
447
|
const selectedFilesStr = output.trim();
|
|
364
448
|
if (selectedFilesStr === 'NONE' || !selectedFilesStr) {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @typedef {Object} Source
|
|
13
13
|
* @property {() => Promise<string[]>} list - 列出所有 knowledge 來源 key(檔名 / DB id)
|
|
14
14
|
* @property {(key: string) => Promise<SourceEntry>} read - 讀取單一來源內容
|
|
15
|
+
* @property {() => Promise<Record<string, string>>} [getFingerprint] - 選用;回傳來源指紋(如 { key: hash })供 build 判斷是否需要重新生成
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
* @property {() => Promise<string[]>} list - 列出已生成的 wiki 檔名(含副檔名)
|
|
20
21
|
* @property {(filename: string) => Promise<string|null>} read - 讀取 wiki 檔;不存在回 null
|
|
21
22
|
* @property {(filename: string, content: string) => Promise<void>} write - 寫入 wiki 檔
|
|
23
|
+
* @property {() => Promise<Record<string, string>|null>} [readManifest] - 選用;讀取上次 build 的來源指紋;不存在回 null
|
|
24
|
+
* @property {(manifest: Record<string, string>) => Promise<void>} [writeManifest] - 選用;寫入本次 build 的來源指紋
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -36,9 +39,14 @@
|
|
|
36
39
|
* @property {string} [routerPrompt] - 自訂 Router prompt 字串,未提供時使用內建預設
|
|
37
40
|
*/
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} BuildOptions
|
|
44
|
+
* @property {boolean} [force=false] - 為 true 時跳過 fingerprint 比對,無條件重建
|
|
45
|
+
*/
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* @typedef {Object} WikiRouterInstance
|
|
41
|
-
* @property {() => Promise<boolean>} build - 建構或更新 Wiki 知識庫
|
|
49
|
+
* @property {(options?: BuildOptions) => Promise<boolean>} build - 建構或更新 Wiki 知識庫
|
|
42
50
|
* @property {(prompt: string) => Promise<string>} getContext - 根據問題取得相關 Wiki 上下文
|
|
43
51
|
*/
|
|
44
52
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* LLM Wiki 提示詞 — 從 src/prompts/*.md 檔案載入
|
|
@@ -95,6 +96,8 @@ function parseWikiOutput(output) {
|
|
|
95
96
|
*/
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
const MANIFEST_FILE = '.manifest.json';
|
|
100
|
+
|
|
98
101
|
/**
|
|
99
102
|
* 建立讀取本地檔案的 Source adapter
|
|
100
103
|
* @param {string} dir - knowledge 來源目錄
|
|
@@ -102,19 +105,30 @@ function parseWikiOutput(output) {
|
|
|
102
105
|
*/
|
|
103
106
|
function fsSource(dir) {
|
|
104
107
|
const resolved = path.resolve(dir);
|
|
108
|
+
async function list() {
|
|
109
|
+
if (!fs.existsSync(resolved)) return []
|
|
110
|
+
return fs
|
|
111
|
+
.readdirSync(resolved)
|
|
112
|
+
.filter(f => f.endsWith('.json') || f.endsWith('.md'))
|
|
113
|
+
.sort()
|
|
114
|
+
}
|
|
115
|
+
async function read(key) {
|
|
116
|
+
const filePath = path.join(resolved, key);
|
|
117
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
118
|
+
const type = key.endsWith('.json') ? 'json' : 'markdown';
|
|
119
|
+
return { type, content }
|
|
120
|
+
}
|
|
105
121
|
return {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const type = key.endsWith('.json') ? 'json' : 'markdown';
|
|
117
|
-
return { type, content }
|
|
122
|
+
list,
|
|
123
|
+
read,
|
|
124
|
+
async getFingerprint() {
|
|
125
|
+
const keys = await list();
|
|
126
|
+
const fp = {};
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
const { content } = await read(key);
|
|
129
|
+
fp[key] = crypto.createHash('sha256').update(content).digest('hex');
|
|
130
|
+
}
|
|
131
|
+
return fp
|
|
118
132
|
},
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -143,6 +157,23 @@ function fsStore(dir) {
|
|
|
143
157
|
ensure();
|
|
144
158
|
fs.writeFileSync(path.join(resolved, filename), content);
|
|
145
159
|
},
|
|
160
|
+
async readManifest() {
|
|
161
|
+
const filePath = path.join(resolved, MANIFEST_FILE);
|
|
162
|
+
if (!fs.existsSync(filePath)) return null
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
165
|
+
} catch {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
async writeManifest(manifest) {
|
|
170
|
+
ensure();
|
|
171
|
+
fs.writeFileSync(
|
|
172
|
+
path.join(resolved, MANIFEST_FILE),
|
|
173
|
+
JSON.stringify(manifest, null, 2),
|
|
174
|
+
'utf-8'
|
|
175
|
+
);
|
|
176
|
+
},
|
|
146
177
|
}
|
|
147
178
|
}
|
|
148
179
|
|
|
@@ -266,11 +297,28 @@ function createWikiRouter(config) {
|
|
|
266
297
|
}
|
|
267
298
|
}
|
|
268
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 比對兩個 fingerprint 字典是否完全一致
|
|
302
|
+
* @param {Record<string,string>|null|undefined} a
|
|
303
|
+
* @param {Record<string,string>|null|undefined} b
|
|
304
|
+
* @returns {boolean}
|
|
305
|
+
*/
|
|
306
|
+
function fingerprintsEqual(a, b) {
|
|
307
|
+
if (!a || !b) return false
|
|
308
|
+
const ak = Object.keys(a);
|
|
309
|
+
const bk = Object.keys(b);
|
|
310
|
+
if (ak.length !== bk.length) return false
|
|
311
|
+
return ak.every(k => a[k] === b[k])
|
|
312
|
+
}
|
|
313
|
+
|
|
269
314
|
/**
|
|
270
315
|
* 建構或更新 LLM Wiki 知識庫
|
|
316
|
+
* @param {{ force?: boolean }} [options]
|
|
317
|
+
* force=true 時跳過 fingerprint 比對,無條件重建
|
|
271
318
|
* @returns {Promise<boolean>}
|
|
272
319
|
*/
|
|
273
|
-
async function build() {
|
|
320
|
+
async function build(options = {}) {
|
|
321
|
+
const { force = false } = options;
|
|
274
322
|
try {
|
|
275
323
|
const knowledgeKeys = await activeSource.list();
|
|
276
324
|
|
|
@@ -279,6 +327,25 @@ function createWikiRouter(config) {
|
|
|
279
327
|
return false
|
|
280
328
|
}
|
|
281
329
|
|
|
330
|
+
// Fingerprint cache: 若來源未變動且既有 wiki 存在,直接跳過 LLM
|
|
331
|
+
let currentFp = null;
|
|
332
|
+
const sourceSupportsFp = typeof activeSource.getFingerprint === 'function';
|
|
333
|
+
const storeSupportsManifest =
|
|
334
|
+
typeof activeStore.readManifest === 'function' &&
|
|
335
|
+
typeof activeStore.writeManifest === 'function';
|
|
336
|
+
|
|
337
|
+
if (sourceSupportsFp && storeSupportsManifest) {
|
|
338
|
+
currentFp = await activeSource.getFingerprint();
|
|
339
|
+
if (!force) {
|
|
340
|
+
const storedFp = await activeStore.readManifest();
|
|
341
|
+
const existingFiles = await activeStore.list();
|
|
342
|
+
if (existingFiles.length > 0 && fingerprintsEqual(currentFp, storedFp)) {
|
|
343
|
+
console.log('[Wiki] Source unchanged, skipping build (fingerprint match)');
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
282
349
|
let totalFiles = 0;
|
|
283
350
|
|
|
284
351
|
for (const key of knowledgeKeys) {
|
|
@@ -320,6 +387,12 @@ function createWikiRouter(config) {
|
|
|
320
387
|
}
|
|
321
388
|
|
|
322
389
|
if (totalFiles === 0) return false
|
|
390
|
+
|
|
391
|
+
// build 成功 → 寫入 manifest 供下次比對
|
|
392
|
+
if (currentFp && storeSupportsManifest) {
|
|
393
|
+
await activeStore.writeManifest(currentFp);
|
|
394
|
+
}
|
|
395
|
+
|
|
323
396
|
return true
|
|
324
397
|
} catch (err) {
|
|
325
398
|
console.error('[Wiki Generation Error]', err);
|
|
@@ -352,11 +425,22 @@ function createWikiRouter(config) {
|
|
|
352
425
|
.replace('{{indexContent}}', indexContent);
|
|
353
426
|
|
|
354
427
|
const routeModel = routerModelId || process.env.WIKI_ROUTER_MODEL || modelId || process.env.WIKI_MODEL;
|
|
355
|
-
|
|
428
|
+
let output = await chat(
|
|
356
429
|
[{ role: 'user', content: selectionPrompt }],
|
|
357
430
|
routeModel
|
|
358
431
|
);
|
|
359
|
-
|
|
432
|
+
// Cloud LLM (e.g. Gemini) 偶爾會回空 stream,retry 一次
|
|
433
|
+
if (!output || !output.trim()) {
|
|
434
|
+
console.warn('[Wiki] Router LLM returned empty, retrying once...');
|
|
435
|
+
output = await chat(
|
|
436
|
+
[{ role: 'user', content: selectionPrompt }],
|
|
437
|
+
routeModel
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (!output) {
|
|
441
|
+
console.warn('[Wiki] Router LLM returned empty after retry.');
|
|
442
|
+
return ''
|
|
443
|
+
}
|
|
360
444
|
|
|
361
445
|
const selectedFilesStr = output.trim();
|
|
362
446
|
if (selectedFilesStr === 'NONE' || !selectedFilesStr) {
|
package/package.json
CHANGED
package/src/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @typedef {Object} Source
|
|
13
13
|
* @property {() => Promise<string[]>} list - 列出所有 knowledge 來源 key(檔名 / DB id)
|
|
14
14
|
* @property {(key: string) => Promise<SourceEntry>} read - 讀取單一來源內容
|
|
15
|
+
* @property {() => Promise<Record<string, string>>} [getFingerprint] - 選用;回傳來源指紋(如 { key: hash })供 build 判斷是否需要重新生成
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
* @property {() => Promise<string[]>} list - 列出已生成的 wiki 檔名(含副檔名)
|
|
20
21
|
* @property {(filename: string) => Promise<string|null>} read - 讀取 wiki 檔;不存在回 null
|
|
21
22
|
* @property {(filename: string, content: string) => Promise<void>} write - 寫入 wiki 檔
|
|
23
|
+
* @property {() => Promise<Record<string, string>|null>} [readManifest] - 選用;讀取上次 build 的來源指紋;不存在回 null
|
|
24
|
+
* @property {(manifest: Record<string, string>) => Promise<void>} [writeManifest] - 選用;寫入本次 build 的來源指紋
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -36,9 +39,14 @@
|
|
|
36
39
|
* @property {string} [routerPrompt] - 自訂 Router prompt 字串,未提供時使用內建預設
|
|
37
40
|
*/
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} BuildOptions
|
|
44
|
+
* @property {boolean} [force=false] - 為 true 時跳過 fingerprint 比對,無條件重建
|
|
45
|
+
*/
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* @typedef {Object} WikiRouterInstance
|
|
41
|
-
* @property {() => Promise<boolean>} build - 建構或更新 Wiki 知識庫
|
|
49
|
+
* @property {(options?: BuildOptions) => Promise<boolean>} build - 建構或更新 Wiki 知識庫
|
|
42
50
|
* @property {(prompt: string) => Promise<string>} getContext - 根據問題取得相關 Wiki 上下文
|
|
43
51
|
*/
|
|
44
52
|
|