dsh-remote-control 1.2.0 → 2.0.1

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.
@@ -36,6 +36,7 @@ module.exports = __toCommonJS(main_exports);
36
36
  var STATUS_ROUTE = "/plugins/dsh-remote-control/status";
37
37
  var NEW_ROUTE = "/plugins/dsh-remote-control/pairing/new";
38
38
  var IMAGE_ROUTE = "/plugins/dsh-remote-control/pairing.png";
39
+ var UNPAIR_ROUTE = "/plugins/dsh-remote-control/unpair";
39
40
  var SLOT_NAME = "conversation.composer.dock";
40
41
  var PILL_ID = "dsh-remote-control";
41
42
  var PILL_ORDER = 20;
@@ -65,34 +66,58 @@ function panelViewFor(answer, httpStatus) {
65
66
  function pillLabel(status) {
66
67
  const relay = status?.relay;
67
68
  if (relay === "idle") return { text: "\u8FDC\u7A0B\u672A\u542F\u52A8", tone: "off" };
68
- if (relay === "offline") return { text: "\u8FDC\u7A0B\u672A\u8FDE\u63A5", tone: "off" };
69
+ if (relay === "offline") return { text: "\u5DF2\u65AD\u5F00\u8FDE\u63A5", tone: "error" };
69
70
  if (relay === "connecting") return { text: "\u8FDE\u63A5\u4E2D", tone: "wait" };
70
71
  if (relay !== "online") return { text: "\u8FDC\u7A0B\u63A7\u5236", tone: "off" };
71
72
  const paired = typeof status?.paired === "number" ? status.paired : 0;
72
- return paired > 0 ? { text: "\u5DF2\u914D\u5BF9", tone: "on" } : { text: "\u672A\u914D\u5BF9", tone: "off" };
73
+ if (paired > 0) {
74
+ const waiting = waitingCount(status);
75
+ if (waiting > 0) return { text: `\u7B49 ${waiting} \u4EF6\u4E8B`, tone: "wait" };
76
+ return { text: "\u5DF2\u914D\u5BF9", tone: "on" };
77
+ }
78
+ if (pairingCount(status) > 0) return { text: "\u624B\u673A\u79BB\u7EBF", tone: "wait" };
79
+ return { text: "\u672A\u914D\u5BF9", tone: "off" };
80
+ }
81
+ function waitingCount(status) {
82
+ const count = status?.waiting;
83
+ return typeof count === "number" && count > 0 ? Math.min(Math.floor(count), 99) : 0;
84
+ }
85
+ function pairingCount(status) {
86
+ const count = status?.pairings;
87
+ return typeof count === "number" ? count : 0;
88
+ }
89
+ function humanDuration(sec) {
90
+ const total = typeof sec === "number" && sec > 0 ? Math.floor(sec) : 0;
91
+ if (total < 60) return `${total} \u79D2`;
92
+ const minutes = Math.floor(total / 60);
93
+ return minutes >= 10 ? `${minutes} \u5206` : `${minutes} \u5206 ${total % 60} \u79D2`;
94
+ }
95
+ function waitingRow(status) {
96
+ const count = waitingCount(status);
97
+ if (count === 0) return "";
98
+ const oldest = typeof status?.waitingOldestSec === "number" ? status.waitingOldestSec : 0;
99
+ return `${count} \u4EF6${oldest > 0 ? ` \xB7 \u6700\u4E45 ${humanDuration(oldest)}` : ""}`.slice(0, 40);
73
100
  }
74
- var RELAY_TEXT = {
75
- online: "\u5DF2\u8FDE\u63A5",
76
- connecting: "\u8FDE\u63A5\u4E2D",
77
- offline: "\u672A\u8FDE\u63A5",
78
- idle: "\u672A\u542F\u52A8"
79
- };
80
101
  function relayAddress(url) {
81
102
  if (typeof url !== "string" || url.trim() === "") return "";
82
103
  const withoutScheme = url.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
83
104
  const authority = /^[^/?]*/.exec(withoutScheme)?.[0] ?? "";
84
105
  return authority.slice(0, 40);
85
106
  }
86
- function infoRows(status) {
87
- const rows = [];
88
- const label = typeof status?.hostLabel === "string" ? status.hostLabel.trim() : "";
89
- if (label !== "") rows.push(["\u672C\u673A", label]);
90
- const server = relayAddress(status?.serverUrl);
91
- if (server !== "") rows.push(["\u4E2D\u7EE7", server]);
92
- const relay = typeof status?.relay === "string" ? status.relay : "";
93
- rows.push(["\u72B6\u6001", RELAY_TEXT[relay] ?? "\u672A\u77E5"]);
94
- rows.push(["\u5DF2\u914D\u5BF9", `${typeof status?.paired === "number" ? status.paired : 0} \u53F0`]);
95
- return rows;
107
+ function relayRow(status) {
108
+ return relayAddress(status?.serverUrl);
109
+ }
110
+ function relayStateRow(status) {
111
+ const relay = status?.relay;
112
+ if (relay === "online") return "\u5DF2\u8FDE\u63A5";
113
+ if (relay === "connecting") return "\u8FDE\u63A5\u4E2D";
114
+ if (relay === "offline") return "\u5DF2\u65AD\u5F00";
115
+ if (relay === "idle") return "\u672A\u542F\u52A8";
116
+ return "";
117
+ }
118
+ function versionRow(status) {
119
+ const version = status?.version;
120
+ return typeof version === "string" && version.trim() !== "" ? version.trim().slice(0, 24) : "";
96
121
  }
97
122
  function secondsText(ms) {
98
123
  const whole = Math.max(0, Math.round(ms / 1e3));
@@ -112,24 +137,36 @@ var CSS = `
112
137
  .drc-dot { width: 6px; height: 6px; flex: 0 0 auto; border-radius: 50%; background: var(--dsw-alias-label-dimmed, #9aa0a6); }
113
138
  .drc-dot[data-tone="wait"] { background: var(--dsw-alias-state-warn-primary, #faad14); }
114
139
  .drc-dot[data-tone="on"] { background: var(--dsw-alias-state-success-primary, #52c41a); }
140
+ .drc-dot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #ff4d4f); }
115
141
  .drc-panel { position: absolute; bottom: calc(100% + 8px); right: 0; z-index: 30; width: 240px;
116
- padding: 12px 14px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1)); border-radius: 12px;
142
+ padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1)); border-radius: 12px;
117
143
  background: var(--dsw-specific-tip, var(--dsw-alias-bg-layer-1, #ffffff));
118
144
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.06);
119
145
  color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
120
- /* \u8FDE\u63A5\u4FE1\u606F\u90A3\u4E00\u680F\uFF1A\u952E\u5DE6\u503C\u53F3\uFF0C\u503C\u88AB\u622A\u65AD\u65F6\u7701\u7565\u53F7\u2014\u2014\u548C\u90A3\u9897 pill \u540C\u4E00\u5957"\u6324\u4E0D\u4E0B\u5C31\u622A\u6587\u5B57\u4E0D\u6491\u5BB9\u5668"\u3002 */
121
- .drc-info { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px;
122
- padding-bottom: 6px; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08)); }
123
- .drc-row { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
146
+ /* \u62AC\u5934\u4E00\u884C\uFF1A\u5DE6\u4FA7\u72B6\u6001\uFF0C\u53F3\u4FA7\u52A8\u4F5C\u3002\u4E24\u9897\u6309\u94AE\u90FD\u6536\u5728\u8FD9\u4E2A\u53F3\u4E0A\u89D2\uFF082026-10-03 \u91CD\u8BBE\u8BA1\u7684\u8981\u6C42\uFF09\u3002 */
147
+ .drc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
148
+ .drc-head-label { font-weight: 600; }
149
+ .drc-head-label[data-tone="off"] { color: var(--dsw-alias-label-secondary, #6b7280); }
150
+ .drc-head-label[data-tone="wait"] { color: var(--dsw-alias-state-warn-primary, #faad14); }
151
+ .drc-head-label[data-tone="on"] { color: var(--dsw-alias-state-success-primary, #52c41a); }
152
+ .drc-head-label[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #ff4d4f); }
153
+ .drc-actions { display: flex; gap: 6px; flex: 0 0 auto; }
154
+ .drc-btn { padding: 2px 10px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
155
+ border-radius: 8px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer;
156
+ white-space: nowrap; }
157
+ .drc-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04)); }
158
+ /* \u6B63\u6587\u90A3\u4E09\u884C\uFF08\u4E2D\u7EE7 / \u72B6\u6001 / \u7248\u672C\uFF09\uFF1A\u952E\u5DE6\u503C\u53F3\uFF08space-between\uFF09\uFF0C\u503C\u88AB\u622A\u65AD\u65F6\u7701\u7565\u53F7\u2014\u2014
159
+ \u548C\u90A3\u9897 pill \u540C\u4E00\u5957"\u6324\u4E0D\u4E0B\u5C31\u622A\u6587\u5B57\u4E0D\u6491\u5BB9\u5668"\u3002\u7B2C\u4E00\u884C\u5E26\u90A3\u6761\u5206\u9694\u7EBF\uFF0C\u540E\u4E24\u884C\u53EA\u7559\u884C\u8DDD\u3002 */
160
+ .drc-info { display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
161
+ white-space: nowrap; margin-top: 8px; padding-top: 6px;
162
+ border-top: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08)); }
163
+ .drc-info + .drc-info { margin-top: 2px; padding-top: 0; border-top: 0; }
124
164
  .drc-key { flex: 0 0 auto; color: var(--dsw-alias-label-tertiary); }
125
- .drc-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: right; }
126
- .drc-qr { display: block; width: 200px; height: 200px; margin: 0 auto; image-rendering: pixelated; }
165
+ .drc-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
166
+ .drc-qr { display: block; width: 200px; height: 200px; margin: 8px auto 0; image-rendering: pixelated; }
127
167
  .drc-code { margin: 8px 0 0; text-align: center; font-weight: 600; font-size: 14px; letter-spacing: 2px; }
128
168
  .drc-note { margin: 4px 0 0; text-align: center; color: var(--dsw-alias-label-tertiary); }
129
169
  .drc-note[data-kind="failed"] { color: var(--dsw-alias-state-error-primary, #ff4d4f); }
130
- .drc-actions { display: flex; gap: 6px; justify-content: center; margin-top: 8px; }
131
- .drc-btn { padding: 2px 10px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
132
- border-radius: 8px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer; }
133
170
  `;
134
171
  function isSlots(value) {
135
172
  if (!value || typeof value !== "object") return false;
@@ -168,7 +205,11 @@ function warn(message, error) {
168
205
  }
169
206
  function ensureStyle(doc) {
170
207
  try {
171
- if (doc.querySelector(`style[data-plugin-css="${STYLE_ID}"]`)) return;
208
+ const existing = doc.querySelector(`style[data-plugin-css="${STYLE_ID}"]`);
209
+ if (existing) {
210
+ if (existing.textContent !== CSS) existing.textContent = CSS;
211
+ return;
212
+ }
172
213
  const element = doc.createElement("style");
173
214
  element.setAttribute("data-plugin-css", STYLE_ID);
174
215
  element.textContent = CSS;
@@ -200,7 +241,7 @@ function buildPill(root, deps) {
200
241
  let expiresInMs = 0;
201
242
  let lastStatus;
202
243
  let disposed = false;
203
- const pairedNow = () => typeof lastStatus?.paired === "number" ? lastStatus.paired : 0;
244
+ const pairingNow = () => pairingCount(lastStatus);
204
245
  const write = () => {
205
246
  const next = pillLabel(lastStatus);
206
247
  label.textContent = next.text;
@@ -222,59 +263,68 @@ function buildPill(root, deps) {
222
263
  node.addEventListener("click", onClick);
223
264
  return node;
224
265
  };
225
- const infoBlock = () => {
226
- if (!lastStatus) return void 0;
227
- const box = doc.createElement("div");
228
- box.className = "drc-info";
229
- for (const [key, value] of infoRows(lastStatus)) {
266
+ const qrNote = () => expiresInMs > 0 ? `\u626B\u7801\u914D\u5BF9 \xB7 ${secondsText(expiresInMs)}\u540E\u8FC7\u671F` : "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u70B9\u53F3\u4E0A\u89D2\u300C\u5237\u65B0\u300D\u91CD\u65B0\u751F\u6210";
267
+ const header = () => {
268
+ const head = doc.createElement("div");
269
+ head.className = "drc-head";
270
+ const state = pillLabel(lastStatus);
271
+ const stateText = text("span", "drc-head-label", state.text);
272
+ stateText.setAttribute("data-tone", state.tone);
273
+ head.appendChild(stateText);
274
+ const actions = doc.createElement("div");
275
+ actions.className = "drc-actions";
276
+ const action = pairingNow() > 0 ? { label: "\u9000\u51FA\u914D\u5BF9", run: requestUnpair } : { label: "\u5237\u65B0", run: requestPairing };
277
+ actions.appendChild(buttonOf(action.label, () => void action.run()));
278
+ head.appendChild(actions);
279
+ return head;
280
+ };
281
+ const offlineNote = () => {
282
+ if (!lastStatus) return null;
283
+ if (pillLabel(lastStatus).text !== "\u624B\u673A\u79BB\u7EBF") return null;
284
+ return text("p", "drc-note", "\u914D\u5BF9\u8FD8\u5728\uFF1A\u5C0F\u7A0B\u5E8F\u56DE\u524D\u53F0\u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF0C\u4E0D\u7528\u91CD\u65B0\u626B\u7801\u3002");
285
+ };
286
+ const infoRows = () => {
287
+ if (!lastStatus) return [];
288
+ const rows = [
289
+ // 排第一:它是唯一一条"看完要做点什么"的,其余三条都是"知道就行"。
290
+ ["\u5F85\u5904\u7406", waitingRow(lastStatus)],
291
+ ["\u4E2D\u7EE7", relayRow(lastStatus)],
292
+ ["\u72B6\u6001", relayStateRow(lastStatus)],
293
+ ["\u7248\u672C", versionRow(lastStatus)]
294
+ ];
295
+ return rows.filter(([, value]) => value !== "").map(([key, value]) => {
230
296
  const row = doc.createElement("div");
231
- row.className = "drc-row";
297
+ row.className = "drc-info";
232
298
  row.appendChild(text("span", "drc-key", key));
233
299
  row.appendChild(text("span", "drc-value", value));
234
- box.appendChild(row);
235
- }
236
- return box;
300
+ return row;
301
+ });
237
302
  };
238
303
  const paint = () => {
239
304
  if (!panel) return;
240
305
  panel.replaceChildren();
241
- const info = infoBlock();
242
- if (info) panel.appendChild(info);
243
- if (view.kind === "info") {
244
- const actions2 = doc.createElement("div");
245
- actions2.className = "drc-actions";
246
- actions2.appendChild(buttonOf(pairedNow() > 0 ? "\u518D\u914D\u4E00\u53F0" : "\u751F\u6210\u914D\u5BF9\u7801", () => void requestPairing()));
247
- panel.appendChild(actions2);
248
- return;
249
- }
306
+ panel.appendChild(header());
250
307
  if (view.kind === "loading") {
251
308
  panel.appendChild(text("p", "drc-note", "\u6B63\u5728\u751F\u6210\u914D\u5BF9\u7801\u2026"));
252
- return;
253
- }
254
- if (view.kind === "qr") {
309
+ } else if (view.kind === "qr") {
255
310
  const image = doc.createElement("img");
256
311
  image.className = "drc-qr";
257
312
  image.setAttribute("alt", "\u914D\u5BF9\u4E8C\u7EF4\u7801");
258
313
  image.src = view.imageSrc;
259
314
  panel.appendChild(image);
260
315
  panel.appendChild(text("p", "drc-code", view.token));
261
- panel.appendChild(text("p", "drc-note", `\u626B\u7801\u914D\u5BF9 \xB7 ${secondsText(expiresInMs)}\u540E\u8FC7\u671F`));
262
- const actions2 = doc.createElement("div");
263
- actions2.className = "drc-actions";
264
- actions2.appendChild(buttonOf("\u6362\u4E00\u5F20", () => void requestPairing()));
265
- panel.appendChild(actions2);
266
- return;
316
+ panel.appendChild(text("p", "drc-note", qrNote()));
317
+ } else if (view.kind === "unavailable" || view.kind === "failed") {
318
+ const note2 = view.kind === "unavailable" ? text("p", "drc-note", view.reason) : (() => {
319
+ const node = text("p", "drc-note", `\u914D\u5BF9\u8BF7\u6C42\u6CA1\u6210\u529F\uFF1A${view.detail}`);
320
+ node.setAttribute("data-kind", "failed");
321
+ return node;
322
+ })();
323
+ panel.appendChild(note2);
267
324
  }
268
- const note = view.kind === "unavailable" ? text("p", "drc-note", view.reason) : (() => {
269
- const node = text("p", "drc-note", `\u914D\u5BF9\u8BF7\u6C42\u6CA1\u6210\u529F\uFF1A${view.detail}`);
270
- node.setAttribute("data-kind", "failed");
271
- return node;
272
- })();
273
- panel.appendChild(note);
274
- const actions = doc.createElement("div");
275
- actions.className = "drc-actions";
276
- actions.appendChild(buttonOf("\u518D\u8BD5\u4E00\u6B21", () => void requestPairing()));
277
- panel.appendChild(actions);
325
+ for (const row of infoRows()) panel.appendChild(row);
326
+ const note = offlineNote();
327
+ if (note) panel.appendChild(note);
278
328
  };
279
329
  const closePanel = () => {
280
330
  if (countTimer !== void 0) {
@@ -313,24 +363,49 @@ function buildPill(root, deps) {
313
363
  if (countTimer === void 0) {
314
364
  countTimer = setInterval(() => {
315
365
  expiresInMs -= 1e3;
316
- if (expiresInMs <= 0) void requestPairing();
317
- else paint();
366
+ if (expiresInMs <= 0) {
367
+ clearInterval(countTimer);
368
+ countTimer = void 0;
369
+ }
370
+ paint();
318
371
  }, 1e3);
319
372
  }
320
373
  }
321
374
  paint();
322
375
  }
376
+ async function requestUnpair() {
377
+ if (disposed || !panel) return;
378
+ try {
379
+ await deps.fetchImpl(UNPAIR_ROUTE, {
380
+ method: "POST",
381
+ headers: { "x-drc-pair": "1" },
382
+ credentials: "same-origin"
383
+ });
384
+ } catch (error) {
385
+ warn("\u9000\u51FA\u914D\u5BF9\u8BF7\u6C42\u5931\u8D25\uFF08\u9762\u677F\u4F1A\u9760\u4E0B\u4E00\u6B21\u8F6E\u8BE2\u5BF9\u8D26\uFF09", error);
386
+ }
387
+ if (disposed || !panel) return;
388
+ if (lastStatus) lastStatus = { ...lastStatus, paired: 0, pairings: 0 };
389
+ write();
390
+ view = { kind: "loading" };
391
+ paint();
392
+ void requestPairing();
393
+ }
323
394
  const openPanel = () => {
324
395
  if (panel || disposed) return;
325
396
  panel = doc.createElement("div");
326
397
  panel.className = "drc-panel";
327
398
  panel.setAttribute("role", "dialog");
328
- panel.setAttribute("aria-label", "dsh-remote-control \u8FDE\u63A5\u4FE1\u606F");
399
+ panel.setAttribute("aria-label", "dsh-remote-control \u914D\u5BF9");
329
400
  panel.tabIndex = -1;
330
401
  root.appendChild(panel);
331
402
  button.setAttribute("aria-expanded", "true");
332
- view = { kind: "info" };
333
- paint();
403
+ if (pairingNow() > 0) {
404
+ view = { kind: "info" };
405
+ paint();
406
+ } else {
407
+ void requestPairing();
408
+ }
334
409
  try {
335
410
  panel.focus();
336
411
  } catch {
@@ -367,6 +442,14 @@ function buildPill(root, deps) {
367
442
  const body = await response.json?.();
368
443
  if (!body || typeof body !== "object") return;
369
444
  lastStatus = body;
445
+ if (view.kind === "qr" && pairingNow() > 0) {
446
+ view = { kind: "info" };
447
+ if (countTimer !== void 0) {
448
+ clearInterval(countTimer);
449
+ countTimer = void 0;
450
+ }
451
+ if (panel) paint();
452
+ }
370
453
  write();
371
454
  } catch (error) {
372
455
  deps.warn?.("status poll failed");