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,462 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
+
import z from '@deepseek-ai/schemastery'
|
|
4
|
+
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
|
|
5
|
+
import type {} from '@deepseek-ai/dsh-settings'
|
|
6
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
7
|
+
import { envelopeSuccess } from 'dsh-data-contracts/envelope'
|
|
8
|
+
import type { LineageContext } from 'dsh-data-contracts/refs'
|
|
9
|
+
import { DEFAULT_ACTS, DEFAULT_CATEGORIES, DEFAULT_INTENT_SETTINGS, DEFAULT_PREDICATES, type IntentActDef, type IntentSettings } from './types.js'
|
|
10
|
+
import { IntentEngine } from './engine.js'
|
|
11
|
+
import { appendIntentRecord, INTENT_REGISTRY_FILE, readIntentRecords, readIntentRegistry, writeIntentRegistry, type IntentRegistryFile } from './store.js'
|
|
12
|
+
import type { IntentToolHost } from './tools.js'
|
|
13
|
+
import { lineageOf, sessionKeyOf } from './tools.js'
|
|
14
|
+
|
|
15
|
+
export * from './types.js'
|
|
16
|
+
export * from './engine.js'
|
|
17
|
+
export * from './tools.js'
|
|
18
|
+
|
|
19
|
+
export const SETTINGS_NAMESPACE = 'tita-data-agent-intent'
|
|
20
|
+
/** Read-only HTTP surface the 意图 panel reads: `/intent/overview`, `/intent/store`, `/intent/registry`. */
|
|
21
|
+
export const INTENT_MOUNT_PATH = '/intent'
|
|
22
|
+
/** Cordis service name consumers read to inherit the session's active task ids. */
|
|
23
|
+
export const INTENT_SERVICE = 'titaDataAgentIntent'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 意图引擎的对外操作面。
|
|
27
|
+
*
|
|
28
|
+
* 两类消费者,一个服务:
|
|
29
|
+
* - `activeLineage` —— 其它插件(`data-table`)读本轮活动血缘。
|
|
30
|
+
* - 其余成员 —— agent 平面的挂载行(`./preset-tools.ts`)按它注册 `intent_*` 工具。
|
|
31
|
+
*
|
|
32
|
+
* 0029 之前这三个工具由本模块直接全局注册;现在注册搬到了 agent 平面,这里只留
|
|
33
|
+
* 「引擎能做什么」。引擎只有一份,工具定义也只有一份(`./tools.ts`)。
|
|
34
|
+
*/
|
|
35
|
+
export interface IntentService extends IntentToolHost {
|
|
36
|
+
/**
|
|
37
|
+
* Ids a downstream tool call in `sessionKey` should carry. `committed` is
|
|
38
|
+
* false while the current turn has not recorded yet, so callers can tell a
|
|
39
|
+
* provisional subtask id from an authoritative record.
|
|
40
|
+
* @param sessionKey - session whose lineage is requested.
|
|
41
|
+
* @returns lineage plus whether a record already committed it, or undefined when nothing is known.
|
|
42
|
+
*/
|
|
43
|
+
activeLineage(sessionKey: string): { lineage: Partial<LineageContext>; committed: boolean } | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @deprecated 0029:改用 `IntentService`。保留别名只为不动既有 import。 */
|
|
47
|
+
export type IntentLineageService = IntentService
|
|
48
|
+
|
|
49
|
+
// 三个 schema 搬到了 `./settings-schema.js`(纯数据、可在 node --test 里独立解析)。
|
|
50
|
+
// `CATEGORY_SCHEMA` / `ACT_SCHEMA` 在下面的注册表写入校验里还要用,所以先 import
|
|
51
|
+
// 再 re-export —— 单独一条 `export … from` 不会把它们带进本文件的作用域。
|
|
52
|
+
import { ACT_SCHEMA, CATEGORY_SCHEMA } from './settings-schema.js'
|
|
53
|
+
export { ACT_SCHEMA, CATEGORY_SCHEMA, INTENT_SETTINGS_SCHEMA } from './settings-schema.js'
|
|
54
|
+
|
|
55
|
+
/** Concatenate the text of one user message, whichever content form it carries. */
|
|
56
|
+
function userText(message: unknown): string {
|
|
57
|
+
const content = (message as { content?: unknown } | undefined)?.content
|
|
58
|
+
if (typeof content === 'string') return content
|
|
59
|
+
if (!Array.isArray(content)) return ''
|
|
60
|
+
return content
|
|
61
|
+
.map(block => typeof block === 'string' ? block : (block as { text?: unknown } | undefined)?.text)
|
|
62
|
+
.filter((text): text is string => typeof text === 'string')
|
|
63
|
+
.join('\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 意图记录模块(原 `@tita-data-agent/data-domain-intent`,0020 并入本插件)。
|
|
68
|
+
*
|
|
69
|
+
* 它是 `KnowledgePanel` 五个视图之一「意图」的宿主实现。0029 起它**只负责引擎**:
|
|
70
|
+
* 设置、轮次记账、`/intent/**` 只读面,以及通过 `INTENT_SERVICE` 暴露操作面。
|
|
71
|
+
*
|
|
72
|
+
* 三个 `intent_*` 工具**不再在这里注册** —— 它们搬到了 agent 平面的
|
|
73
|
+
* `./preset-tools.ts`,只被自定义 preset 的 `agent.cordis.yml` 挂载,于是只有跑
|
|
74
|
+
* TitaAgent 那套组合的会话才看得到它们。理由见那个文件的模块注释。
|
|
75
|
+
*
|
|
76
|
+
* 本模块由 `src/index.ts` 组合根调用,不自己导出 `name` / `inject`。
|
|
77
|
+
* @module dsh-data-domain-knowledge/intent
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* 组合根交给意图模块的东西。
|
|
81
|
+
*
|
|
82
|
+
* `intentRoot` 是**函数**而不是字符串:根路径由设置决定,可以在运行时改,而
|
|
83
|
+
* 模块加载时还不知道。每次读写都重新问一次,于是「改了根路径」立刻生效,
|
|
84
|
+
* 不存在「配置说 A、实际写 B」。
|
|
85
|
+
*/
|
|
86
|
+
export interface IntentHostDeps {
|
|
87
|
+
/** `<agentRoot>/intent`。省略时退回内存态(只测引擎的单元测试用这个形态)。 */
|
|
88
|
+
intentRoot?: () => string
|
|
89
|
+
/**
|
|
90
|
+
* 已解析的 `intent` 设置组(来自插件 entry 的 config)。
|
|
91
|
+
*
|
|
92
|
+
* 0.1.7 起设置不再是命名空间 —— 它随 `apply(ctx, config)` 进来。省略时用
|
|
93
|
+
* `DEFAULT_INTENT_SETTINGS`,这样只测引擎的装配方式仍然能跑。
|
|
94
|
+
*
|
|
95
|
+
* 允许给**函数**:设置字段全是 volatile,schema 解析出来是引用,而「解包成普通值」
|
|
96
|
+
* 是一次快照 —— 快照会漏掉「用户在设置页改了值」(loader 只就地改引用、不重载插件)。
|
|
97
|
+
* 给函数则每次读都重新解包,`reprocess()` 才真的拿得到新值。
|
|
98
|
+
*/
|
|
99
|
+
config?: Partial<IntentSettings> | (() => Partial<IntentSettings>)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `applyIntent` 交给组合根的那一个把手。
|
|
104
|
+
*
|
|
105
|
+
* volatile 改动不会重载插件(`Entry._commitVolatile()` 只就地改引用再发一个
|
|
106
|
+
* `loader/volatile-update`),所以「设置存下了但没生效」只能由插件自己兜住 ——
|
|
107
|
+
* 见 `settings-volatile.ts` 末尾那段。重灌设置之后还要 `notify()`:agent 平面的行
|
|
108
|
+
* 挂在那个订阅上,而 `intent_record` 的**工具描述就是路由提示词**,词表一变描述就
|
|
109
|
+
* 必须变。
|
|
110
|
+
*/
|
|
111
|
+
export interface IntentHost {
|
|
112
|
+
/** 按**当前**的 entry config 重灌一次引擎设置,并通知订阅者。 */
|
|
113
|
+
reprocess(): void
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function applyIntent(ctx: Context, deps: IntentHostDeps = {}): IntentHost {
|
|
117
|
+
const engine = new IntentEngine(DEFAULT_INTENT_SETTINGS)
|
|
118
|
+
const root = (): string | undefined => deps.intentRoot?.()
|
|
119
|
+
/**
|
|
120
|
+
* 注册表的**真源**(0033)。
|
|
121
|
+
*
|
|
122
|
+
* `undefined` = 文件还不存在(首启)。这时按 settings 里解析出来的值启动,
|
|
123
|
+
* 并在第一次读到时把它**播种**成文件 —— 这样老用户已经改过的分类不会因为
|
|
124
|
+
* 「真源搬家」而悄悄回到出厂预置。
|
|
125
|
+
*/
|
|
126
|
+
let fileRegistry: IntentRegistryFile | undefined
|
|
127
|
+
/**
|
|
128
|
+
* 每个知识库…不,意图不分库(见 `layout.ts` 原理 6),所以这里只有一个根。
|
|
129
|
+
* 但**记录是会话级的**,读的时候要按 sessionKey 过滤。
|
|
130
|
+
*/
|
|
131
|
+
/**
|
|
132
|
+
* 读注册表;文件不存在时**按设置里解析出来的值播种**。
|
|
133
|
+
*
|
|
134
|
+
* 播种的输入必须是 `seedFrom`(刚解析出来的设置),**不能**读 `engine.registry()`
|
|
135
|
+
* —— 那一刻引擎里还是出厂预置(`apply()` 还没跑),于是「老用户在 settings 里改过
|
|
136
|
+
* 的分类」会在真源搬家的瞬间被悄悄换回出厂值。这条路径只在文件不存在时走一次,
|
|
137
|
+
* 而它承载的正是「搬家不丢用户的编辑」。
|
|
138
|
+
*/
|
|
139
|
+
const loadRegistry = (seedFrom: IntentSettings): void => {
|
|
140
|
+
const base = root()
|
|
141
|
+
if (!base) return
|
|
142
|
+
const fromFile = readIntentRegistry(base)
|
|
143
|
+
if (fromFile) { fileRegistry = fromFile; return }
|
|
144
|
+
// **同步**,因为它在设置解析的回调里 —— 见 store.ts 的 writeAtomicSync 注释。
|
|
145
|
+
try {
|
|
146
|
+
fileRegistry = writeIntentRegistry(base, {
|
|
147
|
+
acts: seedFrom.acts,
|
|
148
|
+
// `examples` 在 settings 那侧是可选的(0012 时代的配置没有这个字段),
|
|
149
|
+
// 落成文件时补成空数组 —— 文件是要被人读的,缺字段比空数组更难懂。
|
|
150
|
+
categories: seedFrom.categories.map(category => ({
|
|
151
|
+
id: category.id,
|
|
152
|
+
label: category.label,
|
|
153
|
+
route: category.route,
|
|
154
|
+
description: category.description ?? '',
|
|
155
|
+
examples: [...(category.examples ?? [])],
|
|
156
|
+
enabled: category.enabled,
|
|
157
|
+
})),
|
|
158
|
+
})
|
|
159
|
+
} catch { /* 目录不可写:按出厂预置跑,不留痕也不该让插件起不来 */ }
|
|
160
|
+
}
|
|
161
|
+
/** 把「文件(若有)+ settings 里的标量」合成一份引擎设置。 */
|
|
162
|
+
const merged = (settings: IntentSettings): IntentSettings => fileRegistry
|
|
163
|
+
? { ...settings, acts: fileRegistry.acts, categories: fileRegistry.categories }
|
|
164
|
+
: settings
|
|
165
|
+
/**
|
|
166
|
+
* 引擎变化(设置改了、词表改了)的订阅者。
|
|
167
|
+
*
|
|
168
|
+
* 唯一消费者是 agent 平面的注册行:`intent_record` 的**工具描述就是路由提示词**
|
|
169
|
+
* (0023),而工具描述是静态字符串 —— 词表一变必须重新注册那个工具,下一轮模型
|
|
170
|
+
* 看到的规则才是新的。宿主平面只负责**通知**,重挂由知道工具在哪的那个平面做。
|
|
171
|
+
*/
|
|
172
|
+
const listeners = new Set<() => void>()
|
|
173
|
+
const notify = (): void => { for (const listener of [...listeners]) { try { listener() } catch { /* 一个坏订阅者不该打断设置生效 */ } } }
|
|
174
|
+
/**
|
|
175
|
+
* 当前生效的设置来源。
|
|
176
|
+
*
|
|
177
|
+
* 放在 `applyIntent` 作用域而不是 settings 回调里:注册表的写路径(`/intent/registry`
|
|
178
|
+
* 的 POST)也要用它把新词表灌回引擎,而那条路径在 webServer 回调里。
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* 设置来源。0.1.7 起**设置就是插件自己 entry 的 config**(`apply(ctx, config)`
|
|
182
|
+
* 的第二个参数),`settings.installSection` 那套命名空间注册已经整个删除了。
|
|
183
|
+
*
|
|
184
|
+
* 组合层会把 schema 默认值合进来;这里再 `merged()` 一次是为了兼容直接调用
|
|
185
|
+
* `applyIntent` 的装配方式(那时可能只给一个片段,甚至什么都不给)。
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* 读取当前设置组。
|
|
189
|
+
*
|
|
190
|
+
* 组合根给的是**函数**时(真实装配方式,见 `IntentHostDeps.config`)每次都重新解包,
|
|
191
|
+
* 于是「改设置 → `reprocess()`」这条路拿到的是新值;给普通对象时(单元测试、直接
|
|
192
|
+
* 调用)原样用。
|
|
193
|
+
*/
|
|
194
|
+
const readConfig = (): Partial<IntentSettings> =>
|
|
195
|
+
(typeof deps.config === 'function' ? deps.config() : deps.config) ?? {}
|
|
196
|
+
/**
|
|
197
|
+
* 生效的设置 = 出厂默认 ← entry config ← 文件真源(`merged`)。
|
|
198
|
+
*
|
|
199
|
+
* 铺一层 `DEFAULT_INTENT_SETTINGS` 是为了让「直接调用 `applyIntent` 且只给一个
|
|
200
|
+
* 片段」也成立:缺的字段落下就是 `undefined`,而 `{...settings, ...next}` 的合并
|
|
201
|
+
* 会让 `undefined` **覆盖**掉引擎里已有的默认值。
|
|
202
|
+
*/
|
|
203
|
+
const source = (): IntentSettings => merged({ ...DEFAULT_INTENT_SETTINGS, ...readConfig() })
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 初始灌入:先按**文件真源**读一次注册表,再把结果灌进引擎,最后通知订阅者。
|
|
207
|
+
*
|
|
208
|
+
* 旧实现把这条路径拆成两个回调(`setSource` 在 install 时 `loadRegistry` + `apply`,
|
|
209
|
+
* `onChange` 只 `apply`)。新模型下改设置会让插件**整个重载**,所以只需在 apply
|
|
210
|
+
* 时跑一次 install 那条路径 —— 顺序也保持不变,否则「先 apply 后 loadRegistry」
|
|
211
|
+
* 会让第一轮工具描述用出厂词表。
|
|
212
|
+
*/
|
|
213
|
+
loadRegistry(source())
|
|
214
|
+
engine.setSettings(merged(source() as IntentSettings))
|
|
215
|
+
notify()
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 留痕出口(0033):每条记录追加进 `intent/records.ndjson`。
|
|
219
|
+
*
|
|
220
|
+
* 装的是**同步**闭包,内部 fire-and-forget 一个 Promise:`save()` 在记录路径上,
|
|
221
|
+
* 让它 await 磁盘等于把「记录不拦截请求」这条契约交给磁盘可用性。写失败由
|
|
222
|
+
* `appendIntentRecord` 自己吞掉 —— 丢一条留痕比让这一轮回答失败轻得多。
|
|
223
|
+
*/
|
|
224
|
+
if (deps.intentRoot) {
|
|
225
|
+
engine.store.setSink(record => { void appendIntentRecord(root() as string, record) })
|
|
226
|
+
/**
|
|
227
|
+
* 把留痕读回工作集。放在这里而不是 `apply()` 里:`apply` 每次改设置都会跑,
|
|
228
|
+
* 而 hydrate 只该在启动时发生一次(重复 hydrate 只是重复覆盖同样的记录,
|
|
229
|
+
* 但没必要每次都读一遍磁盘)。
|
|
230
|
+
*/
|
|
231
|
+
void readIntentRecords(root() as string).then(records => {
|
|
232
|
+
const loaded = engine.store.hydrate(records)
|
|
233
|
+
if (loaded > 0) notify()
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ctx.provide(INTENT_SERVICE, {
|
|
238
|
+
activeLineage: (sessionKey: string) => engine.activeLineage(sessionKey),
|
|
239
|
+
routingPrompt: () => engine.routingPrompt(),
|
|
240
|
+
/**
|
|
241
|
+
* `intent_record` 的参数落进引擎。放在这里而不是工具定义里:工具定义要与平面
|
|
242
|
+
* 无关(`./tools.ts`),而「参数怎么变成一条记录」是引擎的事。
|
|
243
|
+
*/
|
|
244
|
+
record: (args, exec) => {
|
|
245
|
+
const lineage = lineageOf(args, exec)
|
|
246
|
+
return engine.record({
|
|
247
|
+
userInput: typeof args.user_input === 'string' ? args.user_input : '',
|
|
248
|
+
...(typeof args.act === 'string' ? { act: args.act } : {}),
|
|
249
|
+
...(typeof args.category === 'string' ? { category: args.category } : {}),
|
|
250
|
+
...(typeof args.predicate === 'string' ? { predicate: args.predicate } : {}),
|
|
251
|
+
...(args.slots !== undefined ? { slots: args.slots as Record<string, unknown> } : {}),
|
|
252
|
+
...(typeof args.confidence === 'number' ? { confidence: args.confidence } : {}),
|
|
253
|
+
...(Array.isArray(args.refs) ? { refs: args.refs as Array<{ id: string }> } : {}),
|
|
254
|
+
...(args.relation === 'new_task' || args.relation === 'subtask_of' ? { relation: args.relation } : {}),
|
|
255
|
+
sessionKey: sessionKeyOf(args, exec),
|
|
256
|
+
...(lineage ? { lineage } : {}),
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
view: (sessionKey, includePrompt) => engine.view(sessionKey, includePrompt),
|
|
260
|
+
registry: () => envelopeSuccess({ registry: engine.registry(), prompt: engine.routingPrompt() }),
|
|
261
|
+
subscribe: (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener) } },
|
|
262
|
+
} satisfies IntentService)
|
|
263
|
+
|
|
264
|
+
// 0023:路由提示词不再单独注入 system prompt 段,它现在是 `intent_record` 的工具
|
|
265
|
+
// 描述(见 `./tools.ts` 的 `registerIntentTools`)。两处都放会重复占用上下文,
|
|
266
|
+
// 而且会漂移。
|
|
267
|
+
|
|
268
|
+
// Turn bookkeeping: allocate provisional ids per turn and backfill one record
|
|
269
|
+
// if the turn ends without one (issue 0012 decision B).
|
|
270
|
+
const pendingInput = new Map<string, string>()
|
|
271
|
+
ctx.on('session/event', (session, event) => {
|
|
272
|
+
const sessionKey = session.header.id as unknown as string
|
|
273
|
+
if (event.type === 'user/message') {
|
|
274
|
+
pendingInput.set(sessionKey, userText(event.data))
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
if (event.type === 'turn/start') {
|
|
278
|
+
engine.beginTurn(sessionKey, event.data.turn, pendingInput.get(sessionKey) ?? '')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (event.type === 'turn/end') {
|
|
282
|
+
engine.fallbackWhenMissing(sessionKey, event.data.turn)
|
|
283
|
+
pendingInput.delete(sessionKey)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Read-only HTTP surface for the 意图 panel. The intent store is in-memory and
|
|
288
|
+
// session-scoped, so the panel reads it over HTTP instead of duplicating the
|
|
289
|
+
// engine in the browser.
|
|
290
|
+
ctx.inject(['webServer'], webCtx => {
|
|
291
|
+
const web = webCtx as unknown as {
|
|
292
|
+
webServer: {
|
|
293
|
+
register(route: { kind: 'exact' | 'prefix'; path: string; handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> | void }): () => unknown
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const send = (res: ServerResponse, status: number, body: unknown): void => {
|
|
297
|
+
res.statusCode = status
|
|
298
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
299
|
+
res.setHeader('cache-control', 'no-store')
|
|
300
|
+
res.end(JSON.stringify(body))
|
|
301
|
+
}
|
|
302
|
+
const readJsonBody = async (req: IncomingMessage): Promise<Record<string, unknown>> => {
|
|
303
|
+
const chunks: Buffer[] = []
|
|
304
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)))
|
|
305
|
+
if (!chunks.length) return {}
|
|
306
|
+
const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
307
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) throw new Error('请求体必须是一个 JSON 对象')
|
|
308
|
+
return parsed as Record<string, unknown>
|
|
309
|
+
}
|
|
310
|
+
/** 文件里的词表与出厂预置是否不同 —— 面板据此显示「已自定义」并放开「恢复出厂」。 */
|
|
311
|
+
const customized = (): { categories: boolean; acts: boolean } => {
|
|
312
|
+
const same = (left: unknown, right: unknown): boolean => JSON.stringify(left) === JSON.stringify(right)
|
|
313
|
+
return {
|
|
314
|
+
categories: !same(fileRegistry?.categories ?? DEFAULT_CATEGORIES, DEFAULT_CATEGORIES),
|
|
315
|
+
acts: !same(fileRegistry?.acts ?? DEFAULT_ACTS, DEFAULT_ACTS),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** 写一份新词表:落盘 → 灌回引擎 → 通知重挂工具描述。 */
|
|
319
|
+
const saveRegistry = (next: { acts?: IntentRegistryFile['acts']; categories?: IntentRegistryFile['categories'] }): void => {
|
|
320
|
+
const base = root()
|
|
321
|
+
if (!base) throw new Error('意图目录尚未就绪(agentRoot 还没解析出来)')
|
|
322
|
+
const current = engine.registry()
|
|
323
|
+
fileRegistry = writeIntentRegistry(base, {
|
|
324
|
+
acts: next.acts ?? fileRegistry?.acts ?? current.acts,
|
|
325
|
+
categories: next.categories ?? fileRegistry?.categories ?? current.categories,
|
|
326
|
+
})
|
|
327
|
+
// 词表变了 = 工具描述变了 = 下一轮模型看到的规则要变。
|
|
328
|
+
engine.setSettings(merged(source()) as Partial<IntentSettings>)
|
|
329
|
+
notify()
|
|
330
|
+
}
|
|
331
|
+
const routes = [
|
|
332
|
+
{
|
|
333
|
+
kind: 'exact' as const,
|
|
334
|
+
path: `${INTENT_MOUNT_PATH}/overview`,
|
|
335
|
+
handler: async (_req: IncomingMessage, res: ServerResponse) => {
|
|
336
|
+
const overview = engine.overview()
|
|
337
|
+
const base = root()
|
|
338
|
+
if (base) {
|
|
339
|
+
/**
|
|
340
|
+
* 用**留痕**补齐工作集里已经没有的会话(0033)。
|
|
341
|
+
*
|
|
342
|
+
* 工作集有 600 秒 TTL,所以重启或隔一段时间回来,面板上的会话列表会
|
|
343
|
+
* 空掉 —— 而磁盘上明明留着。这里把文件里出现过、工作集里没有的会话补上,
|
|
344
|
+
* 数字与最后一条分类都从留痕读。工作集里已有的不覆盖:那一条带着活的
|
|
345
|
+
* `tasks`(血缘),比从文件重算的更准。
|
|
346
|
+
*/
|
|
347
|
+
const all = await readIntentRecords(base, undefined, 500)
|
|
348
|
+
const seen = new Set(overview.sessions.map(session => String(session.sessionKey)))
|
|
349
|
+
const bySession = new Map<string, typeof all>()
|
|
350
|
+
for (const record of all) {
|
|
351
|
+
const list = bySession.get(record.sessionKey) ?? []
|
|
352
|
+
list.push(record)
|
|
353
|
+
bySession.set(record.sessionKey, list)
|
|
354
|
+
}
|
|
355
|
+
for (const [sessionKey, records] of bySession) {
|
|
356
|
+
if (seen.has(sessionKey)) continue
|
|
357
|
+
const last = records[records.length - 1]
|
|
358
|
+
if (!last) continue
|
|
359
|
+
overview.sessions.push({
|
|
360
|
+
sessionKey,
|
|
361
|
+
recordCount: records.length,
|
|
362
|
+
lastAt: last.updatedAt ?? last.createdAt,
|
|
363
|
+
activeIntentId: last.id,
|
|
364
|
+
lastAct: last.act,
|
|
365
|
+
...(last.category ? { lastCategory: last.category } : {}),
|
|
366
|
+
lastRelation: last.relation,
|
|
367
|
+
...(last.lineage.taskId ? { activeTaskId: last.lineage.taskId } : {}),
|
|
368
|
+
...(last.confidence !== undefined ? { lastConfidence: last.confidence } : {}),
|
|
369
|
+
durable: true,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
overview.sessions.sort((left, right) => String(right.lastAt ?? '').localeCompare(String(left.lastAt ?? '')))
|
|
373
|
+
}
|
|
374
|
+
send(res, 200, envelopeSuccess(overview))
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
kind: 'exact' as const,
|
|
379
|
+
path: `${INTENT_MOUNT_PATH}/store`,
|
|
380
|
+
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
381
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
382
|
+
const sessionKey = url.searchParams.get('session_key')?.trim() || 'default'
|
|
383
|
+
const includePrompt = url.searchParams.get('include_prompt') === 'true'
|
|
384
|
+
const view = engine.view(sessionKey, includePrompt)
|
|
385
|
+
const base = root()
|
|
386
|
+
if (base) {
|
|
387
|
+
/**
|
|
388
|
+
* `recent` 读**留痕**而不是工作集(0033)。
|
|
389
|
+
*
|
|
390
|
+
* 工作集有 TTL:600 秒之后同一个会话的 `records()` 就是空数组,面板上
|
|
391
|
+
* 「意图记录」会莫名其妙变空,而文件里明明一条不少。`tasks` /
|
|
392
|
+
* `activeTaskId` 仍来自工作集 —— 那是**活的血缘**,不是历史。
|
|
393
|
+
*/
|
|
394
|
+
const durable = await readIntentRecords(base, sessionKey, 50)
|
|
395
|
+
const store = (view.data as { store?: { recent?: unknown[] } }).store
|
|
396
|
+
if (store) store.recent = durable.slice(-10).reverse()
|
|
397
|
+
}
|
|
398
|
+
send(res, 200, view)
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
kind: 'exact' as const,
|
|
403
|
+
path: `${INTENT_MOUNT_PATH}/registry`,
|
|
404
|
+
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
405
|
+
/**
|
|
406
|
+
* 注册表**可写**(0033):真源是 `intent/registry.json`,不再是宿主的
|
|
407
|
+
* settings 文档。写在这里而不是走设置作用域,是因为设置作用域改的是
|
|
408
|
+
* 「宿主的配置」,而分类词表是这个 Agent 自己的文件 —— 它要能跟着
|
|
409
|
+
* `agentRoot` 一起被备份、被 diff。
|
|
410
|
+
*/
|
|
411
|
+
if (req.method === 'POST') {
|
|
412
|
+
try {
|
|
413
|
+
const body = await readJsonBody(req)
|
|
414
|
+
const reset = typeof body.reset === 'string' ? body.reset : undefined
|
|
415
|
+
if (reset) {
|
|
416
|
+
saveRegistry({
|
|
417
|
+
...(reset === 'acts' || reset === 'all' ? { acts: DEFAULT_ACTS } : {}),
|
|
418
|
+
...(reset === 'categories' || reset === 'all' ? { categories: DEFAULT_CATEGORIES } : {}),
|
|
419
|
+
})
|
|
420
|
+
} else {
|
|
421
|
+
// 请求体是 `unknown`,schema 的入参类型是「已解析过的形状」;这里的窄化
|
|
422
|
+
// 正是校验本身要做的事 —— 校验不过会抛,抛了就走下面的 400。
|
|
423
|
+
const acts = body.acts === undefined ? undefined : z.array(ACT_SCHEMA)(body.acts as never) as IntentActDef[]
|
|
424
|
+
const categories = body.categories === undefined ? undefined : z.array(CATEGORY_SCHEMA)(body.categories as never) as IntentRegistryFile['categories']
|
|
425
|
+
if (!acts && !categories) throw new Error('请求体里既没有 categories 也没有 acts')
|
|
426
|
+
saveRegistry({
|
|
427
|
+
...(acts ? { acts } : {}),
|
|
428
|
+
...(categories ? { categories } : {}),
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
} catch (cause) {
|
|
432
|
+
const message = cause instanceof Error ? cause.message : String(cause)
|
|
433
|
+
return send(res, 400, envelopeSuccess({
|
|
434
|
+
registry: engine.registry(),
|
|
435
|
+
prompt: engine.routingPrompt(),
|
|
436
|
+
customized: customized(),
|
|
437
|
+
source: fileRegistry ? 'file' : 'factory',
|
|
438
|
+
error: message,
|
|
439
|
+
}))
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
send(res, 200, envelopeSuccess({
|
|
443
|
+
registry: engine.registry(),
|
|
444
|
+
prompt: engine.routingPrompt(),
|
|
445
|
+
customized: customized(),
|
|
446
|
+
source: fileRegistry ? 'file' : 'factory',
|
|
447
|
+
...(root() ? { path: `${root() as string}/${INTENT_REGISTRY_FILE}` } : {}),
|
|
448
|
+
}))
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
]
|
|
452
|
+
const disposers = routes.map(route => web.webServer.register(route))
|
|
453
|
+
return () => { for (const dispose of disposers) void dispose() }
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
reprocess: () => {
|
|
458
|
+
engine.setSettings(merged(source()))
|
|
459
|
+
notify()
|
|
460
|
+
},
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `intent_*` 工具的 **agent 平面**挂载行(0029)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么这三个工具要单独一个挂载行
|
|
5
|
+
*
|
|
6
|
+
* 意图记录是这个方案自己的东西:只有跑 TitaAgent 那套组合的会话才该在提示词里看到
|
|
7
|
+
* `intent_record`。而 DSH 里「只有某个 preset 能用」唯一支持的形态就是**注册作用域** ——
|
|
8
|
+
* 注册在哪个作用域,就只在哪个作用域可见。宿主平面注册 = 全局工具,每个 agent 都会
|
|
9
|
+
* 看到它;`ctx.tools.restrict()` 只能做减法、且宿主平面调它会直接抛,没有「只给某一个
|
|
10
|
+
* 作用域」的正向开关。
|
|
11
|
+
*
|
|
12
|
+
* 所以这三个工具的注册搬到这里:这个模块**只被自定义 preset 的 `agent.cordis.yml`
|
|
13
|
+
* 挂载**,它 `apply` 时拿到的 ctx 是那个 preset 的 standing mount 作用域,于是注册出来
|
|
14
|
+
* 的工具只覆盖加入该 preset 的会话。别的 preset、临时会话都看不到它们 —— 而「看不到」
|
|
15
|
+
* 与「调不了」在 `dsh-tools` 里是同一件事(不可见的工具会以 `UNKNOWN_TOOL` 拒掉)。
|
|
16
|
+
*
|
|
17
|
+
* ## 两条约束
|
|
18
|
+
*
|
|
19
|
+
* 1. **它不 provide 任何服务。** preset 的挂载审计拒绝在根 realm 发布服务;这个行只往
|
|
20
|
+
* `tools` 注册表里塞定义,和随包的 `tool-fs` / `tool-bash` 是同一类行。
|
|
21
|
+
* 2. **它不自己造引擎。** 引擎(设置、轮次记账、HTTP 只读面)住在宿主平面,通过
|
|
22
|
+
* `INTENT_SERVICE` 暴露操作面;这一行只按那个面注册工具。引擎因此只有一份。
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
28
|
+
import { INTENT_SERVICE, type IntentService } from './host.js'
|
|
29
|
+
import { registerIntentTools } from './tools.js'
|
|
30
|
+
|
|
31
|
+
/** 与 `package.json` 的 `exports["./intent-tools"]` 对应。 */
|
|
32
|
+
export const name = 'dsh-data-domain-knowledge/intent-tools'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `tools` 是注册目标;`INTENT_SERVICE` 是引擎操作面。
|
|
36
|
+
*
|
|
37
|
+
* 把服务写进 `inject` 而不是在 `apply` 里判空:宿主平面还没就绪时这一行应该**等**,
|
|
38
|
+
* 而不是挂上一个没有引擎可用的空壳 —— 后者会让模型看到一个必然报错的工具。
|
|
39
|
+
*/
|
|
40
|
+
export const inject = ['tools', INTENT_SERVICE] as const
|
|
41
|
+
|
|
42
|
+
export function apply(ctx: Context): void {
|
|
43
|
+
const service = ctx.get(INTENT_SERVICE) as IntentService | undefined
|
|
44
|
+
if (!service) return
|
|
45
|
+
// `effect` 把 disposer 绑到这个 fiber 上:卸载时三个工具一起撤掉,不留下悬空的
|
|
46
|
+
// 注册。preset 换掉或进程退出都走这条路径。
|
|
47
|
+
ctx.effect(() => registerIntentTools(ctx as unknown as { tools: { register(definition: unknown): unknown } }, service), 'tita intent tools')
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 意图设置的 schema(纯数据,单独一个模块)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么从 `host.ts` 搬出来
|
|
5
|
+
*
|
|
6
|
+
* 0.1.7 之后设置是一个 entry 的 **config schema**,而 `Config` 要在
|
|
7
|
+
* `tests/settings-schema.test.mjs` 里能被**独立解析一遍** —— 那正是一个「写错一个
|
|
8
|
+
* 默认值、整个 entry 装载失败」的东西该有的守卫。但 `host.ts` 会 `import` 工具层
|
|
9
|
+
* (`@deepseek-ai/dsh-tools`),它在 `node --test` 里拉不起来:`dsh-tools` 的
|
|
10
|
+
* `@deepseek-ai/dsh-sandbox` 是 peer,而本仓库 `autoInstallPeers: false`。
|
|
11
|
+
*
|
|
12
|
+
* 于是「schema 住在哪个文件」不是风格问题,而是**这个测试能不能跑**的问题。
|
|
13
|
+
* 与 `src/settings-schema.ts` 同一个理由(那边的文件头写着同一件事)。
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import z from '@deepseek-ai/schemastery'
|
|
19
|
+
import { volatileFields } from '../settings-volatile.js'
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_ACTS, DEFAULT_CATEGORIES, DEFAULT_INTENT_SETTINGS, DEFAULT_PREDICATES,
|
|
22
|
+
type IntentSettings,
|
|
23
|
+
} from './types.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 一条分类的 schema。`/intent/registry` 的写入校验也用它 —— 所以它和
|
|
27
|
+
* `INTENT_SETTINGS_SCHEMA` 一起住在这里,而不是留在 `host.ts`。
|
|
28
|
+
*/
|
|
29
|
+
export const CATEGORY_SCHEMA = z.object({
|
|
30
|
+
id: z.string().required(),
|
|
31
|
+
label: z.string().required(),
|
|
32
|
+
route: z.string().required(),
|
|
33
|
+
description: z.string(),
|
|
34
|
+
// 预置说法(few-shot)。老配置没有这个字段,缺省空数组。
|
|
35
|
+
examples: z.array(z.string()).default([]),
|
|
36
|
+
enabled: z.boolean().default(true),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** 一个 act 的 schema。同样被注册表写入校验复用。 */
|
|
40
|
+
export const ACT_SCHEMA = z.object({
|
|
41
|
+
id: z.string().required(),
|
|
42
|
+
label: z.string(),
|
|
43
|
+
description: z.string(),
|
|
44
|
+
enabled: z.boolean().default(true),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 意图设置 schema。
|
|
49
|
+
*
|
|
50
|
+
* 显式标注 `Schemastery<IntentSettings>` 不是为了文档 —— 而是**导出**它之后
|
|
51
|
+
* `tsc` 必须把这个类型写进 `.d.ts`,而纯推断出来的类型会引用 `@deepseek-ai/cosmokit`
|
|
52
|
+
* 的 `Dict`(`z.object` 的 `ObjectT` 用它),那是一条 `.pnpm/...` 里的不可移植路径,
|
|
53
|
+
* 于是报 TS2742。标注之后写出去的是这个名字,路径问题就没了。
|
|
54
|
+
*/
|
|
55
|
+
export const INTENT_SETTINGS_SCHEMA: Schemastery<IntentSettings> = z.object(volatileFields({
|
|
56
|
+
clarifyThreshold: z.number().min(0).max(1).default(DEFAULT_INTENT_SETTINGS.clarifyThreshold),
|
|
57
|
+
ttlSeconds: z.number().step(1).min(1).max(86400).default(DEFAULT_INTENT_SETTINGS.ttlSeconds),
|
|
58
|
+
// 0023:路由提示词没有开关。它随 `intent_record` 的工具描述下发,始终在场。
|
|
59
|
+
fallbackEnabled: z.boolean().default(DEFAULT_INTENT_SETTINGS.fallbackEnabled),
|
|
60
|
+
categories: z.array(CATEGORY_SCHEMA).default(DEFAULT_CATEGORIES),
|
|
61
|
+
acts: z.array(ACT_SCHEMA).default(DEFAULT_ACTS),
|
|
62
|
+
predicates: z.array(z.string()).default(DEFAULT_PREDICATES),
|
|
63
|
+
}))
|