hiwork-knowledge 0.1.1 → 0.2.0
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 +133 -39
- package/cordis.patch.yml +19 -2
- package/lib/client.js +1599 -45
- package/lib/index.js +27793 -6
- package/lib/prompt.js +31 -0
- package/lib/protocol.js +139 -0
- package/lib/rpc.js +103 -0
- package/lib/service.js +344 -0
- package/lib/tool-names.js +15 -0
- package/lib/tools.js +244 -0
- package/lib/types/client/KnowledgeView.d.ts +19 -5
- package/lib/types/client/SettingsSection.d.ts +42 -0
- package/lib/types/client/ToolCards.d.ts +26 -0
- package/lib/types/client/composer.d.ts +98 -0
- package/lib/types/client/contracts.d.ts +100 -2
- package/lib/types/client/focus.d.ts +19 -0
- package/lib/types/client/format.d.ts +74 -0
- package/lib/types/client/icons.d.ts +26 -0
- package/lib/types/client/index.d.ts +31 -2
- package/lib/types/client/locales.d.ts +213 -6
- package/lib/types/client/runtime.d.ts +66 -0
- package/lib/types/client/tool-result.d.ts +90 -0
- package/lib/types/index.d.ts +49 -10
- package/lib/types/prompt.d.ts +21 -0
- package/lib/types/protocol.d.ts +200 -0
- package/lib/types/rpc.d.ts +18 -0
- package/lib/types/service.d.ts +142 -0
- package/lib/types/tool-names.d.ts +11 -0
- package/lib/types/tools.d.ts +21 -0
- package/lib/types/types.d.ts +109 -0
- package/lib/types/weknora.d.ts +112 -0
- package/lib/types.js +112 -0
- package/lib/weknora.js +366 -0
- package/package.json +25 -5
package/lib/tools.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 侧知识库工具(M0:全部只读)。
|
|
3
|
+
*
|
|
4
|
+
* 规则:
|
|
5
|
+
* - 工具只在**挂载它们的根 Agent** 上生效:`exec.agent` 不是本 Agent、
|
|
6
|
+
* 或 `exec.signal.aborted` 时统一返回 `{ ok: false, code: 'cancelled' }`,不碰后端;
|
|
7
|
+
* - 检索/阅读结果按 `maxChunkChars` 截断,并在返回里显式 `truncated: true`
|
|
8
|
+
* (**不静默截断**:模型需要知道还有更多内容);
|
|
9
|
+
* - **不返回 `score`**:实测未绑 rerank 时它是 RRF 定值(0.0164),当相关度用会误导模型;
|
|
10
|
+
* - `knowledge_ask` 走 WeKnora 自己的问答链路(不是把片段塞给模型自答),
|
|
11
|
+
* 需要设置里配好自建 Agent,未配置时返回可执行的提示而不是让模型盲目重试;
|
|
12
|
+
* - 所有失败都收敛成 `{ ok: false, code, message }`,message 里不含凭据。
|
|
13
|
+
*/
|
|
14
|
+
import { defineTool, } from '@deepseek-ai/dsh-tools';
|
|
15
|
+
import { KNOWLEDGE_TOOL_GUIDE } from './prompt.js';
|
|
16
|
+
import { KNOWLEDGE_TOOL_NAMES } from './tool-names.js';
|
|
17
|
+
import { KnowledgeError } from './types.js';
|
|
18
|
+
// 工具名的单一事实源在 `tool-names.ts`(Web 半边认领 keyed 座位时用的是同一份)。
|
|
19
|
+
export { KNOWLEDGE_TOOL_NAMES };
|
|
20
|
+
const JSON_OUTPUT = {
|
|
21
|
+
schema: { type: 'json' },
|
|
22
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
23
|
+
};
|
|
24
|
+
function asRecord(value) {
|
|
25
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
26
|
+
return {};
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function toToolJson(value) {
|
|
30
|
+
return JSON.parse(JSON.stringify(value));
|
|
31
|
+
}
|
|
32
|
+
function errorMessage(error) {
|
|
33
|
+
if (error instanceof KnowledgeError)
|
|
34
|
+
return error.message;
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
function failureCode(error) {
|
|
38
|
+
return error instanceof KnowledgeError ? error.code : 'knowledge_error';
|
|
39
|
+
}
|
|
40
|
+
function failure(signal, error) {
|
|
41
|
+
if (signal.aborted)
|
|
42
|
+
return toToolJson({ ok: false, code: 'cancelled' });
|
|
43
|
+
return toToolJson({ ok: false, code: failureCode(error), message: errorMessage(error) });
|
|
44
|
+
}
|
|
45
|
+
function presentCall(title, kind, rawInput) {
|
|
46
|
+
return { card: 'generic', title, kind, ...(rawInput === undefined ? {} : { rawInput }) };
|
|
47
|
+
}
|
|
48
|
+
function isExecutingAgent(exec, agent) {
|
|
49
|
+
if (exec.agent === agent)
|
|
50
|
+
return true;
|
|
51
|
+
const value = asRecord(exec.agent);
|
|
52
|
+
return value['id'] === agent.id;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 给一个根 Agent 注册知识库工具。
|
|
56
|
+
* @param service - Host 侧知识库服务。
|
|
57
|
+
* @param agent - 目标根 Agent。
|
|
58
|
+
* @returns 注销器(注销全部已注册工具)。
|
|
59
|
+
*/
|
|
60
|
+
export function registerKnowledgeTools(service, agent) {
|
|
61
|
+
const disposers = [];
|
|
62
|
+
const register = (definition) => {
|
|
63
|
+
disposers.push(agent.ctx.tools.register(definition));
|
|
64
|
+
};
|
|
65
|
+
const guard = (exec) => isExecutingAgent(exec, agent) && !exec.signal.aborted;
|
|
66
|
+
try {
|
|
67
|
+
register(defineTool({
|
|
68
|
+
name: 'knowledge_list_bases',
|
|
69
|
+
description: '列出当前凭据可见的企业知识库(名称、ID、文档数)。当不确定该查哪个库时先调用它;' +
|
|
70
|
+
'知识库名称往往不足以区分用途,需要时应先用它拿到 ID 再限定检索范围。',
|
|
71
|
+
parameters: {},
|
|
72
|
+
output: JSON_OUTPUT,
|
|
73
|
+
async execute(_args, exec) {
|
|
74
|
+
if (!guard(exec))
|
|
75
|
+
return toToolJson({ ok: false, code: 'cancelled' });
|
|
76
|
+
try {
|
|
77
|
+
const bases = await service.listBases(exec.signal);
|
|
78
|
+
return toToolJson({
|
|
79
|
+
ok: true,
|
|
80
|
+
bases: bases.map(base => ({
|
|
81
|
+
id: base.id,
|
|
82
|
+
name: base.name,
|
|
83
|
+
description: base.description,
|
|
84
|
+
documentCount: base.documentCount,
|
|
85
|
+
})),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return failure(exec.signal, error);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
presentCall: () => presentCall('列出知识库', 'read'),
|
|
93
|
+
}));
|
|
94
|
+
register(defineTool({
|
|
95
|
+
name: 'knowledge_search',
|
|
96
|
+
description: '在企业知识库里做混合检索,返回**原文片段**(含所属文档标题与 chunk 编号),不做总结。' +
|
|
97
|
+
'这是查企业资料的主力工具:拿到片段后自己判断,必要时用 knowledge_read_document 读上下文。' +
|
|
98
|
+
'knowledge_base_ids 省略时检索默认范围(或凭据可见的全部知识库)。',
|
|
99
|
+
parameters: {
|
|
100
|
+
query: { type: 'string', required: true, description: '检索词;用业务语言描述你要找的内容。' },
|
|
101
|
+
knowledge_base_ids: {
|
|
102
|
+
type: 'array',
|
|
103
|
+
items: { type: 'string' },
|
|
104
|
+
description: '限定检索的知识库 ID 列表;省略时用默认范围。',
|
|
105
|
+
},
|
|
106
|
+
knowledge_ids: {
|
|
107
|
+
type: 'array',
|
|
108
|
+
items: { type: 'string' },
|
|
109
|
+
description: '进一步限定到具体文档(knowledge_id)列表。',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
output: JSON_OUTPUT,
|
|
113
|
+
timeoutMs: 60_000,
|
|
114
|
+
async execute(args, exec) {
|
|
115
|
+
if (!guard(exec))
|
|
116
|
+
return toToolJson({ ok: false, code: 'cancelled' });
|
|
117
|
+
try {
|
|
118
|
+
const hits = await service.search(args.query, {
|
|
119
|
+
baseIds: args.knowledge_base_ids,
|
|
120
|
+
...(args.knowledge_ids === undefined ? {} : { knowledgeIds: args.knowledge_ids }),
|
|
121
|
+
}, exec.signal);
|
|
122
|
+
const limit = service.effectiveConfig().maxChunkChars;
|
|
123
|
+
const items = hits.map(hit => {
|
|
124
|
+
const truncated = hit.content.length > limit;
|
|
125
|
+
return {
|
|
126
|
+
knowledgeId: hit.knowledgeId,
|
|
127
|
+
chunkId: hit.chunkId,
|
|
128
|
+
knowledgeTitle: hit.knowledgeTitle,
|
|
129
|
+
fileName: hit.fileName,
|
|
130
|
+
chunkIndex: hit.chunkIndex,
|
|
131
|
+
chunkType: hit.chunkType,
|
|
132
|
+
content: truncated ? hit.content.slice(0, limit) : hit.content,
|
|
133
|
+
truncated,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
return toToolJson({
|
|
137
|
+
ok: true,
|
|
138
|
+
hitCount: items.length,
|
|
139
|
+
hits: items,
|
|
140
|
+
...(items.length === 0
|
|
141
|
+
? {
|
|
142
|
+
hint: '检索返回 0 条。可能原因:① 后端「检索设置」未绑 rerank 模型(任意查询都会失真);' +
|
|
143
|
+
'② rerank 模型被删除重建导致配置里的 ID 失效;③ 该知识库确实没有相关内容。' +
|
|
144
|
+
'请把这一情况原样告诉用户,不要凭常识作答。',
|
|
145
|
+
}
|
|
146
|
+
: {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return failure(exec.signal, error);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
presentCall: args => presentCall(`检索知识库:${args.query}`, 'read', args.query),
|
|
154
|
+
}));
|
|
155
|
+
register(defineTool({
|
|
156
|
+
name: 'knowledge_read_document',
|
|
157
|
+
description: '按 knowledge_id 读取一篇文档的正文(分块拼装、按顺序返回,可分页)。' +
|
|
158
|
+
'检索只给片段,需要上下文时用它补齐;首页包含文档标题,长文档可以分页翻。',
|
|
159
|
+
parameters: {
|
|
160
|
+
knowledge_id: { type: 'string', required: true, description: '文档 ID(来自检索结果)。' },
|
|
161
|
+
page: { type: 'integer', description: '页码,从 1 开始,默认 1。' },
|
|
162
|
+
page_size: { type: 'integer', description: '每页分块数,默认 20。' },
|
|
163
|
+
},
|
|
164
|
+
output: JSON_OUTPUT,
|
|
165
|
+
timeoutMs: 60_000,
|
|
166
|
+
async execute(args, exec) {
|
|
167
|
+
if (!guard(exec))
|
|
168
|
+
return toToolJson({ ok: false, code: 'cancelled' });
|
|
169
|
+
try {
|
|
170
|
+
const doc = await service.readDocument(args.knowledge_id, { page: args.page, pageSize: args.page_size }, exec.signal);
|
|
171
|
+
return toToolJson({ ok: true, document: doc });
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return failure(exec.signal, error);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
presentCall: args => presentCall(`读取文档:${args.knowledge_id}`, 'read', args.knowledge_id),
|
|
178
|
+
}));
|
|
179
|
+
register(defineTool({
|
|
180
|
+
name: 'knowledge_ask',
|
|
181
|
+
description: '把问题**整体交给企业知识库的问答链路**(后端自己做检索与推理),返回带引用的答案。' +
|
|
182
|
+
'适合跨多篇文档的综述型问题;它会再消耗一次模型调用,较慢,且返回结论而非证据。' +
|
|
183
|
+
'需要逐条证据时请改用 knowledge_search。',
|
|
184
|
+
parameters: {
|
|
185
|
+
query: { type: 'string', required: true, description: '要问的问题。' },
|
|
186
|
+
knowledge_base_ids: {
|
|
187
|
+
type: 'array',
|
|
188
|
+
items: { type: 'string' },
|
|
189
|
+
description: '限定问答范围的知识库 ID;省略时用默认范围。',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
output: JSON_OUTPUT,
|
|
193
|
+
timeoutMs: 300_000,
|
|
194
|
+
async execute(args, exec) {
|
|
195
|
+
if (!guard(exec))
|
|
196
|
+
return toToolJson({ ok: false, code: 'cancelled' });
|
|
197
|
+
try {
|
|
198
|
+
const answer = await service.ask(args.query, args.knowledge_base_ids === undefined ? {} : { baseIds: args.knowledge_base_ids }, exec.signal);
|
|
199
|
+
const limit = service.effectiveConfig().maxChunkChars;
|
|
200
|
+
return toToolJson({
|
|
201
|
+
ok: true,
|
|
202
|
+
answer: answer.answer,
|
|
203
|
+
sessionId: answer.sessionId,
|
|
204
|
+
elapsedMs: answer.elapsedMs,
|
|
205
|
+
toolsUsed: answer.toolsUsed,
|
|
206
|
+
references: answer.references.map(reference => {
|
|
207
|
+
const truncated = reference.content.length > limit;
|
|
208
|
+
return {
|
|
209
|
+
knowledgeId: reference.knowledgeId,
|
|
210
|
+
chunkId: reference.chunkId,
|
|
211
|
+
knowledgeTitle: reference.knowledgeTitle,
|
|
212
|
+
chunkIndex: reference.chunkIndex,
|
|
213
|
+
chunkType: reference.chunkType,
|
|
214
|
+
content: truncated ? reference.content.slice(0, limit) : reference.content,
|
|
215
|
+
truncated,
|
|
216
|
+
};
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return failure(exec.signal, error);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
presentCall: args => presentCall(`问知识库:${args.query}`, 'read', args.query),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
for (const dispose of disposers.splice(0)) {
|
|
229
|
+
try {
|
|
230
|
+
dispose();
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// 注册中途失败:尽力回滚,不掩盖原始异常。
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return () => {
|
|
239
|
+
for (const dispose of disposers.splice(0))
|
|
240
|
+
dispose();
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** 供测试断言:工具说明里必须出现的行为约束。 */
|
|
244
|
+
export const KNOWLEDGE_TOOL_GUIDE_TEXT = KNOWLEDGE_TOOL_GUIDE;
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 中央页「知识库」(feature id `knowledge` / order 30)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 只读视图:知识库列表 + 文档列表 + 检索片段 + **文档正文**。
|
|
5
|
+
* - 数据全部来自 Host 的 loopback RPC(`KnowledgeRuntime`),本组件不发任何 HTTP;
|
|
6
|
+
* - 后端不可用时显示 `lastError` 与重试按钮,而不是空白页;
|
|
7
|
+
* - 自检告警(如「未绑 rerank」)直接显示在顶部:这类错误在后端是静默的,
|
|
8
|
+
* 只有显式提示用户,才不会把"结果不准"当成"企业没这个资料"。
|
|
9
|
+
*
|
|
10
|
+
* 版式:左栏选知识库、右栏看内容(列表 / 检索结果 / 文档正文三态互斥),检索行钉在右栏顶部。
|
|
11
|
+
*
|
|
12
|
+
* 中央区接管时**页面根不滚**,滚动收进两栏各自的 `.hiwork-knowledge-scroll`:在几百篇
|
|
13
|
+
* 文档里滚到一半想换个库或改检索词,控件还得在手边——而整页滚动会把它们一起带走。
|
|
14
|
+
* 降级到设置页分区时没有高度约束,那几条「填满剩余高度再裁剪」的规则自然不生效。
|
|
15
|
+
*
|
|
16
|
+
* 三个可观察的版式纪律(都有测试盯着,见 `tests/styles-coverage.spec.ts`):
|
|
17
|
+
* 1. 原生 `<ul>` 的圆点与 40px 左缩进必须清掉——留着就是「没写样式」的观感;
|
|
18
|
+
* 2. 区块标题不能是裸 `<h2>`(浏览器默认给 1.5em 粗体,比页头还大,层级直接反过来);
|
|
19
|
+
* 3. 组件里出现的每个 `hiwork-knowledge-*` 类名都必须在 `styles.css` 里有规则。
|
|
6
20
|
*/
|
|
7
21
|
import { type ReactElement } from 'react';
|
|
8
22
|
import type { KnowledgeViewProps } from './contracts.js';
|
|
9
23
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @param props -
|
|
24
|
+
* 知识库页(中央内容区)。
|
|
25
|
+
* @param props - 翻译函数、运行时、可选的关闭回调与会话发送能力。
|
|
12
26
|
*/
|
|
13
27
|
export declare function KnowledgeView(props: KnowledgeViewProps): ReactElement;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设置页「知识库」分区:凭据与默认范围 + 自检。
|
|
3
|
+
*
|
|
4
|
+
* 与中央页的分工:
|
|
5
|
+
* - 中央页是**浏览**(列表/文档/检索);
|
|
6
|
+
* - 本分区是**配置**,所以它不会被中央页的注册撤下——没有 core 时也照常可用;
|
|
7
|
+
* - 自检(`测试连接`)会真实打后端,并展示逐条结论:连接、探针检索、问答 Agent。
|
|
8
|
+
* 其中「rerank 未生效」只有靠分数区间才看得出来(后端不会报错),是这块 UI 的主要价值。
|
|
9
|
+
*/
|
|
10
|
+
import { type ReactElement } from 'react';
|
|
11
|
+
import type { KnowledgeSettingsProps } from './contracts.js';
|
|
12
|
+
import type { KnowledgeConfigPatchWire } from './runtime.js';
|
|
13
|
+
/** 把逗号/换行分隔的知识库 ID 串解析成数组。 */
|
|
14
|
+
export declare function parseBaseIds(raw: string): string[];
|
|
15
|
+
/** 表单初值(从 Host 快照投影)。 */
|
|
16
|
+
export interface SettingsForm {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
apiKey: string;
|
|
19
|
+
tenantId: string;
|
|
20
|
+
defaultBases: string;
|
|
21
|
+
maxResults: string;
|
|
22
|
+
maxChunkChars: string;
|
|
23
|
+
agentId: string;
|
|
24
|
+
chatModelId: string;
|
|
25
|
+
}
|
|
26
|
+
/** 依据快照构造表单初值(apiKey 永远留空:Key 不回传)。 */
|
|
27
|
+
export declare function formFromSnapshot(config: {
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
tenantId: string;
|
|
30
|
+
defaultBaseIds: readonly string[];
|
|
31
|
+
maxResults: number;
|
|
32
|
+
maxChunkChars: number;
|
|
33
|
+
agentId: string;
|
|
34
|
+
chatModelId: string;
|
|
35
|
+
}): SettingsForm;
|
|
36
|
+
/** 表单 → 补丁:只提交变化过的字段;`apiKey` 仅在用户真的输入时才提交。 */
|
|
37
|
+
export declare function patchFromForm(form: SettingsForm, initial: SettingsForm): KnowledgeConfigPatchWire;
|
|
38
|
+
/**
|
|
39
|
+
* 知识库设置分区。
|
|
40
|
+
* @param props - 翻译函数、运行时与宿主提供的关闭回调。
|
|
41
|
+
*/
|
|
42
|
+
export declare function KnowledgeSettings(props: KnowledgeSettingsProps): ReactElement;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识库工具调用的**专属卡片**(`tool.call.toolview`,按线上工具名 keyed)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须有它:M0 起 Agent 就能查知识库了,但调用走的是 shell 的**通用工具行**,
|
|
5
|
+
* 用户看到的是「它回答了」而不是「它查到了什么」。卡片把检索结果变成可读的凭据:
|
|
6
|
+
* 文档名 + 段号 + 片段 + 出处,以及「追问」与「在知识库页打开」两个动作。
|
|
7
|
+
*
|
|
8
|
+
* 注册是**纯增量**的:keyed 槽位里未认领的键落回通用工具行,我们只认领自己的 4 个工具名。
|
|
9
|
+
*
|
|
10
|
+
* 三条实测得来的渲染约束:
|
|
11
|
+
* 1. `block.argsRaw` 是**原始 JSON 字符串**(running 阶段唯一的可用信息),结果则藏在
|
|
12
|
+
* `content[0].text` 里又是一层 JSON —— 解析全在 `tool-result.ts`(纯函数,有单测);
|
|
13
|
+
* 2. 结果还没回来时(running)也要渲染:只显示「正在检索知识库 · <查询词>」,否则工具执行
|
|
14
|
+
* 期间这个位置是空的,用户不知道它在干嘛;
|
|
15
|
+
* 3. `followUp` 与 `openDocument` 是**可选能力**:宿主没给(或会话作用域解析不到)时不渲染
|
|
16
|
+
* 对应按钮,而不是渲染一个点了报错的按钮。
|
|
17
|
+
*/
|
|
18
|
+
import { type ReactElement } from 'react';
|
|
19
|
+
import type { KnowledgeToolRowProps } from './contracts.js';
|
|
20
|
+
/**
|
|
21
|
+
* 一次知识库工具调用的卡片。
|
|
22
|
+
* @param props - keyed 工具视图的 owner 面 + 本地化座 + 可选动作。
|
|
23
|
+
*/
|
|
24
|
+
export declare function KnowledgeToolRow(props: KnowledgeToolRowProps): ReactElement;
|
|
25
|
+
/** 便于测试断言:卡片只认领这几个工具名。 */
|
|
26
|
+
export declare const KNOWLEDGE_TOOLVIEW_TITLES: Record<string, string>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把一段文本放进**某个会话的输入框**(卡片上的「追问」、页面上的「带进聊天」)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不是 `conversation.send(text)`:那会立刻排一轮对话发出去,用户没有反悔余地;
|
|
5
|
+
* 而且 `send()` 是**按调用方作用域寻址**的,本插件的 ctx 是 root 作用域,直接调会抛
|
|
6
|
+
* `conversation.send requires a session scope`。放进草稿既符合「带进聊天」的语义,
|
|
7
|
+
* 也把「发不发、要不要改」留给用户。
|
|
8
|
+
*
|
|
9
|
+
* 三个必须照抄的宿主细节(都来自 `hiwork-capabilities/src/client/composer.ts` 的实测结论):
|
|
10
|
+
* 1. **`actx.get('conversation')` 而不是 `actx.conversation`**:后者是属性访问,会撞上
|
|
11
|
+
* cordis 的注入门禁(`cannot get property "conversation" without inject`),而本插件
|
|
12
|
+
* 的顶层 `inject` 里没有 `conversation`(也不该有——那是可选能力);
|
|
13
|
+
* 2. **`sessions.scope(id)` 失败时退到 `sessions.binding(id).ctx`**:两条路在不同宿主
|
|
14
|
+
* 版本上互为兜底,且都要 try/catch——读属性本身就可能抛;
|
|
15
|
+
* 3. **读快照与插入之间不得有 await**:`draftRev` 是 CAS 票据,中间让出一次事件循环,
|
|
16
|
+
* 草稿被别人改过就会必然被拒。所以本模块整体是**同步**的。
|
|
17
|
+
*
|
|
18
|
+
* 本文件零依赖:bundle 必须自包含。
|
|
19
|
+
*/
|
|
20
|
+
/** 草稿里的引用 chip 占位(结构契约)。 */
|
|
21
|
+
export interface ComposerOccurrence {
|
|
22
|
+
readonly offset: number;
|
|
23
|
+
readonly length: number;
|
|
24
|
+
}
|
|
25
|
+
/** 输入机状态里本模块用到的部分(结构契约)。 */
|
|
26
|
+
export interface ComposerInputState {
|
|
27
|
+
/** 剪贴板投影:chip 展开成各自的 `clipboardText`。 */
|
|
28
|
+
readonly draft: string;
|
|
29
|
+
/** 单调递增的编辑器版本号,插入时用作 CAS 票据。 */
|
|
30
|
+
readonly draftRev: number;
|
|
31
|
+
/**
|
|
32
|
+
* 提交相位;旧宿主可能没有这个字段,缺省视为可写。
|
|
33
|
+
*
|
|
34
|
+
* 写成 `| undefined`(而不是裸 `?`)是本仓库 tsconfig 的 `exactOptionalPropertyTypes`
|
|
35
|
+
* 要求:裸可选成员不能显式传 `undefined`,而调用方/测试需要能这么写。
|
|
36
|
+
*/
|
|
37
|
+
readonly phase?: 'plain' | 'adjudicating' | 'claimed' | 'submitting' | undefined;
|
|
38
|
+
/** 草稿里的引用 chip,按 offset 升序。 */
|
|
39
|
+
readonly occurrences?: readonly ComposerOccurrence[] | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** 会话作用域的输入机面(`conversation.input.for(actx)` 的返回值)。 */
|
|
42
|
+
export interface ComposerInsertionTarget {
|
|
43
|
+
readonly state: {
|
|
44
|
+
getSnapshot(): ComposerInputState;
|
|
45
|
+
};
|
|
46
|
+
/** 把普通文本替换进 pick-time span;旧宿主可能没有这个方法。 */
|
|
47
|
+
insertText?(text: string, span: {
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
draftRev: number;
|
|
51
|
+
}, keepCompleting?: boolean): boolean;
|
|
52
|
+
}
|
|
53
|
+
/** `actx.get('conversation')` 的最小结构面。 */
|
|
54
|
+
export interface ConversationAccess {
|
|
55
|
+
readonly input: {
|
|
56
|
+
for(actx: unknown): ComposerInsertionTarget | undefined;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** `ctx.sessions` 的最小面(`scope` 与 `binding` 互为兜底)。 */
|
|
60
|
+
export interface SessionScopeAccess {
|
|
61
|
+
scope?(id: string): unknown;
|
|
62
|
+
binding?(id: string): {
|
|
63
|
+
ctx?: unknown;
|
|
64
|
+
} | undefined;
|
|
65
|
+
list?: {
|
|
66
|
+
getSnapshot(): {
|
|
67
|
+
readonly current?: string | undefined;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** 插入结果。失败一律**返回原因**,不抛、不静默——静默会让按钮看起来是坏的。 */
|
|
72
|
+
export type ComposerInsertResult = {
|
|
73
|
+
readonly ok: true;
|
|
74
|
+
} | {
|
|
75
|
+
readonly ok: false;
|
|
76
|
+
readonly reason: 'no-target' | 'busy' | 'rejected';
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* 探测草稿在**坐标投影**里的末尾偏移。
|
|
80
|
+
*
|
|
81
|
+
* 用 `occurrences` 而不是 `draft.length`:chip 在坐标里只占 1 个位置,而 `draft` 里是
|
|
82
|
+
* 展开后的全文。拿错投影会被宿主直接拒掉。
|
|
83
|
+
* @param state - 输入机快照。
|
|
84
|
+
* @returns 探测坐标里的草稿末尾偏移。
|
|
85
|
+
*/
|
|
86
|
+
export declare function detectTextLength(state: ComposerInputState): number;
|
|
87
|
+
/**
|
|
88
|
+
* 把文本追加进某个会话输入框的草稿末尾。
|
|
89
|
+
*
|
|
90
|
+
* 落点选**末尾**:`/名字 ` 这类文本由宿主自己的触发器敲在光标处,我们追加在末尾不会
|
|
91
|
+
* 抢走用户已经写了一半的话;中间同样不得有 await(见文件头第 3 点)。
|
|
92
|
+
*
|
|
93
|
+
* @param sessions - `ctx.sessions`;缺失时直接判定没有目标。
|
|
94
|
+
* @param text - 要追加的文本。
|
|
95
|
+
* @param sessionId - 槽位注入的会话 id;缺省退到 `sessions.list.current`。
|
|
96
|
+
* @returns 成功或失败原因。
|
|
97
|
+
*/
|
|
98
|
+
export declare function insertDraftText(sessions: SessionScopeAccess | undefined, text: string, sessionId?: string): ComposerInsertResult;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComposerInsertResult, SessionScopeAccess } from './composer.js';
|
|
1
2
|
/**
|
|
2
3
|
* client 半边的**类型专用**宿主契约。
|
|
3
4
|
*
|
|
@@ -7,16 +8,74 @@
|
|
|
7
8
|
* - 所有成员都按“可选能力”建模:宿主版本差异只会让某块 UI 降级,不会让整页崩掉。
|
|
8
9
|
*/
|
|
9
10
|
import type { ComponentType, ReactNode } from 'react';
|
|
11
|
+
import type { KnowledgeRuntime } from './runtime.js';
|
|
12
|
+
/** 宿主 loopback RPC:`rpc.call(channel, endpoint, payload, signal?)`。 */
|
|
13
|
+
export interface ClientRpc {
|
|
14
|
+
call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
15
|
+
}
|
|
10
16
|
/** `ctx.slots.register` 的注册选项(label 用函数以便跟随语言切换)。 */
|
|
11
17
|
export interface SlotRegisterOptions {
|
|
12
18
|
name: string;
|
|
13
19
|
id?: string;
|
|
20
|
+
/**
|
|
21
|
+
* **keyed 槽位的分派键**。`tool.call.toolview` 按**线上工具名**分派:
|
|
22
|
+
* 未认领的键落回通用工具行,所以为自己的工具注册是纯增量行为(不是接管);
|
|
23
|
+
* 拼错不会抛错,只是永远不渲染。
|
|
24
|
+
*/
|
|
25
|
+
key?: string;
|
|
14
26
|
order?: number;
|
|
15
27
|
locale?: string;
|
|
16
28
|
label?: () => string;
|
|
17
29
|
icon?: string;
|
|
18
|
-
/**
|
|
19
|
-
|
|
30
|
+
/**
|
|
31
|
+
* 宿主合成到组件属性上的注入面。
|
|
32
|
+
*
|
|
33
|
+
* 参数形状按槽位作用域而异(实测自 shell 自己的注册):
|
|
34
|
+
* - root 作用域槽位:`inject()`;
|
|
35
|
+
* - **session 作用域槽位:`inject(sessionId, actions)`** —— 第一个参数是会话 id
|
|
36
|
+
* (`tool.call.toolview` 的 scope 就是 `session`,因此卡片能拿到自己所属的会话);
|
|
37
|
+
* - `actions` 是**本注册自己提供的 store** 的动作(不传 store 时为 undefined),
|
|
38
|
+
* 所以它不是通用的编辑器接口,别拿它当 `setDraft` 用。
|
|
39
|
+
*/
|
|
40
|
+
inject?: ((...args: never[]) => object) | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** 线上文本块(工具 `render` 产出的内容块;只建模本插件用得到的形状)。 */
|
|
43
|
+
export interface ContentBlockLike {
|
|
44
|
+
readonly type?: unknown;
|
|
45
|
+
readonly text?: unknown;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `tool.call.toolview` 的 `block`:running 或 settled 二选一。
|
|
49
|
+
*
|
|
50
|
+
* 形状来自 shell 的 `ToolCallBlock = RunningToolCall | ToolResultNode`(实测):
|
|
51
|
+
* - running 只有 `argsRaw`(**原始 JSON 字符串**),没有 `kind`;
|
|
52
|
+
* - settled 带 `kind: 'tool-result'`,结果在 `content: ContentBlock[]` 里,
|
|
53
|
+
* 并且能用 `call?.argsRaw` 回填调用参数(窗口截断时 `call` 为 null)。
|
|
54
|
+
* 本插件不改这些字段,只读。
|
|
55
|
+
*/
|
|
56
|
+
export interface ToolCallBlockLike {
|
|
57
|
+
readonly kind?: 'tool-result' | undefined;
|
|
58
|
+
readonly callId?: string | undefined;
|
|
59
|
+
readonly argsRaw?: string | undefined;
|
|
60
|
+
readonly call?: {
|
|
61
|
+
readonly name?: unknown;
|
|
62
|
+
readonly argsRaw?: unknown;
|
|
63
|
+
} | null | undefined;
|
|
64
|
+
readonly content?: readonly ContentBlockLike[] | undefined;
|
|
65
|
+
readonly isError?: boolean | undefined;
|
|
66
|
+
readonly error?: {
|
|
67
|
+
readonly name?: unknown;
|
|
68
|
+
readonly code?: unknown;
|
|
69
|
+
} | undefined;
|
|
70
|
+
readonly subCalls?: readonly ToolCallBlockLike[] | undefined;
|
|
71
|
+
}
|
|
72
|
+
/** `tool.call.toolview` 的 owner 面(本插件只消费这几个字段)。 */
|
|
73
|
+
export interface ToolCallOwnerPropsLike {
|
|
74
|
+
readonly callId?: string | undefined;
|
|
75
|
+
readonly toolName?: string | undefined;
|
|
76
|
+
readonly block: ToolCallBlockLike;
|
|
77
|
+
/** 可选:把这次调用丢进轨迹视图(宿主没提供时不渲染该入口)。 */
|
|
78
|
+
readonly inspect?: (() => void) | undefined;
|
|
20
79
|
}
|
|
21
80
|
/** 宿主语言服务:注册命名空间词典 + 绑定翻译函数。 */
|
|
22
81
|
export interface ClientLocale {
|
|
@@ -37,6 +96,10 @@ export interface ClientSlots {
|
|
|
37
96
|
/** client 插件拿到的宿主上下文(结构兼容 DSH 0.1.2-rc.1 client Context)。 */
|
|
38
97
|
export interface ClientContext {
|
|
39
98
|
effect(factory: () => void | (() => void), label?: string): void;
|
|
99
|
+
/** loopback RPC:Web 半边取数与提交意图的唯一通道(不直接访问 WeKnora)。 */
|
|
100
|
+
connection: {
|
|
101
|
+
readonly rpc: ClientRpc;
|
|
102
|
+
};
|
|
40
103
|
locale: ClientLocale;
|
|
41
104
|
slots: ClientSlots;
|
|
42
105
|
/**
|
|
@@ -89,12 +152,47 @@ export interface HiWorkFeatureRenderContext {
|
|
|
89
152
|
readonly openNativeSession: (sessionId: string) => void;
|
|
90
153
|
readonly openNativeHome: () => void;
|
|
91
154
|
}
|
|
155
|
+
/** `ctx.sessions` 的最小面(结构契约见 `./composer.js`)。 */
|
|
156
|
+
export type ClientSessions = SessionScopeAccess;
|
|
92
157
|
/** 翻译函数:`t(key, params)`,未命中时宿主回退到 key 本身。 */
|
|
93
158
|
export type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
159
|
+
/**
|
|
160
|
+
* 工具卡片(`tool.call.toolview`)的属性。
|
|
161
|
+
*
|
|
162
|
+
* `t` 来自注册时的 `locale` 座;`block`/`toolName`/`inspect` 来自 keyed 槽位的 owner 面;
|
|
163
|
+
* `followUp` / `openDocument` 是本插件自己经 `inject` 注入的**可选能力**——
|
|
164
|
+
* 宿主或会话作用域解析不到时就不传,卡片会隐藏对应按钮而不是点了报错。
|
|
165
|
+
*/
|
|
166
|
+
export interface KnowledgeToolRowProps extends ToolCallOwnerPropsLike {
|
|
167
|
+
t: Translate;
|
|
168
|
+
/** 往这个卡片所属的会话发一条消息(「追问」)。 */
|
|
169
|
+
/** 把追问写进本会话的输入框;**返回结果**,失败原因由卡片显示在行内。 */
|
|
170
|
+
followUp?: ((text: string) => ComposerInsertResult) | undefined;
|
|
171
|
+
/** 在知识库页打开这篇文档。 */
|
|
172
|
+
openDocument?: ((knowledgeId: string) => void) | undefined;
|
|
173
|
+
}
|
|
94
174
|
/** 知识库页组件属性。 */
|
|
95
175
|
export interface KnowledgeViewProps {
|
|
96
176
|
/** 本插件命名空间(`hiwork-knowledge`)的翻译函数。 */
|
|
97
177
|
t: Translate;
|
|
178
|
+
/** 客户端运行时(Host 快照 + 动作;视图不直接访问 WeKnora)。 */
|
|
179
|
+
runtime: KnowledgeRuntime;
|
|
98
180
|
/** 关闭设置面板(降级到设置页分区时由宿主提供)。 */
|
|
99
181
|
closeSettings?: () => void;
|
|
182
|
+
/**
|
|
183
|
+
* 把一段文本放进**当前打开**会话的输入框(「带进聊天」)。
|
|
184
|
+
*
|
|
185
|
+
* 可选:解析不到会话(没有打开的会话 / 宿主没提供 `sessions`)时不传,
|
|
186
|
+
* 页面因此不渲染那两个按钮——比渲染一个点了没反应的入口诚实。返回结果而不是抛,
|
|
187
|
+
* 是为了让页面能把「为什么没放进去」直接显示出来。
|
|
188
|
+
*/
|
|
189
|
+
putInComposer?: ((text: string) => ComposerInsertResult) | undefined;
|
|
190
|
+
}
|
|
191
|
+
/** 设置页「知识库」分区的组件属性(与中央页共用同一个运行时)。 */
|
|
192
|
+
export interface KnowledgeSettingsProps {
|
|
193
|
+
t: Translate;
|
|
194
|
+
runtime: KnowledgeRuntime;
|
|
195
|
+
/** 宿主提供的关闭回调(不同宿主版本给的键名不同)。 */
|
|
196
|
+
close?: () => void;
|
|
197
|
+
closeSettings?: () => void;
|
|
100
198
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨座位的「跳转焦点」:聊天里的卡片要求知识库页打开某篇文档。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:中央页是 DOM 接管 + 独立 React 根,卡片在**聊天**里,两者之间没有
|
|
5
|
+
* 共享的 React 树,插件也不能碰 shell 的 DOM。于是用模块级的一次性信箱传递意图:
|
|
6
|
+
* 卡片写入 → 调用 `hiworkFeatureCenter.openFeature('knowledge')` 切页 → 页面挂载时取走。
|
|
7
|
+
*
|
|
8
|
+
* 取走即清空(consume 语义),所以:
|
|
9
|
+
* - 页面重复渲染不会反复跳转;
|
|
10
|
+
* - 用户手动切库/返回列表后不会被旧意图拽回去。
|
|
11
|
+
*
|
|
12
|
+
* client bundle 纯度:本文件零依赖(连 React 都不 import),可被任何半边引用。
|
|
13
|
+
*/
|
|
14
|
+
/** 登记「下次打开知识库页时请定位到这篇文档」。 */
|
|
15
|
+
export declare function requestDocumentFocus(knowledgeId: string): void;
|
|
16
|
+
/** 取走当前焦点(取走即清空);没有待处理意图时返回 null。 */
|
|
17
|
+
export declare function consumeDocumentFocus(): string | null;
|
|
18
|
+
/** 是否有待处理意图(只读,不消费;供渲染期判断用)。 */
|
|
19
|
+
export declare function hasDocumentFocus(): boolean;
|