@xmanrui/dsh-im 0.2.2 → 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.
- package/README.md +19 -11
- package/lib/client.js +1796 -1094
- package/lib/index.js +116 -115
- package/package.json +2 -2
- package/plugin-src/client/channel-logos.js +13 -0
- package/plugin-src/client/channels/dingtalk/index.js +1 -1
- package/plugin-src/client/channels/feishu/index.js +1 -2
- package/plugin-src/client/channels/qq/index.js +1 -1
- package/plugin-src/client/channels/shared/token-channel.js +1 -2
- package/plugin-src/client/channels/slack/api.js +11 -0
- package/plugin-src/client/channels/slack/index.js +130 -0
- package/plugin-src/client/channels/slack/styles.js +34 -0
- package/plugin-src/client/channels/wecom/index.js +1 -1
- package/plugin-src/client/channels/weixin/index.js +1 -2
- package/plugin-src/client/channels/whatsapp/index.js +1 -1
- package/plugin-src/client/credential-binding.js +1 -2
- package/plugin-src/client/i18n.js +443 -0
- package/plugin-src/client/index.js +29 -4
- package/plugin-src/client/styles.js +16 -8
- package/plugin-src/host/channels/shared/production.mjs +2 -2
- package/plugin-src/host/channels/slack/index.mjs +28 -0
- package/plugin-src/host/channels/slack/production.mjs +91 -0
- package/plugin-src/host/channels/slack/rpc.mjs +127 -0
- package/plugin-src/host/index.mjs +3 -0
- package/scripts/verify-package.mjs +10 -3
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/slack/config-store.mjs +173 -0
- package/src/channels/slack/harness-client.mjs +3 -0
- package/src/channels/slack/manifest.mjs +31 -0
- package/src/channels/slack/slack-api.mjs +259 -0
- package/src/channels/slack/slack-bridge.mjs +15 -0
- package/src/channels/slack/slack-controller.mjs +318 -0
- package/src/channels/slack/slack-runtime.mjs +462 -0
- package/src/channels/slack/state-store.mjs +3 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
|
@@ -37,23 +37,26 @@ const CSS = String.raw`
|
|
|
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
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:
|
|
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 .
|
|
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
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
57
|
.dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
55
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%); }
|
|
56
|
-
.dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge {
|
|
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
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; }
|
|
@@ -120,7 +123,7 @@ const CSS = String.raw`
|
|
|
120
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:
|
|
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
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; }
|
|
@@ -128,7 +131,7 @@ const CSS = String.raw`
|
|
|
128
131
|
.dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
|
129
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:
|
|
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; }
|
|
@@ -141,14 +144,19 @@ const CSS = String.raw`
|
|
|
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
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:
|
|
145
|
-
.dim-panel .dim-botMetric { min-width: 0; padding:
|
|
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"')
|
|
51
|
-
|
|
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.
|
|
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),
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
|
|
6
|
+
const BOT_ID_PATTERN = /^slack_[a-f0-9]{24}$/;
|
|
7
|
+
const BOT_TOKEN_REF_PATTERN = /^DSH_SLACK_BOT_TOKEN_[A-F0-9]{24}$/;
|
|
8
|
+
const APP_TOKEN_REF_PATTERN = /^DSH_SLACK_APP_TOKEN_[A-F0-9]{24}$/;
|
|
9
|
+
|
|
10
|
+
function cleanString(value) {
|
|
11
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function deriveSlackBotIdentity(platformId) {
|
|
15
|
+
const raw = cleanString(platformId);
|
|
16
|
+
if (!raw) throw new TypeError('platformId is required');
|
|
17
|
+
const digest = createHash('sha256').update(raw).digest('hex').slice(0, 24);
|
|
18
|
+
const suffix = digest.toUpperCase();
|
|
19
|
+
return {
|
|
20
|
+
botId: `slack_${digest}`,
|
|
21
|
+
botTokenRef: `DSH_SLACK_BOT_TOKEN_${suffix}`,
|
|
22
|
+
appTokenRef: `DSH_SLACK_APP_TOKEN_${suffix}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function maskSlackBotId(platformId) {
|
|
27
|
+
const value = cleanString(platformId) ?? '';
|
|
28
|
+
const [teamId, userId] = value.split(':');
|
|
29
|
+
if (teamId && userId) return `${teamId.slice(0, 5)}••• · ${userId.slice(0, 5)}•••`;
|
|
30
|
+
return value ? `${value.slice(0, 6)}••••` : 'Slack机器人';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SlackConfigStore {
|
|
34
|
+
#path;
|
|
35
|
+
#value = EMPTY_DOCUMENT;
|
|
36
|
+
#writeQueue = Promise.resolve();
|
|
37
|
+
|
|
38
|
+
constructor(path) {
|
|
39
|
+
this.#path = path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async load() {
|
|
43
|
+
try {
|
|
44
|
+
const normalized = this.#normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
45
|
+
if (!normalized) throw new Error('dsh-im Slack config contains invalid bot data');
|
|
46
|
+
this.#value = normalized;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
49
|
+
this.#value = EMPTY_DOCUMENT;
|
|
50
|
+
}
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
list() {
|
|
55
|
+
return structuredClone(this.#value.bots);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get(botId) {
|
|
59
|
+
const bot = this.#value.bots.find((candidate) => candidate.botId === botId);
|
|
60
|
+
return bot ? structuredClone(bot) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getByPlatformId(platformId) {
|
|
64
|
+
const bot = this.#value.bots.find((candidate) => candidate.platformId === platformId);
|
|
65
|
+
return bot ? structuredClone(bot) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async save(value) {
|
|
69
|
+
const normalized = this.#normalizeBot(value);
|
|
70
|
+
if (!normalized) throw new Error('Refusing to persist incomplete Slack bot data');
|
|
71
|
+
return this.#mutate((bots) => {
|
|
72
|
+
const collision = bots.find((bot) => (
|
|
73
|
+
bot.botId !== normalized.botId
|
|
74
|
+
&& (bot.platformId === normalized.platformId
|
|
75
|
+
|| bot.botTokenRef === normalized.botTokenRef
|
|
76
|
+
|| bot.appTokenRef === normalized.appTokenRef)
|
|
77
|
+
));
|
|
78
|
+
if (collision) throw new Error('Duplicate Slack bot identity');
|
|
79
|
+
const index = bots.findIndex((bot) => bot.botId === normalized.botId);
|
|
80
|
+
if (index === -1) bots.push(normalized);
|
|
81
|
+
else bots[index] = normalized;
|
|
82
|
+
return structuredClone(normalized);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async remove(botId) {
|
|
87
|
+
if (!BOT_ID_PATTERN.test(botId)) throw new TypeError('Invalid Slack bot id');
|
|
88
|
+
return this.#mutate((bots) => {
|
|
89
|
+
const index = bots.findIndex((bot) => bot.botId === botId);
|
|
90
|
+
if (index === -1) return null;
|
|
91
|
+
return structuredClone(bots.splice(index, 1)[0]);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async clear() {
|
|
96
|
+
const operation = this.#writeQueue.then(async () => {
|
|
97
|
+
try {
|
|
98
|
+
await unlink(this.#path);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
101
|
+
}
|
|
102
|
+
this.#value = EMPTY_DOCUMENT;
|
|
103
|
+
});
|
|
104
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
105
|
+
await operation;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#normalizeBot(value) {
|
|
109
|
+
if (!value || typeof value !== 'object') return null;
|
|
110
|
+
const botId = cleanString(value.botId);
|
|
111
|
+
const platformId = cleanString(value.platformId);
|
|
112
|
+
const botTokenRef = cleanString(value.botTokenRef);
|
|
113
|
+
const appTokenRef = cleanString(value.appTokenRef);
|
|
114
|
+
const name = cleanString(value.name);
|
|
115
|
+
if (!botId || !platformId || !botTokenRef || !appTokenRef || !name
|
|
116
|
+
|| !BOT_ID_PATTERN.test(botId)
|
|
117
|
+
|| !BOT_TOKEN_REF_PATTERN.test(botTokenRef)
|
|
118
|
+
|| !APP_TOKEN_REF_PATTERN.test(appTokenRef)) return null;
|
|
119
|
+
const derived = deriveSlackBotIdentity(platformId);
|
|
120
|
+
if (derived.botId !== botId
|
|
121
|
+
|| derived.botTokenRef !== botTokenRef
|
|
122
|
+
|| derived.appTokenRef !== appTokenRef) return null;
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
botId,
|
|
125
|
+
platformId,
|
|
126
|
+
botTokenRef,
|
|
127
|
+
appTokenRef,
|
|
128
|
+
name,
|
|
129
|
+
username: cleanString(value.username),
|
|
130
|
+
teamId: cleanString(value.teamId),
|
|
131
|
+
teamName: cleanString(value.teamName),
|
|
132
|
+
createdAt: cleanString(value.createdAt) ?? new Date().toISOString(),
|
|
133
|
+
connectedAt: cleanString(value.connectedAt),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#normalizeDocument(value) {
|
|
138
|
+
if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null;
|
|
139
|
+
const bots = value.bots.map((bot) => this.#normalizeBot(bot));
|
|
140
|
+
if (bots.some((bot) => bot === null)) return null;
|
|
141
|
+
const ids = new Set();
|
|
142
|
+
const platformIds = new Set();
|
|
143
|
+
const refs = new Set();
|
|
144
|
+
for (const bot of bots) {
|
|
145
|
+
if (ids.has(bot.botId) || platformIds.has(bot.platformId)
|
|
146
|
+
|| refs.has(bot.botTokenRef) || refs.has(bot.appTokenRef)) return null;
|
|
147
|
+
ids.add(bot.botId);
|
|
148
|
+
platformIds.add(bot.platformId);
|
|
149
|
+
refs.add(bot.botTokenRef);
|
|
150
|
+
refs.add(bot.appTokenRef);
|
|
151
|
+
}
|
|
152
|
+
return Object.freeze({ version: 1, bots: Object.freeze(bots) });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #mutate(mutator) {
|
|
156
|
+
let result;
|
|
157
|
+
const operation = this.#writeQueue.then(async () => {
|
|
158
|
+
const bots = [...this.#value.bots];
|
|
159
|
+
result = mutator(bots);
|
|
160
|
+
const document = Object.freeze({ version: 1, bots: Object.freeze(bots) });
|
|
161
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
162
|
+
const temporary = `${this.#path}.tmp`;
|
|
163
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
164
|
+
encoding: 'utf8', mode: 0o600,
|
|
165
|
+
});
|
|
166
|
+
await rename(temporary, this.#path);
|
|
167
|
+
this.#value = document;
|
|
168
|
+
});
|
|
169
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
170
|
+
await operation;
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const SLACK_APP_MANIFEST_YAML = `_metadata:
|
|
2
|
+
major_version: 1
|
|
3
|
+
display_information:
|
|
4
|
+
name: DeepSeek Harness
|
|
5
|
+
description: Connect Slack conversations to a local DeepSeek Harness agent.
|
|
6
|
+
background_color: "#4A154B"
|
|
7
|
+
features:
|
|
8
|
+
app_home:
|
|
9
|
+
home_tab_enabled: false
|
|
10
|
+
messages_tab_enabled: true
|
|
11
|
+
messages_tab_read_only_enabled: false
|
|
12
|
+
bot_user:
|
|
13
|
+
display_name: DeepSeek Harness
|
|
14
|
+
always_online: false
|
|
15
|
+
oauth_config:
|
|
16
|
+
scopes:
|
|
17
|
+
bot:
|
|
18
|
+
- app_mentions:read
|
|
19
|
+
- chat:write
|
|
20
|
+
- im:history
|
|
21
|
+
settings:
|
|
22
|
+
event_subscriptions:
|
|
23
|
+
bot_events:
|
|
24
|
+
- app_mention
|
|
25
|
+
- message.im
|
|
26
|
+
org_deploy_enabled: false
|
|
27
|
+
socket_mode_enabled: true
|
|
28
|
+
token_rotation_enabled: false
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const SLACK_CREATE_APP_URL = 'https://api.slack.com/apps?new_app=1';
|