@wenbin_wb/dsh-bridge 2.2.9 → 2.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.
package/README.en.md CHANGED
@@ -19,6 +19,7 @@ Seamlessly extend your local DeepSeek Harness to mobile phones, tablets, public
19
19
  - **Custom Tunnel**: Connect to your own tunnel server with a fixed domain ([Setup Guide](docs/custom-tunnel.md))
20
20
  - **WeChat Bot (ClawBot / iLink)**: Scan a QR code to log in a WeChat personal account, then chat with, control, and approve your DeepSeek Harness agents right inside WeChat. **Multi-workspace selection, restart-persistent sessions, grouped session listing with titles, media (image/file/voice) transfer, and permission approvals** — over Tencent's official iLink Bot API, no public server or tunnel required ([Usage Guide](docs/wechat-usage.md))
21
21
  - **QQ Bot (OpenAPI v2)**: Connect your QQ Bot to receive private/group messages, send Markdown, button keyboards, and rich media. **Full event coverage (C2C / GROUP_AT_MESSAGE_CREATE), auto token refresh, reconnection with backoff, message deduplication** — over Tencent's official QQ Bot OpenAPI v2 ([Usage Guide](docs/qq-usage.md))
22
+ - **Feishu / Lark Bot (Official WebSocket)**: Connect enterprise self-built apps via Feishu's official WebSocket protocol. **No public IP / no Webhook required, Markdown table formatting, native interactive card permission approvals with 1-click button actions** ([Usage Guide](docs/feishu-usage.md))
22
23
  - **Official Brand SVG Icons**: Authentic vector brand icons for WeChat, QQ, Feishu, Telegram with real-time status indicators
23
24
  - **Fast Version Check & 1-Click Upgrade**: Dual-channel registry check (npmmirror + npmjs fallback in ~200ms) with seamless **1-click in-app upgrade**, no terminal copying required
24
25
  - **Dark Mode Support**: Deep integration with DeepSeek Harness Design Tokens (`--dsw-alias-*`), QR code background protection for safe dark mode scanning
@@ -38,7 +39,7 @@ Seamlessly extend your local DeepSeek Harness to mobile phones, tablets, public
38
39
  | **Platform Abstraction** | Platform-agnostic core (sessions / approvals / commands / digest) shared across IM channels | ✅ **Completed** (v2.0.0) |
39
40
  | **WeChat** | Chat with your Agent directly in WeChat | ✅ Supported (workspaces / persisted sessions / media / approvals) |
40
41
  | **QQ Bot** | QQ bot integration for group/private chat | ✅ **Completed** (v2.1.0) — Markdown / buttons / rich media |
41
- | **Feishu** | Feishu message/bot integration for workplace scenarios | Planned |
42
+ | **Feishu** | Feishu / Lark official persistent WebSocket bot | **Completed** (v2.3.0) — No-public-IP WS / Card Approvals |
42
43
  | **Telegram** | IM channel suited for self-hosting and overseas | Planned |
43
44
  | **OpenClaw** | Connect with the OpenClaw ecosystem | Planned |
44
45
 
package/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - **自建隧道**:连接自己的隧道服务器,获得固定域名([搭建教程](docs/custom-tunnel.md))
20
20
  - **微信 Bot(ClawBot / iLink)**:扫码登录微信个人号后,直接在微信里对话、控制 DeepSeek Harness 的 agent。**支持多工作区选择、会话跨重启持久化、按工作区分组查看、媒体(图片/文件/语音)收发、权限审批**——走腾讯官方 iLink Bot API,无需公网([使用说明](docs/wechat-usage.md))
21
21
  - **QQ Bot(OpenAPI v2)**:接入 QQ 机器人,私聊/群聊接收消息,发送 Markdown、按钮键盘和富媒体。**完整事件覆盖(C2C / GROUP_AT_MESSAGE_CREATE)、Token 自动刷新、断线重连、消息去重**——走腾讯官方 QQ Bot OpenAPI v2([使用说明](docs/qq-usage.md))
22
+ - **飞书 Bot(官方 WebSocket 长连接)**:接入飞书开放平台企业自建应用,私聊/群聊实时交互。**无需公网 IP / 无需 Webhook、支持飞书 Markdown 表格排版、原生交互卡片权限审批一键点击确认**——走飞书官方最新 WebSocket 长连接协议([使用说明](docs/feishu-usage.md))
22
23
  - **IM 官方品牌矢量图标**:微信、QQ、飞书、Telegram 官方矢量图标与状态展示,直接在聊天软件里呼唤你的 Agent
23
24
  - **极速版本检查与一键升级**:国内高速镜像(npmmirror)优先 + 官方源毫秒级双通道检查,检测到新版本支持**界面一键直接升级**,无需手动打开终端
24
25
  - **深色模式深度适配**:完美适配 DeepSeek Harness 设计系统明暗主题切换,二维码自带白底安全垫,暗光下手机扫码 100% 极速识别
@@ -38,7 +39,7 @@
38
39
  | **平台抽象层** | 平台无关的核心(会话/审批/命令/digest)跨 IM 渠道复用 | ✅ **已完成**(v2.0.0) |
39
40
  | **微信** | 在微信里直接与你的 Agent 对话 | ✅ 已支持(多工作区 / 会话持久化 / 媒体 / 审批) |
40
41
  | **QQ Bot** | 接入 QQ 机器人,群聊/私聊唤起 Agent | ✅ **已完成**(v2.1.0)— Markdown / 按钮 / 富媒体 |
41
- | **飞书** | 飞书消息/机器人集成,办公场景直接调用 | 规划中 |
42
+ | **飞书** | 飞书开放平台长连接机器人,办公场景直接调用 | **已完成**(v2.3.0)— 免公网 WS / 卡片审批 |
42
43
  | **Telegram** | 适合自托管与海外的 IM 渠道 | 规划中 |
43
44
  | **OpenClaw** | 与 OpenClaw 生态打通 | 规划中 |
44
45
 
package/client/client.js CHANGED
@@ -425,7 +425,9 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
425
425
  sendChunkDelayMs: String(platform.config.sendChunkDelayMs ?? 1500),
426
426
  appId: platform.config.appId ?? "",
427
427
  // Secret 不由后端回传;空值表示沿用已保存密钥
428
- clientSecret: ""
428
+ clientSecret: "",
429
+ appSecret: "",
430
+ domain: platform.config.domain ?? "feishu"
429
431
  });
430
432
  }
431
433
  }, [platform?.config]);
@@ -501,10 +503,14 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
501
503
  if (platformId === "qq") {
502
504
  payload.appId = cfgDraft.appId.trim();
503
505
  payload.clientSecret = cfgDraft.clientSecret.trim();
506
+ } else if (platformId === "feishu") {
507
+ payload.appId = cfgDraft.appId.trim();
508
+ payload.appSecret = cfgDraft.appSecret.trim();
509
+ payload.domain = cfgDraft.domain || "feishu";
504
510
  }
505
511
  await act(BRIDGE_ENDPOINTS.platformSetConfig, payload);
506
512
  }, [act, cfgDraft, platformId]);
507
- const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs || platformId === "qq" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.clientSecret !== (platform.config.clientSecret ?? "")));
513
+ const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs || platformId === "qq" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.clientSecret !== (platform.config.clientSecret ?? "")) || platformId === "feishu" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.appSecret !== (platform.config.appSecret ?? "")));
508
514
  if (!platform && !err) {
509
515
  return React.createElement(
510
516
  "div",
@@ -561,6 +567,18 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
561
567
  rel: "noopener noreferrer",
562
568
  style: s.btnGhost
563
569
  }, "\u{1F310} QQ \u5F00\u653E\u5E73\u53F0"),
570
+ platformId === "feishu" && React.createElement("a", {
571
+ href: "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/feishu-usage.md",
572
+ target: "_blank",
573
+ rel: "noopener noreferrer",
574
+ style: s.btnGhost
575
+ }, "\u{1F4D6} \u98DE\u4E66\u4F7F\u7528\u8BF4\u660E"),
576
+ platformId === "feishu" && React.createElement("a", {
577
+ href: "https://open.feishu.cn/app",
578
+ target: "_blank",
579
+ rel: "noopener noreferrer",
580
+ style: s.btnGhost
581
+ }, "\u{1F310} \u98DE\u4E66\u5F00\u653E\u5E73\u53F0"),
564
582
  React.createElement("button", {
565
583
  style: s.btnGhost,
566
584
  onClick: () => setShowHelp((v) => !v)
@@ -695,19 +713,19 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
695
713
  React.createElement(
696
714
  "div",
697
715
  { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" } },
698
- platform.status !== "connected" && platform.status !== "starting" && React.createElement("button", { style: s.btnPri, onClick: onLogin, disabled: busy }, "\u91CD\u65B0\u767B\u5F55"),
716
+ platform.status !== "connected" && platform.status !== "starting" && React.createElement("button", { style: s.btnPri, onClick: onLogin, disabled: busy }, "\u91CD\u65B0\u8FDE\u63A5"),
699
717
  (platform.status === "connected" || platform.status === "starting") && React.createElement("button", { style: s.btnGhost, onClick: onStop, disabled: busy }, "\u65AD\u5F00"),
700
718
  React.createElement("button", {
701
719
  style: { ...s.btnGhost, color: "var(--dsw-alias-state-error-primary,#dc2626)", borderColor: "var(--dsw-alias-state-error-primary,#dc2626)", opacity: busy ? 0.5 : 1 },
702
720
  disabled: busy,
703
721
  onClick: () => {
704
- if (window.confirm("\u786E\u8BA4\u89E3\u7ED1\uFF1F\u8FD9\u5C06\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u767B\u5F55\u3002")) act(BRIDGE_ENDPOINTS.platformUnbind, {});
722
+ if (window.confirm("\u786E\u8BA4\u89E3\u7ED1\uFF1F\u8FD9\u5C06\u6E05\u9664\u4FDD\u5B58\u7684\u51ED\u8BC1\u3002")) act(BRIDGE_ENDPOINTS.platformUnbind, {});
705
723
  },
706
- title: "\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u767B\u5F55"
724
+ title: "\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u914D\u7F6E"
707
725
  }, "\u89E3\u7ED1\u8D26\u53F7")
708
726
  )
709
727
  ),
710
- // 未配置 / 登录中:二维码(QQ 无二维码,显示凭证表单)
728
+ // 未配置 / 登录中:表单(QQ / 飞书)或二维码(微信)
711
729
  (!platform?.configured || showQr) && React.createElement(
712
730
  "div",
713
731
  { style: s.block },
@@ -721,39 +739,41 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
721
739
  login.phase === "scaned" ? "\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u2026" : platformId === "wechat" ? "\u8BF7\u4F7F\u7528\u5FAE\u4FE1\u626B\u7801\u767B\u5F55\uFF08ClawBot\uFF09" : "\u8BF7\u626B\u7801\u767B\u5F55"
722
740
  ),
723
741
  login.error && React.createElement("div", { style: { ...s.muted, marginTop: 4, color: "var(--dsw-alias-state-warn-primary,#92400e)" } }, login.error)
724
- ) : platformId === "qq" ? React.createElement(
742
+ ) : platformId === "qq" || platformId === "feishu" ? React.createElement(
725
743
  "div",
726
744
  { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4 } },
727
745
  React.createElement(
728
746
  "div",
729
747
  null,
730
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "AppID \u2014 QQ \u5F00\u653E\u5E73\u53F0\u673A\u5668\u4EBA\u5E94\u7528 ID"),
748
+ React.createElement(
749
+ "div",
750
+ { style: { ...s.muted, marginBottom: 4 } },
751
+ platformId === "qq" ? "AppID \u2014 QQ \u5F00\u653E\u5E73\u53F0\u673A\u5668\u4EBA\u5E94\u7528 ID" : "App ID \u2014 \u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u81EA\u5EFA\u5E94\u7528 ID (cli_xxx)"
752
+ ),
731
753
  React.createElement("input", {
732
754
  style: { ...s.input, width: "100%" },
733
- placeholder: "\u8BF7\u8F93\u5165 AppID",
755
+ placeholder: platformId === "qq" ? "\u8BF7\u8F93\u5165 AppID" : "\u8BF7\u8F93\u5165 App ID (\u5982 cli_a1b2c3d4...)",
734
756
  value: cfgDraft?.appId ?? "",
735
- onChange: (e) => setCfgDraft((d) => ({ ...d, appId: e.target.value })),
736
- onKeyDown: (e) => {
737
- if (e.key === "Enter" && cfgDraft?.appId?.trim() && cfgDraft?.clientSecret?.trim() && !busy) saveConfig();
738
- }
757
+ onChange: (e) => setCfgDraft((d) => ({ ...d, appId: e.target.value }))
739
758
  })
740
759
  ),
741
760
  React.createElement(
742
761
  "div",
743
762
  null,
744
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "ClientSecret \u2014 QQ \u5F00\u653E\u5E73\u53F0\u673A\u5668\u4EBA\u5BC6\u94A5"),
763
+ React.createElement(
764
+ "div",
765
+ { style: { ...s.muted, marginBottom: 4 } },
766
+ platformId === "qq" ? "ClientSecret \u2014 QQ \u5F00\u653E\u5E73\u53F0\u673A\u5668\u4EBA\u5BC6\u94A5" : "App Secret \u2014 \u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u5E94\u7528\u5BC6\u94A5"
767
+ ),
745
768
  React.createElement(
746
769
  "div",
747
770
  { style: { display: "flex", gap: 6, alignItems: "center" } },
748
771
  React.createElement("input", {
749
772
  style: { ...s.input, flex: 1 },
750
773
  type: showSecret ? "text" : "password",
751
- placeholder: "\u8BF7\u8F93\u5165 ClientSecret",
752
- value: cfgDraft?.clientSecret ?? "",
753
- onChange: (e) => setCfgDraft((d) => ({ ...d, clientSecret: e.target.value })),
754
- onKeyDown: (e) => {
755
- if (e.key === "Enter" && cfgDraft?.appId?.trim() && cfgDraft?.clientSecret?.trim() && !busy) saveConfig();
756
- }
774
+ placeholder: platformId === "qq" ? "\u8BF7\u8F93\u5165 ClientSecret" : "\u8BF7\u8F93\u5165 App Secret",
775
+ value: platformId === "qq" ? cfgDraft?.clientSecret ?? "" : cfgDraft?.appSecret ?? "",
776
+ onChange: (e) => setCfgDraft((d) => platformId === "qq" ? { ...d, clientSecret: e.target.value } : { ...d, appSecret: e.target.value })
757
777
  }),
758
778
  React.createElement("button", {
759
779
  style: { ...s.btnGhost, height: 32, padding: "0 10px", fontSize: 13, flexShrink: 0 },
@@ -767,11 +787,11 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
767
787
  "div",
768
788
  null,
769
789
  React.createElement("a", {
770
- href: "https://bot.q.qq.com/wiki/develop/api-v2/",
790
+ href: platformId === "qq" ? "https://bot.q.qq.com/wiki/develop/api-v2/" : "https://open.feishu.cn/app",
771
791
  target: "_blank",
772
792
  rel: "noopener noreferrer",
773
793
  style: s.btnLink
774
- }, "\u{1F4D6} \u524D\u5F80 QQ \u5F00\u653E\u5E73\u53F0\u7533\u8BF7\u673A\u5668\u4EBA")
794
+ }, platformId === "qq" ? "\u{1F4D6} \u524D\u5F80 QQ \u5F00\u653E\u5E73\u53F0\u7533\u8BF7\u673A\u5668\u4EBA" : "\u{1F4D6} \u524D\u5F80\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u521B\u5EFA\u4F01\u4E1A\u81EA\u5EFA\u5E94\u7528")
775
795
  ),
776
796
  React.createElement(
777
797
  "div",
@@ -779,7 +799,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
779
799
  React.createElement("button", {
780
800
  style: { ...s.btnPri, opacity: busy ? 0.5 : 1 },
781
801
  onClick: saveConfig,
782
- disabled: busy || !cfgDraft?.appId?.trim() || !cfgDraft?.clientSecret?.trim()
802
+ disabled: busy || !cfgDraft?.appId?.trim() || (platformId === "qq" ? !cfgDraft?.clientSecret?.trim() : !cfgDraft?.appSecret?.trim())
783
803
  }, busy ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u5E76\u8FDE\u63A5"),
784
804
  login.phase === "error" && React.createElement("div", { style: { ...s.muted, fontSize: 12 } }, login.error ?? "\u8FDE\u63A5\u5931\u8D25")
785
805
  )
@@ -794,138 +814,6 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
794
814
  login.phase === "error" && React.createElement("div", { style: { ...s.muted, fontSize: 12 } }, login.error ?? "\u767B\u5F55\u5931\u8D25")
795
815
  )
796
816
  ),
797
- // 高级设置(紧凑 2x2 网格 + 恢复默认值)
798
- cfgDraft && React.createElement(
799
- "div",
800
- { style: s.block },
801
- React.createElement("button", {
802
- style: { ...s.btnLink, fontSize: 12, marginBottom: showAdvanced ? 10 : 0 },
803
- onClick: () => setShowAdvanced((v) => !v)
804
- }, showAdvanced ? "\u25BE \u9AD8\u7EA7\u8BBE\u7F6E" : "\u25B8 \u9AD8\u7EA7\u8BBE\u7F6E"),
805
- showAdvanced && React.createElement(
806
- "div",
807
- { style: { display: "flex", flexDirection: "column", gap: 10 } },
808
- // 2x2 参数网格
809
- React.createElement(
810
- "div",
811
- {
812
- style: {
813
- display: "grid",
814
- gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
815
- gap: 10
816
- }
817
- },
818
- // 心跳间隔
819
- React.createElement(
820
- "div",
821
- null,
822
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5FC3\u8DF3\u8FDB\u5EA6\u95F4\u9694 (\u79D2)"),
823
- React.createElement("input", {
824
- style: { ...s.input, width: "100%" },
825
- type: "number",
826
- min: 30,
827
- max: 3600,
828
- value: cfgDraft.digestIntervalSec,
829
- onChange: (e) => setCfgDraft((d) => ({ ...d, digestIntervalSec: e.target.value }))
830
- })
831
- ),
832
- // 审批超时
833
- React.createElement(
834
- "div",
835
- null,
836
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5BA1\u6279\u8D85\u65F6\u81EA\u52A8\u62D2\u7EDD (\u79D2)"),
837
- React.createElement("input", {
838
- style: { ...s.input, width: "100%" },
839
- type: "number",
840
- min: 30,
841
- max: 86400,
842
- value: cfgDraft.approvalTimeoutSec,
843
- onChange: (e) => setCfgDraft((d) => ({ ...d, approvalTimeoutSec: e.target.value }))
844
- })
845
- ),
846
- // 每气泡字数
847
- React.createElement(
848
- "div",
849
- null,
850
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5355\u6761\u6700\u5927\u5B57\u6570\u9650\u5236 (\u5B57)"),
851
- React.createElement("input", {
852
- style: { ...s.input, width: "100%" },
853
- type: "number",
854
- min: 100,
855
- max: 1e4,
856
- value: cfgDraft.maxMessageChars,
857
- onChange: (e) => setCfgDraft((d) => ({ ...d, maxMessageChars: e.target.value }))
858
- })
859
- ),
860
- // 分块延迟
861
- React.createElement(
862
- "div",
863
- null,
864
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5206\u5757\u53D1\u9001\u5EF6\u8FDF (\u6BEB\u79D2)"),
865
- React.createElement("input", {
866
- style: { ...s.input, width: "100%" },
867
- type: "number",
868
- min: 0,
869
- max: 1e4,
870
- value: cfgDraft.sendChunkDelayMs,
871
- onChange: (e) => setCfgDraft((d) => ({ ...d, sendChunkDelayMs: e.target.value }))
872
- })
873
- )
874
- ),
875
- // QQ 平台凭证
876
- platformId === "qq" && React.createElement(
877
- "div",
878
- { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4 } },
879
- React.createElement(
880
- "div",
881
- null,
882
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "AppID \u2014 QQ \u5F00\u653E\u5E73\u53F0\u673A\u5668\u4EBA\u5E94\u7528 ID"),
883
- React.createElement("input", {
884
- style: { ...s.input, width: "100%" },
885
- placeholder: "\u8BF7\u8F93\u5165 AppID",
886
- value: cfgDraft.appId,
887
- onChange: (e) => setCfgDraft((d) => ({ ...d, appId: e.target.value }))
888
- })
889
- ),
890
- React.createElement(
891
- "div",
892
- null,
893
- React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "ClientSecret \u2014 QQ \u5F00\u653E\u5E73\u53F0\u673A\u5668\u4EBA\u5BC6\u94A5\uFF08\u7559\u7A7A\u5219\u4FDD\u6301\u5DF2\u4FDD\u5B58\u5BC6\u94A5\uFF09"),
894
- React.createElement(
895
- "div",
896
- { style: { display: "flex", gap: 6, alignItems: "center" } },
897
- React.createElement("input", {
898
- style: { ...s.input, flex: 1 },
899
- type: showSecret ? "text" : "password",
900
- placeholder: "\u7559\u7A7A\u4FDD\u6301\u5DF2\u4FDD\u5B58\u5BC6\u94A5",
901
- value: cfgDraft.clientSecret,
902
- onChange: (e) => setCfgDraft((d) => ({ ...d, clientSecret: e.target.value }))
903
- }),
904
- React.createElement("button", {
905
- style: { ...s.btnGhost, height: 32, padding: "0 10px", fontSize: 13, flexShrink: 0 },
906
- onClick: () => setShowSecret((v) => !v),
907
- type: "button",
908
- title: showSecret ? "\u9690\u85CF\u5BC6\u94A5" : "\u663E\u793A\u660E\u6587"
909
- }, showSecret ? "\u{1F648} \u9690\u85CF" : "\u{1F441}\uFE0F \u663E\u793A")
910
- )
911
- )
912
- ),
913
- React.createElement(
914
- "div",
915
- { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 4 } },
916
- React.createElement("button", {
917
- style: { ...s.btnPri, opacity: cfgDirty && !busy ? 1 : 0.5 },
918
- disabled: !cfgDirty || busy,
919
- onClick: saveConfig
920
- }, busy ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u8BBE\u7F6E"),
921
- React.createElement("button", {
922
- style: { ...s.btnGhost, height: 32, fontSize: 12 },
923
- onClick: resetDefaults,
924
- title: "\u6062\u590D\u63A8\u8350\u9ED8\u8BA4\u914D\u7F6E"
925
- }, "\u21BA \u6062\u590D\u63A8\u8350\u9ED8\u8BA4")
926
- )
927
- )
928
- ),
929
817
  React.createElement(
930
818
  "div",
931
819
  { style: s.block },
@@ -1365,7 +1253,7 @@ function BridgePanel({ rpcCall }) {
1365
1253
  const IM_PLATFORMS = [
1366
1254
  { id: "wechat", label: "\u5FAE\u4FE1", icon: Icons.wechat, brandColor: "#07C160", desc: "ClawBot \u626B\u7801\u76F4\u8FDE \xB7 \u65E0\u9700\u516C\u7F51" },
1367
1255
  { id: "qq", label: "QQ", icon: Icons.qq, brandColor: "#12B7F5", desc: "\u5B98\u65B9\u673A\u5668\u4EBA \xB7 \u79C1\u804A/\u7FA4\u804A/\u6309\u94AE" },
1368
- { id: "feishu", label: "\u98DE\u4E66", icon: Icons.feishu, brandColor: "#00D6B9", desc: "\u5B98\u65B9\u4E8B\u4EF6\u56DE\u8C03 API" },
1256
+ { id: "feishu", label: "\u98DE\u4E66", icon: Icons.feishu, brandColor: "#00D6B9", desc: "\u5B98\u65B9 WebSocket \u957F\u8FDE\u63A5 \xB7 \u514D\u516C\u7F51" },
1369
1257
  { id: "telegram", label: "Telegram", icon: Icons.telegram, brandColor: "#24A1DE", desc: "\u5B98\u65B9 Bot API" }
1370
1258
  ];
1371
1259
  tabContent = React.createElement(
package/client/index.js CHANGED
@@ -348,6 +348,8 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
348
348
  appId: platform.config.appId ?? '',
349
349
  // Secret 不由后端回传;空值表示沿用已保存密钥
350
350
  clientSecret: '',
351
+ appSecret: '',
352
+ domain: platform.config.domain ?? 'feishu',
351
353
  });
352
354
  }
353
355
  }, [platform?.config]);
@@ -436,10 +438,14 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
436
438
  maxMessageChars: Number(cfgDraft.maxMessageChars),
437
439
  sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs),
438
440
  };
439
- // QQ 平台额外携带凭证
441
+ // QQ / 飞书 平台额外携带凭证
440
442
  if (platformId === 'qq') {
441
443
  payload.appId = cfgDraft.appId.trim();
442
444
  payload.clientSecret = cfgDraft.clientSecret.trim();
445
+ } else if (platformId === 'feishu') {
446
+ payload.appId = cfgDraft.appId.trim();
447
+ payload.appSecret = cfgDraft.appSecret.trim();
448
+ payload.domain = cfgDraft.domain || 'feishu';
443
449
  }
444
450
  await act(BRIDGE_ENDPOINTS.platformSetConfig, payload);
445
451
  }, [act, cfgDraft, platformId]);
@@ -451,6 +457,10 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
451
457
  (platformId === 'qq' && (
452
458
  cfgDraft.appId !== (platform.config.appId ?? '') ||
453
459
  cfgDraft.clientSecret !== (platform.config.clientSecret ?? '')
460
+ )) ||
461
+ (platformId === 'feishu' && (
462
+ cfgDraft.appId !== (platform.config.appId ?? '') ||
463
+ cfgDraft.appSecret !== (platform.config.appSecret ?? '')
454
464
  ))
455
465
  );
456
466
 
@@ -503,6 +513,16 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
503
513
  target: '_blank', rel: 'noopener noreferrer',
504
514
  style: s.btnGhost,
505
515
  }, '🌐 QQ 开放平台'),
516
+ platformId === 'feishu' && React.createElement('a', {
517
+ href: 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/feishu-usage.md',
518
+ target: '_blank', rel: 'noopener noreferrer',
519
+ style: s.btnGhost,
520
+ }, '📖 飞书使用说明'),
521
+ platformId === 'feishu' && React.createElement('a', {
522
+ href: 'https://open.feishu.cn/app',
523
+ target: '_blank', rel: 'noopener noreferrer',
524
+ style: s.btnGhost,
525
+ }, '🌐 飞书开放平台'),
506
526
  React.createElement('button', {
507
527
  style: s.btnGhost,
508
528
  onClick: () => setShowHelp(v => !v),
@@ -613,19 +633,19 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
613
633
  ),
614
634
  React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' } },
615
635
  platform.status !== 'connected' && platform.status !== 'starting' &&
616
- React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '重新登录'),
636
+ React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '重新连接'),
617
637
  (platform.status === 'connected' || platform.status === 'starting') &&
618
638
  React.createElement('button', { style: s.btnGhost, onClick: onStop, disabled: busy }, '断开'),
619
639
  React.createElement('button', {
620
640
  style: { ...s.btnGhost, color: 'var(--dsw-alias-state-error-primary,#dc2626)', borderColor: 'var(--dsw-alias-state-error-primary,#dc2626)', opacity: busy ? 0.5 : 1 },
621
641
  disabled: busy,
622
- onClick: () => { if (window.confirm('确认解绑?这将清除登录凭证,下次需重新登录。')) act(BRIDGE_ENDPOINTS.platformUnbind, {}); },
623
- title: '清除登录凭证,下次需重新登录',
642
+ onClick: () => { if (window.confirm('确认解绑?这将清除保存的凭证。')) act(BRIDGE_ENDPOINTS.platformUnbind, {}); },
643
+ title: '清除登录凭证,下次需重新配置',
624
644
  }, '解绑账号'),
625
645
  ),
626
646
  ),
627
647
 
628
- // 未配置 / 登录中:二维码(QQ 无二维码,显示凭证表单)
648
+ // 未配置 / 登录中:表单(QQ / 飞书)或二维码(微信)
629
649
  (!platform?.configured || showQr) && React.createElement('div', { style: s.block },
630
650
  showQr && login.qr
631
651
  ? React.createElement('div', null,
@@ -637,28 +657,30 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
637
657
  ),
638
658
  login.error && React.createElement('div', { style: { ...s.muted, marginTop: 4, color: 'var(--dsw-alias-state-warn-primary,#92400e)' } }, login.error),
639
659
  )
640
- : platformId === 'qq'
660
+ : (platformId === 'qq' || platformId === 'feishu')
641
661
  ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 4 } },
642
662
  React.createElement('div', null,
643
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'AppID — QQ 开放平台机器人应用 ID'),
663
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
664
+ platformId === 'qq' ? 'AppID — QQ 开放平台机器人应用 ID' : 'App ID — 飞书开放平台自建应用 ID (cli_xxx)'
665
+ ),
644
666
  React.createElement('input', {
645
667
  style: { ...s.input, width: '100%' },
646
- placeholder: '请输入 AppID',
668
+ placeholder: platformId === 'qq' ? '请输入 AppID' : '请输入 App ID (如 cli_a1b2c3d4...)',
647
669
  value: cfgDraft?.appId ?? '',
648
670
  onChange: (e) => setCfgDraft(d => ({ ...d, appId: e.target.value })),
649
- onKeyDown: (e) => { if (e.key === 'Enter' && cfgDraft?.appId?.trim() && cfgDraft?.clientSecret?.trim() && !busy) saveConfig(); },
650
671
  }),
651
672
  ),
652
673
  React.createElement('div', null,
653
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'ClientSecret — QQ 开放平台机器人密钥'),
674
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
675
+ platformId === 'qq' ? 'ClientSecret — QQ 开放平台机器人密钥' : 'App Secret — 飞书开放平台应用密钥'
676
+ ),
654
677
  React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
655
678
  React.createElement('input', {
656
679
  style: { ...s.input, flex: 1 },
657
680
  type: showSecret ? 'text' : 'password',
658
- placeholder: '请输入 ClientSecret',
659
- value: cfgDraft?.clientSecret ?? '',
660
- onChange: (e) => setCfgDraft(d => ({ ...d, clientSecret: e.target.value })),
661
- onKeyDown: (e) => { if (e.key === 'Enter' && cfgDraft?.appId?.trim() && cfgDraft?.clientSecret?.trim() && !busy) saveConfig(); },
681
+ placeholder: platformId === 'qq' ? '请输入 ClientSecret' : '请输入 App Secret',
682
+ value: platformId === 'qq' ? (cfgDraft?.clientSecret ?? '') : (cfgDraft?.appSecret ?? ''),
683
+ onChange: (e) => setCfgDraft(d => platformId === 'qq' ? ({ ...d, clientSecret: e.target.value }) : ({ ...d, appSecret: e.target.value })),
662
684
  }),
663
685
  React.createElement('button', {
664
686
  style: { ...s.btnGhost, height: 32, padding: '0 10px', fontSize: 13, flexShrink: 0 },
@@ -670,15 +692,16 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
670
692
  ),
671
693
  React.createElement('div', null,
672
694
  React.createElement('a', {
673
- href: 'https://bot.q.qq.com/wiki/develop/api-v2/',
695
+ href: platformId === 'qq' ? 'https://bot.q.qq.com/wiki/develop/api-v2/' : 'https://open.feishu.cn/app',
674
696
  target: '_blank', rel: 'noopener noreferrer',
675
697
  style: s.btnLink,
676
- }, '📖 前往 QQ 开放平台申请机器人'),
698
+ }, platformId === 'qq' ? '📖 前往 QQ 开放平台申请机器人' : '📖 前往飞书开放平台创建企业自建应用'),
677
699
  ),
678
700
  React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
679
701
  React.createElement('button', {
680
702
  style: { ...s.btnPri, opacity: busy ? 0.5 : 1 },
681
- onClick: saveConfig, disabled: busy || !cfgDraft?.appId?.trim() || !cfgDraft?.clientSecret?.trim(),
703
+ onClick: saveConfig,
704
+ disabled: busy || !cfgDraft?.appId?.trim() || (platformId === 'qq' ? !cfgDraft?.clientSecret?.trim() : !cfgDraft?.appSecret?.trim()),
682
705
  }, busy ? '保存中…' : '保存并连接'),
683
706
  login.phase === 'error' && React.createElement('div', { style: { ...s.muted, fontSize: 12 } }, login.error ?? '连接失败'),
684
707
  ),
@@ -692,106 +715,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
692
715
  ),
693
716
  ),
694
717
 
695
- // 高级设置(紧凑 2x2 网格 + 恢复默认值)
696
- cfgDraft && React.createElement('div', { style: s.block },
697
- React.createElement('button', {
698
- style: { ...s.btnLink, fontSize: 12, marginBottom: showAdvanced ? 10 : 0 },
699
- onClick: () => setShowAdvanced(v => !v),
700
- }, showAdvanced ? '▾ 高级设置' : '▸ 高级设置'),
701
- showAdvanced && React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
702
- // 2x2 参数网格
703
- React.createElement('div', {
704
- style: {
705
- display: 'grid',
706
- gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
707
- gap: 10,
708
- },
709
- },
710
- // 心跳间隔
711
- React.createElement('div', null,
712
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '心跳进度间隔 (秒)'),
713
- React.createElement('input', {
714
- style: { ...s.input, width: '100%' },
715
- type: 'number', min: 30, max: 3600,
716
- value: cfgDraft.digestIntervalSec,
717
- onChange: (e) => setCfgDraft(d => ({ ...d, digestIntervalSec: e.target.value })),
718
- }),
719
- ),
720
- // 审批超时
721
- React.createElement('div', null,
722
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '审批超时自动拒绝 (秒)'),
723
- React.createElement('input', {
724
- style: { ...s.input, width: '100%' },
725
- type: 'number', min: 30, max: 86400,
726
- value: cfgDraft.approvalTimeoutSec,
727
- onChange: (e) => setCfgDraft(d => ({ ...d, approvalTimeoutSec: e.target.value })),
728
- }),
729
- ),
730
- // 每气泡字数
731
- React.createElement('div', null,
732
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '单条最大字数限制 (字)'),
733
- React.createElement('input', {
734
- style: { ...s.input, width: '100%' },
735
- type: 'number', min: 100, max: 10000,
736
- value: cfgDraft.maxMessageChars,
737
- onChange: (e) => setCfgDraft(d => ({ ...d, maxMessageChars: e.target.value })),
738
- }),
739
- ),
740
- // 分块延迟
741
- React.createElement('div', null,
742
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '分块发送延迟 (毫秒)'),
743
- React.createElement('input', {
744
- style: { ...s.input, width: '100%' },
745
- type: 'number', min: 0, max: 10000,
746
- value: cfgDraft.sendChunkDelayMs,
747
- onChange: (e) => setCfgDraft(d => ({ ...d, sendChunkDelayMs: e.target.value })),
748
- }),
749
- ),
750
- ),
751
- // QQ 平台凭证
752
- platformId === 'qq' && React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 4 } },
753
- React.createElement('div', null,
754
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'AppID — QQ 开放平台机器人应用 ID'),
755
- React.createElement('input', {
756
- style: { ...s.input, width: '100%' },
757
- placeholder: '请输入 AppID',
758
- value: cfgDraft.appId,
759
- onChange: (e) => setCfgDraft(d => ({ ...d, appId: e.target.value })),
760
- }),
761
- ),
762
- React.createElement('div', null,
763
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'ClientSecret — QQ 开放平台机器人密钥(留空则保持已保存密钥)'),
764
- React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
765
- React.createElement('input', {
766
- style: { ...s.input, flex: 1 },
767
- type: showSecret ? 'text' : 'password',
768
- placeholder: '留空保持已保存密钥',
769
- value: cfgDraft.clientSecret,
770
- onChange: (e) => setCfgDraft(d => ({ ...d, clientSecret: e.target.value })),
771
- }),
772
- React.createElement('button', {
773
- style: { ...s.btnGhost, height: 32, padding: '0 10px', fontSize: 13, flexShrink: 0 },
774
- onClick: () => setShowSecret(v => !v),
775
- type: 'button',
776
- title: showSecret ? '隐藏密钥' : '显示明文',
777
- }, showSecret ? '🙈 隐藏' : '👁️ 显示'),
778
- ),
779
- ),
780
- ),
781
- React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 4 } },
782
- React.createElement('button', {
783
- style: { ...s.btnPri, opacity: (cfgDirty && !busy) ? 1 : 0.5 },
784
- disabled: !cfgDirty || busy,
785
- onClick: saveConfig,
786
- }, busy ? '保存中…' : '保存设置'),
787
- React.createElement('button', {
788
- style: { ...s.btnGhost, height: 32, fontSize: 12 },
789
- onClick: resetDefaults,
790
- title: '恢复推荐默认配置',
791
- }, '↺ 恢复推荐默认'),
792
- ),
793
- ),
794
- ),
718
+
795
719
 
796
720
  React.createElement('div', { style: s.block },
797
721
  React.createElement('div', { style: { ...s.tip, fontSize: 12 } },
@@ -1221,7 +1145,7 @@ function BridgePanel({ rpcCall }) {
1221
1145
  const IM_PLATFORMS = [
1222
1146
  { id: 'wechat', label: '微信', icon: Icons.wechat, brandColor: '#07C160', desc: 'ClawBot 扫码直连 · 无需公网' },
1223
1147
  { id: 'qq', label: 'QQ', icon: Icons.qq, brandColor: '#12B7F5', desc: '官方机器人 · 私聊/群聊/按钮' },
1224
- { id: 'feishu', label: '飞书', icon: Icons.feishu, brandColor: '#00D6B9', desc: '官方事件回调 API' },
1148
+ { id: 'feishu', label: '飞书', icon: Icons.feishu, brandColor: '#00D6B9', desc: '官方 WebSocket 长连接 · 免公网' },
1225
1149
  { id: 'telegram', label: 'Telegram', icon: Icons.telegram, brandColor: '#24A1DE', desc: '官方 Bot API' },
1226
1150
  ];
1227
1151