@roaming-ai/dsh-group-chat 0.3.0 → 0.3.2
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 +93 -28
- package/lib/client.js +295 -326
- package/lib/client.js.map +1 -1
- package/lib/index.js +234 -221
- package/lib/types/client/components/AsidePanel.d.ts +3 -19
- package/lib/types/client/components/NavPanel.d.ts +9 -11
- package/lib/types/client/components/RoleDrawer.d.ts +3 -2
- package/lib/types/client/components/ToolRow.d.ts +2 -2
- package/lib/types/client/hooks/useGroupChatState.d.ts +0 -16
- package/lib/types/client/lib/model.d.ts +1 -7
- package/lib/types/client/utils/utils.d.ts +4 -4
- package/lib/types/core/json.d.ts +6 -0
- package/lib/types/core/types.d.ts +59 -64
- package/lib/types/host/engine/defaults.d.ts +13 -0
- package/lib/types/host/persistence/persistence.d.ts +2 -4
- package/lib/types/host/persistence/store.d.ts +5 -1
- package/lib/types/host/service.d.ts +2 -2
- package/lib/types/host/state.d.ts +1 -0
- package/lib/types/host/tools/tools.d.ts +2 -1
- package/lib/types/index.d.ts +3 -0
- package/lib/types/shared/file-mention-grammar.d.ts +3 -12
- package/package.json +4 -1
- package/src/client/GroupChatPanel.tsx +9 -56
- package/src/client/components/AsidePanel.tsx +40 -15
- package/src/client/components/ChatPanel.tsx +1 -1
- package/src/client/components/Composer.tsx +2 -2
- package/src/client/components/ConstraintList.tsx +1 -6
- package/src/client/components/MessageFlow.tsx +2 -1
- package/src/client/components/NavPanel.tsx +96 -78
- package/src/client/components/RoleDrawer.tsx +29 -7
- package/src/client/components/ToolRow.tsx +2 -2
- package/src/client/hooks/useComposer.ts +14 -53
- package/src/client/hooks/useGroupChatState.ts +2 -13
- package/src/client/lib/model.ts +1 -14
- package/src/client/lib/styles.ts +6 -2
- package/src/client/utils/utils.ts +6 -6
- package/src/core/constraints.ts +6 -12
- package/src/core/errors.ts +2 -12
- package/src/core/json.ts +17 -0
- package/src/core/types.ts +29 -20
- package/src/host/api/actions.ts +14 -13
- package/src/host/engine/conversation.ts +50 -51
- package/src/host/engine/defaults.ts +28 -0
- package/src/host/engine/fold.ts +2 -17
- package/src/host/engine/retitle.ts +9 -31
- package/src/host/materials/materials.ts +2 -0
- package/src/host/persistence/persistence.ts +57 -34
- package/src/host/persistence/store.ts +5 -1
- package/src/host/service.ts +4 -4
- package/src/host/state.ts +5 -2
- package/src/host/tools/tools.ts +42 -48
- package/src/index.ts +7 -5
- package/src/shared/file-mention-grammar.test.ts +1 -38
- package/src/shared/file-mention-grammar.ts +7 -33
- package/lib/types/client/Bubble.d.ts +0 -10
- package/lib/types/client/Glyph.d.ts +0 -12
- package/lib/types/client/GroupChatPanel.old.d.ts +0 -10
- package/lib/types/client/GroupChatSettingsSection.d.ts +0 -15
- package/lib/types/client/PermissionSelect.d.ts +0 -13
- package/lib/types/client/RoleDrawer.d.ts +0 -17
- package/lib/types/client/ThinkRow.d.ts +0 -9
- package/lib/types/client/ToolRow.d.ts +0 -9
- package/lib/types/client/api.d.ts +0 -8
- package/lib/types/client/components.d.ts +0 -22
- package/lib/types/client/drawer.d.ts +0 -17
- package/lib/types/client/glyph.d.ts +0 -12
- package/lib/types/client/model.d.ts +0 -78
- package/lib/types/client/panel.d.ts +0 -10
- package/lib/types/client/settings.d.ts +0 -15
- package/lib/types/client/styles.d.ts +0 -10
- package/lib/types/client/ui.d.ts +0 -17
- package/lib/types/client/utils.d.ts +0 -22
- package/lib/types/host/actions.d.ts +0 -34
- package/lib/types/host/conversation.d.ts +0 -25
- package/lib/types/host/http.d.ts +0 -16
- package/lib/types/host/materials.d.ts +0 -32
- package/lib/types/host/persistence.d.ts +0 -31
- package/lib/types/host/routes.d.ts +0 -11
- package/lib/types/host/store.d.ts +0 -65
- package/lib/types/host/tools.d.ts +0 -28
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, sep } from "node:path";
|
|
6
6
|
import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
|
|
7
|
-
import {
|
|
7
|
+
import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
8
8
|
//#region src/mount-once.ts
|
|
9
9
|
/**
|
|
10
10
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
@@ -46,6 +46,59 @@ function mountOnce(packageName, fn) {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
//#endregion
|
|
49
|
+
//#region src/core/json.ts
|
|
50
|
+
/** 消息 → 落盘 JSON(可选字段无则省略;兼容保留 error)。 */
|
|
51
|
+
function messageJson(m) {
|
|
52
|
+
if (!m || typeof m.id !== "string") return null;
|
|
53
|
+
const o = {
|
|
54
|
+
id: m.id,
|
|
55
|
+
speaker: m.speaker,
|
|
56
|
+
text: String(m.text || ""),
|
|
57
|
+
ts: typeof m.ts === "number" ? m.ts : Date.now(),
|
|
58
|
+
seq: typeof m.seq === "number" ? m.seq : 0
|
|
59
|
+
};
|
|
60
|
+
if (m.model !== void 0) o.model = m.model;
|
|
61
|
+
if (m.reasoning !== void 0) o.reasoning = m.reasoning;
|
|
62
|
+
if (m.reasoningFull !== void 0) o.reasoningFull = m.reasoningFull;
|
|
63
|
+
if (m.thinkingSummary !== void 0) o.thinkingSummary = m.thinkingSummary;
|
|
64
|
+
if (m.error !== void 0) o.error = m.error;
|
|
65
|
+
if (typeof m.failedRoleId === "string" && m.failedRoleId) o.failedRoleId = m.failedRoleId;
|
|
66
|
+
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) o.toolCalls = m.toolCalls;
|
|
67
|
+
return o;
|
|
68
|
+
}
|
|
69
|
+
/** 角色 → 落盘 JSON(groupId 由目录归属,不再写入)。 */
|
|
70
|
+
function roleJson(r) {
|
|
71
|
+
const o = {
|
|
72
|
+
id: r.id,
|
|
73
|
+
name: String(r.name || "成员"),
|
|
74
|
+
persona: String(r.persona || ""),
|
|
75
|
+
provider: String(r.provider || ""),
|
|
76
|
+
model: String(r.model || ""),
|
|
77
|
+
enabled: r.enabled !== false,
|
|
78
|
+
thinking: r.thinking === true
|
|
79
|
+
};
|
|
80
|
+
if (r.color) o.color = r.color;
|
|
81
|
+
if (typeof r.temperature === "number" && !Number.isNaN(r.temperature)) o.temperature = r.temperature;
|
|
82
|
+
if (typeof r.reasoningEffort === "string" && r.reasoningEffort && r.reasoningEffort !== "default") o.reasoningEffort = r.reasoningEffort;
|
|
83
|
+
return o;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 宽容 JSON 提取(模型输出/供应商错误原文的统一解析入口):
|
|
87
|
+
* 取首个 { 至末个 } 的片段解析,仅接受普通对象;失败返回 null。
|
|
88
|
+
* 代码围栏剥离由调用方按需先行处理(供应商错误原文不剥)。
|
|
89
|
+
*/
|
|
90
|
+
function looseJson(raw) {
|
|
91
|
+
const start = raw.indexOf("{");
|
|
92
|
+
const end = raw.lastIndexOf("}");
|
|
93
|
+
if (start < 0 || end <= start) return null;
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(raw.slice(start, end + 1));
|
|
96
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
49
102
|
//#region src/core/errors.ts
|
|
50
103
|
const PREFIX = "模型输出异常终止: ";
|
|
51
104
|
const LEGACY_ROLE = /^角色「([^」]+)」发言失败[::]\s*/;
|
|
@@ -176,6 +229,7 @@ function createHostState(ctx) {
|
|
|
176
229
|
ctx,
|
|
177
230
|
llm: ctx.llm,
|
|
178
231
|
fs: ctx.fs,
|
|
232
|
+
shell: ctx.shell,
|
|
179
233
|
groups: /* @__PURE__ */ new Map(),
|
|
180
234
|
sessions: /* @__PURE__ */ new Map(),
|
|
181
235
|
roles: /* @__PURE__ */ new Map(),
|
|
@@ -190,7 +244,7 @@ function createHostState(ctx) {
|
|
|
190
244
|
queue: [],
|
|
191
245
|
pendingConfirm: null,
|
|
192
246
|
confirmSignal: null,
|
|
193
|
-
|
|
247
|
+
commandAbort: null,
|
|
194
248
|
finished: null,
|
|
195
249
|
replaceMessageId: null
|
|
196
250
|
},
|
|
@@ -226,7 +280,7 @@ function createActions(core, deps) {
|
|
|
226
280
|
const { llm, groups, sessions, roles, messages, run } = core;
|
|
227
281
|
const { touch, snapshot, schedulePersist, dropDirty, appendMessage, runLoop, wakeConfirm, killChild, browse, fileSearch, disposeFileSearch } = deps;
|
|
228
282
|
const mutate = (args) => {
|
|
229
|
-
const op = args
|
|
283
|
+
const op = args.op;
|
|
230
284
|
if (op === "createGroup") {
|
|
231
285
|
const g = {
|
|
232
286
|
id: core.nid("grp"),
|
|
@@ -1117,18 +1171,11 @@ function sanitizeConstraints(raw) {
|
|
|
1117
1171
|
* 解析折叠模型输出。null = 解析失败(水位不推);[] = 无新结论(水位推、备忘不动)。
|
|
1118
1172
|
*/
|
|
1119
1173
|
function parseConstraints(raw) {
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
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
|
-
}
|
|
1174
|
+
const o = looseJson(raw.replace(/```(?:json)?/g, ""));
|
|
1175
|
+
if (o === null) return null;
|
|
1176
|
+
if (!Object.prototype.hasOwnProperty.call(o, "constraints")) return null;
|
|
1177
|
+
if (!Array.isArray(o.constraints)) return null;
|
|
1178
|
+
return sanitizeConstraints(o.constraints);
|
|
1132
1179
|
}
|
|
1133
1180
|
/** 本批无用户消息时,新的已定/否决降为未决。 */
|
|
1134
1181
|
function downgradeWithoutUser(list, hasUser) {
|
|
@@ -1144,9 +1191,13 @@ function constraintBlock(list) {
|
|
|
1144
1191
|
return "\n# 已确认约束\n" + list.map((c) => "- " + KIND_LABEL[c.kind] + ":" + c.text).join("\n");
|
|
1145
1192
|
}
|
|
1146
1193
|
//#endregion
|
|
1147
|
-
//#region src/host/engine/
|
|
1148
|
-
/**
|
|
1149
|
-
|
|
1194
|
+
//#region src/host/engine/defaults.ts
|
|
1195
|
+
/**
|
|
1196
|
+
* 引擎共享小件:DSH 默认模型读取 + 发言人展示名(retitle / fold / conversation 共用)。
|
|
1197
|
+
* @module dsh-group-chat/host/engine/defaults
|
|
1198
|
+
*/
|
|
1199
|
+
/** DSH 默认模型(agentDefaultModel 服务缺位或未配置时返回 null,调用方静默跳过)。 */
|
|
1200
|
+
const defaultModel = (core) => {
|
|
1150
1201
|
try {
|
|
1151
1202
|
const svc = core.ctx.reflect.get("agentDefaultModel");
|
|
1152
1203
|
const sel = svc ? svc.currentSelection() : null;
|
|
@@ -1158,6 +1209,10 @@ const defaultModel$1 = (core) => {
|
|
|
1158
1209
|
return null;
|
|
1159
1210
|
}
|
|
1160
1211
|
};
|
|
1212
|
+
/** 发言人展示名(roles 表内查角色名;user/system 固定文案,见 speakerLabel)。 */
|
|
1213
|
+
const speakerNameOf = (roles, speaker) => speakerLabel(speaker, roles.get(speaker)?.name);
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region src/host/engine/fold.ts
|
|
1161
1216
|
/**
|
|
1162
1217
|
* 每轮 send 结束后折叠窗口外约束:fire-and-forget、不产生消息、静默失败。
|
|
1163
1218
|
* 同会话去重;会话已清空则 abort。内存 {constraints, constraintsUpToSeq} 一次挂上。
|
|
@@ -1166,12 +1221,11 @@ function createFold(core, deps) {
|
|
|
1166
1221
|
const { llm, messages, roles } = core;
|
|
1167
1222
|
const { touch, schedulePersist } = deps;
|
|
1168
1223
|
const folding = /* @__PURE__ */ new Set();
|
|
1169
|
-
const nameOf = (speaker) => speakerLabel(speaker, (roles.get(speaker) || { name: void 0 }).name);
|
|
1170
1224
|
return async (sess) => {
|
|
1171
1225
|
if (folding.has(sess.id)) return;
|
|
1172
1226
|
const squeezed = squeezedMessages(messages, sess);
|
|
1173
1227
|
if (!squeezed.length) return;
|
|
1174
|
-
const input = takeFoldBatch(squeezed, (m) =>
|
|
1228
|
+
const input = takeFoldBatch(squeezed, (m) => speakerNameOf(roles, m.speaker));
|
|
1175
1229
|
const watermark = squeezedMaxSeq(input.consumed);
|
|
1176
1230
|
if (watermark <= 0) return;
|
|
1177
1231
|
const commit = (next) => {
|
|
@@ -1186,7 +1240,7 @@ function createFold(core, deps) {
|
|
|
1186
1240
|
commit(void 0);
|
|
1187
1241
|
return;
|
|
1188
1242
|
}
|
|
1189
|
-
const dm = defaultModel
|
|
1243
|
+
const dm = defaultModel(core);
|
|
1190
1244
|
if (!dm) return;
|
|
1191
1245
|
folding.add(sess.id);
|
|
1192
1246
|
try {
|
|
@@ -1240,47 +1294,26 @@ function createFold(core, deps) {
|
|
|
1240
1294
|
//#region src/host/engine/retitle.ts
|
|
1241
1295
|
/** 仍是新建占位名(含改名之前的「会话 N」存量),自动标题尚未落地。 */
|
|
1242
1296
|
const isPlaceholderName = (name) => name === "新会话" || /^会话 \d+$/.test(name);
|
|
1243
|
-
/** DSH 默认模型(agentDefaultModel 服务缺位或未配置时返回 null,调用方静默跳过)。 */
|
|
1244
|
-
const defaultModel = (core) => {
|
|
1245
|
-
try {
|
|
1246
|
-
const svc = core.ctx.reflect.get("agentDefaultModel");
|
|
1247
|
-
const sel = svc ? svc.currentSelection() : null;
|
|
1248
|
-
return sel && typeof sel.provider === "string" && typeof sel.model === "string" && sel.provider && sel.model ? {
|
|
1249
|
-
provider: sel.provider,
|
|
1250
|
-
model: sel.model
|
|
1251
|
-
} : null;
|
|
1252
|
-
} catch {
|
|
1253
|
-
return null;
|
|
1254
|
-
}
|
|
1255
|
-
};
|
|
1256
1297
|
/** 命名输入:最近 40 条非系统消息的紧凑转写(每条 500 字符封顶,命名不需要全文)。 */
|
|
1257
1298
|
const titleTranscript = (core, sess) => {
|
|
1258
1299
|
const out = [];
|
|
1259
1300
|
for (const mid of sess.messageIds.slice(-40)) {
|
|
1260
1301
|
const m = core.messages.get(mid);
|
|
1261
1302
|
if (!m || m.speaker === "system" || m.error || !m.text) continue;
|
|
1262
|
-
|
|
1263
|
-
out.push("【" + name + "】" + m.text.replace(/\s+/g, " ").slice(0, 500));
|
|
1303
|
+
out.push("【" + speakerNameOf(core.roles, m.speaker) + "】" + m.text.replace(/\s+/g, " ").slice(0, 500));
|
|
1264
1304
|
}
|
|
1265
1305
|
return out.join("\n");
|
|
1266
1306
|
};
|
|
1267
|
-
/** 宽容解析模型输出:剥代码围栏 →
|
|
1307
|
+
/** 宽容解析模型输出:剥代码围栏 → looseJson → 校验并封顶字段。 */
|
|
1268
1308
|
const parseRetitle = (raw) => {
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
return {
|
|
1278
|
-
name: name ? name.slice(0, 24) : void 0,
|
|
1279
|
-
topic: topic ? topic.slice(0, 120) : void 0
|
|
1280
|
-
};
|
|
1281
|
-
} catch {
|
|
1282
|
-
return {};
|
|
1283
|
-
}
|
|
1309
|
+
const o = looseJson(raw.replace(/```(?:json)?/g, ""));
|
|
1310
|
+
if (!o) return {};
|
|
1311
|
+
const name = typeof o.name === "string" ? o.name.trim() : "";
|
|
1312
|
+
const topic = typeof o.topic === "string" ? o.topic.trim() : "";
|
|
1313
|
+
return {
|
|
1314
|
+
name: name ? name.slice(0, 24) : void 0,
|
|
1315
|
+
topic: topic ? topic.slice(0, 120) : void 0
|
|
1316
|
+
};
|
|
1284
1317
|
};
|
|
1285
1318
|
/**
|
|
1286
1319
|
* 每轮结束后根据聊天内容整理会话名称与主题:后台 fire-and-forget、不产生
|
|
@@ -1382,7 +1415,6 @@ function createConversation(core, deps) {
|
|
|
1382
1415
|
touch,
|
|
1383
1416
|
schedulePersist
|
|
1384
1417
|
});
|
|
1385
|
-
const nameOf = (speaker) => speakerLabel(speaker, (roles.get(speaker) || { name: void 0 }).name);
|
|
1386
1418
|
const appendMessage = (sess, speaker, text, extra) => {
|
|
1387
1419
|
const msg = {
|
|
1388
1420
|
id: core.nid("msg"),
|
|
@@ -1410,60 +1442,56 @@ function createConversation(core, deps) {
|
|
|
1410
1442
|
schedulePersist({ session: sess.id });
|
|
1411
1443
|
return msg;
|
|
1412
1444
|
};
|
|
1445
|
+
/** 原地覆盖既有消息(重试槽位);目标不存在或不属于本会话时返回 false,由调用方走追加。 */
|
|
1446
|
+
const overwriteMessage = (sess, replaceId, patch) => {
|
|
1447
|
+
if (!replaceId) return false;
|
|
1448
|
+
const existing = messages.get(replaceId);
|
|
1449
|
+
if (!existing || existing.sessionId !== sess.id) return false;
|
|
1450
|
+
Object.assign(existing, patch, { ts: Date.now() });
|
|
1451
|
+
touch();
|
|
1452
|
+
schedulePersist({ session: sess.id });
|
|
1453
|
+
return true;
|
|
1454
|
+
};
|
|
1413
1455
|
/** 把失败回合写成该角色的消息(speaker = 角色 id),不再用系统胶囊顶替。 */
|
|
1414
1456
|
const writeFailure = (sess, role, err, replaceId) => {
|
|
1415
1457
|
const raw = unwrapSpeakFailure(String(err && err.message || err));
|
|
1458
|
+
const model = role.provider + " / " + role.model;
|
|
1416
1459
|
const extra = {
|
|
1417
1460
|
error: true,
|
|
1418
1461
|
failedRoleId: role.id,
|
|
1419
|
-
model
|
|
1462
|
+
model
|
|
1420
1463
|
};
|
|
1421
|
-
if (replaceId
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
}
|
|
1464
|
+
if (overwriteMessage(sess, replaceId, {
|
|
1465
|
+
speaker: role.id,
|
|
1466
|
+
text: raw,
|
|
1467
|
+
...extra,
|
|
1468
|
+
reasoning: void 0,
|
|
1469
|
+
reasoningFull: void 0,
|
|
1470
|
+
thinkingSummary: void 0,
|
|
1471
|
+
toolCalls: void 0
|
|
1472
|
+
})) return;
|
|
1439
1473
|
appendMessage(sess, role.id, raw, extra);
|
|
1440
1474
|
};
|
|
1441
1475
|
/** 成功发言写入:replaceId 存在则原地覆盖失败卡。 */
|
|
1442
1476
|
const writeSuccess = (sess, role, out, replaceId) => {
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1477
|
+
const model = role.provider + " / " + role.model;
|
|
1478
|
+
const toolCalls = Array.isArray(out.toolCalls) && out.toolCalls.length > 0 ? out.toolCalls : void 0;
|
|
1479
|
+
if (overwriteMessage(sess, replaceId, {
|
|
1480
|
+
speaker: role.id,
|
|
1481
|
+
text: out.text,
|
|
1482
|
+
error: void 0,
|
|
1483
|
+
failedRoleId: void 0,
|
|
1484
|
+
model,
|
|
1485
|
+
reasoning: out.reasoning,
|
|
1486
|
+
reasoningFull: void 0,
|
|
1487
|
+
thinkingSummary: void 0,
|
|
1488
|
+
toolCalls
|
|
1489
|
+
})) return;
|
|
1490
|
+
appendMessage(sess, role.id, out.text, {
|
|
1491
|
+
model,
|
|
1445
1492
|
reasoning: out.reasoning,
|
|
1446
1493
|
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);
|
|
1494
|
+
});
|
|
1467
1495
|
};
|
|
1468
1496
|
/** 群聊记录 → 角色上下文块(最近 40 条 + 未折入临时原文)。失败卡不进上下文。重试截到该条之前。 */
|
|
1469
1497
|
const transcriptBlock = (sess, skipId) => {
|
|
@@ -1471,10 +1499,10 @@ function createConversation(core, deps) {
|
|
|
1471
1499
|
for (const mid of prefixIds(sess.messageIds, skipId).slice(-40)) {
|
|
1472
1500
|
const m = messages.get(mid);
|
|
1473
1501
|
if (!m || m.id === skipId || m.error) continue;
|
|
1474
|
-
out.push(formatTranscriptLine(m,
|
|
1502
|
+
out.push(formatTranscriptLine(m, speakerNameOf(roles, m.speaker)));
|
|
1475
1503
|
}
|
|
1476
1504
|
const live = out.join("\n\n");
|
|
1477
|
-
const temp = tempTranscript(squeezedMessages(messages, sess, skipId), (m) =>
|
|
1505
|
+
const temp = tempTranscript(squeezedMessages(messages, sess, skipId), (m) => speakerNameOf(roles, m.speaker));
|
|
1478
1506
|
if (!temp) return live;
|
|
1479
1507
|
if (!live) return temp;
|
|
1480
1508
|
return temp + "\n\n" + live;
|
|
@@ -1671,6 +1699,7 @@ function createConversation(core, deps) {
|
|
|
1671
1699
|
});
|
|
1672
1700
|
for (const tc of round.tcs) {
|
|
1673
1701
|
if (run.stopping) break;
|
|
1702
|
+
if (wsRoot === null) break;
|
|
1674
1703
|
const res = await tools.executeTool(g, wsRoot, tc);
|
|
1675
1704
|
const output = String(res.output || "");
|
|
1676
1705
|
toolCalls.push({
|
|
@@ -1718,12 +1747,13 @@ function createConversation(core, deps) {
|
|
|
1718
1747
|
};
|
|
1719
1748
|
};
|
|
1720
1749
|
const runLoop = async (sess, opts) => {
|
|
1721
|
-
const g = groups.get(sess.groupId);
|
|
1750
|
+
const g = groups.get(sess.groupId) ?? null;
|
|
1722
1751
|
const startCount = sess.messageIds.length;
|
|
1723
1752
|
let failed = false;
|
|
1724
1753
|
let replaceId = opts && opts.replaceMessageId;
|
|
1725
1754
|
run.replaceMessageId = replaceId || null;
|
|
1726
1755
|
try {
|
|
1756
|
+
if (!g) return;
|
|
1727
1757
|
while (run.queue.length > 0 && !run.stopping) {
|
|
1728
1758
|
const roleId = run.queue.shift();
|
|
1729
1759
|
const role = roles.get(roleId);
|
|
@@ -1770,7 +1800,7 @@ function createConversation(core, deps) {
|
|
|
1770
1800
|
run.queue = [];
|
|
1771
1801
|
run.pendingConfirm = null;
|
|
1772
1802
|
run.confirmSignal = null;
|
|
1773
|
-
run.
|
|
1803
|
+
run.commandAbort = null;
|
|
1774
1804
|
run.stopping = false;
|
|
1775
1805
|
run.replaceMessageId = null;
|
|
1776
1806
|
touch();
|
|
@@ -2049,43 +2079,6 @@ function createMaterials(core) {
|
|
|
2049
2079
|
};
|
|
2050
2080
|
}
|
|
2051
2081
|
//#endregion
|
|
2052
|
-
//#region src/core/json.ts
|
|
2053
|
-
/** 消息 → 落盘 JSON(可选字段无则省略;兼容保留 error)。 */
|
|
2054
|
-
function messageJson(m) {
|
|
2055
|
-
if (!m || typeof m.id !== "string") return null;
|
|
2056
|
-
const o = {
|
|
2057
|
-
id: m.id,
|
|
2058
|
-
speaker: m.speaker,
|
|
2059
|
-
text: String(m.text || ""),
|
|
2060
|
-
ts: typeof m.ts === "number" ? m.ts : Date.now(),
|
|
2061
|
-
seq: typeof m.seq === "number" ? m.seq : 0
|
|
2062
|
-
};
|
|
2063
|
-
if (m.model !== void 0) o.model = m.model;
|
|
2064
|
-
if (m.reasoning !== void 0) o.reasoning = m.reasoning;
|
|
2065
|
-
if (m.reasoningFull !== void 0) o.reasoningFull = m.reasoningFull;
|
|
2066
|
-
if (m.thinkingSummary !== void 0) o.thinkingSummary = m.thinkingSummary;
|
|
2067
|
-
if (m.error !== void 0) o.error = m.error;
|
|
2068
|
-
if (typeof m.failedRoleId === "string" && m.failedRoleId) o.failedRoleId = m.failedRoleId;
|
|
2069
|
-
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) o.toolCalls = m.toolCalls;
|
|
2070
|
-
return o;
|
|
2071
|
-
}
|
|
2072
|
-
/** 角色 → 落盘 JSON(groupId 由目录归属,不再写入)。 */
|
|
2073
|
-
function roleJson(r) {
|
|
2074
|
-
const o = {
|
|
2075
|
-
id: r.id,
|
|
2076
|
-
name: String(r.name || "成员"),
|
|
2077
|
-
persona: String(r.persona || ""),
|
|
2078
|
-
provider: String(r.provider || ""),
|
|
2079
|
-
model: String(r.model || ""),
|
|
2080
|
-
enabled: r.enabled !== false,
|
|
2081
|
-
thinking: r.thinking === true
|
|
2082
|
-
};
|
|
2083
|
-
if (r.color) o.color = r.color;
|
|
2084
|
-
if (typeof r.temperature === "number" && !Number.isNaN(r.temperature)) o.temperature = r.temperature;
|
|
2085
|
-
if (typeof r.reasoningEffort === "string" && r.reasoningEffort && r.reasoningEffort !== "default") o.reasoningEffort = r.reasoningEffort;
|
|
2086
|
-
return o;
|
|
2087
|
-
}
|
|
2088
|
-
//#endregion
|
|
2089
2082
|
//#region src/host/persistence/store.ts
|
|
2090
2083
|
/**
|
|
2091
2084
|
* 持久化(PERSISTENCE.md v2.1:会话级文件隔离)。
|
|
@@ -2198,7 +2191,11 @@ var Store = class {
|
|
|
2198
2191
|
}
|
|
2199
2192
|
} catch {}
|
|
2200
2193
|
}
|
|
2201
|
-
/**
|
|
2194
|
+
/**
|
|
2195
|
+
* 同步原子写(tmp+fsync+rename):仅供构造期 v1 迁移(migrateV1)与测试
|
|
2196
|
+
* 直用;运行时 flush 走 persistence.ts 的 writeFileAtomic(异步、wx 独占
|
|
2197
|
+
* 创建 + 随机后缀 + 符号链接安全,无 per-file fsync,对齐 DSH 基座标准)。
|
|
2198
|
+
*/
|
|
2202
2199
|
atomicWrite(file, text) {
|
|
2203
2200
|
mkdirSync(dirname(file), { recursive: true });
|
|
2204
2201
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
@@ -2441,6 +2438,11 @@ function emptyGroup(id, name) {
|
|
|
2441
2438
|
* 锁失败/写失败时降级为内存态运行(console 告警)。
|
|
2442
2439
|
* @module dsh-group-chat/host/persistence
|
|
2443
2440
|
*/
|
|
2441
|
+
/** writeFileAtomic 权限位:私有数据 0o600、目录 0o700(对齐 Store.atomicWrite)。 */
|
|
2442
|
+
const WRITE_FILE_OPTS = {
|
|
2443
|
+
mode: 384,
|
|
2444
|
+
dirMode: 448
|
|
2445
|
+
};
|
|
2444
2446
|
/**
|
|
2445
2447
|
* 创建持久化面:构造即完成 store 初始化 + hydrate(v1 迁移 → 残留清理 →
|
|
2446
2448
|
* 清单 → 群组数据 → 会话文件;缺文件空重建;补建默认会话/群组立即落盘)。
|
|
@@ -2453,11 +2455,15 @@ function createPersistence(core) {
|
|
|
2453
2455
|
core.store = null;
|
|
2454
2456
|
}
|
|
2455
2457
|
const store = () => core.store;
|
|
2456
|
-
const dirtySessions = /* @__PURE__ */ new
|
|
2457
|
-
const dirtyRoles = /* @__PURE__ */ new
|
|
2458
|
-
const dirtyWorkspace = /* @__PURE__ */ new
|
|
2459
|
-
let ledgerDirty =
|
|
2458
|
+
const dirtySessions = /* @__PURE__ */ new Map();
|
|
2459
|
+
const dirtyRoles = /* @__PURE__ */ new Map();
|
|
2460
|
+
const dirtyWorkspace = /* @__PURE__ */ new Map();
|
|
2461
|
+
let ledgerDirty = 0;
|
|
2460
2462
|
let flushScheduled = false;
|
|
2463
|
+
let flushChain = Promise.resolve();
|
|
2464
|
+
const mark = (map, key) => {
|
|
2465
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
2466
|
+
};
|
|
2461
2467
|
const ledgerDocument = () => ({
|
|
2462
2468
|
schema: 3,
|
|
2463
2469
|
savedAt: Date.now(),
|
|
@@ -2490,74 +2496,85 @@ function createPersistence(core) {
|
|
|
2490
2496
|
savedAt: Date.now(),
|
|
2491
2497
|
roles: g.roleIds.map((rid) => core.roles.get(rid)).filter((r) => Boolean(r)).map((r) => roleJson(r))
|
|
2492
2498
|
});
|
|
2493
|
-
/**
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2499
|
+
/**
|
|
2500
|
+
* 异步 flush(writeFileAtomic:wx 独占创建 + 随机后缀 tmp + rename,无
|
|
2501
|
+
* per-file fsync——崩溃持久性对齐 DSH 基座标准):写全部脏文件;写成功且
|
|
2502
|
+
* 写入期间未被重新标脏(版本未变)才清脏标记;每个实际发生 rename 的
|
|
2503
|
+
* 目录一次 fsync(调用侧批量执行)。
|
|
2504
|
+
*/
|
|
2505
|
+
const flushAll = async (s) => {
|
|
2497
2506
|
const fsyncDirs = /* @__PURE__ */ new Set();
|
|
2498
|
-
for (const sid of [...dirtySessions]) {
|
|
2507
|
+
for (const [sid, v] of [...dirtySessions]) {
|
|
2499
2508
|
const sess = core.sessions.get(sid);
|
|
2500
2509
|
if (!sess) {
|
|
2501
2510
|
dirtySessions.delete(sid);
|
|
2502
2511
|
continue;
|
|
2503
2512
|
}
|
|
2504
2513
|
try {
|
|
2505
|
-
|
|
2506
|
-
dirtySessions.delete(sid);
|
|
2514
|
+
await writeFileAtomic(s.sessionFile(sess.groupId, sess.id), JSON.stringify(sessionDocument(sess)), WRITE_FILE_OPTS);
|
|
2515
|
+
if (dirtySessions.get(sid) === v) dirtySessions.delete(sid);
|
|
2507
2516
|
fsyncDirs.add(s.sessionsDir(sess.groupId));
|
|
2508
2517
|
} catch (e) {
|
|
2509
2518
|
console.error(`[dsh-group-chat] 会话 ${sid} 落盘失败(保留脏标记待重试):`, e);
|
|
2510
2519
|
}
|
|
2511
2520
|
}
|
|
2512
|
-
for (const gid of [...dirtyRoles]) {
|
|
2521
|
+
for (const [gid, v] of [...dirtyRoles]) {
|
|
2513
2522
|
const g = core.groups.get(gid);
|
|
2514
2523
|
if (!g) {
|
|
2515
2524
|
dirtyRoles.delete(gid);
|
|
2516
2525
|
continue;
|
|
2517
2526
|
}
|
|
2518
2527
|
try {
|
|
2519
|
-
|
|
2520
|
-
dirtyRoles.delete(gid);
|
|
2528
|
+
await writeFileAtomic(s.rolesFile(gid), JSON.stringify(rolesDocument(g)), WRITE_FILE_OPTS);
|
|
2529
|
+
if (dirtyRoles.get(gid) === v) dirtyRoles.delete(gid);
|
|
2521
2530
|
fsyncDirs.add(s.groupDir(gid));
|
|
2522
2531
|
} catch (e) {
|
|
2523
2532
|
console.error(`[dsh-group-chat] 群组 ${gid} roles.json 落盘失败(保留脏标记待重试):`, e);
|
|
2524
2533
|
}
|
|
2525
2534
|
}
|
|
2526
|
-
for (const gid of [...dirtyWorkspace]) {
|
|
2535
|
+
for (const [gid, v] of [...dirtyWorkspace]) {
|
|
2527
2536
|
const g = core.groups.get(gid);
|
|
2528
2537
|
if (!g) {
|
|
2529
2538
|
dirtyWorkspace.delete(gid);
|
|
2530
2539
|
continue;
|
|
2531
2540
|
}
|
|
2532
2541
|
try {
|
|
2533
|
-
|
|
2534
|
-
dirtyWorkspace.delete(gid);
|
|
2542
|
+
await writeFileAtomic(s.workspaceFile(gid), (g.workspaceDir || "") + "\n", WRITE_FILE_OPTS);
|
|
2543
|
+
if (dirtyWorkspace.get(gid) === v) dirtyWorkspace.delete(gid);
|
|
2535
2544
|
fsyncDirs.add(s.groupDir(gid));
|
|
2536
2545
|
} catch (e) {
|
|
2537
2546
|
console.error(`[dsh-group-chat] 群组 ${gid} workspaceDir 落盘失败(保留脏标记待重试):`, e);
|
|
2538
2547
|
}
|
|
2539
2548
|
}
|
|
2540
|
-
if (ledgerDirty)
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2549
|
+
if (ledgerDirty > 0) {
|
|
2550
|
+
const v = ledgerDirty;
|
|
2551
|
+
try {
|
|
2552
|
+
await writeFileAtomic(s.ledgerFile, JSON.stringify(ledgerDocument(), null, 2), WRITE_FILE_OPTS);
|
|
2553
|
+
if (ledgerDirty === v) ledgerDirty = 0;
|
|
2554
|
+
fsyncDirs.add(s.dir);
|
|
2555
|
+
} catch (e) {
|
|
2556
|
+
console.error("[dsh-group-chat] ledger.json 落盘失败(保留脏标记待重试):", e);
|
|
2557
|
+
}
|
|
2546
2558
|
}
|
|
2547
2559
|
for (const d of fsyncDirs) s.fsyncDir(d);
|
|
2548
2560
|
};
|
|
2549
2561
|
/** 事件驱动落盘;同一 tick 内多次变更合并为一次写。 */
|
|
2550
2562
|
const schedulePersist = ({ ledger = false, session = null, roles: roleGroup = null, workspace = null } = {}) => {
|
|
2551
2563
|
if (store() === null) return;
|
|
2552
|
-
if (ledger) ledgerDirty
|
|
2553
|
-
if (session) dirtySessions
|
|
2554
|
-
if (roleGroup) dirtyRoles
|
|
2555
|
-
if (workspace) dirtyWorkspace
|
|
2564
|
+
if (ledger) ledgerDirty++;
|
|
2565
|
+
if (session) mark(dirtySessions, session);
|
|
2566
|
+
if (roleGroup) mark(dirtyRoles, roleGroup);
|
|
2567
|
+
if (workspace) mark(dirtyWorkspace, workspace);
|
|
2556
2568
|
if (flushScheduled) return;
|
|
2557
2569
|
flushScheduled = true;
|
|
2558
2570
|
Promise.resolve().then(() => {
|
|
2559
2571
|
flushScheduled = false;
|
|
2560
|
-
|
|
2572
|
+
flushChain = flushChain.then(async () => {
|
|
2573
|
+
const s = store();
|
|
2574
|
+
if (s !== null) await flushAll(s);
|
|
2575
|
+
}).catch((e) => {
|
|
2576
|
+
console.error("[dsh-group-chat] 落盘 flush 异常(脏标记保留待重试):", e);
|
|
2577
|
+
});
|
|
2561
2578
|
});
|
|
2562
2579
|
};
|
|
2563
2580
|
const dropDirty = ({ session = null, roles: roleGroup = null, workspace = null }) => {
|
|
@@ -2717,18 +2734,18 @@ function createPersistence(core) {
|
|
|
2717
2734
|
hydrate();
|
|
2718
2735
|
return {
|
|
2719
2736
|
schedulePersist,
|
|
2720
|
-
flushNow,
|
|
2721
2737
|
dropDirty,
|
|
2722
|
-
release() {
|
|
2738
|
+
async release() {
|
|
2723
2739
|
const s = store();
|
|
2724
2740
|
if (s === null) return;
|
|
2741
|
+
core.store = null;
|
|
2725
2742
|
try {
|
|
2726
|
-
|
|
2743
|
+
await flushChain;
|
|
2744
|
+
await flushAll(s);
|
|
2727
2745
|
} catch {}
|
|
2728
2746
|
try {
|
|
2729
2747
|
s.release();
|
|
2730
2748
|
} catch {}
|
|
2731
|
-
core.store = null;
|
|
2732
2749
|
}
|
|
2733
2750
|
};
|
|
2734
2751
|
}
|
|
@@ -2737,12 +2754,13 @@ function createPersistence(core) {
|
|
|
2737
2754
|
/**
|
|
2738
2755
|
* 工具执行(TOOLS.md §2:沙箱 / §3:确认闸门):read_file / list_dir /
|
|
2739
2756
|
* run_command 三件套;realpath 硬边界 + 分隔符比较;run_command 按群组
|
|
2740
|
-
*
|
|
2757
|
+
* 权限档位走逐条确认或直接执行,经 `shell` 服务(ctx.shell 沙箱执行器)
|
|
2758
|
+
* 以 per-call sandboxPolicy 收紧到群工作区。
|
|
2741
2759
|
* @module dsh-group-chat/host/tools
|
|
2742
2760
|
*/
|
|
2743
2761
|
/** 创建工具面。 */
|
|
2744
2762
|
function createTools(core, touch) {
|
|
2745
|
-
const { run } = core;
|
|
2763
|
+
const { run, shell } = core;
|
|
2746
2764
|
/** realpath 硬边界:目标必须在群组工作区内(带分隔符比较,防 /ws/foo 放行 /ws/foobar)。 */
|
|
2747
2765
|
const resolveInWorkspace = (root, rawPath) => {
|
|
2748
2766
|
const p = rawPath === void 0 || rawPath === null || String(rawPath).trim() === "" ? "." : String(rawPath);
|
|
@@ -2829,54 +2847,50 @@ function createTools(core, touch) {
|
|
|
2829
2847
|
};
|
|
2830
2848
|
}
|
|
2831
2849
|
};
|
|
2832
|
-
|
|
2833
|
-
|
|
2850
|
+
/**
|
|
2851
|
+
* 经 `shell` 服务执行命令(TOOLS.md §2):cwd 与沙箱均收紧到群工作区根
|
|
2852
|
+
* (workspace_write 档经 sandboxPolicy 强制;full_access 档对齐 DSH
|
|
2853
|
+
* danger-full-access 语义免受限)。超时/中止由执行器 kill 进程并按首因
|
|
2854
|
+
* 分类报告;runner 失效(如 SANDBOX_UNAVAILABLE)经 catch 报错不执行。
|
|
2855
|
+
*/
|
|
2856
|
+
const runCommandTool = async (g, root, command) => {
|
|
2857
|
+
const abort = new AbortController();
|
|
2858
|
+
run.commandAbort = abort;
|
|
2834
2859
|
try {
|
|
2835
|
-
|
|
2860
|
+
const spec = shell.resolve({
|
|
2861
|
+
command,
|
|
2862
|
+
workdir: root,
|
|
2863
|
+
timeoutMs: RUN_CMD_TIMEOUT_MS,
|
|
2864
|
+
stdoutMaxBytes: CMD_CAPTURE_MAX_BYTES,
|
|
2865
|
+
sandboxPolicy: g.permissionTier === "full_access" ? {
|
|
2866
|
+
mode: "danger-full-access",
|
|
2867
|
+
workspaceRoot: root
|
|
2868
|
+
} : {
|
|
2869
|
+
mode: "workspace-write",
|
|
2870
|
+
workspaceRoot: root
|
|
2871
|
+
},
|
|
2872
|
+
signal: abort.signal
|
|
2873
|
+
});
|
|
2874
|
+
const r = await shell.run(spec);
|
|
2875
|
+
let text = r.stdout.text + r.stderr.text;
|
|
2876
|
+
if (r.stdout.truncated || r.stderr.truncated) text += "\n[输出采集超限,已截断(保留末尾)]";
|
|
2877
|
+
if (r.sandbox?.denied) text += "\n[沙箱拦截了越界的文件操作]";
|
|
2878
|
+
if (r.timedOut) text += "\n[执行超时(" + Math.round(RUN_CMD_TIMEOUT_MS / 1e3) + "s),已强制终止]";
|
|
2879
|
+
if (r.exitCode !== 0 && r.exitCode !== null && !r.timedOut) text += "\n[退出码 " + r.exitCode + "]";
|
|
2880
|
+
if (text.length > 8e3) text = text.slice(0, CMD_OUTPUT_MAX_CHARS) + "\n…(输出超长,已截断)";
|
|
2881
|
+
return {
|
|
2882
|
+
status: r.exitCode === 0 ? "ok" : "error",
|
|
2883
|
+
output: text || "(无输出)"
|
|
2884
|
+
};
|
|
2836
2885
|
} catch (e) {
|
|
2837
|
-
|
|
2886
|
+
return {
|
|
2838
2887
|
status: "error",
|
|
2839
2888
|
output: "无法启动命令:" + String(e && e.message || e)
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2889
|
+
};
|
|
2890
|
+
} finally {
|
|
2891
|
+
if (run.commandAbort === abort) run.commandAbort = null;
|
|
2842
2892
|
}
|
|
2843
|
-
|
|
2844
|
-
let out = "";
|
|
2845
|
-
let dropped = 0;
|
|
2846
|
-
let timedOut = false;
|
|
2847
|
-
const onChunk = (chunk) => {
|
|
2848
|
-
if (out.length < 1048576) out += chunk.toString("utf8");
|
|
2849
|
-
else dropped += chunk.length;
|
|
2850
|
-
};
|
|
2851
|
-
child.stdout?.on("data", onChunk);
|
|
2852
|
-
child.stderr?.on("data", onChunk);
|
|
2853
|
-
const timer = setTimeout(() => {
|
|
2854
|
-
timedOut = true;
|
|
2855
|
-
try {
|
|
2856
|
-
child.kill("SIGKILL");
|
|
2857
|
-
} catch {}
|
|
2858
|
-
}, RUN_CMD_TIMEOUT_MS);
|
|
2859
|
-
const finish = (result) => {
|
|
2860
|
-
clearTimeout(timer);
|
|
2861
|
-
if (run.childProc === child) run.childProc = null;
|
|
2862
|
-
resolve(result);
|
|
2863
|
-
};
|
|
2864
|
-
child.on("error", (e) => finish({
|
|
2865
|
-
status: "error",
|
|
2866
|
-
output: "执行失败:" + String(e && e.message || e)
|
|
2867
|
-
}));
|
|
2868
|
-
child.on("close", (code) => {
|
|
2869
|
-
let text = out.length > 1048576 ? out.slice(0, CMD_CAPTURE_MAX_BYTES) : out;
|
|
2870
|
-
if (dropped > 0) text += "\n[输出采集超限,已丢弃 " + dropped + " 字节]";
|
|
2871
|
-
if (timedOut) text += "\n[执行超时(" + Math.round(RUN_CMD_TIMEOUT_MS / 1e3) + "s),已强制终止]";
|
|
2872
|
-
if (code !== 0 && code !== null && !timedOut) text += "\n[退出码 " + code + "]";
|
|
2873
|
-
if (text.length > 8e3) text = text.slice(0, CMD_OUTPUT_MAX_CHARS) + "\n…(输出超长,已截断)";
|
|
2874
|
-
finish({
|
|
2875
|
-
status: code === 0 ? "ok" : "error",
|
|
2876
|
-
output: text || "(无输出)"
|
|
2877
|
-
});
|
|
2878
|
-
});
|
|
2879
|
-
});
|
|
2893
|
+
};
|
|
2880
2894
|
/** run_command 确认闸门:置 pendingConfirm 后无限等待,confirmCommand/stop/dispose 唤醒。 */
|
|
2881
2895
|
const requestConfirmation = (toolCallId, args) => new Promise((resolve) => {
|
|
2882
2896
|
run.pendingConfirm = {
|
|
@@ -2896,8 +2910,8 @@ function createTools(core, touch) {
|
|
|
2896
2910
|
} catch {}
|
|
2897
2911
|
};
|
|
2898
2912
|
const killChild = () => {
|
|
2899
|
-
|
|
2900
|
-
run.
|
|
2913
|
+
try {
|
|
2914
|
+
run.commandAbort?.abort();
|
|
2901
2915
|
} catch {}
|
|
2902
2916
|
};
|
|
2903
2917
|
const executeTool = async (g, root, tc) => {
|
|
@@ -2925,8 +2939,8 @@ function createTools(core, touch) {
|
|
|
2925
2939
|
status: "denied",
|
|
2926
2940
|
output: "用户拒绝了这次命令执行"
|
|
2927
2941
|
};
|
|
2928
|
-
else res = await runCommandTool(root, String(args.command || ""));
|
|
2929
|
-
} else res = await runCommandTool(root, String(args.command || ""));
|
|
2942
|
+
else res = await runCommandTool(g, root, String(args.command || ""));
|
|
2943
|
+
} else res = await runCommandTool(g, root, String(args.command || ""));
|
|
2930
2944
|
} else res = {
|
|
2931
2945
|
status: "error",
|
|
2932
2946
|
output: "未知工具:" + tc.name
|
|
@@ -2996,7 +3010,7 @@ function createGroupChatService(ctx) {
|
|
|
2996
3010
|
tools.killChild();
|
|
2997
3011
|
materials.disposeFileSearch();
|
|
2998
3012
|
bus.dispose();
|
|
2999
|
-
persist.release();
|
|
3013
|
+
return persist.release();
|
|
3000
3014
|
}
|
|
3001
3015
|
};
|
|
3002
3016
|
}
|
|
@@ -3006,6 +3020,7 @@ const name = "group-chat";
|
|
|
3006
3020
|
const inject = [
|
|
3007
3021
|
"llm",
|
|
3008
3022
|
"fs",
|
|
3023
|
+
"shell",
|
|
3009
3024
|
"webServer",
|
|
3010
3025
|
"workspaceRegistry"
|
|
3011
3026
|
];
|
|
@@ -3025,9 +3040,7 @@ const apply = mountOnce("dsh-group-chat", (ctx, config) => {
|
|
|
3025
3040
|
for (const dispose of disposers) dispose();
|
|
3026
3041
|
};
|
|
3027
3042
|
}, "group-chat: host API routes");
|
|
3028
|
-
ctx.effect(() => () =>
|
|
3029
|
-
service.dispose();
|
|
3030
|
-
});
|
|
3043
|
+
ctx.effect(() => () => service.dispose());
|
|
3031
3044
|
let current = () => config ?? {};
|
|
3032
3045
|
const sync = () => {
|
|
3033
3046
|
if (!(current().enabled ?? true)) service.stopAll();
|