botmux 2.9.0 → 2.9.2
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 +140 -76
- package/README.md +134 -75
- package/dist/adapters/backend/pty-backend.d.ts +6 -0
- package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +10 -0
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +11 -0
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -0
- package/dist/adapters/backend/session-backend-selector.js +26 -0
- package/dist/adapters/backend/session-backend-selector.js.map +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts +80 -3
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +301 -49
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +100 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -0
- package/dist/adapters/backend/tmux-pipe-backend.js +473 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -0
- package/dist/adapters/cli/aiden.d.ts.map +1 -1
- package/dist/adapters/cli/aiden.js +5 -0
- package/dist/adapters/cli/aiden.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts +40 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +470 -49
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +191 -9
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +94 -17
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +2 -2
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +7 -5
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +38 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/autostart.d.ts +14 -0
- package/dist/autostart.d.ts.map +1 -0
- package/dist/autostart.js +357 -0
- package/dist/autostart.js.map +1 -0
- package/dist/bot-registry.d.ts +29 -3
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +91 -12
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +11 -0
- package/dist/cli/arg-utils.d.ts.map +1 -0
- package/dist/cli/arg-utils.js +25 -0
- package/dist/cli/arg-utils.js.map +1 -0
- package/dist/cli/create-group-resolver.d.ts +32 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -0
- package/dist/cli/create-group-resolver.js +70 -0
- package/dist/cli/create-group-resolver.js.map +1 -0
- package/dist/cli/quoted-render.d.ts +30 -0
- package/dist/cli/quoted-render.d.ts.map +1 -0
- package/dist/cli/quoted-render.js +29 -0
- package/dist/cli/quoted-render.js.map +1 -0
- package/dist/cli.js +916 -272
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -8
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts +43 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +167 -64
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-events.d.ts +57 -0
- package/dist/core/dashboard-events.d.ts.map +1 -0
- package/dist/core/dashboard-events.js +23 -0
- package/dist/core/dashboard-events.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts +43 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
- package/dist/core/dashboard-ipc-server.js +481 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -0
- package/dist/core/dashboard-locate.d.ts +20 -0
- package/dist/core/dashboard-locate.d.ts.map +1 -0
- package/dist/core/dashboard-locate.js +26 -0
- package/dist/core/dashboard-locate.js.map +1 -0
- package/dist/core/dashboard-rows.d.ts +31 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -0
- package/dist/core/dashboard-rows.js +65 -0
- package/dist/core/dashboard-rows.js.map +1 -0
- package/dist/core/inherit-peer.d.ts +14 -0
- package/dist/core/inherit-peer.d.ts.map +1 -0
- package/dist/core/inherit-peer.js +32 -0
- package/dist/core/inherit-peer.js.map +1 -0
- package/dist/core/scheduler.d.ts +24 -0
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +93 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-activity.d.ts +3 -0
- package/dist/core/session-activity.d.ts.map +1 -0
- package/dist/core/session-activity.js +20 -0
- package/dist/core/session-activity.js.map +1 -0
- package/dist/core/session-discovery.d.ts +39 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +114 -21
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +72 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +396 -106
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +27 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +72 -3
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +459 -38
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +601 -309
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +41 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -0
- package/dist/dashboard/aggregator.js +125 -0
- package/dist/dashboard/aggregator.js.map +1 -0
- package/dist/dashboard/auth.d.ts +23 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +66 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/operator-selector.d.ts +20 -0
- package/dist/dashboard/operator-selector.d.ts.map +1 -0
- package/dist/dashboard/operator-selector.js +39 -0
- package/dist/dashboard/operator-selector.js.map +1 -0
- package/dist/dashboard/registry.d.ts +35 -0
- package/dist/dashboard/registry.d.ts.map +1 -0
- package/dist/dashboard/registry.js +74 -0
- package/dist/dashboard/registry.js.map +1 -0
- package/dist/dashboard/web/app.d.ts +2 -0
- package/dist/dashboard/web/app.d.ts.map +1 -0
- package/dist/dashboard/web/app.js +45 -0
- package/dist/dashboard/web/app.js.map +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts +2 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -0
- package/dist/dashboard/web/bot-defaults.js +201 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -0
- package/dist/dashboard/web/groups.d.ts +16 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -0
- package/dist/dashboard/web/groups.js +584 -0
- package/dist/dashboard/web/groups.js.map +1 -0
- package/dist/dashboard/web/schedules.d.ts +2 -0
- package/dist/dashboard/web/schedules.d.ts.map +1 -0
- package/dist/dashboard/web/schedules.js +105 -0
- package/dist/dashboard/web/schedules.js.map +1 -0
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -0
- package/dist/dashboard/web/sessions.js +374 -0
- package/dist/dashboard/web/sessions.js.map +1 -0
- package/dist/dashboard/web/store.d.ts +23 -0
- package/dist/dashboard/web/store.d.ts.map +1 -0
- package/dist/dashboard/web/store.js +82 -0
- package/dist/dashboard/web/store.js.map +1 -0
- package/dist/dashboard-web/app.js +263 -0
- package/dist/dashboard-web/index.html +23 -0
- package/dist/dashboard-web/style.css +93 -0
- package/dist/dashboard.d.ts +2 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +639 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +18 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +70 -9
- 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 +123 -109
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +36 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +119 -13
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +92 -8
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +410 -89
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/forwarded-renderer.d.ts +23 -0
- package/dist/im/lark/forwarded-renderer.d.ts.map +1 -0
- package/dist/im/lark/forwarded-renderer.js +105 -0
- package/dist/im/lark/forwarded-renderer.js.map +1 -0
- package/dist/im/lark/md-card.d.ts +73 -0
- package/dist/im/lark/md-card.d.ts.map +1 -0
- package/dist/im/lark/md-card.js +332 -0
- package/dist/im/lark/md-card.js.map +1 -0
- package/dist/im/lark/merge-forward.d.ts +32 -0
- package/dist/im/lark/merge-forward.d.ts.map +1 -0
- package/dist/im/lark/merge-forward.js +110 -0
- package/dist/im/lark/merge-forward.js.map +1 -0
- package/dist/im/lark/message-parser.d.ts +9 -3
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +48 -13
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/quote-hint.d.ts +18 -0
- package/dist/im/lark/quote-hint.d.ts.map +1 -0
- package/dist/im/lark/quote-hint.js +23 -0
- package/dist/im/lark/quote-hint.js.map +1 -0
- package/dist/services/bridge-fallback-gate.d.ts +42 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -0
- package/dist/services/bridge-fallback-gate.js +12 -0
- package/dist/services/bridge-fallback-gate.js.map +1 -0
- package/dist/services/bridge-rotation-policy.d.ts +139 -0
- package/dist/services/bridge-rotation-policy.d.ts.map +1 -0
- package/dist/services/bridge-rotation-policy.js +125 -0
- package/dist/services/bridge-rotation-policy.js.map +1 -0
- package/dist/services/bridge-turn-queue.d.ts +154 -0
- package/dist/services/bridge-turn-queue.d.ts.map +1 -0
- package/dist/services/bridge-turn-queue.js +316 -0
- package/dist/services/bridge-turn-queue.js.map +1 -0
- package/dist/services/chat-first-seen-store.d.ts +27 -0
- package/dist/services/chat-first-seen-store.d.ts.map +1 -0
- package/dist/services/chat-first-seen-store.js +114 -0
- package/dist/services/chat-first-seen-store.js.map +1 -0
- package/dist/services/claude-transcript.d.ts +268 -0
- package/dist/services/claude-transcript.d.ts.map +1 -0
- package/dist/services/claude-transcript.js +798 -0
- package/dist/services/claude-transcript.js.map +1 -0
- package/dist/services/coco-transcript.d.ts +35 -0
- package/dist/services/coco-transcript.d.ts.map +1 -0
- package/dist/services/coco-transcript.js +192 -0
- package/dist/services/coco-transcript.js.map +1 -0
- package/dist/services/codex-bridge-queue.d.ts +56 -0
- package/dist/services/codex-bridge-queue.d.ts.map +1 -0
- package/dist/services/codex-bridge-queue.js +150 -0
- package/dist/services/codex-bridge-queue.js.map +1 -0
- package/dist/services/codex-transcript.d.ts +84 -0
- package/dist/services/codex-transcript.d.ts.map +1 -0
- package/dist/services/codex-transcript.js +298 -0
- package/dist/services/codex-transcript.js.map +1 -0
- package/dist/services/group-creator.d.ts +23 -0
- package/dist/services/group-creator.d.ts.map +1 -0
- package/dist/services/group-creator.js +75 -0
- package/dist/services/group-creator.js.map +1 -0
- package/dist/services/groups-store.d.ts +98 -0
- package/dist/services/groups-store.d.ts.map +1 -0
- package/dist/services/groups-store.js +213 -0
- package/dist/services/groups-store.js.map +1 -0
- package/dist/services/oncall-store.d.ts +80 -8
- package/dist/services/oncall-store.d.ts.map +1 -1
- package/dist/services/oncall-store.js +265 -55
- package/dist/services/oncall-store.js.map +1 -1
- package/dist/services/project-scanner.d.ts +1 -2
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +118 -68
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/schedule-store.d.ts +5 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +77 -1
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/session-store.d.ts +22 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +62 -4
- package/dist/services/session-store.js.map +1 -1
- package/dist/setup/bots-store.d.ts +3 -0
- package/dist/setup/bots-store.d.ts.map +1 -0
- package/dist/setup/bots-store.js +24 -0
- package/dist/setup/bots-store.js.map +1 -0
- package/dist/setup/detect-platform.d.ts +14 -0
- package/dist/setup/detect-platform.d.ts.map +1 -0
- package/dist/setup/detect-platform.js +139 -0
- package/dist/setup/detect-platform.js.map +1 -0
- package/dist/setup/ensure-fonts.d.ts +13 -0
- package/dist/setup/ensure-fonts.d.ts.map +1 -0
- package/dist/setup/ensure-fonts.js +225 -0
- package/dist/setup/ensure-fonts.js.map +1 -0
- package/dist/setup/ensure-tmux.d.ts +60 -0
- package/dist/setup/ensure-tmux.d.ts.map +1 -0
- package/dist/setup/ensure-tmux.js +236 -0
- package/dist/setup/ensure-tmux.js.map +1 -0
- package/dist/setup/index.d.ts +9 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +46 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/lark-scopes.json +301 -0
- package/dist/setup/register-app.d.ts +52 -0
- package/dist/setup/register-app.d.ts.map +1 -0
- package/dist/setup/register-app.js +91 -0
- package/dist/setup/register-app.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts +115 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -0
- package/dist/setup/verify-permissions.js +207 -0
- package/dist/setup/verify-permissions.js.map +1 -0
- package/dist/skills/definitions.d.ts +4 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +133 -19
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/installer.d.ts +3 -1
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +18 -3
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +6 -0
- package/dist/utils/bot-routing.d.ts.map +1 -0
- package/dist/utils/bot-routing.js +50 -0
- package/dist/utils/bot-routing.js.map +1 -0
- package/dist/utils/file-lock.d.ts +2 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +114 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/font-installer.js +1 -1
- package/dist/utils/font-installer.js.map +1 -1
- package/dist/utils/idle-detector.d.ts +6 -0
- package/dist/utils/idle-detector.d.ts.map +1 -1
- package/dist/utils/idle-detector.js +25 -4
- package/dist/utils/idle-detector.js.map +1 -1
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +60 -8
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/render-dimensions.d.ts +48 -0
- package/dist/utils/render-dimensions.d.ts.map +1 -0
- package/dist/utils/render-dimensions.js +55 -0
- package/dist/utils/render-dimensions.js.map +1 -0
- package/dist/utils/screen-analyzer.d.ts.map +1 -1
- package/dist/utils/screen-analyzer.js +24 -0
- package/dist/utils/screen-analyzer.js.map +1 -1
- package/dist/utils/screenshot-renderer.d.ts.map +1 -1
- package/dist/utils/screenshot-renderer.js +67 -23
- package/dist/utils/screenshot-renderer.js.map +1 -1
- package/dist/utils/terminal-renderer.d.ts +16 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +40 -23
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/utils/transient-snapshot.d.ts +28 -0
- package/dist/utils/transient-snapshot.d.ts.map +1 -0
- package/dist/utils/transient-snapshot.js +96 -0
- package/dist/utils/transient-snapshot.js.map +1 -0
- package/dist/worker.js +2220 -83
- package/dist/worker.js.map +1 -1
- package/package.json +12 -5
|
@@ -10,7 +10,14 @@ import { createCliAdapterSync } from '../adapters/cli/registry.js';
|
|
|
10
10
|
import { TmuxBackend } from '../adapters/backend/tmux-backend.js';
|
|
11
11
|
import { getBot, getAllBots } from '../bot-registry.js';
|
|
12
12
|
import { validateAdoptTarget } from './session-discovery.js';
|
|
13
|
-
import { sessionKey } from './types.js';
|
|
13
|
+
import { sessionKey, sessionAnchorId } from './types.js';
|
|
14
|
+
import { markSessionActivity } from './session-activity.js';
|
|
15
|
+
function sessionCreatedAtMs(session) {
|
|
16
|
+
return session.createdAt ? (Date.parse(session.createdAt) || Date.now()) : Date.now();
|
|
17
|
+
}
|
|
18
|
+
function sessionLastMessageAtMs(session) {
|
|
19
|
+
return session.lastMessageAt ? (Date.parse(session.lastMessageAt) || sessionCreatedAtMs(session)) : sessionCreatedAtMs(session);
|
|
20
|
+
}
|
|
14
21
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
15
22
|
export function expandHome(p) {
|
|
16
23
|
return p.startsWith('~') ? join(homedir(), p.slice(1)) : p;
|
|
@@ -80,7 +87,10 @@ export async function downloadResources(larkAppId, messageId, resources) {
|
|
|
80
87
|
attachments.push({ type: res.type, path: savePath, name: res.name });
|
|
81
88
|
}
|
|
82
89
|
catch (err) {
|
|
83
|
-
|
|
90
|
+
// Download failure usually means missing User Token scope or a
|
|
91
|
+
// legitimately revoked attachment — the caller surfaces `needLogin`
|
|
92
|
+
// to the user. Per-failure log stays at info to aid retries.
|
|
93
|
+
logger.info(`Failed to download ${res.type} ${res.key}: ${err.message}`);
|
|
84
94
|
if (err.message?.includes('User Token'))
|
|
85
95
|
needLogin = true;
|
|
86
96
|
}
|
|
@@ -109,74 +119,85 @@ export async function getAvailableBots(currentAppId, chatId) {
|
|
|
109
119
|
return [];
|
|
110
120
|
}
|
|
111
121
|
}
|
|
122
|
+
/** XML-escape a string for use as element text content or attribute value.
|
|
123
|
+
* Covers the five XML-mandated entities; sufficient for our use case
|
|
124
|
+
* (paths, names, open_ids, bot identifiers) since we never embed raw user
|
|
125
|
+
* input in attribute values. */
|
|
126
|
+
function xmlEscape(s) {
|
|
127
|
+
return s
|
|
128
|
+
.replace(/&/g, '&')
|
|
129
|
+
.replace(/</g, '<')
|
|
130
|
+
.replace(/>/g, '>')
|
|
131
|
+
.replace(/"/g, '"')
|
|
132
|
+
.replace(/'/g, ''');
|
|
133
|
+
}
|
|
112
134
|
export function formatAttachmentsHint(attachments) {
|
|
113
135
|
if (!attachments || attachments.length === 0)
|
|
114
136
|
return '';
|
|
115
137
|
let imgN = 0, fileN = 0;
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
138
|
+
const items = attachments.map(a => {
|
|
139
|
+
const tag = a.type === 'image' ? 'image' : 'file';
|
|
140
|
+
const n = a.type === 'image' ? ++imgN : ++fileN;
|
|
141
|
+
return ` <${tag} n="${n}" path="${xmlEscape(a.path)}" />`;
|
|
119
142
|
});
|
|
120
|
-
return
|
|
143
|
+
return `<attachments hint="使用 Read 工具查看,序号与正文中的 [图片 N] / [文件 N] 占位符对应">\n${items.join('\n')}\n</attachments>`;
|
|
121
144
|
}
|
|
122
145
|
export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverride, attachments, mentions, availableBots, followUps, botIdentity) {
|
|
123
146
|
const adapter = createCliAdapterSync(cliId, cliPathOverride);
|
|
124
147
|
const hints = adapter.systemHints;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
let
|
|
148
|
+
const routingBlock = hints.length > 0
|
|
149
|
+
? `<botmux_routing>\n${hints.join('\n')}\n</botmux_routing>`
|
|
150
|
+
: '';
|
|
151
|
+
let identityBlock = '';
|
|
129
152
|
if (botIdentity && (botIdentity.name || botIdentity.openId)) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
'
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
identityBlock = [
|
|
154
|
+
'<identity>',
|
|
155
|
+
` <name>${xmlEscape(botIdentity.name ?? '(未知)')}</name>`,
|
|
156
|
+
` <open_id>${xmlEscape(botIdentity.openId ?? '(未知)')}</open_id>`,
|
|
157
|
+
' <routing_rules>提醒:让别的 bot 接力干活必须 `botmux send --mention <对方 open_id>`,否则对方 bot 不会被触发。</routing_rules>',
|
|
158
|
+
'</identity>',
|
|
159
|
+
].join('\n');
|
|
137
160
|
}
|
|
138
|
-
|
|
139
|
-
let mentionSection = '';
|
|
161
|
+
let mentionBlock = '';
|
|
140
162
|
if (mentions && mentions.length > 0) {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
return
|
|
163
|
+
const items = mentions.map(m => {
|
|
164
|
+
const oid = m.openId ? ` open_id="${xmlEscape(m.openId)}"` : '';
|
|
165
|
+
return ` <mention name="${xmlEscape(m.name)}"${oid} />`;
|
|
144
166
|
});
|
|
145
|
-
|
|
167
|
+
mentionBlock = `<mentions>\n${items.join('\n')}\n</mentions>`;
|
|
146
168
|
}
|
|
147
|
-
|
|
148
|
-
let botSection = '';
|
|
169
|
+
let botBlock = '';
|
|
149
170
|
if (availableBots && availableBots.length > 0) {
|
|
150
171
|
const mentionedOpenIds = new Set(mentions?.map(m => m.openId).filter(Boolean));
|
|
151
172
|
const unmentionedBots = availableBots.filter(b => !mentionedOpenIds.has(b.openId));
|
|
152
173
|
if (unmentionedBots.length > 0) {
|
|
153
|
-
const
|
|
154
|
-
|
|
174
|
+
const items = unmentionedBots.map(b => ` <bot name="${xmlEscape(b.displayName)}" open_id="${xmlEscape(b.openId)}" />`);
|
|
175
|
+
botBlock = `<available_bots hint="让这里的某个 bot 接力干活必须 --mention 它的 open_id(botmux send --mention ou_xxx ...),不 --mention 对方 bot 完全收不到消息">\n${items.join('\n')}\n</available_bots>`;
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const attachHint = formatAttachmentsHint(attachments);
|
|
160
|
-
const parts = adapter.injectsSessionContext
|
|
161
|
-
? [`${userMessage}${attachHint}`]
|
|
162
|
-
: [`用户发送了:\n---\n${userMessage}${attachHint}\n---`];
|
|
163
|
-
// Append follow-up messages buffered during repo selection
|
|
178
|
+
const userBlock = `<user_message>\n${userMessage}\n</user_message>`;
|
|
179
|
+
const parts = [userBlock];
|
|
164
180
|
if (followUps && followUps.length > 0) {
|
|
165
181
|
for (const fu of followUps) {
|
|
166
|
-
parts.push(
|
|
182
|
+
parts.push(`<follow_up_message>\n${fu}\n</follow_up_message>`);
|
|
167
183
|
}
|
|
168
184
|
}
|
|
185
|
+
const attachHint = formatAttachmentsHint(attachments);
|
|
186
|
+
if (attachHint)
|
|
187
|
+
parts.push(attachHint);
|
|
188
|
+
// CLIs with injectsSessionContext (Claude Code) get Lark routing/identity
|
|
189
|
+
// and session ID via system prompt, so skip those blocks here.
|
|
169
190
|
if (!adapter.injectsSessionContext) {
|
|
170
|
-
parts.push(
|
|
191
|
+
parts.push(`<session_id>${xmlEscape(sessionId)}</session_id>`);
|
|
192
|
+
if (routingBlock)
|
|
193
|
+
parts.push(routingBlock);
|
|
194
|
+
if (identityBlock)
|
|
195
|
+
parts.push(identityBlock);
|
|
171
196
|
}
|
|
172
|
-
if (
|
|
173
|
-
parts.push(
|
|
174
|
-
if (
|
|
175
|
-
parts.push(
|
|
176
|
-
if (mentionSection)
|
|
177
|
-
parts.push(mentionSection.trim());
|
|
178
|
-
if (botSection)
|
|
179
|
-
parts.push(botSection.trim());
|
|
197
|
+
if (mentionBlock)
|
|
198
|
+
parts.push(mentionBlock);
|
|
199
|
+
if (botBlock)
|
|
200
|
+
parts.push(botBlock);
|
|
180
201
|
return parts.join('\n\n');
|
|
181
202
|
}
|
|
182
203
|
/**
|
|
@@ -185,41 +206,141 @@ export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverri
|
|
|
185
206
|
* Session ID is omitted for adopt mode and CLIs with injectsSessionContext.
|
|
186
207
|
*/
|
|
187
208
|
export function buildFollowUpContent(content, sessionId, opts) {
|
|
188
|
-
const parts = [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
const parts = [`<user_message>\n${content}\n</user_message>`];
|
|
210
|
+
const attachHint = opts?.attachments && opts.attachments.length > 0
|
|
211
|
+
? formatAttachmentsHint(opts.attachments)
|
|
212
|
+
: '';
|
|
213
|
+
if (attachHint)
|
|
214
|
+
parts.push(attachHint);
|
|
193
215
|
if (!opts?.isAdoptMode) {
|
|
194
|
-
// CLIs with injectsSessionContext get session ID via system prompt +
|
|
216
|
+
// CLIs with injectsSessionContext get session ID via system prompt + ancestor-pid auto-detection
|
|
195
217
|
const skipSessionId = opts?.cliId
|
|
196
218
|
? createCliAdapterSync(opts.cliId, opts.cliPathOverride).injectsSessionContext
|
|
197
219
|
: false;
|
|
198
220
|
if (!skipSessionId) {
|
|
199
|
-
parts.push(
|
|
221
|
+
parts.push(`<session_id>${xmlEscape(sessionId)}</session_id>`);
|
|
200
222
|
}
|
|
201
223
|
}
|
|
202
224
|
if (opts?.mentions && opts.mentions.length > 0) {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
return
|
|
225
|
+
const items = opts.mentions.map(m => {
|
|
226
|
+
const oid = m.openId ? ` open_id="${xmlEscape(m.openId)}"` : '';
|
|
227
|
+
return ` <mention name="${xmlEscape(m.name)}"${oid} />`;
|
|
206
228
|
});
|
|
207
|
-
parts.push(
|
|
208
|
-
}
|
|
209
|
-
// Per-message routing hint — only for CLIs without system prompt context.
|
|
210
|
-
// CLIs with injectsSessionContext (e.g. Claude Code) already have the
|
|
211
|
-
// "use botmux send" instruction in --append-system-prompt, no need to repeat.
|
|
212
|
-
const skipHint = opts?.cliId
|
|
213
|
-
? createCliAdapterSync(opts.cliId, opts.cliPathOverride).injectsSessionContext
|
|
214
|
-
: false;
|
|
215
|
-
if (!skipHint) {
|
|
216
|
-
parts.push('[请用 botmux send "消息" - 这个shell工具回复飞书用户,终端输出用户看不到]');
|
|
229
|
+
parts.push(`<mentions>\n${items.join('\n')}\n</mentions>`);
|
|
217
230
|
}
|
|
231
|
+
// Per-message routing hint — system prompt routing block can fade in long
|
|
232
|
+
// sessions, so re-state the core "use botmux send" rule at the tail of every
|
|
233
|
+
// follow-up regardless of CLI.
|
|
234
|
+
parts.push('<botmux_reminder>回复必须 botmux send,终端输出用户看不到</botmux_reminder>');
|
|
218
235
|
return parts.join('\n\n');
|
|
219
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Build raw input content for adopt-bridge mode.
|
|
239
|
+
*
|
|
240
|
+
* Bridge mode injects the user's text into the existing CLI exactly as the
|
|
241
|
+
* local user would type it: NO `<session_id>`, NO `<botmux_reminder>`, NO
|
|
242
|
+
* Skills hint. The model is intentionally unaware of botmux — the daemon
|
|
243
|
+
* harvests final output via the transcript watcher and forwards it to Lark
|
|
244
|
+
* out-of-band.
|
|
245
|
+
*
|
|
246
|
+
* Attachments and @mentions are surfaced as plain prose so the user's intent
|
|
247
|
+
* carries over, but the format avoids any wording that would prompt the
|
|
248
|
+
* model to call `botmux send` / route through botmux tooling.
|
|
249
|
+
*/
|
|
250
|
+
export function buildBridgeInputContent(content, opts) {
|
|
251
|
+
const selfMention = opts?.selfMention;
|
|
252
|
+
const selfNames = new Set();
|
|
253
|
+
if (selfMention?.name)
|
|
254
|
+
selfNames.add(selfMention.name);
|
|
255
|
+
for (const m of opts?.mentions ?? []) {
|
|
256
|
+
if (selfMention?.openId && m.openId === selfMention.openId)
|
|
257
|
+
selfNames.add(m.name);
|
|
258
|
+
if (selfMention?.name && m.name === selfMention.name)
|
|
259
|
+
selfNames.add(m.name);
|
|
260
|
+
}
|
|
261
|
+
const isSelfMention = (m) => {
|
|
262
|
+
// openId is authoritative when both sides have it — avoids classifying
|
|
263
|
+
// a different bot as self in the (theoretical) case where two bots in
|
|
264
|
+
// the same chat share a display name.
|
|
265
|
+
if (selfMention?.openId && m.openId) {
|
|
266
|
+
return m.openId === selfMention.openId;
|
|
267
|
+
}
|
|
268
|
+
// At least one side is missing openId (cold-start window before
|
|
269
|
+
// probeBotOpenId returns, or a mention without openId): fall back to
|
|
270
|
+
// name match.
|
|
271
|
+
return !!selfMention?.name && selfNames.has(m.name);
|
|
272
|
+
};
|
|
273
|
+
const stripLeadingSelfMentions = (s) => {
|
|
274
|
+
if (selfNames.size === 0)
|
|
275
|
+
return s;
|
|
276
|
+
let out = s.trimStart();
|
|
277
|
+
const tags = [...selfNames]
|
|
278
|
+
.sort((a, b) => b.length - a.length)
|
|
279
|
+
.map(name => `@${name}`);
|
|
280
|
+
let changed = true;
|
|
281
|
+
while (changed) {
|
|
282
|
+
changed = false;
|
|
283
|
+
for (const tag of tags) {
|
|
284
|
+
if (!out.startsWith(tag))
|
|
285
|
+
continue;
|
|
286
|
+
const next = out.charAt(tag.length);
|
|
287
|
+
// Avoid stripping prefixes like "@CodexFoo" when the bot name is
|
|
288
|
+
// "Codex"; Lark-rendered mentions are followed by whitespace or EOL.
|
|
289
|
+
if (next && !/\s/.test(next))
|
|
290
|
+
continue;
|
|
291
|
+
out = out.slice(tag.length).trimStart();
|
|
292
|
+
changed = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
};
|
|
298
|
+
const parts = [stripLeadingSelfMentions(content)];
|
|
299
|
+
if (opts?.attachments && opts.attachments.length > 0) {
|
|
300
|
+
const lines = opts.attachments.map(a => `- ${a.name} (${a.path})`);
|
|
301
|
+
parts.push(`\n[附件]\n${lines.join('\n')}`);
|
|
302
|
+
}
|
|
303
|
+
const mentions = opts?.mentions?.filter(m => !isSelfMention(m)) ?? [];
|
|
304
|
+
if (mentions.length > 0) {
|
|
305
|
+
const lines = mentions.map(m => `- @${m.name}`);
|
|
306
|
+
parts.push(`\n[@提及]\n${lines.join('\n')}`);
|
|
307
|
+
}
|
|
308
|
+
return parts.join('\n');
|
|
309
|
+
}
|
|
220
310
|
// ─── Stream-card state persistence ───────────────────────────────────────────
|
|
221
311
|
/** Sentinel value (CARD_POSTING_SENTINEL from worker-pool) we must skip — it marks an in-flight POST, not a real message_id. */
|
|
222
312
|
const STREAM_CARD_SENTINEL = '__posting__';
|
|
313
|
+
/**
|
|
314
|
+
* Build the prompt that gets piped into a freshly-spawned CLI when an existing
|
|
315
|
+
* (non-bridge) session re-forks its worker. Hits the `worker=null` re-fork
|
|
316
|
+
* branch in handleThreadReply: resume after /close, daemon-restart + new
|
|
317
|
+
* message, and any other path that lands a new turn without a live worker.
|
|
318
|
+
*
|
|
319
|
+
* Without wrapping, the worker would queue the user's raw text as the initial
|
|
320
|
+
* prompt — the CLI sees no `<user_message>` / `<botmux_reminder>` envelope
|
|
321
|
+
* and answers in its own terminal instead of calling `botmux send`. This
|
|
322
|
+
* helper centralises the wrap so both daemon.ts and tests agree on the shape.
|
|
323
|
+
*
|
|
324
|
+
* Adopt-bridge sessions go through `buildBridgeInputContent` instead — see
|
|
325
|
+
* the buildBridgeInputContent docstring for why bridge prompts intentionally
|
|
326
|
+
* skip botmux routing tags.
|
|
327
|
+
*/
|
|
328
|
+
export function buildReforkPrompt(ds, content, opts) {
|
|
329
|
+
if (ds.adoptedFrom) {
|
|
330
|
+
return buildBridgeInputContent(content, {
|
|
331
|
+
attachments: opts?.attachments,
|
|
332
|
+
mentions: opts?.mentions,
|
|
333
|
+
selfMention: opts?.selfMention,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return buildFollowUpContent(content, ds.session.sessionId, {
|
|
337
|
+
attachments: opts?.attachments,
|
|
338
|
+
mentions: opts?.mentions,
|
|
339
|
+
isAdoptMode: false,
|
|
340
|
+
cliId: opts?.cliId,
|
|
341
|
+
cliPathOverride: opts?.cliPathOverride,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
223
344
|
/**
|
|
224
345
|
* Copy current streaming-card fields from `ds` into the persisted Session and save.
|
|
225
346
|
* Lets the existing card be PATCHed on next screen_update after a daemon restart,
|
|
@@ -256,6 +377,9 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
256
377
|
killStalePids(active);
|
|
257
378
|
logger.info(`Registering ${active.length} active session(s) (no CLI spawn until new messages arrive)...`);
|
|
258
379
|
for (const session of active) {
|
|
380
|
+
// Restored sessions persisted before the scope field was added default to
|
|
381
|
+
// 'thread' — that matches the legacy thread-only behaviour.
|
|
382
|
+
const scope = session.scope === 'chat' ? 'chat' : 'thread';
|
|
259
383
|
// Adopt sessions: restore if original CLI is still alive, otherwise close
|
|
260
384
|
if (session.title?.startsWith('Adopt:') && session.adoptedFrom) {
|
|
261
385
|
const adopted = session.adoptedFrom;
|
|
@@ -265,7 +389,6 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
265
389
|
continue;
|
|
266
390
|
}
|
|
267
391
|
// Original CLI still alive — re-register and fork adopt worker
|
|
268
|
-
messageQueue.ensureQueue(session.rootMessageId);
|
|
269
392
|
const larkAppId = session.larkAppId ?? getAllBots()[0]?.config.larkAppId ?? '';
|
|
270
393
|
const ds = {
|
|
271
394
|
session,
|
|
@@ -275,9 +398,10 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
275
398
|
larkAppId,
|
|
276
399
|
chatId: session.chatId,
|
|
277
400
|
chatType: session.chatType ?? 'group',
|
|
278
|
-
|
|
401
|
+
scope,
|
|
402
|
+
spawnedAt: sessionCreatedAtMs(session),
|
|
279
403
|
cliVersion: getCurrentCliVersion(),
|
|
280
|
-
lastMessageAt:
|
|
404
|
+
lastMessageAt: sessionLastMessageAtMs(session),
|
|
281
405
|
hasHistory: false,
|
|
282
406
|
workingDir: adopted.cwd,
|
|
283
407
|
adoptedFrom: adopted,
|
|
@@ -289,9 +413,11 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
289
413
|
currentImageKey: session.currentImageKey,
|
|
290
414
|
currentTurnTitle: session.currentTurnTitle,
|
|
291
415
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
416
|
+
const anchor = sessionAnchorId(ds);
|
|
417
|
+
messageQueue.ensureQueue(anchor);
|
|
418
|
+
activeSessions.set(sessionKey(anchor, larkAppId), ds);
|
|
419
|
+
forkAdoptWorker(ds, { restoredFromMetadata: true });
|
|
420
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] Restored adopt session (target: ${adopted.tmuxTarget}, scope: ${scope})`);
|
|
295
421
|
continue;
|
|
296
422
|
}
|
|
297
423
|
// Adopt sessions without persisted metadata — close (legacy)
|
|
@@ -300,9 +426,8 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
300
426
|
sessionStore.closeSession(session.sessionId);
|
|
301
427
|
continue;
|
|
302
428
|
}
|
|
303
|
-
messageQueue.ensureQueue(session.rootMessageId);
|
|
304
429
|
const larkAppId = session.larkAppId ?? getAllBots()[0]?.config.larkAppId ?? '';
|
|
305
|
-
|
|
430
|
+
const ds = {
|
|
306
431
|
session,
|
|
307
432
|
worker: null,
|
|
308
433
|
workerPort: null,
|
|
@@ -310,9 +435,10 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
310
435
|
larkAppId,
|
|
311
436
|
chatId: session.chatId,
|
|
312
437
|
chatType: session.chatType ?? 'group',
|
|
313
|
-
|
|
438
|
+
scope,
|
|
439
|
+
spawnedAt: sessionCreatedAtMs(session),
|
|
314
440
|
cliVersion: getCurrentCliVersion(),
|
|
315
|
-
lastMessageAt:
|
|
441
|
+
lastMessageAt: sessionLastMessageAtMs(session),
|
|
316
442
|
hasHistory: true, // restored sessions have prior CLI history
|
|
317
443
|
workingDir: session.workingDir,
|
|
318
444
|
// Restore persisted streaming-card state — next screen_update will PATCH
|
|
@@ -325,8 +451,11 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
325
451
|
displayMode: session.displayMode ?? (session.streamExpanded ? 'screenshot' : 'hidden'),
|
|
326
452
|
currentImageKey: session.currentImageKey,
|
|
327
453
|
currentTurnTitle: session.currentTurnTitle,
|
|
328
|
-
}
|
|
329
|
-
|
|
454
|
+
};
|
|
455
|
+
const anchor = sessionAnchorId(ds);
|
|
456
|
+
messageQueue.ensureQueue(anchor);
|
|
457
|
+
activeSessions.set(sessionKey(anchor, larkAppId), ds);
|
|
458
|
+
logger.debug(`Registered session ${session.sessionId} (scope: ${scope}, anchor: ${anchor})`);
|
|
330
459
|
}
|
|
331
460
|
// Tmux mode: auto-fork workers for sessions with surviving tmux sessions
|
|
332
461
|
if (config.daemon.backendType === 'tmux') {
|
|
@@ -340,46 +469,199 @@ export function restoreActiveSessions(activeSessions) {
|
|
|
340
469
|
}
|
|
341
470
|
logger.info(`Restored ${active.length} session(s)${config.daemon.backendType === 'tmux' ? '' : ', waiting for messages to resume'}`);
|
|
342
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Reactivate a single closed session — used by the "▶️ 恢复会话" card button
|
|
474
|
+
* and the `botmux resume <id>` CLI command. Mirrors the per-session branch
|
|
475
|
+
* of `restoreActiveSessions` but operates on one record by id and without
|
|
476
|
+
* killing stale pids (the `/close` flow that produced this closed record
|
|
477
|
+
* already killed them).
|
|
478
|
+
*
|
|
479
|
+
* Returns `{ ok: true, ds }` on success; structured error otherwise so callers
|
|
480
|
+
* (HTTP IPC, card handler) can surface a precise message.
|
|
481
|
+
*
|
|
482
|
+
* - 'not_found' — sessionId doesn't exist in any session file
|
|
483
|
+
* - 'not_closed' — session is still active or in some other state
|
|
484
|
+
* - 'anchor_occupied' — another active session already owns this anchor
|
|
485
|
+
* (e.g. user kept typing after /close, auto-creating
|
|
486
|
+
* a fresh thread session); refuse rather than clobber
|
|
487
|
+
* - 'adopt_unsupported' — adopt sessions are torn down by /close and have
|
|
488
|
+
* no resume semantics
|
|
489
|
+
*/
|
|
490
|
+
export function resumeSession(sessionId, activeSessions) {
|
|
491
|
+
const session = sessionStore.getSession(sessionId);
|
|
492
|
+
if (!session)
|
|
493
|
+
return { ok: false, error: 'not_found' };
|
|
494
|
+
if (session.status !== 'closed')
|
|
495
|
+
return { ok: false, error: 'not_closed' };
|
|
496
|
+
// Adopt sessions don't survive /close — the user's tmux pane and original
|
|
497
|
+
// CLI pid have already moved on, and bringing the bridge back without a live
|
|
498
|
+
// pane is meaningless.
|
|
499
|
+
if (session.title?.startsWith('Adopt:') || session.adoptedFrom) {
|
|
500
|
+
return { ok: false, error: 'adopt_unsupported' };
|
|
501
|
+
}
|
|
502
|
+
const scope = session.scope === 'chat' ? 'chat' : 'thread';
|
|
503
|
+
const larkAppId = session.larkAppId ?? getAllBots()[0]?.config.larkAppId ?? '';
|
|
504
|
+
const anchor = scope === 'thread' ? session.rootMessageId : session.chatId;
|
|
505
|
+
const key = sessionKey(anchor, larkAppId);
|
|
506
|
+
const existing = activeSessions.get(key);
|
|
507
|
+
if (existing) {
|
|
508
|
+
return { ok: false, error: 'anchor_occupied', activeSessionId: existing.session.sessionId };
|
|
509
|
+
}
|
|
510
|
+
// Belt-and-suspenders: also scan persisted sessions for any *other* active
|
|
511
|
+
// session pinned to the same (larkAppId, scope, anchor). The in-memory Map
|
|
512
|
+
// is the authoritative routing source for a running daemon, but it's only
|
|
513
|
+
// hydrated for sessions that survived restoreActiveSessions. Cross-process
|
|
514
|
+
// and partial-load situations (e.g. another bot's daemon writes a session
|
|
515
|
+
// file but our Map hasn't caught up, or a closed session was orphaned by a
|
|
516
|
+
// crash that left a sibling session active in the same anchor) can leave a
|
|
517
|
+
// store-level conflict invisible to the Map check above. Refuse instead of
|
|
518
|
+
// overwriting the routing key.
|
|
519
|
+
const conflict = sessionStore.listSessions().find(s => s.sessionId !== sessionId
|
|
520
|
+
&& s.status === 'active'
|
|
521
|
+
&& (s.larkAppId ?? '') === larkAppId
|
|
522
|
+
&& (s.scope === 'chat' ? 'chat' : 'thread') === scope
|
|
523
|
+
&& (scope === 'thread' ? s.rootMessageId === anchor : s.chatId === anchor));
|
|
524
|
+
if (conflict) {
|
|
525
|
+
return { ok: false, error: 'anchor_occupied', activeSessionId: conflict.sessionId };
|
|
526
|
+
}
|
|
527
|
+
// Reactivate in store — clear closedAt so dashboard rows don't keep showing
|
|
528
|
+
// the stale close timestamp on the now-active session.
|
|
529
|
+
session.status = 'active';
|
|
530
|
+
session.closedAt = undefined;
|
|
531
|
+
session.lastMessageAt = new Date().toISOString();
|
|
532
|
+
sessionStore.updateSession(session);
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
const ds = {
|
|
535
|
+
session,
|
|
536
|
+
worker: null,
|
|
537
|
+
workerPort: null,
|
|
538
|
+
workerToken: null,
|
|
539
|
+
larkAppId,
|
|
540
|
+
chatId: session.chatId,
|
|
541
|
+
chatType: session.chatType ?? 'group',
|
|
542
|
+
scope,
|
|
543
|
+
spawnedAt: sessionCreatedAtMs(session),
|
|
544
|
+
cliVersion: getCurrentCliVersion(),
|
|
545
|
+
lastMessageAt: now,
|
|
546
|
+
hasHistory: true, // resumed sessions carry CLI history (--resume on next fork)
|
|
547
|
+
workingDir: session.workingDir,
|
|
548
|
+
ownerOpenId: session.ownerOpenId,
|
|
549
|
+
streamCardId: session.streamCardId,
|
|
550
|
+
streamCardNonce: session.streamCardNonce,
|
|
551
|
+
displayMode: session.displayMode ?? (session.streamExpanded ? 'screenshot' : 'hidden'),
|
|
552
|
+
currentImageKey: session.currentImageKey,
|
|
553
|
+
currentTurnTitle: session.currentTurnTitle,
|
|
554
|
+
};
|
|
555
|
+
messageQueue.ensureQueue(anchor);
|
|
556
|
+
activeSessions.set(key, ds);
|
|
557
|
+
logger.info(`Resumed session ${sessionId.substring(0, 8)} (scope: ${scope}, anchor: ${anchor.substring(0, 12)})`);
|
|
558
|
+
return { ok: true, ds };
|
|
559
|
+
}
|
|
343
560
|
// ─── Scheduled task execution ────────────────────────────────────────────────
|
|
344
561
|
export async function executeScheduledTask(task, activeSessions, refreshCliVersion) {
|
|
345
562
|
// Resolve which bot to use — prefer the task's original bot so replies come from
|
|
346
563
|
// the same account the user set up the schedule with.
|
|
347
564
|
const allBots = getAllBots();
|
|
348
565
|
if (allBots.length === 0) {
|
|
349
|
-
|
|
566
|
+
// Expected at startup before bot configs finish loading; scheduler will
|
|
567
|
+
// re-fire on the next cron tick. Not actionable.
|
|
568
|
+
logger.debug('No bots configured, skipping scheduled task');
|
|
350
569
|
return;
|
|
351
570
|
}
|
|
352
571
|
const bot = (task.larkAppId && allBots.find(b => b.config.larkAppId === task.larkAppId)) ||
|
|
353
572
|
allBots[0];
|
|
354
573
|
const larkAppId = bot.config.larkAppId;
|
|
355
|
-
const { sendMessage, replyMessage } = await import('../im/lark/client.js');
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
574
|
+
const { getChatMode, sendMessage, replyMessage } = await import('../im/lark/client.js');
|
|
575
|
+
// Scope resolution — explicit task.scope wins; otherwise fall back to legacy
|
|
576
|
+
// semantics (rootMessageId present → thread, absent → chat). Restoring an
|
|
577
|
+
// older schedule without scope keeps current behaviour.
|
|
578
|
+
const scope = task.scope === 'chat'
|
|
579
|
+
? 'chat'
|
|
580
|
+
: task.scope === 'thread'
|
|
581
|
+
? 'thread'
|
|
582
|
+
: (task.rootMessageId ? 'thread' : 'chat');
|
|
583
|
+
// Decide where to route the "🕐 task started" notification and where the
|
|
584
|
+
// session conversation lands.
|
|
585
|
+
//
|
|
586
|
+
// Thread-scope (legacy and current default):
|
|
587
|
+
// - cross-thread (creator != target): notify creator's thread; deliver
|
|
588
|
+
// execution into target rootMessageId
|
|
589
|
+
// - same-thread: notify into the bound thread,
|
|
590
|
+
// which doubles as the session anchor
|
|
591
|
+
// - missing rootMessageId: fall back to a fresh top-level
|
|
592
|
+
// post in the chat (one-shot session)
|
|
593
|
+
//
|
|
594
|
+
// Chat-scope (auto-adopt / 普通群): post the start notification straight to
|
|
595
|
+
// the chat without reply_in_thread; the chat IS the session anchor.
|
|
596
|
+
let anchor;
|
|
359
597
|
let isContinuation = false;
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
598
|
+
if (scope === 'chat') {
|
|
599
|
+
// A group may have been converted from 普通群 to 话题群 after the schedule
|
|
600
|
+
// was created. In topic mode, a top-level sendMessage creates a new topic;
|
|
601
|
+
// keep scheduled continuations in the original thread when we have one.
|
|
602
|
+
const chatMode = await getChatMode(larkAppId, task.chatId, { forceRefresh: true });
|
|
603
|
+
if (chatMode === 'topic' && task.rootMessageId) {
|
|
604
|
+
try {
|
|
605
|
+
await replyMessage(larkAppId, task.rootMessageId, `🕐 定时任务「${task.name}」开始执行`, 'text', true);
|
|
606
|
+
anchor = task.rootMessageId;
|
|
607
|
+
isContinuation = true;
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
logger.warn(`[scheduler] Failed to reply in converted topic chat ${task.rootMessageId} (${err.message}); falling back to new thread`);
|
|
611
|
+
anchor = await sendMessage(larkAppId, task.chatId, `🕐 定时任务「${task.name}」开始执行`);
|
|
612
|
+
}
|
|
368
613
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
614
|
+
else if (task.creatorRootMessageId && task.creatorChatId !== task.chatId) {
|
|
615
|
+
const creatorAppId = task.creatorLarkAppId ?? larkAppId;
|
|
616
|
+
replyMessage(creatorAppId, task.creatorRootMessageId, `🕐 定时任务「${task.name}」已在目标群聊触发`, 'text', true).catch((err) => {
|
|
617
|
+
logger.warn(`[scheduler] Failed to notify creator thread ${task.creatorRootMessageId} (${err.message})`);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
// Same-chat: post the start banner to the chat as a plain message.
|
|
622
|
+
try {
|
|
623
|
+
await sendMessage(larkAppId, task.chatId, `🕐 定时任务「${task.name}」开始执行`);
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
logger.warn(`[scheduler] Failed to post start banner in chat ${task.chatId} (${err.message})`);
|
|
627
|
+
}
|
|
372
628
|
}
|
|
629
|
+
anchor = task.chatId;
|
|
630
|
+
isContinuation = !!activeSessions.get(sessionKey(anchor, larkAppId));
|
|
373
631
|
}
|
|
374
632
|
else {
|
|
375
|
-
|
|
633
|
+
// thread-scope path (existing logic)
|
|
634
|
+
const isCrossThread = !!task.creatorRootMessageId &&
|
|
635
|
+
!!task.rootMessageId &&
|
|
636
|
+
task.creatorRootMessageId !== task.rootMessageId;
|
|
637
|
+
if (isCrossThread) {
|
|
638
|
+
const creatorAppId = task.creatorLarkAppId ?? larkAppId;
|
|
639
|
+
replyMessage(creatorAppId, task.creatorRootMessageId, `🕐 定时任务「${task.name}」已在目标话题触发`, 'text', true).catch((err) => {
|
|
640
|
+
logger.warn(`[scheduler] Failed to notify creator thread ${task.creatorRootMessageId} (${err.message})`);
|
|
641
|
+
});
|
|
642
|
+
anchor = task.rootMessageId;
|
|
643
|
+
isContinuation = true;
|
|
644
|
+
}
|
|
645
|
+
else if (task.rootMessageId) {
|
|
646
|
+
try {
|
|
647
|
+
await replyMessage(larkAppId, task.rootMessageId, `🕐 定时任务「${task.name}」开始执行`, 'text', true);
|
|
648
|
+
anchor = task.rootMessageId;
|
|
649
|
+
isContinuation = true;
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
logger.warn(`[scheduler] Failed to reply in original thread ${task.rootMessageId} (${err.message}); falling back to new thread`);
|
|
653
|
+
anchor = await sendMessage(larkAppId, task.chatId, `🕐 定时任务「${task.name}」开始执行`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
anchor = await sendMessage(larkAppId, task.chatId, `🕐 定时任务「${task.name}」开始执行`);
|
|
658
|
+
}
|
|
376
659
|
}
|
|
377
660
|
refreshCliVersion(bot.config.cliId, bot.config.cliPathOverride);
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
const existing = activeSessions.get(sessionKey(threadRootId, larkAppId));
|
|
661
|
+
// Inject into a live session if one already exists at this anchor.
|
|
662
|
+
const existing = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
381
663
|
if (isContinuation && existing?.worker && !existing.worker.killed) {
|
|
382
|
-
existing
|
|
664
|
+
markSessionActivity(existing);
|
|
383
665
|
try {
|
|
384
666
|
existing.worker.send({ type: 'message', content: task.prompt });
|
|
385
667
|
logger.info(`[scheduler] Task "${task.name}" injected into live session ${existing.session.sessionId}`);
|
|
@@ -389,12 +671,19 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
389
671
|
logger.warn(`[scheduler] Failed to inject into live session (${err.message}); spawning fresh worker`);
|
|
390
672
|
}
|
|
391
673
|
}
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
674
|
+
// Spawn a fresh session bound to the chosen anchor.
|
|
675
|
+
// Thread-scope: rootMessageId = anchor. Chat-scope: rootMessageId stores the
|
|
676
|
+
// chatId-as-seed for audit (sessionAnchorId() returns chatId via scope). If a
|
|
677
|
+
// formerly chat-scope task was redirected into a converted topic chat, promote
|
|
678
|
+
// the runtime session to thread-scope so follow-up replies stay in-thread.
|
|
679
|
+
const runtimeScope = scope === 'chat' && anchor !== task.chatId ? 'thread' : scope;
|
|
680
|
+
const session = sessionStore.createSession(task.chatId, anchor, `[定时] ${task.name}`);
|
|
681
|
+
const now = Date.now();
|
|
395
682
|
session.larkAppId = larkAppId;
|
|
683
|
+
session.scope = runtimeScope;
|
|
684
|
+
session.lastMessageAt = new Date(now).toISOString();
|
|
396
685
|
sessionStore.updateSession(session);
|
|
397
|
-
messageQueue.ensureQueue(
|
|
686
|
+
messageQueue.ensureQueue(anchor);
|
|
398
687
|
const prompt = buildNewTopicPrompt(task.prompt, session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId });
|
|
399
688
|
const ds = {
|
|
400
689
|
session,
|
|
@@ -404,14 +693,15 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
404
693
|
larkAppId,
|
|
405
694
|
chatId: task.chatId,
|
|
406
695
|
chatType: task.chatType === 'p2p' ? 'p2p' : 'group',
|
|
407
|
-
|
|
696
|
+
scope: runtimeScope,
|
|
697
|
+
spawnedAt: sessionCreatedAtMs(session),
|
|
408
698
|
cliVersion: getCurrentCliVersion(),
|
|
409
|
-
lastMessageAt:
|
|
410
|
-
hasHistory: isContinuation,
|
|
699
|
+
lastMessageAt: now,
|
|
700
|
+
hasHistory: isContinuation,
|
|
411
701
|
workingDir: task.workingDir,
|
|
412
702
|
};
|
|
413
|
-
activeSessions.set(sessionKey(
|
|
703
|
+
activeSessions.set(sessionKey(anchor, larkAppId), ds);
|
|
414
704
|
forkWorker(ds, prompt);
|
|
415
|
-
logger.info(`[scheduler] Task "${task.name}" spawned (session: ${session.sessionId},
|
|
705
|
+
logger.info(`[scheduler] Task "${task.name}" spawned (session: ${session.sessionId}, scope: ${scope}, anchor: ${anchor}, continuation: ${isContinuation})`);
|
|
416
706
|
}
|
|
417
707
|
//# sourceMappingURL=session-manager.js.map
|