@michengai/dsh-codex-ui 0.2.101 → 0.2.102

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.
@@ -1,10 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
3
+ import { lstat, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { basename, dirname, isAbsolute, join, resolve, sep, win32 } from "node:path";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { randomUUID } from "node:crypto";
7
+ import { randomBytes, randomUUID } from "node:crypto";
8
8
  //#region src/dependencies.ts
9
9
  const SUITE_PACKAGE = "@michengai/dsh-codex-suite";
10
10
  const SUITE_MEMBER_PACKAGES = [
@@ -820,11 +820,26 @@ function requireService(ctx, key, method) {
820
820
  }
821
821
  /** 在唯一的宿主边界校验服务能力;宿主 API 变更时立即失败,不会静默返回 503。 */
822
822
  function hostServices(ctx) {
823
+ const emit = ctx.emit;
823
824
  return {
824
825
  webServer: requireService(ctx, "webServer", "register"),
825
826
  agents: requireService(ctx, "agents", "get"),
827
+ sessions: requireService(ctx, "sessions", "get"),
828
+ sessionPersistence: requireService(ctx, "sessionPersistence", "list"),
826
829
  tools: requireService(ctx, "tools", "schemas"),
827
- workspaceRegistry: requireService(ctx, "workspaceRegistry", "list")
830
+ workspaceRegistry: requireService(ctx, "workspaceRegistry", "list"),
831
+ sessionProjectionCache: ctx.get("sessionProjectionCache"),
832
+ emit: (event, ...args) => {
833
+ emit.call(ctx, event, ...args);
834
+ },
835
+ logger: {
836
+ warn: (message) => {
837
+ ctx.logger.warn("%s", message);
838
+ },
839
+ info: (message) => {
840
+ ctx.logger.info("%s", message);
841
+ }
842
+ }
828
843
  };
829
844
  }
830
845
  //#endregion
@@ -1046,6 +1061,409 @@ var ForegroundExplorer = class {
1046
1061
  }
1047
1062
  };
1048
1063
  //#endregion
1064
+ //#region src/session-migration.ts
1065
+ var SessionMoveError = class extends Error {
1066
+ code;
1067
+ constructor(code, message, options) {
1068
+ super(message, options);
1069
+ this.code = code;
1070
+ this.name = "SessionMoveError";
1071
+ }
1072
+ };
1073
+ const movingSessions = /* @__PURE__ */ new Set();
1074
+ function rawSessionIds(workspace) {
1075
+ return Array.isArray(workspace.record?.sessionIds) ? workspace.record.sessionIds : workspace.sessionIds;
1076
+ }
1077
+ function validIdentifier(value) {
1078
+ return value.length > 0 && value.length <= 512 && !/[\u0000-\u001f\u007f]/.test(value);
1079
+ }
1080
+ async function pathExists(path) {
1081
+ try {
1082
+ await lstat(path);
1083
+ return true;
1084
+ } catch (error) {
1085
+ if (error.code === "ENOENT") return false;
1086
+ throw error;
1087
+ }
1088
+ }
1089
+ async function writeTemporaryFile(finalPath, data) {
1090
+ const temporaryPath = `${finalPath}.${randomBytes(6).toString("hex")}.tmp`;
1091
+ const handle = await open(temporaryPath, "wx", 384);
1092
+ try {
1093
+ await handle.writeFile(data);
1094
+ await handle.sync();
1095
+ } finally {
1096
+ await handle.close();
1097
+ }
1098
+ return temporaryPath;
1099
+ }
1100
+ async function encodeSessionArtifact(headerLine, body, zstd) {
1101
+ if (!zstd) return Buffer.from(`${headerLine}\n${body}`, "utf8");
1102
+ const { zstdCompress } = await import("node:zlib");
1103
+ if (typeof zstdCompress !== "function") throw new SessionMoveError("session-move/zstd-unavailable", "当前 Node.js 运行时不支持 Zstd 会话迁移。");
1104
+ const compress = (data) => new Promise((resolvePromise, rejectPromise) => {
1105
+ zstdCompress(data, (error, output) => {
1106
+ if (error === null) resolvePromise(output);
1107
+ else rejectPromise(error);
1108
+ });
1109
+ });
1110
+ const headerFrame = await compress(Buffer.from(`${headerLine}\n`, "utf8"));
1111
+ if (body === "") return headerFrame;
1112
+ const bodyFrame = await compress(Buffer.from(body, "utf8"));
1113
+ return Buffer.concat([headerFrame, bodyFrame]);
1114
+ }
1115
+ var ArtifactDirectoryMove = class {
1116
+ oldArtifact;
1117
+ newArtifact;
1118
+ directoryMoved = false;
1119
+ backupCreated = false;
1120
+ published = false;
1121
+ temporaryPath;
1122
+ oldDirectory;
1123
+ newDirectory;
1124
+ movedOldArtifact;
1125
+ backupArtifact;
1126
+ constructor(oldArtifact, newArtifact) {
1127
+ this.oldArtifact = oldArtifact;
1128
+ this.newArtifact = newArtifact;
1129
+ this.oldDirectory = dirname(oldArtifact);
1130
+ this.newDirectory = dirname(newArtifact);
1131
+ const relativeArtifact = relative(this.oldDirectory, oldArtifact);
1132
+ if (relativeArtifact.startsWith("..") || resolve(this.oldDirectory, relativeArtifact) !== resolve(oldArtifact)) throw new SessionMoveError("session-move/path-invalid", "会话工件路径无效。");
1133
+ this.movedOldArtifact = join(this.newDirectory, relativeArtifact);
1134
+ this.backupArtifact = join(this.newDirectory, `${basename(oldArtifact)}.${randomBytes(6).toString("hex")}.dcu-backup`);
1135
+ }
1136
+ async publish(bytes) {
1137
+ if (resolve(this.oldDirectory).toLowerCase() === resolve(this.newDirectory).toLowerCase()) throw new SessionMoveError("session-move/path-conflict", "源项目和目标项目使用了相同的会话目录。");
1138
+ if (await pathExists(this.newDirectory)) throw new SessionMoveError("session-move/destination-occupied", "目标项目已经存在同名会话工件。");
1139
+ try {
1140
+ await mkdir(dirname(this.newDirectory), { recursive: true });
1141
+ await rename(this.oldDirectory, this.newDirectory);
1142
+ this.directoryMoved = true;
1143
+ await rename(this.movedOldArtifact, this.backupArtifact);
1144
+ this.backupCreated = true;
1145
+ this.temporaryPath = await writeTemporaryFile(this.newArtifact, bytes);
1146
+ await rename(this.temporaryPath, this.newArtifact);
1147
+ this.temporaryPath = void 0;
1148
+ this.published = true;
1149
+ } catch (error) {
1150
+ try {
1151
+ await this.rollback();
1152
+ } catch (rollbackError) {
1153
+ throw new SessionMoveError("session-move/rollback-failed", "迁移会话工件失败,且自动回滚未完整完成。", { cause: rollbackError });
1154
+ }
1155
+ if (error instanceof SessionMoveError) throw error;
1156
+ throw new SessionMoveError("session-move/artifact-failed", "迁移会话工件失败,原会话已恢复。", { cause: error });
1157
+ }
1158
+ }
1159
+ async rollback() {
1160
+ const failures = [];
1161
+ if (this.temporaryPath !== void 0) {
1162
+ try {
1163
+ await rm(this.temporaryPath, { force: true });
1164
+ } catch (error) {
1165
+ failures.push(error);
1166
+ }
1167
+ this.temporaryPath = void 0;
1168
+ }
1169
+ if (this.published) try {
1170
+ await rm(this.newArtifact, { force: true });
1171
+ this.published = false;
1172
+ } catch (error) {
1173
+ failures.push(error);
1174
+ }
1175
+ if (this.backupCreated) try {
1176
+ await rename(this.backupArtifact, this.movedOldArtifact);
1177
+ this.backupCreated = false;
1178
+ } catch (error) {
1179
+ failures.push(error);
1180
+ }
1181
+ if (this.directoryMoved) try {
1182
+ await mkdir(dirname(this.oldDirectory), { recursive: true });
1183
+ await rename(this.newDirectory, this.oldDirectory);
1184
+ this.directoryMoved = false;
1185
+ } catch (error) {
1186
+ failures.push(error);
1187
+ }
1188
+ if (failures.length > 0) throw new AggregateError(failures, "会话工件回滚失败");
1189
+ }
1190
+ async commit(logger) {
1191
+ if (!this.backupCreated) return;
1192
+ try {
1193
+ await rm(this.backupArtifact, { force: true });
1194
+ this.backupCreated = false;
1195
+ } catch (error) {
1196
+ logger?.warn(`会话迁移成功,但旧工件备份清理失败:${String(error)}`);
1197
+ }
1198
+ }
1199
+ };
1200
+ function workspaceSnapshots(workspaces, sessionId) {
1201
+ return workspaces.map((workspace) => {
1202
+ const ids = [...rawSessionIds(workspace)];
1203
+ const index = ids.indexOf(sessionId);
1204
+ return {
1205
+ workspace,
1206
+ contained: index >= 0,
1207
+ beforeId: index >= 0 ? ids[index + 1] : void 0
1208
+ };
1209
+ });
1210
+ }
1211
+ async function restoreWorkspaceSnapshots(snapshots, sessionId) {
1212
+ for (const snapshot of snapshots) if (!snapshot.contained && rawSessionIds(snapshot.workspace).includes(sessionId)) await snapshot.workspace.detachSession(sessionId);
1213
+ for (const snapshot of snapshots) {
1214
+ if (!snapshot.contained) continue;
1215
+ if (!rawSessionIds(snapshot.workspace).includes(sessionId)) await snapshot.workspace.attachSession(sessionId);
1216
+ const currentIds = rawSessionIds(snapshot.workspace);
1217
+ const beforeId = snapshot.beforeId !== void 0 && currentIds.includes(snapshot.beforeId) ? snapshot.beforeId : void 0;
1218
+ await snapshot.workspace.insertSessionBefore(sessionId, beforeId);
1219
+ }
1220
+ }
1221
+ async function enterStoredSession(services, sessionId, stored) {
1222
+ const preparation = services.sessionPersistence.prepare === void 0 ? void 0 : await services.sessionPersistence.prepare(sessionId);
1223
+ const session = preparation?.session ?? services.sessions.prepare(sessionId, {
1224
+ seedSource: "persistence",
1225
+ seed: stored.events,
1226
+ meta: stored.meta,
1227
+ inheritedEventCount: stored.inheritedEventCount
1228
+ });
1229
+ const detach = services.sessions.enter(session);
1230
+ try {
1231
+ services.sessions.announce?.(session);
1232
+ } catch (error) {
1233
+ detach();
1234
+ preparation?.[Symbol.dispose]();
1235
+ throw error;
1236
+ }
1237
+ return {
1238
+ detach,
1239
+ releasePreparation: () => {
1240
+ preparation?.[Symbol.dispose]();
1241
+ }
1242
+ };
1243
+ }
1244
+ function detachOriginalEntry(store, sessionId, entry) {
1245
+ if (store === void 0 || entry === void 0 || store.get(sessionId) !== entry) return;
1246
+ if (entry.detach === void 0) throw new SessionMoveError("session-move/service-unavailable", "宿主无法安全释放原会话入口。");
1247
+ entry.detach();
1248
+ if (store.get(sessionId) === entry) throw new SessionMoveError("session-move/quiesce-failed", "原会话入口未能完整释放。");
1249
+ }
1250
+ async function rollbackMove(services, sessionId, transaction, snapshots, originalStored, originalEntry, enteredPlaceholder) {
1251
+ const failures = [];
1252
+ try {
1253
+ enteredPlaceholder?.detach();
1254
+ } catch (error) {
1255
+ failures.push(error);
1256
+ }
1257
+ try {
1258
+ enteredPlaceholder?.releasePreparation();
1259
+ } catch (error) {
1260
+ failures.push(error);
1261
+ }
1262
+ try {
1263
+ await transaction.rollback();
1264
+ } catch (error) {
1265
+ failures.push(error);
1266
+ }
1267
+ let restored;
1268
+ try {
1269
+ if (services.sessions.get(sessionId) === void 0) restored = await enterStoredSession(services, sessionId, originalStored);
1270
+ await restoreWorkspaceSnapshots(snapshots, sessionId);
1271
+ } catch (error) {
1272
+ failures.push(error);
1273
+ } finally {
1274
+ try {
1275
+ if (originalEntry === void 0) restored?.detach();
1276
+ restored?.releasePreparation();
1277
+ } catch (error) {
1278
+ failures.push(error);
1279
+ }
1280
+ }
1281
+ if (failures.length > 0) throw new AggregateError(failures, "会话迁移回滚失败");
1282
+ }
1283
+ async function moveAccounting(target, snapshots, sessionId) {
1284
+ for (const snapshot of snapshots) if (snapshot.workspace.id !== target.id && snapshot.contained) await snapshot.workspace.detachSession(sessionId);
1285
+ await target.attachSession(sessionId);
1286
+ }
1287
+ /**
1288
+ * 把持久化会话完整迁移到目标项目。整个会话目录一起移动,任何提交前失败都会恢复原工件和项目顺序。
1289
+ */
1290
+ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, options = {}) {
1291
+ if (!validIdentifier(sessionId) || !validIdentifier(targetWorkspaceId)) throw new SessionMoveError("session-move/invalid-request", "会话或目标项目标识无效。");
1292
+ if (movingSessions.has(sessionId)) throw new SessionMoveError("session-move/busy", "该会话正在移动,请稍后重试。");
1293
+ movingSessions.add(sessionId);
1294
+ try {
1295
+ const workspaces = services.workspaceRegistry.list();
1296
+ const target = workspaces.find((workspace) => workspace.id === targetWorkspaceId);
1297
+ if (target === void 0) throw new SessionMoveError("session-move/workspace-not-found", "目标项目不存在。");
1298
+ const snapshots = workspaceSnapshots(workspaces, sessionId);
1299
+ const targetSnapshot = snapshots.find((snapshot) => snapshot.workspace.id === target.id);
1300
+ const sources = snapshots.filter((snapshot) => snapshot.contained && snapshot.workspace.id !== target.id);
1301
+ if (sources.length > 1 || targetSnapshot?.contained === true && sources.length > 0) throw new SessionMoveError("session-move/accounting-invalid", "会话当前的项目归属不一致,无法安全移动。");
1302
+ const persistedHeader = (await services.sessionPersistence.list()).find((header) => header.id === sessionId);
1303
+ if (persistedHeader === void 0) throw new SessionMoveError("session-move/session-not-found", "该会话没有可迁移的持久化记录。");
1304
+ if (persistedHeader.origin === "subagent") throw new SessionMoveError("session-move/subagent-unsupported", "子代理会话不能移动到其他项目。");
1305
+ const targetPath = await realpath(target.path);
1306
+ let currentPath;
1307
+ if (persistedHeader.cwd !== void 0) try {
1308
+ currentPath = await realpath(persistedHeader.cwd);
1309
+ } catch {
1310
+ currentPath = void 0;
1311
+ }
1312
+ if (currentPath === targetPath && targetSnapshot?.contained === true && sources.length === 0) return {
1313
+ sessionId,
1314
+ moved: false,
1315
+ fromWorkspaceIds: [target.id],
1316
+ toWorkspaceId: target.id,
1317
+ toWorkspaceTitle: target.title || target.id
1318
+ };
1319
+ const liveSession = services.sessions.get(sessionId);
1320
+ const originalEntry = services.sessions.store?.get(sessionId);
1321
+ if (liveSession !== void 0 && (services.sessions.store === void 0 || originalEntry === void 0)) throw new SessionMoveError("session-move/service-unavailable", "宿主无法提供可恢复的会话入口,暂时不能移动活跃会话。");
1322
+ if (currentPath === targetPath) {
1323
+ const stored = liveSession === void 0 ? await services.sessionPersistence.loadStored(sessionId) : void 0;
1324
+ if (liveSession === void 0 && stored === void 0) throw new SessionMoveError("session-move/session-not-found", "读取会话持久化记录失败。");
1325
+ let enteredPlaceholder;
1326
+ try {
1327
+ if (stored !== void 0) enteredPlaceholder = await enterStoredSession(services, sessionId, stored);
1328
+ await moveAccounting(target, snapshots, sessionId);
1329
+ } catch (error) {
1330
+ try {
1331
+ await restoreWorkspaceSnapshots(snapshots, sessionId);
1332
+ } catch (rollbackError) {
1333
+ throw new SessionMoveError("session-move/rollback-failed", "恢复项目归属失败。", { cause: rollbackError });
1334
+ }
1335
+ throw new SessionMoveError("session-move/accounting-failed", "更新项目归属失败,原会话已恢复。", { cause: error });
1336
+ } finally {
1337
+ enteredPlaceholder?.detach();
1338
+ enteredPlaceholder?.releasePreparation();
1339
+ }
1340
+ return {
1341
+ sessionId,
1342
+ moved: true,
1343
+ fromWorkspaceIds: sources.map((snapshot) => snapshot.workspace.id),
1344
+ toWorkspaceId: target.id,
1345
+ toWorkspaceTitle: target.title || target.id
1346
+ };
1347
+ }
1348
+ const agent = services.agents.get(sessionId);
1349
+ try {
1350
+ if (agent !== void 0) {
1351
+ agent.cancel({ kind: "disposed" });
1352
+ await agent.whenIdle?.();
1353
+ }
1354
+ if (liveSession !== void 0) await services.sessions.flush(liveSession);
1355
+ } catch (error) {
1356
+ throw new SessionMoveError("session-move/quiesce-failed", "会话仍在运行,暂时无法移动。", { cause: error });
1357
+ }
1358
+ const raw = await services.sessionPersistence.readRaw(sessionId);
1359
+ const originalStored = await services.sessionPersistence.loadStored(sessionId);
1360
+ if (raw === void 0 || originalStored === void 0) throw new SessionMoveError("session-move/session-not-found", "读取会话持久化记录失败。");
1361
+ const newlineIndex = raw.content.indexOf("\n");
1362
+ const headerText = newlineIndex < 0 ? raw.content : raw.content.slice(0, newlineIndex);
1363
+ const body = newlineIndex < 0 ? "" : raw.content.slice(newlineIndex + 1);
1364
+ let header;
1365
+ try {
1366
+ header = JSON.parse(headerText);
1367
+ } catch (error) {
1368
+ throw new SessionMoveError("session-move/artifact-invalid", "会话工件头部无法解析。", { cause: error });
1369
+ }
1370
+ if (header.id !== sessionId) throw new SessionMoveError("session-move/artifact-invalid", "会话工件标识与请求不一致。");
1371
+ const targetHeader = {
1372
+ ...header,
1373
+ cwd: targetPath
1374
+ };
1375
+ const targetMeta = {
1376
+ ...raw.meta,
1377
+ cwd: targetPath
1378
+ };
1379
+ const oldLocation = services.sessionPersistence.locate(raw.meta);
1380
+ const newLocation = services.sessionPersistence.locate(targetMeta);
1381
+ if (oldLocation === void 0 || newLocation === void 0) throw new SessionMoveError("session-move/path-invalid", "宿主无法定位会话工件。");
1382
+ const bytes = await (options.encodeArtifact ?? encodeSessionArtifact)(JSON.stringify(targetHeader), body, newLocation.path.endsWith(".zstd"));
1383
+ const transaction = new ArtifactDirectoryMove(oldLocation.path, newLocation.path);
1384
+ try {
1385
+ await agent?.scope?.dispose?.();
1386
+ services.agents.store?.delete(sessionId);
1387
+ detachOriginalEntry(services.sessions.store, sessionId, originalEntry);
1388
+ } catch (error) {
1389
+ if (originalEntry !== void 0 && services.sessions.get(sessionId) === void 0) try {
1390
+ (await enterStoredSession(services, sessionId, originalStored)).releasePreparation();
1391
+ } catch (restoreError) {
1392
+ throw new SessionMoveError("session-move/rollback-failed", "恢复原会话入口失败。", { cause: restoreError });
1393
+ }
1394
+ throw new SessionMoveError("session-move/quiesce-failed", "会话仍在运行,暂时无法移动。", { cause: error });
1395
+ }
1396
+ let enteredPlaceholder;
1397
+ try {
1398
+ await transaction.publish(bytes);
1399
+ const movedStored = await services.sessionPersistence.loadStored(sessionId);
1400
+ if (movedStored === void 0 || movedStored.meta.cwd !== targetPath) throw new SessionMoveError("session-move/validation-failed", "迁移后的会话工件校验失败。");
1401
+ enteredPlaceholder = await enterStoredSession(services, sessionId, movedStored);
1402
+ try {
1403
+ await moveAccounting(target, snapshots, sessionId);
1404
+ } catch (error) {
1405
+ throw new SessionMoveError("session-move/accounting-failed", "更新项目归属失败。", { cause: error });
1406
+ }
1407
+ enteredPlaceholder.detach();
1408
+ enteredPlaceholder.releasePreparation();
1409
+ enteredPlaceholder = void 0;
1410
+ await transaction.commit(services.logger);
1411
+ } catch (error) {
1412
+ try {
1413
+ await rollbackMove(services, sessionId, transaction, snapshots, originalStored, originalEntry, enteredPlaceholder);
1414
+ } catch (rollbackError) {
1415
+ throw new SessionMoveError("session-move/rollback-failed", "会话移动失败,且自动回滚未完整完成。", { cause: rollbackError });
1416
+ }
1417
+ if (error instanceof SessionMoveError) throw error;
1418
+ throw new SessionMoveError("session-move/failed", "会话移动失败,原会话已恢复。", { cause: error });
1419
+ }
1420
+ try {
1421
+ await services.sessionProjectionCache?.coldSnapshot?.(sessionId);
1422
+ } catch (error) {
1423
+ services.logger?.warn(`会话已移动,但投影缓存刷新失败:${String(error)}`);
1424
+ }
1425
+ services.logger?.info?.(`会话 ${sessionId} 已移动到项目 ${target.id}`);
1426
+ return {
1427
+ sessionId,
1428
+ moved: true,
1429
+ fromWorkspaceIds: sources.map((snapshot) => snapshot.workspace.id),
1430
+ toWorkspaceId: target.id,
1431
+ toWorkspaceTitle: target.title || target.id
1432
+ };
1433
+ } finally {
1434
+ movingSessions.delete(sessionId);
1435
+ }
1436
+ }
1437
+ /** 对持久化分组做严格校验,避免一个项目同时出现在多个分组。 */
1438
+ function parseWorkspaceGroups(value) {
1439
+ if (!Array.isArray(value) || value.length > 100) return void 0;
1440
+ const groupIds = /* @__PURE__ */ new Set();
1441
+ const groupTitles = /* @__PURE__ */ new Set();
1442
+ const workspaceIds = /* @__PURE__ */ new Set();
1443
+ const groups = [];
1444
+ for (const valueGroup of value) {
1445
+ if (valueGroup === null || typeof valueGroup !== "object") return void 0;
1446
+ const group = valueGroup;
1447
+ const id = typeof group.id === "string" ? group.id.trim() : "";
1448
+ const title = typeof group.title === "string" ? group.title.trim() : "";
1449
+ if (id === "" || id.length > 128 || title === "" || title.length > 80) return void 0;
1450
+ if (groupIds.has(id) || groupTitles.has(title.toLocaleLowerCase())) return void 0;
1451
+ if (!Array.isArray(group.workspaceIds) || group.workspaceIds.length > 1e3) return void 0;
1452
+ const normalizedIds = [...new Set(group.workspaceIds)];
1453
+ if (!normalizedIds.every((workspaceId) => typeof workspaceId === "string" && workspaceId.trim() !== "" && workspaceId.length <= 256)) return void 0;
1454
+ if (normalizedIds.some((workspaceId) => workspaceIds.has(workspaceId))) return void 0;
1455
+ groupIds.add(id);
1456
+ groupTitles.add(title.toLocaleLowerCase());
1457
+ normalizedIds.forEach((workspaceId) => workspaceIds.add(workspaceId));
1458
+ groups.push({
1459
+ id,
1460
+ title,
1461
+ workspaceIds: normalizedIds
1462
+ });
1463
+ }
1464
+ return groups;
1465
+ }
1466
+ //#endregion
1049
1467
  //#region src/workspace-preferences.ts
1050
1468
  const WORKSPACE_PREFERENCES_FILE = ".dsh-codex-ui-preferences.json";
1051
1469
  /** Desktop 和普通 DSH Web 共用 Profile;服务换端口或重启后目录仍保持稳定。 */
@@ -1061,11 +1479,19 @@ function parsePinnedWorkspaceIds(value) {
1061
1479
  function parseWorkspacePreferences(value) {
1062
1480
  if (value === null || typeof value !== "object") return void 0;
1063
1481
  const record = value;
1064
- if (record.version !== 1) return void 0;
1065
1482
  const pinnedWorkspaceIds = parsePinnedWorkspaceIds(record.pinnedWorkspaceIds);
1066
- return pinnedWorkspaceIds === void 0 ? void 0 : {
1067
- version: 1,
1068
- pinnedWorkspaceIds
1483
+ if (pinnedWorkspaceIds === void 0) return void 0;
1484
+ if (record.version === 1) return {
1485
+ version: 2,
1486
+ pinnedWorkspaceIds,
1487
+ workspaceGroups: []
1488
+ };
1489
+ if (record.version !== 2) return void 0;
1490
+ const workspaceGroups = parseWorkspaceGroups(record.workspaceGroups);
1491
+ return workspaceGroups === void 0 ? void 0 : {
1492
+ version: 2,
1493
+ pinnedWorkspaceIds,
1494
+ workspaceGroups
1069
1495
  };
1070
1496
  }
1071
1497
  async function readWorkspacePreferences(path = workspacePreferencesPath()) {
@@ -1078,8 +1504,9 @@ async function readWorkspacePreferences(path = workspacePreferencesPath()) {
1078
1504
  };
1079
1505
  } catch (error) {
1080
1506
  if (error.code === "ENOENT") return {
1081
- version: 1,
1507
+ version: 2,
1082
1508
  pinnedWorkspaceIds: [],
1509
+ workspaceGroups: [],
1083
1510
  exists: false
1084
1511
  };
1085
1512
  throw error;
@@ -1090,16 +1517,18 @@ function temporaryPath(path) {
1090
1517
  }
1091
1518
  let writeQueue = Promise.resolve();
1092
1519
  /** 串行、原子保存,避免快速拖动排序产生乱序或半截 JSON。 */
1093
- function writeWorkspacePreferences(pinnedWorkspaceIds, path = workspacePreferencesPath()) {
1520
+ function writeWorkspacePreferences(pinnedWorkspaceIds, workspaceGroups = [], path = workspacePreferencesPath()) {
1094
1521
  const normalized = parsePinnedWorkspaceIds([...pinnedWorkspaceIds]);
1095
- if (normalized === void 0) return Promise.reject(/* @__PURE__ */ new Error("置顶工作区数据无效。"));
1522
+ const normalizedGroups = parseWorkspaceGroups([...workspaceGroups]);
1523
+ if (normalized === void 0 || normalizedGroups === void 0) return Promise.reject(/* @__PURE__ */ new Error("工作区偏好数据无效。"));
1096
1524
  const task = writeQueue.catch(() => void 0).then(async () => {
1097
1525
  await mkdir(dirname(path), { recursive: true });
1098
1526
  const temporary = temporaryPath(path);
1099
1527
  try {
1100
1528
  await writeFile(temporary, `${JSON.stringify({
1101
- version: 1,
1102
- pinnedWorkspaceIds: normalized
1529
+ version: 2,
1530
+ pinnedWorkspaceIds: normalized,
1531
+ workspaceGroups: normalizedGroups
1103
1532
  }, void 0, 2)}\n`, "utf8");
1104
1533
  await rename(temporary, path);
1105
1534
  } finally {
@@ -1115,6 +1544,7 @@ const connectorsEndpoint = "/api/michengai/codex-ui/connectors";
1115
1544
  const dependenciesEndpoint = "/api/michengai/codex-ui/dependencies";
1116
1545
  const explorerEndpoint = "/api/michengai/codex-ui/open-in-explorer";
1117
1546
  const preferencesEndpoint = "/api/michengai/codex-ui/preferences";
1547
+ const sessionMoveEndpoint = "/api/michengai/codex-ui/session-move";
1118
1548
  const maxPreferencesBodyBytes = 32768;
1119
1549
  function headerValue(headers, name) {
1120
1550
  const value = headers[name];
@@ -1169,8 +1599,37 @@ const inject = [
1169
1599
  "webServer",
1170
1600
  "agents",
1171
1601
  "tools",
1172
- "workspaceRegistry"
1602
+ "workspaceRegistry",
1603
+ "sessions",
1604
+ "sessionPersistence"
1173
1605
  ];
1606
+ function sessionMoveStatus(error) {
1607
+ if (!(error instanceof SessionMoveError)) return 500;
1608
+ if (error.code === "session-move/invalid-request") return 400;
1609
+ if (error.code === "session-move/session-not-found" || error.code === "session-move/workspace-not-found") return 404;
1610
+ if (error.code === "session-move/service-unavailable") return 503;
1611
+ if (error.code === "session-move/zstd-unavailable") return 501;
1612
+ if (error.code === "session-move/busy" || error.code === "session-move/subagent-unsupported" || error.code === "session-move/accounting-invalid" || error.code === "session-move/destination-occupied") return 409;
1613
+ return 500;
1614
+ }
1615
+ function publicSessionMoveError(error) {
1616
+ const code = error instanceof SessionMoveError ? error.code : "session-move/failed";
1617
+ return {
1618
+ code,
1619
+ error: {
1620
+ "session-move/invalid-request": "会话或目标项目标识无效。",
1621
+ "session-move/session-not-found": "该会话没有可迁移的持久化记录。",
1622
+ "session-move/workspace-not-found": "目标项目不存在。",
1623
+ "session-move/service-unavailable": "宿主暂时无法安全移动活跃会话。",
1624
+ "session-move/subagent-unsupported": "子代理会话不能移动到其他项目。",
1625
+ "session-move/busy": "该会话正在移动,请稍后重试。",
1626
+ "session-move/accounting-invalid": "会话当前的项目归属不一致,无法安全移动。",
1627
+ "session-move/destination-occupied": "目标项目已经存在同名会话工件。",
1628
+ "session-move/zstd-unavailable": "当前运行环境不支持该会话的存储格式。",
1629
+ "session-move/rollback-failed": "移动失败,自动恢复未完整完成,请查看服务端日志。"
1630
+ }[code] ?? "暂时无法移动该会话,请稍后重试。"
1631
+ };
1632
+ }
1174
1633
  /** 提供不泄露地址、命令和凭证的连接器目录。 */
1175
1634
  function apply(ctx) {
1176
1635
  const host = hostServices(ctx);
@@ -1337,23 +1796,27 @@ function apply(ctx) {
1337
1796
  return;
1338
1797
  }
1339
1798
  const body = JSON.parse(await readRequestBody(request));
1340
- const pinnedWorkspaceIds = body !== null && typeof body === "object" ? parsePinnedWorkspaceIds(body.pinnedWorkspaceIds) : void 0;
1341
- if (pinnedWorkspaceIds === void 0) {
1799
+ const record = body !== null && typeof body === "object" ? body : void 0;
1800
+ const pinnedWorkspaceIds = record === void 0 ? void 0 : parsePinnedWorkspaceIds(record.pinnedWorkspaceIds);
1801
+ const existing = await readWorkspacePreferences();
1802
+ const workspaceGroups = record === void 0 ? void 0 : "workspaceGroups" in record ? parseWorkspaceGroups(record.workspaceGroups) : existing.workspaceGroups;
1803
+ if (pinnedWorkspaceIds === void 0 || workspaceGroups === void 0) {
1342
1804
  response.writeHead(400, {
1343
1805
  "content-type": "application/json; charset=utf-8",
1344
1806
  "cache-control": "no-store"
1345
1807
  });
1346
- response.end(JSON.stringify({ error: "置顶偏好格式无效。" }));
1808
+ response.end(JSON.stringify({ error: "工作区偏好格式无效。" }));
1347
1809
  return;
1348
1810
  }
1349
- await writeWorkspacePreferences(pinnedWorkspaceIds);
1811
+ await writeWorkspacePreferences(pinnedWorkspaceIds, workspaceGroups);
1350
1812
  response.writeHead(200, {
1351
1813
  "content-type": "application/json; charset=utf-8",
1352
1814
  "cache-control": "no-store"
1353
1815
  });
1354
1816
  response.end(JSON.stringify({
1355
- version: 1,
1817
+ version: 2,
1356
1818
  pinnedWorkspaceIds,
1819
+ workspaceGroups,
1357
1820
  exists: true
1358
1821
  }));
1359
1822
  return;
@@ -1386,6 +1849,79 @@ function apply(ctx) {
1386
1849
  }
1387
1850
  }
1388
1851
  });
1852
+ const disposeSessionMove = host.webServer.register({
1853
+ kind: "exact",
1854
+ path: sessionMoveEndpoint,
1855
+ handler: async (request, response) => {
1856
+ if (request.method !== "POST") {
1857
+ response.writeHead(405, { allow: "POST" });
1858
+ response.end();
1859
+ return;
1860
+ }
1861
+ if (crossSiteRequest(request)) {
1862
+ response.writeHead(403, {
1863
+ "content-type": "application/json; charset=utf-8",
1864
+ "cache-control": "no-store"
1865
+ });
1866
+ response.end(JSON.stringify({
1867
+ ok: false,
1868
+ code: "session-move/cross-site",
1869
+ error: "已拒绝跨站请求。"
1870
+ }));
1871
+ return;
1872
+ }
1873
+ try {
1874
+ const body = JSON.parse(await readRequestBody(request));
1875
+ const record = body !== null && typeof body === "object" ? body : void 0;
1876
+ const sessionId = typeof record?.sessionId === "string" ? record.sessionId.trim() : "";
1877
+ const targetWorkspaceId = typeof record?.targetWorkspaceId === "string" ? record.targetWorkspaceId.trim() : "";
1878
+ const result = await moveSessionToWorkspace(host, sessionId, targetWorkspaceId);
1879
+ response.writeHead(200, {
1880
+ "content-type": "application/json; charset=utf-8",
1881
+ "cache-control": "no-store"
1882
+ });
1883
+ response.end(JSON.stringify({
1884
+ ok: true,
1885
+ result
1886
+ }));
1887
+ } catch (error) {
1888
+ if (error instanceof RequestBodyTooLargeError) {
1889
+ response.writeHead(413, {
1890
+ "content-type": "application/json; charset=utf-8",
1891
+ "cache-control": "no-store"
1892
+ });
1893
+ response.end(JSON.stringify({
1894
+ ok: false,
1895
+ code: "session-move/invalid-request",
1896
+ error: "请求体过大。"
1897
+ }));
1898
+ return;
1899
+ }
1900
+ if (error instanceof SyntaxError) {
1901
+ response.writeHead(400, {
1902
+ "content-type": "application/json; charset=utf-8",
1903
+ "cache-control": "no-store"
1904
+ });
1905
+ response.end(JSON.stringify({
1906
+ ok: false,
1907
+ code: "session-move/invalid-request",
1908
+ error: "请求格式无效。"
1909
+ }));
1910
+ return;
1911
+ }
1912
+ ctx.logger.warn("session move failed: %s", error);
1913
+ const payload = publicSessionMoveError(error);
1914
+ response.writeHead(sessionMoveStatus(error), {
1915
+ "content-type": "application/json; charset=utf-8",
1916
+ "cache-control": "no-store"
1917
+ });
1918
+ response.end(JSON.stringify({
1919
+ ok: false,
1920
+ ...payload
1921
+ }));
1922
+ }
1923
+ }
1924
+ });
1389
1925
  const disposeExplorer = host.webServer.register({
1390
1926
  kind: "exact",
1391
1927
  path: explorerEndpoint,
@@ -1456,6 +1992,7 @@ function apply(ctx) {
1456
1992
  disposeDependencies();
1457
1993
  disposeExplorer();
1458
1994
  disposePreferences();
1995
+ disposeSessionMove();
1459
1996
  };
1460
1997
  }, "michengai-codex-ui: catalogs");
1461
1998
  }