@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.
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/dist/adapter-factory.d.ts +30 -0
- package/dist/adapter-factory.js +198 -0
- package/dist/attachments.d.ts +13 -0
- package/dist/attachments.js +74 -0
- package/dist/channel.d.ts +112 -0
- package/dist/channel.js +60 -0
- package/dist/chat-gateway.d.ts +58 -0
- package/dist/chat-gateway.js +232 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +259 -0
- package/dist/codeshell.d.ts +6 -0
- package/dist/codeshell.js +6 -0
- package/dist/config.d.ts +125 -0
- package/dist/config.js +469 -0
- package/dist/delivery-queue.d.ts +93 -0
- package/dist/delivery-queue.js +401 -0
- package/dist/desktop-control-client.d.ts +51 -0
- package/dist/desktop-control-client.js +309 -0
- package/dist/dingtalk.d.ts +19 -0
- package/dist/dingtalk.js +93 -0
- package/dist/discord.d.ts +23 -0
- package/dist/discord.js +134 -0
- package/dist/gateway.d.ts +34 -0
- package/dist/gateway.js +287 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/ingress.d.ts +8 -0
- package/dist/ingress.js +61 -0
- package/dist/instance-lock.d.ts +24 -0
- package/dist/instance-lock.js +100 -0
- package/dist/lark.d.ts +20 -0
- package/dist/lark.js +184 -0
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +16 -0
- package/dist/line.d.ts +17 -0
- package/dist/line.js +123 -0
- package/dist/matrix.d.ts +21 -0
- package/dist/matrix.js +208 -0
- package/dist/mattermost.d.ts +18 -0
- package/dist/mattermost.js +170 -0
- package/dist/media.d.ts +39 -0
- package/dist/media.js +223 -0
- package/dist/notification-progress.d.ts +46 -0
- package/dist/notification-progress.js +299 -0
- package/dist/notification-relay.d.ts +43 -0
- package/dist/notification-relay.js +285 -0
- package/dist/platform-canary.d.ts +22 -0
- package/dist/platform-canary.js +55 -0
- package/dist/protocol.d.ts +102 -0
- package/dist/protocol.js +1 -0
- package/dist/service-manager.d.ts +47 -0
- package/dist/service-manager.js +222 -0
- package/dist/slack.d.ts +17 -0
- package/dist/slack.js +136 -0
- package/dist/teams.d.ts +26 -0
- package/dist/teams.js +193 -0
- package/dist/telegram.d.ts +33 -0
- package/dist/telegram.js +263 -0
- package/dist/webhook.d.ts +26 -0
- package/dist/webhook.js +91 -0
- package/dist/wechat-login.d.ts +21 -0
- package/dist/wechat-login.js +97 -0
- package/dist/wechat-storage.d.ts +61 -0
- package/dist/wechat-storage.js +251 -0
- package/dist/wechat.d.ts +129 -0
- package/dist/wechat.js +1082 -0
- package/dist/wecom.d.ts +16 -0
- package/dist/wecom.js +149 -0
- package/dist/whatsapp.d.ts +27 -0
- package/dist/whatsapp.js +218 -0
- package/package.json +123 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { defaultWechatDataDirectory, FileWechatCredentialStore } from "./wechat-storage.js";
|
|
5
|
+
export function defaultGatewayConfigPath(env = process.env) {
|
|
6
|
+
return join(resolveHome(env), ".code-shell", "im-gateway", "config.json");
|
|
7
|
+
}
|
|
8
|
+
export function defaultDesktopControlDescriptorPath(env = process.env) {
|
|
9
|
+
return join(resolveHome(env), ".code-shell", "im-gateway", "desktop-control.json");
|
|
10
|
+
}
|
|
11
|
+
/** Safe, disabled-by-default starter file used by the Desktop Link editor. */
|
|
12
|
+
export function gatewayConfigTemplate() {
|
|
13
|
+
return {
|
|
14
|
+
_help: "Set enabled=true for the channels you want, fill their credentials and allowlists, then start Chat Gateway in CodeShell Desktop.",
|
|
15
|
+
telegram: {
|
|
16
|
+
enabled: false,
|
|
17
|
+
botToken: "",
|
|
18
|
+
allowedChatIds: [],
|
|
19
|
+
allowedUserIds: [],
|
|
20
|
+
},
|
|
21
|
+
discord: {
|
|
22
|
+
enabled: false,
|
|
23
|
+
botToken: "",
|
|
24
|
+
allowedChannelIds: [],
|
|
25
|
+
allowedUserIds: [],
|
|
26
|
+
},
|
|
27
|
+
slack: {
|
|
28
|
+
enabled: false,
|
|
29
|
+
botToken: "",
|
|
30
|
+
appToken: "",
|
|
31
|
+
allowedChannelIds: [],
|
|
32
|
+
allowedUserIds: [],
|
|
33
|
+
},
|
|
34
|
+
lark: {
|
|
35
|
+
enabled: false,
|
|
36
|
+
appId: "",
|
|
37
|
+
appSecret: "",
|
|
38
|
+
allowedChatIds: [],
|
|
39
|
+
allowedUserIds: [],
|
|
40
|
+
},
|
|
41
|
+
dingtalk: {
|
|
42
|
+
enabled: false,
|
|
43
|
+
clientId: "",
|
|
44
|
+
clientSecret: "",
|
|
45
|
+
allowedConversationIds: [],
|
|
46
|
+
allowedUserIds: [],
|
|
47
|
+
},
|
|
48
|
+
wecom: {
|
|
49
|
+
enabled: false,
|
|
50
|
+
botId: "",
|
|
51
|
+
secret: "",
|
|
52
|
+
allowedChatIds: [],
|
|
53
|
+
allowedUserIds: [],
|
|
54
|
+
},
|
|
55
|
+
wechat: { enabled: false },
|
|
56
|
+
matrix: {
|
|
57
|
+
enabled: false,
|
|
58
|
+
homeserverUrl: "",
|
|
59
|
+
accessToken: "",
|
|
60
|
+
botUserId: "",
|
|
61
|
+
allowedRoomIds: [],
|
|
62
|
+
allowedUserIds: [],
|
|
63
|
+
},
|
|
64
|
+
mattermost: {
|
|
65
|
+
enabled: false,
|
|
66
|
+
serverUrl: "",
|
|
67
|
+
botToken: "",
|
|
68
|
+
botUserId: "",
|
|
69
|
+
allowedChannelIds: [],
|
|
70
|
+
allowedUserIds: [],
|
|
71
|
+
},
|
|
72
|
+
line: {
|
|
73
|
+
enabled: false,
|
|
74
|
+
channelSecret: "",
|
|
75
|
+
channelAccessToken: "",
|
|
76
|
+
allowedTargetIds: [],
|
|
77
|
+
allowedUserIds: [],
|
|
78
|
+
},
|
|
79
|
+
whatsapp: {
|
|
80
|
+
enabled: false,
|
|
81
|
+
accessToken: "",
|
|
82
|
+
appSecret: "",
|
|
83
|
+
verifyToken: "",
|
|
84
|
+
phoneNumberId: "",
|
|
85
|
+
allowedPhoneNumbers: [],
|
|
86
|
+
},
|
|
87
|
+
teams: {
|
|
88
|
+
enabled: false,
|
|
89
|
+
appId: "",
|
|
90
|
+
appPassword: "",
|
|
91
|
+
appType: "MultiTenant",
|
|
92
|
+
allowedConversationIds: [],
|
|
93
|
+
allowedUserIds: [],
|
|
94
|
+
},
|
|
95
|
+
webhook: { host: "127.0.0.1", port: 8787, maxBodyBytes: 1048576 },
|
|
96
|
+
runtime: {
|
|
97
|
+
maxPending: 1000,
|
|
98
|
+
maxConcurrent: 4,
|
|
99
|
+
maxPerTarget: 1,
|
|
100
|
+
maxMessagesPerUserPerMinute: 20,
|
|
101
|
+
},
|
|
102
|
+
notifications: {
|
|
103
|
+
enabled: false,
|
|
104
|
+
_help: "Optional proactive Desktop/tunnel event targets, keyed by channel.",
|
|
105
|
+
targets: {},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function loadGatewayConfig(opts = {}) {
|
|
110
|
+
const env = opts.env ?? process.env;
|
|
111
|
+
const platform = opts.platform ?? process.platform;
|
|
112
|
+
const configPath = resolve(opts.configPath ?? env.CODE_SHELL_IM_GATEWAY_CONFIG ?? defaultGatewayConfigPath(env));
|
|
113
|
+
const raw = readRawConfig(configPath);
|
|
114
|
+
if (existsSync(configPath) && platform !== "win32") {
|
|
115
|
+
const mode = statSync(configPath).mode & 0o777;
|
|
116
|
+
if ((mode & 0o077) !== 0) {
|
|
117
|
+
throw new Error(`IM gateway 配置权限必须为 0600:${configPath}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const channels = loadChannels(raw, env);
|
|
121
|
+
if (channels.length === 0) {
|
|
122
|
+
throw new Error("至少配置一个 IM channel;当前支持 Telegram、Discord、Slack、飞书、钉钉、个人微信、企业微信、Matrix、Mattermost、LINE、WhatsApp、Teams");
|
|
123
|
+
}
|
|
124
|
+
const configuredCommand = readNonEmptyString(raw.desktop?.command);
|
|
125
|
+
const configuredArgs = readStringList(raw.desktop?.args, false);
|
|
126
|
+
const defaultLaunch = defaultDesktopLaunch(platform);
|
|
127
|
+
const config = {
|
|
128
|
+
channels,
|
|
129
|
+
desktop: {
|
|
130
|
+
descriptorPath: readNonEmptyString(raw.desktop?.descriptorPath) ?? defaultDesktopControlDescriptorPath(env),
|
|
131
|
+
autoLaunch: raw.desktop?.autoLaunch !== false,
|
|
132
|
+
command: configuredCommand ?? defaultLaunch.command,
|
|
133
|
+
args: configuredArgs ?? (configuredCommand ? [] : defaultLaunch.args),
|
|
134
|
+
startupTimeoutMs: readPositiveNumber(raw.desktop?.startupTimeoutMs) ?? 30_000,
|
|
135
|
+
},
|
|
136
|
+
webhook: {
|
|
137
|
+
host: readNonEmptyString(raw.webhook?.host) ?? "127.0.0.1",
|
|
138
|
+
port: readPort(raw.webhook?.port) ?? 8787,
|
|
139
|
+
maxBodyBytes: readPositiveNumber(raw.webhook?.maxBodyBytes) ?? 1_048_576,
|
|
140
|
+
},
|
|
141
|
+
runtime: {
|
|
142
|
+
lockPath: readNonEmptyString(raw.runtime?.lockPath) ??
|
|
143
|
+
join(resolveHome(env), ".code-shell", "im-gateway", "gateway.lock"),
|
|
144
|
+
inboxPath: readNonEmptyString(raw.runtime?.inboxPath) ??
|
|
145
|
+
join(resolveHome(env), ".code-shell", "im-gateway", "inbox.json"),
|
|
146
|
+
eventCursorPath: readNonEmptyString(raw.runtime?.eventCursorPath) ??
|
|
147
|
+
join(resolveHome(env), ".code-shell", "im-gateway", "desktop-events.json"),
|
|
148
|
+
maxPending: readPositiveInteger(raw.runtime?.maxPending) ?? 1_000,
|
|
149
|
+
maxConcurrent: readPositiveInteger(raw.runtime?.maxConcurrent) ?? 4,
|
|
150
|
+
maxPerTarget: readPositiveInteger(raw.runtime?.maxPerTarget) ?? 1,
|
|
151
|
+
maxMessagesPerUserPerMinute: readPositiveInteger(raw.runtime?.maxMessagesPerUserPerMinute) ?? 20,
|
|
152
|
+
adapterRestartBaseMs: readPositiveInteger(raw.runtime?.adapterRestartBaseMs) ?? 1_000,
|
|
153
|
+
adapterRestartMaxMs: readPositiveInteger(raw.runtime?.adapterRestartMaxMs) ?? 30_000,
|
|
154
|
+
},
|
|
155
|
+
notifications: [],
|
|
156
|
+
};
|
|
157
|
+
config.notifications = loadNotificationTargets(raw.notifications, channels);
|
|
158
|
+
return config;
|
|
159
|
+
}
|
|
160
|
+
function loadNotificationTargets(raw, channels) {
|
|
161
|
+
if (!raw || raw.enabled === false)
|
|
162
|
+
return [];
|
|
163
|
+
const targets = raw.targets;
|
|
164
|
+
if (!targets || typeof targets !== "object" || Array.isArray(targets)) {
|
|
165
|
+
throw new Error("notifications.targets 必须是 channel 到 target 数组的映射");
|
|
166
|
+
}
|
|
167
|
+
const result = [];
|
|
168
|
+
for (const [channelName, values] of Object.entries(targets)) {
|
|
169
|
+
const channel = channels.find(({ channel }) => channel === channelName);
|
|
170
|
+
if (!channel)
|
|
171
|
+
throw new Error(`notifications 引用了未启用的 channel:${channelName}`);
|
|
172
|
+
for (const target of readStringList(values)) {
|
|
173
|
+
if (!channel.allowedTargetIds.includes(target)) {
|
|
174
|
+
throw new Error(`notifications target 不在 ${channelName} 会话白名单中:${target}`);
|
|
175
|
+
}
|
|
176
|
+
result.push({ channel: channel.channel, target });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (result.length === 0)
|
|
180
|
+
throw new Error("notifications.enabled=true 时至少配置一个 target");
|
|
181
|
+
if (result.length > 32)
|
|
182
|
+
throw new Error("notifications targets 最多配置 32 个");
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
function loadChannels(raw, env) {
|
|
186
|
+
return [
|
|
187
|
+
loadTelegram(raw.telegram, env),
|
|
188
|
+
loadDiscord(raw.discord, env),
|
|
189
|
+
loadSlack(raw.slack, env),
|
|
190
|
+
loadLark(raw.lark, env),
|
|
191
|
+
loadDingTalk(raw.dingtalk, env),
|
|
192
|
+
loadWeCom(raw.wecom, env),
|
|
193
|
+
loadWechat(raw.wechat, env),
|
|
194
|
+
loadMatrix(raw.matrix, env),
|
|
195
|
+
loadMattermost(raw.mattermost, env),
|
|
196
|
+
loadLine(raw.line, env),
|
|
197
|
+
loadWhatsApp(raw.whatsapp, env),
|
|
198
|
+
loadTeams(raw.teams, env),
|
|
199
|
+
].filter((channel) => channel !== undefined);
|
|
200
|
+
}
|
|
201
|
+
function loadTelegram(raw, env) {
|
|
202
|
+
const botToken = envString(env, "CODE_SHELL_TELEGRAM_BOT_TOKEN") ?? readNonEmptyString(raw?.botToken);
|
|
203
|
+
if (!sectionEnabled(raw, botToken))
|
|
204
|
+
return undefined;
|
|
205
|
+
return {
|
|
206
|
+
channel: "telegram",
|
|
207
|
+
botToken: requireValue("Telegram", "botToken", botToken),
|
|
208
|
+
...loadAllowlist("Telegram", raw, env, "CODE_SHELL_TELEGRAM_ALLOWED_CHAT_IDS", "CODE_SHELL_TELEGRAM_ALLOWED_USER_IDS", "allowedChatIds"),
|
|
209
|
+
apiBaseUrl: readNonEmptyString(raw?.apiBaseUrl)?.replace(/\/$/, "") ?? "https://api.telegram.org",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function loadDiscord(raw, env) {
|
|
213
|
+
const botToken = envString(env, "CODE_SHELL_DISCORD_BOT_TOKEN") ?? readNonEmptyString(raw?.botToken);
|
|
214
|
+
if (!sectionEnabled(raw, botToken))
|
|
215
|
+
return undefined;
|
|
216
|
+
return {
|
|
217
|
+
channel: "discord",
|
|
218
|
+
botToken: requireValue("Discord", "botToken", botToken),
|
|
219
|
+
...loadAllowlist("Discord", raw, env, "CODE_SHELL_DISCORD_ALLOWED_CHANNEL_IDS", "CODE_SHELL_DISCORD_ALLOWED_USER_IDS", "allowedChannelIds"),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function loadSlack(raw, env) {
|
|
223
|
+
const botToken = envString(env, "CODE_SHELL_SLACK_BOT_TOKEN") ?? readNonEmptyString(raw?.botToken);
|
|
224
|
+
const appToken = envString(env, "CODE_SHELL_SLACK_APP_TOKEN") ?? readNonEmptyString(raw?.appToken);
|
|
225
|
+
if (!sectionEnabled(raw, botToken, appToken))
|
|
226
|
+
return undefined;
|
|
227
|
+
return {
|
|
228
|
+
channel: "slack",
|
|
229
|
+
botToken: requireValue("Slack", "botToken", botToken),
|
|
230
|
+
appToken: requireValue("Slack", "appToken", appToken),
|
|
231
|
+
...loadAllowlist("Slack", raw, env, "CODE_SHELL_SLACK_ALLOWED_CHANNEL_IDS", "CODE_SHELL_SLACK_ALLOWED_USER_IDS", "allowedChannelIds"),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function loadLark(raw, env) {
|
|
235
|
+
const appId = envString(env, "CODE_SHELL_LARK_APP_ID") ?? readNonEmptyString(raw?.appId);
|
|
236
|
+
const appSecret = envString(env, "CODE_SHELL_LARK_APP_SECRET") ?? readNonEmptyString(raw?.appSecret);
|
|
237
|
+
if (!sectionEnabled(raw, appId, appSecret))
|
|
238
|
+
return undefined;
|
|
239
|
+
return {
|
|
240
|
+
channel: "lark",
|
|
241
|
+
appId: requireValue("飞书/Lark", "appId", appId),
|
|
242
|
+
appSecret: requireValue("飞书/Lark", "appSecret", appSecret),
|
|
243
|
+
domain: envString(env, "CODE_SHELL_LARK_DOMAIN") ?? readNonEmptyString(raw?.domain),
|
|
244
|
+
...loadAllowlist("飞书/Lark", raw, env, "CODE_SHELL_LARK_ALLOWED_CHAT_IDS", "CODE_SHELL_LARK_ALLOWED_USER_IDS", "allowedChatIds"),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function loadDingTalk(raw, env) {
|
|
248
|
+
const clientId = envString(env, "CODE_SHELL_DINGTALK_CLIENT_ID") ?? readNonEmptyString(raw?.clientId);
|
|
249
|
+
const clientSecret = envString(env, "CODE_SHELL_DINGTALK_CLIENT_SECRET") ?? readNonEmptyString(raw?.clientSecret);
|
|
250
|
+
if (!sectionEnabled(raw, clientId, clientSecret))
|
|
251
|
+
return undefined;
|
|
252
|
+
return {
|
|
253
|
+
channel: "dingtalk",
|
|
254
|
+
clientId: requireValue("钉钉", "clientId", clientId),
|
|
255
|
+
clientSecret: requireValue("钉钉", "clientSecret", clientSecret),
|
|
256
|
+
...loadAllowlist("钉钉", raw, env, "CODE_SHELL_DINGTALK_ALLOWED_CONVERSATION_IDS", "CODE_SHELL_DINGTALK_ALLOWED_USER_IDS", "allowedConversationIds"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function loadWeCom(raw, env) {
|
|
260
|
+
const botId = envString(env, "CODE_SHELL_WECOM_BOT_ID") ?? readNonEmptyString(raw?.botId);
|
|
261
|
+
const secret = envString(env, "CODE_SHELL_WECOM_SECRET") ?? readNonEmptyString(raw?.secret);
|
|
262
|
+
if (!sectionEnabled(raw, botId, secret))
|
|
263
|
+
return undefined;
|
|
264
|
+
return {
|
|
265
|
+
channel: "wecom",
|
|
266
|
+
botId: requireValue("企业微信", "botId", botId),
|
|
267
|
+
secret: requireValue("企业微信", "secret", secret),
|
|
268
|
+
...loadAllowlist("企业微信", raw, env, "CODE_SHELL_WECOM_ALLOWED_CHAT_IDS", "CODE_SHELL_WECOM_ALLOWED_USER_IDS", "allowedChatIds"),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function loadWechat(raw, env) {
|
|
272
|
+
const explicitAccountId = envString(env, "CODE_SHELL_WECHAT_ACCOUNT_ID") ?? readNonEmptyString(raw?.accountId);
|
|
273
|
+
const explicitToken = envString(env, "CODE_SHELL_WECHAT_BOT_TOKEN") ?? readNonEmptyString(raw?.botToken);
|
|
274
|
+
const configuredDirectory = envString(env, "CODE_SHELL_WECHAT_CREDENTIALS_DIR") ?? readNonEmptyString(raw?.credentialsDir);
|
|
275
|
+
if (!sectionEnabled(raw, explicitAccountId, explicitToken, configuredDirectory))
|
|
276
|
+
return undefined;
|
|
277
|
+
const credentialsDir = resolve(configuredDirectory ?? defaultWechatDataDirectory(env));
|
|
278
|
+
const store = new FileWechatCredentialStore(credentialsDir);
|
|
279
|
+
const stored = store.load(explicitAccountId);
|
|
280
|
+
const accountId = explicitAccountId ?? stored?.accountId;
|
|
281
|
+
const token = explicitToken ?? stored?.token;
|
|
282
|
+
if (!accountId || !token) {
|
|
283
|
+
throw new Error("个人微信未登录:请先执行 code-shell-chat wechat login");
|
|
284
|
+
}
|
|
285
|
+
const defaultOwner = stored?.userId ? [stored.userId] : [];
|
|
286
|
+
const allowedTargetIds = readCsvOverride(env.CODE_SHELL_WECHAT_ALLOWED_USER_IDS) ??
|
|
287
|
+
readStringList(raw?.allowedUserIds, false) ??
|
|
288
|
+
defaultOwner;
|
|
289
|
+
if (allowedTargetIds.length === 0) {
|
|
290
|
+
throw new Error("个人微信至少配置 allowedUserIds,或重新扫码以记录管理员身份");
|
|
291
|
+
}
|
|
292
|
+
const allowedUserIds = readCsvOverride(env.CODE_SHELL_WECHAT_ALLOWED_USER_IDS) ??
|
|
293
|
+
readStringList(raw?.allowedUserIds, false) ??
|
|
294
|
+
defaultOwner;
|
|
295
|
+
return {
|
|
296
|
+
channel: "wechat",
|
|
297
|
+
accountId,
|
|
298
|
+
token,
|
|
299
|
+
baseUrl: envString(env, "CODE_SHELL_WECHAT_BASE_URL") ??
|
|
300
|
+
readNonEmptyString(raw?.baseUrl) ??
|
|
301
|
+
stored?.baseUrl ??
|
|
302
|
+
"https://ilinkai.weixin.qq.com",
|
|
303
|
+
botAgent: envString(env, "CODE_SHELL_WECHAT_BOT_AGENT") ?? readNonEmptyString(raw?.botAgent),
|
|
304
|
+
protocolVersion: envString(env, "CODE_SHELL_WECHAT_PROTOCOL_VERSION") ??
|
|
305
|
+
readNonEmptyString(raw?.protocolVersion),
|
|
306
|
+
allowUnsafeBaseUrl: raw?.allowUnsafeBaseUrl === true,
|
|
307
|
+
extraCdnDownloadHosts: readCsvOverride(env.CODE_SHELL_WECHAT_EXTRA_CDN_DOWNLOAD_HOSTS) ??
|
|
308
|
+
readStringList(raw?.extraCdnDownloadHosts, false),
|
|
309
|
+
credentialsDir,
|
|
310
|
+
statePath: store.statePath(accountId),
|
|
311
|
+
allowedTargetIds,
|
|
312
|
+
allowedUserIds,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function loadMatrix(raw, env) {
|
|
316
|
+
const homeserverUrl = envString(env, "CODE_SHELL_MATRIX_HOMESERVER_URL") ?? readNonEmptyString(raw?.homeserverUrl);
|
|
317
|
+
const accessToken = envString(env, "CODE_SHELL_MATRIX_ACCESS_TOKEN") ?? readNonEmptyString(raw?.accessToken);
|
|
318
|
+
if (!sectionEnabled(raw, homeserverUrl, accessToken))
|
|
319
|
+
return undefined;
|
|
320
|
+
return {
|
|
321
|
+
channel: "matrix",
|
|
322
|
+
homeserverUrl: requireValue("Matrix", "homeserverUrl", homeserverUrl).replace(/\/$/, ""),
|
|
323
|
+
accessToken: requireValue("Matrix", "accessToken", accessToken),
|
|
324
|
+
botUserId: envString(env, "CODE_SHELL_MATRIX_BOT_USER_ID") ?? readNonEmptyString(raw?.botUserId),
|
|
325
|
+
...loadAllowlist("Matrix", raw, env, "CODE_SHELL_MATRIX_ALLOWED_ROOM_IDS", "CODE_SHELL_MATRIX_ALLOWED_USER_IDS", "allowedRoomIds"),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function loadMattermost(raw, env) {
|
|
329
|
+
const serverUrl = envString(env, "CODE_SHELL_MATTERMOST_SERVER_URL") ?? readNonEmptyString(raw?.serverUrl);
|
|
330
|
+
const botToken = envString(env, "CODE_SHELL_MATTERMOST_BOT_TOKEN") ?? readNonEmptyString(raw?.botToken);
|
|
331
|
+
if (!sectionEnabled(raw, serverUrl, botToken))
|
|
332
|
+
return undefined;
|
|
333
|
+
return {
|
|
334
|
+
channel: "mattermost",
|
|
335
|
+
serverUrl: requireValue("Mattermost", "serverUrl", serverUrl).replace(/\/$/, ""),
|
|
336
|
+
botToken: requireValue("Mattermost", "botToken", botToken),
|
|
337
|
+
botUserId: envString(env, "CODE_SHELL_MATTERMOST_BOT_USER_ID") ?? readNonEmptyString(raw?.botUserId),
|
|
338
|
+
...loadAllowlist("Mattermost", raw, env, "CODE_SHELL_MATTERMOST_ALLOWED_CHANNEL_IDS", "CODE_SHELL_MATTERMOST_ALLOWED_USER_IDS", "allowedChannelIds"),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function loadLine(raw, env) {
|
|
342
|
+
const channelSecret = envString(env, "CODE_SHELL_LINE_CHANNEL_SECRET") ?? readNonEmptyString(raw?.channelSecret);
|
|
343
|
+
const channelAccessToken = envString(env, "CODE_SHELL_LINE_CHANNEL_ACCESS_TOKEN") ??
|
|
344
|
+
readNonEmptyString(raw?.channelAccessToken);
|
|
345
|
+
if (!sectionEnabled(raw, channelSecret, channelAccessToken))
|
|
346
|
+
return undefined;
|
|
347
|
+
return {
|
|
348
|
+
channel: "line",
|
|
349
|
+
channelSecret: requireValue("LINE", "channelSecret", channelSecret),
|
|
350
|
+
channelAccessToken: requireValue("LINE", "channelAccessToken", channelAccessToken),
|
|
351
|
+
...loadAllowlist("LINE", raw, env, "CODE_SHELL_LINE_ALLOWED_TARGET_IDS", "CODE_SHELL_LINE_ALLOWED_USER_IDS", "allowedTargetIds"),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function loadWhatsApp(raw, env) {
|
|
355
|
+
const accessToken = envString(env, "CODE_SHELL_WHATSAPP_ACCESS_TOKEN") ?? readNonEmptyString(raw?.accessToken);
|
|
356
|
+
const appSecret = envString(env, "CODE_SHELL_WHATSAPP_APP_SECRET") ?? readNonEmptyString(raw?.appSecret);
|
|
357
|
+
const verifyToken = envString(env, "CODE_SHELL_WHATSAPP_VERIFY_TOKEN") ?? readNonEmptyString(raw?.verifyToken);
|
|
358
|
+
const phoneNumberId = envString(env, "CODE_SHELL_WHATSAPP_PHONE_NUMBER_ID") ?? readNonEmptyString(raw?.phoneNumberId);
|
|
359
|
+
if (!sectionEnabled(raw, accessToken, appSecret, verifyToken, phoneNumberId))
|
|
360
|
+
return undefined;
|
|
361
|
+
return {
|
|
362
|
+
channel: "whatsapp",
|
|
363
|
+
accessToken: requireValue("WhatsApp", "accessToken", accessToken),
|
|
364
|
+
appSecret: requireValue("WhatsApp", "appSecret", appSecret),
|
|
365
|
+
verifyToken: requireValue("WhatsApp", "verifyToken", verifyToken),
|
|
366
|
+
phoneNumberId: requireValue("WhatsApp", "phoneNumberId", phoneNumberId),
|
|
367
|
+
apiVersion: envString(env, "CODE_SHELL_WHATSAPP_API_VERSION") ??
|
|
368
|
+
readNonEmptyString(raw?.apiVersion) ??
|
|
369
|
+
"v25.0",
|
|
370
|
+
...loadAllowlist("WhatsApp", raw, env, "CODE_SHELL_WHATSAPP_ALLOWED_PHONE_NUMBERS", "CODE_SHELL_WHATSAPP_ALLOWED_PHONE_NUMBERS", "allowedPhoneNumbers"),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function loadTeams(raw, env) {
|
|
374
|
+
const appId = envString(env, "CODE_SHELL_TEAMS_APP_ID") ?? readNonEmptyString(raw?.appId);
|
|
375
|
+
const appPassword = envString(env, "CODE_SHELL_TEAMS_APP_PASSWORD") ?? readNonEmptyString(raw?.appPassword);
|
|
376
|
+
if (!sectionEnabled(raw, appId, appPassword))
|
|
377
|
+
return undefined;
|
|
378
|
+
return {
|
|
379
|
+
channel: "teams",
|
|
380
|
+
appId: requireValue("Microsoft Teams", "appId", appId),
|
|
381
|
+
appPassword: requireValue("Microsoft Teams", "appPassword", appPassword),
|
|
382
|
+
appType: envString(env, "CODE_SHELL_TEAMS_APP_TYPE") ??
|
|
383
|
+
readNonEmptyString(raw?.appType) ??
|
|
384
|
+
"MultiTenant",
|
|
385
|
+
tenantId: envString(env, "CODE_SHELL_TEAMS_TENANT_ID") ?? readNonEmptyString(raw?.tenantId),
|
|
386
|
+
statePath: envString(env, "CODE_SHELL_TEAMS_STATE_PATH") ??
|
|
387
|
+
readNonEmptyString(raw?.statePath) ??
|
|
388
|
+
join(resolveHome(env), ".code-shell", "im-gateway", "teams-conversations.json"),
|
|
389
|
+
...loadAllowlist("Microsoft Teams", raw, env, "CODE_SHELL_TEAMS_ALLOWED_CONVERSATION_IDS", "CODE_SHELL_TEAMS_ALLOWED_USER_IDS", "allowedConversationIds"),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function loadAllowlist(name, raw, env, targetEnv, userEnv, rawTargetKey) {
|
|
393
|
+
const allowedTargetIds = readCsvOverride(env[targetEnv]) ?? readStringList(raw?.[rawTargetKey]);
|
|
394
|
+
const allowedUserIds = readCsvOverride(env[userEnv]) ?? readStringList(raw?.allowedUserIds);
|
|
395
|
+
if (allowedTargetIds.length === 0) {
|
|
396
|
+
throw new Error(`${name} 至少配置一个会话白名单:${rawTargetKey} 或 ${targetEnv}`);
|
|
397
|
+
}
|
|
398
|
+
return { allowedTargetIds, allowedUserIds };
|
|
399
|
+
}
|
|
400
|
+
function readRawConfig(configPath) {
|
|
401
|
+
if (!existsSync(configPath))
|
|
402
|
+
return {};
|
|
403
|
+
try {
|
|
404
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
405
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
406
|
+
throw new Error("top-level value must be an object");
|
|
407
|
+
}
|
|
408
|
+
return parsed;
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
throw new Error(`无法读取 IM gateway 配置 ${configPath}:${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function sectionEnabled(raw, ...values) {
|
|
415
|
+
if (raw?.enabled === false)
|
|
416
|
+
return false;
|
|
417
|
+
return raw !== undefined || values.some(Boolean);
|
|
418
|
+
}
|
|
419
|
+
function requireValue(channel, key, value) {
|
|
420
|
+
if (!value)
|
|
421
|
+
throw new Error(`${channel} 配置不完整:缺少 ${key}`);
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
function envString(env, key) {
|
|
425
|
+
return readNonEmptyString(env[key]);
|
|
426
|
+
}
|
|
427
|
+
function readNonEmptyString(value) {
|
|
428
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
429
|
+
}
|
|
430
|
+
function readStringList(value, fallbackToEmpty = true) {
|
|
431
|
+
if (value === undefined && !fallbackToEmpty)
|
|
432
|
+
return undefined;
|
|
433
|
+
if (!Array.isArray(value))
|
|
434
|
+
return [];
|
|
435
|
+
return [
|
|
436
|
+
...new Set(value.map(readNonEmptyString).filter((item) => Boolean(item))),
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
function readCsvOverride(value) {
|
|
440
|
+
if (value === undefined)
|
|
441
|
+
return undefined;
|
|
442
|
+
return [
|
|
443
|
+
...new Set(value
|
|
444
|
+
.split(",")
|
|
445
|
+
.map((item) => item.trim())
|
|
446
|
+
.filter(Boolean)),
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
function readPositiveNumber(value) {
|
|
450
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
451
|
+
}
|
|
452
|
+
function readPositiveInteger(value) {
|
|
453
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
454
|
+
}
|
|
455
|
+
function readPort(value) {
|
|
456
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65_535
|
|
457
|
+
? value
|
|
458
|
+
: undefined;
|
|
459
|
+
}
|
|
460
|
+
function resolveHome(env) {
|
|
461
|
+
return env.HOME ?? homedir();
|
|
462
|
+
}
|
|
463
|
+
function defaultDesktopLaunch(platform) {
|
|
464
|
+
if (platform === "darwin")
|
|
465
|
+
return { command: "/usr/bin/open", args: ["-a", "code-shell"] };
|
|
466
|
+
if (platform === "win32")
|
|
467
|
+
return { command: "code-shell.exe", args: [] };
|
|
468
|
+
return { command: "code-shell", args: [] };
|
|
469
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ChannelMessage } from "./channel.js";
|
|
2
|
+
export interface DeliveryQueueConfig {
|
|
3
|
+
path?: string;
|
|
4
|
+
maxPending: number;
|
|
5
|
+
maxConcurrent: number;
|
|
6
|
+
maxPerTarget: number;
|
|
7
|
+
retryBaseMs: number;
|
|
8
|
+
retryMaxMs: number;
|
|
9
|
+
completedTtlMs: number;
|
|
10
|
+
/**
|
|
11
|
+
* Directory for spooled attachment bytes. Defaults to `<path>.attachments`.
|
|
12
|
+
*
|
|
13
|
+
* Without a spool, a message carrying attachments could not be persisted (its
|
|
14
|
+
* `load()` is a function), so it stayed in memory only — while the adapter had
|
|
15
|
+
* ALREADY been told the message was accepted. Telegram advances its long-poll
|
|
16
|
+
* offset and Slack acks on handler return, so a restart between enqueue and
|
|
17
|
+
* delivery lost the message permanently: upstream will not resend, and the
|
|
18
|
+
* durable inbox has no record.
|
|
19
|
+
*/
|
|
20
|
+
attachmentSpoolDir?: string;
|
|
21
|
+
/** Largest single attachment to spool. Bigger ones fall back to memory-only. */
|
|
22
|
+
maxSpooledAttachmentBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DeliveryQueueStatus {
|
|
25
|
+
pending: number;
|
|
26
|
+
inFlight: number;
|
|
27
|
+
delayed: number;
|
|
28
|
+
}
|
|
29
|
+
export declare class DeliveryBackpressureError extends Error {
|
|
30
|
+
constructor(limit: number);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Thrown by the deliver callback when a record can never be routed (e.g. a
|
|
34
|
+
* persisted message references an adapter that no longer exists after a config
|
|
35
|
+
* change). The queue drops such records instead of retrying them forever.
|
|
36
|
+
*/
|
|
37
|
+
export declare class UnroutableDeliveryError extends Error {
|
|
38
|
+
constructor(message: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Durable, bounded inbox. Text webhook deliveries are acknowledged only after
|
|
42
|
+
* this queue has atomically recorded them; processing happens out of band with
|
|
43
|
+
* per-target ordering and retry. Lazy attachment functions stay in-memory.
|
|
44
|
+
*/
|
|
45
|
+
export declare class DeliveryQueue {
|
|
46
|
+
private readonly config;
|
|
47
|
+
private readonly deliver;
|
|
48
|
+
private readonly onError;
|
|
49
|
+
private readonly pending;
|
|
50
|
+
private readonly completed;
|
|
51
|
+
private readonly inFlight;
|
|
52
|
+
private readonly targetInFlight;
|
|
53
|
+
private mutation;
|
|
54
|
+
private retryTimer?;
|
|
55
|
+
private stopped;
|
|
56
|
+
constructor(config: DeliveryQueueConfig, deliver: (adapterId: string, message: ChannelMessage) => Promise<void>, onError: (error: unknown, message: ChannelMessage) => void);
|
|
57
|
+
start(): Promise<void>;
|
|
58
|
+
stop(): void;
|
|
59
|
+
status(): DeliveryQueueStatus;
|
|
60
|
+
enqueue(adapterId: string, message: ChannelMessage): Promise<"queued" | "duplicate">;
|
|
61
|
+
private pump;
|
|
62
|
+
private process;
|
|
63
|
+
/** Directory holding spooled attachment bytes, or undefined when not durable. */
|
|
64
|
+
private spoolDir;
|
|
65
|
+
/**
|
|
66
|
+
* Materialize a message's attachments to disk so the record can be persisted.
|
|
67
|
+
*
|
|
68
|
+
* Returns undefined when there is nothing to spool, or when spooling fails —
|
|
69
|
+
* in which case the caller falls back to the old memory-only behaviour rather
|
|
70
|
+
* than dropping the message.
|
|
71
|
+
*/
|
|
72
|
+
private spoolAttachments;
|
|
73
|
+
/** Rebuild attachments whose bytes live in the spool. */
|
|
74
|
+
private rehydrateAttachments;
|
|
75
|
+
/**
|
|
76
|
+
* Delete spool files that no pending record references.
|
|
77
|
+
*
|
|
78
|
+
* `discardSpool` covers the normal path, but files can still be orphaned: a
|
|
79
|
+
* crash between writing the bytes and persisting the record, or the
|
|
80
|
+
* oversized-attachment fallback that spools nothing yet may have written
|
|
81
|
+
* earlier attachments of the same message. Without this the directory only
|
|
82
|
+
* grows, since nothing else ever looks at it.
|
|
83
|
+
*
|
|
84
|
+
* Runs once at load(), when the set of live records is exactly known.
|
|
85
|
+
*/
|
|
86
|
+
private sweepOrphanedSpool;
|
|
87
|
+
/** Delete a record's spooled files once it will never be delivered again. */
|
|
88
|
+
private discardSpool;
|
|
89
|
+
private load;
|
|
90
|
+
private persist;
|
|
91
|
+
private pruneCompleted;
|
|
92
|
+
private withMutation;
|
|
93
|
+
}
|