@dshtrading/knowledge 0.6.3 → 0.7.1

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.
@@ -0,0 +1,82 @@
1
+ import { KnowledgeCard } from "./types.js";
2
+ //#region src/bundle.d.ts
3
+ /** bundle 格式版本:本机只认这一版,改动卡片契约时递增。 */
4
+ declare const KNOWLEDGE_BUNDLE_SCHEMA_VERSION = 1;
5
+ /** 单次导入卡片数上限:防止一次导入把库撑到图形/检索不可用的量级。 */
6
+ declare const KNOWLEDGE_BUNDLE_MAX_CARDS = 2000;
7
+ /** 单文件字节上限(8 MiB):整个文件要先读进内存再解析,读之前就要有界。 */
8
+ declare const KNOWLEDGE_BUNDLE_MAX_BYTES = 8388608;
9
+ interface KnowledgeBundleMeta {
10
+ readonly schemaVersion: number;
11
+ readonly exportedAt: string;
12
+ readonly cardCount: number;
13
+ readonly generator: string;
14
+ readonly notice: string;
15
+ }
16
+ interface KnowledgeBundle {
17
+ readonly meta: KnowledgeBundleMeta;
18
+ readonly cards: readonly KnowledgeCard[];
19
+ }
20
+ /**
21
+ * 由卡片列表构造 bundle。
22
+ *
23
+ * 不做深拷贝也不做字段裁剪:导出的是库里已有的卡片(store 写入面已归一作者),
24
+ * 这里改字段只会让「导出—导入—再导出」三份内容对不上。
25
+ */
26
+ declare function createKnowledgeBundle(cards: readonly KnowledgeCard[], options?: {
27
+ readonly now?: Date;
28
+ }): KnowledgeBundle;
29
+ /** 落盘文本:2 空格缩进(人可读、可 diff)+ 收尾换行(符合 POSIX 文本约定)。 */
30
+ declare function serializeKnowledgeBundle(bundle: KnowledgeBundle): string;
31
+ /**
32
+ * 解析 bundle 文本,逐条判定并把失败原因写成能直接给用户看的一句话。
33
+ *
34
+ * 判定顺序即优先级:先看「有没有内容/是不是 JSON/顶层像不像 bundle」这类
35
+ * 一眼能定性的问题,再进 schemaVersion 与卡数上限,最后才逐卡看必填字段——
36
+ * 用户拿到的第一条错误就是最该先修的那条。
37
+ */
38
+ declare function parseKnowledgeBundle(text: string): {
39
+ readonly ok: true;
40
+ readonly bundle: KnowledgeBundle;
41
+ } | {
42
+ readonly ok: false;
43
+ readonly error: string;
44
+ };
45
+ type KnowledgeImportMode = 'skip' | 'overwrite';
46
+ interface KnowledgeImportEntry {
47
+ readonly id: string;
48
+ readonly title: string;
49
+ readonly url: string;
50
+ }
51
+ interface KnowledgeImportPlan {
52
+ readonly create: readonly KnowledgeCard[];
53
+ readonly update: readonly KnowledgeCard[];
54
+ readonly skipped: readonly (KnowledgeImportEntry & {
55
+ readonly reason: string;
56
+ })[];
57
+ readonly droppedRelated: readonly {
58
+ readonly id: string;
59
+ readonly missing: readonly string[];
60
+ }[];
61
+ readonly invalid: readonly {
62
+ readonly index: number;
63
+ readonly title?: string;
64
+ readonly error: string;
65
+ }[];
66
+ readonly remappedIds: readonly {
67
+ readonly from: string;
68
+ readonly to: string;
69
+ }[];
70
+ }
71
+ /**
72
+ * 纯函数:算出「把 incoming 导入本机库」要做的事,不读文件、不调 store、不写库。
73
+ *
74
+ * 去重键是 source.url(与 knowledge-fs.getByUrl 同口径:trim 后精确相等)。
75
+ * 顺序是有意的:先给每张 incoming 定身份与最终 id,再改 related(此时映射表已完整),
76
+ * 最后统一过 validate——任何一张校验失败只进 invalid,不影响其它卡片。
77
+ */
78
+ declare function planKnowledgeImport(local: readonly KnowledgeCard[], incoming: readonly KnowledgeCard[], mode: KnowledgeImportMode, options?: {
79
+ readonly now?: Date;
80
+ }): KnowledgeImportPlan;
81
+ //#endregion
82
+ export { KNOWLEDGE_BUNDLE_MAX_BYTES, KNOWLEDGE_BUNDLE_MAX_CARDS, KNOWLEDGE_BUNDLE_SCHEMA_VERSION, KnowledgeBundle, KnowledgeBundleMeta, KnowledgeImportEntry, KnowledgeImportMode, KnowledgeImportPlan, createKnowledgeBundle, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle };
package/lib/bundle.js ADDED
@@ -0,0 +1,296 @@
1
+ import { generateCardId, validateKnowledgeCard } from "./validate.js";
2
+ //#region src/bundle.ts
3
+ /** bundle 格式版本:本机只认这一版,改动卡片契约时递增。 */
4
+ const KNOWLEDGE_BUNDLE_SCHEMA_VERSION = 1;
5
+ /** 单次导入卡片数上限:防止一次导入把库撑到图形/检索不可用的量级。 */
6
+ const KNOWLEDGE_BUNDLE_MAX_CARDS = 2e3;
7
+ /** 单文件字节上限(8 MiB):整个文件要先读进内存再解析,读之前就要有界。 */
8
+ const KNOWLEDGE_BUNDLE_MAX_BYTES = 8388608;
9
+ const BUNDLE_GENERATOR = "dsh-trading";
10
+ const BUNDLE_NOTICE = "知识卡片是第三方观点的结构化转述,不构成投资建议;来源作者与原始链接随卡片保留以便查证。";
11
+ /**
12
+ * 由卡片列表构造 bundle。
13
+ *
14
+ * 不做深拷贝也不做字段裁剪:导出的是库里已有的卡片(store 写入面已归一作者),
15
+ * 这里改字段只会让「导出—导入—再导出」三份内容对不上。
16
+ */
17
+ function createKnowledgeBundle(cards, options = {}) {
18
+ return {
19
+ meta: {
20
+ schemaVersion: 1,
21
+ exportedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
22
+ cardCount: cards.length,
23
+ generator: BUNDLE_GENERATOR,
24
+ notice: BUNDLE_NOTICE
25
+ },
26
+ cards
27
+ };
28
+ }
29
+ /** 落盘文本:2 空格缩进(人可读、可 diff)+ 收尾换行(符合 POSIX 文本约定)。 */
30
+ function serializeKnowledgeBundle(bundle) {
31
+ return JSON.stringify(bundle, null, 2) + "\n";
32
+ }
33
+ function isRecord(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+ /**
37
+ * 解析 bundle 文本,逐条判定并把失败原因写成能直接给用户看的一句话。
38
+ *
39
+ * 判定顺序即优先级:先看「有没有内容/是不是 JSON/顶层像不像 bundle」这类
40
+ * 一眼能定性的问题,再进 schemaVersion 与卡数上限,最后才逐卡看必填字段——
41
+ * 用户拿到的第一条错误就是最该先修的那条。
42
+ */
43
+ function parseKnowledgeBundle(text) {
44
+ if (typeof text !== "string" || text.trim().length === 0) return {
45
+ ok: false,
46
+ error: "bundle 内容为空"
47
+ };
48
+ const bytes = new TextEncoder().encode(text).length;
49
+ if (bytes > 8388608) return {
50
+ ok: false,
51
+ error: `bundle 体积 ${bytes} 字节超过上限 ${KNOWLEDGE_BUNDLE_MAX_BYTES} 字节(8 MiB),请拆分后再导入`
52
+ };
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(text);
56
+ } catch (error) {
57
+ return {
58
+ ok: false,
59
+ error: `bundle 不是合法 JSON: ${error instanceof Error ? error.message : String(error)}`
60
+ };
61
+ }
62
+ if (!isRecord(parsed)) return {
63
+ ok: false,
64
+ error: "bundle 顶层必须是 JSON 对象(含 meta 与 cards 两个字段)"
65
+ };
66
+ if (!isRecord(parsed.meta)) return {
67
+ ok: false,
68
+ error: "bundle 缺少 meta 字段(应为对象)"
69
+ };
70
+ if (parsed.cards === void 0) return {
71
+ ok: false,
72
+ error: "bundle 缺少 cards 字段(应为卡片数组)"
73
+ };
74
+ const meta = parsed.meta;
75
+ if (meta.schemaVersion !== 1) return {
76
+ ok: false,
77
+ error: `不支持的 schemaVersion: ${String(meta.schemaVersion)}(本机支持 1)`
78
+ };
79
+ if (!Array.isArray(parsed.cards)) return {
80
+ ok: false,
81
+ error: "bundle 的 cards 必须是数组"
82
+ };
83
+ const cards = parsed.cards;
84
+ if (cards.length > 2e3) return {
85
+ ok: false,
86
+ error: `bundle 卡片数 ${cards.length} 超过上限 ${KNOWLEDGE_BUNDLE_MAX_CARDS} 张,请拆分后再导入`
87
+ };
88
+ for (let index = 0; index < cards.length; index += 1) {
89
+ const card = cards[index];
90
+ const position = `第 ${index + 1} 张卡片(下标 ${index})`;
91
+ if (!isRecord(card)) return {
92
+ ok: false,
93
+ error: `${position}不是 JSON 对象`
94
+ };
95
+ const missing = [];
96
+ if (typeof card.id !== "string" || card.id.trim().length === 0) missing.push("id");
97
+ if (typeof card.title !== "string" || card.title.trim().length === 0) missing.push("title");
98
+ const source = card.source;
99
+ if (!isRecord(source) || typeof source.url !== "string" || source.url.trim().length === 0) missing.push("source.url");
100
+ if (missing.length > 0) return {
101
+ ok: false,
102
+ error: `${position}缺少或非法字段: ${missing.join(", ")}`
103
+ };
104
+ }
105
+ return {
106
+ ok: true,
107
+ bundle: parsed
108
+ };
109
+ }
110
+ /**
111
+ * 拆出 related 与其余字段。
112
+ *
113
+ * related 不能简单「置 undefined」:exactOptionalPropertyTypes 下「没有关联」是
114
+ * 不带这个键,而且基础内容若残留 incoming 里已被丢弃的旧引用,校验会替我们放行
115
+ * 一条本该消失的关联。先摘出来、过滤完再条件回填,两个问题一起消掉。
116
+ */
117
+ function splitRelated(input) {
118
+ const { related, ...base } = input;
119
+ return {
120
+ base,
121
+ related
122
+ };
123
+ }
124
+ /**
125
+ * related 存在性校验用的最小占位卡片。
126
+ *
127
+ * 只为凑出「本地 id ∪ 本批次待写 id」这个集合(validateKnowledgeCard 只读 id),
128
+ * 永远不会被写库——所以字段能给多小给多小。
129
+ */
130
+ function placeholderCard(id) {
131
+ return {
132
+ id,
133
+ title: "",
134
+ summary: "",
135
+ source: {
136
+ type: "manual",
137
+ url: "",
138
+ author: ""
139
+ },
140
+ credibility: "low",
141
+ coreClaims: [],
142
+ factCheck: {
143
+ verified: [],
144
+ discrepancies: [],
145
+ unverifiable: []
146
+ },
147
+ takeaways: [],
148
+ boundaries: [],
149
+ tags: [],
150
+ createdAt: "",
151
+ updatedAt: ""
152
+ };
153
+ }
154
+ /**
155
+ * 纯函数:算出「把 incoming 导入本机库」要做的事,不读文件、不调 store、不写库。
156
+ *
157
+ * 去重键是 source.url(与 knowledge-fs.getByUrl 同口径:trim 后精确相等)。
158
+ * 顺序是有意的:先给每张 incoming 定身份与最终 id,再改 related(此时映射表已完整),
159
+ * 最后统一过 validate——任何一张校验失败只进 invalid,不影响其它卡片。
160
+ */
161
+ function planKnowledgeImport(local, incoming, mode, options = {}) {
162
+ const localIds = new Set(local.map((card) => card.id));
163
+ const localByUrl = /* @__PURE__ */ new Map();
164
+ for (const card of local) {
165
+ const url = typeof card.source?.url === "string" ? card.source.url.trim() : "";
166
+ if (url.length > 0 && !localByUrl.has(url)) localByUrl.set(url, card);
167
+ }
168
+ const skipped = [];
169
+ const remappedIds = [];
170
+ const pending = [];
171
+ /** 本批次已占用的 id:先到的卡片赢,后到的冲突重生成。 */
172
+ const claimedIds = /* @__PURE__ */ new Set();
173
+ /**
174
+ * remappedIds 的 from 侧。这些 bundle id 已经指向别的卡片(本机同链接卡、或本批更早的卡),
175
+ * 同批次里就不能再拿它当新卡 id——否则同一个 id 在映射表里同时指向两张卡,related 解析变含糊。
176
+ */
177
+ const remappedFrom = /* @__PURE__ */ new Set();
178
+ /** 记录一条 id 重映射:数组给报告与映射表用,集合给「已占用」判定用,两处必须同步。 */
179
+ const recordRemap = (from, to) => {
180
+ remappedIds.push({
181
+ from,
182
+ to
183
+ });
184
+ remappedFrom.add(from);
185
+ };
186
+ for (let index = 0; index < incoming.length; index += 1) {
187
+ const card = incoming[index];
188
+ if (!isRecord(card)) continue;
189
+ const url = typeof card.source?.url === "string" ? card.source.url.trim() : "";
190
+ const existing = url.length > 0 ? localByUrl.get(url) : void 0;
191
+ if (existing) {
192
+ const targetId = existing.id;
193
+ const bundleId = typeof card.id === "string" ? card.id.trim() : "";
194
+ if (bundleId.length > 0 && bundleId !== targetId) recordRemap(bundleId, targetId);
195
+ if (mode === "skip") {
196
+ skipped.push({
197
+ id: targetId,
198
+ title: existing.title,
199
+ url: existing.source.url,
200
+ reason: "source.url 已存在"
201
+ });
202
+ continue;
203
+ }
204
+ const id = targetId;
205
+ claimedIds.add(id);
206
+ const merged = {
207
+ ...existing,
208
+ ...card,
209
+ id,
210
+ createdAt: existing.createdAt
211
+ };
212
+ pending.push({
213
+ kind: "update",
214
+ index,
215
+ id,
216
+ ...splitRelated(merged)
217
+ });
218
+ continue;
219
+ }
220
+ const bundleId = typeof card.id === "string" ? card.id.trim() : "";
221
+ let id = bundleId.length > 0 && bundleId.startsWith("kc_") ? bundleId : generateCardId();
222
+ if (localIds.has(id) || claimedIds.has(id) || remappedFrom.has(id)) {
223
+ const fresh = generateCardId();
224
+ recordRemap(id, fresh);
225
+ id = fresh;
226
+ }
227
+ claimedIds.add(id);
228
+ const createdAt = typeof card.createdAt === "string" ? card.createdAt : void 0;
229
+ const created = createdAt === void 0 ? {
230
+ ...card,
231
+ id
232
+ } : {
233
+ ...card,
234
+ id,
235
+ createdAt
236
+ };
237
+ pending.push({
238
+ kind: "create",
239
+ index,
240
+ id,
241
+ ...splitRelated(created)
242
+ });
243
+ }
244
+ const pendingIds = new Set(pending.map((item) => item.id));
245
+ const knownIds = /* @__PURE__ */ new Set([...localIds, ...pendingIds]);
246
+ const remap = new Map(remappedIds.map((entry) => [entry.from, entry.to]));
247
+ const existingForValidation = [...local, ...[...pendingIds].map(placeholderCard)];
248
+ const create = [];
249
+ const update = [];
250
+ const droppedRelated = [];
251
+ const invalid = [];
252
+ const nowIso = options.now?.toISOString();
253
+ for (const item of pending) {
254
+ let related;
255
+ if (Array.isArray(item.related) && item.related.length > 0) {
256
+ const mapped = item.related.map((value) => remap.get(value) ?? value);
257
+ const kept = mapped.filter((value) => knownIds.has(value));
258
+ const missing = Array.from(new Set(mapped.filter((value) => !knownIds.has(value))));
259
+ if (missing.length > 0) droppedRelated.push({
260
+ id: item.id,
261
+ missing
262
+ });
263
+ related = kept.length > 0 ? Array.from(new Set(kept)) : void 0;
264
+ }
265
+ const input = related === void 0 ? item.base : {
266
+ ...item.base,
267
+ related
268
+ };
269
+ const validation = validateKnowledgeCard(input, existingForValidation);
270
+ if (!validation.ok || !validation.card) {
271
+ const titleText = typeof input.title === "string" && input.title.trim().length > 0 ? input.title : void 0;
272
+ invalid.push({
273
+ index: item.index,
274
+ error: validation.error ?? "未知校验错误",
275
+ ...titleText === void 0 ? {} : { title: titleText }
276
+ });
277
+ continue;
278
+ }
279
+ const card = nowIso === void 0 ? validation.card : {
280
+ ...validation.card,
281
+ updatedAt: nowIso
282
+ };
283
+ if (item.kind === "create") create.push(card);
284
+ else update.push(card);
285
+ }
286
+ return {
287
+ create,
288
+ update,
289
+ skipped,
290
+ droppedRelated,
291
+ invalid,
292
+ remappedIds
293
+ };
294
+ }
295
+ //#endregion
296
+ export { KNOWLEDGE_BUNDLE_MAX_BYTES, KNOWLEDGE_BUNDLE_MAX_CARDS, KNOWLEDGE_BUNDLE_SCHEMA_VERSION, createKnowledgeBundle, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle };
package/lib/index.d.ts CHANGED
@@ -2,5 +2,6 @@ import { BuildGraphOptions, GraphLinkKind, KnowledgeCard, KnowledgeCardInput, Kn
2
2
  import { ValidationResult, generateCardId, validateKnowledgeCard } from "./validate.js";
3
3
  import { buildGraph } from "./graph.js";
4
4
  import { createMemoryKnowledgeCardStore } from "./store-memory.js";
5
+ import { KNOWLEDGE_BUNDLE_MAX_BYTES, KNOWLEDGE_BUNDLE_MAX_CARDS, KNOWLEDGE_BUNDLE_SCHEMA_VERSION, KnowledgeBundle, KnowledgeBundleMeta, KnowledgeImportEntry, KnowledgeImportMode, KnowledgeImportPlan, createKnowledgeBundle, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle } from "./bundle.js";
5
6
  import { AUTHOR_ALIASES, canonicalAuthor, canonicalizeCardAuthor } from "./authors.js";
6
- export { AUTHOR_ALIASES, type BuildGraphOptions, type GraphLinkKind, type KnowledgeCard, type KnowledgeCardInput, type KnowledgeCardStore, type KnowledgeCredibility, type KnowledgeFactCheck, type KnowledgeGraphData, type KnowledgeGraphLink, type KnowledgeGraphNode, type KnowledgeSource, type KnowledgeSourceType, type ValidationResult, buildGraph, canonicalAuthor, canonicalizeCardAuthor, createMemoryKnowledgeCardStore, generateCardId, validateKnowledgeCard };
7
+ export { AUTHOR_ALIASES, type BuildGraphOptions, type GraphLinkKind, KNOWLEDGE_BUNDLE_MAX_BYTES, KNOWLEDGE_BUNDLE_MAX_CARDS, KNOWLEDGE_BUNDLE_SCHEMA_VERSION, type KnowledgeBundle, type KnowledgeBundleMeta, type KnowledgeCard, type KnowledgeCardInput, type KnowledgeCardStore, type KnowledgeCredibility, type KnowledgeFactCheck, type KnowledgeGraphData, type KnowledgeGraphLink, type KnowledgeGraphNode, type KnowledgeImportEntry, type KnowledgeImportMode, type KnowledgeImportPlan, type KnowledgeSource, type KnowledgeSourceType, type ValidationResult, buildGraph, canonicalAuthor, canonicalizeCardAuthor, createKnowledgeBundle, createMemoryKnowledgeCardStore, generateCardId, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle, validateKnowledgeCard };
package/lib/index.js CHANGED
@@ -2,4 +2,5 @@ import { AUTHOR_ALIASES, canonicalAuthor, canonicalizeCardAuthor } from "./autho
2
2
  import { generateCardId, validateKnowledgeCard } from "./validate.js";
3
3
  import { buildGraph } from "./graph.js";
4
4
  import { createMemoryKnowledgeCardStore } from "./store-memory.js";
5
- export { AUTHOR_ALIASES, buildGraph, canonicalAuthor, canonicalizeCardAuthor, createMemoryKnowledgeCardStore, generateCardId, validateKnowledgeCard };
5
+ import { KNOWLEDGE_BUNDLE_MAX_BYTES, KNOWLEDGE_BUNDLE_MAX_CARDS, KNOWLEDGE_BUNDLE_SCHEMA_VERSION, createKnowledgeBundle, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle } from "./bundle.js";
6
+ export { AUTHOR_ALIASES, KNOWLEDGE_BUNDLE_MAX_BYTES, KNOWLEDGE_BUNDLE_MAX_CARDS, KNOWLEDGE_BUNDLE_SCHEMA_VERSION, buildGraph, canonicalAuthor, canonicalizeCardAuthor, createKnowledgeBundle, createMemoryKnowledgeCardStore, generateCardId, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle, validateKnowledgeCard };
package/lib/plugin.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { countGraphSummary } from "./graph.js";
2
2
  import { createFileKnowledgeCardStore } from "./knowledge-fs.js";
3
- import { createKnowledgeDeleteTool, createKnowledgeGetTool, createKnowledgeIngestTool, createKnowledgeSearchTool } from "./tool.js";
3
+ import { createKnowledgeDeleteTool, createKnowledgeExportTool, createKnowledgeGetTool, createKnowledgeImportTool, createKnowledgeIngestTool, createKnowledgeSearchTool } from "./tool.js";
4
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
+ import path from "node:path";
5
6
  import { dshHomeDir } from "@dshtrading/dsh-home";
6
7
  import { Service } from "@deepseek-ai/cordis";
7
- import path from "node:path";
8
8
  //#region src/plugin.ts
9
9
  /** Cordis 插件名 = patch 行 id(TEMPLATES §8)。 */
10
10
  const name = "dsh-trading-knowledge";
@@ -71,6 +71,8 @@ function registerKnowledgeTools(ctx, deps) {
71
71
  register(createKnowledgeSearchTool(deps.store));
72
72
  register(createKnowledgeGetTool(deps.store));
73
73
  register(createKnowledgeDeleteTool(deps.store, { onWritten: () => events()?.emit("knowledge") }));
74
+ register(createKnowledgeExportTool(deps.store));
75
+ register(createKnowledgeImportTool(deps.store, { onWritten: () => events()?.emit("knowledge") }));
74
76
  register(createKnowledgeGraphTool({ store: deps.store }));
75
77
  });
76
78
  }
package/lib/tool.d.ts CHANGED
@@ -13,5 +13,11 @@ interface KnowledgeDeleteToolOptions {
13
13
  onWritten?: () => void;
14
14
  }
15
15
  declare function createKnowledgeDeleteTool(store: KnowledgeCardStore, options?: KnowledgeDeleteToolOptions): import("@deepseek-ai/dsh-tools").ToolDefinition;
16
+ declare function createKnowledgeExportTool(store: KnowledgeCardStore): import("@deepseek-ai/dsh-tools").ToolDefinition;
17
+ interface KnowledgeImportToolOptions {
18
+ /** 可选:成功写库后的回调(与 ingest/delete 一致,emit('knowledge') 通知 UI 刷新)。 */
19
+ onWritten?: () => void;
20
+ }
21
+ declare function createKnowledgeImportTool(store: KnowledgeCardStore, options?: KnowledgeImportToolOptions): import("@deepseek-ai/dsh-tools").ToolDefinition;
16
22
  //#endregion
17
- export { KnowledgeDeleteToolOptions, KnowledgeIngestToolOptions, createFileKnowledgeCardStore, createKnowledgeDeleteTool, createKnowledgeGetTool, createKnowledgeIngestTool, createKnowledgeSearchTool };
23
+ export { KnowledgeDeleteToolOptions, KnowledgeImportToolOptions, KnowledgeIngestToolOptions, createFileKnowledgeCardStore, createKnowledgeDeleteTool, createKnowledgeExportTool, createKnowledgeGetTool, createKnowledgeImportTool, createKnowledgeIngestTool, createKnowledgeSearchTool };
package/lib/tool.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { validateKnowledgeCard } from "./validate.js";
2
+ import { createKnowledgeBundle, parseKnowledgeBundle, planKnowledgeImport, serializeKnowledgeBundle } from "./bundle.js";
2
3
  import { createFileKnowledgeCardStore } from "./knowledge-fs.js";
3
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { dshHomeDir } from "@dshtrading/dsh-home";
4
9
  //#region src/tool.ts
5
10
  /**
6
11
  * 知识库 Agent 工具(对齐 docs/design/knowledge-graph.md §4 与 dsh-tools 规范)。
@@ -10,6 +15,8 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
10
15
  * - knowledge_search: 跨字段检索(相关度排序)+ 多维过滤 + detail=full 全文返回
11
16
  * - knowledge_get: 按 id 读取单张卡片全文
12
17
  * - knowledge_delete: 证伪下架(删除卡片并清理 related 悬空引用)
18
+ * - knowledge_export: 按检索同款过滤导出单文件 JSON bundle
19
+ * - knowledge_import: bundle 先算计划(预览)再写库,source.url 去重
13
20
  */
14
21
  /** 卡片全文的统一文本渲染(knowledge_get 与 knowledge_search detail=full 共用)。 */
15
22
  function renderCardFullLines(card) {
@@ -426,5 +433,155 @@ function createKnowledgeDeleteTool(store, options = {}) {
426
433
  }
427
434
  });
428
435
  }
436
+ /** bundle 文件名里的时间戳(本地时区):用户在导出目录里按时间对号,UTC 会错位。 */
437
+ function compactTimestamp(date) {
438
+ const pad = (value) => String(value).padStart(2, "0");
439
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
440
+ }
441
+ /** 导出/导入共用的路径解析:`~` 前缀展开 + 相对路径按 process.cwd() 解析(与 node 工具惯例一致)。 */
442
+ function resolveUserPath(input) {
443
+ const trimmed = input.trim();
444
+ if (trimmed === "~") return os.homedir();
445
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return path.resolve(os.homedir(), trimmed.slice(2));
446
+ return path.resolve(process.cwd(), trimmed);
447
+ }
448
+ function createKnowledgeExportTool(store) {
449
+ return defineTool({
450
+ name: "knowledge_export",
451
+ description: "Export knowledge cards into one JSON bundle file (meta + cards) for backup or transfer to another library. Filters follow knowledge_search semantics exactly: tags (comma separated, matches any one), sourceType, credibility, and cluster (the card first tag). Without filters the whole library is exported. Import it back with knowledge_import.",
452
+ parameters: {
453
+ path: {
454
+ type: "string",
455
+ description: "Target file path (absolute, or relative to the current working directory). Omit it to write $DSH_HOME/knowledge/exports/knowledge-<yyyyMMdd-HHmmss>.json."
456
+ },
457
+ tags: {
458
+ type: "string",
459
+ description: "Comma-separated tag filter, e.g. \"宏观,高股息\"; matches any one of them."
460
+ },
461
+ sourceType: {
462
+ type: "string",
463
+ description: "Source kind filter: \"bilibili\" | \"wechat\" | \"manual\"."
464
+ },
465
+ credibility: {
466
+ type: "string",
467
+ description: "Credibility filter: \"high\" | \"medium\" | \"low\"."
468
+ },
469
+ cluster: {
470
+ type: "string",
471
+ description: "Exact subject filter (subject = the card first tag), same as knowledge_search cluster."
472
+ }
473
+ },
474
+ output: {
475
+ schema: { type: "string" },
476
+ render: (_args, value) => [{
477
+ type: "text",
478
+ text: String(value)
479
+ }]
480
+ },
481
+ async execute(raw) {
482
+ const args = raw ?? {};
483
+ const requestedPath = typeof args.path === "string" ? args.path.trim() : "";
484
+ const rawTags = typeof args.tags === "string" ? args.tags.split(",").map((tag) => tag.trim().toLowerCase()).filter(Boolean) : [];
485
+ const targetSourceType = typeof args.sourceType === "string" ? args.sourceType.trim() : "";
486
+ const targetCredibility = typeof args.credibility === "string" ? args.credibility.trim() : "";
487
+ const targetCluster = typeof args.cluster === "string" ? args.cluster.trim().toLowerCase() : "";
488
+ const selected = (await store.list()).filter((card) => {
489
+ if (targetSourceType && card.source.type !== targetSourceType) return false;
490
+ if (targetCredibility && card.credibility !== targetCredibility) return false;
491
+ if (targetCluster && (card.tags[0] ?? "").trim().toLowerCase() !== targetCluster) return false;
492
+ if (rawTags.length > 0) {
493
+ const cardTags = card.tags.map((tag) => tag.toLowerCase());
494
+ if (!rawTags.some((tag) => cardTags.includes(tag))) return false;
495
+ }
496
+ return true;
497
+ });
498
+ const filePath = requestedPath.length > 0 ? resolveUserPath(requestedPath) : path.join(dshHomeDir(), "knowledge", "exports", `knowledge-${compactTimestamp(/* @__PURE__ */ new Date())}.json`);
499
+ try {
500
+ await mkdir(path.dirname(filePath), { recursive: true });
501
+ await writeFile(filePath, serializeKnowledgeBundle(createKnowledgeBundle(selected)), "utf8");
502
+ } catch (error) {
503
+ return `[knowledge_export] 导出失败: ${error instanceof Error ? error.message : String(error)}`;
504
+ }
505
+ const filterParts = [];
506
+ if (rawTags.length > 0) filterParts.push(`标签命中任一: ${rawTags.join(" / ")}`);
507
+ if (targetSourceType) filterParts.push(`来源类型: ${targetSourceType}`);
508
+ if (targetCredibility) filterParts.push(`可信度: ${targetCredibility}`);
509
+ if (targetCluster) filterParts.push(`主体(聚类): ${targetCluster}`);
510
+ return [`[knowledge_export] 已导出 ${selected.length} 张卡片(筛选: ${filterParts.length > 0 ? filterParts.join(" | ") : "全库"})`, ` 文件: ${filePath}`].join("\n");
511
+ }
512
+ });
513
+ }
514
+ function createKnowledgeImportTool(store, options = {}) {
515
+ const { onWritten } = options;
516
+ return defineTool({
517
+ name: "knowledge_import",
518
+ description: "Import knowledge cards from a JSON bundle file produced by knowledge_export. The dedup key is source.url: mode=\"skip\" (default) keeps existing cards and reports them as skipped, mode=\"overwrite\" refreshes their content while keeping the local id and createdAt. The plan is computed first (dryRun=true only previews it, nothing is written), then written with a freshly computed plan. Related ids are remapped inside the bundle; references to cards outside it are dropped and reported.",
519
+ parameters: {
520
+ path: {
521
+ type: "string",
522
+ required: true,
523
+ description: "Bundle JSON file path (absolute, \"~/\" prefixed, or relative to the current working directory)."
524
+ },
525
+ mode: {
526
+ type: "string",
527
+ description: "Duplicate source.url handling: \"skip\" (default, keep the local card) | \"overwrite\" (refresh content, keep local id/createdAt)."
528
+ },
529
+ dryRun: {
530
+ type: "boolean",
531
+ description: "Preview the plan without writing anything (default false)."
532
+ }
533
+ },
534
+ output: {
535
+ schema: { type: "string" },
536
+ render: (_args, value) => [{
537
+ type: "text",
538
+ text: String(value)
539
+ }]
540
+ },
541
+ async execute(raw) {
542
+ const args = raw ?? {};
543
+ const requestedPath = typeof args.path === "string" ? args.path.trim() : "";
544
+ if (requestedPath.length === 0) return "[knowledge_import] 参数 path 为必填字符串(bundle JSON 文件路径)";
545
+ const rawMode = typeof args.mode === "string" ? args.mode.trim() : "";
546
+ if (rawMode !== "" && rawMode !== "skip" && rawMode !== "overwrite") return `[knowledge_import] 参数 mode 非法: ${rawMode}(允许值: skip, overwrite;省略即 skip)`;
547
+ const mode = rawMode === "overwrite" ? "overwrite" : "skip";
548
+ const dryRun = args.dryRun === true;
549
+ const filePath = resolveUserPath(requestedPath);
550
+ let text;
551
+ try {
552
+ text = await readFile(filePath, "utf8");
553
+ } catch (error) {
554
+ if (error?.code === "ENOENT") return `[knowledge_import] 文件不存在: ${filePath}`;
555
+ return `[knowledge_import] 读取失败: ${error instanceof Error ? error.message : String(error)}(${filePath})`;
556
+ }
557
+ const parsed = parseKnowledgeBundle(text);
558
+ if (!parsed.ok) return `[knowledge_import] ${parsed.error}`;
559
+ const local = await store.list();
560
+ const plan = planKnowledgeImport(local, parsed.bundle.cards, mode);
561
+ const toWrite = [...plan.create, ...plan.update];
562
+ let written = 0;
563
+ if (!dryRun && toWrite.length > 0) {
564
+ try {
565
+ if (typeof store.saveMany === "function") await store.saveMany(toWrite);
566
+ else for (const card of toWrite) await store.save(card);
567
+ } catch (error) {
568
+ return `[knowledge_import] 写入失败: ${error instanceof Error ? error.message : String(error)}(计划写入 ${toWrite.length} 张)`;
569
+ }
570
+ written = toWrite.length;
571
+ onWritten?.();
572
+ }
573
+ const lines = [`[knowledge_import] ${dryRun ? "dryRun 预览(未写库)" : `已写入 ${written} 张`}, mode=${mode}`, ` 新增 ${plan.create.length} / 更新 ${plan.update.length} / 跳过 ${plan.skipped.length} / 丢弃关联 ${plan.droppedRelated.length} / 非法 ${plan.invalid.length}`];
574
+ for (const entry of plan.skipped) lines.push(` 跳过: [${entry.id}] "${entry.title}"(${entry.reason})`);
575
+ for (const entry of plan.droppedRelated) lines.push(` 丢弃关联: [${entry.id}] 缺失 ${entry.missing.join(", ")}`);
576
+ for (const entry of plan.invalid) {
577
+ const title = entry.title === void 0 ? "" : ` "${entry.title}"`;
578
+ lines.push(` 非法: 第 ${entry.index + 1} 张卡(下标 ${entry.index})${title}: ${entry.error}`);
579
+ }
580
+ for (const entry of plan.remappedIds) lines.push(` id 重映射: ${entry.from} → ${entry.to}`);
581
+ if (toWrite.length === 0) lines.push(" (没有需要写入的卡片)");
582
+ return lines.join("\n");
583
+ }
584
+ });
585
+ }
429
586
  //#endregion
430
- export { createFileKnowledgeCardStore, createKnowledgeDeleteTool, createKnowledgeGetTool, createKnowledgeIngestTool, createKnowledgeSearchTool };
587
+ export { createFileKnowledgeCardStore, createKnowledgeDeleteTool, createKnowledgeExportTool, createKnowledgeGetTool, createKnowledgeImportTool, createKnowledgeIngestTool, createKnowledgeSearchTool };
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.3",
4
+ "version": "0.7.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -25,11 +25,11 @@
25
25
  ],
26
26
  "license": "PolyForm-Noncommercial-1.0.0",
27
27
  "dependencies": {
28
- "@dshtrading/dsh-home": "^0.6.3"
28
+ "@dshtrading/dsh-home": "^0.7.1"
29
29
  },
30
30
  "peerDependencies": {
31
- "@deepseek-ai/cordis": ">=4.0.0",
32
- "@deepseek-ai/dsh-tools": ">=0.2.1-rc.2"
31
+ "@deepseek-ai/cordis": ">=4.1.0",
32
+ "@deepseek-ai/dsh-tools": ">=0.2.2-rc.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^24.0.0",