@xmanrui/dsh-im 0.1.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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/THIRD_PARTY_NOTICES.md +15 -0
  4. package/bin/dsh-im.mjs +99 -0
  5. package/cordis.patch.yml +3 -0
  6. package/lib/client.js +6211 -0
  7. package/lib/index.js +13339 -0
  8. package/package.json +73 -0
  9. package/plugin-src/client/build.mjs +38 -0
  10. package/plugin-src/client/channel-logos.js +107 -0
  11. package/plugin-src/client/channels/dingtalk/api.js +203 -0
  12. package/plugin-src/client/channels/dingtalk/index.js +709 -0
  13. package/plugin-src/client/channels/dingtalk/styles.js +140 -0
  14. package/plugin-src/client/channels/discord/api.js +11 -0
  15. package/plugin-src/client/channels/discord/index.js +25 -0
  16. package/plugin-src/client/channels/discord/styles.js +19 -0
  17. package/plugin-src/client/channels/feishu/api.js +325 -0
  18. package/plugin-src/client/channels/feishu/index.js +986 -0
  19. package/plugin-src/client/channels/feishu/styles.js +505 -0
  20. package/plugin-src/client/channels/qq/api.js +119 -0
  21. package/plugin-src/client/channels/qq/index.js +402 -0
  22. package/plugin-src/client/channels/qq/styles.js +19 -0
  23. package/plugin-src/client/channels/shared/token-api.js +88 -0
  24. package/plugin-src/client/channels/shared/token-channel.js +277 -0
  25. package/plugin-src/client/channels/telegram/api.js +11 -0
  26. package/plugin-src/client/channels/telegram/index.js +25 -0
  27. package/plugin-src/client/channels/telegram/styles.js +19 -0
  28. package/plugin-src/client/channels/wecom/api.js +119 -0
  29. package/plugin-src/client/channels/wecom/index.js +402 -0
  30. package/plugin-src/client/channels/wecom/styles.js +19 -0
  31. package/plugin-src/client/channels/weixin/api.js +156 -0
  32. package/plugin-src/client/channels/weixin/index.js +577 -0
  33. package/plugin-src/client/channels/weixin/styles.js +120 -0
  34. package/plugin-src/client/credential-binding.js +117 -0
  35. package/plugin-src/client/index.js +192 -0
  36. package/plugin-src/client/lifecycle.js +86 -0
  37. package/plugin-src/client/styles.js +192 -0
  38. package/plugin-src/host/build.mjs +24 -0
  39. package/plugin-src/host/channels/dingtalk/connection-supervisor.mjs +130 -0
  40. package/plugin-src/host/channels/dingtalk/index.mjs +39 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +122 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +237 -0
  43. package/plugin-src/host/channels/discord/index.mjs +30 -0
  44. package/plugin-src/host/channels/discord/production.mjs +17 -0
  45. package/plugin-src/host/channels/discord/rpc.mjs +21 -0
  46. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +167 -0
  47. package/plugin-src/host/channels/feishu/controller.mjs +172 -0
  48. package/plugin-src/host/channels/feishu/credential-store.mjs +84 -0
  49. package/plugin-src/host/channels/feishu/index.mjs +73 -0
  50. package/plugin-src/host/channels/feishu/production.mjs +138 -0
  51. package/plugin-src/host/channels/feishu/rpc.mjs +454 -0
  52. package/plugin-src/host/channels/qq/connection-supervisor.mjs +124 -0
  53. package/plugin-src/host/channels/qq/index.mjs +30 -0
  54. package/plugin-src/host/channels/qq/production.mjs +109 -0
  55. package/plugin-src/host/channels/qq/rpc.mjs +146 -0
  56. package/plugin-src/host/channels/shared/connection-supervisor.mjs +133 -0
  57. package/plugin-src/host/channels/shared/production.mjs +104 -0
  58. package/plugin-src/host/channels/shared/rpc.mjs +119 -0
  59. package/plugin-src/host/channels/telegram/index.mjs +30 -0
  60. package/plugin-src/host/channels/telegram/production.mjs +17 -0
  61. package/plugin-src/host/channels/telegram/rpc.mjs +21 -0
  62. package/plugin-src/host/channels/wecom/connection-supervisor.mjs +124 -0
  63. package/plugin-src/host/channels/wecom/index.mjs +36 -0
  64. package/plugin-src/host/channels/wecom/production.mjs +113 -0
  65. package/plugin-src/host/channels/wecom/rpc.mjs +151 -0
  66. package/plugin-src/host/channels/weixin/connection-supervisor.mjs +127 -0
  67. package/plugin-src/host/channels/weixin/index.mjs +39 -0
  68. package/plugin-src/host/channels/weixin/production.mjs +119 -0
  69. package/plugin-src/host/channels/weixin/rpc.mjs +178 -0
  70. package/plugin-src/host/index.mjs +44 -0
  71. package/plugin-src/host/rpc-authority.mjs +11 -0
  72. package/scripts/verify-package.mjs +97 -0
  73. package/src/channels/dingtalk/config-store.mjs +282 -0
  74. package/src/channels/dingtalk/device-auth.mjs +237 -0
  75. package/src/channels/dingtalk/dingtalk-api.mjs +579 -0
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +281 -0
  77. package/src/channels/dingtalk/dingtalk-card-stream.mjs +233 -0
  78. package/src/channels/dingtalk/dingtalk-controller.mjs +752 -0
  79. package/src/channels/dingtalk/dingtalk-runtime.mjs +358 -0
  80. package/src/channels/dingtalk/harness-client.mjs +299 -0
  81. package/src/channels/dingtalk/state-store.mjs +212 -0
  82. package/src/channels/discord/config-store.mjs +24 -0
  83. package/src/channels/discord/discord-api.mjs +153 -0
  84. package/src/channels/discord/discord-bridge.mjs +15 -0
  85. package/src/channels/discord/discord-controller.mjs +16 -0
  86. package/src/channels/discord/discord-runtime.mjs +460 -0
  87. package/src/channels/discord/harness-client.mjs +3 -0
  88. package/src/channels/discord/state-store.mjs +3 -0
  89. package/src/channels/feishu/bridge.mjs +216 -0
  90. package/src/channels/feishu/config.mjs +71 -0
  91. package/src/channels/feishu/feishu-app.mjs +51 -0
  92. package/src/channels/feishu/feishu-channel.mjs +153 -0
  93. package/src/channels/feishu/feishu-runtime.mjs +222 -0
  94. package/src/channels/feishu/harness-client.mjs +268 -0
  95. package/src/channels/feishu/message-utils.mjs +51 -0
  96. package/src/channels/feishu/multi-bot-controller.mjs +686 -0
  97. package/src/channels/feishu/plugin-config-store.mjs +204 -0
  98. package/src/channels/feishu/plugin-controller.mjs +248 -0
  99. package/src/channels/feishu/registration-manager.mjs +345 -0
  100. package/src/channels/feishu/state-store.mjs +71 -0
  101. package/src/channels/qq/config-store.mjs +171 -0
  102. package/src/channels/qq/harness-client.mjs +3 -0
  103. package/src/channels/qq/qq-bridge.mjs +177 -0
  104. package/src/channels/qq/qq-controller.mjs +452 -0
  105. package/src/channels/qq/qq-runtime.mjs +208 -0
  106. package/src/channels/qq/qr-auth.mjs +24 -0
  107. package/src/channels/qq/state-store.mjs +96 -0
  108. package/src/channels/shared/conversation-state-store.mjs +107 -0
  109. package/src/channels/shared/editable-message-stream.mjs +81 -0
  110. package/src/channels/shared/text-harness-bridge.mjs +180 -0
  111. package/src/channels/shared/token-bot-controller.mjs +298 -0
  112. package/src/channels/shared/token-config-store.mjs +180 -0
  113. package/src/channels/telegram/config-store.mjs +24 -0
  114. package/src/channels/telegram/harness-client.mjs +3 -0
  115. package/src/channels/telegram/state-store.mjs +3 -0
  116. package/src/channels/telegram/telegram-api.mjs +120 -0
  117. package/src/channels/telegram/telegram-bridge.mjs +15 -0
  118. package/src/channels/telegram/telegram-controller.mjs +16 -0
  119. package/src/channels/telegram/telegram-runtime.mjs +278 -0
  120. package/src/channels/wecom/config-store.mjs +170 -0
  121. package/src/channels/wecom/harness-client.mjs +3 -0
  122. package/src/channels/wecom/qr-auth.mjs +95 -0
  123. package/src/channels/wecom/state-store.mjs +90 -0
  124. package/src/channels/wecom/wecom-bridge.mjs +243 -0
  125. package/src/channels/wecom/wecom-controller.mjs +474 -0
  126. package/src/channels/wecom/wecom-runtime.mjs +209 -0
  127. package/src/channels/weixin/config-store.mjs +182 -0
  128. package/src/channels/weixin/harness-client.mjs +259 -0
  129. package/src/channels/weixin/state-store.mjs +112 -0
  130. package/src/channels/weixin/weixin-api.mjs +319 -0
  131. package/src/channels/weixin/weixin-bridge.mjs +173 -0
  132. package/src/channels/weixin/weixin-controller.mjs +545 -0
  133. package/src/channels/weixin/weixin-runtime.mjs +204 -0
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@xmanrui/dsh-im",
3
+ "version": "0.1.0",
4
+ "description": "通过扫码或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Telegram和Discord)。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/xmanrui/dsh-im.git"
10
+ },
11
+ "homepage": "https://github.com/xmanrui/dsh-im#readme",
12
+ "bugs": "https://github.com/xmanrui/dsh-im/issues",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "bin": {
17
+ "dsh-im": "./bin/dsh-im.mjs"
18
+ },
19
+ "main": "./lib/index.js",
20
+ "exports": {
21
+ ".": "./lib/index.js",
22
+ "./client": "./lib/client.js",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "bin",
27
+ "cordis.patch.yml",
28
+ "lib",
29
+ "plugin-src",
30
+ "scripts",
31
+ "src",
32
+ "LICENSE",
33
+ "README.md",
34
+ "THIRD_PARTY_NOTICES.md"
35
+ ],
36
+ "dsh": {
37
+ "bundle": {
38
+ "patch": "./cordis.patch.yml"
39
+ },
40
+ "client": {
41
+ "inject": [
42
+ "@deepseek-ai/dsh-client-connection",
43
+ "@deepseek-ai/dsh-client-runtime",
44
+ "@deepseek-ai/dsh-client-ui-settings",
45
+ "@deepseek-ai/dsh-client-ui-slots",
46
+ "@deepseek-ai/dsh-client-locale"
47
+ ],
48
+ "platform": "web"
49
+ }
50
+ },
51
+ "scripts": {
52
+ "build": "node plugin-src/client/build.mjs && node plugin-src/host/build.mjs",
53
+ "test": "node --test test/*.test.mjs test/channels/*/*.test.mjs",
54
+ "check": "npm test && npm run build && node scripts/verify-package.mjs"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.19"
58
+ },
59
+ "dependencies": {
60
+ "@larksuiteoapi/node-sdk": "1.73.0",
61
+ "@tencent-connect/qqbot-connector": "1.2.0",
62
+ "@tencent-connect/qqbot-nodejs": "1.0.4",
63
+ "@wecom/aibot-node-sdk": "1.0.7",
64
+ "dingtalk-stream": "2.1.4",
65
+ "qrcode": "1.5.4"
66
+ },
67
+ "devDependencies": {
68
+ "esbuild": "0.25.9",
69
+ "react": "18.3.1",
70
+ "react-dom": "18.3.1",
71
+ "react-test-renderer": "18.3.1"
72
+ }
73
+ }
@@ -0,0 +1,38 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { build } from 'esbuild';
6
+
7
+ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = resolve(sourceDirectory, '../..');
9
+ const outputPath = resolve(packageRoot, 'lib/client.js');
10
+ const loaderId = process.env.DSH_IM_CLIENT_ID ?? '@xmanrui/dsh-im';
11
+
12
+ const result = await build({
13
+ entryPoints: [resolve(sourceDirectory, 'index.js')],
14
+ bundle: true,
15
+ format: 'cjs',
16
+ platform: 'browser',
17
+ target: ['chrome100'],
18
+ external: ['react'],
19
+ write: false,
20
+ minify: process.env.NODE_ENV === 'production',
21
+ legalComments: 'none',
22
+ });
23
+ const bundled = result.outputFiles?.[0]?.text;
24
+ if (!bundled) throw new Error('esbuild did not produce a client bundle');
25
+
26
+ const wrapped = `window.__ModuleLoader__.load({
27
+ id: ${JSON.stringify(loaderId)},
28
+ factory: (require) => {
29
+ var module = { exports: {} };
30
+ var exports = module.exports;
31
+ ${bundled}
32
+ return module.exports;
33
+ }
34
+ });
35
+ `;
36
+ await mkdir(dirname(outputPath), { recursive: true });
37
+ await writeFile(outputPath, wrapped, 'utf8');
38
+ console.log(`Wrote ${outputPath}`);
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+
3
+ const h = React.createElement;
4
+
5
+ function dimensions(size) {
6
+ return size === undefined ? {} : { width: size, height: size };
7
+ }
8
+
9
+ export function WeixinLogoGlyph({ size } = {}) {
10
+ return h('svg', {
11
+ ...dimensions(size),
12
+ viewBox: '0 0 24 24',
13
+ focusable: 'false',
14
+ 'aria-hidden': 'true',
15
+ 'data-im-channel-logo': 'weixin',
16
+ }, h('path', {
17
+ fill: 'currentColor',
18
+ d: 'M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z',
19
+ }));
20
+ }
21
+
22
+ export function FeishuLogoGlyph({ size } = {}) {
23
+ return h('svg', {
24
+ ...dimensions(size),
25
+ viewBox: '0 0 24 24',
26
+ focusable: 'false',
27
+ 'aria-hidden': 'true',
28
+ 'data-im-channel-logo': 'feishu',
29
+ },
30
+ h('path', { fill: '#00D6B9', d: 'M7.2 4.5h7.6c1.2 0 2.1.55 2.7 1.58 1.05 1.8 1.55 3.45 1.58 4.95-2.04-.62-4.2-.15-6.22 1.45C11.3 9.7 9.42 7.04 7.2 4.5Z' }),
31
+ h('path', { fill: '#1456B8', d: 'M10.8 13.55c3.3-2.93 5.72-4.24 9.47-2.52-1.2 1.45-2.27 4.18-3.86 5.43-1.67 1.31-3.9.5-5.61-.64v-2.27Z' }),
32
+ h('path', { fill: '#3370FF', d: 'M4.4 8.35c3.47 3.61 7.25 6.1 10.33 5.7 1.06-.14 2.2-.72 3.4-1.72-1.04 2.65-2.6 4.8-5.06 6-2.46 1.2-5.56.52-7.42-.72A2.76 2.76 0 0 1 4.4 15.3V8.35Z' }));
33
+ }
34
+
35
+ export function DingtalkLogoGlyph({ size } = {}) {
36
+ return h('svg', {
37
+ ...dimensions(size),
38
+ viewBox: '0 0 48 48',
39
+ focusable: 'false',
40
+ 'aria-hidden': 'true',
41
+ 'data-im-channel-logo': 'dingtalk',
42
+ }, h('path', {
43
+ fill: 'currentColor',
44
+ d: 'M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498',
45
+ }));
46
+ }
47
+
48
+ export function QqLogoGlyph({ size } = {}) {
49
+ return h('svg', {
50
+ ...dimensions(size),
51
+ viewBox: '0 0 24 24',
52
+ focusable: 'false',
53
+ 'aria-hidden': 'true',
54
+ 'data-im-channel-logo': 'qq',
55
+ }, h('path', {
56
+ fill: 'currentColor',
57
+ d: 'M21.395 15.035a40 40 0 0 0-.803-2.264l-1.079-2.695c.001-.032.014-.562.014-.836C19.526 4.632 17.351 0 12 0S4.474 4.632 4.474 9.241c0 .274.013.804.014.836l-1.08 2.695a39 39 0 0 0-.802 2.264c-1.021 3.283-.69 4.643-.438 4.673.54.065 2.103-2.472 2.103-2.472 0 1.469.756 3.387 2.394 4.771-.612.188-1.363.479-1.845.835-.434.32-.379.646-.301.778.343.578 5.883.369 7.482.189 1.6.18 7.14.389 7.483-.189.078-.132.132-.458-.301-.778-.483-.356-1.233-.646-1.846-.836 1.637-1.384 2.393-3.302 2.393-4.771 0 0 1.563 2.537 2.103 2.472.251-.03.581-1.39-.438-4.673',
58
+ }));
59
+ }
60
+
61
+ export function WecomLogoGlyph({ size } = {}) {
62
+ return h('svg', {
63
+ ...dimensions(size),
64
+ viewBox: '0 0 24 24',
65
+ focusable: 'false',
66
+ 'aria-hidden': 'true',
67
+ 'data-im-channel-logo': 'wecom',
68
+ },
69
+ h('path', {
70
+ fill: 'none',
71
+ stroke: '#3370FF',
72
+ strokeWidth: '2.35',
73
+ strokeLinecap: 'round',
74
+ strokeLinejoin: 'round',
75
+ d: 'M17.7 14.5c1.05-1.12 1.65-2.52 1.65-4.03 0-3.82-3.58-6.92-8-6.92s-8 3.1-8 6.92 3.58 6.92 8 6.92c1.17 0 2.28-.22 3.28-.62',
76
+ }),
77
+ h('path', { fill: '#07C160', d: 'M16.1 15.15c.7-.7 1.83-.7 2.53 0s.7 1.83 0 2.53-1.83.7-2.53 0-.7-1.83 0-2.53Z' }),
78
+ h('path', { fill: '#FFB800', d: 'M19.25 13.45a1.36 1.36 0 1 1 1.92 1.92 1.36 1.36 0 0 1-1.92-1.92Z' }),
79
+ h('path', { fill: '#FF7A00', d: 'M19.55 18.05a1.16 1.16 0 1 1 1.64 1.64 1.16 1.16 0 0 1-1.64-1.64Z' }),
80
+ h('path', { fill: '#3370FF', d: 'M15.25 18.75a.92.92 0 1 1 1.3 1.3.92.92 0 0 1-1.3-1.3Z' }));
81
+ }
82
+
83
+ export function TelegramLogoGlyph({ size } = {}) {
84
+ return h('svg', {
85
+ ...dimensions(size),
86
+ viewBox: '0 0 24 24',
87
+ focusable: 'false',
88
+ 'aria-hidden': 'true',
89
+ 'data-im-channel-logo': 'telegram',
90
+ }, h('path', {
91
+ fill: 'currentColor',
92
+ d: 'M23.95 4.57c-.36-1.45-1.43-1.76-2.82-1.24L1.5 10.9c-1.34.52-1.32 1.27-.24 1.6l5.03 1.57 11.66-7.36c.55-.34 1.05-.16.64.21l-9.44 8.52-.37 5.12c.54 0 .78-.24 1.08-.53l2.59-2.51 5.38 3.97c.99.55 1.7.27 1.95-.92L23.95 4.57Z',
93
+ }));
94
+ }
95
+
96
+ export function DiscordLogoGlyph({ size } = {}) {
97
+ return h('svg', {
98
+ ...dimensions(size),
99
+ viewBox: '0 0 24 24',
100
+ focusable: 'false',
101
+ 'aria-hidden': 'true',
102
+ 'data-im-channel-logo': 'discord',
103
+ }, h('path', {
104
+ fill: 'currentColor',
105
+ d: 'M20.32 4.37a19.8 19.8 0 0 0-4.89-1.51c-.21.38-.46.89-.63 1.29a18.4 18.4 0 0 0-5.59 0 13 13 0 0 0-.64-1.29c-1.71.29-3.36.8-4.89 1.52C.59 9.09-.25 13.68.17 18.2a19.9 19.9 0 0 0 6 3.04c.48-.66.91-1.36 1.28-2.1-.7-.26-1.37-.58-2-.96.17-.12.33-.25.49-.38 3.86 1.79 8.04 1.79 11.86 0 .16.13.32.26.49.38-.64.38-1.31.7-2.01.97.37.73.8 1.44 1.28 2.09a19.8 19.8 0 0 0 6-3.04c.49-5.24-.84-9.79-3.24-13.83ZM8.02 15.42c-1.16 0-2.11-1.07-2.11-2.38s.93-2.38 2.11-2.38c1.18 0 2.13 1.08 2.11 2.38 0 1.31-.93 2.38-2.11 2.38Zm7.95 0c-1.16 0-2.11-1.07-2.11-2.38s.93-2.38 2.11-2.38c1.18 0 2.13 1.08 2.11 2.38 0 1.31-.93 2.38-2.11 2.38Z',
106
+ }));
107
+ }
@@ -0,0 +1,203 @@
1
+ export const DINGTALK_RPC_CHANNEL = '/dingtalk';
2
+
3
+ export const DINGTALK_ENDPOINTS = Object.freeze({
4
+ status: 'connection.status',
5
+ beginProvisioning: 'provision.begin',
6
+ pollProvisioning: 'provision.poll',
7
+ cancelProvisioning: 'provision.cancel',
8
+ bindCredentials: 'bot.bind-credentials',
9
+ reconnectBot: 'bot.reconnect',
10
+ deleteBot: 'bot.delete',
11
+ });
12
+
13
+ const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
14
+ const SNAPSHOT_STATES = new Set(['disconnected', 'offline', 'provisioning', 'connected', 'degraded']);
15
+ const PROVISION_STATES = new Set([
16
+ 'starting',
17
+ 'pending',
18
+ 'scanned',
19
+ 'authorizing',
20
+ 'creating',
21
+ 'connecting',
22
+ 'connected',
23
+ 'expired',
24
+ 'failed',
25
+ 'cancelled',
26
+ ]);
27
+ const HEALTH_STATES = new Set(['healthy', 'checking', 'degraded', 'offline']);
28
+ const FORBIDDEN_ERROR_FIELDS = /(client[_-]?secret|secret[_-]?ref|device[_-]?code|app[_-]?secret|access[_-]?token|token)/i;
29
+ const QR_DATA_URL = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i;
30
+ const MAX_QR_SOURCE_LENGTH = 2 * 1024 * 1024;
31
+
32
+ function isRecord(value) {
33
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
34
+ }
35
+
36
+ function optionalString(value, maxLength = 240) {
37
+ if (typeof value !== 'string') return undefined;
38
+ const trimmed = value.trim();
39
+ return trimmed ? trimmed.slice(0, maxLength) : undefined;
40
+ }
41
+
42
+ function opaqueId(value) {
43
+ const id = optionalString(value, 128);
44
+ return id && /^[a-z\d_-]+$/i.test(id) ? id : undefined;
45
+ }
46
+
47
+ function timestamp(value) {
48
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
49
+ if (typeof value === 'string' && value.trim()) {
50
+ const parsed = Date.parse(value);
51
+ return Number.isNaN(parsed) ? undefined : parsed;
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ function nonNegativeInteger(value) {
57
+ const number = Number(value);
58
+ return Number.isSafeInteger(number) && number >= 0 ? number : 0;
59
+ }
60
+
61
+ function clamp(value, min, max, fallback) {
62
+ const number = Number(value);
63
+ return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
64
+ }
65
+
66
+ function safeErrorCode(value, fallback) {
67
+ const code = optionalString(value, 80);
68
+ return code && /^[a-z][a-z\d_.:-]*$/i.test(code) && !FORBIDDEN_ERROR_FIELDS.test(code)
69
+ ? code
70
+ : fallback;
71
+ }
72
+
73
+ function sanitizeMessage(value, fallback) {
74
+ const message = optionalString(value, 480) ?? fallback;
75
+ if (FORBIDDEN_ERROR_FIELDS.test(message)) return fallback;
76
+ return message.replace(/([=:]\s*)[^\s,;,。]+/g, '$1••••••').slice(0, 240);
77
+ }
78
+
79
+ function normalizeError(value, fallbackCode, fallbackMessage) {
80
+ if (!isRecord(value)) return undefined;
81
+ return {
82
+ code: safeErrorCode(value.code, fallbackCode),
83
+ message: sanitizeMessage(value.message, fallbackMessage),
84
+ };
85
+ }
86
+
87
+ export function unwrapRpcResult(result) {
88
+ if (!isRecord(result) || typeof result.ok !== 'boolean') {
89
+ throw new Error('钉钉服务返回了无法识别的响应');
90
+ }
91
+ if (!result.ok) {
92
+ const error = new Error(sanitizeMessage(result.error?.message, '钉钉操作失败'));
93
+ error.code = safeErrorCode(result.error?.code, 'DINGTALK_RPC_ERROR');
94
+ throw error;
95
+ }
96
+ return result.value;
97
+ }
98
+
99
+ export function safeQrSource(value) {
100
+ if (typeof value !== 'string' || value.length > MAX_QR_SOURCE_LENGTH) return undefined;
101
+ return QR_DATA_URL.test(value) ? value : undefined;
102
+ }
103
+
104
+ export function normalizeProvisioning(value, now = Date.now()) {
105
+ const source = isRecord(value?.provisioning) ? value.provisioning : value;
106
+ if (!isRecord(source)) throw new Error('钉钉服务没有返回扫码绑定进度');
107
+ const attemptId = opaqueId(source.attemptId);
108
+ if (!attemptId) throw new Error('钉钉扫码服务没有返回有效的绑定任务');
109
+
110
+ const reportedStatus = optionalString(source.status, 32) ?? optionalString(source.state, 32);
111
+ const status = PROVISION_STATES.has(reportedStatus) ? reportedStatus : 'failed';
112
+ const expiresAt = timestamp(source.expiresAt)
113
+ ?? now + clamp(source.expiresIn, 1, 2 * 60 * 60, 10 * 60) * 1_000;
114
+ const result = {
115
+ attemptId,
116
+ status,
117
+ expiresAt,
118
+ pollIntervalMs: clamp(source.pollIntervalMs, 1_000, 10_000, 3_000),
119
+ };
120
+ const qrCodeDataUrl = safeQrSource(source.qrCodeDataUrl);
121
+ if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
122
+ if (opaqueId(source.botId)) result.botId = opaqueId(source.botId);
123
+ if (source.alreadyConnected === true) result.alreadyConnected = true;
124
+ const error = normalizeError(
125
+ source.error,
126
+ 'DINGTALK_PROVISION_FAILED',
127
+ '钉钉机器人没有接入完成',
128
+ );
129
+ if (error) result.error = error;
130
+ return result;
131
+ }
132
+
133
+ function normalizeBot(value) {
134
+ if (!isRecord(value)) return undefined;
135
+ const botId = opaqueId(value.botId);
136
+ if (!botId) return undefined;
137
+ const bot = isRecord(value.bot) ? value.bot : {};
138
+ const connected = value.connected === true;
139
+ const reportedState = ACCOUNT_STATES.has(value.state) ? value.state : 'offline';
140
+ const state = connected ? 'connected' : reportedState === 'connected' ? 'connecting' : reportedState;
141
+ const health = isRecord(value.health) ? value.health : {};
142
+ const stats = isRecord(value.stats) ? value.stats : {};
143
+ return {
144
+ botId,
145
+ state,
146
+ connected,
147
+ configured: value.configured !== false,
148
+ bot: {
149
+ name: optionalString(bot.name, 100) ?? '钉钉机器人',
150
+ clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
151
+ },
152
+ health: {
153
+ status: HEALTH_STATES.has(health.status)
154
+ ? health.status
155
+ : connected ? 'healthy' : 'offline',
156
+ summary: optionalString(health.summary, 200)
157
+ ?? (connected ? '钉钉 Stream 长连接运行正常' : '钉钉连接尚未就绪'),
158
+ lastCheckedAt: timestamp(health.lastCheckedAt),
159
+ lastConnectedAt: timestamp(health.lastConnectedAt),
160
+ },
161
+ stats: {
162
+ messagesReceived: nonNegativeInteger(stats.messagesReceived),
163
+ messagesReplied: nonNegativeInteger(stats.messagesReplied),
164
+ },
165
+ error: normalizeError(value.error, 'DINGTALK_ACCOUNT_ERROR', '钉钉连接尚未就绪') ?? null,
166
+ };
167
+ }
168
+
169
+ export function normalizeSnapshot(value) {
170
+ const source = isRecord(value?.snapshot) ? value.snapshot : value;
171
+ if (!isRecord(source) || !Array.isArray(source.bots)) {
172
+ throw new Error('钉钉服务没有返回有效的机器人列表');
173
+ }
174
+ const seen = new Set();
175
+ const bots = source.bots.map(normalizeBot).filter((bot) => {
176
+ if (!bot || seen.has(bot.botId)) return false;
177
+ seen.add(bot.botId);
178
+ return true;
179
+ });
180
+ return {
181
+ schemaVersion: Number.isSafeInteger(source.schemaVersion) ? source.schemaVersion : 1,
182
+ revision: nonNegativeInteger(source.revision),
183
+ state: SNAPSHOT_STATES.has(source.state) ? source.state : 'offline',
184
+ bots,
185
+ totals: {
186
+ configured: bots.length,
187
+ connected: bots.filter((bot) => bot.connected).length,
188
+ },
189
+ provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
190
+ };
191
+ }
192
+
193
+ export function presentError(error) {
194
+ return {
195
+ code: safeErrorCode(error?.code, 'DINGTALK_ERROR'),
196
+ message: sanitizeMessage(error?.message, '钉钉操作失败,请稍后重试'),
197
+ };
198
+ }
199
+
200
+ export function formatRemaining(milliseconds) {
201
+ const seconds = Math.max(0, Math.ceil(Number(milliseconds) / 1_000) || 0);
202
+ return `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`;
203
+ }