@dshtrading/knowledge 0.6.1 → 0.6.3
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/lib/knowledge-fs.js +30 -25
- package/lib/store-memory.js +3 -0
- package/lib/tool.js +60 -16
- package/lib/types.d.ts +8 -0
- package/package.json +4 -5
package/lib/knowledge-fs.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { canonicalizeCardAuthor } from "./authors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { SKIP_WRITE, transactStore } from "@dshtrading/dsh-home";
|
|
2
|
+
import { SKIP_WRITE, createRecordStoreAdapter, readStoreText } from "@dshtrading/dsh-home";
|
|
4
3
|
//#region src/knowledge-fs.ts
|
|
5
4
|
/**
|
|
6
5
|
* 文件持久化版知识卡片存储(Node.js 宿主端专用)。
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* 锁内新鲜读盘 +
|
|
10
|
-
*
|
|
7
|
+
* 读写骨架经 `@dshtrading/dsh-home` 的 {@link createRecordStoreAdapter}——跨进程排他锁 +
|
|
8
|
+
* 锁内新鲜读盘 + 原子写 + 落盘成功后才发布缓存(2026-10-09 多进程互相覆盖事故修复)。
|
|
9
|
+
* 读取面作者归一与坏文件降级纪律不变(逐行对齐 custom-fs.ts 先例)。
|
|
11
10
|
*/
|
|
12
11
|
const LOG_PREFIX = "[dsh-trading/knowledge] failed to atomic flush knowledge cards to";
|
|
13
12
|
function createFileKnowledgeCardStore(filePath) {
|
|
14
|
-
|
|
13
|
+
/** 读盘/解析失败的日志前缀(坏 JSON 与非 ENOENT 读失败同一条,与既有 load 口径逐字一致)。 */
|
|
14
|
+
const READ_FAILED = `[dsh-trading/knowledge] failed to read knowledge cards from ${filePath}:`;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* 文本 → 卡片表(作者别名归一、坏 JSON 降级空表)。
|
|
17
17
|
*
|
|
18
18
|
* 作者别名在读取时归一(authors.ts):历史数据即刻在 GUI/检索层表现为规范名。
|
|
19
19
|
* 读本身不写盘,但归一结果留在缓存里,会被下一次写操作的全表回写落盘——
|
|
@@ -21,10 +21,10 @@ function createFileKnowledgeCardStore(filePath) {
|
|
|
21
21
|
* 缺 source / 缺 id 的脏行原样保留(不丢数据);结构性校验只在入库发生,
|
|
22
22
|
* 但这类行仍会让 getByUrl/search/graph 的解引用抛错(既有边界,未在此收口)。
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
function decode(text) {
|
|
25
|
+
if (text === void 0) return /* @__PURE__ */ new Map();
|
|
25
26
|
try {
|
|
26
|
-
const
|
|
27
|
-
const parsed = JSON.parse(content);
|
|
27
|
+
const parsed = JSON.parse(text);
|
|
28
28
|
const map = /* @__PURE__ */ new Map();
|
|
29
29
|
if (Array.isArray(parsed)) {
|
|
30
30
|
let healed = 0;
|
|
@@ -37,40 +37,45 @@ function createFileKnowledgeCardStore(filePath) {
|
|
|
37
37
|
}
|
|
38
38
|
return map;
|
|
39
39
|
} catch (err) {
|
|
40
|
-
|
|
40
|
+
console.error(READ_FAILED, err);
|
|
41
41
|
return /* @__PURE__ */ new Map();
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return cache;
|
|
52
|
-
}
|
|
44
|
+
const store = createRecordStoreAdapter({
|
|
45
|
+
filePath,
|
|
46
|
+
logPrefix: LOG_PREFIX,
|
|
47
|
+
readFromDisk: () => readStoreText(filePath, READ_FAILED),
|
|
48
|
+
decode,
|
|
49
|
+
encode: (table) => [...table.values()]
|
|
50
|
+
});
|
|
53
51
|
return {
|
|
54
52
|
async list() {
|
|
55
|
-
return [...(await load()).values()];
|
|
53
|
+
return [...(await store.load()).values()];
|
|
56
54
|
},
|
|
57
55
|
async get(id) {
|
|
58
|
-
return (await load()).get(id);
|
|
56
|
+
return (await store.load()).get(id);
|
|
59
57
|
},
|
|
60
58
|
async getByUrl(url) {
|
|
61
|
-
const map = await load();
|
|
59
|
+
const map = await store.load();
|
|
62
60
|
const trimmed = url.trim();
|
|
63
61
|
for (const card of map.values()) if (card.source.url.trim() === trimmed) return card;
|
|
64
62
|
},
|
|
65
63
|
async save(card) {
|
|
66
|
-
await commit((onDisk) => {
|
|
64
|
+
await store.commit((onDisk) => {
|
|
67
65
|
onDisk.set(card.id, canonicalizeCardAuthor({ ...card }));
|
|
68
66
|
return onDisk;
|
|
69
67
|
});
|
|
70
68
|
},
|
|
69
|
+
async saveMany(cards) {
|
|
70
|
+
if (cards.length === 0) return;
|
|
71
|
+
await store.commit((onDisk) => {
|
|
72
|
+
for (const card of cards) onDisk.set(card.id, canonicalizeCardAuthor({ ...card }));
|
|
73
|
+
return onDisk;
|
|
74
|
+
});
|
|
75
|
+
},
|
|
71
76
|
async delete(id) {
|
|
72
77
|
let existed = false;
|
|
73
|
-
await commit((onDisk) => {
|
|
78
|
+
await store.commit((onDisk) => {
|
|
74
79
|
existed = onDisk.delete(id);
|
|
75
80
|
return existed ? onDisk : SKIP_WRITE;
|
|
76
81
|
});
|
package/lib/store-memory.js
CHANGED
|
@@ -17,6 +17,9 @@ function createMemoryKnowledgeCardStore(initialCards = []) {
|
|
|
17
17
|
async save(card) {
|
|
18
18
|
map.set(card.id, canonicalizeCardAuthor({ ...card }));
|
|
19
19
|
},
|
|
20
|
+
async saveMany(cards) {
|
|
21
|
+
for (const card of cards) map.set(card.id, canonicalizeCardAuthor({ ...card }));
|
|
22
|
+
},
|
|
20
23
|
async delete(id) {
|
|
21
24
|
return map.delete(id);
|
|
22
25
|
}
|
package/lib/tool.js
CHANGED
|
@@ -26,6 +26,19 @@ function renderCardFullLines(card) {
|
|
|
26
26
|
if (card.tickers && card.tickers.length > 0) lines.push(` 关联标的: ${card.tickers.join(", ")}`);
|
|
27
27
|
return lines;
|
|
28
28
|
}
|
|
29
|
+
/** 时间字段的可读回显:空值/缺省一律写「未知」,不留空位冒充有日期。 */
|
|
30
|
+
function shownTimestamp(value) {
|
|
31
|
+
return value !== void 0 && value.trim().length > 0 ? value : "未知";
|
|
32
|
+
}
|
|
33
|
+
/** 解析显式时效筛选的 ISO 日期:空值表示不筛,非法值抛错由工具回显而不是静默当无命中。 */
|
|
34
|
+
function parseCutoff(value, field) {
|
|
35
|
+
if (typeof value !== "string") return void 0;
|
|
36
|
+
const trimmed = value.trim();
|
|
37
|
+
if (trimmed.length === 0) return void 0;
|
|
38
|
+
const ms = Date.parse(trimmed);
|
|
39
|
+
if (Number.isNaN(ms)) throw new Error(`[knowledge_search] 参数 ${field} 不是合法日期: ${trimmed}`);
|
|
40
|
+
return ms;
|
|
41
|
+
}
|
|
29
42
|
function createKnowledgeIngestTool(store, options = {}) {
|
|
30
43
|
const { onWritten } = options;
|
|
31
44
|
return defineTool({
|
|
@@ -67,35 +80,35 @@ function createKnowledgeIngestTool(store, options = {}) {
|
|
|
67
80
|
description: "Overall fact-check grade: \"high\" | \"medium\" | \"low\"."
|
|
68
81
|
},
|
|
69
82
|
coreClaimsJson: {
|
|
70
|
-
type: "
|
|
83
|
+
type: "json",
|
|
71
84
|
required: true,
|
|
72
|
-
description: "JSON
|
|
85
|
+
description: "JSON array of core claims (also accepted as a JSON string), preserving the author's reasoning chain, e.g. '[\"claim1\", \"claim2\"]'."
|
|
73
86
|
},
|
|
74
87
|
factCheckJson: {
|
|
75
|
-
type: "
|
|
88
|
+
type: "json",
|
|
76
89
|
required: true,
|
|
77
|
-
description: "JSON object with the three fact-check buckets, e.g. '{\"verified\":[\"...\"],\"discrepancies\":[\"...\"],\"unverifiable\":[]}'."
|
|
90
|
+
description: "JSON object with the three fact-check buckets (also accepted as a JSON string), e.g. '{\"verified\":[\"...\"],\"discrepancies\":[\"...\"],\"unverifiable\":[]}'."
|
|
78
91
|
},
|
|
79
92
|
takeawaysJson: {
|
|
80
|
-
type: "
|
|
81
|
-
description: "Optional JSON
|
|
93
|
+
type: "json",
|
|
94
|
+
description: "Optional JSON array of reusable analysis frameworks and lessons (also accepted as a JSON string)."
|
|
82
95
|
},
|
|
83
96
|
boundariesJson: {
|
|
84
|
-
type: "
|
|
85
|
-
description: "Optional JSON
|
|
97
|
+
type: "json",
|
|
98
|
+
description: "Optional JSON array of applicability boundaries, invalidation cases and pitfalls (also accepted as a JSON string)."
|
|
86
99
|
},
|
|
87
100
|
tagsJson: {
|
|
88
|
-
type: "
|
|
101
|
+
type: "json",
|
|
89
102
|
required: true,
|
|
90
|
-
description: "JSON
|
|
103
|
+
description: "JSON array of controlled topic tags (also accepted as a JSON string), e.g. '[\"宏观\", \"高股息\", \"红利策略\"]'."
|
|
91
104
|
},
|
|
92
105
|
tickersJson: {
|
|
93
|
-
type: "
|
|
94
|
-
description: "Optional JSON
|
|
106
|
+
type: "json",
|
|
107
|
+
description: "Optional JSON array of related instrument codes (also accepted as a JSON string), e.g. '[\"BTCUSDT\", \"600519.SH\"]'."
|
|
95
108
|
},
|
|
96
109
|
relatedJson: {
|
|
97
|
-
type: "
|
|
98
|
-
description: "Optional JSON
|
|
110
|
+
type: "json",
|
|
111
|
+
description: "Optional JSON array of related card ids already in the library (also accepted as a JSON string), e.g. '[\"kc_01j...\", \"kc_01k...\"]'."
|
|
99
112
|
}
|
|
100
113
|
},
|
|
101
114
|
output: {
|
|
@@ -196,7 +209,7 @@ function createKnowledgeIngestTool(store, options = {}) {
|
|
|
196
209
|
function createKnowledgeSearchTool(store) {
|
|
197
210
|
return defineTool({
|
|
198
211
|
name: "knowledge_search",
|
|
199
|
-
description: "Search knowledge cards in the local library. With a keyword the results rank by field hit weight (tags > title > core claims > summary/author); without one they come newest-updated first. Filters: cluster (the graph clustering key, pair with knowledge_graph for two-stage retrieval), author, source kind and
|
|
212
|
+
description: "Search knowledge cards in the local library. With a keyword the results rank by field hit weight (tags > title > core claims > summary/author); without one they come newest-updated first. Filters: cluster (the graph clustering key, pair with knowledge_graph for two-stage retrieval), author, source kind, credibility and explicit freshness cutoffs updatedAfter / publishedAfter. Every result reports its updatedAt and source publish date (unknown is stated as such); cards are never dropped or downgraded by age unless an explicit cutoff asks for it. detail=\"full\" also returns core claims, the fact-check buckets and reusable lessons (capped at 20 cards).",
|
|
200
213
|
parameters: {
|
|
201
214
|
query: {
|
|
202
215
|
type: "string",
|
|
@@ -222,6 +235,14 @@ function createKnowledgeSearchTool(store) {
|
|
|
222
235
|
type: "string",
|
|
223
236
|
description: "Credibility filter: \"high\" | \"medium\" | \"low\"."
|
|
224
237
|
},
|
|
238
|
+
updatedAfter: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Explicit freshness cutoff: keep only cards updated at or after this ISO date/datetime (e.g. \"2026-01-01\" or \"2026-01-01T00:00:00Z\"). Cards with an unparsable updatedAt never match. Omitting it applies no age filter."
|
|
241
|
+
},
|
|
242
|
+
publishedAfter: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description: "Explicit freshness cutoff on the source publish date: keep only cards whose source.publishedAt is at or after this ISO date. Cards without a source publish date never match an explicit cutoff (unknown is not assumed fresh)."
|
|
245
|
+
},
|
|
225
246
|
limit: {
|
|
226
247
|
type: "number",
|
|
227
248
|
description: "Maximum results to return (default 20).",
|
|
@@ -249,12 +270,29 @@ function createKnowledgeSearchTool(store) {
|
|
|
249
270
|
const targetCluster = typeof params.cluster === "string" ? params.cluster.trim().toLowerCase() : "";
|
|
250
271
|
const targetSourceType = typeof params.sourceType === "string" ? params.sourceType.trim() : void 0;
|
|
251
272
|
const targetCredibility = typeof params.credibility === "string" ? params.credibility.trim() : void 0;
|
|
273
|
+
let updatedAfterMs;
|
|
274
|
+
let publishedAfterMs;
|
|
275
|
+
try {
|
|
276
|
+
updatedAfterMs = parseCutoff(params.updatedAfter, "updatedAfter");
|
|
277
|
+
publishedAfterMs = parseCutoff(params.publishedAfter, "publishedAfter");
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return error instanceof Error ? error.message : String(error);
|
|
280
|
+
}
|
|
252
281
|
const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.min(params.limit, 100)) : 20;
|
|
253
282
|
const detail = params.detail === "full" ? "full" : "summary";
|
|
254
283
|
const effectiveLimit = detail === "full" ? Math.min(limit, 20) : limit;
|
|
255
284
|
const matched = allCards.filter((card) => {
|
|
256
285
|
if (targetSourceType && card.source.type !== targetSourceType) return false;
|
|
257
286
|
if (targetCredibility && card.credibility !== targetCredibility) return false;
|
|
287
|
+
if (updatedAfterMs !== void 0) {
|
|
288
|
+
const updatedAtMs = Date.parse(card.updatedAt);
|
|
289
|
+
if (Number.isNaN(updatedAtMs) || updatedAtMs < updatedAfterMs) return false;
|
|
290
|
+
}
|
|
291
|
+
if (publishedAfterMs !== void 0) {
|
|
292
|
+
const publishedAt = card.source.publishedAt;
|
|
293
|
+
const publishedAtMs = publishedAt === void 0 ? NaN : Date.parse(publishedAt);
|
|
294
|
+
if (Number.isNaN(publishedAtMs) || publishedAtMs < publishedAfterMs) return false;
|
|
295
|
+
}
|
|
258
296
|
if (targetCluster && (card.tags[0] ?? "").trim().toLowerCase() !== targetCluster) return false;
|
|
259
297
|
if (authorLower && !card.source.author.toLowerCase().includes(authorLower)) return false;
|
|
260
298
|
if (rawTags.length > 0) {
|
|
@@ -290,6 +328,7 @@ function createKnowledgeSearchTool(store) {
|
|
|
290
328
|
const lines = [`[knowledge_search] 匹配到 ${matched.length} 张卡片 (展示前 ${results.length} 项${detail === "full" ? ", detail=full" : ""}):`];
|
|
291
329
|
for (const card of results) {
|
|
292
330
|
lines.push(`- [${card.id}] "${card.title}" (${card.source.type} @ ${card.source.author}, 可信度: ${card.credibility})`);
|
|
331
|
+
lines.push(` 时间: 更新 ${shownTimestamp(card.updatedAt)} | 发布 ${shownTimestamp(card.source.publishedAt)}`);
|
|
293
332
|
lines.push(` 摘要: ${card.summary}`);
|
|
294
333
|
lines.push(` 标签: ${card.tags.join(", ")} | 链接: ${card.source.url}`);
|
|
295
334
|
if (detail === "full") lines.push(...renderCardFullLines(card));
|
|
@@ -364,15 +403,20 @@ function createKnowledgeDeleteTool(store, options = {}) {
|
|
|
364
403
|
const target = await store.get(id);
|
|
365
404
|
if (!target) return `[knowledge_delete] 未找到卡片 [${id}],请先用 knowledge_search 确认卡片 id`;
|
|
366
405
|
const referencing = [];
|
|
406
|
+
const cleaned = [];
|
|
367
407
|
for (const card of await store.list()) if (card.related && card.related.includes(id)) {
|
|
368
408
|
const remaining = card.related.filter((r) => r !== id);
|
|
369
409
|
referencing.push(card.id);
|
|
370
|
-
|
|
410
|
+
cleaned.push({
|
|
371
411
|
...card,
|
|
372
412
|
related: remaining.length > 0 ? remaining : void 0,
|
|
373
413
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
374
414
|
});
|
|
375
415
|
}
|
|
416
|
+
if (cleaned.length > 0) {
|
|
417
|
+
if (typeof store.saveMany === "function") await store.saveMany(cleaned);
|
|
418
|
+
else for (const card of cleaned) await store.save(card);
|
|
419
|
+
}
|
|
376
420
|
if (!await store.delete(id)) return `[knowledge_delete] 删除失败:卡片 [${id}] 在删除时已不存在`;
|
|
377
421
|
onWritten?.();
|
|
378
422
|
const lines = [`[knowledge_delete] 已删除卡片 [${target.id}] "${target.title}"`, ` 核心论点(留痕回显): ${target.coreClaims.join(";")}`];
|
package/lib/types.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ interface KnowledgeCardStore {
|
|
|
73
73
|
get(id: string): Promise<KnowledgeCard | undefined>;
|
|
74
74
|
getByUrl(url: string): Promise<KnowledgeCard | undefined>;
|
|
75
75
|
save(card: KnowledgeCard): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* 批量写入(可选能力):最终状态等同于按序逐张 save,但实现可为一次事务。
|
|
78
|
+
*
|
|
79
|
+
* 存在的理由:文件版 store 的 save 是整表事务(跨进程排他锁 + 锁内读盘 + 序列化 +
|
|
80
|
+
* 原子写),knowledge_delete 清理 F 张引用方就是 F+1 次整表落盘。调用方在能力存在时
|
|
81
|
+
* 必须走本方法,缺席(轻量/第三方实现)则逐张 save 降级——正确性两版一致,只差落盘次数。
|
|
82
|
+
*/
|
|
83
|
+
saveMany?(cards: readonly KnowledgeCard[]): Promise<void>;
|
|
76
84
|
delete(id: string): Promise<boolean>;
|
|
77
85
|
}
|
|
78
86
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/knowledge",
|
|
3
3
|
"description": "Knowledge base data plane for dsh-trading: KnowledgeCard data model, validation, graph builder, atomic file store, and knowledge_ingest / knowledge_search agent tools.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -25,12 +25,11 @@
|
|
|
25
25
|
],
|
|
26
26
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@deepseek-ai/schemastery": "^3.18.4",
|
|
30
|
-
"@dshtrading/dsh-home": "^0.6.1"
|
|
28
|
+
"@dshtrading/dsh-home": "^0.6.3"
|
|
31
29
|
},
|
|
32
30
|
"peerDependencies": {
|
|
33
|
-
"@deepseek-ai/cordis": ">=4.0.0"
|
|
31
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
32
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.2"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@types/node": "^24.0.0",
|