@xmanrui/dsh-im 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.en.md +5 -3
  2. package/README.md +5 -3
  3. package/lib/client.js +212 -0
  4. package/lib/index.js +166 -163
  5. package/package.json +1 -1
  6. package/plugin-src/client/channels/whatsapp/api.js +11 -0
  7. package/plugin-src/client/channels/whatsapp/index.js +125 -0
  8. package/plugin-src/client/channels/whatsapp/styles.js +25 -0
  9. package/plugin-src/client/i18n.js +18 -0
  10. package/plugin-src/host/channels/dingtalk/production.mjs +3 -1
  11. package/plugin-src/host/channels/feishu/production.mjs +3 -1
  12. package/plugin-src/host/channels/qq/production.mjs +3 -1
  13. package/plugin-src/host/channels/shared/production.mjs +3 -1
  14. package/plugin-src/host/channels/slack/production.mjs +3 -1
  15. package/plugin-src/host/channels/wecom/production.mjs +3 -1
  16. package/plugin-src/host/channels/weixin/production.mjs +3 -1
  17. package/plugin-src/host/channels/whatsapp/production.mjs +3 -1
  18. package/plugin-src/host/channels/whatsapp/rpc.mjs +19 -1
  19. package/plugin-src/host/harness-session-coordinator.mjs +32 -5
  20. package/src/channels/dingtalk/dingtalk-api.mjs +93 -27
  21. package/src/channels/dingtalk/dingtalk-bridge.mjs +60 -13
  22. package/src/channels/discord/discord-runtime.mjs +23 -0
  23. package/src/channels/feishu/bridge.mjs +18 -10
  24. package/src/channels/feishu/message-utils.mjs +47 -0
  25. package/src/channels/qq/qq-bridge.mjs +80 -28
  26. package/src/channels/shared/file-download.mjs +64 -0
  27. package/src/channels/shared/harness-client.mjs +45 -0
  28. package/src/channels/shared/inbound-file.mjs +206 -0
  29. package/src/channels/shared/text-harness-bridge.mjs +31 -11
  30. package/src/channels/slack/slack-api.mjs +27 -4
  31. package/src/channels/slack/slack-runtime.mjs +55 -5
  32. package/src/channels/telegram/telegram-api.mjs +21 -6
  33. package/src/channels/telegram/telegram-runtime.mjs +24 -3
  34. package/src/channels/wecom/wecom-bridge.mjs +73 -10
  35. package/src/channels/weixin/weixin-api.mjs +45 -0
  36. package/src/channels/weixin/weixin-bridge.mjs +52 -18
  37. package/src/channels/whatsapp/config-store.mjs +43 -0
  38. package/src/channels/whatsapp/whatsapp-controller.mjs +22 -1
  39. package/src/channels/whatsapp/whatsapp-runtime.mjs +83 -2
  40. package/src/channels/whatsapp/whatsapp-web-session.mjs +1 -1
package/README.en.md CHANGED
@@ -53,7 +53,7 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
53
53
  | Slack | Create an app from the bundled App Manifest, then enter a Bot Token (`xoxb-`) and App Token (`xapp-`) | Socket Mode connection; direct DM replies, mention-only channel replies, and preferred native streaming API |
54
54
  | Telegram | Enter a Bot Token generated by @BotFather | Bot API long polling; DMs work by default and groups respond to mentions or replies, while each bot can optionally enable a private-DM allowlist; streaming uses message edits |
55
55
  | Discord | Enter a Bot Token generated in the Developer Portal | Gateway v10 connection; direct DM replies, mention-only server replies, and streaming through message edits |
56
- | WhatsApp | Scan a QR code with mobile WhatsApp to link a device | WhatsApp Web connection; read receipt and typing indicator followed by the final answer |
56
+ | WhatsApp | Scan a QR code with mobile WhatsApp to link a device | WhatsApp Web connection; self-chat only by default, with optional selected-contact and open-response modes; read receipt and typing indicator followed by the final answer |
57
57
 
58
58
  Other IM platforms can be added through the same channel-adapter structure.
59
59
 
@@ -116,6 +116,8 @@ If this machine must use a forward proxy to reach Feishu, set `HTTPS_PROXY` to a
116
116
 
117
117
  Each Telegram bot has its own access-mode control on its bot card. Existing and newly connected bots both default to **Compatible mode**: DMs receive replies, while group messages require a mention of or reply to the bot. Restrictions apply only after explicitly switching that bot to **Safe mode (private-chat allowlist)**. Safe mode ignores every group message and admits only numeric User IDs in that bot's allowlist. Enter one ID per line. Switching back to Compatible mode retains the allowlist without enforcing it, so it is available when Safe mode is enabled again. An empty allowlist in Safe mode rejects all inbound messages for that bot.
118
118
 
119
+ Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only me**, which is also the default for newly linked bots and accepts only self-chat messages from the linked account. **Selected contacts** additionally accepts direct messages from allowlisted phone numbers and ignores groups. Enter one number with its country or region code per line; a leading `+` is optional. **Open responses** preserves the previous behavior: all direct messages are accepted, together with group mentions or replies. Switching modes retains the allowlist. An empty Selected contacts allowlist behaves like Only me, and rejected messages are ignored silently.
120
+
119
121
  ## Bot commands
120
122
 
121
123
  | Command | Description |
@@ -166,7 +168,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
166
168
  - `/session` accepts exactly one Session ID obtained from `/sessionlist`. It neither creates a session nor immediately prompts the model; later messages in the current chat continue the bound session. Regular archived sessions can be bound without being unarchived, while subagent sessions cannot be bound.
167
169
  - `/session` locates the session's unique workspace automatically. Binding inside the current workspace replaces only this chat's mapping. A cross-workspace binding switches the bot workspace, clears the old session mappings for all of that bot's chats, and then binds this chat, so it affects the bot's other chats. A reply already being generated may still finish.
168
170
  - Workspace switches and session bindings only clear or replace dsh-im chat mappings. They never delete, empty, or archive old Session contents; an old Session can still be listed and bound again.
169
- - Any user within the platform bot's visibility scope who can normally message it can run these commands; there is no separate administrator role. Telegram Compatible mode follows the original DM and group mention/reply rules. Safe mode admits only private users in that bot's allowlist and always ignores group commands.
171
+ - Any user admitted by the current channel access policy can run these commands; there is no separate administrator role. Telegram Compatible mode follows the original DM and group mention/reply rules, while Safe mode admits only allowlisted private users. WhatsApp Only me accepts self-chat only, Selected contacts accepts self-chat plus allowlisted direct messages, and Open responses accepts every direct message plus group mentions or replies.
170
172
  - Agent Preset names and IDs come from the same Harness Host, and any command-authorized user can change the Preset used by all future new Sessions across this bot's chats. Expose `/presetlist` and `/preset` only to trusted users.
171
173
  - The list comes from the Harness Host's global registry and can include local absolute paths for other bots, other channels, or non-IM projects. Restrict the bot's visibility to trusted users.
172
174
  - Session results also come from the global Harness Host. Session IDs and titles can belong to other bots, other channels, or non-IM projects, and may contain sensitive metadata. Enable these commands only when every user in the bot's visibility scope is trusted.
@@ -191,7 +193,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
191
193
  - Follows the DeepSeek Harness language preference and switches the settings UI live between Chinese and English.
192
194
  - Uses logos for WeChat, Feishu, DingTalk, WeCom, QQ, Slack, Telegram, Discord, WhatsApp, and AI Office navigation without enable/disable switches.
193
195
  - Keeps RPC endpoints, credentials, connection supervision, and session mappings isolated by IM channel; the Office Connector separately owns Device credentials, Job leases, approval waits, and concurrency limits.
194
- - Returns only QR codes, the public Slack Manifest, redacted status data, and the access mode and allowlist User IDs that the user explicitly saved for the current Telegram bot. Manually entered secrets and Tokens travel one way to the local Host; no RPC response returns App Secrets, `bot_token`, DingTalk `client_secret`, WeCom Secrets, QQ `app_secret`, Slack Bot/App Tokens, Telegram/Discord Bot Tokens, WhatsApp linked-device keys, AI Office Device Tokens, or other raw user identifiers observed from platform messages.
196
+ - Returns only QR codes, the public Slack Manifest, redacted status data, and access modes or allowlist identifiers explicitly saved for the current Telegram or WhatsApp bot. Manually entered secrets and Tokens travel one way to the local Host; no RPC response returns App Secrets, `bot_token`, DingTalk `client_secret`, WeCom Secrets, QQ `app_secret`, Slack Bot/App Tokens, Telegram/Discord Bot Tokens, WhatsApp linked-device keys, AI Office Device Tokens, or other raw user identifiers observed from platform messages.
195
197
 
196
198
  ## Local development
197
199
 
package/README.md CHANGED
@@ -56,7 +56,7 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
56
56
  | Slack | 使用预置 App Manifest 创建应用,再填写 Bot Token(`xoxb-`)和 App Token(`xapp-`) | Socket Mode 长连接;私聊直接回复,频道被 @ 后响应,优先使用官方流式消息 API |
57
57
  | Telegram | 使用 @BotFather 生成的 Bot Token | Bot API 长轮询;默认私聊直接响应、群聊被提及或回复时响应,也可为每个机器人独立启用私聊白名单安全模式;通过编辑消息流式显示回答 |
58
58
  | Discord | 使用 Developer Portal 生成的 Bot Token | Gateway v10 长连接;私信直接回复,服务器频道被提及时响应,通过编辑消息流式显示回答 |
59
- | WhatsApp | 使用手机 WhatsApp 扫码关联设备 | WhatsApp Web 长连接;显示已读和“正在输入”,再发送最终回答 |
59
+ | WhatsApp | 使用手机 WhatsApp 扫码关联设备 | WhatsApp Web 长连接;默认仅响应账号自聊,也可切换到指定联系人或开放响应模式;显示已读和“正在输入”,再发送最终回答 |
60
60
 
61
61
  其他 IM 平台可继续按同一渠道适配器结构接入。
62
62
 
@@ -119,6 +119,8 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
119
119
 
120
120
  每个 Telegram 机器人都可以在自己的卡片中切换访问模式。旧机器人和新接入机器人均默认使用**兼容模式**:私聊直接响应,群聊仅在提及机器人或回复机器人消息时响应。只有主动切换到**安全模式(私聊白名单)**后,机器人才会忽略全部群聊,并只接受该机器人白名单中的数字 User ID。白名单每行一个 ID、按机器人独立保存;切回兼容模式时会保留但不使用,再切回安全模式即可继续使用。安全模式的空白名单会拒绝该机器人的所有入站消息。
121
121
 
122
+ 每个 WhatsApp 机器人也有独立的访问模式。旧机器人升级后和新接入机器人都默认使用**仅自己模式**,只响应已绑定账号的自聊消息。**指定联系人模式**额外接受白名单电话号码的私聊并忽略群聊;号码需包含国家或地区代码,每行一个,可带开头的 `+`。**开放响应模式**保留原有行为:响应所有私聊,以及群聊中的提及或回复。切换模式会保留白名单;指定联系人模式的空白名单等同于仅自己模式。未授权消息会被静默忽略。
123
+
122
124
  ## 机器人命令
123
125
 
124
126
  | 命令 | 作用 |
@@ -169,7 +171,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
169
171
  - `/session` 只接受一个由 `/sessionlist` 获得的 Session ID。它不会新建会话或立即向模型发送消息;绑定成功后,当前聊天的后续消息会继续该会话。普通归档会话可以绑定但不会自动取消归档,子代理会话不能绑定。
170
172
  - `/session` 会自动定位会话唯一所属的工作区。同工作区绑定只替换当前聊天的映射;跨工作区绑定会切换该机器人的工作区、清除该机器人所有聊天的旧会话映射,再绑定当前聊天,因此会影响该机器人的其他聊天。已经开始生成的回复仍可完成。
171
173
  - 工作区切换和会话绑定只会清除或替换 dsh-im 的聊天映射,不会删除、清空或归档任何旧 Session 内容;旧 Session 仍可再次列出和绑定。
172
- - 任何已在对应平台可见范围内、能够正常向机器人发消息的用户都可以执行这些命令,不区分管理员和普通用户。Telegram 兼容模式遵循原有私聊及群聊提及/回复规则;安全模式只允许当前机器人白名单中的私聊用户执行,群聊命令始终忽略。
174
+ - 任何通过当前渠道访问策略的用户都可以执行这些命令,不另行区分管理员和普通用户。Telegram 兼容模式遵循原有私聊及群聊提及/回复规则;安全模式只允许当前机器人白名单中的私聊用户执行。WhatsApp 仅自己模式只接受自聊,指定联系人模式接受自聊和白名单私聊,开放响应模式接受所有私聊及群聊中的提及或回复。
173
175
  - Agent Preset 名称和 ID 来自同一个 Harness Host,且任何有命令权限的用户都能修改该机器人所有聊天未来新 Session 的 Preset;请只向可信用户开放 `/presetlist` 和 `/preset`。
174
176
  - 工作区列表来自 Harness Host 的全局登记信息,可能包含其他机器人、其他渠道或非 IM 项目的本机绝对路径。请将机器人可见范围限制给可信用户。
175
177
  - 会话列表同样来自该全局 Harness Host;会话 ID 和标题可能属于其他机器人、其他渠道或非 IM 项目,并可能包含敏感元数据。开放命令前请确保所有可见用户都可信。
@@ -194,7 +196,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
194
196
  - 设置页跟随 DeepSeek Harness 的语言选择,在中文和 English 之间即时切换;
195
197
  - 左侧使用 Logo 切换微信、飞书、钉钉、企业微信、QQ、Slack、Telegram、Discord、WhatsApp 和 AI Office,不使用启用/停用开关;
196
198
  - 九个 IM 渠道保持独立的 RPC、凭据、连接监督和会话映射;Office Connector 另行维护设备凭据、Job 租约、审批等待与并发上限;
197
- - 浏览器只获得二维码、Manifest、脱敏状态,以及用户为当前 Telegram 机器人主动保存的访问模式和白名单 User ID;手动输入的 Secret 或 Token 仅单向提交给本机 Host,任何 RPC 响应都不会返回 App Secret、`bot_token`、钉钉 `client_secret`、企业微信 Secret、QQ `app_secret`、Slack Bot/App Token、Telegram/Discord Bot Token、WhatsApp 关联设备密钥、AI Office Device Token,或从平台消息中观察到的其他原始用户标识。
199
+ - 浏览器只获得二维码、Manifest、脱敏状态,以及用户为当前 Telegram WhatsApp 机器人主动保存的访问模式和白名单标识;手动输入的 Secret 或 Token 仅单向提交给本机 Host,任何 RPC 响应都不会返回 App Secret、`bot_token`、钉钉 `client_secret`、企业微信 Secret、QQ `app_secret`、Slack Bot/App Token、Telegram/Discord Bot Token、WhatsApp 关联设备密钥、AI Office Device Token,或从平台消息中观察到的其他原始用户标识。
198
200
 
199
201
  ## 本地开发
200
202
 
package/lib/client.js CHANGED
@@ -550,6 +550,24 @@ var EN = Object.freeze({
550
550
  "\u6B63\u5728\u5EFA\u7ACB\u5B89\u5168\u7684\u5173\u8054\u8BBE\u5907\u4F1A\u8BDD\u3002": "Creating a secure linked-device session.",
551
551
  "\u5173\u8054\u8BBE\u5907\u6B63\u5728\u63A5\u5165 DeepSeek Harness\u3002": "Linking the device to DeepSeek Harness.",
552
552
  "WhatsApp Web \u5173\u8054\u8BBE\u5907\u8FD0\u884C\u6B63\u5E38": "WhatsApp linked device is healthy",
553
+ "\u67E5\u770B WhatsApp \u8BBF\u95EE\u6A21\u5F0F\u8BF4\u660E": "View WhatsApp access mode details",
554
+ "WhatsApp \u8BBF\u95EE\u6A21\u5F0F": "WhatsApp access mode",
555
+ "\u4EC5\u81EA\u5DF1\u6A21\u5F0F": "Only me",
556
+ "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F": "Selected contacts",
557
+ "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F": "Open responses",
558
+ "\u4EC5\u81EA\u5DF1\u6A21\u5F0F\uFF08\u9ED8\u8BA4\uFF09": "Only me (default)",
559
+ "\u5DF2\u751F\u6548\uFF1A": "Active: ",
560
+ "\u53EA\u54CD\u5E94\u5DF2\u7ED1\u5B9A WhatsApp \u8D26\u53F7\u7684\u81EA\u804A\u6D88\u606F\u3002": "Only respond to self-chat messages from the linked WhatsApp account.",
561
+ "\u54CD\u5E94\u81EA\u804A\u548C\u767D\u540D\u5355\u8054\u7CFB\u4EBA\u7684\u79C1\u804A\uFF0C\u5FFD\u7565\u7FA4\u804A\u3002": "Respond to self-chat and allowlisted direct messages; ignore group messages.",
562
+ "\u54CD\u5E94\u6240\u6709\u79C1\u804A\uFF0C\u4EE5\u53CA\u7FA4\u804A\u4E2D\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002": "Respond to all direct messages and to group mentions or replies.",
563
+ "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801": "WhatsApp phone numbers allowed to send direct messages",
564
+ "\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801": "One number with country or region code per line",
565
+ "\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002": "A leading + is allowed and removed when saved.",
566
+ "\u4EC5\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F\u4F7F\u7528\u767D\u540D\u5355\uFF0C\u5207\u6362\u6A21\u5F0F\u65F6\u4F1A\u4FDD\u7559\u3002": "Only Selected contacts uses the allowlist; it is retained when modes change.",
567
+ "\u767D\u540D\u5355\u4E3A\u7A7A\uFF1B\u4FDD\u5B58\u540E\u5C06\u53EA\u63A5\u53D7\u81EA\u804A\u6D88\u606F\u3002": "The allowlist is empty; only self-chat messages will be accepted after saving.",
568
+ "\u7535\u8BDD\u53F7\u7801\u5FC5\u987B\u5305\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\uFF0C\u6BCF\u884C\u4E00\u4E2A\u3002": "Each phone number must include a country or region code on its own line.",
569
+ "WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002": "WhatsApp access settings are currently unavailable.",
570
+ "WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\u3002": "Could not save WhatsApp access settings.",
553
571
  "Bot API \u957F\u8F6E\u8BE2": "Bot API long polling",
554
572
  " Gateway \u957F\u8FDE\u63A5": " Gateway persistent connection",
555
573
  "Gateway \u957F\u8FDE\u63A5": "Gateway persistent connection",
@@ -9075,6 +9093,7 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
9075
9093
  cancelProvisioning: "provision.cancel",
9076
9094
  reconnectBot: "bot.reconnect",
9077
9095
  deleteBot: "bot.delete",
9096
+ setAccessPolicy: "bot.access-policy.set",
9078
9097
  setWorkspace: "bot.workspace.set",
9079
9098
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT
9080
9099
  });
@@ -9142,6 +9161,12 @@ function normalizeBot6(value) {
9142
9161
  state: connected ? "connected" : state,
9143
9162
  workspace: text4(value.workspace, "", 4096),
9144
9163
  agentPreset: normalizeAgentPresetId(value.agentPreset),
9164
+ accessPolicy: {
9165
+ accessMode: ["self-only", "private-allowlist", "open"].includes(
9166
+ value.accessPolicy?.accessMode
9167
+ ) ? value.accessPolicy.accessMode : "self-only",
9168
+ allowedNumbers: Array.isArray(value.accessPolicy?.allowedNumbers) ? [...new Set(value.accessPolicy.allowedNumbers.filter((entry) => typeof entry === "string" && /^[1-9]\d{4,14}$/.test(entry)))] : []
9169
+ },
9145
9170
  bot: {
9146
9171
  name: text4(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
9147
9172
  idMasked: text4(value.bot?.idMasked, "WhatsApp\u8D26\u53F7", 140)
@@ -9190,6 +9215,31 @@ var CSS10 = String.raw`
9190
9215
  .dwa-page { --ddt-accent: #25d366; --ddt-accent-deep: #128c7e; --ddt-accent-wash: #eafbf0; }
9191
9216
  .dwa-avatar { color: #fff; background: #25d366; }
9192
9217
  .dwa-avatar svg { display: block; }
9218
+ .dwa-access { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
9219
+ .dwa-accessHeading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
9220
+ .dwa-accessHeading > strong { font-size: 13px; }
9221
+ .dwa-accessStatus { min-width: 0; display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
9222
+ .dwa-accessBadge { flex: none; padding: 3px 8px; border-radius: 999px; color: #08785f; background: #eafbf0; font-size: 11px; font-weight: 700; }
9223
+ .dwa-accessBadge[data-mode="private-allowlist"] { color: #0f6f8f; background: #eaf7fd; }
9224
+ .dwa-accessBadge[data-mode="open"] { color: #a15c00; background: #fff3d6; }
9225
+ .dwa-accessHelp { position: relative; display: inline-flex; flex: none; }
9226
+ .dwa-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #25d366 34%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #128c7e; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; }
9227
+ .dwa-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 270px; max-width: min(290px, calc(100vw - 48px)); display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
9228
+ .dwa-accessTooltipItem { display: grid; gap: 2px; }
9229
+ .dwa-accessTooltipItem + .dwa-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
9230
+ .dwa-accessTooltipItem strong { font-size: 12px; line-height: 17px; }
9231
+ .dwa-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
9232
+ .dwa-accessHelp:hover .dwa-accessTooltip, .dwa-accessHelp:focus-within .dwa-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
9233
+ .dwa-accessField { display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
9234
+ .dwa-accessField select, .dwa-accessField textarea { width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
9235
+ .dwa-accessField select { height: 34px; padding: 0 9px; }
9236
+ .dwa-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
9237
+ .dwa-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
9238
+ .dwa-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
9239
+ .dwa-accessWarning, .dwa-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
9240
+ .dwa-accessWarning { color: #a15c00; }
9241
+ .dwa-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
9242
+ .dwa-accessActions { display: flex; justify-content: flex-end; }
9193
9243
  `;
9194
9244
  function installWhatsappStyles() {
9195
9245
  if (typeof document === "undefined") return () => {
@@ -9207,6 +9257,156 @@ function installWhatsappStyles() {
9207
9257
 
9208
9258
  // plugin-src/client/channels/whatsapp/index.js
9209
9259
  var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]);
9260
+ function accessPolicyFor(account) {
9261
+ const accessMode = ["self-only", "private-allowlist", "open"].includes(
9262
+ account?.accessPolicy?.accessMode
9263
+ ) ? account.accessPolicy.accessMode : "self-only";
9264
+ return {
9265
+ accessMode,
9266
+ allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers) ? account.accessPolicy.allowedNumbers : []
9267
+ };
9268
+ }
9269
+ function allowedNumbersFromText(value) {
9270
+ const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
9271
+ const normalized = entries.map((entry) => entry.replace(/^\+/, ""));
9272
+ if (normalized.some((entry) => !/^[1-9]\d{4,14}$/.test(entry))) {
9273
+ throw new TypeError("\u7535\u8BDD\u53F7\u7801\u5FC5\u987B\u5305\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\uFF0C\u6BCF\u884C\u4E00\u4E2A\u3002");
9274
+ }
9275
+ return [...new Set(normalized)];
9276
+ }
9277
+ function WhatsappAccessSettings({ account, busy = false, onSave }) {
9278
+ const policy = accessPolicyFor(account);
9279
+ const sourceNumbers = policy.allowedNumbers.join("\n");
9280
+ const helpId = React19.useId();
9281
+ const [accessMode, setAccessMode] = React19.useState(policy.accessMode);
9282
+ const [allowedNumbers, setAllowedNumbers] = React19.useState(sourceNumbers);
9283
+ const [error, setError] = React19.useState(null);
9284
+ React19.useEffect(() => {
9285
+ setAccessMode(policy.accessMode);
9286
+ setAllowedNumbers(sourceNumbers);
9287
+ setError(null);
9288
+ }, [policy.accessMode, sourceNumbers]);
9289
+ const save = async (event) => {
9290
+ event.preventDefault();
9291
+ setError(null);
9292
+ try {
9293
+ const normalized = allowedNumbersFromText(allowedNumbers);
9294
+ if (typeof onSave !== "function") throw new Error("WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
9295
+ await onSave({ accessMode, allowedNumbers: normalized });
9296
+ } catch (caught) {
9297
+ setError(caught?.message ?? "WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\u3002");
9298
+ }
9299
+ };
9300
+ const allowlistEnabled = accessMode === "private-allowlist";
9301
+ const labels = {
9302
+ "self-only": "\u4EC5\u81EA\u5DF1\u6A21\u5F0F",
9303
+ "private-allowlist": "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F",
9304
+ open: "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F"
9305
+ };
9306
+ return h2(
9307
+ "form",
9308
+ { className: "dwa-access", onSubmit: save },
9309
+ h2(
9310
+ "div",
9311
+ { className: "dwa-accessHeading" },
9312
+ h2("strong", null, "\u8BBF\u95EE\u8BBE\u7F6E"),
9313
+ h2(
9314
+ "span",
9315
+ { className: "dwa-accessStatus" },
9316
+ h2(
9317
+ "span",
9318
+ { className: "dwa-accessBadge", "data-mode": policy.accessMode },
9319
+ ["\u5DF2\u751F\u6548\uFF1A", labels[policy.accessMode]]
9320
+ ),
9321
+ h2(
9322
+ "span",
9323
+ { className: "dwa-accessHelp" },
9324
+ h2("button", {
9325
+ type: "button",
9326
+ className: "dwa-accessHelpButton",
9327
+ "aria-label": "\u67E5\u770B WhatsApp \u8BBF\u95EE\u6A21\u5F0F\u8BF4\u660E",
9328
+ "aria-describedby": helpId
9329
+ }, h2("span", { "aria-hidden": "true" }, "?")),
9330
+ h2(
9331
+ "span",
9332
+ { id: helpId, className: "dwa-accessTooltip", role: "tooltip" },
9333
+ h2(
9334
+ "span",
9335
+ { className: "dwa-accessTooltipItem" },
9336
+ h2("strong", null, "\u4EC5\u81EA\u5DF1\u6A21\u5F0F"),
9337
+ h2("span", null, "\u53EA\u54CD\u5E94\u5DF2\u7ED1\u5B9A WhatsApp \u8D26\u53F7\u7684\u81EA\u804A\u6D88\u606F\u3002")
9338
+ ),
9339
+ h2(
9340
+ "span",
9341
+ { className: "dwa-accessTooltipItem" },
9342
+ h2("strong", null, "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F"),
9343
+ h2("span", null, "\u54CD\u5E94\u81EA\u804A\u548C\u767D\u540D\u5355\u8054\u7CFB\u4EBA\u7684\u79C1\u804A\uFF0C\u5FFD\u7565\u7FA4\u804A\u3002")
9344
+ ),
9345
+ h2(
9346
+ "span",
9347
+ { className: "dwa-accessTooltipItem" },
9348
+ h2("strong", null, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F"),
9349
+ h2("span", null, "\u54CD\u5E94\u6240\u6709\u79C1\u804A\uFF0C\u4EE5\u53CA\u7FA4\u804A\u4E2D\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002")
9350
+ )
9351
+ )
9352
+ )
9353
+ )
9354
+ ),
9355
+ h2(
9356
+ "label",
9357
+ { className: "dwa-accessField" },
9358
+ h2("span", null, "\u6A21\u5F0F"),
9359
+ h2(
9360
+ "select",
9361
+ {
9362
+ value: accessMode,
9363
+ disabled: busy,
9364
+ "aria-label": "WhatsApp \u8BBF\u95EE\u6A21\u5F0F",
9365
+ onChange: (event) => {
9366
+ setAccessMode(event.target.value);
9367
+ setError(null);
9368
+ }
9369
+ },
9370
+ h2("option", { value: "self-only" }, "\u4EC5\u81EA\u5DF1\u6A21\u5F0F\uFF08\u9ED8\u8BA4\uFF09"),
9371
+ h2("option", { value: "private-allowlist" }, "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F"),
9372
+ h2("option", { value: "open" }, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F")
9373
+ )
9374
+ ),
9375
+ allowlistEnabled ? h2(
9376
+ "label",
9377
+ { className: "dwa-accessField" },
9378
+ h2("span", null, "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801"),
9379
+ h2("textarea", {
9380
+ value: allowedNumbers,
9381
+ disabled: busy,
9382
+ rows: 3,
9383
+ placeholder: "\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801",
9384
+ "aria-label": "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801",
9385
+ onChange: (event) => {
9386
+ setAllowedNumbers(event.target.value);
9387
+ setError(null);
9388
+ }
9389
+ }),
9390
+ h2("small", null, "\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002")
9391
+ ) : null,
9392
+ allowlistEnabled && allowedNumbers.trim() === "" ? h2(
9393
+ "p",
9394
+ { className: "dwa-accessWarning", role: "status" },
9395
+ "\u767D\u540D\u5355\u4E3A\u7A7A\uFF1B\u4FDD\u5B58\u540E\u5C06\u53EA\u63A5\u53D7\u81EA\u804A\u6D88\u606F\u3002"
9396
+ ) : null,
9397
+ error ? h2("p", { className: "dwa-accessError", role: "alert" }, error) : null,
9398
+ h2(
9399
+ "div",
9400
+ { className: "dwa-accessActions" },
9401
+ h2("button", {
9402
+ type: "submit",
9403
+ className: "ddt-button",
9404
+ "data-kind": "secondary",
9405
+ disabled: busy
9406
+ }, busy ? "\u6B63\u5728\u4FDD\u5B58\u2026" : "\u4FDD\u5B58\u8BBF\u95EE\u8BBE\u7F6E")
9407
+ )
9408
+ );
9409
+ }
9210
9410
  var Button14 = React19.forwardRef(function Button15({ children, kind = "secondary", className = "", ...props }, ref) {
9211
9411
  return h2("button", {
9212
9412
  ...props,
@@ -9430,6 +9630,7 @@ function WhatsappAccountCard({
9430
9630
  onReconnect,
9431
9631
  onWorkspaceSave,
9432
9632
  onAgentPresetSave,
9633
+ onAccessPolicySave,
9433
9634
  onRequestRemove,
9434
9635
  onConfirmRemove,
9435
9636
  onCancelRemove
@@ -9480,6 +9681,11 @@ function WhatsappAccountCard({
9480
9681
  disabled: Boolean(busy),
9481
9682
  onSave: onAgentPresetSave
9482
9683
  }),
9684
+ h2(WhatsappAccessSettings, {
9685
+ account,
9686
+ busy: Boolean(busy),
9687
+ onSave: onAccessPolicySave
9688
+ }),
9483
9689
  h2(
9484
9690
  "div",
9485
9691
  { className: "ddt-accountFooter dim-cardFooter" },
@@ -9747,6 +9953,12 @@ function WhatsappSettingsTab({ rpcCall }) {
9747
9953
  WHATSAPP_ENDPOINTS.setAgentPreset,
9748
9954
  { botId: account.botId, agentPreset }
9749
9955
  ),
9956
+ onAccessPolicySave: (accessPolicy) => botAction(
9957
+ account,
9958
+ "access",
9959
+ WHATSAPP_ENDPOINTS.setAccessPolicy,
9960
+ { botId: account.botId, ...accessPolicy }
9961
+ ),
9750
9962
  onRequestRemove: () => setRemoveTarget(account.botId),
9751
9963
  onCancelRemove: () => setRemoveTarget(null),
9752
9964
  onConfirmRemove: async () => {