@wfm-cn/dsh-storage 0.0.0-stage → 0.3.0

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +137 -2
  4. package/cordis.patch.yml +3 -0
  5. package/lib/backends/database.js +578 -0
  6. package/lib/backfill.js +331 -0
  7. package/lib/client/index.js +79 -0
  8. package/lib/client/panel.js +299 -0
  9. package/lib/client/toast.js +66 -0
  10. package/lib/client.js +1 -0
  11. package/lib/compaction.js +108 -0
  12. package/lib/config.js +171 -0
  13. package/lib/ddl.js +323 -0
  14. package/lib/generated/mysql/browser.js +17 -0
  15. package/lib/generated/mysql/client.js +34 -0
  16. package/lib/generated/mysql/commonInputTypes.js +10 -0
  17. package/lib/generated/mysql/enums.js +10 -0
  18. package/lib/generated/mysql/internal/class.js +49 -0
  19. package/lib/generated/mysql/internal/prismaNamespace.js +146 -0
  20. package/lib/generated/mysql/internal/prismaNamespaceBrowser.js +117 -0
  21. package/lib/generated/mysql/models/AiChatHistory.js +1 -0
  22. package/lib/generated/mysql/models/AiMessage.js +1 -0
  23. package/lib/generated/mysql/models.js +1 -0
  24. package/lib/generated/postgresql/browser.js +17 -0
  25. package/lib/generated/postgresql/client.js +34 -0
  26. package/lib/generated/postgresql/commonInputTypes.js +10 -0
  27. package/lib/generated/postgresql/enums.js +10 -0
  28. package/lib/generated/postgresql/internal/class.js +49 -0
  29. package/lib/generated/postgresql/internal/prismaNamespace.js +131 -0
  30. package/lib/generated/postgresql/internal/prismaNamespaceBrowser.js +102 -0
  31. package/lib/generated/postgresql/models/AiChatHistory.js +1 -0
  32. package/lib/generated/postgresql/models/AiMessage.js +1 -0
  33. package/lib/generated/postgresql/models.js +1 -0
  34. package/lib/generated/sqlite/browser.js +17 -0
  35. package/lib/generated/sqlite/client.js +34 -0
  36. package/lib/generated/sqlite/commonInputTypes.js +10 -0
  37. package/lib/generated/sqlite/enums.js +10 -0
  38. package/lib/generated/sqlite/internal/class.js +49 -0
  39. package/lib/generated/sqlite/internal/prismaNamespace.js +128 -0
  40. package/lib/generated/sqlite/internal/prismaNamespaceBrowser.js +99 -0
  41. package/lib/generated/sqlite/models/AiChatHistory.js +1 -0
  42. package/lib/generated/sqlite/models/AiMessage.js +1 -0
  43. package/lib/generated/sqlite/models.js +1 -0
  44. package/lib/generated/sqlserver/browser.js +17 -0
  45. package/lib/generated/sqlserver/client.js +34 -0
  46. package/lib/generated/sqlserver/commonInputTypes.js +10 -0
  47. package/lib/generated/sqlserver/enums.js +10 -0
  48. package/lib/generated/sqlserver/internal/class.js +49 -0
  49. package/lib/generated/sqlserver/internal/prismaNamespace.js +119 -0
  50. package/lib/generated/sqlserver/internal/prismaNamespaceBrowser.js +90 -0
  51. package/lib/generated/sqlserver/models/AiChatHistory.js +1 -0
  52. package/lib/generated/sqlserver/models/AiMessage.js +1 -0
  53. package/lib/generated/sqlserver/models.js +1 -0
  54. package/lib/health.js +180 -0
  55. package/lib/index.js +681 -0
  56. package/lib/memory-tools.js +365 -0
  57. package/lib/notify.js +56 -0
  58. package/lib/projector.js +191 -0
  59. package/lib/status-route.js +172 -0
  60. package/lib/types/backends/database.d.ts +78 -0
  61. package/lib/types/backfill.d.ts +67 -0
  62. package/lib/types/client/index.d.ts +12 -0
  63. package/lib/types/client/panel.d.ts +79 -0
  64. package/lib/types/client/toast.d.ts +38 -0
  65. package/lib/types/compaction.d.ts +3 -0
  66. package/lib/types/config.d.ts +52 -0
  67. package/lib/types/ddl.d.ts +49 -0
  68. package/lib/types/generated/mysql/browser.d.ts +14 -0
  69. package/lib/types/generated/mysql/client.d.ts +33 -0
  70. package/lib/types/generated/mysql/commonInputTypes.d.ts +396 -0
  71. package/lib/types/generated/mysql/enums.d.ts +1 -0
  72. package/lib/types/generated/mysql/internal/class.d.ts +143 -0
  73. package/lib/types/generated/mysql/internal/prismaNamespace.d.ts +709 -0
  74. package/lib/types/generated/mysql/internal/prismaNamespaceBrowser.d.ts +112 -0
  75. package/lib/types/generated/mysql/models/AiChatHistory.d.ts +1258 -0
  76. package/lib/types/generated/mysql/models/AiMessage.d.ts +1359 -0
  77. package/lib/types/generated/mysql/models.d.ts +3 -0
  78. package/lib/types/generated/postgresql/browser.d.ts +14 -0
  79. package/lib/types/generated/postgresql/client.d.ts +33 -0
  80. package/lib/types/generated/postgresql/commonInputTypes.d.ts +392 -0
  81. package/lib/types/generated/postgresql/enums.d.ts +1 -0
  82. package/lib/types/generated/postgresql/internal/class.d.ts +143 -0
  83. package/lib/types/generated/postgresql/internal/prismaNamespace.d.ts +728 -0
  84. package/lib/types/generated/postgresql/internal/prismaNamespaceBrowser.d.ts +95 -0
  85. package/lib/types/generated/postgresql/models/AiChatHistory.d.ts +1373 -0
  86. package/lib/types/generated/postgresql/models/AiMessage.d.ts +1496 -0
  87. package/lib/types/generated/postgresql/models.d.ts +3 -0
  88. package/lib/types/generated/sqlite/browser.d.ts +14 -0
  89. package/lib/types/generated/sqlite/client.d.ts +33 -0
  90. package/lib/types/generated/sqlite/commonInputTypes.d.ts +373 -0
  91. package/lib/types/generated/sqlite/enums.d.ts +1 -0
  92. package/lib/types/generated/sqlite/internal/class.d.ts +143 -0
  93. package/lib/types/generated/sqlite/internal/prismaNamespace.d.ts +705 -0
  94. package/lib/types/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +92 -0
  95. package/lib/types/generated/sqlite/models/AiChatHistory.d.ts +1371 -0
  96. package/lib/types/generated/sqlite/models/AiMessage.d.ts +1493 -0
  97. package/lib/types/generated/sqlite/models.d.ts +3 -0
  98. package/lib/types/generated/sqlserver/browser.d.ts +14 -0
  99. package/lib/types/generated/sqlserver/client.d.ts +33 -0
  100. package/lib/types/generated/sqlserver/commonInputTypes.d.ts +333 -0
  101. package/lib/types/generated/sqlserver/enums.d.ts +1 -0
  102. package/lib/types/generated/sqlserver/internal/class.d.ts +143 -0
  103. package/lib/types/generated/sqlserver/internal/prismaNamespace.d.ts +669 -0
  104. package/lib/types/generated/sqlserver/internal/prismaNamespaceBrowser.d.ts +80 -0
  105. package/lib/types/generated/sqlserver/models/AiChatHistory.d.ts +1257 -0
  106. package/lib/types/generated/sqlserver/models/AiMessage.d.ts +1375 -0
  107. package/lib/types/generated/sqlserver/models.d.ts +3 -0
  108. package/lib/types/health.d.ts +52 -0
  109. package/lib/types/index.d.ts +91 -0
  110. package/lib/types/memory-tools.d.ts +168 -0
  111. package/lib/types/notify.d.ts +35 -0
  112. package/lib/types/projector.d.ts +51 -0
  113. package/lib/types/status-route.d.ts +48 -0
  114. package/lib/types/types.d.ts +58 -0
  115. package/lib/types.js +8 -0
  116. package/package.json +127 -4
  117. package/preview.png +0 -0
  118. package/prisma/schema.mysql.prisma +78 -0
  119. package/prisma/schema.postgresql.prisma +70 -0
  120. package/prisma/schema.sqlite.prisma +70 -0
  121. package/prisma/schema.sqlserver.prisma +76 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Settings-panel component for dsh-storage (会话存储镜像).
3
+ *
4
+ * Authored against the reference client plugins (dsh-pet / dsh-video-skin):
5
+ * the browser half receives React from the host module loader inside the
6
+ * __ModuleLoader__ factory, so the component is built through a factory that
7
+ * takes the React module — no module-scope React import survives into the
8
+ * bundle. All RPC goes through the injected `client` (connection rpc; see
9
+ * src/client/index.ts), which keeps this file render-pure and testable with
10
+ * a fake client.
11
+ */
12
+ export const DEFAULT_TIMEZONES = ['+08:00', '+00:00', '+09:00', '-05:00', '-08:00', '+05:30'];
13
+ /** Mirrors config.ts isValidPort — kept local so no node builtins enter the browser bundle. */
14
+ export function isValidPort(value) {
15
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 65535;
16
+ }
17
+ /**
18
+ * The cfg object to persist: the port is sent only when it is a valid
19
+ * integer — a cleared/garbage port field is omitted so the RPC rejects it
20
+ * with an honest per-field error instead of building a host:NaN URL.
21
+ */
22
+ export function buildSaveConfig(cfg) {
23
+ if (isValidPort(cfg.port))
24
+ return cfg;
25
+ const { port: _omitted, ...rest } = cfg;
26
+ return rest;
27
+ }
28
+ /** Status-light copy per HealthStatus.state. */
29
+ export function statusView(status) {
30
+ const details = [];
31
+ switch (status.state) {
32
+ case 'ok':
33
+ if (status.mirroredTotal !== undefined)
34
+ details.push(`已镜像 ${status.mirroredTotal} 条`);
35
+ if (status.lastOkAt !== undefined)
36
+ details.push(`最近成功 ${formatTime(status.lastOkAt)}`);
37
+ return { tone: 'ok', label: '运行正常', details };
38
+ case 'down':
39
+ details.push(`已重试 ${status.retryCount} 次`);
40
+ details.push(`最近成功 ${status.lastOkAt !== undefined ? formatTime(status.lastOkAt) : '—'}`);
41
+ if (status.lastError !== undefined)
42
+ details.push(status.lastError);
43
+ return { tone: 'bad', label: '连接中断', details };
44
+ case 'disabled':
45
+ return { tone: 'idle', label: '未启用', details };
46
+ case 'config-error':
47
+ if (status.lastError !== undefined)
48
+ details.push(status.lastError);
49
+ return { tone: 'bad', label: '配置错误', details };
50
+ }
51
+ }
52
+ function formatTime(ms) {
53
+ const date = new Date(ms);
54
+ const pad = (n) => String(n).padStart(2, '0');
55
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
56
+ }
57
+ /** Three-state connection-test copy: green ok / yellow no-tables / red fail. */
58
+ export function testResultView(result) {
59
+ if (result === 'ok')
60
+ return { tone: 'ok', label: '连接成功' };
61
+ if (result === 'no-tables')
62
+ return { tone: 'warn', label: '已连通,但数据表缺失——点「建表」' };
63
+ const reason = result.startsWith('fail:') ? result.slice('fail:'.length) : String(result);
64
+ return { tone: 'bad', label: `连接失败:${reason}` };
65
+ }
66
+ export function createResultView(result) {
67
+ if (result === 'ok')
68
+ return { tone: 'ok', label: '建表完成' };
69
+ const reason = result.slice('fail:'.length);
70
+ return { tone: 'bad', label: `建表失败:${reason}` };
71
+ }
72
+ /** 补传 summary copy: green when clean, yellow when some sessions failed. */
73
+ export function backfillResultView(summary) {
74
+ const base = `补传完成:${summary.sessions} 个会话,${summary.replayed} 条事件,${summary.skipped} 条重复跳过`;
75
+ if (summary.errors.length === 0)
76
+ return { tone: 'ok', label: base };
77
+ return { tone: 'warn', label: `${base}(${summary.errors.length} 个会话失败)` };
78
+ }
79
+ const FIELD_STYLE = 'dsh-storage-field';
80
+ const TONE_CLASS = {
81
+ ok: 'dsh-storage-tone-ok',
82
+ warn: 'dsh-storage-tone-warn',
83
+ bad: 'dsh-storage-tone-bad',
84
+ idle: 'dsh-storage-tone-idle',
85
+ };
86
+ export function makeStoragePanel(React) {
87
+ const h = React.createElement;
88
+ function StoragePanel(props) {
89
+ const client = props.client;
90
+ const pollMs = props.pollMs ?? 30_000;
91
+ const [cfg, setCfg] = React.useState(null);
92
+ const [configError, setConfigError] = React.useState(null);
93
+ const [snapshot, setSnapshot] = React.useState(null);
94
+ const [testResult, setTestResult] = React.useState(null);
95
+ const [createResult, setCreateResult] = React.useState(null);
96
+ const [backfillResult, setBackfillResult] = React.useState(null);
97
+ const [busy, setBusy] = React.useState(null);
98
+ const [saved, setSaved] = React.useState(false);
99
+ // Initial config load + status poll (30s).
100
+ React.useEffect(() => {
101
+ let disposed = false;
102
+ client
103
+ .getConfig()
104
+ .then((loaded) => {
105
+ if (!disposed) {
106
+ setCfg(loaded.config);
107
+ setConfigError(loaded.error === 'parse' ? '配置文件损坏,已加载默认值' : null);
108
+ }
109
+ })
110
+ .catch((error) => {
111
+ if (!disposed)
112
+ setConfigError(String(error));
113
+ });
114
+ const refresh = () => {
115
+ client
116
+ .snapshot()
117
+ .then((next) => {
118
+ if (!disposed)
119
+ setSnapshot(next);
120
+ })
121
+ .catch(() => { });
122
+ };
123
+ refresh();
124
+ const timer = setInterval(refresh, pollMs);
125
+ return () => {
126
+ disposed = true;
127
+ clearInterval(timer);
128
+ };
129
+ }, [client, pollMs]);
130
+ if (cfg === null) {
131
+ return h('div', { className: 'dsh-storage-panel', 'data-testid': 'dsh-storage-loading' }, configError === null ? '加载中…' : configError);
132
+ }
133
+ const isSqlite = cfg.provider === 'sqlite';
134
+ const set = (patch) => {
135
+ setCfg({ ...cfg, ...patch });
136
+ setSaved(false);
137
+ };
138
+ const runTest = () => {
139
+ setBusy('test');
140
+ setTestResult(null);
141
+ client
142
+ .testConnection(cfg)
143
+ .then(setTestResult)
144
+ .catch((error) => setTestResult(`fail:${String(error)}`))
145
+ .finally(() => setBusy(null));
146
+ };
147
+ const runCreate = () => {
148
+ setBusy('create');
149
+ setCreateResult(null);
150
+ client
151
+ .createTables(cfg)
152
+ .then(setCreateResult)
153
+ .catch((error) => setCreateResult(`fail:${String(error)}`))
154
+ .finally(() => setBusy(null));
155
+ };
156
+ const runSave = () => {
157
+ setBusy('save');
158
+ client
159
+ .saveConfig(buildSaveConfig(cfg))
160
+ .then(() => setSaved(true))
161
+ .catch((error) => setConfigError(String(error)))
162
+ .finally(() => setBusy(null));
163
+ };
164
+ const runBackfill = () => {
165
+ setBusy('backfill');
166
+ setBackfillResult(null);
167
+ client
168
+ .backfill()
169
+ .then(setBackfillResult)
170
+ .catch((error) => {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ setBackfillResult(`fail:${message}`);
173
+ })
174
+ .finally(() => setBusy(null));
175
+ };
176
+ const status = snapshot === null ? null : statusView(snapshot);
177
+ const test = testResult === null ? null : testResultView(testResult);
178
+ const created = createResult === null ? null : createResultView(createResult);
179
+ const backfilled = backfillResult === null
180
+ ? null
181
+ : typeof backfillResult === 'string'
182
+ ? { tone: 'bad', label: `补传失败:${backfillResult.slice('fail:'.length)}` }
183
+ : backfillResultView(backfillResult);
184
+ const field = (label, control, key) => h('label', { className: FIELD_STYLE, key }, h('span', { className: 'dsh-storage-label' }, label), control);
185
+ return h('div', { className: 'dsh-storage-panel' },
186
+ // Status light.
187
+ h('div', { className: 'dsh-storage-status', 'data-testid': 'dsh-storage-status' }, h('span', {
188
+ className: `dsh-storage-light ${status === null ? '' : TONE_CLASS[status.tone]}`,
189
+ 'data-testid': 'dsh-storage-light',
190
+ 'data-state': snapshot === null ? 'loading' : snapshot.state,
191
+ }, status === null ? '…' : status.label), status === null
192
+ ? null
193
+ : status.details.map((detail, index) => h('span', { className: 'dsh-storage-detail', key: index }, detail)), configError === null ? null : h('span', { className: 'dsh-storage-detail' }, configError)),
194
+ // Form.
195
+ h('div', { className: 'dsh-storage-form', 'data-testid': 'dsh-storage-form' }, field('启用同步', h('input', {
196
+ type: 'checkbox',
197
+ checked: cfg.enabled,
198
+ 'data-testid': 'dsh-storage-enabled',
199
+ onChange: (e) => set({ enabled: e.target.checked }),
200
+ }), 'enabled'), field('数据库类型', h('select', {
201
+ value: cfg.provider,
202
+ 'data-testid': 'dsh-storage-provider',
203
+ onChange: (e) => set({ provider: e.target.value }),
204
+ }, ['mysql', 'postgresql', 'sqlite', 'sqlserver'].map((provider) => h('option', { key: provider, value: provider }, provider))), 'provider'), ...(isSqlite
205
+ ? [
206
+ field('数据库文件路径', h('input', {
207
+ type: 'text',
208
+ value: cfg.database,
209
+ 'data-testid': 'dsh-storage-path',
210
+ placeholder: '/absolute/path/to/dsh-storage.sqlite',
211
+ onChange: (e) => set({ database: e.target.value }),
212
+ }), 'database'),
213
+ ]
214
+ : [
215
+ field('主机', h('input', {
216
+ type: 'text',
217
+ value: cfg.host,
218
+ 'data-testid': 'dsh-storage-host',
219
+ onChange: (e) => set({ host: e.target.value }),
220
+ }), 'host'),
221
+ field('端口', h('input', {
222
+ type: 'number',
223
+ value: cfg.port ?? '',
224
+ 'data-testid': 'dsh-storage-port',
225
+ // A cleared/invalid field is "unset" (undefined), never 0
226
+ // or NaN — buildSaveConfig omits it from the saved cfg.
227
+ onChange: (e) => {
228
+ const parsed = e.target.value === '' ? NaN : Number(e.target.value);
229
+ set({ port: isValidPort(parsed) ? parsed : undefined });
230
+ },
231
+ }), 'port'),
232
+ field('数据库', h('input', {
233
+ type: 'text',
234
+ value: cfg.database,
235
+ 'data-testid': 'dsh-storage-database',
236
+ onChange: (e) => set({ database: e.target.value }),
237
+ }), 'database'),
238
+ ]), field('用户名', h('input', {
239
+ type: 'text',
240
+ value: cfg.user,
241
+ 'data-testid': 'dsh-storage-user',
242
+ autoComplete: 'off',
243
+ onChange: (e) => set({ user: e.target.value }),
244
+ }), 'user'), field('密码', h('input', {
245
+ type: 'password',
246
+ value: cfg.password,
247
+ 'data-testid': 'dsh-storage-password',
248
+ autoComplete: 'new-password',
249
+ onChange: (e) => set({ password: e.target.value }),
250
+ }), 'password'), field('时区', h('select', {
251
+ value: cfg.timezone,
252
+ 'data-testid': 'dsh-storage-timezone',
253
+ onChange: (e) => set({ timezone: e.target.value }),
254
+ },
255
+ // A custom stored timezone becomes an option instead of silently
256
+ // displaying '+08:00' — which would mislead on the next save.
257
+ (DEFAULT_TIMEZONES.includes(cfg.timezone)
258
+ ? DEFAULT_TIMEZONES
259
+ : [cfg.timezone, ...DEFAULT_TIMEZONES]).map((tz) => h('option', { key: tz, value: tz }, tz))), 'timezone')),
260
+ // Actions.
261
+ h('div', { className: 'dsh-storage-actions' }, h('button', {
262
+ type: 'button',
263
+ 'data-testid': 'dsh-storage-save',
264
+ disabled: busy !== null,
265
+ onClick: runSave,
266
+ }, busy === 'save' ? '保存中…' : saved ? '已保存' : '保存配置'), h('button', {
267
+ type: 'button',
268
+ 'data-testid': 'dsh-storage-test',
269
+ disabled: busy !== null,
270
+ onClick: runTest,
271
+ }, busy === 'test' ? '测试中…' : '测试连接'), h('button', {
272
+ type: 'button',
273
+ 'data-testid': 'dsh-storage-create',
274
+ disabled: busy !== null,
275
+ onClick: runCreate,
276
+ }, busy === 'create' ? '建表中…' : '建表'), h('button', {
277
+ type: 'button',
278
+ 'data-testid': 'dsh-storage-backfill',
279
+ disabled: busy !== null,
280
+ onClick: runBackfill,
281
+ }, busy === 'backfill' ? '补传中…' : '补传')),
282
+ // Result lines.
283
+ test === null
284
+ ? null
285
+ : h('p', { className: `dsh-storage-result ${TONE_CLASS[test.tone]}`, 'data-testid': 'dsh-storage-test-result' }, test.label), created === null
286
+ ? null
287
+ : h('p', {
288
+ className: `dsh-storage-result ${TONE_CLASS[created.tone]}`,
289
+ 'data-testid': 'dsh-storage-create-result',
290
+ }, created.label), backfilled === null
291
+ ? null
292
+ : h('p', {
293
+ className: `dsh-storage-result ${TONE_CLASS[backfilled.tone]}`,
294
+ 'data-testid': 'dsh-storage-backfill-result',
295
+ }, backfilled.label));
296
+ }
297
+ StoragePanel.displayName = 'DshStoragePanel';
298
+ return StoragePanel;
299
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Sync-status toast notifications (Task 6): a standalone React portal the
3
+ * client entry mounts on document.body — independent of the settings panel
4
+ * section, so toasts appear even when the panel is closed. The host records
5
+ * toasts on HealthMonitor transitions (src/notify.ts); this component polls
6
+ * them through the status bridge with a `sinceId` cursor.
7
+ *
8
+ * Factory pattern like panel.tsx: React (and react-dom's createPortal) come
9
+ * from the host page's module loader, never bundled.
10
+ */
11
+ /** Default poll interval; transitions are rare, this only carries toasts. */
12
+ export const TOAST_POLL_MS = 5_000;
13
+ /** How long a toast stays on screen. */
14
+ export const TOAST_DISPLAY_MS = 8_000;
15
+ export function makeToastHost(React, ReactDOM) {
16
+ const h = React.createElement;
17
+ function ToastHost(props) {
18
+ const pollMs = props.pollMs ?? TOAST_POLL_MS;
19
+ const [visible, setVisible] = React.useState([]);
20
+ const cursor = React.useRef(0);
21
+ const dismiss = (id) => setVisible((prev) => prev.filter((t) => t.id !== id));
22
+ React.useEffect(() => {
23
+ let disposed = false;
24
+ const timers = new Set();
25
+ const poll = () => {
26
+ props.client
27
+ .toasts({ sinceId: cursor.current })
28
+ .then((records) => {
29
+ if (disposed || records.length === 0)
30
+ return;
31
+ for (const record of records)
32
+ cursor.current = Math.max(cursor.current, record.id);
33
+ setVisible((prev) => [...prev, ...records]);
34
+ for (const record of records) {
35
+ const timer = setTimeout(() => {
36
+ timers.delete(timer);
37
+ dismiss(record.id);
38
+ }, TOAST_DISPLAY_MS);
39
+ timers.add(timer);
40
+ }
41
+ })
42
+ .catch(() => {
43
+ // The bridge being down must not break the page; next poll retries.
44
+ });
45
+ };
46
+ poll();
47
+ const timer = setInterval(poll, pollMs);
48
+ return () => {
49
+ disposed = true;
50
+ clearInterval(timer);
51
+ for (const t of timers)
52
+ clearTimeout(t);
53
+ };
54
+ }, [props.client, pollMs]);
55
+ return ReactDOM.createPortal(h('div', { className: 'dsh-storage-toasts', 'data-testid': 'dsh-storage-toasts' }, ...visible.map((toast) => h('div', {
56
+ key: toast.id,
57
+ className: `dsh-storage-toast dsh-storage-toast-${toast.kind}`,
58
+ role: 'status',
59
+ onClick: () => dismiss(toast.id),
60
+ }, h('span', { className: 'dsh-storage-toast-text' }, toast.text), toast.hint === undefined
61
+ ? null
62
+ : h('span', { className: 'dsh-storage-toast-hint' }, toast.hint)))), document.body);
63
+ }
64
+ ToastHost.displayName = 'DshStorageToasts';
65
+ return ToastHost;
66
+ }
package/lib/client.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";(()=>{var M="dsh-storage-select-open",oe={display:"inline-flex",alignItems:"center",justifyContent:"space-between",gap:8,background:"var(--dsw-alias-bg-multi-select)",border:"1px solid var(--dsw-alias-border-l2)",borderRadius:"8px",padding:"4px 10px",fontSize:13,color:"var(--dsw-alias-label-primary)",outline:"none",minWidth:0,width:220,cursor:"pointer",textAlign:"left",fontFamily:"inherit"},se={position:"fixed",background:"var(--dsw-alias-bg-layer-1)",border:"1px solid var(--dsw-alias-border-l2)",borderRadius:"12px",padding:"6px",boxShadow:"0 8px 24px rgba(0,0,0,.4)",zIndex:1e3,boxSizing:"border-box"};function le(e){return{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,padding:"8px 12px",borderRadius:"8px",fontSize:13,color:"var(--dsw-alias-label-primary)",cursor:"pointer",background:e?"var(--dsw-alias-interactive-bg-hover)":"transparent",whiteSpace:"nowrap"}}function q(e){let t=e.createElement;return function(o){let[m,g]=e.useState(!1),[c,p]=e.useState(0),a=e.useRef(null),[s,i]=e.useState(null),k=e.useCallback(()=>g(!1),[]);e.useEffect(()=>{if(!m)return;let r=()=>g(!1),l=w=>{let y=a.current;if(y&&w.target instanceof Node&&y.contains(w.target))return;let v=w.target;v&&v.closest("[data-dsh-select-popover]")||g(!1)},b=w=>{w.key==="Escape"&&g(!1)};return window.addEventListener(M,r),window.addEventListener("mousedown",l),window.addEventListener("keydown",b),()=>{window.removeEventListener(M,r),window.removeEventListener("mousedown",l),window.removeEventListener("keydown",b)}},[m]);let u=()=>{if(m){g(!1);return}let r=a.current,l=r!==null&&typeof r.getBoundingClientRect=="function"?r.getBoundingClientRect():{left:0,top:0,bottom:0,width:0};i({left:l.left,top:l.bottom+4,width:l.width}),p(Math.max(0,o.options.indexOf(o.value))),window.dispatchEvent(new window.Event(M)),g(!0)},T=r=>{if(r.key==="Enter"||r.key===" "){r.preventDefault(),u();return}if(r.key==="ArrowDown"||r.key==="ArrowUp"){if(r.preventDefault(),!m){u();return}let l=r.key==="ArrowDown"?1:-1,b=o.options.length;p(w=>(w+l+b)%b)}},x=r=>{o.onChange(r),g(!1)},E=r=>{if(r.key==="ArrowDown"||r.key==="ArrowUp"){r.preventDefault();let l=r.key==="ArrowDown"?1:-1,b=o.options.length;p(w=>(w+l+b)%b)}else if(r.key==="Enter"||r.key===" "){r.preventDefault();let l=o.options[c];l!==void 0&&x(l)}};return t("div",{style:{position:"relative",display:"inline-block"}},t("button",{type:"button",ref:a,style:oe,"data-testid":o.testId,"aria-haspopup":"listbox","aria-expanded":m?"true":"false",disabled:o.disabled===!0,onClick:u,onKeyDown:T},t("span",{style:{overflow:"hidden",textOverflow:"ellipsis"}},o.value),t("span",{"aria-hidden":"true",style:{color:"var(--dsw-alias-label-tertiary)"}},"\u25BE")),m?t("div",{role:"listbox","data-dsh-select-popover":"","data-testid":o.testId===void 0?void 0:`${o.testId}-listbox`,style:{...se,minWidth:s===null?220:s.width,left:s===null?0:s.left,top:s===null?0:s.top},onKeyDown:E},o.options.map((r,l)=>t("div",{key:r,role:"option","aria-selected":r===o.value?"true":"false","data-testid":o.testId===void 0?void 0:`${o.testId}-option-${r}`,style:le(r===o.value||l===c),onMouseEnter:()=>p(l),onClick:()=>x(r)},t("span",null,r),r===o.value?t("span",{"aria-hidden":"true",style:{color:"var(--dsw-alias-label-primary)"}},"\u2713"):null))):null)}}var N=["+08:00","+00:00","+09:00","-05:00","-08:00","+05:30"];function Y(e){return typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=65535}function ie(e){if(Y(e.port))return e;let{port:t,...f}=e;return f}var d={bgCard:"var(--dsw-alias-bg-layer-1)",bgControl:"var(--dsw-alias-bg-multi-select)",border:"1px solid var(--dsw-alias-border-l2)",radiusCard:"12px",radiusControl:"8px",labelPrimary:"var(--dsw-alias-label-primary)",labelSecondary:"var(--dsw-alias-label-secondary)",labelTertiary:"var(--dsw-alias-label-tertiary)",accent:"var(--dsw-alias-state-business-primary)",ok:"var(--dsw-alias-state-ok, #34c759)",warn:"var(--dsw-alias-state-warn, #d9a410)",error:"var(--dsw-alias-state-error-primary)"},H={ok:d.ok,warn:d.warn,bad:d.error,idle:d.labelTertiary},de={ok:"color-mix(in srgb, var(--dsw-alias-state-ok, #34c759) 14%, transparent)",warn:"color-mix(in srgb, var(--dsw-alias-state-warn, #d9a410) 16%, transparent)",bad:"color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 14%, transparent)",idle:"var(--dsw-alias-bg-multi-select)"},P={background:d.bgCard,border:d.border,borderRadius:d.radiusCard,padding:"16px 18px",boxSizing:"border-box"},U={fontSize:12,lineHeight:"16px",color:d.labelTertiary,margin:0,marginBottom:10},ue={display:"flex",alignItems:"center",justifyContent:"space-between",gap:12,minHeight:32},ce={fontSize:13,color:d.labelSecondary},B={background:d.bgControl,border:d.border,borderRadius:d.radiusControl,padding:"4px 10px",fontSize:13,color:d.labelPrimary,outline:"none",minWidth:0},ge={borderRadius:d.radiusControl,padding:"6px 14px",fontSize:13,border:"none",background:d.accent,color:"var(--dsw-alias-label-contrast, #fff)",cursor:"pointer"},W={borderRadius:d.radiusControl,padding:"6px 14px",fontSize:13,background:"transparent",border:d.border,color:d.labelPrimary,cursor:"pointer"},I={opacity:.5,cursor:"default"};function j(e){return{display:"inline-flex",alignItems:"center",gap:6,borderRadius:"999px",padding:"3px 10px",fontSize:12,background:de[e],color:H[e],margin:0,maxWidth:"100%",overflowWrap:"anywhere"}}function pe(e){let t=[];switch(e.state){case"ok":return e.mirroredTotal!==void 0&&t.push(`\u5DF2\u955C\u50CF ${e.mirroredTotal} \u6761`),e.lastOkAt!==void 0&&t.push(`\u6700\u8FD1\u6210\u529F ${F(e.lastOkAt)}`),{tone:"ok",label:"\u8FD0\u884C\u6B63\u5E38",details:t};case"down":return t.push(`\u5DF2\u91CD\u8BD5 ${e.retryCount} \u6B21`),t.push(`\u6700\u8FD1\u6210\u529F ${e.lastOkAt!==void 0?F(e.lastOkAt):"\u2014"}`),e.lastError!==void 0&&t.push(e.lastError),{tone:"bad",label:"\u8FDE\u63A5\u4E2D\u65AD",details:t};case"disabled":return{tone:"idle",label:"\u672A\u542F\u7528",details:t};case"config-error":return e.lastError!==void 0&&t.push(e.lastError),{tone:"bad",label:"\u914D\u7F6E\u9519\u8BEF",details:t}}}function F(e){let t=new Date(e),f=o=>String(o).padStart(2,"0");return`${f(t.getHours())}:${f(t.getMinutes())}:${f(t.getSeconds())}`}function be(e){return e==="ok"?{tone:"ok",label:"\u8FDE\u63A5\u6210\u529F"}:e==="no-tables"?{tone:"warn",label:"\u5DF2\u8FDE\u901A\uFF0C\u4F46\u6570\u636E\u8868\u7F3A\u5931\u2014\u2014\u70B9\u300C\u5EFA\u8868\u300D"}:{tone:"bad",label:`\u8FDE\u63A5\u5931\u8D25\uFF1A${e.startsWith("fail:")?e.slice(5):String(e)}`}}function fe(e){return e==="ok"?{tone:"ok",label:"\u5EFA\u8868\u5B8C\u6210"}:{tone:"bad",label:`\u5EFA\u8868\u5931\u8D25\uFF1A${e.slice(5)}`}}function ye(e){let t=`\u8865\u4F20\u5B8C\u6210\uFF1A${e.sessions} \u4E2A\u4F1A\u8BDD\uFF0C${e.replayed} \u6761\u4E8B\u4EF6\uFF0C${e.skipped} \u6761\u91CD\u590D\u8DF3\u8FC7`;return e.errors.length===0?{tone:"ok",label:t}:{tone:"warn",label:`${t}\uFF08${e.errors.length} \u4E2A\u4F1A\u8BDD\u5931\u8D25\uFF09`}}function G(e){let t=e.createElement,f=q(e);function o(g,c,p){return t("div",{style:ue,key:p},t("span",{style:ce},g),c)}function m(g){let c=g.client,p=g.pollMs??3e4,[a,s]=e.useState(null),[i,k]=e.useState(null),[u,T]=e.useState(null),[x,E]=e.useState(null),[r,l]=e.useState(null),[b,w]=e.useState(null),[y,v]=e.useState(null),[J,K]=e.useState(!1),z=e.useRef(()=>{});e.useEffect(()=>{let n=!1;c.getConfig().then(C=>{n||(s(C.config),k(C.error==="parse"?"\u914D\u7F6E\u6587\u4EF6\u635F\u574F\uFF0C\u5DF2\u52A0\u8F7D\u9ED8\u8BA4\u503C":null))}).catch(C=>{n||k(String(C))});let h=()=>{c.snapshot().then(C=>{n||T(C)}).catch(()=>{})};z.current=h,h();let L=setInterval(h,p);return()=>{n=!0,clearInterval(L)}},[c,p]);let Q=n=>{setTimeout(()=>{z.current()},n)};if(a===null)return t("div",{style:P,"data-testid":"dsh-storage-loading"},i===null?"\u52A0\u8F7D\u4E2D\u2026":i);let X=a.provider==="sqlite",S=n=>{s({...a,...n}),K(!1)},ee=()=>{v("test"),E(null),c.testConnection(a).then(E).catch(n=>E(`fail:${String(n)}`)).finally(()=>{v(null),z.current()})},te=()=>{v("create"),l(null),c.createTables(a).then(l).catch(n=>l(`fail:${String(n)}`)).finally(()=>v(null))},ne=()=>{v("save"),c.saveConfig(ie(a)).then(()=>K(!0)).then(()=>{Q(400)}).catch(n=>k(String(n))).finally(()=>v(null))},re=()=>{v("backfill"),w(null),c.backfill().then(w).catch(n=>{let h=n instanceof Error?n.message:String(n);w(`fail:${h}`)}).finally(()=>v(null))},R=u===null?null:pe(u),A=x===null?null:be(x),$=r===null?null:fe(r),_=b===null?null:typeof b=="string"?{tone:"bad",label:`\u8865\u4F20\u5931\u8D25\uFF1A${b.slice(5)}`}:ye(b),D=n=>({type:"text",style:{...B,width:220},...n}),ae=n=>t("input",{type:"checkbox",...n}),V=(n,h,L,C)=>t(f,{value:h,options:L,onChange:C,testId:n});return t("div",{style:{display:"flex",flexDirection:"column",gap:14}},t("div",{style:P,"data-testid":"dsh-storage-status"},t("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"}},t("span",{style:{width:8,height:8,borderRadius:"50%",background:u===null?d.labelTertiary:H[R.tone],flexShrink:0},"data-testid":"dsh-storage-light","data-state":u===null?"loading":u.state}),t("span",{style:{fontSize:13,color:u===null||R.tone==="idle"?d.labelTertiary:d.labelPrimary}},R===null?"\u2026":R.label),R===null?null:R.details.map((n,h)=>t("span",{key:h,style:{fontSize:12,color:d.labelTertiary}},n)),t("span",{style:{fontSize:12,color:d.labelTertiary},"data-testid":"dsh-storage-device-badge"},`\u8BBE\u5907: ${a?.device??"\u2014"}`),i===null?null:t("span",{style:{fontSize:12,color:H.bad}},i))),t("div",{style:P,"data-testid":"dsh-storage-form"},t("p",{style:U},"\u8FDE\u63A5\u8BBE\u7F6E"),t("div",{style:{display:"flex",flexDirection:"column",gap:12}},o("\u542F\u7528\u540C\u6B65",ae({checked:a.enabled,"data-testid":"dsh-storage-enabled",onChange:n=>S({enabled:n.target.checked})}),"enabled"),o("\u6570\u636E\u5E93\u7C7B\u578B",V("dsh-storage-provider",a.provider,["mysql","postgresql","sqlite","sqlserver"],n=>S({provider:n})),"provider"),...X?[o("\u6570\u636E\u5E93\u6587\u4EF6\u8DEF\u5F84",t("input",D({value:a.database,"data-testid":"dsh-storage-path",placeholder:"/absolute/path/to/dsh-storage.sqlite",onChange:n=>S({database:n.target.value})})),"database")]:[o("\u4E3B\u673A",t("input",D({value:a.host,"data-testid":"dsh-storage-host",onChange:n=>S({host:n.target.value})})),"host"),o("\u7AEF\u53E3",t("input",{type:"number",style:{...B,width:220},value:a.port??"","data-testid":"dsh-storage-port",onChange:n=>{let h=n.target.value===""?NaN:Number(n.target.value);S({port:Y(h)?h:void 0})}}),"port"),o("\u6570\u636E\u5E93",t("input",D({value:a.database,"data-testid":"dsh-storage-database",onChange:n=>S({database:n.target.value})})),"database")],o("\u8BBE\u5907\u540D\u79F0",t("input",D({value:a.device??"","data-testid":"dsh-storage-device",placeholder:"\u9ED8\u8BA4\u4E3A\u4E3B\u673A\u540D",onChange:n=>S({device:n.target.value})})),"device"),o("\u7528\u6237\u540D",t("input",D({value:a.user,"data-testid":"dsh-storage-user",autoComplete:"off",onChange:n=>S({user:n.target.value})})),"user"),o("\u5BC6\u7801",t("input",{type:"password",style:{...B,width:220},value:a.password,"data-testid":"dsh-storage-password",autoComplete:"new-password",onChange:n=>S({password:n.target.value})}),"password"),o("\u65F6\u533A",V("dsh-storage-timezone",a.timezone,N.includes(a.timezone)?N:[a.timezone,...N],n=>S({timezone:n})),"timezone"))),t("div",{style:P},t("p",{style:U},"\u64CD\u4F5C"),t("div",{style:{display:"flex",gap:8,flexWrap:"wrap"}},t("button",{type:"button",style:{...ge,...y!==null?I:{}},"data-testid":"dsh-storage-save",disabled:y!==null,onClick:ne},y==="save"?"\u4FDD\u5B58\u4E2D\u2026":J?"\u5DF2\u4FDD\u5B58":"\u4FDD\u5B58"),t("button",{type:"button",style:{...W,...y!==null?I:{}},"data-testid":"dsh-storage-test",disabled:y!==null,onClick:ee},y==="test"?"\u6D4B\u8BD5\u4E2D\u2026":"\u6D4B\u8BD5\u8FDE\u63A5"),t("button",{type:"button",style:{...W,...y!==null?I:{}},"data-testid":"dsh-storage-create",disabled:y!==null,onClick:te},y==="create"?"\u5EFA\u8868\u4E2D\u2026":"\u5EFA\u8868"),t("button",{type:"button",style:{...W,...y!==null?I:{}},"data-testid":"dsh-storage-backfill",disabled:y!==null,onClick:re},y==="backfill"?"\u8865\u4F20\u4E2D\u2026":"\u8865\u4F20")),A===null?null:t("div",{style:{marginTop:12}},t("p",{style:j(A.tone),"data-testid":"dsh-storage-test-result"},A.label)),$===null?null:t("div",{style:{marginTop:12}},t("p",{style:j($.tone),"data-testid":"dsh-storage-create-result"},$.label)),_===null?null:t("div",{style:{marginTop:12}},t("p",{style:j(_.tone),"data-testid":"dsh-storage-backfill-result"},_.label))))}return m.displayName="DshStoragePanel",m}function Z(e,t){let f=e.createElement,o={background:"var(--dsw-alias-bg-layer-1)",border:"1px solid var(--dsw-alias-border-l2)",borderRadius:"12px",padding:"10px 14px",color:"var(--dsw-alias-label-primary)",fontSize:13,boxShadow:"0 4px 16px rgba(0, 0, 0, 0.12)",maxWidth:360,cursor:"pointer"},m={interrupted:"var(--dsw-alias-state-error-primary)",recovered:"var(--dsw-alias-state-ok, #34c759)"};function g(c){let p=c.pollMs??5e3,[a,s]=e.useState([]),i=e.useRef(0),k=u=>s(T=>T.filter(x=>x.id!==u));return e.useEffect(()=>{let u=!1,T=new Set,x=()=>{c.client.toasts({sinceId:i.current}).then(r=>{if(!(u||r.length===0)){for(let l of r)i.current=Math.max(i.current,l.id);s(l=>[...l,...r]);for(let l of r){let b=setTimeout(()=>{T.delete(b),k(l.id)},8e3);T.add(b)}}}).catch(()=>{})};x();let E=setInterval(x,p);return()=>{u=!0,clearInterval(E);for(let r of T)clearTimeout(r)}},[c.client,p]),t.createPortal(f("div",{style:{position:"fixed",right:16,bottom:16,display:"flex",flexDirection:"column",gap:8,zIndex:9999},"data-testid":"dsh-storage-toasts"},...a.map(u=>f("div",{key:u.id,style:{...o,borderLeft:`3px solid ${m[u.kind]??"var(--dsw-alias-border-l2)"}`},className:`dsh-storage-toast dsh-storage-toast-${u.kind}`,role:"status",onClick:()=>k(u.id)},f("div",{style:{color:"var(--dsw-alias-label-primary)"}},u.text),u.hint===void 0?null:f("div",{style:{fontSize:12,marginTop:4,color:"var(--dsw-alias-label-secondary)"}},u.hint)))),globalThis.document.body)}return g.displayName="DshStorageToasts",g}var me="@wfm-cn/dsh-storage",we="/dsh-storage";function he(e){if(e&&e.ok===!0)return e.value;let t=e?.error?.message||"\u8BF7\u6C42\u5931\u8D25";throw new Error(t)}function ve(){return e=>{let t=e("react"),f=G(t),o=Z(t,e("react-dom"));function m(a){let s=(i,k={})=>a.connection.rpc.call(we,i,k).then(he);return{snapshot:()=>s("snapshot"),testConnection:i=>s("testConnection",{cfg:i}),createTables:i=>s("createTables",{cfg:i}),getConfig:()=>s("getConfig"),saveConfig:i=>s("saveConfig",{cfg:i}),backfill:()=>s("backfill"),toasts:i=>s("toasts",i)}}function g(a){if(typeof globalThis.document>"u")return;let s=e("react-dom/client");if(!s||typeof s.createRoot!="function")return;let i=globalThis.document.createElement("div");i.className="dsh-storage-toast-root",globalThis.document.body.appendChild(i),s.createRoot(i).render(t.createElement(o,{client:a}))}function c(a){let s=m(a);g(s),!(!a.slots||typeof a.slots.inject!="function")&&a.slots.inject("settings.section",()=>a.slots.register({name:"settings.section",id:"dsh-storage",order:60,label:()=>"\u4F1A\u8BDD\u5B58\u50A8",inject:()=>({client:s})},f))}let p={exports:{}};return p.exports.apply=c,p.exports.inject=["slots","connection"],p.exports}}var O=globalThis.__ModuleLoader__;typeof O=="object"&&O!==null&&typeof O.load=="function"&&O.load({id:me,factory:ve()});})();
@@ -0,0 +1,108 @@
1
+ function recordedData(block) {
2
+ return { text: `[Recorded DSH ${block.type} data]\n${JSON.stringify(block)}` };
3
+ }
4
+ /** Legacy Content[] checkpoints remain readable by databaseChatStorage. */
5
+ export function compressionSnapshot(session, event) {
6
+ if (event?.type !== 'compaction/end' || event.data?.error !== undefined)
7
+ return null;
8
+ const calls = new Map();
9
+ const messages = session.deriveMessages().flatMap((message) => {
10
+ if (message.role === 'system')
11
+ return [];
12
+ if (message.role === 'tool') {
13
+ const id = message.toolCallId ?? message.source?.callId;
14
+ const name = calls.get(id);
15
+ if (!name)
16
+ return [
17
+ {
18
+ role: 'user',
19
+ parts: [
20
+ recordedData({
21
+ type: 'tool-result',
22
+ toolCallId: id,
23
+ content: message.content,
24
+ isError: Boolean(message.isError),
25
+ }),
26
+ ],
27
+ },
28
+ ];
29
+ return [
30
+ {
31
+ role: 'user',
32
+ parts: [
33
+ {
34
+ functionResponse: {
35
+ id,
36
+ name,
37
+ response: { output: message.content, isError: Boolean(message.isError) },
38
+ },
39
+ },
40
+ ],
41
+ },
42
+ ];
43
+ }
44
+ const parts = message.content.map((block, index) => {
45
+ if (message.role === 'developer')
46
+ return recordedData(block);
47
+ switch (block.type) {
48
+ case 'text':
49
+ return { text: block.text };
50
+ case 'reasoning': {
51
+ const replay = message.source?.replayState?.blocks?.[index];
52
+ const signature = replay?.signature ?? replay?.thoughtSignature;
53
+ return {
54
+ text: block.text,
55
+ thought: true,
56
+ ...(signature ? { thoughtSignature: signature } : {}),
57
+ };
58
+ }
59
+ case 'tool-call': {
60
+ calls.delete(block.id);
61
+ let args;
62
+ try {
63
+ args = block.arguments ? JSON.parse(block.arguments) : {};
64
+ }
65
+ catch {
66
+ return recordedData(block);
67
+ }
68
+ if (!args || typeof args !== 'object' || Array.isArray(args))
69
+ return recordedData(block);
70
+ calls.set(block.id, block.name);
71
+ return {
72
+ functionCall: { id: block.id, name: block.name, args },
73
+ };
74
+ }
75
+ default:
76
+ return recordedData(block);
77
+ }
78
+ });
79
+ return [{ role: message.role === 'assistant' ? 'model' : 'user', parts }];
80
+ });
81
+ const content = [];
82
+ for (const message of messages) {
83
+ const previous = content.at(-1);
84
+ // Legacy recovery discards adjacent equal roles instead of merging them.
85
+ if (previous && previous.role === message.role)
86
+ previous.parts.push(...message.parts);
87
+ else
88
+ content.push(message);
89
+ }
90
+ return {
91
+ id: event.data.compactionId,
92
+ sessionId: session.id,
93
+ historyId: null,
94
+ agentId: 'main',
95
+ type: 'utility_compressor',
96
+ content: JSON.stringify(content),
97
+ model: 'agent-history-provider-summarizer',
98
+ createdAt: new Date(event.time),
99
+ metadata: {
100
+ id: event.data.compactionId,
101
+ event: event.type,
102
+ seq: event.seq,
103
+ sessionCreatedAt: session.header?.createdAt,
104
+ sessionId: session.id,
105
+ newHistoryLength: content.length,
106
+ },
107
+ };
108
+ }