@michengai/dsh-codex-ui 1.1.8 → 1.1.10
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/CHANGELOG.md +50 -0
- package/CHANGELOG.zh-CN.md +50 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/cordis.patch.yml +2 -0
- package/lib/client.js +121 -19
- package/lib/index.mjs +247 -238
- package/lib/session-title-plugin.d.mts +8 -0
- package/lib/session-title-plugin.mjs +913 -0
- package/lib/usage-frame.css +1 -1
- package/lib/usage-frame.js +61 -54
- package/package.json +39 -35
package/lib/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apply as apply$1 } from "@deepseek-ai/dsh-client-ui-settings-general";
|
|
2
|
-
import { lstat, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -1192,6 +1192,173 @@ var SessionMoveError = class extends Error {
|
|
|
1192
1192
|
}
|
|
1193
1193
|
};
|
|
1194
1194
|
const movingSessions = /* @__PURE__ */ new Set();
|
|
1195
|
+
const ZSTD_MAGIC = 4247762216;
|
|
1196
|
+
/** 旧宿主 list() 直接给会话头;0.1.6 起返回 { header, revision }。 */
|
|
1197
|
+
function persistedSessionHeader(item) {
|
|
1198
|
+
if (item === null || typeof item !== "object") return void 0;
|
|
1199
|
+
const record = item;
|
|
1200
|
+
if (typeof record.id === "string" && record.id.length > 0) return record;
|
|
1201
|
+
const nested = record.header;
|
|
1202
|
+
if (nested !== null && typeof nested === "object") {
|
|
1203
|
+
const header = nested;
|
|
1204
|
+
if (typeof header.id === "string" && header.id.length > 0) return header;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async function listPersistedHeader(persistence, sessionId) {
|
|
1208
|
+
for (const item of await persistence.list()) {
|
|
1209
|
+
const header = persistedSessionHeader(item);
|
|
1210
|
+
if (header?.id === sessionId) return header;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function isMissingArtifact(error) {
|
|
1214
|
+
if (error.code === "ENOENT") return true;
|
|
1215
|
+
return error.name === "SessionPersistenceNotFoundError";
|
|
1216
|
+
}
|
|
1217
|
+
function missingArtifactError(error) {
|
|
1218
|
+
return new SessionMoveError("session-move/session-not-found", "读取会话持久化记录失败。", error === void 0 ? void 0 : { cause: error });
|
|
1219
|
+
}
|
|
1220
|
+
async function loadStoredSession(persistence, sessionId) {
|
|
1221
|
+
try {
|
|
1222
|
+
if (typeof persistence.loadStored === "function") return persistence.loadStored(sessionId);
|
|
1223
|
+
if (typeof persistence.open !== "function") return void 0;
|
|
1224
|
+
const handle = await persistence.open(sessionId, "read");
|
|
1225
|
+
try {
|
|
1226
|
+
const { events } = await handle.read();
|
|
1227
|
+
return {
|
|
1228
|
+
meta: handle.header,
|
|
1229
|
+
events,
|
|
1230
|
+
inheritedEventCount: handle.inheritedEventCount
|
|
1231
|
+
};
|
|
1232
|
+
} finally {
|
|
1233
|
+
await handle.close();
|
|
1234
|
+
}
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
if (isMissingArtifact(error)) throw missingArtifactError(error);
|
|
1237
|
+
throw error;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function locateArtifact(persistence, meta) {
|
|
1241
|
+
if (typeof persistence.locate !== "function") return void 0;
|
|
1242
|
+
return persistence.locate(meta);
|
|
1243
|
+
}
|
|
1244
|
+
async function resolveArtifactPath(persistence, sessionId, meta) {
|
|
1245
|
+
const located = locateArtifact(persistence, meta);
|
|
1246
|
+
if (located?.path !== void 0 && located.path !== "") return located.path;
|
|
1247
|
+
if (typeof persistence.resolveCurrentLog === "function") return persistence.resolveCurrentLog(sessionId);
|
|
1248
|
+
}
|
|
1249
|
+
function firstZstdFrameEnd(buffer) {
|
|
1250
|
+
if (buffer.length < 4 || buffer.readUInt32LE(0) !== ZSTD_MAGIC) return void 0;
|
|
1251
|
+
let offset = 4;
|
|
1252
|
+
if (offset >= buffer.length) return void 0;
|
|
1253
|
+
const descriptor = buffer.readUInt8(offset);
|
|
1254
|
+
offset += 1;
|
|
1255
|
+
if ((descriptor & 24) !== 0) return void 0;
|
|
1256
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
1257
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
1258
|
+
const checksum = (descriptor & 4) !== 0;
|
|
1259
|
+
const dictionaryFlag = descriptor & 3;
|
|
1260
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
1261
|
+
const contentSizeBytes = contentSizeFlag === 0 ? singleSegment ? 1 : 0 : 1 << contentSizeFlag;
|
|
1262
|
+
offset += (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
1263
|
+
for (;;) {
|
|
1264
|
+
if (buffer.length - offset < 3) return void 0;
|
|
1265
|
+
const blockHeader = buffer.readUIntLE(offset, 3);
|
|
1266
|
+
offset += 3;
|
|
1267
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
1268
|
+
const blockType = blockHeader >>> 1 & 3;
|
|
1269
|
+
const blockSize = blockHeader >>> 3;
|
|
1270
|
+
if (blockType === 3) return void 0;
|
|
1271
|
+
const payloadBytes = blockType === 1 ? 1 : blockSize;
|
|
1272
|
+
if (buffer.length - offset < payloadBytes) return void 0;
|
|
1273
|
+
offset += payloadBytes;
|
|
1274
|
+
if (lastBlock) break;
|
|
1275
|
+
}
|
|
1276
|
+
if (checksum) {
|
|
1277
|
+
if (buffer.length - offset < 4) return void 0;
|
|
1278
|
+
offset += 4;
|
|
1279
|
+
}
|
|
1280
|
+
return offset;
|
|
1281
|
+
}
|
|
1282
|
+
async function zlibZstd(name, data, options) {
|
|
1283
|
+
const transform = (await import("node:zlib"))[name];
|
|
1284
|
+
if (typeof transform !== "function") throw new SessionMoveError("session-move/zstd-unavailable", "当前 Node.js 运行时不支持 Zstd 会话迁移。");
|
|
1285
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
1286
|
+
const done = (error, output) => {
|
|
1287
|
+
if (error === null) resolvePromise(output);
|
|
1288
|
+
else rejectPromise(error);
|
|
1289
|
+
};
|
|
1290
|
+
if (options === void 0) transform(data, done);
|
|
1291
|
+
else transform(data, options, done);
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async function zstdChecksumOptions() {
|
|
1295
|
+
const { constants } = await import("node:zlib");
|
|
1296
|
+
const flag = constants.ZSTD_c_checksumFlag;
|
|
1297
|
+
return typeof flag === "number" ? { params: { [flag]: 1 } } : void 0;
|
|
1298
|
+
}
|
|
1299
|
+
function isSessionGenerationFilename(name) {
|
|
1300
|
+
return /^session(?:\.v[1-9]\d*)?\.jsonl(?:\.zstd)?$/.test(name);
|
|
1301
|
+
}
|
|
1302
|
+
async function decodeArtifactFile(path) {
|
|
1303
|
+
let bytes;
|
|
1304
|
+
try {
|
|
1305
|
+
bytes = await readFile(path);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
if (isMissingArtifact(error)) throw missingArtifactError(error);
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
if (!(path.endsWith(".zstd") || bytes.length >= 4 && bytes.readUInt32LE(0) === ZSTD_MAGIC)) {
|
|
1311
|
+
const text = bytes.toString("utf8");
|
|
1312
|
+
const newlineIndex = text.indexOf("\n");
|
|
1313
|
+
return {
|
|
1314
|
+
firstLine: newlineIndex < 0 ? text : text.slice(0, newlineIndex),
|
|
1315
|
+
body: newlineIndex < 0 ? "" : text.slice(newlineIndex + 1),
|
|
1316
|
+
zstd: false
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
const frameEnd = firstZstdFrameEnd(bytes);
|
|
1320
|
+
if (frameEnd === void 0) throw new SessionMoveError("session-move/artifact-invalid", "会话工件头部无法解析。");
|
|
1321
|
+
return {
|
|
1322
|
+
firstLine: (await zlibZstd("zstdDecompress", bytes.subarray(0, frameEnd))).toString("utf8").replace(/\n$/, ""),
|
|
1323
|
+
body: "",
|
|
1324
|
+
zstd: true,
|
|
1325
|
+
trailingZstd: bytes.subarray(frameEnd)
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
async function readSessionArtifact(persistence, sessionId, meta) {
|
|
1329
|
+
try {
|
|
1330
|
+
const path = await resolveArtifactPath(persistence, sessionId, meta);
|
|
1331
|
+
if (path === void 0) throw new SessionMoveError("session-move/path-invalid", "宿主无法定位会话工件。");
|
|
1332
|
+
if (typeof persistence.readRaw === "function") {
|
|
1333
|
+
const raw = await persistence.readRaw(sessionId);
|
|
1334
|
+
if (raw === void 0) throw missingArtifactError();
|
|
1335
|
+
const newlineIndex = raw.content.indexOf("\n");
|
|
1336
|
+
return {
|
|
1337
|
+
path,
|
|
1338
|
+
decoded: {
|
|
1339
|
+
firstLine: newlineIndex < 0 ? raw.content : raw.content.slice(0, newlineIndex),
|
|
1340
|
+
body: newlineIndex < 0 ? "" : raw.content.slice(newlineIndex + 1),
|
|
1341
|
+
zstd: path.endsWith(".zstd")
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
path,
|
|
1347
|
+
decoded: await decodeArtifactFile(path)
|
|
1348
|
+
};
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
if (error instanceof SessionMoveError) throw error;
|
|
1351
|
+
if (isMissingArtifact(error)) throw missingArtifactError(error);
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
async function encodeRewrittenArtifact(firstLine, decoded, encodeArtifact) {
|
|
1356
|
+
if (decoded.zstd && decoded.trailingZstd !== void 0) {
|
|
1357
|
+
const headerFrame = await zlibZstd("zstdCompress", Buffer.from(`${firstLine}\n`, "utf8"), await zstdChecksumOptions());
|
|
1358
|
+
return Buffer.concat([headerFrame, decoded.trailingZstd]);
|
|
1359
|
+
}
|
|
1360
|
+
return encodeArtifact(firstLine, decoded.body, decoded.zstd);
|
|
1361
|
+
}
|
|
1195
1362
|
function rawSessionIds(workspace) {
|
|
1196
1363
|
return Array.isArray(workspace.record?.sessionIds) ? workspace.record.sessionIds : workspace.sessionIds;
|
|
1197
1364
|
}
|
|
@@ -1220,17 +1387,10 @@ async function writeTemporaryFile(finalPath, data) {
|
|
|
1220
1387
|
}
|
|
1221
1388
|
async function encodeSessionArtifact(headerLine, body, zstd) {
|
|
1222
1389
|
if (!zstd) return Buffer.from(`${headerLine}\n${body}`, "utf8");
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
const compress = (data) => new Promise((resolvePromise, rejectPromise) => {
|
|
1226
|
-
zstdCompress(data, (error, output) => {
|
|
1227
|
-
if (error === null) resolvePromise(output);
|
|
1228
|
-
else rejectPromise(error);
|
|
1229
|
-
});
|
|
1230
|
-
});
|
|
1231
|
-
const headerFrame = await compress(Buffer.from(`${headerLine}\n`, "utf8"));
|
|
1390
|
+
const options = await zstdChecksumOptions();
|
|
1391
|
+
const headerFrame = await zlibZstd("zstdCompress", Buffer.from(`${headerLine}\n`, "utf8"), options);
|
|
1232
1392
|
if (body === "") return headerFrame;
|
|
1233
|
-
const bodyFrame = await
|
|
1393
|
+
const bodyFrame = await zlibZstd("zstdCompress", Buffer.from(body, "utf8"), options);
|
|
1234
1394
|
return Buffer.concat([headerFrame, bodyFrame]);
|
|
1235
1395
|
}
|
|
1236
1396
|
var ArtifactDirectoryMove = class {
|
|
@@ -1240,6 +1400,7 @@ var ArtifactDirectoryMove = class {
|
|
|
1240
1400
|
backupCreated = false;
|
|
1241
1401
|
published = false;
|
|
1242
1402
|
temporaryPath;
|
|
1403
|
+
extraBackups = [];
|
|
1243
1404
|
oldDirectory;
|
|
1244
1405
|
newDirectory;
|
|
1245
1406
|
movedOldArtifact;
|
|
@@ -1254,7 +1415,7 @@ var ArtifactDirectoryMove = class {
|
|
|
1254
1415
|
this.movedOldArtifact = join(this.newDirectory, relativeArtifact);
|
|
1255
1416
|
this.backupArtifact = join(this.newDirectory, `${basename(oldArtifact)}.${randomBytes(6).toString("hex")}.dcu-backup`);
|
|
1256
1417
|
}
|
|
1257
|
-
async publish(bytes) {
|
|
1418
|
+
async publish(bytes, siblings) {
|
|
1258
1419
|
if (resolve(this.oldDirectory).toLowerCase() === resolve(this.newDirectory).toLowerCase()) throw new SessionMoveError("session-move/path-conflict", "源项目和目标项目使用了相同的会话目录。");
|
|
1259
1420
|
if (await pathExists(this.newDirectory)) throw new SessionMoveError("session-move/destination-occupied", "目标项目已经存在同名会话工件。");
|
|
1260
1421
|
try {
|
|
@@ -1267,6 +1428,7 @@ var ArtifactDirectoryMove = class {
|
|
|
1267
1428
|
await rename(this.temporaryPath, this.newArtifact);
|
|
1268
1429
|
this.temporaryPath = void 0;
|
|
1269
1430
|
this.published = true;
|
|
1431
|
+
if (siblings !== void 0) await this.rewriteSiblingGenerations(siblings);
|
|
1270
1432
|
} catch (error) {
|
|
1271
1433
|
try {
|
|
1272
1434
|
await this.rollback();
|
|
@@ -1277,6 +1439,39 @@ var ArtifactDirectoryMove = class {
|
|
|
1277
1439
|
throw new SessionMoveError("session-move/artifact-failed", "迁移会话工件失败,原会话已恢复。", { cause: error });
|
|
1278
1440
|
}
|
|
1279
1441
|
}
|
|
1442
|
+
async rewriteSiblingGenerations(siblings) {
|
|
1443
|
+
for (const name of await readdir(this.newDirectory)) {
|
|
1444
|
+
if (!isSessionGenerationFilename(name)) continue;
|
|
1445
|
+
const path = join(this.newDirectory, name);
|
|
1446
|
+
if (resolve(path) === resolve(this.newArtifact) || resolve(path) === resolve(this.backupArtifact)) continue;
|
|
1447
|
+
let decoded;
|
|
1448
|
+
try {
|
|
1449
|
+
decoded = await decodeArtifactFile(path);
|
|
1450
|
+
} catch {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
let header;
|
|
1454
|
+
try {
|
|
1455
|
+
header = JSON.parse(decoded.firstLine);
|
|
1456
|
+
} catch {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (header.id !== siblings.sessionId) continue;
|
|
1460
|
+
const backup = join(this.newDirectory, `${name}.${randomBytes(6).toString("hex")}.dcu-backup`);
|
|
1461
|
+
await rename(path, backup);
|
|
1462
|
+
this.extraBackups.push({
|
|
1463
|
+
path,
|
|
1464
|
+
backup
|
|
1465
|
+
});
|
|
1466
|
+
const rewritten = await encodeRewrittenArtifact(JSON.stringify({
|
|
1467
|
+
...header,
|
|
1468
|
+
cwd: siblings.targetCwd
|
|
1469
|
+
}), decoded, siblings.encodeArtifact);
|
|
1470
|
+
this.temporaryPath = await writeTemporaryFile(path, rewritten);
|
|
1471
|
+
await rename(this.temporaryPath, path);
|
|
1472
|
+
this.temporaryPath = void 0;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1280
1475
|
async rollback() {
|
|
1281
1476
|
const failures = [];
|
|
1282
1477
|
if (this.temporaryPath !== void 0) {
|
|
@@ -1287,6 +1482,18 @@ var ArtifactDirectoryMove = class {
|
|
|
1287
1482
|
}
|
|
1288
1483
|
this.temporaryPath = void 0;
|
|
1289
1484
|
}
|
|
1485
|
+
for (const extra of this.extraBackups.splice(0).reverse()) {
|
|
1486
|
+
try {
|
|
1487
|
+
await rm(extra.path, { force: true });
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
failures.push(error);
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
await rename(extra.backup, extra.path);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
failures.push(error);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1290
1497
|
if (this.published) try {
|
|
1291
1498
|
await rm(this.newArtifact, { force: true });
|
|
1292
1499
|
this.published = false;
|
|
@@ -1309,10 +1516,10 @@ var ArtifactDirectoryMove = class {
|
|
|
1309
1516
|
if (failures.length > 0) throw new AggregateError(failures, "会话工件回滚失败");
|
|
1310
1517
|
}
|
|
1311
1518
|
async commit(logger) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1519
|
+
const leftovers = [...this.backupCreated ? [this.backupArtifact] : [], ...this.extraBackups.splice(0).map((extra) => extra.backup)];
|
|
1520
|
+
this.backupCreated = false;
|
|
1521
|
+
for (const backup of leftovers) try {
|
|
1522
|
+
await rm(backup, { force: true });
|
|
1316
1523
|
} catch (error) {
|
|
1317
1524
|
logger?.warn(`会话迁移成功,但旧工件备份清理失败:${String(error)}`);
|
|
1318
1525
|
}
|
|
@@ -1406,7 +1613,7 @@ async function moveAccounting(target, snapshots, sessionId) {
|
|
|
1406
1613
|
await target.attachSession(sessionId);
|
|
1407
1614
|
}
|
|
1408
1615
|
/**
|
|
1409
|
-
*
|
|
1616
|
+
* 把持久化会话完整迁移到目标项目。整个会话目录一起移动,当前代际和同目录旧代际的 cwd 一并改写;任何提交前失败都会恢复原工件和项目顺序。
|
|
1410
1617
|
*/
|
|
1411
1618
|
async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, options = {}) {
|
|
1412
1619
|
if (!validIdentifier(sessionId) || !validIdentifier(targetWorkspaceId)) throw new SessionMoveError("session-move/invalid-request", "会话或目标项目标识无效。");
|
|
@@ -1420,7 +1627,7 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
|
|
|
1420
1627
|
const targetSnapshot = snapshots.find((snapshot) => snapshot.workspace.id === target.id);
|
|
1421
1628
|
const sources = snapshots.filter((snapshot) => snapshot.contained && snapshot.workspace.id !== target.id);
|
|
1422
1629
|
if (sources.length > 1 || targetSnapshot?.contained === true && sources.length > 0) throw new SessionMoveError("session-move/accounting-invalid", "会话当前的项目归属不一致,无法安全移动。");
|
|
1423
|
-
const persistedHeader =
|
|
1630
|
+
const persistedHeader = await listPersistedHeader(services.sessionPersistence, sessionId);
|
|
1424
1631
|
if (persistedHeader === void 0) throw new SessionMoveError("session-move/session-not-found", "该会话没有可迁移的持久化记录。");
|
|
1425
1632
|
if (persistedHeader.origin === "subagent") throw new SessionMoveError("session-move/subagent-unsupported", "子代理会话不能移动到其他项目。");
|
|
1426
1633
|
const targetPath = await realpath(target.path);
|
|
@@ -1441,7 +1648,7 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
|
|
|
1441
1648
|
const originalEntry = services.sessions.store?.get(sessionId);
|
|
1442
1649
|
if (liveSession !== void 0 && (services.sessions.store === void 0 || originalEntry === void 0)) throw new SessionMoveError("session-move/service-unavailable", "宿主无法提供可恢复的会话入口,暂时不能移动活跃会话。");
|
|
1443
1650
|
if (currentPath === targetPath) {
|
|
1444
|
-
const stored = liveSession === void 0 ? await services.sessionPersistence
|
|
1651
|
+
const stored = liveSession === void 0 ? await loadStoredSession(services.sessionPersistence, sessionId) : void 0;
|
|
1445
1652
|
if (liveSession === void 0 && stored === void 0) throw new SessionMoveError("session-move/session-not-found", "读取会话持久化记录失败。");
|
|
1446
1653
|
let enteredPlaceholder;
|
|
1447
1654
|
try {
|
|
@@ -1476,15 +1683,12 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
|
|
|
1476
1683
|
} catch (error) {
|
|
1477
1684
|
throw new SessionMoveError("session-move/quiesce-failed", "会话仍在运行,暂时无法移动。", { cause: error });
|
|
1478
1685
|
}
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const newlineIndex = raw.content.indexOf("\n");
|
|
1483
|
-
const headerText = newlineIndex < 0 ? raw.content : raw.content.slice(0, newlineIndex);
|
|
1484
|
-
const body = newlineIndex < 0 ? "" : raw.content.slice(newlineIndex + 1);
|
|
1686
|
+
const originalStored = await loadStoredSession(services.sessionPersistence, sessionId);
|
|
1687
|
+
if (originalStored === void 0) throw new SessionMoveError("session-move/session-not-found", "读取会话持久化记录失败。");
|
|
1688
|
+
const artifact = await readSessionArtifact(services.sessionPersistence, sessionId, persistedHeader);
|
|
1485
1689
|
let header;
|
|
1486
1690
|
try {
|
|
1487
|
-
header = JSON.parse(
|
|
1691
|
+
header = JSON.parse(artifact.decoded.firstLine);
|
|
1488
1692
|
} catch (error) {
|
|
1489
1693
|
throw new SessionMoveError("session-move/artifact-invalid", "会话工件头部无法解析。", { cause: error });
|
|
1490
1694
|
}
|
|
@@ -1494,14 +1698,14 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
|
|
|
1494
1698
|
cwd: targetPath
|
|
1495
1699
|
};
|
|
1496
1700
|
const targetMeta = {
|
|
1497
|
-
...
|
|
1701
|
+
...originalStored.meta,
|
|
1498
1702
|
cwd: targetPath
|
|
1499
1703
|
};
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
const bytes = await (
|
|
1504
|
-
const transaction = new ArtifactDirectoryMove(
|
|
1704
|
+
const newPath = locateArtifact(services.sessionPersistence, targetMeta)?.path;
|
|
1705
|
+
if (newPath === void 0) throw new SessionMoveError("session-move/path-invalid", "宿主无法定位会话工件。");
|
|
1706
|
+
const encoder = options.encodeArtifact ?? encodeSessionArtifact;
|
|
1707
|
+
const bytes = await encodeRewrittenArtifact(JSON.stringify(targetHeader), artifact.decoded, encoder);
|
|
1708
|
+
const transaction = new ArtifactDirectoryMove(artifact.path, newPath);
|
|
1505
1709
|
try {
|
|
1506
1710
|
await agent?.scope?.dispose?.();
|
|
1507
1711
|
services.agents.store?.delete(sessionId);
|
|
@@ -1516,8 +1720,17 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
|
|
|
1516
1720
|
}
|
|
1517
1721
|
let enteredPlaceholder;
|
|
1518
1722
|
try {
|
|
1519
|
-
await transaction.publish(bytes
|
|
1520
|
-
|
|
1723
|
+
await transaction.publish(bytes, {
|
|
1724
|
+
sessionId,
|
|
1725
|
+
targetCwd: targetPath,
|
|
1726
|
+
encodeArtifact: encoder
|
|
1727
|
+
});
|
|
1728
|
+
let movedStored;
|
|
1729
|
+
try {
|
|
1730
|
+
movedStored = await loadStoredSession(services.sessionPersistence, sessionId);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
throw new SessionMoveError("session-move/validation-failed", "迁移后的会话工件校验失败。", { cause: error });
|
|
1733
|
+
}
|
|
1521
1734
|
if (movedStored === void 0 || movedStored.meta.cwd !== targetPath) throw new SessionMoveError("session-move/validation-failed", "迁移后的会话工件校验失败。");
|
|
1522
1735
|
enteredPlaceholder = await enterStoredSession(services, sessionId, movedStored);
|
|
1523
1736
|
try {
|
|
@@ -1555,209 +1768,6 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
|
|
|
1555
1768
|
movingSessions.delete(sessionId);
|
|
1556
1769
|
}
|
|
1557
1770
|
}
|
|
1558
|
-
//#endregion
|
|
1559
|
-
//#region src/session-title.ts
|
|
1560
|
-
const SESSION_TITLE_TYPES = [
|
|
1561
|
-
"功能",
|
|
1562
|
-
"设计",
|
|
1563
|
-
"修复",
|
|
1564
|
-
"优化",
|
|
1565
|
-
"发布",
|
|
1566
|
-
"探索",
|
|
1567
|
-
"文档",
|
|
1568
|
-
"研究"
|
|
1569
|
-
];
|
|
1570
|
-
const SESSION_TITLE_EMOJI = {
|
|
1571
|
-
功能: "✨",
|
|
1572
|
-
设计: "🎨",
|
|
1573
|
-
修复: "🐛",
|
|
1574
|
-
优化: "⚡",
|
|
1575
|
-
发布: "🚀",
|
|
1576
|
-
探索: "🔍",
|
|
1577
|
-
文档: "📝",
|
|
1578
|
-
研究: "🔬"
|
|
1579
|
-
};
|
|
1580
|
-
const TITLE_TYPES = new Set(SESSION_TITLE_TYPES);
|
|
1581
|
-
const LEADING_EMOJI = /^(?:\p{Extended_Pictographic}(?:\uFE0F|\u200D\p{Extended_Pictographic})*)+\s*/u;
|
|
1582
|
-
const TITLE_SEPARATORS = /[||│]/g;
|
|
1583
|
-
const GENERIC_THEME_SUFFIXES = [
|
|
1584
|
-
"类型",
|
|
1585
|
-
"分类",
|
|
1586
|
-
"相关",
|
|
1587
|
-
"需求",
|
|
1588
|
-
"问题",
|
|
1589
|
-
"会话",
|
|
1590
|
-
"标题",
|
|
1591
|
-
"主题",
|
|
1592
|
-
"内容"
|
|
1593
|
-
];
|
|
1594
|
-
function stripLeadingNoise(part) {
|
|
1595
|
-
const trimmed = part.trim();
|
|
1596
|
-
if (/^\d{4}$/.test(trimmed)) return "";
|
|
1597
|
-
return trimmed.replace(/^\d{4}\s*/, "").replace(LEADING_EMOJI, "").trim();
|
|
1598
|
-
}
|
|
1599
|
-
function parseTypeAndTheme(raw) {
|
|
1600
|
-
const parts = raw.trim().replace(/^["'`]+|["'`]+$/g, "").trim().replace(TITLE_SEPARATORS, "|").split("|").map(stripLeadingNoise).filter((part) => part !== "");
|
|
1601
|
-
const type = parts[0];
|
|
1602
|
-
const theme = parts[1];
|
|
1603
|
-
if (type === void 0 || theme === void 0 || !TITLE_TYPES.has(type)) return void 0;
|
|
1604
|
-
return {
|
|
1605
|
-
type,
|
|
1606
|
-
theme
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
function isRedundantTheme(type, theme) {
|
|
1610
|
-
const normalized = theme.replace(/[\s·.\-_/]/g, "");
|
|
1611
|
-
if (normalized === type || normalized === `新${type}`) return true;
|
|
1612
|
-
if (type !== "" && normalized.length >= type.length * 2 && normalized.length % type.length === 0 && normalized === type.repeat(normalized.length / type.length)) return true;
|
|
1613
|
-
return GENERIC_THEME_SUFFIXES.some((suffix) => normalized === suffix || normalized === `${type}${suffix}`);
|
|
1614
|
-
}
|
|
1615
|
-
function assembleSessionTitle(type, theme) {
|
|
1616
|
-
const parsed = parseTypeAndTheme(`${type}|${theme}`);
|
|
1617
|
-
if (parsed === void 0) return void 0;
|
|
1618
|
-
const title = isRedundantTheme(parsed.type, parsed.theme) ? `${SESSION_TITLE_EMOJI[parsed.type]} ${parsed.type}` : `${SESSION_TITLE_EMOJI[parsed.type]} ${parsed.type}|${parsed.theme}`;
|
|
1619
|
-
if (Buffer.byteLength(title, "utf8") > 80) return void 0;
|
|
1620
|
-
return title;
|
|
1621
|
-
}
|
|
1622
|
-
function shouldSkipAutoTitle(session) {
|
|
1623
|
-
const origin = session.origin ?? session.header?.origin;
|
|
1624
|
-
return session.id.startsWith("dsh-automation-session-") || session.id.startsWith("im:") || origin === "subagent" || session.header?.parentSession !== void 0 && session.header.parentSession !== "";
|
|
1625
|
-
}
|
|
1626
|
-
//#endregion
|
|
1627
|
-
//#region src/session-title-provider.ts
|
|
1628
|
-
const SESSION_TITLE_PROVIDER_ID = "michengai-codex-ui-session-title";
|
|
1629
|
-
const MAX_INPUT_BYTES = 4096;
|
|
1630
|
-
const MAX_OUTPUT_TOKENS = 64;
|
|
1631
|
-
const TIMEOUT_MS = 6e4;
|
|
1632
|
-
const SYSTEM_PROMPT = [
|
|
1633
|
-
"为编程助手会话生成标题。",
|
|
1634
|
-
"只输出一行,恰好两段:类型|主题",
|
|
1635
|
-
"类型必须是其中一个:功能、设计、修复、优化、发布、探索、文档、研究",
|
|
1636
|
-
"主题必须是具体事项,不能重复类型,不要写“功能”“功能类型”这类空主题。不要写日期或表情,不要引号、前缀、解释或 Markdown。",
|
|
1637
|
-
"使用用户消息的语言。"
|
|
1638
|
-
].join("\n");
|
|
1639
|
-
function asTitleService(value) {
|
|
1640
|
-
if (value === null || typeof value !== "object") return void 0;
|
|
1641
|
-
return typeof value.register === "function" ? value : void 0;
|
|
1642
|
-
}
|
|
1643
|
-
function asLlm(value) {
|
|
1644
|
-
if (value === null || typeof value !== "object") return void 0;
|
|
1645
|
-
return typeof value.stream === "function" ? value : void 0;
|
|
1646
|
-
}
|
|
1647
|
-
function abortableSignal(signal) {
|
|
1648
|
-
const timeout = AbortSignal.timeout(TIMEOUT_MS);
|
|
1649
|
-
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Assemble the streamed text the way the host's `BlockAssembler` does: deltas
|
|
1653
|
-
* accumulate per block index and a closing `block-end` replaces them, so the
|
|
1654
|
-
* adapters that send both channels cannot duplicate the title text.
|
|
1655
|
-
*/
|
|
1656
|
-
function collectText(chunks) {
|
|
1657
|
-
return (async () => {
|
|
1658
|
-
const parts = /* @__PURE__ */ new Map();
|
|
1659
|
-
let finish = "stop";
|
|
1660
|
-
const part = (index) => {
|
|
1661
|
-
let existing = parts.get(index);
|
|
1662
|
-
if (existing === void 0) {
|
|
1663
|
-
existing = {
|
|
1664
|
-
text: "",
|
|
1665
|
-
closed: false
|
|
1666
|
-
};
|
|
1667
|
-
parts.set(index, existing);
|
|
1668
|
-
}
|
|
1669
|
-
return existing;
|
|
1670
|
-
};
|
|
1671
|
-
for await (const chunk of chunks) {
|
|
1672
|
-
if (chunk === null || typeof chunk !== "object") continue;
|
|
1673
|
-
const row = chunk;
|
|
1674
|
-
if (row.type === "tool-call-delta" || row.block?.type === "tool-call") throw new Error("codex-ui session title output must be text");
|
|
1675
|
-
if (row.type === "finish") {
|
|
1676
|
-
finish = typeof row.reason?.kind === "string" ? row.reason.kind : "unknown";
|
|
1677
|
-
continue;
|
|
1678
|
-
}
|
|
1679
|
-
if (row.type !== "text-delta" && row.type !== "block-end") continue;
|
|
1680
|
-
if (typeof row.index !== "number" || !Number.isSafeInteger(row.index)) continue;
|
|
1681
|
-
if (row.type === "text-delta") {
|
|
1682
|
-
if (typeof row.text !== "string") continue;
|
|
1683
|
-
const target = part(row.index);
|
|
1684
|
-
if (!target.closed) target.text += row.text;
|
|
1685
|
-
continue;
|
|
1686
|
-
}
|
|
1687
|
-
if (row.block?.type !== "text" || typeof row.block.text !== "string") continue;
|
|
1688
|
-
const target = part(row.index);
|
|
1689
|
-
if (target.closed) continue;
|
|
1690
|
-
target.text = row.block.text;
|
|
1691
|
-
target.closed = true;
|
|
1692
|
-
}
|
|
1693
|
-
if (finish !== "stop") throw new Error(`codex-ui session title output finished with "${finish}"`);
|
|
1694
|
-
return [...parts.values()].map((segment) => segment.text).join("").trim().split(/\r?\n/, 1)[0] ?? "";
|
|
1695
|
-
})();
|
|
1696
|
-
}
|
|
1697
|
-
async function generateCodexSessionTitle(llm, request) {
|
|
1698
|
-
request.signal?.throwIfAborted();
|
|
1699
|
-
const session = request.session;
|
|
1700
|
-
if (session === void 0 || shouldSkipAutoTitle(session)) throw new Error("codex-ui session title skipped");
|
|
1701
|
-
const first = request.messages?.[0];
|
|
1702
|
-
const text = first?.text?.trim() ?? "";
|
|
1703
|
-
if (first === void 0 || text === "") throw new Error("codex-ui session title missing message");
|
|
1704
|
-
const route = request.route;
|
|
1705
|
-
if (route === void 0 || route.provider === "" || route.model === "") throw new Error("codex-ui session title missing route");
|
|
1706
|
-
const framed = `根据这条用户消息生成会话标题。只返回“类型|主题”一行,不要第三段,不要重复主题。\n${JSON.stringify([{
|
|
1707
|
-
seq: first.seq,
|
|
1708
|
-
text
|
|
1709
|
-
}])}`;
|
|
1710
|
-
if (Buffer.byteLength(framed, "utf8") > MAX_INPUT_BYTES) throw new Error("codex-ui session title input too large");
|
|
1711
|
-
const signal = abortableSignal(request.signal);
|
|
1712
|
-
const raw = await collectText(llm.stream({
|
|
1713
|
-
provider: route.provider,
|
|
1714
|
-
model: route.model,
|
|
1715
|
-
messages: [{
|
|
1716
|
-
id: crypto.randomUUID(),
|
|
1717
|
-
role: "user",
|
|
1718
|
-
content: [{
|
|
1719
|
-
type: "text",
|
|
1720
|
-
text: framed
|
|
1721
|
-
}],
|
|
1722
|
-
source: {
|
|
1723
|
-
kind: "plugin",
|
|
1724
|
-
plugin: "michengai-codex-ui"
|
|
1725
|
-
}
|
|
1726
|
-
}],
|
|
1727
|
-
system: SYSTEM_PROMPT,
|
|
1728
|
-
maxTokens: MAX_OUTPUT_TOKENS,
|
|
1729
|
-
sessionId: session.id,
|
|
1730
|
-
purpose: "session-title",
|
|
1731
|
-
signal
|
|
1732
|
-
}));
|
|
1733
|
-
signal.throwIfAborted();
|
|
1734
|
-
const parsed = parseTypeAndTheme(raw);
|
|
1735
|
-
if (parsed === void 0) throw new Error("codex-ui session title invalid model output");
|
|
1736
|
-
const title = assembleSessionTitle(parsed.type, parsed.theme);
|
|
1737
|
-
if (title === void 0) throw new Error("codex-ui session title rejected");
|
|
1738
|
-
return {
|
|
1739
|
-
title,
|
|
1740
|
-
messageSeqs: typeof first.seq === "number" ? [first.seq] : [],
|
|
1741
|
-
model: route
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
function registerSessionTitleProvider(ctx) {
|
|
1745
|
-
const sessionTitle = asTitleService(ctx.get("sessionTitle"));
|
|
1746
|
-
const llm = asLlm(ctx.get("llm"));
|
|
1747
|
-
if (sessionTitle === void 0 || llm === void 0) return void 0;
|
|
1748
|
-
try {
|
|
1749
|
-
const dispose = sessionTitle.register({
|
|
1750
|
-
id: SESSION_TITLE_PROVIDER_ID,
|
|
1751
|
-
automatic: "first-prompt",
|
|
1752
|
-
generate: (request) => generateCodexSessionTitle(llm, request)
|
|
1753
|
-
});
|
|
1754
|
-
return typeof dispose === "function" ? dispose : void 0;
|
|
1755
|
-
} catch (error) {
|
|
1756
|
-
const reason = error instanceof Error ? error.message : typeof error;
|
|
1757
|
-
ctx.logger?.warn(`Codex UI 未能注册会话标题提供方:${reason}`);
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
1771
|
/** 校验结构和归属;存储兼容模式保留旧 locale 下合法的大小写变体名称。 */
|
|
1762
1772
|
function parseGroups(value, preserveCaseVariants) {
|
|
1763
1773
|
if (!Array.isArray(value) || value.length > 100) return void 0;
|
|
@@ -1976,7 +1986,6 @@ function sessionMoveAuthenticationError(status, error) {
|
|
|
1976
1986
|
/** 提供不泄露地址、命令和凭证的连接器目录。 */
|
|
1977
1987
|
function apply(ctx) {
|
|
1978
1988
|
apply$1(ctx);
|
|
1979
|
-
ctx.effect(() => registerSessionTitleProvider(ctx) ?? (() => {}), "michengai-codex-ui: session title");
|
|
1980
1989
|
const host = hostServices(ctx);
|
|
1981
1990
|
ctx.effect(() => {
|
|
1982
1991
|
const disposers = USAGE_FRAME_ROUTES.map((path) => host.webServer.register({
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/session-title-plugin.d.ts
|
|
3
|
+
declare const name = "michengai-codex-ui-session-title";
|
|
4
|
+
declare const inject: string[];
|
|
5
|
+
/** 独立挂到官方 session-title-llm 同级,等标题服务和模型服务就绪后再占 first-prompt 位。 */
|
|
6
|
+
declare function apply(ctx: Context): void;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { apply, inject, name };
|