dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.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/README.md +31 -7
- package/cordis.patch.yml +11 -5
- package/lib/client.js +5557 -3056
- package/lib/doc-page.js +7740 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +36 -9
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +51 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +239 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +18 -1
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +114 -26
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +336 -167
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/fs-atomic.d.ts +12 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +42 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +214 -24
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +10 -2
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +20 -18
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/proposals.d.ts +166 -0
- package/lib/types/proposals.d.ts.map +1 -0
- package/lib/types/proposals.js +238 -0
- package/lib/types/proposals.js.map +1 -0
- package/lib/types/runtime.d.ts +199 -7
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +728 -145
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -2
- package/lib/types/types.d.ts +61 -10
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +56 -8
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-types.d.ts +8 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +16 -25
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +11 -5
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +190 -80
- package/src/client/KnowledgePanel.tsx +42 -9
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +87 -33
- package/src/client/ProposalReview.tsx +313 -0
- package/src/client/TypeDefinitionDrawer.tsx +72 -38
- package/src/client/WikiTypeEditor.tsx +193 -77
- package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +29 -9
- package/src/client/architecture.tsx +54 -29
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +507 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +296 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +6 -1
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +263 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +317 -43
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +41 -7
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +245 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +28 -3
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +358 -174
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/fs-atomic.ts +42 -0
- package/src/index.ts +209 -22
- package/src/knowledge-bases.ts +20 -18
- package/src/proposals.ts +337 -0
- package/src/runtime.ts +736 -135
- package/src/types.ts +113 -19
- package/src/wiki-types.ts +17 -27
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
2
|
import { createReadStream, existsSync } from 'node:fs'
|
|
3
|
-
import { stat } from 'node:fs/promises'
|
|
4
|
-
import { extname, isAbsolute, join, resolve, sep } from 'node:path'
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
4
|
+
import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
5
6
|
import type { Context } from '@deepseek-ai/cordis'
|
|
6
7
|
import z from '@deepseek-ai/schemastery'
|
|
7
8
|
import { defineTool, type PreToolDecision, type ToolRunContext } from '@deepseek-ai/dsh-tools'
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
} from './types.js'
|
|
14
15
|
import { defaultKnowledgeRoot, KnowledgeRuntime, success, failure, type KnowledgeRuntimeOptions, type WikiSchemaProvider } from './runtime.js'
|
|
15
16
|
import { unwrapGroup } from './settings-volatile.js'
|
|
17
|
+
import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './document-page.js'
|
|
16
18
|
import type { ModelCall } from './extract-model.js'
|
|
17
19
|
import {
|
|
18
20
|
agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot, legacyDefaultKnowledgeRoot,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
PickerError, createDirectory, listDirectory, pickDirectory, pickerKind, pickerOf,
|
|
25
27
|
} from './directory-picker.js'
|
|
26
28
|
import { DiagnosisStore } from './diagnosis/store.js'
|
|
29
|
+
import type { DiagnosisRunRecord } from './diagnosis/types.js'
|
|
27
30
|
import { KNOWLEDGE_SETTINGS_SCHEMA, SETTINGS_NAMESPACE } from './settings-schema.js'
|
|
28
31
|
import { DEFAULT_KB_ID, KnowledgeBases } from './knowledge-bases.js'
|
|
29
32
|
import {
|
|
@@ -180,6 +183,39 @@ function clampInt(raw: string | null, fallback: number, min: number, max: number
|
|
|
180
183
|
return Math.min(max, Math.max(min, value))
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
function escapeHtml(value: string): string {
|
|
187
|
+
return value.replace(/[&<>'"]/g, character => ({
|
|
188
|
+
'&': '&', '<': '<', '>': '>', "'": ''', '"': '"',
|
|
189
|
+
})[character] ?? character)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 独立整页的 HTML 外壳。
|
|
194
|
+
*
|
|
195
|
+
* 与 Notebook 的整页同一个形状,而且**只做三件事**:标题、挂载点、把自己那个 bundle
|
|
196
|
+
* 拉起来。正文不在外壳里 —— 页面自己去 `/knowledge/documents/:docId/content` 一页一页地取,
|
|
197
|
+
* 所以这份 HTML 的大小与文档大小无关。
|
|
198
|
+
*/
|
|
199
|
+
function standaloneDocumentHtml(title: string): string {
|
|
200
|
+
return `<!doctype html>
|
|
201
|
+
<html lang="zh-CN">
|
|
202
|
+
<head>
|
|
203
|
+
<meta charset="utf-8">
|
|
204
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
205
|
+
<title>${escapeHtml(title)} · 原文</title>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<div id="tita-document-root"></div>
|
|
209
|
+
<script src="/knowledge/doc-page.js"></script>
|
|
210
|
+
</body>
|
|
211
|
+
</html>`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 独立整页那个 bundle 的位置:`lib/types/index.js` → `lib/doc-page.js`。 */
|
|
215
|
+
function documentPageBundle(): string {
|
|
216
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '../doc-page.js')
|
|
217
|
+
}
|
|
218
|
+
|
|
183
219
|
/** 删除守卫的 HTTP 状态:这三类冲突必须是 409,前端据此提示用户。 */
|
|
184
220
|
const CONFLICT_CODES = new Set([
|
|
185
221
|
'FILE_TYPE_IN_USE', 'BASE_IS_DEFAULT', 'BASE_IN_USE',
|
|
@@ -633,9 +669,40 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
633
669
|
})),
|
|
634
670
|
})))
|
|
635
671
|
ctx.tools.register(tool('list_source_types', 'List the SourceRaw business classifications (the per-knowledge-base source_type registry) together with the built-in read-only classifier prompt. Call this before add_source whenever the classification is not obvious; the registry is editable per knowledge base, so custom ids such as test_unit may exist.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => Promise.resolve(target.sourceTypeCatalog()))))
|
|
636
|
-
ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields,
|
|
672
|
+
ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields, template card, and the number of card proposals awaiting human review. Call this before propose_wiki_cards / write_knowledge_card: entity_type must be one of these ids, and the required sections/fields are what makes the card usable.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => target.wikiTypeCatalog())))
|
|
637
673
|
ctx.tools.register(tool('add_source', 'Add or update a SourceRaw document (原始可追溯文档). source_type is a per-knowledge-base business classification: call list_source_types first when unsure, and never guess silently. DSH requests one user approval before execution.', { source_type: { type: 'string', required: true, description: '来源业务分类 id(按业务场景,不是文件格式)。先调 list_source_types 拿注册表;拿不准就返回候选问用户,不要默认 document。' }, title: { type: 'string' }, content: { type: 'string', required: true }, external_id: { type: 'string' }, metadata: { type: 'json' }, source_id: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.addSource({ sourceType: args.source_type, title: args.title, content: args.content, externalId: args.external_id, metadata: args.metadata, sourceId: args.source_id, lineage: lineageOf(args, exec) }))))
|
|
638
|
-
|
|
674
|
+
/**
|
|
675
|
+
* `write_knowledge_card` —— **它只接受一条已确认、且尚未落卡的提议**(0021 §6.1)。
|
|
676
|
+
*
|
|
677
|
+
* 工具名与参数形状不变(冻结契约),但语义收紧了:没有 `proposal_id` 就没有写入。
|
|
678
|
+
* 这不是流程洁癖 —— 类型层是**收敛**的那一层,它错一条,所有现象的排序一起错;
|
|
679
|
+
* 而 Agent 手里只要还留着一条「未经确认也能写」的路,上面那条规则就只是文档上的一句话。
|
|
680
|
+
*
|
|
681
|
+
* 正常动线是「Agent 提提议 → 人在面板确认 → 面板直接落卡」。这个工具服务于另一条
|
|
682
|
+
* 更慢的路:人在面板上**只确认**(不落卡),落卡交给 Agent 这一轮来做。
|
|
683
|
+
*/
|
|
684
|
+
ctx.tools.register(tool('write_knowledge_card', 'Write a Wiki knowledge card for a proposal that a HUMAN has already confirmed (status=confirmed, not yet written). Cards can no longer be written without a confirmed proposal_id: call propose_wiki_cards first, then ask the user to review it in the knowledge panel. DSH requests one user approval before execution.', { proposal_id: { type: 'string', required: true, description: '已确认、尚未落卡的提议 id。它来自 propose_wiki_cards 的返回,确认动作只能由人在面板上做' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.applyProposal(args.proposal_id, lineageOf(args, exec)))))
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `propose_wiki_cards` —— 草案工具(0024 §4)。
|
|
688
|
+
*
|
|
689
|
+
* 入参是一条 `source_id`(提议挂在触发它的那条 SourceRaw 上)加一批草案。
|
|
690
|
+
* **它不落库**:草案停在待办队列里,等人确认。
|
|
691
|
+
*/
|
|
692
|
+
ctx.tools.register(tool('propose_wiki_cards', 'Propose Wiki card drafts attached to a SourceRaw. This does NOT write any card: drafts stay in the review queue until a HUMAN confirms them in the knowledge panel. Call list_wiki_types first to pick entity_type and see the required sections/fields.', { source_id: { type: 'string', required: true, description: '触发这批提议的那条 SourceRaw 的 source_id(先 add_source)' }, proposals: { type: 'array', required: true, description: '草案数组,每项 { action?, entity_type, entity_id, title, summary?, content, source_refs?, metadata? };content 是卡片正文(小节 + ```json diagnosis 结构化块)' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async args => withRuntime(args.kb, target => target.proposeCards({ sourceId: args.source_id, proposals: (Array.isArray(args.proposals) ? args.proposals : []).map(item => ({ action: item?.action, entityType: item?.entity_type, entityId: item?.entity_id, title: item?.title, summary: item?.summary, content: item?.content, sourceRefs: item?.source_refs, metadata: item?.metadata })) }))))
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* `list_card_proposals` —— Agent 自己看得到待办队列。
|
|
696
|
+
*
|
|
697
|
+
* 少了它,Agent 会**反复提同一条**:它看不到「我已经提过了」「这条被人拒了」。
|
|
698
|
+
* 判重其实在存储层(内容指纹),但工具层给出清单,模型才有机会换一种写法。
|
|
699
|
+
*/
|
|
700
|
+
ctx.tools.register(tool('list_card_proposals', 'List Wiki card proposals awaiting human review (plus the rejected ones, so you do not re-propose something a human already turned down). Proposals never land in 02_wiki until a human confirms them.', { status: { type: 'string', enum: ['pending', 'confirmed', 'rejected', 'all'], description: '默认 pending' }, kb: { type: 'string' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, async target => {
|
|
701
|
+
const wanted = args.status ?? 'pending'
|
|
702
|
+
const all = await target.cardProposals().list()
|
|
703
|
+
const items = all.filter(item => wanted === 'all' || item.status === wanted)
|
|
704
|
+
return success({ total: items.length, proposals: items.map(item => ({ id: item.id, status: item.status, trigger: item.trigger, action: item.action, entity_type: item.entityType, entity_id: item.entityId, title: item.title, source_ref: item.sourceRef, issues: item.issues, ...(item.reason ? { reason: item.reason } : {}) })) })
|
|
705
|
+
})))
|
|
639
706
|
ctx.tools.register(tool('set_knowledge_status', 'Change a Wiki card status. problem requires problem_note and recovery must pass through unreviewed. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, status: { type: 'string', enum: ['unreviewed', 'draft', 'reviewed', 'active', 'deprecated', 'problem'], required: true }, problem_note: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.setStatus(args.entity_id, args.status as KnowledgeStatus, args.problem_note, lineageOf(args, exec)))))
|
|
640
707
|
ctx.tools.register(tool('rebuild_knowledge', 'Rebuild schema/wiki/compiled knowledge indexes. DSH requests one user approval before execution.', { only: { type: 'string', enum: ['all', 'schema', 'wiki', 'embeddings'] }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.rebuild(args.only ?? 'all', lineageOf(args, exec)))))
|
|
641
708
|
ctx.tools.register(tool('record_feedback', 'Record bounded feedback for a knowledge result. DSH requests one user approval before execution.', { feedback_id: { type: 'string' }, unit_id: { type: 'string' }, rating: { type: 'integer', required: true }, comment: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.recordFeedback({ feedbackId: args.feedback_id, unitId: args.unit_id, rating: args.rating, comment: args.comment, sessionKey: args.session_key, lineage: lineageOf(args, exec) }))))
|
|
@@ -663,16 +730,26 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
663
730
|
* 每个知识库有自己的 `05_diagnosis/`,和它的 wiki / 编译产物同源;缓存键用
|
|
664
731
|
* rootDir 而不是 baseId,因为独立 runtime(未挂到知识库列表上的)也要能读写。
|
|
665
732
|
*/
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
733
|
+
const storeOf = (target: KnowledgeRuntime): DiagnosisStore => target.diagnosisStore()
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* `/diagnosis/graph` 与 `/diagnosis/sample` 的同一个响应体。
|
|
737
|
+
*
|
|
738
|
+
* 两条路由必须长得一样:界面拿到的要么是「这个库当前的知识」,要么是「刚载入示例之后
|
|
739
|
+
* 的知识」,它不该为第二种再写一条解析分支。
|
|
740
|
+
*
|
|
741
|
+
* 字段各有理由,不是把 snapshot 摊平:
|
|
742
|
+
* - `issues` 三层校验的结果要能直接显示,而不是只写进日志;
|
|
743
|
+
* - `source` / `migrated` 让界面说清「读的是卡片还是旧 jsonl」「迁移有没有生效」;
|
|
744
|
+
* - `canLoadSample` 是空态的开关 —— 这个库还什么都没有时才为真。
|
|
745
|
+
*/
|
|
746
|
+
const diagnosisPayload = (snapshot: Awaited<ReturnType<DiagnosisStore['snapshot']>>): Record<string, unknown> => ({
|
|
747
|
+
graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
|
|
748
|
+
seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source,
|
|
749
|
+
canLoadSample: snapshot.canLoadSample, problems: snapshot.problems,
|
|
750
|
+
// 待确认的提议条数:归因这一栏只挂角标,review 在「知识库 → Wiki」的待办区。
|
|
751
|
+
proposals: snapshot.proposals,
|
|
752
|
+
})
|
|
676
753
|
|
|
677
754
|
const routes = [
|
|
678
755
|
{ kind: 'exact' as const, path: '/knowledge/stats', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
@@ -818,20 +895,80 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
818
895
|
{ kind: 'exact' as const, path: '/diagnosis/graph', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
819
896
|
const resolved = resolveBase(query(req).searchParams.get('kb'))
|
|
820
897
|
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
898
|
+
return send(res, 200, success(diagnosisPayload(await storeOf(resolved.runtime).snapshot())))
|
|
899
|
+
} },
|
|
900
|
+
/**
|
|
901
|
+
* 载入**内置示例**(0021 的示例内容,原来是自动写入的)。
|
|
902
|
+
*
|
|
903
|
+
* 它是一次显式的**写**:用户在这个库上还没录任何排查知识时,界面上那枚
|
|
904
|
+
* 「载入内置示例」按钮打的就是这里。单独一条路由而不是让读取顺手写 ——
|
|
905
|
+
* 读接口写用户的 `01_source_truth/` 正是上一版的毛病。
|
|
906
|
+
*/
|
|
907
|
+
{ kind: 'exact' as const, path: '/diagnosis/sample', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
908
|
+
if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
|
|
909
|
+
const resolved = resolveBase(query(req).searchParams.get('kb'))
|
|
910
|
+
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
911
|
+
const store = storeOf(resolved.runtime)
|
|
912
|
+
const { problems } = await store.loadSample()
|
|
913
|
+
return send(res, 200, success(diagnosisPayload(await store.snapshot()), problems))
|
|
829
914
|
} },
|
|
830
915
|
{ kind: 'exact' as const, path: '/diagnosis/cases', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
831
916
|
const resolved = resolveBase(query(req).searchParams.get('kb'))
|
|
832
917
|
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
833
918
|
return send(res, 200, success({ cases: (await storeOf(resolved.runtime).snapshot()).cases }))
|
|
834
919
|
} },
|
|
920
|
+
/**
|
|
921
|
+
* 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并顺手**提议**一张案例卡片。
|
|
922
|
+
*
|
|
923
|
+
* 两件事一起做但性质不同:日志是实例层的原始记录(Agent 的产物,可以直接落),
|
|
924
|
+
* 案例卡片落在收敛的那一侧(只能提议,等人确认)。所以响应里分别给 `sourceId`
|
|
925
|
+
* 与 `proposalId` —— 界面要能说清「记录已经留下了,但那张卡片还在等人 review」。
|
|
926
|
+
*/
|
|
927
|
+
{ kind: 'exact' as const, path: '/diagnosis/run', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
928
|
+
if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
|
|
929
|
+
const body = await readBody(req, 2_000_000)
|
|
930
|
+
const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
|
|
931
|
+
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
932
|
+
const record = body.record as DiagnosisRunRecord | undefined
|
|
933
|
+
if (!record || typeof record.runId !== 'string' || typeof record.symptomId !== 'string') {
|
|
934
|
+
return send(res, 400, envelopeError('RUN_RECORD_INVALID', 'record 必须是 { runId, symptomId, startedAt, durationMinutes, path, evidence, rootCause?, misleading? }'))
|
|
935
|
+
}
|
|
936
|
+
const outcome = await storeOf(resolved.runtime).recordRun(record)
|
|
937
|
+
if (outcome.problems.length > 0 && outcome.sourceId === undefined) {
|
|
938
|
+
return send(res, 200, failure({ sourceId: '', proposalId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
|
|
939
|
+
}
|
|
940
|
+
return send(res, 200, success({ sourceId: outcome.sourceId ?? '', proposalId: outcome.proposalId ?? '' }, outcome.problems))
|
|
941
|
+
} },
|
|
942
|
+
/**
|
|
943
|
+
* 卡片提议的待办队列(0021 §6.1)。
|
|
944
|
+
*
|
|
945
|
+
* 读接口给「知识库 → Wiki」的待办区用;写接口是**人的 review 动作**,
|
|
946
|
+
* 刻意不做成 Agent 工具 —— 确认权收在人手里,Agent 只有 `propose_wiki_cards`。
|
|
947
|
+
*/
|
|
948
|
+
{ kind: 'exact' as const, path: '/knowledge/card-proposals', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
949
|
+
const url = query(req)
|
|
950
|
+
const resolved = resolveBase(url.searchParams.get('kb'))
|
|
951
|
+
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
952
|
+
const wanted = url.searchParams.get('status') ?? 'pending'
|
|
953
|
+
const all = await resolved.runtime.cardProposals().list()
|
|
954
|
+
const items = all.filter(item => wanted === 'all' || item.status === wanted)
|
|
955
|
+
return send(res, 200, success({ total: items.length, proposals: items }))
|
|
956
|
+
} },
|
|
957
|
+
{ kind: 'exact' as const, path: '/knowledge/card-proposals/review', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
958
|
+
if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
|
|
959
|
+
const body = await readBody(req, 2_000_000)
|
|
960
|
+
const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
|
|
961
|
+
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
962
|
+
const action = body.action === 'reject' ? 'reject' : body.action === 'confirm' ? 'confirm' : 'confirm-and-write'
|
|
963
|
+
return sendEnvelope(res, await resolved.runtime.reviewProposal({
|
|
964
|
+
proposalId: String(body.proposal_id ?? ''),
|
|
965
|
+
action,
|
|
966
|
+
...(body.reviewer ? { reviewer: String(body.reviewer) } : {}),
|
|
967
|
+
...(body.reason ? { reason: String(body.reason) } : {}),
|
|
968
|
+
...(body.patch ? { patch: body.patch as { title?: string; summary?: string; content?: string } } : {}),
|
|
969
|
+
lineage: lineageOf(body),
|
|
970
|
+
}))
|
|
971
|
+
} },
|
|
835
972
|
{ kind: 'prefix' as const, path: '/knowledge', handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
836
973
|
const method = req.method ?? 'GET'
|
|
837
974
|
try {
|
|
@@ -852,6 +989,25 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
852
989
|
return sendEnvelope(res, await withRuntime(body.kb, target => target.rebuild(body.only ?? 'all', lineageOf(body))))
|
|
853
990
|
}
|
|
854
991
|
|
|
992
|
+
/**
|
|
993
|
+
* 独立整页的 bundle(`lib/doc-page.js`,构建产物)。
|
|
994
|
+
*
|
|
995
|
+
* 它挂在本插件的 `/knowledge` 前缀下而不是另开一条顶层路由:整页是这个插件
|
|
996
|
+
* 的一部分,路径上不该假装是一个独立服务。`no-cache` 是刻意的 —— 这份文件在
|
|
997
|
+
* 开发时会被反复重建,缓存住它等于让人对着旧页面调试。
|
|
998
|
+
*/
|
|
999
|
+
if (rest.length === 1 && rest[0] === 'doc-page.js') {
|
|
1000
|
+
if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
|
|
1001
|
+
try {
|
|
1002
|
+
const bundle = await readFile(documentPageBundle())
|
|
1003
|
+
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' })
|
|
1004
|
+
res.end(bundle)
|
|
1005
|
+
} catch {
|
|
1006
|
+
return send(res, 404, envelopeError('NOT_FOUND', 'doc-page.js 不存在;先构建这个插件(pnpm build)'))
|
|
1007
|
+
}
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
|
|
855
1011
|
// ---------------------------------------------- /knowledge/bases
|
|
856
1012
|
if (rest[0] === 'bases') {
|
|
857
1013
|
if (rest.length === 1) {
|
|
@@ -1025,6 +1181,12 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
1025
1181
|
if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
|
|
1026
1182
|
if (rest.length === 2) {
|
|
1027
1183
|
if (method === 'GET') {
|
|
1184
|
+
/**
|
|
1185
|
+
* 正文在这里**按 `SOURCE_PREVIEW_CHARS` 封顶**(`readDocument` 的默认值)。
|
|
1186
|
+
* 抽屉只渲染前 4000 字符,而过去这个接口把整份正文发过去 —— 传输、JSON
|
|
1187
|
+
* 解析、进 React state 全按全文的量级付费,那就是「打开弹层要等半天」。
|
|
1188
|
+
* 要读全文有独立整页:`.../view`,一页一页地取。
|
|
1189
|
+
*/
|
|
1028
1190
|
const document = await resolved.runtime.readDocument(docId)
|
|
1029
1191
|
if (!document) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
|
|
1030
1192
|
return send(res, 200, success({ document }))
|
|
@@ -1042,6 +1204,31 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
|
|
|
1042
1204
|
if (versions.length === 0) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
|
|
1043
1205
|
return send(res, 200, success({ versions }))
|
|
1044
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* 独立整页的**正文分页**接口:一次一段,切点对齐空行(见 `documentContentPage`)。
|
|
1209
|
+
*
|
|
1210
|
+
* 它是 `.../view` 那个页面的数据源,也是「原文全文」唯一的分发口 ——
|
|
1211
|
+
* 面板那条明细接口刻意不发全文。
|
|
1212
|
+
*/
|
|
1213
|
+
if (rest.length === 3 && rest[2] === 'content') {
|
|
1214
|
+
if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
|
|
1215
|
+
const body = await resolved.runtime.readDocumentBody(docId)
|
|
1216
|
+
if (body === undefined) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
|
|
1217
|
+
const offset = clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
|
|
1218
|
+
const limit = clampInt(url.searchParams.get('limit'), DOC_PAGE_CHARS, 1, DOC_PAGE_CHARS_MAX)
|
|
1219
|
+
return send(res, 200, success(documentContentPage(body, offset, limit)))
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* 独立整页的外壳。前端从抽屉里「新页面打开」跳过来,也可以直接粘贴这个地址。
|
|
1223
|
+
*/
|
|
1224
|
+
if (rest.length === 3 && rest[2] === 'view') {
|
|
1225
|
+
if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
|
|
1226
|
+
const document = await resolved.runtime.readDocument(docId)
|
|
1227
|
+
if (!document) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
|
|
1228
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' })
|
|
1229
|
+
res.end(standaloneDocumentHtml(document.title))
|
|
1230
|
+
return
|
|
1231
|
+
}
|
|
1045
1232
|
if (rest.length === 3 && rest[2] === 'revert') {
|
|
1046
1233
|
if (method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
|
|
1047
1234
|
const body = await readBody(req)
|
package/src/knowledge-bases.ts
CHANGED
|
@@ -29,12 +29,12 @@ import { homedir } from 'node:os'
|
|
|
29
29
|
import { dirname, join, resolve } from 'node:path'
|
|
30
30
|
import {
|
|
31
31
|
BUILTIN_SOURCE_TYPES, defaultSourceTypes, FALLBACK_SOURCE_TYPE, isBuiltinSourceType,
|
|
32
|
-
MAX_SOURCE_TYPES, SOURCE_TYPE_ID_PATTERN, validateSourceTypeDraft, validateSourceTypeText, WIKI_TYPE_ID_PATTERN,
|
|
32
|
+
LEGACY_SOURCE_TYPE_IDS, MAX_SOURCE_TYPES, SOURCE_TYPE_ID_PATTERN, validateSourceTypeDraft, validateSourceTypeText, WIKI_TYPE_ID_PATTERN,
|
|
33
33
|
type Envelope, type KnowledgeBase, type KnowledgeConfig,
|
|
34
34
|
type SourceTypeDef, type SourceTypeDraft, type WikiSchemaMode, type WikiTypeDef,
|
|
35
35
|
} from './types.js'
|
|
36
36
|
import {
|
|
37
|
-
defaultWikiTypes, isBuiltinWikiType, parseFieldLines, parseSectionLines, templateCardFor,
|
|
37
|
+
defaultWikiTypes, isBuiltinWikiType, LEGACY_WIKI_TYPE_IDS, parseFieldLines, parseSectionLines, templateCardFor,
|
|
38
38
|
validateWikiTypeDraft, validateWikiTypeText, type WikiTypeDraft,
|
|
39
39
|
} from './wiki-types.js'
|
|
40
40
|
import { defaultKnowledgeRoot, failure, KnowledgeRuntime, success } from './runtime.js'
|
|
@@ -592,7 +592,6 @@ export class KnowledgeBases {
|
|
|
592
592
|
layer: patch.layer ?? existing.layer,
|
|
593
593
|
sections: patch.sections ?? existing.sections,
|
|
594
594
|
fields: patch.fields ?? existing.fields,
|
|
595
|
-
sourceTypes: patch.sourceTypes ?? existing.sourceTypes,
|
|
596
595
|
examples: patch.examples ?? existing.examples,
|
|
597
596
|
}
|
|
598
597
|
const problem = validateWikiTypeText(merged)
|
|
@@ -606,7 +605,6 @@ export class KnowledgeBases {
|
|
|
606
605
|
layer: merged.layer === 'instance' ? 'instance' : 'type',
|
|
607
606
|
sections,
|
|
608
607
|
fields,
|
|
609
|
-
sourceTypes: this.stringList(merged.sourceTypes),
|
|
610
608
|
examples: this.stringList(merged.examples),
|
|
611
609
|
template: templateCardFor(sections, fields),
|
|
612
610
|
enabled: patch.enabled === undefined ? existing.enabled : patch.enabled !== false,
|
|
@@ -664,7 +662,6 @@ export class KnowledgeBases {
|
|
|
664
662
|
layer: draft.layer === 'instance' ? 'instance' : 'type',
|
|
665
663
|
sections,
|
|
666
664
|
fields,
|
|
667
|
-
sourceTypes: this.stringList(draft.sourceTypes),
|
|
668
665
|
examples: this.stringList(draft.examples),
|
|
669
666
|
template: templateCardFor(sections, fields),
|
|
670
667
|
enabled: true,
|
|
@@ -683,15 +680,21 @@ export class KnowledgeBases {
|
|
|
683
680
|
*
|
|
684
681
|
* 与 `normalizeSourceTypes` 同一套语义:老配置(没有 `wikiTypes` 字段)升级时补齐内置,
|
|
685
682
|
* 缺失的内置类型被恢复(内置只允许停用,不允许删除)。
|
|
683
|
+
*
|
|
684
|
+
* 0040 的两次改名(`check` → `check_point`、`failure-mode` → `failure_reason`)在这里
|
|
685
|
+
* 也认别名:存量配置里那一项写着旧 id,不认的话它会被当成一个**自定义类型**留下来,
|
|
686
|
+
* 而内置的那一项会被补回来 —— 于是注册表里同时有 `diagnosis/check` 与
|
|
687
|
+
* `diagnosis/check-point` 两个类型,卡片该归哪个就再也说不清了。
|
|
686
688
|
*/
|
|
687
689
|
private normalizeWikiTypes(raw: unknown): WikiTypeDef[] {
|
|
688
690
|
const persisted = Array.isArray(raw) ? raw : []
|
|
689
691
|
const byId = new Map<string, Record<string, unknown>>()
|
|
690
692
|
for (const item of persisted) {
|
|
691
693
|
const value = record(item)
|
|
692
|
-
const
|
|
694
|
+
const rawId = typeof value.id === 'string' ? value.id.trim() : ''
|
|
695
|
+
const id = LEGACY_WIKI_TYPE_IDS[rawId] ?? rawId
|
|
693
696
|
if (!id || !WIKI_TYPE_ID_PATTERN.test(id) || byId.has(id)) continue
|
|
694
|
-
byId.set(id, value)
|
|
697
|
+
byId.set(id, { ...value, id })
|
|
695
698
|
}
|
|
696
699
|
const defaults = defaultWikiTypes()
|
|
697
700
|
const builtins = defaults.map(def => {
|
|
@@ -703,7 +706,7 @@ export class KnowledgeBases {
|
|
|
703
706
|
const customs: WikiTypeDef[] = []
|
|
704
707
|
for (const [id, value] of byId) {
|
|
705
708
|
if (isBuiltinWikiType(id)) continue
|
|
706
|
-
const base: WikiTypeDef = { id, label: id, description: '', layer: 'type', sections: [], fields: [],
|
|
709
|
+
const base: WikiTypeDef = { id, label: id, description: '', layer: 'type', sections: [], fields: [], examples: [], template: '', enabled: true, builtin: false, cardCount: 0 }
|
|
707
710
|
const draft = this.wikiTypeFromRecord(base, value)
|
|
708
711
|
// 只有能写出骨架的自定义类型才值得留下;否则它既没有模板也校验不了。
|
|
709
712
|
if (validateWikiTypeText(draft).ok) customs.push(draft)
|
|
@@ -722,7 +725,6 @@ export class KnowledgeBases {
|
|
|
722
725
|
layer: value.layer === 'instance' ? 'instance' : base.layer,
|
|
723
726
|
sections: sections.length > 0 ? sections : base.sections.map(section => ({ ...section })),
|
|
724
727
|
fields: Array.isArray(value.fields) ? fields : base.fields.map(field => ({ ...field })),
|
|
725
|
-
sourceTypes: Array.isArray(value.sourceTypes) ? this.stringList(value.sourceTypes) : [...base.sourceTypes],
|
|
726
728
|
examples: this.stringList(value.examples).length > 0 ? this.stringList(value.examples) : [...base.examples],
|
|
727
729
|
template: textOr(value.template, base.template),
|
|
728
730
|
enabled: value.enabled !== false,
|
|
@@ -735,7 +737,7 @@ export class KnowledgeBases {
|
|
|
735
737
|
private wikiTypesDifferFromDefaults(types: WikiTypeDef[]): boolean {
|
|
736
738
|
const signature = (list: WikiTypeDef[]): string => JSON.stringify(list.map(def => ({
|
|
737
739
|
id: def.id, label: def.label, description: def.description, layer: def.layer, enabled: def.enabled,
|
|
738
|
-
sections: def.sections, fields: def.fields,
|
|
740
|
+
sections: def.sections, fields: def.fields, examples: def.examples,
|
|
739
741
|
})))
|
|
740
742
|
const defaults = defaultWikiTypes()
|
|
741
743
|
if (types.length !== defaults.length) return true
|
|
@@ -880,18 +882,22 @@ export class KnowledgeBases {
|
|
|
880
882
|
/**
|
|
881
883
|
* 把磁盘上的 `sourceTypes` 规整成一个**必定包含全部内置分类**的合法注册表。
|
|
882
884
|
*
|
|
883
|
-
*
|
|
885
|
+
* 三个关键行为:
|
|
884
886
|
* 1. 老注册表(没有 `sourceTypes` 字段)升级时自动补上全部内置分类;
|
|
885
|
-
* 2.
|
|
887
|
+
* 2. 任何缺失的内置分类都会被补回来——内置分类只允许停用,不允许删除;
|
|
888
|
+
* 3. **改名过的内置分类按别名认**(`LEGACY_SOURCE_TYPE_IDS`):存量配置里那一项写着
|
|
889
|
+
* 旧 id,不认的话它会被当成一个自定义分类留下来,而新的内置项同时被补回来 ——
|
|
890
|
+
* 于是「注册表里到底有什么」永远说不清。
|
|
886
891
|
*/
|
|
887
892
|
private normalizeSourceTypes(raw: unknown): SourceTypeDef[] {
|
|
888
893
|
const persisted = Array.isArray(raw) ? raw : []
|
|
889
894
|
const byId = new Map<string, Record<string, unknown>>()
|
|
890
895
|
for (const item of persisted) {
|
|
891
896
|
const value = record(item)
|
|
892
|
-
const
|
|
897
|
+
const rawId = typeof value.id === 'string' ? value.id.trim() : ''
|
|
898
|
+
const id = LEGACY_SOURCE_TYPE_IDS[rawId] ?? rawId
|
|
893
899
|
if (!id || !SOURCE_TYPE_ID_PATTERN.test(id) || byId.has(id)) continue
|
|
894
|
-
byId.set(id, value)
|
|
900
|
+
byId.set(id, { ...value, id })
|
|
895
901
|
}
|
|
896
902
|
const defaults = defaultSourceTypes()
|
|
897
903
|
const builtins = defaults.map(def => {
|
|
@@ -1065,7 +1071,6 @@ export class KnowledgeBases {
|
|
|
1065
1071
|
layer: value.layer === 'instance' ? 'instance' : previous.layer,
|
|
1066
1072
|
sections: Array.isArray(value.sections) ? value.sections : previous.sections,
|
|
1067
1073
|
fields: Array.isArray(value.fields) ? value.fields : previous.fields,
|
|
1068
|
-
sourceTypes: Array.isArray(value.sourceTypes) ? value.sourceTypes : previous.sourceTypes,
|
|
1069
1074
|
examples: Array.isArray(value.examples) ? value.examples : previous.examples,
|
|
1070
1075
|
}
|
|
1071
1076
|
const problem = validateWikiTypeText(draft)
|
|
@@ -1079,7 +1084,6 @@ export class KnowledgeBases {
|
|
|
1079
1084
|
layer: draft.layer === 'instance' ? 'instance' : 'type',
|
|
1080
1085
|
sections,
|
|
1081
1086
|
fields,
|
|
1082
|
-
sourceTypes: this.stringList(draft.sourceTypes),
|
|
1083
1087
|
examples: this.stringList(draft.examples),
|
|
1084
1088
|
template: templateCardFor(sections, fields),
|
|
1085
1089
|
enabled: value.enabled === undefined ? previous.enabled : value.enabled !== false,
|
|
@@ -1098,7 +1102,6 @@ export class KnowledgeBases {
|
|
|
1098
1102
|
layer: value.layer === 'instance' ? 'instance' : previous?.layer ?? 'type',
|
|
1099
1103
|
sections: Array.isArray(value.sections) ? value.sections : previous?.sections ?? [],
|
|
1100
1104
|
fields: Array.isArray(value.fields) ? value.fields : previous?.fields ?? [],
|
|
1101
|
-
sourceTypes: Array.isArray(value.sourceTypes) ? value.sourceTypes : previous?.sourceTypes ?? [],
|
|
1102
1105
|
examples: Array.isArray(value.examples) ? value.examples : previous?.examples ?? [],
|
|
1103
1106
|
}
|
|
1104
1107
|
const problem = previous ? validateWikiTypeText(draft) : validateWikiTypeDraft(draft, new Set(existing.map(def => def.id)))
|
|
@@ -1112,7 +1115,6 @@ export class KnowledgeBases {
|
|
|
1112
1115
|
layer: draft.layer === 'instance' ? 'instance' : 'type',
|
|
1113
1116
|
sections,
|
|
1114
1117
|
fields,
|
|
1115
|
-
sourceTypes: this.stringList(draft.sourceTypes),
|
|
1116
1118
|
examples: this.stringList(draft.examples),
|
|
1117
1119
|
template: templateCardFor(sections, fields),
|
|
1118
1120
|
enabled: value.enabled === undefined ? previous?.enabled ?? true : value.enabled !== false,
|