@xmanrui/dsh-im 4.14.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -60,6 +60,8 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
60
60
 
61
61
  Other IM platforms can be added through the same channel-adapter structure.
62
62
 
63
+ Feishu group chats accept messages from other bots that explicitly mention the current bot by default, with no extra setting. Messages without that mention, mentions of only other members or everyone, self-sent messages, and bot DMs are still ignored, even in all-message mode. Group allowlists and command permissions still apply. The app needs the `im:message.group_at_msg.include_bot:readonly` tenant scope. New apps request it through the QR flow; existing or manually connected apps can use **Complete permissions** or `/repair` in a direct chat, scan the QR code, and complete any publication or approval required by Feishu. See the [Feishu message-event permissions](https://open.feishu.cn/document/server-docs/im-v1/message/events/receive).
64
+
63
65
  All nine built-in channels can send JPEG, PNG, and WebP images, plus GIFs sent as image files, with optional captions to Harness. Each image is limited to 5 MB, and images in one message are limited to 20 MB in total. Downloading images or files from Feishu user messages requires the `im:message:readonly` tenant scope, shown on the confirmation page as **Read direct and group messages**; Feishu currently offers no narrower image-only scope for that download endpoint. Apps created through the built-in QR flow request it by default; for existing or manually connected apps, click **Complete permissions** on the IM Bot settings page and scan the QR code to incrementally add that scope, `im:resource` for uploading bot-sent images or files, `application:app_slash_command:read` / `write` for the native command panel, and the card callback.
64
66
 
65
67
  ### Results after a reply timeout
@@ -132,6 +134,11 @@ Use the proxy URL required by your network and restart the Host after changing i
132
134
  | Model | Every bot in all nine IM channels can choose a model directly below its workspace, or follow the Host default. A change applies only to later new Sessions; send `/new` and then an ordinary message in the current chat to use it. |
133
135
  | Agent Preset | Each bot can choose an Agent Preset on its settings card. When none is chosen, new Sessions follow the Host's `agent-presets.default`. A channel-level `config.agentPreset` is only the default for later new bots on that channel. Changing the preset never modifies or clears existing Sessions; if the current chat already has a Session, send `/new` and then a regular message to create one with the new selection. |
134
136
  | Context enhancement | Open settings from a bot card to enable groups and DMs independently. Both switches default to off, including for existing bots after an upgrade. |
137
+ | Session channel identity | Sessions from all nine IM channels and AI Office on the local Host are marked with their source. The Web Session list and search results display channel logos in place of prefixes such as “WeChat ·”, preserving DSH's automatic title generation and updates. Existing Sessions receive the prefix when next loaded. |
138
+
139
+ Channel prefixes are appended after DSH produces a title, preserving its complete text and automatic/manual source without pinning automatic titles or making additional model calls. Regeneration, refresh, and restarts do not stack prefixes; actual manual renames retain DSH's normal pinning behavior. This feature uses the current Host's Session events; an explicit remote `harnessBaseUrl` requires the plugin on the destination Host.
140
+
141
+ Logos are a dsh-im browser enhancement and require no changes to DSH. The adapter preserves original text nodes, clicks, menus, and dragging; copied titles, screen readers, and other surfaces retain the textual channel name. Unrecognized DSH page structures, unsupported browsers, or image-loading failures keep the text prefix. Unloading the plugin restores the original display.
135
142
 
136
143
  ### Proactive delivery
137
144
 
@@ -179,7 +186,7 @@ See the [Proactive Delivery Guide](PROACTIVE_DELIVERY.en.md) ([简体中文](PRO
179
186
  | `/batch` | Start batch input in a direct chat and collect up to 10 text messages. |
180
187
  | `/send` | Submit the collected messages, in order, as one input. |
181
188
  | `/cancel` | Cancel batch input and discard its collected messages. |
182
- | `/repair` | In a Feishu direct chat, incrementally repair the card callback and permissions required for media and the native Slash Command panel. |
189
+ | `/repair` | In a Feishu direct chat, incrementally repair the card callback and permissions required for media, group bot mentions, and the native Slash Command panel. |
183
190
  | `/compact` | Immediately compact older context in the Session bound to the current chat. |
184
191
  | `/workspace <workspace index or absolute path>`, `/ws <workspace index or absolute path>` | Switch the current bot's Harness workspace by `/workspacelist` index or absolute path. |
185
192
  | `/workspacelist`, `/workspaces`, `/wsl` | List workspace absolute paths that still exist on the current Harness Host. |
package/README.md CHANGED
@@ -63,6 +63,8 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
63
63
 
64
64
  其他 IM 平台可继续按同一渠道适配器结构接入。
65
65
 
66
+ 飞书群聊默认接收其他机器人明确 @ 当前机器人的消息,无需额外开关;未 @、仅 @ 其他成员或全体、机器人自身发送的消息和机器人私聊消息仍会忽略,即使群聊响应方式设为“全部”。消息仍受群聊白名单与命令权限约束。飞书应用需要租户权限 `im:message.group_at_msg.include_bot:readonly`(“获取群组中其他机器人和用户@当前机器人的消息”);扫码新建应用会默认申请,已有或手动绑定的应用可点击“补全权限”或私聊执行 `/repair`,扫码并完成飞书要求的发布审批后生效。详见[飞书接收消息权限说明](https://open.feishu.cn/document/server-docs/im-v1/message/events/receive)。
67
+
66
68
  九个内置渠道均支持把 JPEG、PNG、WebP 图片,以及以图片文件方式发送的 GIF,连同可选文字说明发送给 Harness;单张图片上限为 5 MB,单条消息中的图片总大小上限为 20 MB。飞书下载用户消息中的图片或文件需要租户权限 `im:message:readonly`,确认页将其显示为“获取单聊、群组消息”;飞书目前没有为该下载接口提供仅限图片的更窄权限。扫码新建的应用会默认申请;已有或手动绑定的应用可私聊机器人执行 `/repair`,或在「IM机器人」设置页点击“补全权限”,扫码增量补全该权限、上传机器人图片或文件所需的 `im:resource`、原生命令面板所需的 `application:app_slash_command:read` / `write`,以及卡片回调。
67
69
 
68
70
  ### 超时后的结果补发
@@ -135,6 +137,11 @@ dsh web
135
137
  | 模型 | 九个 IM 渠道的每个机器人都可在工作区下方独立选择模型;未选择时跟随 Host 默认。切换只影响之后新建的会话;当前聊天先发送 `/new`,再发送普通消息才会使用新选择。 |
136
138
  | Agent Preset | 每个机器人可在设置页卡片中选择 Agent Preset。未选择时跟随 Host 的 `agent-presets.default`;渠道级 `config.agentPreset` 只作为该渠道之后新接入机器人的默认值。切换不会修改或清空已有会话;若当前聊天已有会话,需先发送 `/new`,再发送一条普通消息,才会按新选择创建会话。 |
137
139
  | 上下文增强 | 从机器人卡片打开设置,分别决定群聊、私聊是否增强;两个开关默认均关闭,旧机器人升级后也不会自动开启。 |
140
+ | 会话渠道标识 | 本机 Host 的九个 IM 渠道与 AI Office 会话自动标记来源。Web 会话列表和搜索结果将「微信 ·」等前缀显示为渠道 Logo;保留 DSH 原有的自动标题生成与更新。已有会话在下次加载时补上。 |
141
+
142
+ 渠道前缀在 DSH 生成标题后追加,完整保留原始标题与自动/手动来源,不会将自动标题锁定为手动命名,也不会额外调用模型。重复生成、刷新或重启不会叠加前缀;真实的手动命名仍遵循 DSH 原有的锁定规则。此功能由当前 Host 的会话事件驱动,显式连接远程 `harnessBaseUrl` 时需在目标 Host 上安装该插件。
143
+
144
+ Logo 由 dsh-im 的浏览器适配显示,无需修改 DSH。适配保留原始文字节点和点击、菜单、拖拽操作;复制、读屏及其他界面仍保留文字渠道名。DSH 页面结构不匹配、浏览器不支持或图标加载失败时,自动保留文字前缀;插件卸载后恢复原始显示。
138
145
 
139
146
  ### 主动投递
140
147
 
@@ -182,7 +189,7 @@ dsh web
182
189
  | `/batch` | 在私聊中开启批量输入,最多收集 10 条纯文字消息。 |
183
190
  | `/send` | 将已收集的消息按原顺序作为一次输入提交。 |
184
191
  | `/cancel` | 取消批量输入并丢弃已收集的消息。 |
185
- | `/repair` | 在飞书私聊中增量修复卡片回调,并补全媒体与原生 Slash Command 面板所需的权限。 |
192
+ | `/repair` | 在飞书私聊中增量修复卡片回调,并补全媒体、群聊机器人 @ 消息与原生 Slash Command 面板所需的权限。 |
186
193
  | `/compact` | 立即压缩当前聊天绑定会话的较早上下文。 |
187
194
  | `/workspace <工作区序号或绝对路径>`、`/ws <工作区序号或绝对路径>` | 按 `/workspacelist` 序号或绝对路径切换当前机器人的 Harness 工作区。 |
188
195
  | `/workspacelist`、`/workspaces`、`/wsl` | 列出当前 Harness Host 上仍然存在的工作区绝对路径。 |
package/lib/client.js CHANGED
@@ -571,7 +571,7 @@ var React27 = __toESM(require("react"), 1);
571
571
  // package.json
572
572
  var package_default = {
573
573
  name: "@xmanrui/dsh-im",
574
- version: "4.14.0",
574
+ version: "4.15.0",
575
575
  description: "\u628A\u4E5D\u79CD IM \u673A\u5668\u4EBA\u548C\u516C\u7F51 AI Office \u63A5\u5165\u672C\u673A DeepSeek Harness\u3002 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
576
576
  keywords: [
577
577
  "deepseek-harness",
@@ -1313,7 +1313,7 @@ var EN = Object.freeze({
1313
1313
  "\u6743\u9650\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u542F\u7528\u5168\u90E8\u6D88\u606F\u6A21\u5F0F\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.",
1314
1314
  "\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002": "Requesting a group-message permission QR code for the existing Feishu app\u2026",
1315
1315
  "\u7FA4\u6D88\u606F\u6743\u9650\u6CA1\u6709\u5F00\u901A\u5B8C\u6210": "Group-message permission was not granted",
1316
- "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u589E\u91CF\u8865\u5145\u5F53\u524D\u7F3A\u5C11\u7684\u5361\u7247\u6309\u94AE\u56DE\u8C03\u3001\u8BFB\u53D6\u7528\u6237\u6D88\u606F\u5185\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:message:readonly\uFF08\u98DE\u4E66\u663E\u793A\u4E3A\u201C\u83B7\u53D6\u5355\u804A\u3001\u7FA4\u7EC4\u6D88\u606F\u201D\uFF09\u3001\u4E0A\u4F20\u673A\u5668\u4EBA\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:resource\uFF0C\u4EE5\u53CA\u539F\u751F\u547D\u4EE4\u9762\u677F\u6240\u9700\u7684 application:app_slash_command:read / write\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u9875\u53EA\u663E\u793A\u5F53\u524D\u7F3A\u5C11\u9879\uFF0C\u5B8C\u6210\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002": "Scanning updates the existing Feishu app with the missing card-button callback, im:message:readonly for reading images or files in user messages (shown by Feishu as \u201CRead direct and group messages\u201D), im:resource for uploading images or files sent by the bot, and application:app_slash_command:read / write for the native command panel. It does not create a new app. The confirmation page shows only missing items; this bot reconnects briefly afterward, while other bots are unaffected.",
1316
+ "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u589E\u91CF\u8865\u5145\u5F53\u524D\u7F3A\u5C11\u7684\u5361\u7247\u6309\u94AE\u56DE\u8C03\u3001\u8BFB\u53D6\u7528\u6237\u6D88\u606F\u5185\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:message:readonly\uFF08\u98DE\u4E66\u663E\u793A\u4E3A\u201C\u83B7\u53D6\u5355\u804A\u3001\u7FA4\u7EC4\u6D88\u606F\u201D\uFF09\u3001\u4E0A\u4F20\u673A\u5668\u4EBA\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:resource\u3001\u63A5\u6536\u7FA4\u5185\u5176\u4ED6\u673A\u5668\u4EBA @ \u5F53\u524D\u673A\u5668\u4EBA\u6240\u9700\u7684 im:message.group_at_msg.include_bot:readonly\uFF0C\u4EE5\u53CA\u539F\u751F\u547D\u4EE4\u9762\u677F\u6240\u9700\u7684 application:app_slash_command:read / write\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u9875\u53EA\u663E\u793A\u5F53\u524D\u7F3A\u5C11\u9879\uFF0C\u5B8C\u6210\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002": "Scanning updates the existing Feishu app with the missing card-button callback, im:message:readonly for reading images or files in user messages (shown by Feishu as \u201CRead direct and group messages\u201D), im:resource for uploading images or files sent by the bot, im:message.group_at_msg.include_bot:readonly for receiving group messages from other bots that mention this bot, and application:app_slash_command:read / write for the native command panel. It does not create a new app. The confirmation page shows only missing items; this bot reconnects briefly afterward, while other bots are unaffected.",
1317
1317
  "\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5F53\u524D\u7F3A\u5C11\u7684\u4E0A\u8FF0\u914D\u7F6E": "Review the existing app name and confirm that only the missing items described above are added",
1318
1318
  "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u6743\u9650\u4E0E\u56DE\u8C03\u8865\u5168\u5B8C\u6210": "Keep this page open until permissions and the callback are complete",
1319
1319
  "\u53D6\u6D88\u8865\u5168": "Cancel setup",
@@ -6344,7 +6344,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
6344
6344
  h2("span", null, repairing ? `\u6B63\u5728\u4E3A\u300C${botName}\u300D\u8865\u5168\u6743\u9650\u4E0E\u56DE\u8C03` : grantingGroupMessages ? `\u6B63\u5728\u4E3A\u300C${botName}\u300D\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650` : "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA")
6345
6345
  ),
6346
6346
  h2("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : repairing ? "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u8865\u5168\u6743\u9650" : grantingGroupMessages ? "\u4F7F\u7528\u98DE\u4E66\u786E\u8BA4\u7FA4\u6D88\u606F\u6743\u9650" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"),
6347
- h2("p", null, repairing ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u589E\u91CF\u8865\u5145\u5F53\u524D\u7F3A\u5C11\u7684\u5361\u7247\u6309\u94AE\u56DE\u8C03\u3001\u8BFB\u53D6\u7528\u6237\u6D88\u606F\u5185\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:message:readonly\uFF08\u98DE\u4E66\u663E\u793A\u4E3A\u201C\u83B7\u53D6\u5355\u804A\u3001\u7FA4\u7EC4\u6D88\u606F\u201D\uFF09\u3001\u4E0A\u4F20\u673A\u5668\u4EBA\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:resource\uFF0C\u4EE5\u53CA\u539F\u751F\u547D\u4EE4\u9762\u677F\u6240\u9700\u7684 application:app_slash_command:read / write\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u9875\u53EA\u663E\u793A\u5F53\u524D\u7F3A\u5C11\u9879\uFF0C\u5B8C\u6210\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : grantingGroupMessages ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u4F1A\u81EA\u52A8\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"),
6347
+ h2("p", null, repairing ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u589E\u91CF\u8865\u5145\u5F53\u524D\u7F3A\u5C11\u7684\u5361\u7247\u6309\u94AE\u56DE\u8C03\u3001\u8BFB\u53D6\u7528\u6237\u6D88\u606F\u5185\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:message:readonly\uFF08\u98DE\u4E66\u663E\u793A\u4E3A\u201C\u83B7\u53D6\u5355\u804A\u3001\u7FA4\u7EC4\u6D88\u606F\u201D\uFF09\u3001\u4E0A\u4F20\u673A\u5668\u4EBA\u56FE\u7247\u6216\u6587\u4EF6\u6240\u9700\u7684 im:resource\u3001\u63A5\u6536\u7FA4\u5185\u5176\u4ED6\u673A\u5668\u4EBA @ \u5F53\u524D\u673A\u5668\u4EBA\u6240\u9700\u7684 im:message.group_at_msg.include_bot:readonly\uFF0C\u4EE5\u53CA\u539F\u751F\u547D\u4EE4\u9762\u677F\u6240\u9700\u7684 application:app_slash_command:read / write\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u9875\u53EA\u663E\u793A\u5F53\u524D\u7F3A\u5C11\u9879\uFF0C\u5B8C\u6210\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : grantingGroupMessages ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u4F1A\u81EA\u52A8\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"),
6348
6348
  h2(
6349
6349
  "ol",
6350
6350
  { className: "bxf-steps dim-steps" },
@@ -15261,6 +15261,254 @@ function installImStyles() {
15261
15261
  return () => style.remove();
15262
15262
  }
15263
15263
 
15264
+ // src/channels/shared/session-channel-labels.mjs
15265
+ var SESSION_CHANNEL_LABELS = Object.freeze(Object.fromEntries(Object.entries({
15266
+ weixin: ["\u5FAE\u4FE1", "WeChat"],
15267
+ feishu: ["\u98DE\u4E66", "Feishu"],
15268
+ dingtalk: ["\u9489\u9489", "DingTalk"],
15269
+ wecom: ["\u4F01\u4E1A\u5FAE\u4FE1", "WeCom"],
15270
+ qq: ["QQ", "QQ"],
15271
+ slack: ["Slack", "Slack"],
15272
+ telegram: ["Telegram", "Telegram"],
15273
+ discord: ["Discord", "Discord"],
15274
+ whatsapp: ["WhatsApp", "WhatsApp"],
15275
+ office: ["AI Office", "AI Office"]
15276
+ }).map(([channel4, labels]) => [channel4, Object.freeze(labels)])));
15277
+ function parseSessionChannelTitle(title) {
15278
+ if (typeof title !== "string") return null;
15279
+ for (const [channel4, labels] of Object.entries(SESSION_CHANNEL_LABELS)) {
15280
+ for (const label of labels) {
15281
+ const prefix = `${label} \xB7 `;
15282
+ if (title.startsWith(prefix) && title.slice(prefix.length).trim()) {
15283
+ return { channel: channel4, title: title.slice(prefix.length) };
15284
+ }
15285
+ }
15286
+ }
15287
+ return null;
15288
+ }
15289
+
15290
+ // plugin-src/client/session-channel-logos.js
15291
+ var GLYPHS = Object.freeze({
15292
+ weixin: [WeixinLogoGlyph, "#07c160"],
15293
+ feishu: [FeishuLogoGlyph, "#3370ff"],
15294
+ dingtalk: [DingtalkLogoGlyph, "#1677ff"],
15295
+ wecom: [WecomLogoGlyph, "#3370ff"],
15296
+ qq: [QqLogoGlyph, "#12b7f5"],
15297
+ slack: [SlackLogoGlyph, "#e01e5a"],
15298
+ telegram: [TelegramLogoGlyph, "#229ed9"],
15299
+ discord: [DiscordLogoGlyph, "#5865f2"],
15300
+ whatsapp: [WhatsappLogoGlyph, "#25d366"],
15301
+ office: [OfficeLogoGlyph, "#f97316"]
15302
+ });
15303
+ var CHANNEL_ATTR = "data-dsh-im-session-channel";
15304
+ var TITLE_ATTR = "data-dsh-im-session-text";
15305
+ var MARKED = `[${CHANNEL_ATTR}]`;
15306
+ var ROW = '[role="treeitem"][aria-selected]';
15307
+ var SVG_NS = "http://www.w3.org/2000/svg";
15308
+ var installations = /* @__PURE__ */ new WeakMap();
15309
+ function hasClassPart(element, part) {
15310
+ const match = new RegExp(`(?:^|[_-])${part}(?:$|[_-])`, "u");
15311
+ return [...element.classList].some((token) => match.test(token));
15312
+ }
15313
+ function titleOf(row) {
15314
+ if (!row.matches(ROW) || row.closest('input, textarea, [contenteditable]:not([contenteditable="false"])')) return null;
15315
+ let title;
15316
+ if (row.tagName === "DIV" && hasClassPart(row, "sessionRow")) {
15317
+ title = [...row.children].find((child) => child.tagName === "SPAN" && hasClassPart(child, "title"));
15318
+ } else if (row.tagName === "BUTTON" && hasClassPart(row, "searchResultRow")) {
15319
+ const heading = [...row.children].find((child) => hasClassPart(child, "searchResultHeading"));
15320
+ title = heading && [...heading.children].find((child) => child.tagName === "SPAN" && hasClassPart(child, "searchResultTitle"));
15321
+ }
15322
+ return title?.childNodes.length === 1 && title.firstChild.nodeType === 3 ? title : null;
15323
+ }
15324
+ function logoUri(document2, channel4) {
15325
+ const [Glyph, color] = GLYPHS[channel4];
15326
+ const render = (node) => {
15327
+ if (!["svg", "path"].includes(node.type)) throw new Error("Unsupported channel logo element");
15328
+ const element = document2.createElementNS(SVG_NS, node.type);
15329
+ for (const [name2, value] of Object.entries(node.props)) {
15330
+ if (name2 === "children") continue;
15331
+ const attribute = name2.startsWith("stroke") ? name2.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`) : name2;
15332
+ element.setAttribute(attribute, String(value));
15333
+ }
15334
+ for (const child of [node.props.children].flat().filter(Boolean)) element.appendChild(render(child));
15335
+ return element;
15336
+ };
15337
+ const svg = render(Glyph({ size: 16 }));
15338
+ svg.setAttribute("xmlns", SVG_NS);
15339
+ svg.setAttribute("color", color);
15340
+ return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
15341
+ }
15342
+ function createInstallation(document2) {
15343
+ const view = document2.defaultView;
15344
+ const ready = /* @__PURE__ */ new Set();
15345
+ const owned = /* @__PURE__ */ new Map();
15346
+ const queued = /* @__PURE__ */ new Set();
15347
+ const images = [];
15348
+ let closed = false;
15349
+ let scheduled = false;
15350
+ const style = document2.createElement("style");
15351
+ style.dataset.pluginCss = "dsh-im-session-channel-logos";
15352
+ const uris = Object.fromEntries(Object.keys(GLYPHS).map((channel4) => [channel4, logoUri(document2, channel4)]));
15353
+ style.textContent = `
15354
+ ${MARKED}[${TITLE_ATTR}] {
15355
+ position: relative;
15356
+ -webkit-text-fill-color: transparent;
15357
+ text-overflow: clip !important;
15358
+ overflow: hidden;
15359
+ }
15360
+ ${MARKED}[${TITLE_ATTR}]::before {
15361
+ content: "";
15362
+ position: absolute;
15363
+ inset-inline-start: 0;
15364
+ top: 50%;
15365
+ transform: translateY(-50%);
15366
+ width: 16px;
15367
+ height: 16px;
15368
+ background: var(--dsh-im-session-logo) center / contain no-repeat;
15369
+ text-indent: 0;
15370
+ pointer-events: none;
15371
+ }
15372
+ ${MARKED}[${TITLE_ATTR}]::after {
15373
+ content: attr(${TITLE_ATTR}) / "";
15374
+ position: absolute;
15375
+ inset: 0;
15376
+ inset-inline-start: 22px;
15377
+ -webkit-text-fill-color: currentColor;
15378
+ text-indent: 0;
15379
+ overflow: hidden;
15380
+ white-space: nowrap;
15381
+ text-overflow: ellipsis;
15382
+ pointer-events: none;
15383
+ }
15384
+ ${Object.entries(uris).map(([channel4, uri]) => `[${CHANNEL_ATTR}="${channel4}"] { --dsh-im-session-logo: url("${uri}"); }`).join("\n")}
15385
+ `;
15386
+ document2.head.appendChild(style);
15387
+ const restore = (title) => {
15388
+ const previous = owned.get(title);
15389
+ if (!previous) return;
15390
+ owned.delete(title);
15391
+ for (const [attribute, value] of previous) {
15392
+ if (value === null) title.removeAttribute(attribute);
15393
+ else title.setAttribute(attribute, value);
15394
+ }
15395
+ };
15396
+ const update = (row) => {
15397
+ const title = row.isConnected ? titleOf(row) : null;
15398
+ for (const marked of row.querySelectorAll(MARKED)) {
15399
+ if (marked !== title) restore(marked);
15400
+ }
15401
+ if (!title) return;
15402
+ const parsed = parseSessionChannelTitle(title.textContent);
15403
+ if (!parsed || !ready.has(parsed.channel)) {
15404
+ restore(title);
15405
+ return;
15406
+ }
15407
+ if (!owned.has(title)) {
15408
+ owned.set(title, [CHANNEL_ATTR, TITLE_ATTR].map((attribute) => [attribute, title.getAttribute(attribute)]));
15409
+ }
15410
+ title.setAttribute(TITLE_ATTR, parsed.title);
15411
+ title.setAttribute(CHANNEL_ATTR, parsed.channel);
15412
+ };
15413
+ const schedule = () => {
15414
+ if (closed || scheduled || !queued.size) return;
15415
+ scheduled = true;
15416
+ queueMicrotask(() => {
15417
+ scheduled = false;
15418
+ if (closed) return;
15419
+ const rows = [...queued];
15420
+ queued.clear();
15421
+ for (const row of rows) {
15422
+ try {
15423
+ update(row);
15424
+ } catch {
15425
+ for (const marked of row.querySelectorAll(MARKED)) restore(marked);
15426
+ }
15427
+ }
15428
+ });
15429
+ };
15430
+ const collect = (node, descendants = false) => {
15431
+ const element = node.nodeType === 1 ? node : node.parentElement;
15432
+ if (!element) return;
15433
+ const row = element.closest(ROW);
15434
+ if (row) queued.add(row);
15435
+ if (descendants) {
15436
+ for (const child of element.querySelectorAll(ROW)) queued.add(child);
15437
+ const marked = [...element.querySelectorAll(MARKED)];
15438
+ if (owned.has(element)) marked.push(element);
15439
+ for (const title of marked) {
15440
+ const owner = title.closest(ROW);
15441
+ if (owner) queued.add(owner);
15442
+ else restore(title);
15443
+ }
15444
+ }
15445
+ schedule();
15446
+ };
15447
+ const observer = new view.MutationObserver((records) => {
15448
+ if (closed) return;
15449
+ for (const record2 of records) {
15450
+ collect(record2.target, record2.type === "attributes");
15451
+ if (record2.type !== "childList") continue;
15452
+ for (const node of record2.addedNodes) collect(node, true);
15453
+ for (const node of record2.removedNodes) {
15454
+ if (node.nodeType !== 1 || node.isConnected) continue;
15455
+ if (owned.has(node)) restore(node);
15456
+ for (const title of node.querySelectorAll(MARKED)) restore(title);
15457
+ }
15458
+ }
15459
+ });
15460
+ observer.observe(document2.body, {
15461
+ subtree: true,
15462
+ childList: true,
15463
+ characterData: true,
15464
+ attributes: true,
15465
+ attributeFilter: ["class", "role", "aria-selected", "contenteditable"]
15466
+ });
15467
+ for (const [channel4, uri] of Object.entries(uris)) {
15468
+ const image = new view.Image();
15469
+ images.push(image);
15470
+ image.onload = () => {
15471
+ if (closed) return;
15472
+ ready.add(channel4);
15473
+ collect(document2.body, true);
15474
+ };
15475
+ image.src = uri;
15476
+ }
15477
+ return () => {
15478
+ closed = true;
15479
+ observer.disconnect();
15480
+ queued.clear();
15481
+ for (const image of images) image.onload = null;
15482
+ for (const title of [...owned.keys()]) restore(title);
15483
+ style.remove();
15484
+ };
15485
+ }
15486
+ function installSessionChannelLogos(document2 = globalThis.document) {
15487
+ const view = document2?.defaultView;
15488
+ if (!document2?.body || !document2?.head || !view?.MutationObserver || !view?.Image || !view.CSS?.supports?.("content", `attr(${TITLE_ATTR}) / ""`) || !view.CSS.supports("-webkit-text-fill-color", "transparent")) return () => {
15489
+ };
15490
+ let entry = installations.get(document2);
15491
+ if (!entry) {
15492
+ try {
15493
+ entry = { references: 0, close: createInstallation(document2) };
15494
+ } catch {
15495
+ return () => {
15496
+ };
15497
+ }
15498
+ installations.set(document2, entry);
15499
+ }
15500
+ entry.references += 1;
15501
+ let released = false;
15502
+ return () => {
15503
+ if (released) return;
15504
+ released = true;
15505
+ entry.references -= 1;
15506
+ if (entry.references > 0) return;
15507
+ entry.close();
15508
+ installations.delete(document2);
15509
+ };
15510
+ }
15511
+
15264
15512
  // plugin-src/client/update-panel.js
15265
15513
  var React26 = __toESM(require("react"), 1);
15266
15514
  var import_react_dom3 = require("react-dom");
@@ -16089,6 +16337,7 @@ function apply(ctx) {
16089
16337
  );
16090
16338
  const t = ctx.locale.bind(IM_LOCALE_NAMESPACE);
16091
16339
  setImTranslator(t);
16340
+ ctx.effect(() => installSessionChannelLogos(), "im-settings: Session channel logos");
16092
16341
  ctx.effect(() => {
16093
16342
  const disposers = [
16094
16343
  installFeishuStyles(),