@cjhyy/code-shell-chat 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +416 -0
  3. package/dist/adapter-factory.d.ts +30 -0
  4. package/dist/adapter-factory.js +198 -0
  5. package/dist/attachments.d.ts +13 -0
  6. package/dist/attachments.js +74 -0
  7. package/dist/channel.d.ts +112 -0
  8. package/dist/channel.js +60 -0
  9. package/dist/chat-gateway.d.ts +58 -0
  10. package/dist/chat-gateway.js +232 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +259 -0
  13. package/dist/codeshell.d.ts +6 -0
  14. package/dist/codeshell.js +6 -0
  15. package/dist/config.d.ts +125 -0
  16. package/dist/config.js +469 -0
  17. package/dist/delivery-queue.d.ts +93 -0
  18. package/dist/delivery-queue.js +401 -0
  19. package/dist/desktop-control-client.d.ts +51 -0
  20. package/dist/desktop-control-client.js +309 -0
  21. package/dist/dingtalk.d.ts +19 -0
  22. package/dist/dingtalk.js +93 -0
  23. package/dist/discord.d.ts +23 -0
  24. package/dist/discord.js +134 -0
  25. package/dist/gateway.d.ts +34 -0
  26. package/dist/gateway.js +287 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.js +13 -0
  29. package/dist/ingress.d.ts +8 -0
  30. package/dist/ingress.js +61 -0
  31. package/dist/instance-lock.d.ts +24 -0
  32. package/dist/instance-lock.js +100 -0
  33. package/dist/lark.d.ts +20 -0
  34. package/dist/lark.js +184 -0
  35. package/dist/lifecycle.d.ts +7 -0
  36. package/dist/lifecycle.js +16 -0
  37. package/dist/line.d.ts +17 -0
  38. package/dist/line.js +123 -0
  39. package/dist/matrix.d.ts +21 -0
  40. package/dist/matrix.js +208 -0
  41. package/dist/mattermost.d.ts +18 -0
  42. package/dist/mattermost.js +170 -0
  43. package/dist/media.d.ts +39 -0
  44. package/dist/media.js +223 -0
  45. package/dist/notification-progress.d.ts +46 -0
  46. package/dist/notification-progress.js +299 -0
  47. package/dist/notification-relay.d.ts +43 -0
  48. package/dist/notification-relay.js +285 -0
  49. package/dist/platform-canary.d.ts +22 -0
  50. package/dist/platform-canary.js +55 -0
  51. package/dist/protocol.d.ts +102 -0
  52. package/dist/protocol.js +1 -0
  53. package/dist/service-manager.d.ts +47 -0
  54. package/dist/service-manager.js +222 -0
  55. package/dist/slack.d.ts +17 -0
  56. package/dist/slack.js +136 -0
  57. package/dist/teams.d.ts +26 -0
  58. package/dist/teams.js +193 -0
  59. package/dist/telegram.d.ts +33 -0
  60. package/dist/telegram.js +263 -0
  61. package/dist/webhook.d.ts +26 -0
  62. package/dist/webhook.js +91 -0
  63. package/dist/wechat-login.d.ts +21 -0
  64. package/dist/wechat-login.js +97 -0
  65. package/dist/wechat-storage.d.ts +61 -0
  66. package/dist/wechat-storage.js +251 -0
  67. package/dist/wechat.d.ts +129 -0
  68. package/dist/wechat.js +1082 -0
  69. package/dist/wecom.d.ts +16 -0
  70. package/dist/wecom.js +149 -0
  71. package/dist/whatsapp.d.ts +27 -0
  72. package/dist/whatsapp.js +218 -0
  73. package/package.json +123 -0
@@ -0,0 +1,55 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { ChatGateway, createAllowlistMiddleware } from "./chat-gateway.js";
3
+ /**
4
+ * Credential-backed canary: starts the real adapters and ingress, then waits
5
+ * for one allowlisted inbound `/canary <nonce>` per configured channel and
6
+ * replies through that same platform. Nothing is sent to users unprompted.
7
+ */
8
+ export async function runPlatformCanary(options) {
9
+ if (options.adapters.length === 0)
10
+ throw new Error("canary requires at least one adapter");
11
+ const expected = new Set(options.adapters.map(({ channel }) => channel));
12
+ const completed = new Set();
13
+ const nonce = options.nonce ?? randomBytes(6).toString("hex");
14
+ const command = `/canary ${nonce}`;
15
+ const timeoutMs = options.timeoutMs ?? 10 * 60_000;
16
+ const abort = new AbortController();
17
+ let resolveComplete;
18
+ const complete = new Promise((resolve) => {
19
+ resolveComplete = resolve;
20
+ });
21
+ const gateway = new ChatGateway({
22
+ adapters: options.adapters,
23
+ webhook: options.webhook,
24
+ delivery: options.delivery,
25
+ });
26
+ gateway.use(createAllowlistMiddleware(options.allowlists));
27
+ gateway.use(async ({ message, reply }) => {
28
+ if (message.text.trim() !== command)
29
+ return;
30
+ await reply({ text: `✅ CodeShell ${message.channel} canary passed (${nonce})` });
31
+ completed.add(message.channel);
32
+ if ([...expected].every((channel) => completed.has(channel)))
33
+ resolveComplete();
34
+ });
35
+ options.onReady?.(`请在以下 channel 的白名单会话中各发送一次:${command}\n${[...expected].join(", ")}`);
36
+ const gatewayTask = gateway.run(abort.signal);
37
+ const timer = setTimeout(() => abort.abort(), timeoutMs);
38
+ try {
39
+ await Promise.race([
40
+ complete,
41
+ gatewayTask.then(() => {
42
+ throw new Error("platform canary gateway exited before completion");
43
+ }),
44
+ new Promise((_resolve, reject) => abort.signal.addEventListener("abort", () => reject(new Error(`platform canary timed out; missing: ${[...expected]
45
+ .filter((channel) => !completed.has(channel))
46
+ .join(", ")}`)), { once: true })),
47
+ ]);
48
+ return { nonce, channels: [...completed].sort(), completedAt: Date.now() };
49
+ }
50
+ finally {
51
+ clearTimeout(timer);
52
+ abort.abort();
53
+ await gatewayTask.catch(() => undefined);
54
+ }
55
+ }
@@ -0,0 +1,102 @@
1
+ import type { ChannelCapabilities } from "./channel.js";
2
+ export declare const DESKTOP_CONTROL_PROTOCOL_VERSION = 1;
3
+ export interface DesktopControlDescriptor {
4
+ version: typeof DESKTOP_CONTROL_PROTOCOL_VERSION;
5
+ pid: number;
6
+ baseUrl: string;
7
+ token: string;
8
+ startedAt: number;
9
+ }
10
+ export interface DesktopControlEvent {
11
+ id: number;
12
+ createdAt: number;
13
+ /**
14
+ * Optional opaque semantic delivery identity. Unlike streamId:id it remains
15
+ * stable when Desktop republishes one durable event after a process restart.
16
+ */
17
+ deliveryKey?: string;
18
+ type: "tunnel.connected" | "tunnel.disconnected" | "tunnel.error" | "pet.task.completed" | "pet.task.failed" | "pet.task.cancelled" | "pet.task.reported" | "automation.completed" | "automation.failed" | "automation.stopped" | "automation.cancelled";
19
+ text: string;
20
+ title?: string;
21
+ button?: {
22
+ text: string;
23
+ url: string;
24
+ };
25
+ attachments?: DesktopControlEventAttachment[];
26
+ /** One originating IM conversation; bypasses general notification targets. */
27
+ target?: {
28
+ channel: string;
29
+ target: string;
30
+ };
31
+ }
32
+ export interface DesktopControlEventAttachment {
33
+ kind: PetChatAttachmentKind;
34
+ name: string;
35
+ mimeType: string;
36
+ size: number;
37
+ /** Validated local path on the same host as the loopback gateway. */
38
+ path: string;
39
+ }
40
+ export interface DesktopControlEventPage {
41
+ /** Durable outbox identity; changes only when Desktop initializes a replacement event store. */
42
+ streamId: string;
43
+ events: DesktopControlEvent[];
44
+ cursor: number;
45
+ /** Desktop retained the stream identity but recovered behind this checkpoint. */
46
+ resetCursor?: true;
47
+ }
48
+ export interface MobileRemoteOpenResult {
49
+ url: string;
50
+ pairingUrl: string;
51
+ expiresAt: number;
52
+ mode: "tunnel" | "lan";
53
+ }
54
+ export interface MobileRemoteStatus {
55
+ running: boolean;
56
+ url?: string;
57
+ mode?: "tunnel" | "lan";
58
+ tunnelRunning: boolean;
59
+ tunnelConnected: boolean;
60
+ passcodeSet: boolean;
61
+ onlineDeviceCount: number;
62
+ }
63
+ export type PetChatAttachmentKind = "image" | "file" | "audio" | "video";
64
+ /** Bytes already fetched by the gateway and ready for loopback transfer to desktop main. */
65
+ export interface PetChatInputAttachment {
66
+ id: string;
67
+ kind: PetChatAttachmentKind;
68
+ name?: string;
69
+ mimeType?: string;
70
+ size: number;
71
+ dataBase64: string;
72
+ }
73
+ export interface PetChatRequest {
74
+ message: string;
75
+ attachments?: PetChatInputAttachment[];
76
+ origin?: {
77
+ channel: string;
78
+ target: string;
79
+ senderId: string;
80
+ messageId?: string;
81
+ /** Adapter-declared implementation capabilities for this exact route. */
82
+ capabilities: ChannelCapabilities;
83
+ /** Enabled adapters in this Gateway process, without credentials or target ids. */
84
+ channels?: PetChatGatewayChannel[];
85
+ };
86
+ }
87
+ export interface PetChatGatewayChannel {
88
+ channel: string;
89
+ capabilities: ChannelCapabilities;
90
+ }
91
+ export interface PetChatResult {
92
+ text: string;
93
+ petSessionId: string;
94
+ reason?: string;
95
+ /** GatewayReply URL action; rendered natively or as a labelled link by the adapter. */
96
+ button?: {
97
+ text: string;
98
+ url: string;
99
+ };
100
+ /** Host-produced reply attachments; same host-local path shape as event attachments. */
101
+ attachments?: DesktopControlEventAttachment[];
102
+ }
@@ -0,0 +1 @@
1
+ export const DESKTOP_CONTROL_PROTOCOL_VERSION = 1;
@@ -0,0 +1,47 @@
1
+ export interface GatewayServiceOptions {
2
+ configPath: string;
3
+ platform?: NodeJS.Platform;
4
+ home?: string;
5
+ executable?: string;
6
+ cliPath?: string;
7
+ run?: (command: string, args: string[]) => Promise<{
8
+ stdout?: string;
9
+ stderr?: string;
10
+ }>;
11
+ }
12
+ export interface GatewayServiceStatus {
13
+ installed: boolean;
14
+ running: boolean;
15
+ definitionPath?: string;
16
+ detail?: string;
17
+ }
18
+ /** Install the gateway as a per-user launchd/systemd/Task Scheduler service. */
19
+ export declare class GatewayServiceManager {
20
+ private readonly options;
21
+ private readonly platform;
22
+ private readonly home;
23
+ private readonly executable;
24
+ private readonly cliPath;
25
+ private readonly run;
26
+ constructor(options: GatewayServiceOptions);
27
+ install(): Promise<GatewayServiceStatus>;
28
+ uninstall(): Promise<GatewayServiceStatus>;
29
+ status(): Promise<GatewayServiceStatus>;
30
+ private installLaunchd;
31
+ private installSystemd;
32
+ private installTaskScheduler;
33
+ private launchdPath;
34
+ private systemdPath;
35
+ }
36
+ export declare function renderLaunchdService(input: {
37
+ executable: string;
38
+ cliPath: string;
39
+ configPath: string;
40
+ stdoutPath: string;
41
+ stderrPath: string;
42
+ }): string;
43
+ export declare function renderSystemdService(input: {
44
+ executable: string;
45
+ cliPath: string;
46
+ configPath: string;
47
+ }): string;
@@ -0,0 +1,222 @@
1
+ import { execFile as nodeExecFile } from "node:child_process";
2
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(nodeExecFile);
7
+ /** Install the gateway as a per-user launchd/systemd/Task Scheduler service. */
8
+ export class GatewayServiceManager {
9
+ options;
10
+ platform;
11
+ home;
12
+ executable;
13
+ cliPath;
14
+ run;
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.platform = options.platform ?? process.platform;
18
+ this.home = resolve(options.home ?? process.env.HOME ?? homedir());
19
+ this.executable = resolve(options.executable ?? process.execPath);
20
+ this.cliPath = resolve(options.cliPath ?? process.argv[1] ?? "");
21
+ this.run = options.run ?? ((command, args) => execFileAsync(command, args));
22
+ }
23
+ async install() {
24
+ if (this.platform === "darwin")
25
+ return this.installLaunchd();
26
+ if (this.platform === "linux")
27
+ return this.installSystemd();
28
+ if (this.platform === "win32")
29
+ return this.installTaskScheduler();
30
+ throw new Error(`不支持的服务平台:${this.platform}`);
31
+ }
32
+ async uninstall() {
33
+ if (this.platform === "darwin") {
34
+ const path = this.launchdPath();
35
+ await this.run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}`, path]).catch(() => undefined);
36
+ await rm(path, { force: true });
37
+ return { installed: false, running: false, definitionPath: path };
38
+ }
39
+ if (this.platform === "linux") {
40
+ const path = this.systemdPath();
41
+ await this.run("systemctl", ["--user", "disable", "--now", "codeshell-chat.service"]).catch(() => undefined);
42
+ await rm(path, { force: true });
43
+ await this.run("systemctl", ["--user", "daemon-reload"]);
44
+ return { installed: false, running: false, definitionPath: path };
45
+ }
46
+ if (this.platform === "win32") {
47
+ await this.run("schtasks.exe", ["/Delete", "/TN", "CodeShell Chat Gateway", "/F"]).catch(() => undefined);
48
+ return { installed: false, running: false };
49
+ }
50
+ throw new Error(`不支持的服务平台:${this.platform}`);
51
+ }
52
+ async status() {
53
+ if (this.platform === "darwin") {
54
+ const path = this.launchdPath();
55
+ const installed = await fileExists(path);
56
+ if (!installed)
57
+ return { installed: false, running: false, definitionPath: path };
58
+ const result = await this.run("launchctl", [
59
+ "print",
60
+ `gui/${process.getuid?.() ?? 0}/com.cjhyy.codeshell.chat-gateway`,
61
+ ]).catch((error) => ({ stderr: String(error) }));
62
+ return {
63
+ installed: true,
64
+ running: !result.stderr,
65
+ definitionPath: path,
66
+ ...(result.stderr ? { detail: result.stderr } : {}),
67
+ };
68
+ }
69
+ if (this.platform === "linux") {
70
+ const path = this.systemdPath();
71
+ const installed = await fileExists(path);
72
+ if (!installed)
73
+ return { installed: false, running: false, definitionPath: path };
74
+ const result = await this.run("systemctl", ["--user", "is-active", "codeshell-chat.service"])
75
+ .then(({ stdout }) => ({ running: stdout?.trim() === "active", detail: stdout?.trim() }))
76
+ .catch((error) => ({ running: false, detail: String(error) }));
77
+ return { installed: true, definitionPath: path, ...result };
78
+ }
79
+ if (this.platform === "win32") {
80
+ const result = await this.run("schtasks.exe", [
81
+ "/Query",
82
+ "/TN",
83
+ "CodeShell Chat Gateway",
84
+ "/FO",
85
+ "LIST",
86
+ ]).catch((error) => ({ stdout: "", stderr: String(error) }));
87
+ const installed = !result.stderr;
88
+ return {
89
+ installed,
90
+ running: installed && /Running/i.test(result.stdout ?? ""),
91
+ ...(result.stderr ? { detail: result.stderr } : {}),
92
+ };
93
+ }
94
+ throw new Error(`不支持的服务平台:${this.platform}`);
95
+ }
96
+ async installLaunchd() {
97
+ const path = this.launchdPath();
98
+ const logDir = join(this.home, ".code-shell", "im-gateway", "logs");
99
+ await mkdir(logDir, { recursive: true, mode: 0o700 });
100
+ await writeOwnerFile(path, renderLaunchdService({
101
+ executable: this.executable,
102
+ cliPath: this.cliPath,
103
+ configPath: resolve(this.options.configPath),
104
+ stdoutPath: join(logDir, "gateway.log"),
105
+ stderrPath: join(logDir, "gateway.error.log"),
106
+ }));
107
+ await this.run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}`, path]).catch(() => undefined);
108
+ await this.run("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 0}`, path]);
109
+ return { installed: true, running: true, definitionPath: path };
110
+ }
111
+ async installSystemd() {
112
+ const path = this.systemdPath();
113
+ await writeOwnerFile(path, renderSystemdService({
114
+ executable: this.executable,
115
+ cliPath: this.cliPath,
116
+ configPath: resolve(this.options.configPath),
117
+ }));
118
+ await this.run("systemctl", ["--user", "daemon-reload"]);
119
+ await this.run("systemctl", ["--user", "enable", "--now", "codeshell-chat.service"]);
120
+ return { installed: true, running: true, definitionPath: path };
121
+ }
122
+ async installTaskScheduler() {
123
+ const command = windowsCommand([
124
+ this.executable,
125
+ this.cliPath,
126
+ "--config",
127
+ resolve(this.options.configPath),
128
+ ]);
129
+ await this.run("schtasks.exe", [
130
+ "/Create",
131
+ "/TN",
132
+ "CodeShell Chat Gateway",
133
+ "/TR",
134
+ command,
135
+ "/SC",
136
+ "ONLOGON",
137
+ "/RL",
138
+ "LIMITED",
139
+ "/F",
140
+ ]);
141
+ await this.run("schtasks.exe", ["/Run", "/TN", "CodeShell Chat Gateway"]);
142
+ return { installed: true, running: true };
143
+ }
144
+ launchdPath() {
145
+ return join(this.home, "Library", "LaunchAgents", "com.cjhyy.codeshell.chat-gateway.plist");
146
+ }
147
+ systemdPath() {
148
+ return join(this.home, ".config", "systemd", "user", "codeshell-chat.service");
149
+ }
150
+ }
151
+ export function renderLaunchdService(input) {
152
+ const args = [input.executable, input.cliPath, "--config", input.configPath]
153
+ .map((value) => ` <string>${escapeXml(value)}</string>`)
154
+ .join("\n");
155
+ return `<?xml version="1.0" encoding="UTF-8"?>
156
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
157
+ <plist version="1.0">
158
+ <dict>
159
+ <key>Label</key><string>com.cjhyy.codeshell.chat-gateway</string>
160
+ <key>ProgramArguments</key>
161
+ <array>
162
+ ${args}
163
+ </array>
164
+ <key>RunAtLoad</key><true/>
165
+ <key>KeepAlive</key><true/>
166
+ <key>ThrottleInterval</key><integer>10</integer>
167
+ <key>StandardOutPath</key><string>${escapeXml(input.stdoutPath)}</string>
168
+ <key>StandardErrorPath</key><string>${escapeXml(input.stderrPath)}</string>
169
+ </dict>
170
+ </plist>
171
+ `;
172
+ }
173
+ export function renderSystemdService(input) {
174
+ return `[Unit]
175
+ Description=CodeShell Chat Gateway
176
+ After=network-online.target
177
+ Wants=network-online.target
178
+
179
+ [Service]
180
+ Type=simple
181
+ ExecStart=${systemdQuote(input.executable)} ${systemdQuote(input.cliPath)} --config ${systemdQuote(input.configPath)}
182
+ Restart=on-failure
183
+ RestartSec=5
184
+ NoNewPrivileges=true
185
+ PrivateTmp=true
186
+ ProtectSystem=strict
187
+ ProtectHome=read-only
188
+ ReadWritePaths=%h/.code-shell
189
+
190
+ [Install]
191
+ WantedBy=default.target
192
+ `;
193
+ }
194
+ async function writeOwnerFile(path, contents) {
195
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
196
+ await writeFile(path, contents, { encoding: "utf-8", mode: 0o600 });
197
+ if (process.platform !== "win32")
198
+ await chmod(path, 0o600);
199
+ }
200
+ async function fileExists(path) {
201
+ try {
202
+ await readFile(path);
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ function escapeXml(value) {
210
+ return value
211
+ .replaceAll("&", "&amp;")
212
+ .replaceAll("<", "&lt;")
213
+ .replaceAll(">", "&gt;")
214
+ .replaceAll('"', "&quot;")
215
+ .replaceAll("'", "&apos;");
216
+ }
217
+ function systemdQuote(value) {
218
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
219
+ }
220
+ function windowsCommand(args) {
221
+ return args.map((value) => `"${value.replaceAll('"', '\\"')}"`).join(" ");
222
+ }
@@ -0,0 +1,17 @@
1
+ import type { ChannelAdapter, ChannelMessageHandler, OutgoingMessage } from "./channel.js";
2
+ export interface SlackAdapterConfig {
3
+ botToken: string;
4
+ appToken: string;
5
+ }
6
+ export declare class SlackAdapter implements ChannelAdapter {
7
+ readonly channel = "slack";
8
+ readonly capabilities: import("./channel.js").ChannelCapabilities;
9
+ readonly combinesTextAndAttachmentsAtomically = true;
10
+ private readonly socket;
11
+ private readonly web;
12
+ private readonly botToken;
13
+ private readonly fetchFn;
14
+ constructor(config: SlackAdapterConfig, fetchFn?: typeof fetch);
15
+ run(handler: ChannelMessageHandler, signal: AbortSignal): Promise<void>;
16
+ send(target: string, message: OutgoingMessage): Promise<void>;
17
+ }
package/dist/slack.js ADDED
@@ -0,0 +1,136 @@
1
+ import { SocketModeClient } from "@slack/socket-mode";
2
+ import { WebClient } from "@slack/web-api";
3
+ import { BUILTIN_CHANNEL_CAPABILITIES } from "./channel.js";
4
+ import { dispatchSafely, formatOutgoingMarkdown, waitForAbort } from "./lifecycle.js";
5
+ import { mediaKind, outgoingAttachments, remoteAttachment } from "./media.js";
6
+ export class SlackAdapter {
7
+ channel = "slack";
8
+ capabilities = BUILTIN_CHANNEL_CAPABILITIES.slack;
9
+ combinesTextAndAttachmentsAtomically = true;
10
+ socket;
11
+ web;
12
+ botToken;
13
+ fetchFn;
14
+ constructor(config, fetchFn = fetch) {
15
+ this.socket = new SocketModeClient({ appToken: config.appToken });
16
+ this.web = new WebClient(config.botToken);
17
+ this.botToken = config.botToken;
18
+ this.fetchFn = fetchFn;
19
+ }
20
+ async run(handler, signal) {
21
+ const onEvent = async ({ event, ack }) => {
22
+ if (event?.type !== "message" ||
23
+ typeof event.channel !== "string" ||
24
+ typeof event.user !== "string" ||
25
+ event.bot_id ||
26
+ (event.subtype && event.subtype !== "file_share")) {
27
+ await ack();
28
+ return;
29
+ }
30
+ const files = Array.isArray(event.files) ? event.files : [];
31
+ const attachments = files.flatMap((file) => {
32
+ const url = file.url_private_download ?? file.url_private;
33
+ if (!file.id || !url)
34
+ return [];
35
+ return [
36
+ remoteAttachment({
37
+ id: file.id,
38
+ kind: mediaKind(file.mimetype, file.name),
39
+ name: file.name,
40
+ mimeType: file.mimetype,
41
+ size: file.size,
42
+ url,
43
+ headers: { authorization: `Bearer ${this.botToken}` },
44
+ fetch: this.fetchFn,
45
+ }),
46
+ ];
47
+ });
48
+ const text = typeof event.text === "string" ? event.text : "";
49
+ if (!text && attachments.length === 0) {
50
+ await ack();
51
+ return;
52
+ }
53
+ // dispatchSafely so a rejected delivery (e.g. inbox backpressure) never
54
+ // escapes this SocketMode listener as an unhandled rejection — emit does
55
+ // not await listeners, so an unhandled rejection would crash the process
56
+ // and skip the ack below.
57
+ await dispatchSafely(handler, {
58
+ channel: this.channel,
59
+ target: event.channel,
60
+ senderId: event.user,
61
+ text,
62
+ ...(event.client_msg_id || event.ts
63
+ ? { messageId: String(event.client_msg_id ?? event.ts) }
64
+ : {}),
65
+ ...(attachments.length > 0 ? { attachments } : {}),
66
+ });
67
+ await ack();
68
+ };
69
+ const onCommand = async ({ body, ack }) => {
70
+ if (typeof body.command !== "string" ||
71
+ typeof body.channel_id !== "string" ||
72
+ typeof body.user_id !== "string") {
73
+ await ack();
74
+ return;
75
+ }
76
+ await dispatchSafely(handler, {
77
+ channel: this.channel,
78
+ target: body.channel_id,
79
+ senderId: body.user_id,
80
+ text: [body.command, typeof body.text === "string" ? body.text : ""]
81
+ .filter(Boolean)
82
+ .join(" "),
83
+ ...(body.trigger_id ? { messageId: String(body.trigger_id) } : {}),
84
+ });
85
+ await ack();
86
+ };
87
+ this.socket.on("events_api", onEvent);
88
+ this.socket.on("slash_commands", onCommand);
89
+ await this.socket.start();
90
+ try {
91
+ await waitForAbort(signal);
92
+ }
93
+ finally {
94
+ this.socket.off("events_api", onEvent);
95
+ this.socket.off("slash_commands", onCommand);
96
+ await this.socket.disconnect();
97
+ }
98
+ }
99
+ async send(target, message) {
100
+ const attachments = outgoingAttachments(message, this.capabilities.outbound.attachments);
101
+ if (attachments.length > 0) {
102
+ // Slack's v2 upload can publish all files and the comment atomically,
103
+ // avoiding a text-first partial send that would duplicate on retry.
104
+ await this.web.filesUploadV2({
105
+ channel_id: target,
106
+ initial_comment: formatOutgoingMarkdown(message.text, message.button),
107
+ file_uploads: attachments.map((attachment) => ({
108
+ file: Buffer.from(attachment.data),
109
+ filename: attachment.name,
110
+ })),
111
+ });
112
+ return;
113
+ }
114
+ await this.web.chat.postMessage({
115
+ channel: target,
116
+ text: message.text,
117
+ ...(message.button
118
+ ? {
119
+ blocks: [
120
+ { type: "section", text: { type: "mrkdwn", text: message.text } },
121
+ {
122
+ type: "actions",
123
+ elements: [
124
+ {
125
+ type: "button",
126
+ text: { type: "plain_text", text: message.button.text },
127
+ url: message.button.url,
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ }
133
+ : {}),
134
+ });
135
+ }
136
+ }
@@ -0,0 +1,26 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { ChannelMessageHandler, OutgoingMessage, WebhookChannelAdapter } from "./channel.js";
3
+ export interface TeamsAdapterConfig {
4
+ appId: string;
5
+ appPassword: string;
6
+ appType?: string;
7
+ tenantId?: string;
8
+ statePath?: string;
9
+ }
10
+ export declare class TeamsAdapter implements WebhookChannelAdapter {
11
+ readonly channel = "teams";
12
+ readonly capabilities: import("./channel.js").ChannelCapabilities;
13
+ readonly combinesTextAndAttachmentsAtomically = true;
14
+ readonly webhookPath = "/webhooks/teams";
15
+ private readonly adapter;
16
+ private readonly appId;
17
+ private readonly contexts;
18
+ private readonly references;
19
+ private readonly statePath?;
20
+ constructor(config: TeamsAdapterConfig);
21
+ run(_handler: ChannelMessageHandler, signal: AbortSignal): Promise<void>;
22
+ handleWebhook(request: IncomingMessage, response: ServerResponse, handler: ChannelMessageHandler, maxBodyBytes: number): Promise<void>;
23
+ send(target: string, message: OutgoingMessage): Promise<void>;
24
+ private loadReferences;
25
+ private saveReferences;
26
+ }