@roaming-ai/dsh-group-chat 0.2.1 → 0.3.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 +3 -2
- package/lib/client.js +986 -298
- package/lib/client.js.map +1 -1
- package/lib/index.js +524 -52
- package/lib/types/client/components/Bubble.d.ts +6 -3
- package/lib/types/client/components/ChatPanel.d.ts +1 -0
- package/lib/types/client/components/ConstraintList.d.ts +11 -0
- package/lib/types/client/components/FailCard.d.ts +9 -0
- package/lib/types/client/components/Fold.d.ts +19 -0
- package/lib/types/client/components/HoverTip.d.ts +22 -0
- package/lib/types/client/components/MessageFlow.d.ts +1 -0
- package/lib/types/client/components/MsgActions.d.ts +14 -0
- package/lib/types/client/hooks/useComposer.d.ts +6 -1
- package/lib/types/client/lib/composer-draft.d.ts +17 -0
- package/lib/types/core/constraints.d.ts +61 -0
- package/lib/types/core/errors.d.ts +54 -0
- package/lib/types/core/types.d.ts +22 -0
- package/lib/types/host/api/actions.d.ts +1 -1
- package/lib/types/host/engine/conversation.d.ts +5 -3
- package/lib/types/host/engine/fold.d.ts +17 -0
- package/lib/types/host/engine/index.d.ts +1 -0
- package/lib/types/host/engine/retitle.d.ts +5 -5
- package/lib/types/host/service.d.ts +1 -1
- package/lib/types/host/state.d.ts +2 -0
- package/lib/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/GroupChatPanel.tsx +18 -3
- package/src/client/components/AsidePanel.tsx +10 -8
- package/src/client/components/Bubble.tsx +45 -18
- package/src/client/components/ChatPanel.tsx +17 -12
- package/src/client/components/Composer.tsx +26 -20
- package/src/client/components/ConstraintList.tsx +69 -0
- package/src/client/components/FailCard.tsx +49 -0
- package/src/client/components/Fold.tsx +72 -0
- package/src/client/components/HoverTip.tsx +134 -0
- package/src/client/components/MessageFlow.tsx +77 -54
- package/src/client/components/MsgActions.tsx +85 -0
- package/src/client/components/NavPanel.tsx +6 -1
- package/src/client/components/ThinkRow.tsx +7 -3
- package/src/client/components/ToolRow.tsx +7 -3
- package/src/client/hooks/useComposer.ts +40 -3
- package/src/client/hooks/useGroupChatState.ts +5 -4
- package/src/client/lib/composer-draft.ts +47 -0
- package/src/client/lib/styles.ts +74 -13
- package/src/client/react-dom-shim.d.ts +2 -0
- package/src/core/constraints.ts +210 -0
- package/src/core/errors.ts +184 -0
- package/src/core/json.ts +1 -0
- package/src/core/types.ts +28 -3
- package/src/host/api/actions.ts +35 -1
- package/src/host/broadcast.ts +3 -3
- package/src/host/engine/conversation.ts +103 -38
- package/src/host/engine/fold.ts +114 -0
- package/src/host/engine/index.ts +1 -0
- package/src/host/engine/retitle.ts +16 -11
- package/src/host/persistence/persistence.ts +18 -1
- package/src/host/service.ts +1 -1
- package/src/host/state.ts +5 -4
- package/src/index.ts +2 -0
package/lib/index.js
CHANGED
|
@@ -46,6 +46,71 @@ function mountOnce(packageName, fn) {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
//#endregion
|
|
49
|
+
//#region src/core/errors.ts
|
|
50
|
+
const PREFIX = "模型输出异常终止: ";
|
|
51
|
+
const LEGACY_ROLE = /^角色「([^」]+)」发言失败[::]\s*/;
|
|
52
|
+
/** 剥旧系统胶囊与引擎包装前缀,保留供应商原文。 */
|
|
53
|
+
function unwrapSpeakFailure(raw) {
|
|
54
|
+
const text = String(raw || "").trim();
|
|
55
|
+
const legacy = parseLegacyRoleFailure(text);
|
|
56
|
+
const body = legacy ? legacy.rest : text;
|
|
57
|
+
return body.startsWith(PREFIX) ? body.slice(10).trim() : body;
|
|
58
|
+
}
|
|
59
|
+
/** 旧系统胶囊文案:角色「名」发言失败:原文。对不上则 null。 */
|
|
60
|
+
function parseLegacyRoleFailure(text) {
|
|
61
|
+
const m = LEGACY_ROLE.exec(String(text || ""));
|
|
62
|
+
if (!m) return null;
|
|
63
|
+
return {
|
|
64
|
+
roleName: m[1],
|
|
65
|
+
rest: String(text).slice(m[0].length)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** 发言失败卡:error 标记,或旧系统胶囊文案。 */
|
|
69
|
+
function isSpeakFailure(m) {
|
|
70
|
+
return !!m.error || !!parseLegacyRoleFailure(m.text);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 把旧系统失败行挂到对应角色(恰好一名命中才迁)。
|
|
74
|
+
* 返回是否改写了记录。
|
|
75
|
+
*/
|
|
76
|
+
function repairFailedMessage(m, roles) {
|
|
77
|
+
if (!isSpeakFailure(m)) return false;
|
|
78
|
+
let changed = false;
|
|
79
|
+
if (!m.error) {
|
|
80
|
+
m.error = true;
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
if (!m.failedRoleId && m.speaker !== "user" && m.speaker !== "system") {
|
|
84
|
+
m.failedRoleId = m.speaker;
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
if (!m.failedRoleId && m.speaker === "system") {
|
|
88
|
+
const parsed = parseLegacyRoleFailure(m.text);
|
|
89
|
+
if (parsed) {
|
|
90
|
+
const hits = roles.filter((r) => r.name === parsed.roleName);
|
|
91
|
+
if (hits.length === 1) {
|
|
92
|
+
const role = hits[0];
|
|
93
|
+
m.speaker = role.id;
|
|
94
|
+
m.failedRoleId = role.id;
|
|
95
|
+
m.text = unwrapSpeakFailure(parsed.rest);
|
|
96
|
+
if (!m.model && (role.provider || role.model)) m.model = (role.provider || "") + " / " + (role.model || "");
|
|
97
|
+
changed = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (m.failedRoleId && m.speaker === "system") {
|
|
101
|
+
m.speaker = m.failedRoleId;
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
if (m.failedRoleId || m.speaker !== "user" && m.speaker !== "system") {
|
|
105
|
+
const unwrapped = unwrapSpeakFailure(m.text);
|
|
106
|
+
if (unwrapped !== m.text) {
|
|
107
|
+
m.text = unwrapped;
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return changed;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
49
114
|
//#region src/core/types.ts
|
|
50
115
|
/** 全部合法档位(展示顺序)。 */
|
|
51
116
|
const PERMISSION_TIERS = [
|
|
@@ -67,6 +132,16 @@ function migrateTier(permissionTier, allowCommands) {
|
|
|
67
132
|
if (tier !== void 0) return tier;
|
|
68
133
|
return allowCommands === true ? "workspace_write" : "view_only";
|
|
69
134
|
}
|
|
135
|
+
/** 全部合法约束类型。 */
|
|
136
|
+
const CONSTRAINT_KINDS = [
|
|
137
|
+
"decided",
|
|
138
|
+
"rejected",
|
|
139
|
+
"open"
|
|
140
|
+
];
|
|
141
|
+
/** 合法 kind 原样,其余 undefined。 */
|
|
142
|
+
function asConstraintKind(value) {
|
|
143
|
+
return typeof value === "string" && CONSTRAINT_KINDS.includes(value) ? value : void 0;
|
|
144
|
+
}
|
|
70
145
|
/** 可选数值参数安全化:数字则原样,否则 undefined。 */
|
|
71
146
|
function asNumber(value) {
|
|
72
147
|
return typeof value === "number" && !Number.isNaN(value) ? value : void 0;
|
|
@@ -116,7 +191,8 @@ function createHostState(ctx) {
|
|
|
116
191
|
pendingConfirm: null,
|
|
117
192
|
confirmSignal: null,
|
|
118
193
|
childProc: null,
|
|
119
|
-
finished: null
|
|
194
|
+
finished: null,
|
|
195
|
+
replaceMessageId: null
|
|
120
196
|
},
|
|
121
197
|
store: null,
|
|
122
198
|
revision: 1,
|
|
@@ -124,12 +200,10 @@ function createHostState(ctx) {
|
|
|
124
200
|
nid: (p) => p + "-" + core.idSeq++,
|
|
125
201
|
lastCreated: null,
|
|
126
202
|
newSession: (groupId, name) => {
|
|
127
|
-
let n = 0;
|
|
128
|
-
for (const s of core.sessions.values()) if (s.groupId === groupId) n++;
|
|
129
203
|
const s = {
|
|
130
204
|
id: randomUUID(),
|
|
131
205
|
groupId,
|
|
132
|
-
name: name || "
|
|
206
|
+
name: name || "新会话",
|
|
133
207
|
topic: "",
|
|
134
208
|
messageIds: [],
|
|
135
209
|
createdAt: Date.now()
|
|
@@ -144,7 +218,7 @@ function createHostState(ctx) {
|
|
|
144
218
|
//#region src/host/api/actions.ts
|
|
145
219
|
/**
|
|
146
220
|
* 动作分发(handleAction,POST /api/group-chat/action 的载荷):
|
|
147
|
-
* mutate(12 种 CRUD/配置操作)| send | stop | confirmCommand | models | efforts。
|
|
221
|
+
* mutate(12 种 CRUD/配置操作)| send | retrySpeak | stop | confirmCommand | models | efforts。
|
|
148
222
|
* @module dsh-group-chat/host/api/actions
|
|
149
223
|
*/
|
|
150
224
|
/** 创建动作分发面。 */
|
|
@@ -382,6 +456,8 @@ function createActions(core, deps) {
|
|
|
382
456
|
};
|
|
383
457
|
for (const mid of sess.messageIds) messages.delete(mid);
|
|
384
458
|
sess.messageIds = [];
|
|
459
|
+
sess.constraints = void 0;
|
|
460
|
+
sess.constraintsUpToSeq = void 0;
|
|
385
461
|
schedulePersist({ session: sess.id });
|
|
386
462
|
touch();
|
|
387
463
|
}
|
|
@@ -426,10 +502,56 @@ function createActions(core, deps) {
|
|
|
426
502
|
run.queue = queue;
|
|
427
503
|
run.stopping = false;
|
|
428
504
|
run.finished = null;
|
|
505
|
+
run.replaceMessageId = null;
|
|
429
506
|
touch();
|
|
430
507
|
runLoop(sess).catch((e) => console.error("group-chat run failed", e));
|
|
431
508
|
return { ok: true };
|
|
432
509
|
};
|
|
510
|
+
/** 对失败卡原地重试:只让该角色再讲一次,成功后覆盖同一条消息。 */
|
|
511
|
+
const retrySpeak = (args) => {
|
|
512
|
+
const sess = sessions.get(String(args && args.sessionId || ""));
|
|
513
|
+
if (!sess) return {
|
|
514
|
+
ok: false,
|
|
515
|
+
error: "会话不存在"
|
|
516
|
+
};
|
|
517
|
+
if (run.running) return {
|
|
518
|
+
ok: false,
|
|
519
|
+
error: "已有对话进行中,请先停止"
|
|
520
|
+
};
|
|
521
|
+
const msg = messages.get(String(args && args.messageId || ""));
|
|
522
|
+
if (!msg || msg.sessionId !== sess.id || !isSpeakFailure(msg)) return {
|
|
523
|
+
ok: false,
|
|
524
|
+
error: "没有可重试的失败发言"
|
|
525
|
+
};
|
|
526
|
+
const g = groups.get(sess.groupId);
|
|
527
|
+
if (!g) return {
|
|
528
|
+
ok: false,
|
|
529
|
+
error: "群组不存在"
|
|
530
|
+
};
|
|
531
|
+
if (repairFailedMessage(msg, g.roleIds.map((id) => roles.get(id)).filter((r) => Boolean(r)))) {
|
|
532
|
+
schedulePersist({ session: sess.id });
|
|
533
|
+
touch();
|
|
534
|
+
}
|
|
535
|
+
const roleId = msg.failedRoleId || (msg.speaker !== "user" && msg.speaker !== "system" ? msg.speaker : "");
|
|
536
|
+
const role = roleId ? roles.get(roleId) : void 0;
|
|
537
|
+
if (!role || role.groupId !== sess.groupId) return {
|
|
538
|
+
ok: false,
|
|
539
|
+
error: "失败角色已不存在,无法重试"
|
|
540
|
+
};
|
|
541
|
+
if (!role.enabled) return {
|
|
542
|
+
ok: false,
|
|
543
|
+
error: "该角色已停用,无法重试"
|
|
544
|
+
};
|
|
545
|
+
run.running = true;
|
|
546
|
+
run.sessionId = sess.id;
|
|
547
|
+
run.queue = [role.id];
|
|
548
|
+
run.stopping = false;
|
|
549
|
+
run.finished = null;
|
|
550
|
+
run.replaceMessageId = msg.id;
|
|
551
|
+
touch();
|
|
552
|
+
runLoop(sess, { replaceMessageId: msg.id }).catch((e) => console.error("group-chat retry failed", e));
|
|
553
|
+
return { ok: true };
|
|
554
|
+
};
|
|
433
555
|
const stop = (args) => {
|
|
434
556
|
if (run.running && (!args || !args.sessionId || run.sessionId === args.sessionId)) {
|
|
435
557
|
run.stopping = true;
|
|
@@ -527,6 +649,7 @@ function createActions(core, deps) {
|
|
|
527
649
|
lastCreated: core.lastCreated
|
|
528
650
|
};
|
|
529
651
|
if (kind === "send") return send(body);
|
|
652
|
+
if (kind === "retrySpeak") return retrySpeak(body);
|
|
530
653
|
if (kind === "stop") return stop(body);
|
|
531
654
|
if (kind === "confirmCommand") return confirmCommand(body);
|
|
532
655
|
if (kind === "models") return {
|
|
@@ -738,7 +861,8 @@ function createBroadcast(core) {
|
|
|
738
861
|
partial: core.run.partial,
|
|
739
862
|
partialReasoning: core.run.partialReasoning,
|
|
740
863
|
pendingConfirm: core.run.pendingConfirm,
|
|
741
|
-
finished: core.run.finished
|
|
864
|
+
finished: core.run.finished,
|
|
865
|
+
replaceMessageId: core.run.replaceMessageId
|
|
742
866
|
},
|
|
743
867
|
lastCreated: core.lastCreated,
|
|
744
868
|
groups: [...core.groups.values()].map((g) => ({
|
|
@@ -754,6 +878,7 @@ function createBroadcast(core) {
|
|
|
754
878
|
groupId: s.groupId,
|
|
755
879
|
name: s.name,
|
|
756
880
|
topic: s.topic,
|
|
881
|
+
...s.constraints && s.constraints.length ? { constraints: s.constraints } : {},
|
|
757
882
|
messageIds: s.messageIds.slice(),
|
|
758
883
|
createdAt: s.createdAt
|
|
759
884
|
})),
|
|
@@ -779,6 +904,7 @@ function createBroadcast(core) {
|
|
|
779
904
|
reasoning: m.reasoning,
|
|
780
905
|
model: m.model,
|
|
781
906
|
error: m.error,
|
|
907
|
+
failedRoleId: m.failedRoleId,
|
|
782
908
|
toolCalls: m.toolCalls,
|
|
783
909
|
ts: m.ts
|
|
784
910
|
}))
|
|
@@ -846,8 +972,274 @@ const TOOL_SCHEMAS = [
|
|
|
846
972
|
}
|
|
847
973
|
}
|
|
848
974
|
];
|
|
975
|
+
/** 折叠失败时临时原文总长上限。 */
|
|
976
|
+
const TEMP_MAX_CHARS = 16e3;
|
|
977
|
+
const KIND_LABEL = {
|
|
978
|
+
decided: "已定",
|
|
979
|
+
rejected: "否决",
|
|
980
|
+
open: "未决"
|
|
981
|
+
};
|
|
982
|
+
/** 已折入水位(缺省 0)。 */
|
|
983
|
+
function constraintsWatermark(sess) {
|
|
984
|
+
return typeof sess.constraintsUpToSeq === "number" && sess.constraintsUpToSeq > 0 ? sess.constraintsUpToSeq : 0;
|
|
985
|
+
}
|
|
986
|
+
/** 重试:只取失败卡之前的时间线;untilId 不在列表则原样。 */
|
|
987
|
+
function prefixIds(ids, untilId) {
|
|
988
|
+
if (!untilId) return ids;
|
|
989
|
+
const i = ids.indexOf(untilId);
|
|
990
|
+
return i >= 0 ? ids.slice(0, i) : ids;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* 新挤出:seq > 水位 且不在最近 40 条。只扫窗口外前缀(旧→新)。
|
|
994
|
+
* untilId:重试时把窗口截到该消息之前,不带上后面已经发生的发言。
|
|
995
|
+
*/
|
|
996
|
+
function squeezedMessages(messages, sess, untilId) {
|
|
997
|
+
const ids = prefixIds(sess.messageIds, untilId);
|
|
998
|
+
if (ids.length <= 40) return [];
|
|
999
|
+
const end = ids.length - 40;
|
|
1000
|
+
const upTo = constraintsWatermark(sess);
|
|
1001
|
+
const last = messages.get(ids[end - 1]);
|
|
1002
|
+
if (last && last.seq <= upTo) return [];
|
|
1003
|
+
const out = [];
|
|
1004
|
+
for (let i = 0; i < end; i++) {
|
|
1005
|
+
const m = messages.get(ids[i]);
|
|
1006
|
+
if (!m || m.seq <= upTo) continue;
|
|
1007
|
+
out.push(m);
|
|
1008
|
+
}
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
1011
|
+
/** 说话人展示名(折叠输入 / transcript 共用)。 */
|
|
1012
|
+
function speakerLabel(speaker, roleName) {
|
|
1013
|
+
if (speaker === "user") return "用户";
|
|
1014
|
+
if (speaker === "system") return "系统";
|
|
1015
|
+
return roleName || "成员";
|
|
1016
|
+
}
|
|
1017
|
+
/** 单条消息压成 transcript 行(正文 8k + 工具一行摘要)。 */
|
|
1018
|
+
function formatTranscriptLine(m, name) {
|
|
1019
|
+
let text = m.text || "";
|
|
1020
|
+
if (text.length > 8e3) text = text.slice(0, 8e3) + "…(已截断)";
|
|
1021
|
+
let line = "【" + name + "】" + text;
|
|
1022
|
+
if (Array.isArray(m.toolCalls)) for (const c of m.toolCalls) {
|
|
1023
|
+
if (!c || typeof c.tool !== "string") continue;
|
|
1024
|
+
let brief = "";
|
|
1025
|
+
try {
|
|
1026
|
+
brief = JSON.stringify(c.args) || "";
|
|
1027
|
+
} catch {
|
|
1028
|
+
brief = "";
|
|
1029
|
+
}
|
|
1030
|
+
if (brief.length > 60) brief = brief.slice(0, 60) + "…";
|
|
1031
|
+
const st = c.status === "ok" ? "成功" : c.status === "denied" ? "用户拒绝" : "失败";
|
|
1032
|
+
let ob = String(c.output || "");
|
|
1033
|
+
if (ob.length > 200) ob = ob.slice(0, 200) + "…";
|
|
1034
|
+
line += "\n [工具] " + c.tool + " " + brief + " → " + st + (ob ? "(" + ob.replace(/\s+/g, " ") + ")" : "");
|
|
1035
|
+
}
|
|
1036
|
+
return line;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* 单轮折叠消耗前缀:从最旧挤出起,最多 40 条 / 16k;系统行与失败卡计入消耗但不进模型。
|
|
1040
|
+
* 水位只能推到 consumed 的 max seq,剩余留待下一轮。
|
|
1041
|
+
*/
|
|
1042
|
+
function takeFoldBatch(squeezed, nameOf) {
|
|
1043
|
+
const consumed = [];
|
|
1044
|
+
const lines = [];
|
|
1045
|
+
let hasUser = false;
|
|
1046
|
+
let chars = 0;
|
|
1047
|
+
for (const m of squeezed) {
|
|
1048
|
+
if (m.speaker === "system" || m.error) {
|
|
1049
|
+
consumed.push(m);
|
|
1050
|
+
if (consumed.length >= 40) break;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const line = formatTranscriptLine(m, nameOf(m));
|
|
1054
|
+
const extra = line.length + (lines.length ? 2 : 0);
|
|
1055
|
+
if (lines.length > 0 && chars + extra > 16e3) break;
|
|
1056
|
+
if (m.speaker === "user") hasUser = true;
|
|
1057
|
+
lines.push(line);
|
|
1058
|
+
chars += extra;
|
|
1059
|
+
consumed.push(m);
|
|
1060
|
+
if (consumed.length >= 40) break;
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
consumed,
|
|
1064
|
+
lines,
|
|
1065
|
+
hasUser,
|
|
1066
|
+
allSystem: lines.length === 0
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* 未折入的挤出原文(失败缓冲):只格式化最近 20 条,再按 16k 从最旧往下丢。
|
|
1071
|
+
*/
|
|
1072
|
+
function tempTranscript(squeezed, nameOf) {
|
|
1073
|
+
if (!squeezed.length) return "";
|
|
1074
|
+
const usable = squeezed.filter((m) => m.speaker !== "system" && !m.error);
|
|
1075
|
+
if (!usable.length) return "";
|
|
1076
|
+
const lines = (usable.length > 20 ? usable.slice(-20) : usable).map((m) => formatTranscriptLine(m, nameOf(m)));
|
|
1077
|
+
let start = 0;
|
|
1078
|
+
let total = lines[0] ? lines[0].length : 0;
|
|
1079
|
+
for (let i = 1; i < lines.length; i++) total += 2 + lines[i].length;
|
|
1080
|
+
while (start < lines.length - 1 && total > 16e3) {
|
|
1081
|
+
total -= lines[start].length + 2;
|
|
1082
|
+
start++;
|
|
1083
|
+
}
|
|
1084
|
+
let block = lines.slice(start).join("\n\n");
|
|
1085
|
+
if (block.length > 16e3) block = block.slice(0, TEMP_MAX_CHARS) + "…(已截断)";
|
|
1086
|
+
return block;
|
|
1087
|
+
}
|
|
1088
|
+
/** 水位 = 本批挤出的 max(seq);空批为 0。 */
|
|
1089
|
+
function squeezedMaxSeq(squeezed) {
|
|
1090
|
+
let max = 0;
|
|
1091
|
+
for (const m of squeezed) if (m.seq > max) max = m.seq;
|
|
1092
|
+
return max;
|
|
1093
|
+
}
|
|
1094
|
+
/** hydrate / 模型输出:非法 kind 丢条目;空 text 丢;条数与总长截断。 */
|
|
1095
|
+
function sanitizeConstraints(raw) {
|
|
1096
|
+
if (!Array.isArray(raw)) return [];
|
|
1097
|
+
const out = [];
|
|
1098
|
+
let total = 0;
|
|
1099
|
+
for (const item of raw) {
|
|
1100
|
+
if (out.length >= 12) break;
|
|
1101
|
+
if (!item || typeof item !== "object") continue;
|
|
1102
|
+
const kind = asConstraintKind(item.kind);
|
|
1103
|
+
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
1104
|
+
if (!kind || !text) continue;
|
|
1105
|
+
const clipped = text.length > 160 ? text.slice(0, 160) + "…" : text;
|
|
1106
|
+
const cost = KIND_LABEL[kind].length + clipped.length;
|
|
1107
|
+
if (total + cost > 1200) break;
|
|
1108
|
+
out.push({
|
|
1109
|
+
kind,
|
|
1110
|
+
text: clipped
|
|
1111
|
+
});
|
|
1112
|
+
total += cost;
|
|
1113
|
+
}
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* 解析折叠模型输出。null = 解析失败(水位不推);[] = 无新结论(水位推、备忘不动)。
|
|
1118
|
+
*/
|
|
1119
|
+
function parseConstraints(raw) {
|
|
1120
|
+
const body = raw.replace(/```(?:json)?/g, "");
|
|
1121
|
+
const l = body.indexOf("{");
|
|
1122
|
+
const r = body.lastIndexOf("}");
|
|
1123
|
+
if (l < 0 || r <= l) return null;
|
|
1124
|
+
try {
|
|
1125
|
+
const o = JSON.parse(body.slice(l, r + 1));
|
|
1126
|
+
if (!Object.prototype.hasOwnProperty.call(o, "constraints")) return null;
|
|
1127
|
+
if (!Array.isArray(o.constraints)) return null;
|
|
1128
|
+
return sanitizeConstraints(o.constraints);
|
|
1129
|
+
} catch {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/** 本批无用户消息时,新的已定/否决降为未决。 */
|
|
1134
|
+
function downgradeWithoutUser(list, hasUser) {
|
|
1135
|
+
if (hasUser) return list;
|
|
1136
|
+
return list.map((c) => c.kind === "open" ? c : {
|
|
1137
|
+
kind: "open",
|
|
1138
|
+
text: c.text
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/** system 提示词「已确认约束」块;空则空串。 */
|
|
1142
|
+
function constraintBlock(list) {
|
|
1143
|
+
if (!list || !list.length) return "";
|
|
1144
|
+
return "\n# 已确认约束\n" + list.map((c) => "- " + KIND_LABEL[c.kind] + ":" + c.text).join("\n");
|
|
1145
|
+
}
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/host/engine/fold.ts
|
|
1148
|
+
/** DSH 默认模型(与 retitle 同一读取面;缺位返回 null)。 */
|
|
1149
|
+
const defaultModel$1 = (core) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const svc = core.ctx.reflect.get("agentDefaultModel");
|
|
1152
|
+
const sel = svc ? svc.currentSelection() : null;
|
|
1153
|
+
return sel && typeof sel.provider === "string" && typeof sel.model === "string" && sel.provider && sel.model ? {
|
|
1154
|
+
provider: sel.provider,
|
|
1155
|
+
model: sel.model
|
|
1156
|
+
} : null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
/**
|
|
1162
|
+
* 每轮 send 结束后折叠窗口外约束:fire-and-forget、不产生消息、静默失败。
|
|
1163
|
+
* 同会话去重;会话已清空则 abort。内存 {constraints, constraintsUpToSeq} 一次挂上。
|
|
1164
|
+
*/
|
|
1165
|
+
function createFold(core, deps) {
|
|
1166
|
+
const { llm, messages, roles } = core;
|
|
1167
|
+
const { touch, schedulePersist } = deps;
|
|
1168
|
+
const folding = /* @__PURE__ */ new Set();
|
|
1169
|
+
const nameOf = (speaker) => speakerLabel(speaker, (roles.get(speaker) || { name: void 0 }).name);
|
|
1170
|
+
return async (sess) => {
|
|
1171
|
+
if (folding.has(sess.id)) return;
|
|
1172
|
+
const squeezed = squeezedMessages(messages, sess);
|
|
1173
|
+
if (!squeezed.length) return;
|
|
1174
|
+
const input = takeFoldBatch(squeezed, (m) => nameOf(m.speaker));
|
|
1175
|
+
const watermark = squeezedMaxSeq(input.consumed);
|
|
1176
|
+
if (watermark <= 0) return;
|
|
1177
|
+
const commit = (next) => {
|
|
1178
|
+
const live = core.sessions.get(sess.id);
|
|
1179
|
+
if (!live || live.messageIds.length === 0) return;
|
|
1180
|
+
if (next && next.length) live.constraints = next;
|
|
1181
|
+
live.constraintsUpToSeq = watermark;
|
|
1182
|
+
schedulePersist({ session: live.id });
|
|
1183
|
+
touch();
|
|
1184
|
+
};
|
|
1185
|
+
if (input.allSystem) {
|
|
1186
|
+
commit(void 0);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const dm = defaultModel$1(core);
|
|
1190
|
+
if (!dm) return;
|
|
1191
|
+
folding.add(sess.id);
|
|
1192
|
+
try {
|
|
1193
|
+
const existing = (sess.constraints || []).map((c) => "- " + KIND_LABEL[c.kind] + ":" + c.text);
|
|
1194
|
+
const sys = [
|
|
1195
|
+
"你是群聊会话的约束整理助手。把已经离开最近对话窗口的旧消息压成无主结论/约束备忘。",
|
|
1196
|
+
"",
|
|
1197
|
+
"# 规则",
|
|
1198
|
+
"- 只输出一行 JSON:{\"constraints\":[{\"kind\":\"decided|rejected|open\",\"text\":\"…\"}, ...]}",
|
|
1199
|
+
"- kind 只能是 decided(已定)/ rejected(否决)/ open(未决)",
|
|
1200
|
+
"- 无主:条目不写说话人。8–12 条、合计不超过 1200 字,最多 12 条",
|
|
1201
|
+
"- 已定/否决只能依据【用户】原文;角色对打一律标 open(未决)",
|
|
1202
|
+
"- 同主题:新已定覆盖旧未决;旧已定不能因角色反对改写,除非【用户】改口",
|
|
1203
|
+
"- 没有新结论时输出 {\"constraints\":[]}(保留旧备忘)",
|
|
1204
|
+
"- 超预算时按 已定 > 未决 > 过程叙述 取舍;语言跟随记录",
|
|
1205
|
+
"",
|
|
1206
|
+
"当前备忘:",
|
|
1207
|
+
existing.length ? existing.join("\n") : "(空)"
|
|
1208
|
+
].join("\n");
|
|
1209
|
+
const user = "旧消息(从旧到新,含工具一行摘要):\n\n" + input.lines.join("\n\n");
|
|
1210
|
+
let acc = "";
|
|
1211
|
+
for await (const chunk of llm.stream({
|
|
1212
|
+
provider: dm.provider,
|
|
1213
|
+
model: dm.model,
|
|
1214
|
+
system: sys,
|
|
1215
|
+
purpose: "session-title",
|
|
1216
|
+
messages: [{
|
|
1217
|
+
id: "g" + core.revision + "-c0",
|
|
1218
|
+
role: "user",
|
|
1219
|
+
content: [{
|
|
1220
|
+
type: "text",
|
|
1221
|
+
text: user
|
|
1222
|
+
}],
|
|
1223
|
+
source: { kind: "user" }
|
|
1224
|
+
}]
|
|
1225
|
+
})) if (chunk.type === "text-delta") {
|
|
1226
|
+
acc += chunk.text;
|
|
1227
|
+
if (acc.length > 4e3) break;
|
|
1228
|
+
} else if (chunk.type === "finish") break;
|
|
1229
|
+
const parsed = parseConstraints(acc);
|
|
1230
|
+
if (parsed === null) return;
|
|
1231
|
+
commit(parsed.length ? downgradeWithoutUser(parsed, input.hasUser) : void 0);
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
console.error("[dsh-group-chat] 会话约束折叠失败(跳过,不影响对话):", e);
|
|
1234
|
+
} finally {
|
|
1235
|
+
folding.delete(sess.id);
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
849
1239
|
//#endregion
|
|
850
1240
|
//#region src/host/engine/retitle.ts
|
|
1241
|
+
/** 仍是新建占位名(含改名之前的「会话 N」存量),自动标题尚未落地。 */
|
|
1242
|
+
const isPlaceholderName = (name) => name === "新会话" || /^会话 \d+$/.test(name);
|
|
851
1243
|
/** DSH 默认模型(agentDefaultModel 服务缺位或未配置时返回 null,调用方静默跳过)。 */
|
|
852
1244
|
const defaultModel = (core) => {
|
|
853
1245
|
try {
|
|
@@ -866,7 +1258,7 @@ const titleTranscript = (core, sess) => {
|
|
|
866
1258
|
const out = [];
|
|
867
1259
|
for (const mid of sess.messageIds.slice(-40)) {
|
|
868
1260
|
const m = core.messages.get(mid);
|
|
869
|
-
if (!m || m.speaker === "system" || !m.text) continue;
|
|
1261
|
+
if (!m || m.speaker === "system" || m.error || !m.text) continue;
|
|
870
1262
|
const name = m.speaker === "user" ? "用户" : (core.roles.get(m.speaker) || { name: void 0 }).name || "成员";
|
|
871
1263
|
out.push("【" + name + "】" + m.text.replace(/\s+/g, " ").slice(0, 500));
|
|
872
1264
|
}
|
|
@@ -892,9 +1284,9 @@ const parseRetitle = (raw) => {
|
|
|
892
1284
|
};
|
|
893
1285
|
/**
|
|
894
1286
|
* 每轮结束后根据聊天内容整理会话名称与主题:后台 fire-and-forget、不产生
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
1287
|
+
* 消息、静默失败。名称只在仍为默认占位时生成一次(「类别 emoji + 对象|目标」);
|
|
1288
|
+
* 主题 = 演进式一句话摘要(对象+目标+当前焦点),每轮更新,注入后续角色上下文。
|
|
1289
|
+
* 手动编辑过的字段永久跳过(隐式固定,apply 时复查)。
|
|
898
1290
|
*/
|
|
899
1291
|
function createRetitle(core, deps) {
|
|
900
1292
|
const { llm } = core;
|
|
@@ -904,7 +1296,8 @@ function createRetitle(core, deps) {
|
|
|
904
1296
|
return async (sess) => {
|
|
905
1297
|
if (retitling.has(sess.id)) return;
|
|
906
1298
|
const dm = defaultModel(core);
|
|
907
|
-
|
|
1299
|
+
const nameFrozen = !!sess.namePinned || !isPlaceholderName(sess.name);
|
|
1300
|
+
if (!dm || nameFrozen && sess.topicPinned) return;
|
|
908
1301
|
const transcript = titleTranscript(core, sess);
|
|
909
1302
|
if (!transcript) return;
|
|
910
1303
|
retitling.add(sess.id);
|
|
@@ -915,9 +1308,9 @@ function createRetitle(core, deps) {
|
|
|
915
1308
|
"# 名称规则",
|
|
916
1309
|
"- 格式:「类别 emoji + 对象|目标」,例如「🔎 缓存选型|Redis 与本地 KV 对比」",
|
|
917
1310
|
"- 类别固定六选一:🔎 调研对比(多方案/多观点比较)、💡 头脑风暴(创意发散)、⚖️ 方案评审(评审已有方案或产物)、🛠️ 排查修复(定位与解决问题)、📝 方法整理(总结沉淀方法与知识)、🗣️ 通用讨论(其余兜底)",
|
|
918
|
-
"-
|
|
1311
|
+
"- 对象在前:把辨识度最高的讨论对象放最前;省略群组名(外层已展示)",
|
|
919
1312
|
"- 目标 = 当前正在做的事,动宾短语,保持简洁",
|
|
920
|
-
"- 名称总长不超过 16
|
|
1313
|
+
"- 名称总长不超过 16 个字;名称只生成一次,不要为了追问/继续而改名",
|
|
921
1314
|
"",
|
|
922
1315
|
"# 主题规则",
|
|
923
1316
|
"- 一句话演进式摘要:讨论对象 + 当前目标 + 当前焦点/分歧点",
|
|
@@ -927,7 +1320,7 @@ function createRetitle(core, deps) {
|
|
|
927
1320
|
"- 语言跟随用户消息的主要语言;保留产品名与技术名词",
|
|
928
1321
|
"- 只输出一行 JSON:{\"name\": \"…\", \"topic\": \"…\"},不要输出其他内容",
|
|
929
1322
|
"",
|
|
930
|
-
"当前名称:" + (
|
|
1323
|
+
"当前名称:" + (nameFrozen ? "(已固定,本次不要输出 name 字段)" : sess.name),
|
|
931
1324
|
"当前主题:" + (sess.topicPinned ? "(已手动固定,本次不要输出 topic 字段)" : sess.topic || "(空)")
|
|
932
1325
|
].join("\n");
|
|
933
1326
|
let acc = "";
|
|
@@ -951,7 +1344,7 @@ function createRetitle(core, deps) {
|
|
|
951
1344
|
} else if (chunk.type === "finish") break;
|
|
952
1345
|
const parsed = parseRetitle(acc);
|
|
953
1346
|
let changed = false;
|
|
954
|
-
if (parsed.name && !sess.namePinned) {
|
|
1347
|
+
if (parsed.name && !sess.namePinned && isPlaceholderName(sess.name)) {
|
|
955
1348
|
sess.name = parsed.name;
|
|
956
1349
|
changed = true;
|
|
957
1350
|
}
|
|
@@ -974,7 +1367,7 @@ function createRetitle(core, deps) {
|
|
|
974
1367
|
//#region src/host/engine/conversation.ts
|
|
975
1368
|
/**
|
|
976
1369
|
* 对话引擎:消息追加、群聊记录转写、角色发言(speak:prompt 构建 + 流式
|
|
977
|
-
* 轮次 + 工具回注循环)、多轮 runLoop
|
|
1370
|
+
* 轮次 + 工具回注循环)、多轮 runLoop(结束时后台 retitle + 窗口外约束折叠)。
|
|
978
1371
|
* @module dsh-group-chat/host/engine/conversation
|
|
979
1372
|
*/
|
|
980
1373
|
/** 创建对话引擎。 */
|
|
@@ -985,6 +1378,11 @@ function createConversation(core, deps) {
|
|
|
985
1378
|
touch,
|
|
986
1379
|
schedulePersist
|
|
987
1380
|
});
|
|
1381
|
+
const fold = createFold(core, {
|
|
1382
|
+
touch,
|
|
1383
|
+
schedulePersist
|
|
1384
|
+
});
|
|
1385
|
+
const nameOf = (speaker) => speakerLabel(speaker, (roles.get(speaker) || { name: void 0 }).name);
|
|
988
1386
|
const appendMessage = (sess, speaker, text, extra) => {
|
|
989
1387
|
const msg = {
|
|
990
1388
|
id: core.nid("msg"),
|
|
@@ -1003,6 +1401,7 @@ function createConversation(core, deps) {
|
|
|
1003
1401
|
if (extra.thinkingSummary !== void 0) msg.thinkingSummary = extra.thinkingSummary;
|
|
1004
1402
|
if (extra.model !== void 0) msg.model = extra.model;
|
|
1005
1403
|
if (extra.error !== void 0) msg.error = extra.error;
|
|
1404
|
+
if (extra.failedRoleId !== void 0) msg.failedRoleId = extra.failedRoleId;
|
|
1006
1405
|
if (Array.isArray(extra.toolCalls) && extra.toolCalls.length > 0) msg.toolCalls = extra.toolCalls;
|
|
1007
1406
|
}
|
|
1008
1407
|
messages.set(msg.id, msg);
|
|
@@ -1011,35 +1410,76 @@ function createConversation(core, deps) {
|
|
|
1011
1410
|
schedulePersist({ session: sess.id });
|
|
1012
1411
|
return msg;
|
|
1013
1412
|
};
|
|
1014
|
-
/**
|
|
1015
|
-
const
|
|
1413
|
+
/** 把失败回合写成该角色的消息(speaker = 角色 id),不再用系统胶囊顶替。 */
|
|
1414
|
+
const writeFailure = (sess, role, err, replaceId) => {
|
|
1415
|
+
const raw = unwrapSpeakFailure(String(err && err.message || err));
|
|
1416
|
+
const extra = {
|
|
1417
|
+
error: true,
|
|
1418
|
+
failedRoleId: role.id,
|
|
1419
|
+
model: role.provider + " / " + role.model
|
|
1420
|
+
};
|
|
1421
|
+
if (replaceId) {
|
|
1422
|
+
const existing = messages.get(replaceId);
|
|
1423
|
+
if (existing && existing.sessionId === sess.id) {
|
|
1424
|
+
existing.speaker = role.id;
|
|
1425
|
+
existing.text = raw;
|
|
1426
|
+
existing.error = true;
|
|
1427
|
+
existing.failedRoleId = role.id;
|
|
1428
|
+
existing.model = extra.model;
|
|
1429
|
+
existing.reasoning = void 0;
|
|
1430
|
+
existing.reasoningFull = void 0;
|
|
1431
|
+
existing.thinkingSummary = void 0;
|
|
1432
|
+
existing.toolCalls = void 0;
|
|
1433
|
+
existing.ts = Date.now();
|
|
1434
|
+
touch();
|
|
1435
|
+
schedulePersist({ session: sess.id });
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
appendMessage(sess, role.id, raw, extra);
|
|
1440
|
+
};
|
|
1441
|
+
/** 成功发言写入:replaceId 存在则原地覆盖失败卡。 */
|
|
1442
|
+
const writeSuccess = (sess, role, out, replaceId) => {
|
|
1443
|
+
const extra = {
|
|
1444
|
+
model: role.provider + " / " + role.model,
|
|
1445
|
+
reasoning: out.reasoning,
|
|
1446
|
+
toolCalls: out.toolCalls
|
|
1447
|
+
};
|
|
1448
|
+
if (replaceId) {
|
|
1449
|
+
const existing = messages.get(replaceId);
|
|
1450
|
+
if (existing && existing.sessionId === sess.id) {
|
|
1451
|
+
existing.speaker = role.id;
|
|
1452
|
+
existing.text = out.text;
|
|
1453
|
+
existing.error = void 0;
|
|
1454
|
+
existing.failedRoleId = void 0;
|
|
1455
|
+
existing.model = extra.model;
|
|
1456
|
+
existing.reasoning = out.reasoning;
|
|
1457
|
+
existing.reasoningFull = void 0;
|
|
1458
|
+
existing.thinkingSummary = void 0;
|
|
1459
|
+
existing.toolCalls = Array.isArray(out.toolCalls) && out.toolCalls.length > 0 ? out.toolCalls : void 0;
|
|
1460
|
+
existing.ts = Date.now();
|
|
1461
|
+
touch();
|
|
1462
|
+
schedulePersist({ session: sess.id });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
appendMessage(sess, role.id, out.text, extra);
|
|
1467
|
+
};
|
|
1468
|
+
/** 群聊记录 → 角色上下文块(最近 40 条 + 未折入临时原文)。失败卡不进上下文。重试截到该条之前。 */
|
|
1469
|
+
const transcriptBlock = (sess, skipId) => {
|
|
1016
1470
|
const out = [];
|
|
1017
|
-
for (const mid of sess.messageIds) {
|
|
1471
|
+
for (const mid of prefixIds(sess.messageIds, skipId).slice(-40)) {
|
|
1018
1472
|
const m = messages.get(mid);
|
|
1019
|
-
if (!m) continue;
|
|
1020
|
-
|
|
1021
|
-
let text = m.text || "";
|
|
1022
|
-
if (text.length > 8e3) text = text.slice(0, 8e3) + "…(已截断)";
|
|
1023
|
-
let line = "【" + name + "】" + text;
|
|
1024
|
-
if (Array.isArray(m.toolCalls)) for (const c of m.toolCalls) {
|
|
1025
|
-
if (!c || typeof c.tool !== "string") continue;
|
|
1026
|
-
let brief = "";
|
|
1027
|
-
try {
|
|
1028
|
-
brief = JSON.stringify(c.args) || "";
|
|
1029
|
-
} catch {
|
|
1030
|
-
brief = "";
|
|
1031
|
-
}
|
|
1032
|
-
if (brief.length > 60) brief = brief.slice(0, 60) + "…";
|
|
1033
|
-
const st = c.status === "ok" ? "成功" : c.status === "denied" ? "用户拒绝" : "失败";
|
|
1034
|
-
let ob = String(c.output || "");
|
|
1035
|
-
if (ob.length > 200) ob = ob.slice(0, 200) + "…";
|
|
1036
|
-
line += "\n [工具] " + c.tool + " " + brief + " → " + st + (ob ? "(" + ob.replace(/\s+/g, " ") + ")" : "");
|
|
1037
|
-
}
|
|
1038
|
-
out.push(line);
|
|
1473
|
+
if (!m || m.id === skipId || m.error) continue;
|
|
1474
|
+
out.push(formatTranscriptLine(m, nameOf(m.speaker)));
|
|
1039
1475
|
}
|
|
1040
|
-
|
|
1476
|
+
const live = out.join("\n\n");
|
|
1477
|
+
const temp = tempTranscript(squeezedMessages(messages, sess, skipId), (m) => nameOf(m.speaker));
|
|
1478
|
+
if (!temp) return live;
|
|
1479
|
+
if (!live) return temp;
|
|
1480
|
+
return temp + "\n\n" + live;
|
|
1041
1481
|
};
|
|
1042
|
-
const speak = async (g, sess, role) => {
|
|
1482
|
+
const speak = async (g, sess, role, skipId) => {
|
|
1043
1483
|
const ws = await materials.loadWorkspaceFiles(g);
|
|
1044
1484
|
const parts = ws.parts;
|
|
1045
1485
|
const sys = [
|
|
@@ -1049,6 +1489,7 @@ function createConversation(core, deps) {
|
|
|
1049
1489
|
"- 名称:" + role.name,
|
|
1050
1490
|
"- 人设:" + (role.persona ? role.persona : "(未填写,请以积极协作者的身份参与讨论)"),
|
|
1051
1491
|
sess.topic ? "\n# 本会话主题\n" + sess.topic : "",
|
|
1492
|
+
constraintBlock(sess.constraints),
|
|
1052
1493
|
materials.materialBlock(parts, ws.dir),
|
|
1053
1494
|
ws.dir ? "\n# 可用工具\n你可以调用工具在群组工作区目录(" + ws.dir + ")内查看文件与目录" + (g.permissionTier === "workspace_write" ? "、执行 shell 命令(命令需用户逐条确认,请优先用于运行测试)" : g.permissionTier === "full_access" ? "、执行 shell 命令(命令将直接执行、无需确认,请谨慎并优先用于运行测试)" : "") + "。需要事实依据时优先用工具查看,不要凭空猜测。" : "",
|
|
1054
1495
|
"\n# 发言要求",
|
|
@@ -1056,7 +1497,7 @@ function createConversation(core, deps) {
|
|
|
1056
1497
|
"- 回应群内最新讨论(消息中「@你的名字」表示用户点名要求你回应,被点名时请优先回应);与其他成员自然对话;有不同观点可以提出并说明理由",
|
|
1057
1498
|
"- 保持简洁,通常不超过 300 字"
|
|
1058
1499
|
].filter((s) => s !== "").join("\n");
|
|
1059
|
-
const history = transcriptBlock(sess);
|
|
1500
|
+
const history = transcriptBlock(sess, skipId);
|
|
1060
1501
|
const intro = history ? "以下是本会话的群聊记录(从旧到新):\n\n" + history : "本会话刚刚开始,请围绕主题做简短开场发言。";
|
|
1061
1502
|
let wsRoot = null;
|
|
1062
1503
|
if (ws.dir) try {
|
|
@@ -1276,10 +1717,12 @@ function createConversation(core, deps) {
|
|
|
1276
1717
|
toolCalls
|
|
1277
1718
|
};
|
|
1278
1719
|
};
|
|
1279
|
-
const runLoop = async (sess) => {
|
|
1720
|
+
const runLoop = async (sess, opts) => {
|
|
1280
1721
|
const g = groups.get(sess.groupId);
|
|
1281
1722
|
const startCount = sess.messageIds.length;
|
|
1282
1723
|
let failed = false;
|
|
1724
|
+
let replaceId = opts && opts.replaceMessageId;
|
|
1725
|
+
run.replaceMessageId = replaceId || null;
|
|
1283
1726
|
try {
|
|
1284
1727
|
while (run.queue.length > 0 && !run.stopping) {
|
|
1285
1728
|
const roleId = run.queue.shift();
|
|
@@ -1289,16 +1732,27 @@ function createConversation(core, deps) {
|
|
|
1289
1732
|
run.partialReasoning = "";
|
|
1290
1733
|
touch();
|
|
1291
1734
|
if (!role) continue;
|
|
1735
|
+
const targetId = replaceId;
|
|
1736
|
+
replaceId = void 0;
|
|
1292
1737
|
try {
|
|
1293
|
-
const out = await speak(g, sess, role);
|
|
1294
|
-
if (!run.stopping && (out.text || Array.isArray(out.toolCalls) && out.toolCalls.length > 0))
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1738
|
+
const out = await speak(g, sess, role, targetId);
|
|
1739
|
+
if (!run.stopping && (out.text || Array.isArray(out.toolCalls) && out.toolCalls.length > 0)) writeSuccess(sess, role, out, targetId);
|
|
1740
|
+
else if (!run.stopping && targetId) {
|
|
1741
|
+
failed = true;
|
|
1742
|
+
run.currentRoleId = null;
|
|
1743
|
+
run.partial = "";
|
|
1744
|
+
run.partialReasoning = "";
|
|
1745
|
+
run.replaceMessageId = null;
|
|
1746
|
+
writeFailure(sess, role, "模型没有返回内容", targetId);
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1299
1749
|
} catch (e) {
|
|
1300
1750
|
failed = true;
|
|
1301
|
-
|
|
1751
|
+
run.currentRoleId = null;
|
|
1752
|
+
run.partial = "";
|
|
1753
|
+
run.partialReasoning = "";
|
|
1754
|
+
run.replaceMessageId = null;
|
|
1755
|
+
writeFailure(sess, role, e, targetId);
|
|
1302
1756
|
break;
|
|
1303
1757
|
}
|
|
1304
1758
|
}
|
|
@@ -1318,9 +1772,13 @@ function createConversation(core, deps) {
|
|
|
1318
1772
|
run.confirmSignal = null;
|
|
1319
1773
|
run.childProc = null;
|
|
1320
1774
|
run.stopping = false;
|
|
1775
|
+
run.replaceMessageId = null;
|
|
1321
1776
|
touch();
|
|
1322
1777
|
}
|
|
1323
|
-
if (sess.messageIds.length > startCount)
|
|
1778
|
+
if (sess.messageIds.length > startCount || opts && opts.replaceMessageId) {
|
|
1779
|
+
retitle(sess);
|
|
1780
|
+
fold(sess);
|
|
1781
|
+
}
|
|
1324
1782
|
};
|
|
1325
1783
|
return {
|
|
1326
1784
|
appendMessage,
|
|
@@ -1607,6 +2065,7 @@ function messageJson(m) {
|
|
|
1607
2065
|
if (m.reasoningFull !== void 0) o.reasoningFull = m.reasoningFull;
|
|
1608
2066
|
if (m.thinkingSummary !== void 0) o.thinkingSummary = m.thinkingSummary;
|
|
1609
2067
|
if (m.error !== void 0) o.error = m.error;
|
|
2068
|
+
if (typeof m.failedRoleId === "string" && m.failedRoleId) o.failedRoleId = m.failedRoleId;
|
|
1610
2069
|
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) o.toolCalls = m.toolCalls;
|
|
1611
2070
|
return o;
|
|
1612
2071
|
}
|
|
@@ -2021,6 +2480,8 @@ function createPersistence(core) {
|
|
|
2021
2480
|
topic: s.topic,
|
|
2022
2481
|
...s.namePinned ? { namePinned: true } : {},
|
|
2023
2482
|
...s.topicPinned ? { topicPinned: true } : {},
|
|
2483
|
+
...s.constraints && s.constraints.length ? { constraints: s.constraints } : {},
|
|
2484
|
+
...typeof s.constraintsUpToSeq === "number" && s.constraintsUpToSeq > 0 ? { constraintsUpToSeq: s.constraintsUpToSeq } : {},
|
|
2024
2485
|
createdAt: s.createdAt,
|
|
2025
2486
|
messages: s.messageIds.map((mid) => core.messages.get(mid)).filter(Boolean).map((m) => messageJson(m)).filter(Boolean)
|
|
2026
2487
|
});
|
|
@@ -2128,6 +2589,9 @@ function createPersistence(core) {
|
|
|
2128
2589
|
if (typeof doc.topic === "string") sess.topic = doc.topic;
|
|
2129
2590
|
if (doc.namePinned === true) sess.namePinned = true;
|
|
2130
2591
|
if (doc.topicPinned === true) sess.topicPinned = true;
|
|
2592
|
+
const constraints = sanitizeConstraints(doc.constraints);
|
|
2593
|
+
if (constraints.length) sess.constraints = constraints;
|
|
2594
|
+
if (typeof doc.constraintsUpToSeq === "number" && doc.constraintsUpToSeq > 0) sess.constraintsUpToSeq = doc.constraintsUpToSeq;
|
|
2131
2595
|
if (typeof doc.createdAt === "number") sess.createdAt = doc.createdAt;
|
|
2132
2596
|
let fallbackSeq = 0;
|
|
2133
2597
|
for (const m of Array.isArray(doc.messages) ? doc.messages : []) {
|
|
@@ -2144,11 +2608,19 @@ function createPersistence(core) {
|
|
|
2144
2608
|
thinkingSummary: m.thinkingSummary !== void 0 ? String(m.thinkingSummary) : void 0,
|
|
2145
2609
|
model: m.model,
|
|
2146
2610
|
error: m.error,
|
|
2611
|
+
failedRoleId: typeof m.failedRoleId === "string" && m.failedRoleId ? m.failedRoleId : void 0,
|
|
2147
2612
|
toolCalls: Array.isArray(m.toolCalls) ? m.toolCalls : void 0,
|
|
2148
2613
|
ts: typeof m.ts === "number" ? m.ts : Date.now()
|
|
2149
2614
|
});
|
|
2150
2615
|
sess.messageIds.push(m.id);
|
|
2151
2616
|
}
|
|
2617
|
+
const groupRoles = g.roleIds.map((rid) => core.roles.get(rid)).filter((r) => Boolean(r));
|
|
2618
|
+
let repaired = false;
|
|
2619
|
+
for (const mid of sess.messageIds) {
|
|
2620
|
+
const rec = core.messages.get(mid);
|
|
2621
|
+
if (rec && repairFailedMessage(rec, groupRoles)) repaired = true;
|
|
2622
|
+
}
|
|
2623
|
+
if (repaired) schedulePersist({ session: sid });
|
|
2152
2624
|
}
|
|
2153
2625
|
core.sessions.set(sid, sess);
|
|
2154
2626
|
g.sessionIds.push(sid);
|
|
@@ -2223,7 +2695,7 @@ function createPersistence(core) {
|
|
|
2223
2695
|
sessionIds: []
|
|
2224
2696
|
};
|
|
2225
2697
|
core.groups.set(g.id, g);
|
|
2226
|
-
const sess = core.newSession(g.id
|
|
2698
|
+
const sess = core.newSession(g.id);
|
|
2227
2699
|
g.sessionIds.push(sess.id);
|
|
2228
2700
|
autoSessions.push(sess.id);
|
|
2229
2701
|
}
|