@coralai/sps-cli 0.65.34 → 0.65.36
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/formValues.d.ts +60 -0
- package/dist/chat-protocol/formValues.d.ts.map +1 -0
- package/dist/chat-protocol/formValues.js +114 -0
- package/dist/chat-protocol/formValues.js.map +1 -0
- package/dist/chat-protocol/index.d.ts +4 -0
- package/dist/chat-protocol/index.d.ts.map +1 -1
- package/dist/chat-protocol/index.js +2 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/selectors.d.ts +31 -0
- package/dist/chat-protocol/selectors.d.ts.map +1 -0
- package/dist/chat-protocol/selectors.js +40 -0
- package/dist/chat-protocol/selectors.js.map +1 -0
- package/dist/console/routes/chat.d.ts.map +1 -1
- package/dist/console/routes/chat.js +55 -40
- package/dist/console/routes/chat.js.map +1 -1
- package/dist/console-assets/assets/{ShowcasePage-x-qRvSx_.js → ShowcasePage-Cglj0rIc.js} +1 -1
- package/dist/console-assets/assets/index-JQXV8MkM.js +845 -0
- package/dist/console-assets/assets/index-Z_nKYXPL.css +1 -0
- package/dist/console-assets/index.html +2 -2
- package/dist/openai-agent/interactions/broker.d.ts +13 -10
- package/dist/openai-agent/interactions/broker.d.ts.map +1 -1
- package/dist/openai-agent/interactions/broker.js +24 -91
- package/dist/openai-agent/interactions/broker.js.map +1 -1
- package/dist/openai-agent/runWorker.d.ts.map +1 -1
- package/dist/openai-agent/runWorker.js +0 -6
- package/dist/openai-agent/runWorker.js.map +1 -1
- package/dist/providers/mcp/spsMcpServer.d.ts.map +1 -1
- package/dist/providers/mcp/spsMcpServer.js +6 -4
- package/dist/providers/mcp/spsMcpServer.js.map +1 -1
- package/package.json +1 -1
- package/dist/console-assets/assets/index-Ifjq4orx.js +0 -845
- package/dist/console-assets/assets/index-cxN7rD-p.css +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chat-protocol/formValues
|
|
3
|
+
* @description `FormSpec` 的**取值语义**:初值、显隐、校验、答案组装。纯函数,不碰 UI。
|
|
4
|
+
*
|
|
5
|
+
* 🔴 为什么这层必须共享:光给产品一份 `FormSpec` 还不够 —— 从 spec 到最终提交的 JSON,
|
|
6
|
+
* 中间这些规则各写各的一定会散,而散掉的后果**不是难看,是数据形状不一致**:
|
|
7
|
+
* 同一张表单,A 端交上来 `{count: "3"}`、B 端 `{count: 3}`、C 端漏掉被隐藏的字段,
|
|
8
|
+
* **模型收到三种不同的 JSON**。这是正确性问题。
|
|
9
|
+
*
|
|
10
|
+
* 而且这个分叉在本仓已经发生过:`ConfigFormPanel` 有一份按类型定初值的实现,
|
|
11
|
+
* `MessageBlocks` 另有一份做数字转换的实现,规则并不完全一致。一个仓两份,
|
|
12
|
+
* 再接两个平台就是四份 —— 和生图模板当初一模一样的剧本。
|
|
13
|
+
*
|
|
14
|
+
* 产品端拿到这四个函数后,真正要写的只剩控件的样子和事件绑定。
|
|
15
|
+
*
|
|
16
|
+
* 零依赖规则见 `./types.ts` 头注释。
|
|
17
|
+
* @layer contract
|
|
18
|
+
* @boundedContext chat
|
|
19
|
+
*/
|
|
20
|
+
import type { FormFieldSpec, FormSpec } from './form.js';
|
|
21
|
+
export type FormValues = Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* 单个字段的初值。**按类型定死**,不让各端自己发挥:
|
|
24
|
+
* 多值 = `[]`、开关 = `false`、数值 = `min ?? 0`、其余 = 空串。给了 `default` 就用它。
|
|
25
|
+
*/
|
|
26
|
+
export declare function initialValue(field: FormFieldSpec): unknown;
|
|
27
|
+
/** 整张表单的初值。`preset`(只读回看已提交表单时)按字段覆盖。 */
|
|
28
|
+
export declare function initialValues(spec: FormSpec, preset?: FormValues): FormValues;
|
|
29
|
+
/**
|
|
30
|
+
* 当前可见的字段(处理 `dependsOn` 条件显隐)。
|
|
31
|
+
* 依赖的字段自己不可见时,本字段也不可见——否则会出现"看不见的必填项"卡住提交。
|
|
32
|
+
*/
|
|
33
|
+
export declare function visibleFields(spec: FormSpec, values: FormValues): FormFieldSpec[];
|
|
34
|
+
/** 一个值算不算"没填"。空串 / null / undefined / 空数组都算。 */
|
|
35
|
+
export declare function isBlank(value: unknown): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 校验,返回**缺失的必填字段**(按 spec 顺序)。
|
|
38
|
+
* 只校验可见字段:被 `dependsOn` 隐藏的必填项不该挡住提交。
|
|
39
|
+
*/
|
|
40
|
+
export declare function validate(spec: FormSpec, values: FormValues): FormFieldSpec[];
|
|
41
|
+
/**
|
|
42
|
+
* 组装最终答案。规则(定死,别各端发挥):
|
|
43
|
+
* - 只收**可见**字段——被条件隐藏的不进答案,否则模型会收到用户根本没看见的值;
|
|
44
|
+
* - 数值字段从输入框的字符串转回 `number`(转不动就原样保留,不猜);
|
|
45
|
+
* - 多值字段保证是数组;
|
|
46
|
+
* - 没填的字段**不进答案**(而不是塞空串)——"没填"和"填了空"对模型是两件事。
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildAnswer(spec: FormSpec, values: FormValues): FormValues;
|
|
49
|
+
/**
|
|
50
|
+
* 把答案渲染成**一句人话**,用作用户侧的指令文本。
|
|
51
|
+
*
|
|
52
|
+
* 🔴 为什么要共享:交互确认后内容会落进**用户气泡**——历史里读到的是"要!金块加时间 +
|
|
53
|
+
* 炸弹扣分",而不是一坨 JSON。三端各渲染各的,同一次确认在不同平台读起来就不一样,
|
|
54
|
+
* 而这是**同一段对话历史**。
|
|
55
|
+
*
|
|
56
|
+
* 规则:按 spec 顺序取可见且已填的字段,`选项标签`优先于原始值(用户看见的是标签),
|
|
57
|
+
* 多值用顿号连接,字段之间用 ` · ` 分隔。单字段(典型:选择器)不加标签前缀,直接就是那句话。
|
|
58
|
+
*/
|
|
59
|
+
export declare function renderAnswer(spec: FormSpec, answer: FormValues): string;
|
|
60
|
+
//# sourceMappingURL=formValues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formValues.d.ts","sourceRoot":"","sources":["../../src/chat-protocol/formValues.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAYjD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAM1D;AAED,yCAAyC;AACzC,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,UAAU,CAM7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CASjF;AAED,gDAAgD;AAChD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG/C;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CAE5E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CAe1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAcvE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** 多值控件(值恒为数组)。 */
|
|
2
|
+
function isMultiValue(type) {
|
|
3
|
+
return type === 'multiselect' || type === 'checkbox';
|
|
4
|
+
}
|
|
5
|
+
/** 数值控件(提交前要从输入框的字符串转回 number)。 */
|
|
6
|
+
function isNumeric(type) {
|
|
7
|
+
return type === 'number' || type === 'slider';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 单个字段的初值。**按类型定死**,不让各端自己发挥:
|
|
11
|
+
* 多值 = `[]`、开关 = `false`、数值 = `min ?? 0`、其余 = 空串。给了 `default` 就用它。
|
|
12
|
+
*/
|
|
13
|
+
export function initialValue(field) {
|
|
14
|
+
if (field.default !== undefined)
|
|
15
|
+
return field.default;
|
|
16
|
+
if (isMultiValue(field.type))
|
|
17
|
+
return [];
|
|
18
|
+
if (field.type === 'toggle')
|
|
19
|
+
return false;
|
|
20
|
+
if (isNumeric(field.type))
|
|
21
|
+
return field.min ?? 0;
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
/** 整张表单的初值。`preset`(只读回看已提交表单时)按字段覆盖。 */
|
|
25
|
+
export function initialValues(spec, preset) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const field of spec.fields) {
|
|
28
|
+
out[field.id] = preset && field.id in preset ? preset[field.id] : initialValue(field);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 当前可见的字段(处理 `dependsOn` 条件显隐)。
|
|
34
|
+
* 依赖的字段自己不可见时,本字段也不可见——否则会出现"看不见的必填项"卡住提交。
|
|
35
|
+
*/
|
|
36
|
+
export function visibleFields(spec, values) {
|
|
37
|
+
const byId = new Map(spec.fields.map((f) => [f.id, f]));
|
|
38
|
+
const visible = (field, depth) => {
|
|
39
|
+
if (!field.dependsOn || depth > 16)
|
|
40
|
+
return true;
|
|
41
|
+
const parent = byId.get(field.dependsOn.field);
|
|
42
|
+
if (!parent || !visible(parent, depth + 1))
|
|
43
|
+
return false;
|
|
44
|
+
return values[field.dependsOn.field] === field.dependsOn.equals;
|
|
45
|
+
};
|
|
46
|
+
return spec.fields.filter((f) => visible(f, 0));
|
|
47
|
+
}
|
|
48
|
+
/** 一个值算不算"没填"。空串 / null / undefined / 空数组都算。 */
|
|
49
|
+
export function isBlank(value) {
|
|
50
|
+
if (value === undefined || value === null || value === '')
|
|
51
|
+
return true;
|
|
52
|
+
return Array.isArray(value) && value.length === 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 校验,返回**缺失的必填字段**(按 spec 顺序)。
|
|
56
|
+
* 只校验可见字段:被 `dependsOn` 隐藏的必填项不该挡住提交。
|
|
57
|
+
*/
|
|
58
|
+
export function validate(spec, values) {
|
|
59
|
+
return visibleFields(spec, values).filter((f) => f.required && isBlank(values[f.id]));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 组装最终答案。规则(定死,别各端发挥):
|
|
63
|
+
* - 只收**可见**字段——被条件隐藏的不进答案,否则模型会收到用户根本没看见的值;
|
|
64
|
+
* - 数值字段从输入框的字符串转回 `number`(转不动就原样保留,不猜);
|
|
65
|
+
* - 多值字段保证是数组;
|
|
66
|
+
* - 没填的字段**不进答案**(而不是塞空串)——"没填"和"填了空"对模型是两件事。
|
|
67
|
+
*/
|
|
68
|
+
export function buildAnswer(spec, values) {
|
|
69
|
+
const answer = {};
|
|
70
|
+
for (const field of visibleFields(spec, values)) {
|
|
71
|
+
const raw = values[field.id];
|
|
72
|
+
if (isBlank(raw) && field.type !== 'toggle')
|
|
73
|
+
continue;
|
|
74
|
+
if (isNumeric(field.type) && typeof raw === 'string') {
|
|
75
|
+
const n = Number(raw);
|
|
76
|
+
answer[field.id] = Number.isFinite(n) ? n : raw;
|
|
77
|
+
}
|
|
78
|
+
else if (isMultiValue(field.type)) {
|
|
79
|
+
answer[field.id] = Array.isArray(raw) ? raw : [raw];
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
answer[field.id] = raw;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return answer;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 把答案渲染成**一句人话**,用作用户侧的指令文本。
|
|
89
|
+
*
|
|
90
|
+
* 🔴 为什么要共享:交互确认后内容会落进**用户气泡**——历史里读到的是"要!金块加时间 +
|
|
91
|
+
* 炸弹扣分",而不是一坨 JSON。三端各渲染各的,同一次确认在不同平台读起来就不一样,
|
|
92
|
+
* 而这是**同一段对话历史**。
|
|
93
|
+
*
|
|
94
|
+
* 规则:按 spec 顺序取可见且已填的字段,`选项标签`优先于原始值(用户看见的是标签),
|
|
95
|
+
* 多值用顿号连接,字段之间用 ` · ` 分隔。单字段(典型:选择器)不加标签前缀,直接就是那句话。
|
|
96
|
+
*/
|
|
97
|
+
export function renderAnswer(spec, answer) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const field of spec.fields) {
|
|
100
|
+
const raw = answer[field.id];
|
|
101
|
+
if (raw === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
const label = (v) => {
|
|
104
|
+
const hit = field.options?.find((o) => o.value === v);
|
|
105
|
+
return hit ? hit.label : String(v);
|
|
106
|
+
};
|
|
107
|
+
const text = Array.isArray(raw) ? raw.map(label).join('、') : label(raw);
|
|
108
|
+
if (!text)
|
|
109
|
+
continue;
|
|
110
|
+
parts.push(spec.fields.length === 1 ? text : `${field.label}:${text}`);
|
|
111
|
+
}
|
|
112
|
+
return parts.join(' · ');
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=formValues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formValues.js","sourceRoot":"","sources":["../../src/chat-protocol/formValues.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,10 @@ export { OPTIMISTIC_ID_PREFIX, initialChatStreamState } from './types.js';
|
|
|
15
15
|
export type { ChatBlock, ChatMessage, ChatStreamState, PendingTurn } from './types.js';
|
|
16
16
|
export { commitTurn, reduceChatEvent } from './reduce.js';
|
|
17
17
|
export { isFormSpec } from './form.js';
|
|
18
|
+
export { buildAnswer, initialValue, initialValues, isBlank, renderAnswer, validate, visibleFields, } from './formValues.js';
|
|
19
|
+
export type { FormValues } from './formValues.js';
|
|
20
|
+
export { activeInteraction, supersededInteractions } from './selectors.js';
|
|
21
|
+
export type { InteractionBlock } from './selectors.js';
|
|
18
22
|
export type { FormFieldSpec, FormFieldType, FormSpec, InteractionRespond } from './form.js';
|
|
19
23
|
export { advanceTypewriter, isDrained, renderBlocks } from './typewriter.js';
|
|
20
24
|
export type { TypewriterProgress } from './typewriter.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/chat-protocol/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC1E,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/chat-protocol/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC1E,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EACL,WAAW,EACX,YAAY,EACZ,aAAa,EACb,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -13,5 +13,7 @@ export { CHAT_PROTOCOL_VERSION, WIRE_EVENT_NAMES, fromWireEvent } from './events
|
|
|
13
13
|
export { OPTIMISTIC_ID_PREFIX, initialChatStreamState } from './types.js';
|
|
14
14
|
export { commitTurn, reduceChatEvent } from './reduce.js';
|
|
15
15
|
export { isFormSpec } from './form.js';
|
|
16
|
+
export { buildAnswer, initialValue, initialValues, isBlank, renderAnswer, validate, visibleFields, } from './formValues.js';
|
|
17
|
+
export { activeInteraction, supersededInteractions } from './selectors.js';
|
|
16
18
|
export { advanceTypewriter, isDrained, renderBlocks } from './typewriter.js';
|
|
17
19
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/chat-protocol/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE1E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/chat-protocol/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE1E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EACL,WAAW,EACX,YAAY,EACZ,aAAa,EACb,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAG3E,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chat-protocol/selectors
|
|
3
|
+
* @description 从 `ChatStreamState` 派生的只读视图。渲染成什么样是产品的事,
|
|
4
|
+
* **"当前该由用户回应哪一个交互"是状态推导**,必须共享 —— 三端各写一遍
|
|
5
|
+
* 必然分叉(尤其 superseded 规则),而分叉出来的是"两张表单都像能点"这种
|
|
6
|
+
* 真实故障,不是样式差异。
|
|
7
|
+
*
|
|
8
|
+
* 零依赖规则见 `./types.ts` 头注释。
|
|
9
|
+
* @layer contract
|
|
10
|
+
* @boundedContext chat
|
|
11
|
+
*/
|
|
12
|
+
import type { ChatBlock, ChatStreamState } from './types.js';
|
|
13
|
+
export type InteractionBlock = Extract<ChatBlock, {
|
|
14
|
+
type: 'interaction';
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* **当前该由用户回应的交互**(没有则 null)。
|
|
18
|
+
*
|
|
19
|
+
* 规则:**最新的那个**。run 挂起时等的本就是恰好一个答案;真出现多个 pending
|
|
20
|
+
* (一个回合里连弹两张),让最新的接管、更早的视为已被取代,而不是并排两张都像能点。
|
|
21
|
+
*
|
|
22
|
+
* 产品把它渲染在输入框上方的固定区:位置稳定(不会随消息流滚走)、和输入框语义一致
|
|
23
|
+
* (都是"这一轮用户怎么回应")、天然只有一个活跃项。
|
|
24
|
+
*/
|
|
25
|
+
export declare function activeInteraction(state: ChatStreamState): InteractionBlock | null;
|
|
26
|
+
/**
|
|
27
|
+
* 被取代的 pending 交互(除最新之外的)。历史里应显示成失效态,**不能仍然可点** ——
|
|
28
|
+
* 它们的答案已经没人接收了。
|
|
29
|
+
*/
|
|
30
|
+
export declare function supersededInteractions(state: ChatStreamState): InteractionBlock[];
|
|
31
|
+
//# sourceMappingURL=selectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../src/chat-protocol/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,CAAC;AAmB3E;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAGjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,EAAE,CAGjF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按出现顺序取出所有 pending 交互:先已提交消息,再在途回合。
|
|
3
|
+
* (在途回合总是最新的,所以放最后。)
|
|
4
|
+
*/
|
|
5
|
+
function pendingInteractions(state) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const message of state.messages) {
|
|
8
|
+
for (const block of message.blocks ?? []) {
|
|
9
|
+
if (block.type === 'interaction' && block.status === 'pending')
|
|
10
|
+
out.push(block);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
for (const block of state.pending?.blocks ?? []) {
|
|
14
|
+
if (block.type === 'interaction' && block.status === 'pending')
|
|
15
|
+
out.push(block);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* **当前该由用户回应的交互**(没有则 null)。
|
|
21
|
+
*
|
|
22
|
+
* 规则:**最新的那个**。run 挂起时等的本就是恰好一个答案;真出现多个 pending
|
|
23
|
+
* (一个回合里连弹两张),让最新的接管、更早的视为已被取代,而不是并排两张都像能点。
|
|
24
|
+
*
|
|
25
|
+
* 产品把它渲染在输入框上方的固定区:位置稳定(不会随消息流滚走)、和输入框语义一致
|
|
26
|
+
* (都是"这一轮用户怎么回应")、天然只有一个活跃项。
|
|
27
|
+
*/
|
|
28
|
+
export function activeInteraction(state) {
|
|
29
|
+
const all = pendingInteractions(state);
|
|
30
|
+
return all.length > 0 ? all[all.length - 1] : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 被取代的 pending 交互(除最新之外的)。历史里应显示成失效态,**不能仍然可点** ——
|
|
34
|
+
* 它们的答案已经没人接收了。
|
|
35
|
+
*/
|
|
36
|
+
export function supersededInteractions(state) {
|
|
37
|
+
const all = pendingInteractions(state);
|
|
38
|
+
return all.slice(0, -1);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=selectors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.js","sourceRoot":"","sources":["../../src/chat-protocol/selectors.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,SAAS,mBAAmB,CAAC,KAAsB;IACjD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;gBAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,CAAC;QAChD,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAsB;IACtD,MAAM,GAAG,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACvC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAsB;IAC3D,MAAM,GAAG,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACvC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -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;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;
|
|
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;AAunCD,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,YAAY,GAAE,qBAA0B,GACvC,IAAI,CA+lBN;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CA+I5C"}
|
|
@@ -244,18 +244,22 @@ function appendInteractionBlock(sessionId, assistantId, blocks, event) {
|
|
|
244
244
|
* 前端按非终态渲染成转圈 —— 用户填表那几十秒里它一直在转,看着像 agent 还在想;
|
|
245
245
|
* - 恢复后该块落成 `interrupted`,前端显示【已中断】,而这其实是**正常**的挂起-恢复。
|
|
246
246
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
247
|
+
* **两条后端路径现在走同一条**:收集输入类交互(表单/选择器)在两边都是"工具即刻返回、
|
|
248
|
+
* 回合收尾、用户确认后那句指令作为新的用户消息进来"。所以这里统一把 tool_use **转成**
|
|
249
|
+
* 交互块,答案走 `respond:'message'`。
|
|
250
|
+
*
|
|
251
|
+
* (openai 那边曾经把 run 挂起等答案 —— 那是单轮进程在假装自己有状态,比暖进程更脆
|
|
252
|
+
* 却拿不到任何好处;已拆除,见 openai-agent/interactions/broker.ts 的 tools()。)
|
|
253
|
+
*
|
|
254
|
+
* ⚠️ 审批 / 对账不走这里:它们是**执行门槛**,必须真的把 run 卡住,仍由 worker 发权威的
|
|
255
|
+
* `interaction` 事件、答案走交互端点恢复。
|
|
252
256
|
*/
|
|
253
|
-
function createInteractionToolSink(sessionId, assistantId, blocks
|
|
257
|
+
function createInteractionToolSink(sessionId, assistantId, blocks) {
|
|
254
258
|
/** toolCallId → 工具名;首帧参数常为空,要等 tool_update 才凑得出 spec。 */
|
|
255
259
|
const tracked = new Map();
|
|
256
260
|
const emitted = new Set();
|
|
257
261
|
const tryEmit = (id, title, input) => {
|
|
258
|
-
if (
|
|
262
|
+
if (emitted.has(id))
|
|
259
263
|
return;
|
|
260
264
|
const spec = toInteractionSpec(title, input);
|
|
261
265
|
if (!spec)
|
|
@@ -656,7 +660,7 @@ async function streamAssistantResponse(sessionId, assistantId, userContent, log,
|
|
|
656
660
|
const subscription = client.subscribeRunResilient(slot);
|
|
657
661
|
// 复用 POST 时 startTurn 预登记的进行中回合(blocks 引用,后续就地追加);未预登记则退化自建(防御)。
|
|
658
662
|
const blocks = activeStreams.get(sessionId)?.blocks ?? startTurn(sessionId, assistantId);
|
|
659
|
-
const interactionSink = createInteractionToolSink(sessionId, assistantId, blocks
|
|
663
|
+
const interactionSink = createInteractionToolSink(sessionId, assistantId, blocks);
|
|
660
664
|
let accumulatedText = '';
|
|
661
665
|
let truncated = false;
|
|
662
666
|
const consume = (async () => {
|
|
@@ -921,8 +925,9 @@ function openaiResumeEnsureArgs(sessionId, session) {
|
|
|
921
925
|
async function streamInteractionResponse(sessionId, assistantId, client, slot, afterSequence, log) {
|
|
922
926
|
const blocks = activeStreams.get(sessionId)?.blocks ??
|
|
923
927
|
startTurn(sessionId, assistantId);
|
|
924
|
-
// 交互恢复路只发生在 openai(durable interactions)
|
|
925
|
-
|
|
928
|
+
// 交互恢复路只发生在 openai(durable interactions:审批/对账)。恢复后的回合里若再弹
|
|
929
|
+
// 表单/选择器,同样收成交互块(与首轮同一条规则)。
|
|
930
|
+
const interactionSink = createInteractionToolSink(sessionId, assistantId, blocks);
|
|
926
931
|
const subscription = client.subscribeRunResilient(slot, afterSequence);
|
|
927
932
|
let accumulatedText = '';
|
|
928
933
|
let truncated = false;
|
|
@@ -1609,37 +1614,47 @@ export function createChatRoute(log, chat, dependencies = {}) {
|
|
|
1609
1614
|
detail: `At most ${MAX_CONCURRENT_SESSIONS} chat sessions can be generating at once — wait for one to finish or interrupt it.`,
|
|
1610
1615
|
}, 429);
|
|
1611
1616
|
}
|
|
1612
|
-
//
|
|
1613
|
-
// ·
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
status: 'cancelled',
|
|
1640
|
-
revision: block.revision,
|
|
1641
|
-
});
|
|
1617
|
+
// 有挂起交互时,一条新用户消息意味着什么?**按块上的 respond 判,不看后端**:
|
|
1618
|
+
// · respond='message'(表单/选择器,两条后端路都是它)→ 这条消息就是它的答案通道。
|
|
1619
|
+
// 命中 body.interaction.id 的落 answered(是用户点了「提交」,答案存下来供只读回看),
|
|
1620
|
+
// 其余 pending 的落 cancelled(用户直接打字绕过了它们)。两者都不再假活。
|
|
1621
|
+
// 🔴 不落定 = 前端的 activeInteraction 永远指着它,固定区表单提交后不消失。
|
|
1622
|
+
// · respond='endpoint'(审批 / 对账)→ 安全门槛不许打字绕过,409 让用户先作答。
|
|
1623
|
+
// (openai 路 form/choices 的老数据也是 endpoint,worker 侧有"以对话作答"通道
|
|
1624
|
+
// 会同步取消 store 记录,见 broker.resolveWithChatReply,所以这里照旧落 cancelled。)
|
|
1625
|
+
const answeredId = body.interaction && typeof body.interaction.id === 'string'
|
|
1626
|
+
? body.interaction.id
|
|
1627
|
+
: null;
|
|
1628
|
+
for (const msg of session.messages) {
|
|
1629
|
+
for (const block of msg.blocks ?? []) {
|
|
1630
|
+
if (block.type !== 'interaction' || block.status !== 'pending')
|
|
1631
|
+
continue;
|
|
1632
|
+
// 🔴 对账交互的 kind 是 'form'(runWorker reconcile_* 卡),不能按 kind 判安全性——
|
|
1633
|
+
// 用 interactionId 前缀识别。approval(危险操作)与对账都是安全门槛。
|
|
1634
|
+
const isReconcile = block.id.startsWith('reconcile_');
|
|
1635
|
+
if (block.kind === 'approval' || isReconcile) {
|
|
1636
|
+
return c.json({
|
|
1637
|
+
type: 'conflict',
|
|
1638
|
+
title: 'approval pending',
|
|
1639
|
+
status: 409,
|
|
1640
|
+
detail: isReconcile
|
|
1641
|
+
? '有一笔操作结果待确认(对账卡片)——请先在卡片上确认,再继续对话。'
|
|
1642
|
+
: '有待审批的操作(卡片)——请先批准或拒绝,再继续对话。',
|
|
1643
|
+
}, 409);
|
|
1642
1644
|
}
|
|
1645
|
+
const answered = answeredId !== null && block.id === answeredId;
|
|
1646
|
+
block.status = answered ? 'answered' : 'cancelled';
|
|
1647
|
+
block.revision += 1;
|
|
1648
|
+
if (answered && body.interaction?.answer !== undefined) {
|
|
1649
|
+
block.answer = body.interaction.answer;
|
|
1650
|
+
}
|
|
1651
|
+
publish('chat.message.interaction.updated', {
|
|
1652
|
+
sessionId: id,
|
|
1653
|
+
interactionId: block.id,
|
|
1654
|
+
status: block.status,
|
|
1655
|
+
revision: block.revision,
|
|
1656
|
+
...(answered && block.answer !== undefined ? { answer: block.answer } : {}),
|
|
1657
|
+
});
|
|
1643
1658
|
}
|
|
1644
1659
|
}
|
|
1645
1660
|
const userMsg = {
|