dsh-data-domain-knowledge 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The convert host — the DSH-facing half of multi-format conversion.
|
|
3
|
+
*
|
|
4
|
+
* Since 0018 this lives inside `plugins/dsh-data-domain-knowledge` (issue 0018):
|
|
5
|
+
* conversion is a sub-capability of knowledge, mounted as one plugin, with one
|
|
6
|
+
* sidebar entry and one settings surface for the user. The kernel
|
|
7
|
+
* (`dsh-convert-core`) stays an independent, optional package that
|
|
8
|
+
* `core-loader.ts` imports lazily.
|
|
9
|
+
*
|
|
10
|
+
* Layering this file must keep:
|
|
11
|
+
*
|
|
12
|
+
* kernel (convert-core) input → knowledge-ready Markdown + assets + console
|
|
13
|
+
* this host settings, agent tools, /convert route, ingestion
|
|
14
|
+
*
|
|
15
|
+
* Nothing about parsing lives here; nothing about DSH lives in the kernel. That
|
|
16
|
+
* is why the same console still works standalone at `tita-convert`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
20
|
+
import { stat } from 'node:fs/promises'
|
|
21
|
+
import { basename, resolve } from 'node:path'
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
23
|
+
import z from '@deepseek-ai/schemastery'
|
|
24
|
+
import { defineTool, type PreToolDecision, type ToolRunContext } from '@deepseek-ai/dsh-tools'
|
|
25
|
+
import type {} from '@deepseek-ai/dsh-settings'
|
|
26
|
+
import type { ConvertRequest, ConvertService, ConvertTask } from 'dsh-convert-core'
|
|
27
|
+
import { ConvertCoreMissingError, loadConvertCore } from './core-loader.js'
|
|
28
|
+
import { tmpRoot, workspaceOutputRoot } from '../layout.js'
|
|
29
|
+
import { ingestDelivery, type IngestOutcome, type IngestTarget } from './ingest.js'
|
|
30
|
+
import {
|
|
31
|
+
CONVERT_SETTINGS_NAMESPACE,
|
|
32
|
+
DEFAULT_CONVERT_CONFIG,
|
|
33
|
+
failure,
|
|
34
|
+
success,
|
|
35
|
+
WRITE_TOOLS,
|
|
36
|
+
type ConvertPluginConfig,
|
|
37
|
+
type Envelope,
|
|
38
|
+
} from './types.js'
|
|
39
|
+
|
|
40
|
+
export { CONVERT_SETTINGS_NAMESPACE, DEFAULT_CONVERT_CONFIG, defaultConvertRoot } from './types.js'
|
|
41
|
+
|
|
42
|
+
/** Route prefix the console and API are mounted on. Frozen contract. */
|
|
43
|
+
export const MOUNT_PATH = '/convert'
|
|
44
|
+
|
|
45
|
+
const PREVIEW_CHARS = 1200
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 转换的设置 schema。
|
|
49
|
+
*
|
|
50
|
+
* 0029 起它**没有路径字段**:交付物与工作区都从知识插件的 `agentRoot` 推出来
|
|
51
|
+
* (`<agentRoot>/output/<日期>/`、`<agentRoot>/tmp/`)。一个 Agent 只有一个根路径,
|
|
52
|
+
* 所以这里再放一个 `rootDir` 就等于把「东西存在哪」这个答案劈成两半。
|
|
53
|
+
*
|
|
54
|
+
* 定义本身搬到了 `./settings-schema.js`(纯数据、可被 `Config` 组合并在
|
|
55
|
+
* `node --test` 里独立解析);这里 re-export,保持既有出口不变。
|
|
56
|
+
*/
|
|
57
|
+
export { CONVERT_SETTINGS_SCHEMA } from './settings-schema.js'
|
|
58
|
+
|
|
59
|
+
/** What the convert host needs from the knowledge plugin it now lives in. */
|
|
60
|
+
export interface ConvertHostDeps {
|
|
61
|
+
/** The live knowledge runtime; the seam may only call `addSource` on it. */
|
|
62
|
+
runtime(): IngestTarget
|
|
63
|
+
/** Absolute knowledge root the runtime currently writes to. */
|
|
64
|
+
knowledgeRoot(): string
|
|
65
|
+
/**
|
|
66
|
+
* 当前工作项目(交付物的落点)。省略时退回进程的工作目录。
|
|
67
|
+
*
|
|
68
|
+
* 转换内核是**宿主级单例**,而「工作项目」是**每个会话各自**的 —— 内核这一层拿不到
|
|
69
|
+
* 会话,所以这里只能给一个宿主级的答案。单项目部署(一个 DSH 进程服务同一个项目)下
|
|
70
|
+
* 两者一致;多项目共用同一个宿主时,交付物会集中落到宿主启动目录下,而不是各自项目里。
|
|
71
|
+
* 要做到完全按会话分,需要内核支持**按请求**解析输出目录,那是内核侧的一处改造。
|
|
72
|
+
*/
|
|
73
|
+
workspace?(): string
|
|
74
|
+
/**
|
|
75
|
+
* Resolve an ingest target for one knowledge base (issue 0019). Optional so the
|
|
76
|
+
* host still works when the registry is unavailable; when it is absent every
|
|
77
|
+
* conversion lands in the default knowledge base.
|
|
78
|
+
*/
|
|
79
|
+
resolveTarget?(kbId?: string): { runtime: IngestTarget; knowledgeRoot: string }
|
|
80
|
+
/**
|
|
81
|
+
* Tita Agent 根路径(0029)。转换的交付物与工作区都从它推出来,所以转换
|
|
82
|
+
* **没有自己的路径设置** —— 一个 Agent 只有一个根路径。
|
|
83
|
+
*/
|
|
84
|
+
agentRoot(): string
|
|
85
|
+
/**
|
|
86
|
+
* 已解析的 `convert` 设置组(来自插件 entry 的 config)。
|
|
87
|
+
*
|
|
88
|
+
* 0.1.7 起设置不再是命名空间,而是随 `apply(ctx, config)` 进来的 entry config,
|
|
89
|
+
* 所以这里由调用方把那一组传下来。省略时用 `DEFAULT_CONVERT_CONFIG`。
|
|
90
|
+
*
|
|
91
|
+
* 允许给**函数**的理由与意图那组相同(见 `intent/host.ts` 的 `IntentHostDeps.config`):
|
|
92
|
+
* 字段是 volatile,解包是一次快照,而 volatile 改动不会重载插件。
|
|
93
|
+
*/
|
|
94
|
+
config?: Partial<ConvertPluginConfig> | (() => Partial<ConvertPluginConfig>)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `applyConvert` 交给组合根的那一个把手,与 `intent/host.ts` 的 `IntentHost` 同形。
|
|
99
|
+
*
|
|
100
|
+
* 转换这一组也有 volatile 字段(`maxConcurrent` / `maxInputBytes` / `retentionDays` /
|
|
101
|
+
* `toolWaitMs` / `ingestEnabled`),其中一个解包漏了就是沉默的错误:`maxInputBytes`
|
|
102
|
+
* 是引用时 `info.size > config.maxInputBytes` 恒为 false,体积上限静默消失;
|
|
103
|
+
* `ingestEnabled` 是引用时它恒为真,开关关不掉。所以这一组也要能就地重灌。
|
|
104
|
+
*/
|
|
105
|
+
export interface ConvertHost {
|
|
106
|
+
/** 按**当前**的 entry config 重灌一次设置,并按新值重置转换内核。 */
|
|
107
|
+
reprocess(): void
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type Args = Record<string, any>
|
|
111
|
+
|
|
112
|
+
interface WebServerLike {
|
|
113
|
+
port?: number
|
|
114
|
+
host?: string
|
|
115
|
+
register(route: { kind: 'exact' | 'prefix'; path: string; handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> | void }): () => unknown
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function output() {
|
|
119
|
+
return { schema: { type: 'json' } as const, render: (_args: unknown, value: unknown) => [{ type: 'text' as const, text: JSON.stringify(value) }] }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function tool(toolName: string, description: string, parameters: Record<string, unknown>, execute: (args: Args, exec: ToolRunContext) => Promise<unknown>) {
|
|
123
|
+
return defineTool({ name: toolName, description, parameters: parameters as any, output: output(), execute: execute as any } as any)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function lineageOf(args: Args, exec?: ToolRunContext): Record<string, string> | undefined {
|
|
127
|
+
const lineage: Record<string, string> = {}
|
|
128
|
+
const sessionId = typeof args.session_key === 'string' && args.session_key.trim() ? args.session_key.trim() : exec?.agent?.session.header.id
|
|
129
|
+
if (sessionId) lineage.sessionId = sessionId
|
|
130
|
+
const fields: Array<[string, string]> = [
|
|
131
|
+
['turnId', 'turn_id'], ['intentId', 'intent_id'], ['taskId', 'task_id'],
|
|
132
|
+
['subtaskId', 'subtask_id'], ['parentSubtaskId', 'parent_subtask_id'], ['runId', 'run_id'], ['traceId', 'trace_id'], ['domain', 'domain'],
|
|
133
|
+
]
|
|
134
|
+
for (const [target, source] of fields) if (typeof args[source] === 'string' && args[source].trim()) lineage[target] = args[source].trim().slice(0, 160)
|
|
135
|
+
return Object.keys(lineage).length > 0 ? lineage : undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function readJsonBody(req: IncomingMessage, maxBytes = 2 * 1024 * 1024): Promise<any> {
|
|
139
|
+
const chunks: Buffer[] = []
|
|
140
|
+
let bytes = 0
|
|
141
|
+
for await (const chunk of req) {
|
|
142
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
|
|
143
|
+
bytes += buffer.length
|
|
144
|
+
if (bytes > maxBytes) throw new Error('REQUEST_TOO_LARGE')
|
|
145
|
+
chunks.push(buffer)
|
|
146
|
+
}
|
|
147
|
+
if (!chunks.length) return {}
|
|
148
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function send(res: ServerResponse, status: number, body: unknown): void {
|
|
152
|
+
res.statusCode = status
|
|
153
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
154
|
+
res.setHeader('cache-control', 'no-store')
|
|
155
|
+
res.end(JSON.stringify(body))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Compact task view for tools — the full ingest payload never enters context. */
|
|
159
|
+
function taskView(task: ConvertTask): Record<string, unknown> {
|
|
160
|
+
const manifest = task.manifest
|
|
161
|
+
return {
|
|
162
|
+
taskId: task.id,
|
|
163
|
+
state: task.state,
|
|
164
|
+
inputKind: task.inputKind,
|
|
165
|
+
title: manifest?.title ?? task.label,
|
|
166
|
+
sourceFormat: manifest?.sourceFormat ?? task.sourceFormat,
|
|
167
|
+
parser: manifest?.parser,
|
|
168
|
+
progress: task.progress,
|
|
169
|
+
stage: task.stage,
|
|
170
|
+
message: task.message,
|
|
171
|
+
error: task.error ?? null,
|
|
172
|
+
warnings: task.warnings.map(warning => warning.code),
|
|
173
|
+
attempts: task.attempts,
|
|
174
|
+
deliveryDir: task.deliveryDir,
|
|
175
|
+
assetCount: manifest?.assets.length ?? 0,
|
|
176
|
+
wordCount: manifest?.wordCount ?? 0,
|
|
177
|
+
contentHash: manifest?.contentHash,
|
|
178
|
+
externalId: manifest?.externalId,
|
|
179
|
+
sourceTypeCandidates: manifest?.ingest.sourceTypeCandidates ?? [],
|
|
180
|
+
sourceTypeRequired: manifest?.ingest.sourceTypeRequired ?? true,
|
|
181
|
+
preview: manifest?.ingest.content.slice(0, PREVIEW_CHARS) ?? null,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function waitForTask(service: ConvertService, taskId: string, timeoutMs: number): Promise<ConvertTask | undefined> {
|
|
186
|
+
const deadline = Date.now() + timeoutMs
|
|
187
|
+
while (Date.now() < deadline) {
|
|
188
|
+
const task = service.get(taskId)
|
|
189
|
+
if (!task) return undefined
|
|
190
|
+
if (task.state === 'succeeded' || task.state === 'failed' || task.state === 'cancelled') return task
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, 250))
|
|
192
|
+
}
|
|
193
|
+
return service.get(taskId)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
|
|
197
|
+
let config: ConvertPluginConfig = { ...DEFAULT_CONVERT_CONFIG }
|
|
198
|
+
let service: ConvertService | undefined
|
|
199
|
+
let starting: Promise<ConvertService | undefined> | undefined
|
|
200
|
+
let kernelHandler: ((req: IncomingMessage, res: ServerResponse) => Promise<void>) | undefined
|
|
201
|
+
/**
|
|
202
|
+
* Set when the optional kernel could not be loaded. Kept as the original error
|
|
203
|
+
* so the tool/route layer can surface its code and install suggestion verbatim.
|
|
204
|
+
*/
|
|
205
|
+
let dependencyError: unknown
|
|
206
|
+
|
|
207
|
+
async function ensureService(): Promise<ConvertService | undefined> {
|
|
208
|
+
if (service) return service
|
|
209
|
+
if (!starting) {
|
|
210
|
+
starting = loadConvertCore()
|
|
211
|
+
.then(async core => {
|
|
212
|
+
// 目录都从知识插件的根路径推出来:状态文件落在 `tmp/` 根(它是运行期
|
|
213
|
+
// 事实,不是交付物,所以不占日期桶),交付与工作区各自按日期分桶。
|
|
214
|
+
const root = deps.agentRoot()
|
|
215
|
+
const created = await core.createConvertService({
|
|
216
|
+
rootDir: tmpRoot(root),
|
|
217
|
+
outRoot: workspaceOutputRoot(deps.workspace?.() ?? process.cwd()),
|
|
218
|
+
workRoot: tmpRoot(root),
|
|
219
|
+
// 根路径契约里 output/ 与 tmp/ 的二级就是日期(`layout.ts`),所以插件
|
|
220
|
+
// 显式打开分桶;独立内核保持扁平布局不受影响。
|
|
221
|
+
bucketByDay: true,
|
|
222
|
+
maxConcurrent: config.maxConcurrent,
|
|
223
|
+
maxInputBytes: config.maxInputBytes,
|
|
224
|
+
retentionDays: config.retentionDays,
|
|
225
|
+
})
|
|
226
|
+
service = created
|
|
227
|
+
kernelHandler = core.createRequestHandler({ service: created, basePath: MOUNT_PATH })
|
|
228
|
+
dependencyError = undefined
|
|
229
|
+
return created
|
|
230
|
+
})
|
|
231
|
+
.catch(error => {
|
|
232
|
+
starting = undefined
|
|
233
|
+
dependencyError = error
|
|
234
|
+
return undefined
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
return await starting
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function resetService(): Promise<void> {
|
|
241
|
+
const current = service
|
|
242
|
+
service = undefined
|
|
243
|
+
starting = undefined
|
|
244
|
+
kernelHandler = undefined
|
|
245
|
+
if (current) await current.close().catch(() => undefined)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The kernel is optional; this is the single place its absence becomes an answer. */
|
|
249
|
+
function missingDependency<T>(data: T): Envelope<T> {
|
|
250
|
+
const error = dependencyError
|
|
251
|
+
const message = error instanceof Error ? error.message : '转换内核 dsh-convert-core 不可用。'
|
|
252
|
+
const suggestion = error instanceof ConvertCoreMissingError
|
|
253
|
+
? error.suggestion
|
|
254
|
+
: '安装可选依赖 dsh-convert-core 后重试。'
|
|
255
|
+
return failure(data, 'DEPENDENCY_MISSING', message, true, suggestion)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function knowledgeBaseUrl(): string {
|
|
259
|
+
const web = ctx.get('webServer') as WebServerLike | undefined
|
|
260
|
+
const host = web?.host === '0.0.0.0' ? '127.0.0.1' : web?.host ?? '127.0.0.1'
|
|
261
|
+
return `http://${host}:${web?.port ?? 0}`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Since 0018 the knowledge root is not resolved from another plugin's settings
|
|
266
|
+
* or an environment variable: the convert host runs inside the knowledge
|
|
267
|
+
* plugin and writes exactly where the live runtime reads.
|
|
268
|
+
*
|
|
269
|
+
* Since 0019 a delivery can also target a **specific** knowledge base: the
|
|
270
|
+
* panel imports a file into whichever base the user selected.
|
|
271
|
+
*/
|
|
272
|
+
function resolveKnowledgeRoot(): { root: string; source: string } {
|
|
273
|
+
return { root: deps.knowledgeRoot(), source: 'knowledge-runtime' }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Ingest target for one knowledge base, falling back to the default base. */
|
|
277
|
+
function targetFor(kbId?: unknown): { runtime: IngestTarget; knowledgeRoot: string; baseId?: string } {
|
|
278
|
+
const id = typeof kbId === 'string' && kbId.trim() ? kbId.trim() : undefined
|
|
279
|
+
const resolved = id ? deps.resolveTarget?.(id) : undefined
|
|
280
|
+
if (resolved) return { ...resolved, baseId: id }
|
|
281
|
+
return { runtime: deps.runtime(), knowledgeRoot: deps.knowledgeRoot(), ...(id ? { baseId: id } : {}) }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 目标知识库中「存在且启用」的来源分类 id(0013)。
|
|
286
|
+
*
|
|
287
|
+
* 用作转换内核的候选提示,**取代内核按载体格式猜出来的那张表**——那张表会给出
|
|
288
|
+
* video / image 这类载体名,和「source_type 描述业务场景」直接冲突。分类判定
|
|
289
|
+
* 仍然由 knowledge 侧负责,这里只是把真实候选告诉调用方。
|
|
290
|
+
*/
|
|
291
|
+
function sourceTypeCandidateIds(runtime: IngestTarget): string[] {
|
|
292
|
+
return (runtime.sourceTypes?.() ?? []).filter(def => def.enabled).map(def => def.id)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 设置来源。0.1.7 起设置随 `applyConvert(ctx, deps)` 的 `deps.config` 进来
|
|
297
|
+
* (插件 entry 的 `convert` 组),`settings.installSection` 那套命名空间注册
|
|
298
|
+
* 已经整个删除。组合层会合上 schema 默认值,这里再铺一层
|
|
299
|
+
* `DEFAULT_CONVERT_CONFIG` 兜底,好让直接调用 `applyConvert` 的装配方式也成立。
|
|
300
|
+
*
|
|
301
|
+
* 「改设置会让插件整个重载」这句话对 volatile 字段**不成立** —— loader 只就地改
|
|
302
|
+
* 引用再发一个事件(见 `settings-volatile.ts`)。所以重灌这件事由组合根在
|
|
303
|
+
* `loader/volatile-update` 时调 `reprocess()` 补上,这里只负责读值。
|
|
304
|
+
*/
|
|
305
|
+
const readConfig = (): Partial<ConvertPluginConfig> =>
|
|
306
|
+
(typeof deps.config === 'function' ? deps.config() : deps.config) ?? {}
|
|
307
|
+
config = { ...DEFAULT_CONVERT_CONFIG, ...readConfig() }
|
|
308
|
+
void resetService()
|
|
309
|
+
|
|
310
|
+
ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
|
|
311
|
+
if ((WRITE_TOOLS as readonly string[]).includes(exec.name)) {
|
|
312
|
+
return Promise.resolve({ kind: 'ask', reason: '该操作会转换本地内容,并写入 Tita Knowledge Store,需要本轮用户审批。' })
|
|
313
|
+
}
|
|
314
|
+
return next()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------- tools
|
|
318
|
+
|
|
319
|
+
ctx.tools.register(tool(
|
|
320
|
+
'convert_capabilities',
|
|
321
|
+
'Inspect the conversion kernel: available adapters, engine version, supported formats and where deliveries are written. Read-only; call this before promising a format will work.',
|
|
322
|
+
{ session_key: { type: 'string' } },
|
|
323
|
+
async () => {
|
|
324
|
+
const kernel = await ensureService()
|
|
325
|
+
if (!kernel) return missingDependency({ adapters: [], supportedExtensions: [] })
|
|
326
|
+
// Forced: this is an explicit question ("can you convert X?"), not the console's
|
|
327
|
+
// poll, and the service caches the report so that poll stays cheap. An agent that
|
|
328
|
+
// just installed a binary and then asked would otherwise get the stale answer.
|
|
329
|
+
const report = await kernel.capabilities({ force: true })
|
|
330
|
+
const knowledge = resolveKnowledgeRoot()
|
|
331
|
+
return success({
|
|
332
|
+
adapters: report.adapters,
|
|
333
|
+
runtime: report.runtime,
|
|
334
|
+
defaults: report.defaults,
|
|
335
|
+
supportedExtensions: report.extensions,
|
|
336
|
+
rootDir: kernel.rootDir,
|
|
337
|
+
outDir: kernel.outDir,
|
|
338
|
+
consolePath: `${MOUNT_PATH}/`,
|
|
339
|
+
knowledgeRoot: knowledge.root,
|
|
340
|
+
knowledgeRootSource: knowledge.source,
|
|
341
|
+
knowledgeBaseUrl: knowledgeBaseUrl(),
|
|
342
|
+
ingestEnabled: config.ingestEnabled,
|
|
343
|
+
})
|
|
344
|
+
},
|
|
345
|
+
))
|
|
346
|
+
|
|
347
|
+
ctx.tools.register(tool(
|
|
348
|
+
'convert_source',
|
|
349
|
+
'Convert a document (Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / text-layer PDF) or inline text into knowledge-ready Markdown + local images. Async kernel task; returns taskId, delivery dir and ingest hints (source_type candidates, external_id, content_hash). Write operation: DSH asks the user once before execution.',
|
|
350
|
+
{
|
|
351
|
+
kind: { type: 'string', enum: ['file', 'text'], required: true },
|
|
352
|
+
path: { type: 'string' },
|
|
353
|
+
text: { type: 'string' },
|
|
354
|
+
title: { type: 'string' },
|
|
355
|
+
source_type: { type: 'string' },
|
|
356
|
+
adapter: { type: 'string' },
|
|
357
|
+
wait: { type: 'boolean' },
|
|
358
|
+
session_key: { type: 'string' },
|
|
359
|
+
task_id: { type: 'string' },
|
|
360
|
+
subtask_id: { type: 'string' },
|
|
361
|
+
parent_subtask_id: { type: 'string' },
|
|
362
|
+
run_id: { type: 'string' },
|
|
363
|
+
turn_id: { type: 'string' },
|
|
364
|
+
trace_id: { type: 'string' },
|
|
365
|
+
domain: { type: 'string' },
|
|
366
|
+
},
|
|
367
|
+
async (args: Args, exec) => {
|
|
368
|
+
const kernel = await ensureService()
|
|
369
|
+
if (!kernel) return missingDependency({ taskId: null })
|
|
370
|
+
const kind = args.kind as ConvertRequest['kind']
|
|
371
|
+
const candidates = sourceTypeCandidateIds(targetFor(args.kb).runtime)
|
|
372
|
+
const request: ConvertRequest = {
|
|
373
|
+
kind,
|
|
374
|
+
...(candidates.length > 0 ? { sourceTypeCandidates: candidates } : {}),
|
|
375
|
+
...(typeof args.text === 'string' ? { text: args.text } : {}),
|
|
376
|
+
...(typeof args.title === 'string' && args.title.trim() ? { title: args.title.trim() } : {}),
|
|
377
|
+
...(typeof args.source_type === 'string' && args.source_type.trim() ? { sourceType: args.source_type.trim(), sourceTypeSource: 'agent' as const } : {}),
|
|
378
|
+
...(typeof args.adapter === 'string' && args.adapter.trim() ? { adapter: args.adapter.trim() } : {}),
|
|
379
|
+
...(lineageOf(args, exec) ? { lineage: lineageOf(args, exec)! } : {}),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Local files are handed to the kernel as a path: it owns staging, size
|
|
383
|
+
// limits and cleanup, so the host never buffers a 256 MB file.
|
|
384
|
+
if (kind === 'file') {
|
|
385
|
+
if (typeof args.path !== 'string' || !args.path.trim()) {
|
|
386
|
+
return failure({ taskId: null }, 'PATH_REQUIRED', 'kind=file 必须提供本地绝对路径 path。', false, '用户上传的文件请走知识面板的「转换」页;Agent 侧只能读取本机已有路径。')
|
|
387
|
+
}
|
|
388
|
+
const absolute = resolve(args.path.trim())
|
|
389
|
+
try {
|
|
390
|
+
const info = await stat(absolute)
|
|
391
|
+
if (!info.isFile()) return failure({ taskId: null }, 'PATH_NOT_A_FILE', `路径不是文件:${absolute}`)
|
|
392
|
+
if (info.size > config.maxInputBytes) {
|
|
393
|
+
return failure({ taskId: null }, 'INPUT_TOO_LARGE', `文件 ${info.size} 字节超过上限 ${config.maxInputBytes}。`, false, '调大转换设置 maxInputBytes,或先压缩文件。')
|
|
394
|
+
}
|
|
395
|
+
} catch (cause) {
|
|
396
|
+
return failure({ taskId: null }, 'PATH_UNREADABLE', `无法读取 ${absolute}:${cause instanceof Error ? cause.message : String(cause)}`)
|
|
397
|
+
}
|
|
398
|
+
request.localPath = absolute
|
|
399
|
+
if (!request.title) request.title = basename(absolute)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const submitted = await kernel.submit(request)
|
|
403
|
+
const shouldWait = args.wait !== false
|
|
404
|
+
const finished = shouldWait ? await waitForTask(kernel, submitted.id, config.toolWaitMs) : submitted
|
|
405
|
+
const view = taskView(finished ?? submitted)
|
|
406
|
+
return success({
|
|
407
|
+
...view,
|
|
408
|
+
waited: shouldWait,
|
|
409
|
+
ingestHint: '调用 convert_ingest 把该任务写入知识库;它会把 Markdown 与图片一起落地,并返回 cardHints。',
|
|
410
|
+
})
|
|
411
|
+
},
|
|
412
|
+
))
|
|
413
|
+
|
|
414
|
+
ctx.tools.register(tool(
|
|
415
|
+
'convert_task',
|
|
416
|
+
'Inspect the conversion queue: list, get, cancel, retry, or read the delivered Markdown/manifest of a task. Read-only.',
|
|
417
|
+
{
|
|
418
|
+
action: { type: 'string', enum: ['list', 'get', 'cancel', 'retry', 'markdown', 'manifest'], required: true },
|
|
419
|
+
task_id: { type: 'string' },
|
|
420
|
+
limit: { type: 'integer' },
|
|
421
|
+
},
|
|
422
|
+
async (args: Args) => {
|
|
423
|
+
const kernel = await ensureService()
|
|
424
|
+
if (!kernel) return missingDependency({ tasks: [] })
|
|
425
|
+
const action = String(args.action)
|
|
426
|
+
if (action === 'list') {
|
|
427
|
+
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(100, Number(args.limit))) : 20
|
|
428
|
+
return success({ tasks: kernel.list().slice(0, limit).map(taskView) })
|
|
429
|
+
}
|
|
430
|
+
const taskId = typeof args.task_id === 'string' ? args.task_id.trim() : ''
|
|
431
|
+
if (!taskId) return failure({ taskId: null }, 'TASK_ID_REQUIRED', `${action} 需要 task_id。`)
|
|
432
|
+
if (action === 'cancel') return success(taskView(kernel.cancel(taskId)))
|
|
433
|
+
if (action === 'retry') return success(taskView(await kernel.retry(taskId)))
|
|
434
|
+
if (action === 'markdown') {
|
|
435
|
+
const file = await kernel.readDeliveryFile(taskId, 'index.md')
|
|
436
|
+
if (!file) return failure({ taskId }, 'ARTIFACT_NOT_FOUND', '该任务没有 index.md(未成功或已被清理)。')
|
|
437
|
+
return success({ taskId, markdown: file.data.toString('utf8') })
|
|
438
|
+
}
|
|
439
|
+
if (action === 'manifest') {
|
|
440
|
+
const file = await kernel.readDeliveryFile(taskId, 'manifest.json')
|
|
441
|
+
if (!file) return failure({ taskId }, 'ARTIFACT_NOT_FOUND', '该任务没有 manifest.json。')
|
|
442
|
+
return success({ taskId, manifest: JSON.parse(file.data.toString('utf8')) })
|
|
443
|
+
}
|
|
444
|
+
const task = kernel.get(taskId)
|
|
445
|
+
if (!task) return failure({ taskId }, 'TASK_NOT_FOUND', `任务不存在:${taskId}`)
|
|
446
|
+
return success(taskView(task))
|
|
447
|
+
},
|
|
448
|
+
))
|
|
449
|
+
|
|
450
|
+
ctx.tools.register(tool(
|
|
451
|
+
'convert_ingest',
|
|
452
|
+
'Ingest a delivered conversion into the knowledge store: copies images into the knowledge root, rewrites local image references, writes the SourceRaw document in-process, and returns cardHints so a wiki card can carry the images into multimodal recall. Write operation: DSH asks the user once before execution. Set plan_only=true for a dry run.',
|
|
453
|
+
{
|
|
454
|
+
convert_task_id: { type: 'string', required: true },
|
|
455
|
+
kb: { type: 'string', description: '目标知识库 id;省略则写入默认知识库' },
|
|
456
|
+
source_type: { type: 'string' },
|
|
457
|
+
title: { type: 'string' },
|
|
458
|
+
plan_only: { type: 'boolean' },
|
|
459
|
+
session_key: { type: 'string' },
|
|
460
|
+
task_id: { type: 'string' },
|
|
461
|
+
subtask_id: { type: 'string' },
|
|
462
|
+
parent_subtask_id: { type: 'string' },
|
|
463
|
+
run_id: { type: 'string' },
|
|
464
|
+
turn_id: { type: 'string' },
|
|
465
|
+
trace_id: { type: 'string' },
|
|
466
|
+
domain: { type: 'string' },
|
|
467
|
+
},
|
|
468
|
+
async (args: Args, exec) => {
|
|
469
|
+
if (!config.ingestEnabled) {
|
|
470
|
+
return failure({ taskId: String(args.convert_task_id) }, 'INGEST_DISABLED', '当前设置禁止转换写入知识库(ingestEnabled=false)。', false, '在 Tita Data Agent → 设置 → 转换 中开启「允许一键入库」,或手动调用 add_source。')
|
|
471
|
+
}
|
|
472
|
+
const kernel = await ensureService()
|
|
473
|
+
if (!kernel) return missingDependency({ taskId: String(args.convert_task_id) })
|
|
474
|
+
const target = targetFor(args.kb)
|
|
475
|
+
const result = await ingestDelivery({
|
|
476
|
+
service: kernel,
|
|
477
|
+
taskId: String(args.convert_task_id),
|
|
478
|
+
runtime: target.runtime,
|
|
479
|
+
knowledgeRoot: target.knowledgeRoot,
|
|
480
|
+
...(typeof args.source_type === 'string' && args.source_type.trim() ? { sourceType: args.source_type.trim() } : {}),
|
|
481
|
+
...(typeof args.title === 'string' && args.title.trim() ? { title: args.title.trim() } : {}),
|
|
482
|
+
planOnly: args.plan_only === true,
|
|
483
|
+
...(lineageOf(args, exec) ? { lineage: lineageOf(args, exec)! } : {}),
|
|
484
|
+
})
|
|
485
|
+
return {
|
|
486
|
+
...result,
|
|
487
|
+
data: {
|
|
488
|
+
...(result.data as IngestOutcome),
|
|
489
|
+
knowledgeRootSource: 'knowledge-runtime',
|
|
490
|
+
...(target.baseId ? { knowledgeBaseId: target.baseId } : {}),
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
))
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------- routes
|
|
497
|
+
|
|
498
|
+
ctx.inject(['webServer'], webCtx => {
|
|
499
|
+
const web = webCtx as unknown as Context & { webServer: WebServerLike }
|
|
500
|
+
|
|
501
|
+
// Plugin-level facts the kernel cannot know about itself.
|
|
502
|
+
const pluginFacts = (): Record<string, unknown> => {
|
|
503
|
+
const knowledge = resolveKnowledgeRoot()
|
|
504
|
+
const root = deps.agentRoot()
|
|
505
|
+
return {
|
|
506
|
+
rootDir: service?.rootDir ?? tmpRoot(root),
|
|
507
|
+
outDir: service?.outRoot ?? workspaceOutputRoot(deps.workspace?.() ?? process.cwd()),
|
|
508
|
+
workDir: service?.workRoot ?? tmpRoot(root),
|
|
509
|
+
mountPath: MOUNT_PATH,
|
|
510
|
+
knowledgeRoot: knowledge.root,
|
|
511
|
+
knowledgeRootSource: knowledge.source,
|
|
512
|
+
knowledgeBaseUrl: knowledgeBaseUrl(),
|
|
513
|
+
ingestEnabled: config.ingestEnabled,
|
|
514
|
+
kernelLoaded: service !== undefined,
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const dispatch = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
519
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
520
|
+
|
|
521
|
+
if (url.pathname === `${MOUNT_PATH}/api/ingest`) {
|
|
522
|
+
if (req.method !== 'POST') return send(res, 405, failure({}, 'METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
|
|
523
|
+
try {
|
|
524
|
+
const body = await readJsonBody(req)
|
|
525
|
+
const kernel = await ensureService()
|
|
526
|
+
if (!kernel) return send(res, 503, missingDependency({ taskId: String(body.task_id ?? '') }))
|
|
527
|
+
const target = targetFor(body.kb)
|
|
528
|
+
const result = await ingestDelivery({
|
|
529
|
+
service: kernel,
|
|
530
|
+
taskId: String(body.task_id ?? ''),
|
|
531
|
+
runtime: target.runtime,
|
|
532
|
+
knowledgeRoot: target.knowledgeRoot,
|
|
533
|
+
...(typeof body.source_type === 'string' && body.source_type.trim() ? { sourceType: body.source_type.trim() } : {}),
|
|
534
|
+
...(typeof body.title === 'string' && body.title.trim() ? { title: body.title.trim() } : {}),
|
|
535
|
+
planOnly: body.plan_only === true,
|
|
536
|
+
...(body.lineage && typeof body.lineage === 'object' ? { lineage: body.lineage as Record<string, string> } : {}),
|
|
537
|
+
})
|
|
538
|
+
return send(res, 200, {
|
|
539
|
+
...result,
|
|
540
|
+
data: {
|
|
541
|
+
...(result.data as IngestOutcome),
|
|
542
|
+
knowledgeRootSource: 'knowledge-runtime',
|
|
543
|
+
...(target.baseId ? { knowledgeBaseId: target.baseId } : {}),
|
|
544
|
+
},
|
|
545
|
+
})
|
|
546
|
+
} catch (cause) {
|
|
547
|
+
return send(res, 400, failure({}, 'REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (url.pathname === `${MOUNT_PATH}/api/plugin`) {
|
|
552
|
+
if (req.method !== 'GET') return send(res, 405, failure({}, 'METHOD_NOT_ALLOWED', '该 endpoint 只接受 GET'))
|
|
553
|
+
return send(res, 200, success(pluginFacts()))
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Everything else belongs to the kernel console/API.
|
|
557
|
+
if (!kernelHandler) await ensureService()
|
|
558
|
+
if (!kernelHandler) return send(res, 503, missingDependency({ mountPath: MOUNT_PATH }))
|
|
559
|
+
return await kernelHandler(req, res)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const dispose = web.webServer.register({ kind: 'prefix', path: MOUNT_PATH, handler: dispatch })
|
|
563
|
+
return () => {
|
|
564
|
+
void dispose()
|
|
565
|
+
void resetService()
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
reprocess: () => {
|
|
571
|
+
const next = { ...DEFAULT_CONVERT_CONFIG, ...readConfig() }
|
|
572
|
+
const changed = (Object.keys(next) as Array<keyof ConvertPluginConfig>).some(key => next[key] !== config[key])
|
|
573
|
+
config = next
|
|
574
|
+
// 只有真变了才重置内核:`loader/volatile-update` 是**这个 entry**的事件,但知识那组
|
|
575
|
+
// 的改动也会走到这里,无谓地关掉内核会让下一次转换重新起一遍内核。
|
|
576
|
+
if (changed) void resetService()
|
|
577
|
+
},
|
|
578
|
+
}
|
|
579
|
+
}
|