@wfm-cn/dsh-storage 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +128 -2
- package/cordis.patch.yml +3 -0
- package/lib/backends/database.js +578 -0
- package/lib/backfill.js +331 -0
- package/lib/client/index.js +79 -0
- package/lib/client/panel.js +299 -0
- package/lib/client/toast.js +66 -0
- package/lib/client.js +1 -0
- package/lib/compaction.js +108 -0
- package/lib/config.js +171 -0
- package/lib/ddl.js +323 -0
- package/lib/generated/mysql/browser.js +17 -0
- package/lib/generated/mysql/client.js +34 -0
- package/lib/generated/mysql/commonInputTypes.js +10 -0
- package/lib/generated/mysql/enums.js +10 -0
- package/lib/generated/mysql/internal/class.js +49 -0
- package/lib/generated/mysql/internal/prismaNamespace.js +146 -0
- package/lib/generated/mysql/internal/prismaNamespaceBrowser.js +117 -0
- package/lib/generated/mysql/models/AiChatHistory.js +1 -0
- package/lib/generated/mysql/models/AiMessage.js +1 -0
- package/lib/generated/mysql/models.js +1 -0
- package/lib/generated/postgresql/browser.js +17 -0
- package/lib/generated/postgresql/client.js +34 -0
- package/lib/generated/postgresql/commonInputTypes.js +10 -0
- package/lib/generated/postgresql/enums.js +10 -0
- package/lib/generated/postgresql/internal/class.js +49 -0
- package/lib/generated/postgresql/internal/prismaNamespace.js +131 -0
- package/lib/generated/postgresql/internal/prismaNamespaceBrowser.js +102 -0
- package/lib/generated/postgresql/models/AiChatHistory.js +1 -0
- package/lib/generated/postgresql/models/AiMessage.js +1 -0
- package/lib/generated/postgresql/models.js +1 -0
- package/lib/generated/sqlite/browser.js +17 -0
- package/lib/generated/sqlite/client.js +34 -0
- package/lib/generated/sqlite/commonInputTypes.js +10 -0
- package/lib/generated/sqlite/enums.js +10 -0
- package/lib/generated/sqlite/internal/class.js +49 -0
- package/lib/generated/sqlite/internal/prismaNamespace.js +128 -0
- package/lib/generated/sqlite/internal/prismaNamespaceBrowser.js +99 -0
- package/lib/generated/sqlite/models/AiChatHistory.js +1 -0
- package/lib/generated/sqlite/models/AiMessage.js +1 -0
- package/lib/generated/sqlite/models.js +1 -0
- package/lib/generated/sqlserver/browser.js +17 -0
- package/lib/generated/sqlserver/client.js +34 -0
- package/lib/generated/sqlserver/commonInputTypes.js +10 -0
- package/lib/generated/sqlserver/enums.js +10 -0
- package/lib/generated/sqlserver/internal/class.js +49 -0
- package/lib/generated/sqlserver/internal/prismaNamespace.js +119 -0
- package/lib/generated/sqlserver/internal/prismaNamespaceBrowser.js +90 -0
- package/lib/generated/sqlserver/models/AiChatHistory.js +1 -0
- package/lib/generated/sqlserver/models/AiMessage.js +1 -0
- package/lib/generated/sqlserver/models.js +1 -0
- package/lib/health.js +180 -0
- package/lib/index.js +676 -0
- package/lib/notify.js +56 -0
- package/lib/projector.js +191 -0
- package/lib/status-route.js +172 -0
- package/lib/types/backends/database.d.ts +78 -0
- package/lib/types/backfill.d.ts +67 -0
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/panel.d.ts +79 -0
- package/lib/types/client/toast.d.ts +38 -0
- package/lib/types/compaction.d.ts +3 -0
- package/lib/types/config.d.ts +52 -0
- package/lib/types/ddl.d.ts +49 -0
- package/lib/types/generated/mysql/browser.d.ts +14 -0
- package/lib/types/generated/mysql/client.d.ts +33 -0
- package/lib/types/generated/mysql/commonInputTypes.d.ts +396 -0
- package/lib/types/generated/mysql/enums.d.ts +1 -0
- package/lib/types/generated/mysql/internal/class.d.ts +143 -0
- package/lib/types/generated/mysql/internal/prismaNamespace.d.ts +709 -0
- package/lib/types/generated/mysql/internal/prismaNamespaceBrowser.d.ts +112 -0
- package/lib/types/generated/mysql/models/AiChatHistory.d.ts +1258 -0
- package/lib/types/generated/mysql/models/AiMessage.d.ts +1359 -0
- package/lib/types/generated/mysql/models.d.ts +3 -0
- package/lib/types/generated/postgresql/browser.d.ts +14 -0
- package/lib/types/generated/postgresql/client.d.ts +33 -0
- package/lib/types/generated/postgresql/commonInputTypes.d.ts +392 -0
- package/lib/types/generated/postgresql/enums.d.ts +1 -0
- package/lib/types/generated/postgresql/internal/class.d.ts +143 -0
- package/lib/types/generated/postgresql/internal/prismaNamespace.d.ts +728 -0
- package/lib/types/generated/postgresql/internal/prismaNamespaceBrowser.d.ts +95 -0
- package/lib/types/generated/postgresql/models/AiChatHistory.d.ts +1373 -0
- package/lib/types/generated/postgresql/models/AiMessage.d.ts +1496 -0
- package/lib/types/generated/postgresql/models.d.ts +3 -0
- package/lib/types/generated/sqlite/browser.d.ts +14 -0
- package/lib/types/generated/sqlite/client.d.ts +33 -0
- package/lib/types/generated/sqlite/commonInputTypes.d.ts +373 -0
- package/lib/types/generated/sqlite/enums.d.ts +1 -0
- package/lib/types/generated/sqlite/internal/class.d.ts +143 -0
- package/lib/types/generated/sqlite/internal/prismaNamespace.d.ts +705 -0
- package/lib/types/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +92 -0
- package/lib/types/generated/sqlite/models/AiChatHistory.d.ts +1371 -0
- package/lib/types/generated/sqlite/models/AiMessage.d.ts +1493 -0
- package/lib/types/generated/sqlite/models.d.ts +3 -0
- package/lib/types/generated/sqlserver/browser.d.ts +14 -0
- package/lib/types/generated/sqlserver/client.d.ts +33 -0
- package/lib/types/generated/sqlserver/commonInputTypes.d.ts +333 -0
- package/lib/types/generated/sqlserver/enums.d.ts +1 -0
- package/lib/types/generated/sqlserver/internal/class.d.ts +143 -0
- package/lib/types/generated/sqlserver/internal/prismaNamespace.d.ts +669 -0
- package/lib/types/generated/sqlserver/internal/prismaNamespaceBrowser.d.ts +80 -0
- package/lib/types/generated/sqlserver/models/AiChatHistory.d.ts +1257 -0
- package/lib/types/generated/sqlserver/models/AiMessage.d.ts +1375 -0
- package/lib/types/generated/sqlserver/models.d.ts +3 -0
- package/lib/types/health.d.ts +52 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/notify.d.ts +35 -0
- package/lib/types/projector.d.ts +51 -0
- package/lib/types/status-route.d.ts +48 -0
- package/lib/types/types.d.ts +58 -0
- package/lib/types.js +8 -0
- package/package.json +126 -3
- package/preview.png +0 -0
- package/prisma/schema.mysql.prisma +78 -0
- package/prisma/schema.postgresql.prisma +70 -0
- package/prisma/schema.sqlite.prisma +70 -0
- 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
|
+
}
|