dsh-session-rollover 0.0.0-stage → 1.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 +30 -0
- package/LICENSE +201 -0
- package/LICENSE.upstream +21 -0
- package/NOTICE +9 -0
- package/README.md +157 -2
- package/assets/icon.png +0 -0
- package/assets/icon.svg +7 -0
- package/client.js +656 -0
- package/compatibility/policy.json +21 -0
- package/compatibility/runtime-0.2.2-rc.1.json +32 -0
- package/cordis.patch.yml +9 -0
- package/docs/COMPATIBILITY.md +70 -0
- package/docs/DETAIL.md +44 -0
- package/docs/DETAIL.zh-CN.md +49 -0
- package/docs/DEVELOPMENT.md +104 -0
- package/docs/PUBLISHING.md +80 -0
- package/docs/architecture.md +71 -0
- package/docs/live-host-validation.json +37 -0
- package/docs/marketplace-validation.md +22 -0
- package/docs/prototype-validation.json +10 -0
- package/docs/r3-demo.html +981 -0
- package/docs/r3-validation.md +59 -0
- package/docs/screenshots/help-dark.png +0 -0
- package/docs/screenshots/help-light.png +0 -0
- package/docs/screenshots/host-archived.png +0 -0
- package/docs/screenshots/host-dark.png +0 -0
- package/docs/screenshots/host-rollover.png +0 -0
- package/docs/screenshots/prototype-fixed.png +0 -0
- package/index.js +2 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/marketplace/Oak-liuhj__dsh-session-rollover.yml +7 -0
- package/marketplace/SUBMISSION.md +37 -0
- package/package.json +103 -4
- package/screenshots.json +6 -0
- package/scripts/build-client.mjs +12 -0
- package/scripts/check-dsh-compatibility.mjs +44 -0
- package/scripts/check-package.mjs +51 -0
- package/scripts/check-release.mjs +76 -0
- package/scripts/dsh-contracts.mjs +69 -0
- package/src/application/handoff-message.mjs +53 -0
- package/src/application/rollover.mjs +236 -0
- package/src/assembly.mjs +4 -0
- package/src/client/rollover-ui.js +311 -0
- package/src/client/toast-client.js +342 -0
- package/src/cluster.mjs +3 -0
- package/src/domain/cluster-index.mjs +65 -0
- package/src/domain/handoff.mjs +67 -0
- package/src/domain/inflight.mjs +25 -0
- package/src/domain/mode.mjs +34 -0
- package/src/domain/naming.mjs +78 -0
- package/src/domain/summary.mjs +68 -0
- package/src/domain/todos.mjs +23 -0
- package/src/domain/turns.mjs +43 -0
- package/src/handoff.mjs +2 -0
- package/src/inflight.mjs +2 -0
- package/src/mode.mjs +2 -0
- package/src/naming.mjs +2 -0
- package/src/notices.mjs +2 -0
- package/src/server/compatibility.mjs +84 -0
- package/src/server/host-adapter.mjs +146 -0
- package/src/server/notice-route.mjs +104 -0
- package/src/server/plugin.mjs +447 -0
- package/src/storage/index-store.mjs +48 -0
- package/src/storage/notices.mjs +117 -0
- package/src/storage/rollover-state.mjs +75 -0
- package/src/summary.mjs +2 -0
- package/src/todos.mjs +2 -0
- package/src/turns.mjs +2 -0
package/client.js
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// Generated by npm run build; edit src/client/. Version 1.2.0.
|
|
2
|
+
// dsh-session-rollover · 浏览器半边(B6 一次性提示)。
|
|
3
|
+
// 通道:server 半把「该提示什么」落盘,这里同源 GET /api-dsh-session-rollover/pending 取走渲染。
|
|
4
|
+
// 不碰宿主 Toast 实现、不建 React hooks 面(D-36:bindInjectSources 会剥掉 hooks 字段),纯 DOM。
|
|
5
|
+
// 形态/时长按 UI 定稿:底部居中、距输入框上沿 12px、max-width 420px、
|
|
6
|
+
// 12.5px/18px、常规 5s、rollback-failed 8s、补显延迟 300ms、去重 (会话,原因)。
|
|
7
|
+
(function () {
|
|
8
|
+
if (typeof window === 'undefined' || !window.__ModuleLoader__) return;
|
|
9
|
+
window.__ModuleLoader__.load({
|
|
10
|
+
// 必须与 dsh.client 条目的 id 逐字一致,使用公开包名。
|
|
11
|
+
id: 'dsh-session-rollover',
|
|
12
|
+
factory: function (require) {
|
|
13
|
+
var module = {exports: {}};
|
|
14
|
+
var NS = 'dsh-session-rollover';
|
|
15
|
+
var API = '/api-dsh-session-rollover/pending';
|
|
16
|
+
var POLL_MS = 1000;
|
|
17
|
+
var REPLAY_DELAY_MS = 300;
|
|
18
|
+
var FADE_MS = 120;
|
|
19
|
+
var MAX_SHOWN = 500;
|
|
20
|
+
|
|
21
|
+
function debug(e) {
|
|
22
|
+
try {
|
|
23
|
+
if (window.console && console.debug) console.debug('[' + NS + '] toast skipped:', e);
|
|
24
|
+
} catch (_) { /* 静默 */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function durationFor(reason) {
|
|
29
|
+
return reason === 'rollback-failed' ? 8000 : 5000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var CSS = [
|
|
33
|
+
'.dsr-toast-layer{position:fixed;left:50%;transform:translateX(-50%);bottom:96px;z-index:2147483000;display:flex;flex-direction:column;align-items:center;gap:8px;max-width:min(420px,calc(100vw - 32px));pointer-events:none;}',
|
|
34
|
+
'.dsr-toast{box-sizing:border-box;max-width:420px;padding:10px 14px;border-radius:8px;background:#FFFFFF;border:1px solid rgba(0,0,0,.08);box-shadow:0 6px 24px rgba(0,0,0,.12);color:#4B5563;font-size:12.5px;line-height:18px;text-align:left;white-space:pre-wrap;overflow-wrap:anywhere;opacity:0;transform:translateY(4px);transition:opacity 120ms ease,transform 120ms ease;}',
|
|
35
|
+
'.dsr-toast.is-in{opacity:1;transform:translateY(0);}',
|
|
36
|
+
'.dsr-toast.is-out{opacity:0;transform:translateY(4px);}',
|
|
37
|
+
'html[data-theme="dark"] .dsr-toast,html.dark .dsr-toast{background:#1F2430;color:#D1D5DB;border-color:rgba(255,255,255,.10);box-shadow:0 6px 24px rgba(0,0,0,.5);}',
|
|
38
|
+
'@media (prefers-color-scheme: dark){.dsr-toast{background:#1F2430;color:#D1D5DB;border-color:rgba(255,255,255,.10);box-shadow:0 6px 24px rgba(0,0,0,.5);}}',
|
|
39
|
+
'@media (prefers-reduced-motion: reduce){.dsr-toast{transition:opacity 120ms ease;transform:none;}.dsr-toast.is-in,.dsr-toast.is-out{transform:none;}}',
|
|
40
|
+
].join('\n');
|
|
41
|
+
|
|
42
|
+
function timersOf(env) {
|
|
43
|
+
var win = env.win || window;
|
|
44
|
+
return env.timers || {
|
|
45
|
+
setTimeout: function (fn, ms) {
|
|
46
|
+
return win.setTimeout(fn, ms);
|
|
47
|
+
},
|
|
48
|
+
clearTimeout: function (id) {
|
|
49
|
+
return win.clearTimeout(id);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 轻量 toaster:单例容器、每条一个 role=status 节点、到时自动摘除。 */
|
|
55
|
+
function createToaster(env) {
|
|
56
|
+
env = env || {};
|
|
57
|
+
var doc = env.doc || document;
|
|
58
|
+
var win = env.win || window;
|
|
59
|
+
var timers = timersOf(env);
|
|
60
|
+
var layer = null;
|
|
61
|
+
var items = [];
|
|
62
|
+
var cssInjected = false;
|
|
63
|
+
|
|
64
|
+
function injectCss() {
|
|
65
|
+
if (cssInjected || !doc.head) return;
|
|
66
|
+
try {
|
|
67
|
+
var style = doc.createElement('style');
|
|
68
|
+
style.setAttribute('data-dsh-plugin', NS);
|
|
69
|
+
style.textContent = CSS;
|
|
70
|
+
doc.head.appendChild(style);
|
|
71
|
+
cssInjected = true;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
debug(e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureLayer() {
|
|
78
|
+
if (layer && layer.parentNode) return layer;
|
|
79
|
+
layer = doc.createElement('div');
|
|
80
|
+
layer.className = 'dsr-toast-layer';
|
|
81
|
+
doc.body.appendChild(layer);
|
|
82
|
+
return layer;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function place() {
|
|
86
|
+
try {
|
|
87
|
+
var ed = doc.querySelector('div[contenteditable="true"]');
|
|
88
|
+
var rect = ed && ed.getBoundingClientRect ? ed.getBoundingClientRect() : null;
|
|
89
|
+
if (rect && rect.top > 0) {
|
|
90
|
+
layer.style.bottom = Math.max(12, Math.round(win.innerHeight - rect.top + 12)) + 'px';
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
debug(e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function remove(rec) {
|
|
98
|
+
for (var i = 0; i < rec.timers.length; i += 1) {
|
|
99
|
+
try {
|
|
100
|
+
timers.clearTimeout(rec.timers[i]);
|
|
101
|
+
} catch (_) { /* 已触发 */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
if (rec.el.parentNode) rec.el.parentNode.removeChild(rec.el);
|
|
106
|
+
} catch (_) { /* 已摘除 */
|
|
107
|
+
}
|
|
108
|
+
var at = items.indexOf(rec);
|
|
109
|
+
if (at >= 0) items.splice(at, 1);
|
|
110
|
+
if (layer && !layer.childNodes.length && layer.parentNode) {
|
|
111
|
+
try {
|
|
112
|
+
layer.parentNode.removeChild(layer);
|
|
113
|
+
} catch (_) { /* 已摘除 */
|
|
114
|
+
}
|
|
115
|
+
layer = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function show(notice) {
|
|
120
|
+
injectCss();
|
|
121
|
+
ensureLayer();
|
|
122
|
+
place();
|
|
123
|
+
var el = doc.createElement('div');
|
|
124
|
+
el.className = 'dsr-toast';
|
|
125
|
+
el.setAttribute('role', 'status');
|
|
126
|
+
el.setAttribute('aria-live', 'polite');
|
|
127
|
+
el.setAttribute('aria-atomic', 'true');
|
|
128
|
+
el.textContent = String((notice && notice.text) || '');
|
|
129
|
+
layer.appendChild(el);
|
|
130
|
+
var rec = {el: el, timers: []};
|
|
131
|
+
items.push(rec);
|
|
132
|
+
var duration = durationFor(notice && notice.reason);
|
|
133
|
+
rec.timers.push(timers.setTimeout(function () {
|
|
134
|
+
try {
|
|
135
|
+
el.className = 'dsr-toast is-in';
|
|
136
|
+
} catch (_) { /* 已摘除 */
|
|
137
|
+
}
|
|
138
|
+
}, 0));
|
|
139
|
+
rec.timers.push(timers.setTimeout(function () {
|
|
140
|
+
try {
|
|
141
|
+
el.className = 'dsr-toast is-out';
|
|
142
|
+
} catch (_) { /* 已摘除 */
|
|
143
|
+
}
|
|
144
|
+
}, Math.max(0, duration - FADE_MS)));
|
|
145
|
+
rec.timers.push(timers.setTimeout(function () {
|
|
146
|
+
remove(rec);
|
|
147
|
+
}, duration));
|
|
148
|
+
return el;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function destroy() {
|
|
152
|
+
items.slice().forEach(remove);
|
|
153
|
+
if (layer && layer.parentNode) {
|
|
154
|
+
try {
|
|
155
|
+
layer.parentNode.removeChild(layer);
|
|
156
|
+
} catch (_) { /* 已摘除 */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
layer = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
show: show, destroy: destroy, count: function () {
|
|
164
|
+
return items.length;
|
|
165
|
+
}, _items: items
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 当前会话:宿主 localStorage 的 dsh.sessions.current({sessionId}),URL ?session= 兜底。 */
|
|
170
|
+
function currentSessionId(env) {
|
|
171
|
+
var win = (env && env.win) || window;
|
|
172
|
+
try {
|
|
173
|
+
var raw = win.localStorage && win.localStorage.getItem('dsh.sessions.current');
|
|
174
|
+
if (raw) {
|
|
175
|
+
var parsed = JSON.parse(raw);
|
|
176
|
+
if (parsed && typeof parsed === 'object') {
|
|
177
|
+
if (parsed.sessionId) return String(parsed.sessionId);
|
|
178
|
+
if (parsed.id) return String(parsed.id);
|
|
179
|
+
} else if (typeof parsed === 'string' && parsed) return parsed;
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
debug(e);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
var m = /(?:^|[?&])session=([^&]+)/.exec((win.location && win.location.search) || '');
|
|
186
|
+
if (m) return decodeURIComponent(m[1]);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
debug(e);
|
|
189
|
+
}
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 轮询取件。会话切换后先等 300ms(补显不与首屏抢帧);页面隐藏不取,回来立即取。 */
|
|
194
|
+
function start(env, toaster) {
|
|
195
|
+
env = env || {};
|
|
196
|
+
var doc = env.doc || document;
|
|
197
|
+
var win = env.win || window;
|
|
198
|
+
var timers = timersOf(env);
|
|
199
|
+
var stopped = false;
|
|
200
|
+
var timer = null;
|
|
201
|
+
var session = '';
|
|
202
|
+
var shown = {};
|
|
203
|
+
var shownCount = 0;
|
|
204
|
+
|
|
205
|
+
function schedule(ms) {
|
|
206
|
+
if (stopped) return;
|
|
207
|
+
timer = timers.setTimeout(tick, ms);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function fetchPending(sid) {
|
|
211
|
+
var doFetch = env.fetch || (win && win.fetch);
|
|
212
|
+
if (typeof doFetch !== 'function') return;
|
|
213
|
+
var p;
|
|
214
|
+
try {
|
|
215
|
+
p = doFetch(API + '?session=' + encodeURIComponent(sid), {
|
|
216
|
+
cache: 'no-store',
|
|
217
|
+
credentials: 'same-origin'
|
|
218
|
+
});
|
|
219
|
+
} catch (e) {
|
|
220
|
+
debug(e);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
Promise.resolve(p).then(function (res) {
|
|
224
|
+
if (!res || !res.ok) return null;
|
|
225
|
+
return res.json();
|
|
226
|
+
}).then(function (body) {
|
|
227
|
+
if (stopped || !body || !Array.isArray(body.notices)) return;
|
|
228
|
+
body.notices.forEach(function (n) {
|
|
229
|
+
if (!n || !n.id || shown[n.id]) return;
|
|
230
|
+
shown[n.id] = 1;
|
|
231
|
+
shownCount += 1;
|
|
232
|
+
if (shownCount > MAX_SHOWN) {
|
|
233
|
+
shown = {};
|
|
234
|
+
shown[n.id] = 1;
|
|
235
|
+
shownCount = 1;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
toaster.show(n);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
debug(e);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}).catch(function (e) {
|
|
244
|
+
debug(e);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function tick() {
|
|
249
|
+
timer = null;
|
|
250
|
+
if (stopped) return;
|
|
251
|
+
try {
|
|
252
|
+
if (doc.visibilityState === 'hidden') {
|
|
253
|
+
schedule(POLL_MS);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
var sid = currentSessionId(env);
|
|
257
|
+
if (!sid) {
|
|
258
|
+
schedule(POLL_MS);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (sid !== session) {
|
|
262
|
+
session = sid;
|
|
263
|
+
schedule(REPLAY_DELAY_MS);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
fetchPending(sid);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
debug(e);
|
|
269
|
+
}
|
|
270
|
+
schedule(POLL_MS);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function onVisibility() {
|
|
274
|
+
if (!stopped && doc.visibilityState !== 'hidden') {
|
|
275
|
+
try {
|
|
276
|
+
tick();
|
|
277
|
+
} catch (e) {
|
|
278
|
+
debug(e);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function stop() {
|
|
284
|
+
stopped = true;
|
|
285
|
+
if (timer != null) {
|
|
286
|
+
try {
|
|
287
|
+
timers.clearTimeout(timer);
|
|
288
|
+
} catch (_) { /* 已触发 */
|
|
289
|
+
}
|
|
290
|
+
timer = null;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
doc.removeEventListener && doc.removeEventListener('visibilitychange', onVisibility);
|
|
294
|
+
} catch (_) { /* 无监听面 */
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
doc.addEventListener && doc.addEventListener('visibilitychange', onVisibility);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
debug(e);
|
|
302
|
+
}
|
|
303
|
+
tick();
|
|
304
|
+
return stop;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** R3 只承担会话状态与交接卡;待办进度复用 R2,不另建任务状态。 */
|
|
308
|
+
function createRolloverUI(React) {
|
|
309
|
+
const {createElement: h, useState, useEffect, useRef} = React;
|
|
310
|
+
const API = '/api-dsh-session-rollover/session';
|
|
311
|
+
const entries = new Map();
|
|
312
|
+
let navigate = null;
|
|
313
|
+
let renderInline = false;
|
|
314
|
+
const clientWarnings = new Set();
|
|
315
|
+
function unavailable(feature, error) {
|
|
316
|
+
clientWarnings.add(feature);
|
|
317
|
+
console.warn('[dsh-session-rollover] client capability unavailable:', feature, error?.message ?? '');
|
|
318
|
+
}
|
|
319
|
+
function inject(ctx, names, setup) {
|
|
320
|
+
try {
|
|
321
|
+
ctx.inject(names, scope => {
|
|
322
|
+
try { return setup(scope); } catch (error) { unavailable(names.join(','), error); }
|
|
323
|
+
});
|
|
324
|
+
} catch (error) { unavailable(names.join(','), error); }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function request(sessionId, action) {
|
|
328
|
+
const response = await fetch(action ? API : `${API}?session=${encodeURIComponent(sessionId)}`, action ? {
|
|
329
|
+
method: 'POST', credentials: 'same-origin', headers: {'content-type': 'application/json'},
|
|
330
|
+
body: JSON.stringify({sessionId, ...action}),
|
|
331
|
+
} : {credentials: 'same-origin', cache: 'no-store'});
|
|
332
|
+
const result = await response.json();
|
|
333
|
+
if (!response.ok) throw new Error(result.message ?? result.error ?? '会话状态读取失败');
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function entry(id) {
|
|
338
|
+
if (!entries.has(id)) entries.set(id, {value: null, listeners: new Set(), stop: null});
|
|
339
|
+
return entries.get(id);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function publish(id, value) {
|
|
343
|
+
const item = entry(id);
|
|
344
|
+
item.value = value;
|
|
345
|
+
for (const listener of item.listeners) listener(value);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function useInfo(id) {
|
|
349
|
+
const [state, setState] = useState(() => entry(id).value);
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
if (!id) return;
|
|
352
|
+
const item = entry(id);
|
|
353
|
+
let alive = true;
|
|
354
|
+
const receive = value => { if (alive) setState(value); };
|
|
355
|
+
item.listeners.add(receive);
|
|
356
|
+
receive(item.value);
|
|
357
|
+
if (!item.stop) {
|
|
358
|
+
let stopped = false, timer;
|
|
359
|
+
const poll = async () => {
|
|
360
|
+
try {
|
|
361
|
+
const value = await request(id);
|
|
362
|
+
if (!stopped) publish(id, value);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
if (!stopped) publish(id, {...item.value, sessionId: id, error: '暂时无法读取会话状态,请稍后重试。'});
|
|
365
|
+
}
|
|
366
|
+
if (!stopped) timer = setTimeout(poll, 1000);
|
|
367
|
+
};
|
|
368
|
+
item.stop = () => { stopped = true; clearTimeout(timer); };
|
|
369
|
+
void poll();
|
|
370
|
+
}
|
|
371
|
+
return () => {
|
|
372
|
+
alive = false;
|
|
373
|
+
item.listeners.delete(receive);
|
|
374
|
+
if (!item.listeners.size) { item.stop?.(); item.stop = null; entries.delete(id); }
|
|
375
|
+
};
|
|
376
|
+
}, [id]);
|
|
377
|
+
// 会话切换当帧不借用上一会话的只读状态。
|
|
378
|
+
return state?.sessionId === id ? state : null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function Source({source}) {
|
|
382
|
+
return source ? h('button', {type: 'button', className: 'dsr-link',
|
|
383
|
+
disabled: !navigate, onClick: () => navigate?.(source.sessionId),
|
|
384
|
+
'aria-label': `跳转到上一卷 ${source.title}`}, `承自 ${source.title}`) : null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function ReadonlyControl({info}) {
|
|
388
|
+
const [busy, setBusy] = useState(false);
|
|
389
|
+
const [error, setError] = useState('');
|
|
390
|
+
if (!info?.sealed) return null;
|
|
391
|
+
return h('div', {className: 'dsr-archive', role: 'status'},
|
|
392
|
+
h('span', null, info.readonly ? '此会话已归档,默认只读。历史可回看。' : '此会话已归档,已允许再次写入。'),
|
|
393
|
+
h('button', {type: 'button', role: 'switch', 'aria-checked': !info.readonly,
|
|
394
|
+
disabled: busy, onClick: async () => {
|
|
395
|
+
setBusy(true); setError('');
|
|
396
|
+
try { publish(info.sessionId, await request(info.sessionId, {action: 'readonly', readonly: !info.readonly})); }
|
|
397
|
+
catch { setError('保存失败,请重试;写入权限未改变。'); }
|
|
398
|
+
finally { setBusy(false); }
|
|
399
|
+
}}, busy ? '正在保存…' : info.readonly ? '允许再次写入' : '恢复只读'),
|
|
400
|
+
error ? h('span', {role: 'alert'}, error) : null);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function Header(props) {
|
|
404
|
+
const info = useInfo(props.sessionId);
|
|
405
|
+
const [busy, setBusy] = useState(false);
|
|
406
|
+
const [error, setError] = useState('');
|
|
407
|
+
const dialog = useRef(null);
|
|
408
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
409
|
+
const action = async type => {
|
|
410
|
+
setBusy(true); setError('');
|
|
411
|
+
try {
|
|
412
|
+
const result = await request(props.sessionId, {action: type});
|
|
413
|
+
if (result.newSessionId ?? result.sessionId) navigate?.(result.newSessionId ?? result.sessionId);
|
|
414
|
+
dialog.current?.close();
|
|
415
|
+
} catch { setError('操作未完成,请稍后重试或检查会话状态。'); }
|
|
416
|
+
finally { setBusy(false); }
|
|
417
|
+
};
|
|
418
|
+
if (!info) return null;
|
|
419
|
+
return h('div', {className: 'dsr-header'},
|
|
420
|
+
h('span', {className: 'dsr-chip'}, `本卷第 ${info.turnCount} 轮 / 阈值 ${info.threshold}`),
|
|
421
|
+
info.volume ? h('span', {className: 'dsr-chip'}, `卷 ${info.volume}/${info.cap}`) : null,
|
|
422
|
+
info.sealed ? h('span', {className: 'dsr-chip'}, info.readonly ? '归档只读' : '归档 · 已恢复写入') : null,
|
|
423
|
+
info.source ? h('span', {className: 'dsr-source'}, h(Source, {source: info.source}),
|
|
424
|
+
info.previousTurnCount != null ? ` · 第 ${info.previousTurnCount} 轮` : '') : null,
|
|
425
|
+
h('button', {type: 'button', 'aria-label': '会话分卷操作', onClick: () => { setShowHelp(false); dialog.current?.showModal(); }}, '⋯'),
|
|
426
|
+
h('dialog', {ref: dialog, className: 'dsr-actions', 'aria-label': '会话操作'},
|
|
427
|
+
showHelp ? h('section', {className: 'dsr-help', 'aria-label': '会话分卷使用说明'},
|
|
428
|
+
h('h3', null, '会话分卷与续接 · 使用说明'),
|
|
429
|
+
h('p', null, '长会话达到阈值后,在本轮执行结束且没有未完成工具调用或压缩时创建下一卷。默认每 10 条真实用户消息分卷,同一系列最多 9 卷。系统上下文和交接消息不计数。'),
|
|
430
|
+
h('h4', null, '从哪里开始'),
|
|
431
|
+
h('p', null, '标题栏查看本卷轮数、阈值和卷序。菜单中的“现在换会话”可提前交接;“新建独立会话”会携带快照另开一个系列。操作需等待当前执行结束。'),
|
|
432
|
+
h('h4', null, '交接卡与任务进度'),
|
|
433
|
+
h('p', null, '新卷首条接续摘要保留切卷时的待办、当前步骤、约束、文件线索及后台任务状态。可展开摘要、查看或复制完整 JSON,并点击来源回看上一卷。右侧实时任务进度由可选的 dsh-todo-progress 插件提供,历史快照不会随任务进度变化。'),
|
|
434
|
+
h('h4', null, '回看与恢复写入'),
|
|
435
|
+
h('p', null, '旧卷默认归档只读,可在输入区域上方切换“允许再次写入 / 恢复只读”。开关会保存;恢复写入不会让旧卷再次自动分卷。达到 9 卷后请新建独立会话。'),
|
|
436
|
+
h('h4', null, '配置与数据'),
|
|
437
|
+
h('p', null, '在宿主插件配置中调整 mode(auto / manual / off)、threshold 和 dataDir。manual 达阈值只提示,off 关闭自动判定;显式手动操作仍可使用。数据默认存于 DSH_HOME/plugin-data/dsh-session-rollover,卸载不会清除。'),
|
|
438
|
+
h('h4', null, '能力边界'),
|
|
439
|
+
h('p', null, '摘要为确定性摘录,不额外调用模型;继续对话使用 DSH 已配置的模型,无需额外 API Key。后台任务仅记录句柄和状态,不迁移或重启进程。摘要或存储异常会提示,复杂内容超预算时可能无法交接。'),
|
|
440
|
+
h('p', {className: 'dsr-help-credit'}, 'dsh-session-rollover · Oak-liuhj')) : null,
|
|
441
|
+
h('div', {className: 'dsr-actions-body'},
|
|
442
|
+
h('button', {type: 'button', 'aria-expanded': showHelp, onClick: () => setShowHelp(!showHelp)}, showHelp ? '收起使用说明' : '使用说明'),
|
|
443
|
+
h('button', {type: 'button', disabled: busy || info.sealed || !navigate || info.compatibility?.compatible === false, onClick: () => action('rollover')}, '现在换会话'),
|
|
444
|
+
h('button', {type: 'button', disabled: busy || !navigate || info.compatibility?.compatible === false, onClick: () => action('new')}, '新建独立会话'),
|
|
445
|
+
h('button', {type: 'button', onClick: () => dialog.current?.close()}, '关闭'))),
|
|
446
|
+
error ? h('span', {role: 'alert'}, error) : null);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function Badges(props) {
|
|
450
|
+
const info = useInfo(props.sessionId);
|
|
451
|
+
if (!info?.volume) return null;
|
|
452
|
+
return h('span', {className: `dsr-row-badges${props.compact ? ' dsr-row-compact' : ''}`, title: `卷 ${info.volume}/${info.cap}${info.readonly ? ' · 归档只读' : ''}`},
|
|
453
|
+
info.readonly ? h('span', null, '只读') : null, h('span', null, `${info.volume}/${info.cap}`));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function Card({payload, source}) {
|
|
457
|
+
const [expanded, setExpanded] = useState(false);
|
|
458
|
+
const [jsonOpen, setJsonOpen] = useState(false);
|
|
459
|
+
const [message, setMessage] = useState('');
|
|
460
|
+
if (!payload) return null;
|
|
461
|
+
const todos = Array.isArray(payload.todos) ? payload.todos : [];
|
|
462
|
+
const summary = payload.summary ?? {};
|
|
463
|
+
const active = payload.current_step?.active_items ?? [];
|
|
464
|
+
const text = Object.entries(summary).map(([k, v]) => `${k}\n${v}`).join('\n');
|
|
465
|
+
const chunks = [
|
|
466
|
+
['上轮工作摘要', summary['上轮工作摘要'] ?? summary['任务目标'] ?? '本轮未提供'],
|
|
467
|
+
['当前工作', active.length ? active.map(n => `第 ${n} 步进行中:${todos[n - 1]?.content ?? ''}`).join('\n') : '尚未开始'],
|
|
468
|
+
['关键约束(原文)', (payload.constraints ?? []).join('\n') || '本轮未提供'],
|
|
469
|
+
['文件与代码', (payload.changed_files ?? []).map(f => `${f.path}${f.deleted ? '(已删除)' : ''}`).join('\n') || '本轮未提供'],
|
|
470
|
+
['未竟后台任务', (payload.open_jobs ?? []).map(j => `${j.id} · ${j.command ?? ''} · ${j.status}${j.recoverable === false ? '(需重跑)' : ''}`).join('\n') || '无后台任务'],
|
|
471
|
+
['待办快照', todos.length ? todos.map((t, i) => `${i + 1}. ${t.content} · ${{completed: '已完成', in_progress: '进行中', pending: '待办', failed: '中断'}[t.status] ?? t.status}`).join('\n') : '无结构化待办'],
|
|
472
|
+
];
|
|
473
|
+
return h('section', {className: 'dsr-card', 'aria-label': '接续摘要'},
|
|
474
|
+
h('div', {className: 'dsr-card-head'}, h('strong', null, '接续摘要'), h(Source, {source}),
|
|
475
|
+
payload.cluster?.vol_no ? h('span', {className: 'dsr-chip'}, `卷 ${payload.cluster.vol_no}/9`) : null,
|
|
476
|
+
payload.health ? h('span', {className: 'dsr-chip'}, `摘要重试 ${payload.health.summary_retry}/2`) : null),
|
|
477
|
+
h('div', {className: expanded ? 'dsr-card-content' : 'dsr-card-preview'},
|
|
478
|
+
expanded ? chunks.map(([name, value]) => h('div', {className: 'dsr-section', key: name},
|
|
479
|
+
h('h4', null, name), h('div', {className: 'dsr-prewrap'}, value))) : text),
|
|
480
|
+
h('button', {type: 'button', className: 'dsr-link dsr-expand', 'aria-expanded': expanded,
|
|
481
|
+
onClick: () => setExpanded(!expanded)}, expanded ? '收起摘要' : '展开全部'),
|
|
482
|
+
h('div', {className: 'dsr-json-controls'},
|
|
483
|
+
h('button', {type: 'button', 'aria-expanded': jsonOpen, onClick: () => setJsonOpen(!jsonOpen)}, jsonOpen ? '收起快照 JSON' : '展开快照 JSON'),
|
|
484
|
+
h('button', {type: 'button', onClick: async () => {
|
|
485
|
+
try { await navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); setMessage('快照已复制'); }
|
|
486
|
+
catch { setJsonOpen(true); setMessage('复制失败,可选择下方 JSON 手动复制。'); }
|
|
487
|
+
}}, '复制快照')),
|
|
488
|
+
jsonOpen ? h('pre', {className: 'dsr-json', tabIndex: 0, 'aria-label': '完整交接快照 JSON'}, JSON.stringify(payload, null, 2)) : null,
|
|
489
|
+
message ? h('div', {role: 'status'}, message) : null);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function Dock(props) {
|
|
493
|
+
const info = useInfo(props.sessionId);
|
|
494
|
+
const previous = useRef(null);
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
const next = info?.completedSessionId;
|
|
497
|
+
// 只自动跳转浏览器亲历的“活动→封卷”;回看历史旧卷时不能被再次弹回新卷。
|
|
498
|
+
if (next && previous.current?.sessionId === props.sessionId && previous.current.sealed === false && info.sealed && navigate) navigate(next);
|
|
499
|
+
if (info) previous.current = info;
|
|
500
|
+
}, [info, props.sessionId]);
|
|
501
|
+
if (!info) return null;
|
|
502
|
+
return h('div', {className: 'dsr-dock'},
|
|
503
|
+
info.error ? h('div', {role: 'alert'}, info.error) : null,
|
|
504
|
+
info.compatibility?.compatible === false ? h('div', {role: 'alert', className: 'dsr-warning'}, info.compatibility.message) : null,
|
|
505
|
+
clientWarnings.size ? h('div', {role: 'alert', className: 'dsr-warning'}, '部分会话界面功能与当前 DSH 不兼容,请更新插件。') : null,
|
|
506
|
+
h(ReadonlyControl, {key: props.sessionId, info}),
|
|
507
|
+
info.capped ? h('div', {role: 'status', className: 'dsr-warning'}, '同簇已达 9 卷上限(9/9),请通过会话操作新建独立会话。') : null,
|
|
508
|
+
info.retryLimitReached ? h('div', {role: 'status', className: 'dsr-warning'}, '摘要生成失败,已停止自动重试。可在修正内容后手动换会话。') : null,
|
|
509
|
+
info.degraded ? h('div', {role: 'alert'}, '分卷索引保存失败,重启前请检查数据目录权限。') : null,
|
|
510
|
+
!renderInline && info.handoff ? h(Card, {key: props.sessionId, payload: info.handoff, source: info.source}) : null);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function parseHandoff(text) {
|
|
514
|
+
if (!text.startsWith('【分卷交接】')) return null;
|
|
515
|
+
try {
|
|
516
|
+
const payload = JSON.parse(/```json\s*([\s\S]*?)```/.exec(text)?.[1]);
|
|
517
|
+
return payload.rollover === 'v1' ? payload : null;
|
|
518
|
+
} catch { return null; }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** 释放时仅撤销自己的禁用;保留模型选择等其他插件后来设置的阻塞原因。 */
|
|
522
|
+
function blockComposer(blocks, id, reason) {
|
|
523
|
+
if (typeof blocks?.storeFor !== 'function' || typeof blocks?.set !== 'function') throw new Error('composer blocks unavailable');
|
|
524
|
+
const store = blocks.storeFor(id);
|
|
525
|
+
if (typeof store?.getSnapshot !== 'function' || typeof store?.subscribe !== 'function') throw new Error('composer store unavailable');
|
|
526
|
+
let previous = store.getSnapshot();
|
|
527
|
+
const own = {reason};
|
|
528
|
+
let writing = false;
|
|
529
|
+
const enforce = () => {
|
|
530
|
+
if (writing || store.getSnapshot() === own) return;
|
|
531
|
+
previous = store.getSnapshot();
|
|
532
|
+
writing = true;
|
|
533
|
+
try { blocks.set(id, own); } finally { writing = false; }
|
|
534
|
+
};
|
|
535
|
+
const off = store.subscribe(enforce);
|
|
536
|
+
enforce();
|
|
537
|
+
return () => { off(); if (store.getSnapshot() === own) blocks.set(id, previous); };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function apply(ctx) {
|
|
541
|
+
if (!ctx?.inject) return;
|
|
542
|
+
const style = document.createElement('style');
|
|
543
|
+
style.textContent = CSS;
|
|
544
|
+
style.setAttribute('data-dsh-plugin', 'dsh-session-rollover-ui');
|
|
545
|
+
document.head.appendChild(style);
|
|
546
|
+
ctx.effect?.(() => () => { style.remove(); for (const item of entries.values()) item.stop?.(); entries.clear(); });
|
|
547
|
+
inject(ctx, ['uiWorkspace'], scope => {
|
|
548
|
+
if (typeof scope.uiWorkspace?.openSession !== 'function') throw new Error('session navigation unavailable');
|
|
549
|
+
navigate = id => scope.uiWorkspace.openSession(id);
|
|
550
|
+
scope.effect?.(() => () => { navigate = null; });
|
|
551
|
+
});
|
|
552
|
+
inject(ctx, ['slots'], scope => {
|
|
553
|
+
const slots = scope.slots;
|
|
554
|
+
if (typeof slots?.inject !== 'function' || typeof slots?.register !== 'function') throw new Error('slots unavailable');
|
|
555
|
+
const mount = (name, Component, options = {}, owner = scope) => owner.effect(() => owner.slots.inject(name, () => owner.slots.register({name,
|
|
556
|
+
id: 'dsh-session-rollover', order: 35, ...options}, Component)));
|
|
557
|
+
mount('conversation.session.header.actions', Header);
|
|
558
|
+
mount('conversation.input.dock', Dock);
|
|
559
|
+
mount('sidebar.session.row.trailing', Badges);
|
|
560
|
+
// 已安装的较旧宿主只开放 leading;新宿主优先 trailing,避免重复徽标。
|
|
561
|
+
mount('sidebar.session.row.leading', props => slots.snapshot?.('sidebar.session.row.trailing')?.length
|
|
562
|
+
? null : h(Badges, {...props, compact: true}));
|
|
563
|
+
// 使用宿主公开 composer block 服务;覆盖输入组件会破坏其子槽授权。
|
|
564
|
+
inject(scope, ['conversation'], inputScope => {
|
|
565
|
+
if (typeof inputScope.conversation?.blocks?.storeFor !== 'function' || typeof inputScope.conversation?.blocks?.set !== 'function') {
|
|
566
|
+
throw new Error('composer blocks unavailable');
|
|
567
|
+
}
|
|
568
|
+
function ComposerGuard(props) {
|
|
569
|
+
const info = useInfo(props.sessionId);
|
|
570
|
+
const reason = !info ? '正在读取会话状态…' : info.readonly
|
|
571
|
+
? '归档会话:不可输入,可通过上方开关恢复写入。' : info.error;
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (props.sessionId && reason) {
|
|
574
|
+
try { return blockComposer(inputScope.conversation.blocks, props.sessionId, reason); }
|
|
575
|
+
catch (error) { unavailable('conversation.blocks', error); }
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
[props.sessionId, reason]);
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
inputScope.effect(() => slots.inject('conversation.input.dock', () => slots.register({
|
|
582
|
+
name: 'conversation.input.dock', id: 'dsr-readonly-guard', order: 34}, ComposerGuard)));
|
|
583
|
+
});
|
|
584
|
+
inject(scope, ['uiConversation'], textScope => {
|
|
585
|
+
if (!textScope.uiConversation.userText?.register) return;
|
|
586
|
+
textScope.effect(() => textScope.uiConversation.userText.register({id: 'dsr-handoff', order: -100,
|
|
587
|
+
match(text, from) {
|
|
588
|
+
if (from !== 0) return;
|
|
589
|
+
const payload = parseHandoff(text);
|
|
590
|
+
return payload ? {id: 'dsr-handoff', start: 0, end: text.length, data: payload} : undefined;
|
|
591
|
+
}}));
|
|
592
|
+
// 插槽必须随 uiConversation 的注入子作用域释放,不能挂在较长寿命的 slots 父作用域。
|
|
593
|
+
// 新宿主重连/热替换领域服务时会重建该子作用域;挂在父级会重复占用同一个 keyed slot。
|
|
594
|
+
mount('conversation.user.text.node', props => h(Card, {payload: props.node.data,
|
|
595
|
+
source: props.node.data.from_session_id && !props.node.data.cluster?.independent ? {sessionId: props.node.data.from_session_id, title: props.node.data.from_title ?? '上一卷'} : null}), {key: 'dsr-handoff'}, textScope);
|
|
596
|
+
renderInline = true;
|
|
597
|
+
textScope.effect?.(() => () => { renderInline = false; });
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const CSS = `
|
|
603
|
+
.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges{--dsr-bg:var(--dsw-alias-bg-base, #fff);--dsr-fg:var(--dsw-alias-label-primary, #111827);--dsr-muted:#61666b;--dsr-line:rgba(0,0,0,.12);--dsr-accent:#3d56d6;--dsr-weak:rgba(77,107,254,.09);color:var(--dsr-fg);font:13px/20px -apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif;box-sizing:border-box}
|
|
604
|
+
body[data-ds-dark-theme] :is(.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges),html[data-theme=dark] :is(.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges),html.dark :is(.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges){--dsr-bg:#171a1f;--dsr-fg:#f9fafb;--dsr-muted:#cfd3d6;--dsr-line:rgba(255,255,255,.16);--dsr-accent:#95a8ff;--dsr-weak:rgba(107,133,255,.13)}
|
|
605
|
+
@media(prefers-color-scheme:dark){html:not([data-theme=light]):not([data-ds-theme-source=light]) :is(.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges){--dsr-bg:#171a1f;--dsr-fg:#f9fafb;--dsr-muted:#cfd3d6;--dsr-line:rgba(255,255,255,.16);--dsr-accent:#95a8ff}}
|
|
606
|
+
.dsr-header{display:flex;gap:6px;align-items:center;flex-wrap:wrap;min-width:0}.dsr-chip{display:inline-flex;align-items:center;border:1px solid var(--dsr-line);border-radius:99px;padding:1px 7px;font-size:11.5px;white-space:nowrap;color:var(--dsr-muted)}
|
|
607
|
+
.dsr-source{font-size:12px;color:var(--dsr-muted)}.dsr-link{color:var(--dsr-accent)!important;background:none!important}.dsr-header button,.dsr-dock button,.dsr-card button,.dsr-actions summary{font:inherit;cursor:pointer;min-height:28px;min-width:28px;border:1px solid var(--dsr-line);background:var(--dsr-bg);color:inherit;border-radius:6px;padding:3px 8px}.dsr-link{border:0!important}.dsr-header button:disabled,.dsr-dock button:disabled{opacity:.6;cursor:default}
|
|
608
|
+
.dsr-dock{width:100%;min-width:0;display:flex;flex-direction:column;gap:8px}.dsr-archive{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border:1px solid var(--dsr-line);border-radius:8px;background:var(--dsr-weak)}.dsr-archive button{margin-left:auto}.dsr-warning{padding:8px 10px;border:1px solid var(--dsr-line);border-radius:8px}
|
|
609
|
+
.dsr-card{display:block;flex:0 0 auto;min-width:0;max-width:100%;border:1px solid var(--dsr-line);border-radius:10px;background:var(--dsr-bg);overflow-wrap:anywhere;white-space:normal;margin:8px 0}.dsr-card-head{display:flex;align-items:center;flex-wrap:wrap;gap:6px;background:var(--dsr-weak);border-radius:10px 10px 0 0;padding:10px 12px}.dsr-card-preview{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;margin:10px 12px;white-space:pre-wrap}.dsr-card-content{display:block}.dsr-section{padding:8px 12px;border-top:1px solid var(--dsr-line)}.dsr-section h4{font-size:12px;line-height:20px;color:var(--dsr-muted);margin:0 0 4px;font-weight:500}.dsr-prewrap{white-space:pre-wrap}.dsr-expand{margin:0 4px 6px}.dsr-json-controls{display:flex;gap:8px;flex-wrap:wrap;padding:8px 12px;border-top:1px solid var(--dsr-line)}.dsr-json{font:12px/18px Consolas,monospace;max-height:240px;overflow:auto;background:var(--dsr-weak);padding:10px;margin:0 12px 12px;white-space:pre-wrap;overflow-wrap:anywhere}.dsr-row-badges{display:inline-flex;gap:3px;font-size:10px;white-space:nowrap}.dsr-row-badges span{background:var(--dsr-weak);padding:0 4px;border-radius:4px}
|
|
610
|
+
.dsr-actions{border:1px solid var(--dsr-line);border-radius:10px;background:var(--dsr-bg);color:var(--dsr-fg);max-width:calc(100vw - 32px)}.dsr-actions::backdrop{background:rgba(0,0,0,.25)}.dsr-actions-body{display:flex;gap:6px;flex-wrap:wrap;padding:6px 0}.dsr-header :focus-visible,.dsr-dock :focus-visible,.dsr-card :focus-visible{outline:2px solid var(--dsr-accent);outline-offset:2px}.dsr-card [role=status]{padding:4px 12px;color:var(--dsr-muted)}
|
|
611
|
+
.dsr-row-compact{flex-direction:column;align-items:center;justify-content:center;gap:0;width:20px;max-width:20px;font-size:9px;line-height:11px}.dsr-row-compact span{padding:0 1px}
|
|
612
|
+
.dsr-actions:has(.dsr-help){width:560px;max-height:calc(100dvh - 48px);overflow:auto}.dsr-help{max-width:100%;line-height:1.7;overflow-wrap:anywhere}.dsr-help h3{margin:4px 0 12px;font-size:17px}.dsr-help h4{margin:14px 0 4px;font-size:13px}.dsr-help p{margin:4px 0 10px}.dsr-help-credit{font-size:11px;color:var(--dsr-muted)}
|
|
613
|
+
@media(max-width:600px){.dsr-header{gap:3px;flex-wrap:nowrap;max-width:calc(100vw - 156px)}.dsr-header>.dsr-chip,.dsr-header>.dsr-source{display:none}.dsr-header>.dsr-chip:first-child{display:inline-flex;font-size:10px;padding:1px 3px}}
|
|
614
|
+
@media(prefers-reduced-motion:reduce){.dsr-card *{animation:none!important;transition:none!important}}
|
|
615
|
+
`;
|
|
616
|
+
return {apply, Header, Dock, Card, Badges, useInfo, parseHandoff, publish, blockComposer, clientWarnings, CSS};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function apply(ctx) {
|
|
620
|
+
try { createRolloverUI(require("react")).apply(ctx); } catch (e) { debug(e); }
|
|
621
|
+
try {
|
|
622
|
+
var env = {doc: document, win: window};
|
|
623
|
+
var toaster = createToaster(env);
|
|
624
|
+
var stop = start(env, toaster);
|
|
625
|
+
var dispose = function () {
|
|
626
|
+
try {
|
|
627
|
+
stop();
|
|
628
|
+
} catch (_) { /* 已停 */
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
toaster.destroy();
|
|
632
|
+
} catch (_) { /* 已清 */
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
if (ctx && typeof ctx.effect === 'function') ctx.effect(function () {
|
|
636
|
+
return dispose;
|
|
637
|
+
}, NS + ': toast');
|
|
638
|
+
else if (typeof window.addEventListener === 'function') window.addEventListener('pagehide', dispose);
|
|
639
|
+
} catch (e) {
|
|
640
|
+
debug(e);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
module.exports = {
|
|
645
|
+
name: NS,
|
|
646
|
+
inject: [],
|
|
647
|
+
apply: apply,
|
|
648
|
+
__test: {
|
|
649
|
+
durationFor: durationFor, createToaster: createToaster, currentSessionId: currentSessionId,
|
|
650
|
+
start: start, API: API, REPLAY_DELAY_MS: REPLAY_DELAY_MS, POLL_MS: POLL_MS,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
return module.exports;
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
})();
|