dsh-as-aistudio 0.5.2 → 0.5.4

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 CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  ## 中文
6
6
 
7
+ **0.5.4** —— 跟进 dsh-log-trim 0.6.0(裁剪统计账本)。
8
+
9
+ - 内嵌的 log-trim 升到 0.6.0:每次改写记一笔窗口/事件/字节账,写在 `~/.dsh/dsh-log-trim/ledger.json`,
10
+ 并暴露 `GET /api/dsh-log-trim/ledger` 给其它插件读。
11
+
12
+ **0.5.3** —— 跟进四个组件:设置卡只挂在包自己的页面上。
13
+
14
+ - edit 0.5.16 / rerun 0.2.9 / delete 0.2.2 / log-trim 0.5.2:去掉 `plugins.row.config`,只保留 `plugins.bundle.config`。
15
+ 于是 `#/panel/plugins/<包名>` 直接看到设置,行上不再有「配置」按钮,也没有两份入口。
16
+ - 契约表随之减少三条 `plugins.row.config` 记录。
17
+
7
18
  **0.5.2** —— 跟进三个动作组件:设置卡片同时挂到包自己的页面。
8
19
 
9
20
  - dsh-edit-turn 0.5.15 / dsh-rerun-turn 0.2.8 / dsh-delete-turn 0.2.1:新增 `plugins.bundle.config`(key 是裸包名),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-as-aistudio",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Google-AI-Studio-style turn controls for DeepSeek Harness, in one install: edit a prompt and re-run it, re-run a reply in place, delete a turn from the model context, and render sent prompts as Markdown. Self-contained: the plugin list shows this one row, and the four components are vendored inside it - installing the studio installs nothing else.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/client.js CHANGED
@@ -43,7 +43,7 @@ window.__ModuleLoader__.load({
43
43
  const RERUN_PREFIX = '/dsh-rerun-turn'
44
44
 
45
45
  /** Keep in sync with package.json and lib/index.js. */
46
- const PLUGIN_VERSION = '0.5.15'
46
+ const PLUGIN_VERSION = '0.5.16'
47
47
 
48
48
  // --- copy -----------------------------------------------------------------
49
49
 
@@ -5302,7 +5302,7 @@ window.__ModuleLoader__.load({
5302
5302
  }
5303
5303
 
5304
5304
  /**
5305
- * The component row's configure page (`plugins.row.config`), the same slot
5305
+ * The package page's configure slot (`plugins.bundle.config`), the same slot
5306
5306
  * dsh-delete-turn uses. The manager's summary asks for a one-liner; the page
5307
5307
  * itself renders the three-way picker.
5308
5308
  */
@@ -5477,16 +5477,6 @@ window.__ModuleLoader__.load({
5477
5477
  ),
5478
5478
  )
5479
5479
 
5480
- ctx.slots.inject('plugins.row.config', () =>
5481
- ctx.slots.register(
5482
- {
5483
- name: 'plugins.row.config',
5484
- key: 'dsh-edit-turn#dsh-edit-turn',
5485
- locale: NS,
5486
- },
5487
- EditorStyleOption,
5488
- ),
5489
- )
5490
5480
 
5491
5481
  // 同一张卡也挂在包自己的页面上(`#/panel/plugins/dsh-edit-turn`),
5492
5482
  // 而不是只藏在 `…/dsh-edit-turn/dsh-edit-turn` 那一层。
@@ -5540,7 +5530,7 @@ window.__ModuleLoader__.load({
5540
5530
  const ROUTE_PREFIX = '/dsh-rerun-turn'
5541
5531
 
5542
5532
  /** Keep in sync with package.json and lib/index.js. */
5543
- const PLUGIN_VERSION = '0.2.8'
5533
+ const PLUGIN_VERSION = '0.2.9'
5544
5534
 
5545
5535
  // --- copy -----------------------------------------------------------------
5546
5536
 
@@ -7030,19 +7020,6 @@ window.__ModuleLoader__.load({
7030
7020
  ),
7031
7021
  )
7032
7022
 
7033
- // The plugin's own page in the manager. The key is the manager's
7034
- // `<bundle package>#<row id>` contract (the row this bundle inserts in
7035
- // cordis.patch.yml), exactly as the sibling plugins declare it.
7036
- ctx.slots.inject('plugins.row.config', () =>
7037
- ctx.slots.register(
7038
- {
7039
- name: 'plugins.row.config',
7040
- key: 'dsh-rerun-turn#dsh-rerun-turn',
7041
- locale: NS,
7042
- },
7043
- AskBeforeRerunOption,
7044
- ),
7045
- )
7046
7023
 
7047
7024
  // 同一张卡也挂在包自己的页面上(`#/panel/plugins/dsh-rerun-turn`),
7048
7025
  // 而不是只藏在 `…/dsh-rerun-turn/dsh-rerun-turn` 那一层。
@@ -8508,7 +8485,7 @@ window.__ModuleLoader__.load({
8508
8485
  }
8509
8486
 
8510
8487
  // The plugin page entry (the component row's "configure" arrow, slot
8511
- // `plugins.row.config`, key `<bundle>#<row id>`). It renders the row's
8488
+ // `plugins.bundle.config`, key `<bundle>`). It renders the bundle page's
8512
8489
  // one-liner for the manager's summary slot and the toggle card for the page.
8513
8490
  function AskBeforeDeleteOption({ view, t }) {
8514
8491
  const tr = typeof t === 'function' ? t : (key) => key
@@ -8641,20 +8618,6 @@ window.__ModuleLoader__.load({
8641
8618
  ),
8642
8619
  )
8643
8620
 
8644
- // The plugin's own page in the manager: the row this bundle inserts
8645
- // (`cordis.patch.yml`, id `dsh-delete-turn`) gains a configure arrow that
8646
- // opens this card. The key is `<bundle package>#<row id>`, the contract of
8647
- // ui-plugin-manager's rowConfigKey.
8648
- ctx.slots.inject('plugins.row.config', () =>
8649
- ctx.slots.register(
8650
- {
8651
- name: 'plugins.row.config',
8652
- key: 'dsh-delete-turn#dsh-delete-turn',
8653
- locale: NS,
8654
- },
8655
- AskBeforeDeleteOption,
8656
- ),
8657
- )
8658
8621
 
8659
8622
  // 同一张卡也挂在包自己的页面上(`#/panel/plugins/dsh-delete-turn`),
8660
8623
  // 而不是只藏在 `…/dsh-delete-turn/dsh-delete-turn` 那一层。
@@ -9614,7 +9577,7 @@ window.__ModuleLoader__.load({
9614
9577
  const react = require('react')
9615
9578
  const { jsx, jsxs } = require('react/jsx-runtime')
9616
9579
 
9617
- const PLUGIN_VERSION = '0.5.1'
9580
+ const PLUGIN_VERSION = '0.6.0'
9618
9581
  const ROUTE_PREFIX = '/api/dsh-log-trim'
9619
9582
 
9620
9583
  /** The knobs this card edits, in the order they are drawn. */
@@ -9656,6 +9619,12 @@ window.__ModuleLoader__.load({
9656
9619
  suffix: '条',
9657
9620
  },
9658
9621
  { key: 'minRatio', label: '最少可丢占比', hint: '占整份日志的比例,默认 0.2(20%)', min: 0, max: 1, step: 0.05, kind: 'ratio' },
9622
+ {
9623
+ key: 'recordLedger',
9624
+ label: '记录裁剪统计',
9625
+ hint: '每次改写日志记一笔(窗口数 / 事件数 / 字节),写到 ~/.dsh/dsh-log-trim/ledger.json;dsh-session-pages 会读它显示,其它插件也能读。关掉就不记',
9626
+ kind: 'toggle',
9627
+ },
9659
9628
  { key: 'minBytes', label: '最小日志体积', hint: '小于它就根本不读,默认 1 MB', min: 0, max: 200, step: 1, kind: 'mb' },
9660
9629
  {
9661
9630
  key: 'backupKeep',
@@ -10091,8 +10060,6 @@ window.__ModuleLoader__.load({
10091
10060
  function apply(ctx) {
10092
10061
  const render = (slotProps) =>
10093
10062
  slotProps && slotProps.view === 'summary' ? summaryText() : jsx(CompactCard, { page: true })
10094
- ctx.slots.inject('plugins.row.config', () =>
10095
- ctx.slots.register({ name: 'plugins.row.config', key: 'dsh-log-trim#dsh-log-trim' }, render))
10096
10063
  ctx.slots.inject('plugins.bundle.config', () =>
10097
10064
  ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-log-trim' }, render))
10098
10065
  }
@@ -10121,7 +10088,7 @@ window.__ModuleLoader__.load({
10121
10088
  const NS = 'dsh-as-aistudio'
10122
10089
 
10123
10090
  /** Keep in sync with package.json and src/components.js. */
10124
- const PLUGIN_VERSION = '0.5.2'
10091
+ const PLUGIN_VERSION = '0.5.4'
10125
10092
 
10126
10093
  /** Presence marker the live verifier reads. */
10127
10094
  const DEBUG_KEY = '__DSH_AS_AISTUDIO__'
package/src/components.js CHANGED
@@ -26,7 +26,7 @@
26
26
  export const STUDIO_ID = 'dsh-as-aistudio'
27
27
 
28
28
  /** Keep in sync with package.json and src/client.js. */
29
- export const STUDIO_VERSION = '0.5.2'
29
+ export const STUDIO_VERSION = '0.5.4'
30
30
 
31
31
  /**
32
32
  * The AI Studio action strip this studio composes, in the order the user meets
@@ -1,6 +1,6 @@
1
1
  // dsh-delete-turn host half (dsh-delete-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-delete-turn/src/index.js (v0.2.1, sha256 5dfc53e5c47750f5)
3
+ // Source: ../dsh-delete-turn/src/index.js (v0.2.2, sha256 5dfc53e5c47750f5)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-delete-turn host half (dsh-delete-turn.logic.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-delete-turn/src/logic.js (v0.2.1, sha256 6da16435446356ca)
3
+ // Source: ../dsh-delete-turn/src/logic.js (v0.2.2, sha256 6da16435446356ca)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-edit-turn host half (dsh-edit-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-edit-turn/lib/index.js (v0.5.15, sha256 c743b82073d1ca1a)
3
+ // Source: ../dsh-edit-turn/lib/index.js (v0.5.16, sha256 3b2f76b4ef352e73)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -85,7 +85,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
85
85
  export const name = PLUGIN_ID
86
86
 
87
87
  /** Keep in sync with package.json and lib/client.js. */
88
- export const PLUGIN_VERSION = '0.5.15'
88
+ export const PLUGIN_VERSION = '0.5.16'
89
89
 
90
90
  // A ring of the last few requests, answering "did the browser half even ask, and
91
91
  // what did it get?" without needing its console. Read-only diagnostics: the
@@ -1,6 +1,6 @@
1
1
  // dsh-edit-turn host half (dsh-edit-turn.patch.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-edit-turn/lib/patch.js (v0.5.15, sha256 5e1e8cce1d168f6c)
3
+ // Source: ../dsh-edit-turn/lib/patch.js (v0.5.16, sha256 5e1e8cce1d168f6c)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.flatten.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/flatten.js (v0.5.1, sha256 3aa1fdcbb120ab06)
3
+ // Source: ../dsh-log-trim/lib/flatten.js (v0.6.0, sha256 3aa1fdcbb120ab06)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.host-libs.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/host-libs.mjs (v0.5.1, sha256 5ebed01988b5bfcf)
3
+ // Source: ../dsh-log-trim/lib/host-libs.mjs (v0.6.0, sha256 5ebed01988b5bfcf)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/index.js (v0.5.1, sha256 bd4d3c438d7c00ba)
3
+ // Source: ../dsh-log-trim/lib/index.js (v0.6.0, sha256 1ef6710b85f69330)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -37,12 +37,13 @@ import { discoverRoots, isCurrentLog, listLegacy } from './dsh-log-trim.roots.js
37
37
  import fs from 'node:fs'
38
38
  import os from 'node:os'
39
39
  import path from 'node:path'
40
- import { applyTrim, foldSurface, isSessionLocked, keepLastWindows, readLog, renumber } from './dsh-log-trim.vendor.compact-session.mjs'
40
+ import { applyTrim, foldSurface, isSessionLocked, keepLastWindows, readLog, renumber, windowsOf } from './dsh-log-trim.vendor.compact-session.mjs'
41
41
  import { applyFlatten, applyLossless, applyStreamOnly, planLossless, applyRebuild } from './dsh-log-trim.lossless.js'
42
+ import { readLedger, recordFor } from './dsh-log-trim.ledger.js'
42
43
 
43
44
  export const name = 'dsh-log-trim'
44
45
  export const PLUGIN_ID = name
45
- export const PLUGIN_VERSION = '0.5.1'
46
+ export const PLUGIN_VERSION = '0.6.0'
46
47
  export const ROUTE_PREFIX = '/api/dsh-log-trim'
47
48
 
48
49
  /**
@@ -118,6 +119,36 @@ export function writeSavedConfig(config) {
118
119
  * @param {number} backupKeep - how many backups to keep afterwards.
119
120
  * @returns {Promise<object>} what happened.
120
121
  */
122
+
123
+ /**
124
+ * 窗口裁剪写完之后记一笔账:窗口数、事件数、字节数都来自这次操作本身。
125
+ *
126
+ * @param {string} logFile - 日志路径。
127
+ * @param {object} result - `applyTrim` 的返回。
128
+ * @param {boolean} record - 是否记录(配置项)。
129
+ */
130
+ function noteWindowTrim(logFile, result, record) {
131
+ if (record === false) return
132
+ try {
133
+ let windowsAfter = null
134
+ try {
135
+ windowsAfter = windowsOf(readLog(logFile).events).length
136
+ } catch {
137
+ windowsAfter = null
138
+ }
139
+ recordFor(logFile, 'window', [], [], {
140
+ windowsBefore: result?.before?.windows,
141
+ windowsAfter: windowsAfter === null ? undefined : windowsAfter,
142
+ eventsBefore: result?.before?.events,
143
+ eventsAfter: result?.after?.events,
144
+ bytesBefore: result?.before?.bytes,
145
+ bytesAfter: result?.after?.bytes,
146
+ })
147
+ } catch {
148
+ // 记账失败不影响裁剪本身。
149
+ }
150
+ }
151
+
121
152
  export async function trimOne(logFile, config) {
122
153
  if (isSessionLocked(logFile)) {
123
154
  throw new Error('这个会话正被宿主打开着(session.lock)。先切到别的对话或重启 DSH,再压缩它')
@@ -130,6 +161,7 @@ export async function trimOne(logFile, config) {
130
161
  result = await applyFlatten(logFile, config)
131
162
  } else if (config.mode === 'window') {
132
163
  result = await applyTrim(logFile, config.keepWindows, { streamKeepTurns: 0 })
164
+ noteWindowTrim(logFile, result, config.recordLedger)
133
165
  } else {
134
166
  const plan = planLossless(logFile, config)
135
167
  if (!plan.should) throw new Error(plan.reason)
@@ -419,6 +451,8 @@ export const DEFAULTS = {
419
451
  streamOnBoot: true,
420
452
  // 每次扫描先把 edit/rerun/delete 的载体折叠掉(重启即自动执行)。
421
453
  flattenOnBoot: false,
454
+ // 每次改写过日志就记一笔统计(账本文件,别的插件可读);关掉就不记。
455
+ recordLedger: true,
422
456
  // 启动时是否裁剪窗口(关掉就只剩折叠/清理)。
423
457
  trimOnBoot: true,
424
458
  // 只在开启时清理“已被 replace 盖住、界面早就不显示”的事件(要重编号,默认关)。
@@ -496,6 +530,10 @@ export function resolveConfig(raw = {}) {
496
530
  if (typeof value === 'boolean') merged.streamOnBoot = value
497
531
  continue
498
532
  }
533
+ if (key === 'recordLedger') {
534
+ if (typeof value === 'boolean') merged.recordLedger = value
535
+ continue
536
+ }
499
537
  if (key === 'losslessOnBoot') {
500
538
  if (typeof value === 'boolean') merged.losslessOnBoot = value
501
539
  continue
@@ -992,6 +1030,7 @@ export async function runScan(config, logger = {}) {
992
1030
  }
993
1031
  try {
994
1032
  const result = config.mode === 'window' ? await applyTrim(logFile, config.keepWindows) : await applyLossless(logFile, config, lossless)
1033
+ if (config.mode === 'window') noteWindowTrim(logFile, result, config.recordLedger)
995
1034
  const pruned = pruneBackups(logFile, config.backupKeep)
996
1035
  summary.savedBytes += Math.max(0, result.before.bytes - result.after.bytes)
997
1036
  summary.trimmed.push({
@@ -1095,7 +1134,10 @@ export function apply(ctx, config = {}) {
1095
1134
  }
1096
1135
  if (live.mode === 'window') {
1097
1136
  const plan = planTrim(report, live)
1098
- if (plan.should) await applyTrim(logFile, live.keepWindows, { streamKeepTurns: 0 })
1137
+ if (plan.should) {
1138
+ const done = await applyTrim(logFile, live.keepWindows, { streamKeepTurns: 0 })
1139
+ noteWindowTrim(logFile, done, live.recordLedger)
1140
+ }
1099
1141
  else await applyRebuild(logFile, live)
1100
1142
  return
1101
1143
  }
@@ -1363,6 +1405,29 @@ export function apply(ctx, config = {}) {
1363
1405
 
1364
1406
  // Where the backups are, and a way to open the folder holding them. The card
1365
1407
  // shows the paths; the button hands them to the system file manager.
1408
+ // 裁剪统计账本:别的插件(如 dsh-session-pages)直接读这个接口,
1409
+ // 不必依赖本包;不带 sessionId 时返回全量,带时只返回那一个会话。
1410
+ fiber.effect(() =>
1411
+ webServer.register({
1412
+ kind: 'exact',
1413
+ path: `${ROUTE_PREFIX}/ledger`,
1414
+ handler: (req, res) => {
1415
+ if (!isLoopback(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' })
1416
+ if (req.method !== 'GET') return sendJson(res, 405, { ok: false, error: 'GET only' })
1417
+ let sessionId = null
1418
+ try {
1419
+ sessionId = new URL(req.url, 'http://127.0.0.1').searchParams.get('sessionId')
1420
+ } catch {
1421
+ sessionId = null
1422
+ }
1423
+ const ledger = readLedger()
1424
+ if (sessionId === null || sessionId === '') return sendJson(res, 200, { ok: true, ...ledger })
1425
+ const entry = ledger.sessions[sessionId] ?? null
1426
+ return sendJson(res, 200, { ok: true, sessionId, trim: entry })
1427
+ },
1428
+ }),
1429
+ )
1430
+
1366
1431
  fiber.effect(() =>
1367
1432
  webServer.register({
1368
1433
  kind: 'exact',
@@ -0,0 +1,131 @@
1
+ // dsh-log-trim host half (dsh-log-trim.ledger.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
+ //
3
+ // Source: ../dsh-log-trim/lib/ledger.js (v0.6.0, sha256 1b391592e0cc0fbf)
4
+ // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
+ //
6
+ // src/shell.js imports the copy named after the component, which is why the
7
+ // studio installs with no runtime dependency on the component package.
8
+ /**
9
+ * 裁剪统计账本:每改写过一次日志就记一条,按会话累计。
10
+ *
11
+ * 放在 `~/.dsh/dsh-log-trim/ledger.json`——**谁能读谁就能显示**:
12
+ * `dsh-session-pages` 之类的插件直接读文件即可,不需要依赖本包;
13
+ * 本插件另外用 `/api/dsh-log-trim/ledger` 把它暴露出来。
14
+ *
15
+ * 账本只增不减(除了用户显式清空):裁剪本身会删日志内容,统计不能跟着丢。
16
+ */
17
+
18
+ import fs from 'node:fs'
19
+ import os from 'node:os'
20
+ import path from 'node:path'
21
+ import { windowsOf } from './dsh-log-trim.vendor.compact-session.mjs'
22
+
23
+ /** 账本版本;结构变了就加一,读者据此决定要不要认。 */
24
+ export const LEDGER_VERSION = 1
25
+
26
+ /** 账本文件路径。 */
27
+ export function ledgerPath(home = os.homedir()) {
28
+ return path.join(home, '.dsh', 'dsh-log-trim', 'ledger.json')
29
+ }
30
+
31
+ /**
32
+ * 读账本;不存在或坏掉时返回一个空账本(读取永远不该让插件起不来)。
33
+ *
34
+ * @param {string} [home] - 家目录(测试用)。
35
+ * @returns {{ version: number, sessions: Record<string, object> }} 账本。
36
+ */
37
+ export function readLedger(home = os.homedir()) {
38
+ const empty = { version: LEDGER_VERSION, sessions: {} }
39
+ let raw
40
+ try {
41
+ raw = fs.readFileSync(ledgerPath(home), 'utf8')
42
+ } catch {
43
+ return empty
44
+ }
45
+ try {
46
+ const parsed = JSON.parse(raw)
47
+ if (parsed === null || typeof parsed !== 'object') return empty
48
+ const sessions = parsed.sessions !== null && typeof parsed.sessions === 'object' ? parsed.sessions : {}
49
+ return { version: LEDGER_VERSION, sessions }
50
+ } catch {
51
+ return empty
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 把一次改写记进账本。
57
+ *
58
+ * @param {object} entry - 记录。
59
+ * @param {string} entry.sessionId - 会话 id。
60
+ * @param {'window'|'flatten'|'stream'|'lossless'|'restore'} entry.mode - 做了什么。
61
+ * @param {object[]} entry.before - 改写前的事件。
62
+ * @param {object[]} entry.after - 改写后的事件。
63
+ * @param {number} [entry.bytesBefore] - 改写前字节数。
64
+ * @param {number} [entry.bytesAfter] - 改写后字节数。
65
+ * @param {string} [home] - 家目录(测试用)。
66
+ * @returns {object|null} 该会话的新累计;写不进去时返回 null。
67
+ */
68
+ export function recordTrim(entry, home = os.homedir()) {
69
+ const ledger = readLedger(home)
70
+ const before = Array.isArray(entry.before) ? entry.before : []
71
+ const after = Array.isArray(entry.after) ? entry.after : []
72
+ // 窗口模式不一定留得下事件数组(裁剪是整份重写),允许直接传数字。
73
+ const windowsBefore = Number.isSafeInteger(entry.windowsBefore) ? entry.windowsBefore : windowsOf(before).length
74
+ const windowsAfter = Number.isSafeInteger(entry.windowsAfter) ? entry.windowsAfter : windowsOf(after).length
75
+ const record = {
76
+ at: new Date().toISOString(),
77
+ mode: entry.mode,
78
+ windowsBefore,
79
+ windowsAfter,
80
+ windowsRemoved: Math.max(0, windowsBefore - windowsAfter),
81
+ eventsBefore: Number.isSafeInteger(entry.eventsBefore) ? entry.eventsBefore : before.length,
82
+ eventsAfter: Number.isSafeInteger(entry.eventsAfter) ? entry.eventsAfter : after.length,
83
+ eventsRemoved: Math.max(0, (Number.isSafeInteger(entry.eventsBefore) ? entry.eventsBefore : before.length) - (Number.isSafeInteger(entry.eventsAfter) ? entry.eventsAfter : after.length)),
84
+ bytesBefore: typeof entry.bytesBefore === 'number' ? entry.bytesBefore : null,
85
+ bytesAfter: typeof entry.bytesAfter === 'number' ? entry.bytesAfter : null,
86
+ }
87
+ const current = ledger.sessions[entry.sessionId] ?? { trims: [], totals: { trims: 0, windowsRemoved: 0, eventsRemoved: 0, bytesSaved: 0, lastAt: null } }
88
+ const trims = [...(Array.isArray(current.trims) ? current.trims : []), record].slice(-200)
89
+ const totals = {
90
+ trims: (current.totals?.trims ?? 0) + 1,
91
+ windowsRemoved: (current.totals?.windowsRemoved ?? 0) + record.windowsRemoved,
92
+ eventsRemoved: (current.totals?.eventsRemoved ?? 0) + record.eventsRemoved,
93
+ bytesSaved: (current.totals?.bytesSaved ?? 0) + Math.max(0, (record.bytesBefore ?? 0) - (record.bytesAfter ?? 0)),
94
+ lastAt: record.at,
95
+ }
96
+ ledger.sessions[entry.sessionId] = { trims, totals }
97
+ try {
98
+ const file = ledgerPath(home)
99
+ fs.mkdirSync(path.dirname(file), { recursive: true })
100
+ fs.writeFileSync(file, JSON.stringify(ledger))
101
+ } catch {
102
+ return null
103
+ }
104
+ return totals
105
+ }
106
+
107
+ /** 清空账本(用户显式要求时)。 */
108
+ export function clearLedger(home = os.homedir()) {
109
+ try {
110
+ fs.rmSync(ledgerPath(home), { force: true })
111
+ return true
112
+ } catch {
113
+ return false
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 按日志路径记一笔:sessionId 就是日志所在目录名。
119
+ *
120
+ * @param {string} logFile - 日志路径。
121
+ * @param {string} mode - window / flatten / stream / lossless / rebuild。
122
+ * @param {object[]} before - 改写前事件。
123
+ * @param {object[]} after - 改写后事件。
124
+ * @param {{ bytesBefore?: number, bytesAfter?: number, record?: boolean, home?: string }} [options] - 附加信息。
125
+ * @returns {object|null} 新累计;没记或记不进去返回 null。
126
+ */
127
+ export function recordFor(logFile, mode, before, after, options = {}) {
128
+ if (options.record === false) return null
129
+ const sessionId = path.basename(path.dirname(logFile))
130
+ return recordTrim({ sessionId, mode, before, after, bytesBefore: options.bytesBefore, bytesAfter: options.bytesAfter }, options.home)
131
+ }
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.lossless.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/lossless.js (v0.5.1, sha256 adf9bf5d79aa41b0)
3
+ // Source: ../dsh-log-trim/lib/lossless.js (v0.6.0, sha256 a35a99d4fe824689)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import fs from 'node:fs'
27
+ import { recordFor } from './dsh-log-trim.ledger.js'
27
28
  import { commitTrim, derivedHistory, dropOrphanCompactions, encodeLog, foldSurface, readLog, renumber, seededHeaderFor, textLengthOf } from './dsh-log-trim.vendor.compact-session.mjs'
28
29
 
29
30
  /**
@@ -224,6 +225,7 @@ export async function applyLossless(logFile, config, plan, options = {}) {
224
225
  }
225
226
 
226
227
  const summary = commitTrim({ staging, backup }, logFile)
228
+ recordFor(logFile, 'lossless', original.events, renamed.events, { bytesBefore: original.compressedBytes, bytesAfter: summary.after.bytes, record: config?.recordLedger !== false })
227
229
  return {
228
230
  ...summary,
229
231
  mode: 'lossless',
@@ -254,6 +256,7 @@ export async function applyRebuild(logFile, config) {
254
256
  }
255
257
  const beforeBytes = fs.statSync(staging).size
256
258
  commitTrim({ staging, backup }, logFile)
259
+ recordFor(logFile, 'rebuild', original.events, rebuilt, { bytesBefore: original.compressedBytes, bytesAfter: beforeBytes, record: config?.recordLedger !== false })
257
260
  return {
258
261
  mode: 'rebuild',
259
262
  before: { events: original.events.length, bytes: original.compressedBytes, windows: 0 },
@@ -271,7 +274,7 @@ export async function applyRebuild(logFile, config) {
271
274
  * @param {number} keepTurns - 保留最近几轮。
272
275
  * @returns {Promise<object|null>} 摘要;没有可清的就返回 null。
273
276
  */
274
- export async function applyStreamOnly(logFile, keepTurns) {
277
+ export async function applyStreamOnly(logFile, keepTurns, options = {}) {
275
278
  const { stripOldStreams } = await import('./dsh-log-trim.vendor.compact-session.mjs')
276
279
  const original = readLog(logFile)
277
280
  const stripped = stripOldStreams(original.events, keepTurns)
@@ -288,6 +291,11 @@ export async function applyStreamOnly(logFile, keepTurns) {
288
291
  }
289
292
  const beforeBytes = fs.statSync(staging).size
290
293
  commitTrim({ staging, backup }, logFile)
294
+ recordFor(logFile, 'stream', original.events, stripped.events, {
295
+ bytesBefore: original.compressedBytes,
296
+ bytesAfter: beforeBytes,
297
+ record: options.record !== false,
298
+ })
291
299
  return { events: stripped.stripped, before: original.compressedBytes, after: beforeBytes, backup: backup.slice(backup.lastIndexOf('.precompact-') + 1) }
292
300
  }
293
301
 
@@ -327,6 +335,7 @@ export async function applyFlatten(logFile, config) {
327
335
  }
328
336
  const beforeBytes = fs.statSync(staging).size
329
337
  commitTrim({ staging, backup }, logFile)
338
+ recordFor(logFile, 'flatten', original.events, folded, { bytesBefore: original.compressedBytes, bytesAfter: beforeBytes, record: config?.recordLedger !== false })
330
339
  return {
331
340
  mode: 'flatten',
332
341
  before: { events: original.events.length, bytes: original.compressedBytes, windows: 0 },
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.roots.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/roots.js (v0.5.1, sha256 028d9c12a395aaab)
3
+ // Source: ../dsh-log-trim/lib/roots.js (v0.6.0, sha256 028d9c12a395aaab)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.vendor.compact-session.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/vendor.compact-session.mjs (v0.5.1, sha256 2c1c819e732c9649)
3
+ // Source: ../dsh-log-trim/lib/vendor.compact-session.mjs (v0.6.0, sha256 2c1c819e732c9649)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-log-trim host half (dsh-log-trim.verify-session-log.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-log-trim/lib/verify-session-log.mjs (v0.5.1, sha256 6a0527e8483fb252)
3
+ // Source: ../dsh-log-trim/lib/verify-session-log.mjs (v0.6.0, sha256 6a0527e8483fb252)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-rerun-turn host half (dsh-rerun-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-rerun-turn/lib/index.js (v0.2.8, sha256 9490939d26c589e3)
3
+ // Source: ../dsh-rerun-turn/lib/index.js (v0.2.9, sha256 60fce2958ddc3d13)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -90,7 +90,7 @@ export const PLUGIN_ID = 'dsh-rerun-turn'
90
90
  export const name = PLUGIN_ID
91
91
 
92
92
  /** Keep in sync with package.json and lib/client.js. */
93
- export const PLUGIN_VERSION = '0.2.8'
93
+ export const PLUGIN_VERSION = '0.2.9'
94
94
 
95
95
  const ROUTE_PREFIX = '/dsh-rerun-turn'
96
96
  // A session id is an OPAQUE string to DSH: the host's own schema is
@@ -1,7 +1,7 @@
1
1
  {
2
- "dsh-edit-turn": "0.5.15",
3
- "dsh-rerun-turn": "0.2.8",
4
- "dsh-delete-turn": "0.2.1",
2
+ "dsh-edit-turn": "0.5.16",
3
+ "dsh-rerun-turn": "0.2.9",
4
+ "dsh-delete-turn": "0.2.2",
5
5
  "dsh-markdown-bubble": "0.1.5",
6
- "dsh-log-trim": "0.5.1"
6
+ "dsh-log-trim": "0.6.0"
7
7
  }