pikiloop 0.4.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Channel credential validation for Telegram, Feishu, and WeChat.
3
+ */
4
+ import * as lark from '@larksuiteoapi/node-sdk';
5
+ import { validateTelegramToken } from '../../cli/setup-wizard.js';
6
+ import { VALIDATION_TIMEOUTS } from '../constants.js';
7
+ import { writeScopedLog } from '../logging.js';
8
+ import { normalizeWeixinBaseUrl, weixinGetUpdates } from '../../channels/weixin/api.js';
9
+ const DEFAULT_FEISHU_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.feishuDefault;
10
+ const DEFAULT_WEIXIN_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.weixinDefault;
11
+ const DEFAULT_SLACK_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.slackDefault;
12
+ const DEFAULT_DISCORD_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.discordDefault;
13
+ const DEFAULT_DINGTALK_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.dingtalkDefault;
14
+ const DEFAULT_WECOM_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.wecomDefault;
15
+ function feishuValidationLog(appId, message) {
16
+ writeScopedLog('feishu-validate', `app=${appId} ${message}`, { level: 'debug' });
17
+ }
18
+ function maskAppId(appId) {
19
+ if (!appId)
20
+ return '(missing)';
21
+ if (appId.length <= 10)
22
+ return appId;
23
+ return `${appId.slice(0, 6)}...${appId.slice(-4)}`;
24
+ }
25
+ class ValidationTimeoutError extends Error {
26
+ constructor(service, timeoutMs) {
27
+ super(`${service} request timed out after ${timeoutMs}ms.`);
28
+ this.name = 'ValidationTimeoutError';
29
+ }
30
+ }
31
+ function withTimeout(promise, timeoutMs, service, onTimeout) {
32
+ return new Promise((resolve, reject) => {
33
+ let settled = false;
34
+ const timer = setTimeout(() => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ onTimeout?.();
39
+ reject(new ValidationTimeoutError(service, timeoutMs));
40
+ }, timeoutMs);
41
+ promise
42
+ .then(value => {
43
+ if (settled)
44
+ return;
45
+ settled = true;
46
+ clearTimeout(timer);
47
+ resolve(value);
48
+ })
49
+ .catch(error => {
50
+ if (settled)
51
+ return;
52
+ settled = true;
53
+ clearTimeout(timer);
54
+ reject(error);
55
+ });
56
+ });
57
+ }
58
+ function missingChannelState(channel, detail) {
59
+ return {
60
+ channel,
61
+ configured: false,
62
+ ready: false,
63
+ validated: false,
64
+ status: 'missing',
65
+ detail,
66
+ };
67
+ }
68
+ function invalidChannelState(channel, detail) {
69
+ return {
70
+ channel,
71
+ configured: true,
72
+ ready: false,
73
+ validated: true,
74
+ status: 'invalid',
75
+ detail,
76
+ };
77
+ }
78
+ function errorChannelState(channel, detail) {
79
+ return {
80
+ channel,
81
+ configured: true,
82
+ ready: false,
83
+ validated: true,
84
+ status: 'error',
85
+ detail,
86
+ };
87
+ }
88
+ function readyChannelState(channel, detail) {
89
+ return {
90
+ channel,
91
+ configured: true,
92
+ ready: true,
93
+ validated: true,
94
+ status: 'ready',
95
+ detail,
96
+ };
97
+ }
98
+ export function normalizeTelegramAllowedChatIds(raw) {
99
+ const value = String(raw || '').trim();
100
+ if (!value)
101
+ return { ok: true, normalized: '', ids: [], error: null };
102
+ const seen = new Set();
103
+ const ids = [];
104
+ for (const part of value.split(',')) {
105
+ const trimmed = part.trim();
106
+ if (!trimmed)
107
+ continue;
108
+ if (!/^-?\d+$/.test(trimmed)) {
109
+ return {
110
+ ok: false,
111
+ normalized: value,
112
+ ids: [],
113
+ error: 'Allowed Chat IDs must be comma-separated numeric chat IDs.',
114
+ };
115
+ }
116
+ const parsed = Number(trimmed);
117
+ if (!Number.isSafeInteger(parsed)) {
118
+ return {
119
+ ok: false,
120
+ normalized: value,
121
+ ids: [],
122
+ error: 'Allowed Chat IDs contains a value outside the safe integer range.',
123
+ };
124
+ }
125
+ if (seen.has(parsed))
126
+ continue;
127
+ seen.add(parsed);
128
+ ids.push(parsed);
129
+ }
130
+ return {
131
+ ok: true,
132
+ normalized: ids.join(','),
133
+ ids,
134
+ error: null,
135
+ };
136
+ }
137
+ function isTelegramNetworkError(error) {
138
+ const detail = String(error || '');
139
+ return detail.startsWith('Failed to reach Telegram:') || detail.startsWith('Telegram returned invalid JSON');
140
+ }
141
+ export async function validateTelegramConfig(token, allowedChatIds) {
142
+ const trimmedToken = String(token || '').trim();
143
+ if (!trimmedToken) {
144
+ return {
145
+ state: missingChannelState('telegram', 'Telegram bot token is not configured.'),
146
+ bot: null,
147
+ normalizedAllowedChatIds: '',
148
+ };
149
+ }
150
+ const ids = normalizeTelegramAllowedChatIds(allowedChatIds);
151
+ if (!ids.ok) {
152
+ return {
153
+ state: invalidChannelState('telegram', ids.error || 'Allowed Chat IDs is invalid.'),
154
+ bot: null,
155
+ normalizedAllowedChatIds: ids.normalized,
156
+ };
157
+ }
158
+ const tokenCheck = await validateTelegramToken(trimmedToken);
159
+ if (!tokenCheck.ok) {
160
+ return {
161
+ state: (isTelegramNetworkError(tokenCheck.error)
162
+ ? errorChannelState('telegram', tokenCheck.error || 'Telegram validation failed.')
163
+ : invalidChannelState('telegram', tokenCheck.error || 'Telegram validation failed.')),
164
+ bot: null,
165
+ normalizedAllowedChatIds: ids.normalized,
166
+ };
167
+ }
168
+ const identity = tokenCheck.bot?.username
169
+ ? `@${tokenCheck.bot.username}${tokenCheck.bot?.displayName ? ` (${tokenCheck.bot.displayName})` : ''}`
170
+ : 'Telegram bot verified.';
171
+ return {
172
+ state: readyChannelState('telegram', identity),
173
+ bot: tokenCheck.bot,
174
+ normalizedAllowedChatIds: ids.normalized,
175
+ };
176
+ }
177
+ export async function validateFeishuConfig(appId, appSecret, options = {}) {
178
+ const trimmedAppId = String(appId || '').trim();
179
+ const trimmedSecret = String(appSecret || '').trim();
180
+ const appLabel = maskAppId(trimmedAppId);
181
+ const apiDomain = String(process.env.FEISHU_DOMAIN || 'https://open.feishu.cn').trim().replace(/\/+$/, '');
182
+ const timeoutMs = Number.isFinite(options.timeoutMs) && Number(options.timeoutMs) > 0
183
+ ? Math.round(Number(options.timeoutMs))
184
+ : DEFAULT_FEISHU_VALIDATION_TIMEOUT_MS;
185
+ if (!trimmedAppId && !trimmedSecret) {
186
+ return {
187
+ state: missingChannelState('feishu', 'Feishu credentials are not configured.'),
188
+ app: null,
189
+ };
190
+ }
191
+ if (!trimmedAppId || !trimmedSecret) {
192
+ return {
193
+ state: invalidChannelState('feishu', 'Both App ID and App Secret are required.'),
194
+ app: null,
195
+ };
196
+ }
197
+ try {
198
+ const startedAt = Date.now();
199
+ feishuValidationLog(appLabel, `start domain=${apiDomain} timeoutMs=${timeoutMs}`);
200
+ const sdkDomain = apiDomain.includes('larksuite.com')
201
+ ? lark.Domain.Lark
202
+ : apiDomain === 'https://open.feishu.cn'
203
+ ? lark.Domain.Feishu
204
+ : apiDomain;
205
+ const client = new lark.Client({
206
+ appId: trimmedAppId,
207
+ appSecret: trimmedSecret,
208
+ domain: sdkDomain,
209
+ loggerLevel: lark.LoggerLevel.warn,
210
+ });
211
+ const parsed = await withTimeout(client.auth.tenantAccessToken.internal({
212
+ data: { app_id: trimmedAppId, app_secret: trimmedSecret },
213
+ }), timeoutMs, 'Feishu validation');
214
+ feishuValidationLog(appLabel, `response code=${String(parsed?.code ?? '')} hasToken=${typeof parsed?.tenant_access_token === 'string'} elapsedMs=${Date.now() - startedAt}`);
215
+ if (parsed?.code !== 0 || typeof parsed?.tenant_access_token !== 'string' || !parsed.tenant_access_token) {
216
+ const detail = typeof parsed?.msg === 'string' && parsed.msg.trim() ? parsed.msg.trim() : 'credentials rejected';
217
+ feishuValidationLog(appLabel, `rejected code=${String(parsed?.code ?? '')} detail=${detail} elapsedMs=${Date.now() - startedAt}`);
218
+ return {
219
+ state: invalidChannelState('feishu', `Feishu rejected these credentials: ${detail}`),
220
+ app: null,
221
+ };
222
+ }
223
+ // Try to fetch bot display name using the tenant access token
224
+ let botDisplayName = null;
225
+ try {
226
+ const botResp = await withTimeout(fetch(`${apiDomain}/open-apis/bot/v3/info`, {
227
+ method: 'GET',
228
+ headers: { Authorization: `Bearer ${parsed.tenant_access_token}` },
229
+ }).then(r => r.json()), VALIDATION_TIMEOUTS.feishuBotInfo, 'Feishu bot info');
230
+ if (botResp?.bot?.app_name) {
231
+ botDisplayName = botResp.bot.app_name;
232
+ }
233
+ }
234
+ catch {
235
+ // Non-critical — proceed without bot name
236
+ }
237
+ const app = { appId: trimmedAppId, displayName: botDisplayName };
238
+ const identity = botDisplayName
239
+ ? `${botDisplayName} (${appLabel})`
240
+ : `App ${appLabel} verified.`;
241
+ feishuValidationLog(appLabel, `verified botName=${botDisplayName ?? '(unknown)'} elapsedMs=${Date.now() - startedAt}`);
242
+ return {
243
+ state: readyChannelState('feishu', identity),
244
+ app,
245
+ };
246
+ }
247
+ catch (err) {
248
+ feishuValidationLog(appLabel, `error ${(err instanceof Error ? err.message : String(err ?? 'unknown error'))}`);
249
+ if (err instanceof ValidationTimeoutError) {
250
+ return {
251
+ state: errorChannelState('feishu', `Failed to reach Feishu: ${err.message}`),
252
+ app: null,
253
+ };
254
+ }
255
+ const message = err instanceof Error ? err.message : String(err ?? 'unknown error');
256
+ return {
257
+ state: errorChannelState('feishu', `Failed to reach Feishu: ${message}`),
258
+ app: null,
259
+ };
260
+ }
261
+ }
262
+ export async function validateWeixinConfig(baseUrl, botToken, accountId, options = {}) {
263
+ const normalizedBaseUrl = normalizeWeixinBaseUrl(baseUrl);
264
+ const trimmedToken = String(botToken || '').trim();
265
+ const trimmedAccountId = String(accountId || '').trim();
266
+ const timeoutMs = Number.isFinite(options.timeoutMs) && Number(options.timeoutMs) > 0
267
+ ? Math.round(Number(options.timeoutMs))
268
+ : DEFAULT_WEIXIN_VALIDATION_TIMEOUT_MS;
269
+ if (!trimmedToken && !trimmedAccountId && !String(baseUrl || '').trim()) {
270
+ return {
271
+ state: missingChannelState('weixin', 'Weixin is not configured.'),
272
+ account: null,
273
+ normalizedBaseUrl,
274
+ };
275
+ }
276
+ if (!trimmedToken || !trimmedAccountId) {
277
+ return {
278
+ state: invalidChannelState('weixin', 'Weixin requires Base URL, Bot Token, and Account ID.'),
279
+ account: null,
280
+ normalizedBaseUrl,
281
+ };
282
+ }
283
+ try {
284
+ const response = await weixinGetUpdates({
285
+ baseUrl: normalizedBaseUrl,
286
+ token: trimmedToken,
287
+ getUpdatesBuf: '',
288
+ timeoutMs,
289
+ });
290
+ if ((response.ret ?? 0) !== 0 || (response.errcode ?? 0) !== 0) {
291
+ const detail = String(response.errmsg || response.errcode || 'credentials rejected').trim();
292
+ return {
293
+ state: invalidChannelState('weixin', `Weixin rejected these credentials: ${detail}`),
294
+ account: null,
295
+ normalizedBaseUrl,
296
+ };
297
+ }
298
+ return {
299
+ state: readyChannelState('weixin', `Weixin account ${trimmedAccountId} verified.`),
300
+ account: {
301
+ accountId: trimmedAccountId,
302
+ baseUrl: normalizedBaseUrl,
303
+ },
304
+ normalizedBaseUrl,
305
+ };
306
+ }
307
+ catch (error) {
308
+ const message = error instanceof Error ? error.message : String(error ?? 'unknown error');
309
+ return {
310
+ state: errorChannelState('weixin', `Failed to reach Weixin: ${message}`),
311
+ account: null,
312
+ normalizedBaseUrl,
313
+ };
314
+ }
315
+ }
316
+ export async function validateSlackConfig(botToken, appToken, options = {}) {
317
+ const trimmedBot = String(botToken || '').trim();
318
+ const trimmedApp = String(appToken || '').trim();
319
+ const timeoutMs = Number.isFinite(options.timeoutMs) && Number(options.timeoutMs) > 0
320
+ ? Math.round(Number(options.timeoutMs))
321
+ : DEFAULT_SLACK_VALIDATION_TIMEOUT_MS;
322
+ if (!trimmedBot && !trimmedApp) {
323
+ return {
324
+ state: missingChannelState('slack', 'Slack credentials are not configured.'),
325
+ bot: null,
326
+ };
327
+ }
328
+ if (!trimmedBot || !trimmedApp) {
329
+ return {
330
+ state: invalidChannelState('slack', 'Slack requires both Bot Token (xoxb-) and App-Level Token (xapp-).'),
331
+ bot: null,
332
+ };
333
+ }
334
+ if (!trimmedBot.startsWith('xoxb-')) {
335
+ return {
336
+ state: invalidChannelState('slack', 'Slack Bot Token must start with "xoxb-".'),
337
+ bot: null,
338
+ };
339
+ }
340
+ if (!trimmedApp.startsWith('xapp-')) {
341
+ return {
342
+ state: invalidChannelState('slack', 'Slack App-Level Token must start with "xapp-".'),
343
+ bot: null,
344
+ };
345
+ }
346
+ try {
347
+ const data = await withTimeout(fetch('https://slack.com/api/auth.test', {
348
+ method: 'POST',
349
+ headers: {
350
+ Authorization: `Bearer ${trimmedBot}`,
351
+ 'Content-Type': 'application/x-www-form-urlencoded',
352
+ },
353
+ body: '',
354
+ }).then(r => r.json()), timeoutMs, 'Slack auth.test');
355
+ if (!data?.ok) {
356
+ const detail = String(data?.error || 'credentials rejected');
357
+ return {
358
+ state: invalidChannelState('slack', `Slack rejected these credentials: ${detail}`),
359
+ bot: null,
360
+ };
361
+ }
362
+ const bot = {
363
+ userId: String(data.user_id || ''),
364
+ team: data.team ? String(data.team) : null,
365
+ username: data.user ? String(data.user) : null,
366
+ };
367
+ const identity = bot.username
368
+ ? `@${bot.username}${bot.team ? ` (${bot.team})` : ''}`
369
+ : `Slack bot ${bot.userId} verified.`;
370
+ return { state: readyChannelState('slack', identity), bot };
371
+ }
372
+ catch (err) {
373
+ if (err instanceof ValidationTimeoutError) {
374
+ return { state: errorChannelState('slack', `Failed to reach Slack: ${err.message}`), bot: null };
375
+ }
376
+ return {
377
+ state: errorChannelState('slack', `Failed to reach Slack: ${err instanceof Error ? err.message : String(err)}`),
378
+ bot: null,
379
+ };
380
+ }
381
+ }
382
+ export async function validateDiscordConfig(botToken, options = {}) {
383
+ const trimmed = String(botToken || '').trim();
384
+ const timeoutMs = Number.isFinite(options.timeoutMs) && Number(options.timeoutMs) > 0
385
+ ? Math.round(Number(options.timeoutMs))
386
+ : DEFAULT_DISCORD_VALIDATION_TIMEOUT_MS;
387
+ if (!trimmed) {
388
+ return {
389
+ state: missingChannelState('discord', 'Discord bot token is not configured.'),
390
+ bot: null,
391
+ };
392
+ }
393
+ try {
394
+ const resp = await withTimeout(fetch('https://discord.com/api/v10/users/@me', {
395
+ method: 'GET',
396
+ headers: { Authorization: `Bot ${trimmed}` },
397
+ }).then(async (r) => ({ status: r.status, body: await r.json().catch(() => ({})) })), timeoutMs, 'Discord users/@me');
398
+ if (resp.status !== 200) {
399
+ const detail = resp.body?.message || `HTTP ${resp.status}`;
400
+ return {
401
+ state: invalidChannelState('discord', `Discord rejected this token: ${detail}`),
402
+ bot: null,
403
+ };
404
+ }
405
+ const body = resp.body || {};
406
+ const bot = {
407
+ userId: String(body.id || ''),
408
+ username: String(body.username || ''),
409
+ applicationId: body.application_id ? String(body.application_id) : null,
410
+ };
411
+ const identity = bot.username
412
+ ? `@${bot.username}${bot.userId ? ` (id=${bot.userId.slice(-6)})` : ''}`
413
+ : 'Discord bot verified.';
414
+ return { state: readyChannelState('discord', identity), bot };
415
+ }
416
+ catch (err) {
417
+ if (err instanceof ValidationTimeoutError) {
418
+ return { state: errorChannelState('discord', `Failed to reach Discord: ${err.message}`), bot: null };
419
+ }
420
+ return {
421
+ state: errorChannelState('discord', `Failed to reach Discord: ${err instanceof Error ? err.message : String(err)}`),
422
+ bot: null,
423
+ };
424
+ }
425
+ }
426
+ export async function validateDingtalkConfig(clientId, clientSecret, options = {}) {
427
+ const trimmedId = String(clientId || '').trim();
428
+ const trimmedSecret = String(clientSecret || '').trim();
429
+ const timeoutMs = Number.isFinite(options.timeoutMs) && Number(options.timeoutMs) > 0
430
+ ? Math.round(Number(options.timeoutMs))
431
+ : DEFAULT_DINGTALK_VALIDATION_TIMEOUT_MS;
432
+ if (!trimmedId && !trimmedSecret) {
433
+ return {
434
+ state: missingChannelState('dingtalk', 'DingTalk credentials are not configured.'),
435
+ app: null,
436
+ };
437
+ }
438
+ if (!trimmedId || !trimmedSecret) {
439
+ return {
440
+ state: invalidChannelState('dingtalk', 'DingTalk requires both Client ID (AppKey) and Client Secret (AppSecret).'),
441
+ app: null,
442
+ };
443
+ }
444
+ try {
445
+ const url = `https://oapi.dingtalk.com/gettoken?appkey=${encodeURIComponent(trimmedId)}&appsecret=${encodeURIComponent(trimmedSecret)}`;
446
+ const data = await withTimeout(fetch(url, { method: 'GET' }).then(r => r.json()), timeoutMs, 'DingTalk gettoken');
447
+ if (typeof data?.errcode === 'number' && data.errcode !== 0) {
448
+ const detail = String(data.errmsg || 'credentials rejected');
449
+ return {
450
+ state: invalidChannelState('dingtalk', `DingTalk rejected these credentials: ${detail}`),
451
+ app: null,
452
+ };
453
+ }
454
+ if (!data?.access_token) {
455
+ return {
456
+ state: invalidChannelState('dingtalk', 'DingTalk did not return an access token.'),
457
+ app: null,
458
+ };
459
+ }
460
+ const masked = trimmedId.length > 12
461
+ ? `${trimmedId.slice(0, 6)}...${trimmedId.slice(-4)}`
462
+ : trimmedId;
463
+ return {
464
+ state: readyChannelState('dingtalk', `DingTalk app ${masked} verified.`),
465
+ app: { clientId: trimmedId },
466
+ };
467
+ }
468
+ catch (err) {
469
+ if (err instanceof ValidationTimeoutError) {
470
+ return { state: errorChannelState('dingtalk', `Failed to reach DingTalk: ${err.message}`), app: null };
471
+ }
472
+ return {
473
+ state: errorChannelState('dingtalk', `Failed to reach DingTalk: ${err instanceof Error ? err.message : String(err)}`),
474
+ app: null,
475
+ };
476
+ }
477
+ }
478
+ export async function validateWecomConfig(botId, botSecret, _options = {}) {
479
+ const trimmedId = String(botId || '').trim();
480
+ const trimmedSecret = String(botSecret || '').trim();
481
+ if (!trimmedId && !trimmedSecret) {
482
+ return {
483
+ state: missingChannelState('wecom', 'WeChat Work credentials are not configured.'),
484
+ bot: null,
485
+ };
486
+ }
487
+ if (!trimmedId || !trimmedSecret) {
488
+ return {
489
+ state: invalidChannelState('wecom', 'WeChat Work requires both Bot ID and Bot Secret.'),
490
+ bot: null,
491
+ };
492
+ }
493
+ // The Smart Bot WS endpoint cannot be probed via a simple HTTP request;
494
+ // we accept the configured credentials and let the websocket subscribe ack
495
+ // surface real auth errors at runtime. A non-empty pair already covers the
496
+ // most common configuration mistake.
497
+ return {
498
+ state: readyChannelState('wecom', `WeChat Work bot ${trimmedId} configured.`),
499
+ bot: { botId: trimmedId },
500
+ };
501
+ }
502
+ export async function collectChannelSetupStates(config) {
503
+ const [telegram, feishu, weixin, slack, discord, dingtalk, wecom] = await Promise.all([
504
+ validateTelegramConfig(config.telegramBotToken, config.telegramAllowedChatIds),
505
+ validateFeishuConfig(config.feishuAppId, config.feishuAppSecret),
506
+ validateWeixinConfig(config.weixinBaseUrl, config.weixinBotToken, config.weixinAccountId),
507
+ validateSlackConfig(config.slackBotToken, config.slackAppToken),
508
+ validateDiscordConfig(config.discordBotToken),
509
+ validateDingtalkConfig(config.dingtalkClientId, config.dingtalkClientSecret),
510
+ validateWecomConfig(config.wecomBotId, config.wecomBotSecret),
511
+ ]);
512
+ return [
513
+ weixin.state,
514
+ telegram.state,
515
+ feishu.state,
516
+ slack.state,
517
+ discord.state,
518
+ dingtalk.state,
519
+ wecom.state,
520
+ ];
521
+ }