dsh-remote-bridge 2.5.1 → 2.6.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/README.en.md +22 -19
- package/README.md +65 -16
- package/docs/api/asyncapi.yaml +51 -2
- package/docs/api/openapi.yaml +326 -4
- package/lib/client.cjs +1128 -532
- package/lib/index.d.ts +547 -15
- package/lib/index.js +2593 -337
- package/package.json +1 -1
package/lib/client.cjs
CHANGED
|
@@ -152,6 +152,17 @@ window.__ModuleLoader__.load({
|
|
|
152
152
|
async revokeDevice(deviceId) {
|
|
153
153
|
await this.manageJson(`/devices/${encodeURIComponent(deviceId)}`, { method: "DELETE" });
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* 一键清理「N 天没动过」的设备。天数由用户填,服务端再夹一次 1..3650。
|
|
157
|
+
*
|
|
158
|
+
* 返回真正删掉的条数 —— 0 也是一次合法结果(「没有可清理的」),界面不该把它当失败。
|
|
159
|
+
*/
|
|
160
|
+
async pruneInactiveDevices(olderThanDays) {
|
|
161
|
+
return this.manageJson("/devices/prune", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: JSON.stringify({ olderThanDays })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
155
166
|
async trashItems() {
|
|
156
167
|
return this.json("/trash");
|
|
157
168
|
}
|
|
@@ -172,9 +183,25 @@ window.__ModuleLoader__.load({
|
|
|
172
183
|
async tunnelActivity() {
|
|
173
184
|
return this.manageJson("/cloudflare/activity");
|
|
174
185
|
}
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
/**
|
|
187
|
+
* 创建(或复用)cloudflared 并启动 Quick Tunnel,返回对外地址。
|
|
188
|
+
*
|
|
189
|
+
* `withConfig=true`(一键「开隧道并出令牌」):请求体带 `configText:true`,服务端在
|
|
190
|
+
* 隧道**起来之前**的这一次调用里等地址并顺带生成令牌配置文本(见 state.configText)。
|
|
191
|
+
* 必须走这班车:隧道一旦运行,单独的 /manage/config/text 会被管理面判据 0 挡 403。
|
|
192
|
+
* endpointLabel/rootIds/deviceName 可选透传给服务端的配置生成。
|
|
193
|
+
*/
|
|
194
|
+
async tunnelCreate(withConfig = false, options = {}) {
|
|
195
|
+
if (withConfig !== true) return this.manageJson("/cloudflare/create", { method: "POST" });
|
|
196
|
+
return this.manageJson("/cloudflare/create", {
|
|
197
|
+
method: "POST",
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
configText: true,
|
|
200
|
+
...options.endpointLabel === void 0 ? {} : { endpointLabel: options.endpointLabel },
|
|
201
|
+
...options.rootIds === void 0 ? {} : { rootIds: options.rootIds },
|
|
202
|
+
...options.deviceName === void 0 ? {} : { deviceName: options.deviceName }
|
|
203
|
+
})
|
|
204
|
+
});
|
|
178
205
|
}
|
|
179
206
|
/** 停止当前隧道。 */
|
|
180
207
|
async tunnelStop() {
|
|
@@ -236,335 +263,42 @@ window.__ModuleLoader__.load({
|
|
|
236
263
|
} catch {}
|
|
237
264
|
throw new WorkspaceApiError(response.status, code, message);
|
|
238
265
|
}
|
|
239
|
-
//#endregion
|
|
240
|
-
//#region src/client/handoff.tsx
|
|
241
|
-
/**
|
|
242
|
-
* 浏览器半边:把**图片交接消息**里的那个地址渲染成图片本身。
|
|
243
|
-
*
|
|
244
|
-
* 这半边是从 `dsh-image-paths` 0.3.1 并过来的,接的是同一个契约:纯文本模型收到图片时,
|
|
245
|
-
* remote-bridge 会在用户消息里追加两行 ——
|
|
246
|
-
*
|
|
247
|
-
* 🖼️ 图片 1「shot.png」(当前模型看不到图,已存到 .dsh-images/<sha>.png;要看图,请用识图工具打开下面那行的路径)
|
|
248
|
-
* [Image: source: /absolute/path/.dsh-images/<sha>.png]
|
|
249
|
-
*
|
|
250
|
-
* 本模块只读第一行里那个地址,把它变成一张真的图。第二行是给模型读的机器契约,同一张图
|
|
251
|
-
* 不再重复出第二张;而且只扫**用户消息**,上下文注入、助手正文、工具输出里引用的路径
|
|
252
|
-
* 一律不碰 —— 否则对话里到处都是 `.dsh-images/…` 的例子,每个都会被渲染成图。
|
|
253
|
-
*
|
|
254
|
-
* 取图走宿主那条只读路由 `/plugin/dsh-remote-bridge/raw?sessionId=…&name=…`:
|
|
255
|
-
* 请求里**只有会话与裸文件名**,目录由宿主按会话工作目录推出,客户端连「读哪个目录」
|
|
256
|
-
* 都递不进去。这条路是给 `<img>` 用的 —— 浏览器不会给图片请求加 Authorization 头,
|
|
257
|
-
* 所以它独立于版本化 REST,按回环 + 同源 + 文件名白名单 + 字节自证守门。
|
|
258
|
-
*
|
|
259
|
-
* 座位选的是 Turn 尾部(`conversation.chat.turnTail`):Chat 节点座位会把这个 kind 收进
|
|
260
|
-
* 「过程分组」,standard 模式下折叠起来 —— DOM 里有、肉眼看不着。Turn 尾在独立成行的
|
|
261
|
-
* 白名单内,任何工作详情模式下都看得见。
|
|
262
|
-
*/
|
|
263
|
-
/** 本插件在 Chat store 里占的节点 kind。 */
|
|
264
|
-
const KIND = "dsh-remote-bridge-handoff-gallery";
|
|
265
|
-
/** 宿主的只读图片路由。 */
|
|
266
|
-
const ROUTE = "/plugin/dsh-remote-bridge/raw";
|
|
267
|
-
/** 每轮最多显示几张。 */
|
|
268
|
-
const MAX_IMAGES = 8;
|
|
269
|
-
/** 长过这个的路径不值得去解析。 */
|
|
270
|
-
const MAX_PATH_CHARS = 4096;
|
|
271
|
-
/** 图片扩展名:与宿主交接写盘的白名单一致。 */
|
|
272
|
-
const IMAGE_EXTENSION = /\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif)$/i;
|
|
273
|
-
/**
|
|
274
|
-
* 交接行的行首标记:中英两种文案都以它开头。
|
|
275
|
-
*
|
|
276
|
-
* 判据选「行首标记」而不是「句子里有 已存到」——后者会把用户自己写的句子也算进来,
|
|
277
|
-
* 而 dsh-companion 摘除这一行用的也是同一个标记,两边口径一致。
|
|
278
|
-
*/
|
|
279
|
-
const HANDOFF_LINE = /^🖼️\s*(?:图片|Image)\s+\d+/i;
|
|
280
266
|
/**
|
|
281
|
-
*
|
|
267
|
+
* @license lucide-react v0.468.0 - ISC
|
|
282
268
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* 相对(`.dsh-images/x.png`)与绝对(兜底目录那版)都认。
|
|
286
|
-
*/
|
|
287
|
-
const HANDOFF_PATH = /(?:已(?:保存|存)到|saved to)[^;;\n]*?([A-Za-z0-9_./@~+-][^\s`"'()[\]{}<>|,;:?,。;:、!?]*?\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif))/i;
|
|
288
|
-
/** 路径里的文件名(路由只收裸文件名)。 */
|
|
289
|
-
function basenameOf(path) {
|
|
290
|
-
const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
291
|
-
return cut === -1 ? path : path.slice(cut + 1);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* 读出一段文本里的交接图片。
|
|
295
|
-
* @param text - 消息的文本内容(已按块拼接)。
|
|
296
|
-
* @returns 单个 `{ path, label }`,不是交接消息就是 null。
|
|
297
|
-
*/
|
|
298
|
-
function handoffMention(text) {
|
|
299
|
-
if (typeof text !== "string" || text === "") return null;
|
|
300
|
-
for (const line of text.split("\n")) {
|
|
301
|
-
if (!HANDOFF_LINE.test(line.trim())) continue;
|
|
302
|
-
const match = HANDOFF_PATH.exec(line);
|
|
303
|
-
if (match === null) continue;
|
|
304
|
-
const path = match[1] ?? "";
|
|
305
|
-
if (path.length === 0 || path.length > MAX_PATH_CHARS) continue;
|
|
306
|
-
if (!IMAGE_EXTENSION.test(path)) continue;
|
|
307
|
-
return {
|
|
308
|
-
path,
|
|
309
|
-
label: basenameOf(path)
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
/** 把一条消息的内容块拼成文本。 */
|
|
315
|
-
function contentText(content) {
|
|
316
|
-
if (!Array.isArray(content)) return "";
|
|
317
|
-
const parts = [];
|
|
318
|
-
for (const block of content) {
|
|
319
|
-
if (block === null || typeof block !== "object") continue;
|
|
320
|
-
const candidate = block;
|
|
321
|
-
if (candidate.type === "text" && typeof candidate.text === "string") parts.push(candidate.text);
|
|
322
|
-
}
|
|
323
|
-
return parts.join("\n");
|
|
324
|
-
}
|
|
325
|
-
/** 一条消息交接出去的那张图,没有就是空数组。 */
|
|
326
|
-
function itemsOfMessage(payload) {
|
|
327
|
-
if (payload === null || typeof payload !== "object") return [];
|
|
328
|
-
const item = handoffMention(contentText(payload.content));
|
|
329
|
-
return item === null ? [] : [item];
|
|
330
|
-
}
|
|
331
|
-
const WRAP_STYLE = {
|
|
332
|
-
display: "flex",
|
|
333
|
-
flexWrap: "wrap",
|
|
334
|
-
alignItems: "flex-start",
|
|
335
|
-
gap: 8,
|
|
336
|
-
margin: "8px 0 4px"
|
|
337
|
-
};
|
|
338
|
-
const ITEM_STYLE = {
|
|
339
|
-
display: "flex",
|
|
340
|
-
flexDirection: "column",
|
|
341
|
-
gap: 2,
|
|
342
|
-
maxWidth: "100%"
|
|
343
|
-
};
|
|
344
|
-
const THUMB_STYLE = {
|
|
345
|
-
display: "block",
|
|
346
|
-
maxHeight: 180,
|
|
347
|
-
maxWidth: "100%",
|
|
348
|
-
borderRadius: 8,
|
|
349
|
-
border: "1px solid rgba(127,127,127,0.28)",
|
|
350
|
-
background: "rgba(127,127,127,0.06)",
|
|
351
|
-
cursor: "zoom-in"
|
|
352
|
-
};
|
|
353
|
-
const CAPTION_STYLE = {
|
|
354
|
-
display: "block",
|
|
355
|
-
maxWidth: 180,
|
|
356
|
-
fontFamily: "inherit",
|
|
357
|
-
fontSize: 11,
|
|
358
|
-
lineHeight: 1.4,
|
|
359
|
-
color: "rgba(127,127,127,0.95)",
|
|
360
|
-
overflow: "hidden",
|
|
361
|
-
textOverflow: "ellipsis",
|
|
362
|
-
whiteSpace: "nowrap"
|
|
363
|
-
};
|
|
364
|
-
const OVERLAY_STYLE = {
|
|
365
|
-
position: "fixed",
|
|
366
|
-
inset: 0,
|
|
367
|
-
zIndex: 1e3,
|
|
368
|
-
display: "flex",
|
|
369
|
-
alignItems: "center",
|
|
370
|
-
justifyContent: "center",
|
|
371
|
-
background: "rgba(0,0,0,0.72)",
|
|
372
|
-
cursor: "zoom-out"
|
|
373
|
-
};
|
|
374
|
-
const FULL_STYLE = {
|
|
375
|
-
maxWidth: "92vw",
|
|
376
|
-
maxHeight: "92vh",
|
|
377
|
-
borderRadius: 6,
|
|
378
|
-
boxShadow: "0 8px 32px rgba(0,0,0,0.5)"
|
|
379
|
-
};
|
|
380
|
-
/** 某条地址在这条路由上的 URL。 */
|
|
381
|
-
function imageUrl(path, sessionId) {
|
|
382
|
-
return `${ROUTE}?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(basenameOf(path))}`;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* 一张图。取不到的(文件已删、会话读不出、格式不符)自己消失,而不是在轮末留一个碎图标。
|
|
269
|
+
* This source code is licensed under the ISC license.
|
|
270
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
386
271
|
*/
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
className: "daw-handoff-caption",
|
|
418
|
-
title: props.item.path,
|
|
419
|
-
style: CAPTION_STYLE,
|
|
420
|
-
children: props.item.path
|
|
421
|
-
})]
|
|
422
|
-
});
|
|
423
|
-
if (!open) return framed;
|
|
424
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [framed, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
425
|
-
style: OVERLAY_STYLE,
|
|
426
|
-
role: "dialog",
|
|
427
|
-
"aria-label": props.item.path,
|
|
428
|
-
onClick: () => setOpen(false),
|
|
429
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
430
|
-
src: url,
|
|
431
|
-
alt: props.item.label,
|
|
432
|
-
decoding: "async",
|
|
433
|
-
referrerPolicy: "no-referrer",
|
|
434
|
-
style: FULL_STYLE
|
|
435
|
-
})
|
|
436
|
-
})] });
|
|
437
|
-
}
|
|
438
|
-
/** 把本轮各节点里交接出去的图收成一串(按出现顺序去重)。 */
|
|
439
|
-
function collectTurnItems(nodes, locations, turn) {
|
|
440
|
-
if (nodes === void 0 || locations === void 0 || turn === void 0 || turn === null) return [];
|
|
441
|
-
let keys;
|
|
442
|
-
try {
|
|
443
|
-
keys = locations.getTurn(turn.turn);
|
|
444
|
-
} catch {
|
|
445
|
-
return [];
|
|
446
|
-
}
|
|
447
|
-
if (!Array.isArray(keys)) return [];
|
|
448
|
-
const seen = /* @__PURE__ */ new Set();
|
|
449
|
-
const items = [];
|
|
450
|
-
for (const key of keys) {
|
|
451
|
-
const node = nodes.get(key);
|
|
452
|
-
if (node === void 0 || node === null || node.kind !== KIND) continue;
|
|
453
|
-
const list = node.data?.items;
|
|
454
|
-
if (!Array.isArray(list)) continue;
|
|
455
|
-
for (const item of list) {
|
|
456
|
-
if (item === null || typeof item !== "object" || typeof item.path !== "string") continue;
|
|
457
|
-
if (seen.has(item.path)) continue;
|
|
458
|
-
seen.add(item.path);
|
|
459
|
-
items.push({
|
|
460
|
-
path: item.path,
|
|
461
|
-
label: typeof item.label === "string" ? item.label : basenameOf(item.path)
|
|
462
|
-
});
|
|
463
|
-
if (items.length >= MAX_IMAGES) return items;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return items;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Turn 尾部的图集:本轮每条交接消息各自那张图。
|
|
470
|
-
*
|
|
471
|
-
* 没有会话 id 就什么都不显示 —— 路由需要它来定位目录,猜一个只会 404。
|
|
472
|
-
*/
|
|
473
|
-
function HandoffTurnTail(props) {
|
|
474
|
-
const items = collectTurnItems(props.useChat((snapshot) => snapshot.nodes), props.useChat((snapshot) => snapshot.locations), props.turn);
|
|
475
|
-
const sessionId = props.sessionId;
|
|
476
|
-
if (items.length === 0) return null;
|
|
477
|
-
if (typeof sessionId !== "string" || sessionId === "") return null;
|
|
478
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
479
|
-
className: "daw-handoff-gallery",
|
|
480
|
-
style: WRAP_STYLE,
|
|
481
|
-
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffImage, {
|
|
482
|
-
item,
|
|
483
|
-
sessionId
|
|
484
|
-
}, item.path))
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* 一条「交接消息 → 一个图集节点」的 Definition。
|
|
489
|
-
*
|
|
490
|
-
* 只匹配 `user/message`:交接说明属于读者自己发的那条消息。助手正文、上下文注入、
|
|
491
|
-
* 工具输出里到处都是引用 `.dsh-images/…` 的文本,读它们会把别人的话切成碎图。
|
|
492
|
-
*
|
|
493
|
-
* 节点自身**不注册渲染器**(Turn 尾负责显示),所以过程分组里不会多出一行、
|
|
494
|
-
* verbose 模式下也不会重复一次。
|
|
495
|
-
*/
|
|
496
|
-
const handoffDefinition = {
|
|
497
|
-
kind: KIND,
|
|
498
|
-
target: "chat",
|
|
499
|
-
match(event) {
|
|
500
|
-
if (event === null || typeof event !== "object" || event.surfaceOp !== "append") return null;
|
|
501
|
-
if (event.type !== "user/message") return null;
|
|
502
|
-
return {
|
|
503
|
-
id: `m:${String(event.seq)}`,
|
|
504
|
-
role: "start"
|
|
505
|
-
};
|
|
506
|
-
},
|
|
507
|
-
start(_context, match) {
|
|
508
|
-
return {
|
|
509
|
-
items: itemsOfMessage(match.event.data),
|
|
510
|
-
anchorSeq: match.event.seq
|
|
511
|
-
};
|
|
512
|
-
},
|
|
513
|
-
update(context) {
|
|
514
|
-
return context.state;
|
|
515
|
-
},
|
|
516
|
-
buildViewNode(context) {
|
|
517
|
-
const state = context.state;
|
|
518
|
-
if (state === void 0 || !Array.isArray(state.items) || state.items.length === 0) return null;
|
|
519
|
-
const start = context.start;
|
|
520
|
-
return {
|
|
521
|
-
key: context.key,
|
|
522
|
-
kind: KIND,
|
|
523
|
-
id: context.id,
|
|
524
|
-
target: "chat",
|
|
525
|
-
anchorSeq: state.anchorSeq,
|
|
526
|
-
location: start !== void 0 && start !== null ? start.location : { kind: "unresolved" },
|
|
527
|
-
visibility: "visible",
|
|
528
|
-
data: { items: state.items }
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
//#endregion
|
|
533
|
-
//#region node_modules/.pnpm/lucide-react@0.468.0_react@18.3.1/node_modules/lucide-react/dist/cjs/lucide-react.js
|
|
534
|
-
/**
|
|
535
|
-
* @license lucide-react v0.468.0 - ISC
|
|
536
|
-
*
|
|
537
|
-
* This source code is licensed under the ISC license.
|
|
538
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
539
|
-
*/
|
|
540
|
-
var require_lucide_react = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
541
|
-
var react$1 = require("react");
|
|
542
|
-
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
543
|
-
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
544
|
-
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
|
|
545
|
-
}).join(" ").trim();
|
|
546
|
-
var defaultAttributes = {
|
|
547
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
548
|
-
width: 24,
|
|
549
|
-
height: 24,
|
|
550
|
-
viewBox: "0 0 24 24",
|
|
551
|
-
fill: "none",
|
|
552
|
-
stroke: "currentColor",
|
|
553
|
-
strokeWidth: 2,
|
|
554
|
-
strokeLinecap: "round",
|
|
555
|
-
strokeLinejoin: "round"
|
|
556
|
-
};
|
|
557
|
-
const Icon = react$1.forwardRef(({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
|
|
558
|
-
return react$1.createElement("svg", {
|
|
559
|
-
ref,
|
|
560
|
-
...defaultAttributes,
|
|
561
|
-
width: size,
|
|
562
|
-
height: size,
|
|
563
|
-
stroke: color,
|
|
564
|
-
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
|
|
565
|
-
className: mergeClasses("lucide", className),
|
|
566
|
-
...rest
|
|
567
|
-
}, [...iconNode.map(([tag, attrs]) => react$1.createElement(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/client/i18n.tsx
|
|
274
|
+
var import_lucide_react = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
275
|
+
var react$1 = require("react");
|
|
276
|
+
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
277
|
+
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
278
|
+
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
|
|
279
|
+
}).join(" ").trim();
|
|
280
|
+
var defaultAttributes = {
|
|
281
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
282
|
+
width: 24,
|
|
283
|
+
height: 24,
|
|
284
|
+
viewBox: "0 0 24 24",
|
|
285
|
+
fill: "none",
|
|
286
|
+
stroke: "currentColor",
|
|
287
|
+
strokeWidth: 2,
|
|
288
|
+
strokeLinecap: "round",
|
|
289
|
+
strokeLinejoin: "round"
|
|
290
|
+
};
|
|
291
|
+
const Icon = react$1.forwardRef(({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
|
|
292
|
+
return react$1.createElement("svg", {
|
|
293
|
+
ref,
|
|
294
|
+
...defaultAttributes,
|
|
295
|
+
width: size,
|
|
296
|
+
height: size,
|
|
297
|
+
stroke: color,
|
|
298
|
+
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
|
|
299
|
+
className: mergeClasses("lucide", className),
|
|
300
|
+
...rest
|
|
301
|
+
}, [...iconNode.map(([tag, attrs]) => react$1.createElement(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
|
|
568
302
|
});
|
|
569
303
|
const createLucideIcon = (iconName, iconNode) => {
|
|
570
304
|
const Component = react$1.forwardRef(({ className, ...props }, ref) => react$1.createElement(Icon, {
|
|
@@ -6770,7 +6504,7 @@ window.__ModuleLoader__.load({
|
|
|
6770
6504
|
d: "m22 10-7.5 7.5L13 16",
|
|
6771
6505
|
key: "ke71qq"
|
|
6772
6506
|
}]]);
|
|
6773
|
-
createLucideIcon("Check", [["path", {
|
|
6507
|
+
const Check = createLucideIcon("Check", [["path", {
|
|
6774
6508
|
d: "M20 6 9 17l-5-5",
|
|
6775
6509
|
key: "1gmf2c"
|
|
6776
6510
|
}]]);
|
|
@@ -9169,7 +8903,7 @@ window.__ModuleLoader__.load({
|
|
|
9169
8903
|
key: "zix9uf"
|
|
9170
8904
|
}]
|
|
9171
8905
|
]);
|
|
9172
|
-
createLucideIcon("Copy", [["rect", {
|
|
8906
|
+
const Copy = createLucideIcon("Copy", [["rect", {
|
|
9173
8907
|
width: "14",
|
|
9174
8908
|
height: "14",
|
|
9175
8909
|
x: "8",
|
|
@@ -15939,7 +15673,7 @@ window.__ModuleLoader__.load({
|
|
|
15939
15673
|
key: "jkujk7"
|
|
15940
15674
|
}]
|
|
15941
15675
|
]);
|
|
15942
|
-
createLucideIcon("Languages", [
|
|
15676
|
+
const Languages = createLucideIcon("Languages", [
|
|
15943
15677
|
["path", {
|
|
15944
15678
|
d: "m5 8 6 6",
|
|
15945
15679
|
key: "1wu5hv"
|
|
@@ -30050,16 +29784,17 @@ window.__ModuleLoader__.load({
|
|
|
30050
29784
|
key: "durymu"
|
|
30051
29785
|
}]
|
|
30052
29786
|
]);
|
|
29787
|
+
exports.Check = Check;
|
|
29788
|
+
exports.Copy = Copy;
|
|
30053
29789
|
exports.Download = Download;
|
|
30054
29790
|
exports.File = File;
|
|
30055
29791
|
exports.HardDrive = HardDrive;
|
|
30056
29792
|
exports.Image = Image;
|
|
29793
|
+
exports.Languages = Languages;
|
|
30057
29794
|
exports.Smartphone = Smartphone;
|
|
30058
29795
|
exports.Trash2 = Trash2;
|
|
30059
29796
|
exports.Upload = Upload;
|
|
30060
|
-
}));
|
|
30061
|
-
//#endregion
|
|
30062
|
-
//#region src/client/i18n.tsx
|
|
29797
|
+
})))();
|
|
30063
29798
|
const STORAGE_KEY = "dsh-workspace-locale";
|
|
30064
29799
|
const dictionaries = {
|
|
30065
29800
|
"zh-CN": {
|
|
@@ -30235,6 +29970,56 @@ window.__ModuleLoader__.load({
|
|
|
30235
29970
|
errorAdminProxyForwarded: "管理面不接受经代理转发的请求。请在本机用 127.0.0.1 或 localhost 直接打开。",
|
|
30236
29971
|
errorAdminHostNotLoopback: "管理面只在本机回环地址下可用。请用 127.0.0.1 或 localhost 打开本页。",
|
|
30237
29972
|
errorAdminLoopbackRequired: "此操作只能在本机执行。",
|
|
29973
|
+
tunnelStageLocating: "正在定位本机 cloudflared…",
|
|
29974
|
+
tunnelStageBinaryFound: "已找到本机二进制,直接复用",
|
|
29975
|
+
tunnelStageDownloadGithub: "正在从 GitHub 下载 cloudflared…",
|
|
29976
|
+
tunnelStageDownloadDeb: "正在从官方 deb 仓库下载 cloudflared…",
|
|
29977
|
+
tunnelStageDownloadProgress: "正在下载 cloudflared… {mb}/{all} MB",
|
|
29978
|
+
tunnelStageDownloadUnverified: "镜像源没有可用的校验和,仍在下载但本次不做完整性校验…",
|
|
29979
|
+
tunnelStageUnpackDeb: "解包官方 deb 并提取二进制…",
|
|
29980
|
+
tunnelStageWriteBinary: "写入二进制并赋予执行权限…",
|
|
29981
|
+
tunnelStageStarting: "正在启动 cloudflared 隧道…",
|
|
29982
|
+
tunnelErrorAlreadyRunning: "隧道已经在运行中,请先停止。",
|
|
29983
|
+
tunnelErrorDownload: "下载 cloudflared 失败,请检查网络后重试;如需换源可设置 CLOUDFLARED_MIRROR。",
|
|
29984
|
+
tunnelErrorExited: "cloudflared 已退出(code={code},signal={signal}),未拿到隧道地址。",
|
|
29985
|
+
tunnelStopConfirm: "停止云隧道后,公网临时地址立即失效,正在经隧道连接的手机会断开。确定停止吗?",
|
|
29986
|
+
tunnelStartAndGenConfig: "开启隧道并生成配置",
|
|
29987
|
+
tunnelRestartAndGenConfig: "停止后重开隧道并生成配置",
|
|
29988
|
+
managementLockedWhileTunnel: "云隧道正在运行,管理面已安全锁定。除停止隧道外的配置暂时不可改,停止隧道后自动恢复。",
|
|
29989
|
+
tunnelConfigAuto: "隧道已开启,配置文本已自动生成",
|
|
29990
|
+
tunnelUrlPending: "隧道已启动,但暂时还没拿到公网地址,稍等片刻后点「用隧道地址生成配置」即可。",
|
|
29991
|
+
copyUrl: "复制地址",
|
|
29992
|
+
pairingExpiresAt: "配对码有效期约 {minutes} 分钟,请尽快在手机上输入。",
|
|
29993
|
+
copyPairing: "复制配对码",
|
|
29994
|
+
pairingCreated: "配对码已生成",
|
|
29995
|
+
configCreated: "配置文本已生成",
|
|
29996
|
+
revokeConfirm: "吊销后这台手机的令牌立即失效,必须重新配对才能访问。确定吊销「{name}」吗?",
|
|
29997
|
+
pruneInactiveDays: "清理超过(天)未使用",
|
|
29998
|
+
pruneInactiveAction: "清理 {days} 天未使用设备",
|
|
29999
|
+
pruneInactiveBusy: "正在清理…",
|
|
30000
|
+
pruneInactiveConfirm: "将删除 {days} 天没有使用过的设备(它们需要重新配对才能访问)。最近用过的设备不受影响。继续吗?",
|
|
30001
|
+
pruneInactiveDone: "已清理 {count} 台设备",
|
|
30002
|
+
pruneInactiveNone: "没有需要清理的设备",
|
|
30003
|
+
removeRootConfirm: "移除根目录「{label}」不会删除磁盘上的文件,但所有手机将立刻失去对它的访问权。确定移除吗?",
|
|
30004
|
+
disableRemoteConfirm: "关闭远程访问后,所有手机连接立即断开,需要重新启用并配对。确定关闭吗?",
|
|
30005
|
+
rootAdded: "根目录已添加",
|
|
30006
|
+
rootRemoved: "根目录已移除",
|
|
30007
|
+
remoteDisabled: "远程访问已关闭",
|
|
30008
|
+
permissionsSaved: "权限已保存",
|
|
30009
|
+
deviceRevoked: "设备已吊销",
|
|
30010
|
+
listenerSaved: "监听设置已保存",
|
|
30011
|
+
trashRestored: "已恢复",
|
|
30012
|
+
trashPurged: "已永久删除",
|
|
30013
|
+
tunnelStarted: "云隧道已开启",
|
|
30014
|
+
auditEmpty: "暂无审计事件",
|
|
30015
|
+
auditLoadMore: "加载更多",
|
|
30016
|
+
auditLoading: "正在加载…",
|
|
30017
|
+
trashEmpty: "回收站是空的",
|
|
30018
|
+
deviceLastSeen: "最后使用 {time}",
|
|
30019
|
+
deviceNeverSeen: "从未连接",
|
|
30020
|
+
imageLoadFailed: "图片加载失败",
|
|
30021
|
+
prevImage: "上一张",
|
|
30022
|
+
nextImage: "下一张",
|
|
30238
30023
|
author: "作者 Github@Hakunm",
|
|
30239
30024
|
version: "当前版本"
|
|
30240
30025
|
},
|
|
@@ -30321,7 +30106,7 @@ window.__ModuleLoader__.load({
|
|
|
30321
30106
|
removeRoot: "Remove root",
|
|
30322
30107
|
builtinRootBadge: "Default",
|
|
30323
30108
|
builtinRootHint: "Default directory · cannot be removed",
|
|
30324
|
-
builtinRootNotice: "New roots are not granted to already paired phones by default. To share one, open
|
|
30109
|
+
builtinRootNotice: "New roots are not granted to already paired phones by default. To share one, open the Devices card and tick that directory for the phone in question.",
|
|
30325
30110
|
errorRootBuiltin: "This is the current DSH working directory, so it cannot be removed. You can still add other roots above.",
|
|
30326
30111
|
scopeChatRead: "Read chat",
|
|
30327
30112
|
scopeChatWrite: "Send chat",
|
|
@@ -30411,6 +30196,56 @@ window.__ModuleLoader__.load({
|
|
|
30411
30196
|
errorAdminProxyForwarded: "Management endpoints do not accept proxied requests. Open them locally via 127.0.0.1 or localhost.",
|
|
30412
30197
|
errorAdminHostNotLoopback: "Management endpoints are available only under a loopback address. Open this page via 127.0.0.1 or localhost.",
|
|
30413
30198
|
errorAdminLoopbackRequired: "This operation is available only from the local machine.",
|
|
30199
|
+
tunnelStageLocating: "Locating the local cloudflared binary…",
|
|
30200
|
+
tunnelStageBinaryFound: "Local binary found, reusing it",
|
|
30201
|
+
tunnelStageDownloadGithub: "Downloading cloudflared from GitHub…",
|
|
30202
|
+
tunnelStageDownloadDeb: "Downloading cloudflared from the official deb repo…",
|
|
30203
|
+
tunnelStageDownloadProgress: "Downloading cloudflared… {mb}/{all} MB",
|
|
30204
|
+
tunnelStageDownloadUnverified: "No checksum is published for this mirror; downloading without an integrity check…",
|
|
30205
|
+
tunnelStageUnpackDeb: "Unpacking the deb and extracting the binary…",
|
|
30206
|
+
tunnelStageWriteBinary: "Writing the binary and marking it executable…",
|
|
30207
|
+
tunnelStageStarting: "Starting the cloudflared tunnel…",
|
|
30208
|
+
tunnelErrorAlreadyRunning: "The tunnel is already running. Stop it first.",
|
|
30209
|
+
tunnelErrorDownload: "Downloading cloudflared failed. Check the network and retry; set CLOUDFLARED_MIRROR to use another source.",
|
|
30210
|
+
tunnelErrorExited: "cloudflared exited (code={code}, signal={signal}) before a tunnel address was obtained.",
|
|
30211
|
+
tunnelStopConfirm: "Stopping the tunnel invalidates the public address immediately and disconnects phones currently connected through it. Stop anyway?",
|
|
30212
|
+
tunnelStartAndGenConfig: "Start tunnel & generate config",
|
|
30213
|
+
tunnelRestartAndGenConfig: "Restart tunnel & generate config",
|
|
30214
|
+
managementLockedWhileTunnel: "The Cloudflare tunnel is running, so the management surface is safely locked. Everything except stopping the tunnel is read-only until you stop it.",
|
|
30215
|
+
tunnelConfigAuto: "Tunnel started; config text generated automatically",
|
|
30216
|
+
tunnelUrlPending: "The tunnel is starting but its public address is not ready yet. Wait a moment, then press “Generate config with tunnel address”.",
|
|
30217
|
+
copyUrl: "Copy address",
|
|
30218
|
+
pairingExpiresAt: "The pairing code is valid for about {minutes} minutes — enter it on the phone soon.",
|
|
30219
|
+
copyPairing: "Copy code",
|
|
30220
|
+
pairingCreated: "Pairing code generated",
|
|
30221
|
+
configCreated: "Config text generated",
|
|
30222
|
+
revokeConfirm: "Revoking invalidates this phone’s token at once; it must pair again to regain access. Revoke “{name}”?",
|
|
30223
|
+
pruneInactiveDays: "Unused for more than (days)",
|
|
30224
|
+
pruneInactiveAction: "Prune devices unused for {days} days",
|
|
30225
|
+
pruneInactiveBusy: "Pruning…",
|
|
30226
|
+
pruneInactiveConfirm: "This deletes devices unused for {days} days (they must pair again to regain access). Recently used devices are untouched. Continue?",
|
|
30227
|
+
pruneInactiveDone: "Pruned {count} device(s)",
|
|
30228
|
+
pruneInactiveNone: "No devices to prune",
|
|
30229
|
+
removeRootConfirm: "Removing “{label}” does not delete files on disk, but every phone immediately loses access to it. Remove it?",
|
|
30230
|
+
disableRemoteConfirm: "Disabling remote access drops every phone connection right away; it must be re-enabled and re-paired. Disable it?",
|
|
30231
|
+
rootAdded: "Root added",
|
|
30232
|
+
rootRemoved: "Root removed",
|
|
30233
|
+
remoteDisabled: "Remote access disabled",
|
|
30234
|
+
permissionsSaved: "Permissions saved",
|
|
30235
|
+
deviceRevoked: "Device revoked",
|
|
30236
|
+
listenerSaved: "Listener settings saved",
|
|
30237
|
+
trashRestored: "Restored",
|
|
30238
|
+
trashPurged: "Permanently deleted",
|
|
30239
|
+
tunnelStarted: "Tunnel started",
|
|
30240
|
+
auditEmpty: "No audit events yet",
|
|
30241
|
+
auditLoadMore: "Load more",
|
|
30242
|
+
auditLoading: "Loading…",
|
|
30243
|
+
trashEmpty: "The recycle bin is empty",
|
|
30244
|
+
deviceLastSeen: "Last seen {time}",
|
|
30245
|
+
deviceNeverSeen: "Never connected",
|
|
30246
|
+
imageLoadFailed: "Image failed to load",
|
|
30247
|
+
prevImage: "Previous image",
|
|
30248
|
+
nextImage: "Next image",
|
|
30414
30249
|
author: "Author Github@Hakunm",
|
|
30415
30250
|
version: "Current version"
|
|
30416
30251
|
}
|
|
@@ -30428,68 +30263,545 @@ window.__ModuleLoader__.load({
|
|
|
30428
30263
|
} catch {}
|
|
30429
30264
|
for (const listener of listeners) listener();
|
|
30430
30265
|
}
|
|
30431
|
-
function subscribeWorkspaceLocale(listener) {
|
|
30432
|
-
listeners.add(listener);
|
|
30433
|
-
return () => listeners.delete(listener);
|
|
30266
|
+
function subscribeWorkspaceLocale(listener) {
|
|
30267
|
+
listeners.add(listener);
|
|
30268
|
+
return () => listeners.delete(listener);
|
|
30269
|
+
}
|
|
30270
|
+
/**
|
|
30271
|
+
* 取一条文案并填占位符。
|
|
30272
|
+
*
|
|
30273
|
+
* **缺键不抛异常**。原来这里是 `dictionaries[locale][key].replace(...)`:任何一个不存在的键
|
|
30274
|
+
* 都会抛 TypeError,而它在 React 渲染路径上 —— 结果不是那一行显示不出原文,而是**整个设置
|
|
30275
|
+
* 面板白屏**。缺一个描述行就把七张卡里的全部设置都搭进去,代价和收益完全不成比例。
|
|
30276
|
+
*
|
|
30277
|
+
* 降级成「回退到另一种语言 → 再回退成键名本身」:最坏情况是某一行露出 `rootsTabDesc`
|
|
30278
|
+
* 这样的字面量,一眼能看出该补哪条,而不是面对一个空白面板。
|
|
30279
|
+
*/
|
|
30280
|
+
function translate(locale, key, values = {}) {
|
|
30281
|
+
const fill = (template) => template?.replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
|
|
30282
|
+
const primary = fill(dictionaries[locale][key]);
|
|
30283
|
+
if (primary !== void 0) return primary;
|
|
30284
|
+
const text = fill(dictionaries["zh-CN"][key]) ?? key;
|
|
30285
|
+
console.warn(`[dsh-remote-bridge] 缺少文案键 ${String(key)}(locale=${locale}),已降级显示键名`);
|
|
30286
|
+
return text;
|
|
30287
|
+
}
|
|
30288
|
+
/**
|
|
30289
|
+
* 权限码 → 人话。
|
|
30290
|
+
*
|
|
30291
|
+
* 用户要求「中文状态下显示中文,英文显示英文」。文案刻意**用人话而不是协议码**:
|
|
30292
|
+
* `读文件` / `写文件` / `删文件` 自己就看得懂,而 `files.read` 要对照文档才知道指什么 ——
|
|
30293
|
+
* 出问题时用户把屏幕上的字念出来,比报一串码有用得多。
|
|
30294
|
+
*
|
|
30295
|
+
* 键名 `scope${码}`(`chat.read` → `scopeChatRead`):与既有 `rootsTab` 一致的拼法。
|
|
30296
|
+
* 用**显式查表**而不是模板拼 —— 拼错会查不到键,而键查不到时 `translate` 会静默降级成
|
|
30297
|
+
* 露出键名(见上面),界面上会突然冒出一行 `scopeChatread`。白屏那次就是这么来的。
|
|
30298
|
+
*/
|
|
30299
|
+
const SCOPE_LABEL_KEYS = {
|
|
30300
|
+
"chat.read": "scopeChatRead",
|
|
30301
|
+
"chat.write": "scopeChatWrite",
|
|
30302
|
+
"files.read": "scopeFilesRead",
|
|
30303
|
+
"files.write": "scopeFilesWrite",
|
|
30304
|
+
"files.delete": "scopeFilesDelete",
|
|
30305
|
+
"settings.read": "scopeSettingsRead",
|
|
30306
|
+
"settings.write": "scopeSettingsWrite"
|
|
30307
|
+
};
|
|
30308
|
+
function scopeLabel(locale, scope) {
|
|
30309
|
+
const key = SCOPE_LABEL_KEYS[scope];
|
|
30310
|
+
if (key === void 0) return scope;
|
|
30311
|
+
return translate(locale, key);
|
|
30312
|
+
}
|
|
30313
|
+
function useWorkspaceI18n() {
|
|
30314
|
+
const locale = (0, react.useSyncExternalStore)(subscribeWorkspaceLocale, getWorkspaceLocale, () => "zh-CN");
|
|
30315
|
+
return {
|
|
30316
|
+
locale,
|
|
30317
|
+
t: (0, react.useCallback)((key, values) => translate(locale, key, values), [locale]),
|
|
30318
|
+
toggle: (0, react.useCallback)(() => setWorkspaceLocale(locale === "zh-CN" ? "en" : "zh-CN"), [locale])
|
|
30319
|
+
};
|
|
30320
|
+
}
|
|
30321
|
+
function LanguageToggle() {
|
|
30322
|
+
const { t, toggle } = useWorkspaceI18n();
|
|
30323
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30324
|
+
className: "daw-icon",
|
|
30325
|
+
type: "button",
|
|
30326
|
+
title: t("switchLanguage"),
|
|
30327
|
+
"aria-label": t("switchLanguage"),
|
|
30328
|
+
onClick: toggle,
|
|
30329
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Languages, { size: 16 })
|
|
30330
|
+
});
|
|
30331
|
+
}
|
|
30332
|
+
function readStoredLocale() {
|
|
30333
|
+
try {
|
|
30334
|
+
return globalThis.localStorage?.getItem(STORAGE_KEY) === "en" ? "en" : "zh-CN";
|
|
30335
|
+
} catch {
|
|
30336
|
+
return "zh-CN";
|
|
30337
|
+
}
|
|
30338
|
+
}
|
|
30339
|
+
//#endregion
|
|
30340
|
+
//#region src/client/handoff.tsx
|
|
30341
|
+
/**
|
|
30342
|
+
* 浏览器半边:把**图片交接消息**里的那个地址渲染成图片本身。
|
|
30343
|
+
*
|
|
30344
|
+
* 这半边是从 `dsh-image-paths` 0.3.1 并过来的,接的是同一个契约:纯文本模型收到图片时,
|
|
30345
|
+
* remote-bridge 会在用户消息里追加两行 ——
|
|
30346
|
+
*
|
|
30347
|
+
* 🖼️ 图片 1「shot.png」(当前模型看不到图,已存到 .dsh-images/<sha>.png;要看图,请用识图工具打开下面那行的路径)
|
|
30348
|
+
* [Image: source: /absolute/path/.dsh-images/<sha>.png]
|
|
30349
|
+
*
|
|
30350
|
+
* 本模块只读第一行里那个地址,把它变成一张真的图。第二行是给模型读的机器契约,同一张图
|
|
30351
|
+
* 不再重复出第二张;而且只扫**用户消息**,上下文注入、助手正文、工具输出里引用的路径
|
|
30352
|
+
* 一律不碰 —— 否则对话里到处都是 `.dsh-images/…` 的例子,每个都会被渲染成图。
|
|
30353
|
+
*
|
|
30354
|
+
* 取图走宿主那条只读路由 `/plugin/dsh-remote-bridge/raw?sessionId=…&name=…`:
|
|
30355
|
+
* 请求里**只有会话与裸文件名**,目录由宿主按会话工作目录推出,客户端连「读哪个目录」
|
|
30356
|
+
* 都递不进去。这条路是给 `<img>` 用的 —— 浏览器不会给图片请求加 Authorization 头,
|
|
30357
|
+
* 所以它独立于版本化 REST,按回环 + 同源 + 文件名白名单 + 字节自证守门。
|
|
30358
|
+
*
|
|
30359
|
+
* 座位选的是 Turn 尾部(`conversation.chat.turnTail`):Chat 节点座位会把这个 kind 收进
|
|
30360
|
+
* 「过程分组」,standard 模式下折叠起来 —— DOM 里有、肉眼看不着。Turn 尾在独立成行的
|
|
30361
|
+
* 白名单内,任何工作详情模式下都看得见。
|
|
30362
|
+
*/
|
|
30363
|
+
/** 本插件在 Chat store 里占的节点 kind。 */
|
|
30364
|
+
const KIND = "dsh-remote-bridge-handoff-gallery";
|
|
30365
|
+
/** 宿主的只读图片路由。 */
|
|
30366
|
+
const ROUTE = "/plugin/dsh-remote-bridge/raw";
|
|
30367
|
+
/** 每轮最多显示几张。 */
|
|
30368
|
+
const MAX_IMAGES = 8;
|
|
30369
|
+
/** 长过这个的路径不值得去解析。 */
|
|
30370
|
+
const MAX_PATH_CHARS = 4096;
|
|
30371
|
+
/** 图片扩展名:与宿主交接写盘的白名单一致。 */
|
|
30372
|
+
const IMAGE_EXTENSION = /\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif)$/i;
|
|
30373
|
+
/**
|
|
30374
|
+
* 交接行的行首标记:中英两种文案都以它开头。
|
|
30375
|
+
*
|
|
30376
|
+
* 判据选「行首标记」而不是「句子里有 已存到」——后者会把用户自己写的句子也算进来,
|
|
30377
|
+
* 而 dsh-companion 摘除这一行用的也是同一个标记,两边口径一致。
|
|
30378
|
+
*/
|
|
30379
|
+
const HANDOFF_LINE = /^🖼️\s*(?:图片|Image)\s+\d+/i;
|
|
30380
|
+
/**
|
|
30381
|
+
* 从交接行里抠出图片地址。
|
|
30382
|
+
*
|
|
30383
|
+
* 允许「已存到 / 已保存到 / saved to」后面隔一段散文(历史文案里有「工作目录」这种词),
|
|
30384
|
+
* 所以是「点心号 + 非贪婪」而不是紧邻匹配。取出的是消息里**原样**的那条路径,
|
|
30385
|
+
* 相对(`.dsh-images/x.png`)与绝对(兜底目录那版)都认。
|
|
30386
|
+
*/
|
|
30387
|
+
const HANDOFF_PATH = /(?:已(?:保存|存)到|saved to)[^;;\n]*?([A-Za-z0-9_./@~+-][^\s`"'()[\]{}<>|,;:?,。;:、!?]*?\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif))/i;
|
|
30388
|
+
/** 路径里的文件名(路由只收裸文件名)。 */
|
|
30389
|
+
function basenameOf(path) {
|
|
30390
|
+
const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
30391
|
+
return cut === -1 ? path : path.slice(cut + 1);
|
|
30392
|
+
}
|
|
30393
|
+
/**
|
|
30394
|
+
* 读出一段文本里的交接图片。
|
|
30395
|
+
* @param text - 消息的文本内容(已按块拼接)。
|
|
30396
|
+
* @returns 单个 `{ path, label }`,不是交接消息就是 null。
|
|
30397
|
+
*/
|
|
30398
|
+
function handoffMention(text) {
|
|
30399
|
+
if (typeof text !== "string" || text === "") return null;
|
|
30400
|
+
for (const line of text.split("\n")) {
|
|
30401
|
+
if (!HANDOFF_LINE.test(line.trim())) continue;
|
|
30402
|
+
const match = HANDOFF_PATH.exec(line);
|
|
30403
|
+
if (match === null) continue;
|
|
30404
|
+
const path = match[1] ?? "";
|
|
30405
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS) continue;
|
|
30406
|
+
if (!IMAGE_EXTENSION.test(path)) continue;
|
|
30407
|
+
return {
|
|
30408
|
+
path,
|
|
30409
|
+
label: basenameOf(path)
|
|
30410
|
+
};
|
|
30411
|
+
}
|
|
30412
|
+
return null;
|
|
30413
|
+
}
|
|
30414
|
+
/** 把一条消息的内容块拼成文本。 */
|
|
30415
|
+
function contentText(content) {
|
|
30416
|
+
if (!Array.isArray(content)) return "";
|
|
30417
|
+
const parts = [];
|
|
30418
|
+
for (const block of content) {
|
|
30419
|
+
if (block === null || typeof block !== "object") continue;
|
|
30420
|
+
const candidate = block;
|
|
30421
|
+
if (candidate.type === "text" && typeof candidate.text === "string") parts.push(candidate.text);
|
|
30422
|
+
}
|
|
30423
|
+
return parts.join("\n");
|
|
30424
|
+
}
|
|
30425
|
+
/** 一条消息交接出去的那张图,没有就是空数组。 */
|
|
30426
|
+
function itemsOfMessage(payload) {
|
|
30427
|
+
if (payload === null || typeof payload !== "object") return [];
|
|
30428
|
+
const item = handoffMention(contentText(payload.content));
|
|
30429
|
+
return item === null ? [] : [item];
|
|
30430
|
+
}
|
|
30431
|
+
const WRAP_STYLE = {
|
|
30432
|
+
display: "flex",
|
|
30433
|
+
flexWrap: "wrap",
|
|
30434
|
+
alignItems: "flex-start",
|
|
30435
|
+
gap: 8,
|
|
30436
|
+
margin: "8px 0 4px"
|
|
30437
|
+
};
|
|
30438
|
+
const ITEM_STYLE = {
|
|
30439
|
+
display: "flex",
|
|
30440
|
+
flexDirection: "column",
|
|
30441
|
+
gap: 2,
|
|
30442
|
+
maxWidth: "100%"
|
|
30443
|
+
};
|
|
30444
|
+
const THUMB_STYLE = {
|
|
30445
|
+
display: "block",
|
|
30446
|
+
maxHeight: 180,
|
|
30447
|
+
maxWidth: "100%",
|
|
30448
|
+
borderRadius: 8,
|
|
30449
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
30450
|
+
background: "rgba(127,127,127,0.06)",
|
|
30451
|
+
cursor: "zoom-in"
|
|
30452
|
+
};
|
|
30453
|
+
const CAPTION_STYLE = {
|
|
30454
|
+
display: "block",
|
|
30455
|
+
maxWidth: 180,
|
|
30456
|
+
fontFamily: "inherit",
|
|
30457
|
+
fontSize: 11,
|
|
30458
|
+
lineHeight: 1.4,
|
|
30459
|
+
color: "rgba(127,127,127,0.95)",
|
|
30460
|
+
overflow: "hidden",
|
|
30461
|
+
textOverflow: "ellipsis",
|
|
30462
|
+
whiteSpace: "nowrap"
|
|
30463
|
+
};
|
|
30464
|
+
const OVERLAY_STYLE = {
|
|
30465
|
+
position: "fixed",
|
|
30466
|
+
inset: 0,
|
|
30467
|
+
zIndex: 1e3,
|
|
30468
|
+
display: "flex",
|
|
30469
|
+
alignItems: "center",
|
|
30470
|
+
justifyContent: "center",
|
|
30471
|
+
background: "rgba(0,0,0,0.72)",
|
|
30472
|
+
cursor: "zoom-out"
|
|
30473
|
+
};
|
|
30474
|
+
const FULL_STYLE = {
|
|
30475
|
+
maxWidth: "92vw",
|
|
30476
|
+
maxHeight: "92vh",
|
|
30477
|
+
borderRadius: 6,
|
|
30478
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)"
|
|
30479
|
+
};
|
|
30480
|
+
/** 加载失败的缩略图占位(不再像旧版那样整张消失,只留一个问号让人瞎猜)。 */
|
|
30481
|
+
const THUMB_BROKEN_STYLE = {
|
|
30482
|
+
...THUMB_STYLE,
|
|
30483
|
+
display: "flex",
|
|
30484
|
+
alignItems: "center",
|
|
30485
|
+
justifyContent: "center",
|
|
30486
|
+
width: 160,
|
|
30487
|
+
height: 120,
|
|
30488
|
+
cursor: "default",
|
|
30489
|
+
fontSize: 12
|
|
30490
|
+
};
|
|
30491
|
+
const LIGHTBOX_NAV_STYLE = {
|
|
30492
|
+
position: "absolute",
|
|
30493
|
+
top: "50%",
|
|
30494
|
+
transform: "translateY(-50%)",
|
|
30495
|
+
zIndex: 1001,
|
|
30496
|
+
width: 44,
|
|
30497
|
+
height: 44,
|
|
30498
|
+
borderRadius: "50%",
|
|
30499
|
+
border: "none",
|
|
30500
|
+
background: "rgba(255,255,255,0.14)",
|
|
30501
|
+
color: "#fff",
|
|
30502
|
+
fontSize: 22,
|
|
30503
|
+
lineHeight: "44px",
|
|
30504
|
+
textAlign: "center",
|
|
30505
|
+
cursor: "pointer",
|
|
30506
|
+
padding: 0
|
|
30507
|
+
};
|
|
30508
|
+
const LIGHTBOX_CLOSE_STYLE = {
|
|
30509
|
+
position: "absolute",
|
|
30510
|
+
top: 16,
|
|
30511
|
+
right: 16,
|
|
30512
|
+
zIndex: 1001,
|
|
30513
|
+
width: 40,
|
|
30514
|
+
height: 40,
|
|
30515
|
+
borderRadius: "50%",
|
|
30516
|
+
border: "none",
|
|
30517
|
+
background: "rgba(255,255,255,0.14)",
|
|
30518
|
+
color: "#fff",
|
|
30519
|
+
fontSize: 20,
|
|
30520
|
+
lineHeight: "40px",
|
|
30521
|
+
textAlign: "center",
|
|
30522
|
+
cursor: "pointer",
|
|
30523
|
+
padding: 0
|
|
30524
|
+
};
|
|
30525
|
+
const LIGHTBOX_FULL_BROKEN_STYLE = {
|
|
30526
|
+
...FULL_STYLE,
|
|
30527
|
+
display: "flex",
|
|
30528
|
+
alignItems: "center",
|
|
30529
|
+
justifyContent: "center",
|
|
30530
|
+
width: "min(72vw, 560px)",
|
|
30531
|
+
height: "min(60vh, 360px)",
|
|
30532
|
+
background: "rgba(127,127,127,0.18)",
|
|
30533
|
+
color: "rgba(255,255,255,0.92)",
|
|
30534
|
+
fontSize: 14
|
|
30535
|
+
};
|
|
30536
|
+
/** 某条地址在这条路由上的 URL。 */
|
|
30537
|
+
function imageUrl(path, sessionId) {
|
|
30538
|
+
return `${ROUTE}?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(basenameOf(path))}`;
|
|
30539
|
+
}
|
|
30540
|
+
/**
|
|
30541
|
+
* 缩略图。取不到的(文件已删、会话读不出、格式不符)显示一块「加载失败」占位,
|
|
30542
|
+
* 而不是像旧版那样整张消失——消失了用户只会以为模型没存图,根本不知道该重试。
|
|
30543
|
+
*/
|
|
30544
|
+
function HandoffThumb(props) {
|
|
30545
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
30546
|
+
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30547
|
+
className: "daw-handoff-item",
|
|
30548
|
+
style: ITEM_STYLE,
|
|
30549
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30550
|
+
style: THUMB_BROKEN_STYLE,
|
|
30551
|
+
title: props.item.path,
|
|
30552
|
+
children: props.failedText
|
|
30553
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30554
|
+
className: "daw-handoff-caption",
|
|
30555
|
+
title: props.item.path,
|
|
30556
|
+
style: CAPTION_STYLE,
|
|
30557
|
+
children: props.item.path
|
|
30558
|
+
})]
|
|
30559
|
+
});
|
|
30560
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30561
|
+
className: "daw-handoff-item",
|
|
30562
|
+
style: ITEM_STYLE,
|
|
30563
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
30564
|
+
src: imageUrl(props.item.path, props.sessionId),
|
|
30565
|
+
alt: props.item.label,
|
|
30566
|
+
title: props.item.path,
|
|
30567
|
+
loading: "lazy",
|
|
30568
|
+
decoding: "async",
|
|
30569
|
+
referrerPolicy: "no-referrer",
|
|
30570
|
+
style: THUMB_STYLE,
|
|
30571
|
+
onError: () => setFailed(true),
|
|
30572
|
+
onClick: props.onOpen
|
|
30573
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30574
|
+
className: "daw-handoff-caption",
|
|
30575
|
+
title: props.item.path,
|
|
30576
|
+
style: CAPTION_STYLE,
|
|
30577
|
+
children: props.item.path
|
|
30578
|
+
})]
|
|
30579
|
+
});
|
|
30580
|
+
}
|
|
30581
|
+
/** 灯箱里的大图;换图时靠 key 重挂载来重置 failed。 */
|
|
30582
|
+
function LightboxImage(props) {
|
|
30583
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
30584
|
+
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30585
|
+
style: LIGHTBOX_FULL_BROKEN_STYLE,
|
|
30586
|
+
children: props.failedText
|
|
30587
|
+
});
|
|
30588
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
30589
|
+
src: imageUrl(props.item.path, props.sessionId),
|
|
30590
|
+
alt: props.item.label,
|
|
30591
|
+
decoding: "async",
|
|
30592
|
+
referrerPolicy: "no-referrer",
|
|
30593
|
+
style: FULL_STYLE,
|
|
30594
|
+
onError: () => setFailed(true)
|
|
30595
|
+
});
|
|
30596
|
+
}
|
|
30597
|
+
/**
|
|
30598
|
+
* 图集灯箱:点遮罩空白处关闭,但**点图片本身不再误关**(旧版 img 的点击冒泡到 overlay)。
|
|
30599
|
+
* 多图时左右切换,Esc 关闭。
|
|
30600
|
+
*/
|
|
30601
|
+
function HandoffLightbox(props) {
|
|
30602
|
+
const { items, index, onClose, onPrev, onNext } = props;
|
|
30603
|
+
const multiple = items.length > 1;
|
|
30604
|
+
(0, react.useEffect)(() => {
|
|
30605
|
+
const onKey = (event) => {
|
|
30606
|
+
if (event.key === "Escape") onClose();
|
|
30607
|
+
else if (multiple && event.key === "ArrowLeft") onPrev();
|
|
30608
|
+
else if (multiple && event.key === "ArrowRight") onNext();
|
|
30609
|
+
};
|
|
30610
|
+
document.addEventListener("keydown", onKey);
|
|
30611
|
+
return () => {
|
|
30612
|
+
document.removeEventListener("keydown", onKey);
|
|
30613
|
+
};
|
|
30614
|
+
}, [
|
|
30615
|
+
multiple,
|
|
30616
|
+
onClose,
|
|
30617
|
+
onPrev,
|
|
30618
|
+
onNext
|
|
30619
|
+
]);
|
|
30620
|
+
const item = items[index];
|
|
30621
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30622
|
+
style: {
|
|
30623
|
+
...OVERLAY_STYLE,
|
|
30624
|
+
cursor: "zoom-out"
|
|
30625
|
+
},
|
|
30626
|
+
role: "dialog",
|
|
30627
|
+
"aria-modal": "true",
|
|
30628
|
+
"aria-label": item.path,
|
|
30629
|
+
onClick: onClose,
|
|
30630
|
+
children: [
|
|
30631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30632
|
+
type: "button",
|
|
30633
|
+
style: LIGHTBOX_CLOSE_STYLE,
|
|
30634
|
+
"aria-label": "×",
|
|
30635
|
+
onClick: onClose,
|
|
30636
|
+
children: "×"
|
|
30637
|
+
}),
|
|
30638
|
+
multiple && index > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30639
|
+
type: "button",
|
|
30640
|
+
style: {
|
|
30641
|
+
...LIGHTBOX_NAV_STYLE,
|
|
30642
|
+
left: 24
|
|
30643
|
+
},
|
|
30644
|
+
"aria-label": props.prevLabel,
|
|
30645
|
+
title: props.prevLabel,
|
|
30646
|
+
onClick: (e) => {
|
|
30647
|
+
e.stopPropagation();
|
|
30648
|
+
onPrev();
|
|
30649
|
+
},
|
|
30650
|
+
children: "‹"
|
|
30651
|
+
}) : null,
|
|
30652
|
+
multiple && index < items.length - 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30653
|
+
type: "button",
|
|
30654
|
+
style: {
|
|
30655
|
+
...LIGHTBOX_NAV_STYLE,
|
|
30656
|
+
right: 24
|
|
30657
|
+
},
|
|
30658
|
+
"aria-label": props.nextLabel,
|
|
30659
|
+
title: props.nextLabel,
|
|
30660
|
+
onClick: (e) => {
|
|
30661
|
+
e.stopPropagation();
|
|
30662
|
+
onNext();
|
|
30663
|
+
},
|
|
30664
|
+
children: "›"
|
|
30665
|
+
}) : null,
|
|
30666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30667
|
+
style: {
|
|
30668
|
+
position: "relative",
|
|
30669
|
+
cursor: "default"
|
|
30670
|
+
},
|
|
30671
|
+
onClick: (e) => e.stopPropagation(),
|
|
30672
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LightboxImage, {
|
|
30673
|
+
item,
|
|
30674
|
+
sessionId: props.sessionId,
|
|
30675
|
+
failedText: props.failedText
|
|
30676
|
+
}, index), multiple ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30677
|
+
style: {
|
|
30678
|
+
position: "absolute",
|
|
30679
|
+
bottom: -28,
|
|
30680
|
+
left: 0,
|
|
30681
|
+
right: 0,
|
|
30682
|
+
textAlign: "center",
|
|
30683
|
+
color: "rgba(255,255,255,0.85)",
|
|
30684
|
+
fontSize: 12
|
|
30685
|
+
},
|
|
30686
|
+
children: [
|
|
30687
|
+
index + 1,
|
|
30688
|
+
" / ",
|
|
30689
|
+
items.length
|
|
30690
|
+
]
|
|
30691
|
+
}) : null]
|
|
30692
|
+
})
|
|
30693
|
+
]
|
|
30694
|
+
});
|
|
30695
|
+
}
|
|
30696
|
+
/** 把本轮各节点里交接出去的图收成一串(按出现顺序去重)。 */
|
|
30697
|
+
function collectTurnItems(nodes, locations, turn) {
|
|
30698
|
+
if (nodes === void 0 || locations === void 0 || turn === void 0 || turn === null) return [];
|
|
30699
|
+
let keys;
|
|
30700
|
+
try {
|
|
30701
|
+
keys = locations.getTurn(turn.turn);
|
|
30702
|
+
} catch {
|
|
30703
|
+
return [];
|
|
30704
|
+
}
|
|
30705
|
+
if (!Array.isArray(keys)) return [];
|
|
30706
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30707
|
+
const items = [];
|
|
30708
|
+
for (const key of keys) {
|
|
30709
|
+
const node = nodes.get(key);
|
|
30710
|
+
if (node === void 0 || node === null || node.kind !== KIND) continue;
|
|
30711
|
+
const list = node.data?.items;
|
|
30712
|
+
if (!Array.isArray(list)) continue;
|
|
30713
|
+
for (const item of list) {
|
|
30714
|
+
if (item === null || typeof item !== "object" || typeof item.path !== "string") continue;
|
|
30715
|
+
if (seen.has(item.path)) continue;
|
|
30716
|
+
seen.add(item.path);
|
|
30717
|
+
items.push({
|
|
30718
|
+
path: item.path,
|
|
30719
|
+
label: typeof item.label === "string" ? item.label : basenameOf(item.path)
|
|
30720
|
+
});
|
|
30721
|
+
if (items.length >= MAX_IMAGES) return items;
|
|
30722
|
+
}
|
|
30723
|
+
}
|
|
30724
|
+
return items;
|
|
30434
30725
|
}
|
|
30435
30726
|
/**
|
|
30436
|
-
*
|
|
30437
|
-
*
|
|
30438
|
-
* **缺键不抛异常**。原来这里是 `dictionaries[locale][key].replace(...)`:任何一个不存在的键
|
|
30439
|
-
* 都会抛 TypeError,而它在 React 渲染路径上 —— 结果不是那一行显示不出原文,而是**整个设置
|
|
30440
|
-
* 面板白屏**。缺一个描述行就把七张卡里的全部设置都搭进去,代价和收益完全不成比例。
|
|
30727
|
+
* Turn 尾部的图集:本轮每条交接消息各自那张图。
|
|
30441
30728
|
*
|
|
30442
|
-
*
|
|
30443
|
-
* 这样的字面量,一眼能看出该补哪条,而不是面对一个空白面板。
|
|
30729
|
+
* 没有会话 id 就什么都不显示 —— 路由需要它来定位目录,猜一个只会 404。
|
|
30444
30730
|
*/
|
|
30445
|
-
function
|
|
30446
|
-
const
|
|
30447
|
-
const
|
|
30448
|
-
|
|
30449
|
-
const
|
|
30450
|
-
|
|
30451
|
-
return
|
|
30731
|
+
function HandoffTurnTail(props) {
|
|
30732
|
+
const items = collectTurnItems(props.useChat((snapshot) => snapshot.nodes), props.useChat((snapshot) => snapshot.locations), props.turn);
|
|
30733
|
+
const sessionId = props.sessionId;
|
|
30734
|
+
const { t } = useWorkspaceI18n();
|
|
30735
|
+
const [activeIndex, setActiveIndex] = (0, react.useState)(null);
|
|
30736
|
+
if (items.length === 0) return null;
|
|
30737
|
+
if (typeof sessionId !== "string" || sessionId === "") return null;
|
|
30738
|
+
const openIndex = activeIndex !== null && activeIndex < items.length ? activeIndex : null;
|
|
30739
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30740
|
+
className: "daw-handoff-gallery",
|
|
30741
|
+
style: WRAP_STYLE,
|
|
30742
|
+
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffThumb, {
|
|
30743
|
+
item,
|
|
30744
|
+
sessionId,
|
|
30745
|
+
failedText: t("imageLoadFailed"),
|
|
30746
|
+
onOpen: () => setActiveIndex(index)
|
|
30747
|
+
}, item.path))
|
|
30748
|
+
}), openIndex !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffLightbox, {
|
|
30749
|
+
items,
|
|
30750
|
+
index: openIndex,
|
|
30751
|
+
sessionId,
|
|
30752
|
+
failedText: t("imageLoadFailed"),
|
|
30753
|
+
prevLabel: t("prevImage"),
|
|
30754
|
+
nextLabel: t("nextImage"),
|
|
30755
|
+
onClose: () => setActiveIndex(null),
|
|
30756
|
+
onPrev: () => setActiveIndex((i) => i === null ? i : Math.max(0, i - 1)),
|
|
30757
|
+
onNext: () => setActiveIndex((i) => i === null ? i : Math.min(items.length - 1, i + 1))
|
|
30758
|
+
}) : null] });
|
|
30452
30759
|
}
|
|
30453
30760
|
/**
|
|
30454
|
-
*
|
|
30761
|
+
* 一条「交接消息 → 一个图集节点」的 Definition。
|
|
30455
30762
|
*
|
|
30456
|
-
*
|
|
30457
|
-
*
|
|
30458
|
-
* 出问题时用户把屏幕上的字念出来,比报一串码有用得多。
|
|
30763
|
+
* 只匹配 `user/message`:交接说明属于读者自己发的那条消息。助手正文、上下文注入、
|
|
30764
|
+
* 工具输出里到处都是引用 `.dsh-images/…` 的文本,读它们会把别人的话切成碎图。
|
|
30459
30765
|
*
|
|
30460
|
-
*
|
|
30461
|
-
*
|
|
30462
|
-
* 露出键名(见上面),界面上会突然冒出一行 `scopeChatread`。白屏那次就是这么来的。
|
|
30766
|
+
* 节点自身**不注册渲染器**(Turn 尾负责显示),所以过程分组里不会多出一行、
|
|
30767
|
+
* verbose 模式下也不会重复一次。
|
|
30463
30768
|
*/
|
|
30464
|
-
const
|
|
30465
|
-
|
|
30466
|
-
|
|
30467
|
-
|
|
30468
|
-
|
|
30469
|
-
|
|
30470
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
|
|
30479
|
-
|
|
30480
|
-
|
|
30481
|
-
|
|
30482
|
-
|
|
30483
|
-
|
|
30484
|
-
|
|
30485
|
-
|
|
30486
|
-
|
|
30487
|
-
|
|
30488
|
-
return
|
|
30489
|
-
|
|
30490
|
-
|
|
30769
|
+
const handoffDefinition = {
|
|
30770
|
+
kind: KIND,
|
|
30771
|
+
target: "chat",
|
|
30772
|
+
match(event) {
|
|
30773
|
+
if (event === null || typeof event !== "object" || event.surfaceOp !== "append") return null;
|
|
30774
|
+
if (event.type !== "user/message") return null;
|
|
30775
|
+
return {
|
|
30776
|
+
id: `m:${String(event.seq)}`,
|
|
30777
|
+
role: "start"
|
|
30778
|
+
};
|
|
30779
|
+
},
|
|
30780
|
+
start(_context, match) {
|
|
30781
|
+
return {
|
|
30782
|
+
items: itemsOfMessage(match.event.data),
|
|
30783
|
+
anchorSeq: match.event.seq
|
|
30784
|
+
};
|
|
30785
|
+
},
|
|
30786
|
+
update(context) {
|
|
30787
|
+
return context.state;
|
|
30788
|
+
},
|
|
30789
|
+
buildViewNode(context) {
|
|
30790
|
+
const state = context.state;
|
|
30791
|
+
if (state === void 0 || !Array.isArray(state.items) || state.items.length === 0) return null;
|
|
30792
|
+
const start = context.start;
|
|
30793
|
+
return {
|
|
30794
|
+
key: context.key,
|
|
30795
|
+
kind: KIND,
|
|
30796
|
+
id: context.id,
|
|
30797
|
+
target: "chat",
|
|
30798
|
+
anchorSeq: state.anchorSeq,
|
|
30799
|
+
location: start !== void 0 && start !== null ? start.location : { kind: "unresolved" },
|
|
30800
|
+
visibility: "visible",
|
|
30801
|
+
data: { items: state.items }
|
|
30802
|
+
};
|
|
30491
30803
|
}
|
|
30492
|
-
}
|
|
30804
|
+
};
|
|
30493
30805
|
//#endregion
|
|
30494
30806
|
//#region src/client/styles.ts
|
|
30495
30807
|
const STYLE_ID = "dsh-workspace-style";
|
|
@@ -30505,16 +30817,33 @@ window.__ModuleLoader__.load({
|
|
|
30505
30817
|
* 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
|
|
30506
30818
|
*/
|
|
30507
30819
|
const WORKSPACE_CSS = `
|
|
30508
|
-
.daw-version{font:600 11px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-muted);white-space:nowrap}
|
|
30509
30820
|
.daw-root,.daw-settings-section{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
|
|
30510
|
-
.daw-root *{box-sizing:border-box;letter-spacing:0}
|
|
30511
|
-
|
|
30821
|
+
.daw-root *,.daw-settings-section *{box-sizing:border-box;letter-spacing:0}
|
|
30822
|
+
/* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
|
|
30823
|
+
深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
|
|
30824
|
+
.daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
|
|
30825
|
+
.daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
|
|
30826
|
+
.daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
|
|
30827
|
+
.daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
|
|
30512
30828
|
.daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
|
|
30513
30829
|
.daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
|
|
30514
30830
|
.daw-icon:disabled{opacity:.45;cursor:not-allowed}
|
|
30515
|
-
|
|
30516
|
-
|
|
30517
|
-
.daw-
|
|
30831
|
+
/* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
|
|
30832
|
+
全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
|
|
30833
|
+
.daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
|
|
30834
|
+
.daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30835
|
+
.daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
|
|
30836
|
+
/* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
|
|
30837
|
+
.daw-command:disabled{opacity:.5;cursor:not-allowed}
|
|
30838
|
+
.daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
|
|
30839
|
+
.daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30840
|
+
.daw-command.danger{color:var(--daw-danger)}
|
|
30841
|
+
.daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
|
|
30842
|
+
/* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
|
|
30843
|
+
.daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
|
|
30844
|
+
.daw-copy-label{font-size:12px}
|
|
30845
|
+
.daw-empty{padding:14px 2px;text-align:center}
|
|
30846
|
+
.daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
|
|
30518
30847
|
.daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
30519
30848
|
.daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30520
30849
|
.daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
@@ -30529,7 +30858,17 @@ window.__ModuleLoader__.load({
|
|
|
30529
30858
|
.daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
|
|
30530
30859
|
.daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
|
|
30531
30860
|
.daw-builtin-hint{text-align:right;align-self:center}
|
|
30532
|
-
.daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-
|
|
30861
|
+
.daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
|
|
30862
|
+
/* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
|
|
30863
|
+
.daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
|
|
30864
|
+
.daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
|
|
30865
|
+
.daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
|
|
30866
|
+
.daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
|
|
30867
|
+
/* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
|
|
30868
|
+
不再把「未知进度」伪装成恒定 30%。 */
|
|
30869
|
+
.daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
|
|
30870
|
+
@keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
|
|
30871
|
+
@media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
|
|
30533
30872
|
`;
|
|
30534
30873
|
function installWorkspaceStyles() {
|
|
30535
30874
|
if (document.getElementById(STYLE_ID) !== null) return;
|
|
@@ -30566,7 +30905,6 @@ window.__ModuleLoader__.load({
|
|
|
30566
30905
|
}];
|
|
30567
30906
|
//#endregion
|
|
30568
30907
|
//#region src/client/workspace.tsx
|
|
30569
|
-
var import_lucide_react = require_lucide_react();
|
|
30570
30908
|
/** What the settings-page launcher opens: authorized roots, the first section. */
|
|
30571
30909
|
const DEFAULT_PANEL_SECTION = "roots";
|
|
30572
30910
|
/**
|
|
@@ -30579,9 +30917,37 @@ window.__ModuleLoader__.load({
|
|
|
30579
30917
|
function tabLabelKey(tab) {
|
|
30580
30918
|
return tab === "app-download" ? "appDownloadTab" : `${tab}Tab`;
|
|
30581
30919
|
}
|
|
30582
|
-
/**
|
|
30583
|
-
function
|
|
30584
|
-
return
|
|
30920
|
+
/** 配对码剩余分钟:向上取整 —— 显示「约 9 分钟」比「约 8 分钟」在最后一刻更诚实。 */
|
|
30921
|
+
function pairingMinutesLeft(expiresAt, now) {
|
|
30922
|
+
return Math.max(0, Math.ceil((expiresAt - now) / 6e4));
|
|
30923
|
+
}
|
|
30924
|
+
/**
|
|
30925
|
+
* 通用复制小按钮:成功后短暂换成对勾 + 「已复制」,2s 自动复位;
|
|
30926
|
+
* 非安全上下文(HTTP/LAN)拿不到 Clipboard API 时退化为选中文本提示,见 copyText 的调用方。
|
|
30927
|
+
*/
|
|
30928
|
+
function CopyButton(props) {
|
|
30929
|
+
const [state, setState] = (0, react.useState)("idle");
|
|
30930
|
+
const [inFlight, setInFlight] = (0, react.useState)(false);
|
|
30931
|
+
const label = state === "done" ? props.copiedText : state === "manual" ? props.manualText ?? props.title : props.title;
|
|
30932
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
30933
|
+
type: "button",
|
|
30934
|
+
className: `daw-command icon-as-command${props.className === void 0 ? "" : ` ${props.className}`}`,
|
|
30935
|
+
title: props.title,
|
|
30936
|
+
"aria-label": props.title,
|
|
30937
|
+
disabled: inFlight || state !== "idle",
|
|
30938
|
+
onClick: () => {
|
|
30939
|
+
if (inFlight) return;
|
|
30940
|
+
setInFlight(true);
|
|
30941
|
+
props.copyText(props.text).then((kind) => {
|
|
30942
|
+
setState(kind === "done" ? "done" : "manual");
|
|
30943
|
+
window.setTimeout(() => setState("idle"), kind === "done" ? 2e3 : 2500);
|
|
30944
|
+
}).finally(() => setInFlight(false));
|
|
30945
|
+
},
|
|
30946
|
+
children: [state === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Check, { size: 15 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Copy, { size: 15 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30947
|
+
className: "daw-copy-label",
|
|
30948
|
+
children: label
|
|
30949
|
+
})]
|
|
30950
|
+
});
|
|
30585
30951
|
}
|
|
30586
30952
|
/**
|
|
30587
30953
|
* 卡片头部的描述行。与 `tabLabelKey` 同理:唯一含连字符的分区显式映射。
|
|
@@ -30600,7 +30966,6 @@ window.__ModuleLoader__.load({
|
|
|
30600
30966
|
devices: "devicesTabDesc",
|
|
30601
30967
|
image: "imageTabDesc",
|
|
30602
30968
|
trash: "trashTabDesc",
|
|
30603
|
-
audit: "auditTabDesc",
|
|
30604
30969
|
"app-download": "appDownloadTabDesc"
|
|
30605
30970
|
};
|
|
30606
30971
|
function tabDescKey(tab) {
|
|
@@ -30706,7 +31071,6 @@ window.__ModuleLoader__.load({
|
|
|
30706
31071
|
const { locale, t } = useWorkspaceI18n();
|
|
30707
31072
|
const [status, setStatus] = (0, react.useState)();
|
|
30708
31073
|
const [trash, setTrash] = (0, react.useState)([]);
|
|
30709
|
-
const [audit, setAudit] = (0, react.useState)([]);
|
|
30710
31074
|
const [path, setPath] = (0, react.useState)("");
|
|
30711
31075
|
const [label, setLabel] = (0, react.useState)("");
|
|
30712
31076
|
const [listenerHost, setListenerHost] = (0, react.useState)("0.0.0.0");
|
|
@@ -30721,9 +31085,52 @@ window.__ModuleLoader__.load({
|
|
|
30721
31085
|
const [openCards, setOpenCards] = (0, react.useState)(() => /* @__PURE__ */ new Set([props.initialOpen]));
|
|
30722
31086
|
const [imageSaved, setImageSaved] = (0, react.useState)(false);
|
|
30723
31087
|
const [error, setError] = (0, react.useState)();
|
|
31088
|
+
const [notice, setNotice] = (0, react.useState)();
|
|
30724
31089
|
const [tunnel, setTunnel] = (0, react.useState)();
|
|
30725
31090
|
const [tunnelBusy, setTunnelBusy] = (0, react.useState)(false);
|
|
30726
31091
|
const [activity, setActivity] = (0, react.useState)();
|
|
31092
|
+
const [pruneDays, setPruneDays] = (0, react.useState)(30);
|
|
31093
|
+
const [pruneBusy, setPruneBusy] = (0, react.useState)(false);
|
|
31094
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
31095
|
+
const notify = (0, react.useCallback)((key, params) => {
|
|
31096
|
+
setNotice(params === void 0 ? {
|
|
31097
|
+
id: Date.now() + Math.random(),
|
|
31098
|
+
key
|
|
31099
|
+
} : {
|
|
31100
|
+
id: Date.now() + Math.random(),
|
|
31101
|
+
key,
|
|
31102
|
+
params
|
|
31103
|
+
});
|
|
31104
|
+
}, []);
|
|
31105
|
+
(0, react.useEffect)(() => {
|
|
31106
|
+
if (notice === void 0) return;
|
|
31107
|
+
const timer = window.setTimeout(() => setNotice(void 0), 3500);
|
|
31108
|
+
return () => window.clearTimeout(timer);
|
|
31109
|
+
}, [notice]);
|
|
31110
|
+
(0, react.useEffect)(() => {
|
|
31111
|
+
const timer = window.setInterval(() => setNow(Date.now()), 3e4);
|
|
31112
|
+
return () => window.clearInterval(timer);
|
|
31113
|
+
}, []);
|
|
31114
|
+
const copyText = (0, react.useCallback)(async (text) => {
|
|
31115
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
31116
|
+
if (clipboard !== void 0) try {
|
|
31117
|
+
await clipboard.writeText(text);
|
|
31118
|
+
return "done";
|
|
31119
|
+
} catch {}
|
|
31120
|
+
try {
|
|
31121
|
+
const node = document.createElement("textarea");
|
|
31122
|
+
node.value = text;
|
|
31123
|
+
node.style.position = "fixed";
|
|
31124
|
+
node.style.top = "-9999px";
|
|
31125
|
+
node.setAttribute("readonly", "");
|
|
31126
|
+
document.body.append(node);
|
|
31127
|
+
node.select();
|
|
31128
|
+
const ok = document.execCommand("copy");
|
|
31129
|
+
node.remove();
|
|
31130
|
+
if (ok) return "done";
|
|
31131
|
+
} catch {}
|
|
31132
|
+
return "manual";
|
|
31133
|
+
}, []);
|
|
30727
31134
|
const isOpen = (0, react.useCallback)((card) => openCards.has(card), [openCards]);
|
|
30728
31135
|
const toggleCard = (0, react.useCallback)((card) => {
|
|
30729
31136
|
setOpenCards((previous) => {
|
|
@@ -30733,60 +31140,170 @@ window.__ModuleLoader__.load({
|
|
|
30733
31140
|
});
|
|
30734
31141
|
}, []);
|
|
30735
31142
|
const refresh = (0, react.useCallback)(async () => {
|
|
31143
|
+
try {
|
|
31144
|
+
setTunnel(await props.api.tunnelStatus());
|
|
31145
|
+
} catch {}
|
|
30736
31146
|
try {
|
|
30737
31147
|
const next = await props.api.status();
|
|
30738
31148
|
setStatus(next);
|
|
30739
31149
|
if (openCards.has("trash")) setTrash((await props.api.trashItems()).items);
|
|
30740
|
-
if (openCards.has("audit")) setAudit((await props.api.audit()).items);
|
|
30741
31150
|
if (openCards.has("image")) {
|
|
30742
31151
|
const settings = await props.api.imageSettings();
|
|
30743
31152
|
setImage(settings);
|
|
30744
31153
|
setImageDraft(settings.subdir);
|
|
30745
31154
|
setImageSaved(false);
|
|
30746
31155
|
}
|
|
30747
|
-
if (openCards.has("remote")) setTunnel(await props.api.tunnelStatus());
|
|
30748
|
-
setError(void 0);
|
|
30749
31156
|
} catch (cause) {
|
|
30750
|
-
setError(cause);
|
|
31157
|
+
if (!(cause instanceof WorkspaceApiError && cause.status === 403 && (cause.code === "TUNNEL_MODE_ADMIN_DISABLED" || cause.code === "ADMIN_PROXY_FORWARDED"))) setError(cause);
|
|
30751
31158
|
}
|
|
30752
31159
|
}, [props.api, openCards]);
|
|
30753
|
-
const
|
|
31160
|
+
const tunnelErrorText = (0, react.useCallback)((s) => {
|
|
31161
|
+
if (s.errorKey !== void 0) return t(s.errorKey, s.errorParams ?? {});
|
|
31162
|
+
return s.error ?? t("listenerFailed");
|
|
31163
|
+
}, [t]);
|
|
31164
|
+
const pollActivityOnce = (0, react.useCallback)(async (active) => {
|
|
31165
|
+
while (active()) {
|
|
31166
|
+
try {
|
|
31167
|
+
const a = await props.api.tunnelActivity();
|
|
31168
|
+
if (!active()) return;
|
|
31169
|
+
setActivity(a.stage === "idle" ? void 0 : a);
|
|
31170
|
+
} catch {}
|
|
31171
|
+
await delay(400);
|
|
31172
|
+
}
|
|
31173
|
+
}, [props.api]);
|
|
31174
|
+
const waitForTunnelUrl = (0, react.useCallback)(async (active) => {
|
|
31175
|
+
for (let attempt = 0; attempt < 40 && active(); attempt += 1) {
|
|
31176
|
+
await delay(500);
|
|
31177
|
+
const s = await props.api.tunnelStatus();
|
|
31178
|
+
if (!active()) return null;
|
|
31179
|
+
setTunnel(s);
|
|
31180
|
+
if (s.errorKey !== void 0 || s.error !== void 0) throw new Error(tunnelErrorText(s));
|
|
31181
|
+
if (s.running && s.url !== null) return s.url;
|
|
31182
|
+
}
|
|
31183
|
+
return null;
|
|
31184
|
+
}, [props.api, tunnelErrorText]);
|
|
31185
|
+
const generateConfigForUrl = (0, react.useCallback)(async (baseUrl, noticeKey = "configCreated") => {
|
|
31186
|
+
setConfigTextBusy(true);
|
|
31187
|
+
setError(void 0);
|
|
31188
|
+
try {
|
|
31189
|
+
const currentRoots = status?.roots ?? [];
|
|
31190
|
+
const value = await props.api.createConfigText({
|
|
31191
|
+
...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) },
|
|
31192
|
+
endpoints: [{
|
|
31193
|
+
label: t("tunnelEndpointLabel"),
|
|
31194
|
+
baseUrl
|
|
31195
|
+
}]
|
|
31196
|
+
});
|
|
31197
|
+
setConfigText(value);
|
|
31198
|
+
notify(noticeKey);
|
|
31199
|
+
return true;
|
|
31200
|
+
} catch (cause) {
|
|
31201
|
+
setError(cause);
|
|
31202
|
+
return false;
|
|
31203
|
+
} finally {
|
|
31204
|
+
setConfigTextBusy(false);
|
|
31205
|
+
}
|
|
31206
|
+
}, [
|
|
31207
|
+
props.api,
|
|
31208
|
+
status,
|
|
31209
|
+
t,
|
|
31210
|
+
notify
|
|
31211
|
+
]);
|
|
31212
|
+
const createTunnel = (0, react.useCallback)(async (opts = {}) => {
|
|
30754
31213
|
if (!window.confirm(t("tunnelStartConfirm"))) return;
|
|
31214
|
+
const wantConfig = opts.thenGenConfig === true;
|
|
31215
|
+
let alive = true;
|
|
30755
31216
|
setTunnelBusy(true);
|
|
30756
31217
|
setError(void 0);
|
|
30757
|
-
|
|
31218
|
+
setConfigText(void 0);
|
|
30758
31219
|
setActivity({
|
|
30759
31220
|
stage: "locating",
|
|
30760
|
-
|
|
31221
|
+
labelKey: "tunnelStageLocating",
|
|
30761
31222
|
percent: null
|
|
30762
31223
|
});
|
|
30763
|
-
|
|
30764
|
-
|
|
30765
|
-
|
|
30766
|
-
}
|
|
30767
|
-
|
|
30768
|
-
props.api.tunnelCreate().then(async (next) => {
|
|
30769
|
-
setTunnel(next);
|
|
31224
|
+
pollActivityOnce(() => alive);
|
|
31225
|
+
try {
|
|
31226
|
+
const currentRoots = status?.roots ?? [];
|
|
31227
|
+
const created = await props.api.tunnelCreate(wantConfig, { ...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) } });
|
|
31228
|
+
setTunnel(created);
|
|
30770
31229
|
setActivity(void 0);
|
|
31230
|
+
if (created.errorKey !== void 0 || created.error !== void 0) throw new Error(tunnelErrorText(created));
|
|
31231
|
+
if (wantConfig && created.configText !== void 0) {
|
|
31232
|
+
setConfigText(created.configText);
|
|
31233
|
+
await refresh();
|
|
31234
|
+
notify("tunnelConfigAuto");
|
|
31235
|
+
return;
|
|
31236
|
+
}
|
|
31237
|
+
const url = created.url ?? await waitForTunnelUrl(() => alive);
|
|
30771
31238
|
await refresh();
|
|
30772
|
-
|
|
30773
|
-
|
|
31239
|
+
if (url === null) {
|
|
31240
|
+
notify("tunnelUrlPending");
|
|
31241
|
+
return;
|
|
31242
|
+
}
|
|
31243
|
+
if (wantConfig) await generateConfigForUrl(url, "tunnelConfigAuto");
|
|
31244
|
+
else notify("tunnelStarted");
|
|
31245
|
+
} catch (cause) {
|
|
31246
|
+
setError(cause);
|
|
31247
|
+
} finally {
|
|
31248
|
+
alive = false;
|
|
30774
31249
|
setActivity(void 0);
|
|
30775
31250
|
setTunnelBusy(false);
|
|
30776
|
-
}
|
|
31251
|
+
}
|
|
30777
31252
|
}, [
|
|
30778
31253
|
props.api,
|
|
30779
31254
|
refresh,
|
|
30780
|
-
|
|
30781
|
-
|
|
30782
|
-
|
|
31255
|
+
status,
|
|
31256
|
+
t,
|
|
31257
|
+
notify,
|
|
31258
|
+
pollActivityOnce,
|
|
31259
|
+
waitForTunnelUrl,
|
|
31260
|
+
tunnelErrorText,
|
|
31261
|
+
generateConfigForUrl
|
|
31262
|
+
]);
|
|
31263
|
+
const stopTunnel = (0, react.useCallback)(async () => {
|
|
31264
|
+
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
30783
31265
|
setTunnelBusy(true);
|
|
30784
31266
|
setError(void 0);
|
|
30785
|
-
|
|
31267
|
+
try {
|
|
31268
|
+
const next = await props.api.tunnelStop();
|
|
30786
31269
|
setTunnel(next);
|
|
30787
31270
|
await refresh();
|
|
30788
|
-
|
|
30789
|
-
|
|
31271
|
+
notify("tunnelStopped");
|
|
31272
|
+
} catch (cause) {
|
|
31273
|
+
setError(cause);
|
|
31274
|
+
} finally {
|
|
31275
|
+
setTunnelBusy(false);
|
|
31276
|
+
}
|
|
31277
|
+
}, [
|
|
31278
|
+
props.api,
|
|
31279
|
+
refresh,
|
|
31280
|
+
t,
|
|
31281
|
+
notify
|
|
31282
|
+
]);
|
|
31283
|
+
const tunnelConfigClick = (0, react.useCallback)(async () => {
|
|
31284
|
+
if (tunnel?.running === true) {
|
|
31285
|
+
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
31286
|
+
setTunnelBusy(true);
|
|
31287
|
+
setError(void 0);
|
|
31288
|
+
try {
|
|
31289
|
+
const stopped = await props.api.tunnelStop();
|
|
31290
|
+
setTunnel(stopped);
|
|
31291
|
+
await refresh();
|
|
31292
|
+
} catch (cause) {
|
|
31293
|
+
setError(cause);
|
|
31294
|
+
} finally {
|
|
31295
|
+
setTunnelBusy(false);
|
|
31296
|
+
}
|
|
31297
|
+
await createTunnel({ thenGenConfig: true });
|
|
31298
|
+
} else await createTunnel({ thenGenConfig: true });
|
|
31299
|
+
}, [
|
|
31300
|
+
props.api,
|
|
31301
|
+
tunnel,
|
|
31302
|
+
t,
|
|
31303
|
+
createTunnel,
|
|
31304
|
+
refresh,
|
|
31305
|
+
setError
|
|
31306
|
+
]);
|
|
30790
31307
|
(0, react.useEffect)(() => {
|
|
30791
31308
|
refresh();
|
|
30792
31309
|
}, [refresh]);
|
|
@@ -30796,25 +31313,7 @@ window.__ModuleLoader__.load({
|
|
|
30796
31313
|
setListenerPort(String(status.configuredPort));
|
|
30797
31314
|
}, [status?.configuredHost, status?.configuredPort]);
|
|
30798
31315
|
const roots = status?.roots ?? [];
|
|
30799
|
-
const
|
|
30800
|
-
setConfigTextBusy(true);
|
|
30801
|
-
setError(void 0);
|
|
30802
|
-
props.api.createConfigText({
|
|
30803
|
-
...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
|
|
30804
|
-
endpoints: [{
|
|
30805
|
-
label: t("tunnelEndpointLabel"),
|
|
30806
|
-
baseUrl
|
|
30807
|
-
}]
|
|
30808
|
-
}).then((value) => {
|
|
30809
|
-
setConfigText(value);
|
|
30810
|
-
return refresh();
|
|
30811
|
-
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
30812
|
-
}, [
|
|
30813
|
-
props.api,
|
|
30814
|
-
refresh,
|
|
30815
|
-
roots,
|
|
30816
|
-
t
|
|
30817
|
-
]);
|
|
31316
|
+
const managementLocked = tunnel?.running === true;
|
|
30818
31317
|
const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEVICE_SCOPES]);
|
|
30819
31318
|
const toggleScope = (0, react.useCallback)((scope) => {
|
|
30820
31319
|
setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
|
|
@@ -30843,10 +31342,25 @@ window.__ModuleLoader__.load({
|
|
|
30843
31342
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
30844
31343
|
className: "daw-admin",
|
|
30845
31344
|
children: [
|
|
31345
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31346
|
+
className: "daw-admin-topbar",
|
|
31347
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LanguageToggle, {})
|
|
31348
|
+
}),
|
|
30846
31349
|
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30847
31350
|
className: "daw-warning",
|
|
31351
|
+
role: "alert",
|
|
30848
31352
|
children: messageOf(error, t)
|
|
30849
31353
|
}),
|
|
31354
|
+
notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31355
|
+
className: "daw-success",
|
|
31356
|
+
role: "status",
|
|
31357
|
+
children: t(notice.key, notice.params)
|
|
31358
|
+
}),
|
|
31359
|
+
managementLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31360
|
+
className: "daw-warning",
|
|
31361
|
+
role: "status",
|
|
31362
|
+
children: t("managementLockedWhileTunnel")
|
|
31363
|
+
}),
|
|
30850
31364
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
30851
31365
|
tab: "roots",
|
|
30852
31366
|
nameKey: tabLabelKey("roots"),
|
|
@@ -30871,10 +31385,14 @@ window.__ModuleLoader__.load({
|
|
|
30871
31385
|
}),
|
|
30872
31386
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30873
31387
|
className: "daw-command primary",
|
|
31388
|
+
disabled: managementLocked,
|
|
31389
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
30874
31390
|
onClick: () => {
|
|
31391
|
+
setError(void 0);
|
|
30875
31392
|
props.api.addRoot(path, label || void 0).then(() => {
|
|
30876
31393
|
setPath("");
|
|
30877
31394
|
setLabel("");
|
|
31395
|
+
notify("rootAdded");
|
|
30878
31396
|
return refresh();
|
|
30879
31397
|
}).catch(setError);
|
|
30880
31398
|
},
|
|
@@ -30901,8 +31419,15 @@ window.__ModuleLoader__.load({
|
|
|
30901
31419
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30902
31420
|
className: "daw-icon danger",
|
|
30903
31421
|
title: t("removeRoot"),
|
|
31422
|
+
"aria-label": t("removeRoot"),
|
|
31423
|
+
disabled: managementLocked,
|
|
30904
31424
|
onClick: () => {
|
|
30905
|
-
|
|
31425
|
+
if (!window.confirm(t("removeRootConfirm", { label: root.label }))) return;
|
|
31426
|
+
setError(void 0);
|
|
31427
|
+
props.api.removeRoot(root.id).then(() => {
|
|
31428
|
+
notify("rootRemoved");
|
|
31429
|
+
return refresh();
|
|
31430
|
+
}).catch(setError);
|
|
30906
31431
|
},
|
|
30907
31432
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
30908
31433
|
})]
|
|
@@ -30925,7 +31450,7 @@ window.__ModuleLoader__.load({
|
|
|
30925
31450
|
className: "daw-warning",
|
|
30926
31451
|
children: t("httpWarning")
|
|
30927
31452
|
}),
|
|
30928
|
-
roots.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31453
|
+
roots.length === 0 && !managementLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30929
31454
|
className: "daw-warning",
|
|
30930
31455
|
children: t("addRootBeforeRemote")
|
|
30931
31456
|
}),
|
|
@@ -30937,7 +31462,7 @@ window.__ModuleLoader__.load({
|
|
|
30937
31462
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindIp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30938
31463
|
className: "daw-input",
|
|
30939
31464
|
value: listenerHost,
|
|
30940
|
-
disabled: status?.remoteEnabled,
|
|
31465
|
+
disabled: status?.remoteEnabled || managementLocked,
|
|
30941
31466
|
spellCheck: false,
|
|
30942
31467
|
placeholder: "0.0.0.0",
|
|
30943
31468
|
onChange: (event) => setListenerHost(event.target.value)
|
|
@@ -30952,15 +31477,19 @@ window.__ModuleLoader__.load({
|
|
|
30952
31477
|
max: 65535,
|
|
30953
31478
|
inputMode: "numeric",
|
|
30954
31479
|
value: listenerPort,
|
|
30955
|
-
disabled: status?.remoteEnabled,
|
|
31480
|
+
disabled: status?.remoteEnabled || managementLocked,
|
|
30956
31481
|
onChange: (event) => setListenerPort(event.target.value)
|
|
30957
31482
|
})]
|
|
30958
31483
|
}),
|
|
30959
31484
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30960
31485
|
className: "daw-command",
|
|
30961
|
-
disabled: status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
31486
|
+
disabled: managementLocked || status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
30962
31487
|
onClick: () => {
|
|
30963
|
-
|
|
31488
|
+
setError(void 0);
|
|
31489
|
+
props.api.configureListener(listenerHost.trim(), parsedListenerPort).then((result) => poll(result.id)).then(() => {
|
|
31490
|
+
notify("listenerSaved");
|
|
31491
|
+
return refresh();
|
|
31492
|
+
}).catch(setError);
|
|
30964
31493
|
},
|
|
30965
31494
|
children: t("saveListener")
|
|
30966
31495
|
})
|
|
@@ -31008,19 +31537,33 @@ window.__ModuleLoader__.load({
|
|
|
31008
31537
|
className: "daw-warning",
|
|
31009
31538
|
children: t("tunnelSecurityWarning")
|
|
31010
31539
|
}),
|
|
31011
|
-
tunnel?.
|
|
31540
|
+
tunnel?.errorKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31541
|
+
className: "daw-warning",
|
|
31542
|
+
role: "alert",
|
|
31543
|
+
children: t(tunnel.errorKey, tunnel.errorParams ?? {})
|
|
31544
|
+
}),
|
|
31545
|
+
tunnel?.errorKey === void 0 && tunnel?.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31012
31546
|
className: "daw-warning",
|
|
31547
|
+
role: "alert",
|
|
31013
31548
|
children: tunnel.error
|
|
31014
31549
|
}),
|
|
31015
|
-
tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
31016
|
-
className: "daw-
|
|
31017
|
-
|
|
31018
|
-
|
|
31550
|
+
tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31551
|
+
className: "daw-url-row",
|
|
31552
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31553
|
+
className: "daw-tunnel-url",
|
|
31554
|
+
children: tunnel.url
|
|
31555
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
31556
|
+
text: tunnel.url,
|
|
31557
|
+
title: t("copyUrl"),
|
|
31558
|
+
copiedText: t("copied"),
|
|
31559
|
+
manualText: t("copyManual"),
|
|
31560
|
+
copyText
|
|
31561
|
+
})]
|
|
31019
31562
|
}),
|
|
31020
31563
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31021
31564
|
className: "daw-command primary",
|
|
31022
31565
|
style: { marginTop: 10 },
|
|
31023
|
-
disabled: tunnelBusy || roots.length === 0,
|
|
31566
|
+
disabled: tunnelBusy || tunnel?.running !== true && roots.length === 0,
|
|
31024
31567
|
onClick: () => {
|
|
31025
31568
|
if (tunnel?.running === true) stopTunnel();
|
|
31026
31569
|
else createTunnel();
|
|
@@ -31033,31 +31576,39 @@ window.__ModuleLoader__.load({
|
|
|
31033
31576
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31034
31577
|
className: "daw-progress-track",
|
|
31035
31578
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31036
|
-
className:
|
|
31037
|
-
style: { width:
|
|
31579
|
+
className: `daw-progress-fill${activity.percent === null ? " indeterminate" : ""}`,
|
|
31580
|
+
style: activity.percent === null ? void 0 : { width: `${activity.percent}%` }
|
|
31038
31581
|
})
|
|
31039
31582
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31040
31583
|
className: "daw-list-meta",
|
|
31041
31584
|
style: { marginTop: 4 },
|
|
31042
|
-
children: activity.
|
|
31585
|
+
children: activity.labelKey === "" ? "" : t(activity.labelKey, activity.labelParams ?? {})
|
|
31043
31586
|
})]
|
|
31044
31587
|
})
|
|
31045
31588
|
]
|
|
31046
31589
|
}),
|
|
31047
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
31048
|
-
|
|
31049
|
-
|
|
31050
|
-
|
|
31051
|
-
|
|
31052
|
-
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
|
|
31058
|
-
|
|
31059
|
-
|
|
31060
|
-
|
|
31590
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
31591
|
+
disabled: managementLocked,
|
|
31592
|
+
style: {
|
|
31593
|
+
border: "none",
|
|
31594
|
+
margin: 0,
|
|
31595
|
+
padding: 0
|
|
31596
|
+
},
|
|
31597
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31598
|
+
className: "daw-scope-grid",
|
|
31599
|
+
children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31600
|
+
className: "daw-check",
|
|
31601
|
+
children: [
|
|
31602
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31603
|
+
type: "checkbox",
|
|
31604
|
+
checked: grantedScopes.includes(scope),
|
|
31605
|
+
onChange: () => toggleScope(scope)
|
|
31606
|
+
}),
|
|
31607
|
+
" ",
|
|
31608
|
+
scopeLabel(locale, scope)
|
|
31609
|
+
]
|
|
31610
|
+
}, scope))
|
|
31611
|
+
})
|
|
31061
31612
|
}),
|
|
31062
31613
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31063
31614
|
className: "daw-list-meta",
|
|
@@ -31065,30 +31616,44 @@ window.__ModuleLoader__.load({
|
|
|
31065
31616
|
}),
|
|
31066
31617
|
status?.remoteEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31067
31618
|
className: "daw-command danger",
|
|
31619
|
+
disabled: managementLocked,
|
|
31620
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31068
31621
|
onClick: () => {
|
|
31069
|
-
|
|
31622
|
+
if (!window.confirm(t("disableRemoteConfirm"))) return;
|
|
31623
|
+
setError(void 0);
|
|
31624
|
+
props.api.disableRemote().then((result) => poll(result.id)).then(() => {
|
|
31625
|
+
notify("remoteDisabled");
|
|
31626
|
+
return refresh();
|
|
31627
|
+
}).catch(setError);
|
|
31070
31628
|
},
|
|
31071
31629
|
children: t("disableRemote")
|
|
31072
31630
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31073
31631
|
className: "daw-command primary",
|
|
31074
|
-
disabled: roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
31632
|
+
disabled: managementLocked || roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
31075
31633
|
onClick: () => {
|
|
31634
|
+
setError(void 0);
|
|
31076
31635
|
props.api.enableRemote(roots.map((root) => root.id), grantedScopes).then((result) => poll(result.id)).catch(setError);
|
|
31077
31636
|
},
|
|
31078
31637
|
children: t("enablePairing")
|
|
31079
31638
|
}),
|
|
31080
|
-
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31639
|
+
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31081
31640
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31082
31641
|
className: "daw-command daw-inline-command",
|
|
31083
|
-
disabled: grantedScopes.length === 0,
|
|
31642
|
+
disabled: managementLocked || grantedScopes.length === 0,
|
|
31643
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31084
31644
|
onClick: () => {
|
|
31085
|
-
|
|
31645
|
+
setError(void 0);
|
|
31646
|
+
props.api.createPairing(roots.map((root) => root.id), grantedScopes).then((value) => {
|
|
31647
|
+
setPairing(value);
|
|
31648
|
+
notify("pairingCreated");
|
|
31649
|
+
}).catch(setError);
|
|
31086
31650
|
},
|
|
31087
31651
|
children: t("newPairingCode")
|
|
31088
31652
|
}),
|
|
31089
31653
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31090
31654
|
className: "daw-command daw-inline-command",
|
|
31091
|
-
disabled: configTextBusy,
|
|
31655
|
+
disabled: managementLocked || configTextBusy,
|
|
31656
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31092
31657
|
onClick: () => {
|
|
31093
31658
|
const manual = configEndpoint.trim();
|
|
31094
31659
|
setConfigTextBusy(true);
|
|
@@ -31101,7 +31666,7 @@ window.__ModuleLoader__.load({
|
|
|
31101
31666
|
}] }
|
|
31102
31667
|
}).then((value) => {
|
|
31103
31668
|
setConfigText(value);
|
|
31104
|
-
|
|
31669
|
+
notify("configCreated");
|
|
31105
31670
|
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
31106
31671
|
},
|
|
31107
31672
|
children: t("newConfigText")
|
|
@@ -31110,21 +31675,34 @@ window.__ModuleLoader__.load({
|
|
|
31110
31675
|
className: "daw-command daw-inline-command",
|
|
31111
31676
|
disabled: configTextBusy || tunnelBusy,
|
|
31112
31677
|
onClick: () => {
|
|
31113
|
-
|
|
31114
|
-
else createTunnel();
|
|
31678
|
+
tunnelConfigClick();
|
|
31115
31679
|
},
|
|
31116
|
-
children: tunnel.running
|
|
31680
|
+
children: tunnel.running !== true ? t("tunnelStartAndGenConfig") : t("tunnelRestartAndGenConfig")
|
|
31117
31681
|
}),
|
|
31118
31682
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31119
31683
|
className: "daw-list-meta daw-listener-hint",
|
|
31120
31684
|
children: t("configTextScopeNote")
|
|
31121
31685
|
})
|
|
31122
31686
|
] }),
|
|
31123
|
-
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
31124
|
-
className: "daw-code",
|
|
31125
|
-
children:
|
|
31126
|
-
|
|
31127
|
-
|
|
31687
|
+
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31688
|
+
className: "daw-code-row",
|
|
31689
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31690
|
+
className: "daw-code",
|
|
31691
|
+
children: pairing.code
|
|
31692
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
31693
|
+
className: "daw-copy-inline",
|
|
31694
|
+
text: pairing.code,
|
|
31695
|
+
title: t("copyPairing"),
|
|
31696
|
+
copiedText: t("copied"),
|
|
31697
|
+
manualText: t("copyManual"),
|
|
31698
|
+
copyText
|
|
31699
|
+
})]
|
|
31700
|
+
}), pairing.expiresAt !== void 0 && pairing.expiresAt > now && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31701
|
+
className: "daw-list-meta",
|
|
31702
|
+
style: { marginTop: -4 },
|
|
31703
|
+
children: t("pairingExpiresAt", { minutes: pairingMinutesLeft(pairing.expiresAt, now) })
|
|
31704
|
+
})] }),
|
|
31705
|
+
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31128
31706
|
style: {
|
|
31129
31707
|
marginTop: 12,
|
|
31130
31708
|
maxWidth: 560
|
|
@@ -31134,6 +31712,7 @@ window.__ModuleLoader__.load({
|
|
|
31134
31712
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("configTextManual") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31135
31713
|
className: "daw-input",
|
|
31136
31714
|
value: configEndpoint,
|
|
31715
|
+
disabled: managementLocked,
|
|
31137
31716
|
spellCheck: false,
|
|
31138
31717
|
placeholder: t("configTextManualPlaceholder"),
|
|
31139
31718
|
onChange: (event) => setConfigEndpoint(event.target.value)
|
|
@@ -31147,22 +31726,51 @@ window.__ModuleLoader__.load({
|
|
|
31147
31726
|
configText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigTextPanel, { result: configText })
|
|
31148
31727
|
]
|
|
31149
31728
|
}, "remote"),
|
|
31150
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
31729
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31151
31730
|
tab: "devices",
|
|
31152
31731
|
nameKey: tabLabelKey("devices"),
|
|
31153
31732
|
descKey: tabDescKey("devices"),
|
|
31154
31733
|
open: isOpen("devices"),
|
|
31155
31734
|
onToggle: () => toggleCard("devices"),
|
|
31156
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
31735
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31736
|
+
className: "daw-inline-command",
|
|
31737
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31738
|
+
className: "daw-field",
|
|
31739
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pruneInactiveDays") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31740
|
+
className: "daw-input",
|
|
31741
|
+
type: "number",
|
|
31742
|
+
min: 1,
|
|
31743
|
+
max: 3650,
|
|
31744
|
+
value: pruneDays,
|
|
31745
|
+
disabled: managementLocked || pruneBusy,
|
|
31746
|
+
onChange: (event) => setPruneDays(Math.max(1, Math.min(3650, Number(event.target.value) || 1)))
|
|
31747
|
+
})]
|
|
31748
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31749
|
+
className: "daw-command",
|
|
31750
|
+
disabled: managementLocked || pruneBusy,
|
|
31751
|
+
onClick: () => {
|
|
31752
|
+
if (!confirm(t("pruneInactiveConfirm", { days: String(pruneDays) }))) return;
|
|
31753
|
+
setPruneBusy(true);
|
|
31754
|
+
setError(void 0);
|
|
31755
|
+
props.api.pruneInactiveDevices(pruneDays).then((result) => {
|
|
31756
|
+
if (result.pruned === 0) notify("pruneInactiveNone");
|
|
31757
|
+
else notify("pruneInactiveDone", { count: String(result.pruned) });
|
|
31758
|
+
return refresh();
|
|
31759
|
+
}).catch(setError).finally(() => setPruneBusy(false));
|
|
31760
|
+
},
|
|
31761
|
+
children: pruneBusy ? t("pruneInactiveBusy") : t("pruneInactiveAction", { days: String(pruneDays) })
|
|
31762
|
+
})]
|
|
31763
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31157
31764
|
className: "daw-list",
|
|
31158
31765
|
children: (status?.devices ?? []).map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceRow, {
|
|
31159
31766
|
api: props.api,
|
|
31160
31767
|
device,
|
|
31161
31768
|
roots,
|
|
31162
31769
|
onChanged: refresh,
|
|
31163
|
-
onError: setError
|
|
31770
|
+
onError: setError,
|
|
31771
|
+
onNotice: notify
|
|
31164
31772
|
}, device.id))
|
|
31165
|
-
})
|
|
31773
|
+
})]
|
|
31166
31774
|
}, "devices"),
|
|
31167
31775
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31168
31776
|
tab: "image",
|
|
@@ -31264,7 +31872,10 @@ window.__ModuleLoader__.load({
|
|
|
31264
31872
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31265
31873
|
className: "daw-list-meta",
|
|
31266
31874
|
children: t("pluginTrashHint")
|
|
31267
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31875
|
+
}), trash.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31876
|
+
className: "daw-list-meta daw-empty",
|
|
31877
|
+
children: t("trashEmpty")
|
|
31878
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31268
31879
|
className: "daw-list",
|
|
31269
31880
|
children: trash.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31270
31881
|
className: "daw-list-row",
|
|
@@ -31283,48 +31894,28 @@ window.__ModuleLoader__.load({
|
|
|
31283
31894
|
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31284
31895
|
className: "daw-command",
|
|
31285
31896
|
onClick: () => {
|
|
31286
|
-
|
|
31897
|
+
setError(void 0);
|
|
31898
|
+
props.api.restoreTrash(item.id).then(() => {
|
|
31899
|
+
notify("trashRestored");
|
|
31900
|
+
return refresh();
|
|
31901
|
+
}).catch(setError);
|
|
31287
31902
|
},
|
|
31288
31903
|
children: t("restore")
|
|
31289
31904
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31290
31905
|
className: "daw-icon danger",
|
|
31291
31906
|
title: t("deletePermanently"),
|
|
31907
|
+
"aria-label": t("deletePermanently"),
|
|
31292
31908
|
onClick: () => {
|
|
31293
|
-
if (confirm(t("purgeConfirm"))) props.api.purgeTrash(item.id).then(
|
|
31909
|
+
if (confirm(t("purgeConfirm"))) props.api.purgeTrash(item.id).then(() => {
|
|
31910
|
+
notify("trashPurged");
|
|
31911
|
+
return refresh();
|
|
31912
|
+
}).catch(setError);
|
|
31294
31913
|
},
|
|
31295
31914
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31296
31915
|
})] })]
|
|
31297
31916
|
}, item.id))
|
|
31298
31917
|
})]
|
|
31299
31918
|
}, "trash"),
|
|
31300
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCard, {
|
|
31301
|
-
tab: "audit",
|
|
31302
|
-
nameKey: tabLabelKey("audit"),
|
|
31303
|
-
descKey: tabDescKey("audit"),
|
|
31304
|
-
open: isOpen("audit"),
|
|
31305
|
-
onToggle: () => toggleCard("audit"),
|
|
31306
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31307
|
-
className: "daw-list",
|
|
31308
|
-
children: audit.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31309
|
-
className: "daw-list-row",
|
|
31310
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31311
|
-
className: "daw-list-title",
|
|
31312
|
-
children: String(item.action ?? t("event"))
|
|
31313
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31314
|
-
className: "daw-list-meta",
|
|
31315
|
-
children: [
|
|
31316
|
-
new Date(Number(item.time ?? 0)).toLocaleString(locale),
|
|
31317
|
-
" · ",
|
|
31318
|
-
String(item.actorType ?? ""),
|
|
31319
|
-
":",
|
|
31320
|
-
String(item.actorId ?? ""),
|
|
31321
|
-
" · ",
|
|
31322
|
-
String(item.relativePath ?? "")
|
|
31323
|
-
]
|
|
31324
|
-
})] })
|
|
31325
|
-
}, String(item.id ?? index)))
|
|
31326
|
-
})
|
|
31327
|
-
}, "audit"),
|
|
31328
31919
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {
|
|
31329
31920
|
open: isOpen("app-download"),
|
|
31330
31921
|
onToggle: () => toggleCard("app-download")
|
|
@@ -31383,13 +31974,6 @@ window.__ModuleLoader__.load({
|
|
|
31383
31974
|
});
|
|
31384
31975
|
}
|
|
31385
31976
|
/**
|
|
31386
|
-
* Standalone settings overlay kept for the `/dsh-workspace` page (mobile WebView / direct link).
|
|
31387
|
-
*
|
|
31388
|
-
* The desktop WebUI does **not** use this: its settings live inline inside DSH's own settings page
|
|
31389
|
-
* (`PhoneSettingsBody`), so a user never opens a dialog from inside a dialog. This one keeps its own
|
|
31390
|
-
* shell because the standalone page has no settings container to render into.
|
|
31391
|
-
*/
|
|
31392
|
-
/**
|
|
31393
31977
|
* Shows a freshly minted `DSH1:` config text.
|
|
31394
31978
|
*
|
|
31395
31979
|
* The text is a credential carrier, so this panel keeps the "it is a key" reminder visible and lists
|
|
@@ -31520,6 +32104,8 @@ window.__ModuleLoader__.load({
|
|
|
31520
32104
|
const [scopes, setScopes] = (0, react.useState)(props.device.scopes);
|
|
31521
32105
|
const [rootIds, setRootIds] = (0, react.useState)(props.device.rootIds);
|
|
31522
32106
|
const toggle = (list, value) => list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
|
|
32107
|
+
const shortId = props.device.id.length > 14 ? `${props.device.id.slice(0, 8)}…${props.device.id.slice(-4)}` : props.device.id;
|
|
32108
|
+
const lastSeen = props.device.lastSeenAt === void 0 ? t("deviceNeverSeen") : t("deviceLastSeen", { time: new Date(props.device.lastSeenAt).toLocaleString(locale) });
|
|
31523
32109
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31524
32110
|
className: "daw-list-row",
|
|
31525
32111
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
@@ -31537,7 +32123,12 @@ window.__ModuleLoader__.load({
|
|
|
31537
32123
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31538
32124
|
className: "daw-list-meta",
|
|
31539
32125
|
children: [
|
|
31540
|
-
|
|
32126
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32127
|
+
title: props.device.id,
|
|
32128
|
+
children: shortId
|
|
32129
|
+
}),
|
|
32130
|
+
" · ",
|
|
32131
|
+
lastSeen,
|
|
31541
32132
|
" · ",
|
|
31542
32133
|
props.device.scopes.length === 0 ? t("noScopes") : props.device.scopes.map((scope) => scopeLabel(locale, scope)).join(" · ")
|
|
31543
32134
|
]
|
|
@@ -31567,7 +32158,10 @@ window.__ModuleLoader__.load({
|
|
|
31567
32158
|
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31568
32159
|
className: "daw-command primary",
|
|
31569
32160
|
onClick: () => {
|
|
31570
|
-
props.api.updateDevice(props.device.id, scopes, rootIds).then(props.onChanged).then(() =>
|
|
32161
|
+
props.api.updateDevice(props.device.id, scopes, rootIds).then(props.onChanged).then(() => {
|
|
32162
|
+
setEditing(false);
|
|
32163
|
+
props.onNotice("permissionsSaved");
|
|
32164
|
+
}).catch(props.onError);
|
|
31571
32165
|
},
|
|
31572
32166
|
children: t("save")
|
|
31573
32167
|
}),
|
|
@@ -31579,8 +32173,10 @@ window.__ModuleLoader__.load({
|
|
|
31579
32173
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31580
32174
|
className: "daw-icon danger",
|
|
31581
32175
|
title: t("revokeDevice"),
|
|
32176
|
+
"aria-label": t("revokeDevice"),
|
|
31582
32177
|
onClick: () => {
|
|
31583
|
-
|
|
32178
|
+
if (!window.confirm(t("revokeConfirm", { name: props.device.name }))) return;
|
|
32179
|
+
props.api.revokeDevice(props.device.id).then(props.onChanged).then(() => props.onNotice("deviceRevoked")).catch(props.onError);
|
|
31584
32180
|
},
|
|
31585
32181
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31586
32182
|
})
|