botmux 2.88.0 → 2.89.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/dist/bot-registry.d.ts +2 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +4 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
- package/dist/cli/dashboard-endpoint.js +9 -3
- package/dist/cli/dashboard-endpoint.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +9 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-command/groups.d.ts +20 -0
- package/dist/core/dashboard-command/groups.d.ts.map +1 -0
- package/dist/core/dashboard-command/groups.js +52 -0
- package/dist/core/dashboard-command/groups.js.map +1 -0
- package/dist/core/dashboard-command/index.d.ts +30 -0
- package/dist/core/dashboard-command/index.d.ts.map +1 -0
- package/dist/core/dashboard-command/index.js +81 -0
- package/dist/core/dashboard-command/index.js.map +1 -0
- package/dist/core/dashboard-command/overview.d.ts +29 -0
- package/dist/core/dashboard-command/overview.d.ts.map +1 -0
- package/dist/core/dashboard-command/overview.js +61 -0
- package/dist/core/dashboard-command/overview.js.map +1 -0
- package/dist/core/dashboard-command/owner-gate.d.ts +41 -0
- package/dist/core/dashboard-command/owner-gate.d.ts.map +1 -0
- package/dist/core/dashboard-command/owner-gate.js +40 -0
- package/dist/core/dashboard-command/owner-gate.js.map +1 -0
- package/dist/core/dashboard-command/schedules.d.ts +20 -0
- package/dist/core/dashboard-command/schedules.d.ts.map +1 -0
- package/dist/core/dashboard-command/schedules.js +47 -0
- package/dist/core/dashboard-command/schedules.js.map +1 -0
- package/dist/core/dashboard-command/sessions.d.ts +22 -0
- package/dist/core/dashboard-command/sessions.d.ts.map +1 -0
- package/dist/core/dashboard-command/sessions.js +43 -0
- package/dist/core/dashboard-command/sessions.js.map +1 -0
- package/dist/core/dashboard-command/settings.d.ts +21 -0
- package/dist/core/dashboard-command/settings.d.ts.map +1 -0
- package/dist/core/dashboard-command/settings.js +55 -0
- package/dist/core/dashboard-command/settings.js.map +1 -0
- package/dist/core/dashboard-command/stub.d.ts +16 -0
- package/dist/core/dashboard-command/stub.d.ts.map +1 -0
- package/dist/core/dashboard-command/stub.js +28 -0
- package/dist/core/dashboard-command/stub.js.map +1 -0
- package/dist/core/dashboard-command/workflows.d.ts +20 -0
- package/dist/core/dashboard-command/workflows.d.ts.map +1 -0
- package/dist/core/dashboard-command/workflows.js +45 -0
- package/dist/core/dashboard-command/workflows.js.map +1 -0
- 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/session-create.d.ts +0 -1
- package/dist/core/session-create.d.ts.map +1 -1
- package/dist/core/session-create.js +0 -3
- package/dist/core/session-create.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +3 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/daemon-internal-client-wrapper.d.ts +41 -0
- package/dist/daemon-internal-client-wrapper.d.ts.map +1 -0
- package/dist/daemon-internal-client-wrapper.js +65 -0
- package/dist/daemon-internal-client-wrapper.js.map +1 -0
- package/dist/dashboard/aggregator.d.ts +12 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +16 -0
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/auth.d.ts +7 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +21 -0
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/card-model-types.d.ts +50 -0
- package/dist/dashboard/card-model-types.d.ts.map +1 -0
- package/dist/dashboard/card-model-types.js +9 -0
- package/dist/dashboard/card-model-types.js.map +1 -0
- package/dist/dashboard/daemon-internal-api.d.ts +90 -0
- package/dist/dashboard/daemon-internal-api.d.ts.map +1 -0
- package/dist/dashboard/daemon-internal-api.js +534 -0
- package/dist/dashboard/daemon-internal-api.js.map +1 -0
- package/dist/dashboard/daemon-internal-auth.d.ts +124 -0
- package/dist/dashboard/daemon-internal-auth.d.ts.map +1 -0
- package/dist/dashboard/daemon-internal-auth.js +188 -0
- package/dist/dashboard/daemon-internal-auth.js.map +1 -0
- package/dist/dashboard/daemon-internal-client.d.ts +67 -0
- package/dist/dashboard/daemon-internal-client.d.ts.map +1 -0
- package/dist/dashboard/daemon-internal-client.js +157 -0
- package/dist/dashboard/daemon-internal-client.js.map +1 -0
- package/dist/dashboard/dashboard-admins.d.ts +8 -0
- package/dist/dashboard/dashboard-admins.d.ts.map +1 -0
- package/dist/dashboard/dashboard-admins.js +15 -0
- package/dist/dashboard/dashboard-admins.js.map +1 -0
- package/dist/dashboard/federation-spoke-api.d.ts +3 -0
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +11 -8
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/groups-action-helpers.d.ts +69 -0
- package/dist/dashboard/groups-action-helpers.d.ts.map +1 -0
- package/dist/dashboard/groups-action-helpers.js +141 -0
- package/dist/dashboard/groups-action-helpers.js.map +1 -0
- package/dist/dashboard/groups-card-model.d.ts +120 -0
- package/dist/dashboard/groups-card-model.d.ts.map +1 -0
- package/dist/dashboard/groups-card-model.js +175 -0
- package/dist/dashboard/groups-card-model.js.map +1 -0
- package/dist/dashboard/schedule-card-model.d.ts +172 -0
- package/dist/dashboard/schedule-card-model.d.ts.map +1 -0
- package/dist/dashboard/schedule-card-model.js +290 -0
- package/dist/dashboard/schedule-card-model.js.map +1 -0
- package/dist/dashboard/session-card-model.d.ts +83 -0
- package/dist/dashboard/session-card-model.d.ts.map +1 -0
- package/dist/dashboard/session-card-model.js +196 -0
- package/dist/dashboard/session-card-model.js.map +1 -0
- package/dist/dashboard/settings-card-model.d.ts +73 -0
- package/dist/dashboard/settings-card-model.d.ts.map +1 -0
- package/dist/dashboard/settings-card-model.js +94 -0
- package/dist/dashboard/settings-card-model.js.map +1 -0
- package/dist/dashboard/settings-owner-resolver.d.ts +32 -0
- package/dist/dashboard/settings-owner-resolver.d.ts.map +1 -0
- package/dist/dashboard/settings-owner-resolver.js +43 -0
- package/dist/dashboard/settings-owner-resolver.js.map +1 -0
- package/dist/dashboard/settings-write-applier.d.ts +84 -0
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -0
- package/dist/dashboard/settings-write-applier.js +119 -0
- package/dist/dashboard/settings-write-applier.js.map +1 -0
- package/dist/dashboard/workflow-api.d.ts.map +1 -1
- package/dist/dashboard/workflow-api.js +38 -119
- package/dist/dashboard/workflow-api.js.map +1 -1
- package/dist/dashboard/workflow-card-model.d.ts +127 -0
- package/dist/dashboard/workflow-card-model.d.ts.map +1 -0
- package/dist/dashboard/workflow-card-model.js +186 -0
- package/dist/dashboard/workflow-card-model.js.map +1 -0
- package/dist/dashboard/workflows-action-helpers.d.ts +105 -0
- package/dist/dashboard/workflows-action-helpers.d.ts.map +1 -0
- package/dist/dashboard/workflows-action-helpers.js +196 -0
- package/dist/dashboard/workflows-action-helpers.js.map +1 -0
- package/dist/dashboard.js +196 -271
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +266 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +266 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +2 -0
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +2 -1
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +54 -2
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +127 -1
- 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 +22 -0
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/groups-card.d.ts +90 -0
- package/dist/im/lark/groups-card.d.ts.map +1 -0
- package/dist/im/lark/groups-card.js +832 -0
- package/dist/im/lark/groups-card.js.map +1 -0
- package/dist/im/lark/overview-card.d.ts +101 -0
- package/dist/im/lark/overview-card.d.ts.map +1 -0
- package/dist/im/lark/overview-card.js +475 -0
- package/dist/im/lark/overview-card.js.map +1 -0
- package/dist/im/lark/schedules-card.d.ts +96 -0
- package/dist/im/lark/schedules-card.d.ts.map +1 -0
- package/dist/im/lark/schedules-card.js +892 -0
- package/dist/im/lark/schedules-card.js.map +1 -0
- package/dist/im/lark/sessions-card.d.ts +117 -0
- package/dist/im/lark/sessions-card.d.ts.map +1 -0
- package/dist/im/lark/sessions-card.js +870 -0
- package/dist/im/lark/sessions-card.js.map +1 -0
- package/dist/im/lark/settings-card.d.ts +125 -0
- package/dist/im/lark/settings-card.d.ts.map +1 -0
- package/dist/im/lark/settings-card.js +433 -0
- package/dist/im/lark/settings-card.js.map +1 -0
- package/dist/im/lark/workflows-card.d.ts +102 -0
- package/dist/im/lark/workflows-card.d.ts.map +1 -0
- package/dist/im/lark/workflows-card.js +737 -0
- package/dist/im/lark/workflows-card.js.map +1 -0
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/dashboard.ts
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { createServer as createTcpServer } from 'node:net';
|
|
4
|
-
import { readFileSync, existsSync,
|
|
4
|
+
import { readFileSync, existsSync, mkdirSync, statSync, createReadStream, } from 'node:fs';
|
|
5
5
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
6
6
|
import { join, dirname, extname, resolve, relative, isAbsolute } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
@@ -10,7 +10,7 @@ import { randomBytes, createHmac } from 'node:crypto';
|
|
|
10
10
|
import { logger } from './utils/logger.js';
|
|
11
11
|
import { config } from './config.js';
|
|
12
12
|
import { listenWithProbe } from './utils/listen-with-probe.js';
|
|
13
|
-
import { generateToken, parseCookie, buildSetCookie, verifyHmac, cliAuthBind, decideDashboardAuth, loadPersistedToken, persistToken, } from './dashboard/auth.js';
|
|
13
|
+
import { generateToken, parseCookie, buildSetCookie, verifyHmac, cliAuthBind, decideDashboardAuth, loadPersistedToken, persistToken, loadDashboardSecret, loadOrCreateDashboardSecret, } from './dashboard/auth.js';
|
|
14
14
|
import { DaemonRegistry } from './dashboard/registry.js';
|
|
15
15
|
import { Aggregator, subscribeDaemon } from './dashboard/aggregator.js';
|
|
16
16
|
import { pickCreatorForGroup } from './dashboard/operator-selector.js';
|
|
@@ -26,9 +26,8 @@ import { getRunsDir } from './workflows/runs-dir.js';
|
|
|
26
26
|
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
|
-
import {
|
|
29
|
+
import { mergeGlobalConfig, readGlobalConfig } from './global-config.js';
|
|
30
30
|
import { deleteWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled } from './services/whiteboard-store.js';
|
|
31
|
-
import { isLocale } from './i18n/types.js';
|
|
32
31
|
import { isLocalDevInstall, botmuxVersion } from './utils/install-info.js';
|
|
33
32
|
import { checkNode, detectBotmuxInstalls, resolveCurrentVersion } from './utils/install-diagnostics.js';
|
|
34
33
|
import { fetchLatestVersion, fetchReleasesSince, isNewerVersion } from './core/update-check.js';
|
|
@@ -37,6 +36,11 @@ import { spawnDetachedRestart, npmGlobalUpdateLockTarget } from './core/maintena
|
|
|
37
36
|
import { writeRestartIntent } from './services/restart-intent-store.js';
|
|
38
37
|
import { withFileLock } from './utils/file-lock.js';
|
|
39
38
|
import { spawn } from 'node:child_process';
|
|
39
|
+
import { applySettingsWrite, defaultSettingsWriteApplierDeps, } from './dashboard/settings-write-applier.js';
|
|
40
|
+
import { addBotsToGroup, bindOncall, disbandGroup, leaveGroup, unbindOncall, } from './dashboard/groups-action-helpers.js';
|
|
41
|
+
import { defaultWorkflowsActionDeps } from './dashboard/workflows-action-helpers.js';
|
|
42
|
+
import { listRuns as listRunsImpl, readRunSnapshot as readRunSnapshotImpl, scrubSnapshotForUnauthed as scrubSnapshotImpl, isValidRunId as isValidRunIdImpl, TERMINAL_RUN_STATUSES as TERMINAL_RUN_STATUSES_IMPL, } from './workflows/ops-projection.js';
|
|
43
|
+
import { createDaemonInternalApi } from './dashboard/daemon-internal-api.js';
|
|
40
44
|
import { listTeamReports, readTeamBoard, setTeamBoardEntry } from './services/team-board-store.js';
|
|
41
45
|
import { hd2dAssetPath, hd2dStatus, startHd2dDownload } from './dashboard/hd2d-assets.js';
|
|
42
46
|
import { installLocalSkillLinks, readSkillRegistry, removeInstalledSkill, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
|
|
@@ -63,14 +67,26 @@ const REGISTRY_DIR = join(homedir(), '.botmux', 'data', 'dashboard-daemons');
|
|
|
63
67
|
// the `botmux dashboard` CLI can reach /__cli/rotate without guessing.
|
|
64
68
|
const PORT_PATH = join(homedir(), '.botmux', '.dashboard-port');
|
|
65
69
|
function loadOrCreateSecret() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let existing;
|
|
71
|
+
try {
|
|
72
|
+
existing = loadDashboardSecret(SECRET_PATH);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
logger.error(`[dashboard] Failed to read dashboard secret at ${SECRET_PATH}: ${e.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
const existed = existsSync(SECRET_PATH);
|
|
81
|
+
try {
|
|
82
|
+
const secret = loadOrCreateDashboardSecret(SECRET_PATH);
|
|
83
|
+
logger.info(`[dashboard] ${existed ? 'Regenerated empty' : 'Generated'} dashboard secret at ${SECRET_PATH}`);
|
|
84
|
+
return secret;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
logger.error(`[dashboard] Failed to create dashboard secret at ${SECRET_PATH}: ${e.message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
74
90
|
}
|
|
75
91
|
// The active dashboard token is persisted to disk so a previously-issued
|
|
76
92
|
// dashboard URL survives `botmux restart`; only `botmux dashboard` (the
|
|
@@ -131,6 +147,65 @@ function resolveDashboardSettings() {
|
|
|
131
147
|
whiteboard: { enabled: global.whiteboard?.enabled === true },
|
|
132
148
|
};
|
|
133
149
|
}
|
|
150
|
+
// Single shared deps object for `applySettingsWrite` — both the browser
|
|
151
|
+
// `PUT /api/settings` route and (PR2 C6) the HMAC-gated `PUT /__daemon/settings-write`
|
|
152
|
+
// route call through this so error codes / merge semantics stay identical.
|
|
153
|
+
async function reloadLocaleOnAllDaemons() {
|
|
154
|
+
await Promise.all(registry.list().map(d => fetch(`http://127.0.0.1:${d.ipcPort}/api/locale/reload`, { method: 'POST' }).catch(() => undefined)));
|
|
155
|
+
}
|
|
156
|
+
const settingsWriteApplierDeps = defaultSettingsWriteApplierDeps(resolveDashboardSettings, reloadLocaleOnAllDaemons);
|
|
157
|
+
/** Helper to render a {status, body} HandlerResult through `res`. */
|
|
158
|
+
function writeHandlerResult(res, result) {
|
|
159
|
+
const headers = { 'content-type': 'application/json', ...(result.headers ?? {}) };
|
|
160
|
+
res.writeHead(result.status, headers);
|
|
161
|
+
res.end(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
|
|
162
|
+
}
|
|
163
|
+
// Shared deps for groups-action-helpers — both the browser
|
|
164
|
+
// `/api/groups/*` routes and (PR2 C6) the HMAC-gated `/__daemon/groups/*`
|
|
165
|
+
// routes use these helpers so response shapes / cascade-close semantics
|
|
166
|
+
// stay identical.
|
|
167
|
+
const groupsActionDeps = {
|
|
168
|
+
registryList: () => registry.list(),
|
|
169
|
+
registryGetByAppId: (id) => registry.getByAppId(id),
|
|
170
|
+
proxyToDaemon,
|
|
171
|
+
closeSessionsMatching,
|
|
172
|
+
};
|
|
173
|
+
// ─── PR2 C8: Route B internal API (`/__daemon/*`) ───────────────────────────
|
|
174
|
+
// HMAC + loopback + ts ±60s + nonce TTL, signed-request envelope = full
|
|
175
|
+
// (ts, nonce, method, pathWithQuery, sha256(body)). Reuses `.dashboard-secret`
|
|
176
|
+
// for the HMAC key — the same secret `/__cli/rotate` already uses — but the
|
|
177
|
+
// signing material is wider so a `/__cli/rotate` signature cannot be replayed
|
|
178
|
+
// here and vice versa (different protocols, same secret, no cross-replay).
|
|
179
|
+
//
|
|
180
|
+
// SECRET fail-closed: `loadOrCreateSecret()` returns a 32-byte base64url
|
|
181
|
+
// string and never empty; we still guard below at server-startup time.
|
|
182
|
+
if (!SECRET || SECRET.length === 0) {
|
|
183
|
+
logger.error('[dashboard] SECRET is empty — refusing to mount /__daemon/* dispatcher');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const daemonInternalApi = createDaemonInternalApi({
|
|
187
|
+
secret: SECRET,
|
|
188
|
+
getSessions: () => aggregator.getSessions(),
|
|
189
|
+
getSchedules: () => aggregator.getSchedules(),
|
|
190
|
+
resolveDashboardSettings,
|
|
191
|
+
buildGroupsMatrix,
|
|
192
|
+
settingsApplierDeps: settingsWriteApplierDeps,
|
|
193
|
+
groupsActionDeps,
|
|
194
|
+
workflowsActionDeps: defaultWorkflowsActionDeps({
|
|
195
|
+
runsDir: getRunsDir(),
|
|
196
|
+
proxyToDaemon,
|
|
197
|
+
listRuns: listRunsImpl,
|
|
198
|
+
readRunSnapshot: readRunSnapshotImpl,
|
|
199
|
+
scrubSnapshotForUnauthed: scrubSnapshotImpl,
|
|
200
|
+
TERMINAL_RUN_STATUSES: TERMINAL_RUN_STATUSES_IMPL,
|
|
201
|
+
isValidRunId: isValidRunIdImpl,
|
|
202
|
+
}),
|
|
203
|
+
proxyToDaemon,
|
|
204
|
+
ownerOf: (sid) => aggregator.ownerOf(sid),
|
|
205
|
+
scheduleOwnerOf: (id) => aggregator.scheduleOwnerOf(id),
|
|
206
|
+
scheduleExists: (id) => aggregator.scheduleExists(id),
|
|
207
|
+
sessionExists: (sessionId) => aggregator.sessionExists(sessionId),
|
|
208
|
+
});
|
|
134
209
|
async function readJsonBody(req) {
|
|
135
210
|
const chunks = [];
|
|
136
211
|
for await (const c of req)
|
|
@@ -468,6 +543,73 @@ async function createLifecycleGroupForWebhook(connector, args) {
|
|
|
468
543
|
}
|
|
469
544
|
return { chatId: parsed.chatId, creatorLarkAppId: parsed.creator ?? pick.creatorLarkAppId };
|
|
470
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Build the per-(chat × bot) coverage matrix shared by `GET /api/groups`
|
|
548
|
+
* (browser) and `GET /__daemon/groups-matrix` (Route B). Pure aggregation,
|
|
549
|
+
* always returns the raw (unscrubbed) view — the browser route applies its
|
|
550
|
+
* `redactGroupsForPublic` scrub on top when the caller is unauthed.
|
|
551
|
+
*/
|
|
552
|
+
async function buildGroupsMatrix() {
|
|
553
|
+
const out = new Map();
|
|
554
|
+
const cliIds = configuredCliIds();
|
|
555
|
+
const onlineBots = [...registry.list()]
|
|
556
|
+
.map(b => withConfiguredCliId(b, cliIds))
|
|
557
|
+
.sort((a, b) => a.botIndex - b.botIndex);
|
|
558
|
+
await Promise.all(onlineBots.map(async (d) => {
|
|
559
|
+
try {
|
|
560
|
+
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/groups`);
|
|
561
|
+
if (!r.ok)
|
|
562
|
+
return;
|
|
563
|
+
const j = await r.json();
|
|
564
|
+
for (const c of j.chats ?? []) {
|
|
565
|
+
const { oncallChat, firstSeenAt, hasRole, observedBotNames, ...chatBase } = c;
|
|
566
|
+
const cur = out.get(c.chatId) ?? {
|
|
567
|
+
...chatBase,
|
|
568
|
+
memberBots: [],
|
|
569
|
+
_firstSeenAt: null,
|
|
570
|
+
observedBotNames: [],
|
|
571
|
+
};
|
|
572
|
+
if (Array.isArray(observedBotNames) && observedBotNames.length > 0) {
|
|
573
|
+
cur.observedBotNames = [...new Set([...(cur.observedBotNames ?? []), ...observedBotNames])];
|
|
574
|
+
}
|
|
575
|
+
cur.memberBots.push({
|
|
576
|
+
larkAppId: d.larkAppId,
|
|
577
|
+
botName: d.botName,
|
|
578
|
+
cliId: d.cliId,
|
|
579
|
+
inChat: true,
|
|
580
|
+
oncallChat: oncallChat ?? null,
|
|
581
|
+
hasRole: hasRole ?? false,
|
|
582
|
+
});
|
|
583
|
+
if (typeof firstSeenAt === 'number') {
|
|
584
|
+
cur._firstSeenAt = cur._firstSeenAt === null
|
|
585
|
+
? firstSeenAt
|
|
586
|
+
: Math.min(cur._firstSeenAt, firstSeenAt);
|
|
587
|
+
}
|
|
588
|
+
out.set(c.chatId, cur);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch { /* skip offline daemons silently — best-effort */ }
|
|
592
|
+
}));
|
|
593
|
+
for (const c of out.values()) {
|
|
594
|
+
const present = new Set(c.memberBots.map((mb) => mb.larkAppId));
|
|
595
|
+
for (const b of onlineBots) {
|
|
596
|
+
if (!present.has(b.larkAppId)) {
|
|
597
|
+
c.memberBots.push({ larkAppId: b.larkAppId, botName: b.botName, cliId: b.cliId, inChat: false, oncallChat: null, hasRole: false });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const chats = [...out.values()]
|
|
602
|
+
.sort((a, b) => {
|
|
603
|
+
const ta = a._firstSeenAt ?? 0;
|
|
604
|
+
const tb = b._firstSeenAt ?? 0;
|
|
605
|
+
if (tb !== ta)
|
|
606
|
+
return tb - ta;
|
|
607
|
+
return (a.name ?? a.chatId).localeCompare(b.name ?? b.chatId);
|
|
608
|
+
})
|
|
609
|
+
.map(({ _firstSeenAt, ...rest }) => rest);
|
|
610
|
+
const bots = onlineBots.map(botSummaryPayload);
|
|
611
|
+
return { chats, bots };
|
|
612
|
+
}
|
|
471
613
|
/**
|
|
472
614
|
* Close every active session matching `pred` by routing to its owning daemon.
|
|
473
615
|
* Used after disband (close all sessions in chat) and leave (close only the
|
|
@@ -680,6 +822,16 @@ const server = createServer(async (req, res) => {
|
|
|
680
822
|
if (await handleFederationApi(req, res, url, { createTeamGroup, liveBots })) {
|
|
681
823
|
return;
|
|
682
824
|
}
|
|
825
|
+
// Route B: daemon internal API (`/__daemon/*`) — HMAC + loopback,
|
|
826
|
+
// mounted BEFORE the browser cookie/token gate because this protocol is
|
|
827
|
+
// entirely self-contained (the daemon caller has the shared secret and
|
|
828
|
+
// the signing-envelope already binds method/path/body to the timestamp).
|
|
829
|
+
// Letting the auth gate touch these paths would be wrong: there is no
|
|
830
|
+
// cookie or token to set/check; the gate would either 401 the daemon
|
|
831
|
+
// (false negative) or grant cross-protocol access (false positive).
|
|
832
|
+
if (await daemonInternalApi.handle(req, res, url)) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
683
835
|
// CLI rotate (HMAC + loopback only) — for `botmux dashboard`. Mints a fresh
|
|
684
836
|
// token, invalidating any previously-issued link.
|
|
685
837
|
if (req.method === 'POST' && url.pathname === '/__cli/rotate') {
|
|
@@ -893,78 +1045,10 @@ const server = createServer(async (req, res) => {
|
|
|
893
1045
|
catch {
|
|
894
1046
|
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
895
1047
|
}
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return jsonRes(res, 400, { ok: false, error: 'invalid_publicReadOnly' });
|
|
901
|
-
patch.publicReadOnly = body.publicReadOnly;
|
|
902
|
-
}
|
|
903
|
-
if ('openTerminalInFeishu' in body) {
|
|
904
|
-
if (typeof body.openTerminalInFeishu !== 'boolean')
|
|
905
|
-
return jsonRes(res, 400, { ok: false, error: 'invalid_openTerminalInFeishu' });
|
|
906
|
-
patch.openTerminalInFeishu = body.openTerminalInFeishu;
|
|
907
|
-
}
|
|
908
|
-
let touched = false;
|
|
909
|
-
if (Object.keys(patch).length > 0) {
|
|
910
|
-
mergeDashboardConfig(patch);
|
|
911
|
-
touched = true;
|
|
912
|
-
}
|
|
913
|
-
if ('repoPickerMode' in body) {
|
|
914
|
-
const v = body.repoPickerMode;
|
|
915
|
-
if (v !== 'all' && v !== 'repos')
|
|
916
|
-
return jsonRes(res, 400, { ok: false, error: 'invalid_repoPickerMode' });
|
|
917
|
-
mergeGlobalConfig({ repoPickerMode: v });
|
|
918
|
-
touched = true;
|
|
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
|
-
}
|
|
930
|
-
if ('maintenance' in body) {
|
|
931
|
-
const r = parseMaintenancePatch(body.maintenance);
|
|
932
|
-
if (!r.ok)
|
|
933
|
-
return jsonRes(res, 400, { ok: false, error: r.error });
|
|
934
|
-
// Auto-update is npm-global only; refuse enabling it on a source checkout.
|
|
935
|
-
if (r.patch.autoUpdate?.enabled && isLocalDevInstall()) {
|
|
936
|
-
return jsonRes(res, 400, { ok: false, error: 'local_dev_no_autoupdate' });
|
|
937
|
-
}
|
|
938
|
-
// Auto-restart only applies an auto-update — it's meaningless without it.
|
|
939
|
-
// Refuse enabling auto-restart unless auto-update is (or is being) on.
|
|
940
|
-
if (r.patch.autoRestart?.enabled) {
|
|
941
|
-
const autoUpdateOn = r.patch.autoUpdate?.enabled
|
|
942
|
-
?? readGlobalConfig().maintenance?.autoUpdate?.enabled
|
|
943
|
-
?? false;
|
|
944
|
-
if (!autoUpdateOn)
|
|
945
|
-
return jsonRes(res, 400, { ok: false, error: 'autoupdate_required' });
|
|
946
|
-
}
|
|
947
|
-
mergeMaintenanceConfig(r.patch);
|
|
948
|
-
touched = true;
|
|
949
|
-
}
|
|
950
|
-
if ('lang' in body) {
|
|
951
|
-
// Global UI locale — single source of truth shared with `botmux lang`
|
|
952
|
-
// and the Feishu cards. Persist to ~/.botmux/config.json, then fan out
|
|
953
|
-
// to every online daemon over the same IPC bus the per-bot config
|
|
954
|
-
// writes use, so running cards switch language live (no restart).
|
|
955
|
-
const v = body.lang;
|
|
956
|
-
if (v === null)
|
|
957
|
-
setGlobalLocale(null);
|
|
958
|
-
else if (isLocale(v))
|
|
959
|
-
setGlobalLocale(v);
|
|
960
|
-
else
|
|
961
|
-
return jsonRes(res, 400, { ok: false, error: 'invalid_lang' });
|
|
962
|
-
await Promise.all(registry.list().map(d => fetch(`http://127.0.0.1:${d.ipcPort}/api/locale/reload`, { method: 'POST' }).catch(() => undefined)));
|
|
963
|
-
touched = true;
|
|
964
|
-
}
|
|
965
|
-
if (!touched)
|
|
966
|
-
return jsonRes(res, 400, { ok: false, error: 'empty_patch' });
|
|
967
|
-
return jsonRes(res, 200, { ok: true, settings: resolveDashboardSettings() });
|
|
1048
|
+
const result = await applySettingsWrite(parsed, settingsWriteApplierDeps);
|
|
1049
|
+
if (!result.ok)
|
|
1050
|
+
return jsonRes(res, 400, { ok: false, error: result.error });
|
|
1051
|
+
return jsonRes(res, 200, { ok: true, settings: result.settings });
|
|
968
1052
|
}
|
|
969
1053
|
// ─── Version & manual update ─────────────────────────────────────────────
|
|
970
1054
|
// `npm install -g` and a host restart are privileged: none of these paths
|
|
@@ -1457,77 +1541,14 @@ const server = createServer(async (req, res) => {
|
|
|
1457
1541
|
}
|
|
1458
1542
|
// ─── Groups (Phase B) ────────────────────────────────────────────────────
|
|
1459
1543
|
if (req.method === 'GET' && url.pathname === '/api/groups') {
|
|
1460
|
-
// Fan out
|
|
1461
|
-
//
|
|
1462
|
-
|
|
1463
|
-
//
|
|
1464
|
-
|
|
1465
|
-
const cliIds = configuredCliIds();
|
|
1466
|
-
const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, cliIds)).sort((a, b) => a.botIndex - b.botIndex);
|
|
1467
|
-
await Promise.all(onlineBots.map(async (d) => {
|
|
1468
|
-
try {
|
|
1469
|
-
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/groups`);
|
|
1470
|
-
if (!r.ok)
|
|
1471
|
-
return;
|
|
1472
|
-
const j = await r.json();
|
|
1473
|
-
for (const c of j.chats ?? []) {
|
|
1474
|
-
// Strip per-bot fields from chat-level so the merged record stays
|
|
1475
|
-
// bot-agnostic. oncallChat lives inside memberBots; firstSeenAt is
|
|
1476
|
-
// accumulated as the earliest observation across all bots.
|
|
1477
|
-
const { oncallChat, firstSeenAt, hasRole, observedBotNames, ...chatBase } = c;
|
|
1478
|
-
const cur = out.get(c.chatId) ?? { ...chatBase, memberBots: [], _firstSeenAt: null, observedBotNames: [] };
|
|
1479
|
-
// /introduce 记录按观察者(bot)分文件——跨 daemon 取并集(按名字去重)
|
|
1480
|
-
if (Array.isArray(observedBotNames) && observedBotNames.length) {
|
|
1481
|
-
cur.observedBotNames = [...new Set([...(cur.observedBotNames ?? []), ...observedBotNames])];
|
|
1482
|
-
}
|
|
1483
|
-
cur.memberBots.push({
|
|
1484
|
-
larkAppId: d.larkAppId,
|
|
1485
|
-
botName: d.botName,
|
|
1486
|
-
cliId: d.cliId,
|
|
1487
|
-
inChat: true,
|
|
1488
|
-
oncallChat: oncallChat ?? null,
|
|
1489
|
-
hasRole: hasRole ?? false,
|
|
1490
|
-
});
|
|
1491
|
-
if (typeof firstSeenAt === 'number') {
|
|
1492
|
-
cur._firstSeenAt = cur._firstSeenAt === null
|
|
1493
|
-
? firstSeenAt
|
|
1494
|
-
: Math.min(cur._firstSeenAt, firstSeenAt);
|
|
1495
|
-
}
|
|
1496
|
-
out.set(c.chatId, cur);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
catch { /* skip offline daemons silently — best-effort */ }
|
|
1500
|
-
}));
|
|
1501
|
-
// Fill in inChat:false slots for bots NOT returned for a given chat (matrix view)
|
|
1502
|
-
for (const c of out.values()) {
|
|
1503
|
-
const present = new Set(c.memberBots.map((mb) => mb.larkAppId));
|
|
1504
|
-
for (const b of onlineBots) {
|
|
1505
|
-
if (!present.has(b.larkAppId)) {
|
|
1506
|
-
c.memberBots.push({ larkAppId: b.larkAppId, botName: b.botName, cliId: b.cliId, inChat: false, oncallChat: null, hasRole: false });
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
// Sort newest-first by client-side firstSeenAt (Lark exposes no chat
|
|
1511
|
-
// create_time, so daemon stamps timestamps the first time it lists each
|
|
1512
|
-
// chat). Tie-break by name asc so chats backfilled in the same listChats
|
|
1513
|
-
// pass — typically every chat on first deploy — get a stable order.
|
|
1514
|
-
const sorted = [...out.values()]
|
|
1515
|
-
.sort((a, b) => {
|
|
1516
|
-
const ta = a._firstSeenAt ?? 0;
|
|
1517
|
-
const tb = b._firstSeenAt ?? 0;
|
|
1518
|
-
if (tb !== ta)
|
|
1519
|
-
return tb - ta;
|
|
1520
|
-
return (a.name ?? a.chatId).localeCompare(b.name ?? b.chatId);
|
|
1521
|
-
})
|
|
1522
|
-
.map(({ _firstSeenAt, ...rest }) => rest);
|
|
1523
|
-
// Public-read carve-out: oncall bindings carry workingDir (repo/customer
|
|
1524
|
-
// paths). The read-only board only needs chat/bot names; the oncall
|
|
1525
|
-
// editor that consumes oncallChat is authed-only. Strip for anon so the
|
|
1526
|
-
// bound dirs don't leak via /api/groups (mirrors the /api/schedules
|
|
1527
|
-
// prompt strip + keeps /api/bots oncall removal honest).
|
|
1544
|
+
// Fan out via the shared `buildGroupsMatrix` helper so the browser
|
|
1545
|
+
// route and the Route B `/__daemon/groups-matrix` endpoint return the
|
|
1546
|
+
// same matrix shape. Public-read carve-out: oncall bindings carry
|
|
1547
|
+
// workingDir (repo/customer paths) so we scrub when unauthed.
|
|
1548
|
+
const matrix = await buildGroupsMatrix();
|
|
1528
1549
|
return jsonRes(res, 200, {
|
|
1529
|
-
chats: authed ?
|
|
1530
|
-
bots:
|
|
1550
|
+
chats: authed ? matrix.chats : redactGroupsForPublic(matrix.chats),
|
|
1551
|
+
bots: matrix.bots,
|
|
1531
1552
|
});
|
|
1532
1553
|
}
|
|
1533
1554
|
// ─── Roles (proxy to daemon) ────────────────────────────────────────────
|
|
@@ -1697,45 +1718,15 @@ const server = createServer(async (req, res) => {
|
|
|
1697
1718
|
let m2;
|
|
1698
1719
|
if (req.method === 'POST' && (m2 = url.pathname.match(/^\/api\/groups\/([^/]+)\/add-bots$/))) {
|
|
1699
1720
|
const chatId = decodeURIComponent(m2[1]);
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1707
|
-
JSON.parse(raw); // validate is JSON
|
|
1708
|
-
}
|
|
1709
|
-
catch {
|
|
1710
|
-
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1711
|
-
}
|
|
1712
|
-
// Find a daemon whose bot is already in this chat
|
|
1713
|
-
let proxy;
|
|
1714
|
-
for (const d of registry.list()) {
|
|
1715
|
-
try {
|
|
1716
|
-
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/groups/${encodeURIComponent(chatId)}/membership`);
|
|
1717
|
-
if (!r.ok)
|
|
1718
|
-
continue;
|
|
1719
|
-
const j = await r.json();
|
|
1720
|
-
if (j.inChat) {
|
|
1721
|
-
proxy = d;
|
|
1722
|
-
break;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
catch { /* skip */ }
|
|
1726
|
-
}
|
|
1727
|
-
if (!proxy)
|
|
1728
|
-
return jsonRes(res, 200, { ok: false, error: 'no_proxy_bot' });
|
|
1729
|
-
const upstream = await fetch(`http://127.0.0.1:${proxy.ipcPort}/api/groups/${encodeURIComponent(chatId)}/add-bots`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: raw });
|
|
1730
|
-
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1731
|
-
res.end(await upstream.text());
|
|
1732
|
-
return;
|
|
1721
|
+
const chunks = [];
|
|
1722
|
+
for await (const c of req)
|
|
1723
|
+
chunks.push(c);
|
|
1724
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1725
|
+
const result = await addBotsToGroup(chatId, raw, groupsActionDeps);
|
|
1726
|
+
return writeHandlerResult(res, result);
|
|
1733
1727
|
}
|
|
1734
1728
|
// Disband a chat. Body: `{ larkAppId }` — the bot whose daemon should
|
|
1735
|
-
// perform the delete.
|
|
1736
|
-
// the chat owner (or creator with operate_as_owner scope, which botmux
|
|
1737
|
-
// doesn't request by default), so the frontend is responsible for picking
|
|
1738
|
-
// a viable bot. The route just proxies and surfaces Lark's error verbatim.
|
|
1729
|
+
// perform the delete. See `dashboard/groups-action-helpers.ts:disbandGroup`.
|
|
1739
1730
|
let mDisband;
|
|
1740
1731
|
if (req.method === 'POST' && (mDisband = url.pathname.match(/^\/api\/groups\/([^/]+)\/disband$/))) {
|
|
1741
1732
|
const chatId = decodeURIComponent(mDisband[1]);
|
|
@@ -1749,30 +1740,12 @@ const server = createServer(async (req, res) => {
|
|
|
1749
1740
|
catch {
|
|
1750
1741
|
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1751
1742
|
}
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
return jsonRes(res, 400, { ok: false, error: 'larkAppId_required' });
|
|
1755
|
-
const upstream = await proxyToDaemon(appId, `/api/groups/${encodeURIComponent(chatId)}/disband`, { method: 'POST' });
|
|
1756
|
-
const upstreamText = await upstream.text();
|
|
1757
|
-
let upstreamJson = null;
|
|
1758
|
-
try {
|
|
1759
|
-
upstreamJson = JSON.parse(upstreamText);
|
|
1760
|
-
}
|
|
1761
|
-
catch { /* tolerate */ }
|
|
1762
|
-
// On successful disband, the chat is gone for everyone — every bot's
|
|
1763
|
-
// session in this chat becomes a zombie (worker still alive, can't post).
|
|
1764
|
-
// Close them all so the UI / Sessions list don't keep them as active.
|
|
1765
|
-
let closedSessions = [];
|
|
1766
|
-
if (upstreamJson?.ok) {
|
|
1767
|
-
closedSessions = await closeSessionsMatching(s => s.chatId === chatId);
|
|
1768
|
-
}
|
|
1769
|
-
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1770
|
-
res.end(JSON.stringify({ ...(upstreamJson ?? {}), closedSessions }));
|
|
1771
|
-
return;
|
|
1743
|
+
const result = await disbandGroup(chatId, parsed, groupsActionDeps);
|
|
1744
|
+
return writeHandlerResult(res, result);
|
|
1772
1745
|
}
|
|
1773
|
-
// Make selected bots leave a chat. Body: `{ larkAppIds: string[] }`.
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1746
|
+
// Make selected bots leave a chat. Body: `{ larkAppIds: string[] }`. See
|
|
1747
|
+
// `dashboard/groups-action-helpers.ts:leaveGroup` for membership probe +
|
|
1748
|
+
// cascade-close semantics.
|
|
1776
1749
|
let mLeave;
|
|
1777
1750
|
if (req.method === 'POST' && (mLeave = url.pathname.match(/^\/api\/groups\/([^/]+)\/leave$/))) {
|
|
1778
1751
|
const chatId = decodeURIComponent(mLeave[1]);
|
|
@@ -1786,54 +1759,12 @@ const server = createServer(async (req, res) => {
|
|
|
1786
1759
|
catch {
|
|
1787
1760
|
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1788
1761
|
}
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
: [];
|
|
1792
|
-
if (ids.length === 0)
|
|
1793
|
-
return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
|
|
1794
|
-
// Re-check membership on the daemon side before issuing leave — UI cache
|
|
1795
|
-
// can be stale, and Lark's bot-self-remove returns a confusing error if
|
|
1796
|
-
// the bot isn't actually in the chat. Skipping such bots up-front keeps
|
|
1797
|
-
// the per-bot result useful (`not_in_chat`) instead of a vague API error.
|
|
1798
|
-
const result = await Promise.all(ids.map(async (appId) => {
|
|
1799
|
-
const d = registry.getByAppId(appId);
|
|
1800
|
-
if (!d)
|
|
1801
|
-
return { larkAppId: appId, ok: false, error: 'daemon_offline' };
|
|
1802
|
-
try {
|
|
1803
|
-
const memRes = await fetch(`http://127.0.0.1:${d.ipcPort}/api/groups/${encodeURIComponent(chatId)}/membership`);
|
|
1804
|
-
const memJson = await memRes.json();
|
|
1805
|
-
if (!memJson.inChat)
|
|
1806
|
-
return { larkAppId: appId, ok: false, error: 'not_in_chat' };
|
|
1807
|
-
}
|
|
1808
|
-
catch (e) {
|
|
1809
|
-
return { larkAppId: appId, ok: false, error: `membership_check_failed: ${e?.message ?? e}` };
|
|
1810
|
-
}
|
|
1811
|
-
const upstream = await proxyToDaemon(appId, `/api/groups/${encodeURIComponent(chatId)}/leave`, { method: 'POST' });
|
|
1812
|
-
const text = await upstream.text();
|
|
1813
|
-
let body = null;
|
|
1814
|
-
try {
|
|
1815
|
-
body = JSON.parse(text);
|
|
1816
|
-
}
|
|
1817
|
-
catch { /* tolerate */ }
|
|
1818
|
-
// On successful leave, the leaving bot can no longer post into the
|
|
1819
|
-
// chat — its sessions there are stranded. Close only THIS bot's
|
|
1820
|
-
// sessions for THIS chat (other bots may still be in the chat with
|
|
1821
|
-
// their own active sessions).
|
|
1822
|
-
const closedSessions = body?.ok
|
|
1823
|
-
? await closeSessionsMatching(s => s.chatId === chatId && s.larkAppId === appId)
|
|
1824
|
-
: [];
|
|
1825
|
-
return {
|
|
1826
|
-
larkAppId: appId,
|
|
1827
|
-
ok: !!body?.ok,
|
|
1828
|
-
error: body?.ok ? undefined : (body?.error ?? `http_${upstream.status}`),
|
|
1829
|
-
closedSessions,
|
|
1830
|
-
};
|
|
1831
|
-
}));
|
|
1832
|
-
return jsonRes(res, 200, { result });
|
|
1762
|
+
const result = await leaveGroup(chatId, parsed, groupsActionDeps);
|
|
1763
|
+
return writeHandlerResult(res, result);
|
|
1833
1764
|
}
|
|
1834
1765
|
// ─── Oncall bindings (per chat × bot) ────────────────────────────────────
|
|
1835
|
-
// PUT /api/groups/:chatId/oncall/:larkAppId
|
|
1836
|
-
// DELETE /api/
|
|
1766
|
+
// External: PUT/DELETE /api/groups/:chatId/oncall/:larkAppId
|
|
1767
|
+
// Internal: PUT/DELETE /api/oncall/:chatId (on the named bot's daemon).
|
|
1837
1768
|
let mOncall;
|
|
1838
1769
|
if ((mOncall = url.pathname.match(/^\/api\/groups\/([^/]+)\/oncall\/([^/]+)$/))) {
|
|
1839
1770
|
const chatId = decodeURIComponent(mOncall[1]);
|
|
@@ -1842,17 +1773,13 @@ const server = createServer(async (req, res) => {
|
|
|
1842
1773
|
const chunks = [];
|
|
1843
1774
|
for await (const c of req)
|
|
1844
1775
|
chunks.push(c);
|
|
1845
|
-
const raw = Buffer.concat(chunks).toString('utf8')
|
|
1846
|
-
const
|
|
1847
|
-
res
|
|
1848
|
-
res.end(await upstream.text());
|
|
1849
|
-
return;
|
|
1776
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
1777
|
+
const result = await bindOncall(chatId, appId, raw, groupsActionDeps);
|
|
1778
|
+
return writeHandlerResult(res, result);
|
|
1850
1779
|
}
|
|
1851
1780
|
if (req.method === 'DELETE') {
|
|
1852
|
-
const
|
|
1853
|
-
res
|
|
1854
|
-
res.end(await upstream.text());
|
|
1855
|
-
return;
|
|
1781
|
+
const result = await unbindOncall(chatId, appId, groupsActionDeps);
|
|
1782
|
+
return writeHandlerResult(res, result);
|
|
1856
1783
|
}
|
|
1857
1784
|
}
|
|
1858
1785
|
// ─── Per-bot defaults (Bot Defaults tab) ─────────────────────────────────
|
|
@@ -2166,8 +2093,6 @@ const server = createServer(async (req, res) => {
|
|
|
2166
2093
|
const content = typeof parsed.content === 'string' ? parsed.content.replace(/\s+$/u, '') : '';
|
|
2167
2094
|
if (!content.trim())
|
|
2168
2095
|
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
2096
|
const selectedIds = Array.isArray(parsed.larkAppIds)
|
|
2172
2097
|
? Array.from(new Set(parsed.larkAppIds.filter((x) => typeof x === 'string')))
|
|
2173
2098
|
: [];
|