botmux 2.51.1 → 2.53.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 +22 -265
- package/README.md +21 -296
- package/dist/adapters/backend/session-backend-selector.d.ts +5 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +15 -1
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +3 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +22 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js +7 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts +132 -0
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zellij-backend.js +375 -0
- package/dist/adapters/backend/zellij-backend.js.map +1 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts +62 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zellij-observe-backend.js +218 -0
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -0
- package/dist/adapters/cli/claude-code.d.ts +39 -5
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +53 -31
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +2 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +3 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/seed.d.ts +29 -0
- package/dist/adapters/cli/seed.d.ts.map +1 -0
- package/dist/adapters/cli/seed.js +63 -0
- package/dist/adapters/cli/seed.js.map +1 -0
- package/dist/adapters/cli/types.d.ts +17 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +31 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +31 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/ask-hook/registry.d.ts.map +1 -1
- package/dist/core/ask-hook/registry.js +4 -0
- package/dist/core/ask-hook/registry.js.map +1 -1
- package/dist/core/command-handler.d.ts +4 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +100 -8
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +37 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch.d.ts +33 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +26 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/session-discovery.d.ts +13 -4
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +5 -5
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +10 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +43 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +5 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +22 -9
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/core/zellij-adopt-discovery.d.ts +28 -0
- package/dist/core/zellij-adopt-discovery.d.ts.map +1 -0
- package/dist/core/zellij-adopt-discovery.js +255 -0
- package/dist/core/zellij-adopt-discovery.js.map +1 -0
- package/dist/core/zellij-session-discovery.d.ts +73 -0
- package/dist/core/zellij-session-discovery.d.ts.map +1 -0
- package/dist/core/zellij-session-discovery.js +259 -0
- package/dist/core/zellij-session-discovery.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +145 -13
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +114 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +23 -1
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +1 -0
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/workflows.js +1 -1
- package/dist/dashboard/web/workflows.js.map +1 -1
- package/dist/dashboard-web/app.js +449 -426
- package/dist/dashboard.js +20 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +15 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +16 -2
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +8 -3
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +74 -5
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +72 -10
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +12 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +39 -31
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +26 -0
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +142 -3
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +7 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +12 -6
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +20 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -0
- package/dist/services/codex-app-threads.js +165 -0
- package/dist/services/codex-app-threads.js.map +1 -0
- package/dist/services/grant-prefs-store.d.ts +23 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -0
- package/dist/services/grant-prefs-store.js +94 -0
- package/dist/services/grant-prefs-store.js.map +1 -0
- package/dist/services/grant-store.d.ts +34 -2
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +160 -9
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/quota-dedup.d.ts +33 -0
- package/dist/services/quota-dedup.d.ts.map +1 -0
- package/dist/services/quota-dedup.js +67 -0
- package/dist/services/quota-dedup.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +5 -4
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-zellij.d.ts +48 -0
- package/dist/setup/ensure-zellij.d.ts.map +1 -0
- package/dist/setup/ensure-zellij.js +93 -0
- package/dist/setup/ensure-zellij.js.map +1 -0
- package/dist/types.d.ts +9 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.d.ts +3 -2
- package/dist/utils/anchor-serializer.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.js +20 -5
- package/dist/utils/anchor-serializer.js.map +1 -1
- package/dist/utils/transient-snapshot.js +2 -2
- package/dist/utils/transient-snapshot.js.map +1 -1
- package/dist/worker.js +124 -30
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts +1 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js +1 -1
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface LayoutPane {
|
|
2
|
+
/** Foreground command (argv0) zellij introspected for this pane, e.g. "claude". */
|
|
3
|
+
command: string;
|
|
4
|
+
/** Explicit pane name if set (zellij `name=`), else undefined. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Absolute cwd of the pane (layout base cwd joined with the pane's relative cwd). */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Command args, when present in the dump. */
|
|
9
|
+
args: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface ListedPane {
|
|
12
|
+
/** "terminal_<n>" — the id used to target zellij `action` commands. */
|
|
13
|
+
paneId: string;
|
|
14
|
+
isPlugin: boolean;
|
|
15
|
+
isFloating: boolean;
|
|
16
|
+
title?: string;
|
|
17
|
+
terminalCommand?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface DiscoveredCli {
|
|
20
|
+
session: string;
|
|
21
|
+
paneId: string;
|
|
22
|
+
command: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
title?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse `zellij action dump-layout` output, returning only the panes that have
|
|
29
|
+
* a foreground `command=` (i.e. real terminal panes running something) in
|
|
30
|
+
* document order. Template sections (new_tab_template / swap_*_layout) are cut
|
|
31
|
+
* off first — their bare `pane` nodes have no command and would otherwise add
|
|
32
|
+
* noise. Plugin panes (tab-bar / status-bar / about) have no command= and are
|
|
33
|
+
* naturally excluded.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseDumpLayoutPanes(kdl: string): LayoutPane[];
|
|
36
|
+
export interface LeafPane {
|
|
37
|
+
/** Foreground command (argv0) if the pane is running one; undefined for an
|
|
38
|
+
* idle shell pane (zellij emits a bare `pane` with no command=). */
|
|
39
|
+
command?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
cwd?: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse `zellij action dump-layout` into the ordered list of LEAF terminal
|
|
46
|
+
* panes — both command-bearing panes AND idle bare shell panes — skipping
|
|
47
|
+
* plugin panes (tab-bar/status-bar/about), container panes (splits), and the
|
|
48
|
+
* floating subtree. Preserving bare panes is what makes a positional join to
|
|
49
|
+
* `list-panes` correct: a bare shell pane that sorts before the CLI pane would
|
|
50
|
+
* otherwise shift every command pane onto the wrong pane id (the bug Codex
|
|
51
|
+
* found). Templates (new_tab_template / swap_*) are cut off first.
|
|
52
|
+
*
|
|
53
|
+
* zellij always pretty-prints dump-layout one node per line, so a brace-stack
|
|
54
|
+
* line walk is reliable here.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseDumpLayoutLeafPanes(kdl: string): LeafPane[];
|
|
57
|
+
/** Parse `zellij action list-panes --json` into a flat list (document order). */
|
|
58
|
+
export declare function parseListPanesJson(json: string): ListedPane[];
|
|
59
|
+
/**
|
|
60
|
+
* Join dump-layout command panes with list-panes terminal panes by document
|
|
61
|
+
* order: the i-th command pane ↔ the i-th non-plugin terminal pane (sorted by
|
|
62
|
+
* id). zellij assigns pane ids in creation order and walks the tree in a stable
|
|
63
|
+
* order, so this aligns for normal layouts. Returns one DiscoveredCli per
|
|
64
|
+
* command pane that could be bound to an id.
|
|
65
|
+
*/
|
|
66
|
+
export declare function joinPanes(session: string, layoutPanes: LayoutPane[], listed: ListedPane[]): DiscoveredCli[];
|
|
67
|
+
/** Names of live (non-exited) zellij sessions. */
|
|
68
|
+
export declare function listLiveSessions(): string[];
|
|
69
|
+
/** Discover CLIs running in one session. */
|
|
70
|
+
export declare function discoverSessionClis(session: string): DiscoveredCli[];
|
|
71
|
+
/** Discover CLIs across every live zellij session. */
|
|
72
|
+
export declare function discoverAllClis(): DiscoveredCli[];
|
|
73
|
+
//# sourceMappingURL=zellij-session-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zellij-session-discovery.d.ts","sourceRoot":"","sources":["../../src/core/zellij-session-discovery.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CA6C9D;AAED,MAAM,WAAW,QAAQ;IACvB;yEACqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAuDhE;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAW7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,aAAa,EAAE,CAmB3G;AAID,kDAAkD;AAClD,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAS3C;AAUD,4CAA4C;AAC5C,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAKpE;AAED,sDAAsD;AACtD,wBAAgB,eAAe,IAAI,aAAa,EAAE,CAEjD"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zellij session discovery for /adopt — find CLIs already running inside a
|
|
3
|
+
* user's zellij sessions and the pane needed to drive them.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (and why it's not just `list-panes`): zellij's
|
|
6
|
+
* `list-panes --json` exposes pane ids + geometry but NOT the running command,
|
|
7
|
+
* cwd, or pid, and its `terminal_command` is null for anything the user started
|
|
8
|
+
* interactively (typed `claude` into a shell — the common case). The data we
|
|
9
|
+
* need is instead surfaced by zellij's **session-resurrection** machinery: to be
|
|
10
|
+
* able to restore a session after a reboot, zellij continuously introspects each
|
|
11
|
+
* pane's *foreground process command + cwd* and exposes it via
|
|
12
|
+
* `zellij action dump-layout`. That's where we read "what CLI is in this pane".
|
|
13
|
+
*
|
|
14
|
+
* Discovery pipeline:
|
|
15
|
+
* 1. dump-layout → per-pane { command, args, cwd } (detection)
|
|
16
|
+
* 2. list-panes --json → per-pane { id: terminal_<n> } (drive target)
|
|
17
|
+
* 3. order/geometry join → bind command ↔ pane id
|
|
18
|
+
* 4. (caller) /proc descent under the pane shell → pid → ~/.claude/sessions/<pid>.json
|
|
19
|
+
*
|
|
20
|
+
* Parsers here are pure (string in, struct out) so they unit-test without a
|
|
21
|
+
* live zellij. The order-join (step 3) is robust for normal single/few-pane
|
|
22
|
+
* layouts; exotic multi-tab/floating arrangements may need the geometry/proc
|
|
23
|
+
* cross-check the caller layers on top.
|
|
24
|
+
*/
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { isAbsolute, join as pathJoin } from 'node:path';
|
|
27
|
+
import { zellijEnv } from '../setup/ensure-zellij.js';
|
|
28
|
+
const TEMPLATE_MARKERS = /\b(new_tab_template|swap_tiled_layout|swap_floating_layout)\b/;
|
|
29
|
+
/**
|
|
30
|
+
* Parse `zellij action dump-layout` output, returning only the panes that have
|
|
31
|
+
* a foreground `command=` (i.e. real terminal panes running something) in
|
|
32
|
+
* document order. Template sections (new_tab_template / swap_*_layout) are cut
|
|
33
|
+
* off first — their bare `pane` nodes have no command and would otherwise add
|
|
34
|
+
* noise. Plugin panes (tab-bar / status-bar / about) have no command= and are
|
|
35
|
+
* naturally excluded.
|
|
36
|
+
*/
|
|
37
|
+
export function parseDumpLayoutPanes(kdl) {
|
|
38
|
+
// Drop the template tail so we only see live tab content.
|
|
39
|
+
const tmplIdx = kdl.search(TEMPLATE_MARKERS);
|
|
40
|
+
const body = tmplIdx >= 0 ? kdl.slice(0, tmplIdx) : kdl;
|
|
41
|
+
const lines = body.split('\n');
|
|
42
|
+
const panes = [];
|
|
43
|
+
// Layout base cwd is a NODE: `cwd "..."` (space). Pane cwd is an ATTRIBUTE:
|
|
44
|
+
// `cwd="..."` (equals). The first node-form cwd is the layout base.
|
|
45
|
+
let layoutCwd;
|
|
46
|
+
const baseCwdMatch = body.match(/^\s*cwd\s+"([^"]*)"/m);
|
|
47
|
+
if (baseCwdMatch)
|
|
48
|
+
layoutCwd = baseCwdMatch[1];
|
|
49
|
+
let pending = null;
|
|
50
|
+
const flush = () => { if (pending) {
|
|
51
|
+
panes.push(pending);
|
|
52
|
+
pending = null;
|
|
53
|
+
} };
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
// A pane that runs a command (attributes can appear in any order).
|
|
57
|
+
if (/^pane\b/.test(trimmed) && /\bcommand=/.test(trimmed)) {
|
|
58
|
+
flush();
|
|
59
|
+
const command = attr(trimmed, 'command');
|
|
60
|
+
const name = attr(trimmed, 'name');
|
|
61
|
+
const cwdAttr = attr(trimmed, 'cwd');
|
|
62
|
+
pending = {
|
|
63
|
+
command: command ?? '',
|
|
64
|
+
name: name ?? undefined,
|
|
65
|
+
cwd: resolveCwd(layoutCwd, cwdAttr),
|
|
66
|
+
args: [],
|
|
67
|
+
};
|
|
68
|
+
// Single-line pane (closed on same line, no block) — flush immediately.
|
|
69
|
+
if (trimmed.includes('}') && !trimmed.endsWith('{'))
|
|
70
|
+
flush();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// args node inside the current pane block: `args "a" "b" …`
|
|
74
|
+
if (pending && /^args\b/.test(trimmed)) {
|
|
75
|
+
pending.args = [...trimmed.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map(m => unescapeKdl(m[1]));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Next pane / tab / closing — a new `pane` (without command) ends the block.
|
|
79
|
+
if (pending && /^pane\b/.test(trimmed))
|
|
80
|
+
flush();
|
|
81
|
+
}
|
|
82
|
+
flush();
|
|
83
|
+
return panes;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse `zellij action dump-layout` into the ordered list of LEAF terminal
|
|
87
|
+
* panes — both command-bearing panes AND idle bare shell panes — skipping
|
|
88
|
+
* plugin panes (tab-bar/status-bar/about), container panes (splits), and the
|
|
89
|
+
* floating subtree. Preserving bare panes is what makes a positional join to
|
|
90
|
+
* `list-panes` correct: a bare shell pane that sorts before the CLI pane would
|
|
91
|
+
* otherwise shift every command pane onto the wrong pane id (the bug Codex
|
|
92
|
+
* found). Templates (new_tab_template / swap_*) are cut off first.
|
|
93
|
+
*
|
|
94
|
+
* zellij always pretty-prints dump-layout one node per line, so a brace-stack
|
|
95
|
+
* line walk is reliable here.
|
|
96
|
+
*/
|
|
97
|
+
export function parseDumpLayoutLeafPanes(kdl) {
|
|
98
|
+
const tmplIdx = kdl.search(TEMPLATE_MARKERS);
|
|
99
|
+
const body = tmplIdx >= 0 ? kdl.slice(0, tmplIdx) : kdl;
|
|
100
|
+
const baseCwdMatch = body.match(/^\s*cwd\s+"([^"]*)"/m);
|
|
101
|
+
const layoutCwd = baseCwdMatch ? baseCwdMatch[1] : undefined;
|
|
102
|
+
const stack = [];
|
|
103
|
+
const leaves = [];
|
|
104
|
+
const inFloating = () => stack.some(f => f.isFloating);
|
|
105
|
+
const emit = (command, name, cwdAttr, args) => {
|
|
106
|
+
if (inFloating())
|
|
107
|
+
return;
|
|
108
|
+
leaves.push({ command, name, cwd: resolveCwd(layoutCwd, cwdAttr), args });
|
|
109
|
+
};
|
|
110
|
+
for (const raw of body.split('\n')) {
|
|
111
|
+
const line = raw.trim();
|
|
112
|
+
if (!line)
|
|
113
|
+
continue;
|
|
114
|
+
if (line === '}') {
|
|
115
|
+
const f = stack.pop();
|
|
116
|
+
// A pane frame that contained neither a plugin nor child panes is a leaf
|
|
117
|
+
// terminal pane (its block held only props like args/start_suspended).
|
|
118
|
+
if (f?.isPane && !f.hasPlugin && !f.hasChildPane && !inFloating()) {
|
|
119
|
+
leaves.push({ command: f.command, name: f.name, cwd: resolveCwd(layoutCwd, f.cwdAttr), args: f.args });
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const opensBlock = line.endsWith('{');
|
|
124
|
+
if (line.startsWith('plugin')) {
|
|
125
|
+
if (stack.length && stack[stack.length - 1].isPane)
|
|
126
|
+
stack[stack.length - 1].hasPlugin = true;
|
|
127
|
+
if (opensBlock)
|
|
128
|
+
stack.push({ isPane: false, isFloating: false, args: [], hasPlugin: false, hasChildPane: false });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (line.startsWith('pane')) {
|
|
132
|
+
if (stack.length && stack[stack.length - 1].isPane)
|
|
133
|
+
stack[stack.length - 1].hasChildPane = true;
|
|
134
|
+
const command = attr(line, 'command');
|
|
135
|
+
const name = attr(line, 'name');
|
|
136
|
+
const cwdAttr = attr(line, 'cwd');
|
|
137
|
+
if (opensBlock) {
|
|
138
|
+
stack.push({ isPane: true, isFloating: false, command, name, cwdAttr, args: [], hasPlugin: false, hasChildPane: false });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
emit(command, name, cwdAttr, []); // bare leaf, no block
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (opensBlock) {
|
|
146
|
+
// tab / floating_panes / swap_* / other container
|
|
147
|
+
stack.push({ isPane: false, isFloating: line.startsWith('floating_panes'), args: [], hasPlugin: false, hasChildPane: false });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (line.startsWith('args') && stack.length && stack[stack.length - 1].isPane) {
|
|
151
|
+
stack[stack.length - 1].args = [...line.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map(m => unescapeKdl(m[1]));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return leaves;
|
|
155
|
+
}
|
|
156
|
+
/** Parse `zellij action list-panes --json` into a flat list (document order). */
|
|
157
|
+
export function parseListPanesJson(json) {
|
|
158
|
+
let arr;
|
|
159
|
+
try {
|
|
160
|
+
arr = JSON.parse(json);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(arr))
|
|
166
|
+
return [];
|
|
167
|
+
return arr.map((p) => ({
|
|
168
|
+
paneId: `terminal_${p.id}`,
|
|
169
|
+
isPlugin: !!p.is_plugin,
|
|
170
|
+
isFloating: !!p.is_floating,
|
|
171
|
+
title: typeof p.title === 'string' ? p.title : undefined,
|
|
172
|
+
terminalCommand: p.terminal_command ?? null,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Join dump-layout command panes with list-panes terminal panes by document
|
|
177
|
+
* order: the i-th command pane ↔ the i-th non-plugin terminal pane (sorted by
|
|
178
|
+
* id). zellij assigns pane ids in creation order and walks the tree in a stable
|
|
179
|
+
* order, so this aligns for normal layouts. Returns one DiscoveredCli per
|
|
180
|
+
* command pane that could be bound to an id.
|
|
181
|
+
*/
|
|
182
|
+
export function joinPanes(session, layoutPanes, listed) {
|
|
183
|
+
const terminals = listed
|
|
184
|
+
.filter(p => !p.isPlugin)
|
|
185
|
+
.sort((a, b) => paneNum(a.paneId) - paneNum(b.paneId));
|
|
186
|
+
const out = [];
|
|
187
|
+
for (let i = 0; i < layoutPanes.length; i++) {
|
|
188
|
+
const lp = layoutPanes[i];
|
|
189
|
+
const tp = terminals[i];
|
|
190
|
+
if (!tp)
|
|
191
|
+
break;
|
|
192
|
+
out.push({
|
|
193
|
+
session,
|
|
194
|
+
paneId: tp.paneId,
|
|
195
|
+
command: lp.command,
|
|
196
|
+
cwd: lp.cwd,
|
|
197
|
+
args: lp.args,
|
|
198
|
+
title: tp.title,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
// ─── Runtime (shells out to zellij) ─────────────────────────────────────────
|
|
204
|
+
/** Names of live (non-exited) zellij sessions. */
|
|
205
|
+
export function listLiveSessions() {
|
|
206
|
+
try {
|
|
207
|
+
const out = execFileSync('zellij', ['list-sessions', '--no-formatting'], {
|
|
208
|
+
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, env: zellijEnv(),
|
|
209
|
+
});
|
|
210
|
+
return out.split('\n').map(l => l.trim())
|
|
211
|
+
.filter(l => l.length > 0 && !/EXITED/i.test(l))
|
|
212
|
+
.map(l => l.split(/\s+/)[0]).filter(Boolean);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function zellijAction(session, args) {
|
|
219
|
+
try {
|
|
220
|
+
return execFileSync('zellij', ['--session', session, 'action', ...args], {
|
|
221
|
+
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000, env: zellijEnv(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Discover CLIs running in one session. */
|
|
229
|
+
export function discoverSessionClis(session) {
|
|
230
|
+
const layout = zellijAction(session, ['dump-layout']);
|
|
231
|
+
const panesJson = zellijAction(session, ['list-panes', '--json']);
|
|
232
|
+
if (!layout || !panesJson)
|
|
233
|
+
return [];
|
|
234
|
+
return joinPanes(session, parseDumpLayoutPanes(layout), parseListPanesJson(panesJson));
|
|
235
|
+
}
|
|
236
|
+
/** Discover CLIs across every live zellij session. */
|
|
237
|
+
export function discoverAllClis() {
|
|
238
|
+
return listLiveSessions().flatMap(discoverSessionClis);
|
|
239
|
+
}
|
|
240
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
241
|
+
function attr(line, key) {
|
|
242
|
+
const m = line.match(new RegExp(`\\b${key}="((?:[^"\\\\]|\\\\.)*)"`));
|
|
243
|
+
return m ? unescapeKdl(m[1]) : undefined;
|
|
244
|
+
}
|
|
245
|
+
function resolveCwd(base, paneCwd) {
|
|
246
|
+
if (!paneCwd)
|
|
247
|
+
return base;
|
|
248
|
+
if (isAbsolute(paneCwd))
|
|
249
|
+
return paneCwd;
|
|
250
|
+
return base ? pathJoin(base, paneCwd) : paneCwd;
|
|
251
|
+
}
|
|
252
|
+
function paneNum(paneId) {
|
|
253
|
+
const m = paneId.match(/(\d+)$/);
|
|
254
|
+
return m ? Number(m[1]) : 0;
|
|
255
|
+
}
|
|
256
|
+
function unescapeKdl(s) {
|
|
257
|
+
return s.replace(/\\(.)/g, '$1');
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=zellij-session-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zellij-session-discovery.js","sourceRoot":"","sources":["../../src/core/zellij-session-discovery.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type { DaemonSession } from './core/types.js';
|
|
2
2
|
import { WorkflowEventWatcher } from './workflows/fanout.js';
|
|
3
3
|
import type { WorkflowRuntimeContext } from './workflows/runtime.js';
|
|
4
|
+
export declare function enforceMessageQuotaForCliInput(larkAppId: string, chatId: string, senderOpenId: string | undefined, messageId: string, anchor: string): Promise<boolean>;
|
|
5
|
+
export declare function grantRestrictedCommandText(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined, cmd: string): string | undefined;
|
|
6
|
+
export declare function grantRestrictedSlashCommandText(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined, cmd: string): string | undefined;
|
|
4
7
|
export declare function attachWorkflowEventWatcher(runId: string, ctx?: WorkflowRuntimeContext): WorkflowEventWatcher;
|
|
5
8
|
export declare function startDaemon(botIndex?: number): Promise<void>;
|
|
6
9
|
//# sourceMappingURL=daemon.d.ts.map
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AA0BA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AA0BA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAsErD,OAAO,EAAE,oBAAoB,EAA6B,MAAM,uBAAuB,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAiB,MAAM,wBAAwB,CAAC;AAsQpF,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CA4ClB;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAGpB;AAyJD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,sBAAsB,GAAG,oBAAoB,CA0C5G;AAkpED,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6TlE"}
|
package/dist/daemon.js
CHANGED
|
@@ -27,7 +27,7 @@ import { buildTerminalUrl, setTerminalProxyPort } from './core/terminal-url.js';
|
|
|
27
27
|
import { startTerminalProxy } from './core/terminal-proxy.js';
|
|
28
28
|
import * as scheduler from './core/scheduler.js';
|
|
29
29
|
import { scanMultipleProjects } from './services/project-scanner.js';
|
|
30
|
-
import { buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
|
|
30
|
+
import { buildQuotaExhaustedCard, buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
|
|
31
31
|
import { t as tr, botLocale, localeForBot } from './i18n/index.js';
|
|
32
32
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
33
33
|
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
|
|
@@ -36,14 +36,16 @@ import { saveFrozenCards } from './services/frozen-card-store.js';
|
|
|
36
36
|
import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, PASSTHROUGH_COMMANDS, handleCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from './core/command-handler.js';
|
|
37
37
|
import { findInheritablePeer } from './core/inherit-peer.js';
|
|
38
38
|
import { isCallbackUrl, handleCallbackUrl } from './utils/user-token.js';
|
|
39
|
+
import { consumeQuota, removeChatGrant, removeGlobalGrant } from './services/grant-store.js';
|
|
40
|
+
import { abortCharge, commitCharge, beginCharge } from './services/quota-dedup.js';
|
|
39
41
|
import { getSessionWorkingDir, getProjectScanDirs, expandHome, downloadResources, formatAttachmentsHint, buildNewTopicPrompt, buildFollowUpContent, buildBridgeInputContent, buildReforkPrompt, getAvailableBots, restoreActiveSessions, executeScheduledTask, persistStreamCardState, rememberLastCliInput, } from './core/session-manager.js';
|
|
40
42
|
import { handleCardAction } from './im/lark/card-handler.js';
|
|
41
|
-
import { executeWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
|
|
43
|
+
import { executeWorkflowCommand, parseWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
|
|
42
44
|
import { workflowRunDetailUrl } from './im/lark/workflow-cards.js';
|
|
43
45
|
import { buildWorkflowStartingCard, buildWorkflowProgressCard, buildAttemptDeeplinkEnricher, } from './im/lark/workflow-progress-card.js';
|
|
44
46
|
import { EventLog as WorkflowEventLog } from './workflows/events/append.js';
|
|
45
47
|
import { replay as replayWorkflow } from './workflows/events/replay.js';
|
|
46
|
-
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
48
|
+
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
47
49
|
import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
|
|
48
50
|
import { renderSenderTag } from './core/session-manager.js';
|
|
49
51
|
import { markSessionActivity } from './core/session-activity.js';
|
|
@@ -222,6 +224,94 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId) {
|
|
|
222
224
|
// Thread-scope (or unknown / legacy): reply in thread.
|
|
223
225
|
return replyMessage(appId, anchor, content, msgType, true);
|
|
224
226
|
}
|
|
227
|
+
async function revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev) {
|
|
228
|
+
const result = ev.reason === 'chatGrant'
|
|
229
|
+
? await removeChatGrant(larkAppId, chatId, senderOpenId)
|
|
230
|
+
: ev.reason === 'globalGrant'
|
|
231
|
+
? await removeGlobalGrant(larkAppId, senderOpenId)
|
|
232
|
+
: { ok: true, removed: false };
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
logger.warn(`[quota:${larkAppId}] revoke after quota exhaustion failed: reason=${result.reason} user=${senderOpenId.substring(0, 12)} reasonType=${ev.reason}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function notifyQuotaExhausted(larkAppId, anchor, senderOpenId, limit) {
|
|
238
|
+
if (typeof limit !== 'number')
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
await sessionReply(anchor, buildQuotaExhaustedCard(senderOpenId, limit, localeForBot(larkAppId)), 'interactive', larkAppId);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
logger.warn(`[quota:${larkAppId}] quota exhausted notify failed: ${err}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export async function enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor) {
|
|
248
|
+
const ev = evaluateTalk(larkAppId, chatId, senderOpenId);
|
|
249
|
+
if (!ev.allowed) {
|
|
250
|
+
logger.debug(`[quota:${larkAppId}] dropping message ${messageId.substring(0, 12)} from non-allowed sender ${senderOpenId?.substring(0, 12) ?? '?'}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (!ev.quotaKey)
|
|
254
|
+
return true;
|
|
255
|
+
if (!senderOpenId)
|
|
256
|
+
return false;
|
|
257
|
+
// 去重三态:'done' = 同条已成功扣费 → 放行(不重复扣);'pending' = 同条扣费 in-flight 未定论
|
|
258
|
+
// → fail-closed drop(绝不在定论前放行第二投);'fresh' = 首次见 → 继续扣费。
|
|
259
|
+
const charge = beginCharge(larkAppId, messageId);
|
|
260
|
+
if (charge === 'done')
|
|
261
|
+
return true;
|
|
262
|
+
if (charge === 'pending')
|
|
263
|
+
return false;
|
|
264
|
+
let quota;
|
|
265
|
+
try {
|
|
266
|
+
quota = await consumeQuota(larkAppId, ev.quotaKey);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
logger.warn(`[quota:${larkAppId}] consume failed; dropping message ${messageId.substring(0, 12)}: ${err}`);
|
|
270
|
+
abortCharge(larkAppId, messageId);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
// 无额度记录(无限授权):放行;标 done 去重后续重投。
|
|
274
|
+
if (!quota.tracked) {
|
|
275
|
+
commitCharge(larkAppId, messageId);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// 已超额:fail-closed drop。**绝不 commit 成 done**(否则同条重投会被 'done' 直接放行,
|
|
279
|
+
// 在 revoke 自愈失败/竞态时绕过硬上限)——abortCharge 让重投重新走扣费判定(仍会被拒,
|
|
280
|
+
// 或在授权已收回时被上面的 evaluateTalk 闸拦掉)。
|
|
281
|
+
if (!quota.allow) {
|
|
282
|
+
abortCharge(larkAppId, messageId);
|
|
283
|
+
await revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev);
|
|
284
|
+
await notifyQuotaExhausted(larkAppId, anchor, senderOpenId, quota.limit);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
// 扣费成功才定论为 done。
|
|
288
|
+
commitCharge(larkAppId, messageId);
|
|
289
|
+
if (quota.exhausted) {
|
|
290
|
+
await revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev);
|
|
291
|
+
await notifyQuotaExhausted(larkAppId, anchor, senderOpenId, quota.limit);
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
export function grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd) {
|
|
296
|
+
return grantCommandRestriction(larkAppId, chatId, senderOpenId).blocked
|
|
297
|
+
? tr('cmd.grant_restricted', { cmd }, localeForBot(larkAppId))
|
|
298
|
+
: undefined;
|
|
299
|
+
}
|
|
300
|
+
export function grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd) {
|
|
301
|
+
if (!/^\/[a-z][a-z0-9_-]*$/.test(cmd))
|
|
302
|
+
return undefined;
|
|
303
|
+
return grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
304
|
+
}
|
|
305
|
+
async function replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, cmd) {
|
|
306
|
+
const text = grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
307
|
+
if (!text)
|
|
308
|
+
return false;
|
|
309
|
+
await sessionReply(anchor, text, 'text', larkAppId);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function forceTopicCommandLabel(content) {
|
|
313
|
+
return /^\/topic(?:\s|$)/i.test(content.trimStart()) ? '/topic' : '/t';
|
|
314
|
+
}
|
|
225
315
|
// ─── PID file ────────────────────────────────────────────────────────────────
|
|
226
316
|
function getPidFile() {
|
|
227
317
|
const botIndex = process.env.BOTMUX_BOT_INDEX;
|
|
@@ -1384,8 +1474,12 @@ async function handleNewTopic(data, ctx) {
|
|
|
1384
1474
|
// (already thread-scope) it's just a prefix strip — no routing change.
|
|
1385
1475
|
// Empty prompt is allowed: the user can fill it in while the repo card is
|
|
1386
1476
|
// pending (pendingFollowUps in handleThreadReply picks up subsequent text).
|
|
1477
|
+
const senderOpenId = data.sender?.sender_id?.open_id;
|
|
1387
1478
|
const forceTopic = parseForceTopicInvocation(cmdContent);
|
|
1388
1479
|
if (forceTopic) {
|
|
1480
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, forceTopicCommandLabel(cmdContent))) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1389
1483
|
if (scope === 'chat') {
|
|
1390
1484
|
scope = 'thread';
|
|
1391
1485
|
anchor = messageId;
|
|
@@ -1395,10 +1489,15 @@ async function handleNewTopic(data, ctx) {
|
|
|
1395
1489
|
cmdContent = forceTopic.prompt;
|
|
1396
1490
|
logger.info(`[/t] Force-topic invocation: prompt="${forceTopic.prompt.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)})`);
|
|
1397
1491
|
}
|
|
1398
|
-
|
|
1492
|
+
// senderOpenId 已在上方(force-topic grant 限制前)声明;这里只补 master 新增的 senderUnionId。
|
|
1399
1493
|
const senderUnionId = data.sender?.sender_id?.union_id;
|
|
1400
1494
|
const botCfg = getBot(larkAppId).config;
|
|
1401
1495
|
logger.info(`New session: "${content.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)}, resources: ${resources.length}, active: ${getActiveCount()}, messageId: ${messageId}, chatId: ${chatId})`);
|
|
1496
|
+
if (parseWorkflowCommand(cmdContent)) {
|
|
1497
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/workflow')) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1402
1501
|
if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
|
|
1403
1502
|
return;
|
|
1404
1503
|
}
|
|
@@ -1406,6 +1505,11 @@ async function handleNewTopic(data, ctx) {
|
|
|
1406
1505
|
const invocation = parseSlashCommandInvocation(cmdContent);
|
|
1407
1506
|
if (invocation) {
|
|
1408
1507
|
const { cmd, content: commandContent } = invocation;
|
|
1508
|
+
const restrictedText = grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
1509
|
+
if (restrictedText) {
|
|
1510
|
+
await sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1409
1513
|
if (PASSTHROUGH_COMMANDS.has(cmd)) {
|
|
1410
1514
|
await sessionReply(anchor, tr('daemon.cmd_requires_session', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
1411
1515
|
return;
|
|
@@ -1479,6 +1583,9 @@ async function handleNewTopic(data, ctx) {
|
|
|
1479
1583
|
return;
|
|
1480
1584
|
}
|
|
1481
1585
|
}
|
|
1586
|
+
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor)) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1482
1589
|
// Download attachments
|
|
1483
1590
|
const { attachments, needLogin } = await downloadResources(larkAppId, messageId, resources);
|
|
1484
1591
|
if (attachments.length > 0) {
|
|
@@ -1874,6 +1981,8 @@ async function handleThreadReply(data, ctx) {
|
|
|
1874
1981
|
const content = parsed.content.trim();
|
|
1875
1982
|
// Strip leading @<bot> mentions so "@bot /restart" is recognized as a command.
|
|
1876
1983
|
const cmdContent = stripLeadingMentions(content, parsed.mentions);
|
|
1984
|
+
const threadSenderOpenId = parsed.senderId || data?.sender?.sender_id?.open_id;
|
|
1985
|
+
const threadChatId = ctxChatId ?? data?.message?.chat_id;
|
|
1877
1986
|
// Intercept OAuth callback URLs (from /login flow)
|
|
1878
1987
|
if (isCallbackUrl(content)) {
|
|
1879
1988
|
const result = await handleCallbackUrl(content);
|
|
@@ -1885,13 +1994,31 @@ async function handleThreadReply(data, ctx) {
|
|
|
1885
1994
|
return;
|
|
1886
1995
|
}
|
|
1887
1996
|
}
|
|
1888
|
-
|
|
1997
|
+
const threadForceTopic = parseForceTopicInvocation(cmdContent);
|
|
1998
|
+
if (threadForceTopic) {
|
|
1999
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, forceTopicCommandLabel(cmdContent))) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (parseWorkflowCommand(cmdContent)) {
|
|
2004
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/workflow')) {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
|
|
1889
2009
|
return;
|
|
1890
2010
|
}
|
|
1891
2011
|
// Intercept daemon commands
|
|
1892
2012
|
const invocation = parseSlashCommandInvocation(cmdContent);
|
|
1893
2013
|
if (invocation) {
|
|
1894
2014
|
const { cmd, content: commandContent } = invocation;
|
|
2015
|
+
const existingDs = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
2016
|
+
const effectiveThreadChatId = existingDs?.chatId ?? threadChatId;
|
|
2017
|
+
const restrictedText = grantRestrictedSlashCommandText(larkAppId, effectiveThreadChatId, threadSenderOpenId, cmd);
|
|
2018
|
+
if (restrictedText) {
|
|
2019
|
+
await sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
1895
2022
|
if (PASSTHROUGH_COMMANDS.has(cmd)) {
|
|
1896
2023
|
// 语义边界(刻意保留,非疏漏):passthrough(/model /clear /compact 等)按
|
|
1897
2024
|
// “发给 CLI 的对话输入”处理,因此不过下面 DAEMON_COMMANDS 的 oncall
|
|
@@ -1900,7 +2027,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
1900
2027
|
// 已存在的 session 发这些命令(清上下文/换模型,需已有活跃 worker,无法凭空
|
|
1901
2028
|
// 拉起)。TODO(后续产品决策):是否把 CLI passthrough 也纳入 canOperate,
|
|
1902
2029
|
// 收紧到与 daemon 命令同档;这会同时改变真人 oncall 成员的现有行为,应单独评估。
|
|
1903
|
-
const ds =
|
|
2030
|
+
const ds = existingDs;
|
|
1904
2031
|
if (ds?.worker && !ds.worker.killed) {
|
|
1905
2032
|
// Mark a new turn so the CLI's response to /model, /clear, /compact, etc.
|
|
1906
2033
|
// shows up as a fresh streaming card instead of silently PATCH-ing the
|
|
@@ -1918,10 +2045,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
1918
2045
|
if (DAEMON_COMMANDS.has(cmd)) {
|
|
1919
2046
|
// canOperate gate for thread-reply daemon commands — required in every chat
|
|
1920
2047
|
// (see spawn-path gate above). Denies chat-granted users management commands.
|
|
1921
|
-
|
|
1922
|
-
const threadChatId = existingDs?.chatId ?? ctxChatId ?? data?.message?.chat_id;
|
|
1923
|
-
const threadSenderOpenId = parsed.senderId || data?.sender?.sender_id?.open_id;
|
|
1924
|
-
if (!canOperate(larkAppId, threadChatId, threadSenderOpenId)) {
|
|
2048
|
+
if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId)) {
|
|
1925
2049
|
sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
1926
2050
|
return;
|
|
1927
2051
|
}
|
|
@@ -1992,6 +2116,10 @@ async function handleThreadReply(data, ctx) {
|
|
|
1992
2116
|
return;
|
|
1993
2117
|
}
|
|
1994
2118
|
}
|
|
2119
|
+
const quotaSenderOpenId = threadSenderOpenId;
|
|
2120
|
+
if (!await enforceMessageQuotaForCliInput(larkAppId, ctxChatId ?? data?.message?.chat_id, quotaSenderOpenId, parsed.messageId, anchor)) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
1995
2123
|
// Download attachments
|
|
1996
2124
|
const effectiveAppId = ds?.larkAppId ?? larkAppId;
|
|
1997
2125
|
const { attachments, needLogin } = await downloadResources(effectiveAppId, parsed.messageId, resources);
|
|
@@ -2534,9 +2662,13 @@ export async function startDaemon(botIndex) {
|
|
|
2534
2662
|
const backendType = ds.larkAppId
|
|
2535
2663
|
? (getBot(ds.larkAppId).config.backendType ?? config.daemon.backendType)
|
|
2536
2664
|
: config.daemon.backendType;
|
|
2537
|
-
if (backendType === 'tmux') {
|
|
2538
|
-
//
|
|
2539
|
-
//
|
|
2665
|
+
if (backendType === 'tmux' || backendType === 'zellij') {
|
|
2666
|
+
// Persistent backends (tmux / zellij): just kill the worker process —
|
|
2667
|
+
// the multiplexer session survives for re-attach. The worker's SIGTERM
|
|
2668
|
+
// handler calls backend.kill(), which only DETACHES. Going through
|
|
2669
|
+
// killWorker() instead would send {type:'close'} → destroySession() →
|
|
2670
|
+
// `zellij delete-session -f`, permanently erasing the session and
|
|
2671
|
+
// breaking daemon-restart reattach (the blocker Codex flagged).
|
|
2540
2672
|
try {
|
|
2541
2673
|
w.kill('SIGTERM');
|
|
2542
2674
|
}
|