@zhin.js/adapter-slack 4.1.0 → 4.1.1
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/CHANGELOG.md +41 -0
- package/README.md +58 -250
- package/adapters/slack.ts +34 -0
- package/agent/tools/add_reaction.ts +2 -2
- package/agent/tools/archive_channel.ts +2 -2
- package/agent/tools/edit_message.ts +5 -4
- package/agent/tools/invite_to_channel.ts +2 -2
- package/agent/tools/pin_message.ts +2 -2
- package/agent/tools/remove_reaction.ts +2 -2
- package/agent/tools/set_purpose.ts +2 -2
- package/agent/tools/set_topic.ts +2 -2
- package/agent/tools/unarchive.ts +2 -2
- package/agent/tools/unpin_message.ts +2 -2
- package/agent/tools/user_info.ts +2 -2
- package/lib/endpoint.d.ts +147 -0
- package/lib/endpoint.js +335 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +11 -0
- package/lib/markdown-to-mrkdwn.d.ts +9 -0
- package/lib/markdown-to-mrkdwn.js +121 -0
- package/lib/mrkdwn-to-markdown.d.ts +4 -0
- package/lib/{src/mrkdwn-to-markdown.js → mrkdwn-to-markdown.js} +0 -1
- package/lib/platform-permit.d.ts +16 -0
- package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
- package/lib/protocol.d.ts +143 -0
- package/lib/protocol.js +225 -0
- package/lib/slack-agent-deps.d.ts +28 -0
- package/lib/slack-agent-deps.js +30 -0
- package/lib/slack-inbound-filter.d.ts +12 -0
- package/lib/{src/slack-inbound-filter.js → slack-inbound-filter.js} +0 -1
- package/lib/slack-message-ref.d.ts +7 -0
- package/lib/{src/slack-message-ref.js → slack-message-ref.js} +0 -1
- package/lib/slack-outbound.d.ts +24 -0
- package/lib/slack-outbound.js +109 -0
- package/lib/slack-reaction.d.ts +1 -0
- package/lib/{src/slack-reaction.js → slack-reaction.js} +0 -1
- package/lib/slack-response-url.d.ts +5 -0
- package/lib/{src/slack-response-url.js → slack-response-url.js} +0 -1
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +69 -0
- package/package.json +41 -20
- package/plugin.ts +13 -0
- package/schema.json +38 -0
- package/src/endpoint.ts +317 -227
- package/src/index.ts +62 -66
- package/src/markdown-to-mrkdwn.ts +63 -4
- package/src/platform-permit.ts +1 -1
- package/src/protocol.ts +380 -0
- package/src/slack-agent-deps.ts +46 -11
- package/src/slack-inbound-filter.ts +1 -1
- package/src/slack-outbound.ts +32 -134
- package/src/webhook.ts +103 -0
- package/lib/agent/tools/add_reaction.js +0 -21
- package/lib/agent/tools/add_reaction.js.map +0 -1
- package/lib/agent/tools/archive_channel.js +0 -21
- package/lib/agent/tools/archive_channel.js.map +0 -1
- package/lib/agent/tools/edit_message.js +0 -22
- package/lib/agent/tools/edit_message.js.map +0 -1
- package/lib/agent/tools/invite_to_channel.js +0 -22
- package/lib/agent/tools/invite_to_channel.js.map +0 -1
- package/lib/agent/tools/pin_message.js +0 -22
- package/lib/agent/tools/pin_message.js.map +0 -1
- package/lib/agent/tools/remove_reaction.js +0 -21
- package/lib/agent/tools/remove_reaction.js.map +0 -1
- package/lib/agent/tools/set_purpose.js +0 -22
- package/lib/agent/tools/set_purpose.js.map +0 -1
- package/lib/agent/tools/set_topic.js +0 -22
- package/lib/agent/tools/set_topic.js.map +0 -1
- package/lib/agent/tools/unarchive.js +0 -21
- package/lib/agent/tools/unarchive.js.map +0 -1
- package/lib/agent/tools/unpin_message.js +0 -22
- package/lib/agent/tools/unpin_message.js.map +0 -1
- package/lib/agent/tools/user_info.js +0 -28
- package/lib/agent/tools/user_info.js.map +0 -1
- package/lib/src/adapter.js +0 -62
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/endpoint.js +0 -312
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/event-dispatcher.js +0 -233
- package/lib/src/event-dispatcher.js.map +0 -1
- package/lib/src/index.js +0 -51
- package/lib/src/index.js.map +0 -1
- package/lib/src/markdown-to-mrkdwn.js +0 -61
- package/lib/src/markdown-to-mrkdwn.js.map +0 -1
- package/lib/src/mrkdwn-to-markdown.js.map +0 -1
- package/lib/src/platform-permit.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/signing.js +0 -21
- package/lib/src/signing.js.map +0 -1
- package/lib/src/slack-agent-deps.js +0 -10
- package/lib/src/slack-agent-deps.js.map +0 -1
- package/lib/src/slack-inbound-filter.js.map +0 -1
- package/lib/src/slack-inbound.js +0 -127
- package/lib/src/slack-inbound.js.map +0 -1
- package/lib/src/slack-message-ref.js.map +0 -1
- package/lib/src/slack-outbound.js +0 -223
- package/lib/src/slack-outbound.js.map +0 -1
- package/lib/src/slack-reaction.js.map +0 -1
- package/lib/src/slack-response-url.js.map +0 -1
- package/lib/src/slack-side-events.js +0 -64
- package/lib/src/slack-side-events.js.map +0 -1
- package/lib/src/transport-http.js +0 -83
- package/lib/src/transport-http.js.map +0 -1
- package/lib/src/transport-socket.js +0 -68
- package/lib/src/transport-socket.js.map +0 -1
- package/lib/src/types.js +0 -5
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -75
- package/src/event-dispatcher.ts +0 -255
- package/src/segment-mapper.ts +0 -1
- package/src/signing.ts +0 -28
- package/src/slack-inbound.ts +0 -140
- package/src/slack-side-events.ts +0 -74
- package/src/transport-http.ts +0 -95
- package/src/transport-socket.ts +0 -77
- package/src/types.ts +0 -113
package/src/index.ts
CHANGED
|
@@ -1,72 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
export {
|
|
2
|
+
formatInboundContent,
|
|
3
|
+
formatInteractionContent,
|
|
4
|
+
formatOutboundWire,
|
|
5
|
+
formatSlashContent,
|
|
6
|
+
headerValue,
|
|
7
|
+
inboundMessageId,
|
|
8
|
+
keyboardToBlockKitBlocks,
|
|
9
|
+
normalizeWebhookPath,
|
|
10
|
+
readTextBody,
|
|
11
|
+
resolveSlackChannelType,
|
|
12
|
+
resolveSlackConfig,
|
|
13
|
+
verifySlackSignature,
|
|
14
|
+
type ResolvedSlackConfig,
|
|
15
|
+
type SlackAdapterConfig,
|
|
16
|
+
type SlackBlockAction,
|
|
17
|
+
type SlackEvent,
|
|
18
|
+
type SlackEventEnvelope,
|
|
19
|
+
type SlackInteractionPayload,
|
|
20
|
+
type SlackMessageEvent,
|
|
21
|
+
type SlackSlashCommand,
|
|
22
|
+
type SlackUrlVerification,
|
|
23
|
+
type SlackWireSegment,
|
|
24
|
+
} from './protocol.js';
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
export {
|
|
27
|
+
SlackEndpoint,
|
|
28
|
+
type SlackEndpointOptions,
|
|
29
|
+
type SlackSocketLike,
|
|
30
|
+
type SlackWebClientLike,
|
|
31
|
+
} from './endpoint.js';
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await adapter.start();
|
|
34
|
-
return adapter;
|
|
35
|
-
},
|
|
36
|
-
dispose: async (adapter: SlackAdapter) => {
|
|
37
|
-
await adapter.stop();
|
|
38
|
-
},
|
|
39
|
-
});
|
|
33
|
+
export {
|
|
34
|
+
registerSlackWebhookRoutes,
|
|
35
|
+
handleSlackWebhookRequest,
|
|
36
|
+
type SlackWebhookHandler,
|
|
37
|
+
} from './webhook.js';
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
return () => {};
|
|
50
|
-
});
|
|
39
|
+
export {
|
|
40
|
+
getSlackAgentDeps,
|
|
41
|
+
registerSlackAgentEndpoint,
|
|
42
|
+
setSlackAgentDeps,
|
|
43
|
+
type SlackAgentDeps,
|
|
44
|
+
type SlackAgentEndpoint,
|
|
45
|
+
} from './slack-agent-deps.js';
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return endpoint;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
setSlackAgentDeps({ getEndpoint, getAdapter: () => slack });
|
|
47
|
+
export {
|
|
48
|
+
checkSlackPlatformPermit,
|
|
49
|
+
normalizeSlackSenderForPermit,
|
|
50
|
+
platformPermit,
|
|
51
|
+
registerSlackPlatformPermitChecker,
|
|
52
|
+
slackGroupPermitResolver,
|
|
53
|
+
} from './platform-permit.js';
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
|
|
55
|
+
export {
|
|
56
|
+
formatSlackMessageRef,
|
|
57
|
+
parseSlackMessageRef,
|
|
58
|
+
slackMessageTs,
|
|
59
|
+
} from './slack-message-ref.js';
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
}
|
|
61
|
+
export { normalizeSlackReactionName } from './slack-reaction.js';
|
|
62
|
+
export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
|
|
63
|
+
export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
64
|
+
export {
|
|
65
|
+
createSlackInboundFilterState,
|
|
66
|
+
shouldDropSlackInboundMessage,
|
|
67
|
+
} from './slack-inbound-filter.js';
|
|
68
|
+
export { editSlackContent, sendSlackContent } from './slack-outbound.js';
|
|
@@ -51,8 +51,67 @@ export function splitMrkdwnText(text: string, maxLen = SLACK_MRKDWN_TEXT_MAX): s
|
|
|
51
51
|
|
|
52
52
|
/** 通知栏 / 无障碍 fallback 用的纯文本 */
|
|
53
53
|
export function mrkdwnToPlainFallback(text: string): string {
|
|
54
|
-
return text
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
return stripMrkdwnMarkers(stripAngleBrackets(rewriteAngleLinks(text)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `<url|text>` → `text (url)`。
|
|
59
|
+
* 线性扫描,语义等价于 `/<([^|>]+)\|([^>]+)>/g`(分组一不含 `|`,
|
|
60
|
+
* 因此切分点恒为首个 `|`),但避免量词重叠在超长 `<...` 输入上的
|
|
61
|
+
* 二次方回溯(js/polynomial-redos)。
|
|
62
|
+
*/
|
|
63
|
+
function rewriteAngleLinks(text: string): string {
|
|
64
|
+
let out = '';
|
|
65
|
+
let i = 0;
|
|
66
|
+
while (i < text.length) {
|
|
67
|
+
const open = text.indexOf('<', i);
|
|
68
|
+
if (open < 0) break;
|
|
69
|
+
const close = text.indexOf('>', open + 1);
|
|
70
|
+
if (close < 0) break;
|
|
71
|
+
const inner = text.slice(open + 1, close);
|
|
72
|
+
const pipe = inner.indexOf('|');
|
|
73
|
+
if (pipe > 0 && pipe < inner.length - 1) {
|
|
74
|
+
out += text.slice(i, open);
|
|
75
|
+
out += `${inner.slice(pipe + 1)} (${inner.slice(0, pipe)})`;
|
|
76
|
+
i = close + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// 非链接形态:本趟不匹配,原样保留 `<` 并继续(与原正则行为一致)。
|
|
80
|
+
out += text.slice(i, open + 1);
|
|
81
|
+
i = open + 1;
|
|
82
|
+
}
|
|
83
|
+
return out + text.slice(i);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 剥掉剩余 `<...>` 的尖括号。线性扫描,等价于 `/<([^>]+)>/g`
|
|
88
|
+
* (空内容 `<>` 不匹配、原样保留)。
|
|
89
|
+
*/
|
|
90
|
+
function stripAngleBrackets(text: string): string {
|
|
91
|
+
let out = '';
|
|
92
|
+
let i = 0;
|
|
93
|
+
while (i < text.length) {
|
|
94
|
+
const open = text.indexOf('<', i);
|
|
95
|
+
if (open < 0) break;
|
|
96
|
+
const close = text.indexOf('>', open + 1);
|
|
97
|
+
if (close < 0) break;
|
|
98
|
+
if (close === open + 1) {
|
|
99
|
+
out += text.slice(i, open + 1);
|
|
100
|
+
i = open + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
out += text.slice(i, open);
|
|
104
|
+
out += text.slice(open + 1, close);
|
|
105
|
+
i = close + 1;
|
|
106
|
+
}
|
|
107
|
+
return out + text.slice(i);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 去掉 `*` `_` `~` `` ` `` 样式符号(等价于 `/[*_~`]/g`,逐字符线性)。 */
|
|
111
|
+
function stripMrkdwnMarkers(text: string): string {
|
|
112
|
+
let out = '';
|
|
113
|
+
for (const ch of text) {
|
|
114
|
+
if (ch !== '*' && ch !== '_' && ch !== '~' && ch !== '`') out += ch;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
58
117
|
}
|
package/src/platform-permit.ts
CHANGED
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import type { IncomingMessage } from 'node:http';
|
|
7
|
+
import { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
8
|
+
|
|
9
|
+
const SLACK_SIG_VERSION = 'v0';
|
|
10
|
+
const MAX_TIMESTAMP_DRIFT_SECONDS = 300;
|
|
11
|
+
|
|
12
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
13
|
+
export interface SlackAdapterConfig {
|
|
14
|
+
readonly name?: string;
|
|
15
|
+
readonly token?: string;
|
|
16
|
+
readonly signingSecret?: string;
|
|
17
|
+
readonly appToken?: string;
|
|
18
|
+
/** Default true (Socket Mode). Set false for HTTP Events API via httpHostToken. */
|
|
19
|
+
readonly socketMode?: boolean;
|
|
20
|
+
readonly webhookPath?: string;
|
|
21
|
+
readonly clientPingTimeout?: number;
|
|
22
|
+
/** Transitional: legacy root `endpoints[]` with `context: slack`. */
|
|
23
|
+
readonly endpoints?: ReadonlyArray<Partial<ResolvedSlackConfig> & {
|
|
24
|
+
readonly context?: string;
|
|
25
|
+
readonly socketMode?: boolean;
|
|
26
|
+
readonly signingSecret?: string;
|
|
27
|
+
readonly appToken?: string;
|
|
28
|
+
readonly webhookPath?: string;
|
|
29
|
+
readonly clientPingTimeout?: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResolvedSlackConfig {
|
|
34
|
+
readonly context: 'slack';
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly token: string;
|
|
37
|
+
readonly mode: 'socket' | 'http';
|
|
38
|
+
readonly signingSecret: string;
|
|
39
|
+
readonly appToken?: string;
|
|
40
|
+
readonly webhookPath: string;
|
|
41
|
+
readonly clientPingTimeout: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SlackEventEnvelope {
|
|
45
|
+
readonly token?: string;
|
|
46
|
+
readonly team_id?: string;
|
|
47
|
+
readonly api_app_id?: string;
|
|
48
|
+
readonly event: SlackEvent;
|
|
49
|
+
readonly type: 'event_callback';
|
|
50
|
+
readonly event_id?: string;
|
|
51
|
+
readonly event_time?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SlackUrlVerification {
|
|
55
|
+
readonly type: 'url_verification';
|
|
56
|
+
readonly token: string;
|
|
57
|
+
readonly challenge: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SlackInteractionPayload {
|
|
61
|
+
readonly type: 'block_actions' | 'message_action' | 'shortcut' | 'view_submission' | 'view_closed';
|
|
62
|
+
readonly trigger_id?: string;
|
|
63
|
+
readonly user: { id: string; username?: string; name?: string; team_id?: string };
|
|
64
|
+
readonly channel?: { id: string; name?: string };
|
|
65
|
+
readonly message?: { ts: string; text?: string; [key: string]: unknown };
|
|
66
|
+
readonly actions?: SlackBlockAction[];
|
|
67
|
+
readonly response_url?: string;
|
|
68
|
+
readonly [key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SlackBlockAction {
|
|
72
|
+
readonly type: string;
|
|
73
|
+
readonly action_id: string;
|
|
74
|
+
readonly block_id: string;
|
|
75
|
+
readonly value?: string;
|
|
76
|
+
readonly text?: { type: string; text: string };
|
|
77
|
+
readonly action_ts?: string;
|
|
78
|
+
readonly [key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SlackSlashCommand {
|
|
82
|
+
readonly token: string;
|
|
83
|
+
readonly team_id: string;
|
|
84
|
+
readonly channel_id: string;
|
|
85
|
+
readonly channel_name: string;
|
|
86
|
+
readonly user_id: string;
|
|
87
|
+
readonly user_name: string;
|
|
88
|
+
readonly command: string;
|
|
89
|
+
readonly text: string;
|
|
90
|
+
readonly response_url: string;
|
|
91
|
+
readonly trigger_id: string;
|
|
92
|
+
readonly api_app_id?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface SlackEvent {
|
|
96
|
+
readonly type: string;
|
|
97
|
+
readonly ts?: string;
|
|
98
|
+
readonly event_ts?: string;
|
|
99
|
+
readonly user?: string;
|
|
100
|
+
readonly channel?: string;
|
|
101
|
+
readonly channel_type?: string;
|
|
102
|
+
readonly text?: string;
|
|
103
|
+
readonly thread_ts?: string;
|
|
104
|
+
readonly subtype?: string;
|
|
105
|
+
readonly bot_id?: string;
|
|
106
|
+
readonly [key: string]: unknown;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type SlackMessageEvent = SlackEvent & {
|
|
110
|
+
readonly type: 'message' | 'app_mention';
|
|
111
|
+
readonly ts: string;
|
|
112
|
+
readonly channel: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export interface SlackWireSegment {
|
|
116
|
+
readonly type: string;
|
|
117
|
+
readonly data?: Record<string, unknown>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveSlackConfig(config: SlackAdapterConfig = {}): ResolvedSlackConfig {
|
|
121
|
+
const entry = config.endpoints?.find((item) => item.context === 'slack');
|
|
122
|
+
const token = config.token
|
|
123
|
+
?? entry?.token
|
|
124
|
+
?? process.env.SLACK_BOT_TOKEN
|
|
125
|
+
?? process.env.SLACK_TOKEN;
|
|
126
|
+
if (!token) {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
'Slack adapter requires token (plugins.<key>.token or endpoints with context: slack)',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
133
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
134
|
+
|| process.env.SLACK_BOT_NAME
|
|
135
|
+
|| 'slack-bot';
|
|
136
|
+
|
|
137
|
+
const socketMode = config.socketMode ?? entry?.socketMode;
|
|
138
|
+
// Prefer Socket Mode (default true) — no public URL required.
|
|
139
|
+
const mode: 'socket' | 'http' = socketMode === false ? 'http' : 'socket';
|
|
140
|
+
|
|
141
|
+
const signingSecret = config.signingSecret
|
|
142
|
+
?? entry?.signingSecret
|
|
143
|
+
?? process.env.SLACK_SIGNING_SECRET
|
|
144
|
+
?? '';
|
|
145
|
+
const appToken = config.appToken
|
|
146
|
+
?? entry?.appToken
|
|
147
|
+
?? process.env.SLACK_APP_TOKEN
|
|
148
|
+
?? undefined;
|
|
149
|
+
|
|
150
|
+
if (mode === 'socket' && !appToken) {
|
|
151
|
+
throw new TypeError(
|
|
152
|
+
'Slack Socket Mode requires appToken (xapp-...); set socketMode: false for HTTP Events API',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (mode === 'http' && !signingSecret) {
|
|
156
|
+
throw new TypeError(
|
|
157
|
+
'Slack HTTP Events API requires signingSecret',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
context: 'slack',
|
|
163
|
+
name,
|
|
164
|
+
token,
|
|
165
|
+
mode,
|
|
166
|
+
signingSecret,
|
|
167
|
+
appToken,
|
|
168
|
+
webhookPath: normalizeWebhookPath(
|
|
169
|
+
config.webhookPath ?? entry?.webhookPath ?? '/slack/events',
|
|
170
|
+
),
|
|
171
|
+
clientPingTimeout: config.clientPingTimeout
|
|
172
|
+
?? entry?.clientPingTimeout
|
|
173
|
+
?? 15_000,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function normalizeWebhookPath(path: string): string {
|
|
178
|
+
const trimmed = path.trim() || '/slack/events';
|
|
179
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function resolveSlackChannelType(event: Pick<SlackEvent, 'channel_type'>): 'private' | 'group' {
|
|
183
|
+
return event.channel_type === 'im' ? 'private' : 'group';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Build inbound text for MessageGateway.receive. */
|
|
187
|
+
export function formatInboundContent(event: SlackMessageEvent | SlackEvent): string {
|
|
188
|
+
const text = typeof event.text === 'string' ? event.text : '';
|
|
189
|
+
if (text) return mrkdwnToMarkdown(text);
|
|
190
|
+
if ('files' in event && Array.isArray(event.files) && event.files.length > 0) {
|
|
191
|
+
return '[file]';
|
|
192
|
+
}
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatInteractionContent(payload: SlackInteractionPayload): string {
|
|
197
|
+
const action = payload.actions?.[0];
|
|
198
|
+
if (!action) return '[action]';
|
|
199
|
+
const label = action.text?.text ?? action.value ?? action.action_id;
|
|
200
|
+
return `[action: ${action.action_id}${label ? ` ${label}` : ''}]`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatSlashContent(cmd: SlackSlashCommand): string {
|
|
204
|
+
return `${cmd.command} ${cmd.text}`.trim();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function inboundMessageId(event: SlackMessageEvent): string {
|
|
208
|
+
return `${event.channel}:${event.ts}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Wire-encode an already-rendered outbound payload into Slack text + Block Kit blocks.
|
|
213
|
+
* Segment canonicalization is intentionally not done here.
|
|
214
|
+
*/
|
|
215
|
+
export function formatOutboundWire(payload: unknown): {
|
|
216
|
+
text: string;
|
|
217
|
+
blocks: Record<string, unknown>[];
|
|
218
|
+
attachments: Record<string, unknown>[];
|
|
219
|
+
files: Array<{ buffer?: Buffer; url?: string; path?: string; name?: string }>;
|
|
220
|
+
} {
|
|
221
|
+
if (typeof payload === 'string') {
|
|
222
|
+
return { text: payload, blocks: [], attachments: [], files: [] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const items: Array<string | SlackWireSegment> = Array.isArray(payload)
|
|
226
|
+
? payload as Array<string | SlackWireSegment>
|
|
227
|
+
: payload && typeof payload === 'object' && 'type' in (payload as object)
|
|
228
|
+
? [payload as SlackWireSegment]
|
|
229
|
+
: [];
|
|
230
|
+
|
|
231
|
+
if (items.length === 0) {
|
|
232
|
+
const text = payload == null
|
|
233
|
+
? ''
|
|
234
|
+
: typeof payload === 'object'
|
|
235
|
+
? JSON.stringify(payload)
|
|
236
|
+
: String(payload);
|
|
237
|
+
return { text, blocks: [], attachments: [], files: [] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let text = '';
|
|
241
|
+
const blocks: Record<string, unknown>[] = [];
|
|
242
|
+
const attachments: Record<string, unknown>[] = [];
|
|
243
|
+
const files: Array<{ buffer?: Buffer; url?: string; path?: string; name?: string }> = [];
|
|
244
|
+
|
|
245
|
+
for (const item of items) {
|
|
246
|
+
if (typeof item === 'string') {
|
|
247
|
+
text += item;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const data = item.data ?? {};
|
|
251
|
+
switch (item.type) {
|
|
252
|
+
case 'text':
|
|
253
|
+
text += String(data.text ?? data.content ?? '');
|
|
254
|
+
break;
|
|
255
|
+
case 'at':
|
|
256
|
+
case 'mention':
|
|
257
|
+
text += `<@${String(data.id ?? data.target ?? '')}>`;
|
|
258
|
+
break;
|
|
259
|
+
case 'channel_mention':
|
|
260
|
+
text += `<#${String(data.id ?? '')}>`;
|
|
261
|
+
break;
|
|
262
|
+
case 'link':
|
|
263
|
+
if (data.text && data.text !== data.url) {
|
|
264
|
+
text += `<${String(data.url)}|${String(data.text)}>`;
|
|
265
|
+
} else {
|
|
266
|
+
text += `<${String(data.url ?? '')}>`;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case 'image':
|
|
270
|
+
if (typeof data.url === 'string' && data.url) {
|
|
271
|
+
attachments.push({
|
|
272
|
+
image_url: data.url,
|
|
273
|
+
title: String(data.name ?? data.title ?? ''),
|
|
274
|
+
});
|
|
275
|
+
} else if (data.media && typeof data.media === 'object') {
|
|
276
|
+
files.push(resolveMediaToFile(data.media as { kind: string; value: string }, String(data.alt ?? 'image')));
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case 'audio':
|
|
280
|
+
case 'video':
|
|
281
|
+
case 'file':
|
|
282
|
+
if (data.media && typeof data.media === 'object') {
|
|
283
|
+
files.push(resolveMediaToFile(data.media as { kind: string; value: string }, String(data.name ?? item.type)));
|
|
284
|
+
} else if (data.file || data.url) {
|
|
285
|
+
files.push({
|
|
286
|
+
path: typeof data.file === 'string' ? data.file : undefined,
|
|
287
|
+
url: typeof data.url === 'string' ? data.url : undefined,
|
|
288
|
+
name: String(data.name ?? item.type),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case 'keyboard':
|
|
293
|
+
blocks.push(...keyboardToBlockKitBlocks(data));
|
|
294
|
+
break;
|
|
295
|
+
default:
|
|
296
|
+
text += String(data.text ?? `[${item.type}]`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { text, blocks, attachments, files };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function keyboardToBlockKitBlocks(data: Record<string, unknown>): Record<string, unknown>[] {
|
|
304
|
+
const rows = data.rows as Array<Array<Record<string, unknown>>> | undefined;
|
|
305
|
+
if (!rows?.length) return [];
|
|
306
|
+
|
|
307
|
+
const blocks: Record<string, unknown>[] = [];
|
|
308
|
+
for (const row of rows) {
|
|
309
|
+
const elements = row.slice(0, 5).map((btn, index) => ({
|
|
310
|
+
type: 'button',
|
|
311
|
+
text: { type: 'plain_text', text: String(btn.label ?? btn.text ?? 'button').slice(0, 75) },
|
|
312
|
+
action_id: String(btn.id ?? btn.action_id ?? `btn_${blocks.length}_${index}`),
|
|
313
|
+
...(btn.value != null ? { value: String(btn.value) } : {}),
|
|
314
|
+
...(btn.style === 'primary' ? { style: 'primary' } : {}),
|
|
315
|
+
...(btn.style === 'danger' ? { style: 'danger' } : {}),
|
|
316
|
+
}));
|
|
317
|
+
if (elements.length > 0) {
|
|
318
|
+
blocks.push({ type: 'actions', elements });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return blocks;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function resolveMediaToFile(
|
|
325
|
+
media: { kind: string; value: string },
|
|
326
|
+
name: string,
|
|
327
|
+
): { buffer?: Buffer; url?: string; name: string } {
|
|
328
|
+
if (media.kind === 'base64') {
|
|
329
|
+
return { buffer: Buffer.from(media.value, 'base64'), name };
|
|
330
|
+
}
|
|
331
|
+
return { url: media.value, name };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function verifySlackSignature(
|
|
335
|
+
signingSecret: string,
|
|
336
|
+
rawBody: string,
|
|
337
|
+
timestamp: string,
|
|
338
|
+
signature: string,
|
|
339
|
+
): boolean {
|
|
340
|
+
const now = Math.floor(Date.now() / 1000);
|
|
341
|
+
const ts = parseInt(timestamp, 10);
|
|
342
|
+
if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_TIMESTAMP_DRIFT_SECONDS) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const baseString = `${SLACK_SIG_VERSION}:${timestamp}:${rawBody}`;
|
|
347
|
+
const hmac = createHmac('sha256', signingSecret).update(baseString).digest('hex');
|
|
348
|
+
const expected = `${SLACK_SIG_VERSION}=${hmac}`;
|
|
349
|
+
|
|
350
|
+
if (expected.length !== signature.length) return false;
|
|
351
|
+
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function readTextBody(
|
|
355
|
+
request: IncomingMessage,
|
|
356
|
+
options: { readonly limit?: number } = {},
|
|
357
|
+
): Promise<string> {
|
|
358
|
+
const limit = options.limit ?? 1_048_576;
|
|
359
|
+
const chunks: Buffer[] = [];
|
|
360
|
+
let size = 0;
|
|
361
|
+
for await (const chunk of request) {
|
|
362
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
363
|
+
size += buffer.length;
|
|
364
|
+
if (size > limit) {
|
|
365
|
+
request.destroy();
|
|
366
|
+
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
367
|
+
}
|
|
368
|
+
chunks.push(buffer);
|
|
369
|
+
}
|
|
370
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function headerValue(
|
|
374
|
+
headers: IncomingMessage['headers'],
|
|
375
|
+
name: string,
|
|
376
|
+
): string {
|
|
377
|
+
const raw = headers[name.toLowerCase()];
|
|
378
|
+
if (Array.isArray(raw)) return raw[0] ?? '';
|
|
379
|
+
return raw ?? '';
|
|
380
|
+
}
|
package/src/slack-agent-deps.ts
CHANGED
|
@@ -1,22 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Agent tool deps for slack.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
export interface SlackAgentEndpoint {
|
|
7
|
+
inviteToChannel(channel: string, users: string[]): Promise<boolean>;
|
|
8
|
+
kickFromChannel(channel: string, user: string): Promise<boolean>;
|
|
9
|
+
setChannelTopic(channel: string, topic: string): Promise<boolean>;
|
|
10
|
+
setChannelPurpose(channel: string, purpose: string): Promise<boolean>;
|
|
11
|
+
archiveChannel(channel: string): Promise<boolean>;
|
|
12
|
+
unarchiveChannel(channel: string): Promise<boolean>;
|
|
13
|
+
renameChannel(channel: string, name: string): Promise<boolean>;
|
|
14
|
+
getChannelMembers(channel: string): Promise<string[]>;
|
|
15
|
+
getChannelInfo(channel: string): Promise<unknown>;
|
|
16
|
+
getUserInfo(user: string): Promise<unknown>;
|
|
17
|
+
addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
|
|
18
|
+
removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
|
|
19
|
+
pinMessage(channel: string, timestamp: string): Promise<boolean>;
|
|
20
|
+
unpinMessage(channel: string, timestamp: string): Promise<boolean>;
|
|
21
|
+
editMessage(channel: string, messageTs: string, content: unknown): Promise<void>;
|
|
22
|
+
}
|
|
7
23
|
|
|
8
24
|
export interface SlackAgentDeps {
|
|
9
|
-
getEndpoint: (endpointId: string) =>
|
|
10
|
-
getAdapter: () => SlackAdapter;
|
|
25
|
+
getEndpoint: (endpointId: string) => SlackAgentEndpoint;
|
|
11
26
|
}
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
const endpoints = new Map<string, SlackAgentEndpoint>();
|
|
29
|
+
let override: SlackAgentDeps | null = null;
|
|
30
|
+
|
|
31
|
+
export function registerSlackAgentEndpoint(
|
|
32
|
+
endpointId: string,
|
|
33
|
+
endpoint: SlackAgentEndpoint,
|
|
34
|
+
): () => void {
|
|
35
|
+
endpoints.set(endpointId, endpoint);
|
|
36
|
+
return () => {
|
|
37
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
38
|
+
endpoints.delete(endpointId);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
43
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
44
|
+
export function setSlackAgentDeps(deps: SlackAgentDeps | null): void {
|
|
45
|
+
override = deps;
|
|
17
46
|
}
|
|
18
47
|
|
|
19
48
|
export function getSlackAgentDeps(): SlackAgentDeps {
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
49
|
+
if (override) return override;
|
|
50
|
+
return {
|
|
51
|
+
getEndpoint(endpointId: string): SlackAgentEndpoint {
|
|
52
|
+
const registered = endpoints.get(endpointId);
|
|
53
|
+
if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
54
|
+
return registered;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
22
57
|
}
|