dsh-remote-bridge 2.5.0 → 2.5.2
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 -7
- package/README.md +20 -7
- package/docs/api/asyncapi.yaml +51 -2
- package/docs/api/openapi.yaml +187 -4
- package/lib/client.cjs +1113 -509
- package/lib/index.d.ts +462 -15
- package/lib/index.js +2431 -505
- package/package.json +1 -1
package/lib/client.cjs
CHANGED
|
@@ -172,9 +172,25 @@ window.__ModuleLoader__.load({
|
|
|
172
172
|
async tunnelActivity() {
|
|
173
173
|
return this.manageJson("/cloudflare/activity");
|
|
174
174
|
}
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
/**
|
|
176
|
+
* 创建(或复用)cloudflared 并启动 Quick Tunnel,返回对外地址。
|
|
177
|
+
*
|
|
178
|
+
* `withConfig=true`(一键「开隧道并出令牌」):请求体带 `configText:true`,服务端在
|
|
179
|
+
* 隧道**起来之前**的这一次调用里等地址并顺带生成令牌配置文本(见 state.configText)。
|
|
180
|
+
* 必须走这班车:隧道一旦运行,单独的 /manage/config/text 会被管理面判据 0 挡 403。
|
|
181
|
+
* endpointLabel/rootIds/deviceName 可选透传给服务端的配置生成。
|
|
182
|
+
*/
|
|
183
|
+
async tunnelCreate(withConfig = false, options = {}) {
|
|
184
|
+
if (withConfig !== true) return this.manageJson("/cloudflare/create", { method: "POST" });
|
|
185
|
+
return this.manageJson("/cloudflare/create", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
configText: true,
|
|
189
|
+
...options.endpointLabel === void 0 ? {} : { endpointLabel: options.endpointLabel },
|
|
190
|
+
...options.rootIds === void 0 ? {} : { rootIds: options.rootIds },
|
|
191
|
+
...options.deviceName === void 0 ? {} : { deviceName: options.deviceName }
|
|
192
|
+
})
|
|
193
|
+
});
|
|
178
194
|
}
|
|
179
195
|
/** 停止当前隧道。 */
|
|
180
196
|
async tunnelStop() {
|
|
@@ -236,335 +252,42 @@ window.__ModuleLoader__.load({
|
|
|
236
252
|
} catch {}
|
|
237
253
|
throw new WorkspaceApiError(response.status, code, message);
|
|
238
254
|
}
|
|
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
|
-
/**
|
|
281
|
-
* 从交接行里抠出图片地址。
|
|
282
|
-
*
|
|
283
|
-
* 允许「已存到 / 已保存到 / saved to」后面隔一段散文(历史文案里有「工作目录」这种词),
|
|
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
|
-
* 一张图。取不到的(文件已删、会话读不出、格式不符)自己消失,而不是在轮末留一个碎图标。
|
|
386
|
-
*/
|
|
387
|
-
function HandoffImage(props) {
|
|
388
|
-
const [failed, setFailed] = (0, react.useState)(false);
|
|
389
|
-
const [open, setOpen] = (0, react.useState)(false);
|
|
390
|
-
(0, react.useEffect)(() => {
|
|
391
|
-
if (!open) return void 0;
|
|
392
|
-
const onKey = (event) => {
|
|
393
|
-
if (event.key === "Escape") setOpen(false);
|
|
394
|
-
};
|
|
395
|
-
document.addEventListener("keydown", onKey);
|
|
396
|
-
return () => {
|
|
397
|
-
document.removeEventListener("keydown", onKey);
|
|
398
|
-
};
|
|
399
|
-
}, [open]);
|
|
400
|
-
if (failed) return null;
|
|
401
|
-
const url = imageUrl(props.item.path, props.sessionId);
|
|
402
|
-
const thumb = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
403
|
-
src: url,
|
|
404
|
-
alt: props.item.label,
|
|
405
|
-
title: props.item.path,
|
|
406
|
-
loading: "lazy",
|
|
407
|
-
decoding: "async",
|
|
408
|
-
referrerPolicy: "no-referrer",
|
|
409
|
-
style: THUMB_STYLE,
|
|
410
|
-
onError: () => setFailed(true),
|
|
411
|
-
onClick: () => setOpen(true)
|
|
412
|
-
});
|
|
413
|
-
const framed = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
414
|
-
className: "daw-handoff-item",
|
|
415
|
-
style: ITEM_STYLE,
|
|
416
|
-
children: [thumb, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
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
255
|
/**
|
|
469
|
-
*
|
|
256
|
+
* @license lucide-react v0.468.0 - ISC
|
|
470
257
|
*
|
|
471
|
-
*
|
|
258
|
+
* This source code is licensed under the ISC license.
|
|
259
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
472
260
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
className
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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]]);
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/client/i18n.tsx
|
|
263
|
+
var import_lucide_react = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
264
|
+
var react$1 = require("react");
|
|
265
|
+
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
266
|
+
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
267
|
+
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
|
|
268
|
+
}).join(" ").trim();
|
|
269
|
+
var defaultAttributes = {
|
|
270
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
271
|
+
width: 24,
|
|
272
|
+
height: 24,
|
|
273
|
+
viewBox: "0 0 24 24",
|
|
274
|
+
fill: "none",
|
|
275
|
+
stroke: "currentColor",
|
|
276
|
+
strokeWidth: 2,
|
|
277
|
+
strokeLinecap: "round",
|
|
278
|
+
strokeLinejoin: "round"
|
|
279
|
+
};
|
|
280
|
+
const Icon = react$1.forwardRef(({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
|
|
281
|
+
return react$1.createElement("svg", {
|
|
282
|
+
ref,
|
|
283
|
+
...defaultAttributes,
|
|
284
|
+
width: size,
|
|
285
|
+
height: size,
|
|
286
|
+
stroke: color,
|
|
287
|
+
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
|
|
288
|
+
className: mergeClasses("lucide", className),
|
|
289
|
+
...rest
|
|
290
|
+
}, [...iconNode.map(([tag, attrs]) => react$1.createElement(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
|
|
568
291
|
});
|
|
569
292
|
const createLucideIcon = (iconName, iconNode) => {
|
|
570
293
|
const Component = react$1.forwardRef(({ className, ...props }, ref) => react$1.createElement(Icon, {
|
|
@@ -6770,7 +6493,7 @@ window.__ModuleLoader__.load({
|
|
|
6770
6493
|
d: "m22 10-7.5 7.5L13 16",
|
|
6771
6494
|
key: "ke71qq"
|
|
6772
6495
|
}]]);
|
|
6773
|
-
createLucideIcon("Check", [["path", {
|
|
6496
|
+
const Check = createLucideIcon("Check", [["path", {
|
|
6774
6497
|
d: "M20 6 9 17l-5-5",
|
|
6775
6498
|
key: "1gmf2c"
|
|
6776
6499
|
}]]);
|
|
@@ -9169,7 +8892,7 @@ window.__ModuleLoader__.load({
|
|
|
9169
8892
|
key: "zix9uf"
|
|
9170
8893
|
}]
|
|
9171
8894
|
]);
|
|
9172
|
-
createLucideIcon("Copy", [["rect", {
|
|
8895
|
+
const Copy = createLucideIcon("Copy", [["rect", {
|
|
9173
8896
|
width: "14",
|
|
9174
8897
|
height: "14",
|
|
9175
8898
|
x: "8",
|
|
@@ -15939,7 +15662,7 @@ window.__ModuleLoader__.load({
|
|
|
15939
15662
|
key: "jkujk7"
|
|
15940
15663
|
}]
|
|
15941
15664
|
]);
|
|
15942
|
-
createLucideIcon("Languages", [
|
|
15665
|
+
const Languages = createLucideIcon("Languages", [
|
|
15943
15666
|
["path", {
|
|
15944
15667
|
d: "m5 8 6 6",
|
|
15945
15668
|
key: "1wu5hv"
|
|
@@ -23108,7 +22831,7 @@ window.__ModuleLoader__.load({
|
|
|
23108
22831
|
key: "1o3tu8"
|
|
23109
22832
|
}]
|
|
23110
22833
|
]);
|
|
23111
|
-
createLucideIcon("ScrollText", [
|
|
22834
|
+
const ScrollText = createLucideIcon("ScrollText", [
|
|
23112
22835
|
["path", {
|
|
23113
22836
|
d: "M15 12h-5",
|
|
23114
22837
|
key: "r7krc0"
|
|
@@ -30050,16 +29773,18 @@ window.__ModuleLoader__.load({
|
|
|
30050
29773
|
key: "durymu"
|
|
30051
29774
|
}]
|
|
30052
29775
|
]);
|
|
29776
|
+
exports.Check = Check;
|
|
29777
|
+
exports.Copy = Copy;
|
|
30053
29778
|
exports.Download = Download;
|
|
30054
29779
|
exports.File = File;
|
|
30055
29780
|
exports.HardDrive = HardDrive;
|
|
30056
29781
|
exports.Image = Image;
|
|
29782
|
+
exports.Languages = Languages;
|
|
29783
|
+
exports.ScrollText = ScrollText;
|
|
30057
29784
|
exports.Smartphone = Smartphone;
|
|
30058
29785
|
exports.Trash2 = Trash2;
|
|
30059
29786
|
exports.Upload = Upload;
|
|
30060
|
-
}));
|
|
30061
|
-
//#endregion
|
|
30062
|
-
//#region src/client/i18n.tsx
|
|
29787
|
+
})))();
|
|
30063
29788
|
const STORAGE_KEY = "dsh-workspace-locale";
|
|
30064
29789
|
const dictionaries = {
|
|
30065
29790
|
"zh-CN": {
|
|
@@ -30231,6 +29956,54 @@ window.__ModuleLoader__.load({
|
|
|
30231
29956
|
errorNotFound: "请求的文件或资源不存在。",
|
|
30232
29957
|
errorConflict: "目标已存在或当前状态发生冲突。",
|
|
30233
29958
|
errorRateLimited: "请求过于频繁,请稍后重试。",
|
|
29959
|
+
errorTunnelModeAdminDisabled: "云隧道正在运行,管理面已停用。请先在本机面板里停止隧道,或直接在本机 WebUI 操作。",
|
|
29960
|
+
errorAdminProxyForwarded: "管理面不接受经代理转发的请求。请在本机用 127.0.0.1 或 localhost 直接打开。",
|
|
29961
|
+
errorAdminHostNotLoopback: "管理面只在本机回环地址下可用。请用 127.0.0.1 或 localhost 打开本页。",
|
|
29962
|
+
errorAdminLoopbackRequired: "此操作只能在本机执行。",
|
|
29963
|
+
tunnelStageLocating: "正在定位本机 cloudflared…",
|
|
29964
|
+
tunnelStageBinaryFound: "已找到本机二进制,直接复用",
|
|
29965
|
+
tunnelStageDownloadGithub: "正在从 GitHub 下载 cloudflared…",
|
|
29966
|
+
tunnelStageDownloadDeb: "正在从官方 deb 仓库下载 cloudflared…",
|
|
29967
|
+
tunnelStageDownloadProgress: "正在下载 cloudflared… {mb}/{all} MB",
|
|
29968
|
+
tunnelStageDownloadUnverified: "镜像源没有可用的校验和,仍在下载但本次不做完整性校验…",
|
|
29969
|
+
tunnelStageUnpackDeb: "解包官方 deb 并提取二进制…",
|
|
29970
|
+
tunnelStageWriteBinary: "写入二进制并赋予执行权限…",
|
|
29971
|
+
tunnelStageStarting: "正在启动 cloudflared 隧道…",
|
|
29972
|
+
tunnelErrorAlreadyRunning: "隧道已经在运行中,请先停止。",
|
|
29973
|
+
tunnelErrorDownload: "下载 cloudflared 失败,请检查网络后重试;如需换源可设置 CLOUDFLARED_MIRROR。",
|
|
29974
|
+
tunnelErrorExited: "cloudflared 已退出(code={code},signal={signal}),未拿到隧道地址。",
|
|
29975
|
+
tunnelStopConfirm: "停止云隧道后,公网临时地址立即失效,正在经隧道连接的手机会断开。确定停止吗?",
|
|
29976
|
+
tunnelStartAndGenConfig: "开启隧道并生成配置",
|
|
29977
|
+
tunnelRestartAndGenConfig: "停止后重开隧道并生成配置",
|
|
29978
|
+
managementLockedWhileTunnel: "云隧道正在运行,管理面已安全锁定。除停止隧道外的配置暂时不可改,停止隧道后自动恢复。",
|
|
29979
|
+
tunnelConfigAuto: "隧道已开启,配置文本已自动生成",
|
|
29980
|
+
tunnelUrlPending: "隧道已启动,但暂时还没拿到公网地址,稍等片刻后点「用隧道地址生成配置」即可。",
|
|
29981
|
+
copyUrl: "复制地址",
|
|
29982
|
+
pairingExpiresAt: "配对码有效期约 {minutes} 分钟,请尽快在手机上输入。",
|
|
29983
|
+
copyPairing: "复制配对码",
|
|
29984
|
+
pairingCreated: "配对码已生成",
|
|
29985
|
+
configCreated: "配置文本已生成",
|
|
29986
|
+
revokeConfirm: "吊销后这台手机的令牌立即失效,必须重新配对才能访问。确定吊销「{name}」吗?",
|
|
29987
|
+
removeRootConfirm: "移除根目录「{label}」不会删除磁盘上的文件,但所有手机将立刻失去对它的访问权。确定移除吗?",
|
|
29988
|
+
disableRemoteConfirm: "关闭远程访问后,所有手机连接立即断开,需要重新启用并配对。确定关闭吗?",
|
|
29989
|
+
rootAdded: "根目录已添加",
|
|
29990
|
+
rootRemoved: "根目录已移除",
|
|
29991
|
+
remoteDisabled: "远程访问已关闭",
|
|
29992
|
+
permissionsSaved: "权限已保存",
|
|
29993
|
+
deviceRevoked: "设备已吊销",
|
|
29994
|
+
listenerSaved: "监听设置已保存",
|
|
29995
|
+
trashRestored: "已恢复",
|
|
29996
|
+
trashPurged: "已永久删除",
|
|
29997
|
+
tunnelStarted: "云隧道已开启",
|
|
29998
|
+
auditEmpty: "暂无审计事件",
|
|
29999
|
+
auditLoadMore: "加载更多",
|
|
30000
|
+
auditLoading: "正在加载…",
|
|
30001
|
+
trashEmpty: "回收站是空的",
|
|
30002
|
+
deviceLastSeen: "最后使用 {time}",
|
|
30003
|
+
deviceNeverSeen: "从未连接",
|
|
30004
|
+
imageLoadFailed: "图片加载失败",
|
|
30005
|
+
prevImage: "上一张",
|
|
30006
|
+
nextImage: "下一张",
|
|
30234
30007
|
author: "作者 Github@Hakunm",
|
|
30235
30008
|
version: "当前版本"
|
|
30236
30009
|
},
|
|
@@ -30317,7 +30090,7 @@ window.__ModuleLoader__.load({
|
|
|
30317
30090
|
removeRoot: "Remove root",
|
|
30318
30091
|
builtinRootBadge: "Default",
|
|
30319
30092
|
builtinRootHint: "Default directory · cannot be removed",
|
|
30320
|
-
builtinRootNotice: "New roots are not granted to already paired phones by default. To share one, open
|
|
30093
|
+
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.",
|
|
30321
30094
|
errorRootBuiltin: "This is the current DSH working directory, so it cannot be removed. You can still add other roots above.",
|
|
30322
30095
|
scopeChatRead: "Read chat",
|
|
30323
30096
|
scopeChatWrite: "Send chat",
|
|
@@ -30403,6 +30176,54 @@ window.__ModuleLoader__.load({
|
|
|
30403
30176
|
errorNotFound: "The requested file or resource was not found.",
|
|
30404
30177
|
errorConflict: "The target already exists or its state has changed.",
|
|
30405
30178
|
errorRateLimited: "Too many requests. Try again shortly.",
|
|
30179
|
+
errorTunnelModeAdminDisabled: "The Cloudflare tunnel is running, so management endpoints are disabled. Stop the tunnel in the local panel, or use the local WebUI.",
|
|
30180
|
+
errorAdminProxyForwarded: "Management endpoints do not accept proxied requests. Open them locally via 127.0.0.1 or localhost.",
|
|
30181
|
+
errorAdminHostNotLoopback: "Management endpoints are available only under a loopback address. Open this page via 127.0.0.1 or localhost.",
|
|
30182
|
+
errorAdminLoopbackRequired: "This operation is available only from the local machine.",
|
|
30183
|
+
tunnelStageLocating: "Locating the local cloudflared binary…",
|
|
30184
|
+
tunnelStageBinaryFound: "Local binary found, reusing it",
|
|
30185
|
+
tunnelStageDownloadGithub: "Downloading cloudflared from GitHub…",
|
|
30186
|
+
tunnelStageDownloadDeb: "Downloading cloudflared from the official deb repo…",
|
|
30187
|
+
tunnelStageDownloadProgress: "Downloading cloudflared… {mb}/{all} MB",
|
|
30188
|
+
tunnelStageDownloadUnverified: "No checksum is published for this mirror; downloading without an integrity check…",
|
|
30189
|
+
tunnelStageUnpackDeb: "Unpacking the deb and extracting the binary…",
|
|
30190
|
+
tunnelStageWriteBinary: "Writing the binary and marking it executable…",
|
|
30191
|
+
tunnelStageStarting: "Starting the cloudflared tunnel…",
|
|
30192
|
+
tunnelErrorAlreadyRunning: "The tunnel is already running. Stop it first.",
|
|
30193
|
+
tunnelErrorDownload: "Downloading cloudflared failed. Check the network and retry; set CLOUDFLARED_MIRROR to use another source.",
|
|
30194
|
+
tunnelErrorExited: "cloudflared exited (code={code}, signal={signal}) before a tunnel address was obtained.",
|
|
30195
|
+
tunnelStopConfirm: "Stopping the tunnel invalidates the public address immediately and disconnects phones currently connected through it. Stop anyway?",
|
|
30196
|
+
tunnelStartAndGenConfig: "Start tunnel & generate config",
|
|
30197
|
+
tunnelRestartAndGenConfig: "Restart tunnel & generate config",
|
|
30198
|
+
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.",
|
|
30199
|
+
tunnelConfigAuto: "Tunnel started; config text generated automatically",
|
|
30200
|
+
tunnelUrlPending: "The tunnel is starting but its public address is not ready yet. Wait a moment, then press “Generate config with tunnel address”.",
|
|
30201
|
+
copyUrl: "Copy address",
|
|
30202
|
+
pairingExpiresAt: "The pairing code is valid for about {minutes} minutes — enter it on the phone soon.",
|
|
30203
|
+
copyPairing: "Copy code",
|
|
30204
|
+
pairingCreated: "Pairing code generated",
|
|
30205
|
+
configCreated: "Config text generated",
|
|
30206
|
+
revokeConfirm: "Revoking invalidates this phone’s token at once; it must pair again to regain access. Revoke “{name}”?",
|
|
30207
|
+
removeRootConfirm: "Removing “{label}” does not delete files on disk, but every phone immediately loses access to it. Remove it?",
|
|
30208
|
+
disableRemoteConfirm: "Disabling remote access drops every phone connection right away; it must be re-enabled and re-paired. Disable it?",
|
|
30209
|
+
rootAdded: "Root added",
|
|
30210
|
+
rootRemoved: "Root removed",
|
|
30211
|
+
remoteDisabled: "Remote access disabled",
|
|
30212
|
+
permissionsSaved: "Permissions saved",
|
|
30213
|
+
deviceRevoked: "Device revoked",
|
|
30214
|
+
listenerSaved: "Listener settings saved",
|
|
30215
|
+
trashRestored: "Restored",
|
|
30216
|
+
trashPurged: "Permanently deleted",
|
|
30217
|
+
tunnelStarted: "Tunnel started",
|
|
30218
|
+
auditEmpty: "No audit events yet",
|
|
30219
|
+
auditLoadMore: "Load more",
|
|
30220
|
+
auditLoading: "Loading…",
|
|
30221
|
+
trashEmpty: "The recycle bin is empty",
|
|
30222
|
+
deviceLastSeen: "Last seen {time}",
|
|
30223
|
+
deviceNeverSeen: "Never connected",
|
|
30224
|
+
imageLoadFailed: "Image failed to load",
|
|
30225
|
+
prevImage: "Previous image",
|
|
30226
|
+
nextImage: "Next image",
|
|
30406
30227
|
author: "Author Github@Hakunm",
|
|
30407
30228
|
version: "Current version"
|
|
30408
30229
|
}
|
|
@@ -30412,76 +30233,553 @@ window.__ModuleLoader__.load({
|
|
|
30412
30233
|
function getWorkspaceLocale() {
|
|
30413
30234
|
return currentLocale;
|
|
30414
30235
|
}
|
|
30415
|
-
function setWorkspaceLocale(locale) {
|
|
30416
|
-
if (locale === currentLocale) return;
|
|
30417
|
-
currentLocale = locale;
|
|
30236
|
+
function setWorkspaceLocale(locale) {
|
|
30237
|
+
if (locale === currentLocale) return;
|
|
30238
|
+
currentLocale = locale;
|
|
30239
|
+
try {
|
|
30240
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, locale);
|
|
30241
|
+
} catch {}
|
|
30242
|
+
for (const listener of listeners) listener();
|
|
30243
|
+
}
|
|
30244
|
+
function subscribeWorkspaceLocale(listener) {
|
|
30245
|
+
listeners.add(listener);
|
|
30246
|
+
return () => listeners.delete(listener);
|
|
30247
|
+
}
|
|
30248
|
+
/**
|
|
30249
|
+
* 取一条文案并填占位符。
|
|
30250
|
+
*
|
|
30251
|
+
* **缺键不抛异常**。原来这里是 `dictionaries[locale][key].replace(...)`:任何一个不存在的键
|
|
30252
|
+
* 都会抛 TypeError,而它在 React 渲染路径上 —— 结果不是那一行显示不出原文,而是**整个设置
|
|
30253
|
+
* 面板白屏**。缺一个描述行就把七张卡里的全部设置都搭进去,代价和收益完全不成比例。
|
|
30254
|
+
*
|
|
30255
|
+
* 降级成「回退到另一种语言 → 再回退成键名本身」:最坏情况是某一行露出 `rootsTabDesc`
|
|
30256
|
+
* 这样的字面量,一眼能看出该补哪条,而不是面对一个空白面板。
|
|
30257
|
+
*/
|
|
30258
|
+
function translate(locale, key, values = {}) {
|
|
30259
|
+
const fill = (template) => template?.replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
|
|
30260
|
+
const primary = fill(dictionaries[locale][key]);
|
|
30261
|
+
if (primary !== void 0) return primary;
|
|
30262
|
+
const text = fill(dictionaries["zh-CN"][key]) ?? key;
|
|
30263
|
+
console.warn(`[dsh-remote-bridge] 缺少文案键 ${String(key)}(locale=${locale}),已降级显示键名`);
|
|
30264
|
+
return text;
|
|
30265
|
+
}
|
|
30266
|
+
/**
|
|
30267
|
+
* 权限码 → 人话。
|
|
30268
|
+
*
|
|
30269
|
+
* 用户要求「中文状态下显示中文,英文显示英文」。文案刻意**用人话而不是协议码**:
|
|
30270
|
+
* `读文件` / `写文件` / `删文件` 自己就看得懂,而 `files.read` 要对照文档才知道指什么 ——
|
|
30271
|
+
* 出问题时用户把屏幕上的字念出来,比报一串码有用得多。
|
|
30272
|
+
*
|
|
30273
|
+
* 键名 `scope${码}`(`chat.read` → `scopeChatRead`):与既有 `rootsTab` 一致的拼法。
|
|
30274
|
+
* 用**显式查表**而不是模板拼 —— 拼错会查不到键,而键查不到时 `translate` 会静默降级成
|
|
30275
|
+
* 露出键名(见上面),界面上会突然冒出一行 `scopeChatread`。白屏那次就是这么来的。
|
|
30276
|
+
*/
|
|
30277
|
+
const SCOPE_LABEL_KEYS = {
|
|
30278
|
+
"chat.read": "scopeChatRead",
|
|
30279
|
+
"chat.write": "scopeChatWrite",
|
|
30280
|
+
"files.read": "scopeFilesRead",
|
|
30281
|
+
"files.write": "scopeFilesWrite",
|
|
30282
|
+
"files.delete": "scopeFilesDelete",
|
|
30283
|
+
"settings.read": "scopeSettingsRead",
|
|
30284
|
+
"settings.write": "scopeSettingsWrite"
|
|
30285
|
+
};
|
|
30286
|
+
function scopeLabel(locale, scope) {
|
|
30287
|
+
const key = SCOPE_LABEL_KEYS[scope];
|
|
30288
|
+
if (key === void 0) return scope;
|
|
30289
|
+
return translate(locale, key);
|
|
30290
|
+
}
|
|
30291
|
+
function useWorkspaceI18n() {
|
|
30292
|
+
const locale = (0, react.useSyncExternalStore)(subscribeWorkspaceLocale, getWorkspaceLocale, () => "zh-CN");
|
|
30293
|
+
return {
|
|
30294
|
+
locale,
|
|
30295
|
+
t: (0, react.useCallback)((key, values) => translate(locale, key, values), [locale]),
|
|
30296
|
+
toggle: (0, react.useCallback)(() => setWorkspaceLocale(locale === "zh-CN" ? "en" : "zh-CN"), [locale])
|
|
30297
|
+
};
|
|
30298
|
+
}
|
|
30299
|
+
function LanguageToggle() {
|
|
30300
|
+
const { t, toggle } = useWorkspaceI18n();
|
|
30301
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30302
|
+
className: "daw-icon",
|
|
30303
|
+
type: "button",
|
|
30304
|
+
title: t("switchLanguage"),
|
|
30305
|
+
"aria-label": t("switchLanguage"),
|
|
30306
|
+
onClick: toggle,
|
|
30307
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Languages, { size: 16 })
|
|
30308
|
+
});
|
|
30309
|
+
}
|
|
30310
|
+
function readStoredLocale() {
|
|
30311
|
+
try {
|
|
30312
|
+
return globalThis.localStorage?.getItem(STORAGE_KEY) === "en" ? "en" : "zh-CN";
|
|
30313
|
+
} catch {
|
|
30314
|
+
return "zh-CN";
|
|
30315
|
+
}
|
|
30316
|
+
}
|
|
30317
|
+
//#endregion
|
|
30318
|
+
//#region src/client/handoff.tsx
|
|
30319
|
+
/**
|
|
30320
|
+
* 浏览器半边:把**图片交接消息**里的那个地址渲染成图片本身。
|
|
30321
|
+
*
|
|
30322
|
+
* 这半边是从 `dsh-image-paths` 0.3.1 并过来的,接的是同一个契约:纯文本模型收到图片时,
|
|
30323
|
+
* remote-bridge 会在用户消息里追加两行 ——
|
|
30324
|
+
*
|
|
30325
|
+
* 🖼️ 图片 1「shot.png」(当前模型看不到图,已存到 .dsh-images/<sha>.png;要看图,请用识图工具打开下面那行的路径)
|
|
30326
|
+
* [Image: source: /absolute/path/.dsh-images/<sha>.png]
|
|
30327
|
+
*
|
|
30328
|
+
* 本模块只读第一行里那个地址,把它变成一张真的图。第二行是给模型读的机器契约,同一张图
|
|
30329
|
+
* 不再重复出第二张;而且只扫**用户消息**,上下文注入、助手正文、工具输出里引用的路径
|
|
30330
|
+
* 一律不碰 —— 否则对话里到处都是 `.dsh-images/…` 的例子,每个都会被渲染成图。
|
|
30331
|
+
*
|
|
30332
|
+
* 取图走宿主那条只读路由 `/plugin/dsh-remote-bridge/raw?sessionId=…&name=…`:
|
|
30333
|
+
* 请求里**只有会话与裸文件名**,目录由宿主按会话工作目录推出,客户端连「读哪个目录」
|
|
30334
|
+
* 都递不进去。这条路是给 `<img>` 用的 —— 浏览器不会给图片请求加 Authorization 头,
|
|
30335
|
+
* 所以它独立于版本化 REST,按回环 + 同源 + 文件名白名单 + 字节自证守门。
|
|
30336
|
+
*
|
|
30337
|
+
* 座位选的是 Turn 尾部(`conversation.chat.turnTail`):Chat 节点座位会把这个 kind 收进
|
|
30338
|
+
* 「过程分组」,standard 模式下折叠起来 —— DOM 里有、肉眼看不着。Turn 尾在独立成行的
|
|
30339
|
+
* 白名单内,任何工作详情模式下都看得见。
|
|
30340
|
+
*/
|
|
30341
|
+
/** 本插件在 Chat store 里占的节点 kind。 */
|
|
30342
|
+
const KIND = "dsh-remote-bridge-handoff-gallery";
|
|
30343
|
+
/** 宿主的只读图片路由。 */
|
|
30344
|
+
const ROUTE = "/plugin/dsh-remote-bridge/raw";
|
|
30345
|
+
/** 每轮最多显示几张。 */
|
|
30346
|
+
const MAX_IMAGES = 8;
|
|
30347
|
+
/** 长过这个的路径不值得去解析。 */
|
|
30348
|
+
const MAX_PATH_CHARS = 4096;
|
|
30349
|
+
/** 图片扩展名:与宿主交接写盘的白名单一致。 */
|
|
30350
|
+
const IMAGE_EXTENSION = /\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif)$/i;
|
|
30351
|
+
/**
|
|
30352
|
+
* 交接行的行首标记:中英两种文案都以它开头。
|
|
30353
|
+
*
|
|
30354
|
+
* 判据选「行首标记」而不是「句子里有 已存到」——后者会把用户自己写的句子也算进来,
|
|
30355
|
+
* 而 dsh-companion 摘除这一行用的也是同一个标记,两边口径一致。
|
|
30356
|
+
*/
|
|
30357
|
+
const HANDOFF_LINE = /^🖼️\s*(?:图片|Image)\s+\d+/i;
|
|
30358
|
+
/**
|
|
30359
|
+
* 从交接行里抠出图片地址。
|
|
30360
|
+
*
|
|
30361
|
+
* 允许「已存到 / 已保存到 / saved to」后面隔一段散文(历史文案里有「工作目录」这种词),
|
|
30362
|
+
* 所以是「点心号 + 非贪婪」而不是紧邻匹配。取出的是消息里**原样**的那条路径,
|
|
30363
|
+
* 相对(`.dsh-images/x.png`)与绝对(兜底目录那版)都认。
|
|
30364
|
+
*/
|
|
30365
|
+
const HANDOFF_PATH = /(?:已(?:保存|存)到|saved to)[^;;\n]*?([A-Za-z0-9_./@~+-][^\s`"'()[\]{}<>|,;:?,。;:、!?]*?\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif))/i;
|
|
30366
|
+
/** 路径里的文件名(路由只收裸文件名)。 */
|
|
30367
|
+
function basenameOf(path) {
|
|
30368
|
+
const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
30369
|
+
return cut === -1 ? path : path.slice(cut + 1);
|
|
30370
|
+
}
|
|
30371
|
+
/**
|
|
30372
|
+
* 读出一段文本里的交接图片。
|
|
30373
|
+
* @param text - 消息的文本内容(已按块拼接)。
|
|
30374
|
+
* @returns 单个 `{ path, label }`,不是交接消息就是 null。
|
|
30375
|
+
*/
|
|
30376
|
+
function handoffMention(text) {
|
|
30377
|
+
if (typeof text !== "string" || text === "") return null;
|
|
30378
|
+
for (const line of text.split("\n")) {
|
|
30379
|
+
if (!HANDOFF_LINE.test(line.trim())) continue;
|
|
30380
|
+
const match = HANDOFF_PATH.exec(line);
|
|
30381
|
+
if (match === null) continue;
|
|
30382
|
+
const path = match[1] ?? "";
|
|
30383
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS) continue;
|
|
30384
|
+
if (!IMAGE_EXTENSION.test(path)) continue;
|
|
30385
|
+
return {
|
|
30386
|
+
path,
|
|
30387
|
+
label: basenameOf(path)
|
|
30388
|
+
};
|
|
30389
|
+
}
|
|
30390
|
+
return null;
|
|
30391
|
+
}
|
|
30392
|
+
/** 把一条消息的内容块拼成文本。 */
|
|
30393
|
+
function contentText(content) {
|
|
30394
|
+
if (!Array.isArray(content)) return "";
|
|
30395
|
+
const parts = [];
|
|
30396
|
+
for (const block of content) {
|
|
30397
|
+
if (block === null || typeof block !== "object") continue;
|
|
30398
|
+
const candidate = block;
|
|
30399
|
+
if (candidate.type === "text" && typeof candidate.text === "string") parts.push(candidate.text);
|
|
30400
|
+
}
|
|
30401
|
+
return parts.join("\n");
|
|
30402
|
+
}
|
|
30403
|
+
/** 一条消息交接出去的那张图,没有就是空数组。 */
|
|
30404
|
+
function itemsOfMessage(payload) {
|
|
30405
|
+
if (payload === null || typeof payload !== "object") return [];
|
|
30406
|
+
const item = handoffMention(contentText(payload.content));
|
|
30407
|
+
return item === null ? [] : [item];
|
|
30408
|
+
}
|
|
30409
|
+
const WRAP_STYLE = {
|
|
30410
|
+
display: "flex",
|
|
30411
|
+
flexWrap: "wrap",
|
|
30412
|
+
alignItems: "flex-start",
|
|
30413
|
+
gap: 8,
|
|
30414
|
+
margin: "8px 0 4px"
|
|
30415
|
+
};
|
|
30416
|
+
const ITEM_STYLE = {
|
|
30417
|
+
display: "flex",
|
|
30418
|
+
flexDirection: "column",
|
|
30419
|
+
gap: 2,
|
|
30420
|
+
maxWidth: "100%"
|
|
30421
|
+
};
|
|
30422
|
+
const THUMB_STYLE = {
|
|
30423
|
+
display: "block",
|
|
30424
|
+
maxHeight: 180,
|
|
30425
|
+
maxWidth: "100%",
|
|
30426
|
+
borderRadius: 8,
|
|
30427
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
30428
|
+
background: "rgba(127,127,127,0.06)",
|
|
30429
|
+
cursor: "zoom-in"
|
|
30430
|
+
};
|
|
30431
|
+
const CAPTION_STYLE = {
|
|
30432
|
+
display: "block",
|
|
30433
|
+
maxWidth: 180,
|
|
30434
|
+
fontFamily: "inherit",
|
|
30435
|
+
fontSize: 11,
|
|
30436
|
+
lineHeight: 1.4,
|
|
30437
|
+
color: "rgba(127,127,127,0.95)",
|
|
30438
|
+
overflow: "hidden",
|
|
30439
|
+
textOverflow: "ellipsis",
|
|
30440
|
+
whiteSpace: "nowrap"
|
|
30441
|
+
};
|
|
30442
|
+
const OVERLAY_STYLE = {
|
|
30443
|
+
position: "fixed",
|
|
30444
|
+
inset: 0,
|
|
30445
|
+
zIndex: 1e3,
|
|
30446
|
+
display: "flex",
|
|
30447
|
+
alignItems: "center",
|
|
30448
|
+
justifyContent: "center",
|
|
30449
|
+
background: "rgba(0,0,0,0.72)",
|
|
30450
|
+
cursor: "zoom-out"
|
|
30451
|
+
};
|
|
30452
|
+
const FULL_STYLE = {
|
|
30453
|
+
maxWidth: "92vw",
|
|
30454
|
+
maxHeight: "92vh",
|
|
30455
|
+
borderRadius: 6,
|
|
30456
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)"
|
|
30457
|
+
};
|
|
30458
|
+
/** 加载失败的缩略图占位(不再像旧版那样整张消失,只留一个问号让人瞎猜)。 */
|
|
30459
|
+
const THUMB_BROKEN_STYLE = {
|
|
30460
|
+
...THUMB_STYLE,
|
|
30461
|
+
display: "flex",
|
|
30462
|
+
alignItems: "center",
|
|
30463
|
+
justifyContent: "center",
|
|
30464
|
+
width: 160,
|
|
30465
|
+
height: 120,
|
|
30466
|
+
cursor: "default",
|
|
30467
|
+
fontSize: 12
|
|
30468
|
+
};
|
|
30469
|
+
const LIGHTBOX_NAV_STYLE = {
|
|
30470
|
+
position: "absolute",
|
|
30471
|
+
top: "50%",
|
|
30472
|
+
transform: "translateY(-50%)",
|
|
30473
|
+
zIndex: 1001,
|
|
30474
|
+
width: 44,
|
|
30475
|
+
height: 44,
|
|
30476
|
+
borderRadius: "50%",
|
|
30477
|
+
border: "none",
|
|
30478
|
+
background: "rgba(255,255,255,0.14)",
|
|
30479
|
+
color: "#fff",
|
|
30480
|
+
fontSize: 22,
|
|
30481
|
+
lineHeight: "44px",
|
|
30482
|
+
textAlign: "center",
|
|
30483
|
+
cursor: "pointer",
|
|
30484
|
+
padding: 0
|
|
30485
|
+
};
|
|
30486
|
+
const LIGHTBOX_CLOSE_STYLE = {
|
|
30487
|
+
position: "absolute",
|
|
30488
|
+
top: 16,
|
|
30489
|
+
right: 16,
|
|
30490
|
+
zIndex: 1001,
|
|
30491
|
+
width: 40,
|
|
30492
|
+
height: 40,
|
|
30493
|
+
borderRadius: "50%",
|
|
30494
|
+
border: "none",
|
|
30495
|
+
background: "rgba(255,255,255,0.14)",
|
|
30496
|
+
color: "#fff",
|
|
30497
|
+
fontSize: 20,
|
|
30498
|
+
lineHeight: "40px",
|
|
30499
|
+
textAlign: "center",
|
|
30500
|
+
cursor: "pointer",
|
|
30501
|
+
padding: 0
|
|
30502
|
+
};
|
|
30503
|
+
const LIGHTBOX_FULL_BROKEN_STYLE = {
|
|
30504
|
+
...FULL_STYLE,
|
|
30505
|
+
display: "flex",
|
|
30506
|
+
alignItems: "center",
|
|
30507
|
+
justifyContent: "center",
|
|
30508
|
+
width: "min(72vw, 560px)",
|
|
30509
|
+
height: "min(60vh, 360px)",
|
|
30510
|
+
background: "rgba(127,127,127,0.18)",
|
|
30511
|
+
color: "rgba(255,255,255,0.92)",
|
|
30512
|
+
fontSize: 14
|
|
30513
|
+
};
|
|
30514
|
+
/** 某条地址在这条路由上的 URL。 */
|
|
30515
|
+
function imageUrl(path, sessionId) {
|
|
30516
|
+
return `${ROUTE}?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(basenameOf(path))}`;
|
|
30517
|
+
}
|
|
30518
|
+
/**
|
|
30519
|
+
* 缩略图。取不到的(文件已删、会话读不出、格式不符)显示一块「加载失败」占位,
|
|
30520
|
+
* 而不是像旧版那样整张消失——消失了用户只会以为模型没存图,根本不知道该重试。
|
|
30521
|
+
*/
|
|
30522
|
+
function HandoffThumb(props) {
|
|
30523
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
30524
|
+
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30525
|
+
className: "daw-handoff-item",
|
|
30526
|
+
style: ITEM_STYLE,
|
|
30527
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30528
|
+
style: THUMB_BROKEN_STYLE,
|
|
30529
|
+
title: props.item.path,
|
|
30530
|
+
children: props.failedText
|
|
30531
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30532
|
+
className: "daw-handoff-caption",
|
|
30533
|
+
title: props.item.path,
|
|
30534
|
+
style: CAPTION_STYLE,
|
|
30535
|
+
children: props.item.path
|
|
30536
|
+
})]
|
|
30537
|
+
});
|
|
30538
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30539
|
+
className: "daw-handoff-item",
|
|
30540
|
+
style: ITEM_STYLE,
|
|
30541
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
30542
|
+
src: imageUrl(props.item.path, props.sessionId),
|
|
30543
|
+
alt: props.item.label,
|
|
30544
|
+
title: props.item.path,
|
|
30545
|
+
loading: "lazy",
|
|
30546
|
+
decoding: "async",
|
|
30547
|
+
referrerPolicy: "no-referrer",
|
|
30548
|
+
style: THUMB_STYLE,
|
|
30549
|
+
onError: () => setFailed(true),
|
|
30550
|
+
onClick: props.onOpen
|
|
30551
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30552
|
+
className: "daw-handoff-caption",
|
|
30553
|
+
title: props.item.path,
|
|
30554
|
+
style: CAPTION_STYLE,
|
|
30555
|
+
children: props.item.path
|
|
30556
|
+
})]
|
|
30557
|
+
});
|
|
30558
|
+
}
|
|
30559
|
+
/** 灯箱里的大图;换图时靠 key 重挂载来重置 failed。 */
|
|
30560
|
+
function LightboxImage(props) {
|
|
30561
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
30562
|
+
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30563
|
+
style: LIGHTBOX_FULL_BROKEN_STYLE,
|
|
30564
|
+
children: props.failedText
|
|
30565
|
+
});
|
|
30566
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
30567
|
+
src: imageUrl(props.item.path, props.sessionId),
|
|
30568
|
+
alt: props.item.label,
|
|
30569
|
+
decoding: "async",
|
|
30570
|
+
referrerPolicy: "no-referrer",
|
|
30571
|
+
style: FULL_STYLE,
|
|
30572
|
+
onError: () => setFailed(true)
|
|
30573
|
+
});
|
|
30574
|
+
}
|
|
30575
|
+
/**
|
|
30576
|
+
* 图集灯箱:点遮罩空白处关闭,但**点图片本身不再误关**(旧版 img 的点击冒泡到 overlay)。
|
|
30577
|
+
* 多图时左右切换,Esc 关闭。
|
|
30578
|
+
*/
|
|
30579
|
+
function HandoffLightbox(props) {
|
|
30580
|
+
const { items, index, onClose, onPrev, onNext } = props;
|
|
30581
|
+
const multiple = items.length > 1;
|
|
30582
|
+
(0, react.useEffect)(() => {
|
|
30583
|
+
const onKey = (event) => {
|
|
30584
|
+
if (event.key === "Escape") onClose();
|
|
30585
|
+
else if (multiple && event.key === "ArrowLeft") onPrev();
|
|
30586
|
+
else if (multiple && event.key === "ArrowRight") onNext();
|
|
30587
|
+
};
|
|
30588
|
+
document.addEventListener("keydown", onKey);
|
|
30589
|
+
return () => {
|
|
30590
|
+
document.removeEventListener("keydown", onKey);
|
|
30591
|
+
};
|
|
30592
|
+
}, [
|
|
30593
|
+
multiple,
|
|
30594
|
+
onClose,
|
|
30595
|
+
onPrev,
|
|
30596
|
+
onNext
|
|
30597
|
+
]);
|
|
30598
|
+
const item = items[index];
|
|
30599
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30600
|
+
style: {
|
|
30601
|
+
...OVERLAY_STYLE,
|
|
30602
|
+
cursor: "zoom-out"
|
|
30603
|
+
},
|
|
30604
|
+
role: "dialog",
|
|
30605
|
+
"aria-modal": "true",
|
|
30606
|
+
"aria-label": item.path,
|
|
30607
|
+
onClick: onClose,
|
|
30608
|
+
children: [
|
|
30609
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30610
|
+
type: "button",
|
|
30611
|
+
style: LIGHTBOX_CLOSE_STYLE,
|
|
30612
|
+
"aria-label": "×",
|
|
30613
|
+
onClick: onClose,
|
|
30614
|
+
children: "×"
|
|
30615
|
+
}),
|
|
30616
|
+
multiple && index > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30617
|
+
type: "button",
|
|
30618
|
+
style: {
|
|
30619
|
+
...LIGHTBOX_NAV_STYLE,
|
|
30620
|
+
left: 24
|
|
30621
|
+
},
|
|
30622
|
+
"aria-label": props.prevLabel,
|
|
30623
|
+
title: props.prevLabel,
|
|
30624
|
+
onClick: (e) => {
|
|
30625
|
+
e.stopPropagation();
|
|
30626
|
+
onPrev();
|
|
30627
|
+
},
|
|
30628
|
+
children: "‹"
|
|
30629
|
+
}) : null,
|
|
30630
|
+
multiple && index < items.length - 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30631
|
+
type: "button",
|
|
30632
|
+
style: {
|
|
30633
|
+
...LIGHTBOX_NAV_STYLE,
|
|
30634
|
+
right: 24
|
|
30635
|
+
},
|
|
30636
|
+
"aria-label": props.nextLabel,
|
|
30637
|
+
title: props.nextLabel,
|
|
30638
|
+
onClick: (e) => {
|
|
30639
|
+
e.stopPropagation();
|
|
30640
|
+
onNext();
|
|
30641
|
+
},
|
|
30642
|
+
children: "›"
|
|
30643
|
+
}) : null,
|
|
30644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30645
|
+
style: {
|
|
30646
|
+
position: "relative",
|
|
30647
|
+
cursor: "default"
|
|
30648
|
+
},
|
|
30649
|
+
onClick: (e) => e.stopPropagation(),
|
|
30650
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LightboxImage, {
|
|
30651
|
+
item,
|
|
30652
|
+
sessionId: props.sessionId,
|
|
30653
|
+
failedText: props.failedText
|
|
30654
|
+
}, index), multiple ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30655
|
+
style: {
|
|
30656
|
+
position: "absolute",
|
|
30657
|
+
bottom: -28,
|
|
30658
|
+
left: 0,
|
|
30659
|
+
right: 0,
|
|
30660
|
+
textAlign: "center",
|
|
30661
|
+
color: "rgba(255,255,255,0.85)",
|
|
30662
|
+
fontSize: 12
|
|
30663
|
+
},
|
|
30664
|
+
children: [
|
|
30665
|
+
index + 1,
|
|
30666
|
+
" / ",
|
|
30667
|
+
items.length
|
|
30668
|
+
]
|
|
30669
|
+
}) : null]
|
|
30670
|
+
})
|
|
30671
|
+
]
|
|
30672
|
+
});
|
|
30673
|
+
}
|
|
30674
|
+
/** 把本轮各节点里交接出去的图收成一串(按出现顺序去重)。 */
|
|
30675
|
+
function collectTurnItems(nodes, locations, turn) {
|
|
30676
|
+
if (nodes === void 0 || locations === void 0 || turn === void 0 || turn === null) return [];
|
|
30677
|
+
let keys;
|
|
30418
30678
|
try {
|
|
30419
|
-
|
|
30420
|
-
} catch {
|
|
30421
|
-
|
|
30422
|
-
|
|
30423
|
-
|
|
30424
|
-
|
|
30425
|
-
|
|
30679
|
+
keys = locations.getTurn(turn.turn);
|
|
30680
|
+
} catch {
|
|
30681
|
+
return [];
|
|
30682
|
+
}
|
|
30683
|
+
if (!Array.isArray(keys)) return [];
|
|
30684
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30685
|
+
const items = [];
|
|
30686
|
+
for (const key of keys) {
|
|
30687
|
+
const node = nodes.get(key);
|
|
30688
|
+
if (node === void 0 || node === null || node.kind !== KIND) continue;
|
|
30689
|
+
const list = node.data?.items;
|
|
30690
|
+
if (!Array.isArray(list)) continue;
|
|
30691
|
+
for (const item of list) {
|
|
30692
|
+
if (item === null || typeof item !== "object" || typeof item.path !== "string") continue;
|
|
30693
|
+
if (seen.has(item.path)) continue;
|
|
30694
|
+
seen.add(item.path);
|
|
30695
|
+
items.push({
|
|
30696
|
+
path: item.path,
|
|
30697
|
+
label: typeof item.label === "string" ? item.label : basenameOf(item.path)
|
|
30698
|
+
});
|
|
30699
|
+
if (items.length >= MAX_IMAGES) return items;
|
|
30700
|
+
}
|
|
30701
|
+
}
|
|
30702
|
+
return items;
|
|
30426
30703
|
}
|
|
30427
30704
|
/**
|
|
30428
|
-
*
|
|
30429
|
-
*
|
|
30430
|
-
* **缺键不抛异常**。原来这里是 `dictionaries[locale][key].replace(...)`:任何一个不存在的键
|
|
30431
|
-
* 都会抛 TypeError,而它在 React 渲染路径上 —— 结果不是那一行显示不出原文,而是**整个设置
|
|
30432
|
-
* 面板白屏**。缺一个描述行就把七张卡里的全部设置都搭进去,代价和收益完全不成比例。
|
|
30705
|
+
* Turn 尾部的图集:本轮每条交接消息各自那张图。
|
|
30433
30706
|
*
|
|
30434
|
-
*
|
|
30435
|
-
* 这样的字面量,一眼能看出该补哪条,而不是面对一个空白面板。
|
|
30707
|
+
* 没有会话 id 就什么都不显示 —— 路由需要它来定位目录,猜一个只会 404。
|
|
30436
30708
|
*/
|
|
30437
|
-
function
|
|
30438
|
-
const
|
|
30439
|
-
const
|
|
30440
|
-
|
|
30441
|
-
const
|
|
30442
|
-
|
|
30443
|
-
return
|
|
30709
|
+
function HandoffTurnTail(props) {
|
|
30710
|
+
const items = collectTurnItems(props.useChat((snapshot) => snapshot.nodes), props.useChat((snapshot) => snapshot.locations), props.turn);
|
|
30711
|
+
const sessionId = props.sessionId;
|
|
30712
|
+
const { t } = useWorkspaceI18n();
|
|
30713
|
+
const [activeIndex, setActiveIndex] = (0, react.useState)(null);
|
|
30714
|
+
if (items.length === 0) return null;
|
|
30715
|
+
if (typeof sessionId !== "string" || sessionId === "") return null;
|
|
30716
|
+
const openIndex = activeIndex !== null && activeIndex < items.length ? activeIndex : null;
|
|
30717
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30718
|
+
className: "daw-handoff-gallery",
|
|
30719
|
+
style: WRAP_STYLE,
|
|
30720
|
+
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffThumb, {
|
|
30721
|
+
item,
|
|
30722
|
+
sessionId,
|
|
30723
|
+
failedText: t("imageLoadFailed"),
|
|
30724
|
+
onOpen: () => setActiveIndex(index)
|
|
30725
|
+
}, item.path))
|
|
30726
|
+
}), openIndex !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffLightbox, {
|
|
30727
|
+
items,
|
|
30728
|
+
index: openIndex,
|
|
30729
|
+
sessionId,
|
|
30730
|
+
failedText: t("imageLoadFailed"),
|
|
30731
|
+
prevLabel: t("prevImage"),
|
|
30732
|
+
nextLabel: t("nextImage"),
|
|
30733
|
+
onClose: () => setActiveIndex(null),
|
|
30734
|
+
onPrev: () => setActiveIndex((i) => i === null ? i : Math.max(0, i - 1)),
|
|
30735
|
+
onNext: () => setActiveIndex((i) => i === null ? i : Math.min(items.length - 1, i + 1))
|
|
30736
|
+
}) : null] });
|
|
30444
30737
|
}
|
|
30445
30738
|
/**
|
|
30446
|
-
*
|
|
30739
|
+
* 一条「交接消息 → 一个图集节点」的 Definition。
|
|
30447
30740
|
*
|
|
30448
|
-
*
|
|
30449
|
-
*
|
|
30450
|
-
* 出问题时用户把屏幕上的字念出来,比报一串码有用得多。
|
|
30741
|
+
* 只匹配 `user/message`:交接说明属于读者自己发的那条消息。助手正文、上下文注入、
|
|
30742
|
+
* 工具输出里到处都是引用 `.dsh-images/…` 的文本,读它们会把别人的话切成碎图。
|
|
30451
30743
|
*
|
|
30452
|
-
*
|
|
30453
|
-
*
|
|
30454
|
-
* 露出键名(见上面),界面上会突然冒出一行 `scopeChatread`。白屏那次就是这么来的。
|
|
30744
|
+
* 节点自身**不注册渲染器**(Turn 尾负责显示),所以过程分组里不会多出一行、
|
|
30745
|
+
* verbose 模式下也不会重复一次。
|
|
30455
30746
|
*/
|
|
30456
|
-
const
|
|
30457
|
-
|
|
30458
|
-
|
|
30459
|
-
|
|
30460
|
-
|
|
30461
|
-
|
|
30462
|
-
|
|
30463
|
-
|
|
30464
|
-
|
|
30465
|
-
|
|
30466
|
-
|
|
30467
|
-
|
|
30468
|
-
|
|
30469
|
-
|
|
30470
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
|
|
30479
|
-
|
|
30480
|
-
return
|
|
30481
|
-
|
|
30482
|
-
|
|
30747
|
+
const handoffDefinition = {
|
|
30748
|
+
kind: KIND,
|
|
30749
|
+
target: "chat",
|
|
30750
|
+
match(event) {
|
|
30751
|
+
if (event === null || typeof event !== "object" || event.surfaceOp !== "append") return null;
|
|
30752
|
+
if (event.type !== "user/message") return null;
|
|
30753
|
+
return {
|
|
30754
|
+
id: `m:${String(event.seq)}`,
|
|
30755
|
+
role: "start"
|
|
30756
|
+
};
|
|
30757
|
+
},
|
|
30758
|
+
start(_context, match) {
|
|
30759
|
+
return {
|
|
30760
|
+
items: itemsOfMessage(match.event.data),
|
|
30761
|
+
anchorSeq: match.event.seq
|
|
30762
|
+
};
|
|
30763
|
+
},
|
|
30764
|
+
update(context) {
|
|
30765
|
+
return context.state;
|
|
30766
|
+
},
|
|
30767
|
+
buildViewNode(context) {
|
|
30768
|
+
const state = context.state;
|
|
30769
|
+
if (state === void 0 || !Array.isArray(state.items) || state.items.length === 0) return null;
|
|
30770
|
+
const start = context.start;
|
|
30771
|
+
return {
|
|
30772
|
+
key: context.key,
|
|
30773
|
+
kind: KIND,
|
|
30774
|
+
id: context.id,
|
|
30775
|
+
target: "chat",
|
|
30776
|
+
anchorSeq: state.anchorSeq,
|
|
30777
|
+
location: start !== void 0 && start !== null ? start.location : { kind: "unresolved" },
|
|
30778
|
+
visibility: "visible",
|
|
30779
|
+
data: { items: state.items }
|
|
30780
|
+
};
|
|
30483
30781
|
}
|
|
30484
|
-
}
|
|
30782
|
+
};
|
|
30485
30783
|
//#endregion
|
|
30486
30784
|
//#region src/client/styles.ts
|
|
30487
30785
|
const STYLE_ID = "dsh-workspace-style";
|
|
@@ -30497,16 +30795,33 @@ window.__ModuleLoader__.load({
|
|
|
30497
30795
|
* 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
|
|
30498
30796
|
*/
|
|
30499
30797
|
const WORKSPACE_CSS = `
|
|
30500
|
-
.daw-version{font:600 11px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-muted);white-space:nowrap}
|
|
30501
30798
|
.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}
|
|
30502
|
-
.daw-root *{box-sizing:border-box;letter-spacing:0}
|
|
30503
|
-
|
|
30799
|
+
.daw-root *,.daw-settings-section *{box-sizing:border-box;letter-spacing:0}
|
|
30800
|
+
/* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
|
|
30801
|
+
深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
|
|
30802
|
+
.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}
|
|
30803
|
+
.daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
|
|
30804
|
+
.daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
|
|
30805
|
+
.daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
|
|
30504
30806
|
.daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
|
|
30505
30807
|
.daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
|
|
30506
30808
|
.daw-icon:disabled{opacity:.45;cursor:not-allowed}
|
|
30507
|
-
|
|
30508
|
-
|
|
30509
|
-
.daw-
|
|
30809
|
+
/* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
|
|
30810
|
+
全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
|
|
30811
|
+
.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}
|
|
30812
|
+
.daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30813
|
+
.daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
|
|
30814
|
+
/* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
|
|
30815
|
+
.daw-command:disabled{opacity:.5;cursor:not-allowed}
|
|
30816
|
+
.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}
|
|
30817
|
+
.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))}
|
|
30818
|
+
.daw-command.danger{color:var(--daw-danger)}
|
|
30819
|
+
.daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
|
|
30820
|
+
/* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
|
|
30821
|
+
.daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
|
|
30822
|
+
.daw-copy-label{font-size:12px}
|
|
30823
|
+
.daw-empty{padding:14px 2px;text-align:center}
|
|
30824
|
+
.daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
|
|
30510
30825
|
.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}
|
|
30511
30826
|
.daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30512
30827
|
.daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
@@ -30521,7 +30836,17 @@ window.__ModuleLoader__.load({
|
|
|
30521
30836
|
.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}
|
|
30522
30837
|
.daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
|
|
30523
30838
|
.daw-builtin-hint{text-align:right;align-self:center}
|
|
30524
|
-
.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-
|
|
30839
|
+
.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}
|
|
30840
|
+
/* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
|
|
30841
|
+
.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}
|
|
30842
|
+
.daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
|
|
30843
|
+
.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}
|
|
30844
|
+
.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}
|
|
30845
|
+
/* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
|
|
30846
|
+
不再把「未知进度」伪装成恒定 30%。 */
|
|
30847
|
+
.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}
|
|
30848
|
+
@keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
|
|
30849
|
+
@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}}
|
|
30525
30850
|
`;
|
|
30526
30851
|
function installWorkspaceStyles() {
|
|
30527
30852
|
if (document.getElementById(STYLE_ID) !== null) return;
|
|
@@ -30558,7 +30883,6 @@ window.__ModuleLoader__.load({
|
|
|
30558
30883
|
}];
|
|
30559
30884
|
//#endregion
|
|
30560
30885
|
//#region src/client/workspace.tsx
|
|
30561
|
-
var import_lucide_react = require_lucide_react();
|
|
30562
30886
|
/** What the settings-page launcher opens: authorized roots, the first section. */
|
|
30563
30887
|
const DEFAULT_PANEL_SECTION = "roots";
|
|
30564
30888
|
/**
|
|
@@ -30571,9 +30895,37 @@ window.__ModuleLoader__.load({
|
|
|
30571
30895
|
function tabLabelKey(tab) {
|
|
30572
30896
|
return tab === "app-download" ? "appDownloadTab" : `${tab}Tab`;
|
|
30573
30897
|
}
|
|
30574
|
-
/**
|
|
30575
|
-
function
|
|
30576
|
-
return
|
|
30898
|
+
/** 配对码剩余分钟:向上取整 —— 显示「约 9 分钟」比「约 8 分钟」在最后一刻更诚实。 */
|
|
30899
|
+
function pairingMinutesLeft(expiresAt, now) {
|
|
30900
|
+
return Math.max(0, Math.ceil((expiresAt - now) / 6e4));
|
|
30901
|
+
}
|
|
30902
|
+
/**
|
|
30903
|
+
* 通用复制小按钮:成功后短暂换成对勾 + 「已复制」,2s 自动复位;
|
|
30904
|
+
* 非安全上下文(HTTP/LAN)拿不到 Clipboard API 时退化为选中文本提示,见 copyText 的调用方。
|
|
30905
|
+
*/
|
|
30906
|
+
function CopyButton(props) {
|
|
30907
|
+
const [state, setState] = (0, react.useState)("idle");
|
|
30908
|
+
const [inFlight, setInFlight] = (0, react.useState)(false);
|
|
30909
|
+
const label = state === "done" ? props.copiedText : state === "manual" ? props.manualText ?? props.title : props.title;
|
|
30910
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
30911
|
+
type: "button",
|
|
30912
|
+
className: `daw-command icon-as-command${props.className === void 0 ? "" : ` ${props.className}`}`,
|
|
30913
|
+
title: props.title,
|
|
30914
|
+
"aria-label": props.title,
|
|
30915
|
+
disabled: inFlight || state !== "idle",
|
|
30916
|
+
onClick: () => {
|
|
30917
|
+
if (inFlight) return;
|
|
30918
|
+
setInFlight(true);
|
|
30919
|
+
props.copyText(props.text).then((kind) => {
|
|
30920
|
+
setState(kind === "done" ? "done" : "manual");
|
|
30921
|
+
window.setTimeout(() => setState("idle"), kind === "done" ? 2e3 : 2500);
|
|
30922
|
+
}).finally(() => setInFlight(false));
|
|
30923
|
+
},
|
|
30924
|
+
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", {
|
|
30925
|
+
className: "daw-copy-label",
|
|
30926
|
+
children: label
|
|
30927
|
+
})]
|
|
30928
|
+
});
|
|
30577
30929
|
}
|
|
30578
30930
|
/**
|
|
30579
30931
|
* 卡片头部的描述行。与 `tabLabelKey` 同理:唯一含连字符的分区显式映射。
|
|
@@ -30713,9 +31065,52 @@ window.__ModuleLoader__.load({
|
|
|
30713
31065
|
const [openCards, setOpenCards] = (0, react.useState)(() => /* @__PURE__ */ new Set([props.initialOpen]));
|
|
30714
31066
|
const [imageSaved, setImageSaved] = (0, react.useState)(false);
|
|
30715
31067
|
const [error, setError] = (0, react.useState)();
|
|
31068
|
+
const [notice, setNotice] = (0, react.useState)();
|
|
30716
31069
|
const [tunnel, setTunnel] = (0, react.useState)();
|
|
30717
31070
|
const [tunnelBusy, setTunnelBusy] = (0, react.useState)(false);
|
|
30718
31071
|
const [activity, setActivity] = (0, react.useState)();
|
|
31072
|
+
const [auditLimit, setAuditLimit] = (0, react.useState)(50);
|
|
31073
|
+
const [auditLoading, setAuditLoading] = (0, react.useState)(false);
|
|
31074
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
31075
|
+
const notify = (0, react.useCallback)((key, params) => {
|
|
31076
|
+
setNotice(params === void 0 ? {
|
|
31077
|
+
id: Date.now() + Math.random(),
|
|
31078
|
+
key
|
|
31079
|
+
} : {
|
|
31080
|
+
id: Date.now() + Math.random(),
|
|
31081
|
+
key,
|
|
31082
|
+
params
|
|
31083
|
+
});
|
|
31084
|
+
}, []);
|
|
31085
|
+
(0, react.useEffect)(() => {
|
|
31086
|
+
if (notice === void 0) return;
|
|
31087
|
+
const timer = window.setTimeout(() => setNotice(void 0), 3500);
|
|
31088
|
+
return () => window.clearTimeout(timer);
|
|
31089
|
+
}, [notice]);
|
|
31090
|
+
(0, react.useEffect)(() => {
|
|
31091
|
+
const timer = window.setInterval(() => setNow(Date.now()), 3e4);
|
|
31092
|
+
return () => window.clearInterval(timer);
|
|
31093
|
+
}, []);
|
|
31094
|
+
const copyText = (0, react.useCallback)(async (text) => {
|
|
31095
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
31096
|
+
if (clipboard !== void 0) try {
|
|
31097
|
+
await clipboard.writeText(text);
|
|
31098
|
+
return "done";
|
|
31099
|
+
} catch {}
|
|
31100
|
+
try {
|
|
31101
|
+
const node = document.createElement("textarea");
|
|
31102
|
+
node.value = text;
|
|
31103
|
+
node.style.position = "fixed";
|
|
31104
|
+
node.style.top = "-9999px";
|
|
31105
|
+
node.setAttribute("readonly", "");
|
|
31106
|
+
document.body.append(node);
|
|
31107
|
+
node.select();
|
|
31108
|
+
const ok = document.execCommand("copy");
|
|
31109
|
+
node.remove();
|
|
31110
|
+
if (ok) return "done";
|
|
31111
|
+
} catch {}
|
|
31112
|
+
return "manual";
|
|
31113
|
+
}, []);
|
|
30719
31114
|
const isOpen = (0, react.useCallback)((card) => openCards.has(card), [openCards]);
|
|
30720
31115
|
const toggleCard = (0, react.useCallback)((card) => {
|
|
30721
31116
|
setOpenCards((previous) => {
|
|
@@ -30725,6 +31120,9 @@ window.__ModuleLoader__.load({
|
|
|
30725
31120
|
});
|
|
30726
31121
|
}, []);
|
|
30727
31122
|
const refresh = (0, react.useCallback)(async () => {
|
|
31123
|
+
try {
|
|
31124
|
+
setTunnel(await props.api.tunnelStatus());
|
|
31125
|
+
} catch {}
|
|
30728
31126
|
try {
|
|
30729
31127
|
const next = await props.api.status();
|
|
30730
31128
|
setStatus(next);
|
|
@@ -30736,49 +31134,157 @@ window.__ModuleLoader__.load({
|
|
|
30736
31134
|
setImageDraft(settings.subdir);
|
|
30737
31135
|
setImageSaved(false);
|
|
30738
31136
|
}
|
|
30739
|
-
if (openCards.has("remote")) setTunnel(await props.api.tunnelStatus());
|
|
30740
|
-
setError(void 0);
|
|
30741
31137
|
} catch (cause) {
|
|
30742
|
-
setError(cause);
|
|
31138
|
+
if (!(cause instanceof WorkspaceApiError && cause.status === 403 && (cause.code === "TUNNEL_MODE_ADMIN_DISABLED" || cause.code === "ADMIN_PROXY_FORWARDED"))) setError(cause);
|
|
30743
31139
|
}
|
|
30744
31140
|
}, [props.api, openCards]);
|
|
30745
|
-
const
|
|
31141
|
+
const tunnelErrorText = (0, react.useCallback)((s) => {
|
|
31142
|
+
if (s.errorKey !== void 0) return t(s.errorKey, s.errorParams ?? {});
|
|
31143
|
+
return s.error ?? t("listenerFailed");
|
|
31144
|
+
}, [t]);
|
|
31145
|
+
const pollActivityOnce = (0, react.useCallback)(async (active) => {
|
|
31146
|
+
while (active()) {
|
|
31147
|
+
try {
|
|
31148
|
+
const a = await props.api.tunnelActivity();
|
|
31149
|
+
if (!active()) return;
|
|
31150
|
+
setActivity(a.stage === "idle" ? void 0 : a);
|
|
31151
|
+
} catch {}
|
|
31152
|
+
await delay(400);
|
|
31153
|
+
}
|
|
31154
|
+
}, [props.api]);
|
|
31155
|
+
const waitForTunnelUrl = (0, react.useCallback)(async (active) => {
|
|
31156
|
+
for (let attempt = 0; attempt < 40 && active(); attempt += 1) {
|
|
31157
|
+
await delay(500);
|
|
31158
|
+
const s = await props.api.tunnelStatus();
|
|
31159
|
+
if (!active()) return null;
|
|
31160
|
+
setTunnel(s);
|
|
31161
|
+
if (s.errorKey !== void 0 || s.error !== void 0) throw new Error(tunnelErrorText(s));
|
|
31162
|
+
if (s.running && s.url !== null) return s.url;
|
|
31163
|
+
}
|
|
31164
|
+
return null;
|
|
31165
|
+
}, [props.api, tunnelErrorText]);
|
|
31166
|
+
const generateConfigForUrl = (0, react.useCallback)(async (baseUrl, noticeKey = "configCreated") => {
|
|
31167
|
+
setConfigTextBusy(true);
|
|
31168
|
+
setError(void 0);
|
|
31169
|
+
try {
|
|
31170
|
+
const currentRoots = status?.roots ?? [];
|
|
31171
|
+
const value = await props.api.createConfigText({
|
|
31172
|
+
...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) },
|
|
31173
|
+
endpoints: [{
|
|
31174
|
+
label: t("tunnelEndpointLabel"),
|
|
31175
|
+
baseUrl
|
|
31176
|
+
}]
|
|
31177
|
+
});
|
|
31178
|
+
setConfigText(value);
|
|
31179
|
+
notify(noticeKey);
|
|
31180
|
+
return true;
|
|
31181
|
+
} catch (cause) {
|
|
31182
|
+
setError(cause);
|
|
31183
|
+
return false;
|
|
31184
|
+
} finally {
|
|
31185
|
+
setConfigTextBusy(false);
|
|
31186
|
+
}
|
|
31187
|
+
}, [
|
|
31188
|
+
props.api,
|
|
31189
|
+
status,
|
|
31190
|
+
t,
|
|
31191
|
+
notify
|
|
31192
|
+
]);
|
|
31193
|
+
const createTunnel = (0, react.useCallback)(async (opts = {}) => {
|
|
30746
31194
|
if (!window.confirm(t("tunnelStartConfirm"))) return;
|
|
31195
|
+
const wantConfig = opts.thenGenConfig === true;
|
|
31196
|
+
let alive = true;
|
|
30747
31197
|
setTunnelBusy(true);
|
|
30748
31198
|
setError(void 0);
|
|
30749
|
-
|
|
31199
|
+
setConfigText(void 0);
|
|
30750
31200
|
setActivity({
|
|
30751
31201
|
stage: "locating",
|
|
30752
|
-
|
|
31202
|
+
labelKey: "tunnelStageLocating",
|
|
30753
31203
|
percent: null
|
|
30754
31204
|
});
|
|
30755
|
-
|
|
30756
|
-
|
|
30757
|
-
|
|
30758
|
-
}
|
|
30759
|
-
|
|
30760
|
-
props.api.tunnelCreate().then(async (next) => {
|
|
30761
|
-
setTunnel(next);
|
|
31205
|
+
pollActivityOnce(() => alive);
|
|
31206
|
+
try {
|
|
31207
|
+
const currentRoots = status?.roots ?? [];
|
|
31208
|
+
const created = await props.api.tunnelCreate(wantConfig, { ...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) } });
|
|
31209
|
+
setTunnel(created);
|
|
30762
31210
|
setActivity(void 0);
|
|
31211
|
+
if (created.errorKey !== void 0 || created.error !== void 0) throw new Error(tunnelErrorText(created));
|
|
31212
|
+
if (wantConfig && created.configText !== void 0) {
|
|
31213
|
+
setConfigText(created.configText);
|
|
31214
|
+
await refresh();
|
|
31215
|
+
notify("tunnelConfigAuto");
|
|
31216
|
+
return;
|
|
31217
|
+
}
|
|
31218
|
+
const url = created.url ?? await waitForTunnelUrl(() => alive);
|
|
30763
31219
|
await refresh();
|
|
30764
|
-
|
|
30765
|
-
|
|
31220
|
+
if (url === null) {
|
|
31221
|
+
notify("tunnelUrlPending");
|
|
31222
|
+
return;
|
|
31223
|
+
}
|
|
31224
|
+
if (wantConfig) await generateConfigForUrl(url, "tunnelConfigAuto");
|
|
31225
|
+
else notify("tunnelStarted");
|
|
31226
|
+
} catch (cause) {
|
|
31227
|
+
setError(cause);
|
|
31228
|
+
} finally {
|
|
31229
|
+
alive = false;
|
|
30766
31230
|
setActivity(void 0);
|
|
30767
31231
|
setTunnelBusy(false);
|
|
30768
|
-
}
|
|
31232
|
+
}
|
|
30769
31233
|
}, [
|
|
30770
31234
|
props.api,
|
|
30771
31235
|
refresh,
|
|
30772
|
-
|
|
30773
|
-
|
|
30774
|
-
|
|
31236
|
+
status,
|
|
31237
|
+
t,
|
|
31238
|
+
notify,
|
|
31239
|
+
pollActivityOnce,
|
|
31240
|
+
waitForTunnelUrl,
|
|
31241
|
+
tunnelErrorText,
|
|
31242
|
+
generateConfigForUrl
|
|
31243
|
+
]);
|
|
31244
|
+
const stopTunnel = (0, react.useCallback)(async () => {
|
|
31245
|
+
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
30775
31246
|
setTunnelBusy(true);
|
|
30776
31247
|
setError(void 0);
|
|
30777
|
-
|
|
31248
|
+
try {
|
|
31249
|
+
const next = await props.api.tunnelStop();
|
|
30778
31250
|
setTunnel(next);
|
|
30779
31251
|
await refresh();
|
|
30780
|
-
|
|
30781
|
-
|
|
31252
|
+
notify("tunnelStopped");
|
|
31253
|
+
} catch (cause) {
|
|
31254
|
+
setError(cause);
|
|
31255
|
+
} finally {
|
|
31256
|
+
setTunnelBusy(false);
|
|
31257
|
+
}
|
|
31258
|
+
}, [
|
|
31259
|
+
props.api,
|
|
31260
|
+
refresh,
|
|
31261
|
+
t,
|
|
31262
|
+
notify
|
|
31263
|
+
]);
|
|
31264
|
+
const tunnelConfigClick = (0, react.useCallback)(async () => {
|
|
31265
|
+
if (tunnel?.running === true) {
|
|
31266
|
+
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
31267
|
+
setTunnelBusy(true);
|
|
31268
|
+
setError(void 0);
|
|
31269
|
+
try {
|
|
31270
|
+
const stopped = await props.api.tunnelStop();
|
|
31271
|
+
setTunnel(stopped);
|
|
31272
|
+
await refresh();
|
|
31273
|
+
} catch (cause) {
|
|
31274
|
+
setError(cause);
|
|
31275
|
+
} finally {
|
|
31276
|
+
setTunnelBusy(false);
|
|
31277
|
+
}
|
|
31278
|
+
await createTunnel({ thenGenConfig: true });
|
|
31279
|
+
} else await createTunnel({ thenGenConfig: true });
|
|
31280
|
+
}, [
|
|
31281
|
+
props.api,
|
|
31282
|
+
tunnel,
|
|
31283
|
+
t,
|
|
31284
|
+
createTunnel,
|
|
31285
|
+
refresh,
|
|
31286
|
+
setError
|
|
31287
|
+
]);
|
|
30782
31288
|
(0, react.useEffect)(() => {
|
|
30783
31289
|
refresh();
|
|
30784
31290
|
}, [refresh]);
|
|
@@ -30788,25 +31294,7 @@ window.__ModuleLoader__.load({
|
|
|
30788
31294
|
setListenerPort(String(status.configuredPort));
|
|
30789
31295
|
}, [status?.configuredHost, status?.configuredPort]);
|
|
30790
31296
|
const roots = status?.roots ?? [];
|
|
30791
|
-
const
|
|
30792
|
-
setConfigTextBusy(true);
|
|
30793
|
-
setError(void 0);
|
|
30794
|
-
props.api.createConfigText({
|
|
30795
|
-
...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
|
|
30796
|
-
endpoints: [{
|
|
30797
|
-
label: t("tunnelEndpointLabel"),
|
|
30798
|
-
baseUrl
|
|
30799
|
-
}]
|
|
30800
|
-
}).then((value) => {
|
|
30801
|
-
setConfigText(value);
|
|
30802
|
-
return refresh();
|
|
30803
|
-
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
30804
|
-
}, [
|
|
30805
|
-
props.api,
|
|
30806
|
-
refresh,
|
|
30807
|
-
roots,
|
|
30808
|
-
t
|
|
30809
|
-
]);
|
|
31297
|
+
const managementLocked = tunnel?.running === true;
|
|
30810
31298
|
const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEVICE_SCOPES]);
|
|
30811
31299
|
const toggleScope = (0, react.useCallback)((scope) => {
|
|
30812
31300
|
setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
|
|
@@ -30835,10 +31323,25 @@ window.__ModuleLoader__.load({
|
|
|
30835
31323
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
30836
31324
|
className: "daw-admin",
|
|
30837
31325
|
children: [
|
|
31326
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31327
|
+
className: "daw-admin-topbar",
|
|
31328
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LanguageToggle, {})
|
|
31329
|
+
}),
|
|
30838
31330
|
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30839
31331
|
className: "daw-warning",
|
|
31332
|
+
role: "alert",
|
|
30840
31333
|
children: messageOf(error, t)
|
|
30841
31334
|
}),
|
|
31335
|
+
notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31336
|
+
className: "daw-success",
|
|
31337
|
+
role: "status",
|
|
31338
|
+
children: t(notice.key, notice.params)
|
|
31339
|
+
}),
|
|
31340
|
+
managementLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31341
|
+
className: "daw-warning",
|
|
31342
|
+
role: "status",
|
|
31343
|
+
children: t("managementLockedWhileTunnel")
|
|
31344
|
+
}),
|
|
30842
31345
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
30843
31346
|
tab: "roots",
|
|
30844
31347
|
nameKey: tabLabelKey("roots"),
|
|
@@ -30863,10 +31366,14 @@ window.__ModuleLoader__.load({
|
|
|
30863
31366
|
}),
|
|
30864
31367
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30865
31368
|
className: "daw-command primary",
|
|
31369
|
+
disabled: managementLocked,
|
|
31370
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
30866
31371
|
onClick: () => {
|
|
31372
|
+
setError(void 0);
|
|
30867
31373
|
props.api.addRoot(path, label || void 0).then(() => {
|
|
30868
31374
|
setPath("");
|
|
30869
31375
|
setLabel("");
|
|
31376
|
+
notify("rootAdded");
|
|
30870
31377
|
return refresh();
|
|
30871
31378
|
}).catch(setError);
|
|
30872
31379
|
},
|
|
@@ -30893,8 +31400,15 @@ window.__ModuleLoader__.load({
|
|
|
30893
31400
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30894
31401
|
className: "daw-icon danger",
|
|
30895
31402
|
title: t("removeRoot"),
|
|
31403
|
+
"aria-label": t("removeRoot"),
|
|
31404
|
+
disabled: managementLocked,
|
|
30896
31405
|
onClick: () => {
|
|
30897
|
-
|
|
31406
|
+
if (!window.confirm(t("removeRootConfirm", { label: root.label }))) return;
|
|
31407
|
+
setError(void 0);
|
|
31408
|
+
props.api.removeRoot(root.id).then(() => {
|
|
31409
|
+
notify("rootRemoved");
|
|
31410
|
+
return refresh();
|
|
31411
|
+
}).catch(setError);
|
|
30898
31412
|
},
|
|
30899
31413
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
30900
31414
|
})]
|
|
@@ -30917,7 +31431,7 @@ window.__ModuleLoader__.load({
|
|
|
30917
31431
|
className: "daw-warning",
|
|
30918
31432
|
children: t("httpWarning")
|
|
30919
31433
|
}),
|
|
30920
|
-
roots.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31434
|
+
roots.length === 0 && !managementLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30921
31435
|
className: "daw-warning",
|
|
30922
31436
|
children: t("addRootBeforeRemote")
|
|
30923
31437
|
}),
|
|
@@ -30929,7 +31443,7 @@ window.__ModuleLoader__.load({
|
|
|
30929
31443
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindIp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30930
31444
|
className: "daw-input",
|
|
30931
31445
|
value: listenerHost,
|
|
30932
|
-
disabled: status?.remoteEnabled,
|
|
31446
|
+
disabled: status?.remoteEnabled || managementLocked,
|
|
30933
31447
|
spellCheck: false,
|
|
30934
31448
|
placeholder: "0.0.0.0",
|
|
30935
31449
|
onChange: (event) => setListenerHost(event.target.value)
|
|
@@ -30944,15 +31458,19 @@ window.__ModuleLoader__.load({
|
|
|
30944
31458
|
max: 65535,
|
|
30945
31459
|
inputMode: "numeric",
|
|
30946
31460
|
value: listenerPort,
|
|
30947
|
-
disabled: status?.remoteEnabled,
|
|
31461
|
+
disabled: status?.remoteEnabled || managementLocked,
|
|
30948
31462
|
onChange: (event) => setListenerPort(event.target.value)
|
|
30949
31463
|
})]
|
|
30950
31464
|
}),
|
|
30951
31465
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30952
31466
|
className: "daw-command",
|
|
30953
|
-
disabled: status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
31467
|
+
disabled: managementLocked || status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
30954
31468
|
onClick: () => {
|
|
30955
|
-
|
|
31469
|
+
setError(void 0);
|
|
31470
|
+
props.api.configureListener(listenerHost.trim(), parsedListenerPort).then((result) => poll(result.id)).then(() => {
|
|
31471
|
+
notify("listenerSaved");
|
|
31472
|
+
return refresh();
|
|
31473
|
+
}).catch(setError);
|
|
30956
31474
|
},
|
|
30957
31475
|
children: t("saveListener")
|
|
30958
31476
|
})
|
|
@@ -31000,19 +31518,33 @@ window.__ModuleLoader__.load({
|
|
|
31000
31518
|
className: "daw-warning",
|
|
31001
31519
|
children: t("tunnelSecurityWarning")
|
|
31002
31520
|
}),
|
|
31003
|
-
tunnel?.
|
|
31521
|
+
tunnel?.errorKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31522
|
+
className: "daw-warning",
|
|
31523
|
+
role: "alert",
|
|
31524
|
+
children: t(tunnel.errorKey, tunnel.errorParams ?? {})
|
|
31525
|
+
}),
|
|
31526
|
+
tunnel?.errorKey === void 0 && tunnel?.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31004
31527
|
className: "daw-warning",
|
|
31528
|
+
role: "alert",
|
|
31005
31529
|
children: tunnel.error
|
|
31006
31530
|
}),
|
|
31007
|
-
tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
31008
|
-
className: "daw-
|
|
31009
|
-
|
|
31010
|
-
|
|
31531
|
+
tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31532
|
+
className: "daw-url-row",
|
|
31533
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31534
|
+
className: "daw-tunnel-url",
|
|
31535
|
+
children: tunnel.url
|
|
31536
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
31537
|
+
text: tunnel.url,
|
|
31538
|
+
title: t("copyUrl"),
|
|
31539
|
+
copiedText: t("copied"),
|
|
31540
|
+
manualText: t("copyManual"),
|
|
31541
|
+
copyText
|
|
31542
|
+
})]
|
|
31011
31543
|
}),
|
|
31012
31544
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31013
31545
|
className: "daw-command primary",
|
|
31014
31546
|
style: { marginTop: 10 },
|
|
31015
|
-
disabled: tunnelBusy || roots.length === 0,
|
|
31547
|
+
disabled: tunnelBusy || tunnel?.running !== true && roots.length === 0,
|
|
31016
31548
|
onClick: () => {
|
|
31017
31549
|
if (tunnel?.running === true) stopTunnel();
|
|
31018
31550
|
else createTunnel();
|
|
@@ -31025,31 +31557,39 @@ window.__ModuleLoader__.load({
|
|
|
31025
31557
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31026
31558
|
className: "daw-progress-track",
|
|
31027
31559
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31028
|
-
className:
|
|
31029
|
-
style: { width:
|
|
31560
|
+
className: `daw-progress-fill${activity.percent === null ? " indeterminate" : ""}`,
|
|
31561
|
+
style: activity.percent === null ? void 0 : { width: `${activity.percent}%` }
|
|
31030
31562
|
})
|
|
31031
31563
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31032
31564
|
className: "daw-list-meta",
|
|
31033
31565
|
style: { marginTop: 4 },
|
|
31034
|
-
children: activity.
|
|
31566
|
+
children: activity.labelKey === "" ? "" : t(activity.labelKey, activity.labelParams ?? {})
|
|
31035
31567
|
})]
|
|
31036
31568
|
})
|
|
31037
31569
|
]
|
|
31038
31570
|
}),
|
|
31039
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
31040
|
-
|
|
31041
|
-
|
|
31042
|
-
|
|
31043
|
-
|
|
31044
|
-
|
|
31045
|
-
|
|
31046
|
-
|
|
31047
|
-
|
|
31048
|
-
|
|
31049
|
-
|
|
31050
|
-
|
|
31051
|
-
|
|
31052
|
-
|
|
31571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
31572
|
+
disabled: managementLocked,
|
|
31573
|
+
style: {
|
|
31574
|
+
border: "none",
|
|
31575
|
+
margin: 0,
|
|
31576
|
+
padding: 0
|
|
31577
|
+
},
|
|
31578
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31579
|
+
className: "daw-scope-grid",
|
|
31580
|
+
children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31581
|
+
className: "daw-check",
|
|
31582
|
+
children: [
|
|
31583
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31584
|
+
type: "checkbox",
|
|
31585
|
+
checked: grantedScopes.includes(scope),
|
|
31586
|
+
onChange: () => toggleScope(scope)
|
|
31587
|
+
}),
|
|
31588
|
+
" ",
|
|
31589
|
+
scopeLabel(locale, scope)
|
|
31590
|
+
]
|
|
31591
|
+
}, scope))
|
|
31592
|
+
})
|
|
31053
31593
|
}),
|
|
31054
31594
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31055
31595
|
className: "daw-list-meta",
|
|
@@ -31057,30 +31597,44 @@ window.__ModuleLoader__.load({
|
|
|
31057
31597
|
}),
|
|
31058
31598
|
status?.remoteEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31059
31599
|
className: "daw-command danger",
|
|
31600
|
+
disabled: managementLocked,
|
|
31601
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31060
31602
|
onClick: () => {
|
|
31061
|
-
|
|
31603
|
+
if (!window.confirm(t("disableRemoteConfirm"))) return;
|
|
31604
|
+
setError(void 0);
|
|
31605
|
+
props.api.disableRemote().then((result) => poll(result.id)).then(() => {
|
|
31606
|
+
notify("remoteDisabled");
|
|
31607
|
+
return refresh();
|
|
31608
|
+
}).catch(setError);
|
|
31062
31609
|
},
|
|
31063
31610
|
children: t("disableRemote")
|
|
31064
31611
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31065
31612
|
className: "daw-command primary",
|
|
31066
|
-
disabled: roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
31613
|
+
disabled: managementLocked || roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
31067
31614
|
onClick: () => {
|
|
31615
|
+
setError(void 0);
|
|
31068
31616
|
props.api.enableRemote(roots.map((root) => root.id), grantedScopes).then((result) => poll(result.id)).catch(setError);
|
|
31069
31617
|
},
|
|
31070
31618
|
children: t("enablePairing")
|
|
31071
31619
|
}),
|
|
31072
|
-
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31620
|
+
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31073
31621
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31074
31622
|
className: "daw-command daw-inline-command",
|
|
31075
|
-
disabled: grantedScopes.length === 0,
|
|
31623
|
+
disabled: managementLocked || grantedScopes.length === 0,
|
|
31624
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31076
31625
|
onClick: () => {
|
|
31077
|
-
|
|
31626
|
+
setError(void 0);
|
|
31627
|
+
props.api.createPairing(roots.map((root) => root.id), grantedScopes).then((value) => {
|
|
31628
|
+
setPairing(value);
|
|
31629
|
+
notify("pairingCreated");
|
|
31630
|
+
}).catch(setError);
|
|
31078
31631
|
},
|
|
31079
31632
|
children: t("newPairingCode")
|
|
31080
31633
|
}),
|
|
31081
31634
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31082
31635
|
className: "daw-command daw-inline-command",
|
|
31083
|
-
disabled: configTextBusy,
|
|
31636
|
+
disabled: managementLocked || configTextBusy,
|
|
31637
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31084
31638
|
onClick: () => {
|
|
31085
31639
|
const manual = configEndpoint.trim();
|
|
31086
31640
|
setConfigTextBusy(true);
|
|
@@ -31093,7 +31647,7 @@ window.__ModuleLoader__.load({
|
|
|
31093
31647
|
}] }
|
|
31094
31648
|
}).then((value) => {
|
|
31095
31649
|
setConfigText(value);
|
|
31096
|
-
|
|
31650
|
+
notify("configCreated");
|
|
31097
31651
|
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
31098
31652
|
},
|
|
31099
31653
|
children: t("newConfigText")
|
|
@@ -31102,21 +31656,34 @@ window.__ModuleLoader__.load({
|
|
|
31102
31656
|
className: "daw-command daw-inline-command",
|
|
31103
31657
|
disabled: configTextBusy || tunnelBusy,
|
|
31104
31658
|
onClick: () => {
|
|
31105
|
-
|
|
31106
|
-
else createTunnel();
|
|
31659
|
+
tunnelConfigClick();
|
|
31107
31660
|
},
|
|
31108
|
-
children: tunnel.running
|
|
31661
|
+
children: tunnel.running !== true ? t("tunnelStartAndGenConfig") : t("tunnelRestartAndGenConfig")
|
|
31109
31662
|
}),
|
|
31110
31663
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31111
31664
|
className: "daw-list-meta daw-listener-hint",
|
|
31112
31665
|
children: t("configTextScopeNote")
|
|
31113
31666
|
})
|
|
31114
31667
|
] }),
|
|
31115
|
-
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
31116
|
-
className: "daw-code",
|
|
31117
|
-
children:
|
|
31118
|
-
|
|
31119
|
-
|
|
31668
|
+
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31669
|
+
className: "daw-code-row",
|
|
31670
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31671
|
+
className: "daw-code",
|
|
31672
|
+
children: pairing.code
|
|
31673
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
31674
|
+
className: "daw-copy-inline",
|
|
31675
|
+
text: pairing.code,
|
|
31676
|
+
title: t("copyPairing"),
|
|
31677
|
+
copiedText: t("copied"),
|
|
31678
|
+
manualText: t("copyManual"),
|
|
31679
|
+
copyText
|
|
31680
|
+
})]
|
|
31681
|
+
}), pairing.expiresAt !== void 0 && pairing.expiresAt > now && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31682
|
+
className: "daw-list-meta",
|
|
31683
|
+
style: { marginTop: -4 },
|
|
31684
|
+
children: t("pairingExpiresAt", { minutes: pairingMinutesLeft(pairing.expiresAt, now) })
|
|
31685
|
+
})] }),
|
|
31686
|
+
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31120
31687
|
style: {
|
|
31121
31688
|
marginTop: 12,
|
|
31122
31689
|
maxWidth: 560
|
|
@@ -31126,6 +31693,7 @@ window.__ModuleLoader__.load({
|
|
|
31126
31693
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("configTextManual") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31127
31694
|
className: "daw-input",
|
|
31128
31695
|
value: configEndpoint,
|
|
31696
|
+
disabled: managementLocked,
|
|
31129
31697
|
spellCheck: false,
|
|
31130
31698
|
placeholder: t("configTextManualPlaceholder"),
|
|
31131
31699
|
onChange: (event) => setConfigEndpoint(event.target.value)
|
|
@@ -31152,7 +31720,8 @@ window.__ModuleLoader__.load({
|
|
|
31152
31720
|
device,
|
|
31153
31721
|
roots,
|
|
31154
31722
|
onChanged: refresh,
|
|
31155
|
-
onError: setError
|
|
31723
|
+
onError: setError,
|
|
31724
|
+
onNotice: notify
|
|
31156
31725
|
}, device.id))
|
|
31157
31726
|
})
|
|
31158
31727
|
}, "devices"),
|
|
@@ -31256,7 +31825,10 @@ window.__ModuleLoader__.load({
|
|
|
31256
31825
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31257
31826
|
className: "daw-list-meta",
|
|
31258
31827
|
children: t("pluginTrashHint")
|
|
31259
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31828
|
+
}), trash.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31829
|
+
className: "daw-list-meta daw-empty",
|
|
31830
|
+
children: t("trashEmpty")
|
|
31831
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31260
31832
|
className: "daw-list",
|
|
31261
31833
|
children: trash.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31262
31834
|
className: "daw-list-row",
|
|
@@ -31275,14 +31847,22 @@ window.__ModuleLoader__.load({
|
|
|
31275
31847
|
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31276
31848
|
className: "daw-command",
|
|
31277
31849
|
onClick: () => {
|
|
31278
|
-
|
|
31850
|
+
setError(void 0);
|
|
31851
|
+
props.api.restoreTrash(item.id).then(() => {
|
|
31852
|
+
notify("trashRestored");
|
|
31853
|
+
return refresh();
|
|
31854
|
+
}).catch(setError);
|
|
31279
31855
|
},
|
|
31280
31856
|
children: t("restore")
|
|
31281
31857
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31282
31858
|
className: "daw-icon danger",
|
|
31283
31859
|
title: t("deletePermanently"),
|
|
31860
|
+
"aria-label": t("deletePermanently"),
|
|
31284
31861
|
onClick: () => {
|
|
31285
|
-
if (confirm(t("purgeConfirm"))) props.api.purgeTrash(item.id).then(
|
|
31862
|
+
if (confirm(t("purgeConfirm"))) props.api.purgeTrash(item.id).then(() => {
|
|
31863
|
+
notify("trashPurged");
|
|
31864
|
+
return refresh();
|
|
31865
|
+
}).catch(setError);
|
|
31286
31866
|
},
|
|
31287
31867
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31288
31868
|
})] })]
|
|
@@ -31295,9 +31875,12 @@ window.__ModuleLoader__.load({
|
|
|
31295
31875
|
descKey: tabDescKey("audit"),
|
|
31296
31876
|
open: isOpen("audit"),
|
|
31297
31877
|
onToggle: () => toggleCard("audit"),
|
|
31298
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31878
|
+
children: audit.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31879
|
+
className: "daw-list-meta daw-empty",
|
|
31880
|
+
children: t("auditEmpty")
|
|
31881
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31299
31882
|
className: "daw-list",
|
|
31300
|
-
children: audit.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31883
|
+
children: audit.slice(0, auditLimit).map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31301
31884
|
className: "daw-list-row",
|
|
31302
31885
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31303
31886
|
className: "daw-list-title",
|
|
@@ -31315,7 +31898,18 @@ window.__ModuleLoader__.load({
|
|
|
31315
31898
|
]
|
|
31316
31899
|
})] })
|
|
31317
31900
|
}, String(item.id ?? index)))
|
|
31318
|
-
})
|
|
31901
|
+
}), auditLimit < audit.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31902
|
+
className: "daw-command daw-inline-command",
|
|
31903
|
+
disabled: auditLoading,
|
|
31904
|
+
onClick: () => {
|
|
31905
|
+
setAuditLoading(true);
|
|
31906
|
+
window.setTimeout(() => {
|
|
31907
|
+
setAuditLimit((value) => value + 50);
|
|
31908
|
+
setAuditLoading(false);
|
|
31909
|
+
}, 0);
|
|
31910
|
+
},
|
|
31911
|
+
children: auditLoading ? t("auditLoading") : t("auditLoadMore")
|
|
31912
|
+
})] })
|
|
31319
31913
|
}, "audit"),
|
|
31320
31914
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {
|
|
31321
31915
|
open: isOpen("app-download"),
|
|
@@ -31375,13 +31969,6 @@ window.__ModuleLoader__.load({
|
|
|
31375
31969
|
});
|
|
31376
31970
|
}
|
|
31377
31971
|
/**
|
|
31378
|
-
* Standalone settings overlay kept for the `/dsh-workspace` page (mobile WebView / direct link).
|
|
31379
|
-
*
|
|
31380
|
-
* The desktop WebUI does **not** use this: its settings live inline inside DSH's own settings page
|
|
31381
|
-
* (`PhoneSettingsBody`), so a user never opens a dialog from inside a dialog. This one keeps its own
|
|
31382
|
-
* shell because the standalone page has no settings container to render into.
|
|
31383
|
-
*/
|
|
31384
|
-
/**
|
|
31385
31972
|
* Shows a freshly minted `DSH1:` config text.
|
|
31386
31973
|
*
|
|
31387
31974
|
* The text is a credential carrier, so this panel keeps the "it is a key" reminder visible and lists
|
|
@@ -31512,6 +32099,8 @@ window.__ModuleLoader__.load({
|
|
|
31512
32099
|
const [scopes, setScopes] = (0, react.useState)(props.device.scopes);
|
|
31513
32100
|
const [rootIds, setRootIds] = (0, react.useState)(props.device.rootIds);
|
|
31514
32101
|
const toggle = (list, value) => list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
|
|
32102
|
+
const shortId = props.device.id.length > 14 ? `${props.device.id.slice(0, 8)}…${props.device.id.slice(-4)}` : props.device.id;
|
|
32103
|
+
const lastSeen = props.device.lastSeenAt === void 0 ? t("deviceNeverSeen") : t("deviceLastSeen", { time: new Date(props.device.lastSeenAt).toLocaleString(locale) });
|
|
31515
32104
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31516
32105
|
className: "daw-list-row",
|
|
31517
32106
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
@@ -31529,7 +32118,12 @@ window.__ModuleLoader__.load({
|
|
|
31529
32118
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31530
32119
|
className: "daw-list-meta",
|
|
31531
32120
|
children: [
|
|
31532
|
-
|
|
32121
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32122
|
+
title: props.device.id,
|
|
32123
|
+
children: shortId
|
|
32124
|
+
}),
|
|
32125
|
+
" · ",
|
|
32126
|
+
lastSeen,
|
|
31533
32127
|
" · ",
|
|
31534
32128
|
props.device.scopes.length === 0 ? t("noScopes") : props.device.scopes.map((scope) => scopeLabel(locale, scope)).join(" · ")
|
|
31535
32129
|
]
|
|
@@ -31559,7 +32153,10 @@ window.__ModuleLoader__.load({
|
|
|
31559
32153
|
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31560
32154
|
className: "daw-command primary",
|
|
31561
32155
|
onClick: () => {
|
|
31562
|
-
props.api.updateDevice(props.device.id, scopes, rootIds).then(props.onChanged).then(() =>
|
|
32156
|
+
props.api.updateDevice(props.device.id, scopes, rootIds).then(props.onChanged).then(() => {
|
|
32157
|
+
setEditing(false);
|
|
32158
|
+
props.onNotice("permissionsSaved");
|
|
32159
|
+
}).catch(props.onError);
|
|
31563
32160
|
},
|
|
31564
32161
|
children: t("save")
|
|
31565
32162
|
}),
|
|
@@ -31571,8 +32168,10 @@ window.__ModuleLoader__.load({
|
|
|
31571
32168
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31572
32169
|
className: "daw-icon danger",
|
|
31573
32170
|
title: t("revokeDevice"),
|
|
32171
|
+
"aria-label": t("revokeDevice"),
|
|
31574
32172
|
onClick: () => {
|
|
31575
|
-
|
|
32173
|
+
if (!window.confirm(t("revokeConfirm", { name: props.device.name }))) return;
|
|
32174
|
+
props.api.revokeDevice(props.device.id).then(props.onChanged).then(() => props.onNotice("deviceRevoked")).catch(props.onError);
|
|
31576
32175
|
},
|
|
31577
32176
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31578
32177
|
})
|
|
@@ -31584,6 +32183,7 @@ window.__ModuleLoader__.load({
|
|
|
31584
32183
|
if (tab === "devices") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Smartphone, { size: 15 });
|
|
31585
32184
|
if (tab === "image") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Image, { size: 15 });
|
|
31586
32185
|
if (tab === "trash") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 });
|
|
32186
|
+
if (tab === "audit") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.ScrollText, { size: 15 });
|
|
31587
32187
|
if (tab === "remote") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Upload, { size: 15 });
|
|
31588
32188
|
if (tab === "app-download") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Download, { size: 15 });
|
|
31589
32189
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.File, { size: 15 });
|
|
@@ -31614,6 +32214,10 @@ window.__ModuleLoader__.load({
|
|
|
31614
32214
|
if (error.code === "LISTENER_PORT_INVALID") return "listenerPortInvalid";
|
|
31615
32215
|
if (error.code === "REMOTE_ENABLED") return "disableToEditListener";
|
|
31616
32216
|
if (error.code === "NO_ENDPOINT") return "configTextNoEndpoints";
|
|
32217
|
+
if (error.code === "TUNNEL_MODE_ADMIN_DISABLED") return "errorTunnelModeAdminDisabled";
|
|
32218
|
+
if (error.code === "ADMIN_PROXY_FORWARDED") return "errorAdminProxyForwarded";
|
|
32219
|
+
if (error.code === "ADMIN_HOST_NOT_LOOPBACK") return "errorAdminHostNotLoopback";
|
|
32220
|
+
if (error.code === "ADMIN_LOOPBACK_REQUIRED") return "errorAdminLoopbackRequired";
|
|
31617
32221
|
if (error.code.includes("PATH") || error.code.includes("SYMLINK") || error.code.includes("REPARSE")) return "errorPathInvalid";
|
|
31618
32222
|
if (error.status === 403) return "errorForbidden";
|
|
31619
32223
|
if (error.status === 404) return "errorNotFound";
|