botmux 2.85.0 → 2.86.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/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -13
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +209 -1
- 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 +240 -2
- package/dist/core/dashboard-ipc-server.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/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/daemon.d.ts.map +1 -1
- package/dist/daemon.js +4 -1
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +24 -8
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +170 -49
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +43 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -0
- package/dist/dashboard/bot-payload.js +44 -0
- package/dist/dashboard/bot-payload.js.map +1 -0
- package/dist/dashboard/registry.d.ts +2 -0
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +15 -4
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +122 -3
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +60 -4
- package/dist/dashboard/web/bot-onboarding.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 +631 -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/overview.d.ts +22 -0
- package/dist/dashboard/web/overview.d.ts.map +1 -1
- package/dist/dashboard/web/overview.js +6 -1
- package/dist/dashboard/web/overview.js.map +1 -1
- 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/app.js +1246 -823
- package/dist/dashboard-web/index.html +2 -1
- package/dist/dashboard-web/style.css +1085 -3
- package/dist/dashboard.js +273 -39
- package/dist/dashboard.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/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +23 -1
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +16 -9
- package/dist/im/lark/event-dispatcher.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/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/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/setup/cli-selection.d.ts +20 -1
- package/dist/setup/cli-selection.d.ts.map +1 -1
- package/dist/setup/cli-selection.js +45 -5
- package/dist/setup/cli-selection.js.map +1 -1
- package/dist/worker.js +10 -1
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -36,8 +36,14 @@ import { redactGitUrlCredentials } from './core/skills/sources.js';
|
|
|
36
36
|
import { loadBotConfigs } from './bot-registry.js';
|
|
37
37
|
import { analyzeSkillReferences } from './core/skills/references.js';
|
|
38
38
|
import { installDashboardSkill, parseDashboardSkillInstallRequest } from './dashboard/skill-install-request.js';
|
|
39
|
+
import { botDefaultsPayload, botSummaryPayload } from './dashboard/bot-payload.js';
|
|
40
|
+
import { isValidRoleProfileId } from './services/role-profile-store.js';
|
|
41
|
+
import { mergeSafeInsightOverviews } from './services/insight/report.js';
|
|
39
42
|
const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
40
43
|
const TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
|
|
44
|
+
/** Per-daemon budget for the cross-daemon insight overview fan-out — bounds
|
|
45
|
+
* aggregate latency when one daemon's insight parse is slow or hung. */
|
|
46
|
+
const INSIGHT_FANOUT_TIMEOUT_MS = 10_000;
|
|
41
47
|
const BOTS_JSON_PATH = join(homedir(), '.botmux', 'bots.json');
|
|
42
48
|
const REGISTRY_DIR = join(homedir(), '.botmux', 'data', 'dashboard-daemons');
|
|
43
49
|
// The dashboard probes upward if its configured port is busy (e.g. a second
|
|
@@ -218,7 +224,7 @@ function serveFileAbs(res, fp) {
|
|
|
218
224
|
createReadStream(fp).pipe(res);
|
|
219
225
|
return true;
|
|
220
226
|
}
|
|
221
|
-
function serveStatic(
|
|
227
|
+
function serveStatic(req, res, pathname) {
|
|
222
228
|
const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
223
229
|
const fp = resolve(WEB_DIR, rel);
|
|
224
230
|
const webRoot = resolve(WEB_DIR);
|
|
@@ -230,7 +236,23 @@ function serveStatic(_req, res, pathname) {
|
|
|
230
236
|
const st = statSync(fp);
|
|
231
237
|
if (!st.isFile())
|
|
232
238
|
return false;
|
|
233
|
-
|
|
239
|
+
// Bundle filenames are fixed (app.js/style.css), so without revalidation
|
|
240
|
+
// browsers heuristic-cache them and serve a stale build after a deploy
|
|
241
|
+
// (new JS + old CSS → broken layout). `no-cache` + an mtime/size ETag makes
|
|
242
|
+
// the browser revalidate every load: 304 when unchanged (cheap), fresh 200
|
|
243
|
+
// when the build changed. No manual hard-refresh needed after deploy.
|
|
244
|
+
const etag = `W/"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
|
|
245
|
+
const headers = {
|
|
246
|
+
'content-type': MIME[extname(fp)] ?? 'application/octet-stream',
|
|
247
|
+
'cache-control': 'no-cache',
|
|
248
|
+
etag,
|
|
249
|
+
};
|
|
250
|
+
if (req.headers['if-none-match'] === etag) {
|
|
251
|
+
res.writeHead(304, headers);
|
|
252
|
+
res.end();
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
res.writeHead(200, headers);
|
|
234
256
|
res.end(readFileSync(fp));
|
|
235
257
|
return true;
|
|
236
258
|
}
|
|
@@ -260,10 +282,25 @@ async function proxyToDaemon(larkAppId, daemonPath, init) {
|
|
|
260
282
|
* Surfaces invalidBotIds/invalidUserIds so the UI never implies a non-added
|
|
261
283
|
* bot/user joined. */
|
|
262
284
|
/** Live daemon-registry bots — authoritative source for THIS deployment's
|
|
263
|
-
* bots
|
|
264
|
-
*
|
|
285
|
+
* bots. cliId comes from the daemon descriptor, with bots.json as a
|
|
286
|
+
* compatibility fallback for descriptors written by older daemons. */
|
|
287
|
+
function configuredCliIds() {
|
|
288
|
+
try {
|
|
289
|
+
return new Map(loadBotConfigs().map(b => [b.larkAppId, b.cliId]));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return new Map();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function withConfiguredCliId(bot, ids) {
|
|
296
|
+
return bot.cliId ? bot : { ...bot, cliId: ids.get(bot.larkAppId) };
|
|
297
|
+
}
|
|
265
298
|
function liveBots() {
|
|
266
|
-
|
|
299
|
+
const ids = configuredCliIds();
|
|
300
|
+
return registry.list().map(d => {
|
|
301
|
+
const b = withConfiguredCliId(d, ids);
|
|
302
|
+
return { larkAppId: b.larkAppId, botName: b.botName, cliId: b.cliId };
|
|
303
|
+
});
|
|
267
304
|
}
|
|
268
305
|
async function createTeamGroup(args) {
|
|
269
306
|
const selectedIds = Array.from(new Set(args.larkAppIds.filter(Boolean)));
|
|
@@ -286,7 +323,13 @@ async function createTeamGroup(args) {
|
|
|
286
323
|
const upstream = await proxyToDaemon(plan.creatorLarkAppId, '/api/groups/create', {
|
|
287
324
|
method: 'POST',
|
|
288
325
|
headers: { 'content-type': 'application/json' },
|
|
289
|
-
body: JSON.stringify({
|
|
326
|
+
body: JSON.stringify({
|
|
327
|
+
name: args.name,
|
|
328
|
+
larkAppIds: selectedIds,
|
|
329
|
+
userOpenIds,
|
|
330
|
+
ownerUnionIds: args.ownerUnionIds ?? [],
|
|
331
|
+
...(args.roleProfileId ? { roleProfileId: args.roleProfileId } : {}),
|
|
332
|
+
}),
|
|
290
333
|
});
|
|
291
334
|
const text = await upstream.text();
|
|
292
335
|
let parsed = null;
|
|
@@ -657,6 +700,29 @@ const server = createServer(async (req, res) => {
|
|
|
657
700
|
});
|
|
658
701
|
return jsonRes(res, 200, { sessions });
|
|
659
702
|
}
|
|
703
|
+
if (req.method === 'GET' && url.pathname === '/api/insights/summary') {
|
|
704
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '200', 10) || 200, 1), 500);
|
|
705
|
+
// Per-daemon timeout + isolate failures: an upstream insight parse can be
|
|
706
|
+
// heavy, so a slow/hung daemon must not stall the aggregated summary. A
|
|
707
|
+
// timed-out / errored chunk drops to null and is filtered out below.
|
|
708
|
+
const chunks = await Promise.all(registry.list().map(async (d) => {
|
|
709
|
+
try {
|
|
710
|
+
const upstream = await proxyToDaemon(d.larkAppId, `/api/insights/summary?limit=${limit}`, {
|
|
711
|
+
method: 'GET',
|
|
712
|
+
signal: AbortSignal.timeout(INSIGHT_FANOUT_TIMEOUT_MS),
|
|
713
|
+
});
|
|
714
|
+
if (!upstream.ok)
|
|
715
|
+
return null;
|
|
716
|
+
const body = await upstream.json().catch(() => null);
|
|
717
|
+
return body?.overview ?? null;
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}));
|
|
723
|
+
const overview = mergeSafeInsightOverviews(chunks.filter((x) => !!x), { limit });
|
|
724
|
+
return jsonRes(res, 200, { ok: true, overview });
|
|
725
|
+
}
|
|
660
726
|
if (req.method === 'GET' && url.pathname === '/api/schedules') {
|
|
661
727
|
// Public-read carve-out: the row carries CONTENT (prompt = business
|
|
662
728
|
// instructions) and a bound `workingDir` (repo/customer path) — strip
|
|
@@ -908,6 +974,35 @@ const server = createServer(async (req, res) => {
|
|
|
908
974
|
const job = botOnboarding.start({ cliId, wrapperCli, workingDir, model });
|
|
909
975
|
return jsonRes(res, 202, { job: botOnboarding.get(job.id) });
|
|
910
976
|
}
|
|
977
|
+
let mOwner;
|
|
978
|
+
if (req.method === 'POST' && (mOwner = url.pathname.match(/^\/api\/bot-onboarding\/([^/]+)\/owner$/))) {
|
|
979
|
+
// needs_owner 状态下用户手动提交 owner:扫码人身份验证不了时的兜底入口。
|
|
980
|
+
// submitOwner 内部做格式 + 可用性校验, 通过才落盘并转 completed。
|
|
981
|
+
const onboardingId = decodeURIComponent(mOwner[1]);
|
|
982
|
+
let parsedOwner;
|
|
983
|
+
try {
|
|
984
|
+
const chunks = [];
|
|
985
|
+
for await (const c of req)
|
|
986
|
+
chunks.push(c);
|
|
987
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
988
|
+
parsedOwner = raw ? JSON.parse(raw) : {};
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
992
|
+
}
|
|
993
|
+
// 接受 owner 字符串 (逗号/空白分隔) 或 allowedUsers 数组。
|
|
994
|
+
const entries = Array.isArray(parsedOwner.allowedUsers)
|
|
995
|
+
? parsedOwner.allowedUsers.filter((v) => typeof v === 'string')
|
|
996
|
+
: typeof parsedOwner.owner === 'string'
|
|
997
|
+
? parsedOwner.owner.split(/[,\s]+/).map(s => s.trim()).filter(Boolean)
|
|
998
|
+
: [];
|
|
999
|
+
const r = await botOnboarding.submitOwner(onboardingId, entries);
|
|
1000
|
+
if (!r.ok) {
|
|
1001
|
+
const status = r.error === 'unknown_onboarding_job' ? 404 : 400;
|
|
1002
|
+
return jsonRes(res, status, r);
|
|
1003
|
+
}
|
|
1004
|
+
return jsonRes(res, 200, { job: botOnboarding.get(onboardingId) });
|
|
1005
|
+
}
|
|
911
1006
|
let mOnboard;
|
|
912
1007
|
if (req.method === 'GET' && (mOnboard = url.pathname.match(/^\/api\/bot-onboarding\/([^/]+)$/))) {
|
|
913
1008
|
const job = botOnboarding.get(decodeURIComponent(mOnboard[1]));
|
|
@@ -987,6 +1082,30 @@ const server = createServer(async (req, res) => {
|
|
|
987
1082
|
res.end(await upstream.text());
|
|
988
1083
|
return;
|
|
989
1084
|
}
|
|
1085
|
+
// 会话 insight(只读 trace 分析:动作 span / 失败聚合 / 规则建议)。
|
|
1086
|
+
// owner-only:不在公开读白名单 → decideDashboardAuth 已对只读访客 401,
|
|
1087
|
+
// 公开/联邦访客看不到 tab 也拿不到 span。代理到 owner daemon 的同名 IPC。
|
|
1088
|
+
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/insight$/))) {
|
|
1089
|
+
const sid = decodeURIComponent(m[1]);
|
|
1090
|
+
const owner = aggregator.ownerOf(sid);
|
|
1091
|
+
if (!owner)
|
|
1092
|
+
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
1093
|
+
const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/insight${url.search ?? ''}`, { method: 'GET' });
|
|
1094
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1095
|
+
res.end(await upstream.text());
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/insight\/turn\/([^/]+)$/))) {
|
|
1099
|
+
const sid = decodeURIComponent(m[1]);
|
|
1100
|
+
const turnIndex = decodeURIComponent(m[2]);
|
|
1101
|
+
const owner = aggregator.ownerOf(sid);
|
|
1102
|
+
if (!owner)
|
|
1103
|
+
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
1104
|
+
const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/insight/turn/${turnIndex}${url.search ?? ''}`, { method: 'GET' });
|
|
1105
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1106
|
+
res.end(await upstream.text());
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
990
1109
|
// Writable web-terminal link (token-bearing). Not in any public allow-list,
|
|
991
1110
|
// so decideDashboardAuth has already 401'd unauthenticated callers before we
|
|
992
1111
|
// get here — the token only reaches authenticated dashboard sessions.
|
|
@@ -1054,7 +1173,8 @@ const server = createServer(async (req, res) => {
|
|
|
1054
1173
|
const out = new Map();
|
|
1055
1174
|
// Sort by botIndex so the matrix columns + the create-group bot picker
|
|
1056
1175
|
// both match the order in bots.json (fs.readdir order is unstable).
|
|
1057
|
-
const
|
|
1176
|
+
const cliIds = configuredCliIds();
|
|
1177
|
+
const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, cliIds)).sort((a, b) => a.botIndex - b.botIndex);
|
|
1058
1178
|
await Promise.all(onlineBots.map(async (d) => {
|
|
1059
1179
|
try {
|
|
1060
1180
|
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/groups`);
|
|
@@ -1074,6 +1194,7 @@ const server = createServer(async (req, res) => {
|
|
|
1074
1194
|
cur.memberBots.push({
|
|
1075
1195
|
larkAppId: d.larkAppId,
|
|
1076
1196
|
botName: d.botName,
|
|
1197
|
+
cliId: d.cliId,
|
|
1077
1198
|
inChat: true,
|
|
1078
1199
|
oncallChat: oncallChat ?? null,
|
|
1079
1200
|
hasRole: hasRole ?? false,
|
|
@@ -1093,7 +1214,7 @@ const server = createServer(async (req, res) => {
|
|
|
1093
1214
|
const present = new Set(c.memberBots.map((mb) => mb.larkAppId));
|
|
1094
1215
|
for (const b of onlineBots) {
|
|
1095
1216
|
if (!present.has(b.larkAppId)) {
|
|
1096
|
-
c.memberBots.push({ larkAppId: b.larkAppId, botName: b.botName, inChat: false, oncallChat: null, hasRole: false });
|
|
1217
|
+
c.memberBots.push({ larkAppId: b.larkAppId, botName: b.botName, cliId: b.cliId, inChat: false, oncallChat: null, hasRole: false });
|
|
1097
1218
|
}
|
|
1098
1219
|
}
|
|
1099
1220
|
}
|
|
@@ -1117,7 +1238,7 @@ const server = createServer(async (req, res) => {
|
|
|
1117
1238
|
// prompt strip + keeps /api/bots oncall removal honest).
|
|
1118
1239
|
return jsonRes(res, 200, {
|
|
1119
1240
|
chats: authed ? sorted : redactGroupsForPublic(sorted),
|
|
1120
|
-
bots: onlineBots.map(
|
|
1241
|
+
bots: onlineBots.map(botSummaryPayload),
|
|
1121
1242
|
});
|
|
1122
1243
|
}
|
|
1123
1244
|
// ─── Roles (proxy to daemon) ────────────────────────────────────────────
|
|
@@ -1155,6 +1276,135 @@ const server = createServer(async (req, res) => {
|
|
|
1155
1276
|
return;
|
|
1156
1277
|
}
|
|
1157
1278
|
}
|
|
1279
|
+
// ─── Profiles (aggregate/proxy to daemon) ─────────────────────────────
|
|
1280
|
+
if (req.method === 'GET' && url.pathname === '/api/role-profiles') {
|
|
1281
|
+
const merged = new Map();
|
|
1282
|
+
await Promise.all(registry.list().map(async (d) => {
|
|
1283
|
+
try {
|
|
1284
|
+
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/role-profiles`);
|
|
1285
|
+
if (!r.ok)
|
|
1286
|
+
return;
|
|
1287
|
+
const j = await r.json();
|
|
1288
|
+
for (const p of j.profiles ?? []) {
|
|
1289
|
+
if (typeof p.profileId !== 'string')
|
|
1290
|
+
continue;
|
|
1291
|
+
const cur = merged.get(p.profileId) ?? { profileId: p.profileId, entryCount: 0, updatedAt: null, botEntries: [] };
|
|
1292
|
+
cur.entryCount = Math.max(cur.entryCount, typeof p.entryCount === 'number' ? p.entryCount : 0);
|
|
1293
|
+
if (typeof p.updatedAt === 'number')
|
|
1294
|
+
cur.updatedAt = cur.updatedAt === null ? p.updatedAt : Math.max(cur.updatedAt, p.updatedAt);
|
|
1295
|
+
const larkAppId = j.larkAppId ?? d.larkAppId;
|
|
1296
|
+
if (!cur.botEntries.some(entry => entry.larkAppId === larkAppId)) {
|
|
1297
|
+
cur.botEntries.push({ larkAppId, hasEntry: p.hasCurrentBotEntry === true });
|
|
1298
|
+
}
|
|
1299
|
+
merged.set(p.profileId, cur);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
catch { /* skip offline/bad daemon */ }
|
|
1303
|
+
}));
|
|
1304
|
+
return jsonRes(res, 200, {
|
|
1305
|
+
profiles: [...merged.values()]
|
|
1306
|
+
.map(p => ({
|
|
1307
|
+
...p,
|
|
1308
|
+
entryCount: Math.max(p.entryCount, p.botEntries.filter(entry => entry.hasEntry).length),
|
|
1309
|
+
}))
|
|
1310
|
+
.sort((a, b) => a.profileId.localeCompare(b.profileId)),
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
let mRoleProfileApply;
|
|
1314
|
+
if (req.method === 'POST' && (mRoleProfileApply = url.pathname.match(/^\/api\/role-profiles\/([^/]+)\/apply$/))) {
|
|
1315
|
+
const profileId = decodeURIComponent(mRoleProfileApply[1]);
|
|
1316
|
+
if (!isValidRoleProfileId(profileId))
|
|
1317
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1318
|
+
let raw = '{}';
|
|
1319
|
+
let parsed;
|
|
1320
|
+
try {
|
|
1321
|
+
const chunks = [];
|
|
1322
|
+
for await (const c of req)
|
|
1323
|
+
chunks.push(c);
|
|
1324
|
+
raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1325
|
+
parsed = JSON.parse(raw);
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1329
|
+
}
|
|
1330
|
+
const larkAppId = typeof parsed.larkAppId === 'string' ? parsed.larkAppId : '';
|
|
1331
|
+
if (!larkAppId)
|
|
1332
|
+
return jsonRes(res, 400, { ok: false, error: 'larkAppId_required' });
|
|
1333
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/apply`, {
|
|
1334
|
+
method: 'POST',
|
|
1335
|
+
headers: { 'content-type': 'application/json' },
|
|
1336
|
+
body: raw,
|
|
1337
|
+
});
|
|
1338
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1339
|
+
res.end(await upstream.text());
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
let mRoleProfileEntry;
|
|
1343
|
+
if ((mRoleProfileEntry = url.pathname.match(/^\/api\/role-profiles\/([^/]+)\/([^/]+)$/))) {
|
|
1344
|
+
const profileId = decodeURIComponent(mRoleProfileEntry[1]);
|
|
1345
|
+
const larkAppId = decodeURIComponent(mRoleProfileEntry[2]);
|
|
1346
|
+
if (!isValidRoleProfileId(profileId))
|
|
1347
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1348
|
+
if (req.method === 'GET') {
|
|
1349
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'GET' });
|
|
1350
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1351
|
+
res.end(await upstream.text());
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (req.method === 'PUT') {
|
|
1355
|
+
const chunks = [];
|
|
1356
|
+
for await (const c of req)
|
|
1357
|
+
chunks.push(c);
|
|
1358
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
1359
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, {
|
|
1360
|
+
method: 'PUT',
|
|
1361
|
+
headers: { 'content-type': 'application/json' },
|
|
1362
|
+
body: raw,
|
|
1363
|
+
});
|
|
1364
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1365
|
+
res.end(await upstream.text());
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (req.method === 'DELETE') {
|
|
1369
|
+
const upstream = await proxyToDaemon(larkAppId, `/api/role-profiles/${encodeURIComponent(profileId)}/${encodeURIComponent(larkAppId)}`, { method: 'DELETE' });
|
|
1370
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
1371
|
+
res.end(await upstream.text());
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
let mRoleProfile;
|
|
1376
|
+
if (req.method === 'GET' && (mRoleProfile = url.pathname.match(/^\/api\/role-profiles\/([^/]+)$/))) {
|
|
1377
|
+
const profileId = decodeURIComponent(mRoleProfile[1]);
|
|
1378
|
+
if (!isValidRoleProfileId(profileId))
|
|
1379
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1380
|
+
const byBot = new Map();
|
|
1381
|
+
await Promise.all(registry.list().map(async (d) => {
|
|
1382
|
+
try {
|
|
1383
|
+
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/role-profiles/${encodeURIComponent(profileId)}`);
|
|
1384
|
+
if (!r.ok)
|
|
1385
|
+
return;
|
|
1386
|
+
const j = await r.json();
|
|
1387
|
+
for (const entry of j.entries ?? []) {
|
|
1388
|
+
if (typeof entry.larkAppId !== 'string' || typeof entry.content !== 'string')
|
|
1389
|
+
continue;
|
|
1390
|
+
const updatedAt = typeof entry.updatedAt === 'number' ? entry.updatedAt : null;
|
|
1391
|
+
const current = byBot.get(entry.larkAppId);
|
|
1392
|
+
if (current && (current.updatedAt ?? 0) > (updatedAt ?? 0))
|
|
1393
|
+
continue;
|
|
1394
|
+
byBot.set(entry.larkAppId, {
|
|
1395
|
+
profileId,
|
|
1396
|
+
larkAppId: entry.larkAppId,
|
|
1397
|
+
content: entry.content,
|
|
1398
|
+
byteLength: typeof entry.byteLength === 'number' ? entry.byteLength : Buffer.byteLength(entry.content, 'utf-8'),
|
|
1399
|
+
updatedAt,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
catch { /* skip */ }
|
|
1404
|
+
}));
|
|
1405
|
+
const entries = [...byBot.values()].sort((a, b) => a.larkAppId.localeCompare(b.larkAppId));
|
|
1406
|
+
return jsonRes(res, 200, { profileId, entries });
|
|
1407
|
+
}
|
|
1158
1408
|
let m2;
|
|
1159
1409
|
if (req.method === 'POST' && (m2 = url.pathname.match(/^\/api\/groups\/([^/]+)\/add-bots$/))) {
|
|
1160
1410
|
const chatId = decodeURIComponent(m2[1]);
|
|
@@ -1321,44 +1571,19 @@ const server = createServer(async (req, res) => {
|
|
|
1321
1571
|
// [{larkAppId, botName, defaultOncall, ...}]
|
|
1322
1572
|
// PUT /api/bots/:appId/default-oncall — proxy to that bot's daemon
|
|
1323
1573
|
if (req.method === 'GET' && url.pathname === '/api/bots') {
|
|
1324
|
-
const
|
|
1574
|
+
const cliIds = configuredCliIds();
|
|
1575
|
+
const onlineBots = [...registry.list()].map(b => withConfiguredCliId(b, cliIds)).sort((a, b) => a.botIndex - b.botIndex);
|
|
1325
1576
|
const out = await Promise.all(onlineBots.map(async (d) => {
|
|
1326
1577
|
try {
|
|
1327
1578
|
const r = await fetch(`http://127.0.0.1:${d.ipcPort}/api/bot-default-oncall`);
|
|
1328
1579
|
if (!r.ok) {
|
|
1329
|
-
return
|
|
1580
|
+
return botDefaultsPayload(d, undefined, `http_${r.status}`);
|
|
1330
1581
|
}
|
|
1331
1582
|
const j = await r.json();
|
|
1332
|
-
return {
|
|
1333
|
-
larkAppId: d.larkAppId,
|
|
1334
|
-
botName: d.botName ?? j.botName,
|
|
1335
|
-
online: true,
|
|
1336
|
-
defaultOncall: j.defaultOncall,
|
|
1337
|
-
autoboundChatCount: j.autoboundChatCount ?? 0,
|
|
1338
|
-
brandLabel: j.brandLabel ?? null,
|
|
1339
|
-
sandbox: j.sandbox === true,
|
|
1340
|
-
disableStreamingCard: j.disableStreamingCard === true,
|
|
1341
|
-
writableTerminalLinkInCard: j.writableTerminalLinkInCard === true,
|
|
1342
|
-
privateCard: j.privateCard === true,
|
|
1343
|
-
autoStartOnGroupJoin: j.autoStartOnGroupJoin === true,
|
|
1344
|
-
autoStartOnGroupJoinPrompt: typeof j.autoStartOnGroupJoinPrompt === 'string' ? j.autoStartOnGroupJoinPrompt : '',
|
|
1345
|
-
autoStartOnNewTopic: j.autoStartOnNewTopic === true,
|
|
1346
|
-
regularGroupReplyMode: (j.regularGroupReplyMode === 'new-topic' || j.regularGroupReplyMode === 'shared')
|
|
1347
|
-
? j.regularGroupReplyMode
|
|
1348
|
-
: 'chat',
|
|
1349
|
-
regularGroupMentionMode: (j.regularGroupMentionMode === 'topic' || j.regularGroupMentionMode === 'never')
|
|
1350
|
-
? j.regularGroupMentionMode
|
|
1351
|
-
: 'always',
|
|
1352
|
-
restrictGrantCommands: j.restrictGrantCommands === true,
|
|
1353
|
-
messageQuotaDefaultLimit: typeof j.messageQuotaDefaultLimit === 'number' ? j.messageQuotaDefaultLimit : null,
|
|
1354
|
-
p2pMode: j.p2pMode === 'chat' ? 'chat' : 'thread',
|
|
1355
|
-
maxLiveWorkers: typeof j.maxLiveWorkers === 'number' ? j.maxLiveWorkers : null,
|
|
1356
|
-
startupCommands: typeof j.startupCommands === 'string' ? j.startupCommands : '',
|
|
1357
|
-
skills: j.skills && typeof j.skills === 'object' ? j.skills : null,
|
|
1358
|
-
};
|
|
1583
|
+
return botDefaultsPayload({ ...d, botName: d.botName ?? j.botName }, j);
|
|
1359
1584
|
}
|
|
1360
1585
|
catch (e) {
|
|
1361
|
-
return
|
|
1586
|
+
return botDefaultsPayload(d, undefined, e?.message ?? String(e));
|
|
1362
1587
|
}
|
|
1363
1588
|
}));
|
|
1364
1589
|
return jsonRes(res, 200, { bots: out });
|
|
@@ -1548,6 +1773,12 @@ const server = createServer(async (req, res) => {
|
|
|
1548
1773
|
if (selectedIds.length === 0) {
|
|
1549
1774
|
return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
|
|
1550
1775
|
}
|
|
1776
|
+
const roleProfileId = typeof parsed.roleProfileId === 'string' && parsed.roleProfileId.trim()
|
|
1777
|
+
? parsed.roleProfileId.trim()
|
|
1778
|
+
: null;
|
|
1779
|
+
if (roleProfileId && !isValidRoleProfileId(roleProfileId)) {
|
|
1780
|
+
return jsonRes(res, 400, { ok: false, error: 'invalid_role_profile_id' });
|
|
1781
|
+
}
|
|
1551
1782
|
const explicit = Array.isArray(parsed.userOpenIds)
|
|
1552
1783
|
? parsed.userOpenIds.filter((x) => typeof x === 'string')
|
|
1553
1784
|
: [];
|
|
@@ -1578,6 +1809,7 @@ const server = createServer(async (req, res) => {
|
|
|
1578
1809
|
bindWorkingDir: typeof parsed.bindWorkingDir === 'string' && parsed.bindWorkingDir.trim()
|
|
1579
1810
|
? parsed.bindWorkingDir.trim()
|
|
1580
1811
|
: undefined,
|
|
1812
|
+
roleProfileId: roleProfileId ?? undefined,
|
|
1581
1813
|
};
|
|
1582
1814
|
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) });
|
|
1583
1815
|
const upstreamText = await upstream.text();
|
|
@@ -1587,6 +1819,8 @@ const server = createServer(async (req, res) => {
|
|
|
1587
1819
|
}
|
|
1588
1820
|
catch { /* leave null */ }
|
|
1589
1821
|
if (upstreamJson && typeof upstreamJson === 'object') {
|
|
1822
|
+
if (roleProfileId)
|
|
1823
|
+
upstreamJson.roleProfileId = roleProfileId;
|
|
1590
1824
|
// If Lark rejected the invite (open_id wrong scope, banned user, etc.)
|
|
1591
1825
|
// null out autoInvitedOpenId so the frontend doesn't falsely claim
|
|
1592
1826
|
// success — the user actually isn't a member of the new chat.
|