@meetopenbot/slack 0.0.5 → 0.0.8
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 +1 -1
- package/dist/agent.js +21 -0
- package/dist/config.js +42 -81
- package/dist/index.d.ts +4 -2
- package/dist/index.js +22 -439
- package/dist/invoke.js +246 -0
- package/dist/webhook.js +112 -0
- package/package.json +15 -12
- package/dist/cloud-mode.js +0 -10
- package/dist/credits-auth.js +0 -53
- package/dist/logger.js +0 -17
- package/dist/model-registry.js +0 -40
- package/dist/model.js +0 -24
- package/dist/slack-agent.js +0 -30
- package/dist/slack-mcp.js +0 -15
- package/dist/slack-threads.js +0 -20
- package/dist/slack-verify.d.ts +0 -1
- /package/dist/{slack-context.js → context.js} +0 -0
- /package/dist/{slack-post.js → post.js} +0 -0
- /package/dist/{slack-verify.js → verify.js} +0 -0
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ C01234567:engineering,C89ABCDEF:support,D0789ABC:slack-dms
|
|
|
43
43
|
- **Left side** — Slack channel id (`C…` for channels, `D…` for DMs). In Slack: right-click the channel → **View channel details** → copy the id at the bottom.
|
|
44
44
|
- **Right side** — OpenBot channel id to run in (must already exist).
|
|
45
45
|
|
|
46
|
-
Slack channels without a mapping route to **`
|
|
46
|
+
Slack channels without a mapping route to **`general`**.
|
|
47
47
|
|
|
48
48
|
This applies only to **webhook ingress delegation**, not Slack MCP tools.
|
|
49
49
|
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runMcpTurn } from "@meetopenbot/plugin-sdk";
|
|
2
|
+
const SYSTEM_PROMPT = `You are the OpenBot Slack specialist agent.
|
|
3
|
+
Use Slack MCP tools to post messages, list channels, read history, and perform other workspace actions when needed.
|
|
4
|
+
Summarize results clearly in plain text for the user. Be concise and friendly.`;
|
|
5
|
+
export async function runSlackAgent(args) {
|
|
6
|
+
return runMcpTurn({
|
|
7
|
+
mcp: {
|
|
8
|
+
command: "npx",
|
|
9
|
+
args: ["-y", "@modelcontextprotocol/server-slack"],
|
|
10
|
+
env: () => ({
|
|
11
|
+
SLACK_BOT_TOKEN: args.botToken,
|
|
12
|
+
SLACK_TEAM_ID: args.teamId,
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
apiKey: args.botToken,
|
|
16
|
+
model: args.model ?? "openai/gpt-4o-mini",
|
|
17
|
+
system: SYSTEM_PROMPT,
|
|
18
|
+
prompt: args.prompt,
|
|
19
|
+
maxSteps: 10,
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,125 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const
|
|
4
|
-
|
|
1
|
+
import { lookupSecret, modelConfigField, trimmedString, } from "@meetopenbot/plugin-sdk";
|
|
2
|
+
export const DEFAULT_OPENBOT_CHANNEL_ID = "general";
|
|
3
|
+
export const ROUTING_AGENT_ID = "system";
|
|
4
|
+
export const SLACK_USER_DISPLAY_NAME = "Slack user";
|
|
5
|
+
const modelField = await modelConfigField({
|
|
6
|
+
providers: ["openai"],
|
|
7
|
+
override: true,
|
|
8
|
+
defaultValue: "openai/gpt-4o-mini",
|
|
9
|
+
});
|
|
10
|
+
export const pluginConfigSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
model: {
|
|
14
|
+
...modelField,
|
|
15
|
+
description: "Outbound — OpenAI model for direct Slack agent invocations",
|
|
16
|
+
},
|
|
17
|
+
channelMappings: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Inbound — webhook routing. Comma-separated slackChannelId:openbotChannelId pairs (e.g. C01234567:engineering,C89ABCDEF:support). Unmapped Slack channels use general.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
5
23
|
export function buildSlackChannelMappingIndex(config) {
|
|
6
24
|
const index = new Map();
|
|
7
|
-
const raw =
|
|
8
|
-
? config.channelMappings.trim()
|
|
9
|
-
: "";
|
|
25
|
+
const raw = trimmedString(config.channelMappings) ?? "";
|
|
10
26
|
if (!raw)
|
|
11
27
|
return index;
|
|
12
28
|
for (const segment of raw.split(",")) {
|
|
13
29
|
const trimmed = segment.trim();
|
|
14
|
-
if (!trimmed)
|
|
15
|
-
continue;
|
|
16
30
|
const colon = trimmed.indexOf(":");
|
|
17
31
|
if (colon <= 0 || colon === trimmed.length - 1)
|
|
18
32
|
continue;
|
|
19
33
|
const slackChannelId = trimmed.slice(0, colon).trim();
|
|
20
34
|
const channelId = trimmed.slice(colon + 1).trim();
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
index.set(slackChannelId, channelId);
|
|
35
|
+
if (slackChannelId && channelId)
|
|
36
|
+
index.set(slackChannelId, channelId);
|
|
24
37
|
}
|
|
25
38
|
return index;
|
|
26
39
|
}
|
|
27
|
-
/** Map a Slack channel id from webhook ingress to an OpenBot channel id. */
|
|
28
40
|
export function resolveOpenBotChannelId(slackChannelId, config) {
|
|
29
41
|
const normalized = slackChannelId.trim();
|
|
30
42
|
if (!normalized)
|
|
31
43
|
return DEFAULT_OPENBOT_CHANNEL_ID;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
function variableValue(variables, key) {
|
|
36
|
-
const entry = variables[key];
|
|
37
|
-
if (typeof entry === "string")
|
|
38
|
-
return entry || undefined;
|
|
39
|
-
return entry?.value || undefined;
|
|
44
|
+
return (buildSlackChannelMappingIndex(config).get(normalized) ??
|
|
45
|
+
DEFAULT_OPENBOT_CHANNEL_ID);
|
|
40
46
|
}
|
|
41
47
|
export function readSlackConfig(config) {
|
|
42
48
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: undefined,
|
|
46
|
-
botToken: typeof config.botToken === "string" && config.botToken.trim()
|
|
47
|
-
? config.botToken.trim()
|
|
48
|
-
: undefined,
|
|
49
|
-
teamId: typeof config.teamId === "string" && config.teamId.trim()
|
|
50
|
-
? config.teamId.trim()
|
|
51
|
-
: undefined,
|
|
52
|
-
channelMappings: typeof config.channelMappings === "string" &&
|
|
53
|
-
config.channelMappings.trim()
|
|
54
|
-
? config.channelMappings.trim()
|
|
55
|
-
: undefined,
|
|
56
|
-
authMode: resolveAuthMode(config),
|
|
57
|
-
model: typeof config.model === "string" && config.model.trim()
|
|
58
|
-
? config.model.trim()
|
|
59
|
-
: undefined,
|
|
49
|
+
channelMappings: trimmedString(config.channelMappings),
|
|
50
|
+
model: trimmedString(config.model),
|
|
60
51
|
};
|
|
61
52
|
}
|
|
62
|
-
export function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
export function resolveSlackSigningSecret() {
|
|
54
|
+
return lookupSecret({ envKeys: ["SLACK_SIGNING_SECRET"] });
|
|
55
|
+
}
|
|
56
|
+
export function formatMissingCredentials(missing) {
|
|
57
|
+
const lines = ["Slack agent setup is incomplete. Set these workspace variables:"];
|
|
66
58
|
if (missing.includes("botToken")) {
|
|
67
|
-
lines.push("- `
|
|
59
|
+
lines.push("- `SLACK_BOT_TOKEN` — Slack bot OAuth token (`xoxb-…`)");
|
|
68
60
|
}
|
|
69
61
|
if (missing.includes("teamId")) {
|
|
70
|
-
lines.push("- `
|
|
71
|
-
}
|
|
72
|
-
if (missing.includes("openaiApiKey")) {
|
|
73
|
-
if (authMode === "credits") {
|
|
74
|
-
lines.push("- OpenAI API key is required in BYOK mode — add `OPENAI_API_KEY` under workspace settings, or switch `authMode` to `credits` on cloud");
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
lines.push("- `OPENAI_API_KEY` — OpenAI API key for the agent loop (BYOK mode), or switch `authMode` to `credits` on cloud");
|
|
78
|
-
}
|
|
62
|
+
lines.push("- `SLACK_TEAM_ID` — workspace team id (`T…`)");
|
|
79
63
|
}
|
|
80
64
|
return lines.join("\n");
|
|
81
65
|
}
|
|
82
|
-
export
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const resolve = (configKey, envKey) => {
|
|
88
|
-
const fromConfig = config[configKey];
|
|
89
|
-
if (typeof fromConfig === "string" && fromConfig.trim()) {
|
|
90
|
-
return fromConfig.trim();
|
|
91
|
-
}
|
|
92
|
-
if (process.env[envKey]?.trim())
|
|
93
|
-
return process.env[envKey].trim();
|
|
94
|
-
return variableValue(variables, envKey)?.trim();
|
|
95
|
-
};
|
|
96
|
-
const signingSecret = (typeof config.signingSecret === "string" && config.signingSecret.trim()) ||
|
|
97
|
-
process.env.SLACK_SIGNING_SECRET?.trim() ||
|
|
98
|
-
variableValue(variables, "SLACK_SIGNING_SECRET") ||
|
|
99
|
-
"";
|
|
100
|
-
const botToken = resolve("botToken", "SLACK_BOT_TOKEN");
|
|
101
|
-
const teamId = resolve("teamId", "SLACK_TEAM_ID");
|
|
102
|
-
const openaiApiKey = process.env.OPENAI_API_KEY?.trim() ||
|
|
103
|
-
variableValue(variables, "OPENAI_API_KEY");
|
|
104
|
-
const model = resolve("model", "OPENAI_MODEL") ?? "openai/gpt-4o-mini";
|
|
66
|
+
export function resolveSlackCredentials(config) {
|
|
67
|
+
const signingSecret = lookupSecret({ envKeys: ["SLACK_SIGNING_SECRET"] }) ?? "";
|
|
68
|
+
const botToken = lookupSecret({ envKeys: ["SLACK_BOT_TOKEN"] });
|
|
69
|
+
const teamId = lookupSecret({ envKeys: ["SLACK_TEAM_ID"] });
|
|
70
|
+
const model = trimmedString(config.model) ?? "openai/gpt-4o-mini";
|
|
105
71
|
const missing = [];
|
|
106
72
|
if (!botToken)
|
|
107
73
|
missing.push("botToken");
|
|
108
74
|
if (!teamId)
|
|
109
75
|
missing.push("teamId");
|
|
110
|
-
if (
|
|
111
|
-
missing.push("openaiApiKey");
|
|
112
|
-
if (missing.length > 0) {
|
|
76
|
+
if (missing.length > 0)
|
|
113
77
|
return { ok: false, missing };
|
|
114
|
-
}
|
|
115
78
|
return {
|
|
116
79
|
ok: true,
|
|
117
80
|
credentials: {
|
|
118
81
|
signingSecret,
|
|
119
82
|
botToken: botToken,
|
|
120
83
|
teamId: teamId,
|
|
121
|
-
authMode,
|
|
122
|
-
openaiApiKey: openaiApiKey || undefined,
|
|
123
84
|
model,
|
|
124
85
|
},
|
|
125
86
|
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,451 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { resolveModelConfigField } from "./model-registry.js";
|
|
7
|
-
import { buildPromptWithSlackContext } from "./slack-context.js";
|
|
8
|
-
import { postSlackMessage } from "./slack-post.js";
|
|
9
|
-
import { runSlackAgent } from "./slack-agent.js";
|
|
10
|
-
import { verifySlackSignature } from "./slack-verify.js";
|
|
11
|
-
const ROUTING_AGENT_ID = "system";
|
|
12
|
-
const SLACK_USER_DISPLAY_NAME = "Slack user";
|
|
13
|
-
function isSlackWebhookIngress(meta) {
|
|
14
|
-
return meta?.source === "slack";
|
|
15
|
-
}
|
|
16
|
-
const seenSlackEvents = new Map();
|
|
17
|
-
const SLACK_EVENT_DEDUP_MS = 5 * 60 * 1000;
|
|
18
|
-
function purgeStaleSlackEvents(now) {
|
|
19
|
-
for (const [id, at] of seenSlackEvents) {
|
|
20
|
-
if (now - at > SLACK_EVENT_DEDUP_MS)
|
|
21
|
-
seenSlackEvents.delete(id);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function markSlackEventSeen(dedupKey) {
|
|
25
|
-
const now = Date.now();
|
|
26
|
-
purgeStaleSlackEvents(now);
|
|
27
|
-
if (seenSlackEvents.has(dedupKey))
|
|
28
|
-
return true;
|
|
29
|
-
seenSlackEvents.set(dedupKey, now);
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
function slackEventDedupKey(payload, slackEvent) {
|
|
33
|
-
if (payload.event_id)
|
|
34
|
-
return payload.event_id;
|
|
35
|
-
return `${slackEvent.channel}:${slackEvent.ts}`;
|
|
36
|
-
}
|
|
37
|
-
/** MVP: channel mentions via app_mention; DMs via message.im only. */
|
|
38
|
-
function shouldProcessSlackEvent(slackEvent) {
|
|
39
|
-
if (slackEvent.bot_id)
|
|
40
|
-
return false;
|
|
41
|
-
if (slackEvent.subtype)
|
|
42
|
-
return false;
|
|
43
|
-
if (slackEvent.type === "app_mention")
|
|
44
|
-
return true;
|
|
45
|
-
if (slackEvent.type === "message" && slackEvent.channel_type === "im") {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
function normalizeSlackText(text) {
|
|
51
|
-
return text.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
52
|
-
}
|
|
53
|
-
function getSlackThreadMeta(meta) {
|
|
54
|
-
const slack = meta?.slack;
|
|
55
|
-
if (!slack || typeof slack !== "object")
|
|
56
|
-
return undefined;
|
|
57
|
-
return slack;
|
|
58
|
-
}
|
|
59
|
-
async function* bridgeSystemAgentRun(args) {
|
|
60
|
-
const runId = `slack_${randomUUID()}`;
|
|
61
|
-
const slackMeta = getSlackThreadMeta(args.meta);
|
|
62
|
-
const eventQueue = [];
|
|
63
|
-
let resolveNext = null;
|
|
64
|
-
let isFinished = false;
|
|
65
|
-
const runPromise = args.host
|
|
66
|
-
.runAgent({
|
|
67
|
-
runId,
|
|
68
|
-
agentId: ROUTING_AGENT_ID,
|
|
69
|
-
publicBaseUrl: args.publicBaseUrl,
|
|
70
|
-
// Runtime's webhook ingress handles persistence.
|
|
71
|
-
persistEvents: false,
|
|
72
|
-
event: {
|
|
73
|
-
type: "agent:invoke",
|
|
74
|
-
data: {
|
|
75
|
-
agentId: ROUTING_AGENT_ID,
|
|
76
|
-
role: "user",
|
|
77
|
-
content: args.userMessage,
|
|
78
|
-
},
|
|
79
|
-
meta: {
|
|
80
|
-
channelId: args.channelId,
|
|
81
|
-
threadId: args.threadId,
|
|
82
|
-
source: "slack",
|
|
83
|
-
slack: slackMeta,
|
|
84
|
-
userName: SLACK_USER_DISPLAY_NAME,
|
|
85
|
-
...(typeof args.meta?.userId === "string"
|
|
86
|
-
? { userId: args.meta.userId }
|
|
87
|
-
: {}),
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
onEvent: async (outEvent) => {
|
|
91
|
-
if (outEvent.type === "agent:output" &&
|
|
92
|
-
!outEvent.meta?.parentToolCallId) {
|
|
93
|
-
const output = outEvent;
|
|
94
|
-
eventQueue.push({
|
|
95
|
-
...output,
|
|
96
|
-
meta: {
|
|
97
|
-
...output.meta,
|
|
98
|
-
agentId: ROUTING_AGENT_ID,
|
|
99
|
-
threadId: args.threadId,
|
|
100
|
-
source: "slack",
|
|
101
|
-
slack: slackMeta,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
if (resolveNext) {
|
|
106
|
-
resolveNext();
|
|
107
|
-
resolveNext = null;
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
.catch((error) => {
|
|
112
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
-
eventQueue.push({
|
|
114
|
-
type: "agent:output",
|
|
115
|
-
data: { content: `Slack agent error: ${message}` },
|
|
116
|
-
meta: {
|
|
117
|
-
agentId: ROUTING_AGENT_ID,
|
|
118
|
-
threadId: args.threadId,
|
|
119
|
-
source: "slack",
|
|
120
|
-
slack: slackMeta,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
})
|
|
124
|
-
.finally(() => {
|
|
125
|
-
isFinished = true;
|
|
126
|
-
if (resolveNext) {
|
|
127
|
-
resolveNext();
|
|
128
|
-
resolveNext = null;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
while (!isFinished || eventQueue.length > 0) {
|
|
132
|
-
if (eventQueue.length === 0) {
|
|
133
|
-
await new Promise((resolve) => {
|
|
134
|
-
resolveNext = resolve;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
while (eventQueue.length > 0) {
|
|
138
|
-
yield eventQueue.shift();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
await runPromise;
|
|
142
|
-
}
|
|
143
|
-
const modelField = await resolveModelConfigField();
|
|
144
|
-
const slackPluginConfigSchema = {
|
|
145
|
-
type: "object",
|
|
146
|
-
properties: {
|
|
147
|
-
...(isCloudMode()
|
|
148
|
-
? {
|
|
149
|
-
authMode: {
|
|
150
|
-
type: "string",
|
|
151
|
-
description: "Outbound — Credits uses your workspace credit balance via OpenBot. BYOK uses `OPENAI_API_KEY` from workspace settings.",
|
|
152
|
-
enum: ["credits", "byok"],
|
|
153
|
-
default: "credits",
|
|
154
|
-
},
|
|
155
|
-
}
|
|
156
|
-
: {}),
|
|
157
|
-
model: {
|
|
158
|
-
...modelField,
|
|
159
|
-
description: "Outbound — OpenAI model for direct Slack agent invocations",
|
|
160
|
-
},
|
|
161
|
-
botToken: {
|
|
162
|
-
type: "string",
|
|
163
|
-
description: "Outbound — Slack bot token (OAuth & Permissions → Bot User OAuth Token)",
|
|
164
|
-
format: "password",
|
|
165
|
-
},
|
|
166
|
-
teamId: {
|
|
167
|
-
type: "string",
|
|
168
|
-
description: "Outbound — Slack workspace team id (starts with T)",
|
|
169
|
-
},
|
|
170
|
-
signingSecret: {
|
|
171
|
-
type: "string",
|
|
172
|
-
description: "Inbound — Slack app signing secret (Basic Information → App Credentials)",
|
|
173
|
-
format: "password",
|
|
174
|
-
},
|
|
175
|
-
channelMappings: {
|
|
176
|
-
type: "string",
|
|
177
|
-
description: "Inbound — webhook routing. Comma-separated slackChannelId:openbotChannelId pairs (e.g. C01234567:engineering,C89ABCDEF:support). Unmapped Slack channels use uncategorized.",
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
required: ["signingSecret"],
|
|
181
|
-
};
|
|
182
|
-
export default definePlugin({
|
|
1
|
+
import { definePlugin, } from "@meetopenbot/plugin-sdk";
|
|
2
|
+
import { pluginConfigSchema, readSlackConfig } from "./config.js";
|
|
3
|
+
import { handleSlackInvoke, handleSlackOutbound } from "./invoke.js";
|
|
4
|
+
import { handleSlackWebhook } from "./webhook.js";
|
|
5
|
+
export const plugin = definePlugin({
|
|
183
6
|
name: "Slack",
|
|
184
7
|
description: "Slack Events API ingress, local agent runtime, and system delegation for webhooks",
|
|
185
|
-
configSchema:
|
|
8
|
+
configSchema: pluginConfigSchema,
|
|
186
9
|
factory: (pluginContext) => (builder) => {
|
|
187
|
-
const { agentId, config,
|
|
10
|
+
const { agentId, config, host, publicBaseUrl } = pluginContext;
|
|
188
11
|
const slackConfig = readSlackConfig(config);
|
|
189
|
-
const host = pluginContext.host;
|
|
190
|
-
const publicBaseUrl = pluginContext.publicBaseUrl ?? "";
|
|
191
12
|
builder.on("action:webhook", async function* (event, ctx) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
? auth.credentials.signingSecret
|
|
198
|
-
: String(config.signingSecret ?? "").trim();
|
|
199
|
-
const rawBody = decodeWebhookRawBody(webhook);
|
|
200
|
-
let payload;
|
|
201
|
-
try {
|
|
202
|
-
payload = JSON.parse(rawBody.toString("utf8"));
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
yield webhookHttpResponse({
|
|
206
|
-
status: 400,
|
|
207
|
-
body: { error: "invalid json" },
|
|
208
|
-
});
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (payload.type === "url_verification") {
|
|
212
|
-
yield webhookHttpResponse({
|
|
213
|
-
status: 200,
|
|
214
|
-
body: { challenge: payload.challenge },
|
|
215
|
-
});
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const timestamp = getWebhookHeader(webhook, "x-slack-request-timestamp");
|
|
219
|
-
const signature = getWebhookHeader(webhook, "x-slack-signature");
|
|
220
|
-
if (!verifySlackSignature(signingSecret, rawBody, timestamp, signature)) {
|
|
221
|
-
yield webhookHttpResponse({
|
|
222
|
-
status: 401,
|
|
223
|
-
body: { error: "invalid signature" },
|
|
224
|
-
});
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const slackEvent = payload.type === "event_callback" ? payload.event : undefined;
|
|
228
|
-
if (!slackEvent) {
|
|
229
|
-
yield webhookHttpResponse({ status: 200, body: {} });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (!shouldProcessSlackEvent(slackEvent)) {
|
|
233
|
-
yield webhookHttpResponse({ status: 200, body: {} });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const dedupKey = slackEventDedupKey(payload, slackEvent);
|
|
237
|
-
if (markSlackEventSeen(dedupKey)) {
|
|
238
|
-
yield webhookHttpResponse({ status: 200, body: {} });
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const text = normalizeSlackText(slackEvent.text ?? "");
|
|
242
|
-
if (!text) {
|
|
243
|
-
yield webhookHttpResponse({ status: 200, body: {} });
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const threadId = slackEvent.thread_ts ?? slackEvent.ts;
|
|
247
|
-
if (!threadId) {
|
|
248
|
-
yield webhookHttpResponse({ status: 200, body: {} });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
ctx.state.threadId = threadId;
|
|
252
|
-
const slackMeta = {
|
|
253
|
-
teamId: payload.team_id,
|
|
254
|
-
channel: slackEvent.channel,
|
|
255
|
-
user: slackEvent.user,
|
|
256
|
-
ts: slackEvent.ts,
|
|
257
|
-
threadTs: slackEvent.thread_ts,
|
|
258
|
-
};
|
|
259
|
-
const openBotChannelId = slackEvent.channel
|
|
260
|
-
? resolveOpenBotChannelId(slackEvent.channel, slackConfig)
|
|
261
|
-
: DEFAULT_OPENBOT_CHANNEL_ID;
|
|
262
|
-
ctx.state.channelId = openBotChannelId;
|
|
263
|
-
yield webhookHttpResponse({ status: 200, body: {} });
|
|
264
|
-
yield {
|
|
265
|
-
type: "agent:invoke",
|
|
266
|
-
data: {
|
|
267
|
-
agentId: ROUTING_AGENT_ID,
|
|
268
|
-
role: "user",
|
|
269
|
-
content: text,
|
|
270
|
-
},
|
|
271
|
-
meta: {
|
|
272
|
-
channelId: openBotChannelId,
|
|
273
|
-
threadId,
|
|
274
|
-
source: "slack",
|
|
275
|
-
slack: slackMeta,
|
|
276
|
-
userName: SLACK_USER_DISPLAY_NAME,
|
|
277
|
-
...(slackEvent.user ? { userId: slackEvent.user } : {}),
|
|
278
|
-
},
|
|
279
|
-
};
|
|
13
|
+
yield* handleSlackWebhook({
|
|
14
|
+
event: event,
|
|
15
|
+
ctx,
|
|
16
|
+
config: slackConfig,
|
|
17
|
+
});
|
|
280
18
|
});
|
|
281
19
|
builder.on("agent:invoke", async function* (event, ctx) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
if (event.meta?.threadId) {
|
|
292
|
-
ctx.state.threadId = event.meta.threadId;
|
|
293
|
-
}
|
|
294
|
-
const threadId = event.meta?.threadId ?? ctx.state.threadId;
|
|
295
|
-
const userMessage = (event.data.content ?? "").trim();
|
|
296
|
-
if (!userMessage || !threadId)
|
|
297
|
-
return;
|
|
298
|
-
if (fromWebhook) {
|
|
299
|
-
const auth = await resolveSlackCredentials(slackConfig, storage, {
|
|
300
|
-
requireOpenAi: false,
|
|
301
|
-
});
|
|
302
|
-
if (!auth.ok) {
|
|
303
|
-
yield {
|
|
304
|
-
type: "agent:output",
|
|
305
|
-
data: {
|
|
306
|
-
content: formatMissingCredentials(auth.missing, slackConfig.authMode),
|
|
307
|
-
},
|
|
308
|
-
meta: {
|
|
309
|
-
agentId: ROUTING_AGENT_ID,
|
|
310
|
-
threadId,
|
|
311
|
-
source: "slack",
|
|
312
|
-
slack: getSlackThreadMeta(event.meta),
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (!host?.runAgent) {
|
|
318
|
-
yield {
|
|
319
|
-
type: "agent:output",
|
|
320
|
-
data: {
|
|
321
|
-
content: "Slack plugin requires host.runAgent to delegate to the system agent.",
|
|
322
|
-
},
|
|
323
|
-
meta: {
|
|
324
|
-
agentId: ROUTING_AGENT_ID,
|
|
325
|
-
threadId,
|
|
326
|
-
source: "slack",
|
|
327
|
-
slack: getSlackThreadMeta(event.meta),
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
const slackMeta = getSlackThreadMeta(event.meta);
|
|
333
|
-
const channelId = slackMeta?.channel
|
|
334
|
-
? resolveOpenBotChannelId(slackMeta.channel, slackConfig)
|
|
335
|
-
: ctx.state.channelId;
|
|
336
|
-
if (!channelId)
|
|
337
|
-
return;
|
|
338
|
-
let prompt = userMessage;
|
|
339
|
-
if (slackMeta?.channel && slackMeta.ts) {
|
|
340
|
-
try {
|
|
341
|
-
prompt = await buildPromptWithSlackContext({
|
|
342
|
-
token: auth.credentials.botToken,
|
|
343
|
-
channel: slackMeta.channel,
|
|
344
|
-
messageTs: slackMeta.ts,
|
|
345
|
-
threadTs: slackMeta.threadTs,
|
|
346
|
-
userMessage,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
// Fall back to the raw mention if Slack history is unavailable.
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
try {
|
|
354
|
-
yield* bridgeSystemAgentRun({
|
|
355
|
-
host,
|
|
356
|
-
channelId,
|
|
357
|
-
threadId,
|
|
358
|
-
userMessage: prompt,
|
|
359
|
-
meta: event.meta,
|
|
360
|
-
publicBaseUrl,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
-
yield {
|
|
366
|
-
type: "agent:output",
|
|
367
|
-
data: { content: `Slack agent error: ${message}` },
|
|
368
|
-
meta: {
|
|
369
|
-
agentId: ROUTING_AGENT_ID,
|
|
370
|
-
threadId,
|
|
371
|
-
source: "slack",
|
|
372
|
-
slack: getSlackThreadMeta(event.meta),
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
const auth = await resolveSlackCredentials(slackConfig, storage);
|
|
379
|
-
if (!auth.ok) {
|
|
380
|
-
yield agentOutput({
|
|
381
|
-
agentId,
|
|
382
|
-
content: formatMissingCredentials(auth.missing, slackConfig.authMode),
|
|
383
|
-
threadId,
|
|
384
|
-
meta: event.meta,
|
|
385
|
-
});
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (auth.credentials.authMode === "credits" &&
|
|
389
|
-
!resolveCreditsAuthConfig()) {
|
|
390
|
-
yield agentOutput({
|
|
391
|
-
agentId,
|
|
392
|
-
content: CREDITS_NOT_CONFIGURED_MESSAGE,
|
|
393
|
-
threadId,
|
|
394
|
-
meta: event.meta,
|
|
395
|
-
});
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
const reply = await runSlackAgent({
|
|
400
|
-
prompt: userMessage,
|
|
401
|
-
authMode: auth.credentials.authMode,
|
|
402
|
-
openaiApiKey: auth.credentials.openaiApiKey,
|
|
403
|
-
botToken: auth.credentials.botToken,
|
|
404
|
-
teamId: auth.credentials.teamId,
|
|
405
|
-
model: auth.credentials.model,
|
|
406
|
-
});
|
|
407
|
-
yield agentOutput({
|
|
408
|
-
agentId,
|
|
409
|
-
content: reply,
|
|
410
|
-
threadId,
|
|
411
|
-
meta: event.meta,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
416
|
-
const creditsMessage = auth.credentials.authMode === "credits"
|
|
417
|
-
? creditsErrorMessage(message)
|
|
418
|
-
: undefined;
|
|
419
|
-
yield agentOutput({
|
|
420
|
-
agentId,
|
|
421
|
-
content: creditsMessage ?? `Slack agent error: ${message}`,
|
|
422
|
-
threadId,
|
|
423
|
-
meta: event.meta,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
20
|
+
yield* handleSlackInvoke({
|
|
21
|
+
event,
|
|
22
|
+
ctx,
|
|
23
|
+
agentId,
|
|
24
|
+
config: slackConfig,
|
|
25
|
+
host,
|
|
26
|
+
publicBaseUrl: publicBaseUrl ?? "",
|
|
27
|
+
});
|
|
426
28
|
});
|
|
427
29
|
builder.on("agent:output", async function* (event) {
|
|
428
|
-
|
|
429
|
-
return;
|
|
430
|
-
if (event.meta?.agentId !== ROUTING_AGENT_ID)
|
|
431
|
-
return;
|
|
432
|
-
const slack = getSlackThreadMeta(event.meta);
|
|
433
|
-
if (!slack?.channel)
|
|
434
|
-
return;
|
|
435
|
-
const auth = await resolveSlackCredentials(slackConfig, storage);
|
|
436
|
-
if (!auth.ok)
|
|
437
|
-
return;
|
|
438
|
-
try {
|
|
439
|
-
await postSlackMessage({
|
|
440
|
-
token: auth.credentials.botToken,
|
|
441
|
-
channel: slack.channel,
|
|
442
|
-
text: event.data.content,
|
|
443
|
-
threadTs: slack.threadTs ?? slack.ts,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
// Avoid failing the bus run if Slack delivery fails.
|
|
448
|
-
}
|
|
30
|
+
yield* handleSlackOutbound({ event, config: slackConfig });
|
|
449
31
|
});
|
|
450
32
|
},
|
|
451
33
|
});
|
|
34
|
+
export default plugin;
|
package/dist/invoke.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { agentOutput, creditsErrorMessage, hasLlmAuth, llmAuthNotConfiguredMessage, shouldHandleInvoke, } from "@meetopenbot/plugin-sdk";
|
|
3
|
+
import { runSlackAgent } from "./agent.js";
|
|
4
|
+
import { formatMissingCredentials, resolveOpenBotChannelId, resolveSlackCredentials, ROUTING_AGENT_ID, SLACK_USER_DISPLAY_NAME, } from "./config.js";
|
|
5
|
+
import { buildPromptWithSlackContext } from "./context.js";
|
|
6
|
+
import { postSlackMessage } from "./post.js";
|
|
7
|
+
import { getSlackThreadMeta, isSlackWebhookIngress } from "./webhook.js";
|
|
8
|
+
async function* bridgeSystemAgentRun(args) {
|
|
9
|
+
const runId = `slack_${randomUUID()}`;
|
|
10
|
+
const slackMeta = getSlackThreadMeta(args.meta);
|
|
11
|
+
const eventQueue = [];
|
|
12
|
+
let resolveNext = null;
|
|
13
|
+
let isFinished = false;
|
|
14
|
+
const runPromise = args.host
|
|
15
|
+
.runAgent({
|
|
16
|
+
runId,
|
|
17
|
+
agentId: ROUTING_AGENT_ID,
|
|
18
|
+
publicBaseUrl: args.publicBaseUrl,
|
|
19
|
+
persistEvents: false,
|
|
20
|
+
event: {
|
|
21
|
+
type: "agent:invoke",
|
|
22
|
+
data: {
|
|
23
|
+
agentId: ROUTING_AGENT_ID,
|
|
24
|
+
role: "user",
|
|
25
|
+
content: args.userMessage,
|
|
26
|
+
},
|
|
27
|
+
meta: {
|
|
28
|
+
channelId: args.channelId,
|
|
29
|
+
threadId: args.threadId,
|
|
30
|
+
source: "slack",
|
|
31
|
+
slack: slackMeta,
|
|
32
|
+
userName: SLACK_USER_DISPLAY_NAME,
|
|
33
|
+
...(typeof args.meta?.userId === "string" ? { userId: args.meta.userId } : {}),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
onEvent: async (outEvent) => {
|
|
37
|
+
if (outEvent.type === "agent:output" && !outEvent.meta?.parentToolCallId) {
|
|
38
|
+
const output = outEvent;
|
|
39
|
+
eventQueue.push({
|
|
40
|
+
...output,
|
|
41
|
+
meta: {
|
|
42
|
+
...output.meta,
|
|
43
|
+
agentId: ROUTING_AGENT_ID,
|
|
44
|
+
threadId: args.threadId,
|
|
45
|
+
source: "slack",
|
|
46
|
+
slack: slackMeta,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (resolveNext) {
|
|
51
|
+
resolveNext();
|
|
52
|
+
resolveNext = null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
.catch((error) => {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
eventQueue.push({
|
|
59
|
+
type: "agent:output",
|
|
60
|
+
data: { content: `Slack agent error: ${message}` },
|
|
61
|
+
meta: {
|
|
62
|
+
agentId: ROUTING_AGENT_ID,
|
|
63
|
+
threadId: args.threadId,
|
|
64
|
+
source: "slack",
|
|
65
|
+
slack: slackMeta,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
isFinished = true;
|
|
71
|
+
if (resolveNext) {
|
|
72
|
+
resolveNext();
|
|
73
|
+
resolveNext = null;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
while (!isFinished || eventQueue.length > 0) {
|
|
77
|
+
if (eventQueue.length === 0) {
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
resolveNext = resolve;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
while (eventQueue.length > 0) {
|
|
83
|
+
yield eventQueue.shift();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await runPromise;
|
|
87
|
+
}
|
|
88
|
+
export async function* handleSlackInvoke(args) {
|
|
89
|
+
const fromWebhook = isSlackWebhookIngress(args.event.meta);
|
|
90
|
+
if (fromWebhook) {
|
|
91
|
+
if (!shouldHandleInvoke(args.event, ROUTING_AGENT_ID))
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
else if (!shouldHandleInvoke(args.event, args.agentId)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (args.event.meta?.threadId) {
|
|
98
|
+
args.ctx.state.threadId = args.event.meta.threadId;
|
|
99
|
+
}
|
|
100
|
+
const threadId = args.event.meta?.threadId ?? args.ctx.state.threadId;
|
|
101
|
+
const userMessage = (args.event.data.content ?? "").trim();
|
|
102
|
+
if (!userMessage || !threadId)
|
|
103
|
+
return;
|
|
104
|
+
if (fromWebhook) {
|
|
105
|
+
const auth = resolveSlackCredentials(args.config);
|
|
106
|
+
if (!auth.ok) {
|
|
107
|
+
yield {
|
|
108
|
+
type: "agent:output",
|
|
109
|
+
data: { content: formatMissingCredentials(auth.missing) },
|
|
110
|
+
meta: {
|
|
111
|
+
agentId: ROUTING_AGENT_ID,
|
|
112
|
+
threadId,
|
|
113
|
+
source: "slack",
|
|
114
|
+
slack: getSlackThreadMeta(args.event.meta),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!args.host?.runAgent) {
|
|
120
|
+
yield {
|
|
121
|
+
type: "agent:output",
|
|
122
|
+
data: {
|
|
123
|
+
content: "Slack plugin requires host.runAgent to delegate to the system agent.",
|
|
124
|
+
},
|
|
125
|
+
meta: {
|
|
126
|
+
agentId: ROUTING_AGENT_ID,
|
|
127
|
+
threadId,
|
|
128
|
+
source: "slack",
|
|
129
|
+
slack: getSlackThreadMeta(args.event.meta),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const slackMeta = getSlackThreadMeta(args.event.meta);
|
|
135
|
+
const channelId = slackMeta?.channel
|
|
136
|
+
? resolveOpenBotChannelId(slackMeta.channel, args.config)
|
|
137
|
+
: args.ctx.state.channelId;
|
|
138
|
+
if (!channelId)
|
|
139
|
+
return;
|
|
140
|
+
let prompt = userMessage;
|
|
141
|
+
if (slackMeta?.channel && slackMeta.ts) {
|
|
142
|
+
try {
|
|
143
|
+
prompt = await buildPromptWithSlackContext({
|
|
144
|
+
token: auth.credentials.botToken,
|
|
145
|
+
channel: slackMeta.channel,
|
|
146
|
+
messageTs: slackMeta.ts,
|
|
147
|
+
threadTs: slackMeta.threadTs,
|
|
148
|
+
userMessage,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Fall back to the raw mention if Slack history is unavailable.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
yield* bridgeSystemAgentRun({
|
|
157
|
+
host: args.host,
|
|
158
|
+
channelId,
|
|
159
|
+
threadId,
|
|
160
|
+
userMessage: prompt,
|
|
161
|
+
meta: args.event.meta,
|
|
162
|
+
publicBaseUrl: args.publicBaseUrl,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
yield {
|
|
168
|
+
type: "agent:output",
|
|
169
|
+
data: { content: `Slack agent error: ${message}` },
|
|
170
|
+
meta: {
|
|
171
|
+
agentId: ROUTING_AGENT_ID,
|
|
172
|
+
threadId,
|
|
173
|
+
source: "slack",
|
|
174
|
+
slack: getSlackThreadMeta(args.event.meta),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const auth = resolveSlackCredentials(args.config);
|
|
181
|
+
if (!auth.ok) {
|
|
182
|
+
yield agentOutput({
|
|
183
|
+
agentId: args.agentId,
|
|
184
|
+
content: formatMissingCredentials(auth.missing),
|
|
185
|
+
threadId,
|
|
186
|
+
meta: args.event.meta,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!hasLlmAuth("openai")) {
|
|
191
|
+
yield agentOutput({
|
|
192
|
+
agentId: args.agentId,
|
|
193
|
+
content: llmAuthNotConfiguredMessage(),
|
|
194
|
+
threadId,
|
|
195
|
+
meta: args.event.meta,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const reply = await runSlackAgent({
|
|
201
|
+
prompt: userMessage,
|
|
202
|
+
botToken: auth.credentials.botToken,
|
|
203
|
+
teamId: auth.credentials.teamId,
|
|
204
|
+
model: auth.credentials.model,
|
|
205
|
+
});
|
|
206
|
+
yield agentOutput({
|
|
207
|
+
agentId: args.agentId,
|
|
208
|
+
content: reply,
|
|
209
|
+
threadId,
|
|
210
|
+
meta: args.event.meta,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
yield agentOutput({
|
|
216
|
+
agentId: args.agentId,
|
|
217
|
+
content: creditsErrorMessage(message, { agentName: "Slack" }) ??
|
|
218
|
+
`Slack agent error: ${message}`,
|
|
219
|
+
threadId,
|
|
220
|
+
meta: args.event.meta,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export async function* handleSlackOutbound(args) {
|
|
225
|
+
if (args.event.meta?.source !== "slack")
|
|
226
|
+
return;
|
|
227
|
+
if (args.event.meta?.agentId !== ROUTING_AGENT_ID)
|
|
228
|
+
return;
|
|
229
|
+
const slack = getSlackThreadMeta(args.event.meta);
|
|
230
|
+
if (!slack?.channel)
|
|
231
|
+
return;
|
|
232
|
+
const auth = resolveSlackCredentials(args.config);
|
|
233
|
+
if (!auth.ok)
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
await postSlackMessage({
|
|
237
|
+
token: auth.credentials.botToken,
|
|
238
|
+
channel: slack.channel,
|
|
239
|
+
text: args.event.data.content,
|
|
240
|
+
threadTs: slack.threadTs ?? slack.ts,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Avoid failing the bus run if Slack delivery fails.
|
|
245
|
+
}
|
|
246
|
+
}
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { decodeWebhookRawBody, getWebhookHeader, webhookHttpResponse, } from "@meetopenbot/plugin-sdk";
|
|
2
|
+
import { DEFAULT_OPENBOT_CHANNEL_ID, ROUTING_AGENT_ID, SLACK_USER_DISPLAY_NAME, resolveOpenBotChannelId, resolveSlackSigningSecret, } from "./config.js";
|
|
3
|
+
import { verifySlackSignature } from "./verify.js";
|
|
4
|
+
const seenSlackEvents = new Map();
|
|
5
|
+
const SLACK_EVENT_DEDUP_MS = 5 * 60 * 1000;
|
|
6
|
+
function purgeStaleSlackEvents(now) {
|
|
7
|
+
for (const [id, at] of seenSlackEvents) {
|
|
8
|
+
if (now - at > SLACK_EVENT_DEDUP_MS)
|
|
9
|
+
seenSlackEvents.delete(id);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function markSlackEventSeen(dedupKey) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
purgeStaleSlackEvents(now);
|
|
15
|
+
if (seenSlackEvents.has(dedupKey))
|
|
16
|
+
return true;
|
|
17
|
+
seenSlackEvents.set(dedupKey, now);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function slackEventDedupKey(payload, slackEvent) {
|
|
21
|
+
if (payload.event_id)
|
|
22
|
+
return payload.event_id;
|
|
23
|
+
return `${slackEvent.channel}:${slackEvent.ts}`;
|
|
24
|
+
}
|
|
25
|
+
function shouldProcessSlackEvent(slackEvent) {
|
|
26
|
+
if (slackEvent.bot_id || slackEvent.subtype)
|
|
27
|
+
return false;
|
|
28
|
+
if (slackEvent.type === "app_mention")
|
|
29
|
+
return true;
|
|
30
|
+
return slackEvent.type === "message" && slackEvent.channel_type === "im";
|
|
31
|
+
}
|
|
32
|
+
function normalizeSlackText(text) {
|
|
33
|
+
return text.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
34
|
+
}
|
|
35
|
+
export function isSlackWebhookIngress(meta) {
|
|
36
|
+
return meta?.source === "slack";
|
|
37
|
+
}
|
|
38
|
+
export function getSlackThreadMeta(meta) {
|
|
39
|
+
const slack = meta?.slack;
|
|
40
|
+
if (!slack || typeof slack !== "object")
|
|
41
|
+
return undefined;
|
|
42
|
+
return slack;
|
|
43
|
+
}
|
|
44
|
+
export async function* handleSlackWebhook(args) {
|
|
45
|
+
if (args.event.data.provider !== "slack")
|
|
46
|
+
return;
|
|
47
|
+
const signingSecret = resolveSlackSigningSecret() ?? "";
|
|
48
|
+
const rawBody = decodeWebhookRawBody(args.event);
|
|
49
|
+
let payload;
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(rawBody.toString("utf8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
yield webhookHttpResponse({ status: 400, body: { error: "invalid json" } });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (payload.type === "url_verification") {
|
|
58
|
+
yield webhookHttpResponse({ status: 200, body: { challenge: payload.challenge } });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const timestamp = getWebhookHeader(args.event, "x-slack-request-timestamp");
|
|
62
|
+
const signature = getWebhookHeader(args.event, "x-slack-signature");
|
|
63
|
+
if (!verifySlackSignature(signingSecret, rawBody, timestamp, signature)) {
|
|
64
|
+
yield webhookHttpResponse({ status: 401, body: { error: "invalid signature" } });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const slackEvent = payload.type === "event_callback" ? payload.event : undefined;
|
|
68
|
+
if (!slackEvent || !shouldProcessSlackEvent(slackEvent)) {
|
|
69
|
+
yield webhookHttpResponse({ status: 200, body: {} });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const dedupKey = slackEventDedupKey(payload, slackEvent);
|
|
73
|
+
if (markSlackEventSeen(dedupKey)) {
|
|
74
|
+
yield webhookHttpResponse({ status: 200, body: {} });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const text = normalizeSlackText(slackEvent.text ?? "");
|
|
78
|
+
const threadId = slackEvent.thread_ts ?? slackEvent.ts;
|
|
79
|
+
if (!text || !threadId) {
|
|
80
|
+
yield webhookHttpResponse({ status: 200, body: {} });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
args.ctx.state.threadId = threadId;
|
|
84
|
+
const slackMeta = {
|
|
85
|
+
teamId: payload.team_id,
|
|
86
|
+
channel: slackEvent.channel,
|
|
87
|
+
user: slackEvent.user,
|
|
88
|
+
ts: slackEvent.ts,
|
|
89
|
+
threadTs: slackEvent.thread_ts,
|
|
90
|
+
};
|
|
91
|
+
const openBotChannelId = slackEvent.channel
|
|
92
|
+
? resolveOpenBotChannelId(slackEvent.channel, args.config)
|
|
93
|
+
: DEFAULT_OPENBOT_CHANNEL_ID;
|
|
94
|
+
args.ctx.state.channelId = openBotChannelId;
|
|
95
|
+
yield webhookHttpResponse({ status: 200, body: {} });
|
|
96
|
+
yield {
|
|
97
|
+
type: "agent:invoke",
|
|
98
|
+
data: {
|
|
99
|
+
agentId: ROUTING_AGENT_ID,
|
|
100
|
+
role: "user",
|
|
101
|
+
content: text,
|
|
102
|
+
},
|
|
103
|
+
meta: {
|
|
104
|
+
channelId: openBotChannelId,
|
|
105
|
+
threadId,
|
|
106
|
+
source: "slack",
|
|
107
|
+
slack: slackMeta,
|
|
108
|
+
userName: SLACK_USER_DISPLAY_NAME,
|
|
109
|
+
...(slackEvent.user ? { userId: slackEvent.user } : {}),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/slack",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Slack Events API ingress, simple agent replies, and thread delivery for OpenBot",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
10
14
|
},
|
|
11
15
|
"publishConfig": {
|
|
12
16
|
"access": "public"
|
|
@@ -14,17 +18,16 @@
|
|
|
14
18
|
"files": [
|
|
15
19
|
"dist"
|
|
16
20
|
],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsc"
|
|
19
|
-
},
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"@
|
|
22
|
-
"@ai-sdk/openai": "^4.0.15",
|
|
23
|
-
"@meetopenbot/plugin-sdk": "^0.1.8",
|
|
24
|
-
"ai": "^7.0.29"
|
|
22
|
+
"@meetopenbot/plugin-sdk": "^0.3.0"
|
|
25
23
|
},
|
|
26
24
|
"devDependencies": {
|
|
27
25
|
"@types/node": "^20.10.1",
|
|
28
26
|
"typescript": "^5.9.3"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc && node ../../scripts/write-plugin-declaration.mjs",
|
|
30
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
29
32
|
}
|
|
30
|
-
}
|
|
33
|
+
}
|
package/dist/cloud-mode.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** True when this runtime is a platform-managed cloud deployment. */
|
|
2
|
-
export const isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === "1";
|
|
3
|
-
/** Default auth mode: Credits on cloud, BYOK locally. */
|
|
4
|
-
export const defaultAuthMode = () => isCloudMode() ? "credits" : "byok";
|
|
5
|
-
export function resolveAuthMode(config) {
|
|
6
|
-
if (config.authMode === "byok" || config.authMode === "credits") {
|
|
7
|
-
return config.authMode;
|
|
8
|
-
}
|
|
9
|
-
return defaultAuthMode();
|
|
10
|
-
}
|
package/dist/credits-auth.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { isCloudMode } from "./cloud-mode.js";
|
|
2
|
-
export const INTEGRATIONS_TOKEN_HEADER = "x-openbot-integrations-token";
|
|
3
|
-
export const CREDITS_API_KEY_PLACEHOLDER = "openbot-credits";
|
|
4
|
-
/** Cloud host injects these when routing LLM calls through OpenBot Credits. */
|
|
5
|
-
export function resolveCreditsAuthConfig() {
|
|
6
|
-
const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
|
|
7
|
-
const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
|
|
8
|
-
if (!baseUrl || !token)
|
|
9
|
-
return undefined;
|
|
10
|
-
return { baseUrl: baseUrl.replace(/\/$/, ""), token };
|
|
11
|
-
}
|
|
12
|
-
export function creditsProviderBaseUrl(config) {
|
|
13
|
-
return `${config.baseUrl}/openai/v1`;
|
|
14
|
-
}
|
|
15
|
-
export function shouldUseCreditsAuth(options) {
|
|
16
|
-
if (options?.authMode === "byok")
|
|
17
|
-
return false;
|
|
18
|
-
if (options?.authMode === "credits")
|
|
19
|
-
return true;
|
|
20
|
-
return isCloudMode() && resolveCreditsAuthConfig() !== undefined;
|
|
21
|
-
}
|
|
22
|
-
export function isCreditsErrorMessage(message) {
|
|
23
|
-
const lower = message.toLowerCase();
|
|
24
|
-
return (lower.includes("insufficient_credits") ||
|
|
25
|
-
lower.includes("insufficient credits") ||
|
|
26
|
-
lower.includes("402"));
|
|
27
|
-
}
|
|
28
|
-
export function isAuthErrorMessage(message) {
|
|
29
|
-
const lower = message.toLowerCase();
|
|
30
|
-
return (lower.includes("api key") ||
|
|
31
|
-
lower.includes("401") ||
|
|
32
|
-
lower.includes("unauthorized") ||
|
|
33
|
-
lower.includes("authentication"));
|
|
34
|
-
}
|
|
35
|
-
export function isIntegrationsProviderError(message) {
|
|
36
|
-
const lower = message.toLowerCase();
|
|
37
|
-
return (lower.includes("provider api key not configured") ||
|
|
38
|
-
(lower.includes("503") && lower.includes("provider")));
|
|
39
|
-
}
|
|
40
|
-
export const CREDITS_NOT_CONFIGURED_MESSAGE = "OpenBot Credits is not configured on this runtime. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN (try redeploying the workspace).";
|
|
41
|
-
export const CREDITS_PROVIDER_UNAVAILABLE_MESSAGE = "OpenBot Credits could not reach OpenAI — the platform provider API key is not configured yet. Try again later or switch this agent to BYOK mode.";
|
|
42
|
-
export const CREDITS_AUTH_FAILED_MESSAGE = "Slack could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.";
|
|
43
|
-
export function creditsErrorMessage(message) {
|
|
44
|
-
if (isIntegrationsProviderError(message)) {
|
|
45
|
-
return CREDITS_PROVIDER_UNAVAILABLE_MESSAGE;
|
|
46
|
-
}
|
|
47
|
-
if (isCreditsErrorMessage(message)) {
|
|
48
|
-
return "Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.";
|
|
49
|
-
}
|
|
50
|
-
if (isAuthErrorMessage(message))
|
|
51
|
-
return CREDITS_AUTH_FAILED_MESSAGE;
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
package/dist/logger.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const PREFIX = "[slack]";
|
|
2
|
-
function formatMessage(message, details) {
|
|
3
|
-
if (!details || Object.keys(details).length === 0)
|
|
4
|
-
return message;
|
|
5
|
-
return `${message} ${JSON.stringify(details)}`;
|
|
6
|
-
}
|
|
7
|
-
export const log = {
|
|
8
|
-
info(message, details) {
|
|
9
|
-
console.log(`${PREFIX} ${formatMessage(message, details)}`);
|
|
10
|
-
},
|
|
11
|
-
warn(message, details) {
|
|
12
|
-
console.warn(`${PREFIX} ${formatMessage(message, details)}`);
|
|
13
|
-
},
|
|
14
|
-
debug(message, details) {
|
|
15
|
-
console.debug(`${PREFIX} ${formatMessage(message, details)}`);
|
|
16
|
-
},
|
|
17
|
-
};
|
package/dist/model-registry.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
const REGISTRY_URL = "https://raw.githubusercontent.com/meetopenbot/openbot-registry/main/registry.json";
|
|
2
|
-
const OPENAI_PROVIDER = "openai";
|
|
3
|
-
const freeInputModelField = () => ({
|
|
4
|
-
type: "string",
|
|
5
|
-
override: true,
|
|
6
|
-
description: "OpenAI model in provider/model-id format (e.g. openai/gpt-4o-mini).",
|
|
7
|
-
default: "openai/gpt-4o-mini",
|
|
8
|
-
});
|
|
9
|
-
/** Registry-backed model field for plugin configSchema (`enum` + labeled `options`). */
|
|
10
|
-
export async function resolveModelConfigField() {
|
|
11
|
-
try {
|
|
12
|
-
const res = await fetch(REGISTRY_URL, {
|
|
13
|
-
headers: { Accept: "application/json" },
|
|
14
|
-
signal: AbortSignal.timeout(15_000),
|
|
15
|
-
});
|
|
16
|
-
if (!res.ok)
|
|
17
|
-
return freeInputModelField();
|
|
18
|
-
const registry = (await res.json());
|
|
19
|
-
const provider = registry.providers?.[OPENAI_PROVIDER];
|
|
20
|
-
const models = provider?.models ?? [];
|
|
21
|
-
if (models.length === 0)
|
|
22
|
-
return freeInputModelField();
|
|
23
|
-
const defaultModel = models.find((model) => model.id === "gpt-4o-mini")?.id ?? models[0].id;
|
|
24
|
-
return {
|
|
25
|
-
type: "string",
|
|
26
|
-
override: true,
|
|
27
|
-
description: "OpenAI model from the OpenBot registry.",
|
|
28
|
-
default: `${OPENAI_PROVIDER}/${defaultModel}`,
|
|
29
|
-
enum: models.map((model) => `${OPENAI_PROVIDER}/${model.id}`),
|
|
30
|
-
options: models.map((model) => ({
|
|
31
|
-
label: `${provider?.label ?? "OpenAI"} — ${model.label}`,
|
|
32
|
-
value: `${OPENAI_PROVIDER}/${model.id}`,
|
|
33
|
-
description: model.description,
|
|
34
|
-
})),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return freeInputModelField();
|
|
39
|
-
}
|
|
40
|
-
}
|
package/dist/model.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
-
import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from "./credits-auth.js";
|
|
3
|
-
function normalizeOpenAiModelId(model) {
|
|
4
|
-
return model.includes("/") ? model.split("/").slice(1).join("/") : model;
|
|
5
|
-
}
|
|
6
|
-
export function resolveOpenAiModel(model, options) {
|
|
7
|
-
const modelId = normalizeOpenAiModelId(model);
|
|
8
|
-
const useCredits = shouldUseCreditsAuth(options);
|
|
9
|
-
if (useCredits) {
|
|
10
|
-
const config = resolveCreditsAuthConfig();
|
|
11
|
-
if (!config) {
|
|
12
|
-
throw new Error("OpenBot Credits is not configured. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN.");
|
|
13
|
-
}
|
|
14
|
-
const baseURL = creditsProviderBaseUrl(config);
|
|
15
|
-
const headers = { [INTEGRATIONS_TOKEN_HEADER]: config.token };
|
|
16
|
-
const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
|
|
17
|
-
return createOpenAI({ baseURL, apiKey, headers })(modelId);
|
|
18
|
-
}
|
|
19
|
-
const apiKey = options?.openaiApiKey?.trim();
|
|
20
|
-
if (!apiKey) {
|
|
21
|
-
throw new Error("OpenAI API key is required in BYOK mode. Add `OPENAI_API_KEY` under workspace settings or switch `authMode` to `credits` on cloud.");
|
|
22
|
-
}
|
|
23
|
-
return createOpenAI({ apiKey })(modelId);
|
|
24
|
-
}
|
package/dist/slack-agent.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { generateText, stepCountIs } from "ai";
|
|
2
|
-
import { resolveOpenAiModel } from "./model.js";
|
|
3
|
-
import { createSlackMcpClient } from "./slack-mcp.js";
|
|
4
|
-
const SYSTEM_PROMPT = `You are the OpenBot Slack specialist agent.
|
|
5
|
-
Use Slack MCP tools to post messages, list channels, read history, and perform other workspace actions when needed.
|
|
6
|
-
Summarize results clearly in plain text for the user. Be concise and friendly.`;
|
|
7
|
-
export async function runSlackAgent(args) {
|
|
8
|
-
const model = resolveOpenAiModel(args.model ?? "openai/gpt-4o-mini", {
|
|
9
|
-
authMode: args.authMode,
|
|
10
|
-
openaiApiKey: args.openaiApiKey,
|
|
11
|
-
});
|
|
12
|
-
const mcpClient = await createSlackMcpClient({
|
|
13
|
-
botToken: args.botToken,
|
|
14
|
-
teamId: args.teamId,
|
|
15
|
-
});
|
|
16
|
-
try {
|
|
17
|
-
const tools = await mcpClient.tools();
|
|
18
|
-
const result = await generateText({
|
|
19
|
-
model,
|
|
20
|
-
system: SYSTEM_PROMPT,
|
|
21
|
-
prompt: args.prompt,
|
|
22
|
-
tools,
|
|
23
|
-
stopWhen: stepCountIs(10),
|
|
24
|
-
});
|
|
25
|
-
return result.text.trim() || "Done.";
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
await mcpClient.close();
|
|
29
|
-
}
|
|
30
|
-
}
|
package/dist/slack-mcp.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { createMCPClient } from "@ai-sdk/mcp";
|
|
2
|
-
import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
|
|
3
|
-
export async function createSlackMcpClient(args) {
|
|
4
|
-
return createMCPClient({
|
|
5
|
-
transport: new Experimental_StdioMCPTransport({
|
|
6
|
-
command: "npx",
|
|
7
|
-
args: ["-y", "@modelcontextprotocol/server-slack"],
|
|
8
|
-
env: {
|
|
9
|
-
...process.env,
|
|
10
|
-
SLACK_BOT_TOKEN: args.botToken,
|
|
11
|
-
SLACK_TEAM_ID: args.teamId,
|
|
12
|
-
},
|
|
13
|
-
}),
|
|
14
|
-
});
|
|
15
|
-
}
|
package/dist/slack-threads.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
function slackThreadKey(channel, rootTs) {
|
|
2
|
-
return `${channel}:${rootTs}`;
|
|
3
|
-
}
|
|
4
|
-
const activeSlackThreads = new Set();
|
|
5
|
-
const threadHistories = new Map();
|
|
6
|
-
export function markSlackThreadActive(channel, rootTs) {
|
|
7
|
-
activeSlackThreads.add(slackThreadKey(channel, rootTs));
|
|
8
|
-
}
|
|
9
|
-
export function isSlackThreadActive(channel, rootTs) {
|
|
10
|
-
return activeSlackThreads.has(slackThreadKey(channel, rootTs));
|
|
11
|
-
}
|
|
12
|
-
export function getSlackThreadHistory(channel, rootTs) {
|
|
13
|
-
return threadHistories.get(slackThreadKey(channel, rootTs)) ?? [];
|
|
14
|
-
}
|
|
15
|
-
export function appendSlackThreadHistory(channel, rootTs, ...messages) {
|
|
16
|
-
const key = slackThreadKey(channel, rootTs);
|
|
17
|
-
const history = threadHistories.get(key) ?? [];
|
|
18
|
-
history.push(...messages);
|
|
19
|
-
threadHistories.set(key, history);
|
|
20
|
-
}
|
package/dist/slack-verify.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function verifySlackSignature(signingSecret: string, rawBody: Buffer, timestamp: string | undefined, signature: string | undefined): boolean;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|