dsh-connect-workbuddy 1.4.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.
package/lib/client.js CHANGED
@@ -29,6 +29,17 @@ window.__ModuleLoader__.load({
29
29
  const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
30
30
  /** Plugin-owned daily check-in action endpoint. */
31
31
  const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
32
+ /** Query parameter naming the region a card request addresses. */
33
+ const WORKBUDDY_REGION_PARAM = "region";
34
+ /** Every region, in card tab order. */
35
+ const WORKBUDDY_REGIONS = ["cn", "global"];
36
+ /**
37
+ * Address one region's status route. The two regions are separate provider
38
+ * stacks; every card request carries the region whose tab the user is on.
39
+ */
40
+ function withWorkBuddyRegion(path, region) {
41
+ return `${path}?${WORKBUDDY_REGION_PARAM}=${region}`;
42
+ }
32
43
  /**
33
44
  * Project one card row into its persisted `lastCatalog` shape: the native
34
45
  * context window becomes the stored `contextWindow`, and the card-only
@@ -70,7 +81,8 @@ window.__ModuleLoader__.load({
70
81
  * dingminhua/dsh-subagent-default-model(MIT)的 SETTINGS_CSS。
71
82
  * 沿用目的是让两个插件共享同一套外部表现语言。
72
83
  * 改动:类名由 `dsm-trae-*` 改为 `dsm-workbuddy-*`;
73
- * 移除 trae 特有的 1M 变体样式,新增按套餐聚合的积分行样式。
84
+ * 移除 trae 特有的 1M 变体样式,新增按套餐聚合的积分行样式;
85
+ * 双 provider 化后新增国内版/国际版 tab 栏样式。
74
86
  *
75
87
  * @module dsh-connect-workbuddy/client/styles
76
88
  */
@@ -95,6 +107,12 @@ window.__ModuleLoader__.load({
95
107
  .dsm-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
96
108
  .dsm-btn-primary:hover:not(:disabled){opacity:.9}
97
109
  .dsm-workbuddy-usage{display:flex;flex-direction:column;gap:16px;margin:0;padding:16px 0 4px}
110
+ .dsm-workbuddy-tabs{display:flex;gap:6px;padding:4px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
111
+ .dsm-workbuddy-tab{appearance:none;font:inherit;cursor:pointer;flex:1;border:0;border-radius:7px;padding:7px 10px;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;font-weight:500;line-height:18px;background:transparent;transition:color .15s,background .15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
112
+ .dsm-workbuddy-tab:hover:not(:disabled):not(.dsm-workbuddy-tab-active){color:var(--dsw-alias-label-primary,#e6e6e6)}
113
+ .dsm-workbuddy-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
114
+ .dsm-workbuddy-tab-active{color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-2,#232529);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2,#3a3d45)}
115
+ .dsm-workbuddy-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;vertical-align:baseline}
98
116
  .dsm-workbuddy-usage-account{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:14px;background:var(--dsw-alias-bg-layer-2,#24262c)}
99
117
  .dsm-workbuddy-usage-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
100
118
  .dsm-workbuddy-usage-expiry{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
@@ -177,12 +195,15 @@ window.__ModuleLoader__.load({
177
195
  * 60 秒轮询与 AbortController 清理、以及
178
196
  * `IconChevronDownOutline14` 的使用,均来自该项目的 TraeUsageCard。
179
197
  * 折叠卡片外壳与 `settings.plugin.item` 槽位形态来自
180
- * dingminhua/dsh-subagent-default-model(MIT)。
198
+ * dingminhua/dsh-subagent-default-model(MIT)。
181
199
  * 改动:
182
200
  * 1. 积分区改为「合计 + 按套餐名聚合的进度条」,因为实测单个账号下
183
201
  * 同名套餐可达 19 个,逐条渲染会淹没卡片(原项目按上游条目直出);
184
202
  * 2. 模型行补上 WorkBuddy 上游给出的积分倍率、多模态与推理档位;
185
- * 3. 移除与 WorkBuddy 上游无关的 1M 变体勾选。
203
+ * 3. 移除与 WorkBuddy 上游无关的 1M 变体勾选;
204
+ * 4. 双 provider 化后卡片顶部为「国内版 / 国际版」tab 栏 —— 每个 tab
205
+ * 是一个独立供应商(workbuddy / workbuddy-global),账号、积分、
206
+ * 模型目录与草稿完全按区域隔离,切 tab 不丢另一侧未保存的草稿。
186
207
  *
187
208
  * @module dsh-connect-workbuddy/client/WorkBuddyCard
188
209
  */
@@ -234,20 +255,33 @@ window.__ModuleLoader__.load({
234
255
  function dotStyle(status) {
235
256
  return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
236
257
  }
258
+ /** Read the per-region account selections out of the settings snapshot. */
259
+ function configuredAccountsOf(configured) {
260
+ const accounts = configured?.accounts;
261
+ return typeof accounts === "object" && accounts !== null ? accounts : {};
262
+ }
237
263
  /** Render WorkBuddy sign-in state, credits, and model selection as one card. */
238
264
  function WorkBuddyCard({ t, settingsScope }) {
239
265
  if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
240
266
  const [open, setOpen] = (0, react.useState)(false);
241
- const [status, setStatus] = (0, react.useState)({
242
- status: "signed-out",
243
- accounts: []
267
+ /** The region whose tab is on screen; each tab is its own provider stack. */
268
+ const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
269
+ /** Last-known usage per region, so tab dots survive tab switches. */
270
+ const [statusByRegion, setStatusByRegion] = (0, react.useState)({
271
+ cn: {
272
+ status: "signed-out",
273
+ accounts: []
274
+ },
275
+ global: {
276
+ status: "signed-out",
277
+ accounts: []
278
+ }
244
279
  });
245
280
  const [busy, setBusy] = (0, react.useState)(false);
246
281
  const [settingsRevision, setSettingsRevision] = (0, react.useState)(0);
247
- const [draftModels, setDraftModels] = (0, react.useState)(void 0);
248
- const [draftEnabledIds, setDraftEnabledIds] = (0, react.useState)(void 0);
249
- const [draftImageIds, setDraftImageIds] = (0, react.useState)(void 0);
250
- const [draftContextBudgets, setDraftContextBudgets] = (0, react.useState)(void 0);
282
+ /** Per-region unsaved model edits; a draft on one tab is never dropped by
283
+ * switching to the other tab, only by that tab's discard/save. */
284
+ const [drafts, setDrafts] = (0, react.useState)({});
251
285
  const [saving, setSaving] = (0, react.useState)(false);
252
286
  /** Save failure surfaced next to the buttons; cleared by the next attempt. */
253
287
  const [saveError, setSaveError] = (0, react.useState)(void 0);
@@ -264,38 +298,53 @@ window.__ModuleLoader__.load({
264
298
  (0, react.useEffect)(() => settingsScope?.subscribe(() => {
265
299
  setSettingsRevision((value) => value + 1);
266
300
  }), [settingsScope]);
267
- const refreshUsage = (0, react.useCallback)(async (signal) => {
301
+ const refreshUsage = (0, react.useCallback)(async (region, signal) => {
268
302
  try {
269
- const response = await fetch(WORKBUDDY_USAGE_PATH, {
303
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, region), {
270
304
  headers: { accept: "application/json" },
271
305
  credentials: "same-origin",
272
306
  ...signal === void 0 ? {} : { signal }
273
307
  });
274
308
  const value = await response.json().catch(() => void 0);
275
309
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
276
- if (mounted.current && signal?.aborted !== true) setStatus(value);
277
- return value;
310
+ const usage = value;
311
+ if (mounted.current && signal?.aborted !== true) setStatusByRegion((prev) => ({
312
+ ...prev,
313
+ [region]: usage
314
+ }));
315
+ return usage;
278
316
  } catch (error) {
279
- if (mounted.current && signal?.aborted !== true) setStatus({
280
- status: "error",
281
- message: error instanceof Error ? error.message : t("row.requestFailed")
282
- });
317
+ if (mounted.current && signal?.aborted !== true) setStatusByRegion((prev) => ({
318
+ ...prev,
319
+ [region]: {
320
+ status: "error",
321
+ message: error instanceof Error ? error.message : t("row.requestFailed")
322
+ }
323
+ }));
283
324
  return;
284
325
  }
285
326
  }, [t]);
286
327
  (0, react.useEffect)(() => {
287
328
  if (!open) return;
288
329
  const controller = new AbortController();
289
- refreshUsage(controller.signal);
330
+ refreshUsage(activeRegion, controller.signal);
290
331
  return () => {
291
332
  controller.abort();
292
333
  };
293
- }, [open, refreshUsage]);
334
+ }, [
335
+ open,
336
+ activeRegion,
337
+ refreshUsage
338
+ ]);
339
+ const status = statusByRegion[activeRegion] ?? {
340
+ status: "signed-out",
341
+ accounts: []
342
+ };
294
343
  (0, react.useEffect)(() => {
295
344
  if (!open || status.status !== "signed-in") return;
296
345
  const controller = new AbortController();
297
346
  const timer = window.setInterval(() => {
298
- refreshUsage(controller.signal);
347
+ refreshUsage(activeRegion, controller.signal);
299
348
  }, POLL_INTERVAL_MS);
300
349
  return () => {
301
350
  window.clearInterval(timer);
@@ -303,13 +352,14 @@ window.__ModuleLoader__.load({
303
352
  };
304
353
  }, [
305
354
  open,
355
+ activeRegion,
306
356
  refreshUsage,
307
357
  status.status
308
358
  ]);
309
359
  const rescanAccounts = async () => {
310
360
  setBusy(true);
311
361
  try {
312
- const response = await fetch(WORKBUDDY_ACCOUNTS_REFRESH_PATH, {
362
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNTS_REFRESH_PATH, activeRegion), {
313
363
  method: "POST",
314
364
  headers: { accept: "application/json" },
315
365
  credentials: "same-origin"
@@ -317,10 +367,13 @@ window.__ModuleLoader__.load({
317
367
  const body = await response.json();
318
368
  if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
319
369
  const selected = body.accounts.find((account) => account.selected)?.id;
320
- const configured = settingsScope?.getSnapshot().value;
321
- const configuredId = typeof configured === "object" && configured !== null && typeof configured.accountId === "string" ? configured.accountId : void 0;
322
- if (selected !== void 0 && selected !== configuredId && settingsScope?.getSnapshot().writable === true) await settingsScope.set("accountId", selected);
323
- await refreshUsage();
370
+ const configuredAccounts = configuredAccountsOf(settingsScope?.getSnapshot().value);
371
+ const configuredId = configuredAccounts[activeRegion];
372
+ if (selected !== void 0 && selected !== configuredId && settingsScope?.getSnapshot().writable === true) await settingsScope.set("accounts", {
373
+ ...configuredAccounts,
374
+ [activeRegion]: selected
375
+ });
376
+ await refreshUsage(activeRegion);
324
377
  } finally {
325
378
  if (mounted.current) setBusy(false);
326
379
  }
@@ -329,8 +382,12 @@ window.__ModuleLoader__.load({
329
382
  if (settingsScope === void 0) return;
330
383
  setSwitchingAccount(true);
331
384
  try {
332
- await settingsScope.set("accountId", accountId);
333
- await refreshUsage();
385
+ const configuredAccounts = configuredAccountsOf(settingsScope.getSnapshot().value);
386
+ await settingsScope.set("accounts", {
387
+ ...configuredAccounts,
388
+ [activeRegion]: accountId
389
+ });
390
+ await refreshUsage(activeRegion);
334
391
  } finally {
335
392
  if (mounted.current) setSwitchingAccount(false);
336
393
  }
@@ -339,14 +396,14 @@ window.__ModuleLoader__.load({
339
396
  setCheckingIn(true);
340
397
  setCheckinActionError(void 0);
341
398
  try {
342
- const response = await fetch(WORKBUDDY_CHECKIN_PATH, {
399
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_CHECKIN_PATH, activeRegion), {
343
400
  method: "POST",
344
401
  headers: { accept: "application/json" },
345
402
  credentials: "same-origin"
346
403
  });
347
404
  const body = await response.json().catch(() => void 0);
348
405
  if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
349
- await refreshUsage();
406
+ await refreshUsage(activeRegion);
350
407
  } catch (error) {
351
408
  if (mounted.current) setCheckinActionError(error instanceof Error ? error.message : t("row.requestFailed"));
352
409
  } finally {
@@ -356,7 +413,7 @@ window.__ModuleLoader__.load({
356
413
  const refreshModels = async () => {
357
414
  setBusy(true);
358
415
  try {
359
- const response = await fetch(WORKBUDDY_MODELS_REFRESH_PATH, {
416
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_MODELS_REFRESH_PATH, activeRegion), {
360
417
  method: "POST",
361
418
  headers: { accept: "application/json" },
362
419
  credentials: "same-origin"
@@ -372,54 +429,86 @@ window.__ModuleLoader__.load({
372
429
  const budget = activeContextBudgets[id];
373
430
  if (typeof budget === "number") stillBudgets[id] = budget;
374
431
  }
375
- setDraftModels(fresh);
376
- setDraftEnabledIds(new Set(stillEnabled));
377
- setDraftImageIds(new Set(stillImages));
378
- setDraftContextBudgets(stillBudgets);
432
+ setDrafts((prev) => ({
433
+ ...prev,
434
+ [activeRegion]: {
435
+ models: fresh,
436
+ enabledIds: new Set(stillEnabled),
437
+ imageIds: new Set(stillImages),
438
+ contextBudgets: stillBudgets
439
+ }
440
+ }));
379
441
  } catch (error) {
380
- if (mounted.current) setStatus({
381
- status: "error",
382
- message: error instanceof Error ? error.message : t("row.requestFailed")
383
- });
442
+ if (mounted.current) setStatusByRegion((prev) => ({
443
+ ...prev,
444
+ [activeRegion]: {
445
+ status: "error",
446
+ message: error instanceof Error ? error.message : t("row.requestFailed")
447
+ }
448
+ }));
384
449
  } finally {
385
450
  if (mounted.current) setBusy(false);
386
451
  }
387
452
  };
388
- const visibleModels = draftModels ?? (status.status === "signed-in" ? status.models : []);
453
+ const draft = drafts[activeRegion];
454
+ const visibleModels = draft?.models ?? (status.status === "signed-in" ? status.models : []);
389
455
  const savedEnabledIds = status.status === "signed-in" ? new Set(status.enabledModelIds) : /* @__PURE__ */ new Set();
390
- const activeEnabledIds = draftEnabledIds ?? savedEnabledIds;
456
+ const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
391
457
  const savedImageIds = status.status === "signed-in" ? new Set(status.imageModelIds) : /* @__PURE__ */ new Set();
392
- const activeImageIds = draftImageIds ?? savedImageIds;
458
+ const activeImageIds = draft?.imageIds ?? savedImageIds;
393
459
  const configured = settingsScope?.getSnapshot().value;
394
460
  const configuredRegion = status.status === "signed-in" ? configured?.regions?.[status.region] : void 0;
395
461
  const legacyContextBudgets = status.status === "signed-in" && status.region === "cn" ? configured?.contextBudgets : void 0;
396
462
  const savedContextBudgetsSource = configuredRegion?.contextBudgets ?? legacyContextBudgets;
397
- const activeContextBudgets = draftContextBudgets ?? (typeof savedContextBudgetsSource === "object" && savedContextBudgetsSource !== null ? savedContextBudgetsSource : {});
398
- const dirty = draftModels !== void 0 || draftEnabledIds !== void 0 || draftImageIds !== void 0 || draftContextBudgets !== void 0;
463
+ const savedContextBudgets = typeof savedContextBudgetsSource === "object" && savedContextBudgetsSource !== null ? savedContextBudgetsSource : {};
464
+ const activeContextBudgets = draft?.contextBudgets ?? savedContextBudgets;
465
+ const dirty = draft !== void 0;
466
+ const editDraft = (edit) => {
467
+ setDrafts((prev) => ({
468
+ ...prev,
469
+ [activeRegion]: edit(prev[activeRegion] ?? {
470
+ models: [...visibleModels],
471
+ enabledIds: new Set(activeEnabledIds),
472
+ imageIds: new Set(activeImageIds),
473
+ contextBudgets: { ...activeContextBudgets }
474
+ })
475
+ }));
476
+ };
399
477
  const toggleModel = (modelId) => {
400
- const next = new Set(activeEnabledIds);
401
- if (!next.delete(modelId)) next.add(modelId);
402
- setDraftEnabledIds(next);
403
- setDraftModels([...visibleModels]);
478
+ editDraft((current) => {
479
+ const next = new Set(current.enabledIds);
480
+ if (!next.delete(modelId)) next.add(modelId);
481
+ return {
482
+ ...current,
483
+ enabledIds: next
484
+ };
485
+ });
404
486
  };
405
487
  const toggleImage = (modelId) => {
406
- const next = new Set(activeImageIds);
407
- if (!next.delete(modelId)) next.add(modelId);
408
- setDraftImageIds(next);
409
- setDraftModels([...visibleModels]);
488
+ editDraft((current) => {
489
+ const next = new Set(current.imageIds);
490
+ if (!next.delete(modelId)) next.add(modelId);
491
+ return {
492
+ ...current,
493
+ imageIds: next
494
+ };
495
+ });
410
496
  };
411
497
  const setContextBudget = (modelId, budget) => {
412
- setDraftContextBudgets({
413
- ...activeContextBudgets,
414
- [modelId]: budget
415
- });
416
- setDraftModels([...visibleModels]);
498
+ editDraft((current) => ({
499
+ ...current,
500
+ contextBudgets: {
501
+ ...current.contextBudgets,
502
+ [modelId]: budget
503
+ }
504
+ }));
417
505
  };
418
506
  const discardModels = () => {
419
- setDraftModels(void 0);
420
- setDraftEnabledIds(void 0);
421
- setDraftImageIds(void 0);
422
- setDraftContextBudgets(void 0);
507
+ setDrafts((prev) => {
508
+ const next = { ...prev };
509
+ delete next[activeRegion];
510
+ return next;
511
+ });
423
512
  };
424
513
  const saveModels = async () => {
425
514
  if (settingsScope === void 0) return;
@@ -438,7 +527,7 @@ window.__ModuleLoader__.load({
438
527
  }
439
528
  });
440
529
  discardModels();
441
- await refreshUsage();
530
+ await refreshUsage(activeRegion);
442
531
  } catch (error) {
443
532
  if (mounted.current) setSaveError(error instanceof Error ? error.message : t("row.requestFailed"));
444
533
  } finally {
@@ -485,6 +574,32 @@ window.__ModuleLoader__.load({
485
574
  children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
486
575
  className: "dsm-workbuddy-usage",
487
576
  children: [
577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
578
+ className: "dsm-workbuddy-tabs",
579
+ role: "tablist",
580
+ "aria-label": title,
581
+ children: WORKBUDDY_REGIONS.map((region) => {
582
+ const regionStatus = statusByRegion[region];
583
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
584
+ type: "button",
585
+ role: "tab",
586
+ "aria-selected": region === activeRegion,
587
+ className: `dsm-workbuddy-tab${region === activeRegion ? " dsm-workbuddy-tab-active" : ""}`,
588
+ onClick: () => {
589
+ setActiveRegion(region);
590
+ },
591
+ children: [regionStatus === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
592
+ "aria-hidden": "true",
593
+ className: "dsm-workbuddy-tab-dot",
594
+ style: dotStyle(regionStatus.status)
595
+ }), region === "cn" ? t("row.tabCn") : t("row.tabGlobal")]
596
+ }, region);
597
+ })
598
+ }),
599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
600
+ className: "dsm-workbuddy-models-summary",
601
+ children: t("row.tabHint")
602
+ }),
488
603
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
489
604
  className: "dsm-workbuddy-usage-account",
490
605
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -787,9 +902,12 @@ window.__ModuleLoader__.load({
787
902
  */
788
903
  const en = {
789
904
  "row.title": "WorkBuddy credits & models (dsh-connect-workbuddy)",
790
- "row.desc": "Use the models in the WorkBuddy desktop app directly in DSH, and see your remaining credits at a glance.",
905
+ "row.desc": "Use WorkBuddy models in DSH and see your remaining credits; the domestic and international sides are two independent providers, each with its own account — use both at the same time.",
791
906
  "row.expand": "Expand",
792
907
  "row.collapse": "Collapse",
908
+ "row.tabCn": "Domestic",
909
+ "row.tabGlobal": "Global",
910
+ "row.tabHint": "Each tab is a separate provider (workbuddy / workbuddy-global) with its own account, credits, and models. Both sides are live at once: different sessions can pick from either side, and changes on one tab never touch the other.",
793
911
  "row.signedOut": "Not signed in",
794
912
  "row.signedOutHint": "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
795
913
  "row.signedIn": "Signed in: {accountName}",
@@ -832,9 +950,12 @@ window.__ModuleLoader__.load({
832
950
  };
833
951
  const zh = {
834
952
  "row.title": "接入使用 WorkBuddy 积分与模型(dsh-connect-workbuddy)",
835
- "row.desc": "在 DSH 中直接使用 WorkBuddy 桌面 App 包含的模型,并随时查看剩余积分。",
953
+ "row.desc": "在 DSH 中使用 WorkBuddy 模型并随时查看剩余积分;国内版与国际版是两个独立供应商,各有自己的账号,可同时使用。",
836
954
  "row.expand": "展开",
837
955
  "row.collapse": "收起",
956
+ "row.tabCn": "国内版",
957
+ "row.tabGlobal": "国际版",
958
+ "row.tabHint": "每个 tab 是一个独立供应商(workbuddy / workbuddy-global),各有自己的账号、积分与模型。两边同时生效:不同会话可各选一边,一侧的改动不影响另一侧。",
838
959
  "row.signedOut": "未登录",
839
960
  "row.signedOutHint": "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
840
961
  "row.signedIn": "已登录:{accountName}",