@xmanrui/dsh-im 0.2.1 → 0.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 (39) hide show
  1. package/README.md +19 -11
  2. package/lib/client.js +1847 -1145
  3. package/lib/index.js +116 -115
  4. package/package.json +2 -2
  5. package/plugin-src/client/channel-logos.js +13 -0
  6. package/plugin-src/client/channels/dingtalk/index.js +1 -1
  7. package/plugin-src/client/channels/dingtalk/styles.js +8 -8
  8. package/plugin-src/client/channels/feishu/index.js +1 -2
  9. package/plugin-src/client/channels/feishu/styles.js +3 -3
  10. package/plugin-src/client/channels/qq/index.js +1 -1
  11. package/plugin-src/client/channels/shared/token-channel.js +1 -2
  12. package/plugin-src/client/channels/slack/api.js +11 -0
  13. package/plugin-src/client/channels/slack/index.js +130 -0
  14. package/plugin-src/client/channels/slack/styles.js +34 -0
  15. package/plugin-src/client/channels/wecom/index.js +1 -1
  16. package/plugin-src/client/channels/wecom/styles.js +1 -1
  17. package/plugin-src/client/channels/weixin/index.js +1 -2
  18. package/plugin-src/client/channels/weixin/styles.js +9 -9
  19. package/plugin-src/client/channels/whatsapp/index.js +1 -1
  20. package/plugin-src/client/credential-binding.js +1 -2
  21. package/plugin-src/client/i18n.js +443 -0
  22. package/plugin-src/client/index.js +29 -4
  23. package/plugin-src/client/styles.js +46 -38
  24. package/plugin-src/host/channels/shared/production.mjs +2 -2
  25. package/plugin-src/host/channels/slack/index.mjs +28 -0
  26. package/plugin-src/host/channels/slack/production.mjs +91 -0
  27. package/plugin-src/host/channels/slack/rpc.mjs +127 -0
  28. package/plugin-src/host/index.mjs +3 -0
  29. package/scripts/verify-package.mjs +10 -3
  30. package/src/channels/discord/discord-api.mjs +1 -1
  31. package/src/channels/slack/config-store.mjs +173 -0
  32. package/src/channels/slack/harness-client.mjs +3 -0
  33. package/src/channels/slack/manifest.mjs +31 -0
  34. package/src/channels/slack/slack-api.mjs +259 -0
  35. package/src/channels/slack/slack-bridge.mjs +15 -0
  36. package/src/channels/slack/slack-controller.mjs +318 -0
  37. package/src/channels/slack/slack-runtime.mjs +462 -0
  38. package/src/channels/slack/state-store.mjs +3 -0
  39. package/src/channels/weixin/weixin-api.mjs +1 -1
@@ -2,7 +2,7 @@ export const IM_STYLE_ID = 'xmanrui-dsh-im-settings';
2
2
 
3
3
  const CSS = String.raw`
4
4
  .dim-page {
5
- --dim-blue: #3370ff;
5
+ --dim-blue: var(--dsw-alias-state-business-primary, #3370ff);
6
6
  --dim-blue-soft: color-mix(in srgb, var(--dim-blue) 9%, transparent);
7
7
  width: 100%;
8
8
  max-width: 1080px;
@@ -14,48 +14,51 @@ const CSS = String.raw`
14
14
  .dim-title { margin: 0 0 26px; }
15
15
  .dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; white-space: nowrap; }
16
16
  .dim-layout { display: grid; grid-template-columns: 174px 1px minmax(0, 1fr); gap: 24px; align-items: start; }
17
- .dim-rail { max-height: 520px; display: grid; align-content: start; gap: 8px; overflow-y: auto; padding: 1px 4px 1px 1px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-line-border, #dfe1e5) transparent; }
17
+ .dim-rail { max-height: 520px; display: grid; align-content: start; gap: 8px; overflow-y: auto; padding: 1px 4px 1px 1px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
18
18
  .dim-rail::-webkit-scrollbar { width: 4px; }
19
- .dim-rail::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-line-border, #dfe1e5); }
20
- .dim-channel { width: 100%; min-height: 48px; display: grid; grid-template-columns: 30px minmax(0, 1fr); align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--dsw-alias-line-border, #eef0f3); border-radius: 14px; color: inherit; background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 2px 8px rgb(31 35 41 / 3%); font: inherit; text-align: left; cursor: pointer; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; }
21
- .dim-channel:hover { border-color: color-mix(in srgb, var(--dim-blue) 25%, var(--dsw-alias-line-border, #eef0f3)); background: color-mix(in srgb, var(--dim-blue) 2%, var(--dsw-alias-bg-layer-3, #fff)); box-shadow: 0 5px 16px rgb(31 35 41 / 5%); }
22
- .dim-channel[aria-selected="true"] { border-color: color-mix(in srgb, var(--dim-blue) 43%, white); color: var(--dim-blue); background: color-mix(in srgb, var(--dim-blue) 9%, white); box-shadow: 0 3px 12px rgb(51 112 255 / 7%); }
23
- .dim-channel:focus-visible { outline: none; border-color: color-mix(in srgb, var(--dim-blue) 72%, white); box-shadow: 0 0 0 1px color-mix(in srgb, var(--dim-blue) 24%, transparent) inset, 0 3px 12px rgb(51 112 255 / 7%); }
19
+ .dim-rail::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-l2, #dfe1e5); }
20
+ .dim-channel { width: 100%; min-height: 48px; display: grid; grid-template-columns: 30px minmax(0, 1fr); align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l2, #eef0f3); border-radius: 14px; color: inherit; background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 2px 8px rgb(31 35 41 / 3%); font: inherit; text-align: left; cursor: pointer; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; }
21
+ .dim-channel:hover { border-color: color-mix(in srgb, var(--dim-blue) 25%, var(--dsw-alias-border-l2, #eef0f3)); background: color-mix(in srgb, var(--dim-blue) 2%, var(--dsw-alias-bg-layer-3, #fff)); box-shadow: 0 5px 16px rgb(31 35 41 / 5%); }
22
+ .dim-channel[aria-selected="true"] { border-color: color-mix(in srgb, var(--dim-blue) 43%, var(--dsw-alias-border-l2, #dfe1e5)); color: var(--dim-blue); background: color-mix(in srgb, var(--dim-blue) 12%, var(--dsw-alias-bg-layer-3, #fff)); box-shadow: 0 3px 12px rgb(51 112 255 / 7%); }
23
+ .dim-channel:focus-visible { outline: none; border-color: color-mix(in srgb, var(--dim-blue) 72%, var(--dsw-alias-border-l2, #dfe1e5)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--dim-blue) 24%, transparent) inset, 0 3px 12px rgb(51 112 255 / 7%); }
24
24
  .dim-logo { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px; box-shadow: 0 1px 3px rgb(31 35 41 / 7%); }
25
25
  .dim-logo svg { display: block; width: 20px; height: 20px; }
26
26
  .dim-logoWeixin { color: white; background: #07c160; }
27
27
  .dim-logoWeixin svg { width: 19px; height: 19px; }
28
- .dim-logoFeishu { background: white; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); }
28
+ .dim-logoFeishu { background: white; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); }
29
29
  .dim-logoFeishu svg { width: 28px; height: 28px; }
30
30
  .dim-logoDingtalk { color: white; background: #1677ff; }
31
31
  .dim-logoDingtalk svg { width: 24px; height: 24px; }
32
32
  .dim-logoQq { color: white; background: #1677ff; }
33
33
  .dim-logoQq svg { width: 21px; height: 21px; }
34
- .dim-logoWecom { background: white; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); }
34
+ .dim-logoWecom { background: white; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); }
35
35
  .dim-logoWecom svg { width: 22px; height: 22px; }
36
36
  .dim-logoTelegram { color: white; background: #229ed9; }
37
37
  .dim-logoTelegram svg { width: 21px; height: 21px; }
38
38
  .dim-logoDiscord { color: white; background: #5865f2; }
39
39
  .dim-logoDiscord svg { width: 21px; height: 21px; }
40
+ .dim-logoSlack { color: white; background: #4a154b; }
41
+ .dim-logoSlack svg { width: 21px; height: 21px; }
40
42
  .dim-logoWhatsapp { color: white; background: #25d366; }
41
43
  .dim-logoWhatsapp svg { width: 21px; height: 21px; }
42
44
  .dim-channelCopy { min-width: 0; display: block; }
43
45
  .dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
44
- .dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-line-divider, #eef0f3); }
46
+ .dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-border-l1, #eef0f3); }
45
47
  .dim-panel { min-width: 0; container-type: inline-size; }
46
- .dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
48
+ .dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dsl-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
47
49
  .dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
48
- .dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: space-between; }
50
+ .dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; justify-content: stretch; gap: 8px; }
49
51
  .dim-panel .dim-bindActions { min-width: 0; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; }
50
- .dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; }
52
+ .dim-panel .dim-bindActions > button { min-width: 0; }
53
+ .dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; justify-self: start; gap: 6px; padding: 0 10px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; }
51
54
  .dim-panel .bxf-headingTools .dim-scanButton:hover:not(:disabled), .dim-panel .dxw-tools .dim-scanButton:hover:not(:disabled), .dim-panel .ddt-tools .dim-scanButton:hover:not(:disabled) { border-color: #0958d9; background: #0958d9; }
52
- .dim-panel .dim-credentialButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 1px solid #86909c; border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 5%); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
55
+ .dim-panel .dim-credentialButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 1px solid #86909c; border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 5%); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
53
56
  .dim-panel .dim-actionIcon { width: 15px; height: 15px; flex: 0 0 15px; }
54
- .dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
55
- .dim-panel .dim-credentialButton[aria-pressed="true"] { border-color: #4e5969; background: var(--dsw-alias-fill-secondary, #f2f3f5); box-shadow: inset 0 0 0 1px rgb(78 89 105 / 8%); }
56
- .dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { flex: none; min-height: 30px; display: inline-flex; align-items: center; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-fill-secondary, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
57
+ .dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
58
+ .dim-panel .dim-credentialButton[aria-pressed="true"] { border-color: #4e5969; background: var(--dsw-alias-bg-module-platform, #f2f3f5); box-shadow: inset 0 0 0 1px rgb(78 89 105 / 8%); }
59
+ .dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { min-height: 30px; display: inline-flex; align-items: center; justify-self: end; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
57
60
  .dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap: 18px; padding: 0 0 24px; color: var(--dsw-alias-label-primary, #1f2329); box-sizing: border-box; }
58
- .dim-panel .dim-surfaceCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
61
+ .dim-panel .dim-surfaceCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
59
62
  .dim-panel .dim-surfaceCard::before { display: none; }
60
63
  .dim-panel .dim-surfaceBody { padding: 24px; }
61
64
  .dim-panel .dim-credentialPanel { display: grid; gap: 18px; padding: 20px; }
@@ -63,7 +66,7 @@ const CSS = String.raw`
63
66
  .dim-panel .dim-credentialForm { min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; }
64
67
  .dim-panel .dim-credentialFormSingle { grid-template-columns: minmax(0, 1fr); }
65
68
  .dim-panel .dim-credentialField { min-width: 0; display: grid; gap: 7px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; font-weight: 560; }
66
- .dim-panel .dim-credentialField input { width: 100%; min-width: 0; height: 38px; padding: 0 11px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; transition: border-color .16s ease, box-shadow .16s ease; }
69
+ .dim-panel .dim-credentialField input { width: 100%; min-width: 0; height: 38px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; transition: border-color .16s ease, box-shadow .16s ease; }
67
70
  .dim-panel .dim-credentialField input:focus { border-color: #4e5969; box-shadow: 0 0 0 3px rgb(78 89 105 / 10%); }
68
71
  .dim-panel .dim-credentialField input::placeholder { color: var(--dsw-alias-label-tertiary, #8f959e); font-family: inherit; }
69
72
  .dim-panel .dim-credentialError, .dim-panel .dim-credentialActions { grid-column: 1 / -1; }
@@ -76,7 +79,7 @@ const CSS = String.raw`
76
79
  .dim-panel .dim-loadingView { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
77
80
  .dim-panel .dim-loadingView h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: normal; font-weight: 650; }
78
81
  .dim-panel .dim-loadingView p { margin: 0; line-height: 1.6; }
79
- .dim-panel .dim-spinner { width: 24px; height: 24px; margin: 0 auto 13px; border: 3px solid var(--dsw-alias-line-border, #e6e8eb); border-top-color: #1677ff; border-radius: 50%; animation: dim-spin .8s linear infinite; }
82
+ .dim-panel .dim-spinner { width: 24px; height: 24px; margin: 0 auto 13px; border: 3px solid var(--dsw-alias-border-l2, #e6e8eb); border-top-color: #1677ff; border-radius: 50%; animation: dim-spin .8s linear infinite; }
80
83
  @keyframes dim-spin { to { transform: rotate(360deg); } }
81
84
  .dim-panel .dim-emptyView { min-height: 230px; display: grid; grid-template-columns: minmax(0, 1fr) 180px; align-items: center; gap: 30px; }
82
85
  .dim-panel .dim-emptyCopy { min-width: 0; }
@@ -86,49 +89,49 @@ const CSS = String.raw`
86
89
  .dim-panel .dim-stateLabel { display: inline-flex; align-items: center; gap: 8px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; font-weight: 600; }
87
90
  .dim-panel .dim-stateDot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #8f959e); box-shadow: none; }
88
91
  .dim-panel .dim-stateDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); }
89
- .dim-panel .dim-stateDot[data-tone="warning"] { background: var(--dsw-alias-state-warning-primary, #d97706); }
92
+ .dim-panel .dim-stateDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
90
93
  .dim-panel .dim-stateDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
91
94
  .dim-panel .dim-viewActions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
92
- .dim-panel .dim-viewActions .bxf-button, .dim-panel .dim-viewActions .dxw-button, .dim-panel .dim-viewActions .ddt-button { min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
95
+ .dim-panel .dim-viewActions .bxf-button, .dim-panel .dim-viewActions .dxw-button, .dim-panel .dim-viewActions .ddt-button { min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
93
96
  .dim-panel .dim-viewActions .bxf-button[data-kind="primary"], .dim-panel .dim-viewActions .dxw-button[data-kind="primary"], .dim-panel .dim-viewActions .ddt-button[data-kind="primary"] { border-color: #1677ff; color: #fff; background: #1677ff; box-shadow: none; }
94
97
  .dim-panel .dim-viewActions .bxf-button[data-kind="danger"], .dim-panel .dim-viewActions .dxw-button[data-kind="danger"], .dim-panel .dim-viewActions .ddt-button[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
95
98
  .dim-panel .dim-qrLayout { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 34px; align-items: start; }
96
99
  .dim-panel .dim-qrColumn { width: 100%; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 12px; }
97
- .dim-panel .dim-qrFrame { position: relative; width: min(270px, 100%); height: auto; aspect-ratio: 1; display: grid; place-items: center; overflow: hidden; padding: 10px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 16px; background: #fff; }
98
- .dim-panel .dim-qrFrame::before { content: ""; position: absolute; inset: 7px; z-index: 0; border: 1px solid color-mix(in srgb, #1677ff 16%, var(--dsw-alias-line-border, #dfe1e5)); border-radius: 12px; pointer-events: none; }
100
+ .dim-panel .dim-qrFrame { position: relative; width: min(270px, 100%); height: auto; aspect-ratio: 1; display: grid; place-items: center; overflow: hidden; padding: 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 16px; background: #fff; }
101
+ .dim-panel .dim-qrFrame::before { content: ""; position: absolute; inset: 7px; z-index: 0; border: 1px solid color-mix(in srgb, #1677ff 16%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 12px; pointer-events: none; }
99
102
  .dim-panel .dim-qrFrame::after { display: none; }
100
103
  .dim-panel .dim-qrFrame img { position: relative; z-index: 1; width: 100%; height: 100%; display: block; object-fit: contain; }
101
104
  .dim-panel .dim-qrFallback { position: relative; z-index: 1; display: grid; place-items: center; gap: 8px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 1.5; text-align: center; }
102
- .dim-panel .dim-qrExpired { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; padding: 20px; color: var(--dsw-alias-label-primary, #1f2329); background: rgb(255 255 255 / 92%); font-size: 15px; line-height: 1.6; font-weight: 650; text-align: center; white-space: pre-line; backdrop-filter: blur(3px); }
105
+ .dim-panel .dim-qrExpired { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; padding: 20px; color: var(--dsw-static-neutral-bluish-1000, #0f1115); background: rgb(255 255 255 / 92%); font-size: 15px; line-height: 1.6; font-weight: 650; text-align: center; white-space: pre-line; backdrop-filter: blur(3px); }
103
106
  .dim-panel .dim-countdown { width: min(270px, 100%); margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
104
107
  .dim-panel .dim-countdownTop { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
105
108
  .dim-panel .dim-countdownTop strong { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 650; }
106
- .dim-panel .dim-progress { height: 4px; overflow: hidden; margin: 0; border-radius: 99px; background: var(--dsw-alias-fill-secondary, #eef0f3); }
109
+ .dim-panel .dim-progress { height: 4px; overflow: hidden; margin: 0; border-radius: 99px; background: var(--dsw-alias-bg-module-platform, #eef0f3); }
107
110
  .dim-panel .dim-progress span { display: block; width: var(--bxf-progress, var(--dxw-progress, var(--ddt-progress, 0%))); height: 100%; border-radius: inherit; background: #1677ff; transition: width .25s linear; }
108
111
  .dim-panel .dim-qrCopy { min-width: 0; overflow-wrap: anywhere; }
109
112
  .dim-panel .dim-qrCopy h3 { margin: 9px 0 8px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 18px; line-height: 1.35; font-weight: 650; }
110
113
  .dim-panel .dim-qrCopy > p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.65; }
111
114
  .dim-panel .dim-steps { margin: 18px 0 16px; padding: 0; list-style: none; counter-reset: dim-step; }
112
115
  .dim-panel .dim-steps li { position: relative; min-height: 28px; display: flex; align-items: center; padding: 5px 0 5px 36px; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.5; counter-increment: dim-step; }
113
- .dim-panel .dim-steps li::before { content: counter(dim-step); position: absolute; left: 0; top: 4px; width: 25px; height: 25px; display: grid; place-items: center; border-radius: 8px; color: #1677ff; background: color-mix(in srgb, #1677ff 10%, white); font-size: 12px; font-weight: 650; }
116
+ .dim-panel .dim-steps li::before { content: counter(dim-step); position: absolute; left: 0; top: 4px; width: 25px; height: 25px; display: grid; place-items: center; border-radius: 8px; color: #4d93f8; background: color-mix(in srgb, #1677ff 16%, var(--dsw-alias-bg-layer-1, #fff)); font-size: 12px; font-weight: 650; }
114
117
  .dim-panel .dim-specialView { padding: 32px; text-align: center; }
115
- .dim-panel .dim-statusNotice { display: flex; align-items: flex-start; gap: 10px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 22%, white); border-radius: 10px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 6%, white); font-size: 13px; line-height: 1.5; }
116
- .dim-panel .dim-inlineError { display: flex; align-items: flex-start; flex-direction: column; gap: 10px; padding: 22px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 6%, white); }
118
+ .dim-panel .dim-statusNotice { display: flex; align-items: flex-start; gap: 10px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 22%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 10px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 8%, var(--dsw-alias-bg-layer-1, #fff)); font-size: 13px; line-height: 1.5; }
119
+ .dim-panel .dim-inlineError { display: flex; align-items: flex-start; flex-direction: column; gap: 10px; padding: 22px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 8%, var(--dsw-alias-bg-layer-1, #fff)); }
117
120
  .dim-panel .dim-inlineError > div { min-width: 0; }
118
121
  .dim-panel .dim-inlineError h3 { margin: 0; color: inherit; font-size: 17px; line-height: 1.35; font-weight: 650; }
119
122
  .dim-panel .dim-inlineError p { margin: 7px 0 0; color: inherit; line-height: 1.6; }
120
- .dim-panel .dim-confirm { padding: 18px 24px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
123
+ .dim-panel .dim-confirm { padding: 18px 24px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
121
124
  .dim-panel .dim-confirm strong, .dim-panel .dim-confirm h4 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 1.4; font-weight: 650; }
122
125
  .dim-panel .dim-confirm p { margin: 7px 0 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.6; }
123
- .dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); }
126
+ .dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
124
127
  .dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
125
128
  .dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
126
- .dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
127
- .dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
129
+ .dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
130
+ .dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
128
131
  .dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
129
- .dim-panel .dim-botCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
132
+ .dim-panel .dim-botCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
130
133
  .dim-panel .dim-botCard::before { display: none; }
131
- .dim-panel .dim-botCardBody { position: relative; padding: 24px; }
134
+ .dim-panel .dim-botCardBody { position: relative; padding: 16px; }
132
135
  .dim-panel .dim-botCardTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
133
136
  .dim-panel .dim-botIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; }
134
137
  .dim-panel .dim-botAvatar { flex: none; width: 42px; height: 42px; display: grid; place-items: center; overflow: hidden; border-radius: 12px; box-shadow: none; }
@@ -139,16 +142,21 @@ const CSS = String.raw`
139
142
  .dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
140
143
  .dim-panel .dim-botCard .dim-healthDot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: #aeb3bb; box-shadow: none; }
141
144
  .dim-panel .dim-botCard .dim-healthDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
142
- .dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warning-primary, #d97706); }
145
+ .dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
143
146
  .dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
144
- .dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 20px 0; }
145
- .dim-panel .dim-botMetric { min-width: 0; padding: 12px; border: 0; border-radius: 9px; background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
147
+ .dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 12px 0; }
148
+ .dim-panel .dim-botMetric { min-width: 0; padding: 10px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
146
149
  .dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
147
150
  .dim-panel .dim-botMetric dd { overflow: hidden; margin: 5px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
148
151
  .dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
149
152
  .dim-panel .ddt-headingCopy { display: none; }
150
153
  .dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
151
154
  @container (max-width: 680px) {
155
+ .dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { gap: 6px; }
156
+ .dim-panel .dim-bindActions { gap: 6px; }
157
+ .dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton, .dim-panel .dim-credentialButton { gap: 5px; padding-inline: 8px; font-size: 12px; }
158
+ .dim-panel .dim-actionIcon { width: 13px; height: 13px; flex-basis: 13px; }
159
+ .dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { padding-inline: 8px; font-size: 11px; }
152
160
  .dim-panel .dim-credentialForm { grid-template-columns: minmax(0, 1fr); }
153
161
  .dim-panel .dim-credentialError, .dim-panel .dim-credentialActions { grid-column: auto; }
154
162
  .dim-panel .dim-emptyView { min-height: 0; grid-template-columns: minmax(0, 1fr); }
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path';
4
4
 
5
5
  import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
6
6
 
7
- function harnessOrigin(webServer, configured) {
7
+ export function harnessOrigin(webServer, configured) {
8
8
  if (configured !== undefined) return new URL(configured);
9
9
  const port = webServer?.port;
10
10
  if (!Number.isInteger(port) || port < 1 || port > 65_535) {
@@ -13,7 +13,7 @@ function harnessOrigin(webServer, configured) {
13
13
  return new URL(`http://127.0.0.1:${port}`);
14
14
  }
15
15
 
16
- function pluginPaths(config, channel) {
16
+ export function pluginPaths(config, channel) {
17
17
  const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
18
18
  const root = resolve(config.dataDir ?? join(dshHome, 'integrations', `dsh-${channel}`));
19
19
  return {
@@ -0,0 +1,28 @@
1
+ import { createProductionController } from './production.mjs';
2
+ import { installSlackRpc } from './rpc.mjs';
3
+
4
+ export const name = 'dsh-im-slack-host';
5
+ export const inject = ['connection', 'credentials', 'webServer'];
6
+
7
+ export async function apply(ctx, config = {}) {
8
+ if (config?.controller) return installSlackRpc(ctx, config.controller, config.rpcAuthority);
9
+ const production = await createProductionController(ctx, config, config.internals ?? {});
10
+ const disposeRpc = installSlackRpc(ctx, production.controller, config.rpcAuthority);
11
+ ctx.effect(() => async () => production.close(), 'dsh-im: close Slack bot connections');
12
+ return disposeRpc;
13
+ }
14
+
15
+ export function createSlackHostPlugin(config) {
16
+ return Object.freeze({ name, inject, apply: (ctx) => apply(ctx, config) });
17
+ }
18
+
19
+ export { createProductionController } from './production.mjs';
20
+ export {
21
+ SLACK_ENDPOINTS,
22
+ SLACK_RPC_CHANNEL,
23
+ SLACK_RPC_ENDPOINTS,
24
+ createSlackRpcHandler,
25
+ installSlackRpc,
26
+ } from './rpc.mjs';
27
+ export { SlackController } from '../../../../src/channels/slack/slack-controller.mjs';
28
+ export { SlackRuntime } from '../../../../src/channels/slack/slack-runtime.mjs';
@@ -0,0 +1,91 @@
1
+ import { unlink } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ import { SlackConfigStore } from '../../../../src/channels/slack/config-store.mjs';
5
+ import { SlackController } from '../../../../src/channels/slack/slack-controller.mjs';
6
+ import { SlackHarnessClient } from '../../../../src/channels/slack/harness-client.mjs';
7
+ import { SlackRuntime } from '../../../../src/channels/slack/slack-runtime.mjs';
8
+ import { SlackStateStore } from '../../../../src/channels/slack/state-store.mjs';
9
+ import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
10
+ import { harnessOrigin, pluginPaths } from '../shared/production.mjs';
11
+
12
+ export async function createProductionController(ctx, config = {}, internals = {}) {
13
+ if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
14
+ if (!ctx?.webServer) throw new TypeError('dsh-im slack requires ctx.webServer');
15
+
16
+ const ResolvedConfigStore = internals.ConfigStore ?? SlackConfigStore;
17
+ const ResolvedStateStore = internals.StateStore ?? SlackStateStore;
18
+ const ResolvedHarness = internals.HarnessClient ?? SlackHarnessClient;
19
+ const ResolvedController = internals.Controller ?? SlackController;
20
+ const ResolvedRuntime = internals.Runtime ?? SlackRuntime;
21
+ const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
22
+ const logger = typeof ctx.logger === 'function'
23
+ ? ctx.logger('dsh-im:slack') : (ctx.logger ?? console);
24
+ const paths = pluginPaths(config, 'slack');
25
+ const configStore = await new ResolvedConfigStore(paths.config).load();
26
+ const stateStores = new Map();
27
+ const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
28
+ const stateFor = async (botId) => {
29
+ let state = stateStores.get(botId);
30
+ if (!state) {
31
+ state = await new ResolvedStateStore(statePath(botId)).load();
32
+ stateStores.set(botId, state);
33
+ }
34
+ return state;
35
+ };
36
+ const harness = new ResolvedHarness({
37
+ baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
38
+ workspace: resolve(config.workspace ?? process.cwd()),
39
+ agentPreset: config.agentPreset ?? 'standard',
40
+ autostart: false,
41
+ dshBin: config.dshBin ?? 'dsh',
42
+ });
43
+ const controller = new ResolvedController({
44
+ credentials: ctx.credentials,
45
+ configStore,
46
+ logger,
47
+ ...(internals.inspectCredentials ? { inspectCredentials: internals.inspectCredentials } : {}),
48
+ createRuntime: async ({ botId, config: botConfig, botToken, appToken }) => new ResolvedRuntime({
49
+ config: botConfig,
50
+ botToken,
51
+ appToken,
52
+ harness,
53
+ state: await stateFor(botId),
54
+ replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
55
+ connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
56
+ logger: {
57
+ error: (...args) => logger.error?.(`[${botId}]`, ...args),
58
+ warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
59
+ info: (...args) => logger.info?.(`[${botId}]`, ...args),
60
+ debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
61
+ },
62
+ }),
63
+ deleteState: async ({ botId }) => {
64
+ const state = stateStores.get(botId);
65
+ stateStores.delete(botId);
66
+ if (state && typeof state.remove === 'function') return state.remove();
67
+ try {
68
+ await unlink(statePath(botId));
69
+ } catch (error) {
70
+ if (error?.code !== 'ENOENT') throw error;
71
+ }
72
+ },
73
+ });
74
+ const supervisor = createSupervisor({
75
+ channel: 'slack',
76
+ controller,
77
+ harness,
78
+ logger,
79
+ retryDelaysMs: config.retryDelaysMs,
80
+ healthyIntervalMs: config.healthyIntervalMs,
81
+ }).start();
82
+ return {
83
+ controller,
84
+ ready: supervisor.ready,
85
+ async close() {
86
+ await supervisor.close();
87
+ await controller.close();
88
+ harness.stopManagedProcess();
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,127 @@
1
+ import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
+
3
+ export const SLACK_RPC_CHANNEL = '/slack';
4
+ export const SLACK_ENDPOINTS = Object.freeze({
5
+ status: 'connection.status',
6
+ bindCredentials: 'bot.bind-credentials',
7
+ reconnectBot: 'bot.reconnect',
8
+ deleteBot: 'bot.delete',
9
+ });
10
+ export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
11
+
12
+ const FORBIDDEN_PUBLIC_KEYS = new Set([
13
+ 'token', 'botToken', 'appToken', 'botTokenRef', 'appTokenRef',
14
+ 'tokenRef', 'platformId', 'secret', 'secretRef',
15
+ ]);
16
+
17
+ function isRecord(value) {
18
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ function exactKeys(value, allowed) {
22
+ return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
23
+ }
24
+
25
+ function validId(value) {
26
+ return typeof value === 'string' && /^slack_[a-f0-9]{24}$/.test(value);
27
+ }
28
+
29
+ function validBotToken(value) {
30
+ return typeof value === 'string' && /^xoxb-[A-Za-z0-9-]{16,}$/.test(value.trim())
31
+ && value.length <= 4_096;
32
+ }
33
+
34
+ function validAppToken(value) {
35
+ return typeof value === 'string' && /^xapp-[A-Za-z0-9-]{16,}$/.test(value.trim())
36
+ && value.length <= 4_096;
37
+ }
38
+
39
+ function payloadFailure(endpoint, payload) {
40
+ if (!isRecord(payload)) return 'Payload must be an object.';
41
+ if (endpoint === SLACK_ENDPOINTS.status) {
42
+ return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
43
+ }
44
+ if (endpoint === SLACK_ENDPOINTS.bindCredentials) {
45
+ return exactKeys(payload, ['botToken', 'appToken'])
46
+ && validBotToken(payload.botToken) && validAppToken(payload.appToken)
47
+ ? null : 'bot.bind-credentials requires xoxb Bot Token and xapp App Token.';
48
+ }
49
+ if (endpoint === SLACK_ENDPOINTS.reconnectBot) {
50
+ return exactKeys(payload, ['botId']) && validId(payload.botId)
51
+ ? null : 'bot.reconnect requires a botId.';
52
+ }
53
+ if (endpoint === SLACK_ENDPOINTS.deleteBot) {
54
+ return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
55
+ ? null : 'bot.delete requires a botId and confirm=true.';
56
+ }
57
+ return 'Unknown Slack endpoint.';
58
+ }
59
+
60
+ function sanitizePublic(value) {
61
+ if (Array.isArray(value)) return value.map(sanitizePublic);
62
+ if (!isRecord(value)) return value;
63
+ const safe = {};
64
+ for (const [key, child] of Object.entries(value)) {
65
+ if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child);
66
+ }
67
+ return safe;
68
+ }
69
+
70
+ function operationError(error) {
71
+ if (error?.code === 'slack-invalid-bot-token') {
72
+ return { code: 'invalid-bot-token', message: 'Slack Bot Token 无效,请确认使用以 xoxb- 开头的令牌。' };
73
+ }
74
+ if (error?.code === 'slack-invalid-app-token') {
75
+ return { code: 'invalid-app-token', message: 'Slack App Token 无效,请确认使用以 xapp- 开头的令牌。' };
76
+ }
77
+ if (error?.code === 'slack-missing-scope') {
78
+ return { code: 'missing-scope', message: 'Slack 应用权限不完整,请重新导入 Manifest 并安装到工作区。' };
79
+ }
80
+ if (error?.code === 'slack-socket-mode') {
81
+ return { code: 'socket-mode-unavailable', message: error.message };
82
+ }
83
+ return { code: 'slack-operation-failed', message: 'Slack 操作失败,请稍后重试。' };
84
+ }
85
+
86
+ export function createSlackRpcHandler(controller) {
87
+ for (const method of ['status', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
88
+ if (typeof controller?.[method] !== 'function') {
89
+ throw new TypeError(`A complete Slack controller is required (${method})`);
90
+ }
91
+ }
92
+ return async (endpoint, payload, signal) => {
93
+ if (signal?.aborted) {
94
+ return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
95
+ }
96
+ if (!SLACK_RPC_ENDPOINTS.includes(endpoint)) {
97
+ return { ok: false, error: { code: 'bad-request', message: 'Unknown Slack endpoint.' } };
98
+ }
99
+ const invalid = payloadFailure(endpoint, payload);
100
+ if (invalid) return { ok: false, error: { code: 'bad-request', message: invalid } };
101
+ try {
102
+ let value;
103
+ if (endpoint === SLACK_ENDPOINTS.status) value = await controller.status();
104
+ else if (endpoint === SLACK_ENDPOINTS.bindCredentials) value = await controller.bindCredentials(payload);
105
+ else if (endpoint === SLACK_ENDPOINTS.reconnectBot) value = await controller.reconnectBot(payload.botId);
106
+ else value = await controller.deleteBot(payload.botId);
107
+ return signal?.aborted
108
+ ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
109
+ : { ok: true, value: sanitizePublic(value) };
110
+ } catch (error) {
111
+ return signal?.aborted
112
+ ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
113
+ : { ok: false, error: operationError(error) };
114
+ }
115
+ };
116
+ }
117
+
118
+ export function installSlackRpc(ctx, controller, authority) {
119
+ if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
120
+ throw new TypeError('DSH Host Connection RPC is required');
121
+ }
122
+ return ctx.connection.rpc.handle(
123
+ SLACK_RPC_CHANNEL,
124
+ createSlackRpcHandler(controller),
125
+ { authority: resolveRpcAuthority(authority) },
126
+ );
127
+ }
@@ -2,6 +2,7 @@ import { apply as applyDingtalk } from './channels/dingtalk/index.mjs';
2
2
  import { apply as applyDiscord } from './channels/discord/index.mjs';
3
3
  import { apply as applyFeishu } from './channels/feishu/index.mjs';
4
4
  import { apply as applyQq } from './channels/qq/index.mjs';
5
+ import { apply as applySlack } from './channels/slack/index.mjs';
5
6
  import { apply as applyTelegram } from './channels/telegram/index.mjs';
6
7
  import { apply as applyWecom } from './channels/wecom/index.mjs';
7
8
  import { apply as applyWeixin } from './channels/weixin/index.mjs';
@@ -23,6 +24,7 @@ export function createImHostPlugin(internals = {}) {
23
24
  const startDingtalk = internals.applyDingtalk ?? applyDingtalk;
24
25
  const startWecom = internals.applyWecom ?? applyWecom;
25
26
  const startQq = internals.applyQq ?? applyQq;
27
+ const startSlack = internals.applySlack ?? applySlack;
26
28
  const startTelegram = internals.applyTelegram ?? applyTelegram;
27
29
  const startDiscord = internals.applyDiscord ?? applyDiscord;
28
30
  const startWhatsapp = internals.applyWhatsapp ?? applyWhatsapp;
@@ -35,6 +37,7 @@ export function createImHostPlugin(internals = {}) {
35
37
  await startDingtalk(ctx, channelConfig(config, 'dingtalk'));
36
38
  await startWecom(ctx, channelConfig(config, 'wecom'));
37
39
  await startQq(ctx, channelConfig(config, 'qq'));
40
+ await startSlack(ctx, channelConfig(config, 'slack'));
38
41
  await startTelegram(ctx, channelConfig(config, 'telegram'));
39
42
  await startDiscord(ctx, channelConfig(config, 'discord'));
40
43
  await startWhatsapp(ctx, channelConfig(config, 'whatsapp'));
@@ -11,10 +11,13 @@ const required = [
11
11
  'README.md',
12
12
  'THIRD_PARTY_NOTICES.md',
13
13
  'plugin-src/client/channels/dingtalk/index.js',
14
+ 'plugin-src/client/channels/slack/index.js',
15
+ 'plugin-src/client/i18n.js',
14
16
  'plugin-src/host/channels/feishu/index.mjs',
15
17
  'plugin-src/host/channels/weixin/index.mjs',
16
18
  'plugin-src/host/channels/dingtalk/index.mjs',
17
19
  'plugin-src/host/channels/qq/index.mjs',
20
+ 'plugin-src/host/channels/slack/index.mjs',
18
21
  'plugin-src/host/channels/wecom/index.mjs',
19
22
  'plugin-src/host/channels/telegram/index.mjs',
20
23
  'plugin-src/host/channels/discord/index.mjs',
@@ -23,6 +26,7 @@ const required = [
23
26
  'src/channels/weixin/weixin-runtime.mjs',
24
27
  'src/channels/dingtalk/dingtalk-runtime.mjs',
25
28
  'src/channels/qq/qq-runtime.mjs',
29
+ 'src/channels/slack/slack-runtime.mjs',
26
30
  'src/channels/wecom/wecom-runtime.mjs',
27
31
  'src/channels/telegram/telegram-runtime.mjs',
28
32
  'src/channels/discord/discord-runtime.mjs',
@@ -47,8 +51,11 @@ const lock = JSON.parse(lockText);
47
51
  if (!client.includes('id: "@xmanrui/dsh-im"')) {
48
52
  throw new Error('client bundle does not register the dsh-im loader id');
49
53
  }
50
- if (!client.includes('id: "im"') || !client.includes('label: "IM\\u673A\\u5668\\u4EBA"')) {
51
- throw new Error('client bundle does not register the IM机器人 settings tab');
54
+ if (!client.includes('id: "im"')
55
+ || !client.includes('label: () => t("IM\\u673A\\u5668\\u4EBA")')
56
+ || !client.includes('locale: IM_LOCALE_NAMESPACE')
57
+ || !client.includes('IM_LOCALE_NAMESPACE = "dsh-im"')) {
58
+ throw new Error('client bundle does not register the localized IM settings tab');
52
59
  }
53
60
  if ((client.match(/ctx\.slots\.inject\("settings\.plugins\.tab"/g) ?? []).length !== 1) {
54
61
  throw new Error('client bundle must register exactly one settings tab');
@@ -60,7 +67,7 @@ if (!client.includes('container-type: inline-size')
60
67
  || !client.includes('@container (max-width: 680px)')) {
61
68
  throw new Error('client bundle does not contain the narrow-panel DingTalk QR layout');
62
69
  }
63
- for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/telegram', '/discord', '/whatsapp']) {
70
+ for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp']) {
64
71
  if (!host.includes(marker)) {
65
72
  throw new Error(`host bundle does not contain the internal ${marker} RPC provider`);
66
73
  }
@@ -108,7 +108,7 @@ export class DiscordApi {
108
108
  headers: {
109
109
  authorization: `Bot ${this.#token}`,
110
110
  'content-type': 'application/json',
111
- 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.2.1)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.3.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),