@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,287 @@
1
+ import { channelCapabilities, supportsOutgoingAttachment, } from "./channel.js";
2
+ import { materializeChatAttachments } from "./attachments.js";
3
+ import { DesktopControlOperationError, DesktopControlUnavailableError, } from "./desktop-control-client.js";
4
+ import { materializeOutgoingAttachments, splitNotificationTextForChannel, } from "./notification-relay.js";
5
+ const MIMI_REPLY_CACHE_MAX_ENTRIES = 2_048;
6
+ export const CODE_SHELL_REMOTE_COMMANDS = [
7
+ { name: "open", description: "打开 CodeShell 手机遥控隧道" },
8
+ { name: "close", description: "关闭 CodeShell 手机遥控隧道" },
9
+ { name: "status", description: "查看 CodeShell 手机遥控状态" },
10
+ ];
11
+ /** Optional CodeShell integration. Unknown messages fall through to the next middleware. */
12
+ export function createCodeShellRemoteCommands(options) {
13
+ return async ({ message, adapter, reply }, next) => {
14
+ const command = parseGatewayIntent(message.text);
15
+ if (command === "unsupported") {
16
+ await next();
17
+ return;
18
+ }
19
+ try {
20
+ if (command === "open") {
21
+ const opened = await options.desktop.open();
22
+ const qr = supportsOutgoingAttachment(adapter, "image")
23
+ ? await renderPairingQrAttachment(opened.pairingUrl)
24
+ : undefined;
25
+ await reply({
26
+ text: [
27
+ `公网隧道已开启:${opened.url}`,
28
+ qr
29
+ ? "配对入口 10 分钟内有效,请点击下方按钮或扫描二维码。"
30
+ : "配对入口 10 分钟内有效,请点击下方按钮。",
31
+ "打开后仍需输入桌面端已设置的访问口令。",
32
+ ].join("\n"),
33
+ button: { text: "打开手机遥控", url: opened.pairingUrl },
34
+ ...(qr ? { attachments: [qr] } : {}),
35
+ });
36
+ return;
37
+ }
38
+ if (command === "close") {
39
+ await options.desktop.close();
40
+ await reply({ text: "公网隧道已关闭。" });
41
+ return;
42
+ }
43
+ const status = await options.desktop.status();
44
+ await reply({ text: formatStatus(status) });
45
+ }
46
+ catch (error) {
47
+ if (error instanceof DesktopControlUnavailableError) {
48
+ await reply({ text: `桌面端未在线:${error.message}` });
49
+ return;
50
+ }
51
+ if (error instanceof DesktopControlOperationError) {
52
+ await reply({ text: `操作失败:${error.message}` });
53
+ return;
54
+ }
55
+ await reply({
56
+ text: `操作失败:${error instanceof Error ? error.message : String(error)}`,
57
+ });
58
+ }
59
+ };
60
+ }
61
+ export function parseGatewayCommand(text) {
62
+ const first = text.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "";
63
+ const command = first.replace(/@[a-z0-9_]+$/i, "");
64
+ if (command === "/open")
65
+ return "open";
66
+ if (command === "/close")
67
+ return "close";
68
+ if (command === "/status")
69
+ return "status";
70
+ return "unsupported";
71
+ }
72
+ /**
73
+ * Only explicit slash commands short-circuit here. Every natural-language
74
+ * message — including "打开手机遥控" or "给我隧道地址" — flows to Mimi, whose
75
+ * MobileRemote tool decides the intent; regex alias matching was removed on
76
+ * purpose so intent understanding lives in one place (the model), not two.
77
+ */
78
+ export function parseGatewayIntent(text) {
79
+ return parseGatewayCommand(text);
80
+ }
81
+ /** Final middleware: route ordinary text/media turns into the durable Mimi Pet session. */
82
+ export function createMimiPetChat(options) {
83
+ // DeliveryQueue retries the same platform message after adapter.send fails.
84
+ // Cache the fully materialized reply (including attachment bytes) before the
85
+ // first send so retrying cannot re-run Mimi or any already-executed host
86
+ // action. Failed desktop/materialization work is evicted and remains retryable.
87
+ const replies = new Map();
88
+ const configuredChannels = options.channels ?? [];
89
+ return async ({ message, adapter, reply }) => {
90
+ if (!message.text.trim() && !message.attachments?.length)
91
+ return;
92
+ const cacheKey = mimiReplyCacheKey(message);
93
+ let cacheEntry;
94
+ let outgoing;
95
+ try {
96
+ if (!cacheKey) {
97
+ outgoing = await buildMimiOutgoingReplies(options, message, channelCapabilities(adapter), configuredChannels);
98
+ }
99
+ else {
100
+ const cached = replies.get(cacheKey);
101
+ if (cached) {
102
+ // Refresh insertion order so active retries are not the first evicted.
103
+ replies.delete(cacheKey);
104
+ replies.set(cacheKey, cached);
105
+ cacheEntry = cached;
106
+ outgoing = await cached.outgoing;
107
+ }
108
+ else {
109
+ const pending = buildMimiOutgoingReplies(options, message, channelCapabilities(adapter), configuredChannels);
110
+ const entry = {
111
+ outgoing: pending,
112
+ nextSendIndex: 0,
113
+ activeSends: 0,
114
+ sendFailed: false,
115
+ };
116
+ replies.set(cacheKey, entry);
117
+ cacheEntry = entry;
118
+ while (replies.size > MIMI_REPLY_CACHE_MAX_ENTRIES) {
119
+ const oldest = replies.keys().next().value;
120
+ if (typeof oldest !== "string")
121
+ break;
122
+ replies.delete(oldest);
123
+ }
124
+ pending.catch(() => {
125
+ if (replies.get(cacheKey) === entry)
126
+ replies.delete(cacheKey);
127
+ });
128
+ outgoing = await pending;
129
+ }
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (error instanceof DesktopControlUnavailableError) {
134
+ await reply({ text: `桌面端未在线:${error.message}` });
135
+ return;
136
+ }
137
+ if (error instanceof DesktopControlOperationError) {
138
+ await reply({ text: `Mimi Pet 处理失败:${error.message}` });
139
+ return;
140
+ }
141
+ await reply({
142
+ text: `Mimi Pet 处理失败:${error instanceof Error ? error.message : String(error)}`,
143
+ });
144
+ return;
145
+ }
146
+ // Adapter failures must escape to DeliveryQueue. Catching them above would
147
+ // turn a failed real reply into a successful generic error and lose it.
148
+ if (cacheEntry) {
149
+ if (cacheEntry.activeSends === 0)
150
+ cacheEntry.sendFailed = false;
151
+ cacheEntry.activeSends += 1;
152
+ }
153
+ try {
154
+ let index = cacheEntry?.nextSendIndex ?? 0;
155
+ while (index < outgoing.length) {
156
+ await reply(outgoing[index]);
157
+ index += 1;
158
+ if (cacheEntry)
159
+ cacheEntry.nextSendIndex = index;
160
+ }
161
+ }
162
+ catch (error) {
163
+ if (cacheEntry)
164
+ cacheEntry.sendFailed = true;
165
+ throw error;
166
+ }
167
+ finally {
168
+ if (cacheEntry) {
169
+ cacheEntry.activeSends -= 1;
170
+ // DeliveryQueue now owns a durable completed-message dedupe record.
171
+ // Keep memory proportional to failed/in-flight sends, not successful chat.
172
+ if (cacheKey &&
173
+ cacheEntry.activeSends === 0 &&
174
+ !cacheEntry.sendFailed &&
175
+ replies.get(cacheKey) === cacheEntry) {
176
+ replies.delete(cacheKey);
177
+ }
178
+ }
179
+ }
180
+ };
181
+ }
182
+ async function buildMimiOutgoingReplies(options, message, capabilities, configuredChannels) {
183
+ const attachments = await materializeChatAttachments(message.attachments);
184
+ const result = await options.desktop.petChat({
185
+ message: message.text,
186
+ ...(attachments.length > 0 ? { attachments } : {}),
187
+ origin: {
188
+ channel: message.channel,
189
+ target: message.target,
190
+ senderId: message.senderId,
191
+ ...(message.messageId ? { messageId: message.messageId } : {}),
192
+ capabilities,
193
+ channels: gatewayChannelCatalog(message.channel, capabilities, configuredChannels),
194
+ },
195
+ });
196
+ const chunks = splitNotificationTextForChannel(result.text || "Mimi Pet 已处理,但没有返回文字内容。", message.channel, capabilities);
197
+ const outgoing = chunks.map((text) => ({ text }));
198
+ const last = outgoing.at(-1);
199
+ const button = normalizePetReplyButton(result.button);
200
+ if (button)
201
+ last.button = button;
202
+ if (result.attachments?.length && capabilities.outbound.attachments.length > 0) {
203
+ try {
204
+ const supported = result.attachments.filter((attachment) => capabilities.outbound.attachments.includes(attachment.kind));
205
+ const materialized = await materializeOutgoingAttachments(supported);
206
+ if (materialized.length > 0)
207
+ last.attachments = materialized;
208
+ }
209
+ catch {
210
+ // A stale or invalid host file must not drop Mimi's text reply.
211
+ }
212
+ }
213
+ return outgoing;
214
+ }
215
+ function gatewayChannelCatalog(currentChannel, currentCapabilities, adapters) {
216
+ const channels = new Map();
217
+ for (const adapter of adapters) {
218
+ if (!channels.has(adapter.channel)) {
219
+ channels.set(adapter.channel, channelCapabilities(adapter));
220
+ }
221
+ }
222
+ if (!channels.has(currentChannel) && channels.size >= 32) {
223
+ channels.delete([...channels.keys()].at(-1));
224
+ }
225
+ // The exact adapter that admitted this message is authoritative when several
226
+ // accounts or compatibility adapters share one channel name.
227
+ channels.set(currentChannel, currentCapabilities);
228
+ return [...channels].slice(0, 32).map(([channel, declared]) => ({
229
+ channel,
230
+ capabilities: declared,
231
+ }));
232
+ }
233
+ function normalizePetReplyButton(value) {
234
+ if (!value || typeof value !== "object" || Array.isArray(value))
235
+ return undefined;
236
+ const record = value;
237
+ if (Object.keys(record).some((key) => key !== "text" && key !== "url") ||
238
+ typeof record.text !== "string" ||
239
+ !record.text.trim() ||
240
+ record.text.length > 80 ||
241
+ /[\u0000-\u001f\u007f]/u.test(record.text) ||
242
+ typeof record.url !== "string" ||
243
+ record.url.length > 2_048 ||
244
+ record.url !== record.url.trim()) {
245
+ return undefined;
246
+ }
247
+ try {
248
+ const url = new URL(record.url);
249
+ if (url.protocol !== "https:" && url.protocol !== "http:")
250
+ return undefined;
251
+ }
252
+ catch {
253
+ return undefined;
254
+ }
255
+ return { text: record.text.trim(), url: record.url };
256
+ }
257
+ function mimiReplyCacheKey(message) {
258
+ if (!message.messageId)
259
+ return undefined;
260
+ return [message.channel, message.target, message.senderId, message.messageId].join("\0");
261
+ }
262
+ /** Render the one-time pairing URL as a PNG QR code; failures degrade to text+button. */
263
+ async function renderPairingQrAttachment(pairingUrl) {
264
+ try {
265
+ const { default: QRCode } = await import("qrcode");
266
+ const data = await QRCode.toBuffer(pairingUrl, { type: "png", width: 512, margin: 2 });
267
+ return { kind: "image", name: "pairing-qr.png", mimeType: "image/png", data };
268
+ }
269
+ catch {
270
+ return undefined;
271
+ }
272
+ }
273
+ function formatStatus(status) {
274
+ const tunnel = status.tunnelConnected
275
+ ? "已连接"
276
+ : status.tunnelRunning
277
+ ? "连接中/异常"
278
+ : "未运行";
279
+ return [
280
+ "桌面端:在线",
281
+ `手机服务:${status.running ? "运行中" : "未运行"}`,
282
+ `公网隧道:${tunnel}`,
283
+ `访问口令:${status.passcodeSet ? "已设置" : "未设置"}`,
284
+ `在线设备:${status.onlineDeviceCount}`,
285
+ ...(status.url ? [`当前地址:${status.url}`] : []),
286
+ ].join("\n");
287
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./chat-gateway.js";
2
+ export * from "./channel.js";
3
+ export * from "./attachments.js";
4
+ export * from "./media.js";
5
+ export * from "./lifecycle.js";
6
+ export * from "./webhook.js";
7
+ export * from "./delivery-queue.js";
8
+ export * from "./instance-lock.js";
9
+ export * from "./notification-relay.js";
10
+ export * from "./notification-progress.js";
11
+ export * from "./service-manager.js";
12
+ export * from "./ingress.js";
13
+ export * from "./platform-canary.js";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./chat-gateway.js";
2
+ export * from "./channel.js";
3
+ export * from "./attachments.js";
4
+ export * from "./media.js";
5
+ export * from "./lifecycle.js";
6
+ export * from "./webhook.js";
7
+ export * from "./delivery-queue.js";
8
+ export * from "./instance-lock.js";
9
+ export * from "./notification-relay.js";
10
+ export * from "./notification-progress.js";
11
+ export * from "./service-manager.js";
12
+ export * from "./ingress.js";
13
+ export * from "./platform-canary.js";
@@ -0,0 +1,8 @@
1
+ export declare const CHAT_WEBHOOK_PATHS: readonly ["/webhooks/line", "/webhooks/whatsapp", "/webhooks/teams"];
2
+ export type IngressFormat = "caddy" | "nginx";
3
+ export declare function renderWebhookIngress(input: {
4
+ format: IngressFormat;
5
+ publicHost: string;
6
+ upstream?: string;
7
+ maxBodyBytes?: number;
8
+ }): string;
@@ -0,0 +1,61 @@
1
+ export const CHAT_WEBHOOK_PATHS = [
2
+ "/webhooks/line",
3
+ "/webhooks/whatsapp",
4
+ "/webhooks/teams",
5
+ ];
6
+ export function renderWebhookIngress(input) {
7
+ const host = validateHost(input.publicHost);
8
+ const upstream = validateUpstream(input.upstream ?? "127.0.0.1:8787");
9
+ const maxBodyBytes = input.maxBodyBytes ?? 1_048_576;
10
+ if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) {
11
+ throw new Error("maxBodyBytes must be a positive integer");
12
+ }
13
+ if (input.format === "caddy") {
14
+ return `${host} {
15
+ request_body {
16
+ max_size ${maxBodyBytes}
17
+ }
18
+ @chat_webhooks path ${CHAT_WEBHOOK_PATHS.join(" ")}
19
+ reverse_proxy @chat_webhooks ${upstream}
20
+ respond /healthz 404
21
+ respond 404
22
+ }
23
+ `;
24
+ }
25
+ return `server {
26
+ listen 443 ssl http2;
27
+ server_name ${host};
28
+ client_max_body_size ${maxBodyBytes};
29
+
30
+ # Configure ssl_certificate and ssl_certificate_key using your ACME client.
31
+ location ~ ^/webhooks/(line|whatsapp|teams)$ {
32
+ proxy_pass http://${upstream};
33
+ proxy_set_header Host $host;
34
+ proxy_set_header X-Forwarded-Proto https;
35
+ proxy_request_buffering on;
36
+ }
37
+
38
+ location / { return 404; }
39
+ }
40
+ `;
41
+ }
42
+ function validateHost(value) {
43
+ const host = value.trim().toLowerCase();
44
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(host)) {
45
+ throw new Error("public host must be a DNS hostname");
46
+ }
47
+ return host;
48
+ }
49
+ function validateUpstream(value) {
50
+ const url = new URL(`http://${value}`);
51
+ if (!["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname) ||
52
+ !url.port ||
53
+ url.pathname !== "/" ||
54
+ url.username ||
55
+ url.password ||
56
+ url.search ||
57
+ url.hash) {
58
+ throw new Error("webhook upstream must be a loopback host:port");
59
+ }
60
+ return value;
61
+ }
@@ -0,0 +1,24 @@
1
+ interface GatewayLockRecord {
2
+ version: 1;
3
+ pid: number;
4
+ owner: string;
5
+ token: string;
6
+ startedAt: number;
7
+ }
8
+ export interface GatewayInstanceLease {
9
+ readonly path: string;
10
+ readonly owner: string;
11
+ release(): void;
12
+ }
13
+ export declare class GatewayAlreadyRunningError extends Error {
14
+ readonly path: string;
15
+ readonly current?: Readonly<Pick<GatewayLockRecord, "pid" | "owner" | "startedAt">> | undefined;
16
+ constructor(path: string, current?: Readonly<Pick<GatewayLockRecord, "pid" | "owner" | "startedAt">> | undefined);
17
+ }
18
+ /**
19
+ * Cross-process single-owner lease shared by the CLI and Desktop lifecycle.
20
+ * A dead owner's lock is reclaimed, while a live or unreadable lock fails
21
+ * closed so two processes can never consume the same platform updates.
22
+ */
23
+ export declare function acquireGatewayInstanceLock(path: string, owner: string): GatewayInstanceLease;
24
+ export {};
@@ -0,0 +1,100 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ export class GatewayAlreadyRunningError extends Error {
5
+ path;
6
+ current;
7
+ constructor(path, current) {
8
+ super(current
9
+ ? `Chat Gateway 已由 ${current.owner} 运行(PID ${current.pid})`
10
+ : `Chat Gateway 已在运行(锁文件:${path})`);
11
+ this.path = path;
12
+ this.current = current;
13
+ this.name = "GatewayAlreadyRunningError";
14
+ }
15
+ }
16
+ /**
17
+ * Cross-process single-owner lease shared by the CLI and Desktop lifecycle.
18
+ * A dead owner's lock is reclaimed, while a live or unreadable lock fails
19
+ * closed so two processes can never consume the same platform updates.
20
+ */
21
+ export function acquireGatewayInstanceLock(path, owner) {
22
+ if (!path || !owner)
23
+ throw new Error("gateway lock path and owner are required");
24
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
25
+ const record = {
26
+ version: 1,
27
+ pid: process.pid,
28
+ owner,
29
+ token: randomUUID(),
30
+ startedAt: Date.now(),
31
+ };
32
+ for (let attempt = 0; attempt < 3; attempt++) {
33
+ let fd;
34
+ try {
35
+ fd = openSync(path, "wx", 0o600);
36
+ writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf-8");
37
+ closeSync(fd);
38
+ fd = undefined;
39
+ if (process.platform !== "win32")
40
+ chmodSync(path, 0o600);
41
+ let released = false;
42
+ return {
43
+ path,
44
+ owner,
45
+ release: () => {
46
+ if (released)
47
+ return;
48
+ released = true;
49
+ removeOwnedLock(path, record.token);
50
+ },
51
+ };
52
+ }
53
+ catch (error) {
54
+ if (fd !== undefined)
55
+ closeSync(fd);
56
+ if (error.code !== "EEXIST")
57
+ throw error;
58
+ const existing = readLock(path);
59
+ if (!existing || isProcessAlive(existing.pid)) {
60
+ throw new GatewayAlreadyRunningError(path, existing
61
+ ? { pid: existing.pid, owner: existing.owner, startedAt: existing.startedAt }
62
+ : undefined);
63
+ }
64
+ // Only reclaim a well-formed lock whose process is demonstrably gone.
65
+ removeOwnedLock(path, existing.token);
66
+ }
67
+ }
68
+ throw new GatewayAlreadyRunningError(path);
69
+ }
70
+ function readLock(path) {
71
+ try {
72
+ const value = JSON.parse(readFileSync(path, "utf-8"));
73
+ if (value.version !== 1 ||
74
+ !Number.isSafeInteger(value.pid) ||
75
+ (value.pid ?? 0) <= 0 ||
76
+ typeof value.owner !== "string" ||
77
+ typeof value.token !== "string" ||
78
+ typeof value.startedAt !== "number") {
79
+ return undefined;
80
+ }
81
+ return value;
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function isProcessAlive(pid) {
88
+ try {
89
+ process.kill(pid, 0);
90
+ return true;
91
+ }
92
+ catch (error) {
93
+ return error.code === "EPERM";
94
+ }
95
+ }
96
+ function removeOwnedLock(path, token) {
97
+ const current = readLock(path);
98
+ if (current?.token === token)
99
+ rmSync(path, { force: true });
100
+ }
package/dist/lark.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { ChannelAdapter, ChannelMessageHandler, OutgoingMessage } from "./channel.js";
2
+ export interface LarkAdapterConfig {
3
+ appId: string;
4
+ appSecret: string;
5
+ domain?: string;
6
+ }
7
+ export declare class LarkAdapter implements ChannelAdapter {
8
+ readonly channel = "lark";
9
+ readonly capabilities: import("./channel.js").ChannelCapabilities;
10
+ private readonly client;
11
+ private readonly ws;
12
+ private readonly delivery;
13
+ constructor(config: LarkAdapterConfig);
14
+ run(handler: ChannelMessageHandler, signal: AbortSignal): Promise<void>;
15
+ send(target: string, message: OutgoingMessage): Promise<void>;
16
+ private toAttachment;
17
+ private loadMessageResource;
18
+ private sendAttachment;
19
+ private createMessage;
20
+ }