@xmanrui/dsh-im 0.3.0 → 0.5.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 (68) hide show
  1. package/README.md +64 -0
  2. package/assets/logo-icon.png +0 -0
  3. package/assets/logo.png +0 -0
  4. package/lib/client.js +765 -295
  5. package/lib/index.js +112 -110
  6. package/package.json +2 -1
  7. package/plugin-src/client/build.mjs +6 -1
  8. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +67 -14
  10. package/plugin-src/client/channels/feishu/api.js +2 -0
  11. package/plugin-src/client/channels/feishu/index.js +70 -22
  12. package/plugin-src/client/channels/qq/api.js +2 -0
  13. package/plugin-src/client/channels/qq/index.js +45 -8
  14. package/plugin-src/client/channels/shared/token-api.js +2 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +34 -8
  16. package/plugin-src/client/channels/wecom/api.js +2 -0
  17. package/plugin-src/client/channels/wecom/index.js +45 -8
  18. package/plugin-src/client/channels/weixin/api.js +2 -0
  19. package/plugin-src/client/channels/weixin/index.js +68 -8
  20. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +27 -5
  22. package/plugin-src/client/i18n.js +15 -1
  23. package/plugin-src/client/index.js +28 -1
  24. package/plugin-src/client/styles.js +32 -9
  25. package/plugin-src/client/workspace-editor.js +96 -0
  26. package/plugin-src/client/workspace-snapshot-fence.js +28 -0
  27. package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
  28. package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
  29. package/plugin-src/host/channels/feishu/production.mjs +42 -5
  30. package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
  31. package/plugin-src/host/channels/qq/production.mjs +48 -22
  32. package/plugin-src/host/channels/qq/rpc.mjs +16 -2
  33. package/plugin-src/host/channels/shared/production.mjs +48 -22
  34. package/plugin-src/host/channels/shared/rpc.mjs +15 -0
  35. package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
  36. package/plugin-src/host/channels/slack/production.mjs +48 -23
  37. package/plugin-src/host/channels/slack/rpc.mjs +16 -0
  38. package/plugin-src/host/channels/wecom/production.mjs +49 -23
  39. package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
  40. package/plugin-src/host/channels/weixin/production.mjs +31 -11
  41. package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
  42. package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
  43. package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
  44. package/src/channels/dingtalk/dingtalk-bridge.mjs +22 -11
  45. package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
  46. package/src/channels/dingtalk/harness-client.mjs +4 -3
  47. package/src/channels/dingtalk/state-store.mjs +5 -0
  48. package/src/channels/discord/discord-api.mjs +1 -1
  49. package/src/channels/feishu/bridge.mjs +36 -16
  50. package/src/channels/feishu/harness-client.mjs +6 -5
  51. package/src/channels/feishu/state-store.mjs +5 -0
  52. package/src/channels/qq/qq-bridge.mjs +25 -15
  53. package/src/channels/qq/qq-controller.mjs +8 -1
  54. package/src/channels/qq/state-store.mjs +5 -0
  55. package/src/channels/shared/bot-workspace-store.mjs +584 -0
  56. package/src/channels/shared/conversation-state-store.mjs +5 -0
  57. package/src/channels/shared/text-harness-bridge.mjs +23 -13
  58. package/src/channels/shared/workspace-command.mjs +26 -0
  59. package/src/channels/shared/workspace-session.mjs +44 -0
  60. package/src/channels/wecom/state-store.mjs +5 -0
  61. package/src/channels/wecom/wecom-bridge.mjs +22 -13
  62. package/src/channels/wecom/wecom-controller.mjs +8 -1
  63. package/src/channels/weixin/harness-client.mjs +6 -5
  64. package/src/channels/weixin/state-store.mjs +5 -0
  65. package/src/channels/weixin/weixin-api.mjs +1 -1
  66. package/src/channels/weixin/weixin-bridge.mjs +16 -6
  67. package/src/channels/weixin/weixin-controller.mjs +6 -1
  68. 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);
@@ -366,7 +368,8 @@ var EN = Object.freeze({
366
368
  "IM\u673A\u5668\u4EBA": "IM bots",
367
369
  "IM\u673A\u5668\u4EBA\u8BBE\u7F6E": "IM bot settings",
368
370
  "IM \u6E20\u9053": "IM channels",
369
- "\u628A\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness": "Connect bots to DeepSeek Harness",
371
+ "\u8BA9\u804A\u5929\u673A\u5668\u4EBA\u8F7B\u677E\u63A5\u5165 DeepSeek Harness": "Connect IM bots to DeepSeek Harness with ease",
372
+ "\u5E2E\u52A9\u4E0E\u53CD\u9988 \xB7 \u524D\u5F80 GitHub": "Help & feedback \xB7 Open GitHub",
370
373
  "\u5FAE\u4FE1": "WeChat",
371
374
  "\u98DE\u4E66": "Feishu",
372
375
  "\u9489\u9489": "DingTalk",
@@ -402,6 +405,19 @@ var EN = Object.freeze({
402
405
  "\u5DF2\u65AD\u5F00": "Disconnected",
403
406
  "\u6D88\u606F\u901A\u9053": "Message channel",
404
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.",
405
421
  "\u5C1A\u672A\u68C0\u67E5": "Not checked yet",
406
422
  "\u521A\u521A": "Just now",
407
423
  "\u68C0\u67E5\u8FDE\u63A5": "Check connection",
@@ -946,6 +962,132 @@ function CredentialBindingPanel({
946
962
  );
947
963
  }
948
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
+
949
1091
  // plugin-src/client/channels/dingtalk/styles.js
950
1092
  var DINGTALK_STYLE_ID = "xmanrui-dsh-dingtalk-settings";
951
1093
  var CSS = String.raw`
@@ -1104,7 +1246,7 @@ function DingtalkIcon({ size = 28 }) {
1104
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"
1105
1247
  }));
1106
1248
  }
1107
- 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) {
1108
1250
  return h2("button", {
1109
1251
  ...props,
1110
1252
  ref,
@@ -1200,13 +1342,13 @@ function EmptyView({ busy, onStart }) {
1200
1342
  );
1201
1343
  }
1202
1344
  function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
1203
- const [imageFailed, setImageFailed] = React4.useState(false);
1345
+ const [imageFailed, setImageFailed] = React6.useState(false);
1204
1346
  const source = safeQrSource(provision.qrCodeDataUrl);
1205
1347
  const remaining = Math.max(0, provision.expiresAt - now);
1206
1348
  const expired = remaining === 0 || provision.status === "expired";
1207
1349
  const duration = Math.max(1, provision.durationMs ?? 10 * 6e4);
1208
1350
  const progress = Math.round(Math.min(1, remaining / duration) * 100);
1209
- React4.useEffect(() => setImageFailed(false), [source]);
1351
+ React6.useEffect(() => setImageFailed(false), [source]);
1210
1352
  return h2(
1211
1353
  "div",
1212
1354
  { className: "ddt-card dim-surfaceCard" },
@@ -1323,8 +1465,8 @@ function checkedTime(value) {
1323
1465
  }
1324
1466
  }
1325
1467
  function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
1326
- const cancelRef = React4.useRef(null);
1327
- React4.useEffect(() => cancelRef.current?.focus(), []);
1468
+ const cancelRef = React6.useRef(null);
1469
+ React6.useEffect(() => cancelRef.current?.focus(), []);
1328
1470
  return h2(
1329
1471
  "div",
1330
1472
  {
@@ -1354,6 +1496,7 @@ function AccountCard({
1354
1496
  busy,
1355
1497
  removing,
1356
1498
  onReconnect,
1499
+ onWorkspaceSave,
1357
1500
  onRequestRemove,
1358
1501
  onConfirmRemove,
1359
1502
  onCancelRemove
@@ -1405,6 +1548,11 @@ function AccountCard({
1405
1548
  h2("dd", null, checkedTime(account.health.lastCheckedAt))
1406
1549
  )
1407
1550
  ),
1551
+ h2(WorkspaceEditor, {
1552
+ workspace: account.workspace,
1553
+ disabled: Boolean(busy),
1554
+ onSave: onWorkspaceSave
1555
+ }),
1408
1556
  h2(
1409
1557
  "div",
1410
1558
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -1450,6 +1598,7 @@ function AccountList(props) {
1450
1598
  busy: props.busyByBot[account.botId],
1451
1599
  removing: props.removeTarget === account.botId,
1452
1600
  onReconnect: () => props.onReconnect(account),
1601
+ onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
1453
1602
  onRequestRemove: () => props.onRequestRemove(account),
1454
1603
  onConfirmRemove: () => props.onConfirmRemove(account),
1455
1604
  onCancelRemove: props.onCancelRemove
@@ -1459,27 +1608,28 @@ function AccountList(props) {
1459
1608
  }
1460
1609
  var EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 });
1461
1610
  function DingtalkSettingsTab({ rpcCall }) {
1462
- const [model, setModel] = React4.useState({
1611
+ const [model, setModel] = React6.useState({
1463
1612
  phase: "loading",
1464
1613
  bots: [],
1465
1614
  totals: EMPTY_TOTALS,
1466
1615
  revision: 0,
1467
1616
  error: null
1468
1617
  });
1469
- const [provision, setProvision] = React4.useState(null);
1470
- const [busy, setBusy] = React4.useState(false);
1471
- const [busyByBot, setBusyByBot] = React4.useState({});
1472
- const [removeTarget, setRemoveTarget] = React4.useState(null);
1473
- const [credentialOpen, setCredentialOpen] = React4.useState(false);
1474
- const [credentialError, setCredentialError] = React4.useState(null);
1475
- const [notice, setNotice] = React4.useState("");
1476
- const [now, setNow] = React4.useState(() => Date.now());
1477
- const addButtonRef = React4.useRef(null);
1478
- const mountedRef = React4.useRef(true);
1479
- const statusRequestRef = React4.useRef(0);
1480
- const noticeFrameRef = React4.useRef(null);
1481
- const focusFrameRef = React4.useRef(null);
1482
- 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(() => {
1483
1633
  mountedRef.current = true;
1484
1634
  return () => {
1485
1635
  mountedRef.current = false;
@@ -1494,8 +1644,8 @@ function DingtalkSettingsTab({ rpcCall }) {
1494
1644
  }
1495
1645
  };
1496
1646
  }, []);
1497
- React4.useEffect(() => installDingtalkStyles(), []);
1498
- const announce = React4.useCallback((message) => {
1647
+ React6.useEffect(() => installDingtalkStyles(), []);
1648
+ const announce = React6.useCallback((message) => {
1499
1649
  if (!mountedRef.current) return;
1500
1650
  if (noticeFrameRef.current !== null) {
1501
1651
  window.cancelAnimationFrame(noticeFrameRef.current);
@@ -1509,7 +1659,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1509
1659
  });
1510
1660
  }
1511
1661
  }, []);
1512
- const focusAddButton = React4.useCallback(() => {
1662
+ const focusAddButton = React6.useCallback(() => {
1513
1663
  if (!mountedRef.current) return;
1514
1664
  if (focusFrameRef.current !== null) window.cancelAnimationFrame(focusFrameRef.current);
1515
1665
  focusFrameRef.current = window.requestAnimationFrame(() => {
@@ -1517,19 +1667,21 @@ function DingtalkSettingsTab({ rpcCall }) {
1517
1667
  if (mountedRef.current) addButtonRef.current?.focus();
1518
1668
  });
1519
1669
  }, []);
1520
- const invoke = React4.useCallback(async (endpoint, payload = {}, signal) => {
1670
+ const invoke = React6.useCallback(async (endpoint, payload = {}, signal) => {
1521
1671
  if (typeof rpcCall !== "function") throw new TypeError("\u9489\u9489\u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
1522
1672
  return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
1523
1673
  }, [rpcCall]);
1524
- const loadStatus = React4.useCallback(async ({
1674
+ const loadStatus = React6.useCallback(async ({
1525
1675
  signal,
1526
1676
  silent = false,
1527
1677
  restoreProvisioning = false
1528
1678
  } = {}) => {
1529
1679
  if (!mountedRef.current || signal?.aborted) return void 0;
1680
+ const workspaceVersion = workspaceFence.beginStatus();
1681
+ if (workspaceVersion === null) return void 0;
1530
1682
  const requestId = statusRequestRef.current + 1;
1531
1683
  statusRequestRef.current = requestId;
1532
- const canCommit = () => mountedRef.current && !signal?.aborted && statusRequestRef.current === requestId;
1684
+ const canCommit = () => mountedRef.current && !signal?.aborted && statusRequestRef.current === requestId && workspaceFence.canCommitStatus(workspaceVersion);
1533
1685
  if (!silent && canCommit()) {
1534
1686
  setModel((current) => ({ ...current, phase: "loading", error: null }));
1535
1687
  }
@@ -1560,13 +1712,13 @@ function DingtalkSettingsTab({ rpcCall }) {
1560
1712
  }));
1561
1713
  return void 0;
1562
1714
  }
1563
- }, [invoke]);
1564
- React4.useEffect(() => {
1715
+ }, [invoke, workspaceFence]);
1716
+ React6.useEffect(() => {
1565
1717
  const controller = new AbortController();
1566
1718
  void loadStatus({ signal: controller.signal, restoreProvisioning: true });
1567
1719
  return () => controller.abort();
1568
1720
  }, [loadStatus]);
1569
- React4.useEffect(() => {
1721
+ React6.useEffect(() => {
1570
1722
  if (model.phase !== "ready") return void 0;
1571
1723
  const controller = new AbortController();
1572
1724
  let running = false;
@@ -1585,14 +1737,14 @@ function DingtalkSettingsTab({ rpcCall }) {
1585
1737
  window.clearInterval(timer);
1586
1738
  };
1587
1739
  }, [loadStatus, model.phase]);
1588
- React4.useEffect(() => {
1740
+ React6.useEffect(() => {
1589
1741
  if (!provision || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0;
1590
1742
  const timer = window.setInterval(() => {
1591
1743
  if (mountedRef.current) setNow(Date.now());
1592
1744
  }, 1e3);
1593
1745
  return () => window.clearInterval(timer);
1594
1746
  }, [provision?.attemptId, provision?.status]);
1595
- const startProvisioning = React4.useCallback(async ({ replace = false } = {}) => {
1747
+ const startProvisioning = React6.useCallback(async ({ replace = false } = {}) => {
1596
1748
  if (!mountedRef.current) return;
1597
1749
  setCredentialOpen(false);
1598
1750
  setCredentialError(null);
@@ -1630,8 +1782,9 @@ function DingtalkSettingsTab({ rpcCall }) {
1630
1782
  if (mountedRef.current) setBusy(false);
1631
1783
  }
1632
1784
  }, [announce, invoke, provision?.attemptId]);
1633
- const bindCredentials = React4.useCallback(async ({ identity, secret }) => {
1785
+ const bindCredentials = React6.useCallback(async ({ identity, secret }) => {
1634
1786
  if (!mountedRef.current) return;
1787
+ const snapshotVersion = workspaceFence.beginMutation();
1635
1788
  setBusy(true);
1636
1789
  setCredentialError(null);
1637
1790
  try {
@@ -1640,22 +1793,26 @@ function DingtalkSettingsTab({ rpcCall }) {
1640
1793
  { clientId: identity, clientSecret: secret }
1641
1794
  ));
1642
1795
  if (!mountedRef.current) return;
1643
- setModel({
1644
- phase: "ready",
1645
- bots: snapshot.bots,
1646
- totals: snapshot.totals,
1647
- revision: snapshot.revision,
1648
- error: null
1649
- });
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
+ }
1650
1805
  setCredentialOpen(false);
1651
1806
  announce("\u9489\u9489\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
1652
1807
  } catch (error) {
1653
1808
  if (mountedRef.current) setCredentialError(presentError(error));
1654
1809
  } finally {
1810
+ const shouldRefresh = workspaceFence.endMutation();
1811
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
1655
1812
  if (mountedRef.current) setBusy(false);
1656
1813
  }
1657
- }, [announce, invoke]);
1658
- const cancelProvisioning = React4.useCallback(async () => {
1814
+ }, [announce, invoke, loadStatus, workspaceFence]);
1815
+ const cancelProvisioning = React6.useCallback(async () => {
1659
1816
  if (!mountedRef.current) return;
1660
1817
  setBusy(true);
1661
1818
  try {
@@ -1673,7 +1830,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1673
1830
  if (mountedRef.current) setBusy(false);
1674
1831
  }
1675
1832
  }, [announce, focusAddButton, invoke, provision?.attemptId, provision?.status]);
1676
- React4.useEffect(() => {
1833
+ React6.useEffect(() => {
1677
1834
  const attemptId = provision?.attemptId;
1678
1835
  if (!attemptId || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0;
1679
1836
  const controller = new AbortController();
@@ -1732,7 +1889,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1732
1889
  timer = null;
1733
1890
  };
1734
1891
  }, [announce, invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
1735
- const setBotBusy = React4.useCallback((botId, operation) => {
1892
+ const setBotBusy = React6.useCallback((botId, operation) => {
1736
1893
  if (!mountedRef.current) return;
1737
1894
  setBusyByBot((current) => {
1738
1895
  const next = { ...current };
@@ -1741,14 +1898,22 @@ function DingtalkSettingsTab({ rpcCall }) {
1741
1898
  return next;
1742
1899
  });
1743
1900
  }, []);
1744
- const runBotAction = React4.useCallback(async ({ account, operation, endpoint, payload, success }) => {
1901
+ const runBotAction = React6.useCallback(async ({ account, operation, endpoint, payload, success }) => {
1745
1902
  if (!mountedRef.current) return void 0;
1903
+ const snapshotVersion = workspaceFence.beginMutation();
1746
1904
  setBotBusy(account.botId, operation);
1747
1905
  try {
1748
- await invoke(endpoint, payload);
1749
- if (!mountedRef.current) return void 0;
1750
- const snapshot = await loadStatus({ silent: true, restoreProvisioning: false });
1906
+ const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
1751
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
+ }
1752
1917
  announce(typeof success === "function" ? success(snapshot) : success);
1753
1918
  return snapshot;
1754
1919
  } catch (error) {
@@ -1756,17 +1921,42 @@ function DingtalkSettingsTab({ rpcCall }) {
1756
1921
  announce(`\u64CD\u4F5C\u5931\u8D25\uFF1A${presentError(error).message}`);
1757
1922
  return void 0;
1758
1923
  } finally {
1924
+ const shouldRefresh = workspaceFence.endMutation();
1925
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true, restoreProvisioning: false });
1759
1926
  if (mountedRef.current) setBotBusy(account.botId, null);
1760
1927
  }
1761
- }, [announce, invoke, loadStatus, setBotBusy]);
1762
- const reconnect = React4.useCallback((account) => runBotAction({
1928
+ }, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
1929
+ const reconnect = React6.useCallback((account) => runBotAction({
1763
1930
  account,
1764
1931
  operation: "reconnect",
1765
1932
  endpoint: DINGTALK_ENDPOINTS.reconnectBot,
1766
1933
  payload: { botId: account.botId },
1767
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"
1768
1935
  }), [runBotAction]);
1769
- 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) => {
1770
1960
  const snapshot = await runBotAction({
1771
1961
  account,
1772
1962
  operation: "delete",
@@ -1848,7 +2038,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1848
2038
  h2(Button, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
1849
2039
  )
1850
2040
  ) : h2(
1851
- React4.Fragment,
2041
+ React6.Fragment,
1852
2042
  null,
1853
2043
  credentialView,
1854
2044
  provisionView,
@@ -1858,6 +2048,7 @@ function DingtalkSettingsTab({ rpcCall }) {
1858
2048
  busyByBot,
1859
2049
  removeTarget,
1860
2050
  onReconnect: (account) => void reconnect(account),
2051
+ onWorkspaceSave: saveWorkspace,
1861
2052
  onRequestRemove: (account) => setRemoveTarget(account.botId),
1862
2053
  onConfirmRemove: (account) => void remove(account),
1863
2054
  onCancelRemove: () => setRemoveTarget(null)
@@ -1887,7 +2078,8 @@ var TOKEN_BOT_ENDPOINTS = Object.freeze({
1887
2078
  status: "connection.status",
1888
2079
  bindCredentials: "bot.bind-credentials",
1889
2080
  reconnectBot: "bot.reconnect",
1890
- deleteBot: "bot.delete"
2081
+ deleteBot: "bot.delete",
2082
+ setWorkspace: "bot.workspace.set"
1891
2083
  });
1892
2084
  function createTokenChannelApi(channel4, connectionSummary) {
1893
2085
  const unwrapRpcResult10 = (result) => {
@@ -1909,6 +2101,7 @@ function createTokenChannelApi(channel4, connectionSummary) {
1909
2101
  botId: id(value.botId),
1910
2102
  connected,
1911
2103
  state: connected ? "connected" : state,
2104
+ workspace: text(value.workspace, "", 4096),
1912
2105
  bot: {
1913
2106
  name: text(value.bot?.name, `${channel4}\u673A\u5668\u4EBA`, 100),
1914
2107
  username: text(value.bot?.username, "", 100),
@@ -1955,8 +2148,8 @@ var normalizeSnapshot2 = api.normalizeSnapshot;
1955
2148
  var presentError2 = api.presentError;
1956
2149
 
1957
2150
  // plugin-src/client/channels/shared/token-channel.js
1958
- var React5 = __toESM(require("react"), 1);
1959
- 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) {
1960
2153
  return h2("button", {
1961
2154
  ...props,
1962
2155
  ref,
@@ -1999,7 +2192,7 @@ function createTokenChannelSettings(definition) {
1999
2192
  credentialNoun = "Bot Token",
2000
2193
  emptyActionLabel = "\u586B\u5199 Bot Token"
2001
2194
  } = definition;
2002
- function AccountCard5({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
2195
+ function AccountCard5({ account, busy, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
2003
2196
  const state = busy === "reconnect" ? "connecting" : account.state;
2004
2197
  const tone = account.connected ? "success" : state === "error" ? "error" : "warning";
2005
2198
  const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
@@ -2052,6 +2245,11 @@ function createTokenChannelSettings(definition) {
2052
2245
  h2("dd", null, checkedTime2(account.health.lastCheckedAt))
2053
2246
  )
2054
2247
  ),
2248
+ h2(WorkspaceEditor, {
2249
+ workspace: account.workspace,
2250
+ disabled: Boolean(busy),
2251
+ onSave: onWorkspaceSave
2252
+ }),
2055
2253
  h2(
2056
2254
  "div",
2057
2255
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -2092,19 +2290,20 @@ function createTokenChannelSettings(definition) {
2092
2290
  );
2093
2291
  }
2094
2292
  function SettingsTab({ rpcCall }) {
2095
- const [model, setModel] = React5.useState({
2293
+ const [model, setModel] = React7.useState({
2096
2294
  phase: "loading",
2097
2295
  bots: [],
2098
2296
  totals: { configured: 0, connected: 0 },
2099
2297
  error: null
2100
2298
  });
2101
- const [credentialOpen, setCredentialOpen] = React5.useState(false);
2102
- const [credentialError, setCredentialError] = React5.useState(null);
2103
- const [busy, setBusy] = React5.useState(false);
2104
- const [busyByBot, setBusyByBot] = React5.useState({});
2105
- const [removeTarget, setRemoveTarget] = React5.useState(null);
2106
- const mounted = React5.useRef(true);
2107
- 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(() => {
2108
2307
  const disposeDingtalk = installDingtalkStyles();
2109
2308
  const disposeChannel = installStyles();
2110
2309
  mounted.current = true;
@@ -2114,18 +2313,20 @@ function createTokenChannelSettings(definition) {
2114
2313
  disposeDingtalk();
2115
2314
  };
2116
2315
  }, []);
2117
- const invoke = React5.useCallback(async (endpoint, payload = {}, signal) => {
2316
+ const invoke = React7.useCallback(async (endpoint, payload = {}, signal) => {
2118
2317
  if (typeof rpcCall !== "function") throw new TypeError(`${channel4} \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5`);
2119
2318
  return api4.unwrapRpcResult(await rpcCall(endpoint, payload, signal));
2120
2319
  }, [rpcCall]);
2121
- 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;
2122
2323
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
2123
2324
  try {
2124
2325
  const snapshot = api4.normalizeSnapshot(await invoke(endpoints.status, {}, signal));
2125
- if (!mounted.current || signal?.aborted) return;
2326
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return;
2126
2327
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
2127
2328
  } catch (error) {
2128
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
2329
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
2129
2330
  setModel((current) => ({
2130
2331
  ...current,
2131
2332
  phase: silent ? current.phase : "error",
@@ -2133,13 +2334,13 @@ function createTokenChannelSettings(definition) {
2133
2334
  }));
2134
2335
  }
2135
2336
  }
2136
- }, [invoke]);
2137
- React5.useEffect(() => {
2337
+ }, [invoke, workspaceFence]);
2338
+ React7.useEffect(() => {
2138
2339
  const controller = new AbortController();
2139
2340
  void loadStatus({ signal: controller.signal });
2140
2341
  return () => controller.abort();
2141
2342
  }, [loadStatus]);
2142
- React5.useEffect(() => {
2343
+ React7.useEffect(() => {
2143
2344
  if (model.phase !== "ready") return void 0;
2144
2345
  const controller = new AbortController();
2145
2346
  const timer = window.setInterval(
@@ -2151,7 +2352,8 @@ function createTokenChannelSettings(definition) {
2151
2352
  window.clearInterval(timer);
2152
2353
  };
2153
2354
  }, [loadStatus, model.phase]);
2154
- const bindCredentials = React5.useCallback(async (values) => {
2355
+ const bindCredentials = React7.useCallback(async (values) => {
2356
+ const snapshotVersion = workspaceFence.beginMutation();
2155
2357
  setBusy(true);
2156
2358
  setCredentialError(null);
2157
2359
  try {
@@ -2160,29 +2362,36 @@ function createTokenChannelSettings(definition) {
2160
2362
  credentialPayload(values)
2161
2363
  ));
2162
2364
  if (!mounted.current) return;
2163
- 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
+ }
2164
2368
  setCredentialOpen(false);
2165
2369
  } catch (error) {
2166
2370
  if (mounted.current) setCredentialError(api4.presentError(error));
2167
2371
  } finally {
2372
+ const shouldRefresh = workspaceFence.endMutation();
2373
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
2168
2374
  if (mounted.current) setBusy(false);
2169
2375
  }
2170
- }, [invoke]);
2171
- 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();
2172
2379
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
2173
2380
  try {
2174
2381
  const snapshot = api4.normalizeSnapshot(await invoke(endpoint, payload));
2175
- if (mounted.current) {
2382
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
2176
2383
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
2177
2384
  }
2178
2385
  } finally {
2386
+ const shouldRefresh = workspaceFence.endMutation();
2387
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
2179
2388
  if (mounted.current) setBusyByBot((current) => {
2180
2389
  const next = { ...current };
2181
2390
  delete next[account.botId];
2182
2391
  return next;
2183
2392
  });
2184
2393
  }
2185
- }, [invoke]);
2394
+ }, [invoke, loadStatus, workspaceFence]);
2186
2395
  const botList = model.bots.length > 0 ? h2(
2187
2396
  "section",
2188
2397
  { className: "dim-listSection" },
@@ -2201,6 +2410,12 @@ function createTokenChannelSettings(definition) {
2201
2410
  endpoints.reconnectBot,
2202
2411
  { botId: account.botId }
2203
2412
  ),
2413
+ onWorkspaceSave: (workspace) => botAction(
2414
+ account,
2415
+ "workspace",
2416
+ endpoints.setWorkspace,
2417
+ { botId: account.botId, workspace }
2418
+ ),
2204
2419
  onRequestRemove: () => setRemoveTarget(account.botId),
2205
2420
  onCancelRemove: () => setRemoveTarget(null),
2206
2421
  onConfirmRemove: async () => {
@@ -2260,7 +2475,7 @@ function createTokenChannelSettings(definition) {
2260
2475
  h2(Button3, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
2261
2476
  )
2262
2477
  ) : h2(
2263
- React5.Fragment,
2478
+ React7.Fragment,
2264
2479
  null,
2265
2480
  credentialOpen ? CredentialPanel ? h2(CredentialPanel, {
2266
2481
  busy,
@@ -2361,7 +2576,7 @@ var DiscordSettingsTab = channel.SettingsTab;
2361
2576
  var DiscordAccountCard = channel.AccountCard;
2362
2577
 
2363
2578
  // plugin-src/client/channels/feishu/index.js
2364
- var React7 = __toESM(require("react"), 1);
2579
+ var React9 = __toESM(require("react"), 1);
2365
2580
 
2366
2581
  // plugin-src/client/channels/feishu/api.js
2367
2582
  var FEISHU_RPC_CHANNEL = "/feishu";
@@ -2374,6 +2589,7 @@ var FEISHU_ENDPOINTS = Object.freeze({
2374
2589
  reconnectBot: "bot.reconnect",
2375
2590
  disconnectBot: "bot.disconnect",
2376
2591
  deleteBot: "bot.delete",
2592
+ setWorkspace: "bot.workspace.set",
2377
2593
  // Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
2378
2594
  testConnection: "connection.test",
2379
2595
  disconnect: "connection.disconnect"
@@ -2490,6 +2706,7 @@ function normalizeBotConnection(value, fallbackBotId) {
2490
2706
  state: authoritativeState(value.state, connected),
2491
2707
  connected,
2492
2708
  configured: value.configured !== false,
2709
+ workspace: optionalString2(value.workspace)?.slice(0, 4096) ?? "",
2493
2710
  bot: normalizeBot2(value.bot),
2494
2711
  health: normalizeHealth(value.health, connected),
2495
2712
  error: normalizeError2(value.error)
@@ -2579,7 +2796,7 @@ function formatRemaining2(milliseconds) {
2579
2796
  }
2580
2797
 
2581
2798
  // plugin-src/client/lifecycle.js
2582
- var React6 = __toESM(require("react"), 1);
2799
+ var React8 = __toESM(require("react"), 1);
2583
2800
  function createPollScheduler({ setTimeoutFn, clearTimeoutFn }) {
2584
2801
  let disposed = false;
2585
2802
  let timer;
@@ -2641,8 +2858,8 @@ function createAnimationFrameScheduler({ requestFrame, cancelFrame }) {
2641
2858
  };
2642
2859
  }
2643
2860
  function useAnimationFrameScheduler() {
2644
- const schedulerRef = React6.useRef(null);
2645
- React6.useEffect(() => {
2861
+ const schedulerRef = React8.useRef(null);
2862
+ React8.useEffect(() => {
2646
2863
  const scheduler = createAnimationFrameScheduler({
2647
2864
  requestFrame: (callback) => window.requestAnimationFrame(callback),
2648
2865
  cancelFrame: (frame) => window.cancelAnimationFrame(frame)
@@ -2653,7 +2870,7 @@ function useAnimationFrameScheduler() {
2653
2870
  if (schedulerRef.current === scheduler) schedulerRef.current = null;
2654
2871
  };
2655
2872
  }, []);
2656
- return React6.useCallback(
2873
+ return React8.useCallback(
2657
2874
  (callback, key) => schedulerRef.current?.schedule(callback, key) ?? false,
2658
2875
  []
2659
2876
  );
@@ -3221,7 +3438,7 @@ function QrIcon({ size = 58 }) {
3221
3438
  fill: "currentColor"
3222
3439
  }));
3223
3440
  }
3224
- 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) {
3225
3442
  return h2("button", {
3226
3443
  ...props,
3227
3444
  ref,
@@ -3333,13 +3550,13 @@ function safeQrSource2(value) {
3333
3550
  return /^data:image\/(?:png|webp|svg\+xml)(?:;charset=[^;,]+)?;base64,/i.test(value) ? value : void 0;
3334
3551
  }
3335
3552
  function QrPane({ provision, now, onRefresh, onCancel, busy }) {
3336
- const [imageFailed, setImageFailed] = React7.useState(false);
3553
+ const [imageFailed, setImageFailed] = React9.useState(false);
3337
3554
  const qrSource = safeQrSource2(provision.qrCodeDataUrl);
3338
3555
  const href = safeVerificationHref(provision.verificationUrl);
3339
3556
  const remaining = Math.max(0, provision.expiresAt - now);
3340
3557
  const expired = provision.expired === true || remaining === 0;
3341
3558
  const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
3342
- React7.useEffect(() => setImageFailed(false), [qrSource]);
3559
+ React9.useEffect(() => setImageFailed(false), [qrSource]);
3343
3560
  return h2(
3344
3561
  "div",
3345
3562
  { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
@@ -3489,11 +3706,11 @@ function formatCheckedTime(timestamp7) {
3489
3706
  }
3490
3707
  }
3491
3708
  function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) {
3492
- const cancelRef = React7.useRef(null);
3709
+ const cancelRef = React9.useRef(null);
3493
3710
  const idPart = bot.botId.replace(/[^a-zA-Z0-9_-]/g, "-");
3494
3711
  const titleId = `bxf-remove-title-${idPart}`;
3495
3712
  const descriptionId = `bxf-remove-description-${idPart}`;
3496
- React7.useEffect(() => cancelRef.current?.focus(), []);
3713
+ React9.useEffect(() => cancelRef.current?.focus(), []);
3497
3714
  return h2(
3498
3715
  "div",
3499
3716
  {
@@ -3532,6 +3749,7 @@ function BotCard({
3532
3749
  actionError,
3533
3750
  removing,
3534
3751
  onReconnect,
3752
+ onWorkspaceSave,
3535
3753
  onRequestRemove,
3536
3754
  onConfirmRemove,
3537
3755
  onCancelRemove,
@@ -3596,6 +3814,11 @@ function BotCard({
3596
3814
  h2("dd", null, formatCheckedTime(health.lastCheckedAt))
3597
3815
  )
3598
3816
  ),
3817
+ h2(WorkspaceEditor, {
3818
+ workspace: connection.workspace,
3819
+ disabled: Boolean(busy),
3820
+ onSave: onWorkspaceSave
3821
+ }),
3599
3822
  h2(
3600
3823
  "div",
3601
3824
  { className: "bxf-connectedFooter dim-cardFooter" },
@@ -3654,6 +3877,7 @@ function BotList(props) {
3654
3877
  actionError: props.errorsByBot[bot.botId],
3655
3878
  removing: props.removeTargetId === bot.botId,
3656
3879
  onReconnect: () => props.onReconnect(bot),
3880
+ onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
3657
3881
  onRequestRemove: () => props.onRequestRemove(bot),
3658
3882
  onConfirmRemove: () => props.onConfirmRemove(bot),
3659
3883
  onCancelRemove: props.onCancelRemove,
@@ -3714,7 +3938,7 @@ function mergeFeishuSnapshotState(current, snapshot, { restoreProvisioning = fal
3714
3938
  };
3715
3939
  }
3716
3940
  function FeishuSettingsTab({ rpcCall }) {
3717
- const [model, setModel] = React7.useState({
3941
+ const [model, setModel] = React9.useState({
3718
3942
  phase: "loading",
3719
3943
  revision: 0,
3720
3944
  bots: [],
@@ -3723,31 +3947,39 @@ function FeishuSettingsTab({ rpcCall }) {
3723
3947
  pageError: null,
3724
3948
  statusError: null
3725
3949
  });
3726
- const [pageBusy, setPageBusy] = React7.useState(false);
3727
- const [provisionBusy, setProvisionBusy] = React7.useState(false);
3728
- const [credentialOpen, setCredentialOpen] = React7.useState(false);
3729
- const [credentialBusy, setCredentialBusy] = React7.useState(false);
3730
- const [credentialError, setCredentialError] = React7.useState(null);
3731
- const [busyByBot, setBusyByBot] = React7.useState({});
3732
- const [errorsByBot, setErrorsByBot] = React7.useState({});
3733
- const [removeTargetId, setRemoveTargetId] = React7.useState(null);
3734
- const [announcement, setAnnouncement] = React7.useState("");
3735
- const [now, setNow] = React7.useState(() => Date.now());
3736
- const [focusBotId, setFocusBotId] = React7.useState(null);
3737
- const cardRefs = React7.useRef(/* @__PURE__ */ new Map());
3738
- const removeButtonRefs = React7.useRef(/* @__PURE__ */ new Map());
3739
- 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();
3740
3966
  const scheduleAnimationFrame = useAnimationFrameScheduler();
3741
- 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) => {
3742
3974
  setAnnouncement("");
3743
3975
  scheduleAnimationFrame(() => {
3744
3976
  if (message) setAnnouncement(message);
3745
3977
  }, "announcement");
3746
3978
  }, [scheduleAnimationFrame]);
3747
- const invoke = React7.useCallback(async (endpoint, payload = {}, signal) => {
3979
+ const invoke = React9.useCallback(async (endpoint, payload = {}, signal) => {
3748
3980
  return unwrapRpcResult3(await rpcCall(endpoint, payload, signal));
3749
3981
  }, [rpcCall]);
3750
- const mergeSnapshot = React7.useCallback((snapshot, { restoreProvisioning = false } = {}) => {
3982
+ const mergeSnapshot = React9.useCallback((snapshot, { restoreProvisioning = false } = {}) => {
3751
3983
  const now2 = Date.now();
3752
3984
  setModel((current) => mergeFeishuSnapshotState(
3753
3985
  current,
@@ -3755,28 +3987,30 @@ function FeishuSettingsTab({ rpcCall }) {
3755
3987
  { restoreProvisioning, now: now2 }
3756
3988
  ));
3757
3989
  }, []);
3758
- 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;
3759
3993
  if (!silent) setPageBusy(true);
3760
3994
  try {
3761
3995
  const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.status, {}, signal));
3762
- if (signal?.aborted) return void 0;
3996
+ if (signal?.aborted || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
3763
3997
  mergeSnapshot(snapshot, { restoreProvisioning });
3764
3998
  return snapshot;
3765
3999
  } catch (error) {
3766
- if (signal?.aborted || error?.name === "AbortError") return void 0;
4000
+ if (signal?.aborted || error?.name === "AbortError" || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
3767
4001
  const presented = presentError3(error);
3768
4002
  setModel((current) => current.phase === "loading" || !silent ? { ...current, phase: "error", pageError: presented } : { ...current, statusError: presented });
3769
4003
  return void 0;
3770
4004
  } finally {
3771
- if (!silent && !signal?.aborted) setPageBusy(false);
4005
+ if (!silent && !signal?.aborted && mountedRef.current) setPageBusy(false);
3772
4006
  }
3773
- }, [invoke, mergeSnapshot]);
3774
- React7.useEffect(() => {
4007
+ }, [invoke, mergeSnapshot, workspaceFence]);
4008
+ React9.useEffect(() => {
3775
4009
  const controller = new AbortController();
3776
4010
  void loadStatus({ signal: controller.signal, restoreProvisioning: true });
3777
4011
  return () => controller.abort();
3778
4012
  }, [loadStatus]);
3779
- React7.useEffect(() => {
4013
+ React9.useEffect(() => {
3780
4014
  if (model.phase !== "ready") return void 0;
3781
4015
  const controller = new AbortController();
3782
4016
  let inFlight = false;
@@ -3795,7 +4029,7 @@ function FeishuSettingsTab({ rpcCall }) {
3795
4029
  window.clearInterval(timer);
3796
4030
  };
3797
4031
  }, [loadStatus, model.phase]);
3798
- React7.useEffect(() => {
4032
+ React9.useEffect(() => {
3799
4033
  if (!focusBotId) return;
3800
4034
  const node = cardRefs.current.get(focusBotId);
3801
4035
  if (!node) return;
@@ -3803,7 +4037,7 @@ function FeishuSettingsTab({ rpcCall }) {
3803
4037
  node.focus({ preventScroll: true });
3804
4038
  setFocusBotId(null);
3805
4039
  }, [focusBotId, model.bots]);
3806
- const startProvisioning = React7.useCallback(async ({ replace = false } = {}) => {
4040
+ const startProvisioning = React9.useCallback(async ({ replace = false } = {}) => {
3807
4041
  setCredentialOpen(false);
3808
4042
  setCredentialError(null);
3809
4043
  setProvisionBusy(true);
@@ -3843,7 +4077,8 @@ function FeishuSettingsTab({ rpcCall }) {
3843
4077
  setProvisionBusy(false);
3844
4078
  }
3845
4079
  }, [announce, invoke, model.provisioning?.attemptId]);
3846
- const bindCredentials = React7.useCallback(async ({ identity, secret }) => {
4080
+ const bindCredentials = React9.useCallback(async ({ identity, secret }) => {
4081
+ const snapshotVersion = workspaceFence.beginMutation();
3847
4082
  setCredentialBusy(true);
3848
4083
  setCredentialError(null);
3849
4084
  try {
@@ -3851,16 +4086,20 @@ function FeishuSettingsTab({ rpcCall }) {
3851
4086
  FEISHU_ENDPOINTS.bindCredentials,
3852
4087
  { appId: identity, appSecret: secret }
3853
4088
  ));
3854
- mergeSnapshot(snapshot);
4089
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4090
+ mergeSnapshot(snapshot);
4091
+ }
3855
4092
  setCredentialOpen(false);
3856
4093
  announce("\u98DE\u4E66\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
3857
4094
  } catch (error) {
3858
4095
  setCredentialError(presentError3(error));
3859
4096
  } finally {
4097
+ const shouldRefresh = workspaceFence.endMutation();
4098
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
3860
4099
  setCredentialBusy(false);
3861
4100
  }
3862
- }, [announce, invoke, mergeSnapshot]);
3863
- const cancelProvisioning = React7.useCallback(async () => {
4101
+ }, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]);
4102
+ const cancelProvisioning = React9.useCallback(async () => {
3864
4103
  const attemptId = model.provisioning?.attemptId;
3865
4104
  setProvisionBusy(true);
3866
4105
  try {
@@ -3882,7 +4121,7 @@ function FeishuSettingsTab({ rpcCall }) {
3882
4121
  const countdownPhase = model.provisioning?.phase;
3883
4122
  const countdownExpiresAt = model.provisioning?.expiresAt;
3884
4123
  const countdownExpired = model.provisioning?.expired;
3885
- React7.useEffect(() => {
4124
+ React9.useEffect(() => {
3886
4125
  if (!countdownAttemptId || countdownPhase !== "qr" || countdownExpired) return void 0;
3887
4126
  const tick = () => {
3888
4127
  const timestamp7 = Date.now();
@@ -3895,7 +4134,7 @@ function FeishuSettingsTab({ rpcCall }) {
3895
4134
  const timer = window.setInterval(tick, 1e3);
3896
4135
  return () => window.clearInterval(timer);
3897
4136
  }, [countdownAttemptId, countdownPhase, countdownExpiresAt, countdownExpired]);
3898
- React7.useEffect(() => {
4137
+ React9.useEffect(() => {
3899
4138
  const provision2 = model.provisioning;
3900
4139
  if (!provision2 || !["qr", "connecting"].includes(provision2.phase) || !provision2.attemptId || provision2.expired) return void 0;
3901
4140
  const controller = new AbortController();
@@ -3959,7 +4198,7 @@ function FeishuSettingsTab({ rpcCall }) {
3959
4198
  window.clearTimeout(timer);
3960
4199
  };
3961
4200
  }, [announce, invoke, loadStatus, model.provisioning]);
3962
- const setBotBusy = React7.useCallback((botId, value) => {
4201
+ const setBotBusy = React9.useCallback((botId, value) => {
3963
4202
  setBusyByBot((current) => {
3964
4203
  const next = { ...current };
3965
4204
  if (value) next[botId] = value;
@@ -3967,7 +4206,7 @@ function FeishuSettingsTab({ rpcCall }) {
3967
4206
  return next;
3968
4207
  });
3969
4208
  }, []);
3970
- const setBotError = React7.useCallback((botId, error) => {
4209
+ const setBotError = React9.useCallback((botId, error) => {
3971
4210
  setErrorsByBot((current) => {
3972
4211
  const next = { ...current };
3973
4212
  if (error) next[botId] = presentError3(error);
@@ -3975,13 +4214,16 @@ function FeishuSettingsTab({ rpcCall }) {
3975
4214
  return next;
3976
4215
  });
3977
4216
  }, []);
3978
- const reconnectOneBot = React7.useCallback(async (connection) => {
4217
+ const reconnectOneBot = React9.useCallback(async (connection) => {
3979
4218
  const { botId, bot } = connection;
4219
+ const snapshotVersion = workspaceFence.beginMutation();
3980
4220
  setBotBusy(botId, "reconnect");
3981
4221
  setBotError(botId, null);
3982
4222
  try {
3983
4223
  const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.reconnectBot, { botId }));
3984
- mergeSnapshot(snapshot);
4224
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4225
+ mergeSnapshot(snapshot);
4226
+ }
3985
4227
  const refreshed = snapshot.bots.find((item) => item.botId === botId);
3986
4228
  if (!refreshed?.connected) {
3987
4229
  const error = new Error(
@@ -3995,45 +4237,63 @@ function FeishuSettingsTab({ rpcCall }) {
3995
4237
  setBotError(botId, error);
3996
4238
  announce(`${bot.name}\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u673A\u5668\u4EBA\u72B6\u6001\u3002`);
3997
4239
  } finally {
4240
+ const shouldRefresh = workspaceFence.endMutation();
4241
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
3998
4242
  setBotBusy(botId, null);
3999
4243
  }
4000
- }, [announce, invoke, mergeSnapshot, setBotBusy, setBotError]);
4001
- 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) => {
4002
4265
  setRemoveTargetId(connection.botId);
4003
4266
  }, []);
4004
- const cancelRemove = React7.useCallback(() => {
4267
+ const cancelRemove = React9.useCallback(() => {
4005
4268
  const botId = removeTargetId;
4006
4269
  setRemoveTargetId(null);
4007
4270
  scheduleAnimationFrame(() => removeButtonRefs.current.get(botId)?.focus(), "focus");
4008
4271
  }, [removeTargetId, scheduleAnimationFrame]);
4009
- const confirmRemove = React7.useCallback(async (connection) => {
4272
+ const confirmRemove = React9.useCallback(async (connection) => {
4010
4273
  const { botId, bot } = connection;
4274
+ const snapshotVersion = workspaceFence.beginMutation();
4011
4275
  setBotBusy(botId, "delete");
4012
4276
  setBotError(botId, null);
4013
4277
  try {
4014
- await invoke(FEISHU_ENDPOINTS.deleteBot, { botId, confirm: true });
4278
+ const snapshot = normalizeBotsSnapshot(await invoke(
4279
+ FEISHU_ENDPOINTS.deleteBot,
4280
+ { botId, confirm: true }
4281
+ ));
4015
4282
  setRemoveTargetId(null);
4016
- setModel((current) => {
4017
- const bots = current.bots.filter((item) => item.botId !== botId);
4018
- return {
4019
- ...current,
4020
- bots,
4021
- totals: {
4022
- configured: bots.length,
4023
- connected: bots.filter((item) => item.connected).length
4024
- }
4025
- };
4026
- });
4283
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
4284
+ mergeSnapshot(snapshot);
4285
+ }
4027
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`);
4028
- await loadStatus({ silent: true });
4029
4287
  scheduleAnimationFrame(() => addButtonRef.current?.focus(), "focus");
4030
4288
  } catch (error) {
4031
4289
  setBotError(botId, error);
4032
4290
  announce(`${bot.name}\u79FB\u9664\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002`);
4033
4291
  } finally {
4292
+ const shouldRefresh = workspaceFence.endMutation();
4293
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
4034
4294
  setBotBusy(botId, null);
4035
4295
  }
4036
- }, [announce, invoke, loadStatus, scheduleAnimationFrame, setBotBusy, setBotError]);
4296
+ }, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
4037
4297
  const provision = model.provisioning;
4038
4298
  let provisionContent = null;
4039
4299
  if (provision?.phase === "creating") {
@@ -4074,11 +4334,11 @@ function FeishuSettingsTab({ rpcCall }) {
4074
4334
  setCredentialError(null);
4075
4335
  }
4076
4336
  }) : null;
4077
- const setCardRef = React7.useCallback((botId, node) => {
4337
+ const setCardRef = React9.useCallback((botId, node) => {
4078
4338
  if (node) cardRefs.current.set(botId, node);
4079
4339
  else cardRefs.current.delete(botId);
4080
4340
  }, []);
4081
- const setRemoveButtonRef = React7.useCallback((botId, node) => {
4341
+ const setRemoveButtonRef = React9.useCallback((botId, node) => {
4082
4342
  if (node) removeButtonRefs.current.set(botId, node);
4083
4343
  else removeButtonRefs.current.delete(botId);
4084
4344
  }, []);
@@ -4115,7 +4375,7 @@ function FeishuSettingsTab({ rpcCall }) {
4115
4375
  onRetry: () => void loadStatus(),
4116
4376
  busy: pageBusy
4117
4377
  }) : h2(
4118
- React7.Fragment,
4378
+ React9.Fragment,
4119
4379
  null,
4120
4380
  credentialContent,
4121
4381
  provisionContent,
@@ -4126,6 +4386,7 @@ function FeishuSettingsTab({ rpcCall }) {
4126
4386
  errorsByBot,
4127
4387
  removeTargetId,
4128
4388
  onReconnect: (bot) => void reconnectOneBot(bot),
4389
+ onWorkspaceSave: saveWorkspace,
4129
4390
  onRequestRemove: requestRemove,
4130
4391
  onConfirmRemove: (bot) => void confirmRemove(bot),
4131
4392
  onCancelRemove: cancelRemove,
@@ -4145,7 +4406,8 @@ var QQ_ENDPOINTS = Object.freeze({
4145
4406
  cancelProvisioning: "provision.cancel",
4146
4407
  bindCredentials: "bot.bind-credentials",
4147
4408
  reconnectBot: "bot.reconnect",
4148
- deleteBot: "bot.delete"
4409
+ deleteBot: "bot.delete",
4410
+ setWorkspace: "bot.workspace.set"
4149
4411
  });
4150
4412
  var PROVISION_STATES2 = /* @__PURE__ */ new Set(["starting", "pending", "refreshing", "connecting", "connected", "failed", "cancelled"]);
4151
4413
  var ACCOUNT_STATES3 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
@@ -4207,6 +4469,7 @@ function normalizeBot3(value) {
4207
4469
  botId: id2(value.botId),
4208
4470
  connected,
4209
4471
  state: connected ? "connected" : state,
4472
+ workspace: text2(value.workspace, "", 4096),
4210
4473
  bot: {
4211
4474
  name: text2(value.bot?.name, "QQ\u673A\u5668\u4EBA", 100),
4212
4475
  appIdMasked: text2(value.bot?.appIdMasked, "\u5E94\u7528\u6807\u8BC6\u5DF2\u5B89\u5168\u4FDD\u5B58", 140)
@@ -4244,7 +4507,7 @@ function formatRemaining3(milliseconds) {
4244
4507
  }
4245
4508
 
4246
4509
  // plugin-src/client/channels/qq/index.js
4247
- var React8 = __toESM(require("react"), 1);
4510
+ var React10 = __toESM(require("react"), 1);
4248
4511
 
4249
4512
  // plugin-src/client/channels/qq/styles.js
4250
4513
  var QQ_STYLE_ID = "xmanrui-dsh-im-qq-settings";
@@ -4269,7 +4532,7 @@ function installQqStyles() {
4269
4532
 
4270
4533
  // plugin-src/client/channels/qq/index.js
4271
4534
  var ACTIVE_STATES = /* @__PURE__ */ new Set(["pending", "refreshing", "connecting"]);
4272
- 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) {
4273
4536
  return h2("button", {
4274
4537
  ...props,
4275
4538
  ref,
@@ -4476,7 +4739,16 @@ function RemoveConfirmation3({ account, busy, onConfirm, onCancel }) {
4476
4739
  )
4477
4740
  );
4478
4741
  }
4479
- 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
+ }) {
4480
4752
  const tone = account.connected ? "success" : account.state === "error" ? "error" : "warning";
4481
4753
  const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : account.state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
4482
4754
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
@@ -4513,6 +4785,11 @@ function AccountCard2({ account, busy, removing, onReconnect, onRequestRemove, o
4513
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")),
4514
4786
  h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), h2("dd", null, checkedTime3(account.health.lastCheckedAt)))
4515
4787
  ),
4788
+ h2(WorkspaceEditor, {
4789
+ workspace: account.workspace,
4790
+ disabled: Boolean(busy),
4791
+ onSave: onWorkspaceSave
4792
+ }),
4516
4793
  h2(
4517
4794
  "div",
4518
4795
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -4534,17 +4811,18 @@ function AccountCard2({ account, busy, removing, onReconnect, onRequestRemove, o
4534
4811
  );
4535
4812
  }
4536
4813
  function QqSettingsTab({ rpcCall }) {
4537
- const [model, setModel] = React8.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null });
4538
- const [provision, setProvision] = React8.useState(null);
4539
- const [busy, setBusy] = React8.useState(false);
4540
- const [busyByBot, setBusyByBot] = React8.useState({});
4541
- const [removeTarget, setRemoveTarget] = React8.useState(null);
4542
- const [credentialOpen, setCredentialOpen] = React8.useState(false);
4543
- const [credentialError, setCredentialError] = React8.useState(null);
4544
- const [now, setNow] = React8.useState(Date.now());
4545
- const mounted = React8.useRef(true);
4546
- const addButtonRef = React8.useRef(null);
4547
- 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(() => {
4548
4826
  const disposeDingtalk = installDingtalkStyles();
4549
4827
  const disposeQq = installQqStyles();
4550
4828
  mounted.current = true;
@@ -4554,15 +4832,17 @@ function QqSettingsTab({ rpcCall }) {
4554
4832
  disposeDingtalk();
4555
4833
  };
4556
4834
  }, []);
4557
- const invoke = React8.useCallback(async (endpoint, payload = {}, signal) => {
4835
+ const invoke = React10.useCallback(async (endpoint, payload = {}, signal) => {
4558
4836
  if (typeof rpcCall !== "function") throw new TypeError("QQ \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
4559
4837
  return unwrapRpcResult4(await rpcCall(endpoint, payload, signal));
4560
4838
  }, [rpcCall]);
4561
- 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;
4562
4842
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
4563
4843
  try {
4564
4844
  const snapshot = normalizeSnapshot3(await invoke(QQ_ENDPOINTS.status, {}, signal));
4565
- if (!mounted.current || signal?.aborted) return void 0;
4845
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
4566
4846
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
4567
4847
  if (restore && snapshot.provisioning) setProvision({
4568
4848
  ...snapshot.provisioning,
@@ -4570,18 +4850,18 @@ function QqSettingsTab({ rpcCall }) {
4570
4850
  });
4571
4851
  return snapshot;
4572
4852
  } catch (error) {
4573
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
4853
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
4574
4854
  setModel((current) => ({ ...current, phase: silent ? current.phase : "error", error: presentError4(error) }));
4575
4855
  }
4576
4856
  return void 0;
4577
4857
  }
4578
- }, [invoke]);
4579
- React8.useEffect(() => {
4858
+ }, [invoke, workspaceFence]);
4859
+ React10.useEffect(() => {
4580
4860
  const controller = new AbortController();
4581
4861
  void loadStatus({ signal: controller.signal, restore: true });
4582
4862
  return () => controller.abort();
4583
4863
  }, [loadStatus]);
4584
- React8.useEffect(() => {
4864
+ React10.useEffect(() => {
4585
4865
  if (model.phase !== "ready") return void 0;
4586
4866
  const controller = new AbortController();
4587
4867
  const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15e3);
@@ -4590,12 +4870,12 @@ function QqSettingsTab({ rpcCall }) {
4590
4870
  window.clearInterval(timer);
4591
4871
  };
4592
4872
  }, [loadStatus, model.phase]);
4593
- React8.useEffect(() => {
4873
+ React10.useEffect(() => {
4594
4874
  if (!provision || !ACTIVE_STATES.has(provision.status)) return void 0;
4595
4875
  const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
4596
4876
  return () => window.clearInterval(timer);
4597
4877
  }, [provision?.attemptId, provision?.status]);
4598
- const startProvisioning = React8.useCallback(async (replace = false) => {
4878
+ const startProvisioning = React10.useCallback(async (replace = false) => {
4599
4879
  setCredentialOpen(false);
4600
4880
  setCredentialError(null);
4601
4881
  setBusy(true);
@@ -4613,7 +4893,8 @@ function QqSettingsTab({ rpcCall }) {
4613
4893
  if (mounted.current) setBusy(false);
4614
4894
  }
4615
4895
  }, [invoke, provision?.attemptId]);
4616
- const bindCredentials = React8.useCallback(async ({ identity, secret }) => {
4896
+ const bindCredentials = React10.useCallback(async ({ identity, secret }) => {
4897
+ const snapshotVersion = workspaceFence.beginMutation();
4617
4898
  setBusy(true);
4618
4899
  setCredentialError(null);
4619
4900
  try {
@@ -4622,15 +4903,19 @@ function QqSettingsTab({ rpcCall }) {
4622
4903
  { appId: identity, appSecret: secret }
4623
4904
  ));
4624
4905
  if (!mounted.current) return;
4625
- 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
+ }
4626
4909
  setCredentialOpen(false);
4627
4910
  } catch (error) {
4628
4911
  if (mounted.current) setCredentialError(presentError4(error));
4629
4912
  } finally {
4913
+ const shouldRefresh = workspaceFence.endMutation();
4914
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
4630
4915
  if (mounted.current) setBusy(false);
4631
4916
  }
4632
- }, [invoke]);
4633
- const closeProvision = React8.useCallback(async () => {
4917
+ }, [invoke, loadStatus, workspaceFence]);
4918
+ const closeProvision = React10.useCallback(async () => {
4634
4919
  setBusy(true);
4635
4920
  try {
4636
4921
  if (provision?.attemptId && ACTIVE_STATES.has(provision.status)) {
@@ -4641,7 +4926,7 @@ function QqSettingsTab({ rpcCall }) {
4641
4926
  if (mounted.current) setBusy(false);
4642
4927
  }
4643
4928
  }, [invoke, provision?.attemptId, provision?.status]);
4644
- React8.useEffect(() => {
4929
+ React10.useEffect(() => {
4645
4930
  const attemptId = provision?.attemptId;
4646
4931
  if (!attemptId || !ACTIVE_STATES.has(provision.status)) return void 0;
4647
4932
  const controller = new AbortController();
@@ -4671,20 +4956,25 @@ function QqSettingsTab({ rpcCall }) {
4671
4956
  window.clearTimeout(timer);
4672
4957
  };
4673
4958
  }, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
4674
- const botAction = React8.useCallback(async (account, operation, endpoint, payload) => {
4959
+ const botAction = React10.useCallback(async (account, operation, endpoint, payload) => {
4960
+ const snapshotVersion = workspaceFence.beginMutation();
4675
4961
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
4676
4962
  try {
4677
4963
  const snapshot = normalizeSnapshot3(await invoke(endpoint, payload));
4678
- 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
+ }
4679
4967
  return snapshot;
4680
4968
  } finally {
4969
+ const shouldRefresh = workspaceFence.endMutation();
4970
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
4681
4971
  if (mounted.current) setBusyByBot((current) => {
4682
4972
  const next = { ...current };
4683
4973
  delete next[account.botId];
4684
4974
  return next;
4685
4975
  });
4686
4976
  }
4687
- }, [invoke]);
4977
+ }, [invoke, loadStatus, workspaceFence]);
4688
4978
  let provisionView = null;
4689
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");
4690
4980
  else if (["pending", "refreshing"].includes(provision?.status)) provisionView = h2(QrPanel2, {
@@ -4713,6 +5003,12 @@ function QqSettingsTab({ rpcCall }) {
4713
5003
  busy: busyByBot[account.botId],
4714
5004
  removing: removeTarget === account.botId,
4715
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
+ ),
4716
5012
  onRequestRemove: () => setRemoveTarget(account.botId),
4717
5013
  onCancelRemove: () => setRemoveTarget(null),
4718
5014
  onConfirmRemove: async () => {
@@ -4751,7 +5047,7 @@ function QqSettingsTab({ rpcCall }) {
4751
5047
  addButtonRef
4752
5048
  }),
4753
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(
4754
- React8.Fragment,
5050
+ React10.Fragment,
4755
5051
  null,
4756
5052
  credentialView,
4757
5053
  provisionView,
@@ -4770,7 +5066,7 @@ var normalizeSnapshot4 = api2.normalizeSnapshot;
4770
5066
  var presentError5 = api2.presentError;
4771
5067
 
4772
5068
  // plugin-src/client/channels/slack/index.js
4773
- var React9 = __toESM(require("react"), 1);
5069
+ var React11 = __toESM(require("react"), 1);
4774
5070
 
4775
5071
  // src/channels/slack/manifest.mjs
4776
5072
  var SLACK_APP_MANIFEST_YAML = `_metadata:
@@ -4842,10 +5138,10 @@ function installSlackStyles() {
4842
5138
 
4843
5139
  // plugin-src/client/channels/slack/index.js
4844
5140
  function SlackCredentialPanel({ busy, error, onSubmit, onCancel }) {
4845
- const [botToken, setBotToken] = React9.useState("");
4846
- const [appToken, setAppToken] = React9.useState("");
4847
- const [copied, setCopied] = React9.useState(false);
4848
- 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();
4849
5145
  const copyManifest = async () => {
4850
5146
  try {
4851
5147
  await navigator.clipboard.writeText(SLACK_APP_MANIFEST_YAML);
@@ -5038,7 +5334,8 @@ var WECOM_ENDPOINTS = Object.freeze({
5038
5334
  cancelProvisioning: "provision.cancel",
5039
5335
  bindCredentials: "bot.bind-credentials",
5040
5336
  reconnectBot: "bot.reconnect",
5041
- deleteBot: "bot.delete"
5337
+ deleteBot: "bot.delete",
5338
+ setWorkspace: "bot.workspace.set"
5042
5339
  });
5043
5340
  var PROVISION_STATES3 = /* @__PURE__ */ new Set(["starting", "pending", "refreshing", "connecting", "connected", "failed", "cancelled"]);
5044
5341
  var ACCOUNT_STATES4 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
@@ -5100,6 +5397,7 @@ function normalizeBot4(value) {
5100
5397
  botId: id3(value.botId),
5101
5398
  connected,
5102
5399
  state: connected ? "connected" : state,
5400
+ workspace: text3(value.workspace, "", 4096),
5103
5401
  bot: {
5104
5402
  name: text3(value.bot?.name, "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA", 100),
5105
5403
  appIdMasked: text3(value.bot?.appIdMasked, "\u5E94\u7528\u6807\u8BC6\u5DF2\u5B89\u5168\u4FDD\u5B58", 140)
@@ -5137,7 +5435,7 @@ function formatRemaining4(milliseconds) {
5137
5435
  }
5138
5436
 
5139
5437
  // plugin-src/client/channels/wecom/index.js
5140
- var React10 = __toESM(require("react"), 1);
5438
+ var React12 = __toESM(require("react"), 1);
5141
5439
 
5142
5440
  // plugin-src/client/channels/wecom/styles.js
5143
5441
  var WECOM_STYLE_ID = "xmanrui-dsh-im-wecom-settings";
@@ -5162,7 +5460,7 @@ function installWecomStyles() {
5162
5460
 
5163
5461
  // plugin-src/client/channels/wecom/index.js
5164
5462
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["pending", "refreshing", "connecting"]);
5165
- 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) {
5166
5464
  return h2("button", {
5167
5465
  ...props,
5168
5466
  ref,
@@ -5369,7 +5667,16 @@ function RemoveConfirmation4({ account, busy, onConfirm, onCancel }) {
5369
5667
  )
5370
5668
  );
5371
5669
  }
5372
- 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
+ }) {
5373
5680
  const tone = account.connected ? "success" : account.state === "error" ? "error" : "warning";
5374
5681
  const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : account.state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
5375
5682
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
@@ -5406,6 +5713,11 @@ function AccountCard3({ account, busy, removing, onReconnect, onRequestRemove, o
5406
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")),
5407
5714
  h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), h2("dd", null, checkedTime4(account.health.lastCheckedAt)))
5408
5715
  ),
5716
+ h2(WorkspaceEditor, {
5717
+ workspace: account.workspace,
5718
+ disabled: Boolean(busy),
5719
+ onSave: onWorkspaceSave
5720
+ }),
5409
5721
  h2(
5410
5722
  "div",
5411
5723
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -5427,17 +5739,18 @@ function AccountCard3({ account, busy, removing, onReconnect, onRequestRemove, o
5427
5739
  );
5428
5740
  }
5429
5741
  function WecomSettingsTab({ rpcCall }) {
5430
- const [model, setModel] = React10.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null });
5431
- const [provision, setProvision] = React10.useState(null);
5432
- const [busy, setBusy] = React10.useState(false);
5433
- const [busyByBot, setBusyByBot] = React10.useState({});
5434
- const [removeTarget, setRemoveTarget] = React10.useState(null);
5435
- const [credentialOpen, setCredentialOpen] = React10.useState(false);
5436
- const [credentialError, setCredentialError] = React10.useState(null);
5437
- const [now, setNow] = React10.useState(Date.now());
5438
- const mounted = React10.useRef(true);
5439
- const addButtonRef = React10.useRef(null);
5440
- 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(() => {
5441
5754
  const disposeDingtalk = installDingtalkStyles();
5442
5755
  const disposeWecom = installWecomStyles();
5443
5756
  mounted.current = true;
@@ -5447,15 +5760,17 @@ function WecomSettingsTab({ rpcCall }) {
5447
5760
  disposeDingtalk();
5448
5761
  };
5449
5762
  }, []);
5450
- const invoke = React10.useCallback(async (endpoint, payload = {}, signal) => {
5763
+ const invoke = React12.useCallback(async (endpoint, payload = {}, signal) => {
5451
5764
  if (typeof rpcCall !== "function") throw new TypeError("\u4F01\u4E1A\u5FAE\u4FE1\u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
5452
5765
  return unwrapRpcResult7(await rpcCall(endpoint, payload, signal));
5453
5766
  }, [rpcCall]);
5454
- 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;
5455
5770
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
5456
5771
  try {
5457
5772
  const snapshot = normalizeSnapshot6(await invoke(WECOM_ENDPOINTS.status, {}, signal));
5458
- if (!mounted.current || signal?.aborted) return void 0;
5773
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
5459
5774
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
5460
5775
  if (restore && snapshot.provisioning) setProvision({
5461
5776
  ...snapshot.provisioning,
@@ -5463,18 +5778,18 @@ function WecomSettingsTab({ rpcCall }) {
5463
5778
  });
5464
5779
  return snapshot;
5465
5780
  } catch (error) {
5466
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
5781
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
5467
5782
  setModel((current) => ({ ...current, phase: silent ? current.phase : "error", error: presentError7(error) }));
5468
5783
  }
5469
5784
  return void 0;
5470
5785
  }
5471
- }, [invoke]);
5472
- React10.useEffect(() => {
5786
+ }, [invoke, workspaceFence]);
5787
+ React12.useEffect(() => {
5473
5788
  const controller = new AbortController();
5474
5789
  void loadStatus({ signal: controller.signal, restore: true });
5475
5790
  return () => controller.abort();
5476
5791
  }, [loadStatus]);
5477
- React10.useEffect(() => {
5792
+ React12.useEffect(() => {
5478
5793
  if (model.phase !== "ready") return void 0;
5479
5794
  const controller = new AbortController();
5480
5795
  const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15e3);
@@ -5483,12 +5798,12 @@ function WecomSettingsTab({ rpcCall }) {
5483
5798
  window.clearInterval(timer);
5484
5799
  };
5485
5800
  }, [loadStatus, model.phase]);
5486
- React10.useEffect(() => {
5801
+ React12.useEffect(() => {
5487
5802
  if (!provision || !ACTIVE_STATES2.has(provision.status)) return void 0;
5488
5803
  const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
5489
5804
  return () => window.clearInterval(timer);
5490
5805
  }, [provision?.attemptId, provision?.status]);
5491
- const startProvisioning = React10.useCallback(async (replace = false) => {
5806
+ const startProvisioning = React12.useCallback(async (replace = false) => {
5492
5807
  setCredentialOpen(false);
5493
5808
  setCredentialError(null);
5494
5809
  setBusy(true);
@@ -5506,7 +5821,8 @@ function WecomSettingsTab({ rpcCall }) {
5506
5821
  if (mounted.current) setBusy(false);
5507
5822
  }
5508
5823
  }, [invoke, provision?.attemptId]);
5509
- const bindCredentials = React10.useCallback(async ({ identity, secret }) => {
5824
+ const bindCredentials = React12.useCallback(async ({ identity, secret }) => {
5825
+ const snapshotVersion = workspaceFence.beginMutation();
5510
5826
  setBusy(true);
5511
5827
  setCredentialError(null);
5512
5828
  try {
@@ -5515,15 +5831,19 @@ function WecomSettingsTab({ rpcCall }) {
5515
5831
  { botId: identity, secret }
5516
5832
  ));
5517
5833
  if (!mounted.current) return;
5518
- 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
+ }
5519
5837
  setCredentialOpen(false);
5520
5838
  } catch (error) {
5521
5839
  if (mounted.current) setCredentialError(presentError7(error));
5522
5840
  } finally {
5841
+ const shouldRefresh = workspaceFence.endMutation();
5842
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
5523
5843
  if (mounted.current) setBusy(false);
5524
5844
  }
5525
- }, [invoke]);
5526
- const closeProvision = React10.useCallback(async () => {
5845
+ }, [invoke, loadStatus, workspaceFence]);
5846
+ const closeProvision = React12.useCallback(async () => {
5527
5847
  setBusy(true);
5528
5848
  try {
5529
5849
  if (provision?.attemptId && ACTIVE_STATES2.has(provision.status)) {
@@ -5534,7 +5854,7 @@ function WecomSettingsTab({ rpcCall }) {
5534
5854
  if (mounted.current) setBusy(false);
5535
5855
  }
5536
5856
  }, [invoke, provision?.attemptId, provision?.status]);
5537
- React10.useEffect(() => {
5857
+ React12.useEffect(() => {
5538
5858
  const attemptId = provision?.attemptId;
5539
5859
  if (!attemptId || !ACTIVE_STATES2.has(provision.status)) return void 0;
5540
5860
  const controller = new AbortController();
@@ -5564,20 +5884,25 @@ function WecomSettingsTab({ rpcCall }) {
5564
5884
  window.clearTimeout(timer);
5565
5885
  };
5566
5886
  }, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
5567
- const botAction = React10.useCallback(async (account, operation, endpoint, payload) => {
5887
+ const botAction = React12.useCallback(async (account, operation, endpoint, payload) => {
5888
+ const snapshotVersion = workspaceFence.beginMutation();
5568
5889
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
5569
5890
  try {
5570
5891
  const snapshot = normalizeSnapshot6(await invoke(endpoint, payload));
5571
- 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
+ }
5572
5895
  return snapshot;
5573
5896
  } finally {
5897
+ const shouldRefresh = workspaceFence.endMutation();
5898
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
5574
5899
  if (mounted.current) setBusyByBot((current) => {
5575
5900
  const next = { ...current };
5576
5901
  delete next[account.botId];
5577
5902
  return next;
5578
5903
  });
5579
5904
  }
5580
- }, [invoke]);
5905
+ }, [invoke, loadStatus, workspaceFence]);
5581
5906
  let provisionView = null;
5582
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");
5583
5908
  else if (["pending", "refreshing"].includes(provision?.status)) provisionView = h2(QrPanel3, {
@@ -5606,6 +5931,12 @@ function WecomSettingsTab({ rpcCall }) {
5606
5931
  busy: busyByBot[account.botId],
5607
5932
  removing: removeTarget === account.botId,
5608
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
+ ),
5609
5940
  onRequestRemove: () => setRemoveTarget(account.botId),
5610
5941
  onCancelRemove: () => setRemoveTarget(null),
5611
5942
  onConfirmRemove: async () => {
@@ -5644,7 +5975,7 @@ function WecomSettingsTab({ rpcCall }) {
5644
5975
  addButtonRef
5645
5976
  }),
5646
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(
5647
- React10.Fragment,
5978
+ React12.Fragment,
5648
5979
  null,
5649
5980
  credentialView,
5650
5981
  provisionView,
@@ -5655,7 +5986,7 @@ function WecomSettingsTab({ rpcCall }) {
5655
5986
  }
5656
5987
 
5657
5988
  // plugin-src/client/channels/weixin/index.js
5658
- var React11 = __toESM(require("react"), 1);
5989
+ var React13 = __toESM(require("react"), 1);
5659
5990
 
5660
5991
  // plugin-src/client/channels/weixin/api.js
5661
5992
  var WEIXIN_RPC_CHANNEL = "/weixin";
@@ -5666,7 +5997,8 @@ var WEIXIN_ENDPOINTS = Object.freeze({
5666
5997
  submitVerification: "provision.verify",
5667
5998
  cancelProvisioning: "provision.cancel",
5668
5999
  reconnectBot: "bot.reconnect",
5669
- deleteBot: "bot.delete"
6000
+ deleteBot: "bot.delete",
6001
+ setWorkspace: "bot.workspace.set"
5670
6002
  });
5671
6003
  var ACCOUNT_STATES5 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
5672
6004
  var PROVISION_STATES4 = /* @__PURE__ */ new Set([
@@ -5748,6 +6080,7 @@ function normalizeBot5(value) {
5748
6080
  state: connected ? "connected" : state,
5749
6081
  connected,
5750
6082
  configured: value.configured === true,
6083
+ workspace: string(value.workspace).slice(0, 4096),
5751
6084
  bot: {
5752
6085
  name: string(value.bot.name, "\u5FAE\u4FE1\u673A\u5668\u4EBA"),
5753
6086
  accountIdMasked: string(value.bot.accountIdMasked, "\u5DF2\u5B89\u5168\u4FDD\u5B58")
@@ -5918,7 +6251,7 @@ function installWeixinStyles() {
5918
6251
  }
5919
6252
 
5920
6253
  // plugin-src/client/channels/weixin/index.js
5921
- 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) {
5922
6255
  return h2("button", {
5923
6256
  ...props,
5924
6257
  ref,
@@ -5991,14 +6324,14 @@ function EmptyView5({ onStart, busy }) {
5991
6324
  );
5992
6325
  }
5993
6326
  function QrPanel4({ provision, now, busy, onRefresh, onCancel }) {
5994
- const [imageFailed, setImageFailed] = React11.useState(false);
6327
+ const [imageFailed, setImageFailed] = React13.useState(false);
5995
6328
  const source = safeQrSource5(provision.qrCodeDataUrl);
5996
6329
  const href = safeVerificationUrl(provision.verificationUrl);
5997
6330
  const remaining = Math.max(0, provision.expiresAt - now);
5998
6331
  const expired = remaining === 0 || provision.status === "expired";
5999
6332
  const duration = Math.max(1, provision.durationMs ?? 5 * 6e4);
6000
6333
  const progress = Math.round(Math.min(1, remaining / duration) * 100);
6001
- React11.useEffect(() => setImageFailed(false), [source]);
6334
+ React13.useEffect(() => setImageFailed(false), [source]);
6002
6335
  return h2(
6003
6336
  "div",
6004
6337
  { className: "dxw-card dim-surfaceCard" },
@@ -6065,9 +6398,9 @@ function QrPanel4({ provision, now, busy, onRefresh, onCancel }) {
6065
6398
  );
6066
6399
  }
6067
6400
  function VerificationPanel({ provision, busy, onSubmit, onCancel }) {
6068
- const [code, setCode] = React11.useState("");
6401
+ const [code, setCode] = React13.useState("");
6069
6402
  const valid = /^\d{4,8}$/.test(code);
6070
- React11.useEffect(() => setCode(""), [provision.attemptId]);
6403
+ React13.useEffect(() => setCode(""), [provision.attemptId]);
6071
6404
  return h2(
6072
6405
  "div",
6073
6406
  { className: "dxw-card dim-surfaceCard" },
@@ -6158,7 +6491,16 @@ function checkedTime5(timestamp7) {
6158
6491
  return "\u521A\u521A";
6159
6492
  }
6160
6493
  }
6161
- 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
+ }) {
6162
6504
  const state = busy === "reconnect" ? "connecting" : account.state;
6163
6505
  const tone = account.connected ? "success" : state === "error" ? "error" : "warning";
6164
6506
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
@@ -6200,6 +6542,11 @@ function AccountCard4({ account, busy, removing, onReconnect, onRequestRemove, o
6200
6542
  h2("dd", null, checkedTime5(account.health.lastCheckedAt))
6201
6543
  )
6202
6544
  ),
6545
+ h2(WorkspaceEditor, {
6546
+ workspace: account.workspace,
6547
+ disabled: Boolean(busy),
6548
+ onSave: onWorkspaceSave
6549
+ }),
6203
6550
  h2(
6204
6551
  "div",
6205
6552
  { className: "dxw-accountFooter dim-cardFooter" },
@@ -6247,6 +6594,7 @@ function AccountList2(props) {
6247
6594
  busy: props.busyByBot[account.botId],
6248
6595
  removing: props.removeTarget === account.botId,
6249
6596
  onReconnect: () => props.onReconnect(account),
6597
+ onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
6250
6598
  onRequestRemove: () => props.onRequestRemove(account),
6251
6599
  onConfirmRemove: () => props.onConfirmRemove(account),
6252
6600
  onCancelRemove: props.onCancelRemove
@@ -6265,39 +6613,49 @@ function mergeWeixinProvisioningSnapshot(current, incoming, { restoreProvisionin
6265
6613
  };
6266
6614
  }
6267
6615
  function WeixinSettingsTab({ rpcCall }) {
6268
- const [model, setModel] = React11.useState({
6616
+ const [model, setModel] = React13.useState({
6269
6617
  phase: "loading",
6270
6618
  bots: [],
6271
6619
  totals: EMPTY_TOTALS3,
6272
6620
  revision: 0,
6273
6621
  error: null
6274
6622
  });
6275
- const [provision, setProvision] = React11.useState(null);
6276
- const [busy, setBusy] = React11.useState(false);
6277
- const [busyByBot, setBusyByBot] = React11.useState({});
6278
- const [removeTarget, setRemoveTarget] = React11.useState(null);
6279
- const [notice, setNotice] = React11.useState("");
6280
- const [now, setNow] = React11.useState(() => Date.now());
6281
- 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();
6282
6632
  const scheduleAnimationFrame = useAnimationFrameScheduler();
6283
- 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) => {
6284
6640
  setNotice("");
6285
6641
  scheduleAnimationFrame(() => {
6286
6642
  if (value) setNotice(value);
6287
6643
  }, "announcement");
6288
6644
  }, [scheduleAnimationFrame]);
6289
- const invoke = React11.useCallback(async (endpoint, payload = {}, signal) => {
6645
+ const invoke = React13.useCallback(async (endpoint, payload = {}, signal) => {
6290
6646
  return unwrapRpcResult8(await rpcCall(endpoint, payload, signal));
6291
6647
  }, [rpcCall]);
6292
- const loadStatus = React11.useCallback(async ({
6648
+ const loadStatus = React13.useCallback(async ({
6293
6649
  signal,
6294
6650
  silent = false,
6295
6651
  restoreProvisioning = false
6296
6652
  } = {}) => {
6653
+ const workspaceVersion = workspaceFence.beginStatus();
6654
+ if (workspaceVersion === null || !mountedRef.current) return void 0;
6297
6655
  if (!silent) setModel((current) => ({ ...current, phase: "loading", error: null }));
6298
6656
  try {
6299
6657
  const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.status, {}, signal));
6300
- if (signal?.aborted) return void 0;
6658
+ if (signal?.aborted || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
6301
6659
  setModel({
6302
6660
  phase: "ready",
6303
6661
  bots: snapshot.bots,
@@ -6314,7 +6672,7 @@ function WeixinSettingsTab({ rpcCall }) {
6314
6672
  }
6315
6673
  return snapshot;
6316
6674
  } catch (error) {
6317
- if (signal?.aborted || error?.name === "AbortError") return void 0;
6675
+ if (signal?.aborted || error?.name === "AbortError" || !mountedRef.current || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
6318
6676
  setModel((current) => ({
6319
6677
  ...current,
6320
6678
  phase: silent && current.phase === "ready" ? "ready" : "error",
@@ -6322,13 +6680,13 @@ function WeixinSettingsTab({ rpcCall }) {
6322
6680
  }));
6323
6681
  return void 0;
6324
6682
  }
6325
- }, [invoke]);
6326
- React11.useEffect(() => {
6683
+ }, [invoke, workspaceFence]);
6684
+ React13.useEffect(() => {
6327
6685
  const controller = new AbortController();
6328
6686
  void loadStatus({ signal: controller.signal, restoreProvisioning: true });
6329
6687
  return () => controller.abort();
6330
6688
  }, [loadStatus]);
6331
- React11.useEffect(() => {
6689
+ React13.useEffect(() => {
6332
6690
  if (model.phase !== "ready") return void 0;
6333
6691
  const controller = new AbortController();
6334
6692
  let running = false;
@@ -6347,12 +6705,12 @@ function WeixinSettingsTab({ rpcCall }) {
6347
6705
  window.clearInterval(timer);
6348
6706
  };
6349
6707
  }, [loadStatus, model.phase]);
6350
- React11.useEffect(() => {
6708
+ React13.useEffect(() => {
6351
6709
  if (!provision || !["pending", "scanned"].includes(provision.status)) return void 0;
6352
6710
  const timer = window.setInterval(() => setNow(Date.now()), 1e3);
6353
6711
  return () => window.clearInterval(timer);
6354
6712
  }, [provision?.attemptId, provision?.status]);
6355
- const startProvisioning = React11.useCallback(async ({ replace = false } = {}) => {
6713
+ const startProvisioning = React13.useCallback(async ({ replace = false } = {}) => {
6356
6714
  setBusy(true);
6357
6715
  try {
6358
6716
  if (replace && provision?.attemptId) {
@@ -6373,7 +6731,7 @@ function WeixinSettingsTab({ rpcCall }) {
6373
6731
  setBusy(false);
6374
6732
  }
6375
6733
  }, [announce, invoke, provision?.attemptId]);
6376
- const cancelProvisioning = React11.useCallback(async () => {
6734
+ const cancelProvisioning = React13.useCallback(async () => {
6377
6735
  setBusy(true);
6378
6736
  try {
6379
6737
  if (provision?.attemptId && !["failed", "expired", "cancelled"].includes(provision.status)) {
@@ -6388,7 +6746,7 @@ function WeixinSettingsTab({ rpcCall }) {
6388
6746
  setBusy(false);
6389
6747
  }
6390
6748
  }, [announce, invoke, provision?.attemptId, provision?.status, scheduleAnimationFrame]);
6391
- const submitVerification = React11.useCallback(async (verifyCode) => {
6749
+ const submitVerification = React13.useCallback(async (verifyCode) => {
6392
6750
  if (!provision?.attemptId) return;
6393
6751
  setBusy(true);
6394
6752
  try {
@@ -6404,7 +6762,7 @@ function WeixinSettingsTab({ rpcCall }) {
6404
6762
  setBusy(false);
6405
6763
  }
6406
6764
  }, [announce, invoke, provision?.attemptId]);
6407
- React11.useEffect(() => {
6765
+ React13.useEffect(() => {
6408
6766
  const attemptId = provision?.attemptId;
6409
6767
  if (!attemptId || !["pending", "scanned", "connecting"].includes(provision.status)) return void 0;
6410
6768
  const controller = new AbortController();
@@ -6452,7 +6810,7 @@ function WeixinSettingsTab({ rpcCall }) {
6452
6810
  controller.abort();
6453
6811
  };
6454
6812
  }, [announce, invoke, loadStatus, provision?.attemptId, provision?.status, provision?.pollIntervalMs]);
6455
- const setBotBusy = React11.useCallback((botId, value) => {
6813
+ const setBotBusy = React13.useCallback((botId, value) => {
6456
6814
  setBusyByBot((current) => {
6457
6815
  const next = { ...current };
6458
6816
  if (value) next[botId] = value;
@@ -6460,35 +6818,68 @@ function WeixinSettingsTab({ rpcCall }) {
6460
6818
  return next;
6461
6819
  });
6462
6820
  }, []);
6463
- const reconnect = React11.useCallback(async (account) => {
6821
+ const reconnect = React13.useCallback(async (account) => {
6822
+ const snapshotVersion = workspaceFence.beginMutation();
6464
6823
  setBotBusy(account.botId, "reconnect");
6465
6824
  try {
6466
6825
  const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId }));
6467
- 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
+ }
6468
6829
  const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
6469
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");
6470
6831
  } catch (error) {
6471
6832
  announce(`\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF1A${presentError8(error).message}`);
6472
6833
  } finally {
6834
+ const shouldRefresh = workspaceFence.endMutation();
6835
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
6473
6836
  setBotBusy(account.botId, null);
6474
6837
  }
6475
- }, [announce, invoke, setBotBusy]);
6476
- 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();
6477
6864
  setBotBusy(account.botId, "delete");
6478
6865
  try {
6479
6866
  const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.deleteBot, {
6480
6867
  botId: account.botId,
6481
6868
  confirm: true
6482
6869
  }));
6483
- 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
+ }
6484
6873
  setRemoveTarget(null);
6485
6874
  announce("\u5FAE\u4FE1\u8D26\u53F7\u53CA\u672C\u673A\u51ED\u636E\u5DF2\u79FB\u9664\u3002");
6486
6875
  } catch (error) {
6487
6876
  announce(`\u79FB\u9664\u5931\u8D25\uFF1A${presentError8(error).message}`);
6488
6877
  } finally {
6878
+ const shouldRefresh = workspaceFence.endMutation();
6879
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
6489
6880
  setBotBusy(account.botId, null);
6490
6881
  }
6491
- }, [announce, invoke, setBotBusy]);
6882
+ }, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
6492
6883
  let provisionView = null;
6493
6884
  if (provision?.status === "starting") {
6494
6885
  provisionView = h2(ProgressPanel2, { busy });
@@ -6544,7 +6935,7 @@ function WeixinSettingsTab({ rpcCall }) {
6544
6935
  h2(Button11, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
6545
6936
  )
6546
6937
  ) : h2(
6547
- React11.Fragment,
6938
+ React13.Fragment,
6548
6939
  null,
6549
6940
  provisionView,
6550
6941
  model.bots.length === 0 && !provision ? h2(EmptyView5, { onStart: () => void startProvisioning(), busy }) : null,
@@ -6553,6 +6944,7 @@ function WeixinSettingsTab({ rpcCall }) {
6553
6944
  busyByBot,
6554
6945
  removeTarget,
6555
6946
  onReconnect: (account) => void reconnect(account),
6947
+ onWorkspaceSave: saveWorkspace,
6556
6948
  onRequestRemove: (account) => setRemoveTarget(account.botId),
6557
6949
  onConfirmRemove: (account) => void remove(account),
6558
6950
  onCancelRemove: () => setRemoveTarget(null)
@@ -6569,7 +6961,8 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
6569
6961
  pollProvisioning: "provision.poll",
6570
6962
  cancelProvisioning: "provision.cancel",
6571
6963
  reconnectBot: "bot.reconnect",
6572
- deleteBot: "bot.delete"
6964
+ deleteBot: "bot.delete",
6965
+ setWorkspace: "bot.workspace.set"
6573
6966
  });
6574
6967
  var PROVISION_STATES5 = /* @__PURE__ */ new Set(["starting", "pending", "connecting", "connected", "failed", "cancelled"]);
6575
6968
  var BOT_STATES = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
@@ -6633,6 +7026,7 @@ function normalizeBot6(value) {
6633
7026
  botId: id4(value.botId),
6634
7027
  connected,
6635
7028
  state: connected ? "connected" : state,
7029
+ workspace: text4(value.workspace, "", 4096),
6636
7030
  bot: {
6637
7031
  name: text4(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
6638
7032
  idMasked: text4(value.bot?.idMasked, "WhatsApp\u8D26\u53F7", 140)
@@ -6672,7 +7066,7 @@ function formatRemaining6(milliseconds) {
6672
7066
  }
6673
7067
 
6674
7068
  // plugin-src/client/channels/whatsapp/index.js
6675
- var React12 = __toESM(require("react"), 1);
7069
+ var React14 = __toESM(require("react"), 1);
6676
7070
 
6677
7071
  // plugin-src/client/channels/whatsapp/styles.js
6678
7072
  var WHATSAPP_STYLE_ID = "xmanrui-dsh-im-whatsapp-settings";
@@ -6697,7 +7091,7 @@ function installWhatsappStyles() {
6697
7091
 
6698
7092
  // plugin-src/client/channels/whatsapp/index.js
6699
7093
  var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]);
6700
- 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) {
6701
7095
  return h2("button", {
6702
7096
  ...props,
6703
7097
  ref,
@@ -6908,6 +7302,7 @@ function WhatsappAccountCard({
6908
7302
  busy,
6909
7303
  removing,
6910
7304
  onReconnect,
7305
+ onWorkspaceSave,
6911
7306
  onRequestRemove,
6912
7307
  onConfirmRemove,
6913
7308
  onCancelRemove
@@ -6962,6 +7357,11 @@ function WhatsappAccountCard({
6962
7357
  h2("dd", null, checkedTime6(account.health.lastCheckedAt))
6963
7358
  )
6964
7359
  ),
7360
+ h2(WorkspaceEditor, {
7361
+ workspace: account.workspace,
7362
+ disabled: Boolean(busy),
7363
+ onSave: onWorkspaceSave
7364
+ }),
6965
7365
  h2(
6966
7366
  "div",
6967
7367
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -6992,20 +7392,21 @@ function WhatsappAccountCard({
6992
7392
  );
6993
7393
  }
6994
7394
  function WhatsappSettingsTab({ rpcCall }) {
6995
- const [model, setModel] = React12.useState({
7395
+ const [model, setModel] = React14.useState({
6996
7396
  phase: "loading",
6997
7397
  bots: [],
6998
7398
  totals: { configured: 0, connected: 0 },
6999
7399
  error: null
7000
7400
  });
7001
- const [provision, setProvision] = React12.useState(null);
7002
- const [busy, setBusy] = React12.useState(false);
7003
- const [busyByBot, setBusyByBot] = React12.useState({});
7004
- const [removeTarget, setRemoveTarget] = React12.useState(null);
7005
- const [now, setNow] = React12.useState(Date.now());
7006
- const mounted = React12.useRef(true);
7007
- const addButtonRef = React12.useRef(null);
7008
- 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(() => {
7009
7410
  const disposeDingtalk = installDingtalkStyles();
7010
7411
  const disposeWhatsapp = installWhatsappStyles();
7011
7412
  mounted.current = true;
@@ -7015,15 +7416,17 @@ function WhatsappSettingsTab({ rpcCall }) {
7015
7416
  disposeDingtalk();
7016
7417
  };
7017
7418
  }, []);
7018
- const invoke = React12.useCallback(async (endpoint, payload = {}, signal) => {
7419
+ const invoke = React14.useCallback(async (endpoint, payload = {}, signal) => {
7019
7420
  if (typeof rpcCall !== "function") throw new TypeError("WhatsApp \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
7020
7421
  return unwrapRpcResult9(await rpcCall(endpoint, payload, signal));
7021
7422
  }, [rpcCall]);
7022
- 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;
7023
7426
  if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
7024
7427
  try {
7025
7428
  const snapshot = normalizeSnapshot8(await invoke(WHATSAPP_ENDPOINTS.status, {}, signal));
7026
- if (!mounted.current || signal?.aborted) return void 0;
7429
+ if (!mounted.current || signal?.aborted || !workspaceFence.canCommitStatus(workspaceVersion)) return void 0;
7027
7430
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
7028
7431
  if (restore && snapshot.provisioning) setProvision({
7029
7432
  ...snapshot.provisioning,
@@ -7031,7 +7434,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7031
7434
  });
7032
7435
  return snapshot;
7033
7436
  } catch (error) {
7034
- if (error?.name !== "AbortError" && mounted.current && !signal?.aborted) {
7437
+ if (error?.name !== "AbortError" && mounted.current && !signal?.aborted && workspaceFence.canCommitStatus(workspaceVersion)) {
7035
7438
  setModel((current) => ({
7036
7439
  ...current,
7037
7440
  phase: silent ? current.phase : "error",
@@ -7040,13 +7443,13 @@ function WhatsappSettingsTab({ rpcCall }) {
7040
7443
  }
7041
7444
  return void 0;
7042
7445
  }
7043
- }, [invoke]);
7044
- React12.useEffect(() => {
7446
+ }, [invoke, workspaceFence]);
7447
+ React14.useEffect(() => {
7045
7448
  const controller = new AbortController();
7046
7449
  void loadStatus({ signal: controller.signal, restore: true });
7047
7450
  return () => controller.abort();
7048
7451
  }, [loadStatus]);
7049
- React12.useEffect(() => {
7452
+ React14.useEffect(() => {
7050
7453
  if (model.phase !== "ready") return void 0;
7051
7454
  const controller = new AbortController();
7052
7455
  const timer = window.setInterval(
@@ -7058,12 +7461,12 @@ function WhatsappSettingsTab({ rpcCall }) {
7058
7461
  window.clearInterval(timer);
7059
7462
  };
7060
7463
  }, [loadStatus, model.phase]);
7061
- React12.useEffect(() => {
7464
+ React14.useEffect(() => {
7062
7465
  if (!provision || !ACTIVE_STATES3.has(provision.status)) return void 0;
7063
7466
  const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
7064
7467
  return () => window.clearInterval(timer);
7065
7468
  }, [provision?.attemptId, provision?.status]);
7066
- const startProvisioning = React12.useCallback(async (replace = false) => {
7469
+ const startProvisioning = React14.useCallback(async (replace = false) => {
7067
7470
  setBusy(true);
7068
7471
  try {
7069
7472
  if (replace && provision?.attemptId) {
@@ -7081,7 +7484,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7081
7484
  if (mounted.current) setBusy(false);
7082
7485
  }
7083
7486
  }, [invoke, provision?.attemptId]);
7084
- const closeProvision = React12.useCallback(async () => {
7487
+ const closeProvision = React14.useCallback(async () => {
7085
7488
  setBusy(true);
7086
7489
  try {
7087
7490
  if (provision?.attemptId && ACTIVE_STATES3.has(provision.status)) {
@@ -7092,7 +7495,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7092
7495
  if (mounted.current) setBusy(false);
7093
7496
  }
7094
7497
  }, [invoke, provision?.attemptId, provision?.status]);
7095
- React12.useEffect(() => {
7498
+ React14.useEffect(() => {
7096
7499
  const attemptId = provision?.attemptId;
7097
7500
  if (!attemptId || !ACTIVE_STATES3.has(provision.status)) return void 0;
7098
7501
  const controller = new AbortController();
@@ -7133,21 +7536,24 @@ function WhatsappSettingsTab({ rpcCall }) {
7133
7536
  if (timer) window.clearTimeout(timer);
7134
7537
  };
7135
7538
  }, [invoke, loadStatus, provision?.attemptId, provision?.status]);
7136
- const botAction = React12.useCallback(async (account, operation, endpoint, payload) => {
7539
+ const botAction = React14.useCallback(async (account, operation, endpoint, payload) => {
7540
+ const snapshotVersion = workspaceFence.beginMutation();
7137
7541
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
7138
7542
  try {
7139
7543
  const snapshot = normalizeSnapshot8(await invoke(endpoint, payload));
7140
- if (mounted.current) {
7544
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
7141
7545
  setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
7142
7546
  }
7143
7547
  } finally {
7548
+ const shouldRefresh = workspaceFence.endMutation();
7549
+ if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
7144
7550
  if (mounted.current) setBusyByBot((current) => {
7145
7551
  const next = { ...current };
7146
7552
  delete next[account.botId];
7147
7553
  return next;
7148
7554
  });
7149
7555
  }
7150
- }, [invoke]);
7556
+ }, [invoke, loadStatus, workspaceFence]);
7151
7557
  const botList = model.bots.length > 0 ? h2(
7152
7558
  "section",
7153
7559
  { className: "dim-listSection" },
@@ -7166,6 +7572,12 @@ function WhatsappSettingsTab({ rpcCall }) {
7166
7572
  WHATSAPP_ENDPOINTS.reconnectBot,
7167
7573
  { botId: account.botId }
7168
7574
  ),
7575
+ onWorkspaceSave: (workspace) => botAction(
7576
+ account,
7577
+ "workspace",
7578
+ WHATSAPP_ENDPOINTS.setWorkspace,
7579
+ { botId: account.botId, workspace }
7580
+ ),
7169
7581
  onRequestRemove: () => setRemoveTarget(account.botId),
7170
7582
  onCancelRemove: () => setRemoveTarget(null),
7171
7583
  onConfirmRemove: async () => {
@@ -7200,7 +7612,7 @@ function WhatsappSettingsTab({ rpcCall }) {
7200
7612
  h2(Button13, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
7201
7613
  )
7202
7614
  ) : h2(
7203
- React12.Fragment,
7615
+ React14.Fragment,
7204
7616
  null,
7205
7617
  provision?.status === "pending" ? h2(QrPanel5, {
7206
7618
  provision,
@@ -7232,8 +7644,17 @@ var CSS10 = String.raw`
7232
7644
  box-sizing: border-box;
7233
7645
  }
7234
7646
  .dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
7235
- .dim-title { margin: 0 0 26px; }
7236
- .dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; white-space: nowrap; }
7647
+ .dim-title { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; }
7648
+ .dim-brand { min-width: 0; display: flex; align-items: center; gap: 12px; }
7649
+ .dim-brandLogo { width: 48px; height: 48px; flex: 0 0 48px; display: block; object-fit: contain; filter: grayscale(1) contrast(1.08); }
7650
+ .dim-title p { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 20px; font-weight: 600; white-space: nowrap; }
7651
+ .dim-githubAction { position: relative; display: inline-flex; flex: none; }
7652
+ .dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
7653
+ .dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
7654
+ .dim-githubLink:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
7655
+ .dim-githubArrow { font-size: 13px; line-height: 1; }
7656
+ .dim-githubTooltip { position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 20; width: max-content; max-width: min(220px, 80vw); padding: 6px 9px; 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-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
7657
+ .dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
7237
7658
  .dim-layout { display: grid; grid-template-columns: 174px 1px minmax(0, 1fr); gap: 24px; align-items: start; }
7238
7659
  .dim-rail { max-height: 520px; display: grid; align-content: start; gap: 8px; overflow-y: auto; padding: 1px 4px 1px 1px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
7239
7660
  .dim-rail::-webkit-scrollbar { width: 4px; }
@@ -7278,7 +7699,7 @@ var CSS10 = String.raw`
7278
7699
  .dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
7279
7700
  .dim-panel .dim-credentialButton[aria-pressed="true"] { border-color: #4e5969; background: var(--dsw-alias-bg-module-platform, #f2f3f5); box-shadow: inset 0 0 0 1px rgb(78 89 105 / 8%); }
7280
7701
  .dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { min-height: 30px; display: inline-flex; align-items: center; justify-self: end; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
7281
- .dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap: 18px; padding: 0 0 24px; color: var(--dsw-alias-label-primary, #1f2329); box-sizing: border-box; }
7702
+ .dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap: 12px; padding: 0 0 24px; color: var(--dsw-alias-label-primary, #1f2329); box-sizing: border-box; }
7282
7703
  .dim-panel .dim-surfaceCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
7283
7704
  .dim-panel .dim-surfaceCard::before { display: none; }
7284
7705
  .dim-panel .dim-surfaceBody { padding: 24px; }
@@ -7294,9 +7715,9 @@ var CSS10 = String.raw`
7294
7715
  .dim-panel .dim-credentialError { margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.5; }
7295
7716
  .dim-panel .dim-credentialActions { margin-top: 0; }
7296
7717
  .dim-panel .dim-listSection { display: flex; flex-direction: column; gap: 0; }
7297
- .dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 2px 0 9px; padding: 0; }
7718
+ .dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 6px; padding: 0; }
7298
7719
  .dim-panel .dim-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
7299
- .dim-panel .dim-botList { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
7720
+ .dim-panel .dim-botList { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
7300
7721
  .dim-panel .dim-loadingView { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
7301
7722
  .dim-panel .dim-loadingView h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: normal; font-weight: 650; }
7302
7723
  .dim-panel .dim-loadingView p { margin: 0; line-height: 1.6; }
@@ -7344,7 +7765,20 @@ var CSS10 = String.raw`
7344
7765
  .dim-panel .dim-confirm { padding: 18px 24px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
7345
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; }
7346
7767
  .dim-panel .dim-confirm p { margin: 7px 0 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.6; }
7347
- .dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
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; }
7348
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; }
7349
7783
  .dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
7350
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; }
@@ -7352,7 +7786,7 @@ var CSS10 = String.raw`
7352
7786
  .dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
7353
7787
  .dim-panel .dim-botCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
7354
7788
  .dim-panel .dim-botCard::before { display: none; }
7355
- .dim-panel .dim-botCardBody { position: relative; padding: 16px; }
7789
+ .dim-panel .dim-botCardBody { position: relative; padding: 14px; }
7356
7790
  .dim-panel .dim-botCardTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
7357
7791
  .dim-panel .dim-botIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; }
7358
7792
  .dim-panel .dim-botAvatar { flex: none; width: 42px; height: 42px; display: grid; place-items: center; overflow: hidden; border-radius: 12px; box-shadow: none; }
@@ -7365,8 +7799,8 @@ var CSS10 = String.raw`
7365
7799
  .dim-panel .dim-botCard .dim-healthDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
7366
7800
  .dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
7367
7801
  .dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
7368
- .dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 12px 0; }
7369
- .dim-panel .dim-botMetric { min-width: 0; padding: 10px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
7802
+ .dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 8px 0; }
7803
+ .dim-panel .dim-botMetric { min-width: 0; padding: 8px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
7370
7804
  .dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
7371
7805
  .dim-panel .dim-botMetric dd { overflow: hidden; margin: 5px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
7372
7806
  .dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
@@ -7403,6 +7837,7 @@ var CSS10 = String.raw`
7403
7837
  @media (max-width: 560px) {
7404
7838
  .dim-title { flex-direction: column; gap: 10px; }
7405
7839
  .dim-title p { white-space: normal; }
7840
+ .dim-githubTooltip { right: auto; left: 0; }
7406
7841
  .dim-rail { grid-template-columns: minmax(0, 1fr); }
7407
7842
  }
7408
7843
  @media (prefers-reduced-motion: reduce) {
@@ -7426,6 +7861,7 @@ function installImStyles() {
7426
7861
  // plugin-src/client/index.js
7427
7862
  var name = "im-settings";
7428
7863
  var inject = ["slots", "connection", "locale"];
7864
+ var IM_PLUGIN_LOGO_URL = "data:image/png;base64,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";
7429
7865
  var CHANNELS = Object.freeze([
7430
7866
  { id: "weixin", label: "\u5FAE\u4FE1" },
7431
7867
  { id: "feishu", label: "\u98DE\u4E66" },
@@ -7514,7 +7950,8 @@ function IMSettingsTab({
7514
7950
  weixinRpcCall,
7515
7951
  whatsappRpcCall
7516
7952
  }) {
7517
- const [selected, setSelected] = React13.useState("weixin");
7953
+ const [selected, setSelected] = React15.useState("weixin");
7954
+ const githubTooltipId = React15.useId();
7518
7955
  const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
7519
7956
  return h2(
7520
7957
  "section",
@@ -7522,7 +7959,40 @@ function IMSettingsTab({
7522
7959
  h2(
7523
7960
  "header",
7524
7961
  { className: "dim-title" },
7525
- h2("p", null, "\u628A\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness")
7962
+ h2(
7963
+ "div",
7964
+ { className: "dim-brand" },
7965
+ h2("img", {
7966
+ className: "dim-brandLogo",
7967
+ src: IM_PLUGIN_LOGO_URL,
7968
+ alt: "dsh-im",
7969
+ width: 48,
7970
+ height: 48
7971
+ }),
7972
+ h2("p", null, "\u8BA9\u804A\u5929\u673A\u5668\u4EBA\u8F7B\u677E\u63A5\u5165 DeepSeek Harness")
7973
+ ),
7974
+ h2(
7975
+ "span",
7976
+ { className: "dim-githubAction" },
7977
+ h2(
7978
+ "a",
7979
+ {
7980
+ className: "dim-githubLink",
7981
+ href: "https://github.com/xmanrui/dsh-im",
7982
+ target: "_blank",
7983
+ rel: "noopener noreferrer",
7984
+ "aria-label": "dsh-im GitHub",
7985
+ "aria-describedby": githubTooltipId
7986
+ },
7987
+ h2("span", null, "GitHub"),
7988
+ h2("span", { className: "dim-githubArrow", "aria-hidden": "true" }, "\u2197")
7989
+ ),
7990
+ h2("span", {
7991
+ id: githubTooltipId,
7992
+ className: "dim-githubTooltip",
7993
+ role: "tooltip"
7994
+ }, "\u5E2E\u52A9\u4E0E\u53CD\u9988 \xB7 \u524D\u5F80 GitHub")
7995
+ )
7526
7996
  ),
7527
7997
  h2(
7528
7998
  "div",