dsh-connect-workbuddy 4.0.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/docs/WINDOWS.md +38 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +1130 -15
- package/lib/{host-heartbeat-vvVjdY_q.js → host-heartbeat-jNlsUMZO.js} +610 -26
- package/lib/index.d.ts +297 -2
- package/lib/index.js +794 -11
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -70,12 +70,47 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
* splitting them would duplicate the guards and invite them to drift apart.
|
|
71
71
|
*/
|
|
72
72
|
const WORKBUDDY_POOL_PATH = "/plugins/dsh-connect-workbuddy/pool";
|
|
73
|
+
/**
|
|
74
|
+
* Plugin-owned OAuth sign-in endpoint: one session step per call.
|
|
75
|
+
*
|
|
76
|
+
* `?action=start` asks the upstream for a login state and returns the URL to
|
|
77
|
+
* open; `?action=poll` (+ `loginId` in the body) reports whether the user
|
|
78
|
+
* finished, and on success persists the account into the vault. POST +
|
|
79
|
+
* loopback-only like the card's other mutations: the poll carries the login
|
|
80
|
+
* binding, and the Host-side session state must not be settable by another
|
|
81
|
+
* origin.
|
|
82
|
+
*/
|
|
83
|
+
const WORKBUDDY_OAUTH_PATH = "/plugins/dsh-connect-workbuddy/oauth";
|
|
73
84
|
/** Query parameter selecting which pool batch action a request means. */
|
|
74
85
|
const WORKBUDDY_POOL_ACTION_PARAM = "action";
|
|
86
|
+
/** How long the card waits between polls while the user is scanning. */
|
|
87
|
+
const OAUTH_POLL_INTERVAL_MS = 2e3;
|
|
88
|
+
/**
|
|
89
|
+
* Plugin-owned account transfer endpoint: the batch export / import of
|
|
90
|
+
* credentials between machines and between tools of the SAME format family.
|
|
91
|
+
*
|
|
92
|
+
* `?action=export` POSTs account ids, answers a JSON ARRAY of credential
|
|
93
|
+
* records; `?action=preview` POSTs file text, answers a token-free listing;
|
|
94
|
+
* `?action=import` POSTs file text plus selected indexes, merges into the
|
|
95
|
+
* vault. POST + loopback-only like every other mutation: the file content is
|
|
96
|
+
* arbitrary user data and export carries token material.
|
|
97
|
+
*/
|
|
98
|
+
const WORKBUDDY_TRANSFER_PATH = "/plugins/dsh-connect-workbuddy/transfer";
|
|
75
99
|
/** Address one region's pool endpoint with an action. */
|
|
76
100
|
function withWorkBuddyRegionAndAction(path, region, action) {
|
|
77
101
|
return `${withWorkBuddyRegion(path, region)}&${WORKBUDDY_POOL_ACTION_PARAM}=${action}`;
|
|
78
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Address one region's endpoint for ANY route that dispatches on `action`.
|
|
105
|
+
*
|
|
106
|
+
* The OAuth and transfer routes reject a request with no action (400), so
|
|
107
|
+
* their callers must build the URL here rather than with
|
|
108
|
+
* {@link withWorkBuddyRegion} — a request missing the parameter fails the same
|
|
109
|
+
* way whether the omission was a mistake or not.
|
|
110
|
+
*/
|
|
111
|
+
function withWorkBuddyRouteAction(path, region, action) {
|
|
112
|
+
return `${withWorkBuddyRegion(path, region)}&${WORKBUDDY_POOL_ACTION_PARAM}=${action}`;
|
|
113
|
+
}
|
|
79
114
|
/** Query parameter naming the region a card request addresses. */
|
|
80
115
|
const WORKBUDDY_REGION_PARAM = "region";
|
|
81
116
|
/** Every region, in card tab order. */
|
|
@@ -614,6 +649,40 @@ window.__ModuleLoader__.load({
|
|
|
614
649
|
}
|
|
615
650
|
}
|
|
616
651
|
//#endregion
|
|
652
|
+
//#region src/client/BusyOverlay.tsx
|
|
653
|
+
/**
|
|
654
|
+
* The veil. Deliberately NOT a modal dialog: it carries no focusable control
|
|
655
|
+
* and no dismissal — there is nothing for the user to decide, only a step to
|
|
656
|
+
* wait through. `aria-busy` plus a polite live region state that to screen
|
|
657
|
+
* readers, which is the whole accessibility contract a wait screen needs.
|
|
658
|
+
*/
|
|
659
|
+
function BusyOverlay({ message, hint, visible }) {
|
|
660
|
+
if (!visible) return null;
|
|
661
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
662
|
+
className: "dsm-workbuddy-veil",
|
|
663
|
+
role: "status",
|
|
664
|
+
"aria-busy": "true",
|
|
665
|
+
"aria-live": "polite",
|
|
666
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
667
|
+
className: "dsm-workbuddy-veil-card",
|
|
668
|
+
children: [
|
|
669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
670
|
+
className: "dsm-workbuddy-veil-spinner",
|
|
671
|
+
"aria-hidden": "true"
|
|
672
|
+
}),
|
|
673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
674
|
+
className: "dsm-workbuddy-veil-text",
|
|
675
|
+
children: message
|
|
676
|
+
}),
|
|
677
|
+
hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
678
|
+
className: "dsm-workbuddy-veil-hint",
|
|
679
|
+
children: hint
|
|
680
|
+
})
|
|
681
|
+
]
|
|
682
|
+
})
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
617
686
|
//#region src/client/ndjson.ts
|
|
618
687
|
/**
|
|
619
688
|
* Read a newline-delimited JSON body, one object at a time.
|
|
@@ -1091,7 +1160,7 @@ window.__ModuleLoader__.load({
|
|
|
1091
1160
|
* section implying the feature exists.
|
|
1092
1161
|
*/
|
|
1093
1162
|
function AccountPool(props) {
|
|
1094
|
-
const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount } = props;
|
|
1163
|
+
const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount, accountTools } = props;
|
|
1095
1164
|
const [draft, setDraft] = (0, react.useState)(null);
|
|
1096
1165
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
1097
1166
|
const [saveError, setSaveError] = (0, react.useState)(void 0);
|
|
@@ -1116,6 +1185,24 @@ window.__ModuleLoader__.load({
|
|
|
1116
1185
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1117
1186
|
};
|
|
1118
1187
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1188
|
+
/**
|
|
1189
|
+
* Whether the pool is RUNNING right now, which is the SAVED value.
|
|
1190
|
+
*
|
|
1191
|
+
* `active` is the user's draft and only becomes reality at Save. Using it for
|
|
1192
|
+
* the parts that DESCRIBE the running pool made the card state a mode that had
|
|
1193
|
+
* not taken effect yet: flipping the switch off (unsaved) revealed the manual
|
|
1194
|
+
* picker while the pool was still ranking every request, so the picker showed
|
|
1195
|
+
* the rotated account as "in use" and a selection made there was overridden by
|
|
1196
|
+
* the very pool the user was looking at — the "shows A, bills B" trap this
|
|
1197
|
+
* section keeps guarding against. The mirror case was milder but the same
|
|
1198
|
+
* class of wrong: turning the pool ON (unsaved) hid the picker while the manual
|
|
1199
|
+
* account was still the one being billed.
|
|
1200
|
+
*
|
|
1201
|
+
* Everything that reports pool state reads this; only the switch itself (its
|
|
1202
|
+
* controlled value) and `dirty` read the draft, because those two ARE about
|
|
1203
|
+
* the pending edit.
|
|
1204
|
+
*/
|
|
1205
|
+
const poolRunning = saved.enabled;
|
|
1119
1206
|
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1120
1207
|
/**
|
|
1121
1208
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
@@ -1367,6 +1454,118 @@ window.__ModuleLoader__.load({
|
|
|
1367
1454
|
region,
|
|
1368
1455
|
t
|
|
1369
1456
|
]);
|
|
1457
|
+
/** The account whose per-row action is in flight; gates every row's buttons. */
|
|
1458
|
+
const [rowBusyAccountId, setRowBusyAccountId] = (0, react.useState)(void 0);
|
|
1459
|
+
/**
|
|
1460
|
+
* Which per-row action is running, for the veil's scenario line.
|
|
1461
|
+
*
|
|
1462
|
+
* A removal and a test both set `rowBusyAccountId`, but they mean different
|
|
1463
|
+
* things to the person watching ("this account is going away" versus "this
|
|
1464
|
+
* account is being measured") — and the veil exists precisely to say which.
|
|
1465
|
+
*/
|
|
1466
|
+
const [rowBusyAction, setRowBusyAction] = (0, react.useState)(void 0);
|
|
1467
|
+
/**
|
|
1468
|
+
* Test ONE account from its row.
|
|
1469
|
+
*
|
|
1470
|
+
* Same Host rule as the batch (testOne reuses the batch's probe), the same
|
|
1471
|
+
* log vocabulary, and the same refresh-after — a row result must read exactly
|
|
1472
|
+
* like the batch result it replaces.
|
|
1473
|
+
*/
|
|
1474
|
+
const runRowTest = (0, react.useCallback)(async (accountId) => {
|
|
1475
|
+
setRowBusyAccountId(accountId);
|
|
1476
|
+
setRowBusyAction("test");
|
|
1477
|
+
setActionError(void 0);
|
|
1478
|
+
const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "test") + "&accountId=" + encodeURIComponent(accountId);
|
|
1479
|
+
try {
|
|
1480
|
+
const response = await fetch(url, {
|
|
1481
|
+
method: "POST",
|
|
1482
|
+
headers: { accept: "application/json" },
|
|
1483
|
+
credentials: "same-origin"
|
|
1484
|
+
});
|
|
1485
|
+
const body = await response.json().catch(() => void 0);
|
|
1486
|
+
if (!response.ok) throw new Error(poolFailureText(t, {
|
|
1487
|
+
status: response.status,
|
|
1488
|
+
reason: body?.reason,
|
|
1489
|
+
error: body?.error
|
|
1490
|
+
}));
|
|
1491
|
+
if (!mounted.current) return;
|
|
1492
|
+
const row = body?.rows?.[0];
|
|
1493
|
+
if (row === void 0) {
|
|
1494
|
+
setActionError(t("row.poolLogTestRowMalformed", { accountName: t("row.accountUnnamed") }));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const outcome = row.result?.outcome;
|
|
1498
|
+
if (outcome === void 0) appendLog(t("row.poolLogTestRowMalformed", { accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName }), "warn");
|
|
1499
|
+
else appendLog(t("row.poolLogTestRow", {
|
|
1500
|
+
accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
|
|
1501
|
+
outcome: outcomeText(t, outcome)
|
|
1502
|
+
}), outcomeOk(outcome) ? "ok" : "warn");
|
|
1503
|
+
onRefresh?.();
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
const message = error instanceof Error ? error.message : t("row.requestFailed");
|
|
1506
|
+
if (mounted.current) {
|
|
1507
|
+
setActionError(message);
|
|
1508
|
+
appendLog(t("row.poolLogBatchFailed", { message }), "error");
|
|
1509
|
+
}
|
|
1510
|
+
} finally {
|
|
1511
|
+
if (mounted.current) {
|
|
1512
|
+
setRowBusyAccountId(void 0);
|
|
1513
|
+
setRowBusyAction(void 0);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}, [
|
|
1517
|
+
appendLog,
|
|
1518
|
+
onRefresh,
|
|
1519
|
+
region,
|
|
1520
|
+
t
|
|
1521
|
+
]);
|
|
1522
|
+
/**
|
|
1523
|
+
* Remove ONE account from its row, via the Host.
|
|
1524
|
+
*
|
|
1525
|
+
* The Host dismisses the account (and deletes its vault entry when it has
|
|
1526
|
+
* one), so the row stays gone across rescans. The desktop app is not signed
|
|
1527
|
+
* out. The message below still distinguishes the two cases, because "there
|
|
1528
|
+
* was no plugin-stored copy" is worth saying: it tells the user the account
|
|
1529
|
+
* comes from the app itself rather than from an OAuth sign-in or an import.
|
|
1530
|
+
*/
|
|
1531
|
+
const runRowRemove = (0, react.useCallback)(async (accountId, accountName) => {
|
|
1532
|
+
setRowBusyAccountId(accountId);
|
|
1533
|
+
setRowBusyAction("remove");
|
|
1534
|
+
setActionError(void 0);
|
|
1535
|
+
const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "remove") + "&accountId=" + encodeURIComponent(accountId);
|
|
1536
|
+
try {
|
|
1537
|
+
const response = await fetch(url, {
|
|
1538
|
+
method: "POST",
|
|
1539
|
+
headers: { accept: "application/json" },
|
|
1540
|
+
credentials: "same-origin"
|
|
1541
|
+
});
|
|
1542
|
+
const body = await response.json().catch(() => void 0);
|
|
1543
|
+
if (!response.ok) throw new Error(poolFailureText(t, {
|
|
1544
|
+
status: response.status,
|
|
1545
|
+
reason: body?.reason,
|
|
1546
|
+
error: body?.error
|
|
1547
|
+
}));
|
|
1548
|
+
if (!mounted.current) return;
|
|
1549
|
+
appendLog(body?.removed === true ? t("row.poolRowRemoved", { accountName: accountName === "" ? t("row.accountUnnamed") : accountName }) : t("row.poolRowRemoveDesktopOnly", { accountName: accountName === "" ? t("row.accountUnnamed") : accountName }), body?.removed === true ? "ok" : "info");
|
|
1550
|
+
await onRefresh?.();
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
const message = error instanceof Error ? error.message : t("row.requestFailed");
|
|
1553
|
+
if (mounted.current) {
|
|
1554
|
+
setActionError(message);
|
|
1555
|
+
appendLog(t("row.poolLogBatchFailed", { message }), "error");
|
|
1556
|
+
}
|
|
1557
|
+
} finally {
|
|
1558
|
+
if (mounted.current) {
|
|
1559
|
+
setRowBusyAccountId(void 0);
|
|
1560
|
+
setRowBusyAction(void 0);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}, [
|
|
1564
|
+
appendLog,
|
|
1565
|
+
onRefresh,
|
|
1566
|
+
region,
|
|
1567
|
+
t
|
|
1568
|
+
]);
|
|
1370
1569
|
if (pool === void 0) return null;
|
|
1371
1570
|
/**
|
|
1372
1571
|
* Where the target model came from, and which model it is.
|
|
@@ -1390,7 +1589,15 @@ window.__ModuleLoader__.load({
|
|
|
1390
1589
|
*/
|
|
1391
1590
|
const currentAccount = pool.accounts.find((account) => account.current);
|
|
1392
1591
|
const currentName = currentAccount === void 0 ? void 0 : currentAccount.accountName === "" ? t("row.accountUnnamed") : currentAccount.accountName;
|
|
1393
|
-
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" },
|
|
1592
|
+
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, rowBusyAction === "remove" ? (0, react.createElement)(BusyOverlay, {
|
|
1593
|
+
visible: true,
|
|
1594
|
+
message: t("row.poolRowRemoving"),
|
|
1595
|
+
hint: t("row.veilHintList")
|
|
1596
|
+
}) : null, saving ? (0, react.createElement)(BusyOverlay, {
|
|
1597
|
+
visible: true,
|
|
1598
|
+
message: t("row.poolSaving"),
|
|
1599
|
+
hint: t("row.veilHintList")
|
|
1600
|
+
}) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")), currentName === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-badge" }, t("row.poolCurrentHeader", { account: currentName }))), !poolRunning && onSelectAccount !== void 0 && pool.accounts.length > 0 ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolManualAccountLabel")), (0, react.createElement)("span", null, t("row.poolManualAccountHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
|
|
1394
1601
|
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
|
|
1395
1602
|
value: currentAccount?.accountId ?? "",
|
|
1396
1603
|
disabled: selectingAccount === true,
|
|
@@ -1423,7 +1630,7 @@ window.__ModuleLoader__.load({
|
|
|
1423
1630
|
className: "dsm-btn dsm-btn-outline",
|
|
1424
1631
|
disabled: rescanning === true,
|
|
1425
1632
|
onClick: onRescan
|
|
1426
|
-
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !
|
|
1633
|
+
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !poolRunning ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, poolRunning && usable.length === 0 ? (0, react.createElement)("p", {
|
|
1427
1634
|
className: "dsm-workbuddy-pool-warn",
|
|
1428
1635
|
role: "status"
|
|
1429
1636
|
}, `${t("row.poolNoCandidate")} ${t("row.poolNoCandidateHint")}`) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-head" }, (0, react.createElement)("div", null, (0, react.createElement)("h4", { className: "dsm-workbuddy-pool-members-title" }, t("row.poolMembersTitle")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolMembersHint"))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-actions" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolSelectedCount", {
|
|
@@ -1439,13 +1646,22 @@ window.__ModuleLoader__.load({
|
|
|
1439
1646
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
1440
1647
|
disabled: effectiveMembers.length === 0 || !canEditPool,
|
|
1441
1648
|
onClick: () => selectAll(false)
|
|
1442
|
-
}, t("row.poolSelectNone")))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-table" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), pool.checkinSupported ? (0, react.createElement)("span", null, t("row.poolColumnCheckin")) : null), ...pool.accounts.map((account) => renderAccountRow({
|
|
1649
|
+
}, t("row.poolSelectNone")))), (0, react.createElement)("div", { className: `dsm-workbuddy-pool-table${pool.checkinSupported ? " dsm-workbuddy-pool-table-checkin" : ""}` }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), pool.checkinSupported ? (0, react.createElement)("span", null, t("row.poolColumnCheckin")) : null, (0, react.createElement)("span", null)), ...pool.accounts.map((account) => renderAccountRow({
|
|
1443
1650
|
t,
|
|
1444
1651
|
account,
|
|
1445
1652
|
checked: active.memberAccountIds.includes(account.accountId),
|
|
1446
1653
|
canEdit: canEditPool,
|
|
1447
1654
|
checkinSupported: pool.checkinSupported,
|
|
1448
|
-
onToggle: (next) => toggleMember(account.accountId, next)
|
|
1655
|
+
onToggle: (next) => toggleMember(account.accountId, next),
|
|
1656
|
+
busy: rowBusyAccountId !== void 0,
|
|
1657
|
+
testDisabled: pool.targetModelSource === "none" || pool.targetModelSource === "stale",
|
|
1658
|
+
targetHint: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
|
|
1659
|
+
onTest: () => {
|
|
1660
|
+
runRowTest(account.accountId);
|
|
1661
|
+
},
|
|
1662
|
+
onRemove: () => {
|
|
1663
|
+
runRowRemove(account.accountId, account.accountName);
|
|
1664
|
+
}
|
|
1449
1665
|
}))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
|
|
1450
1666
|
className: "dsm-workbuddy-pool-warn",
|
|
1451
1667
|
role: "status"
|
|
@@ -1483,7 +1699,8 @@ window.__ModuleLoader__.load({
|
|
|
1483
1699
|
},
|
|
1484
1700
|
onDiscard: discard,
|
|
1485
1701
|
appendLog,
|
|
1486
|
-
targetProvenance
|
|
1702
|
+
targetProvenance,
|
|
1703
|
+
...accountTools === void 0 ? {} : { accountTools }
|
|
1487
1704
|
}), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolRegionNote")), renderLog({
|
|
1488
1705
|
t,
|
|
1489
1706
|
log,
|
|
@@ -1496,7 +1713,7 @@ window.__ModuleLoader__.load({
|
|
|
1496
1713
|
}
|
|
1497
1714
|
/** One account's row. */
|
|
1498
1715
|
function renderAccountRow(input) {
|
|
1499
|
-
const { t, account, checked, canEdit, checkinSupported, onToggle } = input;
|
|
1716
|
+
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
|
|
1500
1717
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
1501
1718
|
const excluded = exclusionText(t, account.excludedBy);
|
|
1502
1719
|
const probe = account.probe;
|
|
@@ -1537,11 +1754,24 @@ window.__ModuleLoader__.load({
|
|
|
1537
1754
|
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
1538
1755
|
className: "dsm-workbuddy-pool-unknown",
|
|
1539
1756
|
title: t("row.poolNeverTested")
|
|
1540
|
-
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null)
|
|
1757
|
+
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("button", {
|
|
1758
|
+
type: "button",
|
|
1759
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
1760
|
+
disabled: busy || testDisabled,
|
|
1761
|
+
title: testDisabled ? targetHint : void 0,
|
|
1762
|
+
onClick: onTest
|
|
1763
|
+
}, t("row.poolRowTest")), (0, react.createElement)("button", {
|
|
1764
|
+
type: "button",
|
|
1765
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
1766
|
+
disabled: busy,
|
|
1767
|
+
title: t("row.poolRowRemoveHint"),
|
|
1768
|
+
"aria-label": t("row.poolRowRemoveAria", { accountName: name }),
|
|
1769
|
+
onClick: onRemove
|
|
1770
|
+
}, t("row.poolRowRemove"))));
|
|
1541
1771
|
}
|
|
1542
1772
|
/** The preference editor plus its save/discard actions. */
|
|
1543
1773
|
function renderSettings(input) {
|
|
1544
|
-
const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance } = input;
|
|
1774
|
+
const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance, accountTools } = input;
|
|
1545
1775
|
const canEdit = settingsScope !== void 0;
|
|
1546
1776
|
const toggle = (label, hint, checked, onChange, disabled = false) => (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, label), (0, react.createElement)("span", null, hint)), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("label", { className: "sw" }, (0, react.createElement)("input", {
|
|
1547
1777
|
type: "checkbox",
|
|
@@ -1549,7 +1779,7 @@ window.__ModuleLoader__.load({
|
|
|
1549
1779
|
disabled: disabled || !canEdit,
|
|
1550
1780
|
onChange: (event) => onChange(event.currentTarget.checked)
|
|
1551
1781
|
}), (0, react.createElement)("span", { className: "sw-track" }))));
|
|
1552
|
-
return (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings-wrap" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolSettingsTitle")), dirty ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-dirty" }, t("row.poolDirty")) : null), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolSettingsHint")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings" }, toggle(t("row.poolEnabled"), t("row.poolEnabledHint"), active.enabled, (next) => onEdit((current) => ({
|
|
1782
|
+
return (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings-wrap" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolSettingsTitle")), dirty ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-dirty" }, t("row.poolDirty")) : null), accountTools === void 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-tools" }, accountTools), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolSettingsHint")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings" }, toggle(t("row.poolEnabled"), t("row.poolEnabledHint"), active.enabled, (next) => onEdit((current) => ({
|
|
1553
1783
|
...current,
|
|
1554
1784
|
enabled: next
|
|
1555
1785
|
}))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolTargetLabel")), (0, react.createElement)("span", null, targetProvenance === null ? null : `${targetProvenance} `, t("row.poolTargetHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
|
|
@@ -1621,6 +1851,690 @@ window.__ModuleLoader__.load({
|
|
|
1621
1851
|
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-log-time" }, formatClock$1(entry.atMs)), (0, react.createElement)("span", { className: `dsm-workbuddy-pool-log-text dsm-workbuddy-pool-log-${entry.tone}` }, entry.text)))));
|
|
1622
1852
|
}
|
|
1623
1853
|
//#endregion
|
|
1854
|
+
//#region src/client/OAuthSignIn.tsx
|
|
1855
|
+
/**
|
|
1856
|
+
* The card's OAuth QR sign-in section: one resting bar row plus a dialog
|
|
1857
|
+
* opened by the sign-in button.
|
|
1858
|
+
*
|
|
1859
|
+
* 参考:workbuddy2api-panel 的「添加账号」弹窗交互形态 —— 静态入口一行,
|
|
1860
|
+
* 点开后弹窗内分步走:获取授权链接 → 自动打开浏览器 → 弹窗内显示链接
|
|
1861
|
+
* 与等待状态 → 完成/失败回填结果。本实现沿用该步骤划分;弹窗样式直接
|
|
1862
|
+
* 用本项目导出/导入弹框的类(dsm-workbuddy-transfer-dialog),不引入
|
|
1863
|
+
* 第二套视觉。
|
|
1864
|
+
* 改动:自动打开浏览器。弹窗拦截的规避方式是「用户手势内同步 window.open
|
|
1865
|
+
* 占位,拿到授权 URL 后再导航」——点击「获取授权链接」的那一刻浏览器
|
|
1866
|
+
* 仍认定这是用户激活的手势链,占位窗口不会被拦;若 Host 环境连占位都
|
|
1867
|
+
* 拦(返回 null),降级为弹窗内手动「打开登录页」链接,功能不损失。
|
|
1868
|
+
*
|
|
1869
|
+
* 轮询循环压成一个组件内 effect:按 `OAUTH_POLL_INTERVAL_MS` 节奏问 Host
|
|
1870
|
+
* 「完成了吗」,完成即停。所有状态就地管理,零跨组件往返;Host 重启
|
|
1871
|
+
* 导致 loginId 失效会得到 done+error,与超时同一条文案路径。
|
|
1872
|
+
*
|
|
1873
|
+
* @module dsh-connect-workbuddy/client/OAuthSignIn
|
|
1874
|
+
*/
|
|
1875
|
+
/** Seconds since the login started, for the waiting line. */
|
|
1876
|
+
function elapsedSeconds(sinceMs, nowMs) {
|
|
1877
|
+
return Math.max(0, Math.floor((nowMs - sinceMs) / 1e3));
|
|
1878
|
+
}
|
|
1879
|
+
/** Copy the sign-in URL to the clipboard; true on success. */
|
|
1880
|
+
async function copyUrl(url) {
|
|
1881
|
+
try {
|
|
1882
|
+
await navigator.clipboard.writeText(url);
|
|
1883
|
+
return true;
|
|
1884
|
+
} catch {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* The section. Rendered on BOTH sign-in states: a signed-out machine gains
|
|
1890
|
+
* its first account here; a signed-in one can add another without leaving
|
|
1891
|
+
* the card.
|
|
1892
|
+
*/
|
|
1893
|
+
function OAuthSignIn({ t, region, onAdded }) {
|
|
1894
|
+
const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
|
|
1895
|
+
const [dialogOpen, setDialogOpen] = (0, react.useState)(false);
|
|
1896
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1897
|
+
const [pollTick, setPollTick] = (0, react.useState)(0);
|
|
1898
|
+
const [nowMs, setNowMs] = (0, react.useState)(() => Date.now());
|
|
1899
|
+
const mounted = (0, react.useRef)(true);
|
|
1900
|
+
(0, react.useEffect)(() => {
|
|
1901
|
+
mounted.current = true;
|
|
1902
|
+
return () => {
|
|
1903
|
+
mounted.current = false;
|
|
1904
|
+
};
|
|
1905
|
+
}, []);
|
|
1906
|
+
const waiting = phase.kind === "waiting";
|
|
1907
|
+
(0, react.useEffect)(() => {
|
|
1908
|
+
if (!waiting || phase.kind !== "waiting") return;
|
|
1909
|
+
const expiresAtMs = phase.expiresAtMs;
|
|
1910
|
+
const timer = setInterval(() => {
|
|
1911
|
+
if (!mounted.current) return;
|
|
1912
|
+
const now = Date.now();
|
|
1913
|
+
setNowMs(now);
|
|
1914
|
+
if (now > expiresAtMs) setPhase({
|
|
1915
|
+
kind: "finished",
|
|
1916
|
+
message: "error",
|
|
1917
|
+
error: t("row.oauthTimedOut")
|
|
1918
|
+
});
|
|
1919
|
+
}, 1e3);
|
|
1920
|
+
return () => {
|
|
1921
|
+
clearInterval(timer);
|
|
1922
|
+
};
|
|
1923
|
+
}, [
|
|
1924
|
+
waiting,
|
|
1925
|
+
phase,
|
|
1926
|
+
t
|
|
1927
|
+
]);
|
|
1928
|
+
(0, react.useEffect)(() => {
|
|
1929
|
+
if (phase.kind !== "waiting") return;
|
|
1930
|
+
const { loginId } = phase;
|
|
1931
|
+
let cancelled = false;
|
|
1932
|
+
const timer = setTimeout(() => {
|
|
1933
|
+
(async () => {
|
|
1934
|
+
try {
|
|
1935
|
+
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_OAUTH_PATH, region, "poll"), {
|
|
1936
|
+
method: "POST",
|
|
1937
|
+
headers: {
|
|
1938
|
+
"content-type": "application/json",
|
|
1939
|
+
accept: "application/json"
|
|
1940
|
+
},
|
|
1941
|
+
credentials: "same-origin",
|
|
1942
|
+
body: JSON.stringify({ loginId })
|
|
1943
|
+
});
|
|
1944
|
+
const body = await response.json();
|
|
1945
|
+
if (cancelled || !mounted.current) return;
|
|
1946
|
+
if (!response.ok || !body || typeof body !== "object") {
|
|
1947
|
+
setPhase({
|
|
1948
|
+
kind: "finished",
|
|
1949
|
+
message: "error",
|
|
1950
|
+
error: `HTTP ${response.status}`
|
|
1951
|
+
});
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (!body.done) {
|
|
1955
|
+
setPollTick((tick) => tick + 1);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (typeof body.error === "string" && body.error !== "") setPhase({
|
|
1959
|
+
kind: "finished",
|
|
1960
|
+
message: "error",
|
|
1961
|
+
error: body.error
|
|
1962
|
+
});
|
|
1963
|
+
else {
|
|
1964
|
+
const accountName = body.accountName ?? "";
|
|
1965
|
+
setPhase({
|
|
1966
|
+
kind: "syncing",
|
|
1967
|
+
accountName
|
|
1968
|
+
});
|
|
1969
|
+
await onAdded?.();
|
|
1970
|
+
if (cancelled || !mounted.current) return;
|
|
1971
|
+
setPhase({
|
|
1972
|
+
kind: "finished",
|
|
1973
|
+
message: "added",
|
|
1974
|
+
accountName
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
} catch {
|
|
1978
|
+
setPollTick((tick) => tick + 1);
|
|
1979
|
+
}
|
|
1980
|
+
})();
|
|
1981
|
+
}, OAUTH_POLL_INTERVAL_MS);
|
|
1982
|
+
return () => {
|
|
1983
|
+
cancelled = true;
|
|
1984
|
+
clearTimeout(timer);
|
|
1985
|
+
};
|
|
1986
|
+
}, [
|
|
1987
|
+
onAdded,
|
|
1988
|
+
phase,
|
|
1989
|
+
pollTick,
|
|
1990
|
+
region
|
|
1991
|
+
]);
|
|
1992
|
+
/**
|
|
1993
|
+
* Open a placeholder window WITHOUT `noopener`, and deliberately so.
|
|
1994
|
+
*
|
|
1995
|
+
* `noopener` is the correct feature for a link whose destination never needs
|
|
1996
|
+
* a handle — and that is exactly why it is wrong here. Per the spec, a
|
|
1997
|
+
* `window.open` call carrying `noopener` opens the tab but returns `null`
|
|
1998
|
+
* (MDN: "the new window will not have access to the originating window via
|
|
1999
|
+
* `Window.opener` and returns `null`"). The placeholder exists ONLY to be
|
|
2000
|
+
* navigated once the Host answers, so the handle IS the point: passing
|
|
2001
|
+
* `noopener` made `placeholder` null, the navigation was skipped, and every
|
|
2002
|
+
* sign-in left an empty `about:blank` tab behind instead of the sign-in page.
|
|
2003
|
+
*
|
|
2004
|
+
* The security property is kept, just established on the handle instead of
|
|
2005
|
+
* through the feature: `opener = null` is applied to the placeholder before
|
|
2006
|
+
* anything is loaded into it, so the destination still cannot reach back
|
|
2007
|
+
* through `window.opener`. `noreferrer` is dropped for the same reason —
|
|
2008
|
+
* the spec makes it imply `noopener`, so it would reintroduce the null.
|
|
2009
|
+
* Nothing is lost: the placeholder is same-origin and empty until navigated,
|
|
2010
|
+
* and the referrer would only ever have been the settings page.
|
|
2011
|
+
*/
|
|
2012
|
+
function openPlaceholder() {
|
|
2013
|
+
const placeholder = window.open("", "_blank");
|
|
2014
|
+
if (placeholder !== null) placeholder.opener = null;
|
|
2015
|
+
return placeholder;
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Fetch the authorization URL AND open the browser in one gesture chain.
|
|
2019
|
+
* The placeholder window is opened synchronously — inside the click's user
|
|
2020
|
+
* activation, before any await — so popup blocking cannot intervene; the
|
|
2021
|
+
* URL is navigated once the Host answers. A blocked placeholder (null)
|
|
2022
|
+
* degrades to the manual link inside the dialog, losing nothing but the
|
|
2023
|
+
* convenience.
|
|
2024
|
+
*/
|
|
2025
|
+
const start = async (placeholder) => {
|
|
2026
|
+
setPhase({ kind: "opening" });
|
|
2027
|
+
setCopied(false);
|
|
2028
|
+
try {
|
|
2029
|
+
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_OAUTH_PATH, region, "start"), {
|
|
2030
|
+
method: "POST",
|
|
2031
|
+
headers: { accept: "application/json" },
|
|
2032
|
+
credentials: "same-origin"
|
|
2033
|
+
});
|
|
2034
|
+
const body = await response.json();
|
|
2035
|
+
if (!mounted.current) {
|
|
2036
|
+
placeholder?.close();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (!response.ok || typeof body.loginId !== "string" || typeof body.verificationUri !== "string") {
|
|
2040
|
+
placeholder?.close();
|
|
2041
|
+
setPhase({
|
|
2042
|
+
kind: "finished",
|
|
2043
|
+
message: "error",
|
|
2044
|
+
error: body.error ?? `HTTP ${response.status}`
|
|
2045
|
+
});
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (placeholder !== null) placeholder.location.href = body.verificationUri;
|
|
2049
|
+
setPhase({
|
|
2050
|
+
kind: "waiting",
|
|
2051
|
+
loginId: body.loginId,
|
|
2052
|
+
verificationUri: body.verificationUri,
|
|
2053
|
+
startedAtMs: Date.now(),
|
|
2054
|
+
expiresAtMs: Date.now() + (typeof body.expiresIn === "number" && body.expiresIn > 0 ? body.expiresIn : 600) * 1e3
|
|
2055
|
+
});
|
|
2056
|
+
setNowMs(Date.now());
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
placeholder?.close();
|
|
2059
|
+
if (mounted.current) setPhase({
|
|
2060
|
+
kind: "finished",
|
|
2061
|
+
message: "error",
|
|
2062
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
/** Open the dialog and begin the login in the same click: the placeholder
|
|
2067
|
+
* window must be created synchronously to count as a user gesture. */
|
|
2068
|
+
const beginSignIn = () => {
|
|
2069
|
+
setDialogOpen(true);
|
|
2070
|
+
start(openPlaceholder());
|
|
2071
|
+
};
|
|
2072
|
+
/** Re-open the sign-in page from the waiting dialog (no new session).
|
|
2073
|
+
*
|
|
2074
|
+
* Here `noopener,noreferrer` IS right: this is a one-way navigation to a
|
|
2075
|
+
* third-party page and nothing needs a handle on it — the contrast with
|
|
2076
|
+
* {@link openPlaceholder} is the whole distinction. */
|
|
2077
|
+
const reopen = () => {
|
|
2078
|
+
if (phase.kind !== "waiting") return;
|
|
2079
|
+
window.open(phase.verificationUri, "_blank", "noopener,noreferrer");
|
|
2080
|
+
};
|
|
2081
|
+
/**
|
|
2082
|
+
* The three strings that NAME the sign-in method, per region.
|
|
2083
|
+
*
|
|
2084
|
+
* CN signs in by scanning a QR code with the WorkBuddy app; the international
|
|
2085
|
+
* build signs in on a web page. Every other line in this section (waiting,
|
|
2086
|
+
* cancel, done, copy) is about the flow, not the method, so it stays shared —
|
|
2087
|
+
* splitting it too would let the two regions drift apart for no gain.
|
|
2088
|
+
*/
|
|
2089
|
+
const titleKey = region === "cn" ? "row.oauthTitle" : "row.oauthTitleGlobal";
|
|
2090
|
+
const startKey = region === "cn" ? "row.oauthStart" : "row.oauthStartGlobal";
|
|
2091
|
+
const finishText = phase.kind === "finished" ? phase.message === "added" ? t("row.oauthDone", { accountName: phase.accountName === "" || phase.accountName === void 0 ? t("row.accountUnnamed") : phase.accountName }) : phase.message === "cancelled" ? t("row.oauthCancelled") : t("row.oauthError", { message: phase.error ?? "" }) : "";
|
|
2092
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2093
|
+
className: "dsm-workbuddy-bar",
|
|
2094
|
+
"aria-label": t(titleKey),
|
|
2095
|
+
children: [
|
|
2096
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
|
|
2097
|
+
visible: phase.kind === "syncing",
|
|
2098
|
+
message: t("row.oauthSyncing"),
|
|
2099
|
+
hint: t("row.veilHintList")
|
|
2100
|
+
}),
|
|
2101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2102
|
+
className: "dsm-workbuddy-bar-label",
|
|
2103
|
+
children: t(titleKey)
|
|
2104
|
+
}),
|
|
2105
|
+
phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2106
|
+
className: `dsm-workbuddy-bar-status ${phase.message === "added" ? "dsm-workbuddy-bar-ok" : "dsm-workbuddy-bar-error"}`,
|
|
2107
|
+
children: finishText
|
|
2108
|
+
}) : phase.kind === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2109
|
+
className: "dsm-workbuddy-bar-status",
|
|
2110
|
+
children: t("row.oauthOpened")
|
|
2111
|
+
}) : null,
|
|
2112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2113
|
+
className: "dsm-workbuddy-bar-actions",
|
|
2114
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2115
|
+
type: "button",
|
|
2116
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2117
|
+
disabled: phase.kind === "opening",
|
|
2118
|
+
onClick: beginSignIn,
|
|
2119
|
+
children: phase.kind === "opening" ? t("row.oauthStarting") : t(startKey)
|
|
2120
|
+
})
|
|
2121
|
+
}),
|
|
2122
|
+
dialogOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2123
|
+
className: "dsm-workbuddy-transfer-dialog",
|
|
2124
|
+
role: "dialog",
|
|
2125
|
+
"aria-label": t(titleKey),
|
|
2126
|
+
children: phase.kind === "opening" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.oauthStarting") }) : phase.kind === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.oauthOpenHint") }),
|
|
2128
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2129
|
+
className: "dsm-workbuddy-oauth-url",
|
|
2130
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2131
|
+
href: phase.verificationUri,
|
|
2132
|
+
target: "_blank",
|
|
2133
|
+
rel: "noopener noreferrer",
|
|
2134
|
+
children: phase.verificationUri
|
|
2135
|
+
})
|
|
2136
|
+
}),
|
|
2137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2138
|
+
className: "dsm-workbuddy-hint",
|
|
2139
|
+
children: [
|
|
2140
|
+
t("row.oauthWaiting", { seconds: elapsedSeconds(phase.startedAtMs, nowMs) }),
|
|
2141
|
+
" ",
|
|
2142
|
+
t("row.oauthExpires", { minutes: Math.max(1, Math.round((phase.expiresAtMs - nowMs) / 6e4)) })
|
|
2143
|
+
]
|
|
2144
|
+
}),
|
|
2145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2146
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2147
|
+
children: [
|
|
2148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2149
|
+
type: "button",
|
|
2150
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2151
|
+
onClick: () => {
|
|
2152
|
+
copyUrl(phase.verificationUri).then((ok) => {
|
|
2153
|
+
if (mounted.current) setCopied(ok);
|
|
2154
|
+
});
|
|
2155
|
+
},
|
|
2156
|
+
children: copied ? t("row.oauthCopied") : t("row.oauthCopy")
|
|
2157
|
+
}),
|
|
2158
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2159
|
+
type: "button",
|
|
2160
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2161
|
+
onClick: reopen,
|
|
2162
|
+
children: t("row.oauthReopen")
|
|
2163
|
+
}),
|
|
2164
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2165
|
+
type: "button",
|
|
2166
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2167
|
+
onClick: () => {
|
|
2168
|
+
setPhase({
|
|
2169
|
+
kind: "finished",
|
|
2170
|
+
message: "cancelled"
|
|
2171
|
+
});
|
|
2172
|
+
},
|
|
2173
|
+
children: t("row.oauthCancel")
|
|
2174
|
+
})
|
|
2175
|
+
]
|
|
2176
|
+
})
|
|
2177
|
+
] }) : phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2178
|
+
className: `${phase.message === "added" ? "dsm-workbuddy-bar-ok" : phase.message === "error" ? "dsm-workbuddy-bar-error" : ""}`,
|
|
2179
|
+
children: finishText
|
|
2180
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2181
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2182
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2183
|
+
type: "button",
|
|
2184
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2185
|
+
onClick: () => {
|
|
2186
|
+
setDialogOpen(false);
|
|
2187
|
+
},
|
|
2188
|
+
children: t("row.oauthDialogClose")
|
|
2189
|
+
})
|
|
2190
|
+
})] }) : null
|
|
2191
|
+
}) : null
|
|
2192
|
+
]
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
//#endregion
|
|
2196
|
+
//#region src/client/AccountTransfer.tsx
|
|
2197
|
+
/**
|
|
2198
|
+
* The card's account-transfer section: batch export / import of the region's
|
|
2199
|
+
* stored credentials, in the file format shared with sibling credential
|
|
2200
|
+
* managers for the same upstream.
|
|
2201
|
+
*
|
|
2202
|
+
* 参考:扫码登录小节(OAuthSignIn)的组件形态 —— 单个 section、就地管理
|
|
2203
|
+
* 全部状态(busy / 结果行 / 错误)、零跨组件往返。导出弹框的交互(多选
|
|
2204
|
+
* 账号 → 下载 JSON)与导入弹框(选文件 → 脱敏预览 → 勾选 → 导入)沿用
|
|
2205
|
+
* 通行实现的步骤划分;预览只含展示字段与 hasToken 布尔量,token 明文
|
|
2206
|
+
* 永不进入浏览器。
|
|
2207
|
+
*
|
|
2208
|
+
* 导出用 <a download> Blob:卡片运行在设置页 iframe 里,无宿主保存对话框
|
|
2209
|
+
* 可用;a[download] 同源下载是沙箱内最可靠的方式。
|
|
2210
|
+
*
|
|
2211
|
+
* @module dsh-connect-workbuddy/client/AccountTransfer
|
|
2212
|
+
*/
|
|
2213
|
+
/** Build the default export file name: workbuddy-accounts-YYYY-MM-DD.json. */
|
|
2214
|
+
function exportFileName() {
|
|
2215
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
2216
|
+
const d = /* @__PURE__ */ new Date();
|
|
2217
|
+
return `workbuddy-accounts-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.json`;
|
|
2218
|
+
}
|
|
2219
|
+
/** Download a JSON array as a local file (no host save dialog inside the iframe). */
|
|
2220
|
+
function downloadJson(fileName, data) {
|
|
2221
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
2222
|
+
const url = URL.createObjectURL(blob);
|
|
2223
|
+
const anchor = document.createElement("a");
|
|
2224
|
+
anchor.href = url;
|
|
2225
|
+
anchor.download = fileName;
|
|
2226
|
+
document.body.appendChild(anchor);
|
|
2227
|
+
anchor.click();
|
|
2228
|
+
anchor.remove();
|
|
2229
|
+
URL.revokeObjectURL(url);
|
|
2230
|
+
}
|
|
2231
|
+
/** The section. Rendered on both sign-in states — a signed-out machine can
|
|
2232
|
+
* still import a file exported elsewhere; export without accounts answers
|
|
2233
|
+
* the error line. */
|
|
2234
|
+
function AccountTransfer({ t, region, accounts, onChanged }) {
|
|
2235
|
+
const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
|
|
2236
|
+
const [dialog, setDialog] = (0, react.useState)(void 0);
|
|
2237
|
+
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2238
|
+
const [preview, setPreview] = (0, react.useState)(void 0);
|
|
2239
|
+
const [importSelected, setImportSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2240
|
+
const fileInput = (0, react.useRef)(null);
|
|
2241
|
+
/**
|
|
2242
|
+
* The text of the file the user just picked.
|
|
2243
|
+
*
|
|
2244
|
+
* Held here rather than re-read from the input at import time: the picker
|
|
2245
|
+
* is a bare system dialog that closes immediately (see the import button),
|
|
2246
|
+
* so between preview and confirm there is no dialog on screen holding the
|
|
2247
|
+
* File. Re-reading `fileInput.current.files` would also break the moment
|
|
2248
|
+
* anything clears the input — and it must be cleared after every pick, or
|
|
2249
|
+
* choosing the SAME file twice fires no change event.
|
|
2250
|
+
*/
|
|
2251
|
+
const importText = (0, react.useRef)("");
|
|
2252
|
+
const mounted = (0, react.useRef)(true);
|
|
2253
|
+
(0, react.useEffect)(() => {
|
|
2254
|
+
mounted.current = true;
|
|
2255
|
+
return () => {
|
|
2256
|
+
mounted.current = false;
|
|
2257
|
+
};
|
|
2258
|
+
}, []);
|
|
2259
|
+
const closeDialog = () => {
|
|
2260
|
+
setDialog(void 0);
|
|
2261
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
2262
|
+
setPreview(void 0);
|
|
2263
|
+
setImportSelected(/* @__PURE__ */ new Set());
|
|
2264
|
+
};
|
|
2265
|
+
const runExport = async () => {
|
|
2266
|
+
if (selected.size === 0) return;
|
|
2267
|
+
setPhase({ kind: "exporting" });
|
|
2268
|
+
try {
|
|
2269
|
+
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "export"), {
|
|
2270
|
+
method: "POST",
|
|
2271
|
+
headers: {
|
|
2272
|
+
"content-type": "application/json",
|
|
2273
|
+
accept: "application/json"
|
|
2274
|
+
},
|
|
2275
|
+
credentials: "same-origin",
|
|
2276
|
+
body: JSON.stringify({ accountIds: [...selected] })
|
|
2277
|
+
});
|
|
2278
|
+
const body = await response.json();
|
|
2279
|
+
if (!mounted.current) return;
|
|
2280
|
+
if (!response.ok || !Array.isArray(body.accounts)) {
|
|
2281
|
+
setPhase({
|
|
2282
|
+
kind: "error",
|
|
2283
|
+
message: body.error ?? `HTTP ${response.status}`
|
|
2284
|
+
});
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
downloadJson(exportFileName(), body.accounts);
|
|
2288
|
+
closeDialog();
|
|
2289
|
+
setPhase({
|
|
2290
|
+
kind: "done",
|
|
2291
|
+
message: t("row.transferExported", {
|
|
2292
|
+
count: body.accounts.length,
|
|
2293
|
+
fileName: exportFileName()
|
|
2294
|
+
})
|
|
2295
|
+
});
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
if (mounted.current) setPhase({
|
|
2298
|
+
kind: "error",
|
|
2299
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
const readImportFile = async (file) => {
|
|
2304
|
+
setPhase({ kind: "importing" });
|
|
2305
|
+
try {
|
|
2306
|
+
const text = await file.text();
|
|
2307
|
+
importText.current = text;
|
|
2308
|
+
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "preview"), {
|
|
2309
|
+
method: "POST",
|
|
2310
|
+
headers: {
|
|
2311
|
+
"content-type": "application/json",
|
|
2312
|
+
accept: "application/json"
|
|
2313
|
+
},
|
|
2314
|
+
credentials: "same-origin",
|
|
2315
|
+
body: JSON.stringify({ text })
|
|
2316
|
+
});
|
|
2317
|
+
const body = await response.json();
|
|
2318
|
+
if (!mounted.current) return;
|
|
2319
|
+
if (!response.ok || !Array.isArray(body.accounts)) {
|
|
2320
|
+
setPhase({
|
|
2321
|
+
kind: "error",
|
|
2322
|
+
message: body.error ?? `HTTP ${response.status}`
|
|
2323
|
+
});
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
setPreview(body);
|
|
2327
|
+
setImportSelected(new Set(body.accounts.filter((entry) => entry.hasToken).map((entry) => entry.index)));
|
|
2328
|
+
setPhase({ kind: "idle" });
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
if (mounted.current) setPhase({
|
|
2331
|
+
kind: "error",
|
|
2332
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
const runImport = async () => {
|
|
2337
|
+
if (preview === void 0 || importSelected.size === 0 || importText.current === "") return;
|
|
2338
|
+
setPhase({ kind: "importing" });
|
|
2339
|
+
try {
|
|
2340
|
+
const text = importText.current;
|
|
2341
|
+
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "import"), {
|
|
2342
|
+
method: "POST",
|
|
2343
|
+
headers: {
|
|
2344
|
+
"content-type": "application/json",
|
|
2345
|
+
accept: "application/json"
|
|
2346
|
+
},
|
|
2347
|
+
credentials: "same-origin",
|
|
2348
|
+
body: JSON.stringify({
|
|
2349
|
+
text,
|
|
2350
|
+
indexes: [...importSelected]
|
|
2351
|
+
})
|
|
2352
|
+
});
|
|
2353
|
+
const body = await response.json();
|
|
2354
|
+
if (!mounted.current) return;
|
|
2355
|
+
if (!response.ok || typeof body.imported !== "number") {
|
|
2356
|
+
setPhase({
|
|
2357
|
+
kind: "error",
|
|
2358
|
+
message: body.error ?? `HTTP ${response.status}`
|
|
2359
|
+
});
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
closeDialog();
|
|
2363
|
+
const message = t("row.transferImported", {
|
|
2364
|
+
imported: body.imported,
|
|
2365
|
+
skipped: body.skipped ?? 0
|
|
2366
|
+
});
|
|
2367
|
+
setPhase({
|
|
2368
|
+
kind: "refreshing",
|
|
2369
|
+
message
|
|
2370
|
+
});
|
|
2371
|
+
await onChanged?.();
|
|
2372
|
+
if (!mounted.current) return;
|
|
2373
|
+
setPhase({
|
|
2374
|
+
kind: "done",
|
|
2375
|
+
message
|
|
2376
|
+
});
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
if (mounted.current) setPhase({
|
|
2379
|
+
kind: "error",
|
|
2380
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
const busy = phase.kind === "exporting" || phase.kind === "importing" || phase.kind === "refreshing";
|
|
2385
|
+
/** The veil's scenario line: what is happening, in the user's terms. */
|
|
2386
|
+
const veilMessage = phase.kind === "exporting" ? t("row.transferExporting") : phase.kind === "importing" || phase.kind === "refreshing" ? t("row.transferImporting") : "";
|
|
2387
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2388
|
+
className: "dsm-workbuddy-bar",
|
|
2389
|
+
"aria-label": t("row.transferTitle"),
|
|
2390
|
+
children: [
|
|
2391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
|
|
2392
|
+
visible: busy,
|
|
2393
|
+
message: veilMessage,
|
|
2394
|
+
...busy ? { hint: t("row.veilHintList") } : {}
|
|
2395
|
+
}),
|
|
2396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2397
|
+
className: "dsm-workbuddy-bar-label",
|
|
2398
|
+
children: t("row.transferTitle")
|
|
2399
|
+
}),
|
|
2400
|
+
phase.kind === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2401
|
+
className: "dsm-workbuddy-bar-status dsm-workbuddy-bar-ok",
|
|
2402
|
+
children: phase.message
|
|
2403
|
+
}) : phase.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2404
|
+
className: "dsm-workbuddy-bar-status dsm-workbuddy-bar-error",
|
|
2405
|
+
children: t("row.transferError", { message: phase.message })
|
|
2406
|
+
}) : null,
|
|
2407
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2408
|
+
className: "dsm-workbuddy-bar-actions",
|
|
2409
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2410
|
+
type: "button",
|
|
2411
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2412
|
+
disabled: busy || accounts.length === 0,
|
|
2413
|
+
onClick: () => {
|
|
2414
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
2415
|
+
setDialog("export");
|
|
2416
|
+
},
|
|
2417
|
+
children: t("row.transferExport")
|
|
2418
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2419
|
+
type: "button",
|
|
2420
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2421
|
+
disabled: busy,
|
|
2422
|
+
onClick: () => {
|
|
2423
|
+
fileInput.current?.click();
|
|
2424
|
+
},
|
|
2425
|
+
children: t("row.transferImport")
|
|
2426
|
+
})]
|
|
2427
|
+
}),
|
|
2428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2429
|
+
ref: fileInput,
|
|
2430
|
+
type: "file",
|
|
2431
|
+
accept: "application/json,.json",
|
|
2432
|
+
className: "dsm-workbuddy-transfer-file",
|
|
2433
|
+
onChange: (event) => {
|
|
2434
|
+
const file = event.target.files?.[0];
|
|
2435
|
+
event.target.value = "";
|
|
2436
|
+
if (file !== void 0) {
|
|
2437
|
+
setPreview(void 0);
|
|
2438
|
+
setImportSelected(/* @__PURE__ */ new Set());
|
|
2439
|
+
setDialog("import");
|
|
2440
|
+
readImportFile(file);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}),
|
|
2444
|
+
dialog === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2445
|
+
className: "dsm-workbuddy-transfer-dialog",
|
|
2446
|
+
role: "dialog",
|
|
2447
|
+
"aria-label": t("row.transferExport"),
|
|
2448
|
+
children: [
|
|
2449
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferExportPick", { total: accounts.length }) }),
|
|
2450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2451
|
+
className: "dsm-workbuddy-hint",
|
|
2452
|
+
children: t("row.transferHint")
|
|
2453
|
+
}),
|
|
2454
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2455
|
+
className: "dsm-workbuddy-transfer-list",
|
|
2456
|
+
children: accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
2457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2458
|
+
type: "checkbox",
|
|
2459
|
+
checked: selected.has(account.id),
|
|
2460
|
+
onChange: (event) => {
|
|
2461
|
+
const next = new Set(selected);
|
|
2462
|
+
if (event.target.checked) next.add(account.id);
|
|
2463
|
+
else next.delete(account.id);
|
|
2464
|
+
setSelected(next);
|
|
2465
|
+
}
|
|
2466
|
+
}),
|
|
2467
|
+
" ",
|
|
2468
|
+
account.accountName === "" ? t("row.accountUnnamed") : account.accountName
|
|
2469
|
+
] }) }, account.id))
|
|
2470
|
+
}),
|
|
2471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2472
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2473
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2474
|
+
type: "button",
|
|
2475
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2476
|
+
onClick: closeDialog,
|
|
2477
|
+
children: t("row.transferCancel")
|
|
2478
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2479
|
+
type: "button",
|
|
2480
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2481
|
+
disabled: selected.size === 0 || phase.kind === "exporting",
|
|
2482
|
+
onClick: () => {
|
|
2483
|
+
runExport();
|
|
2484
|
+
},
|
|
2485
|
+
children: phase.kind === "exporting" ? t("row.transferExporting") : t("row.transferExportConfirm", { count: selected.size })
|
|
2486
|
+
})]
|
|
2487
|
+
})
|
|
2488
|
+
]
|
|
2489
|
+
}) : null,
|
|
2490
|
+
dialog === "import" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2491
|
+
className: "dsm-workbuddy-transfer-dialog",
|
|
2492
|
+
role: "dialog",
|
|
2493
|
+
"aria-label": t("row.transferImport"),
|
|
2494
|
+
children: preview === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferPreviewing") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferPreviewTotal", { total: preview.total }) }),
|
|
2496
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2497
|
+
className: "dsm-workbuddy-transfer-list",
|
|
2498
|
+
children: preview.accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
2499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2500
|
+
type: "checkbox",
|
|
2501
|
+
disabled: !entry.hasToken,
|
|
2502
|
+
checked: importSelected.has(entry.index),
|
|
2503
|
+
onChange: (event) => {
|
|
2504
|
+
const next = new Set(importSelected);
|
|
2505
|
+
if (event.target.checked) next.add(entry.index);
|
|
2506
|
+
else next.delete(entry.index);
|
|
2507
|
+
setImportSelected(next);
|
|
2508
|
+
}
|
|
2509
|
+
}),
|
|
2510
|
+
" ",
|
|
2511
|
+
entry.nickname !== "" ? entry.nickname : entry.email !== "" ? entry.email : entry.uid !== "" ? entry.uid : t("row.transferEntryUnknown"),
|
|
2512
|
+
entry.hasToken ? "" : ` — ${t("row.transferNoToken")}`
|
|
2513
|
+
] }) }, entry.index))
|
|
2514
|
+
}),
|
|
2515
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2516
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2517
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2518
|
+
type: "button",
|
|
2519
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2520
|
+
onClick: closeDialog,
|
|
2521
|
+
children: t("row.transferCancel")
|
|
2522
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2523
|
+
type: "button",
|
|
2524
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2525
|
+
disabled: importSelected.size === 0 || phase.kind === "importing",
|
|
2526
|
+
onClick: () => {
|
|
2527
|
+
runImport();
|
|
2528
|
+
},
|
|
2529
|
+
children: phase.kind === "importing" ? t("row.transferImporting") : t("row.transferImportConfirm", { count: importSelected.size })
|
|
2530
|
+
})]
|
|
2531
|
+
})
|
|
2532
|
+
] })
|
|
2533
|
+
}) : null
|
|
2534
|
+
]
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
//#endregion
|
|
1624
2538
|
//#region src/native-modality.ts
|
|
1625
2539
|
/**
|
|
1626
2540
|
* Reviewed vendor classifications, keyed by EXACT model id (never a prefix).
|
|
@@ -1845,7 +2759,9 @@ window.__ModuleLoader__.load({
|
|
|
1845
2759
|
.dsm-workbuddy-pool-warn{margin:0;padding:9px 11px;border-radius:9px;font-size:12px;line-height:17px;color:var(--dsw-alias-state-warn-primary,#f59e0b);background:rgba(245,158,11,.09);border:1px solid rgba(245,158,11,.3)}
|
|
1846
2760
|
.dsm-workbuddy-pool-error{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-state-error-primary,#ef4444);white-space:pre-line}
|
|
1847
2761
|
.dsm-workbuddy-pool-table{border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
|
|
1848
|
-
.dsm-workbuddy-pool-row{display:grid;grid-template-columns:1.6fr 1fr 1.3fr .9fr;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:13px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
2762
|
+
.dsm-workbuddy-pool-row{display:grid;grid-template-columns:1.6fr 1fr 1.3fr .9fr .8fr;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:13px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
2763
|
+
/* No check-in column (the international region): the freed track goes to the actions. */
|
|
2764
|
+
.dsm-workbuddy-pool-table:not(.dsm-workbuddy-pool-table-checkin) .dsm-workbuddy-pool-row{grid-template-columns:1.6fr 1fr 1.3fr 1.7fr}
|
|
1849
2765
|
.dsm-workbuddy-pool-row:first-child{border-top:0}
|
|
1850
2766
|
.dsm-workbuddy-pool-row-head{padding:8px 14px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}
|
|
1851
2767
|
.dsm-workbuddy-pool-row-current{background:rgba(86,134,254,.07)}
|
|
@@ -1889,6 +2805,12 @@ window.__ModuleLoader__.load({
|
|
|
1889
2805
|
.dsm-workbuddy-pool-settings-wrap{display:flex;flex-direction:column;gap:10px}
|
|
1890
2806
|
.dsm-workbuddy-pool-dirty{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
1891
2807
|
.dsm-workbuddy-pool-settings{display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
|
|
2808
|
+
/* QR sign-in + export/import, hosted inside the pool settings block.
|
|
2809
|
+
The block's own bordered rows belong to the SAVED preferences, so these
|
|
2810
|
+
get their own container: same rhythm (12px 14px) so the two read as one
|
|
2811
|
+
group, but a dashed edge marking them as immediate actions rather than
|
|
2812
|
+
drafts the Save button below would commit. */
|
|
2813
|
+
.dsm-workbuddy-pool-tools{display:flex;flex-direction:column;gap:2px;border:1px dashed var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;padding:4px 10px}
|
|
1892
2814
|
.dsm-workbuddy-pool-set{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:12px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31)}
|
|
1893
2815
|
.dsm-workbuddy-pool-set:first-child{border-top:0}
|
|
1894
2816
|
.dsm-workbuddy-pool-set-copy{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
|
|
@@ -1918,6 +2840,9 @@ window.__ModuleLoader__.load({
|
|
|
1918
2840
|
.dsm-workbuddy-pool-log-error{color:var(--dsw-alias-state-error-primary,#ef4444)}
|
|
1919
2841
|
.dsm-workbuddy-pool-log-info{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
1920
2842
|
.dsm-workbuddy-pool-log-empty{padding:14px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
2843
|
+
/* Per-row actions (test / remove), right side of each pool row. */
|
|
2844
|
+
.dsm-workbuddy-pool-row-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex:none}
|
|
2845
|
+
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
|
|
1921
2846
|
@media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
|
|
1922
2847
|
/* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
|
|
1923
2848
|
.dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
|
|
@@ -1928,6 +2853,63 @@ window.__ModuleLoader__.load({
|
|
|
1928
2853
|
.dsm-workbuddy-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
|
|
1929
2854
|
.dsm-workbuddy-pool .sw input:disabled+.sw-track{opacity:.4}
|
|
1930
2855
|
.dsm-workbuddy-pool .sw input:disabled{cursor:default}
|
|
2856
|
+
/* Account action bars: QR sign-in and export/import.
|
|
2857
|
+
One row each — label, live status, actions — instead of a padded block
|
|
2858
|
+
per feature. The bar is the resting height; only an open dialog (below)
|
|
2859
|
+
grows vertically, so the card does not scroll past a wall of chrome to
|
|
2860
|
+
reach the buttons. Status text shares the row rather than claiming its own,
|
|
2861
|
+
and is what truncates first when the card is narrow. */
|
|
2862
|
+
.dsm-workbuddy-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0;padding:0}
|
|
2863
|
+
.dsm-workbuddy-bar-label{flex:none;margin:0;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
2864
|
+
.dsm-workbuddy-bar-status{flex:1 1 auto;min-width:0;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e9ed);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2865
|
+
.dsm-workbuddy-bar-hint{color:var(--dsw-alias-label-tertiary,#999)}
|
|
2866
|
+
.dsm-workbuddy-bar-actions{display:flex;gap:8px;flex:none;margin-left:auto}
|
|
2867
|
+
.dsm-workbuddy-bar a{color:var(--dsw-alias-brand-primary,#5686fe);text-decoration:none}
|
|
2868
|
+
.dsm-workbuddy-bar a:hover{text-decoration:underline}
|
|
2869
|
+
.dsm-workbuddy-bar-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
2870
|
+
.dsm-workbuddy-bar-error{color:var(--dsw-alias-state-error-primary,#ef4444)}
|
|
2871
|
+
/* Secondary text inside the transfer DIALOG (not a bar): the privacy note
|
|
2872
|
+
shown when credentials leave the machine. Kept independent of the bar
|
|
2873
|
+
classes so the dialog does not depend on the bar's layout to be readable. */
|
|
2874
|
+
.dsm-workbuddy-hint{color:var(--dsw-alias-label-tertiary,#999)}
|
|
2875
|
+
/* The transfer dialog is deliberately a block: it holds a scrollable account
|
|
2876
|
+
list and per-entry checkboxes, which cannot live in a one-row bar. */
|
|
2877
|
+
.dsm-workbuddy-transfer-dialog{margin-top:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;background:var(--dsw-alias-bg-secondary,transparent)}
|
|
2878
|
+
.dsm-workbuddy-transfer-dialog p{margin:0 0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e9ed)}
|
|
2879
|
+
.dsm-workbuddy-transfer-dialog-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
|
|
2880
|
+
.dsm-workbuddy-transfer-list{list-style:none;margin:0 0 8px;padding:0;max-height:180px;overflow-y:auto}
|
|
2881
|
+
.dsm-workbuddy-transfer-list label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);cursor:pointer;padding:2px 0}
|
|
2882
|
+
.dsm-workbuddy-transfer-list input[type="checkbox"]{flex:none}
|
|
2883
|
+
.dsm-workbuddy-transfer input[type="file"]{font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);margin-bottom:8px}
|
|
2884
|
+
/* The import picker's input is driven by the 「import」 button, so it must not
|
|
2885
|
+
render as a visible field: hide it from layout AND from assistive tech (the
|
|
2886
|
+
button carries the accessible name). Not display:none alone — that also
|
|
2887
|
+
removes it from the tab order in some engines, which is fine here, but the
|
|
2888
|
+
clip-based form is the one that reliably keeps programmatic .click() working
|
|
2889
|
+
across browsers. */
|
|
2890
|
+
.dsm-workbuddy-transfer-file{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
2891
|
+
/* The sign-in URL inside the OAuth dialog: long https links must wrap rather
|
|
2892
|
+
than push the dialog wide — break-all, since URLs have no soft wrap points. */
|
|
2893
|
+
.dsm-workbuddy-oauth-url{word-break:break-all;overflow-wrap:anywhere}
|
|
2894
|
+
.dsm-workbuddy-oauth-url a{color:var(--dsw-alias-brand-primary,#5686fe);text-decoration:none}
|
|
2895
|
+
.dsm-workbuddy-oauth-url a:hover{text-decoration:underline}
|
|
2896
|
+
/* The shared busy veil: a fixed overlay for operations whose result only shows
|
|
2897
|
+
up after the list re-read (remove / import / sign-in).
|
|
2898
|
+
It is NOT a modal: no control, no dismissal — only "this is running" and
|
|
2899
|
+
"here is why nothing moved yet". Fixed positioning so it covers the card
|
|
2900
|
+
regardless of which section started the operation, and a semi-transparent
|
|
2901
|
+
backdrop so the table stays visible underneath — the user is watching a
|
|
2902
|
+
specific row change, and hiding it would defeat the point. */
|
|
2903
|
+
.dsm-workbuddy-veil{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.38);cursor:progress}
|
|
2904
|
+
.dsm-workbuddy-veil-card{display:flex;flex-direction:column;align-items:center;gap:8px;max-width:min(340px,80vw);padding:16px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-primary,#1b1c20);box-shadow:0 8px 28px rgba(0,0,0,.35)}
|
|
2905
|
+
.dsm-workbuddy-veil-text{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#e8e9ed);text-align:center}
|
|
2906
|
+
.dsm-workbuddy-veil-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999);text-align:center}
|
|
2907
|
+
/* The spinner is the one thing that proves the UI is alive when the row under
|
|
2908
|
+
it has not changed yet — which is precisely the confusion the veil exists to
|
|
2909
|
+
remove. Reduced-motion users get the static ring instead of a rotation. */
|
|
2910
|
+
.dsm-workbuddy-veil-spinner{width:18px;height:18px;border:2px solid var(--dsw-alias-border-l2,#3a3d45);border-top-color:var(--dsw-alias-brand-primary,#5686fe);border-radius:50%;animation:dsm-workbuddy-veil-spin .7s linear infinite}
|
|
2911
|
+
@keyframes dsm-workbuddy-veil-spin{to{transform:rotate(360deg)}}
|
|
2912
|
+
@media (prefers-reduced-motion: reduce){.dsm-workbuddy-veil-spinner{animation:none}}
|
|
1931
2913
|
`;
|
|
1932
2914
|
/**
|
|
1933
2915
|
* Composer credit readout styles, injected by the readout component itself.
|
|
@@ -2557,8 +3539,18 @@ window.__ModuleLoader__.load({
|
|
|
2557
3539
|
accounts: [],
|
|
2558
3540
|
selectionExplicit: false
|
|
2559
3541
|
};
|
|
3542
|
+
/**
|
|
3543
|
+
* Whether the ACTIVE region knows of any account at all.
|
|
3544
|
+
*
|
|
3545
|
+
* This is the poll's precondition in place of `status === 'signed-in'`. The
|
|
3546
|
+
* signed-in flag means "a credential resolves", which is false for a region
|
|
3547
|
+
* whose accounts exist but none is usable yet — exactly the state an import
|
|
3548
|
+
* or a fresh OAuth sign-in creates. Polling on it meant those regions never
|
|
3549
|
+
* refreshed, so the list stayed empty until the card was reopened.
|
|
3550
|
+
*/
|
|
3551
|
+
const hasAccounts = status.status !== "error" && status.accounts.length > 0;
|
|
2560
3552
|
(0, react.useEffect)(() => {
|
|
2561
|
-
if (!open ||
|
|
3553
|
+
if (!open || !hasAccounts) return;
|
|
2562
3554
|
const controller = new AbortController();
|
|
2563
3555
|
const timer = window.setInterval(() => {
|
|
2564
3556
|
refreshUsage(activeRegion, controller.signal);
|
|
@@ -2588,6 +3580,7 @@ window.__ModuleLoader__.load({
|
|
|
2588
3580
|
*/
|
|
2589
3581
|
const rescanAccounts = async () => {
|
|
2590
3582
|
setBusy(true);
|
|
3583
|
+
setAccountError(void 0);
|
|
2591
3584
|
try {
|
|
2592
3585
|
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNTS_REFRESH_PATH, activeRegion), {
|
|
2593
3586
|
method: "POST",
|
|
@@ -2596,7 +3589,10 @@ window.__ModuleLoader__.load({
|
|
|
2596
3589
|
});
|
|
2597
3590
|
const body = await response.json();
|
|
2598
3591
|
if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
|
|
3592
|
+
if (body.dismissalsCleared === false) setAccountError(t("row.rescanHidesKept"));
|
|
2599
3593
|
await refreshUsage(activeRegion);
|
|
3594
|
+
} catch (error) {
|
|
3595
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
2600
3596
|
} finally {
|
|
2601
3597
|
if (mounted.current) setBusy(false);
|
|
2602
3598
|
}
|
|
@@ -2945,6 +3941,27 @@ window.__ModuleLoader__.load({
|
|
|
2945
3941
|
searched
|
|
2946
3942
|
});
|
|
2947
3943
|
status.status === "error" || status.selectionExplicit;
|
|
3944
|
+
/**
|
|
3945
|
+
* Account-adding and account-migration controls, defined ONCE and rendered
|
|
3946
|
+
* in both sign-in states: inside the pool settings block when an account
|
|
3947
|
+
* exists, and in the signed-out branch when it does not (the pool section is
|
|
3948
|
+
* not rendered there at all). Two JSX copies would be free to drift, and the
|
|
3949
|
+
* two states are the same feature — a user who learns these controls in one
|
|
3950
|
+
* must find them in the same order in the other.
|
|
3951
|
+
*/
|
|
3952
|
+
const accountTools = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OAuthSignIn, {
|
|
3953
|
+
t,
|
|
3954
|
+
region: activeRegion,
|
|
3955
|
+
onAdded: () => refreshUsage(activeRegion)
|
|
3956
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
|
|
3957
|
+
t,
|
|
3958
|
+
region: activeRegion,
|
|
3959
|
+
onChanged: () => refreshUsage(activeRegion),
|
|
3960
|
+
accounts: status.status === "error" ? [] : status.accounts.map((account) => ({
|
|
3961
|
+
id: account.id,
|
|
3962
|
+
accountName: account.accountName
|
|
3963
|
+
}))
|
|
3964
|
+
})] });
|
|
2948
3965
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2949
3966
|
className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
|
|
2950
3967
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -3045,6 +4062,11 @@ window.__ModuleLoader__.load({
|
|
|
3045
4062
|
className: "dsm-workbuddy-usage-hint",
|
|
3046
4063
|
children: t("row.reloginHint")
|
|
3047
4064
|
}) : null,
|
|
4065
|
+
accountError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4066
|
+
className: "dsm-workbuddy-account-error",
|
|
4067
|
+
role: "alert",
|
|
4068
|
+
children: accountError
|
|
4069
|
+
}),
|
|
3048
4070
|
selectionLost ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3049
4071
|
className: "dsm-workbuddy-usage-hint",
|
|
3050
4072
|
children: t("row.selectionLostHint")
|
|
@@ -3091,12 +4113,11 @@ window.__ModuleLoader__.load({
|
|
|
3091
4113
|
switchAccount(accountId);
|
|
3092
4114
|
},
|
|
3093
4115
|
selectingAccount: switchingAccount,
|
|
4116
|
+
accountTools,
|
|
3094
4117
|
siblingBusy: saving || togglingRegion !== void 0,
|
|
3095
4118
|
onBusyChange: setPoolBusy,
|
|
3096
4119
|
onSaved: async () => await refreshUsage(activeRegion) !== void 0,
|
|
3097
|
-
onRefresh: () =>
|
|
3098
|
-
refreshUsage(activeRegion);
|
|
3099
|
-
}
|
|
4120
|
+
onRefresh: () => refreshUsage(activeRegion)
|
|
3100
4121
|
}),
|
|
3101
4122
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
3102
4123
|
className: "dsm-workbuddy-models",
|
|
@@ -3329,6 +4350,10 @@ window.__ModuleLoader__.load({
|
|
|
3329
4350
|
children: busy ? t("row.accountsScanning") : t("row.accountsRescan")
|
|
3330
4351
|
})
|
|
3331
4352
|
}),
|
|
4353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4354
|
+
className: "dsm-workbuddy-pool-tools",
|
|
4355
|
+
children: accountTools
|
|
4356
|
+
}),
|
|
3332
4357
|
searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchedPaths, {
|
|
3333
4358
|
items: searched,
|
|
3334
4359
|
t
|
|
@@ -3940,6 +4965,7 @@ window.__ModuleLoader__.load({
|
|
|
3940
4965
|
"row.sourceDsh": "plugin copy",
|
|
3941
4966
|
"row.searchedMore": "Show the {count} path(s) that were not found",
|
|
3942
4967
|
"row.reloginHint": "If errors occur, sign back in to the WorkBuddy app.",
|
|
4968
|
+
"row.rescanHidesKept": "The accounts you removed are still hidden: the plugin could not update its own list file. On Windows this is usually a file lock — close antivirus or sync tools that may be holding files in the DSH profile, then try again.",
|
|
3943
4969
|
"row.credentialRejectedTitle": "WorkBuddy refused this account's token",
|
|
3944
4970
|
"row.credentialRejectedIntro": "The token for \"{accountName}\" is no longer accepted by the upstream, so credits and check-in are unavailable. Signing in again only helps if the problem is the token itself — here is what actually applies:",
|
|
3945
4971
|
"row.credentialRejectedSwitch": "Another sign-in on this machine answered the upstream: \"{accountName}\". Switching to it fixes this — no need to sign in again.",
|
|
@@ -4006,6 +5032,43 @@ window.__ModuleLoader__.load({
|
|
|
4006
5032
|
"row.probeFailedStatus": "✗ Test failed (HTTP {status})",
|
|
4007
5033
|
"row.probeError": "Could not run the test: {message}",
|
|
4008
5034
|
"row.cheer": "Star on GitHub",
|
|
5035
|
+
"row.oauthTitle": "Add an account by QR sign-in",
|
|
5036
|
+
"row.oauthStart": "Sign in with QR code",
|
|
5037
|
+
"row.oauthStarting": "Opening…",
|
|
5038
|
+
"row.oauthOpenHint": "Open the sign-in page and confirm — the account is added here automatically when you finish:",
|
|
5039
|
+
"row.oauthOpened": "Opened. Waiting for you to finish…",
|
|
5040
|
+
"row.oauthWaiting": "Waiting for the sign-in to finish… ({seconds}s)",
|
|
5041
|
+
"row.oauthExpires": "The sign-in window expires in about {minutes} min.",
|
|
5042
|
+
"row.oauthCancel": "Cancel",
|
|
5043
|
+
"row.oauthCancelled": "Sign-in cancelled.",
|
|
5044
|
+
"row.oauthTimedOut": "The sign-in timed out — start again when ready.",
|
|
5045
|
+
"row.oauthDone": "Added “{accountName}”. Pick it above to use it.",
|
|
5046
|
+
"row.oauthError": "Sign-in failed: {message}",
|
|
5047
|
+
"row.oauthCopy": "Copy link",
|
|
5048
|
+
"row.oauthCopied": "Link copied",
|
|
5049
|
+
"row.oauthReopen": "Open page again",
|
|
5050
|
+
"row.oauthDialogClose": "Close",
|
|
5051
|
+
"row.oauthTitleGlobal": "Add an account by web sign-in",
|
|
5052
|
+
"row.oauthStartGlobal": "Sign in on the web",
|
|
5053
|
+
"row.veilHintList": "The list refreshes as soon as this finishes.",
|
|
5054
|
+
"row.oauthSyncing": "Adding the account…",
|
|
5055
|
+
"row.transferTitle": "Export / import accounts",
|
|
5056
|
+
"row.transferHint": "Move accounts between computers as a JSON file. The file carries the credentials — keep it private. The format is shared with other WorkBuddy credential managers.",
|
|
5057
|
+
"row.transferExport": "Export accounts",
|
|
5058
|
+
"row.transferExporting": "Exporting…",
|
|
5059
|
+
"row.transferExportPick": "Choose the accounts to export ({total} available):",
|
|
5060
|
+
"row.transferExportConfirm": "Export {count} account(s)",
|
|
5061
|
+
"row.transferExported": "Exported {count} account(s) to {fileName}.",
|
|
5062
|
+
"row.transferImport": "Import accounts",
|
|
5063
|
+
"row.transferImporting": "Importing…",
|
|
5064
|
+
"row.transferPreviewing": "Reading the file…",
|
|
5065
|
+
"row.transferPreviewTotal": "{total} account(s) in the file — untick what should stay out. Accounts already stored here are overwritten by the import.",
|
|
5066
|
+
"row.transferImportConfirm": "Import {count} account(s)",
|
|
5067
|
+
"row.transferImported": "Imported {imported} account(s); {skipped} skipped (no token, or belongs to the other region).",
|
|
5068
|
+
"row.transferEntryUnknown": "unknown account",
|
|
5069
|
+
"row.transferNoToken": "no token in this record",
|
|
5070
|
+
"row.transferCancel": "Cancel",
|
|
5071
|
+
"row.transferError": "Transfer failed: {message}",
|
|
4009
5072
|
"row.poolTitle": "Account pool",
|
|
4010
5073
|
"row.poolNoCandidate": "No account in this pool can be used right now.",
|
|
4011
5074
|
"row.poolNoCandidateHint": "Members are rate-limited, out of credits, rejected, or unreachable. Rate limits, drained quotas and transient failures return to the pool on their own — at the time the upstream states, or after a 30-minute fallback. Only a rejected credential needs a new sign-in.",
|
|
@@ -4104,6 +5167,13 @@ window.__ModuleLoader__.load({
|
|
|
4104
5167
|
"row.poolCurrentHeader": "In use: {account}",
|
|
4105
5168
|
"row.poolNotMember": "Not in pool",
|
|
4106
5169
|
"row.poolUnsavedMembers": "Selection changed — save to apply",
|
|
5170
|
+
"row.poolRowTest": "Test",
|
|
5171
|
+
"row.poolRowRemove": "Remove",
|
|
5172
|
+
"row.poolRowRemoveHint": "Remove this account from the list. The WorkBuddy desktop app is not signed out — it keeps its own sign-in. Removing only hides the account here; 「Detect accounts again」 brings it back.",
|
|
5173
|
+
"row.poolRowRemoveAria": "Remove the stored credential of {accountName}",
|
|
5174
|
+
"row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
|
|
5175
|
+
"row.poolRowRemoving": "Removing the account…",
|
|
5176
|
+
"row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
|
|
4107
5177
|
"row.showCreditsCn": "Show WB CN credits in the composer",
|
|
4108
5178
|
"row.showCreditsGlobal": "Show WB AI credits in the composer",
|
|
4109
5179
|
"row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
|
|
@@ -4155,6 +5225,7 @@ window.__ModuleLoader__.load({
|
|
|
4155
5225
|
"row.sourceDsh": "插件副本",
|
|
4156
5226
|
"row.searchedMore": "显示另外 {count} 条「不存在」的路径",
|
|
4157
5227
|
"row.reloginHint": "出现错误,重新登录 WorkBuddy APP 即可。",
|
|
5228
|
+
"row.rescanHidesKept": "已删除的账号仍是隐藏状态:插件没能更新自己的名单文件。Windows 上通常是文件被占用——请关闭可能正在占用 DSH 配置目录的杀毒/同步工具后重试。",
|
|
4158
5229
|
"row.credentialRejectedTitle": "WorkBuddy 拒绝了这个账号的 token",
|
|
4159
5230
|
"row.credentialRejectedIntro": "「{accountName}」的 token 已不被上游接受,所以积分与签到都用不了。重新登录只在「确实是 token 的问题」时才有用——当前实际适用的是下面这一条:",
|
|
4160
5231
|
"row.credentialRejectedSwitch": "本机另一个账号「{accountName}」刚刚实测可用,切换到它即可恢复,不必重新登录。",
|
|
@@ -4221,6 +5292,43 @@ window.__ModuleLoader__.load({
|
|
|
4221
5292
|
"row.probeFailedStatus": "✗ 测试失败(HTTP {status})",
|
|
4222
5293
|
"row.probeError": "无法执行测试:{message}",
|
|
4223
5294
|
"row.cheer": "鼓励一下",
|
|
5295
|
+
"row.oauthTitle": "扫码添加账号",
|
|
5296
|
+
"row.oauthStart": "扫码登录",
|
|
5297
|
+
"row.oauthStarting": "正在打开…",
|
|
5298
|
+
"row.oauthOpenHint": "打开登录页并完成确认——完成后账号会自动出现在这里:",
|
|
5299
|
+
"row.oauthOpened": "已打开,等待你完成登录…",
|
|
5300
|
+
"row.oauthWaiting": "正在等待登录完成…({seconds} 秒)",
|
|
5301
|
+
"row.oauthExpires": "登录窗口约 {minutes} 分钟后过期。",
|
|
5302
|
+
"row.oauthCancel": "取消",
|
|
5303
|
+
"row.oauthCancelled": "已取消登录。",
|
|
5304
|
+
"row.oauthTimedOut": "登录超时——请重新开始。",
|
|
5305
|
+
"row.oauthDone": "已添加「{accountName}」。在上方选中它即可使用。",
|
|
5306
|
+
"row.oauthError": "登录失败:{message}",
|
|
5307
|
+
"row.oauthCopy": "复制链接",
|
|
5308
|
+
"row.oauthCopied": "已复制链接",
|
|
5309
|
+
"row.oauthReopen": "重新打开页面",
|
|
5310
|
+
"row.oauthDialogClose": "关闭",
|
|
5311
|
+
"row.oauthTitleGlobal": "网页登录添加账号",
|
|
5312
|
+
"row.oauthStartGlobal": "网页登录",
|
|
5313
|
+
"row.veilHintList": "完成后列表会立即刷新。",
|
|
5314
|
+
"row.oauthSyncing": "正在添加账号…",
|
|
5315
|
+
"row.transferTitle": "导出 / 导入账号",
|
|
5316
|
+
"row.transferHint": "用 JSON 文件在电脑之间迁移账号。文件里是账号凭据——请妥善保管。该格式与同类 WorkBuddy 凭据管理工具通用。",
|
|
5317
|
+
"row.transferExport": "导出账号",
|
|
5318
|
+
"row.transferExporting": "正在导出…",
|
|
5319
|
+
"row.transferExportPick": "选择要导出的账号(共 {total} 个):",
|
|
5320
|
+
"row.transferExportConfirm": "导出 {count} 个账号",
|
|
5321
|
+
"row.transferExported": "已导出 {count} 个账号到 {fileName}。",
|
|
5322
|
+
"row.transferImport": "导入账号",
|
|
5323
|
+
"row.transferImporting": "正在导入…",
|
|
5324
|
+
"row.transferPreviewing": "正在读取文件…",
|
|
5325
|
+
"row.transferPreviewTotal": "文件中共 {total} 个账号——不需要的请取消勾选。导入会覆盖这里已存储的同名账号。",
|
|
5326
|
+
"row.transferImportConfirm": "导入 {count} 个账号",
|
|
5327
|
+
"row.transferImported": "已导入 {imported} 个账号;跳过 {skipped} 个(缺 token,或属于另一个区域)。",
|
|
5328
|
+
"row.transferEntryUnknown": "未知账号",
|
|
5329
|
+
"row.transferNoToken": "此记录没有 token",
|
|
5330
|
+
"row.transferCancel": "取消",
|
|
5331
|
+
"row.transferError": "传输失败:{message}",
|
|
4224
5332
|
"row.poolTitle": "账号池",
|
|
4225
5333
|
"row.poolNoCandidate": "账号池里目前没有可用的账号。",
|
|
4226
5334
|
"row.poolNoCandidateHint": "成员要么在限流冷却中、要么积分耗尽、要么凭据被拒、要么上游无响应。限流、积分耗尽与瞬时故障都会自动回池——按上游给出的恢复时间,没给时间就按 30 分钟兜底,不需要你操作;只有「凭据被拒」需要重新登录。",
|
|
@@ -4319,6 +5427,13 @@ window.__ModuleLoader__.load({
|
|
|
4319
5427
|
"row.poolCurrentHeader": "当前使用:{account}",
|
|
4320
5428
|
"row.poolNotMember": "未入池",
|
|
4321
5429
|
"row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
|
|
5430
|
+
"row.poolRowTest": "测试",
|
|
5431
|
+
"row.poolRowRemove": "删除",
|
|
5432
|
+
"row.poolRowRemoveHint": "把这个账号从列表中移除。桌面 App 不会被退出登录——它保留自己的登录状态。移除只是暂时隐藏,点「重新检测账号」即可找回。",
|
|
5433
|
+
"row.poolRowRemoveAria": "删除 {accountName} 的已存凭据",
|
|
5434
|
+
"row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
|
|
5435
|
+
"row.poolRowRemoving": "正在删除账号…",
|
|
5436
|
+
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
4322
5437
|
"row.showCreditsCn": "在对话框显示 WB CN 积分",
|
|
4323
5438
|
"row.showCreditsGlobal": "在对话框显示 WB AI 积分",
|
|
4324
5439
|
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|