@xmanrui/dsh-im 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +36 -0
  2. package/lib/client.js +709 -285
  3. package/lib/index.js +114 -110
  4. package/package.json +1 -1
  5. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  6. package/plugin-src/client/channels/dingtalk/index.js +67 -14
  7. package/plugin-src/client/channels/feishu/api.js +2 -0
  8. package/plugin-src/client/channels/feishu/index.js +70 -22
  9. package/plugin-src/client/channels/qq/api.js +2 -0
  10. package/plugin-src/client/channels/qq/index.js +45 -8
  11. package/plugin-src/client/channels/shared/token-api.js +2 -0
  12. package/plugin-src/client/channels/shared/token-channel.js +34 -8
  13. package/plugin-src/client/channels/wecom/api.js +2 -0
  14. package/plugin-src/client/channels/wecom/index.js +45 -8
  15. package/plugin-src/client/channels/weixin/api.js +2 -0
  16. package/plugin-src/client/channels/weixin/index.js +68 -8
  17. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  18. package/plugin-src/client/channels/whatsapp/index.js +27 -5
  19. package/plugin-src/client/i18n.js +13 -0
  20. package/plugin-src/client/styles.js +13 -0
  21. package/plugin-src/client/workspace-editor.js +96 -0
  22. package/plugin-src/client/workspace-snapshot-fence.js +28 -0
  23. package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
  24. package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
  25. package/plugin-src/host/channels/feishu/production.mjs +42 -5
  26. package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
  27. package/plugin-src/host/channels/qq/production.mjs +48 -22
  28. package/plugin-src/host/channels/qq/rpc.mjs +16 -2
  29. package/plugin-src/host/channels/shared/production.mjs +48 -22
  30. package/plugin-src/host/channels/shared/rpc.mjs +15 -0
  31. package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
  32. package/plugin-src/host/channels/slack/production.mjs +48 -23
  33. package/plugin-src/host/channels/slack/rpc.mjs +16 -0
  34. package/plugin-src/host/channels/wecom/production.mjs +49 -23
  35. package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
  36. package/plugin-src/host/channels/weixin/production.mjs +31 -11
  37. package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
  38. package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
  39. package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
  40. package/src/channels/dingtalk/dingtalk-bridge.mjs +25 -11
  41. package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
  42. package/src/channels/dingtalk/harness-client.mjs +17 -3
  43. package/src/channels/dingtalk/state-store.mjs +5 -0
  44. package/src/channels/discord/discord-api.mjs +1 -1
  45. package/src/channels/feishu/bridge.mjs +39 -16
  46. package/src/channels/feishu/harness-client.mjs +19 -5
  47. package/src/channels/feishu/state-store.mjs +5 -0
  48. package/src/channels/qq/qq-bridge.mjs +28 -15
  49. package/src/channels/qq/qq-controller.mjs +8 -1
  50. package/src/channels/qq/state-store.mjs +5 -0
  51. package/src/channels/shared/bot-workspace-store.mjs +618 -0
  52. package/src/channels/shared/conversation-state-store.mjs +5 -0
  53. package/src/channels/shared/text-harness-bridge.mjs +26 -13
  54. package/src/channels/shared/workspace-command.mjs +115 -0
  55. package/src/channels/shared/workspace-session.mjs +44 -0
  56. package/src/channels/wecom/state-store.mjs +5 -0
  57. package/src/channels/wecom/wecom-bridge.mjs +25 -13
  58. package/src/channels/wecom/wecom-controller.mjs +8 -1
  59. package/src/channels/weixin/harness-client.mjs +19 -5
  60. package/src/channels/weixin/state-store.mjs +5 -0
  61. package/src/channels/weixin/weixin-api.mjs +1 -1
  62. package/src/channels/weixin/weixin-bridge.mjs +19 -6
  63. package/src/channels/weixin/weixin-controller.mjs +6 -1
  64. package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
package/lib/client.js CHANGED
@@ -40,7 +40,7 @@ __export(index_exports, {
40
40
  name: () => name
41
41
  });
42
42
  module.exports = __toCommonJS(index_exports);
43
- var React13 = __toESM(require("react"), 1);
43
+ var React15 = __toESM(require("react"), 1);
44
44
 
45
45
  // plugin-src/client/channel-logos.js
46
46
  var React = __toESM(require("react"), 1);
@@ -181,7 +181,8 @@ var DINGTALK_ENDPOINTS = Object.freeze({
181
181
  cancelProvisioning: "provision.cancel",
182
182
  bindCredentials: "bot.bind-credentials",
183
183
  reconnectBot: "bot.reconnect",
184
- deleteBot: "bot.delete"
184
+ deleteBot: "bot.delete",
185
+ setWorkspace: "bot.workspace.set"
185
186
  });
186
187
  var ACCOUNT_STATES = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
187
188
  var SNAPSHOT_STATES = /* @__PURE__ */ new Set(["disconnected", "offline", "provisioning", "connected", "degraded"]);
@@ -301,6 +302,7 @@ function normalizeBot(value) {
301
302
  state,
302
303
  connected,
303
304
  configured: value.configured !== false,
305
+ workspace: optionalString(value.workspace, 4096) ?? "",
304
306
  bot: {
305
307
  name: optionalString(bot.name, 100) ?? "\u9489\u9489\u673A\u5668\u4EBA",
306
308
  clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? "\u5DF2\u5B89\u5168\u4FDD\u5B58"
@@ -353,7 +355,7 @@ function formatRemaining(milliseconds) {
353
355
  }
354
356
 
355
357
  // plugin-src/client/channels/dingtalk/index.js
356
- var React4 = __toESM(require("react"), 1);
358
+ var React6 = __toESM(require("react"), 1);
357
359
 
358
360
  // plugin-src/client/credential-binding.js
359
361
  var React3 = __toESM(require("react"), 1);
@@ -403,6 +405,19 @@ var EN = Object.freeze({
403
405
  "\u5DF2\u65AD\u5F00": "Disconnected",
404
406
  "\u6D88\u606F\u901A\u9053": "Message channel",
405
407
  "\u6700\u8FD1\u68C0\u67E5": "Last checked",
408
+ "\u5F53\u524D\u5DE5\u4F5C\u533A": "Current workspace",
409
+ "\u5DE5\u4F5C\u533A\u7EDD\u5BF9\u8DEF\u5F84": "Absolute workspace path",
410
+ "/\u7EDD\u5BF9\u8DEF\u5F84/\u5230/\u5DE5\u4F5C\u533A": "/absolute/path/to/workspace",
411
+ "\u4FEE\u6539": "Change",
412
+ "\u4FDD\u5B58": "Save",
413
+ "\u4FDD\u5B58\u4E2D\u2026": "Saving\u2026",
414
+ "\u672A\u8BBE\u7F6E": "Not set",
415
+ "\u5DE5\u4F5C\u533A\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not update the workspace. Try again.",
416
+ "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u533A\u7EDD\u5BF9\u8DEF\u5F84\u3002": "Enter an absolute workspace path.",
417
+ "\u5DE5\u4F5C\u533A\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\u3002": "The workspace must be an absolute path.",
418
+ "\u5DE5\u4F5C\u533A\u8DEF\u5F84\u4E0D\u5B58\u5728\u3002": "The workspace path does not exist.",
419
+ "\u5DE5\u4F5C\u533A\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u4E00\u4E2A\u76EE\u5F55\u3002": "The workspace path must point to a directory.",
420
+ "\u627E\u4E0D\u5230\u8981\u4FEE\u6539\u7684\u673A\u5668\u4EBA\u3002": "The bot could not be found.",
406
421
  "\u5C1A\u672A\u68C0\u67E5": "Not checked yet",
407
422
  "\u521A\u521A": "Just now",
408
423
  "\u68C0\u67E5\u8FDE\u63A5": "Check connection",
@@ -947,6 +962,132 @@ function CredentialBindingPanel({
947
962
  );
948
963
  }
949
964
 
965
+ // plugin-src/client/workspace-editor.js
966
+ var React4 = __toESM(require("react"), 1);
967
+ function looksLikeAbsolutePath(value) {
968
+ return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value);
969
+ }
970
+ function WorkspaceEditor({ workspace, disabled = false, onSave }) {
971
+ const [editing, setEditing] = React4.useState(false);
972
+ const [draft, setDraft] = React4.useState(workspace ?? "");
973
+ const [saving, setSaving] = React4.useState(false);
974
+ const [error, setError] = React4.useState(null);
975
+ const inputRef = React4.useRef(null);
976
+ const editButtonRef = React4.useRef(null);
977
+ const restoreFocus = React4.useRef(false);
978
+ React4.useEffect(() => {
979
+ if (!editing) setDraft(workspace ?? "");
980
+ }, [workspace, editing]);
981
+ React4.useEffect(() => {
982
+ if (editing) inputRef.current?.focus();
983
+ else if (restoreFocus.current) {
984
+ restoreFocus.current = false;
985
+ editButtonRef.current?.focus();
986
+ }
987
+ }, [editing]);
988
+ const cancel = () => {
989
+ setDraft(workspace ?? "");
990
+ setError(null);
991
+ restoreFocus.current = true;
992
+ setEditing(false);
993
+ };
994
+ const submit = async (event) => {
995
+ event.preventDefault();
996
+ const value = draft.trim();
997
+ if (!value || saving || disabled) return;
998
+ if (!looksLikeAbsolutePath(value)) {
999
+ setError("\u5DE5\u4F5C\u533A\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\u3002");
1000
+ return;
1001
+ }
1002
+ setSaving(true);
1003
+ setError(null);
1004
+ try {
1005
+ await onSave?.(value);
1006
+ restoreFocus.current = true;
1007
+ setEditing(false);
1008
+ } catch (cause) {
1009
+ setError(cause?.message ?? "\u5DE5\u4F5C\u533A\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002");
1010
+ } finally {
1011
+ setSaving(false);
1012
+ }
1013
+ };
1014
+ return h2(
1015
+ "div",
1016
+ { className: "dim-workspace" },
1017
+ h2(
1018
+ "div",
1019
+ { className: "dim-workspaceHeader" },
1020
+ h2("span", null, "\u5F53\u524D\u5DE5\u4F5C\u533A"),
1021
+ editing ? null : h2("button", {
1022
+ type: "button",
1023
+ ref: editButtonRef,
1024
+ className: "dim-workspaceEdit",
1025
+ onClick: () => {
1026
+ setEditing(true);
1027
+ setError(null);
1028
+ },
1029
+ disabled
1030
+ }, "\u4FEE\u6539")
1031
+ ),
1032
+ editing ? h2(
1033
+ "form",
1034
+ { className: "dim-workspaceForm", onSubmit: submit },
1035
+ h2("input", {
1036
+ ref: inputRef,
1037
+ value: draft,
1038
+ onChange: (event) => setDraft(event.target.value),
1039
+ placeholder: "/\u7EDD\u5BF9\u8DEF\u5F84/\u5230/\u5DE5\u4F5C\u533A",
1040
+ "aria-label": "\u5DE5\u4F5C\u533A\u7EDD\u5BF9\u8DEF\u5F84",
1041
+ autoCapitalize: "none",
1042
+ autoCorrect: "off",
1043
+ spellCheck: false,
1044
+ maxLength: 4096,
1045
+ required: true,
1046
+ disabled: saving || disabled
1047
+ }),
1048
+ h2(
1049
+ "div",
1050
+ { className: "dim-workspaceActions" },
1051
+ h2("button", {
1052
+ type: "submit",
1053
+ disabled: saving || disabled || !draft.trim()
1054
+ }, saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58"),
1055
+ h2("button", { type: "button", onClick: cancel, disabled: saving || disabled }, "\u53D6\u6D88")
1056
+ ),
1057
+ error ? h2("p", { className: "dim-workspaceError", role: "alert" }, error) : null
1058
+ ) : workspace ? React4.createElement("code", {
1059
+ className: "dim-workspacePath",
1060
+ title: workspace
1061
+ }, workspace) : h2("code", { className: "dim-workspacePath" }, "\u672A\u8BBE\u7F6E")
1062
+ );
1063
+ }
1064
+
1065
+ // plugin-src/client/workspace-snapshot-fence.js
1066
+ var React5 = __toESM(require("react"), 1);
1067
+ function useWorkspaceSnapshotFence() {
1068
+ const state = React5.useRef({ version: 0, pendingMutations: 0 });
1069
+ return React5.useMemo(() => Object.freeze({
1070
+ beginStatus() {
1071
+ return state.current.pendingMutations === 0 ? state.current.version : null;
1072
+ },
1073
+ canCommitStatus(version) {
1074
+ return version !== null && state.current.pendingMutations === 0 && state.current.version === version;
1075
+ },
1076
+ beginMutation() {
1077
+ state.current.pendingMutations += 1;
1078
+ state.current.version += 1;
1079
+ return state.current.version;
1080
+ },
1081
+ canCommitMutation(version) {
1082
+ return state.current.version === version;
1083
+ },
1084
+ endMutation() {
1085
+ state.current.pendingMutations = Math.max(0, state.current.pendingMutations - 1);
1086
+ return state.current.pendingMutations === 0;
1087
+ }
1088
+ }), []);
1089
+ }
1090
+
950
1091
  // plugin-src/client/channels/dingtalk/styles.js
951
1092
  var DINGTALK_STYLE_ID = "xmanrui-dsh-dingtalk-settings";
952
1093
  var CSS = String.raw`
@@ -1105,7 +1246,7 @@ function DingtalkIcon({ size = 28 }) {
1105
1246
  d: "M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498"
1106
1247
  }));
1107
1248
  }
1108
- var Button = React4.forwardRef(function Button2({ children, kind = "secondary", className = "", ...props }, ref) {
1249
+ var Button = React6.forwardRef(function Button2({ children, kind = "secondary", className = "", ...props }, ref) {
1109
1250
  return h2("button", {
1110
1251
  ...props,
1111
1252
  ref,
@@ -1201,13 +1342,13 @@ function EmptyView({ busy, onStart }) {
1201
1342
  );
1202
1343
  }
1203
1344
  function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
1204
- const [imageFailed, setImageFailed] = React4.useState(false);
1345
+ const [imageFailed, setImageFailed] = React6.useState(false);
1205
1346
  const source = safeQrSource(provision.qrCodeDataUrl);
1206
1347
  const remaining = Math.max(0, provision.expiresAt - now);
1207
1348
  const expired = remaining === 0 || provision.status === "expired";
1208
1349
  const duration = Math.max(1, provision.durationMs ?? 10 * 6e4);
1209
1350
  const progress = Math.round(Math.min(1, remaining / duration) * 100);
1210
- React4.useEffect(() => setImageFailed(false), [source]);
1351
+ React6.useEffect(() => setImageFailed(false), [source]);
1211
1352
  return h2(
1212
1353
  "div",
1213
1354
  { className: "ddt-card dim-surfaceCard" },
@@ -1324,8 +1465,8 @@ function checkedTime(value) {
1324
1465
  }
1325
1466
  }
1326
1467
  function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
1327
- const cancelRef = React4.useRef(null);
1328
- React4.useEffect(() => cancelRef.current?.focus(), []);
1468
+ const cancelRef = React6.useRef(null);
1469
+ React6.useEffect(() => cancelRef.current?.focus(), []);
1329
1470
  return h2(
1330
1471
  "div",
1331
1472
  {
@@ -1355,6 +1496,7 @@ function AccountCard({
1355
1496
  busy,
1356
1497
  removing,
1357
1498
  onReconnect,
1499
+ onWorkspaceSave,
1358
1500
  onRequestRemove,
1359
1501
  onConfirmRemove,
1360
1502
  onCancelRemove
@@ -1406,6 +1548,11 @@ function AccountCard({
1406
1548
  h2("dd", null, checkedTime(account.health.lastCheckedAt))
1407
1549
  )
1408
1550
  ),
1551
+ h2(WorkspaceEditor, {
1552
+ workspace: account.workspace,
1553
+ disabled: Boolean(busy),
1554
+ onSave: onWorkspaceSave
1555
+ }),
1409
1556
  h2(
1410
1557
  "div",
1411
1558
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -1451,6 +1598,7 @@ function AccountList(props) {
1451
1598
  busy: props.busyByBot[account.botId],
1452
1599
  removing: props.removeTarget === account.botId,
1453
1600
  onReconnect: () => props.onReconnect(account),
1601
+ onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
1454
1602
  onRequestRemove: () => props.onRequestRemove(account),
1455
1603
  onConfirmRemove: () => props.onConfirmRemove(account),
1456
1604
  onCancelRemove: props.onCancelRemove
@@ -1460,27 +1608,28 @@ function AccountList(props) {
1460
1608
  }
1461
1609
  var EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 });
1462
1610
  function DingtalkSettingsTab({ rpcCall }) {
1463
- const [model, setModel] = React4.useState({
1611
+ const [model, setModel] = React6.useState({
1464
1612
  phase: "loading",
1465
1613
  bots: [],
1466
1614
  totals: EMPTY_TOTALS,
1467
1615
  revision: 0,
1468
1616
  error: null
1469
1617
  });
1470
- const [provision, setProvision] = React4.useState(null);
1471
- const [busy, setBusy] = React4.useState(false);
1472
- const [busyByBot, setBusyByBot] = React4.useState({});
1473
- const [removeTarget, setRemoveTarget] = React4.useState(null);
1474
- const [credentialOpen, setCredentialOpen] = React4.useState(false);
1475
- const [credentialError, setCredentialError] = React4.useState(null);
1476
- const [notice, setNotice] = React4.useState("");
1477
- const [now, setNow] = React4.useState(() => Date.now());
1478
- const addButtonRef = React4.useRef(null);
1479
- const mountedRef = React4.useRef(true);
1480
- const statusRequestRef = React4.useRef(0);
1481
- const noticeFrameRef = React4.useRef(null);
1482
- const focusFrameRef = React4.useRef(null);
1483
- React4.useEffect(() => {
1618
+ const [provision, setProvision] = React6.useState(null);
1619
+ const [busy, setBusy] = React6.useState(false);
1620
+ const [busyByBot, setBusyByBot] = React6.useState({});
1621
+ const [removeTarget, setRemoveTarget] = React6.useState(null);
1622
+ const [credentialOpen, setCredentialOpen] = React6.useState(false);
1623
+ const [credentialError, setCredentialError] = React6.useState(null);
1624
+ const [notice, setNotice] = React6.useState("");
1625
+ const [now, setNow] = React6.useState(() => Date.now());
1626
+ const addButtonRef = React6.useRef(null);
1627
+ const mountedRef = React6.useRef(true);
1628
+ const statusRequestRef = React6.useRef(0);
1629
+ const workspaceFence = useWorkspaceSnapshotFence();
1630
+ const noticeFrameRef = React6.useRef(null);
1631
+ const focusFrameRef = React6.useRef(null);
1632
+ React6.useEffect(() => {
1484
1633
  mountedRef.current = true;
1485
1634
  return () => {
1486
1635
  mountedRef.current = false;
@@ -1495,8 +1644,8 @@ function DingtalkSettingsTab({ rpcCall }) {
1495
1644
  }
1496
1645
  };
1497
1646
  }, []);
1498
- React4.useEffect(() => installDingtalkStyles(), []);
1499
- const announce = React4.useCallback((message) => {
1647
+ React6.useEffect(() => installDingtalkStyles(), []);
1648
+ const announce = React6.useCallback((message) => {
1500
1649
  if (!mountedRef.current) return;
1501
1650
  if (noticeFrameRef.current !== null) {
1502
1651
  window.cancelAnimationFrame(noticeFrameRef.current);
@@ -1510,7 +1659,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1510
1659
  });
1511
1660
  }
1512
1661
  }, []);
1513
- const focusAddButton = React4.useCallback(() => {
1662
+ const focusAddButton = React6.useCallback(() => {
1514
1663
  if (!mountedRef.current) return;
1515
1664
  if (focusFrameRef.current !== null) window.cancelAnimationFrame(focusFrameRef.current);
1516
1665
  focusFrameRef.current = window.requestAnimationFrame(() => {
@@ -1518,19 +1667,21 @@ function DingtalkSettingsTab({ rpcCall }) {
1518
1667
  if (mountedRef.current) addButtonRef.current?.focus();
1519
1668
  });
1520
1669
  }, []);
1521
- const invoke = React4.useCallback(async (endpoint, payload = {}, signal) => {
1670
+ const invoke = React6.useCallback(async (endpoint, payload = {}, signal) => {
1522
1671
  if (typeof rpcCall !== "function") throw new TypeError("\u9489\u9489\u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
1523
1672
  return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
1524
1673
  }, [rpcCall]);
1525
- const loadStatus = React4.useCallback(async ({
1674
+ const loadStatus = React6.useCallback(async ({
1526
1675
  signal,
1527
1676
  silent = false,
1528
1677
  restoreProvisioning = false
1529
1678
  } = {}) => {
1530
1679
  if (!mountedRef.current || signal?.aborted) return void 0;
1680
+ const workspaceVersion = workspaceFence.beginStatus();
1681
+ if (workspaceVersion === null) return void 0;
1531
1682
  const requestId = statusRequestRef.current + 1;
1532
1683
  statusRequestRef.current = requestId;
1533
- const canCommit = () => mountedRef.current && !signal?.aborted && statusRequestRef.current === requestId;
1684
+ const canCommit = () => mountedRef.current && !signal?.aborted && statusRequestRef.current === requestId && workspaceFence.canCommitStatus(workspaceVersion);
1534
1685
  if (!silent && canCommit()) {
1535
1686
  setModel((current) => ({ ...current, phase: "loading", error: null }));
1536
1687
  }
@@ -1561,13 +1712,13 @@ function DingtalkSettingsTab({ rpcCall }) {
1561
1712
  }));
1562
1713
  return void 0;
1563
1714
  }
1564
- }, [invoke]);
1565
- React4.useEffect(() => {
1715
+ }, [invoke, workspaceFence]);
1716
+ React6.useEffect(() => {
1566
1717
  const controller = new AbortController();
1567
1718
  void loadStatus({ signal: controller.signal, restoreProvisioning: true });
1568
1719
  return () => controller.abort();
1569
1720
  }, [loadStatus]);
1570
- React4.useEffect(() => {
1721
+ React6.useEffect(() => {
1571
1722
  if (model.phase !== "ready") return void 0;
1572
1723
  const controller = new AbortController();
1573
1724
  let running = false;
@@ -1586,14 +1737,14 @@ function DingtalkSettingsTab({ rpcCall }) {
1586
1737
  window.clearInterval(timer);
1587
1738
  };
1588
1739
  }, [loadStatus, model.phase]);
1589
- React4.useEffect(() => {
1740
+ React6.useEffect(() => {
1590
1741
  if (!provision || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0;
1591
1742
  const timer = window.setInterval(() => {
1592
1743
  if (mountedRef.current) setNow(Date.now());
1593
1744
  }, 1e3);
1594
1745
  return () => window.clearInterval(timer);
1595
1746
  }, [provision?.attemptId, provision?.status]);
1596
- const startProvisioning = React4.useCallback(async ({ replace = false } = {}) => {
1747
+ const startProvisioning = React6.useCallback(async ({ replace = false } = {}) => {
1597
1748
  if (!mountedRef.current) return;
1598
1749
  setCredentialOpen(false);
1599
1750
  setCredentialError(null);
@@ -1631,8 +1782,9 @@ function DingtalkSettingsTab({ rpcCall }) {
1631
1782
  if (mountedRef.current) setBusy(false);
1632
1783
  }
1633
1784
  }, [announce, invoke, provision?.attemptId]);
1634
- const bindCredentials = React4.useCallback(async ({ identity, secret }) => {
1785
+ const bindCredentials = React6.useCallback(async ({ identity, secret }) => {
1635
1786
  if (!mountedRef.current) return;
1787
+ const snapshotVersion = workspaceFence.beginMutation();
1636
1788
  setBusy(true);
1637
1789
  setCredentialError(null);
1638
1790
  try {
@@ -1641,22 +1793,26 @@ function DingtalkSettingsTab({ rpcCall }) {
1641
1793
  { clientId: identity, clientSecret: secret }
1642
1794
  ));
1643
1795
  if (!mountedRef.current) return;
1644
- setModel({
1645
- phase: "ready",
1646
- bots: snapshot.bots,
1647
- totals: snapshot.totals,
1648
- revision: snapshot.revision,
1649
- error: null
1650
- });
1796
+ if (workspaceFence.canCommitMutation(snapshotVersion)) {
1797
+ setModel({
1798
+ phase: "ready",
1799
+ bots: snapshot.bots,
1800
+ totals: snapshot.totals,
1801
+ revision: snapshot.revision,
1802
+ error: null
1803
+ });
1804
+ }
1651
1805
  setCredentialOpen(false);
1652
1806
  announce("\u9489\u9489\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
1653
1807
  } catch (error) {
1654
1808
  if (mountedRef.current) setCredentialError(presentError(error));
1655
1809
  } finally {
1810
+ const shouldRefresh = workspaceFence.endMutation();
1811
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
1656
1812
  if (mountedRef.current) setBusy(false);
1657
1813
  }
1658
- }, [announce, invoke]);
1659
- const cancelProvisioning = React4.useCallback(async () => {
1814
+ }, [announce, invoke, loadStatus, workspaceFence]);
1815
+ const cancelProvisioning = React6.useCallback(async () => {
1660
1816
  if (!mountedRef.current) return;
1661
1817
  setBusy(true);
1662
1818
  try {
@@ -1674,7 +1830,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1674
1830
  if (mountedRef.current) setBusy(false);
1675
1831
  }
1676
1832
  }, [announce, focusAddButton, invoke, provision?.attemptId, provision?.status]);
1677
- React4.useEffect(() => {
1833
+ React6.useEffect(() => {
1678
1834
  const attemptId = provision?.attemptId;
1679
1835
  if (!attemptId || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0;
1680
1836
  const controller = new AbortController();
@@ -1733,7 +1889,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1733
1889
  timer = null;
1734
1890
  };
1735
1891
  }, [announce, invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
1736
- const setBotBusy = React4.useCallback((botId, operation) => {
1892
+ const setBotBusy = React6.useCallback((botId, operation) => {
1737
1893
  if (!mountedRef.current) return;
1738
1894
  setBusyByBot((current) => {
1739
1895
  const next = { ...current };
@@ -1742,14 +1898,22 @@ function DingtalkSettingsTab({ rpcCall }) {
1742
1898
  return next;
1743
1899
  });
1744
1900
  }, []);
1745
- const runBotAction = React4.useCallback(async ({ account, operation, endpoint, payload, success }) => {
1901
+ const runBotAction = React6.useCallback(async ({ account, operation, endpoint, payload, success }) => {
1746
1902
  if (!mountedRef.current) return void 0;
1903
+ const snapshotVersion = workspaceFence.beginMutation();
1747
1904
  setBotBusy(account.botId, operation);
1748
1905
  try {
1749
- await invoke(endpoint, payload);
1750
- if (!mountedRef.current) return void 0;
1751
- const snapshot = await loadStatus({ silent: true, restoreProvisioning: false });
1906
+ const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
1752
1907
  if (!mountedRef.current) return void 0;
1908
+ if (workspaceFence.canCommitMutation(snapshotVersion)) {
1909
+ setModel({
1910
+ phase: "ready",
1911
+ bots: snapshot.bots,
1912
+ totals: snapshot.totals,
1913
+ revision: snapshot.revision,
1914
+ error: null
1915
+ });
1916
+ }
1753
1917
  announce(typeof success === "function" ? success(snapshot) : success);
1754
1918
  return snapshot;
1755
1919
  } catch (error) {
@@ -1757,17 +1921,42 @@ function DingtalkSettingsTab({ rpcCall }) {
1757
1921
  announce(`\u64CD\u4F5C\u5931\u8D25\uFF1A${presentError(error).message}`);
1758
1922
  return void 0;
1759
1923
  } finally {
1924
+ const shouldRefresh = workspaceFence.endMutation();
1925
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true, restoreProvisioning: false });
1760
1926
  if (mountedRef.current) setBotBusy(account.botId, null);
1761
1927
  }
1762
- }, [announce, invoke, loadStatus, setBotBusy]);
1763
- const reconnect = React4.useCallback((account) => runBotAction({
1928
+ }, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
1929
+ const reconnect = React6.useCallback((account) => runBotAction({
1764
1930
  account,
1765
1931
  operation: "reconnect",
1766
1932
  endpoint: DINGTALK_ENDPOINTS.reconnectBot,
1767
1933
  payload: { botId: account.botId },
1768
1934
  success: (snapshot) => snapshot?.bots.find((bot) => bot.botId === account.botId)?.connected ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002" : "\u9489\u9489\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002"
1769
1935
  }), [runBotAction]);
1770
- const remove = React4.useCallback(async (account) => {
1936
+ const saveWorkspace = React6.useCallback(async (account, workspace) => {
1937
+ const workspaceVersion = workspaceFence.beginMutation();
1938
+ setBotBusy(account.botId, "workspace");
1939
+ try {
1940
+ const snapshot = normalizeSnapshot(await invoke(
1941
+ DINGTALK_ENDPOINTS.setWorkspace,
1942
+ { botId: account.botId, workspace }
1943
+ ));
1944
+ if (mountedRef.current && workspaceFence.canCommitMutation(workspaceVersion)) {
1945
+ setModel({
1946
+ phase: "ready",
1947
+ bots: snapshot.bots,
1948
+ totals: snapshot.totals,
1949
+ revision: snapshot.revision,
1950
+ error: null
1951
+ });
1952
+ }
1953
+ } finally {
1954
+ const shouldRefresh = workspaceFence.endMutation();
1955
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
1956
+ if (mountedRef.current) setBotBusy(account.botId, null);
1957
+ }
1958
+ }, [invoke, loadStatus, setBotBusy, workspaceFence]);
1959
+ const remove = React6.useCallback(async (account) => {
1771
1960
  const snapshot = await runBotAction({
1772
1961
  account,
1773
1962
  operation: "delete",
@@ -1849,7 +2038,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1849
2038
  h2(Button, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
1850
2039
  )
1851
2040
  ) : h2(
1852
- React4.Fragment,
2041
+ React6.Fragment,
1853
2042
  null,
1854
2043
  credentialView,
1855
2044
  provisionView,
@@ -1859,6 +2048,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1859
2048
  busyByBot,
1860
2049
  removeTarget,
1861
2050
  onReconnect: (account) => void reconnect(account),
2051
+ onWorkspaceSave: saveWorkspace,
1862
2052
  onRequestRemove: (account) => setRemoveTarget(account.botId),
1863
2053
  onConfirmRemove: (account) => void remove(account),
1864
2054
  onCancelRemove: () => setRemoveTarget(null)
@@ -1888,7 +2078,8 @@ var TOKEN_BOT_ENDPOINTS = Object.freeze({
1888
2078
  status: "connection.status",
1889
2079
  bindCredentials: "bot.bind-credentials",
1890
2080
  reconnectBot: "bot.reconnect",
1891
- deleteBot: "bot.delete"
2081
+ deleteBot: "bot.delete",
2082
+ setWorkspace: "bot.workspace.set"
1892
2083
  });
1893
2084
  function createTokenChannelApi(channel4, connectionSummary) {
1894
2085
  const unwrapRpcResult10 = (result) => {
@@ -1910,6 +2101,7 @@ function createTokenChannelApi(channel4, connectionSummary) {
1910
2101
  botId: id(value.botId),
1911
2102
  connected,
1912
2103
  state: connected ? "connected" : state,
2104
+ workspace: text(value.workspace, "", 4096),
1913
2105
  bot: {
1914
2106
  name: text(value.bot?.name, `${channel4}\u673A\u5668\u4EBA`, 100),
1915
2107
  username: text(value.bot?.username, "", 100),
@@ -1956,8 +2148,8 @@ var normalizeSnapshot2 = api.normalizeSnapshot;
1956
2148
  var presentError2 = api.presentError;
1957
2149
 
1958
2150
  // plugin-src/client/channels/shared/token-channel.js
1959
- var React5 = __toESM(require("react"), 1);
1960
- var Button3 = React5.forwardRef(function Button4({ children, kind = "secondary", className = "", ...props }, ref) {
2151
+ var React7 = __toESM(require("react"), 1);
2152
+ var Button3 = React7.forwardRef(function Button4({ children, kind = "secondary", className = "", ...props }, ref) {
1961
2153
  return h2("button", {
1962
2154
  ...props,
1963
2155
  ref,
@@ -2000,7 +2192,7 @@ function createTokenChannelSettings(definition) {
2000
2192
  credentialNoun = "Bot Token",
2001
2193
  emptyActionLabel = "\u586B\u5199 Bot Token"
2002
2194
  } = definition;
2003
- function AccountCard5({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
2195
+ function AccountCard5({ account, busy, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
2004
2196
  const state = busy === "reconnect" ? "connecting" : account.state;
2005
2197
  const tone = account.connected ? "success" : state === "error" ? "error" : "warning";
2006
2198
  const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
@@ -2053,6 +2245,11 @@ function createTokenChannelSettings(definition) {
2053
2245
  h2("dd", null, checkedTime2(account.health.lastCheckedAt))
2054
2246
  )
2055
2247
  ),
2248
+ h2(WorkspaceEditor, {
2249
+ workspace: account.workspace,
2250
+ disabled: Boolean(busy),
2251
+ onSave: onWorkspaceSave
2252
+ }),
2056
2253
  h2(
2057
2254
  "div",
2058
2255
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -2093,19 +2290,20 @@ function createTokenChannelSettings(definition) {
2093
2290
  );
2094
2291
  }
2095
2292
  function SettingsTab({ rpcCall }) {
2096
- const [model, setModel] = React5.useState({
2293
+ const [model, setModel] = React7.useState({
2097
2294
  phase: "loading",
2098
2295
  bots: [],
2099
2296
  totals: { configured: 0, connected: 0 },
2100
2297
  error: null
2101
2298
  });
2102
- const [credentialOpen, setCredentialOpen] = React5.useState(false);
2103
- const [credentialError, setCredentialError] = React5.useState(null);
2104
- const [busy, setBusy] = React5.useState(false);
2105
- const [busyByBot, setBusyByBot] = React5.useState({});
2106
- const [removeTarget, setRemoveTarget] = React5.useState(null);
2107
- const mounted = React5.useRef(true);
2108
- React5.useEffect(() => {
2299
+ const [credentialOpen, setCredentialOpen] = React7.useState(false);
2300
+ const [credentialError, setCredentialError] = React7.useState(null);
2301
+ const [busy, setBusy] = React7.useState(false);
2302
+ const [busyByBot, setBusyByBot] = React7.useState({});
2303
+ const [removeTarget, setRemoveTarget] = React7.useState(null);
2304
+ const mounted = React7.useRef(true);
2305
+ const workspaceFence = useWorkspaceSnapshotFence();
2306
+ React7.useEffect(() => {
2109
2307
  const disposeDingtalk = installDingtalkStyles();
2110
2308
  const disposeChannel = installStyles();
2111
2309
  mounted.current = true;
@@ -2115,18 +2313,20 @@ function createTokenChannelSettings(definition) {
2115
2313
  disposeDingtalk();
2116
2314
  };
2117
2315
  }, []);
2118
- const invoke = React5.useCallback(async (endpoint, payload = {}, signal) => {
2316
+ const invoke = React7.useCallback(async (endpoint, payload = {}, signal) => {
2119
2317
  if (typeof rpcCall !== "function") throw new TypeError(`${channel4} \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5`);
2120
2318
  return api4.unwrapRpcResult(await rpcCall(endpoint, payload, signal));
2121
2319
  }, [rpcCall]);
2122
- const loadStatus = React5.useCallback(async ({ signal, silent = false } = {}) => {
2320
+ const loadStatus = React7.useCallback(async ({ signal, silent = false } = {}) => {
2321
+ const workspaceVersion = workspaceFence.beginStatus();
2322
+ if (workspaceVersion === null) return;
2123
2323
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
2124
2324
  try {
2125
2325
  const snapshot = api4.normalizeSnapshot(await invoke(endpoints.status, {}, signal));
2126
- if (!mounted.current || signal?.aborted) return;
2326
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return;
2127
2327
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
2128
2328
  } catch (error) {
2129
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
2329
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
2130
2330
  setModel((current) => ({
2131
2331
  ...current,
2132
2332
  phase: silent ? current.phase : "error",
@@ -2134,13 +2334,13 @@ function createTokenChannelSettings(definition) {
2134
2334
  }));
2135
2335
  }
2136
2336
  }
2137
- }, [invoke]);
2138
- React5.useEffect(() => {
2337
+ }, [invoke, workspaceFence]);
2338
+ React7.useEffect(() => {
2139
2339
  const controller = new AbortController();
2140
2340
  void loadStatus({ signal: controller.signal });
2141
2341
  return () => controller.abort();
2142
2342
  }, [loadStatus]);
2143
- React5.useEffect(() => {
2343
+ React7.useEffect(() => {
2144
2344
  if (model.phase !== "ready") return void 0;
2145
2345
  const controller = new AbortController();
2146
2346
  const timer = window.setInterval(
@@ -2152,7 +2352,8 @@ function createTokenChannelSettings(definition) {
2152
2352
  window.clearInterval(timer);
2153
2353
  };
2154
2354
  }, [loadStatus, model.phase]);
2155
- const bindCredentials = React5.useCallback(async (values) => {
2355
+ const bindCredentials = React7.useCallback(async (values) => {
2356
+ const snapshotVersion = workspaceFence.beginMutation();
2156
2357
  setBusy(true);
2157
2358
  setCredentialError(null);
2158
2359
  try {
@@ -2161,29 +2362,36 @@ function createTokenChannelSettings(definition) {
2161
2362
  credentialPayload(values)
2162
2363
  ));
2163
2364
  if (!mounted.current) return;
2164
- setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
2365
+ if (workspaceFence.canCommitMutation(snapshotVersion)) {
2366
+ setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
2367
+ }
2165
2368
  setCredentialOpen(false);
2166
2369
  } catch (error) {
2167
2370
  if (mounted.current) setCredentialError(api4.presentError(error));
2168
2371
  } finally {
2372
+ const shouldRefresh = workspaceFence.endMutation();
2373
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
2169
2374
  if (mounted.current) setBusy(false);
2170
2375
  }
2171
- }, [invoke]);
2172
- const botAction = React5.useCallback(async (account, operation, endpoint, payload) => {
2376
+ }, [invoke, loadStatus, workspaceFence]);
2377
+ const botAction = React7.useCallback(async (account, operation, endpoint, payload) => {
2378
+ const snapshotVersion = workspaceFence.beginMutation();
2173
2379
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
2174
2380
  try {
2175
2381
  const snapshot = api4.normalizeSnapshot(await invoke(endpoint, payload));
2176
- if (mounted.current) {
2382
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
2177
2383
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
2178
2384
  }
2179
2385
  } finally {
2386
+ const shouldRefresh = workspaceFence.endMutation();
2387
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
2180
2388
  if (mounted.current) setBusyByBot((current) => {
2181
2389
  const next = { ...current };
2182
2390
  delete next[account.botId];
2183
2391
  return next;
2184
2392
  });
2185
2393
  }
2186
- }, [invoke]);
2394
+ }, [invoke, loadStatus, workspaceFence]);
2187
2395
  const botList = model.bots.length > 0 ? h2(
2188
2396
  "section",
2189
2397
  { className: "dim-listSection" },
@@ -2202,6 +2410,12 @@ function createTokenChannelSettings(definition) {
2202
2410
  endpoints.reconnectBot,
2203
2411
  { botId: account.botId }
2204
2412
  ),
2413
+ onWorkspaceSave: (workspace) => botAction(
2414
+ account,
2415
+ "workspace",
2416
+ endpoints.setWorkspace,
2417
+ { botId: account.botId, workspace }
2418
+ ),
2205
2419
  onRequestRemove: () => setRemoveTarget(account.botId),
2206
2420
  onCancelRemove: () => setRemoveTarget(null),
2207
2421
  onConfirmRemove: async () => {
@@ -2261,7 +2475,7 @@ function createTokenChannelSettings(definition) {
2261
2475
  h2(Button3, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
2262
2476
  )
2263
2477
  ) : h2(
2264
- React5.Fragment,
2478
+ React7.Fragment,
2265
2479
  null,
2266
2480
  credentialOpen ? CredentialPanel ? h2(CredentialPanel, {
2267
2481
  busy,
@@ -2362,7 +2576,7 @@ var DiscordSettingsTab = channel.SettingsTab;
2362
2576
  var DiscordAccountCard = channel.AccountCard;
2363
2577
 
2364
2578
  // plugin-src/client/channels/feishu/index.js
2365
- var React7 = __toESM(require("react"), 1);
2579
+ var React9 = __toESM(require("react"), 1);
2366
2580
 
2367
2581
  // plugin-src/client/channels/feishu/api.js
2368
2582
  var FEISHU_RPC_CHANNEL = "/feishu";
@@ -2375,6 +2589,7 @@ var FEISHU_ENDPOINTS = Object.freeze({
2375
2589
  reconnectBot: "bot.reconnect",
2376
2590
  disconnectBot: "bot.disconnect",
2377
2591
  deleteBot: "bot.delete",
2592
+ setWorkspace: "bot.workspace.set",
2378
2593
  // Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
2379
2594
  testConnection: "connection.test",
2380
2595
  disconnect: "connection.disconnect"
@@ -2491,6 +2706,7 @@ function normalizeBotConnection(value, fallbackBotId) {
2491
2706
  state: authoritativeState(value.state, connected),
2492
2707
  connected,
2493
2708
  configured: value.configured !== false,
2709
+ workspace: optionalString2(value.workspace)?.slice(0, 4096) ?? "",
2494
2710
  bot: normalizeBot2(value.bot),
2495
2711
  health: normalizeHealth(value.health, connected),
2496
2712
  error: normalizeError2(value.error)
@@ -2580,7 +2796,7 @@ function formatRemaining2(milliseconds) {
2580
2796
  }
2581
2797
 
2582
2798
  // plugin-src/client/lifecycle.js
2583
- var React6 = __toESM(require("react"), 1);
2799
+ var React8 = __toESM(require("react"), 1);
2584
2800
  function createPollScheduler({ setTimeoutFn, clearTimeoutFn }) {
2585
2801
  let disposed = false;
2586
2802
  let timer;
@@ -2642,8 +2858,8 @@ function createAnimationFrameScheduler({ requestFrame, cancelFrame }) {
2642
2858
  };
2643
2859
  }
2644
2860
  function useAnimationFrameScheduler() {
2645
- const schedulerRef = React6.useRef(null);
2646
- React6.useEffect(() => {
2861
+ const schedulerRef = React8.useRef(null);
2862
+ React8.useEffect(() => {
2647
2863
  const scheduler = createAnimationFrameScheduler({
2648
2864
  requestFrame: (callback) => window.requestAnimationFrame(callback),
2649
2865
  cancelFrame: (frame) => window.cancelAnimationFrame(frame)
@@ -2654,7 +2870,7 @@ function useAnimationFrameScheduler() {
2654
2870
  if (schedulerRef.current === scheduler) schedulerRef.current = null;
2655
2871
  };
2656
2872
  }, []);
2657
- return React6.useCallback(
2873
+ return React8.useCallback(
2658
2874
  (callback, key) => schedulerRef.current?.schedule(callback, key) ?? false,
2659
2875
  []
2660
2876
  );
@@ -3222,7 +3438,7 @@ function QrIcon({ size = 58 }) {
3222
3438
  fill: "currentColor"
3223
3439
  }));
3224
3440
  }
3225
- var Button5 = React7.forwardRef(function Button6({ children, kind = "secondary", size, icon, className = "", ...props }, ref) {
3441
+ var Button5 = React9.forwardRef(function Button6({ children, kind = "secondary", size, icon, className = "", ...props }, ref) {
3226
3442
  return h2("button", {
3227
3443
  ...props,
3228
3444
  ref,
@@ -3334,13 +3550,13 @@ function safeQrSource2(value) {
3334
3550
  return /^data:image\/(?:png|webp|svg\+xml)(?:;charset=[^;,]+)?;base64,/i.test(value) ? value : void 0;
3335
3551
  }
3336
3552
  function QrPane({ provision, now, onRefresh, onCancel, busy }) {
3337
- const [imageFailed, setImageFailed] = React7.useState(false);
3553
+ const [imageFailed, setImageFailed] = React9.useState(false);
3338
3554
  const qrSource = safeQrSource2(provision.qrCodeDataUrl);
3339
3555
  const href = safeVerificationHref(provision.verificationUrl);
3340
3556
  const remaining = Math.max(0, provision.expiresAt - now);
3341
3557
  const expired = provision.expired === true || remaining === 0;
3342
3558
  const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
3343
- React7.useEffect(() => setImageFailed(false), [qrSource]);
3559
+ React9.useEffect(() => setImageFailed(false), [qrSource]);
3344
3560
  return h2(
3345
3561
  "div",
3346
3562
  { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
@@ -3490,11 +3706,11 @@ function formatCheckedTime(timestamp7) {
3490
3706
  }
3491
3707
  }
3492
3708
  function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) {
3493
- const cancelRef = React7.useRef(null);
3709
+ const cancelRef = React9.useRef(null);
3494
3710
  const idPart = bot.botId.replace(/[^a-zA-Z0-9_-]/g, "-");
3495
3711
  const titleId = `bxf-remove-title-${idPart}`;
3496
3712
  const descriptionId = `bxf-remove-description-${idPart}`;
3497
- React7.useEffect(() => cancelRef.current?.focus(), []);
3713
+ React9.useEffect(() => cancelRef.current?.focus(), []);
3498
3714
  return h2(
3499
3715
  "div",
3500
3716
  {
@@ -3533,6 +3749,7 @@ function BotCard({
3533
3749
  actionError,
3534
3750
  removing,
3535
3751
  onReconnect,
3752
+ onWorkspaceSave,
3536
3753
  onRequestRemove,
3537
3754
  onConfirmRemove,
3538
3755
  onCancelRemove,
@@ -3597,6 +3814,11 @@ function BotCard({
3597
3814
  h2("dd", null, formatCheckedTime(health.lastCheckedAt))
3598
3815
  )
3599
3816
  ),
3817
+ h2(WorkspaceEditor, {
3818
+ workspace: connection.workspace,
3819
+ disabled: Boolean(busy),
3820
+ onSave: onWorkspaceSave
3821
+ }),
3600
3822
  h2(
3601
3823
  "div",
3602
3824
  { className: "bxf-connectedFooter dim-cardFooter" },
@@ -3655,6 +3877,7 @@ function BotList(props) {
3655
3877
  actionError: props.errorsByBot[bot.botId],
3656
3878
  removing: props.removeTargetId === bot.botId,
3657
3879
  onReconnect: () => props.onReconnect(bot),
3880
+ onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
3658
3881
  onRequestRemove: () => props.onRequestRemove(bot),
3659
3882
  onConfirmRemove: () => props.onConfirmRemove(bot),
3660
3883
  onCancelRemove: props.onCancelRemove,
@@ -3715,7 +3938,7 @@ function mergeFeishuSnapshotState(current, snapshot, { restoreProvisioning = fal
3715
3938
  };
3716
3939
  }
3717
3940
  function FeishuSettingsTab({ rpcCall }) {
3718
- const [model, setModel] = React7.useState({
3941
+ const [model, setModel] = React9.useState({
3719
3942
  phase: "loading",
3720
3943
  revision: 0,
3721
3944
  bots: [],
@@ -3724,31 +3947,39 @@ function FeishuSettingsTab({ rpcCall }) {
3724
3947
  pageError: null,
3725
3948
  statusError: null
3726
3949
  });
3727
- const [pageBusy, setPageBusy] = React7.useState(false);
3728
- const [provisionBusy, setProvisionBusy] = React7.useState(false);
3729
- const [credentialOpen, setCredentialOpen] = React7.useState(false);
3730
- const [credentialBusy, setCredentialBusy] = React7.useState(false);
3731
- const [credentialError, setCredentialError] = React7.useState(null);
3732
- const [busyByBot, setBusyByBot] = React7.useState({});
3733
- const [errorsByBot, setErrorsByBot] = React7.useState({});
3734
- const [removeTargetId, setRemoveTargetId] = React7.useState(null);
3735
- const [announcement, setAnnouncement] = React7.useState("");
3736
- const [now, setNow] = React7.useState(() => Date.now());
3737
- const [focusBotId, setFocusBotId] = React7.useState(null);
3738
- const cardRefs = React7.useRef(/* @__PURE__ */ new Map());
3739
- const removeButtonRefs = React7.useRef(/* @__PURE__ */ new Map());
3740
- const addButtonRef = React7.useRef(null);
3950
+ const [pageBusy, setPageBusy] = React9.useState(false);
3951
+ const [provisionBusy, setProvisionBusy] = React9.useState(false);
3952
+ const [credentialOpen, setCredentialOpen] = React9.useState(false);
3953
+ const [credentialBusy, setCredentialBusy] = React9.useState(false);
3954
+ const [credentialError, setCredentialError] = React9.useState(null);
3955
+ const [busyByBot, setBusyByBot] = React9.useState({});
3956
+ const [errorsByBot, setErrorsByBot] = React9.useState({});
3957
+ const [removeTargetId, setRemoveTargetId] = React9.useState(null);
3958
+ const [announcement, setAnnouncement] = React9.useState("");
3959
+ const [now, setNow] = React9.useState(() => Date.now());
3960
+ const [focusBotId, setFocusBotId] = React9.useState(null);
3961
+ const cardRefs = React9.useRef(/* @__PURE__ */ new Map());
3962
+ const removeButtonRefs = React9.useRef(/* @__PURE__ */ new Map());
3963
+ const addButtonRef = React9.useRef(null);
3964
+ const mountedRef = React9.useRef(true);
3965
+ const workspaceFence = useWorkspaceSnapshotFence();
3741
3966
  const scheduleAnimationFrame = useAnimationFrameScheduler();
3742
- const announce = React7.useCallback((message) => {
3967
+ React9.useEffect(() => {
3968
+ mountedRef.current = true;
3969
+ return () => {
3970
+ mountedRef.current = false;
3971
+ };
3972
+ }, []);
3973
+ const announce = React9.useCallback((message) => {
3743
3974
  setAnnouncement("");
3744
3975
  scheduleAnimationFrame(() => {
3745
3976
  if (message) setAnnouncement(message);
3746
3977
  }, "announcement");
3747
3978
  }, [scheduleAnimationFrame]);
3748
- const invoke = React7.useCallback(async (endpoint, payload = {}, signal) => {
3979
+ const invoke = React9.useCallback(async (endpoint, payload = {}, signal) => {
3749
3980
  return unwrapRpcResult3(await rpcCall(endpoint, payload, signal));
3750
3981
  }, [rpcCall]);
3751
- const mergeSnapshot = React7.useCallback((snapshot, { restoreProvisioning = false } = {}) => {
3982
+ const mergeSnapshot = React9.useCallback((snapshot, { restoreProvisioning = false } = {}) => {
3752
3983
  const now2 = Date.now();
3753
3984
  setModel((current) => mergeFeishuSnapshotState(
3754
3985
  current,
@@ -3756,28 +3987,30 @@ function FeishuSettingsTab({ rpcCall }) {
3756
3987
  { restoreProvisioning, now: now2 }
3757
3988
  ));
3758
3989
  }, []);
3759
- const loadStatus = React7.useCallback(async ({ signal, silent = false, restoreProvisioning = false } = {}) => {
3990
+ const loadStatus = React9.useCallback(async ({ signal, silent = false, restoreProvisioning = false } = {}) => {
3991
+ const workspaceVersion = workspaceFence.beginStatus();
3992
+ if (workspaceVersion === null || !mountedRef.current) return void 0;
3760
3993
  if (!silent) setPageBusy(true);
3761
3994
  try {
3762
3995
  const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.status, {}, signal));
3763
- if (signal?.aborted) return void 0;
3996
+ if (signal?.aborted || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
3764
3997
  mergeSnapshot(snapshot, { restoreProvisioning });
3765
3998
  return snapshot;
3766
3999
  } catch (error) {
3767
- if (signal?.aborted || error?.name === "AbortError") return void 0;
4000
+ if (signal?.aborted || error?.name === "AbortError" || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
3768
4001
  const presented = presentError3(error);
3769
4002
  setModel((current) => current.phase === "loading" || !silent ? { ...current, phase: "error", pageError: presented } : { ...current, statusError: presented });
3770
4003
  return void 0;
3771
4004
  } finally {
3772
- if (!silent && !signal?.aborted) setPageBusy(false);
4005
+ if (!silent && !signal?.aborted && mountedRef.current) setPageBusy(false);
3773
4006
  }
3774
- }, [invoke, mergeSnapshot]);
3775
- React7.useEffect(() => {
4007
+ }, [invoke, mergeSnapshot, workspaceFence]);
4008
+ React9.useEffect(() => {
3776
4009
  const controller = new AbortController();
3777
4010
  void loadStatus({ signal: controller.signal, restoreProvisioning: true });
3778
4011
  return () => controller.abort();
3779
4012
  }, [loadStatus]);
3780
- React7.useEffect(() => {
4013
+ React9.useEffect(() => {
3781
4014
  if (model.phase !== "ready") return void 0;
3782
4015
  const controller = new AbortController();
3783
4016
  let inFlight = false;
@@ -3796,7 +4029,7 @@ function FeishuSettingsTab({ rpcCall }) {
3796
4029
  window.clearInterval(timer);
3797
4030
  };
3798
4031
  }, [loadStatus, model.phase]);
3799
- React7.useEffect(() => {
4032
+ React9.useEffect(() => {
3800
4033
  if (!focusBotId) return;
3801
4034
  const node = cardRefs.current.get(focusBotId);
3802
4035
  if (!node) return;
@@ -3804,7 +4037,7 @@ function FeishuSettingsTab({ rpcCall }) {
3804
4037
  node.focus({ preventScroll: true });
3805
4038
  setFocusBotId(null);
3806
4039
  }, [focusBotId, model.bots]);
3807
- const startProvisioning = React7.useCallback(async ({ replace = false } = {}) => {
4040
+ const startProvisioning = React9.useCallback(async ({ replace = false } = {}) => {
3808
4041
  setCredentialOpen(false);
3809
4042
  setCredentialError(null);
3810
4043
  setProvisionBusy(true);
@@ -3844,7 +4077,8 @@ function FeishuSettingsTab({ rpcCall }) {
3844
4077
  setProvisionBusy(false);
3845
4078
  }
3846
4079
  }, [announce, invoke, model.provisioning?.attemptId]);
3847
- const bindCredentials = React7.useCallback(async ({ identity, secret }) => {
4080
+ const bindCredentials = React9.useCallback(async ({ identity, secret }) => {
4081
+ const snapshotVersion = workspaceFence.beginMutation();
3848
4082
  setCredentialBusy(true);
3849
4083
  setCredentialError(null);
3850
4084
  try {
@@ -3852,16 +4086,20 @@ function FeishuSettingsTab({ rpcCall }) {
3852
4086
  FEISHU_ENDPOINTS.bindCredentials,
3853
4087
  { appId: identity, appSecret: secret }
3854
4088
  ));
3855
- mergeSnapshot(snapshot);
4089
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4090
+ mergeSnapshot(snapshot);
4091
+ }
3856
4092
  setCredentialOpen(false);
3857
4093
  announce("\u98DE\u4E66\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
3858
4094
  } catch (error) {
3859
4095
  setCredentialError(presentError3(error));
3860
4096
  } finally {
4097
+ const shouldRefresh = workspaceFence.endMutation();
4098
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
3861
4099
  setCredentialBusy(false);
3862
4100
  }
3863
- }, [announce, invoke, mergeSnapshot]);
3864
- const cancelProvisioning = React7.useCallback(async () => {
4101
+ }, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]);
4102
+ const cancelProvisioning = React9.useCallback(async () => {
3865
4103
  const attemptId = model.provisioning?.attemptId;
3866
4104
  setProvisionBusy(true);
3867
4105
  try {
@@ -3883,7 +4121,7 @@ function FeishuSettingsTab({ rpcCall }) {
3883
4121
  const countdownPhase = model.provisioning?.phase;
3884
4122
  const countdownExpiresAt = model.provisioning?.expiresAt;
3885
4123
  const countdownExpired = model.provisioning?.expired;
3886
- React7.useEffect(() => {
4124
+ React9.useEffect(() => {
3887
4125
  if (!countdownAttemptId || countdownPhase !== "qr" || countdownExpired) return void 0;
3888
4126
  const tick = () => {
3889
4127
  const timestamp7 = Date.now();
@@ -3896,7 +4134,7 @@ function FeishuSettingsTab({ rpcCall }) {
3896
4134
  const timer = window.setInterval(tick, 1e3);
3897
4135
  return () => window.clearInterval(timer);
3898
4136
  }, [countdownAttemptId, countdownPhase, countdownExpiresAt, countdownExpired]);
3899
- React7.useEffect(() => {
4137
+ React9.useEffect(() => {
3900
4138
  const provision2 = model.provisioning;
3901
4139
  if (!provision2 || !["qr", "connecting"].includes(provision2.phase) || !provision2.attemptId || provision2.expired) return void 0;
3902
4140
  const controller = new AbortController();
@@ -3960,7 +4198,7 @@ function FeishuSettingsTab({ rpcCall }) {
3960
4198
  window.clearTimeout(timer);
3961
4199
  };
3962
4200
  }, [announce, invoke, loadStatus, model.provisioning]);
3963
- const setBotBusy = React7.useCallback((botId, value) => {
4201
+ const setBotBusy = React9.useCallback((botId, value) => {
3964
4202
  setBusyByBot((current) => {
3965
4203
  const next = { ...current };
3966
4204
  if (value) next[botId] = value;
@@ -3968,7 +4206,7 @@ function FeishuSettingsTab({ rpcCall }) {
3968
4206
  return next;
3969
4207
  });
3970
4208
  }, []);
3971
- const setBotError = React7.useCallback((botId, error) => {
4209
+ const setBotError = React9.useCallback((botId, error) => {
3972
4210
  setErrorsByBot((current) => {
3973
4211
  const next = { ...current };
3974
4212
  if (error) next[botId] = presentError3(error);
@@ -3976,13 +4214,16 @@ function FeishuSettingsTab({ rpcCall }) {
3976
4214
  return next;
3977
4215
  });
3978
4216
  }, []);
3979
- const reconnectOneBot = React7.useCallback(async (connection) => {
4217
+ const reconnectOneBot = React9.useCallback(async (connection) => {
3980
4218
  const { botId, bot } = connection;
4219
+ const snapshotVersion = workspaceFence.beginMutation();
3981
4220
  setBotBusy(botId, "reconnect");
3982
4221
  setBotError(botId, null);
3983
4222
  try {
3984
4223
  const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.reconnectBot, { botId }));
3985
- mergeSnapshot(snapshot);
4224
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4225
+ mergeSnapshot(snapshot);
4226
+ }
3986
4227
  const refreshed = snapshot.bots.find((item) => item.botId === botId);
3987
4228
  if (!refreshed?.connected) {
3988
4229
  const error = new Error(
@@ -3996,45 +4237,63 @@ function FeishuSettingsTab({ rpcCall }) {
3996
4237
  setBotError(botId, error);
3997
4238
  announce(`${bot.name}\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u673A\u5668\u4EBA\u72B6\u6001\u3002`);
3998
4239
  } finally {
4240
+ const shouldRefresh = workspaceFence.endMutation();
4241
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
3999
4242
  setBotBusy(botId, null);
4000
4243
  }
4001
- }, [announce, invoke, mergeSnapshot, setBotBusy, setBotError]);
4002
- const requestRemove = React7.useCallback((connection) => {
4244
+ }, [announce, invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
4245
+ const saveWorkspace = React9.useCallback(async (connection, workspace) => {
4246
+ const { botId } = connection;
4247
+ const workspaceVersion = workspaceFence.beginMutation();
4248
+ setBotBusy(botId, "workspace");
4249
+ setBotError(botId, null);
4250
+ try {
4251
+ const snapshot = normalizeBotsSnapshot(await invoke(
4252
+ FEISHU_ENDPOINTS.setWorkspace,
4253
+ { botId, workspace }
4254
+ ));
4255
+ if (mountedRef.current && workspaceFence.canCommitMutation(workspaceVersion)) {
4256
+ mergeSnapshot(snapshot);
4257
+ }
4258
+ } finally {
4259
+ const shouldRefresh = workspaceFence.endMutation();
4260
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
4261
+ if (mountedRef.current) setBotBusy(botId, null);
4262
+ }
4263
+ }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
4264
+ const requestRemove = React9.useCallback((connection) => {
4003
4265
  setRemoveTargetId(connection.botId);
4004
4266
  }, []);
4005
- const cancelRemove = React7.useCallback(() => {
4267
+ const cancelRemove = React9.useCallback(() => {
4006
4268
  const botId = removeTargetId;
4007
4269
  setRemoveTargetId(null);
4008
4270
  scheduleAnimationFrame(() => removeButtonRefs.current.get(botId)?.focus(), "focus");
4009
4271
  }, [removeTargetId, scheduleAnimationFrame]);
4010
- const confirmRemove = React7.useCallback(async (connection) => {
4272
+ const confirmRemove = React9.useCallback(async (connection) => {
4011
4273
  const { botId, bot } = connection;
4274
+ const snapshotVersion = workspaceFence.beginMutation();
4012
4275
  setBotBusy(botId, "delete");
4013
4276
  setBotError(botId, null);
4014
4277
  try {
4015
- await invoke(FEISHU_ENDPOINTS.deleteBot, { botId, confirm: true });
4278
+ const snapshot = normalizeBotsSnapshot(await invoke(
4279
+ FEISHU_ENDPOINTS.deleteBot,
4280
+ { botId, confirm: true }
4281
+ ));
4016
4282
  setRemoveTargetId(null);
4017
- setModel((current) => {
4018
- const bots = current.bots.filter((item) => item.botId !== botId);
4019
- return {
4020
- ...current,
4021
- bots,
4022
- totals: {
4023
- configured: bots.length,
4024
- connected: bots.filter((item) => item.connected).length
4025
- }
4026
- };
4027
- });
4283
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4284
+ mergeSnapshot(snapshot);
4285
+ }
4028
4286
  announce(`${bot.name}\u5DF2\u4ECE\u6B64 DeepSeek Harness \u79FB\u9664\uFF1B\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u4E2D\u7684\u5E94\u7528\u672A\u88AB\u5220\u9664\u3002`);
4029
- await loadStatus({ silent: true });
4030
4287
  scheduleAnimationFrame(() => addButtonRef.current?.focus(), "focus");
4031
4288
  } catch (error) {
4032
4289
  setBotError(botId, error);
4033
4290
  announce(`${bot.name}\u79FB\u9664\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002`);
4034
4291
  } finally {
4292
+ const shouldRefresh = workspaceFence.endMutation();
4293
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
4035
4294
  setBotBusy(botId, null);
4036
4295
  }
4037
- }, [announce, invoke, loadStatus, scheduleAnimationFrame, setBotBusy, setBotError]);
4296
+ }, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
4038
4297
  const provision = model.provisioning;
4039
4298
  let provisionContent = null;
4040
4299
  if (provision?.phase === "creating") {
@@ -4075,11 +4334,11 @@ function FeishuSettingsTab({ rpcCall }) {
4075
4334
  setCredentialError(null);
4076
4335
  }
4077
4336
  }) : null;
4078
- const setCardRef = React7.useCallback((botId, node) => {
4337
+ const setCardRef = React9.useCallback((botId, node) => {
4079
4338
  if (node) cardRefs.current.set(botId, node);
4080
4339
  else cardRefs.current.delete(botId);
4081
4340
  }, []);
4082
- const setRemoveButtonRef = React7.useCallback((botId, node) => {
4341
+ const setRemoveButtonRef = React9.useCallback((botId, node) => {
4083
4342
  if (node) removeButtonRefs.current.set(botId, node);
4084
4343
  else removeButtonRefs.current.delete(botId);
4085
4344
  }, []);
@@ -4116,7 +4375,7 @@ function FeishuSettingsTab({ rpcCall }) {
4116
4375
  onRetry: () => void loadStatus(),
4117
4376
  busy: pageBusy
4118
4377
  }) : h2(
4119
- React7.Fragment,
4378
+ React9.Fragment,
4120
4379
  null,
4121
4380
  credentialContent,
4122
4381
  provisionContent,
@@ -4127,6 +4386,7 @@ function FeishuSettingsTab({ rpcCall }) {
4127
4386
  errorsByBot,
4128
4387
  removeTargetId,
4129
4388
  onReconnect: (bot) => void reconnectOneBot(bot),
4389
+ onWorkspaceSave: saveWorkspace,
4130
4390
  onRequestRemove: requestRemove,
4131
4391
  onConfirmRemove: (bot) => void confirmRemove(bot),
4132
4392
  onCancelRemove: cancelRemove,
@@ -4146,7 +4406,8 @@ var QQ_ENDPOINTS = Object.freeze({
4146
4406
  cancelProvisioning: "provision.cancel",
4147
4407
  bindCredentials: "bot.bind-credentials",
4148
4408
  reconnectBot: "bot.reconnect",
4149
- deleteBot: "bot.delete"
4409
+ deleteBot: "bot.delete",
4410
+ setWorkspace: "bot.workspace.set"
4150
4411
  });
4151
4412
  var PROVISION_STATES2 = /* @__PURE__ */ new Set(["starting", "pending", "refreshing", "connecting", "connected", "failed", "cancelled"]);
4152
4413
  var ACCOUNT_STATES3 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
@@ -4208,6 +4469,7 @@ function normalizeBot3(value) {
4208
4469
  botId: id2(value.botId),
4209
4470
  connected,
4210
4471
  state: connected ? "connected" : state,
4472
+ workspace: text2(value.workspace, "", 4096),
4211
4473
  bot: {
4212
4474
  name: text2(value.bot?.name, "QQ\u673A\u5668\u4EBA", 100),
4213
4475
  appIdMasked: text2(value.bot?.appIdMasked, "\u5E94\u7528\u6807\u8BC6\u5DF2\u5B89\u5168\u4FDD\u5B58", 140)
@@ -4245,7 +4507,7 @@ function formatRemaining3(milliseconds) {
4245
4507
  }
4246
4508
 
4247
4509
  // plugin-src/client/channels/qq/index.js
4248
- var React8 = __toESM(require("react"), 1);
4510
+ var React10 = __toESM(require("react"), 1);
4249
4511
 
4250
4512
  // plugin-src/client/channels/qq/styles.js
4251
4513
  var QQ_STYLE_ID = "xmanrui-dsh-im-qq-settings";
@@ -4270,7 +4532,7 @@ function installQqStyles() {
4270
4532
 
4271
4533
  // plugin-src/client/channels/qq/index.js
4272
4534
  var ACTIVE_STATES = /* @__PURE__ */ new Set(["pending", "refreshing", "connecting"]);
4273
- var Button7 = React8.forwardRef(function Button8({ children, kind = "secondary", className = "", ...props }, ref) {
4535
+ var Button7 = React10.forwardRef(function Button8({ children, kind = "secondary", className = "", ...props }, ref) {
4274
4536
  return h2("button", {
4275
4537
  ...props,
4276
4538
  ref,
@@ -4477,7 +4739,16 @@ function RemoveConfirmation3({ account, busy, onConfirm, onCancel }) {
4477
4739
  )
4478
4740
  );
4479
4741
  }
4480
- function AccountCard2({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
4742
+ function AccountCard2({
4743
+ account,
4744
+ busy,
4745
+ removing,
4746
+ onReconnect,
4747
+ onWorkspaceSave,
4748
+ onRequestRemove,
4749
+ onConfirmRemove,
4750
+ onCancelRemove
4751
+ }) {
4481
4752
  const tone = account.connected ? "success" : account.state === "error" ? "error" : "warning";
4482
4753
  const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : account.state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
4483
4754
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
@@ -4514,6 +4785,11 @@ function AccountCard2({ account, busy, removing, onReconnect, onRequestRemove, o
4514
4785
  h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6D88\u606F\u901A\u9053"), h2("dd", null, account.connected ? "WebSocket \u957F\u8FDE\u63A5" : "\u79BB\u7EBF")),
4515
4786
  h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), h2("dd", null, checkedTime3(account.health.lastCheckedAt)))
4516
4787
  ),
4788
+ h2(WorkspaceEditor, {
4789
+ workspace: account.workspace,
4790
+ disabled: Boolean(busy),
4791
+ onSave: onWorkspaceSave
4792
+ }),
4517
4793
  h2(
4518
4794
  "div",
4519
4795
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -4535,17 +4811,18 @@ function AccountCard2({ account, busy, removing, onReconnect, onRequestRemove, o
4535
4811
  );
4536
4812
  }
4537
4813
  function QqSettingsTab({ rpcCall }) {
4538
- const [model, setModel] = React8.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null });
4539
- const [provision, setProvision] = React8.useState(null);
4540
- const [busy, setBusy] = React8.useState(false);
4541
- const [busyByBot, setBusyByBot] = React8.useState({});
4542
- const [removeTarget, setRemoveTarget] = React8.useState(null);
4543
- const [credentialOpen, setCredentialOpen] = React8.useState(false);
4544
- const [credentialError, setCredentialError] = React8.useState(null);
4545
- const [now, setNow] = React8.useState(Date.now());
4546
- const mounted = React8.useRef(true);
4547
- const addButtonRef = React8.useRef(null);
4548
- React8.useEffect(() => {
4814
+ const [model, setModel] = React10.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null });
4815
+ const [provision, setProvision] = React10.useState(null);
4816
+ const [busy, setBusy] = React10.useState(false);
4817
+ const [busyByBot, setBusyByBot] = React10.useState({});
4818
+ const [removeTarget, setRemoveTarget] = React10.useState(null);
4819
+ const [credentialOpen, setCredentialOpen] = React10.useState(false);
4820
+ const [credentialError, setCredentialError] = React10.useState(null);
4821
+ const [now, setNow] = React10.useState(Date.now());
4822
+ const mounted = React10.useRef(true);
4823
+ const workspaceFence = useWorkspaceSnapshotFence();
4824
+ const addButtonRef = React10.useRef(null);
4825
+ React10.useEffect(() => {
4549
4826
  const disposeDingtalk = installDingtalkStyles();
4550
4827
  const disposeQq = installQqStyles();
4551
4828
  mounted.current = true;
@@ -4555,15 +4832,17 @@ function QqSettingsTab({ rpcCall }) {
4555
4832
  disposeDingtalk();
4556
4833
  };
4557
4834
  }, []);
4558
- const invoke = React8.useCallback(async (endpoint, payload = {}, signal) => {
4835
+ const invoke = React10.useCallback(async (endpoint, payload = {}, signal) => {
4559
4836
  if (typeof rpcCall !== "function") throw new TypeError("QQ \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
4560
4837
  return unwrapRpcResult4(await rpcCall(endpoint, payload, signal));
4561
4838
  }, [rpcCall]);
4562
- const loadStatus = React8.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
4839
+ const loadStatus = React10.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
4840
+ const workspaceVersion = workspaceFence.beginStatus();
4841
+ if (workspaceVersion === null) return void 0;
4563
4842
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
4564
4843
  try {
4565
4844
  const snapshot = normalizeSnapshot3(await invoke(QQ_ENDPOINTS.status, {}, signal));
4566
- if (!mounted.current || signal?.aborted) return void 0;
4845
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
4567
4846
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
4568
4847
  if (restore && snapshot.provisioning) setProvision({
4569
4848
  ...snapshot.provisioning,
@@ -4571,18 +4850,18 @@ function QqSettingsTab({ rpcCall }) {
4571
4850
  });
4572
4851
  return snapshot;
4573
4852
  } catch (error) {
4574
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
4853
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
4575
4854
  setModel((current) => ({ ...current, phase: silent ? current.phase : "error", error: presentError4(error) }));
4576
4855
  }
4577
4856
  return void 0;
4578
4857
  }
4579
- }, [invoke]);
4580
- React8.useEffect(() => {
4858
+ }, [invoke, workspaceFence]);
4859
+ React10.useEffect(() => {
4581
4860
  const controller = new AbortController();
4582
4861
  void loadStatus({ signal: controller.signal, restore: true });
4583
4862
  return () => controller.abort();
4584
4863
  }, [loadStatus]);
4585
- React8.useEffect(() => {
4864
+ React10.useEffect(() => {
4586
4865
  if (model.phase !== "ready") return void 0;
4587
4866
  const controller = new AbortController();
4588
4867
  const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15e3);
@@ -4591,12 +4870,12 @@ function QqSettingsTab({ rpcCall }) {
4591
4870
  window.clearInterval(timer);
4592
4871
  };
4593
4872
  }, [loadStatus, model.phase]);
4594
- React8.useEffect(() => {
4873
+ React10.useEffect(() => {
4595
4874
  if (!provision || !ACTIVE_STATES.has(provision.status)) return void 0;
4596
4875
  const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
4597
4876
  return () => window.clearInterval(timer);
4598
4877
  }, [provision?.attemptId, provision?.status]);
4599
- const startProvisioning = React8.useCallback(async (replace = false) => {
4878
+ const startProvisioning = React10.useCallback(async (replace = false) => {
4600
4879
  setCredentialOpen(false);
4601
4880
  setCredentialError(null);
4602
4881
  setBusy(true);
@@ -4614,7 +4893,8 @@ function QqSettingsTab({ rpcCall }) {
4614
4893
  if (mounted.current) setBusy(false);
4615
4894
  }
4616
4895
  }, [invoke, provision?.attemptId]);
4617
- const bindCredentials = React8.useCallback(async ({ identity, secret }) => {
4896
+ const bindCredentials = React10.useCallback(async ({ identity, secret }) => {
4897
+ const snapshotVersion = workspaceFence.beginMutation();
4618
4898
  setBusy(true);
4619
4899
  setCredentialError(null);
4620
4900
  try {
@@ -4623,15 +4903,19 @@ function QqSettingsTab({ rpcCall }) {
4623
4903
  { appId: identity, appSecret: secret }
4624
4904
  ));
4625
4905
  if (!mounted.current) return;
4626
- setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
4906
+ if (workspaceFence.canCommitMutation(snapshotVersion)) {
4907
+ setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
4908
+ }
4627
4909
  setCredentialOpen(false);
4628
4910
  } catch (error) {
4629
4911
  if (mounted.current) setCredentialError(presentError4(error));
4630
4912
  } finally {
4913
+ const shouldRefresh = workspaceFence.endMutation();
4914
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
4631
4915
  if (mounted.current) setBusy(false);
4632
4916
  }
4633
- }, [invoke]);
4634
- const closeProvision = React8.useCallback(async () => {
4917
+ }, [invoke, loadStatus, workspaceFence]);
4918
+ const closeProvision = React10.useCallback(async () => {
4635
4919
  setBusy(true);
4636
4920
  try {
4637
4921
  if (provision?.attemptId && ACTIVE_STATES.has(provision.status)) {
@@ -4642,7 +4926,7 @@ function QqSettingsTab({ rpcCall }) {
4642
4926
  if (mounted.current) setBusy(false);
4643
4927
  }
4644
4928
  }, [invoke, provision?.attemptId, provision?.status]);
4645
- React8.useEffect(() => {
4929
+ React10.useEffect(() => {
4646
4930
  const attemptId = provision?.attemptId;
4647
4931
  if (!attemptId || !ACTIVE_STATES.has(provision.status)) return void 0;
4648
4932
  const controller = new AbortController();
@@ -4672,20 +4956,25 @@ function QqSettingsTab({ rpcCall }) {
4672
4956
  window.clearTimeout(timer);
4673
4957
  };
4674
4958
  }, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
4675
- const botAction = React8.useCallback(async (account, operation, endpoint, payload) => {
4959
+ const botAction = React10.useCallback(async (account, operation, endpoint, payload) => {
4960
+ const snapshotVersion = workspaceFence.beginMutation();
4676
4961
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
4677
4962
  try {
4678
4963
  const snapshot = normalizeSnapshot3(await invoke(endpoint, payload));
4679
- if (mounted.current) setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
4964
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4965
+ setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
4966
+ }
4680
4967
  return snapshot;
4681
4968
  } finally {
4969
+ const shouldRefresh = workspaceFence.endMutation();
4970
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
4682
4971
  if (mounted.current) setBusyByBot((current) => {
4683
4972
  const next = { ...current };
4684
4973
  delete next[account.botId];
4685
4974
  return next;
4686
4975
  });
4687
4976
  }
4688
- }, [invoke]);
4977
+ }, [invoke, loadStatus, workspaceFence]);
4689
4978
  let provisionView = null;
4690
4979
  if (provision?.status === "starting") provisionView = h2("div", { className: "ddt-card ddt-loading dim-surfaceCard" }, h2("div", { className: "ddt-spinner" }), "\u6B63\u5728\u7533\u8BF7 QQ \u4E8C\u7EF4\u7801\u2026");
4691
4980
  else if (["pending", "refreshing"].includes(provision?.status)) provisionView = h2(QrPanel2, {
@@ -4714,6 +5003,12 @@ function QqSettingsTab({ rpcCall }) {
4714
5003
  busy: busyByBot[account.botId],
4715
5004
  removing: removeTarget === account.botId,
4716
5005
  onReconnect: () => void botAction(account, "reconnect", QQ_ENDPOINTS.reconnectBot, { botId: account.botId }),
5006
+ onWorkspaceSave: (workspace) => botAction(
5007
+ account,
5008
+ "workspace",
5009
+ QQ_ENDPOINTS.setWorkspace,
5010
+ { botId: account.botId, workspace }
5011
+ ),
4717
5012
  onRequestRemove: () => setRemoveTarget(account.botId),
4718
5013
  onCancelRemove: () => setRemoveTarget(null),
4719
5014
  onConfirmRemove: async () => {
@@ -4752,7 +5047,7 @@ function QqSettingsTab({ rpcCall }) {
4752
5047
  addButtonRef
4753
5048
  }),
4754
5049
  model.phase === "loading" ? h2(LoadingView3) : model.phase === "error" ? h2("div", { className: "ddt-card dim-surfaceCard" }, h2("div", { className: "ddt-inlineError dim-inlineError" }, h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6 QQ \u673A\u5668\u4EBA\u72B6\u6001"), h2("p", null, model.error?.message), h2(Button7, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6"))) : h2(
4755
- React8.Fragment,
5050
+ React10.Fragment,
4756
5051
  null,
4757
5052
  credentialView,
4758
5053
  provisionView,
@@ -4771,7 +5066,7 @@ var normalizeSnapshot4 = api2.normalizeSnapshot;
4771
5066
  var presentError5 = api2.presentError;
4772
5067
 
4773
5068
  // plugin-src/client/channels/slack/index.js
4774
- var React9 = __toESM(require("react"), 1);
5069
+ var React11 = __toESM(require("react"), 1);
4775
5070
 
4776
5071
  // src/channels/slack/manifest.mjs
4777
5072
  var SLACK_APP_MANIFEST_YAML = `_metadata:
@@ -4843,10 +5138,10 @@ function installSlackStyles() {
4843
5138
 
4844
5139
  // plugin-src/client/channels/slack/index.js
4845
5140
  function SlackCredentialPanel({ busy, error, onSubmit, onCancel }) {
4846
- const [botToken, setBotToken] = React9.useState("");
4847
- const [appToken, setAppToken] = React9.useState("");
4848
- const [copied, setCopied] = React9.useState(false);
4849
- const headingId = React9.useId();
5141
+ const [botToken, setBotToken] = React11.useState("");
5142
+ const [appToken, setAppToken] = React11.useState("");
5143
+ const [copied, setCopied] = React11.useState(false);
5144
+ const headingId = React11.useId();
4850
5145
  const copyManifest = async () => {
4851
5146
  try {
4852
5147
  await navigator.clipboard.writeText(SLACK_APP_MANIFEST_YAML);
@@ -5039,7 +5334,8 @@ var WECOM_ENDPOINTS = Object.freeze({
5039
5334
  cancelProvisioning: "provision.cancel",
5040
5335
  bindCredentials: "bot.bind-credentials",
5041
5336
  reconnectBot: "bot.reconnect",
5042
- deleteBot: "bot.delete"
5337
+ deleteBot: "bot.delete",
5338
+ setWorkspace: "bot.workspace.set"
5043
5339
  });
5044
5340
  var PROVISION_STATES3 = /* @__PURE__ */ new Set(["starting", "pending", "refreshing", "connecting", "connected", "failed", "cancelled"]);
5045
5341
  var ACCOUNT_STATES4 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
@@ -5101,6 +5397,7 @@ function normalizeBot4(value) {
5101
5397
  botId: id3(value.botId),
5102
5398
  connected,
5103
5399
  state: connected ? "connected" : state,
5400
+ workspace: text3(value.workspace, "", 4096),
5104
5401
  bot: {
5105
5402
  name: text3(value.bot?.name, "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA", 100),
5106
5403
  appIdMasked: text3(value.bot?.appIdMasked, "\u5E94\u7528\u6807\u8BC6\u5DF2\u5B89\u5168\u4FDD\u5B58", 140)
@@ -5138,7 +5435,7 @@ function formatRemaining4(milliseconds) {
5138
5435
  }
5139
5436
 
5140
5437
  // plugin-src/client/channels/wecom/index.js
5141
- var React10 = __toESM(require("react"), 1);
5438
+ var React12 = __toESM(require("react"), 1);
5142
5439
 
5143
5440
  // plugin-src/client/channels/wecom/styles.js
5144
5441
  var WECOM_STYLE_ID = "xmanrui-dsh-im-wecom-settings";
@@ -5163,7 +5460,7 @@ function installWecomStyles() {
5163
5460
 
5164
5461
  // plugin-src/client/channels/wecom/index.js
5165
5462
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["pending", "refreshing", "connecting"]);
5166
- var Button9 = React10.forwardRef(function Button10({ children, kind = "secondary", className = "", ...props }, ref) {
5463
+ var Button9 = React12.forwardRef(function Button10({ children, kind = "secondary", className = "", ...props }, ref) {
5167
5464
  return h2("button", {
5168
5465
  ...props,
5169
5466
  ref,
@@ -5370,7 +5667,16 @@ function RemoveConfirmation4({ account, busy, onConfirm, onCancel }) {
5370
5667
  )
5371
5668
  );
5372
5669
  }
5373
- function AccountCard3({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
5670
+ function AccountCard3({
5671
+ account,
5672
+ busy,
5673
+ removing,
5674
+ onReconnect,
5675
+ onWorkspaceSave,
5676
+ onRequestRemove,
5677
+ onConfirmRemove,
5678
+ onCancelRemove
5679
+ }) {
5374
5680
  const tone = account.connected ? "success" : account.state === "error" ? "error" : "warning";
5375
5681
  const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : account.state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
5376
5682
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
@@ -5407,6 +5713,11 @@ function AccountCard3({ account, busy, removing, onReconnect, onRequestRemove, o
5407
5713
  h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6D88\u606F\u901A\u9053"), h2("dd", null, account.connected ? "WebSocket \u957F\u8FDE\u63A5" : "\u79BB\u7EBF")),
5408
5714
  h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), h2("dd", null, checkedTime4(account.health.lastCheckedAt)))
5409
5715
  ),
5716
+ h2(WorkspaceEditor, {
5717
+ workspace: account.workspace,
5718
+ disabled: Boolean(busy),
5719
+ onSave: onWorkspaceSave
5720
+ }),
5410
5721
  h2(
5411
5722
  "div",
5412
5723
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -5428,17 +5739,18 @@ function AccountCard3({ account, busy, removing, onReconnect, onRequestRemove, o
5428
5739
  );
5429
5740
  }
5430
5741
  function WecomSettingsTab({ rpcCall }) {
5431
- const [model, setModel] = React10.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null });
5432
- const [provision, setProvision] = React10.useState(null);
5433
- const [busy, setBusy] = React10.useState(false);
5434
- const [busyByBot, setBusyByBot] = React10.useState({});
5435
- const [removeTarget, setRemoveTarget] = React10.useState(null);
5436
- const [credentialOpen, setCredentialOpen] = React10.useState(false);
5437
- const [credentialError, setCredentialError] = React10.useState(null);
5438
- const [now, setNow] = React10.useState(Date.now());
5439
- const mounted = React10.useRef(true);
5440
- const addButtonRef = React10.useRef(null);
5441
- React10.useEffect(() => {
5742
+ const [model, setModel] = React12.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null });
5743
+ const [provision, setProvision] = React12.useState(null);
5744
+ const [busy, setBusy] = React12.useState(false);
5745
+ const [busyByBot, setBusyByBot] = React12.useState({});
5746
+ const [removeTarget, setRemoveTarget] = React12.useState(null);
5747
+ const [credentialOpen, setCredentialOpen] = React12.useState(false);
5748
+ const [credentialError, setCredentialError] = React12.useState(null);
5749
+ const [now, setNow] = React12.useState(Date.now());
5750
+ const mounted = React12.useRef(true);
5751
+ const workspaceFence = useWorkspaceSnapshotFence();
5752
+ const addButtonRef = React12.useRef(null);
5753
+ React12.useEffect(() => {
5442
5754
  const disposeDingtalk = installDingtalkStyles();
5443
5755
  const disposeWecom = installWecomStyles();
5444
5756
  mounted.current = true;
@@ -5448,15 +5760,17 @@ function WecomSettingsTab({ rpcCall }) {
5448
5760
  disposeDingtalk();
5449
5761
  };
5450
5762
  }, []);
5451
- const invoke = React10.useCallback(async (endpoint, payload = {}, signal) => {
5763
+ const invoke = React12.useCallback(async (endpoint, payload = {}, signal) => {
5452
5764
  if (typeof rpcCall !== "function") throw new TypeError("\u4F01\u4E1A\u5FAE\u4FE1\u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
5453
5765
  return unwrapRpcResult7(await rpcCall(endpoint, payload, signal));
5454
5766
  }, [rpcCall]);
5455
- const loadStatus = React10.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
5767
+ const loadStatus = React12.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
5768
+ const workspaceVersion = workspaceFence.beginStatus();
5769
+ if (workspaceVersion === null) return void 0;
5456
5770
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
5457
5771
  try {
5458
5772
  const snapshot = normalizeSnapshot6(await invoke(WECOM_ENDPOINTS.status, {}, signal));
5459
- if (!mounted.current || signal?.aborted) return void 0;
5773
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
5460
5774
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
5461
5775
  if (restore && snapshot.provisioning) setProvision({
5462
5776
  ...snapshot.provisioning,
@@ -5464,18 +5778,18 @@ function WecomSettingsTab({ rpcCall }) {
5464
5778
  });
5465
5779
  return snapshot;
5466
5780
  } catch (error) {
5467
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
5781
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
5468
5782
  setModel((current) => ({ ...current, phase: silent ? current.phase : "error", error: presentError7(error) }));
5469
5783
  }
5470
5784
  return void 0;
5471
5785
  }
5472
- }, [invoke]);
5473
- React10.useEffect(() => {
5786
+ }, [invoke, workspaceFence]);
5787
+ React12.useEffect(() => {
5474
5788
  const controller = new AbortController();
5475
5789
  void loadStatus({ signal: controller.signal, restore: true });
5476
5790
  return () => controller.abort();
5477
5791
  }, [loadStatus]);
5478
- React10.useEffect(() => {
5792
+ React12.useEffect(() => {
5479
5793
  if (model.phase !== "ready") return void 0;
5480
5794
  const controller = new AbortController();
5481
5795
  const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15e3);
@@ -5484,12 +5798,12 @@ function WecomSettingsTab({ rpcCall }) {
5484
5798
  window.clearInterval(timer);
5485
5799
  };
5486
5800
  }, [loadStatus, model.phase]);
5487
- React10.useEffect(() => {
5801
+ React12.useEffect(() => {
5488
5802
  if (!provision || !ACTIVE_STATES2.has(provision.status)) return void 0;
5489
5803
  const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
5490
5804
  return () => window.clearInterval(timer);
5491
5805
  }, [provision?.attemptId, provision?.status]);
5492
- const startProvisioning = React10.useCallback(async (replace = false) => {
5806
+ const startProvisioning = React12.useCallback(async (replace = false) => {
5493
5807
  setCredentialOpen(false);
5494
5808
  setCredentialError(null);
5495
5809
  setBusy(true);
@@ -5507,7 +5821,8 @@ function WecomSettingsTab({ rpcCall }) {
5507
5821
  if (mounted.current) setBusy(false);
5508
5822
  }
5509
5823
  }, [invoke, provision?.attemptId]);
5510
- const bindCredentials = React10.useCallback(async ({ identity, secret }) => {
5824
+ const bindCredentials = React12.useCallback(async ({ identity, secret }) => {
5825
+ const snapshotVersion = workspaceFence.beginMutation();
5511
5826
  setBusy(true);
5512
5827
  setCredentialError(null);
5513
5828
  try {
@@ -5516,15 +5831,19 @@ function WecomSettingsTab({ rpcCall }) {
5516
5831
  { botId: identity, secret }
5517
5832
  ));
5518
5833
  if (!mounted.current) return;
5519
- setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
5834
+ if (workspaceFence.canCommitMutation(snapshotVersion)) {
5835
+ setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
5836
+ }
5520
5837
  setCredentialOpen(false);
5521
5838
  } catch (error) {
5522
5839
  if (mounted.current) setCredentialError(presentError7(error));
5523
5840
  } finally {
5841
+ const shouldRefresh = workspaceFence.endMutation();
5842
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
5524
5843
  if (mounted.current) setBusy(false);
5525
5844
  }
5526
- }, [invoke]);
5527
- const closeProvision = React10.useCallback(async () => {
5845
+ }, [invoke, loadStatus, workspaceFence]);
5846
+ const closeProvision = React12.useCallback(async () => {
5528
5847
  setBusy(true);
5529
5848
  try {
5530
5849
  if (provision?.attemptId && ACTIVE_STATES2.has(provision.status)) {
@@ -5535,7 +5854,7 @@ function WecomSettingsTab({ rpcCall }) {
5535
5854
  if (mounted.current) setBusy(false);
5536
5855
  }
5537
5856
  }, [invoke, provision?.attemptId, provision?.status]);
5538
- React10.useEffect(() => {
5857
+ React12.useEffect(() => {
5539
5858
  const attemptId = provision?.attemptId;
5540
5859
  if (!attemptId || !ACTIVE_STATES2.has(provision.status)) return void 0;
5541
5860
  const controller = new AbortController();
@@ -5565,20 +5884,25 @@ function WecomSettingsTab({ rpcCall }) {
5565
5884
  window.clearTimeout(timer);
5566
5885
  };
5567
5886
  }, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
5568
- const botAction = React10.useCallback(async (account, operation, endpoint, payload) => {
5887
+ const botAction = React12.useCallback(async (account, operation, endpoint, payload) => {
5888
+ const snapshotVersion = workspaceFence.beginMutation();
5569
5889
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
5570
5890
  try {
5571
5891
  const snapshot = normalizeSnapshot6(await invoke(endpoint, payload));
5572
- if (mounted.current) setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
5892
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
5893
+ setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
5894
+ }
5573
5895
  return snapshot;
5574
5896
  } finally {
5897
+ const shouldRefresh = workspaceFence.endMutation();
5898
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
5575
5899
  if (mounted.current) setBusyByBot((current) => {
5576
5900
  const next = { ...current };
5577
5901
  delete next[account.botId];
5578
5902
  return next;
5579
5903
  });
5580
5904
  }
5581
- }, [invoke]);
5905
+ }, [invoke, loadStatus, workspaceFence]);
5582
5906
  let provisionView = null;
5583
5907
  if (provision?.status === "starting") provisionView = h2("div", { className: "ddt-card ddt-loading dim-surfaceCard" }, h2("div", { className: "ddt-spinner" }), "\u6B63\u5728\u7533\u8BF7\u4F01\u4E1A\u5FAE\u4FE1\u4E8C\u7EF4\u7801\u2026");
5584
5908
  else if (["pending", "refreshing"].includes(provision?.status)) provisionView = h2(QrPanel3, {
@@ -5607,6 +5931,12 @@ function WecomSettingsTab({ rpcCall }) {
5607
5931
  busy: busyByBot[account.botId],
5608
5932
  removing: removeTarget === account.botId,
5609
5933
  onReconnect: () => void botAction(account, "reconnect", WECOM_ENDPOINTS.reconnectBot, { botId: account.botId }),
5934
+ onWorkspaceSave: (workspace) => botAction(
5935
+ account,
5936
+ "workspace",
5937
+ WECOM_ENDPOINTS.setWorkspace,
5938
+ { botId: account.botId, workspace }
5939
+ ),
5610
5940
  onRequestRemove: () => setRemoveTarget(account.botId),
5611
5941
  onCancelRemove: () => setRemoveTarget(null),
5612
5942
  onConfirmRemove: async () => {
@@ -5645,7 +5975,7 @@ function WecomSettingsTab({ rpcCall }) {
5645
5975
  addButtonRef
5646
5976
  }),
5647
5977
  model.phase === "loading" ? h2(LoadingView4) : model.phase === "error" ? h2("div", { className: "ddt-card dim-surfaceCard" }, h2("div", { className: "ddt-inlineError dim-inlineError" }, h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u72B6\u6001"), h2("p", null, model.error?.message), h2(Button9, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6"))) : h2(
5648
- React10.Fragment,
5978
+ React12.Fragment,
5649
5979
  null,
5650
5980
  credentialView,
5651
5981
  provisionView,
@@ -5656,7 +5986,7 @@ function WecomSettingsTab({ rpcCall }) {
5656
5986
  }
5657
5987
 
5658
5988
  // plugin-src/client/channels/weixin/index.js
5659
- var React11 = __toESM(require("react"), 1);
5989
+ var React13 = __toESM(require("react"), 1);
5660
5990
 
5661
5991
  // plugin-src/client/channels/weixin/api.js
5662
5992
  var WEIXIN_RPC_CHANNEL = "/weixin";
@@ -5667,7 +5997,8 @@ var WEIXIN_ENDPOINTS = Object.freeze({
5667
5997
  submitVerification: "provision.verify",
5668
5998
  cancelProvisioning: "provision.cancel",
5669
5999
  reconnectBot: "bot.reconnect",
5670
- deleteBot: "bot.delete"
6000
+ deleteBot: "bot.delete",
6001
+ setWorkspace: "bot.workspace.set"
5671
6002
  });
5672
6003
  var ACCOUNT_STATES5 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
5673
6004
  var PROVISION_STATES4 = /* @__PURE__ */ new Set([
@@ -5749,6 +6080,7 @@ function normalizeBot5(value) {
5749
6080
  state: connected ? "connected" : state,
5750
6081
  connected,
5751
6082
  configured: value.configured === true,
6083
+ workspace: string(value.workspace).slice(0, 4096),
5752
6084
  bot: {
5753
6085
  name: string(value.bot.name, "\u5FAE\u4FE1\u673A\u5668\u4EBA"),
5754
6086
  accountIdMasked: string(value.bot.accountIdMasked, "\u5DF2\u5B89\u5168\u4FDD\u5B58")
@@ -5919,7 +6251,7 @@ function installWeixinStyles() {
5919
6251
  }
5920
6252
 
5921
6253
  // plugin-src/client/channels/weixin/index.js
5922
- var Button11 = React11.forwardRef(function Button12({ children, kind = "secondary", className = "", ...props }, ref) {
6254
+ var Button11 = React13.forwardRef(function Button12({ children, kind = "secondary", className = "", ...props }, ref) {
5923
6255
  return h2("button", {
5924
6256
  ...props,
5925
6257
  ref,
@@ -5992,14 +6324,14 @@ function EmptyView5({ onStart, busy }) {
5992
6324
  );
5993
6325
  }
5994
6326
  function QrPanel4({ provision, now, busy, onRefresh, onCancel }) {
5995
- const [imageFailed, setImageFailed] = React11.useState(false);
6327
+ const [imageFailed, setImageFailed] = React13.useState(false);
5996
6328
  const source = safeQrSource5(provision.qrCodeDataUrl);
5997
6329
  const href = safeVerificationUrl(provision.verificationUrl);
5998
6330
  const remaining = Math.max(0, provision.expiresAt - now);
5999
6331
  const expired = remaining === 0 || provision.status === "expired";
6000
6332
  const duration = Math.max(1, provision.durationMs ?? 5 * 6e4);
6001
6333
  const progress = Math.round(Math.min(1, remaining / duration) * 100);
6002
- React11.useEffect(() => setImageFailed(false), [source]);
6334
+ React13.useEffect(() => setImageFailed(false), [source]);
6003
6335
  return h2(
6004
6336
  "div",
6005
6337
  { className: "dxw-card dim-surfaceCard" },
@@ -6066,9 +6398,9 @@ function QrPanel4({ provision, now, busy, onRefresh, onCancel }) {
6066
6398
  );
6067
6399
  }
6068
6400
  function VerificationPanel({ provision, busy, onSubmit, onCancel }) {
6069
- const [code, setCode] = React11.useState("");
6401
+ const [code, setCode] = React13.useState("");
6070
6402
  const valid = /^\d{4,8}$/.test(code);
6071
- React11.useEffect(() => setCode(""), [provision.attemptId]);
6403
+ React13.useEffect(() => setCode(""), [provision.attemptId]);
6072
6404
  return h2(
6073
6405
  "div",
6074
6406
  { className: "dxw-card dim-surfaceCard" },
@@ -6159,7 +6491,16 @@ function checkedTime5(timestamp7) {
6159
6491
  return "\u521A\u521A";
6160
6492
  }
6161
6493
  }
6162
- function AccountCard4({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
6494
+ function AccountCard4({
6495
+ account,
6496
+ busy,
6497
+ removing,
6498
+ onReconnect,
6499
+ onWorkspaceSave,
6500
+ onRequestRemove,
6501
+ onConfirmRemove,
6502
+ onCancelRemove
6503
+ }) {
6163
6504
  const state = busy === "reconnect" ? "connecting" : account.state;
6164
6505
  const tone = account.connected ? "success" : state === "error" ? "error" : "warning";
6165
6506
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
@@ -6201,6 +6542,11 @@ function AccountCard4({ account, busy, removing, onReconnect, onRequestRemove, o
6201
6542
  h2("dd", null, checkedTime5(account.health.lastCheckedAt))
6202
6543
  )
6203
6544
  ),
6545
+ h2(WorkspaceEditor, {
6546
+ workspace: account.workspace,
6547
+ disabled: Boolean(busy),
6548
+ onSave: onWorkspaceSave
6549
+ }),
6204
6550
  h2(
6205
6551
  "div",
6206
6552
  { className: "dxw-accountFooter dim-cardFooter" },
@@ -6248,6 +6594,7 @@ function AccountList2(props) {
6248
6594
  busy: props.busyByBot[account.botId],
6249
6595
  removing: props.removeTarget === account.botId,
6250
6596
  onReconnect: () => props.onReconnect(account),
6597
+ onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
6251
6598
  onRequestRemove: () => props.onRequestRemove(account),
6252
6599
  onConfirmRemove: () => props.onConfirmRemove(account),
6253
6600
  onCancelRemove: props.onCancelRemove
@@ -6266,39 +6613,49 @@ function mergeWeixinProvisioningSnapshot(current, incoming, { restoreProvisionin
6266
6613
  };
6267
6614
  }
6268
6615
  function WeixinSettingsTab({ rpcCall }) {
6269
- const [model, setModel] = React11.useState({
6616
+ const [model, setModel] = React13.useState({
6270
6617
  phase: "loading",
6271
6618
  bots: [],
6272
6619
  totals: EMPTY_TOTALS3,
6273
6620
  revision: 0,
6274
6621
  error: null
6275
6622
  });
6276
- const [provision, setProvision] = React11.useState(null);
6277
- const [busy, setBusy] = React11.useState(false);
6278
- const [busyByBot, setBusyByBot] = React11.useState({});
6279
- const [removeTarget, setRemoveTarget] = React11.useState(null);
6280
- const [notice, setNotice] = React11.useState("");
6281
- const [now, setNow] = React11.useState(() => Date.now());
6282
- const addButtonRef = React11.useRef(null);
6623
+ const [provision, setProvision] = React13.useState(null);
6624
+ const [busy, setBusy] = React13.useState(false);
6625
+ const [busyByBot, setBusyByBot] = React13.useState({});
6626
+ const [removeTarget, setRemoveTarget] = React13.useState(null);
6627
+ const [notice, setNotice] = React13.useState("");
6628
+ const [now, setNow] = React13.useState(() => Date.now());
6629
+ const addButtonRef = React13.useRef(null);
6630
+ const mountedRef = React13.useRef(true);
6631
+ const workspaceFence = useWorkspaceSnapshotFence();
6283
6632
  const scheduleAnimationFrame = useAnimationFrameScheduler();
6284
- const announce = React11.useCallback((value) => {
6633
+ React13.useEffect(() => {
6634
+ mountedRef.current = true;
6635
+ return () => {
6636
+ mountedRef.current = false;
6637
+ };
6638
+ }, []);
6639
+ const announce = React13.useCallback((value) => {
6285
6640
  setNotice("");
6286
6641
  scheduleAnimationFrame(() => {
6287
6642
  if (value) setNotice(value);
6288
6643
  }, "announcement");
6289
6644
  }, [scheduleAnimationFrame]);
6290
- const invoke = React11.useCallback(async (endpoint, payload = {}, signal) => {
6645
+ const invoke = React13.useCallback(async (endpoint, payload = {}, signal) => {
6291
6646
  return unwrapRpcResult8(await rpcCall(endpoint, payload, signal));
6292
6647
  }, [rpcCall]);
6293
- const loadStatus = React11.useCallback(async ({
6648
+ const loadStatus = React13.useCallback(async ({
6294
6649
  signal,
6295
6650
  silent = false,
6296
6651
  restoreProvisioning = false
6297
6652
  } = {}) => {
6653
+ const workspaceVersion = workspaceFence.beginStatus();
6654
+ if (workspaceVersion === null || !mountedRef.current) return void 0;
6298
6655
  if (!silent) setModel((current) => ({ ...current, phase: "loading", error: null }));
6299
6656
  try {
6300
6657
  const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.status, {}, signal));
6301
- if (signal?.aborted) return void 0;
6658
+ if (signal?.aborted || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
6302
6659
  setModel({
6303
6660
  phase: "ready",
6304
6661
  bots: snapshot.bots,
@@ -6315,7 +6672,7 @@ function WeixinSettingsTab({ rpcCall }) {
6315
6672
  }
6316
6673
  return snapshot;
6317
6674
  } catch (error) {
6318
- if (signal?.aborted || error?.name === "AbortError") return void 0;
6675
+ if (signal?.aborted || error?.name === "AbortError" || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
6319
6676
  setModel((current) => ({
6320
6677
  ...current,
6321
6678
  phase: silent && current.phase === "ready" ? "ready" : "error",
@@ -6323,13 +6680,13 @@ function WeixinSettingsTab({ rpcCall }) {
6323
6680
  }));
6324
6681
  return void 0;
6325
6682
  }
6326
- }, [invoke]);
6327
- React11.useEffect(() => {
6683
+ }, [invoke, workspaceFence]);
6684
+ React13.useEffect(() => {
6328
6685
  const controller = new AbortController();
6329
6686
  void loadStatus({ signal: controller.signal, restoreProvisioning: true });
6330
6687
  return () => controller.abort();
6331
6688
  }, [loadStatus]);
6332
- React11.useEffect(() => {
6689
+ React13.useEffect(() => {
6333
6690
  if (model.phase !== "ready") return void 0;
6334
6691
  const controller = new AbortController();
6335
6692
  let running = false;
@@ -6348,12 +6705,12 @@ function WeixinSettingsTab({ rpcCall }) {
6348
6705
  window.clearInterval(timer);
6349
6706
  };
6350
6707
  }, [loadStatus, model.phase]);
6351
- React11.useEffect(() => {
6708
+ React13.useEffect(() => {
6352
6709
  if (!provision || !["pending", "scanned"].includes(provision.status)) return void 0;
6353
6710
  const timer = window.setInterval(() => setNow(Date.now()), 1e3);
6354
6711
  return () => window.clearInterval(timer);
6355
6712
  }, [provision?.attemptId, provision?.status]);
6356
- const startProvisioning = React11.useCallback(async ({ replace = false } = {}) => {
6713
+ const startProvisioning = React13.useCallback(async ({ replace = false } = {}) => {
6357
6714
  setBusy(true);
6358
6715
  try {
6359
6716
  if (replace && provision?.attemptId) {
@@ -6374,7 +6731,7 @@ function WeixinSettingsTab({ rpcCall }) {
6374
6731
  setBusy(false);
6375
6732
  }
6376
6733
  }, [announce, invoke, provision?.attemptId]);
6377
- const cancelProvisioning = React11.useCallback(async () => {
6734
+ const cancelProvisioning = React13.useCallback(async () => {
6378
6735
  setBusy(true);
6379
6736
  try {
6380
6737
  if (provision?.attemptId && !["failed", "expired", "cancelled"].includes(provision.status)) {
@@ -6389,7 +6746,7 @@ function WeixinSettingsTab({ rpcCall }) {
6389
6746
  setBusy(false);
6390
6747
  }
6391
6748
  }, [announce, invoke, provision?.attemptId, provision?.status, scheduleAnimationFrame]);
6392
- const submitVerification = React11.useCallback(async (verifyCode) => {
6749
+ const submitVerification = React13.useCallback(async (verifyCode) => {
6393
6750
  if (!provision?.attemptId) return;
6394
6751
  setBusy(true);
6395
6752
  try {
@@ -6405,7 +6762,7 @@ function WeixinSettingsTab({ rpcCall }) {
6405
6762
  setBusy(false);
6406
6763
  }
6407
6764
  }, [announce, invoke, provision?.attemptId]);
6408
- React11.useEffect(() => {
6765
+ React13.useEffect(() => {
6409
6766
  const attemptId = provision?.attemptId;
6410
6767
  if (!attemptId || !["pending", "scanned", "connecting"].includes(provision.status)) return void 0;
6411
6768
  const controller = new AbortController();
@@ -6453,7 +6810,7 @@ function WeixinSettingsTab({ rpcCall }) {
6453
6810
  controller.abort();
6454
6811
  };
6455
6812
  }, [announce, invoke, loadStatus, provision?.attemptId, provision?.status, provision?.pollIntervalMs]);
6456
- const setBotBusy = React11.useCallback((botId, value) => {
6813
+ const setBotBusy = React13.useCallback((botId, value) => {
6457
6814
  setBusyByBot((current) => {
6458
6815
  const next = { ...current };
6459
6816
  if (value) next[botId] = value;
@@ -6461,35 +6818,68 @@ function WeixinSettingsTab({ rpcCall }) {
6461
6818
  return next;
6462
6819
  });
6463
6820
  }, []);
6464
- const reconnect = React11.useCallback(async (account) => {
6821
+ const reconnect = React13.useCallback(async (account) => {
6822
+ const snapshotVersion = workspaceFence.beginMutation();
6465
6823
  setBotBusy(account.botId, "reconnect");
6466
6824
  try {
6467
6825
  const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId }));
6468
- setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
6826
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
6827
+ setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
6828
+ }
6469
6829
  const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
6470
6830
  announce(refreshed?.connected ? "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002" : "\u5FAE\u4FE1\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002");
6471
6831
  } catch (error) {
6472
6832
  announce(`\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF1A${presentError8(error).message}`);
6473
6833
  } finally {
6834
+ const shouldRefresh = workspaceFence.endMutation();
6835
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
6474
6836
  setBotBusy(account.botId, null);
6475
6837
  }
6476
- }, [announce, invoke, setBotBusy]);
6477
- const remove = React11.useCallback(async (account) => {
6838
+ }, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
6839
+ const saveWorkspace = React13.useCallback(async (account, workspace) => {
6840
+ const workspaceVersion = workspaceFence.beginMutation();
6841
+ setBotBusy(account.botId, "workspace");
6842
+ try {
6843
+ const snapshot = normalizeSnapshot7(await invoke(
6844
+ WEIXIN_ENDPOINTS.setWorkspace,
6845
+ { botId: account.botId, workspace }
6846
+ ));
6847
+ if (mountedRef.current && workspaceFence.canCommitMutation(workspaceVersion)) {
6848
+ setModel({
6849
+ phase: "ready",
6850
+ bots: snapshot.bots,
6851
+ totals: snapshot.totals,
6852
+ revision: snapshot.revision,
6853
+ error: null
6854
+ });
6855
+ }
6856
+ } finally {
6857
+ const shouldRefresh = workspaceFence.endMutation();
6858
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
6859
+ if (mountedRef.current) setBotBusy(account.botId, null);
6860
+ }
6861
+ }, [invoke, loadStatus, setBotBusy, workspaceFence]);
6862
+ const remove = React13.useCallback(async (account) => {
6863
+ const snapshotVersion = workspaceFence.beginMutation();
6478
6864
  setBotBusy(account.botId, "delete");
6479
6865
  try {
6480
6866
  const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.deleteBot, {
6481
6867
  botId: account.botId,
6482
6868
  confirm: true
6483
6869
  }));
6484
- setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
6870
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
6871
+ setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
6872
+ }
6485
6873
  setRemoveTarget(null);
6486
6874
  announce("\u5FAE\u4FE1\u8D26\u53F7\u53CA\u672C\u673A\u51ED\u636E\u5DF2\u79FB\u9664\u3002");
6487
6875
  } catch (error) {
6488
6876
  announce(`\u79FB\u9664\u5931\u8D25\uFF1A${presentError8(error).message}`);
6489
6877
  } finally {
6878
+ const shouldRefresh = workspaceFence.endMutation();
6879
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
6490
6880
  setBotBusy(account.botId, null);
6491
6881
  }
6492
- }, [announce, invoke, setBotBusy]);
6882
+ }, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
6493
6883
  let provisionView = null;
6494
6884
  if (provision?.status === "starting") {
6495
6885
  provisionView = h2(ProgressPanel2, { busy });
@@ -6545,7 +6935,7 @@ function WeixinSettingsTab({ rpcCall }) {
6545
6935
  h2(Button11, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
6546
6936
  )
6547
6937
  ) : h2(
6548
- React11.Fragment,
6938
+ React13.Fragment,
6549
6939
  null,
6550
6940
  provisionView,
6551
6941
  model.bots.length === 0 && !provision ? h2(EmptyView5, { onStart: () => void startProvisioning(), busy }) : null,
@@ -6554,6 +6944,7 @@ function WeixinSettingsTab({ rpcCall }) {
6554
6944
  busyByBot,
6555
6945
  removeTarget,
6556
6946
  onReconnect: (account) => void reconnect(account),
6947
+ onWorkspaceSave: saveWorkspace,
6557
6948
  onRequestRemove: (account) => setRemoveTarget(account.botId),
6558
6949
  onConfirmRemove: (account) => void remove(account),
6559
6950
  onCancelRemove: () => setRemoveTarget(null)
@@ -6570,7 +6961,8 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
6570
6961
  pollProvisioning: "provision.poll",
6571
6962
  cancelProvisioning: "provision.cancel",
6572
6963
  reconnectBot: "bot.reconnect",
6573
- deleteBot: "bot.delete"
6964
+ deleteBot: "bot.delete",
6965
+ setWorkspace: "bot.workspace.set"
6574
6966
  });
6575
6967
  var PROVISION_STATES5 = /* @__PURE__ */ new Set(["starting", "pending", "connecting", "connected", "failed", "cancelled"]);
6576
6968
  var BOT_STATES = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
@@ -6634,6 +7026,7 @@ function normalizeBot6(value) {
6634
7026
  botId: id4(value.botId),
6635
7027
  connected,
6636
7028
  state: connected ? "connected" : state,
7029
+ workspace: text4(value.workspace, "", 4096),
6637
7030
  bot: {
6638
7031
  name: text4(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
6639
7032
  idMasked: text4(value.bot?.idMasked, "WhatsApp\u8D26\u53F7", 140)
@@ -6673,7 +7066,7 @@ function formatRemaining6(milliseconds) {
6673
7066
  }
6674
7067
 
6675
7068
  // plugin-src/client/channels/whatsapp/index.js
6676
- var React12 = __toESM(require("react"), 1);
7069
+ var React14 = __toESM(require("react"), 1);
6677
7070
 
6678
7071
  // plugin-src/client/channels/whatsapp/styles.js
6679
7072
  var WHATSAPP_STYLE_ID = "xmanrui-dsh-im-whatsapp-settings";
@@ -6698,7 +7091,7 @@ function installWhatsappStyles() {
6698
7091
 
6699
7092
  // plugin-src/client/channels/whatsapp/index.js
6700
7093
  var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]);
6701
- var Button13 = React12.forwardRef(function Button14({ children, kind = "secondary", className = "", ...props }, ref) {
7094
+ var Button13 = React14.forwardRef(function Button14({ children, kind = "secondary", className = "", ...props }, ref) {
6702
7095
  return h2("button", {
6703
7096
  ...props,
6704
7097
  ref,
@@ -6909,6 +7302,7 @@ function WhatsappAccountCard({
6909
7302
  busy,
6910
7303
  removing,
6911
7304
  onReconnect,
7305
+ onWorkspaceSave,
6912
7306
  onRequestRemove,
6913
7307
  onConfirmRemove,
6914
7308
  onCancelRemove
@@ -6963,6 +7357,11 @@ function WhatsappAccountCard({
6963
7357
  h2("dd", null, checkedTime6(account.health.lastCheckedAt))
6964
7358
  )
6965
7359
  ),
7360
+ h2(WorkspaceEditor, {
7361
+ workspace: account.workspace,
7362
+ disabled: Boolean(busy),
7363
+ onSave: onWorkspaceSave
7364
+ }),
6966
7365
  h2(
6967
7366
  "div",
6968
7367
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -6993,20 +7392,21 @@ function WhatsappAccountCard({
6993
7392
  );
6994
7393
  }
6995
7394
  function WhatsappSettingsTab({ rpcCall }) {
6996
- const [model, setModel] = React12.useState({
7395
+ const [model, setModel] = React14.useState({
6997
7396
  phase: "loading",
6998
7397
  bots: [],
6999
7398
  totals: { configured: 0, connected: 0 },
7000
7399
  error: null
7001
7400
  });
7002
- const [provision, setProvision] = React12.useState(null);
7003
- const [busy, setBusy] = React12.useState(false);
7004
- const [busyByBot, setBusyByBot] = React12.useState({});
7005
- const [removeTarget, setRemoveTarget] = React12.useState(null);
7006
- const [now, setNow] = React12.useState(Date.now());
7007
- const mounted = React12.useRef(true);
7008
- const addButtonRef = React12.useRef(null);
7009
- React12.useEffect(() => {
7401
+ const [provision, setProvision] = React14.useState(null);
7402
+ const [busy, setBusy] = React14.useState(false);
7403
+ const [busyByBot, setBusyByBot] = React14.useState({});
7404
+ const [removeTarget, setRemoveTarget] = React14.useState(null);
7405
+ const [now, setNow] = React14.useState(Date.now());
7406
+ const mounted = React14.useRef(true);
7407
+ const workspaceFence = useWorkspaceSnapshotFence();
7408
+ const addButtonRef = React14.useRef(null);
7409
+ React14.useEffect(() => {
7010
7410
  const disposeDingtalk = installDingtalkStyles();
7011
7411
  const disposeWhatsapp = installWhatsappStyles();
7012
7412
  mounted.current = true;
@@ -7016,15 +7416,17 @@ function WhatsappSettingsTab({ rpcCall }) {
7016
7416
  disposeDingtalk();
7017
7417
  };
7018
7418
  }, []);
7019
- const invoke = React12.useCallback(async (endpoint, payload = {}, signal) => {
7419
+ const invoke = React14.useCallback(async (endpoint, payload = {}, signal) => {
7020
7420
  if (typeof rpcCall !== "function") throw new TypeError("WhatsApp \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
7021
7421
  return unwrapRpcResult9(await rpcCall(endpoint, payload, signal));
7022
7422
  }, [rpcCall]);
7023
- const loadStatus = React12.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
7423
+ const loadStatus = React14.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
7424
+ const workspaceVersion = workspaceFence.beginStatus();
7425
+ if (workspaceVersion === null) return void 0;
7024
7426
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
7025
7427
  try {
7026
7428
  const snapshot = normalizeSnapshot8(await invoke(WHATSAPP_ENDPOINTS.status, {}, signal));
7027
- if (!mounted.current || signal?.aborted) return void 0;
7429
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
7028
7430
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
7029
7431
  if (restore && snapshot.provisioning) setProvision({
7030
7432
  ...snapshot.provisioning,
@@ -7032,7 +7434,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7032
7434
  });
7033
7435
  return snapshot;
7034
7436
  } catch (error) {
7035
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
7437
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
7036
7438
  setModel((current) => ({
7037
7439
  ...current,
7038
7440
  phase: silent ? current.phase : "error",
@@ -7041,13 +7443,13 @@ function WhatsappSettingsTab({ rpcCall }) {
7041
7443
  }
7042
7444
  return void 0;
7043
7445
  }
7044
- }, [invoke]);
7045
- React12.useEffect(() => {
7446
+ }, [invoke, workspaceFence]);
7447
+ React14.useEffect(() => {
7046
7448
  const controller = new AbortController();
7047
7449
  void loadStatus({ signal: controller.signal, restore: true });
7048
7450
  return () => controller.abort();
7049
7451
  }, [loadStatus]);
7050
- React12.useEffect(() => {
7452
+ React14.useEffect(() => {
7051
7453
  if (model.phase !== "ready") return void 0;
7052
7454
  const controller = new AbortController();
7053
7455
  const timer = window.setInterval(
@@ -7059,12 +7461,12 @@ function WhatsappSettingsTab({ rpcCall }) {
7059
7461
  window.clearInterval(timer);
7060
7462
  };
7061
7463
  }, [loadStatus, model.phase]);
7062
- React12.useEffect(() => {
7464
+ React14.useEffect(() => {
7063
7465
  if (!provision || !ACTIVE_STATES3.has(provision.status)) return void 0;
7064
7466
  const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
7065
7467
  return () => window.clearInterval(timer);
7066
7468
  }, [provision?.attemptId, provision?.status]);
7067
- const startProvisioning = React12.useCallback(async (replace = false) => {
7469
+ const startProvisioning = React14.useCallback(async (replace = false) => {
7068
7470
  setBusy(true);
7069
7471
  try {
7070
7472
  if (replace && provision?.attemptId) {
@@ -7082,7 +7484,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7082
7484
  if (mounted.current) setBusy(false);
7083
7485
  }
7084
7486
  }, [invoke, provision?.attemptId]);
7085
- const closeProvision = React12.useCallback(async () => {
7487
+ const closeProvision = React14.useCallback(async () => {
7086
7488
  setBusy(true);
7087
7489
  try {
7088
7490
  if (provision?.attemptId && ACTIVE_STATES3.has(provision.status)) {
@@ -7093,7 +7495,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7093
7495
  if (mounted.current) setBusy(false);
7094
7496
  }
7095
7497
  }, [invoke, provision?.attemptId, provision?.status]);
7096
- React12.useEffect(() => {
7498
+ React14.useEffect(() => {
7097
7499
  const attemptId = provision?.attemptId;
7098
7500
  if (!attemptId || !ACTIVE_STATES3.has(provision.status)) return void 0;
7099
7501
  const controller = new AbortController();
@@ -7134,21 +7536,24 @@ function WhatsappSettingsTab({ rpcCall }) {
7134
7536
  if (timer) window.clearTimeout(timer);
7135
7537
  };
7136
7538
  }, [invoke, loadStatus, provision?.attemptId, provision?.status]);
7137
- const botAction = React12.useCallback(async (account, operation, endpoint, payload) => {
7539
+ const botAction = React14.useCallback(async (account, operation, endpoint, payload) => {
7540
+ const snapshotVersion = workspaceFence.beginMutation();
7138
7541
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
7139
7542
  try {
7140
7543
  const snapshot = normalizeSnapshot8(await invoke(endpoint, payload));
7141
- if (mounted.current) {
7544
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
7142
7545
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
7143
7546
  }
7144
7547
  } finally {
7548
+ const shouldRefresh = workspaceFence.endMutation();
7549
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
7145
7550
  if (mounted.current) setBusyByBot((current) => {
7146
7551
  const next = { ...current };
7147
7552
  delete next[account.botId];
7148
7553
  return next;
7149
7554
  });
7150
7555
  }
7151
- }, [invoke]);
7556
+ }, [invoke, loadStatus, workspaceFence]);
7152
7557
  const botList = model.bots.length > 0 ? h2(
7153
7558
  "section",
7154
7559
  { className: "dim-listSection" },
@@ -7167,6 +7572,12 @@ function WhatsappSettingsTab({ rpcCall }) {
7167
7572
  WHATSAPP_ENDPOINTS.reconnectBot,
7168
7573
  { botId: account.botId }
7169
7574
  ),
7575
+ onWorkspaceSave: (workspace) => botAction(
7576
+ account,
7577
+ "workspace",
7578
+ WHATSAPP_ENDPOINTS.setWorkspace,
7579
+ { botId: account.botId, workspace }
7580
+ ),
7170
7581
  onRequestRemove: () => setRemoveTarget(account.botId),
7171
7582
  onCancelRemove: () => setRemoveTarget(null),
7172
7583
  onConfirmRemove: async () => {
@@ -7201,7 +7612,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7201
7612
  h2(Button13, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
7202
7613
  )
7203
7614
  ) : h2(
7204
- React12.Fragment,
7615
+ React14.Fragment,
7205
7616
  null,
7206
7617
  provision?.status === "pending" ? h2(QrPanel5, {
7207
7618
  provision,
@@ -7355,6 +7766,19 @@ var CSS10 = String.raw`
7355
7766
  .dim-panel .dim-confirm strong, .dim-panel .dim-confirm h4 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 1.4; font-weight: 650; }
7356
7767
  .dim-panel .dim-confirm p { margin: 7px 0 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.6; }
7357
7768
  .dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
7769
+ .dim-panel .dim-workspace { min-width: 0; display: grid; gap: 7px; margin-top: 14px; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
7770
+ .dim-panel .dim-workspaceHeader { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
7771
+ .dim-panel .dim-workspaceEdit { padding: 0; border: 0; color: #1677ff; background: transparent; font: inherit; font-weight: 560; cursor: pointer; }
7772
+ .dim-panel .dim-workspaceEdit:disabled { cursor: not-allowed; opacity: .55; }
7773
+ .dim-panel .dim-workspacePath { min-width: 0; display: block; overflow: hidden; color: var(--dsw-alias-label-primary, #1f2329); font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
7774
+ .dim-panel .dim-workspaceForm { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 8px; }
7775
+ .dim-panel .dim-workspaceForm input { width: 100%; min-width: 0; height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }
7776
+ .dim-panel .dim-workspaceForm input:focus { border-color: #1677ff; box-shadow: 0 0 0 2px rgb(22 119 255 / 10%); }
7777
+ .dim-panel .dim-workspaceActions { display: flex; gap: 6px; }
7778
+ .dim-panel .dim-workspaceActions button { min-height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; }
7779
+ .dim-panel .dim-workspaceActions button[type="submit"] { border-color: #1677ff; color: white; background: #1677ff; }
7780
+ .dim-panel .dim-workspaceActions button:disabled { cursor: not-allowed; opacity: .55; }
7781
+ .dim-panel .dim-workspaceError { grid-column: 1 / -1; margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.5; }
7358
7782
  .dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
7359
7783
  .dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
7360
7784
  .dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
@@ -7526,8 +7950,8 @@ function IMSettingsTab({
7526
7950
  weixinRpcCall,
7527
7951
  whatsappRpcCall
7528
7952
  }) {
7529
- const [selected, setSelected] = React13.useState("weixin");
7530
- const githubTooltipId = React13.useId();
7953
+ const [selected, setSelected] = React15.useState("weixin");
7954
+ const githubTooltipId = React15.useId();
7531
7955
  const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
7532
7956
  return h2(
7533
7957
  "section",