@xmanrui/dsh-im 0.2.2 → 0.4.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 (38) hide show
  1. package/README.md +53 -11
  2. package/assets/logo-icon.png +0 -0
  3. package/assets/logo.png +0 -0
  4. package/lib/client.js +1847 -1099
  5. package/lib/index.js +116 -115
  6. package/package.json +3 -2
  7. package/plugin-src/client/build.mjs +6 -1
  8. package/plugin-src/client/channel-logos.js +13 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +1 -1
  10. package/plugin-src/client/channels/feishu/index.js +1 -2
  11. package/plugin-src/client/channels/qq/index.js +1 -1
  12. package/plugin-src/client/channels/shared/token-channel.js +1 -2
  13. package/plugin-src/client/channels/slack/api.js +11 -0
  14. package/plugin-src/client/channels/slack/index.js +130 -0
  15. package/plugin-src/client/channels/slack/styles.js +34 -0
  16. package/plugin-src/client/channels/wecom/index.js +1 -1
  17. package/plugin-src/client/channels/weixin/index.js +1 -2
  18. package/plugin-src/client/channels/whatsapp/index.js +1 -1
  19. package/plugin-src/client/credential-binding.js +1 -2
  20. package/plugin-src/client/i18n.js +444 -0
  21. package/plugin-src/client/index.js +57 -5
  22. package/plugin-src/client/styles.js +31 -13
  23. package/plugin-src/host/channels/shared/production.mjs +2 -2
  24. package/plugin-src/host/channels/slack/index.mjs +28 -0
  25. package/plugin-src/host/channels/slack/production.mjs +91 -0
  26. package/plugin-src/host/channels/slack/rpc.mjs +127 -0
  27. package/plugin-src/host/index.mjs +3 -0
  28. package/scripts/verify-package.mjs +10 -3
  29. package/src/channels/discord/discord-api.mjs +1 -1
  30. package/src/channels/slack/config-store.mjs +173 -0
  31. package/src/channels/slack/harness-client.mjs +3 -0
  32. package/src/channels/slack/manifest.mjs +31 -0
  33. package/src/channels/slack/slack-api.mjs +259 -0
  34. package/src/channels/slack/slack-bridge.mjs +15 -0
  35. package/src/channels/slack/slack-controller.mjs +318 -0
  36. package/src/channels/slack/slack-runtime.mjs +462 -0
  37. package/src/channels/slack/state-store.mjs +3 -0
  38. package/src/channels/weixin/weixin-api.mjs +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.2.2",
4
- "description": "通过扫码或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Telegram、Discord和WhatsApp)。",
3
+ "version": "0.4.0",
4
+ "description": "通过扫码、App Manifest或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord和WhatsApp)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -23,6 +23,7 @@
23
23
  "./package.json": "./package.json"
24
24
  },
25
25
  "files": [
26
+ "assets",
26
27
  "bin",
27
28
  "cordis.patch.yml",
28
29
  "lib",
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
@@ -8,6 +8,8 @@ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
8
8
  const packageRoot = resolve(sourceDirectory, '../..');
9
9
  const outputPath = resolve(packageRoot, 'lib/client.js');
10
10
  const loaderId = process.env.DSH_IM_CLIENT_ID ?? '@xmanrui/dsh-im';
11
+ const logoPath = resolve(packageRoot, 'assets/logo-icon.png');
12
+ const logoDataUrl = `data:image/png;base64,${(await readFile(logoPath)).toString('base64')}`;
11
13
 
12
14
  const result = await build({
13
15
  entryPoints: [resolve(sourceDirectory, 'index.js')],
@@ -16,6 +18,9 @@ const result = await build({
16
18
  platform: 'browser',
17
19
  target: ['chrome100'],
18
20
  external: ['react'],
21
+ define: {
22
+ 'globalThis.__DSH_IM_LOGO_DATA_URL__': JSON.stringify(logoDataUrl),
23
+ },
19
24
  write: false,
20
25
  minify: process.env.NODE_ENV === 'production',
21
26
  legalComments: 'none',
@@ -106,6 +106,19 @@ export function DiscordLogoGlyph({ size } = {}) {
106
106
  }));
107
107
  }
108
108
 
109
+ export function SlackLogoGlyph({ size } = {}) {
110
+ return h('svg', {
111
+ ...dimensions(size),
112
+ viewBox: '0 0 24 24',
113
+ focusable: 'false',
114
+ 'aria-hidden': 'true',
115
+ 'data-im-channel-logo': 'slack',
116
+ }, h('path', {
117
+ fill: 'currentColor',
118
+ d: 'M6 15a2 2 0 1 1-2-2h2v2Zm1 0a2 2 0 1 1 4 0v5a2 2 0 1 1-4 0v-5Zm2-8a2 2 0 1 1 2-2v2H9Zm0 1a2 2 0 1 1 0 4H4a2 2 0 1 1 0-4h5Zm8 2a2 2 0 1 1 2 2h-2v-2Zm-1 0a2 2 0 1 1-4 0V5a2 2 0 1 1 4 0v5Zm-2 8a2 2 0 1 1-2 2v-2h2Zm0-1a2 2 0 1 1 0-4h5a2 2 0 1 1 0 4h-5Z',
119
+ }));
120
+ }
121
+
109
122
  export function WhatsappLogoGlyph({ size } = {}) {
110
123
  return h('svg', {
111
124
  ...dimensions(size),
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
4
+ import { h } from '../../i18n.js';
4
5
  import {
5
6
  DINGTALK_ENDPOINTS,
6
7
  DINGTALK_RPC_CHANNEL,
@@ -13,7 +14,6 @@ import {
13
14
  } from './api.js';
14
15
  import { installDingtalkStyles } from './styles.js';
15
16
 
16
- const h = React.createElement;
17
17
  const ACTIVE_PROVISION_STATES = new Set(['pending', 'scanned', 'authorizing', 'creating', 'connecting']);
18
18
 
19
19
  export const name = 'dingtalk-settings';
@@ -2,6 +2,7 @@ import * as React from "react";
2
2
 
3
3
  import { FeishuLogoGlyph } from "../../channel-logos.js";
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from "../../credential-binding.js";
5
+ import { h } from "../../i18n.js";
5
6
  import {
6
7
  FEISHU_ENDPOINTS,
7
8
  FEISHU_RPC_CHANNEL,
@@ -15,8 +16,6 @@ import {
15
16
  import { useAnimationFrameScheduler } from "../../lifecycle.js";
16
17
  import { installFeishuStyles } from "./styles.js";
17
18
 
18
- const h = React.createElement;
19
-
20
19
  export const name = "feishu-settings";
21
20
  export const inject = ["slots", "connection"];
22
21
 
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { QqLogoGlyph } from '../../channel-logos.js';
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
5
+ import { h } from '../../i18n.js';
5
6
  import { installDingtalkStyles } from '../dingtalk/styles.js';
6
7
  import {
7
8
  QQ_ENDPOINTS,
@@ -15,7 +16,6 @@ import {
15
16
  } from './api.js';
16
17
  import { installQqStyles } from './styles.js';
17
18
 
18
- const h = React.createElement;
19
19
  const ACTIVE_STATES = new Set(['pending', 'refreshing', 'connecting']);
20
20
 
21
21
  const Button = React.forwardRef(function Button({ children, kind = 'secondary', className = '', ...props }, ref) {
@@ -1,10 +1,9 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-binding.js';
4
+ import { h } from '../../i18n.js';
4
5
  import { installDingtalkStyles } from '../dingtalk/styles.js';
5
6
 
6
- const h = React.createElement;
7
-
8
7
  const Button = React.forwardRef(function Button(
9
8
  { children, kind = 'secondary', className = '', ...props },
10
9
  ref,
@@ -0,0 +1,11 @@
1
+ import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
2
+
3
+ export const SLACK_RPC_CHANNEL = '/slack';
4
+ export const SLACK_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
5
+
6
+ const api = createTokenChannelApi('Slack', ' Socket Mode 长连接');
7
+
8
+ export const unwrapRpcResult = api.unwrapRpcResult;
9
+ export const normalizeSnapshot = api.normalizeSnapshot;
10
+ export const presentError = api.presentError;
11
+ export { api as slackClientApi };
@@ -0,0 +1,130 @@
1
+ import * as React from 'react';
2
+
3
+ import { SlackLogoGlyph } from '../../channel-logos.js';
4
+ import { h } from '../../i18n.js';
5
+ import { createTokenChannelSettings } from '../shared/token-channel.js';
6
+ import {
7
+ SLACK_CREATE_APP_URL,
8
+ SLACK_APP_MANIFEST_YAML,
9
+ } from '../../../../src/channels/slack/manifest.mjs';
10
+ import { SLACK_ENDPOINTS, slackClientApi } from './api.js';
11
+ import { installSlackStyles } from './styles.js';
12
+
13
+ export function SlackCredentialPanel({ busy, error, onSubmit, onCancel }) {
14
+ const [botToken, setBotToken] = React.useState('');
15
+ const [appToken, setAppToken] = React.useState('');
16
+ const [copied, setCopied] = React.useState(false);
17
+ const headingId = React.useId();
18
+
19
+ const copyManifest = async () => {
20
+ try {
21
+ await navigator.clipboard.writeText(SLACK_APP_MANIFEST_YAML);
22
+ setCopied(true);
23
+ window.setTimeout(() => setCopied(false), 2_000);
24
+ } catch {
25
+ setCopied(false);
26
+ }
27
+ };
28
+
29
+ const submit = (event) => {
30
+ event.preventDefault();
31
+ const normalizedBotToken = botToken.trim();
32
+ const normalizedAppToken = appToken.trim();
33
+ if (!normalizedBotToken || !normalizedAppToken || busy) return;
34
+ void onSubmit?.({ botToken: normalizedBotToken, appToken: normalizedAppToken });
35
+ };
36
+
37
+ return h('section', {
38
+ className: 'ddt-card dim-surfaceCard dim-credentialPanel dsl-setup',
39
+ 'aria-labelledby': headingId,
40
+ },
41
+ h('h3', { id: headingId, className: 'dim-credentialTitle' }, '接入 Slack 机器人'),
42
+ h('div', { className: 'dsl-guide' },
43
+ h('div', { className: 'dsl-guideCopy' },
44
+ h('strong', null, '先用 Manifest 创建并配置 Slack App'),
45
+ h('p', null, '复制配置后,在 Slack 选择 From a manifest;创建完成后生成 connections:write App Token,并将应用安装到工作区。')),
46
+ h('div', { className: 'dsl-guideActions' },
47
+ h('button', {
48
+ type: 'button',
49
+ className: 'ddt-button',
50
+ onClick: () => void copyManifest(),
51
+ disabled: busy,
52
+ }, copied ? h('span', { className: 'dsl-copyState' }, '已复制 Manifest') : '复制 Manifest'),
53
+ h('a', {
54
+ className: 'ddt-button',
55
+ href: SLACK_CREATE_APP_URL,
56
+ target: '_blank',
57
+ rel: 'noreferrer',
58
+ }, '打开 Slack 创建页'))),
59
+ h('form', { className: 'dim-credentialForm dim-credentialFormSingle', onSubmit: submit },
60
+ h('div', { className: 'dsl-fields' },
61
+ h('label', { className: 'dim-credentialField' },
62
+ h('span', null, 'Bot Token'),
63
+ h('input', {
64
+ type: 'password',
65
+ value: botToken,
66
+ onChange: (event) => setBotToken(event.target.value),
67
+ placeholder: 'xoxb-…',
68
+ maxLength: 4096,
69
+ autoCapitalize: 'none',
70
+ autoCorrect: 'off',
71
+ spellCheck: false,
72
+ autoComplete: 'new-password',
73
+ disabled: busy,
74
+ required: true,
75
+ })),
76
+ h('label', { className: 'dim-credentialField' },
77
+ h('span', null, 'App Token'),
78
+ h('input', {
79
+ type: 'password',
80
+ value: appToken,
81
+ onChange: (event) => setAppToken(event.target.value),
82
+ placeholder: 'xapp-…',
83
+ maxLength: 4096,
84
+ autoCapitalize: 'none',
85
+ autoCorrect: 'off',
86
+ spellCheck: false,
87
+ autoComplete: 'new-password',
88
+ disabled: busy,
89
+ required: true,
90
+ })),
91
+ h('p', { className: 'dsl-tokenHint' }, 'Bot Token 来自 OAuth & Permissions;App Token 来自 Basic Information,并且必须包含 connections:write。')),
92
+ error ? h('p', { className: 'dim-credentialError', role: 'alert' }, error.message ?? String(error)) : null,
93
+ h('div', { className: 'ddt-actions dim-viewActions dim-credentialActions' },
94
+ h('button', {
95
+ type: 'submit',
96
+ className: 'ddt-button',
97
+ 'data-kind': 'primary',
98
+ disabled: busy || !botToken.trim() || !appToken.trim(),
99
+ }, busy ? '正在验证并连接…' : '验证并连接'),
100
+ h('button', {
101
+ type: 'button',
102
+ className: 'ddt-button',
103
+ onClick: onCancel,
104
+ disabled: busy,
105
+ }, '取消'))));
106
+ }
107
+
108
+ const channel = createTokenChannelSettings({
109
+ channel: 'Slack',
110
+ endpoints: SLACK_ENDPOINTS,
111
+ api: slackClientApi,
112
+ LogoGlyph: SlackLogoGlyph,
113
+ installStyles: installSlackStyles,
114
+ pageClass: 'dsl-page',
115
+ avatarClass: 'dsl-avatar',
116
+ connectionLabel: 'Socket Mode 长连接',
117
+ emptyTitle: '接入 Slack 机器人',
118
+ emptyDescription: '使用官方 App Manifest 快速配置机器人,再填写 Bot Token 与 App Token 建立本地 Socket Mode 连接。',
119
+ platformLabel: 'Slack 工作区',
120
+ CredentialPanel: SlackCredentialPanel,
121
+ credentialPayload: ({ botToken, appToken }) => ({ botToken, appToken }),
122
+ credentialAriaLabel: '使用 Manifest 和双 Token 接入 Slack 机器人',
123
+ credentialOpenLabel: '接入机器人',
124
+ credentialCloseLabel: '收起接入',
125
+ credentialNoun: 'Bot Token 与 App Token',
126
+ emptyActionLabel: '开始接入',
127
+ });
128
+
129
+ export const SlackSettingsTab = channel.SettingsTab;
130
+ export const SlackAccountCard = channel.AccountCard;
@@ -0,0 +1,34 @@
1
+ export const SLACK_STYLE_ID = 'xmanrui-dsh-im-slack-settings';
2
+
3
+ const CSS = String.raw`
4
+ .dsl-page { --ddt-accent: #4a154b; --ddt-accent-deep: #321033; --ddt-accent-wash: #f7eef7; }
5
+ .dsl-avatar { color: #fff; background: #4a154b; }
6
+ .dsl-avatar svg { display: block; }
7
+ .dsl-setup { display: grid; gap: 18px; }
8
+ .dsl-guide { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 18px; padding: 16px; border: 1px solid color-mix(in srgb, #4a154b 18%, var(--dsw-alias-border-l2, #e5e6eb)); border-radius: 11px; background: color-mix(in srgb, #4a154b 4%, var(--dsw-alias-bg-layer-1, #fff)); }
9
+ .dsl-guideCopy { min-width: 0; }
10
+ .dsl-guideCopy strong { display: block; margin-bottom: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; }
11
+ .dsl-guideCopy p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.6; }
12
+ .dsl-guideActions { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
13
+ .dsl-guideActions .ddt-button { white-space: nowrap; }
14
+ .dsl-copyState { color: var(--dsw-alias-state-success-primary, #20a162); }
15
+ .dsl-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
16
+ .dsl-tokenHint { grid-column: 1 / -1; margin: -4px 0 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.55; }
17
+ @container (max-width: 680px) {
18
+ .dsl-guide { grid-template-columns: minmax(0, 1fr); }
19
+ .dsl-guideActions { justify-content: flex-start; }
20
+ .dsl-fields { grid-template-columns: minmax(0, 1fr); }
21
+ }
22
+ `;
23
+
24
+ export function installSlackStyles() {
25
+ if (typeof document === 'undefined') return () => {};
26
+ const existing = document.querySelector(`style[data-plugin-css="${SLACK_STYLE_ID}"]`);
27
+ if (existing) return () => {};
28
+ const style = document.createElement('style');
29
+ style.dataset.plugin = '@xmanrui/dsh-im';
30
+ style.dataset.pluginCss = SLACK_STYLE_ID;
31
+ style.textContent = CSS;
32
+ document.head.appendChild(style);
33
+ return () => style.remove();
34
+ }
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { WecomLogoGlyph } from '../../channel-logos.js';
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
5
+ import { h } from '../../i18n.js';
5
6
  import { installDingtalkStyles } from '../dingtalk/styles.js';
6
7
  import {
7
8
  WECOM_ENDPOINTS,
@@ -15,7 +16,6 @@ import {
15
16
  } from './api.js';
16
17
  import { installWecomStyles } from './styles.js';
17
18
 
18
- const h = React.createElement;
19
19
  const ACTIVE_STATES = new Set(['pending', 'refreshing', 'connecting']);
20
20
 
21
21
  const Button = React.forwardRef(function Button({ children, kind = 'secondary', className = '', ...props }, ref) {
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { WeixinLogoGlyph } from '../../channel-logos.js';
4
4
  import { QrActionIcon } from '../../credential-binding.js';
5
+ import { h } from '../../i18n.js';
5
6
  import {
6
7
  WEIXIN_ENDPOINTS,
7
8
  WEIXIN_RPC_CHANNEL,
@@ -16,8 +17,6 @@ import {
16
17
  import { createPollScheduler, useAnimationFrameScheduler } from '../../lifecycle.js';
17
18
  import { installWeixinStyles } from './styles.js';
18
19
 
19
- const h = React.createElement;
20
-
21
20
  export const name = 'weixin-settings';
22
21
  export const inject = ['slots', 'connection'];
23
22
 
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { WhatsappLogoGlyph } from '../../channel-logos.js';
4
4
  import { QrActionIcon } from '../../credential-binding.js';
5
+ import { h } from '../../i18n.js';
5
6
  import { installDingtalkStyles } from '../dingtalk/styles.js';
6
7
  import {
7
8
  WHATSAPP_ENDPOINTS,
@@ -14,7 +15,6 @@ import {
14
15
  } from './api.js';
15
16
  import { installWhatsappStyles } from './styles.js';
16
17
 
17
- const h = React.createElement;
18
18
  const ACTIVE_STATES = new Set(['pending', 'connecting']);
19
19
 
20
20
  const Button = React.forwardRef(function Button(
@@ -1,6 +1,5 @@
1
1
  import * as React from 'react';
2
-
3
- const h = React.createElement;
2
+ import { h } from './i18n.js';
4
3
 
5
4
  function ActionIcon({ children }) {
6
5
  return h('svg', {