@jobmatchme/bee-slack 0.1.9 → 0.1.11
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 +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/gateway.d.ts +4 -0
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +79 -1
- package/dist/gateway.js.map +1 -1
- package/dist/handoff-reply-cache.d.ts +9 -0
- package/dist/handoff-reply-cache.d.ts.map +1 -0
- package/dist/handoff-reply-cache.js +32 -0
- package/dist/handoff-reply-cache.js.map +1 -0
- package/dist/handoff.d.ts +28 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +193 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/slack-sink.d.ts +6 -1
- package/dist/slack-sink.d.ts.map +1 -1
- package/dist/slack-sink.js +7 -2
- package/dist/slack-sink.js.map +1 -1
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,11 +23,32 @@ The package is intentionally thin. It owns Slack-specific concerns:
|
|
|
23
23
|
- route matching
|
|
24
24
|
- posting and updating Slack messages
|
|
25
25
|
- uploading artifacts to Slack
|
|
26
|
+
- public handoffs from trusted internal frontends into allowlisted Slack threads
|
|
26
27
|
|
|
27
28
|
It does not own protocol orchestration itself. That responsibility stays in
|
|
28
29
|
`@jobmatchme/bee-gate`, which keeps the Slack adapter replaceable and easier to
|
|
29
30
|
compare against other frontends.
|
|
30
31
|
|
|
32
|
+
## Grafana and other authenticated handoffs
|
|
33
|
+
|
|
34
|
+
The optional handoff HTTP server lets a trusted internal frontend create a
|
|
35
|
+
public Slack thread, dispatch the same question to an allowlisted Bee worker,
|
|
36
|
+
return the Slack permalink immediately, and read the thread replies. When the
|
|
37
|
+
Slack app has no channel-history scope, the endpoint falls back to the
|
|
38
|
+
outbound Bee replies observed by this gateway; the permalink remains the
|
|
39
|
+
complete shared conversation. The
|
|
40
|
+
handoff route fixes both channel and worker server-side. Keep the Service
|
|
41
|
+
cluster-internal and restrict ingress to the trusted frontend namespace. POST
|
|
42
|
+
requests require Grafana's authenticated `X-Grafana-User` data-proxy header;
|
|
43
|
+
the server replaces any browser-supplied actor identity with that value.
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
GET /health
|
|
47
|
+
GET /api/handoffs/routes
|
|
48
|
+
POST /api/handoffs
|
|
49
|
+
GET /api/handoffs/:routeId/:threadTs/replies
|
|
50
|
+
```
|
|
51
|
+
|
|
31
52
|
## Local development
|
|
32
53
|
|
|
33
54
|
For local manual testing, copy `local.config.example.json` to
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,kBAAkB,CA+BlE","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tconst handoffConfig = process.env.BEE_SLACK_HANDOFF_CONFIG;\n\tif (handoffConfig) {\n\t\tconfig.handoff = JSON.parse(handoffConfig) as SlackGatewayConfig[\"handoff\"];\n\t}\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\tif (config.handoff?.enabled) {\n\t\tif (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {\n\t\t\tthrow new Error(`handoff.routes must be a non-empty array in ${fullPath}`);\n\t\t}\n\t\tfor (const route of config.handoff.routes) {\n\t\t\tif (!route.id || !route.channelId || !route.worker?.subject) {\n\t\t\t\tthrow new Error(`Every handoff route needs id, channelId and worker.subject in ${fullPath}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn config;\n}\n"]}
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,10 @@ export function loadConfig(configPath) {
|
|
|
7
7
|
}
|
|
8
8
|
const fullPath = resolve(path);
|
|
9
9
|
const config = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
10
|
+
const handoffConfig = process.env.BEE_SLACK_HANDOFF_CONFIG;
|
|
11
|
+
if (handoffConfig) {
|
|
12
|
+
config.handoff = JSON.parse(handoffConfig);
|
|
13
|
+
}
|
|
10
14
|
if (!config.appToken)
|
|
11
15
|
throw new Error(`Missing appToken in ${fullPath}`);
|
|
12
16
|
if (!config.botToken)
|
|
@@ -17,6 +21,16 @@ export function loadConfig(configPath) {
|
|
|
17
21
|
if (!Array.isArray(config.routes) || config.routes.length === 0) {
|
|
18
22
|
throw new Error(`Missing routes in ${fullPath}`);
|
|
19
23
|
}
|
|
24
|
+
if (config.handoff?.enabled) {
|
|
25
|
+
if (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {
|
|
26
|
+
throw new Error(`handoff.routes must be a non-empty array in ${fullPath}`);
|
|
27
|
+
}
|
|
28
|
+
for (const route of config.handoff.routes) {
|
|
29
|
+
if (!route.id || !route.channelId || !route.worker?.subject) {
|
|
30
|
+
throw new Error(`Every handoff route needs id, channelId and worker.subject in ${fullPath}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
20
34
|
return config;
|
|
21
35
|
}
|
|
22
36
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,UAAU,UAAU,CAAC,UAAmB,EAAsB;IACnE,MAAM,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAuB,CAAC;IACjF,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\treturn config;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,UAAU,UAAU,CAAC,UAAmB,EAAsB;IACnE,MAAM,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAuB,CAAC;IACjF,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC;IAC3D,IAAI,aAAa,EAAE,CAAC;QACnB,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAkC,CAAC;IAC7E,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,KAAK,CAAC,+CAA+C,QAAQ,EAAE,CAAC,CAAC;QAC5E,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC7D,MAAM,IAAI,KAAK,CAAC,iEAAiE,QAAQ,EAAE,CAAC,CAAC;YAC9F,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tconst handoffConfig = process.env.BEE_SLACK_HANDOFF_CONFIG;\n\tif (handoffConfig) {\n\t\tconfig.handoff = JSON.parse(handoffConfig) as SlackGatewayConfig[\"handoff\"];\n\t}\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\tif (config.handoff?.enabled) {\n\t\tif (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {\n\t\t\tthrow new Error(`handoff.routes must be a non-empty array in ${fullPath}`);\n\t\t}\n\t\tfor (const route of config.handoff.routes) {\n\t\t\tif (!route.id || !route.channelId || !route.worker?.subject) {\n\t\t\t\tthrow new Error(`Every handoff route needs id, channelId and worker.subject in ${fullPath}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn config;\n}\n"]}
|
package/dist/gateway.d.ts
CHANGED
|
@@ -12,8 +12,12 @@ export declare class SlackGateway {
|
|
|
12
12
|
private sink;
|
|
13
13
|
private engine;
|
|
14
14
|
private workerClient;
|
|
15
|
+
private handoffReplyCache;
|
|
16
|
+
private slackThreadHistoryUnavailable;
|
|
15
17
|
constructor(config: SlackGatewayConfig);
|
|
16
18
|
start(): Promise<void>;
|
|
19
|
+
private startHandoffServer;
|
|
20
|
+
private botDisplayName;
|
|
17
21
|
private setupEventHandlers;
|
|
18
22
|
private enqueueInbound;
|
|
19
23
|
private buildResolvedTurn;
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAiC,kBAAkB,EAAuB,MAAM,YAAY,CAAC;AAazG,qBAAa,YAAY;IAaZ,OAAO,CAAC,MAAM;IAZ1B,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,YAAY,CAAgC;IAEpD,YAAoB,MAAM,EAAE,kBAAkB,EAU7C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB3B;IAED,OAAO,CAAC,kBAAkB;YAkEZ,cAAc;IA0C5B,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,iBAAiB;YAOX,mBAAmB;YA+BnB,UAAU;YAsBV,aAAa;CA0B3B;AAED,wBAAsB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5E","sourcesContent":["import {\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { join } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute } from \"./router.js\";\nimport { SlackSink } from \"./slack-sink.js\";\nimport type { ResolvedSlackRoute, SlackFile, SlackGatewayConfig, SlackInboundMessage } from \"./types.js\";\n\ninterface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\ninterface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\nexport class SlackGateway {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate botUserId: string | null = null;\n\tprivate teamId: string | null = null;\n\tprivate teamName: string | null = null;\n\tprivate blobStore: BlobStore;\n\tprivate sink: SlackSink;\n\tprivate engine: BeeGatewayEngine<string> | null = null;\n\tprivate workerClient: BeeWorkerClient | null = null;\n\n\tconstructor(private config: SlackGatewayConfig) {\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.blobStore = new LocalFileBlobStore(\n\t\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t\t);\n\t\tthis.sink = new SlackSink(this.webClient, this.blobStore);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.workerClient = await createNatsBeeClient(this.config.nats);\n\t\tthis.engine = new BeeGatewayEngine({\n\t\t\tsink: this.sink,\n\t\t\tworkerClient: this.workerClient,\n\t\t\tlogger: {\n\t\t\t\tinfo: log.logInfo,\n\t\t\t\twarn: log.logWarning,\n\t\t\t\terror: log.logError,\n\t\t\t},\n\t\t});\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tthis.teamId = auth.team_id as string;\n\t\tthis.teamName = (auth.team as string | undefined) || null;\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\t\tlog.logInfo(\n\t\t\t`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`,\n\t\t);\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.channel.startsWith(\"D\")) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) return;\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") return;\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) return;\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);\n\t\t\tif (!isDM && isBotMention) return;\n\t\t\tif (!isDM) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"dm\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async enqueueInbound(message: SlackInboundMessage): Promise<void> {\n\t\tif (!this.botUserId || !this.teamId) {\n\t\t\tthrow new Error(\"Gateway has not been initialized\");\n\t\t}\n\t\tif (!this.engine) {\n\t\t\tthrow new Error(\"Gateway engine has not been initialized\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst resolved = resolveRoute(this.config.routes, message, {\n\t\t\t\tbotUserId: this.botUserId,\n\t\t\t\tteamId: this.teamId,\n\t\t\t\tteamName: this.teamName || undefined,\n\t\t\t});\n\t\t\tif (!resolved) {\n\t\t\t\tlog.logWarning(`No route for channel ${message.channelId}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outputTarget = this.buildOutputTarget(message, resolved);\n\t\t\tif (message.text.trim().toLowerCase() === \"stop\") {\n\t\t\t\tconst stopped = await this.engine.stopActiveRun(resolved.sessionId);\n\t\t\t\tawait this.sink.postMessage(outputTarget, stopped ? \"Stopping active run.\" : \"Nothing running.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst attachments = await this.downloadAttachments(resolved, message);\n\t\t\tconst input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);\n\t\t\tthis.engine.dispatch(input);\n\t\t} catch (error) {\n\t\t\tconst messageText = error instanceof Error ? error.message : String(error);\n\t\t\tlog.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);\n\t\t\tawait this.sink.postMessage(\n\t\t\t\t{\n\t\t\t\t\tchannelId: message.channelId,\n\t\t\t\t\tthreadId: message.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t\t\t},\n\t\t\t\t`_Gateway error: ${messageText}_`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate buildResolvedTurn(\n\t\tmessage: SlackInboundMessage,\n\t\tresolved: ResolvedSlackRoute,\n\t\tattachments: AttachmentRef[],\n\t\toutput: TransportOutputTarget,\n\t): BeeResolvedTurn {\n\t\tif (!this.teamId || !this.botUserId) {\n\t\t\tthrow new Error(\"Missing Slack gateway identity\");\n\t\t}\n\n\t\treturn {\n\t\t\tsessionId: resolved.sessionId,\n\t\t\tthreadId: resolved.threadTs,\n\t\t\tworker: resolved.route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId: resolved.conversationId,\n\t\t\t},\n\t\t\tactor: {\n\t\t\t\tuserId: message.userId,\n\t\t\t\tuserName: message.userName,\n\t\t\t\tdisplayName: message.displayName,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttext: message.text,\n\t\t\t},\n\t\t\tattachments,\n\t\t\toutput,\n\t\t};\n\t}\n\n\tprivate buildOutputTarget(message: SlackInboundMessage, resolved: ResolvedSlackRoute): TransportOutputTarget {\n\t\treturn {\n\t\t\tchannelId: message.channelId,\n\t\t\tthreadId: resolved.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t};\n\t}\n\n\tprivate async downloadAttachments(\n\t\tresolved: ResolvedSlackRoute,\n\t\tmessage: SlackInboundMessage,\n\t): Promise<AttachmentRef[]> {\n\t\tconst files = message.files || [];\n\t\tif (files.length === 0) return [];\n\n\t\tconst attachments: AttachmentRef[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url || !file.name) continue;\n\t\t\tconst response = await fetch(url, {\n\t\t\t\theaders: { authorization: `Bearer ${this.config.botToken}` },\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);\n\t\t\t}\n\t\t\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\t\t\tattachments.push(\n\t\t\t\tawait this.blobStore.put({\n\t\t\t\t\tnamespace: `incoming/slack/${resolved.sessionId}`,\n\t\t\t\t\tname: file.name,\n\t\t\t\t\ttitle: file.name,\n\t\t\t\t\tmimeType: file.mimetype,\n\t\t\t\t\tdata: bytes,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn attachments;\n\t}\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const user of members) {\n\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\tthis.users.set(user.id, {\n\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel,im\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; user?: string }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\tif (!channel.id) continue;\n\t\t\t\t\tif (channel.name) {\n\t\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name });\n\t\t\t\t\t} else if (channel.user && this.users.has(channel.user)) {\n\t\t\t\t\t\tthis.channels.set(channel.id, {\n\t\t\t\t\t\t\tid: channel.id,\n\t\t\t\t\t\t\tname: `DM:${this.users.get(channel.user)!.userName}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n\nexport async function startGatewayFromEnv(configPath?: string): Promise<void> {\n\tconst config = loadConfig(configPath);\n\tconst gateway = new SlackGateway(config);\n\tawait gateway.start();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAiC,kBAAkB,EAAuB,MAAM,YAAY,CAAC;AAazG,qBAAa,YAAY;IAeZ,OAAO,CAAC,MAAM;IAd1B,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,6BAA6B,CAAS;IAE9C,YAAoB,MAAM,EAAE,kBAAkB,EAa7C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB3B;IAED,OAAO,CAAC,kBAAkB;IAqD1B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,kBAAkB;YAkEZ,cAAc;IA0C5B,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,iBAAiB;YAOX,mBAAmB;YA+BnB,UAAU;YAsBV,aAAa;CA0B3B;AAaD,wBAAsB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5E","sourcesContent":["import {\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { join } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport { createHandoffServer, SlackHandoffController } from \"./handoff.js\";\nimport { HandoffReplyCache } from \"./handoff-reply-cache.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute } from \"./router.js\";\nimport { SlackSink } from \"./slack-sink.js\";\nimport type { ResolvedSlackRoute, SlackFile, SlackGatewayConfig, SlackInboundMessage } from \"./types.js\";\n\ninterface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\ninterface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\nexport class SlackGateway {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate botUserId: string | null = null;\n\tprivate teamId: string | null = null;\n\tprivate teamName: string | null = null;\n\tprivate blobStore: BlobStore;\n\tprivate sink: SlackSink;\n\tprivate engine: BeeGatewayEngine<string> | null = null;\n\tprivate workerClient: BeeWorkerClient | null = null;\n\tprivate handoffReplyCache = new HandoffReplyCache();\n\tprivate slackThreadHistoryUnavailable = false;\n\n\tconstructor(private config: SlackGatewayConfig) {\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.blobStore = new LocalFileBlobStore(\n\t\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t\t);\n\t\tthis.sink = new SlackSink(this.webClient, this.blobStore, {\n\t\t\tposted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),\n\t\t\tupdated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),\n\t\t});\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.workerClient = await createNatsBeeClient(this.config.nats);\n\t\tthis.engine = new BeeGatewayEngine({\n\t\t\tsink: this.sink,\n\t\t\tworkerClient: this.workerClient,\n\t\t\tlogger: {\n\t\t\t\tinfo: log.logInfo,\n\t\t\t\twarn: log.logWarning,\n\t\t\t\terror: log.logError,\n\t\t\t},\n\t\t});\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tthis.teamId = auth.team_id as string;\n\t\tthis.teamName = (auth.team as string | undefined) || null;\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tthis.startHandoffServer();\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\t\tlog.logInfo(\n\t\t\t`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`,\n\t\t);\n\t}\n\n\tprivate startHandoffServer(): void {\n\t\tconst handoffConfig = this.config.handoff;\n\t\tif (!handoffConfig?.enabled) return;\n\t\tif (!this.teamId || !this.engine) throw new Error(\"Gateway must be initialized before handoff server startup\");\n\t\tconst controller = new SlackHandoffController(handoffConfig, {\n\t\t\tteamId: this.teamId,\n\t\t\tpostRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),\n\t\t\tgetPermalink: async (channelId, messageTs) => {\n\t\t\t\tconst result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });\n\t\t\t\tif (!result.permalink) throw new Error(\"Slack did not return a permalink\");\n\t\t\t\treturn result.permalink;\n\t\t\t},\n\t\t\tdispatch: (input) => this.engine!.dispatch(input),\n\t\t\tgetReplies: async (channelId, threadTs) => {\n\t\t\t\tif (this.slackThreadHistoryUnavailable) return this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.webClient.conversations.replies({\n\t\t\t\t\t\tchannel: channelId,\n\t\t\t\t\t\tts: threadTs,\n\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t});\n\t\t\t\t\treturn (result.messages || []).map((message) => {\n\t\t\t\t\t\tconst typed = message as typeof message & {\n\t\t\t\t\t\t\tbot_id?: string;\n\t\t\t\t\t\t\tbot_profile?: { name?: string };\n\t\t\t\t\t\t\tusername?: string;\n\t\t\t\t\t\t\tthread_ts?: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tts: String(typed.ts || \"\"),\n\t\t\t\t\t\t\tthreadTs: typed.thread_ts,\n\t\t\t\t\t\t\ttext: typed.text || \"\",\n\t\t\t\t\t\t\tauthor: typed.user\n\t\t\t\t\t\t\t\t? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user\n\t\t\t\t\t\t\t\t: typed.bot_profile?.name || typed.username || \"Bot\",\n\t\t\t\t\t\t\tisBot: Boolean(typed.bot_id),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst errorCode = slackErrorCode(error);\n\t\t\t\t\tif (!isSlackHistoryPermissionError(errorCode)) throw error;\n\t\t\t\t\tthis.slackThreadHistoryUnavailable = true;\n\t\t\t\t\tlog.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);\n\t\t\t\t\treturn this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst server = createHandoffServer(controller);\n\t\tconst host = handoffConfig.host || \"0.0.0.0\";\n\t\tconst port = handoffConfig.port || 8080;\n\t\tserver.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));\n\t}\n\n\tprivate botDisplayName(): string {\n\t\tif (!this.botUserId) return \"Bee\";\n\t\tconst bot = this.users.get(this.botUserId);\n\t\treturn bot?.displayName || bot?.userName || \"Bee\";\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.channel.startsWith(\"D\")) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) return;\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") return;\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) return;\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);\n\t\t\tif (!isDM && isBotMention) return;\n\t\t\tif (!isDM) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"dm\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async enqueueInbound(message: SlackInboundMessage): Promise<void> {\n\t\tif (!this.botUserId || !this.teamId) {\n\t\t\tthrow new Error(\"Gateway has not been initialized\");\n\t\t}\n\t\tif (!this.engine) {\n\t\t\tthrow new Error(\"Gateway engine has not been initialized\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst resolved = resolveRoute(this.config.routes, message, {\n\t\t\t\tbotUserId: this.botUserId,\n\t\t\t\tteamId: this.teamId,\n\t\t\t\tteamName: this.teamName || undefined,\n\t\t\t});\n\t\t\tif (!resolved) {\n\t\t\t\tlog.logWarning(`No route for channel ${message.channelId}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outputTarget = this.buildOutputTarget(message, resolved);\n\t\t\tif (message.text.trim().toLowerCase() === \"stop\") {\n\t\t\t\tconst stopped = await this.engine.stopActiveRun(resolved.sessionId);\n\t\t\t\tawait this.sink.postMessage(outputTarget, stopped ? \"Stopping active run.\" : \"Nothing running.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst attachments = await this.downloadAttachments(resolved, message);\n\t\t\tconst input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);\n\t\t\tthis.engine.dispatch(input);\n\t\t} catch (error) {\n\t\t\tconst messageText = error instanceof Error ? error.message : String(error);\n\t\t\tlog.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);\n\t\t\tawait this.sink.postMessage(\n\t\t\t\t{\n\t\t\t\t\tchannelId: message.channelId,\n\t\t\t\t\tthreadId: message.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t\t\t},\n\t\t\t\t`_Gateway error: ${messageText}_`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate buildResolvedTurn(\n\t\tmessage: SlackInboundMessage,\n\t\tresolved: ResolvedSlackRoute,\n\t\tattachments: AttachmentRef[],\n\t\toutput: TransportOutputTarget,\n\t): BeeResolvedTurn {\n\t\tif (!this.teamId || !this.botUserId) {\n\t\t\tthrow new Error(\"Missing Slack gateway identity\");\n\t\t}\n\n\t\treturn {\n\t\t\tsessionId: resolved.sessionId,\n\t\t\tthreadId: resolved.threadTs,\n\t\t\tworker: resolved.route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId: resolved.conversationId,\n\t\t\t},\n\t\t\tactor: {\n\t\t\t\tuserId: message.userId,\n\t\t\t\tuserName: message.userName,\n\t\t\t\tdisplayName: message.displayName,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttext: message.text,\n\t\t\t},\n\t\t\tattachments,\n\t\t\toutput,\n\t\t};\n\t}\n\n\tprivate buildOutputTarget(message: SlackInboundMessage, resolved: ResolvedSlackRoute): TransportOutputTarget {\n\t\treturn {\n\t\t\tchannelId: message.channelId,\n\t\t\tthreadId: resolved.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t};\n\t}\n\n\tprivate async downloadAttachments(\n\t\tresolved: ResolvedSlackRoute,\n\t\tmessage: SlackInboundMessage,\n\t): Promise<AttachmentRef[]> {\n\t\tconst files = message.files || [];\n\t\tif (files.length === 0) return [];\n\n\t\tconst attachments: AttachmentRef[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url || !file.name) continue;\n\t\t\tconst response = await fetch(url, {\n\t\t\t\theaders: { authorization: `Bearer ${this.config.botToken}` },\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);\n\t\t\t}\n\t\t\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\t\t\tattachments.push(\n\t\t\t\tawait this.blobStore.put({\n\t\t\t\t\tnamespace: `incoming/slack/${resolved.sessionId}`,\n\t\t\t\t\tname: file.name,\n\t\t\t\t\ttitle: file.name,\n\t\t\t\t\tmimeType: file.mimetype,\n\t\t\t\t\tdata: bytes,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn attachments;\n\t}\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const user of members) {\n\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\tthis.users.set(user.id, {\n\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel,im\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; user?: string }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\tif (!channel.id) continue;\n\t\t\t\t\tif (channel.name) {\n\t\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name });\n\t\t\t\t\t} else if (channel.user && this.users.has(channel.user)) {\n\t\t\t\t\t\tthis.channels.set(channel.id, {\n\t\t\t\t\t\t\tid: channel.id,\n\t\t\t\t\t\t\tname: `DM:${this.users.get(channel.user)!.userName}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n\nfunction slackErrorCode(error: unknown): string | undefined {\n\tif (!error || typeof error !== \"object\" || !(\"data\" in error)) return undefined;\n\tconst data = error.data;\n\tif (!data || typeof data !== \"object\" || !(\"error\" in data)) return undefined;\n\treturn typeof data.error === \"string\" ? data.error : undefined;\n}\n\nfunction isSlackHistoryPermissionError(errorCode: string | undefined): boolean {\n\treturn errorCode === \"missing_scope\" || errorCode === \"no_permission\" || errorCode === \"not_allowed_token_type\";\n}\n\nexport async function startGatewayFromEnv(configPath?: string): Promise<void> {\n\tconst config = loadConfig(configPath);\n\tconst gateway = new SlackGateway(config);\n\tawait gateway.start();\n}\n"]}
|
package/dist/gateway.js
CHANGED
|
@@ -3,6 +3,8 @@ import { SocketModeClient } from "@slack/socket-mode";
|
|
|
3
3
|
import { WebClient } from "@slack/web-api";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { loadConfig } from "./config.js";
|
|
6
|
+
import { createHandoffServer, SlackHandoffController } from "./handoff.js";
|
|
7
|
+
import { HandoffReplyCache } from "./handoff-reply-cache.js";
|
|
6
8
|
import * as log from "./log.js";
|
|
7
9
|
import { resolveRoute } from "./router.js";
|
|
8
10
|
import { SlackSink } from "./slack-sink.js";
|
|
@@ -19,6 +21,8 @@ export class SlackGateway {
|
|
|
19
21
|
sink;
|
|
20
22
|
engine = null;
|
|
21
23
|
workerClient = null;
|
|
24
|
+
handoffReplyCache = new HandoffReplyCache();
|
|
25
|
+
slackThreadHistoryUnavailable = false;
|
|
22
26
|
constructor(config) {
|
|
23
27
|
this.config = config;
|
|
24
28
|
this.socketClient = new SocketModeClient({ appToken: config.appToken });
|
|
@@ -27,7 +31,10 @@ export class SlackGateway {
|
|
|
27
31
|
process.env.BEE_BLOB_STORE_ROOT ||
|
|
28
32
|
process.env.HUDAI_BLOB_STORE_ROOT ||
|
|
29
33
|
join(process.cwd(), ".bee-blob-store"));
|
|
30
|
-
this.sink = new SlackSink(this.webClient, this.blobStore
|
|
34
|
+
this.sink = new SlackSink(this.webClient, this.blobStore, {
|
|
35
|
+
posted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),
|
|
36
|
+
updated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),
|
|
37
|
+
});
|
|
31
38
|
}
|
|
32
39
|
async start() {
|
|
33
40
|
this.workerClient = await createNatsBeeClient(this.config.nats);
|
|
@@ -45,10 +52,70 @@ export class SlackGateway {
|
|
|
45
52
|
this.teamId = auth.team_id;
|
|
46
53
|
this.teamName = auth.team || null;
|
|
47
54
|
await Promise.all([this.fetchUsers(), this.fetchChannels()]);
|
|
55
|
+
this.startHandoffServer();
|
|
48
56
|
this.setupEventHandlers();
|
|
49
57
|
await this.socketClient.start();
|
|
50
58
|
log.logInfo(`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`);
|
|
51
59
|
}
|
|
60
|
+
startHandoffServer() {
|
|
61
|
+
const handoffConfig = this.config.handoff;
|
|
62
|
+
if (!handoffConfig?.enabled)
|
|
63
|
+
return;
|
|
64
|
+
if (!this.teamId || !this.engine)
|
|
65
|
+
throw new Error("Gateway must be initialized before handoff server startup");
|
|
66
|
+
const controller = new SlackHandoffController(handoffConfig, {
|
|
67
|
+
teamId: this.teamId,
|
|
68
|
+
postRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),
|
|
69
|
+
getPermalink: async (channelId, messageTs) => {
|
|
70
|
+
const result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });
|
|
71
|
+
if (!result.permalink)
|
|
72
|
+
throw new Error("Slack did not return a permalink");
|
|
73
|
+
return result.permalink;
|
|
74
|
+
},
|
|
75
|
+
dispatch: (input) => this.engine.dispatch(input),
|
|
76
|
+
getReplies: async (channelId, threadTs) => {
|
|
77
|
+
if (this.slackThreadHistoryUnavailable)
|
|
78
|
+
return this.handoffReplyCache.replies(channelId, threadTs);
|
|
79
|
+
try {
|
|
80
|
+
const result = await this.webClient.conversations.replies({
|
|
81
|
+
channel: channelId,
|
|
82
|
+
ts: threadTs,
|
|
83
|
+
limit: 100,
|
|
84
|
+
});
|
|
85
|
+
return (result.messages || []).map((message) => {
|
|
86
|
+
const typed = message;
|
|
87
|
+
return {
|
|
88
|
+
ts: String(typed.ts || ""),
|
|
89
|
+
threadTs: typed.thread_ts,
|
|
90
|
+
text: typed.text || "",
|
|
91
|
+
author: typed.user
|
|
92
|
+
? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user
|
|
93
|
+
: typed.bot_profile?.name || typed.username || "Bot",
|
|
94
|
+
isBot: Boolean(typed.bot_id),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const errorCode = slackErrorCode(error);
|
|
100
|
+
if (!isSlackHistoryPermissionError(errorCode))
|
|
101
|
+
throw error;
|
|
102
|
+
this.slackThreadHistoryUnavailable = true;
|
|
103
|
+
log.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);
|
|
104
|
+
return this.handoffReplyCache.replies(channelId, threadTs);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const server = createHandoffServer(controller);
|
|
109
|
+
const host = handoffConfig.host || "0.0.0.0";
|
|
110
|
+
const port = handoffConfig.port || 8080;
|
|
111
|
+
server.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));
|
|
112
|
+
}
|
|
113
|
+
botDisplayName() {
|
|
114
|
+
if (!this.botUserId)
|
|
115
|
+
return "Bee";
|
|
116
|
+
const bot = this.users.get(this.botUserId);
|
|
117
|
+
return bot?.displayName || bot?.userName || "Bee";
|
|
118
|
+
}
|
|
52
119
|
setupEventHandlers() {
|
|
53
120
|
this.socketClient.on("app_mention", async ({ event, ack }) => {
|
|
54
121
|
await ack();
|
|
@@ -237,6 +304,17 @@ export class SlackGateway {
|
|
|
237
304
|
} while (cursor);
|
|
238
305
|
}
|
|
239
306
|
}
|
|
307
|
+
function slackErrorCode(error) {
|
|
308
|
+
if (!error || typeof error !== "object" || !("data" in error))
|
|
309
|
+
return undefined;
|
|
310
|
+
const data = error.data;
|
|
311
|
+
if (!data || typeof data !== "object" || !("error" in data))
|
|
312
|
+
return undefined;
|
|
313
|
+
return typeof data.error === "string" ? data.error : undefined;
|
|
314
|
+
}
|
|
315
|
+
function isSlackHistoryPermissionError(errorCode) {
|
|
316
|
+
return errorCode === "missing_scope" || errorCode === "no_permission" || errorCode === "not_allowed_token_type";
|
|
317
|
+
}
|
|
240
318
|
export async function startGatewayFromEnv(configPath) {
|
|
241
319
|
const config = loadConfig(configPath);
|
|
242
320
|
const gateway = new SlackGateway(config);
|
package/dist/gateway.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,gBAAgB,EAIhB,mBAAmB,EACnB,kBAAkB,GAElB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAc5C,MAAM,OAAO,YAAY;IAaJ,MAAM;IAZlB,YAAY,CAAmB;IAC/B,SAAS,CAAY;IACrB,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IACrC,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC3C,SAAS,GAAkB,IAAI,CAAC;IAChC,MAAM,GAAkB,IAAI,CAAC;IAC7B,QAAQ,GAAkB,IAAI,CAAC;IAC/B,SAAS,CAAY;IACrB,IAAI,CAAY;IAChB,MAAM,GAAoC,IAAI,CAAC;IAC/C,YAAY,GAA2B,IAAI,CAAC;IAEpD,YAAoB,MAA0B,EAAE;sBAA5B,MAAM;QACzB,IAAI,CAAC,YAAY,GAAG,IAAI,gBAAgB,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,kBAAkB,CACtC,OAAO,CAAC,GAAG,CAAC,yBAAyB;YACpC,OAAO,CAAC,GAAG,CAAC,mBAAmB;YAC/B,OAAO,CAAC,GAAG,CAAC,qBAAqB;YACjC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,iBAAiB,CAAC,CACvC,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAAA,CAC1D;IAED,KAAK,CAAC,KAAK,GAAkB;QAC5B,IAAI,CAAC,YAAY,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAgB,CAAC;YAClC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,MAAM,EAAE;gBACP,IAAI,EAAE,GAAG,CAAC,OAAO;gBACjB,IAAI,EAAE,GAAG,CAAC,UAAU;gBACpB,KAAK,EAAE,GAAG,CAAC,QAAQ;aACnB;SACD,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAiB,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAiB,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAI,IAAI,CAAC,IAA2B,IAAI,IAAI,CAAC;QAC1D,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC1B,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAChC,GAAG,CAAC,OAAO,CACV,gBAAgB,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,QAAQ,CAAC,IAAI,iBAAiB,IAAI,CAAC,KAAK,CAAC,IAAI,QAAQ,CACtH,CAAC;IAAA,CACF;IAEO,kBAAkB,GAAS;QAClC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YAC7D,MAAM,GAAG,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,KAOT,CAAC;YAEF,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,OAAO;YAEtC,MAAM,IAAI,CAAC,cAAc,CAAC;gBACzB,IAAI,EAAE,SAAS;gBACf,SAAS,EAAE,CAAC,CAAC,OAAO;gBACpB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI;gBAC/C,QAAQ,EAAE,CAAC,CAAC,SAAS;gBACrB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,MAAM,EAAE,CAAC,CAAC,IAAI;gBACd,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ;gBAC1C,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,WAAW;gBAChD,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;gBACjD,KAAK,EAAE,CAAC,CAAC,KAAK;aACd,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YACzD,MAAM,GAAG,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,KAUT,CAAC;YAEF,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS;gBAAE,OAAO;YAC7D,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,YAAY;gBAAE,OAAO;YAClE,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;gBAAE,OAAO;YAE1D,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,KAAK,IAAI,CAAC;YACrC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;YAClF,IAAI,CAAC,IAAI,IAAI,YAAY;gBAAE,OAAO;YAClC,IAAI,CAAC,IAAI;gBAAE,OAAO;YAElB,MAAM,IAAI,CAAC,cAAc,CAAC;gBACzB,IAAI,EAAE,IAAI;gBACV,SAAS,EAAE,CAAC,CAAC,OAAO;gBACpB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI;gBAC/C,QAAQ,EAAE,CAAC,CAAC,SAAS;gBACrB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,MAAM,EAAE,CAAC,CAAC,IAAI;gBACd,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ;gBAC1C,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,WAAW;gBAChD,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;gBACzD,KAAK,EAAE,CAAC,CAAC,KAAK;aACd,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;IAAA,CACH;IAEO,KAAK,CAAC,cAAc,CAAC,OAA4B,EAAiB;QACzE,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC5D,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE;gBAC1D,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,SAAS;aACpC,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,GAAG,CAAC,UAAU,CAAC,wBAAwB,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;gBAC5D,OAAO;YACR,CAAC;YAED,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC/D,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;gBAClD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;gBACpE,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;gBACjG,OAAO;YACR,CAAC;YAED,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACtE,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;YACnF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,WAAW,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC3E,GAAG,CAAC,QAAQ,CAAC,6CAA6C,OAAO,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC;YACrF,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAC1B;gBACC,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;aACnF,EACD,mBAAmB,WAAW,GAAG,CACjC,CAAC;QACH,CAAC;IAAA,CACD;IAEO,iBAAiB,CACxB,OAA4B,EAC5B,QAA4B,EAC5B,WAA4B,EAC5B,MAA6B,EACX;QAClB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACnD,CAAC;QAED,OAAO;YACN,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;YAC7B,YAAY,EAAE;gBACb,SAAS,EAAE,OAAO;gBAClB,cAAc,EAAE,QAAQ,CAAC,cAAc;aACvC;YACD,KAAK,EAAE;gBACN,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;aAChC;YACD,OAAO,EAAE;gBACR,IAAI,EAAE,OAAO,CAAC,IAAI;aAClB;YACD,WAAW;YACX,MAAM;SACN,CAAC;IAAA,CACF;IAEO,iBAAiB,CAAC,OAA4B,EAAE,QAA4B,EAAyB;QAC5G,OAAO;YACN,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;SACpF,CAAC;IAAA,CACF;IAEO,KAAK,CAAC,mBAAmB,CAChC,QAA4B,EAC5B,OAA4B,EACD;QAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAElC,MAAM,WAAW,GAAoB,EAAE,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,WAAW,CAAC;YAC1D,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACjC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBACjC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE;aAC5D,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACzF,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;YAC3D,WAAW,CAAC,IAAI,CACf,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;gBACxB,SAAS,EAAE,kBAAkB,QAAQ,CAAC,SAAS,EAAE;gBACjD,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,IAAI,CAAC,IAAI;gBAChB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,KAAK;aACX,CAAC,CACF,CAAC;QACH,CAAC;QACD,OAAO,WAAW,CAAC;IAAA,CACnB;IAEO,KAAK,CAAC,UAAU,GAAkB;QACzC,IAAI,MAA0B,CAAC;QAC/B,GAAG,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;YACvE,MAAM,OAAO,GAAG,MAAM,CAAC,OAEX,CAAC;YACb,IAAI,OAAO,EAAE,CAAC;gBACb,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;oBAC5B,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;wBAC3C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE;4BACvB,EAAE,EAAE,IAAI,CAAC,EAAE;4BACX,QAAQ,EAAE,IAAI,CAAC,IAAI;4BACnB,WAAW,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI;yBACxC,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC;YACD,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAAC;QAChD,CAAC,QAAQ,MAAM,EAAE;IAAA,CACjB;IAEO,KAAK,CAAC,aAAa,GAAkB;QAC5C,IAAI,MAA0B,CAAC;QAC/B,GAAG,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC;gBACtD,KAAK,EAAE,mCAAmC;gBAC1C,gBAAgB,EAAE,IAAI;gBACtB,KAAK,EAAE,GAAG;gBACV,MAAM;aACN,CAAC,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,CAAC,QAA4E,CAAC;YACrG,IAAI,QAAQ,EAAE,CAAC;gBACd,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAChC,IAAI,CAAC,OAAO,CAAC,EAAE;wBAAE,SAAS;oBAC1B,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;wBAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;oBACvE,CAAC;yBAAM,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;wBACzD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE;4BAC7B,EAAE,EAAE,OAAO,CAAC,EAAE;4BACd,IAAI,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAE,CAAC,QAAQ,EAAE;yBACpD,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC;YACD,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAAC;QAChD,CAAC,QAAQ,MAAM,EAAE;IAAA,CACjB;CACD;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,UAAmB,EAAiB;IAC7E,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;AAAA,CACtB","sourcesContent":["import {\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { join } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute } from \"./router.js\";\nimport { SlackSink } from \"./slack-sink.js\";\nimport type { ResolvedSlackRoute, SlackFile, SlackGatewayConfig, SlackInboundMessage } from \"./types.js\";\n\ninterface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\ninterface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\nexport class SlackGateway {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate botUserId: string | null = null;\n\tprivate teamId: string | null = null;\n\tprivate teamName: string | null = null;\n\tprivate blobStore: BlobStore;\n\tprivate sink: SlackSink;\n\tprivate engine: BeeGatewayEngine<string> | null = null;\n\tprivate workerClient: BeeWorkerClient | null = null;\n\n\tconstructor(private config: SlackGatewayConfig) {\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.blobStore = new LocalFileBlobStore(\n\t\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t\t);\n\t\tthis.sink = new SlackSink(this.webClient, this.blobStore);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.workerClient = await createNatsBeeClient(this.config.nats);\n\t\tthis.engine = new BeeGatewayEngine({\n\t\t\tsink: this.sink,\n\t\t\tworkerClient: this.workerClient,\n\t\t\tlogger: {\n\t\t\t\tinfo: log.logInfo,\n\t\t\t\twarn: log.logWarning,\n\t\t\t\terror: log.logError,\n\t\t\t},\n\t\t});\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tthis.teamId = auth.team_id as string;\n\t\tthis.teamName = (auth.team as string | undefined) || null;\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\t\tlog.logInfo(\n\t\t\t`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`,\n\t\t);\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.channel.startsWith(\"D\")) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) return;\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") return;\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) return;\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);\n\t\t\tif (!isDM && isBotMention) return;\n\t\t\tif (!isDM) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"dm\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async enqueueInbound(message: SlackInboundMessage): Promise<void> {\n\t\tif (!this.botUserId || !this.teamId) {\n\t\t\tthrow new Error(\"Gateway has not been initialized\");\n\t\t}\n\t\tif (!this.engine) {\n\t\t\tthrow new Error(\"Gateway engine has not been initialized\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst resolved = resolveRoute(this.config.routes, message, {\n\t\t\t\tbotUserId: this.botUserId,\n\t\t\t\tteamId: this.teamId,\n\t\t\t\tteamName: this.teamName || undefined,\n\t\t\t});\n\t\t\tif (!resolved) {\n\t\t\t\tlog.logWarning(`No route for channel ${message.channelId}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outputTarget = this.buildOutputTarget(message, resolved);\n\t\t\tif (message.text.trim().toLowerCase() === \"stop\") {\n\t\t\t\tconst stopped = await this.engine.stopActiveRun(resolved.sessionId);\n\t\t\t\tawait this.sink.postMessage(outputTarget, stopped ? \"Stopping active run.\" : \"Nothing running.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst attachments = await this.downloadAttachments(resolved, message);\n\t\t\tconst input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);\n\t\t\tthis.engine.dispatch(input);\n\t\t} catch (error) {\n\t\t\tconst messageText = error instanceof Error ? error.message : String(error);\n\t\t\tlog.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);\n\t\t\tawait this.sink.postMessage(\n\t\t\t\t{\n\t\t\t\t\tchannelId: message.channelId,\n\t\t\t\t\tthreadId: message.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t\t\t},\n\t\t\t\t`_Gateway error: ${messageText}_`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate buildResolvedTurn(\n\t\tmessage: SlackInboundMessage,\n\t\tresolved: ResolvedSlackRoute,\n\t\tattachments: AttachmentRef[],\n\t\toutput: TransportOutputTarget,\n\t): BeeResolvedTurn {\n\t\tif (!this.teamId || !this.botUserId) {\n\t\t\tthrow new Error(\"Missing Slack gateway identity\");\n\t\t}\n\n\t\treturn {\n\t\t\tsessionId: resolved.sessionId,\n\t\t\tthreadId: resolved.threadTs,\n\t\t\tworker: resolved.route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId: resolved.conversationId,\n\t\t\t},\n\t\t\tactor: {\n\t\t\t\tuserId: message.userId,\n\t\t\t\tuserName: message.userName,\n\t\t\t\tdisplayName: message.displayName,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttext: message.text,\n\t\t\t},\n\t\t\tattachments,\n\t\t\toutput,\n\t\t};\n\t}\n\n\tprivate buildOutputTarget(message: SlackInboundMessage, resolved: ResolvedSlackRoute): TransportOutputTarget {\n\t\treturn {\n\t\t\tchannelId: message.channelId,\n\t\t\tthreadId: resolved.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t};\n\t}\n\n\tprivate async downloadAttachments(\n\t\tresolved: ResolvedSlackRoute,\n\t\tmessage: SlackInboundMessage,\n\t): Promise<AttachmentRef[]> {\n\t\tconst files = message.files || [];\n\t\tif (files.length === 0) return [];\n\n\t\tconst attachments: AttachmentRef[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url || !file.name) continue;\n\t\t\tconst response = await fetch(url, {\n\t\t\t\theaders: { authorization: `Bearer ${this.config.botToken}` },\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);\n\t\t\t}\n\t\t\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\t\t\tattachments.push(\n\t\t\t\tawait this.blobStore.put({\n\t\t\t\t\tnamespace: `incoming/slack/${resolved.sessionId}`,\n\t\t\t\t\tname: file.name,\n\t\t\t\t\ttitle: file.name,\n\t\t\t\t\tmimeType: file.mimetype,\n\t\t\t\t\tdata: bytes,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn attachments;\n\t}\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const user of members) {\n\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\tthis.users.set(user.id, {\n\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel,im\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; user?: string }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\tif (!channel.id) continue;\n\t\t\t\t\tif (channel.name) {\n\t\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name });\n\t\t\t\t\t} else if (channel.user && this.users.has(channel.user)) {\n\t\t\t\t\t\tthis.channels.set(channel.id, {\n\t\t\t\t\t\t\tid: channel.id,\n\t\t\t\t\t\t\tname: `DM:${this.users.get(channel.user)!.userName}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n\nexport async function startGatewayFromEnv(configPath?: string): Promise<void> {\n\tconst config = loadConfig(configPath);\n\tconst gateway = new SlackGateway(config);\n\tawait gateway.start();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { join } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport { createHandoffServer, SlackHandoffController } from \"./handoff.js\";\nimport { HandoffReplyCache } from \"./handoff-reply-cache.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute } from \"./router.js\";\nimport { SlackSink } from \"./slack-sink.js\";\nimport type { ResolvedSlackRoute, SlackFile, SlackGatewayConfig, SlackInboundMessage } from \"./types.js\";\n\ninterface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\ninterface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\nexport class SlackGateway {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate botUserId: string | null = null;\n\tprivate teamId: string | null = null;\n\tprivate teamName: string | null = null;\n\tprivate blobStore: BlobStore;\n\tprivate sink: SlackSink;\n\tprivate engine: BeeGatewayEngine<string> | null = null;\n\tprivate workerClient: BeeWorkerClient | null = null;\n\tprivate handoffReplyCache = new HandoffReplyCache();\n\tprivate slackThreadHistoryUnavailable = false;\n\n\tconstructor(private config: SlackGatewayConfig) {\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.blobStore = new LocalFileBlobStore(\n\t\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t\t);\n\t\tthis.sink = new SlackSink(this.webClient, this.blobStore, {\n\t\t\tposted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),\n\t\t\tupdated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),\n\t\t});\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.workerClient = await createNatsBeeClient(this.config.nats);\n\t\tthis.engine = new BeeGatewayEngine({\n\t\t\tsink: this.sink,\n\t\t\tworkerClient: this.workerClient,\n\t\t\tlogger: {\n\t\t\t\tinfo: log.logInfo,\n\t\t\t\twarn: log.logWarning,\n\t\t\t\terror: log.logError,\n\t\t\t},\n\t\t});\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tthis.teamId = auth.team_id as string;\n\t\tthis.teamName = (auth.team as string | undefined) || null;\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tthis.startHandoffServer();\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\t\tlog.logInfo(\n\t\t\t`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`,\n\t\t);\n\t}\n\n\tprivate startHandoffServer(): void {\n\t\tconst handoffConfig = this.config.handoff;\n\t\tif (!handoffConfig?.enabled) return;\n\t\tif (!this.teamId || !this.engine) throw new Error(\"Gateway must be initialized before handoff server startup\");\n\t\tconst controller = new SlackHandoffController(handoffConfig, {\n\t\t\tteamId: this.teamId,\n\t\t\tpostRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),\n\t\t\tgetPermalink: async (channelId, messageTs) => {\n\t\t\t\tconst result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });\n\t\t\t\tif (!result.permalink) throw new Error(\"Slack did not return a permalink\");\n\t\t\t\treturn result.permalink;\n\t\t\t},\n\t\t\tdispatch: (input) => this.engine!.dispatch(input),\n\t\t\tgetReplies: async (channelId, threadTs) => {\n\t\t\t\tif (this.slackThreadHistoryUnavailable) return this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.webClient.conversations.replies({\n\t\t\t\t\t\tchannel: channelId,\n\t\t\t\t\t\tts: threadTs,\n\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t});\n\t\t\t\t\treturn (result.messages || []).map((message) => {\n\t\t\t\t\t\tconst typed = message as typeof message & {\n\t\t\t\t\t\t\tbot_id?: string;\n\t\t\t\t\t\t\tbot_profile?: { name?: string };\n\t\t\t\t\t\t\tusername?: string;\n\t\t\t\t\t\t\tthread_ts?: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tts: String(typed.ts || \"\"),\n\t\t\t\t\t\t\tthreadTs: typed.thread_ts,\n\t\t\t\t\t\t\ttext: typed.text || \"\",\n\t\t\t\t\t\t\tauthor: typed.user\n\t\t\t\t\t\t\t\t? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user\n\t\t\t\t\t\t\t\t: typed.bot_profile?.name || typed.username || \"Bot\",\n\t\t\t\t\t\t\tisBot: Boolean(typed.bot_id),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst errorCode = slackErrorCode(error);\n\t\t\t\t\tif (!isSlackHistoryPermissionError(errorCode)) throw error;\n\t\t\t\t\tthis.slackThreadHistoryUnavailable = true;\n\t\t\t\t\tlog.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);\n\t\t\t\t\treturn this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst server = createHandoffServer(controller);\n\t\tconst host = handoffConfig.host || \"0.0.0.0\";\n\t\tconst port = handoffConfig.port || 8080;\n\t\tserver.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));\n\t}\n\n\tprivate botDisplayName(): string {\n\t\tif (!this.botUserId) return \"Bee\";\n\t\tconst bot = this.users.get(this.botUserId);\n\t\treturn bot?.displayName || bot?.userName || \"Bee\";\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.channel.startsWith(\"D\")) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) return;\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") return;\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) return;\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);\n\t\t\tif (!isDM && isBotMention) return;\n\t\t\tif (!isDM) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"dm\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async enqueueInbound(message: SlackInboundMessage): Promise<void> {\n\t\tif (!this.botUserId || !this.teamId) {\n\t\t\tthrow new Error(\"Gateway has not been initialized\");\n\t\t}\n\t\tif (!this.engine) {\n\t\t\tthrow new Error(\"Gateway engine has not been initialized\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst resolved = resolveRoute(this.config.routes, message, {\n\t\t\t\tbotUserId: this.botUserId,\n\t\t\t\tteamId: this.teamId,\n\t\t\t\tteamName: this.teamName || undefined,\n\t\t\t});\n\t\t\tif (!resolved) {\n\t\t\t\tlog.logWarning(`No route for channel ${message.channelId}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outputTarget = this.buildOutputTarget(message, resolved);\n\t\t\tif (message.text.trim().toLowerCase() === \"stop\") {\n\t\t\t\tconst stopped = await this.engine.stopActiveRun(resolved.sessionId);\n\t\t\t\tawait this.sink.postMessage(outputTarget, stopped ? \"Stopping active run.\" : \"Nothing running.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst attachments = await this.downloadAttachments(resolved, message);\n\t\t\tconst input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);\n\t\t\tthis.engine.dispatch(input);\n\t\t} catch (error) {\n\t\t\tconst messageText = error instanceof Error ? error.message : String(error);\n\t\t\tlog.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);\n\t\t\tawait this.sink.postMessage(\n\t\t\t\t{\n\t\t\t\t\tchannelId: message.channelId,\n\t\t\t\t\tthreadId: message.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t\t\t},\n\t\t\t\t`_Gateway error: ${messageText}_`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate buildResolvedTurn(\n\t\tmessage: SlackInboundMessage,\n\t\tresolved: ResolvedSlackRoute,\n\t\tattachments: AttachmentRef[],\n\t\toutput: TransportOutputTarget,\n\t): BeeResolvedTurn {\n\t\tif (!this.teamId || !this.botUserId) {\n\t\t\tthrow new Error(\"Missing Slack gateway identity\");\n\t\t}\n\n\t\treturn {\n\t\t\tsessionId: resolved.sessionId,\n\t\t\tthreadId: resolved.threadTs,\n\t\t\tworker: resolved.route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId: resolved.conversationId,\n\t\t\t},\n\t\t\tactor: {\n\t\t\t\tuserId: message.userId,\n\t\t\t\tuserName: message.userName,\n\t\t\t\tdisplayName: message.displayName,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttext: message.text,\n\t\t\t},\n\t\t\tattachments,\n\t\t\toutput,\n\t\t};\n\t}\n\n\tprivate buildOutputTarget(message: SlackInboundMessage, resolved: ResolvedSlackRoute): TransportOutputTarget {\n\t\treturn {\n\t\t\tchannelId: message.channelId,\n\t\t\tthreadId: resolved.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t};\n\t}\n\n\tprivate async downloadAttachments(\n\t\tresolved: ResolvedSlackRoute,\n\t\tmessage: SlackInboundMessage,\n\t): Promise<AttachmentRef[]> {\n\t\tconst files = message.files || [];\n\t\tif (files.length === 0) return [];\n\n\t\tconst attachments: AttachmentRef[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url || !file.name) continue;\n\t\t\tconst response = await fetch(url, {\n\t\t\t\theaders: { authorization: `Bearer ${this.config.botToken}` },\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);\n\t\t\t}\n\t\t\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\t\t\tattachments.push(\n\t\t\t\tawait this.blobStore.put({\n\t\t\t\t\tnamespace: `incoming/slack/${resolved.sessionId}`,\n\t\t\t\t\tname: file.name,\n\t\t\t\t\ttitle: file.name,\n\t\t\t\t\tmimeType: file.mimetype,\n\t\t\t\t\tdata: bytes,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn attachments;\n\t}\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const user of members) {\n\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\tthis.users.set(user.id, {\n\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel,im\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; user?: string }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\tif (!channel.id) continue;\n\t\t\t\t\tif (channel.name) {\n\t\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name });\n\t\t\t\t\t} else if (channel.user && this.users.has(channel.user)) {\n\t\t\t\t\t\tthis.channels.set(channel.id, {\n\t\t\t\t\t\t\tid: channel.id,\n\t\t\t\t\t\t\tname: `DM:${this.users.get(channel.user)!.userName}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n\nfunction slackErrorCode(error: unknown): string | undefined {\n\tif (!error || typeof error !== \"object\" || !(\"data\" in error)) return undefined;\n\tconst data = error.data;\n\tif (!data || typeof data !== \"object\" || !(\"error\" in data)) return undefined;\n\treturn typeof data.error === \"string\" ? data.error : undefined;\n}\n\nfunction isSlackHistoryPermissionError(errorCode: string | undefined): boolean {\n\treturn errorCode === \"missing_scope\" || errorCode === \"no_permission\" || errorCode === \"not_allowed_token_type\";\n}\n\nexport async function startGatewayFromEnv(configPath?: string): Promise<void> {\n\tconst config = loadConfig(configPath);\n\tconst gateway = new SlackGateway(config);\n\tawait gateway.start();\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TransportOutputTarget } from "@jobmatchme/bee-gate";
|
|
2
|
+
import type { SlackHandoffReply } from "./types.js";
|
|
3
|
+
export declare class HandoffReplyCache {
|
|
4
|
+
private repliesByThread;
|
|
5
|
+
recordPosted(target: TransportOutputTarget, ts: string, text: string, author?: string): void;
|
|
6
|
+
recordUpdated(target: TransportOutputTarget, ts: string, text: string, author?: string): void;
|
|
7
|
+
replies(channelId: string, threadTs: string): SlackHandoffReply[];
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=handoff-reply-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-reply-cache.d.ts","sourceRoot":"","sources":["../src/handoff-reply-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAIpD,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,eAAe,CAA0C;IAE1D,YAAY,CAAC,MAAM,EAAE,qBAAqB,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAQ,GAAG,IAAI,CAMjG;IAEM,aAAa,CAAC,MAAM,EAAE,qBAAqB,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAQ,GAAG,IAAI,CAWlG;IAEM,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAEvE;CACD","sourcesContent":["import type { TransportOutputTarget } from \"@jobmatchme/bee-gate\";\nimport type { SlackHandoffReply } from \"./types.js\";\n\nconst MAX_REPLIES_PER_THREAD = 100;\n\nexport class HandoffReplyCache {\n\tprivate repliesByThread = new Map<string, SlackHandoffReply[]>();\n\n\tpublic recordPosted(target: TransportOutputTarget, ts: string, text: string, author = \"Bee\"): void {\n\t\tif (!target.channelId || !target.threadId) return;\n\t\tconst key = threadKey(target.channelId, target.threadId);\n\t\tconst replies = this.repliesByThread.get(key) || [];\n\t\treplies.push({ ts, threadTs: target.threadId, text, author, isBot: true });\n\t\tthis.repliesByThread.set(key, replies.slice(-MAX_REPLIES_PER_THREAD));\n\t}\n\n\tpublic recordUpdated(target: TransportOutputTarget, ts: string, text: string, author = \"Bee\"): void {\n\t\tif (!target.channelId || !target.threadId) return;\n\t\tconst key = threadKey(target.channelId, target.threadId);\n\t\tconst replies = this.repliesByThread.get(key) || [];\n\t\tconst existing = replies.find((reply) => reply.ts === ts);\n\t\tif (existing) {\n\t\t\texisting.text = text;\n\t\t\texisting.author = author;\n\t\t\treturn;\n\t\t}\n\t\tthis.recordPosted(target, ts, text, author);\n\t}\n\n\tpublic replies(channelId: string, threadTs: string): SlackHandoffReply[] {\n\t\treturn (this.repliesByThread.get(threadKey(channelId, threadTs)) || []).map((reply) => ({ ...reply }));\n\t}\n}\n\nfunction threadKey(channelId: string, threadTs: string): string {\n\treturn `${channelId}:${threadTs}`;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const MAX_REPLIES_PER_THREAD = 100;
|
|
2
|
+
export class HandoffReplyCache {
|
|
3
|
+
repliesByThread = new Map();
|
|
4
|
+
recordPosted(target, ts, text, author = "Bee") {
|
|
5
|
+
if (!target.channelId || !target.threadId)
|
|
6
|
+
return;
|
|
7
|
+
const key = threadKey(target.channelId, target.threadId);
|
|
8
|
+
const replies = this.repliesByThread.get(key) || [];
|
|
9
|
+
replies.push({ ts, threadTs: target.threadId, text, author, isBot: true });
|
|
10
|
+
this.repliesByThread.set(key, replies.slice(-MAX_REPLIES_PER_THREAD));
|
|
11
|
+
}
|
|
12
|
+
recordUpdated(target, ts, text, author = "Bee") {
|
|
13
|
+
if (!target.channelId || !target.threadId)
|
|
14
|
+
return;
|
|
15
|
+
const key = threadKey(target.channelId, target.threadId);
|
|
16
|
+
const replies = this.repliesByThread.get(key) || [];
|
|
17
|
+
const existing = replies.find((reply) => reply.ts === ts);
|
|
18
|
+
if (existing) {
|
|
19
|
+
existing.text = text;
|
|
20
|
+
existing.author = author;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.recordPosted(target, ts, text, author);
|
|
24
|
+
}
|
|
25
|
+
replies(channelId, threadTs) {
|
|
26
|
+
return (this.repliesByThread.get(threadKey(channelId, threadTs)) || []).map((reply) => ({ ...reply }));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function threadKey(channelId, threadTs) {
|
|
30
|
+
return `${channelId}:${threadTs}`;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=handoff-reply-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-reply-cache.js","sourceRoot":"","sources":["../src/handoff-reply-cache.ts"],"names":[],"mappings":"AAGA,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,OAAO,iBAAiB;IACrB,eAAe,GAAG,IAAI,GAAG,EAA+B,CAAC;IAE1D,YAAY,CAAC,MAA6B,EAAE,EAAU,EAAE,IAAY,EAAE,MAAM,GAAG,KAAK,EAAQ;QAClG,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO;QAClD,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3E,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC;IAAA,CACtE;IAEM,aAAa,CAAC,MAA6B,EAAE,EAAU,EAAE,IAAY,EAAE,MAAM,GAAG,KAAK,EAAQ;QACnG,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO;QAClD,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1D,IAAI,QAAQ,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;YACrB,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;YACzB,OAAO;QACR,CAAC;QACD,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAAA,CAC5C;IAEM,OAAO,CAAC,SAAiB,EAAE,QAAgB,EAAuB;QACxE,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;IAAA,CACvG;CACD;AAED,SAAS,SAAS,CAAC,SAAiB,EAAE,QAAgB,EAAU;IAC/D,OAAO,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC;AAAA,CAClC","sourcesContent":["import type { TransportOutputTarget } from \"@jobmatchme/bee-gate\";\nimport type { SlackHandoffReply } from \"./types.js\";\n\nconst MAX_REPLIES_PER_THREAD = 100;\n\nexport class HandoffReplyCache {\n\tprivate repliesByThread = new Map<string, SlackHandoffReply[]>();\n\n\tpublic recordPosted(target: TransportOutputTarget, ts: string, text: string, author = \"Bee\"): void {\n\t\tif (!target.channelId || !target.threadId) return;\n\t\tconst key = threadKey(target.channelId, target.threadId);\n\t\tconst replies = this.repliesByThread.get(key) || [];\n\t\treplies.push({ ts, threadTs: target.threadId, text, author, isBot: true });\n\t\tthis.repliesByThread.set(key, replies.slice(-MAX_REPLIES_PER_THREAD));\n\t}\n\n\tpublic recordUpdated(target: TransportOutputTarget, ts: string, text: string, author = \"Bee\"): void {\n\t\tif (!target.channelId || !target.threadId) return;\n\t\tconst key = threadKey(target.channelId, target.threadId);\n\t\tconst replies = this.repliesByThread.get(key) || [];\n\t\tconst existing = replies.find((reply) => reply.ts === ts);\n\t\tif (existing) {\n\t\t\texisting.text = text;\n\t\t\texisting.author = author;\n\t\t\treturn;\n\t\t}\n\t\tthis.recordPosted(target, ts, text, author);\n\t}\n\n\tpublic replies(channelId: string, threadTs: string): SlackHandoffReply[] {\n\t\treturn (this.repliesByThread.get(threadKey(channelId, threadTs)) || []).map((reply) => ({ ...reply }));\n\t}\n}\n\nfunction threadKey(channelId: string, threadTs: string): string {\n\treturn `${channelId}:${threadTs}`;\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type BeeResolvedTurn } from "@jobmatchme/bee-gate";
|
|
2
|
+
import { type Server } from "http";
|
|
3
|
+
import type { SlackHandoffConfig, SlackHandoffRecord, SlackHandoffReply, SlackHandoffRequest } from "./types.js";
|
|
4
|
+
export interface SlackHandoffDependencies {
|
|
5
|
+
teamId: string;
|
|
6
|
+
postRootMessage(channelId: string, text: string): Promise<string>;
|
|
7
|
+
getPermalink(channelId: string, messageTs: string): Promise<string>;
|
|
8
|
+
dispatch(input: BeeResolvedTurn): void;
|
|
9
|
+
getReplies(channelId: string, threadTs: string): Promise<SlackHandoffReply[]>;
|
|
10
|
+
}
|
|
11
|
+
export declare class SlackHandoffController {
|
|
12
|
+
private config;
|
|
13
|
+
private dependencies;
|
|
14
|
+
private routes;
|
|
15
|
+
constructor(config: SlackHandoffConfig, dependencies: SlackHandoffDependencies);
|
|
16
|
+
publicRoutes(): Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
}>;
|
|
20
|
+
create(request: SlackHandoffRequest): Promise<SlackHandoffRecord>;
|
|
21
|
+
replies(routeId: string, threadTs: string): Promise<SlackHandoffReply[]>;
|
|
22
|
+
private requireRoute;
|
|
23
|
+
}
|
|
24
|
+
export declare function createHandoffServer(controller: SlackHandoffController): Server;
|
|
25
|
+
export declare function withTrustedGrafanaActor(request: SlackHandoffRequest, trustedUserHeader: string | string[] | undefined): SlackHandoffRequest;
|
|
26
|
+
export declare function renderSlackHandoffMessage(request: SlackHandoffRequest): string;
|
|
27
|
+
export declare function renderAgentRequest(request: SlackHandoffRequest): string;
|
|
28
|
+
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,eAAe,EAIpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,MAAM,CAAC;AAC5F,OAAO,KAAK,EACX,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EAEnB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,wBAAwB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CAC9E;AAED,qBAAa,sBAAsB;IAIjC,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IAJrB,OAAO,CAAC,MAAM,CAA8C;IAE5D,YACS,MAAM,EAAE,kBAAkB,EAC1B,YAAY,EAAE,wBAAwB,EAG9C;IAEM,YAAY,IAAI,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAE1D;IAEY,MAAM,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+B7E;IAEY,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAIpF;IAED,OAAO,CAAC,YAAY;CAKpB;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAU9E;AAiCD,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,mBAAmB,EAC5B,iBAAiB,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAC9C,mBAAmB,CAQrB;AAmCD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAqB9E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAgBvE","sourcesContent":["import {\n\ttype BeeResolvedTurn,\n\tbuildConversationId,\n\tbuildSessionKey,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"http\";\nimport type {\n\tSlackHandoffConfig,\n\tSlackHandoffRecord,\n\tSlackHandoffReply,\n\tSlackHandoffRequest,\n\tSlackHandoffRouteConfig,\n} from \"./types.js\";\n\nconst MAX_BODY_BYTES = 32 * 1024;\nconst MAX_QUESTION_LENGTH = 4000;\nconst THREAD_TS_PATTERN = /^\\d{10,}\\.\\d{6}$/;\n\nexport interface SlackHandoffDependencies {\n\tteamId: string;\n\tpostRootMessage(channelId: string, text: string): Promise<string>;\n\tgetPermalink(channelId: string, messageTs: string): Promise<string>;\n\tdispatch(input: BeeResolvedTurn): void;\n\tgetReplies(channelId: string, threadTs: string): Promise<SlackHandoffReply[]>;\n}\n\nexport class SlackHandoffController {\n\tprivate routes = new Map<string, SlackHandoffRouteConfig>();\n\n\tconstructor(\n\t\tprivate config: SlackHandoffConfig,\n\t\tprivate dependencies: SlackHandoffDependencies,\n\t) {\n\t\tfor (const route of config.routes) this.routes.set(route.id, route);\n\t}\n\n\tpublic publicRoutes(): Array<{ id: string; label: string }> {\n\t\treturn this.config.routes.map((route) => ({ id: route.id, label: route.label || route.id }));\n\t}\n\n\tpublic async create(request: SlackHandoffRequest): Promise<SlackHandoffRecord> {\n\t\tconst route = this.requireRoute(request.routeId);\n\t\tvalidateRequest(request, this.config.allowedDashboardHosts);\n\n\t\tconst threadTs = await this.dependencies.postRootMessage(route.channelId, renderSlackHandoffMessage(request));\n\t\tconst permalink = await this.dependencies.getPermalink(route.channelId, threadTs);\n\t\tconst conversationId = buildConversationId([\"slack\", this.dependencies.teamId, route.channelId, threadTs]);\n\t\tconst sessionBase = route.session?.strategy === \"channel\" ? route.channelId : threadTs;\n\t\tconst sessionId = buildSessionKey(\n\t\t\troute.session?.prefix || route.id,\n\t\t\tbuildConversationId([\"slack\", this.dependencies.teamId, route.channelId, sessionBase]),\n\t\t);\n\t\tconst output: TransportOutputTarget = { channelId: route.channelId, threadId: threadTs };\n\t\tthis.dependencies.dispatch({\n\t\t\tsessionId,\n\t\t\tthreadId: threadTs,\n\t\t\tworker: route.worker,\n\t\t\tconversation: { transport: \"slack\", conversationId },\n\t\t\tactor: request.actor,\n\t\t\tmessage: { text: renderAgentRequest(request) },\n\t\t\tattachments: [],\n\t\t\toutput,\n\t\t});\n\n\t\treturn {\n\t\t\trouteId: route.id,\n\t\t\tchannelId: route.channelId,\n\t\t\tthreadTs,\n\t\t\tpermalink,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t};\n\t}\n\n\tpublic async replies(routeId: string, threadTs: string): Promise<SlackHandoffReply[]> {\n\t\tconst route = this.requireRoute(routeId);\n\t\tif (!THREAD_TS_PATTERN.test(threadTs)) throw new HandoffHttpError(400, \"Invalid thread timestamp\");\n\t\treturn this.dependencies.getReplies(route.channelId, threadTs);\n\t}\n\n\tprivate requireRoute(routeId: string): SlackHandoffRouteConfig {\n\t\tconst route = this.routes.get(routeId);\n\t\tif (!route) throw new HandoffHttpError(404, \"Unknown handoff route\");\n\t\treturn route;\n\t}\n}\n\nexport function createHandoffServer(controller: SlackHandoffController): Server {\n\treturn createServer(async (request, response) => {\n\t\ttry {\n\t\t\tawait handleRequest(controller, request, response);\n\t\t} catch (error) {\n\t\t\tconst status = error instanceof HandoffHttpError ? error.status : 500;\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tsendJson(response, status, { error: status === 500 ? \"Internal handoff error\" : message });\n\t\t}\n\t});\n}\n\nasync function handleRequest(\n\tcontroller: SlackHandoffController,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\tconst method = request.method || \"GET\";\n\tconst url = new URL(request.url || \"/\", \"http://bee-slack.internal\");\n\tif (method === \"GET\" && url.pathname === \"/health\") {\n\t\tsendJson(response, 200, { ok: true, service: \"bee-slack-handoff\" });\n\t\treturn;\n\t}\n\tif (method === \"GET\" && url.pathname === \"/api/handoffs/routes\") {\n\t\tsendJson(response, 200, { routes: controller.publicRoutes() });\n\t\treturn;\n\t}\n\tif (method === \"POST\" && url.pathname === \"/api/handoffs\") {\n\t\tconst body = withTrustedGrafanaActor(\n\t\t\tawait readJsonBody<SlackHandoffRequest>(request),\n\t\t\trequest.headers[\"x-grafana-user\"],\n\t\t);\n\t\tsendJson(response, 201, { handoff: await controller.create(body) });\n\t\treturn;\n\t}\n\tconst match = url.pathname.match(/^\\/api\\/handoffs\\/([^/]+)\\/([^/]+)\\/replies$/);\n\tif (method === \"GET\" && match) {\n\t\tsendJson(response, 200, { replies: await controller.replies(decodeURIComponent(match[1]), match[2]) });\n\t\treturn;\n\t}\n\tthrow new HandoffHttpError(404, \"Not found\");\n}\n\nexport function withTrustedGrafanaActor(\n\trequest: SlackHandoffRequest,\n\ttrustedUserHeader: string | string[] | undefined,\n): SlackHandoffRequest {\n\tif (typeof trustedUserHeader !== \"string\" || !trustedUserHeader.trim()) {\n\t\tthrow new HandoffHttpError(401, \"Authenticated Grafana user header is required\");\n\t}\n\treturn {\n\t\t...request,\n\t\tactor: { userId: trustedUserHeader, userName: trustedUserHeader },\n\t};\n}\n\nasync function readJsonBody<T>(request: IncomingMessage): Promise<T> {\n\tlet body = \"\";\n\tfor await (const chunk of request) {\n\t\tbody += chunk;\n\t\tif (Buffer.byteLength(body) > MAX_BODY_BYTES) throw new HandoffHttpError(413, \"Request body too large\");\n\t}\n\ttry {\n\t\treturn JSON.parse(body) as T;\n\t} catch {\n\t\tthrow new HandoffHttpError(400, \"Invalid JSON body\");\n\t}\n}\n\nfunction validateRequest(request: SlackHandoffRequest, allowedHosts?: string[]): void {\n\tif (!request || typeof request !== \"object\") throw new HandoffHttpError(400, \"Request body is required\");\n\tif (!request.text?.trim()) throw new HandoffHttpError(400, \"Question is required\");\n\tif (request.text.length > MAX_QUESTION_LENGTH) throw new HandoffHttpError(400, \"Question is too long\");\n\tif (!request.actor?.userId?.trim()) throw new HandoffHttpError(400, \"Actor userId is required\");\n\tif (!request.context?.url?.trim()) throw new HandoffHttpError(400, \"Grafana context URL is required\");\n\tlet contextUrl: URL;\n\ttry {\n\t\tcontextUrl = new URL(request.context.url);\n\t} catch {\n\t\tthrow new HandoffHttpError(400, \"Grafana context URL is invalid\");\n\t}\n\tif (contextUrl.protocol !== \"https:\" && contextUrl.hostname !== \"localhost\") {\n\t\tthrow new HandoffHttpError(400, \"Grafana context URL must use HTTPS\");\n\t}\n\tif (allowedHosts?.length && !allowedHosts.includes(contextUrl.hostname)) {\n\t\tthrow new HandoffHttpError(400, \"Grafana context host is not allowed\");\n\t}\n}\n\nexport function renderSlackHandoffMessage(request: SlackHandoffRequest): string {\n\tconst actor = request.actor.displayName || request.actor.userName || request.actor.userId;\n\tconst context = request.context;\n\tconst title = context.panelTitle || context.dashboardTitle || \"Grafana\";\n\tconst details = [\n\t\tcontext.dashboardTitle ? `*Dashboard:* ${escapeSlack(context.dashboardTitle)}` : undefined,\n\t\tcontext.panelTitle ? `*Panel:* ${escapeSlack(context.panelTitle)}` : undefined,\n\t\tcontext.timeRange ? `*Zeitraum:* ${escapeSlack(context.timeRange)}` : undefined,\n\t\t...Object.entries(context.variables || {}).map(\n\t\t\t([name, value]) => `*${escapeSlack(name)}:* ${escapeSlack(value)}`,\n\t\t),\n\t].filter((value): value is string => Boolean(value));\n\treturn [\n\t\t`:honeybee: *Frage aus Grafana · ${escapeSlack(title)}*`,\n\t\t`*Von:* ${escapeSlack(actor)}`,\n\t\t...details,\n\t\t\"\",\n\t\tescapeSlack(request.text.trim()),\n\t\t\"\",\n\t\t`<${escapeSlack(context.url)}|Dashboard-Kontext öffnen>`,\n\t].join(\"\\n\");\n}\n\nexport function renderAgentRequest(request: SlackHandoffRequest): string {\n\tconst variables = Object.entries(request.context.variables || {})\n\t\t.map(([name, value]) => `- ${name}: ${value}`)\n\t\t.join(\"\\n\");\n\treturn [\n\t\trequest.text.trim(),\n\t\t\"\",\n\t\t\"Grafana-Kontext:\",\n\t\trequest.context.dashboardTitle ? `- Dashboard: ${request.context.dashboardTitle}` : undefined,\n\t\trequest.context.panelTitle ? `- Panel: ${request.context.panelTitle}` : undefined,\n\t\trequest.context.timeRange ? `- Zeitraum: ${request.context.timeRange}` : undefined,\n\t\tvariables || undefined,\n\t\t`- URL: ${request.context.url}`,\n\t]\n\t\t.filter((value): value is string => Boolean(value))\n\t\t.join(\"\\n\");\n}\n\nfunction escapeSlack(value: string): string {\n\treturn value.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\nfunction sendJson(response: ServerResponse, status: number, body: unknown): void {\n\tresponse.writeHead(status, { \"content-type\": \"application/json; charset=utf-8\", \"cache-control\": \"no-store\" });\n\tresponse.end(JSON.stringify(body));\n}\n\nclass HandoffHttpError extends Error {\n\tconstructor(\n\t\tpublic status: number,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t}\n}\n"]}
|
package/dist/handoff.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { buildConversationId, buildSessionKey, } from "@jobmatchme/bee-gate";
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
const MAX_BODY_BYTES = 32 * 1024;
|
|
4
|
+
const MAX_QUESTION_LENGTH = 4000;
|
|
5
|
+
const THREAD_TS_PATTERN = /^\d{10,}\.\d{6}$/;
|
|
6
|
+
export class SlackHandoffController {
|
|
7
|
+
config;
|
|
8
|
+
dependencies;
|
|
9
|
+
routes = new Map();
|
|
10
|
+
constructor(config, dependencies) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.dependencies = dependencies;
|
|
13
|
+
for (const route of config.routes)
|
|
14
|
+
this.routes.set(route.id, route);
|
|
15
|
+
}
|
|
16
|
+
publicRoutes() {
|
|
17
|
+
return this.config.routes.map((route) => ({ id: route.id, label: route.label || route.id }));
|
|
18
|
+
}
|
|
19
|
+
async create(request) {
|
|
20
|
+
const route = this.requireRoute(request.routeId);
|
|
21
|
+
validateRequest(request, this.config.allowedDashboardHosts);
|
|
22
|
+
const threadTs = await this.dependencies.postRootMessage(route.channelId, renderSlackHandoffMessage(request));
|
|
23
|
+
const permalink = await this.dependencies.getPermalink(route.channelId, threadTs);
|
|
24
|
+
const conversationId = buildConversationId(["slack", this.dependencies.teamId, route.channelId, threadTs]);
|
|
25
|
+
const sessionBase = route.session?.strategy === "channel" ? route.channelId : threadTs;
|
|
26
|
+
const sessionId = buildSessionKey(route.session?.prefix || route.id, buildConversationId(["slack", this.dependencies.teamId, route.channelId, sessionBase]));
|
|
27
|
+
const output = { channelId: route.channelId, threadId: threadTs };
|
|
28
|
+
this.dependencies.dispatch({
|
|
29
|
+
sessionId,
|
|
30
|
+
threadId: threadTs,
|
|
31
|
+
worker: route.worker,
|
|
32
|
+
conversation: { transport: "slack", conversationId },
|
|
33
|
+
actor: request.actor,
|
|
34
|
+
message: { text: renderAgentRequest(request) },
|
|
35
|
+
attachments: [],
|
|
36
|
+
output,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
routeId: route.id,
|
|
40
|
+
channelId: route.channelId,
|
|
41
|
+
threadTs,
|
|
42
|
+
permalink,
|
|
43
|
+
createdAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async replies(routeId, threadTs) {
|
|
47
|
+
const route = this.requireRoute(routeId);
|
|
48
|
+
if (!THREAD_TS_PATTERN.test(threadTs))
|
|
49
|
+
throw new HandoffHttpError(400, "Invalid thread timestamp");
|
|
50
|
+
return this.dependencies.getReplies(route.channelId, threadTs);
|
|
51
|
+
}
|
|
52
|
+
requireRoute(routeId) {
|
|
53
|
+
const route = this.routes.get(routeId);
|
|
54
|
+
if (!route)
|
|
55
|
+
throw new HandoffHttpError(404, "Unknown handoff route");
|
|
56
|
+
return route;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function createHandoffServer(controller) {
|
|
60
|
+
return createServer(async (request, response) => {
|
|
61
|
+
try {
|
|
62
|
+
await handleRequest(controller, request, response);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const status = error instanceof HandoffHttpError ? error.status : 500;
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
sendJson(response, status, { error: status === 500 ? "Internal handoff error" : message });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function handleRequest(controller, request, response) {
|
|
72
|
+
const method = request.method || "GET";
|
|
73
|
+
const url = new URL(request.url || "/", "http://bee-slack.internal");
|
|
74
|
+
if (method === "GET" && url.pathname === "/health") {
|
|
75
|
+
sendJson(response, 200, { ok: true, service: "bee-slack-handoff" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (method === "GET" && url.pathname === "/api/handoffs/routes") {
|
|
79
|
+
sendJson(response, 200, { routes: controller.publicRoutes() });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (method === "POST" && url.pathname === "/api/handoffs") {
|
|
83
|
+
const body = withTrustedGrafanaActor(await readJsonBody(request), request.headers["x-grafana-user"]);
|
|
84
|
+
sendJson(response, 201, { handoff: await controller.create(body) });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const match = url.pathname.match(/^\/api\/handoffs\/([^/]+)\/([^/]+)\/replies$/);
|
|
88
|
+
if (method === "GET" && match) {
|
|
89
|
+
sendJson(response, 200, { replies: await controller.replies(decodeURIComponent(match[1]), match[2]) });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw new HandoffHttpError(404, "Not found");
|
|
93
|
+
}
|
|
94
|
+
export function withTrustedGrafanaActor(request, trustedUserHeader) {
|
|
95
|
+
if (typeof trustedUserHeader !== "string" || !trustedUserHeader.trim()) {
|
|
96
|
+
throw new HandoffHttpError(401, "Authenticated Grafana user header is required");
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
...request,
|
|
100
|
+
actor: { userId: trustedUserHeader, userName: trustedUserHeader },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function readJsonBody(request) {
|
|
104
|
+
let body = "";
|
|
105
|
+
for await (const chunk of request) {
|
|
106
|
+
body += chunk;
|
|
107
|
+
if (Buffer.byteLength(body) > MAX_BODY_BYTES)
|
|
108
|
+
throw new HandoffHttpError(413, "Request body too large");
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(body);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new HandoffHttpError(400, "Invalid JSON body");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateRequest(request, allowedHosts) {
|
|
118
|
+
if (!request || typeof request !== "object")
|
|
119
|
+
throw new HandoffHttpError(400, "Request body is required");
|
|
120
|
+
if (!request.text?.trim())
|
|
121
|
+
throw new HandoffHttpError(400, "Question is required");
|
|
122
|
+
if (request.text.length > MAX_QUESTION_LENGTH)
|
|
123
|
+
throw new HandoffHttpError(400, "Question is too long");
|
|
124
|
+
if (!request.actor?.userId?.trim())
|
|
125
|
+
throw new HandoffHttpError(400, "Actor userId is required");
|
|
126
|
+
if (!request.context?.url?.trim())
|
|
127
|
+
throw new HandoffHttpError(400, "Grafana context URL is required");
|
|
128
|
+
let contextUrl;
|
|
129
|
+
try {
|
|
130
|
+
contextUrl = new URL(request.context.url);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new HandoffHttpError(400, "Grafana context URL is invalid");
|
|
134
|
+
}
|
|
135
|
+
if (contextUrl.protocol !== "https:" && contextUrl.hostname !== "localhost") {
|
|
136
|
+
throw new HandoffHttpError(400, "Grafana context URL must use HTTPS");
|
|
137
|
+
}
|
|
138
|
+
if (allowedHosts?.length && !allowedHosts.includes(contextUrl.hostname)) {
|
|
139
|
+
throw new HandoffHttpError(400, "Grafana context host is not allowed");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function renderSlackHandoffMessage(request) {
|
|
143
|
+
const actor = request.actor.displayName || request.actor.userName || request.actor.userId;
|
|
144
|
+
const context = request.context;
|
|
145
|
+
const title = context.panelTitle || context.dashboardTitle || "Grafana";
|
|
146
|
+
const details = [
|
|
147
|
+
context.dashboardTitle ? `*Dashboard:* ${escapeSlack(context.dashboardTitle)}` : undefined,
|
|
148
|
+
context.panelTitle ? `*Panel:* ${escapeSlack(context.panelTitle)}` : undefined,
|
|
149
|
+
context.timeRange ? `*Zeitraum:* ${escapeSlack(context.timeRange)}` : undefined,
|
|
150
|
+
...Object.entries(context.variables || {}).map(([name, value]) => `*${escapeSlack(name)}:* ${escapeSlack(value)}`),
|
|
151
|
+
].filter((value) => Boolean(value));
|
|
152
|
+
return [
|
|
153
|
+
`:honeybee: *Frage aus Grafana · ${escapeSlack(title)}*`,
|
|
154
|
+
`*Von:* ${escapeSlack(actor)}`,
|
|
155
|
+
...details,
|
|
156
|
+
"",
|
|
157
|
+
escapeSlack(request.text.trim()),
|
|
158
|
+
"",
|
|
159
|
+
`<${escapeSlack(context.url)}|Dashboard-Kontext öffnen>`,
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
export function renderAgentRequest(request) {
|
|
163
|
+
const variables = Object.entries(request.context.variables || {})
|
|
164
|
+
.map(([name, value]) => `- ${name}: ${value}`)
|
|
165
|
+
.join("\n");
|
|
166
|
+
return [
|
|
167
|
+
request.text.trim(),
|
|
168
|
+
"",
|
|
169
|
+
"Grafana-Kontext:",
|
|
170
|
+
request.context.dashboardTitle ? `- Dashboard: ${request.context.dashboardTitle}` : undefined,
|
|
171
|
+
request.context.panelTitle ? `- Panel: ${request.context.panelTitle}` : undefined,
|
|
172
|
+
request.context.timeRange ? `- Zeitraum: ${request.context.timeRange}` : undefined,
|
|
173
|
+
variables || undefined,
|
|
174
|
+
`- URL: ${request.context.url}`,
|
|
175
|
+
]
|
|
176
|
+
.filter((value) => Boolean(value))
|
|
177
|
+
.join("\n");
|
|
178
|
+
}
|
|
179
|
+
function escapeSlack(value) {
|
|
180
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
181
|
+
}
|
|
182
|
+
function sendJson(response, status, body) {
|
|
183
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
184
|
+
response.end(JSON.stringify(body));
|
|
185
|
+
}
|
|
186
|
+
class HandoffHttpError extends Error {
|
|
187
|
+
status;
|
|
188
|
+
constructor(status, message) {
|
|
189
|
+
super(message);
|
|
190
|
+
this.status = status;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=handoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.js","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype BeeResolvedTurn,\n\tbuildConversationId,\n\tbuildSessionKey,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"http\";\nimport type {\n\tSlackHandoffConfig,\n\tSlackHandoffRecord,\n\tSlackHandoffReply,\n\tSlackHandoffRequest,\n\tSlackHandoffRouteConfig,\n} from \"./types.js\";\n\nconst MAX_BODY_BYTES = 32 * 1024;\nconst MAX_QUESTION_LENGTH = 4000;\nconst THREAD_TS_PATTERN = /^\\d{10,}\\.\\d{6}$/;\n\nexport interface SlackHandoffDependencies {\n\tteamId: string;\n\tpostRootMessage(channelId: string, text: string): Promise<string>;\n\tgetPermalink(channelId: string, messageTs: string): Promise<string>;\n\tdispatch(input: BeeResolvedTurn): void;\n\tgetReplies(channelId: string, threadTs: string): Promise<SlackHandoffReply[]>;\n}\n\nexport class SlackHandoffController {\n\tprivate routes = new Map<string, SlackHandoffRouteConfig>();\n\n\tconstructor(\n\t\tprivate config: SlackHandoffConfig,\n\t\tprivate dependencies: SlackHandoffDependencies,\n\t) {\n\t\tfor (const route of config.routes) this.routes.set(route.id, route);\n\t}\n\n\tpublic publicRoutes(): Array<{ id: string; label: string }> {\n\t\treturn this.config.routes.map((route) => ({ id: route.id, label: route.label || route.id }));\n\t}\n\n\tpublic async create(request: SlackHandoffRequest): Promise<SlackHandoffRecord> {\n\t\tconst route = this.requireRoute(request.routeId);\n\t\tvalidateRequest(request, this.config.allowedDashboardHosts);\n\n\t\tconst threadTs = await this.dependencies.postRootMessage(route.channelId, renderSlackHandoffMessage(request));\n\t\tconst permalink = await this.dependencies.getPermalink(route.channelId, threadTs);\n\t\tconst conversationId = buildConversationId([\"slack\", this.dependencies.teamId, route.channelId, threadTs]);\n\t\tconst sessionBase = route.session?.strategy === \"channel\" ? route.channelId : threadTs;\n\t\tconst sessionId = buildSessionKey(\n\t\t\troute.session?.prefix || route.id,\n\t\t\tbuildConversationId([\"slack\", this.dependencies.teamId, route.channelId, sessionBase]),\n\t\t);\n\t\tconst output: TransportOutputTarget = { channelId: route.channelId, threadId: threadTs };\n\t\tthis.dependencies.dispatch({\n\t\t\tsessionId,\n\t\t\tthreadId: threadTs,\n\t\t\tworker: route.worker,\n\t\t\tconversation: { transport: \"slack\", conversationId },\n\t\t\tactor: request.actor,\n\t\t\tmessage: { text: renderAgentRequest(request) },\n\t\t\tattachments: [],\n\t\t\toutput,\n\t\t});\n\n\t\treturn {\n\t\t\trouteId: route.id,\n\t\t\tchannelId: route.channelId,\n\t\t\tthreadTs,\n\t\t\tpermalink,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t};\n\t}\n\n\tpublic async replies(routeId: string, threadTs: string): Promise<SlackHandoffReply[]> {\n\t\tconst route = this.requireRoute(routeId);\n\t\tif (!THREAD_TS_PATTERN.test(threadTs)) throw new HandoffHttpError(400, \"Invalid thread timestamp\");\n\t\treturn this.dependencies.getReplies(route.channelId, threadTs);\n\t}\n\n\tprivate requireRoute(routeId: string): SlackHandoffRouteConfig {\n\t\tconst route = this.routes.get(routeId);\n\t\tif (!route) throw new HandoffHttpError(404, \"Unknown handoff route\");\n\t\treturn route;\n\t}\n}\n\nexport function createHandoffServer(controller: SlackHandoffController): Server {\n\treturn createServer(async (request, response) => {\n\t\ttry {\n\t\t\tawait handleRequest(controller, request, response);\n\t\t} catch (error) {\n\t\t\tconst status = error instanceof HandoffHttpError ? error.status : 500;\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tsendJson(response, status, { error: status === 500 ? \"Internal handoff error\" : message });\n\t\t}\n\t});\n}\n\nasync function handleRequest(\n\tcontroller: SlackHandoffController,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\tconst method = request.method || \"GET\";\n\tconst url = new URL(request.url || \"/\", \"http://bee-slack.internal\");\n\tif (method === \"GET\" && url.pathname === \"/health\") {\n\t\tsendJson(response, 200, { ok: true, service: \"bee-slack-handoff\" });\n\t\treturn;\n\t}\n\tif (method === \"GET\" && url.pathname === \"/api/handoffs/routes\") {\n\t\tsendJson(response, 200, { routes: controller.publicRoutes() });\n\t\treturn;\n\t}\n\tif (method === \"POST\" && url.pathname === \"/api/handoffs\") {\n\t\tconst body = withTrustedGrafanaActor(\n\t\t\tawait readJsonBody<SlackHandoffRequest>(request),\n\t\t\trequest.headers[\"x-grafana-user\"],\n\t\t);\n\t\tsendJson(response, 201, { handoff: await controller.create(body) });\n\t\treturn;\n\t}\n\tconst match = url.pathname.match(/^\\/api\\/handoffs\\/([^/]+)\\/([^/]+)\\/replies$/);\n\tif (method === \"GET\" && match) {\n\t\tsendJson(response, 200, { replies: await controller.replies(decodeURIComponent(match[1]), match[2]) });\n\t\treturn;\n\t}\n\tthrow new HandoffHttpError(404, \"Not found\");\n}\n\nexport function withTrustedGrafanaActor(\n\trequest: SlackHandoffRequest,\n\ttrustedUserHeader: string | string[] | undefined,\n): SlackHandoffRequest {\n\tif (typeof trustedUserHeader !== \"string\" || !trustedUserHeader.trim()) {\n\t\tthrow new HandoffHttpError(401, \"Authenticated Grafana user header is required\");\n\t}\n\treturn {\n\t\t...request,\n\t\tactor: { userId: trustedUserHeader, userName: trustedUserHeader },\n\t};\n}\n\nasync function readJsonBody<T>(request: IncomingMessage): Promise<T> {\n\tlet body = \"\";\n\tfor await (const chunk of request) {\n\t\tbody += chunk;\n\t\tif (Buffer.byteLength(body) > MAX_BODY_BYTES) throw new HandoffHttpError(413, \"Request body too large\");\n\t}\n\ttry {\n\t\treturn JSON.parse(body) as T;\n\t} catch {\n\t\tthrow new HandoffHttpError(400, \"Invalid JSON body\");\n\t}\n}\n\nfunction validateRequest(request: SlackHandoffRequest, allowedHosts?: string[]): void {\n\tif (!request || typeof request !== \"object\") throw new HandoffHttpError(400, \"Request body is required\");\n\tif (!request.text?.trim()) throw new HandoffHttpError(400, \"Question is required\");\n\tif (request.text.length > MAX_QUESTION_LENGTH) throw new HandoffHttpError(400, \"Question is too long\");\n\tif (!request.actor?.userId?.trim()) throw new HandoffHttpError(400, \"Actor userId is required\");\n\tif (!request.context?.url?.trim()) throw new HandoffHttpError(400, \"Grafana context URL is required\");\n\tlet contextUrl: URL;\n\ttry {\n\t\tcontextUrl = new URL(request.context.url);\n\t} catch {\n\t\tthrow new HandoffHttpError(400, \"Grafana context URL is invalid\");\n\t}\n\tif (contextUrl.protocol !== \"https:\" && contextUrl.hostname !== \"localhost\") {\n\t\tthrow new HandoffHttpError(400, \"Grafana context URL must use HTTPS\");\n\t}\n\tif (allowedHosts?.length && !allowedHosts.includes(contextUrl.hostname)) {\n\t\tthrow new HandoffHttpError(400, \"Grafana context host is not allowed\");\n\t}\n}\n\nexport function renderSlackHandoffMessage(request: SlackHandoffRequest): string {\n\tconst actor = request.actor.displayName || request.actor.userName || request.actor.userId;\n\tconst context = request.context;\n\tconst title = context.panelTitle || context.dashboardTitle || \"Grafana\";\n\tconst details = [\n\t\tcontext.dashboardTitle ? `*Dashboard:* ${escapeSlack(context.dashboardTitle)}` : undefined,\n\t\tcontext.panelTitle ? `*Panel:* ${escapeSlack(context.panelTitle)}` : undefined,\n\t\tcontext.timeRange ? `*Zeitraum:* ${escapeSlack(context.timeRange)}` : undefined,\n\t\t...Object.entries(context.variables || {}).map(\n\t\t\t([name, value]) => `*${escapeSlack(name)}:* ${escapeSlack(value)}`,\n\t\t),\n\t].filter((value): value is string => Boolean(value));\n\treturn [\n\t\t`:honeybee: *Frage aus Grafana · ${escapeSlack(title)}*`,\n\t\t`*Von:* ${escapeSlack(actor)}`,\n\t\t...details,\n\t\t\"\",\n\t\tescapeSlack(request.text.trim()),\n\t\t\"\",\n\t\t`<${escapeSlack(context.url)}|Dashboard-Kontext öffnen>`,\n\t].join(\"\\n\");\n}\n\nexport function renderAgentRequest(request: SlackHandoffRequest): string {\n\tconst variables = Object.entries(request.context.variables || {})\n\t\t.map(([name, value]) => `- ${name}: ${value}`)\n\t\t.join(\"\\n\");\n\treturn [\n\t\trequest.text.trim(),\n\t\t\"\",\n\t\t\"Grafana-Kontext:\",\n\t\trequest.context.dashboardTitle ? `- Dashboard: ${request.context.dashboardTitle}` : undefined,\n\t\trequest.context.panelTitle ? `- Panel: ${request.context.panelTitle}` : undefined,\n\t\trequest.context.timeRange ? `- Zeitraum: ${request.context.timeRange}` : undefined,\n\t\tvariables || undefined,\n\t\t`- URL: ${request.context.url}`,\n\t]\n\t\t.filter((value): value is string => Boolean(value))\n\t\t.join(\"\\n\");\n}\n\nfunction escapeSlack(value: string): string {\n\treturn value.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\nfunction sendJson(response: ServerResponse, status: number, body: unknown): void {\n\tresponse.writeHead(status, { \"content-type\": \"application/json; charset=utf-8\", \"cache-control\": \"no-store\" });\n\tresponse.end(JSON.stringify(body));\n}\n\nclass HandoffHttpError extends Error {\n\tconstructor(\n\t\tpublic status: number,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t}\n}\n"]}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./router.js\";\nexport * from \"./scheduled.js\";\nexport * from \"./slack-sink.js\";\nexport * from \"./types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./handoff.js\";\nexport * from \"./router.js\";\nexport * from \"./scheduled.js\";\nexport * from \"./slack-sink.js\";\nexport * from \"./types.js\";\n"]}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./router.js\";\nexport * from \"./scheduled.js\";\nexport * from \"./slack-sink.js\";\nexport * from \"./types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./handoff.js\";\nexport * from \"./router.js\";\nexport * from \"./scheduled.js\";\nexport * from \"./slack-sink.js\";\nexport * from \"./types.js\";\n"]}
|
package/dist/slack-sink.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from "@jobmatchme/bee-gate";
|
|
2
2
|
import type { WebClient } from "@slack/web-api";
|
|
3
|
+
export interface SlackSinkObserver {
|
|
4
|
+
posted(target: TransportOutputTarget, ref: string, text: string): void;
|
|
5
|
+
updated(target: TransportOutputTarget, ref: string, text: string): void;
|
|
6
|
+
}
|
|
3
7
|
export declare class SlackSink implements TransportSink<string> {
|
|
4
8
|
private webClient;
|
|
5
9
|
private blobStore;
|
|
6
|
-
|
|
10
|
+
private observer?;
|
|
11
|
+
constructor(webClient: WebClient, blobStore: BlobStore, observer?: SlackSinkObserver | undefined);
|
|
7
12
|
postMessage(target: TransportOutputTarget, text: string): Promise<string>;
|
|
8
13
|
updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void>;
|
|
9
14
|
publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void>;
|
package/dist/slack-sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;
|
|
1
|
+
{"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,OAAO,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxE;AAED,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ,CAAC;IAHlB,YACS,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,QAAQ,CAAC,+BAAmB,EACjC;IAEE,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;IAEK,aAAa,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3F;IAEK,eAAe,CAAC,MAAM,EAAE,qBAAqB,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;CACD","sourcesContent":["import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: createReadStream(materialized.path),\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n"]}
|
package/dist/slack-sink.js
CHANGED
|
@@ -2,9 +2,11 @@ import { createReadStream } from "fs";
|
|
|
2
2
|
export class SlackSink {
|
|
3
3
|
webClient;
|
|
4
4
|
blobStore;
|
|
5
|
-
|
|
5
|
+
observer;
|
|
6
|
+
constructor(webClient, blobStore, observer) {
|
|
6
7
|
this.webClient = webClient;
|
|
7
8
|
this.blobStore = blobStore;
|
|
9
|
+
this.observer = observer;
|
|
8
10
|
}
|
|
9
11
|
async postMessage(target, text) {
|
|
10
12
|
if (!target.channelId) {
|
|
@@ -15,7 +17,9 @@ export class SlackSink {
|
|
|
15
17
|
text,
|
|
16
18
|
thread_ts: target.threadId,
|
|
17
19
|
});
|
|
18
|
-
|
|
20
|
+
const ref = result.ts;
|
|
21
|
+
this.observer?.posted(target, ref, text);
|
|
22
|
+
return ref;
|
|
19
23
|
}
|
|
20
24
|
async updateMessage(target, ref, text) {
|
|
21
25
|
if (!target.channelId) {
|
|
@@ -26,6 +30,7 @@ export class SlackSink {
|
|
|
26
30
|
ts: ref,
|
|
27
31
|
text,
|
|
28
32
|
});
|
|
33
|
+
this.observer?.updated(target, ref, text);
|
|
29
34
|
}
|
|
30
35
|
async publishArtifact(target, artifact) {
|
|
31
36
|
if (!target.channelId) {
|
package/dist/slack-sink.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAOtC,MAAM,OAAO,SAAS;IAEZ,SAAS;IACT,SAAS;IACT,QAAQ;IAHjB,YACS,SAAoB,EACpB,SAAoB,EACpB,QAA4B,EACnC;yBAHO,SAAS;yBACT,SAAS;wBACT,QAAQ;IACd,CAAC;IAEJ,KAAK,CAAC,WAAW,CAAC,MAA6B,EAAE,IAAY,EAAmB;QAC/E,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;YACpD,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,IAAI;YACJ,SAAS,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,EAAY,CAAC;QAChC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACzC,OAAO,GAAG,CAAC;IAAA,CACX;IAED,KAAK,CAAC,aAAa,CAAC,MAA6B,EAAE,GAAW,EAAE,IAAY,EAAiB;QAC5F,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;YAChC,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,EAAE,EAAE,GAAG;YACP,IAAI;SACJ,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAAA,CAC1C;IAED,KAAK,CAAC,eAAe,CAAC,MAA6B,EAAE,QAAqB,EAAiB;QAC1F,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAChE,IAAI,CAAC;YACJ,MAAO,IAAI,CAAC,SAAS,CAAC,KAAa,CAAC,QAAQ,CAAC;gBAC5C,UAAU,EAAE,MAAM,CAAC,SAAS;gBAC5B,SAAS,EAAE,MAAM,CAAC,QAAQ;gBAC1B,IAAI,EAAE,gBAAgB,CAAC,YAAY,CAAC,IAAI,CAAC;gBACzC,QAAQ,EAAE,YAAY,CAAC,QAAQ;gBAC/B,KAAK,EAAE,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC,QAAQ;aAC/D,CAAC,CAAC;QACJ,CAAC;gBAAS,CAAC;YACV,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QAChC,CAAC;IAAA,CACD;CACD","sourcesContent":["import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: createReadStream(materialized.path),\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,55 @@ export interface SlackGatewayConfig {
|
|
|
23
23
|
name?: string;
|
|
24
24
|
};
|
|
25
25
|
routes: SlackRouteConfig[];
|
|
26
|
+
handoff?: SlackHandoffConfig;
|
|
27
|
+
}
|
|
28
|
+
export interface SlackHandoffConfig {
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
host?: string;
|
|
31
|
+
port?: number;
|
|
32
|
+
allowedDashboardHosts?: string[];
|
|
33
|
+
routes: SlackHandoffRouteConfig[];
|
|
34
|
+
}
|
|
35
|
+
export interface SlackHandoffRouteConfig {
|
|
36
|
+
id: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
channelId: string;
|
|
39
|
+
worker: BeeWorkerTargetConfig;
|
|
40
|
+
session?: SlackSessionConfig;
|
|
41
|
+
}
|
|
42
|
+
export interface SlackHandoffActor {
|
|
43
|
+
userId: string;
|
|
44
|
+
userName?: string;
|
|
45
|
+
displayName?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface SlackHandoffContext {
|
|
48
|
+
dashboardTitle?: string;
|
|
49
|
+
dashboardUid?: string;
|
|
50
|
+
panelTitle?: string;
|
|
51
|
+
panelId?: number;
|
|
52
|
+
url: string;
|
|
53
|
+
timeRange?: string;
|
|
54
|
+
variables?: Record<string, string>;
|
|
55
|
+
}
|
|
56
|
+
export interface SlackHandoffRequest {
|
|
57
|
+
routeId: string;
|
|
58
|
+
text: string;
|
|
59
|
+
actor: SlackHandoffActor;
|
|
60
|
+
context: SlackHandoffContext;
|
|
61
|
+
}
|
|
62
|
+
export interface SlackHandoffReply {
|
|
63
|
+
ts: string;
|
|
64
|
+
threadTs?: string;
|
|
65
|
+
text: string;
|
|
66
|
+
author?: string;
|
|
67
|
+
isBot: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface SlackHandoffRecord {
|
|
70
|
+
routeId: string;
|
|
71
|
+
channelId: string;
|
|
72
|
+
threadTs: string;
|
|
73
|
+
permalink: string;
|
|
74
|
+
createdAt: string;
|
|
26
75
|
}
|
|
27
76
|
export interface SlackFile {
|
|
28
77
|
id?: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QACL,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE,gBAAgB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n\thandoff?: SlackHandoffConfig;\n}\n\nexport interface SlackHandoffConfig {\n\tenabled?: boolean;\n\thost?: string;\n\tport?: number;\n\tallowedDashboardHosts?: string[];\n\troutes: SlackHandoffRouteConfig[];\n}\n\nexport interface SlackHandoffRouteConfig {\n\tid: string;\n\tlabel?: string;\n\tchannelId: string;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackHandoffActor {\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n}\n\nexport interface SlackHandoffContext {\n\tdashboardTitle?: string;\n\tdashboardUid?: string;\n\tpanelTitle?: string;\n\tpanelId?: number;\n\turl: string;\n\ttimeRange?: string;\n\tvariables?: Record<string, string>;\n}\n\nexport interface SlackHandoffRequest {\n\trouteId: string;\n\ttext: string;\n\tactor: SlackHandoffActor;\n\tcontext: SlackHandoffContext;\n}\n\nexport interface SlackHandoffReply {\n\tts: string;\n\tthreadTs?: string;\n\ttext: string;\n\tauthor?: string;\n\tisBot: boolean;\n}\n\nexport interface SlackHandoffRecord {\n\trouteId: string;\n\tchannelId: string;\n\tthreadTs: string;\n\tpermalink: string;\n\tcreatedAt: string;\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n\thandoff?: SlackHandoffConfig;\n}\n\nexport interface SlackHandoffConfig {\n\tenabled?: boolean;\n\thost?: string;\n\tport?: number;\n\tallowedDashboardHosts?: string[];\n\troutes: SlackHandoffRouteConfig[];\n}\n\nexport interface SlackHandoffRouteConfig {\n\tid: string;\n\tlabel?: string;\n\tchannelId: string;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackHandoffActor {\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n}\n\nexport interface SlackHandoffContext {\n\tdashboardTitle?: string;\n\tdashboardUid?: string;\n\tpanelTitle?: string;\n\tpanelId?: number;\n\turl: string;\n\ttimeRange?: string;\n\tvariables?: Record<string, string>;\n}\n\nexport interface SlackHandoffRequest {\n\trouteId: string;\n\ttext: string;\n\tactor: SlackHandoffActor;\n\tcontext: SlackHandoffContext;\n}\n\nexport interface SlackHandoffReply {\n\tts: string;\n\tthreadTs?: string;\n\ttext: string;\n\tauthor?: string;\n\tisBot: boolean;\n}\n\nexport interface SlackHandoffRecord {\n\trouteId: string;\n\tchannelId: string;\n\tthreadTs: string;\n\tpermalink: string;\n\tcreatedAt: string;\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|