@coralai/sps-cli 0.65.33 → 0.65.34
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/dist/chat-protocol/events.d.ts +131 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/events.js +134 -0
- package/dist/chat-protocol/events.js.map +1 -0
- package/dist/chat-protocol/form.d.ts +66 -0
- package/dist/chat-protocol/form.d.ts.map +1 -0
- package/dist/chat-protocol/form.js +25 -0
- package/dist/chat-protocol/form.js.map +1 -0
- package/dist/chat-protocol/index.d.ts +21 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +17 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/reduce.d.ts +33 -0
- package/dist/chat-protocol/reduce.d.ts.map +1 -0
- package/dist/chat-protocol/reduce.js +204 -0
- package/dist/chat-protocol/reduce.js.map +1 -0
- package/dist/chat-protocol/types.d.ts +85 -0
- package/dist/chat-protocol/types.d.ts.map +1 -0
- package/dist/chat-protocol/types.js +9 -0
- package/dist/chat-protocol/types.js.map +1 -0
- package/dist/chat-protocol/typewriter.d.ts +25 -0
- package/dist/chat-protocol/typewriter.d.ts.map +1 -0
- package/dist/chat-protocol/typewriter.js +36 -0
- package/dist/chat-protocol/typewriter.js.map +1 -0
- package/dist/console/chatEventLog.d.ts +47 -0
- package/dist/console/chatEventLog.d.ts.map +1 -0
- package/dist/console/chatEventLog.js +79 -0
- package/dist/console/chatEventLog.js.map +1 -0
- package/dist/console/routes/chat.d.ts +3 -0
- package/dist/console/routes/chat.d.ts.map +1 -1
- package/dist/console/routes/chat.js +173 -114
- package/dist/console/routes/chat.js.map +1 -1
- package/dist/console-assets/assets/{ShowcasePage-NmXybDqC.js → ShowcasePage-x-qRvSx_.js} +1 -1
- package/dist/console-assets/assets/{index-DCAnGMiQ.js → index-Ifjq4orx.js} +216 -216
- package/dist/console-assets/index.html +1 -1
- package/dist/core/activityStream.d.ts +1 -1
- package/dist/core/activityStream.d.ts.map +1 -1
- package/dist/core/activityStream.js +7 -2
- package/dist/core/activityStream.js.map +1 -1
- package/dist/core/agents/AgentBackend.d.ts +11 -1
- package/dist/core/agents/AgentBackend.d.ts.map +1 -1
- package/dist/core/agents/AgentBackend.js.map +1 -1
- package/dist/core/interactions/formSpec.d.ts +57 -0
- package/dist/core/interactions/formSpec.d.ts.map +1 -0
- package/dist/core/interactions/formSpec.js +123 -0
- package/dist/core/interactions/formSpec.js.map +1 -0
- package/dist/openai-agent/interactions/broker.d.ts.map +1 -1
- package/dist/openai-agent/interactions/broker.js +38 -77
- package/dist/openai-agent/interactions/broker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { OPTIMISTIC_ID_PREFIX } from './types.js';
|
|
2
|
+
function freshPending(sessionId, turnId) {
|
|
3
|
+
return { sessionId, turnId, blocks: [], done: false, finalMessage: null };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 取当前回合作为基底;回合对不上(没收到 turn.pending、或新回合直接来了内容)就自建,
|
|
7
|
+
* **不丢第一个 chunk**。
|
|
8
|
+
*/
|
|
9
|
+
function baseFor(state, turnId) {
|
|
10
|
+
return state.pending && state.pending.turnId === turnId
|
|
11
|
+
? state.pending
|
|
12
|
+
: freshPending(state.sessionId, turnId);
|
|
13
|
+
}
|
|
14
|
+
function appendMessage(messages, message) {
|
|
15
|
+
if (messages.some((m) => m.id === message.id))
|
|
16
|
+
return messages;
|
|
17
|
+
// 乐观发送的用户消息:用服务端权威版本替换,而不是并排显示两条。
|
|
18
|
+
const optIdx = messages.findIndex((m) => m.role === 'user' &&
|
|
19
|
+
m.id.startsWith(OPTIMISTIC_ID_PREFIX) &&
|
|
20
|
+
m.role === message.role &&
|
|
21
|
+
m.content === message.content);
|
|
22
|
+
if (optIdx >= 0) {
|
|
23
|
+
const next = [...messages];
|
|
24
|
+
next[optIdx] = message;
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
return [...messages, message];
|
|
28
|
+
}
|
|
29
|
+
/** 把交互块的结果落到一组消息里(已提交的历史消息)。 */
|
|
30
|
+
function applyInteractionToMessages(messages, interactionId, patch) {
|
|
31
|
+
let touched = false;
|
|
32
|
+
const next = messages.map((message) => {
|
|
33
|
+
if (!message.blocks?.some((b) => b.type === 'interaction' && b.id === interactionId))
|
|
34
|
+
return message;
|
|
35
|
+
touched = true;
|
|
36
|
+
return {
|
|
37
|
+
...message,
|
|
38
|
+
blocks: message.blocks.map((block) => block.type === 'interaction' && block.id === interactionId ? { ...block, ...patch } : block),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return touched ? next : messages;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 唯一的状态归并入口。纯函数:同样的 (state, event) 永远得到同样的结果。
|
|
45
|
+
*
|
|
46
|
+
* 事件不属于本会话 → 原样返回(切会话时别的会话的事件不该污染当前状态)。
|
|
47
|
+
*/
|
|
48
|
+
export function reduceChatEvent(state, event) {
|
|
49
|
+
if (event.sessionId !== state.sessionId)
|
|
50
|
+
return state;
|
|
51
|
+
// 幂等:重放/重复投递的旧事件直接丢(阶段 2 的 Last-Event-ID 补发会真的产生重复)。
|
|
52
|
+
// 🔴 只对 **> 0** 的 seq 生效:`chat.message.pending` 固定发 seq:0、用户消息干脆不带 seq,
|
|
53
|
+
// 把它们纳入游标会在回合一开始就把 pending 丢掉(lastSeq 初值也是 0)。
|
|
54
|
+
if (typeof event.seq === 'number' && event.seq > 0) {
|
|
55
|
+
if (event.seq <= state.lastSeq)
|
|
56
|
+
return state;
|
|
57
|
+
if (state.lastSeq > 0 && event.seq > state.lastSeq + 1) {
|
|
58
|
+
state = { ...state, gapDetected: true };
|
|
59
|
+
}
|
|
60
|
+
state = { ...state, lastSeq: event.seq };
|
|
61
|
+
}
|
|
62
|
+
switch (event.type) {
|
|
63
|
+
case 'message.appended':
|
|
64
|
+
return { ...state, messages: appendMessage(state.messages, event.message) };
|
|
65
|
+
case 'turn.pending':
|
|
66
|
+
return { ...state, pending: freshPending(state.sessionId, event.turnId) };
|
|
67
|
+
case 'turn.snapshot':
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
pending: { ...freshPending(state.sessionId, event.turnId), blocks: [...event.blocks] },
|
|
71
|
+
};
|
|
72
|
+
case 'text.delta': {
|
|
73
|
+
const base = baseFor(state, event.turnId);
|
|
74
|
+
const blocks = [...base.blocks];
|
|
75
|
+
const last = blocks[blocks.length - 1];
|
|
76
|
+
if (last && last.type === 'text') {
|
|
77
|
+
blocks[blocks.length - 1] = { ...last, text: last.text + event.text };
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
blocks.push({ type: 'text', text: event.text });
|
|
81
|
+
}
|
|
82
|
+
return { ...state, pending: { ...base, blocks } };
|
|
83
|
+
}
|
|
84
|
+
case 'tool_use.started': {
|
|
85
|
+
const base = baseFor(state, event.turnId);
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
pending: {
|
|
89
|
+
...base,
|
|
90
|
+
blocks: [
|
|
91
|
+
...base.blocks,
|
|
92
|
+
{
|
|
93
|
+
type: 'tool_use',
|
|
94
|
+
id: event.id,
|
|
95
|
+
title: event.title,
|
|
96
|
+
kind: event.kind,
|
|
97
|
+
status: event.status,
|
|
98
|
+
input: event.input,
|
|
99
|
+
locations: event.locations,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case 'tool_use.updated': {
|
|
106
|
+
// 更新只作用于**已存在**的回合 —— 凭空自建一个只有 update 的回合没有意义。
|
|
107
|
+
if (!state.pending || state.pending.turnId !== event.turnId)
|
|
108
|
+
return state;
|
|
109
|
+
const pending = state.pending;
|
|
110
|
+
return {
|
|
111
|
+
...state,
|
|
112
|
+
pending: {
|
|
113
|
+
...pending,
|
|
114
|
+
blocks: pending.blocks.map((b) => b.type === 'tool_use' && b.id === event.id
|
|
115
|
+
? {
|
|
116
|
+
...b,
|
|
117
|
+
status: event.status,
|
|
118
|
+
// 首帧的 input 常为空,后续 update 才带全 —— 已有值不覆盖。
|
|
119
|
+
input: b.input ?? event.input,
|
|
120
|
+
locations: b.locations ?? event.locations,
|
|
121
|
+
}
|
|
122
|
+
: b),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
case 'interaction.required': {
|
|
127
|
+
const base = baseFor(state, event.turnId);
|
|
128
|
+
// 同一 interactionId 重复下发(重连/重放)→ 就地更新,不叠出两张表单。
|
|
129
|
+
const existing = base.blocks.findIndex((b) => b.type === 'interaction' && b.id === event.id);
|
|
130
|
+
const block = {
|
|
131
|
+
type: 'interaction',
|
|
132
|
+
id: event.id,
|
|
133
|
+
kind: event.kind,
|
|
134
|
+
title: event.title,
|
|
135
|
+
schema: event.schema,
|
|
136
|
+
...(event.respond ? { respond: event.respond } : {}),
|
|
137
|
+
revision: event.revision,
|
|
138
|
+
status: 'pending',
|
|
139
|
+
};
|
|
140
|
+
const blocks = [...base.blocks];
|
|
141
|
+
if (existing >= 0)
|
|
142
|
+
blocks[existing] = block;
|
|
143
|
+
else
|
|
144
|
+
blocks.push(block);
|
|
145
|
+
return { ...state, pending: { ...base, blocks } };
|
|
146
|
+
}
|
|
147
|
+
case 'interaction.updated': {
|
|
148
|
+
const patch = {
|
|
149
|
+
status: event.status,
|
|
150
|
+
revision: event.revision,
|
|
151
|
+
...(event.answer !== undefined ? { answer: event.answer } : {}),
|
|
152
|
+
};
|
|
153
|
+
// 🔴 在途回合也要更新:表单几乎总是在回合还没结束时被提交的。
|
|
154
|
+
// (此前只更新了已提交消息,导致在途那张表单永远停在 pending。)
|
|
155
|
+
const pending = state.pending;
|
|
156
|
+
const nextPending = pending && pending.blocks.some((b) => b.type === 'interaction' && b.id === event.interactionId)
|
|
157
|
+
? {
|
|
158
|
+
...pending,
|
|
159
|
+
blocks: pending.blocks.map((b) => b.type === 'interaction' && b.id === event.interactionId ? { ...b, ...patch } : b),
|
|
160
|
+
}
|
|
161
|
+
: pending;
|
|
162
|
+
return {
|
|
163
|
+
...state,
|
|
164
|
+
pending: nextPending,
|
|
165
|
+
messages: applyInteractionToMessages(state.messages, event.interactionId, patch),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
case 'turn.completed': {
|
|
169
|
+
const base = baseFor(state, event.turnId);
|
|
170
|
+
// 只标记终态,**不立即提交** —— 带打字机的产品要等文本追齐;不带的收到 done 立刻 commitTurn。
|
|
171
|
+
return { ...state, pending: { ...base, done: true, finalMessage: event.message } };
|
|
172
|
+
}
|
|
173
|
+
case 'resync.required':
|
|
174
|
+
// reducer 不擅自补数据 —— 只打标记,由产品去拉快照重建(和 seq 断档同一套处理)。
|
|
175
|
+
return { ...state, gapDetected: true };
|
|
176
|
+
case 'turn.failed': {
|
|
177
|
+
const matches = !!state.pending && (!event.turnId || state.pending.turnId === event.turnId);
|
|
178
|
+
return {
|
|
179
|
+
...state,
|
|
180
|
+
pending: matches ? null : state.pending,
|
|
181
|
+
messages: event.message ? appendMessage(state.messages, event.message) : state.messages,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
return state;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 把已收尾的回合提交进消息列表并清空在途态。
|
|
190
|
+
*
|
|
191
|
+
* 由**产品**决定时机:带打字机的等追齐后调(见 `./typewriter.ts` 的 `isDrained`),
|
|
192
|
+
* 不带的收到 `pending.done` 就调。未收尾时调用是安全的空操作。
|
|
193
|
+
*/
|
|
194
|
+
export function commitTurn(state) {
|
|
195
|
+
const pending = state.pending;
|
|
196
|
+
if (!pending?.done || !pending.finalMessage)
|
|
197
|
+
return state;
|
|
198
|
+
return {
|
|
199
|
+
...state,
|
|
200
|
+
messages: appendMessage(state.messages, pending.finalMessage),
|
|
201
|
+
pending: null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=reduce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reduce.js","sourceRoot":"","sources":["../../src/chat-protocol/reduce.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chat-protocol/types
|
|
3
|
+
* @description 对话流的**状态类型**——消息、块、进行中回合。
|
|
4
|
+
*
|
|
5
|
+
* 🔴 本目录(`src/chat-protocol/`)是**零依赖叶子模块**:不许 import 目录外的任何东西
|
|
6
|
+
* (包括 node 内置模块、sps 其它模块、任何 npm 包)。因为它要被打进三个浏览器 bundle
|
|
7
|
+
* (sps-console / coral-platform/web / gameforge/web),任何依赖都会被一起拖进去。
|
|
8
|
+
* 这条规则由 `chat-protocol.deps.test.ts` 卡死。
|
|
9
|
+
*
|
|
10
|
+
* @layer contract
|
|
11
|
+
* @boundedContext chat
|
|
12
|
+
*/
|
|
13
|
+
import type { InteractionRespond } from './form.js';
|
|
14
|
+
/** 消息块。与服务端持久化格式(`console/routes/chat.ts` 的 ChatMessageBlock)保持一致。 */
|
|
15
|
+
export type ChatBlock = {
|
|
16
|
+
type: 'text';
|
|
17
|
+
text: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'tool_use';
|
|
20
|
+
id: string;
|
|
21
|
+
/** ⚠️ 历史命名:这里装的是**工具名**(如 `Read` / `exec_command`),不是标题。
|
|
22
|
+
* 改名要迁移已落盘的会话文件,不值当;产品端按工具名消费即可。 */
|
|
23
|
+
title: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
status: string;
|
|
26
|
+
input?: unknown;
|
|
27
|
+
locations?: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'interaction';
|
|
30
|
+
id: string;
|
|
31
|
+
/** `approval` | `choices` | `form` | …(引擎侧 InteractionKind) */
|
|
32
|
+
kind: string;
|
|
33
|
+
title: string;
|
|
34
|
+
/**
|
|
35
|
+
* 已**完全展开**的规范 `FormSpec`(见 `./form.ts`)——产品端不需要认识任何模板名。
|
|
36
|
+
* 选择器(`kind:'choices'`)也是 FormSpec,只是字段都是 radio;产品可按 kind 决定
|
|
37
|
+
* 渲染成一键选项还是表单。旧会话里可能是 JSON Schema,用 `isFormSpec` 判别。
|
|
38
|
+
*/
|
|
39
|
+
schema: unknown;
|
|
40
|
+
/** 答案怎么送回去;缺省(旧数据)按 'endpoint' 处理。 */
|
|
41
|
+
respond?: InteractionRespond;
|
|
42
|
+
revision: number;
|
|
43
|
+
status: 'pending' | 'answered' | 'cancelled';
|
|
44
|
+
resumeAfterSequence?: number;
|
|
45
|
+
answer?: unknown;
|
|
46
|
+
};
|
|
47
|
+
/** 与服务端 `console/routes/chat.ts` 的 ChatMessage 逐字段对齐(服务端是权威)。 */
|
|
48
|
+
export interface ChatMessage {
|
|
49
|
+
id: string;
|
|
50
|
+
role: 'user' | 'assistant' | 'error';
|
|
51
|
+
content: string;
|
|
52
|
+
blocks?: ChatBlock[];
|
|
53
|
+
/** 服务端按字节上限截断过正文。 */
|
|
54
|
+
truncated?: boolean;
|
|
55
|
+
ts: string;
|
|
56
|
+
status?: 'streaming' | 'complete' | 'error';
|
|
57
|
+
/** 附件绝对路径列表(仅 user 消息有)。 */
|
|
58
|
+
attachments?: string[];
|
|
59
|
+
}
|
|
60
|
+
/** 进行中的回合。**绑 sessionId** —— 切会话时别的会话的在途回合不该被误提交。 */
|
|
61
|
+
export interface PendingTurn {
|
|
62
|
+
sessionId: string;
|
|
63
|
+
/** 服务端的 assistantId(等同 turnId)。 */
|
|
64
|
+
turnId: string;
|
|
65
|
+
blocks: ChatBlock[];
|
|
66
|
+
/** 已收到 turn.completed —— 但**尚未提交**,等产品端(打字机)追齐后调 `commitTurn`。 */
|
|
67
|
+
done: boolean;
|
|
68
|
+
finalMessage: ChatMessage | null;
|
|
69
|
+
}
|
|
70
|
+
export interface ChatStreamState {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
messages: ChatMessage[];
|
|
73
|
+
pending: PendingTurn | null;
|
|
74
|
+
/** 最后处理的事件序号(用于幂等丢重复,见 reduce)。 */
|
|
75
|
+
lastSeq: number;
|
|
76
|
+
/** 检测到 seq 断档 —— 产品端应触发一次快照/补拉对账。reducer 不自动处理。 */
|
|
77
|
+
gapDetected: boolean;
|
|
78
|
+
}
|
|
79
|
+
export declare function initialChatStreamState(sessionId: string): ChatStreamState;
|
|
80
|
+
/**
|
|
81
|
+
* 乐观发送的消息 id 前缀。收到服务端权威 user 消息时按此前缀 + 同 role/content 替换。
|
|
82
|
+
* 三端共用这个约定,别各造各的。
|
|
83
|
+
*/
|
|
84
|
+
export declare const OPTIMISTIC_ID_PREFIX = "optim-";
|
|
85
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/chat-protocol/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEpD,sEAAsE;AACtE,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX;yCACqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB,sCAAsC;IACtC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEN,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,qBAAqB;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;IAC5C,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,kEAAkE;IAClE,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,CAEzE;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function initialChatStreamState(sessionId) {
|
|
2
|
+
return { sessionId, messages: [], pending: null, lastSeq: 0, gapDetected: false };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* 乐观发送的消息 id 前缀。收到服务端权威 user 消息时按此前缀 + 同 role/content 替换。
|
|
6
|
+
* 三端共用这个约定,别各造各的。
|
|
7
|
+
*/
|
|
8
|
+
export const OPTIMISTIC_ID_PREFIX = 'optim-';
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/chat-protocol/types.ts"],"names":[],"mappings":"AAmFA,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AACpF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chat-protocol/typewriter
|
|
3
|
+
* @description 打字机效果的**可选**纯函数 helper。
|
|
4
|
+
*
|
|
5
|
+
* 为什么不放进 reducer:打字机是**展示**,不是状态语义。coral 的语音页可能想让文字跟着
|
|
6
|
+
* TTS 走,gameforge 可能压根不要动效。硬塞进 reducer 就等于强迫三端接受 console 的审美。
|
|
7
|
+
* 需要的产品调这里,不需要的直接渲染 `pending.blocks`。
|
|
8
|
+
*
|
|
9
|
+
* 状态外置成一个 `number[]`(每个块已显示的字符数),因为 reducer 的状态必须保持可比较
|
|
10
|
+
* (「同一串事件 → 深度相等」的验收标准不该被动效进度污染)。
|
|
11
|
+
*
|
|
12
|
+
* 零依赖规则见 `./types.ts` 头注释。
|
|
13
|
+
* @layer contract
|
|
14
|
+
* @boundedContext chat
|
|
15
|
+
*/
|
|
16
|
+
import type { ChatBlock } from './types.js';
|
|
17
|
+
/** 每个块已显示的字符数(非 text 块的位置恒为 0,占位保持下标对齐)。 */
|
|
18
|
+
export type TypewriterProgress = readonly number[];
|
|
19
|
+
/** 全部文本都已显示完? */
|
|
20
|
+
export declare function isDrained(blocks: readonly ChatBlock[], progress: TypewriterProgress): boolean;
|
|
21
|
+
/** 推进一个 tick,返回新的进度数组。已追齐时返回原数组(引用不变,便于跳过重渲染)。 */
|
|
22
|
+
export declare function advanceTypewriter(blocks: readonly ChatBlock[], progress: TypewriterProgress): number[] | TypewriterProgress;
|
|
23
|
+
/** 按进度裁剪出可直接渲染的块(text 块只保留已显示部分,其余原样)。 */
|
|
24
|
+
export declare function renderBlocks(blocks: readonly ChatBlock[], progress: TypewriterProgress): ChatBlock[];
|
|
25
|
+
//# sourceMappingURL=typewriter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typewriter.d.ts","sourceRoot":"","sources":["../../src/chat-protocol/typewriter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,4CAA4C;AAC5C,MAAM,MAAM,kBAAkB,GAAG,SAAS,MAAM,EAAE,CAAC;AAenD,iBAAiB;AACjB,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAE7F;AAED,kDAAkD;AAClD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,EAAE,GAAG,kBAAkB,CAc/B;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,QAAQ,EAAE,kBAAkB,GAC3B,SAAS,EAAE,CAIb"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** 一次 tick 最多推进整体差额的 1/40(与 console 既有手感一致),至少 1 个字符。 */
|
|
2
|
+
const TICK_DIVISOR = 40;
|
|
3
|
+
function gapOf(block, shown) {
|
|
4
|
+
return block.type === 'text' ? Math.max(0, block.text.length - shown) : 0;
|
|
5
|
+
}
|
|
6
|
+
/** 块数变化时补齐/裁剪进度数组。 */
|
|
7
|
+
function align(blocks, progress) {
|
|
8
|
+
const next = blocks.map((_, i) => progress[i] ?? 0);
|
|
9
|
+
return next;
|
|
10
|
+
}
|
|
11
|
+
/** 全部文本都已显示完? */
|
|
12
|
+
export function isDrained(blocks, progress) {
|
|
13
|
+
return blocks.every((b, i) => gapOf(b, progress[i] ?? 0) === 0);
|
|
14
|
+
}
|
|
15
|
+
/** 推进一个 tick,返回新的进度数组。已追齐时返回原数组(引用不变,便于跳过重渲染)。 */
|
|
16
|
+
export function advanceTypewriter(blocks, progress) {
|
|
17
|
+
const aligned = align(blocks, progress);
|
|
18
|
+
const totalGap = blocks.reduce((sum, b, i) => sum + gapOf(b, aligned[i] ?? 0), 0);
|
|
19
|
+
if (totalGap <= 0)
|
|
20
|
+
return progress.length === aligned.length ? progress : aligned;
|
|
21
|
+
let remaining = Math.max(1, Math.ceil(totalGap / TICK_DIVISOR));
|
|
22
|
+
return blocks.map((b, i) => {
|
|
23
|
+
const shown = aligned[i] ?? 0;
|
|
24
|
+
const gap = gapOf(b, shown);
|
|
25
|
+
if (gap <= 0 || remaining <= 0)
|
|
26
|
+
return shown;
|
|
27
|
+
const step = Math.min(gap, remaining);
|
|
28
|
+
remaining -= step;
|
|
29
|
+
return shown + step;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** 按进度裁剪出可直接渲染的块(text 块只保留已显示部分,其余原样)。 */
|
|
33
|
+
export function renderBlocks(blocks, progress) {
|
|
34
|
+
return blocks.map((b, i) => b.type === 'text' ? { ...b, text: b.text.slice(0, progress[i] ?? 0) } : b);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=typewriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typewriter.js","sourceRoot":"","sources":["../../src/chat-protocol/typewriter.ts"],"names":[],"mappings":"AAoBA,yDAAyD;AACzD,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB,SAAS,KAAK,CAAC,KAAgB,EAAE,KAAa;IAC5C,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,sBAAsB;AACtB,SAAS,KAAK,CAAC,MAA4B,EAAE,QAA4B;IACvE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,SAAS,CAAC,MAA4B,EAAE,QAA4B;IAClF,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,iBAAiB,CAC/B,MAA4B,EAC5B,QAA4B;IAE5B,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClF,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;IAElF,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC;IAChE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC5B,IAAI,GAAG,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACtC,SAAS,IAAI,IAAI,CAAC;QAClB,OAAO,KAAK,GAAG,IAAI,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,YAAY,CAC1B,MAA4B,EAC5B,QAA4B;IAE5B,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACzB,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAC1E,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module console/chatEventLog
|
|
3
|
+
* @description 每会话的**有限事件日志**,支撑 SSE 的 `id:` + `Last-Event-ID` 逐事件续传。
|
|
4
|
+
*
|
|
5
|
+
* 在此之前,chat 的 SSE 断线只能靠 snapshot / complete / 消息补拉**最终收敛**——中间那些
|
|
6
|
+
* 增量是真丢了,表现为"刷新后少一段文字"。有了游标之后,重连能从断点精确补发;
|
|
7
|
+
* 只有当断点已经滚出窗口时,才退回快照重建(`chat.resync.required`)。
|
|
8
|
+
*
|
|
9
|
+
* 边界(有意为之):
|
|
10
|
+
* - **进程内**。多实例部署要换成 Redis Streams / 持久事件表,那是阶段 4。
|
|
11
|
+
* - 只保 `MAX_FRAMES_PER_SESSION` 条 / `MAX_SESSIONS` 个会话,超出即淘汰——它是**传输层
|
|
12
|
+
* 补偿**,不是审计日志;权威数据始终是持久化的消息。
|
|
13
|
+
*
|
|
14
|
+
* @layer delivery
|
|
15
|
+
* @boundedContext chat
|
|
16
|
+
*/
|
|
17
|
+
export interface ChatFrame {
|
|
18
|
+
/** 会话内单调递增,SSE 的 `id:` 就是它。 */
|
|
19
|
+
id: number;
|
|
20
|
+
event: string;
|
|
21
|
+
data: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** 记一帧,返回分配到的帧号。 */
|
|
24
|
+
export declare function appendFrame(sessionId: string, event: string, data: unknown): number;
|
|
25
|
+
export type ReplayResult =
|
|
26
|
+
/** 断点还在窗口内:按序补发这些帧即可。 */
|
|
27
|
+
{
|
|
28
|
+
kind: 'frames';
|
|
29
|
+
frames: ChatFrame[];
|
|
30
|
+
}
|
|
31
|
+
/** 断点已滚出窗口:客户端必须靠快照/补拉重建,不能装作无缝续上。 */
|
|
32
|
+
| {
|
|
33
|
+
kind: 'resync';
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 取 `lastEventId` 之后的帧。
|
|
37
|
+
*
|
|
38
|
+
* 判定"滚出窗口"要**严格**:窗口里最老的帧号若已经大于 `lastEventId + 1`,说明中间那段
|
|
39
|
+
* 被淘汰了 —— 这时候宁可让客户端重建,也不能补发一段有洞的序列(有洞比没有更危险,
|
|
40
|
+
* 客户端会以为自己是完整的)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function framesSince(sessionId: string, lastEventId: number): ReplayResult;
|
|
43
|
+
/** 会话删除时清掉,别把日志留成孤儿。 */
|
|
44
|
+
export declare function dropSessionLog(sessionId: string): void;
|
|
45
|
+
/** 仅测试用:清空全部。 */
|
|
46
|
+
export declare function resetChatEventLog(): void;
|
|
47
|
+
//# sourceMappingURL=chatEventLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatEventLog.d.ts","sourceRoot":"","sources":["../../src/console/chatEventLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,SAAS;IACxB,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAgCD,oBAAoB;AACpB,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAQnF;AAED,MAAM,MAAM,YAAY;AACtB,yBAAyB;AACvB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE;AACzC,sCAAsC;GACpC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvB;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,YAAY,CAShF;AAED,wBAAwB;AACxB,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,iBAAiB;AACjB,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module console/chatEventLog
|
|
3
|
+
* @description 每会话的**有限事件日志**,支撑 SSE 的 `id:` + `Last-Event-ID` 逐事件续传。
|
|
4
|
+
*
|
|
5
|
+
* 在此之前,chat 的 SSE 断线只能靠 snapshot / complete / 消息补拉**最终收敛**——中间那些
|
|
6
|
+
* 增量是真丢了,表现为"刷新后少一段文字"。有了游标之后,重连能从断点精确补发;
|
|
7
|
+
* 只有当断点已经滚出窗口时,才退回快照重建(`chat.resync.required`)。
|
|
8
|
+
*
|
|
9
|
+
* 边界(有意为之):
|
|
10
|
+
* - **进程内**。多实例部署要换成 Redis Streams / 持久事件表,那是阶段 4。
|
|
11
|
+
* - 只保 `MAX_FRAMES_PER_SESSION` 条 / `MAX_SESSIONS` 个会话,超出即淘汰——它是**传输层
|
|
12
|
+
* 补偿**,不是审计日志;权威数据始终是持久化的消息。
|
|
13
|
+
*
|
|
14
|
+
* @layer delivery
|
|
15
|
+
* @boundedContext chat
|
|
16
|
+
*/
|
|
17
|
+
/** 单会话保留的帧数。一个长回合的增量大致在几百条量级,512 够覆盖常见的断线窗口。 */
|
|
18
|
+
const MAX_FRAMES_PER_SESSION = 512;
|
|
19
|
+
/** 同时保留日志的会话数上限(按最近活跃淘汰),防止长跑进程无界增长。 */
|
|
20
|
+
const MAX_SESSIONS = 200;
|
|
21
|
+
const logs = new Map();
|
|
22
|
+
function touch(sessionId) {
|
|
23
|
+
const existing = logs.get(sessionId);
|
|
24
|
+
if (existing) {
|
|
25
|
+
// Map 保持插入序:删了再塞 = 提到最近位置,便于按最久未用淘汰。
|
|
26
|
+
logs.delete(sessionId);
|
|
27
|
+
logs.set(sessionId, existing);
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const created = { next: 1, frames: [] };
|
|
31
|
+
logs.set(sessionId, created);
|
|
32
|
+
while (logs.size > MAX_SESSIONS) {
|
|
33
|
+
const oldest = logs.keys().next().value;
|
|
34
|
+
if (oldest === undefined)
|
|
35
|
+
break;
|
|
36
|
+
logs.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
return created;
|
|
39
|
+
}
|
|
40
|
+
/** 记一帧,返回分配到的帧号。 */
|
|
41
|
+
export function appendFrame(sessionId, event, data) {
|
|
42
|
+
const log = touch(sessionId);
|
|
43
|
+
const id = log.next++;
|
|
44
|
+
log.frames.push({ id, event, data });
|
|
45
|
+
if (log.frames.length > MAX_FRAMES_PER_SESSION) {
|
|
46
|
+
log.frames.splice(0, log.frames.length - MAX_FRAMES_PER_SESSION);
|
|
47
|
+
}
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 取 `lastEventId` 之后的帧。
|
|
52
|
+
*
|
|
53
|
+
* 判定"滚出窗口"要**严格**:窗口里最老的帧号若已经大于 `lastEventId + 1`,说明中间那段
|
|
54
|
+
* 被淘汰了 —— 这时候宁可让客户端重建,也不能补发一段有洞的序列(有洞比没有更危险,
|
|
55
|
+
* 客户端会以为自己是完整的)。
|
|
56
|
+
*/
|
|
57
|
+
export function framesSince(sessionId, lastEventId) {
|
|
58
|
+
const log = logs.get(sessionId);
|
|
59
|
+
if (!log)
|
|
60
|
+
return { kind: 'resync' };
|
|
61
|
+
// 客户端游标比服务端还新(会话重建/进程重启后帧号从 1 重来)→ 重建。
|
|
62
|
+
if (lastEventId >= log.next)
|
|
63
|
+
return { kind: 'resync' };
|
|
64
|
+
const frames = log.frames.filter((f) => f.id > lastEventId);
|
|
65
|
+
if (frames.length === 0)
|
|
66
|
+
return { kind: 'frames', frames: [] };
|
|
67
|
+
if (frames[0].id > lastEventId + 1)
|
|
68
|
+
return { kind: 'resync' };
|
|
69
|
+
return { kind: 'frames', frames };
|
|
70
|
+
}
|
|
71
|
+
/** 会话删除时清掉,别把日志留成孤儿。 */
|
|
72
|
+
export function dropSessionLog(sessionId) {
|
|
73
|
+
logs.delete(sessionId);
|
|
74
|
+
}
|
|
75
|
+
/** 仅测试用:清空全部。 */
|
|
76
|
+
export function resetChatEventLog() {
|
|
77
|
+
logs.clear();
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=chatEventLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatEventLog.js","sourceRoot":"","sources":["../../src/console/chatEventLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,gDAAgD;AAChD,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,wCAAwC;AACxC,MAAM,YAAY,GAAG,GAAG,CAAC;AAOzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE3C,SAAS,KAAK,CAAC,SAAiB;IAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACrC,IAAI,QAAQ,EAAE,CAAC;QACb,qCAAqC;QACrC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC9B,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,OAAO,GAAe,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACpD,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,IAAI,CAAC,IAAI,GAAG,YAAY,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QACxC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM;QAChC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,oBAAoB;AACpB,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,KAAa,EAAE,IAAa;IACzE,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACtB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,sBAAsB,EAAE,CAAC;QAC/C,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,sBAAsB,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAQD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,WAAmB;IAChE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACpC,uCAAuC;IACvC,IAAI,WAAW,IAAI,GAAG,CAAC,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACvD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC;IAC5D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC/D,IAAI,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,GAAG,WAAW,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC/D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC;AAED,wBAAwB;AACxB,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACzB,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - 干掉原 console/sse/eventBus.ts(所有 chat-specific publish 挪到 chatBus 里)
|
|
12
12
|
*/
|
|
13
13
|
import { Hono } from 'hono';
|
|
14
|
+
import type { InteractionRespond } from '../../chat-protocol/index.js';
|
|
14
15
|
import type { Logger } from '../../core/logger.js';
|
|
15
16
|
import { DaemonClient } from '../../daemon/daemonClient.js';
|
|
16
17
|
import type { ChatService } from '../../services/ChatService.js';
|
|
@@ -34,6 +35,8 @@ export type ChatMessageBlock = {
|
|
|
34
35
|
type: 'interaction';
|
|
35
36
|
id: string;
|
|
36
37
|
kind: string;
|
|
38
|
+
/** 答案怎么送回去(见 chat-protocol/form 的 InteractionRespond);旧数据缺省按 endpoint。 */
|
|
39
|
+
respond?: InteractionRespond;
|
|
37
40
|
title: string;
|
|
38
41
|
schema: unknown;
|
|
39
42
|
revision: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/console/routes/chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/console/routes/chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAY,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAMjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAO5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAuBjE,KAAK,gBAAgB,GAAG,IAAI,CAC1B,YAAY,EACV,eAAe,GACf,UAAU,GACV,SAAS,GACT,aAAa,GACb,uBAAuB,GACvB,UAAU,GACV,mBAAmB,GACnB,mBAAmB,CACtB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,kBAAkB,CAAC,EAAE,MAAM,gBAAgB,CAAC;CAC7C;AAmED,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GACnH;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;IAC5C,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAoBD,6CAA6C;AAC7C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AACtD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ;AAoFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAGpD;AAmnCD,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,YAAY,GAAE,qBAA0B,GACvC,IAAI,CA+kBN;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CA+I5C"}
|