@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/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 { zstdCompress } = await import("node:zlib");
1224
- if (typeof zstdCompress !== "function") throw new SessionMoveError("session-move/zstd-unavailable", "当前 Node.js 运行时不支持 Zstd 会话迁移。");
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 compress(Buffer.from(body, "utf8"));
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
- if (!this.backupCreated) return;
1313
- try {
1314
- await rm(this.backupArtifact, { force: true });
1315
- this.backupCreated = false;
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 = (await services.sessionPersistence.list()).find((header) => header.id === sessionId);
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.loadStored(sessionId) : void 0;
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 raw = await services.sessionPersistence.readRaw(sessionId);
1480
- const originalStored = await services.sessionPersistence.loadStored(sessionId);
1481
- if (raw === void 0 || originalStored === void 0) throw new SessionMoveError("session-move/session-not-found", "读取会话持久化记录失败。");
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(headerText);
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
- ...raw.meta,
1701
+ ...originalStored.meta,
1498
1702
  cwd: targetPath
1499
1703
  };
1500
- const oldLocation = services.sessionPersistence.locate(raw.meta);
1501
- const newLocation = services.sessionPersistence.locate(targetMeta);
1502
- if (oldLocation === void 0 || newLocation === void 0) throw new SessionMoveError("session-move/path-invalid", "宿主无法定位会话工件。");
1503
- const bytes = await (options.encodeArtifact ?? encodeSessionArtifact)(JSON.stringify(targetHeader), body, newLocation.path.endsWith(".zstd"));
1504
- const transaction = new ArtifactDirectoryMove(oldLocation.path, newLocation.path);
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
- const movedStored = await services.sessionPersistence.loadStored(sessionId);
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 };