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.
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- package/package.json +82 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channel-supervisor.ts — Owns the set of running IM channel bots and
|
|
3
|
+
* reconciles it against the current user config without restarting the
|
|
4
|
+
* pikiloop process.
|
|
5
|
+
*
|
|
6
|
+
* Subscribes to onUserConfigChange. Whenever the channels array or any
|
|
7
|
+
* channel's credentials change, the supervisor stops the affected bot,
|
|
8
|
+
* disposes its transport, and (if still configured) launches a fresh
|
|
9
|
+
* instance with the new config — all in-process, leaving the dashboard,
|
|
10
|
+
* other channels, and active coding sessions on unaffected bots running.
|
|
11
|
+
*
|
|
12
|
+
* Dashboard-as-terminal: the Web Dashboard is a first-class terminal, equal to
|
|
13
|
+
* the IM channels. When no IM channel is configured, the supervisor keeps a
|
|
14
|
+
* headless bot attached to the dashboard so it's a fully usable terminal on its
|
|
15
|
+
* own; when an IM channel is added, that channel's bot takes over the dashboard
|
|
16
|
+
* attachment and the headless bot is torn down.
|
|
17
|
+
*/
|
|
18
|
+
import { loadUserConfig, onUserConfigChange } from '../core/config/user-config.js';
|
|
19
|
+
import { resolveConfiguredChannels } from './channels.js';
|
|
20
|
+
const CHANNEL_AFFECTING_KEYS = new Set([
|
|
21
|
+
'channels',
|
|
22
|
+
'telegramBotToken',
|
|
23
|
+
'telegramAllowedChatIds',
|
|
24
|
+
'feishuAppId',
|
|
25
|
+
'feishuAppSecret',
|
|
26
|
+
'weixinBaseUrl',
|
|
27
|
+
'weixinBotToken',
|
|
28
|
+
'weixinAccountId',
|
|
29
|
+
'slackBotToken',
|
|
30
|
+
'slackAppToken',
|
|
31
|
+
'discordBotToken',
|
|
32
|
+
'dingtalkClientId',
|
|
33
|
+
'dingtalkClientSecret',
|
|
34
|
+
'wecomBotId',
|
|
35
|
+
'wecomBotSecret',
|
|
36
|
+
'wecomEndpoint',
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* How long to wait between stopping a channel and starting a replacement
|
|
40
|
+
* with new credentials. Some IM providers hold the old connection slot
|
|
41
|
+
* server-side for a moment after we close the socket; if a new client
|
|
42
|
+
* with the same credentials connects too quickly the server rejects with
|
|
43
|
+
* "system busy" and the SDK ends up in a broken state.
|
|
44
|
+
*
|
|
45
|
+
* Feishu's lark SDK is the known offender — its WSClient handshake races
|
|
46
|
+
* with a "PingInterval" setup that crashes when the server rejects the
|
|
47
|
+
* second connect. Telegram (long-poll over HTTP) and Weixin (HTTP long-
|
|
48
|
+
* poll) tolerate immediate replacement.
|
|
49
|
+
*/
|
|
50
|
+
const CHANNEL_REPLACE_SETTLE_MS = {
|
|
51
|
+
telegram: 0,
|
|
52
|
+
weixin: 0,
|
|
53
|
+
feishu: 5_000,
|
|
54
|
+
// The Slack / Discord / DingTalk / WeChat-Work providers all hold the
|
|
55
|
+
// single-bot socket slot for a moment after disconnect; give them a
|
|
56
|
+
// small grace window so token rotation doesn't trip "already connected"
|
|
57
|
+
// style errors on the immediate replacement connection.
|
|
58
|
+
slack: 3_000,
|
|
59
|
+
discord: 3_000,
|
|
60
|
+
dingtalk: 3_000,
|
|
61
|
+
wecom: 3_000,
|
|
62
|
+
};
|
|
63
|
+
function sleep(ms) {
|
|
64
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
65
|
+
}
|
|
66
|
+
function snapshotCredsForChannel(channel, config) {
|
|
67
|
+
switch (channel) {
|
|
68
|
+
case 'telegram':
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
token: String(config.telegramBotToken || '').trim(),
|
|
71
|
+
allowed: String(config.telegramAllowedChatIds || '').trim(),
|
|
72
|
+
});
|
|
73
|
+
case 'feishu':
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
appId: String(config.feishuAppId || '').trim(),
|
|
76
|
+
appSecret: String(config.feishuAppSecret || '').trim(),
|
|
77
|
+
});
|
|
78
|
+
case 'weixin':
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
baseUrl: String(config.weixinBaseUrl || '').trim(),
|
|
81
|
+
token: String(config.weixinBotToken || '').trim(),
|
|
82
|
+
accountId: String(config.weixinAccountId || '').trim(),
|
|
83
|
+
});
|
|
84
|
+
case 'slack':
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
botToken: String(config.slackBotToken || '').trim(),
|
|
87
|
+
appToken: String(config.slackAppToken || '').trim(),
|
|
88
|
+
});
|
|
89
|
+
case 'discord':
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
botToken: String(config.discordBotToken || '').trim(),
|
|
92
|
+
});
|
|
93
|
+
case 'dingtalk':
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
clientId: String(config.dingtalkClientId || '').trim(),
|
|
96
|
+
clientSecret: String(config.dingtalkClientSecret || '').trim(),
|
|
97
|
+
});
|
|
98
|
+
case 'wecom':
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
botId: String(config.wecomBotId || '').trim(),
|
|
101
|
+
botSecret: String(config.wecomBotSecret || '').trim(),
|
|
102
|
+
endpoint: String(config.wecomEndpoint || '').trim(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function createBotForChannel(channel) {
|
|
107
|
+
switch (channel) {
|
|
108
|
+
case 'telegram': {
|
|
109
|
+
const { TelegramBot } = await import('../channels/telegram/bot.js');
|
|
110
|
+
return new TelegramBot();
|
|
111
|
+
}
|
|
112
|
+
case 'feishu': {
|
|
113
|
+
const { FeishuBot } = await import('../channels/feishu/bot.js');
|
|
114
|
+
return new FeishuBot();
|
|
115
|
+
}
|
|
116
|
+
case 'weixin': {
|
|
117
|
+
const { WeixinBot } = await import('../channels/weixin/bot.js');
|
|
118
|
+
return new WeixinBot();
|
|
119
|
+
}
|
|
120
|
+
case 'slack': {
|
|
121
|
+
const { SlackBot } = await import('../channels/slack/bot.js');
|
|
122
|
+
return new SlackBot();
|
|
123
|
+
}
|
|
124
|
+
case 'discord': {
|
|
125
|
+
const { DiscordBot } = await import('../channels/discord/bot.js');
|
|
126
|
+
return new DiscordBot();
|
|
127
|
+
}
|
|
128
|
+
case 'dingtalk': {
|
|
129
|
+
const { DingtalkBot } = await import('../channels/dingtalk/bot.js');
|
|
130
|
+
return new DingtalkBot();
|
|
131
|
+
}
|
|
132
|
+
case 'wecom': {
|
|
133
|
+
const { WeComBot } = await import('../channels/wecom/bot.js');
|
|
134
|
+
return new WeComBot();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function describeError(error) {
|
|
139
|
+
return error instanceof Error ? error.message : String(error ?? 'unknown error');
|
|
140
|
+
}
|
|
141
|
+
export class ChannelSupervisor {
|
|
142
|
+
running = new Map();
|
|
143
|
+
/** Headless dashboard-terminal bot, present only while no IM channel runs. */
|
|
144
|
+
headless = null;
|
|
145
|
+
dashboard;
|
|
146
|
+
log;
|
|
147
|
+
reconcileInFlight = false;
|
|
148
|
+
reconcilePending = false;
|
|
149
|
+
unsubscribe = null;
|
|
150
|
+
started = false;
|
|
151
|
+
constructor(opts) {
|
|
152
|
+
this.dashboard = opts.dashboard;
|
|
153
|
+
this.log = opts.log;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Initial launch + subscribe to config changes. Idempotent: calling
|
|
157
|
+
* twice is a no-op for the second call.
|
|
158
|
+
*/
|
|
159
|
+
async start() {
|
|
160
|
+
if (this.started)
|
|
161
|
+
return;
|
|
162
|
+
this.started = true;
|
|
163
|
+
await this.reconcile(loadUserConfig());
|
|
164
|
+
this.unsubscribe = onUserConfigChange((config, changedKeys) => {
|
|
165
|
+
if (!changedKeys.some(key => CHANNEL_AFFECTING_KEYS.has(key)))
|
|
166
|
+
return;
|
|
167
|
+
void this.reconcile(config);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** Stop every running channel and unsubscribe from config changes. */
|
|
171
|
+
async stop() {
|
|
172
|
+
this.unsubscribe?.();
|
|
173
|
+
this.unsubscribe = null;
|
|
174
|
+
const channels = [...this.running.keys()];
|
|
175
|
+
await Promise.all(channels.map(channel => this.stopChannel(channel)));
|
|
176
|
+
await this.stopHeadless();
|
|
177
|
+
}
|
|
178
|
+
/** Reconcile running bots against the desired channel set + credentials. */
|
|
179
|
+
async reconcile(config) {
|
|
180
|
+
if (this.reconcileInFlight) {
|
|
181
|
+
this.reconcilePending = true;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.reconcileInFlight = true;
|
|
185
|
+
try {
|
|
186
|
+
let current = config;
|
|
187
|
+
while (true) {
|
|
188
|
+
this.reconcilePending = false;
|
|
189
|
+
await this.doReconcile(current);
|
|
190
|
+
if (!this.reconcilePending)
|
|
191
|
+
break;
|
|
192
|
+
// Newer config arrived during reconcile — re-read from disk so the
|
|
193
|
+
// next pass acts on the latest state.
|
|
194
|
+
current = loadUserConfig();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
this.reconcileInFlight = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async doReconcile(config) {
|
|
202
|
+
const desired = resolveConfiguredChannels({ config });
|
|
203
|
+
const desiredSet = new Set(desired);
|
|
204
|
+
// Stop channels that are no longer desired OR whose credentials changed.
|
|
205
|
+
const toStop = [];
|
|
206
|
+
for (const [channel, entry] of this.running) {
|
|
207
|
+
if (!desiredSet.has(channel)) {
|
|
208
|
+
toStop.push(channel);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (snapshotCredsForChannel(channel, config) !== entry.credSnapshot) {
|
|
212
|
+
toStop.push(channel);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (toStop.length) {
|
|
216
|
+
await Promise.all(toStop.map(channel => this.stopChannel(channel)));
|
|
217
|
+
}
|
|
218
|
+
// For channels being replaced (still desired, just stopped above for
|
|
219
|
+
// credential rotation), wait the per-channel settle delay so the
|
|
220
|
+
// remote server has time to release the old connection slot before
|
|
221
|
+
// we open a new one.
|
|
222
|
+
const replacing = toStop.filter(channel => desiredSet.has(channel));
|
|
223
|
+
const settleMs = replacing.reduce((max, channel) => Math.max(max, CHANNEL_REPLACE_SETTLE_MS[channel] ?? 0), 0);
|
|
224
|
+
if (settleMs > 0) {
|
|
225
|
+
this.log(`waiting ${settleMs}ms for ${replacing.join(', ')} to settle before relaunch`);
|
|
226
|
+
await sleep(settleMs);
|
|
227
|
+
}
|
|
228
|
+
// Start channels that should be running but aren't (either added or
|
|
229
|
+
// just stopped above due to credential rotation).
|
|
230
|
+
for (const channel of desired) {
|
|
231
|
+
if (this.running.has(channel))
|
|
232
|
+
continue;
|
|
233
|
+
try {
|
|
234
|
+
await this.startChannel(channel, config);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
this.log(`channel ${channel}: failed to start — ${describeError(err)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Dashboard-as-terminal: keep a headless bot attached whenever no channel
|
|
241
|
+
// bot is actually running (none configured, or all failed to start), so the
|
|
242
|
+
// dashboard stays a usable terminal on its own. Gate on `running.size`
|
|
243
|
+
// rather than `desired.length` so a channel that fails to come up still
|
|
244
|
+
// leaves the dashboard with a working bot.
|
|
245
|
+
if (this.running.size > 0) {
|
|
246
|
+
await this.stopHeadless();
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
await this.startHeadless();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Launch the headless dashboard-terminal bot and attach it to the dashboard. */
|
|
253
|
+
async startHeadless() {
|
|
254
|
+
if (this.headless)
|
|
255
|
+
return;
|
|
256
|
+
this.log('dashboard terminal: starting (no IM channel configured)');
|
|
257
|
+
const { HeadlessBot } = await import('../bot/headless-bot.js');
|
|
258
|
+
const bot = new HeadlessBot();
|
|
259
|
+
if (this.dashboard)
|
|
260
|
+
this.dashboard.attachBot(bot);
|
|
261
|
+
const runPromise = bot.run().catch((err) => {
|
|
262
|
+
this.log(`dashboard terminal: run() exited with error — ${describeError(err)}`);
|
|
263
|
+
});
|
|
264
|
+
this.headless = { bot, runPromise };
|
|
265
|
+
}
|
|
266
|
+
/** Tear down the headless bot once a real channel bot owns the dashboard. */
|
|
267
|
+
async stopHeadless() {
|
|
268
|
+
if (!this.headless)
|
|
269
|
+
return;
|
|
270
|
+
this.log('dashboard terminal: stopping (IM channel took over)');
|
|
271
|
+
const { bot, runPromise } = this.headless;
|
|
272
|
+
this.headless = null;
|
|
273
|
+
bot.requestStop();
|
|
274
|
+
try {
|
|
275
|
+
await runPromise;
|
|
276
|
+
}
|
|
277
|
+
catch { /* already logged */ }
|
|
278
|
+
}
|
|
279
|
+
async startChannel(channel, config) {
|
|
280
|
+
this.log(`channel ${channel}: starting`);
|
|
281
|
+
const bot = await createBotForChannel(channel);
|
|
282
|
+
if (this.dashboard)
|
|
283
|
+
this.dashboard.attachBot(bot);
|
|
284
|
+
const runPromise = bot.run().catch((err) => {
|
|
285
|
+
this.log(`channel ${channel}: run() exited with error — ${describeError(err)}`);
|
|
286
|
+
});
|
|
287
|
+
this.running.set(channel, {
|
|
288
|
+
bot,
|
|
289
|
+
credSnapshot: snapshotCredsForChannel(channel, config),
|
|
290
|
+
runPromise,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async stopChannel(channel) {
|
|
294
|
+
const entry = this.running.get(channel);
|
|
295
|
+
if (!entry)
|
|
296
|
+
return;
|
|
297
|
+
this.log(`channel ${channel}: stopping`);
|
|
298
|
+
// Remove from the map up-front so a concurrent reconcile pass doesn't
|
|
299
|
+
// try to stop the same instance twice.
|
|
300
|
+
this.running.delete(channel);
|
|
301
|
+
try {
|
|
302
|
+
entry.bot.requestStop();
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
this.log(`channel ${channel}: requestStop threw — ${describeError(err)}`);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
await entry.runPromise;
|
|
309
|
+
}
|
|
310
|
+
catch { }
|
|
311
|
+
this.log(`channel ${channel}: stopped`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel resolution helpers for CLI startup.
|
|
3
|
+
*/
|
|
4
|
+
import { applyChannelEnvFallback } from '../core/config/user-config.js';
|
|
5
|
+
export function hasConfiguredChannelToken(rawConfig, channel, tokenOverride) {
|
|
6
|
+
// Channel transports already fall back to env vars at runtime
|
|
7
|
+
// (e.g. TELEGRAM_BOT_TOKEN). Mirror that here so a docker-only env setup
|
|
8
|
+
// doesn't make startup think there's nothing configured.
|
|
9
|
+
const config = applyChannelEnvFallback(rawConfig);
|
|
10
|
+
switch (channel) {
|
|
11
|
+
case 'telegram':
|
|
12
|
+
return !!(config.telegramBotToken || tokenOverride);
|
|
13
|
+
case 'feishu':
|
|
14
|
+
return !!((config.feishuAppId && config.feishuAppSecret) || tokenOverride);
|
|
15
|
+
case 'weixin':
|
|
16
|
+
return !!(config.channels?.includes('weixin')
|
|
17
|
+
&& config.weixinBaseUrl
|
|
18
|
+
&& config.weixinBotToken
|
|
19
|
+
&& config.weixinAccountId);
|
|
20
|
+
case 'slack':
|
|
21
|
+
return !!(config.slackBotToken && config.slackAppToken);
|
|
22
|
+
case 'discord':
|
|
23
|
+
return !!config.discordBotToken;
|
|
24
|
+
case 'dingtalk':
|
|
25
|
+
return !!(config.dingtalkClientId && config.dingtalkClientSecret);
|
|
26
|
+
case 'wecom':
|
|
27
|
+
return !!(config.wecomBotId && config.wecomBotSecret);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function resolveConfiguredChannels(opts) {
|
|
31
|
+
const rawChannels = String(opts.explicitChannels || '').trim();
|
|
32
|
+
if (rawChannels) {
|
|
33
|
+
return rawChannels.split(',').map(channel => channel.trim().toLowerCase()).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
if (opts.config.channels?.length) {
|
|
36
|
+
return opts.config.channels.filter(channel => hasConfiguredChannelToken(opts.config, channel, opts.tokenOverride));
|
|
37
|
+
}
|
|
38
|
+
const detected = [];
|
|
39
|
+
if (hasConfiguredChannelToken(opts.config, 'weixin', opts.tokenOverride))
|
|
40
|
+
detected.push('weixin');
|
|
41
|
+
if (hasConfiguredChannelToken(opts.config, 'feishu', opts.tokenOverride))
|
|
42
|
+
detected.push('feishu');
|
|
43
|
+
if (hasConfiguredChannelToken(opts.config, 'telegram', opts.tokenOverride))
|
|
44
|
+
detected.push('telegram');
|
|
45
|
+
if (hasConfiguredChannelToken(opts.config, 'slack', opts.tokenOverride))
|
|
46
|
+
detected.push('slack');
|
|
47
|
+
if (hasConfiguredChannelToken(opts.config, 'discord', opts.tokenOverride))
|
|
48
|
+
detected.push('discord');
|
|
49
|
+
if (hasConfiguredChannelToken(opts.config, 'dingtalk', opts.tokenOverride))
|
|
50
|
+
detected.push('dingtalk');
|
|
51
|
+
if (hasConfiguredChannelToken(opts.config, 'wecom', opts.tokenOverride))
|
|
52
|
+
detected.push('wecom');
|
|
53
|
+
return detected;
|
|
54
|
+
}
|