@wenbin_wb/dsh-bridge 2.3.0 → 2.3.2

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
@@ -246,6 +246,43 @@ Integrates Tencent's official QQ Bot platform. Supports private chat, group chat
246
246
 
247
247
  ---
248
248
 
249
+ ### Feishu / Lark Bot (Official WebSocket)
250
+
251
+ Connect your enterprise self-built app via Feishu's official WebSocket protocol. Supports private chat and group mentions without public IP, domain name, or webhook configuration.
252
+
253
+ **Key Highlights**
254
+
255
+ - ⚡ **100% No Public IP Required**: Direct duplex WebSocket connection to Feishu Open Platform
256
+ - 📜 **Card JSON 2.0 Streaming**: In-place single-card incremental streaming updates, eliminating message bubble fragmentation
257
+ - 🛡️ **Card 2.0 Interactive Approvals**: Native orange approval card with `[✓ Approve]` / `[✕ Reject]` action buttons for 1-click execution
258
+ - 📝 **Full Markdown Rendering**: Native support for headings, tables, syntax highlighting, blockquotes, and lists
259
+ - 🔄 **Workspace & Session Management**: Table-formatted `/sessions`, `/use N` switching, and `/workspaces` listing
260
+
261
+ **Setup Steps**
262
+
263
+ 1. Go to [Feishu Open Platform](https://open.feishu.cn/app) to create a self-built app, enable "Bot" capability, and publish a version ([Detailed Guide](docs/feishu-usage.md))
264
+ 2. Under "Events & Callbacks", select "Use WebSocket to receive events", and add `im.message.receive_v1` & `card.action.trigger`
265
+ 3. Open DSH Settings → "Remote Access" → "IM Bots" → select "Feishu"
266
+ 4. Fill in App ID and App Secret, then click "Save & Connect"
267
+
268
+ **Feishu Bot Commands** (Full guide in [Feishu Bot Usage Guide](docs/feishu-usage.md))
269
+
270
+ | Command | Description |
271
+ |---------|-------------|
272
+ | *(plain text)* | Send to current active agent |
273
+ | `/new <prompt>` | Create and start a new session in current workspace |
274
+ | `/new <prompt> @N` | Create a new session in workspace N |
275
+ | `/sessions` (or `/list`) | List all sessions in a structured Markdown table |
276
+ | `/use N` (or `/resume N`) | Switch to/resume session N |
277
+ | `/workspaces` | List all available workspaces |
278
+ | `/end` | End current session |
279
+ | `/stop` | Stop currently executing task |
280
+ | `/status` | View agent status dashboard |
281
+ | `/yes` `/no` (or `1`/`2`) | Respond to permission approval requests (or click card buttons) |
282
+ | `/help` | Display full command help |
283
+
284
+ ---
285
+
249
286
  ## Optional Configuration
250
287
 
251
288
  The plugin is ready to use out of the box. To customize the proxy port, add to `cordis.yml`:
package/README.md CHANGED
@@ -246,6 +246,43 @@ dsh plugin --profile web add @wenbin_wb/dsh-bridge@latest
246
246
 
247
247
  ---
248
248
 
249
+ ### 飞书 Bot(官方 WebSocket 长连接)
250
+
251
+ 接入飞书开放平台企业自建应用,支持单聊与群聊(群聊需 @机器人)。走飞书官方最新 WebSocket 长连接协议,无需公网 IP、无需域名、免配置 Webhook。
252
+
253
+ **功能亮点**
254
+
255
+ - ⚡ **100% 免公网 IP**:官方 WebSocket 全双工长连接,本地电脑即可直连飞书开放平台
256
+ - 📜 **Card JSON 2.0 原生流式打字机**:单条卡片原地增量打字机更新,彻底告别气泡拆分碎片化
257
+ - 🛡️ **Card 2.0 交互卡片审批**:敏感操作触发审批时下发橙色告警卡片,手机/电脑端点击 `[✓ 批准执行]` / `[✕ 拒绝执行]` 按钮一键处理
258
+ - 📝 **全量 Markdown 渲染**:支持多级标题、表格、代码高亮、引用块与列表
259
+ - 🔄 **会话与工作区管理**:支持 `/sessions` 表格化查看、`/use N` 切换、`/workspaces` 查看工作区
260
+
261
+ **使用步骤**
262
+
263
+ 1. 前往 [飞书开放平台](https://open.feishu.cn/app) 创建企业自建应用,开启「机器人」能力并发布版本(详见 [飞书接入指南](docs/feishu-usage.md))
264
+ 2. 在「事件与回调」中开启「使用长连接接收事件」,添加 `im.message.receive_v1` 和 `card.action.trigger` 事件
265
+ 3. 打开 DSH 设置页「远程访问」→「IM 机器人」→ 选中「飞书」
266
+ 4. 填入 App ID 和 App Secret,点击「保存并连接」即可
267
+
268
+ **飞书里的命令**(完整说明见 [飞书 Bot 使用说明](docs/feishu-usage.md))
269
+
270
+ | 命令 | 说明 |
271
+ |------|------|
272
+ | *(普通文本)* | 发给当前活动 agent |
273
+ | `/new <提示词>` | 在当前工作区新建会话并开始 |
274
+ | `/new <提示词> @N` | 在指定工作区新建会话 |
275
+ | `/sessions`(或 `/list`) | 查看所有历史会话(结构化表格排版) |
276
+ | `/use N`(或 `/resume N`) | 切换到会话 N |
277
+ | `/workspaces` | 列出可用工作区 |
278
+ | `/end` | 结束当前会话 |
279
+ | `/stop` | 停止当前任务 |
280
+ | `/status` | 查看 agent 状态看板 |
281
+ | `/yes` `/no`(或 `1`/`2`) | 响应权限审批请求(或直接点击卡片按钮) |
282
+ | `/help` | 查看全部命令 |
283
+
284
+ ---
285
+
249
286
  ## 可选配置
250
287
 
251
288
  插件开箱即用,无需配置。如需修改代理端口,在 cordis.yml 中添加:
package/client/client.js CHANGED
@@ -723,6 +723,44 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
723
723
  },
724
724
  title: "\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u914D\u7F6E"
725
725
  }, "\u89E3\u7ED1\u8D26\u53F7")
726
+ ),
727
+ // 飞书专属:手机扫码直达 Bot 对话
728
+ platformId === "feishu" && platform.botQr && React.createElement(
729
+ "div",
730
+ {
731
+ style: {
732
+ marginTop: 12,
733
+ padding: 12,
734
+ background: "var(--dsw-alias-bg-layer-1,#ffffff)",
735
+ border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)",
736
+ borderRadius: 8,
737
+ display: "flex",
738
+ gap: 14,
739
+ alignItems: "center",
740
+ flexWrap: "wrap"
741
+ }
742
+ },
743
+ React.createElement("img", { src: platform.botQr, alt: "Feishu Bot QR", style: { width: 110, height: 110, borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", padding: 4, background: "#fff" } }),
744
+ React.createElement(
745
+ "div",
746
+ { style: { flex: 1, minWidth: 160 } },
747
+ React.createElement("div", { style: { ...s.label, fontSize: 13, fontWeight: 600 } }, "\u{1F4F1} \u624B\u673A\u98DE\u4E66\u626B\u7801\u76F4\u8FBE\u5BF9\u8BDD"),
748
+ React.createElement(
749
+ "div",
750
+ { style: { ...s.muted, fontSize: 12, marginTop: 4, lineHeight: 1.5 } },
751
+ "\u7528\u98DE\u4E66\u624B\u673A App \u626B\u63CF\u5DE6\u4FA7\u4E8C\u7EF4\u7801\uFF0C\u7ACB\u5373\u6253\u5F00\u4E0E Bot \u5BF9\u8BDD\uFF1B\u53D1\u9001\u9996\u6761\u6D88\u606F\u81EA\u52A8\u5B8C\u6210\u767D\u540D\u5355\u6388\u6743\u3002"
752
+ ),
753
+ platform.botLink && React.createElement(
754
+ "div",
755
+ { style: { display: "flex", gap: 8, marginTop: 8 } },
756
+ React.createElement("a", {
757
+ href: platform.botLink,
758
+ target: "_blank",
759
+ rel: "noopener noreferrer",
760
+ style: { ...s.btnGhost, textDecoration: "none", display: "inline-flex", alignItems: "center", gap: 4, padding: "4px 10px", fontSize: 12 }
761
+ }, "\u5728\u98DE\u4E66\u5BA2\u6237\u7AEF\u6253\u5F00 \u2197")
762
+ )
763
+ )
726
764
  )
727
765
  ),
728
766
  // 未配置 / 登录中:表单(QQ / 飞书)或二维码(微信)
@@ -742,6 +780,23 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
742
780
  ) : platformId === "qq" || platformId === "feishu" ? React.createElement(
743
781
  "div",
744
782
  { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4 } },
783
+ platformId === "feishu" && React.createElement(
784
+ "div",
785
+ {
786
+ style: {
787
+ background: "var(--dsw-alias-bg-layer-1,#ffffff)",
788
+ border: "1px dashed var(--dsw-alias-border-l2,#e5e7eb)",
789
+ borderRadius: 6,
790
+ padding: "8px 10px",
791
+ fontSize: 12,
792
+ lineHeight: 1.5
793
+ }
794
+ },
795
+ React.createElement("span", { style: s.label }, "\u{1F4A1} \u626B\u7801\u81EA\u52A8\u521B\u5EFA\u5F15\u5BFC\uFF1A"),
796
+ React.createElement("span", { style: s.muted }, " \u53EF\u5728\u7EC8\u7AEF\u8FD0\u884C "),
797
+ React.createElement("code", { style: { ...s.code, fontSize: 11, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "2px 4px", borderRadius: 4 } }, "npx feishu-bot-bootstrap"),
798
+ React.createElement("span", { style: s.muted }, " \u624B\u673A\u626B\u7801\u4E00\u952E\u81EA\u52A8\u521B\u5EFA\u5E94\u7528\u5E76\u8F93\u51FA\u51ED\u8BC1\uFF1B\u6216\u5728\u4E0B\u65B9\u624B\u52A8\u586B\u5165\u51ED\u8BC1\u3002")
799
+ ),
745
800
  React.createElement(
746
801
  "div",
747
802
  null,
package/client/index.js CHANGED
@@ -643,6 +643,36 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
643
643
  title: '清除登录凭证,下次需重新配置',
644
644
  }, '解绑账号'),
645
645
  ),
646
+ // 飞书专属:手机扫码直达 Bot 对话
647
+ platformId === 'feishu' && platform.botQr && React.createElement('div', {
648
+ style: {
649
+ marginTop: 12,
650
+ padding: 12,
651
+ background: 'var(--dsw-alias-bg-layer-1,#ffffff)',
652
+ border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
653
+ borderRadius: 8,
654
+ display: 'flex',
655
+ gap: 14,
656
+ alignItems: 'center',
657
+ flexWrap: 'wrap',
658
+ },
659
+ },
660
+ React.createElement('img', { src: platform.botQr, alt: 'Feishu Bot QR', style: { width: 110, height: 110, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', padding: 4, background: '#fff' } }),
661
+ React.createElement('div', { style: { flex: 1, minWidth: 160 } },
662
+ React.createElement('div', { style: { ...s.label, fontSize: 13, fontWeight: 600 } }, '📱 手机飞书扫码直达对话'),
663
+ React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 4, lineHeight: 1.5 } },
664
+ '用飞书手机 App 扫描左侧二维码,立即打开与 Bot 对话;发送首条消息自动完成白名单授权。'
665
+ ),
666
+ platform.botLink && React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8 } },
667
+ React.createElement('a', {
668
+ href: platform.botLink,
669
+ target: '_blank',
670
+ rel: 'noopener noreferrer',
671
+ style: { ...s.btnGhost, textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, padding: '4px 10px', fontSize: 12 },
672
+ }, '在飞书客户端打开 ↗'),
673
+ ),
674
+ ),
675
+ ),
646
676
  ),
647
677
 
648
678
  // 未配置 / 登录中:表单(QQ / 飞书)或二维码(微信)
@@ -659,6 +689,21 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
659
689
  )
660
690
  : (platformId === 'qq' || platformId === 'feishu')
661
691
  ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 4 } },
692
+ platformId === 'feishu' && React.createElement('div', {
693
+ style: {
694
+ background: 'var(--dsw-alias-bg-layer-1,#ffffff)',
695
+ border: '1px dashed var(--dsw-alias-border-l2,#e5e7eb)',
696
+ borderRadius: 6,
697
+ padding: '8px 10px',
698
+ fontSize: 12,
699
+ lineHeight: 1.5,
700
+ },
701
+ },
702
+ React.createElement('span', { style: s.label }, '💡 扫码自动创建引导:'),
703
+ React.createElement('span', { style: s.muted }, ' 可在终端运行 '),
704
+ React.createElement('code', { style: { ...s.code, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '2px 4px', borderRadius: 4 } }, 'npx feishu-bot-bootstrap'),
705
+ React.createElement('span', { style: s.muted }, ' 手机扫码一键自动创建应用并输出凭证;或在下方手动填入凭证。')
706
+ ),
662
707
  React.createElement('div', null,
663
708
  React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
664
709
  platformId === 'qq' ? 'AppID — QQ 开放平台机器人应用 ID' : 'App ID — 飞书开放平台自建应用 ID (cli_xxx)'
@@ -69,12 +69,22 @@ export class FeishuGateway extends Service {
69
69
  const isLark = this.config.domain === 'lark'
70
70
  const domain = isLark ? Lark.Domain.Lark : Lark.Domain.Feishu
71
71
 
72
+ const sdkLogger = {
73
+ debug: (...args) => this.logger?.debug?.('[lark-sdk debug]', ...args),
74
+ info: (...args) => this.logger?.debug?.('[lark-sdk info]', ...args),
75
+ warn: (...args) => this.logger?.debug?.('[lark-sdk warn]', ...args),
76
+ error: (...args) => this.logger?.error?.('[lark-sdk error]', ...args),
77
+ trace: () => {},
78
+ }
79
+
72
80
  // 1. 初始化 REST API Client
73
81
  this.client = new Lark.Client({
74
82
  appId: this.config.appId,
75
83
  appSecret: this.config.appSecret,
76
84
  domain,
77
85
  appType: Lark.AppType.SelfBuild,
86
+ logger: sdkLogger,
87
+ loggerLevel: Lark.LoggerLevel.error,
78
88
  })
79
89
 
80
90
  // 2. 初始化长连接 WSClient
@@ -82,11 +92,14 @@ export class FeishuGateway extends Service {
82
92
  appId: this.config.appId,
83
93
  appSecret: this.config.appSecret,
84
94
  domain,
95
+ logger: sdkLogger,
85
96
  loggerLevel: Lark.LoggerLevel.error,
86
97
  })
87
98
 
88
99
  // 3. 构建事件分发器
89
- this.eventDispatcher = new Lark.EventDispatcher({})
100
+ this.eventDispatcher = new Lark.EventDispatcher({
101
+ logger: sdkLogger,
102
+ })
90
103
 
91
104
  // 注册接收消息事件
92
105
  this.eventDispatcher.register({
@@ -108,6 +121,9 @@ export class FeishuGateway extends Service {
108
121
  'im.message.message_read_v1': async () => {
109
122
  // 消息已读回执事件,忽略
110
123
  },
124
+ 'im.chat.access_event.bot_p2p_chat_entered_v1': async () => {
125
+ // 用户进入与机器人的单聊窗口事件,忽略
126
+ },
111
127
  })
112
128
 
113
129
  // 4. 尝试获取机器人信息(验证凭证,非阻断)
@@ -2,6 +2,7 @@
2
2
  // 编排 FeishuGateway(官方 OpenAPI/WSClient 网关)+ FeishuConversationNode(飞书⇄DSH 会话桥)。
3
3
  // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
4
 
5
+ import QRCode from 'qrcode'
5
6
  import { Platform } from '../platform/base.js'
6
7
  import { FeishuGateway } from './gateway.js'
7
8
  import { FeishuConversationNode } from './node.js'
@@ -18,6 +19,7 @@ export class FeishuService extends Platform {
18
19
  super({ ctx, logger, config, onPersist })
19
20
  this.id = 'feishu'
20
21
  this.name = 'Feishu'
22
+ this._botQrCache = { appId: '', qr: '' }
21
23
 
22
24
  this.gateway = new FeishuGateway(ctx, {
23
25
  appId: config.appId ?? '',
@@ -125,6 +127,18 @@ export class FeishuService extends Platform {
125
127
 
126
128
  getStatus() {
127
129
  const allowFrom = [...(this.node?.config?.allowFrom ?? [])]
130
+ const appId = this.gateway.config.appId || ''
131
+ const botLink = appId ? `https://applink.feishu.cn/client/bot/open?appId=${encodeURIComponent(appId)}` : null
132
+ if (botLink && this._botQrCache.appId !== appId) {
133
+ this._botQrCache.appId = appId
134
+ void QRCode.toDataURL(botLink, {
135
+ width: 260,
136
+ margin: 2,
137
+ color: { dark: '#1F2421', light: '#FFFFFF' },
138
+ }).then((qr) => {
139
+ this._botQrCache.qr = qr
140
+ }).catch(() => {})
141
+ }
128
142
  return {
129
143
  id: this.id,
130
144
  name: this.name,
@@ -151,6 +165,8 @@ export class FeishuService extends Platform {
151
165
  domain: this.gateway.config.domain,
152
166
  },
153
167
  botInfo: this.gateway.botInfo,
168
+ botLink,
169
+ botQr: this._botQrCache.qr,
154
170
  error: this.gateway.error,
155
171
  }
156
172
  }
@@ -818,12 +818,13 @@ function getWorkspaceBasename(cwd) {
818
818
 
819
819
  async function routeCommand(node, text) {
820
820
  const trimmed = text.trim()
821
- if (!trimmed.startsWith('/')) return false
822
821
 
823
822
  if (trimmed === '/yes' || trimmed === '/no' || /^[12]$/.test(trimmed)) {
824
823
  if (node.resolveApproval(trimmed)) return true
825
824
  }
826
825
 
826
+ if (!trimmed.startsWith('/')) return false
827
+
827
828
  const [command, ...rest] = trimmed.slice(1).split(/\s+/)
828
829
  switch (command) {
829
830
  case 'help':
package/lib/qq/node.js CHANGED
@@ -205,6 +205,38 @@ export class QqConversationNode extends ConversationBridge {
205
205
  return this._sendMarkdown(peerId, text, { scope, msgId: replyMsgId, keyboard })
206
206
  }
207
207
 
208
+ // 审批申请:发送带「✓ 批准执行」与「✕ 拒绝执行」交互按钮的 Markdown 卡片
209
+ const isApprovalMessage = text.includes('操作权限确认')
210
+ if (isApprovalMessage) {
211
+ const keyboard = {
212
+ content: {
213
+ rows: [
214
+ {
215
+ buttons: [
216
+ {
217
+ id: 'approve_yes',
218
+ render_data: { label: '✓ 批准执行', visited_label: '已批准', style: 1 },
219
+ action: { type: 1, permission: { type: 2 }, data: '/yes', unsupport_tips: '请升级QQ客户端后使用' },
220
+ },
221
+ {
222
+ id: 'approve_no',
223
+ render_data: { label: '✕ 拒绝执行', visited_label: '已拒绝', style: 0 },
224
+ action: { type: 1, permission: { type: 2 }, data: '/no', unsupport_tips: '请升级QQ客户端后使用' },
225
+ },
226
+ ],
227
+ },
228
+ ],
229
+ },
230
+ }
231
+ return this._sendMarkdown(peerId, sanitizeQQMarkdown(text), { scope, keyboard })
232
+ }
233
+
234
+ // 审批决议通知(已批准/已拒绝):直接单条 Markdown 发送,无需流式分片
235
+ const isResolutionMessage = text.includes('已批准执行') || text.includes('已拒绝执行')
236
+ if (isResolutionMessage) {
237
+ return this._sendMarkdown(peerId, sanitizeQQMarkdown(text), { scope })
238
+ }
239
+
208
240
  // 其他回复统一走「流式 Markdown」:content_type=markdown 让手机端渲染
209
241
  const content = String(text || '').trim()
210
242
  if (content.length === 0) return { success: true }
@@ -421,6 +453,10 @@ export class QqConversationNode extends ConversationBridge {
421
453
  await this.handleInbound({ senderId: authId, text: '/list', isGroup })
422
454
  } else if (buttonId === 'help') {
423
455
  await this.handleInbound({ senderId: authId, text: '/help', isGroup })
456
+ } else if (buttonId === 'approve_yes') {
457
+ await this.handleInbound({ senderId: authId, text: '/yes', isGroup })
458
+ } else if (buttonId === 'approve_no') {
459
+ await this.handleInbound({ senderId: authId, text: '/no', isGroup })
424
460
  } else {
425
461
  this.logger?.info?.(`[dsh-bridge qq] unknown button interaction: ${buttonId}`)
426
462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenbin_wb/dsh-bridge",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",