botmux 2.86.0 → 2.88.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/README.en.md +27 -0
- package/README.md +27 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +12 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/liveness-gate.d.ts +38 -0
- package/dist/adapters/backend/liveness-gate.d.ts.map +1 -0
- package/dist/adapters/backend/liveness-gate.js +53 -0
- package/dist/adapters/backend/liveness-gate.js.map +1 -0
- package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +4 -1
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +22 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +124 -36
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +20 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +86 -20
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +9 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +1 -1
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.d.ts +13 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.js +51 -12
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +8 -5
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +2 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +25 -3
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +4 -0
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +6 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +23 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +9 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +368 -16
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +7 -5
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +210 -5
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +3 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +4 -1
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/maintenance.d.ts +18 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +58 -30
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/per-bot-env.d.ts +38 -0
- package/dist/core/per-bot-env.d.ts.map +1 -0
- package/dist/core/per-bot-env.js +79 -0
- package/dist/core/per-bot-env.js.map +1 -0
- package/dist/core/session-activity.d.ts +1 -0
- package/dist/core/session-activity.d.ts.map +1 -1
- package/dist/core/session-activity.js +7 -4
- package/dist/core/session-activity.js.map +1 -1
- package/dist/core/session-create.d.ts +62 -0
- package/dist/core/session-create.d.ts.map +1 -0
- package/dist/core/session-create.js +131 -0
- package/dist/core/session-create.js.map +1 -0
- package/dist/core/session-manager.d.ts +43 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +306 -8
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/skills/discovery.d.ts +7 -0
- package/dist/core/skills/discovery.d.ts.map +1 -1
- package/dist/core/skills/discovery.js +51 -0
- package/dist/core/skills/discovery.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +5 -2
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/update-check.d.ts +67 -0
- package/dist/core/update-check.d.ts.map +1 -0
- package/dist/core/update-check.js +159 -0
- package/dist/core/update-check.js.map +1 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +82 -4
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +40 -11
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +2 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +2 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/session-cleanup.d.ts +30 -0
- package/dist/dashboard/session-cleanup.d.ts.map +1 -0
- package/dist/dashboard/session-cleanup.js +47 -0
- package/dist/dashboard/session-cleanup.js.map +1 -0
- package/dist/dashboard/skill-install-request.d.ts +10 -0
- package/dist/dashboard/skill-install-request.d.ts.map +1 -1
- package/dist/dashboard/skill-install-request.js +17 -0
- package/dist/dashboard/skill-install-request.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +29 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +82 -6
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +312 -2
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/insights.d.ts.map +1 -1
- package/dist/dashboard/web/insights.js +355 -30
- package/dist/dashboard/web/insights.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +457 -30
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings.d.ts.map +1 -1
- package/dist/dashboard/web/settings.js +289 -0
- package/dist/dashboard/web/settings.js.map +1 -1
- package/dist/dashboard/web/skills.d.ts.map +1 -1
- package/dist/dashboard/web/skills.js +183 -10
- package/dist/dashboard/web/skills.js.map +1 -1
- package/dist/dashboard/web/whiteboards.d.ts +2 -0
- package/dist/dashboard/web/whiteboards.d.ts.map +1 -0
- package/dist/dashboard/web/whiteboards.js +267 -0
- package/dist/dashboard/web/whiteboards.js.map +1 -0
- package/dist/dashboard-web/app.js +879 -591
- package/dist/dashboard-web/index.html +1 -0
- package/dist/dashboard-web/style.css +528 -8
- package/dist/dashboard.js +472 -5
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +6 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +12 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +21 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +21 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +4 -5
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +113 -11
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +3 -2
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +174 -14
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +19 -5
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +9 -2
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/index-daemon.js +5 -0
- package/dist/index-daemon.js.map +1 -1
- package/dist/platform/bind.d.ts +2 -0
- package/dist/platform/bind.d.ts.map +1 -0
- package/dist/platform/bind.js +91 -0
- package/dist/platform/bind.js.map +1 -0
- package/dist/platform/binding.d.ts +22 -0
- package/dist/platform/binding.d.ts.map +1 -0
- package/dist/platform/binding.js +33 -0
- package/dist/platform/binding.js.map +1 -0
- package/dist/platform/tunnel-client.d.ts +15 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -0
- package/dist/platform/tunnel-client.js +193 -0
- package/dist/platform/tunnel-client.js.map +1 -0
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +20 -0
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/git-worktree.d.ts +10 -0
- package/dist/services/git-worktree.d.ts.map +1 -1
- package/dist/services/git-worktree.js +71 -26
- package/dist/services/git-worktree.js.map +1 -1
- package/dist/services/grant-prefs-store.d.ts +3 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -1
- package/dist/services/grant-prefs-store.js +17 -2
- package/dist/services/grant-prefs-store.js.map +1 -1
- package/dist/services/insight/claude-span-reader.d.ts.map +1 -1
- package/dist/services/insight/claude-span-reader.js +18 -2
- package/dist/services/insight/claude-span-reader.js.map +1 -1
- package/dist/services/insight/codex-span-reader.d.ts.map +1 -1
- package/dist/services/insight/codex-span-reader.js +17 -1
- package/dist/services/insight/codex-span-reader.js.map +1 -1
- package/dist/services/insight/report.d.ts.map +1 -1
- package/dist/services/insight/report.js +48 -6
- package/dist/services/insight/report.js.map +1 -1
- package/dist/services/insight/types.d.ts +9 -0
- package/dist/services/insight/types.d.ts.map +1 -1
- package/dist/services/insight/types.js.map +1 -1
- package/dist/services/resumable-session-discovery.d.ts.map +1 -1
- package/dist/services/resumable-session-discovery.js +21 -4
- package/dist/services/resumable-session-discovery.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts +1 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +32 -0
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/whiteboard-store.d.ts +67 -0
- package/dist/services/whiteboard-store.d.ts.map +1 -0
- package/dist/services/whiteboard-store.js +450 -0
- package/dist/services/whiteboard-store.js.map +1 -0
- package/dist/services/worktree-slug-ai.d.ts.map +1 -1
- package/dist/services/worktree-slug-ai.js +3 -1
- package/dist/services/worktree-slug-ai.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts +8 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +9 -0
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +118 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/crash-log.d.ts +22 -0
- package/dist/utils/crash-log.d.ts.map +1 -0
- package/dist/utils/crash-log.js +37 -0
- package/dist/utils/crash-log.js.map +1 -0
- package/dist/utils/input-gate.d.ts +10 -0
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +7 -0
- package/dist/utils/input-gate.js.map +1 -1
- package/dist/utils/install-diagnostics.d.ts +52 -0
- package/dist/utils/install-diagnostics.d.ts.map +1 -0
- package/dist/utils/install-diagnostics.js +155 -0
- package/dist/utils/install-diagnostics.js.map +1 -0
- package/dist/utils/install-info.d.ts +4 -0
- package/dist/utils/install-info.d.ts.map +1 -1
- package/dist/utils/install-info.js +6 -0
- package/dist/utils/install-info.js.map +1 -1
- package/dist/utils/stdio-epipe-guard.d.ts +39 -0
- package/dist/utils/stdio-epipe-guard.d.ts.map +1 -0
- package/dist/utils/stdio-epipe-guard.js +67 -0
- package/dist/utils/stdio-epipe-guard.js.map +1 -0
- package/dist/worker.js +222 -26
- package/dist/worker.js.map +1 -1
- package/dist/workflows/daemon-spawn.d.ts +2 -0
- package/dist/workflows/daemon-spawn.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -27,18 +27,30 @@ import { BotOnboardingManager } from './dashboard/bot-onboarding.js';
|
|
|
27
27
|
import { CLI_SELECT_OPTIONS, resolveCliSelection, isTtadkWrapper, ttadkAcceptsModel, TTADK_DEFAULT_MODEL, TTADK_MODEL_SUGGESTIONS, } from './setup/cli-selection.js';
|
|
28
28
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
29
29
|
import { mergeDashboardConfig, mergeGlobalConfig, mergeMaintenanceConfig, parseMaintenancePatch, readGlobalConfig, setGlobalLocale } from './global-config.js';
|
|
30
|
+
import { deleteWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled } from './services/whiteboard-store.js';
|
|
30
31
|
import { isLocale } from './i18n/types.js';
|
|
31
|
-
import { isLocalDevInstall } from './utils/install-info.js';
|
|
32
|
+
import { isLocalDevInstall, botmuxVersion } from './utils/install-info.js';
|
|
33
|
+
import { checkNode, detectBotmuxInstalls, resolveCurrentVersion } from './utils/install-diagnostics.js';
|
|
34
|
+
import { fetchLatestVersion, fetchReleasesSince, isNewerVersion } from './core/update-check.js';
|
|
35
|
+
import { GITHUB_REPO } from './core/restart-report.js';
|
|
36
|
+
import { spawnDetachedRestart, npmGlobalUpdateLockTarget } from './core/maintenance.js';
|
|
37
|
+
import { writeRestartIntent } from './services/restart-intent-store.js';
|
|
38
|
+
import { withFileLock } from './utils/file-lock.js';
|
|
39
|
+
import { spawn } from 'node:child_process';
|
|
32
40
|
import { listTeamReports, readTeamBoard, setTeamBoardEntry } from './services/team-board-store.js';
|
|
33
41
|
import { hd2dAssetPath, hd2dStatus, startHd2dDownload } from './dashboard/hd2d-assets.js';
|
|
34
|
-
import { readSkillRegistry, removeInstalledSkill, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
|
|
42
|
+
import { installLocalSkillLinks, readSkillRegistry, removeInstalledSkill, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
|
|
35
43
|
import { redactGitUrlCredentials } from './core/skills/sources.js';
|
|
36
44
|
import { loadBotConfigs } from './bot-registry.js';
|
|
45
|
+
import { discoverNativeCliSkillGroups } from './core/skills/discovery.js';
|
|
37
46
|
import { analyzeSkillReferences } from './core/skills/references.js';
|
|
38
|
-
import { installDashboardSkill, parseDashboardSkillInstallRequest } from './dashboard/skill-install-request.js';
|
|
47
|
+
import { installDashboardSkill, parseDashboardSkillInstallRequest, parseInstallLocalLinksSources, MAX_LOCAL_LINK_SOURCES } from './dashboard/skill-install-request.js';
|
|
39
48
|
import { botDefaultsPayload, botSummaryPayload } from './dashboard/bot-payload.js';
|
|
40
49
|
import { isValidRoleProfileId } from './services/role-profile-store.js';
|
|
41
50
|
import { mergeSafeInsightOverviews } from './services/insight/report.js';
|
|
51
|
+
import { readPlatformBinding } from './platform/binding.js';
|
|
52
|
+
import { startPlatformTunnelClient } from './platform/tunnel-client.js';
|
|
53
|
+
import { cleanupIdleSessions, parseIdleCleanupHours } from './dashboard/session-cleanup.js';
|
|
42
54
|
const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
43
55
|
const TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
|
|
44
56
|
/** Per-daemon budget for the cross-daemon insight overview fan-out — bounds
|
|
@@ -116,6 +128,7 @@ function resolveDashboardSettings() {
|
|
|
116
128
|
repoPickerMode: global.repoPickerMode ?? 'all',
|
|
117
129
|
maintenance: global.maintenance ?? {},
|
|
118
130
|
localDevInstall: isLocalDevInstall(),
|
|
131
|
+
whiteboard: { enabled: global.whiteboard?.enabled === true },
|
|
119
132
|
};
|
|
120
133
|
}
|
|
121
134
|
async function readJsonBody(req) {
|
|
@@ -125,6 +138,67 @@ async function readJsonBody(req) {
|
|
|
125
138
|
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
126
139
|
return raw ? JSON.parse(raw) : {};
|
|
127
140
|
}
|
|
141
|
+
/** Fast in-process guard against double-clicks within this dashboard process.
|
|
142
|
+
* Cross-process serialization against the maintenance auto-update (a different
|
|
143
|
+
* process) is handled separately by the shared file lock in the run route. */
|
|
144
|
+
let updateInFlight = false;
|
|
145
|
+
// Cache the upstream version/changelog lookups so the nav-badge check + the
|
|
146
|
+
// Settings card don't hammer the npm registry / GitHub on every page load.
|
|
147
|
+
// GitHub's unauthenticated API is only 60 req/h per IP, so caching the changelog
|
|
148
|
+
// also keeps us from exhausting it. Failures cache briefly so they self-heal.
|
|
149
|
+
const LATEST_TTL_MS = 30 * 60_000;
|
|
150
|
+
const CHANGELOG_TTL_MS = 15 * 60_000;
|
|
151
|
+
const FAILURE_TTL_MS = 60_000;
|
|
152
|
+
let latestVersionCache = null;
|
|
153
|
+
let changelogCache = null;
|
|
154
|
+
async function cachedLatestVersion(now = Date.now()) {
|
|
155
|
+
const ttl = latestVersionCache?.value ? LATEST_TTL_MS : FAILURE_TTL_MS;
|
|
156
|
+
if (latestVersionCache && now - latestVersionCache.at < ttl)
|
|
157
|
+
return latestVersionCache.value;
|
|
158
|
+
const value = await fetchLatestVersion();
|
|
159
|
+
latestVersionCache = { value, at: now };
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
async function cachedChangelog(current, now = Date.now()) {
|
|
163
|
+
const ttl = changelogCache?.value.ok ? CHANGELOG_TTL_MS : FAILURE_TTL_MS;
|
|
164
|
+
if (changelogCache && changelogCache.key === current && now - changelogCache.at < ttl)
|
|
165
|
+
return changelogCache.value;
|
|
166
|
+
const value = await fetchReleasesSince(current);
|
|
167
|
+
changelogCache = { key: current, value, at: now };
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Run `npm install -g botmux@latest` for the manual-update flow WITHOUT blocking
|
|
172
|
+
* the event loop (async spawn, not execSync — the dashboard must keep serving
|
|
173
|
+
* during the ~10-30s install). Resolves on exit 0; rejects with the tail of
|
|
174
|
+
* stdout/stderr on a non-zero exit, spawn error, or 3-minute timeout. Args are
|
|
175
|
+
* a fixed literal — no shell interpolation of untrusted input.
|
|
176
|
+
*/
|
|
177
|
+
function runNpmInstallLatest() {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const child = spawn('npm', ['install', '-g', 'botmux@latest'], {
|
|
180
|
+
env: process.env,
|
|
181
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
182
|
+
shell: process.platform === 'win32', // resolve npm.cmd on Windows
|
|
183
|
+
});
|
|
184
|
+
let tail = '';
|
|
185
|
+
const capture = (d) => { tail = (tail + d.toString()).slice(-2000); };
|
|
186
|
+
child.stdout?.on('data', capture);
|
|
187
|
+
child.stderr?.on('data', capture);
|
|
188
|
+
const timer = setTimeout(() => {
|
|
189
|
+
child.kill('SIGKILL');
|
|
190
|
+
reject(new Error('npm install timed out after 180s'));
|
|
191
|
+
}, 180_000);
|
|
192
|
+
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
193
|
+
child.on('exit', (code) => {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
if (code === 0)
|
|
196
|
+
resolve();
|
|
197
|
+
else
|
|
198
|
+
reject(new Error(`npm exited ${code}: ${tail.trim().slice(-500)}`));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
128
202
|
/**
|
|
129
203
|
* Attach to one daemon: hydrate its sessions/schedules into the aggregator,
|
|
130
204
|
* THEN open the SSE subscription. Order matters — hydrating after subscribe
|
|
@@ -507,12 +581,33 @@ function sanitizeSkillForDashboard(skill) {
|
|
|
507
581
|
source: { ...skill.source, url: redactGitUrlCredentials(skill.source.url) },
|
|
508
582
|
};
|
|
509
583
|
}
|
|
584
|
+
function dashboardSkillCliIds() {
|
|
585
|
+
const ids = new Set();
|
|
586
|
+
try {
|
|
587
|
+
for (const cliId of configuredCliIds().values())
|
|
588
|
+
ids.add(cliId);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Fall back to daemon descriptors below when persistent config is unavailable.
|
|
592
|
+
}
|
|
593
|
+
for (const bot of registry.list()) {
|
|
594
|
+
if (bot.cliId)
|
|
595
|
+
ids.add(bot.cliId);
|
|
596
|
+
}
|
|
597
|
+
return [...ids];
|
|
598
|
+
}
|
|
510
599
|
function dashboardSkillsPayload() {
|
|
511
600
|
const globalSkills = readGlobalConfig().skills ?? {};
|
|
601
|
+
const nativeSkillGroups = discoverNativeCliSkillGroups(dashboardSkillCliIds())
|
|
602
|
+
.map(group => ({
|
|
603
|
+
...group,
|
|
604
|
+
skills: group.skills.map(sanitizeSkillForDashboard),
|
|
605
|
+
}));
|
|
512
606
|
return {
|
|
513
607
|
skills: Object.values(readSkillRegistry().skills)
|
|
514
608
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
515
609
|
.map(sanitizeSkillForDashboard),
|
|
610
|
+
nativeSkillGroups,
|
|
516
611
|
trustProjectSkills: globalSkills.trustProjectSkills ?? 'off',
|
|
517
612
|
delivery: globalSkills.delivery ?? 'auto',
|
|
518
613
|
};
|
|
@@ -700,6 +795,54 @@ const server = createServer(async (req, res) => {
|
|
|
700
795
|
});
|
|
701
796
|
return jsonRes(res, 200, { sessions });
|
|
702
797
|
}
|
|
798
|
+
if (req.method === 'POST' && url.pathname === '/api/sessions/cleanup-idle') {
|
|
799
|
+
let body;
|
|
800
|
+
try {
|
|
801
|
+
body = await readJsonBody(req);
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
805
|
+
}
|
|
806
|
+
const olderThanHours = parseIdleCleanupHours(body?.olderThanHours);
|
|
807
|
+
if (!olderThanHours)
|
|
808
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_threshold' });
|
|
809
|
+
// WYSIWYG: the UI scopes cleanup to the rows currently visible under the
|
|
810
|
+
// page filters and sends their sessionIds, so the closed set matches the
|
|
811
|
+
// confirmed count. We still hand the scoped rows to cleanupIdleSessions,
|
|
812
|
+
// which re-validates each is a genuine idle candidate — a stale/forged id
|
|
813
|
+
// can never close a non-idle session. Omitting sessionIds (e.g. an older
|
|
814
|
+
// client) falls back to a deployment-wide sweep. Cap the id set so a giant
|
|
815
|
+
// body can't blow up the Set build.
|
|
816
|
+
const idScope = Array.isArray(body?.sessionIds)
|
|
817
|
+
? new Set(body.sessionIds.slice(0, 10000).map(String))
|
|
818
|
+
: null;
|
|
819
|
+
const rows = aggregator.getSessions();
|
|
820
|
+
const scoped = idScope ? rows.filter(s => idScope.has(s.sessionId)) : rows;
|
|
821
|
+
const result = await cleanupIdleSessions(scoped, olderThanHours, async (s) => {
|
|
822
|
+
try {
|
|
823
|
+
const upstream = await proxyToDaemon(s.larkAppId, `/api/sessions/${encodeURIComponent(s.sessionId)}/close`, { method: 'POST' });
|
|
824
|
+
const text = await upstream.text();
|
|
825
|
+
let parsed = null;
|
|
826
|
+
try {
|
|
827
|
+
parsed = JSON.parse(text);
|
|
828
|
+
}
|
|
829
|
+
catch { /* tolerate */ }
|
|
830
|
+
// The daemon close route always replies 200 {ok:true}; treat anything
|
|
831
|
+
// else (incl. an unparseable/missing body) as a failure rather than a
|
|
832
|
+
// silent success.
|
|
833
|
+
const ok = upstream.ok && parsed?.ok === true;
|
|
834
|
+
return {
|
|
835
|
+
sessionId: s.sessionId,
|
|
836
|
+
ok,
|
|
837
|
+
error: ok ? undefined : (parsed?.error ?? `http_${upstream.status}`),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
catch (e) {
|
|
841
|
+
return { sessionId: s.sessionId, ok: false, error: e?.message ?? String(e) };
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return jsonRes(res, 200, result);
|
|
845
|
+
}
|
|
703
846
|
if (req.method === 'GET' && url.pathname === '/api/insights/summary') {
|
|
704
847
|
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '200', 10) || 200, 1), 500);
|
|
705
848
|
// Per-daemon timeout + isolate failures: an upstream insight parse can be
|
|
@@ -774,6 +917,16 @@ const server = createServer(async (req, res) => {
|
|
|
774
917
|
mergeGlobalConfig({ repoPickerMode: v });
|
|
775
918
|
touched = true;
|
|
776
919
|
}
|
|
920
|
+
if ('whiteboard' in body) {
|
|
921
|
+
const raw = body.whiteboard;
|
|
922
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
923
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_whiteboard' });
|
|
924
|
+
const wb = raw;
|
|
925
|
+
if (typeof wb.enabled !== 'boolean')
|
|
926
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_whiteboard_enabled' });
|
|
927
|
+
mergeGlobalConfig({ whiteboard: { enabled: wb.enabled } });
|
|
928
|
+
touched = true;
|
|
929
|
+
}
|
|
777
930
|
if ('maintenance' in body) {
|
|
778
931
|
const r = parseMaintenancePatch(body.maintenance);
|
|
779
932
|
if (!r.ok)
|
|
@@ -813,6 +966,96 @@ const server = createServer(async (req, res) => {
|
|
|
813
966
|
return jsonRes(res, 400, { ok: false, error: 'empty_patch' });
|
|
814
967
|
return jsonRes(res, 200, { ok: true, settings: resolveDashboardSettings() });
|
|
815
968
|
}
|
|
969
|
+
// ─── Version & manual update ─────────────────────────────────────────────
|
|
970
|
+
// `npm install -g` and a host restart are privileged: none of these paths
|
|
971
|
+
// are on PUBLIC_READ_PATHS, so decideDashboardAuth already 401s an
|
|
972
|
+
// unauthenticated caller (in both normal and public-read mode). The explicit
|
|
973
|
+
// `authed` guards on the two mutations are defense-in-depth for host actions.
|
|
974
|
+
if (req.method === 'GET' && url.pathname === '/api/update/status') {
|
|
975
|
+
const current = resolveCurrentVersion();
|
|
976
|
+
// Compare against the npm `latest` dist-tag (always stable; the update
|
|
977
|
+
// button installs `@latest`). isNewerVersion uses semver precedence, so a
|
|
978
|
+
// canary running AHEAD of the latest stable (e.g. 2.87.0-canary.0 vs
|
|
979
|
+
// 2.86.0) is NOT flagged behind — exactly the canary case we want.
|
|
980
|
+
const latest = await cachedLatestVersion();
|
|
981
|
+
return jsonRes(res, 200, {
|
|
982
|
+
current,
|
|
983
|
+
latest,
|
|
984
|
+
behind: !!latest && isNewerVersion(latest, current),
|
|
985
|
+
localDevInstall: isLocalDevInstall(),
|
|
986
|
+
node: checkNode(),
|
|
987
|
+
installs: detectBotmuxInstalls(),
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
if (req.method === 'GET' && url.pathname === '/api/update/changelog') {
|
|
991
|
+
const current = resolveCurrentVersion();
|
|
992
|
+
const result = await cachedChangelog(current);
|
|
993
|
+
return jsonRes(res, 200, {
|
|
994
|
+
current,
|
|
995
|
+
ok: result.ok,
|
|
996
|
+
rateLimited: result.rateLimited === true,
|
|
997
|
+
releases: result.releases,
|
|
998
|
+
releasesUrl: `https://github.com/${GITHUB_REPO}/releases`,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if (req.method === 'POST' && url.pathname === '/api/update/run') {
|
|
1002
|
+
if (!authed)
|
|
1003
|
+
return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
|
|
1004
|
+
if (isLocalDevInstall())
|
|
1005
|
+
return jsonRes(res, 400, { ok: false, error: 'local_dev_no_update' });
|
|
1006
|
+
const node = checkNode();
|
|
1007
|
+
if (!node.ok)
|
|
1008
|
+
return jsonRes(res, 400, { ok: false, error: 'node_too_old', node });
|
|
1009
|
+
if (updateInFlight)
|
|
1010
|
+
return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
|
|
1011
|
+
updateInFlight = true;
|
|
1012
|
+
const oldVersion = botmuxVersion();
|
|
1013
|
+
// Acquire the shared cross-process lock so a scheduled maintenance
|
|
1014
|
+
// auto-update (running in the bot-0 daemon) can't `npm install -g` at the
|
|
1015
|
+
// same time. `acquired` distinguishes "lock held by maintenance" (409)
|
|
1016
|
+
// from "npm itself failed" (500). Short wait: don't block the request on a
|
|
1017
|
+
// full in-progress install — report busy fast.
|
|
1018
|
+
let acquired = false;
|
|
1019
|
+
try {
|
|
1020
|
+
await withFileLock(npmGlobalUpdateLockTarget(), async () => {
|
|
1021
|
+
acquired = true;
|
|
1022
|
+
await runNpmInstallLatest();
|
|
1023
|
+
}, { maxWaitMs: 2_000 });
|
|
1024
|
+
}
|
|
1025
|
+
catch (e) {
|
|
1026
|
+
if (!acquired)
|
|
1027
|
+
return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
|
|
1028
|
+
return jsonRes(res, 500, { ok: false, error: 'npm_failed', detail: e instanceof Error ? e.message : String(e) });
|
|
1029
|
+
}
|
|
1030
|
+
finally {
|
|
1031
|
+
updateInFlight = false;
|
|
1032
|
+
}
|
|
1033
|
+
const newVersion = botmuxVersion();
|
|
1034
|
+
return jsonRes(res, 200, { ok: true, oldVersion, newVersion, changed: newVersion !== oldVersion });
|
|
1035
|
+
}
|
|
1036
|
+
if (req.method === 'POST' && url.pathname === '/api/update/restart') {
|
|
1037
|
+
if (!authed)
|
|
1038
|
+
return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
|
|
1039
|
+
let body = {};
|
|
1040
|
+
try {
|
|
1041
|
+
const parsed = await readJsonBody(req);
|
|
1042
|
+
if (parsed && typeof parsed === 'object')
|
|
1043
|
+
body = parsed;
|
|
1044
|
+
}
|
|
1045
|
+
catch { /* empty / bad body → plain restart */ }
|
|
1046
|
+
const upd = body.update && typeof body.update === 'object' ? body.update : null;
|
|
1047
|
+
// After a manual update, leave an `update` breadcrumb so the fresh daemon
|
|
1048
|
+
// DMs the owner the changelog (reuses the restart-report pipeline). A plain
|
|
1049
|
+
// restart leaves none here; cmdRestart writes a `manual` breadcrumb itself.
|
|
1050
|
+
if (upd && typeof upd.oldVersion === 'string' && typeof upd.newVersion === 'string' && upd.oldVersion !== upd.newVersion) {
|
|
1051
|
+
try {
|
|
1052
|
+
writeRestartIntent({ kind: 'update', oldVersion: upd.oldVersion, newVersion: upd.newVersion, at: new Date().toISOString() });
|
|
1053
|
+
}
|
|
1054
|
+
catch { /* breadcrumb is best-effort */ }
|
|
1055
|
+
}
|
|
1056
|
+
spawnDetachedRestart('dashboard');
|
|
1057
|
+
return jsonRes(res, 200, { ok: true });
|
|
1058
|
+
}
|
|
816
1059
|
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
817
1060
|
return jsonRes(res, 200, dashboardSkillsPayload());
|
|
818
1061
|
}
|
|
@@ -865,6 +1108,30 @@ const server = createServer(async (req, res) => {
|
|
|
865
1108
|
return jsonRes(res, 400, { ok: false, error: redactGitUrlCredentials(err?.message ?? String(err)) });
|
|
866
1109
|
}
|
|
867
1110
|
}
|
|
1111
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/install-local-links') {
|
|
1112
|
+
let parsed;
|
|
1113
|
+
try {
|
|
1114
|
+
parsed = await readJsonBody(req);
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1118
|
+
}
|
|
1119
|
+
const sources = parseInstallLocalLinksSources(parsed);
|
|
1120
|
+
if (sources.length === 0)
|
|
1121
|
+
return jsonRes(res, 400, { ok: false, error: 'sources_required' });
|
|
1122
|
+
if (sources.length > MAX_LOCAL_LINK_SOURCES)
|
|
1123
|
+
return jsonRes(res, 400, { ok: false, error: 'too_many_sources' });
|
|
1124
|
+
try {
|
|
1125
|
+
const skills = installLocalSkillLinks(sources);
|
|
1126
|
+
// Frontend re-fetches /api/skills (refresh()) after success, so we keep
|
|
1127
|
+
// the response lean — no need to spread a full dashboardSkillsPayload()
|
|
1128
|
+
// (which would re-run the native-skill discovery scan a second time).
|
|
1129
|
+
return jsonRes(res, 200, { ok: true, installed: skills.map(sanitizeSkillForDashboard) });
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
return jsonRes(res, 400, { ok: false, error: redactGitUrlCredentials(err?.message ?? String(err)) });
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
868
1135
|
let mSkillJob;
|
|
869
1136
|
if (req.method === 'GET' && (mSkillJob = url.pathname.match(/^\/api\/skills\/jobs\/([^/]+)$/))) {
|
|
870
1137
|
const job = skillJobs.get(decodeURIComponent(mSkillJob[1]));
|
|
@@ -908,6 +1175,28 @@ const server = createServer(async (req, res) => {
|
|
|
908
1175
|
...dashboardSkillsPayload(),
|
|
909
1176
|
});
|
|
910
1177
|
}
|
|
1178
|
+
if (req.method === 'GET' && url.pathname === '/api/whiteboards') {
|
|
1179
|
+
return jsonRes(res, 200, { enabled: whiteboardEnabled(), whiteboards: listWhiteboards() });
|
|
1180
|
+
}
|
|
1181
|
+
const mWhiteboard = url.pathname.match(/^\/api\/whiteboards\/([^/]+)$/);
|
|
1182
|
+
if (req.method === 'GET' && mWhiteboard) {
|
|
1183
|
+
try {
|
|
1184
|
+
const id = decodeURIComponent(mWhiteboard[1]);
|
|
1185
|
+
return jsonRes(res, 200, { enabled: whiteboardEnabled(), id, content: readWhiteboard(id, { allowDisabled: true }) });
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
return jsonRes(res, 404, { ok: false, error: err?.message ?? 'whiteboard_not_found' });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (req.method === 'DELETE' && mWhiteboard) {
|
|
1192
|
+
try {
|
|
1193
|
+
const id = decodeURIComponent(mWhiteboard[1]);
|
|
1194
|
+
return jsonRes(res, 200, deleteWhiteboard(id));
|
|
1195
|
+
}
|
|
1196
|
+
catch (err) {
|
|
1197
|
+
return jsonRes(res, 400, { ok: false, error: err?.message ?? 'whiteboard_delete_failed' });
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
911
1200
|
if (await handleConnectorApi(req, res, url)) {
|
|
912
1201
|
return;
|
|
913
1202
|
}
|
|
@@ -1011,7 +1300,7 @@ const server = createServer(async (req, res) => {
|
|
|
1011
1300
|
return jsonRes(res, 200, { job });
|
|
1012
1301
|
}
|
|
1013
1302
|
let m;
|
|
1014
|
-
if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume)$/))) {
|
|
1303
|
+
if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume|restart|start)$/))) {
|
|
1015
1304
|
const sid = decodeURIComponent(m[1]);
|
|
1016
1305
|
const op = m[2];
|
|
1017
1306
|
const owner = aggregator.ownerOf(sid);
|
|
@@ -1658,6 +1947,24 @@ const server = createServer(async (req, res) => {
|
|
|
1658
1947
|
res.end(await upstream.text());
|
|
1659
1948
|
return;
|
|
1660
1949
|
}
|
|
1950
|
+
// PUT /api/bots/:appId/env — proxy to that bot's daemon. Body
|
|
1951
|
+
// `{ env: string }` (raw JSON text; '' = clear).
|
|
1952
|
+
let mBotEnv;
|
|
1953
|
+
if (req.method === 'PUT' && (mBotEnv = url.pathname.match(/^\/api\/bots\/([^/]+)\/env$/))) {
|
|
1954
|
+
const appId = decodeURIComponent(mBotEnv[1]);
|
|
1955
|
+
const chunks = [];
|
|
1956
|
+
for await (const c of req)
|
|
1957
|
+
chunks.push(c);
|
|
1958
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1959
|
+
const upstream = await proxyToDaemon(appId, `/api/bot-env`, {
|
|
1960
|
+
method: 'PUT',
|
|
1961
|
+
headers: { 'content-type': 'application/json' },
|
|
1962
|
+
body: raw,
|
|
1963
|
+
});
|
|
1964
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1965
|
+
res.end(await upstream.text());
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1661
1968
|
// PUT /api/bots/:appId/sandbox — proxy to that bot's daemon. Body `{ enabled: boolean }`.
|
|
1662
1969
|
let mBotSandbox;
|
|
1663
1970
|
if (req.method === 'PUT' && (mBotSandbox = url.pathname.match(/^\/api\/bots\/([^/]+)\/sandbox$/))) {
|
|
@@ -1713,7 +2020,8 @@ const server = createServer(async (req, res) => {
|
|
|
1713
2020
|
return;
|
|
1714
2021
|
}
|
|
1715
2022
|
// PUT /api/bots/:appId/grant-prefs — proxy to that bot's daemon. Body carries
|
|
1716
|
-
// any subset of `{ restrictGrantCommands?: boolean,
|
|
2023
|
+
// any subset of `{ restrictGrantCommands?: boolean, autoGrantRequestCards?: boolean,
|
|
2024
|
+
// messageQuotaDefaultLimit?: number|null }`.
|
|
1717
2025
|
let mBotGrantPrefs;
|
|
1718
2026
|
if (req.method === 'PUT' && (mBotGrantPrefs = url.pathname.match(/^\/api\/bots\/([^/]+)\/grant-prefs$/))) {
|
|
1719
2027
|
const appId = decodeURIComponent(mBotGrantPrefs[1]);
|
|
@@ -1841,6 +2149,133 @@ const server = createServer(async (req, res) => {
|
|
|
1841
2149
|
res.end(upstreamJson ? JSON.stringify(upstreamJson) : upstreamText);
|
|
1842
2150
|
return;
|
|
1843
2151
|
}
|
|
2152
|
+
// Dashboard「创建会话」:建飞书群 + 拉选中的 bot,然后按协作模式给各 bot 拉起/暂存
|
|
2153
|
+
// 一条 chat-scope 会话。一起开工=每个被选 bot 各起一条;lead 分配=只起 lead,由它
|
|
2154
|
+
// 在群里 @ 拉起 sub bot。in_progress=立即开跑;backlog=入待办池(parked,等激活)。
|
|
2155
|
+
if (req.method === 'POST' && url.pathname === '/api/sessions/create') {
|
|
2156
|
+
let parsed;
|
|
2157
|
+
try {
|
|
2158
|
+
const chunks = [];
|
|
2159
|
+
for await (const c of req)
|
|
2160
|
+
chunks.push(c);
|
|
2161
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
2162
|
+
}
|
|
2163
|
+
catch {
|
|
2164
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
2165
|
+
}
|
|
2166
|
+
const content = typeof parsed.content === 'string' ? parsed.content.replace(/\s+$/u, '') : '';
|
|
2167
|
+
if (!content.trim())
|
|
2168
|
+
return jsonRes(res, 400, { ok: false, error: 'empty_content' });
|
|
2169
|
+
if (content.length > 8000)
|
|
2170
|
+
return jsonRes(res, 400, { ok: false, error: 'content_too_long' });
|
|
2171
|
+
const selectedIds = Array.isArray(parsed.larkAppIds)
|
|
2172
|
+
? Array.from(new Set(parsed.larkAppIds.filter((x) => typeof x === 'string')))
|
|
2173
|
+
: [];
|
|
2174
|
+
if (selectedIds.length === 0)
|
|
2175
|
+
return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
|
|
2176
|
+
const mode = parsed.mode === 'lead' ? 'lead' : parsed.mode === 'all' ? 'all' : null;
|
|
2177
|
+
if (!mode)
|
|
2178
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_mode' });
|
|
2179
|
+
const column = parsed.column === 'backlog' ? 'backlog' : parsed.column === 'in_progress' ? 'in_progress' : null;
|
|
2180
|
+
if (!column)
|
|
2181
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_column' });
|
|
2182
|
+
const bindWorkingDir = typeof parsed.bindWorkingDir === 'string' && parsed.bindWorkingDir.trim()
|
|
2183
|
+
? parsed.bindWorkingDir.trim() : undefined;
|
|
2184
|
+
const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim().slice(0, 60) : undefined;
|
|
2185
|
+
// 解析 creator:lead 模式 = lead bot;一起开工 = pickCreatorForGroup 在选中里挑一个在线的。
|
|
2186
|
+
let creatorLarkAppId;
|
|
2187
|
+
if (mode === 'lead') {
|
|
2188
|
+
const leadLarkAppId = typeof parsed.leadLarkAppId === 'string' ? parsed.leadLarkAppId : '';
|
|
2189
|
+
if (!leadLarkAppId || !selectedIds.includes(leadLarkAppId)) {
|
|
2190
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_lead' });
|
|
2191
|
+
}
|
|
2192
|
+
if (!registry.getByAppId(leadLarkAppId))
|
|
2193
|
+
return jsonRes(res, 503, { ok: false, error: 'lead_offline' });
|
|
2194
|
+
creatorLarkAppId = leadLarkAppId;
|
|
2195
|
+
}
|
|
2196
|
+
else {
|
|
2197
|
+
const pick = pickCreatorForGroup(selectedIds, (id) => {
|
|
2198
|
+
const d = registry.getByAppId(id);
|
|
2199
|
+
return d ? { larkAppId: d.larkAppId, resolvedAllowedUsers: d.resolvedAllowedUsers ?? [] } : undefined;
|
|
2200
|
+
});
|
|
2201
|
+
if (!pick)
|
|
2202
|
+
return jsonRes(res, 503, { ok: false, error: 'no_online_daemon' });
|
|
2203
|
+
creatorLarkAppId = pick.creatorLarkAppId;
|
|
2204
|
+
}
|
|
2205
|
+
// creator 作用域里的操作者 open_id(首个 ou_ allowedUser)——用于邀请进群 + 转群主 + @通知。
|
|
2206
|
+
// 同时取 on_(union_id,租户内跨 app 稳定)做兜底邀请:lead 模式强制 creator=lead,
|
|
2207
|
+
// 万一 lead 的 allowlist 没有 ou_ 条目,open_id 解析不到、操作者就进不了群——union_id
|
|
2208
|
+
// 不受 app 作用域影响,仍能把人拉进来(createGroupWithBots 走 ownerUnionIds 通道)。
|
|
2209
|
+
const creatorDesc = registry.getByAppId(creatorLarkAppId);
|
|
2210
|
+
const allowed = creatorDesc.resolvedAllowedUsers ?? [];
|
|
2211
|
+
const userOpenId = allowed.find(u => u.startsWith('ou_'));
|
|
2212
|
+
const ownerUnionIds = allowed.filter(u => u.startsWith('on_'));
|
|
2213
|
+
// 建群(拉所有选中 bot + 邀请操作者 + 转群主 + @通知 + 可选绑 oncall 工作目录)。
|
|
2214
|
+
let groupResp = null;
|
|
2215
|
+
try {
|
|
2216
|
+
const groupUpstream = await proxyToDaemon(creatorLarkAppId, '/api/groups/create', {
|
|
2217
|
+
method: 'POST',
|
|
2218
|
+
headers: { 'content-type': 'application/json' },
|
|
2219
|
+
body: JSON.stringify({
|
|
2220
|
+
name,
|
|
2221
|
+
larkAppIds: selectedIds,
|
|
2222
|
+
userOpenIds: userOpenId ? [userOpenId] : [],
|
|
2223
|
+
ownerUnionIds,
|
|
2224
|
+
transferOwnerTo: userOpenId,
|
|
2225
|
+
notifyOwnerOpenId: userOpenId,
|
|
2226
|
+
bindWorkingDir,
|
|
2227
|
+
}),
|
|
2228
|
+
});
|
|
2229
|
+
groupResp = await groupUpstream.json().catch(() => null);
|
|
2230
|
+
if (!groupUpstream.ok || !groupResp?.ok || typeof groupResp.chatId !== 'string') {
|
|
2231
|
+
return jsonRes(res, 502, { ok: false, error: groupResp?.error ?? `group_create_http_${groupUpstream.status}` });
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
catch {
|
|
2235
|
+
return jsonRes(res, 502, { ok: false, error: 'group_create_proxy_failed' });
|
|
2236
|
+
}
|
|
2237
|
+
const chatId = groupResp.chatId;
|
|
2238
|
+
const invalidBotIds = Array.isArray(groupResp.invalidBotIds) ? groupResp.invalidBotIds : [];
|
|
2239
|
+
// spawn 目标:lead 模式只有 lead;一起开工是所有成功入群的选中 bot。
|
|
2240
|
+
const joinedIds = selectedIds.filter(id => !invalidBotIds.includes(id) && !!registry.getByAppId(id));
|
|
2241
|
+
const targets = mode === 'lead'
|
|
2242
|
+
? (joinedIds.includes(creatorLarkAppId) ? [creatorLarkAppId] : [])
|
|
2243
|
+
: joinedIds;
|
|
2244
|
+
if (targets.length === 0) {
|
|
2245
|
+
return jsonRes(res, 200, { ok: true, chatId, shareLink: groupResp.shareLink, spawned: [], failed: [], warning: 'no_spawn_target' });
|
|
2246
|
+
}
|
|
2247
|
+
const bots = liveBots();
|
|
2248
|
+
const nameOf = (id) => bots.find(b => b.larkAppId === id)?.botName ?? id;
|
|
2249
|
+
const spawned = [];
|
|
2250
|
+
const failed = [];
|
|
2251
|
+
await Promise.all(targets.map(async (appId) => {
|
|
2252
|
+
const role = mode === 'lead' ? 'lead' : (targets.length > 1 ? 'collab' : 'solo');
|
|
2253
|
+
// lead 的 coworker = 所有 sub(除自己);collab 的 coworker = 其它并列 bot(除自己)。
|
|
2254
|
+
const coworkerIds = (mode === 'lead' ? selectedIds : targets).filter(id => id !== appId);
|
|
2255
|
+
const coworkers = coworkerIds.map(id => ({ name: nameOf(id) }));
|
|
2256
|
+
try {
|
|
2257
|
+
const up = await proxyToDaemon(appId, '/api/sessions/spawn', {
|
|
2258
|
+
method: 'POST',
|
|
2259
|
+
headers: { 'content-type': 'application/json' },
|
|
2260
|
+
body: JSON.stringify({
|
|
2261
|
+
chatId, content, column, role, coworkers,
|
|
2262
|
+
postBanner: appId === creatorLarkAppId,
|
|
2263
|
+
}),
|
|
2264
|
+
});
|
|
2265
|
+
const b = await up.json().catch(() => null);
|
|
2266
|
+
if (up.ok && b?.ok)
|
|
2267
|
+
spawned.push(appId);
|
|
2268
|
+
else
|
|
2269
|
+
failed.push({ larkAppId: appId, error: b?.error ?? `http_${up.status}` });
|
|
2270
|
+
}
|
|
2271
|
+
catch (e) {
|
|
2272
|
+
failed.push({ larkAppId: appId, error: e?.message ?? String(e) });
|
|
2273
|
+
}
|
|
2274
|
+
}));
|
|
2275
|
+
return jsonRes(res, 200, {
|
|
2276
|
+
ok: true, chatId, shareLink: groupResp.shareLink, mode, column, spawned, failed,
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
1844
2279
|
// Public SSE — relays aggregator's listener events
|
|
1845
2280
|
if (req.method === 'GET' && url.pathname === '/events') {
|
|
1846
2281
|
res.writeHead(200, {
|
|
@@ -1899,6 +2334,7 @@ listenWithProbe({
|
|
|
1899
2334
|
logger.warn(`[dashboard] Failed to persist port to ${PORT_PATH}: ${e.message}`);
|
|
1900
2335
|
}
|
|
1901
2336
|
logger.info(`[dashboard] listening on ${config.dashboard.host}:${port}`);
|
|
2337
|
+
startPlatformTunnelIfBound();
|
|
1902
2338
|
}).catch((err) => {
|
|
1903
2339
|
logger.error(`[dashboard] could not bind near ${config.dashboard.host}:${config.dashboard.port} after probing — set BOTMUX_DASHBOARD_PORT to a free port. ${err.message}`);
|
|
1904
2340
|
process.exit(1);
|
|
@@ -1913,12 +2349,43 @@ const federationSync = setInterval(() => {
|
|
|
1913
2349
|
.catch(() => { });
|
|
1914
2350
|
}, 2 * 60 * 1000);
|
|
1915
2351
|
federationSync.unref();
|
|
2352
|
+
// 中心化平台隧道(已绑定才启动;每台机器一个,跑在 dashboard 进程里)
|
|
2353
|
+
let platformTunnel = null;
|
|
2354
|
+
function readBotmuxVersion() {
|
|
2355
|
+
try {
|
|
2356
|
+
const pkg = JSON.parse(readFileSync(join(dirname(__dirname), 'package.json'), 'utf8'));
|
|
2357
|
+
return pkg.version || 'unknown';
|
|
2358
|
+
}
|
|
2359
|
+
catch {
|
|
2360
|
+
return 'unknown';
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
function startPlatformTunnelIfBound() {
|
|
2364
|
+
try {
|
|
2365
|
+
const binding = readPlatformBinding();
|
|
2366
|
+
if (!binding)
|
|
2367
|
+
return;
|
|
2368
|
+
const version = readBotmuxVersion();
|
|
2369
|
+
platformTunnel = startPlatformTunnelClient({
|
|
2370
|
+
binding,
|
|
2371
|
+
getDashboardPort: () => boundDashboardPort,
|
|
2372
|
+
getDashboardToken: () => activeToken,
|
|
2373
|
+
getVersion: () => version,
|
|
2374
|
+
log: (msg, extra) => logger.info(`[platform-tunnel] ${msg}${extra ? ' ' + JSON.stringify(extra) : ''}`),
|
|
2375
|
+
});
|
|
2376
|
+
logger.info(`[platform-tunnel] 绑定到 ${binding.platformUrl},启动隧道`);
|
|
2377
|
+
}
|
|
2378
|
+
catch (e) {
|
|
2379
|
+
logger.warn(`[platform-tunnel] 启动失败: ${e.message}`);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
1916
2382
|
// Graceful shutdown
|
|
1917
2383
|
function shutdown() {
|
|
1918
2384
|
for (const off of subs.values())
|
|
1919
2385
|
off();
|
|
1920
2386
|
subs.clear();
|
|
1921
2387
|
registry.stop();
|
|
2388
|
+
platformTunnel?.stop();
|
|
1922
2389
|
server.close(() => process.exit(0));
|
|
1923
2390
|
// Hard-exit fallback after 5s
|
|
1924
2391
|
setTimeout(() => process.exit(0), 5_000).unref();
|