@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.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/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +759 -2
- package/dist/app.d.ts +384 -0
- package/dist/app.js +1148 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +353 -0
- package/dist/config.d.ts +97 -0
- package/dist/config.js +171 -0
- package/dist/handlers/agentHandlers.d.ts +18 -0
- package/dist/handlers/agentHandlers.js +20 -0
- package/dist/handlers/assetHandlers.d.ts +29 -0
- package/dist/handlers/assetHandlers.js +35 -0
- package/dist/handlers/capabilities.d.ts +26 -0
- package/dist/handlers/capabilities.js +138 -0
- package/dist/handlers/completionHandlers.d.ts +18 -0
- package/dist/handlers/completionHandlers.js +13 -0
- package/dist/handlers/configHandlers.d.ts +31 -0
- package/dist/handlers/configHandlers.js +59 -0
- package/dist/handlers/conversationHandlers.d.ts +118 -0
- package/dist/handlers/conversationHandlers.js +314 -0
- package/dist/handlers/handoffHandlers.d.ts +17 -0
- package/dist/handlers/handoffHandlers.js +14 -0
- package/dist/handlers/hostHandlers.d.ts +39 -0
- package/dist/handlers/hostHandlers.js +56 -0
- package/dist/handlers/index.d.ts +128 -0
- package/dist/handlers/index.js +92 -0
- package/dist/handlers/messageHandlers.d.ts +11 -0
- package/dist/handlers/messageHandlers.js +10 -0
- package/dist/handlers/modelHandlers.d.ts +17 -0
- package/dist/handlers/modelHandlers.js +19 -0
- package/dist/handlers/noticeHandlers.d.ts +24 -0
- package/dist/handlers/noticeHandlers.js +24 -0
- package/dist/handlers/questionHandlers.d.ts +16 -0
- package/dist/handlers/questionHandlers.js +16 -0
- package/dist/handlers/searchHandlers.d.ts +12 -0
- package/dist/handlers/searchHandlers.js +12 -0
- package/dist/handlers/workspaceHandlers.d.ts +29 -0
- package/dist/handlers/workspaceHandlers.js +47 -0
- package/dist/i18n/en.d.ts +243 -0
- package/dist/i18n/en.js +270 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +72 -0
- package/dist/i18n/zh.d.ts +240 -0
- package/dist/i18n/zh.js +257 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/net/UploadReceiver.d.ts +186 -0
- package/dist/net/UploadReceiver.js +494 -0
- package/dist/net/WsHub.d.ts +231 -0
- package/dist/net/WsHub.js +689 -0
- package/dist/net/auth.d.ts +73 -0
- package/dist/net/auth.js +74 -0
- package/dist/net/discovery.d.ts +72 -0
- package/dist/net/discovery.js +171 -0
- package/dist/net/index.d.ts +14 -0
- package/dist/net/index.js +10 -0
- package/dist/runtime/ChatRunner.d.ts +374 -0
- package/dist/runtime/ChatRunner.js +1204 -0
- package/dist/runtime/ConversationDrafts.d.ts +43 -0
- package/dist/runtime/ConversationDrafts.js +47 -0
- package/dist/runtime/RunRegistry.d.ts +79 -0
- package/dist/runtime/RunRegistry.js +108 -0
- package/dist/runtime/ScopeRegistry.d.ts +153 -0
- package/dist/runtime/ScopeRegistry.js +259 -0
- package/dist/runtime/TransientRunner.d.ts +90 -0
- package/dist/runtime/TransientRunner.js +179 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.js +10 -0
- package/dist/services/AgentService.d.ts +150 -0
- package/dist/services/AgentService.js +718 -0
- package/dist/services/AssetService.d.ts +338 -0
- package/dist/services/AssetService.js +739 -0
- package/dist/services/AutoNaming.d.ts +92 -0
- package/dist/services/AutoNaming.js +235 -0
- package/dist/services/CompletionService.d.ts +122 -0
- package/dist/services/CompletionService.js +442 -0
- package/dist/services/ConfigService.d.ts +171 -0
- package/dist/services/ConfigService.js +695 -0
- package/dist/services/ConversationService.d.ts +274 -0
- package/dist/services/ConversationService.js +902 -0
- package/dist/services/CredentialService.d.ts +33 -0
- package/dist/services/CredentialService.js +86 -0
- package/dist/services/DiscoveryService.d.ts +182 -0
- package/dist/services/DiscoveryService.js +998 -0
- package/dist/services/HandoffService.d.ts +189 -0
- package/dist/services/HandoffService.js +414 -0
- package/dist/services/Logger.d.ts +130 -0
- package/dist/services/Logger.js +256 -0
- package/dist/services/MessageService.d.ts +88 -0
- package/dist/services/MessageService.js +280 -0
- package/dist/services/ModelService.d.ts +89 -0
- package/dist/services/ModelService.js +489 -0
- package/dist/services/NoticeService.d.ts +93 -0
- package/dist/services/NoticeService.js +217 -0
- package/dist/services/QuestionService.d.ts +184 -0
- package/dist/services/QuestionService.js +457 -0
- package/dist/services/SearchService.d.ts +54 -0
- package/dist/services/SearchService.js +117 -0
- package/dist/services/WorkspaceService.d.ts +96 -0
- package/dist/services/WorkspaceService.js +213 -0
- package/dist/services/imageSize.d.ts +41 -0
- package/dist/services/imageSize.js +146 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.js +15 -0
- package/dist/services/secretMask.d.ts +24 -0
- package/dist/services/secretMask.js +34 -0
- package/dist/storage/AssetRepository.d.ts +87 -0
- package/dist/storage/AssetRepository.js +180 -0
- package/dist/storage/ConversationRepository.d.ts +55 -0
- package/dist/storage/ConversationRepository.js +128 -0
- package/dist/storage/CredentialRepository.d.ts +28 -0
- package/dist/storage/CredentialRepository.js +47 -0
- package/dist/storage/Database.d.ts +95 -0
- package/dist/storage/Database.js +180 -0
- package/dist/storage/MessageRepository.d.ts +104 -0
- package/dist/storage/MessageRepository.js +323 -0
- package/dist/storage/MetaRepository.d.ts +21 -0
- package/dist/storage/MetaRepository.js +33 -0
- package/dist/storage/NoticeRepository.d.ts +31 -0
- package/dist/storage/NoticeRepository.js +89 -0
- package/dist/storage/QuestionRepository.d.ts +34 -0
- package/dist/storage/QuestionRepository.js +96 -0
- package/dist/storage/WorkspaceRepository.d.ts +25 -0
- package/dist/storage/WorkspaceRepository.js +44 -0
- package/dist/storage/dbWorker.d.ts +10 -0
- package/dist/storage/dbWorker.js +132 -0
- package/dist/storage/index.d.ts +20 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/migrations.d.ts +26 -0
- package/dist/storage/migrations.js +207 -0
- package/dist/storage/types.d.ts +298 -0
- package/dist/storage/types.js +14 -0
- package/dist/wire/errors.d.ts +105 -0
- package/dist/wire/errors.js +143 -0
- package/dist/wire/events.d.ts +102 -0
- package/dist/wire/events.js +13 -0
- package/dist/wire/frames.d.ts +106 -0
- package/dist/wire/frames.js +156 -0
- package/dist/wire/index.d.ts +31 -0
- package/dist/wire/index.js +31 -0
- package/dist/wire/methods.d.ts +1896 -0
- package/dist/wire/methods.js +99 -0
- package/dist/wire/states.d.ts +487 -0
- package/dist/wire/states.js +61 -0
- package/package.json +63 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通知服务:一条**独立通道**(PRD F-I8),它不进消息流。
|
|
3
|
+
*
|
|
4
|
+
* 三件事在这里定死:
|
|
5
|
+
*
|
|
6
|
+
* 1. **持久化**(PRD L3):通知落 `notices` 表,重启之后还在;永久通知的消失只有「手动清」
|
|
7
|
+
* 这一条路径(`notice.dismiss` / `notice.clear`),带持续时间的到点自己消失。
|
|
8
|
+
* 2. **持续时间的两套单位**(R15):契约用**秒**(用户语义:「这条提示留 3 秒」),
|
|
9
|
+
* 库里存**毫秒**(`notices.durationMs`)。换算只有本文件这一处 —— 别处再换一次,
|
|
10
|
+
* 迟早有一条通知留 3 毫秒或 3 天。
|
|
11
|
+
* 3. **存码不存句子**(D8 / architecture §6):库里存的是「码 + 参数」,读出时按**调用方
|
|
12
|
+
* 给的语言**渲染。语言挂在连接上,一条通知存死一种语言、换个语言连回来就错了。
|
|
13
|
+
*
|
|
14
|
+
* 语气三档 `info | warning | error`(R4):「已保存」这种一秒提示是 `info` + 短 duration,
|
|
15
|
+
* 不新增 success 档;`warning` / `error` 同时是宿主日志上浮的通道(PRD F-J2)。
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { DEFAULT_LANGUAGE, hasMessage, render } from "../i18n/index.js";
|
|
19
|
+
import { HostError } from "../wire/errors.js";
|
|
20
|
+
import { NOTICE_LEVELS } from "../wire/states.js";
|
|
21
|
+
/** 库里那条 `code` 认不出来时用的文案键(配置被手改坏、旧版本写进去的码) */
|
|
22
|
+
const UNKNOWN_CODE_KEY = "notice.unknown";
|
|
23
|
+
/** 时间戳转 wire 上的 ISO 8601 */
|
|
24
|
+
function iso(ms) {
|
|
25
|
+
return new Date(ms).toISOString();
|
|
26
|
+
}
|
|
27
|
+
/** 库里的语气档位:认不出来就按 `info` 摆(手改坏的数据不该让整条列表读不出来) */
|
|
28
|
+
function toLevel(value) {
|
|
29
|
+
return NOTICE_LEVELS.includes(value) ? value : "info";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 秒 → 毫秒(R15 的唯一一处换算)。
|
|
33
|
+
*
|
|
34
|
+
* 不是数字、负数、`NaN`、无穷大都按「参数不对」拒:这条通知要么留一段确定的时间,
|
|
35
|
+
* 要么永久留着,不存在「留一段说不清多久」。
|
|
36
|
+
*/
|
|
37
|
+
function toDurationMs(duration, field) {
|
|
38
|
+
if (duration === undefined || duration === null)
|
|
39
|
+
return null;
|
|
40
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) {
|
|
41
|
+
throw new HostError("invalid_params", {
|
|
42
|
+
params: { field },
|
|
43
|
+
detail: { field, reason: "not_a_duration", value: duration },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return Math.round(duration * 1000);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 一行库记录 → 契约上的 `NoticeView`(按给定语言渲染)。
|
|
50
|
+
*
|
|
51
|
+
* `duration` 在这里由毫秒换回秒:库里是机器口径,出网是用户口径。
|
|
52
|
+
*/
|
|
53
|
+
function toView(notice, language) {
|
|
54
|
+
const params = notice.params ?? {};
|
|
55
|
+
const known = hasMessage(notice.code);
|
|
56
|
+
const messageKey = known ? notice.code : UNKNOWN_CODE_KEY;
|
|
57
|
+
// 认不出来的码:把原码当参数交给兜底那句,用户至少能看到它是什么
|
|
58
|
+
const renderParams = known ? params : { ...params, code: notice.code };
|
|
59
|
+
const text = {
|
|
60
|
+
code: notice.code,
|
|
61
|
+
messageKey,
|
|
62
|
+
params: renderParams,
|
|
63
|
+
message: render(language, messageKey, renderParams),
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
id: notice.id,
|
|
67
|
+
level: toLevel(notice.level),
|
|
68
|
+
duration: notice.durationMs === null ? null : notice.durationMs / 1000,
|
|
69
|
+
text,
|
|
70
|
+
conversationId: notice.conversationId,
|
|
71
|
+
createdAt: iso(notice.createdAt),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** 按语言重渲染一句已存的文案(`messageKey` + `params` 都在 `LocalizedText` 里) */
|
|
75
|
+
export function relocalizeText(text, language) {
|
|
76
|
+
const messageKey = hasMessage(text.messageKey) ? text.messageKey : UNKNOWN_CODE_KEY;
|
|
77
|
+
return { ...text, message: render(language, messageKey, text.params) };
|
|
78
|
+
}
|
|
79
|
+
/** 按语言重渲染一个问题的提示与候选(多端各自的语言,PRD D8) */
|
|
80
|
+
function relocalizeQuestion(question, language) {
|
|
81
|
+
return {
|
|
82
|
+
...question,
|
|
83
|
+
prompt: relocalizeText(question.prompt, language),
|
|
84
|
+
options: question.options.map((option) => ({
|
|
85
|
+
...option,
|
|
86
|
+
label: relocalizeText(option.label, language),
|
|
87
|
+
detail: option.detail === null ? null : relocalizeText(option.detail, language),
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** 造上面那个本地化器(接线层把它注入 `WsHub` 的 `localizeEvent`) */
|
|
92
|
+
export function createEventLocalizer() {
|
|
93
|
+
return (event, payload, language) => {
|
|
94
|
+
if (event === "notice") {
|
|
95
|
+
const noticePayload = payload;
|
|
96
|
+
const notice = noticePayload.notice;
|
|
97
|
+
const localized = {
|
|
98
|
+
...noticePayload,
|
|
99
|
+
notice: { ...notice, text: relocalizeText(notice.text, language) },
|
|
100
|
+
};
|
|
101
|
+
return localized;
|
|
102
|
+
}
|
|
103
|
+
if (event === "question") {
|
|
104
|
+
const questionPayload = payload;
|
|
105
|
+
const localized = {
|
|
106
|
+
...questionPayload,
|
|
107
|
+
question: relocalizeQuestion(questionPayload.question, language),
|
|
108
|
+
};
|
|
109
|
+
return localized;
|
|
110
|
+
}
|
|
111
|
+
return payload;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export class NoticeService {
|
|
115
|
+
notices;
|
|
116
|
+
emit;
|
|
117
|
+
now;
|
|
118
|
+
newId;
|
|
119
|
+
constructor(deps) {
|
|
120
|
+
this.notices = deps.notices;
|
|
121
|
+
this.emit = deps.emit;
|
|
122
|
+
this.now = deps.now ?? Date.now;
|
|
123
|
+
this.newId = deps.newId ?? randomUUID;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 发一条通知(内部入口)。
|
|
127
|
+
*
|
|
128
|
+
* 落库 → 推 `notice` 事件 → 返回渲染好的视图。落库在推送之前:推送只对在线连接有意义
|
|
129
|
+
* (D5:断线即作废),而库里的那条是重连之后还能看到的真相(L3)。
|
|
130
|
+
*/
|
|
131
|
+
async create(input, language = DEFAULT_LANGUAGE) {
|
|
132
|
+
if (!NOTICE_LEVELS.includes(input.level)) {
|
|
133
|
+
throw new HostError("invalid_params", {
|
|
134
|
+
params: { field: "level" },
|
|
135
|
+
detail: { field: "level", value: input.level, levels: NOTICE_LEVELS },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (!hasMessage(input.code)) {
|
|
139
|
+
throw new HostError("invalid_params", {
|
|
140
|
+
params: { field: "code" },
|
|
141
|
+
detail: { field: "code", reason: "unknown_message_key", value: input.code },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const durationMs = toDurationMs(input.duration, "duration");
|
|
145
|
+
const params = { ...(input.params ?? {}) };
|
|
146
|
+
const id = this.newId();
|
|
147
|
+
const createdAt = this.now();
|
|
148
|
+
const row = {
|
|
149
|
+
id,
|
|
150
|
+
level: input.level,
|
|
151
|
+
code: input.code,
|
|
152
|
+
params: Object.keys(params).length > 0 ? params : null,
|
|
153
|
+
durationMs,
|
|
154
|
+
conversationId: input.conversationId ?? null,
|
|
155
|
+
createdAt,
|
|
156
|
+
};
|
|
157
|
+
await this.notices.create(row);
|
|
158
|
+
const notification = toView(row, language);
|
|
159
|
+
this.emit?.("notice", { notice: notification, at: iso(createdAt) });
|
|
160
|
+
return notification;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 日志上浮(PRD F-J2:warn / error 也要上通知栏)。
|
|
164
|
+
*
|
|
165
|
+
* 没有持续时间 = 永久:界面上的一闪而过不适合承载「宿主出了什么事」,
|
|
166
|
+
* 这类通知要留到用户看过并手动清掉。
|
|
167
|
+
*/
|
|
168
|
+
async log(level, summary, language = DEFAULT_LANGUAGE) {
|
|
169
|
+
return await this.create({
|
|
170
|
+
level,
|
|
171
|
+
code: level === "warning" ? "notice.logWarning" : "notice.logError",
|
|
172
|
+
params: { summary },
|
|
173
|
+
duration: null,
|
|
174
|
+
}, language);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 现在该显示的通知(PRD L3)。
|
|
178
|
+
*
|
|
179
|
+
* 先清掉已经到点的(`deleteExpired`),再按「永久 + 还没到点」读一遍:只按时间过滤而不清,
|
|
180
|
+
* 表会一直涨;只清不按时间过滤,同一毫秒内到点的那条仍会被读出来。
|
|
181
|
+
*/
|
|
182
|
+
async list(params = {}, language = DEFAULT_LANGUAGE) {
|
|
183
|
+
const now = this.now();
|
|
184
|
+
await this.notices.deleteExpired(now);
|
|
185
|
+
const active = await this.notices.listActive(now);
|
|
186
|
+
const views = active
|
|
187
|
+
.map((notice) => toView(notice, language))
|
|
188
|
+
.filter((view) => (params.level === undefined ? true : view.level === params.level))
|
|
189
|
+
.filter((view) => params.conversationId === undefined ? true : view.conversationId === params.conversationId);
|
|
190
|
+
return { notices: views };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 清掉一条(PRD L3:永久通知唯一能消失的方式)。
|
|
194
|
+
*
|
|
195
|
+
* 已经不存在时回 `dismissed: false`(这次有没有真的改动库),与 `notice.clear` 的口径一致。
|
|
196
|
+
*/
|
|
197
|
+
async dismiss(params) {
|
|
198
|
+
const id = typeof params.noticeId === "string" ? params.noticeId.trim() : "";
|
|
199
|
+
if (id.length === 0) {
|
|
200
|
+
throw new HostError("invalid_params", {
|
|
201
|
+
params: { field: "noticeId" },
|
|
202
|
+
detail: { field: "noticeId", reason: "empty" },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const changes = await this.notices.clear(id);
|
|
206
|
+
return { dismissed: changes > 0 };
|
|
207
|
+
}
|
|
208
|
+
/** 清掉一批(不给 `conversationId` 就清全部) */
|
|
209
|
+
async clear(params = {}) {
|
|
210
|
+
if (params.conversationId === undefined) {
|
|
211
|
+
const cleared = await this.notices.clearAll();
|
|
212
|
+
return { cleared };
|
|
213
|
+
}
|
|
214
|
+
const cleared = await this.notices.clearByConversation(params.conversationId);
|
|
215
|
+
return { cleared };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 问题服务:把「要不要交接 / 要不要登记这个目录 / 确认一下」变成**对象**(PRD D22 / F-I9)。
|
|
3
|
+
*
|
|
4
|
+
* 一个问题的四件事实都在这里:有 id、有状态、有生命周期、可持久化。多端看到的是同一个
|
|
5
|
+
* 对象(库里那一行),所以「另一个窗口已经答过了」这件事不需要另开一条同步通道 ——
|
|
6
|
+
* 第二次拉 `question.list` 就能看到状态变了。
|
|
7
|
+
*
|
|
8
|
+
* 三条贯穿本文件的边界:
|
|
9
|
+
*
|
|
10
|
+
* 1. **存码不存句子**(D8):`questions.payload` 里存的是文案键 + 参数 + 候选项的值,
|
|
11
|
+
* 读出时按调用方给的语言渲染。一条问题存死一种语言,换个连接就错了。
|
|
12
|
+
* 2. **答完之后接着往下走**(D22):`answer` 的副作用是宿主自己做掉的 —— 答「自交接 /
|
|
13
|
+
* 独立摘要」就当场开始交接,客户端不必再发一次 `handoff.start`。
|
|
14
|
+
* 3. **越线两端都拒绝时问题继续挂着**(D23 / F-D10):答不下去的那些路径(对话正在跑、
|
|
15
|
+
* 已经过了硬线而用户选了「就在这段里继续」)都不动这一行状态,所以问题不会被悄悄关掉。
|
|
16
|
+
* 本文件里没有「自动过期」「答错一次就作废」这类隐式转移。
|
|
17
|
+
*/
|
|
18
|
+
import type { ConfigManager } from "@ai-zen/agents-sdk";
|
|
19
|
+
import type { Language, MessageKey } from "../i18n/index.js";
|
|
20
|
+
import type { ChatEventEmitter } from "../runtime/ChatRunner.js";
|
|
21
|
+
import type { RunsView } from "../runtime/RunRegistry.js";
|
|
22
|
+
import type { ConversationRepository } from "../storage/ConversationRepository.js";
|
|
23
|
+
import type { MessageRepository } from "../storage/MessageRepository.js";
|
|
24
|
+
import type { QuestionRepository } from "../storage/QuestionRepository.js";
|
|
25
|
+
import type { HandoffStartParams, QuestionAnswerParams, QuestionAnswerResult, QuestionKind, QuestionListParams, QuestionListResult, QuestionView } from "../wire/index.js";
|
|
26
|
+
/** 一句话的「码 + 参数」(库里存的是它,读出时才渲染成一句人话) */
|
|
27
|
+
export interface LocalizedSource {
|
|
28
|
+
messageKey: MessageKey;
|
|
29
|
+
params?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/** 一个候选答案的入参 */
|
|
32
|
+
export interface QuestionOptionInput {
|
|
33
|
+
/** 提交回答时回传的值 */
|
|
34
|
+
value: string;
|
|
35
|
+
label: LocalizedSource;
|
|
36
|
+
detail?: LocalizedSource | null;
|
|
37
|
+
}
|
|
38
|
+
/** 内部「问一个问题」的入口(交接线、登记项目、二次确认都走它) */
|
|
39
|
+
export interface AskQuestionInput {
|
|
40
|
+
/** 挂在哪段对话上(问题总是为某一段对话停下来问的) */
|
|
41
|
+
conversationId: string;
|
|
42
|
+
kind: QuestionKind;
|
|
43
|
+
prompt: LocalizedSource;
|
|
44
|
+
options: QuestionOptionInput[];
|
|
45
|
+
/**
|
|
46
|
+
* 是否挡着这段对话(挡着时 `conversation.send` 会被拒,业务码 `question_pending`)。
|
|
47
|
+
*
|
|
48
|
+
* 缺省按种类定:交接确认与不可撤销动作的确认挡着对话,登记项目不挡 —— 后者与某一段
|
|
49
|
+
* 对话的生成无关,挡了就变成「必须先答一个跟这次发送没关系的问题」。
|
|
50
|
+
*/
|
|
51
|
+
blocking?: boolean;
|
|
52
|
+
/** 出网的稳定码;缺省用 `prompt.messageKey` */
|
|
53
|
+
code?: string;
|
|
54
|
+
/**
|
|
55
|
+
* 与界面无关、但答完之后用得到的事实(例如「哪个目录待登记」、要接着建哪段对话)。
|
|
56
|
+
*
|
|
57
|
+
* 存进 `questions.payload.data`:它不是文案,不跟着语言变;也不是契约上的东西(客户端
|
|
58
|
+
* 只看得到渲染好的句子与候选项),宿主答完后自己取用。
|
|
59
|
+
*/
|
|
60
|
+
data?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/** 回答时的现场事实:哪条连接答的、用哪种语言回话 */
|
|
63
|
+
export interface QuestionAnswerContext {
|
|
64
|
+
language?: Language;
|
|
65
|
+
/** 回答它的那条连接(多端时用来显示「别的窗口答了」) */
|
|
66
|
+
answeredBy?: string | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 答完之后的副作用入口(交接那一步)。
|
|
70
|
+
*
|
|
71
|
+
* 类型故意只声明本服务用得到的那一部分(`newConversationId`):交接服务返回的字段比这多,
|
|
72
|
+
* 结构上照样满足,免得两个服务为了一个字段互相 import 具体类。
|
|
73
|
+
*/
|
|
74
|
+
export interface QuestionHandoffPort {
|
|
75
|
+
start(params: HandoffStartParams, language?: Language): Promise<{
|
|
76
|
+
newConversationId: string | null;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
/** 硬线判定口:这段对话现在是不是已经过了硬线(PRD F-D1 / D23) */
|
|
80
|
+
export interface QuestionGuardProbe {
|
|
81
|
+
overGuard(conversationId: string): Promise<boolean>;
|
|
82
|
+
}
|
|
83
|
+
export interface QuestionServiceDeps {
|
|
84
|
+
questions: QuestionRepository;
|
|
85
|
+
/** 「这段对话正在跑一轮」的唯一真相;不注入时按「没在跑」处理 */
|
|
86
|
+
runs?: RunsView;
|
|
87
|
+
/** 硬线判定;不注入时按「没过线」处理 */
|
|
88
|
+
guard?: QuestionGuardProbe;
|
|
89
|
+
/** 交接入口(答完接着交接,D22);不注入时答完只改状态、不触发交接 */
|
|
90
|
+
handoff?: QuestionHandoffPort;
|
|
91
|
+
/** 推 `question` 事件(多端看到同一个,D22) */
|
|
92
|
+
emit?: ChatEventEmitter;
|
|
93
|
+
now?: () => number;
|
|
94
|
+
newId?: () => string;
|
|
95
|
+
}
|
|
96
|
+
/** 交接问题的三个候选值(F-D2:自交接 / 独立摘要 / 不交接) */
|
|
97
|
+
export declare const HANDOFF_OPTION_SELF = "self";
|
|
98
|
+
export declare const HANDOFF_OPTION_SUMMARY = "summary";
|
|
99
|
+
export declare const HANDOFF_OPTION_DECLINE = "decline";
|
|
100
|
+
export declare class QuestionService {
|
|
101
|
+
private readonly questions;
|
|
102
|
+
private readonly runs?;
|
|
103
|
+
private readonly guard?;
|
|
104
|
+
private readonly handoff?;
|
|
105
|
+
private readonly emit?;
|
|
106
|
+
private readonly now;
|
|
107
|
+
private readonly newId;
|
|
108
|
+
constructor(deps: QuestionServiceDeps);
|
|
109
|
+
/**
|
|
110
|
+
* 问一个问题(内部入口)。
|
|
111
|
+
*
|
|
112
|
+
* 先落库再推事件:库里那一行是「多端看同一个」的真相(D22),推送只是提醒在线的那几个
|
|
113
|
+
* 窗口去拉(L5 断线即作废)。
|
|
114
|
+
*/
|
|
115
|
+
ask(input: AskQuestionInput, language?: Language): Promise<QuestionView>;
|
|
116
|
+
/**
|
|
117
|
+
* 到交接线时问「自交接 / 独立摘要 / 不交接」(PRD F-D2 / D22)。
|
|
118
|
+
*
|
|
119
|
+
* 触发点在**对话停下之后**:这里只负责把问题摆出来,不参与任何流式过程。
|
|
120
|
+
* `percent` 是给人看的那个数(用量 / 交接线),数字本身留在库里与状态里。
|
|
121
|
+
*/
|
|
122
|
+
askHandoff(input: {
|
|
123
|
+
conversationId: string;
|
|
124
|
+
promptTokens: number;
|
|
125
|
+
handoffTokens: number;
|
|
126
|
+
blocking?: boolean;
|
|
127
|
+
}, language?: Language): Promise<QuestionView>;
|
|
128
|
+
/**
|
|
129
|
+
* 列出问题(PRD D22:多端看同一个)。
|
|
130
|
+
*
|
|
131
|
+
* `conversationId` 可选(R61):给了就只看那段对话的;不给就看全库(含没挂对话的那些,
|
|
132
|
+
* 例如「要不要把这个目录登记成项目」)。`state` 两种情况下都生效。
|
|
133
|
+
*/
|
|
134
|
+
list(params?: QuestionListParams, language?: Language): Promise<QuestionListResult>;
|
|
135
|
+
/** 这段对话上还挂着、且正在挡着它的那些问题(`conversation.send` 的前置检查用它) */
|
|
136
|
+
blockingQuestions(conversationId: string): Promise<QuestionView[]>;
|
|
137
|
+
/**
|
|
138
|
+
* 有挡着的问题就拒绝(PRD D23 / F-D10 的「宿主也拒绝」)。
|
|
139
|
+
*
|
|
140
|
+
* 只在真有 `blocking` 的问题时抛 `question_pending`,并把那条问题的提问当参数带上:
|
|
141
|
+
* 用户看到的是「哪个问题在挡着」,不是一句抽象的「还有问题没答」。
|
|
142
|
+
*/
|
|
143
|
+
assertSendable(conversationId: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* 回答一个问题(PRD D22 / F-I9)。
|
|
146
|
+
*
|
|
147
|
+
* 顺序是刻意的:
|
|
148
|
+
* 1. 已经答过 / 已失效的直接回当前状态(多端同时点,后到的那一端看到的是「已经答了」,
|
|
149
|
+
* 不重复触发副作用);
|
|
150
|
+
* 2. 值不在候选里 → `invalid_params`;
|
|
151
|
+
* 3. 对话正在跑一轮 → `busy`(D22:交接在对话停下之后触发,不在流式中途);
|
|
152
|
+
* 4. 过了硬线又选了「就在这段里继续」 → `context_guard`,**问题继续挂着**(D23);
|
|
153
|
+
* 5. 改状态 → 该交接就当场交接。
|
|
154
|
+
*
|
|
155
|
+
* 第 3、4 两步都在写状态之前返回,所以「答不下去」不会把问题弄成一个半答的中间态。
|
|
156
|
+
*/
|
|
157
|
+
answer(params: QuestionAnswerParams, context?: QuestionAnswerContext): Promise<QuestionAnswerResult>;
|
|
158
|
+
/**
|
|
159
|
+
* 显式取消一个问题(生命周期里的 `cancelled`)。
|
|
160
|
+
*
|
|
161
|
+
* 只有调用方明确要关掉时才用(例如对话被删、工作流换了方向)。没有任何自动取消的路径 ——
|
|
162
|
+
* 越线拒绝之后问题要继续挂着(D23),所以「答不下去」与「关掉它」是两件事。
|
|
163
|
+
*/
|
|
164
|
+
cancel(questionId: string, language?: Language): Promise<QuestionView>;
|
|
165
|
+
/**
|
|
166
|
+
* 答完之后接着往下走:交接(D22)。
|
|
167
|
+
*
|
|
168
|
+
* 只在问题已经落成 `answered` 之后才跑:答不下去的那些情况(越线、对话在跑)
|
|
169
|
+
* 在写状态之前就返回了,所以这里不需要再判断一遍。
|
|
170
|
+
*/
|
|
171
|
+
private applyEffects;
|
|
172
|
+
private requireQuestion;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 按配置与历史算「这段对话是不是已经过了硬线」。
|
|
176
|
+
*
|
|
177
|
+
* 硬线跟着模型走(`tokenThresholds.guard`,D27),用量取最近一条带用量的消息:
|
|
178
|
+
* 与 `conversation.state` 里那份 `ContextState` 是同一个口径(都看 `usage.prompt_tokens`)。
|
|
179
|
+
*/
|
|
180
|
+
export declare function createConversationGuardProbe(deps: {
|
|
181
|
+
conversations: ConversationRepository;
|
|
182
|
+
messages: MessageRepository;
|
|
183
|
+
configManager: ConfigManager;
|
|
184
|
+
}): QuestionGuardProbe;
|