botmux 2.85.1 → 2.87.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 +26 -0
- package/README.md +26 -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/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 +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +22 -9
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
- 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/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/bot-registry.d.ts +10 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +6 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +362 -16
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +215 -5
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +7 -106
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +285 -2
- package/dist/core/dashboard-ipc-server.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/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +1 -1
- package/dist/core/passthrough-commands.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/role-resolver.d.ts +1 -0
- package/dist/core/role-resolver.d.ts.map +1 -1
- package/dist/core/role-resolver.js +14 -0
- package/dist/core/role-resolver.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +70 -6
- 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 +4 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +29 -6
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +1 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +1 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- 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 +43 -4
- 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 +178 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/groups.d.ts +2 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -1
- package/dist/dashboard/web/groups.js +419 -3
- package/dist/dashboard/web/groups.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +745 -3
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/insights.d.ts +2 -0
- package/dist/dashboard/web/insights.d.ts.map +1 -0
- package/dist/dashboard/web/insights.js +1523 -0
- package/dist/dashboard/web/insights.js.map +1 -0
- package/dist/dashboard/web/role-profile-match.d.ts +31 -0
- package/dist/dashboard/web/role-profile-match.d.ts.map +1 -0
- package/dist/dashboard/web/role-profile-match.js +58 -0
- package/dist/dashboard/web/role-profile-match.js.map +1 -0
- package/dist/dashboard/web/roles.d.ts +1 -0
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +520 -27
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +84 -0
- 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 +1406 -833
- package/dist/dashboard-web/index.html +3 -1
- package/dist/dashboard-web/style.css +1382 -30
- package/dist/dashboard.js +474 -6
- 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 +34 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +34 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +7 -3
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +19 -0
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/group-creator.d.ts +6 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +54 -5
- package/dist/services/group-creator.js.map +1 -1
- package/dist/services/insight/antigravity-span-reader.d.ts +3 -0
- package/dist/services/insight/antigravity-span-reader.d.ts.map +1 -0
- package/dist/services/insight/antigravity-span-reader.js +249 -0
- package/dist/services/insight/antigravity-span-reader.js.map +1 -0
- package/dist/services/insight/classify.d.ts +7 -0
- package/dist/services/insight/classify.d.ts.map +1 -0
- package/dist/services/insight/classify.js +46 -0
- package/dist/services/insight/classify.js.map +1 -0
- package/dist/services/insight/claude-span-reader.d.ts +3 -0
- package/dist/services/insight/claude-span-reader.d.ts.map +1 -0
- package/dist/services/insight/claude-span-reader.js +257 -0
- package/dist/services/insight/claude-span-reader.js.map +1 -0
- package/dist/services/insight/codex-span-reader.d.ts +3 -0
- package/dist/services/insight/codex-span-reader.d.ts.map +1 -0
- package/dist/services/insight/codex-span-reader.js +290 -0
- package/dist/services/insight/codex-span-reader.js.map +1 -0
- package/dist/services/insight/intent.d.ts +5 -0
- package/dist/services/insight/intent.d.ts.map +1 -0
- package/dist/services/insight/intent.js +145 -0
- package/dist/services/insight/intent.js.map +1 -0
- package/dist/services/insight/jsonl.d.ts +10 -0
- package/dist/services/insight/jsonl.d.ts.map +1 -0
- package/dist/services/insight/jsonl.js +36 -0
- package/dist/services/insight/jsonl.js.map +1 -0
- package/dist/services/insight/prompt.d.ts +3 -0
- package/dist/services/insight/prompt.d.ts.map +1 -0
- package/dist/services/insight/prompt.js +99 -0
- package/dist/services/insight/prompt.js.map +1 -0
- package/dist/services/insight/redact.d.ts +4 -0
- package/dist/services/insight/redact.d.ts.map +1 -0
- package/dist/services/insight/redact.js +67 -0
- package/dist/services/insight/redact.js.map +1 -0
- package/dist/services/insight/report.d.ts +29 -0
- package/dist/services/insight/report.d.ts.map +1 -0
- package/dist/services/insight/report.js +1126 -0
- package/dist/services/insight/report.js.map +1 -0
- package/dist/services/insight/safe-detail.d.ts +5 -0
- package/dist/services/insight/safe-detail.d.ts.map +1 -0
- package/dist/services/insight/safe-detail.js +59 -0
- package/dist/services/insight/safe-detail.js.map +1 -0
- package/dist/services/insight/scrub.d.ts +22 -0
- package/dist/services/insight/scrub.d.ts.map +1 -0
- package/dist/services/insight/scrub.js +70 -0
- package/dist/services/insight/scrub.js.map +1 -0
- package/dist/services/insight/types.d.ts +394 -0
- package/dist/services/insight/types.d.ts.map +1 -0
- package/dist/services/insight/types.js +2 -0
- package/dist/services/insight/types.js.map +1 -0
- 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/role-profile-store.d.ts +25 -0
- package/dist/services/role-profile-store.d.ts.map +1 -0
- package/dist/services/role-profile-store.js +171 -0
- package/dist/services/role-profile-store.js.map +1 -0
- 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/transcript-resolver.d.ts +26 -0
- package/dist/services/transcript-resolver.d.ts.map +1 -0
- package/dist/services/transcript-resolver.js +111 -0
- package/dist/services/transcript-resolver.js.map +1 -0
- 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/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 +3 -0
- package/dist/types.d.ts.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/worker.js +12 -0
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -27,18 +27,32 @@ 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';
|
|
49
|
+
import { isValidRoleProfileId } from './services/role-profile-store.js';
|
|
50
|
+
import { mergeSafeInsightOverviews } from './services/insight/report.js';
|
|
40
51
|
const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
41
52
|
const TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
|
|
53
|
+
/** Per-daemon budget for the cross-daemon insight overview fan-out — bounds
|
|
54
|
+
* aggregate latency when one daemon's insight parse is slow or hung. */
|
|
55
|
+
const INSIGHT_FANOUT_TIMEOUT_MS = 10_000;
|
|
42
56
|
const BOTS_JSON_PATH = join(homedir(), '.botmux', 'bots.json');
|
|
43
57
|
const REGISTRY_DIR = join(homedir(), '.botmux', 'data', 'dashboard-daemons');
|
|
44
58
|
// The dashboard probes upward if its configured port is busy (e.g. a second
|
|
@@ -111,6 +125,7 @@ function resolveDashboardSettings() {
|
|
|
111
125
|
repoPickerMode: global.repoPickerMode ?? 'all',
|
|
112
126
|
maintenance: global.maintenance ?? {},
|
|
113
127
|
localDevInstall: isLocalDevInstall(),
|
|
128
|
+
whiteboard: { enabled: global.whiteboard?.enabled === true },
|
|
114
129
|
};
|
|
115
130
|
}
|
|
116
131
|
async function readJsonBody(req) {
|
|
@@ -120,6 +135,67 @@ async function readJsonBody(req) {
|
|
|
120
135
|
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
121
136
|
return raw ? JSON.parse(raw) : {};
|
|
122
137
|
}
|
|
138
|
+
/** Fast in-process guard against double-clicks within this dashboard process.
|
|
139
|
+
* Cross-process serialization against the maintenance auto-update (a different
|
|
140
|
+
* process) is handled separately by the shared file lock in the run route. */
|
|
141
|
+
let updateInFlight = false;
|
|
142
|
+
// Cache the upstream version/changelog lookups so the nav-badge check + the
|
|
143
|
+
// Settings card don't hammer the npm registry / GitHub on every page load.
|
|
144
|
+
// GitHub's unauthenticated API is only 60 req/h per IP, so caching the changelog
|
|
145
|
+
// also keeps us from exhausting it. Failures cache briefly so they self-heal.
|
|
146
|
+
const LATEST_TTL_MS = 30 * 60_000;
|
|
147
|
+
const CHANGELOG_TTL_MS = 15 * 60_000;
|
|
148
|
+
const FAILURE_TTL_MS = 60_000;
|
|
149
|
+
let latestVersionCache = null;
|
|
150
|
+
let changelogCache = null;
|
|
151
|
+
async function cachedLatestVersion(now = Date.now()) {
|
|
152
|
+
const ttl = latestVersionCache?.value ? LATEST_TTL_MS : FAILURE_TTL_MS;
|
|
153
|
+
if (latestVersionCache && now - latestVersionCache.at < ttl)
|
|
154
|
+
return latestVersionCache.value;
|
|
155
|
+
const value = await fetchLatestVersion();
|
|
156
|
+
latestVersionCache = { value, at: now };
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
async function cachedChangelog(current, now = Date.now()) {
|
|
160
|
+
const ttl = changelogCache?.value.ok ? CHANGELOG_TTL_MS : FAILURE_TTL_MS;
|
|
161
|
+
if (changelogCache && changelogCache.key === current && now - changelogCache.at < ttl)
|
|
162
|
+
return changelogCache.value;
|
|
163
|
+
const value = await fetchReleasesSince(current);
|
|
164
|
+
changelogCache = { key: current, value, at: now };
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Run `npm install -g botmux@latest` for the manual-update flow WITHOUT blocking
|
|
169
|
+
* the event loop (async spawn, not execSync — the dashboard must keep serving
|
|
170
|
+
* during the ~10-30s install). Resolves on exit 0; rejects with the tail of
|
|
171
|
+
* stdout/stderr on a non-zero exit, spawn error, or 3-minute timeout. Args are
|
|
172
|
+
* a fixed literal — no shell interpolation of untrusted input.
|
|
173
|
+
*/
|
|
174
|
+
function runNpmInstallLatest() {
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const child = spawn('npm', ['install', '-g', 'botmux@latest'], {
|
|
177
|
+
env: process.env,
|
|
178
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
179
|
+
shell: process.platform === 'win32', // resolve npm.cmd on Windows
|
|
180
|
+
});
|
|
181
|
+
let tail = '';
|
|
182
|
+
const capture = (d) => { tail = (tail + d.toString()).slice(-2000); };
|
|
183
|
+
child.stdout?.on('data', capture);
|
|
184
|
+
child.stderr?.on('data', capture);
|
|
185
|
+
const timer = setTimeout(() => {
|
|
186
|
+
child.kill('SIGKILL');
|
|
187
|
+
reject(new Error('npm install timed out after 180s'));
|
|
188
|
+
}, 180_000);
|
|
189
|
+
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
190
|
+
child.on('exit', (code) => {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
if (code === 0)
|
|
193
|
+
resolve();
|
|
194
|
+
else
|
|
195
|
+
reject(new Error(`npm exited ${code}: ${tail.trim().slice(-500)}`));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
123
199
|
/**
|
|
124
200
|
* Attach to one daemon: hydrate its sessions/schedules into the aggregator,
|
|
125
201
|
* THEN open the SSE subscription. Order matters — hydrating after subscribe
|
|
@@ -219,7 +295,7 @@ function serveFileAbs(res, fp) {
|
|
|
219
295
|
createReadStream(fp).pipe(res);
|
|
220
296
|
return true;
|
|
221
297
|
}
|
|
222
|
-
function serveStatic(
|
|
298
|
+
function serveStatic(req, res, pathname) {
|
|
223
299
|
const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
224
300
|
const fp = resolve(WEB_DIR, rel);
|
|
225
301
|
const webRoot = resolve(WEB_DIR);
|
|
@@ -231,7 +307,23 @@ function serveStatic(_req, res, pathname) {
|
|
|
231
307
|
const st = statSync(fp);
|
|
232
308
|
if (!st.isFile())
|
|
233
309
|
return false;
|
|
234
|
-
|
|
310
|
+
// Bundle filenames are fixed (app.js/style.css), so without revalidation
|
|
311
|
+
// browsers heuristic-cache them and serve a stale build after a deploy
|
|
312
|
+
// (new JS + old CSS → broken layout). `no-cache` + an mtime/size ETag makes
|
|
313
|
+
// the browser revalidate every load: 304 when unchanged (cheap), fresh 200
|
|
314
|
+
// when the build changed. No manual hard-refresh needed after deploy.
|
|
315
|
+
const etag = `W/"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
|
|
316
|
+
const headers = {
|
|
317
|
+
'content-type': MIME[extname(fp)] ?? 'application/octet-stream',
|
|
318
|
+
'cache-control': 'no-cache',
|
|
319
|
+
etag,
|
|
320
|
+
};
|
|
321
|
+
if (req.headers['if-none-match'] === etag) {
|
|
322
|
+
res.writeHead(304, headers);
|
|
323
|
+
res.end();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
res.writeHead(200, headers);
|
|
235
327
|
res.end(readFileSync(fp));
|
|
236
328
|
return true;
|
|
237
329
|
}
|
|
@@ -302,7 +394,13 @@ async function createTeamGroup(args) {
|
|
|
302
394
|
const upstream = await proxyToDaemon(plan.creatorLarkAppId, '/api/groups/create', {
|
|
303
395
|
method: 'POST',
|
|
304
396
|
headers: { 'content-type': 'application/json' },
|
|
305
|
-
body: JSON.stringify({
|
|
397
|
+
body: JSON.stringify({
|
|
398
|
+
name: args.name,
|
|
399
|
+
larkAppIds: selectedIds,
|
|
400
|
+
userOpenIds,
|
|
401
|
+
ownerUnionIds: args.ownerUnionIds ?? [],
|
|
402
|
+
...(args.roleProfileId ? { roleProfileId: args.roleProfileId } : {}),
|
|
403
|
+
}),
|
|
306
404
|
});
|
|
307
405
|
const text = await upstream.text();
|
|
308
406
|
let parsed = null;
|
|
@@ -480,12 +578,33 @@ function sanitizeSkillForDashboard(skill) {
|
|
|
480
578
|
source: { ...skill.source, url: redactGitUrlCredentials(skill.source.url) },
|
|
481
579
|
};
|
|
482
580
|
}
|
|
581
|
+
function dashboardSkillCliIds() {
|
|
582
|
+
const ids = new Set();
|
|
583
|
+
try {
|
|
584
|
+
for (const cliId of configuredCliIds().values())
|
|
585
|
+
ids.add(cliId);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Fall back to daemon descriptors below when persistent config is unavailable.
|
|
589
|
+
}
|
|
590
|
+
for (const bot of registry.list()) {
|
|
591
|
+
if (bot.cliId)
|
|
592
|
+
ids.add(bot.cliId);
|
|
593
|
+
}
|
|
594
|
+
return [...ids];
|
|
595
|
+
}
|
|
483
596
|
function dashboardSkillsPayload() {
|
|
484
597
|
const globalSkills = readGlobalConfig().skills ?? {};
|
|
598
|
+
const nativeSkillGroups = discoverNativeCliSkillGroups(dashboardSkillCliIds())
|
|
599
|
+
.map(group => ({
|
|
600
|
+
...group,
|
|
601
|
+
skills: group.skills.map(sanitizeSkillForDashboard),
|
|
602
|
+
}));
|
|
485
603
|
return {
|
|
486
604
|
skills: Object.values(readSkillRegistry().skills)
|
|
487
605
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
488
606
|
.map(sanitizeSkillForDashboard),
|
|
607
|
+
nativeSkillGroups,
|
|
489
608
|
trustProjectSkills: globalSkills.trustProjectSkills ?? 'off',
|
|
490
609
|
delivery: globalSkills.delivery ?? 'auto',
|
|
491
610
|
};
|
|
@@ -673,6 +792,29 @@ const server = createServer(async (req, res) => {
|
|
|
673
792
|
});
|
|
674
793
|
return jsonRes(res, 200, { sessions });
|
|
675
794
|
}
|
|
795
|
+
if (req.method === 'GET' && url.pathname === '/api/insights/summary') {
|
|
796
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '200', 10) || 200, 1), 500);
|
|
797
|
+
// Per-daemon timeout + isolate failures: an upstream insight parse can be
|
|
798
|
+
// heavy, so a slow/hung daemon must not stall the aggregated summary. A
|
|
799
|
+
// timed-out / errored chunk drops to null and is filtered out below.
|
|
800
|
+
const chunks = await Promise.all(registry.list().map(async (d) => {
|
|
801
|
+
try {
|
|
802
|
+
const upstream = await proxyToDaemon(d.larkAppId, `/api/insights/summary?limit=${limit}`, {
|
|
803
|
+
method: 'GET',
|
|
804
|
+
signal: AbortSignal.timeout(INSIGHT_FANOUT_TIMEOUT_MS),
|
|
805
|
+
});
|
|
806
|
+
if (!upstream.ok)
|
|
807
|
+
return null;
|
|
808
|
+
const body = await upstream.json().catch(() => null);
|
|
809
|
+
return body?.overview ?? null;
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
}));
|
|
815
|
+
const overview = mergeSafeInsightOverviews(chunks.filter((x) => !!x), { limit });
|
|
816
|
+
return jsonRes(res, 200, { ok: true, overview });
|
|
817
|
+
}
|
|
676
818
|
if (req.method === 'GET' && url.pathname === '/api/schedules') {
|
|
677
819
|
// Public-read carve-out: the row carries CONTENT (prompt = business
|
|
678
820
|
// instructions) and a bound `workingDir` (repo/customer path) — strip
|
|
@@ -724,6 +866,16 @@ const server = createServer(async (req, res) => {
|
|
|
724
866
|
mergeGlobalConfig({ repoPickerMode: v });
|
|
725
867
|
touched = true;
|
|
726
868
|
}
|
|
869
|
+
if ('whiteboard' in body) {
|
|
870
|
+
const raw = body.whiteboard;
|
|
871
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
872
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_whiteboard' });
|
|
873
|
+
const wb = raw;
|
|
874
|
+
if (typeof wb.enabled !== 'boolean')
|
|
875
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_whiteboard_enabled' });
|
|
876
|
+
mergeGlobalConfig({ whiteboard: { enabled: wb.enabled } });
|
|
877
|
+
touched = true;
|
|
878
|
+
}
|
|
727
879
|
if ('maintenance' in body) {
|
|
728
880
|
const r = parseMaintenancePatch(body.maintenance);
|
|
729
881
|
if (!r.ok)
|
|
@@ -763,6 +915,96 @@ const server = createServer(async (req, res) => {
|
|
|
763
915
|
return jsonRes(res, 400, { ok: false, error: 'empty_patch' });
|
|
764
916
|
return jsonRes(res, 200, { ok: true, settings: resolveDashboardSettings() });
|
|
765
917
|
}
|
|
918
|
+
// ─── Version & manual update ─────────────────────────────────────────────
|
|
919
|
+
// `npm install -g` and a host restart are privileged: none of these paths
|
|
920
|
+
// are on PUBLIC_READ_PATHS, so decideDashboardAuth already 401s an
|
|
921
|
+
// unauthenticated caller (in both normal and public-read mode). The explicit
|
|
922
|
+
// `authed` guards on the two mutations are defense-in-depth for host actions.
|
|
923
|
+
if (req.method === 'GET' && url.pathname === '/api/update/status') {
|
|
924
|
+
const current = resolveCurrentVersion();
|
|
925
|
+
// Compare against the npm `latest` dist-tag (always stable; the update
|
|
926
|
+
// button installs `@latest`). isNewerVersion uses semver precedence, so a
|
|
927
|
+
// canary running AHEAD of the latest stable (e.g. 2.87.0-canary.0 vs
|
|
928
|
+
// 2.86.0) is NOT flagged behind — exactly the canary case we want.
|
|
929
|
+
const latest = await cachedLatestVersion();
|
|
930
|
+
return jsonRes(res, 200, {
|
|
931
|
+
current,
|
|
932
|
+
latest,
|
|
933
|
+
behind: !!latest && isNewerVersion(latest, current),
|
|
934
|
+
localDevInstall: isLocalDevInstall(),
|
|
935
|
+
node: checkNode(),
|
|
936
|
+
installs: detectBotmuxInstalls(),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
if (req.method === 'GET' && url.pathname === '/api/update/changelog') {
|
|
940
|
+
const current = resolveCurrentVersion();
|
|
941
|
+
const result = await cachedChangelog(current);
|
|
942
|
+
return jsonRes(res, 200, {
|
|
943
|
+
current,
|
|
944
|
+
ok: result.ok,
|
|
945
|
+
rateLimited: result.rateLimited === true,
|
|
946
|
+
releases: result.releases,
|
|
947
|
+
releasesUrl: `https://github.com/${GITHUB_REPO}/releases`,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
if (req.method === 'POST' && url.pathname === '/api/update/run') {
|
|
951
|
+
if (!authed)
|
|
952
|
+
return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
|
|
953
|
+
if (isLocalDevInstall())
|
|
954
|
+
return jsonRes(res, 400, { ok: false, error: 'local_dev_no_update' });
|
|
955
|
+
const node = checkNode();
|
|
956
|
+
if (!node.ok)
|
|
957
|
+
return jsonRes(res, 400, { ok: false, error: 'node_too_old', node });
|
|
958
|
+
if (updateInFlight)
|
|
959
|
+
return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
|
|
960
|
+
updateInFlight = true;
|
|
961
|
+
const oldVersion = botmuxVersion();
|
|
962
|
+
// Acquire the shared cross-process lock so a scheduled maintenance
|
|
963
|
+
// auto-update (running in the bot-0 daemon) can't `npm install -g` at the
|
|
964
|
+
// same time. `acquired` distinguishes "lock held by maintenance" (409)
|
|
965
|
+
// from "npm itself failed" (500). Short wait: don't block the request on a
|
|
966
|
+
// full in-progress install — report busy fast.
|
|
967
|
+
let acquired = false;
|
|
968
|
+
try {
|
|
969
|
+
await withFileLock(npmGlobalUpdateLockTarget(), async () => {
|
|
970
|
+
acquired = true;
|
|
971
|
+
await runNpmInstallLatest();
|
|
972
|
+
}, { maxWaitMs: 2_000 });
|
|
973
|
+
}
|
|
974
|
+
catch (e) {
|
|
975
|
+
if (!acquired)
|
|
976
|
+
return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
|
|
977
|
+
return jsonRes(res, 500, { ok: false, error: 'npm_failed', detail: e instanceof Error ? e.message : String(e) });
|
|
978
|
+
}
|
|
979
|
+
finally {
|
|
980
|
+
updateInFlight = false;
|
|
981
|
+
}
|
|
982
|
+
const newVersion = botmuxVersion();
|
|
983
|
+
return jsonRes(res, 200, { ok: true, oldVersion, newVersion, changed: newVersion !== oldVersion });
|
|
984
|
+
}
|
|
985
|
+
if (req.method === 'POST' && url.pathname === '/api/update/restart') {
|
|
986
|
+
if (!authed)
|
|
987
|
+
return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
|
|
988
|
+
let body = {};
|
|
989
|
+
try {
|
|
990
|
+
const parsed = await readJsonBody(req);
|
|
991
|
+
if (parsed && typeof parsed === 'object')
|
|
992
|
+
body = parsed;
|
|
993
|
+
}
|
|
994
|
+
catch { /* empty / bad body → plain restart */ }
|
|
995
|
+
const upd = body.update && typeof body.update === 'object' ? body.update : null;
|
|
996
|
+
// After a manual update, leave an `update` breadcrumb so the fresh daemon
|
|
997
|
+
// DMs the owner the changelog (reuses the restart-report pipeline). A plain
|
|
998
|
+
// restart leaves none here; cmdRestart writes a `manual` breadcrumb itself.
|
|
999
|
+
if (upd && typeof upd.oldVersion === 'string' && typeof upd.newVersion === 'string' && upd.oldVersion !== upd.newVersion) {
|
|
1000
|
+
try {
|
|
1001
|
+
writeRestartIntent({ kind: 'update', oldVersion: upd.oldVersion, newVersion: upd.newVersion, at: new Date().toISOString() });
|
|
1002
|
+
}
|
|
1003
|
+
catch { /* breadcrumb is best-effort */ }
|
|
1004
|
+
}
|
|
1005
|
+
spawnDetachedRestart('dashboard');
|
|
1006
|
+
return jsonRes(res, 200, { ok: true });
|
|
1007
|
+
}
|
|
766
1008
|
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
767
1009
|
return jsonRes(res, 200, dashboardSkillsPayload());
|
|
768
1010
|
}
|
|
@@ -815,6 +1057,30 @@ const server = createServer(async (req, res) => {
|
|
|
815
1057
|
return jsonRes(res, 400, { ok: false, error: redactGitUrlCredentials(err?.message ?? String(err)) });
|
|
816
1058
|
}
|
|
817
1059
|
}
|
|
1060
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/install-local-links') {
|
|
1061
|
+
let parsed;
|
|
1062
|
+
try {
|
|
1063
|
+
parsed = await readJsonBody(req);
|
|
1064
|
+
}
|
|
1065
|
+
catch {
|
|
1066
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1067
|
+
}
|
|
1068
|
+
const sources = parseInstallLocalLinksSources(parsed);
|
|
1069
|
+
if (sources.length === 0)
|
|
1070
|
+
return jsonRes(res, 400, { ok: false, error: 'sources_required' });
|
|
1071
|
+
if (sources.length > MAX_LOCAL_LINK_SOURCES)
|
|
1072
|
+
return jsonRes(res, 400, { ok: false, error: 'too_many_sources' });
|
|
1073
|
+
try {
|
|
1074
|
+
const skills = installLocalSkillLinks(sources);
|
|
1075
|
+
// Frontend re-fetches /api/skills (refresh()) after success, so we keep
|
|
1076
|
+
// the response lean — no need to spread a full dashboardSkillsPayload()
|
|
1077
|
+
// (which would re-run the native-skill discovery scan a second time).
|
|
1078
|
+
return jsonRes(res, 200, { ok: true, installed: skills.map(sanitizeSkillForDashboard) });
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
return jsonRes(res, 400, { ok: false, error: redactGitUrlCredentials(err?.message ?? String(err)) });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
818
1084
|
let mSkillJob;
|
|
819
1085
|
if (req.method === 'GET' && (mSkillJob = url.pathname.match(/^\/api\/skills\/jobs\/([^/]+)$/))) {
|
|
820
1086
|
const job = skillJobs.get(decodeURIComponent(mSkillJob[1]));
|
|
@@ -858,6 +1124,28 @@ const server = createServer(async (req, res) => {
|
|
|
858
1124
|
...dashboardSkillsPayload(),
|
|
859
1125
|
});
|
|
860
1126
|
}
|
|
1127
|
+
if (req.method === 'GET' && url.pathname === '/api/whiteboards') {
|
|
1128
|
+
return jsonRes(res, 200, { enabled: whiteboardEnabled(), whiteboards: listWhiteboards() });
|
|
1129
|
+
}
|
|
1130
|
+
const mWhiteboard = url.pathname.match(/^\/api\/whiteboards\/([^/]+)$/);
|
|
1131
|
+
if (req.method === 'GET' && mWhiteboard) {
|
|
1132
|
+
try {
|
|
1133
|
+
const id = decodeURIComponent(mWhiteboard[1]);
|
|
1134
|
+
return jsonRes(res, 200, { enabled: whiteboardEnabled(), id, content: readWhiteboard(id, { allowDisabled: true }) });
|
|
1135
|
+
}
|
|
1136
|
+
catch (err) {
|
|
1137
|
+
return jsonRes(res, 404, { ok: false, error: err?.message ?? 'whiteboard_not_found' });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (req.method === 'DELETE' && mWhiteboard) {
|
|
1141
|
+
try {
|
|
1142
|
+
const id = decodeURIComponent(mWhiteboard[1]);
|
|
1143
|
+
return jsonRes(res, 200, deleteWhiteboard(id));
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
return jsonRes(res, 400, { ok: false, error: err?.message ?? 'whiteboard_delete_failed' });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
861
1149
|
if (await handleConnectorApi(req, res, url)) {
|
|
862
1150
|
return;
|
|
863
1151
|
}
|
|
@@ -1032,6 +1320,30 @@ const server = createServer(async (req, res) => {
|
|
|
1032
1320
|
res.end(await upstream.text());
|
|
1033
1321
|
return;
|
|
1034
1322
|
}
|
|
1323
|
+
// 会话 insight(只读 trace 分析:动作 span / 失败聚合 / 规则建议)。
|
|
1324
|
+
// owner-only:不在公开读白名单 → decideDashboardAuth 已对只读访客 401,
|
|
1325
|
+
// 公开/联邦访客看不到 tab 也拿不到 span。代理到 owner daemon 的同名 IPC。
|
|
1326
|
+
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/insight$/))) {
|
|
1327
|
+
const sid = decodeURIComponent(m[1]);
|
|
1328
|
+
const owner = aggregator.ownerOf(sid);
|
|
1329
|
+
if (!owner)
|
|
1330
|
+
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
1331
|
+
const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/insight${url.search ?? ''}`, { method: 'GET' });
|
|
1332
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1333
|
+
res.end(await upstream.text());
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/insight\/turn\/([^/]+)$/))) {
|
|
1337
|
+
const sid = decodeURIComponent(m[1]);
|
|
1338
|
+
const turnIndex = decodeURIComponent(m[2]);
|
|
1339
|
+
const owner = aggregator.ownerOf(sid);
|
|
1340
|
+
if (!owner)
|
|
1341
|
+
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
1342
|
+
const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/insight/turn/${turnIndex}${url.search ?? ''}`, { method: 'GET' });
|
|
1343
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1344
|
+
res.end(await upstream.text());
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1035
1347
|
// Writable web-terminal link (token-bearing). Not in any public allow-list,
|
|
1036
1348
|
// so decideDashboardAuth has already 401'd unauthenticated callers before we
|
|
1037
1349
|
// get here — the token only reaches authenticated dashboard sessions.
|
|
@@ -1202,6 +1514,135 @@ const server = createServer(async (req, res) => {
|
|
|
1202
1514
|
return;
|
|
1203
1515
|
}
|
|
1204
1516
|
}
|
|
1517
|
+
// ─── Profiles (aggregate/proxy to daemon) ─────────────────────────────
|
|
1518
|
+
if (req.method === 'GET' && url.pathname === '/api/role-profiles') {
|
|
1519
|
+
const merged = new Map();
|
|
1520
|
+
await Promise.all(registry.list().map(async (d) => {
|
|
1521
|
+
try {
|
|
1522
|
+
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/role-profiles`);
|
|
1523
|
+
if (!r.ok)
|
|
1524
|
+
return;
|
|
1525
|
+
const j = await r.json();
|
|
1526
|
+
for (const p of j.profiles ?? []) {
|
|
1527
|
+
if (typeof p.profileId !== 'string')
|
|
1528
|
+
continue;
|
|
1529
|
+
const cur = merged.get(p.profileId) ?? { profileId: p.profileId, entryCount: 0, updatedAt: null, botEntries: [] };
|
|
1530
|
+
cur.entryCount = Math.max(cur.entryCount, typeof p.entryCount === 'number' ? p.entryCount : 0);
|
|
1531
|
+
if (typeof p.updatedAt === 'number')
|
|
1532
|
+
cur.updatedAt = cur.updatedAt === null ? p.updatedAt : Math.max(cur.updatedAt, p.updatedAt);
|
|
1533
|
+
const larkAppId = j.larkAppId ?? d.larkAppId;
|
|
1534
|
+
if (!cur.botEntries.some(entry => entry.larkAppId === larkAppId)) {
|
|
1535
|
+
cur.botEntries.push({ larkAppId, hasEntry: p.hasCurrentBotEntry === true });
|
|
1536
|
+
}
|
|
1537
|
+
merged.set(p.profileId, cur);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
catch { /* skip offline/bad daemon */ }
|
|
1541
|
+
}));
|
|
1542
|
+
return jsonRes(res, 200, {
|
|
1543
|
+
profiles: [...merged.values()]
|
|
1544
|
+
.map(p => ({
|
|
1545
|
+
...p,
|
|
1546
|
+
entryCount: Math.max(p.entryCount, p.botEntries.filter(entry => entry.hasEntry).length),
|
|
1547
|
+
}))
|
|
1548
|
+
.sort((a, b) => a.profileId.localeCompare(b.profileId)),
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
let mRoleProfileApply;
|
|
1552
|
+
if (req.method === 'POST' && (mRoleProfileApply = url.pathname.match(/^\/api\/role-profiles\/([^/]+)\/apply$/))) {
|
|
1553
|
+
const profileId = decodeURIComponent(mRoleProfileApply[1]);
|
|
1554
|
+
if (!isValidRoleProfileId(profileId))
|
|
1555
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1556
|
+
let raw = '{}';
|
|
1557
|
+
let parsed;
|
|
1558
|
+
try {
|
|
1559
|
+
const chunks = [];
|
|
1560
|
+
for await (const c of req)
|
|
1561
|
+
chunks.push(c);
|
|
1562
|
+
raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1563
|
+
parsed = JSON.parse(raw);
|
|
1564
|
+
}
|
|
1565
|
+
catch {
|
|
1566
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1567
|
+
}
|
|
1568
|
+
const larkAppId = typeof parsed.larkAppId === 'string' ? parsed.larkAppId : '';
|
|
1569
|
+
if (!larkAppId)
|
|
1570
|
+
return jsonRes(res, 400, { ok: false, error: 'larkAppId_required' });
|
|
1571
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/apply`, {
|
|
1572
|
+
method: 'POST',
|
|
1573
|
+
headers: { 'content-type': 'application/json' },
|
|
1574
|
+
body: raw,
|
|
1575
|
+
});
|
|
1576
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1577
|
+
res.end(await upstream.text());
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
let mRoleProfileEntry;
|
|
1581
|
+
if ((mRoleProfileEntry = url.pathname.match(/^\/api\/role-profiles\/([^/]+)\/([^/]+)$/))) {
|
|
1582
|
+
const profileId = decodeURIComponent(mRoleProfileEntry[1]);
|
|
1583
|
+
const larkAppId = decodeURIComponent(mRoleProfileEntry[2]);
|
|
1584
|
+
if (!isValidRoleProfileId(profileId))
|
|
1585
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1586
|
+
if (req.method === 'GET') {
|
|
1587
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'GET' });
|
|
1588
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1589
|
+
res.end(await upstream.text());
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (req.method === 'PUT') {
|
|
1593
|
+
const chunks = [];
|
|
1594
|
+
for await (const c of req)
|
|
1595
|
+
chunks.push(c);
|
|
1596
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1597
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, {
|
|
1598
|
+
method: 'PUT',
|
|
1599
|
+
headers: { 'content-type': 'application/json' },
|
|
1600
|
+
body: raw,
|
|
1601
|
+
});
|
|
1602
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1603
|
+
res.end(await upstream.text());
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (req.method === 'DELETE') {
|
|
1607
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'DELETE' });
|
|
1608
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1609
|
+
res.end(await upstream.text());
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
let mRoleProfile;
|
|
1614
|
+
if (req.method === 'GET' && (mRoleProfile = url.pathname.match(/^\/api\/role-profiles\/([^/]+)$/))) {
|
|
1615
|
+
const profileId = decodeURIComponent(mRoleProfile[1]);
|
|
1616
|
+
if (!isValidRoleProfileId(profileId))
|
|
1617
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1618
|
+
const byBot = new Map();
|
|
1619
|
+
await Promise.all(registry.list().map(async (d) => {
|
|
1620
|
+
try {
|
|
1621
|
+
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/role-profiles/${encodeURIComponent(profileId)}`);
|
|
1622
|
+
if (!r.ok)
|
|
1623
|
+
return;
|
|
1624
|
+
const j = await r.json();
|
|
1625
|
+
for (const entry of j.entries ?? []) {
|
|
1626
|
+
if (typeof entry.larkAppId !== 'string' || typeof entry.content !== 'string')
|
|
1627
|
+
continue;
|
|
1628
|
+
const updatedAt = typeof entry.updatedAt === 'number' ? entry.updatedAt : null;
|
|
1629
|
+
const current = byBot.get(entry.larkAppId);
|
|
1630
|
+
if (current && (current.updatedAt ?? 0) > (updatedAt ?? 0))
|
|
1631
|
+
continue;
|
|
1632
|
+
byBot.set(entry.larkAppId, {
|
|
1633
|
+
profileId,
|
|
1634
|
+
larkAppId: entry.larkAppId,
|
|
1635
|
+
content: entry.content,
|
|
1636
|
+
byteLength: typeof entry.byteLength === 'number' ? entry.byteLength : Buffer.byteLength(entry.content, 'utf-8'),
|
|
1637
|
+
updatedAt,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
catch { /* skip */ }
|
|
1642
|
+
}));
|
|
1643
|
+
const entries = [...byBot.values()].sort((a, b) => a.larkAppId.localeCompare(b.larkAppId));
|
|
1644
|
+
return jsonRes(res, 200, { profileId, entries });
|
|
1645
|
+
}
|
|
1205
1646
|
let m2;
|
|
1206
1647
|
if (req.method === 'POST' && (m2 = url.pathname.match(/^\/api\/groups\/([^/]+)\/add-bots$/))) {
|
|
1207
1648
|
const chatId = decodeURIComponent(m2[1]);
|
|
@@ -1455,6 +1896,24 @@ const server = createServer(async (req, res) => {
|
|
|
1455
1896
|
res.end(await upstream.text());
|
|
1456
1897
|
return;
|
|
1457
1898
|
}
|
|
1899
|
+
// PUT /api/bots/:appId/env — proxy to that bot's daemon. Body
|
|
1900
|
+
// `{ env: string }` (raw JSON text; '' = clear).
|
|
1901
|
+
let mBotEnv;
|
|
1902
|
+
if (req.method === 'PUT' && (mBotEnv = url.pathname.match(/^\/api\/bots\/([^/]+)\/env$/))) {
|
|
1903
|
+
const appId = decodeURIComponent(mBotEnv[1]);
|
|
1904
|
+
const chunks = [];
|
|
1905
|
+
for await (const c of req)
|
|
1906
|
+
chunks.push(c);
|
|
1907
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1908
|
+
const upstream = await proxyToDaemon(appId, `/api/bot-env`, {
|
|
1909
|
+
method: 'PUT',
|
|
1910
|
+
headers: { 'content-type': 'application/json' },
|
|
1911
|
+
body: raw,
|
|
1912
|
+
});
|
|
1913
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1914
|
+
res.end(await upstream.text());
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1458
1917
|
// PUT /api/bots/:appId/sandbox — proxy to that bot's daemon. Body `{ enabled: boolean }`.
|
|
1459
1918
|
let mBotSandbox;
|
|
1460
1919
|
if (req.method === 'PUT' && (mBotSandbox = url.pathname.match(/^\/api\/bots\/([^/]+)\/sandbox$/))) {
|
|
@@ -1570,6 +2029,12 @@ const server = createServer(async (req, res) => {
|
|
|
1570
2029
|
if (selectedIds.length === 0) {
|
|
1571
2030
|
return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
|
|
1572
2031
|
}
|
|
2032
|
+
const roleProfileId = typeof parsed.roleProfileId === 'string' && parsed.roleProfileId.trim()
|
|
2033
|
+
? parsed.roleProfileId.trim()
|
|
2034
|
+
: null;
|
|
2035
|
+
if (roleProfileId && !isValidRoleProfileId(roleProfileId)) {
|
|
2036
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
2037
|
+
}
|
|
1573
2038
|
const explicit = Array.isArray(parsed.userOpenIds)
|
|
1574
2039
|
? parsed.userOpenIds.filter((x) => typeof x === 'string')
|
|
1575
2040
|
: [];
|
|
@@ -1600,6 +2065,7 @@ const server = createServer(async (req, res) => {
|
|
|
1600
2065
|
bindWorkingDir: typeof parsed.bindWorkingDir === 'string' && parsed.bindWorkingDir.trim()
|
|
1601
2066
|
? parsed.bindWorkingDir.trim()
|
|
1602
2067
|
: undefined,
|
|
2068
|
+
roleProfileId: roleProfileId ?? undefined,
|
|
1603
2069
|
};
|
|
1604
2070
|
const upstream = await fetch(`http://127.0.0.1:${creator.ipcPort}/api/groups/create`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(forwardBody) });
|
|
1605
2071
|
const upstreamText = await upstream.text();
|
|
@@ -1609,6 +2075,8 @@ const server = createServer(async (req, res) => {
|
|
|
1609
2075
|
}
|
|
1610
2076
|
catch { /* leave null */ }
|
|
1611
2077
|
if (upstreamJson && typeof upstreamJson === 'object') {
|
|
2078
|
+
if (roleProfileId)
|
|
2079
|
+
upstreamJson.roleProfileId = roleProfileId;
|
|
1612
2080
|
// If Lark rejected the invite (open_id wrong scope, banned user, etc.)
|
|
1613
2081
|
// null out autoInvitedOpenId so the frontend doesn't falsely claim
|
|
1614
2082
|
// success — the user actually isn't a member of the new chat.
|